@gotgenes/pi-subagents 6.14.1 → 6.16.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/CHANGELOG.md +28 -0
- package/docs/architecture/architecture.md +14 -30
- package/docs/plans/0144-consolidate-observation-model.md +263 -0
- package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +290 -0
- package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +56 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +7 -9
- package/src/agent-record.ts +11 -0
- package/src/index.ts +27 -13
- package/src/notification.ts +21 -24
- package/src/service-adapter.ts +19 -17
- package/src/tools/agent-tool.ts +56 -113
- package/src/tools/background-spawner.ts +34 -52
- package/src/tools/foreground-runner.ts +43 -61
- package/src/tools/get-result-tool.ts +3 -3
- package/src/tools/spawn-config.ts +146 -0
- package/src/tools/steer-tool.ts +1 -1
- package/src/ui/agent-activity-tracker.ts +3 -27
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/agent-widget.ts +3 -4
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +1 -12
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn-config.ts — Pure config resolution for the Agent tool.
|
|
3
|
+
*
|
|
4
|
+
* Extracts all config resolution logic from execute: type resolution,
|
|
5
|
+
* invocation config merge, model resolution, max-turns normalization,
|
|
6
|
+
* tag building, and detail-base construction.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
10
|
+
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
11
|
+
import type { AgentTypeRegistry } from "../agent-types.js";
|
|
12
|
+
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
13
|
+
import { resolveInvocationModel } from "../model-resolver.js";
|
|
14
|
+
import type { AgentInvocation, IsolationMode, SubagentType, ThinkingLevel } from "../types.js";
|
|
15
|
+
import {
|
|
16
|
+
type AgentDetails,
|
|
17
|
+
buildInvocationTags,
|
|
18
|
+
getDisplayName,
|
|
19
|
+
getPromptModeLabel,
|
|
20
|
+
} from "../ui/display.js";
|
|
21
|
+
|
|
22
|
+
/** Model info extracted from the parent session context. */
|
|
23
|
+
export interface ModelInfo {
|
|
24
|
+
parentModel: { id: string; name?: string } | undefined;
|
|
25
|
+
modelRegistry: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Fully resolved config for spawning an agent. */
|
|
29
|
+
export interface ResolvedSpawnConfig {
|
|
30
|
+
subagentType: string;
|
|
31
|
+
rawType: SubagentType;
|
|
32
|
+
fellBack: boolean;
|
|
33
|
+
displayName: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
description: string;
|
|
36
|
+
model: Model<any> | undefined;
|
|
37
|
+
effectiveMaxTurns: number | undefined;
|
|
38
|
+
thinking: ThinkingLevel | undefined;
|
|
39
|
+
inheritContext: boolean;
|
|
40
|
+
runInBackground: boolean;
|
|
41
|
+
isolated: boolean;
|
|
42
|
+
isolation: IsolationMode | undefined;
|
|
43
|
+
modelName: string | undefined;
|
|
44
|
+
agentInvocation: AgentInvocation;
|
|
45
|
+
agentTags: string[];
|
|
46
|
+
detailBase: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Error result when model resolution fails. */
|
|
50
|
+
export interface SpawnConfigError {
|
|
51
|
+
error: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve all config for an Agent tool invocation.
|
|
56
|
+
*
|
|
57
|
+
* Pure function — no SDK types, no side effects.
|
|
58
|
+
* Returns either a fully resolved config or an error.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveSpawnConfig(
|
|
61
|
+
params: Record<string, unknown>,
|
|
62
|
+
registry: AgentTypeRegistry,
|
|
63
|
+
modelInfo: ModelInfo,
|
|
64
|
+
settings: { readonly defaultMaxTurns: number | undefined },
|
|
65
|
+
): ResolvedSpawnConfig | SpawnConfigError {
|
|
66
|
+
const rawType = params.subagent_type as SubagentType;
|
|
67
|
+
const resolved = registry.resolveType(rawType);
|
|
68
|
+
const subagentType = resolved ?? "general-purpose";
|
|
69
|
+
const fellBack = resolved === undefined;
|
|
70
|
+
|
|
71
|
+
const displayName = getDisplayName(subagentType, registry);
|
|
72
|
+
|
|
73
|
+
// Merge agent config defaults with tool-call params
|
|
74
|
+
const customConfig = registry.resolveAgentConfig(subagentType);
|
|
75
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
76
|
+
|
|
77
|
+
// Resolve model
|
|
78
|
+
const resolution = resolveInvocationModel(
|
|
79
|
+
modelInfo.parentModel,
|
|
80
|
+
resolvedConfig.modelInput,
|
|
81
|
+
resolvedConfig.modelFromParams,
|
|
82
|
+
modelInfo.modelRegistry as any,
|
|
83
|
+
);
|
|
84
|
+
if (resolution.error) return { error: resolution.error };
|
|
85
|
+
const model = resolution.model;
|
|
86
|
+
|
|
87
|
+
const thinking = resolvedConfig.thinking;
|
|
88
|
+
const inheritContext = resolvedConfig.inheritContext;
|
|
89
|
+
const runInBackground = resolvedConfig.runInBackground;
|
|
90
|
+
const isolated = resolvedConfig.isolated;
|
|
91
|
+
const isolation = resolvedConfig.isolation;
|
|
92
|
+
|
|
93
|
+
// Compute display model name (only shown when different from parent)
|
|
94
|
+
const parentModelId = modelInfo.parentModel?.id;
|
|
95
|
+
const effectiveModelId = model?.id;
|
|
96
|
+
const modelName =
|
|
97
|
+
effectiveModelId && effectiveModelId !== parentModelId
|
|
98
|
+
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
99
|
+
: undefined;
|
|
100
|
+
|
|
101
|
+
const effectiveMaxTurns = normalizeMaxTurns(
|
|
102
|
+
resolvedConfig.maxTurns ?? settings.defaultMaxTurns,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const agentInvocation: AgentInvocation = {
|
|
106
|
+
modelName,
|
|
107
|
+
thinking,
|
|
108
|
+
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
109
|
+
isolated,
|
|
110
|
+
inheritContext,
|
|
111
|
+
runInBackground,
|
|
112
|
+
isolation,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const modeLabel = getPromptModeLabel(subagentType, registry);
|
|
116
|
+
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
117
|
+
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
118
|
+
|
|
119
|
+
const detailBase = {
|
|
120
|
+
displayName,
|
|
121
|
+
description: params.description as string,
|
|
122
|
+
subagentType,
|
|
123
|
+
modelName,
|
|
124
|
+
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
subagentType,
|
|
129
|
+
rawType,
|
|
130
|
+
fellBack,
|
|
131
|
+
displayName,
|
|
132
|
+
prompt: params.prompt as string,
|
|
133
|
+
description: params.description as string,
|
|
134
|
+
model,
|
|
135
|
+
effectiveMaxTurns,
|
|
136
|
+
thinking,
|
|
137
|
+
inheritContext,
|
|
138
|
+
runInBackground,
|
|
139
|
+
isolated,
|
|
140
|
+
isolation,
|
|
141
|
+
modelName,
|
|
142
|
+
agentInvocation,
|
|
143
|
+
agentTags,
|
|
144
|
+
detailBase,
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/tools/steer-tool.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function createSteerTool(deps: SteerToolDeps) {
|
|
|
49
49
|
`Agent "${params.agent_id}" is not running (status: ${record.status}). Cannot steer a non-running agent.`,
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
|
-
const session = record.
|
|
52
|
+
const session = record.session;
|
|
53
53
|
if (!session) {
|
|
54
54
|
// Session not ready yet — queue via manager for delivery once initialized
|
|
55
55
|
deps.queueSteer(record.id, params.message);
|
|
@@ -5,24 +5,15 @@
|
|
|
5
5
|
* in `ui-observer.ts`. Callers use named transition methods; readers use read-only accessors.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
/** Usage delta accepted by onUsageUpdate — matches the LifetimeUsage accumulator shape. */
|
|
11
|
-
export interface UsageDelta {
|
|
12
|
-
input: number;
|
|
13
|
-
output: number;
|
|
14
|
-
cacheWrite: number;
|
|
15
|
-
}
|
|
8
|
+
import type { SessionLike } from "../usage.js";
|
|
16
9
|
|
|
17
10
|
/** Per-agent live activity state with explicit transition methods and read-only accessors. */
|
|
18
11
|
export class AgentActivityTracker {
|
|
19
12
|
private _activeTools = new Map<string, string>();
|
|
20
13
|
private _toolKeySeq = 0;
|
|
21
|
-
private _toolUses = 0;
|
|
22
14
|
private _responseText = "";
|
|
23
15
|
private _session: SessionLike | undefined = undefined;
|
|
24
16
|
private _turnCount = 1;
|
|
25
|
-
private _lifetimeUsage: LifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
|
|
26
17
|
|
|
27
18
|
constructor(private readonly _maxTurns?: number) {}
|
|
28
19
|
|
|
@@ -33,12 +24,11 @@ export class AgentActivityTracker {
|
|
|
33
24
|
this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
27
|
+
/** Remove a tool from active tools (called when tool execution ends). No-op when no matching tool is active. */
|
|
28
|
+
onToolDone(toolName: string): void {
|
|
38
29
|
for (const [key, name] of this._activeTools) {
|
|
39
30
|
if (name === toolName) {
|
|
40
31
|
this._activeTools.delete(key);
|
|
41
|
-
this._toolUses++;
|
|
42
32
|
break;
|
|
43
33
|
}
|
|
44
34
|
}
|
|
@@ -59,11 +49,6 @@ export class AgentActivityTracker {
|
|
|
59
49
|
this._turnCount++;
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
/** Accumulate a usage delta into the lifetime usage totals. */
|
|
63
|
-
onUsageUpdate(delta: UsageDelta): void {
|
|
64
|
-
addUsage(this._lifetimeUsage, delta);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
52
|
/** Bind the session reference (called once when the agent session is created). */
|
|
68
53
|
setSession(session: SessionLike): void {
|
|
69
54
|
this._session = session;
|
|
@@ -76,11 +61,6 @@ export class AgentActivityTracker {
|
|
|
76
61
|
return this._activeTools;
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
/** Total completed tool invocations. */
|
|
80
|
-
get toolUses(): number {
|
|
81
|
-
return this._toolUses;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
64
|
/** The agent's latest partial response text (reset at each message start). */
|
|
85
65
|
get responseText(): string {
|
|
86
66
|
return this._responseText;
|
|
@@ -101,8 +81,4 @@ export class AgentActivityTracker {
|
|
|
101
81
|
return this._maxTurns;
|
|
102
82
|
}
|
|
103
83
|
|
|
104
|
-
/** Accumulated lifetime token usage (survives compaction). */
|
|
105
|
-
get lifetimeUsage(): Readonly<LifetimeUsage> {
|
|
106
|
-
return this._lifetimeUsage;
|
|
107
|
-
}
|
|
108
84
|
}
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -207,7 +207,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
|
|
210
|
-
const session = record.
|
|
210
|
+
const session = record.session;
|
|
211
211
|
if (!session) {
|
|
212
212
|
ctx.ui.notify(
|
|
213
213
|
`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -191,14 +191,13 @@ export class AgentWidget {
|
|
|
191
191
|
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
192
192
|
|
|
193
193
|
const bg = this.agentActivity.get(a.id);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const contextPercent = getSessionContextPercent(bg?.session);
|
|
194
|
+
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
195
|
+
const contextPercent = getSessionContextPercent(a.session);
|
|
197
196
|
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
|
|
198
197
|
|
|
199
198
|
const parts: string[] = [];
|
|
200
199
|
if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
|
|
201
|
-
if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
200
|
+
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
202
201
|
if (tokenText) parts.push(tokenText);
|
|
203
202
|
parts.push(elapsed);
|
|
204
203
|
const statsText = parts.join(" · ");
|
|
@@ -155,11 +155,11 @@ export class ConversationViewer implements Component {
|
|
|
155
155
|
const duration = formatDuration(this.record.startedAt, this.record.completedAt);
|
|
156
156
|
|
|
157
157
|
const headerParts: string[] = [duration];
|
|
158
|
-
const toolUses = this.
|
|
158
|
+
const toolUses = this.record.toolUses;
|
|
159
159
|
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
160
|
-
const tokens = getLifetimeTotal(this.
|
|
160
|
+
const tokens = getLifetimeTotal(this.record.lifetimeUsage);
|
|
161
161
|
if (tokens > 0) {
|
|
162
|
-
const percent = getSessionContextPercent(this.
|
|
162
|
+
const percent = getSessionContextPercent(this.record.session);
|
|
163
163
|
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
164
164
|
}
|
|
165
165
|
|
package/src/ui/ui-observer.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function subscribeUIObserver(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (event.type === "tool_execution_end") {
|
|
43
|
-
tracker.
|
|
43
|
+
tracker.onToolDone(event.toolName);
|
|
44
44
|
onUpdate?.();
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -61,16 +61,5 @@ export function subscribeUIObserver(
|
|
|
61
61
|
onUpdate?.();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
65
|
-
const u = event.message.usage;
|
|
66
|
-
if (u) {
|
|
67
|
-
tracker.onUsageUpdate({
|
|
68
|
-
input: u.input ?? 0,
|
|
69
|
-
output: u.output ?? 0,
|
|
70
|
-
cacheWrite: u.cacheWrite ?? 0,
|
|
71
|
-
});
|
|
72
|
-
onUpdate?.();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
64
|
});
|
|
76
65
|
}
|