@gotgenes/pi-subagents 13.1.0 → 13.2.1
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 +15 -0
- package/README.md +19 -10
- package/docs/architecture/architecture.md +31 -118
- package/docs/architecture/history/phase-16-invert-dependencies.md +143 -0
- package/docs/plans/0277-encapsulate-agent-session.md +304 -0
- package/docs/retro/0265-born-complete-subagent-session.md +39 -0
- package/docs/retro/0277-encapsulate-agent-session.md +80 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +2 -2
- package/src/lifecycle/agent.ts +51 -16
- package/src/lifecycle/subagent-session.ts +31 -1
- package/src/observation/notification.ts +2 -2
- package/src/service/service-adapter.ts +1 -7
- package/src/tools/agent-tool.ts +1 -1
- package/src/tools/background-spawner.ts +4 -3
- package/src/tools/foreground-runner.ts +4 -3
- package/src/tools/get-result-tool.ts +4 -8
- package/src/tools/steer-tool.ts +7 -13
- package/src/ui/agent-menu.ts +1 -3
- package/src/ui/conversation-viewer.ts +5 -10
|
@@ -90,13 +90,7 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
90
90
|
if (record?.status !== "running") {
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
if (!session) {
|
|
95
|
-
// Session not ready yet — buffer on the agent for delivery once initialized
|
|
96
|
-
record.queueSteer(message);
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
await session.steer(message);
|
|
93
|
+
await record.steer(message);
|
|
100
94
|
return true;
|
|
101
95
|
}
|
|
102
96
|
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -108,7 +108,7 @@ export class AgentTool {
|
|
|
108
108
|
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
|
-
if (!existing.
|
|
111
|
+
if (!existing.isSessionReady()) {
|
|
112
112
|
return textResult(
|
|
113
113
|
`Agent "${params.resume}" has no active session to resume.`,
|
|
114
114
|
);
|
|
@@ -53,9 +53,10 @@ export function spawnBackground(
|
|
|
53
53
|
isBackground: true,
|
|
54
54
|
invocation: execution.agentInvocation,
|
|
55
55
|
observer: {
|
|
56
|
-
onSessionCreated: (
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
onSessionCreated: (agent) => {
|
|
57
|
+
const sub = agent.subagentSession!;
|
|
58
|
+
bgState.setSession(sub);
|
|
59
|
+
subscribeUIObserver(sub, bgState);
|
|
59
60
|
},
|
|
60
61
|
},
|
|
61
62
|
});
|
|
@@ -108,10 +108,11 @@ export async function runForeground(
|
|
|
108
108
|
signal,
|
|
109
109
|
parentSession: params.parentSession,
|
|
110
110
|
observer: {
|
|
111
|
-
onSessionCreated: (agent
|
|
112
|
-
|
|
111
|
+
onSessionCreated: (agent) => {
|
|
112
|
+
const sub = agent.subagentSession!;
|
|
113
|
+
fgState.setSession(sub);
|
|
113
114
|
recordRef = agent;
|
|
114
|
-
unsubUI = subscribeUIObserver(
|
|
115
|
+
unsubUI = subscribeUIObserver(sub, fgState, streamUpdate);
|
|
115
116
|
fgId = agent.id;
|
|
116
117
|
agentActivity.set(agent.id, fgState);
|
|
117
118
|
widget.ensureTimer();
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
4
|
-
import { getSessionContextPercent } from "#src/lifecycle/usage";
|
|
5
|
-
import { getAgentConversation } from "#src/session/conversation";
|
|
6
4
|
import { formatLifetimeTokens, textResult } from "#src/tools/helpers";
|
|
7
5
|
import type { Agent } from "#src/types";
|
|
8
6
|
import { formatDuration, getDisplayName } from "#src/ui/display";
|
|
@@ -53,7 +51,7 @@ export class GetResultTool {
|
|
|
53
51
|
const displayName = getDisplayName(record.type, this.registry);
|
|
54
52
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
55
53
|
const tokens = formatLifetimeTokens(record);
|
|
56
|
-
const contextPercent =
|
|
54
|
+
const contextPercent = record.getContextPercent();
|
|
57
55
|
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
58
56
|
if (tokens) statsParts.push(tokens);
|
|
59
57
|
if (contextPercent !== null) statsParts.push(`Context: ${Math.round(contextPercent)}%`);
|
|
@@ -80,11 +78,9 @@ export class GetResultTool {
|
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
// Verbose: include full conversation
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
output += `\n\n--- Agent Conversation ---\n${conversation}`;
|
|
87
|
-
}
|
|
81
|
+
const conversation = params.verbose ? record.getConversation() : undefined;
|
|
82
|
+
if (conversation) {
|
|
83
|
+
output += `\n\n--- Agent Conversation ---\n${conversation}`;
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
return textResult(output);
|
package/src/tools/steer-tool.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { getSessionContextPercent } from "#src/lifecycle/usage";
|
|
4
3
|
import { formatLifetimeTokens, textResult } from "#src/tools/helpers";
|
|
5
4
|
import type { Agent } from "#src/types";
|
|
6
5
|
|
|
@@ -40,21 +39,16 @@ export class SteerTool {
|
|
|
40
39
|
`Agent "${params.agent_id}" is not running (status: ${record.status}). Cannot steer a non-running agent.`,
|
|
41
40
|
);
|
|
42
41
|
}
|
|
43
|
-
const session = record.session;
|
|
44
|
-
if (!session) {
|
|
45
|
-
// Session not ready yet — buffer on the agent for delivery once initialized
|
|
46
|
-
record.queueSteer(params.message);
|
|
47
|
-
this.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
48
|
-
return textResult(
|
|
49
|
-
`Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
42
|
try {
|
|
54
|
-
await
|
|
43
|
+
const delivered = await record.steer(params.message);
|
|
55
44
|
this.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
45
|
+
if (!delivered) {
|
|
46
|
+
return textResult(
|
|
47
|
+
`Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
56
50
|
const tokens = formatLifetimeTokens(record);
|
|
57
|
-
const contextPercent =
|
|
51
|
+
const contextPercent = record.getContextPercent();
|
|
58
52
|
const stateParts: string[] = [];
|
|
59
53
|
if (tokens) stateParts.push(tokens);
|
|
60
54
|
stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -252,8 +252,7 @@ export class AgentsMenuHandler {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
private async viewAgentConversation(ui: MenuUI, record: Agent): Promise<void> {
|
|
255
|
-
|
|
256
|
-
if (!session) {
|
|
255
|
+
if (!record.isSessionReady()) {
|
|
257
256
|
ui.notify(
|
|
258
257
|
`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
|
|
259
258
|
"info",
|
|
@@ -270,7 +269,6 @@ export class AgentsMenuHandler {
|
|
|
270
269
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
271
270
|
return new ConversationViewer({
|
|
272
271
|
tui,
|
|
273
|
-
session,
|
|
274
272
|
record,
|
|
275
273
|
activity,
|
|
276
274
|
theme,
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
* Subscribes to session events for real-time streaming updates.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
9
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
11
|
-
import { getLifetimeTotal
|
|
10
|
+
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
12
11
|
import type { Agent } from "#src/types";
|
|
13
12
|
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
14
13
|
import { buildInvocationTags, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "#src/ui/display";
|
|
@@ -24,7 +23,6 @@ export const VIEWPORT_HEIGHT_PCT = 70;
|
|
|
24
23
|
|
|
25
24
|
export interface ConversationViewerOptions {
|
|
26
25
|
tui: TUI;
|
|
27
|
-
session: AgentSession;
|
|
28
26
|
record: Agent;
|
|
29
27
|
activity: AgentActivityTracker | undefined;
|
|
30
28
|
theme: Theme;
|
|
@@ -41,7 +39,6 @@ export class ConversationViewer implements Component {
|
|
|
41
39
|
private closed = false;
|
|
42
40
|
|
|
43
41
|
private tui: TUI;
|
|
44
|
-
private session: AgentSession;
|
|
45
42
|
private record: Agent;
|
|
46
43
|
private activity: AgentActivityTracker | undefined;
|
|
47
44
|
private theme: Theme;
|
|
@@ -51,7 +48,6 @@ export class ConversationViewer implements Component {
|
|
|
51
48
|
|
|
52
49
|
constructor({
|
|
53
50
|
tui,
|
|
54
|
-
session,
|
|
55
51
|
record,
|
|
56
52
|
activity,
|
|
57
53
|
theme,
|
|
@@ -60,14 +56,13 @@ export class ConversationViewer implements Component {
|
|
|
60
56
|
wrapText,
|
|
61
57
|
}: ConversationViewerOptions) {
|
|
62
58
|
this.tui = tui;
|
|
63
|
-
this.session = session;
|
|
64
59
|
this.record = record;
|
|
65
60
|
this.activity = activity;
|
|
66
61
|
this.theme = theme;
|
|
67
62
|
this.done = done;
|
|
68
63
|
this.registry = registry;
|
|
69
64
|
this.wrapText = wrapText;
|
|
70
|
-
this.unsubscribe =
|
|
65
|
+
this.unsubscribe = record.subscribeToUpdates(() => {
|
|
71
66
|
if (this.closed) return;
|
|
72
67
|
this.tui.requestRender();
|
|
73
68
|
});
|
|
@@ -142,7 +137,7 @@ export class ConversationViewer implements Component {
|
|
|
142
137
|
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
143
138
|
const tokens = getLifetimeTotal(this.record.lifetimeUsage);
|
|
144
139
|
if (tokens > 0) {
|
|
145
|
-
const percent =
|
|
140
|
+
const percent = this.record.getContextPercent();
|
|
146
141
|
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
147
142
|
}
|
|
148
143
|
|
|
@@ -220,7 +215,7 @@ export class ConversationViewer implements Component {
|
|
|
220
215
|
|
|
221
216
|
const th = this.theme;
|
|
222
217
|
const ctx = { theme: th, wrapText: this.wrapText };
|
|
223
|
-
const messages = this.
|
|
218
|
+
const messages = this.record.messages;
|
|
224
219
|
|
|
225
220
|
if (messages.length === 0) {
|
|
226
221
|
return [th.fg("dim", "(waiting for first message...)")];
|
|
@@ -229,7 +224,7 @@ export class ConversationViewer implements Component {
|
|
|
229
224
|
const lines: string[] = [];
|
|
230
225
|
let needsSeparator = false;
|
|
231
226
|
for (const msg of messages) {
|
|
232
|
-
const formatted = formatMessage(msg as
|
|
227
|
+
const formatted = formatMessage(msg as { role: string; [key: string]: unknown }, width, ctx);
|
|
233
228
|
if (!formatted) continue;
|
|
234
229
|
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
235
230
|
lines.push(...formatted);
|