@meistrari/agent-core 0.2.0-experimental.0 → 0.2.0-experimental.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.
@@ -2,137 +2,182 @@ import type { SupervisorRpcCancelEnvelope, SupervisorRpcRequestEnvelope, Supervi
2
2
  import type { ConnectionAuthorityStore, RpcDispatcher, SessionSandboxRef, SupervisorConnection } from './ports'
3
3
  import { isAppError } from '../errors'
4
4
 
5
- const maximumConcurrentRequests = 32
5
+ /** Preparation is awaited in frame order; the returned external operation runs concurrently. */
6
+ export type PrepareSupervisorRpc = (request: SupervisorRpcRequestEnvelope, signal: AbortSignal) => Promise<() => Promise<unknown>>
7
+ interface RpcError { code: string, message: string, retryable: boolean }
8
+ export interface PreparedSupervisorRpcOptions {
9
+ prepare: PrepareSupervisorRpc
10
+ send: (response: SupervisorRpcResponseEnvelope) => Promise<void>
11
+ onFailure: (error: unknown) => void
12
+ maximumConcurrentRequests?: number
13
+ maximumDeadlineMs?: number
14
+ capacityError?: RpcError
15
+ formatError?: (error: unknown, aborted: boolean) => RpcError | null
16
+ }
17
+ interface LegacyDependencies {
18
+ ref: SessionSandboxRef
19
+ generation: number
20
+ runtimeConnectionAttemptId: string
21
+ authorityStore: ConnectionAuthorityStore
22
+ dispatcher: RpcDispatcher
23
+ connection: SupervisorConnection
24
+ }
25
+ interface ActiveRequest {
26
+ stop: AbortController
27
+ cancelled: boolean
28
+ done: ReturnType<typeof Promise.withResolvers<void>>
29
+ }
6
30
 
31
+ /** One lifecycle implementation for both legacy authority ports and ordered preparation ports. */
7
32
  export class SupervisorRpcRequestManager {
8
- private readonly active = new Map<string, AbortController>()
33
+ private readonly active = new Map<string, ActiveRequest>()
9
34
  private stopped = false
35
+ private readonly options: PreparedSupervisorRpcOptions
36
+ private readonly legacyConnection?: SupervisorConnection
10
37
 
11
- constructor(private readonly dependencies: {
12
- ref: SessionSandboxRef
13
- generation: number
14
- runtimeConnectionAttemptId: string
15
- authorityStore: ConnectionAuthorityStore
16
- dispatcher: RpcDispatcher
17
- connection: SupervisorConnection
18
- }) {}
38
+ constructor(dependencies: PreparedSupervisorRpcOptions | LegacyDependencies) {
39
+ this.options = 'prepare' in dependencies ? dependencies : legacyOptions(dependencies)
40
+ this.legacyConnection = 'prepare' in dependencies ? undefined : dependencies.connection
41
+ for (const limit of [this.options.maximumConcurrentRequests ?? 32, this.options.maximumDeadlineMs ?? 30000]) {
42
+ if (!Number.isSafeInteger(limit) || limit < 1)
43
+ throw new RangeError('RPC limits must be positive safe integers')
44
+ }
45
+ }
19
46
 
47
+ /** Compatibility entrypoint; the frame processor need not await external I/O. */
20
48
  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.'))
49
+ void this.request(request, new AbortController().signal).catch((error) => {
50
+ if (this.legacyConnection && error instanceof Error && error.message === 'duplicate_active_rpc_request') {
51
+ this.legacyConnection.close(1008, 'Supervisor RPC request ID was reused.')
52
+ this.stop(error)
53
+ }
54
+ else {
55
+ this.failed(error)
56
+ }
57
+ })
58
+ }
59
+
60
+ async request(request: SupervisorRpcRequestEnvelope, parent: AbortSignal): Promise<void> {
61
+ if (this.stopped || parent.aborted)
26
62
  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
- })
63
+ if (this.active.has(request.requestId))
64
+ throw new Error('duplicate_active_rpc_request')
65
+ if (this.active.size >= (this.options.maximumConcurrentRequests ?? 32)) {
66
+ await this.sendFailure(request.requestId, this.options.capacityError ?? { code: 'rpc_capacity_exceeded', message: 'rpc_capacity_exceeded', retryable: true })
35
67
  return
36
68
  }
