deepline 0.3.72 → 0.3.73

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.
@@ -69,6 +69,8 @@ export interface ToolCallRequest {
69
69
  providerIdempotencyKeyBase?: string | null;
70
70
  cacheable?: boolean;
71
71
  receiptKey?: string | null;
72
+ /** Pre-cutover result-cache keys to read before executing a canonical miss. */
73
+ legacyReceiptKeys?: string[];
72
74
  executionAuthScopeDigest?: string | null;
73
75
  receiptLeaseId?: string | null;
74
76
  receiptLeaseExpiresAt?: string | null;
@@ -45,10 +45,39 @@ export function buildDurableToolCallCacheKey(input: {
45
45
  /** Run-stable clock used to choose the stale bucket. */
46
46
  cacheEpochMs?: number;
47
47
  playLocalScope?: string | null;
48
- /**
49
- * Explicit revision for a response transformation. Bump it only when the
50
- * serialized tool output changes; omit it for historical artifacts.
51
- */
48
+ }): string {
49
+ return buildDurableToolCallCacheKeyWithPresentation(input);
50
+ }
51
+
52
+ /**
53
+ * Reads receipt keys written before response presentation stopped partitioning
54
+ * the exact-request cache. New writes must use `buildDurableToolCallCacheKey`.
55
+ */
56
+ export function buildLegacyRevisionedDurableToolCallCacheKey(input: {
57
+ orgId?: string | null;
58
+ toolId: string;
59
+ requestInput: Record<string, unknown>;
60
+ authScopeDigest?: string | null;
61
+ providerActionVersion?: string | null;
62
+ cachePolicyVersion?: string | null;
63
+ staleAfterSeconds?: number | null;
64
+ cacheEpochMs?: number;
65
+ playLocalScope?: string | null;
66
+ toolResponseReceiptRevision: string;
67
+ }): string {
68
+ return buildDurableToolCallCacheKeyWithPresentation(input);
69
+ }
70
+
71
+ function buildDurableToolCallCacheKeyWithPresentation(input: {
72
+ orgId?: string | null;
73
+ toolId: string;
74
+ requestInput: Record<string, unknown>;
75
+ authScopeDigest?: string | null;
76
+ providerActionVersion?: string | null;
77
+ cachePolicyVersion?: string | null;
78
+ staleAfterSeconds?: number | null;
79
+ cacheEpochMs?: number;
80
+ playLocalScope?: string | null;
52
81
  toolResponseReceiptRevision?: string | null;
53
82
  }): string {
54
83
  const orgId = input.orgId?.trim() || 'org';
@@ -63,11 +92,6 @@ export function buildDurableToolCallCacheKey(input: {
63
92
  );
64
93
  }
65
94
  const playLocalScope = input.playLocalScope?.trim() || 'play';
66
- // Keep historical digests byte-for-byte stable when no explicit revision
67
- // exists on the stored artifact.
68
- const toolResponseReceiptRevision = normalizeToolResponseReceiptRevision(
69
- input.toolResponseReceiptRevision,
70
- );
71
95
  const digest = sha256Hex(
72
96
  stableStringify({
73
97
  kind: 'tool' satisfies DurableCallKind,
@@ -78,9 +102,15 @@ export function buildDurableToolCallCacheKey(input: {
78
102
  requestInput: input.requestInput,
79
103
  authScopeDigest: input.authScopeDigest ?? null,
80
104
  providerActionVersion,
105
+ ...(input.toolResponseReceiptRevision
106
+ ? {
107
+ toolResponseReceiptRevision: normalizeToolResponseReceiptRevision(
108
+ input.toolResponseReceiptRevision,
109
+ ),
110
+ }
111
+ : {}),
81
112
  cachePolicyVersion:
82
113
  input.cachePolicyVersion ?? DURABLE_CALL_CACHE_POLICY_VERSION,
83
- ...(toolResponseReceiptRevision ? { toolResponseReceiptRevision } : {}),
84
114
  staleBucket: durableCacheStaleBucket({
85
115
  staleAfterSeconds: input.staleAfterSeconds,
86
116
  nowMs: input.cacheEpochMs,
@@ -15,7 +15,6 @@ export {
15
15
  } from './runtime-step-receipts-adapter';
16
16
  export {
17
17
  createToolCallReceiptCohortJob,
18
- ToolCallReceiptCohortWaitTimeoutError,
19
18
  type ToolCallReceiptCohort,
20
19
  type ToolCallReceiptCohortJob,
21
20
  type ToolCallReceiptCohortMember,
@@ -54,12 +53,8 @@ export { ToolCallReceiptLeaseLostError } from './contract';
54
53
  * Adapts a receipt lifecycle that is already owned by a higher-level runtime
55
54
  * job. This is a composition seam, not a second receipt implementation.
56
55
  *
57
- * PlayContext's direct-call path currently owns a richer runtime receipt
58
- * protocol: completed-cache publication, non-idempotent execution locks,
59
- * auth-scope re-keying, and external-call-slot retention. Once it has
60
- * claimed that protocol's receipt, ToolCall can still own the call boundary
61
- * (validation and dispatch) without trying to claim or terminally write the
62
- * same receipt a second time. The outer job remains the sole durable owner.
56
+ * The outer runtime owns the physical batch operation. ToolCall keeps the
57
+ * result boundary uniform without performing a second cache write.
63
58
  *
64
59
  * Do not use this for a new caller. New runtimes should provide a real
65
60
  * ToolResultReceipts Adapter. This exists only while PlayContext's historical
@@ -117,21 +112,14 @@ export function createExternallyOwnedToolResultReceipts(input: {
117
112
  },
118
113
  async heartbeat(candidate) {
119
114
  assertKeyAndLease(candidate);
120
- // The outer runtime owns durable heartbeating / execution locks. Returning
121
- // active here deliberately avoids a duplicate heartbeat write.
122
115
  return 'active';
123
116
  },
124
117
  async complete(candidate) {
125
118
  assertKeyAndLease(candidate);
126
- // The outer runtime atomically publishes the result, including its
127
- // cache-only convergence behaviour. A second completion here would alter
128
- // that race and can overwrite its execution metadata.
129
119
  return { kind: 'stored', result: candidate.result };
130
120
  },
131
121
  async fail(candidate) {
132
122
  assertKeyAndLease(candidate);
133
- // Failure publication belongs to the outer state machine because it
134
- // classifies auth-scope changes, suspensions, and lost ownership.
135
123
  },
136
124
  };
137
125
  }
@@ -1,6 +1,5 @@
1
1
  import type { ToolExecuteResult } from '../tool-result';
2
2
  import {
3
- ToolCallReceiptLeaseLostError,
4
3
  type ToolCallInput,
5
4
  type ToolCallJob,
6
5
  type ToolCallReceiptLease,
@@ -24,8 +23,6 @@ export type NativeBatchToolCallMember = {
24
23
 
25
24
  export type NativeBatchOwnedToolCallMember = NativeBatchToolCallMember & {
26
25
  receipt: ToolCallReceiptLease;
27
- /** Actual member lease expiry supplied by the receipt-cohort Adapter. */
28
- leaseExpiresAt?: string | null;
29
26
  };
30
27
 
31
28
  export type NativeBatchToolCallInput = {
@@ -50,14 +47,6 @@ export type NativeBatchToolCallInput = {
50
47
  }):
51
48
  | Array<{ memberId: string; result: ToolExecuteResult }>
52
49
  | Promise<Array<{ memberId: string; result: ToolExecuteResult }>>;
53
- /** Keep all owned member leases alive during a long provider batch. */
54
- heartbeat?: {
55
- /** May derive a real interval from the claimed member leases. */
56
- intervalMs:
57
- | number
58
- | ((input: { members: NativeBatchOwnedToolCallMember[] }) => number);
59
- onTransientFailure?(error: unknown): void;
60
- };
61
50
  };
62
51
 
63
52
  /** A native batch returns each local member result, including cache followers. */
@@ -70,12 +59,8 @@ export type NativeBatchToolCallJob = {
70
59
  /**
71
60
  * Owns one provider-native map batch.
72
61
  *
73
- * The Module composes two deep jobs rather than copying either protocol:
74
- * `ToolCallReceiptCohort` owns per-member receipt leases, recovery and terminal
75
- * fanout; `ToolCall` owns the physical provider-operation invocation. This
76
- * Module supplies the missing semantic rule between them: exactly one
77
- * aggregate operation may be dispatched, and it must be split completely
78
- * before every owned member receipt is completed.
62
+ * It reads each member's completed-result cache before assembling one physical
63
+ * provider batch for the misses, then atomically stores each successful split.
79
64
  */
80
65
  export function createNativeBatchToolCallJob(dependencies: {
81
66
  receiptCohort: ToolCallReceiptCohortJob;
@@ -98,68 +83,26 @@ export function createNativeBatchToolCallJob(dependencies: {
98
83
  forceFailedRefresh: member.forceFailedRefresh,
99
84
  waitMaxAttempts: member.waitMaxAttempts,
100
85
  })),
101
- // Do not make locally owned rows wait behind a foreign receipt's
102
- // bounded recovery. The first aggregate can run now; any receipt
103
- // reclaimed after the wait forms its own later aggregate operation.
104
- eager: true,
105
86
  });
106
87
  const executeOwned = async (): Promise<void> => {
107
88
  const owned = ownedMembers(cohort, input.members);
108
89
  if (owned.length === 0) return;
109
90
  const physical = input.buildPhysicalCall({ members: owned });
110
91
  validatePhysicalCall(physical, input.owner);
111
- let leaseLost: ToolCallReceiptLeaseLostError | null = null;
112
- const heartbeatIntervalMs = input.heartbeat
113
- ? typeof input.heartbeat.intervalMs === 'function'
114
- ? input.heartbeat.intervalMs({ members: owned })
115
- : input.heartbeat.intervalMs
116
- : null;
117
- const heartbeat =
118
- input.heartbeat && heartbeatIntervalMs !== null
119
- ? cohort.startHeartbeat({
120
- intervalMs: positiveHeartbeatInterval(heartbeatIntervalMs),
121
- onLeaseLost(error) {
122
- leaseLost ??= error;
123
- },
124
- onTransientFailure: input.heartbeat.onTransientFailure,
125
- })
126
- : null;
127
92
  try {
128
- await assertCohortOwned(cohort, heartbeat, leaseLost);
129
93
  const aggregate = await dependencies.physicalToolCall.call(physical);
130
- await assertCohortOwned(cohort, heartbeat, leaseLost);
131
- let split: Array<{ memberId: string; result: ToolExecuteResult }>;
132
- try {
133
- split = await input.splitResult({
134
- result: aggregate,
135
- members: owned,
136
- });
137
- validateSplit(owned, split);
138
- } catch (error) {
139
- throw error;
140
- }
94
+ const split = await input.splitResult({
95
+ result: aggregate,
96
+ members: owned,
97
+ });
98
+ validateSplit(owned, split);
141
99
  await cohort.complete(split);
142
100
  } catch (error) {
143
- // Once the aggregate call begins, a provider/transport failure is a
144
- // terminal fact for every owned member. Lease loss is the exception:
145
- // a stale owner must not overwrite the new holder's receipt state.
146
- if (!(error instanceof ToolCallReceiptLeaseLostError)) {
147
- await failOwned(cohort, owned, error);
148
- }
101
+ await failOwned(cohort, owned, error);
149
102
  throw error;
150
- } finally {
151
- heartbeat?.stop();
152
103
  }
153
104
  };
154
105
 
155
- // Start the known-owned aggregate before foreign receipt hydration. The
156
- // settlement is still awaited before collection, and any newly reclaimed
157
- // member gets a distinct physical operation rather than being silently
158
- // omitted from this batch.
159
- const initialExecution = executeOwned();
160
- const settlement = cohort.settle();
161
- await initialExecution;
162
- await settlement;
163
106
  await executeOwned();
164
107
 
165
108
  return await collectMemberResults(cohort, input.members);
@@ -178,21 +121,11 @@ function ownedMembers(
178
121
  owned.push({
179
122
  ...member,
180
123
  receipt: state.lease,
181
- ...(state.leaseExpiresAt ? { leaseExpiresAt: state.leaseExpiresAt } : {}),
182
124
  });
183
125
  }
184
126
  return owned;
185
127
  }
186
128
 
187
- function positiveHeartbeatInterval(value: number): number {
188
- if (!Number.isFinite(value) || value <= 0) {
189
- throw new Error(
190
- 'Native Batch Tool Call heartbeat interval must be positive.',
191
- );
192
- }
193
- return Math.max(1, Math.floor(value));
194
- }
195
-
196
129
  async function collectMemberResults(
197
130
  cohort: ToolCallReceiptCohort,
198
131
  members: NativeBatchToolCallMember[],
@@ -204,11 +137,10 @@ async function collectMemberResults(
204
137
  results.set(member.memberId, state.result);
205
138
  continue;
206
139
  }
207
- if (state.kind === 'following' || state.kind === 'recovering') {
140
+ if (state.kind === 'following') {
208
141
  results.set(member.memberId, await state.result);
209
142
  continue;
210
143
  }
211
- if (state.kind === 'failed') throw state.error;
212
144
  throw new Error(
213
145
  `Native Batch Tool Call left owned member ${member.memberId} without a terminal result.`,
214
146
  );
@@ -216,21 +148,6 @@ async function collectMemberResults(
216
148
  return results;
217
149
  }
218
150
 
219
- async function assertCohortOwned(
220
- cohort: ToolCallReceiptCohort,
221
- heartbeat: { assertOwned(): void } | null,
222
- leaseLost: ToolCallReceiptLeaseLostError | null,
223
- ): Promise<void> {
224
- heartbeat?.assertOwned();
225
- if (leaseLost) throw leaseLost;
226
- const status = await cohort.heartbeat();
227
- if (status === 'terminal') {
228
- throw new ToolCallReceiptLeaseLostError('native-batch-cohort');
229
- }
230
- heartbeat?.assertOwned();
231
- if (leaseLost) throw leaseLost;
232
- }
233
-
234
151
  async function failOwned(
235
152
  cohort: ToolCallReceiptCohort,
236
153
  owned: NativeBatchOwnedToolCallMember[],