@tangle-network/agent-runtime 0.13.1 → 0.14.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/dist/index.d.ts +228 -1
- package/dist/index.js +293 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ interface BackendRetryPolicy {
|
|
|
59
59
|
jitter?: number;
|
|
60
60
|
/** Status codes that trigger a retry. Default: 408, 425, 429, 500, 502, 503, 504. */
|
|
61
61
|
retryStatuses?: ReadonlyArray<number>;
|
|
62
|
+
/**
|
|
63
|
+
* Per-attempt wall-clock deadline in ms. If a single fetch attempt does
|
|
64
|
+
* not return headers within this window the attempt is aborted and
|
|
65
|
+
* retried. Default 120000 (2 min). Without this a hung upstream blocks
|
|
66
|
+
* the attempt indefinitely — observed in production as a 15-minute
|
|
67
|
+
* `fetch failed` that burned an entire eval persona. Set to 0 to disable.
|
|
68
|
+
*/
|
|
69
|
+
requestTimeoutMs?: number;
|
|
62
70
|
}
|
|
63
71
|
declare function createOpenAICompatibleBackend<TInput extends AgentBackendInput = AgentBackendInput>(options: {
|
|
64
72
|
apiKey: string;
|
|
@@ -384,6 +392,225 @@ declare class DurableAwaitEventTimeoutError extends DurableRunError {
|
|
|
384
392
|
constructor(message: string);
|
|
385
393
|
}
|
|
386
394
|
|
|
395
|
+
/**
|
|
396
|
+
* `runDurableTurn` — a streaming, backend-agnostic, checkpoint+replay durable
|
|
397
|
+
* turn. The single reusable primitive every product's chat handler routes
|
|
398
|
+
* through, so per-product durability code drops to zero.
|
|
399
|
+
*
|
|
400
|
+
* A **turn** is one request→response unit: a producer yields a stream of
|
|
401
|
+
* events and, once drained, exposes the turn's final text. `runDurableTurn`
|
|
402
|
+
* wraps that with a `DurableRunStore`:
|
|
403
|
+
*
|
|
404
|
+
* - **Fresh run** — no completed step for this `(runId)`. The producer
|
|
405
|
+
* runs; its events forward live to the caller (streaming preserved)
|
|
406
|
+
* while final text accumulates; on drain the text is checkpointed.
|
|
407
|
+
*
|
|
408
|
+
* - **Replay** — a completed step already exists (the worker died after
|
|
409
|
+
* the turn finished but before the response reached the client, and the
|
|
410
|
+
* client retried the same turn). The cached text is emitted as a single
|
|
411
|
+
* synthetic event; the producer is never constructed — no LLM call, no
|
|
412
|
+
* double-billing.
|
|
413
|
+
*
|
|
414
|
+
* - **Mid-stream crash** — a turn that died *while streaming* leaves step 0
|
|
415
|
+
* in `running`/`failed`. There is no partial-stream checkpoint (the
|
|
416
|
+
* substrate checkpoints JSON values at step granularity), so the turn
|
|
417
|
+
* re-runs from the top. This is the honest durability ceiling: a
|
|
418
|
+
* *completed* turn is free to replay; an *interrupted* turn re-runs.
|
|
419
|
+
*
|
|
420
|
+
* Generic over the event type `TEvent` so a product can stream its own NDJSON
|
|
421
|
+
* shape or the runtime's `RuntimeStreamEvent` — `runDurableTurn` never
|
|
422
|
+
* inspects events, it only forwards them and reads `finalText()` after drain.
|
|
423
|
+
*
|
|
424
|
+
* Lease: a turn is a single step, fast enough that the heartbeat in
|
|
425
|
+
* `runDurable` is unnecessary — `runDurableTurn` claims the lease once via
|
|
426
|
+
* `startOrResume` and releases it on `endRun`. Concurrent workers on the same
|
|
427
|
+
* `runId` are rejected with `DurableRunLeaseHeldError` (the client retried
|
|
428
|
+
* before the first attempt finished); callers surface that as "turn already
|
|
429
|
+
* in flight."
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
/** The live side of a turn — what a fresh run produces. */
|
|
433
|
+
interface DurableTurnProducer<TEvent> {
|
|
434
|
+
/** The turn's event stream. Forwarded verbatim to the caller. */
|
|
435
|
+
stream: AsyncGenerator<TEvent, void, unknown>;
|
|
436
|
+
/** The turn's final assistant text. Read once, after `stream` drains. */
|
|
437
|
+
finalText(): string;
|
|
438
|
+
}
|
|
439
|
+
interface RunDurableTurnOptions<TEvent> {
|
|
440
|
+
store: DurableRunStore;
|
|
441
|
+
/** Stable per-turn run id. Convention: `chat:<threadId>:<turnIndex>`. The
|
|
442
|
+
* same id on a retry is what enables replay. */
|
|
443
|
+
runId: string;
|
|
444
|
+
manifest: DurableRunManifest;
|
|
445
|
+
/** Stable per-isolate worker id. Defaults to a fresh `deriveWorkerId()`
|
|
446
|
+
* per call when omitted — fine for single-attempt turns. */
|
|
447
|
+
workerId: string;
|
|
448
|
+
/** Lease window in ms. Default 60_000 — a turn rarely runs longer. */
|
|
449
|
+
leaseMs?: number;
|
|
450
|
+
/** Human-readable step label. Default `turn`. */
|
|
451
|
+
intent?: string;
|
|
452
|
+
/** Builds the live producer. Called exactly once, on a fresh run; never
|
|
453
|
+
* called on the replay path. */
|
|
454
|
+
produce: () => DurableTurnProducer<TEvent>;
|
|
455
|
+
/** Synthesizes the single event emitted on the replay path from the
|
|
456
|
+
* cached final text (e.g. a product's `{ type: 'result', data: {...} }`). */
|
|
457
|
+
replayEvent: (finalText: string) => TEvent;
|
|
458
|
+
/** Optional live accumulator. When the producer's `finalText()` is only
|
|
459
|
+
* valid after drain, this lets `runDurableTurn` also observe each event
|
|
460
|
+
* to build the text — return the running text or `undefined` to ignore
|
|
461
|
+
* an event. When omitted, `producer.finalText()` is the sole source. */
|
|
462
|
+
accumulate?: (event: TEvent, current: string) => string | undefined;
|
|
463
|
+
}
|
|
464
|
+
interface DurableTurnHandle<TEvent> {
|
|
465
|
+
/** Drop-in stream. Fresh runs forward producer events live; replays emit
|
|
466
|
+
* exactly one `replayEvent(cachedText)`. */
|
|
467
|
+
stream: AsyncGenerator<TEvent, void, unknown>;
|
|
468
|
+
/** The turn's final text. Valid after `stream` drains. */
|
|
469
|
+
finalText(): string;
|
|
470
|
+
/** True iff this turn replayed a cached result (no producer ran). Valid
|
|
471
|
+
* after `stream` drains. */
|
|
472
|
+
replayed(): boolean;
|
|
473
|
+
/** The durable `RunRecord` for this turn. Valid after `stream` drains. */
|
|
474
|
+
record(): RunRecord | undefined;
|
|
475
|
+
}
|
|
476
|
+
declare function runDurableTurn<TEvent>(options: RunDurableTurnOptions<TEvent>): DurableTurnHandle<TEvent>;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* `DurableChatTurnEngine` — the framework-neutral chat-turn orchestrator every
|
|
480
|
+
* product chat handler routes through. It owns the parts that were copy-pasted
|
|
481
|
+
* across legal / gtm / creative / tax: durable checkpointing, the NDJSON
|
|
482
|
+
* `StreamEvent` line protocol, the `session.run.*` lifecycle vocabulary, the
|
|
483
|
+
* runtime-run cost ledger, and trace flush. Everything genuinely
|
|
484
|
+
* product-specific is a hook the product supplies.
|
|
485
|
+
*
|
|
486
|
+
* What the engine owns:
|
|
487
|
+
* - durable turn (`runDurableTurn`): completed turns replay free, no re-bill
|
|
488
|
+
* - the `session.run.started` / `session.run.completed` / `session.run.failed`
|
|
489
|
+
* event envelope around the producer's events
|
|
490
|
+
* - NDJSON encoding into a `ReadableStream<Uint8Array>` (the body every
|
|
491
|
+
* product returns, React Router or Hono alike)
|
|
492
|
+
* - calling the product's persist / post-process hooks in the right order,
|
|
493
|
+
* after the stream drains, with the assembled final text
|
|
494
|
+
* - never throwing into the HTTP layer — a producer failure becomes an
|
|
495
|
+
* `error` + `session.run.failed` event pair, the stream still closes
|
|
496
|
+
*
|
|
497
|
+
* What the product supplies (`ChatTurnHooks`):
|
|
498
|
+
* - `produce` — build the backend stream for this turn (sandbox / router
|
|
499
|
+
* / tcloud / runtime — the engine does not care which)
|
|
500
|
+
* - `persistAssistantMessage` — write the assistant turn to the product DB
|
|
501
|
+
* - `onTurnComplete` (optional) — post-process (proposals, citations, …)
|
|
502
|
+
* - `onEvent` (optional) — per-event side-channel (e.g. DO broadcast)
|
|
503
|
+
* - `transformFinalText` (optional) — pre-persist transform (e.g. PII redact)
|
|
504
|
+
*
|
|
505
|
+
* Framework neutrality: the engine takes already-resolved values
|
|
506
|
+
* (`userId`, identity tuple, parsed message, a `DurableRunStore`, a
|
|
507
|
+
* `waitUntil`), never a `Request` or a `Context`. The product's thin route
|
|
508
|
+
* adapter does auth + parse + access-control, then calls `engine.runTurn(...)`
|
|
509
|
+
* and returns `result.body` as its platform `Response`.
|
|
510
|
+
*/
|
|
511
|
+
|
|
512
|
+
/** The NDJSON line protocol every product chat client already speaks. */
|
|
513
|
+
interface ChatStreamEvent {
|
|
514
|
+
type: string;
|
|
515
|
+
data?: Record<string, unknown>;
|
|
516
|
+
}
|
|
517
|
+
/** Identity of a chat turn. `tenantId` is the workspace id for workspace-
|
|
518
|
+
* scoped products and the user id for session-scoped products. */
|
|
519
|
+
interface ChatTurnIdentity {
|
|
520
|
+
tenantId: string;
|
|
521
|
+
/** Thread / session id — the durable run is keyed on this + `turnIndex`. */
|
|
522
|
+
sessionId: string;
|
|
523
|
+
userId: string;
|
|
524
|
+
/** Monotonic 0-based turn index within the session. */
|
|
525
|
+
turnIndex: number;
|
|
526
|
+
}
|
|
527
|
+
interface ChatTurnHooks {
|
|
528
|
+
/**
|
|
529
|
+
* Build the backend stream for this turn. The engine never inspects which
|
|
530
|
+
* backend this is — sandbox container, tcloud router, direct runtime, a
|
|
531
|
+
* test double — it only forwards the events and reads `finalText()`.
|
|
532
|
+
*/
|
|
533
|
+
produce(): DurableTurnProducer<ChatStreamEvent>;
|
|
534
|
+
/**
|
|
535
|
+
* Persist the completed assistant message to the product's own store.
|
|
536
|
+
* Called once, after the stream drains, on a fresh (non-replay) run.
|
|
537
|
+
* Receives the assembled (and `transformFinalText`-transformed) text.
|
|
538
|
+
*/
|
|
539
|
+
persistAssistantMessage(input: {
|
|
540
|
+
identity: ChatTurnIdentity;
|
|
541
|
+
finalText: string;
|
|
542
|
+
record: RunRecord | undefined;
|
|
543
|
+
}): Promise<void>;
|
|
544
|
+
/**
|
|
545
|
+
* Optional post-processing after persistence — proposal extraction,
|
|
546
|
+
* citation validation, credit metering, etc. Product policy; the engine
|
|
547
|
+
* has no shared logic here. Errors are swallowed + logged (post-process
|
|
548
|
+
* must never fail the turn that already streamed successfully).
|
|
549
|
+
*/
|
|
550
|
+
onTurnComplete?(input: {
|
|
551
|
+
identity: ChatTurnIdentity;
|
|
552
|
+
finalText: string;
|
|
553
|
+
}): Promise<void>;
|
|
554
|
+
/**
|
|
555
|
+
* Optional per-event side channel (e.g. Durable Object broadcast). Runs
|
|
556
|
+
* for every event the engine emits, lifecycle envelope included. Errors
|
|
557
|
+
* are swallowed — a broadcast failure must not break the chat stream.
|
|
558
|
+
*/
|
|
559
|
+
onEvent?(event: ChatStreamEvent): void | Promise<void>;
|
|
560
|
+
/**
|
|
561
|
+
* Optional pre-persist transform of the final text (e.g. PII redaction).
|
|
562
|
+
* Affects only what is persisted; the live stream is never altered.
|
|
563
|
+
*/
|
|
564
|
+
transformFinalText?(text: string): string | Promise<string>;
|
|
565
|
+
/**
|
|
566
|
+
* Optional trace flush — resolves when OTLP export completes. The engine
|
|
567
|
+
* hands it to `waitUntil` so the worker isolate stays alive for the POST.
|
|
568
|
+
*/
|
|
569
|
+
traceFlush?(): Promise<void>;
|
|
570
|
+
}
|
|
571
|
+
interface RunChatTurnInput {
|
|
572
|
+
identity: ChatTurnIdentity;
|
|
573
|
+
/** The user's message for this turn. Hashed into the durable run identity. */
|
|
574
|
+
userMessage: string;
|
|
575
|
+
/** Product id for telemetry / the durable manifest (`legal-agent`, …). */
|
|
576
|
+
projectId: string;
|
|
577
|
+
/** Domain tag for the task spec (`legal`, `gtm`, …). */
|
|
578
|
+
domain: string;
|
|
579
|
+
/** Model id, when known — recorded on the manifest. */
|
|
580
|
+
model?: string;
|
|
581
|
+
store: DurableRunStore;
|
|
582
|
+
hooks: ChatTurnHooks;
|
|
583
|
+
/** Worker liveness hook (`ctx.waitUntil` / `executionCtx.waitUntil`). When
|
|
584
|
+
* omitted, trace flush is awaited inline before the stream closes. */
|
|
585
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
586
|
+
/** Stable per-isolate worker id. Defaults to a fresh `deriveWorkerId()`. */
|
|
587
|
+
workerId?: string;
|
|
588
|
+
/** Lease window in ms. Default 60_000. */
|
|
589
|
+
leaseMs?: number;
|
|
590
|
+
/** Optional structured logger for swallowed hook errors. */
|
|
591
|
+
log?: (message: string, meta?: Record<string, unknown>) => void;
|
|
592
|
+
}
|
|
593
|
+
interface ChatTurnResult {
|
|
594
|
+
/** NDJSON body — return this as the platform `Response` body. */
|
|
595
|
+
body: ReadableStream<Uint8Array>;
|
|
596
|
+
/** Content type for the response. */
|
|
597
|
+
contentType: 'application/x-ndjson';
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The engine. One instance is stateless and reusable across requests — all
|
|
601
|
+
* per-turn state lives in `runTurn`'s closure.
|
|
602
|
+
*/
|
|
603
|
+
declare class DurableChatTurnEngine {
|
|
604
|
+
/**
|
|
605
|
+
* Run one durable chat turn. Returns immediately with a `ReadableStream`
|
|
606
|
+
* body; the turn executes as the body is pulled. Never rejects — backend
|
|
607
|
+
* failures surface as `error` + `session.run.failed` events.
|
|
608
|
+
*/
|
|
609
|
+
runTurn(input: RunChatTurnInput): ChatTurnResult;
|
|
610
|
+
}
|
|
611
|
+
/** Convenience singleton — the engine is stateless, one instance is enough. */
|
|
612
|
+
declare const durableChatTurnEngine: DurableChatTurnEngine;
|
|
613
|
+
|
|
387
614
|
/**
|
|
388
615
|
* D1DurableRunStore — the production path for Cloudflare Workers. Backed by
|
|
389
616
|
* a D1 (SQLite-compatible) database via the binding the worker already holds.
|
|
@@ -1424,4 +1651,4 @@ declare function createTraceBridge(options: TraceBridgeOptions): TraceBridge;
|
|
|
1424
1651
|
*/
|
|
1425
1652
|
declare function toAgentEvalTrace(event: RuntimeStreamEvent, options: TraceBridgeOptions): TraceEvent | undefined;
|
|
1426
1653
|
|
|
1427
|
-
export { AgentBackendContext, AgentBackendInput, AgentExecutionBackend, AgentRuntimeEvent, AgentTaskRunResult, AgentTaskRunSummary, AgentTaskSpec, AgentTaskStatus, type BackendRetryPolicy, BackendTransportError, ChatTurnError, type ChatTurnMessage, type ChatTurnOverlay, type ChatTurnSandbox, type ClassifyIntentOptions, type ClassifyIntentResult, type ConformanceIssue, type ConformanceOptions, type ConformanceResult, type D1DatabaseLike, D1DurableRunStore, type D1PreparedStatementLike, DURABLE_SCHEMA_SQL, DURABLE_SCHEMA_VERSION, DurableAwaitEventTimeoutError, type DurableContext, DurableRunDivergenceError, DurableRunError, DurableRunInputMismatchError, DurableRunLeaseHeldError, type DurableRunManifest, type DurableRunStore, type EventRecord, FileSystemDurableRunStore, InMemoryDurableRunStore, InMemoryRuntimeSessionStore, KnowledgeReadinessDecision, RunAgentTaskOptions, RunAgentTaskStreamOptions, type RunChatTurnOptions, type RunDurableInput, type RunDurableResult, type RunOnWorkflowStepInput, type RunOutcome, type RunStatus, type RuntimeEventCollector, type RuntimeRunCompleteInput, type RuntimeRunCost, type RuntimeRunHandle, type RuntimeRunOptions, type RuntimeRunPersistenceAdapter, type RuntimeRunRow, RuntimeRunStateError, type RuntimeRunStatus, RuntimeSession, RuntimeSessionStore, RuntimeStreamEvent, type RuntimeStreamEventCollector, type RuntimeStreamEventSink, type RuntimeStreamEventSummary, type RuntimeTelemetryOptions, type SanitizedKnowledgeReadinessReport, type SanitizedKnowledgeRequirement, type ServerSentEventOptions, SessionMismatchError, type StepError, type StepKind, type StepRecord, type StepStatus, type SubagentMatcher, type TraceBridge, type TraceBridgeOptions, type WorkflowStepConfig, type WorkflowStepLike, assertProfileConformance, canonicalHash, canonicalJson, classifyIntent, composeTurnProfile, createIterableBackend, createOpenAICompatibleBackend, createRuntimeEventCollector, createRuntimeStreamEventCollector, createSandboxPromptBackend, createTraceBridge, decideKnowledgeReadiness, deriveWorkerId, encodeServerSentEvent, manifestHash, readinessServerSentEvent, runAgentTask, runAgentTaskStream, runChatTurn, runDurable, runOnWorkflowStep, runtimeStreamServerSentEvent, sandboxAsChatTurnTarget, sanitizeAgentRuntimeEvent, sanitizeKnowledgeReadinessReport, sanitizeRuntimeStreamEvent, startRuntimeRun, stepId, summarizeAgentTaskRun, toAgentEvalTrace };
|
|
1654
|
+
export { AgentBackendContext, AgentBackendInput, AgentExecutionBackend, AgentRuntimeEvent, AgentTaskRunResult, AgentTaskRunSummary, AgentTaskSpec, AgentTaskStatus, type BackendRetryPolicy, BackendTransportError, type ChatStreamEvent, ChatTurnError, type ChatTurnHooks, type ChatTurnIdentity, type ChatTurnMessage, type ChatTurnOverlay, type ChatTurnResult, type ChatTurnSandbox, type ClassifyIntentOptions, type ClassifyIntentResult, type ConformanceIssue, type ConformanceOptions, type ConformanceResult, type D1DatabaseLike, D1DurableRunStore, type D1PreparedStatementLike, DURABLE_SCHEMA_SQL, DURABLE_SCHEMA_VERSION, DurableAwaitEventTimeoutError, DurableChatTurnEngine, type DurableContext, DurableRunDivergenceError, DurableRunError, DurableRunInputMismatchError, DurableRunLeaseHeldError, type DurableRunManifest, type DurableRunStore, type DurableTurnHandle, type DurableTurnProducer, type EventRecord, FileSystemDurableRunStore, InMemoryDurableRunStore, InMemoryRuntimeSessionStore, KnowledgeReadinessDecision, RunAgentTaskOptions, RunAgentTaskStreamOptions, type RunChatTurnInput, type RunChatTurnOptions, type RunDurableInput, type RunDurableResult, type RunDurableTurnOptions, type RunOnWorkflowStepInput, type RunOutcome, type RunStatus, type RuntimeEventCollector, type RuntimeRunCompleteInput, type RuntimeRunCost, type RuntimeRunHandle, type RuntimeRunOptions, type RuntimeRunPersistenceAdapter, type RuntimeRunRow, RuntimeRunStateError, type RuntimeRunStatus, RuntimeSession, RuntimeSessionStore, RuntimeStreamEvent, type RuntimeStreamEventCollector, type RuntimeStreamEventSink, type RuntimeStreamEventSummary, type RuntimeTelemetryOptions, type SanitizedKnowledgeReadinessReport, type SanitizedKnowledgeRequirement, type ServerSentEventOptions, SessionMismatchError, type StepError, type StepKind, type StepRecord, type StepStatus, type SubagentMatcher, type TraceBridge, type TraceBridgeOptions, type WorkflowStepConfig, type WorkflowStepLike, assertProfileConformance, canonicalHash, canonicalJson, classifyIntent, composeTurnProfile, createIterableBackend, createOpenAICompatibleBackend, createRuntimeEventCollector, createRuntimeStreamEventCollector, createSandboxPromptBackend, createTraceBridge, decideKnowledgeReadiness, deriveWorkerId, durableChatTurnEngine, encodeServerSentEvent, manifestHash, readinessServerSentEvent, runAgentTask, runAgentTaskStream, runChatTurn, runDurable, runDurableTurn, runOnWorkflowStep, runtimeStreamServerSentEvent, sandboxAsChatTurnTarget, sanitizeAgentRuntimeEvent, sanitizeKnowledgeReadinessReport, sanitizeRuntimeStreamEvent, startRuntimeRun, stepId, summarizeAgentTaskRun, toAgentEvalTrace };
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,32 @@ function pickRetryDelayMs(attempt, policy) {
|
|
|
113
113
|
const jitter = capped * policy.jitter * (Math.random() * 2 - 1);
|
|
114
114
|
return Math.max(0, Math.round(capped + jitter));
|
|
115
115
|
}
|
|
116
|
+
function withTimeout(callerSignal, timeoutMs) {
|
|
117
|
+
if (timeoutMs <= 0) {
|
|
118
|
+
return { signal: callerSignal ?? new AbortController().signal, dispose: () => void 0 };
|
|
119
|
+
}
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timer = setTimeout(
|
|
122
|
+
() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)),
|
|
123
|
+
timeoutMs
|
|
124
|
+
);
|
|
125
|
+
if (typeof timer.unref === "function") {
|
|
126
|
+
;
|
|
127
|
+
timer.unref();
|
|
128
|
+
}
|
|
129
|
+
const onCallerAbort = () => controller.abort(callerSignal?.reason ?? new Error("aborted"));
|
|
130
|
+
if (callerSignal) {
|
|
131
|
+
if (callerSignal.aborted) onCallerAbort();
|
|
132
|
+
else callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
dispose: () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
116
142
|
function sleep(ms, signal) {
|
|
117
143
|
return new Promise((resolve, reject) => {
|
|
118
144
|
if (signal?.aborted) {
|
|
@@ -138,7 +164,8 @@ function createOpenAICompatibleBackend(options) {
|
|
|
138
164
|
initialBackoffMs: options.retry?.initialBackoffMs ?? 1e3,
|
|
139
165
|
maxBackoffMs: options.retry?.maxBackoffMs ?? 3e4,
|
|
140
166
|
jitter: options.retry?.jitter ?? 0.25,
|
|
141
|
-
retryStatuses: options.retry?.retryStatuses ?? DEFAULT_RETRY_STATUSES
|
|
167
|
+
retryStatuses: options.retry?.retryStatuses ?? DEFAULT_RETRY_STATUSES,
|
|
168
|
+
requestTimeoutMs: options.retry?.requestTimeoutMs ?? 12e4
|
|
142
169
|
};
|
|
143
170
|
return {
|
|
144
171
|
kind,
|
|
@@ -146,24 +173,40 @@ function createOpenAICompatibleBackend(options) {
|
|
|
146
173
|
return newRuntimeSession(kind, context.requestedSessionId);
|
|
147
174
|
},
|
|
148
175
|
async *stream(input, context) {
|
|
176
|
+
const url = `${options.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
177
|
+
const requestBody = JSON.stringify({
|
|
178
|
+
model: options.model,
|
|
179
|
+
stream: true,
|
|
180
|
+
messages: input.messages ?? [
|
|
181
|
+
{ role: "user", content: input.message ?? context.task.intent }
|
|
182
|
+
]
|
|
183
|
+
});
|
|
149
184
|
let response;
|
|
150
185
|
let lastStatus = 0;
|
|
186
|
+
let lastThrown;
|
|
151
187
|
for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
lastThrown = void 0;
|
|
189
|
+
const attemptSignal = withTimeout(context.signal, retryPolicy.requestTimeoutMs);
|
|
190
|
+
try {
|
|
191
|
+
response = await fetcher(url, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
195
|
+
"Content-Type": "application/json"
|
|
196
|
+
},
|
|
197
|
+
body: requestBody,
|
|
198
|
+
signal: attemptSignal.signal
|
|
199
|
+
});
|
|
200
|
+
} catch (err) {
|
|
201
|
+
attemptSignal.dispose();
|
|
202
|
+
if (context.signal?.aborted) throw err;
|
|
203
|
+
lastThrown = err;
|
|
204
|
+
response = void 0;
|
|
205
|
+
if (attempt === retryPolicy.maxAttempts) break;
|
|
206
|
+
await sleep(pickRetryDelayMs(attempt, retryPolicy), context.signal);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
attemptSignal.dispose();
|
|
167
210
|
if (response.ok) break;
|
|
168
211
|
lastStatus = response.status;
|
|
169
212
|
if (!retryPolicy.retryStatuses.includes(response.status)) break;
|
|
@@ -175,7 +218,15 @@ function createOpenAICompatibleBackend(options) {
|
|
|
175
218
|
const delayMs = pickRetryDelayMs(attempt, retryPolicy);
|
|
176
219
|
await sleep(delayMs, context.signal);
|
|
177
220
|
}
|
|
178
|
-
if (!response
|
|
221
|
+
if (!response) {
|
|
222
|
+
const reason = lastThrown instanceof Error ? lastThrown.message : String(lastThrown);
|
|
223
|
+
throw new BackendTransportError(
|
|
224
|
+
kind,
|
|
225
|
+
`chat backend unreachable after ${retryPolicy.maxAttempts} attempts: ${reason}`,
|
|
226
|
+
{ status: 0 }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (!response.ok) {
|
|
179
230
|
throw new BackendTransportError(kind, `chat backend returned ${lastStatus || "unknown"}`, {
|
|
180
231
|
status: lastStatus || 0
|
|
181
232
|
});
|
|
@@ -497,6 +548,228 @@ function deriveWorkerId() {
|
|
|
497
548
|
return `${host}:${pid}:${rand}:${counter}`;
|
|
498
549
|
}
|
|
499
550
|
|
|
551
|
+
// src/durable/turn.ts
|
|
552
|
+
var STEP_INDEX = 0;
|
|
553
|
+
function runDurableTurn(options) {
|
|
554
|
+
const { store, runId, manifest, workerId } = options;
|
|
555
|
+
const leaseMs = options.leaseMs ?? 6e4;
|
|
556
|
+
const intent = options.intent ?? "turn";
|
|
557
|
+
const inputHash = canonicalHash(manifest.input);
|
|
558
|
+
let accumulated = "";
|
|
559
|
+
let didReplay = false;
|
|
560
|
+
let finalRecord;
|
|
561
|
+
async function* stream() {
|
|
562
|
+
const { completedSteps } = await store.startOrResume({
|
|
563
|
+
runId,
|
|
564
|
+
manifest,
|
|
565
|
+
workerId,
|
|
566
|
+
leaseMs
|
|
567
|
+
});
|
|
568
|
+
const prior = completedSteps.find((s) => s.stepIndex === STEP_INDEX);
|
|
569
|
+
if (prior && prior.status === "completed") {
|
|
570
|
+
didReplay = true;
|
|
571
|
+
const cached = prior.result;
|
|
572
|
+
accumulated = cached?.finalText ?? "";
|
|
573
|
+
yield options.replayEvent(accumulated);
|
|
574
|
+
finalRecord = await store.endRun({ runId, workerId, status: "completed" });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
await store.beginStep({
|
|
578
|
+
runId,
|
|
579
|
+
stepIndex: STEP_INDEX,
|
|
580
|
+
intent,
|
|
581
|
+
kind: "llm",
|
|
582
|
+
inputHash
|
|
583
|
+
});
|
|
584
|
+
try {
|
|
585
|
+
const producer = options.produce();
|
|
586
|
+
for await (const event of producer.stream) {
|
|
587
|
+
if (options.accumulate) {
|
|
588
|
+
const next = options.accumulate(event, accumulated);
|
|
589
|
+
if (typeof next === "string") accumulated = next;
|
|
590
|
+
}
|
|
591
|
+
yield event;
|
|
592
|
+
}
|
|
593
|
+
const producerText = producer.finalText();
|
|
594
|
+
if (producerText) accumulated = producerText;
|
|
595
|
+
await store.completeStep({
|
|
596
|
+
runId,
|
|
597
|
+
stepIndex: STEP_INDEX,
|
|
598
|
+
result: { finalText: accumulated }
|
|
599
|
+
});
|
|
600
|
+
finalRecord = await store.endRun({
|
|
601
|
+
runId,
|
|
602
|
+
workerId,
|
|
603
|
+
status: "completed",
|
|
604
|
+
outcome: { notes: intent, metadata: { chars: accumulated.length } }
|
|
605
|
+
});
|
|
606
|
+
} catch (err) {
|
|
607
|
+
await store.failStep({
|
|
608
|
+
runId,
|
|
609
|
+
stepIndex: STEP_INDEX,
|
|
610
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
611
|
+
});
|
|
612
|
+
finalRecord = await store.endRun({ runId, workerId, status: "failed" });
|
|
613
|
+
throw err;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
stream: stream(),
|
|
618
|
+
finalText: () => accumulated,
|
|
619
|
+
replayed: () => didReplay,
|
|
620
|
+
record: () => finalRecord
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/durable/chat-engine.ts
|
|
625
|
+
var encoder = new TextEncoder();
|
|
626
|
+
function encodeLine(event) {
|
|
627
|
+
return encoder.encode(`${JSON.stringify(event)}
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
var DurableChatTurnEngine = class {
|
|
631
|
+
/**
|
|
632
|
+
* Run one durable chat turn. Returns immediately with a `ReadableStream`
|
|
633
|
+
* body; the turn executes as the body is pulled. Never rejects — backend
|
|
634
|
+
* failures surface as `error` + `session.run.failed` events.
|
|
635
|
+
*/
|
|
636
|
+
runTurn(input) {
|
|
637
|
+
const workerId = input.workerId ?? deriveWorkerId();
|
|
638
|
+
const log = input.log ?? (() => void 0);
|
|
639
|
+
const { identity } = input;
|
|
640
|
+
const runId = `chat:${identity.sessionId}:${identity.turnIndex}`;
|
|
641
|
+
const manifest = {
|
|
642
|
+
projectId: input.projectId,
|
|
643
|
+
scenarioId: identity.sessionId,
|
|
644
|
+
task: {
|
|
645
|
+
id: `${input.projectId}:chat:${identity.sessionId}:${identity.turnIndex}`,
|
|
646
|
+
intent: `Run a ${input.domain} chat turn with workspace context.`,
|
|
647
|
+
domain: input.domain,
|
|
648
|
+
requiredKnowledge: [],
|
|
649
|
+
metadata: {
|
|
650
|
+
tenantId: identity.tenantId,
|
|
651
|
+
sessionId: identity.sessionId,
|
|
652
|
+
turnIndex: identity.turnIndex
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
input: {
|
|
656
|
+
userMessage: input.userMessage,
|
|
657
|
+
model: input.model ?? null
|
|
658
|
+
},
|
|
659
|
+
tags: {
|
|
660
|
+
session_id: identity.sessionId,
|
|
661
|
+
tenant_id: identity.tenantId
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
const body = new ReadableStream({
|
|
665
|
+
start: async (controller) => {
|
|
666
|
+
const emit2 = async (event) => {
|
|
667
|
+
controller.enqueue(encodeLine(event));
|
|
668
|
+
if (input.hooks.onEvent) {
|
|
669
|
+
try {
|
|
670
|
+
await input.hooks.onEvent(event);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
log("[chat-engine] onEvent hook threw", {
|
|
673
|
+
error: err instanceof Error ? err.message : String(err)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
let turnFailed = false;
|
|
679
|
+
try {
|
|
680
|
+
await emit2({
|
|
681
|
+
type: "session.run.started",
|
|
682
|
+
data: {
|
|
683
|
+
sessionId: identity.sessionId,
|
|
684
|
+
tenantId: identity.tenantId,
|
|
685
|
+
turnIndex: identity.turnIndex
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
const turn = runDurableTurn({
|
|
689
|
+
store: input.store,
|
|
690
|
+
runId,
|
|
691
|
+
manifest,
|
|
692
|
+
workerId,
|
|
693
|
+
leaseMs: input.leaseMs,
|
|
694
|
+
intent: `chat:turn-${identity.turnIndex}`,
|
|
695
|
+
produce: input.hooks.produce,
|
|
696
|
+
replayEvent: (finalText2) => ({ type: "result", data: { finalText: finalText2 } }),
|
|
697
|
+
accumulate: (event, current) => {
|
|
698
|
+
if (event.type === "message.part.updated") {
|
|
699
|
+
const data = event.data ?? {};
|
|
700
|
+
const delta = typeof data.delta === "string" ? data.delta : "";
|
|
701
|
+
const part = data.part;
|
|
702
|
+
if (delta) return current + delta;
|
|
703
|
+
if (part?.type === "text" && typeof part.text === "string") return part.text;
|
|
704
|
+
return void 0;
|
|
705
|
+
}
|
|
706
|
+
if (event.type === "result") {
|
|
707
|
+
const data = event.data ?? {};
|
|
708
|
+
if (typeof data.finalText === "string") return data.finalText;
|
|
709
|
+
}
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
for await (const event of turn.stream) {
|
|
714
|
+
await emit2(event);
|
|
715
|
+
}
|
|
716
|
+
const rawFinal = turn.finalText();
|
|
717
|
+
const finalText = input.hooks.transformFinalText ? await input.hooks.transformFinalText(rawFinal) : rawFinal;
|
|
718
|
+
if (!turn.replayed()) {
|
|
719
|
+
try {
|
|
720
|
+
await input.hooks.persistAssistantMessage({
|
|
721
|
+
identity,
|
|
722
|
+
finalText,
|
|
723
|
+
record: turn.record()
|
|
724
|
+
});
|
|
725
|
+
} catch (err) {
|
|
726
|
+
log("[chat-engine] persistAssistantMessage threw", {
|
|
727
|
+
error: err instanceof Error ? err.message : String(err)
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (input.hooks.onTurnComplete) {
|
|
731
|
+
try {
|
|
732
|
+
await input.hooks.onTurnComplete({ identity, finalText });
|
|
733
|
+
} catch (err) {
|
|
734
|
+
log("[chat-engine] onTurnComplete threw", {
|
|
735
|
+
error: err instanceof Error ? err.message : String(err)
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
await emit2({
|
|
741
|
+
type: "session.run.completed",
|
|
742
|
+
data: { sessionId: identity.sessionId, replayed: turn.replayed() }
|
|
743
|
+
});
|
|
744
|
+
} catch (err) {
|
|
745
|
+
turnFailed = true;
|
|
746
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
747
|
+
log("[chat-engine] turn failed", { error: message });
|
|
748
|
+
await emit2({ type: "error", data: { message } });
|
|
749
|
+
await emit2({
|
|
750
|
+
type: "session.run.failed",
|
|
751
|
+
data: { sessionId: identity.sessionId, message }
|
|
752
|
+
});
|
|
753
|
+
} finally {
|
|
754
|
+
if (input.hooks.traceFlush) {
|
|
755
|
+
const flush = input.hooks.traceFlush().catch(
|
|
756
|
+
(err) => log("[chat-engine] traceFlush threw", {
|
|
757
|
+
error: err instanceof Error ? err.message : String(err)
|
|
758
|
+
})
|
|
759
|
+
);
|
|
760
|
+
if (input.waitUntil) input.waitUntil(flush);
|
|
761
|
+
else await flush;
|
|
762
|
+
}
|
|
763
|
+
controller.close();
|
|
764
|
+
void turnFailed;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
return { body, contentType: "application/x-ndjson" };
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
var durableChatTurnEngine = new DurableChatTurnEngine();
|
|
772
|
+
|
|
500
773
|
// src/durable/types.ts
|
|
501
774
|
var DurableRunError = class extends Error {
|
|
502
775
|
constructor(message, code) {
|
|
@@ -2865,6 +3138,7 @@ export {
|
|
|
2865
3138
|
DURABLE_SCHEMA_SQL,
|
|
2866
3139
|
DURABLE_SCHEMA_VERSION,
|
|
2867
3140
|
DurableAwaitEventTimeoutError,
|
|
3141
|
+
DurableChatTurnEngine,
|
|
2868
3142
|
DurableRunDivergenceError,
|
|
2869
3143
|
DurableRunError,
|
|
2870
3144
|
DurableRunInputMismatchError,
|
|
@@ -2892,6 +3166,7 @@ export {
|
|
|
2892
3166
|
createTraceBridge,
|
|
2893
3167
|
decideKnowledgeReadiness,
|
|
2894
3168
|
deriveWorkerId,
|
|
3169
|
+
durableChatTurnEngine,
|
|
2895
3170
|
encodeServerSentEvent,
|
|
2896
3171
|
manifestHash,
|
|
2897
3172
|
readinessServerSentEvent,
|
|
@@ -2899,6 +3174,7 @@ export {
|
|
|
2899
3174
|
runAgentTaskStream,
|
|
2900
3175
|
runChatTurn,
|
|
2901
3176
|
runDurable,
|
|
3177
|
+
runDurableTurn,
|
|
2902
3178
|
runOnWorkflowStep,
|
|
2903
3179
|
runtimeStreamServerSentEvent,
|
|
2904
3180
|
sandboxAsChatTurnTarget,
|