37
- const controller = new AbortController()
38
- const timeout = setTimeout(() => {
39
- controller.abort(new DOMException('Supervisor RPC deadline elapsed.', 'TimeoutError'))
40
- }, request.deadlineMs)
69
+ const entry: ActiveRequest = { stop: new AbortController(), cancelled: false, done: Promise.withResolvers<void>() }
70
+ this.active.set(request.requestId, entry)
71
+ const deadline = new AbortController()
72
+ const timeout = setTimeout(() => deadline.abort(new DOMException('Supervisor RPC deadline elapsed.', 'TimeoutError')), Math.min(request.deadlineMs, this.options.maximumDeadlineMs ?? 30000))
41
73
  timeout.unref?.()
42
- this.active.set(request.requestId, controller)
43
- void this.execute({ request, controller }).finally(() => {
74
+ const signal = AbortSignal.any([parent, entry.stop.signal, deadline.signal])
75
+ const finish = () => {
44
76
  clearTimeout(timeout)
45
- if (this.active.get(request.requestId) === controller)
46
- this.active.delete(request.requestId)
47
- })
77
+ this.active.delete(request.requestId)
78
+ entry.done.resolve()
79
+ }
80
+ const respondFailure = async (error: unknown) => {
81
+ if (this.stopped || parent.aborted || entry.cancelled)
82
+ return
83
+ const code = signal.aborted ? 'rpc_deadline_exceeded' : 'rpc_failed'
84
+ const failure = this.options.formatError
85
+ ? this.options.formatError(error, signal.aborted)
86
+ : { code, message: code, retryable: signal.aborted }
87
+ if (failure)
88
+ await this.sendFailure(request.requestId, failure)
89
+ }
90
+ let execute: () => Promise<unknown>
91
+ try {
92
+ execute = await this.options.prepare(request, signal)
93
+ signal.throwIfAborted()
94
+ }
95
+ catch (error) {
96
+ try {
97
+ await respondFailure(error)
98
+ }
99
+ finally {
100
+ finish()
101
+ }
102
+ return
103
+ }
104
+ void (async () => {
105
+ let value: unknown
106
+ try {
107
+ value = await execute()
108
+ signal.throwIfAborted()
109
+ }
110
+ catch (error) {
111
+ await respondFailure(error)
112
+ return
113
+ }
114
+ if (!this.stopped && !entry.cancelled && !parent.aborted)
115
+ await this.options.send({ kind: 'rpc.response', requestId: request.requestId, result: { status: 'ok', value } })
116
+ })().catch(error => this.failed(error)).finally(finish)
48
117
  }
49
118
 
50
- cancel(frame: SupervisorRpcCancelEnvelope): void {
51
- this.active.get(frame.requestId)?.abort(new DOMException('Supervisor RPC was cancelled.', 'AbortError'))
119
+ cancel(frame: SupervisorRpcCancelEnvelope | string): void {
120
+ const entry = this.active.get(typeof frame === 'string' ? frame : frame.requestId)
121
+ if (entry) {
122
+ entry.cancelled = true
123
+ entry.stop.abort(new DOMException('Supervisor RPC was cancelled.', 'AbortError'))
124
+ }
52
125
  }
53
126
 
54
127
  stop(reason: unknown = new DOMException('Supervisor connection closed.', 'AbortError')): void {
55
- if (this.stopped)
56
- return
57
128
  this.stopped = true
58
- for (const controller of this.active.values())
59
- controller.abort(reason)
60
- this.active.clear()
129
+ for (const entry of this.active.values()) {
130
+ entry.cancelled = true
131
+ entry.stop.abort(reason)
132
+ }
133
+ }
134
+
135
+ async drain(): Promise<void> {
136
+ await Promise.all([...this.active.values()].map(entry => entry.done.promise))
61
137
  }
62
138
 
63
- private async execute(input: {
64
- request: SupervisorRpcRequestEnvelope
65
- controller: AbortController
66
- }): Promise<void> {
139
+ async close(): Promise<void> {
140
+ this.stop()
141
+ await this.drain()
142
+ }
143
+
144
+ private async sendFailure(requestId: string, error: RpcError): Promise<void> {
145
+ if (!this.stopped)
146
+ await this.options.send({ kind: 'rpc.response', requestId, result: { status: 'error', error } })
147
+ }
148
+
149
+ private failed(error: unknown): void {
67
150
  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))
151
+ this.options.onFailure(error)
87
152
  }
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
- })
153
+ catch {
154
+ this.stop(error)
115
155
  }
