@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.
Files changed (90) hide show
  1. package/README.md +82 -0
  2. package/docs/consumer-capability-matrix.md +374 -0
  3. package/package.json +17 -10
  4. package/scripts/generate-codex-protocol.ts +711 -0
  5. package/src/agents/agent-operation.ts +8 -0
  6. package/src/agents/agent-provider.ts +3 -0
  7. package/src/agents/agent-run.ts +14 -2
  8. package/src/agents/agent-runtime-error.ts +28 -0
  9. package/src/agents/agent-session-events.ts +33 -0
  10. package/src/agents/agent-tool-runner.ts +12 -11
  11. package/src/agents/agent-tool.ts +12 -4
  12. package/src/agents/author-context.ts +1 -1
  13. package/src/agents/claude/claude-auth.ts +40 -0
  14. package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
  15. package/src/agents/claude/claude-code-process.ts +98 -0
  16. package/src/agents/claude/claude-command-mapper.ts +63 -3
  17. package/src/agents/claude/claude-desktop.ts +55 -0
  18. package/src/agents/claude/claude-event-mapper.ts +876 -124
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +344 -29
  21. package/src/agents/claude/claude-run.ts +922 -91
  22. package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
  23. package/src/agents/claude/claude-skill-catalog.ts +75 -0
  24. package/src/agents/claude/claude-steered-notifications.ts +180 -0
  25. package/src/agents/claude/claude-tool-mapper.ts +81 -2
  26. package/src/agents/claude/claude-workspace-skills.ts +87 -0
  27. package/src/agents/claude/index.ts +2 -0
  28. package/src/agents/codex/codex-auth-mapper.ts +27 -0
  29. package/src/agents/codex/codex-auth.ts +19 -0
  30. package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
  31. package/src/agents/codex/codex-browser-auth-status.ts +55 -0
  32. package/src/agents/codex/codex-command-mapper.ts +38 -3
  33. package/src/agents/codex/codex-event-mapper.ts +375 -64
  34. package/src/agents/codex/codex-json-rpc-client.ts +66 -21
  35. package/src/agents/codex/codex-provider.ts +118 -236
  36. package/src/agents/codex/codex-run-auth.ts +113 -0
  37. package/src/agents/codex/codex-run.ts +428 -48
  38. package/src/agents/codex/codex-tool-mapper.ts +2 -2
  39. package/src/agents/codex/codex.errors.ts +8 -0
  40. package/src/agents/codex/generated/meta.gen.ts +144 -6
  41. package/src/agents/codex/generated/namespaces.gen.ts +92 -3
  42. package/src/agents/codex/generated/schema.gen.ts +24698 -15638
  43. package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
  44. package/src/agents/index.ts +5 -0
  45. package/src/agents/input-attachment-preparation.ts +13 -0
  46. package/src/agents/mcp-tool-content.ts +82 -0
  47. package/src/agents/model-rebinding-run.ts +14 -3
  48. package/src/agents/selected-repository-instructions.ts +202 -0
  49. package/src/agents/session-context.ts +41 -0
  50. package/src/agents/subscription-token-refresh.ts +73 -0
  51. package/src/agents/user-input-request.ts +19 -20
  52. package/src/errors/index.ts +1 -1
  53. package/src/errors/validation-error.ts +6 -0
  54. package/src/protocol/agent-catalog.ts +159 -0
  55. package/src/protocol/agent-command.ts +54 -7
  56. package/src/protocol/agent-content.ts +36 -16
  57. package/src/protocol/agent-event.ts +210 -73
  58. package/src/protocol/agent-identity.ts +29 -0
  59. package/src/protocol/agent-json.ts +122 -8
  60. package/src/protocol/agent-metadata.ts +2 -2
  61. package/src/protocol/agent-model.ts +13 -1
  62. package/src/protocol/agent-notification.ts +110 -0
  63. package/src/protocol/agent-presentation.ts +71 -20
  64. package/src/protocol/agent-tool.ts +66 -10
  65. package/src/protocol/agent-usage.ts +4 -1
  66. package/src/protocol/agent-user-input.ts +4 -4
  67. package/src/protocol/agent-work.ts +12 -10
  68. package/src/protocol/index.ts +2 -0
  69. package/src/protocol/rejected-keys.ts +11 -0
  70. package/src/provenance.gen.ts +3 -3
  71. package/src/steps/contract.ts +4 -1
  72. package/src/steps/projector.ts +17 -1
  73. package/src/supervisor/agent-provider-factory.ts +60 -5
  74. package/src/supervisor/bootstrap-binder.ts +3 -2
  75. package/src/supervisor/index.ts +1 -1
  76. package/src/supervisor/persistence/supervisor-store.ts +22 -8
  77. package/src/supervisor/runtime-handler.ts +30 -2
  78. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
  79. package/src/supervisor-protocol/bootstrap.ts +4 -1
  80. package/src/supervisor-protocol/command-body.ts +4 -1
  81. package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
  82. package/src/testing/in-memory-runtime-control-plane.ts +118 -12
  83. package/src/testing/index.ts +1 -1
  84. package/src/worker-runtime-client/command-pump.ts +85 -10
  85. package/src/worker-runtime-client/connection-attempt.ts +95 -20
  86. package/src/worker-runtime-client/frame-processor.ts +242 -58
  87. package/src/worker-runtime-client/index.ts +8 -2
  88. package/src/worker-runtime-client/ports.ts +34 -2
  89. package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
  90. package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
