@hheei/pi-subagents 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/child-control.js +31 -0
- package/dist/launch-spec.js +1 -0
- package/dist/manager.js +18 -17
- package/dist/state.d.ts +1 -0
- package/dist/state.js +17 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -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/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) => {
|
|
@@ -1177,25 +1183,20 @@ export class SubagentManager {
|
|
|
1177
1183
|
const delivered = await this.contactParent(childId, reason, payload.message, record);
|
|
1178
1184
|
if ("reason" in delivered)
|
|
1179
1185
|
throw new BridgeError("delivery_failed", delivered.reason);
|
|
1180
|
-
if (reason === "success") {
|
|
1181
|
-
const
|
|
1182
|
-
if (projector !== undefined) {
|
|
1183
|
-
projector.syncState("done");
|
|
1184
|
-
}
|
|
1185
|
-
await this.#update(childId, (current) => ({
|
|
1186
|
-
...current,
|
|
1187
|
-
state: "done",
|
|
1188
|
-
}));
|
|
1189
|
-
}
|
|
1190
|
-
else if (reason === "blocked") {
|
|
1186
|
+
if (reason === "success" || reason === "blocked") {
|
|
1187
|
+
const terminalState = reason === "success" ? "done" : "blocked";
|
|
1191
1188
|
const projector = this.#projectors.get(childId);
|
|
1192
1189
|
if (projector !== undefined) {
|
|
1193
|
-
projector.syncState(
|
|
1190
|
+
projector.syncState(terminalState);
|
|
1194
1191
|
}
|
|
1195
|
-
await this.#update(childId, (current) =>
|
|
1196
|
-
...current
|
|
1197
|
-
|
|
1198
|
-
|
|
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);
|
|
1199
1200
|
}
|
|
1200
1201
|
else {
|
|
1201
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
|
}
|
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",
|