@xingular/harness 1.2.0 → 1.3.1-unstable.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/CHANGELOG.md +35 -0
- package/README.md +10 -5
- package/dist/cli.js +5560 -5056
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +4 -1
- package/dist/sdk/agents/profiles.js +17 -6
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/plugins/builtin.js +3 -3
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +2 -2
- package/dist/sdk/prompt/compile.d.ts +2 -2
- package/dist/sdk/prompt/compile.js +14 -16
- package/dist/sdk/server/methods.js +4 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.js +29 -11
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +194 -54
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +28 -14
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +7 -0
- package/dist/sdk/terminal/terminals.js +86 -14
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.js +14 -4
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +8 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/terminal-opentui.js +1416 -348
- package/dist/terminal-pi.js +315 -126
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +65 -34
- package/docs/architecture/04-prompt.md +8 -6
- package/docs/architecture/06-plugins-and-capabilities.md +10 -6
- package/docs/architecture/07-tools-and-gate.md +31 -5
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +16 -9
- package/docs/architecture/11-terminal.md +51 -26
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +18 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/README.md +10 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +9 -9
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/README.md +2 -2
- package/docs/terminal-tui.md +85 -46
- package/package.json +1 -1
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
|
@@ -8,14 +8,15 @@ export const WORK_SLICE_EXTENSION = 'work-slice';
|
|
|
8
8
|
/** The foundation defaults: enough for one meaningful step of work, short enough to report. */
|
|
9
9
|
export const DEFAULT_SLICE_POLICY = {
|
|
10
10
|
enabled: true,
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
11
|
+
// A chapter of a task, not an interruption every few edits: roughly 80 minutes of ordinary work
|
|
12
|
+
// (about 15 s a call), with 60 minutes of wall clock as the ceiling, whichever comes first. A task
|
|
13
|
+
// still gets eight slices before it returns to the person.
|
|
14
|
+
maxToolCalls: 320,
|
|
15
|
+
maxMinutes: 60,
|
|
16
|
+
// No token cap: the tool calls and the clock are the bound. Counting new input, cache writes and
|
|
17
|
+
// output made a few large reads end a reply before useful work (see the "Confirm plan model
|
|
18
|
+
// support" thread), so a cap is available to whoever asks for one and off unless they do.
|
|
19
|
+
maxNewTokens: 0,
|
|
19
20
|
maxSlices: 8,
|
|
20
21
|
continue: true
|
|
21
22
|
};
|
|
@@ -100,7 +101,7 @@ export function sliceLimitReached(policy, state, now = Date.now()) {
|
|
|
100
101
|
const HANDOFF_TOOLS = new Set(['checkpoint_write', 'session_name']);
|
|
101
102
|
/** Tools whose repetition is how waiting works, not a loop. */
|
|
102
103
|
const POLLING_TOOLS = new Set(['wait_subagents', 'subagents.list', 'subagents.status']);
|
|
103
|
-
/** Commands that read as a check
|
|
104
|
+
/** Commands that read as a check for the repeated-failure rule. */
|
|
104
105
|
export function looksLikeCheck(command) {
|
|
105
106
|
return /\b(test|tests|typecheck|tsc|vitest|jest|cargo|check:|prove|lint|ci|build)\b/u.test(command);
|
|
106
107
|
}
|
|
@@ -122,51 +123,78 @@ function canonical(value, depth = 0) {
|
|
|
122
123
|
/**
|
|
123
124
|
* A call's identity: its name and every argument, hashed so the key stays small even when an
|
|
124
125
|
* argument is a whole file. A changed argument is a different call; only an identical one repeats.
|
|
125
|
-
* Reading a fixed handful of fields made every `
|
|
126
|
+
* Reading a fixed handful of fields made every `todo_write` look identical, so a reply that
|
|
126
127
|
* reported its progress was told it was repeating work.
|
|
127
128
|
*/
|
|
128
129
|
const signature = (name, input) => `${name}\u0000${createHash('sha256').update(canonical(input ?? null)).digest('hex')}`;
|
|
129
130
|
const clip = (value, limit) => (value.length <= limit ? value : `${value.slice(0, limit - 1)}…`);
|
|
130
|
-
function handoffRequest(limit, state) {
|
|
131
|
-
|
|
131
|
+
function handoffRequest(limit, state, checkpointAvailable = true) {
|
|
132
|
+
const handoff = checkpointAvailable
|
|
133
|
+
? 'Call checkpoint_write now with what is done, the single next step, anything blocking, the files you touched and the checks that matter. Then tell the user in a few lines where you stopped.'
|
|
134
|
+
: 'Stop using tools and return a concise partial research report with findings, blockers and the next useful research step. The Harness records an evidence-derived checkpoint.';
|
|
135
|
+
return clip(`This reply reached its work-slice limit (${limit}; step ${state.steps}). ${handoff} Do not call other tools.`, 600);
|
|
132
136
|
}
|
|
133
|
-
function stalledNote(reason) {
|
|
134
|
-
|
|
137
|
+
function stalledNote(reason, checkpointAvailable = true) {
|
|
138
|
+
const handoff = checkpointAvailable
|
|
139
|
+
? 'Choose a different approach, read what the earlier attempt returned, or call checkpoint_write with the blocker so the next slice can decide.'
|
|
140
|
+
: 'Do not use more tools. Return concise findings and blockers in your final response; the Harness records an evidence-derived checkpoint.';
|
|
141
|
+
return clip(`You are repeating work: ${reason}. Do not repeat it. ${handoff}`, 600);
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
|
-
* Bounds one reply and names repeated work.
|
|
138
|
-
*
|
|
144
|
+
* Bounds one reply and names repeated work. Refusals request a checkpoint when available; without
|
|
145
|
+
* one, they ask for a partial report and prohibit further tools. A repeated call or failing command
|
|
146
|
+
* is named once, then ends the reply.
|
|
139
147
|
*/
|
|
140
148
|
export function workSliceGuard(options) {
|
|
141
149
|
const { policy, state } = options;
|
|
142
150
|
const calls = new Map();
|
|
151
|
+
const checkCalls = new Set();
|
|
143
152
|
const edits = new Map();
|
|
144
153
|
const failures = new Map();
|
|
145
|
-
let stepsSinceProgress = 0;
|
|
146
154
|
let nudged = false;
|
|
155
|
+
const checkpointAvailable = () => typeof options.checkpointAvailable === 'function'
|
|
156
|
+
? options.checkpointAvailable()
|
|
157
|
+
: options.checkpointAvailable !== false;
|
|
147
158
|
/** Repetition is judged inside one reply; the next reply starts from a clean count. */
|
|
148
159
|
const resetRepetition = () => {
|
|
149
160
|
calls.clear();
|
|
161
|
+
checkCalls.clear();
|
|
150
162
|
edits.clear();
|
|
151
163
|
failures.clear();
|
|
152
|
-
stepsSinceProgress = 0;
|
|
153
164
|
nudged = false;
|
|
154
165
|
};
|
|
155
166
|
const end = (kind, limit) => {
|
|
156
167
|
state.endedBy = kind;
|
|
157
168
|
state.endedByLimit = limit;
|
|
158
169
|
options.onEnd?.(kind, limit);
|
|
170
|
+
const canCheckpoint = checkpointAvailable();
|
|
159
171
|
const reason = kind === 'stuck'
|
|
160
|
-
? stalledNote(limit)
|
|
172
|
+
? stalledNote(limit, canCheckpoint)
|
|
161
173
|
: kind === 'compaction-thrash'
|
|
162
|
-
? handoffRequest('three context compactions in one reply', state)
|
|
163
|
-
: handoffRequest(limit, state);
|
|
174
|
+
? handoffRequest('three context compactions in one reply', state, canCheckpoint)
|
|
175
|
+
: handoffRequest(limit, state, canCheckpoint);
|
|
164
176
|
return { block: true, terminate: true, reason };
|
|
165
177
|
};
|
|
178
|
+
const nudge = (reason) => {
|
|
179
|
+
if (nudged)
|
|
180
|
+
return end('stuck', reason);
|
|
181
|
+
nudged = true;
|
|
182
|
+
const canCheckpoint = checkpointAvailable();
|
|
183
|
+
if (!canCheckpoint) {
|
|
184
|
+
state.endedBy = 'stuck';
|
|
185
|
+
state.endedByLimit = reason;
|
|
186
|
+
}
|
|
187
|
+
return { block: true, reason: stalledNote(reason, canCheckpoint) };
|
|
188
|
+
};
|
|
166
189
|
const repeated = (name, input) => {
|
|
167
190
|
if (POLLING_TOOLS.has(name))
|
|
168
191
|
return null;
|
|
169
192
|
const key = signature(name, input);
|
|
193
|
+
if (name === 'bash') {
|
|
194
|
+
const command = input && typeof input === 'object' ? String(input.command ?? '') : '';
|
|
195
|
+
if (command && looksLikeCheck(command))
|
|
196
|
+
checkCalls.add(key);
|
|
197
|
+
}
|
|
170
198
|
const count = (calls.get(key) ?? 0) + 1;
|
|
171
199
|
calls.set(key, count);
|
|
172
200
|
if (count < 3)
|
|
@@ -197,9 +225,19 @@ export function workSliceGuard(options) {
|
|
|
197
225
|
pi.on('tool_result', (event) => {
|
|
198
226
|
if (event.toolName === 'checkpoint_write' && !event.isError) {
|
|
199
227
|
state.checkpointWritten = true;
|
|
200
|
-
stepsSinceProgress = 0;
|
|
201
228
|
nudged = false;
|
|
202
229
|
}
|
|
230
|
+
if ((event.toolName === 'edit' || event.toolName === 'write') && event.isError === false) {
|
|
231
|
+
const path = event.input && typeof event.input === 'object' ? String(event.input.path ?? '') : '';
|
|
232
|
+
if (path) {
|
|
233
|
+
edits.set(path, (edits.get(path) ?? 0) + 1);
|
|
234
|
+
failures.clear();
|
|
235
|
+
for (const key of checkCalls)
|
|
236
|
+
calls.delete(key);
|
|
237
|
+
checkCalls.clear();
|
|
238
|
+
nudged = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
203
241
|
if (event.toolName !== 'bash')
|
|
204
242
|
return;
|
|
205
243
|
const command = event.input && typeof event.input === 'object' ? String(event.input.command ?? '') : '';
|
|
@@ -213,17 +251,21 @@ export function workSliceGuard(options) {
|
|
|
213
251
|
failures.delete(command);
|
|
214
252
|
edits.clear();
|
|
215
253
|
calls.clear();
|
|
216
|
-
|
|
254
|
+
checkCalls.clear();
|
|
217
255
|
nudged = false;
|
|
218
256
|
});
|
|
219
257
|
pi.on('tool_call', (event) => {
|
|
220
258
|
const name = event.toolName;
|
|
221
259
|
state.steps += 1;
|
|
222
260
|
state.lastTool = name;
|
|
223
|
-
const
|
|
224
|
-
|
|
261
|
+
const canCheckpoint = checkpointAvailable();
|
|
262
|
+
const handoff = canCheckpoint && HANDOFF_TOOLS.has(name);
|
|
263
|
+
// A handoff is callable only when checkpoint_write is active; after the slice ended it is the only one.
|
|
225
264
|
if (state.endedBy) {
|
|
226
|
-
|
|
265
|
+
const reason = state.checkpointWritten
|
|
266
|
+
? 'This reply is over. Its checkpoint is recorded; nothing else runs.'
|
|
267
|
+
: 'This reply is over. The Harness will record an evidence-derived checkpoint; nothing else runs.';
|
|
268
|
+
return handoff ? undefined : { block: true, terminate: true, reason };
|
|
227
269
|
}
|
|
228
270
|
if (state.compactions >= 3) {
|
|
229
271
|
if (handoff) {
|
|
@@ -240,39 +282,29 @@ export function workSliceGuard(options) {
|
|
|
240
282
|
state.endedBy = 'budget';
|
|
241
283
|
state.endedByLimit = limit;
|
|
242
284
|
state.refusedAt ??= state.steps;
|
|
243
|
-
|
|
244
|
-
return handoff ? undefined : { block: true, reason: handoffRequest(limit, state) };
|
|
285
|
+
return handoff ? undefined : { block: true, reason: handoffRequest(limit, state, canCheckpoint) };
|
|
245
286
|
}
|
|
246
287
|
if (handoff)
|
|
247
288
|
return undefined;
|
|
289
|
+
// Todo updates are bookkeeping, not evidence of progress or a stall. They still count above
|
|
290
|
+
// toward the reply's overall tool-call budget.
|
|
291
|
+
if (name === 'todo_write')
|
|
292
|
+
return undefined;
|
|
248
293
|
const edit = name === 'edit' || name === 'write';
|
|
249
294
|
const path = edit && event.input && typeof event.input === 'object' ? String(event.input.path ?? '') : '';
|
|
250
295
|
if (edit && path) {
|
|
251
|
-
const count =
|
|
252
|
-
edits.set(path, count);
|
|
253
|
-
stepsSinceProgress = 0;
|
|
296
|
+
const count = edits.get(path) ?? 0;
|
|
254
297
|
if (count >= 4) {
|
|
255
298
|
const reason = `${path} was edited ${count} times with no passing check between`;
|
|
256
|
-
|
|
257
|
-
return end('stuck', reason);
|
|
258
|
-
nudged = true;
|
|
259
|
-
return { block: true, reason: stalledNote(reason) };
|
|
299
|
+
return nudge(reason);
|
|
260
300
|
}
|
|
261
301
|
}
|
|
262
|
-
else {
|
|
263
|
-
stepsSinceProgress += 1;
|
|
264
|
-
}
|
|
265
302
|
const repeat = repeated(name, event.input);
|
|
266
303
|
const failure = failing(name, event.input);
|
|
267
|
-
const
|
|
268
|
-
const reason = repeat ?? failure ?? noProgress;
|
|
304
|
+
const reason = repeat ?? failure;
|
|
269
305
|
if (!reason)
|
|
270
306
|
return undefined;
|
|
271
|
-
|
|
272
|
-
return end('stuck', reason);
|
|
273
|
-
nudged = true;
|
|
274
|
-
stepsSinceProgress = 0;
|
|
275
|
-
return { block: true, reason: stalledNote(reason) };
|
|
307
|
+
return nudge(reason);
|
|
276
308
|
});
|
|
277
309
|
};
|
|
278
310
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type RunEvent, type RunRecord } from "../sessions/contracts.js";
|
|
2
2
|
import type { ActiveRun, ApprovalDecision, QuestionAnswer } from "../sessions/registry.js";
|
|
3
3
|
import { type TerminalSubagentActivity, type TerminalSubagentProfile, type TerminalSubagentRun } from "./subagentMonitor.js";
|
|
4
|
+
type MonitorRuntime = Pick<TerminalSubagentRun, 'accountLabel' | 'model' | 'effort'>;
|
|
5
|
+
/** A saved run to show in the monitor after the conversation is reopened. */
|
|
6
|
+
export interface MonitorEntry {
|
|
7
|
+
record: RunRecord;
|
|
8
|
+
events: readonly RunEvent[];
|
|
9
|
+
profile?: TerminalSubagentProfile | null;
|
|
10
|
+
runtime?: MonitorRuntime;
|
|
11
|
+
}
|
|
4
12
|
export interface PolicyReply {
|
|
5
13
|
allowed: boolean;
|
|
6
14
|
reason?: string | null;
|
|
@@ -17,6 +25,8 @@ export interface ControllerHooks {
|
|
|
17
25
|
reservedRunId: string | null;
|
|
18
26
|
nativeTurnId: string;
|
|
19
27
|
text: string;
|
|
28
|
+
model?: string | null;
|
|
29
|
+
effort?: string | null;
|
|
20
30
|
}): ActiveRun;
|
|
21
31
|
/** Whether a native tool call may run. */
|
|
22
32
|
policy(run: ActiveRun, tool: string, args: unknown): Promise<PolicyReply>;
|
|
@@ -30,6 +40,11 @@ export interface ControllerHooks {
|
|
|
30
40
|
allowToolGlobally(target: string): void;
|
|
31
41
|
/** The native conversation identity changed. */
|
|
32
42
|
native(nativeSessionId: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* The conversation opened with no turn running: show its latest parent run's rows again. `owners`
|
|
45
|
+
* are the parent run ids recorded on the native branch, oldest first.
|
|
46
|
+
*/
|
|
47
|
+
hydrate?(owners: string[]): void;
|
|
33
48
|
telemetry(event: string): void;
|
|
34
49
|
interrupt?(runId: string): Promise<void>;
|
|
35
50
|
resolveApproval?(runId: string, requestId: string, decision: ApprovalDecision): void;
|
|
@@ -76,6 +91,10 @@ export declare class TerminalController {
|
|
|
76
91
|
observe(event: RunEvent, descendant?: boolean): void;
|
|
77
92
|
/** A child event that belongs to the active terminal parent run. */
|
|
78
93
|
observeSubagent(parentRunId: string, run: RunRecord, event: RunEvent, profile?: TerminalSubagentProfile | null, runtime?: Pick<TerminalSubagentRun, 'accountLabel' | 'model' | 'effort'>): void;
|
|
94
|
+
/** An event of the tracked parent run itself, shown as the `main` row; it outlives settlement. */
|
|
95
|
+
observeMain(run: RunRecord, event: RunEvent, runtime?: MonitorRuntime): void;
|
|
96
|
+
/** Shows a saved parent run and its descendants, as they ended, until another run begins. */
|
|
97
|
+
restoreMonitor(parent: MonitorEntry, children: readonly MonitorEntry[]): void;
|
|
79
98
|
/** Read-only child activity for the latest parent run, retained until another run begins. */
|
|
80
99
|
subagentSnapshot(): TerminalSubagentActivity[];
|
|
81
100
|
/** The socket grants this interface only to the process that owns terminal input. */
|
|
@@ -84,3 +103,4 @@ export declare class TerminalController {
|
|
|
84
103
|
accept(event: string, data: Record<string, any>): Promise<unknown>;
|
|
85
104
|
dispose(): void;
|
|
86
105
|
}
|
|
106
|
+
export {};
|
|
@@ -6,7 +6,21 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
6
6
|
import { HarnessError } from "../core/errors.js";
|
|
7
7
|
import { runStatusSchema } from "../sessions/contracts.js";
|
|
8
8
|
import { briefTitle } from "../sessions/subagents.js";
|
|
9
|
-
import { SubagentMonitor } from "./subagentMonitor.js";
|
|
9
|
+
import { MAIN_PROFILE, SubagentMonitor } from "./subagentMonitor.js";
|
|
10
|
+
function monitorRun(record, relation, profile, runtime) {
|
|
11
|
+
return {
|
|
12
|
+
id: record.id,
|
|
13
|
+
parentRunId: record.parentRunId,
|
|
14
|
+
relation,
|
|
15
|
+
title: record.title ?? (briefTitle(record.goal) || (relation === 'main' ? 'Main agent' : '')),
|
|
16
|
+
goal: record.goal,
|
|
17
|
+
status: runStatusSchema.parse(record.status),
|
|
18
|
+
startedAt: record.startedAt,
|
|
19
|
+
endedAt: record.endedAt,
|
|
20
|
+
profile: relation === 'main' ? MAIN_PROFILE : profile,
|
|
21
|
+
...runtime
|
|
22
|
+
};
|
|
23
|
+
}
|
|
10
24
|
const READY_TIMEOUT_MS = 90_000;
|
|
11
25
|
const ACK_TIMEOUT_MS = 25_000;
|
|
12
26
|
export class TerminalController {
|
|
@@ -41,7 +55,7 @@ export class TerminalController {
|
|
|
41
55
|
requestId(nativeId) {
|
|
42
56
|
return createHash('sha256').update(`${this.hooks.sessionId}:${this.run?.runId}:${nativeId}`).digest('hex');
|
|
43
57
|
}
|
|
44
|
-
begin(nativeTurnId, text) {
|
|
58
|
+
begin(nativeTurnId, text, launch = {}) {
|
|
45
59
|
if (this.startupError)
|
|
46
60
|
throw new Error(this.startupError);
|
|
47
61
|
this.dirty = false;
|
|
@@ -51,7 +65,7 @@ export class TerminalController {
|
|
|
51
65
|
const delivery = this.delivery;
|
|
52
66
|
if (delivery && text && delivery.text !== text)
|
|
53
67
|
throw new Error('The native prompt did not match the reserved delivery. Inspect the terminal before continuing.');
|
|
54
|
-
const run = this.hooks.begin({ reservedRunId: delivery?.runId ?? null, nativeTurnId, text });
|
|
68
|
+
const run = this.hooks.begin({ reservedRunId: delivery?.runId ?? null, nativeTurnId, text, ...launch });
|
|
55
69
|
this.run = run;
|
|
56
70
|
this.subagentParentRunId = run.runId;
|
|
57
71
|
this.subagents.begin(run.runId);
|
|
@@ -150,18 +164,23 @@ export class TerminalController {
|
|
|
150
164
|
observeSubagent(parentRunId, run, event, profile = null, runtime = {}) {
|
|
151
165
|
if (this.disposed || this.activeRun?.runId !== parentRunId || run.relation !== 'subagent')
|
|
152
166
|
return;
|
|
153
|
-
this.subagents.observe(parentRunId,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
this.subagents.observe(parentRunId, monitorRun(run, 'subagent', profile, runtime), event);
|
|
168
|
+
}
|
|
169
|
+
/** An event of the tracked parent run itself, shown as the `main` row; it outlives settlement. */
|
|
170
|
+
observeMain(run, event, runtime = {}) {
|
|
171
|
+
if (this.disposed || this.subagentParentRunId !== run.id || run.relation)
|
|
172
|
+
return;
|
|
173
|
+
this.subagents.observe(run.id, monitorRun(run, 'main', null, runtime), event);
|
|
174
|
+
}
|
|
175
|
+
/** Shows a saved parent run and its descendants, as they ended, until another run begins. */
|
|
176
|
+
restoreMonitor(parent, children) {
|
|
177
|
+
if (this.disposed || this.activeRun || this.delivery)
|
|
178
|
+
return;
|
|
179
|
+
this.subagentParentRunId = parent.record.id;
|
|
180
|
+
this.subagents.restore(parent.record.id, [
|
|
181
|
+
{ run: monitorRun(parent.record, 'main', null, parent.runtime ?? {}), events: parent.events },
|
|
182
|
+
...children.map((child) => ({ run: monitorRun(child.record, 'subagent', child.profile ?? null, child.runtime ?? {}), events: child.events }))
|
|
183
|
+
]);
|
|
165
184
|
}
|
|
166
185
|
/** Read-only child activity for the latest parent run, retained until another run begins. */
|
|
167
186
|
subagentSnapshot() {
|
|
@@ -316,10 +335,16 @@ export class TerminalController {
|
|
|
316
335
|
this.nativeId = data.nativeSessionId;
|
|
317
336
|
this.hooks.native(data.nativeSessionId);
|
|
318
337
|
}
|
|
338
|
+
const owners = Array.isArray(data.owners) ? data.owners.filter((id) => typeof id === 'string') : [];
|
|
339
|
+
if (!this.activeRun && !this.delivery && this.hooks.hydrate)
|
|
340
|
+
this.hooks.hydrate(owners);
|
|
319
341
|
return {};
|
|
320
342
|
}
|
|
321
343
|
if (event === 'begin') {
|
|
322
|
-
const run = this.begin(String(data.turnId ?? randomUUID()), String(data.text ?? '')
|
|
344
|
+
const run = this.begin(String(data.turnId ?? randomUUID()), String(data.text ?? ''), {
|
|
345
|
+
model: typeof data.model === 'string' && data.model ? data.model : null,
|
|
346
|
+
effort: typeof data.effort === 'string' && data.effort ? data.effort : null
|
|
347
|
+
});
|
|
323
348
|
return { runId: run.runId, workspaceRoot: this.hooks.workspaceRoot(run) };
|
|
324
349
|
}
|
|
325
350
|
if (event === 'shutdown') {
|
|
@@ -377,8 +402,8 @@ export class TerminalController {
|
|
|
377
402
|
}
|
|
378
403
|
if (event === 'usage')
|
|
379
404
|
run.emit('usage.updated', data);
|
|
380
|
-
else if (event === 'checklist')
|
|
381
|
-
run.emit('
|
|
405
|
+
else if (event === 'todo' || event === 'checklist')
|
|
406
|
+
run.emit('todo.updated', { items: data.items });
|
|
382
407
|
else if (event === 'checkpoint')
|
|
383
408
|
run.emit('checkpoint', data);
|
|
384
409
|
else if (event === 'file')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AnsiStyle {
|
|
2
|
+
fg?: string;
|
|
3
|
+
bg?: string;
|
|
4
|
+
bold?: boolean;
|
|
5
|
+
dim?: boolean;
|
|
6
|
+
italic?: boolean;
|
|
7
|
+
underline?: boolean;
|
|
8
|
+
inverse?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface AnsiSpan {
|
|
11
|
+
text: string;
|
|
12
|
+
style: AnsiStyle;
|
|
13
|
+
}
|
|
14
|
+
export interface AnsiLine {
|
|
15
|
+
spans: AnsiSpan[];
|
|
16
|
+
}
|
|
17
|
+
export declare function ansi256Color(index: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* Parses SGR styled lines. Non-style escape sequences, including Pi's cursor markers, are removed;
|
|
20
|
+
* an OSC 8 hyperlink keeps its visible label.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseAnsi(text: string): AnsiLine[];
|
|
23
|
+
export declare function ansiPlain(text: string): string;
|
|
24
|
+
/** A short single-line form for labels and tool summaries. */
|
|
25
|
+
export declare function ansiLabel(text: string, limit?: number): string;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Pi components and trusted extensions render ANSI text, not OpenTUI nodes. This module turns
|
|
2
|
+
// that text into the small style model OpenTUI can draw, and never executes anything in it.
|
|
3
|
+
const BASE_COLORS = [
|
|
4
|
+
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5',
|
|
5
|
+
'#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff'
|
|
6
|
+
];
|
|
7
|
+
const CUBE = [0, 95, 135, 175, 215, 255];
|
|
8
|
+
export function ansi256Color(index) {
|
|
9
|
+
if (!Number.isFinite(index))
|
|
10
|
+
return BASE_COLORS[7];
|
|
11
|
+
const value = Math.max(0, Math.min(255, Math.trunc(index)));
|
|
12
|
+
if (value < 16)
|
|
13
|
+
return BASE_COLORS[value];
|
|
14
|
+
if (value < 232) {
|
|
15
|
+
const n = value - 16;
|
|
16
|
+
const r = CUBE[Math.floor(n / 36) % 6];
|
|
17
|
+
const g = CUBE[Math.floor(n / 6) % 6];
|
|
18
|
+
const b = CUBE[n % 6];
|
|
19
|
+
return hex(r, g, b);
|
|
20
|
+
}
|
|
21
|
+
const grey = 8 + (value - 232) * 10;
|
|
22
|
+
return hex(grey, grey, grey);
|
|
23
|
+
}
|
|
24
|
+
function hex(r, g, b) {
|
|
25
|
+
const part = (value) => Math.max(0, Math.min(255, Math.trunc(value))).toString(16).padStart(2, '0');
|
|
26
|
+
return `#${part(r)}${part(g)}${part(b)}`;
|
|
27
|
+
}
|
|
28
|
+
function defaultStyle() {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
function applySgr(style, params) {
|
|
32
|
+
const next = { ...style };
|
|
33
|
+
const parts = params === '' ? ['0'] : params.split(';');
|
|
34
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
35
|
+
const raw = parts[index];
|
|
36
|
+
const value = raw === '' ? 0 : Number.parseInt(raw, 10);
|
|
37
|
+
if (!Number.isFinite(value))
|
|
38
|
+
continue;
|
|
39
|
+
if (value === 0) {
|
|
40
|
+
for (const key of Object.keys(next))
|
|
41
|
+
delete next[key];
|
|
42
|
+
}
|
|
43
|
+
else if (value === 1)
|
|
44
|
+
next.bold = true;
|
|
45
|
+
else if (value === 2)
|
|
46
|
+
next.dim = true;
|
|
47
|
+
else if (value === 3)
|
|
48
|
+
next.italic = true;
|
|
49
|
+
else if (value === 4)
|
|
50
|
+
next.underline = true;
|
|
51
|
+
else if (value === 7)
|
|
52
|
+
next.inverse = true;
|
|
53
|
+
else if (value === 22) {
|
|
54
|
+
next.bold = false;
|
|
55
|
+
next.dim = false;
|
|
56
|
+
}
|
|
57
|
+
else if (value === 23)
|
|
58
|
+
next.italic = false;
|
|
59
|
+
else if (value === 24)
|
|
60
|
+
next.underline = false;
|
|
61
|
+
else if (value === 27)
|
|
62
|
+
next.inverse = false;
|
|
63
|
+
else if (value >= 30 && value <= 37)
|
|
64
|
+
next.fg = BASE_COLORS[value - 30];
|
|
65
|
+
else if (value >= 90 && value <= 97)
|
|
66
|
+
next.fg = BASE_COLORS[value - 90 + 8];
|
|
67
|
+
else if (value >= 40 && value <= 47)
|
|
68
|
+
next.bg = BASE_COLORS[value - 40];
|
|
69
|
+
else if (value >= 100 && value <= 107)
|
|
70
|
+
next.bg = BASE_COLORS[value - 100 + 8];
|
|
71
|
+
else if (value === 39)
|
|
72
|
+
delete next.fg;
|
|
73
|
+
else if (value === 49)
|
|
74
|
+
delete next.bg;
|
|
75
|
+
else if (value === 38 || value === 48) {
|
|
76
|
+
const target = value === 38 ? 'fg' : 'bg';
|
|
77
|
+
const mode = parts[index + 1];
|
|
78
|
+
if (mode === '5') {
|
|
79
|
+
const color = Number.parseInt(parts[index + 2] ?? '', 10);
|
|
80
|
+
if (Number.isFinite(color))
|
|
81
|
+
next[target] = ansi256Color(color);
|
|
82
|
+
index += 2;
|
|
83
|
+
}
|
|
84
|
+
else if (mode === '2') {
|
|
85
|
+
const r = Number.parseInt(parts[index + 2] ?? '', 10);
|
|
86
|
+
const g = Number.parseInt(parts[index + 3] ?? '', 10);
|
|
87
|
+
const b = Number.parseInt(parts[index + 4] ?? '', 10);
|
|
88
|
+
if ([r, g, b].every(Number.isFinite))
|
|
89
|
+
next[target] = hex(r, g, b);
|
|
90
|
+
index += 4;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
/** Consumes a CSI sequence at `start`; returns the index after it. */
|
|
97
|
+
function csiEnd(text, start) {
|
|
98
|
+
let index = start + 2;
|
|
99
|
+
while (index < text.length) {
|
|
100
|
+
const code = text.charCodeAt(index);
|
|
101
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
102
|
+
return index + 1;
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
return text.length;
|
|
106
|
+
}
|
|
107
|
+
/** Consumes an OSC or APC sequence at `start`; returns the index after its terminator. */
|
|
108
|
+
function stringSequenceEnd(text, start) {
|
|
109
|
+
let index = start + 2;
|
|
110
|
+
while (index < text.length) {
|
|
111
|
+
const code = text.charCodeAt(index);
|
|
112
|
+
if (code === 0x07)
|
|
113
|
+
return index + 1;
|
|
114
|
+
if (code === 0x1b && text.charCodeAt(index + 1) === 0x5c)
|
|
115
|
+
return index + 2;
|
|
116
|
+
index += 1;
|
|
117
|
+
}
|
|
118
|
+
return text.length;
|
|
119
|
+
}
|
|
120
|
+
function stripControl(text) {
|
|
121
|
+
return text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parses SGR styled lines. Non-style escape sequences, including Pi's cursor markers, are removed;
|
|
125
|
+
* an OSC 8 hyperlink keeps its visible label.
|
|
126
|
+
*/
|
|
127
|
+
export function parseAnsi(text) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
let spans = [];
|
|
130
|
+
let style = defaultStyle();
|
|
131
|
+
let buffer = '';
|
|
132
|
+
const flush = () => {
|
|
133
|
+
if (!buffer)
|
|
134
|
+
return;
|
|
135
|
+
spans.push({ text: stripControl(buffer), style: { ...style } });
|
|
136
|
+
buffer = '';
|
|
137
|
+
};
|
|
138
|
+
const newline = () => {
|
|
139
|
+
flush();
|
|
140
|
+
lines.push({ spans });
|
|
141
|
+
spans = [];
|
|
142
|
+
};
|
|
143
|
+
for (let index = 0; index < text.length;) {
|
|
144
|
+
const char = text[index];
|
|
145
|
+
if (char === '\n') {
|
|
146
|
+
newline();
|
|
147
|
+
index += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (char === '\r') {
|
|
151
|
+
index += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (char === '\t') {
|
|
155
|
+
buffer += ' ';
|
|
156
|
+
index += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char !== '\x1b') {
|
|
160
|
+
buffer += char;
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const next = text[index + 1];
|
|
165
|
+
if (next === '[') {
|
|
166
|
+
const end = csiEnd(text, index);
|
|
167
|
+
const body = text.slice(index + 2, end);
|
|
168
|
+
if (body.endsWith('m')) {
|
|
169
|
+
flush();
|
|
170
|
+
style = applySgr(style, body.slice(0, -1));
|
|
171
|
+
}
|
|
172
|
+
index = end;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (next === ']' || next === '_' || next === 'P' || next === '^') {
|
|
176
|
+
index = stringSequenceEnd(text, index);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
index += next === undefined ? 1 : 2;
|
|
180
|
+
}
|
|
181
|
+
flush();
|
|
182
|
+
lines.push({ spans });
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
export function ansiPlain(text) {
|
|
186
|
+
return parseAnsi(text).map(line => line.spans.map(span => span.text).join('')).join('\n');
|
|
187
|
+
}
|
|
188
|
+
/** A short single-line form for labels and tool summaries. */
|
|
189
|
+
export function ansiLabel(text, limit = 120) {
|
|
190
|
+
const plain = ansiPlain(text).replace(/\s+/g, ' ').trim();
|
|
191
|
+
return plain.length > limit ? `${plain.slice(0, Math.max(0, limit - 1))}…` : plain;
|
|
192
|
+
}
|