@abelxiaoxing/cadence 1.0.10 → 1.0.12
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 +49 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +14 -2
- package/prompts/abel-implement.md +10 -2
- package/skills/abel-workflow/SKILL.md +36 -0
- package/src/candidate-preflight.ts +237 -90
- package/src/child-session.ts +26 -3
- package/src/contracts.ts +482 -41
- package/src/index.ts +14 -4
- package/src/parent-payload-bridge.ts +31 -7
- package/src/patch.ts +13 -1
- package/src/result-store.ts +6 -9
- package/src/runtime.ts +64 -9
- package/src/scoped-tools.ts +278 -38
- package/src/subagent-activity.ts +80 -12
- package/src/verification-capability.ts +481 -0
package/src/scoped-tools.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type Stats,
|
|
9
9
|
statSync,
|
|
10
10
|
} from "node:fs";
|
|
11
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
12
12
|
|
|
13
13
|
export const TOOL_LIMITS = {
|
|
14
14
|
maxReadBytes: 50 * 1024,
|
|
@@ -19,6 +19,115 @@ export const TOOL_LIMITS = {
|
|
|
19
19
|
maxEntries: 20000,
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
|
+
function globToRegExp(pattern: string): RegExp | { error: string } {
|
|
23
|
+
if (pattern.length === 0) return { error: "missing pattern" };
|
|
24
|
+
if (pattern.length > TOOL_LIMITS.maxGrepPattern) {
|
|
25
|
+
return {
|
|
26
|
+
error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let source = "^";
|
|
30
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
31
|
+
const ch = pattern[i];
|
|
32
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
33
|
+
const next = pattern[i + 2];
|
|
34
|
+
if (next === "/") {
|
|
35
|
+
source += "(?:.*/)?";
|
|
36
|
+
i += 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
source += ".*";
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === "*") {
|
|
44
|
+
source += "[^/]*";
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === "?") {
|
|
48
|
+
source += "[^/]";
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === "[") {
|
|
52
|
+
let end = i + 1;
|
|
53
|
+
if (pattern[end] === "!" || pattern[end] === "^") end++;
|
|
54
|
+
while (end < pattern.length && pattern[end] !== "]") end++;
|
|
55
|
+
if (end === pattern.length) {
|
|
56
|
+
source += "\\[";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const raw = pattern.slice(i + 1, end);
|
|
60
|
+
const negated = raw.startsWith("!") || raw.startsWith("^");
|
|
61
|
+
const body = raw.slice(negated ? 1 : 0);
|
|
62
|
+
if (body.length === 0 || body.includes("/")) {
|
|
63
|
+
return { error: "invalid glob character class" };
|
|
64
|
+
}
|
|
65
|
+
const escaped = [...body]
|
|
66
|
+
.map((value, index) => {
|
|
67
|
+
if (value === "\\" || value === "[" || value === "]") {
|
|
68
|
+
return `\\${value}`;
|
|
69
|
+
}
|
|
70
|
+
return value === "^" && index === 0 ? "\\^" : value;
|
|
71
|
+
})
|
|
72
|
+
.join("");
|
|
73
|
+
source += `[${negated ? "^" : ""}${escaped}]`;
|
|
74
|
+
i = end;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if ("\\^$+()[]{}|.".includes(ch)) source += `\\${ch}`;
|
|
78
|
+
else source += ch;
|
|
79
|
+
}
|
|
80
|
+
source += "$";
|
|
81
|
+
try {
|
|
82
|
+
return new RegExp(source, "u");
|
|
83
|
+
} catch {
|
|
84
|
+
return { error: "invalid glob pattern" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function boundedInteger(value: unknown): number | undefined {
|
|
89
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
90
|
+
? value
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isWithinPath(path: string, parent: string): boolean {
|
|
95
|
+
return path === parent || path.startsWith(`${parent}${sep}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isAllowedContent(path: string, allowedPaths?: string[]): boolean {
|
|
99
|
+
return (
|
|
100
|
+
allowedPaths === undefined ||
|
|
101
|
+
allowedPaths.some((allowed) => isWithinPath(path, allowed))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRelatedToAllowedPath(
|
|
106
|
+
path: string,
|
|
107
|
+
allowedPaths?: string[],
|
|
108
|
+
): boolean {
|
|
109
|
+
return (
|
|
110
|
+
allowedPaths === undefined ||
|
|
111
|
+
allowedPaths.some(
|
|
112
|
+
(allowed) => isWithinPath(path, allowed) || isWithinPath(allowed, path),
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function commonAncestor(paths: string[]): string {
|
|
118
|
+
let candidate = paths[0];
|
|
119
|
+
while (!paths.every((path) => isWithinPath(path, candidate))) {
|
|
120
|
+
const parent = dirname(candidate);
|
|
121
|
+
if (parent === candidate) return candidate;
|
|
122
|
+
candidate = parent;
|
|
123
|
+
}
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function relativePath(base: string, path: string): string {
|
|
128
|
+
return (relative(base, path) || ".").split(sep).join("/");
|
|
129
|
+
}
|
|
130
|
+
|
|
22
131
|
export type Observation =
|
|
23
132
|
| { kind: "file"; path: string }
|
|
24
133
|
| { kind: "dir"; path: string };
|
|
@@ -38,11 +147,13 @@ interface Options {
|
|
|
38
147
|
type PathResult =
|
|
39
148
|
| { ok: true; abs: string; rel: string }
|
|
40
149
|
| { ok: false; error: string };
|
|
150
|
+
type ResolvedPath = Extract<PathResult, { ok: true }>;
|
|
41
151
|
|
|
42
152
|
function resolveScoped(
|
|
43
153
|
roots: string[],
|
|
44
154
|
input: unknown,
|
|
45
155
|
allowedPaths?: string[],
|
|
156
|
+
allowScopedAncestor = false,
|
|
46
157
|
): PathResult {
|
|
47
158
|
if (typeof input !== "string" || input.length === 0) {
|
|
48
159
|
return { ok: false, error: "missing path" };
|
|
@@ -69,7 +180,9 @@ function resolveScoped(
|
|
|
69
180
|
if (
|
|
70
181
|
allowedPaths &&
|
|
71
182
|
!allowedPaths.some(
|
|
72
|
-
(allowed) =>
|
|
183
|
+
(allowed) =>
|
|
184
|
+
isWithinPath(abs, allowed) ||
|
|
185
|
+
(allowScopedAncestor && isWithinPath(allowed, abs)),
|
|
73
186
|
)
|
|
74
187
|
) {
|
|
75
188
|
continue;
|
|
@@ -135,6 +248,37 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
135
248
|
const roots = opts.roots.map((r) => resolve(r));
|
|
136
249
|
const allowedPaths = opts.allowedPaths?.map((p) => resolve(p));
|
|
137
250
|
const observe = opts.observer ?? (() => {});
|
|
251
|
+
const virtualRoot = commonAncestor(roots);
|
|
252
|
+
|
|
253
|
+
function resolveScanTargets(
|
|
254
|
+
input: unknown,
|
|
255
|
+
):
|
|
256
|
+
| { ok: true; targets: ResolvedPath[]; virtual: boolean }
|
|
257
|
+
| { ok: false; error: string } {
|
|
258
|
+
if (input !== undefined) {
|
|
259
|
+
const resolved = resolveScoped(roots, input, allowedPaths, true);
|
|
260
|
+
return resolved.ok
|
|
261
|
+
? { ok: true, targets: [resolved], virtual: false }
|
|
262
|
+
: resolved;
|
|
263
|
+
}
|
|
264
|
+
const targets: ResolvedPath[] = [];
|
|
265
|
+
for (const root of roots) {
|
|
266
|
+
const resolved = resolveScoped([root], ".", allowedPaths, true);
|
|
267
|
+
if (resolved.ok) targets.push(resolved);
|
|
268
|
+
}
|
|
269
|
+
return targets.length > 0
|
|
270
|
+
? { ok: true, targets, virtual: roots.length > 1 }
|
|
271
|
+
: {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: "path is outside the declared read/write scope",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function rootRelativePath(path: string, virtual: boolean): string {
|
|
278
|
+
if (virtual) return relativePath(virtualRoot, path);
|
|
279
|
+
const root = roots.find((candidate) => isWithinPath(path, candidate));
|
|
280
|
+
return relativePath(root ?? roots[0], path);
|
|
281
|
+
}
|
|
138
282
|
|
|
139
283
|
const readTool: ScopedToolDef = {
|
|
140
284
|
name: "read",
|
|
@@ -153,21 +297,39 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
153
297
|
const text = utf8Text(resolved.abs);
|
|
154
298
|
if (text === null) return { ok: false, error: "not valid UTF-8 text" };
|
|
155
299
|
observe({ kind: "file", path: resolved.rel });
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
const allLines = text.split("\n");
|
|
301
|
+
const offset = boundedInteger(params.offset);
|
|
302
|
+
const limit = boundedInteger(params.limit);
|
|
303
|
+
if (offset !== undefined && offset > allLines.length) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
error: `offset ${offset} exceeds file length (${allLines.length} lines)`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const start = offset === undefined ? 0 : Math.max(0, offset - 1);
|
|
310
|
+
const windowed =
|
|
311
|
+
offset === undefined && limit === undefined
|
|
312
|
+
? allLines
|
|
313
|
+
: allLines.slice(
|
|
314
|
+
start,
|
|
315
|
+
limit === undefined ? undefined : start + limit,
|
|
316
|
+
);
|
|
317
|
+
let content = windowed.join("\n");
|
|
318
|
+
let truncated =
|
|
319
|
+
Buffer.byteLength(content, "utf8") > TOOL_LIMITS.maxReadBytes ||
|
|
320
|
+
windowed.length > TOOL_LIMITS.maxReadLines;
|
|
321
|
+
if (Buffer.byteLength(content, "utf8") > TOOL_LIMITS.maxReadBytes) {
|
|
322
|
+
content = Buffer.from(content, "utf8")
|
|
163
323
|
.subarray(0, TOOL_LIMITS.maxReadBytes)
|
|
164
324
|
.toString("utf8");
|
|
325
|
+
truncated = true;
|
|
165
326
|
}
|
|
166
327
|
if (content.split("\n").length > TOOL_LIMITS.maxReadLines) {
|
|
167
328
|
content = content
|
|
168
329
|
.split("\n")
|
|
169
330
|
.slice(0, TOOL_LIMITS.maxReadLines)
|
|
170
331
|
.join("\n");
|
|
332
|
+
truncated = true;
|
|
171
333
|
}
|
|
172
334
|
return { ok: true, content, truncated };
|
|
173
335
|
},
|
|
@@ -188,8 +350,8 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
188
350
|
error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
|
|
189
351
|
};
|
|
190
352
|
}
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
353
|
+
const scan = resolveScanTargets(params.path);
|
|
354
|
+
if (!scan.ok) return { ok: false, error: scan.error };
|
|
193
355
|
let regex: RegExp;
|
|
194
356
|
try {
|
|
195
357
|
regex = new RegExp(pattern, "u");
|
|
@@ -199,20 +361,29 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
199
361
|
const start = performance.now();
|
|
200
362
|
const matches: { path: string; line: number; text: string }[] = [];
|
|
201
363
|
let scanned = 0;
|
|
202
|
-
const files =
|
|
364
|
+
const files: string[] = [];
|
|
365
|
+
for (const target of scan.targets) {
|
|
366
|
+
collectFiles(target.abs, files, TOOL_LIMITS.maxEntries, allowedPaths);
|
|
367
|
+
}
|
|
368
|
+
files.sort((left, right) => {
|
|
369
|
+
const leftPath = rootRelativePath(left, scan.virtual);
|
|
370
|
+
const rightPath = rootRelativePath(right, scan.virtual);
|
|
371
|
+
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
|
|
372
|
+
});
|
|
203
373
|
for (const file of files) {
|
|
204
374
|
if (scanned >= TOOL_LIMITS.maxGrepFiles) break;
|
|
205
375
|
if (performance.now() - start > TOOL_LIMITS.maxGrepMillis) break;
|
|
206
376
|
scanned++;
|
|
207
377
|
const text = utf8Text(file);
|
|
208
378
|
if (text === null) continue;
|
|
209
|
-
|
|
379
|
+
const path = rootRelativePath(file, scan.virtual);
|
|
380
|
+
observe({ kind: "file", path });
|
|
210
381
|
const lines = text.split("\n");
|
|
211
382
|
for (let i = 0; i < lines.length; i++) {
|
|
212
383
|
regex.lastIndex = 0;
|
|
213
384
|
if (regex.test(lines[i])) {
|
|
214
385
|
matches.push({
|
|
215
|
-
path
|
|
386
|
+
path,
|
|
216
387
|
line: i + 1,
|
|
217
388
|
text: lines[i].slice(0, 500),
|
|
218
389
|
});
|
|
@@ -228,18 +399,41 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
228
399
|
description:
|
|
229
400
|
"List a directory within the approved scope (max 20,000 entries, stable code-unit order).",
|
|
230
401
|
async execute(params) {
|
|
231
|
-
const
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
402
|
+
const scan = resolveScanTargets(params.path);
|
|
403
|
+
if (!scan.ok) return { ok: false, error: scan.error };
|
|
404
|
+
for (const target of scan.targets) {
|
|
405
|
+
let st: Stats | undefined;
|
|
406
|
+
try {
|
|
407
|
+
st = statSync(target.abs);
|
|
408
|
+
} catch {
|
|
409
|
+
return { ok: false, error: "directory does not exist" };
|
|
410
|
+
}
|
|
411
|
+
if (!st.isDirectory()) return { ok: false, error: "not a directory" };
|
|
412
|
+
observe({
|
|
413
|
+
kind: "dir",
|
|
414
|
+
path: rootRelativePath(target.abs, scan.virtual),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (scan.virtual) {
|
|
418
|
+
const names = new Set(
|
|
419
|
+
scan.targets.map(
|
|
420
|
+
(target) => relativePath(virtualRoot, target.abs).split("/")[0],
|
|
421
|
+
),
|
|
422
|
+
);
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
entries: [...names]
|
|
426
|
+
.sort()
|
|
427
|
+
.slice(0, TOOL_LIMITS.maxEntries)
|
|
428
|
+
.map((name) => ({ name, type: "dir" })),
|
|
429
|
+
};
|
|
238
430
|
}
|
|
239
|
-
|
|
240
|
-
observe({ kind: "dir", path: resolved.rel });
|
|
431
|
+
const resolved = scan.targets[0];
|
|
241
432
|
const entries = readdirSync(resolved.abs, { withFileTypes: true })
|
|
242
433
|
.filter((e) => !isHidden(e.name, roots))
|
|
434
|
+
.filter((e) =>
|
|
435
|
+
isRelatedToAllowedPath(join(resolved.abs, e.name), allowedPaths),
|
|
436
|
+
)
|
|
243
437
|
.slice(0, TOOL_LIMITS.maxEntries)
|
|
244
438
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
245
439
|
return {
|
|
@@ -255,33 +449,79 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
255
449
|
const findTool: ScopedToolDef = {
|
|
256
450
|
name: "find",
|
|
257
451
|
description:
|
|
258
|
-
"
|
|
452
|
+
"Search for files by glob pattern within the approved scope; paths are relative to the search directory (max 20,000 entries).",
|
|
259
453
|
async execute(params) {
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
454
|
+
const pattern = params.pattern;
|
|
455
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
456
|
+
return { ok: false, error: "missing pattern" };
|
|
457
|
+
}
|
|
458
|
+
const limit =
|
|
459
|
+
params.limit === undefined
|
|
460
|
+
? TOOL_LIMITS.maxEntries
|
|
461
|
+
: boundedInteger(params.limit);
|
|
462
|
+
if (limit === undefined) {
|
|
463
|
+
return { ok: false, error: "limit must be a positive integer" };
|
|
267
464
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
465
|
+
const scan = resolveScanTargets(params.path);
|
|
466
|
+
if (!scan.ok) return { ok: false, error: scan.error };
|
|
467
|
+
for (const target of scan.targets) {
|
|
468
|
+
let st: Stats | undefined;
|
|
469
|
+
try {
|
|
470
|
+
st = statSync(target.abs);
|
|
471
|
+
} catch {
|
|
472
|
+
return { ok: false, error: "directory does not exist" };
|
|
473
|
+
}
|
|
474
|
+
if (!st.isDirectory()) return { ok: false, error: "not a directory" };
|
|
475
|
+
observe({
|
|
476
|
+
kind: "dir",
|
|
477
|
+
path: rootRelativePath(target.abs, scan.virtual),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const matcher = globToRegExp(pattern);
|
|
481
|
+
if ("error" in matcher) {
|
|
482
|
+
return { ok: false, error: matcher.error };
|
|
483
|
+
}
|
|
484
|
+
const files: string[] = [];
|
|
485
|
+
for (const target of scan.targets) {
|
|
486
|
+
collectFiles(target.abs, files, TOOL_LIMITS.maxEntries, allowedPaths);
|
|
487
|
+
}
|
|
488
|
+
const findBase = scan.virtual ? virtualRoot : scan.targets[0].abs;
|
|
489
|
+
const out = files.map((abs) => relativePath(findBase, abs));
|
|
490
|
+
const entries = out.filter(
|
|
491
|
+
(rel) => matcher.test(rel) || matcher.test(basename(rel)),
|
|
492
|
+
);
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
entries: entries
|
|
496
|
+
.sort()
|
|
497
|
+
.slice(0, Math.min(limit, TOOL_LIMITS.maxEntries)),
|
|
498
|
+
};
|
|
272
499
|
},
|
|
273
500
|
};
|
|
274
501
|
|
|
275
|
-
function collectFiles(
|
|
502
|
+
function collectFiles(
|
|
503
|
+
dir: string,
|
|
504
|
+
out: string[],
|
|
505
|
+
limit: number,
|
|
506
|
+
scopedPaths?: string[],
|
|
507
|
+
): string[] {
|
|
276
508
|
if (out.length >= limit) return out;
|
|
509
|
+
const target = statSync(dir);
|
|
510
|
+
if (target.isFile()) {
|
|
511
|
+
if (isAllowedContent(dir, scopedPaths)) out.push(dir);
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
if (!target.isDirectory()) return out;
|
|
277
515
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
278
516
|
for (const e of entries) {
|
|
279
517
|
if (out.length >= limit) break;
|
|
280
518
|
if (isHidden(e.name, roots)) continue;
|
|
281
519
|
const full = join(dir, e.name);
|
|
282
520
|
if (e.isDirectory()) {
|
|
283
|
-
|
|
284
|
-
|
|
521
|
+
if (isRelatedToAllowedPath(full, scopedPaths)) {
|
|
522
|
+
collectFiles(full, out, limit, scopedPaths);
|
|
523
|
+
}
|
|
524
|
+
} else if (e.isFile() && isAllowedContent(full, scopedPaths)) {
|
|
285
525
|
out.push(full);
|
|
286
526
|
}
|
|
287
527
|
}
|
package/src/subagent-activity.ts
CHANGED
|
@@ -87,6 +87,11 @@ type ActivityTheme = {
|
|
|
87
87
|
fg?: (color: ActivityColor, text: string) => string;
|
|
88
88
|
};
|
|
89
89
|
type ActivityUi = Pick<ExtensionUIContext, "setWidget" | "setStatus">;
|
|
90
|
+
type ActivityRenderContext = {
|
|
91
|
+
args?: unknown;
|
|
92
|
+
executionStarted?: boolean;
|
|
93
|
+
isError?: boolean;
|
|
94
|
+
};
|
|
90
95
|
type TimerHandle = ReturnType<typeof setInterval>;
|
|
91
96
|
|
|
92
97
|
export interface ActivityControllerOptions {
|
|
@@ -734,12 +739,41 @@ export function renderActivityResult(
|
|
|
734
739
|
result: unknown,
|
|
735
740
|
options: ToolRenderResultOptions,
|
|
736
741
|
theme?: ActivityTheme,
|
|
742
|
+
context?: ActivityRenderContext,
|
|
737
743
|
): Component {
|
|
738
744
|
const details = asRecord(asRecord(result)?.details);
|
|
739
745
|
const activity = details?.[ACTIVITY_DETAILS_KEY];
|
|
740
|
-
|
|
746
|
+
const failed = context?.isError === true;
|
|
747
|
+
const args = asRecord(context?.args);
|
|
748
|
+
const failedRun = failed && args?.action === "run";
|
|
749
|
+
if (isActivityDisplay(activity) && !failed) {
|
|
741
750
|
return new ActivityInlineComponent(activity, theme, options.expanded);
|
|
742
751
|
}
|
|
752
|
+
if (failedRun) {
|
|
753
|
+
const snapshot = identityFromDispatchArgs(asRecord(context?.args)?.request);
|
|
754
|
+
const display: ActivityDisplay = {
|
|
755
|
+
version: 1,
|
|
756
|
+
kind: "activityDisplay",
|
|
757
|
+
requestId: isActivityDisplay(activity)
|
|
758
|
+
? activity.requestId
|
|
759
|
+
: snapshot.requestId,
|
|
760
|
+
role: isActivityDisplay(activity) ? activity.role : snapshot.role,
|
|
761
|
+
phase: isActivityDisplay(activity) ? activity.phase : snapshot.phase,
|
|
762
|
+
objective: isActivityDisplay(activity)
|
|
763
|
+
? activity.objective
|
|
764
|
+
: snapshot.objective,
|
|
765
|
+
state: "failed",
|
|
766
|
+
tone: "error",
|
|
767
|
+
elapsedMs: isActivityDisplay(activity) ? activity.elapsedMs : 0,
|
|
768
|
+
reason: sanitizeFailureReason(
|
|
769
|
+
isActivityDisplay(activity) ? activity.reason : undefined,
|
|
770
|
+
),
|
|
771
|
+
};
|
|
772
|
+
return new ActivityInlineComponent(display, theme);
|
|
773
|
+
}
|
|
774
|
+
if (failed) {
|
|
775
|
+
return new Text(`${dispatchLabel(args?.action)} failed`, 0, 0);
|
|
776
|
+
}
|
|
743
777
|
const content = asRecord(result)?.content;
|
|
744
778
|
const text = Array.isArray(content)
|
|
745
779
|
? content
|
|
@@ -750,6 +784,11 @@ export function renderActivityResult(
|
|
|
750
784
|
return new Text(text, 0, 0);
|
|
751
785
|
}
|
|
752
786
|
|
|
787
|
+
function dispatchLabel(action: unknown): string {
|
|
788
|
+
const value = sanitizeDisplayText(action ?? "");
|
|
789
|
+
return value.length > 0 ? `Abel Dispatch ${value}` : "Abel Dispatch";
|
|
790
|
+
}
|
|
791
|
+
|
|
753
792
|
function isActivityDisplay(value: unknown): value is ActivityDisplay {
|
|
754
793
|
const record = asRecord(value);
|
|
755
794
|
return (
|
|
@@ -764,26 +803,55 @@ function isActivityDisplay(value: unknown): value is ActivityDisplay {
|
|
|
764
803
|
);
|
|
765
804
|
}
|
|
766
805
|
|
|
806
|
+
function identityFromDispatchArgs(request: unknown): {
|
|
807
|
+
requestId: string;
|
|
808
|
+
role: string;
|
|
809
|
+
phase: string;
|
|
810
|
+
objective: string;
|
|
811
|
+
} {
|
|
812
|
+
const value = asRecord(request);
|
|
813
|
+
const attempt = asRecord(value?.attempt);
|
|
814
|
+
const boundary = asRecord(value?.boundary);
|
|
815
|
+
return {
|
|
816
|
+
requestId: sanitizeDisplayText(
|
|
817
|
+
attempt?.requestId ?? value?.id ?? "unknown",
|
|
818
|
+
128,
|
|
819
|
+
),
|
|
820
|
+
role: sanitizeDisplayText(
|
|
821
|
+
value?.role ??
|
|
822
|
+
(value?.stage === "abel-implement"
|
|
823
|
+
? "implementation-worker"
|
|
824
|
+
: "unknown"),
|
|
825
|
+
80,
|
|
826
|
+
),
|
|
827
|
+
phase: sanitizeDisplayText(attempt?.phase ?? value?.phase ?? "unknown", 40),
|
|
828
|
+
objective: sanitizeDisplayText(
|
|
829
|
+
value?.objective ?? boundary?.objective ?? "",
|
|
830
|
+
240,
|
|
831
|
+
),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
767
835
|
export function renderActivityCall(
|
|
768
836
|
args: unknown,
|
|
769
837
|
theme?: ActivityTheme,
|
|
838
|
+
context?: ActivityRenderContext,
|
|
770
839
|
): Component {
|
|
771
840
|
const value = asRecord(args);
|
|
772
841
|
if (value?.action !== "run") {
|
|
773
|
-
return new Text(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
);
|
|
842
|
+
return new Text(context?.isError ? "" : dispatchLabel(value?.action), 0, 0);
|
|
843
|
+
}
|
|
844
|
+
if (context?.executionStarted || context?.isError) {
|
|
845
|
+
return new Text("");
|
|
778
846
|
}
|
|
779
|
-
|
|
780
|
-
|
|
847
|
+
if (!asRecord(value.request)) return new Text("Subagent", 0, 0);
|
|
848
|
+
const identity = identityFromDispatchArgs(value.request);
|
|
781
849
|
const display: ActivitySnapshot = {
|
|
782
850
|
toolCallId: "call",
|
|
783
|
-
requestId:
|
|
784
|
-
role:
|
|
785
|
-
phase:
|
|
786
|
-
objective:
|
|
851
|
+
requestId: identity.requestId,
|
|
852
|
+
role: identity.role,
|
|
853
|
+
phase: identity.phase,
|
|
854
|
+
objective: identity.objective,
|
|
787
855
|
state: "queued",
|
|
788
856
|
sequence: 0,
|
|
789
857
|
startedAt: Date.now(),
|