deepline 0.1.198 → 0.1.200

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.
Files changed (72) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +5 -1
  2. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +51 -17
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
  7. package/dist/bundling-sources/sdk/src/http.ts +5 -2
  8. package/dist/bundling-sources/sdk/src/play.ts +1 -1
  9. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
  10. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  11. package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
  12. package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
  14. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
  15. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
  17. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
  18. package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
  22. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
  25. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
  30. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
  31. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
  34. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +2 -3
  38. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +459 -103
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
  52. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
  53. package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
  55. package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
  58. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
  59. package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
  60. package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
  61. package/dist/cli/index.js +441 -101
  62. package/dist/cli/index.mjs +441 -101
  63. package/dist/index.d.mts +1 -1
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.js +8 -8
  66. package/dist/index.mjs +8 -8
  67. package/dist/plays/bundle-play-file.mjs +15 -20
  68. package/package.json +1 -1
  69. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
  70. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
  71. package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
  72. package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
@@ -13,6 +13,9 @@ export interface MapExecutionFrame {
13
13
  totalRows: number;
14
14
  completedRowKeys: string[];
15
15
  pendingRowKeys: string[];
16
+ completedRowsCount?: number;
17
+ pendingRowsCount?: number;
18
+ failedRowsCount?: number;
16
19
  activeBoundaryId?: string | null;
17
20
  startedAt: number;
18
21
  updatedAt: number;
@@ -1,4 +1,4 @@
1
- // Core types for PlayContext — cloud-only execution via Temporal
1
+ // Core types for PlayContext.
2
2
  import type { PlayBundleArtifact } from '../plays/artifact-types';
3
3
  import type { PlayRunContractSnapshot } from '../plays/contracts';
4
4
  import type { PlayStructuredDefinition } from '../plays/definition';
@@ -13,6 +13,7 @@ import type {
13
13
  RateStateBackend,
14
14
  } from './governor/rate-state-backend';
15
15
  import type { GovernanceSnapshot } from './governor/governor';
16
+ import type { RuntimeMapMemoryLimits } from './map-memory-limits';
16
17
  import type { AnyBatchOperationStrategy } from './batching-types';
17
18
  import type { ToolResultMetadataInput } from './tool-result';
18
19
  import type { PreviousCell } from './cell-staleness';
@@ -30,6 +31,7 @@ export interface ToolCallRequest {
30
31
  receiptKey?: string | null;
31
32
  executionAuthScopeDigest?: string | null;
32
33
  receiptLeaseId?: string | null;
34
+ receiptLeaseExpiresAt?: string | null;
33
35
  force?: boolean;
34
36
  forceFailedRefresh?: boolean;
35
37
  rowId: number;
@@ -50,7 +52,7 @@ export interface ToolBatchResult {
50
52
 
51
53
  export interface RuntimeStepReceipt {
52
54
  key: string;
53
- status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
55
+ status: 'queued' | 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
54
56
  output?: unknown;
55
57
  error?: string;
56
58
  failureKind?: WorkReceiptFailureKind | null;
@@ -76,6 +78,13 @@ export interface ClaimRuntimeStepReceiptInput {
76
78
  forceFailedRefresh?: boolean;
77
79
  }
78
80
 
81
+ export interface MarkRuntimeStepReceiptRunningInput {
82
+ key: string;
83
+ runId: string;
84
+ runAttempt?: number | null;
85
+ leaseId?: string | null;
86
+ }
87
+
79
88
  export interface CompleteRuntimeStepReceiptInput {
80
89
  key: string;
81
90
  runId: string;
@@ -122,6 +131,24 @@ export interface ClaimRuntimeStepReceiptsInput {
122
131
  forceFailedRefresh?: boolean;
123
132
  }
124
133
 
134
+ export interface MarkRuntimeStepReceiptsRunningInput {
135
+ receipts: Array<{
136
+ key: string;
137
+ runId: string;
138
+ runAttempt?: number | null;
139
+ leaseId?: string | null;
140
+ }>;
141
+ }
142
+
143
+ export interface MarkRuntimeStepReceiptsQueuedInput {
144
+ receipts: Array<{
145
+ key: string;
146
+ runId: string;
147
+ runAttempt?: number | null;
148
+ leaseId?: string | null;
149
+ }>;
150
+ }
151
+
125
152
  export interface CompleteRuntimeStepReceiptsInput {
126
153
  receipts: Array<{
127
154
  key: string;
@@ -332,6 +359,9 @@ export interface MapExecutionFrame {
332
359
  totalRows: number;
333
360
  completedRowKeys: string[];
334
361
  pendingRowKeys: string[];
362
+ completedRowsCount?: number;
363
+ pendingRowsCount?: number;
364
+ failedRowsCount?: number;
335
365
  activeBoundaryId?: string | null;
336
366
  startedAt: number;
337
367
  updatedAt: number;
@@ -452,11 +482,18 @@ export interface ContextOptions {
452
482
  artifactHash?: string | null;
453
483
  convexUrl?: string;
454
484
  runtimeSchedulerSchema?: string | null;
485
+ /**
486
+ * Execution profile for scheduler-backed `ctx.runPlay` child launches. Threaded
487
+ * from the parent launch so a child inherits the parent's scheduler profile
488
+ * (e.g. `absurd`) on the child `/api/v2/plays/run` submission. Absent defaults
489
+ * to the `absurd` profile.
490
+ */
491
+ childRunProfile?: string | null;
455
492
  staticPipeline?: PlayStaticPipeline | null;
456
493
  workflowId?: string;
457
494
  sessionId?: string;
458
495
  verbose?: boolean;
459
- /** Heartbeat checkpoint from a previous Temporal activity attempt. */
496
+ /** Checkpoint from a previous scheduler attempt. */
460
497
  checkpoint?: PlayCheckpoint;
461
498
  /** Enables durable boundary replay such as ctx.sleep() resumptions. */
462
499
  durableBoundaries?: boolean;
@@ -470,7 +507,7 @@ export interface ContextOptions {
470
507
  forceToolRefresh?: boolean;
471
508
  forceFailedToolRefresh?: boolean;
472
509
  };
473
- /** Called after each durable batch completes for Temporal heartbeating. */
510
+ /** Called after each durable batch completes for scheduler checkpointing. */
474
511
  onBatchComplete?: (checkpoint: PlayCheckpoint) => void;
475
512
  /** Called when the runtime emits a new execution log line. */
476
513
  onLog?: (line: string) => void;
@@ -497,6 +534,7 @@ export interface ContextOptions {
497
534
  executorToken?: string;
498
535
  staticPipeline?: PlayStaticPipeline | null;
499
536
  forceRefresh?: boolean;
537
+ inputOffset?: number;
500
538
  },
501
539
  ) => Promise<MapStartResult>;
502
540
  /**
@@ -528,6 +566,18 @@ export interface ContextOptions {
528
566
  * context without a shared rate-state backend is a configuration error.
529
567
  */
530
568
  requireSharedRateState?: boolean;
569
+ /**
570
+ * Node/Daytona map memory guard. Production adapters should normally omit
571
+ * this and use the runtime constants; tests and explicit resource profiles may
572
+ * pass a smaller/larger budget to make the OOM boundary deterministic.
573
+ */
574
+ runtimeMapMemoryLimits?: Partial<RuntimeMapMemoryLimits>;
575
+ /**
576
+ * Return map PlayDataset handles backed by Runtime Sheet reads after a map
577
+ * has persisted. Runtime adapters with real sheet APIs should enable this;
578
+ * in-memory callback tests can omit it and keep local result resolvers.
579
+ */
580
+ runtimeSheetBackedMapDatasets?: boolean;
531
581
  resolvePlay?: (playRef: string) => Promise<ResolvedPlayExecution | null>;
532
582
  getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
533
583
  getToolRetryPolicy?: (toolId: string) => Promise<{
@@ -583,6 +633,19 @@ export interface ContextOptions {
583
633
  ) =>
584
634
  | Promise<Array<RuntimeStepReceipt | null>>
585
635
  | Array<RuntimeStepReceipt | null>;
636
+ markRuntimeStepReceiptRunning?: (
637
+ input: MarkRuntimeStepReceiptRunningInput,
638
+ ) => Promise<RuntimeStepReceipt | null> | RuntimeStepReceipt | null;
639
+ markRuntimeStepReceiptsRunning?: (
640
+ input: MarkRuntimeStepReceiptsRunningInput,
641
+ ) =>
642
+ | Promise<Array<RuntimeStepReceipt | null>>
643
+ | Array<RuntimeStepReceipt | null>;
644
+ markRuntimeStepReceiptsQueued?: (
645
+ input: MarkRuntimeStepReceiptsQueuedInput,
646
+ ) =>
647
+ | Promise<Array<RuntimeStepReceipt | null>>
648
+ | Array<RuntimeStepReceipt | null>;
586
649
  completeRuntimeStepReceipt?: (
587
650
  input: CompleteRuntimeStepReceiptInput,
588
651
  ) => Promise<RuntimeStepReceipt | null> | RuntimeStepReceipt | null;
@@ -15,8 +15,10 @@ import { getToolHttpErrorReceiptFailureKind } from './tool-http-errors';
15
15
  import type { WorkReceiptFailureKind } from './work-receipts';
16
16
  import {
17
17
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
18
+ runtimeLeaseHeartbeatIntervalFromExpiry,
18
19
  runtimeLeaseHeartbeatIntervalMs,
19
20
  } from './lease-policy';
21
+ import { createRuntimeReceiptHeartbeatSupervisor } from './receipt-heartbeat-supervisor';
20
22
 
21
23
  const DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS = 240;
22
24
  const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
@@ -42,8 +44,14 @@ export class RuntimeReceiptLeaseLostError extends Error {
42
44
 
43
45
  function isInFlightRuntimeReceipt(
44
46
  receipt: RuntimeStepReceipt | null | undefined,
45
- ): receipt is RuntimeStepReceipt & { status: 'pending' | 'running' } {
46
- return receipt?.status === 'pending' || receipt?.status === 'running';
47
+ ): receipt is RuntimeStepReceipt & {
48
+ status: 'queued' | 'pending' | 'running';
49
+ } {
50
+ return (
51
+ receipt?.status === 'queued' ||
52
+ receipt?.status === 'pending' ||
53
+ receipt?.status === 'running'
54
+ );
47
55
  }
48
56
 
49
57
  export function runtimeReceiptFailureKindForError(
@@ -51,6 +59,7 @@ export function runtimeReceiptFailureKindForError(
51
59
  ): WorkReceiptFailureKind {
52
60
  const toolFailureKind = getToolHttpErrorReceiptFailureKind(error);
53
61
  if (toolFailureKind) return toolFailureKind;
62
+
54
63
  return isPlayExecutionSuspendedError(error) ||
55
64
  isPlayRowExecutionSuspendedError(error)
56
65
  ? 'repairable'
@@ -140,6 +149,11 @@ export type DurableReceiptExecutionStore = {
140
149
  forceRefresh?: boolean,
141
150
  forceFailedRefresh?: boolean,
142
151
  ): Promise<RuntimeStepReceipt | null>;
152
+ markRunning?(
153
+ receiptKey: string,
154
+ runId: string,
155
+ leaseId?: string | null,
156
+ ): Promise<RuntimeStepReceipt | null>;
143
157
  complete(
144
158
  receiptKey: string,
145
159
  runId: string,
@@ -173,6 +187,7 @@ async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
173
187
  leaseId: string;
174
188
  heartbeatIntervalMs?: number;
175
189
  store: DurableReceiptExecutionStore;
190
+ onTerminalHeartbeat?: (receipt: RuntimeStepReceipt) => void;
176
191
  execute: () => Promise<T>;
177
192
  }): Promise<T> {
178
193
  if (!input.store.heartbeat) {
@@ -185,57 +200,56 @@ async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
185
200
  throw new Error('Runtime receipt heartbeat interval must be positive.');
186
201
  }
187
202
 
188
- let stopped = false;
189
- let timeout: ReturnType<typeof setTimeout> | null = null;
190
203
  let rejectLeaseLost!: (error: RuntimeReceiptLeaseLostError) => void;
191
204
  const leaseLost = new Promise<never>((_, reject) => {
192
205
  rejectLeaseLost = reject;
193
206
  });
194
- const stop = () => {
195
- stopped = true;
196
- if (timeout !== null) {
197
- clearTimeout(timeout);
198
- timeout = null;
199
- }
200
- };
201
- const heartbeatOnce = async () => {
207
+ const heartbeatOnce = async (): Promise<'active' | 'terminal'> => {
202
208
  const receipt = await input.store.heartbeat!(
203
209
  input.receiptKey,
204
210
  input.runId,
205
211
  input.leaseId,
206
212
  );
207
- if (
208
- !receipt ||
209
- receipt.status !== 'running' ||
210
- receipt.leaseId !== input.leaseId
211
- ) {
213
+ const isTerminal =
214
+ receipt?.status === 'completed' || receipt?.status === 'skipped';
215
+ if (receipt && isTerminal) {
216
+ input.onTerminalHeartbeat?.(receipt);
217
+ return 'terminal';
218
+ }
219
+ const stillOwnsReceipt =
220
+ receipt &&
221
+ (receipt.status === 'running' || receipt.status === 'queued') &&
222
+ receipt.leaseId === input.leaseId;
223
+ if (!stillOwnsReceipt) {
224
+ const latest = await input.store.get(input.receiptKey);
225
+ if (
226
+ latest &&
227
+ (latest.status === 'completed' || latest.status === 'skipped')
228
+ ) {
229
+ input.onTerminalHeartbeat?.(latest);
230
+ return 'terminal';
231
+ }
232
+ }
233
+ if (!stillOwnsReceipt) {
212
234
  throw new RuntimeReceiptLeaseLostError(input);
213
235
  }
236
+ return 'active';
214
237
  };
215
- const schedule = () => {
216
- timeout = setTimeout(() => {
217
- void (async () => {
218
- try {
219
- await heartbeatOnce();
220
- if (!stopped) schedule();
221
- } catch (error) {
222
- stop();
223
- rejectLeaseLost(
224
- error instanceof RuntimeReceiptLeaseLostError
225
- ? error
226
- : new RuntimeReceiptLeaseLostError(input),
227
- );
228
- }
229
- })();
230
- }, heartbeatIntervalMs);
231
- };
238
+ const supervisor = createRuntimeReceiptHeartbeatSupervisor({
239
+ intervalMs: heartbeatIntervalMs,
240
+ heartbeat: heartbeatOnce,
241
+ isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
242
+ onLeaseLost: (error) =>
243
+ rejectLeaseLost(error as RuntimeReceiptLeaseLostError),
244
+ });
232
245
 
233
- await heartbeatOnce();
234
- schedule();
246
+ if ((await heartbeatOnce()) === 'active') {
247
+ supervisor.start();
248
+ }
235
249
  try {
236
250
  return await Promise.race([input.execute(), leaseLost]);
237
251
  } finally {
238
- stop();
252
+ supervisor.stop();
239
253
  }
240
254
  }
241
255
 
@@ -274,6 +288,66 @@ export async function waitForCompletedRuntimeReceipt(input: {
274
288
  throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
275
289
  }
276
290
 
291
+ export async function waitForCompletedRuntimeReceipts(input: {
292
+ receiptKeys: string[];
293
+ store: Pick<DurableReceiptExecutionStore, 'getMany'>;
294
+ maxAttempts?: number;
295
+ delayMs?: number;
296
+ abortSignal?: AbortSignal;
297
+ }): Promise<{
298
+ completed: Map<string, RuntimeStepReceipt>;
299
+ failed: Map<string, Error>;
300
+ timedOut: Set<string>;
301
+ }> {
302
+ const uniqueKeys = [
303
+ ...new Set(input.receiptKeys.map((key) => key.trim()).filter(Boolean)),
304
+ ];
305
+ const pending = new Set(uniqueKeys);
306
+ const completed = new Map<string, RuntimeStepReceipt>();
307
+ const failed = new Map<string, Error>();
308
+ const maxAttempts = input.maxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
309
+ const delayMs = input.delayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
310
+
311
+ for (
312
+ let attempt = 0;
313
+ attempt < maxAttempts && pending.size > 0;
314
+ attempt += 1
315
+ ) {
316
+ throwIfReceiptWaitAborted(input.abortSignal);
317
+ if (attempt > 0) {
318
+ await sleepReceiptWait(delayMs, input.abortSignal);
319
+ }
320
+ let receipts: Map<string, RuntimeStepReceipt>;
321
+ try {
322
+ receipts = await input.store.getMany([...pending]);
323
+ } catch (error) {
324
+ if (error instanceof RuntimeReceiptWaitTimeoutError) {
325
+ break;
326
+ }
327
+ throw error;
328
+ }
329
+ for (const key of [...pending]) {
330
+ const receipt = receipts.get(key);
331
+ if (receipt?.status === 'completed' || receipt?.status === 'skipped') {
332
+ completed.set(key, receipt);
333
+ pending.delete(key);
334
+ continue;
335
+ }
336
+ if (receipt?.status === 'failed') {
337
+ failed.set(
338
+ key,
339
+ new Error(
340
+ `Durable tool call ${key} failed: ${receipt.error ?? 'unknown error'}`,
341
+ ),
342
+ );
343
+ pending.delete(key);
344
+ }
345
+ }
346
+ }
347
+
348
+ return { completed, failed, timedOut: pending };
349
+ }
350
+
277
351
  export async function executeWithDurableRuntimeReceipt<T>(input: {
278
352
  operation: DurableReceiptOperation;
279
353
  id: string;
@@ -295,6 +369,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
295
369
  onClaimedResult?: (output: T, receiptKey: string) => T;
296
370
  shouldPersistFailure?: (error: unknown) => boolean;
297
371
  heartbeatIntervalMs?: number;
372
+ markRunningBeforeExecute?: boolean;
298
373
  formatError: (error: unknown) => string;
299
374
  log: (message: string) => void;
300
375
  execute: (context: { leaseId: string | null }) => Promise<T>;
@@ -324,6 +399,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
324
399
  };
325
400
 
326
401
  let ownedLeaseId: string | null = null;
402
+ let ownedLeaseExpiresAt: string | null = null;
327
403
 
328
404
  const reclaimReceipt = async (): Promise<
329
405
  { kind: 'recovered'; output: T } | { kind: 'claimed' }
@@ -351,6 +427,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
351
427
  throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
352
428
  }
353
429
  ownedLeaseId = reclaimed?.leaseId ?? null;
430
+ ownedLeaseExpiresAt = reclaimed?.leaseExpiresAt ?? null;
354
431
  return { kind: 'claimed' };
355
432
  };
356
433
  const shouldRepairSameRunRunningReceipt = (
@@ -439,6 +516,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
439
516
  if (recovered.kind === 'recovered') return recovered.output;
440
517
  } else {
441
518
  ownedLeaseId = claimed.leaseId ?? null;
519
+ ownedLeaseExpiresAt = claimed.leaseExpiresAt ?? null;
442
520
  }
443
521
  } else if (claimed.status === 'failed') {
444
522
  await repairFailedReceipt(claimed);
@@ -446,14 +524,40 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
446
524
 
447
525
  let result: T;
448
526
  try {
527
+ if (input.markRunningBeforeExecute !== false && input.store.markRunning) {
528
+ const running = await input.store.markRunning(
529
+ input.receiptKey,
530
+ input.runId,
531
+ ownedLeaseId,
532
+ );
533
+ if (!running || running.status !== 'running') {
534
+ throw new RuntimeReceiptLeaseLostError({
535
+ receiptKey: input.receiptKey,
536
+ runId: input.runId,
537
+ leaseId: ownedLeaseId ?? '',
538
+ });
539
+ }
540
+ ownedLeaseId = running.leaseId ?? ownedLeaseId;
541
+ ownedLeaseExpiresAt = running.leaseExpiresAt ?? ownedLeaseExpiresAt;
542
+ }
449
543
  const executed =
450
544
  ownedLeaseId && input.store.heartbeat
451
545
  ? await executeWithDurableRuntimeReceiptHeartbeat({
452
546
  receiptKey: input.receiptKey,
453
547
  runId: input.runId,
454
548
  leaseId: ownedLeaseId,
455
- heartbeatIntervalMs: input.heartbeatIntervalMs,
549
+ heartbeatIntervalMs:
550
+ input.heartbeatIntervalMs ??
551
+ runtimeLeaseHeartbeatIntervalFromExpiry({
552
+ leaseExpiresAt: ownedLeaseExpiresAt,
553
+ fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
554
+ }),
456
555
  store: input.store,
556
+ onTerminalHeartbeat: (receipt) => {
557
+ input.log(
558
+ `ctx.${input.operation}(${input.id}): receipt heartbeat observed terminal ${receipt.status}; treating inner gateway completion as ownership handoff`,
559
+ );
560
+ },
457
561
  execute: () => input.execute({ leaseId: ownedLeaseId }),
458
562
  })
459
563
  : await input.execute({ leaseId: ownedLeaseId });
@@ -474,7 +578,10 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
474
578
  input.runId,
475
579
  ownedLeaseId,
476
580
  );
477
- if (!released || released.status !== 'pending') {
581
+ if (
582
+ !released ||
583
+ (released.status !== 'queued' && released.status !== 'pending')
584
+ ) {
478
585
  throw new Error(
479
586
  `ctx.${input.operation}(${input.id}): receipt suspended but ownership could not be released: ${input.formatError(error)}.`,
480
587
  );
@@ -502,6 +609,21 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
502
609
  isToolExecuteResult(result) ? serializeToolExecuteResult(result) : result,
503
610
  ownedLeaseId,
504
611
  );
612
+ if (
613
+ !completed ||
614
+ (completed.status !== 'completed' && completed.status !== 'skipped')
615
+ ) {
616
+ const latest = await input.store.get(input.receiptKey);
617
+ if (
618
+ latest &&
619
+ (latest.status === 'completed' || latest.status === 'skipped')
620
+ ) {
621
+ input.log(
622
+ `ctx.${input.operation}(${input.id}): receipt completion response reconciled from durable store read-back`,
623
+ );
624
+ return await recoverCompletedReceipt(latest, 'owner');
625
+ }
626
+ }
505
627
  if (!completed && input.store.canPersistCompletion) {
506
628
  throw new Error(
507
629
  `ctx.${input.operation}(${input.id}): lost durable receipt ownership before completion.`,
@@ -0,0 +1,218 @@
1
+ import type { PacingRule } from './rate-state-backend';
2
+
3
+ export type ResolvedPacing = {
4
+ provider: string;
5
+ rules: PacingRule[];
6
+ };
7
+
8
+ export type AdaptiveAdmissionSnapshot = {
9
+ provider: string;
10
+ currentRequestsPerSecond: number;
11
+ latencyMsEwma: number | null;
12
+ successCount: number;
13
+ rateLimitCount: number;
14
+ retryAfterUntilMs: number | null;
15
+ };
16
+
17
+ export interface RuntimeAdaptiveAdmission {
18
+ resolvePacing(input: {
19
+ orgId: string;
20
+ toolId: string;
21
+ declaredPacing: ResolvedPacing | null;
22
+ fallbackPacing: ResolvedPacing;
23
+ }): ResolvedPacing;
24
+ observeProviderSuccess(input: {
25
+ orgId: string;
26
+ provider: string;
27
+ latencyMs?: number | null;
28
+ }): void;
29
+ observeProviderBackpressure(input: {
30
+ orgId: string;
31
+ provider: string;
32
+ retryAfterMs: number;
33
+ }): void;
34
+ snapshot(): Record<string, AdaptiveAdmissionSnapshot>;
35
+ }
36
+
37
+ export type InMemoryAdaptiveAdmissionOptions = {
38
+ initialRequestsPerSecond: number;
39
+ minRequestsPerSecond?: number;
40
+ maxRequestsPerSecond: number;
41
+ additiveIncreasePerWindow?: number;
42
+ cleanWindowMs?: number;
43
+ now?: () => number;
44
+ };
45
+
46
+ type ProviderState = {
47
+ provider: string;
48
+ currentRequestsPerSecond: number;
49
+ successCount: number;
50
+ successCountAtLastIncrease: number;
51
+ rateLimitCount: number;
52
+ lastIncreaseAtMs: number;
53
+ retryAfterUntilMs: number | null;
54
+ latencyMsEwma: number | null;
55
+ fallbackRules: PacingRule[];
56
+ };
57
+
58
+ function finitePositive(value: number, fallback: number): number {
59
+ return Number.isFinite(value) && value > 0 ? value : fallback;
60
+ }
61
+
62
+ function stateKey(orgId: string, provider: string): string {
63
+ return `${orgId}:${provider}`;
64
+ }
65
+
66
+ function cloneRulesWithRequestsPerSecond(
67
+ rules: readonly PacingRule[],
68
+ requestsPerSecond: number,
69
+ ): PacingRule[] {
70
+ const rps = Math.max(1, Math.floor(requestsPerSecond));
71
+ return rules.map((rule) => ({
72
+ ...rule,
73
+ requestsPerWindow:
74
+ rule.windowMs === 1_000
75
+ ? rps
76
+ : Math.max(1, Math.floor((rps * rule.windowMs) / 1_000)),
77
+ }));
78
+ }
79
+
80
+ export function createInMemoryAdaptiveAdmission(
81
+ options: InMemoryAdaptiveAdmissionOptions,
82
+ ): RuntimeAdaptiveAdmission {
83
+ const now = options.now ?? Date.now;
84
+ const minRequestsPerSecond = finitePositive(
85
+ options.minRequestsPerSecond ?? 1,
86
+ 1,
87
+ );
88
+ const initialRequestsPerSecond = Math.max(
89
+ minRequestsPerSecond,
90
+ finitePositive(options.initialRequestsPerSecond, minRequestsPerSecond),
91
+ );
92
+ const maxRequestsPerSecond = Math.max(
93
+ initialRequestsPerSecond,
94
+ finitePositive(options.maxRequestsPerSecond, initialRequestsPerSecond),
95
+ );
96
+ const additiveIncreasePerWindow = finitePositive(
97
+ options.additiveIncreasePerWindow ?? 5,
98
+ 5,
99
+ );
100
+ const cleanWindowMs = finitePositive(options.cleanWindowMs ?? 1_000, 1_000);
101
+ const states = new Map<string, ProviderState>();
102
+
103
+ function getOrCreateState(input: {
104
+ orgId: string;
105
+ provider: string;
106
+ fallbackRules: readonly PacingRule[];
107
+ }): ProviderState {
108
+ const key = stateKey(input.orgId, input.provider);
109
+ let state = states.get(key);
110
+ if (!state) {
111
+ state = {
112
+ provider: input.provider,
113
+ currentRequestsPerSecond: initialRequestsPerSecond,
114
+ successCount: 0,
115
+ successCountAtLastIncrease: 0,
116
+ rateLimitCount: 0,
117
+ lastIncreaseAtMs: now(),
118
+ retryAfterUntilMs: null,
119
+ latencyMsEwma: null,
120
+ fallbackRules: [...input.fallbackRules],
121
+ };
122
+ states.set(key, state);
123
+ }
124
+ return state;
125
+ }
126
+
127
+ function getExistingState(
128
+ orgId: string,
129
+ provider: string,
130
+ ): ProviderState | null {
131
+ return states.get(stateKey(orgId, provider)) ?? null;
132
+ }
133
+
134
+ return {
135
+ resolvePacing(input) {
136
+ if (input.declaredPacing && input.declaredPacing.rules.length > 0) {
137
+ return input.declaredPacing;
138
+ }
139
+ const state = getOrCreateState({
140
+ orgId: input.orgId,
141
+ provider: input.fallbackPacing.provider,
142
+ fallbackRules: input.fallbackPacing.rules,
143
+ });
144
+ return {
145
+ provider: input.fallbackPacing.provider,
146
+ rules: cloneRulesWithRequestsPerSecond(
147
+ state.fallbackRules,
148
+ state.currentRequestsPerSecond,
149
+ ),
150
+ };
151
+ },
152
+
153
+ observeProviderSuccess(input) {
154
+ const state = getExistingState(input.orgId, input.provider);
155
+ if (!state) return;
156
+ const observedLatency = input.latencyMs;
157
+ if (observedLatency != null && Number.isFinite(observedLatency)) {
158
+ const latency = Math.max(0, observedLatency);
159
+ state.latencyMsEwma =
160
+ state.latencyMsEwma == null
161
+ ? latency
162
+ : state.latencyMsEwma * 0.8 + latency * 0.2;
163
+ }
164
+ state.successCount += 1;
165
+ const currentTime = now();
166
+ if (
167
+ state.retryAfterUntilMs != null &&
168
+ state.retryAfterUntilMs > currentTime
169
+ ) {
170
+ return;
171
+ }
172
+ state.retryAfterUntilMs = null;
173
+ const successesSinceIncrease =
174
+ state.successCount - state.successCountAtLastIncrease;
175
+ if (
176
+ currentTime - state.lastIncreaseAtMs >= cleanWindowMs &&
177
+ successesSinceIncrease >= Math.max(1, state.currentRequestsPerSecond)
178
+ ) {
179
+ state.currentRequestsPerSecond = Math.min(
180
+ maxRequestsPerSecond,
181
+ state.currentRequestsPerSecond + additiveIncreasePerWindow,
182
+ );
183
+ state.lastIncreaseAtMs = currentTime;
184
+ state.successCountAtLastIncrease = state.successCount;
185
+ }
186
+ },
187
+
188
+ observeProviderBackpressure(input) {
189
+ const state = getExistingState(input.orgId, input.provider);
190
+ if (!state) return;
191
+ state.rateLimitCount += 1;
192
+ state.currentRequestsPerSecond = Math.max(
193
+ minRequestsPerSecond,
194
+ Math.floor(state.currentRequestsPerSecond / 2),
195
+ );
196
+ const currentTime = now();
197
+ state.retryAfterUntilMs = currentTime + Math.max(0, input.retryAfterMs);
198
+ state.lastIncreaseAtMs = currentTime;
199
+ state.successCountAtLastIncrease = state.successCount;
200
+ },
201
+
202
+ snapshot() {
203
+ return Object.fromEntries(
204
+ [...states.entries()].map(([key, state]) => [
205
+ key,
206
+ {
207
+ provider: state.provider,
208
+ currentRequestsPerSecond: state.currentRequestsPerSecond,
209
+ latencyMsEwma: state.latencyMsEwma,
210
+ successCount: state.successCount,
211
+ rateLimitCount: state.rateLimitCount,
212
+ retryAfterUntilMs: state.retryAfterUntilMs,
213
+ },
214
+ ]),
215
+ );
216
+ },
217
+ };
218
+ }