@gotgenes/pi-subagents 6.5.0 → 6.7.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 +30 -0
- package/docs/architecture/architecture.md +14 -14
- package/docs/plans/0110-agent-activity-tracker.md +297 -0
- package/docs/plans/0118-settings-manager-apply-methods.md +271 -0
- package/docs/retro/0109-extract-settings-manager.md +55 -0
- package/docs/retro/0118-settings-manager-apply-methods.md +40 -0
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/notification.ts +3 -3
- package/src/runtime.ts +3 -2
- package/src/settings.ts +31 -1
- package/src/tools/agent-tool.ts +7 -20
- package/src/ui/agent-activity-tracker.ts +108 -0
- package/src/ui/agent-menu.ts +15 -24
- package/src/ui/agent-widget.ts +4 -17
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +16 -23
|
@@ -0,0 +1,108 @@
|
|
|
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 { addUsage, type LifetimeUsage, type SessionLike } from "../usage.js";
|
|
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
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Per-agent live activity state with explicit transition methods and read-only accessors. */
|
|
18
|
+
export class AgentActivityTracker {
|
|
19
|
+
private _activeTools = new Map<string, string>();
|
|
20
|
+
private _toolKeySeq = 0;
|
|
21
|
+
private _toolUses = 0;
|
|
22
|
+
private _responseText = "";
|
|
23
|
+
private _session: SessionLike | undefined = undefined;
|
|
24
|
+
private _turnCount = 1;
|
|
25
|
+
private _lifetimeUsage: LifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
|
|
26
|
+
|
|
27
|
+
constructor(private readonly _maxTurns?: number) {}
|
|
28
|
+
|
|
29
|
+
// ── Transition methods (write surface) ──────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Record that a tool has started executing. */
|
|
32
|
+
onToolStart(toolName: string): void {
|
|
33
|
+
this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Record that a tool has finished executing; increments toolUses. No-op when no matching tool is active. */
|
|
37
|
+
onToolEnd(toolName: string): void {
|
|
38
|
+
for (const [key, name] of this._activeTools) {
|
|
39
|
+
if (name === toolName) {
|
|
40
|
+
this._activeTools.delete(key);
|
|
41
|
+
this._toolUses++;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reset the current response text (called at the start of each assistant message). */
|
|
48
|
+
onMessageStart(): void {
|
|
49
|
+
this._responseText = "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Append a text delta to the current response text. */
|
|
53
|
+
onMessageUpdate(delta: string): void {
|
|
54
|
+
this._responseText += delta;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Record that a turn has ended; increments turnCount. */
|
|
58
|
+
onTurnEnd(): void {
|
|
59
|
+
this._turnCount++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Accumulate a usage delta into the lifetime usage totals. */
|
|
63
|
+
onUsageUpdate(delta: UsageDelta): void {
|
|
64
|
+
addUsage(this._lifetimeUsage, delta);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Bind the session reference (called once when the agent session is created). */
|
|
68
|
+
setSession(session: SessionLike): void {
|
|
69
|
+
this._session = session;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Read-only accessors ──────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** Currently-active tools: key → tool name. Multiple entries for concurrent same-name tools. */
|
|
75
|
+
get activeTools(): ReadonlyMap<string, string> {
|
|
76
|
+
return this._activeTools;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Total completed tool invocations. */
|
|
80
|
+
get toolUses(): number {
|
|
81
|
+
return this._toolUses;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The agent's latest partial response text (reset at each message start). */
|
|
85
|
+
get responseText(): string {
|
|
86
|
+
return this._responseText;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The active SDK session, or undefined before the first session is created. */
|
|
90
|
+
get session(): SessionLike | undefined {
|
|
91
|
+
return this._session;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Current turn count (starts at 1). */
|
|
95
|
+
get turnCount(): number {
|
|
96
|
+
return this._turnCount;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Effective max turns for this agent, or undefined for unlimited. */
|
|
100
|
+
get maxTurns(): number | undefined {
|
|
101
|
+
return this._maxTurns;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Accumulated lifetime token usage (survives compaction). */
|
|
105
|
+
get lifetimeUsage(): Readonly<LifetimeUsage> {
|
|
106
|
+
return this._lifetimeUsage;
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "../agent-types.js";
|
|
10
10
|
import type { ModelRegistry } from "../model-resolver.js";
|
|
11
11
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
12
|
-
import type {
|
|
12
|
+
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
13
13
|
import { formatDuration, getDisplayName } from "./agent-widget.js";
|
|
14
14
|
|
|
15
15
|
// ---- Deps interface ----
|
|
@@ -20,22 +20,22 @@ export interface AgentMenuManager {
|
|
|
20
20
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
21
21
|
/** Used by generate wizard to spawn an agent that writes the .md file. */
|
|
22
22
|
spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
|
|
23
|
-
/** Drain the concurrency queue after maxConcurrent has been updated on SettingsManager. */
|
|
24
|
-
notifyConcurrencyChanged: () => void;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/** Narrow settings interface required by the agent menu. */
|
|
28
26
|
export interface AgentMenuSettings {
|
|
29
|
-
maxConcurrent: number;
|
|
30
|
-
defaultMaxTurns: number | undefined;
|
|
31
|
-
graceTurns: number;
|
|
32
|
-
|
|
27
|
+
readonly maxConcurrent: number;
|
|
28
|
+
readonly defaultMaxTurns: number | undefined;
|
|
29
|
+
readonly graceTurns: number;
|
|
30
|
+
applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" };
|
|
31
|
+
applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" };
|
|
32
|
+
applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface AgentMenuDeps {
|
|
36
36
|
manager: AgentMenuManager;
|
|
37
37
|
registry: AgentTypeRegistry;
|
|
38
|
-
agentActivity: Map<string,
|
|
38
|
+
agentActivity: Map<string, AgentActivityTracker>;
|
|
39
39
|
/** Resolve model label for a given agent type + registry. */
|
|
40
40
|
getModelLabel: (type: string, registry?: ModelRegistry) => string;
|
|
41
41
|
/** Settings manager — owns in-memory values and persistence. */
|
|
@@ -617,9 +617,8 @@ ${systemPrompt}
|
|
|
617
617
|
if (val) {
|
|
618
618
|
const n = parseInt(val, 10);
|
|
619
619
|
if (n >= 1) {
|
|
620
|
-
deps.settings.
|
|
621
|
-
|
|
622
|
-
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
620
|
+
const toast = deps.settings.applyMaxConcurrent(n);
|
|
621
|
+
ctx.ui.notify(toast.message, toast.level);
|
|
623
622
|
} else {
|
|
624
623
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
625
624
|
}
|
|
@@ -631,12 +630,9 @@ ${systemPrompt}
|
|
|
631
630
|
);
|
|
632
631
|
if (val) {
|
|
633
632
|
const n = parseInt(val, 10);
|
|
634
|
-
if (n
|
|
635
|
-
deps.settings.
|
|
636
|
-
|
|
637
|
-
} else if (n >= 1) {
|
|
638
|
-
deps.settings.defaultMaxTurns = n;
|
|
639
|
-
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
633
|
+
if (n >= 0) {
|
|
634
|
+
const toast = deps.settings.applyDefaultMaxTurns(n);
|
|
635
|
+
ctx.ui.notify(toast.message, toast.level);
|
|
640
636
|
} else {
|
|
641
637
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
642
638
|
}
|
|
@@ -649,8 +645,8 @@ ${systemPrompt}
|
|
|
649
645
|
if (val) {
|
|
650
646
|
const n = parseInt(val, 10);
|
|
651
647
|
if (n >= 1) {
|
|
652
|
-
deps.settings.
|
|
653
|
-
|
|
648
|
+
const toast = deps.settings.applyGraceTurns(n);
|
|
649
|
+
ctx.ui.notify(toast.message, toast.level);
|
|
654
650
|
} else {
|
|
655
651
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
656
652
|
}
|
|
@@ -658,11 +654,6 @@ ${systemPrompt}
|
|
|
658
654
|
}
|
|
659
655
|
}
|
|
660
656
|
|
|
661
|
-
function notifyApplied(ctx: ExtensionContext, successMsg: string) {
|
|
662
|
-
const { message, level } = deps.settings.saveAndNotify(successMsg);
|
|
663
|
-
ctx.ui.notify(message, level as "info" | "warning" | "error");
|
|
664
|
-
}
|
|
665
|
-
|
|
666
657
|
// Return the handler function
|
|
667
658
|
return async (ctx: ExtensionContext) => {
|
|
668
659
|
await showAgentsMenu(ctx);
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
10
|
import { type AgentConfigLookup, AgentTypeRegistry } from "../agent-types.js";
|
|
11
11
|
import type { AgentInvocation, SubagentType } from "../types.js";
|
|
12
|
-
import { getLifetimeTotal, getSessionContextPercent
|
|
12
|
+
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
13
|
+
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
13
14
|
|
|
14
15
|
// ---- Constants ----
|
|
15
16
|
|
|
@@ -49,20 +50,6 @@ export type UICtx = {
|
|
|
49
50
|
): void;
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
/** Per-agent live activity state. */
|
|
53
|
-
export interface AgentActivity {
|
|
54
|
-
activeTools: Map<string, string>;
|
|
55
|
-
toolUses: number;
|
|
56
|
-
responseText: string;
|
|
57
|
-
session?: SessionLike;
|
|
58
|
-
/** Current turn count. */
|
|
59
|
-
turnCount: number;
|
|
60
|
-
/** Effective max turns for this agent (undefined = unlimited). */
|
|
61
|
-
maxTurns?: number;
|
|
62
|
-
/** Lifetime usage breakdown — see LifetimeUsage docs. */
|
|
63
|
-
lifetimeUsage: LifetimeUsage;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
53
|
/** Metadata attached to Agent tool results for custom rendering. */
|
|
67
54
|
export interface AgentDetails {
|
|
68
55
|
displayName: string;
|
|
@@ -177,7 +164,7 @@ function truncateLine(text: string, len = 60): string {
|
|
|
177
164
|
}
|
|
178
165
|
|
|
179
166
|
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
180
|
-
export function describeActivity(activeTools:
|
|
167
|
+
export function describeActivity(activeTools: ReadonlyMap<string, string>, responseText?: string): string {
|
|
181
168
|
if (activeTools.size > 0) {
|
|
182
169
|
const groups = new Map<string, number>();
|
|
183
170
|
for (const toolName of activeTools.values()) {
|
|
@@ -224,7 +211,7 @@ export class AgentWidget {
|
|
|
224
211
|
|
|
225
212
|
constructor(
|
|
226
213
|
private manager: AgentManager,
|
|
227
|
-
private agentActivity: Map<string,
|
|
214
|
+
private agentActivity: Map<string, AgentActivityTracker>,
|
|
228
215
|
private registry: AgentTypeRegistry,
|
|
229
216
|
) {}
|
|
230
217
|
|
|
@@ -11,8 +11,8 @@ import type { AgentConfigLookup } from "../agent-types.js";
|
|
|
11
11
|
import { extractText } from "../context.js";
|
|
12
12
|
import type { AgentRecord } from "../types.js";
|
|
13
13
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
14
|
-
import type {
|
|
15
|
-
import {
|
|
14
|
+
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
15
|
+
import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./agent-widget.js";
|
|
16
16
|
|
|
17
17
|
/** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
18
18
|
const CHROME_LINES_BASE = 6;
|
|
@@ -31,7 +31,7 @@ export class ConversationViewer implements Component {
|
|
|
31
31
|
private tui: TUI,
|
|
32
32
|
private session: AgentSession,
|
|
33
33
|
private record: AgentRecord,
|
|
34
|
-
private activity:
|
|
34
|
+
private activity: AgentActivityTracker | undefined,
|
|
35
35
|
private theme: Theme,
|
|
36
36
|
private done: (result: undefined) => void,
|
|
37
37
|
private registry: AgentConfigLookup,
|
package/src/ui/ui-observer.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ui-observer.ts — Subscribes to session events and updates
|
|
2
|
+
* ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
|
|
3
3
|
*
|
|
4
4
|
* Replaces the callback-based createActivityTracker pattern with a direct
|
|
5
5
|
* session subscription for streaming UI state (active tools, response text,
|
|
6
6
|
* turn count, lifetime usage).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import type { AgentActivity } from "./agent-widget.js";
|
|
9
|
+
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
11
10
|
|
|
12
11
|
/** Narrow session interface — only the subscribe method needed by the observer. */
|
|
13
12
|
interface SubscribableSession {
|
|
@@ -15,15 +14,15 @@ interface SubscribableSession {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
* Subscribe to session events and stream UI state into an
|
|
17
|
+
* Subscribe to session events and stream UI state into an AgentActivityTracker.
|
|
19
18
|
*
|
|
20
19
|
* Handles:
|
|
21
|
-
* - `tool_execution_start` →
|
|
22
|
-
* - `tool_execution_end` →
|
|
23
|
-
* - `message_start` →
|
|
24
|
-
* - `message_update` (text_delta) →
|
|
25
|
-
* - `turn_end` → `
|
|
26
|
-
* - `message_end` (assistant, with usage) → `
|
|
20
|
+
* - `tool_execution_start` → `tracker.onToolStart(name)`
|
|
21
|
+
* - `tool_execution_end` → `tracker.onToolEnd(name)`
|
|
22
|
+
* - `message_start` → `tracker.onMessageStart()`
|
|
23
|
+
* - `message_update` (text_delta) → `tracker.onMessageUpdate(delta)`
|
|
24
|
+
* - `turn_end` → `tracker.onTurnEnd()`
|
|
25
|
+
* - `message_end` (assistant, with usage) → `tracker.onUsageUpdate(usage)`
|
|
27
26
|
*
|
|
28
27
|
* Calls `onUpdate?.()` after each state mutation to trigger re-renders.
|
|
29
28
|
*
|
|
@@ -31,47 +30,41 @@ interface SubscribableSession {
|
|
|
31
30
|
*/
|
|
32
31
|
export function subscribeUIObserver(
|
|
33
32
|
session: SubscribableSession,
|
|
34
|
-
|
|
33
|
+
tracker: AgentActivityTracker,
|
|
35
34
|
onUpdate?: () => void,
|
|
36
35
|
): () => void {
|
|
37
36
|
return session.subscribe((event: any) => {
|
|
38
37
|
if (event.type === "tool_execution_start") {
|
|
39
|
-
|
|
38
|
+
tracker.onToolStart(event.toolName);
|
|
40
39
|
onUpdate?.();
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
if (event.type === "tool_execution_end") {
|
|
44
|
-
|
|
45
|
-
if (name === event.toolName) {
|
|
46
|
-
state.activeTools.delete(key);
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
state.toolUses++;
|
|
43
|
+
tracker.onToolEnd(event.toolName);
|
|
51
44
|
onUpdate?.();
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
if (event.type === "message_start") {
|
|
55
|
-
|
|
48
|
+
tracker.onMessageStart();
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
if (
|
|
59
52
|
event.type === "message_update" &&
|
|
60
53
|
event.assistantMessageEvent?.type === "text_delta"
|
|
61
54
|
) {
|
|
62
|
-
|
|
55
|
+
tracker.onMessageUpdate(event.assistantMessageEvent.delta);
|
|
63
56
|
onUpdate?.();
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
if (event.type === "turn_end") {
|
|
67
|
-
|
|
60
|
+
tracker.onTurnEnd();
|
|
68
61
|
onUpdate?.();
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
72
65
|
const u = event.message.usage;
|
|
73
66
|
if (u) {
|
|
74
|
-
|
|
67
|
+
tracker.onUsageUpdate({
|
|
75
68
|
input: u.input ?? 0,
|
|
76
69
|
output: u.output ?? 0,
|
|
77
70
|
cacheWrite: u.cacheWrite ?? 0,
|