@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.
- package/README.md +24 -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 +736 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +464 -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 +708 -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/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +125 -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 +434 -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 +205 -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 +132 -0
- package/src/worker-runtime-client/connection-attempt.ts +340 -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 +137 -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 +300 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Logger } from '../logger'
|
|
2
|
+
import type { CommandAckEnvelope, RuntimeStateEnvelope } from '../supervisor-protocol/envelopes/supervisor-to-control-plane'
|
|
3
|
+
import type {
|
|
4
|
+
ConnectionAuthorityStore,
|
|
5
|
+
DurableEventSink,
|
|
6
|
+
EphemeralEventSink,
|
|
7
|
+
RpcDispatcher,
|
|
8
|
+
SessionFailureSink,
|
|
9
|
+
SessionSandboxRef,
|
|
10
|
+
SupervisorConnection,
|
|
11
|
+
} from './ports'
|
|
12
|
+
import {
|
|
13
|
+
supervisorBootstrapRejectionReceiptCloseCode,
|
|
14
|
+
supervisorBootstrapRejectionReceiptCloseReason,
|
|
15
|
+
} from '../supervisor-protocol/bootstrap-rejection-receipt'
|
|
16
|
+
import { isEphemeralWrappedAgentEvent } from '../supervisor-protocol/durability'
|
|
17
|
+
import { decodeSupervisorFrame, encodeWireFrame } from '../supervisor-protocol/wire-codec'
|
|
18
|
+
import { SupervisorRpcRequestManager } from './rpc-request-manager'
|
|
19
|
+
|
|
20
|
+
export class SupervisorFrameProcessor {
|
|
21
|
+
private readonly readiness = Promise.withResolvers<RuntimeStateEnvelope>()
|
|
22
|
+
private readonly rpc: SupervisorRpcRequestManager
|
|
23
|
+
private processing = Promise.resolve()
|
|
24
|
+
private stopped = false
|
|
25
|
+
private supervisorLivenessObserved = false
|
|
26
|
+
private readinessSettled = false
|
|
27
|
+
|
|
28
|
+
constructor(private readonly dependencies: {
|
|
29
|
+
ref: SessionSandboxRef
|
|
30
|
+
generation: number
|
|
31
|
+
runtimeConnectionId: string
|
|
32
|
+
runtimeConnectionAttemptId: string
|
|
33
|
+
connection: SupervisorConnection
|
|
34
|
+
authorityStore: ConnectionAuthorityStore
|
|
35
|
+
durableEvents: DurableEventSink
|
|
36
|
+
ephemeralEvents: EphemeralEventSink
|
|
37
|
+
rpcDispatcher: RpcDispatcher
|
|
38
|
+
failures: SessionFailureSink
|
|
39
|
+
logger: Logger
|
|
40
|
+
onCommandAck: (ack: CommandAckEnvelope) => void
|
|
41
|
+
onFatal: (error: unknown) => void
|
|
42
|
+
}) {
|
|
43
|
+
void this.readiness.promise.catch(() => undefined)
|
|
44
|
+
this.rpc = new SupervisorRpcRequestManager({
|
|
45
|
+
ref: dependencies.ref,
|
|
46
|
+
generation: dependencies.generation,
|
|
47
|
+
runtimeConnectionAttemptId: dependencies.runtimeConnectionAttemptId,
|
|
48
|
+
authorityStore: dependencies.authorityStore,
|
|
49
|
+
dispatcher: dependencies.rpcDispatcher,
|
|
50
|
+
connection: dependencies.connection,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
receive(data: string | ArrayBuffer | Uint8Array): void {
|
|
55
|
+
if (this.stopped)
|
|
56
|
+
return
|
|
57
|
+
this.processing = this.processing.then(async () => await this.process(frameText(data))).catch((error) => {
|
|
58
|
+
this.dependencies.onFatal(error)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async waitForReadiness(input: { signal: AbortSignal }): Promise<RuntimeStateEnvelope> {
|
|
63
|
+
if (input.signal.aborted)
|
|
64
|
+
throw input.signal.reason
|
|
65
|
+
const aborted = Promise.withResolvers<never>()
|
|
66
|
+
const onAbort = () => aborted.reject(input.signal.reason)
|
|
67
|
+
input.signal.addEventListener('abort', onAbort, { once: true })
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([this.readiness.promise, aborted.promise])
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
input.signal.removeEventListener('abort', onAbort)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
takeSupervisorLivenessObservation(): boolean {
|
|
77
|
+
const observed = this.supervisorLivenessObserved
|
|
78
|
+
this.supervisorLivenessObserved = false
|
|
79
|
+
return observed
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stop(reason?: unknown): void {
|
|
83
|
+
if (this.stopped)
|
|
84
|
+
return
|
|
85
|
+
this.stopped = true
|
|
86
|
+
this.rpc.stop(reason)
|
|
87
|
+
this.readiness.reject(reason ?? new DOMException('Supervisor connection closed.', 'AbortError'))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async drain(): Promise<void> {
|
|
91
|
+
await this.processing
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async process(text: string): Promise<void> {
|
|
95
|
+
if (this.stopped)
|
|
96
|
+
return
|
|
97
|
+
const decoded = decodeSupervisorFrame(text)
|
|
98
|
+
if (decoded.kind !== 'ok')
|
|
99
|
+
throw new Error(decoded.detail)
|
|
100
|
+
const frame = decoded.frame
|
|
101
|
+
if (frame.kind === 'runtime.state') {
|
|
102
|
+
if (this.readinessSettled)
|
|
103
|
+
throw new Error('Supervisor emitted more than one bootstrap outcome.')
|
|
104
|
+
this.readinessSettled = true
|
|
105
|
+
this.readiness.resolve(frame)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (frame.kind === 'command.ack') {
|
|
109
|
+
if (!this.readinessSettled && frame.status === 'rejected') {
|
|
110
|
+
this.readinessSettled = true
|
|
111
|
+
this.readiness.reject(new BootstrapRejectedAckError(frame))
|
|
112
|
+
}
|
|
113
|
+
this.dependencies.onCommandAck(frame)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if (frame.kind === 'rpc.request') {
|
|
117
|
+
this.rpc.handle(frame)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (frame.kind === 'rpc.cancel') {
|
|
121
|
+
this.rpc.cancel(frame)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (frame.delivery_semantics === 'ephemeral') {
|
|
125
|
+
if (frame.body.type === 'supervisor.heartbeat') {
|
|
126
|
+
this.supervisorLivenessObserved = true
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (!isEphemeralWrappedAgentEvent(frame.body))
|
|
130
|
+
throw new Error('Supervisor emitted an invalid ephemeral event body.')
|
|
131
|
+
this.dependencies.ephemeralEvents.publish({
|
|
132
|
+
sessionId: this.dependencies.ref.sessionId,
|
|
133
|
+
occurredAt: frame.occurredAt,
|
|
134
|
+
body: frame.body,
|
|
135
|
+
})
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = await this.dependencies.durableEvents.record({
|
|
140
|
+
...this.dependencies.ref,
|
|
141
|
+
runtimeConnectionId: this.dependencies.runtimeConnectionId,
|
|
142
|
+
event: frame,
|
|
143
|
+
})
|
|
144
|
+
if (!('highWaterMark' in result))
|
|
145
|
+
throw new Error(`Durable supervisor event was rejected as ${result.status}.`)
|
|
146
|
+
await this.dependencies.connection.send(encodeWireFrame({
|
|
147
|
+
kind: 'event.ack',
|
|
148
|
+
highWaterMark: result.highWaterMark,
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async acknowledgeBootstrapRejection(ack: Extract<CommandAckEnvelope, { status: 'rejected' }>): Promise<void> {
|
|
153
|
+
await this.dependencies.failures.failDeterministicEstablishment({
|
|
154
|
+
...this.dependencies.ref,
|
|
155
|
+
generation: this.dependencies.generation,
|
|
156
|
+
code: ack.errorCode,
|
|
157
|
+
detail: ack.detail,
|
|
158
|
+
})
|
|
159
|
+
this.dependencies.connection.close(
|
|
160
|
+
supervisorBootstrapRejectionReceiptCloseCode,
|
|
161
|
+
supervisorBootstrapRejectionReceiptCloseReason,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class BootstrapRejectedAckError extends Error {
|
|
167
|
+
override readonly name = 'BootstrapRejectedAckError'
|
|
168
|
+
|
|
169
|
+
constructor(readonly ack: Extract<CommandAckEnvelope, { status: 'rejected' }>) {
|
|
170
|
+
super(ack.detail ?? ack.errorCode)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function frameText(data: string | ArrayBuffer | Uint8Array): string {
|
|
175
|
+
if (typeof data === 'string')
|
|
176
|
+
return data
|
|
177
|
+
return new TextDecoder().decode(data)
|
|
178
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { SessionCommandPump } from './command-pump'
|
|
2
|
+
export { ControlAuthorityConfigurationError, createControlAuthoritySigner } from './control-authority-signer'
|
|
3
|
+
export { createE2bSupervisorConnectionOpener, openE2bSupervisorConnection } from './e2b-supervisor-connection'
|
|
4
|
+
export { reconcileConnectionLeases } from './lease-reconciler'
|
|
5
|
+
export type {
|
|
6
|
+
BootstrapProvider,
|
|
7
|
+
CommandSource,
|
|
8
|
+
ConnectionAuthorityStore,
|
|
9
|
+
ConnectionClaim,
|
|
10
|
+
ControlAuthoritySigner,
|
|
11
|
+
DurableEventResult,
|
|
12
|
+
DurableEventSink,
|
|
13
|
+
EphemeralEventSink,
|
|
14
|
+
RpcDispatcher,
|
|
15
|
+
RuntimeNotifications,
|
|
16
|
+
SandboxConnectionRuntimeTimings,
|
|
17
|
+
SessionFailureSink,
|
|
18
|
+
SessionSandboxRef,
|
|
19
|
+
SupervisorConnection,
|
|
20
|
+
SupervisorConnectionOpener,
|
|
21
|
+
} from './ports'
|
|
22
|
+
export { createPostgresNotificationListener } from './postgres-notification-listener'
|
|
23
|
+
export type { PostgresNotificationListener } from './postgres-notification-listener'
|
|
24
|
+
export { createRpcDispatcher } from './rpc-dispatcher'
|
|
25
|
+
export type { SupervisorRpcHandler } from './rpc-dispatcher'
|
|
26
|
+
export { createSandboxConnectionRuntime } from './sandbox-connection-runtime'
|
|
27
|
+
export { decryptToken, encryptToken, TokenDecryptionError } from './token-crypto'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConnectionAuthorityStore } from './ports'
|
|
2
|
+
|
|
3
|
+
export async function reconcileConnectionLeases(input: {
|
|
4
|
+
authorityStore: ConnectionAuthorityStore
|
|
5
|
+
limit?: number
|
|
6
|
+
}): Promise<number> {
|
|
7
|
+
const candidates = await input.authorityStore.listConnectionCandidates({ limit: input.limit ?? 100 })
|
|
8
|
+
if (candidates.length > 0) {
|
|
9
|
+
await input.authorityStore.notifyConnectionDemand({
|
|
10
|
+
sessionSandboxIds: candidates.map(candidate => candidate.sessionSandboxId),
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
return candidates.length
|
|
14
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { EphemeralCredentials, 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, EphemeralEventEnvelope } 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
|
+
|
|
8
|
+
export interface SessionSandboxRef {
|
|
9
|
+
sessionId: string
|
|
10
|
+
sessionSandboxId: string
|
|
11
|
+
providerSandboxId: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ConnectionClaim = SessionSandboxRef & {
|
|
15
|
+
runtimeConnectionGeneration: number
|
|
16
|
+
runtimeConnectionId: string
|
|
17
|
+
trafficAccessTokenEncrypted: string
|
|
18
|
+
leaseExpiresAt: Date
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ConnectionAuthorityStore {
|
|
22
|
+
listConnectionCandidates: (input: { limit: number }) => Promise<{ sessionSandboxId: string }[]>
|
|
23
|
+
notifyConnectionDemand: (input: { sessionSandboxIds: string[] }) => Promise<void>
|
|
24
|
+
claimConnection: (input: { sessionSandboxId: string, replicaId: string, leaseDurationMs: number }) => Promise<ConnectionClaim | undefined>
|
|
25
|
+
renewConnectionLease: (input: {
|
|
26
|
+
sessionSandboxId: string
|
|
27
|
+
replicaId: string
|
|
28
|
+
generation: number
|
|
29
|
+
leaseDurationMs: number
|
|
30
|
+
supervisorLivenessObserved: boolean
|
|
31
|
+
}) => Promise<{ renewed: boolean, leaseExpiresAt?: Date, hasOutstandingCommand: boolean }>
|
|
32
|
+
confirmConnectionAuthority: (input: {
|
|
33
|
+
sessionSandboxId: string
|
|
34
|
+
generation: number
|
|
35
|
+
runtimeConnectionAttemptId: string
|
|
36
|
+
}) => Promise<boolean>
|
|
37
|
+
markConnectionReady: (input: {
|
|
38
|
+
sessionSandboxId: string
|
|
39
|
+
generation: number
|
|
40
|
+
runtimeConnectionId: string
|
|
41
|
+
lastReceivedCommandSeq: number
|
|
42
|
+
agentRunSnapshot: SupervisorAgentRunSnapshot
|
|
43
|
+
}) => Promise<boolean>
|
|
44
|
+
markConnectionReconnecting: (input: { sessionSandboxId: string, generation: number }) => Promise<void>
|
|
45
|
+
releaseConnection: (input: { sessionSandboxId: string, generation: number, reason: string }) => Promise<void>
|
|
46
|
+
markConnectionRetryExhausted: (input: { sessionSandboxId: string, generation: number, replicaId: string }) => Promise<boolean>
|
|
47
|
+
projectAppliedCommand: (input: { sessionSandboxId: string, runtimeConnectionId: string, commandSeq: number }) => Promise<void>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CommandSource {
|
|
51
|
+
initial: (input: { sessionId: string }) => Promise<CommandEnvelope | undefined>
|
|
52
|
+
next: (input: { sessionId: string, runtimeConnectionId: string, afterSequence: number }) => Promise<CommandEnvelope | undefined>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type DurableEventResult
|
|
56
|
+
= | { status: 'recorded' | 'duplicate', highWaterMark: number }
|
|
57
|
+
| { status: 'gap' | 'stale_owner' }
|
|
58
|
+
|
|
59
|
+
export interface DurableEventSink {
|
|
60
|
+
getAckFloor: (input: { sessionSandboxId: string }) => Promise<number>
|
|
61
|
+
record: (input: SessionSandboxRef & { runtimeConnectionId: string, event: DurableEventEnvelope }) => Promise<DurableEventResult>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface EphemeralEventSink {
|
|
65
|
+
publish: (input: { sessionId: string, occurredAt: string, body: EphemeralEventEnvelope['body'] }) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RpcDispatcher {
|
|
69
|
+
handle: (input: SessionSandboxRef & { request: SupervisorRpcRequestEnvelope, signal: AbortSignal }) => Promise<unknown>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SupervisorConnection {
|
|
73
|
+
send: (data: string) => void | Promise<void>
|
|
74
|
+
close: (code?: number, reason?: string) => void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type SupervisorConnectionOpener = (input: {
|
|
78
|
+
providerSandboxId: string
|
|
79
|
+
trafficAccessToken: string
|
|
80
|
+
sessionId: string
|
|
81
|
+
sessionSandboxId: string
|
|
82
|
+
runtimeConnectionAttemptId: string
|
|
83
|
+
authorityAssertion: string
|
|
84
|
+
connectionGeneration: number
|
|
85
|
+
eventAckFloor: number
|
|
86
|
+
signal: AbortSignal
|
|
87
|
+
onMessage: (data: string | ArrayBuffer | Uint8Array) => void
|
|
88
|
+
onClose: (input: { code: number, reason: string }) => void
|
|
89
|
+
}) => Promise<SupervisorConnection>
|
|
90
|
+
|
|
91
|
+
export type ControlAuthoritySigner = (claims: {
|
|
92
|
+
providerSandboxId: string
|
|
93
|
+
sessionId: string
|
|
94
|
+
sessionSandboxId: string
|
|
95
|
+
connectionGeneration: number
|
|
96
|
+
runtimeConnectionAttemptId: string
|
|
97
|
+
eventAckFloor: number
|
|
98
|
+
}) => Promise<string>
|
|
99
|
+
|
|
100
|
+
export interface BootstrapProvider {
|
|
101
|
+
loadBootstrapBody: (input: { sessionId: string }) => Promise<Omit<SessionBootstrapBody, 'sessionId' | 'initialCommand'>>
|
|
102
|
+
issueEphemeralSecrets: (input: { sessionId: string, eventAckFloor: number, signal: AbortSignal }) => Promise<{
|
|
103
|
+
gitToken?: { token: string, expiresAt: string }
|
|
104
|
+
credentials?: EphemeralCredentials
|
|
105
|
+
}>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SessionFailureSink {
|
|
109
|
+
markSessionFailed: (input: SessionSandboxRef & { code: string, detail?: string }) => Promise<void>
|
|
110
|
+
failDeterministicEstablishment: (input: SessionSandboxRef & { generation: number, code: string, detail?: string }) => Promise<void>
|
|
111
|
+
markEstablishmentExhausted: (input: SessionSandboxRef & { generation: number }) => Promise<void>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RuntimeNotifications {
|
|
115
|
+
start: (input: {
|
|
116
|
+
onConnectionDemand: (sessionSandboxId: string) => void
|
|
117
|
+
onCommandsReady: (sessionId: string) => void
|
|
118
|
+
onOwnerChanged: (input: { sessionSandboxId: string, generation: number }) => void
|
|
119
|
+
}) => Promise<void>
|
|
120
|
+
stop: () => Promise<void>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface SandboxConnectionRuntimeTimings {
|
|
124
|
+
leaseDurationMs: number
|
|
125
|
+
leaseRenewIntervalMs: number
|
|
126
|
+
establishmentBudgetMs: number
|
|
127
|
+
dialTimeoutMs: number
|
|
128
|
+
retryInitialDelayMs: number
|
|
129
|
+
retryMaxDelayMs: number
|
|
130
|
+
candidateLimit: number
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface BootstrapCommand {
|
|
134
|
+
commandId: string
|
|
135
|
+
commandSeq: number
|
|
136
|
+
body: WireCommandBody
|
|
137
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Client as PostgresClient } from 'pg'
|
|
2
|
+
import type { Logger } from '../logger'
|
|
3
|
+
|
|
4
|
+
export interface PostgresNotificationListener {
|
|
5
|
+
start: () => Promise<void>
|
|
6
|
+
stop: () => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createPostgresNotificationListener(input: {
|
|
10
|
+
connectionString: string
|
|
11
|
+
channels: readonly string[]
|
|
12
|
+
onNotification: (input: { channel: string, payload: string | undefined }) => void
|
|
13
|
+
onDisconnected?: () => void
|
|
14
|
+
onReady?: () => void
|
|
15
|
+
logger: Logger
|
|
16
|
+
reconnectDelayMs?: number
|
|
17
|
+
}): PostgresNotificationListener {
|
|
18
|
+
const controller = new AbortController()
|
|
19
|
+
let task: Promise<void> | undefined
|
|
20
|
+
let client: PostgresClient | undefined
|
|
21
|
+
const firstReady = Promise.withResolvers<void>()
|
|
22
|
+
let connectedOnce = false
|
|
23
|
+
const reconnectDelayMs = input.reconnectDelayMs ?? 1_000
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
async start() {
|
|
27
|
+
if (task)
|
|
28
|
+
return
|
|
29
|
+
task = run().catch((error) => {
|
|
30
|
+
if (!connectedOnce)
|
|
31
|
+
firstReady.reject(error)
|
|
32
|
+
throw error
|
|
33
|
+
})
|
|
34
|
+
await firstReady.promise
|
|
35
|
+
},
|
|
36
|
+
async stop() {
|
|
37
|
+
if (!controller.signal.aborted)
|
|
38
|
+
controller.abort(new DOMException('PostgreSQL notification listener stopped.', 'AbortError'))
|
|
39
|
+
await client?.end().catch(() => undefined)
|
|
40
|
+
await task?.catch(() => undefined)
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function run(): Promise<void> {
|
|
45
|
+
const { Client, escapeIdentifier } = await import('pg')
|
|
46
|
+
while (!controller.signal.aborted) {
|
|
47
|
+
const candidate = new Client({ connectionString: input.connectionString })
|
|
48
|
+
client = candidate
|
|
49
|
+
try {
|
|
50
|
+
candidate.on('notification', notification => input.onNotification({
|
|
51
|
+
channel: notification.channel,
|
|
52
|
+
payload: notification.payload,
|
|
53
|
+
}))
|
|
54
|
+
candidate.on('error', error => input.logger.warn({ err: error }, 'PostgreSQL notification listener error'))
|
|
55
|
+
await candidate.connect()
|
|
56
|
+
for (const channel of input.channels)
|
|
57
|
+
await candidate.query(`LISTEN ${escapeIdentifier(channel)}`)
|
|
58
|
+
connectedOnce = true
|
|
59
|
+
firstReady.resolve()
|
|
60
|
+
input.onReady?.()
|
|
61
|
+
await waitForEnd({ client: candidate, signal: controller.signal })
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!controller.signal.aborted)
|
|
65
|
+
input.logger.warn({ err: error }, 'PostgreSQL notification listener disconnected')
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
input.onDisconnected?.()
|
|
69
|
+
if (client === candidate)
|
|
70
|
+
client = undefined
|
|
71
|
+
await candidate.end().catch(() => undefined)
|
|
72
|
+
}
|
|
73
|
+
if (!controller.signal.aborted)
|
|
74
|
+
await Bun.sleep(reconnectDelayMs)
|
|
75
|
+
}
|
|
76
|
+
if (!connectedOnce)
|
|
77
|
+
firstReady.reject(controller.signal.reason ?? new Error('PostgreSQL notification listener stopped before connecting.'))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function waitForEnd(input: { client: PostgresClient, signal: AbortSignal }): Promise<void> {
|
|
82
|
+
await new Promise<void>((resolve) => {
|
|
83
|
+
const finish = () => {
|
|
84
|
+
input.client.removeListener('end', finish)
|
|
85
|
+
input.signal.removeEventListener('abort', finish)
|
|
86
|
+
resolve()
|
|
87
|
+
}
|
|
88
|
+
input.client.once('end', finish)
|
|
89
|
+
input.signal.addEventListener('abort', finish, { once: true })
|
|
90
|
+
})
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SupervisorRpcMethod, SupervisorRpcRequestEnvelope } from '../supervisor-protocol/rpc'
|
|
2
|
+
import type { RpcDispatcher, SessionSandboxRef } from './ports'
|
|
3
|
+
|
|
4
|
+
export type SupervisorRpcHandler = (input: SessionSandboxRef & {
|
|
5
|
+
request: SupervisorRpcRequestEnvelope
|
|
6
|
+
signal: AbortSignal
|
|
7
|
+
}) => Promise<unknown>
|
|
8
|
+
|
|
9
|
+
export function createRpcDispatcher(input: {
|
|
10
|
+
handlers: ReadonlyMap<SupervisorRpcMethod, SupervisorRpcHandler> | Readonly<Record<string, SupervisorRpcHandler>>
|
|
11
|
+
requiredMethods: readonly string[]
|
|
12
|
+
}): RpcDispatcher {
|
|
13
|
+
const handlers = input.handlers instanceof Map
|
|
14
|
+
? new Map(input.handlers)
|
|
15
|
+
: new Map(Object.entries(input.handlers) as Array<[SupervisorRpcMethod, SupervisorRpcHandler]>)
|
|
16
|
+
const missing = input.requiredMethods.filter(method => !handlers.has(method as SupervisorRpcMethod))
|
|
17
|
+
if (missing.length > 0)
|
|
18
|
+
throw new Error(`Missing required supervisor RPC handlers: ${missing.join(', ')}.`)
|
|
19
|
+
return {
|
|
20
|
+
async handle(request) {
|
|
21
|
+
const handler = handlers.get(request.request.method)
|
|
22
|
+
if (!handler)
|
|
23
|
+
throw new Error(`Supervisor RPC method is not registered: ${request.request.method}.`)
|
|
24
|
+
return await handler(request)
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { SupervisorRpcCancelEnvelope, SupervisorRpcRequestEnvelope, SupervisorRpcResponseEnvelope } from '../supervisor-protocol/rpc'
|
|
2
|
+
import type { ConnectionAuthorityStore, RpcDispatcher, SessionSandboxRef, SupervisorConnection } from './ports'
|
|
3
|
+
import { isAppError } from '../errors'
|
|
4
|
+
|
|
5
|
+
const maximumConcurrentRequests = 32
|
|
6
|
+
|
|
7
|
+
export class SupervisorRpcRequestManager {
|
|
8
|
+
private readonly active = new Map<string, AbortController>()
|
|
9
|
+
private stopped = false
|
|
10
|
+
|
|
11
|
+
constructor(private readonly dependencies: {
|
|
12
|
+
ref: SessionSandboxRef
|
|
13
|
+
generation: number
|
|
14
|
+
runtimeConnectionAttemptId: string
|
|
15
|
+
authorityStore: ConnectionAuthorityStore
|
|
16
|
+
dispatcher: RpcDispatcher
|
|
17
|
+
connection: SupervisorConnection
|
|
18
|
+
}) {}
|
|
19
|
+
|
|
20
|
+
handle(request: SupervisorRpcRequestEnvelope): void {
|
|
21
|
+
if (this.stopped)
|
|
22
|
+
return
|
|
23
|
+
if (this.active.has(request.requestId)) {
|
|
24
|
+
this.dependencies.connection.close(1008, 'Supervisor RPC request ID was reused.')
|
|
25
|
+
this.stop(new Error('Supervisor RPC request ID was reused.'))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (this.active.size >= maximumConcurrentRequests) {
|
|
29
|
+
this.sendFailure({
|
|
30
|
+
requestId: request.requestId,
|
|
31
|
+
code: 'agent-core.rpc-capacity-exceeded',
|
|
32
|
+
message: 'Supervisor RPC capacity is unavailable.',
|
|
33
|
+
retryable: true,
|
|
34
|
+
})
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const controller = new AbortController()
|
|
38
|
+
const timeout = setTimeout(() => {
|
|
39
|
+
controller.abort(new DOMException('Supervisor RPC deadline elapsed.', 'TimeoutError'))
|
|
40
|
+
}, request.deadlineMs)
|
|
41
|
+
timeout.unref?.()
|
|
42
|
+
this.active.set(request.requestId, controller)
|
|
43
|
+
void this.execute({ request, controller }).finally(() => {
|
|
44
|
+
clearTimeout(timeout)
|
|
45
|
+
if (this.active.get(request.requestId) === controller)
|
|
46
|
+
this.active.delete(request.requestId)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cancel(frame: SupervisorRpcCancelEnvelope): void {
|
|
51
|
+
this.active.get(frame.requestId)?.abort(new DOMException('Supervisor RPC was cancelled.', 'AbortError'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
stop(reason: unknown = new DOMException('Supervisor connection closed.', 'AbortError')): void {
|
|
55
|
+
if (this.stopped)
|
|
56
|
+
return
|
|
57
|
+
this.stopped = true
|
|
58
|
+
for (const controller of this.active.values())
|
|
59
|
+
controller.abort(reason)
|
|
60
|
+
this.active.clear()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async execute(input: {
|
|
64
|
+
request: SupervisorRpcRequestEnvelope
|
|
65
|
+
controller: AbortController
|
|
66
|
+
}): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const authorized = await this.dependencies.authorityStore.confirmConnectionAuthority({
|
|
69
|
+
sessionSandboxId: this.dependencies.ref.sessionSandboxId,
|
|
70
|
+
generation: this.dependencies.generation,
|
|
71
|
+
runtimeConnectionAttemptId: this.dependencies.runtimeConnectionAttemptId,
|
|
72
|
+
})
|
|
73
|
+
if (!authorized)
|
|
74
|
+
throw new RpcAuthorityLostError()
|
|
75
|
+
const value = await this.dependencies.dispatcher.handle({
|
|
76
|
+
...this.dependencies.ref,
|
|
77
|
+
request: input.request,
|
|
78
|
+
signal: input.controller.signal,
|
|
79
|
+
})
|
|
80
|
+
if (input.controller.signal.aborted || this.stopped)
|
|
81
|
+
return
|
|
82
|
+
await this.dependencies.connection.send(JSON.stringify({
|
|
83
|
+
kind: 'rpc.response',
|
|
84
|
+
requestId: input.request.requestId,
|
|
85
|
+
result: { status: 'ok', value },
|
|
86
|
+
} satisfies SupervisorRpcResponseEnvelope))
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (input.controller.signal.aborted || this.stopped)
|
|
90
|
+
return
|
|
91
|
+
if (error instanceof RpcAuthorityLostError) {
|
|
92
|
+
this.sendFailure({
|
|
93
|
+
requestId: input.request.requestId,
|
|
94
|
+
code: 'agent-core.connection-authority-lost',
|
|
95
|
+
message: 'Supervisor connection authority is no longer current.',
|
|
96
|
+
retryable: true,
|
|
97
|
+
})
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (isAppError(error)) {
|
|
101
|
+
this.sendFailure({
|
|
102
|
+
requestId: input.request.requestId,
|
|
103
|
+
code: error.code,
|
|
104
|
+
message: error.publicMessage,
|
|
105
|
+
retryable: error.retryable,
|
|
106
|
+
})
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
this.sendFailure({
|
|
110
|
+
requestId: input.request.requestId,
|
|
111
|
+
code: 'agent-core.rpc-failed',
|
|
112
|
+
message: 'Supervisor RPC failed.',
|
|
113
|
+
retryable: true,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private sendFailure(input: {
|
|
119
|
+
requestId: string
|
|
120
|
+
code: string
|
|
121
|
+
message: string
|
|
122
|
+
retryable: boolean
|
|
123
|
+
}): void {
|
|
124
|
+
if (this.stopped)
|
|
125
|
+
return
|
|
126
|
+
void Promise.resolve(this.dependencies.connection.send(JSON.stringify({
|
|
127
|
+
kind: 'rpc.response',
|
|
128
|
+
requestId: input.requestId,
|
|
129
|
+
result: {
|
|
130
|
+
status: 'error',
|
|
131
|
+
error: { code: input.code, message: input.message, retryable: input.retryable },
|
|
132
|
+
},
|
|
133
|
+
} satisfies SupervisorRpcResponseEnvelope))).catch(() => {
|
|
134
|
+
this.dependencies.connection.close(1011, 'Supervisor RPC response delivery failed.')
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class RpcAuthorityLostError extends Error {
|
|
140
|
+
override readonly name = 'RpcAuthorityLostError'
|
|
141
|
+
}
|