@abelxiaoxing/cadence 1.0.1 → 1.0.3
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/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +53 -13
- package/skills/abel-workflow/SKILL.md +60 -22
- package/src/candidate-preflight.ts +1468 -0
- package/src/child-session.ts +136 -30
- package/src/contracts.ts +1343 -63
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +99 -28
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +166 -21
- package/src/patch.ts +331 -33
- package/src/result-store.ts +282 -7
- package/src/runtime.ts +1584 -154
- package/src/scheduler.ts +60 -16
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +15 -3
- package/src/worker.ts +114 -85
package/src/child-session.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import type { Model, Usage } from "@earendil-works/pi-ai";
|
|
2
3
|
import {
|
|
3
4
|
createAgentSession,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
SettingsManager,
|
|
8
9
|
} from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { Type } from "typebox";
|
|
10
|
-
import type { DiffResult, EvidenceResult } from "./contracts.ts";
|
|
11
|
+
import type { ChildFailure, DiffResult, EvidenceResult } from "./contracts.ts";
|
|
11
12
|
import { EmptyResourceLoader } from "./empty-resource-loader.ts";
|
|
12
13
|
import { createScopedTools } from "./scoped-tools.ts";
|
|
13
14
|
import {
|
|
@@ -30,6 +31,22 @@ function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
const ABORT_SETTLE_GRACE_MS = 250;
|
|
35
|
+
|
|
36
|
+
async function settleAbort(promise: Promise<void>): Promise<void> {
|
|
37
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
38
|
+
try {
|
|
39
|
+
await Promise.race([
|
|
40
|
+
promise.catch(() => undefined),
|
|
41
|
+
new Promise<void>((resolve) => {
|
|
42
|
+
timer = setTimeout(resolve, ABORT_SETTLE_GRACE_MS);
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
45
|
+
} finally {
|
|
46
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
const ZERO_USAGE: Usage = {
|
|
34
51
|
input: 0,
|
|
35
52
|
output: 0,
|
|
@@ -45,11 +62,21 @@ export class UsageAggregator {
|
|
|
45
62
|
add(id: string, value: Usage): boolean {
|
|
46
63
|
if (this.ids.has(id)) return false;
|
|
47
64
|
this.ids.add(id);
|
|
65
|
+
const cacheWrite1h =
|
|
66
|
+
this.usage.cacheWrite1h === undefined && value.cacheWrite1h === undefined
|
|
67
|
+
? undefined
|
|
68
|
+
: (this.usage.cacheWrite1h ?? 0) + (value.cacheWrite1h ?? 0);
|
|
69
|
+
const reasoning =
|
|
70
|
+
this.usage.reasoning === undefined && value.reasoning === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: (this.usage.reasoning ?? 0) + (value.reasoning ?? 0);
|
|
48
73
|
this.usage = {
|
|
49
74
|
input: this.usage.input + value.input,
|
|
50
75
|
output: this.usage.output + value.output,
|
|
51
76
|
cacheRead: this.usage.cacheRead + value.cacheRead,
|
|
52
77
|
cacheWrite: this.usage.cacheWrite + value.cacheWrite,
|
|
78
|
+
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
79
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
53
80
|
totalTokens: this.usage.totalTokens + value.totalTokens,
|
|
54
81
|
cost: {
|
|
55
82
|
input: this.usage.cost.input + value.cost.input,
|
|
@@ -61,14 +88,30 @@ export class UsageAggregator {
|
|
|
61
88
|
};
|
|
62
89
|
return true;
|
|
63
90
|
}
|
|
91
|
+
hasUsage(): boolean {
|
|
92
|
+
return this.ids.size > 0;
|
|
93
|
+
}
|
|
64
94
|
total(): Usage {
|
|
65
95
|
return structuredClone(this.usage);
|
|
66
96
|
}
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
function wrapScopedTools(
|
|
99
|
+
function wrapScopedTools(
|
|
100
|
+
roots: string[],
|
|
101
|
+
cwd: string,
|
|
102
|
+
allowedPaths?: string[],
|
|
103
|
+
) {
|
|
70
104
|
const order = ["read", "grep", "find", "ls"];
|
|
71
|
-
return createScopedTools({
|
|
105
|
+
return createScopedTools({
|
|
106
|
+
roots,
|
|
107
|
+
...(allowedPaths
|
|
108
|
+
? {
|
|
109
|
+
allowedPaths: allowedPaths.map((relative) =>
|
|
110
|
+
path.resolve(cwd, relative),
|
|
111
|
+
),
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
})
|
|
72
115
|
.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
|
|
73
116
|
.map((scoped) =>
|
|
74
117
|
defineTool({
|
|
@@ -94,19 +137,7 @@ function wrapScopedTools(roots: string[]) {
|
|
|
94
137
|
|
|
95
138
|
export type ChildFailureKind = "failed" | "cancelled" | "timed-out";
|
|
96
139
|
|
|
97
|
-
export
|
|
98
|
-
cwd: string;
|
|
99
|
-
modelRuntime: ModelRuntime;
|
|
100
|
-
model: Model<string>;
|
|
101
|
-
systemPrompt: string;
|
|
102
|
-
requestId: string;
|
|
103
|
-
role: string;
|
|
104
|
-
phase?: string;
|
|
105
|
-
output: "evidence" | "diff";
|
|
106
|
-
roots: string[];
|
|
107
|
-
timeoutMs: number;
|
|
108
|
-
signal?: AbortSignal;
|
|
109
|
-
}): Promise<
|
|
140
|
+
export type ChildSessionResult =
|
|
110
141
|
| {
|
|
111
142
|
ok: true;
|
|
112
143
|
result: EvidenceResult | DiffResult;
|
|
@@ -119,18 +150,37 @@ export async function runChildSession(input: {
|
|
|
119
150
|
| {
|
|
120
151
|
ok: false;
|
|
121
152
|
error: string;
|
|
153
|
+
failure: ChildFailure;
|
|
122
154
|
failureKind: ChildFailureKind;
|
|
155
|
+
transportFailure: boolean;
|
|
123
156
|
disposeCount: number;
|
|
157
|
+
usage: Usage;
|
|
124
158
|
classification: SubmitClassification;
|
|
125
|
-
}
|
|
126
|
-
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export async function runChildSession(input: {
|
|
162
|
+
cwd: string;
|
|
163
|
+
modelRuntime: ModelRuntime;
|
|
164
|
+
model: Model<string>;
|
|
165
|
+
systemPrompt: string;
|
|
166
|
+
requestId: string;
|
|
167
|
+
taskId?: string;
|
|
168
|
+
role: string;
|
|
169
|
+
phase?: string;
|
|
170
|
+
output: "evidence" | "diff";
|
|
171
|
+
roots: string[];
|
|
172
|
+
allowedPaths?: string[];
|
|
173
|
+
timeoutMs: number;
|
|
174
|
+
signal?: AbortSignal;
|
|
175
|
+
}): Promise<ChildSessionResult> {
|
|
127
176
|
const submit = createSubmitTool({
|
|
128
177
|
requestId: input.requestId,
|
|
178
|
+
taskId: input.taskId,
|
|
129
179
|
role: input.role,
|
|
130
180
|
phase: input.phase ?? "red",
|
|
131
181
|
output: input.output,
|
|
132
182
|
});
|
|
133
|
-
const readTools = wrapScopedTools(input.roots);
|
|
183
|
+
const readTools = wrapScopedTools(input.roots, input.cwd, input.allowedPaths);
|
|
134
184
|
const customTools = [...readTools, submit.tool];
|
|
135
185
|
const toolNames = customTools.map((tool) => tool.name);
|
|
136
186
|
let disposeCount = 0;
|
|
@@ -152,6 +202,8 @@ export async function runChildSession(input: {
|
|
|
152
202
|
| Awaited<ReturnType<typeof createAgentSession>>["session"]
|
|
153
203
|
| undefined;
|
|
154
204
|
let unsubscribe: (() => void) | undefined;
|
|
205
|
+
let abortSettlement: Promise<void> | undefined;
|
|
206
|
+
let assistantSequence = 0;
|
|
155
207
|
const disposeOnce = () => {
|
|
156
208
|
if (session) {
|
|
157
209
|
session.dispose();
|
|
@@ -185,6 +237,23 @@ export async function runChildSession(input: {
|
|
|
185
237
|
identity: submit.getIdentity(),
|
|
186
238
|
};
|
|
187
239
|
};
|
|
240
|
+
const transportFailed = (): boolean => {
|
|
241
|
+
const final = session?.messages
|
|
242
|
+
.filter((message) => message.role === "assistant")
|
|
243
|
+
.at(-1);
|
|
244
|
+
if (final === undefined) return true;
|
|
245
|
+
if (final.stopReason === "error") {
|
|
246
|
+
return (
|
|
247
|
+
submit.getAttempts() === 0 &&
|
|
248
|
+
!final.content.some(
|
|
249
|
+
(content) =>
|
|
250
|
+
content.type === "toolCall" &&
|
|
251
|
+
content.name === "abel_submit_result",
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return final.stopReason === "aborted";
|
|
256
|
+
};
|
|
188
257
|
try {
|
|
189
258
|
abort.signal.throwIfAborted();
|
|
190
259
|
const creation = createAgentSession({
|
|
@@ -209,19 +278,24 @@ export async function runChildSession(input: {
|
|
|
209
278
|
.catch(() => undefined);
|
|
210
279
|
throw error;
|
|
211
280
|
}
|
|
212
|
-
abort.signal.throwIfAborted();
|
|
213
281
|
unsubscribe = session.subscribe((event) => {
|
|
214
282
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
215
|
-
usage.add(
|
|
216
|
-
`${event.message.provider}:${event.message.model}:${event.message.timestamp}`,
|
|
217
|
-
event.message.usage,
|
|
218
|
-
);
|
|
283
|
+
usage.add(`assistant:${assistantSequence++}`, event.message.usage);
|
|
219
284
|
}
|
|
220
285
|
});
|
|
221
|
-
const
|
|
286
|
+
const startAbortSettlement = () => {
|
|
287
|
+
if (session && !abortSettlement) abortSettlement = session.abort();
|
|
288
|
+
return abortSettlement;
|
|
289
|
+
};
|
|
290
|
+
const onAbort = () => {
|
|
291
|
+
void startAbortSettlement()?.catch(() => undefined);
|
|
292
|
+
};
|
|
222
293
|
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
223
294
|
try {
|
|
224
|
-
if (abort.signal.aborted)
|
|
295
|
+
if (abort.signal.aborted) {
|
|
296
|
+
onAbort();
|
|
297
|
+
throw abort.signal.reason;
|
|
298
|
+
}
|
|
225
299
|
await Promise.race([
|
|
226
300
|
session.prompt(input.systemPrompt, { expandPromptTemplates: false }),
|
|
227
301
|
new Promise<never>((_, reject) =>
|
|
@@ -239,20 +313,31 @@ export async function runChildSession(input: {
|
|
|
239
313
|
const attempts = submit.getAttempts();
|
|
240
314
|
const classification = classifySession();
|
|
241
315
|
if (!result || attempts !== 1) {
|
|
316
|
+
const transportFailure = transportFailed();
|
|
317
|
+
const failure =
|
|
318
|
+
submit.getFailure() ??
|
|
319
|
+
(transportFailure
|
|
320
|
+
? ({ kind: "transport", code: "transport-failure" } as const)
|
|
321
|
+
: ({
|
|
322
|
+
kind: "artifact",
|
|
323
|
+
code: "invalid-structural-result",
|
|
324
|
+
} as const));
|
|
242
325
|
disposeOnce();
|
|
243
326
|
return {
|
|
244
327
|
ok: false,
|
|
245
328
|
error: "child did not retain exactly one structural submission",
|
|
329
|
+
failure,
|
|
246
330
|
failureKind: "failed",
|
|
331
|
+
transportFailure,
|
|
247
332
|
disposeCount,
|
|
333
|
+
usage: usage.total(),
|
|
248
334
|
classification,
|
|
249
335
|
};
|
|
250
336
|
}
|
|
251
337
|
const assistants = session.messages.filter((m) => m.role === "assistant");
|
|
252
338
|
const final = assistants.at(-1);
|
|
253
339
|
if (
|
|
254
|
-
|
|
255
|
-
final.role !== "assistant" ||
|
|
340
|
+
final?.role !== "assistant" ||
|
|
256
341
|
final.content.length !== 1 ||
|
|
257
342
|
final.content[0]?.type !== "toolCall" ||
|
|
258
343
|
final.content[0]?.name !== "abel_submit_result"
|
|
@@ -261,8 +346,11 @@ export async function runChildSession(input: {
|
|
|
261
346
|
return {
|
|
262
347
|
ok: false,
|
|
263
348
|
error: "final assistant message is not one structural submit",
|
|
349
|
+
failure: { kind: "artifact", code: "invalid-structural-result" },
|
|
264
350
|
failureKind: "failed",
|
|
351
|
+
transportFailure: false,
|
|
265
352
|
disposeCount,
|
|
353
|
+
usage: usage.total(),
|
|
266
354
|
classification,
|
|
267
355
|
};
|
|
268
356
|
}
|
|
@@ -277,19 +365,37 @@ export async function runChildSession(input: {
|
|
|
277
365
|
classification,
|
|
278
366
|
};
|
|
279
367
|
} catch (error) {
|
|
280
|
-
|
|
368
|
+
if (abort.signal.aborted && session) {
|
|
369
|
+
const settlement = abortSettlement ?? session.abort();
|
|
370
|
+
abortSettlement = settlement;
|
|
371
|
+
await settleAbort(settlement);
|
|
372
|
+
}
|
|
281
373
|
const failureKind: ChildFailureKind = timedOut
|
|
282
374
|
? "timed-out"
|
|
283
375
|
: input.signal !== undefined &&
|
|
284
376
|
(input.signal.aborted || abort.signal.reason === input.signal.reason)
|
|
285
377
|
? "cancelled"
|
|
286
378
|
: "failed";
|
|
379
|
+
const classification = classifySession();
|
|
380
|
+
const transportFailure = transportFailed();
|
|
381
|
+
const failure: ChildFailure = timedOut
|
|
382
|
+
? { kind: "transport", code: "timeout" }
|
|
383
|
+
: failureKind === "cancelled"
|
|
384
|
+
? { kind: "cancelled", code: "cancelled" }
|
|
385
|
+
: (submit.getFailure() ??
|
|
386
|
+
(transportFailure
|
|
387
|
+
? { kind: "transport", code: "transport-failure" }
|
|
388
|
+
: { kind: "artifact", code: "invalid-structural-result" }));
|
|
389
|
+
disposeOnce();
|
|
287
390
|
return {
|
|
288
391
|
ok: false,
|
|
289
392
|
error: (error as Error).message,
|
|
393
|
+
failure,
|
|
290
394
|
failureKind,
|
|
395
|
+
transportFailure,
|
|
291
396
|
disposeCount,
|
|
292
|
-
|
|
397
|
+
usage: usage.total(),
|
|
398
|
+
classification,
|
|
293
399
|
};
|
|
294
400
|
} finally {
|
|
295
401
|
clearTimeout(timer);
|