@meistrari/agent-core 0.1.3 → 0.1.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 +2 -0
- package/package.json +1 -1
- package/scripts/probe-codex-diagnostic-storage.ts +110 -0
- package/src/agents/claude/claude-persisted-output.ts +65 -0
- package/src/agents/claude/claude-provider.ts +10 -0
- package/src/agents/claude/claude-run.ts +43 -3
- package/src/agents/claude/claude-startup-timing.ts +34 -0
- package/src/agents/codex/codex-auth.ts +11 -0
- package/src/agents/codex/codex-bedrock-diagnostics.ts +6 -0
- package/src/agents/codex/codex-bedrock-session-auth.ts +79 -0
- package/src/agents/codex/codex-json-rpc-client.ts +2 -1
- package/src/agents/codex/codex-provider.ts +81 -12
- package/src/agents/codex/codex-run.ts +23 -1
- package/src/agents/model-rebinding-run.ts +166 -0
- package/src/protocol/agent-command.ts +2 -0
- package/src/provenance.gen.ts +3 -3
- package/src/steps/projector.ts +7 -1
- package/src/supervisor/agent-provider-factory.ts +1 -0
- package/src/supervisor-protocol/command-body.ts +1 -0
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ The package ships TypeScript source and targets Bun `>=1.3.10`. There is no wire
|
|
|
21
21
|
|
|
22
22
|
agent-core does not own a universal E2B template. Each consuming service builds and onboards its own template against the shared contracts and records that template's agent-core provenance. agent-api and Remy may therefore use different base images, harness versions, system packages, and startup hooks without forking the control protocol.
|
|
23
23
|
|
|
24
|
+
Prompt commands may carry an application-resolved `model`. Applications validate harness compatibility and persist the selection with command admission; they should send the resolved model on every prompt so recovery does not depend on mutable harness memory. Claude applies a changed model through its control channel before input; Codex supplies it to `turn/start`. Neither adapter changes models on an active turn, and unchanged Claude selections add no control request. This is not an effort override contract. Existing immutable sandboxes must not be advertised as supporting this field unless their baked runtime includes it.
|
|
25
|
+
|
|
24
26
|
## Development
|
|
25
27
|
|
|
26
28
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/agent-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.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",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { mkdtemp, readdir, readFile, stat } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { createInterface } from 'node:readline'
|
|
6
|
+
import { Database } from 'bun:sqlite'
|
|
7
|
+
|
|
8
|
+
// Offline, synthetic-only probe. Never inherit model or AWS credentials.
|
|
9
|
+
const executable = process.argv[2]
|
|
10
|
+
if (!executable)
|
|
11
|
+
throw new Error('Pass the absolute path to the pinned Codex executable.')
|
|
12
|
+
|
|
13
|
+
for (const mode of ['default', 'rust-log-off', 'feedback-off'] as const) {
|
|
14
|
+
const directory = await mkdtemp(join(tmpdir(), `codex-diagnostic-${mode}-`))
|
|
15
|
+
const canary = 'SYNTHETIC_CODEX_DIAGNOSTIC_CANARY_NO_AUTHORITY'
|
|
16
|
+
const child = spawn(executable, [
|
|
17
|
+
'app-server',
|
|
18
|
+
'--listen',
|
|
19
|
+
'stdio://',
|
|
20
|
+
'-c',
|
|
21
|
+
'cli_auth_credentials_store="ephemeral"',
|
|
22
|
+
...(mode === 'feedback-off' ? ['-c', 'feedback.enabled=false'] : []),
|
|
23
|
+
], {
|
|
24
|
+
env: {
|
|
25
|
+
PATH: process.env.PATH,
|
|
26
|
+
HOME: directory,
|
|
27
|
+
CODEX_HOME: directory,
|
|
28
|
+
HTTP_PROXY: 'http://127.0.0.1:1',
|
|
29
|
+
HTTPS_PROXY: 'http://127.0.0.1:1',
|
|
30
|
+
ALL_PROXY: 'http://127.0.0.1:1',
|
|
31
|
+
NO_PROXY: '',
|
|
32
|
+
...(mode === 'rust-log-off' ? { RUST_LOG: 'off' } : {}),
|
|
33
|
+
},
|
|
34
|
+
stdio: 'pipe',
|
|
35
|
+
})
|
|
36
|
+
const exited = new Promise<number | null>((resolve, reject) => {
|
|
37
|
+
child.once('close', resolve)
|
|
38
|
+
child.once('error', reject)
|
|
39
|
+
})
|
|
40
|
+
let stderrHasCanary = false
|
|
41
|
+
child.stderr.on('data', (data) => {
|
|
42
|
+
stderrHasCanary ||= String(data).includes(canary)
|
|
43
|
+
})
|
|
44
|
+
const send = (value: unknown) => child.stdin.write(`${JSON.stringify(value)}\n`)
|
|
45
|
+
let loginResponded = false
|
|
46
|
+
let turnAccepted = false
|
|
47
|
+
const protocolErrors: { id: unknown, code: unknown }[] = []
|
|
48
|
+
const lines = createInterface({ input: child.stdout })
|
|
49
|
+
lines.on('line', (line) => {
|
|
50
|
+
const value = JSON.parse(line)
|
|
51
|
+
if (value.error)
|
|
52
|
+
protocolErrors.push({ id: value.id, code: value.error.code })
|
|
53
|
+
if (value.id === 1) {
|
|
54
|
+
send({ method: 'initialized' })
|
|
55
|
+
send({ id: 2, method: 'account/login/start', params: { type: 'amazonBedrock', apiKey: canary, region: 'us-east-1' } })
|
|
56
|
+
}
|
|
57
|
+
if (value.id === 2) {
|
|
58
|
+
loginResponded = true
|
|
59
|
+
send({ id: 3, method: 'thread/start', params: { modelProvider: 'amazon-bedrock', model: 'openai.gpt-5.6-terra', cwd: directory, approvalPolicy: 'never', sandbox: 'danger-full-access' } })
|
|
60
|
+
}
|
|
61
|
+
if (value.id === 3 && value.result?.thread?.id) {
|
|
62
|
+
send({ id: 4, method: 'turn/start', params: { threadId: value.result.thread.id, input: [{ type: 'text', text: 'Synthetic offline diagnostic probe. Do not use tools.' }] } })
|
|
63
|
+
}
|
|
64
|
+
if (value.id === 4 || value.error) {
|
|
65
|
+
turnAccepted = value.id === 4 && Boolean(value.result?.turn?.id)
|
|
66
|
+
// Allow the runtime's asynchronous diagnostic writer to flush.
|
|
67
|
+
setTimeout(() => child.stdin.end(), 2_000)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'diagnostic-canary-probe', version: '1' }, capabilities: { experimentalApi: true } } })
|
|
71
|
+
const timeout = setTimeout(() => child.kill('SIGTERM'), 10_000)
|
|
72
|
+
const exitCode = await exited
|
|
73
|
+
clearTimeout(timeout)
|
|
74
|
+
lines.close()
|
|
75
|
+
const files = await readdir(directory)
|
|
76
|
+
const logs = files.filter(name => /^logs_\d+\.sqlite$/.test(name))
|
|
77
|
+
const databases = logs.map((name) => {
|
|
78
|
+
// These synthetic-only files belong to this probe. SQLite may need to
|
|
79
|
+
// recreate its WAL shared-memory file after the child exits.
|
|
80
|
+
const db = new Database(join(directory, name), { readwrite: true, create: false })
|
|
81
|
+
try {
|
|
82
|
+
return { name, ...db.query('SELECT count(*) AS records, coalesce(sum(instr(feedback_log_body, ?) > 0), 0) AS canaryRecords FROM logs').get(canary) as { records: number, canaryRecords: number } }
|
|
83
|
+
}
|
|
84
|
+
finally { db.close() }
|
|
85
|
+
})
|
|
86
|
+
const canaryFiles: string[] = []
|
|
87
|
+
const skippedFiles: string[] = []
|
|
88
|
+
let scannedFiles = 0
|
|
89
|
+
async function scan(relative = ''): Promise<void> {
|
|
90
|
+
for (const entry of await readdir(join(directory, relative), { withFileTypes: true })) {
|
|
91
|
+
const path = join(relative, entry.name)
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
await scan(path)
|
|
94
|
+
}
|
|
95
|
+
else if (entry.isFile()) {
|
|
96
|
+
if ((await stat(join(directory, path))).size > 16 * 1024 * 1024) {
|
|
97
|
+
skippedFiles.push(path)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
scannedFiles++
|
|
101
|
+
if ((await readFile(join(directory, path))).includes(canary))
|
|
102
|
+
canaryFiles.push(path)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
await scan()
|
|
107
|
+
console.log(JSON.stringify({ mode, directory, loginResponded, turnAccepted, protocolErrors, exitCode, stderrHasCanary, databases, scannedFiles, canaryFiles, skippedFiles }))
|
|
108
|
+
if (!turnAccepted || protocolErrors.length || exitCode !== 0 || stderrHasCanary || canaryFiles.length || skippedFiles.length)
|
|
109
|
+
process.exitCode = 1
|
|
110
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
import { constants } from 'node:fs'
|
|
4
|
+
import { open, realpath } from 'node:fs/promises'
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
export async function hydrateClaudeToolOutput(message: SDKMessage, input: {
|
|
10
|
+
transcriptPath: string
|
|
11
|
+
toolNameById: ReadonlyMap<string, string>
|
|
12
|
+
}): Promise<SDKMessage> {
|
|
13
|
+
if (message.type !== 'user' || !message.tool_use_result || typeof message.tool_use_result !== 'object')
|
|
14
|
+
return message
|
|
15
|
+
const result = message.tool_use_result as Record<string, unknown>
|
|
16
|
+
if (result.persistedOutputPath === undefined)
|
|
17
|
+
return message
|
|
18
|
+
const content = message.message.content
|
|
19
|
+
if (!Array.isArray(content))
|
|
20
|
+
throw new Error('Claude persisted output has no correlated tool result.')
|
|
21
|
+
const blocks = content.filter(block => block.type === 'tool_result')
|
|
22
|
+
if (blocks.length !== 1 || input.toolNameById.get(blocks[0]!.tool_use_id) !== 'bash')
|
|
23
|
+
throw new Error('Claude persisted output has ambiguous tool correlation.')
|
|
24
|
+
if (typeof result.persistedOutputPath !== 'string' || typeof result.persistedOutputSize !== 'number')
|
|
25
|
+
throw new Error('Claude persisted output metadata is malformed.')
|
|
26
|
+
const text = await readClaudePersistedOutput({ transcriptPath: input.transcriptPath, path: result.persistedOutputPath, byteSize: result.persistedOutputSize })
|
|
27
|
+
return { ...message, message: { ...message.message, content: content.map(block => block === blocks[0] ? { ...block, content: text } : block) } }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read only the SDK-owned tool-results file, never a path parsed from model text. */
|
|
31
|
+
export async function readClaudePersistedOutput(input: {
|
|
32
|
+
transcriptPath: string
|
|
33
|
+
path: string
|
|
34
|
+
byteSize: number
|
|
35
|
+
}): Promise<string> {
|
|
36
|
+
const transcript = resolve(input.transcriptPath)
|
|
37
|
+
const root = join(dirname(transcript), basename(transcript, '.jsonl'), 'tool-results')
|
|
38
|
+
if (!Number.isSafeInteger(input.byteSize) || input.byteSize < 0 || input.byteSize > MAX_OUTPUT_BYTES)
|
|
39
|
+
throw new Error('Claude persisted tool output exceeds the bounded capture budget.')
|
|
40
|
+
if (input.path !== resolve(input.path) || dirname(input.path) !== root || await realpath(root) !== root)
|
|
41
|
+
throw new Error('Claude persisted tool output is outside its provider session.')
|
|
42
|
+
const file = await open(input.path, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
43
|
+
try {
|
|
44
|
+
const before = await file.stat()
|
|
45
|
+
if (!before.isFile() || before.nlink !== 1 || before.size !== input.byteSize)
|
|
46
|
+
throw new Error('Claude persisted tool output metadata does not match its file.')
|
|
47
|
+
// Bounded reads also detect growth between stat and read; readFile could
|
|
48
|
+
// allocate arbitrarily if the file is concurrently extended.
|
|
49
|
+
const bytes = Buffer.alloc(input.byteSize + 1)
|
|
50
|
+
let offset = 0
|
|
51
|
+
while (offset < bytes.length) {
|
|
52
|
+
const result = await file.read(bytes, offset, bytes.length - offset, offset)
|
|
53
|
+
if (!result.bytesRead)
|
|
54
|
+
break
|
|
55
|
+
offset += result.bytesRead
|
|
56
|
+
}
|
|
57
|
+
const after = await file.stat()
|
|
58
|
+
if (offset !== input.byteSize || after.size !== before.size || after.mtimeMs !== before.mtimeMs)
|
|
59
|
+
throw new Error('Claude persisted tool output changed during capture.')
|
|
60
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, offset))
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await file.close()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { AgentOperationOptions } from '../agent-operation'
|
|
|
4
4
|
import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
|
|
5
5
|
import type { AgentTool } from '../agent-tool'
|
|
6
6
|
import type { InstructionComposer } from '../instructions'
|
|
7
|
+
import type { ClaudeStartupTiming } from './claude-startup-timing'
|
|
7
8
|
import { homedir } from 'node:os'
|
|
8
9
|
import { join } from 'node:path'
|
|
9
10
|
import { query as queryClaude } from '@anthropic-ai/claude-agent-sdk'
|
|
@@ -15,6 +16,7 @@ import { removeUndefined } from '../normalize'
|
|
|
15
16
|
import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
|
|
16
17
|
import { createClaudeEventMapperState } from './claude-event-mapper'
|
|
17
18
|
import { ClaudeRun } from './claude-run'
|
|
19
|
+
import { createClaudeStartupTiming, observeClaudeControlInitialization } from './claude-startup-timing'
|
|
18
20
|
import { claudeAllowedToolNames, claudeToolActorHooks, createClaudeToolRuntimeRef, toClaudeMcpServers } from './claude-tool-mapper'
|
|
19
21
|
|
|
20
22
|
export interface ClaudeProviderConfig {
|
|
@@ -22,6 +24,7 @@ export interface ClaudeProviderConfig {
|
|
|
22
24
|
executable?: string
|
|
23
25
|
environment?: Record<string, string | undefined>
|
|
24
26
|
instructions?: InstructionComposer
|
|
27
|
+
onStartupTiming?: (timing: ClaudeStartupTiming) => void
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
@@ -30,12 +33,14 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
30
33
|
private readonly executable: string | undefined
|
|
31
34
|
private readonly environment: Record<string, string | undefined>
|
|
32
35
|
private readonly instructions: InstructionComposer
|
|
36
|
+
private readonly onStartupTiming: ClaudeProviderConfig['onStartupTiming']
|
|
33
37
|
|
|
34
38
|
private constructor(config: ClaudeProviderConfig) {
|
|
35
39
|
this.tools = config.tools ?? []
|
|
36
40
|
this.executable = config.executable
|
|
37
41
|
this.environment = config.environment ?? {}
|
|
38
42
|
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
43
|
+
this.onStartupTiming = config.onStartupTiming
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
static create(config: ClaudeProviderConfig = {}): ClaudeProvider {
|
|
@@ -96,7 +101,11 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
96
101
|
pathToClaudeCodeExecutable: this.executable,
|
|
97
102
|
})
|
|
98
103
|
|
|
104
|
+
const recordStartup = createClaudeStartupTiming(this.onStartupTiming)
|
|
99
105
|
const query = queryClaude({ prompt: inputQueue, options: sdkOptions })
|
|
106
|
+
recordStartup('query_created')
|
|
107
|
+
if (this.onStartupTiming)
|
|
108
|
+
observeClaudeControlInitialization(query.initializationResult(), recordStartup)
|
|
100
109
|
try {
|
|
101
110
|
// Binding metadata comes from pre-input facts only: the live runtime emits system/init
|
|
102
111
|
// only after the first user message, so the run validates identity/config on the first
|
|
@@ -122,6 +131,7 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
122
131
|
userInputRequests,
|
|
123
132
|
input: inputQueue,
|
|
124
133
|
eventMapperState: state,
|
|
134
|
+
recordStartup,
|
|
125
135
|
init: {
|
|
126
136
|
expectedSessionId,
|
|
127
137
|
requestedModel: input.model,
|
|
@@ -13,14 +13,16 @@ import type { AgentOperationOptions } from '../agent-operation'
|
|
|
13
13
|
import type { AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
14
14
|
import type { UserInputRequestInput, UserInputRequestStore } from '../user-input-request'
|
|
15
15
|
import type { ClaudeEventMapperState } from './claude-event-mapper'
|
|
16
|
+
import type { ClaudeStartupPhase } from './claude-startup-timing'
|
|
16
17
|
import z from 'zod'
|
|
17
18
|
import { serializeAgentError } from '../agent-error-serializer'
|
|
18
19
|
import { AgentEventStream } from '../agent-event-stream'
|
|
19
|
-
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentProviderMetadataError, AgentProviderProtocolError, AgentRunStateError } from '../agent-runtime-error'
|
|
20
|
+
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentPromptDeferredError, AgentProviderMetadataError, AgentProviderProtocolError, AgentRunStateError } from '../agent-runtime-error'
|
|
20
21
|
import { sessionConfiguredEvent } from '../agent-session-events'
|
|
21
22
|
import { deriveMessageId } from '../message-id'
|
|
22
23
|
import { mapClaudeUserMessage, toClaudeUserMessage } from './claude-command-mapper'
|
|
23
24
|
import { mapClaudeMessage } from './claude-event-mapper'
|
|
25
|
+
import { hydrateClaudeToolOutput } from './claude-persisted-output'
|
|
24
26
|
|
|
25
27
|
// The ONE long-lived SDK input stream created with the query. SDK 0.3.207 Query.streamInput ends
|
|
26
28
|
// stdin after any finite iterable and the transport silently drops later writes, so every user
|
|
@@ -77,6 +79,7 @@ interface ClaudeRunInput {
|
|
|
77
79
|
input: ClaudeInputStream
|
|
78
80
|
eventMapperState: ClaudeEventMapperState
|
|
79
81
|
init: ClaudeInitExpectation
|
|
82
|
+
recordStartup?: (phase: ClaudeStartupPhase) => void
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
export class ClaudeRun implements AgentRun {
|
|
@@ -87,23 +90,27 @@ export class ClaudeRun implements AgentRun {
|
|
|
87
90
|
private readonly input: ClaudeInputStream
|
|
88
91
|
private readonly eventMapperState: ClaudeEventMapperState
|
|
89
92
|
private readonly init: ClaudeInitExpectation
|
|
93
|
+
private readonly recordStartup: ClaudeRunInput['recordStartup']
|
|
90
94
|
private readonly initialization: Promise<void>
|
|
91
95
|
private initializationResolve: () => void = () => undefined
|
|
92
96
|
private initializationReject: (error: unknown) => void = () => undefined
|
|
93
97
|
private initialized = false
|
|
94
98
|
private firstInputPushed = false
|
|
99
|
+
private modelChangeReady = true
|
|
100
|
+
private modelChangeInFlight = false
|
|
95
101
|
private finished = false
|
|
96
102
|
private interruptRequest?: { turnId: string, promise: Promise<void> }
|
|
97
103
|
private readonly acceptanceWaiters = new Map<string, AcceptanceWaiter>()
|
|
98
104
|
private readonly commandByClientMessageId = new Map<string, string>()
|
|
99
105
|
|
|
100
|
-
constructor({ metadata, query, userInputRequests, input, eventMapperState, init }: ClaudeRunInput) {
|
|
106
|
+
constructor({ metadata, query, userInputRequests, input, eventMapperState, init, recordStartup }: ClaudeRunInput) {
|
|
101
107
|
this.metadata = metadata
|
|
102
108
|
this.query = query
|
|
103
109
|
this.userInputRequests = userInputRequests
|
|
104
110
|
this.input = input
|
|
105
111
|
this.eventMapperState = eventMapperState
|
|
106
112
|
this.init = init
|
|
113
|
+
this.recordStartup = recordStartup
|
|
107
114
|
this.initialization = new Promise<void>((resolve, reject) => {
|
|
108
115
|
this.initializationResolve = resolve
|
|
109
116
|
this.initializationReject = reject
|
|
@@ -130,6 +137,10 @@ export class ClaudeRun implements AgentRun {
|
|
|
130
137
|
pushEvent(draft: AgentEventDraft): void {
|
|
131
138
|
if (this.finished)
|
|
132
139
|
return
|
|
140
|
+
if (draft.type === 'turn.started')
|
|
141
|
+
this.modelChangeReady = false
|
|
142
|
+
if (draft.type === 'session.state.changed' && draft.payload.state === 'idle')
|
|
143
|
+
this.modelChangeReady = true
|
|
133
144
|
const decorated = this.userInputRequests.decorateTurnEnded(draft)
|
|
134
145
|
if (decorated.type === 'session.state.changed' && decorated.payload.state === 'idle' && this.userInputRequests.shouldHoldIdle()) {
|
|
135
146
|
this.events.push({ type: 'session.state.changed', payload: { state: 'waiting_for_user_input' } })
|
|
@@ -161,6 +172,26 @@ export class ClaudeRun implements AgentRun {
|
|
|
161
172
|
const input = { prompt: command.prompt, mode: command.mode, commandId: command.commandId, origin: command.origin, author: command.author, senderContext: options.senderContext }
|
|
162
173
|
const message = await mapClaudeUserMessage(input, options.inputAttachmentPreparation)
|
|
163
174
|
options.signal?.throwIfAborted()
|
|
175
|
+
if (this.modelChangeInFlight)
|
|
176
|
+
throw new AgentPromptDeferredError({ message: 'Claude model configuration is still in progress.' })
|
|
177
|
+
const currentModel = this.init.requestedModel ?? this.metadata.model
|
|
178
|
+
if (command.model && command.model !== currentModel) {
|
|
179
|
+
if (!this.modelChangeReady || this.eventMapperState.currentTurnId || this.commandByClientMessageId.size > 0)
|
|
180
|
+
throw new AgentPromptDeferredError({ message: 'Claude model changes require an idle turn boundary.' })
|
|
181
|
+
// The control receipt must precede the input. Reapplying the same resolved model
|
|
182
|
+
// after a crash is safe; never retry an uncertain prompt to retry configuration.
|
|
183
|
+
this.modelChangeInFlight = true
|
|
184
|
+
try {
|
|
185
|
+
await this.query.setModel(command.model)
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
this.modelChangeInFlight = false
|
|
189
|
+
}
|
|
190
|
+
this.metadata.model = command.model
|
|
191
|
+
this.init.requestedModel = command.model
|
|
192
|
+
options.signal?.throwIfAborted()
|
|
193
|
+
}
|
|
194
|
+
this.modelChangeReady = false
|
|
164
195
|
// Register before the push: the CLI's queued receipt can outrun the local push resolution,
|
|
165
196
|
// and a receipt with no registered waiter would be lost.
|
|
166
197
|
const clientMessageId = deriveMessageId(command.commandId)
|
|
@@ -374,6 +405,7 @@ export class ClaudeRun implements AgentRun {
|
|
|
374
405
|
private async pushOwnedStream(message: SDKUserMessage): Promise<void> {
|
|
375
406
|
try {
|
|
376
407
|
await this.input.push(message)
|
|
408
|
+
this.recordStartup?.('first_input_queued')
|
|
377
409
|
}
|
|
378
410
|
catch (error) {
|
|
379
411
|
if (error instanceof AgentRunStateError) {
|
|
@@ -401,7 +433,13 @@ export class ClaudeRun implements AgentRun {
|
|
|
401
433
|
this.completeInitialization(message)
|
|
402
434
|
continue
|
|
403
435
|
}
|
|
404
|
-
|
|
436
|
+
const completeMessage = message.type === 'user'
|
|
437
|
+
? await hydrateClaudeToolOutput(message, {
|
|
438
|
+
transcriptPath: this.metadata.transcriptPath,
|
|
439
|
+
toolNameById: this.eventMapperState.toolNameById,
|
|
440
|
+
})
|
|
441
|
+
: message
|
|
442
|
+
for (const draft of mapClaudeMessage(completeMessage, this.eventMapperState)) this.pushEvent(draft)
|
|
405
443
|
}
|
|
406
444
|
// An intentional stop already finalized the run; the ended stream is expected then.
|
|
407
445
|
if (this.finished)
|
|
@@ -437,6 +475,7 @@ export class ClaudeRun implements AgentRun {
|
|
|
437
475
|
if (!effectiveModel) {
|
|
438
476
|
throw new AgentProviderMetadataError({ message: 'Claude SDK init did not include model.', details: { provider: 'claude' } })
|
|
439
477
|
}
|
|
478
|
+
this.metadata.model = effectiveModel
|
|
440
479
|
|
|
441
480
|
this.pushEvent({ type: 'session.started', payload: this.metadata })
|
|
442
481
|
this.pushEvent(sessionConfiguredEvent({
|
|
@@ -449,6 +488,7 @@ export class ClaudeRun implements AgentRun {
|
|
|
449
488
|
this.pushEvent({ type: 'session.state.changed', payload: { state: 'ready' } })
|
|
450
489
|
for (const draft of mapClaudeMessage(init, this.eventMapperState)) this.pushEvent(draft)
|
|
451
490
|
this.initialized = true
|
|
491
|
+
this.recordStartup?.('session_ready')
|
|
452
492
|
this.initializationResolve()
|
|
453
493
|
}
|
|
454
494
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ClaudeStartupPhase = 'query_created' | 'control_ready' | 'control_failed' | 'first_input_queued' | 'session_ready'
|
|
2
|
+
|
|
3
|
+
export interface ClaudeStartupTiming {
|
|
4
|
+
phase: ClaudeStartupPhase
|
|
5
|
+
elapsedMs: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Payload-free diagnostics only; never becomes an execution readiness barrier. */
|
|
9
|
+
export function createClaudeStartupTiming(
|
|
10
|
+
observer?: (timing: ClaudeStartupTiming) => void,
|
|
11
|
+
now: () => number = () => performance.now(),
|
|
12
|
+
): (phase: ClaudeStartupPhase) => void {
|
|
13
|
+
const started = now()
|
|
14
|
+
const seen = new Set<ClaudeStartupPhase>()
|
|
15
|
+
return (phase) => {
|
|
16
|
+
if (!observer || seen.has(phase))
|
|
17
|
+
return
|
|
18
|
+
seen.add(phase)
|
|
19
|
+
try {
|
|
20
|
+
observer({ phase, elapsedMs: Math.max(0, now() - started) })
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Observability must not reject a command or stop the provider stream.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function observeClaudeControlInitialization(
|
|
29
|
+
initialization: Promise<unknown>,
|
|
30
|
+
record: (phase: ClaudeStartupPhase) => void,
|
|
31
|
+
): void {
|
|
32
|
+
// Discard account/configuration response and error bodies, which may be sensitive.
|
|
33
|
+
void initialization.then(() => record('control_ready'), () => record('control_failed'))
|
|
34
|
+
}
|
|
@@ -29,6 +29,17 @@ export interface CodexApiKeyAuthProvider {
|
|
|
29
29
|
getApiKey: (input?: CodexAuthTokenRequest) => Promise<CodexApiKeyCredentials>
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface CodexBedrockCredentials {
|
|
33
|
+
bearerToken: string
|
|
34
|
+
expiresAt: number
|
|
35
|
+
generation: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CodexBedrockAuthProvider {
|
|
39
|
+
getCredentials: (input?: CodexAuthTokenRequest) => Promise<CodexBedrockCredentials>
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
export type CodexAuthentication
|
|
33
43
|
= | { kind: 'chatgpt', provider: CodexAuthProvider }
|
|
34
44
|
| { kind: 'api-key', provider: CodexApiKeyAuthProvider, modelProvider?: string }
|
|
45
|
+
| { kind: 'amazon-bedrock', region: string, provider: CodexBedrockAuthProvider }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Raw runtime diagnostics can contain bearer credentials and request bodies. */
|
|
2
|
+
export function bedrockDiagnosticMessage(source: 'stderr' | 'invalid-json-rpc'): string {
|
|
3
|
+
return source === 'stderr'
|
|
4
|
+
? 'Codex emitted a provider diagnostic; raw content withheld.'
|
|
5
|
+
: 'Codex emitted an invalid JSON-RPC line; raw content withheld.'
|
|
6
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CodexBedrockAuthProvider, CodexBedrockCredentials } from './codex-auth'
|
|
2
|
+
import { AgentPromptDeferredError, AgentProviderRuntimeError } from '../agent-runtime-error'
|
|
3
|
+
|
|
4
|
+
export function validateBedrockCredentials(credentials: CodexBedrockCredentials, now = Date.now()): void {
|
|
5
|
+
if (!credentials.bearerToken || !Number.isFinite(credentials.expiresAt) || credentials.expiresAt <= now
|
|
6
|
+
|| !Number.isSafeInteger(credentials.generation) || credentials.generation < 1) {
|
|
7
|
+
throw new AgentProviderRuntimeError({ message: 'Codex Bedrock credentials are invalid or expired.' })
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Updates authentication only; never dispatches or retries a provider prompt. */
|
|
12
|
+
export class CodexBedrockSessionAuth {
|
|
13
|
+
private current: CodexBedrockCredentials
|
|
14
|
+
private pending: Promise<void> | undefined
|
|
15
|
+
private terminalFailure: Error | undefined
|
|
16
|
+
|
|
17
|
+
constructor(private readonly input: {
|
|
18
|
+
initial: CodexBedrockCredentials
|
|
19
|
+
provider: CodexBedrockAuthProvider
|
|
20
|
+
login: (credentials: CodexBedrockCredentials) => Promise<void>
|
|
21
|
+
isActive: () => boolean
|
|
22
|
+
now?: () => number
|
|
23
|
+
refreshTimeoutMs?: number
|
|
24
|
+
}) {
|
|
25
|
+
this.current = { ...input.initial }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async beforePrompt(signal?: AbortSignal): Promise<void> {
|
|
29
|
+
signal?.throwIfAborted()
|
|
30
|
+
if (this.terminalFailure)
|
|
31
|
+
throw this.terminalFailure
|
|
32
|
+
const now = this.input.now ?? Date.now
|
|
33
|
+
if (this.current.expiresAt - now() > 5 * 60_000)
|
|
34
|
+
return
|
|
35
|
+
if (this.input.isActive())
|
|
36
|
+
throw new AgentPromptDeferredError({ message: 'Bedrock credential refresh requires an idle turn boundary.' })
|
|
37
|
+
this.pending ??= this.boundedRefresh(now).finally(() => {
|
|
38
|
+
this.pending = undefined
|
|
39
|
+
})
|
|
40
|
+
await this.pending
|
|
41
|
+
signal?.throwIfAborted()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async boundedRefresh(now: () => number): Promise<void> {
|
|
45
|
+
const abort = new AbortController()
|
|
46
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
47
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
this.terminalFailure = new AgentProviderRuntimeError({ message: 'Bedrock authentication refresh timed out; the provider connection must be reopened before dispatch.' })
|
|
50
|
+
abort.abort(this.terminalFailure)
|
|
51
|
+
reject(this.terminalFailure)
|
|
52
|
+
}, this.input.refreshTimeoutMs ?? 30_000)
|
|
53
|
+
})
|
|
54
|
+
try {
|
|
55
|
+
await Promise.race([this.refresh(now, abort.signal), deadline])
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearTimeout(timer)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async refresh(now: () => number, signal: AbortSignal): Promise<void> {
|
|
63
|
+
// A caller abort must not cancel refresh shared by other pending callers.
|
|
64
|
+
const credentials = await this.input.provider.getCredentials({ signal })
|
|
65
|
+
signal.throwIfAborted()
|
|
66
|
+
validateBedrockCredentials(credentials, now())
|
|
67
|
+
if (credentials.expiresAt - now() <= 5 * 60_000)
|
|
68
|
+
throw new AgentProviderRuntimeError({ message: 'Bedrock credentials expire inside the refresh window.' })
|
|
69
|
+
if (credentials.generation < this.current.generation
|
|
70
|
+
|| (credentials.generation === this.current.generation && (credentials.bearerToken !== this.current.bearerToken || credentials.expiresAt !== this.current.expiresAt))) {
|
|
71
|
+
throw new AgentProviderRuntimeError({ message: 'Bedrock credential generation is stale or conflicting.' })
|
|
72
|
+
}
|
|
73
|
+
if (this.input.isActive())
|
|
74
|
+
throw new AgentPromptDeferredError({ message: 'Bedrock credential refresh raced an active turn.' })
|
|
75
|
+
await this.input.login(credentials)
|
|
76
|
+
signal.throwIfAborted()
|
|
77
|
+
this.current = { ...credentials }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -70,12 +70,13 @@ export class CodexJsonRpcClient {
|
|
|
70
70
|
options: {
|
|
71
71
|
cwd: string
|
|
72
72
|
environment?: Record<string, string | undefined>
|
|
73
|
+
configOverrides?: readonly string[]
|
|
73
74
|
onMessage: (message: CodexJsonRpcMessage) => void
|
|
74
75
|
onInvalidLine: (line: string, error: unknown) => void
|
|
75
76
|
onStderr: (line: string) => void
|
|
76
77
|
},
|
|
77
78
|
) {
|
|
78
|
-
this.child = spawn(executable, ['app-server', '--listen', 'stdio://'], {
|
|
79
|
+
this.child = spawn(executable, ['app-server', '--listen', 'stdio://', ...(options.configOverrides ?? []).flatMap(value => ['-c', value])], {
|
|
79
80
|
cwd: options.cwd,
|
|
80
81
|
env: options.environment,
|
|
81
82
|
stdio: 'pipe',
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AgentReasoningEffort, AgentToolDefinition, CodexModelId } from '../../protocol'
|
|
2
2
|
import type { AgentOperationOptions } from '../agent-operation'
|
|
3
3
|
import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
|
|
4
|
+
import type { AgentRun } from '../agent-run'
|
|
4
5
|
import type { AgentTool } from '../agent-tool'
|
|
5
6
|
import type { InstructionComposer } from '../instructions'
|
|
6
|
-
import type { CodexAuthentication, CodexAuthProvider, CodexAuthRefreshInput, CodexAuthTokens } from './codex-auth'
|
|
7
|
+
import type { CodexAuthentication, CodexAuthProvider, CodexAuthRefreshInput, CodexAuthTokens, CodexBedrockCredentials } from './codex-auth'
|
|
7
8
|
import type { CodexEventMapperState, CodexNotification } from './codex-event-mapper'
|
|
8
9
|
import type { CodexJsonRpcMessage, CodexServerRequest } from './codex-json-rpc-client'
|
|
9
10
|
import type { ClientRequestResponsesByMethod, CodexChatGPTAuthTokensRefreshParams } from './codex-protocol'
|
|
@@ -16,7 +17,10 @@ import { AgentNotAcceptedError, AgentProviderMetadataError, AgentProviderProtoco
|
|
|
16
17
|
import { sessionConfiguredEvent } from '../agent-session-events'
|
|
17
18
|
import { selectAgentTools, toolByName } from '../agent-tool'
|
|
18
19
|
import { emptyInstructionComposer } from '../instructions'
|
|
20
|
+
import { ModelRebindingRun } from '../model-rebinding-run'
|
|
19
21
|
import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
|
|
22
|
+
import { bedrockDiagnosticMessage } from './codex-bedrock-diagnostics'
|
|
23
|
+
import { CodexBedrockSessionAuth, validateBedrockCredentials } from './codex-bedrock-session-auth'
|
|
20
24
|
import { cacheCodexThreadMetadata, codexNotificationFromMessage, createCodexEventMapperState, mapCodexNotification } from './codex-event-mapper'
|
|
21
25
|
import { CodexJsonRpcClient, isCodexServerRequest } from './codex-json-rpc-client'
|
|
22
26
|
import { CodexRun } from './codex-run'
|
|
@@ -38,17 +42,21 @@ export interface CodexProviderConfig {
|
|
|
38
42
|
environment?: Record<string, string | undefined>
|
|
39
43
|
onStartupStage?: (fact: CodexStartupStageFact) => void
|
|
40
44
|
instructions?: InstructionComposer
|
|
45
|
+
/** Service-owned public model to inference model mapping. */
|
|
46
|
+
resolveInferenceModel?: (model: string) => string
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
interface CodexRunHolder {
|
|
44
50
|
toolsByName: Map<string, AgentTool>
|
|
45
51
|
run?: CodexRun
|
|
46
52
|
authTokens?: CodexAuthTokens
|
|
53
|
+
bedrockAuth?: CodexBedrockSessionAuth
|
|
47
54
|
startupFailure?: Error
|
|
48
55
|
}
|
|
49
56
|
type CodexAuthMaterial
|
|
50
57
|
= | { kind: 'chatgpt', tokens: CodexAuthTokens }
|
|
51
58
|
| { kind: 'api-key', apiKey: string }
|
|
59
|
+
| { kind: 'amazon-bedrock', region: string, credentials: CodexBedrockCredentials }
|
|
52
60
|
interface CodexConnection {
|
|
53
61
|
client: CodexJsonRpcClient
|
|
54
62
|
abortController: AbortController
|
|
@@ -78,6 +86,8 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
78
86
|
private readonly auth: CodexAuthentication
|
|
79
87
|
private readonly onStartupStage: ((fact: CodexStartupStageFact) => void) | undefined
|
|
80
88
|
private readonly instructions: InstructionComposer
|
|
89
|
+
private readonly resolveInferenceModel: (model: string) => string
|
|
90
|
+
private readonly hasInferenceModelMapping: boolean
|
|
81
91
|
|
|
82
92
|
private constructor(config: CodexProviderConfig) {
|
|
83
93
|
this.auth = normalizeCodexAuthentication(config.auth)
|
|
@@ -86,21 +96,23 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
86
96
|
this.environment = config.environment ?? {}
|
|
87
97
|
this.onStartupStage = config.onStartupStage
|
|
88
98
|
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
99
|
+
this.resolveInferenceModel = config.resolveInferenceModel ?? (model => model)
|
|
100
|
+
this.hasInferenceModelMapping = config.resolveInferenceModel !== undefined
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
static create(config: CodexProviderConfig): CodexProvider {
|
|
92
104
|
return new CodexProvider(config)
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
async openSession(input: AgentSessionOpenInput<'codex'>, options: AgentOperationOptions = {}): Promise<
|
|
96
|
-
|
|
107
|
+
async openSession(input: AgentSessionOpenInput<'codex'>, options: AgentOperationOptions = {}): Promise<AgentRun> {
|
|
108
|
+
const run = await this.startSession({
|
|
97
109
|
config: input,
|
|
98
110
|
options,
|
|
99
111
|
stage: 'thread-start',
|
|
100
112
|
requestThread: async ({ client, tools }): Promise<CodexThreadBootstrapResult> => await client.request('thread/start', {
|
|
101
113
|
cwd: input.cwd,
|
|
102
|
-
model: input.model,
|
|
103
|
-
modelProvider: this.
|
|
114
|
+
model: input.model ? this.resolveInferenceModel(input.model) : undefined,
|
|
115
|
+
modelProvider: this.modelProvider(),
|
|
104
116
|
ephemeral: false,
|
|
105
117
|
approvalPolicy: 'never',
|
|
106
118
|
sandbox: 'danger-full-access',
|
|
@@ -108,9 +120,26 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
108
120
|
dynamicTools: toCodexDynamicTools(tools),
|
|
109
121
|
}),
|
|
110
122
|
})
|
|
123
|
+
return this.wrapBedrockRun(run, input)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async resumeSession(input: AgentSessionResumeInput<'codex'>, options: AgentOperationOptions = {}): Promise<AgentRun> {
|
|
127
|
+
return this.wrapBedrockRun(await this.resumeRawSession(input, options), input)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private wrapBedrockRun(run: CodexRun, input: AgentSessionOpenInput<'codex'>): AgentRun {
|
|
131
|
+
if (this.auth.kind !== 'amazon-bedrock')
|
|
132
|
+
return run
|
|
133
|
+
const roots = new Set(input.workspaceRepositoryRoots)
|
|
134
|
+
return new ModelRebindingRun(run, async (model, signal) => await this.resumeRawSession({
|
|
135
|
+
...input,
|
|
136
|
+
model: codexModelIdSchema.parse(model),
|
|
137
|
+
providerSessionId: run.metadata.providerSessionId,
|
|
138
|
+
workspaceRepositoryRoots: [...roots],
|
|
139
|
+
}, { signal }), root => roots.add(root))
|
|
111
140
|
}
|
|
112
141
|
|
|
113
|
-
async
|
|
142
|
+
private async resumeRawSession(input: AgentSessionResumeInput<'codex'>, options: AgentOperationOptions = {}): Promise<CodexRun> {
|
|
114
143
|
return await this.startSession({
|
|
115
144
|
config: input,
|
|
116
145
|
options,
|
|
@@ -120,8 +149,8 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
120
149
|
cwd: input.cwd,
|
|
121
150
|
approvalPolicy: 'never',
|
|
122
151
|
sandbox: 'danger-full-access',
|
|
123
|
-
model: input.model,
|
|
124
|
-
modelProvider: this.
|
|
152
|
+
model: input.model ? this.resolveInferenceModel(input.model) : undefined,
|
|
153
|
+
modelProvider: this.modelProvider(),
|
|
125
154
|
developerInstructions: this.instructions({ provider: 'codex', tools }),
|
|
126
155
|
}),
|
|
127
156
|
validateThread: (result) => {
|
|
@@ -320,6 +349,12 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
320
349
|
}
|
|
321
350
|
|
|
322
351
|
private async getAuthMaterial(signal: AbortSignal): Promise<CodexAuthMaterial> {
|
|
352
|
+
if (this.auth.kind === 'amazon-bedrock') {
|
|
353
|
+
const credentials = await this.auth.provider.getCredentials({ signal })
|
|
354
|
+
signal.throwIfAborted()
|
|
355
|
+
validateBedrockCredentials(credentials)
|
|
356
|
+
return { kind: 'amazon-bedrock', region: this.auth.region, credentials }
|
|
357
|
+
}
|
|
323
358
|
if (this.auth.kind === 'api-key') {
|
|
324
359
|
const credentials = await this.auth.provider.getApiKey({ signal })
|
|
325
360
|
return { kind: 'api-key', apiKey: credentials.apiKey }
|
|
@@ -328,6 +363,10 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
328
363
|
return { kind: 'chatgpt', tokens }
|
|
329
364
|
}
|
|
330
365
|
|
|
366
|
+
private modelProvider(): string | undefined {
|
|
367
|
+
return this.auth.kind === 'amazon-bedrock' ? 'amazon-bedrock' : this.auth.kind === 'api-key' ? this.auth.modelProvider : undefined
|
|
368
|
+
}
|
|
369
|
+
|
|
331
370
|
private async openConnection(input: {
|
|
332
371
|
sessionId: string
|
|
333
372
|
cwd: string
|
|
@@ -347,11 +386,19 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
347
386
|
client = new CodexJsonRpcClient(this.executable, {
|
|
348
387
|
cwd,
|
|
349
388
|
environment: this.environment,
|
|
389
|
+
configOverrides: this.auth.kind === 'amazon-bedrock'
|
|
390
|
+
? ['cli_auth_credentials_store="ephemeral"', 'model_provider="amazon-bedrock"', `model_providers.amazon-bedrock.aws.region=${JSON.stringify(this.auth.region)}`, 'model_providers.amazon-bedrock.aws.wire_api="responses"']
|
|
391
|
+
: undefined,
|
|
350
392
|
onMessage: (message) => {
|
|
351
393
|
messageQueue = this.enqueueMessageHandling(messageQueue, { message, holder, state, client, signal: abortController.signal })
|
|
352
394
|
},
|
|
353
|
-
onInvalidLine: (line, error) => holder.run?.pushEvent({
|
|
354
|
-
|
|
395
|
+
onInvalidLine: (line, error) => holder.run?.pushEvent({
|
|
396
|
+
type: 'error',
|
|
397
|
+
payload: this.auth.kind === 'amazon-bedrock'
|
|
398
|
+
? { message: bedrockDiagnosticMessage('invalid-json-rpc'), fatal: false, source: 'transport' }
|
|
399
|
+
: { message: `Invalid Codex JSON-RPC line: ${line.slice(0, 200)}`, fatal: false, source: 'transport', error: serializeAgentError(error) },
|
|
400
|
+
}),
|
|
401
|
+
onStderr: line => holder.run?.pushEvent({ type: 'error', payload: { message: this.auth.kind === 'amazon-bedrock' ? bedrockDiagnosticMessage('stderr') : line, fatal: false, source: 'provider' } }),
|
|
355
402
|
})
|
|
356
403
|
}
|
|
357
404
|
catch (error) {
|
|
@@ -483,12 +530,15 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
483
530
|
})
|
|
484
531
|
}
|
|
485
532
|
|
|
533
|
+
if (this.hasInferenceModelMapping && config.model && result.model && result.model !== this.resolveInferenceModel(config.model))
|
|
534
|
+
throw new AgentProviderProtocolError({ message: 'Codex returned a model that does not match the configured inference binding.' })
|
|
535
|
+
|
|
486
536
|
const metadata = {
|
|
487
537
|
provider: 'codex' as const,
|
|
488
538
|
sessionId: config.sessionId,
|
|
489
539
|
providerSessionId: thread.id,
|
|
490
540
|
cwd: thread.cwd ?? config.cwd,
|
|
491
|
-
model: firstCodexModelId(effectiveModel),
|
|
541
|
+
model: firstCodexModelId(this.hasInferenceModelMapping ? config.model ?? effectiveModel : effectiveModel),
|
|
492
542
|
transcriptPath: thread.path,
|
|
493
543
|
}
|
|
494
544
|
throwIfStartupFailed(holder)
|
|
@@ -507,8 +557,10 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
507
557
|
])
|
|
508
558
|
startupSignal?.throwIfAborted()
|
|
509
559
|
const run = new CodexRun({
|
|
560
|
+
beforePrompt: holder.bedrockAuth ? signal => holder.bedrockAuth!.beforePrompt(signal) : undefined,
|
|
510
561
|
metadata,
|
|
511
562
|
reasoningEffort: config.reasoningEffort,
|
|
563
|
+
resolveInferenceModel: this.resolveInferenceModel,
|
|
512
564
|
client,
|
|
513
565
|
requestedCwd: config.cwd,
|
|
514
566
|
workspaceRepositoryRoots: config.workspaceRepositoryRoots,
|
|
@@ -529,7 +581,7 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
529
581
|
holder.run = run
|
|
530
582
|
|
|
531
583
|
run.pushEvent({ type: 'session.started', payload: run.metadata })
|
|
532
|
-
run.pushEvent(sessionConfiguredEvent({ model: effectiveModel, cwd: run.metadata.cwd, reasoningEffort: config.reasoningEffort, tools: config.tools, environment: this.environment }))
|
|
584
|
+
run.pushEvent(sessionConfiguredEvent({ model: run.metadata.model ?? effectiveModel, cwd: run.metadata.cwd, reasoningEffort: config.reasoningEffort, tools: config.tools, environment: this.environment }))
|
|
533
585
|
run.pushEvent({ type: 'session.state.changed', payload: { state: 'ready' } })
|
|
534
586
|
return run
|
|
535
587
|
}
|
|
@@ -615,6 +667,23 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
615
667
|
}): Promise<void> {
|
|
616
668
|
const { client, holder, authMaterial } = input
|
|
617
669
|
throwIfStartupFailed(holder)
|
|
670
|
+
if (authMaterial.kind === 'amazon-bedrock') {
|
|
671
|
+
if (authMaterial.credentials.expiresAt <= Date.now())
|
|
672
|
+
throw new AgentProviderRuntimeError({ message: 'Codex Bedrock credentials expired before login.' })
|
|
673
|
+
await client.request('account/login/start', { type: 'amazonBedrock', apiKey: authMaterial.credentials.bearerToken, region: authMaterial.region })
|
|
674
|
+
throwIfStartupFailed(holder)
|
|
675
|
+
if (this.auth.kind !== 'amazon-bedrock')
|
|
676
|
+
throw new AgentProviderProtocolError({ message: 'Bedrock authentication mode changed during login.' })
|
|
677
|
+
holder.bedrockAuth = new CodexBedrockSessionAuth({
|
|
678
|
+
initial: authMaterial.credentials,
|
|
679
|
+
provider: this.auth.provider,
|
|
680
|
+
isActive: () => holder.run?.hasActiveTurn() ?? false,
|
|
681
|
+
login: async (credentials) => {
|
|
682
|
+
await client.request('account/login/start', { type: 'amazonBedrock', apiKey: credentials.bearerToken, region: authMaterial.region })
|
|
683
|
+
},
|
|
684
|
+
})
|
|
685
|
+
return
|
|
686
|
+
}
|
|
618
687
|
if (authMaterial.kind === 'api-key') {
|
|
619
688
|
await client.request('account/login/start', { type: 'apiKey', apiKey: authMaterial.apiKey })
|
|
620
689
|
throwIfStartupFailed(holder)
|
|
@@ -35,9 +35,12 @@ interface PreparedCodexTurn {
|
|
|
35
35
|
input: CodexUserInput[]
|
|
36
36
|
commandId: string
|
|
37
37
|
origin: 'user' | 'supervisor'
|
|
38
|
+
model?: string
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
interface CodexRunInput {
|
|
42
|
+
beforePrompt?: (signal?: AbortSignal) => Promise<void>
|
|
43
|
+
resolveInferenceModel?: (model: string) => string
|
|
41
44
|
metadata: AgentRunMetadata
|
|
42
45
|
reasoningEffort?: AgentReasoningEffort
|
|
43
46
|
client: CodexJsonRpcClient
|
|
@@ -56,6 +59,8 @@ export class CodexRun implements AgentRun {
|
|
|
56
59
|
readonly events: AgentEventStream
|
|
57
60
|
readonly metadata: AgentRunMetadata
|
|
58
61
|
private readonly client: CodexJsonRpcClient
|
|
62
|
+
private readonly beforePrompt: ((signal?: AbortSignal) => Promise<void>) | undefined
|
|
63
|
+
private readonly resolveInferenceModel: (model: string) => string
|
|
59
64
|
private readonly reasoningEffort: AgentReasoningEffort | undefined
|
|
60
65
|
private readonly onStop: () => void
|
|
61
66
|
private readonly userInputRequests: UserInputRequestStore
|
|
@@ -74,6 +79,8 @@ export class CodexRun implements AgentRun {
|
|
|
74
79
|
constructor(input: CodexRunInput) {
|
|
75
80
|
this.metadata = input.metadata
|
|
76
81
|
this.client = input.client
|
|
82
|
+
this.beforePrompt = input.beforePrompt
|
|
83
|
+
this.resolveInferenceModel = input.resolveInferenceModel ?? (model => model)
|
|
77
84
|
this.reasoningEffort = input.reasoningEffort
|
|
78
85
|
this.onStop = input.onStop
|
|
79
86
|
this.userInputRequests = input.userInputRequests
|
|
@@ -225,6 +232,10 @@ export class CodexRun implements AgentRun {
|
|
|
225
232
|
// and answering a phantom turn id, so the next-vs-active race must be refused from adapter
|
|
226
233
|
// state before any request — nothing is prepared or retained.
|
|
227
234
|
this.deferNextWhileTurnActive(command.mode)
|
|
235
|
+
if (this.beforePrompt)
|
|
236
|
+
await this.beforePrompt(options.signal)
|
|
237
|
+
this.assertRunning()
|
|
238
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
228
239
|
const prepared = await this.prepareTurn({
|
|
229
240
|
prompt: command.prompt,
|
|
230
241
|
commandId: command.commandId,
|
|
@@ -232,10 +243,13 @@ export class CodexRun implements AgentRun {
|
|
|
232
243
|
author: command.author,
|
|
233
244
|
options,
|
|
234
245
|
})
|
|
246
|
+
prepared.model = command.model
|
|
235
247
|
// A turn/started fact may have reduced during attachment preparation.
|
|
236
248
|
this.deferNextWhileTurnActive(command.mode)
|
|
237
249
|
|
|
238
250
|
const activeTurnId = this.activeTurnId
|
|
251
|
+
if (activeTurnId && command.model && command.model !== this.metadata.model)
|
|
252
|
+
throw new AgentPromptDeferredError({ message: 'Codex model changes require an idle turn boundary.' })
|
|
239
253
|
if (command.mode === 'next' || !activeTurnId)
|
|
240
254
|
return await this.startPreparedTurn(prepared)
|
|
241
255
|
|
|
@@ -261,6 +275,10 @@ export class CodexRun implements AgentRun {
|
|
|
261
275
|
return { kind: 'turn', turnId: result.turnId, placement: 'steered' }
|
|
262
276
|
}
|
|
263
277
|
|
|
278
|
+
hasActiveTurn(): boolean {
|
|
279
|
+
return this.activeTurnId !== undefined
|
|
280
|
+
}
|
|
281
|
+
|
|
264
282
|
private deferNextWhileTurnActive(mode: SendPromptCommand['mode']): void {
|
|
265
283
|
if (mode !== 'next' || !this.activeTurnId)
|
|
266
284
|
return
|
|
@@ -336,6 +354,8 @@ export class CodexRun implements AgentRun {
|
|
|
336
354
|
}
|
|
337
355
|
|
|
338
356
|
private async startPreparedTurn(prepared: PreparedCodexTurn): Promise<AgentPromptAcceptance> {
|
|
357
|
+
const publicModel = prepared.model ?? this.metadata.model
|
|
358
|
+
const inferenceModel = publicModel ? this.resolveInferenceModel(publicModel) : undefined
|
|
339
359
|
const clientUserMessageId = this.rememberPromptCommand(prepared.commandId, prepared.origin)
|
|
340
360
|
let result: ClientRequestResponsesByMethod['turn/start']
|
|
341
361
|
try {
|
|
@@ -343,7 +363,7 @@ export class CodexRun implements AgentRun {
|
|
|
343
363
|
threadId: this.metadata.providerSessionId,
|
|
344
364
|
input: prepared.input,
|
|
345
365
|
cwd: this.metadata.cwd,
|
|
346
|
-
model:
|
|
366
|
+
model: inferenceModel,
|
|
347
367
|
effort: codexEffort(this.reasoningEffort),
|
|
348
368
|
summary: codexReasoningSummary,
|
|
349
369
|
approvalPolicy: 'never',
|
|
@@ -355,6 +375,8 @@ export class CodexRun implements AgentRun {
|
|
|
355
375
|
throw mapTurnTransportLoss(error)
|
|
356
376
|
}
|
|
357
377
|
this.activeTurnId = result.turn.id
|
|
378
|
+
if (prepared.model)
|
|
379
|
+
this.metadata.model = prepared.model
|
|
358
380
|
return { kind: 'turn', turnId: result.turn.id, placement: 'started' }
|
|
359
381
|
}
|
|
360
382
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { AgentEvent, AgentRunMetadata, SendPromptCommand } from '../protocol'
|
|
2
|
+
import type { AgentOperationOptions } from './agent-operation'
|
|
3
|
+
import type { AgentPromptAcceptance, AgentRun } from './agent-run'
|
|
4
|
+
import { AgentEventStream } from './agent-event-stream'
|
|
5
|
+
import { AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from './agent-runtime-error'
|
|
6
|
+
|
|
7
|
+
/** Keeps one public event stream while replacing an idle provider connection. */
|
|
8
|
+
export class ModelRebindingRun implements AgentRun {
|
|
9
|
+
readonly events: AgentEventStream
|
|
10
|
+
readonly metadata: AgentRunMetadata
|
|
11
|
+
private run: AgentRun
|
|
12
|
+
private pump: Promise<void>
|
|
13
|
+
private activeTurn: string | undefined
|
|
14
|
+
private lastEndedTurn: string | undefined
|
|
15
|
+
private dispatching: Promise<AgentPromptAcceptance> | undefined
|
|
16
|
+
private changing = false
|
|
17
|
+
private needsRebind = false
|
|
18
|
+
private stopped = false
|
|
19
|
+
private workspacePrepared = false
|
|
20
|
+
private workspaceChanging = false
|
|
21
|
+
private readonly lifetime = new AbortController()
|
|
22
|
+
|
|
23
|
+
constructor(run: AgentRun, private readonly reopen: (model: string, signal: AbortSignal) => Promise<AgentRun>, private readonly rootAdded?: (root: string) => void) {
|
|
24
|
+
this.run = run
|
|
25
|
+
this.metadata = { ...run.metadata }
|
|
26
|
+
this.events = new AgentEventStream(this.metadata)
|
|
27
|
+
this.pump = this.consume(run)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
prepareWorkspace: AgentRun['prepareWorkspace'] = async (options) => {
|
|
31
|
+
await this.withWorkspaceChange(async () => {
|
|
32
|
+
await this.run.prepareWorkspace(options)
|
|
33
|
+
this.workspacePrepared = true
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addWorkspaceRepositoryRoot: AgentRun['addWorkspaceRepositoryRoot'] = async (root, options) => {
|
|
38
|
+
await this.withWorkspaceChange(async () => {
|
|
39
|
+
await this.run.addWorkspaceRepositoryRoot(root, options)
|
|
40
|
+
this.rootAdded?.(root)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
respondUserInput: AgentRun['respondUserInput'] = async (command, options) => await this.run.respondUserInput(command, options)
|
|
45
|
+
interrupt: AgentRun['interrupt'] = async (command, options) => await this.run.interrupt(command, options)
|
|
46
|
+
|
|
47
|
+
async sendPrompt(command: SendPromptCommand, options: AgentOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
48
|
+
if (this.dispatching || this.workspaceChanging)
|
|
49
|
+
throw new AgentPromptDeferredError({ message: 'A prompt admission is already in progress.' })
|
|
50
|
+
const task = this.dispatchPrompt(command, options)
|
|
51
|
+
this.dispatching = task
|
|
52
|
+
try {
|
|
53
|
+
return await task
|
|
54
|
+
}
|
|
55
|
+
finally { this.dispatching = undefined }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async dispatchPrompt(command: SendPromptCommand, options: AgentOperationOptions): Promise<AgentPromptAcceptance> {
|
|
59
|
+
if (this.stopped)
|
|
60
|
+
throw new AgentRunStateError({ message: 'Cannot send a prompt after the run stopped.' })
|
|
61
|
+
if (command.sessionId !== this.metadata.sessionId || command.providerSessionId !== this.metadata.providerSessionId || command.provider !== this.metadata.provider)
|
|
62
|
+
throw new AgentRunStateError({ message: 'Prompt does not target the bound run.' })
|
|
63
|
+
if (this.changing)
|
|
64
|
+
throw new AgentPromptDeferredError({ message: 'The provider model is being rebound.' })
|
|
65
|
+
const model = command.model ?? this.metadata.model
|
|
66
|
+
if (model && (this.needsRebind || model !== this.metadata.model)) {
|
|
67
|
+
if (this.activeTurn)
|
|
68
|
+
throw new AgentPromptDeferredError({ message: 'Model changes require an idle turn boundary.' })
|
|
69
|
+
this.changing = true
|
|
70
|
+
const signal = AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : []), AbortSignal.timeout(10_000)])
|
|
71
|
+
let replacement: AgentRun | undefined
|
|
72
|
+
let identityMismatch = false
|
|
73
|
+
try {
|
|
74
|
+
signal.throwIfAborted()
|
|
75
|
+
this.needsRebind = true
|
|
76
|
+
await this.run.stop({ type: 'agent.stop', ...this.target(), reason: 'model_rebind' }, { signal })
|
|
77
|
+
await this.pump
|
|
78
|
+
signal.throwIfAborted()
|
|
79
|
+
replacement = await this.reopen(model, signal)
|
|
80
|
+
signal.throwIfAborted()
|
|
81
|
+
if (replacement.metadata.providerSessionId !== this.metadata.providerSessionId || replacement.metadata.sessionId !== this.metadata.sessionId || replacement.metadata.provider !== this.metadata.provider || replacement.metadata.model !== model) {
|
|
82
|
+
identityMismatch = true
|
|
83
|
+
throw new Error('Provider rebind changed the persisted identity or model.')
|
|
84
|
+
}
|
|
85
|
+
if (this.workspacePrepared)
|
|
86
|
+
await replacement.prepareWorkspace({ signal })
|
|
87
|
+
signal.throwIfAborted()
|
|
88
|
+
this.run = replacement
|
|
89
|
+
this.metadata.model = replacement.metadata.model
|
|
90
|
+
this.needsRebind = false
|
|
91
|
+
this.pump = this.consume(replacement, true)
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
if (replacement)
|
|
95
|
+
await replacement.stop({ type: 'agent.stop', provider: replacement.metadata.provider, sessionId: replacement.metadata.sessionId, providerSessionId: replacement.metadata.providerSessionId, reason: 'model_rebind_failed' }).catch(() => undefined)
|
|
96
|
+
throw new AgentNotAcceptedError({ message: 'Provider model rebind failed before prompt dispatch.', recovery: identityMismatch ? { kind: 'terminal', reason: 'model_rebind_identity_mismatch' } : { kind: 'retry', reason: 'model_rebind_failed', retryAfterMs: 1_000 } })
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.changing = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.lifetime.signal.throwIfAborted()
|
|
103
|
+
options.signal?.throwIfAborted()
|
|
104
|
+
const acceptance = await this.run.sendPrompt(command, { ...options, signal: AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : [])]) })
|
|
105
|
+
if (acceptance.kind === 'turn' && acceptance.turnId !== this.lastEndedTurn)
|
|
106
|
+
this.activeTurn = acceptance.turnId
|
|
107
|
+
return acceptance
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
stop: AgentRun['stop'] = async (command, options) => {
|
|
111
|
+
if (command.sessionId !== this.metadata.sessionId || command.providerSessionId !== this.metadata.providerSessionId || command.provider !== this.metadata.provider)
|
|
112
|
+
throw new AgentRunStateError({ message: 'Stop does not target the bound run.' })
|
|
113
|
+
this.stopped = true
|
|
114
|
+
this.lifetime.abort()
|
|
115
|
+
await this.run.stop(command, options)
|
|
116
|
+
await this.dispatching?.catch(() => undefined)
|
|
117
|
+
await this.pump
|
|
118
|
+
this.events.end()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private target() {
|
|
122
|
+
return { provider: this.metadata.provider, sessionId: this.metadata.sessionId, providerSessionId: this.metadata.providerSessionId }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async withWorkspaceChange(operation: () => Promise<void>): Promise<void> {
|
|
126
|
+
if (this.stopped)
|
|
127
|
+
throw new AgentRunStateError({ message: 'The run stopped.' })
|
|
128
|
+
if (this.dispatching || this.workspaceChanging)
|
|
129
|
+
throw new AgentPromptDeferredError({ message: 'A provider operation is already in progress.' })
|
|
130
|
+
this.workspaceChanging = true
|
|
131
|
+
try {
|
|
132
|
+
await operation()
|
|
133
|
+
}
|
|
134
|
+
finally { this.workspaceChanging = false }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async consume(run: AgentRun, rebound = false): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
for await (const event of run.events) {
|
|
140
|
+
if (event.type === 'turn.started' && event.actor.type === 'main')
|
|
141
|
+
this.activeTurn = event.turnId
|
|
142
|
+
if (event.type === 'turn.ended' && event.actor.type === 'main') {
|
|
143
|
+
this.lastEndedTurn = event.turnId
|
|
144
|
+
if (event.turnId === this.activeTurn)
|
|
145
|
+
this.activeTurn = undefined
|
|
146
|
+
}
|
|
147
|
+
// Internal connection teardown is not product-session completion.
|
|
148
|
+
if (this.changing && (event.type === 'session.ended' || event.type === 'session.state.changed'))
|
|
149
|
+
continue
|
|
150
|
+
if (rebound && event.type === 'session.started')
|
|
151
|
+
continue
|
|
152
|
+
this.emit(event)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
this.stopped = true
|
|
157
|
+
this.lifetime.abort()
|
|
158
|
+
this.events.push({ type: 'error', payload: { message: 'Provider event stream failed.', fatal: true, source: 'provider' } })
|
|
159
|
+
this.events.end()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private emit(event: AgentEvent): void {
|
|
164
|
+
this.events.push(event)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -26,6 +26,8 @@ export const sendPromptCommandSchema = runningCommandBaseSchema.extend({
|
|
|
26
26
|
origin: z.enum(['user', 'supervisor']).default('user'),
|
|
27
27
|
prompt: agentPromptSchema,
|
|
28
28
|
commandId: commandIdSchema,
|
|
29
|
+
/** Resolved by the application for this prompt, including after runtime recovery. */
|
|
30
|
+
model: z.string().trim().min(1).max(256).optional(),
|
|
29
31
|
author: agentAuthorSchema.optional(),
|
|
30
32
|
}).strict()
|
|
31
33
|
|
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.1.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-09-
|
|
3
|
+
version: "0.1.8",
|
|
4
|
+
sha: "ff5b7ece6b3b115d420a21046bd47b465813132b",
|
|
5
|
+
buildTime: "2026-09-10T20:15:33.140Z",
|
|
6
6
|
} as const
|
package/src/steps/projector.ts
CHANGED
|
@@ -51,7 +51,13 @@ export class DurableStepProjector {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async process(event: AgentEvent, emit: (body: WireEventBody) => void): Promise<void> {
|
|
54
|
-
|
|
54
|
+
// Tool stdout is not assistant text. Its canonical completion carries
|
|
55
|
+
// full content to detail storage and emits only a bounded preview.
|
|
56
|
+
// Splitting stdout into small frames would still stream heavy bytes
|
|
57
|
+
// through the API and defeat the individual-details boundary.
|
|
58
|
+
if (event.type === 'tool.output.delta')
|
|
59
|
+
return
|
|
60
|
+
if (['message.delta', 'reasoning.summary.delta'].includes(event.type)) {
|
|
55
61
|
// Live deltas never become historical tool payloads. Large transport
|
|
56
62
|
// deltas are split rather than dropped by the supervisor frame limit.
|
|
57
63
|
const body = wrapAgentEvent(event)
|
|
@@ -128,6 +128,7 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
128
128
|
providerSessionId: metadata.providerSessionId,
|
|
129
129
|
commandId: input.commandId,
|
|
130
130
|
mode: input.body.mode,
|
|
131
|
+
model: input.body.model,
|
|
131
132
|
origin: 'user',
|
|
132
133
|
prompt: input.body.prompt,
|
|
133
134
|
author: input.body.user
|
|
@@ -40,6 +40,7 @@ export const wireCommandExtensionsSchema = jsonObjectSchema.refine(
|
|
|
40
40
|
const wireSendPromptCommandSchema = z.object({
|
|
41
41
|
type: z.literal('agent.send-prompt'),
|
|
42
42
|
mode: agentPromptModeSchema,
|
|
43
|
+
model: z.string().trim().min(1).max(256).optional(),
|
|
43
44
|
prompt: agentPromptSchema,
|
|
44
45
|
attachments: z.array(wireInputAttachmentRefSchema).superRefine((attachments, context) => {
|
|
45
46
|
const attachmentIds = new Set<string>()
|