@abelxiaoxing/cadence 1.4.0 → 1.5.0
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/README.md +130 -8
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/config/plan-draft.quick.example.json +78 -0
- package/package.json +10 -4
- package/prompts/abel-design.md +101 -44
- package/prompts/abel-implement.md +9 -0
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +920 -0
- package/src/child-budget.ts +58 -0
- package/src/child-model.ts +142 -0
- package/src/child-session.ts +289 -326
- package/src/contracts.ts +200 -76
- package/src/delivery-compiler.ts +105 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +213 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/execution-profile.ts +38 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/isolation-backend.ts +62 -40
- package/src/model-source.ts +22 -0
- package/src/operator-cli-entry.mjs +24 -0
- package/src/operator-cli.mjs +1421 -0
- package/src/operator-tools.ts +249 -0
- package/src/package-candidate.ts +227 -0
- package/src/package-verification.ts +86 -8
- package/src/package-workflow.ts +291 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +149 -0
- package/src/plan-draft.ts +493 -0
- package/src/single-task-draft.ts +142 -0
- package/src/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +52 -0
- package/src/verification-capability.ts +5 -3
- package/src/verification-diagnostics.ts +76 -0
- package/src/verification-environment-io.ts +280 -0
- package/src/verification-environment.ts +56 -154
- package/src/verification-scan.ts +64 -0
- package/src/worker-broker.ts +39 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +19 -1
- package/src/workspace-io-worker.mjs +275 -50
- package/src/workspace-io.ts +6 -1
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/child-session.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import type { AssistantMessage, Model, Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "@earendil-works/pi-
|
|
4
|
+
type AssistantMessage,
|
|
5
|
+
type Context,
|
|
6
|
+
type Model,
|
|
7
|
+
type Usage,
|
|
8
|
+
validateToolArguments,
|
|
9
|
+
} from "@earendil-works/pi-ai";
|
|
10
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { Type } from "typebox";
|
|
12
|
+
import { childContextBudget, classifyProviderFailure } from "./child-budget.ts";
|
|
13
|
+
import {
|
|
14
|
+
abortable,
|
|
15
|
+
type ChildModelClient,
|
|
16
|
+
disposeChildTransport,
|
|
17
|
+
requestChildTurn,
|
|
18
|
+
} from "./child-model.ts";
|
|
11
19
|
import type {
|
|
12
20
|
ChildFailure,
|
|
13
21
|
DiffResult,
|
|
@@ -15,7 +23,6 @@ import type {
|
|
|
15
23
|
IdentityDimension,
|
|
16
24
|
SafeFailureDetails,
|
|
17
25
|
} from "./contracts.ts";
|
|
18
|
-
import { EmptyResourceLoader } from "./empty-resource-loader.ts";
|
|
19
26
|
import {
|
|
20
27
|
createScopedTools,
|
|
21
28
|
type Observation,
|
|
@@ -28,40 +35,15 @@ import {
|
|
|
28
35
|
createCandidateArtifactTool,
|
|
29
36
|
createStructuredPatchTool,
|
|
30
37
|
createSubmitTool,
|
|
31
|
-
type FinalCategory,
|
|
32
38
|
type SubmitClassification,
|
|
33
39
|
} from "./submit-tool.ts";
|
|
34
40
|
import { serializeTaskLedgerProjection } from "./task-ledger.ts";
|
|
41
|
+
import { TransportTimeout } from "./transport-budget.ts";
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
signal.removeEventListener("abort", onAbort);
|
|
41
|
-
reject(signal.reason);
|
|
42
|
-
};
|
|
43
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
-
promise
|
|
45
|
-
.then(resolve, reject)
|
|
46
|
-
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const ABORT_SETTLE_GRACE_MS = 250;
|
|
51
|
-
|
|
52
|
-
async function settleAbort(promise: Promise<void>): Promise<void> {
|
|
53
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
-
try {
|
|
55
|
-
await Promise.race([
|
|
56
|
-
promise.catch(() => undefined),
|
|
57
|
-
new Promise<void>((resolve) => {
|
|
58
|
-
timer = setTimeout(resolve, ABORT_SETTLE_GRACE_MS);
|
|
59
|
-
}),
|
|
60
|
-
]);
|
|
61
|
-
} finally {
|
|
62
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
43
|
+
export const CHILD_EXECUTION_LIMITS = Object.freeze({
|
|
44
|
+
maxTurns: 64,
|
|
45
|
+
maxContextBytes: 4 * 1024 * 1024,
|
|
46
|
+
});
|
|
65
47
|
|
|
66
48
|
const ZERO_USAGE: Usage = {
|
|
67
49
|
input: 0,
|
|
@@ -189,26 +171,10 @@ function finalDeliveryContent(message: AssistantMessage | undefined) {
|
|
|
189
171
|
);
|
|
190
172
|
}
|
|
191
173
|
|
|
192
|
-
function hasStructuralSubmit(message: AssistantMessage | undefined) {
|
|
193
|
-
return finalDeliveryContent(message).some(
|
|
194
|
-
(content) =>
|
|
195
|
-
content.type === "toolCall" && content.name === "abel_submit_result",
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function structuralSubmitCount(message: AssistantMessage | undefined) {
|
|
200
|
-
return finalDeliveryContent(message).filter(
|
|
201
|
-
(content) =>
|
|
202
|
-
content.type === "toolCall" && content.name === "abel_submit_result",
|
|
203
|
-
).length;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
174
|
function safeChildError(failure: ChildFailure): string {
|
|
207
175
|
switch (failure.kind) {
|
|
208
176
|
case "environment":
|
|
209
|
-
return
|
|
210
|
-
? "child session creation failed"
|
|
211
|
-
: "child environment unavailable";
|
|
177
|
+
return "child environment unavailable";
|
|
212
178
|
case "transport":
|
|
213
179
|
switch (failure.code) {
|
|
214
180
|
case "child-timeout":
|
|
@@ -232,6 +198,7 @@ function safeChildError(failure: ChildFailure): string {
|
|
|
232
198
|
case "stale":
|
|
233
199
|
case "approval-boundary":
|
|
234
200
|
case "verification-adapter":
|
|
201
|
+
case "execution-limit":
|
|
235
202
|
case "result-limit":
|
|
236
203
|
return "child result rejected";
|
|
237
204
|
}
|
|
@@ -291,7 +258,7 @@ export type ChildSessionResult =
|
|
|
291
258
|
|
|
292
259
|
export async function runChildSession(input: {
|
|
293
260
|
cwd: string;
|
|
294
|
-
modelRuntime:
|
|
261
|
+
modelRuntime: ChildModelClient;
|
|
295
262
|
model: Model<string>;
|
|
296
263
|
systemPrompt: string;
|
|
297
264
|
requestId: string;
|
|
@@ -303,7 +270,6 @@ export async function runChildSession(input: {
|
|
|
303
270
|
allowedPaths?: string[];
|
|
304
271
|
timeoutMs: number;
|
|
305
272
|
signal?: AbortSignal;
|
|
306
|
-
failureOverride?: () => ChildFailure | undefined;
|
|
307
273
|
ledgerProjection?: unknown;
|
|
308
274
|
captureObservations?: boolean;
|
|
309
275
|
candidateArtifact?: CandidateArtifactSubmission;
|
|
@@ -313,9 +279,9 @@ export async function runChildSession(input: {
|
|
|
313
279
|
deletePaths: string[];
|
|
314
280
|
};
|
|
315
281
|
onStreamStart?: () => void;
|
|
282
|
+
onStreamHeaders?: () => void;
|
|
316
283
|
onStreamProgress?: () => void;
|
|
317
284
|
}): Promise<ChildSessionResult> {
|
|
318
|
-
const candidateProtocol = input.candidateArtifact !== undefined;
|
|
319
285
|
const submit = input.candidateArtifact
|
|
320
286
|
? createCandidateArtifactTool(input.candidateArtifact)
|
|
321
287
|
: input.structuredPatch
|
|
@@ -375,303 +341,300 @@ export async function runChildSession(input: {
|
|
|
375
341
|
timedOut = true;
|
|
376
342
|
abort.abort(new Error("child phase timeout"));
|
|
377
343
|
}, input.timeoutMs);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
344
|
+
const context: Context = {
|
|
345
|
+
systemPrompt: effectivePrompt,
|
|
346
|
+
messages: [
|
|
347
|
+
{
|
|
348
|
+
role: "user",
|
|
349
|
+
content: "Complete the bounded task and submit its result.",
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
tools: customTools.map(({ name, description, parameters }) => ({
|
|
354
|
+
name,
|
|
355
|
+
description,
|
|
356
|
+
parameters,
|
|
357
|
+
})),
|
|
358
|
+
};
|
|
359
|
+
const sessionId = randomUUID();
|
|
383
360
|
let assistantSequence = 0;
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
361
|
+
let structuralAttempts = 0;
|
|
362
|
+
let last: AssistantMessage | undefined;
|
|
363
|
+
let preflightRejected = false;
|
|
364
|
+
let turns = 0;
|
|
365
|
+
const contextBudget = childContextBudget(input.model);
|
|
366
|
+
let reminderUsed = false;
|
|
367
|
+
const observeStart = () => {
|
|
388
368
|
try {
|
|
389
369
|
input.onStreamStart?.();
|
|
390
370
|
} catch {
|
|
391
|
-
|
|
371
|
+
/* observation only */
|
|
392
372
|
}
|
|
393
373
|
};
|
|
394
|
-
const
|
|
395
|
-
notifyStreamStart();
|
|
374
|
+
const observeProgress = () => {
|
|
396
375
|
try {
|
|
397
376
|
input.onStreamProgress?.();
|
|
398
377
|
} catch {
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
const disposeOnce = () => {
|
|
403
|
-
if (session) {
|
|
404
|
-
session.dispose();
|
|
405
|
-
disposeCount++;
|
|
406
|
-
session = undefined;
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
const classifySession = (): SubmitClassification => {
|
|
410
|
-
const assistants =
|
|
411
|
-
session?.messages.filter((m) => m.role === "assistant") ?? [];
|
|
412
|
-
const last = assistants.at(-1);
|
|
413
|
-
const structuralAttempts = assistants.reduce(
|
|
414
|
-
(count, message) => count + structuralSubmitCount(message),
|
|
415
|
-
0,
|
|
416
|
-
);
|
|
417
|
-
let finalCategory: FinalCategory;
|
|
418
|
-
if (submit.getResult() !== undefined) {
|
|
419
|
-
finalCategory =
|
|
420
|
-
structuralAttempts > 1 ? "multiple-submit" : "single-submit-only";
|
|
421
|
-
} else if (!last) {
|
|
422
|
-
finalCategory = "no-final-assistant";
|
|
423
|
-
} else {
|
|
424
|
-
const content = finalDeliveryContent(last);
|
|
425
|
-
finalCategory =
|
|
426
|
-
content.length === 1 && content[0]?.type === "text"
|
|
427
|
-
? "text-only"
|
|
428
|
-
: "mixed";
|
|
429
|
-
}
|
|
430
|
-
return {
|
|
431
|
-
finalCategory,
|
|
432
|
-
attempts: submit.getAttempts(),
|
|
433
|
-
schema: submit.getSchema(),
|
|
434
|
-
identity: submit.getIdentity(),
|
|
435
|
-
};
|
|
436
|
-
};
|
|
437
|
-
const transportFailure = ():
|
|
438
|
-
| Extract<ChildFailure, { kind: "transport" }>
|
|
439
|
-
| undefined => {
|
|
440
|
-
const final = session?.messages
|
|
441
|
-
.filter((message) => message.role === "assistant")
|
|
442
|
-
.at(-1);
|
|
443
|
-
if (final === undefined) {
|
|
444
|
-
return {
|
|
445
|
-
kind: "transport",
|
|
446
|
-
code: "child-no-final-assistant",
|
|
447
|
-
stage: "child-finalization",
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
if (final.stopReason === "error") {
|
|
451
|
-
return submit.getAttempts() === 0 &&
|
|
452
|
-
!final.content.some(
|
|
453
|
-
(content) =>
|
|
454
|
-
content.type === "toolCall" &&
|
|
455
|
-
content.name === "abel_submit_result",
|
|
456
|
-
)
|
|
457
|
-
? {
|
|
458
|
-
kind: "transport",
|
|
459
|
-
code: "child-provider-stream-error",
|
|
460
|
-
stage: "child-provider-stream",
|
|
461
|
-
}
|
|
462
|
-
: undefined;
|
|
463
|
-
}
|
|
464
|
-
if (final.stopReason === "aborted") {
|
|
465
|
-
return {
|
|
466
|
-
kind: "transport",
|
|
467
|
-
code: "child-provider-stream-aborted",
|
|
468
|
-
stage: "child-provider-stream",
|
|
469
|
-
};
|
|
378
|
+
/* observation only */
|
|
470
379
|
}
|
|
471
|
-
return undefined;
|
|
472
380
|
};
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
381
|
+
const classification = (): SubmitClassification => ({
|
|
382
|
+
finalCategory:
|
|
383
|
+
submit.getResult() !== undefined
|
|
384
|
+
? structuralAttempts > 1
|
|
385
|
+
? "multiple-submit"
|
|
386
|
+
: "single-submit-only"
|
|
387
|
+
: !last
|
|
388
|
+
? "no-final-assistant"
|
|
389
|
+
: finalDeliveryContent(last).length === 1 &&
|
|
390
|
+
finalDeliveryContent(last)[0]?.type === "text"
|
|
391
|
+
? "text-only"
|
|
392
|
+
: "mixed",
|
|
393
|
+
attempts: structuralAttempts,
|
|
394
|
+
schema: preflightRejected ? "invalid" : submit.getSchema(),
|
|
395
|
+
identity: submit.getIdentity(),
|
|
396
|
+
});
|
|
397
|
+
let failure: ChildFailure | undefined;
|
|
398
|
+
const missingResult = (): ChildFailure => ({
|
|
399
|
+
kind: "artifact",
|
|
400
|
+
code:
|
|
401
|
+
structuralAttempts === 0
|
|
402
|
+
? "child-no-structural-submit"
|
|
403
|
+
: "invalid-structural-result",
|
|
404
|
+
stage:
|
|
405
|
+
structuralAttempts === 0 ? "child-finalization" : "structural-submit",
|
|
406
|
+
});
|
|
407
|
+
try {
|
|
408
|
+
for (;;) {
|
|
409
|
+
abort.signal.throwIfAborted();
|
|
410
|
+
if (turns >= CHILD_EXECUTION_LIMITS.maxTurns) {
|
|
411
|
+
failure = { kind: "execution-limit", code: "child-turn-limit" };
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
if (Buffer.byteLength(JSON.stringify(context), "utf8") > contextBudget) {
|
|
415
|
+
failure = { kind: "execution-limit", code: "child-context-limit" };
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
turns++;
|
|
419
|
+
const message = await requestChildTurn({
|
|
420
|
+
client: input.modelRuntime,
|
|
421
|
+
model: input.model,
|
|
422
|
+
context,
|
|
423
|
+
signal: abort.signal,
|
|
424
|
+
sessionId,
|
|
425
|
+
onStart: observeStart,
|
|
426
|
+
onHeaders: input.onStreamHeaders,
|
|
427
|
+
onProgress: observeProgress,
|
|
428
|
+
onMessage: (message) => {
|
|
429
|
+
usage.add(`assistant:${assistantSequence++}`, message.usage);
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
last = message;
|
|
433
|
+
if (!message) {
|
|
434
|
+
failure = {
|
|
435
|
+
kind: "transport",
|
|
436
|
+
code: "child-no-final-assistant",
|
|
481
437
|
stage: "child-finalization",
|
|
482
|
-
}
|
|
483
|
-
: {
|
|
484
|
-
kind: "artifact",
|
|
485
|
-
code: "invalid-structural-result",
|
|
486
|
-
stage: "structural-submit",
|
|
487
438
|
};
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
context.messages.push(message);
|
|
442
|
+
if (Buffer.byteLength(JSON.stringify(context), "utf8") > contextBudget) {
|
|
443
|
+
failure = { kind: "execution-limit", code: "child-context-limit" };
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
// Only complete, normal turns may execute tools, including terminal submit.
|
|
447
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
448
|
+
failure =
|
|
449
|
+
message.stopReason === "error"
|
|
450
|
+
? classifyProviderFailure(message.errorMessage)
|
|
451
|
+
: {
|
|
452
|
+
kind: "transport",
|
|
453
|
+
code: "child-provider-stream-aborted",
|
|
454
|
+
stage: "child-provider-stream",
|
|
455
|
+
};
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (message.stopReason !== "toolUse" && message.stopReason !== "stop") {
|
|
459
|
+
failure = missingResult();
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
const calls = message.content.filter((part) => part.type === "toolCall");
|
|
463
|
+
if (calls.length === 0) {
|
|
464
|
+
if (
|
|
465
|
+
!reminderUsed &&
|
|
466
|
+
structuralAttempts === 0 &&
|
|
467
|
+
message.stopReason === "stop" &&
|
|
468
|
+
finalDeliveryContent(message).length > 0 &&
|
|
469
|
+
finalDeliveryContent(message).every((part) => part.type === "text")
|
|
470
|
+
) {
|
|
471
|
+
reminderUsed = true;
|
|
472
|
+
context.messages.push({
|
|
473
|
+
role: "user",
|
|
474
|
+
timestamp: Date.now(),
|
|
475
|
+
content:
|
|
476
|
+
"No result has been accepted. Submit existing findings through abel_submit_result, not prose. Do not repeat investigation or expand scope. Keep evidence gaps explicit; use context-request if available and needed. This is the only missing-submit reminder; the original deadline still applies.",
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
519
479
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
480
|
+
failure = missingResult();
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
// Source-order execution is a Cadence contract, not Pi's batch scheduling.
|
|
484
|
+
// Process the batch even after acceptance to reject a duplicate terminal
|
|
485
|
+
// submission while still allowing harmless scoped reads in that batch.
|
|
486
|
+
for (const call of calls) {
|
|
487
|
+
abort.signal.throwIfAborted();
|
|
488
|
+
const isSubmit = call.name === "abel_submit_result";
|
|
489
|
+
if (isSubmit) {
|
|
490
|
+
structuralAttempts++;
|
|
491
|
+
if (structuralAttempts > 2) {
|
|
492
|
+
failure = {
|
|
493
|
+
kind: "artifact",
|
|
494
|
+
code: "invalid-structural-result",
|
|
495
|
+
stage: "structural-submit",
|
|
496
|
+
};
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
if (submit.getResult() !== undefined) {
|
|
500
|
+
preflightRejected = true;
|
|
501
|
+
failure = {
|
|
502
|
+
kind: "artifact",
|
|
503
|
+
code: "invalid-structural-result",
|
|
504
|
+
stage: "structural-submit",
|
|
505
|
+
};
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const tool = customTools.find((tool) => tool.name === call.name);
|
|
510
|
+
let content: Array<{ type: "text"; text: string }>;
|
|
511
|
+
let isError = false;
|
|
512
|
+
let validated = false;
|
|
513
|
+
try {
|
|
514
|
+
if (!tool) throw new Error("unknown scoped tool");
|
|
515
|
+
const params = validateToolArguments(tool, call);
|
|
516
|
+
validated = true;
|
|
517
|
+
if (isSubmit) preflightRejected = false;
|
|
518
|
+
const result = await abortable(
|
|
519
|
+
tool.execute(
|
|
520
|
+
call.id,
|
|
521
|
+
params as never,
|
|
522
|
+
abort.signal,
|
|
523
|
+
undefined,
|
|
524
|
+
{} as never,
|
|
525
|
+
),
|
|
526
|
+
abort.signal,
|
|
527
|
+
);
|
|
528
|
+
content = result.content as typeof content;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
abort.signal.throwIfAborted();
|
|
531
|
+
isError = true;
|
|
532
|
+
if (isSubmit && !validated) preflightRejected = true;
|
|
533
|
+
content = [
|
|
534
|
+
{
|
|
535
|
+
type: "text",
|
|
536
|
+
text:
|
|
537
|
+
error instanceof Error
|
|
538
|
+
? error.message
|
|
539
|
+
: "tool execution failed",
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
context.messages.push({
|
|
544
|
+
role: "toolResult",
|
|
545
|
+
toolCallId: call.id,
|
|
546
|
+
toolName: call.name,
|
|
547
|
+
content,
|
|
548
|
+
isError,
|
|
549
|
+
timestamp: Date.now(),
|
|
550
|
+
});
|
|
537
551
|
}
|
|
552
|
+
if (failure) break;
|
|
553
|
+
if (submit.getResult() !== undefined) break;
|
|
538
554
|
if (
|
|
539
|
-
|
|
540
|
-
|
|
555
|
+
structuralAttempts >= 2 ||
|
|
556
|
+
submit.getFailure()?.kind === "result-limit"
|
|
541
557
|
) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
545
|
-
usage.add(`assistant:${assistantSequence++}`, event.message.usage);
|
|
558
|
+
failure = missingResult();
|
|
559
|
+
break;
|
|
546
560
|
}
|
|
547
|
-
});
|
|
548
|
-
const startAbortSettlement = () => {
|
|
549
|
-
if (session && !abortSettlement) abortSettlement = session.abort();
|
|
550
|
-
return abortSettlement;
|
|
551
|
-
};
|
|
552
|
-
const onAbort = () => {
|
|
553
|
-
void startAbortSettlement()?.catch(() => undefined);
|
|
554
|
-
};
|
|
555
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
556
|
-
try {
|
|
557
|
-
if (abort.signal.aborted) {
|
|
558
|
-
onAbort();
|
|
559
|
-
throw abort.signal.reason;
|
|
560
|
-
}
|
|
561
|
-
await Promise.race([
|
|
562
|
-
session.prompt(effectivePrompt, { expandPromptTemplates: false }),
|
|
563
|
-
new Promise<never>((_, reject) =>
|
|
564
|
-
abort.signal.addEventListener(
|
|
565
|
-
"abort",
|
|
566
|
-
() => reject(abort.signal.reason),
|
|
567
|
-
{ once: true },
|
|
568
|
-
),
|
|
569
|
-
),
|
|
570
|
-
]);
|
|
571
|
-
} finally {
|
|
572
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
573
561
|
}
|
|
574
|
-
const result = submit.getResult();
|
|
575
|
-
const attempts = submit.getAttempts();
|
|
576
|
-
const classification = classifySession();
|
|
577
|
-
const terminalSubmitFailure = submit.getFailure();
|
|
578
|
-
const candidateComplete =
|
|
579
|
-
candidateProtocol &&
|
|
580
|
-
result !== undefined &&
|
|
581
|
-
(result.kind === "sealed-candidate" || result.kind === "context-request");
|
|
582
|
-
if (
|
|
583
|
-
!result ||
|
|
584
|
-
terminalSubmitFailure !== undefined ||
|
|
585
|
-
(candidateProtocol && !candidateComplete)
|
|
586
|
-
) {
|
|
587
|
-
const failure = withSubmitDetails(
|
|
588
|
-
terminalSubmitFailure ??
|
|
589
|
-
input.failureOverride?.() ??
|
|
590
|
-
transportFailure() ??
|
|
591
|
-
noStructuralSubmit(),
|
|
592
|
-
classification,
|
|
593
|
-
);
|
|
594
|
-
const isTransport = failure.kind === "transport";
|
|
595
|
-
disposeOnce();
|
|
596
|
-
return {
|
|
597
|
-
ok: false,
|
|
598
|
-
error: safeChildError(failure),
|
|
599
|
-
failure,
|
|
600
|
-
failureKind: "failed",
|
|
601
|
-
transportFailure: isTransport,
|
|
602
|
-
disposeCount,
|
|
603
|
-
usage: usage.total(),
|
|
604
|
-
classification,
|
|
605
|
-
...observationMetadata(),
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
// The trusted submit tool already validates and seals the one accepted
|
|
609
|
-
// result. Harmless text or read-only calls do not alter that admission;
|
|
610
|
-
// another terminal submission does.
|
|
611
|
-
disposeOnce();
|
|
612
|
-
return {
|
|
613
|
-
ok: true,
|
|
614
|
-
result,
|
|
615
|
-
toolNames,
|
|
616
|
-
submitCount: attempts,
|
|
617
|
-
disposeCount,
|
|
618
|
-
usage: usage.total(),
|
|
619
|
-
classification,
|
|
620
|
-
...observationMetadata(),
|
|
621
|
-
};
|
|
622
562
|
} catch (error) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
abortSettlement = settlement;
|
|
626
|
-
await settleAbort(settlement);
|
|
627
|
-
}
|
|
628
|
-
const failureKind: ChildFailureKind = timedOut
|
|
629
|
-
? "timed-out"
|
|
630
|
-
: input.signal?.aborted === true
|
|
631
|
-
? "cancelled"
|
|
632
|
-
: "failed";
|
|
633
|
-
const classification = classifySession();
|
|
634
|
-
const failure: ChildFailure = withSubmitDetails(
|
|
635
|
-
timedOut
|
|
563
|
+
failure =
|
|
564
|
+
error instanceof TransportTimeout
|
|
636
565
|
? {
|
|
637
566
|
kind: "transport",
|
|
638
|
-
code:
|
|
639
|
-
stage: "child-
|
|
567
|
+
code: error.code,
|
|
568
|
+
stage: "child-provider-stream",
|
|
640
569
|
}
|
|
641
|
-
:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
570
|
+
: classifyProviderFailure(error);
|
|
571
|
+
} finally {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
input.signal?.removeEventListener("abort", forwardCancellation);
|
|
574
|
+
// Dispose our conversation and only this child's provider session resources.
|
|
575
|
+
context.messages.length = 0;
|
|
576
|
+
try {
|
|
577
|
+
disposeChildTransport(sessionId);
|
|
578
|
+
} catch {
|
|
579
|
+
failure ??= {
|
|
580
|
+
kind: "environment",
|
|
581
|
+
code: "sandbox-runtime-unavailable",
|
|
582
|
+
stage: "child-finalization",
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
disposeCount++;
|
|
586
|
+
}
|
|
587
|
+
const result = submit.getResult();
|
|
588
|
+
// All exit paths share the same priority, including thrown/empty streams.
|
|
589
|
+
// A later transport failure cannot erase a rejected submission. Preflight
|
|
590
|
+
// rejection never reaches execute, so retain it independently of the tool.
|
|
591
|
+
// A valid correction clears both the tool failure and preflightRejected.
|
|
592
|
+
const terminalFailure: ChildFailure | undefined = timedOut
|
|
593
|
+
? { kind: "transport", code: "child-timeout", stage: "child-timeout" }
|
|
594
|
+
: input.signal?.aborted
|
|
595
|
+
? { kind: "cancelled", code: "cancelled" }
|
|
596
|
+
: (submit.getFailure() ??
|
|
597
|
+
(preflightRejected ? missingResult() : undefined) ??
|
|
598
|
+
failure ??
|
|
599
|
+
(!result ? missingResult() : undefined));
|
|
600
|
+
const projection = classification();
|
|
601
|
+
if (terminalFailure) {
|
|
602
|
+
const typed = withSubmitDetails(terminalFailure, projection);
|
|
657
603
|
return {
|
|
658
604
|
ok: false,
|
|
659
605
|
error:
|
|
660
|
-
|
|
661
|
-
?
|
|
662
|
-
: safeChildError(
|
|
663
|
-
failure,
|
|
664
|
-
failureKind
|
|
665
|
-
|
|
606
|
+
typed.kind === "cancelled" && input.signal?.reason instanceof Error
|
|
607
|
+
? input.signal.reason.message
|
|
608
|
+
: safeChildError(typed),
|
|
609
|
+
failure: typed,
|
|
610
|
+
failureKind:
|
|
611
|
+
timedOut ||
|
|
612
|
+
(typed.kind === "transport" &&
|
|
613
|
+
[
|
|
614
|
+
"first-progress-timeout",
|
|
615
|
+
"stream-idle-timeout",
|
|
616
|
+
"attempt-timeout",
|
|
617
|
+
].includes(typed.code))
|
|
618
|
+
? "timed-out"
|
|
619
|
+
: typed.kind === "cancelled"
|
|
620
|
+
? "cancelled"
|
|
621
|
+
: "failed",
|
|
622
|
+
transportFailure: typed.kind === "transport",
|
|
666
623
|
disposeCount,
|
|
667
624
|
usage: usage.total(),
|
|
668
|
-
classification,
|
|
625
|
+
classification: projection,
|
|
669
626
|
...observationMetadata(),
|
|
670
627
|
};
|
|
671
|
-
} finally {
|
|
672
|
-
clearTimeout(timer);
|
|
673
|
-
input.signal?.removeEventListener("abort", forwardCancellation);
|
|
674
|
-
unsubscribe?.();
|
|
675
|
-
disposeOnce();
|
|
676
628
|
}
|
|
629
|
+
if (!result) throw new Error("child-result-invariant");
|
|
630
|
+
return {
|
|
631
|
+
ok: true,
|
|
632
|
+
result,
|
|
633
|
+
toolNames,
|
|
634
|
+
submitCount: structuralAttempts,
|
|
635
|
+
disposeCount,
|
|
636
|
+
usage: usage.total(),
|
|
637
|
+
classification: projection,
|
|
638
|
+
...observationMetadata(),
|
|
639
|
+
};
|
|
677
640
|
}
|