deepline 0.3.44 → 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/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,291 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ToolCallDependencies,
|
|
3
|
+
ToolCallInput,
|
|
4
|
+
ToolCallJob,
|
|
5
|
+
ToolCallReceiptLease,
|
|
6
|
+
ToolCallResultFormat,
|
|
7
|
+
ToolResultReceipts,
|
|
8
|
+
} from './contract';
|
|
9
|
+
import { ToolCallReceiptLeaseLostError } from './contract';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
createRuntimeStepToolResultReceipts,
|
|
13
|
+
type RuntimeStepToolResultReceiptStore,
|
|
14
|
+
type RuntimeStepToolResultReceiptsInput,
|
|
15
|
+
} from './runtime-step-receipts-adapter';
|
|
16
|
+
export {
|
|
17
|
+
createToolCallReceiptCohortJob,
|
|
18
|
+
ToolCallReceiptCohortWaitTimeoutError,
|
|
19
|
+
type ToolCallReceiptCohort,
|
|
20
|
+
type ToolCallReceiptCohortJob,
|
|
21
|
+
type ToolCallReceiptCohortMember,
|
|
22
|
+
type ToolCallReceiptCohortMemberState,
|
|
23
|
+
type ToolCallReceiptCohortStore,
|
|
24
|
+
type ToolCallReceiptRecord,
|
|
25
|
+
} from './receipt-cohort';
|
|
26
|
+
export {
|
|
27
|
+
createNativeBatchToolCallJob,
|
|
28
|
+
type NativeBatchOwnedToolCallMember,
|
|
29
|
+
type NativeBatchToolCallInput,
|
|
30
|
+
type NativeBatchToolCallJob,
|
|
31
|
+
type NativeBatchToolCallMember,
|
|
32
|
+
} from './native-batch';
|
|
33
|
+
export {
|
|
34
|
+
createAlwaysFreshToolCallAdapter,
|
|
35
|
+
type AlwaysFreshToolCall,
|
|
36
|
+
type AlwaysFreshToolCallInput,
|
|
37
|
+
} from './always-fresh-adapter';
|
|
38
|
+
|
|
39
|
+
export type {
|
|
40
|
+
ToolCallCompatibilityIdentity,
|
|
41
|
+
ToolCallDependencies,
|
|
42
|
+
ToolCallDispatcher,
|
|
43
|
+
ToolCallInput,
|
|
44
|
+
ToolCallJob,
|
|
45
|
+
ToolCallReceiptLease,
|
|
46
|
+
ToolCallReceiptRecoverySource,
|
|
47
|
+
ToolCallResultFormat,
|
|
48
|
+
ToolCallPersistence,
|
|
49
|
+
ToolResultReceipts,
|
|
50
|
+
} from './contract';
|
|
51
|
+
export { ToolCallReceiptLeaseLostError } from './contract';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Adapts a receipt lifecycle that is already owned by a higher-level runtime
|
|
55
|
+
* job. This is a composition seam, not a second receipt implementation.
|
|
56
|
+
*
|
|
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.
|
|
63
|
+
*
|
|
64
|
+
* Do not use this for a new caller. New runtimes should provide a real
|
|
65
|
+
* ToolResultReceipts Adapter. This exists only while PlayContext's historical
|
|
66
|
+
* receipt state machine is being migrated behind the Tool Call Module.
|
|
67
|
+
*/
|
|
68
|
+
export function createExternallyOwnedToolResultReceipts(input: {
|
|
69
|
+
resultReceiptKey: string;
|
|
70
|
+
ownerRunId: string;
|
|
71
|
+
leaseId?: string | null;
|
|
72
|
+
}): ToolResultReceipts {
|
|
73
|
+
const resultReceiptKey = input.resultReceiptKey.trim();
|
|
74
|
+
const ownerRunId = input.ownerRunId.trim();
|
|
75
|
+
if (!resultReceiptKey) {
|
|
76
|
+
throw new Error('External Tool Result Receipt needs a receipt key.');
|
|
77
|
+
}
|
|
78
|
+
if (!ownerRunId) {
|
|
79
|
+
throw new Error('External Tool Result Receipt needs an owner run id.');
|
|
80
|
+
}
|
|
81
|
+
// A completed-cache-only receipt has no ordinary lease. It is still owned
|
|
82
|
+
// by the outer operation for this invocation, so give ToolCall a local
|
|
83
|
+
// identity solely for its in-process contract checks.
|
|
84
|
+
const lease = {
|
|
85
|
+
leaseId: input.leaseId?.trim() || 'outer-runtime-owner',
|
|
86
|
+
ownerRunId,
|
|
87
|
+
};
|
|
88
|
+
const assertKeyAndLease = (candidate: {
|
|
89
|
+
resultReceiptKey: string;
|
|
90
|
+
lease?: ToolCallReceiptLease;
|
|
91
|
+
}): void => {
|
|
92
|
+
if (candidate.resultReceiptKey !== resultReceiptKey) {
|
|
93
|
+
throw new Error('ToolCall used a receipt outside its owned boundary.');
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
candidate.lease &&
|
|
97
|
+
(candidate.lease.leaseId !== lease.leaseId ||
|
|
98
|
+
candidate.lease.ownerRunId !== lease.ownerRunId)
|
|
99
|
+
) {
|
|
100
|
+
throw new ToolCallReceiptLeaseLostError(resultReceiptKey);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
async claim(candidate) {
|
|
106
|
+
assertKeyAndLease({ resultReceiptKey: candidate.resultReceiptKey });
|
|
107
|
+
if (candidate.ownerRunId !== ownerRunId) {
|
|
108
|
+
throw new ToolCallReceiptLeaseLostError(resultReceiptKey);
|
|
109
|
+
}
|
|
110
|
+
return { kind: 'owned', lease };
|
|
111
|
+
},
|
|
112
|
+
async recover(candidate) {
|
|
113
|
+
assertKeyAndLease({ resultReceiptKey: candidate.resultReceiptKey });
|
|
114
|
+
throw new Error(
|
|
115
|
+
'An externally owned Tool Result Receipt cannot recover through ToolCall.',
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
async heartbeat(candidate) {
|
|
119
|
+
assertKeyAndLease(candidate);
|
|
120
|
+
// The outer runtime owns durable heartbeating / execution locks. Returning
|
|
121
|
+
// active here deliberately avoids a duplicate heartbeat write.
|
|
122
|
+
return 'active';
|
|
123
|
+
},
|
|
124
|
+
async complete(candidate) {
|
|
125
|
+
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
|
+
return { kind: 'stored', result: candidate.result };
|
|
130
|
+
},
|
|
131
|
+
async fail(candidate) {
|
|
132
|
+
assertKeyAndLease(candidate);
|
|
133
|
+
// Failure publication belongs to the outer state machine because it
|
|
134
|
+
// classifies auth-scope changes, suspensions, and lost ownership.
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const RESULT_FORMATS = new Set<ToolCallResultFormat>([
|
|
140
|
+
'legacy',
|
|
141
|
+
'v2',
|
|
142
|
+
'raw-v2',
|
|
143
|
+
]);
|
|
144
|
+
const REVISION_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates the runner-portable Tool Call job.
|
|
148
|
+
*
|
|
149
|
+
* The job owns the observable receipt lifecycle. The supplied runtime Adapter
|
|
150
|
+
* retains the current durable result/provider keys during the migration, and
|
|
151
|
+
* chooses how a direct call or a map batch reaches the provider.
|
|
152
|
+
*/
|
|
153
|
+
export function createToolCallJob(
|
|
154
|
+
dependencies: ToolCallDependencies,
|
|
155
|
+
): ToolCallJob {
|
|
156
|
+
return {
|
|
157
|
+
async call(input: ToolCallInput) {
|
|
158
|
+
validateToolCall(input);
|
|
159
|
+
if (isAlwaysFresh(input)) {
|
|
160
|
+
return await dependencies.dispatcher.dispatch({
|
|
161
|
+
call: input,
|
|
162
|
+
providerOperationKey: null,
|
|
163
|
+
receipt: null,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const compatibility = input.compatibility;
|
|
168
|
+
if (!compatibility) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'Receipt-backed Tool Call requires compatibility identities.',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const resultReceipts = dependencies.resultReceipts;
|
|
174
|
+
if (!resultReceipts) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
'Receipt-backed Tool Call requires a Tool Result Receipt Adapter.',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const receiptKey = compatibility.resultReceiptKey;
|
|
180
|
+
const claim = await resultReceipts.claim({
|
|
181
|
+
resultReceiptKey: receiptKey,
|
|
182
|
+
ownerRunId: input.execution.ownerRunId,
|
|
183
|
+
force: input.execution.force === true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (claim.kind === 'completed') return claim.result;
|
|
187
|
+
if (claim.kind === 'following') {
|
|
188
|
+
return await resultReceipts.recover({
|
|
189
|
+
resultReceiptKey: receiptKey,
|
|
190
|
+
ownerRunId: input.execution.ownerRunId,
|
|
191
|
+
source: 'in_flight',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const heartbeat = await resultReceipts.heartbeat({
|
|
196
|
+
resultReceiptKey: receiptKey,
|
|
197
|
+
lease: claim.lease,
|
|
198
|
+
});
|
|
199
|
+
if (heartbeat === 'completed') {
|
|
200
|
+
return await resultReceipts.recover({
|
|
201
|
+
resultReceiptKey: receiptKey,
|
|
202
|
+
ownerRunId: input.execution.ownerRunId,
|
|
203
|
+
source: 'in_flight',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (heartbeat === 'lost') {
|
|
207
|
+
throw new ToolCallReceiptLeaseLostError(receiptKey);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await dependencies.dispatcher.dispatch({
|
|
213
|
+
call: input,
|
|
214
|
+
providerOperationKey: compatibility.providerOperationKey,
|
|
215
|
+
receipt: claim.lease,
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Preserve the provider/transport error. A best-effort failure write
|
|
219
|
+
// records the lifecycle fact but must not hide the caller-visible one.
|
|
220
|
+
await resultReceipts
|
|
221
|
+
.fail({
|
|
222
|
+
resultReceiptKey: receiptKey,
|
|
223
|
+
lease: claim.lease,
|
|
224
|
+
error,
|
|
225
|
+
})
|
|
226
|
+
.catch(() => undefined);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const completed = await resultReceipts.complete({
|
|
231
|
+
resultReceiptKey: receiptKey,
|
|
232
|
+
lease: claim.lease,
|
|
233
|
+
result,
|
|
234
|
+
});
|
|
235
|
+
return completed.result;
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validateToolCall(input: ToolCallInput): void {
|
|
241
|
+
required(input.scope.organizationId, 'organizationId');
|
|
242
|
+
required(input.scope.playScope, 'playScope');
|
|
243
|
+
required(input.tool.id, 'tool id');
|
|
244
|
+
// Historical artifacts can legitimately have no separately discoverable
|
|
245
|
+
// action-version string. During the compatibility-key migration their
|
|
246
|
+
// already-derived providerOperationKey remains authoritative; rejecting the
|
|
247
|
+
// call here would turn a previously valid legacy tool call into a runtime
|
|
248
|
+
// failure without improving identity safety.
|
|
249
|
+
required(input.output.intent, 'output intent');
|
|
250
|
+
required(input.execution.ownerRunId, 'owner run id');
|
|
251
|
+
if (isAlwaysFresh(input)) {
|
|
252
|
+
if (input.compatibility !== undefined) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
'Always-fresh Tool Call must not carry receipt or provider-operation identities.',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
if (!input.compatibility) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'Receipt-backed Tool Call requires compatibility identities.',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
required(input.compatibility.resultReceiptKey, 'result receipt key');
|
|
264
|
+
required(
|
|
265
|
+
input.compatibility.providerOperationKey,
|
|
266
|
+
'provider operation key',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (!RESULT_FORMATS.has(input.output.format)) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Unsupported Tool Call result format ${String(input.output.format)}.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const revision = input.output.revision;
|
|
275
|
+
if (
|
|
276
|
+
revision != null &&
|
|
277
|
+
(typeof revision !== 'string' || !REVISION_PATTERN.test(revision))
|
|
278
|
+
) {
|
|
279
|
+
throw new Error('Tool Call result revision must be a static identifier.');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isAlwaysFresh(input: ToolCallInput): boolean {
|
|
284
|
+
return input.execution.persistence === 'always-fresh';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function required(value: string, name: string): void {
|
|
288
|
+
if (!value?.trim()) {
|
|
289
|
+
throw new Error(`Tool Call requires a non-empty ${name}.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { ToolExecuteResult } from '../tool-result';
|
|
2
|
+
import {
|
|
3
|
+
ToolCallReceiptLeaseLostError,
|
|
4
|
+
type ToolCallInput,
|
|
5
|
+
type ToolCallJob,
|
|
6
|
+
type ToolCallReceiptLease,
|
|
7
|
+
} from './contract';
|
|
8
|
+
import {
|
|
9
|
+
type ToolCallReceiptCohort,
|
|
10
|
+
type ToolCallReceiptCohortJob,
|
|
11
|
+
} from './receipt-cohort';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One logical map item which may have a distinct Tool Result Receipt even
|
|
15
|
+
* though a provider-native batch will execute several items in one request.
|
|
16
|
+
*/
|
|
17
|
+
export type NativeBatchToolCallMember = {
|
|
18
|
+
memberId: string;
|
|
19
|
+
call: ToolCallInput;
|
|
20
|
+
/** Failed-only refresh is a map policy, not part of the scalar ToolCall ABI. */
|
|
21
|
+
forceFailedRefresh?: boolean;
|
|
22
|
+
waitMaxAttempts?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type NativeBatchOwnedToolCallMember = NativeBatchToolCallMember & {
|
|
26
|
+
receipt: ToolCallReceiptLease;
|
|
27
|
+
/** Actual member lease expiry supplied by the receipt-cohort Adapter. */
|
|
28
|
+
leaseExpiresAt?: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type NativeBatchToolCallInput = {
|
|
32
|
+
owner: { runId: string; attempt: number };
|
|
33
|
+
members: NativeBatchToolCallMember[];
|
|
34
|
+
/**
|
|
35
|
+
* Builds the one physical provider call from exactly the receipts this run
|
|
36
|
+
* owns. Completed and foreign members are intentionally absent: including
|
|
37
|
+
* them would turn a cache hit into a duplicate provider operation.
|
|
38
|
+
*/
|
|
39
|
+
buildPhysicalCall(input: {
|
|
40
|
+
members: NativeBatchOwnedToolCallMember[];
|
|
41
|
+
}): ToolCallInput;
|
|
42
|
+
/**
|
|
43
|
+
* Decodes one aggregate provider result into exactly one result for every
|
|
44
|
+
* owned receipt. The Module validates the correspondence before publishing
|
|
45
|
+
* any member terminal fact.
|
|
46
|
+
*/
|
|
47
|
+
splitResult(input: {
|
|
48
|
+
result: ToolExecuteResult;
|
|
49
|
+
members: NativeBatchOwnedToolCallMember[];
|
|
50
|
+
}):
|
|
51
|
+
| Array<{ memberId: string; result: ToolExecuteResult }>
|
|
52
|
+
| 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
|
+
};
|
|
62
|
+
|
|
63
|
+
/** A native batch returns each local member result, including cache followers. */
|
|
64
|
+
export type NativeBatchToolCallJob = {
|
|
65
|
+
execute(
|
|
66
|
+
input: NativeBatchToolCallInput,
|
|
67
|
+
): Promise<Map<string, ToolExecuteResult>>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Owns one provider-native map batch.
|
|
72
|
+
*
|
|
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.
|
|
79
|
+
*/
|
|
80
|
+
export function createNativeBatchToolCallJob(dependencies: {
|
|
81
|
+
receiptCohort: ToolCallReceiptCohortJob;
|
|
82
|
+
/**
|
|
83
|
+
* The aggregate call is itself a ToolCall job, normally built with the
|
|
84
|
+
* externally-owned aggregate receipt Adapter. Member receipts remain the
|
|
85
|
+
* sole durable authority for the native map's terminal results.
|
|
86
|
+
*/
|
|
87
|
+
physicalToolCall: ToolCallJob;
|
|
88
|
+
}): NativeBatchToolCallJob {
|
|
89
|
+
return {
|
|
90
|
+
async execute(input) {
|
|
91
|
+
validateInput(input);
|
|
92
|
+
const cohort = await dependencies.receiptCohort.acquire({
|
|
93
|
+
owner: input.owner,
|
|
94
|
+
members: input.members.map((member) => ({
|
|
95
|
+
memberId: member.memberId,
|
|
96
|
+
resultReceiptKey: memberReceiptKey(member),
|
|
97
|
+
force: member.call.execution.force === true,
|
|
98
|
+
forceFailedRefresh: member.forceFailedRefresh,
|
|
99
|
+
waitMaxAttempts: member.waitMaxAttempts,
|
|
100
|
+
})),
|
|
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
|
+
});
|
|
106
|
+
const executeOwned = async (): Promise<void> => {
|
|
107
|
+
const owned = ownedMembers(cohort, input.members);
|
|
108
|
+
if (owned.length === 0) return;
|
|
109
|
+
const physical = input.buildPhysicalCall({ members: owned });
|
|
110
|
+
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
|
+
try {
|
|
128
|
+
await assertCohortOwned(cohort, heartbeat, leaseLost);
|
|
129
|
+
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
|
+
}
|
|
141
|
+
await cohort.complete(split);
|
|
142
|
+
} 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
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
} finally {
|
|
151
|
+
heartbeat?.stop();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
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
|
+
await executeOwned();
|
|
164
|
+
|
|
165
|
+
return await collectMemberResults(cohort, input.members);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ownedMembers(
|
|
171
|
+
cohort: ToolCallReceiptCohort,
|
|
172
|
+
members: NativeBatchToolCallMember[],
|
|
173
|
+
): NativeBatchOwnedToolCallMember[] {
|
|
174
|
+
const owned: NativeBatchOwnedToolCallMember[] = [];
|
|
175
|
+
for (const member of members) {
|
|
176
|
+
const state = cohort.member(member.memberId);
|
|
177
|
+
if (state.kind !== 'owned') continue;
|
|
178
|
+
owned.push({
|
|
179
|
+
...member,
|
|
180
|
+
receipt: state.lease,
|
|
181
|
+
...(state.leaseExpiresAt ? { leaseExpiresAt: state.leaseExpiresAt } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return owned;
|
|
185
|
+
}
|
|
186
|
+
|
|
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
|
+
async function collectMemberResults(
|
|
197
|
+
cohort: ToolCallReceiptCohort,
|
|
198
|
+
members: NativeBatchToolCallMember[],
|
|
199
|
+
): Promise<Map<string, ToolExecuteResult>> {
|
|
200
|
+
const results = new Map<string, ToolExecuteResult>();
|
|
201
|
+
for (const member of members) {
|
|
202
|
+
const state = cohort.member(member.memberId);
|
|
203
|
+
if (state.kind === 'completed') {
|
|
204
|
+
results.set(member.memberId, state.result);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (state.kind === 'following' || state.kind === 'recovering') {
|
|
208
|
+
results.set(member.memberId, await state.result);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (state.kind === 'failed') throw state.error;
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Native Batch Tool Call left owned member ${member.memberId} without a terminal result.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
|
|
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
|
+
async function failOwned(
|
|
235
|
+
cohort: ToolCallReceiptCohort,
|
|
236
|
+
owned: NativeBatchOwnedToolCallMember[],
|
|
237
|
+
error: unknown,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
if (owned.length === 0) return;
|
|
240
|
+
await cohort
|
|
241
|
+
.fail(owned.map((member) => ({ memberId: member.memberId, error })))
|
|
242
|
+
// Preserve the provider/split failure. The cohort itself fails closed on a
|
|
243
|
+
// stale lease, but a best-effort terminal write must not conceal it.
|
|
244
|
+
.catch(() => undefined);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function validateInput(input: NativeBatchToolCallInput): void {
|
|
248
|
+
if (!input.owner.runId.trim()) {
|
|
249
|
+
throw new Error('Native Batch Tool Call requires an owner run id.');
|
|
250
|
+
}
|
|
251
|
+
if (!Number.isInteger(input.owner.attempt) || input.owner.attempt < 0) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
'Native Batch Tool Call owner attempt must be a non-negative integer.',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (input.members.length === 0) {
|
|
257
|
+
throw new Error('Native Batch Tool Call requires at least one member.');
|
|
258
|
+
}
|
|
259
|
+
const memberIds = new Set<string>();
|
|
260
|
+
for (const member of input.members) {
|
|
261
|
+
if (!member.memberId.trim()) {
|
|
262
|
+
throw new Error('Native Batch Tool Call member needs a member id.');
|
|
263
|
+
}
|
|
264
|
+
if (memberIds.has(member.memberId)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Duplicate Native Batch Tool Call member ${member.memberId}.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
memberIds.add(member.memberId);
|
|
270
|
+
if (member.call.execution.mode !== 'map') {
|
|
271
|
+
throw new Error('Native Batch Tool Call members must be map calls.');
|
|
272
|
+
}
|
|
273
|
+
if (member.call.execution.ownerRunId !== input.owner.runId) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
'Native Batch Tool Call member owner does not match the cohort.',
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
memberReceiptKey(member);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validatePhysicalCall(
|
|
283
|
+
physical: ToolCallInput,
|
|
284
|
+
owner: { runId: string },
|
|
285
|
+
): void {
|
|
286
|
+
if (physical.execution.mode !== 'map') {
|
|
287
|
+
throw new Error(
|
|
288
|
+
'Native Batch Tool Call physical operation must be a map call.',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (physical.execution.ownerRunId !== owner.runId) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
'Native Batch Tool Call physical owner does not match the cohort.',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (!physical.compatibility) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
'Native Batch Tool Call physical operation requires aggregate compatibility identities.',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function memberReceiptKey(member: NativeBatchToolCallMember): string {
|
|
304
|
+
const receiptKey = member.call.compatibility?.resultReceiptKey?.trim();
|
|
305
|
+
if (!receiptKey) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
'Native Batch Tool Call members require durable result receipt identities.',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return receiptKey;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function validateSplit(
|
|
314
|
+
owned: NativeBatchOwnedToolCallMember[],
|
|
315
|
+
split: Array<{ memberId: string; result: ToolExecuteResult }>,
|
|
316
|
+
): void {
|
|
317
|
+
const expected = new Set(owned.map((member) => member.memberId));
|
|
318
|
+
if (split.length !== expected.size) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
'Native Batch Tool Call split must yield exactly one result per owned member.',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
for (const entry of split) {
|
|
324
|
+
if (!expected.delete(entry.memberId)) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Native Batch Tool Call split returned an unknown or duplicate member ${entry.memberId}.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (expected.size > 0) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Native Batch Tool Call split omitted owned member ${[...expected][0]}.`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|