@abelxiaoxing/cadence 1.0.0 → 1.0.2
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 +50 -22
- package/package.json +3 -1
- package/prompts/abel-design.md +1 -0
- package/prompts/abel-diagnose.md +1 -0
- package/prompts/abel-implement.md +14 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +98 -10
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +191 -14
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +124 -16
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +1013 -60
- package/src/scheduler.ts +47 -10
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +747 -0
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
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,
|
|
@@ -16,6 +17,20 @@ import {
|
|
|
16
17
|
type SubmitClassification,
|
|
17
18
|
} from "./submit-tool.ts";
|
|
18
19
|
|
|
20
|
+
function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
return new Promise<T>((resolve, reject) => {
|
|
23
|
+
const onAbort = () => {
|
|
24
|
+
signal.removeEventListener("abort", onAbort);
|
|
25
|
+
reject(signal.reason);
|
|
26
|
+
};
|
|
27
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
28
|
+
promise
|
|
29
|
+
.then(resolve, reject)
|
|
30
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
const ZERO_USAGE: Usage = {
|
|
20
35
|
input: 0,
|
|
21
36
|
output: 0,
|
|
@@ -52,9 +67,22 @@ export class UsageAggregator {
|
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
function wrapScopedTools(
|
|
70
|
+
function wrapScopedTools(
|
|
71
|
+
roots: string[],
|
|
72
|
+
cwd: string,
|
|
73
|
+
allowedPaths?: string[],
|
|
74
|
+
) {
|
|
56
75
|
const order = ["read", "grep", "find", "ls"];
|
|
57
|
-
return createScopedTools({
|
|
76
|
+
return createScopedTools({
|
|
77
|
+
roots,
|
|
78
|
+
...(allowedPaths
|
|
79
|
+
? {
|
|
80
|
+
allowedPaths: allowedPaths.map((relative) =>
|
|
81
|
+
path.resolve(cwd, relative),
|
|
82
|
+
),
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
85
|
+
})
|
|
58
86
|
.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
|
|
59
87
|
.map((scoped) =>
|
|
60
88
|
defineTool({
|
|
@@ -78,17 +106,22 @@ function wrapScopedTools(roots: string[]) {
|
|
|
78
106
|
);
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
export type ChildFailureKind = "failed" | "cancelled" | "timed-out";
|
|
110
|
+
|
|
81
111
|
export async function runChildSession(input: {
|
|
82
112
|
cwd: string;
|
|
83
113
|
modelRuntime: ModelRuntime;
|
|
84
114
|
model: Model<string>;
|
|
85
115
|
systemPrompt: string;
|
|
86
116
|
requestId: string;
|
|
117
|
+
taskId?: string;
|
|
87
118
|
role: string;
|
|
88
119
|
phase?: string;
|
|
89
120
|
output: "evidence" | "diff";
|
|
90
121
|
roots: string[];
|
|
122
|
+
allowedPaths?: string[];
|
|
91
123
|
timeoutMs: number;
|
|
124
|
+
signal?: AbortSignal;
|
|
92
125
|
}): Promise<
|
|
93
126
|
| {
|
|
94
127
|
ok: true;
|
|
@@ -102,26 +135,37 @@ export async function runChildSession(input: {
|
|
|
102
135
|
| {
|
|
103
136
|
ok: false;
|
|
104
137
|
error: string;
|
|
138
|
+
failureKind: ChildFailureKind;
|
|
139
|
+
transportFailure: boolean;
|
|
105
140
|
disposeCount: number;
|
|
106
141
|
classification: SubmitClassification;
|
|
107
142
|
}
|
|
108
143
|
> {
|
|
109
144
|
const submit = createSubmitTool({
|
|
110
145
|
requestId: input.requestId,
|
|
146
|
+
taskId: input.taskId,
|
|
111
147
|
role: input.role,
|
|
112
148
|
phase: input.phase ?? "red",
|
|
113
149
|
output: input.output,
|
|
114
150
|
});
|
|
115
|
-
const readTools = wrapScopedTools(input.roots);
|
|
151
|
+
const readTools = wrapScopedTools(input.roots, input.cwd, input.allowedPaths);
|
|
116
152
|
const customTools = [...readTools, submit.tool];
|
|
117
153
|
const toolNames = customTools.map((tool) => tool.name);
|
|
118
154
|
let disposeCount = 0;
|
|
119
155
|
const usage = new UsageAggregator();
|
|
120
156
|
const abort = new AbortController();
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
const forwardCancellation = () =>
|
|
158
|
+
abort.abort(input.signal?.reason ?? new Error("child phase cancelled"));
|
|
159
|
+
if (input.signal?.aborted) forwardCancellation();
|
|
160
|
+
else
|
|
161
|
+
input.signal?.addEventListener("abort", forwardCancellation, {
|
|
162
|
+
once: true,
|
|
163
|
+
});
|
|
164
|
+
let timedOut = false;
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
timedOut = true;
|
|
167
|
+
abort.abort(new Error("child phase timeout"));
|
|
168
|
+
}, input.timeoutMs);
|
|
125
169
|
let session:
|
|
126
170
|
| Awaited<ReturnType<typeof createAgentSession>>["session"]
|
|
127
171
|
| undefined;
|
|
@@ -159,8 +203,26 @@ export async function runChildSession(input: {
|
|
|
159
203
|
identity: submit.getIdentity(),
|
|
160
204
|
};
|
|
161
205
|
};
|
|
206
|
+
const transportFailed = (): boolean => {
|
|
207
|
+
const final = session?.messages
|
|
208
|
+
.filter((message) => message.role === "assistant")
|
|
209
|
+
.at(-1);
|
|
210
|
+
if (final === undefined) return true;
|
|
211
|
+
if (final.stopReason === "error") {
|
|
212
|
+
return (
|
|
213
|
+
submit.getAttempts() === 0 &&
|
|
214
|
+
!final.content.some(
|
|
215
|
+
(content) =>
|
|
216
|
+
content.type === "toolCall" &&
|
|
217
|
+
content.name === "abel_submit_result",
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return final.stopReason === "aborted";
|
|
222
|
+
};
|
|
162
223
|
try {
|
|
163
|
-
(
|
|
224
|
+
abort.signal.throwIfAborted();
|
|
225
|
+
const creation = createAgentSession({
|
|
164
226
|
cwd: input.cwd,
|
|
165
227
|
modelRuntime: input.modelRuntime,
|
|
166
228
|
model: input.model,
|
|
@@ -173,7 +235,16 @@ export async function runChildSession(input: {
|
|
|
173
235
|
compaction: { enabled: false },
|
|
174
236
|
retry: { enabled: false, maxRetries: 0, provider: { maxRetries: 0 } },
|
|
175
237
|
}),
|
|
176
|
-
})
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
({ session } = await abortable(creation, abort.signal));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
void creation
|
|
243
|
+
.then(({ session: lateSession }) => lateSession.dispose())
|
|
244
|
+
.catch(() => undefined);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
abort.signal.throwIfAborted();
|
|
177
248
|
unsubscribe = session.subscribe((event) => {
|
|
178
249
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
179
250
|
usage.add(
|
|
@@ -185,6 +256,7 @@ export async function runChildSession(input: {
|
|
|
185
256
|
const onAbort = () => void session?.abort();
|
|
186
257
|
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
187
258
|
try {
|
|
259
|
+
if (abort.signal.aborted) throw abort.signal.reason;
|
|
188
260
|
await Promise.race([
|
|
189
261
|
session.prompt(input.systemPrompt, { expandPromptTemplates: false }),
|
|
190
262
|
new Promise<never>((_, reject) =>
|
|
@@ -202,10 +274,13 @@ export async function runChildSession(input: {
|
|
|
202
274
|
const attempts = submit.getAttempts();
|
|
203
275
|
const classification = classifySession();
|
|
204
276
|
if (!result || attempts !== 1) {
|
|
277
|
+
const transportFailure = transportFailed();
|
|
205
278
|
disposeOnce();
|
|
206
279
|
return {
|
|
207
280
|
ok: false,
|
|
208
281
|
error: "child did not retain exactly one structural submission",
|
|
282
|
+
failureKind: "failed",
|
|
283
|
+
transportFailure,
|
|
209
284
|
disposeCount,
|
|
210
285
|
classification,
|
|
211
286
|
};
|
|
@@ -223,6 +298,8 @@ export async function runChildSession(input: {
|
|
|
223
298
|
return {
|
|
224
299
|
ok: false,
|
|
225
300
|
error: "final assistant message is not one structural submit",
|
|
301
|
+
failureKind: "failed",
|
|
302
|
+
transportFailure: false,
|
|
226
303
|
disposeCount,
|
|
227
304
|
classification,
|
|
228
305
|
};
|
|
@@ -238,15 +315,26 @@ export async function runChildSession(input: {
|
|
|
238
315
|
classification,
|
|
239
316
|
};
|
|
240
317
|
} catch (error) {
|
|
318
|
+
const failureKind: ChildFailureKind = timedOut
|
|
319
|
+
? "timed-out"
|
|
320
|
+
: input.signal !== undefined &&
|
|
321
|
+
(input.signal.aborted || abort.signal.reason === input.signal.reason)
|
|
322
|
+
? "cancelled"
|
|
323
|
+
: "failed";
|
|
324
|
+
const classification = classifySession();
|
|
325
|
+
const transportFailure = transportFailed();
|
|
241
326
|
disposeOnce();
|
|
242
327
|
return {
|
|
243
328
|
ok: false,
|
|
244
329
|
error: (error as Error).message,
|
|
330
|
+
failureKind,
|
|
331
|
+
transportFailure,
|
|
245
332
|
disposeCount,
|
|
246
|
-
classification
|
|
333
|
+
classification,
|
|
247
334
|
};
|
|
248
335
|
} finally {
|
|
249
336
|
clearTimeout(timer);
|
|
337
|
+
input.signal?.removeEventListener("abort", forwardCancellation);
|
|
250
338
|
unsubscribe?.();
|
|
251
339
|
disposeOnce();
|
|
252
340
|
}
|