@nklisch/pi-enhanced 0.2.7 → 0.3.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 +10 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/README.md +4 -2
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js +81 -8
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js +32 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +10 -4
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +144 -10
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +40 -14
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +19 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +75 -47
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +171 -140
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/settings.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +8 -7
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +12 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +111 -104
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +5 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +31 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/default-agents.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/invocation-config.ts +7 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +18 -9
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +10 -63
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +122 -76
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +3 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +4 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +2 -23
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +177 -252
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +90 -280
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-state.ts +153 -239
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -700
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +78 -122
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +15 -42
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +44 -95
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +157 -90
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service.ts +60 -68
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/conversation.ts +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query-source.ts +28 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +327 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +171 -249
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +51 -94
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +9 -6
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts +46 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/parent-tool-registry.ts +17 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +330 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts +105 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +26 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts +53 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +11 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +33 -27
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +6 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +121 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +430 -112
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/subagents-settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +8 -9
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +0 -81
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +0 -149
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +0 -77
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* synchronously at run-start. The core has no knowledge of git or worktrees.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { SubagentStatus } from "#src/lifecycle/subagent";
|
|
14
|
+
import type { SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
15
15
|
import type { AgentInvocation, SubagentType } from "#src/types";
|
|
16
16
|
|
|
17
17
|
/** Context the core hands a provider when a child run starts. */
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { debugLog } from "#src/debug";
|
|
2
2
|
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
3
|
-
import type { Subagent } from "#src/
|
|
3
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
4
|
+
import type { SubagentTerminalReason } from "#src/lifecycle/subagent-state";
|
|
4
5
|
|
|
5
|
-
/** Details attached to custom notification messages for visual rendering. */
|
|
6
6
|
export interface NotificationDetails {
|
|
7
7
|
id: string;
|
|
8
8
|
description: string;
|
|
9
|
+
runId: number;
|
|
10
|
+
mode: string;
|
|
9
11
|
modelLabel: string;
|
|
10
12
|
thinkingLevel: Subagent["effectiveThinkingLevel"];
|
|
11
13
|
status: string;
|
|
14
|
+
terminalReason?: SubagentTerminalReason;
|
|
12
15
|
toolUses: number;
|
|
13
16
|
turnCount: number;
|
|
14
17
|
maxTurns?: number;
|
|
@@ -19,170 +22,129 @@ export interface NotificationDetails {
|
|
|
19
22
|
resultPreview: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
// ---- Pure helpers (exported for unit testing) ----
|
|
23
|
-
|
|
24
|
-
/** Escape XML text and attribute delimiters in structured notifications. */
|
|
25
25
|
export function escapeXml(s: string): string {
|
|
26
|
-
return s
|
|
27
|
-
.replace(/&/g, "&")
|
|
28
|
-
.replace(/</g, "<")
|
|
29
|
-
.replace(/>/g, ">")
|
|
30
|
-
.replace(/"/g, """)
|
|
31
|
-
.replace(/'/g, "'");
|
|
26
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return "Aborted (max turns exceeded)";
|
|
41
|
-
case "steered":
|
|
42
|
-
return "Wrapped up (turn limit)";
|
|
43
|
-
case "stopped":
|
|
44
|
-
return "Stopped";
|
|
45
|
-
default:
|
|
46
|
-
return "Done";
|
|
47
|
-
}
|
|
29
|
+
export function getStatusLabel(status: string, reason?: SubagentTerminalReason, error?: string): string {
|
|
30
|
+
if (status === "error") return `Error: ${error ?? "unknown"}`;
|
|
31
|
+
if (reason === "turn_limit_graceful") return "Completed (turn limit)";
|
|
32
|
+
if (reason === "turn_limit_hard") return "Stopped (turn limit)";
|
|
33
|
+
if (status === "stopped") return reason ? `Stopped (${reason.replaceAll("_", " ")})` : "Stopped";
|
|
34
|
+
return "Done";
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
/** Format a structured <task-notification> XML block for the parent agent to parse. */
|
|
51
37
|
export function formatTaskNotification(record: Subagent, resultMaxLen: number): string {
|
|
52
|
-
const status = getStatusLabel(record.status, record.error);
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const contextPercent = record.getContextPercent();
|
|
56
|
-
const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
|
|
57
|
-
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
58
|
-
|
|
59
|
-
const resultPreview = record.stoppedWhileQueued
|
|
60
|
-
? "Agent was stopped while queued and never started. No work was performed."
|
|
61
|
-
: record.result
|
|
62
|
-
? record.result.length > resultMaxLen
|
|
63
|
-
? record.result.slice(0, resultMaxLen) + "\n...(truncated, use get_subagent_result for full output)"
|
|
64
|
-
: record.result
|
|
38
|
+
const status = getStatusLabel(record.status, record.stateTerminalReason, record.error);
|
|
39
|
+
const resultPreview = record.result
|
|
40
|
+
? record.result.length > resultMaxLen ? record.result.slice(0, resultMaxLen) + "\n...(truncated, use get_subagent_result for full output)" : record.result
|
|
65
41
|
: "No output.";
|
|
66
|
-
|
|
67
|
-
const toolCallId = record.toolCallId;
|
|
68
42
|
const outputFile = record.outputFile;
|
|
69
43
|
return [
|
|
70
44
|
"<task-notification>",
|
|
71
45
|
`<task-id>${record.id}</task-id>`,
|
|
72
|
-
|
|
73
|
-
|
|
46
|
+
`<run-id>${record.runId}</run-id>`,
|
|
47
|
+
`<mode>${record.mode}</mode>`,
|
|
74
48
|
`<status>${escapeXml(status)}</status>`,
|
|
49
|
+
record.stateTerminalReason ? `<terminal-reason>${record.stateTerminalReason}</terminal-reason>` : null,
|
|
75
50
|
`<model>${escapeXml(record.modelLabel)}</model>`,
|
|
76
51
|
`<thinking_level>${escapeXml(record.effectiveThinkingLevel)}</thinking_level>`,
|
|
77
|
-
`<summary>Subagent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
52
|
+
`<summary>Subagent "${escapeXml(record.description)}" ${escapeXml(record.status)}</summary>`,
|
|
78
53
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
79
|
-
`<usage><total_tokens>${
|
|
54
|
+
`<usage><total_tokens>${getLifetimeTotal(record.lifetimeUsage)}</total_tokens><tool_uses>${record.toolUses}</tool_uses><duration_ms>${record.activeRuntimeMs}</duration_ms></usage>`,
|
|
55
|
+
outputFile ? `<output-file>${escapeXml(outputFile)}</output-file>` : null,
|
|
80
56
|
"</task-notification>",
|
|
81
|
-
]
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
.join("\n");
|
|
57
|
+
].filter((line): line is string => line !== null).join("\n");
|
|
84
58
|
}
|
|
85
59
|
|
|
86
|
-
|
|
87
|
-
export function buildNotificationDetails(
|
|
88
|
-
record: Subagent,
|
|
89
|
-
resultMaxLen: number,
|
|
90
|
-
): NotificationDetails {
|
|
91
|
-
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
92
|
-
|
|
60
|
+
export function buildNotificationDetails(record: Subagent, resultMaxLen: number): NotificationDetails {
|
|
93
61
|
return {
|
|
94
62
|
id: record.id,
|
|
95
63
|
description: record.description,
|
|
64
|
+
runId: record.runId,
|
|
65
|
+
mode: record.mode,
|
|
96
66
|
modelLabel: record.modelLabel,
|
|
97
67
|
thinkingLevel: record.effectiveThinkingLevel,
|
|
98
68
|
status: record.status,
|
|
69
|
+
terminalReason: record.stateTerminalReason,
|
|
99
70
|
toolUses: record.toolUses,
|
|
100
71
|
turnCount: record.turnCount,
|
|
101
72
|
maxTurns: record.maxTurns,
|
|
102
|
-
totalTokens,
|
|
103
|
-
durationMs: record.
|
|
73
|
+
totalTokens: getLifetimeTotal(record.lifetimeUsage),
|
|
74
|
+
durationMs: record.activeRuntimeMs,
|
|
104
75
|
outputFile: record.outputFile,
|
|
105
76
|
error: record.error,
|
|
106
|
-
resultPreview: record.
|
|
107
|
-
? "Agent was stopped while queued and never started. No work was performed."
|
|
108
|
-
: record.result
|
|
109
|
-
? record.result.length > resultMaxLen
|
|
110
|
-
? record.result.slice(0, resultMaxLen) + "…"
|
|
111
|
-
: record.result
|
|
112
|
-
: "No output.",
|
|
77
|
+
resultPreview: record.result ? record.result.slice(0, resultMaxLen) : "No output.",
|
|
113
78
|
};
|
|
114
79
|
}
|
|
115
80
|
|
|
116
|
-
/** Build event data for lifecycle events from a Subagent. */
|
|
117
81
|
export function buildEventData(record: Subagent) {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const total = getLifetimeTotal(u);
|
|
121
|
-
const tokens =
|
|
122
|
-
total > 0
|
|
123
|
-
? { input: u.input, output: u.output, total }
|
|
124
|
-
: undefined;
|
|
82
|
+
const usage = record.lifetimeUsage;
|
|
83
|
+
const total = getLifetimeTotal(usage);
|
|
125
84
|
return {
|
|
126
85
|
id: record.id,
|
|
86
|
+
runId: record.runId,
|
|
87
|
+
mode: record.mode,
|
|
127
88
|
type: record.type,
|
|
128
89
|
description: record.description,
|
|
129
90
|
result: record.result,
|
|
130
91
|
error: record.error,
|
|
131
92
|
status: record.status,
|
|
93
|
+
terminalReason: record.stateTerminalReason,
|
|
94
|
+
modelLabel: record.modelLabel,
|
|
95
|
+
thinkingLevel: record.effectiveThinkingLevel,
|
|
132
96
|
toolUses: record.toolUses,
|
|
133
|
-
|
|
134
|
-
tokens,
|
|
97
|
+
activeRuntimeMs: record.activeRuntimeMs,
|
|
98
|
+
tokens: total > 0 ? { input: usage.input, output: usage.output, total } : undefined,
|
|
135
99
|
};
|
|
136
100
|
}
|
|
137
101
|
|
|
138
|
-
// ---- Notification system factory ----
|
|
139
|
-
|
|
140
102
|
export interface NotificationSystem {
|
|
141
|
-
sendCompletion
|
|
142
|
-
dispose
|
|
103
|
+
sendCompletion(record: Subagent): void;
|
|
104
|
+
dispose(): void;
|
|
143
105
|
}
|
|
144
106
|
|
|
145
107
|
export class NotificationManager implements NotificationSystem {
|
|
146
|
-
|
|
147
|
-
// active so consumption can be rechecked at the actual delivery boundary.
|
|
148
|
-
private pendingNudges = new Map<string, Subagent>();
|
|
108
|
+
private readonly pendingNudges = new Map<string, { record: Subagent; runId: number; content: string; details: NotificationDetails }>();
|
|
149
109
|
private parentRunActive = false;
|
|
150
110
|
private disposed = false;
|
|
151
111
|
|
|
152
|
-
constructor(
|
|
153
|
-
private sendMessage: (
|
|
154
|
-
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
155
|
-
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
156
|
-
) => void,
|
|
157
|
-
) {}
|
|
112
|
+
constructor(private readonly sendMessage: (msg: { customType: string; content: string; display: boolean; details?: unknown }, opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }) => void) {}
|
|
158
113
|
|
|
159
114
|
sendCompletion(record: Subagent): void {
|
|
160
|
-
|
|
161
|
-
// this terminal outcome. Never enqueue the same payload as a follow-up,
|
|
162
|
-
// even if Pi's parent lifecycle currently appears idle.
|
|
163
|
-
if (this.disposed || record.consumed || record.hasPendingResultWait) return;
|
|
115
|
+
if (this.disposed || record.mode !== "detached" || record.consumed) return;
|
|
164
116
|
if (this.parentRunActive) {
|
|
165
|
-
this.pendingNudges.set(record.id
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
117
|
+
this.pendingNudges.set(`${record.id}:${record.runId}`, {
|
|
118
|
+
record,
|
|
119
|
+
runId: record.runId,
|
|
120
|
+
content: formatTaskNotification(record, 500),
|
|
121
|
+
details: buildNotificationDetails(record, 500),
|
|
122
|
+
});
|
|
123
|
+
} else this.emitIndividualNudge(record);
|
|
169
124
|
}
|
|
170
125
|
|
|
171
|
-
onParentAgentStart(): void {
|
|
172
|
-
if (!this.disposed) this.parentRunActive = true;
|
|
173
|
-
}
|
|
126
|
+
onParentAgentStart(): void { if (!this.disposed) this.parentRunActive = true; }
|
|
174
127
|
|
|
175
128
|
onParentAgentSettled(): void {
|
|
176
129
|
if (this.disposed) return;
|
|
177
130
|
this.parentRunActive = false;
|
|
178
|
-
const
|
|
131
|
+
const pending = [...this.pendingNudges.values()];
|
|
179
132
|
this.pendingNudges.clear();
|
|
180
|
-
for (const
|
|
133
|
+
for (const nudge of pending) {
|
|
181
134
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
135
|
+
// A resume may advance the record while the parent is running. The
|
|
136
|
+
// queued notification describes the old run and must not wake the
|
|
137
|
+
// parent after that run has been superseded (or after delivery mode
|
|
138
|
+
// changed to joined).
|
|
139
|
+
if (
|
|
140
|
+
nudge.runId === nudge.record.runId &&
|
|
141
|
+
nudge.record.mode === "detached" &&
|
|
142
|
+
!nudge.record.isActive() &&
|
|
143
|
+
!nudge.record.consumed
|
|
144
|
+
) {
|
|
145
|
+
this.emitNudge(nudge.record, nudge.content, nudge.details);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) { debugLog("notification render", error); }
|
|
186
148
|
}
|
|
187
149
|
}
|
|
188
150
|
|
|
@@ -192,24 +154,18 @@ export class NotificationManager implements NotificationSystem {
|
|
|
192
154
|
}
|
|
193
155
|
|
|
194
156
|
private emitIndividualNudge(record: Subagent): void {
|
|
195
|
-
if (this.disposed || record.consumed) return;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const outputFile = record.outputFile;
|
|
199
|
-
const footer = !record.stoppedWhileQueued && outputFile
|
|
200
|
-
? `\nFull transcript available at: ${outputFile}`
|
|
201
|
-
: "";
|
|
157
|
+
if (this.disposed || record.consumed || record.mode !== "detached") return;
|
|
158
|
+
this.emitNudge(record, formatTaskNotification(record, 500), buildNotificationDetails(record, 500));
|
|
159
|
+
}
|
|
202
160
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
212
|
-
// Push delivery collected the terminal outcome just as surely as a pull.
|
|
213
|
-
record.markConsumed();
|
|
161
|
+
private emitNudge(record: Subagent, notification: string, details: NotificationDetails, markConsumed = true): void {
|
|
162
|
+
const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : "";
|
|
163
|
+
this.sendMessage({
|
|
164
|
+
customType: "subagent-notification",
|
|
165
|
+
content: notification + footer,
|
|
166
|
+
display: true,
|
|
167
|
+
details,
|
|
168
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
169
|
+
if (markConsumed) record.markConsumed();
|
|
214
170
|
}
|
|
215
171
|
}
|
|
@@ -2,66 +2,39 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { NotificationDetails } from "#src/observation/notification";
|
|
3
3
|
import { formatModelThinking, formatMs, formatTokens, formatTurns } from "#src/ui/display";
|
|
4
4
|
|
|
5
|
-
/** Narrow theme interface — only the methods the renderer actually calls. */
|
|
6
5
|
interface RendererTheme {
|
|
7
6
|
fg(style: string, text: string): string;
|
|
8
7
|
bold(text: string): string;
|
|
9
8
|
}
|
|
9
|
+
interface RendererMessage { details?: NotificationDetails; }
|
|
10
|
+
interface RenderOptions { expanded: boolean; }
|
|
10
11
|
|
|
11
|
-
/** Narrow message interface — only the fields the renderer reads. */
|
|
12
|
-
interface RendererMessage {
|
|
13
|
-
details?: NotificationDetails;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Narrow render options — only the fields the renderer reads. */
|
|
17
|
-
interface RenderOptions {
|
|
18
|
-
expanded: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Create the notification renderer callback for `pi.registerMessageRenderer`.
|
|
23
|
-
* Returns a factory so the renderer is independently testable without the Pi SDK.
|
|
24
|
-
*/
|
|
25
12
|
export function createNotificationRenderer() {
|
|
26
13
|
return (message: RendererMessage, { expanded }: RenderOptions, theme: RendererTheme): Text | undefined => {
|
|
27
14
|
const d = message.details;
|
|
28
15
|
if (!d) return undefined;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Line 1: icon + agent description + status
|
|
16
|
+
const stopped = d.status === "stopped" || d.status === "error";
|
|
17
|
+
const icon = stopped ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
18
|
+
const statusText = d.status === "error"
|
|
19
|
+
? `Error: ${d.error ?? "unknown"}`
|
|
20
|
+
: d.terminalReason === "turn_limit_graceful"
|
|
21
|
+
? "completed (turn limit)"
|
|
22
|
+
: d.status === "stopped" && d.terminalReason
|
|
23
|
+
? `stopped (${d.terminalReason})`
|
|
24
|
+
: d.status;
|
|
39
25
|
let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
|
|
40
|
-
|
|
41
|
-
// Line 2: stats
|
|
42
26
|
const parts: string[] = [formatModelThinking(d.modelLabel, d.thinkingLevel)];
|
|
43
27
|
if (d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
44
28
|
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
45
29
|
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
46
30
|
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
47
|
-
|
|
48
|
-
line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Line 3: result preview (collapsed) or full (expanded)
|
|
31
|
+
line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
52
32
|
if (expanded) {
|
|
53
|
-
const
|
|
54
|
-
for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
|
|
33
|
+
for (const item of d.resultPreview.split("\n").slice(0, 30)) line += "\n" + theme.fg("dim", ` ${item}`);
|
|
55
34
|
} else {
|
|
56
|
-
|
|
57
|
-
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
35
|
+
line += "\n " + theme.fg("dim", `⎿ ${d.resultPreview.split("\n")[0]?.slice(0, 80) ?? ""}`);
|
|
58
36
|
}
|
|
59
|
-
|
|
60
|
-
// Line 4: output file link (if present)
|
|
61
|
-
if (d.outputFile) {
|
|
62
|
-
line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
37
|
+
if (d.outputFile) line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
65
38
|
return new Text(line, 0, 0);
|
|
66
39
|
};
|
|
67
40
|
}
|
|
@@ -3,106 +3,55 @@ import type { SubagentManagerObserver } from "#src/lifecycle/subagent-manager";
|
|
|
3
3
|
import { buildEventData, type NotificationSystem } from "#src/observation/notification";
|
|
4
4
|
import type { CompactionInfo, Subagent } from "#src/types";
|
|
5
5
|
|
|
6
|
-
/** Emit callback — a subset of `pi.events.emit`. */
|
|
7
6
|
export type EventEmit = (channel: string, data: unknown) => void;
|
|
8
|
-
|
|
9
|
-
/** Append callback — a subset of `pi.appendEntry`. */
|
|
10
7
|
export type AppendEntry = (customType: string, data: unknown) => void;
|
|
11
8
|
|
|
12
9
|
export interface SubagentEventsObserverDeps {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
emit: EventEmit;
|
|
11
|
+
appendEntry: AppendEntry;
|
|
12
|
+
notifications: NotificationSystem;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
/**
|
|
19
|
-
* Receives agent lifecycle notifications from SubagentManager and dispatches
|
|
20
|
-
* them to three concerns: pi.events lifecycle events, session-entry persistence,
|
|
21
|
-
* and completion notifications.
|
|
22
|
-
*
|
|
23
|
-
* Constructed with narrow deps (emit, appendEntry, NotificationSystem) so all
|
|
24
|
-
* three concerns are unit-testable without booting the extension.
|
|
25
|
-
*/
|
|
26
15
|
export class SubagentEventsObserver implements SubagentManagerObserver {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// The manager decides whether to nudge (it owns the consumed-result state).
|
|
69
|
-
runSafely("subagent completion notification", () => this.notifications.sendCompletion(record));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
onSubagentResumed(record: Subagent): void {
|
|
73
|
-
const eventData = buildEventData(record);
|
|
74
|
-
runSafely("subagent event emission resumed", () => this.emit("subagents:resumed", eventData));
|
|
75
|
-
runSafely("subagent record append resumed", () => this.appendEntry("subagents:record", {
|
|
76
|
-
id: record.id,
|
|
77
|
-
type: record.type,
|
|
78
|
-
description: record.description,
|
|
79
|
-
status: record.status,
|
|
80
|
-
result: record.result,
|
|
81
|
-
error: record.error,
|
|
82
|
-
startedAt: record.startedAt,
|
|
83
|
-
completedAt: record.completedAt,
|
|
84
|
-
}));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
onSubagentCompacted(record: Subagent, info: CompactionInfo): void {
|
|
88
|
-
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
89
|
-
runSafely("subagent event emission compacted", () => this.emit("subagents:compacted", {
|
|
90
|
-
id: record.id,
|
|
91
|
-
type: record.type,
|
|
92
|
-
description: record.description,
|
|
93
|
-
reason: info.reason,
|
|
94
|
-
tokensBefore: info.tokensBefore,
|
|
95
|
-
compactionCount: record.compactionCount,
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
onSubagentCreated(record: Subagent): void {
|
|
100
|
-
// Emit created event for background agents (before limiter admission).
|
|
101
|
-
runSafely("subagent event emission created", () => this.emit("subagents:created", {
|
|
102
|
-
id: record.id,
|
|
103
|
-
type: record.type,
|
|
104
|
-
description: record.description,
|
|
105
|
-
isBackground: true,
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
16
|
+
constructor(private readonly deps: SubagentEventsObserverDeps) {}
|
|
17
|
+
|
|
18
|
+
onSubagentStarted(record: Subagent): void {
|
|
19
|
+
runSafely("subagent event emission started", () => this.deps.emit("subagents:started", {
|
|
20
|
+
id: record.id, runId: record.runId, mode: record.mode, type: record.type, description: record.description,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onSubagentCompleted(record: Subagent): void {
|
|
25
|
+
const eventData = buildEventData(record);
|
|
26
|
+
const channel = record.status === "error" || record.status === "stopped" ? "subagents:failed" : "subagents:completed";
|
|
27
|
+
runSafely("subagent event emission terminal", () => this.deps.emit(channel, eventData));
|
|
28
|
+
runSafely("subagent record append", () => this.deps.appendEntry("subagents:record", eventData));
|
|
29
|
+
runSafely("subagent completion notification", () => this.deps.notifications.sendCompletion(record));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onSubagentResumedStarted(record: Subagent): void {
|
|
33
|
+
runSafely("subagent event emission resumed-started", () => this.deps.emit("subagents:started", {
|
|
34
|
+
id: record.id, runId: record.runId, mode: record.mode, type: record.type, description: record.description, resumed: true,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onSubagentResumed(record: Subagent): void {
|
|
39
|
+
const eventData = buildEventData(record);
|
|
40
|
+
runSafely("subagent event emission resumed", () => this.deps.emit("subagents:resumed", eventData));
|
|
41
|
+
runSafely("subagent record append resumed", () => this.deps.appendEntry("subagents:record", eventData));
|
|
42
|
+
runSafely("subagent resumed notification", () => this.deps.notifications.sendCompletion(record));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onSubagentCompacted(record: Subagent, info: CompactionInfo): void {
|
|
46
|
+
runSafely("subagent event emission compacted", () => this.deps.emit("subagents:compacted", {
|
|
47
|
+
id: record.id, runId: record.runId, mode: record.mode, type: record.type, description: record.description,
|
|
48
|
+
reason: info.reason, tokensBefore: info.tokensBefore, compactionCount: record.compactionCount,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onSubagentCreated(record: Subagent): void {
|
|
53
|
+
runSafely("subagent event emission created", () => this.deps.emit("subagents:created", {
|
|
54
|
+
id: record.id, runId: record.runId, mode: record.mode, type: record.type, description: record.description,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
108
57
|
}
|