@xingular/harness 1.5.0-unstable → 1.6.0-unstable
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/CHANGELOG.md +93 -0
- package/browser-extension/README.md +32 -0
- package/browser-extension/background.js +612 -0
- package/browser-extension/manifest.json +16 -0
- package/browser-extension/overlay.js +97 -0
- package/browser-extension/popup.css +19 -0
- package/browser-extension/popup.html +30 -0
- package/browser-extension/popup.js +101 -0
- package/browser-extension/site-access.js +63 -0
- package/dist/cli.js +1489 -680
- package/dist/sdk/browser/config.d.ts +3 -0
- package/dist/sdk/browser/config.js +2 -1
- package/dist/sdk/browser/extension/bridge.d.ts +55 -0
- package/dist/sdk/browser/extension/bridge.js +336 -0
- package/dist/sdk/browser/extension/session.d.ts +50 -0
- package/dist/sdk/browser/extension/session.js +321 -0
- package/dist/sdk/browser/index.d.ts +7 -2
- package/dist/sdk/browser/index.js +58 -25
- package/dist/sdk/browser/serverMethods.js +4 -1
- package/dist/sdk/browser/tools.d.ts +99 -2
- package/dist/sdk/browser/tools.js +6 -1
- package/dist/sdk/commands/registry.d.ts +5 -0
- package/dist/sdk/commands/registry.js +1 -0
- package/dist/sdk/harness.d.ts +4 -0
- package/dist/sdk/harness.js +1 -1
- package/dist/sdk/pi/retryPolicy.d.ts +19 -0
- package/dist/sdk/pi/retryPolicy.js +11 -0
- package/dist/sdk/prompt/compile.d.ts +0 -2
- package/dist/sdk/prompt/compile.js +2 -6
- package/dist/sdk/prompt/reporting.d.ts +1 -1
- package/dist/sdk/prompt/reporting.js +30 -13
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/remote/webClient.js +6 -2
- package/dist/sdk/sessions/contracts.d.ts +2 -0
- package/dist/sdk/sessions/contracts.js +5 -1
- package/dist/sdk/sessions/planMode.d.ts +16 -6
- package/dist/sdk/sessions/planMode.js +59 -23
- package/dist/sdk/sessions/runner.js +14 -5
- package/dist/sdk/sessions/workSlice.d.ts +15 -1
- package/dist/sdk/sessions/workSlice.js +17 -9
- package/dist/sdk/terminal/controller.d.ts +6 -0
- package/dist/sdk/terminal/controller.js +44 -5
- package/dist/sdk/terminal/opentui/model.js +3 -1
- package/dist/sdk/terminal/piExtension.js +7 -2
- package/dist/sdk/terminal/stallWatch.d.ts +18 -0
- package/dist/sdk/terminal/stallWatch.js +20 -0
- package/dist/sdk/terminal/subagentMonitor.d.ts +2 -0
- package/dist/sdk/terminal/subagentMonitor.js +4 -1
- package/dist/sdk/terminal/terminals.js +35 -4
- package/dist/sdk/terminal/toolName.d.ts +6 -0
- package/dist/sdk/terminal/toolName.js +12 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +14 -0
- package/dist/sdk/terminal/tui/commands.js +4 -1
- package/dist/sdk/terminal/tui/pages/followUpQueuePage.d.ts +34 -0
- package/dist/sdk/terminal/tui/pages/followUpQueuePage.js +130 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +37 -7
- package/dist/sdk/terminal/tui/runtime.js +2 -1
- package/dist/sdk/terminal/tui/settingsModel.js +88 -7
- package/dist/sdk/tools/askUser.js +2 -2
- package/dist/sdk/world/world.d.ts +12 -2
- package/dist/sdk/world/world.js +42 -4
- package/dist/terminal-opentui.js +1456 -756
- package/dist/terminal-pi.js +62 -31
- package/docs/README.md +1 -0
- package/docs/architecture/02-sessions-and-runs.md +43 -25
- package/docs/architecture/04-prompt.md +23 -22
- package/docs/architecture/10-server-protocol.md +23 -3
- package/docs/architecture/11-terminal.md +41 -29
- package/docs/architecture/12-storage.md +2 -1
- package/docs/architecture/13-world-tools.md +35 -22
- package/docs/architecture/14-surfaces-and-parity.md +24 -20
- package/docs/architecture/README.md +4 -1
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +3 -1
- package/docs/decisions/ADR-0027-connected-sketch-display.md +4 -2
- package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +6 -1
- package/docs/decisions/ADR-0077-plan-runs-may-conclude-no-plan-needed.md +120 -0
- package/docs/decisions/ADR-0078-child-aware-stall-watch.md +46 -0
- package/docs/decisions/ADR-0079-phase-led-reporting-guidance.md +72 -0
- package/docs/decisions/ADR-0080-fixed-width-question-sketch-canvas.md +50 -0
- package/docs/decisions/ADR-0081-no-work-slice-budget-by-default.md +36 -0
- package/docs/decisions/ADR-0082-connected-brave-browser.md +72 -0
- package/docs/parity.md +7 -5
- package/docs/releases/1.5.0-unstable.md +12 -3
- package/docs/releases/1.5.1-unstable.md +24 -0
- package/docs/releases/1.6.0-unstable.md +59 -0
- package/docs/study/00-index.md +17 -1
- package/docs/study/STUDY-0007-claude-code-prompt.md +162 -0
- package/docs/study/STUDY-0008-browser-control.md +281 -0
- package/docs/study/STUDY-0009-claude-code-reporting.md +205 -0
- package/docs/study/sources/claude-code.md +51 -39
- package/docs/terminal-tui.md +46 -18
- package/package.json +2 -1
|
@@ -22,7 +22,13 @@ export interface SlicePolicy {
|
|
|
22
22
|
maxSlices: number;
|
|
23
23
|
continue: boolean;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The foundation defaults: no budget (ADR-0081). A reply is not cut at a number of tool calls, a
|
|
27
|
+
* length of time or a number of tokens, and nothing is continued as a "next slice"; it ends when the
|
|
28
|
+
* work is done, a person's decision is needed, or the person interrupts. The limits stay available
|
|
29
|
+
* to a caller that asks for one (`workSlice`, `XINGULAR_WORK_SLICE`), and the three-compaction guard
|
|
30
|
+
* is not a budget and stays on.
|
|
31
|
+
*/
|
|
26
32
|
export declare const DEFAULT_SLICE_POLICY: SlicePolicy;
|
|
27
33
|
export declare class WorkSliceError extends Error {
|
|
28
34
|
}
|
|
@@ -150,4 +156,12 @@ export declare function sliceContinuation(options: SliceContinuationOptions): (p
|
|
|
150
156
|
export declare function sliceOf(root: string, runId: string, limit?: number): number;
|
|
151
157
|
/** The reason a checkpoint the harness derives (no model handoff) records. */
|
|
152
158
|
export declare function derivedReason(end: SliceEnd | null): Checkpoint['reason'];
|
|
159
|
+
/** What a heartbeat reports about the tools in flight: their names once each, and the oldest one's age. */
|
|
160
|
+
export declare function activeToolBeat(tools: ReadonlyMap<string, {
|
|
161
|
+
name: string;
|
|
162
|
+
at: number;
|
|
163
|
+
}>, now?: number): {
|
|
164
|
+
activeTools: string[];
|
|
165
|
+
oldestToolMs: number;
|
|
166
|
+
} | Record<string, never>;
|
|
153
167
|
export {};
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { findRun } from "./runStore.js";
|
|
2
2
|
export const WORK_SLICE_EXTENSION = 'work-slice';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The foundation defaults: no budget (ADR-0081). A reply is not cut at a number of tool calls, a
|
|
5
|
+
* length of time or a number of tokens, and nothing is continued as a "next slice"; it ends when the
|
|
6
|
+
* work is done, a person's decision is needed, or the person interrupts. The limits stay available
|
|
7
|
+
* to a caller that asks for one (`workSlice`, `XINGULAR_WORK_SLICE`), and the three-compaction guard
|
|
8
|
+
* is not a budget and stays on.
|
|
9
|
+
*/
|
|
4
10
|
export const DEFAULT_SLICE_POLICY = {
|
|
5
11
|
enabled: true,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// still gets eight slices before it returns to the person.
|
|
9
|
-
maxToolCalls: 320,
|
|
10
|
-
maxMinutes: 60,
|
|
11
|
-
// No token cap: the tool calls and the clock are the bound. Counting new input, cache writes and
|
|
12
|
-
// output made a few large reads end a reply before useful work (see the "Confirm plan model
|
|
13
|
-
// support" thread), so a cap is available to whoever asks for one and off unless they do.
|
|
12
|
+
maxToolCalls: 0,
|
|
13
|
+
maxMinutes: 0,
|
|
14
14
|
maxNewTokens: 0,
|
|
15
|
+
// Inert while no limit ends a slice; a caller that sets one gets the same continuation as before.
|
|
15
16
|
maxSlices: 8,
|
|
16
17
|
continue: true
|
|
17
18
|
};
|
|
@@ -256,3 +257,10 @@ export function sliceOf(root, runId, limit = 100) {
|
|
|
256
257
|
export function derivedReason(end) {
|
|
257
258
|
return end ?? 'requested';
|
|
258
259
|
}
|
|
260
|
+
/** What a heartbeat reports about the tools in flight: their names once each, and the oldest one's age. */
|
|
261
|
+
export function activeToolBeat(tools, now = Date.now()) {
|
|
262
|
+
if (!tools.size)
|
|
263
|
+
return {};
|
|
264
|
+
const names = [...new Set([...tools.values()].map((tool) => tool.name))].slice(0, 16);
|
|
265
|
+
return { activeTools: names, oldestToolMs: Math.max(0, now - Math.min(...[...tools.values()].map((tool) => tool.at))) };
|
|
266
|
+
}
|
|
@@ -78,6 +78,8 @@ export declare class TerminalController {
|
|
|
78
78
|
/** When the reply last produced something other than a heartbeat; silence is what a stall is. */
|
|
79
79
|
private progressAt;
|
|
80
80
|
private readonly runningTools;
|
|
81
|
+
/** Each live subagent's own clock: its last real event and the tools it is inside. */
|
|
82
|
+
private readonly children;
|
|
81
83
|
private stallRequested;
|
|
82
84
|
private stallTimer;
|
|
83
85
|
constructor(hooks: ControllerHooks);
|
|
@@ -103,6 +105,8 @@ export declare class TerminalController {
|
|
|
103
105
|
* Ends a reply its native turn can no longer finish: the process is alive and keeps reporting, but
|
|
104
106
|
* the reply produced nothing for the whole window (a model request that never came back). A person
|
|
105
107
|
* may already be answering an approval, and a tool may still be working, so neither is cut short.
|
|
108
|
+
* A reply that is only waiting for subagents is judged by them: a quiet child is ended on its own
|
|
109
|
+
* clock, which returns the wait and lets the reply carry on without anyone stepping in.
|
|
106
110
|
*/
|
|
107
111
|
private checkStall;
|
|
108
112
|
/** Hands the native process an instruction and waits until it acknowledges the prompt. */
|
|
@@ -115,6 +119,8 @@ export declare class TerminalController {
|
|
|
115
119
|
observe(event: RunEvent, descendant?: boolean): void;
|
|
116
120
|
/** A child event that belongs to the active terminal parent run. */
|
|
117
121
|
observeSubagent(parentRunId: string, run: RunRecord, event: RunEvent, profile?: TerminalSubagentProfile | null, runtime?: Pick<TerminalSubagentRun, 'accountLabel' | 'model' | 'effort'>): void;
|
|
122
|
+
/** Folds a child's event into its own stall clock; a heartbeat only proves the process lives. */
|
|
123
|
+
private trackChild;
|
|
118
124
|
/** An event of the tracked parent run itself, shown as the `main` row; it outlives settlement. */
|
|
119
125
|
observeMain(run: RunRecord, event: RunEvent, runtime?: MonitorRuntime): void;
|
|
120
126
|
/** Shows a saved parent run and its descendants, as they ended, until another run begins. */
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// extension's questions about policy, approvals, and what to do next.
|
|
5
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
6
6
|
import { HarnessError } from "../core/errors.js";
|
|
7
|
-
import { runStatusSchema } from "../sessions/contracts.js";
|
|
7
|
+
import { isActiveStatus, runStatusSchema } from "../sessions/contracts.js";
|
|
8
8
|
import { briefTitle } from "../sessions/subagents.js";
|
|
9
9
|
import { MAIN_PROFILE, SubagentMonitor } from "./subagentMonitor.js";
|
|
10
|
-
import { replyStalled } from "./stallWatch.js";
|
|
10
|
+
import { childStalled, replyStalled, waitingOnChildren } from "./stallWatch.js";
|
|
11
11
|
/** Stop codes the native extension may report for a reply it settled itself. */
|
|
12
12
|
const SETTLED_CODES = new Set(['slice_budget', 'compaction_thrash', 'needs_person', 'unfinished']);
|
|
13
13
|
function monitorRun(record, relation, profile, runtime) {
|
|
@@ -50,6 +50,8 @@ export class TerminalController {
|
|
|
50
50
|
/** When the reply last produced something other than a heartbeat; silence is what a stall is. */
|
|
51
51
|
progressAt = 0;
|
|
52
52
|
runningTools = new Map();
|
|
53
|
+
/** Each live subagent's own clock: its last real event and the tools it is inside. */
|
|
54
|
+
children = new Map();
|
|
53
55
|
stallRequested = null;
|
|
54
56
|
stallTimer = null;
|
|
55
57
|
constructor(hooks) {
|
|
@@ -131,6 +133,7 @@ export class TerminalController {
|
|
|
131
133
|
watchProgress() {
|
|
132
134
|
this.progressAt = Date.now();
|
|
133
135
|
this.runningTools.clear();
|
|
136
|
+
this.children.clear();
|
|
134
137
|
this.stallRequested = null;
|
|
135
138
|
if (this.stallTimer)
|
|
136
139
|
clearInterval(this.stallTimer);
|
|
@@ -143,17 +146,34 @@ export class TerminalController {
|
|
|
143
146
|
this.stallTimer = null;
|
|
144
147
|
this.stallRequested = null;
|
|
145
148
|
this.runningTools.clear();
|
|
149
|
+
this.children.clear();
|
|
146
150
|
}
|
|
147
151
|
/**
|
|
148
152
|
* Ends a reply its native turn can no longer finish: the process is alive and keeps reporting, but
|
|
149
153
|
* the reply produced nothing for the whole window (a model request that never came back). A person
|
|
150
154
|
* may already be answering an approval, and a tool may still be working, so neither is cut short.
|
|
155
|
+
* A reply that is only waiting for subagents is judged by them: a quiet child is ended on its own
|
|
156
|
+
* clock, which returns the wait and lets the reply carry on without anyone stepping in.
|
|
151
157
|
*/
|
|
152
158
|
checkStall() {
|
|
153
159
|
const run = this.activeRun;
|
|
154
|
-
if (!run || this.
|
|
160
|
+
if (!run || this.pending.size)
|
|
155
161
|
return;
|
|
156
|
-
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
const children = [...this.children.values()].map((child) => ({ progressAt: child.progressAt, runningTools: [...child.tools.values()], stopRequestedAt: child.stopRequestedAt }));
|
|
164
|
+
for (const [id, child] of this.children) {
|
|
165
|
+
if (!childStalled(now, { progressAt: child.progressAt, runningTools: [...child.tools.values()], stopRequestedAt: child.stopRequestedAt }))
|
|
166
|
+
continue;
|
|
167
|
+
child.stopRequestedAt = now;
|
|
168
|
+
this.hooks.telemetry('stall');
|
|
169
|
+
void this.hooks.interrupt?.(id, { reason: 'stalled', code: 'stalled' }).catch(() => { });
|
|
170
|
+
}
|
|
171
|
+
if (this.stallRequested)
|
|
172
|
+
return;
|
|
173
|
+
const tools = [...this.runningTools.values()];
|
|
174
|
+
if (waitingOnChildren(now, tools.map((tool) => tool.name), children))
|
|
175
|
+
return;
|
|
176
|
+
if (!replyStalled({ now, progressAt: this.progressAt, runningTools: tools.map((tool) => tool.at) }))
|
|
157
177
|
return;
|
|
158
178
|
this.stallRequested = run.runId;
|
|
159
179
|
this.hooks.telemetry('stall');
|
|
@@ -242,6 +262,25 @@ export class TerminalController {
|
|
|
242
262
|
if (this.disposed || this.activeRun?.runId !== parentRunId || run.relation !== 'subagent')
|
|
243
263
|
return;
|
|
244
264
|
this.subagents.observe(parentRunId, monitorRun(run, 'subagent', profile, runtime), event);
|
|
265
|
+
this.trackChild(run, event);
|
|
266
|
+
}
|
|
267
|
+
/** Folds a child's event into its own stall clock; a heartbeat only proves the process lives. */
|
|
268
|
+
trackChild(run, event) {
|
|
269
|
+
const status = event.payload.status;
|
|
270
|
+
const settled = event.type === 'completed' || event.type === 'failed' || (event.type === 'status.changed' && typeof status === 'string' && !isActiveStatus(status));
|
|
271
|
+
if (settled || !isActiveStatus(run.status)) {
|
|
272
|
+
this.children.delete(run.id);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const child = this.children.get(run.id) ?? { progressAt: Date.now(), tools: new Map(), stopRequestedAt: null };
|
|
276
|
+
if (event.type !== 'heartbeat')
|
|
277
|
+
child.progressAt = Date.now();
|
|
278
|
+
const toolCallId = String(event.payload.toolCallId ?? '');
|
|
279
|
+
if (event.type === 'tool.started' && toolCallId)
|
|
280
|
+
child.tools.set(toolCallId, Date.now());
|
|
281
|
+
else if (event.type === 'tool.completed')
|
|
282
|
+
child.tools.delete(toolCallId);
|
|
283
|
+
this.children.set(run.id, child);
|
|
245
284
|
}
|
|
246
285
|
/** An event of the tracked parent run itself, shown as the `main` row; it outlives settlement. */
|
|
247
286
|
observeMain(run, event, runtime = {}) {
|
|
@@ -514,7 +553,7 @@ export class TerminalController {
|
|
|
514
553
|
run.emit('message.completed', { messageId: data.id ?? randomUUID(), role: data.role ?? 'assistant', text: String(data.text) });
|
|
515
554
|
else if (event === 'tool') {
|
|
516
555
|
if (data.phase === 'start')
|
|
517
|
-
this.runningTools.set(String(data.id), Date.now());
|
|
556
|
+
this.runningTools.set(String(data.id), { name: String(data.name ?? ''), at: Date.now() });
|
|
518
557
|
else
|
|
519
558
|
this.runningTools.delete(String(data.id));
|
|
520
559
|
run.emit(data.phase === 'start' ? 'tool.started' : 'tool.completed', {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sessionEntryToContextMessages } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { parseProposedPlan } from "../tui/planMode.js";
|
|
3
|
+
import { toolDisplayName } from "../toolName.js";
|
|
3
4
|
import { ansiLabel, ansiPlain } from "./ansi.js";
|
|
4
5
|
const MAX_TOOL_OUTPUT = 12_000;
|
|
5
6
|
const OUTPUT_CLIP_MARKER = '\n… output clipped …\n';
|
|
@@ -447,7 +448,8 @@ export class TranscriptModel {
|
|
|
447
448
|
const text = contentText(message.content);
|
|
448
449
|
if (!text.trim())
|
|
449
450
|
return items;
|
|
450
|
-
|
|
451
|
+
const customType = stringField(message, 'customType') ?? '';
|
|
452
|
+
items.push({ kind: 'custom', id: `m${index}`, label: toolDisplayName(customType) || 'Extension', text });
|
|
451
453
|
return items;
|
|
452
454
|
}
|
|
453
455
|
if (role === 'compactionSummary') {
|
|
@@ -16,7 +16,7 @@ import { goalStopOutcome, handoffFields, stopCheckpointReason, stopSummary } fro
|
|
|
16
16
|
import { launchSwitchOff, sessionCheckModel } from "../sessions/completionCheck.js";
|
|
17
17
|
import { GOAL_STATE_ENTRY, GOAL_TOOLS, goalTools } from "../sessions/goal.js";
|
|
18
18
|
import { GOAL_EXTENSION, goalLoop, goalNotice } from "../sessions/goalLoop.js";
|
|
19
|
-
import { createSliceState, DEFAULT_SLICE_POLICY, parseWorkSlice, sliceContinuation, workSliceGuard, WORK_SLICE_EXTENSION } from "../sessions/workSlice.js";
|
|
19
|
+
import { activeToolBeat, createSliceState, DEFAULT_SLICE_POLICY, parseWorkSlice, sliceContinuation, workSliceGuard, WORK_SLICE_EXTENSION } from "../sessions/workSlice.js";
|
|
20
20
|
import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
|
|
21
21
|
import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
|
|
22
22
|
import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
|
|
@@ -76,6 +76,7 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
76
76
|
let requestStartedAt = null;
|
|
77
77
|
let pendingRefusals = [];
|
|
78
78
|
const toolStartedAt = new Map();
|
|
79
|
+
const toolsInFlight = new Map();
|
|
79
80
|
const fileTools = new Map();
|
|
80
81
|
const writeAuthorizations = new Map();
|
|
81
82
|
let workspaceRoot = null;
|
|
@@ -298,6 +299,7 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
298
299
|
// A live reply reports progress so a person and the watchdog can tell a long turn from a wedged
|
|
299
300
|
// one; a turn that stops producing anything is settled with reason `stalled` (INV-RUN-14).
|
|
300
301
|
turnStartedAt = Date.now();
|
|
302
|
+
toolsInFlight.clear();
|
|
301
303
|
if (heartbeat)
|
|
302
304
|
clearInterval(heartbeat);
|
|
303
305
|
heartbeat = setInterval(() => {
|
|
@@ -307,7 +309,8 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
307
309
|
lastTool: sliceState.lastTool,
|
|
308
310
|
compactions: sliceState.compactions,
|
|
309
311
|
slice: sliceState.slice,
|
|
310
|
-
maxToolCalls: slicePolicy.enabled ? slicePolicy.maxToolCalls : 0
|
|
312
|
+
maxToolCalls: slicePolicy.enabled ? slicePolicy.maxToolCalls : 0,
|
|
313
|
+
...activeToolBeat(toolsInFlight)
|
|
311
314
|
}, 10_000).catch(() => { });
|
|
312
315
|
}, 30_000);
|
|
313
316
|
heartbeat.unref?.();
|
|
@@ -735,11 +738,13 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
735
738
|
if (['edit', 'write'].includes(event.toolName) && typeof event.args.path === 'string')
|
|
736
739
|
fileTools.set(event.toolCallId, event.args.path);
|
|
737
740
|
toolStartedAt.set(event.toolCallId, Date.now());
|
|
741
|
+
toolsInFlight.set(event.toolCallId, { name: event.toolName, at: Date.now() });
|
|
738
742
|
await terminalCall('tool', { phase: 'start', id: event.toolCallId, name: event.toolName, summary: JSON.stringify(event.args).slice(0, 200) });
|
|
739
743
|
});
|
|
740
744
|
pi.on('tool_execution_end', async (event) => {
|
|
741
745
|
const started = toolStartedAt.get(event.toolCallId);
|
|
742
746
|
toolStartedAt.delete(event.toolCallId);
|
|
747
|
+
toolsInFlight.delete(event.toolCallId);
|
|
743
748
|
const resultChars = resultSize(event.result);
|
|
744
749
|
await terminalCall('tool', { phase: 'end', id: event.toolCallId, name: event.toolName, ok: !event.isError, ...(started === undefined ? {} : { durationMs: Date.now() - started }), ...(resultChars === null ? {} : { resultChars }) });
|
|
745
750
|
const path = fileTools.get(event.toolCallId);
|
|
@@ -11,3 +11,21 @@ export interface ReplyProgress {
|
|
|
11
11
|
}
|
|
12
12
|
/** Whether a live reply has gone too long without progress and should be ended. The oldest running tool counts. */
|
|
13
13
|
export declare function replyStalled({ now, progressAt, runningTools }: ReplyProgress): boolean;
|
|
14
|
+
/** The tools that block a reply until its children settle. */
|
|
15
|
+
export declare const CHILD_WAIT_TOOLS: ReadonlySet<string>;
|
|
16
|
+
/** How long a child may stay unsettled after it was told to stop before its parent's own watch resumes. */
|
|
17
|
+
export declare const CHILD_STOP_GRACE_MS: number;
|
|
18
|
+
/** One subagent the terminal reply is waiting on. */
|
|
19
|
+
export interface ChildProgress {
|
|
20
|
+
progressAt: number;
|
|
21
|
+
runningTools: readonly number[];
|
|
22
|
+
/** When the stall watch ended this child, if it did. */
|
|
23
|
+
stopRequestedAt: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** Whether a child has gone quiet for the same windows as a reply, and was not yet told to stop. */
|
|
26
|
+
export declare function childStalled(now: number, child: ChildProgress): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the parent's own watch is paused: it is only waiting for children, and at least one still
|
|
29
|
+
* answers to its own watch. A child that ignored the stop for the grace period hands the clock back.
|
|
30
|
+
*/
|
|
31
|
+
export declare function waitingOnChildren(now: number, runningToolNames: readonly string[], children: readonly ChildProgress[]): boolean;
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// told instead of watching a run that never settles. This is the turn-level rule; the run-store
|
|
3
3
|
// sweep (`reconcileStaleRuns`) settles a record whose owner stopped reporting, and both use the
|
|
4
4
|
// same reason, `stalled` (INV-RUN-14).
|
|
5
|
+
//
|
|
6
|
+
// A reply that waits for its subagents is not silent: each child is watched on its own clock and ended
|
|
7
|
+
// with the same reason when it goes quiet, so the wait returns and the parent carries on by itself.
|
|
5
8
|
/** Silence with no tool running: the model request never came back. */
|
|
6
9
|
export const REPLY_STALL_MS = 15 * 60_000;
|
|
7
10
|
/** One tool may run this long; past it the reply is wedged, not busy. */
|
|
@@ -14,3 +17,20 @@ export function replyStalled({ now, progressAt, runningTools }) {
|
|
|
14
17
|
? now - Math.min(...runningTools) >= TOOL_STALL_MS
|
|
15
18
|
: now - progressAt >= REPLY_STALL_MS;
|
|
16
19
|
}
|
|
20
|
+
/** The tools that block a reply until its children settle. */
|
|
21
|
+
export const CHILD_WAIT_TOOLS = new Set(['wait_subagents', 'spawn_subagent']);
|
|
22
|
+
/** How long a child may stay unsettled after it was told to stop before its parent's own watch resumes. */
|
|
23
|
+
export const CHILD_STOP_GRACE_MS = 5 * 60_000;
|
|
24
|
+
/** Whether a child has gone quiet for the same windows as a reply, and was not yet told to stop. */
|
|
25
|
+
export function childStalled(now, child) {
|
|
26
|
+
return child.stopRequestedAt === null && replyStalled({ now, progressAt: child.progressAt, runningTools: child.runningTools });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether the parent's own watch is paused: it is only waiting for children, and at least one still
|
|
30
|
+
* answers to its own watch. A child that ignored the stop for the grace period hands the clock back.
|
|
31
|
+
*/
|
|
32
|
+
export function waitingOnChildren(now, runningToolNames, children) {
|
|
33
|
+
if (!runningToolNames.length || !runningToolNames.every((name) => CHILD_WAIT_TOOLS.has(name)))
|
|
34
|
+
return false;
|
|
35
|
+
return children.some((child) => child.stopRequestedAt === null || now - child.stopRequestedAt < CHILD_STOP_GRACE_MS);
|
|
36
|
+
}
|
|
@@ -28,6 +28,8 @@ export interface TerminalSubagentRun {
|
|
|
28
28
|
export type TerminalSubagentUsage = Partial<Pick<UsagePayload, 'provider' | 'model' | 'inputTokens' | 'outputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens' | 'costUsd' | 'costSource' | 'requests' | 'contextTokens' | 'contextWindow'>>;
|
|
29
29
|
export interface TerminalSubagentActivity extends TerminalSubagentRun {
|
|
30
30
|
activity: string;
|
|
31
|
+
/** When the run last did anything but heartbeat; how long it has been quiet is what a stall is. */
|
|
32
|
+
lastEventAt?: string;
|
|
31
33
|
/** Cumulative run usage only; provider reasoning summaries and request details stay private. */
|
|
32
34
|
usage: TerminalSubagentUsage | null;
|
|
33
35
|
/** How far the child's own todo is: counts only, never the items. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { usagePayloadSchema } from "../sessions/contracts.js";
|
|
2
|
+
import { toolDisplayName } from "./toolName.js";
|
|
2
3
|
/** The main agent's row: the parent run itself, shown by name rather than by a profile. */
|
|
3
4
|
export const MAIN_PROFILE = { name: 'main', color: 'main' };
|
|
4
5
|
export const ACTIVE_SUBAGENT_STATUSES = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
|
|
@@ -92,12 +93,14 @@ export class SubagentMonitor {
|
|
|
92
93
|
row.startedAt = run.startedAt;
|
|
93
94
|
row.endedAt = run.endedAt;
|
|
94
95
|
const payload = event ? payloadOf(event) : {};
|
|
96
|
+
if (event && event.type !== 'heartbeat')
|
|
97
|
+
row.lastEventAt = event.timestamp;
|
|
95
98
|
if (!event) {
|
|
96
99
|
// A seeded row keeps what it already folded.
|
|
97
100
|
}
|
|
98
101
|
else if (event.type === 'tool.started') {
|
|
99
102
|
const toolCallId = stringOf(payload.toolCallId);
|
|
100
|
-
const name = stringOf(payload.name) ?? 'Tool';
|
|
103
|
+
const name = toolDisplayName(stringOf(payload.name) ?? '') || 'Tool';
|
|
101
104
|
if (toolCallId)
|
|
102
105
|
row.tools.set(toolCallId, { sequence: event.sequence, text: name });
|
|
103
106
|
}
|
|
@@ -1259,15 +1259,46 @@ export class Terminals {
|
|
|
1259
1259
|
const world = this.options.world;
|
|
1260
1260
|
if (!world)
|
|
1261
1261
|
throw invalid('Browser settings are unavailable in this host.');
|
|
1262
|
-
|
|
1262
|
+
const config = world.browserConfig();
|
|
1263
|
+
return { backend: config.backend, allowPrivate: config.allowPrivate };
|
|
1263
1264
|
}
|
|
1264
1265
|
if (call === 'terminal.browser.configure') {
|
|
1265
1266
|
const world = this.options.world;
|
|
1266
1267
|
if (!world)
|
|
1267
1268
|
throw invalid('Browser settings are unavailable in this host.');
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1269
|
+
const change = {};
|
|
1270
|
+
if ('allowPrivate' in params) {
|
|
1271
|
+
if (typeof params.allowPrivate !== 'boolean')
|
|
1272
|
+
throw invalid('allowPrivate must be a boolean.');
|
|
1273
|
+
change.allowPrivate = params.allowPrivate;
|
|
1274
|
+
}
|
|
1275
|
+
if ('backend' in params) {
|
|
1276
|
+
if (params.backend !== 'isolated' && params.backend !== 'brave')
|
|
1277
|
+
throw invalid('backend must be isolated or brave.');
|
|
1278
|
+
change.backend = params.backend;
|
|
1279
|
+
}
|
|
1280
|
+
if (!Object.keys(change).length)
|
|
1281
|
+
throw invalid('Set a browser backend or allowPrivate value.');
|
|
1282
|
+
const config = await world.configureBrowser(change);
|
|
1283
|
+
return { backend: config.backend, allowPrivate: config.allowPrivate };
|
|
1284
|
+
}
|
|
1285
|
+
if (call === 'terminal.browser.extension.status') {
|
|
1286
|
+
const world = this.options.world;
|
|
1287
|
+
if (!world)
|
|
1288
|
+
throw invalid('Browser settings are unavailable in this host.');
|
|
1289
|
+
return world.browserExtensionStatus();
|
|
1290
|
+
}
|
|
1291
|
+
if (call === 'terminal.browser.extension.start') {
|
|
1292
|
+
const world = this.options.world;
|
|
1293
|
+
if (!world)
|
|
1294
|
+
throw invalid('Browser settings are unavailable in this host.');
|
|
1295
|
+
return world.browserExtensionStart();
|
|
1296
|
+
}
|
|
1297
|
+
if (call === 'terminal.browser.extension.stop') {
|
|
1298
|
+
const world = this.options.world;
|
|
1299
|
+
if (!world)
|
|
1300
|
+
throw invalid('Browser settings are unavailable in this host.');
|
|
1301
|
+
return world.browserExtensionStop();
|
|
1271
1302
|
}
|
|
1272
1303
|
if (call === 'terminal.browser.snapshot')
|
|
1273
1304
|
return browser.snapshot(params.refresh === true);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool or message id as a person reads it: `todo_write` becomes `TodoWrite`,
|
|
3
|
+
* `computer-screenshot` becomes `ComputerScreenshot`. Only the first letter of each part is
|
|
4
|
+
* raised, so a name that already carries capitals keeps them.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toolDisplayName(name: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool or message id as a person reads it: `todo_write` becomes `TodoWrite`,
|
|
3
|
+
* `computer-screenshot` becomes `ComputerScreenshot`. Only the first letter of each part is
|
|
4
|
+
* raised, so a name that already carries capitals keeps them.
|
|
5
|
+
*/
|
|
6
|
+
export function toolDisplayName(name) {
|
|
7
|
+
return name
|
|
8
|
+
.split(/[^A-Za-z0-9]+/u)
|
|
9
|
+
.filter(part => part !== '')
|
|
10
|
+
.map(part => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
11
|
+
.join('');
|
|
12
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { TerminalPage } from "./page.js";
|
|
|
2
2
|
import type { TerminalCommandHost } from "./commands.js";
|
|
3
3
|
import { type AnnotateStart } from "./pages/annotatePage.js";
|
|
4
4
|
import { type ContextTab } from "./pages/contextPage.js";
|
|
5
|
-
export type CommandPageId = 'doctor' | 'accounts' | 'permissions' | 'plugins' | 'hub' | 'components' | 'settings' | 'trajectory' | 'log' | 'harness' | 'skills' | 'connections' | 'tools' | 'worktree' | 'notes' | 'annotate' | 'subagents' | 'agents' | 'usage' | 'context' | 'remote-control';
|
|
5
|
+
export type CommandPageId = 'doctor' | 'accounts' | 'permissions' | 'plugins' | 'hub' | 'components' | 'settings' | 'trajectory' | 'log' | 'harness' | 'skills' | 'connections' | 'tools' | 'worktree' | 'notes' | 'annotate' | 'subagents' | 'agents' | 'usage' | 'context' | 'remote-control' | 'follow-up';
|
|
6
6
|
/** Construct one independent full-screen page for a slash command. */
|
|
7
7
|
export declare function createCommandPage(host: TerminalCommandHost, id: CommandPageId, options?: {
|
|
8
8
|
runId?: string;
|
|
@@ -14,12 +14,15 @@ import { AgentsPage } from "./pages/agentsPage.js";
|
|
|
14
14
|
import { UsagePage } from "./pages/usagePage.js";
|
|
15
15
|
import { ContextPage } from "./pages/contextPage.js";
|
|
16
16
|
import { RemoteControlPage } from "./pages/remoteControlPage.js";
|
|
17
|
+
import { FollowUpQueuePage } from "./pages/followUpQueuePage.js";
|
|
17
18
|
import { createSettingsModel } from "./settingsModel.js";
|
|
18
19
|
/** Construct one independent full-screen page for a slash command. */
|
|
19
20
|
export function createCommandPage(host, id, options = {}) {
|
|
20
21
|
const theme = () => host.ui.theme;
|
|
21
22
|
const notify = (message, type) => host.ui.notify(message, type === 'error' || type === 'warning' ? type : 'info');
|
|
22
23
|
switch (id) {
|
|
24
|
+
case 'follow-up':
|
|
25
|
+
return new FollowUpQueuePage({ editor: host.followUpQueue(), theme, changed: host.changed, close: options.close ?? (() => { }) });
|
|
23
26
|
case 'remote-control':
|
|
24
27
|
return new RemoteControlPage({ theme, call: host.control, changed: host.changed, copy: host.copyText, notify });
|
|
25
28
|
case 'context':
|
|
@@ -7,8 +7,22 @@ import type { TerminalSubagentActivity } from "../subagentMonitor.js";
|
|
|
7
7
|
import { type TerminalCommandCompletion, type TerminalCommandHelpGroup } from "../commandHelp.js";
|
|
8
8
|
export type { TerminalCommandCompletion } from "../commandHelp.js";
|
|
9
9
|
export { parseAnnotate } from "./pluginCommands.js";
|
|
10
|
+
export type FollowUpQueueEntry = {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: 'steering' | 'followUp';
|
|
13
|
+
text: string;
|
|
14
|
+
hasImages: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type FollowUpQueueEditor = {
|
|
17
|
+
entries(): readonly FollowUpQueueEntry[];
|
|
18
|
+
edit(id: string): Promise<void>;
|
|
19
|
+
sendNext(): Promise<void>;
|
|
20
|
+
remove(id: string): void;
|
|
21
|
+
close(): void;
|
|
22
|
+
};
|
|
10
23
|
export interface TerminalCommandHost {
|
|
11
24
|
session(): AgentSession;
|
|
25
|
+
followUpQueue(): FollowUpQueueEditor;
|
|
12
26
|
/** Navigation is scope checked by the Harness runtime adapter. */
|
|
13
27
|
runtime(): Pick<TerminalRuntime, 'newSession' | 'switchSession' | 'fork' | 'importFromJsonl' | 'reload' | 'navigateTree' | 'installationDeviceId' | 'permissions' | 'setFullPermission' | 'setGlobalTool' | 'snapshot' | 'interrupt'>;
|
|
14
28
|
/** The protected `accounts.*`, `plugins.*` and `hub.*` calls of this session, made with its control key. */
|
|
@@ -356,13 +356,16 @@ export function createTerminalCommands(host) {
|
|
|
356
356
|
case 'worktree':
|
|
357
357
|
await openPage('worktree');
|
|
358
358
|
break;
|
|
359
|
+
case 'follow-up':
|
|
360
|
+
await openPage('follow-up');
|
|
361
|
+
break;
|
|
359
362
|
case 'trajectory':
|
|
360
363
|
await openPage('trajectory', { runId: argument });
|
|
361
364
|
break;
|
|
362
365
|
case 'todo': {
|
|
363
366
|
const items = await host.todo();
|
|
364
367
|
if (!items.length)
|
|
365
|
-
notify('No todo list in this reply yet. The agent writes one with
|
|
368
|
+
notify('No todo list in this reply yet. The agent writes one with TodoWrite for multi-step work.');
|
|
366
369
|
else
|
|
367
370
|
host.showTodo(items);
|
|
368
371
|
break;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { TabInfo } from "../chrome.js";
|
|
3
|
+
import type { FollowUpQueueEditor } from "../commands.js";
|
|
4
|
+
import type { TerminalMouseInput } from "../mouse.js";
|
|
5
|
+
import type { TerminalPage } from "../page.js";
|
|
6
|
+
export declare class FollowUpQueuePage implements TerminalPage {
|
|
7
|
+
private readonly options;
|
|
8
|
+
presentation: "centered";
|
|
9
|
+
private selected;
|
|
10
|
+
private disposed;
|
|
11
|
+
private closed;
|
|
12
|
+
constructor(options: {
|
|
13
|
+
editor: FollowUpQueueEditor;
|
|
14
|
+
theme(): Theme;
|
|
15
|
+
changed(): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
});
|
|
18
|
+
get focused(): boolean;
|
|
19
|
+
set focused(_value: boolean);
|
|
20
|
+
tabs(): readonly TabInfo[];
|
|
21
|
+
activeTab(): string;
|
|
22
|
+
hint(): string;
|
|
23
|
+
activate(): void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
private closeEditor;
|
|
26
|
+
private entries;
|
|
27
|
+
render(_width: number, _rows: number): string[];
|
|
28
|
+
private act;
|
|
29
|
+
handleInput(data: string): boolean;
|
|
30
|
+
handleMouse(event: TerminalMouseInput): {
|
|
31
|
+
handled: boolean;
|
|
32
|
+
render: boolean;
|
|
33
|
+
} | undefined;
|
|
34
|
+
}
|