@meistrari/agent-core 0.2.0-experimental.0 → 0.2.0-experimental.1
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 +32 -0
- package/package.json +1 -1
- package/src/agents/claude/claude-event-mapper.ts +29 -6
- package/src/agents/claude/claude-run.ts +70 -2
- package/src/agents/codex/codex-run.ts +12 -2
- package/src/protocol/agent-command.ts +2 -0
- package/src/protocol/agent-usage.ts +1 -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/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
package/README.md
CHANGED
|
@@ -48,6 +48,38 @@ Use `--loopback-tcp` for local and image-build probes. Production images default
|
|
|
48
48
|
|
|
49
49
|
Publishing runs from `main` through `.github/workflows/publish.yml` (conventional commits decide the bump).
|
|
50
50
|
|
|
51
|
+
## Command transports
|
|
52
|
+
|
|
53
|
+
`session.bootstrap` carries an optional `commandTransport`. Both modes share the same wire frames, ES256
|
|
54
|
+
control authority, durable event ACK floor, reverse RPC and SQLite journal.
|
|
55
|
+
|
|
56
|
+
- **Omitted (legacy, background-coding-agent).** The bootstrap embeds command 1, the journal requires a
|
|
57
|
+
contiguous `commandSeq`, and the worker pump advances a cumulative cursor through `CommandSource.next`.
|
|
58
|
+
- **`'wss-v1'` (agent-api v5).** The bootstrap carries no command and rejects one. The supervisor attaches
|
|
59
|
+
the provider immediately, the journal accepts gaps and a delivery order different from admission while
|
|
60
|
+
keeping identity/hash deduplication, and `runtime.state.sourceId` announces a journal identity persisted
|
|
61
|
+
in SQLite and preserved across reopen. The worker passes that identity to
|
|
62
|
+
`ConnectionAuthorityStore.markConnectionReady`; the store pins it on first readiness and answers a
|
|
63
|
+
`ConnectionReadinessRejection` when a later supervisor announces a different journal, which fails the
|
|
64
|
+
session deterministically instead of resuming from a stale cursor. Durable events replayed after
|
|
65
|
+
`runtime.state` are persisted only after the store accepted the readiness. The worker pump selects work
|
|
66
|
+
through `CommandSource.pending` and persists each receipt through `CommandSource.received` before
|
|
67
|
+
selecting again; `next` is never called. A command the supervisor will never accept (invalid body,
|
|
68
|
+
sequence already journaled under another identity) is answered with a `rejected` ACK
|
|
69
|
+
(`supervisor.command.invalid-body`, `supervisor.command.conflicting-duplicate`) instead of a socket
|
|
70
|
+
close; the pump persists it through `CommandSource.rejected` and keeps delivering the session's other
|
|
71
|
+
work. The pump also accepts `ackTimeoutMs`, `onFailure` and an awaitable `close()`.
|
|
72
|
+
|
|
73
|
+
The product-owned `pending` selection decides priority: withdrawn commands leave gaps, and cancel or steer
|
|
74
|
+
may be delivered ahead of a queued message. The resident supervisor executes commands in arrival order;
|
|
75
|
+
a `next` prompt dispatched while a turn is active may be refused by the adapter (Codex defers it), so the
|
|
76
|
+
selection must hold such messages back until the turn ends.
|
|
77
|
+
|
|
78
|
+
`e2e/loopback.test.ts` covers the legacy mode. `e2e/wss-v1-loopback.test.ts` covers `wss-v1` through
|
|
79
|
+
`createSandboxConnectionRuntime` and through the exported helpers agent-api's worker composes directly:
|
|
80
|
+
`openSupervisorConnectionBeforeDeadline`, `sendSupervisorBootstrap`, `createOrderedSupervisorInbox`,
|
|
81
|
+
`routeSupervisorFrame`, `SessionCommandPump` and `drainSupervisorConnection`.
|
|
82
|
+
|
|
51
83
|
## Durable command outcomes
|
|
52
84
|
|
|
53
85
|
A supervisor `received` ACK advances the command delivery cursor only. Implementations of
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/agent-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.0-experimental.
|
|
4
|
+
"version": "0.2.0-experimental.1",
|
|
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",
|
|
@@ -5,6 +5,7 @@ import type { BetaMessage, BetaRawMessageStreamEvent, BetaUsage } from '@anthrop
|
|
|
5
5
|
import type { AgentError, AgentEventActor, AgentUsage, JsonValue } from '../../protocol'
|
|
6
6
|
import type { AgentWorkObservation } from '../../protocol/agent-work'
|
|
7
7
|
import type { AgentEventDraft } from '../agent-event-stream'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
8
9
|
import z from 'zod'
|
|
9
10
|
import { jsonValueSchema } from '../../protocol'
|
|
10
11
|
import { canonicalToolName } from '../../protocol/agent-tool-name'
|
|
@@ -95,6 +96,7 @@ const taskListWorkOutputSchema = z.object({
|
|
|
95
96
|
}).passthrough() satisfies z.ZodType<TaskListWorkOutput>
|
|
96
97
|
|
|
97
98
|
export interface ClaudeEventMapperState {
|
|
99
|
+
readonly usageScopeId: string
|
|
98
100
|
readonly toolNameById: Map<string, string>
|
|
99
101
|
readonly pendingWorkCallById: Map<string, PendingClaudeWorkCall>
|
|
100
102
|
readonly startedMessageIds: Set<string>
|
|
@@ -118,6 +120,7 @@ export interface ClaudeEventMapperState {
|
|
|
118
120
|
|
|
119
121
|
export function createClaudeEventMapperState(): ClaudeEventMapperState {
|
|
120
122
|
return {
|
|
123
|
+
usageScopeId: `claude-query:${randomUUID()}`,
|
|
121
124
|
toolNameById: new Map(),
|
|
122
125
|
pendingWorkCallById: new Map(),
|
|
123
126
|
startedMessageIds: new Set(),
|
|
@@ -135,7 +138,7 @@ export function createClaudeEventMapperState(): ClaudeEventMapperState {
|
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
141
|
+
export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperState, options: { continueTurn?: boolean } = {}): AgentEventDraft[] {
|
|
139
142
|
const drafts: AgentEventDraft[] = []
|
|
140
143
|
|
|
141
144
|
if (message.type === 'system') {
|
|
@@ -189,23 +192,33 @@ export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperSt
|
|
|
189
192
|
if (message.type === 'result') {
|
|
190
193
|
const turnId = ensureTurnStarted(drafts, state)
|
|
191
194
|
const usage = usageFromObject(message.usage)
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
+
// SDK usage is per result, but modelUsage is cumulative within this query process.
|
|
196
|
+
// A resumed CLI resets modelUsage even when it retains the native session ID.
|
|
197
|
+
// Keep display counters separate; the control plane deduplicates/differences accounting.
|
|
195
198
|
if (usage) {
|
|
196
199
|
drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage, accounting: {
|
|
197
200
|
observationId: message.uuid,
|
|
198
|
-
mode: 'increment',
|
|
199
|
-
scopeId: turnId,
|
|
201
|
+
mode: Object.keys(message.modelUsage ?? {}).length ? 'cumulative' : 'increment',
|
|
202
|
+
scopeId: Object.keys(message.modelUsage ?? {}).length ? state.usageScopeId : turnId,
|
|
200
203
|
includesChildren: true,
|
|
201
204
|
models: Object.fromEntries(Object.entries(message.modelUsage ?? {}).map(([model, counts]) => [model, {
|
|
202
205
|
inputTokens: counts.inputTokens,
|
|
203
206
|
outputTokens: counts.outputTokens,
|
|
204
207
|
cacheReadTokens: counts.cacheReadInputTokens,
|
|
205
208
|
cacheWriteTokens: counts.cacheCreationInputTokens,
|
|
209
|
+
webSearchRequests: counts.webSearchRequests,
|
|
206
210
|
}])),
|
|
207
211
|
} } })
|
|
208
212
|
}
|
|
213
|
+
// A native steer may finish one query segment while retaining the logical turn.
|
|
214
|
+
// Usage belongs to this segment and must still be emitted exactly once. The segment
|
|
215
|
+
// may have been aborted mid-generation (error_during_execution): close whatever it
|
|
216
|
+
// streamed so the steer's user message follows a closed assistant step.
|
|
217
|
+
if (options.continueTurn) {
|
|
218
|
+
endOpenReasoning(drafts, state, turnId)
|
|
219
|
+
endOpenAssistantMessage(drafts, state, turnId)
|
|
220
|
+
return drafts
|
|
221
|
+
}
|
|
209
222
|
endOpenReasoning(drafts, state, turnId)
|
|
210
223
|
const interruptAccepted = state.interrupt.status === 'accepted' && state.interrupt.turnId === turnId
|
|
211
224
|
drafts.push({ type: 'turn.ended', turnId, actor: mainActor, payload: turnEndedPayload(message, interruptAccepted) })
|
|
@@ -599,6 +612,16 @@ function endOpenReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperSta
|
|
|
599
612
|
}
|
|
600
613
|
}
|
|
601
614
|
|
|
615
|
+
function endOpenAssistantMessage(drafts: AgentEventDraft[], state: ClaudeEventMapperState, turnId: string): void {
|
|
616
|
+
const messageId = state.currentMessageId
|
|
617
|
+
if (!messageId)
|
|
618
|
+
return
|
|
619
|
+
endAssistantMessageById(drafts, state, turnId, messageId, state.streamedTextByMessageId.get(messageId), state.actorByMessageId.get(messageId))
|
|
620
|
+
state.actorByMessageId.delete(messageId)
|
|
621
|
+
state.currentMessageId = undefined
|
|
622
|
+
state.contentBlockTypeByIndex.clear()
|
|
623
|
+
}
|
|
624
|
+
|
|
602
625
|
function reasoningIdForMessage(messageId: string): string {
|
|
603
626
|
return `reasoning-${messageId}`
|
|
604
627
|
}
|
|
@@ -96,12 +96,14 @@ export class ClaudeRun implements AgentRun {
|
|
|
96
96
|
private initializationReject: (error: unknown) => void = () => undefined
|
|
97
97
|
private initialized = false
|
|
98
98
|
private firstInputPushed = false
|
|
99
|
+
private pendingSteerResult?: Extract<SDKMessage, { type: 'result' }>
|
|
99
100
|
private modelChangeReady = true
|
|
100
101
|
private modelChangeInFlight = false
|
|
101
102
|
private finished = false
|
|
102
103
|
private interruptRequest?: { turnId: string, promise: Promise<void> }
|
|
103
104
|
private readonly acceptanceWaiters = new Map<string, AcceptanceWaiter>()
|
|
104
105
|
private readonly commandByClientMessageId = new Map<string, string>()
|
|
106
|
+
private readonly pendingMessages = new Map<string, { messageId: string, mode: 'next' | 'steer', turnId?: string, started: boolean }>()
|
|
105
107
|
|
|
106
108
|
constructor({ metadata, query, userInputRequests, input, eventMapperState, init, recordStartup }: ClaudeRunInput) {
|
|
107
109
|
this.metadata = metadata
|
|
@@ -147,6 +149,20 @@ export class ClaudeRun implements AgentRun {
|
|
|
147
149
|
return
|
|
148
150
|
}
|
|
149
151
|
this.events.push(decorated)
|
|
152
|
+
if (draft.type === 'turn.started' && draft.payload.commandId)
|
|
153
|
+
this.emitAcceptedMessage(draft.payload.commandId, draft.turnId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private emitAcceptedMessage(commandId: string, turnId: string): void {
|
|
157
|
+
const pending = this.pendingMessages.get(commandId)
|
|
158
|
+
if (!pending?.started || !this.initialized || this.finished)
|
|
159
|
+
return
|
|
160
|
+
if (pending.mode === 'steer' && pending.turnId && pending.turnId !== turnId)
|
|
161
|
+
return
|
|
162
|
+
this.pendingMessages.delete(commandId)
|
|
163
|
+
const actor = { type: 'main', actorId: 'main' } as const
|
|
164
|
+
this.events.push({ type: 'message.started', turnId, actor, payload: { messageId: pending.messageId, commandId, role: 'user' } })
|
|
165
|
+
this.events.push({ type: 'message.ended', turnId, actor, payload: { messageId: pending.messageId, commandId, role: 'user' } })
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
async requestUserInput(input: UserInputRequestInput): Promise<{ requestId: string }> {
|
|
@@ -169,6 +185,8 @@ export class ClaudeRun implements AgentRun {
|
|
|
169
185
|
this.assertCommandTargetsRun(command)
|
|
170
186
|
this.assertRunning()
|
|
171
187
|
options.signal?.throwIfAborted()
|
|
188
|
+
if (command.reasoningEffort !== undefined && command.reasoningEffort !== this.init.reasoningEffort)
|
|
189
|
+
throw new AgentNotAcceptedError({ message: 'Claude reasoning effort changes are not supported by this adapter.', recovery: { kind: 'terminal', reason: 'reasoning_effort_change_not_supported' } })
|
|
172
190
|
const input = { prompt: command.prompt, mode: command.mode, commandId: command.commandId, origin: command.origin, author: command.author, senderContext: options.senderContext }
|
|
173
191
|
const message = await mapClaudeUserMessage(input, options.inputAttachmentPreparation)
|
|
174
192
|
options.signal?.throwIfAborted()
|
|
@@ -191,12 +209,15 @@ export class ClaudeRun implements AgentRun {
|
|
|
191
209
|
this.init.requestedModel = command.model
|
|
192
210
|
options.signal?.throwIfAborted()
|
|
193
211
|
}
|
|
212
|
+
if (command.turnId && (command.mode !== 'steer' || command.turnId !== this.eventMapperState.currentTurnId))
|
|
213
|
+
throw new AgentNotAcceptedError({ message: 'The targeted claude turn is no longer active.', recovery: { kind: 'terminal', reason: 'target_turn_not_active' } })
|
|
194
214
|
this.modelChangeReady = false
|
|
195
215
|
// Register before the push: the CLI's queued receipt can outrun the local push resolution,
|
|
196
216
|
// and a receipt with no registered waiter would be lost.
|
|
197
217
|
const clientMessageId = deriveMessageId(command.commandId)
|
|
198
218
|
const receipt = this.registerAcceptanceWaiter(clientMessageId)
|
|
199
219
|
this.commandByClientMessageId.set(clientMessageId, command.commandId)
|
|
220
|
+
this.pendingMessages.set(command.commandId, { messageId: clientMessageId, mode: command.mode, turnId: command.turnId, started: false })
|
|
200
221
|
try {
|
|
201
222
|
await this.deliverInput(message)
|
|
202
223
|
// `submitted` requires the provider's positive acceptance receipt — the pinned
|
|
@@ -233,6 +254,8 @@ export class ClaudeRun implements AgentRun {
|
|
|
233
254
|
this.assertRunning()
|
|
234
255
|
options.signal?.throwIfAborted()
|
|
235
256
|
const turnId = this.eventMapperState.currentTurnId
|
|
257
|
+
if (command.turnId && command.turnId !== turnId)
|
|
258
|
+
return
|
|
236
259
|
if (!turnId)
|
|
237
260
|
return
|
|
238
261
|
if (this.eventMapperState.interrupt.status === 'accepted' && this.eventMapperState.interrupt.turnId === turnId)
|
|
@@ -291,6 +314,7 @@ export class ClaudeRun implements AgentRun {
|
|
|
291
314
|
}
|
|
292
315
|
this.acceptanceWaiters.clear()
|
|
293
316
|
this.commandByClientMessageId.clear()
|
|
317
|
+
this.pendingMessages.clear()
|
|
294
318
|
this.events.push({ type: 'session.ended', payload: error ? { reason, error } : { reason } })
|
|
295
319
|
this.events.push({ type: 'session.state.changed', payload: { state: reason === 'failed' ? 'failed' : 'stopped' } })
|
|
296
320
|
this.events.end()
|
|
@@ -322,6 +346,20 @@ export class ClaudeRun implements AgentRun {
|
|
|
322
346
|
// they reject any pending waiter as delivery-unknown so command processing can advance.
|
|
323
347
|
// Valid positive frames without a pending
|
|
324
348
|
// waiter are internal lifecycle for already-settled or non-prompt inputs and are ignored.
|
|
349
|
+
private hasPendingSteer(): boolean {
|
|
350
|
+
return [...this.pendingMessages.values()].some(pending => pending.mode === 'steer'
|
|
351
|
+
&& (!pending.turnId || pending.turnId === this.eventMapperState.currentTurnId))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private flushSteerResult(continueTurn = false): void {
|
|
355
|
+
const result = this.pendingSteerResult
|
|
356
|
+
this.pendingSteerResult = undefined
|
|
357
|
+
if (result) {
|
|
358
|
+
for (const draft of mapClaudeMessage(result, this.eventMapperState, { continueTurn }))
|
|
359
|
+
this.pushEvent(draft)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
325
363
|
private settleAcceptance(frame: ClaudeCommandLifecycleMessage): void {
|
|
326
364
|
const parsed = commandLifecycleFrameSchema.safeParse(frame)
|
|
327
365
|
if (!parsed.success) {
|
|
@@ -336,21 +374,39 @@ export class ClaudeRun implements AgentRun {
|
|
|
336
374
|
details: { expected: this.metadata.providerSessionId, actual: parsed.data.session_id },
|
|
337
375
|
})
|
|
338
376
|
}
|
|
377
|
+
const commandId = this.commandByClientMessageId.get(parsed.data.command_uuid)
|
|
339
378
|
if (parsed.data.state === 'cancelled' || parsed.data.state === 'discarded') {
|
|
379
|
+
if (commandId)
|
|
380
|
+
this.pendingMessages.delete(commandId)
|
|
340
381
|
this.acceptanceWaiters.get(parsed.data.command_uuid)?.reject(new AgentDeliveryUnknownError({
|
|
341
382
|
message: `Claude command ended as ${parsed.data.state} before acceptance was observed.`,
|
|
342
383
|
}))
|
|
343
384
|
this.commandByClientMessageId.delete(parsed.data.command_uuid)
|
|
385
|
+
if (!this.hasPendingSteer())
|
|
386
|
+
this.flushSteerResult()
|
|
344
387
|
return
|
|
345
388
|
}
|
|
346
389
|
// A queued receipt acknowledges admission, not turn ownership. Keep the
|
|
347
390
|
// mapping after acceptance settles: the provider's started receipt is
|
|
348
391
|
// the authority for the next root turn, even with several queued inputs.
|
|
349
392
|
if (parsed.data.state === 'started') {
|
|
350
|
-
|
|
393
|
+
const pending = commandId ? this.pendingMessages.get(commandId) : undefined
|
|
394
|
+
// Only a native started receipt can join the interrupted segment to a steer.
|
|
395
|
+
// A queued receipt alone must never suppress the logical turn's completion.
|
|
396
|
+
this.flushSteerResult(pending?.mode === 'steer' && (!pending.turnId || pending.turnId === this.eventMapperState.currentTurnId))
|
|
397
|
+
this.eventMapperState.currentCommandId = commandId
|
|
398
|
+
if (pending && commandId) {
|
|
399
|
+
pending.started = true
|
|
400
|
+
const turnId = this.eventMapperState.currentTurnId
|
|
401
|
+
if (pending.mode === 'steer' && turnId && (!pending.turnId || pending.turnId === turnId))
|
|
402
|
+
this.emitAcceptedMessage(commandId, turnId)
|
|
403
|
+
}
|
|
351
404
|
}
|
|
352
|
-
if (parsed.data.state === 'completed')
|
|
405
|
+
if (parsed.data.state === 'completed') {
|
|
353
406
|
this.commandByClientMessageId.delete(parsed.data.command_uuid)
|
|
407
|
+
if (commandId)
|
|
408
|
+
this.pendingMessages.delete(commandId)
|
|
409
|
+
}
|
|
354
410
|
this.acceptanceWaiters.get(parsed.data.command_uuid)?.resolve()
|
|
355
411
|
}
|
|
356
412
|
|
|
@@ -433,6 +489,17 @@ export class ClaudeRun implements AgentRun {
|
|
|
433
489
|
this.completeInitialization(message)
|
|
434
490
|
continue
|
|
435
491
|
}
|
|
492
|
+
this.flushSteerResult()
|
|
493
|
+
if (message.type === 'result' && this.hasPendingSteer()) {
|
|
494
|
+
// priority: now ends the current segment and yields its result before the
|
|
495
|
+
// cancelled/started receipts: a success after a tool call or mid-response
|
|
496
|
+
// (null stop reason), or error_during_execution when the CLI aborted the
|
|
497
|
+
// in-flight model call (the steer landed while the model was still thinking).
|
|
498
|
+
// Hold it until that native boundary proves continuation of the logical turn
|
|
499
|
+
// or its true completion; a steer must never open a new turn.
|
|
500
|
+
this.pendingSteerResult = message
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
436
503
|
const completeMessage = message.type === 'user'
|
|
437
504
|
? await hydrateClaudeToolOutput(message, {
|
|
438
505
|
transcriptPath: this.metadata.transcriptPath,
|
|
@@ -447,6 +514,7 @@ export class ClaudeRun implements AgentRun {
|
|
|
447
514
|
if (!this.initialized) {
|
|
448
515
|
throw new AgentProviderProtocolError({ message: 'Claude SDK ended before emitting system init.' })
|
|
449
516
|
}
|
|
517
|
+
this.flushSteerResult()
|
|
450
518
|
this.finish('completed')
|
|
451
519
|
}
|
|
452
520
|
catch (error) {
|
|
@@ -36,6 +36,7 @@ interface PreparedCodexTurn {
|
|
|
36
36
|
commandId: string
|
|
37
37
|
origin: 'user' | 'supervisor'
|
|
38
38
|
model?: string
|
|
39
|
+
reasoningEffort?: AgentReasoningEffort
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
interface CodexRunInput {
|
|
@@ -61,7 +62,7 @@ export class CodexRun implements AgentRun {
|
|
|
61
62
|
private readonly client: CodexJsonRpcClient
|
|
62
63
|
private readonly beforePrompt: ((signal?: AbortSignal) => Promise<void>) | undefined
|
|
63
64
|
private readonly resolveInferenceModel: (model: string) => string
|
|
64
|
-
private
|
|
65
|
+
private reasoningEffort: AgentReasoningEffort | undefined
|
|
65
66
|
private readonly onStop: () => void
|
|
66
67
|
private readonly userInputRequests: UserInputRequestStore
|
|
67
68
|
private readonly eventMapperState: CodexEventMapperState
|
|
@@ -244,12 +245,17 @@ export class CodexRun implements AgentRun {
|
|
|
244
245
|
options,
|
|
245
246
|
})
|
|
246
247
|
prepared.model = command.model
|
|
248
|
+
prepared.reasoningEffort = command.reasoningEffort
|
|
247
249
|
// A turn/started fact may have reduced during attachment preparation.
|
|
248
250
|
this.deferNextWhileTurnActive(command.mode)
|
|
249
251
|
|
|
252
|
+
if (command.turnId && (command.mode !== 'steer' || command.turnId !== this.activeTurnId))
|
|
253
|
+
throw new AgentNotAcceptedError({ message: 'The targeted codex turn is no longer active.', recovery: { kind: 'terminal', reason: 'target_turn_not_active' } })
|
|
250
254
|
const activeTurnId = this.activeTurnId
|
|
251
255
|
if (activeTurnId && command.model && command.model !== this.metadata.model)
|
|
252
256
|
throw new AgentPromptDeferredError({ message: 'Codex model changes require an idle turn boundary.' })
|
|
257
|
+
if (activeTurnId && command.reasoningEffort && command.reasoningEffort !== this.reasoningEffort)
|
|
258
|
+
throw new AgentPromptDeferredError({ message: 'Codex reasoning effort changes require an idle turn boundary.' })
|
|
253
259
|
if (command.mode === 'next' || !activeTurnId)
|
|
254
260
|
return await this.startPreparedTurn(prepared)
|
|
255
261
|
|
|
@@ -306,6 +312,8 @@ export class CodexRun implements AgentRun {
|
|
|
306
312
|
this.assertCommandTargetsRun(command)
|
|
307
313
|
this.assertRunning()
|
|
308
314
|
options.signal?.throwIfAborted()
|
|
315
|
+
if (command.turnId && command.turnId !== this.activeTurnId)
|
|
316
|
+
return
|
|
309
317
|
const turnId = command.turnId ?? this.activeTurnId
|
|
310
318
|
if (!turnId)
|
|
311
319
|
return
|
|
@@ -364,7 +372,7 @@ export class CodexRun implements AgentRun {
|
|
|
364
372
|
input: prepared.input,
|
|
365
373
|
cwd: this.metadata.cwd,
|
|
366
374
|
model: inferenceModel,
|
|
367
|
-
effort: codexEffort(this.reasoningEffort),
|
|
375
|
+
effort: codexEffort(prepared.reasoningEffort ?? this.reasoningEffort),
|
|
368
376
|
summary: codexReasoningSummary,
|
|
369
377
|
approvalPolicy: 'never',
|
|
370
378
|
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
@@ -375,6 +383,8 @@ export class CodexRun implements AgentRun {
|
|
|
375
383
|
throw mapTurnTransportLoss(error)
|
|
376
384
|
}
|
|
377
385
|
this.activeTurnId = result.turn.id
|
|
386
|
+
if (prepared.reasoningEffort)
|
|
387
|
+
this.reasoningEffort = prepared.reasoningEffort
|
|
378
388
|
if (prepared.model)
|
|
379
389
|
this.metadata.model = prepared.model
|
|
380
390
|
return { kind: 'turn', turnId: result.turn.id, placement: 'started' }
|
|
@@ -23,11 +23,13 @@ const runningCommandBaseSchema = z.object({
|
|
|
23
23
|
export const sendPromptCommandSchema = runningCommandBaseSchema.extend({
|
|
24
24
|
type: z.literal('agent.send-prompt'),
|
|
25
25
|
mode: agentPromptModeSchema,
|
|
26
|
+
turnId: z.string().min(1).optional(),
|
|
26
27
|
origin: z.enum(['user', 'supervisor']).default('user'),
|
|
27
28
|
prompt: agentPromptSchema,
|
|
28
29
|
commandId: commandIdSchema,
|
|
29
30
|
/** Resolved by the application for this prompt, including after runtime recovery. */
|
|
30
31
|
model: z.string().trim().min(1).max(256).optional(),
|
|
32
|
+
reasoningEffort: agentReasoningEffortSchema.optional(),
|
|
31
33
|
author: agentAuthorSchema.optional(),
|
|
32
34
|
}).strict()
|
|
33
35
|
|
|
@@ -9,6 +9,7 @@ export const agentUsageSchema = z.object({
|
|
|
9
9
|
cacheWriteTokens: tokenCountSchema.optional(),
|
|
10
10
|
reasoningOutputTokens: tokenCountSchema.optional(),
|
|
11
11
|
totalTokens: tokenCountSchema.optional(),
|
|
12
|
+
webSearchRequests: tokenCountSchema.optional(),
|
|
12
13
|
}).strict()
|
|
13
14
|
|
|
14
15
|
export type AgentUsage = z.infer<typeof agentUsageSchema>
|
package/src/provenance.gen.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/write-provenance.ts. Do not edit by hand.
|
|
2
2
|
export const generatedProvenance = {
|
|
3
|
-
version: "0.2.0-experimental.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-09-22T18:
|
|
3
|
+
version: "0.2.0-experimental.1",
|
|
4
|
+
sha: "e5c42a085c63ca53443040d94e1ef40b159073e2",
|
|
5
|
+
buildTime: "2026-09-22T18:27:33.931Z",
|
|
6
6
|
} as const
|
package/src/steps/contract.ts
CHANGED
|
@@ -81,8 +81,11 @@ export function splitStepText(text: string): string[] {
|
|
|
81
81
|
|
|
82
82
|
export function stepPreview(value: unknown): string {
|
|
83
83
|
const text = (typeof value === 'string' ? value : JSON.stringify(value) ?? '').replaceAll('\0', '\\0')
|
|
84
|
+
if (Buffer.byteLength(JSON.stringify(text)) <= 2048)
|
|
85
|
+
return text
|
|
84
86
|
let preview = ''
|
|
85
|
-
|
|
87
|
+
// Include the JSON quotes and truncation marker in the same preview budget.
|
|
88
|
+
let bytes = Buffer.byteLength(JSON.stringify('…'))
|
|
86
89
|
for (const character of text) {
|
|
87
90
|
const size = Buffer.byteLength(JSON.stringify(character)) - 2
|
|
88
91
|
if (bytes + size > 2048)
|
package/src/steps/projector.ts
CHANGED
|
@@ -272,8 +272,24 @@ export class DurableStepProjector {
|
|
|
272
272
|
).get(...key)?.command_id
|
|
273
273
|
const payload = 'payload' in event ? event.payload as Record<string, unknown> : event
|
|
274
274
|
const explicit = typeof payload.commandId === 'string' ? payload.commandId : undefined
|
|
275
|
-
if (previous && explicit && previous !== explicit)
|
|
275
|
+
if (previous && explicit && previous !== explicit) {
|
|
276
|
+
if ('type' in event && event.actor.type === 'main' && payload.role === 'user'
|
|
277
|
+
&& (event.type === 'message.started' || event.type === 'message.ended') && 'messageId' in payload && typeof payload.messageId === 'string') {
|
|
278
|
+
const messageStepId = stepIdFor({ sessionId: event.sessionId, providerSessionId: event.providerSessionId, actor: event.actor, turnId: event.turnId, kind: 'message', sourceId: payload.messageId })
|
|
279
|
+
const known = this.db.query<{ command_id: string | null }, [string]>(
|
|
280
|
+
'select json_extract(body,\'$.commandId\') as command_id from steps where id=?',
|
|
281
|
+
).get(messageStepId)
|
|
282
|
+
// Delayed/repeated facts for an older message must not rewind the turn's current command.
|
|
283
|
+
if (known?.command_id === explicit)
|
|
284
|
+
return explicit
|
|
285
|
+
if (!known && event.type === 'message.started') {
|
|
286
|
+
this.db.query('update turn_commands set command_id=? where session_id=? and provider_session_id=? and actor_id=? and turn_id=?')
|
|
287
|
+
.run(explicit, ...key)
|
|
288
|
+
return explicit
|
|
289
|
+
}
|
|
290
|
+
}
|
|
276
291
|
throw new Error('Conflicting command identity for a provider turn.')
|
|
292
|
+
}
|
|
277
293
|
let inherited: string | undefined
|
|
278
294
|
if (!previous && !explicit && event.actor.type === 'subagent') {
|
|
279
295
|
const toolCallId = event.actor.parentToolCallId
|
|
@@ -7,6 +7,7 @@ import type { WireEventBody } from '../supervisor-protocol/event-body'
|
|
|
7
7
|
import type { SupervisorAgentRunSnapshot } from '../supervisor-protocol/supervisor-agent-run-snapshot'
|
|
8
8
|
import type { SupervisorAgentRuntime, SupervisorProviderFactory } from './provider-factory'
|
|
9
9
|
import type { SupervisorRpcClient } from './rpc-client'
|
|
10
|
+
import { Database } from 'bun:sqlite'
|
|
10
11
|
import { wrapAgentEvent } from '../supervisor-protocol/agent-event-wrapper'
|
|
11
12
|
import { createSupervisorProviderFactory } from './provider-factory'
|
|
12
13
|
|
|
@@ -15,6 +16,40 @@ export interface AgentProviderBindingStore {
|
|
|
15
16
|
save: (input: { sessionId: string, provider: AgentProviderId, providerSessionId: string }) => Promise<void>
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** Local provider identity only; credentials and transcripts do not belong in this store. */
|
|
20
|
+
export class SqliteAgentProviderBindingStore implements AgentProviderBindingStore {
|
|
21
|
+
private readonly db: Database
|
|
22
|
+
|
|
23
|
+
constructor(path: string) {
|
|
24
|
+
this.db = new Database(path, { create: true })
|
|
25
|
+
this.db.exec(`pragma journal_mode=WAL; pragma synchronous=FULL;
|
|
26
|
+
create table if not exists provider_bindings(
|
|
27
|
+
session_id text not null,
|
|
28
|
+
provider text not null,
|
|
29
|
+
provider_session_id text not null,
|
|
30
|
+
primary key(session_id, provider)
|
|
31
|
+
);`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async load(input: { sessionId: string, provider: AgentProviderId }): Promise<string | undefined> {
|
|
35
|
+
return this.db.query<{ provider_session_id: string }, [string, string]>(
|
|
36
|
+
'select provider_session_id from provider_bindings where session_id=? and provider=?',
|
|
37
|
+
).get(input.sessionId, input.provider)?.provider_session_id
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async save(input: { sessionId: string, provider: AgentProviderId, providerSessionId: string }): Promise<void> {
|
|
41
|
+
if (!input.sessionId || !input.providerSessionId)
|
|
42
|
+
throw new Error('Provider binding requires session and provider session identities.')
|
|
43
|
+
this.db.query(`insert into provider_bindings(session_id,provider,provider_session_id) values (?,?,?)
|
|
44
|
+
on conflict(session_id,provider) do update set provider_session_id=excluded.provider_session_id`)
|
|
45
|
+
.run(input.sessionId, input.provider, input.providerSessionId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
close(): void {
|
|
49
|
+
this.db.close()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
export interface AgentSupervisorProviderFactoryOptions {
|
|
19
54
|
eventProcessor?: {
|
|
20
55
|
recover: (emit: (body: WireEventBody) => void, options?: { interruptUnfinishedTurns: boolean }) => Promise<void>
|
|
@@ -65,11 +100,28 @@ export function createAgentSupervisorProviderFactory(
|
|
|
65
100
|
? await provider.resumeSession({ ...sessionInput, provider: 'codex', providerSessionId: previousProviderSessionId }, { signal: input.signal })
|
|
66
101
|
: await provider.openSession({ ...sessionInput, provider: 'codex' }, { signal: input.signal })
|
|
67
102
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
try {
|
|
104
|
+
await options.bindingStore.save({
|
|
105
|
+
sessionId: input.bootstrap.sessionId,
|
|
106
|
+
provider: provider.id,
|
|
107
|
+
providerSessionId: run.metadata.providerSessionId,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
try {
|
|
112
|
+
await run.stop({
|
|
113
|
+
type: 'agent.stop',
|
|
114
|
+
provider: run.metadata.provider,
|
|
115
|
+
sessionId: run.metadata.sessionId,
|
|
116
|
+
providerSessionId: run.metadata.providerSessionId,
|
|
117
|
+
reason: 'provider_binding_persistence_failed',
|
|
118
|
+
}, { signal: new AbortController().signal })
|
|
119
|
+
}
|
|
120
|
+
catch (cleanupError) {
|
|
121
|
+
throw new AggregateError([error, cleanupError], 'Provider binding persistence and cleanup failed.')
|
|
122
|
+
}
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
73
125
|
|
|
74
126
|
return new AgentSupervisorRuntime({
|
|
75
127
|
run,
|
|
@@ -138,7 +190,9 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
138
190
|
providerSessionId: metadata.providerSessionId,
|
|
139
191
|
commandId: input.commandId,
|
|
140
192
|
mode: input.body.mode,
|
|
193
|
+
turnId: input.body.turnId,
|
|
141
194
|
model: input.body.model,
|
|
195
|
+
reasoningEffort: input.body.reasoningEffort,
|
|
142
196
|
origin: 'user',
|
|
143
197
|
prompt: input.body.prompt,
|
|
144
198
|
author: input.body.user
|
|
@@ -158,6 +212,7 @@ class AgentSupervisorRuntime implements SupervisorAgentRuntime {
|
|
|
158
212
|
if (input.body.type === 'agent.interrupt') {
|
|
159
213
|
await this.input.run.interrupt({
|
|
160
214
|
type: 'agent.interrupt',
|
|
215
|
+
turnId: input.body.turnId,
|
|
161
216
|
provider: metadata.provider,
|
|
162
217
|
sessionId: metadata.sessionId,
|
|
163
218
|
providerSessionId: metadata.providerSessionId,
|
|
@@ -51,8 +51,8 @@ export function createBootstrapBinder(input: {
|
|
|
51
51
|
outcome: 'rejected',
|
|
52
52
|
ack: {
|
|
53
53
|
kind: 'command.ack',
|
|
54
|
-
commandId: request.envelope.body.initialCommand.
|
|
55
|
-
commandSeq: request.envelope.body.initialCommand
|
|
54
|
+
commandId: request.envelope.body.initialCommand?.commandId ?? `bootstrap:${request.envelope.body.sessionId}`,
|
|
55
|
+
commandSeq: request.envelope.body.initialCommand?.commandSeq ?? 1,
|
|
56
56
|
status: 'rejected',
|
|
57
57
|
errorCode: error.code,
|
|
58
58
|
detail: error.publicMessage.slice(0, 240),
|
|
@@ -123,6 +123,7 @@ export function createBootstrapBinder(input: {
|
|
|
123
123
|
outcome: 'accepted',
|
|
124
124
|
runtimeState: {
|
|
125
125
|
kind: 'runtime.state',
|
|
126
|
+
...(request.envelope.body.commandTransport === 'wss-v1' ? { sourceId: acceptedBinding.store.journalSourceId() } : {}),
|
|
126
127
|
lastReceivedCommandSeq: acceptedBinding.store.lastReceivedCommandSeq(),
|
|
127
128
|
nextEventSeq: acceptedBinding.store.localEventMax() + 1,
|
|
128
129
|
agentRun: acceptedBinding.store.agentRunSnapshot(),
|
package/src/supervisor/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createAgentSupervisorProviderFactory } from './agent-provider-factory'
|
|
1
|
+
export { createAgentSupervisorProviderFactory, SqliteAgentProviderBindingStore } from './agent-provider-factory'
|
|
2
2
|
export type { AgentProviderBindingStore, AgentSupervisorProviderFactoryOptions } from './agent-provider-factory'
|
|
3
3
|
export {
|
|
4
4
|
CLEAN_EXIT_CODE,
|
|
@@ -46,6 +46,9 @@ export interface PersistedSupervisorBinding {
|
|
|
46
46
|
runtimeConnectionAttemptId: string
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** A command whose sequence is already journaled under a different identity or body. */
|
|
50
|
+
export const commandSequenceConflictCode = 'agent-core.command-sequence-conflict'
|
|
51
|
+
|
|
49
52
|
export class SupervisorStore {
|
|
50
53
|
private closed = false
|
|
51
54
|
|
|
@@ -113,12 +116,14 @@ export class SupervisorStore {
|
|
|
113
116
|
now,
|
|
114
117
|
)
|
|
115
118
|
this.insertAttempt(input)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
if (input.body.initialCommand) {
|
|
120
|
+
this.insertCommand({
|
|
121
|
+
kind: 'command',
|
|
122
|
+
commandId: input.body.initialCommand.commandId,
|
|
123
|
+
commandSeq: input.body.initialCommand.commandSeq,
|
|
124
|
+
body: input.body.initialCommand.body,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
122
127
|
return {
|
|
123
128
|
body: input.body,
|
|
124
129
|
bodyHash,
|
|
@@ -175,12 +180,12 @@ export class SupervisorStore {
|
|
|
175
180
|
if (existing.command_id !== envelope.commandId
|
|
176
181
|
|| existing.body_hash !== sha256CanonicalJson(body)
|
|
177
182
|
|| existing.body_json !== canonicalJson(body)) {
|
|
178
|
-
throw new SupervisorPersistenceError({ message: 'Command sequence conflicts with durable inbox state.' })
|
|
183
|
+
throw new SupervisorPersistenceError({ code: commandSequenceConflictCode, message: 'Command sequence conflicts with durable inbox state.' })
|
|
179
184
|
}
|
|
180
185
|
return { status: 'received', duplicate: true }
|
|
181
186
|
}
|
|
182
187
|
const maximum = this.lastReceivedCommandSeq()
|
|
183
|
-
if (envelope.commandSeq !== maximum + 1)
|
|
188
|
+
if (this.binding()?.body.commandTransport !== 'wss-v1' && envelope.commandSeq !== maximum + 1)
|
|
184
189
|
throw new SupervisorPersistenceError({ message: 'Command sequence contains a gap.' })
|
|
185
190
|
try {
|
|
186
191
|
this.insertCommand({ ...envelope, body })
|
|
@@ -240,6 +245,15 @@ export class SupervisorStore {
|
|
|
240
245
|
})()
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
/** Stable identity of this SQLite journal, independent of socket and worker. */
|
|
249
|
+
journalSourceId(): string {
|
|
250
|
+
this.database.query('INSERT OR IGNORE INTO metadata (key, value) VALUES (\'journal_source_id\', ?)').run(crypto.randomUUID())
|
|
251
|
+
const row = this.database.query<{ value: string }, []>('SELECT value FROM metadata WHERE key = \'journal_source_id\'').get()
|
|
252
|
+
if (!row)
|
|
253
|
+
throw new SupervisorPersistenceError({ message: 'Missing journal identity.' })
|
|
254
|
+
return row.value
|
|
255
|
+
}
|
|
256
|
+
|
|
243
257
|
lastReceivedCommandSeq(): number {
|
|
244
258
|
return this.database.query<{ maximum: number }, []>('SELECT COALESCE(MAX(seq), 0) AS maximum FROM commands').get()?.maximum ?? 0
|
|
245
259
|
}
|