@@ -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
- let bytes = 0
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)
@@ -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
- await options.bindingStore.save({
69
- sessionId: input.bootstrap.sessionId,
70
- provider: provider.id,
71
- providerSessionId: run.metadata.providerSessionId,
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.commandId,
55
- commandSeq: request.envelope.body.initialCommand.commandSeq,
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(),
@@ -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
- this.insertCommand({
117
- kind: 'command',
118
- commandId: input.body.initialCommand.commandId,
119
- commandSeq: input.body.initialCommand.commandSeq,
120
- body: input.body.initialCommand.body,
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
  }
@@ -6,12 +6,15 @@ import type { EventEnvelope } from '../supervisor-protocol/envelopes/supervisor-
6
6
  import type { WireEventBody } from '../supervisor-protocol/event-body'
7
7
  import type { SupervisorStore } from './persistence/supervisor-store'
8
8
  import type { SupervisorAgentRuntime, SupervisorProviderFactory } from './provider-factory'
9
+ import { ZodError } from 'zod'
9
10
  import { jsonUtf8ByteLength } from '../protocol'
10
11
  import { durabilityOf } from '../supervisor-protocol/durability'
11
12
  import { wireEventBodySchema } from '../supervisor-protocol/event-body'
12
13
  import { maxDurableEventBytes, maxEphemeralDeltaBytes } from '../supervisor-protocol/payload-overflow'
13
14
  import { decodeControlPlaneFrame, encodeWireFrame } from '../supervisor-protocol/wire-codec'
14
15
  import { EphemeralFrameBuffer } from './ephemeral-frame-buffer'
16
+ import { SupervisorPersistenceError } from './errors'
17
+ import { commandSequenceConflictCode } from './persistence/supervisor-store'
15
18
  import { SupervisorRpcClient } from './rpc-client'
16
19
 
17
20
  export interface ResidentSupervisorSocket {
@@ -99,8 +102,24 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
99
102
  this.schedulePendingCommands()
100
103
  }
101
104
  catch (error) {
102
- this.dependencies.logger.warn({ err: error }, 'rejecting invalid supervisor command')
103
- socket.close(1008, 'Supervisor command was rejected.')
105
+ const rejection = this.dependencies.store.binding()?.body.commandTransport === 'wss-v1'
106
+ ? terminalCommandRejection(error)
107
+ : undefined
108
+ if (!rejection) {
109
+ this.dependencies.logger.warn({ err: error }, 'rejecting invalid supervisor command')
110
+ socket.close(1008, 'Supervisor command was rejected.')
111
+ return
112
+ }
113
+ // wss-v1: answer a terminal rejection so the worker persists it instead of
114
+ // redelivering the same command on every reconnect.
115
+ this.dependencies.logger.warn({ err: error, commandSeq: frame.commandSeq, errorCode: rejection.errorCode }, 'rejecting supervisor command')
116
+ this.sendRetained(encodeWireFrame({
117
+ kind: 'command.ack',
118
+ commandId: frame.commandId,
119
+ commandSeq: frame.commandSeq,
120
+ status: 'rejected',
121
+ ...rejection,
122
+ }))
104
123
  }
105
124
  return
106
125
  }
@@ -328,3 +347,12 @@ function frameText(data: string | ArrayBuffer | Uint8Array): string {
328
347
  return data
329
348
  return new TextDecoder().decode(data)
330
349
  }
350
+
351
+ /** Only failures the same frame would reproduce are terminal; storage faults still close the socket. */
352
+ function terminalCommandRejection(error: unknown): { errorCode: string, detail: string } | undefined {
353
+ if (error instanceof ZodError)
354
+ return { errorCode: 'supervisor.command.invalid-body', detail: 'Command body failed wire validation.' }
355
+ if (error instanceof SupervisorPersistenceError && error.code === commandSequenceConflictCode)
356
+ return { errorCode: 'supervisor.command.conflicting-duplicate', detail: error.message.slice(0, 240) }
357
+ return undefined
358
+ }
@@ -2,16 +2,20 @@ import type { AgentEvent } from '../protocol'
2
2
  import z from 'zod'
3
3
  import {
4
4
  contextCompactionCompletedAgentEventSchema,
5
+ contextCompactionRequestCompletedAgentEventSchema,
5
6
  contextCompactionStartedAgentEventSchema,
7
+ contextUpdatedAgentEventSchema,
6
8
  errorAgentEventSchema,
7
9
  messageDeltaAgentEventSchema,
8
10
  messageEndedAgentEventSchema,
9
11
  messageStartedAgentEventSchema,
12
+ promptTurnCorrelatedAgentEventSchema,
10
13
  reasoningEndedAgentEventSchema,
11
14
  reasoningStartedAgentEventSchema,
12
15
  reasoningSummaryDeltaAgentEventSchema,
13
16
  sessionConfiguredAgentEventSchema,
14
17
  sessionEndedAgentEventSchema,
18
+ sessionSettingsChangedAgentEventSchema,
15
19
  sessionSkillsUpdatedAgentEventSchema,
16
20
  sessionStartedAgentEventSchema,
17
21
  sessionStateChangedAgentEventSchema,
@@ -21,6 +25,7 @@ import {
21
25
  toolCallCompletedAgentEventSchema,
22
26
  toolCallStartedAgentEventSchema,
23
27
  toolOutputDeltaAgentEventSchema,
28
+ toolOutputUpdatedAgentEventSchema,
24
29
  turnEndedAgentEventSchema,
25
30
  turnStartedAgentEventSchema,
26
31
  usageAgentEventSchema,
@@ -42,6 +47,10 @@ export const wrappedSessionConfiguredAgentEventSchema = sessionConfiguredAgentEv
42
47
  type: z.literal('agent.session.configured'),
43
48
  }).strict()
44
49
 
50
+ export const wrappedSessionSettingsChangedAgentEventSchema = sessionSettingsChangedAgentEventSchema.extend({
51
+ type: z.literal('agent.session.settings.changed'),
52
+ }).strict()
53
+
45
54
  export const wrappedSessionSkillsUpdatedAgentEventSchema = sessionSkillsUpdatedAgentEventSchema.extend({
46
55
  type: z.literal('agent.session.skills.updated'),
47
56
  }).strict()
@@ -58,6 +67,10 @@ export const wrappedTurnStartedAgentEventSchema = turnStartedAgentEventSchema.ex
58
67
  type: z.literal('agent.turn.started'),
59
68
  }).strict()
