@meistrari/agent-core 0.2.0-experimental.0 → 0.2.0-experimental.2
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 +82 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +876 -124
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +922 -91
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +428 -48
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +54 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +4 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/steps/contract.ts +4 -1
- package/src/steps/projector.ts +17 -1
- package/src/supervisor/agent-provider-factory.ts +60 -5
- package/src/supervisor/bootstrap-binder.ts +3 -2
- package/src/supervisor/index.ts +1 -1
- package/src/supervisor/persistence/supervisor-store.ts +22 -8
- package/src/supervisor/runtime-handler.ts +30 -2
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
- package/src/supervisor-protocol/bootstrap.ts +4 -1
- package/src/supervisor-protocol/command-body.ts +4 -1
- package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
- package/src/testing/in-memory-runtime-control-plane.ts +118 -12
- package/src/testing/index.ts +1 -1
- package/src/worker-runtime-client/command-pump.ts +85 -10
- package/src/worker-runtime-client/connection-attempt.ts +95 -20
- package/src/worker-runtime-client/frame-processor.ts +242 -58
- package/src/worker-runtime-client/index.ts +8 -2
- package/src/worker-runtime-client/ports.ts +34 -2
- package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
|
@@ -1,48 +1,85 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentAuthor,
|
|
3
|
+
AgentContextCompactionRequestResult,
|
|
4
|
+
AgentNotifyCommand,
|
|
3
5
|
AgentPrompt,
|
|
4
6
|
AgentReasoningEffort,
|
|
5
7
|
AgentRunMetadata,
|
|
8
|
+
AgentSettingsChange,
|
|
9
|
+
CompactContextAgentCommand,
|
|
6
10
|
InterruptAgentCommand,
|
|
7
11
|
RespondUserInputAgentCommand,
|
|
8
12
|
SendPromptCommand,
|
|
9
13
|
StopAgentCommand,
|
|
10
14
|
} from '../../protocol'
|
|
11
15
|
import type { AgentEventDraft } from '../agent-event-stream'
|
|
12
|
-
import type { AgentOperationOptions } from '../agent-operation'
|
|
13
|
-
import type { AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
16
|
+
import type { AgentOperationOptions, AgentPromptOperationOptions } from '../agent-operation'
|
|
17
|
+
import type { AgentNotificationAcceptance, AgentPromptAcceptance, AgentRun, AgentUserInputSubmission } from '../agent-run'
|
|
18
|
+
import type { AgentSettingsPlacement } from '../agent-session-events'
|
|
14
19
|
import type { UserInputRequestInput, UserInputRequestStore } from '../user-input-request'
|
|
15
20
|
import type { CodexEventMapperState } from './codex-event-mapper'
|
|
16
21
|
import type { CodexJsonRpcClient } from './codex-json-rpc-client'
|
|
17
|
-
import type { ClientRequestResponsesByMethod,
|
|
22
|
+
import type { ClientRequestResponsesByMethod, CodexUserInput } from './codex-protocol'
|
|
23
|
+
import { serializeAgentNotificationToolOutput } from '../../protocol'
|
|
18
24
|
import { AgentEventStream } from '../agent-event-stream'
|
|
19
25
|
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from '../agent-runtime-error'
|
|
26
|
+
import { sessionSettingsChangedEvent } from '../agent-session-events'
|
|
20
27
|
import { deriveMessageId } from '../message-id'
|
|
21
|
-
import { codexEffort, prepareCodexUserInput, toCodexUserInput } from './codex-command-mapper'
|
|
28
|
+
import { codexEffort, codexReasoningSummary, prepareCodexUserInput, toCodexUserInput } from './codex-command-mapper'
|
|
22
29
|
import { CodexJsonRpcResponseError } from './codex-json-rpc-client'
|
|
23
30
|
import { CodexSkillCatalog } from './codex-skill-catalog'
|
|
24
31
|
import { catalogRepositorySkillRoots, workspaceSkillRoots } from './codex-skill-roots'
|
|
25
32
|
import { CodexJsonRpcTransportClosedError } from './codex.errors'
|
|
26
33
|
|
|
27
|
-
const
|
|
34
|
+
const codexNotificationTurnTrigger = 'application_notification'
|
|
35
|
+
const codexNotificationToolNamespace = 'coding_agent'
|
|
36
|
+
const codexNotificationToolName = 'session_notifications'
|
|
28
37
|
|
|
29
38
|
interface CodexRunAuthFailureInput {
|
|
30
39
|
reason: 'codex_auth_refresh_failed'
|
|
31
40
|
error: { message: string, name?: string, stack?: string }
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
interface
|
|
43
|
+
interface PreparedCodexPromptTurn {
|
|
35
44
|
input: CodexUserInput[]
|
|
36
45
|
commandId: string
|
|
37
46
|
origin: 'user' | 'supervisor'
|
|
38
|
-
|
|
47
|
+
sessionContext: string | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The model and effort the run sends with every turn/start. Codex applies turn/start settings to that
|
|
51
|
+
// turn and every later one, so each turn must carry the current values or it silently reverts a change.
|
|
52
|
+
interface CodexTurnSettings {
|
|
53
|
+
model: string
|
|
54
|
+
effort: AgentReasoningEffort | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PreparedCodexTurnStart {
|
|
58
|
+
commandId: string
|
|
59
|
+
settings: CodexTurnSettings
|
|
60
|
+
content:
|
|
61
|
+
| { kind: 'prompt', input: CodexUserInput[], origin: 'user' | 'supervisor' }
|
|
62
|
+
| { kind: 'notification', output: string }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface CodexTurnStartReceipt {
|
|
66
|
+
kind: 'turn'
|
|
67
|
+
turnId: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface CodexTurnStartResult {
|
|
71
|
+
receipt: CodexTurnStartReceipt
|
|
72
|
+
placement: 'started' | 'steered'
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
interface CodexRunInput {
|
|
42
76
|
beforePrompt?: (signal?: AbortSignal) => Promise<void>
|
|
43
77
|
resolveInferenceModel?: (model: string) => string
|
|
44
78
|
metadata: AgentRunMetadata
|
|
45
|
-
|
|
79
|
+
// Delivered once, with the session's first prompt, then cleared (see session-context.ts).
|
|
80
|
+
sessionContext: string | undefined
|
|
81
|
+
// The resolved settings the thread was opened or resumed with.
|
|
82
|
+
settings: CodexTurnSettings
|
|
46
83
|
client: CodexJsonRpcClient
|
|
47
84
|
requestedCwd: string
|
|
48
85
|
workspaceRepositoryRoots: readonly string[]
|
|
@@ -52,7 +89,7 @@ interface CodexRunInput {
|
|
|
52
89
|
userInputRequests: UserInputRequestStore
|
|
53
90
|
eventMapperState: CodexEventMapperState
|
|
54
91
|
abortSignal: AbortSignal
|
|
55
|
-
onStop: () => void
|
|
92
|
+
onStop: () => void | Promise<void>
|
|
56
93
|
}
|
|
57
94
|
|
|
58
95
|
export class CodexRun implements AgentRun {
|
|
@@ -61,8 +98,8 @@ export class CodexRun implements AgentRun {
|
|
|
61
98
|
private readonly client: CodexJsonRpcClient
|
|
62
99
|
private readonly beforePrompt: ((signal?: AbortSignal) => Promise<void>) | undefined
|
|
63
100
|
private readonly resolveInferenceModel: (model: string) => string
|
|
64
|
-
private
|
|
65
|
-
private readonly onStop: () => void
|
|
101
|
+
private current: CodexTurnSettings
|
|
102
|
+
private readonly onStop: () => void | Promise<void>
|
|
66
103
|
private readonly userInputRequests: UserInputRequestStore
|
|
67
104
|
private readonly eventMapperState: CodexEventMapperState
|
|
68
105
|
private readonly skillCatalog: CodexSkillCatalog
|
|
@@ -74,14 +111,16 @@ export class CodexRun implements AgentRun {
|
|
|
74
111
|
private stopped = false
|
|
75
112
|
private stoppingTask: Promise<void> | undefined
|
|
76
113
|
private activeTurnId: string | undefined
|
|
114
|
+
private sessionContext: string | undefined
|
|
77
115
|
private readonly interruptingTurnIds = new Set<string>()
|
|
78
116
|
|
|
79
117
|
constructor(input: CodexRunInput) {
|
|
80
118
|
this.metadata = input.metadata
|
|
119
|
+
this.sessionContext = input.sessionContext
|
|
81
120
|
this.client = input.client
|
|
82
121
|
this.beforePrompt = input.beforePrompt
|
|
83
122
|
this.resolveInferenceModel = input.resolveInferenceModel ?? (model => model)
|
|
84
|
-
this.
|
|
123
|
+
this.current = { ...input.settings }
|
|
85
124
|
this.onStop = input.onStop
|
|
86
125
|
this.userInputRequests = input.userInputRequests
|
|
87
126
|
this.eventMapperState = input.eventMapperState
|
|
@@ -204,6 +243,10 @@ export class CodexRun implements AgentRun {
|
|
|
204
243
|
}
|
|
205
244
|
|
|
206
245
|
async failActiveTurnAndEndRun(input: CodexRunAuthFailureInput): Promise<void> {
|
|
246
|
+
if (this.stoppingTask) {
|
|
247
|
+
await this.stoppingTask
|
|
248
|
+
return
|
|
249
|
+
}
|
|
207
250
|
if (this.stopped)
|
|
208
251
|
return
|
|
209
252
|
this.pushEvent({ type: 'error', payload: { message: 'Codex auth refresh failed.', fatal: false, source: 'provider', error: input.error } })
|
|
@@ -219,23 +262,38 @@ export class CodexRun implements AgentRun {
|
|
|
219
262
|
}
|
|
220
263
|
this.pushEvent({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
221
264
|
this.stopped = true
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
265
|
+
// The refresh error was flushed before this path. Revoke run capabilities while
|
|
266
|
+
// native EOF/signal escalation proceeds, not after a potentially slow child exit.
|
|
267
|
+
const stopping = Promise.withResolvers<void>()
|
|
268
|
+
this.stoppingTask = stopping.promise
|
|
269
|
+
this.startStopping(() => {
|
|
270
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: input.error } })
|
|
271
|
+
this.events.end()
|
|
272
|
+
}, stopping)
|
|
273
|
+
await stopping.promise
|
|
226
274
|
}
|
|
227
275
|
|
|
228
|
-
async sendPrompt(command: SendPromptCommand, options:
|
|
276
|
+
async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
229
277
|
this.assertCommandTargetsRun(command)
|
|
230
278
|
this.assertRunning()
|
|
279
|
+
options.signal?.throwIfAborted()
|
|
280
|
+
this.assertSubmissionOrder(command.mode, options.submissionOrder)
|
|
231
281
|
// Released 0.145.0 accepts turn/start during an active turn by silently steering the input
|
|
232
|
-
// and answering a phantom turn id, so
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
if (
|
|
282
|
+
// and answering a phantom turn id, so in-order next-vs-active races are refused before any
|
|
283
|
+
// request. Bypass submission instead requires an active turn before preparation.
|
|
284
|
+
const submissionOrder = options.submissionOrder ?? 'in-order'
|
|
285
|
+
if (submissionOrder === 'in-order')
|
|
286
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
287
|
+
else
|
|
288
|
+
this.deferBypassWithoutTurn()
|
|
289
|
+
if (this.beforePrompt) {
|
|
236
290
|
await this.beforePrompt(options.signal)
|
|
237
|
-
|
|
238
|
-
|
|
291
|
+
this.assertRunning()
|
|
292
|
+
if (submissionOrder === 'in-order')
|
|
293
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
294
|
+
else
|
|
295
|
+
this.deferBypassWithoutTurn()
|
|
296
|
+
}
|
|
239
297
|
const prepared = await this.prepareTurn({
|
|
240
298
|
prompt: command.prompt,
|
|
241
299
|
commandId: command.commandId,
|
|
@@ -243,19 +301,49 @@ export class CodexRun implements AgentRun {
|
|
|
243
301
|
author: command.author,
|
|
244
302
|
options,
|
|
245
303
|
})
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
this.
|
|
304
|
+
// Provider events and stop can reduce while attachment preparation is awaited. Recheck the
|
|
305
|
+
// run and use the execution-time active turn before creating correlation or provider input.
|
|
306
|
+
this.assertRunning()
|
|
307
|
+
options.signal?.throwIfAborted()
|
|
308
|
+
if (submissionOrder === 'in-order')
|
|
309
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
310
|
+
else
|
|
311
|
+
this.deferBypassWithoutTurn()
|
|
249
312
|
|
|
313
|
+
if (command.turnId && (command.mode !== 'steer' || command.turnId !== this.activeTurnId))
|
|
314
|
+
throw new AgentNotAcceptedError({ message: 'The targeted codex turn is no longer active.', recovery: { kind: 'terminal', reason: 'target_turn_not_active' } })
|
|
250
315
|
const activeTurnId = this.activeTurnId
|
|
251
316
|
if (activeTurnId && command.model && command.model !== this.metadata.model)
|
|
252
317
|
throw new AgentPromptDeferredError({ message: 'Codex model changes require an idle turn boundary.' })
|
|
253
|
-
if (command.
|
|
254
|
-
|
|
318
|
+
if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.current.effort)
|
|
319
|
+
throw new AgentPromptDeferredError({ message: 'Codex reasoning effort changes require an idle turn boundary.' })
|
|
320
|
+
if (command.mode === 'next' || !activeTurnId) {
|
|
321
|
+
const settings = withSettingsChange(this.current, command.settings ?? { model: command.model, reasoningEffort: command.reasoningEffort })
|
|
322
|
+
const { receipt } = await this.startTurn({
|
|
323
|
+
commandId: prepared.commandId,
|
|
324
|
+
settings,
|
|
325
|
+
content: { kind: 'prompt', input: prepared.input, origin: prepared.origin },
|
|
326
|
+
}, options)
|
|
327
|
+
this.sessionContextDelivered(prepared.sessionContext)
|
|
328
|
+
// The turn this prompt started runs on the merged settings from its first request.
|
|
329
|
+
if (command.settings)
|
|
330
|
+
this.settingsAccepted({ settings, commandId: command.commandId, turnId: receipt.turnId, placement: { appliesFrom: 'current_turn' } })
|
|
331
|
+
return { ...receipt, placement: 'started' }
|
|
332
|
+
}
|
|
255
333
|
|
|
256
|
-
|
|
334
|
+
// Settings must reach the provider before the input. Keep the committed settings until
|
|
335
|
+
// acceptance and compensate a proven rejection before returning the prompt to the caller.
|
|
336
|
+
const previousSettings = this.current
|
|
337
|
+
const settings = command.settings && withSettingsChange(previousSettings, command.settings)
|
|
338
|
+
const placement = settings && await this.applySteerSettings({ turnId: activeTurnId, settings })
|
|
339
|
+
const clientUserMessageId = deriveMessageId(prepared.commandId)
|
|
340
|
+
let steerRequested = false
|
|
257
341
|
let result: ClientRequestResponsesByMethod['turn/steer']
|
|
258
342
|
try {
|
|
343
|
+
this.assertRunning()
|
|
344
|
+
options.signal?.throwIfAborted()
|
|
345
|
+
this.rememberPromptCommand(prepared.commandId, prepared.origin)
|
|
346
|
+
steerRequested = true
|
|
259
347
|
result = await this.client.request('turn/steer', {
|
|
260
348
|
threadId: this.metadata.providerSessionId,
|
|
261
349
|
expectedTurnId: activeTurnId,
|
|
@@ -264,13 +352,32 @@ export class CodexRun implements AgentRun {
|
|
|
264
352
|
})
|
|
265
353
|
}
|
|
266
354
|
catch (error) {
|
|
355
|
+
const mapped = mapTurnTransportLoss(error)
|
|
356
|
+
// A lost response may represent accepted input; leave its settings intact and retain
|
|
357
|
+
// delivery-unknown recovery. Cancellation before writing and explicit refusal are safe
|
|
358
|
+
// to compensate, without using the cancelled operation's signal.
|
|
359
|
+
if (settings && placement && (!steerRequested || isSteerNotAcceptedResponse(error) || mapped instanceof AgentNotAcceptedError))
|
|
360
|
+
await this.restoreSteerSettings({ turnId: activeTurnId, previousSettings, settings, placement })
|
|
267
361
|
if (isSteerNotAcceptedResponse(error)) {
|
|
362
|
+
if (submissionOrder === 'bypass')
|
|
363
|
+
this.eventMapperState.promptCommandByClientUserMessageId.delete(clientUserMessageId)
|
|
268
364
|
throw new AgentPromptDeferredError({
|
|
269
365
|
message: 'Codex rejected the steer at a turn boundary race.',
|
|
270
366
|
cause: error,
|
|
271
367
|
})
|
|
272
368
|
}
|
|
273
|
-
|
|
369
|
+
if (submissionOrder === 'bypass' && mapped instanceof AgentNotAcceptedError)
|
|
370
|
+
this.eventMapperState.promptCommandByClientUserMessageId.delete(clientUserMessageId)
|
|
371
|
+
throw mapped
|
|
372
|
+
}
|
|
373
|
+
this.sessionContextDelivered(prepared.sessionContext)
|
|
374
|
+
if (settings && placement) {
|
|
375
|
+
this.settingsAccepted({
|
|
376
|
+
settings,
|
|
377
|
+
commandId: command.commandId,
|
|
378
|
+
turnId: placement.appliesFrom === 'current_turn' ? activeTurnId : undefined,
|
|
379
|
+
placement,
|
|
380
|
+
})
|
|
274
381
|
}
|
|
275
382
|
return { kind: 'turn', turnId: result.turnId, placement: 'steered' }
|
|
276
383
|
}
|
|
@@ -279,6 +386,97 @@ export class CodexRun implements AgentRun {
|
|
|
279
386
|
return this.activeTurnId !== undefined
|
|
280
387
|
}
|
|
281
388
|
|
|
389
|
+
// Changes the running turn (only honoured with `features.step_model_switching`) and the thread's
|
|
390
|
+
// later turns. Sends only the fields that differ from `current`; with none, no provider call and the
|
|
391
|
+
// running turn already runs on those values.
|
|
392
|
+
private async applySteerSettings({ turnId, settings }: { turnId: string, settings: CodexTurnSettings }): Promise<AgentSettingsPlacement> {
|
|
393
|
+
const changes = {
|
|
394
|
+
...(settings.model === this.current.model ? {} : { model: this.resolveInferenceModel(settings.model) }),
|
|
395
|
+
...(settings.effort === this.current.effort ? {} : { effort: codexEffort(settings.effort) }),
|
|
396
|
+
}
|
|
397
|
+
if (Object.keys(changes).length === 0)
|
|
398
|
+
return { appliesFrom: 'current_turn' }
|
|
399
|
+
|
|
400
|
+
const threadId = this.metadata.providerSessionId
|
|
401
|
+
// Least durable effect first: thread defaults only reach future turns, and every turn/start
|
|
402
|
+
// re-sends the committed `current`, so a failure after this call leaves nothing that outlives
|
|
403
|
+
// the next turn. The running turn changes last, so a failed thread update never leaves the
|
|
404
|
+
// active turn on settings the steer was not accepted for.
|
|
405
|
+
try {
|
|
406
|
+
await this.client.request('thread/settings/update', { threadId, ...changes })
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
throw mapSettingsTransportLoss(error)
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const result = await this.client.request('turn/settings/update', { threadId, turnId, ...changes })
|
|
413
|
+
// `targetUnavailable` (the turn ended or was replaced first) carries no provider reason.
|
|
414
|
+
return result.status === 'applied' ? { appliesFrom: 'current_turn' } : { appliesFrom: 'next_turn' }
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (!isTurnSettingsUnavailableResponse(error))
|
|
418
|
+
throw mapSettingsTransportLoss(error)
|
|
419
|
+
return { appliesFrom: 'next_turn', currentTurnUnavailableReason: error.responseErrorMessage || undefined }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async restoreSteerSettings(input: { turnId: string, previousSettings: CodexTurnSettings, settings: CodexTurnSettings, placement: AgentSettingsPlacement }): Promise<void> {
|
|
424
|
+
const { turnId, previousSettings, settings, placement } = input
|
|
425
|
+
const modelChanged = settings.model !== previousSettings.model
|
|
426
|
+
const effortChanged = settings.effort !== previousSettings.effort
|
|
427
|
+
if (!modelChanged && !effortChanged)
|
|
428
|
+
return
|
|
429
|
+
const threadId = this.metadata.providerSessionId
|
|
430
|
+
try {
|
|
431
|
+
// The SDK treats null/omitted effort as unchanged. An implicit provider default cannot
|
|
432
|
+
// be reconstructed here, so retire this run instead of claiming it was restored.
|
|
433
|
+
if (effortChanged && previousSettings.effort === undefined)
|
|
434
|
+
throw new AgentRunStateError({ message: 'Cannot restore an implicit Codex reasoning effort.' })
|
|
435
|
+
const changes = {
|
|
436
|
+
...(modelChanged ? { model: this.resolveInferenceModel(previousSettings.model) } : {}),
|
|
437
|
+
...(effortChanged ? { effort: codexEffort(previousSettings.effort) } : {}),
|
|
438
|
+
}
|
|
439
|
+
// Restore the active turn first. targetUnavailable is safe: that turn can no longer
|
|
440
|
+
// execute with the tentative settings. A refusal/transport failure is not restoration.
|
|
441
|
+
if (placement.appliesFrom === 'current_turn')
|
|
442
|
+
await this.client.request('turn/settings/update', { threadId, turnId, ...changes })
|
|
443
|
+
await this.client.request('thread/settings/update', { threadId, ...changes })
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
// Do not defer while an active provider can continue on unaccepted settings.
|
|
447
|
+
await this.stop({ type: 'agent.stop', provider: 'codex', sessionId: this.metadata.sessionId, providerSessionId: threadId, reason: 'settings-rollback-failed' })
|
|
448
|
+
throw new AgentNotAcceptedError({
|
|
449
|
+
message: 'Codex settings could not be restored after a rejected steer; the run was stopped.',
|
|
450
|
+
recovery: { kind: 'retry', reason: 'provider_connection_lost' },
|
|
451
|
+
cause: error,
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private settingsAccepted(input: { settings: CodexTurnSettings, commandId: string, turnId: string | undefined, placement: AgentSettingsPlacement }): void {
|
|
457
|
+
this.current = input.settings
|
|
458
|
+
this.pushEvent(sessionSettingsChangedEvent({
|
|
459
|
+
model: input.settings.model,
|
|
460
|
+
reasoningEffort: input.settings.effort,
|
|
461
|
+
commandId: input.commandId,
|
|
462
|
+
turnId: input.turnId,
|
|
463
|
+
...input.placement,
|
|
464
|
+
}))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private sessionContextDelivered(sessionContext: string | undefined): void {
|
|
468
|
+
if (sessionContext !== undefined && this.sessionContext === sessionContext)
|
|
469
|
+
this.sessionContext = undefined
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private assertSubmissionOrder(mode: SendPromptCommand['mode'], submissionOrder: AgentPromptOperationOptions['submissionOrder']): void {
|
|
473
|
+
if (submissionOrder === 'bypass' && mode === 'next') {
|
|
474
|
+
throw new AgentRunStateError({
|
|
475
|
+
message: 'Bypass submission requires a steer prompt.',
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
282
480
|
private deferNextWhileTurnActive(mode: SendPromptCommand['mode']): void {
|
|
283
481
|
if (mode !== 'next' || !this.activeTurnId)
|
|
284
482
|
return
|
|
@@ -287,6 +485,93 @@ export class CodexRun implements AgentRun {
|
|
|
287
485
|
})
|
|
288
486
|
}
|
|
289
487
|
|
|
488
|
+
private deferBypassWithoutTurn(): void {
|
|
489
|
+
if (this.activeTurnId)
|
|
490
|
+
return
|
|
491
|
+
throw new AgentPromptDeferredError({
|
|
492
|
+
message: 'Codex has no active turn for bypass prompt submission.',
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async notify(command: AgentNotifyCommand, options: AgentOperationOptions = {}): Promise<AgentNotificationAcceptance> {
|
|
497
|
+
this.assertCommandTargetsRun(command)
|
|
498
|
+
this.assertRunning()
|
|
499
|
+
options.signal?.throwIfAborted()
|
|
500
|
+
this.deferNotificationForUnresolvedUserInput()
|
|
501
|
+
|
|
502
|
+
const { receipt, placement } = await this.startTurn({
|
|
503
|
+
commandId: command.commandId,
|
|
504
|
+
settings: this.current,
|
|
505
|
+
content: {
|
|
506
|
+
kind: 'notification',
|
|
507
|
+
output: serializeAgentNotificationToolOutput({
|
|
508
|
+
commandId: command.commandId,
|
|
509
|
+
instruction: command.instruction,
|
|
510
|
+
notifications: command.notifications,
|
|
511
|
+
}),
|
|
512
|
+
},
|
|
513
|
+
}, options)
|
|
514
|
+
return { ...receipt, placement }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private deferNotificationForUnresolvedUserInput(): void {
|
|
518
|
+
if (this.userInputRequests.hasUnresolvedUserInput()) {
|
|
519
|
+
throw new AgentPromptDeferredError({
|
|
520
|
+
message: 'Codex is waiting for unresolved user input; a notification cannot be submitted.',
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async compactContext(command: CompactContextAgentCommand, options: AgentOperationOptions = {}): Promise<AgentContextCompactionRequestResult> {
|
|
526
|
+
this.assertCommandTargetsRun(command)
|
|
527
|
+
this.assertRunning()
|
|
528
|
+
options.signal?.throwIfAborted()
|
|
529
|
+
if (this.activeTurnId) {
|
|
530
|
+
throw new AgentPromptDeferredError({
|
|
531
|
+
message: 'Codex turn is active; context compaction must wait for an idle main thread.',
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let result: AgentContextCompactionRequestResult
|
|
536
|
+
try {
|
|
537
|
+
await this.client.request('thread/compact/start', { threadId: this.metadata.providerSessionId })
|
|
538
|
+
// The pinned RPC response is an empty acknowledgement. Native item/turn lifecycle has
|
|
539
|
+
// no request identity or manual/auto discriminator, so this control result stays unknown.
|
|
540
|
+
result = { status: 'unknown' }
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
if (error instanceof CodexJsonRpcTransportClosedError) {
|
|
544
|
+
if (error.writeState === 'possible_write') {
|
|
545
|
+
result = { status: 'unknown' }
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
throw new AgentNotAcceptedError({
|
|
549
|
+
message: 'Codex app-server connection closed before the compact request was written.',
|
|
550
|
+
recovery: { kind: 'retry', reason: 'provider_connection_lost' },
|
|
551
|
+
cause: error,
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (error instanceof CodexJsonRpcResponseError) {
|
|
556
|
+
result = {
|
|
557
|
+
status: 'failed',
|
|
558
|
+
error: {
|
|
559
|
+
message: error.responseErrorMessage || 'Codex rejected context compaction.',
|
|
560
|
+
...(error.responseErrorCode === undefined ? {} : { code: String(error.responseErrorCode) }),
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
throw error
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
this.pushEvent({
|
|
569
|
+
type: 'context.compaction.request.completed',
|
|
570
|
+
payload: { commandId: command.commandId, result },
|
|
571
|
+
})
|
|
572
|
+
return result
|
|
573
|
+
}
|
|
574
|
+
|
|
290
575
|
async respondUserInput(command: RespondUserInputAgentCommand, options: AgentOperationOptions = {}): Promise<AgentUserInputSubmission> {
|
|
291
576
|
this.assertCommandTargetsRun(command)
|
|
292
577
|
this.assertRunning()
|
|
@@ -297,7 +582,12 @@ export class CodexRun implements AgentRun {
|
|
|
297
582
|
if (resolved.event)
|
|
298
583
|
this.pushEvent(resolved.event)
|
|
299
584
|
const prepared = await this.prepareTurn({ prompt: resolved.prompt, commandId: command.commandId, options })
|
|
300
|
-
await this.
|
|
585
|
+
await this.startTurn({
|
|
586
|
+
commandId: prepared.commandId,
|
|
587
|
+
settings: this.current,
|
|
588
|
+
content: { kind: 'prompt', input: prepared.input, origin: prepared.origin },
|
|
589
|
+
}, options)
|
|
590
|
+
this.sessionContextDelivered(prepared.sessionContext)
|
|
301
591
|
this.userInputRequests.markDelivered(command.requestId)
|
|
302
592
|
return 'submitted'
|
|
303
593
|
}
|
|
@@ -306,6 +596,8 @@ export class CodexRun implements AgentRun {
|
|
|
306
596
|
this.assertCommandTargetsRun(command)
|
|
307
597
|
this.assertRunning()
|
|
308
598
|
options.signal?.throwIfAborted()
|
|
599
|
+
if (command.turnId && command.turnId !== this.activeTurnId)
|
|
600
|
+
return
|
|
309
601
|
const turnId = command.turnId ?? this.activeTurnId
|
|
310
602
|
if (!turnId)
|
|
311
603
|
return
|
|
@@ -333,51 +625,100 @@ export class CodexRun implements AgentRun {
|
|
|
333
625
|
options.signal?.throwIfAborted()
|
|
334
626
|
this.stopped = true
|
|
335
627
|
this.events.push({ type: 'session.state.changed', payload: { state: 'stopping' } })
|
|
336
|
-
|
|
337
|
-
this.stoppingTask =
|
|
338
|
-
|
|
628
|
+
const stopping = Promise.withResolvers<void>()
|
|
629
|
+
this.stoppingTask = stopping.promise
|
|
630
|
+
this.startStopping(() => {
|
|
339
631
|
this.events.push({ type: 'session.ended', payload: { reason: 'stopped' } })
|
|
340
632
|
this.events.push({ type: 'session.state.changed', payload: { state: 'stopped' } })
|
|
341
633
|
this.events.end()
|
|
342
|
-
})
|
|
343
|
-
await
|
|
634
|
+
}, stopping)
|
|
635
|
+
await stopping.promise
|
|
344
636
|
}
|
|
345
637
|
|
|
346
|
-
private
|
|
638
|
+
private startStopping(finalize: () => void, stopping: PromiseWithResolvers<void>): void {
|
|
639
|
+
const revocation = new Promise<void>((resolve) => {
|
|
640
|
+
resolve(this.onStop())
|
|
641
|
+
})
|
|
642
|
+
void Promise.all([this.client.stop(), revocation])
|
|
643
|
+
.then(finalize)
|
|
644
|
+
.then(stopping.resolve, stopping.reject)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private async prepareTurn({ prompt, commandId, origin = 'user', author, options }: { prompt: AgentPrompt, commandId: string, origin?: 'user' | 'supervisor', author?: AgentAuthor, options: AgentOperationOptions }): Promise<PreparedCodexPromptTurn> {
|
|
347
648
|
options.signal?.throwIfAborted()
|
|
348
|
-
const
|
|
649
|
+
const sessionContext = this.sessionContext
|
|
650
|
+
const mapperInput = { prompt, origin, author, senderContext: options.senderContext, authorContext: options.authorContext, sessionContext }
|
|
349
651
|
const input = options.inputAttachmentPreparation
|
|
350
652
|
? await prepareCodexUserInput({ ...mapperInput, preparation: options.inputAttachmentPreparation })
|
|
351
653
|
: toCodexUserInput(mapperInput)
|
|
352
654
|
options.signal?.throwIfAborted()
|
|
353
|
-
return { input, commandId, origin }
|
|
655
|
+
return { input, commandId, origin, sessionContext }
|
|
354
656
|
}
|
|
355
657
|
|
|
356
|
-
private async
|
|
357
|
-
|
|
658
|
+
private async startTurn(prepared: PreparedCodexTurnStart, options: AgentOperationOptions): Promise<CodexTurnStartResult> {
|
|
659
|
+
// Provider state can change during prompt preparation or notification serialization. Keep
|
|
660
|
+
// the last no-write guard and abort check at the shared turn/start boundary.
|
|
661
|
+
this.assertRunning()
|
|
662
|
+
options.signal?.throwIfAborted()
|
|
663
|
+
if (prepared.content.kind === 'notification')
|
|
664
|
+
this.deferNotificationForUnresolvedUserInput()
|
|
665
|
+
else if (this.activeTurnId)
|
|
666
|
+
this.deferNextWhileTurnActive('next')
|
|
667
|
+
const activeTurnIdAtSubmission = this.activeTurnId
|
|
668
|
+
const publicModel = prepared.settings.model
|
|
358
669
|
const inferenceModel = publicModel ? this.resolveInferenceModel(publicModel) : undefined
|
|
359
|
-
|
|
670
|
+
|
|
671
|
+
const clientUserMessageId = prepared.content.kind === 'prompt'
|
|
672
|
+
? this.rememberPromptCommand(prepared.commandId, prepared.content.origin)
|
|
673
|
+
: deriveMessageId(prepared.commandId)
|
|
674
|
+
const notificationParams = prepared.content.kind === 'notification'
|
|
675
|
+
? {
|
|
676
|
+
turnTrigger: codexNotificationTurnTrigger,
|
|
677
|
+
toolOutput: {
|
|
678
|
+
namespace: codexNotificationToolNamespace,
|
|
679
|
+
name: codexNotificationToolName,
|
|
680
|
+
output: prepared.content.output,
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
: {}
|
|
684
|
+
|
|
360
685
|
let result: ClientRequestResponsesByMethod['turn/start']
|
|
361
686
|
try {
|
|
362
687
|
result = await this.client.request('turn/start', {
|
|
363
688
|
threadId: this.metadata.providerSessionId,
|
|
364
|
-
input: prepared.input,
|
|
689
|
+
input: prepared.content.kind === 'prompt' ? prepared.content.input : [],
|
|
365
690
|
cwd: this.metadata.cwd,
|
|
366
691
|
model: inferenceModel,
|
|
367
|
-
effort: codexEffort(
|
|
692
|
+
effort: codexEffort(prepared.settings.effort),
|
|
368
693
|
summary: codexReasoningSummary,
|
|
369
694
|
approvalPolicy: 'never',
|
|
370
695
|
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
371
696
|
clientUserMessageId,
|
|
697
|
+
...notificationParams,
|
|
372
698
|
})
|
|
373
699
|
}
|
|
374
700
|
catch (error) {
|
|
701
|
+
if (prepared.content.kind === 'notification' && isNotificationNotAcceptedResponse(error)) {
|
|
702
|
+
throw new AgentPromptDeferredError({
|
|
703
|
+
message: 'Codex active turn cannot accept a notification.',
|
|
704
|
+
cause: error,
|
|
705
|
+
})
|
|
706
|
+
}
|
|
375
707
|
throw mapTurnTransportLoss(error)
|
|
376
708
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
709
|
+
const placement = prepared.content.kind === 'notification'
|
|
710
|
+
&& activeTurnIdAtSubmission === result.turn.id
|
|
711
|
+
? 'steered'
|
|
712
|
+
: 'started'
|
|
713
|
+
if (placement === 'started') {
|
|
714
|
+
this.activeTurnId = result.turn.id
|
|
715
|
+
this.current = prepared.settings
|
|
716
|
+
this.metadata.model = prepared.settings.model
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
receipt: { kind: 'turn', turnId: result.turn.id },
|
|
720
|
+
placement,
|
|
721
|
+
}
|
|
381
722
|
}
|
|
382
723
|
|
|
383
724
|
private rememberPromptCommand(commandId: string, origin: 'user' | 'supervisor'): string {
|
|
@@ -406,6 +747,45 @@ function isSteerNotAcceptedResponse(error: unknown): error is CodexJsonRpcRespon
|
|
|
406
747
|
return error instanceof CodexJsonRpcResponseError && error.method === 'turn/steer' && error.responseErrorCode === -32600
|
|
407
748
|
}
|
|
408
749
|
|
|
750
|
+
function withSettingsChange(current: CodexTurnSettings, change: AgentSettingsChange | undefined): CodexTurnSettings {
|
|
751
|
+
return {
|
|
752
|
+
model: change?.model ?? current.model,
|
|
753
|
+
effort: change?.reasoningEffort ?? current.effort,
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// A running turn that cannot take new settings is not a failure: they apply from the next turn.
|
|
758
|
+
// Pinned Codex answers -32600 with its reason as the message when `features.step_model_switching` is off
|
|
759
|
+
// (live-verified on 0.153.4) or when its legacy turn-safety check refuses the destination model
|
|
760
|
+
// (core/src/session/step_activation.rs at rust-v0.156.1, live-verified for gpt-6-astra -> gpt-5.5).
|
|
761
|
+
function isTurnSettingsUnavailableResponse(error: unknown): error is CodexJsonRpcResponseError {
|
|
762
|
+
return error instanceof CodexJsonRpcResponseError && error.method === 'turn/settings/update' && error.responseErrorCode === -32600
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Settings requests run before the prompt is written, so losing the connection during one proves
|
|
766
|
+
// the prompt was not delivered, whatever the settings request's own write state.
|
|
767
|
+
function mapSettingsTransportLoss(error: unknown): unknown {
|
|
768
|
+
if (!(error instanceof CodexJsonRpcTransportClosedError))
|
|
769
|
+
return error
|
|
770
|
+
return new AgentNotAcceptedError({
|
|
771
|
+
message: 'Codex app-server connection was lost while applying prompt settings; the prompt was not written.',
|
|
772
|
+
recovery: { kind: 'retry', reason: 'provider_connection_lost' },
|
|
773
|
+
cause: error,
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Pinned Codex reports a standalone tool output as definitively not submitted when the active turn is
|
|
778
|
+
// review or compaction work. The app-server currently exposes that Core reason only in this exact error.
|
|
779
|
+
function isNotificationNotAcceptedResponse(error: unknown): error is CodexJsonRpcResponseError {
|
|
780
|
+
if (!(error instanceof CodexJsonRpcResponseError)
|
|
781
|
+
|| error.method !== 'turn/start'
|
|
782
|
+
|| error.responseErrorCode !== -32603) {
|
|
783
|
+
return false
|
|
784
|
+
}
|
|
785
|
+
return error.responseErrorMessage === 'failed to submit turn input: ActiveTurnNotSteerable { turn_kind: Review }'
|
|
786
|
+
|| error.responseErrorMessage === 'failed to submit turn input: ActiveTurnNotSteerable { turn_kind: Compact }'
|
|
787
|
+
}
|
|
788
|
+
|
|
409
789
|
// Transport loss on an input-carrying request maps to write-state truth: possible_write means the
|
|
410
790
|
// input may already be in the provider thread (delivery unknown, never blind-retryable);
|
|
411
791
|
// before_write is proven non-delivery and safe to retry. Anything else passes through unchanged.
|