@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.
@@ -1,10 +1,17 @@
1
1
  // File-level snapshots: content hashes for observed files, deterministic
2
- // directory-entry manifests for observed directories, and explicit absent
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 { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
7
- import { resolve } from "node:path";
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
- /** Deterministic directory-entry manifest: hash of sorted name/type entries. */
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
- const entries = readdirSync(abs, { withFileTypes: true })
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 runtime = new Runtime();
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: { action?: string; request?: unknown; resultId?: string },
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
- params,
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, params, ctx, signal);
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
- ? { ...result, [ACTIVITY_DETAILS_KEY]: display }
146
- : result;
147
- if (result.ok) {
148
- return {
149
- content: [{ type: "text", text: JSON.stringify(result) }],
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: result.error }],
190
+ content: [{ type: "text", text: JSON.stringify(payload) }],
156
191
  details,
157
- isError: true,
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 () => {