@meistrari/agent-core 0.2.0-experimental.6 → 0.2.0-experimental.8
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/README.md +7 -0
- package/docs/diagrams/model-effort-change.png +0 -0
- package/docs/model-effort-changes.md +60 -0
- package/package.json +1 -1
- package/src/agents/agent-run.ts +2 -0
- package/src/agents/claude/claude-code-process.ts +12 -6
- package/src/agents/claude/claude-provider.ts +27 -7
- package/src/agents/claude/claude-run.ts +61 -21
- package/src/agents/claude/claude-steered-notifications.ts +2 -2
- package/src/agents/codex/codex-json-rpc-client.ts +42 -6
- package/src/agents/codex/codex-provider.ts +19 -5
- package/src/agents/codex/codex-run.ts +38 -19
- package/src/agents/input-attachment-preparation.ts +4 -2
- package/src/agents/materialized-input-attachment.ts +2 -0
- package/src/agents/model-rebinding-run.ts +63 -13
- package/src/protocol/agent-command.ts +10 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +19 -3
- package/src/supervisor/persistence/supervisor-store.ts +29 -0
- package/src/supervisor/provider-factory.ts +2 -1
- package/src/supervisor/resident.ts +1 -0
- package/src/supervisor/runtime-handler.ts +23 -23
- package/src/supervisor-protocol/command-body.ts +4 -1
package/README.md
CHANGED
|
@@ -149,3 +149,10 @@ bypass-permissions mode as root, so run them as a regular user.
|
|
|
149
149
|
Regenerate the experimental Codex wire types with `bun run generate:codex-protocol`.
|
|
150
150
|
The generator pins Codex 0.159.0 and verifies any `CODEX_EXECUTABLE` override before
|
|
151
151
|
using it. Commit generated files with generator changes; CI checks reproducibility.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Standalone settings changes
|
|
155
|
+
|
|
156
|
+
`agent.change-settings` carries a full `settings: { model, reasoningEffort }` pair through the supervisor wire and the agent protocol. Claude and Codex apply it only at an idle turn boundary, without submitting input, and emit `session.settings.changed` with the command ID and `appliesFrom: next_turn`. Product catalog, provider compatibility and admission ordering remain the caller's responsibility.
|
|
157
|
+
|
|
158
|
+
The supervisor advertises `capabilities["model-change"] = "v1"` in health. It commits the settings command outcome and inbox status in one SQLite transaction. After a process restart, an interrupted settings command can be reapplied idempotently; prompt commands retain their uncertain-delivery policy and are never retried by this path. Confirmed settings override the immutable bootstrap defaults when resuming the provider. Prompt and user-input-answer commands may carry resolved `settings` so their execution does not rely solely on mutable defaults.
|
|
Binary file
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Model and reasoning effort changes
|
|
2
|
+
|
|
3
|
+
agent-core can update a session's model and reasoning effort without sending a
|
|
4
|
+
conversational prompt. Healthy Claude and Codex sessions keep the same CLI process
|
|
5
|
+
and provider session, preserving their warmed runtime state.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Standalone command
|
|
10
|
+
|
|
11
|
+
Send `agent.change-settings` with `settings: { model, reasoningEffort }` at an idle
|
|
12
|
+
turn boundary. The caller schedules that boundary; a busy run defers the operation.
|
|
13
|
+
|
|
14
|
+
The supervisor persists the command in its inbox and acknowledges receipt with
|
|
15
|
+
`command.ack`. Receipt does not mean the provider has applied the settings.
|
|
16
|
+
|
|
17
|
+
| Provider | How settings are applied | Acceptance |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| Claude | `query.setModel(model)` and `query.applyFlagSettings({ effortLevel })`, skipping unchanged known values | The required control responses succeed |
|
|
20
|
+
| Codex | `thread/settings/update` with `{ threadId, model, effort }` | The JSON-RPC request succeeds |
|
|
21
|
+
|
|
22
|
+
After acceptance, the run updates its defaults and emits `session.settings.changed`
|
|
23
|
+
with `model`, `reasoningEffort`, the initiating `commandId`, and
|
|
24
|
+
`appliesFrom: 'next_turn'`. The supervisor commits the accepted settings together
|
|
25
|
+
with command completion, then sends `supervisor.command.applied`.
|
|
26
|
+
|
|
27
|
+
A provider failure does not produce a successful settings-change event or a
|
|
28
|
+
successful command completion. Receipt alone must never update a consumer's
|
|
29
|
+
display of confirmed settings.
|
|
30
|
+
|
|
31
|
+
## Settings carried by inputs
|
|
32
|
+
|
|
33
|
+
`agent.send-prompt.settings` and `agent.respond-user-input.settings` can carry
|
|
34
|
+
partial settings. Omitted fields retain the currently accepted values. Prompt
|
|
35
|
+
settings retain the provider's `next` and `steer` behavior; the confirmation event
|
|
36
|
+
identifies whether the change applies to the current or next turn. Already-delivered
|
|
37
|
+
answers do not change defaults or emit another settings-change event.
|
|
38
|
+
|
|
39
|
+
For prompts, a supplied `settings` object takes precedence over the entire legacy
|
|
40
|
+
top-level `model` / `reasoningEffort` pair, even when the object is empty. Without
|
|
41
|
+
that object, accepted legacy fields are also persisted as default changes. Missing
|
|
42
|
+
legacy fields do not clear previously accepted defaults.
|
|
43
|
+
|
|
44
|
+
Claude's legacy top-level effort field must match the current accepted effort.
|
|
45
|
+
Use `settings.reasoningEffort` or the standalone command to change it. For example,
|
|
46
|
+
after starting at `medium` and successfully applying standalone `high`, a legacy
|
|
47
|
+
prompt specifying `high` is accepted; one specifying `medium` is rejected.
|
|
48
|
+
|
|
49
|
+
## Recovery
|
|
50
|
+
|
|
51
|
+
The supervisor restores the latest accepted defaults across standalone commands,
|
|
52
|
+
prompts, and answers. For example, standalone A/high, followed by a legacy prompt
|
|
53
|
+
B/low, followed by `settings: { reasoningEffort: 'medium' }`, recovers as B/medium.
|
|
54
|
+
Failed commands and already-delivered answers do not replace those defaults.
|
|
55
|
+
|
|
56
|
+
The Bedrock wrapper retains its legacy prompt model-rebinding path. Standalone
|
|
57
|
+
changes bypass that path on a healthy connection. If a previous replacement failed
|
|
58
|
+
and left the old run stopped, the wrapper first recovers a connection with its
|
|
59
|
+
previously accepted settings, then applies the requested update. Recovery preserves
|
|
60
|
+
the provider session identity and does not replay the failed prompt.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/agent-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.0-experimental.
|
|
4
|
+
"version": "0.2.0-experimental.8",
|
|
5
5
|
"packageManager": "bun@1.3.12",
|
|
6
6
|
"description": "Shared contracts and runtime modules for Tela coding-agent sandboxes: agent protocol, supervisor wire protocol, resident supervisor, worker runtime client, and Claude/Codex harness adapters.",
|
|
7
7
|
"license": "UNLICENSED",
|
package/src/agents/agent-run.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AgentEvent,
|
|
4
4
|
AgentNotifyCommand,
|
|
5
5
|
AgentRunMetadata,
|
|
6
|
+
ChangeSettingsAgentCommand,
|
|
6
7
|
CompactContextAgentCommand,
|
|
7
8
|
InterruptAgentCommand,
|
|
8
9
|
RespondUserInputAgentCommand,
|
|
@@ -34,6 +35,7 @@ export interface AgentRun {
|
|
|
34
35
|
prepareWorkspace: (options?: AgentOperationOptions) => Promise<void>
|
|
35
36
|
// Providers without root registration accept the root without provider I/O.
|
|
36
37
|
addWorkspaceRepositoryRoot: (root: string, options?: AgentOperationOptions) => Promise<void>
|
|
38
|
+
changeSettings?: (command: ChangeSettingsAgentCommand, options?: AgentOperationOptions) => Promise<void>
|
|
37
39
|
sendPrompt: (command: SendPromptCommand, options?: AgentPromptOperationOptions) => Promise<AgentPromptAcceptance>
|
|
38
40
|
// Optional capabilities: the Claude and Codex runs implement both; other runs may omit them.
|
|
39
41
|
notify?: (command: AgentNotifyCommand, options?: AgentOperationOptions) => Promise<AgentNotificationAcceptance>
|
|
@@ -25,7 +25,7 @@ export class ClaudeCodeProcess {
|
|
|
25
25
|
private termination: Promise<void> | undefined
|
|
26
26
|
private stderr = ''
|
|
27
27
|
|
|
28
|
-
constructor(private readonly exitTimeoutMs = CLAUDE_PROCESS_EXIT_TIMEOUT_MS) {}
|
|
28
|
+
constructor(private readonly exitTimeoutMs = CLAUDE_PROCESS_EXIT_TIMEOUT_MS, private readonly processLauncher?: string) {}
|
|
29
29
|
|
|
30
30
|
/** The SDK `spawnClaudeCodeProcess` hook. A run spawns at most one process. */
|
|
31
31
|
readonly spawn = (options: SpawnOptions): SpawnedProcess => {
|
|
@@ -33,10 +33,10 @@ export class ClaudeCodeProcess {
|
|
|
33
33
|
throw new Error('Claude Code process already spawned for this run.')
|
|
34
34
|
// `detached` makes the CLI a process-group leader. The forwarded signal aborts only after the
|
|
35
35
|
// SDK's graceful stdin-EOF window, as for the SDK's own spawn.
|
|
36
|
-
const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ['pipe', 'pipe', 'pipe'], detached: true, windowsHide: true })
|
|
36
|
+
const child = spawn(this.processLauncher ?? options.command, this.processLauncher ? [options.command, ...options.args] : options.args, { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ['pipe', 'pipe', 'pipe'], detached: true, windowsHide: true })
|
|
37
37
|
this.child = child
|
|
38
38
|
this.exit = new Promise((resolve) => {
|
|
39
|
-
child.once('exit', () => resolve())
|
|
39
|
+
child.once(this.processLauncher ? 'close' : 'exit', () => resolve())
|
|
40
40
|
// A failed spawn emits `error` without `exit`; there is no process to wait for.
|
|
41
41
|
child.once('error', () => {
|
|
42
42
|
if (child.pid === undefined)
|
|
@@ -70,15 +70,21 @@ export class ClaudeCodeProcess {
|
|
|
70
70
|
const exit = this.exit
|
|
71
71
|
if (!child || !exit || child.pid === undefined)
|
|
72
72
|
return
|
|
73
|
-
|
|
73
|
+
// A fork/wait launcher can exit while its CLI still holds the stdio pipes.
|
|
74
|
+
// For launchers, await close and clean the group even if the wrapper already exited.
|
|
75
|
+
if (await settlesWithin(exit, this.exitTimeoutMs) && !this.processLauncher)
|
|
74
76
|
return
|
|
75
77
|
try {
|
|
76
78
|
process.kill(-child.pid, 'SIGKILL')
|
|
77
79
|
}
|
|
78
|
-
catch {
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (this.processLauncher && process.platform !== 'win32' && (error as NodeJS.ErrnoException).code !== 'ESRCH')
|
|
82
|
+
throw error
|
|
79
83
|
child.kill('SIGKILL')
|
|
80
84
|
}
|
|
81
|
-
await settlesWithin(exit, killedExitWaitMs)
|
|
85
|
+
const closed = await settlesWithin(exit, killedExitWaitMs)
|
|
86
|
+
if (this.processLauncher && !closed)
|
|
87
|
+
throw new Error('Claude launcher process group did not close after SIGKILL.')
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -71,6 +71,14 @@ export interface ClaudeProviderConfig {
|
|
|
71
71
|
desktop?: ClaudeDesktopConfig | null
|
|
72
72
|
tools?: readonly AgentTool[]
|
|
73
73
|
executable?: string
|
|
74
|
+
/**
|
|
75
|
+
* Trusted launcher invoked as <CLI executable> <CLI args>; owns any privilege drop.
|
|
76
|
+
* Preserve stdio and the process group: exec the CLI or keep children in that group.
|
|
77
|
+
* On Windows, the launcher must exec the CLI in place (group signals are unavailable).
|
|
78
|
+
*/
|
|
79
|
+
processLauncher?: string
|
|
80
|
+
/** Private provider stores; defaults to the CLI transcript directory when omitted. */
|
|
81
|
+
runtimeStateRoot?: string
|
|
74
82
|
environment?: Record<string, string | undefined>
|
|
75
83
|
nativeToolActorBridge?: ClaudeNativeToolActorBridge
|
|
76
84
|
startupDeadlineMs?: number
|
|
@@ -139,6 +147,8 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
139
147
|
private readonly desktop: ClaudeDesktopConfig | null
|
|
140
148
|
private readonly tools: readonly AgentTool[]
|
|
141
149
|
private readonly executable: string | undefined
|
|
150
|
+
private readonly processLauncher: string | undefined
|
|
151
|
+
private readonly runtimeStateRoot: string | undefined
|
|
142
152
|
private readonly environment: Record<string, string | undefined>
|
|
143
153
|
private readonly nativeToolActorBridge: ClaudeNativeToolActorBridge | undefined
|
|
144
154
|
private readonly startupDeadlineMs: number
|
|
@@ -156,6 +166,8 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
156
166
|
this.desktop = config.desktop ?? null
|
|
157
167
|
this.tools = config.tools ?? []
|
|
158
168
|
this.executable = config.executable
|
|
169
|
+
this.processLauncher = config.processLauncher
|
|
170
|
+
this.runtimeStateRoot = config.runtimeStateRoot
|
|
159
171
|
this.environment = config.environment ?? {}
|
|
160
172
|
this.nativeToolActorBridge = config.nativeToolActorBridge
|
|
161
173
|
this.startupDeadlineMs = config.startupDeadlineMs ?? CLAUDE_STARTUP_DEADLINE_MS
|
|
@@ -202,10 +214,10 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
202
214
|
const runtime = createClaudeToolRuntimeRef({ turnId: () => state.currentTurnId })
|
|
203
215
|
// The transcript location is a pre-input fact; session-local stores live beside it.
|
|
204
216
|
const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
|
|
205
|
-
const steeredNotifications = ClaudeSteeredNotificationStore.open({ sessionId: input.sessionId, transcriptPath })
|
|
217
|
+
const steeredNotifications = ClaudeSteeredNotificationStore.open({ sessionId: input.sessionId, transcriptPath, rootDir: this.runtimeStateRoot })
|
|
206
218
|
const resources = await this.openRunResources({ input, expectedSessionId, signal: abortController.signal })
|
|
207
219
|
const { nativeToolActorRun, workspaceSkills, selectedRepositoryInstructions, browserAuthBroker, browserTurnMetadata } = resources
|
|
208
|
-
const claudeProcess = new ClaudeCodeProcess()
|
|
220
|
+
const claudeProcess = new ClaudeCodeProcess(undefined, this.processLauncher)
|
|
209
221
|
const declaredSubscriptionType = this.auth ? fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) : undefined
|
|
210
222
|
const runtimeMcpServers = toClaudeMcpServers({
|
|
211
223
|
tools,
|
|
@@ -302,7 +314,7 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
302
314
|
const [userInputRequests, initialization, , sessionContext] = await Promise.all([
|
|
303
315
|
UserInputRequestStore.open({
|
|
304
316
|
sessionId: metadata.sessionId,
|
|
305
|
-
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
317
|
+
rootDir: this.runtimeStateRoot ?? userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
306
318
|
}),
|
|
307
319
|
awaitClaudeInitialization({ query, signal: abortController.signal, deadlineMs: this.startupDeadlineMs }),
|
|
308
320
|
this.auth?.acquire({ signal: abortController.signal }),
|
|
@@ -314,8 +326,12 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
314
326
|
if (this.auth && declaredSubscriptionType !== undefined && declaredSubscriptionType !== fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) && !params.respawnedForSubscriptionType) {
|
|
315
327
|
query.close()
|
|
316
328
|
inputQueue.close()
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
try {
|
|
330
|
+
await claudeProcess.terminate()
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
await closeRunResources(resources)
|
|
334
|
+
}
|
|
319
335
|
return await this.openRun({ ...params, respawnedForSubscriptionType: true })
|
|
320
336
|
}
|
|
321
337
|
|
|
@@ -372,8 +388,12 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
372
388
|
query.close()
|
|
373
389
|
inputQueue.close()
|
|
374
390
|
// A failed open (for example a missed initialize deadline) must not leave its CLI behind.
|
|
375
|
-
|
|
376
|
-
|
|
391
|
+
try {
|
|
392
|
+
await claudeProcess.terminate()
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
await closeRunResources(resources)
|
|
396
|
+
}
|
|
377
397
|
throw error
|
|
378
398
|
}
|
|
379
399
|
}
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import type { Query, SDKMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
2
2
|
import type { UUID } from 'node:crypto'
|
|
3
|
-
import type {
|
|
4
|
-
AgentContextCompactionRequestResult,
|
|
5
|
-
AgentError,
|
|
6
|
-
AgentNotifyCommand,
|
|
7
|
-
AgentReasoningEffort,
|
|
8
|
-
AgentRunMetadata,
|
|
9
|
-
AgentSettingsChange,
|
|
10
|
-
CompactContextAgentCommand,
|
|
11
|
-
InterruptAgentCommand,
|
|
12
|
-
RespondUserInputAgentCommand,
|
|
13
|
-
SendPromptCommand,
|
|
14
|
-
StopAgentCommand,
|
|
15
|
-
} from '../../protocol'
|
|
3
|
+
import type { AgentContextCompactionRequestResult, AgentError, AgentNotifyCommand, AgentReasoningEffort, AgentRunMetadata, AgentSettingsChange, ChangeSettingsAgentCommand, CompactContextAgentCommand, InterruptAgentCommand, RespondUserInputAgentCommand, SendPromptCommand, StopAgentCommand } from '../../protocol'
|
|
16
4
|
import type { AgentEventDraft } from '../agent-event-stream'
|
|
17
5
|
import type { AgentOperationOptions, AgentPromptOperationOptions } from '../agent-operation'
|
|
18
6
|
import type { AgentNotificationAcceptance, AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
@@ -297,6 +285,10 @@ export class ClaudeRun implements AgentRun {
|
|
|
297
285
|
this.events.push(decorated)
|
|
298
286
|
if (draft.type === 'turn.started' && draft.payload.commandId)
|
|
299
287
|
this.emitAcceptedMessage(draft.payload.commandId, draft.turnId)
|
|
288
|
+
// Running may precede the input's started receipt. Consumption identifies the turn
|
|
289
|
+
// after that receipt, before the mapper emits the assistant's message.
|
|
290
|
+
if (draft.type === 'prompt.turn-correlated')
|
|
291
|
+
this.emitAcceptedMessage(draft.payload.commandId, draft.payload.turnId)
|
|
300
292
|
}
|
|
301
293
|
|
|
302
294
|
// The accepted input becomes a user message of the turn that consumed it, correlated with the
|
|
@@ -356,6 +348,28 @@ export class ClaudeRun implements AgentRun {
|
|
|
356
348
|
this.skillReloadOwed = false
|
|
357
349
|
}
|
|
358
350
|
|
|
351
|
+
async changeSettings(command: ChangeSettingsAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
352
|
+
this.assertCommandTargetsRun(command)
|
|
353
|
+
this.assertRunning()
|
|
354
|
+
options.signal?.throwIfAborted()
|
|
355
|
+
if (this.eventMapperState.currentTurnId || this.commandByClientMessageId.size > 0 || this.modelChangeInFlight)
|
|
356
|
+
throw new AgentPromptDeferredError({ message: 'Settings changes require an idle turn boundary.' })
|
|
357
|
+
this.modelChangeInFlight = true
|
|
358
|
+
try {
|
|
359
|
+
const settings = this.settingsWith(command.settings)
|
|
360
|
+
await this.applyProviderSettings(settings)
|
|
361
|
+
this.current = settings
|
|
362
|
+
if (settings.model) {
|
|
363
|
+
this.metadata.model = settings.model
|
|
364
|
+
this.init.requestedModel = settings.model
|
|
365
|
+
}
|
|
366
|
+
this.pushEvent(sessionSettingsChangedEvent({ model: settings.model!, reasoningEffort: settings.effort, commandId: command.commandId, appliesFrom: 'next_turn' }))
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
this.modelChangeInFlight = false
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
359
373
|
async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
360
374
|
this.assertCommandTargetsRun(command)
|
|
361
375
|
this.assertRunning()
|
|
@@ -365,7 +379,7 @@ export class ClaudeRun implements AgentRun {
|
|
|
365
379
|
message: 'Bypass submission requires a steer prompt.',
|
|
366
380
|
})
|
|
367
381
|
}
|
|
368
|
-
if (command.reasoningEffort !== undefined && command.reasoningEffort !== this.
|
|
382
|
+
if (!command.settings && command.reasoningEffort !== undefined && command.reasoningEffort !== this.current.effort)
|
|
369
383
|
throw new AgentNotAcceptedError({ message: 'Claude reasoning effort changes are not supported by this adapter.', recovery: { kind: 'terminal', reason: 'reasoning_effort_change_not_supported' } })
|
|
370
384
|
// Claude Code steers by cancelling the running ask, which would abort a manual compaction
|
|
371
385
|
// (CLI 2.1.280). Like Codex's non-steerable compaction turn, the steer waits for it to end.
|
|
@@ -441,6 +455,8 @@ export class ClaudeRun implements AgentRun {
|
|
|
441
455
|
private async applyCommandModel(command: SendPromptCommand, options: AgentOperationOptions): Promise<void> {
|
|
442
456
|
if (this.modelChangeInFlight)
|
|
443
457
|
throw new AgentPromptDeferredError({ message: 'Claude model configuration is still in progress.' })
|
|
458
|
+
if (command.settings)
|
|
459
|
+
return
|
|
444
460
|
const currentModel = this.init.requestedModel ?? this.metadata.model
|
|
445
461
|
if (!command.model || command.model === currentModel)
|
|
446
462
|
return
|
|
@@ -742,7 +758,8 @@ export class ClaudeRun implements AgentRun {
|
|
|
742
758
|
return 'already_delivered'
|
|
743
759
|
// Deliver first: on a prebound resume this is the run's first input, and the resolved
|
|
744
760
|
// event must not precede the lifecycle events the init barrier emits.
|
|
745
|
-
|
|
761
|
+
const settings = this.settingsWith(command.settings)
|
|
762
|
+
await this.applyProviderSettings(settings)
|
|
746
763
|
// The answer opens its turn as the user's input, correlated like a prompt: the turn it
|
|
747
764
|
// starts carries this command, and the answer is accepted as that turn's user message.
|
|
748
765
|
const clientMessageId = deriveMessageId(command.commandId)
|
|
@@ -762,6 +779,16 @@ export class ClaudeRun implements AgentRun {
|
|
|
762
779
|
finally {
|
|
763
780
|
this.acceptanceWaiters.delete(clientMessageId)
|
|
764
781
|
}
|
|
782
|
+
if (command.settings) {
|
|
783
|
+
this.current = settings
|
|
784
|
+
this.pushEvent(sessionSettingsChangedEvent({
|
|
785
|
+
model: settings.model ?? await this.initialization,
|
|
786
|
+
reasoningEffort: settings.effort,
|
|
787
|
+
commandId: command.commandId,
|
|
788
|
+
turnId: this.eventMapperState.currentTurnId,
|
|
789
|
+
appliesFrom: 'current_turn',
|
|
790
|
+
}))
|
|
791
|
+
}
|
|
765
792
|
if (resolved.event)
|
|
766
793
|
this.pushEvent(resolved.event)
|
|
767
794
|
this.userInputRequests.markDelivered(command.requestId)
|
|
@@ -879,18 +906,31 @@ export class ClaudeRun implements AgentRun {
|
|
|
879
906
|
cause: error,
|
|
880
907
|
}))
|
|
881
908
|
this.ended = this.endAfterProcessExit(reason, error)
|
|
909
|
+
// Autonomous stream failure may finish without a stop caller; keep rejection observed.
|
|
910
|
+
void this.ended.catch(() => undefined)
|
|
882
911
|
}
|
|
883
912
|
|
|
884
|
-
// session.ended means the CLI is gone: the terminal events wait for its exit (bounded by
|
|
913
|
+
// A successful session.ended means the CLI is gone: the terminal events wait for its exit (bounded by
|
|
885
914
|
// CLAUDE_PROCESS_EXIT_TIMEOUT_MS, then a process-group kill). close() is idempotent and sends
|
|
886
915
|
// the stdin EOF the CLI exits on.
|
|
887
916
|
private async endAfterProcessExit(reason: 'completed' | 'stopped' | 'interrupted' | 'failed', error: AgentError | undefined): Promise<void> {
|
|
888
917
|
this.closeQuery()
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
918
|
+
try {
|
|
919
|
+
await this.process.terminate()
|
|
920
|
+
this.events.push({ type: 'session.ended', payload: error ? { reason, error } : { reason } })
|
|
921
|
+
this.events.push({ type: 'session.state.changed', payload: { state: reason === 'failed' ? 'failed' : 'stopped' } })
|
|
922
|
+
}
|
|
923
|
+
catch (cause) {
|
|
924
|
+
const shutdownError = { ...serializeAgentError(cause), code: 'agents.provider-shutdown-failed' }
|
|
925
|
+
this.events.push({ type: 'error', payload: { message: 'Claude CLI shutdown could not be confirmed.', fatal: true, source: 'runtime', error: shutdownError } })
|
|
926
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: shutdownError } })
|
|
927
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
928
|
+
throw cause
|
|
929
|
+
}
|
|
930
|
+
finally {
|
|
931
|
+
this.events.end()
|
|
932
|
+
await this.onFinished().catch(() => undefined)
|
|
933
|
+
}
|
|
894
934
|
}
|
|
895
935
|
|
|
896
936
|
// Reports the request's one result; later calls return the reported one. The caller decides
|
|
@@ -48,9 +48,9 @@ export interface SteeredNotificationDelivery {
|
|
|
48
48
|
export class ClaudeSteeredNotificationStore {
|
|
49
49
|
private constructor(private readonly filePath: string, private file: StoreFile) {}
|
|
50
50
|
|
|
51
|
-
static open(input: { sessionId: string, transcriptPath: string }): ClaudeSteeredNotificationStore {
|
|
51
|
+
static open(input: { sessionId: string, transcriptPath: string, rootDir?: string }): ClaudeSteeredNotificationStore {
|
|
52
52
|
const filePath = join(
|
|
53
|
-
dirname(input.transcriptPath),
|
|
53
|
+
input.rootDir ?? dirname(input.transcriptPath),
|
|
54
54
|
storeDirectoryName,
|
|
55
55
|
Buffer.from(input.sessionId, 'utf8').toString('base64url'),
|
|
56
56
|
storeFileName,
|
|
@@ -59,7 +59,9 @@ export class CodexJsonRpcClient {
|
|
|
59
59
|
readonly child: ChildProcessWithoutNullStreams
|
|
60
60
|
private nextId = 0
|
|
61
61
|
private stopped = false
|
|
62
|
+
private shutdown: Promise<void> | undefined
|
|
62
63
|
private readonly closed: Promise<void>
|
|
64
|
+
private readonly processGroup: boolean
|
|
63
65
|
private readonly pending = new Map<string, {
|
|
64
66
|
method: ClientRequestMethod
|
|
65
67
|
resolve: (value: unknown) => void
|
|
@@ -70,6 +72,7 @@ export class CodexJsonRpcClient {
|
|
|
70
72
|
executable: string,
|
|
71
73
|
options: {
|
|
72
74
|
cwd: string
|
|
75
|
+
processLauncher?: string
|
|
73
76
|
environment?: Record<string, string | undefined>
|
|
74
77
|
configOverrides?: readonly string[]
|
|
75
78
|
onMessage: (message: CodexJsonRpcMessage) => void
|
|
@@ -78,10 +81,13 @@ export class CodexJsonRpcClient {
|
|
|
78
81
|
maxLineBytes?: number
|
|
79
82
|
},
|
|
80
83
|
) {
|
|
81
|
-
this.
|
|
84
|
+
this.processGroup = options.processLauncher !== undefined && process.platform !== 'win32'
|
|
85
|
+
const args = ['app-server', '--listen', 'stdio://', ...(options.configOverrides ?? []).flatMap(value => ['-c', value])]
|
|
86
|
+
this.child = spawn(options.processLauncher ?? executable, options.processLauncher ? [executable, ...args] : args, {
|
|
82
87
|
cwd: options.cwd,
|
|
83
88
|
env: options.environment,
|
|
84
89
|
stdio: 'pipe',
|
|
90
|
+
detached: this.processGroup,
|
|
85
91
|
})
|
|
86
92
|
this.closed = new Promise(resolve => this.child.once('close', () => resolve()))
|
|
87
93
|
|
|
@@ -229,9 +235,16 @@ export class CodexJsonRpcClient {
|
|
|
229
235
|
}
|
|
230
236
|
|
|
231
237
|
async closeInputAndStop(): Promise<void> {
|
|
238
|
+
this.shutdown ??= this.drainAndStop()
|
|
239
|
+
await this.shutdown
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async drainAndStop(): Promise<void> {
|
|
232
243
|
if (this.stopped) {
|
|
233
244
|
if (!await waitForClosed(this.closed, PROCESS_SHUTDOWN_GRACE_MS))
|
|
234
|
-
await terminateChild(this.child, this.closed)
|
|
245
|
+
await terminateChild(this.child, this.closed, this.processGroup)
|
|
246
|
+
if (this.processGroup)
|
|
247
|
+
signalProcessGroup(this.child, 'SIGKILL')
|
|
235
248
|
return
|
|
236
249
|
}
|
|
237
250
|
this.stopped = true
|
|
@@ -242,9 +255,10 @@ export class CodexJsonRpcClient {
|
|
|
242
255
|
// EOF/drain and process exit share one grace window. Never wait separately for a
|
|
243
256
|
// buffered stdin write: a child that stops reading must still reach signal escalation.
|
|
244
257
|
this.child.stdin.end()
|
|
245
|
-
if (await waitForClosed(this.closed, PROCESS_SHUTDOWN_GRACE_MS))
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
if (!await waitForClosed(this.closed, PROCESS_SHUTDOWN_GRACE_MS))
|
|
259
|
+
await terminateChild(this.child, this.closed, this.processGroup)
|
|
260
|
+
if (this.processGroup)
|
|
261
|
+
signalProcessGroup(this.child, 'SIGKILL')
|
|
248
262
|
}
|
|
249
263
|
|
|
250
264
|
async stop(): Promise<void> {
|
|
@@ -288,7 +302,17 @@ export class CodexJsonRpcClient {
|
|
|
288
302
|
}
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
async function terminateChild(child: ChildProcessWithoutNullStreams, closed: Promise<void
|
|
305
|
+
async function terminateChild(child: ChildProcessWithoutNullStreams, closed: Promise<void>, processGroup: boolean): Promise<void> {
|
|
306
|
+
if (processGroup) {
|
|
307
|
+
// The launcher may already be gone while the CLI retains its pipes. Signal the
|
|
308
|
+
// retained group regardless of the wrapper's exitCode, and bound the close wait.
|
|
309
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGKILL'] as const) {
|
|
310
|
+
signalProcessGroup(child, signal)
|
|
311
|
+
if (await waitForClosed(closed, signal === 'SIGKILL' ? 1_000 : PROCESS_SHUTDOWN_GRACE_MS))
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
throw new Error('Codex launcher process group did not close after SIGKILL.')
|
|
315
|
+
}
|
|
292
316
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
293
317
|
await closed
|
|
294
318
|
return
|
|
@@ -308,6 +332,18 @@ async function terminateChild(child: ChildProcessWithoutNullStreams, closed: Pro
|
|
|
308
332
|
await closed
|
|
309
333
|
}
|
|
310
334
|
|
|
335
|
+
function signalProcessGroup(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): void {
|
|
336
|
+
if (child.pid === undefined)
|
|
337
|
+
return
|
|
338
|
+
try {
|
|
339
|
+
process.kill(-child.pid, signal)
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if ((error as NodeJS.ErrnoException).code !== 'ESRCH')
|
|
343
|
+
throw error
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
311
347
|
async function waitForClosed(closed: Promise<void>, timeoutMs: number): Promise<boolean> {
|
|
312
348
|
return await new Promise<boolean>((resolve) => {
|
|
313
349
|
const timer = setTimeout(() => resolve(false), timeoutMs)
|
|
@@ -54,6 +54,14 @@ export interface CodexProviderConfig {
|
|
|
54
54
|
auth: CodexAuthProvider | CodexAuthentication
|
|
55
55
|
tools?: readonly AgentTool[]
|
|
56
56
|
executable?: string
|
|
57
|
+
/**
|
|
58
|
+
* Trusted launcher invoked as <CLI executable> <CLI args>; owns any privilege drop.
|
|
59
|
+
* Preserve stdio and the process group: exec the CLI or keep children in that group.
|
|
60
|
+
* On Windows, the launcher must exec the CLI in place (group signals are unavailable).
|
|
61
|
+
*/
|
|
62
|
+
processLauncher?: string
|
|
63
|
+
/** Private provider stores; defaults to the CLI transcript directory when omitted. */
|
|
64
|
+
runtimeStateRoot?: string
|
|
57
65
|
environment?: Record<string, string | undefined>
|
|
58
66
|
onStartupStage?: (fact: CodexStartupStageFact) => void
|
|
59
67
|
instructions?: InstructionComposer
|
|
@@ -111,6 +119,8 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
111
119
|
private readonly tools: readonly AgentTool[]
|
|
112
120
|
private readonly executable: string
|
|
113
121
|
private readonly environment: Record<string, string | undefined>
|
|
122
|
+
private readonly processLauncher: string | undefined
|
|
123
|
+
private readonly runtimeStateRoot: string | undefined
|
|
114
124
|
private readonly auth: CodexAuthentication
|
|
115
125
|
/** The validated endpoint of an API key login that names one. */
|
|
116
126
|
private readonly apiKeyEndpoint: string | undefined
|
|
@@ -130,6 +140,8 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
130
140
|
this.tools = config.tools ?? []
|
|
131
141
|
this.executable = config.executable ?? 'codex'
|
|
132
142
|
this.environment = config.environment ?? {}
|
|
143
|
+
this.processLauncher = config.processLauncher
|
|
144
|
+
this.runtimeStateRoot = config.runtimeStateRoot
|
|
133
145
|
this.onStartupStage = config.onStartupStage
|
|
134
146
|
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
135
147
|
this.sessionContext = config.sessionContext
|
|
@@ -169,18 +181,19 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
169
181
|
return this.wrapBedrockRun(await this.resumeRawSession(input, options), input)
|
|
170
182
|
}
|
|
171
183
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
184
|
+
// Legacy prompt-only model changes rebind at an idle boundary. Explicit settings
|
|
185
|
+
// commands use the native update on the existing thread and keep the CLI warm.
|
|
174
186
|
private wrapBedrockRun(run: CodexRun, input: AgentSessionOpenInput<'codex'>): AgentRun {
|
|
175
187
|
if (this.auth.kind !== 'amazon-bedrock')
|
|
176
188
|
return run
|
|
177
189
|
const roots = new Set(input.workspaceRepositoryRoots)
|
|
178
|
-
return new ModelRebindingRun(run, async (model, signal) => await this.resumeRawSession({
|
|
190
|
+
return new ModelRebindingRun(run, async (model, signal, reasoningEffort) => await this.resumeRawSession({
|
|
179
191
|
...input,
|
|
180
192
|
model: codexModelIdSchema.parse(model),
|
|
193
|
+
reasoningEffort,
|
|
181
194
|
providerSessionId: run.metadata.providerSessionId,
|
|
182
195
|
workspaceRepositoryRoots: [...roots],
|
|
183
|
-
}, { signal }), root => roots.add(root))
|
|
196
|
+
}, { signal }), root => roots.add(root), input.reasoningEffort)
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
private async resumeRawSession(input: AgentSessionResumeInput<'codex'>, options: AgentOperationOptions = {}): Promise<CodexRun> {
|
|
@@ -458,6 +471,7 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
458
471
|
|
|
459
472
|
try {
|
|
460
473
|
client = new CodexJsonRpcClient(this.executable, {
|
|
474
|
+
processLauncher: this.processLauncher,
|
|
461
475
|
cwd,
|
|
462
476
|
environment: holder.browserAuthBroker
|
|
463
477
|
? { ...this.environment, CODING_AGENT_BROWSER_AUTH_SOCKET: holder.browserAuthBroker.socketPath }
|
|
@@ -637,7 +651,7 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
637
651
|
}),
|
|
638
652
|
UserInputRequestStore.open({
|
|
639
653
|
sessionId: metadata.sessionId,
|
|
640
|
-
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
654
|
+
rootDir: this.runtimeStateRoot ?? userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
641
655
|
}),
|
|
642
656
|
pendingSessionContext({
|
|
643
657
|
compose: this.sessionContext,
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentAuthor,
|
|
3
|
-
AgentContextCompactionRequestResult,
|
|
4
|
-
AgentNotifyCommand,
|
|
5
|
-
AgentPrompt,
|
|
6
|
-
AgentReasoningEffort,
|
|
7
|
-
AgentRunMetadata,
|
|
8
|
-
AgentSettingsChange,
|
|
9
|
-
CompactContextAgentCommand,
|
|
10
|
-
InterruptAgentCommand,
|
|
11
|
-
RespondUserInputAgentCommand,
|
|
12
|
-
SendPromptCommand,
|
|
13
|
-
StopAgentCommand,
|
|
14
|
-
} from '../../protocol'
|
|
1
|
+
import type { AgentAuthor, AgentContextCompactionRequestResult, AgentNotifyCommand, AgentPrompt, AgentReasoningEffort, AgentRunMetadata, AgentSettingsChange, ChangeSettingsAgentCommand, CompactContextAgentCommand, InterruptAgentCommand, RespondUserInputAgentCommand, SendPromptCommand, StopAgentCommand } from '../../protocol'
|
|
15
2
|
import type { AgentEventDraft } from '../agent-event-stream'
|
|
16
3
|
import type { AgentOperationOptions, AgentPromptOperationOptions } from '../agent-operation'
|
|
17
4
|
import type { AgentNotificationAcceptance, AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
@@ -21,6 +8,7 @@ import type { CodexEventMapperState } from './codex-event-mapper'
|
|
|
21
8
|
import type { CodexJsonRpcClient } from './codex-json-rpc-client'
|
|
22
9
|
import type { ClientRequestResponsesByMethod, CodexUserInput } from './codex-protocol'
|
|
23
10
|
import { serializeAgentNotificationToolOutput } from '../../protocol'
|
|
11
|
+
import { serializeAgentError } from '../agent-error-serializer'
|
|
24
12
|
import { AgentEventStream } from '../agent-event-stream'
|
|
25
13
|
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from '../agent-runtime-error'
|
|
26
14
|
import { sessionSettingsChangedEvent } from '../agent-session-events'
|
|
@@ -146,7 +134,7 @@ export class CodexRun implements AgentRun {
|
|
|
146
134
|
sessionId: this.metadata.sessionId,
|
|
147
135
|
providerSessionId: this.metadata.providerSessionId,
|
|
148
136
|
reason: 'aborted',
|
|
149
|
-
}))
|
|
137
|
+
}).catch(() => undefined))
|
|
150
138
|
}
|
|
151
139
|
|
|
152
140
|
pushEvent(draft: AgentEventDraft): void {
|
|
@@ -273,6 +261,22 @@ export class CodexRun implements AgentRun {
|
|
|
273
261
|
await stopping.promise
|
|
274
262
|
}
|
|
275
263
|
|
|
264
|
+
async changeSettings(command: ChangeSettingsAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
265
|
+
this.assertCommandTargetsRun(command)
|
|
266
|
+
this.assertRunning()
|
|
267
|
+
options.signal?.throwIfAborted()
|
|
268
|
+
this.deferNextWhileTurnActive('next')
|
|
269
|
+
const settings = withSettingsChange(this.current, command.settings)
|
|
270
|
+
try {
|
|
271
|
+
await this.client.request('thread/settings/update', { threadId: this.metadata.providerSessionId, model: this.resolveInferenceModel(settings.model), effort: codexEffort(settings.effort) })
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
throw mapSettingsTransportLoss(error)
|
|
275
|
+
}
|
|
276
|
+
this.metadata.model = settings.model
|
|
277
|
+
this.settingsAccepted({ settings, commandId: command.commandId, turnId: undefined, placement: { appliesFrom: 'next_turn' } })
|
|
278
|
+
}
|
|
279
|
+
|
|
276
280
|
async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
277
281
|
this.assertCommandTargetsRun(command)
|
|
278
282
|
this.assertRunning()
|
|
@@ -582,11 +586,14 @@ export class CodexRun implements AgentRun {
|
|
|
582
586
|
if (resolved.event)
|
|
583
587
|
this.pushEvent(resolved.event)
|
|
584
588
|
const prepared = await this.prepareTurn({ prompt: resolved.prompt, commandId: command.commandId, options })
|
|
585
|
-
|
|
589
|
+
const settings = withSettingsChange(this.current, command.settings)
|
|
590
|
+
const { receipt } = await this.startTurn({
|
|
586
591
|
commandId: prepared.commandId,
|
|
587
|
-
settings
|
|
592
|
+
settings,
|
|
588
593
|
content: { kind: 'prompt', input: prepared.input, origin: prepared.origin },
|
|
589
594
|
}, options)
|
|
595
|
+
if (command.settings)
|
|
596
|
+
this.settingsAccepted({ settings, commandId: command.commandId, turnId: receipt.turnId, placement: { appliesFrom: 'current_turn' } })
|
|
590
597
|
this.sessionContextDelivered(prepared.sessionContext)
|
|
591
598
|
this.userInputRequests.markDelivered(command.requestId)
|
|
592
599
|
return 'submitted'
|
|
@@ -639,8 +646,20 @@ export class CodexRun implements AgentRun {
|
|
|
639
646
|
const revocation = new Promise<void>((resolve) => {
|
|
640
647
|
resolve(this.onStop())
|
|
641
648
|
})
|
|
642
|
-
void Promise.
|
|
643
|
-
.then(
|
|
649
|
+
void Promise.allSettled([this.client.stop(), revocation])
|
|
650
|
+
.then((outcomes) => {
|
|
651
|
+
const failure = outcomes.find(outcome => outcome.status === 'rejected')
|
|
652
|
+
if (!failure) {
|
|
653
|
+
finalize()
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
const error = { ...serializeAgentError(failure.reason), code: 'agents.provider-shutdown-failed' }
|
|
657
|
+
this.events.push({ type: 'error', payload: { message: 'Codex run shutdown could not be confirmed.', fatal: true, source: 'runtime', error } })
|
|
658
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'failed', error } })
|
|
659
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
660
|
+
this.events.end()
|
|
661
|
+
throw failure.reason
|
|
662
|
+
})
|
|
644
663
|
.then(stopping.resolve, stopping.reject)
|
|
645
664
|
}
|
|
646
665
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Buffer } from 'node:buffer'
|
|
2
1
|
import type { MaterializedAgentInputAttachment } from './materialized-input-attachment'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { AgentInputAttachmentPreparationFailure } from './input-attachment.errors'
|
|
5
5
|
|
|
@@ -8,7 +8,9 @@ type NativeImageMimeType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/web
|
|
|
8
8
|
export async function readAttachmentBytes(attachment: MaterializedAgentInputAttachment, signal: AbortSignal): Promise<Buffer> {
|
|
9
9
|
signal.throwIfAborted()
|
|
10
10
|
try {
|
|
11
|
-
const bytes =
|
|
11
|
+
const bytes = attachment.readBytes
|
|
12
|
+
? Buffer.from(await attachment.readBytes(signal))
|
|
13
|
+
: await readFile(attachment.localPath, { signal })
|
|
12
14
|
signal.throwIfAborted()
|
|
13
15
|
return bytes
|
|
14
16
|
}
|
|
@@ -6,6 +6,8 @@ export type MaterializedAgentInputAttachment = Readonly<{
|
|
|
6
6
|
byteSize: number
|
|
7
7
|
sha256: string
|
|
8
8
|
localPath: string
|
|
9
|
+
/** Read as the file owner when the provider uses a different process identity. */
|
|
10
|
+
readBytes?: (signal: AbortSignal) => Promise<Uint8Array>
|
|
9
11
|
}>
|
|
10
12
|
|
|
11
13
|
export type AgentInputAttachmentProjection = Readonly<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEvent, AgentRunMetadata, SendPromptCommand } from '../protocol'
|
|
1
|
+
import type { AgentEvent, AgentReasoningEffort, AgentRunMetadata, ChangeSettingsAgentCommand, SendPromptCommand } from '../protocol'
|
|
2
2
|
import type { AgentOperationOptions, AgentPromptOperationOptions } from './agent-operation'
|
|
3
3
|
import type { AgentPromptAcceptance, AgentRun } from './agent-run'
|
|
4
4
|
import { AgentEventStream } from './agent-event-stream'
|
|
@@ -12,7 +12,8 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
12
12
|
private pump: Promise<void>
|
|
13
13
|
private activeTurn: string | undefined
|
|
14
14
|
private lastEndedTurn: string | undefined
|
|
15
|
-
private dispatching: Promise<
|
|
15
|
+
private dispatching: Promise<unknown> | undefined
|
|
16
|
+
private settingsChanging = false
|
|
16
17
|
private changing = false
|
|
17
18
|
private needsRebind = false
|
|
18
19
|
private stopped = false
|
|
@@ -21,7 +22,7 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
21
22
|
private readonly lifetime = new AbortController()
|
|
22
23
|
private readonly controls = new Set<Promise<unknown>>()
|
|
23
24
|
|
|
24
|
-
constructor(run: AgentRun, private readonly reopen: (model: string, signal: AbortSignal) => Promise<AgentRun>, private readonly rootAdded?: (root: string) => void) {
|
|
25
|
+
constructor(run: AgentRun, private readonly reopen: (model: string, signal: AbortSignal, reasoningEffort?: AgentReasoningEffort) => Promise<AgentRun>, private readonly rootAdded?: (root: string) => void, private reasoningEffort?: AgentReasoningEffort) {
|
|
25
26
|
this.run = run
|
|
26
27
|
this.metadata = { ...run.metadata }
|
|
27
28
|
this.events = new AgentEventStream(this.metadata)
|
|
@@ -42,7 +43,13 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
42
43
|
})
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
respondUserInput: AgentRun['respondUserInput'] = async (command, options) => await this.withControl(signal =>
|
|
46
|
+
respondUserInput: AgentRun['respondUserInput'] = async (command, options) => await this.withControl(async (signal) => {
|
|
47
|
+
const result = await this.run.respondUserInput(command, { ...options, signal })
|
|
48
|
+
if (result === 'submitted')
|
|
49
|
+
this.reasoningEffort = command.settings?.reasoningEffort ?? this.reasoningEffort
|
|
50
|
+
return result
|
|
51
|
+
}, options)
|
|
52
|
+
|
|
46
53
|
interrupt: AgentRun['interrupt'] = async (command, options) => await this.withControl(signal => this.run.interrupt(command, { ...options, signal }), options)
|
|
47
54
|
notify: NonNullable<AgentRun['notify']> = async (command, options) => await this.withControl(async (signal) => {
|
|
48
55
|
if (!this.run.notify)
|
|
@@ -74,7 +81,56 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
74
81
|
throw new AgentRunStateError({ message: 'Prompt does not target the bound run.' })
|
|
75
82
|
if (this.changing)
|
|
76
83
|
throw new AgentPromptDeferredError({ message: 'The provider model is being rebound.' })
|
|
77
|
-
const
|
|
84
|
+
const settings = command.settings ?? { model: command.model, reasoningEffort: command.reasoningEffort }
|
|
85
|
+
await this.rebind(settings.model ?? this.metadata.model, options)
|
|
86
|
+
this.lifetime.signal.throwIfAborted()
|
|
87
|
+
options.signal?.throwIfAborted()
|
|
88
|
+
const acceptance = await this.run.sendPrompt(command, { ...options, signal: AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : [])]) })
|
|
89
|
+
this.reasoningEffort = settings.reasoningEffort ?? this.reasoningEffort
|
|
90
|
+
if (acceptance.kind === 'turn' && acceptance.turnId !== this.lastEndedTurn)
|
|
91
|
+
this.activeTurn = acceptance.turnId
|
|
92
|
+
return acceptance
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async changeSettings(command: ChangeSettingsAgentCommand, options: AgentOperationOptions = {}): Promise<void> {
|
|
96
|
+
if (this.stopped)
|
|
97
|
+
throw new AgentRunStateError({ message: 'The run stopped.' })
|
|
98
|
+
if (command.sessionId !== this.metadata.sessionId || command.providerSessionId !== this.metadata.providerSessionId || command.provider !== this.metadata.provider)
|
|
99
|
+
throw new AgentRunStateError({ message: 'Settings command does not target the bound run.' })
|
|
100
|
+
if (this.dispatching || this.workspaceChanging || this.controls.size || this.changing)
|
|
101
|
+
throw new AgentPromptDeferredError({ message: 'A provider operation is already in progress.' })
|
|
102
|
+
if (this.activeTurn)
|
|
103
|
+
throw new AgentPromptDeferredError({ message: 'Settings changes require an idle turn boundary.' })
|
|
104
|
+
this.settingsChanging = true
|
|
105
|
+
const task = this.dispatchSettings(command, options)
|
|
106
|
+
this.dispatching = task
|
|
107
|
+
try {
|
|
108
|
+
await task
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this.dispatching = undefined
|
|
112
|
+
this.settingsChanging = false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async dispatchSettings(command: ChangeSettingsAgentCommand, options: AgentOperationOptions): Promise<void> {
|
|
117
|
+
const signal = AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : [])])
|
|
118
|
+
signal.throwIfAborted()
|
|
119
|
+
// A failed legacy prompt rebind leaves the old run stopped. Recover its
|
|
120
|
+
// accepted settings before applying the new command on the replacement.
|
|
121
|
+
if (this.needsRebind)
|
|
122
|
+
await this.rebind(this.metadata.model ?? command.settings.model, { signal })
|
|
123
|
+
signal.throwIfAborted()
|
|
124
|
+
// Native settings updates configure the existing app-server thread, including
|
|
125
|
+
// empty threads. Keep the CLI and its warmed state instead of reopening it.
|
|
126
|
+
if (!this.run.changeSettings)
|
|
127
|
+
throw new AgentRunStateError({ message: 'The provider run does not support settings changes.' })
|
|
128
|
+
await this.run.changeSettings(command, { signal })
|
|
129
|
+
this.metadata.model = this.run.metadata.model
|
|
130
|
+
this.reasoningEffort = command.settings.reasoningEffort
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async rebind(model: string | undefined, options: AgentOperationOptions): Promise<void> {
|
|
78
134
|
if (model && (this.needsRebind || model !== this.metadata.model)) {
|
|
79
135
|
if (this.activeTurn)
|
|
80
136
|
throw new AgentPromptDeferredError({ message: 'Model changes require an idle turn boundary.' })
|
|
@@ -88,7 +144,7 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
88
144
|
await this.run.stop({ type: 'agent.stop', ...this.target(), reason: 'model_rebind' }, { signal })
|
|
89
145
|
await this.pump
|
|
90
146
|
signal.throwIfAborted()
|
|
91
|
-
replacement = await this.reopen(model, signal)
|
|
147
|
+
replacement = await this.reopen(model, signal, this.reasoningEffort)
|
|
92
148
|
signal.throwIfAborted()
|
|
93
149
|
if (replacement.metadata.providerSessionId !== this.metadata.providerSessionId || replacement.metadata.sessionId !== this.metadata.sessionId || replacement.metadata.provider !== this.metadata.provider || replacement.metadata.model !== model) {
|
|
94
150
|
identityMismatch = true
|
|
@@ -111,12 +167,6 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
111
167
|
this.changing = false
|
|
112
168
|
}
|
|
113
169
|
}
|
|
114
|
-
this.lifetime.signal.throwIfAborted()
|
|
115
|
-
options.signal?.throwIfAborted()
|
|
116
|
-
const acceptance = await this.run.sendPrompt(command, { ...options, signal: AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : [])]) })
|
|
117
|
-
if (acceptance.kind === 'turn' && acceptance.turnId !== this.lastEndedTurn)
|
|
118
|
-
this.activeTurn = acceptance.turnId
|
|
119
|
-
return acceptance
|
|
120
170
|
}
|
|
121
171
|
|
|
122
172
|
stop: AgentRun['stop'] = async (command, options) => {
|
|
@@ -138,7 +188,7 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
138
188
|
private async withControl<T>(operation: (signal: AbortSignal) => Promise<T>, options: AgentOperationOptions = {}): Promise<T> {
|
|
139
189
|
if (this.stopped)
|
|
140
190
|
throw new AgentRunStateError({ message: 'The run stopped.' })
|
|
141
|
-
if (this.changing || this.needsRebind)
|
|
191
|
+
if (this.settingsChanging || this.changing || this.needsRebind)
|
|
142
192
|
throw new AgentPromptDeferredError({ message: 'The provider model is being rebound.' })
|
|
143
193
|
const signal = AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : [])])
|
|
144
194
|
const task = Promise.resolve().then(async () => {
|
|
@@ -90,10 +90,18 @@ export const respondUserInputAgentCommandSchema = runningCommandBaseSchema.exten
|
|
|
90
90
|
commandId: commandIdSchema,
|
|
91
91
|
requestId: z.string().min(1),
|
|
92
92
|
answers: agentUserInputAnswersSchema,
|
|
93
|
+
settings: agentSettingsChangeSchema.optional(),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
export const changeSettingsAgentCommandSchema = runningCommandBaseSchema.extend({
|
|
97
|
+
type: z.literal('agent.change-settings'),
|
|
98
|
+
commandId: commandIdSchema,
|
|
99
|
+
settings: z.strictObject({ model: agentModelIdSchema, reasoningEffort: agentReasoningEffortSchema }),
|
|
93
100
|
})
|
|
94
101
|
|
|
95
102
|
export const agentCommandSchema = z.union([
|
|
96
103
|
sendPromptCommandSchema,
|
|
104
|
+
changeSettingsAgentCommandSchema,
|
|
97
105
|
agentNotifyCommandSchema,
|
|
98
106
|
interruptAgentCommandSchema,
|
|
99
107
|
compactContextAgentCommandSchema,
|
|
@@ -112,3 +120,5 @@ export type CompactContextAgentCommand = z.infer<typeof compactContextAgentComma
|
|
|
112
120
|
export type StopAgentCommand = z.infer<typeof stopAgentCommandSchema>
|
|
113
121
|
export type RespondUserInputAgentCommand = z.infer<typeof respondUserInputAgentCommandSchema>
|
|
114
122
|
export type AgentCommand = z.infer<typeof agentCommandSchema>
|
|
123
|
+
|
|
124
|
+
export type ChangeSettingsAgentCommand = z.infer<typeof changeSettingsAgentCommandSchema>
|
package/src/provenance.gen.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/write-provenance.ts. Do not edit by hand.
|
|
2
2
|
export const generatedProvenance = {
|
|
3
|
-
version: "0.2.0-experimental.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-10-
|
|
3
|
+
version: "0.2.0-experimental.8",
|
|
4
|
+
sha: "4b63c95e2cc33fde3fbae56b5135006a2afac1e8",
|
|
5
|
+
buildTime: "2026-10-20T14:09:24.027Z",
|
|
6
6
|
} as const
|
|
@@ -176,12 +176,18 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
176
176
|
|
|
177
177
|
async ready(): Promise<void> { await this.recovered.promise }
|
|
178
178
|
|
|
179
|
-
async handle(input: Parameters<SupervisorAgentRuntime['handle']>[0]):
|
|
179
|
+
async handle(input: Parameters<SupervisorAgentRuntime['handle']>[0]): ReturnType<SupervisorAgentRuntime['handle']> {
|
|
180
180
|
this.attachEmitter(input.emit)
|
|
181
181
|
await this.ready()
|
|
182
182
|
if (this.eventFailure)
|
|
183
183
|
throw this.eventFailure
|
|
184
184
|
const metadata = this.input.run.metadata
|
|
185
|
+
if (input.body.type === 'agent.change-settings') {
|
|
186
|
+
if (!this.input.run.changeSettings)
|
|
187
|
+
throw new Error('Runtime does not support standalone settings changes.')
|
|
188
|
+
await this.input.run.changeSettings({ ...metadata, type: 'agent.change-settings', commandId: input.commandId, settings: input.body.settings }, { signal: input.signal })
|
|
189
|
+
return { appliedSettings: input.body.settings }
|
|
190
|
+
}
|
|
185
191
|
if (input.body.type === 'agent.send-prompt') {
|
|
186
192
|
const inputAttachmentPreparation = await this.input.prepareInputAttachments?.({
|
|
187
193
|
commandId: input.commandId,
|
|
@@ -196,6 +202,7 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
196
202
|
commandId: input.commandId,
|
|
197
203
|
mode: input.body.mode,
|
|
198
204
|
turnId: input.body.turnId,
|
|
205
|
+
settings: input.body.settings,
|
|
199
206
|
model: input.body.model,
|
|
200
207
|
reasoningEffort: input.body.reasoningEffort,
|
|
201
208
|
origin: 'user',
|
|
@@ -212,7 +219,13 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
212
219
|
inputAttachmentPreparation,
|
|
213
220
|
senderContext: input.body.user?.senderContext,
|
|
214
221
|
})
|
|
215
|
-
|
|
222
|
+
// Both prompt formats can change later defaults. Omitted legacy fields must
|
|
223
|
+
// not overwrite accepted values, and settings takes whole-object precedence.
|
|
224
|
+
const settings = input.body.settings ?? {
|
|
225
|
+
...(input.body.model === undefined ? {} : { model: input.body.model }),
|
|
226
|
+
...(input.body.reasoningEffort === undefined ? {} : { reasoningEffort: input.body.reasoningEffort }),
|
|
227
|
+
}
|
|
228
|
+
return input.body.settings || Object.keys(settings).length ? { appliedSettings: settings } : undefined
|
|
216
229
|
}
|
|
217
230
|
if (input.body.type === 'agent.interrupt') {
|
|
218
231
|
await this.input.run.interrupt({
|
|
@@ -225,7 +238,7 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
225
238
|
}, { signal: input.signal })
|
|
226
239
|
return
|
|
227
240
|
}
|
|
228
|
-
await this.input.run.respondUserInput({
|
|
241
|
+
const submission = await this.input.run.respondUserInput({
|
|
229
242
|
type: 'agent.respond-user-input',
|
|
230
243
|
provider: metadata.provider,
|
|
231
244
|
sessionId: metadata.sessionId,
|
|
@@ -233,7 +246,10 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
233
246
|
commandId: input.commandId,
|
|
234
247
|
requestId: input.body.requestId,
|
|
235
248
|
answers: input.body.answers,
|
|
249
|
+
settings: input.body.settings,
|
|
236
250
|
}, { signal: input.signal })
|
|
251
|
+
if (submission === 'submitted' && input.body.settings)
|
|
252
|
+
return { appliedSettings: input.body.settings }
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
async refreshSecrets(input: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSettingsChange } from '../../protocol'
|
|
1
2
|
import type { SessionBootstrapBody } from '../../supervisor-protocol/bootstrap'
|
|
2
3
|
import type { CommandEnvelope } from '../../supervisor-protocol/envelopes/control-plane-to-supervisor'
|
|
3
4
|
import type { DurableEventEnvelope } from '../../supervisor-protocol/envelopes/supervisor-to-control-plane'
|
|
@@ -6,6 +7,7 @@ import type { SupervisorAgentRunSnapshot } from '../../supervisor-protocol/super
|
|
|
6
7
|
import { mkdirSync } from 'node:fs'
|
|
7
8
|
import { dirname } from 'node:path'
|
|
8
9
|
import { Database } from 'bun:sqlite'
|
|
10
|
+
import { agentSettingsChangeSchema } from '../../protocol'
|
|
9
11
|
import { sessionBootstrapBodySchema } from '../../supervisor-protocol/bootstrap'
|
|
10
12
|
import { wireCommandBodySchema } from '../../supervisor-protocol/command-body'
|
|
11
13
|
import { wireEventBodySchema } from '../../supervisor-protocol/event-body'
|
|
@@ -215,6 +217,27 @@ export class SupervisorStore {
|
|
|
215
217
|
})()
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
/** Restore accepted defaults in application order; the bootstrap remains immutable. */
|
|
221
|
+
appliedSettings(): AgentSettingsChange | undefined {
|
|
222
|
+
const row = this.database.query<{ value: string }, []>('SELECT value FROM metadata WHERE key = \'applied_settings\'').get()
|
|
223
|
+
return row ? agentSettingsChangeSchema.parse(JSON.parse(row.value)) : undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Commit settings, the outcome and inbox state together, including partial prompt/answer updates. */
|
|
227
|
+
finishCommand(input: { commandSeq: number, body: WireEventBody, appliedSettings?: AgentSettingsChange }): DurableEventEnvelope {
|
|
228
|
+
return this.database.transaction(() => {
|
|
229
|
+
const event = this.appendDurableEvent({ occurredAt: new Date().toISOString(), body: input.body })
|
|
230
|
+
const applied = input.body.type === 'supervisor.command.applied'
|
|
231
|
+
if (applied && input.appliedSettings) {
|
|
232
|
+
const settings = agentSettingsChangeSchema.parse({ ...this.appliedSettings(), ...input.appliedSettings })
|
|
233
|
+
this.database.query(`INSERT INTO metadata (key, value) VALUES ('applied_settings', ?)
|
|
234
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(canonicalJson(settings))
|
|
235
|
+
}
|
|
236
|
+
this.markCommand({ commandSeq: input.commandSeq, status: applied ? 'applied' : 'failed' })
|
|
237
|
+
return event
|
|
238
|
+
})()
|
|
239
|
+
}
|
|
240
|
+
|
|
218
241
|
/** Commit before calling a provider: a process crash makes the outcome uncertain. */
|
|
219
242
|
beginCommandDispatch(commandSeq: number): void {
|
|
220
243
|
this.database.query('INSERT INTO command_dispatches(seq) VALUES (?)').run(commandSeq)
|
|
@@ -228,6 +251,12 @@ export class SupervisorStore {
|
|
|
228
251
|
JOIN command_dispatches ON command_dispatches.seq=commands.seq ORDER BY commands.seq
|
|
229
252
|
`).all()
|
|
230
253
|
for (const row of rows) {
|
|
254
|
+
// Reapplying a full settings pair is idempotent and submits no input. Prompt
|
|
255
|
+
// commands retain the existing uncertain-delivery policy below.
|
|
256
|
+
if (commandBodyFromRow(row).type === 'agent.change-settings') {
|
|
257
|
+
this.database.query('DELETE FROM command_dispatches WHERE seq = ?').run(row.seq)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
231
260
|
this.appendDurableEvent({
|
|
232
261
|
occurredAt: new Date().toISOString(),
|
|
233
262
|
body: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSettingsChange } from '../protocol'
|
|
1
2
|
import type { EphemeralCredentials, SessionBootstrapBody, SessionBootstrapGitToken } from '../supervisor-protocol/bootstrap'
|
|
2
3
|
import type { WireCommandBody } from '../supervisor-protocol/command-body'
|
|
3
4
|
import type { WireEventBody } from '../supervisor-protocol/event-body'
|
|
@@ -13,7 +14,7 @@ export interface SupervisorAgentRuntime {
|
|
|
13
14
|
body: WireCommandBody
|
|
14
15
|
signal: AbortSignal
|
|
15
16
|
emit: (body: WireEventBody) => void
|
|
16
|
-
}) => Promise<void>
|
|
17
|
+
}) => Promise<void | { appliedSettings: AgentSettingsChange }>
|
|
17
18
|
refreshSecrets?: (input: {
|
|
18
19
|
credentials?: EphemeralCredentials
|
|
19
20
|
gitToken?: SessionBootstrapGitToken
|
|
@@ -105,6 +105,7 @@ export async function createResidentSupervisor(input: {
|
|
|
105
105
|
binding: binding.status,
|
|
106
106
|
profile: input.profile,
|
|
107
107
|
agentCore: agentCoreProvenance(),
|
|
108
|
+
capabilities: { 'model-change': 'v1' },
|
|
108
109
|
}),
|
|
109
110
|
getBoundSessionId: () => binding.status === 'bound' ? binding.sessionId : undefined,
|
|
110
111
|
getLocalEventMax: () => binding.status === 'bound' ? binding.store.localEventMax() : 0,
|
|
@@ -207,7 +207,7 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
|
|
|
207
207
|
gitToken: this.dependencies.gitToken,
|
|
208
208
|
}
|
|
209
209
|
this.createAgentTask = this.dependencies.providerFactory.create({
|
|
210
|
-
bootstrap: binding.body,
|
|
210
|
+
bootstrap: { ...binding.body, agent: { ...binding.body.agent, ...this.dependencies.store.appliedSettings() } },
|
|
211
211
|
...initialSecrets,
|
|
212
212
|
rpc: this.rpc,
|
|
213
213
|
signal: this.lifecycleController.signal,
|
|
@@ -240,42 +240,42 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
|
|
|
240
240
|
agent: SupervisorAgentRuntime
|
|
241
241
|
command: CommandEnvelope & { body: WireCommandBody }
|
|
242
242
|
}): Promise<void> {
|
|
243
|
+
this.dependencies.store.beginCommandDispatch(input.command.commandSeq)
|
|
244
|
+
let result: Awaited<ReturnType<SupervisorAgentRuntime['handle']>>
|
|
243
245
|
try {
|
|
244
|
-
|
|
245
|
-
await input.agent.handle({
|
|
246
|
+
result = await input.agent.handle({
|
|
246
247
|
commandId: input.command.commandId,
|
|
247
248
|
commandSeq: input.command.commandSeq,
|
|
248
249
|
body: input.command.body,
|
|
249
250
|
signal: this.lifecycleController.signal,
|
|
250
251
|
emit: body => this.emit(body),
|
|
251
252
|
})
|
|
252
|
-
this.dependencies.store.markCommand({ commandSeq: input.command.commandSeq, status: 'applied' })
|
|
253
|
-
this.emit({
|
|
254
|
-
type: 'supervisor.command.applied',
|
|
255
|
-
payload: {
|
|
256
|
-
commandId: input.command.commandId,
|
|
257
|
-
commandSeq: input.command.commandSeq,
|
|
258
|
-
bodyType: input.command.body.type,
|
|
259
|
-
},
|
|
260
|
-
})
|
|
261
253
|
}
|
|
262
|
-
catch {
|
|
254
|
+
catch (error) {
|
|
263
255
|
if (this.lifecycleController.signal.aborted)
|
|
264
256
|
return
|
|
265
|
-
this.dependencies.
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
payload: {
|
|
257
|
+
this.dependencies.logger.error({ err: error, commandId: input.command.commandId, bodyType: input.command.body.type }, 'Agent command execution failed')
|
|
258
|
+
this.sendEvent(this.dependencies.store.finishCommand({
|
|
259
|
+
commandSeq: input.command.commandSeq,
|
|
260
|
+
body: { type: 'supervisor.command.failed', payload: {
|
|
269
261
|
commandId: input.command.commandId,
|
|
270
262
|
commandSeq: input.command.commandSeq,
|
|
271
263
|
bodyType: input.command.body.type,
|
|
272
|
-
failure: {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
},
|
|
277
|
-
})
|
|
264
|
+
failure: { type: 'command', detail: 'Agent command execution failed.' },
|
|
265
|
+
} },
|
|
266
|
+
}))
|
|
267
|
+
return
|
|
278
268
|
}
|
|
269
|
+
// Persistence and socket errors must not turn an accepted command into a failed one.
|
|
270
|
+
this.sendEvent(this.dependencies.store.finishCommand({
|
|
271
|
+
commandSeq: input.command.commandSeq,
|
|
272
|
+
appliedSettings: result?.appliedSettings,
|
|
273
|
+
body: { type: 'supervisor.command.applied', payload: {
|
|
274
|
+
commandId: input.command.commandId,
|
|
275
|
+
commandSeq: input.command.commandSeq,
|
|
276
|
+
bodyType: input.command.body.type,
|
|
277
|
+
} },
|
|
278
|
+
}))
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
private emit(body: WireEventBody): void {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from 'zod'
|
|
2
|
-
import { agentPromptModeSchema, agentPromptSchema, agentReasoningEffortSchema, agentUserInputAnswersSchema, jsonObjectSchema, jsonUtf8ByteLength } from '../protocol'
|
|
2
|
+
import { agentPromptModeSchema, agentPromptSchema, agentReasoningEffortSchema, agentSettingsChangeSchema, agentUserInputAnswersSchema, jsonObjectSchema, jsonUtf8ByteLength } from '../protocol'
|
|
3
3
|
|
|
4
4
|
const wireSenderContextSchema = z.discriminatedUnion('kind', [
|
|
5
5
|
z.object({ kind: z.literal('application') }).strict(),
|
|
@@ -43,6 +43,7 @@ const wireSendPromptCommandSchema = z.object({
|
|
|
43
43
|
turnId: z.string().min(1).optional(),
|
|
44
44
|
model: z.string().trim().min(1).max(256).optional(),
|
|
45
45
|
reasoningEffort: agentReasoningEffortSchema.optional(),
|
|
46
|
+
settings: agentSettingsChangeSchema.optional(),
|
|
46
47
|
prompt: agentPromptSchema,
|
|
47
48
|
attachments: z.array(wireInputAttachmentRefSchema).superRefine((attachments, context) => {
|
|
48
49
|
const attachmentIds = new Set<string>()
|
|
@@ -69,10 +70,12 @@ const wireRespondUserInputCommandSchema = z.object({
|
|
|
69
70
|
type: z.literal('agent.respond-user-input'),
|
|
70
71
|
requestId: z.string().min(1),
|
|
71
72
|
answers: agentUserInputAnswersSchema,
|
|
73
|
+
settings: agentSettingsChangeSchema.optional(),
|
|
72
74
|
user: wireCommandUserSchema.optional(),
|
|
73
75
|
}).strict()
|
|
74
76
|
|
|
75
77
|
export const wireCommandBodySchema = z.discriminatedUnion('type', [
|
|
78
|
+
z.strictObject({ type: z.literal('agent.change-settings'), settings: z.strictObject({ model: z.string().trim().min(1).max(256), reasoningEffort: agentReasoningEffortSchema }) }),
|
|
76
79
|
wireSendPromptCommandSchema,
|
|
77
80
|
wireInterruptCommandSchema,
|
|
78
81
|
wireRespondUserInputCommandSchema,
|