@gotgenes/pi-subagents 16.5.0 → 17.0.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 +18 -0
- package/dist/public.d.ts +4 -1
- package/docs/architecture/architecture.md +39 -31
- package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +309 -0
- package/docs/plans/0422-delete-activity-tracker-ui-observer.md +204 -0
- package/docs/plans/0423-widget-self-drive-from-lifecycle.md +264 -0
- package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +166 -0
- package/docs/plans/0425-reconcile-subagent-events-contract.md +144 -0
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +45 -0
- package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +105 -0
- package/docs/retro/0422-delete-activity-tracker-ui-observer.md +97 -0
- package/docs/retro/0423-widget-self-drive-from-lifecycle.md +112 -0
- package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +89 -0
- package/docs/retro/0425-reconcile-subagent-events-contract.md +46 -0
- package/package.json +1 -1
- package/src/index.ts +12 -6
- package/src/observation/composite-subagent-observer.ts +49 -0
- package/src/observation/notification.ts +3 -12
- package/src/observation/subagent-events-observer.ts +1 -4
- package/src/runtime.ts +0 -6
- package/src/service/service.ts +4 -1
- package/src/tools/agent-tool.ts +1 -36
- package/src/tools/background-spawner.ts +1 -25
- package/src/tools/foreground-runner.ts +9 -34
- package/src/tools/helpers.ts +5 -4
- package/src/types.ts +1 -1
- package/src/ui/agent-menu.ts +0 -12
- package/src/ui/agent-widget.ts +56 -17
- package/src/ui/conversation-viewer.ts +4 -9
- package/src/ui/widget-renderer.ts +14 -22
- package/src/ui/agent-activity-tracker.ts +0 -84
- package/src/ui/ui-observer.ts +0 -61
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
3
3
|
import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
|
|
4
|
-
import type { AgentActivityAccess } from "#src/tools/agent-tool";
|
|
5
4
|
import {
|
|
6
5
|
buildDetails,
|
|
7
6
|
formatLifetimeTokens,
|
|
@@ -10,14 +9,12 @@ import {
|
|
|
10
9
|
} from "#src/tools/helpers";
|
|
11
10
|
import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
|
|
12
11
|
import type { ParentSessionInfo, Subagent } from "#src/types";
|
|
13
|
-
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
14
12
|
import {
|
|
15
13
|
type AgentDetails,
|
|
16
14
|
describeActivity,
|
|
17
15
|
formatMs,
|
|
18
16
|
SPINNER,
|
|
19
17
|
} from "#src/ui/display";
|
|
20
|
-
import { subscribeUIObserver } from "#src/ui/ui-observer";
|
|
21
18
|
|
|
22
19
|
/** Narrow manager interface for the foreground runner. */
|
|
23
20
|
export interface ForegroundManagerDeps {
|
|
@@ -29,12 +26,6 @@ export interface ForegroundManagerDeps {
|
|
|
29
26
|
): Promise<Subagent>;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
/** Narrow widget interface for the foreground runner. */
|
|
33
|
-
export interface ForegroundWidgetDeps {
|
|
34
|
-
ensureTimer(): void;
|
|
35
|
-
markFinished(id: string): void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
/** All values the foreground runner needs beyond the resolved config. */
|
|
39
30
|
export interface ForegroundParams {
|
|
40
31
|
config: ResolvedSpawnConfig;
|
|
@@ -44,13 +35,10 @@ export interface ForegroundParams {
|
|
|
44
35
|
|
|
45
36
|
/**
|
|
46
37
|
* Run an agent synchronously in the foreground, streaming spinner updates.
|
|
47
|
-
* Owns: spinner interval,
|
|
48
|
-
* streaming onUpdate callbacks, cleanup, and result formatting.
|
|
38
|
+
* Owns: spinner interval, streaming onUpdate callbacks, cleanup, and result formatting.
|
|
49
39
|
*/
|
|
50
40
|
export async function runForeground(
|
|
51
41
|
manager: ForegroundManagerDeps,
|
|
52
|
-
widget: ForegroundWidgetDeps,
|
|
53
|
-
agentActivity: AgentActivityAccess,
|
|
54
42
|
params: ForegroundParams,
|
|
55
43
|
signal: AbortSignal | undefined,
|
|
56
44
|
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
@@ -58,10 +46,7 @@ export async function runForeground(
|
|
|
58
46
|
const { identity, execution, presentation } = params.config;
|
|
59
47
|
let spinnerFrame = 0;
|
|
60
48
|
const startedAt = Date.now();
|
|
61
|
-
let fgId: string | undefined;
|
|
62
49
|
|
|
63
|
-
const fgState = new AgentActivityTracker(execution.effectiveMaxTurns);
|
|
64
|
-
let unsubUI: (() => void) | undefined;
|
|
65
50
|
let recordRef: Subagent | undefined;
|
|
66
51
|
|
|
67
52
|
const streamUpdate = () => {
|
|
@@ -70,11 +55,15 @@ export async function runForeground(
|
|
|
70
55
|
...presentation.detailBase,
|
|
71
56
|
toolUses,
|
|
72
57
|
tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
// Read activity off the record; fall back to safe defaults before onSessionCreated fires
|
|
59
|
+
turnCount: recordRef?.turnCount ?? 1,
|
|
60
|
+
maxTurns: recordRef?.maxTurns ?? execution.effectiveMaxTurns,
|
|
75
61
|
durationMs: Date.now() - startedAt,
|
|
76
62
|
status: "running",
|
|
77
|
-
activity: describeActivity(
|
|
63
|
+
activity: describeActivity(
|
|
64
|
+
recordRef?.activeTools ?? new Map(),
|
|
65
|
+
recordRef?.responseText ?? "",
|
|
66
|
+
),
|
|
78
67
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
79
68
|
};
|
|
80
69
|
onUpdate?.({
|
|
@@ -109,34 +98,20 @@ export async function runForeground(
|
|
|
109
98
|
parentSession: params.parentSession,
|
|
110
99
|
observer: {
|
|
111
100
|
onSessionCreated: (agent) => {
|
|
112
|
-
const sub = agent.subagentSession!;
|
|
113
|
-
fgState.setSession(sub);
|
|
114
101
|
recordRef = agent;
|
|
115
|
-
unsubUI = subscribeUIObserver(sub, fgState, streamUpdate);
|
|
116
|
-
fgId = agent.id;
|
|
117
|
-
agentActivity.set(agent.id, fgState);
|
|
118
|
-
widget.ensureTimer();
|
|
119
102
|
},
|
|
120
103
|
},
|
|
121
104
|
},
|
|
122
105
|
);
|
|
123
106
|
} catch (err) {
|
|
124
107
|
clearInterval(spinnerInterval);
|
|
125
|
-
unsubUI?.();
|
|
126
108
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
127
109
|
}
|
|
128
110
|
|
|
129
111
|
clearInterval(spinnerInterval);
|
|
130
|
-
unsubUI?.();
|
|
131
|
-
|
|
132
|
-
// Clean up foreground agent from widget
|
|
133
|
-
if (fgId) {
|
|
134
|
-
agentActivity.delete(fgId);
|
|
135
|
-
widget.markFinished(fgId);
|
|
136
|
-
}
|
|
137
112
|
|
|
138
113
|
const tokenText = formatLifetimeTokens(record);
|
|
139
|
-
const details = buildDetails(presentation.detailBase, record,
|
|
114
|
+
const details = buildDetails(presentation.detailBase, record, { tokens: tokenText });
|
|
140
115
|
|
|
141
116
|
const fallbackNote = identity.fellBack
|
|
142
117
|
? `Note: Unknown agent type "${identity.rawType}" — using general-purpose.\n\n`
|
package/src/tools/helpers.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
2
2
|
import { getLifetimeTotal, type LifetimeUsage } from "#src/lifecycle/usage";
|
|
3
|
-
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
4
3
|
import { type AgentDetails, formatTokens } from "#src/ui/display";
|
|
5
4
|
|
|
6
5
|
/** Parenthetical status note for completed agent result text. */
|
|
@@ -28,16 +27,18 @@ export function buildDetails(
|
|
|
28
27
|
error?: string;
|
|
29
28
|
id?: string;
|
|
30
29
|
lifetimeUsage: LifetimeUsage;
|
|
30
|
+
/** Live-activity counters — exposed as getters on Subagent (Phase 18 Step 2). */
|
|
31
|
+
turnCount?: number;
|
|
32
|
+
maxTurns?: number;
|
|
31
33
|
},
|
|
32
|
-
activity?: AgentActivityTracker,
|
|
33
34
|
overrides?: Partial<AgentDetails>,
|
|
34
35
|
): AgentDetails {
|
|
35
36
|
return {
|
|
36
37
|
...base,
|
|
37
38
|
toolUses: record.toolUses,
|
|
38
39
|
tokens: formatLifetimeTokens(record),
|
|
39
|
-
turnCount:
|
|
40
|
-
maxTurns:
|
|
40
|
+
turnCount: record.turnCount,
|
|
41
|
+
maxTurns: record.maxTurns,
|
|
41
42
|
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
42
43
|
status: record.status as AgentDetails["status"],
|
|
43
44
|
agentId: record.id,
|
package/src/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { AgentSessionEvent, ThinkingLevel };
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Narrow session interface for event subscription.
|
|
15
|
-
* Used by record-observer
|
|
15
|
+
* Used by record-observer — only the subscribe method is needed.
|
|
16
16
|
*/
|
|
17
17
|
export interface SubscribableSession {
|
|
18
18
|
subscribe(fn: (event: AgentSessionEvent) => void): () => void;
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
|
5
5
|
import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
|
|
6
6
|
import { getModelLabelFromConfig } from "#src/tools/helpers";
|
|
7
7
|
import type { AgentConfig, Subagent } from "#src/types";
|
|
8
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
9
8
|
import { AgentConfigEditor } from "#src/ui/agent-config-editor";
|
|
10
9
|
import { AgentCreationWizard } from "#src/ui/agent-creation-wizard";
|
|
11
10
|
import type { AgentFileOps } from "#src/ui/agent-file-ops";
|
|
@@ -36,14 +35,6 @@ export interface AgentMenuSettings {
|
|
|
36
35
|
applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
/**
|
|
40
|
-
* Read-only interface for the agent-menu's agentActivity access.
|
|
41
|
-
* Only the conversation viewer needs to read a tracker by agent ID.
|
|
42
|
-
*/
|
|
43
|
-
export interface AgentActivityReader {
|
|
44
|
-
get(id: string): AgentActivityTracker | undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
// ---- Narrow UI context types ----
|
|
48
39
|
|
|
49
40
|
/** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
|
|
@@ -70,7 +61,6 @@ export class AgentsMenuHandler {
|
|
|
70
61
|
constructor(
|
|
71
62
|
private readonly manager: AgentMenuManager,
|
|
72
63
|
private readonly registry: AgentTypeRegistry,
|
|
73
|
-
private readonly agentActivity: AgentActivityReader,
|
|
74
64
|
private readonly settings: AgentMenuSettings,
|
|
75
65
|
fileOps: AgentFileOps,
|
|
76
66
|
personalAgentsDir: string,
|
|
@@ -263,14 +253,12 @@ export class AgentsMenuHandler {
|
|
|
263
253
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
|
|
264
254
|
"./conversation-viewer"
|
|
265
255
|
);
|
|
266
|
-
const activity = this.agentActivity.get(record.id);
|
|
267
256
|
|
|
268
257
|
await ui.custom<undefined>(
|
|
269
258
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
270
259
|
return new ConversationViewer({
|
|
271
260
|
tui,
|
|
272
261
|
record,
|
|
273
|
-
activity,
|
|
274
262
|
theme,
|
|
275
263
|
done,
|
|
276
264
|
registry: this.registry,
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
10
|
-
import type {
|
|
11
|
-
import type {
|
|
10
|
+
import type { Subagent } from "#src/lifecycle/subagent";
|
|
11
|
+
import type { SubagentManager, SubagentManagerObserver } from "#src/lifecycle/subagent-manager";
|
|
12
|
+
import type { CompactionInfo } from "#src/types";
|
|
12
13
|
import { ERROR_STATUSES, type Theme } from "#src/ui/display";
|
|
13
|
-
import { renderWidgetLines } from "#src/ui/widget-renderer";
|
|
14
|
+
import { renderWidgetLines, type WidgetAgent } from "#src/ui/widget-renderer";
|
|
14
15
|
|
|
15
16
|
// ---- Types ----
|
|
16
17
|
|
|
@@ -61,7 +62,7 @@ export type UICtx = {
|
|
|
61
62
|
|
|
62
63
|
// ---- Widget manager ----
|
|
63
64
|
|
|
64
|
-
export class AgentWidget {
|
|
65
|
+
export class AgentWidget implements SubagentManagerObserver {
|
|
65
66
|
private uiCtx: UICtx | undefined;
|
|
66
67
|
private widgetFrame = 0;
|
|
67
68
|
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
@@ -79,7 +80,6 @@ export class AgentWidget {
|
|
|
79
80
|
|
|
80
81
|
constructor(
|
|
81
82
|
private manager: SubagentManager,
|
|
82
|
-
private agentActivity: Map<string, AgentActivityTracker>,
|
|
83
83
|
private registry: AgentTypeRegistry,
|
|
84
84
|
) {}
|
|
85
85
|
|
|
@@ -108,9 +108,36 @@ export class AgentWidget {
|
|
|
108
108
|
this.update();
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// ---- SubagentManagerObserver: react to lifecycle, self-drive the timer ----
|
|
112
|
+
|
|
113
|
+
/** A subagent started running — ensure the update loop is live and render. */
|
|
114
|
+
onSubagentStarted(_record: Subagent) {
|
|
115
|
+
this.startLoop();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A background subagent was created (queued) — ensure the loop is live and render. */
|
|
119
|
+
onSubagentCreated(_record: Subagent) {
|
|
120
|
+
this.startLoop();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A subagent completed — render so the finished state is seeded and shown. */
|
|
124
|
+
onSubagentCompleted(_record: Subagent) {
|
|
125
|
+
this.update();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A subagent's session compacted — render to refresh the compaction count. */
|
|
129
|
+
onSubagentCompacted(_record: Subagent, _info: CompactionInfo) {
|
|
130
|
+
this.update();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Start the update timer (if not already running) and render immediately. */
|
|
134
|
+
private startLoop() {
|
|
135
|
+
this.ensureTimer();
|
|
136
|
+
this.update();
|
|
137
|
+
}
|
|
138
|
+
|
|
111
139
|
/** Ensure the widget update timer is running. */
|
|
112
|
-
|
|
113
|
-
ensureTimer() {
|
|
140
|
+
private ensureTimer() {
|
|
114
141
|
this.widgetInterval ??= setInterval(() => this.update(), 80);
|
|
115
142
|
}
|
|
116
143
|
|
|
@@ -121,19 +148,31 @@ export class AgentWidget {
|
|
|
121
148
|
return age < maxAge;
|
|
122
149
|
}
|
|
123
150
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
/** Project a live Subagent record onto a pure-data WidgetAgent snapshot. */
|
|
152
|
+
private toWidgetAgent(record: Subagent): WidgetAgent {
|
|
153
|
+
return {
|
|
154
|
+
id: record.id,
|
|
155
|
+
type: record.type,
|
|
156
|
+
status: record.status,
|
|
157
|
+
description: record.description,
|
|
158
|
+
toolUses: record.toolUses,
|
|
159
|
+
startedAt: record.startedAt,
|
|
160
|
+
completedAt: record.completedAt,
|
|
161
|
+
error: record.error,
|
|
162
|
+
lifetimeUsage: record.lifetimeUsage,
|
|
163
|
+
compactionCount: record.compactionCount,
|
|
164
|
+
turnCount: record.turnCount,
|
|
165
|
+
maxTurns: record.maxTurns,
|
|
166
|
+
activeTools: record.activeTools,
|
|
167
|
+
responseText: record.responseText,
|
|
168
|
+
contextPercent: record.getContextPercent(),
|
|
169
|
+
};
|
|
130
170
|
}
|
|
131
171
|
|
|
132
172
|
/** Delegate rendering to the pure widget-renderer module. */
|
|
133
173
|
private renderWidget(tui: any, theme: Theme): string[] {
|
|
134
174
|
return renderWidgetLines({
|
|
135
|
-
agents: this.manager.listAgents(),
|
|
136
|
-
activityMap: this.agentActivity,
|
|
175
|
+
agents: this.manager.listAgents().map(r => this.toWidgetAgent(r)),
|
|
137
176
|
registry: this.registry,
|
|
138
177
|
spinnerFrame: this.widgetFrame,
|
|
139
178
|
terminalWidth: tui.terminal.columns,
|
|
@@ -184,8 +223,8 @@ export class AgentWidget {
|
|
|
184
223
|
|
|
185
224
|
/**
|
|
186
225
|
* Seed linger tracking for any newly-observed finished agent.
|
|
187
|
-
*
|
|
188
|
-
*
|
|
226
|
+
* The widget owns detection of completions it observes via `listAgents()`,
|
|
227
|
+
* so no external bookkeeping call is needed.
|
|
189
228
|
* Idempotent — only seeds when an entry is absent, so repeated updates within
|
|
190
229
|
* a turn neither reset nor advance the age.
|
|
191
230
|
*/
|
|
@@ -9,7 +9,6 @@ import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } f
|
|
|
9
9
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
10
10
|
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
11
11
|
import type { Subagent } from "#src/types";
|
|
12
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
13
12
|
import { buildInvocationTags, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "#src/ui/display";
|
|
14
13
|
import { formatMessage, formatStreamingIndicator } from "#src/ui/message-formatters";
|
|
15
14
|
|
|
@@ -24,7 +23,6 @@ export const VIEWPORT_HEIGHT_PCT = 70;
|
|
|
24
23
|
export interface ConversationViewerOptions {
|
|
25
24
|
tui: TUI;
|
|
26
25
|
record: Subagent;
|
|
27
|
-
activity: AgentActivityTracker | undefined;
|
|
28
26
|
theme: Theme;
|
|
29
27
|
done: (result: undefined) => void;
|
|
30
28
|
registry: AgentConfigLookup;
|
|
@@ -40,7 +38,6 @@ export class ConversationViewer implements Component {
|
|
|
40
38
|
|
|
41
39
|
private tui: TUI;
|
|
42
40
|
private record: Subagent;
|
|
43
|
-
private activity: AgentActivityTracker | undefined;
|
|
44
41
|
private theme: Theme;
|
|
45
42
|
private done: (result: undefined) => void;
|
|
46
43
|
private registry: AgentConfigLookup;
|
|
@@ -49,7 +46,6 @@ export class ConversationViewer implements Component {
|
|
|
49
46
|
constructor({
|
|
50
47
|
tui,
|
|
51
48
|
record,
|
|
52
|
-
activity,
|
|
53
49
|
theme,
|
|
54
50
|
done,
|
|
55
51
|
registry,
|
|
@@ -57,7 +53,6 @@ export class ConversationViewer implements Component {
|
|
|
57
53
|
}: ConversationViewerOptions) {
|
|
58
54
|
this.tui = tui;
|
|
59
55
|
this.record = record;
|
|
60
|
-
this.activity = activity;
|
|
61
56
|
this.theme = theme;
|
|
62
57
|
this.done = done;
|
|
63
58
|
this.registry = registry;
|
|
@@ -231,11 +226,11 @@ export class ConversationViewer implements Component {
|
|
|
231
226
|
needsSeparator = true;
|
|
232
227
|
}
|
|
233
228
|
|
|
234
|
-
// Streaming indicator for running agents
|
|
235
|
-
if (this.record.status === "running"
|
|
229
|
+
// Streaming indicator for running agents — read activity off the record
|
|
230
|
+
if (this.record.status === "running") {
|
|
236
231
|
lines.push(...formatStreamingIndicator(
|
|
237
|
-
this.
|
|
238
|
-
this.
|
|
232
|
+
this.record.activeTools,
|
|
233
|
+
this.record.responseText,
|
|
239
234
|
width,
|
|
240
235
|
th,
|
|
241
236
|
));
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
10
|
-
import type { LifetimeUsage
|
|
11
|
-
import { getLifetimeTotal
|
|
10
|
+
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
11
|
+
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
12
12
|
import type { SubagentType } from "#src/types";
|
|
13
13
|
import {
|
|
14
14
|
describeActivity,
|
|
@@ -35,15 +35,13 @@ export interface WidgetAgent {
|
|
|
35
35
|
readonly error?: string;
|
|
36
36
|
readonly lifetimeUsage?: Readonly<LifetimeUsage>;
|
|
37
37
|
readonly compactionCount: number;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** Read-only activity snapshot for widget rendering. */
|
|
41
|
-
export interface WidgetActivity {
|
|
42
|
-
readonly activeTools: ReadonlyMap<string, string>;
|
|
43
|
-
readonly responseText: string;
|
|
38
|
+
// Live activity (folded from the former WidgetActivity — precomputed by AgentWidget)
|
|
44
39
|
readonly turnCount: number;
|
|
45
40
|
readonly maxTurns?: number;
|
|
46
|
-
readonly
|
|
41
|
+
readonly activeTools: ReadonlyMap<string, string>;
|
|
42
|
+
readonly responseText: string;
|
|
43
|
+
/** Context-window utilisation (0–100), or null when unavailable. */
|
|
44
|
+
readonly contextPercent: number | null;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
// ── Per-agent rendering ──────────────────────────────────────────────────────
|
|
@@ -51,7 +49,6 @@ export interface WidgetActivity {
|
|
|
51
49
|
/** Render a single finished agent line (no tree connector prefix). */
|
|
52
50
|
export function renderFinishedLine(
|
|
53
51
|
agent: WidgetAgent,
|
|
54
|
-
activity: WidgetActivity | undefined,
|
|
55
52
|
registry: AgentConfigLookup,
|
|
56
53
|
theme: Theme,
|
|
57
54
|
): string {
|
|
@@ -81,7 +78,7 @@ export function renderFinishedLine(
|
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
const parts: string[] = [];
|
|
84
|
-
|
|
81
|
+
parts.push(formatTurns(agent.turnCount, agent.maxTurns));
|
|
85
82
|
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
|
|
86
83
|
parts.push(duration);
|
|
87
84
|
|
|
@@ -92,7 +89,6 @@ export function renderFinishedLine(
|
|
|
92
89
|
/** Render a single running agent as header + activity line pair (no tree connector prefix). */
|
|
93
90
|
export function renderRunningLines(
|
|
94
91
|
agent: WidgetAgent,
|
|
95
|
-
activity: WidgetActivity | undefined,
|
|
96
92
|
registry: AgentConfigLookup,
|
|
97
93
|
spinnerFrame: number,
|
|
98
94
|
theme: Theme,
|
|
@@ -103,18 +99,17 @@ export function renderRunningLines(
|
|
|
103
99
|
const elapsed = formatMs(Date.now() - agent.startedAt);
|
|
104
100
|
|
|
105
101
|
const tokens = getLifetimeTotal(agent.lifetimeUsage);
|
|
106
|
-
const
|
|
107
|
-
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, agent.compactionCount) : "";
|
|
102
|
+
const tokenText = tokens > 0 ? formatSessionTokens(tokens, agent.contextPercent, theme, agent.compactionCount) : "";
|
|
108
103
|
|
|
109
104
|
const parts: string[] = [];
|
|
110
|
-
|
|
105
|
+
parts.push(formatTurns(agent.turnCount, agent.maxTurns));
|
|
111
106
|
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
|
|
112
107
|
if (tokenText) parts.push(tokenText);
|
|
113
108
|
parts.push(elapsed);
|
|
114
109
|
const statsText = parts.join(" · ");
|
|
115
110
|
|
|
116
111
|
const frame = SPINNER[spinnerFrame % SPINNER.length];
|
|
117
|
-
const activityText =
|
|
112
|
+
const activityText = describeActivity(agent.activeTools, agent.responseText);
|
|
118
113
|
|
|
119
114
|
const header = `${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", agent.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`;
|
|
120
115
|
const activityLine = theme.fg("dim", ` \u23BF ${activityText}`);
|
|
@@ -157,7 +152,6 @@ interface WidgetSections {
|
|
|
157
152
|
/** Render each agent bucket into pre-formatted lines with ├─ tree connectors. */
|
|
158
153
|
function buildSections(
|
|
159
154
|
categories: AgentCategories,
|
|
160
|
-
activityMap: ReadonlyMap<string, WidgetActivity>,
|
|
161
155
|
registry: AgentConfigLookup,
|
|
162
156
|
spinnerFrame: number,
|
|
163
157
|
theme: Theme,
|
|
@@ -165,12 +159,12 @@ function buildSections(
|
|
|
165
159
|
): WidgetSections {
|
|
166
160
|
const finishedLines: string[] = [];
|
|
167
161
|
for (const a of categories.finished) {
|
|
168
|
-
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a,
|
|
162
|
+
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, registry, theme)));
|
|
169
163
|
}
|
|
170
164
|
|
|
171
165
|
const runningLines: [string, string][] = [];
|
|
172
166
|
for (const a of categories.running) {
|
|
173
|
-
const [header, act] = renderRunningLines(a,
|
|
167
|
+
const [header, act] = renderRunningLines(a, registry, spinnerFrame, theme);
|
|
174
168
|
runningLines.push([
|
|
175
169
|
truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
|
|
176
170
|
truncate(theme.fg("dim", "\u2502 ") + act),
|
|
@@ -259,14 +253,13 @@ function assembleOverflow(
|
|
|
259
253
|
/** Pure rendering of the widget body. Returns lines to display. */
|
|
260
254
|
export function renderWidgetLines(params: {
|
|
261
255
|
agents: readonly WidgetAgent[];
|
|
262
|
-
activityMap: ReadonlyMap<string, WidgetActivity>;
|
|
263
256
|
registry: AgentConfigLookup;
|
|
264
257
|
spinnerFrame: number;
|
|
265
258
|
terminalWidth: number;
|
|
266
259
|
theme: Theme;
|
|
267
260
|
shouldShowFinished: (agentId: string, status: string) => boolean;
|
|
268
261
|
}): string[] {
|
|
269
|
-
const { agents,
|
|
262
|
+
const { agents, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
|
|
270
263
|
|
|
271
264
|
const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
|
|
272
265
|
|
|
@@ -281,7 +274,6 @@ export function renderWidgetLines(params: {
|
|
|
281
274
|
|
|
282
275
|
const { finishedLines, runningLines, queuedLine } = buildSections(
|
|
283
276
|
{ running, queued, finished },
|
|
284
|
-
activityMap,
|
|
285
277
|
registry,
|
|
286
278
|
spinnerFrame,
|
|
287
279
|
theme,
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* agent-activity-tracker.ts — Per-agent live activity state with explicit transition methods.
|
|
3
|
-
*
|
|
4
|
-
* Replaces the mutable `AgentActivity` interface that was written via output arguments
|
|
5
|
-
* in `ui-observer.ts`. Callers use named transition methods; readers use read-only accessors.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { SessionLike } from "#src/lifecycle/usage";
|
|
9
|
-
|
|
10
|
-
/** Per-agent live activity state with explicit transition methods and read-only accessors. */
|
|
11
|
-
export class AgentActivityTracker {
|
|
12
|
-
private _activeTools = new Map<string, string>();
|
|
13
|
-
private _toolKeySeq = 0;
|
|
14
|
-
private _responseText = "";
|
|
15
|
-
private _session: SessionLike | undefined = undefined;
|
|
16
|
-
private _turnCount = 1;
|
|
17
|
-
|
|
18
|
-
constructor(private readonly _maxTurns?: number) {}
|
|
19
|
-
|
|
20
|
-
// ── Transition methods (write surface) ──────────────────────────────────
|
|
21
|
-
|
|
22
|
-
/** Record that a tool has started executing. */
|
|
23
|
-
onToolStart(toolName: string): void {
|
|
24
|
-
this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
|
|
25
|
-
}
|
|
26
|
-
|
|
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 {
|
|
29
|
-
for (const [key, name] of this._activeTools) {
|
|
30
|
-
if (name === toolName) {
|
|
31
|
-
this._activeTools.delete(key);
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Reset the current response text (called at the start of each assistant message). */
|
|
38
|
-
onMessageStart(): void {
|
|
39
|
-
this._responseText = "";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Append a text delta to the current response text. */
|
|
43
|
-
onMessageUpdate(delta: string): void {
|
|
44
|
-
this._responseText += delta;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Record that a turn has ended; increments turnCount. */
|
|
48
|
-
onTurnEnd(): void {
|
|
49
|
-
this._turnCount++;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Bind the session reference (called once when the agent session is created). */
|
|
53
|
-
setSession(session: SessionLike): void {
|
|
54
|
-
this._session = session;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Read-only accessors ──────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/** Currently-active tools: key → tool name. Multiple entries for concurrent same-name tools. */
|
|
60
|
-
get activeTools(): ReadonlyMap<string, string> {
|
|
61
|
-
return this._activeTools;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The agent's latest partial response text (reset at each message start). */
|
|
65
|
-
get responseText(): string {
|
|
66
|
-
return this._responseText;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The active SDK session, or undefined before the first session is created. */
|
|
70
|
-
get session(): SessionLike | undefined {
|
|
71
|
-
return this._session;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Current turn count (starts at 1). */
|
|
75
|
-
get turnCount(): number {
|
|
76
|
-
return this._turnCount;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Effective max turns for this agent, or undefined for unlimited. */
|
|
80
|
-
get maxTurns(): number | undefined {
|
|
81
|
-
return this._maxTurns;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
}
|
package/src/ui/ui-observer.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
|
|
3
|
-
*
|
|
4
|
-
* Replaces the callback-based createActivityTracker pattern with a direct
|
|
5
|
-
* session subscription for streaming UI state (active tools, response text,
|
|
6
|
-
* turn count, lifetime usage).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { SubscribableSession } from "#src/types";
|
|
10
|
-
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Subscribe to session events and stream UI state into an AgentActivityTracker.
|
|
14
|
-
*
|
|
15
|
-
* Handles:
|
|
16
|
-
* - `tool_execution_start` → `tracker.onToolStart(name)`
|
|
17
|
-
* - `tool_execution_end` → `tracker.onToolEnd(name)`
|
|
18
|
-
* - `message_start` → `tracker.onMessageStart()`
|
|
19
|
-
* - `message_update` (text_delta) → `tracker.onMessageUpdate(delta)`
|
|
20
|
-
* - `turn_end` → `tracker.onTurnEnd()`
|
|
21
|
-
* - `message_end` (assistant, with usage) → `tracker.onUsageUpdate(usage)`
|
|
22
|
-
*
|
|
23
|
-
* Calls `onUpdate?.()` after each state mutation to trigger re-renders.
|
|
24
|
-
*
|
|
25
|
-
* @returns An unsubscribe function.
|
|
26
|
-
*/
|
|
27
|
-
export function subscribeUIObserver(
|
|
28
|
-
session: SubscribableSession,
|
|
29
|
-
tracker: AgentActivityTracker,
|
|
30
|
-
onUpdate?: () => void,
|
|
31
|
-
): () => void {
|
|
32
|
-
return session.subscribe((event) => {
|
|
33
|
-
if (event.type === "tool_execution_start") {
|
|
34
|
-
tracker.onToolStart(event.toolName);
|
|
35
|
-
onUpdate?.();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (event.type === "tool_execution_end") {
|
|
39
|
-
tracker.onToolDone(event.toolName);
|
|
40
|
-
onUpdate?.();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (event.type === "message_start") {
|
|
44
|
-
tracker.onMessageStart();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
event.type === "message_update" &&
|
|
49
|
-
event.assistantMessageEvent.type === "text_delta"
|
|
50
|
-
) {
|
|
51
|
-
tracker.onMessageUpdate(event.assistantMessageEvent.delta);
|
|
52
|
-
onUpdate?.();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (event.type === "turn_end") {
|
|
56
|
-
tracker.onTurnEnd();
|
|
57
|
-
onUpdate?.();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
}
|