deepline 0.3.393 → 0.3.394

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 (122) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +477 -10
  3. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +5 -1
  4. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-policy.ts +28 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/freshness-receipt-execution.ts +251 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +12 -1
  7. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +10 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +25 -16
  9. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -0
  10. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +69 -22
  11. package/dist/bundling-sources/shared_libs/plays/core.ts +1 -1
  12. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +18 -0
  13. package/dist/cli/auth-IHKISLJQ.js +39 -0
  14. package/dist/cli/{auth-5PXHA232.mjs → auth-OSXDAVCF.mjs} +4 -4
  15. package/dist/cli/{billing-JFB2XLSO.js → billing-EKLYOXUY.js} +75 -75
  16. package/dist/cli/{billing-Z2N7ZAGM.mjs → billing-ZNNKX3UR.mjs} +4 -4
  17. package/dist/cli/{chunk-OXNOAD6Y.js → chunk-2MDDA7A4.js} +26 -26
  18. package/dist/cli/{chunk-PTDECJGP.js → chunk-354JCXA2.js} +50 -50
  19. package/dist/cli/{chunk-RKMB3N7K.mjs → chunk-3HU47EGZ.mjs} +48 -9
  20. package/dist/cli/{chunk-6I73X254.mjs → chunk-3HWALOL2.mjs} +1 -1
  21. package/dist/cli/{chunk-VIQJIX2P.js → chunk-4P27AXEA.js} +65 -65
  22. package/dist/cli/{chunk-UOGZUA24.mjs → chunk-5BB2RGAX.mjs} +18 -8
  23. package/dist/cli/{chunk-RWHWF5ZJ.js → chunk-6YAUMG27.js} +1 -1
  24. package/dist/cli/{chunk-HLEESTCH.mjs → chunk-BP33SIZF.mjs} +1 -1
  25. package/dist/cli/{chunk-CDFH2DYL.js → chunk-CS6RAIVY.js} +4 -4
  26. package/dist/cli/{chunk-2HAD745V.js → chunk-E23BQ5SC.js} +9 -9
  27. package/dist/cli/{chunk-LMMEAMA7.mjs → chunk-FJGW5GXU.mjs} +1 -1
  28. package/dist/cli/{chunk-Q26LUSQ2.mjs → chunk-JHIJIHFB.mjs} +1 -1
  29. package/dist/cli/{chunk-PWSWAN7Q.js → chunk-KB4SDX5S.js} +48 -9
  30. package/dist/cli/{chunk-M7Y7JYZX.js → chunk-KGNTKMRG.js} +6 -6
  31. package/dist/cli/{chunk-4XX6443T.js → chunk-KI635JN4.js} +59 -59
  32. package/dist/cli/{chunk-Z3OOZM5L.js → chunk-KIUMEOM5.js} +8 -8
  33. package/dist/cli/{chunk-KIYU2TGL.mjs → chunk-M57J2SK4.mjs} +2 -2
  34. package/dist/cli/{chunk-FQ6VHPW4.mjs → chunk-MFB2VKBO.mjs} +2 -2
  35. package/dist/cli/{chunk-MS6GUPBB.js → chunk-MH73HMPV.js} +3 -3
  36. package/dist/cli/{chunk-AARX5L4B.js → chunk-MLPU5SMZ.js} +2 -2
  37. package/dist/cli/{chunk-5PRVMXKB.mjs → chunk-NR3DFKNF.mjs} +1 -1
  38. package/dist/cli/{chunk-W4WOMXR3.js → chunk-QIQBYKLB.js} +15 -15
  39. package/dist/cli/{chunk-VIWE6WRK.js → chunk-RBJYWRCF.js} +260 -250
  40. package/dist/cli/{chunk-LZPCDB7U.mjs → chunk-TJI2EN6K.mjs} +1 -1
  41. package/dist/cli/{chunk-4TCT77UF.js → chunk-URWF7NGQ.js} +6 -1
  42. package/dist/cli/{chunk-5KWKZIQR.mjs → chunk-UXFSWG3Q.mjs} +4 -4
  43. package/dist/cli/{chunk-L2SYLGRP.js → chunk-VK6FYZLP.js} +8 -8
  44. package/dist/cli/{chunk-7HR7BGZL.mjs → chunk-X63FDZJZ.mjs} +3 -3
  45. package/dist/cli/{chunk-4PT2VA3A.mjs → chunk-XRQ2AQL7.mjs} +2 -2
  46. package/dist/cli/{chunk-STNYIMIV.mjs → chunk-XXUDVEXK.mjs} +6 -6
  47. package/dist/cli/{chunk-PQSA443V.mjs → chunk-ZI2ZNDJK.mjs} +6 -1
  48. package/dist/cli/{chunk-2LGC4EFT.mjs → chunk-ZYBGA4AR.mjs} +3 -3
  49. package/dist/cli/command-docs.js +2 -2
  50. package/dist/cli/command-docs.mjs +1 -1
  51. package/dist/cli/{csv-KJWZHKCH.mjs → csv-5AXRB35H.mjs} +3 -3
  52. package/dist/cli/{csv-P3Z3YE7H.js → csv-LU7O75WE.js} +12 -12
  53. package/dist/cli/{db-NCM4CQIS.mjs → db-7XP5UWEW.mjs} +4 -4
  54. package/dist/cli/{db-NUZGJNQC.js → db-F7YTFKVM.js} +29 -29
  55. package/dist/cli/{describe-YGSEN5WC.js → describe-FRH32RDV.js} +20 -20
  56. package/dist/cli/{describe-WT6MM2GO.mjs → describe-K4ES5LIL.mjs} +10 -10
  57. package/dist/cli/{executions-XS2IPJ4W.mjs → executions-IEHEHJUO.mjs} +4 -4
  58. package/dist/cli/{executions-PCOLHN7Q.js → executions-RZV5XPT2.js} +6 -6
  59. package/dist/cli/{feedback-4MSH7TFU.js → feedback-3P65QTX3.js} +7 -7
  60. package/dist/cli/{feedback-CCLTS5SV.mjs → feedback-FPUVZBAN.mjs} +3 -3
  61. package/dist/cli/{git-K2DHPMPC.mjs → git-GLVVP7TY.mjs} +4 -4
  62. package/dist/cli/git-MFFAWVVX.js +23 -0
  63. package/dist/cli/index.js +95 -95
  64. package/dist/cli/index.mjs +33 -33
  65. package/dist/cli/{legacy-noops-B5S4AR3P.mjs → legacy-noops-3MOI6NBM.mjs} +4 -4
  66. package/dist/cli/{legacy-noops-GPNOPFXV.js → legacy-noops-XKSGZR7Q.js} +7 -7
  67. package/dist/cli/{monitors-JLMAIPRG.mjs → monitors-FTBGNEFG.mjs} +4 -4
  68. package/dist/cli/{monitors-ZAWD7NSN.js → monitors-NZ2DYNST.js} +85 -85
  69. package/dist/cli/{org-P2KN4CES.js → org-MCKDQXIV.js} +64 -64
  70. package/dist/cli/{org-T3JTHP4G.mjs → org-V2J5RBCN.mjs} +4 -4
  71. package/dist/cli/{play-NK6BLRGL.mjs → play-AJFGME3B.mjs} +9 -9
  72. package/dist/cli/play-WLSN2EO2.js +158 -0
  73. package/dist/cli/{providers-5QRZN3DT.js → providers-2JEZ2264.js} +7 -7
  74. package/dist/cli/{providers-3NQRA7IT.mjs → providers-I6RQJZFZ.mjs} +4 -4
  75. package/dist/cli/{quickstart-PIA2JGPV.mjs → quickstart-BIDQDMO4.mjs} +3 -3
  76. package/dist/cli/{quickstart-R7NUZGNW.js → quickstart-GJ2RPDU3.js} +5 -5
  77. package/dist/cli/search-5VN4YD4I.js +27 -0
  78. package/dist/cli/{search-AIDIIGLY.mjs → search-ELRXFWG5.mjs} +5 -5
  79. package/dist/cli/{secrets-BT3PGMWN.js → secrets-53IT6VXD.js} +13 -13
  80. package/dist/cli/{secrets-OQNHL45O.mjs → secrets-SQXGZXIC.mjs} +4 -4
  81. package/dist/cli/{sessions-SF7WJPJP.mjs → sessions-2X5QF7SL.mjs} +4 -4
  82. package/dist/cli/{sessions-EYXWOSWK.js → sessions-CAV5M77Y.js} +5 -5
  83. package/dist/cli/{settings-BA3GHBOD.js → settings-532WNAZO.js} +74 -74
  84. package/dist/cli/{settings-Y57JTFDD.mjs → settings-7P67VGCJ.mjs} +7 -7
  85. package/dist/cli/{setup-RERXYC66.js → setup-6L2VSUWJ.js} +41 -41
  86. package/dist/cli/{setup-7GR5GBHA.mjs → setup-QGGBV2LE.mjs} +6 -6
  87. package/dist/cli/{skills-SJWIQAV5.mjs → skills-3FE62WBG.mjs} +4 -4
  88. package/dist/cli/skills-S6NPZAN7.js +28 -0
  89. package/dist/cli/tools-647RLXQS.js +41 -0
  90. package/dist/cli/{tools-ZF3JXPNL.mjs → tools-DMR25DEF.mjs} +7 -7
  91. package/dist/cli/{update-FFFYQAOJ.mjs → update-6RZMHJZP.mjs} +6 -6
  92. package/dist/cli/update-WVTSC3EM.js +28 -0
  93. package/dist/cli/{workflow-XVFEEGAS.mjs → workflow-L2DQRE6R.mjs} +5 -5
  94. package/dist/cli/{workflow-SZJVQH4T.js → workflow-TX4F2PIX.js} +15 -15
  95. package/dist/{client-DU5dtHDy.d.mts → client-BktIPdIj.d.mts} +1 -1
  96. package/dist/{client-mCE88hC4.d.ts → client-C3O1Ipf3.d.ts} +1 -1
  97. package/dist/{compiler-manifest-DE4RdAiY.d.ts → compiler-manifest-DLxUY5kI.d.mts} +65 -11
  98. package/dist/{compiler-manifest-DE4RdAiY.d.mts → compiler-manifest-DLxUY5kI.d.ts} +65 -11
  99. package/dist/{index-Bi-f7HFT.d.ts → index-B_GM9E3q.d.ts} +1 -1
  100. package/dist/{index-BI6nWgaV.d.mts → index-SCb0NvDb.d.mts} +1 -1
  101. package/dist/index.d.mts +5 -5
  102. package/dist/index.d.ts +5 -5
  103. package/dist/index.js +7 -7
  104. package/dist/index.mjs +4 -4
  105. package/dist/install-integrity.json +85 -84
  106. package/dist/plays/bundle-play-file.d.mts +3 -3
  107. package/dist/plays/bundle-play-file.d.ts +3 -3
  108. package/dist/plays/bundle-play-file.mjs +2 -2
  109. package/dist/plays/run-request.d.mts +2 -2
  110. package/dist/plays/run-request.d.ts +2 -2
  111. package/dist/release.d.mts +1 -1
  112. package/dist/release.d.ts +1 -1
  113. package/dist/release.js +2 -2
  114. package/dist/release.mjs +1 -1
  115. package/package.json +1 -1
  116. package/dist/cli/auth-P3MVDTXM.js +0 -39
  117. package/dist/cli/git-RNM45FX4.js +0 -23
  118. package/dist/cli/play-PDXR6JFB.js +0 -158
  119. package/dist/cli/search-QRWL7MFM.js +0 -27
  120. package/dist/cli/skills-V7EY75HB.js +0 -28
  121. package/dist/cli/tools-H5HLBCHH.js +0 -41
  122. package/dist/cli/update-472HA3LT.js +0 -28
