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