116
156
  }
157
+ }
117
158
 
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
- })
159
+ function legacyOptions(dependencies: LegacyDependencies): PreparedSupervisorRpcOptions {
160
+ return {
161
+ maximumConcurrentRequests: 32,
162
+ maximumDeadlineMs: Number.MAX_SAFE_INTEGER,
163
+ prepare: async (request, signal) => {
164
+ const authorized = await dependencies.authorityStore.confirmConnectionAuthority({ sessionSandboxId: dependencies.ref.sessionSandboxId, generation: dependencies.generation, runtimeConnectionAttemptId: dependencies.runtimeConnectionAttemptId })
165
+ if (!authorized)
166
+ throw new RpcAuthorityLostError()
167
+ return () => dependencies.dispatcher.handle({ ...dependencies.ref, request, signal })
168
+ },
169
+ send: async (response) => { await dependencies.connection.send(JSON.stringify(response)) },
170
+ onFailure: () => { dependencies.connection.close(1011, 'Supervisor RPC response delivery failed.') },
171
+ capacityError: { code: 'agent-core.rpc-capacity-exceeded', message: 'Supervisor RPC capacity is unavailable.', retryable: true },
172
+ formatError: (error, aborted) => {
173
+ if (aborted)
174
+ return null
175
+ if (error instanceof RpcAuthorityLostError)
176
+ return { code: 'agent-core.connection-authority-lost', message: 'Supervisor connection authority is no longer current.', retryable: true }
177
+ if (isAppError(error))
178
+ return { code: error.code, message: error.publicMessage, retryable: error.retryable }
179
+ return { code: 'agent-core.rpc-failed', message: 'Supervisor RPC failed.', retryable: true }
180
+ },
136
181
  }
137
182
  }
138
183
 