@@ -202,7 +202,7 @@ export const SDK_RELEASE = {
202
202
  // getters keep their established compatibility behavior.
203
203
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
204
204
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
205
- version: '0.3.393',
205
+ version: '0.3.394',
206
206
  updateSummary:
207
207
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
208
208
  packageCapabilities: {
@@ -195,6 +195,7 @@ import {
195
195
  type ToolExecuteResult,
196
196
  type ToolResultExecutionMetadata,
197
197
  type ToolResultMetadataInput,
198
+ type CallFreshnessMetadata,
198
199
  } from './tool-result';
199
200
  import {
200
201
  EXECUTION_GATEWAY_RECEIPT_OUTPUT_REFERENCE_HEADER,
@@ -269,9 +270,12 @@ import {
269
270
  scopeToolReceiptToInvocation,
270
271
  resolveDurableCallCachePolicy,
271
272
  } from './durable-call-cache';
273
+ import { resolveExplicitDurableCallPolicy } from './durable-call-policy';
274
+ import { executeWithFreshnessReceipt } from './freshness-receipt-execution';
272
275
  import {
273
276
  CtxFetchHttpError,
274
277
  PLAY_AUTHORING_CONTRACT_EDITION,
278
+ PlayAuthoringFieldValidationError,
275
279
  normalizePlayAuthoringCustomerDbStatement,
276
280
  validateOptionalPlayAuthoringField,
277
281
  validatePlayAuthoringField,
@@ -639,6 +643,7 @@ const inlineCompositionContext =
639
643
  const authoredStepContext = new AsyncLocalStorage<{
640
644
  context: PlayContextImpl;
641
645
  stepId: string;
646
+ invocationId?: string;
642
647
  }>();
643
648
  const toolExecutionOverrides = new AsyncLocalStorage<{ force: boolean }>();
644
649
  const PROGRESS_HEARTBEAT_INTERVAL_MS = 1_000;
@@ -2931,6 +2936,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2931
2936
  private mapInvocationIndex = 0;
2932
2937
  private readonly stepCallIndexByKey = new Map<string, number>();
2933
2938
  private readonly toolCallIndexByKey = new Map<string, number>();
2939
+ private readonly freshnessCallIndexByKey = new Map<string, number>();
2934
2940
  /**
2935
2941
  * Parent-level inline-child aggregates. Maintained only by the single-writer
2936
2942
  * progress path (never per child) so concurrent fan-out cannot contend. See
@@ -3208,7 +3214,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3208
3214
  key: string,
3209
3215
  toolId: string,
3210
3216
  input: Record<string, unknown>,
3211
- options?: { description?: string },
3217
+ options?: {
3218
+ description?: string;
3219
+ staleAfterSeconds?: number | null;
3220
+ },
3212
3221
  ): Promise<ToolExecuteResult<TOutput>> {
3213
3222
  validatePlayAuthoringField('ctx.tool.key', key);
3214
3223
  validatePlayAuthoringField('ctx.tool.tool', toolId);
@@ -3224,6 +3233,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3224
3233
  tool: toolId,
3225
3234
  input,
3226
3235
  ...(options?.description ? { description: options.description } : {}),
3236
+ ...(options?.staleAfterSeconds !== undefined
3237
+ ? { staleAfterSeconds: options.staleAfterSeconds }
3238
+ : {}),
3227
3239
  })) as ToolExecuteResult<TOutput>;
3228
3240
  }
3229
3241
 
@@ -3233,9 +3245,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3233
3245
  readonly customerDb = {
3234
3246
  query: async <TRow extends object = Record<string, unknown>>(
3235
3247
  statement: PlaySqlQuery | string,
3236
- options?: { maxRows?: number; timeoutMs?: number },
3248
+ options?: {
3249
+ id?: string;
3250
+ staleAfterSeconds?: number | null;
3251
+ maxRows?: number;
3252
+ timeoutMs?: number;
3253
+ },
3237
3254
  ): Promise<TRow[]> => {
3238
3255
  const sql = normalizePlayAuthoringCustomerDbStatement(statement);
3256
+ if (this.usesExplicitFreshness) {
3257
+ validatePlayAuthoringField(
3258
+ 'ctx.customerDb.query.options.id',
3259
+ options?.id,
3260
+ );
3261
+ } else if (options?.id !== undefined) {
3262
+ validatePlayAuthoringField(
3263
+ 'ctx.customerDb.query.options.id',
3264
+ options.id,
3265
+ );
3266
+ }
3267
+ if (options?.staleAfterSeconds !== undefined) {
3268
+ validatePlayAuthoringField(
3269
+ 'ctx.customerDb.query.options.staleAfterSeconds',
3270
+ options.staleAfterSeconds,
3271
+ );
3272
+ }
3273
+ if (
3274
+ this.usesExplicitFreshness &&
3275
+ options?.staleAfterSeconds === undefined
3276
+ ) {
3277
+ throw new PlayAuthoringFieldValidationError(
3278
+ 'ctx.customerDb.query.options.staleAfterSeconds',
3279
+ 'play_authoring_durable_policy_invalid',
3280
+ 'Edition 11 Customer DB queries require staleAfterSeconds: 0.',
3281
+ );
3282
+ }
3239
3283
  if (options?.maxRows !== undefined) {
3240
3284
  validatePlayAuthoringField(
3241
3285
  'ctx.customerDb.query.options.maxRows',
@@ -3249,7 +3293,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3249
3293
  );
3250
3294
  }
3251
3295
  const result = (await this.tools.execute({
3252
- id: `customer_db_query_${this.customerDbQueryIndex++}`,
3296
+ id: options?.id ?? `customer_db_query_${this.customerDbQueryIndex++}`,
3253
3297
  tool: 'query_customer_db',
3254
3298
  input: {
3255
3299
  sql,
@@ -3260,7 +3304,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3260
3304
  description: 'Customer DB query',
3261
3305
  // The direct executor never reused a prior SQL result. Preserve that
3262
3306
  // imperative behavior for reads and mutations alike.
3263
- force: true,
3307
+ ...(this.usesExplicitFreshness
3308
+ ? { staleAfterSeconds: options?.staleAfterSeconds }
3309
+ : { force: true }),
3264
3310
  ...(options?.timeoutMs !== undefined
3265
3311
  ? { timeoutMs: options.timeoutMs }
3266
3312
  : {}),
@@ -3866,6 +3912,95 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3866
3912
  return this.currentRuntimeContract.semantics;
3867
3913
  }
3868
3914
 
3915
+ private get usesExplicitFreshness(): boolean {
3916
+ return (
3917
+ this.currentRuntimeSemantics.freshness ===
3918
+ 'actual_age_with_operation_recovery'
3919
+ );
3920
+ }
3921
+
3922
+ private freshnessInvocationId(operation: string, id: string): string {
3923
+ const row = rowContext.getStore();
3924
+ const step = authoredStepContext.getStore();
3925
+ const scope = stableStringify({
3926
+ namespace: this.currentExecutionScope.receipt.namespace,
3927
+ operation,
3928
+ id,
3929
+ step: step?.context === this ? (step.invocationId ?? step.stepId) : null,
3930
+ row: row
3931
+ ? {
3932
+ table: row.tableNamespace ?? null,
3933
+ key: row.rowKey ?? row.rowId,
3934
+ field: row.fieldName ?? null,
3935
+ }
3936
+ : null,
3937
+ });
3938
+ const freshIndex = this.freshnessCallIndexByKey.get(scope) ?? 0;
3939
+ this.freshnessCallIndexByKey.set(scope, freshIndex + 1);
3940
+ // Invocation identity follows authored scope and call order. Keep it
3941
+ // separate from the request/auth/action fingerprint used by reusable
3942
+ // result caching, so credential or response-contract changes cannot mint
3943
+ // a second provider operation key for the same authored effect.
3944
+ // A mapped row can be re-entered after provider backpressure; its replay
3945
+ // cursor restores the original call position.
3946
+ const index = nextReplayableCallIndex(`freshness:${scope}`, freshIndex);
3947
+ return sha256Hex(`${scope}:${index}`);
3948
+ }
3949
+
3950
+ private async executeFreshnessControlledCall<T>(input: {
3951
+ operation: DurableCtxOperation;
3952
+ id: string;
3953
+ semanticKey: string;
3954
+ invocationId: string;
3955
+ policy: { requested: number | null; effective: number | null };
3956
+ execute: () => Promise<T>;
3957
+ completedAt?: (value: T) => number | undefined;
3958
+ onOutcome?: (value: T, metadata: CallFreshnessMetadata) => T;
3959
+ }): Promise<T> {
3960
+ const orgId = this.#options.orgId?.trim();
3961
+ if (!orgId) {
3962
+ throw new Error(
3963
+ 'Freshness-controlled calls require a resolved organization identity.',
3964
+ );
3965
+ }
3966
+ return executeWithFreshnessReceipt({
3967
+ orgId,
3968
+ semanticKey: input.semanticKey,
3969
+ invocationId: input.invocationId,
3970
+ runId: this.currentRunId,
3971
+ requestedStaleAfterSeconds: input.policy.requested,
3972
+ effectiveStaleAfterSeconds: input.policy.effective,
3973
+ store: this.durableReceiptExecutionStore(),
3974
+ completedAt: input.completedAt,
3975
+ withHydration: (read) =>
3976
+ this.withRuntimeReceiptHydrationTurn(
3977
+ (turn) =>
3978
+ read((receipt) =>
3979
+ turn.settleBytes(
3980
+ runtimeStepReceiptPayloadBytes(receipt ? [receipt] : []),
3981
+ ),
3982
+ ),
3983
+ { source: 'durable_operation' },
3984
+ ),
3985
+ execute: async () => {
3986
+ const value = await input.execute();
3987
+ this.assertNoPendingDurableSuspension();
3988
+ return value;
3989
+ },
3990
+ onOutcome: (value, metadata) => {
3991
+ this.runtimeLog(
3992
+ JSON.stringify({
3993
+ event: 'call_freshness',
3994
+ operation: input.operation,
3995
+ call_key: input.id,
3996
+ ...metadata,
3997
+ }),
3998
+ );
3999
+ return input.onOutcome ? input.onOutcome(value, metadata) : value;
4000
+ },
4001
+ });
4002
+ }
4003
+
3869
4004
  private recordPlayCallStep(input: {
3870
4005
  playId: string;
3871
4006
  execution?: 'inline';
@@ -6297,6 +6432,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6297
6432
  force?: boolean;
6298
6433
  receiptKey?: string | null;
6299
6434
  semanticKey?: string | null;
6435
+ invocationId?: string;
6300
6436
  staleAfterSeconds?: number | null;
6301
6437
  repairRunningReceiptForSameRun?: boolean;
6302
6438
  repairRunningReceiptForSameRunAfterWaitTimeout?: boolean;
@@ -6340,6 +6476,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6340
6476
  },
6341
6477
  });
6342
6478
  if (opts.transient === true) {
6479
+ if (
6480
+ this.usesExplicitFreshness &&
6481
+ opts.staleAfterSeconds !== undefined &&
6482
+ opts.staleAfterSeconds !== 0
6483
+ ) {
6484
+ throw new Error(
6485
+ 'Transient fetch does not support retained result reuse; omit freshness or choose staleAfterSeconds: 0.',
6486
+ );
6487
+ }
6343
6488
  try {
6344
6489
  return await execute(null);
6345
6490
  } finally {
@@ -6347,6 +6492,44 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6347
6492
  release?.();
6348
6493
  }
6349
6494
  }
6495
+ if (this.usesExplicitFreshness && operation === 'fetch') {
6496
+ const policy = resolveExplicitDurableCallPolicy(opts);
6497
+ const semanticKey = stableStringify({
6498
+ orgId: this.#options.orgId,
6499
+ namespace: this.currentExecutionScope.receipt.namespace,
6500
+ operation,
6501
+ id,
6502
+ semanticKey: opts.semanticKey,
6503
+ });
6504
+ try {
6505
+ return await this.executeFreshnessControlledCall({
6506
+ operation,
6507
+ id,
6508
+ semanticKey,
6509
+ invocationId:
6510
+ opts.invocationId ?? this.freshnessInvocationId(operation, id),
6511
+ policy,
6512
+ execute: () => execute(null),
6513
+ onOutcome: (value, freshness) => {
6514
+ const output =
6515
+ opts.onRecovered && freshness.source !== 'executed'
6516
+ ? opts.onRecovered(
6517
+ value,
6518
+ {
6519
+ key: freshness.operationReceiptKey!,
6520
+ status: 'completed',
6521
+ },
6522
+ 'cache',
6523
+ )
6524
+ : value;
6525
+ return { ...output, freshness };
6526
+ },
6527
+ });
6528
+ } finally {
6529
+ const release = releaseExternalCallSlot as (() => void) | null;
6530
+ release?.();
6531
+ }
6532
+ }
6350
6533
  const stalePolicy = resolveDurableCallCachePolicy(
6351
6534
  opts.staleAfterSeconds,
6352
6535
  operation === 'step'
@@ -11662,6 +11845,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11662
11845
  stepPath,
11663
11846
  }),
11664
11847
  ),
11848
+ ...(step.staleAfterSeconds !== undefined
11849
+ ? { staleAfterSeconds: step.staleAfterSeconds }
11850
+ : {}),
11665
11851
  },
11666
11852
  )
11667
11853
  : await this.executeStepProgramStep(
@@ -12951,6 +13137,165 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12951
13137
  toolId: string,
12952
13138
  input: Record<string, unknown>,
12953
13139
  options?: ToolCallOptions,
13140
+ ): Promise<unknown> {
13141
+ if (!this.usesExplicitFreshness) {
13142
+ return this.executeToolLegacyInternal(key, toolId, input, options);
13143
+ }
13144
+ const policy = resolveExplicitDurableCallPolicy(options ?? {});
13145
+ if (
13146
+ this.#options.cachePolicy?.forceToolRefresh === true ||
13147
+ toolExecutionOverrides.getStore()?.force === true
13148
+ ) {
13149
+ policy.effective = 0;
13150
+ }
13151
+ const normalizedKey = this.normalizeContextKey(key, 'tool');
13152
+ // Reserve operation identity before asynchronous metadata lookup. Two
13153
+ // concurrent calls may share an authored id; assigning their identity
13154
+ // after auth/action metadata resolves would make recovery depend on lookup
13155
+ // completion order instead of authored call order.
13156
+ const invocationId = this.freshnessInvocationId('tool', normalizedKey);
13157
+ const sqlTool = isCustomerDbDatasetTool(toolId);
13158
+ const scalarCustomerDbQuery = sqlTool && input.result_mode === 'rows';
13159
+ const liveDataset =
13160
+ isQueryResultDatasetReadRequest(toolId, input) && !scalarCustomerDbQuery;
13161
+ const sqlMutation =
13162
+ sqlTool &&
13163
+ !isQueryResultDatasetReadRequest(toolId, {
13164
+ ...input,
13165
+ result_mode: undefined,
13166
+ });
13167
+ const [eventWait, invocationScoped, retryPolicy, reusableSupport] =
13168
+ policy.effective === 0
13169
+ ? ([null, false, null, null] as const)
13170
+ : await Promise.all([
13171
+ this.#options.getIntegrationEventWaitHandler?.(toolId) ?? null,
13172
+ this.#options
13173
+ .getToolIdempotencyScope?.(toolId)
13174
+ .then((scope) => scope === 'invocation') ?? false,
13175
+ this.#options.getToolRetryPolicy?.(toolId, input) ?? null,
13176
+ this.#options.getToolReusableResultSupport?.(toolId) ?? null,
13177
+ ]);
13178
+ const requiresExecutionFence = retryPolicy?.requiresExecutionFence === true;
13179
+ if (policy.effective !== 0 && reusableSupport?.supported !== true) {
13180
+ throw new Error(
13181
+ `Tool ${toolId} does not support reusable freshness; use staleAfterSeconds: 0. ${reusableSupport?.unavailableReason ?? 'No read-result reuse capability was supplied by the action owner.'}`,
13182
+ );
13183
+ }
13184
+ if (
13185
+ policy.effective !== 0 &&
13186
+ (eventWait ||
13187
+ invocationScoped ||
13188
+ requiresExecutionFence ||
13189
+ liveDataset ||
13190
+ sqlMutation ||
13191
+ isAlwaysFreshIntegrationTool(toolId))
13192
+ ) {
13193
+ throw new Error(
13194
+ `Tool ${toolId} does not support reusable freshness; use staleAfterSeconds: 0. Live datasets, event waits, invocation-scoped actions, writes, and Workspace operations cannot reuse an older result.`,
13195
+ );
13196
+ }
13197
+
13198
+ const authScope = await this.resolveToolAuthScopeDigest(toolId);
13199
+ const actionVersion =
13200
+ (await this.#options.getToolActionCacheVersion?.(toolId))?.trim() ?? '';
13201
+ if (!actionVersion) {
13202
+ throw new Error(`Tool ${toolId} has no action identity for freshness.`);
13203
+ }
13204
+ const semanticKey = stableStringify({
13205
+ kind: 'tool',
13206
+ orgId: this.#options.orgId,
13207
+ toolId,
13208
+ input,
13209
+ authScope,
13210
+ actionVersion,
13211
+ responseContract: this.currentToolResponseContract,
13212
+ responseRevision: this.currentToolResponseReceiptRevision,
13213
+ });
13214
+ if (sqlTool && input.operation_key !== undefined) {
13215
+ throw new Error(
13216
+ 'Play SQL operation identity is assigned by the runtime. Use the authored call id to identify the operation.',
13217
+ );
13218
+ }
13219
+ const requestInput =
13220
+ sqlTool && (sqlMutation || scalarCustomerDbQuery)
13221
+ ? { ...input, operation_key: `${this.currentRunId}:${invocationId}` }
13222
+ : input;
13223
+
13224
+ if (liveDataset || isAlwaysFreshIntegrationTool(toolId)) {
13225
+ const value = await this.executeToolLegacyInternal(
13226
+ key,
13227
+ toolId,
13228
+ requestInput,
13229
+ { ...options, force: true, staleAfterSeconds: 0 },
13230
+ );
13231
+ const freshness: CallFreshnessMetadata = {
13232
+ requestedStaleAfterSeconds: policy.requested,
13233
+ effectiveStaleAfterSeconds: 0,
13234
+ source: 'live_observation',
13235
+ resultAgeMs: 0,
13236
+ completedAt: Date.now(),
13237
+ unavailableReason: liveDataset
13238
+ ? 'Dataset rows are live observations; the dataset owner controls pagination and recovery.'
13239
+ : 'Workspace reviewer edits and conflict checks must be observed on every invocation.',
13240
+ };
13241
+ return isToolExecuteResult(value)
13242
+ ? cloneToolExecuteResultWithExecution(value, {
13243
+ ...value._metadata.execution,
13244
+ freshness,
13245
+ })
13246
+ : value;
13247
+ }
13248
+
13249
+ return this.executeFreshnessControlledCall({
13250
+ operation: 'tool',
13251
+ id: normalizedKey,
13252
+ semanticKey,
13253
+ invocationId,
13254
+ policy,
13255
+ completedAt: (value) => {
13256
+ if (!isToolExecuteResult(value)) return undefined;
13257
+ const raw = value.toolResponse.raw as {
13258
+ operation?: { completed_at_ms?: unknown };
13259
+ } | null;
13260
+ const completedAt = raw?.operation?.completed_at_ms;
13261
+ return typeof completedAt === 'number' &&
13262
+ Number.isSafeInteger(completedAt)
13263
+ ? completedAt
13264
+ : value._metadata.execution.completedAt;
13265
+ },
13266
+ execute: async () => {
13267
+ const value = await this.executeToolLegacyInternal(
13268
+ key,
13269
+ toolId,
13270
+ requestInput,
13271
+ { ...options, force: true, staleAfterSeconds: 0 },
13272
+ );
13273
+ if ((await this.resolveToolAuthScopeDigest(toolId)) !== authScope) {
13274
+ throw new Error(
13275
+ 'Tool credential identity changed during execution. Resume under the current credential identity before retaining this result.',
13276
+ );
13277
+ }
13278
+ return value;
13279
+ },
13280
+ onOutcome: (value, freshness) =>
13281
+ isToolExecuteResult(value)
13282
+ ? cloneToolExecuteResultWithExecution(value, {
13283
+ ...value._metadata.execution,
13284
+ cached: freshness.source !== 'executed',
13285
+ source: freshness.source === 'executed' ? 'live' : 'cache',
13286
+ resultReceiptKey: freshness.operationReceiptKey,
13287
+ freshness,
13288
+ completedAt: freshness.completedAt,
13289
+ })
13290
+ : value,
13291
+ });
13292
+ }
13293
+
13294
+ private async executeToolLegacyInternal(
13295
+ key: string,
13296
+ toolId: string,
13297
+ input: Record<string, unknown>,
13298
+ options?: ToolCallOptions,
12954
13299
  ): Promise<unknown> {
12955
13300
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
12956
13301
  this.assertNoPendingDurableSuspension();
@@ -15085,6 +15430,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15085
15430
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
15086
15431
  validatePlayAuthoringField('ctx.fetch.key', key);
15087
15432
  const normalizedKey = this.normalizeContextKey(key, 'fetch');
15433
+ const invocationId =
15434
+ this.usesExplicitFreshness && options?.transient !== true
15435
+ ? this.freshnessInvocationId('fetch', normalizedKey)
15436
+ : undefined;
15088
15437
  const rowStore = rowContext.getStore();
15089
15438
  const rowFetchScope = rowStore
15090
15439
  ? {
@@ -15098,6 +15447,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15098
15447
  const url = input.toString();
15099
15448
  const parsedUrl = new URL(url);
15100
15449
  const method = (init.method ?? 'GET').toUpperCase();
15450
+ const reusableReadMethod = ['GET', 'HEAD', 'OPTIONS'].includes(method);
15451
+ if (this.usesExplicitFreshness && options?.transient === true) {
15452
+ if (!reusableReadMethod) {
15453
+ throw new Error(
15454
+ 'Edition 11 transient fetches are limited to read-only HTTP methods.',
15455
+ );
15456
+ }
15457
+ if (
15458
+ options.staleAfterSeconds !== undefined &&
15459
+ options.staleAfterSeconds !== 0
15460
+ ) {
15461
+ throw new Error(
15462
+ 'Transient fetch does not support retained result reuse; omit freshness or choose staleAfterSeconds: 0.',
15463
+ );
15464
+ }
15465
+ } else if (
15466
+ this.usesExplicitFreshness &&
15467
+ !reusableReadMethod &&
15468
+ options?.staleAfterSeconds !== 0
15469
+ ) {
15470
+ throw new Error(
15471
+ 'Edition 11 mutating or non-read-only fetches require staleAfterSeconds: 0.',
15472
+ );
15473
+ }
15101
15474
  const urlContainsResolvedSecret =
15102
15475
  this.secretRedactor.containsRegisteredSecret(url, {
15103
15476
  includeEncoded: true,
@@ -15187,6 +15560,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15187
15560
  : {}),
15188
15561
  }),
15189
15562
  ),
15563
+ invocationId,
15190
15564
  staleAfterSeconds: options?.staleAfterSeconds,
15191
15565
  transient: options?.transient === true,
15192
15566
  onRecovered: (output) => {
@@ -15238,6 +15612,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15238
15612
  },
15239
15613
  body: receiptBody,
15240
15614
  row: rowFetchScope,
15615
+ ...(invocationId ? { invocationId } : {}),
15241
15616
  }),
15242
15617
  )}`,
15243
15618
  );
@@ -15386,6 +15761,24 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15386
15761
  'ctx.step.semanticKey',
15387
15762
  options?.semanticKey,
15388
15763
  );
15764
+ const freshnessPolicy = this.usesExplicitFreshness
15765
+ ? resolveExplicitDurableCallPolicy(options ?? {})
15766
+ : null;
15767
+ if (
15768
+ freshnessPolicy &&
15769
+ this.#options.cachePolicy?.forceStepRefresh === true
15770
+ ) {
15771
+ freshnessPolicy.effective = 0;
15772
+ }
15773
+ if (
15774
+ freshnessPolicy &&
15775
+ freshnessPolicy.requested !== 0 &&
15776
+ !options?.semanticKey?.trim()
15777
+ ) {
15778
+ throw new Error(
15779
+ 'Reusable ctx.step results require a stable semanticKey.',
15780
+ );
15781
+ }
15389
15782
  const normalizedKey = this.normalizeContextKey(key, 'step');
15390
15783
  if (!normalizedKey.trim()) {
15391
15784
  throw new Error('ctx.step(key, fn) requires a non-empty stable step id.');
@@ -15393,7 +15786,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15393
15786
 
15394
15787
  const rowStore = rowContext.getStore();
15395
15788
  const scope = rowStore ? `row-${rowStore.rowId}` : 'workflow';
15396
- const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:${scope}:${normalizedKey}`;
15789
+ const parentStep = authoredStepContext.getStore();
15790
+ const explicitStepScope = freshnessPolicy
15791
+ ? stableDigest(
15792
+ stableStringify({
15793
+ semanticKey: options?.semanticKey ?? null,
15794
+ parent:
15795
+ parentStep?.context === this
15796
+ ? (parentStep.invocationId ?? parentStep.stepId)
15797
+ : null,
15798
+ row: rowStore
15799
+ ? {
15800
+ table: rowStore.tableNamespace,
15801
+ key: rowStore.rowKey ?? rowStore.rowId,
15802
+ field: rowStore.fieldName,
15803
+ }
15804
+ : null,
15805
+ }),
15806
+ )
15807
+ : null;
15808
+ const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:${scope}:${normalizedKey}${explicitStepScope ? `:${explicitStepScope}` : ''}`;
15397
15809
  const freshCallIndex = this.stepCallIndexByKey.get(callIndexKey) ?? 0;
15398
15810
  const callIndex = nextReplayableCallIndex(
15399
15811
  `step:${callIndexKey}`,
@@ -15405,7 +15817,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15405
15817
  );
15406
15818
  const boundarySuffix = callIndex === 0 ? '' : `:${callIndex}`;
15407
15819
  const boundaryId = this.durableBoundaryId(
15408
- `step-${scope}:${normalizedKey}${boundarySuffix}`,
15820
+ `step-${scope}:${normalizedKey}${explicitStepScope ? `:${explicitStepScope}` : ''}${boundarySuffix}`,
15409
15821
  );
15410
15822
  const advanceRecoveredSleepCursor = async (
15411
15823
  receipt?: RuntimeStepReceipt,
@@ -15488,9 +15900,63 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15488
15900
  });
15489
15901
  try {
15490
15902
  const output = await authoredStepContext.run(
15491
- { context: this, stepId: normalizedKey },
15492
- () =>
15493
- this.executeWithRuntimeReceipt<T>(
15903
+ {
15904
+ context: this,
15905
+ stepId: normalizedKey,
15906
+ ...(freshnessPolicy ? { invocationId: boundaryId } : {}),
15907
+ },
15908
+ async () => {
15909
+ if (freshnessPolicy) {
15910
+ const semanticKey = stableStringify({
15911
+ operation: 'step',
15912
+ orgId: this.#options.orgId,
15913
+ namespace: this.currentExecutionScope.receipt.namespace,
15914
+ id: normalizedKey,
15915
+ semanticKey: options?.semanticKey,
15916
+ row: rowStore
15917
+ ? {
15918
+ table: rowStore.tableNamespace,
15919
+ key: rowStore.rowKey ?? rowStore.rowId,
15920
+ field: rowStore.fieldName,
15921
+ }
15922
+ : null,
15923
+ });
15924
+ const completed = await this.executeFreshnessControlledCall({
15925
+ operation: 'step',
15926
+ id: normalizedKey,
15927
+ semanticKey,
15928
+ invocationId: boundaryId,
15929
+ policy: freshnessPolicy,
15930
+ completedAt: (value) => value.completedAt,
15931
+ execute: async () => {
15932
+ const before = this.sleepBoundaryIndex;
15933
+ const value = await executeStep();
15934
+ const boundary =
15935
+ this.checkpoint.resolvedBoundaries?.[boundaryId];
15936
+ return {
15937
+ output: value === undefined ? null : value,
15938
+ undefinedOutput: value === undefined,
15939
+ sleepCount: this.sleepBoundaryIndex - before,
15940
+ completedAt:
15941
+ (boundary?.kind === 'step'
15942
+ ? boundary.completedAt
15943
+ : undefined) ?? Date.now(),
15944
+ };
15945
+ },
15946
+ onOutcome: (value, freshness) => {
15947
+ if (freshness.source !== 'executed') {
15948
+ this.sleepBoundaryIndex += value.sleepCount;
15949
+ }
15950
+ return value;
15951
+ },
15952
+ });
15953
+ const result = completed.undefinedOutput
15954
+ ? undefined
15955
+ : completed.output;
15956
+ assertJsonSerializableStepOutput(normalizedKey, result);
15957
+ return result as T;
15958
+ }
15959
+ return await this.executeWithRuntimeReceipt<T>(
15494
15960
  'step',
15495
15961
  normalizedKey,
15496
15962
  this.currentRunId,
@@ -15520,7 +15986,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15520
15986
  },
15521
15987
  execute: executeStep,
15522
15988
  },
15523
- ),
15989
+ );
15990
+ },
15524
15991
  );
15525
15992
  if (observe)
15526
15993
  this.emitExecutionEvent({
@@ -338,7 +338,7 @@ export interface RuntimeDatasetOptions<TItem = Record<string, unknown>> {
338
338
  * Set staleAfterSeconds to rerun after that many seconds while retries/replays
339
339
  * inside the same window still hit the cache. Daily refreshes use 86400.
340
340
  */
341
- staleAfterSeconds?: number;
341
+ staleAfterSeconds?: number | null;
342
342
  /**
343
343
  * Optional stable key per row. When provided, row identity is derived from
344
344
  * the selected input field(s) or returned value instead of hashing the full
@@ -1083,6 +1083,9 @@ export interface ContextOptions {
1083
1083
  /** Identity policy is required before reading receipts; lookup errors are
1084
1084
  * fatal, unlike the legacy best-effort retry advice below. */
1085
1085
  getToolIdempotencyScope?: (toolId: string) => Promise<'invocation' | null>;
1086
+ getToolReusableResultSupport?: (
1087
+ toolId: string,
1088
+ ) => Promise<{ supported: boolean; unavailableReason: string | null }>;
1086
1089
  getToolRetryPolicy?: (
1087
1090
  toolId: string,
1088
1091
  input: Record<string, unknown>,
@@ -1365,6 +1368,7 @@ export type PlayFetchResponse = {
1365
1368
  headers: Record<string, string>;
1366
1369
  bodyText: string;
1367
1370
  json: unknown | null;
1371
+ freshness?: import('../plays/tool-result-types').CallFreshnessMetadata;
1368
1372
  };
1369
1373
 
1370
1374
  export interface BatchResult {