pi-subagents 0.48.0 → 0.49.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 +47 -19
- package/docs/agents.md +1 -0
- package/docs/configuration.md +28 -5
- package/docs/missions.md +4 -2
- package/docs/observability.md +26 -2
- package/package.json +1 -1
- package/src/agents/agents.ts +27 -12
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +20 -0
- package/src/extension/index.ts +18 -8
- package/src/extension/public-execution.ts +30 -4
- package/src/extension/schemas.ts +8 -7
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +3 -1
- package/src/missions/store.ts +8 -3
- package/src/runs/background/active-async-capacity.ts +82 -25
- package/src/runs/background/async-execution.ts +24 -36
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +11 -1
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +96 -4
- package/src/runs/background/subagent-runner.ts +4 -1
- package/src/runs/foreground/prompt-audit.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +219 -20
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +23 -4
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +3 -1
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +46 -7
|
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { Agent, type AgentTool, type StreamFn } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
|
+
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
6
|
import { Type, type Static } from "typebox";
|
|
7
|
+
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* LLM intent arbiter for the completion mutation guard.
|
|
@@ -28,6 +30,10 @@ export type TaskMutationArbiter = (task: string) => Promise<TaskMutationVerdict>
|
|
|
28
30
|
const DecisionParams = Type.Object(
|
|
29
31
|
{
|
|
30
32
|
classification: Type.String({ enum: ["read_only", "implementation"] }),
|
|
33
|
+
confidence: Type.String({
|
|
34
|
+
enum: ["low", "medium", "high"],
|
|
35
|
+
description: "Confidence in the classification. Only read_only with high confidence rescues a failed run.",
|
|
36
|
+
}),
|
|
31
37
|
reason: Type.String({ description: "One concise reason for this classification." }),
|
|
32
38
|
},
|
|
33
39
|
{ additionalProperties: false },
|
|
@@ -35,6 +41,15 @@ const DecisionParams = Type.Object(
|
|
|
35
41
|
|
|
36
42
|
type DecisionParams = Static<typeof DecisionParams>;
|
|
37
43
|
|
|
44
|
+
/** Map a model decision to a verdict. Only a high-confidence read_only rescues. */
|
|
45
|
+
export function mapArbiterDecision(
|
|
46
|
+
decision: { classification?: string; confidence?: string } | undefined,
|
|
47
|
+
): TaskMutationVerdict {
|
|
48
|
+
if (!decision) return "unavailable";
|
|
49
|
+
if (decision.classification === "read_only" && decision.confidence === "high") return "read-only";
|
|
50
|
+
return "implementation";
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
interface ArbiterRuntime {
|
|
39
54
|
model: NonNullable<RegistryModel>;
|
|
40
55
|
baseStreamFn: StreamFn;
|
|
@@ -43,7 +58,7 @@ interface ArbiterRuntime {
|
|
|
43
58
|
|
|
44
59
|
interface ArbiterAuth {
|
|
45
60
|
apiKey?: string;
|
|
46
|
-
headers?:
|
|
61
|
+
headers?: ProviderHeaders;
|
|
47
62
|
env?: Record<string, string>;
|
|
48
63
|
}
|
|
49
64
|
|
|
@@ -105,18 +120,21 @@ async function resolveArbiterAuth(
|
|
|
105
120
|
ctx: ExtensionContext,
|
|
106
121
|
model: RegistryModel,
|
|
107
122
|
): Promise<ArbiterAuth> {
|
|
108
|
-
const
|
|
123
|
+
const registry = ctx.modelRegistry as {
|
|
109
124
|
getApiKeyAndHeaders?: (m: RegistryModel) => Promise<{
|
|
110
125
|
ok: boolean;
|
|
111
126
|
apiKey?: string;
|
|
112
|
-
headers?:
|
|
127
|
+
headers?: ProviderHeaders;
|
|
113
128
|
env?: Record<string, string>;
|
|
114
129
|
error?: string;
|
|
115
130
|
}>;
|
|
116
|
-
}
|
|
117
|
-
|
|
131
|
+
};
|
|
132
|
+
// Call as a METHOD on the registry: the host ModelRegistry implementation
|
|
133
|
+
// is a class whose method reads instance state (this.runtime), so a
|
|
134
|
+
// detached call silently fails auth. Same shape as the watchdog.
|
|
135
|
+
if (!registry.getApiKeyAndHeaders) return {};
|
|
118
136
|
try {
|
|
119
|
-
const auth = await
|
|
137
|
+
const auth = await registry.getApiKeyAndHeaders(model);
|
|
120
138
|
if (auth.ok === false) return {};
|
|
121
139
|
return {
|
|
122
140
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
@@ -164,14 +182,15 @@ async function runArbitration(
|
|
|
164
182
|
"You classify whether a delegated coding-agent task instructed file or code changes.",
|
|
165
183
|
"A task that asks to review, inspect, verify, report, or summarize is read-only even when it contains words like 'fix' as severity vocabulary ('must-fix items') or conditional change instructions that leave the change optional.",
|
|
166
184
|
"Classify read_only only when the task text alone clearly indicates a read-only outcome. When in doubt, classify implementation.",
|
|
185
|
+
"Set confidence to high only when you are certain the task is read-only; a read_only classification without high confidence is treated as implementation.",
|
|
167
186
|
"The agent's own final message is never evidence: an agent that made no edits may still have failed to implement.",
|
|
168
|
-
"Call task_mutation_decision exactly once with read_only or implementation and a concise reason.",
|
|
187
|
+
"Call task_mutation_decision exactly once with read_only or implementation, a confidence level, and a concise reason.",
|
|
169
188
|
].join("\n"),
|
|
170
189
|
model: runtime.model,
|
|
171
190
|
tools: [tool],
|
|
172
191
|
},
|
|
173
192
|
convertToLlm,
|
|
174
|
-
|
|
193
|
+
...agentStreamOptions(authWrappedStreamFn(runtime.baseStreamFn, auth)),
|
|
175
194
|
getApiKey: (providerName) =>
|
|
176
195
|
providerName === runtime.model.provider ? auth.apiKey : undefined,
|
|
177
196
|
beforeToolCall: async ({ toolCall }) =>
|
|
@@ -179,13 +198,12 @@ async function runArbitration(
|
|
|
179
198
|
toolExecution: "sequential",
|
|
180
199
|
});
|
|
181
200
|
try {
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: `TASK:\n${full.slice(0, 6000)}\n\n[...truncated middle...]\n\n${full.slice(-2000)}`;
|
|
201
|
+
// The rescue gate refuses tasks over 8000 chars; if the arbiter is
|
|
202
|
+
// still invoked with one, fail closed rather than decide from
|
|
203
|
+
// partial evidence (an implementation clause could sit in the
|
|
204
|
+
// omitted middle).
|
|
205
|
+
if (task.length > 8000) return "unavailable";
|
|
206
|
+
const prompt = `TASK:\n${task}`;
|
|
189
207
|
await Promise.race([
|
|
190
208
|
agent.prompt(prompt),
|
|
191
209
|
new Promise<never>((_, reject) => {
|
|
@@ -197,7 +215,7 @@ async function runArbitration(
|
|
|
197
215
|
}),
|
|
198
216
|
]);
|
|
199
217
|
if (!decision) return "unavailable";
|
|
200
|
-
return decision
|
|
218
|
+
return mapArbiterDecision(decision);
|
|
201
219
|
} catch {
|
|
202
220
|
return "unavailable";
|
|
203
221
|
}
|
|
@@ -228,13 +246,6 @@ export function isCompletionGuardFailure(result: { error?: string }): boolean {
|
|
|
228
246
|
return result.error?.startsWith(COMPLETION_GUARD_ERROR_PREFIX) === true;
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
/**
|
|
232
|
-
* Decision helper for the runSync completion-guard arbitration: given the raw
|
|
233
|
-
* guard verdict, decide whether the arbiter rescues the run. Pure and
|
|
234
|
-
* unit-testable; runSync calls this BEFORE any failure side effect is
|
|
235
|
-
* published. Only a confident read-only verdict rescues; every error,
|
|
236
|
-
* timeout, or other verdict keeps the guard's original behavior.
|
|
237
|
-
*/
|
|
238
249
|
export async function arbitrateCompletionGuardRescue(input: {
|
|
239
250
|
guardTriggered: boolean;
|
|
240
251
|
task: string;
|
|
@@ -243,6 +254,11 @@ export async function arbitrateCompletionGuardRescue(input: {
|
|
|
243
254
|
if (!input.guardTriggered || !input.arbiter) {
|
|
244
255
|
return { triggered: input.guardTriggered, rescued: false };
|
|
245
256
|
}
|
|
257
|
+
// Never decide from partial evidence: refuse tasks over 8000 chars before
|
|
258
|
+
// even consulting the model.
|
|
259
|
+
if (input.task.length > 8000) {
|
|
260
|
+
return { triggered: true, rescued: false };
|
|
261
|
+
}
|
|
246
262
|
try {
|
|
247
263
|
const verdict = await input.arbiter(input.task);
|
|
248
264
|
if (verdict === "read-only") return { triggered: false, rescued: true };
|
package/src/shared/artifacts.ts
CHANGED
|
@@ -65,10 +65,6 @@ function patternMatchesArtifactPath(pattern: string, artifactPath: string): bool
|
|
|
65
65
|
|| globMatchesPath(normalized, artifactPath);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function patternMatchesProjectArtifacts(pattern: string): boolean {
|
|
69
|
-
return PROJECT_ARTIFACT_PATHS.some((artifactPath) => patternMatchesArtifactPath(pattern, artifactPath));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
68
|
function ignoreFileExcludesProjectArtifacts(filePath: string): boolean {
|
|
73
69
|
if (!fs.existsSync(filePath)) return false;
|
|
74
70
|
try {
|
|
@@ -144,7 +140,7 @@ export function getProjectChainRunsDir(cwd: string): string {
|
|
|
144
140
|
|
|
145
141
|
export function getChainRunsDir(
|
|
146
142
|
projectCwd: string,
|
|
147
|
-
dirPreference: ArtifactDirPreference = "
|
|
143
|
+
dirPreference: ArtifactDirPreference = "session",
|
|
148
144
|
): string {
|
|
149
145
|
switch (dirPreference) {
|
|
150
146
|
case "project":
|
|
@@ -160,7 +156,7 @@ export function getChainRunsDir(
|
|
|
160
156
|
export function getArtifactsDir(
|
|
161
157
|
sessionFile: string | null,
|
|
162
158
|
projectCwd?: string,
|
|
163
|
-
dirPreference: ArtifactDirPreference = "
|
|
159
|
+
dirPreference: ArtifactDirPreference = "session",
|
|
164
160
|
): string {
|
|
165
161
|
switch (dirPreference) {
|
|
166
162
|
case "session":
|
|
@@ -98,3 +98,53 @@ export function previewDisplayText(value: string, maxLength: number): string {
|
|
|
98
98
|
if (maxLength <= 3) return truncateDisplayText(normalized, maxLength);
|
|
99
99
|
return `${truncateDisplayText(normalized, maxLength - 3)}...`;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
const BINARY_CONTENT_PLACEHOLDER = "[binary content omitted for safe display]";
|
|
103
|
+
|
|
104
|
+
function isUnsafeTerminalCodePoint(codePoint: number): boolean {
|
|
105
|
+
const terminalControl = (codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a)
|
|
106
|
+
|| (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
107
|
+
const bidiControl = codePoint === 0x061c
|
|
108
|
+
|| codePoint === 0x200e
|
|
109
|
+
|| codePoint === 0x200f
|
|
110
|
+
|| (codePoint >= 0x202a && codePoint <= 0x202e)
|
|
111
|
+
|| (codePoint >= 0x2066 && codePoint <= 0x2069);
|
|
112
|
+
const privateUse = (codePoint >= 0xe000 && codePoint <= 0xf8ff)
|
|
113
|
+
|| (codePoint >= 0xf0000 && codePoint <= 0xffffd)
|
|
114
|
+
|| (codePoint >= 0x100000 && codePoint <= 0x10fffd);
|
|
115
|
+
const invalidScalar = codePoint >= 0xd800 && codePoint <= 0xdfff;
|
|
116
|
+
const nonCharacter = (codePoint >= 0xfdd0 && codePoint <= 0xfdef)
|
|
117
|
+
|| (codePoint & 0xffff) === 0xfffe
|
|
118
|
+
|| (codePoint & 0xffff) === 0xffff;
|
|
119
|
+
return terminalControl || bidiControl || privateUse || invalidScalar || nonCharacter;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function looksLikeBinaryContent(text: string): boolean {
|
|
123
|
+
if (text.includes("\0")) return true;
|
|
124
|
+
let suspiciousControls = 0;
|
|
125
|
+
let replacementCharacters = 0;
|
|
126
|
+
let codePoints = 0;
|
|
127
|
+
for (const character of text) {
|
|
128
|
+
codePoints++;
|
|
129
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
130
|
+
if ((codePoint <= 0x08) || (codePoint >= 0x0e && codePoint <= 0x1f)) suspiciousControls++;
|
|
131
|
+
if (codePoint === 0xfffd) replacementCharacters++;
|
|
132
|
+
}
|
|
133
|
+
if (codePoints === 0) return false;
|
|
134
|
+
return (suspiciousControls >= 4 && suspiciousControls / codePoints >= 0.1)
|
|
135
|
+
|| (replacementCharacters >= 3 && replacementCharacters / codePoints >= 0.1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Escapes untrusted text before rendering it in a terminal while preserving safe newlines and tabs. */
|
|
139
|
+
export function safeTerminalText(value: string): string {
|
|
140
|
+
const normalized = value.replace(/\r\n/g, "\n");
|
|
141
|
+
if (looksLikeBinaryContent(normalized)) return BINARY_CONTENT_PLACEHOLDER;
|
|
142
|
+
let safe = "";
|
|
143
|
+
for (const character of normalized) {
|
|
144
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
145
|
+
safe += isUnsafeTerminalCodePoint(codePoint)
|
|
146
|
+
? `[U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}]`
|
|
147
|
+
: character;
|
|
148
|
+
}
|
|
149
|
+
return safe;
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
function isNodeExecutableName(execPath: string): boolean {
|
|
5
|
+
const basename = path.basename(execPath).toLowerCase();
|
|
6
|
+
return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function canUseNodeExecutable(execPath: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveNodeExecutable(execPath = process.execPath): string {
|
|
19
|
+
if (isNodeExecutableName(execPath) && canUseNodeExecutable(execPath)) return execPath;
|
|
20
|
+
return process.platform === "win32" ? "node.exe" : "node";
|
|
21
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -528,6 +528,8 @@ export interface SteeringRecoveryDescriptor {
|
|
|
528
528
|
structuredOutputSchema?: JsonSchemaObject;
|
|
529
529
|
acceptance?: AcceptanceInput;
|
|
530
530
|
controlConfig?: ResolvedControlConfig;
|
|
531
|
+
/** Raw per-run bridge override. Omitted descriptors continue to use global config. */
|
|
532
|
+
intercomBridge?: IntercomBridgeConfig;
|
|
531
533
|
absoluteDeadlineAt?: number;
|
|
532
534
|
initialTurnBudget?: ResolvedTurnBudget;
|
|
533
535
|
initialToolBudget?: ResolvedToolBudget;
|
|
@@ -1086,7 +1088,7 @@ export interface Details {
|
|
|
1086
1088
|
trace: Array<{
|
|
1087
1089
|
operation: "run" | "status";
|
|
1088
1090
|
key: string;
|
|
1089
|
-
state: "started" | "completed" | "failed" | "reused";
|
|
1091
|
+
state: "started" | "completed" | "failed" | "stopped" | "reused";
|
|
1090
1092
|
agent?: string;
|
|
1091
1093
|
runId?: string;
|
|
1092
1094
|
phase?: string;
|
|
@@ -1259,6 +1261,8 @@ export interface AsyncStartedEvent {
|
|
|
1259
1261
|
asyncDir?: string;
|
|
1260
1262
|
/** Parent-resolved launch directory, used as a trusted artifact root while this session is live. */
|
|
1261
1263
|
cwd?: string;
|
|
1264
|
+
/** Parent-resolved child session root, used only while this session owns the live run. */
|
|
1265
|
+
sessionRoot?: string;
|
|
1262
1266
|
pid?: number;
|
|
1263
1267
|
sessionId?: string;
|
|
1264
1268
|
mode?: SubagentRunMode;
|
|
@@ -1462,6 +1466,8 @@ export interface AsyncJobState {
|
|
|
1462
1466
|
asyncDir: string;
|
|
1463
1467
|
/** Parent-resolved launch directory retained for trusted live artifact lookup. */
|
|
1464
1468
|
cwd?: string;
|
|
1469
|
+
/** Parent-resolved child session root retained for trusted live transcript lookup. */
|
|
1470
|
+
sessionRoot?: string;
|
|
1465
1471
|
status: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
1466
1472
|
/** Short caller-facing task/goal shown in fleet surfaces when available. */
|
|
1467
1473
|
description?: string;
|
|
@@ -1661,6 +1667,10 @@ export interface SubagentState {
|
|
|
1661
1667
|
/** Runtime mission-store snapshot used by optional inspector context. */
|
|
1662
1668
|
missionStoreConfig?: MissionStoreConfig;
|
|
1663
1669
|
parentSessionFile?: string | null;
|
|
1670
|
+
/** Extension-owned roots trusted for child session transcript reads. */
|
|
1671
|
+
trustedSessionRoots?: string[];
|
|
1672
|
+
/** Live async session roots created by this parent executor, keyed by run id. */
|
|
1673
|
+
liveAsyncSessionRoots?: Map<string, string>;
|
|
1664
1674
|
/** Last valid parent session model observed for this session; used when continuation contexts omit ctx.model. */
|
|
1665
1675
|
lastParentModel?: { provider: string; id: string };
|
|
1666
1676
|
subagentInProgress?: boolean;
|
|
@@ -1882,6 +1892,13 @@ export const FLEET_KEYBINDING_ACTIONS = [
|
|
|
1882
1892
|
export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
|
|
1883
1893
|
export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
|
|
1884
1894
|
|
|
1895
|
+
export interface MainWindowRendererConfig {
|
|
1896
|
+
/** Unit of horizontal space in main chat subagent call/result rows. Omit to preserve current spacing. Set 0 for no extra padding. */
|
|
1897
|
+
horizontalSpacing?: number;
|
|
1898
|
+
/** Maximum collapsed rich-result rows. Expanded output is not capped. */
|
|
1899
|
+
compactResultMaxLines?: number;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1885
1902
|
export interface ExtensionConfig {
|
|
1886
1903
|
asyncByDefault?: boolean;
|
|
1887
1904
|
/** Show the Claude Code-style navigable fleet. Defaults to true. */
|
|
@@ -1898,6 +1915,8 @@ export interface ExtensionConfig {
|
|
|
1898
1915
|
legacyChainControls?: boolean;
|
|
1899
1916
|
/** Inline chat rendering for the subagent tool. Defaults to rich. */
|
|
1900
1917
|
inlineToolDisplay?: InlineToolDisplay;
|
|
1918
|
+
/** Density controls for the main chat subagent call/result renderer. */
|
|
1919
|
+
mainWindowRenderer?: MainWindowRendererConfig;
|
|
1901
1920
|
forceTopLevelAsync?: boolean;
|
|
1902
1921
|
waitTool?: WaitToolConfig;
|
|
1903
1922
|
defaultSessionDir?: string;
|
|
@@ -1933,7 +1952,7 @@ export interface ExtensionConfig {
|
|
|
1933
1952
|
worktreeSetupHook?: string;
|
|
1934
1953
|
worktreeSetupHookTimeoutMs?: number;
|
|
1935
1954
|
worktreeBaseDir?: string;
|
|
1936
|
-
/** Where to store subagent artifact files. Defaults to "
|
|
1955
|
+
/** Where to store subagent artifact files. Defaults to "session" (the pi session directory, or OS temp when unavailable). Set to "project" for cwd/.pi/subagents. */
|
|
1937
1956
|
artifactDir?: ArtifactDirPreference;
|
|
1938
1957
|
/** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
|
|
1939
1958
|
artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
|
|
@@ -1957,7 +1976,7 @@ export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
|
|
|
1957
1976
|
|
|
1958
1977
|
export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
1959
1978
|
enabled: true,
|
|
1960
|
-
dir: "
|
|
1979
|
+
dir: "session",
|
|
1961
1980
|
includeInput: true,
|
|
1962
1981
|
includeOutput: true,
|
|
1963
1982
|
includeJsonl: false,
|
|
@@ -2044,7 +2063,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
2044
2063
|
export const POLL_INTERVAL_MS = 250;
|
|
2045
2064
|
export const MAX_WIDGET_JOBS = 4;
|
|
2046
2065
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
2047
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
|
2066
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
|
2048
2067
|
|
|
2049
2068
|
export const DEFAULT_FORK_PREAMBLE =
|
|
2050
2069
|
"You are a delegated subagent running from a fork of the parent session. " +
|
|
@@ -2,59 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getLanguageFromPath, highlightCode, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
5
|
+
import { safeTerminalText as safeDisplayText } from "../shared/display-text.ts";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_MAX_RECORDS = 240;
|
|
7
8
|
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
8
9
|
const MAX_MESSAGE_CHARS = 64 * 1024;
|
|
9
10
|
const TOOL_PREVIEW_LINES = 7;
|
|
10
|
-
const BINARY_CONTENT_PLACEHOLDER = "[binary content omitted for safe display]";
|
|
11
|
-
|
|
12
|
-
function isUnsafeDisplayCodePoint(codePoint: number): boolean {
|
|
13
|
-
const terminalControl = (codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a)
|
|
14
|
-
|| (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
15
|
-
const bidiControl = codePoint === 0x061c
|
|
16
|
-
|| codePoint === 0x200e
|
|
17
|
-
|| codePoint === 0x200f
|
|
18
|
-
|| (codePoint >= 0x202a && codePoint <= 0x202e)
|
|
19
|
-
|| (codePoint >= 0x2066 && codePoint <= 0x2069);
|
|
20
|
-
const privateUse = (codePoint >= 0xe000 && codePoint <= 0xf8ff)
|
|
21
|
-
|| (codePoint >= 0xf0000 && codePoint <= 0xffffd)
|
|
22
|
-
|| (codePoint >= 0x100000 && codePoint <= 0x10fffd);
|
|
23
|
-
const invalidScalar = codePoint >= 0xd800 && codePoint <= 0xdfff;
|
|
24
|
-
const nonCharacter = (codePoint >= 0xfdd0 && codePoint <= 0xfdef)
|
|
25
|
-
|| (codePoint & 0xffff) === 0xfffe
|
|
26
|
-
|| (codePoint & 0xffff) === 0xffff;
|
|
27
|
-
return terminalControl || bidiControl || privateUse || invalidScalar || nonCharacter;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function looksLikeBinaryContent(text: string): boolean {
|
|
31
|
-
if (text.includes("\0")) return true;
|
|
32
|
-
let suspiciousControls = 0;
|
|
33
|
-
let replacementCharacters = 0;
|
|
34
|
-
let codePoints = 0;
|
|
35
|
-
for (const character of text) {
|
|
36
|
-
codePoints++;
|
|
37
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
38
|
-
if ((codePoint <= 0x08) || (codePoint >= 0x0e && codePoint <= 0x1f)) suspiciousControls++;
|
|
39
|
-
if (codePoint === 0xfffd) replacementCharacters++;
|
|
40
|
-
}
|
|
41
|
-
if (codePoints === 0) return false;
|
|
42
|
-
return (suspiciousControls >= 4 && suspiciousControls / codePoints >= 0.1)
|
|
43
|
-
|| (replacementCharacters >= 3 && replacementCharacters / codePoints >= 0.1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function safeDisplayText(text: string): string {
|
|
47
|
-
const normalized = text.replace(/\r\n/g, "\n");
|
|
48
|
-
if (looksLikeBinaryContent(normalized)) return BINARY_CONTENT_PLACEHOLDER;
|
|
49
|
-
let safe = "";
|
|
50
|
-
for (const character of normalized) {
|
|
51
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
52
|
-
safe += isUnsafeDisplayCodePoint(codePoint)
|
|
53
|
-
? `[U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}]`
|
|
54
|
-
: character;
|
|
55
|
-
}
|
|
56
|
-
return safe;
|
|
57
|
-
}
|
|
58
11
|
|
|
59
12
|
function sanitizeJsonDisplayValue(value: unknown): { value: unknown; changed: boolean } {
|
|
60
13
|
if (typeof value === "string") {
|
package/src/tui/fleet.ts
CHANGED
|
@@ -473,7 +473,7 @@ function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
|
|
|
473
473
|
return getArtifactsDir(
|
|
474
474
|
state.parentSessionFile ?? null,
|
|
475
475
|
cwd,
|
|
476
|
-
state.artifactDirPreference
|
|
476
|
+
state.artifactDirPreference,
|
|
477
477
|
);
|
|
478
478
|
}
|
|
479
479
|
|
|
@@ -510,6 +510,7 @@ function transcriptTarget(item: FleetItem, state: SubagentState): { path: string
|
|
|
510
510
|
item.run.asyncDir,
|
|
511
511
|
fleetArtifactsRoot(state, state.baseCwd),
|
|
512
512
|
trackedJob?.cwd ? fleetArtifactsRoot(state, trackedJob.cwd) : undefined,
|
|
513
|
+
item.run.sessionFile ? getArtifactsDir(item.run.sessionFile, item.run.cwd ?? state.baseCwd, state.artifactDirPreference) : undefined,
|
|
513
514
|
]),
|
|
514
515
|
};
|
|
515
516
|
}
|
|
@@ -1178,6 +1179,7 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
|
|
|
1178
1179
|
...(input.index !== undefined ? { index: input.index } : {}),
|
|
1179
1180
|
}, {
|
|
1180
1181
|
state,
|
|
1182
|
+
sessionRoots: state.trustedSessionRoots,
|
|
1181
1183
|
cwd: state.baseCwd,
|
|
1182
1184
|
...(state.authorityPolicy ? { authorityPolicy: state.authorityPolicy } : {}),
|
|
1183
1185
|
...(state.missionStoreConfig ? { missions: state.missionStoreConfig } : {}),
|