zob-harness 0.9.2 → 0.10.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.
- package/.pi/extensions/zob-harness/src/runtime/delegation-activity.ts +289 -0
- package/.pi/extensions/zob-harness/src/runtime/delegation-feed.ts +27 -0
- package/.pi/extensions/zob-harness/src/runtime/delegation-monitor.ts +8 -0
- package/.pi/extensions/zob-harness/src/runtime/state.ts +2 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/helpers.ts +31 -1
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/register.ts +3 -0
- package/.pi/extensions/zob-harness/src/runtime/widget.ts +14 -4
- package/package.json +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { isRecord } from "../core/utils/records.js";
|
|
5
|
+
import { sanitizeDelegationText } from "./delegation-markdown.js";
|
|
6
|
+
|
|
7
|
+
const ACTIVITY_MAX_BYTES = 180_000;
|
|
8
|
+
const DEFAULT_RECENT_LIMIT = 5;
|
|
9
|
+
|
|
10
|
+
export type DelegationActivityStatus = "running" | "success" | "error";
|
|
11
|
+
|
|
12
|
+
export interface DelegationActivity {
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
status: DelegationActivityStatus;
|
|
16
|
+
startedAtMs?: number;
|
|
17
|
+
endedAtMs?: number;
|
|
18
|
+
elapsedMs?: number;
|
|
19
|
+
command?: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
target?: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
outputSummary?: string;
|
|
24
|
+
errorSummary?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DelegationActivitySnapshot {
|
|
28
|
+
status: "ok" | "missing" | "blocked" | "empty";
|
|
29
|
+
note?: string;
|
|
30
|
+
fingerprint: string;
|
|
31
|
+
current?: DelegationActivity;
|
|
32
|
+
recent: DelegationActivity[];
|
|
33
|
+
lastUpdatedMs?: number;
|
|
34
|
+
quietMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePath(path: string): string {
|
|
38
|
+
return resolve(path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isInside(root: string, candidate: string): boolean {
|
|
42
|
+
const normalizedRoot = normalizePath(root);
|
|
43
|
+
const normalizedCandidate = normalizePath(candidate);
|
|
44
|
+
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${sep}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readTail(path: string, size: number, maxBytes: number): string {
|
|
48
|
+
if (size <= maxBytes) {
|
|
49
|
+
const buffer = Buffer.alloc(size);
|
|
50
|
+
const fd = openSync(path, "r");
|
|
51
|
+
try {
|
|
52
|
+
readSync(fd, buffer, 0, size, 0);
|
|
53
|
+
} finally {
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
return buffer.toString("utf8");
|
|
57
|
+
}
|
|
58
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
59
|
+
const fd = openSync(path, "r");
|
|
60
|
+
try {
|
|
61
|
+
readSync(fd, buffer, 0, maxBytes, Math.max(0, size - maxBytes));
|
|
62
|
+
} finally {
|
|
63
|
+
closeSync(fd);
|
|
64
|
+
}
|
|
65
|
+
const text = buffer.toString("utf8");
|
|
66
|
+
const firstNewline = text.indexOf("\n");
|
|
67
|
+
const safeTail = firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
|
|
68
|
+
return `{"type":"custom","customType":"zob-activity-tail-capped","data":{"omittedBytes":${size - maxBytes}}}\n${safeTail}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function timestampMs(entry: Record<string, unknown>): number | undefined {
|
|
72
|
+
if (typeof entry.timestamp !== "string") return undefined;
|
|
73
|
+
const parsed = Date.parse(entry.timestamp);
|
|
74
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compactWhitespace(value: string): string {
|
|
78
|
+
return value.replace(/\s+/g, " ").trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function redactDelegationCommand(command: string): string {
|
|
82
|
+
return sanitizeDelegationText(command)
|
|
83
|
+
.replace(/\b([A-Z0-9_]*(?:DATABASE_URL|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi, "$1=<redacted>")
|
|
84
|
+
.replace(/(Authorization\s*:\s*Bearer\s+)("[^"]*"|'[^']*'|\S+)/gi, "$1<redacted>")
|
|
85
|
+
.replace(/\b(--(?:password|passwd|token|secret|api-key|access-key|private-key|database-url)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi, "$1<redacted>")
|
|
86
|
+
.replace(/\b(-H\s+["']?Authorization\s*:\s*Bearer\s+)([^"'\s]+)/gi, "$1<redacted>");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function capInline(value: string, limit: number): string {
|
|
90
|
+
const compact = compactWhitespace(sanitizeDelegationText(value));
|
|
91
|
+
if (compact.length <= limit) return compact;
|
|
92
|
+
const head = Math.max(8, Math.floor(limit * 0.65));
|
|
93
|
+
const tail = Math.max(4, limit - head - 1);
|
|
94
|
+
return `${compact.slice(0, head)}…${compact.slice(-tail)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function contentText(content: unknown): string {
|
|
98
|
+
if (typeof content === "string") return sanitizeDelegationText(content);
|
|
99
|
+
if (!Array.isArray(content)) return "";
|
|
100
|
+
const parts: string[] = [];
|
|
101
|
+
for (const part of content) {
|
|
102
|
+
if (!isRecord(part) || typeof part.type !== "string") continue;
|
|
103
|
+
if (part.type === "text" && typeof part.text === "string") parts.push(sanitizeDelegationText(part.text));
|
|
104
|
+
else if (part.type === "image") parts.push("[image]");
|
|
105
|
+
}
|
|
106
|
+
return parts.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toolArguments(part: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
const direct = part.arguments ?? part.args ?? part.input;
|
|
111
|
+
return isRecord(direct) ? direct : {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toolName(part: Record<string, unknown>): string {
|
|
115
|
+
return typeof part.name === "string" ? part.name : typeof part.toolName === "string" ? part.toolName : "tool";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function timeoutMsFromArgs(args: Record<string, unknown>): number | undefined {
|
|
119
|
+
const raw = args.timeout ?? args.timeout_ms ?? args.timeoutMs;
|
|
120
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return undefined;
|
|
121
|
+
// Pi bash tool accepts timeout in seconds; keep millisecond values if callers already use them.
|
|
122
|
+
return raw > 10_000 ? Math.floor(raw) : Math.floor(raw * 1000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function activityFromToolCall(entry: Record<string, unknown>, part: Record<string, unknown>): DelegationActivity | undefined {
|
|
126
|
+
const id = typeof part.id === "string" ? part.id : undefined;
|
|
127
|
+
if (!id) return undefined;
|
|
128
|
+
const name = toolName(part);
|
|
129
|
+
const lower = name.toLowerCase();
|
|
130
|
+
const args = toolArguments(part);
|
|
131
|
+
const startedAtMs = timestampMs(entry);
|
|
132
|
+
const activity: DelegationActivity = {
|
|
133
|
+
toolCallId: id,
|
|
134
|
+
toolName: name,
|
|
135
|
+
status: "running",
|
|
136
|
+
startedAtMs,
|
|
137
|
+
timeoutMs: timeoutMsFromArgs(args),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const path = typeof args.path === "string" ? args.path : typeof args.file === "string" ? args.file : undefined;
|
|
141
|
+
if (lower === "bash") {
|
|
142
|
+
const command = typeof args.command === "string" ? redactDelegationCommand(args.command) : undefined;
|
|
143
|
+
activity.command = command;
|
|
144
|
+
activity.summary = command ? capInline(command, 140) : "shell command";
|
|
145
|
+
} else if (lower === "read") {
|
|
146
|
+
activity.target = path;
|
|
147
|
+
activity.summary = path ? `read ${path}` : "read file";
|
|
148
|
+
} else if (lower === "grep") {
|
|
149
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : typeof args.query === "string" ? args.query : undefined;
|
|
150
|
+
activity.target = path;
|
|
151
|
+
activity.summary = `grep ${pattern ? capInline(pattern, 60) : "pattern"}${path ? ` in ${path}` : ""}`;
|
|
152
|
+
} else if (lower === "find") {
|
|
153
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : "files";
|
|
154
|
+
activity.target = path;
|
|
155
|
+
activity.summary = `find ${capInline(pattern, 60)}${path ? ` in ${path}` : ""}`;
|
|
156
|
+
} else if (lower === "edit" || lower === "write") {
|
|
157
|
+
activity.target = path;
|
|
158
|
+
const edits = Array.isArray(args.edits) ? ` · ${args.edits.length} edit${args.edits.length === 1 ? "" : "s"}` : "";
|
|
159
|
+
activity.summary = `${lower} ${path ?? "file"}${edits}`;
|
|
160
|
+
} else if (lower === "delegate_task" || lower === "delegate_agent") {
|
|
161
|
+
const agent = typeof args.agent === "string" ? args.agent : undefined;
|
|
162
|
+
activity.summary = `${lower}${agent ? ` → ${agent}` : ""}`;
|
|
163
|
+
} else {
|
|
164
|
+
activity.summary = Object.entries(args).length > 0
|
|
165
|
+
? `${name} ${Object.entries(args).slice(0, 3).map(([key, value]) => `${key}=${typeof value === "string" ? capInline(value, 40) : Array.isArray(value) ? `${value.length} items` : String(value)}`).join(" ")}`
|
|
166
|
+
: name;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return activity;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resultSummary(text: string): string | undefined {
|
|
173
|
+
const trimmed = text.trim();
|
|
174
|
+
if (!trimmed) return undefined;
|
|
175
|
+
const compact = compactWhitespace(trimmed);
|
|
176
|
+
const timeout = compact.match(/Command timed out after\s+([0-9.]+)\s+seconds/i);
|
|
177
|
+
if (timeout) return `timed out after ${timeout[1]}s`;
|
|
178
|
+
const lineCount = trimmed.split("\n").length;
|
|
179
|
+
if (lineCount > 1 || trimmed.length > 120) return `${lineCount} line${lineCount === 1 ? "" : "s"} · ${trimmed.length} chars`;
|
|
180
|
+
return capInline(trimmed, 140);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyToolResult(entry: Record<string, unknown>, message: Record<string, unknown>, byId: Map<string, DelegationActivity>, order: DelegationActivity[]): void {
|
|
184
|
+
const id = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
185
|
+
const name = typeof message.toolName === "string" ? message.toolName : "tool";
|
|
186
|
+
if (!id) return;
|
|
187
|
+
let activity = byId.get(id);
|
|
188
|
+
if (!activity) {
|
|
189
|
+
activity = { toolCallId: id, toolName: name, status: "running" };
|
|
190
|
+
byId.set(id, activity);
|
|
191
|
+
order.push(activity);
|
|
192
|
+
}
|
|
193
|
+
const endedAtMs = timestampMs(entry);
|
|
194
|
+
activity.endedAtMs = endedAtMs;
|
|
195
|
+
activity.elapsedMs = activity.startedAtMs && endedAtMs ? Math.max(0, endedAtMs - activity.startedAtMs) : undefined;
|
|
196
|
+
activity.status = message.isError === true ? "error" : "success";
|
|
197
|
+
const text = contentText(message.content);
|
|
198
|
+
const summary = resultSummary(text);
|
|
199
|
+
if (message.isError === true) activity.errorSummary = summary ?? "tool error";
|
|
200
|
+
else activity.outputSummary = summary ?? "completed";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseActivityText(text: string, nowMs: number): { recent: DelegationActivity[]; current?: DelegationActivity; lastUpdatedMs?: number } {
|
|
204
|
+
const byId = new Map<string, DelegationActivity>();
|
|
205
|
+
const order: DelegationActivity[] = [];
|
|
206
|
+
let lastUpdatedMs: number | undefined;
|
|
207
|
+
|
|
208
|
+
for (const rawLine of text.split("\n")) {
|
|
209
|
+
if (!rawLine.trim()) continue;
|
|
210
|
+
let parsed: unknown;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(rawLine);
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(parsed)) continue;
|
|
217
|
+
const at = timestampMs(parsed);
|
|
218
|
+
if (at !== undefined) lastUpdatedMs = lastUpdatedMs === undefined ? at : Math.max(lastUpdatedMs, at);
|
|
219
|
+
|
|
220
|
+
if (parsed.type === "message" && isRecord(parsed.message)) {
|
|
221
|
+
const message = parsed.message;
|
|
222
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
223
|
+
for (const part of message.content) {
|
|
224
|
+
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
225
|
+
const activity = activityFromToolCall(parsed, part);
|
|
226
|
+
if (!activity) continue;
|
|
227
|
+
byId.set(activity.toolCallId, activity);
|
|
228
|
+
order.push(activity);
|
|
229
|
+
}
|
|
230
|
+
} else if (message.role === "toolResult") {
|
|
231
|
+
applyToolResult(parsed, message, byId, order);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const activity of order) {
|
|
237
|
+
if (activity.status === "running" && activity.startedAtMs !== undefined) activity.elapsedMs = Math.max(0, nowMs - activity.startedAtMs);
|
|
238
|
+
}
|
|
239
|
+
const current = [...order].reverse().find((activity) => activity.status === "running");
|
|
240
|
+
return { recent: order.slice(-DEFAULT_RECENT_LIMIT), current, lastUpdatedMs };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function readDelegationActivitySnapshot(sessionPath: string | undefined, repoRoot: string, nowMs = Date.now()): DelegationActivitySnapshot {
|
|
244
|
+
if (!sessionPath) return { status: "missing", note: "Child session is not captured yet.", fingerprint: "no-session", recent: [] };
|
|
245
|
+
const resolved = resolve(sessionPath);
|
|
246
|
+
if (!isInside(repoRoot, resolved)) {
|
|
247
|
+
return { status: "blocked", note: `Child session path is outside repo root: ${sessionPath}`, fingerprint: `blocked:${sessionPath}`, recent: [] };
|
|
248
|
+
}
|
|
249
|
+
if (!existsSync(resolved)) return { status: "missing", note: `Child session file not found yet: ${sessionPath}`, fingerprint: `missing:${sessionPath}`, recent: [] };
|
|
250
|
+
const stat = statSync(resolved);
|
|
251
|
+
if (stat.size === 0) return { status: "empty", note: "Child session file is empty.", fingerprint: `${resolved}:0:${Math.floor(stat.mtimeMs)}`, recent: [], lastUpdatedMs: Math.floor(stat.mtimeMs), quietMs: Math.max(0, nowMs - stat.mtimeMs) };
|
|
252
|
+
const text = readTail(resolved, stat.size, ACTIVITY_MAX_BYTES);
|
|
253
|
+
const parsed = parseActivityText(text, nowMs);
|
|
254
|
+
const lastUpdatedMs = parsed.lastUpdatedMs ?? Math.floor(stat.mtimeMs);
|
|
255
|
+
const cappedNote = stat.size > ACTIVITY_MAX_BYTES ? `Activity feed read from last ${ACTIVITY_MAX_BYTES} bytes of ${stat.size} bytes.` : undefined;
|
|
256
|
+
return {
|
|
257
|
+
status: "ok",
|
|
258
|
+
note: cappedNote,
|
|
259
|
+
fingerprint: `${resolved}:${stat.size}:${Math.floor(stat.mtimeMs)}`,
|
|
260
|
+
recent: parsed.recent,
|
|
261
|
+
current: parsed.current,
|
|
262
|
+
lastUpdatedMs,
|
|
263
|
+
quietMs: Math.max(0, nowMs - Math.max(lastUpdatedMs, Math.floor(stat.mtimeMs))),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function formatActivityDuration(ms: number | undefined): string | undefined {
|
|
268
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return undefined;
|
|
269
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
270
|
+
const seconds = totalSeconds % 60;
|
|
271
|
+
const minutes = Math.floor(totalSeconds / 60) % 60;
|
|
272
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
273
|
+
const pad2 = (value: number): string => String(value).padStart(2, "0");
|
|
274
|
+
if (hours > 0) return `${hours}:${pad2(minutes)}:${pad2(seconds)}`;
|
|
275
|
+
return `${pad2(minutes)}:${pad2(seconds)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatActivitySummary(activity: DelegationActivity, options: { includeCommand?: boolean; nowMs?: number } = {}): string[] {
|
|
279
|
+
const icon = activity.status === "running" ? "◉" : activity.status === "success" ? "✓" : "✗";
|
|
280
|
+
const duration = formatActivityDuration(activity.elapsedMs ?? (activity.status === "running" && activity.startedAtMs ? (options.nowMs ?? Date.now()) - activity.startedAtMs : undefined));
|
|
281
|
+
const timeout = formatActivityDuration(activity.timeoutMs);
|
|
282
|
+
const detail = activity.status === "running"
|
|
283
|
+
? [duration ? `running ${duration}` : "running", timeout ? `timeout ${timeout}` : undefined].filter(Boolean).join(" / ")
|
|
284
|
+
: activity.errorSummary ?? activity.outputSummary ?? (activity.status === "success" ? "done" : "error");
|
|
285
|
+
const summary = activity.summary ?? activity.command ?? activity.target ?? activity.toolName;
|
|
286
|
+
const lines = [`${icon} ${activity.toolName}${detail ? ` · ${detail}` : ""}${summary && activity.toolName.toLowerCase() !== summary.toLowerCase() ? ` · ${summary}` : ""}`];
|
|
287
|
+
if (options.includeCommand && activity.command) lines.push(`$ ${activity.command}`);
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { Markdown, truncateToWidth, visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
5
5
|
|
|
6
6
|
import { delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCostLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDuration, statusIcon, type DelegationRunView } from "./delegation-monitor.js";
|
|
7
|
+
import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
|
|
7
8
|
import { sanitizeDelegationText } from "./delegation-markdown.js";
|
|
8
9
|
import { isRecord } from "../core/utils/records.js";
|
|
9
10
|
|
|
@@ -180,6 +181,31 @@ function actionCard(lines: string[], title: string, details: string[], width: nu
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
function renderLiveActivityCard(run: DelegationRunView, repoRoot: string, lines: string[], width: number, theme: Theme, nowMs = Date.now()): void {
|
|
185
|
+
const snapshot = readDelegationActivitySnapshot(run.sessionPath, repoRoot, nowMs);
|
|
186
|
+
if (snapshot.recent.length === 0) return;
|
|
187
|
+
pushSpacer(lines);
|
|
188
|
+
const bgKey = snapshot.current ? "toolPendingBg" : "toolSuccessBg";
|
|
189
|
+
lines.push(theme.bg(bgKey, padToWidth(theme.fg(snapshot.current ? "toolTitle" : "success", theme.bold(" Live activity")), width)));
|
|
190
|
+
if (snapshot.current) {
|
|
191
|
+
for (const line of formatActivitySummary(snapshot.current, { includeCommand: true, nowMs })) {
|
|
192
|
+
lines.push(theme.bg(bgKey, padToWidth(theme.fg(line.startsWith("$") ? "toolOutput" : "warning", ` ${line}`), width)));
|
|
193
|
+
}
|
|
194
|
+
if (typeof snapshot.quietMs === "number" && snapshot.quietMs >= 120_000) {
|
|
195
|
+
const quiet = `quiet ${formatActivityDuration(snapshot.quietMs)} since last transcript update`;
|
|
196
|
+
lines.push(theme.bg(bgKey, padToWidth(theme.fg(snapshot.quietMs >= 300_000 ? "warning" : "muted", ` ${quiet}`), width)));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const recent = snapshot.recent.slice(-5).filter((activity) => activity.toolCallId !== snapshot.current?.toolCallId);
|
|
200
|
+
if (recent.length > 0) lines.push(theme.bg(bgKey, padToWidth(theme.fg("muted", " last actions"), width)));
|
|
201
|
+
for (const activity of recent) {
|
|
202
|
+
const colorKey = activity.status === "error" ? "error" : activity.status === "running" ? "warning" : "success";
|
|
203
|
+
const first = formatActivitySummary(activity, { nowMs })[0];
|
|
204
|
+
if (first) lines.push(theme.bg(bgKey, padToWidth(theme.fg(colorKey, ` ${first}`), width)));
|
|
205
|
+
}
|
|
206
|
+
if (snapshot.note) lines.push(theme.bg(bgKey, padToWidth(theme.fg("muted", ` ${snapshot.note}`), width)));
|
|
207
|
+
}
|
|
208
|
+
|
|
183
209
|
function failureDetails(run: DelegationRunView, fallback: string): string[] {
|
|
184
210
|
const details: string[] = [];
|
|
185
211
|
if (run.errorMessage) details.push(`message: ${sanitizeDelegationText(run.errorMessage)}`);
|
|
@@ -372,6 +398,7 @@ export function renderDelegationFeedLines(run: DelegationRunView | undefined, re
|
|
|
372
398
|
lines.push(theme.fg("dim", `${statusIcon(run.status)} ${run.agent}${signalText ? ` · ${theme.fg(delegationSignalColor(signalBadge), signalText)}` : ""}${modelLabel ? ` · ${theme.fg("muted", `(${modelLabel})`)}` : ""} · ${run.status}${run.failureKind ? ` · ${run.failureKind}` : ""} · ${formatDuration(delegationDurationMs(run))} · ${formatDelegationCostLabel(run)} · ${formatDelegationContextLabel(run)}`));
|
|
373
399
|
if (run.usage) lines.push(theme.fg("muted", `usage · in ${run.usage.input} · out ${run.usage.output} · cache ${run.usage.cacheRead}/${run.usage.cacheWrite} · context ${run.usage.contextTokens}`));
|
|
374
400
|
if (run.taskPreview) lines.push(theme.fg("muted", `task · ${sanitizeDelegationText(run.taskPreview)}`));
|
|
401
|
+
renderLiveActivityCard(run, repoRoot, lines, safeWidth, theme);
|
|
375
402
|
let renderedFailureSummary = false;
|
|
376
403
|
if (run.failureKind === "preflight" || run.failureKind === "config") {
|
|
377
404
|
renderedFailureSummary = true;
|
|
@@ -40,6 +40,7 @@ export interface DelegationRunView {
|
|
|
40
40
|
childChangedPaths?: ChildResult["childChangedPaths"];
|
|
41
41
|
usage?: ChildResult["usage"];
|
|
42
42
|
model?: string;
|
|
43
|
+
background?: boolean;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface DelegationMonitorState {
|
|
@@ -73,6 +74,8 @@ const TRANSCRIPT_MAX_BYTES = 240_000;
|
|
|
73
74
|
const TRANSCRIPT_MAX_LINES = 1_200;
|
|
74
75
|
const WIDGET_COMPLETE_TTL_MS = 30_000;
|
|
75
76
|
const WIDGET_FAILURE_TTL_MS = 120_000;
|
|
77
|
+
const WIDGET_BACKGROUND_COMPLETE_TTL_MS = 10 * 60_000;
|
|
78
|
+
const WIDGET_BACKGROUND_FAILURE_TTL_MS = 30 * 60_000;
|
|
76
79
|
|
|
77
80
|
function capPreview(text: string | undefined, limit = PREVIEW_LIMIT): string {
|
|
78
81
|
const value = text ?? "";
|
|
@@ -362,6 +365,10 @@ export function shouldShowRunInWidget(run: DelegationRunView, nowMs = Date.now()
|
|
|
362
365
|
if (run.status === "queued" || run.status === "running") return true;
|
|
363
366
|
if (!run.endedAtMs) return false;
|
|
364
367
|
const ageMs = Math.max(0, nowMs - run.endedAtMs);
|
|
368
|
+
if (run.background) {
|
|
369
|
+
if (run.status === "complete") return ageMs <= WIDGET_BACKGROUND_COMPLETE_TTL_MS;
|
|
370
|
+
return ageMs <= WIDGET_BACKGROUND_FAILURE_TTL_MS;
|
|
371
|
+
}
|
|
365
372
|
if (run.status === "complete") return ageMs <= WIDGET_COMPLETE_TTL_MS;
|
|
366
373
|
return ageMs <= WIDGET_FAILURE_TTL_MS;
|
|
367
374
|
}
|
|
@@ -466,6 +473,7 @@ export function updateDelegationRun(state: DelegationMonitorState, id: string, p
|
|
|
466
473
|
if (patch.errorMessage !== undefined) run.errorMessage = patch.errorMessage;
|
|
467
474
|
if (patch.usage !== undefined) run.usage = patch.usage;
|
|
468
475
|
if (patch.model !== undefined) run.model = patch.model;
|
|
476
|
+
if (patch.background !== undefined) run.background = patch.background;
|
|
469
477
|
return run;
|
|
470
478
|
}
|
|
471
479
|
|
|
@@ -414,6 +414,8 @@ function restoreDelegationRunsFromEntries(state: HarnessRuntimeState, entries: u
|
|
|
414
414
|
errorMessage: Array.isArray(data.errors) ? data.errors.map(String).join("; ") : existing?.errorMessage,
|
|
415
415
|
childChangedPaths: normalizeZcommitChildChangedPathRefs(data.childChangedPathRefs) ?? existing?.childChangedPaths,
|
|
416
416
|
usage: usageField(data) ?? existing?.usage,
|
|
417
|
+
model: stringField(data, "model") ?? existing?.model,
|
|
418
|
+
background: (typeof data.background === "boolean" ? data.background : undefined) ?? existing?.background,
|
|
417
419
|
};
|
|
418
420
|
if (event === "start") restoredRun.endedAtMs = existing?.endedAtMs;
|
|
419
421
|
restored.set(runId, restoredRun);
|
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
statusIcon,
|
|
41
41
|
updateDelegationRun,
|
|
42
42
|
type DelegationRunMode,
|
|
43
|
+
type DelegationRunView,
|
|
43
44
|
} from "../delegation-monitor.js";
|
|
45
|
+
import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "../delegation-activity.js";
|
|
44
46
|
import { delegateViewLink } from "../delegation-mouse.js";
|
|
45
47
|
import type { BackgroundDelegationRuntimeRun, HarnessRuntimeState } from "../state.js";
|
|
46
48
|
import { strictGoalErrors, strictGoalSpecErrors } from "../state.js";
|
|
@@ -728,6 +730,32 @@ export function formatDelegationCatalogSummary(catalog: Record<string, unknown>)
|
|
|
728
730
|
return lines.join("\n");
|
|
729
731
|
}
|
|
730
732
|
|
|
733
|
+
function renderMiniActivityLines(run: DelegationRunView, nowMs: number, expanded: boolean, theme: { fg: (color: any, text: string) => string }): string[] {
|
|
734
|
+
const lines: string[] = [];
|
|
735
|
+
const snapshot = readDelegationActivitySnapshot(run.sessionPath, process.cwd(), nowMs);
|
|
736
|
+
const recent = snapshot.recent.slice(-5);
|
|
737
|
+
if (recent.length === 0) {
|
|
738
|
+
if ((run.status === "running" || run.status === "queued") && snapshot.note && snapshot.status !== "missing") {
|
|
739
|
+
lines.push(` ${theme.fg("muted", snapshot.note)}`);
|
|
740
|
+
}
|
|
741
|
+
return lines;
|
|
742
|
+
}
|
|
743
|
+
const label = expanded ? "last actions" : "last";
|
|
744
|
+
lines.push(` ${theme.fg("dim", `${label} ${recent.length}`)}`);
|
|
745
|
+
for (const activity of recent) {
|
|
746
|
+
const color = activity.status === "running" ? "warning" : activity.status === "error" ? "error" : "success";
|
|
747
|
+
const includeCommand = activity.status === "running" && activity.toolName.toLowerCase() === "bash";
|
|
748
|
+
const rendered = formatActivitySummary(activity, { includeCommand, nowMs });
|
|
749
|
+
const [first, ...rest] = rendered;
|
|
750
|
+
if (first) lines.push(` ${theme.fg(color, first)}`);
|
|
751
|
+
for (const extra of rest.slice(0, expanded ? 2 : 1)) lines.push(` ${theme.fg("muted", extra)}`);
|
|
752
|
+
}
|
|
753
|
+
if (snapshot.current && typeof snapshot.quietMs === "number" && snapshot.quietMs >= 120_000) {
|
|
754
|
+
lines.push(` ${theme.fg(snapshot.quietMs >= 300_000 ? "warning" : "muted", `quiet ${formatActivityDuration(snapshot.quietMs)} since last transcript update`)}`);
|
|
755
|
+
}
|
|
756
|
+
return lines;
|
|
757
|
+
}
|
|
758
|
+
|
|
731
759
|
export function renderDelegationToolResultText(source: "delegate_agent" | "delegate_task", details: DelegationDetails | undefined, state: HarnessRuntimeState, toolCallId: string | undefined, isPartial: boolean, expanded: boolean, theme: { fg: (color: any, text: string) => string; bold: (text: string) => string }): string {
|
|
732
760
|
hydrateDelegationRunsFromDetails(source, details, state, toolCallId);
|
|
733
761
|
const nowMs = Date.now();
|
|
@@ -754,11 +782,13 @@ export function renderDelegationToolResultText(source: "delegate_agent" | "deleg
|
|
|
754
782
|
const prefix = index === visibleRuns.length - 1 && !hasMore ? "└─" : "├─";
|
|
755
783
|
const viewHint = delegateViewLink(run.id);
|
|
756
784
|
const kind = run.failureKind ? ` ${run.failureKind}` : "";
|
|
785
|
+
const background = run.background ? " bg" : "";
|
|
757
786
|
const badge = delegationSignalBadge(run);
|
|
758
787
|
const badgeText = formatDelegationSignalBadge(badge);
|
|
759
788
|
const modelLabel = formatDelegationModelLabel(run);
|
|
760
789
|
const modelSuffix = modelLabel ? ` ${theme.fg("muted", `(${modelLabel})`)}` : "";
|
|
761
|
-
lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
|
|
790
|
+
lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}${background}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
|
|
791
|
+
lines.push(...renderMiniActivityLines(run, nowMs, expanded, theme));
|
|
762
792
|
if (expanded && (run.errorMessage || run.stopReason || run.gateErrors?.length)) lines.push(` ${theme.fg("dim", run.errorMessage ?? run.gateErrors?.join("; ") ?? run.stopReason ?? "")}`);
|
|
763
793
|
}
|
|
764
794
|
if (hasMore) lines.push(theme.fg("dim", `└─ … ${monitoredRuns.length - maxRows} more child run(s)`));
|
|
@@ -827,6 +827,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
|
|
|
827
827
|
taskHash: sha256(structuredTask),
|
|
828
828
|
originalUserAskHash: sha256(params.original_user_ask ?? state.activeGoal?.originalUserAsk ?? ""),
|
|
829
829
|
outputContract,
|
|
830
|
+
background: params.run_in_background === true,
|
|
830
831
|
startedAt,
|
|
831
832
|
});
|
|
832
833
|
|
|
@@ -975,6 +976,8 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
|
|
|
975
976
|
};
|
|
976
977
|
|
|
977
978
|
if (params.run_in_background) {
|
|
979
|
+
updateDelegationRun(state.delegations, runId, { background: true });
|
|
980
|
+
renderDelegationMonitor();
|
|
978
981
|
const backgroundController = new AbortController();
|
|
979
982
|
const backgroundPromise = runDelegateTaskChild(backgroundController.signal, false);
|
|
980
983
|
const backgroundRun: BackgroundDelegationRuntimeRun = { runId, startedAtMs, promise: backgroundPromise, abortController: backgroundController };
|
|
@@ -8,7 +8,8 @@ import { isRecord } from "../core/utils/records.js";
|
|
|
8
8
|
import type { AssistantLikeMessage, ModeName } from "../types.js";
|
|
9
9
|
import { readHarnessReadinessWidgetData } from "../domains/orchestration/widget-readers.js";
|
|
10
10
|
import { buildZpeerPeerRoomSummaries, type ZpeerPeerRoomSummary } from "../domains/coms/coms-v2/zpeer.js";
|
|
11
|
-
import { delegationCost, delegationDurationMs, formatDelegationCost, formatDuration, summarizeDelegations } from "./delegation-monitor.js";
|
|
11
|
+
import { delegationCost, delegationDurationMs, formatDelegationCost, formatDuration, listWidgetDelegationRuns, summarizeDelegations } from "./delegation-monitor.js";
|
|
12
|
+
import { formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
|
|
12
13
|
import { disposeDelegationMouseSupport } from "./delegation-mouse.js";
|
|
13
14
|
import { formatZcompactHudLine } from "./auto-compaction.js";
|
|
14
15
|
import type { HarnessRuntimeState } from "./state.js";
|
|
@@ -339,9 +340,18 @@ export function renderHarnessWidget(pi: ExtensionAPI, state: HarnessRuntimeState
|
|
|
339
340
|
? "working"
|
|
340
341
|
: "idle";
|
|
341
342
|
const assistantsCount = delegationSummary.running + delegationSummary.queued + (runtimeTodoSummary?.delegated ?? 0);
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
const widgetDelegationRuns = listWidgetDelegationRuns(state.delegations, renderNowMs);
|
|
344
|
+
const activeDelegationRun = widgetDelegationRuns.find((run) => run.status === "running" || run.status === "queued") ?? widgetDelegationRuns[0];
|
|
345
|
+
const activeDelegationSnapshot = activeDelegationRun ? readDelegationActivitySnapshot(activeDelegationRun.sessionPath, ctx.cwd, renderNowMs) : undefined;
|
|
346
|
+
const activeDelegationActivity = activeDelegationSnapshot?.current ?? activeDelegationSnapshot?.recent.at(-1);
|
|
347
|
+
const activeDelegationLine = activeDelegationRun && activeDelegationActivity
|
|
348
|
+
? `${activeDelegationRun.agent}: ${formatActivitySummary(activeDelegationActivity, { nowMs: renderNowMs })[0] ?? activeDelegationActivity.toolName}`
|
|
349
|
+
: undefined;
|
|
350
|
+
const assistantsState = activeDelegationLine
|
|
351
|
+
? truncateToWidth(`${assistantsCount > 0 ? `${assistantsCount} active` : "recent"} · ${activeDelegationLine}`, 96, "…")
|
|
352
|
+
: assistantsCount > 0
|
|
353
|
+
? `${assistantsCount} active`
|
|
354
|
+
: "none";
|
|
345
355
|
const zpeerRoomSummaries = cachedZpeerRoomSummaries(state, ctx, renderNowMs);
|
|
346
356
|
const zpeerLast = state.zobLive.lastEvent
|
|
347
357
|
? `${state.zobLive.lastEvent.kind} ${state.zobLive.lastEvent.fromAlias ? `@${state.zobLive.lastEvent.fromAlias}` : "?"}→${state.zobLive.lastEvent.toAlias ? `@${state.zobLive.lastEvent.toAlias}` : "?"} ${state.zobLive.lastEvent.status}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A governed Agent Factory for Pi: launch communicating agent teams, run tmux-backed factories, validate artifacts, and package repeatable workflows.",
|
|
6
6
|
"license": "MIT",
|