@clanker-code/pi-subagents 0.10.5 → 0.10.7
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/AGENTS.md +11 -1
- package/CHANGELOG.md +12 -1
- package/README.md +6 -5
- package/RELEASE.md +18 -12
- package/dist/agent-manager.d.ts +4 -0
- package/dist/agent-manager.js +4 -0
- package/dist/bounded-output.d.ts +16 -0
- package/dist/bounded-output.js +26 -0
- package/dist/index.js +30 -14
- package/dist/notifications.js +7 -5
- package/dist/peek.js +24 -11
- package/package.json +1 -1
- package/src/agent-manager.ts +8 -0
- package/src/bounded-output.ts +35 -0
- package/src/index.ts +30 -14
- package/src/notifications.ts +7 -5
- package/src/peek.ts +25 -12
- package/.plans/PLAN-next-changes.md +0 -183
- package/.plans/README.md +0 -14
- package/reviews/proposal-structured-output-schema.md +0 -135
- package/reviews/recursive-subagent-widget-preview-rev2.png +0 -0
- package/reviews/recursive-subagent-widget-preview.html +0 -137
- package/reviews/recursive-subagent-widget-preview.png +0 -0
- package/reviews/subagent-features-comparison.md +0 -350
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,17 @@ Every release must:
|
|
|
17
17
|
3. **Update `README.md` to document any new or changed user-facing features.**
|
|
18
18
|
4. Run and pass: `npm run lint`, `npm run typecheck`, `npm test`, `npm run build`.
|
|
19
19
|
5. Commit and push.
|
|
20
|
-
6.
|
|
20
|
+
6. Push a `vX.Y.Z` tag. The `release.yml` workflow then publishes to npm and creates a GitHub Release automatically.
|
|
21
|
+
|
|
22
|
+
One-time npm setup for CI publishing:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm trust github @clanker-code/pi-subagents --repo=clankercode/pi-subagents --file=release.yml
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If `npm trust` fails, open `https://www.npmjs.com/package/@clanker-code/pi-subagents/access` and add a GitHub Actions trusted publisher for the `release.yml` workflow.
|
|
29
|
+
|
|
30
|
+
See the general guide at `~/.llm-general/npm-autopublish-via-ci.md` for other repos.
|
|
21
31
|
|
|
22
32
|
## Keeping Upstream In Sync
|
|
23
33
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.7] - 2026-06-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`get_subagent_result` output is now bounded** — normal result previews, `verbose: true` conversation previews, and `peek` responses are capped so a large subagent transcript cannot flood the parent context. Truncated responses include the omitted size, continuation guidance, and the agent output file path for full-log inspection. Output files are now attached to background records before the agent can complete, avoiding a fast-completion race where retrieval guidance could miss the transcript path.
|
|
14
|
+
- **Agent-runner e2e no longer waits on an unnecessary prompt turn** — the real-runtime tool-gating test now stops after capturing active tools at session construction, avoiding intermittent 30s timeouts under full-suite load.
|
|
15
|
+
|
|
16
|
+
## [0.10.6] - 2026-06-22
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Release workflow test** — patch bump to verify the tag-driven CI publish and GitHub Release creation.
|
|
20
|
+
|
|
10
21
|
## [0.10.5] - 2026-06-21
|
|
11
22
|
|
|
12
23
|
### Changed
|
|
@@ -23,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
23
34
|
- **Fork metadata applied** — package author, repository, homepage, bugs, and media URLs now point to the `clankercode/pi-subagents` fork.
|
|
24
35
|
|
|
25
36
|
### Fixed
|
|
26
|
-
- **Subagent completion notifications now include final output and explicit retrieval guidance** — both the machine-readable XML payload and the visible custom renderer include a `get_subagent_result <id>` instruction plus the transcript file path
|
|
37
|
+
- **Subagent completion notifications now include final output previews and explicit retrieval guidance** — both the machine-readable XML payload and the visible custom renderer include a `get_subagent_result <id>` bounded-preview instruction plus the transcript file path for full-log inspection when available.
|
|
27
38
|
|
|
28
39
|
## [0.10.3] - 2026-06-12
|
|
29
40
|
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
19
19
|
### Fork-specific differences
|
|
20
20
|
|
|
21
21
|
- **Packaging under `@clanker-code`** — published from this fork, not the upstream namespace.
|
|
22
|
-
- **Completion notifications include
|
|
22
|
+
- **Completion notifications include bounded-output guidance** — every notification carries a final output preview plus an explicit `get_subagent_result <id>` bounded-preview instruction and transcript path for full-log inspection when available.
|
|
23
23
|
- Additional fork-specific changes are listed in the [CHANGELOG](./CHANGELOG.md).
|
|
24
24
|
|
|
25
25
|
Upstream changes are reviewed for cherry-picking when practical; otherwise they are reimplemented to fit this fork.
|
|
@@ -41,7 +41,7 @@ Upstream changes are reviewed for cherry-picking when practical; otherwise they
|
|
|
41
41
|
- **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
|
|
42
42
|
- **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
|
|
43
43
|
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
|
|
44
|
-
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show
|
|
44
|
+
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show a bounded preview and transcript path. Group completions render each agent individually
|
|
45
45
|
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
|
|
46
46
|
- **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
|
|
47
47
|
- **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same steering-style `subagent-notification` path as manual background completions; manage via `/agents → Scheduled jobs`
|
|
@@ -279,17 +279,18 @@ Launch a sub-agent.
|
|
|
279
279
|
| `isolation` | `"worktree"` | no | Run in an isolated git worktree |
|
|
280
280
|
| `inherit_context` | boolean | no | Fork parent conversation into agent |
|
|
281
281
|
|
|
282
|
-
All `Agent` calls run in the background. Completion notifications are delivered automatically; use `get_subagent_result` only when you explicitly need to check status or
|
|
282
|
+
All `Agent` calls run in the background. Completion notifications are delivered automatically; use `get_subagent_result` only when you explicitly need to check status or inspect a bounded result preview. Full transcripts are written to the agent output file shown in the `Agent` response and `get_subagent_result` output.
|
|
283
283
|
|
|
284
284
|
### `get_subagent_result`
|
|
285
285
|
|
|
286
|
-
Check status and retrieve
|
|
286
|
+
Check status and retrieve bounded output from a background agent. The tool is safe-by-default for LLM context: normal results, verbose conversation output, and `peek` responses are capped. If output is truncated, the response includes the output file path and continuation guidance.
|
|
287
287
|
|
|
288
288
|
| Parameter | Type | Required | Description |
|
|
289
289
|
|-----------|------|----------|-------------|
|
|
290
290
|
| `agent_id` | string | yes | Agent ID to check |
|
|
291
291
|
| `wait` | boolean | no | Wait for completion |
|
|
292
|
-
| `verbose` | boolean | no | Include
|
|
292
|
+
| `verbose` | boolean | no | Include a bounded conversation preview |
|
|
293
|
+
| `peek` | object | no | Return a bounded tail/filter view with line numbers. Supports `lines`, `regex`, and `after` for incremental reads. |
|
|
293
294
|
|
|
294
295
|
### `steer_subagent`
|
|
295
296
|
|
package/RELEASE.md
CHANGED
|
@@ -21,19 +21,25 @@
|
|
|
21
21
|
git push
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
4. **
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
git tag vX.Y.Z
|
|
28
|
-
git push origin vX.Y.Z
|
|
29
|
-
```
|
|
30
|
-
- Open the [GitHub releases page](https://github.com/clankercode/pi-subagents/releases) and create a new release for the tag.
|
|
31
|
-
- Copy the relevant `[x.y.z]` section from `CHANGELOG.md` into the release notes.
|
|
32
|
-
- Highlight any breaking changes, fork-specific features, or upgrade notes.
|
|
33
|
-
|
|
34
|
-
5. **Publish to npm**
|
|
24
|
+
4. **Push the version tag**
|
|
25
|
+
The `release.yml` workflow publishes to npm and creates the GitHub Release automatically:
|
|
35
26
|
```bash
|
|
36
|
-
|
|
27
|
+
git tag vX.Y.Z
|
|
28
|
+
git push origin vX.Y.Z
|
|
37
29
|
```
|
|
38
30
|
|
|
31
|
+
5. **Verify**
|
|
32
|
+
- Check the [Actions run](https://github.com/clankercode/pi-subagents/actions) succeeded.
|
|
33
|
+
- Confirm the package version appears on npm: `npm view @clanker-code/pi-subagents`.
|
|
34
|
+
- Confirm the GitHub Release has the changelog notes.
|
|
35
|
+
|
|
36
|
+
One-time npm trusted-publisher setup:
|
|
37
|
+
```bash
|
|
38
|
+
npm trust github @clanker-code/pi-subagents --repo=clankercode/pi-subagents --file=release.yml
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If `npm trust` fails, open `https://www.npmjs.com/package/@clanker-code/pi-subagents/access` and add a GitHub Actions trusted publisher for the `release.yml` workflow.
|
|
42
|
+
|
|
43
|
+
See `~/.llm-general/npm-autopublish-via-ci.md` for general instructions.
|
|
44
|
+
|
|
39
45
|
> Note: `prepublishOnly` already runs lint, typecheck, tests, and build before publishing.
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ interface SpawnOptions {
|
|
|
53
53
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
54
54
|
/** Called on streaming text deltas from the assistant response. */
|
|
55
55
|
onTextDelta?: (delta: string, fullText: string) => void;
|
|
56
|
+
/** Build a per-agent output file path before the agent can complete. */
|
|
57
|
+
outputFileForAgent?: (agentId: string) => string;
|
|
58
|
+
/** Called after the output file path is attached to the record. */
|
|
59
|
+
onOutputFileCreated?: (path: string, agentId: string) => void;
|
|
56
60
|
/** Called when the agent session is created (for accessing session stats). */
|
|
57
61
|
onSessionCreated?: (session: AgentSession) => void;
|
|
58
62
|
/** Called at the end of each agentic turn with the cumulative count. */
|
package/dist/agent-manager.js
CHANGED
|
@@ -98,6 +98,10 @@ export class AgentManager {
|
|
|
98
98
|
depth,
|
|
99
99
|
parentAgentId: options.parentAgentId,
|
|
100
100
|
};
|
|
101
|
+
if (options.outputFileForAgent) {
|
|
102
|
+
record.outputFile = options.outputFileForAgent(id);
|
|
103
|
+
options.onOutputFileCreated?.(record.outputFile, id);
|
|
104
|
+
}
|
|
101
105
|
this.agents.set(id, record);
|
|
102
106
|
const args = { pi, ctx, type, prompt, options };
|
|
103
107
|
if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared caps for tool responses that may otherwise flood the parent context.
|
|
3
|
+
* Full subagent logs remain available via the per-agent output file.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_RESULT_CHARS = 20000;
|
|
6
|
+
export declare const MAX_VERBOSE_CHARS = 20000;
|
|
7
|
+
export declare const MAX_PEEK_CHARS = 20000;
|
|
8
|
+
export declare const MAX_PEEK_LINES = 200;
|
|
9
|
+
export interface LimitedText {
|
|
10
|
+
text: string;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
omittedChars: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function limitText(text: string, maxChars: number): LimitedText;
|
|
15
|
+
export declare function clampPeekLines(lines: number | undefined): number;
|
|
16
|
+
export declare function formatOutputFileHint(outputFile?: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared caps for tool responses that may otherwise flood the parent context.
|
|
3
|
+
* Full subagent logs remain available via the per-agent output file.
|
|
4
|
+
*/
|
|
5
|
+
export const MAX_RESULT_CHARS = 20_000;
|
|
6
|
+
export const MAX_VERBOSE_CHARS = 20_000;
|
|
7
|
+
export const MAX_PEEK_CHARS = 20_000;
|
|
8
|
+
export const MAX_PEEK_LINES = 200;
|
|
9
|
+
export function limitText(text, maxChars) {
|
|
10
|
+
if (text.length <= maxChars) {
|
|
11
|
+
return { text, truncated: false, omittedChars: 0 };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
text: text.slice(0, maxChars),
|
|
15
|
+
truncated: true,
|
|
16
|
+
omittedChars: text.length - maxChars,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function clampPeekLines(lines) {
|
|
20
|
+
if (typeof lines !== "number" || !Number.isFinite(lines) || lines < 1)
|
|
21
|
+
return 20;
|
|
22
|
+
return Math.min(Math.floor(lines), MAX_PEEK_LINES);
|
|
23
|
+
}
|
|
24
|
+
export function formatOutputFileHint(outputFile) {
|
|
25
|
+
return outputFile ? ` Full output/log: ${outputFile}` : "";
|
|
26
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { AgentManager } from "./agent-manager.js";
|
|
|
22
22
|
import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDepth, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
23
23
|
import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
|
|
24
24
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
25
|
+
import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
|
|
25
26
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
26
27
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
27
28
|
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
@@ -236,7 +237,7 @@ export default function (pi) {
|
|
|
236
237
|
}
|
|
237
238
|
pi.sendMessage({
|
|
238
239
|
customType: "subagent-notification",
|
|
239
|
-
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
240
|
+
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for a bounded preview; inspect the transcript file path for full output when needed.`,
|
|
240
241
|
display: true,
|
|
241
242
|
details,
|
|
242
243
|
}, { deliverAs: "steer", triggerTurn: true });
|
|
@@ -823,7 +824,7 @@ export default function (pi) {
|
|
|
823
824
|
widget.update();
|
|
824
825
|
const resumeDetails = { displayName: getDisplayName(record.type), description: record.description, subagentType: record.type, modelName: record.invocation?.modelName };
|
|
825
826
|
return textResult(`Agent resumed in background.\nAgent ID: ${record.id}\nType: ${resumeDetails.displayName}\nDescription: ${record.description}\n\n` +
|
|
826
|
-
`You will be notified when this agent completes.\nUse get_subagent_result to
|
|
827
|
+
`You will be notified when this agent completes.\nUse get_subagent_result to inspect bounded result previews, or steer_subagent to send it messages.\nDo not duplicate this agent's work.`, buildDetails(resumeDetails, record, state, { status: "background" }));
|
|
827
828
|
}
|
|
828
829
|
// Background execution
|
|
829
830
|
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
|
|
@@ -852,6 +853,8 @@ export default function (pi) {
|
|
|
852
853
|
invocation: agentInvocation,
|
|
853
854
|
depth: nextSubagentDepth,
|
|
854
855
|
parentAgentId: extensionAgentId,
|
|
856
|
+
outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
|
|
857
|
+
onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt, ctx.cwd),
|
|
855
858
|
...bgCallbacks,
|
|
856
859
|
});
|
|
857
860
|
}
|
|
@@ -862,15 +865,13 @@ export default function (pi) {
|
|
|
862
865
|
const h = stashInvocation(P, retryHandle, ["isolation"]);
|
|
863
866
|
return retryableResult(h, err instanceof Error ? err.message : String(err), "isolation");
|
|
864
867
|
}
|
|
865
|
-
// Set
|
|
866
|
-
//
|
|
868
|
+
// Set join mode synchronously after spawn. The output file path is
|
|
869
|
+
// attached inside AgentManager.spawn(), before the agent can complete.
|
|
867
870
|
const joinMode = resolveJoinMode(defaultJoinMode);
|
|
868
871
|
const record = manager.getRecord(id);
|
|
869
872
|
if (record) {
|
|
870
873
|
record.joinMode = joinMode;
|
|
871
874
|
record.toolCallId = toolCallId;
|
|
872
|
-
record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
873
|
-
writeInitialEntry(record.outputFile, id, P.prompt, ctx.cwd);
|
|
874
875
|
}
|
|
875
876
|
if (joinMode === 'async') {
|
|
876
877
|
// No join mode or explicit async — not part of any batch
|
|
@@ -900,7 +901,7 @@ export default function (pi) {
|
|
|
900
901
|
return textResult(`Agent ${isQueued ? "queued" : "started"} in background.\nAgent ID: ${id}\nType: ${displayName}\nDescription: ${P.description}\n` +
|
|
901
902
|
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
902
903
|
(isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
|
|
903
|
-
`\nYou will be notified when this agent completes.\nUse get_subagent_result to
|
|
904
|
+
`\nYou will be notified when this agent completes.\nUse get_subagent_result to inspect bounded result previews, or steer_subagent to send it messages.\nDo not duplicate this agent's work.`, { ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background", agentId: id });
|
|
904
905
|
},
|
|
905
906
|
}));
|
|
906
907
|
// ---- get_subagent_result tool ----
|
|
@@ -917,7 +918,7 @@ export default function (pi) {
|
|
|
917
918
|
description: `If true, block until the agent completes before returning. Blocks up to the configured wait timeout (${formatWaitTimeout(getWaitTimeoutSeconds())} by default); if the agent is still running when the timeout is reached, returns its current status — call again with wait: true to keep waiting. Interruptible by the parent turn. Default: false.`,
|
|
918
919
|
})),
|
|
919
920
|
verbose: Type.Optional(Type.Boolean({
|
|
920
|
-
description: "If true, include the agent's
|
|
921
|
+
description: "If true, include a bounded preview of the agent's conversation (messages + tool calls). Default: false.",
|
|
921
922
|
})),
|
|
922
923
|
peek: Type.Optional(Type.Object({
|
|
923
924
|
lines: Type.Optional(Type.Number({ minimum: 1, description: "Number of trailing lines to return. Default: 20." })),
|
|
@@ -936,7 +937,7 @@ export default function (pi) {
|
|
|
936
937
|
if (params.peek && !params.verbose) {
|
|
937
938
|
const peek = peekAgentOutput(record, params.peek);
|
|
938
939
|
return textResult(peek
|
|
939
|
-
? `${peek.text}\n\n---\nUse verbose: true for
|
|
940
|
+
? `${peek.text}\n\n---\nUse verbose: true for a bounded conversation preview, or omit peek for a bounded result preview.`
|
|
940
941
|
: "No output yet for this agent.");
|
|
941
942
|
}
|
|
942
943
|
// WAIT — race the agent's completion against the configured timeout and
|
|
@@ -964,27 +965,42 @@ export default function (pi) {
|
|
|
964
965
|
statsParts.push(`Duration: ${duration}`);
|
|
965
966
|
let output = `Agent: ${record.id}\n` +
|
|
966
967
|
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
967
|
-
`Description: ${record.description}\n
|
|
968
|
+
`Description: ${record.description}\n` +
|
|
969
|
+
(record.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
970
|
+
`\n`;
|
|
968
971
|
if (record.status === "running") {
|
|
969
972
|
// The wait returned while the agent was still running (timeout or abort).
|
|
970
973
|
output += waitTimeoutMessage(waitOutcome, getWaitTimeoutSeconds());
|
|
971
974
|
}
|
|
972
975
|
else if (record.status === "error") {
|
|
973
|
-
|
|
976
|
+
const limited = limitText(record.error ?? "unknown", MAX_RESULT_CHARS);
|
|
977
|
+
output += `Error: ${limited.text}`;
|
|
978
|
+
if (limited.truncated) {
|
|
979
|
+
output += `\n\n---\nError truncated: omitted ${limited.omittedChars} chars. Inspect the output file for the full error when available.${formatOutputFileHint(record.outputFile)}`;
|
|
980
|
+
}
|
|
974
981
|
}
|
|
975
982
|
else {
|
|
976
|
-
|
|
983
|
+
const resultText = record.result?.trim() || "No output.";
|
|
984
|
+
const limited = limitText(resultText, MAX_RESULT_CHARS);
|
|
985
|
+
output += limited.text;
|
|
986
|
+
if (limited.truncated) {
|
|
987
|
+
output += `\n\n---\nResult truncated: omitted ${limited.omittedChars} chars. Use peek for targeted retrieval, or inspect the output file for the full log.${formatOutputFileHint(record.outputFile)}`;
|
|
988
|
+
}
|
|
977
989
|
}
|
|
978
990
|
// Mark result as consumed — suppresses the completion notification
|
|
979
991
|
if (record.status !== "running" && record.status !== "queued") {
|
|
980
992
|
record.resultConsumed = true;
|
|
981
993
|
cancelNudge(params.agent_id);
|
|
982
994
|
}
|
|
983
|
-
// Verbose: include
|
|
995
|
+
// Verbose: include a bounded conversation preview
|
|
984
996
|
if (params.verbose && record.session) {
|
|
985
997
|
const conversation = getAgentConversation(record.session);
|
|
986
998
|
if (conversation) {
|
|
987
|
-
|
|
999
|
+
const limited = limitText(conversation, MAX_VERBOSE_CHARS);
|
|
1000
|
+
output += `\n\n--- Agent Conversation ---\n${limited.text}`;
|
|
1001
|
+
if (limited.truncated) {
|
|
1002
|
+
output += `\n\n---\nAgent conversation truncated: omitted ${limited.omittedChars} chars. Use peek for targeted retrieval, or inspect the output file for the full log.${formatOutputFileHint(record.outputFile)}`;
|
|
1003
|
+
}
|
|
988
1004
|
}
|
|
989
1005
|
}
|
|
990
1006
|
return textResult(output);
|
package/dist/notifications.js
CHANGED
|
@@ -23,12 +23,14 @@ export function formatTaskNotification(record, resultMaxLen) {
|
|
|
23
23
|
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
24
24
|
const resultPreview = record.result
|
|
25
25
|
? record.result.length > resultMaxLen
|
|
26
|
-
? record.result.slice(0, resultMaxLen) +
|
|
26
|
+
? record.result.slice(0, resultMaxLen) + (record.outputFile
|
|
27
|
+
? "\n...(truncated, use get_subagent_result for a bounded preview or inspect the transcript file)"
|
|
28
|
+
: "\n...(truncated, use get_subagent_result for a bounded preview)")
|
|
27
29
|
: record.result
|
|
28
30
|
: "No output.";
|
|
29
31
|
const fullOutputInstruction = record.outputFile
|
|
30
|
-
? `Read
|
|
31
|
-
: `Read
|
|
32
|
+
? `Read a bounded preview with get_subagent_result for agent ${record.id}, or inspect the full transcript file: ${record.outputFile}`
|
|
33
|
+
: `Read a bounded preview with get_subagent_result for agent ${record.id}.`;
|
|
32
34
|
return [
|
|
33
35
|
`<task-notification>`,
|
|
34
36
|
`<task-id>${record.id}</task-id>`,
|
|
@@ -96,8 +98,8 @@ export function registerSubagentNotificationRenderer(pi) {
|
|
|
96
98
|
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
97
99
|
}
|
|
98
100
|
const fullOutputHint = d.outputFile
|
|
99
|
-
? `
|
|
100
|
-
: `
|
|
101
|
+
? `bounded preview: get_subagent_result ${d.id}; full transcript: ${d.outputFile}`
|
|
102
|
+
: `bounded preview: get_subagent_result ${d.id}`;
|
|
101
103
|
line += "\n " + theme.fg("muted", fullOutputHint);
|
|
102
104
|
return line;
|
|
103
105
|
}
|
package/dist/peek.js
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* `after` for incremental updates without missing anything.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
-
|
|
18
|
-
const DEFAULT_LINES = 20;
|
|
17
|
+
import { clampPeekLines, formatOutputFileHint, limitText, MAX_PEEK_CHARS, MAX_PEEK_LINES } from "./bounded-output.js";
|
|
19
18
|
/**
|
|
20
19
|
* Produce a peek view of an agent's output. Returns null when there is no
|
|
21
20
|
* source content at all (the caller renders a "no output yet" message).
|
|
@@ -26,20 +25,33 @@ export function peekAgentOutput(record, opts = {}) {
|
|
|
26
25
|
return null;
|
|
27
26
|
const regex = opts.regex ? compileRegex(opts.regex) : undefined;
|
|
28
27
|
const after = typeof opts.after === "number" ? opts.after : -1;
|
|
29
|
-
const tail =
|
|
28
|
+
const tail = clampPeekLines(opts.lines);
|
|
30
29
|
// Index each source line with its original position (1-based for display).
|
|
31
30
|
const indexed = lines.map((text, i) => ({ no: i + 1, text }));
|
|
32
31
|
// Filter-then-select.
|
|
33
32
|
const filtered = regex ? indexed.filter((l) => regex.test(l.text)) : indexed;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const matching = after >= 0 ? filtered.filter((l) => l.no > after) : filtered;
|
|
34
|
+
const selected = after >= 0 ? matching.slice(0, MAX_PEEK_LINES) : matching.slice(-tail);
|
|
35
|
+
const lineLimited = matching.length > selected.length;
|
|
37
36
|
const totalLines = lines.length;
|
|
38
37
|
const isRunning = record.status === "running" || record.status === "queued";
|
|
39
38
|
const source = isRunning && record.outputFile && existsSync(record.outputFile) ? "outputFile" : "result";
|
|
40
|
-
const header = buildHeader(opts, selected.length, totalLines, source);
|
|
39
|
+
const header = buildHeader(opts, selected.length, totalLines, source, tail, lineLimited, matching.length);
|
|
41
40
|
const body = selected.map((l) => `[${l.no}] ${l.text}`).join("\n");
|
|
42
|
-
|
|
41
|
+
const limited = limitText(body, MAX_PEEK_CHARS);
|
|
42
|
+
const lastLine = selected.at(-1)?.no;
|
|
43
|
+
const notices = [];
|
|
44
|
+
if (lineLimited && lastLine !== undefined) {
|
|
45
|
+
notices.push(`Peek limited to ${MAX_PEEK_LINES} lines from ${matching.length} matching lines. Use peek.after: ${lastLine} to continue.`);
|
|
46
|
+
}
|
|
47
|
+
if (limited.truncated) {
|
|
48
|
+
notices.push(`Peek output truncated by ${limited.omittedChars} chars. Use a smaller lines value or regex filter.${formatOutputFileHint(record.outputFile)}`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
text: `${header}\n\n${limited.text}${notices.length ? `\n\n---\n${notices.join("\n")}` : ""}`,
|
|
52
|
+
totalLines,
|
|
53
|
+
source,
|
|
54
|
+
};
|
|
43
55
|
}
|
|
44
56
|
/** Read the most useful text lines from the agent's output. */
|
|
45
57
|
function readSourceLines(record) {
|
|
@@ -105,14 +117,15 @@ function compileRegex(pattern) {
|
|
|
105
117
|
return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
106
118
|
}
|
|
107
119
|
}
|
|
108
|
-
function buildHeader(opts, shown, total, source) {
|
|
120
|
+
function buildHeader(opts, shown, total, source, tail, lineLimited, matching) {
|
|
109
121
|
const parts = [];
|
|
110
122
|
if (typeof opts.after === "number") {
|
|
111
123
|
parts.push(`after line number ${opts.after}`);
|
|
124
|
+
if (lineLimited)
|
|
125
|
+
parts.push(`limited to ${MAX_PEEK_LINES} lines from ${matching} matching lines`);
|
|
112
126
|
}
|
|
113
127
|
else {
|
|
114
|
-
|
|
115
|
-
parts.push(`last ${n} lines`);
|
|
128
|
+
parts.push(`last ${tail} lines`);
|
|
116
129
|
}
|
|
117
130
|
if (opts.regex)
|
|
118
131
|
parts.push(`filtered by regex /${opts.regex}/`);
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -90,6 +90,10 @@ interface SpawnOptions {
|
|
|
90
90
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
91
91
|
/** Called on streaming text deltas from the assistant response. */
|
|
92
92
|
onTextDelta?: (delta: string, fullText: string) => void;
|
|
93
|
+
/** Build a per-agent output file path before the agent can complete. */
|
|
94
|
+
outputFileForAgent?: (agentId: string) => string;
|
|
95
|
+
/** Called after the output file path is attached to the record. */
|
|
96
|
+
onOutputFileCreated?: (path: string, agentId: string) => void;
|
|
93
97
|
/** Called when the agent session is created (for accessing session stats). */
|
|
94
98
|
onSessionCreated?: (session: AgentSession) => void;
|
|
95
99
|
/** Called at the end of each agentic turn with the cumulative count. */
|
|
@@ -185,6 +189,10 @@ export class AgentManager {
|
|
|
185
189
|
depth,
|
|
186
190
|
parentAgentId: options.parentAgentId,
|
|
187
191
|
};
|
|
192
|
+
if (options.outputFileForAgent) {
|
|
193
|
+
record.outputFile = options.outputFileForAgent(id);
|
|
194
|
+
options.onOutputFileCreated?.(record.outputFile, id);
|
|
195
|
+
}
|
|
188
196
|
this.agents.set(id, record);
|
|
189
197
|
|
|
190
198
|
const args: SpawnArgs = { pi, ctx, type, prompt, options };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared caps for tool responses that may otherwise flood the parent context.
|
|
3
|
+
* Full subagent logs remain available via the per-agent output file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const MAX_RESULT_CHARS = 20_000;
|
|
7
|
+
export const MAX_VERBOSE_CHARS = 20_000;
|
|
8
|
+
export const MAX_PEEK_CHARS = 20_000;
|
|
9
|
+
export const MAX_PEEK_LINES = 200;
|
|
10
|
+
|
|
11
|
+
export interface LimitedText {
|
|
12
|
+
text: string;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
omittedChars: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function limitText(text: string, maxChars: number): LimitedText {
|
|
18
|
+
if (text.length <= maxChars) {
|
|
19
|
+
return { text, truncated: false, omittedChars: 0 };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
text: text.slice(0, maxChars),
|
|
23
|
+
truncated: true,
|
|
24
|
+
omittedChars: text.length - maxChars,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clampPeekLines(lines: number | undefined): number {
|
|
29
|
+
if (typeof lines !== "number" || !Number.isFinite(lines) || lines < 1) return 20;
|
|
30
|
+
return Math.min(Math.floor(lines), MAX_PEEK_LINES);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatOutputFileHint(outputFile?: string): string {
|
|
34
|
+
return outputFile ? ` Full output/log: ${outputFile}` : "";
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { AgentManager } from "./agent-manager.js";
|
|
|
23
23
|
import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDepth, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
24
24
|
import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
|
|
25
25
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
26
|
+
import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
|
|
26
27
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
27
28
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
28
29
|
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
@@ -280,7 +281,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
280
281
|
|
|
281
282
|
pi.sendMessage<NotificationDetails>({
|
|
282
283
|
customType: "subagent-notification",
|
|
283
|
-
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
284
|
+
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for a bounded preview; inspect the transcript file path for full output when needed.`,
|
|
284
285
|
display: true,
|
|
285
286
|
details,
|
|
286
287
|
}, { deliverAs: "steer", triggerTurn: true });
|
|
@@ -953,7 +954,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
953
954
|
const resumeDetails = { displayName: getDisplayName(record.type), description: record.description, subagentType: record.type, modelName: record.invocation?.modelName };
|
|
954
955
|
return textResult(
|
|
955
956
|
`Agent resumed in background.\nAgent ID: ${record.id}\nType: ${resumeDetails.displayName}\nDescription: ${record.description}\n\n` +
|
|
956
|
-
`You will be notified when this agent completes.\nUse get_subagent_result to
|
|
957
|
+
`You will be notified when this agent completes.\nUse get_subagent_result to inspect bounded result previews, or steer_subagent to send it messages.\nDo not duplicate this agent's work.`,
|
|
957
958
|
buildDetails(resumeDetails, record, state, { status: "background" }),
|
|
958
959
|
);
|
|
959
960
|
}
|
|
@@ -987,6 +988,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
987
988
|
invocation: agentInvocation,
|
|
988
989
|
depth: nextSubagentDepth,
|
|
989
990
|
parentAgentId: extensionAgentId,
|
|
991
|
+
outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
|
|
992
|
+
onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt!, ctx.cwd),
|
|
990
993
|
...bgCallbacks,
|
|
991
994
|
});
|
|
992
995
|
} catch (err) {
|
|
@@ -997,15 +1000,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
997
1000
|
return retryableResult(h, err instanceof Error ? err.message : String(err), "isolation");
|
|
998
1001
|
}
|
|
999
1002
|
|
|
1000
|
-
// Set
|
|
1001
|
-
//
|
|
1003
|
+
// Set join mode synchronously after spawn. The output file path is
|
|
1004
|
+
// attached inside AgentManager.spawn(), before the agent can complete.
|
|
1002
1005
|
const joinMode = resolveJoinMode(defaultJoinMode);
|
|
1003
1006
|
const record = manager.getRecord(id);
|
|
1004
1007
|
if (record) {
|
|
1005
1008
|
record.joinMode = joinMode;
|
|
1006
1009
|
record.toolCallId = toolCallId;
|
|
1007
|
-
record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
1008
|
-
writeInitialEntry(record.outputFile, id, P.prompt!, ctx.cwd);
|
|
1009
1010
|
}
|
|
1010
1011
|
|
|
1011
1012
|
if (joinMode === 'async') {
|
|
@@ -1038,7 +1039,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1038
1039
|
`Agent ${isQueued ? "queued" : "started"} in background.\nAgent ID: ${id}\nType: ${displayName}\nDescription: ${P.description}\n` +
|
|
1039
1040
|
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
1040
1041
|
(isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
|
|
1041
|
-
`\nYou will be notified when this agent completes.\nUse get_subagent_result to
|
|
1042
|
+
`\nYou will be notified when this agent completes.\nUse get_subagent_result to inspect bounded result previews, or steer_subagent to send it messages.\nDo not duplicate this agent's work.`,
|
|
1042
1043
|
{ ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
|
|
1043
1044
|
);
|
|
1044
1045
|
},
|
|
@@ -1063,7 +1064,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1063
1064
|
),
|
|
1064
1065
|
verbose: Type.Optional(
|
|
1065
1066
|
Type.Boolean({
|
|
1066
|
-
description: "If true, include the agent's
|
|
1067
|
+
description: "If true, include a bounded preview of the agent's conversation (messages + tool calls). Default: false.",
|
|
1067
1068
|
}),
|
|
1068
1069
|
),
|
|
1069
1070
|
peek: Type.Optional(
|
|
@@ -1087,7 +1088,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1087
1088
|
const peek = peekAgentOutput(record, params.peek as PeekOptions);
|
|
1088
1089
|
return textResult(
|
|
1089
1090
|
peek
|
|
1090
|
-
? `${peek.text}\n\n---\nUse verbose: true for
|
|
1091
|
+
? `${peek.text}\n\n---\nUse verbose: true for a bounded conversation preview, or omit peek for a bounded result preview.`
|
|
1091
1092
|
: "No output yet for this agent.",
|
|
1092
1093
|
);
|
|
1093
1094
|
}
|
|
@@ -1117,15 +1118,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
1117
1118
|
let output =
|
|
1118
1119
|
`Agent: ${record.id}\n` +
|
|
1119
1120
|
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
1120
|
-
`Description: ${record.description}\n
|
|
1121
|
+
`Description: ${record.description}\n` +
|
|
1122
|
+
(record.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
1123
|
+
`\n`;
|
|
1121
1124
|
|
|
1122
1125
|
if (record.status === "running") {
|
|
1123
1126
|
// The wait returned while the agent was still running (timeout or abort).
|
|
1124
1127
|
output += waitTimeoutMessage(waitOutcome, getWaitTimeoutSeconds());
|
|
1125
1128
|
} else if (record.status === "error") {
|
|
1126
|
-
|
|
1129
|
+
const limited = limitText(record.error ?? "unknown", MAX_RESULT_CHARS);
|
|
1130
|
+
output += `Error: ${limited.text}`;
|
|
1131
|
+
if (limited.truncated) {
|
|
1132
|
+
output += `\n\n---\nError truncated: omitted ${limited.omittedChars} chars. Inspect the output file for the full error when available.${formatOutputFileHint(record.outputFile)}`;
|
|
1133
|
+
}
|
|
1127
1134
|
} else {
|
|
1128
|
-
|
|
1135
|
+
const resultText = record.result?.trim() || "No output.";
|
|
1136
|
+
const limited = limitText(resultText, MAX_RESULT_CHARS);
|
|
1137
|
+
output += limited.text;
|
|
1138
|
+
if (limited.truncated) {
|
|
1139
|
+
output += `\n\n---\nResult truncated: omitted ${limited.omittedChars} chars. Use peek for targeted retrieval, or inspect the output file for the full log.${formatOutputFileHint(record.outputFile)}`;
|
|
1140
|
+
}
|
|
1129
1141
|
}
|
|
1130
1142
|
|
|
1131
1143
|
// Mark result as consumed — suppresses the completion notification
|
|
@@ -1134,11 +1146,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1134
1146
|
cancelNudge(params.agent_id);
|
|
1135
1147
|
}
|
|
1136
1148
|
|
|
1137
|
-
// Verbose: include
|
|
1149
|
+
// Verbose: include a bounded conversation preview
|
|
1138
1150
|
if (params.verbose && record.session) {
|
|
1139
1151
|
const conversation = getAgentConversation(record.session);
|
|
1140
1152
|
if (conversation) {
|
|
1141
|
-
|
|
1153
|
+
const limited = limitText(conversation, MAX_VERBOSE_CHARS);
|
|
1154
|
+
output += `\n\n--- Agent Conversation ---\n${limited.text}`;
|
|
1155
|
+
if (limited.truncated) {
|
|
1156
|
+
output += `\n\n---\nAgent conversation truncated: omitted ${limited.omittedChars} chars. Use peek for targeted retrieval, or inspect the output file for the full log.${formatOutputFileHint(record.outputFile)}`;
|
|
1157
|
+
}
|
|
1142
1158
|
}
|
|
1143
1159
|
}
|
|
1144
1160
|
|