@ouraihub/aether-worker 0.0.0-stage → 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.
@@ -0,0 +1,560 @@
1
+ import { isContract, type EndStatus, type KitError, type RunEvent, type RunSnapshot } from '@ouraihub/aether-contracts'
2
+ import { KitFailure, type Approver } from '@ouraihub/aether-core'
3
+ import type { DurableObjectState } from './cloudflare.js'
4
+ import { immutableActor, isActor, ownerKey, type Actor, type WorkerContext } from './context.js'
5
+ import type { AcceptedHandle } from './conversations/index.js'
6
+ import type { PreparedExecution, WorkerRuntime } from './runtime.js'
7
+
8
+ type Runtime = Pick<WorkerRuntime, 'prepare' | 'execute' | 'abandon' | 'recover'>
9
+ type Phase = 'creating' | 'accepted' | 'running' | EndStatus
10
+ interface Metadata {
11
+ phase: Phase
12
+ runId: string
13
+ actor: Actor
14
+ handle: AcceptedHandle | null
15
+ snapshot: RunSnapshot
16
+ revision: number
17
+ acceptedUntil: number | null
18
+ pending: { callId: string; revision: number } | null
19
+ }
20
+ interface Pending {
21
+ callId: string
22
+ revision: number
23
+ decision: boolean | undefined
24
+ resolve: ((decision: boolean) => void) | null
25
+ }
26
+ const storageKey = 'run'
27
+ const encoder = new TextEncoder()
28
+ const conflict = (reason: string): KitFailure => new KitFailure('conflict', 'Run state conflict', { reason })
29
+ const missing = (): KitFailure => new KitFailure('not_found', 'Run not found')
30
+ const interrupted = (): KitError => ({
31
+ code: 'internal',
32
+ message: 'Run interrupted',
33
+ details: { reason: 'interrupted', errorId: crypto.randomUUID() },
34
+ })
35
+
36
+ /** No arbitrary messages/details cross this boundary, even from host KitFailures. */
37
+ function publicError(error: unknown): KitError {
38
+ const code = error instanceof KitFailure ? error.code : isContract('KitError', error) ? error.code : 'internal'
39
+ const messages: Record<KitError['code'], string> = {
40
+ bad_request: 'Invalid request',
41
+ not_found: 'Run not found',
42
+ conflict: 'Run state conflict',
43
+ unsupported: 'Unsupported operation',
44
+ cancelled: 'Cancelled',
45
+ unauthorized: 'Authentication required',
46
+ forbidden: 'Access denied',
47
+ rate_limited: 'Rate limit exceeded',
48
+ upstream: 'Upstream execution failed',
49
+ timeout: 'Execution timed out',
50
+ network: 'Network execution failed',
51
+ needs_config: 'Configuration required',
52
+ budget_exceeded: 'Execution budget exceeded',
53
+ incompatible_db: 'Incompatible database',
54
+ internal: 'Internal execution failure',
55
+ }
56
+ // Error IDs are generated here, never copied from a potentially secret-bearing host error.
57
+ const reason = error instanceof KitFailure ? error.details?.reason : undefined
58
+ const safeReason =
59
+ reason === 'event_replay_unavailable' || reason === 'subscriber_already_active' || reason === 'approval_not_pending'
60
+ ? reason
61
+ : undefined
62
+ return {
63
+ code,
64
+ message: messages[code],
65
+ ...(code === 'internal' || safeReason
66
+ ? {
67
+ details: {
68
+ ...(safeReason ? { reason: safeReason } : {}),
69
+ ...(code === 'internal' ? { errorId: crypto.randomUUID() } : {}),
70
+ },
71
+ }
72
+ : {}),
73
+ }
74
+ }
75
+ function statusCode(error: KitError): number {
76
+ switch (error.code) {
77
+ case 'bad_request':
78
+ return 400
79
+ case 'unauthorized':
80
+ return 401
81
+ case 'forbidden':
82
+ return 403
83
+ case 'not_found':
84
+ return 404
85
+ case 'conflict':
86
+ return 409
87
+ case 'rate_limited':
88
+ return 429
89
+ case 'internal':
90
+ case 'incompatible_db':
91
+ return 500
92
+ default:
93
+ return 422
94
+ }
95
+ }
96
+ function actorHeader(request: Request): Actor {
97
+ try {
98
+ const encoded = request.headers.get('x-aether-actor')
99
+ if (!encoded || encoded.length > 131072) throw missing()
100
+ const bytes = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0))
101
+ const value: unknown = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
102
+ if (!isActor(value)) throw missing()
103
+ return immutableActor(value)
104
+ } catch {
105
+ throw missing()
106
+ }
107
+ }
108
+ function terminal(phase: Phase): phase is EndStatus {
109
+ return phase === 'completed' || phase === 'failed' || phase === 'cancelled'
110
+ }
111
+ function endEvent(snapshot: RunSnapshot): RunEvent {
112
+ if (snapshot.status === 'running') throw conflict('run_not_terminal')
113
+ return {
114
+ kind: 'end',
115
+ status: snapshot.status,
116
+ ...(snapshot.error ? { error: snapshot.error } : {}),
117
+ ...(snapshot.exhausted === undefined ? {} : { exhausted: snapshot.exhausted }),
118
+ }
119
+ }
120
+
121
+ /** The router alone constructs this private protocol after authentication. */
122
+ export function createRunCoordinator<Env>(createRuntime: (env: Env) => Runtime) {
123
+ return class RunCoordinator {
124
+ private readonly state: DurableObjectState
125
+ private readonly runtime: Runtime
126
+ private readonly ready: Promise<void>
127
+ private queue: Promise<void> = Promise.resolve()
128
+ private metadata: Metadata | null = null
129
+ private prepared: PreparedExecution | null = null
130
+ private readonly abort = new AbortController()
131
+ private operation: Promise<void> | null = null
132
+ private pending: Pending | null = null
133
+ private controller: ReadableStreamDefaultController<Uint8Array> | null = null
134
+ private detach: (() => void) | null = null
135
+ private sequence = 0
136
+ private subscribed = false
137
+
138
+ constructor(state: DurableObjectState, env: Env) {
139
+ this.state = state
140
+ this.runtime = createRuntime(env)
141
+ this.ready = state.blockConcurrencyWhile(async () => {
142
+ this.metadata = (await state.storage.get<Metadata>(storageKey)) ?? null
143
+ const record = this.metadata
144
+ if (!record || terminal(record.phase)) return
145
+ let recoveryFailure: unknown
146
+ if (record.handle) {
147
+ try {
148
+ await this.runtime.recover(record.handle, this.context(record.actor))
149
+ } catch (error) {
150
+ if (!(record.phase === 'creating' && error instanceof KitFailure && error.code === 'not_found'))
151
+ recoveryFailure = error
152
+ }
153
+ }
154
+ record.phase = 'failed'
155
+ record.snapshot = { ...record.snapshot, status: 'failed', error: interrupted() }
156
+ record.pending = null
157
+ record.acceptedUntil = null
158
+ await this.persist()
159
+ await state.storage.deleteAlarm()
160
+ if (recoveryFailure !== undefined) throw recoveryFailure
161
+ })
162
+ }
163
+
164
+ private context(actor: Actor): WorkerContext {
165
+ return { actor, signal: this.abort.signal, requestId: crypto.randomUUID(), now: Date.now() }
166
+ }
167
+ private serialize<T>(action: () => Promise<T>): Promise<T> {
168
+ const result = this.queue.then(action)
169
+ this.queue = result.then(
170
+ () => undefined,
171
+ () => undefined,
172
+ )
173
+ return result
174
+ }
175
+ private async persist(): Promise<void> {
176
+ if (!this.metadata) return
177
+ this.metadata.revision++
178
+ await this.state.storage.put(storageKey, this.metadata)
179
+ }
180
+ private owns(actor: Actor): boolean {
181
+ return (
182
+ this.metadata !== null &&
183
+ ownerKey(actor) === ownerKey(this.metadata.actor) &&
184
+ actor.sessionId === this.metadata.actor.sessionId
185
+ )
186
+ }
187
+ private publish(event: RunEvent): void {
188
+ const controller = this.controller
189
+ if (!controller) return
190
+ try {
191
+ // Bound an unread subscriber rather than retaining an unbounded transcript in memory.
192
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) throw new Error('Subscriber stalled')
193
+ controller.enqueue(
194
+ encoder.encode(`event: run_event\nid: ${++this.sequence}\ndata: ${JSON.stringify(event)}\n\n`),
195
+ )
196
+ } catch {
197
+ this.close()
198
+ this.state.waitUntil(this.cancelRun())
199
+ }
200
+ }
201
+ private close(): void {
202
+ const controller = this.controller
203
+ this.controller = null
204
+ this.detach?.()
205
+ this.detach = null
206
+ try {
207
+ controller?.close()
208
+ } catch {
209
+ /* The consumer may already have cancelled. */
210
+ }
211
+ }
212
+ private receive = (event: RunEvent): void => {
213
+ if (event.kind === 'end' || this.abort.signal.aborted) return
214
+ let approval: Pending | null = null
215
+ if (event.kind === 'approval_required') {
216
+ if (this.pending) throw conflict('approval_already_pending')
217
+ approval = {
218
+ callId: event.callId,
219
+ revision: (this.metadata?.revision ?? 0) + 1,
220
+ decision: undefined,
221
+ resolve: null,
222
+ }
223
+ this.pending = approval
224
+ }
225
+ const registered = approval
226
+ const write = this.serialize(async () => {
227
+ if (!this.metadata || terminal(this.metadata.phase) || this.abort.signal.aborted) return
228
+ if (registered) {
229
+ this.metadata.pending = { callId: registered.callId, revision: registered.revision }
230
+ await this.persist()
231
+ }
232
+ this.publish(event)
233
+ })
234
+ this.state.waitUntil(write.catch(() => this.cancelRun()))
235
+ }
236
+ private readonly approver: Approver = {
237
+ wait: async (runId, call, signal) => {
238
+ const pending = this.pending
239
+ if (!pending || pending.callId !== call.id || this.metadata?.runId !== runId)
240
+ throw conflict('approval_not_pending')
241
+ if (signal.aborted || this.abort.signal.aborted) return false
242
+ let decision = pending.decision
243
+ if (decision === undefined) {
244
+ const { promise, resolve } = Promise.withResolvers<boolean>()
245
+ const abort = (): void => resolve(false)
246
+ pending.resolve = resolve
247
+ signal.addEventListener('abort', abort, { once: true })
248
+ this.abort.signal.addEventListener('abort', abort, { once: true })
249
+ if (signal.aborted || this.abort.signal.aborted) resolve(false)
250
+ try {
251
+ decision = await promise
252
+ } finally {
253
+ signal.removeEventListener('abort', abort)
254
+ this.abort.signal.removeEventListener('abort', abort)
255
+ pending.resolve = null
256
+ }
257
+ }
258
+ await this.serialize(async () => {
259
+ if (this.pending !== pending || !this.metadata) return
260
+ this.pending = null
261
+ this.metadata.pending = null
262
+ await this.persist()
263
+ })
264
+ return decision
265
+ },
266
+ }
267
+ private async finish(snapshot: RunSnapshot): Promise<void> {
268
+ await this.serialize(async () => {
269
+ const record = this.metadata
270
+ if (!record || terminal(record.phase)) return
271
+ if (snapshot.status === 'running') throw conflict('runtime_not_terminal')
272
+ // Rebuild the DTO: no runtime-private additions or unsanitized error details persist.
273
+ record.snapshot = {
274
+ runId: record.runId,
275
+ conversationId: record.snapshot.conversationId,
276
+ turnId: record.snapshot.turnId,
277
+ status: snapshot.status,
278
+ cancelRequested: record.snapshot.cancelRequested || snapshot.cancelRequested,
279
+ ...(snapshot.error ? { error: publicError(snapshot.error) } : {}),
280
+ ...(snapshot.exhausted === undefined ? {} : { exhausted: snapshot.exhausted }),
281
+ }
282
+ record.phase = snapshot.status
283
+ record.pending = null
284
+ record.acceptedUntil = null
285
+ this.pending?.resolve?.(false)
286
+ this.pending = null
287
+ await this.persist()
288
+ await this.state.storage.deleteAlarm()
289
+ this.publish(endEvent(record.snapshot))
290
+ this.close()
291
+ this.prepared = null
292
+ })
293
+ }
294
+ private async fail(error: unknown): Promise<void> {
295
+ const record = this.metadata
296
+ if (!record || terminal(record.phase)) return
297
+ if (record.handle) {
298
+ try {
299
+ await this.runtime.recover(record.handle, this.context(record.actor))
300
+ } catch (recoveryError) {
301
+ if (!(
302
+ record.phase === 'creating' &&
303
+ recoveryError instanceof KitFailure &&
304
+ recoveryError.code === 'not_found'
305
+ ))
306
+ throw recoveryError
307
+ }
308
+ }
309
+ await this.finish({ ...record.snapshot, status: 'failed', error: publicError(error) })
310
+ }
311
+ private async run(prepared: PreparedExecution, execute: boolean): Promise<void> {
312
+ try {
313
+ const snapshot = execute
314
+ ? await this.runtime.execute(prepared, this.abort.signal, this.receive, this.approver)
315
+ : await this.runtime.abandon(prepared, 'cancelled')
316
+ await this.finish(snapshot)
317
+ } catch (error) {
318
+ await this.fail(error)
319
+ }
320
+ }
321
+ private async cancelRun(): Promise<void> {
322
+ await this.serialize(async () => {
323
+ const record = this.metadata
324
+ if (!record || terminal(record.phase)) return
325
+ this.abort.abort()
326
+ this.pending?.resolve?.(false)
327
+ record.snapshot.cancelRequested = true
328
+ await this.persist()
329
+ if ((record.phase === 'accepted' || !this.operation) && this.prepared) {
330
+ const prepared = this.prepared
331
+ record.phase = 'running'
332
+ record.acceptedUntil = null
333
+ await this.persist()
334
+ this.operation = Promise.resolve().then(() => this.run(prepared, false))
335
+ this.state.waitUntil(this.operation)
336
+ }
337
+ })
338
+ await this.operation
339
+ }
340
+ private async accept(request: Request): Promise<Response> {
341
+ let value: unknown
342
+ try {
343
+ value = await request.json()
344
+ } catch {
345
+ throw new KitFailure('bad_request', 'Invalid JSON')
346
+ }
347
+ if (
348
+ typeof value !== 'object' ||
349
+ value === null ||
350
+ !('actor' in value) ||
351
+ !isActor(value.actor) ||
352
+ !('runId' in value) ||
353
+ typeof value.runId !== 'string' ||
354
+ !/^[A-Za-z0-9_-]{1,128}$/u.test(value.runId) ||
355
+ !('request' in value) ||
356
+ !isContract('RunRequest', value.request)
357
+ )
358
+ throw new KitFailure('bad_request', 'Invalid run request')
359
+ const actor = immutableActor(value.actor),
360
+ runId = value.runId,
361
+ input = value.request
362
+ await this.serialize(async () => {
363
+ if (this.metadata) throw conflict('run_already_exists')
364
+ this.metadata = {
365
+ phase: 'creating',
366
+ actor,
367
+ runId,
368
+ handle: null,
369
+ revision: 0,
370
+ pending: null,
371
+ acceptedUntil: null,
372
+ snapshot: { runId, conversationId: null, turnId: null, status: 'running', cancelRequested: false },
373
+ }
374
+ await this.persist()
375
+ })
376
+ const abort = (): void => {
377
+ this.state.waitUntil(this.cancelRun())
378
+ }
379
+ request.signal.addEventListener('abort', abort, { once: true })
380
+ if (request.signal.aborted) this.abort.abort()
381
+ let accepted = false
382
+ const preparation = async (): Promise<void> => {
383
+ try {
384
+ const prepared = await this.runtime.prepare(runId, input, this.context(actor), (handle) =>
385
+ this.serialize(async () => {
386
+ if (!this.metadata || this.abort.signal.aborted) throw new KitFailure('cancelled', 'Cancelled')
387
+ this.metadata.handle = handle
388
+ this.metadata.snapshot.conversationId = handle?.conversationId ?? null
389
+ this.metadata.snapshot.turnId = handle?.turnId ?? null
390
+ await this.persist()
391
+ }),
392
+ )
393
+ this.prepared = prepared
394
+ if (this.abort.signal.aborted) {
395
+ await this.run(prepared, false)
396
+ return
397
+ }
398
+ await this.serialize(async () => {
399
+ if (!this.metadata) throw missing()
400
+ if (this.abort.signal.aborted) throw new KitFailure('cancelled', 'Cancelled')
401
+ this.metadata.phase = 'accepted'
402
+ this.metadata.snapshot.conversationId = prepared.accepted.conversationId
403
+ this.metadata.snapshot.turnId = prepared.accepted.turnId
404
+ this.metadata.acceptedUntil = Date.now() + 30000
405
+ await this.state.storage.setAlarm(this.metadata.acceptedUntil)
406
+ await this.persist()
407
+ })
408
+ accepted = true
409
+ } catch (error) {
410
+ if (this.prepared) await this.run(this.prepared, false)
411
+ else await this.fail(error)
412
+ throw error
413
+ } finally {
414
+ request.signal.removeEventListener('abort', abort)
415
+ }
416
+ }
417
+ const work = preparation()
418
+ this.operation = work
419
+ this.state.waitUntil(work.catch(() => undefined))
420
+ await work
421
+ if (!accepted || !this.prepared) throw new KitFailure('cancelled', 'Cancelled')
422
+ return Response.json(this.prepared.accepted, { status: 202 })
423
+ }
424
+ private async events(request: Request, actor: Actor): Promise<Response> {
425
+ try {
426
+ return await this.serialize(async () => {
427
+ const record = this.metadata
428
+ if (!record || !this.owns(actor)) throw missing()
429
+ if (request.headers.has('Last-Event-ID')) throw conflict('event_replay_unavailable')
430
+ if (request.signal.aborted) throw new KitFailure('cancelled', 'Cancelled')
431
+ if (!terminal(record.phase) && (record.phase !== 'accepted' || this.subscribed))
432
+ throw conflict('subscriber_already_active')
433
+ const stream = new ReadableStream<Uint8Array>(
434
+ {
435
+ start: (controller) => {
436
+ this.controller = controller
437
+ },
438
+ cancel: async () => {
439
+ this.close()
440
+ await this.cancelRun()
441
+ },
442
+ },
443
+ { highWaterMark: 1024 * 1024, size: (chunk) => chunk.byteLength },
444
+ )
445
+ if (terminal(record.phase)) {
446
+ this.publish(endEvent(record.snapshot))
447
+ this.close()
448
+ } else {
449
+ if (!this.prepared) throw conflict('prepared_execution_unavailable')
450
+ this.subscribed = true
451
+ record.phase = 'running'
452
+ this.operation = null
453
+ record.acceptedUntil = null
454
+ await this.persist()
455
+ await this.state.storage.deleteAlarm()
456
+ const abort = (): void => {
457
+ this.close()
458
+ this.state.waitUntil(this.cancelRun())
459
+ }
460
+ request.signal.addEventListener('abort', abort, { once: true })
461
+ this.detach = () => request.signal.removeEventListener('abort', abort)
462
+ const prepared = this.prepared
463
+ this.operation = Promise.resolve().then(() => this.run(prepared, true))
464
+ this.state.waitUntil(this.operation)
465
+ if (request.signal.aborted) abort()
466
+ }
467
+ return new Response(stream, {
468
+ headers: {
469
+ 'Content-Type': 'text/event-stream',
470
+ 'Cache-Control': 'no-cache, no-transform',
471
+ 'X-Accel-Buffering': 'no',
472
+ 'X-Content-Type-Options': 'nosniff',
473
+ },
474
+ })
475
+ })
476
+ } catch (error) {
477
+ // A rejected competing subscriber must never cancel the actual owner stream.
478
+ if (
479
+ this.owns(actor) &&
480
+ ((this.subscribed && !this.operation) || (this.metadata?.phase === 'accepted' && request.signal.aborted))
481
+ ) {
482
+ this.close()
483
+ await this.cancelRun()
484
+ }
485
+ throw error
486
+ }
487
+ }
488
+ async fetch(request: Request): Promise<Response> {
489
+ try {
490
+ await this.ready
491
+ const path = new URL(request.url).pathname
492
+ if (request.method === 'POST' && path === '/accept') return await this.accept(request)
493
+ const actor = actorHeader(request)
494
+ if (request.method === 'POST' && path === '/cancel') {
495
+ if (this.owns(actor)) await this.cancelRun()
496
+ return Response.json({ ok: true })
497
+ }
498
+ if (!this.owns(actor)) throw missing()
499
+ if (request.method === 'GET' && path === '/events') return await this.events(request, actor)
500
+ if (request.method === 'GET' && path === '/snapshot')
501
+ return await this.serialize(async () => Response.json(this.metadata?.snapshot))
502
+ if (request.method === 'POST' && path === '/approval') {
503
+ let value: unknown
504
+ try {
505
+ value = await request.json()
506
+ } catch {
507
+ throw new KitFailure('bad_request', 'Invalid JSON')
508
+ }
509
+ if (
510
+ typeof value !== 'object' ||
511
+ value === null ||
512
+ !('callId' in value) ||
513
+ typeof value.callId !== 'string' ||
514
+ !('decision' in value) ||
515
+ (value.decision !== 'allow' && value.decision !== 'deny')
516
+ )
517
+ throw new KitFailure('bad_request', 'Invalid approval')
518
+ const callId = value.callId,
519
+ allowed = value.decision === 'allow'
520
+ return await this.serialize(async () => {
521
+ const pending = this.pending,
522
+ stored = this.metadata?.pending
523
+ if (
524
+ !pending ||
525
+ !stored ||
526
+ pending.callId !== callId ||
527
+ stored.callId !== pending.callId ||
528
+ stored.revision !== pending.revision ||
529
+ pending.decision !== undefined ||
530
+ this.abort.signal.aborted ||
531
+ this.metadata?.phase !== 'running'
532
+ )
533
+ throw conflict('approval_not_pending')
534
+ pending.decision = allowed
535
+ this.metadata.pending = null
536
+ await this.persist()
537
+ pending.resolve?.(pending.decision)
538
+ return Response.json({ ok: true })
539
+ })
540
+ }
541
+ throw missing()
542
+ } catch (error) {
543
+ const safe = publicError(error)
544
+ return Response.json(safe, { status: statusCode(safe) })
545
+ }
546
+ }
547
+ async alarm(): Promise<void> {
548
+ await this.ready
549
+ const record = this.metadata
550
+ if (record?.phase === 'accepted' && record.acceptedUntil !== null) {
551
+ if (Date.now() >= record.acceptedUntil) await this.cancelRun()
552
+ else
553
+ await this.serialize(async () => {
554
+ if (this.metadata?.phase === 'accepted' && this.metadata.acceptedUntil !== null)
555
+ await this.state.storage.setAlarm(this.metadata.acceptedUntil)
556
+ })
557
+ }
558
+ }
559
+ }
560
+ }