60
69
 
70
+ export const wrappedPromptTurnCorrelatedAgentEventSchema = promptTurnCorrelatedAgentEventSchema.extend({
71
+ type: z.literal('agent.prompt.turn-correlated'),
72
+ }).strict()
73
+
61
74
  export const wrappedTurnEndedAgentEventSchema = turnEndedAgentEventSchema.extend({
62
75
  type: z.literal('agent.turn.ended'),
63
76
  }).strict()
@@ -98,6 +111,10 @@ export const wrappedToolOutputDeltaAgentEventSchema = toolOutputDeltaAgentEventS
98
111
  type: z.literal('agent.tool.output.delta'),
99
112
  }).strict()
100
113
 
114
+ export const wrappedToolOutputUpdatedAgentEventSchema = toolOutputUpdatedAgentEventSchema.extend({
115
+ type: z.literal('agent.tool.output.updated'),
116
+ }).strict()
117
+
101
118
  export const wrappedToolCallCompletedAgentEventSchema = toolCallCompletedAgentEventSchema.extend({
102
119
  type: z.literal('agent.tool.call.completed'),
103
120
  }).strict()
@@ -118,6 +135,10 @@ export const wrappedUsageAgentEventSchema = usageAgentEventSchema.extend({
118
135
  type: z.literal('agent.usage'),
119
136
  }).strict()
