@gotgenes/pi-subagents 7.2.1 → 7.2.3
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/CHANGELOG.md +23 -0
- package/docs/architecture/architecture.md +5 -5
- package/docs/plans/0195-convert-tool-factories-to-classes.md +264 -0
- package/docs/retro/0194-align-tool-interfaces-for-structural-typing.md +26 -0
- package/docs/retro/0195-convert-tool-factories-to-classes.md +42 -0
- package/package.json +1 -1
- package/src/index.ts +8 -42
- package/src/lifecycle/agent-runner.ts +0 -11
- package/src/tools/agent-tool.ts +228 -216
- package/src/tools/get-result-tool.ts +112 -87
- package/src/tools/steer-tool.ts +99 -77
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
2
2
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
5
|
import { Type } from "@sinclair/typebox";
|
|
5
6
|
import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
@@ -15,71 +16,150 @@ import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
|
15
16
|
import { type UICtx } from "#src/ui/agent-widget";
|
|
16
17
|
import { type AgentDetails, getDisplayName } from "#src/ui/display";
|
|
17
18
|
|
|
18
|
-
// ----
|
|
19
|
-
|
|
20
|
-
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
21
|
-
export interface AgentToolManager {
|
|
22
|
-
spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
|
|
23
|
-
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
|
|
24
|
-
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
|
|
25
|
-
getRecord: (id: string) => AgentRecord | undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Narrow widget interface — only the methods the Agent tool calls. */
|
|
29
|
-
export interface AgentToolWidget {
|
|
30
|
-
setUICtx: (ctx: unknown) => void;
|
|
31
|
-
ensureTimer: () => void;
|
|
32
|
-
update: () => void;
|
|
33
|
-
markFinished: (id: string) => void;
|
|
34
|
-
}
|
|
19
|
+
// ---- Shared interfaces (also used by background-spawner and foreground-runner) ----
|
|
35
20
|
|
|
36
21
|
/**
|
|
37
22
|
* Narrow read/write interface for the agent-tool's agentActivity access.
|
|
38
23
|
* The full Map satisfies this structurally — no wrapper needed.
|
|
39
24
|
*/
|
|
40
25
|
export interface AgentActivityAccess {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
get(id: string): AgentActivityTracker | undefined;
|
|
27
|
+
set(id: string, tracker: AgentActivityTracker): void;
|
|
28
|
+
delete(id: string): void;
|
|
44
29
|
}
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
31
|
+
// ---- Deps interfaces ----
|
|
32
|
+
|
|
33
|
+
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
34
|
+
export interface AgentToolManager {
|
|
35
|
+
spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
|
|
36
|
+
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
|
|
37
|
+
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
|
|
38
|
+
getRecord: (id: string) => AgentRecord | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Narrow runtime interface — the Agent tool's slice of SubagentRuntime. */
|
|
42
|
+
export interface AgentToolRuntime {
|
|
43
|
+
readonly agentActivity: AgentActivityAccess;
|
|
44
|
+
setUICtx(ctx: UICtx): void;
|
|
45
|
+
ensureTimer(): void;
|
|
46
|
+
update(): void;
|
|
47
|
+
markFinished(id: string): void;
|
|
48
|
+
buildSnapshot(inheritContext: boolean): ParentSnapshot;
|
|
49
|
+
getModelInfo(): ModelInfo;
|
|
50
|
+
getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
|
|
53
|
+
/** Narrow settings accessor — only the fields the Agent tool reads. */
|
|
54
|
+
export type AgentToolSettings = {
|
|
55
|
+
readonly defaultMaxTurns: number | undefined;
|
|
56
|
+
readonly maxConcurrent: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ---- Class ----
|
|
60
|
+
|
|
61
|
+
export class AgentTool {
|
|
62
|
+
private readonly typeListText: string;
|
|
63
|
+
private readonly availableTypesText: string;
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
private readonly manager: AgentToolManager,
|
|
67
|
+
private readonly runtime: AgentToolRuntime,
|
|
68
|
+
private readonly settings: AgentToolSettings,
|
|
69
|
+
private readonly registry: AgentTypeRegistry,
|
|
70
|
+
private readonly agentDir: string,
|
|
71
|
+
) {
|
|
72
|
+
this.typeListText = buildTypeListText(registry, agentDir);
|
|
73
|
+
this.availableTypesText = registry.getAvailableTypes().join(", ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async execute(
|
|
77
|
+
toolCallId: string,
|
|
78
|
+
params: Record<string, unknown>,
|
|
79
|
+
signal: AbortSignal | undefined,
|
|
80
|
+
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
81
|
+
ctx: any,
|
|
82
|
+
) {
|
|
83
|
+
// Ensure we have UI context for widget rendering
|
|
84
|
+
this.runtime.setUICtx(ctx.ui as UICtx);
|
|
63
85
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
87
|
+
this.registry.reload();
|
|
88
|
+
|
|
89
|
+
// ---- Config resolution (pure) ----
|
|
90
|
+
const config = resolveSpawnConfig(
|
|
91
|
+
params,
|
|
92
|
+
this.registry,
|
|
93
|
+
this.runtime.getModelInfo(),
|
|
94
|
+
this.settings,
|
|
95
|
+
);
|
|
96
|
+
if ("error" in config) return textResult(config.error);
|
|
97
|
+
|
|
98
|
+
// ---- Boundary extraction (after config so inheritContext is resolved) ----
|
|
99
|
+
const snapshot = this.runtime.buildSnapshot(config.execution.inheritContext);
|
|
100
|
+
const { parentSessionFile, parentSessionId } = this.runtime.getSessionInfo();
|
|
101
|
+
const parentSession: ParentSessionInfo = { parentSessionFile, parentSessionId, toolCallId };
|
|
102
|
+
|
|
103
|
+
// ---- Resume existing agent ----
|
|
104
|
+
if (params.resume) {
|
|
105
|
+
const existing = this.manager.getRecord(params.resume as string);
|
|
106
|
+
if (!existing) {
|
|
107
|
+
return textResult(
|
|
108
|
+
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (!existing.session) {
|
|
112
|
+
return textResult(
|
|
113
|
+
`Agent "${params.resume}" has no active session to resume.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const record = await this.manager.resume(
|
|
117
|
+
params.resume as string,
|
|
118
|
+
params.prompt as string,
|
|
119
|
+
signal ?? new AbortController().signal,
|
|
120
|
+
);
|
|
121
|
+
if (!record) {
|
|
122
|
+
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
123
|
+
}
|
|
124
|
+
return textResult(
|
|
125
|
+
record.result?.trim() ?? record.error?.trim() ?? "No output.",
|
|
126
|
+
buildDetails(config.presentation.detailBase, record),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- Background execution ----
|
|
131
|
+
if (config.execution.runInBackground) {
|
|
132
|
+
return spawnBackground(
|
|
133
|
+
this.manager,
|
|
134
|
+
this.runtime,
|
|
135
|
+
this.runtime.agentActivity,
|
|
136
|
+
{ config, snapshot, parentSession, settings: this.settings },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- Foreground execution — stream progress via onUpdate ----
|
|
141
|
+
return runForeground(
|
|
142
|
+
this.manager,
|
|
143
|
+
this.runtime,
|
|
144
|
+
this.runtime.agentActivity,
|
|
145
|
+
{ config, snapshot, parentSession },
|
|
146
|
+
signal,
|
|
147
|
+
onUpdate,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// fallow-ignore-next-line unused-class-member
|
|
152
|
+
toToolDefinition() {
|
|
153
|
+
const typeListText = this.typeListText;
|
|
154
|
+
const availableTypesText = this.availableTypesText;
|
|
155
|
+
const agentDir = this.agentDir;
|
|
156
|
+
const registry = this.registry;
|
|
157
|
+
|
|
158
|
+
return defineTool({
|
|
159
|
+
name: "Agent" as const,
|
|
160
|
+
label: "Agent",
|
|
161
|
+
promptSnippet: "Agent: Launch a specialized agent for complex, multi-step tasks.",
|
|
162
|
+
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
83
163
|
|
|
84
164
|
The Agent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
|
|
85
165
|
|
|
@@ -100,170 +180,102 @@ Guidelines:
|
|
|
100
180
|
- Use thinking to control extended thinking level.
|
|
101
181
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
102
182
|
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).`,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// ---- Custom rendering: Claude Code style ----
|
|
163
|
-
|
|
164
|
-
renderCall(args: Record<string, unknown>, theme: any) {
|
|
165
|
-
const displayName = args.subagent_type
|
|
166
|
-
? getDisplayName(args.subagent_type as string, registry)
|
|
167
|
-
: "Agent";
|
|
168
|
-
const desc = (args.description as string | undefined) ?? "";
|
|
169
|
-
return new Text(
|
|
170
|
-
"▸ " +
|
|
171
|
-
theme.fg("toolTitle", theme.bold(displayName)) +
|
|
172
|
-
(desc ? " " + theme.fg("muted", desc) : ""),
|
|
173
|
-
0,
|
|
174
|
-
0,
|
|
175
|
-
);
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
renderResult(result: any, { expanded, isPartial }: any, theme: any) {
|
|
179
|
-
const details = result.details as AgentDetails | undefined;
|
|
180
|
-
if (!details) {
|
|
181
|
-
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
182
|
-
return new Text(text, 0, 0);
|
|
183
|
-
}
|
|
184
|
-
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
185
|
-
return new Text(
|
|
186
|
-
renderAgentResult(details, resultText, expanded, isPartial, theme),
|
|
187
|
-
0,
|
|
188
|
-
0,
|
|
189
|
-
);
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
// ---- Execute ----
|
|
193
|
-
|
|
194
|
-
execute: async (
|
|
195
|
-
toolCallId: string,
|
|
196
|
-
params: Record<string, unknown>,
|
|
197
|
-
signal: AbortSignal | undefined,
|
|
198
|
-
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
199
|
-
ctx: any,
|
|
200
|
-
) => {
|
|
201
|
-
// Ensure we have UI context for widget rendering
|
|
202
|
-
widget.setUICtx(ctx.ui as UICtx);
|
|
203
|
-
|
|
204
|
-
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
205
|
-
registry.reload();
|
|
206
|
-
|
|
207
|
-
// ---- Config resolution (pure) ----
|
|
208
|
-
const config = resolveSpawnConfig(
|
|
209
|
-
params,
|
|
210
|
-
registry,
|
|
211
|
-
getModelInfo(),
|
|
212
|
-
settings,
|
|
213
|
-
);
|
|
214
|
-
if ("error" in config) return textResult(config.error);
|
|
183
|
+
parameters: Type.Object({
|
|
184
|
+
prompt: Type.String({
|
|
185
|
+
description: "The task for the agent to perform.",
|
|
186
|
+
}),
|
|
187
|
+
description: Type.String({
|
|
188
|
+
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
189
|
+
}),
|
|
190
|
+
subagent_type: Type.String({
|
|
191
|
+
description: `The type of specialized agent to use. Available types: ${availableTypesText}. Custom agents from .pi/agents/<name>.md (project) or ${agentDir}/agents/<name>.md (global) are also available.`,
|
|
192
|
+
}),
|
|
193
|
+
model: Type.Optional(
|
|
194
|
+
Type.String({
|
|
195
|
+
description:
|
|
196
|
+
'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
thinking: Type.Optional(
|
|
200
|
+
Type.String({
|
|
201
|
+
description:
|
|
202
|
+
"Thinking level: off, minimal, low, medium, high, xhigh. Overrides agent default.",
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
max_turns: Type.Optional(
|
|
206
|
+
Type.Number({
|
|
207
|
+
description:
|
|
208
|
+
"Maximum number of agentic turns before stopping. Omit for unlimited (default).",
|
|
209
|
+
minimum: 1,
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
run_in_background: Type.Optional(
|
|
213
|
+
Type.Boolean({
|
|
214
|
+
description:
|
|
215
|
+
"Set to true to run in background. Returns agent ID immediately. You will be notified when it completes.",
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
resume: Type.Optional(
|
|
219
|
+
Type.String({
|
|
220
|
+
description: "Optional agent ID to resume from. Continues from previous context.",
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
isolated: Type.Optional(
|
|
224
|
+
Type.Boolean({
|
|
225
|
+
description: "If true, agent gets no extension/MCP tools — only built-in tools.",
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
inherit_context: Type.Optional(
|
|
229
|
+
Type.Boolean({
|
|
230
|
+
description:
|
|
231
|
+
"If true, fork parent conversation into the agent. Default: false (fresh context).",
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
234
|
+
isolation: Type.Optional(
|
|
235
|
+
Type.Literal("worktree", {
|
|
236
|
+
description:
|
|
237
|
+
'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
238
|
+
}),
|
|
239
|
+
),
|
|
240
|
+
}),
|
|
215
241
|
|
|
216
|
-
|
|
217
|
-
const snapshot = buildSnapshot(config.execution.inheritContext);
|
|
218
|
-
const { parentSessionFile, parentSessionId } = getSessionInfo();
|
|
219
|
-
const parentSession: ParentSessionInfo = { parentSessionFile, parentSessionId, toolCallId };
|
|
242
|
+
// ---- Custom rendering: Claude Code style ----
|
|
220
243
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const record = await manager.resume(
|
|
235
|
-
params.resume as string,
|
|
236
|
-
params.prompt as string,
|
|
237
|
-
signal ?? new AbortController().signal,
|
|
238
|
-
);
|
|
239
|
-
if (!record) {
|
|
240
|
-
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
241
|
-
}
|
|
242
|
-
return textResult(
|
|
243
|
-
record.result?.trim() ?? record.error?.trim() ?? "No output.",
|
|
244
|
-
buildDetails(config.presentation.detailBase, record),
|
|
245
|
-
);
|
|
246
|
-
}
|
|
244
|
+
renderCall(args: Record<string, unknown>, theme: any) {
|
|
245
|
+
const displayName = args.subagent_type
|
|
246
|
+
? getDisplayName(args.subagent_type as string, registry)
|
|
247
|
+
: "Agent";
|
|
248
|
+
const desc = (args.description as string | undefined) ?? "";
|
|
249
|
+
return new Text(
|
|
250
|
+
"▸ " +
|
|
251
|
+
theme.fg("toolTitle", theme.bold(displayName)) +
|
|
252
|
+
(desc ? " " + theme.fg("muted", desc) : ""),
|
|
253
|
+
0,
|
|
254
|
+
0,
|
|
255
|
+
);
|
|
256
|
+
},
|
|
247
257
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
renderResult(result: any, { expanded, isPartial }: any, theme: any) {
|
|
259
|
+
const details = result.details as AgentDetails | undefined;
|
|
260
|
+
if (!details) {
|
|
261
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
262
|
+
return new Text(text, 0, 0);
|
|
263
|
+
}
|
|
264
|
+
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
265
|
+
return new Text(
|
|
266
|
+
renderAgentResult(details, resultText, expanded, isPartial, theme),
|
|
267
|
+
0,
|
|
268
|
+
0,
|
|
269
|
+
);
|
|
270
|
+
},
|
|
257
271
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
268
|
-
};
|
|
272
|
+
execute: (
|
|
273
|
+
toolCallId: string,
|
|
274
|
+
params: Record<string, unknown>,
|
|
275
|
+
signal: AbortSignal | undefined,
|
|
276
|
+
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
277
|
+
ctx: any,
|
|
278
|
+
) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
269
281
|
}
|