@meistrari/agent-core 0.0.0 → 0.1.0

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 (83) hide show
  1. package/README.md +24 -3
  2. package/bin/supervisor.ts +116 -0
  3. package/package.json +42 -3
  4. package/scripts/build-supervisor-executable.ts +32 -0
  5. package/src/agents/agent-error-serializer.ts +23 -0
  6. package/src/agents/agent-event-stream.ts +81 -0
  7. package/src/agents/agent-id.ts +17 -0
  8. package/src/agents/agent-operation.ts +11 -0
  9. package/src/agents/agent-provider.ts +37 -0
  10. package/src/agents/agent-run.ts +32 -0
  11. package/src/agents/agent-runtime-error.ts +161 -0
  12. package/src/agents/agent-session-events.ts +32 -0
  13. package/src/agents/agent-tool-runner.ts +83 -0
  14. package/src/agents/agent-tool.ts +111 -0
  15. package/src/agents/author-context.ts +36 -0
  16. package/src/agents/claude/claude-command-mapper.ts +234 -0
  17. package/src/agents/claude/claude-event-mapper.ts +736 -0
  18. package/src/agents/claude/claude-provider.ts +191 -0
  19. package/src/agents/claude/claude-run.ts +464 -0
  20. package/src/agents/claude/claude-tool-mapper.ts +186 -0
  21. package/src/agents/claude/index.ts +1 -0
  22. package/src/agents/codex/codex-auth.ts +34 -0
  23. package/src/agents/codex/codex-command-mapper.ts +78 -0
  24. package/src/agents/codex/codex-event-mapper.ts +708 -0
  25. package/src/agents/codex/codex-json-rpc-client.ts +326 -0
  26. package/src/agents/codex/codex-protocol.ts +36 -0
  27. package/src/agents/codex/codex-provider.ts +1050 -0
  28. package/src/agents/codex/codex-run.ts +404 -0
  29. package/src/agents/codex/codex-skill-catalog.ts +158 -0
  30. package/src/agents/codex/codex-skill-roots.ts +19 -0
  31. package/src/agents/codex/codex-tool-mapper.ts +55 -0
  32. package/src/agents/codex/codex.errors.ts +68 -0
  33. package/src/agents/codex/generated/meta.gen.ts +606 -0
  34. package/src/agents/codex/generated/namespaces.gen.ts +311 -0
  35. package/src/agents/codex/generated/schema.gen.ts +34883 -0
  36. package/src/agents/codex/index.ts +2 -0
  37. package/src/agents/index.ts +14 -0
  38. package/src/agents/input-attachment-preparation.ts +86 -0
  39. package/src/agents/input-attachment.errors.ts +16 -0
  40. package/src/agents/instructions.ts +8 -0
  41. package/src/agents/materialized-input-attachment.ts +26 -0
  42. package/src/agents/message-id.ts +23 -0
  43. package/src/agents/normalize.ts +8 -0
  44. package/src/agents/sandbox-environment.ts +1 -0
  45. package/src/agents/tools/ping.tool.ts +13 -0
  46. package/src/agents/user-input-request.ts +470 -0
  47. package/src/provenance.gen.ts +3 -3
  48. package/src/supervisor/agent-provider-factory.ts +189 -0
  49. package/src/supervisor/bootstrap-binder.ts +125 -0
  50. package/src/supervisor/config.ts +49 -0
  51. package/src/supervisor/control-authority-verifier.ts +135 -0
  52. package/src/supervisor/create-supervisor-runtime.ts +25 -0
  53. package/src/supervisor/errors.ts +24 -0
  54. package/src/supervisor/index.ts +34 -0
  55. package/src/supervisor/persistence/json.ts +21 -0
  56. package/src/supervisor/persistence/state-discovery.ts +56 -0
  57. package/src/supervisor/persistence/supervisor-store.ts +364 -0
  58. package/src/supervisor/ports/index.ts +109 -0
  59. package/src/supervisor/provider-factory.ts +37 -0
  60. package/src/supervisor/resident.ts +143 -0
  61. package/src/supervisor/rpc-client.ts +120 -0
  62. package/src/supervisor/runtime-handler.ts +309 -0
  63. package/src/supervisor/websocket-server.ts +434 -0
  64. package/src/supervisor-protocol/bootstrap.ts +1 -1
  65. package/src/template-onboarding.ts +47 -0
  66. package/src/testing/es256-test-keys.ts +73 -0
  67. package/src/testing/in-memory-runtime-control-plane.ts +205 -0
  68. package/src/testing/index.ts +6 -0
  69. package/src/testing/loopback-supervisor-connection.ts +71 -0
  70. package/src/testing/scripted-provider.ts +64 -0
  71. package/src/worker-runtime-client/command-pump.ts +132 -0
  72. package/src/worker-runtime-client/connection-attempt.ts +340 -0
  73. package/src/worker-runtime-client/control-authority-signer.ts +100 -0
  74. package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
  75. package/src/worker-runtime-client/frame-processor.ts +178 -0
  76. package/src/worker-runtime-client/index.ts +27 -0
  77. package/src/worker-runtime-client/lease-reconciler.ts +14 -0
  78. package/src/worker-runtime-client/ports.ts +137 -0
  79. package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
  80. package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
  81. package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
  82. package/src/worker-runtime-client/sandbox-connection-runtime.ts +300 -0
  83. package/src/worker-runtime-client/token-crypto.ts +46 -0
