Repository navigation
Expand file tree
/
Copy pathagents.mjs
More file actions
188 lines (182 loc) · 7.04 KB
/
Copy pathagents.mjs
File metadata and controls
188 lines (182 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
* Which coding agent runs the AI steps.
*
* WHAT THIS IS FOR
*
* Two steps here hand a prompt to an agent and let it write files: drafting a
* script, and generating the scene HTML for a composition. Both shelled out to
* `claude` with the argv written inline at the call site, twice, identically.
*
* That hardcodes two things that are really one decision — WHICH agent, and
* therefore whose account and whose bill. `claude` runs on a Claude Code
* subscription, which is the right default and is not free.
*
* Pi (https://pi.dev) is the same shape of tool: a local agent harness driving a
* model, with a non-interactive mode, a JSON event stream and an approve flag.
* It is NOT an inference provider — it routes to 15+ of them — so it does not
* reduce cost by itself. It reduces cost by letting the same work run against a
* cheaper model than Claude.
*
* STATUS: THE PI PATH IS A STUB AND HAS NOT BEEN RUN.
*
* The flags below come from Pi's CLI reference, not from a run against a real
* install, and nothing here has produced a script or a scene. It is wired up so
* that trying it is a config change rather than a patch, and left switched off
* so nobody discovers it by having a render fail. Before turning it on, see
* docs/AGENTS.md — the open questions are listed there and they are not
* cosmetic.
*
* WHY A MODULE RATHER THAN AN IF AT EACH CALL SITE
*
* The two call sites drifted once already in the direction that matters: the
* comment explaining `--output-format stream-json` is copied verbatim in both,
* which is what a shared decision looks like just before it stops being shared.
*/
/**
* The agents this app knows how to start.
*
* `bin` must also be in lib/jobs.mjs BINARIES, or the job runner refuses it —
* deliberately, because that allowlist is the thing standing between a prompt
* and an arbitrary process.
*/
export const AGENTS = {
claude: {
id: "claude",
label: "Claude Code · Fable 5",
bin: "claude",
/*
* Studio's work is deliberately long-horizon: inspect several recordings,
* compare their evidence against a script, and leave a reviewable edit.
* Do not quietly inherit whichever Claude Code model happened to be selected
* in a terminal. Every Studio agent run should make the same explicit choice.
*/
model: "claude-fable-5",
/** Whose bill this lands on, said plainly because it is the reason to change it. */
billing: "your Claude Code subscription",
ready: true,
/**
* stream-json, not the default text output.
*
* `claude -p` in text mode prints one blob when it finishes, so a long
* render showed an empty Console for minutes and looked hung. stream-json
* emits an event per step; --verbose is required alongside it. The Studio
* renders those events rather than showing raw NDJSON.
*/
args: (prompt, { additionalDirectories = [] } = {}) => [
"-p",
prompt,
"--model",
"claude-fable-5",
...(additionalDirectories.length ? ["--add-dir", ...additionalDirectories] : []),
"--permission-mode",
"acceptEdits",
"--output-format",
"stream-json",
"--verbose",
],
/** How the Console should read this agent's stdout. */
stream: "claude-stream-json",
},
pi: {
id: "pi",
label: "Pi (unverified)",
bin: "pi",
billing: "whichever provider Pi is configured for",
/*
* Not ready, and that is a claim about testing rather than about Pi.
*
* `ready: false` keeps it out of the default and makes the UI say so. It
* flips to true when someone has run both steps end to end and a scene
* generated this way has actually rendered.
*/
ready: false,
/*
* Mapped from Pi's CLI reference:
*
* claude -p <prompt> → pi -p <prompt>
* claude --permission-mode acceptEdits → pi --approve
* claude --output-format stream-json → pi --mode json
* --verbose
*
* Provider and model are never frozen into this file. By default Pi
* resolves its own configured default, so the choice of model — which is
* the entire cost question — stays with whoever set Pi up. The Agents page
* can point it at the local oMLX server or at Groq instead, and only then
* are --provider and --model passed.
*/
args: (prompt, { piProvider = "default", piModel = "", additionalDirectories = [] } = {}) => [
"-p",
prompt,
"--approve",
"--mode",
"json",
// The Studio's skill folders (hyperframes-brand, video-from-script…).
// Pi finds ~/.agents/skills on its own, but not these: without them a
// Groq composition was built without the brand rules Claude gets.
...additionalDirectories.flatMap((directory) => ["--skill", directory]),
...(piProvider === "default" ? [] : ["--provider", piProvider]),
...(piModel ? ["--model", piModel] : []),
],
stream: "pi-json",
},
codex: {
id: "codex",
label: "Codex CLI",
bin: "codex",
billing: "your Codex or OpenAI account",
ready: false,
args: (prompt, { model = "", provider = "openai", baseUrl, contextWindow = "", sandbox = "workspace-write", additionalDirectories = [] } = {}) => [
"exec",
// Studio jobs run in a project's own folders, which are not git repos,
// and `codex exec` refuses those outright without this.
"--skip-git-repo-check",
"--json",
"--sandbox",
sandbox,
...(additionalDirectories.flatMap((directory) => ["--add-dir", directory])),
...(provider === "omlx" ? [
"-c", 'model_provider="omlx"',
"-c", 'model_providers.omlx.name="oMLX"',
"-c", `model_providers.omlx.base_url=${JSON.stringify(baseUrl)}`,
"-c", 'model_providers.omlx.env_key="OMLX_API_KEY"',
] : []),
...(contextWindow ? ["-c", `model_context_window=${Number(contextWindow)}`] : []),
...(model ? ["--model", model] : []),
prompt,
],
stream: "codex-json",
},
};
/** The agent used when nothing has chosen one. */
export const DEFAULT_AGENT = "claude";
/**
* Resolve a stored setting to an agent, refusing to guess.
*
* An unknown id falls back to the default rather than throwing: a settings file
* naming an agent this build does not have should degrade to a working render,
* not to a broken app. An agent that is present but not `ready` is only used
* when it was asked for explicitly, which is the point of the flag.
*/
export function agentFor(id) {
const want = AGENTS[String(id ?? "")];
return want ?? AGENTS[DEFAULT_AGENT];
}
/**
* The step this agent runs, in the shape lib/jobs.mjs expects.
*
* One builder, so the two call sites cannot drift again.
*/
export function agentStep(id, { prompt, cwd, label, additionalDirectories, ...settings }) {
const agent = agentFor(id);
return {
label: label ?? `${agent.id} · ${cwd}`,
bin: agent.bin,
args: agent.args(prompt, { additionalDirectories, ...settings }),
cwd,
// Codex and Pi can need a key the Studio holds; the job runner adds it to
// the environment at spawn time so it never appears in the argv.
...(agent.id === "codex" || agent.id === "pi" ? { agentId: agent.id, managedAgent: true } : {}),
/** Read by the Console to decide how to render this job's output. */
stream: agent.stream,
};
}