pi-subagent-in-memory 0.1.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 +151 -0
- package/extensions/index.ts +552 -0
- package/extensions/model.ts +50 -0
- package/extensions/tui-draw.ts +73 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# pi-subagent-in-memory
|
|
2
|
+
|
|
3
|
+
In-process subagent tool for [pi](https://github.com/nicholasgasior/pi-coding-agent) with live TUI card widgets, JSONL session logging, and **zero system-prompt overhead**.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Key Design Principle
|
|
8
|
+
|
|
9
|
+
**This extension adds nothing to your LLM context beyond tool parameter definitions.** No system prompt injection, no hidden instructions, no pre-determined behavior โ the LLM only sees the `subagent_create` tool schema and decides how to use it naturally.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### ๐ค `subagent_create` Tool
|
|
14
|
+
|
|
15
|
+
Spawns an in-process subagent session using pi's `createAgentSession` SDK. The subagent runs in the same process (not a subprocess), with its own session, tools, and model. Multiple subagents can run in parallel.
|
|
16
|
+
|
|
17
|
+
**Tool parameters:**
|
|
18
|
+
|
|
19
|
+
| Parameter | Type | Required | Description |
|
|
20
|
+
|-----------|------|----------|-------------|
|
|
21
|
+
| `task` | string | โ
| The task for the subagent to perform |
|
|
22
|
+
| `title` | string | | Display title for the card widget |
|
|
23
|
+
| `provider` | string | | LLM provider (e.g. `anthropic`, `google`, `openai`) |
|
|
24
|
+
| `model` | string | | Model ID. Supports `provider/model` format (e.g. `openai/gpt-4o-mini`) |
|
|
25
|
+
| `cwd` | string | | Working directory for the subagent |
|
|
26
|
+
| `timeout` | number | | Timeout in seconds. Aborts the subagent if exceeded |
|
|
27
|
+
| `columnWidthPercent` | number | | Card width as % of terminal (33โ100). Controls card grid layout |
|
|
28
|
+
|
|
29
|
+
If `provider` and `model` are omitted, the subagent inherits the main agent's model.
|
|
30
|
+
|
|
31
|
+
### ๐ Live TUI Card Widgets
|
|
32
|
+
|
|
33
|
+
Each running subagent is displayed as a colored card widget above the editor:
|
|
34
|
+
|
|
35
|
+
- Cards show **title**, **model**, **live output preview**, **elapsed time**, and **status icon** (โ running, โ completed, โ error)
|
|
36
|
+
- Cards auto-layout into a responsive grid (1โ3 columns based on `columnWidthPercent`)
|
|
37
|
+
- Output preview shows the last 4 lines of subagent activity, including tool execution indicators (โณ running, โ
done, โ failed)
|
|
38
|
+
- Six rotating color themes for visual distinction between cards
|
|
39
|
+
|
|
40
|
+
### ๐ JSONL Session Logging
|
|
41
|
+
|
|
42
|
+
Every subagent session is logged to disk for debugging and auditing:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
.pi/subagent-in-memory/<mainSessionId>/
|
|
46
|
+
โโโ subagent_1/
|
|
47
|
+
โ โโโ events.jsonl # Full event stream (text, tool calls, results)
|
|
48
|
+
โ โโโ result.md # Final subagent output (or error.md on failure)
|
|
49
|
+
โโโ subagent_2/
|
|
50
|
+
โ โโโ events.jsonl
|
|
51
|
+
โ โโโ result.md
|
|
52
|
+
โโโ ...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The JSONL log includes:
|
|
56
|
+
- Session metadata (model, provider, task, cwd)
|
|
57
|
+
- Aggregated text output (deltas combined into single entries)
|
|
58
|
+
- Tool call arguments and results
|
|
59
|
+
- Timestamps and parent event IDs for tracing
|
|
60
|
+
|
|
61
|
+
### ๐ Nested Subagent Support
|
|
62
|
+
|
|
63
|
+
Subagents can spawn their own subagents. All nested cards render in the main agent's widget โ they share the same module-level state regardless of nesting depth. This is achieved by passing the `subagent_create` tool directly as an `AgentTool` to child sessions.
|
|
64
|
+
|
|
65
|
+
### ๐งน `/in-memory-clear-widgets` Slash Command
|
|
66
|
+
|
|
67
|
+
Type `/in-memory-clear-widgets` in the pi prompt to clear all subagent card widgets from the TUI. Useful after a batch of subagent runs when you want a clean view.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
### From local path (recommended for development)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pi install /path/to/pi-subagent-in-memory
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### From a git repository
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pi install git:github.com/your-org/pi-subagent-in-memory
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Project-local install (shared with team via `.pi/settings.json`)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pi install -l /path/to/pi-subagent-in-memory
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### One-time use without installing
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pi -e /path/to/pi-subagent-in-memory/extensions/index.ts
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Remove
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pi remove pi-subagent-in-memory
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For project-local installs:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pi remove -l pi-subagent-in-memory
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Verify Installation
|
|
108
|
+
|
|
109
|
+
After installing, start pi and check:
|
|
110
|
+
|
|
111
|
+
1. The `subagent_create` tool should appear in the tool list
|
|
112
|
+
2. The `/in-memory-clear-widgets` command should be available (type `/` to see commands)
|
|
113
|
+
3. Ask the agent to "run a subagent to list files" โ you should see a card widget appear
|
|
114
|
+
|
|
115
|
+
## Usage Examples
|
|
116
|
+
|
|
117
|
+
Once installed, the LLM will discover the `subagent_create` tool from its schema and use it when appropriate. Some natural prompts:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
# Single subagent
|
|
121
|
+
"Spawn a subagent to analyze the test coverage in this repo"
|
|
122
|
+
|
|
123
|
+
# Parallel subagents
|
|
124
|
+
"Run 2 subagents in parallel: one to summarize src/ and another to summarize tests/"
|
|
125
|
+
|
|
126
|
+
# Different models
|
|
127
|
+
"Use a subagent with openai/gpt-4o-mini to review the README"
|
|
128
|
+
|
|
129
|
+
# With timeout
|
|
130
|
+
"Spawn a subagent with a 60-second timeout to count lines of code"
|
|
131
|
+
|
|
132
|
+
# Custom working directory
|
|
133
|
+
"Run a subagent in /tmp to check disk space"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## How It Works
|
|
137
|
+
|
|
138
|
+
1. **Tool registration** โ On load, registers `subagent_create` as a tool and `/in-memory-clear-widgets` as a command. No system prompt modifications.
|
|
139
|
+
2. **Session creation** โ When the LLM calls `subagent_create`, a new `createAgentSession` is created in-process with its own model, auth, and coding tools (read, write, edit, bash, grep, find, ls).
|
|
140
|
+
3. **Event streaming** โ All subagent events (text deltas, tool calls, completions) are forwarded as `tool_execution_update` events to the parent agent and logged to JSONL.
|
|
141
|
+
4. **Widget rendering** โ A TUI widget renders card(s) above the editor, updated on every event.
|
|
142
|
+
5. **Result handoff** โ The final text output is written to `result.md`. The parent agent receives a short pointer path, not the full content, keeping context lean.
|
|
143
|
+
|
|
144
|
+
## Requirements
|
|
145
|
+
|
|
146
|
+
- [pi](https://github.com/nicholasgasior/pi-coding-agent) (peer dependency)
|
|
147
|
+
- API keys configured for any providers you want subagents to use (via `pi login` or environment variables)
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-subagent-in-memory โ In-process subagent tool for pi.
|
|
3
|
+
*
|
|
4
|
+
* Registers a `subagent_create` tool that spawns subagent sessions in the same
|
|
5
|
+
* process via the pi SDK's createAgentSession. Live progress is streamed back
|
|
6
|
+
* as tool_execution_update events and rendered as TUI card widgets.
|
|
7
|
+
*
|
|
8
|
+
* Key design principle: apart from tool parameter definitions, this extension
|
|
9
|
+
* adds NOTHING to your LLM context. No system prompt injection, no hidden
|
|
10
|
+
* instructions โ the LLM only sees the tool schema.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Live TUI card widgets showing subagent status and output
|
|
14
|
+
* - JSONL event logging to ~/.pi/subagent-in-memory/<sessionId>/
|
|
15
|
+
* - Nested subagent support (subagents can spawn subagents)
|
|
16
|
+
* - /in-memory-clear-widgets slash command to remove widget cards
|
|
17
|
+
* - Multi-provider support (Anthropic, OpenAI, Google, etc.)
|
|
18
|
+
*
|
|
19
|
+
* Results are written to ./.pi/subagent-in-memory/<mainSessionId>/subagent_<N>/result.md
|
|
20
|
+
* (or error.md on failure) so the calling agent gets a short pointer instead of
|
|
21
|
+
* the full output.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
25
|
+
import {
|
|
26
|
+
createAgentSession,
|
|
27
|
+
SessionManager,
|
|
28
|
+
AuthStorage,
|
|
29
|
+
DefaultResourceLoader,
|
|
30
|
+
getAgentDir,
|
|
31
|
+
createCodingTools,
|
|
32
|
+
createGrepTool,
|
|
33
|
+
createFindTool,
|
|
34
|
+
createLsTool,
|
|
35
|
+
} from "@mariozechner/pi-coding-agent";
|
|
36
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
37
|
+
import type { AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
|
|
38
|
+
import { resolveModel } from "./model.ts";
|
|
39
|
+
import { renderCard, type CardTheme } from "./tui-draw.ts";
|
|
40
|
+
|
|
41
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
42
|
+
import { mkdirSync, writeFileSync, appendFileSync } from "node:fs";
|
|
43
|
+
import { join } from "node:path";
|
|
44
|
+
import { randomUUID } from "node:crypto";
|
|
45
|
+
|
|
46
|
+
// โโ JSONL event logger โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
47
|
+
function jsonlAppend(filePath: string, data: Record<string, any>) {
|
|
48
|
+
appendFileSync(filePath, JSON.stringify(data) + "\n", "utf-8");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// โโ Subagent card state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
52
|
+
interface SubagentCard {
|
|
53
|
+
sessionId: string;
|
|
54
|
+
title: string;
|
|
55
|
+
modelLabel: string;
|
|
56
|
+
status: "created" | "running" | "completed" | "error";
|
|
57
|
+
textPreview: string;
|
|
58
|
+
columnWidthPercent: number;
|
|
59
|
+
startedAt: number;
|
|
60
|
+
endedAt?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const CARD_THEMES: CardTheme[] = [
|
|
64
|
+
{ bg: "\x1b[48;2;20;30;75m", br: "\x1b[38;2;70;110;210m" },
|
|
65
|
+
{ bg: "\x1b[48;2;80;18;28m", br: "\x1b[38;2;210;65;85m" },
|
|
66
|
+
{ bg: "\x1b[48;2;50;22;85m", br: "\x1b[38;2;145;80;220m" },
|
|
67
|
+
{ bg: "\x1b[48;2;12;65;75m", br: "\x1b[38;2;40;175;195m" },
|
|
68
|
+
{ bg: "\x1b[48;2;55;50;10m", br: "\x1b[38;2;190;170;50m" },
|
|
69
|
+
{ bg: "\x1b[48;2;15;55;30m", br: "\x1b[38;2;50;185;100m" },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function formatElapsed(startedAt: number, endedAt?: number): string {
|
|
73
|
+
const elapsed = Math.floor(((endedAt ?? Date.now()) - startedAt) / 1000);
|
|
74
|
+
const m = Math.floor(elapsed / 60);
|
|
75
|
+
const s = elapsed % 60;
|
|
76
|
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// โโ Shared state โ single instance across all nesting levels โโโโ
|
|
80
|
+
const subagents: SubagentCard[] = [];
|
|
81
|
+
let currentCtx: { ui: any } | null = null;
|
|
82
|
+
let mainSessionId = "unknown";
|
|
83
|
+
let subagentCount = 0;
|
|
84
|
+
|
|
85
|
+
function updateSubagentWidget() {
|
|
86
|
+
if (!currentCtx) return;
|
|
87
|
+
const ctx = currentCtx;
|
|
88
|
+
|
|
89
|
+
ctx.ui.setWidget(
|
|
90
|
+
"in-memory-subagent-cards",
|
|
91
|
+
(_tui: any, theme: any) => ({
|
|
92
|
+
render(width: number): string[] {
|
|
93
|
+
if (subagents.length === 0) return [];
|
|
94
|
+
|
|
95
|
+
// Derive cols from columnWidthPercent (all cards share the same value).
|
|
96
|
+
const pct = subagents[subagents.length - 1].columnWidthPercent;
|
|
97
|
+
const cols = Math.min(3, Math.max(1, Math.round(100 / pct)));
|
|
98
|
+
const gap = 1;
|
|
99
|
+
const colWidth = Math.floor((width - gap * (cols - 1)) / cols);
|
|
100
|
+
const maxContentLines = 4;
|
|
101
|
+
const lines: string[] = [""];
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < subagents.length; i += cols) {
|
|
104
|
+
const rowCards = subagents.slice(i, i + cols).map((sa, idx) => {
|
|
105
|
+
const cardTheme = CARD_THEMES[(i + idx) % CARD_THEMES.length];
|
|
106
|
+
|
|
107
|
+
const titleText = `${sa.title} [${sa.modelLabel}]`;
|
|
108
|
+
const innerW = colWidth - 4;
|
|
109
|
+
|
|
110
|
+
const allText = sa.textPreview || "โฆ";
|
|
111
|
+
const contentLines = allText.split("\n");
|
|
112
|
+
const trimmedLines = contentLines.map((l) =>
|
|
113
|
+
visibleWidth(l) > innerW ? "โฆ" + truncateToWidth(l, innerW - 1) : l
|
|
114
|
+
);
|
|
115
|
+
const visible = trimmedLines.slice(-maxContentLines);
|
|
116
|
+
const content = (contentLines.length > maxContentLines ? "โฆ\n" : "") + visible.join("\n");
|
|
117
|
+
|
|
118
|
+
const statusIcon = sa.status === "completed" ? "โ" : sa.status === "error" ? "โ" : "โ";
|
|
119
|
+
const footer = `${statusIcon} ${formatElapsed(sa.startedAt, sa.endedAt)}`;
|
|
120
|
+
|
|
121
|
+
return renderCard({
|
|
122
|
+
title: titleText,
|
|
123
|
+
content,
|
|
124
|
+
footer,
|
|
125
|
+
colWidth,
|
|
126
|
+
theme,
|
|
127
|
+
cardTheme,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Pad incomplete rows
|
|
132
|
+
while (rowCards.length < cols) {
|
|
133
|
+
rowCards.push(Array(rowCards[0].length).fill(" ".repeat(colWidth)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const cardHeight = Math.max(...rowCards.map((c) => c.length));
|
|
137
|
+
for (const card of rowCards) {
|
|
138
|
+
while (card.length < cardHeight) {
|
|
139
|
+
card.push(" ".repeat(colWidth));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (let row = 0; row < cardHeight; row++) {
|
|
144
|
+
lines.push(rowCards.map((card) => card[row]).join(" ".repeat(gap)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return lines;
|
|
149
|
+
},
|
|
150
|
+
invalidate() {},
|
|
151
|
+
}),
|
|
152
|
+
{ placement: "aboveEditor" }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// โโ Parameter schema โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
157
|
+
const SubagentParams = Type.Object({
|
|
158
|
+
task: Type.String({ description: "The task for the subagent to perform" }),
|
|
159
|
+
title: Type.Optional(
|
|
160
|
+
Type.String({ description: "Display title for the subagent card. Defaults to a truncated version of the task." })
|
|
161
|
+
),
|
|
162
|
+
provider: Type.Optional(
|
|
163
|
+
Type.String({ description: "LLM provider (e.g. 'anthropic', 'google'). Defaults to the main agent's provider." })
|
|
164
|
+
),
|
|
165
|
+
model: Type.Optional(
|
|
166
|
+
Type.String({ description: "Model ID (e.g. 'claude-sonnet-4-5'). Defaults to the main agent's model." })
|
|
167
|
+
),
|
|
168
|
+
cwd: Type.Optional(
|
|
169
|
+
Type.String({ description: "Working directory for the subagent. Defaults to the main agent's cwd." })
|
|
170
|
+
),
|
|
171
|
+
timeout: Type.Optional(
|
|
172
|
+
Type.Number({
|
|
173
|
+
description:
|
|
174
|
+
"Timeout in seconds for the subagent execution. If exceeded, the subagent is aborted. " +
|
|
175
|
+
"Defaults to unlimited (no timeout).",
|
|
176
|
+
minimum: 1,
|
|
177
|
+
})
|
|
178
|
+
),
|
|
179
|
+
columnWidthPercent: Type.Optional(
|
|
180
|
+
Type.Number({
|
|
181
|
+
description:
|
|
182
|
+
"Width of this subagent's card as a percentage of terminal width (e.g. 50 for 2 parallel agents, 33 for 3). " +
|
|
183
|
+
"Max 3 cards per row. Defaults to 50.",
|
|
184
|
+
minimum: 33,
|
|
185
|
+
maximum: 100,
|
|
186
|
+
})
|
|
187
|
+
),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
type SubagentParamsType = Static<typeof SubagentParams>;
|
|
191
|
+
|
|
192
|
+
// โโ Core execution logic โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
193
|
+
async function executeSubagent(
|
|
194
|
+
toolCallId: string,
|
|
195
|
+
params: SubagentParamsType,
|
|
196
|
+
signal: AbortSignal | undefined,
|
|
197
|
+
onUpdate: AgentToolUpdateCallback | undefined,
|
|
198
|
+
fallbackProvider?: string,
|
|
199
|
+
fallbackModel?: string,
|
|
200
|
+
fallbackCwd?: string,
|
|
201
|
+
): Promise<AgentToolResult<any>> {
|
|
202
|
+
subagentCount++;
|
|
203
|
+
const subagentNum = subagentCount;
|
|
204
|
+
const outDir = join(".pi", "subagent-in-memory", mainSessionId, `subagent_${subagentNum}`);
|
|
205
|
+
mkdirSync(outDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
// Parse "provider/model" format (e.g. "openai/gpt-4o-mini")
|
|
208
|
+
let providerName = params.provider ?? fallbackProvider;
|
|
209
|
+
let modelId = params.model ?? fallbackModel;
|
|
210
|
+
if (modelId && !params.provider && modelId.includes("/")) {
|
|
211
|
+
const slashIdx = modelId.indexOf("/");
|
|
212
|
+
providerName = modelId.slice(0, slashIdx);
|
|
213
|
+
modelId = modelId.slice(slashIdx + 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!providerName || !modelId) {
|
|
217
|
+
throw new Error("Could not determine model. Provide provider and model parameters.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { model: resolvedModel, apiKey } = await resolveModel(providerName, modelId);
|
|
221
|
+
|
|
222
|
+
const cwd = params.cwd ?? fallbackCwd ?? process.cwd();
|
|
223
|
+
|
|
224
|
+
const authStorage = AuthStorage.create();
|
|
225
|
+
authStorage.setRuntimeApiKey(providerName, apiKey);
|
|
226
|
+
|
|
227
|
+
// Create tools for the subagent, including nested subagent support
|
|
228
|
+
const tools = [
|
|
229
|
+
...createCodingTools(cwd),
|
|
230
|
+
createGrepTool(cwd),
|
|
231
|
+
createFindTool(cwd),
|
|
232
|
+
createLsTool(cwd),
|
|
233
|
+
createSubagentAgentTool(providerName, modelId, cwd),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
237
|
+
cwd,
|
|
238
|
+
agentDir: getAgentDir(),
|
|
239
|
+
});
|
|
240
|
+
await resourceLoader.reload();
|
|
241
|
+
|
|
242
|
+
const { session } = await createAgentSession({
|
|
243
|
+
model: resolvedModel,
|
|
244
|
+
authStorage,
|
|
245
|
+
tools,
|
|
246
|
+
thinkingLevel: "off",
|
|
247
|
+
cwd,
|
|
248
|
+
sessionManager: SessionManager.inMemory(),
|
|
249
|
+
resourceLoader,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Set up JSONL event log
|
|
253
|
+
const jsonlPath = join(outDir, "events.jsonl");
|
|
254
|
+
const sessionTs = new Date().toISOString();
|
|
255
|
+
jsonlAppend(jsonlPath, {
|
|
256
|
+
type: "session",
|
|
257
|
+
version: 1,
|
|
258
|
+
id: session.sessionId,
|
|
259
|
+
timestamp: sessionTs,
|
|
260
|
+
cwd,
|
|
261
|
+
provider: providerName,
|
|
262
|
+
model: modelId,
|
|
263
|
+
task: params.task,
|
|
264
|
+
title: params.title,
|
|
265
|
+
});
|
|
266
|
+
let lastEventId = session.sessionId;
|
|
267
|
+
|
|
268
|
+
// Track card
|
|
269
|
+
const card: SubagentCard = {
|
|
270
|
+
sessionId: session.sessionId,
|
|
271
|
+
title: params.title ?? params.task.slice(0, 30),
|
|
272
|
+
modelLabel: modelId,
|
|
273
|
+
status: "created",
|
|
274
|
+
textPreview: "",
|
|
275
|
+
columnWidthPercent: params.columnWidthPercent ?? 50,
|
|
276
|
+
startedAt: Date.now(),
|
|
277
|
+
};
|
|
278
|
+
subagents.push(card);
|
|
279
|
+
updateSubagentWidget();
|
|
280
|
+
|
|
281
|
+
onUpdate?.({
|
|
282
|
+
content: [{ type: "text", text: `Subagent session created: ${session.sessionId}` }],
|
|
283
|
+
details: { sessionId: session.sessionId, status: "created" },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Timeout handling
|
|
287
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
288
|
+
const timeoutController = new AbortController();
|
|
289
|
+
if (params.timeout) {
|
|
290
|
+
timeoutTimer = setTimeout(() => {
|
|
291
|
+
timeoutController.abort();
|
|
292
|
+
}, params.timeout * 1000);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const combinedAbort = () => {
|
|
296
|
+
session.abort();
|
|
297
|
+
card.status = "error";
|
|
298
|
+
card.endedAt = Date.now();
|
|
299
|
+
updateSubagentWidget();
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
304
|
+
let finalText = "";
|
|
305
|
+
let textDeltaBuffer = "";
|
|
306
|
+
let toolcallDeltaBuffer = "";
|
|
307
|
+
|
|
308
|
+
session.subscribe((event) => {
|
|
309
|
+
const updateData: Record<string, any> = {
|
|
310
|
+
type: event.type,
|
|
311
|
+
sessionId: session.sessionId,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const eventId = randomUUID().slice(0, 8);
|
|
315
|
+
const eventTs = new Date().toISOString();
|
|
316
|
+
const baseLog = { type: event.type, id: eventId, parentId: lastEventId, timestamp: eventTs };
|
|
317
|
+
|
|
318
|
+
switch (event.type) {
|
|
319
|
+
case "agent_start":
|
|
320
|
+
card.status = "running";
|
|
321
|
+
updateSubagentWidget();
|
|
322
|
+
jsonlAppend(jsonlPath, baseLog);
|
|
323
|
+
lastEventId = eventId;
|
|
324
|
+
onUpdate?.({
|
|
325
|
+
content: [{ type: "text", text: "Subagent started..." }],
|
|
326
|
+
details: updateData,
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case "message_update": {
|
|
331
|
+
const ame = event.assistantMessageEvent;
|
|
332
|
+
if (ame.type === "text_delta") {
|
|
333
|
+
textDeltaBuffer += ame.delta;
|
|
334
|
+
finalText += ame.delta;
|
|
335
|
+
card.textPreview = finalText;
|
|
336
|
+
updateSubagentWidget();
|
|
337
|
+
onUpdate?.({
|
|
338
|
+
content: [{ type: "text", text: finalText }],
|
|
339
|
+
details: {
|
|
340
|
+
...updateData,
|
|
341
|
+
data: { assistantMessageEventType: ame.type, delta: ame.delta },
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
} else if (ame.type === "text_end") {
|
|
345
|
+
if (textDeltaBuffer) {
|
|
346
|
+
jsonlAppend(jsonlPath, { ...baseLog, data: { assistantMessageEventType: "text", text: textDeltaBuffer } });
|
|
347
|
+
textDeltaBuffer = "";
|
|
348
|
+
} else {
|
|
349
|
+
jsonlAppend(jsonlPath, { ...baseLog, data: { assistantMessageEventType: ame.type } });
|
|
350
|
+
}
|
|
351
|
+
lastEventId = eventId;
|
|
352
|
+
} else if (ame.type === "toolcall_delta") {
|
|
353
|
+
if ("delta" in ame) toolcallDeltaBuffer += (ame as any).delta ?? "";
|
|
354
|
+
} else if (ame.type === "toolcall_end") {
|
|
355
|
+
if (toolcallDeltaBuffer) {
|
|
356
|
+
jsonlAppend(jsonlPath, { ...baseLog, data: { assistantMessageEventType: "toolcall", content: toolcallDeltaBuffer } });
|
|
357
|
+
toolcallDeltaBuffer = "";
|
|
358
|
+
} else {
|
|
359
|
+
jsonlAppend(jsonlPath, { ...baseLog, data: { assistantMessageEventType: ame.type } });
|
|
360
|
+
}
|
|
361
|
+
lastEventId = eventId;
|
|
362
|
+
} else if (ame.type === "text_start" || ame.type === "toolcall_start") {
|
|
363
|
+
// Skip start markers
|
|
364
|
+
} else {
|
|
365
|
+
jsonlAppend(jsonlPath, { ...baseLog, data: { assistantMessageEventType: ame.type } });
|
|
366
|
+
lastEventId = eventId;
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case "tool_execution_start":
|
|
372
|
+
card.textPreview = finalText + `\n[${event.toolName} โณ]`;
|
|
373
|
+
updateSubagentWidget();
|
|
374
|
+
jsonlAppend(jsonlPath, { ...baseLog, toolName: event.toolName, args: event.args });
|
|
375
|
+
lastEventId = eventId;
|
|
376
|
+
onUpdate?.({
|
|
377
|
+
content: [
|
|
378
|
+
{ type: "text", text: finalText + `\n[Tool: ${event.toolName}]` },
|
|
379
|
+
],
|
|
380
|
+
details: {
|
|
381
|
+
...updateData,
|
|
382
|
+
data: { toolName: event.toolName, args: event.args },
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
|
|
387
|
+
case "tool_execution_end":
|
|
388
|
+
card.textPreview = finalText + `\n[${event.toolName} ${event.isError ? "โ" : "โ
"}]`;
|
|
389
|
+
updateSubagentWidget();
|
|
390
|
+
jsonlAppend(jsonlPath, { ...baseLog, toolName: event.toolName, isError: event.isError, result: event.result });
|
|
391
|
+
lastEventId = eventId;
|
|
392
|
+
onUpdate?.({
|
|
393
|
+
content: [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: finalText + `\n[Tool: ${event.toolName} ${event.isError ? "โ" : "โ
"}]`,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
details: {
|
|
400
|
+
...updateData,
|
|
401
|
+
data: { toolName: event.toolName, isError: event.isError },
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
case "agent_end":
|
|
407
|
+
card.status = "completed";
|
|
408
|
+
card.endedAt = Date.now();
|
|
409
|
+
updateSubagentWidget();
|
|
410
|
+
jsonlAppend(jsonlPath, { ...baseLog, finalTextLength: finalText.length });
|
|
411
|
+
resolve(finalText || "Subagent completed with no text output.");
|
|
412
|
+
break;
|
|
413
|
+
|
|
414
|
+
case "turn_start":
|
|
415
|
+
case "turn_end":
|
|
416
|
+
case "message_start":
|
|
417
|
+
case "message_end":
|
|
418
|
+
jsonlAppend(jsonlPath, baseLog);
|
|
419
|
+
lastEventId = eventId;
|
|
420
|
+
onUpdate?.({
|
|
421
|
+
content: [{ type: "text", text: finalText || "..." }],
|
|
422
|
+
details: updateData,
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
|
|
426
|
+
default:
|
|
427
|
+
jsonlAppend(jsonlPath, { ...baseLog, raw: event });
|
|
428
|
+
lastEventId = eventId;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (signal) {
|
|
434
|
+
signal.addEventListener("abort", () => {
|
|
435
|
+
combinedAbort();
|
|
436
|
+
reject(new Error("Subagent was aborted"));
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
timeoutController.signal.addEventListener("abort", () => {
|
|
440
|
+
combinedAbort();
|
|
441
|
+
reject(new Error(`Subagent timed out after ${params.timeout}s`));
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
session.prompt(params.task).catch((err) => {
|
|
445
|
+
card.status = "error";
|
|
446
|
+
card.endedAt = Date.now();
|
|
447
|
+
updateSubagentWidget();
|
|
448
|
+
reject(err);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
453
|
+
session.dispose();
|
|
454
|
+
|
|
455
|
+
const resultPath = join(outDir, "result.md");
|
|
456
|
+
writeFileSync(resultPath, result, "utf-8");
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
content: [{ type: "text", text: `Execution succeeded. Result is in \`${resultPath}\`` }],
|
|
460
|
+
details: { sessionId: session.sessionId, status: "completed", outputDir: outDir },
|
|
461
|
+
};
|
|
462
|
+
} catch (err: any) {
|
|
463
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
464
|
+
session.dispose();
|
|
465
|
+
|
|
466
|
+
const errorMsg = err?.message ?? String(err);
|
|
467
|
+
const errorPath = join(outDir, "error.md");
|
|
468
|
+
writeFileSync(errorPath, `# Subagent Error\n\n${errorMsg}\n`, "utf-8");
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
content: [{ type: "text", text: `Execution failed. Detail is in \`${errorPath}\`` }],
|
|
472
|
+
details: { sessionId: session.sessionId, status: "error", outputDir: outDir },
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// โโ AgentTool factory for nested subagent sessions โโโโโโโโโโโโโโ
|
|
478
|
+
function createSubagentAgentTool(
|
|
479
|
+
parentProvider: string,
|
|
480
|
+
parentModel: string,
|
|
481
|
+
parentCwd: string,
|
|
482
|
+
) {
|
|
483
|
+
return {
|
|
484
|
+
name: "subagent_create",
|
|
485
|
+
label: "Subagent",
|
|
486
|
+
description:
|
|
487
|
+
"Create a subagent to perform a task. The subagent runs in-process with its own session. " +
|
|
488
|
+
"Progress is streamed back as execution updates. Returns the final result when the subagent finishes.",
|
|
489
|
+
parameters: SubagentParams,
|
|
490
|
+
async execute(
|
|
491
|
+
toolCallId: string,
|
|
492
|
+
params: SubagentParamsType,
|
|
493
|
+
signal?: AbortSignal,
|
|
494
|
+
onUpdate?: AgentToolUpdateCallback,
|
|
495
|
+
) {
|
|
496
|
+
return executeSubagent(
|
|
497
|
+
toolCallId,
|
|
498
|
+
params,
|
|
499
|
+
signal,
|
|
500
|
+
onUpdate,
|
|
501
|
+
parentProvider,
|
|
502
|
+
parentModel,
|
|
503
|
+
parentCwd,
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// โโ Extension entry point โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
510
|
+
export default function (pi: ExtensionAPI) {
|
|
511
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
512
|
+
currentCtx = ctx;
|
|
513
|
+
mainSessionId = ctx.sessionManager.getSessionId?.() ?? `session-${Date.now()}`;
|
|
514
|
+
subagentCount = 0;
|
|
515
|
+
updateSubagentWidget();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
pi.registerCommand("in-memory-clear-widgets", {
|
|
519
|
+
description: "Clear all in-memory subagent card widgets",
|
|
520
|
+
handler: async (_args, ctx) => {
|
|
521
|
+
subagents.length = 0;
|
|
522
|
+
ctx.ui.setWidget("in-memory-subagent-cards", undefined);
|
|
523
|
+
ctx.ui.notify("In-memory subagent widgets cleared", "info");
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
pi.registerTool<typeof SubagentParams>({
|
|
528
|
+
name: "subagent_create",
|
|
529
|
+
label: "Subagent",
|
|
530
|
+
description:
|
|
531
|
+
"Create a subagent to perform a task. The subagent runs in-process with its own session. " +
|
|
532
|
+
"Progress is streamed back as execution updates. Returns the final result when the subagent finishes.",
|
|
533
|
+
parameters: SubagentParams,
|
|
534
|
+
|
|
535
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
536
|
+
const mainModel = ctx.model;
|
|
537
|
+
const providerName = params.provider ?? mainModel?.provider;
|
|
538
|
+
const modelId = params.model ?? mainModel?.id;
|
|
539
|
+
const cwd = params.cwd ?? ctx.cwd;
|
|
540
|
+
|
|
541
|
+
return executeSubagent(
|
|
542
|
+
toolCallId,
|
|
543
|
+
params,
|
|
544
|
+
signal,
|
|
545
|
+
onUpdate,
|
|
546
|
+
providerName,
|
|
547
|
+
modelId,
|
|
548
|
+
cwd,
|
|
549
|
+
);
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getModel, getModels } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export interface ResolvedModel {
|
|
6
|
+
model: Model<Api>;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a model and its API key using ModelRegistry + AuthStorage.
|
|
12
|
+
* Supports subscription/OAuth-based API keys without requiring env vars.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveModel(provider: string, modelId: string): Promise<ResolvedModel> {
|
|
15
|
+
const authStorage = AuthStorage.create();
|
|
16
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
17
|
+
|
|
18
|
+
// Try ModelRegistry first (includes custom models from models.json),
|
|
19
|
+
// fall back to pi-ai's built-in getModel.
|
|
20
|
+
let model: Model<Api> | undefined;
|
|
21
|
+
try {
|
|
22
|
+
model = modelRegistry.find(provider, modelId);
|
|
23
|
+
} catch {
|
|
24
|
+
// ModelRegistry.find may throw if registry failed to load
|
|
25
|
+
}
|
|
26
|
+
if (!model) {
|
|
27
|
+
try {
|
|
28
|
+
model = getModel(provider as any, modelId as any);
|
|
29
|
+
} catch {
|
|
30
|
+
// getModel throws if provider/model combo is unknown
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!model) {
|
|
34
|
+
throw new Error(`Could not find model ${provider}/${modelId}. Check provider and model parameters.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve API key: try ModelRegistry, then AuthStorage directly
|
|
38
|
+
let apiKey: string | undefined;
|
|
39
|
+
try {
|
|
40
|
+
apiKey = await modelRegistry.getApiKey(model);
|
|
41
|
+
} catch {
|
|
42
|
+
// Fall back to AuthStorage if ModelRegistry method fails
|
|
43
|
+
apiKey = await authStorage.getApiKey(provider);
|
|
44
|
+
}
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
throw new Error(`No API key for ${provider}. Login or set the appropriate API key env var.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { model, apiKey };
|
|
50
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tui-draw โ Reusable TUI drawing primitives for pi extensions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
6
|
+
|
|
7
|
+
const FG_RESET = "\x1b[39m";
|
|
8
|
+
const BG_RESET = "\x1b[49m";
|
|
9
|
+
|
|
10
|
+
export interface CardTheme {
|
|
11
|
+
bg: string; // ANSI bg escape
|
|
12
|
+
br: string; // ANSI fg escape for borders
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RenderCardOptions {
|
|
16
|
+
title: string;
|
|
17
|
+
content?: string;
|
|
18
|
+
footer?: string;
|
|
19
|
+
colWidth: number;
|
|
20
|
+
theme: {
|
|
21
|
+
fg: (style: string, text: string) => string;
|
|
22
|
+
bold: (text: string) => string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
};
|
|
25
|
+
cardTheme: CardTheme;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render a bordered card as an array of ANSI-styled lines.
|
|
30
|
+
*
|
|
31
|
+
* Each line is exactly `colWidth` visible characters wide (including borders).
|
|
32
|
+
*/
|
|
33
|
+
export function renderCard(opts: RenderCardOptions): string[] {
|
|
34
|
+
const { title, content, footer, colWidth, theme, cardTheme } = opts;
|
|
35
|
+
const w = colWidth - 2; // inner width (minus left+right border)
|
|
36
|
+
const { bg, br } = cardTheme;
|
|
37
|
+
|
|
38
|
+
const bord = (s: string) => bg + br + s + BG_RESET + FG_RESET;
|
|
39
|
+
|
|
40
|
+
const borderLine = (text: string) => {
|
|
41
|
+
const visLen = visibleWidth(text);
|
|
42
|
+
const pad = " ".repeat(Math.max(0, w - visLen));
|
|
43
|
+
return bord("โ") + bg + text + bg + pad + BG_RESET + bord("โ");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const top = "โ" + "โ".repeat(w) + "โ";
|
|
47
|
+
const bot = "โ" + "โ".repeat(w) + "โ";
|
|
48
|
+
|
|
49
|
+
const lines: string[] = [bord(top)];
|
|
50
|
+
|
|
51
|
+
// Title โ truncate raw text, then style
|
|
52
|
+
const truncTitle = truncateToWidth(title, w - 1);
|
|
53
|
+
const styledTitle = theme.fg("accent", theme.bold(truncTitle));
|
|
54
|
+
lines.push(borderLine(" " + styledTitle));
|
|
55
|
+
|
|
56
|
+
// Content (defaults to "ready" muted) โ supports multi-line
|
|
57
|
+
const contentText = content ?? "ready";
|
|
58
|
+
for (const cLine of contentText.split("\n")) {
|
|
59
|
+
const truncContent = truncateToWidth(cLine, w - 1);
|
|
60
|
+
const styledContent = theme.fg("muted", truncContent);
|
|
61
|
+
lines.push(borderLine(" " + styledContent));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Optional footer โ truncate then style
|
|
65
|
+
if (footer !== undefined) {
|
|
66
|
+
const truncFooter = truncateToWidth(footer, w - 1);
|
|
67
|
+
const styledFooter = theme.fg("muted", truncFooter);
|
|
68
|
+
lines.push(borderLine(" " + styledFooter));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(bord(bot));
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-subagent-in-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "In-process subagent tool for pi with live TUI card widgets, JSONL session logging, and zero system-prompt overhead.",
|
|
5
|
+
"author": "Ross Z",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"subagent",
|
|
10
|
+
"in-memory",
|
|
11
|
+
"agent",
|
|
12
|
+
"widget"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"extensions",
|
|
16
|
+
"package.json",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
21
|
+
"@mariozechner/pi-tui": "*",
|
|
22
|
+
"@mariozechner/pi-ai": "*",
|
|
23
|
+
"@sinclair/typebox": "*"
|
|
24
|
+
},
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"extensions/index.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|