@@ -0,0 +1,205 @@
1
+ import type { SessionBootstrapBody } from '../supervisor-protocol/bootstrap'
2
+ import type { WireCommandBody } from '../supervisor-protocol/command-body'
3
+ import type { CommandEnvelope } from '../supervisor-protocol/envelopes/control-plane-to-supervisor'
4
+ import type { DurableEventEnvelope } from '../supervisor-protocol/envelopes/supervisor-to-control-plane'
5
+ import type { SupervisorRpcRequestEnvelope } from '../supervisor-protocol/rpc'
6
+ import type { SupervisorAgentRunSnapshot } from '../supervisor-protocol/supervisor-agent-run-snapshot'
7
+ import type {
8
+ BootstrapProvider,
9
+ CommandSource,
10
+ ConnectionAuthorityStore,
11
+ ConnectionClaim,
12
+ DurableEventSink,
13
+ EphemeralEventSink,
14
+ RpcDispatcher,
15
+ RuntimeNotifications,
16
+ SessionFailureSink,
17
+ } from '../worker-runtime-client/ports'
18
+ import { ulid } from 'ulid'
19
+
20
+ type NotificationCallbacks = Parameters<RuntimeNotifications['start']>[0]
21
+
22
+ export class InMemoryRuntimeControlPlane implements ConnectionAuthorityStore, CommandSource, DurableEventSink,
23
+ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, RuntimeNotifications {
24
+ readonly durableEventLog: DurableEventEnvelope[] = []
25
+ readonly durableRecordAttempts: number[] = []
26
+ readonly ephemeralEventLog: Array<Parameters<EphemeralEventSink['publish']>[0]> = []
27
+ readonly readySnapshots: SupervisorAgentRunSnapshot[] = []
28
+ readonly projectedCommandSequences: number[] = []
29
+ readonly failureLog: Array<{ kind: string, code?: string, detail?: string }> = []
30
+ readonly rpcCalls: SupervisorRpcRequestEnvelope[] = []
31
+ onDurableRecord: ((event: DurableEventEnvelope) => void | Promise<void>) | undefined
32
+ private readonly commandLog: CommandEnvelope[]
33
+ private notifications: NotificationCallbacks | undefined
34
+ private claimed = false
35
+ private generation = 0
36
+ private connectionId = ''
37
+ private ackFloor = 0
38
+
39
+ constructor(private readonly input: {
40
+ ref: { sessionId: string, sessionSandboxId: string, providerSandboxId: string }
41
+ trafficAccessTokenEncrypted: string
42
+ bootstrap: Omit<SessionBootstrapBody, 'sessionId' | 'initialCommand'>
43
+ initialCommand: { commandId: string, body: WireCommandBody }
44
+ secrets?: Awaited<ReturnType<BootstrapProvider['issueEphemeralSecrets']>>
45
+ rpcHandlers?: Readonly<Record<string, (body: unknown) => unknown | Promise<unknown>>>
46
+ }) {
47
+ this.commandLog = [{
48
+ kind: 'command',
49
+ commandId: input.initialCommand.commandId,
50
+ commandSeq: 1,
51
+ body: input.initialCommand.body,
52
+ }]
53
+ }
54
+
55
+ enqueueCommand(input: { commandId?: string, body: WireCommandBody }): CommandEnvelope {
56
+ const command: CommandEnvelope = {
57
+ kind: 'command',
58
+ commandId: input.commandId ?? ulid(),
59
+ commandSeq: this.commandLog.length + 1,
60
+ body: input.body,
61
+ }
62
+ this.commandLog.push(command)
63
+ this.notifications?.onCommandsReady(this.input.ref.sessionId)
64
+ return command
65
+ }
66
+
67
+ demandConnection(): void {
68
+ this.notifications?.onConnectionDemand(this.input.ref.sessionSandboxId)
69
+ }
70
+
71
+ notifyOwnerChanged(generation = this.generation): void {
72
+ this.notifications?.onOwnerChanged({ sessionSandboxId: this.input.ref.sessionSandboxId, generation })
73
+ }
74
+
75
+ async listConnectionCandidates(): Promise<{ sessionSandboxId: string }[]> {
76
+ return this.claimed ? [] : [{ sessionSandboxId: this.input.ref.sessionSandboxId }]
77
+ }
78
+
79
+ async notifyConnectionDemand(input: { sessionSandboxIds: string[] }): Promise<void> {
80
+ for (const sessionSandboxId of input.sessionSandboxIds)
81
+ this.notifications?.onConnectionDemand(sessionSandboxId)
82
+ }
83
+
84
+ async claimConnection(input: { sessionSandboxId: string }): Promise<ConnectionClaim | undefined> {
85
+ if (this.claimed || input.sessionSandboxId !== this.input.ref.sessionSandboxId)
86
+ return undefined
87
+ this.claimed = true
88
+ this.generation += 1
89
+ this.connectionId = ulid()
90
+ return {
91
+ ...this.input.ref,
92
+ runtimeConnectionGeneration: this.generation,
93
+ runtimeConnectionId: this.connectionId,
94
+ trafficAccessTokenEncrypted: this.input.trafficAccessTokenEncrypted,
95
+ leaseExpiresAt: new Date(Date.now() + 30_000),
96
+ }
97
+ }
98
+
99
+ async renewConnectionLease(input: { generation: number }): Promise<{
100
+ renewed: boolean
101
+ leaseExpiresAt?: Date
102
+ hasOutstandingCommand: boolean
103
+ }> {
104
+ const renewed = this.claimed && input.generation === this.generation
105
+ return {
106
+ renewed,
107
+ ...(renewed ? { leaseExpiresAt: new Date(Date.now() + 30_000) } : {}),
108
+ hasOutstandingCommand: this.projectedCommandSequences.length < this.commandLog.length - 1,
109
+ }
110
+ }
111
+
112
+ async confirmConnectionAuthority(input: { generation: number }): Promise<boolean> {
113
+ return this.claimed && input.generation === this.generation
114
+ }
115
+
116
+ async markConnectionReady(input: { generation: number, agentRunSnapshot: SupervisorAgentRunSnapshot }): Promise<boolean> {
117
+ if (!this.claimed || input.generation !== this.generation)
118
+ return false
119
+ this.readySnapshots.push(input.agentRunSnapshot)
120
+ return true
121
+ }
122
+
123
+ async markConnectionReconnecting(): Promise<void> {}
124
+
125
+ async releaseConnection(input: { generation: number }): Promise<void> {
126
+ if (input.generation === this.generation)
127
+ this.claimed = false
128
+ }
129
+
130
+ async markConnectionRetryExhausted(input: { generation: number }): Promise<boolean> {
131
+ return this.claimed && input.generation === this.generation
132
+ }
133
+
134
+ async projectAppliedCommand(input: { commandSeq: number }): Promise<void> {
135
+ this.projectedCommandSequences.push(input.commandSeq)
136
+ }
137
+
138
+ async initial(): Promise<CommandEnvelope | undefined> {
139
+ return this.commandLog[0]
140
+ }
141
+
142
+ async next(input: { afterSequence: number }): Promise<CommandEnvelope | undefined> {
143
+ return this.commandLog.find(command => command.commandSeq > input.afterSequence)
144
+ }
145
+
146
+ async getAckFloor(): Promise<number> {
147
+ return this.ackFloor
148
+ }
149
+
150
+ async record(input: { event: DurableEventEnvelope }): Promise<{
151
+ status: 'recorded' | 'duplicate'
152
+ highWaterMark: number
153
+ } | { status: 'gap' }> {
154
+ const { event } = input
155
+ this.durableRecordAttempts.push(event.seq)
156
+ if (event.seq <= this.ackFloor)
157
+ return { status: 'duplicate', highWaterMark: this.ackFloor }
158
+ if (event.seq !== this.ackFloor + 1)
159
+ return { status: 'gap' }
160
+ this.durableEventLog.push(event)
161
+ this.ackFloor = event.seq
162
+ await this.onDurableRecord?.(event)
163
+ return { status: 'recorded', highWaterMark: this.ackFloor }
164
+ }
165
+
166
+ publish(input: Parameters<EphemeralEventSink['publish']>[0]): void {
167
+ this.ephemeralEventLog.push(input)
168
+ }
169
+
170
+ async handle(input: { request: SupervisorRpcRequestEnvelope }): Promise<unknown> {
171
+ this.rpcCalls.push(input.request)
172
+ const handler = this.input.rpcHandlers?.[input.request.method]
173
+ if (!handler)
174
+ throw new Error(`No in-memory RPC handler is registered for ${input.request.method}.`)
175
+ return await handler(input.request.body)
176
+ }
177
+
178
+ async loadBootstrapBody(): Promise<Omit<SessionBootstrapBody, 'sessionId' | 'initialCommand'>> {
179
+ return this.input.bootstrap
180
+ }
181
+
182
+ async issueEphemeralSecrets(): Promise<Awaited<ReturnType<BootstrapProvider['issueEphemeralSecrets']>>> {
183
+ return this.input.secrets ?? {}
184
+ }
185
+
186
+ async markSessionFailed(input: { code: string, detail?: string }): Promise<void> {
187
+ this.failureLog.push({ kind: 'session', code: input.code, detail: input.detail })
188
+ }
189
+
190
+ async failDeterministicEstablishment(input: { code: string, detail?: string }): Promise<void> {
191
+ this.failureLog.push({ kind: 'deterministic', code: input.code, detail: input.detail })
192
+ }
193
+
194
+ async markEstablishmentExhausted(): Promise<void> {
195
+ this.failureLog.push({ kind: 'exhausted' })
196
+ }
197
+
198
+ async start(callbacks: NotificationCallbacks): Promise<void> {
199
+ this.notifications = callbacks
200
+ }
201
+
202
+ async stop(): Promise<void> {
203
+ this.notifications = undefined
204
+ }
205
+ }
@@ -0,0 +1,6 @@
1
+ export { createTestControlAuthority } from './es256-test-keys'
2
+ export type { TestControlAuthority } from './es256-test-keys'
3
+ export { InMemoryRuntimeControlPlane } from './in-memory-runtime-control-plane'
4
+ export { createLoopbackSupervisorConnectionOpener } from './loopback-supervisor-connection'
5
+ export { createScriptedProvider } from './scripted-provider'
6
+ export type { ScriptedCommandContext } from './scripted-provider'
@@ -0,0 +1,71 @@
1
+ import type { SupervisorConnection, SupervisorConnectionOpener } from '../worker-runtime-client/ports'
2
+ import { sandboxControlAuthorityHeaderName } from '../supervisor-protocol/control-authority'
3
+
4
+ export function createLoopbackSupervisorConnectionOpener(input: {
5
+ url: string
6
+ supervisorPort?: number
7
+ dialTimeoutMs?: number
8
+ }): SupervisorConnectionOpener {
9
+ const dialTimeoutMs = input.dialTimeoutMs ?? 2_000
10
+ return async (request) => {
11
+ request.signal.throwIfAborted()
12
+ const socket = new WebSocket(input.url, {
13
+ headers: {
14
+ 'E2B-Traffic-Access-Token': request.trafficAccessToken,
15
+ 'E2b-Sandbox-Id': request.providerSandboxId,
16
+ 'E2b-Sandbox-Port': String(input.supervisorPort ?? 8080),
17
+ 'X-Coding-Agent-Session-Id': request.sessionId,
18
+ 'X-Coding-Agent-Session-Sandbox-Id': request.sessionSandboxId,
19
+ 'X-Coding-Agent-Runtime-Connection-Attempt-Id': request.runtimeConnectionAttemptId,
20
+ [sandboxControlAuthorityHeaderName]: request.authorityAssertion,
21
+ 'X-Coding-Agent-Connection-Generation': String(request.connectionGeneration),
22
+ 'X-Coding-Agent-Event-Ack': String(request.eventAckFloor),
23
+ },
24
+ })
25
+ socket.binaryType = 'arraybuffer'
26
+ const connection: SupervisorConnection = {
27
+ send(data) {
28
+ if (socket.readyState !== WebSocket.OPEN)
29
+ throw new Error('Loopback supervisor connection is not open.')
30
+ socket.send(data)
31
+ },
32
+ close: (code, reason) => socket.close(code, reason),
33
+ }
34
+ socket.onmessage = (event) => {
35
+ if (typeof event.data === 'string')
36
+ request.onMessage(event.data)
37
+ else if (event.data instanceof ArrayBuffer)
38
+ request.onMessage(event.data)
39
+ }
40
+
41
+ await new Promise<void>((resolve, reject) => {
42
+ let timeout: ReturnType<typeof setTimeout>
43
+ const abort = () => {
44
+ clearTimeout(timeout)
45
+ request.signal.removeEventListener('abort', abort)
46
+ socket.close()
47
+ reject(request.signal.reason)
48
+ }
49
+ timeout = setTimeout(() => {
50
+ request.signal.removeEventListener('abort', abort)
51
+ socket.close()
52
+ reject(new Error('Timed out opening the loopback supervisor connection.'))
53
+ }, dialTimeoutMs)
54
+ request.signal.addEventListener('abort', abort, { once: true })
55
+ socket.onopen = () => {
56
+ clearTimeout(timeout)
57
+ request.signal.removeEventListener('abort', abort)
58
+ socket.onclose = event => request.onClose({ code: event.code, reason: event.reason })
59
+ socket.onerror = () => socket.close()
60
+ resolve()
61
+ }
62
+ socket.onerror = () => undefined
63
+ socket.onclose = (event) => {
64
+ clearTimeout(timeout)
65
+ request.signal.removeEventListener('abort', abort)
66
+ reject(new Error(`Loopback supervisor connection closed during upgrade (${event.code}).`))
67
+ }
68
+ })
69
+ return connection
70
+ }
71
+ }
@@ -0,0 +1,64 @@
1
+ import type { EphemeralCredentials, SessionBootstrapGitToken } from '../supervisor-protocol/bootstrap'
2
+ import type { WireCommandBody } from '../supervisor-protocol/command-body'
3
+ import type { WireEventBody } from '../supervisor-protocol/event-body'
4
+ import type { SupervisorAgentRunSnapshot } from '../supervisor-protocol/supervisor-agent-run-snapshot'
5
+ import type { SupervisorAgentRuntime, SupervisorProviderFactory } from '../supervisor/provider-factory'
6
+ import type { SupervisorRpcClient } from '../supervisor/rpc-client'
7
+ import { createSupervisorProviderFactory } from '../supervisor/provider-factory'
8
+
9
+ export interface ScriptedCommandContext {
10
+ commandId: string
11
+ commandSeq: number
12
+ body: WireCommandBody
13
+ signal: AbortSignal
14
+ rpc: SupervisorRpcClient
15
+ emit: (body: WireEventBody) => void
16
+ }
17
+
18
+ export function createScriptedProvider(input: {
19
+ snapshot?: SupervisorAgentRunSnapshot
20
+ onCommand?: (context: ScriptedCommandContext) => Promise<void>
21
+ onRefreshSecrets?: (input: { credentials?: EphemeralCredentials, gitToken?: SessionBootstrapGitToken }) => void | Promise<void>
22
+ onClose?: () => Promise<void>
23
+ } = {}): {
24
+ factory: SupervisorProviderFactory
25
+ handledCommands: Array<{ commandId: string, commandSeq: number, body: WireCommandBody }>
26
+ refreshedSecrets: Array<{ credentials?: EphemeralCredentials, gitToken?: SessionBootstrapGitToken }>
27
+ createdCount: () => number
28
+ closedCount: () => number
29
+ } {
30
+ const handledCommands: Array<{ commandId: string, commandSeq: number, body: WireCommandBody }> = []
31
+ const refreshedSecrets: Array<{ credentials?: EphemeralCredentials, gitToken?: SessionBootstrapGitToken }> = []
32
+ let created = 0
33
+ let closed = 0
34
+ const factory = createSupervisorProviderFactory(async ({ rpc }) => {
35
+ created += 1
36
+ const runtime: SupervisorAgentRuntime = {
37
+ snapshot: () => input.snapshot ?? { status: 'not_attached' },
38
+ async handle(command) {
39
+ handledCommands.push({
40
+ commandId: command.commandId,
41
+ commandSeq: command.commandSeq,
42
+ body: command.body,
43
+ })
44
+ await input.onCommand?.({ ...command, rpc })
45
+ },
46
+ async refreshSecrets(secrets) {
47
+ refreshedSecrets.push(secrets)
48
+ await input.onRefreshSecrets?.(secrets)
49
+ },
50
+ async close() {
51
+ closed += 1
52
+ await input.onClose?.()
53
+ },
54
+ }
55
+ return runtime
56
+ })
57
+ return {
58
+ factory,
59
+ handledCommands,
60
+ refreshedSecrets,
61
+ createdCount: () => created,
62
+ closedCount: () => closed,
63
+ }
64
+ }
@@ -0,0 +1,132 @@
1
+ import type { Logger } from '../logger'
2
+ import type { CommandAckEnvelope } from '../supervisor-protocol/envelopes/supervisor-to-control-plane'
3
+ import type { CommandSource, ConnectionAuthorityStore, SupervisorConnection } from './ports'
4
+ import { encodeWireFrame } from '../supervisor-protocol/wire-codec'
5
+
6
+ export class SessionCommandPump {
7
+ private afterSequence: number
8
+ private running = false
9
+ private stopped = false
10
+ private wakePending = false
11
+ private pending: {
12
+ commandId: string
13
+ commandSeq: number
14
+ settle: (ack: CommandAckEnvelope) => void
15
+ } | undefined
16
+
17
+ constructor(private readonly dependencies: {
18
+ sessionId: string
19
+ sessionSandboxId: string
20
+ runtimeConnectionId: string
21
+ afterSequence: number
22
+ commands: CommandSource
23
+ authorityStore: ConnectionAuthorityStore
24
+ connection: SupervisorConnection
25
+ logger: Logger
26
+ onRejected: (ack: Extract<CommandAckEnvelope, { status: 'rejected' }>) => Promise<void>
27
+ }) {
28
+ this.afterSequence = dependencies.afterSequence
29
+ }
30
+
31
+ start(): void {
32
+ this.wake()
33
+ }
34
+
35
+ wake(): void {
36
+ if (this.stopped)
37
+ return
38
+ this.wakePending = true
39
+ if (!this.running)
40
+ void this.run()
41
+ }
42
+
43
+ acknowledge(ack: CommandAckEnvelope): void {
44
+ const pending = this.pending
45
+ if (!pending) {
46
+ if (ack.status === 'received' && ack.commandSeq <= this.afterSequence)
47
+ return
48
+ this.dependencies.logger.warn({ commandSeq: ack.commandSeq }, 'closing after an unsolicited command acknowledgement')
49
+ this.dependencies.connection.close(1008, 'Command ACK has no pending command.')
50
+ this.stop()
51
+ return
52
+ }
53
+ if (ack.commandId !== pending.commandId || ack.commandSeq !== pending.commandSeq) {
54
+ this.dependencies.logger.warn({ commandSeq: ack.commandSeq }, 'closing after a mismatched command acknowledgement')
55
+ this.dependencies.connection.close(1008, 'Command ACK did not match the pending command.')
56
+ this.stop()
57
+ return
58
+ }
59
+ pending.settle(ack)
60
+ }
61
+
62
+ stop(): void {
63
+ this.stopped = true
64
+ this.pending?.settle({
65
+ kind: 'command.ack',
66
+ commandId: this.pending.commandId,
67
+ commandSeq: this.pending.commandSeq,
68
+ status: 'rejected',
69
+ errorCode: 'agent-core.connection-stopped',
70
+ })
71
+ this.pending = undefined
72
+ }
73
+
74
+ cursor(): number {
75
+ return this.afterSequence
76
+ }
77
+
78
+ private async run(): Promise<void> {
79
+ if (this.running)
80
+ return
81
+ this.running = true
82
+ try {
83
+ while (!this.stopped && this.wakePending) {
84
+ this.wakePending = false
85
+ 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
+ })
91
+ if (!command)
92
+ break
93
+ const ack = await this.sendAndWait(command)
94
+ if (this.stopped)
95
+ return
96
+ if (ack.status === 'rejected') {
97
+ await this.dependencies.onRejected(ack)
98
+ return
99
+ }
100
+ this.afterSequence = ack.commandSeq
101
+ await this.dependencies.authorityStore.projectAppliedCommand({
102
+ sessionSandboxId: this.dependencies.sessionSandboxId,
103
+ runtimeConnectionId: this.dependencies.runtimeConnectionId,
104
+ commandSeq: ack.commandSeq,
105
+ })
106
+ }
107
+ }
108
+ }
109
+ finally {
110
+ this.running = false
111
+ if (!this.stopped && this.wakePending)
112
+ void this.run()
113
+ }
114
+ }
115
+
116
+ private async sendAndWait(command: NonNullable<Awaited<ReturnType<CommandSource['next']>>>): Promise<CommandAckEnvelope> {
117
+ const waiter = Promise.withResolvers<CommandAckEnvelope>()
118
+ this.pending = {
119
+ commandId: command.commandId,
120
+ commandSeq: command.commandSeq,
121
+ settle: waiter.resolve,
122
+ }
123
+ try {
124
+ await this.dependencies.connection.send(encodeWireFrame(command))
125
+ return await waiter.promise
126
+ }
127
+ finally {
128
+ if (this.pending?.commandId === command.commandId)
129
+ this.pending = undefined
130
+ }
131
+ }
132
+ }