120
137
 
138
+ export const wrappedContextUpdatedAgentEventSchema = contextUpdatedAgentEventSchema.extend({
139
+ type: z.literal('agent.context.updated'),
140
+ }).strict()
141
+
121
142
  export const wrappedContextCompactionStartedAgentEventSchema = contextCompactionStartedAgentEventSchema.extend({
122
143
  type: z.literal('agent.context.compaction.started'),
123
144
  }).strict()
@@ -126,6 +147,10 @@ export const wrappedContextCompactionCompletedAgentEventSchema = contextCompacti
126
147
  type: z.literal('agent.context.compaction.completed'),
127
148
  }).strict()
128
149
 
150
+ export const wrappedContextCompactionRequestCompletedAgentEventSchema = contextCompactionRequestCompletedAgentEventSchema.extend({
151
+ type: z.literal('agent.context.compaction.request.completed'),
152
+ }).strict()
153
+
129
154
  export const wrappedUserInputRequestedAgentEventSchema = userInputRequestedAgentEventSchema.extend({
130
155
  type: z.literal('agent.user-input.requested'),
131
156
  }).strict()
@@ -141,10 +166,12 @@ export const wrappedErrorAgentEventSchema = errorAgentEventSchema.extend({
141
166
  export const wrappedAgentEventSchema = z.discriminatedUnion('type', [
142
167
  wrappedSessionStartedAgentEventSchema,
143
168
  wrappedSessionConfiguredAgentEventSchema,
169
+ wrappedSessionSettingsChangedAgentEventSchema,
144
170
  wrappedSessionSkillsUpdatedAgentEventSchema,
145
171
  wrappedSessionStateChangedAgentEventSchema,
146
172
  wrappedSessionEndedAgentEventSchema,
147
173
  wrappedTurnStartedAgentEventSchema,
174
+ wrappedPromptTurnCorrelatedAgentEventSchema,
148
175
  wrappedTurnEndedAgentEventSchema,
149
176
  wrappedWorkObservedAgentEventSchema,
150
177
  wrappedMessageStartedAgentEventSchema,
@@ -155,13 +182,16 @@ export const wrappedAgentEventSchema = z.discriminatedUnion('type', [
155
182
  wrappedReasoningEndedAgentEventSchema,
156
183
  wrappedToolCallStartedAgentEventSchema,
157
184
  wrappedToolOutputDeltaAgentEventSchema,
185
+ wrappedToolOutputUpdatedAgentEventSchema,
158
186
  wrappedToolCallCompletedAgentEventSchema,
159
187
  wrappedSubagentStartedAgentEventSchema,
160
188
  wrappedSubagentProgressAgentEventSchema,
161
189
  wrappedSubagentEndedAgentEventSchema,
162
190
  wrappedUsageAgentEventSchema,
191
+ wrappedContextUpdatedAgentEventSchema,
163
192
  wrappedContextCompactionStartedAgentEventSchema,
164
193
  wrappedContextCompactionCompletedAgentEventSchema,
194
+ wrappedContextCompactionRequestCompletedAgentEventSchema,
165
195
  wrappedUserInputRequestedAgentEventSchema,
166
196
  wrappedUserInputResolvedAgentEventSchema,
167
197
  wrappedErrorAgentEventSchema,
@@ -59,12 +59,15 @@ export const sessionBootstrapBodySchema = z.object({
59
59
  }),
60
60
  gitBotIdentity: gitIdentitySchema.optional(),
61
61
  profile: sessionProfileSchema.optional(),
62
+ commandTransport: z.literal('wss-v1').optional(),
62
63
  initialCommand: z.object({
63
64
  commandId: z.string().min(1),
64
65
  commandSeq: z.literal(1),
65
66
  body: wireCommandBodySchema,
66
- }).strict(),
67
+ }).strict().optional(),
67
68
  }).strict().superRefine((body, context) => {
69
+ if (body.commandTransport === 'wss-v1' ? body.initialCommand !== undefined : body.initialCommand === undefined)
70
+ context.addIssue({ code: 'custom', path: ['initialCommand'], message: 'wss-v1 bootstraps without a command; legacy bootstrap requires command one.' })
68
71
  const workspacePrefix = `${body.agent.cwd}/`
69
72
  for (const [index, repository] of body.repositories.entries()) {
70
73
  if (repository.root !== body.agent.cwd && !repository.root.startsWith(workspacePrefix)) {
@@ -1,5 +1,5 @@
1
1
  import z from 'zod'
2
- import { agentPromptModeSchema, agentPromptSchema, agentUserInputAnswersSchema, jsonObjectSchema, jsonUtf8ByteLength } from '../protocol'
2
+ import { agentPromptModeSchema, agentPromptSchema, agentReasoningEffortSchema, agentUserInputAnswersSchema, jsonObjectSchema, jsonUtf8ByteLength } from '../protocol'
3
3
 
4
4
  const wireSenderContextSchema = z.discriminatedUnion('kind', [
5
5
  z.object({ kind: z.literal('application') }).strict(),
@@ -40,7 +40,9 @@ export const wireCommandExtensionsSchema = jsonObjectSchema.refine(
40
40
  const wireSendPromptCommandSchema = z.object({
41
41
  type: z.literal('agent.send-prompt'),
42
42
  mode: agentPromptModeSchema,
43
+ turnId: z.string().min(1).optional(),
43
44
  model: z.string().trim().min(1).max(256).optional(),
45
+ reasoningEffort: agentReasoningEffortSchema.optional(),
44
46
  prompt: agentPromptSchema,
45
47
  attachments: z.array(wireInputAttachmentRefSchema).superRefine((attachments, context) => {
46
48
  const attachmentIds = new Set<string>()
@@ -60,6 +62,7 @@ const wireSendPromptCommandSchema = z.object({
60
62
 
61
63
  const wireInterruptCommandSchema = z.object({
62
64
  type: z.literal('agent.interrupt'),
65
+ turnId: z.string().min(1).optional(),
63
66
  }).strict()
64
67
 
65
68
  const wireRespondUserInputCommandSchema = z.object({
@@ -14,6 +14,7 @@ export const commandAckErrorCodeSchema = z.string()
14
14
 
15
15
  export const runtimeStateEnvelopeSchema = z.object({
16
16
  kind: z.literal('runtime.state'),
17
+ sourceId: z.string().uuid().optional(),
17
18
  lastReceivedCommandSeq: z.number().int().nonnegative(),
18
19
  nextEventSeq: eventSequenceSchema,
19
20
  agentRun: supervisorAgentRunSnapshotSchema,
@@ -9,6 +9,7 @@ import type {
9
9
  CommandSource,
10
10
  ConnectionAuthorityStore,
11
11
  ConnectionClaim,
12
+ ConnectionReadinessRejection,
12
13
  DurableEventSink,
13
14
  EphemeralEventSink,
14
15
  RpcDispatcher,
@@ -19,18 +20,37 @@ import { ulid } from 'ulid'
19
20
 
20
21
  type NotificationCallbacks = Parameters<RuntimeNotifications['start']>[0]
21
22
 
23
+ /** Admission state of an in-memory command; only wss-v1 selection consults it. */
24
+ type InMemoryCommandStatus = 'queued' | 'dispatched' | 'received' | 'withdrawn' | 'rejected'
25
+
26
+ /** Code the fixture answers when a session sandbox announces a different journal than it pinned. */
27
+ export const inMemoryJournalIdentityChangedCode = 'testing.journal-identity-changed'
28
+
22
29
  export class InMemoryRuntimeControlPlane implements ConnectionAuthorityStore, CommandSource, DurableEventSink,
23
30
  EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, RuntimeNotifications {
24
31
  readonly durableEventLog: DurableEventEnvelope[] = []
25
32
  readonly durableRecordAttempts: number[] = []
26
33
  readonly ephemeralEventLog: Array<Parameters<EphemeralEventSink['publish']>[0]> = []
27
34
  readonly readySnapshots: SupervisorAgentRunSnapshot[] = []
35
+ /** `lastReceivedCommandSeq` announced by each accepted bootstrap, in readiness order. */
36
+ readonly readyReceivedSequences: number[] = []
28
37
  readonly projectedCommandSequences: number[] = []
29
38
  readonly failedCommandSequences: number[] = []
39
+ /** wss-v1 receipts, in the order the pump persisted them. */
40
+ readonly receivedCommandSequences: number[] = []
41
+ /** wss-v1 terminal rejections, in the order the pump persisted them. */
42
+ readonly rejectedCommands: Array<{ commandSeq: number, errorCode: string, detail?: string }> = []
43
+ /** Journal identity announced by each accepted readiness, in order. */
44
+ readonly readyJournalSourceIds: Array<string | undefined> = []
45
+ /** Every wss-v1 selection with the receipts that were durable when it ran. */
46
+ readonly pendingSelections: Array<{ commandSeq: number | undefined, receivedBefore: number[] }> = []
30
47
  readonly failureLog: Array<{ kind: string, code?: string, detail?: string }> = []
31
48
  readonly rpcCalls: SupervisorRpcRequestEnvelope[] = []
32
49
  onDurableRecord: ((event: DurableEventEnvelope) => void | Promise<void>) | undefined
33
- private readonly commandLog: CommandEnvelope[]
50
+ private readonly commandLog: Array<CommandEnvelope & { body: WireCommandBody }>
51
+ private readonly commandStatus = new Map<number, InMemoryCommandStatus>()
52
+ private readonly wssV1: boolean
53
+ private pinnedJournalSourceId: string | undefined
34
54
  private notifications: NotificationCallbacks | undefined
35
55
  private claimed = false
36
56
  private generation = 0
@@ -41,30 +61,49 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
41
61
  ref: { sessionId: string, sessionSandboxId: string, providerSandboxId: string }
42
62
  trafficAccessTokenEncrypted: string
43
63
  bootstrap: Omit<SessionBootstrapBody, 'sessionId' | 'initialCommand'>
44
- initialCommand: { commandId: string, body: WireCommandBody }
64
+ /** Required by the legacy bootstrap, forbidden by `commandTransport: 'wss-v1'`. */
65
+ initialCommand?: { commandId: string, body: WireCommandBody }
45
66
  secrets?: Awaited<ReturnType<BootstrapProvider['issueEphemeralSecrets']>>
46
67
  rpcHandlers?: Readonly<Record<string, (body: unknown) => unknown | Promise<unknown>>>
68
+ /** Journal this session sandbox was already bound to before the runtime under test started. */
69
+ pinnedJournalSourceId?: string
47
70
  }) {
48
- this.commandLog = [{
49
- kind: 'command',
50
- commandId: input.initialCommand.commandId,
51
- commandSeq: 1,
52
- body: input.initialCommand.body,
53
- }]
71
+ this.wssV1 = input.bootstrap.commandTransport === 'wss-v1'
72
+ this.pinnedJournalSourceId = input.pinnedJournalSourceId
73
+ if (this.wssV1 === (input.initialCommand !== undefined))
74
+ throw new Error('wss-v1 bootstraps without an initial command; the legacy bootstrap requires one.')
75
+ this.commandLog = input.initialCommand
76
+ ? [{
77
+ kind: 'command',
78
+ commandId: input.initialCommand.commandId,
79
+ commandSeq: 1,
80
+ body: input.initialCommand.body,
81
+ }]
82
+ : []
83
+ for (const command of this.commandLog)
84
+ this.commandStatus.set(command.commandSeq, 'queued')
54
85
  }
55
86
 
56
87
  enqueueCommand(input: { commandId?: string, body: WireCommandBody }): CommandEnvelope {
57
- const command: CommandEnvelope = {
58
- kind: 'command',
88
+ const command = {
89
+ kind: 'command' as const,
59
90
  commandId: input.commandId ?? ulid(),
60
91
  commandSeq: this.commandLog.length + 1,
61
92
  body: input.body,
62
93
  }
63
94
  this.commandLog.push(command)
95
+ this.commandStatus.set(command.commandSeq, 'queued')
64
96
  this.notifications?.onCommandsReady(this.input.ref.sessionId)
65
97
  return command
66
98
  }
67
99
 
100
+ /** Withdraw an admitted command before dispatch; its sequence stays allocated, leaving a gap. */
101
+ withdrawCommand(commandSeq: number): void {
102
+ if (this.commandStatus.get(commandSeq) !== 'queued')
103
+ throw new Error(`Command ${commandSeq} is not queued and cannot be withdrawn.`)
104
+ this.commandStatus.set(commandSeq, 'withdrawn')
105
+ }
106
+
68
107
  demandConnection(): void {
69
108
  this.notifications?.onConnectionDemand(this.input.ref.sessionSandboxId)
70
109
  }
@@ -106,7 +145,9 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
106
145
  return {
107
146
  renewed,
108
147
  ...(renewed ? { leaseExpiresAt: new Date(Date.now() + 30_000) } : {}),
109
- hasOutstandingCommand: this.projectedCommandSequences.length + this.failedCommandSequences.length < this.commandLog.length,
148
+ hasOutstandingCommand: this.wssV1
149
+ ? this.deliverableCommands().length > 0
150
+ : this.projectedCommandSequences.length + this.failedCommandSequences.length < this.commandLog.length,
110
151
  }
111
152
  }
112
153
 
@@ -114,13 +155,32 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
114
155
  return this.claimed && input.generation === this.generation
115
156
  }
116
157
 
117
- async markConnectionReady(input: { generation: number, agentRunSnapshot: SupervisorAgentRunSnapshot }): Promise<boolean> {
158
+ async markConnectionReady(input: {
159
+ generation: number
160
+ lastReceivedCommandSeq: number
161
+ agentRunSnapshot: SupervisorAgentRunSnapshot
162
+ journalSourceId?: string
163
+ }): Promise<boolean | ConnectionReadinessRejection> {
118
164
  if (!this.claimed || input.generation !== this.generation)
119
165
  return false
166
+ if (this.pinnedJournalSourceId !== undefined && input.journalSourceId !== this.pinnedJournalSourceId) {
167
+ return {
168
+ ready: false,
169
+ code: inMemoryJournalIdentityChangedCode,
170
+ detail: `Session sandbox is pinned to journal ${this.pinnedJournalSourceId}; supervisor announced ${input.journalSourceId ?? 'none'}.`,
171
+ }
172
+ }
173
+ this.pinnedJournalSourceId ??= input.journalSourceId
120
174
  this.readySnapshots.push(input.agentRunSnapshot)
175
+ this.readyReceivedSequences.push(input.lastReceivedCommandSeq)
176
+ this.readyJournalSourceIds.push(input.journalSourceId)
121
177
  return true
122
178
  }
123
179
 
180
+ journalSourceId(): string | undefined {
181
+ return this.pinnedJournalSourceId
182
+ }
183
+
124
184
  async markConnectionReconnecting(): Promise<void> {}
125
185
 
126
186
  async releaseConnection(input: { generation: number }): Promise<void> {
@@ -137,9 +197,35 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
137
197
  }
138
198
 
139
199
  async next(input: { afterSequence: number }): Promise<CommandEnvelope | undefined> {
200
+ if (this.wssV1)
201
+ throw new Error('wss-v1 never advances a cumulative command cursor.')
140
202
  return this.commandLog.find(command => command.commandSeq > input.afterSequence)
141
203
  }
142
204
 
205
+ /** wss-v1 selection: cancel and steer jump the queue; withdrawn commands leave gaps. */
206
+ async pending(): Promise<CommandEnvelope | undefined> {
207
+ if (!this.wssV1)
208
+ throw new Error('Individual command selection requires commandTransport wss-v1.')
209
+ // Yield like a database round trip so admissions made in the same tick are visible.
210
+ await Promise.resolve()
211
+ const deliverable = this.deliverableCommands()
212
+ const selected = deliverable.find(command => isPriorityCommand(command.body)) ?? deliverable[0]
213
+ this.pendingSelections.push({ commandSeq: selected?.commandSeq, receivedBefore: [...this.receivedCommandSequences] })
214
+ if (selected)
215
+ this.commandStatus.set(selected.commandSeq, 'dispatched')
216
+ return selected
217
+ }
218
+
219
+ async received(input: { commandId: string, commandSeq: number }): Promise<void> {
220
+ this.settleDispatched(input, 'received')
221
+ this.receivedCommandSequences.push(input.commandSeq)
222
+ }
223
+
224
+ async rejected(input: { commandId: string, commandSeq: number, errorCode: string, detail?: string }): Promise<void> {
225
+ this.settleDispatched(input, 'rejected')
226
+ this.rejectedCommands.push({ commandSeq: input.commandSeq, errorCode: input.errorCode, detail: input.detail })
227
+ }
228
+
143
229
  async getAckFloor(): Promise<number> {
144
230
  return this.ackFloor
145
231
  }
@@ -203,4 +289,24 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
203
289
  async stop(): Promise<void> {
204
290
  this.notifications = undefined
205
291
  }
292
+
293
+ private settleDispatched(input: { commandId: string, commandSeq: number }, status: 'received' | 'rejected'): void {
294
+ const command = this.commandLog[input.commandSeq - 1]
295
+ if (!command || command.commandId !== input.commandId)
296
+ throw new Error(`Receipt for unknown command ${input.commandSeq}.`)
297
+ if (this.commandStatus.get(input.commandSeq) !== 'dispatched')
298
+ throw new Error(`Command ${input.commandSeq} was not dispatched.`)
299
+ this.commandStatus.set(input.commandSeq, status)
300
+ }
301
+
302
+ private deliverableCommands(): Array<CommandEnvelope & { body: WireCommandBody }> {
303
+ return this.commandLog.filter((command) => {
304
+ const status = this.commandStatus.get(command.commandSeq)
305
+ return status === 'queued' || status === 'dispatched'
306
+ })
307
+ }
308
+ }
309
+
310
+ function isPriorityCommand(body: WireCommandBody): boolean {
311
+ return body.type === 'agent.interrupt' || (body.type === 'agent.send-prompt' && body.mode === 'steer')
206
312
  }