@gotgenes/pi-subagents 6.9.0 → 6.9.2
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 +18 -0
- package/docs/architecture/architecture.md +36 -31
- package/docs/plans/0115-decompose-agent-tool.md +337 -0
- package/docs/plans/0116-type-housekeeping.md +351 -0
- package/docs/retro/0114-narrow-agent-tool-menu-deps.md +38 -0
- package/docs/retro/0115-decompose-agent-tool.md +51 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +12 -4
- package/src/agent-runner.ts +2 -1
- package/src/env.ts +7 -1
- package/src/index.ts +2 -4
- package/src/notification.ts +48 -33
- package/src/parent-snapshot.ts +20 -1
- package/src/prompts.ts +3 -2
- package/src/renderer.ts +1 -1
- package/src/session-config.ts +2 -1
- package/src/tools/agent-tool.ts +33 -201
- package/src/tools/background-spawner.ts +116 -0
- package/src/tools/foreground-runner.ts +175 -0
- package/src/tools/helpers.ts +45 -1
- package/src/types.ts +14 -46
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/conversation-viewer.ts +27 -10
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AgentSpawnConfig } from "../agent-manager.js";
|
|
4
|
+
import type { AgentInvocation, AgentRecord, IsolationMode, ThinkingLevel } from "../types.js";
|
|
5
|
+
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
6
|
+
import {
|
|
7
|
+
type AgentDetails,
|
|
8
|
+
describeActivity,
|
|
9
|
+
formatMs,
|
|
10
|
+
SPINNER,
|
|
11
|
+
} from "../ui/agent-widget.js";
|
|
12
|
+
import { subscribeUIObserver } from "../ui/ui-observer.js";
|
|
13
|
+
import type { AgentActivityAccess } from "./agent-tool.js";
|
|
14
|
+
import {
|
|
15
|
+
buildDetails,
|
|
16
|
+
formatLifetimeTokens,
|
|
17
|
+
getStatusNote,
|
|
18
|
+
textResult,
|
|
19
|
+
} from "./helpers.js";
|
|
20
|
+
|
|
21
|
+
/** Narrow manager interface for the foreground runner. */
|
|
22
|
+
export interface ForegroundManagerDeps {
|
|
23
|
+
spawnAndWait(
|
|
24
|
+
ctx: any,
|
|
25
|
+
type: string,
|
|
26
|
+
prompt: string,
|
|
27
|
+
opts: Omit<AgentSpawnConfig, "isBackground">,
|
|
28
|
+
): Promise<AgentRecord>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Narrow widget interface for the foreground runner. */
|
|
32
|
+
export interface ForegroundWidgetDeps {
|
|
33
|
+
ensureTimer(): void;
|
|
34
|
+
markFinished(id: string): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Injected collaborators for runForeground. */
|
|
38
|
+
export interface ForegroundDeps {
|
|
39
|
+
manager: ForegroundManagerDeps;
|
|
40
|
+
widget: ForegroundWidgetDeps;
|
|
41
|
+
agentActivity: AgentActivityAccess;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** All values the foreground runner needs, bundled from shared execute setup. */
|
|
45
|
+
export interface ForegroundParams {
|
|
46
|
+
ctx: {
|
|
47
|
+
sessionManager: {
|
|
48
|
+
getSessionFile(): string;
|
|
49
|
+
getSessionId(): string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
subagentType: string;
|
|
53
|
+
prompt: string;
|
|
54
|
+
description: string;
|
|
55
|
+
detailBase: Pick<
|
|
56
|
+
AgentDetails,
|
|
57
|
+
"displayName" | "description" | "subagentType" | "modelName" | "tags"
|
|
58
|
+
>;
|
|
59
|
+
rawType: string;
|
|
60
|
+
fellBack: boolean;
|
|
61
|
+
model: Model<any> | undefined;
|
|
62
|
+
effectiveMaxTurns: number | undefined;
|
|
63
|
+
isolated: boolean | undefined;
|
|
64
|
+
inheritContext: boolean | undefined;
|
|
65
|
+
thinking: ThinkingLevel | undefined;
|
|
66
|
+
isolation: IsolationMode | undefined;
|
|
67
|
+
agentInvocation: AgentInvocation;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run an agent synchronously in the foreground, streaming spinner updates.
|
|
72
|
+
* Owns: spinner interval, AgentActivityTracker creation, UI observer subscription,
|
|
73
|
+
* streaming onUpdate callbacks, cleanup, and result formatting.
|
|
74
|
+
*/
|
|
75
|
+
export async function runForeground(
|
|
76
|
+
deps: ForegroundDeps,
|
|
77
|
+
params: ForegroundParams,
|
|
78
|
+
signal: AbortSignal | undefined,
|
|
79
|
+
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
80
|
+
) {
|
|
81
|
+
let spinnerFrame = 0;
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
let fgId: string | undefined;
|
|
84
|
+
|
|
85
|
+
const fgState = new AgentActivityTracker(params.effectiveMaxTurns);
|
|
86
|
+
let unsubUI: (() => void) | undefined;
|
|
87
|
+
|
|
88
|
+
const streamUpdate = () => {
|
|
89
|
+
const details: AgentDetails = {
|
|
90
|
+
...params.detailBase,
|
|
91
|
+
toolUses: fgState.toolUses,
|
|
92
|
+
tokens: formatLifetimeTokens(fgState),
|
|
93
|
+
turnCount: fgState.turnCount,
|
|
94
|
+
maxTurns: fgState.maxTurns,
|
|
95
|
+
durationMs: Date.now() - startedAt,
|
|
96
|
+
status: "running",
|
|
97
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
98
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
99
|
+
};
|
|
100
|
+
onUpdate?.({
|
|
101
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
102
|
+
details: details as any,
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Animate spinner at ~80ms (smooth rotation through 10 braille frames)
|
|
107
|
+
const spinnerInterval = setInterval(() => {
|
|
108
|
+
spinnerFrame++;
|
|
109
|
+
streamUpdate();
|
|
110
|
+
}, 80);
|
|
111
|
+
|
|
112
|
+
streamUpdate();
|
|
113
|
+
|
|
114
|
+
let record: AgentRecord;
|
|
115
|
+
try {
|
|
116
|
+
record = await deps.manager.spawnAndWait(
|
|
117
|
+
params.ctx,
|
|
118
|
+
params.subagentType,
|
|
119
|
+
params.prompt,
|
|
120
|
+
{
|
|
121
|
+
description: params.description,
|
|
122
|
+
model: params.model,
|
|
123
|
+
maxTurns: params.effectiveMaxTurns,
|
|
124
|
+
isolated: params.isolated,
|
|
125
|
+
inheritContext: params.inheritContext,
|
|
126
|
+
thinkingLevel: params.thinking,
|
|
127
|
+
isolation: params.isolation,
|
|
128
|
+
invocation: params.agentInvocation,
|
|
129
|
+
signal,
|
|
130
|
+
parentSessionFile: params.ctx.sessionManager.getSessionFile(),
|
|
131
|
+
parentSessionId: params.ctx.sessionManager.getSessionId(),
|
|
132
|
+
onSessionCreated: (session, record) => {
|
|
133
|
+
fgState.setSession(session);
|
|
134
|
+
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
135
|
+
fgId = record.id;
|
|
136
|
+
deps.agentActivity.set(record.id, fgState);
|
|
137
|
+
deps.widget.ensureTimer();
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
clearInterval(spinnerInterval);
|
|
143
|
+
unsubUI?.();
|
|
144
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
clearInterval(spinnerInterval);
|
|
148
|
+
unsubUI?.();
|
|
149
|
+
|
|
150
|
+
// Clean up foreground agent from widget
|
|
151
|
+
if (fgId) {
|
|
152
|
+
deps.agentActivity.delete(fgId);
|
|
153
|
+
deps.widget.markFinished(fgId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const tokenText = formatLifetimeTokens(fgState);
|
|
157
|
+
const details = buildDetails(params.detailBase, record, fgState, { tokens: tokenText });
|
|
158
|
+
|
|
159
|
+
const fallbackNote = params.fellBack
|
|
160
|
+
? `Note: Unknown agent type "${params.rawType}" \u2014 using general-purpose.\n\n`
|
|
161
|
+
: "";
|
|
162
|
+
|
|
163
|
+
if (record.status === "error") {
|
|
164
|
+
return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
168
|
+
const statsParts = [`${record.toolUses} tool uses`];
|
|
169
|
+
if (tokenText) statsParts.push(tokenText);
|
|
170
|
+
return textResult(
|
|
171
|
+
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
172
|
+
(record.result?.trim() || "No output."),
|
|
173
|
+
details,
|
|
174
|
+
);
|
|
175
|
+
}
|
package/src/tools/helpers.ts
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
import type { AgentConfigLookup } from "../agent-types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
3
|
+
import { type AgentDetails, formatTokens } from "../ui/agent-widget.js";
|
|
3
4
|
import { getLifetimeTotal, type LifetimeUsage } from "../usage.js";
|
|
4
5
|
|
|
6
|
+
/** Parenthetical status note for completed agent result text. */
|
|
7
|
+
export function getStatusNote(status: string): string {
|
|
8
|
+
switch (status) {
|
|
9
|
+
case "aborted":
|
|
10
|
+
return " (aborted \u2014 max turns exceeded, output may be incomplete)";
|
|
11
|
+
case "steered":
|
|
12
|
+
return " (wrapped up \u2014 reached turn limit)";
|
|
13
|
+
case "stopped":
|
|
14
|
+
return " (stopped by user)";
|
|
15
|
+
default:
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Build AgentDetails from a base + record-specific fields. */
|
|
21
|
+
export function buildDetails(
|
|
22
|
+
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
23
|
+
record: {
|
|
24
|
+
toolUses: number;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
completedAt?: number;
|
|
27
|
+
status: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
id?: string;
|
|
30
|
+
lifetimeUsage: LifetimeUsage;
|
|
31
|
+
},
|
|
32
|
+
activity?: AgentActivityTracker,
|
|
33
|
+
overrides?: Partial<AgentDetails>,
|
|
34
|
+
): AgentDetails {
|
|
35
|
+
return {
|
|
36
|
+
...base,
|
|
37
|
+
toolUses: record.toolUses,
|
|
38
|
+
tokens: formatLifetimeTokens(record),
|
|
39
|
+
turnCount: activity?.turnCount,
|
|
40
|
+
maxTurns: activity?.maxTurns,
|
|
41
|
+
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
42
|
+
status: record.status as AgentDetails["status"],
|
|
43
|
+
agentId: record.id,
|
|
44
|
+
error: record.error,
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
5
49
|
/** Tool execute return value for a text response. */
|
|
6
50
|
export function textResult(msg: string, details?: unknown) {
|
|
7
51
|
return { content: [{ type: "text" as const, text: msg }], details: details as any };
|
package/src/types.ts
CHANGED
|
@@ -18,11 +18,23 @@ export type MemoryScope = "user" | "project" | "local";
|
|
|
18
18
|
/** Isolation mode for agent execution. */
|
|
19
19
|
export type IsolationMode = "worktree";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
export interface
|
|
21
|
+
/** UI display and agent listing — name, display name, description, prompt mode. */
|
|
22
|
+
export interface AgentIdentity {
|
|
23
23
|
name: string;
|
|
24
24
|
displayName?: string;
|
|
25
25
|
description: string;
|
|
26
|
+
promptMode: "replace" | "append";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Prompt assembly — name, prompt mode, system prompt. */
|
|
30
|
+
export interface AgentPromptConfig {
|
|
31
|
+
name: string;
|
|
32
|
+
promptMode: "replace" | "append";
|
|
33
|
+
systemPrompt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Unified agent configuration — used for both default and user-defined agents. */
|
|
37
|
+
export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
26
38
|
builtinToolNames?: string[];
|
|
27
39
|
/** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
|
|
28
40
|
disallowedTools?: string[];
|
|
@@ -33,8 +45,6 @@ export interface AgentConfig {
|
|
|
33
45
|
model?: string;
|
|
34
46
|
thinking?: ThinkingLevel;
|
|
35
47
|
maxTurns?: number;
|
|
36
|
-
systemPrompt: string;
|
|
37
|
-
promptMode: "replace" | "append";
|
|
38
48
|
/** Default for spawn: fork parent conversation. undefined = caller decides. */
|
|
39
49
|
inheritContext?: boolean;
|
|
40
50
|
/** Default for spawn: run in background. undefined = caller decides. */
|
|
@@ -64,48 +74,6 @@ export interface AgentInvocation {
|
|
|
64
74
|
isolation?: IsolationMode;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
/** Details attached to custom notification messages for visual rendering. */
|
|
68
|
-
export interface NotificationDetails {
|
|
69
|
-
id: string;
|
|
70
|
-
description: string;
|
|
71
|
-
status: string;
|
|
72
|
-
toolUses: number;
|
|
73
|
-
turnCount: number;
|
|
74
|
-
maxTurns?: number;
|
|
75
|
-
totalTokens: number;
|
|
76
|
-
durationMs: number;
|
|
77
|
-
outputFile?: string;
|
|
78
|
-
error?: string;
|
|
79
|
-
resultPreview: string;
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Plain data snapshot of the parent session state captured at spawn time.
|
|
85
|
-
* Replaces live `ExtensionContext` references so queued agents don't read stale state.
|
|
86
|
-
*/
|
|
87
|
-
export interface ParentSnapshot {
|
|
88
|
-
/** Parent working directory. */
|
|
89
|
-
cwd: string;
|
|
90
|
-
/** Parent's effective system prompt (for append-mode agents). */
|
|
91
|
-
systemPrompt: string;
|
|
92
|
-
/** Parent's current model instance (fallback when agent config has no model). */
|
|
93
|
-
model: unknown;
|
|
94
|
-
/** Model registry for resolving config.model strings and creating sessions. */
|
|
95
|
-
modelRegistry: {
|
|
96
|
-
find(provider: string, modelId: string): unknown;
|
|
97
|
-
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
98
|
-
};
|
|
99
|
-
/** Pre-built parent conversation text (when inheritContext was requested). */
|
|
100
|
-
parentContext?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface EnvInfo {
|
|
104
|
-
isGitRepo: boolean;
|
|
105
|
-
branch: string;
|
|
106
|
-
platform: string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
77
|
/**
|
|
110
78
|
* Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
|
|
111
79
|
* Matches the shape of `pi.exec()` without carrying an SDK dependency.
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -220,7 +220,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
220
220
|
|
|
221
221
|
await ctx.ui.custom<undefined>(
|
|
222
222
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
223
|
-
return new ConversationViewer(tui, session, record, activity, theme, done, deps.registry);
|
|
223
|
+
return new ConversationViewer({ tui, session, record, activity, theme, done, registry: deps.registry });
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
overlay: true,
|
|
@@ -20,6 +20,16 @@ const MIN_VIEWPORT = 3;
|
|
|
20
20
|
/** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
|
|
21
21
|
export const VIEWPORT_HEIGHT_PCT = 70;
|
|
22
22
|
|
|
23
|
+
export interface ConversationViewerOptions {
|
|
24
|
+
tui: TUI;
|
|
25
|
+
session: AgentSession;
|
|
26
|
+
record: AgentRecord;
|
|
27
|
+
activity: AgentActivityTracker | undefined;
|
|
28
|
+
theme: Theme;
|
|
29
|
+
done: (result: undefined) => void;
|
|
30
|
+
registry: AgentConfigLookup;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export class ConversationViewer implements Component {
|
|
24
34
|
private scrollOffset = 0;
|
|
25
35
|
private autoScroll = true;
|
|
@@ -27,16 +37,23 @@ export class ConversationViewer implements Component {
|
|
|
27
37
|
private lastInnerW = 0;
|
|
28
38
|
private closed = false;
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
) {
|
|
39
|
-
this.
|
|
40
|
+
private tui: TUI;
|
|
41
|
+
private session: AgentSession;
|
|
42
|
+
private record: AgentRecord;
|
|
43
|
+
private activity: AgentActivityTracker | undefined;
|
|
44
|
+
private theme: Theme;
|
|
45
|
+
private done: (result: undefined) => void;
|
|
46
|
+
private registry: AgentConfigLookup;
|
|
47
|
+
|
|
48
|
+
constructor(options: ConversationViewerOptions) {
|
|
49
|
+
this.tui = options.tui;
|
|
50
|
+
this.session = options.session;
|
|
51
|
+
this.record = options.record;
|
|
52
|
+
this.activity = options.activity;
|
|
53
|
+
this.theme = options.theme;
|
|
54
|
+
this.done = options.done;
|
|
55
|
+
this.registry = options.registry;
|
|
56
|
+
this.unsubscribe = options.session.subscribe(() => {
|
|
40
57
|
if (this.closed) return;
|
|
41
58
|
this.tui.requestRender();
|
|
42
59
|
});
|