@abelxiaoxing/cadence 1.8.2 → 1.8.4
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 +71 -531
- package/agents/design-explorer.md +15 -23
- package/agents/diagnosis-worker.md +12 -23
- package/agents/implementation-worker.md +16 -34
- package/config/.env.example +2 -5
- package/package.json +1 -1
- package/prompts/abel-diagnose.md +5 -5
- package/src/candidate-context.ts +289 -0
- package/src/contracts.ts +12 -22
- package/src/durable-contracts.ts +1 -5
- package/src/durable-workflow.ts +3 -159
- package/src/evidence-draft.ts +1 -1
- package/src/index.ts +12 -16
- package/src/package-candidate-process.ts +206 -0
- package/src/package-candidate.ts +11 -214
- package/src/package-workflow.ts +5 -42
- package/src/packet-runtime.ts +144 -255
- package/src/phase-execution.ts +134 -230
- package/src/startup-card.ts +17 -23
- package/src/subagent-process.ts +476 -0
- package/src/transport-budget.ts +1 -3
- package/src/workflow-policy.ts +9 -18
- package/src/workflow-state-machine.ts +1 -15
- package/config/routes.example.json +0 -39
- package/src/child-budget.ts +0 -58
- package/src/child-model.ts +0 -142
- package/src/child-session.ts +0 -640
- package/src/parent-provider.ts +0 -375
- package/src/route-policy.ts +0 -545
- package/src/submit-tool.ts +0 -950
- package/src/worker-broker.ts +0 -806
package/src/child-session.ts
DELETED
|
@@ -1,640 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
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";
|
|
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";
|
|
19
|
-
import type {
|
|
20
|
-
ChildFailure,
|
|
21
|
-
DiffResult,
|
|
22
|
-
EvidenceResult,
|
|
23
|
-
IdentityDimension,
|
|
24
|
-
SafeFailureDetails,
|
|
25
|
-
} from "./contracts.ts";
|
|
26
|
-
import {
|
|
27
|
-
createScopedTools,
|
|
28
|
-
type Observation,
|
|
29
|
-
ScopedObservationCollector,
|
|
30
|
-
TOOL_LIMITS,
|
|
31
|
-
} from "./scoped-tools.ts";
|
|
32
|
-
import {
|
|
33
|
-
type CandidateArtifactSubmission,
|
|
34
|
-
type CandidateArtifactSubmissionResult,
|
|
35
|
-
createCandidateArtifactTool,
|
|
36
|
-
createStructuredPatchTool,
|
|
37
|
-
createSubmitTool,
|
|
38
|
-
type SubmitClassification,
|
|
39
|
-
} from "./submit-tool.ts";
|
|
40
|
-
import { serializeTaskLedgerProjection } from "./task-ledger.ts";
|
|
41
|
-
import { TransportTimeout } from "./transport-budget.ts";
|
|
42
|
-
|
|
43
|
-
export const CHILD_EXECUTION_LIMITS = Object.freeze({
|
|
44
|
-
maxTurns: 64,
|
|
45
|
-
maxContextBytes: 4 * 1024 * 1024,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const ZERO_USAGE: Usage = {
|
|
49
|
-
input: 0,
|
|
50
|
-
output: 0,
|
|
51
|
-
cacheRead: 0,
|
|
52
|
-
cacheWrite: 0,
|
|
53
|
-
totalTokens: 0,
|
|
54
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export class UsageAggregator {
|
|
58
|
-
private readonly ids = new Set<string>();
|
|
59
|
-
private usage: Usage = structuredClone(ZERO_USAGE);
|
|
60
|
-
add(id: string, value: Usage): boolean {
|
|
61
|
-
if (this.ids.has(id)) return false;
|
|
62
|
-
this.ids.add(id);
|
|
63
|
-
const cacheWrite1h =
|
|
64
|
-
this.usage.cacheWrite1h === undefined && value.cacheWrite1h === undefined
|
|
65
|
-
? undefined
|
|
66
|
-
: (this.usage.cacheWrite1h ?? 0) + (value.cacheWrite1h ?? 0);
|
|
67
|
-
const reasoning =
|
|
68
|
-
this.usage.reasoning === undefined && value.reasoning === undefined
|
|
69
|
-
? undefined
|
|
70
|
-
: (this.usage.reasoning ?? 0) + (value.reasoning ?? 0);
|
|
71
|
-
this.usage = {
|
|
72
|
-
input: this.usage.input + value.input,
|
|
73
|
-
output: this.usage.output + value.output,
|
|
74
|
-
cacheRead: this.usage.cacheRead + value.cacheRead,
|
|
75
|
-
cacheWrite: this.usage.cacheWrite + value.cacheWrite,
|
|
76
|
-
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
77
|
-
...(reasoning === undefined ? {} : { reasoning }),
|
|
78
|
-
totalTokens: this.usage.totalTokens + value.totalTokens,
|
|
79
|
-
cost: {
|
|
80
|
-
input: this.usage.cost.input + value.cost.input,
|
|
81
|
-
output: this.usage.cost.output + value.cost.output,
|
|
82
|
-
cacheRead: this.usage.cost.cacheRead + value.cost.cacheRead,
|
|
83
|
-
cacheWrite: this.usage.cost.cacheWrite + value.cost.cacheWrite,
|
|
84
|
-
total: this.usage.cost.total + value.cost.total,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
hasUsage(): boolean {
|
|
90
|
-
return this.ids.size > 0;
|
|
91
|
-
}
|
|
92
|
-
total(): Usage {
|
|
93
|
-
return structuredClone(this.usage);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function wrapScopedTools(
|
|
98
|
-
roots: string[],
|
|
99
|
-
cwd: string,
|
|
100
|
-
allowedPaths?: string[],
|
|
101
|
-
observer?: (observation: Observation) => void,
|
|
102
|
-
) {
|
|
103
|
-
const order = ["read", "grep", "find", "ls"];
|
|
104
|
-
return createScopedTools({
|
|
105
|
-
cwd,
|
|
106
|
-
roots,
|
|
107
|
-
...(allowedPaths
|
|
108
|
-
? {
|
|
109
|
-
allowedPaths: allowedPaths.map((relative) =>
|
|
110
|
-
path.resolve(cwd, relative),
|
|
111
|
-
),
|
|
112
|
-
}
|
|
113
|
-
: {}),
|
|
114
|
-
...(observer ? { observer } : {}),
|
|
115
|
-
})
|
|
116
|
-
.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
|
|
117
|
-
.map((scoped) =>
|
|
118
|
-
defineTool({
|
|
119
|
-
name: scoped.name,
|
|
120
|
-
label: scoped.name,
|
|
121
|
-
description: scoped.description,
|
|
122
|
-
parameters:
|
|
123
|
-
scoped.name === "grep"
|
|
124
|
-
? Type.Object({
|
|
125
|
-
path: Type.Optional(Type.String()),
|
|
126
|
-
pattern: Type.String(),
|
|
127
|
-
})
|
|
128
|
-
: scoped.name === "find"
|
|
129
|
-
? Type.Object({
|
|
130
|
-
path: Type.Optional(Type.String()),
|
|
131
|
-
pattern: Type.String(),
|
|
132
|
-
limit: Type.Optional(
|
|
133
|
-
Type.Integer({
|
|
134
|
-
minimum: 1,
|
|
135
|
-
maximum: TOOL_LIMITS.maxEntries,
|
|
136
|
-
}),
|
|
137
|
-
),
|
|
138
|
-
})
|
|
139
|
-
: scoped.name === "read"
|
|
140
|
-
? Type.Object({
|
|
141
|
-
path: Type.String(),
|
|
142
|
-
offset: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
143
|
-
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
144
|
-
})
|
|
145
|
-
: Type.Object({
|
|
146
|
-
path: Type.Optional(Type.String()),
|
|
147
|
-
}),
|
|
148
|
-
async execute(_id, params, signal) {
|
|
149
|
-
const result = await scoped.execute(
|
|
150
|
-
params as Record<string, unknown>,
|
|
151
|
-
signal,
|
|
152
|
-
);
|
|
153
|
-
return {
|
|
154
|
-
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
155
|
-
details: result,
|
|
156
|
-
};
|
|
157
|
-
},
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export type ChildFailureKind = "failed" | "cancelled" | "timed-out";
|
|
163
|
-
|
|
164
|
-
function finalDeliveryContent(message: AssistantMessage | undefined) {
|
|
165
|
-
return (
|
|
166
|
-
message?.content.filter(
|
|
167
|
-
(content) =>
|
|
168
|
-
content.type !== "thinking" &&
|
|
169
|
-
!(content.type === "text" && content.text.trim().length === 0),
|
|
170
|
-
) ?? []
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function safeChildError(failure: ChildFailure): string {
|
|
175
|
-
switch (failure.kind) {
|
|
176
|
-
case "environment":
|
|
177
|
-
return "child environment unavailable";
|
|
178
|
-
case "transport":
|
|
179
|
-
switch (failure.code) {
|
|
180
|
-
case "child-timeout":
|
|
181
|
-
case "timeout":
|
|
182
|
-
return "child phase timeout";
|
|
183
|
-
case "child-no-final-assistant":
|
|
184
|
-
return "child produced no final assistant";
|
|
185
|
-
case "child-provider-stream-aborted":
|
|
186
|
-
return "child provider stream aborted";
|
|
187
|
-
case "child-provider-stream-error":
|
|
188
|
-
case "transport-failure":
|
|
189
|
-
return "child provider stream failed";
|
|
190
|
-
}
|
|
191
|
-
return "child provider stream failed";
|
|
192
|
-
case "artifact":
|
|
193
|
-
return failure.code === "child-no-structural-submit"
|
|
194
|
-
? "child produced no structural submission"
|
|
195
|
-
: "child structural submission is invalid";
|
|
196
|
-
case "cancelled":
|
|
197
|
-
return "child phase cancelled";
|
|
198
|
-
case "stale":
|
|
199
|
-
case "approval-boundary":
|
|
200
|
-
case "verification-adapter":
|
|
201
|
-
case "execution-limit":
|
|
202
|
-
case "result-limit":
|
|
203
|
-
return "child result rejected";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function submitDetails(
|
|
208
|
-
classification: SubmitClassification,
|
|
209
|
-
): SafeFailureDetails {
|
|
210
|
-
const identityMismatch = (
|
|
211
|
-
["request", "role", "task", "phase"] as IdentityDimension[]
|
|
212
|
-
).filter((dimension) => !classification.identity[dimension]);
|
|
213
|
-
return {
|
|
214
|
-
finalCategory: classification.finalCategory,
|
|
215
|
-
submitAttempts: Math.min(classification.attempts, 2),
|
|
216
|
-
schema: classification.schema,
|
|
217
|
-
...(identityMismatch.length > 0 ? { identityMismatch } : {}),
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function withSubmitDetails(
|
|
222
|
-
failure: ChildFailure,
|
|
223
|
-
classification: SubmitClassification,
|
|
224
|
-
): ChildFailure {
|
|
225
|
-
return failure.kind === "artifact"
|
|
226
|
-
? {
|
|
227
|
-
...failure,
|
|
228
|
-
details: {
|
|
229
|
-
...failure.details,
|
|
230
|
-
...submitDetails(classification),
|
|
231
|
-
},
|
|
232
|
-
}
|
|
233
|
-
: failure;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export type ChildSessionResult =
|
|
237
|
-
| {
|
|
238
|
-
ok: true;
|
|
239
|
-
result: EvidenceResult | DiffResult | CandidateArtifactSubmissionResult;
|
|
240
|
-
toolNames: string[];
|
|
241
|
-
submitCount: number;
|
|
242
|
-
disposeCount: number;
|
|
243
|
-
usage: Usage;
|
|
244
|
-
classification: SubmitClassification;
|
|
245
|
-
observations?: { observations: Observation[]; truncated: boolean };
|
|
246
|
-
}
|
|
247
|
-
| {
|
|
248
|
-
ok: false;
|
|
249
|
-
error: string;
|
|
250
|
-
failure: ChildFailure;
|
|
251
|
-
failureKind: ChildFailureKind;
|
|
252
|
-
transportFailure: boolean;
|
|
253
|
-
disposeCount: number;
|
|
254
|
-
usage: Usage;
|
|
255
|
-
classification: SubmitClassification;
|
|
256
|
-
observations?: { observations: Observation[]; truncated: boolean };
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
export async function runChildSession(input: {
|
|
260
|
-
cwd: string;
|
|
261
|
-
modelRuntime: ChildModelClient;
|
|
262
|
-
model: Model<string>;
|
|
263
|
-
systemPrompt: string;
|
|
264
|
-
requestId: string;
|
|
265
|
-
taskId?: string;
|
|
266
|
-
role: string;
|
|
267
|
-
phase?: string;
|
|
268
|
-
output: "evidence" | "diff";
|
|
269
|
-
roots: string[];
|
|
270
|
-
allowedPaths?: string[];
|
|
271
|
-
timeoutMs: number;
|
|
272
|
-
signal?: AbortSignal;
|
|
273
|
-
ledgerProjection?: unknown;
|
|
274
|
-
captureObservations?: boolean;
|
|
275
|
-
candidateArtifact?: CandidateArtifactSubmission;
|
|
276
|
-
structuredPatch?: {
|
|
277
|
-
workspaceRoot: string;
|
|
278
|
-
writePaths: string[];
|
|
279
|
-
deletePaths: string[];
|
|
280
|
-
};
|
|
281
|
-
onStreamStart?: () => void;
|
|
282
|
-
onStreamHeaders?: () => void;
|
|
283
|
-
onStreamProgress?: () => void;
|
|
284
|
-
}): Promise<ChildSessionResult> {
|
|
285
|
-
const submit = input.candidateArtifact
|
|
286
|
-
? createCandidateArtifactTool(input.candidateArtifact)
|
|
287
|
-
: input.structuredPatch
|
|
288
|
-
? createStructuredPatchTool({
|
|
289
|
-
requestId: input.requestId,
|
|
290
|
-
taskId: input.taskId,
|
|
291
|
-
role: input.role,
|
|
292
|
-
phase: input.phase ?? "red",
|
|
293
|
-
workspaceRoot: input.structuredPatch.workspaceRoot,
|
|
294
|
-
writePaths: input.structuredPatch.writePaths,
|
|
295
|
-
deletePaths: input.structuredPatch.deletePaths,
|
|
296
|
-
})
|
|
297
|
-
: createSubmitTool({
|
|
298
|
-
requestId: input.requestId,
|
|
299
|
-
taskId: input.taskId,
|
|
300
|
-
role: input.role,
|
|
301
|
-
phase: input.phase ?? "red",
|
|
302
|
-
output: input.output,
|
|
303
|
-
});
|
|
304
|
-
const observationCollector = input.captureObservations
|
|
305
|
-
? new ScopedObservationCollector()
|
|
306
|
-
: undefined;
|
|
307
|
-
const readTools = wrapScopedTools(
|
|
308
|
-
input.roots,
|
|
309
|
-
input.cwd,
|
|
310
|
-
input.allowedPaths,
|
|
311
|
-
observationCollector?.observe,
|
|
312
|
-
);
|
|
313
|
-
const customTools = [...readTools, submit.tool];
|
|
314
|
-
const toolNames = customTools.map((tool) => tool.name);
|
|
315
|
-
let disposeCount = 0;
|
|
316
|
-
const usage = new UsageAggregator();
|
|
317
|
-
const effectivePrompt =
|
|
318
|
-
input.ledgerProjection === undefined
|
|
319
|
-
? input.systemPrompt
|
|
320
|
-
: [
|
|
321
|
-
input.systemPrompt,
|
|
322
|
-
"",
|
|
323
|
-
"<abel-task-ledger-projection>",
|
|
324
|
-
serializeTaskLedgerProjection(input.ledgerProjection),
|
|
325
|
-
"</abel-task-ledger-projection>",
|
|
326
|
-
].join("\n");
|
|
327
|
-
const observationMetadata = () =>
|
|
328
|
-
observationCollector
|
|
329
|
-
? { observations: observationCollector.projection() }
|
|
330
|
-
: {};
|
|
331
|
-
const abort = new AbortController();
|
|
332
|
-
const forwardCancellation = () =>
|
|
333
|
-
abort.abort(input.signal?.reason ?? new Error("child phase cancelled"));
|
|
334
|
-
if (input.signal?.aborted) forwardCancellation();
|
|
335
|
-
else
|
|
336
|
-
input.signal?.addEventListener("abort", forwardCancellation, {
|
|
337
|
-
once: true,
|
|
338
|
-
});
|
|
339
|
-
let timedOut = false;
|
|
340
|
-
const timer = setTimeout(() => {
|
|
341
|
-
timedOut = true;
|
|
342
|
-
abort.abort(new Error("child phase timeout"));
|
|
343
|
-
}, input.timeoutMs);
|
|
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();
|
|
360
|
-
let assistantSequence = 0;
|
|
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 = () => {
|
|
368
|
-
try {
|
|
369
|
-
input.onStreamStart?.();
|
|
370
|
-
} catch {
|
|
371
|
-
/* observation only */
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
const observeProgress = () => {
|
|
375
|
-
try {
|
|
376
|
-
input.onStreamProgress?.();
|
|
377
|
-
} catch {
|
|
378
|
-
/* observation only */
|
|
379
|
-
}
|
|
380
|
-
};
|
|
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",
|
|
437
|
-
stage: "child-finalization",
|
|
438
|
-
};
|
|
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;
|
|
479
|
-
}
|
|
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
|
-
});
|
|
551
|
-
}
|
|
552
|
-
if (failure) break;
|
|
553
|
-
if (submit.getResult() !== undefined) break;
|
|
554
|
-
if (
|
|
555
|
-
structuralAttempts >= 2 ||
|
|
556
|
-
submit.getFailure()?.kind === "result-limit"
|
|
557
|
-
) {
|
|
558
|
-
failure = missingResult();
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
} catch (error) {
|
|
563
|
-
failure =
|
|
564
|
-
error instanceof TransportTimeout
|
|
565
|
-
? {
|
|
566
|
-
kind: "transport",
|
|
567
|
-
code: error.code,
|
|
568
|
-
stage: "child-provider-stream",
|
|
569
|
-
}
|
|
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);
|
|
603
|
-
return {
|
|
604
|
-
ok: false,
|
|
605
|
-
error:
|
|
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",
|
|
623
|
-
disposeCount,
|
|
624
|
-
usage: usage.total(),
|
|
625
|
-
classification: projection,
|
|
626
|
-
...observationMetadata(),
|
|
627
|
-
};
|
|
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
|
-
};
|
|
640
|
-
}
|