@abelxiaoxing/cadence 1.1.0 → 1.2.0

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,6 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { defineTool } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
+ import {
5
+ type CandidatePatchOperation,
6
+ compileCandidatePatch,
7
+ } from "./candidate-patch.ts";
4
8
  import {
5
9
  type CandidateFailure,
6
10
  type DiffResult,
@@ -11,7 +15,11 @@ import {
11
15
  validateDiffResult,
12
16
  validateEvidenceResult,
13
17
  } from "./contracts.ts";
14
- import type { BeginCandidateInput, TaskLedger } from "./task-ledger.ts";
18
+ import {
19
+ type BeginCandidateInput,
20
+ TASK_LEDGER_LIMITS,
21
+ type TaskLedger,
22
+ } from "./task-ledger.ts";
15
23
 
16
24
  const compactEvidenceSchema = Type.Object({
17
25
  id: Type.String(),
@@ -83,17 +91,49 @@ function candidateArtifactSchema(candidateId: string) {
83
91
  return Type.Union([
84
92
  Type.Object(
85
93
  {
86
- kind: Type.Literal("candidate-segment"),
87
- candidateId: Type.Literal(candidateId),
88
- sequence: Type.Integer({ minimum: 0 }),
89
- text: Type.String({ minLength: 1 }),
90
- },
91
- { additionalProperties: false },
92
- ),
93
- Type.Object(
94
- {
95
- kind: Type.Literal("candidate-seal"),
94
+ kind: Type.Literal("candidate-patch"),
96
95
  candidateId: Type.Literal(candidateId),
96
+ operations: Type.Array(
97
+ Type.Union([
98
+ Type.Object(
99
+ {
100
+ kind: Type.Literal("replace"),
101
+ path: Type.String(),
102
+ oldText: Type.String({ minLength: 1 }),
103
+ newText: Type.String(),
104
+ },
105
+ { additionalProperties: false },
106
+ ),
107
+ Type.Object(
108
+ {
109
+ kind: Type.Literal("rewrite"),
110
+ path: Type.String(),
111
+ content: Type.String(),
112
+ },
113
+ { additionalProperties: false },
114
+ ),
115
+ Type.Object(
116
+ {
117
+ kind: Type.Literal("create"),
118
+ path: Type.String(),
119
+ content: Type.String({ minLength: 1 }),
120
+ mode: Type.Union([
121
+ Type.Literal("regular"),
122
+ Type.Literal("executable"),
123
+ ]),
124
+ },
125
+ { additionalProperties: false },
126
+ ),
127
+ Type.Object(
128
+ {
129
+ kind: Type.Literal("delete"),
130
+ path: Type.String(),
131
+ },
132
+ { additionalProperties: false },
133
+ ),
134
+ ]),
135
+ { minItems: 1, maxItems: 128 },
136
+ ),
97
137
  },
98
138
  { additionalProperties: false },
99
139
  ),
@@ -188,6 +228,9 @@ export interface CandidateArtifactSubmission {
188
228
  "beginCandidate" | "appendCandidateSegment" | "sealCandidate"
189
229
  >;
190
230
  identity: BeginCandidateInput;
231
+ workspaceRoot: string;
232
+ writePaths: readonly string[];
233
+ deletePaths: readonly string[];
191
234
  }
192
235
 
193
236
  export function createCandidateArtifactTool(
@@ -199,10 +242,7 @@ export function createCandidateArtifactTool(
199
242
  let failure: CandidateFailure | undefined;
200
243
  let attempts = 0;
201
244
  let schema: SubmitSchemaState = "not-submitted";
202
- let segmentCount = 0;
203
- let totalBytes = 0;
204
- const candidateHash = createHash("sha256");
205
- const identity: IdentityOutcome = {
245
+ let identity: IdentityOutcome = {
206
246
  request: true,
207
247
  role: true,
208
248
  task: true,
@@ -212,7 +252,7 @@ export function createCandidateArtifactTool(
212
252
  name: "abel_submit_result",
213
253
  label: "Submit Abel Candidate Artifact",
214
254
  description:
215
- "Submit ordered raw-text candidate segments, atomically seal the complete candidate, or request bounded approved context. Encoding and hashes are computed by this trusted tool.",
255
+ "Submit one complete structured candidate patch or request bounded approved context. Diff generation, chunking, encoding, hashes, and atomic sealing are owned by this trusted tool.",
216
256
  executionMode: "sequential",
217
257
  parameters,
218
258
  async execute(_toolCallId, params) {
@@ -220,6 +260,22 @@ export function createCandidateArtifactTool(
220
260
  throw new Error("candidate terminal result already submitted");
221
261
  }
222
262
  attempts++;
263
+ if (attempts > 2) {
264
+ schema = "invalid";
265
+ failure = {
266
+ kind: "artifact",
267
+ code: "invalid-structural-result",
268
+ stage: "structural-submit",
269
+ };
270
+ throw new Error("structural submission correction limit exceeded");
271
+ }
272
+ identity = {
273
+ request: true,
274
+ role: true,
275
+ task: true,
276
+ phase: true,
277
+ };
278
+ failure = undefined;
223
279
  const value = params as unknown as Record<string, unknown>;
224
280
  if (value.candidateId !== input.identity.candidateId) {
225
281
  identity.request = false;
@@ -232,18 +288,15 @@ export function createCandidateArtifactTool(
232
288
  throw new Error("candidate identity mismatch");
233
289
  }
234
290
  schema = "valid";
235
- if (value.kind === "candidate-segment") {
236
- const bytes = Buffer.from(value.text as string, "utf8");
237
- const segmentHash = createHash("sha256").update(bytes).digest("hex");
238
- let accepted: ReturnType<
239
- CandidateArtifactSubmission["ledger"]["appendCandidateSegment"]
240
- >;
291
+ if (value.kind === "candidate-patch") {
292
+ let diff: string;
241
293
  try {
242
- accepted = input.ledger.appendCandidateSegment({
243
- ...input.identity,
244
- sequence: value.sequence as number,
245
- bytes,
246
- segmentHash,
294
+ diff = compileCandidatePatch({
295
+ root: input.workspaceRoot,
296
+ writePaths: input.writePaths,
297
+ deletePaths: input.deletePaths,
298
+ operations: value.operations as CandidatePatchOperation[],
299
+ maxBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
247
300
  });
248
301
  } catch (error) {
249
302
  schema = "invalid";
@@ -254,34 +307,59 @@ export function createCandidateArtifactTool(
254
307
  };
255
308
  throw error;
256
309
  }
257
- if (!accepted.ok) {
258
- failure = {
259
- kind: "result-limit",
260
- limitBytes: accepted.limitBytes,
261
- };
262
- } else {
263
- candidateHash.update(bytes);
264
- segmentCount = accepted.nextSequence;
265
- totalBytes = accepted.totalBytes;
310
+ const bytes = Buffer.from(diff, "utf8");
311
+ const candidateHash = createHash("sha256").update(bytes).digest("hex");
312
+ let sequence = 0;
313
+ for (
314
+ let offset = 0;
315
+ offset < bytes.length;
316
+ offset += TASK_LEDGER_LIMITS.maxSegmentBytes
317
+ ) {
318
+ const segment = bytes.subarray(
319
+ offset,
320
+ Math.min(offset + TASK_LEDGER_LIMITS.maxSegmentBytes, bytes.length),
321
+ );
322
+ let accepted: ReturnType<
323
+ CandidateArtifactSubmission["ledger"]["appendCandidateSegment"]
324
+ >;
325
+ try {
326
+ accepted = input.ledger.appendCandidateSegment({
327
+ ...input.identity,
328
+ sequence,
329
+ bytes: segment,
330
+ segmentHash: createHash("sha256").update(segment).digest("hex"),
331
+ });
332
+ } catch (error) {
333
+ schema = "invalid";
334
+ failure = {
335
+ kind: "artifact",
336
+ code: "invalid-diff",
337
+ stage: "candidate-diff",
338
+ };
339
+ throw error;
340
+ }
341
+ if (!accepted.ok) {
342
+ failure = {
343
+ kind: "result-limit",
344
+ limitBytes: accepted.limitBytes,
345
+ };
346
+ return {
347
+ content: [],
348
+ details: accepted,
349
+ terminate: true,
350
+ };
351
+ }
352
+ sequence = accepted.nextSequence;
266
353
  }
267
- return {
268
- content: [
269
- { type: "text" as const, text: "Candidate segment accepted." },
270
- ],
271
- details: accepted,
272
- ...(!accepted.ok ? { terminate: true } : {}),
273
- };
274
- }
275
- if (value.kind === "candidate-seal") {
276
354
  let sealed: ReturnType<
277
355
  CandidateArtifactSubmission["ledger"]["sealCandidate"]
278
356
  >;
279
357
  try {
280
358
  sealed = input.ledger.sealCandidate({
281
359
  ...input.identity,
282
- segmentCount,
283
- totalBytes,
284
- candidateHash: candidateHash.digest("hex"),
360
+ segmentCount: sequence,
361
+ totalBytes: bytes.length,
362
+ candidateHash,
285
363
  });
286
364
  } catch (error) {
287
365
  schema = "invalid";
@@ -318,6 +396,12 @@ export function createCandidateArtifactTool(
318
396
  refs.some((relative) => !isValidRelativePath(relative)) ||
319
397
  new Set(refs).size !== refs.length
320
398
  ) {
399
+ schema = "invalid";
400
+ failure = {
401
+ kind: "artifact",
402
+ code: "invalid-structural-result",
403
+ stage: "structural-submit",
404
+ };
321
405
  throw new Error("context request refs are invalid");
322
406
  }
323
407
  result = {
@@ -377,7 +461,7 @@ export function createSubmitTool(input: {
377
461
  let failure: CandidateFailure | undefined;
378
462
  let attempts = 0;
379
463
  let schema: SubmitSchemaState = "not-submitted";
380
- const identity: IdentityOutcome = {
464
+ let identity: IdentityOutcome = {
381
465
  request: true,
382
466
  role: true,
383
467
  task: true,
@@ -398,6 +482,22 @@ export function createSubmitTool(input: {
398
482
  : diffSchema,
399
483
  async execute(_toolCallId, params) {
400
484
  attempts++;
485
+ if (attempts > 2) {
486
+ schema = "invalid";
487
+ failure = {
488
+ kind: "artifact",
489
+ code: "invalid-structural-result",
490
+ stage: "structural-submit",
491
+ };
492
+ throw new Error("structural submission correction limit exceeded");
493
+ }
494
+ identity = {
495
+ request: true,
496
+ role: true,
497
+ task: true,
498
+ phase: true,
499
+ };
500
+ failure = undefined;
401
501
  const value = params as unknown as Record<string, unknown>;
402
502
  if (value === null || typeof value !== "object") {
403
503
  identity.request = false;
@@ -166,17 +166,39 @@ function localExecutable(
166
166
  }
167
167
  }
168
168
 
169
+ function pathApi(
170
+ platform: NodeJS.Platform,
171
+ ...values: readonly string[]
172
+ ): typeof path.posix {
173
+ if (platform !== "win32") return path.posix;
174
+ if (
175
+ process.platform === "win32" ||
176
+ values.some(
177
+ (value) => /^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\"),
178
+ )
179
+ ) {
180
+ return path.win32;
181
+ }
182
+ // Tests may model Windows executable semantics over a POSIX fixture tree.
183
+ return path.posix;
184
+ }
185
+
169
186
  function pathWithin(
170
187
  root: string,
171
188
  candidate: string,
172
189
  platform: NodeJS.Platform,
173
190
  ): boolean {
174
- const normalizedRoot = platform === "win32" ? root.toLowerCase() : root;
175
- const normalizedCandidate =
176
- platform === "win32" ? candidate.toLowerCase() : candidate;
191
+ const platformPath = pathApi(platform, root, candidate);
192
+ const normalizedRoot = platformPath.resolve(root);
193
+ const relative = platformPath.relative(
194
+ normalizedRoot,
195
+ platformPath.resolve(candidate),
196
+ );
177
197
  return (
178
- normalizedCandidate === normalizedRoot ||
179
- normalizedCandidate.startsWith(`${normalizedRoot}${path.sep}`)
198
+ relative === "" ||
199
+ (relative !== ".." &&
200
+ !relative.startsWith(`..${platformPath.sep}`) &&
201
+ !platformPath.isAbsolute(relative))
180
202
  );
181
203
  }
182
204
 
@@ -261,13 +283,24 @@ function within(root: string, candidate: string): boolean {
261
283
  return candidate === root || candidate.startsWith(`${root}${path.sep}`);
262
284
  }
263
285
 
264
- function commonDirectory(
286
+ export function commonDirectory(
265
287
  left: string,
266
288
  right: string,
267
289
  platform: NodeJS.Platform = process.platform,
268
290
  ): string {
269
- const leftParts = path.resolve(left).split(path.sep).filter(Boolean);
270
- const rightParts = path.resolve(right).split(path.sep).filter(Boolean);
291
+ const platformPath = pathApi(platform, left, right);
292
+ const leftResolved = platformPath.resolve(left);
293
+ const rightResolved = platformPath.resolve(right);
294
+ const leftRoot = platformPath.parse(leftResolved).root;
295
+ const rightRoot = platformPath.parse(rightResolved).root;
296
+ const comparable = (value: string) =>
297
+ platform === "win32" ? value.toLowerCase() : value;
298
+ if (comparable(leftRoot) !== comparable(rightRoot)) return leftRoot;
299
+
300
+ const parts = (value: string, root: string) =>
301
+ platformPath.relative(root, value).split(platformPath.sep).filter(Boolean);
302
+ const leftParts = parts(leftResolved, leftRoot);
303
+ const rightParts = parts(rightResolved, rightRoot);
271
304
  const common: string[] = [];
272
305
  for (
273
306
  let index = 0;
@@ -276,15 +309,10 @@ function commonDirectory(
276
309
  ) {
277
310
  const leftPart = leftParts[index] as string;
278
311
  const rightPart = rightParts[index] as string;
279
- if (
280
- (platform === "win32" ? leftPart.toLowerCase() : leftPart) !==
281
- (platform === "win32" ? rightPart.toLowerCase() : rightPart)
282
- ) {
283
- break;
284
- }
312
+ if (comparable(leftPart) !== comparable(rightPart)) break;
285
313
  common.push(leftPart);
286
314
  }
287
- return path.join(path.parse(path.resolve(left)).root, ...common);
315
+ return platformPath.join(leftRoot, ...common);
288
316
  }
289
317
 
290
318
  function unsafeMountSource(source: string): boolean {
@@ -113,7 +113,6 @@ class RouteAttemptStop extends Error {
113
113
  function isRole(value: unknown): value is WorkerRole {
114
114
  return [
115
115
  "design-explorer",
116
- "contract-reviewer",
117
116
  "implementation-worker",
118
117
  "diagnosis-worker",
119
118
  ].includes(String(value));