@hheei/pi-subagents 0.5.3 → 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 +4 -2
- package/dist/child-control.js +31 -0
- package/dist/domain.d.ts +10 -1
- package/dist/launch-spec.js +1 -0
- package/dist/manager.js +23 -27
- package/dist/state.d.ts +1 -0
- package/dist/state.js +17 -0
- package/dist/tool-presentation.js +1 -1
- package/dist/tools.js +4 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ children and `/subagents stop [id]` to stop one without conflicting with other e
|
|
|
16
16
|
| Tool | Purpose |
|
|
17
17
|
| --- | --- |
|
|
18
18
|
| `agent_spawn({ task, agent, cwd?, title? })` | Spawn an independent background subagent task. Runs in a new Herdr tab (or cmux surface) targeting the parent's workspace when a host is available, and headless in the background otherwise. **Prefer reusing existing subagents** via `agent_send` for related/follow-up work to maintain context and maximize prompt cache and memory efficiency. Tool description lists available agent names and roles, excluding disabled or hidden definitions. Deferred by default; discover via `tool_search({ query: "agent" })`. Do not poll for completion; results return automatically. |
|
|
19
|
-
| `agent_send({ id, message, mode? })` | Send `steer
|
|
19
|
+
| `agent_send({ id, message, mode? })` | Send input to a specific child. `mode: "steer"` (the default) enters the current turn once its tool calls are in, before the next model request; `mode: "follow_up"` waits until the child has no tool calls or steering left, then continues as a new turn. **Primary tool for assigning follow-up or functionally related tasks** to existing subagents: idle, blocked, done, or errored children resume their existing session automatically with historical context intact. A child whose runtime has ended is resumed the same way; spawn a new child only for new work. Stop first only when send says the previous process is still running. Deferred by default; discover via `tool_search({ query: "agent" })`. |
|
|
20
20
|
| `agent_get({ id, keys? })` | Inspect one subagent by id. `keys` is a comma-separated selection of `status` (default), `progress` (default), and `diagnostic`; unknown keys fail. The view never returns the child's task parameters or terminal result, which arrive automatically as reports. From the second successful inspection of the same child in this parent runtime, the result adds a short reminder that results return automatically and must not be polled. Deferred by default; discover via `tool_search({ query: "agent" })`. |
|
|
21
21
|
| `agent_stop({ id })` | Stop one subagent's runtime by id. The child keeps its session, so a later `agent_send` can resume it. Deferred by default; discover via `tool_search({ query: "agent" })`. |
|
|
22
22
|
|
|
@@ -37,6 +37,8 @@ Subagents communicate with the parent through the `commands` table in `state.sql
|
|
|
37
37
|
- **Completion by Direct Output**: Subagents do **not** need to call any reporting tool upon successful completion. The subagent simply outputs its final answer as normal assistant text.
|
|
38
38
|
- **Settlement Batching**: When a subagent settles, the harness captures the trailing assistant text and delivers it to the parent conversation as a `subagent-report`. Reports flush within 500ms when no other child is working, and are batched for up to 10 seconds while another child is still running.
|
|
39
39
|
- **Blockers & Decisions**: The child registers `contact_parent({ message, reason? })` strictly for reporting when it is blocked or urgently requires a parent decision midway (`reason` defaults to `'blocked'`). Blocked reports wake the parent immediately.
|
|
40
|
+
- **Direct `/compact` Command**: Sending `"/compact"` (or `"/compact [instructions]"`) via `agent_send` intercepts the command on the child side without passing it as prompt text to the LLM. The child executes native context compaction and reports completion status back to the parent via `contact_parent`. Duplicate `/compact` requests while a compaction is already in flight are safely acknowledged and ignored.
|
|
41
|
+
- **Context Trim Hint**: When a subagent completes a task with context usage exceeding 100,000 tokens, its report includes a hint advising the parent to trim memory via `agent_send({ id, message: "/compact" })` if the child session will be retained for future work.
|
|
40
42
|
- **Automatic Retry**: Pi's own auto-retry is observed, not invented: reports are suppressed while a retry is in flight, and a retry that succeeds clears the previous error instead of reporting it.
|
|
41
43
|
- **Panel Failure Auto-Close**: When a subagent running in a panel encounters a fatal error or reports a blocker, the manager starts a 15-second countdown after notifying the parent, automatically closing the panel tab to avoid workspace clutter unless new instructions are dispatched.
|
|
42
44
|
## Compact status and reports
|
|
@@ -50,7 +52,7 @@ Use `/subagents` for complete status and management. `agent_send` can resume a d
|
|
|
50
52
|
## Host Integration and Isolation
|
|
51
53
|
|
|
52
54
|
- **Herdr Workspace Affinity**: When spawning in Herdr, tabs are explicitly created in the parent process's current workspace (`--workspace <WORKSPACE_ID>`), preventing child tabs from jumping to whichever workspace happens to have user focus.
|
|
53
|
-
- **
|
|
55
|
+
- **Memory Isolation**: Both `PI_HINDSIGHT_DISABLE=1` and `PI_OBSERVATIONAL_MEMORY_DISABLE=1` are injected into child processes. Hindsight memory auto-registration, recall, and writeback are disabled, and the child bridge blocks all `mcp__hindsight__*` calls. Background Observational Memory workers (observation, reflection, compaction triggers) are suppressed in passive mode.
|
|
54
56
|
|
|
55
57
|
## Agent definitions
|
|
56
58
|
|
package/dist/child-control.js
CHANGED
|
@@ -198,6 +198,20 @@ export function registerChildControl(pi, options) {
|
|
|
198
198
|
}
|
|
199
199
|
return ctx;
|
|
200
200
|
};
|
|
201
|
+
const reportCompaction = (reason, message) => {
|
|
202
|
+
void client
|
|
203
|
+
.report("contact_parent", {
|
|
204
|
+
type: "pi_subagent_report",
|
|
205
|
+
parentSessionId: options.identity.parentSessionId,
|
|
206
|
+
childId: options.identity.subagentId,
|
|
207
|
+
runtimeIdentity: options.identity.runtimeIdentity,
|
|
208
|
+
reason,
|
|
209
|
+
message,
|
|
210
|
+
})
|
|
211
|
+
.catch((err) => {
|
|
212
|
+
options.diagnose?.(`failed to report compaction ${reason}: ${errorMessage(err)}`);
|
|
213
|
+
});
|
|
214
|
+
};
|
|
201
215
|
const deliverPrompt = (payload, deliverAs) => {
|
|
202
216
|
const message = promptMessage(payload);
|
|
203
217
|
// A message needs a session to land in: while the process is between sessions the parent
|
|
@@ -206,6 +220,23 @@ export function registerChildControl(pi, options) {
|
|
|
206
220
|
if (path === undefined) {
|
|
207
221
|
throw new BridgeError("session_not_ready", "This child has no session file path");
|
|
208
222
|
}
|
|
223
|
+
const trimmed = message.trim();
|
|
224
|
+
if (trimmed === "/compact" || trimmed.startsWith("/compact ")) {
|
|
225
|
+
if (isCompacting) {
|
|
226
|
+
return { accepted: true, path, compacting: true };
|
|
227
|
+
}
|
|
228
|
+
const customInstructions = trimmed.slice(8).trim() || undefined;
|
|
229
|
+
requireContext().compact({
|
|
230
|
+
...(customInstructions ? { customInstructions } : {}),
|
|
231
|
+
onComplete: (result) => {
|
|
232
|
+
reportCompaction("success", `Context compacted (${result.tokensBefore} -> ${result.estimatedTokensAfter} tokens).`);
|
|
233
|
+
},
|
|
234
|
+
onError: (err) => {
|
|
235
|
+
reportCompaction("blocked", `Compaction failed: ${err.message}`);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
return { accepted: true, path, compacting: true };
|
|
239
|
+
}
|
|
209
240
|
if (isCompacting) {
|
|
210
241
|
pendingCompactionDeliveries.push({ message, deliverAs });
|
|
211
242
|
return { accepted: true, path, queuedForCompaction: true };
|
package/dist/domain.d.ts
CHANGED
|
@@ -8,7 +8,16 @@ export declare function normalizeSubagentState(value: unknown): SubagentState |
|
|
|
8
8
|
* is created, because it decides who holds the process for the rest of the child's life.
|
|
9
9
|
*/
|
|
10
10
|
export type Presentation = "panel" | "background";
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* How `agent_send` hands its message to a child.
|
|
13
|
+
*
|
|
14
|
+
* `steer` enters the current assistant turn once its tool calls are in, before the next model
|
|
15
|
+
* request, so it redirects work already in flight. `follow_up` waits until the child's run has no
|
|
16
|
+
* tool calls or steering left, then continues as a new turn in the same run. There is no "auto":
|
|
17
|
+
* the two queues already are the two situations, so the caller picks instead of guessing from the
|
|
18
|
+
* child's state.
|
|
19
|
+
*/
|
|
20
|
+
export type SendMode = "steer" | "follow_up";
|
|
12
21
|
export type VisualSubagentState = SubagentState;
|
|
13
22
|
export declare function toVisualSubagentState(state: SubagentState, interrupted?: string): VisualSubagentState;
|
|
14
23
|
export declare const VISUAL_SUBAGENT_GLYPH: Record<VisualSubagentState, string>;
|
package/dist/launch-spec.js
CHANGED
|
@@ -192,6 +192,7 @@ export function buildLaunchSpec(options) {
|
|
|
192
192
|
// Empty is the defined "derive the title in the child" case, mirroring the task channel below.
|
|
193
193
|
[CHILD_TITLE_ENV_KEY]: config.title ?? "",
|
|
194
194
|
PI_HINDSIGHT_DISABLE: "1",
|
|
195
|
+
PI_OBSERVATIONAL_MEMORY_DISABLE: "1",
|
|
195
196
|
...(config.codemodeOnly ? { PI_CODEMODE_MODE: "only" } : {}),
|
|
196
197
|
...(mcpExcluded && mcpExcluded.length > 0
|
|
197
198
|
? { PI_SUBAGENT_MCP_EXCLUDE: mcpExcluded.join(",") }
|
package/dist/manager.js
CHANGED
|
@@ -570,13 +570,19 @@ export class SubagentManager {
|
|
|
570
570
|
else {
|
|
571
571
|
message = text ?? "";
|
|
572
572
|
}
|
|
573
|
+
const reason = isError ? "error" : isBlocked ? "blocked" : "success";
|
|
574
|
+
if (reason === "success" &&
|
|
575
|
+
facts?.contextTokens !== undefined &&
|
|
576
|
+
facts.contextTokens >= 100_000) {
|
|
577
|
+
const kTokens = `${Math.round(facts.contextTokens / 1000)}k`;
|
|
578
|
+
message = `${message}\n\n[Context: ~${kTokens} tokens. If retaining this subagent for future tasks, send "/compact" via agent_send to trim memory.]`;
|
|
579
|
+
}
|
|
573
580
|
const lastDelivered = this.#lastDeliveredTurn.get(id);
|
|
574
581
|
if (lastDelivered !== undefined &&
|
|
575
582
|
lastDelivered.workVersion === workVersion &&
|
|
576
583
|
lastDelivered.message === message) {
|
|
577
584
|
return;
|
|
578
585
|
}
|
|
579
|
-
const reason = isError ? "error" : isBlocked ? "blocked" : "success";
|
|
580
586
|
const projector = this.#projectors.get(id);
|
|
581
587
|
const updatedRecord = !failed
|
|
582
588
|
? await this.#update(id, (current) => {
|
|
@@ -923,7 +929,7 @@ export class SubagentManager {
|
|
|
923
929
|
return failed;
|
|
924
930
|
}
|
|
925
931
|
}
|
|
926
|
-
send(id, rawMessage, mode = "
|
|
932
|
+
send(id, rawMessage, mode = "steer", signal) {
|
|
927
933
|
const message = stripHindsightContent(rawMessage);
|
|
928
934
|
this.#bumpActivity(id);
|
|
929
935
|
this.#clearSettledPanelClose(id);
|
|
@@ -994,15 +1000,10 @@ export class SubagentManager {
|
|
|
994
1000
|
}
|
|
995
1001
|
}
|
|
996
1002
|
const state = isResumed ? "running" : record.state;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
? "steer"
|
|
1002
|
-
: "follow_up"
|
|
1003
|
-
: mode;
|
|
1004
|
-
if (operation === undefined)
|
|
1005
|
-
return failure("send", "Cannot infer send mode from unknown state", id, state);
|
|
1003
|
+
// A resume already starts a fresh run, so the mode has nothing to decide: the message is
|
|
1004
|
+
// that run's prompt. Otherwise the caller's mode is the request, with no state guessing:
|
|
1005
|
+
// a running child steers at its next model step, an idle one simply starts a run.
|
|
1006
|
+
const operation = isResumed ? "prompt" : mode;
|
|
1006
1007
|
if (signal?.aborted)
|
|
1007
1008
|
return failure("send", "Send was cancelled", id, state);
|
|
1008
1009
|
this.#clearSettledPanelClose(id);
|
|
@@ -1182,25 +1183,20 @@ export class SubagentManager {
|
|
|
1182
1183
|
const delivered = await this.contactParent(childId, reason, payload.message, record);
|
|
1183
1184
|
if ("reason" in delivered)
|
|
1184
1185
|
throw new BridgeError("delivery_failed", delivered.reason);
|
|
1185
|
-
if (reason === "success") {
|
|
1186
|
-
const
|
|
1187
|
-
if (projector !== undefined) {
|
|
1188
|
-
projector.syncState("done");
|
|
1189
|
-
}
|
|
1190
|
-
await this.#update(childId, (current) => ({
|
|
1191
|
-
...current,
|
|
1192
|
-
state: "done",
|
|
1193
|
-
}));
|
|
1194
|
-
}
|
|
1195
|
-
else if (reason === "blocked") {
|
|
1186
|
+
if (reason === "success" || reason === "blocked") {
|
|
1187
|
+
const terminalState = reason === "success" ? "done" : "blocked";
|
|
1196
1188
|
const projector = this.#projectors.get(childId);
|
|
1197
1189
|
if (projector !== undefined) {
|
|
1198
|
-
projector.syncState(
|
|
1190
|
+
projector.syncState(terminalState);
|
|
1199
1191
|
}
|
|
1200
|
-
await this.#update(childId, (current) =>
|
|
1201
|
-
...current
|
|
1202
|
-
|
|
1203
|
-
|
|
1192
|
+
await this.#update(childId, (current) => {
|
|
1193
|
+
const { unacknowledgedInput: _input, ...clean } = current;
|
|
1194
|
+
return {
|
|
1195
|
+
...clean,
|
|
1196
|
+
state: terminalState,
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
this.#scheduleSettledPanelClose(childId);
|
|
1204
1200
|
}
|
|
1205
1201
|
else {
|
|
1206
1202
|
const projector = this.#projectors.get(childId);
|
package/dist/state.d.ts
CHANGED
package/dist/state.js
CHANGED
|
@@ -123,12 +123,26 @@ function lastCompletedAssistant(entries) {
|
|
|
123
123
|
}
|
|
124
124
|
return last;
|
|
125
125
|
}
|
|
126
|
+
function lastAssistantContextTokens(message) {
|
|
127
|
+
if (message === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const usage = record(message.usage);
|
|
130
|
+
if (usage === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const input = finite(usage.input) ?? 0;
|
|
133
|
+
const output = finite(usage.output) ?? 0;
|
|
134
|
+
const cacheRead = finite(usage.cacheRead) ?? 0;
|
|
135
|
+
const cacheWrite = finite(usage.cacheWrite) ?? 0;
|
|
136
|
+
const total = input + output + cacheRead + cacheWrite;
|
|
137
|
+
return total > 0 ? total : undefined;
|
|
138
|
+
}
|
|
126
139
|
/** What the parent projects from a session. The transcript itself never crosses the bridge. */
|
|
127
140
|
export function transcriptFacts(entries, expectedInput) {
|
|
128
141
|
const last = lastCompletedAssistant(entries);
|
|
129
142
|
const summary = summarizeCurrentBranch(entries);
|
|
130
143
|
const stopReason = typeof last?.stopReason === "string" ? last.stopReason : undefined;
|
|
131
144
|
const interrupted = last === undefined ? undefined : diagnostic(last);
|
|
145
|
+
const contextTokens = lastAssistantContextTokens(last);
|
|
132
146
|
const containsInput = expectedInput !== undefined &&
|
|
133
147
|
entries.some((entry) => {
|
|
134
148
|
const value = record(entry);
|
|
@@ -140,6 +154,7 @@ export function transcriptFacts(entries, expectedInput) {
|
|
|
140
154
|
...(summary === undefined ? {} : { summary }),
|
|
141
155
|
...(stopReason === undefined ? {} : { stopReason }),
|
|
142
156
|
...(interrupted === undefined ? {} : { interrupted }),
|
|
157
|
+
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
143
158
|
containsInput,
|
|
144
159
|
};
|
|
145
160
|
}
|
|
@@ -169,6 +184,7 @@ export function parseTranscriptFacts(value) {
|
|
|
169
184
|
const summary = typeof value.summary === "string" ? value.summary : undefined;
|
|
170
185
|
const stopReason = typeof value.stopReason === "string" ? value.stopReason : undefined;
|
|
171
186
|
const interrupted = typeof value.interrupted === "string" ? value.interrupted : undefined;
|
|
187
|
+
const contextTokens = usageNumber(value.contextTokens);
|
|
172
188
|
return {
|
|
173
189
|
usage: {
|
|
174
190
|
inputTokens,
|
|
@@ -181,6 +197,7 @@ export function parseTranscriptFacts(value) {
|
|
|
181
197
|
...(summary === undefined ? {} : { summary }),
|
|
182
198
|
...(stopReason === undefined ? {} : { stopReason }),
|
|
183
199
|
...(interrupted === undefined ? {} : { interrupted }),
|
|
200
|
+
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
184
201
|
containsInput: value.containsInput,
|
|
185
202
|
};
|
|
186
203
|
}
|
|
@@ -55,7 +55,7 @@ function spawnPresentation(args, result) {
|
|
|
55
55
|
function sendPresentation(args, result) {
|
|
56
56
|
const record = isRecord(args) ? args : undefined;
|
|
57
57
|
const id = stringField(record, "id") ?? "agent";
|
|
58
|
-
const mode = stringField(record, "mode") ?? "
|
|
58
|
+
const mode = stringField(record, "mode") ?? "steer";
|
|
59
59
|
const message = stringField(record, "message");
|
|
60
60
|
const confirmation = result === undefined ? undefined : agentResultText(result);
|
|
61
61
|
const copy = message === undefined
|
package/dist/tools.js
CHANGED
|
@@ -18,7 +18,9 @@ const spawnSchema = Type.Object({
|
|
|
18
18
|
const sendSchema = Type.Object({
|
|
19
19
|
id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
|
|
20
20
|
message: Type.String({ minLength: 1 }),
|
|
21
|
-
mode: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("follow_up"),
|
|
21
|
+
mode: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("follow_up")], {
|
|
22
|
+
description: "steer (the default) enters the child's current turn once its tool calls are in, before the next model request, so it redirects work already in flight; an idle child just starts a run. follow_up waits until the child has no tool calls or steering left, then continues as a new turn. Use follow_up for work that must not interrupt what the child is doing now.",
|
|
23
|
+
})),
|
|
22
24
|
});
|
|
23
25
|
const idSchema = Type.Object({
|
|
24
26
|
id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
|
|
@@ -66,7 +68,7 @@ function getSpawnDescription(agents) {
|
|
|
66
68
|
formatAvailableAgents(agents),
|
|
67
69
|
].join("\n");
|
|
68
70
|
}
|
|
69
|
-
const SEND_DESCRIPTION = "Send input
|
|
71
|
+
const SEND_DESCRIPTION = "Send input to an existing subagent (e.g. 'agent-1'), resuming its session when it is idle, blocked, done, or errored. A child whose runtime has ended is resumed the same way: send to that id, do not spawn a replacement. Stop it first only when send reports the previous process is still running. Results are returned automatically when the task finishes; do not poll agent_get. Related subagent tools: agent_spawn, agent_get, agent_stop.";
|
|
70
72
|
const GET_DESCRIPTION = "Inspect one subagent's status, progress, or diagnostic details by id (e.g. 'agent-1'). Use keys to choose fields; defaults to 'status,progress'. Results are returned automatically; use this for inspection, not completion polling. Related subagent tools: agent_spawn, agent_send, agent_stop.";
|
|
71
73
|
const STOP_DESCRIPTION = "Stop a subagent's runtime by id (e.g. 'agent-1'). The child keeps its session, so a later agent_send can resume it. Related subagent tools: agent_spawn, agent_send, agent_get.";
|
|
72
74
|
const CONTACT_DESCRIPTION = "Notify the parent session when you are BLOCKED and cannot continue without parent decision or intervention. Do NOT call this on success or task completion: simply output your final answer directly as normal text in your response, and the harness will automatically capture and report your result to the parent.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hheei/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Independent background Pi subagent sessions with durable identity and RPC control.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@hheei/pi-ext-core": "^0.6.
|
|
38
|
+
"@hheei/pi-ext-core": "^0.6.4"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@earendil-works/pi-coding-agent": ">=1.3.0",
|