@ctrl-spc/cs 0.7.9 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codex-home.js +3 -6
- package/dist/panel3/prompt.js +11 -0
- package/dist/panel3/run.js +32 -9
- package/dist/panel3/show.js +5 -1
- package/dist/panel3/spawn.js +24 -13
- package/dist/panel3/tools.js +11 -7
- package/package.json +1 -1
package/dist/codex-home.js
CHANGED
|
@@ -215,12 +215,6 @@ subagentsEnabled = true, persistentPanelOwner = false) {
|
|
|
215
215
|
const lines = [
|
|
216
216
|
'# Written by CTRL+SPC for ONE codex run. Not the user\'s config; never read',
|
|
217
217
|
'# by anything but the worker this was written for.',
|
|
218
|
-
/* The Windows Desktop bundle can lag the account's server-selected default
|
|
219
|
-
model. When that default requires a newer client, every headless worker
|
|
220
|
-
exits before its first action. Pin the current broadly-supported model
|
|
221
|
-
only for that bundled runtime; other platforms keep Codex's own model
|
|
222
|
-
selection. */
|
|
223
|
-
...(runtimePlatform === 'win32' ? ['model = "gpt-5.5"', ''] : []),
|
|
224
218
|
...(persistentPanelOwner ? [
|
|
225
219
|
'sandbox_mode = "workspace-write"',
|
|
226
220
|
'',
|
|
@@ -258,6 +252,9 @@ subagentsEnabled = true, persistentPanelOwner = false) {
|
|
|
258
252
|
if (server) {
|
|
259
253
|
const url = mcpUrl(server, runTodoId);
|
|
260
254
|
lines.push('[mcp_servers.ctrl-spc]', `url = ${JSON.stringify(url)}`,
|
|
255
|
+
// A missing product tool connection must fail startup, not leave a run
|
|
256
|
+
// producing answers without its dispatch and completion tools.
|
|
257
|
+
'required = true',
|
|
261
258
|
/* See the block comment above: without this every tool call is cancelled
|
|
262
259
|
client-side before it leaves the worker. */
|
|
263
260
|
'default_tools_approval_mode = "approve"', '');
|
package/dist/panel3/prompt.js
CHANGED
|
@@ -1165,3 +1165,14 @@ function respawnPrompt(opening, brief, report, children) {
|
|
|
1165
1165
|
brief,
|
|
1166
1166
|
].join('\n');
|
|
1167
1167
|
}
|
|
1168
|
+
/** Selection belongs to each dispatch, not to the card or its parent's model. */
|
|
1169
|
+
export function modelChoiceRules(harness) {
|
|
1170
|
+
return [
|
|
1171
|
+
'MODEL AND EFFORT FOR EACH DISPATCH',
|
|
1172
|
+
`You run under ${harness}. Your dispatched children use the same harness.`,
|
|
1173
|
+
'The dispatch tool accepts optional model and effort strings. Honor the user\'s exact stated model and effort for the work they apply to, including downstream workers. Copy each requested value byte-for-byte into the dispatch argument: do not expand aliases, normalize spelling or case, or replace it with a canonical provider identifier. Carry those wishes and their scope verbatim in every applicable responsibility and boundary.',
|
|
1174
|
+
'When the user requests separate workers or assigns choices to separate parts, call dispatch for those parts. Doing their work yourself does not satisfy that request. If product tools are deferred, discover dispatch before starting; use the product ask_question and write_report tools for questions and completion.',
|
|
1175
|
+
'When the user has not specified a value, choose for this child\'s task using your knowledge of this harness. Pass your choice in the tool arguments; a sentence alone does not select it. Different workers may use different choices.',
|
|
1176
|
+
'Omitting a value chooses the harness default (or the machine-local model default), never the parent\'s model or effort. No product model catalog exists. Pass provider values unchanged; do not silently substitute after a rejection.',
|
|
1177
|
+
].join('\n');
|
|
1178
|
+
}
|
package/dist/panel3/run.js
CHANGED
|
@@ -748,6 +748,17 @@ async function setCardState(client, cardId, state) {
|
|
|
748
748
|
.select('id'), 'set the state of', `card ${cardId}`);
|
|
749
749
|
}
|
|
750
750
|
// ---------------------------------------------------------------------------
|
|
751
|
+
/** Each start reloads only this run's durable choices, including null defaults. */
|
|
752
|
+
export async function settingsForRun(client, runId) {
|
|
753
|
+
const rows = await returned(client.from('panel3_runs').select('model, effort, harness').eq('id', runId), 'read', `model and effort for run ${runId}`);
|
|
754
|
+
const row = rows[0];
|
|
755
|
+
if (!row || (row.harness !== null && row.harness !== 'claude' && row.harness !== 'codex')
|
|
756
|
+
|| (row.model !== null && typeof row.model !== 'string')
|
|
757
|
+
|| (row.effort !== null && typeof row.effort !== 'string')) {
|
|
758
|
+
throw new Error('The run has no readable harness, model and effort selection.');
|
|
759
|
+
}
|
|
760
|
+
return { model: row.model, effort: row.effort, harness: row.harness ?? undefined };
|
|
761
|
+
}
|
|
751
762
|
/**
|
|
752
763
|
* One card, from the turns the take handed over to the answer on its thread.
|
|
753
764
|
*
|
|
@@ -804,9 +815,11 @@ async function answerCard(client, tools, machineId, cardId, turns) {
|
|
|
804
815
|
no rules" — that is a different project than the one the person is on. So it
|
|
805
816
|
joins the three reads above inside this ending rather than beside it. */
|
|
806
817
|
let rules;
|
|
818
|
+
let settings;
|
|
807
819
|
try {
|
|
808
820
|
brief = briefFor(turns[0].card_title, turns, await receiptsFor(client, cardId), await attachmentsFor(client, cardId), await codebasesForRun(client, runId));
|
|
809
821
|
rules = await standingRulesFor(client, runId);
|
|
822
|
+
settings = await settingsForRun(client, runId);
|
|
810
823
|
where = await workingDirectory(client, runId, LEVEL, false);
|
|
811
824
|
}
|
|
812
825
|
catch (error) {
|
|
@@ -823,7 +836,7 @@ async function answerCard(client, tools, machineId, cardId, turns) {
|
|
|
823
836
|
/* AN EMPTY DIRECTORY OF THE USER'S OWN, because level 1 has no code tool to
|
|
824
837
|
use a real one with, and the daemon's inherited cwd under a launchd login
|
|
825
838
|
item is the filesystem root. */
|
|
826
|
-
const started = startAgent(withStandingRules(rules, brief, where.block), LEVEL, tools.urlFor(runId), where.cwd);
|
|
839
|
+
const started = startAgent(withStandingRules(rules, brief, where.block), LEVEL, tools.urlFor(runId), where.cwd, undefined, settings);
|
|
827
840
|
out(`run ${runId} level ${LEVEL} ${started.pid ? `pid ${started.pid}` : 'no process'}`);
|
|
828
841
|
try {
|
|
829
842
|
/* THE BRIEF, NOT WHAT THE PROCESS WAS HANDED. The rules are current at the
|
|
@@ -1162,14 +1175,16 @@ async function workingDirectory(client, runId, level, isOwner, knownCodebase) {
|
|
|
1162
1175
|
* Then the row, then the process, then the pid — constraint 8, in the only order
|
|
1163
1176
|
* that satisfies it.
|
|
1164
1177
|
*/
|
|
1165
|
-
async function startChild(client, tools, machineId, parentRunId, brief, codebase, parentProcessToken) {
|
|
1178
|
+
async function startChild(client, tools, machineId, parentRunId, brief, codebase, parentProcessToken, choice = {}) {
|
|
1166
1179
|
const { data, error } = await client.rpc('panel3_dispatch', {
|
|
1167
1180
|
p_parent_run_id: parentRunId,
|
|
1168
1181
|
p_brief: brief,
|
|
1169
1182
|
p_machine_id: machineId,
|
|
1170
1183
|
p_codebase_id: codebase?.id ?? null,
|
|
1171
1184
|
p_codebase_label: codebase?.name ?? null,
|
|
1172
|
-
|
|
1185
|
+
p_process_token: parentProcessToken ?? null,
|
|
1186
|
+
p_model: choice.model ?? null,
|
|
1187
|
+
p_effort: choice.effort ?? null,
|
|
1173
1188
|
});
|
|
1174
1189
|
if (error)
|
|
1175
1190
|
throw new Error(`could not start an agent under run ${parentRunId}: ${readableWriteError(error.message)}`);
|
|
@@ -1200,12 +1215,14 @@ async function startChild(client, tools, machineId, parentRunId, brief, codebase
|
|
|
1200
1215
|
exists (`panel3_dispatch` wrote it above) and it carries this child's
|
|
1201
1216
|
codebase, which is what decides which codebase-scoped rules it is under. */
|
|
1202
1217
|
let rules;
|
|
1218
|
+
let settings;
|
|
1203
1219
|
try {
|
|
1204
1220
|
where = await workingDirectory(client, row.run_id, level, level === 2, codebase);
|
|
1205
1221
|
prompt = level === 2
|
|
1206
1222
|
? await initialOwnerPrompt(client, row.run_card_id, parentRunId, brief)
|
|
1207
1223
|
: brief;
|
|
1208
1224
|
rules = await standingRulesFor(client, row.run_id);
|
|
1225
|
+
settings = await settingsForRun(client, row.run_id);
|
|
1209
1226
|
/* Inside this site's own try, for `standingRulesFor`'s reason: a failure to
|
|
1210
1227
|
assemble what the agent needs ends the run the way this path already ends
|
|
1211
1228
|
runs, rather than starting a process that is missing it. */
|
|
@@ -1218,7 +1235,7 @@ async function startChild(client, tools, machineId, parentRunId, brief, codebase
|
|
|
1218
1235
|
}
|
|
1219
1236
|
const processToken = row.process_token ?? undefined;
|
|
1220
1237
|
const isOwner = level === 2;
|
|
1221
|
-
const started = startAgent(withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id, processToken), where.cwd, isOwner ? { ownerId: row.run_id } : undefined);
|
|
1238
|
+
const started = startAgent(withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id, processToken), where.cwd, isOwner ? { ownerId: row.run_id } : undefined, settings);
|
|
1222
1239
|
if (started.pid === null) {
|
|
1223
1240
|
/* THERE IS A ROW AND THERE IS NO PROCESS, which is the one shape the record
|
|
1224
1241
|
must never be left in quietly. The answer is already settled — nothing ran
|
|
@@ -1688,9 +1705,11 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1688
1705
|
is a `returned()` message naming tables and columns, which carries no local
|
|
1689
1706
|
path and is therefore shareable. */
|
|
1690
1707
|
let rules;
|
|
1708
|
+
let settings;
|
|
1691
1709
|
let pictures;
|
|
1692
1710
|
try {
|
|
1693
1711
|
rules = await standingRulesFor(client, runId);
|
|
1712
|
+
settings = await settingsForRun(client, runId);
|
|
1694
1713
|
/* ═══ WRITTEN AGAIN ON EVERY START, LIKE THE RULES. ═══ A resumed process is
|
|
1695
1714
|
a NEW process with a new copy of the working directory, so the files a
|
|
1696
1715
|
previous one was handed are not there any more, and the stored brief this
|
|
@@ -1709,7 +1728,7 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1709
1728
|
still running. See `retryPrompt`. */
|
|
1710
1729
|
withStandingRules(rules, afterPid === null
|
|
1711
1730
|
? resumePrompt(claimed.run_brief, claimed.run_report, children)
|
|
1712
|
-
: retryPrompt(claimed.run_brief, claimed.run_report, children), where.block, [], pictures), level, tools.urlFor(runId), where.cwd);
|
|
1731
|
+
: retryPrompt(claimed.run_brief, claimed.run_report, children), where.block, [], pictures), level, tools.urlFor(runId), where.cwd, undefined, settings);
|
|
1713
1732
|
if (started.pid === null) {
|
|
1714
1733
|
/* THE CLAIM HAPPENED AND NO PROCESS DID, which is the one shape the record
|
|
1715
1734
|
must never be left in quietly. Same handling as a dispatch that could not
|
|
@@ -1816,9 +1835,11 @@ async function startRearmed(client, tools, machineId, row) {
|
|
|
1816
1835
|
same way: the re-arm's claim has already happened, so a failure here ends
|
|
1817
1836
|
the run with its reason rather than leaving it claimed with no process. */
|
|
1818
1837
|
let rules;
|
|
1838
|
+
let settings;
|
|
1819
1839
|
let pictures;
|
|
1820
1840
|
try {
|
|
1821
1841
|
rules = await standingRulesFor(client, row.run_id);
|
|
1842
|
+
settings = await settingsForRun(client, row.run_id);
|
|
1822
1843
|
pictures = await picturesOnDisk(client, row.run_card_id, where, level);
|
|
1823
1844
|
}
|
|
1824
1845
|
catch (error) {
|
|
@@ -1826,7 +1847,7 @@ async function startRearmed(client, tools, machineId, row) {
|
|
|
1826
1847
|
await endRun(client, level, row.run_id, row.run_card_id, why);
|
|
1827
1848
|
throw new Error(`NO AGENT IS RUNNING: ${why}`);
|
|
1828
1849
|
}
|
|
1829
|
-
const started = startAgent(withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id), where.cwd);
|
|
1850
|
+
const started = startAgent(withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id), where.cwd, undefined, settings);
|
|
1830
1851
|
if (started.pid === null) {
|
|
1831
1852
|
const answer = await started.answered;
|
|
1832
1853
|
const reason = answer.ok ? 'the process ended before it could be identified' : answer.reason;
|
|
@@ -2064,6 +2085,7 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2064
2085
|
says what the product DID with the person's answer, and the landing needs
|
|
2065
2086
|
the card's copy. Nothing in the prompt depended on it before. */
|
|
2066
2087
|
let rules;
|
|
2088
|
+
let settings;
|
|
2067
2089
|
/* THE CARD'S COPY IS MADE IN THE SAME WINDOW AND UNDER THE SAME ENDING, for
|
|
2068
2090
|
the reason the check above gives: it writes, so it happens after the claim,
|
|
2069
2091
|
and a failure to make it is an activation that ends rather than one that
|
|
@@ -2087,6 +2109,7 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2087
2109
|
try {
|
|
2088
2110
|
where = await ownerDirectory(client, candidate);
|
|
2089
2111
|
rules = await standingRulesFor(client, runId);
|
|
2112
|
+
settings = await settingsForRun(client, runId);
|
|
2090
2113
|
attached = whatWasAttached((await attachmentsFor(client, claimed.run_card_id))
|
|
2091
2114
|
.filter((line) => !line.startsWith('codebase ')));
|
|
2092
2115
|
/* ═══ THE OWNER'S OWN ACTIVATION, WHICH IS WHERE MOST PICTURES ARRIVE. ═══
|
|
@@ -2119,7 +2142,7 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2119
2142
|
are not the same event and `prompt.ts` exists to stop an agent being
|
|
2120
2143
|
told an untrue reason for its own restart. */
|
|
2121
2144
|
afterPid !== null, landing);
|
|
2122
|
-
const started = startAgent(withStandingRules(rules, prompt, where.block, attached, pictures), 2, tools.urlFor(runId, claimed.process_token), where.cwd, { ownerId: runId, ...(resumeSessionId ? { resumeSessionId } : {}) });
|
|
2145
|
+
const started = startAgent(withStandingRules(rules, prompt, where.block, attached, pictures), 2, tools.urlFor(runId, claimed.process_token), where.cwd, { ownerId: runId, ...(resumeSessionId ? { resumeSessionId } : {}) }, settings);
|
|
2123
2146
|
if (started.pid === null) {
|
|
2124
2147
|
const answer = await started.answered;
|
|
2125
2148
|
const reason = answer.ok ? 'the process ended before it could be identified' : answer.reason;
|
|
@@ -3100,8 +3123,8 @@ export async function run(args, injected) {
|
|
|
3100
3123
|
`tools` is referenced inside the callback it is being given, which is safe
|
|
3101
3124
|
for the plain reason that the callback can only run once a request has
|
|
3102
3125
|
arrived at a server that by then exists. */
|
|
3103
|
-
const tools = await startToolsServer(current(), async (parentRunId, brief, codebase, processToken) => {
|
|
3104
|
-
const child = await startChild(current(), tools, machineId, parentRunId, brief, codebase, processToken);
|
|
3126
|
+
const tools = await startToolsServer(current(), async (parentRunId, brief, codebase, processToken, choice) => {
|
|
3127
|
+
const child = await startChild(current(), tools, machineId, parentRunId, brief, codebase, processToken, choice);
|
|
3105
3128
|
hold(child.runId, child.settled);
|
|
3106
3129
|
return { runId: child.runId };
|
|
3107
3130
|
});
|
package/dist/panel3/show.js
CHANGED
|
@@ -83,7 +83,7 @@ const read = (query, what) => returned(query, 'read', what);
|
|
|
83
83
|
const CARD_COLUMNS = 'id, project_id, title, state, created_at, archived_at';
|
|
84
84
|
const TURN_COLUMNS = 'id, card_id, role, body, created_at, addressed_at, run_id';
|
|
85
85
|
const RUN_COLUMNS = 'id, card_id, codebase_id, branch, parent_run_id, level, state, brief, report, failed_because, '
|
|
86
|
-
+ 'activity, machine_id, pid, started_at, resumed_at, read_back_at, ended_at';
|
|
86
|
+
+ 'activity, machine_id, pid, started_at, resumed_at, read_back_at, ended_at, model, effort';
|
|
87
87
|
const ASK_COLUMNS = `id, card_id, run_id, pending_run_id, answered_at, delivered_at, created_at, ${ASK_CONTENT_COLUMNS}`;
|
|
88
88
|
const OUTPUT_COLUMNS = 'id, card_id, run_id, kind, ref_id, label, created_at';
|
|
89
89
|
const ATTACHMENT_COLUMNS = 'id, card_id, kind, ref_id, label, created_at';
|
|
@@ -1125,6 +1125,10 @@ async function showRun(client, run) {
|
|
|
1125
1125
|
out(`RUN ${run.id}`);
|
|
1126
1126
|
out(` card ${run.card_id}${card ? ` ${card.title}` : ' (card not readable)'}`);
|
|
1127
1127
|
out(` level ${run.level}`);
|
|
1128
|
+
if (run.model != null)
|
|
1129
|
+
out(` model ${run.model}`);
|
|
1130
|
+
if (run.effort != null)
|
|
1131
|
+
out(` effort ${run.effort}`);
|
|
1128
1132
|
out(` state ${run.state}${isLive(run) ? ' (live)' : ''}`);
|
|
1129
1133
|
out(` parent ${run.parent_run_id ?? 'none, dispatched by the daemon'}`);
|
|
1130
1134
|
out(` machine ${run.machine_id}${run.pid ? ` pid ${run.pid}` : ' no pid recorded'}`);
|
package/dist/panel3/spawn.js
CHANGED
|
@@ -124,6 +124,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
124
124
|
import { agentPath } from '../agents.js';
|
|
125
125
|
import { ensureCodexRunHome, ensurePanel3CodexOwnerHome, removeCodexRunHome, } from '../codex-home.js';
|
|
126
126
|
import { windowsSafeSpawn } from '../win-shell.js';
|
|
127
|
+
import { modelChoiceRules } from './prompt.js';
|
|
127
128
|
const AGENT_VAR = 'CTRL_SPC_V3_AGENT';
|
|
128
129
|
/**
|
|
129
130
|
* The harness named on this machine, or the reason the name is not one.
|
|
@@ -169,9 +170,9 @@ const allowedTools = (level) => [`mcp__${SERVER}__*`, ...(level === 1 ? [] : COD
|
|
|
169
170
|
* checked without starting a process — which is how the allowlist is proved,
|
|
170
171
|
* and how it stays provable after this task.
|
|
171
172
|
*/
|
|
172
|
-
export function agentArgs(level, toolsUrl, agent = harness(), platform = process.platform, ownerSession) {
|
|
173
|
+
export function agentArgs(level, toolsUrl, agent = harness(), platform = process.platform, ownerSession, choice = {}) {
|
|
173
174
|
if (agent === 'codex')
|
|
174
|
-
return codexArgs(level, toolsUrl, platform, ownerSession);
|
|
175
|
+
return codexArgs(level, toolsUrl, platform, ownerSession, choice);
|
|
175
176
|
const session = ownerSession
|
|
176
177
|
? ownerSession.resumeSessionId
|
|
177
178
|
? ['--resume', ownerSession.resumeSessionId]
|
|
@@ -187,6 +188,8 @@ export function agentArgs(level, toolsUrl, agent = harness(), platform = process
|
|
|
187
188
|
'--tools', builtIns(level),
|
|
188
189
|
'--allowedTools', allowedTools(level),
|
|
189
190
|
...session,
|
|
191
|
+
...(choice.model == null ? [] : ['--model', choice.model]),
|
|
192
|
+
...(choice.effort == null ? [] : ['--effort', choice.effort]),
|
|
190
193
|
/* STATED WHERE THERE IS SOMETHING TO STATE. `acceptEdits` grants file edits
|
|
191
194
|
to a headless process with nobody at a prompt to approve them, and it is
|
|
192
195
|
passed only to the levels that have a file tool to use it with: at level 1
|
|
@@ -221,7 +224,7 @@ export function agentArgs(level, toolsUrl, agent = harness(), platform = process
|
|
|
221
224
|
* `--json` makes the answer an `agent_message` item read out of the stream
|
|
222
225
|
* rather than whatever prose happened to reach stdout (see `codexAnswer`).
|
|
223
226
|
*/
|
|
224
|
-
function codexArgs(level, toolsUrl, platform, ownerSession) {
|
|
227
|
+
function codexArgs(level, toolsUrl, platform, ownerSession, choice = {}) {
|
|
225
228
|
const resume = ownerSession?.resumeSessionId;
|
|
226
229
|
return [
|
|
227
230
|
'exec',
|
|
@@ -229,6 +232,8 @@ function codexArgs(level, toolsUrl, platform, ownerSession) {
|
|
|
229
232
|
// The prompt arrives on stdin, exactly as it does for claude: `codex exec`
|
|
230
233
|
// reads it from there when no prompt argument is given.
|
|
231
234
|
'--json',
|
|
235
|
+
...(choice.model == null ? [] : ['-m', choice.model]),
|
|
236
|
+
...(choice.effort == null ? [] : ['-c', `model_reasoning_effort=${JSON.stringify(choice.effort)}`]),
|
|
232
237
|
// The level 1 scratch directory is not a repository, and neither need a
|
|
233
238
|
// working copy be.
|
|
234
239
|
'--skip-git-repo-check',
|
|
@@ -241,13 +246,13 @@ function codexArgs(level, toolsUrl, platform, ownerSession) {
|
|
|
241
246
|
proven per-run home below. */
|
|
242
247
|
...(platform === 'win32' ? [
|
|
243
248
|
'--ignore-user-config',
|
|
244
|
-
'-c', 'model="gpt-5.5"',
|
|
245
249
|
'-c', 'features.apps=false',
|
|
246
250
|
// The same closing `codex-home.ts` writes into the per-run config, and
|
|
247
251
|
// for the same measured reason: `features.multi_agent=false` parses and
|
|
248
252
|
// leaves the tool reachable.
|
|
249
253
|
'-c', 'agents.enabled=false',
|
|
250
254
|
'-c', `mcp_servers.${SERVER}.url=${JSON.stringify(toolsUrl)}`,
|
|
255
|
+
'-c', `mcp_servers.${SERVER}.required=true`,
|
|
251
256
|
'-c', `mcp_servers.${SERVER}.default_tools_approval_mode="approve"`,
|
|
252
257
|
'-c', 'windows.sandbox="unelevated"',
|
|
253
258
|
'-c', 'windows.sandbox_private_desktop=false',
|
|
@@ -291,7 +296,7 @@ function runKey(toolsUrl) {
|
|
|
291
296
|
* is the forbidden state ux.md is about, so the failure wins wherever both are
|
|
292
297
|
* present.
|
|
293
298
|
*/
|
|
294
|
-
export function codexAnswer(stdout) {
|
|
299
|
+
export function codexAnswer(stdout, exitCode = 0, stderr = '') {
|
|
295
300
|
let text = null;
|
|
296
301
|
let failure = null;
|
|
297
302
|
for (const line of stdout.split('\n')) {
|
|
@@ -319,6 +324,9 @@ export function codexAnswer(stdout) {
|
|
|
319
324
|
if (failure !== null) {
|
|
320
325
|
return { ok: false, reason: `codex could not finish the turn${failure ? `: ${failure}` : ''}` };
|
|
321
326
|
}
|
|
327
|
+
if (exitCode !== 0) {
|
|
328
|
+
return { ok: false, reason: `codex exited ${exitCode}${tail(stderr) || tail(stdout)}` };
|
|
329
|
+
}
|
|
322
330
|
if (text === null || text.trim() === '') {
|
|
323
331
|
return { ok: false, reason: 'codex exited 0 without saying anything to the person' };
|
|
324
332
|
}
|
|
@@ -377,7 +385,7 @@ function tail(text, chars = 500) {
|
|
|
377
385
|
* process still alive" after the daemon that started it has been killed. So the
|
|
378
386
|
* caller gets the pid immediately, writes it, and then waits.
|
|
379
387
|
*/
|
|
380
|
-
export function startAgent(prompt, level, toolsUrl, cwd, ownerSession) {
|
|
388
|
+
export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings = {}) {
|
|
381
389
|
const failed = (reason) => ({
|
|
382
390
|
pid: null,
|
|
383
391
|
session: Promise.resolve(null),
|
|
@@ -385,7 +393,7 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession) {
|
|
|
385
393
|
});
|
|
386
394
|
let agent;
|
|
387
395
|
try {
|
|
388
|
-
agent = harness();
|
|
396
|
+
agent = settings.harness ?? harness();
|
|
389
397
|
}
|
|
390
398
|
catch (err) {
|
|
391
399
|
// A machine configured for a harness this build has never heard of. See
|
|
@@ -395,7 +403,10 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession) {
|
|
|
395
403
|
const launchedOwnerSession = ownerSession && !ownerSession.resumeSessionId
|
|
396
404
|
? { ...ownerSession, freshSessionId: randomUUID() }
|
|
397
405
|
: ownerSession;
|
|
398
|
-
|
|
406
|
+
// A local default applies only to an omitted choice; explicit run values always win.
|
|
407
|
+
const choice = { ...settings, model: settings.model ?? process.env[`CTRL_SPC_V3_${agent.toUpperCase()}_DEFAULT_MODEL`] };
|
|
408
|
+
const ARGS = agentArgs(level, toolsUrl, agent, process.platform, launchedOwnerSession, choice);
|
|
409
|
+
prompt = `${modelChoiceRules(agent)}\n\n${prompt}`;
|
|
399
410
|
const bin = agentPath(agent);
|
|
400
411
|
if (!bin) {
|
|
401
412
|
return failed(`${agent} is not installed on this machine`);
|
|
@@ -531,6 +542,11 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession) {
|
|
|
531
542
|
respawn behind it is refused. Naming the signal is the whole fix. */
|
|
532
543
|
finish({ ok: false, reason: `${agent} was ended by ${signal}${tail(stderr) || tail(stdout)}` });
|
|
533
544
|
}
|
|
545
|
+
else if (agent === 'codex') {
|
|
546
|
+
// Startup failures can put the real error in JSON stdout and only
|
|
547
|
+
// 'Reading prompt from stdin' on stderr, including on non-zero exits.
|
|
548
|
+
finish(codexAnswer(stdout, code ?? 1, stderr));
|
|
549
|
+
}
|
|
534
550
|
else if (code !== 0) {
|
|
535
551
|
/* STDOUT WHEN STDERR IS EMPTY, because `claude -p` prints its own
|
|
536
552
|
failure on stdout and exits non-zero having written nothing to
|
|
@@ -541,11 +557,6 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession) {
|
|
|
541
557
|
else if (stdout.trim() === '') {
|
|
542
558
|
finish({ ok: false, reason: `${agent} exited 0 and said nothing${tail(stderr)}` });
|
|
543
559
|
}
|
|
544
|
-
else if (agent === 'codex') {
|
|
545
|
-
/* The stream, not the buffer. See `codexAnswer`: stdout here is
|
|
546
|
-
protocol, and a failed turn that exited 0 is still a failure. */
|
|
547
|
-
finish(codexAnswer(stdout));
|
|
548
|
-
}
|
|
549
560
|
else {
|
|
550
561
|
const text = stdout.trim();
|
|
551
562
|
finish({
|
package/dist/panel3/tools.js
CHANGED
|
@@ -1833,11 +1833,11 @@ const TOOLS = [
|
|
|
1833
1833
|
const agents = await rows(client.from('panel3_runs')
|
|
1834
1834
|
// `resumed_at` is read because "how long it has been going" is about
|
|
1835
1835
|
// the attempt that is running now. See `elapsed`.
|
|
1836
|
-
.select('id, level, parent_run_id, state, activity, started_at, resumed_at, ended_at')
|
|
1836
|
+
.select('id, level, parent_run_id, state, activity, started_at, resumed_at, ended_at, model, effort')
|
|
1837
1837
|
.eq('card_id', cardId).order('started_at'), 'read', 'the agents on this card');
|
|
1838
1838
|
return listed(agents.map((a) => {
|
|
1839
1839
|
const live = a.state === 'running' && !a.ended_at;
|
|
1840
|
-
return line(a.id, `L${a.level}`, a.parent_run_id ? `parent ${a.parent_run_id}` : 'no parent, dispatched by the daemon', live ? 'still running' : a.state, a.activity ?? null, live
|
|
1840
|
+
return line(a.id, `L${a.level}`, a.parent_run_id ? `parent ${a.parent_run_id}` : 'no parent, dispatched by the daemon', live ? 'still running' : a.state, a.activity ?? null, a.model == null ? null : `model ${a.model}`, a.effort == null ? null : `effort ${a.effort}`, live
|
|
1841
1841
|
? `going ${elapsed(a.started_at, null, a.resumed_at)}`
|
|
1842
1842
|
: `went ${elapsed(a.started_at, a.ended_at, a.resumed_at)}`);
|
|
1843
1843
|
}), 'Nothing has run on this card.');
|
|
@@ -1851,9 +1851,11 @@ const TOOLS = [
|
|
|
1851
1851
|
+ 'in your prompt; this is here for when you need to re-read them mid-run.',
|
|
1852
1852
|
input: {},
|
|
1853
1853
|
handler: async ({ client, runId }) => {
|
|
1854
|
-
const run = await only(client.from('panel3_runs').select('level, brief, report, activity').eq('id', runId), 'read', 'your own run');
|
|
1854
|
+
const run = await only(client.from('panel3_runs').select('level, brief, report, activity, model, effort').eq('id', runId), 'read', 'your own run');
|
|
1855
1855
|
return [
|
|
1856
1856
|
`You are a level ${run.level} agent.`,
|
|
1857
|
+
...(run.model == null ? [] : [`MODEL ${run.model}`]),
|
|
1858
|
+
...(run.effort == null ? [] : [`EFFORT ${run.effort}`]),
|
|
1857
1859
|
'',
|
|
1858
1860
|
'BRIEF, written when you were dispatched and never changed',
|
|
1859
1861
|
run.brief,
|
|
@@ -1878,9 +1880,9 @@ const TOOLS = [
|
|
|
1878
1880
|
+ 'dispatch the same work twice.',
|
|
1879
1881
|
input: {},
|
|
1880
1882
|
handler: async ({ client, runId }) => {
|
|
1881
|
-
const children = await rows(client.from('panel3_runs').select('id, level, state, activity, started_at, ended_at')
|
|
1883
|
+
const children = await rows(client.from('panel3_runs').select('id, level, state, activity, started_at, ended_at, model, effort')
|
|
1882
1884
|
.eq('parent_run_id', runId).order('started_at'), 'read', 'the runs you dispatched');
|
|
1883
|
-
return listed(children.map((c) => line(c.id, `L${c.level}`, c.state === 'running' && !c.ended_at ? 'still running' : c.state, c.activity ?? null)), 'You have dispatched nothing.');
|
|
1885
|
+
return listed(children.map((c) => line(c.id, `L${c.level}`, c.state === 'running' && !c.ended_at ? 'still running' : c.state, c.activity ?? null, c.model == null ? null : `model ${c.model}`, c.effort == null ? null : `effort ${c.effort}`)), 'You have dispatched nothing.');
|
|
1884
1886
|
},
|
|
1885
1887
|
},
|
|
1886
1888
|
{
|
|
@@ -2894,6 +2896,8 @@ const TOOLS = [
|
|
|
2894
2896
|
+ 'context: what to find out or change, and in which part of the codebase.'),
|
|
2895
2897
|
boundary: z.string().min(1).describe('What it must not touch, and where its work stops.'),
|
|
2896
2898
|
work_item_id: z.string().optional().describe('The work item it is working, if there is one.'),
|
|
2899
|
+
model: z.string().min(1).optional().describe('Exact model for this child. Copy the user\'s applicable value verbatim, including aliases; never expand or normalize it. Otherwise choose for the task. Omit for the harness or machine default, never parent inheritance.'),
|
|
2900
|
+
effort: z.string().min(1).optional().describe('Exact effort for this child. Honor the user\'s applicable wish; otherwise choose independently. Passed unchanged to the harness.'),
|
|
2897
2901
|
work_name: z.string().optional().describe(`A few words, ${WORK_NAME_WORDS} at most, naming the WORK this conversation is doing, such `
|
|
2898
2902
|
+ 'as "Fix the sign-out checklist bug". Pass it when the conversation has NO work item '
|
|
2899
2903
|
+ 'attached: the conversation is called this from now on, and the branch the work goes on is '
|
|
@@ -2901,7 +2905,7 @@ const TOOLS = [
|
|
|
2901
2905
|
+ 'attached, because that item is already the name.'),
|
|
2902
2906
|
},
|
|
2903
2907
|
handler: async (caller, args) => {
|
|
2904
|
-
const { codebase_id, responsibility, boundary, work_item_id, work_name } = args;
|
|
2908
|
+
const { codebase_id, responsibility, boundary, work_item_id, work_name, model, effort } = args;
|
|
2905
2909
|
if (caller.level === 2 && !codebase_id) {
|
|
2906
2910
|
throw new Error('A worker must be attached to a registered project codebase.');
|
|
2907
2911
|
}
|
|
@@ -2964,7 +2968,7 @@ const TOOLS = [
|
|
|
2964
2968
|
const attachments = attached.map(attachmentLine);
|
|
2965
2969
|
const { runId } = await caller.dispatch(caller.runId, workBrief(childLevel, responsibility, boundary, work_item_id, attachments, codebase === null ? undefined : {
|
|
2966
2970
|
id: codebase.id, name: codebase.name, identity: codebase.gitRemoteUrl,
|
|
2967
|
-
}), codebase, caller.processToken);
|
|
2971
|
+
}), codebase, caller.processToken, { model, effort });
|
|
2968
2972
|
/* ═══ WHERE ITS ANSWER GOES DEPENDS ON WHICH LEVEL THIS IS, AND THAT IS
|
|
2969
2973
|
KNOWN HERE RATHER THAN GUESSED. ═══ The description above cannot say it,
|
|
2970
2974
|
because it is registered once for both levels that hold the tool; this
|
package/package.json
CHANGED