@abelxiaoxing/cadence 1.3.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 +172 -13
- 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 +9 -5
- package/prompts/abel-design.md +141 -38
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/activity-contracts.ts +82 -0
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +33 -12
- package/src/candidate-workspace.ts +74 -0
- package/src/canonical.ts +16 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +273 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +296 -328
- package/src/contracts.ts +325 -109
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +241 -141
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +182 -15
- package/src/design-diagnostics.ts +150 -0
- package/src/design-journal.ts +216 -72
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +963 -0
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +14 -16
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +341 -1405
- package/src/isolation-backend.ts +84 -23
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -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/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +46 -67
- package/src/submit-tool.ts +40 -61
- package/src/task-ledger.ts +14 -55
- package/src/transport-budget.ts +32 -0
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +134 -91
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1388 -0
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +4086 -0
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +991 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
- 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,
|
|
@@ -120,6 +101,7 @@ function wrapScopedTools(
|
|
|
120
101
|
) {
|
|
121
102
|
const order = ["read", "grep", "find", "ls"];
|
|
122
103
|
return createScopedTools({
|
|
104
|
+
cwd,
|
|
123
105
|
roots,
|
|
124
106
|
...(allowedPaths
|
|
125
107
|
? {
|
|
@@ -162,9 +144,10 @@ function wrapScopedTools(
|
|
|
162
144
|
: Type.Object({
|
|
163
145
|
path: Type.Optional(Type.String()),
|
|
164
146
|
}),
|
|
165
|
-
async execute(_id, params) {
|
|
147
|
+
async execute(_id, params, signal) {
|
|
166
148
|
const result = await scoped.execute(
|
|
167
149
|
params as Record<string, unknown>,
|
|
150
|
+
signal,
|
|
168
151
|
);
|
|
169
152
|
return {
|
|
170
153
|
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
@@ -187,26 +170,10 @@ function finalDeliveryContent(message: AssistantMessage | undefined) {
|
|
|
187
170
|
);
|
|
188
171
|
}
|
|
189
172
|
|
|
190
|
-
function hasStructuralSubmit(message: AssistantMessage | undefined) {
|
|
191
|
-
return finalDeliveryContent(message).some(
|
|
192
|
-
(content) =>
|
|
193
|
-
content.type === "toolCall" && content.name === "abel_submit_result",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function structuralSubmitCount(message: AssistantMessage | undefined) {
|
|
198
|
-
return finalDeliveryContent(message).filter(
|
|
199
|
-
(content) =>
|
|
200
|
-
content.type === "toolCall" && content.name === "abel_submit_result",
|
|
201
|
-
).length;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
173
|
function safeChildError(failure: ChildFailure): string {
|
|
205
174
|
switch (failure.kind) {
|
|
206
175
|
case "environment":
|
|
207
|
-
return
|
|
208
|
-
? "child session creation failed"
|
|
209
|
-
: "child environment unavailable";
|
|
176
|
+
return "child environment unavailable";
|
|
210
177
|
case "transport":
|
|
211
178
|
switch (failure.code) {
|
|
212
179
|
case "child-timeout":
|
|
@@ -230,6 +197,7 @@ function safeChildError(failure: ChildFailure): string {
|
|
|
230
197
|
case "stale":
|
|
231
198
|
case "approval-boundary":
|
|
232
199
|
case "verification-adapter":
|
|
200
|
+
case "execution-limit":
|
|
233
201
|
case "result-limit":
|
|
234
202
|
return "child result rejected";
|
|
235
203
|
}
|
|
@@ -289,7 +257,7 @@ export type ChildSessionResult =
|
|
|
289
257
|
|
|
290
258
|
export async function runChildSession(input: {
|
|
291
259
|
cwd: string;
|
|
292
|
-
modelRuntime:
|
|
260
|
+
modelRuntime: ChildModelClient;
|
|
293
261
|
model: Model<string>;
|
|
294
262
|
systemPrompt: string;
|
|
295
263
|
requestId: string;
|
|
@@ -301,7 +269,6 @@ export async function runChildSession(input: {
|
|
|
301
269
|
allowedPaths?: string[];
|
|
302
270
|
timeoutMs: number;
|
|
303
271
|
signal?: AbortSignal;
|
|
304
|
-
failureOverride?: () => ChildFailure | undefined;
|
|
305
272
|
ledgerProjection?: unknown;
|
|
306
273
|
captureObservations?: boolean;
|
|
307
274
|
candidateArtifact?: CandidateArtifactSubmission;
|
|
@@ -311,9 +278,9 @@ export async function runChildSession(input: {
|
|
|
311
278
|
deletePaths: string[];
|
|
312
279
|
};
|
|
313
280
|
onStreamStart?: () => void;
|
|
281
|
+
onStreamHeaders?: () => void;
|
|
314
282
|
onStreamProgress?: () => void;
|
|
315
283
|
}): Promise<ChildSessionResult> {
|
|
316
|
-
const candidateProtocol = input.candidateArtifact !== undefined;
|
|
317
284
|
const submit = input.candidateArtifact
|
|
318
285
|
? createCandidateArtifactTool(input.candidateArtifact)
|
|
319
286
|
: input.structuredPatch
|
|
@@ -373,304 +340,305 @@ export async function runChildSession(input: {
|
|
|
373
340
|
timedOut = true;
|
|
374
341
|
abort.abort(new Error("child phase timeout"));
|
|
375
342
|
}, input.timeoutMs);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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();
|
|
381
359
|
let assistantSequence = 0;
|
|
382
|
-
let
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
360
|
+
let structuralAttempts = 0;
|
|
361
|
+
let last: AssistantMessage | undefined;
|
|
362
|
+
let preflightRejected = false;
|
|
363
|
+
let turns = 0;
|
|
364
|
+
let reminderUsed = false;
|
|
365
|
+
const observeStart = () => {
|
|
386
366
|
try {
|
|
387
367
|
input.onStreamStart?.();
|
|
388
368
|
} catch {
|
|
389
|
-
|
|
369
|
+
/* observation only */
|
|
390
370
|
}
|
|
391
371
|
};
|
|
392
|
-
const
|
|
393
|
-
notifyStreamStart();
|
|
372
|
+
const observeProgress = () => {
|
|
394
373
|
try {
|
|
395
374
|
input.onStreamProgress?.();
|
|
396
375
|
} catch {
|
|
397
|
-
|
|
376
|
+
/* observation only */
|
|
398
377
|
}
|
|
399
378
|
};
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
: "mixed";
|
|
427
|
-
}
|
|
428
|
-
return {
|
|
429
|
-
finalCategory,
|
|
430
|
-
attempts: submit.getAttempts(),
|
|
431
|
-
schema: submit.getSchema(),
|
|
432
|
-
identity: submit.getIdentity(),
|
|
433
|
-
};
|
|
434
|
-
};
|
|
435
|
-
const transportFailure = ():
|
|
436
|
-
| Extract<ChildFailure, { kind: "transport" }>
|
|
437
|
-
| undefined => {
|
|
438
|
-
const final = session?.messages
|
|
439
|
-
.filter((message) => message.role === "assistant")
|
|
440
|
-
.at(-1);
|
|
441
|
-
if (final === undefined) {
|
|
442
|
-
return {
|
|
443
|
-
kind: "transport",
|
|
444
|
-
code: "child-no-final-assistant",
|
|
445
|
-
stage: "child-finalization",
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
if (final.stopReason === "error") {
|
|
449
|
-
return submit.getAttempts() === 0 &&
|
|
450
|
-
!final.content.some(
|
|
451
|
-
(content) =>
|
|
452
|
-
content.type === "toolCall" &&
|
|
453
|
-
content.name === "abel_submit_result",
|
|
454
|
-
)
|
|
455
|
-
? {
|
|
456
|
-
kind: "transport",
|
|
457
|
-
code: "child-provider-stream-error",
|
|
458
|
-
stage: "child-provider-stream",
|
|
459
|
-
}
|
|
460
|
-
: undefined;
|
|
461
|
-
}
|
|
462
|
-
if (final.stopReason === "aborted") {
|
|
463
|
-
return {
|
|
464
|
-
kind: "transport",
|
|
465
|
-
code: "child-provider-stream-aborted",
|
|
466
|
-
stage: "child-provider-stream",
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
return undefined;
|
|
470
|
-
};
|
|
471
|
-
const noStructuralSubmit = (): ChildFailure => {
|
|
472
|
-
const final = session?.messages
|
|
473
|
-
.filter((message) => message.role === "assistant")
|
|
474
|
-
.at(-1);
|
|
475
|
-
return submit.getAttempts() === 0 && !hasStructuralSubmit(final)
|
|
476
|
-
? {
|
|
477
|
-
kind: "artifact",
|
|
478
|
-
code: "child-no-structural-submit",
|
|
479
|
-
stage: "child-finalization",
|
|
480
|
-
}
|
|
481
|
-
: {
|
|
482
|
-
kind: "artifact",
|
|
483
|
-
code: "invalid-structural-result",
|
|
484
|
-
stage: "structural-submit",
|
|
485
|
-
};
|
|
486
|
-
};
|
|
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
|
+
});
|
|
487
405
|
try {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
tools: toolNames,
|
|
495
|
-
customTools,
|
|
496
|
-
resourceLoader: new EmptyResourceLoader(effectivePrompt),
|
|
497
|
-
sessionManager: SessionManager.inMemory(input.cwd),
|
|
498
|
-
settingsManager: SettingsManager.inMemory({
|
|
499
|
-
compaction: { enabled: false },
|
|
500
|
-
retry: { enabled: false, maxRetries: 0, provider: { maxRetries: 0 } },
|
|
501
|
-
}),
|
|
502
|
-
});
|
|
503
|
-
try {
|
|
504
|
-
({ session } = await abortable(creation, abort.signal));
|
|
505
|
-
} catch (error) {
|
|
506
|
-
void creation
|
|
507
|
-
.then(({ session: lateSession }) => lateSession.dispose())
|
|
508
|
-
.catch(() => undefined);
|
|
509
|
-
throw error;
|
|
510
|
-
}
|
|
511
|
-
const childAgent = session.agent;
|
|
512
|
-
if (childAgent) {
|
|
513
|
-
const previousShouldStopAfterTurn = childAgent.shouldStopAfterTurn;
|
|
514
|
-
childAgent.shouldStopAfterTurn = async (turn, signal) => {
|
|
515
|
-
if (submit.getResult() !== undefined) {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
const structuralAttempts =
|
|
519
|
-
session?.messages
|
|
520
|
-
.filter((message) => message.role === "assistant")
|
|
521
|
-
.reduce(
|
|
522
|
-
(count, message) => count + structuralSubmitCount(message),
|
|
523
|
-
0,
|
|
524
|
-
) ?? 0;
|
|
525
|
-
if (structuralAttempts >= 2) return true;
|
|
526
|
-
return (await previousShouldStopAfterTurn?.(turn, signal)) ?? false;
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
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
|
+
}
|
|
530
412
|
if (
|
|
531
|
-
|
|
532
|
-
|
|
413
|
+
Buffer.byteLength(JSON.stringify(context), "utf8") >
|
|
414
|
+
CHILD_EXECUTION_LIMITS.maxContextBytes
|
|
533
415
|
) {
|
|
534
|
-
|
|
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;
|
|
535
441
|
}
|
|
442
|
+
context.messages.push(message);
|
|
536
443
|
if (
|
|
537
|
-
|
|
538
|
-
|
|
444
|
+
Buffer.byteLength(JSON.stringify(context), "utf8") >
|
|
445
|
+
CHILD_EXECUTION_LIMITS.maxContextBytes
|
|
539
446
|
) {
|
|
540
|
-
|
|
447
|
+
failure = { kind: "execution-limit", code: "child-context-limit" };
|
|
448
|
+
break;
|
|
541
449
|
}
|
|
542
|
-
|
|
543
|
-
|
|
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;
|
|
544
461
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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;
|
|
558
564
|
}
|
|
559
|
-
await Promise.race([
|
|
560
|
-
session.prompt(effectivePrompt, { expandPromptTemplates: false }),
|
|
561
|
-
new Promise<never>((_, reject) =>
|
|
562
|
-
abort.signal.addEventListener(
|
|
563
|
-
"abort",
|
|
564
|
-
() => reject(abort.signal.reason),
|
|
565
|
-
{ once: true },
|
|
566
|
-
),
|
|
567
|
-
),
|
|
568
|
-
]);
|
|
569
|
-
} finally {
|
|
570
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
571
|
-
}
|
|
572
|
-
const result = submit.getResult();
|
|
573
|
-
const attempts = submit.getAttempts();
|
|
574
|
-
const classification = classifySession();
|
|
575
|
-
const terminalSubmitFailure = submit.getFailure();
|
|
576
|
-
const candidateComplete =
|
|
577
|
-
candidateProtocol &&
|
|
578
|
-
result !== undefined &&
|
|
579
|
-
(result.kind === "sealed-candidate" || result.kind === "context-request");
|
|
580
|
-
if (
|
|
581
|
-
!result ||
|
|
582
|
-
terminalSubmitFailure !== undefined ||
|
|
583
|
-
(candidateProtocol && !candidateComplete)
|
|
584
|
-
) {
|
|
585
|
-
const failure = withSubmitDetails(
|
|
586
|
-
terminalSubmitFailure ??
|
|
587
|
-
input.failureOverride?.() ??
|
|
588
|
-
transportFailure() ??
|
|
589
|
-
noStructuralSubmit(),
|
|
590
|
-
classification,
|
|
591
|
-
);
|
|
592
|
-
const isTransport = failure.kind === "transport";
|
|
593
|
-
disposeOnce();
|
|
594
|
-
return {
|
|
595
|
-
ok: false,
|
|
596
|
-
error: safeChildError(failure),
|
|
597
|
-
failure,
|
|
598
|
-
failureKind: "failed",
|
|
599
|
-
transportFailure: isTransport,
|
|
600
|
-
disposeCount,
|
|
601
|
-
usage: usage.total(),
|
|
602
|
-
classification,
|
|
603
|
-
...observationMetadata(),
|
|
604
|
-
};
|
|
605
565
|
}
|
|
606
|
-
// The trusted submit tool already validates and seals the one accepted
|
|
607
|
-
// result. Harmless text or read-only calls do not alter that admission;
|
|
608
|
-
// another terminal submission does.
|
|
609
|
-
disposeOnce();
|
|
610
|
-
return {
|
|
611
|
-
ok: true,
|
|
612
|
-
result,
|
|
613
|
-
toolNames,
|
|
614
|
-
submitCount: attempts,
|
|
615
|
-
disposeCount,
|
|
616
|
-
usage: usage.total(),
|
|
617
|
-
classification,
|
|
618
|
-
...observationMetadata(),
|
|
619
|
-
};
|
|
620
566
|
} catch (error) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
+
};
|
|
625
588
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
kind: "environment",
|
|
645
|
-
code: "child-session-create-failed",
|
|
646
|
-
stage: "child-session-create",
|
|
647
|
-
}
|
|
648
|
-
: (submit.getFailure() ??
|
|
649
|
-
input.failureOverride?.() ??
|
|
650
|
-
transportFailure() ??
|
|
651
|
-
noStructuralSubmit()),
|
|
652
|
-
classification,
|
|
653
|
-
);
|
|
654
|
-
const isTransport = failure.kind === "transport";
|
|
655
|
-
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);
|
|
656
607
|
return {
|
|
657
608
|
ok: false,
|
|
658
609
|
error:
|
|
659
|
-
|
|
660
|
-
?
|
|
661
|
-
: safeChildError(
|
|
662
|
-
failure,
|
|
663
|
-
failureKind
|
|
664
|
-
|
|
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",
|
|
665
627
|
disposeCount,
|
|
666
628
|
usage: usage.total(),
|
|
667
|
-
classification,
|
|
629
|
+
classification: projection,
|
|
668
630
|
...observationMetadata(),
|
|
669
631
|
};
|
|
670
|
-
} finally {
|
|
671
|
-
clearTimeout(timer);
|
|
672
|
-
input.signal?.removeEventListener("abort", forwardCancellation);
|
|
673
|
-
unsubscribe?.();
|
|
674
|
-
disposeOnce();
|
|
675
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
|
+
};
|
|
676
644
|
}
|