@ian-pascoe/pi-minimal-subagents 0.11.0 → 0.13.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 +70 -8
- package/package.json +6 -6
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-child-resources.ts +14 -17
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +115 -24
- package/src/minimal-subagents-extension.ts +3 -0
- package/src/minimal-subagents-registry.ts +3 -1
- package/src/minimal-subagents-render-contract.ts +30 -2
- package/src/minimal-subagents-rendering.ts +318 -52
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +12 -3
- package/src/minimal-subagents-tools.ts +174 -42
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +22 -5
- package/src/minimal-subagents-ui.ts +1 -1
- package/src/minimal-subagents-usage.ts +13 -2
package/README.md
CHANGED
|
@@ -29,7 +29,21 @@ filter in `~/.pi/agent/settings.json` using the repository-relative path:
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
From this package checkout, load the source directly with
|
|
32
|
-
`pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=0.
|
|
32
|
+
`pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=0.99.0`.
|
|
33
|
+
|
|
34
|
+
## Typical workflow
|
|
35
|
+
|
|
36
|
+
1. `subagent` launches a Child Agent from a self-contained task: by default the
|
|
37
|
+
child starts without the parent's conversation. It returns at once with its `agent_id`,
|
|
38
|
+
`turn_id`, and resolved model, thinking level, tools, delegation, and any
|
|
39
|
+
tool-resolution `warnings`. Spawns run sequentially, so a `subagent_wait` or
|
|
40
|
+
`agent_message` in the same tool batch can target the new child.
|
|
41
|
+
2. `subagent_wait` claims the turn's result, or leave it unclaimed and the final
|
|
42
|
+
response arrives automatically as a steer message.
|
|
43
|
+
3. `agent_message` continues an idle child: the result reports
|
|
44
|
+
`disposition: "started-turn"` with the new `turn_id` to wait on.
|
|
45
|
+
4. `subagent_status` inspects children; `subagent_cancel` stops work but keeps
|
|
46
|
+
the session; `subagent_delete` removes it. Deleted IDs cannot be reused.
|
|
33
47
|
|
|
34
48
|
## Configuration
|
|
35
49
|
|
|
@@ -164,7 +178,7 @@ example:
|
|
|
164
178
|
"minimalSubagents": {
|
|
165
179
|
"baseToolset": ["context_*"],
|
|
166
180
|
"readToolset": ["read", "grep", "find", "ls"],
|
|
167
|
-
"modifyToolset": ["bash", "edit", "write", "
|
|
181
|
+
"modifyToolset": ["bash", "edit", "write", "lsp_*", "dap_*"]
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
```
|
|
@@ -203,8 +217,8 @@ Pattern expansion happens when a Child Agent is created. `/reload` applies
|
|
|
203
217
|
settings to future launches without changing existing Launch Contracts. If a tool
|
|
204
218
|
in an existing contract later disappears, normal restoration dependency checks
|
|
205
219
|
still apply; the saved grant is not silently rewritten. Toolsets configure names,
|
|
206
|
-
not tool operations: granting
|
|
207
|
-
|
|
220
|
+
not tool operations: granting a multifunction tool grants that tool's available
|
|
221
|
+
operations, regardless of preset name.
|
|
208
222
|
|
|
209
223
|
Child Agents load Pi's built-in `llama.cpp`, `codemode`, `tool-search`, and `mcp`
|
|
210
224
|
extensions unless settings disable them (`-builtin:<name>`); each child connects
|
|
@@ -240,7 +254,32 @@ and `subagent_status`.
|
|
|
240
254
|
Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
|
|
241
255
|
text, reasoning, tool calls, and tool results. It includes the current streaming
|
|
242
256
|
assistant message but omits image data. Timeout Wait Events include the same
|
|
243
|
-
detailed status snapshot.
|
|
257
|
+
detailed status snapshot. Children target only direct children; the Root Agent
|
|
258
|
+
may inspect any descendant. Model-facing status reports a `child_count` but no
|
|
259
|
+
nested `children` summaries. Reported `usage` always includes `cacheWrite1h`
|
|
260
|
+
and `reasoning`, as `0` when the provider reports none. `subagent_cancel`
|
|
261
|
+
lists in `affected_agent_ids` only agents whose active turns it cancelled.
|
|
262
|
+
|
|
263
|
+
`session_context` defaults to `omit`: the child starts with only its system
|
|
264
|
+
prompt and task, so the task should be a self-contained brief covering the goal,
|
|
265
|
+
target files and non-goals, the change or question, and acceptance criteria.
|
|
266
|
+
Opt in to `inherit` to copy the parent's conversation, or `compact` to summarize
|
|
267
|
+
it first, only when the child needs that discussion. Inheriting is costly: the
|
|
268
|
+
child's system prompt differs from its parent's, so the copied conversation
|
|
269
|
+
cannot reuse the parent's prompt cache, and `compact` adds a summarization call.
|
|
270
|
+
With either opt-in, the parent's conversation is quoted rather than replayed as
|
|
271
|
+
the child's own turns: each parent message becomes one
|
|
272
|
+
`minimal-subagents.parent-context` custom message, sent to the model as
|
|
273
|
+
user-role text wrapped in `<parent_message from="…" role="…">`. Tool calls and
|
|
274
|
+
results become text, a custom message's label names its `customType`, and an
|
|
275
|
+
errored, aborted, or length-limited parent turn is marked as incomplete. Images
|
|
276
|
+
are kept; parent reasoning and the parent's system prompt and tool declarations
|
|
277
|
+
are omitted, since the child declares its own. Each quoted message stays a
|
|
278
|
+
separate session entry, so `compact` can still summarize older entries. The task
|
|
279
|
+
is then framed as a handoff: the quoted conversation is background, its requests
|
|
280
|
+
belonged to the parent, and the child's own assignment follows. Quoting keeps
|
|
281
|
+
the parent's turns distinct from the child's own; it does not guarantee that a
|
|
282
|
+
child, particularly a small model, ignores the quoted requests. Existing Child Agents keep the mode recorded in their Launch Contracts.
|
|
244
283
|
|
|
245
284
|
The `subagent` `tools` argument distinguishes configurable Tool Presets from
|
|
246
285
|
exact lists; every selection also receives the permitted Base Toolset described
|
|
@@ -260,11 +299,18 @@ tools while the Launch Contract continues to record the originally granted
|
|
|
260
299
|
capability names.
|
|
261
300
|
|
|
262
301
|
`agent_message` reports whether a message was delivered through an active
|
|
263
|
-
parent wait, queued
|
|
302
|
+
parent wait (`delivered-via-wait`), queued into the recipient's active turn
|
|
303
|
+
(`queued`), started a new turn on an idle child (`started-turn`, with its
|
|
304
|
+
`turn_id`), or failed. A failed delivery and a
|
|
305
|
+
partially failed `subagent_delete` return an error result (`isError: true`) that
|
|
306
|
+
still carries the declared structured output, so the model sees an error while
|
|
307
|
+
codemode scripts receive the `failures` or `error` data instead of a data-less
|
|
308
|
+
rejection. `subagent_wait` can return an
|
|
264
309
|
intermediate Wait Event containing a Coordination Message before the child turn
|
|
265
310
|
settles. That event claims only its message, so later unconsumed messages and the
|
|
266
|
-
terminal result retain automatic fallback. If the turn has already settled,
|
|
267
|
-
wait returns its terminal result with queued messages in `messages
|
|
311
|
+
terminal result retain automatic fallback. If the turn has already settled, a
|
|
312
|
+
wait returns its terminal result with queued messages in `messages`; waiting
|
|
313
|
+
again for the same settled turn returns the same result. Pass
|
|
268
314
|
optional `turn_id` to address an older retained turn exactly. Without it, waits
|
|
269
315
|
select the oldest observable claimed or pending turn before the active/latest
|
|
270
316
|
turn. A caller may have only one outstanding wait for the same source turn; a
|
|
@@ -323,8 +369,24 @@ process-local fork handoff is lost, recovery reads only the destination's
|
|
|
323
369
|
selected branch and proceeds only when its canonical `parentSession` proves the
|
|
324
370
|
source file; it never substitutes the source session's newer head.
|
|
325
371
|
|
|
372
|
+
## Tool annotations
|
|
373
|
+
|
|
374
|
+
The coordinator tools declare MCP-style `annotations`, which Pi reports through `pi.getAllTools()` for permission extensions and does not send to model providers. `subagent` is destructive and open-world because a Child Agent can use any tool it is granted. `agent_message` is not read-only but is non-destructive and closed-world. `subagent_cancel` is non-destructive and idempotent; `subagent_delete` is destructive and idempotent. `subagent_status` and `subagent_wait` are read-only (`subagent_wait` consumes queued deliveries, so it is not idempotent).
|
|
375
|
+
|
|
326
376
|
## Status and TUI
|
|
327
377
|
|
|
378
|
+
Coordinator tool calls and results stay readable without expanding. A
|
|
379
|
+
`subagent` call shows the launch settings the caller chose and its task; an
|
|
380
|
+
`agent_message` call shows its message. While `subagent_wait` blocks, it shows
|
|
381
|
+
the child's tool-call count and, on a tree rail, the latest work of the turn
|
|
382
|
+
being waited on, rendered with Pi's own message and tool components as in the
|
|
383
|
+
`/subagents` viewer and refreshed each second; collapsed, the rail keeps the
|
|
384
|
+
newest items within 20 lines. A settled wait shows the child's output as
|
|
385
|
+
Markdown, or its error, with duration, tokens, and cost. Long text previews its
|
|
386
|
+
first 10 lines, like Pi's `read`. Every collapsed result ends its first line
|
|
387
|
+
with Pi's tool-expansion hint (normally Ctrl+O), which reveals the rest and the
|
|
388
|
+
full detail. Automatic agent results and messages preview the same way.
|
|
389
|
+
|
|
328
390
|
In TUI mode, `/subagents` or `/subagents status` opens a large, centered,
|
|
329
391
|
framed overlay. Up/Down selects a Child Agent; Enter opens its Child Session
|
|
330
392
|
Transcript. Escape returns to the tree, then Escape closes the overlay. The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ian-pascoe/pi-utils": "^0.3.
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.3.1",
|
|
35
35
|
"minimatch": "^10.2.6"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-agent-core": "
|
|
39
|
-
"@earendil-works/pi-ai": "
|
|
40
|
-
"@earendil-works/pi-coding-agent": "
|
|
41
|
-
"@earendil-works/pi-tui": "
|
|
38
|
+
"@earendil-works/pi-agent-core": ">=0.99.0",
|
|
39
|
+
"@earendil-works/pi-ai": ">=0.99.0",
|
|
40
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0",
|
|
41
|
+
"@earendil-works/pi-tui": ">=0.99.0",
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
@@ -75,7 +75,9 @@ export function resolveOrdinaryToolSelection(
|
|
|
75
75
|
const ceiling = new Set(context.capabilityCeiling);
|
|
76
76
|
const exceeded = uniqueRequested.filter((name) => !ceiling.has(name));
|
|
77
77
|
if (exceeded.length > 0) {
|
|
78
|
-
throw new Error(
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Minimal subagents tool selection: ${exceeded.join(", ")} ${exceeded.length === 1 ? "is" : "are"} unknown or outside the caller's capability ceiling. ${describePermittedTools(excludeCoordinatorTools(context.capabilityCeiling))}`,
|
|
80
|
+
);
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
const toolsets = context.toolsets ?? DEFAULT_TOOLSETS;
|
|
@@ -103,6 +105,15 @@ export function resolveOrdinaryToolSelection(
|
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
const PERMITTED_TOOL_LISTING_LIMIT = 40;
|
|
109
|
+
|
|
110
|
+
function describePermittedTools(permitted: readonly string[]): string {
|
|
111
|
+
if (permitted.length === 0) return "No ordinary tools are permitted.";
|
|
112
|
+
const listed = permitted.slice(0, PERMITTED_TOOL_LISTING_LIMIT).join(", ");
|
|
113
|
+
const remaining = permitted.length - PERMITTED_TOOL_LISTING_LIMIT;
|
|
114
|
+
return `Permitted ordinary tools: ${listed}${remaining > 0 ? `, and ${remaining} more` : ""}.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
/** Return an agent's hierarchy depth where the interactive root is depth zero. */
|
|
107
118
|
export function getSubagentDepth(agentId: string): number {
|
|
108
119
|
if (agentId === "root") return 0;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import {
|
|
5
|
+
createCodemodeExtension,
|
|
6
|
+
createMcpExtension,
|
|
7
|
+
createToolSearchExtension,
|
|
6
8
|
DefaultResourceLoader,
|
|
7
9
|
type ExtensionFactory,
|
|
10
|
+
getPackageDir,
|
|
8
11
|
SettingsManager,
|
|
9
12
|
} from "@earendil-works/pi-coding-agent";
|
|
10
13
|
import { Type } from "typebox";
|
|
@@ -25,9 +28,6 @@ type InlineExtension = NonNullable<
|
|
|
25
28
|
ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"]
|
|
26
29
|
>[number];
|
|
27
30
|
|
|
28
|
-
// SAFETY: Older Pi hosts lack these exports; the namespace yields undefined rather than failing.
|
|
29
|
-
const sdk = piCodingAgent as Partial<typeof piCodingAgent>;
|
|
30
|
-
|
|
31
31
|
const ExtensionModuleSchema = Type.Object({
|
|
32
32
|
default: Type.Function([Type.Unknown()], Type.Unknown()),
|
|
33
33
|
});
|
|
@@ -37,8 +37,7 @@ const LLAMA = "llama.cpp";
|
|
|
37
37
|
|
|
38
38
|
/** Pi ships a file-backed llama.cpp factory but does not export it; undefined when it is absent. */
|
|
39
39
|
function piLlamaExtensionPath(): string | undefined {
|
|
40
|
-
|
|
41
|
-
const path = join(sdk.getPackageDir(), "dist", "extensions", "llama", "index.js");
|
|
40
|
+
const path = join(getPackageDir(), "dist", "extensions", "llama", "index.js");
|
|
42
41
|
return existsSync(path) ? path : undefined;
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -62,20 +61,18 @@ export function childProviderAvailable(provider: string): boolean {
|
|
|
62
61
|
/** Child scripts must not call models outside their Launch Contract. */
|
|
63
62
|
export const CHILD_CODEMODE_MODELS = false;
|
|
64
63
|
|
|
65
|
-
/** Pi's built-in extensions
|
|
64
|
+
/** Pi's built-in extensions in Pi's order, so children match the root's built-in surface. */
|
|
66
65
|
export function childBuiltinExtensions(): InlineExtension[] {
|
|
67
|
-
const createCodemode = sdk.createCodemodeExtension;
|
|
68
66
|
return [
|
|
69
67
|
...llamaExtension(),
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
),
|
|
68
|
+
{
|
|
69
|
+
name: "codemode",
|
|
70
|
+
factory: createCodemodeExtension({ models: CHILD_CODEMODE_MODELS }),
|
|
71
|
+
builtin: true,
|
|
72
|
+
replaceable: true,
|
|
73
|
+
},
|
|
74
|
+
{ name: "tool-search", factory: createToolSearchExtension(), builtin: true, replaceable: true },
|
|
75
|
+
{ name: "mcp", factory: createMcpExtension(), builtin: true, replaceable: true },
|
|
79
76
|
];
|
|
80
77
|
}
|
|
81
78
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
contentText,
|
|
4
|
+
type AssistantMessage,
|
|
5
|
+
type ImageContent,
|
|
6
|
+
type TextContent,
|
|
7
|
+
} from "@earendil-works/pi-ai";
|
|
3
8
|
import { truncateTail } from "@earendil-works/pi-coding-agent";
|
|
4
9
|
import type {
|
|
10
|
+
ActiveTurnProgress,
|
|
5
11
|
ChildAgentTranscriptSnapshot,
|
|
6
12
|
RecentAgentActivity,
|
|
7
13
|
SessionContextMode,
|
|
@@ -141,19 +147,189 @@ export function buildRecentAgentActivity(messages: readonly AgentMessage[]): Rec
|
|
|
141
147
|
return activity.slice(-RECENT_AGENT_ACTIVITY_LIMIT);
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
/** A message the child produced during the turn, as opposed to the turn's prompt or coordination input. */
|
|
151
|
+
function isTurnWork(message: AgentMessage, turnStartedAtMs: number): boolean {
|
|
152
|
+
return (
|
|
153
|
+
message.timestamp >= turnStartedAtMs &&
|
|
154
|
+
(message.role === "assistant" || message.role === "toolResult")
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Count the tool calls a running turn has made so far. */
|
|
159
|
+
export function buildActiveTurnProgress(
|
|
160
|
+
messages: readonly AgentMessage[],
|
|
161
|
+
turnStartedAtMs: number,
|
|
162
|
+
): Omit<ActiveTurnProgress, "turn_id"> {
|
|
163
|
+
let toolCalls = 0;
|
|
164
|
+
for (const message of messages) {
|
|
165
|
+
if (message.role !== "assistant" || !isTurnWork(message, turnStartedAtMs)) continue;
|
|
166
|
+
toolCalls += message.content.filter((content) => content.type === "toolCall").length;
|
|
167
|
+
}
|
|
168
|
+
return { tool_calls: toolCalls };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Keep the running turn's last `assistantMessageCount` assistant messages and the tool results after
|
|
173
|
+
* them, so a live view renders recent work without the turn's earlier history.
|
|
174
|
+
*/
|
|
175
|
+
export function selectActiveTurnTranscript(
|
|
176
|
+
snapshot: ChildAgentTranscriptSnapshot,
|
|
177
|
+
turnStartedAtMs: number,
|
|
178
|
+
assistantMessageCount: number,
|
|
179
|
+
): ChildAgentTranscriptSnapshot {
|
|
180
|
+
const work = snapshot.messages.flatMap((message, index) =>
|
|
181
|
+
isTurnWork(message, turnStartedAtMs) ? [{ message, index }] : [],
|
|
182
|
+
);
|
|
183
|
+
const assistantPositions = work.flatMap((entry, position) =>
|
|
184
|
+
entry.message.role === "assistant" ? [position] : [],
|
|
185
|
+
);
|
|
186
|
+
const tail = work.slice(assistantPositions.at(-assistantMessageCount) ?? 0);
|
|
187
|
+
const streamingPosition = tail.findIndex(
|
|
188
|
+
(entry) => entry.index === snapshot.streamingAssistantIndex,
|
|
189
|
+
);
|
|
190
|
+
return {
|
|
191
|
+
messages: tail.map((entry) => entry.message),
|
|
192
|
+
streamingAssistantIndex: streamingPosition >= 0 ? streamingPosition : undefined,
|
|
193
|
+
toolDefinitions: snapshot.toolDefinitions,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
144
197
|
/** Carries the selected caller messages and whether child preparation should compact them. */
|
|
145
198
|
export interface ImportedSubagentContext {
|
|
146
199
|
messages: AgentMessage[];
|
|
147
200
|
compact: boolean;
|
|
148
201
|
}
|
|
149
202
|
|
|
150
|
-
/**
|
|
203
|
+
/** Custom message type carrying one quoted message from a parent's conversation. */
|
|
204
|
+
export const PARENT_CONTEXT_MESSAGE_TYPE = "minimal-subagents.parent-context";
|
|
205
|
+
|
|
206
|
+
type QuotedContent = TextContent | ImageContent;
|
|
207
|
+
|
|
208
|
+
function quotedParts(content: string | QuotedContent[]): QuotedContent[] {
|
|
209
|
+
if (Array.isArray(content)) return content.map((part) => structuredClone(part));
|
|
210
|
+
return content ? [{ type: "text", text: content }] : [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parentMessageBody(
|
|
214
|
+
message: AgentMessage,
|
|
215
|
+
): { label: string; parts: QuotedContent[] } | undefined {
|
|
216
|
+
switch (message.role) {
|
|
217
|
+
case "user":
|
|
218
|
+
return { label: "user", parts: quotedParts(message.content) };
|
|
219
|
+
case "assistant": {
|
|
220
|
+
// Reasoning is omitted: it is model-private and often signature-bound to the parent's model.
|
|
221
|
+
const parts = message.content.flatMap((content): QuotedContent[] => {
|
|
222
|
+
if (content.type === "text")
|
|
223
|
+
return content.text ? [{ type: "text", text: content.text }] : [];
|
|
224
|
+
if (content.type === "toolCall") {
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: `[tool call ${content.name}] ${JSON.stringify(content.arguments)}`,
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
return [];
|
|
233
|
+
});
|
|
234
|
+
const stopNote = incompleteAssistantNote(message.stopReason, message.errorMessage);
|
|
235
|
+
if (stopNote) parts.push({ type: "text", text: stopNote });
|
|
236
|
+
return { label: "assistant", parts };
|
|
237
|
+
}
|
|
238
|
+
case "toolResult":
|
|
239
|
+
return {
|
|
240
|
+
label: `tool result ${message.toolName}${message.isError ? " (error)" : ""}`,
|
|
241
|
+
parts: quotedParts(message.content),
|
|
242
|
+
};
|
|
243
|
+
case "custom":
|
|
244
|
+
return {
|
|
245
|
+
label: `context message: ${message.customType}`,
|
|
246
|
+
parts: quotedParts(message.content),
|
|
247
|
+
};
|
|
248
|
+
case "bashExecution":
|
|
249
|
+
if (message.excludeFromContext) return undefined;
|
|
250
|
+
return {
|
|
251
|
+
label: "shell command",
|
|
252
|
+
parts: [{ type: "text", text: `$ ${message.command}\n${message.output || "(no output)"}` }],
|
|
253
|
+
};
|
|
254
|
+
case "branchSummary":
|
|
255
|
+
case "compactionSummary":
|
|
256
|
+
return {
|
|
257
|
+
label: message.role === "branchSummary" ? "branch summary" : "compaction summary",
|
|
258
|
+
parts: quotedParts(message.summary),
|
|
259
|
+
};
|
|
260
|
+
case "system":
|
|
261
|
+
// The parent's prompt sections and tool declarations describe the parent, not the child,
|
|
262
|
+
// which declares its own prompt and tools on its first request.
|
|
263
|
+
return undefined;
|
|
264
|
+
default: {
|
|
265
|
+
// Fails typecheck when Pi adds a message role, so new roles are quoted deliberately.
|
|
266
|
+
const unhandled: never = message;
|
|
267
|
+
return unhandled;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Mark a parent turn that ended early, so quoted partial output does not read as complete. */
|
|
273
|
+
function incompleteAssistantNote(
|
|
274
|
+
stopReason: AssistantMessage["stopReason"],
|
|
275
|
+
errorMessage: string | undefined,
|
|
276
|
+
): string | undefined {
|
|
277
|
+
if (stopReason === "error") return `[turn failed: ${errorMessage ?? "unknown error"}]`;
|
|
278
|
+
if (stopReason === "aborted") return "[turn aborted before completion]";
|
|
279
|
+
if (stopReason === "length") return "[turn stopped at the output length limit]";
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Quote one parent message as a user-visible custom message. Replaying the parent's assistant
|
|
285
|
+
* turns as the child's own lets the child continue as its parent; quoting keeps the roles apart
|
|
286
|
+
* while each message stays a separate entry that child compaction can cut between.
|
|
287
|
+
*/
|
|
288
|
+
function quoteParentMessage(message: AgentMessage, parentId: string): AgentMessage | undefined {
|
|
289
|
+
const body = parentMessageBody(message);
|
|
290
|
+
if (!body || body.parts.length === 0) return undefined;
|
|
291
|
+
return {
|
|
292
|
+
role: "custom",
|
|
293
|
+
customType: PARENT_CONTEXT_MESSAGE_TYPE,
|
|
294
|
+
content: [
|
|
295
|
+
{ type: "text", text: `<parent_message from="${parentId}" role="${body.label}">` },
|
|
296
|
+
...body.parts,
|
|
297
|
+
{ type: "text", text: "</parent_message>" },
|
|
298
|
+
],
|
|
299
|
+
display: true,
|
|
300
|
+
timestamp: message.timestamp,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Select and quote the imported parent conversation; compaction is deferred to the child turn. */
|
|
151
305
|
export function assembleImportedContext(
|
|
152
306
|
mode: SessionContextMode,
|
|
153
|
-
committedMessages: AgentMessage[],
|
|
307
|
+
committedMessages: readonly AgentMessage[],
|
|
308
|
+
parentId: string,
|
|
154
309
|
): ImportedSubagentContext {
|
|
155
310
|
if (mode === "omit") return { messages: [], compact: false };
|
|
156
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
messages: committedMessages.flatMap((message) => quoteParentMessage(message, parentId) ?? []),
|
|
313
|
+
compact: mode === "compact",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Frame a child's task after inherited parent conversation so the child does not adopt the
|
|
319
|
+
* parent's earlier requests as its own assignment.
|
|
320
|
+
*/
|
|
321
|
+
export function buildInheritedContextTaskPrompt(
|
|
322
|
+
task: string,
|
|
323
|
+
agentId: string,
|
|
324
|
+
parentId: string,
|
|
325
|
+
): string {
|
|
326
|
+
return [
|
|
327
|
+
`The parent_message entries above, and any summary of earlier ones, come from the conversation of your parent \`${parentId}\`. They are background context only.`,
|
|
328
|
+
`Requests and tool calls in them belong to your parent, not to you: do not continue or repeat your parent's work.`,
|
|
329
|
+
`You are \`${agentId}\`. Your assigned task is:`,
|
|
330
|
+
"",
|
|
331
|
+
task,
|
|
332
|
+
].join("\n");
|
|
157
333
|
}
|
|
158
334
|
|
|
159
335
|
/** Detect image content so incompatible child models fail before agent creation. */
|