@@ -151,34 +151,41 @@ class OwnedConnection {
151
151
  this.claimed?.(claim.sessionId)
152
152
  let establishmentDeadline = performance.now() + this.dependencies.timings.establishmentBudgetMs
153
153
  let backoff = this.dependencies.timings.retryInitialDelayMs
154
- while (!this.controller.signal.aborted) {
155
- const attempt = new SessionSandboxConnectionAttempt({
156
- claim,
157
- establishmentDeadline,
158
- replicaId: this.dependencies.replicaId,
159
- trafficTokenEncryptionKey: this.dependencies.trafficTokenEncryptionKey,
160
- signControlAuthority: this.dependencies.signControlAuthority,
161
- openSupervisorConnection: this.dependencies.openSupervisorConnection,
162
- authorityStore: this.dependencies.authorityStore,
163
- commands: this.dependencies.commands,
164
- durableEvents: this.dependencies.durableEvents,
165
- ephemeralEvents: this.dependencies.ephemeralEvents,
166
- rpc: this.dependencies.rpc,
167
- bootstrap: this.dependencies.bootstrap,
168
- failures: this.dependencies.failures,
169
- timings: this.dependencies.timings,
170
- logger: this.dependencies.logger,
171
- parentSignal: this.controller.signal,
172
- })
173
- this.attempt = attempt
174
- try {
175
- await attempt.run()
176
- }
177
- catch (error) {
178
- attempt.stop(error)
179
- await attempt.drain()
154
+ const exhaustedBudget = await runConnectionAttempts<boolean>({
155
+ signal: this.controller.signal,
156
+ attempt: async () => {
157
+ const attempt = new SessionSandboxConnectionAttempt({
158
+ claim,
159
+ establishmentDeadline,
160
+ replicaId: this.dependencies.replicaId,
161
+ trafficTokenEncryptionKey: this.dependencies.trafficTokenEncryptionKey,
162
+ signControlAuthority: this.dependencies.signControlAuthority,
163
+ openSupervisorConnection: this.dependencies.openSupervisorConnection,
164
+ authorityStore: this.dependencies.authorityStore,
165
+ commands: this.dependencies.commands,
166
+ durableEvents: this.dependencies.durableEvents,
167
+ ephemeralEvents: this.dependencies.ephemeralEvents,
168
+ rpc: this.dependencies.rpc,
169
+ bootstrap: this.dependencies.bootstrap,
170
+ failures: this.dependencies.failures,
171
+ timings: this.dependencies.timings,
172
+ logger: this.dependencies.logger,
173
+ parentSignal: this.controller.signal,
174
+ })
175
+ this.attempt = attempt
176
+ try {
177
+ await attempt.run()
178
+ return { delayMs: 0 }
179
+ }
180
+ catch (error) {
181
+ attempt.stop(error)
182
+ await attempt.drain()
183
+ throw error
184
+ }
185
+ },
186
+ onFailure: async (error) => {
180
187
  if (this.controller.signal.aborted)
181
- return
188
+ return { done: false }
182
189
  if (error instanceof DeterministicEstablishmentError) {
183
190
  if (!error.reported) {
184
191
  await this.dependencies.failures.failDeterministicEstablishment({
@@ -190,10 +197,10 @@ class OwnedConnection {
190
197
  detail: error.detail,
191
198
  })
192
199
  }
193
- return
200
+ return { done: false }
194
201
  }
195
202
  if (error instanceof ConnectionAuthorityLostError)
196
- return
203
+ return { done: false }
197
204
  if (error instanceof TokenDecryptionError) {
198
205
  await this.dependencies.failures.markSessionFailed({
199
206
  sessionId: claim.sessionId,
@@ -201,7 +208,7 @@ class OwnedConnection {
201
208
  providerSandboxId: claim.providerSandboxId,
202
209
  code: 'agent-core.traffic-token-decryption-failed',
203
210
  })
204
- return
211
+ return { done: false }
205
212
  }
206
213
  if (error instanceof ConnectionClosedError && error.ready) {
207
214
  establishmentDeadline = performance.now() + this.dependencies.timings.establishmentBudgetMs
@@ -212,12 +219,13 @@ class OwnedConnection {
212
219
  generation: claim.runtimeConnectionGeneration,
213
220
  })
214
221
  if (performance.now() + backoff >= establishmentDeadline)
215
- break
216
- await abortableDelay(backoff, this.controller.signal)
222
+ return { done: true }
223
+ const delayMs = backoff
217
224
  backoff = Math.min(backoff * 2, this.dependencies.timings.retryMaxDelayMs)
218
- }
219
- }
220
- if (!this.controller.signal.aborted) {
225
+ return { delayMs }
226
+ },
227
+ })
228
+ if (exhaustedBudget && !this.controller.signal.aborted) {
221
229
  const exhausted = await this.dependencies.authorityStore.markConnectionRetryExhausted({
222
230
  sessionSandboxId: claim.sessionSandboxId,
223
231
  generation: claim.runtimeConnectionGeneration,
@@ -299,3 +307,32 @@ function validatedTimings(timings: SandboxConnectionRuntimeTimings): SandboxConn
299
307
  throw new RangeError('retryInitialDelayMs must not exceed retryMaxDelayMs.')
300
308
  return timings
301
309
  }
310
+
311
+ export type ConnectionAttemptOutcome<T> = { done: T } | { delayMs: number }
312
+
313
+ /** Retry scheduling only. Authority, failure classification and budgets belong to callers. */
314
+ export async function runConnectionAttempts<T>(options: {
315
+ signal: AbortSignal
316
+ attempt: (attemptNumber: number) => Promise<ConnectionAttemptOutcome<T>>
317
+ onFailure: (error: unknown, attemptNumber: number) => Promise<ConnectionAttemptOutcome<T>>
318
+ }): Promise<T> {
319
+ let attemptNumber = 0
320
+ while (true) {
321
+ options.signal.throwIfAborted()
322
+ let outcome: ConnectionAttemptOutcome<T>
323
+ try {
324
+ outcome = await options.attempt(attemptNumber)
325
+ }
326
+ catch (error) {
327
+ options.signal.throwIfAborted()
328
+ outcome = await options.onFailure(error, attemptNumber)
329
+ }
330
+ options.signal.throwIfAborted()
331
+ if ('done' in outcome)
332
+ return outcome.done
333
+ if (!Number.isSafeInteger(outcome.delayMs) || outcome.delayMs < 0)
334
+ throw new RangeError('Connection retry delay must be a nonnegative safe integer.')
335
+ await abortableDelay(outcome.delayMs, options.signal)
336
+ attemptNumber++
337
+ }
338
+ }