deepline 0.3.43 → 0.3.45
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/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1540 -265
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +22 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +16 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +418 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/index.ts +452 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +304 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/testkit.ts +83 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/always-fresh-adapter.ts +50 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/contract.ts +135 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +291 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/native-batch.ts +335 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +904 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +522 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/testkit.ts +165 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import type { ToolExecuteResult } from '../tool-result';
|
|
2
|
+
import { createRuntimeReceiptHeartbeatSupervisor } from '../receipt-heartbeat-supervisor';
|
|
3
|
+
import {
|
|
4
|
+
ToolCallReceiptLeaseLostError,
|
|
5
|
+
type ToolCallReceiptLease,
|
|
6
|
+
type ToolCallReceiptRecoverySource,
|
|
7
|
+
type ToolResultReceipts,
|
|
8
|
+
} from './contract';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The durable state of one map-member Tool Result Receipt. This is deliberately
|
|
12
|
+
* smaller than RuntimeStepReceipt: row persistence and provider transport are
|
|
13
|
+
* not receipt-cohort concerns.
|
|
14
|
+
*/
|
|
15
|
+
export type ToolCallReceiptRecord = {
|
|
16
|
+
resultReceiptKey: string;
|
|
17
|
+
status: 'queued' | 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
18
|
+
result?: ToolExecuteResult;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
lease?: ToolCallReceiptLease;
|
|
21
|
+
leaseExpiresAt?: string | null;
|
|
22
|
+
claimState?: 'claimed' | 'existing';
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ToolCallReceiptCohortMember = {
|
|
26
|
+
/** A caller-local identity; it is never persisted. */
|
|
27
|
+
memberId: string;
|
|
28
|
+
resultReceiptKey: string;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
forceFailedRefresh?: boolean;
|
|
31
|
+
/** Number of poll reads before a foreign/incomplete receipt is reclaimed. */
|
|
32
|
+
waitMaxAttempts?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ToolCallReceiptCohortStore = {
|
|
36
|
+
read(input: {
|
|
37
|
+
resultReceiptKeys: string[];
|
|
38
|
+
}): Promise<ToolCallReceiptRecord[]>;
|
|
39
|
+
/**
|
|
40
|
+
* The Adapter may partition this vector by refresh policy to reach a legacy
|
|
41
|
+
* gateway that accepts one policy per RPC. Results are key-aligned by value,
|
|
42
|
+
* not array position, so duplicates are forbidden at this seam.
|
|
43
|
+
*/
|
|
44
|
+
claim(input: {
|
|
45
|
+
ownerRunId: string;
|
|
46
|
+
ownerAttempt: number;
|
|
47
|
+
receipts: Array<{
|
|
48
|
+
resultReceiptKey: string;
|
|
49
|
+
force: boolean;
|
|
50
|
+
forceFailedRefresh: boolean;
|
|
51
|
+
reclaimRunning: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
}): Promise<ToolCallReceiptRecord[]>;
|
|
54
|
+
heartbeat(input: {
|
|
55
|
+
ownerRunId: string;
|
|
56
|
+
ownerAttempt: number;
|
|
57
|
+
receipts: Array<{ resultReceiptKey: string; lease: ToolCallReceiptLease }>;
|
|
58
|
+
}): Promise<ToolCallReceiptRecord[]>;
|
|
59
|
+
complete(input: {
|
|
60
|
+
ownerRunId: string;
|
|
61
|
+
ownerAttempt: number;
|
|
62
|
+
receipts: Array<{
|
|
63
|
+
resultReceiptKey: string;
|
|
64
|
+
lease: ToolCallReceiptLease;
|
|
65
|
+
result: ToolExecuteResult;
|
|
66
|
+
}>;
|
|
67
|
+
}): Promise<ToolCallReceiptRecord[]>;
|
|
68
|
+
fail(input: {
|
|
69
|
+
ownerRunId: string;
|
|
70
|
+
ownerAttempt: number;
|
|
71
|
+
receipts: Array<{
|
|
72
|
+
resultReceiptKey: string;
|
|
73
|
+
lease: ToolCallReceiptLease;
|
|
74
|
+
error: unknown;
|
|
75
|
+
}>;
|
|
76
|
+
}): Promise<ToolCallReceiptRecord[]>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type ToolCallReceiptCohortMemberState =
|
|
80
|
+
| {
|
|
81
|
+
kind: 'completed';
|
|
82
|
+
result: ToolExecuteResult;
|
|
83
|
+
source: 'cache' | 'in_flight' | 'owner';
|
|
84
|
+
}
|
|
85
|
+
| { kind: 'failed'; error: unknown }
|
|
86
|
+
| {
|
|
87
|
+
kind: 'owned';
|
|
88
|
+
resultReceiptKey: string;
|
|
89
|
+
lease: ToolCallReceiptLease;
|
|
90
|
+
leaseExpiresAt?: string | null;
|
|
91
|
+
receipts: ToolResultReceipts;
|
|
92
|
+
}
|
|
93
|
+
| {
|
|
94
|
+
kind: 'following';
|
|
95
|
+
resultReceiptKey: string;
|
|
96
|
+
ownerMemberId: string;
|
|
97
|
+
result: Promise<ToolExecuteResult>;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
/** A foreign receipt is being hydrated/reclaimed by the cohort. */
|
|
101
|
+
kind: 'recovering';
|
|
102
|
+
resultReceiptKey: string;
|
|
103
|
+
result: Promise<ToolExecuteResult>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type ToolCallReceiptCohort = {
|
|
107
|
+
member(memberId: string): ToolCallReceiptCohortMemberState;
|
|
108
|
+
/** A native batch uses this after splitting one physical provider response. */
|
|
109
|
+
complete(
|
|
110
|
+
input: Array<{ memberId: string; result: ToolExecuteResult }>,
|
|
111
|
+
): Promise<Map<string, ToolExecuteResult>>;
|
|
112
|
+
fail(input: Array<{ memberId: string; error: unknown }>): Promise<void>;
|
|
113
|
+
/** One RPC for every distinct owned receipt, including queued map tail work. */
|
|
114
|
+
heartbeat(): Promise<'active' | 'terminal'>;
|
|
115
|
+
/**
|
|
116
|
+
* Starts the cohort's ownership supervisor. Per-member receipt views only
|
|
117
|
+
* assert this shared fence; they never fragment it into one heartbeat RPC per
|
|
118
|
+
* provider call.
|
|
119
|
+
*/
|
|
120
|
+
startHeartbeat(input: {
|
|
121
|
+
intervalMs: number;
|
|
122
|
+
onLeaseLost(error: ToolCallReceiptLeaseLostError): void;
|
|
123
|
+
onTransientFailure?(error: unknown): void;
|
|
124
|
+
}): { stop(): void; assertOwned(): void };
|
|
125
|
+
/** Wait for foreign-receipt hydration/reclaim work started by eager acquire. */
|
|
126
|
+
settle(): Promise<void>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type ToolCallReceiptCohortJob = {
|
|
130
|
+
acquire(input: {
|
|
131
|
+
owner: { runId: string; attempt: number };
|
|
132
|
+
members: ToolCallReceiptCohortMember[];
|
|
133
|
+
/**
|
|
134
|
+
* Return after the atomic initial claim so independently owned map rows
|
|
135
|
+
* can start provider work while foreign rows hydrate. The default keeps
|
|
136
|
+
* the standalone Module's all-settled acquisition contract.
|
|
137
|
+
*/
|
|
138
|
+
eager?: boolean;
|
|
139
|
+
}): Promise<ToolCallReceiptCohort>;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export class ToolCallReceiptCohortWaitTimeoutError extends Error {
|
|
143
|
+
constructor(resultReceiptKey: string) {
|
|
144
|
+
super(`Timed out waiting for Tool Result Receipt ${resultReceiptKey}.`);
|
|
145
|
+
this.name = 'ToolCallReceiptCohortWaitTimeoutError';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Owns the receipt protocol for one mapped Tool Call cohort.
|
|
151
|
+
*
|
|
152
|
+
* Its leverage is deliberately map-specific: scalar ToolCall needs one receipt;
|
|
153
|
+
* a map needs deduplicated bulk ownership, bounded foreign-owner recovery,
|
|
154
|
+
* one heartbeat for undispatched work, and a terminal fanout after a native
|
|
155
|
+
* batch splits. Keeping that protocol out of PlayContext makes ToolCall's
|
|
156
|
+
* scalar Interface remain small without discarding the real map behaviour.
|
|
157
|
+
*/
|
|
158
|
+
export function createToolCallReceiptCohortJob(input: {
|
|
159
|
+
store: ToolCallReceiptCohortStore;
|
|
160
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
161
|
+
waitDelayMs?: number;
|
|
162
|
+
}): ToolCallReceiptCohortJob {
|
|
163
|
+
const sleep = input.sleep ?? defaultSleep;
|
|
164
|
+
const waitDelayMs = positive(input.waitDelayMs ?? 250, 'wait delay');
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
async acquire({ owner, members, eager = false }) {
|
|
168
|
+
const ownerRunId = required(owner.runId, 'owner run id');
|
|
169
|
+
if (!Number.isInteger(owner.attempt) || owner.attempt < 0) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
'Tool Call Receipt Cohort owner attempt must be a non-negative integer.',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (members.length === 0) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
'Tool Call Receipt Cohort requires at least one member.',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const groups = groupMembers(members);
|
|
181
|
+
const states = new Map<string, ToolCallReceiptCohortMemberState>();
|
|
182
|
+
const resultDeferred = new Map<string, Deferred<ToolExecuteResult>>();
|
|
183
|
+
const owned = new Map<string, OwnedGroup>();
|
|
184
|
+
let leaseLost: ToolCallReceiptLeaseLostError | null = null;
|
|
185
|
+
const queuedCompletions: Array<{
|
|
186
|
+
memberId: string;
|
|
187
|
+
result: ToolExecuteResult;
|
|
188
|
+
resolve(result: ToolExecuteResult): void;
|
|
189
|
+
reject(error: unknown): void;
|
|
190
|
+
}> = [];
|
|
191
|
+
const queuedFailures: Array<{
|
|
192
|
+
memberId: string;
|
|
193
|
+
error: unknown;
|
|
194
|
+
resolve(): void;
|
|
195
|
+
reject(error: unknown): void;
|
|
196
|
+
}> = [];
|
|
197
|
+
let completionFlushScheduled = false;
|
|
198
|
+
let failureFlushScheduled = false;
|
|
199
|
+
|
|
200
|
+
const resolveGroup = (
|
|
201
|
+
group: Group,
|
|
202
|
+
result: ToolExecuteResult,
|
|
203
|
+
source: 'cache' | 'in_flight' | 'owner',
|
|
204
|
+
) => {
|
|
205
|
+
const deferred = deferredFor(group, resultDeferred);
|
|
206
|
+
deferred.resolve(result);
|
|
207
|
+
for (const [index, member] of group.members.entries()) {
|
|
208
|
+
states.set(member.memberId, {
|
|
209
|
+
kind: 'completed',
|
|
210
|
+
result,
|
|
211
|
+
// The physical owner receives its live dispatch result. Duplicate
|
|
212
|
+
// map cells attached to that receipt are followers even though
|
|
213
|
+
// the completion fanout is local to this process.
|
|
214
|
+
source: source === 'owner' && index > 0 ? 'in_flight' : source,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const rejectGroup = (group: Group, error: unknown) => {
|
|
219
|
+
const deferred = deferredFor(group, resultDeferred);
|
|
220
|
+
deferred.reject(error);
|
|
221
|
+
for (const member of group.members) {
|
|
222
|
+
states.set(member.memberId, { kind: 'failed', error });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const ownGroup = (group: Group, record: ToolCallReceiptRecord) => {
|
|
227
|
+
const lease = record.lease;
|
|
228
|
+
if (!lease || lease.ownerRunId !== ownerRunId) {
|
|
229
|
+
throw new ToolCallReceiptLeaseLostError(group.resultReceiptKey);
|
|
230
|
+
}
|
|
231
|
+
const ownerMember = group.members[0]!;
|
|
232
|
+
const groupOwned: OwnedGroup = { group, lease };
|
|
233
|
+
owned.set(group.resultReceiptKey, groupOwned);
|
|
234
|
+
states.set(ownerMember.memberId, {
|
|
235
|
+
kind: 'owned',
|
|
236
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
237
|
+
lease,
|
|
238
|
+
...(record.leaseExpiresAt
|
|
239
|
+
? { leaseExpiresAt: record.leaseExpiresAt }
|
|
240
|
+
: {}),
|
|
241
|
+
receipts: receiptView(ownerMember.memberId, groupOwned),
|
|
242
|
+
});
|
|
243
|
+
const deferred = deferredFor(group, resultDeferred);
|
|
244
|
+
for (const follower of group.members.slice(1)) {
|
|
245
|
+
states.set(follower.memberId, {
|
|
246
|
+
kind: 'following',
|
|
247
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
248
|
+
ownerMemberId: ownerMember.memberId,
|
|
249
|
+
result: deferred.promise,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const applyClaim = (
|
|
255
|
+
group: Group,
|
|
256
|
+
record: ToolCallReceiptRecord | undefined,
|
|
257
|
+
source: 'cache' | 'in_flight',
|
|
258
|
+
) => {
|
|
259
|
+
if (!record) return false;
|
|
260
|
+
if (isCompleted(record)) {
|
|
261
|
+
resolveGroup(group, record.result, source);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
if (record.status === 'failed') {
|
|
265
|
+
rejectGroup(
|
|
266
|
+
group,
|
|
267
|
+
record.error ??
|
|
268
|
+
new Error(
|
|
269
|
+
`Tool Result Receipt ${group.resultReceiptKey} failed.`,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (isOwned(record, ownerRunId)) {
|
|
275
|
+
ownGroup(group, record);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const claimGroups = async (targets: Group[], reclaimRunning: boolean) => {
|
|
282
|
+
if (targets.length === 0)
|
|
283
|
+
return new Map<string, ToolCallReceiptRecord>();
|
|
284
|
+
const response = await input.store.claim({
|
|
285
|
+
ownerRunId,
|
|
286
|
+
ownerAttempt: owner.attempt,
|
|
287
|
+
receipts: targets.map((group) => ({
|
|
288
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
289
|
+
force: group.force,
|
|
290
|
+
forceFailedRefresh: group.forceFailedRefresh,
|
|
291
|
+
reclaimRunning,
|
|
292
|
+
})),
|
|
293
|
+
});
|
|
294
|
+
return byKey(response);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// A forced refresh must not overlap an active foreign lease. Other
|
|
298
|
+
// requests go straight to the atomic bulk claim, matching the existing
|
|
299
|
+
// gateway protocol.
|
|
300
|
+
const forceGroups = [...groups.values()].filter((group) => group.force);
|
|
301
|
+
const forceExisting = forceGroups.length
|
|
302
|
+
? byKey(
|
|
303
|
+
await input.store.read({
|
|
304
|
+
resultReceiptKeys: forceGroups.map(
|
|
305
|
+
(group) => group.resultReceiptKey,
|
|
306
|
+
),
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
: new Map<string, ToolCallReceiptRecord>();
|
|
310
|
+
const waitGroups: Group[] = [];
|
|
311
|
+
const initialClaimGroups: Group[] = [];
|
|
312
|
+
for (const group of groups.values()) {
|
|
313
|
+
const existing = forceExisting.get(group.resultReceiptKey);
|
|
314
|
+
if (group.force && existing && isLiveLeased(existing)) {
|
|
315
|
+
waitGroups.push(group);
|
|
316
|
+
} else {
|
|
317
|
+
initialClaimGroups.push(group);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const claims = await claimGroups(initialClaimGroups, false);
|
|
321
|
+
for (const group of initialClaimGroups) {
|
|
322
|
+
if (!applyClaim(group, claims.get(group.resultReceiptKey), 'cache')) {
|
|
323
|
+
waitGroups.push(group);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const waitAttempts = new Map(
|
|
328
|
+
waitGroups.map((group) => [
|
|
329
|
+
group.resultReceiptKey,
|
|
330
|
+
Math.max(
|
|
331
|
+
1,
|
|
332
|
+
...group.members.map((member) => member.waitMaxAttempts ?? 240),
|
|
333
|
+
),
|
|
334
|
+
]),
|
|
335
|
+
);
|
|
336
|
+
for (const group of waitGroups) {
|
|
337
|
+
states.set(group.members[0]!.memberId, {
|
|
338
|
+
kind: 'recovering',
|
|
339
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
340
|
+
result: deferredFor(group, resultDeferred).promise,
|
|
341
|
+
});
|
|
342
|
+
for (const member of group.members.slice(1)) {
|
|
343
|
+
states.set(member.memberId, {
|
|
344
|
+
kind: 'recovering',
|
|
345
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
346
|
+
result: deferredFor(group, resultDeferred).promise,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const settleWaitGroups = async (): Promise<void> => {
|
|
352
|
+
const pending = new Map(
|
|
353
|
+
waitGroups.map((group) => [group.resultReceiptKey, group]),
|
|
354
|
+
);
|
|
355
|
+
try {
|
|
356
|
+
const maxAttempts = Math.max(0, ...waitAttempts.values());
|
|
357
|
+
for (
|
|
358
|
+
let attempt = 0;
|
|
359
|
+
attempt < maxAttempts && pending.size;
|
|
360
|
+
attempt += 1
|
|
361
|
+
) {
|
|
362
|
+
if (attempt > 0) await sleep(waitDelayMs);
|
|
363
|
+
const targets = [...pending.values()].filter(
|
|
364
|
+
(group) =>
|
|
365
|
+
attempt < (waitAttempts.get(group.resultReceiptKey) ?? 0),
|
|
366
|
+
);
|
|
367
|
+
if (targets.length === 0) continue;
|
|
368
|
+
// The context Adapter chunks this one vector sequentially. This is
|
|
369
|
+
// the bounded hydration lane: never turn an existing map into N
|
|
370
|
+
// per-row reads just because each member has its own recovery.
|
|
371
|
+
const latest = byKey(
|
|
372
|
+
await input.store.read({
|
|
373
|
+
resultReceiptKeys: targets.map(
|
|
374
|
+
(group) => group.resultReceiptKey,
|
|
375
|
+
),
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
for (const group of targets) {
|
|
379
|
+
const record = latest.get(group.resultReceiptKey);
|
|
380
|
+
// A forced caller cannot reuse the foreign terminal result, but
|
|
381
|
+
// it also must not spend its whole recovery window polling a
|
|
382
|
+
// receipt that is no longer live. Claim a fresh forced lease as
|
|
383
|
+
// soon as the observed owner has finished (or the row vanished).
|
|
384
|
+
// Waiting until the final attempt strands the map resolver for
|
|
385
|
+
// the entire bounded-recovery budget after a perfectly healthy
|
|
386
|
+
// first call completes.
|
|
387
|
+
if (group.force) {
|
|
388
|
+
if (!record || !isLiveLeased(record)) {
|
|
389
|
+
const claimed = await claimGroups([group], true);
|
|
390
|
+
if (
|
|
391
|
+
applyClaim(
|
|
392
|
+
group,
|
|
393
|
+
claimed.get(group.resultReceiptKey),
|
|
394
|
+
'cache',
|
|
395
|
+
)
|
|
396
|
+
) {
|
|
397
|
+
pending.delete(group.resultReceiptKey);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (!record) continue;
|
|
403
|
+
if (isCompleted(record)) {
|
|
404
|
+
resolveGroup(group, record.result, 'in_flight');
|
|
405
|
+
pending.delete(group.resultReceiptKey);
|
|
406
|
+
} else if (record.status === 'failed') {
|
|
407
|
+
rejectGroup(
|
|
408
|
+
group,
|
|
409
|
+
record.error ??
|
|
410
|
+
new Error(
|
|
411
|
+
`Tool Result Receipt ${group.resultReceiptKey} failed.`,
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
pending.delete(group.resultReceiptKey);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (pending.size === 0) return;
|
|
419
|
+
const unresolved = [...pending.values()];
|
|
420
|
+
const reclaimed = await claimGroups(unresolved, true);
|
|
421
|
+
for (const group of unresolved) {
|
|
422
|
+
if (
|
|
423
|
+
applyClaim(group, reclaimed.get(group.resultReceiptKey), 'cache')
|
|
424
|
+
) {
|
|
425
|
+
pending.delete(group.resultReceiptKey);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const error = new ToolCallReceiptCohortWaitTimeoutError(
|
|
429
|
+
group.resultReceiptKey,
|
|
430
|
+
);
|
|
431
|
+
rejectGroup(group, error);
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
for (const group of pending.values()) rejectGroup(group, error);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
const settlement = settleWaitGroups();
|
|
440
|
+
// An eager caller deliberately awaits this after independently owned
|
|
441
|
+
// work settles. Keep a rejection observed meanwhile so a prompt adapter
|
|
442
|
+
// read failure cannot surface as an unhandled rejection.
|
|
443
|
+
void settlement.catch(() => undefined);
|
|
444
|
+
if (!eager) await settlement;
|
|
445
|
+
|
|
446
|
+
const assertOwned = () => {
|
|
447
|
+
if (leaseLost) throw leaseLost;
|
|
448
|
+
for (const group of owned.values()) {
|
|
449
|
+
const state = states.get(group.group.members[0]!.memberId);
|
|
450
|
+
if (
|
|
451
|
+
!state ||
|
|
452
|
+
state.kind !== 'owned' ||
|
|
453
|
+
state.lease.leaseId !== group.lease.leaseId
|
|
454
|
+
) {
|
|
455
|
+
throw new ToolCallReceiptLeaseLostError(
|
|
456
|
+
group.group.resultReceiptKey,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const heartbeat = async (): Promise<'active' | 'terminal'> => {
|
|
463
|
+
if (owned.size === 0) return 'terminal';
|
|
464
|
+
const current = [...owned.values()];
|
|
465
|
+
const response = byKey(
|
|
466
|
+
await input.store.heartbeat({
|
|
467
|
+
ownerRunId,
|
|
468
|
+
ownerAttempt: owner.attempt,
|
|
469
|
+
receipts: current.map(({ group, lease }) => ({
|
|
470
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
471
|
+
lease,
|
|
472
|
+
})),
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
let active = false;
|
|
476
|
+
for (const groupOwned of current) {
|
|
477
|
+
const record = response.get(groupOwned.group.resultReceiptKey);
|
|
478
|
+
if (record && isCompleted(record)) {
|
|
479
|
+
owned.delete(groupOwned.group.resultReceiptKey);
|
|
480
|
+
resolveGroup(groupOwned.group, record.result, 'in_flight');
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (record?.status === 'failed') {
|
|
484
|
+
owned.delete(groupOwned.group.resultReceiptKey);
|
|
485
|
+
rejectGroup(
|
|
486
|
+
groupOwned.group,
|
|
487
|
+
record.error ??
|
|
488
|
+
new Error(
|
|
489
|
+
`Tool Result Receipt ${groupOwned.group.resultReceiptKey} failed.`,
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (!record || !isSameLease(record, groupOwned.lease)) {
|
|
495
|
+
leaseLost ??= new ToolCallReceiptLeaseLostError(
|
|
496
|
+
groupOwned.group.resultReceiptKey,
|
|
497
|
+
);
|
|
498
|
+
throw leaseLost;
|
|
499
|
+
}
|
|
500
|
+
active = true;
|
|
501
|
+
}
|
|
502
|
+
return active ? 'active' : 'terminal';
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const complete = async (
|
|
506
|
+
entries: Array<{ memberId: string; result: ToolExecuteResult }>,
|
|
507
|
+
) => {
|
|
508
|
+
const requested = ownedEntries(entries, states, owned);
|
|
509
|
+
if (requested.length === 0) return new Map<string, ToolExecuteResult>();
|
|
510
|
+
const response = byKey(
|
|
511
|
+
await input.store.complete({
|
|
512
|
+
ownerRunId,
|
|
513
|
+
ownerAttempt: owner.attempt,
|
|
514
|
+
receipts: requested.map(({ group, lease, result }) => ({
|
|
515
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
516
|
+
lease,
|
|
517
|
+
result,
|
|
518
|
+
})),
|
|
519
|
+
}),
|
|
520
|
+
);
|
|
521
|
+
const results = new Map<string, ToolExecuteResult>();
|
|
522
|
+
for (const entry of requested) {
|
|
523
|
+
const persisted = response.get(entry.group.resultReceiptKey);
|
|
524
|
+
// Completion gateways are allowed to return a compact terminal
|
|
525
|
+
// acknowledgement after durably storing the result. That response
|
|
526
|
+
// is represented as a `no_result` wrapper by the adapter; it is not
|
|
527
|
+
// the provider result and must not replace this invocation's live
|
|
528
|
+
// value. A real no-result provider response is identical to the
|
|
529
|
+
// submitted entry, so retaining the entry is correct there too.
|
|
530
|
+
const result =
|
|
531
|
+
isCompleted(persisted) && persisted.result.status !== 'no_result'
|
|
532
|
+
? persisted.result
|
|
533
|
+
: entry.result;
|
|
534
|
+
owned.delete(entry.group.resultReceiptKey);
|
|
535
|
+
resolveGroup(entry.group, result, 'owner');
|
|
536
|
+
for (const member of entry.group.members)
|
|
537
|
+
results.set(member.memberId, result);
|
|
538
|
+
}
|
|
539
|
+
return results;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const fail = async (
|
|
543
|
+
entries: Array<{ memberId: string; error: unknown }>,
|
|
544
|
+
) => {
|
|
545
|
+
const requested = ownedEntries(entries, states, owned);
|
|
546
|
+
if (requested.length === 0) return;
|
|
547
|
+
await input.store.fail({
|
|
548
|
+
ownerRunId,
|
|
549
|
+
ownerAttempt: owner.attempt,
|
|
550
|
+
receipts: requested.map(({ group, lease, error }) => ({
|
|
551
|
+
resultReceiptKey: group.resultReceiptKey,
|
|
552
|
+
lease,
|
|
553
|
+
error,
|
|
554
|
+
})),
|
|
555
|
+
});
|
|
556
|
+
for (const entry of requested) {
|
|
557
|
+
owned.delete(entry.group.resultReceiptKey);
|
|
558
|
+
rejectGroup(entry.group, entry.error);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const flushCompletions = async (): Promise<void> => {
|
|
563
|
+
completionFlushScheduled = false;
|
|
564
|
+
const entries = queuedCompletions.splice(0);
|
|
565
|
+
if (entries.length === 0) return;
|
|
566
|
+
try {
|
|
567
|
+
const results = await complete(
|
|
568
|
+
entries.map(({ memberId, result }) => ({ memberId, result })),
|
|
569
|
+
);
|
|
570
|
+
for (const entry of entries) {
|
|
571
|
+
entry.resolve(results.get(entry.memberId) ?? entry.result);
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
for (const entry of entries) entry.reject(error);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const flushFailures = async (): Promise<void> => {
|
|
578
|
+
failureFlushScheduled = false;
|
|
579
|
+
const entries = queuedFailures.splice(0);
|
|
580
|
+
if (entries.length === 0) return;
|
|
581
|
+
try {
|
|
582
|
+
await fail(
|
|
583
|
+
entries.map(({ memberId, error }) => ({ memberId, error })),
|
|
584
|
+
);
|
|
585
|
+
for (const entry of entries) entry.resolve();
|
|
586
|
+
} catch (error) {
|
|
587
|
+
for (const entry of entries) entry.reject(error);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
function receiptView(
|
|
592
|
+
memberId: string,
|
|
593
|
+
groupOwned: OwnedGroup,
|
|
594
|
+
): ToolResultReceipts {
|
|
595
|
+
const { group, lease } = groupOwned;
|
|
596
|
+
return {
|
|
597
|
+
async claim(candidate) {
|
|
598
|
+
if (
|
|
599
|
+
candidate.resultReceiptKey !== group.resultReceiptKey ||
|
|
600
|
+
candidate.ownerRunId !== ownerRunId
|
|
601
|
+
) {
|
|
602
|
+
throw new ToolCallReceiptLeaseLostError(
|
|
603
|
+
candidate.resultReceiptKey,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return { kind: 'owned', lease };
|
|
607
|
+
},
|
|
608
|
+
async recover(candidate) {
|
|
609
|
+
if (
|
|
610
|
+
candidate.resultReceiptKey !== group.resultReceiptKey ||
|
|
611
|
+
candidate.ownerRunId !== ownerRunId
|
|
612
|
+
) {
|
|
613
|
+
throw new ToolCallReceiptLeaseLostError(
|
|
614
|
+
candidate.resultReceiptKey,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return await deferredFor(group, resultDeferred).promise;
|
|
618
|
+
},
|
|
619
|
+
async heartbeat(candidate) {
|
|
620
|
+
if (
|
|
621
|
+
candidate.resultReceiptKey !== group.resultReceiptKey ||
|
|
622
|
+
!sameLease(candidate.lease, lease)
|
|
623
|
+
)
|
|
624
|
+
return 'lost';
|
|
625
|
+
try {
|
|
626
|
+
assertOwned();
|
|
627
|
+
// Preserve the per-provider-admission fence from the old map
|
|
628
|
+
// path. The cohort-wide heartbeat keeps queued work alive;
|
|
629
|
+
// this one-member check closes the exact dispatch window.
|
|
630
|
+
const record = byKey(
|
|
631
|
+
await input.store.heartbeat({
|
|
632
|
+
ownerRunId,
|
|
633
|
+
ownerAttempt: owner.attempt,
|
|
634
|
+
receipts: [
|
|
635
|
+
{ resultReceiptKey: group.resultReceiptKey, lease },
|
|
636
|
+
],
|
|
637
|
+
}),
|
|
638
|
+
).get(group.resultReceiptKey);
|
|
639
|
+
if (record && isCompleted(record)) {
|
|
640
|
+
owned.delete(group.resultReceiptKey);
|
|
641
|
+
resolveGroup(group, record.result, 'in_flight');
|
|
642
|
+
return 'completed';
|
|
643
|
+
}
|
|
644
|
+
if (!record || !isSameLease(record, lease)) return 'lost';
|
|
645
|
+
return 'active';
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (error instanceof ToolCallReceiptLeaseLostError) return 'lost';
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
async complete(candidate) {
|
|
652
|
+
if (
|
|
653
|
+
candidate.resultReceiptKey !== group.resultReceiptKey ||
|
|
654
|
+
!sameLease(candidate.lease, lease)
|
|
655
|
+
) {
|
|
656
|
+
throw new ToolCallReceiptLeaseLostError(
|
|
657
|
+
candidate.resultReceiptKey,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const result = await new Promise<ToolExecuteResult>(
|
|
661
|
+
(resolve, reject) => {
|
|
662
|
+
queuedCompletions.push({
|
|
663
|
+
memberId,
|
|
664
|
+
result: candidate.result,
|
|
665
|
+
resolve,
|
|
666
|
+
reject,
|
|
667
|
+
});
|
|
668
|
+
if (!completionFlushScheduled) {
|
|
669
|
+
completionFlushScheduled = true;
|
|
670
|
+
setTimeout(() => void flushCompletions(), 0);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
return { kind: 'stored', result };
|
|
675
|
+
},
|
|
676
|
+
async fail(candidate) {
|
|
677
|
+
if (
|
|
678
|
+
candidate.resultReceiptKey !== group.resultReceiptKey ||
|
|
679
|
+
!sameLease(candidate.lease, lease)
|
|
680
|
+
) {
|
|
681
|
+
throw new ToolCallReceiptLeaseLostError(
|
|
682
|
+
candidate.resultReceiptKey,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
await new Promise<void>((resolve, reject) => {
|
|
686
|
+
queuedFailures.push({
|
|
687
|
+
memberId,
|
|
688
|
+
error: candidate.error,
|
|
689
|
+
resolve,
|
|
690
|
+
reject,
|
|
691
|
+
});
|
|
692
|
+
if (!failureFlushScheduled) {
|
|
693
|
+
failureFlushScheduled = true;
|
|
694
|
+
setTimeout(() => void flushFailures(), 0);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
member(memberId) {
|
|
703
|
+
const state = states.get(memberId);
|
|
704
|
+
if (!state)
|
|
705
|
+
throw new Error(
|
|
706
|
+
`Unknown Tool Call Receipt Cohort member ${memberId}.`,
|
|
707
|
+
);
|
|
708
|
+
return state;
|
|
709
|
+
},
|
|
710
|
+
complete,
|
|
711
|
+
fail,
|
|
712
|
+
heartbeat,
|
|
713
|
+
startHeartbeat({ intervalMs, onLeaseLost, onTransientFailure }) {
|
|
714
|
+
const supervisor = createRuntimeReceiptHeartbeatSupervisor({
|
|
715
|
+
intervalMs: positive(intervalMs, 'heartbeat interval'),
|
|
716
|
+
heartbeat,
|
|
717
|
+
isLeaseLost: (error) =>
|
|
718
|
+
error instanceof ToolCallReceiptLeaseLostError,
|
|
719
|
+
onLeaseLost: (error) => {
|
|
720
|
+
leaseLost ??= error as ToolCallReceiptLeaseLostError;
|
|
721
|
+
onLeaseLost(leaseLost);
|
|
722
|
+
},
|
|
723
|
+
onTransientFailure,
|
|
724
|
+
});
|
|
725
|
+
supervisor.start();
|
|
726
|
+
return { stop: () => supervisor.stop(), assertOwned };
|
|
727
|
+
},
|
|
728
|
+
async settle() {
|
|
729
|
+
await settlement;
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
type Group = {
|
|
737
|
+
resultReceiptKey: string;
|
|
738
|
+
members: ToolCallReceiptCohortMember[];
|
|
739
|
+
force: boolean;
|
|
740
|
+
forceFailedRefresh: boolean;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
type OwnedGroup = { group: Group; lease: ToolCallReceiptLease };
|
|
744
|
+
|
|
745
|
+
type Deferred<T> = {
|
|
746
|
+
promise: Promise<T>;
|
|
747
|
+
resolve(value: T): void;
|
|
748
|
+
reject(error: unknown): void;
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
function groupMembers(
|
|
752
|
+
members: ToolCallReceiptCohortMember[],
|
|
753
|
+
): Map<string, Group> {
|
|
754
|
+
const groups = new Map<string, Group>();
|
|
755
|
+
const memberIds = new Set<string>();
|
|
756
|
+
for (const member of members) {
|
|
757
|
+
const memberId = required(member.memberId, 'member id');
|
|
758
|
+
if (memberIds.has(memberId))
|
|
759
|
+
throw new Error(`Duplicate Tool Call Receipt Cohort member ${memberId}.`);
|
|
760
|
+
memberIds.add(memberId);
|
|
761
|
+
const resultReceiptKey = required(
|
|
762
|
+
member.resultReceiptKey,
|
|
763
|
+
'result receipt key',
|
|
764
|
+
);
|
|
765
|
+
const group = groups.get(resultReceiptKey) ?? {
|
|
766
|
+
resultReceiptKey,
|
|
767
|
+
members: [],
|
|
768
|
+
force: false,
|
|
769
|
+
forceFailedRefresh: false,
|
|
770
|
+
};
|
|
771
|
+
group.members.push({ ...member, memberId, resultReceiptKey });
|
|
772
|
+
group.force ||= member.force === true;
|
|
773
|
+
group.forceFailedRefresh ||= member.forceFailedRefresh === true;
|
|
774
|
+
groups.set(resultReceiptKey, group);
|
|
775
|
+
}
|
|
776
|
+
for (const group of groups.values()) {
|
|
777
|
+
// Full force dominates failed-only refresh for every duplicate member of
|
|
778
|
+
// the same content-addressed receipt, just as the current map grouping.
|
|
779
|
+
if (group.force) group.forceFailedRefresh = false;
|
|
780
|
+
}
|
|
781
|
+
return groups;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function deferredFor(
|
|
785
|
+
group: Group,
|
|
786
|
+
deferred: Map<string, Deferred<ToolExecuteResult>>,
|
|
787
|
+
): Deferred<ToolExecuteResult> {
|
|
788
|
+
let value = deferred.get(group.resultReceiptKey);
|
|
789
|
+
if (value) return value;
|
|
790
|
+
let resolve!: (result: ToolExecuteResult) => void;
|
|
791
|
+
let reject!: (error: unknown) => void;
|
|
792
|
+
const promise = new Promise<ToolExecuteResult>(
|
|
793
|
+
(resolvePromise, rejectPromise) => {
|
|
794
|
+
resolve = resolvePromise;
|
|
795
|
+
reject = rejectPromise;
|
|
796
|
+
},
|
|
797
|
+
);
|
|
798
|
+
// A receipt can fail before a same-run follower attaches. Do not create an
|
|
799
|
+
// unhandled rejection while preserving the rejection for real followers.
|
|
800
|
+
void promise.catch(() => undefined);
|
|
801
|
+
value = { promise, resolve, reject };
|
|
802
|
+
deferred.set(group.resultReceiptKey, value);
|
|
803
|
+
return value;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function ownedEntries<T extends { memberId: string }>(
|
|
807
|
+
entries: T[],
|
|
808
|
+
states: Map<string, ToolCallReceiptCohortMemberState>,
|
|
809
|
+
owned: Map<string, OwnedGroup>,
|
|
810
|
+
): Array<T & OwnedGroup> {
|
|
811
|
+
const byKey = new Map<string, T & OwnedGroup>();
|
|
812
|
+
for (const entry of entries) {
|
|
813
|
+
const state = states.get(entry.memberId);
|
|
814
|
+
if (!state || state.kind !== 'owned') {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`Tool Call Receipt Cohort member ${entry.memberId} does not own a receipt.`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const groupOwned = owned.get(state.resultReceiptKey);
|
|
820
|
+
if (!groupOwned || !sameLease(groupOwned.lease, state.lease)) {
|
|
821
|
+
throw new ToolCallReceiptLeaseLostError(state.resultReceiptKey);
|
|
822
|
+
}
|
|
823
|
+
const existing = byKey.get(groupOwned.group.resultReceiptKey);
|
|
824
|
+
if (existing && existing.memberId !== entry.memberId) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
`Tool Call Receipt Cohort received two terminal outcomes for ${groupOwned.group.resultReceiptKey}.`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
byKey.set(groupOwned.group.resultReceiptKey, { ...entry, ...groupOwned });
|
|
830
|
+
}
|
|
831
|
+
return [...byKey.values()];
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function byKey(
|
|
835
|
+
records: ToolCallReceiptRecord[],
|
|
836
|
+
): Map<string, ToolCallReceiptRecord> {
|
|
837
|
+
const result = new Map<string, ToolCallReceiptRecord>();
|
|
838
|
+
for (const record of records) result.set(record.resultReceiptKey, record);
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function isCompleted(
|
|
843
|
+
record: ToolCallReceiptRecord | undefined,
|
|
844
|
+
): record is ToolCallReceiptRecord & { result: ToolExecuteResult } {
|
|
845
|
+
if (!record) return false;
|
|
846
|
+
return (
|
|
847
|
+
(record.status === 'completed' || record.status === 'skipped') &&
|
|
848
|
+
record.result !== undefined
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function isOwned(record: ToolCallReceiptRecord, ownerRunId: string): boolean {
|
|
853
|
+
return (
|
|
854
|
+
record.claimState !== 'existing' &&
|
|
855
|
+
Boolean(record.lease) &&
|
|
856
|
+
record.lease?.ownerRunId === ownerRunId &&
|
|
857
|
+
(record.status === 'queued' ||
|
|
858
|
+
record.status === 'pending' ||
|
|
859
|
+
record.status === 'running')
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function isLiveLeased(record: ToolCallReceiptRecord): boolean {
|
|
864
|
+
if (
|
|
865
|
+
record.status !== 'queued' &&
|
|
866
|
+
record.status !== 'pending' &&
|
|
867
|
+
record.status !== 'running'
|
|
868
|
+
)
|
|
869
|
+
return false;
|
|
870
|
+
if (!record.lease) return false;
|
|
871
|
+
const expiresAt = Date.parse(record.leaseExpiresAt ?? '');
|
|
872
|
+
return !Number.isFinite(expiresAt) || expiresAt > Date.now();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function isSameLease(
|
|
876
|
+
record: ToolCallReceiptRecord,
|
|
877
|
+
lease: ToolCallReceiptLease,
|
|
878
|
+
): boolean {
|
|
879
|
+
return Boolean(record.lease) && sameLease(record.lease!, lease);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function sameLease(
|
|
883
|
+
left: ToolCallReceiptLease,
|
|
884
|
+
right: ToolCallReceiptLease,
|
|
885
|
+
): boolean {
|
|
886
|
+
return left.leaseId === right.leaseId && left.ownerRunId === right.ownerRunId;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function required(value: string, name: string): string {
|
|
890
|
+
const normalized = value?.trim();
|
|
891
|
+
if (!normalized)
|
|
892
|
+
throw new Error(`Tool Call Receipt Cohort requires a ${name}.`);
|
|
893
|
+
return normalized;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function positive(value: number, name: string): number {
|
|
897
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
898
|
+
throw new Error(`Tool Call Receipt Cohort ${name} must be positive.`);
|
|
899
|
+
return Math.floor(value);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function defaultSleep(milliseconds: number): Promise<void> {
|
|
903
|
+
await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
|
|
904
|
+
}
|