@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
@@ -1,6 +1,6 @@
1
1
  export { createTestControlAuthority } from './es256-test-keys'
2
2
  export type { TestControlAuthority } from './es256-test-keys'
3
- export { InMemoryRuntimeControlPlane } from './in-memory-runtime-control-plane'
3
+ export { inMemoryJournalIdentityChangedCode, InMemoryRuntimeControlPlane } from './in-memory-runtime-control-plane'
4
4
  export { createLoopbackSupervisorConnectionOpener } from './loopback-supervisor-connection'
5
5
  export { createScriptedProvider } from './scripted-provider'
6
6
  export type { ScriptedCommandContext } from './scripted-provider'
@@ -3,11 +3,19 @@ import type { CommandAckEnvelope } from '../supervisor-protocol/envelopes/superv
3
3
  import type { CommandSource, ConnectionAuthorityStore, SupervisorConnection } from './ports'
4
4
  import { encodeWireFrame } from '../supervisor-protocol/wire-codec'
5
5
 
6
+ export class CommandAckTimeoutError extends Error {
7
+ constructor() {
8
+ super('agent-core.command-ack-timeout')
9
+ this.name = 'CommandAckTimeoutError'
10
+ }
11
+ }
12
+
6
13
  export class SessionCommandPump {
7
14
  private afterSequence: number
8
15
  private running = false
9
16
  private stopped = false
10
17
  private wakePending = false
18
+ private readonly idleWaiters = new Set<() => void>()
11
19
  private pending: {
12
20
  commandId: string
13
21
  commandSeq: number
@@ -19,13 +27,20 @@ export class SessionCommandPump {
19
27
  sessionSandboxId: string
20
28
  runtimeConnectionId: string
21
29
  afterSequence: number
30
+ individualReceipts?: boolean
31
+ ackTimeoutMs?: number
32
+ onFailure?: (error: unknown) => void
22
33
  commands: CommandSource
23
- authorityStore: ConnectionAuthorityStore
34
+ authorityStore?: ConnectionAuthorityStore
24
35
  connection: SupervisorConnection
25
36
  logger: Logger
26
37
  onRejected: (ack: Extract<CommandAckEnvelope, { status: 'rejected' }>) => Promise<void>
27
38
  }) {
28
39
  this.afterSequence = dependencies.afterSequence
40
+ if (dependencies.ackTimeoutMs !== undefined && (!Number.isSafeInteger(dependencies.ackTimeoutMs) || dependencies.ackTimeoutMs < 1))
41
+ throw new RangeError('ackTimeoutMs must be a positive safe integer')
42
+ if (dependencies.individualReceipts && (!dependencies.commands.pending || !dependencies.commands.received || !dependencies.commands.rejected))
43
+ throw new Error('wss-v1 requires individual command selection, receipt persistence and rejection persistence')
29
44
  }
30
45
 
31
46
  start(): void {
@@ -43,7 +58,7 @@ export class SessionCommandPump {
43
58
  acknowledge(ack: CommandAckEnvelope): void {
44
59
  const pending = this.pending
45
60
  if (!pending) {
46
- if (ack.status === 'received' && ack.commandSeq <= this.afterSequence)
61
+ if (ack.status === 'received' && (this.dependencies.individualReceipts || ack.commandSeq <= this.afterSequence))
47
62
  return
48
63
  this.dependencies.logger.warn({ commandSeq: ack.commandSeq }, 'closing after an unsolicited command acknowledgement')
49
64
  this.dependencies.connection.close(1008, 'Command ACK has no pending command.')
@@ -75,6 +90,13 @@ export class SessionCommandPump {
75
90
  return this.afterSequence
76
91
  }
77
92
 
93
+ /** Stop dispatch and wait for any in-flight selection/send/receipt to settle. */
94
+ async close(): Promise<void> {
95
+ this.stop()
96
+ if (this.running)
97
+ await new Promise<void>(resolve => this.idleWaiters.add(resolve))
98
+ }
99
+
78
100
  private async run(): Promise<void> {
79
101
  if (this.running)
80
102
  return
@@ -83,26 +105,58 @@ export class SessionCommandPump {
83
105
  while (!this.stopped && this.wakePending) {
84
106
  this.wakePending = false
85
107
  while (!this.stopped) {
86
- const command = await this.dependencies.commands.next({
87
- sessionId: this.dependencies.sessionId,
88
- runtimeConnectionId: this.dependencies.runtimeConnectionId,
89
- afterSequence: this.afterSequence,
90
- })
108
+ const command = this.dependencies.individualReceipts
109
+ ? await this.dependencies.commands.pending!({ sessionId: this.dependencies.sessionId, runtimeConnectionId: this.dependencies.runtimeConnectionId })
110
+ : await this.dependencies.commands.next({
111
+ sessionId: this.dependencies.sessionId,
112
+ runtimeConnectionId: this.dependencies.runtimeConnectionId,
113
+ afterSequence: this.afterSequence,
114
+ })
115
+ if (this.stopped)
116
+ return
91
117
  if (!command)
92
118
  break
93
119
  const ack = await this.sendAndWait(command)
94
120
  if (this.stopped)
95
121
  return
96
122
  if (ack.status === 'rejected') {
123
+ if (!this.dependencies.individualReceipts) {
124
+ await this.dependencies.onRejected(ack)
125
+ return
126
+ }
127
+ // Terminal for this command only: persist it so `pending` never offers it again,
128
+ // then keep delivering the rest of the session's work.
129
+ await this.dependencies.commands.rejected!({
130
+ sessionId: this.dependencies.sessionId,
131
+ runtimeConnectionId: this.dependencies.runtimeConnectionId,
132
+ commandId: ack.commandId,
133
+ commandSeq: ack.commandSeq,
134
+ errorCode: ack.errorCode,
135
+ detail: ack.detail,
136
+ })
97
137
  await this.dependencies.onRejected(ack)
98
- return
138
+ continue
99
139
  }
100
- this.afterSequence = ack.commandSeq
140
+ if (this.dependencies.individualReceipts) {
141
+ await this.dependencies.commands.received!({
142
+ sessionId: this.dependencies.sessionId,
143
+ runtimeConnectionId: this.dependencies.runtimeConnectionId,
144
+ commandId: ack.commandId,
145
+ commandSeq: ack.commandSeq,
146
+ })
147
+ }
148
+ this.afterSequence = Math.max(this.afterSequence, ack.commandSeq)
101
149
  }
102
150
  }
103
151
  }
152
+ catch (error) {
153
+ this.fail(error)
154
+ }
104
155
  finally {
105
156
  this.running = false
157
+ for (const resolve of this.idleWaiters)
158
+ resolve()
159
+ this.idleWaiters.clear()
106
160
  if (!this.stopped && this.wakePending)
107
161
  void this.run()
108
162
  }
@@ -110,18 +164,39 @@ export class SessionCommandPump {
110
164
 
111
165
  private async sendAndWait(command: NonNullable<Awaited<ReturnType<CommandSource['next']>>>): Promise<CommandAckEnvelope> {
112
166
  const waiter = Promise.withResolvers<CommandAckEnvelope>()
167
+ const timeout = this.dependencies.ackTimeoutMs === undefined ? undefined : setTimeout(() => this.fail(new CommandAckTimeoutError()), this.dependencies.ackTimeoutMs)
113
168
  this.pending = {
114
169
  commandId: command.commandId,
115
170
  commandSeq: command.commandSeq,
116
- settle: waiter.resolve,
171
+ settle: (ack) => {
172
+ clearTimeout(timeout)
173
+ waiter.resolve(ack)
174
+ },
117
175
  }
118
176
  try {
119
177
  await this.dependencies.connection.send(encodeWireFrame(command))
120
178
  return await waiter.promise
121
179
  }
122
180
  finally {
181
+ clearTimeout(timeout)
123
182
  if (this.pending?.commandId === command.commandId)
124
183
  this.pending = undefined
125
184
  }
126
185
  }
186
+
187
+ private fail(error: unknown): void {
188
+ if (this.stopped)
189
+ return
190
+ this.dependencies.logger.warn({ err: error }, 'command delivery failed; reconnect and reconcile durable attempts')
191
+ try {
192
+ this.dependencies.onFailure?.(error)
193
+ }
194
+ catch (callbackError) {
195
+ this.dependencies.logger.warn({ err: callbackError }, 'command failure callback failed')
196
+ }
197
+ finally {
198
+ this.stop()
199
+ this.dependencies.connection.close(1011, 'Command delivery failed.')
200
+ }
201
+ }
127
202
  }
@@ -1,4 +1,6 @@
1
1
  import type { Logger } from '../logger'
2
+ import type { SessionBootstrapBody } from '../supervisor-protocol/bootstrap'
3
+ import type { SessionBootstrapEnvelope } from '../supervisor-protocol/envelopes/control-plane-to-supervisor'
2
4
  import type { CommandAckEnvelope } from '../supervisor-protocol/envelopes/supervisor-to-control-plane'
3
5
  import type {
4
6
  BootstrapProvider,
@@ -16,6 +18,7 @@ import type {
16
18
  } from './ports'
17
19
  import { randomUUID } from 'node:crypto'
18
20
  import { wireCommandBodySchema } from '../supervisor-protocol/command-body'
21
+ import { sessionBootstrapEnvelopeSchema } from '../supervisor-protocol/envelopes/control-plane-to-supervisor'
19
22
  import { encodeWireFrame } from '../supervisor-protocol/wire-codec'
20
23
  import { SessionCommandPump } from './command-pump'
21
24
  import { BootstrapRejectedAckError, SupervisorFrameProcessor } from './frame-processor'
@@ -85,7 +88,7 @@ export class SessionSandboxConnectionAttempt {
85
88
  const queuedFrames: Array<string | ArrayBuffer | Uint8Array> = []
86
89
  const closed = Promise.withResolvers<{ code: number, reason: string }>()
87
90
  const authorityAssertion = await authorityAssertionPromise
88
- const connection = await beforeDeadline(this.dependencies.openSupervisorConnection({
91
+ const connection = await openSupervisorConnectionBeforeDeadline(this.dependencies.openSupervisorConnection, {
89
92
  providerSandboxId: claim.providerSandboxId,
90
93
  trafficAccessToken,
91
94
  sessionId: claim.sessionId,
@@ -97,7 +100,7 @@ export class SessionSandboxConnectionAttempt {
97
100
  signal: this.controller.signal,
98
101
  onMessage: frame => this.processor ? this.processor.receive(frame) : queuedFrames.push(frame),
99
102
  onClose: close => closed.resolve(close),
100
- }), Math.min(this.dependencies.establishmentDeadline, performance.now() + this.dependencies.timings.dialTimeoutMs), this.controller.signal, () => new ConnectionClosedError({
103
+ }, Math.min(this.dependencies.establishmentDeadline, performance.now() + this.dependencies.timings.dialTimeoutMs), () => new ConnectionClosedError({
101
104
  ready: false,
102
105
  detail: 'Supervisor connection dial timed out.',
103
106
  }))
@@ -136,30 +139,25 @@ export class SessionSandboxConnectionAttempt {
136
139
  bootstrapBodyPromise,
137
140
  secretsPromise,
138
141
  ])
139
- if (!initialCommand || initialCommand.commandSeq !== 1)
140
- throw new DeterministicEstablishmentError('agent-core.initial-command-invalid', 'Session command 1 is missing or invalid.')
141
- const parsedInitialBody = wireCommandBodySchema.safeParse(initialCommand.body)
142
- if (!parsedInitialBody.success) {
143
- throw new DeterministicEstablishmentError(
144
- 'agent-core.initial-command-invalid',
145
- 'Session command 1 has an invalid body.',
146
- )
142
+ let initialCommandBody: SessionBootstrapBody['initialCommand']
143
+ if (body.commandTransport !== 'wss-v1') {
144
+ if (!initialCommand || initialCommand.commandSeq !== 1)
145
+ throw new DeterministicEstablishmentError('agent-core.initial-command-invalid', 'Session command 1 is missing or invalid.')
146
+ const parsed = wireCommandBodySchema.safeParse(initialCommand.body)
147
+ if (!parsed.success)
148
+ throw new DeterministicEstablishmentError('agent-core.initial-command-invalid', 'Session command 1 has an invalid body.')
149
+ initialCommandBody = { commandId: initialCommand.commandId, commandSeq: 1, body: parsed.data }
147
150
  }
148
- const initialBody = parsedInitialBody.data
149
- await connection.send(encodeWireFrame({
151
+ await sendSupervisorBootstrap(connection, {
150
152
  kind: 'session.bootstrap',
151
153
  connectionGeneration: claim.runtimeConnectionGeneration,
152
154
  body: {
153
155
  ...body,
154
156
  sessionId: claim.sessionId,
155
- initialCommand: {
156
- commandId: initialCommand.commandId,
157
- commandSeq: initialCommand.commandSeq,
158
- body: initialBody,
159
- },
157
+ ...(initialCommandBody ? { initialCommand: initialCommandBody } : {}),
160
158
  },
161
159
  ...secrets,
162
- }))
160
+ }, this.controller.signal)
163
161
 
164
162
  const readiness = await beforeDeadline(Promise.race([
165
163
  processor.waitForReadiness({ signal: this.controller.signal }),
@@ -176,15 +174,24 @@ export class SessionSandboxConnectionAttempt {
176
174
  }
177
175
  throw error
178
176
  })
177
+ const wssV1 = body.commandTransport === 'wss-v1'
178
+ if (wssV1 && readiness.sourceId === undefined)
179
+ throw new DeterministicEstablishmentError('agent-core.journal-identity-missing', 'Supervisor did not announce its journal identity.')
180
+ // The store pins the journal identity before any command is dispatched or event persisted;
181
+ // a replaced journal is refused here instead of being resumed from a stale cursor.
179
182
  const markedReady = await this.dependencies.authorityStore.markConnectionReady({
180
183
  sessionSandboxId: claim.sessionSandboxId,
181
184
  generation: claim.runtimeConnectionGeneration,
182
185
  runtimeConnectionId: claim.runtimeConnectionId,
183
186
  lastReceivedCommandSeq: readiness.lastReceivedCommandSeq,
184
187
  agentRunSnapshot: readiness.agentRun,
188
+ ...(readiness.sourceId === undefined ? {} : { journalSourceId: readiness.sourceId }),
185
189
  })
186
- if (!markedReady)
190
+ if (markedReady === false)
187
191
  throw new ConnectionAuthorityLostError()
192
+ if (markedReady !== true)
193
+ throw new DeterministicEstablishmentError(markedReady.code, markedReady.detail)
194
+ processor.confirmReadiness()
188
195
  this.ready = true
189
196
 
190
197
  const pump = new SessionCommandPump({
@@ -192,11 +199,17 @@ export class SessionSandboxConnectionAttempt {
192
199
  sessionSandboxId: claim.sessionSandboxId,
193
200
  runtimeConnectionId: claim.runtimeConnectionId,
194
201
  afterSequence: readiness.lastReceivedCommandSeq,
202
+ individualReceipts: wssV1,
195
203
  commands: this.dependencies.commands,
196
204
  authorityStore: this.dependencies.authorityStore,
197
205
  connection,
198
206
  logger: this.dependencies.logger,
199
207
  onRejected: async (ack) => {
208
+ if (wssV1) {
209
+ // The pump already persisted the terminal rejection; the session keeps running.
210
+ this.dependencies.logger.warn({ commandSeq: ack.commandSeq, errorCode: ack.errorCode }, 'supervisor rejected a command; it will not be redispatched')
211
+ return
212
+ }
200
213
  await this.dependencies.failures.markSessionFailed({
201
214
  sessionId: claim.sessionId,
202
215
  sessionSandboxId: claim.sessionSandboxId,
@@ -233,7 +246,10 @@ export class SessionSandboxConnectionAttempt {
233
246
  }
234
247
 
235
248
  async drain(): Promise<void> {
236
- await this.processor?.drain()
249
+ await drainSupervisorConnection([
250
+ () => this.processor?.drain(),
251
+ () => this.commandPump?.close(),
252
+ ])
237
253
  }
238
254
 
239
255
  private async renewLease(): Promise<never> {
@@ -347,3 +363,62 @@ async function beforeDeadline<T>(
347
363
  signal.removeEventListener('abort', abort!)
348
364
  }
349
365
  }
366
+
367
+ /** A cancelled/timed-out dial must never leave a late socket alive. */
368
+ export async function openSupervisorConnectionBeforeDeadline(
369
+ open: SupervisorConnectionOpener,
370
+ input: Parameters<SupervisorConnectionOpener>[0],
371
+ deadline: number,
372
+ timeoutError: () => Error,
373
+ ): Promise<SupervisorConnection> {
374
+ input.signal.throwIfAborted()
375
+ const stop = new AbortController()
376
+ const signal = AbortSignal.any([input.signal, stop.signal])
377
+ let opened: SupervisorConnection | undefined
378
+ let retired = false
379
+ const retire = () => {
380
+ if (opened && !retired) {
381
+ retired = true
382
+ opened.close(1001, 'Connection dial cancelled.')
383
+ }
384
+ }
385
+ const task = Promise.resolve().then(() => {
386
+ signal.throwIfAborted()
387
+ return open({ ...input, signal })
388
+ }).then((connection) => {
389
+ opened = connection
390
+ if (signal.aborted) {
391
+ retire()
392
+ throw signal.reason
393
+ }
394
+ return connection
395
+ })
396
+ try {
397
+ return await beforeDeadline(task, deadline, signal, timeoutError)
398
+ }
399
+ catch (error) {
400
+ stop.abort(error)
401
+ retire()
402
+ // Observe late rejection even if the deadline elapsed before beforeDeadline raced it.
403
+ void task.catch(() => undefined)
404
+ throw error
405
+ }
406
+ }
407
+
408
+ /** Release ownership only after every pending connection task has settled. */
409
+ export async function drainSupervisorConnection(tasks: readonly (() => void | Promise<void>)[]): Promise<void> {
410
+ const results = await Promise.allSettled(tasks.map(task => Promise.resolve().then(task)))
411
+ const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : [])
412
+ if (errors.length === 1)
413
+ throw errors[0]
414
+ if (errors.length > 1)
415
+ throw new AggregateError(errors, 'Supervisor connection drain failed.')
416
+ }
417
+
418
+ /** Validate the wire envelope and reject a bootstrap whose owner was cancelled while loading it. */
419
+ export async function sendSupervisorBootstrap(connection: SupervisorConnection, envelope: SessionBootstrapEnvelope, signal: AbortSignal): Promise<void> {
420
+ signal.throwIfAborted()
421
+ const encoded = encodeWireFrame(sessionBootstrapEnvelopeSchema.parse(envelope))
422
+ signal.throwIfAborted()
423
+ await connection.send(encoded)
424
+ }