@meistrari/agent-core 0.0.0 → 0.1.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 +36 -3
- package/bin/supervisor.ts +116 -0
- package/package.json +42 -3
- package/scripts/build-supervisor-executable.ts +32 -0
- package/src/agents/agent-error-serializer.ts +23 -0
- package/src/agents/agent-event-stream.ts +81 -0
- package/src/agents/agent-id.ts +17 -0
- package/src/agents/agent-operation.ts +11 -0
- package/src/agents/agent-provider.ts +37 -0
- package/src/agents/agent-run.ts +32 -0
- package/src/agents/agent-runtime-error.ts +161 -0
- package/src/agents/agent-session-events.ts +32 -0
- package/src/agents/agent-tool-runner.ts +83 -0
- package/src/agents/agent-tool.ts +111 -0
- package/src/agents/author-context.ts +36 -0
- package/src/agents/claude/claude-command-mapper.ts +234 -0
- package/src/agents/claude/claude-event-mapper.ts +748 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +468 -0
- package/src/agents/claude/claude-tool-mapper.ts +186 -0
- package/src/agents/claude/index.ts +1 -0
- package/src/agents/codex/codex-auth.ts +34 -0
- package/src/agents/codex/codex-command-mapper.ts +78 -0
- package/src/agents/codex/codex-event-mapper.ts +721 -0
- package/src/agents/codex/codex-json-rpc-client.ts +326 -0
- package/src/agents/codex/codex-protocol.ts +36 -0
- package/src/agents/codex/codex-provider.ts +1050 -0
- package/src/agents/codex/codex-run.ts +404 -0
- package/src/agents/codex/codex-skill-catalog.ts +158 -0
- package/src/agents/codex/codex-skill-roots.ts +19 -0
- package/src/agents/codex/codex-tool-mapper.ts +55 -0
- package/src/agents/codex/codex.errors.ts +68 -0
- package/src/agents/codex/generated/meta.gen.ts +606 -0
- package/src/agents/codex/generated/namespaces.gen.ts +311 -0
- package/src/agents/codex/generated/schema.gen.ts +34883 -0
- package/src/agents/codex/index.ts +2 -0
- package/src/agents/index.ts +14 -0
- package/src/agents/input-attachment-preparation.ts +86 -0
- package/src/agents/input-attachment.errors.ts +16 -0
- package/src/agents/instructions.ts +8 -0
- package/src/agents/materialized-input-attachment.ts +26 -0
- package/src/agents/message-id.ts +23 -0
- package/src/agents/normalize.ts +8 -0
- package/src/agents/sandbox-environment.ts +1 -0
- package/src/agents/tools/ping.tool.ts +13 -0
- package/src/agents/user-input-request.ts +470 -0
- package/src/protocol/agent-event.ts +2 -1
- package/src/protocol/agent-usage.ts +14 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +133 -0
- package/src/supervisor/config.ts +49 -0
- package/src/supervisor/control-authority-verifier.ts +135 -0
- package/src/supervisor/create-supervisor-runtime.ts +25 -0
- package/src/supervisor/errors.ts +24 -0
- package/src/supervisor/index.ts +34 -0
- package/src/supervisor/persistence/json.ts +21 -0
- package/src/supervisor/persistence/state-discovery.ts +56 -0
- package/src/supervisor/persistence/supervisor-store.ts +364 -0
- package/src/supervisor/ports/index.ts +109 -0
- package/src/supervisor/provider-factory.ts +37 -0
- package/src/supervisor/resident.ts +143 -0
- package/src/supervisor/rpc-client.ts +120 -0
- package/src/supervisor/runtime-handler.ts +309 -0
- package/src/supervisor/websocket-server.ts +435 -0
- package/src/supervisor-protocol/bootstrap.ts +1 -1
- package/src/template-onboarding.ts +47 -0
- package/src/testing/es256-test-keys.ts +73 -0
- package/src/testing/in-memory-runtime-control-plane.ts +206 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/loopback-supervisor-connection.ts +71 -0
- package/src/testing/scripted-provider.ts +64 -0
- package/src/worker-runtime-client/command-pump.ts +127 -0
- package/src/worker-runtime-client/connection-attempt.ts +349 -0
- package/src/worker-runtime-client/control-authority-signer.ts +100 -0
- package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
- package/src/worker-runtime-client/frame-processor.ts +178 -0
- package/src/worker-runtime-client/index.ts +27 -0
- package/src/worker-runtime-client/lease-reconciler.ts +14 -0
- package/src/worker-runtime-client/ports.ts +139 -0
- package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
- package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
- package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +301 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,206 @@
|
|
|
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 failedCommandSequences: number[] = []
|
|
30
|
+
readonly failureLog: Array<{ kind: string, code?: string, detail?: string }> = []
|
|
31
|
+
readonly rpcCalls: SupervisorRpcRequestEnvelope[] = []
|
|
32
|
+
onDurableRecord: ((event: DurableEventEnvelope) => void | Promise<void>) | undefined
|
|
33
|
+
private readonly commandLog: CommandEnvelope[]
|
|
34
|
+
private notifications: NotificationCallbacks | undefined
|
|
35
|
+
private claimed = false
|
|
36
|
+
private generation = 0
|
|
37
|
+
private connectionId = ''
|
|
38
|
+
private ackFloor = 0
|
|
39
|
+
|
|
40
|
+
constructor(private readonly input: {
|
|
41
|
+
ref: { sessionId: string, sessionSandboxId: string, providerSandboxId: string }
|
|
42
|
+
trafficAccessTokenEncrypted: string
|
|
43
|
+
bootstrap: Omit<SessionBootstrapBody, 'sessionId' | 'initialCommand'>
|
|
44
|
+
initialCommand: { commandId: string, body: WireCommandBody }
|
|
45
|
+
secrets?: Awaited<ReturnType<BootstrapProvider['issueEphemeralSecrets']>>
|
|
46
|
+
rpcHandlers?: Readonly<Record<string, (body: unknown) => unknown | Promise<unknown>>>
|
|
47
|
+
}) {
|
|
48
|
+
this.commandLog = [{
|
|
49
|
+
kind: 'command',
|
|
50
|
+
commandId: input.initialCommand.commandId,
|
|
51
|
+
commandSeq: 1,
|
|
52
|
+
body: input.initialCommand.body,
|
|
53
|
+
}]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
enqueueCommand(input: { commandId?: string, body: WireCommandBody }): CommandEnvelope {
|
|
57
|
+
const command: CommandEnvelope = {
|
|
58
|
+
kind: 'command',
|
|
59
|
+
commandId: input.commandId ?? ulid(),
|
|
60
|
+
commandSeq: this.commandLog.length + 1,
|
|
61
|
+
body: input.body,
|
|
62
|
+
}
|
|
63
|
+
this.commandLog.push(command)
|
|
64
|
+
this.notifications?.onCommandsReady(this.input.ref.sessionId)
|
|
65
|
+
return command
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
demandConnection(): void {
|
|
69
|
+
this.notifications?.onConnectionDemand(this.input.ref.sessionSandboxId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
notifyOwnerChanged(generation = this.generation): void {
|
|
73
|
+
this.notifications?.onOwnerChanged({ sessionSandboxId: this.input.ref.sessionSandboxId, generation })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async listConnectionCandidates(): Promise<{ sessionSandboxId: string }[]> {
|
|
77
|
+
return this.claimed ? [] : [{ sessionSandboxId: this.input.ref.sessionSandboxId }]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async notifyConnectionDemand(input: { sessionSandboxIds: string[] }): Promise<void> {
|
|
81
|
+
for (const sessionSandboxId of input.sessionSandboxIds)
|
|
82
|
+
this.notifications?.onConnectionDemand(sessionSandboxId)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async claimConnection(input: { sessionSandboxId: string }): Promise<ConnectionClaim | undefined> {
|
|
86
|
+
if (this.claimed || input.sessionSandboxId !== this.input.ref.sessionSandboxId)
|
|
87
|
+
return undefined
|
|
88
|
+
this.claimed = true
|
|
89
|
+
this.generation += 1
|
|
90
|
+
this.connectionId = ulid()
|
|
91
|
+
return {
|
|
92
|
+
...this.input.ref,
|
|
93
|
+
runtimeConnectionGeneration: this.generation,
|
|
94
|
+
runtimeConnectionId: this.connectionId,
|
|
95
|
+
trafficAccessTokenEncrypted: this.input.trafficAccessTokenEncrypted,
|
|
96
|
+
leaseExpiresAt: new Date(Date.now() + 30_000),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async renewConnectionLease(input: { generation: number }): Promise<{
|
|
101
|
+
renewed: boolean
|
|
102
|
+
leaseExpiresAt?: Date
|
|
103
|
+
hasOutstandingCommand: boolean
|
|
104
|
+
}> {
|
|
105
|
+
const renewed = this.claimed && input.generation === this.generation
|
|
106
|
+
return {
|
|
107
|
+
renewed,
|
|
108
|
+
...(renewed ? { leaseExpiresAt: new Date(Date.now() + 30_000) } : {}),
|
|
109
|
+
hasOutstandingCommand: this.projectedCommandSequences.length + this.failedCommandSequences.length < this.commandLog.length,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async confirmConnectionAuthority(input: { generation: number }): Promise<boolean> {
|
|
114
|
+
return this.claimed && input.generation === this.generation
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async markConnectionReady(input: { generation: number, agentRunSnapshot: SupervisorAgentRunSnapshot }): Promise<boolean> {
|
|
118
|
+
if (!this.claimed || input.generation !== this.generation)
|
|
119
|
+
return false
|
|
120
|
+
this.readySnapshots.push(input.agentRunSnapshot)
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async markConnectionReconnecting(): Promise<void> {}
|
|
125
|
+
|
|
126
|
+
async releaseConnection(input: { generation: number }): Promise<void> {
|
|
127
|
+
if (input.generation === this.generation)
|
|
128
|
+
this.claimed = false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async markConnectionRetryExhausted(input: { generation: number }): Promise<boolean> {
|
|
132
|
+
return this.claimed && input.generation === this.generation
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async initial(): Promise<CommandEnvelope | undefined> {
|
|
136
|
+
return this.commandLog[0]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async next(input: { afterSequence: number }): Promise<CommandEnvelope | undefined> {
|
|
140
|
+
return this.commandLog.find(command => command.commandSeq > input.afterSequence)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async getAckFloor(): Promise<number> {
|
|
144
|
+
return this.ackFloor
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async record(input: { event: DurableEventEnvelope }): Promise<{
|
|
148
|
+
status: 'recorded' | 'duplicate'
|
|
149
|
+
highWaterMark: number
|
|
150
|
+
} | { status: 'gap' }> {
|
|
151
|
+
const { event } = input
|
|
152
|
+
this.durableRecordAttempts.push(event.seq)
|
|
153
|
+
if (event.seq <= this.ackFloor)
|
|
154
|
+
return { status: 'duplicate', highWaterMark: this.ackFloor }
|
|
155
|
+
if (event.seq !== this.ackFloor + 1)
|
|
156
|
+
return { status: 'gap' }
|
|
157
|
+
if (event.body.type === 'supervisor.command.applied')
|
|
158
|
+
this.projectedCommandSequences.push(event.body.payload.commandSeq)
|
|
159
|
+
if (event.body.type === 'supervisor.command.failed')
|
|
160
|
+
this.failedCommandSequences.push(event.body.payload.commandSeq)
|
|
161
|
+
this.durableEventLog.push(event)
|
|
162
|
+
this.ackFloor = event.seq
|
|
163
|
+
await this.onDurableRecord?.(event)
|
|
164
|
+
return { status: 'recorded', highWaterMark: this.ackFloor }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
publish(input: Parameters<EphemeralEventSink['publish']>[0]): void {
|
|
168
|
+
this.ephemeralEventLog.push(input)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async handle(input: { request: SupervisorRpcRequestEnvelope }): Promise<unknown> {
|
|
172
|
+
this.rpcCalls.push(input.request)
|
|
173
|
+
const handler = this.input.rpcHandlers?.[input.request.method]
|
|
174
|
+
if (!handler)
|
|
175
|
+
throw new Error(`No in-memory RPC handler is registered for ${input.request.method}.`)
|
|
176
|
+
return await handler(input.request.body)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async loadBootstrapBody(): Promise<Omit<SessionBootstrapBody, 'sessionId' | 'initialCommand'>> {
|
|
180
|
+
return this.input.bootstrap
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async issueEphemeralSecrets(): Promise<Awaited<ReturnType<BootstrapProvider['issueEphemeralSecrets']>>> {
|
|
184
|
+
return this.input.secrets ?? {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async markSessionFailed(input: { code: string, detail?: string }): Promise<void> {
|
|
188
|
+
this.failureLog.push({ kind: 'session', code: input.code, detail: input.detail })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async failDeterministicEstablishment(input: { code: string, detail?: string }): Promise<void> {
|
|
192
|
+
this.failureLog.push({ kind: 'deterministic', code: input.code, detail: input.detail })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async markEstablishmentExhausted(): Promise<void> {
|
|
196
|
+
this.failureLog.push({ kind: 'exhausted' })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async start(callbacks: NotificationCallbacks): Promise<void> {
|
|
200
|
+
this.notifications = callbacks
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async stop(): Promise<void> {
|
|
204
|
+
this.notifications = undefined
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -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,127 @@
|
|
|
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
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
this.running = false
|
|
106
|
+
if (!this.stopped && this.wakePending)
|
|
107
|
+
void this.run()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async sendAndWait(command: NonNullable<Awaited<ReturnType<CommandSource['next']>>>): Promise<CommandAckEnvelope> {
|
|
112
|
+
const waiter = Promise.withResolvers<CommandAckEnvelope>()
|
|
113
|
+
this.pending = {
|
|
114
|
+
commandId: command.commandId,
|
|
115
|
+
commandSeq: command.commandSeq,
|
|
116
|
+
settle: waiter.resolve,
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await this.dependencies.connection.send(encodeWireFrame(command))
|
|
120
|
+
return await waiter.promise
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (this.pending?.commandId === command.commandId)
|
|
124
|
+
this.pending = undefined
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|