@tt-a1i/openpi 0.3.0 → 0.4.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 +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +35 -44
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -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/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +216 -170
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- 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;
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
}
|
|
24
|
+
|
|
25
|
+
function sliceStartToUtf8Bytes(content: string, maxBytes: number) {
|
|
26
|
+
const bytes = Buffer.from(content, "utf8");
|
|
27
|
+
if (bytes.length <= maxBytes) return content;
|
|
28
|
+
let end = maxBytes;
|
|
29
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
30
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureDirectory(parent: string, name: string) {
|
|
34
|
+
const directory = path.join(parent, name);
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(directory, { mode: 0o700 });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
39
|
+
}
|
|
40
|
+
const stat = lstatSync(directory);
|
|
41
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
42
|
+
throw new Error(`Unsafe result artifact directory: ${directory}`);
|
|
43
|
+
}
|
|
44
|
+
return directory;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Persist one immutable, content-addressed final answer below Pi's cache.
|
|
49
|
+
* Model-authored titles and paths never participate in the filename.
|
|
50
|
+
*/
|
|
51
|
+
export function persistResultArtifact(agentDir: string, content: string) {
|
|
52
|
+
let directory = path.resolve(agentDir);
|
|
53
|
+
for (const segment of RESULT_ARTIFACT_DIR) {
|
|
54
|
+
directory = ensureDirectory(directory, segment);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const digest = createHash("sha256").update(content).digest("hex");
|
|
58
|
+
const artifactPath = path.join(directory, `${digest}.txt`);
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(artifactPath, content, {
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
flag: "wx",
|
|
63
|
+
mode: 0o600,
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
67
|
+
const stat = lstatSync(artifactPath);
|
|
68
|
+
if (
|
|
69
|
+
!stat.isFile() ||
|
|
70
|
+
stat.isSymbolicLink() ||
|
|
71
|
+
readFileSync(artifactPath, "utf8") !== content
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`Result artifact collision: ${artifactPath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return artifactPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the single model-visible projection used by automatic delivery and
|
|
81
|
+
* explicit waits. Short answers pass through byte-for-byte. Long answers keep
|
|
82
|
+
* both decision context at the start and verdict/evidence at the end, while a
|
|
83
|
+
* plain-text artifact preserves the exact final answer for Pi's native read.
|
|
84
|
+
*/
|
|
85
|
+
export function projectResult(
|
|
86
|
+
content: string,
|
|
87
|
+
options: ResultProjectionOptions,
|
|
88
|
+
): ResultProjection {
|
|
89
|
+
const probe = truncateHead(content, {
|
|
90
|
+
maxBytes: options.maxBytes,
|
|
91
|
+
maxLines: options.maxLines,
|
|
92
|
+
});
|
|
93
|
+
if (!probe.truncated) return { text: content, truncated: false };
|
|
94
|
+
|
|
95
|
+
const headLines = Math.max(1, Math.floor(options.maxLines * HEAD_SHARE));
|
|
96
|
+
const tailLines = Math.max(1, options.maxLines - headLines);
|
|
97
|
+
|
|
98
|
+
let artifactPath: string | undefined;
|
|
99
|
+
try {
|
|
100
|
+
artifactPath = options.writeArtifact(content);
|
|
101
|
+
} catch {
|
|
102
|
+
// Delivery is more important than the optional recovery cache. The footer
|
|
103
|
+
// below stays explicit so a failed write never advertises a false path.
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let bodyBudget = options.maxBytes;
|
|
107
|
+
let text = "";
|
|
108
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
109
|
+
const headBytes = Math.max(1, Math.floor(bodyBudget * HEAD_SHARE));
|
|
110
|
+
const tailBytes = Math.max(1, bodyBudget - headBytes);
|
|
111
|
+
const headResult = truncateHead(content, {
|
|
112
|
+
maxBytes: headBytes,
|
|
113
|
+
maxLines: headLines,
|
|
114
|
+
});
|
|
115
|
+
const tailResult = truncateTail(content, {
|
|
116
|
+
maxBytes: tailBytes,
|
|
117
|
+
maxLines: tailLines,
|
|
118
|
+
});
|
|
119
|
+
const head =
|
|
120
|
+
headResult.content || sliceStartToUtf8Bytes(content, headBytes);
|
|
121
|
+
const tail = tailResult.content;
|
|
122
|
+
const shownBytes =
|
|
123
|
+
Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
|
|
124
|
+
const recovery = artifactPath
|
|
125
|
+
? `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.`
|
|
126
|
+
: "Full final answer could not be saved; only the head and tail above are available.";
|
|
127
|
+
const footer =
|
|
128
|
+
`[Output truncated: showing ${formatSize(shownBytes)} of ${formatSize(probe.totalBytes)} ` +
|
|
129
|
+
`across the head and tail (${probe.totalLines} total lines).\n${recovery}]`;
|
|
130
|
+
text = `${head}\n\n[... middle omitted ...]\n\n${tail}\n\n${footer}`;
|
|
131
|
+
|
|
132
|
+
const overflow = Buffer.byteLength(text, "utf8") - options.maxBytes;
|
|
133
|
+
if (overflow <= 0 || bodyBudget <= overflow + 2) break;
|
|
134
|
+
bodyBudget -= overflow;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
text,
|
|
139
|
+
truncated: true,
|
|
140
|
+
...(artifactPath ? { artifactPath } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -1,17 +1,62 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface SubagentResultDeliveryOptions<T> {
|
|
2
|
+
/** True only when the parent has no run or queued continuation in flight. */
|
|
3
|
+
readonly isIdle: () => boolean;
|
|
4
|
+
/** Deliver one drained batch and wake the parent. */
|
|
5
|
+
readonly deliver: (results: readonly T[]) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One-shot result delivery for fire-and-forget subagents.
|
|
10
|
+
*
|
|
11
|
+
* The tool contract promises that a settled child re-invokes the parent. A
|
|
12
|
+
* child that settles while the parent is busy therefore remains retractable
|
|
13
|
+
* until the parent's `agent_settled` event, but it must never be downgraded to
|
|
14
|
+
* a `nextTurn` message that needs another user prompt. There are two symmetric
|
|
15
|
+
* wake-up edges so no ordering can lose the notification:
|
|
16
|
+
*
|
|
17
|
+
* 1. child settles after the parent became idle -> `defer` flushes now;
|
|
18
|
+
* 2. parent settles after the child -> `parentSettled` flushes the batch.
|
|
19
|
+
*
|
|
20
|
+
* The parent boundary wakes even if an earlier extension handler has already
|
|
21
|
+
* started another turn: Pi queues the follow-up into that active run.
|
|
22
|
+
*
|
|
23
|
+
* The Map is the one-shot gate: `subagent_wait` may consume a result before it
|
|
24
|
+
* is delivered, and whichever path drains first prevents duplicate delivery.
|
|
25
|
+
*/
|
|
26
|
+
export function createSubagentResultDelivery<T extends { id: string }>(
|
|
27
|
+
options: SubagentResultDeliveryOptions<T>,
|
|
28
|
+
) {
|
|
2
29
|
const pending = new Map<string, T>();
|
|
3
30
|
|
|
31
|
+
const flush = () => {
|
|
32
|
+
if (pending.size === 0) return;
|
|
33
|
+
const results = [...pending.values()];
|
|
34
|
+
pending.clear();
|
|
35
|
+
try {
|
|
36
|
+
options.deliver(results);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// A synchronous session teardown may reject append/send. Preserve the
|
|
39
|
+
// original batch ahead of anything deferred re-entrantly while delivery
|
|
40
|
+
// ran, so a later boundary can retry without loss or reordering.
|
|
41
|
+
const current = [...pending.values()];
|
|
42
|
+
pending.clear();
|
|
43
|
+
for (const result of results) pending.set(result.id, result);
|
|
44
|
+
for (const result of current) pending.set(result.id, result);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
4
49
|
return {
|
|
5
50
|
defer(result: T) {
|
|
6
51
|
pending.set(result.id, result);
|
|
52
|
+
if (options.isIdle()) flush();
|
|
7
53
|
},
|
|
8
54
|
consume(ids: Iterable<string>) {
|
|
9
55
|
for (const id of ids) pending.delete(id);
|
|
10
56
|
},
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return results;
|
|
57
|
+
/** Flush at the authoritative parent boundary. */
|
|
58
|
+
parentSettled() {
|
|
59
|
+
flush();
|
|
15
60
|
},
|
|
16
61
|
clear() {
|
|
17
62
|
pending.clear();
|
|
@@ -18,12 +18,15 @@ const BackendRegistryLive = Layer.sync(BackendRegistry, () => {
|
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
makeSubagentManagerLayer,
|
|
23
|
+
type SubagentManagerConfig,
|
|
24
|
+
} from "./manager.ts";
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
export function createSubagentRuntime(config: SubagentManagerConfig = {}) {
|
|
27
|
+
return ManagedRuntime.make(
|
|
28
|
+
makeSubagentManagerLayer(config).pipe(Layer.provide(BackendRegistryLive)),
|
|
29
|
+
);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export type SubagentRuntime = ReturnType<typeof createSubagentRuntime>;
|