@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/scheduler.ts
CHANGED
|
@@ -34,6 +34,16 @@ export interface SchedulerOptions<T = unknown> {
|
|
|
34
34
|
): Promise<T>;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface ConflictDeclaration {
|
|
38
|
+
taskId: string;
|
|
39
|
+
requestId?: string;
|
|
40
|
+
read: string[];
|
|
41
|
+
write: string[];
|
|
42
|
+
conflicts: string[];
|
|
43
|
+
resources: string[];
|
|
44
|
+
verificationLocks: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
type EntryState = "queued" | "running" | TerminalStatus;
|
|
38
48
|
|
|
39
49
|
interface Entry<T> {
|
|
@@ -53,34 +63,63 @@ function intersects(left: string[], right: string[]): boolean {
|
|
|
53
63
|
return left.some((path) => right.includes(path));
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
function pathsOverlap(left: string, right: string): boolean {
|
|
67
|
+
return (
|
|
68
|
+
left === "." ||
|
|
69
|
+
right === "." ||
|
|
70
|
+
left === right ||
|
|
71
|
+
left.startsWith(`${right}/`) ||
|
|
72
|
+
right.startsWith(`${left}/`)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pathSetsOverlap(left: string[], right: string[]): boolean {
|
|
77
|
+
return left.some((a) => right.some((b) => pathsOverlap(a, b)));
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
function conflictEdgeMatches(
|
|
57
81
|
conflicts: string[],
|
|
58
|
-
other:
|
|
82
|
+
other: ConflictDeclaration,
|
|
59
83
|
): boolean {
|
|
60
84
|
return conflicts.some(
|
|
61
85
|
(edge) =>
|
|
62
|
-
edge === other.
|
|
63
|
-
other.
|
|
64
|
-
other.
|
|
86
|
+
edge === other.taskId ||
|
|
87
|
+
edge === other.requestId ||
|
|
88
|
+
other.read.some((path) => pathsOverlap(edge, path)) ||
|
|
89
|
+
other.write.some((path) => pathsOverlap(edge, path)),
|
|
65
90
|
);
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
|
|
93
|
+
function requestConflictOf(request: RequestEnvelope): ConflictDeclaration {
|
|
94
|
+
return {
|
|
95
|
+
taskId: request.taskId ?? request.id,
|
|
96
|
+
requestId: request.id,
|
|
97
|
+
read: request.declared.read,
|
|
98
|
+
write: request.declared.write,
|
|
99
|
+
conflicts: request.declared.conflicts,
|
|
100
|
+
resources: request.declared.resources,
|
|
101
|
+
verificationLocks: request.declared.verificationLock
|
|
102
|
+
? [request.declared.verificationLock]
|
|
103
|
+
: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** True when two pure declarations cannot be active at the same time. */
|
|
69
108
|
export function declarationsConflict(
|
|
70
|
-
left:
|
|
71
|
-
right:
|
|
109
|
+
left: ConflictDeclaration,
|
|
110
|
+
right: ConflictDeclaration,
|
|
72
111
|
): boolean {
|
|
73
|
-
const a = left
|
|
74
|
-
const b = right
|
|
75
|
-
if (
|
|
76
|
-
if (
|
|
112
|
+
const a = left;
|
|
113
|
+
const b = right;
|
|
114
|
+
if (a.taskId === b.taskId) return true;
|
|
115
|
+
if (pathSetsOverlap(a.write, b.write)) return true;
|
|
116
|
+
if (pathSetsOverlap(a.write, b.read) || pathSetsOverlap(a.read, b.write)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
77
119
|
if (conflictEdgeMatches(a.conflicts, right)) return true;
|
|
78
120
|
if (conflictEdgeMatches(b.conflicts, left)) return true;
|
|
79
121
|
if (intersects(a.resources, b.resources)) return true;
|
|
80
|
-
return (
|
|
81
|
-
a.verificationLock !== undefined &&
|
|
82
|
-
a.verificationLock === b.verificationLock
|
|
83
|
-
);
|
|
122
|
+
return intersects(a.verificationLocks, b.verificationLocks);
|
|
84
123
|
}
|
|
85
124
|
|
|
86
125
|
function errorMessage(error: unknown): string {
|
|
@@ -234,7 +273,12 @@ export class Scheduler<T> {
|
|
|
234
273
|
|
|
235
274
|
private hasActiveConflict(entry: Entry<T>): boolean {
|
|
236
275
|
for (const activeEntry of this.active) {
|
|
237
|
-
if (
|
|
276
|
+
if (
|
|
277
|
+
declarationsConflict(
|
|
278
|
+
requestConflictOf(entry.request),
|
|
279
|
+
requestConflictOf(activeEntry.request),
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
238
282
|
return true;
|
|
239
283
|
}
|
|
240
284
|
}
|
package/src/scoped-tools.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface ScopedToolDef {
|
|
|
31
31
|
|
|
32
32
|
interface Options {
|
|
33
33
|
roots: string[];
|
|
34
|
+
allowedPaths?: string[];
|
|
34
35
|
observer?: (obs: Observation) => void;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -38,7 +39,11 @@ type PathResult =
|
|
|
38
39
|
| { ok: true; abs: string; rel: string }
|
|
39
40
|
| { ok: false; error: string };
|
|
40
41
|
|
|
41
|
-
function resolveScoped(
|
|
42
|
+
function resolveScoped(
|
|
43
|
+
roots: string[],
|
|
44
|
+
input: unknown,
|
|
45
|
+
allowedPaths?: string[],
|
|
46
|
+
): PathResult {
|
|
42
47
|
if (typeof input !== "string" || input.length === 0) {
|
|
43
48
|
return { ok: false, error: "missing path" };
|
|
44
49
|
}
|
|
@@ -54,11 +59,21 @@ function resolveScoped(roots: string[], input: unknown): PathResult {
|
|
|
54
59
|
if (input.startsWith("./") || input.includes("//") || input.endsWith("/.")) {
|
|
55
60
|
return { ok: false, error: "noncanonical path rejected" };
|
|
56
61
|
}
|
|
62
|
+
let insideRoot = false;
|
|
57
63
|
for (const root of roots) {
|
|
58
64
|
const abs = resolve(root, input);
|
|
59
65
|
const inside =
|
|
60
66
|
abs === root || abs.startsWith(root.endsWith("/") ? root : `${root}/`);
|
|
61
67
|
if (!inside) continue;
|
|
68
|
+
insideRoot = true;
|
|
69
|
+
if (
|
|
70
|
+
allowedPaths &&
|
|
71
|
+
!allowedPaths.some(
|
|
72
|
+
(allowed) => abs === allowed || abs.startsWith(`${allowed}/`),
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
62
77
|
// Hidden directories are rejected unless the root itself is the hidden scope.
|
|
63
78
|
const hiddenRoot =
|
|
64
79
|
basename(realpathSync(root)) === ".git" ||
|
|
@@ -89,7 +104,12 @@ function resolveScoped(roots: string[], input: unknown): PathResult {
|
|
|
89
104
|
}
|
|
90
105
|
return { ok: true, abs: abs, rel: relative(root, abs) || "." };
|
|
91
106
|
}
|
|
92
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: insideRoot
|
|
110
|
+
? "path is outside the declared read/write scope"
|
|
111
|
+
: "path is outside every approved root",
|
|
112
|
+
};
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
function existsSync(p: string): boolean {
|
|
@@ -113,6 +133,7 @@ function utf8Text(abs: string): string | null {
|
|
|
113
133
|
|
|
114
134
|
export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
115
135
|
const roots = opts.roots.map((r) => resolve(r));
|
|
136
|
+
const allowedPaths = opts.allowedPaths?.map((p) => resolve(p));
|
|
116
137
|
const observe = opts.observer ?? (() => {});
|
|
117
138
|
|
|
118
139
|
const readTool: ScopedToolDef = {
|
|
@@ -120,7 +141,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
120
141
|
description:
|
|
121
142
|
"Read a regular UTF-8 text file within the approved scope (max 2,000 lines / 50 KiB).",
|
|
122
143
|
async execute(params) {
|
|
123
|
-
const resolved = resolveScoped(roots, params.path);
|
|
144
|
+
const resolved = resolveScoped(roots, params.path, allowedPaths);
|
|
124
145
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
125
146
|
let st: Stats | undefined;
|
|
126
147
|
try {
|
|
@@ -167,7 +188,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
167
188
|
error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
|
|
168
189
|
};
|
|
169
190
|
}
|
|
170
|
-
const resolved = resolveScoped(roots, params.path ?? ".");
|
|
191
|
+
const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
|
|
171
192
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
172
193
|
let regex: RegExp;
|
|
173
194
|
try {
|
|
@@ -207,7 +228,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
207
228
|
description:
|
|
208
229
|
"List a directory within the approved scope (max 20,000 entries, stable code-unit order).",
|
|
209
230
|
async execute(params) {
|
|
210
|
-
const resolved = resolveScoped(roots, params.path ?? ".");
|
|
231
|
+
const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
|
|
211
232
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
212
233
|
let st: Stats | undefined;
|
|
213
234
|
try {
|
|
@@ -236,7 +257,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
236
257
|
description:
|
|
237
258
|
"Recursively list paths within the approved scope (max 20,000 entries, stable code-unit order).",
|
|
238
259
|
async execute(params) {
|
|
239
|
-
const resolved = resolveScoped(roots, params.path ?? ".");
|
|
260
|
+
const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
|
|
240
261
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
241
262
|
let st: Stats | undefined;
|
|
242
263
|
try {
|
package/src/subagent-activity.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
visibleWidth,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
12
|
import type {
|
|
13
|
-
DispatchResult,
|
|
14
13
|
RuntimeActivityEvent,
|
|
15
14
|
RuntimeActivityObserver,
|
|
16
15
|
RuntimeActivityState,
|
|
@@ -39,12 +38,12 @@ export interface EvidenceActivitySummary {
|
|
|
39
38
|
export interface DiffActivitySummary {
|
|
40
39
|
kind: "diff";
|
|
41
40
|
summary: string;
|
|
42
|
-
nextStep: string;
|
|
43
41
|
riskCount: number;
|
|
44
42
|
retained: boolean;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
export type ActivitySummary = EvidenceActivitySummary | DiffActivitySummary;
|
|
46
|
+
export type ActivityTone = "success" | "warning" | "muted" | "error";
|
|
48
47
|
|
|
49
48
|
export interface ActivityDisplay {
|
|
50
49
|
version: 1;
|
|
@@ -54,6 +53,7 @@ export interface ActivityDisplay {
|
|
|
54
53
|
phase: string;
|
|
55
54
|
objective: string;
|
|
56
55
|
state: TerminalActivityState;
|
|
56
|
+
tone: ActivityTone;
|
|
57
57
|
elapsedMs: number;
|
|
58
58
|
summary?: ActivitySummary;
|
|
59
59
|
reason?: string;
|
|
@@ -217,14 +217,7 @@ function renderActivityLines(
|
|
|
217
217
|
formatElapsed(elapsed),
|
|
218
218
|
].join(" · ");
|
|
219
219
|
const second = ` ${sanitizeDisplayText(snapshot.objective, 240)}`;
|
|
220
|
-
const color =
|
|
221
|
-
snapshot.state === "completed"
|
|
222
|
-
? "success"
|
|
223
|
-
: snapshot.state === "failed" || snapshot.state === "timed-out"
|
|
224
|
-
? "error"
|
|
225
|
-
: snapshot.state === "cancelled"
|
|
226
|
-
? "warning"
|
|
227
|
-
: "accent";
|
|
220
|
+
const color = "tone" in snapshot ? snapshot.tone : "accent";
|
|
228
221
|
return [
|
|
229
222
|
line(styled(theme, color, first), width),
|
|
230
223
|
line(styled(theme, "muted", second), width),
|
|
@@ -311,7 +304,7 @@ export class ActivityInlineComponent implements Component {
|
|
|
311
304
|
lines[1] = line(
|
|
312
305
|
styled(
|
|
313
306
|
this.theme,
|
|
314
|
-
|
|
307
|
+
this.display.tone,
|
|
315
308
|
` reason: ${sanitizeFailureReason(this.display.reason)}`,
|
|
316
309
|
),
|
|
317
310
|
width,
|
|
@@ -323,7 +316,7 @@ export class ActivityInlineComponent implements Component {
|
|
|
323
316
|
const detail =
|
|
324
317
|
summary.kind === "evidence"
|
|
325
318
|
? ` evidence: ${summary.conclusions} conclusions · ${summary.citations} citations · ${summary.risks} risks · ${summary.blockingQuestions} blocking`
|
|
326
|
-
: ` diff: ${sanitizeDisplayText(summary.summary)} ·
|
|
319
|
+
: ` diff: ${sanitizeDisplayText(summary.summary)} · ${summary.riskCount} risks · retained: ${summary.retained ? "yes" : "no"}`;
|
|
327
320
|
return [lines[0] ?? "", line(styled(this.theme, "muted", detail), width)];
|
|
328
321
|
}
|
|
329
322
|
|
|
@@ -337,10 +330,14 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
337
330
|
}
|
|
338
331
|
|
|
339
332
|
export function summarizeDispatchResult(
|
|
340
|
-
result:
|
|
333
|
+
result: unknown,
|
|
341
334
|
): ActivitySummary | undefined {
|
|
342
|
-
|
|
343
|
-
|
|
335
|
+
const outer = asRecord(result);
|
|
336
|
+
if (!outer || outer.ok === false) return undefined;
|
|
337
|
+
const outcome = outer.ok === true ? asRecord(outer.result) : outer;
|
|
338
|
+
if (!outcome) return undefined;
|
|
339
|
+
const value =
|
|
340
|
+
outcome.kind === "candidate" ? asRecord(outcome.result) : outcome;
|
|
344
341
|
if (!value) return undefined;
|
|
345
342
|
if (
|
|
346
343
|
value.kind === "evidence" &&
|
|
@@ -360,20 +357,41 @@ export function summarizeDispatchResult(
|
|
|
360
357
|
if (
|
|
361
358
|
value.kind === "diff" &&
|
|
362
359
|
typeof value.summary === "string" &&
|
|
363
|
-
typeof value.nextStep === "string" &&
|
|
364
360
|
Array.isArray(value.risks)
|
|
365
361
|
) {
|
|
366
362
|
return {
|
|
367
363
|
kind: "diff",
|
|
368
364
|
summary: sanitizeDisplayText(value.summary),
|
|
369
|
-
nextStep: sanitizeDisplayText(value.nextStep),
|
|
370
365
|
riskCount: value.risks.length,
|
|
371
|
-
retained:
|
|
366
|
+
retained:
|
|
367
|
+
typeof outer.resultId === "string" ||
|
|
368
|
+
typeof outcome.resultId === "string",
|
|
372
369
|
};
|
|
373
370
|
}
|
|
374
371
|
return undefined;
|
|
375
372
|
}
|
|
376
373
|
|
|
374
|
+
function toneForResult(result: unknown): ActivityTone | undefined {
|
|
375
|
+
const outer = asRecord(result);
|
|
376
|
+
if (!outer || outer.ok === false) return undefined;
|
|
377
|
+
const value = outer.ok === true ? asRecord(outer.result) : outer;
|
|
378
|
+
switch (value?.kind) {
|
|
379
|
+
case "candidate":
|
|
380
|
+
case "applied":
|
|
381
|
+
case "completed":
|
|
382
|
+
return "success";
|
|
383
|
+
case "deferred":
|
|
384
|
+
case "retry":
|
|
385
|
+
case "checkpoint-required":
|
|
386
|
+
return "warning";
|
|
387
|
+
case "blocked":
|
|
388
|
+
case "cancelled":
|
|
389
|
+
return "muted";
|
|
390
|
+
default:
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
377
395
|
function terminalState(
|
|
378
396
|
event: RuntimeActivityEvent,
|
|
379
397
|
): event is RuntimeActivityEvent & {
|
|
@@ -396,6 +414,12 @@ function displayFromEvent(
|
|
|
396
414
|
phase: entry.phase,
|
|
397
415
|
objective: sanitizeDisplayText(entry.objective),
|
|
398
416
|
state: event.state as TerminalActivityState,
|
|
417
|
+
tone:
|
|
418
|
+
event.state === "failed" || event.state === "timed-out"
|
|
419
|
+
? "error"
|
|
420
|
+
: event.state === "cancelled"
|
|
421
|
+
? "muted"
|
|
422
|
+
: "success",
|
|
399
423
|
elapsedMs,
|
|
400
424
|
...(reason === undefined ? {} : { reason: sanitizeFailureReason(reason) }),
|
|
401
425
|
};
|
|
@@ -404,7 +428,7 @@ function displayFromEvent(
|
|
|
404
428
|
export function createActivityDisplay(
|
|
405
429
|
event: RuntimeActivityEvent,
|
|
406
430
|
elapsedMs: number,
|
|
407
|
-
result?:
|
|
431
|
+
result?: unknown,
|
|
408
432
|
): ActivityDisplay {
|
|
409
433
|
const entry: ActivityEntry = {
|
|
410
434
|
toolCallId: "",
|
|
@@ -419,7 +443,12 @@ export function createActivityDisplay(
|
|
|
419
443
|
};
|
|
420
444
|
const display = displayFromEvent(entry, event, elapsedMs);
|
|
421
445
|
const summary = result ? summarizeDispatchResult(result) : undefined;
|
|
422
|
-
|
|
446
|
+
const tone = result ? toneForResult(result) : undefined;
|
|
447
|
+
return {
|
|
448
|
+
...display,
|
|
449
|
+
...(tone === undefined ? {} : { tone }),
|
|
450
|
+
...(summary === undefined ? {} : { summary }),
|
|
451
|
+
};
|
|
423
452
|
}
|
|
424
453
|
|
|
425
454
|
function activityPartial(display: ActivityDisplay | ActivitySnapshot): unknown {
|
|
@@ -538,15 +567,17 @@ export class ActivityController {
|
|
|
538
567
|
this.syncUi();
|
|
539
568
|
}
|
|
540
569
|
|
|
541
|
-
finalize(
|
|
542
|
-
toolCallId: string,
|
|
543
|
-
result: DispatchResult,
|
|
544
|
-
): ActivityDisplay | undefined {
|
|
570
|
+
finalize(toolCallId: string, result: unknown): ActivityDisplay | undefined {
|
|
545
571
|
const display = this.terminals.get(toolCallId);
|
|
546
572
|
if (!display) return undefined;
|
|
547
573
|
this.terminals.delete(toolCallId);
|
|
548
574
|
const summary = summarizeDispatchResult(result);
|
|
549
|
-
|
|
575
|
+
const tone = toneForResult(result);
|
|
576
|
+
return {
|
|
577
|
+
...display,
|
|
578
|
+
...(tone === undefined ? {} : { tone }),
|
|
579
|
+
...(summary === undefined ? {} : { summary }),
|
|
580
|
+
};
|
|
550
581
|
}
|
|
551
582
|
|
|
552
583
|
getActiveEntries(): readonly ActivitySnapshot[] {
|
package/src/submit-tool.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import {
|
|
4
|
+
type CandidateFailure,
|
|
4
5
|
type DiffResult,
|
|
5
6
|
type EvidenceResult,
|
|
6
7
|
validateDiffResult,
|
|
@@ -36,7 +37,6 @@ const diffSchema = Type.Object({
|
|
|
36
37
|
diff: Type.String(),
|
|
37
38
|
expectedVerification: Type.String(),
|
|
38
39
|
risks: Type.Array(Type.String()),
|
|
39
|
-
nextStep: Type.String(),
|
|
40
40
|
contractCompliant: Type.Literal(true),
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -63,11 +63,13 @@ export interface SubmitClassification {
|
|
|
63
63
|
|
|
64
64
|
export function createSubmitTool(input: {
|
|
65
65
|
requestId: string;
|
|
66
|
+
taskId?: string;
|
|
66
67
|
role: string;
|
|
67
68
|
phase: string;
|
|
68
69
|
output: "evidence" | "diff";
|
|
69
70
|
}) {
|
|
70
71
|
let submitted: EvidenceResult | DiffResult | undefined;
|
|
72
|
+
let failure: CandidateFailure | undefined;
|
|
71
73
|
let attempts = 0;
|
|
72
74
|
let schema: "valid" | "invalid" = "invalid";
|
|
73
75
|
const identity: IdentityOutcome = {
|
|
@@ -93,12 +95,14 @@ export function createSubmitTool(input: {
|
|
|
93
95
|
identity.task = false;
|
|
94
96
|
identity.phase = false;
|
|
95
97
|
schema = "invalid";
|
|
98
|
+
failure = { kind: "artifact", code: "invalid-structural-result" };
|
|
96
99
|
throw new Error("submitted result is not an object");
|
|
97
100
|
}
|
|
98
101
|
if (value.id !== input.requestId) identity.request = false;
|
|
99
102
|
if (value.role !== input.role) identity.role = false;
|
|
100
103
|
if (input.output === "diff") {
|
|
101
|
-
if (value.taskId !== input.requestId)
|
|
104
|
+
if (value.taskId !== (input.taskId ?? input.requestId))
|
|
105
|
+
identity.task = false;
|
|
102
106
|
if (value.phase !== input.phase) identity.phase = false;
|
|
103
107
|
}
|
|
104
108
|
const validation =
|
|
@@ -109,11 +113,17 @@ export function createSubmitTool(input: {
|
|
|
109
113
|
const matches =
|
|
110
114
|
identity.request && identity.role && identity.task && identity.phase;
|
|
111
115
|
if (!validation.ok || !matches) {
|
|
116
|
+
failure =
|
|
117
|
+
validation.failure ??
|
|
118
|
+
({ kind: "artifact", code: "invalid-structural-result" } as const);
|
|
112
119
|
throw new Error(
|
|
113
120
|
validation.reason ?? "submitted result identity does not match",
|
|
114
121
|
);
|
|
115
122
|
}
|
|
116
|
-
if (submitted)
|
|
123
|
+
if (submitted) {
|
|
124
|
+
failure = { kind: "artifact", code: "invalid-structural-result" };
|
|
125
|
+
throw new Error("duplicate structural submission");
|
|
126
|
+
}
|
|
117
127
|
submitted = structuredClone(value) as unknown as
|
|
118
128
|
| EvidenceResult
|
|
119
129
|
| DiffResult;
|
|
@@ -131,5 +141,7 @@ export function createSubmitTool(input: {
|
|
|
131
141
|
getAttempts: () => attempts,
|
|
132
142
|
getSchema: () => schema,
|
|
133
143
|
getIdentity: () => ({ ...identity }),
|
|
144
|
+
getFailure: () =>
|
|
145
|
+
failure === undefined ? undefined : structuredClone(failure),
|
|
134
146
|
};
|
|
135
147
|
}
|