@tt-a1i/openpi 0.3.1 → 0.5.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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -3,54 +3,127 @@
|
|
|
3
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { effectiveChildToolAllowlist } from "../../shared/child-session.ts";
|
|
6
|
-
import
|
|
6
|
+
import { SUBAGENT_ROLE_NAMES } from "../../shared/subagent-roles.ts";
|
|
7
|
+
import { type AgentType, READ_ONLY_AGENT_TOOLS } from "./agent-types.ts";
|
|
8
|
+
import { BACKEND_NAMES, REASONING_EFFORTS } from "./domain.ts";
|
|
7
9
|
import { MAX_RUNNING } from "./manager.ts";
|
|
8
10
|
|
|
11
|
+
export const SUBAGENT_SCHEMA_BUDGETS = Object.freeze({
|
|
12
|
+
rolePurposeBytes: 240,
|
|
13
|
+
roleDirectoryBytes: 4 * 1024,
|
|
14
|
+
defaultSpawnSurfaceBytes: 2.5 * 1024,
|
|
15
|
+
maximumSpawnSurfaceBytes: 16 * 1024,
|
|
16
|
+
});
|
|
17
|
+
|
|
9
18
|
/** Describes subagent_spawn, including the fixed concurrency cap. */
|
|
10
19
|
export const SUBAGENT_SPAWN_TOOL_DESCRIPTION =
|
|
11
|
-
"Spawn a background
|
|
20
|
+
"Spawn a background in-process Pi subagent with its own context, child-safe tools, and normal host permissions. Returns immediately; its final result is delivered automatically. The child cannot see this conversation, ask the user, or orchestrate agents/workflows. Use only trusted working directories. " +
|
|
12
21
|
`Max ${MAX_RUNNING} subagents can be running at once.`;
|
|
13
22
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
/** UTF-8 bounded, whitespace-normalized text for the parent-facing roster. */
|
|
24
|
+
function boundedPurpose(description: string) {
|
|
25
|
+
const normalized = description.trim().replace(/\s+/gu, " ");
|
|
26
|
+
const limit = SUBAGENT_SCHEMA_BUDGETS.rolePurposeBytes;
|
|
27
|
+
if (Buffer.byteLength(normalized, "utf8") <= limit) return normalized;
|
|
28
|
+
const suffix = "…";
|
|
29
|
+
let used = Buffer.byteLength(suffix, "utf8");
|
|
30
|
+
let output = "";
|
|
31
|
+
for (const character of normalized) {
|
|
32
|
+
const bytes = Buffer.byteLength(character, "utf8");
|
|
33
|
+
if (used + bytes > limit) break;
|
|
34
|
+
output += character;
|
|
35
|
+
used += bytes;
|
|
36
|
+
}
|
|
37
|
+
return output.trimEnd() + suffix;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareNames(left: string, right: string) {
|
|
41
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
23
42
|
}
|
|
24
43
|
|
|
25
|
-
/**
|
|
44
|
+
/** Built-ins stay familiar; project/global additions are stable by name. */
|
|
45
|
+
function orderedAgentTypes(agentTypes: readonly AgentType[]) {
|
|
46
|
+
const builtInOrder = new Map<string, number>(
|
|
47
|
+
SUBAGENT_ROLE_NAMES.map((name, index) => [name, index]),
|
|
48
|
+
);
|
|
49
|
+
return [...agentTypes].sort((left, right) => {
|
|
50
|
+
const leftIndex = builtInOrder.get(left.name);
|
|
51
|
+
const rightIndex = builtInOrder.get(right.name);
|
|
52
|
+
if (leftIndex !== undefined || rightIndex !== undefined) {
|
|
53
|
+
if (leftIndex === undefined) return 1;
|
|
54
|
+
if (rightIndex === undefined) return -1;
|
|
55
|
+
return leftIndex - rightIndex;
|
|
56
|
+
}
|
|
57
|
+
return compareNames(left.name, right.name);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function capabilityClass(agentType: AgentType) {
|
|
62
|
+
if (agentType.tools === undefined) return "inherited-tools";
|
|
63
|
+
const tools = effectiveChildToolAllowlist(agentType.tools) ?? [];
|
|
64
|
+
if (tools.length === 0) return "no-tools";
|
|
65
|
+
if (tools.every((tool) => READ_ONLY_AGENT_TOOLS.includes(tool))) {
|
|
66
|
+
return "read-only";
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
tools.some((tool) => tool === "bash" || tool === "edit" || tool === "write")
|
|
70
|
+
) {
|
|
71
|
+
return "workspace-write";
|
|
72
|
+
}
|
|
73
|
+
// Third-party child-safe tools may still have side effects, so only claim
|
|
74
|
+
// the enforceable fact: this preset has a restricted tool set.
|
|
75
|
+
return "restricted";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function agentTypeSummary(agentType: AgentType) {
|
|
79
|
+
const effort = agentType.reasoningEffort
|
|
80
|
+
? ` [default reasoning_effort: ${agentType.reasoningEffort}]`
|
|
81
|
+
: "";
|
|
82
|
+
return `"${agentType.name}" — ${boundedPurpose(agentType.description)} [${capabilityClass(agentType)}]${effort}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A deterministic, bounded selection index; execution details stay in Skill. */
|
|
26
86
|
export function buildAgentTypeParameterDescription(
|
|
27
87
|
agentTypes: readonly AgentType[],
|
|
28
88
|
) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
const ordered = orderedAgentTypes(agentTypes);
|
|
90
|
+
const intro =
|
|
91
|
+
"Optional named preset for the child prompt and capability boundary. Omit for a general-purpose child. Available: ";
|
|
92
|
+
const outro =
|
|
93
|
+
" Preset restrictions are enforced; read the Subagents Skill or role file for full details.";
|
|
94
|
+
const entries: string[] = [];
|
|
95
|
+
for (const agentType of ordered) {
|
|
96
|
+
const summary = agentTypeSummary(agentType);
|
|
97
|
+
const next = [...entries, summary];
|
|
98
|
+
const omitted = ordered.length - next.length;
|
|
99
|
+
const omission = omitted
|
|
100
|
+
? `; ${omitted} presets omitted from this summary; their exact enum names remain valid.`
|
|
101
|
+
: ".";
|
|
102
|
+
const candidate = `${intro}${next.join("; ")}${omission}${outro}`;
|
|
103
|
+
if (
|
|
104
|
+
Buffer.byteLength(candidate, "utf8") >
|
|
105
|
+
SUBAGENT_SCHEMA_BUDGETS.roleDirectoryBytes
|
|
106
|
+
) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
entries.push(summary);
|
|
110
|
+
}
|
|
111
|
+
const omitted = ordered.length - entries.length;
|
|
112
|
+
const omission = omitted
|
|
113
|
+
? `; ${omitted} presets omitted from this summary; their exact enum names remain valid.`
|
|
114
|
+
: ".";
|
|
115
|
+
return `${intro}${entries.join("; ")}${omission}${outro}`;
|
|
44
116
|
}
|
|
45
117
|
|
|
46
118
|
/** Generated schema for the dynamic agent-type roster. */
|
|
47
119
|
export function createAgentTypeParameterSchema(
|
|
48
120
|
agentTypes: readonly AgentType[],
|
|
49
121
|
) {
|
|
122
|
+
const ordered = orderedAgentTypes(agentTypes);
|
|
50
123
|
return Type.Optional(
|
|
51
124
|
StringEnum(
|
|
52
|
-
|
|
53
|
-
{ description: buildAgentTypeParameterDescription(
|
|
125
|
+
ordered.map((agentType) => agentType.name) as [string, ...string[]],
|
|
126
|
+
{ description: buildAgentTypeParameterDescription(ordered) },
|
|
54
127
|
),
|
|
55
128
|
);
|
|
56
129
|
}
|
|
@@ -61,27 +134,69 @@ export const SUBAGENT_SPAWN_PROMPT_SNIPPET =
|
|
|
61
134
|
|
|
62
135
|
/** Guides the parent model to delegate standalone tasks and avoid unnecessary blocking waits. */
|
|
63
136
|
export const SUBAGENT_SPAWN_PROMPT_GUIDELINES = [
|
|
64
|
-
"
|
|
65
|
-
"After
|
|
137
|
+
"Delegate substantial independent work; do a single lookup or edit inline.",
|
|
138
|
+
"After spawning, continue independent work. In an interactive session, end your turn when none remains; automatic delivery will re-invoke you. Do not call subagent_wait merely because the next step depends on the result; use it only when the user explicitly asks to keep the response open, or the same non-interactive invocation must return the result. Never poll or guess the result.",
|
|
66
139
|
];
|
|
67
140
|
|
|
68
141
|
/** Model-facing schema descriptions for subagent_spawn task and execution options. */
|
|
69
142
|
export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
|
|
70
143
|
prompt:
|
|
71
144
|
"Task prompt for the subagent. Must be self-contained: include all needed context, file paths, and what to report back.",
|
|
72
|
-
name: "Short human-readable name
|
|
73
|
-
harness:
|
|
74
|
-
'Optional. The only harness is "pi" (an in-process Pi session that inherits this environment), which is the default; you can omit this.',
|
|
145
|
+
name: "Short human-readable name shown in listings and the UI",
|
|
146
|
+
harness: 'Optional; "pi" is the only harness and the default.',
|
|
75
147
|
workingDir:
|
|
76
|
-
"Trusted working directory
|
|
148
|
+
"Trusted child working directory; defaults to the current directory",
|
|
77
149
|
isolation:
|
|
78
|
-
'
|
|
150
|
+
'Use "worktree" for concurrent writers and tell the child to commit. See the Subagents Skill for lifecycle details.',
|
|
79
151
|
model:
|
|
80
|
-
'Optional
|
|
152
|
+
'Optional "provider/model-id" or current-provider model override. Omit to use the preset, configured role, or parent default. Never guess a model name.',
|
|
81
153
|
reasoningEffort:
|
|
82
|
-
"Optional thinking level
|
|
154
|
+
"Optional child thinking level. Honor the user's requested level. Otherwise choose a level supported by the resolved child model based on the selected role and task difficulty. An explicit value overrides a role default.",
|
|
83
155
|
};
|
|
84
156
|
|
|
157
|
+
/** The exact name/description/wire-schema source used by registration/tests. */
|
|
158
|
+
export function createSubagentSpawnToolSurface(
|
|
159
|
+
agentTypes: readonly AgentType[],
|
|
160
|
+
) {
|
|
161
|
+
return {
|
|
162
|
+
description: SUBAGENT_SPAWN_TOOL_DESCRIPTION,
|
|
163
|
+
parameters: Type.Object({
|
|
164
|
+
agent_type: createAgentTypeParameterSchema(agentTypes),
|
|
165
|
+
prompt: Type.String({
|
|
166
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt,
|
|
167
|
+
}),
|
|
168
|
+
name: Type.String({
|
|
169
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name,
|
|
170
|
+
}),
|
|
171
|
+
harness: Type.Optional(
|
|
172
|
+
StringEnum(BACKEND_NAMES, {
|
|
173
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness,
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
working_dir: Type.Optional(
|
|
177
|
+
Type.String({
|
|
178
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir,
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
isolation: Type.Optional(
|
|
182
|
+
StringEnum(["worktree"] as const, {
|
|
183
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation,
|
|
184
|
+
}),
|
|
185
|
+
),
|
|
186
|
+
model: Type.Optional(
|
|
187
|
+
Type.String({
|
|
188
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model,
|
|
189
|
+
}),
|
|
190
|
+
),
|
|
191
|
+
reasoning_effort: Type.Optional(
|
|
192
|
+
StringEnum(REASONING_EFFORTS, {
|
|
193
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
|
|
194
|
+
}),
|
|
195
|
+
),
|
|
196
|
+
}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
85
200
|
/** Builds the subagent_spawn result that tells the parent model how to continue or inspect the child. */
|
|
86
201
|
export function buildSubagentSpawnResult(options: {
|
|
87
202
|
id: string;
|
|
@@ -170,8 +285,11 @@ export const SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS = {
|
|
|
170
285
|
export const SUBAGENT_LIST_TOOL_DESCRIPTION =
|
|
171
286
|
"List all subagents (running and finished) with their status.";
|
|
172
287
|
|
|
173
|
-
|
|
174
|
-
|
|
288
|
+
const SUBAGENT_RESULT_TRANSPORT_INSTRUCTION =
|
|
289
|
+
"(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)";
|
|
290
|
+
|
|
291
|
+
/** Builds the user-visible child completion/failure projection. */
|
|
292
|
+
export function buildSubagentResultDisplayMessage(options: {
|
|
175
293
|
id: string;
|
|
176
294
|
title: string;
|
|
177
295
|
status: "running" | "done" | "error";
|
|
@@ -182,9 +300,34 @@ export function buildSubagentResultMessage(options: {
|
|
|
182
300
|
let text = `Subagent ${options.id} "${options.title}" ${verb}.`;
|
|
183
301
|
if (options.errorText) text += `\nError: ${options.errorText}`;
|
|
184
302
|
text += `\n\n${options.output}`;
|
|
303
|
+
return text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Builds the child completion/failure wrapper injected into the parent model's context. */
|
|
307
|
+
export function buildSubagentResultMessage(options: {
|
|
308
|
+
id: string;
|
|
309
|
+
title: string;
|
|
310
|
+
status: "running" | "done" | "error";
|
|
311
|
+
errorText?: string;
|
|
312
|
+
output: string;
|
|
313
|
+
}) {
|
|
314
|
+
let text = buildSubagentResultDisplayMessage(options);
|
|
185
315
|
// This message is already displayed to the user, so tell the parent to act on
|
|
186
316
|
// it rather than reprint it verbatim.
|
|
187
|
-
text +=
|
|
188
|
-
"\n\n(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)";
|
|
317
|
+
text += `\n\n${SUBAGENT_RESULT_TRANSPORT_INSTRUCTION}`;
|
|
189
318
|
return text;
|
|
190
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Remove the transport-only suffix from results persisted before the split. */
|
|
322
|
+
export function stripSubagentResultTransportInstruction(content: string) {
|
|
323
|
+
const separator = `\n\n${SUBAGENT_RESULT_TRANSPORT_INSTRUCTION}`;
|
|
324
|
+
const withoutBatchedSeparators = content.replaceAll(
|
|
325
|
+
`${separator}\n\nSubagent `,
|
|
326
|
+
"\n\nSubagent ",
|
|
327
|
+
);
|
|
328
|
+
return (
|
|
329
|
+
withoutBatchedSeparators.endsWith(separator)
|
|
330
|
+
? withoutBatchedSeparators.slice(0, -separator.length)
|
|
331
|
+
: withoutBatchedSeparators
|
|
332
|
+
).trimEnd();
|
|
333
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
formatSize,
|
|
6
|
+
truncateHead,
|
|
7
|
+
truncateTail,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
const HEAD_SHARE = 0.75;
|
|
11
|
+
const RESULT_ARTIFACT_DIR = ["cache", "openpi", "subagent-results"];
|
|
12
|
+
|
|
13
|
+
export interface ResultProjectionOptions {
|
|
14
|
+
readonly maxBytes: number;
|
|
15
|
+
readonly maxLines: number;
|
|
16
|
+
readonly writeArtifact: (content: string) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResultProjection {
|
|
20
|
+
readonly text: string;
|
|
21
|
+
readonly truncated: boolean;
|
|
22
|
+
readonly artifactPath?: string;
|
|
23
|
+
readonly artifactSaveFailed?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sliceStartToUtf8Bytes(content: string, maxBytes: number) {
|
|
27
|
+
const bytes = Buffer.from(content, "utf8");
|
|
28
|
+
if (bytes.length <= maxBytes) return content;
|
|
29
|
+
let end = maxBytes;
|
|
30
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
31
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDirectory(parent: string, name: string) {
|
|
35
|
+
const directory = path.join(parent, name);
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(directory, { mode: 0o700 });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
40
|
+
}
|
|
41
|
+
const stat = lstatSync(directory);
|
|
42
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
43
|
+
throw new Error(`Unsafe result artifact directory: ${directory}`);
|
|
44
|
+
}
|
|
45
|
+
return directory;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Persist one immutable, content-addressed final answer below Pi's cache.
|
|
50
|
+
* Model-authored titles and paths never participate in the filename.
|
|
51
|
+
*/
|
|
52
|
+
export function persistResultArtifact(agentDir: string, content: string) {
|
|
53
|
+
let directory = path.resolve(agentDir);
|
|
54
|
+
for (const segment of RESULT_ARTIFACT_DIR) {
|
|
55
|
+
directory = ensureDirectory(directory, segment);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const digest = createHash("sha256").update(content).digest("hex");
|
|
59
|
+
const artifactPath = path.join(directory, `${digest}.txt`);
|
|
60
|
+
try {
|
|
61
|
+
writeFileSync(artifactPath, content, {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
flag: "wx",
|
|
64
|
+
mode: 0o600,
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
68
|
+
const stat = lstatSync(artifactPath);
|
|
69
|
+
if (
|
|
70
|
+
!stat.isFile() ||
|
|
71
|
+
stat.isSymbolicLink() ||
|
|
72
|
+
readFileSync(artifactPath, "utf8") !== content
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(`Result artifact collision: ${artifactPath}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return artifactPath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the single model-visible projection used by automatic delivery and
|
|
82
|
+
* explicit waits. Short answers pass through byte-for-byte. Long answers keep
|
|
83
|
+
* both decision context at the start and verdict/evidence at the end, while a
|
|
84
|
+
* plain-text artifact preserves the exact final answer for Pi's native read.
|
|
85
|
+
*/
|
|
86
|
+
export function projectResult(
|
|
87
|
+
content: string,
|
|
88
|
+
options: ResultProjectionOptions,
|
|
89
|
+
): ResultProjection {
|
|
90
|
+
const probe = truncateHead(content, {
|
|
91
|
+
maxBytes: options.maxBytes,
|
|
92
|
+
maxLines: options.maxLines,
|
|
93
|
+
});
|
|
94
|
+
if (!probe.truncated) return { text: content, truncated: false };
|
|
95
|
+
|
|
96
|
+
const headLines = Math.max(1, Math.floor(options.maxLines * HEAD_SHARE));
|
|
97
|
+
const tailLines = Math.max(1, options.maxLines - headLines);
|
|
98
|
+
|
|
99
|
+
let artifactPath: string | undefined;
|
|
100
|
+
let artifactSaveFailed = false;
|
|
101
|
+
try {
|
|
102
|
+
artifactPath = options.writeArtifact(content);
|
|
103
|
+
} catch {
|
|
104
|
+
// Delivery is more important than the optional recovery cache. The footer
|
|
105
|
+
// below stays explicit so a failed write never advertises a false path.
|
|
106
|
+
artifactSaveFailed = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let bodyBudget = options.maxBytes;
|
|
110
|
+
let text = "";
|
|
111
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
112
|
+
const headBytes = Math.max(1, Math.floor(bodyBudget * HEAD_SHARE));
|
|
113
|
+
const tailBytes = Math.max(1, bodyBudget - headBytes);
|
|
114
|
+
const headResult = truncateHead(content, {
|
|
115
|
+
maxBytes: headBytes,
|
|
116
|
+
maxLines: headLines,
|
|
117
|
+
});
|
|
118
|
+
const tailResult = truncateTail(content, {
|
|
119
|
+
maxBytes: tailBytes,
|
|
120
|
+
maxLines: tailLines,
|
|
121
|
+
});
|
|
122
|
+
const head =
|
|
123
|
+
headResult.content || sliceStartToUtf8Bytes(content, headBytes);
|
|
124
|
+
const tail = tailResult.content;
|
|
125
|
+
const shownBytes =
|
|
126
|
+
Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
|
|
127
|
+
const recovery = artifactPath
|
|
128
|
+
? `Full final answer: ${JSON.stringify(artifactPath)}\nUse Pi's read tool with path=${JSON.stringify(artifactPath)}, offset=${Math.max(1, headResult.outputLines + 1)}, limit=200 to inspect the omitted middle; adjust offset to continue.`
|
|
129
|
+
: "Full final answer could not be saved; only the head and tail above are available.";
|
|
130
|
+
const footer =
|
|
131
|
+
`[Output truncated: showing ${formatSize(shownBytes)} of ${formatSize(probe.totalBytes)} ` +
|
|
132
|
+
`across the head and tail (${probe.totalLines} total lines).\n${recovery}]`;
|
|
133
|
+
text = `${head}\n\n[... middle omitted ...]\n\n${tail}\n\n${footer}`;
|
|
134
|
+
|
|
135
|
+
const overflow = Buffer.byteLength(text, "utf8") - options.maxBytes;
|
|
136
|
+
if (overflow <= 0 || bodyBudget <= overflow + 2) break;
|
|
137
|
+
bodyBudget -= overflow;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
text,
|
|
142
|
+
truncated: true,
|
|
143
|
+
...(artifactPath ? { artifactPath } : {}),
|
|
144
|
+
...(artifactSaveFailed ? { artifactSaveFailed: true } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
|
|
1
3
|
export interface SubagentResultDeliveryOptions<T> {
|
|
2
4
|
/** True only when the parent has no run or queued continuation in flight. */
|
|
3
5
|
readonly isIdle: () => boolean;
|
|
@@ -46,7 +48,7 @@ export function createSubagentResultDelivery<T extends { id: string }>(
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
const queue = {
|
|
50
52
|
defer(result: T) {
|
|
51
53
|
pending.set(result.id, result);
|
|
52
54
|
if (options.isIdle()) flush();
|
|
@@ -61,5 +63,9 @@ export function createSubagentResultDelivery<T extends { id: string }>(
|
|
|
61
63
|
clear() {
|
|
62
64
|
pending.clear();
|
|
63
65
|
},
|
|
66
|
+
size() {
|
|
67
|
+
return pending.size;
|
|
68
|
+
},
|
|
64
69
|
};
|
|
70
|
+
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
65
71
|
}
|
|
@@ -11,19 +11,36 @@ import { BackendRegistry, type SubagentBackend } from "./backend.ts";
|
|
|
11
11
|
import { piBackend } from "./backends/pi.ts";
|
|
12
12
|
import type { BackendName } from "./domain.ts";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Test-only injection seam for extension-level tests that drive real spawns
|
|
16
|
+
* without a child pi session: production never sets it. The underscore-prefixed
|
|
17
|
+
* setter name makes any accidental production use self-evidently wrong.
|
|
18
|
+
*/
|
|
19
|
+
let testBackends: readonly SubagentBackend[] | undefined;
|
|
20
|
+
|
|
21
|
+
/** Test-only: replace the backends the manager can spawn against. */
|
|
22
|
+
export function __setSubagentTestBackends(
|
|
23
|
+
backends: readonly SubagentBackend[] | undefined,
|
|
24
|
+
) {
|
|
25
|
+
testBackends = backends;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const BackendRegistryLive = Layer.sync(BackendRegistry, () => {
|
|
15
|
-
const backends: SubagentBackend[] = [piBackend];
|
|
29
|
+
const backends: readonly SubagentBackend[] = testBackends ?? [piBackend];
|
|
16
30
|
return new Map<BackendName, SubagentBackend>(
|
|
17
31
|
backends.map((backend) => [backend.name, backend]),
|
|
18
32
|
);
|
|
19
33
|
});
|
|
20
34
|
|
|
21
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
makeSubagentManagerLayer,
|
|
37
|
+
type SubagentManagerConfig,
|
|
38
|
+
} from "./manager.ts";
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
export function createSubagentRuntime(config: SubagentManagerConfig = {}) {
|
|
41
|
+
return ManagedRuntime.make(
|
|
42
|
+
makeSubagentManagerLayer(config).pipe(Layer.provide(BackendRegistryLive)),
|
|
43
|
+
);
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
export type SubagentRuntime = ReturnType<typeof createSubagentRuntime>;
|