@meistrari/agent-core 0.2.0-experimental.1 → 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 +50 -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 +864 -135
- 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 +893 -130
- 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 +423 -53
- 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 +52 -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 +3 -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/supervisor-protocol/agent-event-wrapper.ts +30 -0
|
@@ -1,49 +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
|
-
|
|
39
|
-
|
|
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'
|
|
40
73
|
}
|
|
41
74
|
|
|
42
75
|
interface CodexRunInput {
|
|
43
76
|
beforePrompt?: (signal?: AbortSignal) => Promise<void>
|
|
44
77
|
resolveInferenceModel?: (model: string) => string
|
|
45
78
|
metadata: AgentRunMetadata
|
|
46
|
-
|
|
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
|
|
47
83
|
client: CodexJsonRpcClient
|
|
48
84
|
requestedCwd: string
|
|
49
85
|
workspaceRepositoryRoots: readonly string[]
|
|
@@ -53,7 +89,7 @@ interface CodexRunInput {
|
|
|
53
89
|
userInputRequests: UserInputRequestStore
|
|
54
90
|
eventMapperState: CodexEventMapperState
|
|
55
91
|
abortSignal: AbortSignal
|
|
56
|
-
onStop: () => void
|
|
92
|
+
onStop: () => void | Promise<void>
|
|
57
93
|
}
|
|
58
94
|
|
|
59
95
|
export class CodexRun implements AgentRun {
|
|
@@ -62,8 +98,8 @@ export class CodexRun implements AgentRun {
|
|
|
62
98
|
private readonly client: CodexJsonRpcClient
|
|
63
99
|
private readonly beforePrompt: ((signal?: AbortSignal) => Promise<void>) | undefined
|
|
64
100
|
private readonly resolveInferenceModel: (model: string) => string
|
|
65
|
-
private
|
|
66
|
-
private readonly onStop: () => void
|
|
101
|
+
private current: CodexTurnSettings
|
|
102
|
+
private readonly onStop: () => void | Promise<void>
|
|
67
103
|
private readonly userInputRequests: UserInputRequestStore
|
|
68
104
|
private readonly eventMapperState: CodexEventMapperState
|
|
69
105
|
private readonly skillCatalog: CodexSkillCatalog
|
|
@@ -75,14 +111,16 @@ export class CodexRun implements AgentRun {
|
|
|
75
111
|
private stopped = false
|
|
76
112
|
private stoppingTask: Promise<void> | undefined
|
|
77
113
|
private activeTurnId: string | undefined
|
|
114
|
+
private sessionContext: string | undefined
|
|
78
115
|
private readonly interruptingTurnIds = new Set<string>()
|
|
79
116
|
|
|
80
117
|
constructor(input: CodexRunInput) {
|
|
81
118
|
this.metadata = input.metadata
|
|
119
|
+
this.sessionContext = input.sessionContext
|
|
82
120
|
this.client = input.client
|
|
83
121
|
this.beforePrompt = input.beforePrompt
|
|
84
122
|
this.resolveInferenceModel = input.resolveInferenceModel ?? (model => model)
|
|
85
|
-
this.
|
|
123
|
+
this.current = { ...input.settings }
|
|
86
124
|
this.onStop = input.onStop
|
|
87
125
|
this.userInputRequests = input.userInputRequests
|
|
88
126
|
this.eventMapperState = input.eventMapperState
|
|
@@ -205,6 +243,10 @@ export class CodexRun implements AgentRun {
|
|
|
205
243
|
}
|
|
206
244
|
|
|
207
245
|
async failActiveTurnAndEndRun(input: CodexRunAuthFailureInput): Promise<void> {
|
|
246
|
+
if (this.stoppingTask) {
|
|
247
|
+
await this.stoppingTask
|
|
248
|
+
return
|
|
249
|
+
}
|
|
208
250
|
if (this.stopped)
|
|
209
251
|
return
|
|
210
252
|
this.pushEvent({ type: 'error', payload: { message: 'Codex auth refresh failed.', fatal: false, source: 'provider', error: input.error } })
|
|
@@ -220,23 +262,38 @@ export class CodexRun implements AgentRun {
|
|
|
220
262
|
}
|
|
221
263
|
this.pushEvent({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
222
264
|
this.stopped = true
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
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
|
|
227
274
|
}
|
|
228
275
|
|
|
229
|
-
async sendPrompt(command: SendPromptCommand, options:
|
|
276
|
+
async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
230
277
|
this.assertCommandTargetsRun(command)
|
|
231
278
|
this.assertRunning()
|
|
279
|
+
options.signal?.throwIfAborted()
|
|
280
|
+
this.assertSubmissionOrder(command.mode, options.submissionOrder)
|
|
232
281
|
// Released 0.145.0 accepts turn/start during an active turn by silently steering the input
|
|
233
|
-
// and answering a phantom turn id, so
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
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) {
|
|
237
290
|
await this.beforePrompt(options.signal)
|
|
238
|
-
|
|
239
|
-
|
|
291
|
+
this.assertRunning()
|
|
292
|
+
if (submissionOrder === 'in-order')
|
|
293
|
+
this.deferNextWhileTurnActive(command.mode)
|
|
294
|
+
else
|
|
295
|
+
this.deferBypassWithoutTurn()
|
|
296
|
+
}
|
|
240
297
|
const prepared = await this.prepareTurn({
|
|
241
298
|
prompt: command.prompt,
|
|
242
299
|
commandId: command.commandId,
|
|
@@ -244,24 +301,49 @@ export class CodexRun implements AgentRun {
|
|
|
244
301
|
author: command.author,
|
|
245
302
|
options,
|
|
246
303
|
})
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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()
|
|
251
312
|
|
|
252
313
|
if (command.turnId && (command.mode !== 'steer' || command.turnId !== this.activeTurnId))
|
|
253
314
|
throw new AgentNotAcceptedError({ message: 'The targeted codex turn is no longer active.', recovery: { kind: 'terminal', reason: 'target_turn_not_active' } })
|
|
254
315
|
const activeTurnId = this.activeTurnId
|
|
255
316
|
if (activeTurnId && command.model && command.model !== this.metadata.model)
|
|
256
317
|
throw new AgentPromptDeferredError({ message: 'Codex model changes require an idle turn boundary.' })
|
|
257
|
-
if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.
|
|
318
|
+
if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.current.effort)
|
|
258
319
|
throw new AgentPromptDeferredError({ message: 'Codex reasoning effort changes require an idle turn boundary.' })
|
|
259
|
-
if (command.mode === 'next' || !activeTurnId)
|
|
260
|
-
|
|
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
|
+
}
|
|
261
333
|
|
|
262
|
-
|
|
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
|
|
263
341
|
let result: ClientRequestResponsesByMethod['turn/steer']
|
|
264
342
|
try {
|
|
343
|
+
this.assertRunning()
|
|
344
|
+
options.signal?.throwIfAborted()
|
|
345
|
+
this.rememberPromptCommand(prepared.commandId, prepared.origin)
|
|
346
|
+
steerRequested = true
|
|
265
347
|
result = await this.client.request('turn/steer', {
|
|
266
348
|
threadId: this.metadata.providerSessionId,
|
|
267
349
|
expectedTurnId: activeTurnId,
|
|
@@ -270,13 +352,32 @@ export class CodexRun implements AgentRun {
|
|
|
270
352
|
})
|
|
271
353
|
}
|
|
272
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 })
|
|
273
361
|
if (isSteerNotAcceptedResponse(error)) {
|
|
362
|
+
if (submissionOrder === 'bypass')
|
|
363
|
+
this.eventMapperState.promptCommandByClientUserMessageId.delete(clientUserMessageId)
|
|
274
364
|
throw new AgentPromptDeferredError({
|
|
275
365
|
message: 'Codex rejected the steer at a turn boundary race.',
|
|
276
366
|
cause: error,
|
|
277
367
|
})
|
|
278
368
|
}
|
|
279
|
-
|
|
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
|
+
})
|
|
280
381
|
}
|
|
281
382
|
return { kind: 'turn', turnId: result.turnId, placement: 'steered' }
|
|
282
383
|
}
|
|
@@ -285,6 +386,97 @@ export class CodexRun implements AgentRun {
|
|
|
285
386
|
return this.activeTurnId !== undefined
|
|
286
387
|
}
|
|
287
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
|
+
|
|
288
480
|
private deferNextWhileTurnActive(mode: SendPromptCommand['mode']): void {
|
|
289
481
|
if (mode !== 'next' || !this.activeTurnId)
|
|
290
482
|
return
|
|
@@ -293,6 +485,93 @@ export class CodexRun implements AgentRun {
|
|
|
293
485
|
})
|
|
294
486
|
}
|
|
295
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
|
+
|
|
296
575
|
async respondUserInput(command: RespondUserInputAgentCommand, options: AgentOperationOptions = {}): Promise<AgentUserInputSubmission> {
|
|
297
576
|
this.assertCommandTargetsRun(command)
|
|
298
577
|
this.assertRunning()
|
|
@@ -303,7 +582,12 @@ export class CodexRun implements AgentRun {
|
|
|
303
582
|
if (resolved.event)
|
|
304
583
|
this.pushEvent(resolved.event)
|
|
305
584
|
const prepared = await this.prepareTurn({ prompt: resolved.prompt, commandId: command.commandId, options })
|
|
306
|
-
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)
|
|
307
591
|
this.userInputRequests.markDelivered(command.requestId)
|
|
308
592
|
return 'submitted'
|
|
309
593
|
}
|
|
@@ -341,53 +625,100 @@ export class CodexRun implements AgentRun {
|
|
|
341
625
|
options.signal?.throwIfAborted()
|
|
342
626
|
this.stopped = true
|
|
343
627
|
this.events.push({ type: 'session.state.changed', payload: { state: 'stopping' } })
|
|
344
|
-
|
|
345
|
-
this.stoppingTask =
|
|
346
|
-
|
|
628
|
+
const stopping = Promise.withResolvers<void>()
|
|
629
|
+
this.stoppingTask = stopping.promise
|
|
630
|
+
this.startStopping(() => {
|
|
347
631
|
this.events.push({ type: 'session.ended', payload: { reason: 'stopped' } })
|
|
348
632
|
this.events.push({ type: 'session.state.changed', payload: { state: 'stopped' } })
|
|
349
633
|
this.events.end()
|
|
350
|
-
})
|
|
351
|
-
await
|
|
634
|
+
}, stopping)
|
|
635
|
+
await stopping.promise
|
|
352
636
|
}
|
|
353
637
|
|
|
354
|
-
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> {
|
|
355
648
|
options.signal?.throwIfAborted()
|
|
356
|
-
const
|
|
649
|
+
const sessionContext = this.sessionContext
|
|
650
|
+
const mapperInput = { prompt, origin, author, senderContext: options.senderContext, authorContext: options.authorContext, sessionContext }
|
|
357
651
|
const input = options.inputAttachmentPreparation
|
|
358
652
|
? await prepareCodexUserInput({ ...mapperInput, preparation: options.inputAttachmentPreparation })
|
|
359
653
|
: toCodexUserInput(mapperInput)
|
|
360
654
|
options.signal?.throwIfAborted()
|
|
361
|
-
return { input, commandId, origin }
|
|
655
|
+
return { input, commandId, origin, sessionContext }
|
|
362
656
|
}
|
|
363
657
|
|
|
364
|
-
private async
|
|
365
|
-
|
|
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
|
|
366
669
|
const inferenceModel = publicModel ? this.resolveInferenceModel(publicModel) : undefined
|
|
367
|
-
|
|
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
|
+
|
|
368
685
|
let result: ClientRequestResponsesByMethod['turn/start']
|
|
369
686
|
try {
|
|
370
687
|
result = await this.client.request('turn/start', {
|
|
371
688
|
threadId: this.metadata.providerSessionId,
|
|
372
|
-
input: prepared.input,
|
|
689
|
+
input: prepared.content.kind === 'prompt' ? prepared.content.input : [],
|
|
373
690
|
cwd: this.metadata.cwd,
|
|
374
691
|
model: inferenceModel,
|
|
375
|
-
effort: codexEffort(prepared.
|
|
692
|
+
effort: codexEffort(prepared.settings.effort),
|
|
376
693
|
summary: codexReasoningSummary,
|
|
377
694
|
approvalPolicy: 'never',
|
|
378
695
|
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
379
696
|
clientUserMessageId,
|
|
697
|
+
...notificationParams,
|
|
380
698
|
})
|
|
381
699
|
}
|
|
382
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
|
+
}
|
|
383
707
|
throw mapTurnTransportLoss(error)
|
|
384
708
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
722
|
}
|
|
392
723
|
|
|
393
724
|
private rememberPromptCommand(commandId: string, origin: 'user' | 'supervisor'): string {
|
|
@@ -416,6 +747,45 @@ function isSteerNotAcceptedResponse(error: unknown): error is CodexJsonRpcRespon
|
|
|
416
747
|
return error instanceof CodexJsonRpcResponseError && error.method === 'turn/steer' && error.responseErrorCode === -32600
|
|
417
748
|
}
|
|
418
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
|
+
|
|
419
789
|
// Transport loss on an input-carrying request maps to write-state truth: possible_write means the
|
|
420
790
|
// input may already be in the provider thread (delivery unknown, never blind-retryable);
|
|
421
791
|
// before_write is proven non-delivery and safe to retry. Anything else passes through unchanged.
|