@tt-a1i/openpi 0.6.0 → 0.7.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/README.md +13 -11
- package/SETUP.md +2 -2
- package/THIRD_PARTY_NOTICES.md +242 -0
- package/extensions/ai-providers/README.md +12 -6
- package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
- package/extensions/ai-providers/cursor/input-images.ts +2 -3
- package/extensions/ai-providers/cursor/proto.ts +218 -11
- package/extensions/ai-providers/cursor/protobuf.ts +12 -2
- package/extensions/ai-providers/cursor/provider.ts +280 -37
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/index.ts +3 -3
- package/extensions/file-mutation-display/index.ts +13 -9
- package/extensions/shared/agent-transcript.ts +3 -2
- package/extensions/shared/child-session.ts +14 -0
- package/extensions/subagents/index.ts +20 -3
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +70 -59
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/manager.ts +2 -71
- package/extensions/subagents/src/prompt.ts +2 -2
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/web/index.ts +69 -5
- package/extensions/workflows/artifacts.ts +362 -23
- package/extensions/workflows/dashboard.ts +141 -21
- package/extensions/workflows/index.ts +62 -20
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/runner.ts +5 -162
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +28 -4
- package/skills/subagents/REFERENCE.md +6 -7
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +3 -1
- package/web/dist/app.js +87 -0
- package/web/dist/favicon.svg +9 -0
- package/web/dist/index.html +15 -0
- package/web/dist/styles.css +3 -0
- package/web/host/web-host.ts +6 -9
- package/web/ui/index.html +3 -131
- package/web/ui/public/favicon.svg +9 -0
- package/web/ui/src/app/App.tsx +134 -0
- package/web/ui/src/app/providers.tsx +38 -0
- package/web/ui/src/components/Markdown.tsx +58 -0
- package/web/ui/src/components/OpenPiLogo.tsx +41 -0
- package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
- package/web/ui/src/features/composer/Composer.tsx +237 -0
- package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
- package/web/ui/src/features/transcript/Transcript.tsx +860 -0
- package/web/ui/src/i18n.ts +159 -0
- package/web/ui/src/lib/format.ts +57 -0
- package/web/ui/src/main.tsx +16 -0
- package/web/ui/src/protocol/client.ts +199 -0
- package/web/ui/src/protocol/event-stream.ts +88 -0
- package/web/ui/src/store/web-store.ts +926 -0
- package/web/ui/src/styles.css +420 -0
- package/web/ui/tsconfig.json +12 -0
- package/web/ui/vite-env.d.ts +1 -0
- package/web/vite.config.mjs +21 -1
- package/web/host/static-assets.ts +0 -4
- package/web/ui/app.js +0 -1700
- package/web/ui/styles.css +0 -680
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
} from "../shared/below-editor-navigation.ts";
|
|
58
58
|
import {
|
|
59
59
|
effectiveChildToolAllowlist,
|
|
60
|
+
inheritedChildToolAllowlist,
|
|
60
61
|
resolveStandaloneChildProjectTrust,
|
|
61
62
|
} from "../shared/child-session.ts";
|
|
62
63
|
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
|
@@ -144,6 +145,7 @@ import { createSubagentResultDelivery } from "./src/result-delivery.ts";
|
|
|
144
145
|
import {
|
|
145
146
|
createSubagentRuntime,
|
|
146
147
|
runTool,
|
|
148
|
+
SubagentToolInterruptedError,
|
|
147
149
|
type SubagentRuntime,
|
|
148
150
|
} from "./src/runtime.ts";
|
|
149
151
|
import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
|
|
@@ -886,6 +888,7 @@ export default function (
|
|
|
886
888
|
if (
|
|
887
889
|
planning &&
|
|
888
890
|
agentType &&
|
|
891
|
+
!agentType.planningCompatible &&
|
|
889
892
|
!planModeAllowsDeclaredTools(declaredChildTools)
|
|
890
893
|
) {
|
|
891
894
|
throw new Error(
|
|
@@ -934,7 +937,10 @@ export default function (
|
|
|
934
937
|
const requestedChildTools = planning
|
|
935
938
|
? planModeChildTools(declaredChildTools)
|
|
936
939
|
: declaredChildTools;
|
|
937
|
-
const childTools =
|
|
940
|
+
const childTools = inheritedChildToolAllowlist(
|
|
941
|
+
pi.getActiveTools(),
|
|
942
|
+
requestedChildTools,
|
|
943
|
+
);
|
|
938
944
|
// Read at spawn time so `/openpi-setup` changes affect the next child
|
|
939
945
|
// without reloading this extension. Undefined preserves parent-model
|
|
940
946
|
// inheritance in the backend.
|
|
@@ -980,11 +986,22 @@ export default function (
|
|
|
980
986
|
interruptMessage: "Subagent spawn aborted.",
|
|
981
987
|
});
|
|
982
988
|
} catch (error) {
|
|
983
|
-
//
|
|
984
|
-
//
|
|
989
|
+
// Known startup failures can reclaim their empty checkout. Interrupted
|
|
990
|
+
// startup must preserve it while asynchronous acquisition may continue.
|
|
985
991
|
if (worktree) {
|
|
986
992
|
const spawnError =
|
|
987
993
|
error instanceof Error ? error.message : String(error);
|
|
994
|
+
// Cancelling Effect acquisition does not prove an asynchronous
|
|
995
|
+
// factory or extension hook has quiesced. It may still use this cwd.
|
|
996
|
+
if (
|
|
997
|
+
signal?.aborted ||
|
|
998
|
+
error instanceof SubagentToolInterruptedError
|
|
999
|
+
) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`${spawnError}; startup quiescence is unknown; checkout preserved at ${worktree.path} (branch ${worktree.branch})`,
|
|
1002
|
+
{ cause: error },
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
988
1005
|
let cleanupWarning: string | undefined;
|
|
989
1006
|
let cleanupError: unknown;
|
|
990
1007
|
try {
|
|
@@ -92,6 +92,8 @@ export interface AgentType {
|
|
|
92
92
|
readonly description: string;
|
|
93
93
|
/** Omitted = the child keeps the normal tool set. Present = allowlist. */
|
|
94
94
|
readonly tools?: readonly string[];
|
|
95
|
+
/** Only built-in investigator definitions carry this planning compatibility. */
|
|
96
|
+
readonly planningCompatible?: boolean;
|
|
95
97
|
/** "provider/model-id" or a bare id; resolved by the pi backend. */
|
|
96
98
|
readonly model?: string;
|
|
97
99
|
readonly reasoningEffort?: ReasoningEffort;
|
|
@@ -121,9 +123,9 @@ export const READ_ONLY_AGENT_TOOLS = [
|
|
|
121
123
|
export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [
|
|
122
124
|
{
|
|
123
125
|
name: "explorer",
|
|
126
|
+
planningCompatible: true,
|
|
124
127
|
description:
|
|
125
128
|
"Read-only codebase exploration. Usually use moderate reasoning, increasing it for harder tasks.",
|
|
126
|
-
tools: READ_ONLY_AGENT_TOOLS,
|
|
127
129
|
body: "Explore the codebase read-only. Trace the real flow, inspect related callers, and report concise evidence with file paths and line references.",
|
|
128
130
|
source: "built-in:explorer",
|
|
129
131
|
},
|
|
@@ -131,36 +133,22 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [
|
|
|
131
133
|
name: "implementer",
|
|
132
134
|
description:
|
|
133
135
|
"Focused implementation with repository checks. Usually use medium-high reasoning, adjusted for scope, risk, and task difficulty.",
|
|
134
|
-
tools: [
|
|
135
|
-
"read",
|
|
136
|
-
"bash",
|
|
137
|
-
"edit",
|
|
138
|
-
"write",
|
|
139
|
-
"grep",
|
|
140
|
-
"find",
|
|
141
|
-
"ls",
|
|
142
|
-
"fd",
|
|
143
|
-
"rg",
|
|
144
|
-
"git_show",
|
|
145
|
-
"git_diff",
|
|
146
|
-
"git_log",
|
|
147
|
-
],
|
|
148
136
|
body: "Implement the requested change carefully. Trace the affected flow first, make the smallest correct edit, and run relevant checks before reporting results.",
|
|
149
137
|
source: "built-in:implementer",
|
|
150
138
|
},
|
|
151
139
|
{
|
|
152
140
|
name: "reviewer",
|
|
141
|
+
planningCompatible: true,
|
|
153
142
|
description:
|
|
154
143
|
"Read-only review for correctness, safety, and regressions. Usually use high reasoning, adjusted for task difficulty.",
|
|
155
|
-
tools: READ_ONLY_AGENT_TOOLS,
|
|
156
144
|
body: "Review the requested code or change read-only. Identify concrete correctness, security, and regression risks with evidence; do not modify files.",
|
|
157
145
|
source: "built-in:reviewer",
|
|
158
146
|
},
|
|
159
147
|
{
|
|
160
148
|
name: "advisor",
|
|
149
|
+
planningCompatible: true,
|
|
161
150
|
description:
|
|
162
151
|
"Deep read-only analysis and technical advice. Usually use high reasoning, adjusted for task difficulty.",
|
|
163
|
-
tools: READ_ONLY_AGENT_TOOLS,
|
|
164
152
|
body: "Analyze the problem deeply without modifying files. Explain the relevant tradeoffs, risks, and recommended next step using repository evidence.",
|
|
165
153
|
source: "built-in:advisor",
|
|
166
154
|
},
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import type { Cause, Scope } from "effect";
|
|
26
26
|
import { Effect, Queue, Stream } from "effect";
|
|
27
27
|
import { resolveAgentModel } from "../agent-types.ts";
|
|
28
|
+
import { toolPreview } from "./tool-preview.ts";
|
|
28
29
|
import type {
|
|
29
30
|
SubagentBackend,
|
|
30
31
|
SubagentCleanupReceipt,
|
|
@@ -118,27 +119,6 @@ function safeJson(value: unknown): string | undefined {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
/** First non-empty line of a tool result-ish value (v1 liveToolPreview). */
|
|
122
|
-
function toolPreview(value: unknown): string | undefined {
|
|
123
|
-
if (typeof value === "string") {
|
|
124
|
-
return value
|
|
125
|
-
.split("\n")
|
|
126
|
-
.find((line) => line.trim())
|
|
127
|
-
?.trim();
|
|
128
|
-
}
|
|
129
|
-
if (!value || typeof value !== "object") return undefined;
|
|
130
|
-
const content = (value as { content?: unknown }).content;
|
|
131
|
-
if (!Array.isArray(content)) return undefined;
|
|
132
|
-
for (const part of content) {
|
|
133
|
-
if (!part || typeof part !== "object") continue;
|
|
134
|
-
const record = part as { type?: unknown; text?: unknown };
|
|
135
|
-
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
136
|
-
const firstLine = record.text.split("\n").find((line) => line.trim());
|
|
137
|
-
if (firstLine) return firstLine.trim();
|
|
138
|
-
}
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
122
|
function assistantParts(msg: AssistantMessage): TranscriptPart[] {
|
|
143
123
|
const parts: TranscriptPart[] = [];
|
|
144
124
|
for (const part of msg.content) {
|
|
@@ -215,42 +195,70 @@ const makePiSession = (
|
|
|
215
195
|
capturedStructured = encodeStructuredResult(value);
|
|
216
196
|
});
|
|
217
197
|
|
|
198
|
+
// Own the session before asynchronous startup. Interruption can happen
|
|
199
|
+
// before the normal backend finalizer has been installed.
|
|
200
|
+
let acquiringSession: AgentSession | undefined;
|
|
201
|
+
let startupOwned = true;
|
|
202
|
+
const cleanupStartup = () =>
|
|
203
|
+
acquiringSession
|
|
204
|
+
? shutdownAndDisposeChildSession(acquiringSession, {
|
|
205
|
+
abort: true,
|
|
206
|
+
timeoutMs: options.shutdownTimeoutMs,
|
|
207
|
+
})
|
|
208
|
+
: Promise.resolve();
|
|
209
|
+
yield* Effect.addFinalizer(() =>
|
|
210
|
+
Effect.promise(async () => {
|
|
211
|
+
if (startupOwned) await cleanupStartup();
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
218
215
|
const session = yield* Effect.tryPromise({
|
|
219
|
-
try: async () => {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
projectTrusted: task.parent.projectTrusted,
|
|
229
|
-
...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}),
|
|
230
|
-
});
|
|
231
|
-
const { session } = await (
|
|
232
|
-
options.sessionFactory ?? createAgentSession
|
|
233
|
-
)({
|
|
234
|
-
cwd: task.cwd,
|
|
235
|
-
sessionManager: SessionManager.create(task.cwd),
|
|
236
|
-
settingsManager,
|
|
237
|
-
resourceLoader: loader,
|
|
238
|
-
model,
|
|
239
|
-
thinkingLevel,
|
|
240
|
-
...(structuredOutputTool
|
|
241
|
-
? { customTools: [structuredOutputTool] }
|
|
242
|
-
: {}),
|
|
243
|
-
...childToolPolicy(
|
|
244
|
-
childToolsWithStructuredOutput(
|
|
245
|
-
task.tools,
|
|
246
|
-
structuredOutputTool !== undefined,
|
|
247
|
-
),
|
|
248
|
-
),
|
|
249
|
-
});
|
|
250
|
-
// Start child extension session hooks/resources in headless mode.
|
|
251
|
-
// A rejection here would otherwise leak the freshly created session:
|
|
252
|
-
// the scope finalizer that owns cleanup is only registered later.
|
|
216
|
+
try: async (signal) => {
|
|
217
|
+
const checkCancelled = () => {
|
|
218
|
+
if (signal.aborted)
|
|
219
|
+
throw signal.reason ?? new Error("Subagent startup cancelled");
|
|
220
|
+
};
|
|
221
|
+
const onCancelled = () => {
|
|
222
|
+
void cleanupStartup().catch(() => {});
|
|
223
|
+
};
|
|
224
|
+
signal.addEventListener("abort", onCancelled, { once: true });
|
|
253
225
|
try {
|
|
226
|
+
checkCancelled();
|
|
227
|
+
const appendSystemPrompt = [
|
|
228
|
+
...(task.appendSystemPrompt ?? []),
|
|
229
|
+
...(structuredOutputTool
|
|
230
|
+
? [STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION]
|
|
231
|
+
: []),
|
|
232
|
+
];
|
|
233
|
+
const { loader, settingsManager } = await createChildResources({
|
|
234
|
+
cwd: task.cwd,
|
|
235
|
+
projectTrusted: task.parent.projectTrusted,
|
|
236
|
+
...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}),
|
|
237
|
+
});
|
|
238
|
+
checkCancelled();
|
|
239
|
+
const { session } = await (
|
|
240
|
+
options.sessionFactory ?? createAgentSession
|
|
241
|
+
)({
|
|
242
|
+
cwd: task.cwd,
|
|
243
|
+
sessionManager: SessionManager.create(task.cwd),
|
|
244
|
+
settingsManager,
|
|
245
|
+
resourceLoader: loader,
|
|
246
|
+
model,
|
|
247
|
+
thinkingLevel,
|
|
248
|
+
...(structuredOutputTool
|
|
249
|
+
? { customTools: [structuredOutputTool] }
|
|
250
|
+
: {}),
|
|
251
|
+
...childToolPolicy(
|
|
252
|
+
childToolsWithStructuredOutput(
|
|
253
|
+
task.tools,
|
|
254
|
+
structuredOutputTool !== undefined,
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
});
|
|
258
|
+
acquiringSession = session;
|
|
259
|
+
checkCancelled();
|
|
260
|
+
// Never start extension binding for a factory that completed after
|
|
261
|
+
// cancellation. Already-running hooks retain bounded cleanup ownership.
|
|
254
262
|
await bindChildSessionExtensions(
|
|
255
263
|
session,
|
|
256
264
|
childToolsWithStructuredOutput(
|
|
@@ -258,13 +266,14 @@ const makePiSession = (
|
|
|
258
266
|
structuredOutputTool !== undefined,
|
|
259
267
|
),
|
|
260
268
|
);
|
|
269
|
+
checkCancelled();
|
|
270
|
+
return session;
|
|
261
271
|
} catch (error) {
|
|
262
|
-
await
|
|
263
|
-
timeoutMs: options.shutdownTimeoutMs,
|
|
264
|
-
});
|
|
272
|
+
await cleanupStartup();
|
|
265
273
|
throw error;
|
|
274
|
+
} finally {
|
|
275
|
+
signal.removeEventListener("abort", onCancelled);
|
|
266
276
|
}
|
|
267
|
-
return session;
|
|
268
277
|
},
|
|
269
278
|
catch: (error) => new SpawnError({ message: boundedError(error) }),
|
|
270
279
|
});
|
|
@@ -650,6 +659,8 @@ const makePiSession = (
|
|
|
650
659
|
}),
|
|
651
660
|
);
|
|
652
661
|
|
|
662
|
+
startupOwned = false;
|
|
663
|
+
|
|
653
664
|
/** Start a fresh run (v1 manager.run): fire-and-forget, errors -> events. */
|
|
654
665
|
const startRun = (text: string) => {
|
|
655
666
|
if (state.activePrompt) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Match the manager's existing transcript text limit; canonical Pi tool results
|
|
2
|
+
// remain intact. Only the normalized event's single-line preview is bounded.
|
|
3
|
+
const TOOL_PREVIEW_MAX_LENGTH = 64 * 1_024;
|
|
4
|
+
|
|
5
|
+
function firstMeaningfulLine(text: string) {
|
|
6
|
+
// Search only until the first non-whitespace character. Unlike splitting the
|
|
7
|
+
// entire log, this preserves blank-line behavior without visiting its tail.
|
|
8
|
+
const start = text.search(/\S/);
|
|
9
|
+
if (start < 0) return undefined;
|
|
10
|
+
const prefix = text.slice(start, start + TOOL_PREVIEW_MAX_LENGTH);
|
|
11
|
+
const newline = prefix.indexOf("\n");
|
|
12
|
+
return (newline < 0 ? prefix : prefix.slice(0, newline)).trimEnd();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** First meaningful line of a tool result, without splitting accumulated logs. */
|
|
16
|
+
export function toolPreview(value: unknown) {
|
|
17
|
+
if (typeof value === "string") return firstMeaningfulLine(value);
|
|
18
|
+
if (!value || typeof value !== "object") return undefined;
|
|
19
|
+
const content = (value as { content?: unknown }).content;
|
|
20
|
+
if (!Array.isArray(content)) return undefined;
|
|
21
|
+
for (const part of content) {
|
|
22
|
+
if (!part || typeof part !== "object") continue;
|
|
23
|
+
const record = part as { type?: unknown; text?: unknown };
|
|
24
|
+
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
25
|
+
const firstLine = firstMeaningfulLine(record.text);
|
|
26
|
+
if (firstLine) return firstLine;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
* imperative TUI components (which render synchronously) can read snapshots
|
|
11
11
|
* and issue fire-and-forget commands without touching the Effect runtime.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* failure (releasing its concurrency slot) instead of hanging forever,
|
|
16
|
-
* mirroring the workflow runner's watchdog.
|
|
13
|
+
* Pi owns provider transport timeouts and retries. This manager owns explicit
|
|
14
|
+
* cancellation, settlement, and bounded cleanup, not model-output deadlines.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
17
|
import {
|
|
@@ -60,13 +58,6 @@ export const MAX_TRACKED = 64;
|
|
|
60
58
|
const STOP_TIMEOUT_MS = 5_000;
|
|
61
59
|
/** Session abort/shutdown (5s) plus bounded direct-worktree cleanup (4s). */
|
|
62
60
|
const ENTRY_CLOSE_TIMEOUT_MS = 10_000;
|
|
63
|
-
/**
|
|
64
|
-
* First-response watchdog: a run whose provider accepts the request but
|
|
65
|
-
* never emits an assistant event is settled as a failure so it cannot
|
|
66
|
-
* occupy a concurrency slot forever. Matches the workflow runner's
|
|
67
|
-
* MODEL_PROGRESS_TIMEOUT_MS (extensions/workflows/runner.ts).
|
|
68
|
-
*/
|
|
69
|
-
export const FIRST_RESPONSE_TIMEOUT_MS = 45_000;
|
|
70
61
|
const ERROR_TEXT_MAX_LENGTH = 4_096;
|
|
71
62
|
const TRANSCRIPT_TEXT_MAX_LENGTH = 64 * 1_024;
|
|
72
63
|
const LIVE_ASSISTANT_MAX_LENGTH = 128 * 1_024;
|
|
@@ -77,10 +68,6 @@ function bounded(text: string) {
|
|
|
77
68
|
return text.slice(0, ERROR_TEXT_MAX_LENGTH);
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
function formatWatchdogTimeout(ms: number) {
|
|
81
|
-
return ms % 1_000 === 0 ? `${ms / 1_000} seconds` : `${ms} ms`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
71
|
function boundedTranscriptText(text: string) {
|
|
85
72
|
return text.slice(0, TRANSCRIPT_TEXT_MAX_LENGTH);
|
|
86
73
|
}
|
|
@@ -133,8 +120,6 @@ interface Entry {
|
|
|
133
120
|
scope: Scope.Closeable;
|
|
134
121
|
pump?: Fiber.Fiber<void>;
|
|
135
122
|
liveToolMap: Map<string, LiveToolState>;
|
|
136
|
-
/** First-response watchdog timer for the active (or just-armed) run. */
|
|
137
|
-
watchdogTimer?: ReturnType<typeof setTimeout>;
|
|
138
123
|
/** Idle restart dispatched but RunStarted not folded yet; counts as running
|
|
139
124
|
* so concurrent restarts cannot race past the cap. */
|
|
140
125
|
restarting?: boolean;
|
|
@@ -214,8 +199,6 @@ export class SubagentManager extends Context.Service<
|
|
|
214
199
|
|
|
215
200
|
const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
216
201
|
Effect.gen(function* () {
|
|
217
|
-
const firstResponseTimeoutMs =
|
|
218
|
-
config.firstResponseTimeoutMs ?? FIRST_RESPONSE_TIMEOUT_MS;
|
|
219
202
|
const registry = yield* BackendRegistry;
|
|
220
203
|
// Detached forker for sync contexts (read-model commands, pruning) that
|
|
221
204
|
// preserves the manager's services instead of using the global runtime.
|
|
@@ -344,7 +327,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
344
327
|
};
|
|
345
328
|
|
|
346
329
|
const settle = (entry: Entry, outcome: RunOutcome) => {
|
|
347
|
-
clearWatchdog(entry);
|
|
348
330
|
const s = entry.snapshot;
|
|
349
331
|
const wasRestarting = entry.restarting === true;
|
|
350
332
|
entry.restarting = false;
|
|
@@ -403,44 +385,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
403
385
|
pruneSettled();
|
|
404
386
|
};
|
|
405
387
|
|
|
406
|
-
/** Stop the first-response watchdog (first response arrived / run settled). */
|
|
407
|
-
const clearWatchdog = (entry: Entry) => {
|
|
408
|
-
if (entry.watchdogTimer !== undefined) {
|
|
409
|
-
clearTimeout(entry.watchdogTimer);
|
|
410
|
-
entry.watchdogTimer = undefined;
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
/** Settle a run whose provider never emitted a first assistant response. */
|
|
415
|
-
const watchdogExpired = (entry: Entry) => {
|
|
416
|
-
entry.watchdogTimer = undefined;
|
|
417
|
-
if (!isBusy(entry)) return;
|
|
418
|
-
const model = entry.snapshot.meta.modelLabel;
|
|
419
|
-
settle(entry, {
|
|
420
|
-
_tag: "Failed",
|
|
421
|
-
errorText: `Agent received no assistant response event${model ? ` for ${model}` : ""} within ${formatWatchdogTimeout(firstResponseTimeoutMs)}; the provider request may be stalled. Retry the subagent.`,
|
|
422
|
-
});
|
|
423
|
-
// The stalled session cannot be trusted to abort cooperatively; dispose
|
|
424
|
-
// it like the abort-deadline path so it cannot revive into a zombie run.
|
|
425
|
-
const fiber = runDetached(
|
|
426
|
-
closeEntryScope(entry).pipe(
|
|
427
|
-
Effect.timeout(ENTRY_CLOSE_TIMEOUT_MS),
|
|
428
|
-
Effect.ignore,
|
|
429
|
-
),
|
|
430
|
-
);
|
|
431
|
-
cleanups.add(fiber);
|
|
432
|
-
fiber.addObserver(() => cleanups.delete(fiber));
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
/** Arm the first-response watchdog for the entry's current run. */
|
|
436
|
-
const armWatchdog = (entry: Entry) => {
|
|
437
|
-
clearWatchdog(entry);
|
|
438
|
-
entry.watchdogTimer = setTimeout(
|
|
439
|
-
() => watchdogExpired(entry),
|
|
440
|
-
firstResponseTimeoutMs,
|
|
441
|
-
);
|
|
442
|
-
};
|
|
443
|
-
|
|
444
388
|
const foldEvent = (entry: Entry, event: SubagentEvent) => {
|
|
445
389
|
const s = entry.snapshot;
|
|
446
390
|
switch (event._tag) {
|
|
@@ -451,7 +395,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
451
395
|
s.settledAt = undefined;
|
|
452
396
|
s.errorText = undefined;
|
|
453
397
|
s.structuredResult = undefined;
|
|
454
|
-
armWatchdog(entry);
|
|
455
398
|
break;
|
|
456
399
|
case "RunSettled":
|
|
457
400
|
settle(entry, event.outcome);
|
|
@@ -463,7 +406,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
463
406
|
});
|
|
464
407
|
break;
|
|
465
408
|
case "AssistantDelta": {
|
|
466
|
-
clearWatchdog(entry);
|
|
467
409
|
const live = s.liveAssistant ?? { text: "", thinking: "" };
|
|
468
410
|
s.liveAssistant =
|
|
469
411
|
event.kind === "text"
|
|
@@ -482,7 +424,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
482
424
|
break;
|
|
483
425
|
}
|
|
484
426
|
case "AssistantMessage":
|
|
485
|
-
clearWatchdog(entry);
|
|
486
427
|
appendTranscript(s, {
|
|
487
428
|
kind: "assistant",
|
|
488
429
|
parts: event.parts.map((part) =>
|
|
@@ -628,9 +569,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
628
569
|
liveToolMap: new Map(),
|
|
629
570
|
};
|
|
630
571
|
entries.set(id, entry);
|
|
631
|
-
// The run is live from the caller's perspective before RunStarted
|
|
632
|
-
// reaches the pump; guard that window too.
|
|
633
|
-
armWatchdog(entry);
|
|
634
572
|
|
|
635
573
|
// Pump: fold the event stream into the snapshot. Tied to the entry
|
|
636
574
|
// scope, so closing the scope stops it. If the stream ends while the
|
|
@@ -781,10 +719,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
781
719
|
// both pass the check in that window. Cleared by RunStarted/settle,
|
|
782
720
|
// or here when the backend rejects the send.
|
|
783
721
|
entry.restarting = true;
|
|
784
|
-
// A backend that accepts the send but never starts the run would
|
|
785
|
-
// hold the slot forever; guard the restart window the same way the
|
|
786
|
-
// spawn path guards its pre-RunStarted window.
|
|
787
|
-
armWatchdog(entry);
|
|
788
722
|
return entry.session.send(text).pipe(
|
|
789
723
|
Effect.onError(() =>
|
|
790
724
|
Effect.sync(() => {
|
|
@@ -800,7 +734,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
800
734
|
const disposeAll = Effect.gen(function* () {
|
|
801
735
|
disposed = true;
|
|
802
736
|
const all = [...entries.values()];
|
|
803
|
-
for (const entry of all) clearWatchdog(entry);
|
|
804
737
|
entries.clear();
|
|
805
738
|
yield* Effect.forEach(
|
|
806
739
|
all,
|
|
@@ -878,8 +811,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
878
811
|
});
|
|
879
812
|
|
|
880
813
|
export interface SubagentManagerConfig {
|
|
881
|
-
/** Test-only override for the first-response watchdog timeout. */
|
|
882
|
-
firstResponseTimeoutMs?: number;
|
|
883
814
|
/** Session-branch high-water marks restored by the extension host. */
|
|
884
815
|
initialModelCounter?: number;
|
|
885
816
|
initialBtwCounter?: number;
|
|
@@ -141,11 +141,11 @@ export const SUBAGENT_SPAWN_PROMPT_GUIDELINES = [
|
|
|
141
141
|
/** Model-facing schema descriptions for subagent_spawn task and execution options. */
|
|
142
142
|
export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
|
|
143
143
|
prompt:
|
|
144
|
-
"
|
|
144
|
+
"Self-contained task: include needed context, file paths, and expected report.",
|
|
145
145
|
name: "Short human-readable name shown in listings and the UI",
|
|
146
146
|
harness: 'Optional; "pi" is the only harness and the default.',
|
|
147
147
|
workingDir:
|
|
148
|
-
"
|
|
148
|
+
"Child cwd, absolute or relative to parent. Target project trust is checked separately.",
|
|
149
149
|
isolation:
|
|
150
150
|
'Use "worktree" for concurrent writers and tell the child to commit. See the Subagents Skill for lifecycle details.',
|
|
151
151
|
model:
|
|
@@ -45,6 +45,9 @@ export function createSubagentRuntime(config: SubagentManagerConfig = {}) {
|
|
|
45
45
|
|
|
46
46
|
export type SubagentRuntime = ReturnType<typeof createSubagentRuntime>;
|
|
47
47
|
|
|
48
|
+
/** Canonical interruption, distinct from a known startup failure. */
|
|
49
|
+
export class SubagentToolInterruptedError extends Error {}
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* Run an effect from an async tool handler. Typed failures and defects are
|
|
50
53
|
* converted to thrown Errors (what pi's tool contract expects); interruption
|
|
@@ -60,9 +63,13 @@ export async function runTool<A, E>(
|
|
|
60
63
|
options.signal ? { signal: options.signal } : undefined,
|
|
61
64
|
);
|
|
62
65
|
if (Exit.isSuccess(exit)) return exit.value;
|
|
63
|
-
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
64
|
-
throw new Error(options.interruptMessage ?? "Operation was aborted.");
|
|
65
|
-
}
|
|
66
66
|
const [first] = Cause.prettyErrors(exit.cause);
|
|
67
|
+
if (Cause.hasInterrupts(exit.cause)) {
|
|
68
|
+
const interrupted = options.interruptMessage ?? "Operation was aborted.";
|
|
69
|
+
const detail = Cause.hasInterruptsOnly(exit.cause)
|
|
70
|
+
? ""
|
|
71
|
+
: ` ${first?.message ?? Cause.pretty(exit.cause)}`;
|
|
72
|
+
throw new SubagentToolInterruptedError(`${interrupted}${detail}`);
|
|
73
|
+
}
|
|
67
74
|
throw new Error(first?.message ?? Cause.pretty(exit.cause));
|
|
68
75
|
}
|
package/extensions/web/index.ts
CHANGED
|
@@ -10,12 +10,23 @@ import {
|
|
|
10
10
|
PI_CODING_AGENT_ENTRY_ENV,
|
|
11
11
|
resolvePiCodingAgentEntry,
|
|
12
12
|
} from "../../web/host/pi-coding-agent-entry.ts";
|
|
13
|
+
import { TerminalTextSanitizer } from "../shared/terminal-text.ts";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
16
|
+
const WEB_ERROR_TAIL_MAX_BYTES = 8 * 1024;
|
|
17
|
+
|
|
18
|
+
interface WebProcessErrorStream {
|
|
19
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): this;
|
|
20
|
+
removeListener(
|
|
21
|
+
event: "data",
|
|
22
|
+
listener: (chunk: Buffer | string) => void,
|
|
23
|
+
): this;
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
export interface WebProcess {
|
|
17
27
|
readonly exitCode: number | null;
|
|
18
28
|
readonly signalCode: NodeJS.Signals | null;
|
|
29
|
+
readonly stderr: WebProcessErrorStream | null;
|
|
19
30
|
once(event: "error", listener: (error: Error) => void): this;
|
|
20
31
|
once(
|
|
21
32
|
event: "close",
|
|
@@ -28,7 +39,7 @@ interface SpawnWebOptions {
|
|
|
28
39
|
cwd: string;
|
|
29
40
|
env: NodeJS.ProcessEnv;
|
|
30
41
|
shell: false;
|
|
31
|
-
stdio: "inherit";
|
|
42
|
+
stdio: ["inherit", "inherit", "pipe"];
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
function webProcessEnvironment(
|
|
@@ -51,6 +62,7 @@ function webProcessEnvironment(
|
|
|
51
62
|
export interface WebCommandDependencies {
|
|
52
63
|
entrypoint: string;
|
|
53
64
|
spawn(command: string, args: string[], options: SpawnWebOptions): WebProcess;
|
|
65
|
+
writeStderr(chunk: Buffer | string): void;
|
|
54
66
|
clearTerminal(): void;
|
|
55
67
|
holdParentSigint(): () => void;
|
|
56
68
|
resolvePiCodingAgentEntry(): string | undefined;
|
|
@@ -58,7 +70,12 @@ export interface WebCommandDependencies {
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
type WebExit =
|
|
61
|
-
| {
|
|
73
|
+
| {
|
|
74
|
+
kind: "close";
|
|
75
|
+
code: number | null;
|
|
76
|
+
signal: NodeJS.Signals | null;
|
|
77
|
+
errorDetail?: string;
|
|
78
|
+
}
|
|
62
79
|
| { kind: "error"; error: Error };
|
|
63
80
|
|
|
64
81
|
interface ActiveWebProcess {
|
|
@@ -71,6 +88,9 @@ const defaultDependencies: WebCommandDependencies = {
|
|
|
71
88
|
spawn(command, args, options) {
|
|
72
89
|
return nodeSpawn(command, args, options);
|
|
73
90
|
},
|
|
91
|
+
writeStderr(chunk) {
|
|
92
|
+
process.stderr.write(chunk);
|
|
93
|
+
},
|
|
74
94
|
clearTerminal() {
|
|
75
95
|
process.stdout.write("\u001b[2J\u001b[H");
|
|
76
96
|
},
|
|
@@ -91,6 +111,31 @@ function delay(milliseconds: number) {
|
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
function boundedUtf8Tail(text: string, maxBytes: number) {
|
|
115
|
+
let bytes = 0;
|
|
116
|
+
let start = text.length;
|
|
117
|
+
for (const character of Array.from(text).reverse()) {
|
|
118
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
119
|
+
if (bytes + characterBytes > maxBytes) break;
|
|
120
|
+
bytes += characterBytes;
|
|
121
|
+
start -= character.length;
|
|
122
|
+
}
|
|
123
|
+
return text.slice(start);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function actionableWebError(stderr: string) {
|
|
127
|
+
const lines = stderr
|
|
128
|
+
.split(/\r?\n/u)
|
|
129
|
+
.map((line) => line.trim())
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
const newestFirst = [...lines].reverse();
|
|
132
|
+
return (
|
|
133
|
+
newestFirst.find((line) =>
|
|
134
|
+
line.includes("Failed to start OpenPI Web Workbench:"),
|
|
135
|
+
) ?? newestFirst.find((line) => /^(?:Error|Failed):/u.test(line))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
94
139
|
async function stopWebProcess(active: ActiveWebProcess, timeoutMs: number) {
|
|
95
140
|
if (active.child.exitCode !== null || active.child.signalCode !== null)
|
|
96
141
|
return;
|
|
@@ -116,15 +161,26 @@ function runWebInForeground(
|
|
|
116
161
|
return ctx.ui.custom<WebExit>((tui, _theme, _keybindings, done) => {
|
|
117
162
|
let finished = false;
|
|
118
163
|
let tuiStopped = false;
|
|
164
|
+
let stderrTail = "";
|
|
165
|
+
let childStderr: WebProcessErrorStream | null | undefined;
|
|
119
166
|
let resolveClosed = () => {};
|
|
120
167
|
const closed = new Promise<void>((resolve) => {
|
|
121
168
|
resolveClosed = resolve;
|
|
122
169
|
});
|
|
123
170
|
const releaseParentSigint = dependencies.holdParentSigint();
|
|
171
|
+
const sanitizer = new TerminalTextSanitizer();
|
|
172
|
+
const captureStderr = (chunk: Buffer | string) => {
|
|
173
|
+
dependencies.writeStderr(chunk);
|
|
174
|
+
stderrTail = boundedUtf8Tail(
|
|
175
|
+
`${stderrTail}${sanitizer.push(String(chunk))}`,
|
|
176
|
+
WEB_ERROR_TAIL_MAX_BYTES,
|
|
177
|
+
);
|
|
178
|
+
};
|
|
124
179
|
|
|
125
180
|
const finish = (result: WebExit) => {
|
|
126
181
|
if (finished) return;
|
|
127
182
|
finished = true;
|
|
183
|
+
childStderr?.removeListener("data", captureStderr);
|
|
128
184
|
releaseParentSigint();
|
|
129
185
|
setActive(undefined);
|
|
130
186
|
resolveClosed();
|
|
@@ -147,13 +203,20 @@ function runWebInForeground(
|
|
|
147
203
|
cwd: childCwd,
|
|
148
204
|
env: webProcessEnvironment(childCwd, piCodingAgentEntry),
|
|
149
205
|
shell: false,
|
|
150
|
-
stdio: "inherit",
|
|
206
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
151
207
|
},
|
|
152
208
|
);
|
|
209
|
+
childStderr = child.stderr;
|
|
210
|
+
childStderr?.on("data", captureStderr);
|
|
153
211
|
setActive({ child, closed });
|
|
154
212
|
child.once("error", (error) => finish({ kind: "error", error }));
|
|
155
213
|
child.once("close", (code, signal) =>
|
|
156
|
-
finish({
|
|
214
|
+
finish({
|
|
215
|
+
kind: "close",
|
|
216
|
+
code,
|
|
217
|
+
signal,
|
|
218
|
+
errorDetail: actionableWebError(stderrTail),
|
|
219
|
+
}),
|
|
157
220
|
);
|
|
158
221
|
} catch (error) {
|
|
159
222
|
finish({
|
|
@@ -242,7 +305,8 @@ export default function web(
|
|
|
242
305
|
}
|
|
243
306
|
if (result.code !== 0) {
|
|
244
307
|
ctx.ui.notify(
|
|
245
|
-
|
|
308
|
+
result.errorDetail ??
|
|
309
|
+
`OpenPI Web Workbench exited with code ${result.code ?? "unknown"}.`,
|
|
246
310
|
"error",
|
|
247
311
|
);
|
|
248
312
|
return;
|