@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/file-snapshot.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
// File-level snapshots: content hashes for observed files, deterministic
|
|
2
|
-
//
|
|
2
|
+
// recursive content manifests for observed directories, and explicit absent
|
|
3
3
|
// markers for proposed new paths. Currency is decided per bound path; no
|
|
4
4
|
// global workspace revision participates.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
statSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
8
15
|
|
|
9
16
|
export interface FileBound {
|
|
10
17
|
kind: "file";
|
|
@@ -37,7 +44,57 @@ export function snapshotFile(root: string, relPath: string): FileBound | null {
|
|
|
37
44
|
return { sha256: sha256(bytes), bytes: bytes.length, kind: "file" };
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
function updateManifest(
|
|
48
|
+
hash: ReturnType<typeof createHash>,
|
|
49
|
+
...fields: Array<string | Buffer>
|
|
50
|
+
): void {
|
|
51
|
+
for (const field of fields) {
|
|
52
|
+
const bytes = typeof field === "string" ? Buffer.from(field) : field;
|
|
53
|
+
hash.update(`${bytes.length}:`);
|
|
54
|
+
hash.update(bytes);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function recursiveDirectoryManifest(abs: string): string {
|
|
59
|
+
const hash = createHash("sha256");
|
|
60
|
+
updateManifest(hash, "cadence-directory-manifest-v2");
|
|
61
|
+
const visit = (directory: string, prefix: string): void => {
|
|
62
|
+
const names = readdirSync(directory).sort();
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
65
|
+
const current = join(directory, name);
|
|
66
|
+
const stat = lstatSync(current);
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
updateManifest(hash, "dir", relative, String(stat.mode & 0o777));
|
|
69
|
+
visit(current, relative);
|
|
70
|
+
} else if (stat.isFile()) {
|
|
71
|
+
const bytes = readFileSync(current);
|
|
72
|
+
updateManifest(
|
|
73
|
+
hash,
|
|
74
|
+
"file",
|
|
75
|
+
relative,
|
|
76
|
+
String(stat.mode & 0o777),
|
|
77
|
+
String(bytes.length),
|
|
78
|
+
sha256(bytes),
|
|
79
|
+
);
|
|
80
|
+
} else if (stat.isSymbolicLink()) {
|
|
81
|
+
updateManifest(hash, "symlink", relative, readlinkSync(current));
|
|
82
|
+
} else {
|
|
83
|
+
updateManifest(
|
|
84
|
+
hash,
|
|
85
|
+
"special",
|
|
86
|
+
relative,
|
|
87
|
+
String(stat.mode),
|
|
88
|
+
String(stat.size),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
visit(abs, "");
|
|
94
|
+
return hash.digest("hex");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Deterministic recursive directory manifest bound to installed content. */
|
|
41
98
|
export function snapshotDirManifest(
|
|
42
99
|
root: string,
|
|
43
100
|
relDir: string,
|
|
@@ -45,10 +102,7 @@ export function snapshotDirManifest(
|
|
|
45
102
|
const abs = resolve(root, relDir);
|
|
46
103
|
const st = statSync(abs, { throwIfNoEntry: false });
|
|
47
104
|
if (!st?.isDirectory()) return null;
|
|
48
|
-
|
|
49
|
-
.map((e) => `${e.name}:${e.isDirectory() ? "dir" : "file"}`)
|
|
50
|
-
.sort();
|
|
51
|
-
return { manifest: sha256(Buffer.from(entries.join("\n"))), kind: "dir" };
|
|
105
|
+
return { manifest: recursiveDirectoryManifest(abs), kind: "dir" };
|
|
52
106
|
}
|
|
53
107
|
|
|
54
108
|
/** Build a bound map for the given file paths, observed directories, and absent markers. */
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { type Activation, activateTool, deactivateTool } from "./activation.ts";
|
|
17
17
|
import { ACTIONS } from "./contracts.ts";
|
|
18
|
+
import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
18
19
|
import { Runtime } from "./runtime.ts";
|
|
19
20
|
import {
|
|
20
21
|
ACTIVITY_DETAILS_KEY,
|
|
@@ -68,20 +69,27 @@ function hasExpandedPromptMarker(
|
|
|
68
69
|
);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function isVerifiedStageInvocation(
|
|
73
|
+
pi: ExtensionAPI,
|
|
74
|
+
activation: Activation,
|
|
75
|
+
name: EligiblePrompt,
|
|
76
|
+
prompt: string,
|
|
77
|
+
): boolean {
|
|
78
|
+
return (
|
|
79
|
+
hasPackageProvenance(pi, name) &&
|
|
80
|
+
hasExpandedPromptMarker(prompt, name) &&
|
|
81
|
+
(activation.isActive() || activation.state === "inactive")
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
function activateDispatcher(
|
|
72
86
|
pi: ExtensionAPI,
|
|
73
87
|
activation: Activation,
|
|
74
88
|
name: EligiblePrompt,
|
|
75
89
|
prompt: string,
|
|
76
90
|
): void {
|
|
77
|
-
if (
|
|
78
|
-
!hasPackageProvenance(pi, name) ||
|
|
79
|
-
!hasExpandedPromptMarker(prompt, name)
|
|
80
|
-
) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
91
|
+
if (!isVerifiedStageInvocation(pi, activation, name, prompt)) return;
|
|
83
92
|
if (!activation.isActive()) {
|
|
84
|
-
if (activation.state !== "inactive") return;
|
|
85
93
|
activation.request();
|
|
86
94
|
activation.activate();
|
|
87
95
|
}
|
|
@@ -91,15 +99,24 @@ function activateDispatcher(
|
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
|
|
103
|
+
if (typeof result !== "object" || result === null || !("usage" in result)) {
|
|
104
|
+
return { payload: result };
|
|
105
|
+
}
|
|
106
|
+
const { usage, ...payload } = result as Record<string, unknown>;
|
|
107
|
+
return { payload, usage };
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
export default function register(pi: ExtensionAPI): void {
|
|
95
|
-
const
|
|
111
|
+
const parentPayloadBridge = new ParentPayloadBridge();
|
|
112
|
+
const runtime = new Runtime({ parentPayloadBridge });
|
|
96
113
|
const activity = new ActivityController();
|
|
97
114
|
|
|
98
115
|
pi.registerTool({
|
|
99
116
|
name: DISPATCH_TOOL,
|
|
100
117
|
label: "Abel Dispatch",
|
|
101
118
|
description:
|
|
102
|
-
"Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
119
|
+
"Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, apply a parent-only stable AGENTS checkpoint, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
103
120
|
parameters: {
|
|
104
121
|
type: "object",
|
|
105
122
|
properties: {
|
|
@@ -112,12 +129,32 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
112
129
|
type: "string",
|
|
113
130
|
description: "Retained result id for apply/discard",
|
|
114
131
|
},
|
|
132
|
+
requestId: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Current Implement apply/discard operation identity",
|
|
135
|
+
},
|
|
136
|
+
rejection: {
|
|
137
|
+
type: "object",
|
|
138
|
+
description: "Typed parent rejection for Implement discard",
|
|
139
|
+
},
|
|
140
|
+
agentsCheckpoint: {
|
|
141
|
+
type: "object",
|
|
142
|
+
description:
|
|
143
|
+
"Parent-owned approved managed-block checkpoint for action=apply (mutually exclusive with resultId)",
|
|
144
|
+
},
|
|
115
145
|
},
|
|
116
146
|
required: ["action"],
|
|
117
147
|
},
|
|
118
148
|
async execute(
|
|
119
149
|
toolCallId: string,
|
|
120
|
-
params: {
|
|
150
|
+
params: {
|
|
151
|
+
action?: string;
|
|
152
|
+
request?: unknown;
|
|
153
|
+
resultId?: string;
|
|
154
|
+
requestId?: string;
|
|
155
|
+
rejection?: unknown;
|
|
156
|
+
agentsCheckpoint?: unknown;
|
|
157
|
+
},
|
|
121
158
|
signal: AbortSignal | undefined,
|
|
122
159
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
123
160
|
ctx: ExtensionContext,
|
|
@@ -126,10 +163,11 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
126
163
|
const validRun =
|
|
127
164
|
action === "run" && runtime.validateRequest(params.request).ok;
|
|
128
165
|
const tuiRun = ctx.mode === "tui" && validRun;
|
|
166
|
+
const { action: _action, ...operation } = params;
|
|
129
167
|
const result = tuiRun
|
|
130
168
|
? await runtime.execute(
|
|
131
169
|
action,
|
|
132
|
-
|
|
170
|
+
operation,
|
|
133
171
|
ctx,
|
|
134
172
|
signal,
|
|
135
173
|
activity.observe(
|
|
@@ -137,24 +175,21 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
137
175
|
onUpdate as ((result: unknown) => void) | undefined,
|
|
138
176
|
),
|
|
139
177
|
)
|
|
140
|
-
: await runtime.execute(action,
|
|
178
|
+
: await runtime.execute(action, operation, ctx, signal);
|
|
141
179
|
const display = tuiRun
|
|
142
180
|
? activity.finalize(toolCallId, result)
|
|
143
181
|
: undefined;
|
|
182
|
+
const { payload, usage } = splitUsage(result);
|
|
144
183
|
const details = display
|
|
145
|
-
? {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
details,
|
|
151
|
-
usage: result.usage,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
184
|
+
? {
|
|
185
|
+
...(payload as Record<string, unknown>),
|
|
186
|
+
[ACTIVITY_DETAILS_KEY]: display,
|
|
187
|
+
}
|
|
188
|
+
: payload;
|
|
154
189
|
return {
|
|
155
|
-
content: [{ type: "text", text:
|
|
190
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
156
191
|
details,
|
|
157
|
-
|
|
192
|
+
...(usage === undefined ? {} : { usage }),
|
|
158
193
|
};
|
|
159
194
|
},
|
|
160
195
|
renderCall(args: unknown, theme: Theme, _context: unknown) {
|
|
@@ -177,23 +212,59 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
177
212
|
return { action: "continue" };
|
|
178
213
|
});
|
|
179
214
|
|
|
180
|
-
pi.on("before_agent_start", (event) => {
|
|
215
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
181
216
|
const prompt = pendingPrompt;
|
|
182
217
|
pendingPrompt = undefined;
|
|
218
|
+
if (
|
|
219
|
+
prompt &&
|
|
220
|
+
isVerifiedStageInvocation(pi, runtime.activation, prompt, event.prompt)
|
|
221
|
+
) {
|
|
222
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
223
|
+
if (typeof sessionId === "string") {
|
|
224
|
+
parentPayloadBridge.beginSession(sessionId);
|
|
225
|
+
} else {
|
|
226
|
+
parentPayloadBridge.clear();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (ctx.model) {
|
|
230
|
+
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
231
|
+
}
|
|
183
232
|
if (prompt)
|
|
184
233
|
activateDispatcher(pi, runtime.activation, prompt, event.prompt);
|
|
185
234
|
});
|
|
186
235
|
|
|
187
|
-
pi.on("session_start", (_event, ctx) => {
|
|
236
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
188
237
|
pendingPrompt = undefined;
|
|
189
238
|
activity.detach();
|
|
190
|
-
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
191
|
-
// Restore the default: dispatch is inactive unless a verified stage
|
|
192
|
-
// reactivates it through the activation helpers.
|
|
193
239
|
const active = pi.getActiveTools();
|
|
194
240
|
if (active.includes(DISPATCH_TOOL)) {
|
|
195
241
|
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
196
242
|
}
|
|
243
|
+
await runtime.drain();
|
|
244
|
+
activity.clear();
|
|
245
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
246
|
+
if (typeof sessionId === "string") {
|
|
247
|
+
parentPayloadBridge.beginSession(sessionId);
|
|
248
|
+
if (ctx.model) {
|
|
249
|
+
parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
parentPayloadBridge.clear();
|
|
253
|
+
}
|
|
254
|
+
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
pi.on("model_select", (event, ctx) => {
|
|
258
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
259
|
+
if (typeof sessionId !== "string") {
|
|
260
|
+
parentPayloadBridge.clear();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
parentPayloadBridge.beginSession(sessionId);
|
|
264
|
+
const model = event.model ?? ctx.model;
|
|
265
|
+
if (model) {
|
|
266
|
+
parentPayloadBridge.install(model, ctx.modelRegistry);
|
|
267
|
+
}
|
|
197
268
|
});
|
|
198
269
|
|
|
199
270
|
pi.on("session_shutdown", async () => {
|