@gotgenes/pi-subagents 6.6.0 → 6.8.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 +36 -34
- package/docs/plans/0110-agent-activity-tracker.md +297 -0
- package/docs/plans/0111-split-agent-record-lifecycle.md +582 -0
- package/docs/retro/0110-agent-activity-tracker.md +44 -0
- package/docs/retro/0118-settings-manager-apply-methods.md +40 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +41 -21
- package/src/agent-record.ts +45 -34
- package/src/execution-state.ts +17 -0
- package/src/index.ts +2 -1
- package/src/notification-state.ts +27 -0
- package/src/notification.ts +12 -9
- package/src/record-observer.ts +6 -7
- package/src/runtime.ts +3 -2
- package/src/service-adapter.ts +8 -7
- package/src/tools/agent-tool.ts +13 -24
- package/src/tools/get-result-tool.ts +7 -5
- package/src/tools/steer-tool.ts +8 -6
- package/src/ui/agent-activity-tracker.ts +108 -0
- package/src/ui/agent-menu.ts +4 -4
- package/src/ui/agent-widget.ts +4 -17
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +16 -23
- package/src/worktree-state.ts +35 -0
package/src/tools/agent-tool.ts
CHANGED
|
@@ -7,9 +7,10 @@ import { AgentTypeRegistry } from "../agent-types.js";
|
|
|
7
7
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
8
8
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
9
9
|
|
|
10
|
+
import { NotificationState } from "../notification-state.js";
|
|
10
11
|
import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
|
|
12
|
+
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
11
13
|
import {
|
|
12
|
-
type AgentActivity,
|
|
13
14
|
type AgentDetails,
|
|
14
15
|
buildInvocationTags,
|
|
15
16
|
describeActivity,
|
|
@@ -26,19 +27,6 @@ import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
|
26
27
|
|
|
27
28
|
// ---- Agent-tool-specific helpers ----
|
|
28
29
|
|
|
29
|
-
/** Create a fresh AgentActivity state for tracking UI progress. */
|
|
30
|
-
function createAgentActivity(maxTurns?: number): AgentActivity {
|
|
31
|
-
return {
|
|
32
|
-
activeTools: new Map(),
|
|
33
|
-
toolUses: 0,
|
|
34
|
-
turnCount: 1,
|
|
35
|
-
maxTurns,
|
|
36
|
-
responseText: "",
|
|
37
|
-
session: undefined,
|
|
38
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
30
|
/** Parenthetical status note for completed agent result text. */
|
|
43
31
|
export function getStatusNote(status: string): string {
|
|
44
32
|
switch (status) {
|
|
@@ -66,7 +54,7 @@ export function buildDetails(
|
|
|
66
54
|
session?: any;
|
|
67
55
|
lifetimeUsage: LifetimeUsage;
|
|
68
56
|
},
|
|
69
|
-
activity?:
|
|
57
|
+
activity?: AgentActivityTracker,
|
|
70
58
|
overrides?: Partial<AgentDetails>,
|
|
71
59
|
): AgentDetails {
|
|
72
60
|
return {
|
|
@@ -106,7 +94,7 @@ export interface AgentToolWidget {
|
|
|
106
94
|
export interface AgentToolDeps {
|
|
107
95
|
manager: AgentToolManager;
|
|
108
96
|
widget: AgentToolWidget;
|
|
109
|
-
agentActivity: Map<string,
|
|
97
|
+
agentActivity: Map<string, AgentActivityTracker>;
|
|
110
98
|
emitEvent: (name: string, data: unknown) => void;
|
|
111
99
|
registry: AgentTypeRegistry;
|
|
112
100
|
typeListText: string;
|
|
@@ -394,7 +382,7 @@ Guidelines:
|
|
|
394
382
|
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
395
383
|
);
|
|
396
384
|
}
|
|
397
|
-
if (!existing.session) {
|
|
385
|
+
if (!existing.execution?.session) {
|
|
398
386
|
return textResult(
|
|
399
387
|
`Agent "${params.resume}" has no active session to resume.`,
|
|
400
388
|
);
|
|
@@ -415,7 +403,7 @@ Guidelines:
|
|
|
415
403
|
|
|
416
404
|
// Background execution
|
|
417
405
|
if (runInBackground) {
|
|
418
|
-
const bgState =
|
|
406
|
+
const bgState = new AgentActivityTracker(effectiveMaxTurns);
|
|
419
407
|
|
|
420
408
|
let id: string;
|
|
421
409
|
|
|
@@ -433,7 +421,7 @@ Guidelines:
|
|
|
433
421
|
isolation,
|
|
434
422
|
invocation: agentInvocation,
|
|
435
423
|
onSessionCreated: (session: any) => {
|
|
436
|
-
bgState.session
|
|
424
|
+
bgState.setSession(session);
|
|
437
425
|
subscribeUIObserver(session, bgState);
|
|
438
426
|
},
|
|
439
427
|
});
|
|
@@ -443,7 +431,8 @@ Guidelines:
|
|
|
443
431
|
|
|
444
432
|
const record = deps.manager.getRecord(id);
|
|
445
433
|
if (record) {
|
|
446
|
-
|
|
434
|
+
// Born complete: notification-state object owns toolCallId + resultConsumed.
|
|
435
|
+
record.notification = new NotificationState(toolCallId);
|
|
447
436
|
}
|
|
448
437
|
|
|
449
438
|
deps.agentActivity.set(id, bgState);
|
|
@@ -464,7 +453,7 @@ Guidelines:
|
|
|
464
453
|
`Agent ID: ${id}\n` +
|
|
465
454
|
`Type: ${displayName}\n` +
|
|
466
455
|
`Description: ${params.description}\n` +
|
|
467
|
-
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
456
|
+
(record?.execution?.outputFile ? `Output file: ${record.execution.outputFile}\n` : "") +
|
|
468
457
|
(isQueued
|
|
469
458
|
? `Position: queued (max ${deps.manager.getMaxConcurrent()} concurrent)\n`
|
|
470
459
|
: "") +
|
|
@@ -487,7 +476,7 @@ Guidelines:
|
|
|
487
476
|
const startedAt = Date.now();
|
|
488
477
|
let fgId: string | undefined;
|
|
489
478
|
|
|
490
|
-
const fgState =
|
|
479
|
+
const fgState = new AgentActivityTracker(effectiveMaxTurns);
|
|
491
480
|
let unsubUI: (() => void) | undefined;
|
|
492
481
|
|
|
493
482
|
const streamUpdate = () => {
|
|
@@ -535,10 +524,10 @@ Guidelines:
|
|
|
535
524
|
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
536
525
|
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
537
526
|
onSessionCreated: (session: any) => {
|
|
538
|
-
fgState.session
|
|
527
|
+
fgState.setSession(session);
|
|
539
528
|
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
540
529
|
for (const a of deps.manager.listAgents()) {
|
|
541
|
-
if (a.session === session) {
|
|
530
|
+
if (a.execution?.session === session) {
|
|
542
531
|
fgId = a.id;
|
|
543
532
|
deps.agentActivity.set(a.id, fgState);
|
|
544
533
|
deps.widget.ensureTimer();
|
|
@@ -54,7 +54,9 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
54
54
|
// (attached earlier at spawn time) and always runs before this await resumes.
|
|
55
55
|
// Setting the flag here prevents a redundant follow-up notification.
|
|
56
56
|
if (params.wait && record.status === "running" && record.promise) {
|
|
57
|
-
|
|
57
|
+
// Pre-mark consumed BEFORE awaiting — onComplete fires inside .then() and
|
|
58
|
+
// always runs before this await resumes. Prevents a redundant notification.
|
|
59
|
+
record.notification?.markConsumed();
|
|
58
60
|
deps.cancelNudge(params.agent_id);
|
|
59
61
|
await record.promise;
|
|
60
62
|
}
|
|
@@ -62,7 +64,7 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
62
64
|
const displayName = getDisplayName(record.type, deps.registry);
|
|
63
65
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
64
66
|
const tokens = formatLifetimeTokens(record);
|
|
65
|
-
const contextPercent = getSessionContextPercent(record.session);
|
|
67
|
+
const contextPercent = getSessionContextPercent(record.execution?.session);
|
|
66
68
|
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
67
69
|
if (tokens) statsParts.push(tokens);
|
|
68
70
|
if (contextPercent !== null) statsParts.push(`Context: ${Math.round(contextPercent)}%`);
|
|
@@ -84,13 +86,13 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
84
86
|
|
|
85
87
|
// Mark result as consumed — suppresses the completion notification
|
|
86
88
|
if (record.status !== "running" && record.status !== "queued") {
|
|
87
|
-
record.
|
|
89
|
+
record.notification?.markConsumed();
|
|
88
90
|
deps.cancelNudge(params.agent_id);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
// Verbose: include full conversation
|
|
92
|
-
if (params.verbose && record.session) {
|
|
93
|
-
const conversation = deps.getConversation(record.session);
|
|
94
|
+
if (params.verbose && record.execution?.session) {
|
|
95
|
+
const conversation = deps.getConversation(record.execution.session);
|
|
94
96
|
if (conversation) {
|
|
95
97
|
output += `\n\n--- Agent Conversation ---\n${conversation}`;
|
|
96
98
|
}
|
package/src/tools/steer-tool.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SteerToolDeps {
|
|
|
9
9
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
10
10
|
emitEvent: (name: string, data: unknown) => void;
|
|
11
11
|
steerAgent: (session: AgentSession, message: string) => Promise<void>;
|
|
12
|
+
/** Buffer a steer for an agent whose session isn't ready yet. */
|
|
13
|
+
queueSteer: (id: string, message: string) => boolean;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/** Create the steer_subagent tool definition (without Pi SDK wrapper). */
|
|
@@ -46,10 +48,10 @@ export function createSteerTool(deps: SteerToolDeps) {
|
|
|
46
48
|
`Agent "${params.agent_id}" is not running (status: ${record.status}). Cannot steer a non-running agent.`,
|
|
47
49
|
);
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
record.
|
|
51
|
+
const session = record.execution?.session;
|
|
52
|
+
if (!session) {
|
|
53
|
+
// Session not ready yet — queue via manager for delivery once initialized
|
|
54
|
+
deps.queueSteer(record.id, params.message);
|
|
53
55
|
deps.emitEvent("subagents:steered", { id: record.id, message: params.message });
|
|
54
56
|
return textResult(
|
|
55
57
|
`Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
|
|
@@ -57,10 +59,10 @@ export function createSteerTool(deps: SteerToolDeps) {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
try {
|
|
60
|
-
await deps.steerAgent(
|
|
62
|
+
await deps.steerAgent(session, params.message);
|
|
61
63
|
deps.emitEvent("subagents:steered", { id: record.id, message: params.message });
|
|
62
64
|
const tokens = formatLifetimeTokens(record);
|
|
63
|
-
const contextPercent = getSessionContextPercent(
|
|
65
|
+
const contextPercent = getSessionContextPercent(session);
|
|
64
66
|
const stateParts: string[] = [];
|
|
65
67
|
if (tokens) stateParts.push(tokens);
|
|
66
68
|
stateParts.push(`${record.toolUses} tool ${record.toolUses === 1 ? "use" : "uses"}`);
|
|
@@ -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 ----
|
|
@@ -35,7 +35,7 @@ export interface AgentMenuSettings {
|
|
|
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. */
|
|
@@ -197,7 +197,8 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
|
|
200
|
-
|
|
200
|
+
const session = record.execution?.session;
|
|
201
|
+
if (!session) {
|
|
201
202
|
ctx.ui.notify(
|
|
202
203
|
`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
|
|
203
204
|
"info",
|
|
@@ -208,7 +209,6 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
208
209
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
|
|
209
210
|
"./conversation-viewer.js"
|
|
210
211
|
);
|
|
211
|
-
const session = record.session;
|
|
212
212
|
const activity = deps.agentActivity.get(record.id);
|
|
213
213
|
|
|
214
214
|
await ctx.ui.custom<undefined>(
|
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,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-state.ts — WorktreeState: lifecycle-phase object for worktree-isolated agents.
|
|
3
|
+
*
|
|
4
|
+
* Constructed once when the worktree is set up (before the run begins).
|
|
5
|
+
* Only exists for agents with isolation: "worktree".
|
|
6
|
+
* cleanupResult is recorded once at completion or error — it is not set at construction.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { WorktreeCleanupResult, WorktreeInfo } from "./worktree.js";
|
|
10
|
+
|
|
11
|
+
export type { WorktreeCleanupResult, WorktreeInfo };
|
|
12
|
+
|
|
13
|
+
export class WorktreeState {
|
|
14
|
+
/** Absolute path to the worktree directory. */
|
|
15
|
+
readonly path: string;
|
|
16
|
+
/** Branch name created for this worktree. */
|
|
17
|
+
readonly branch: string;
|
|
18
|
+
|
|
19
|
+
private _cleanupResult?: WorktreeCleanupResult;
|
|
20
|
+
|
|
21
|
+
constructor(info: WorktreeInfo) {
|
|
22
|
+
this.path = info.path;
|
|
23
|
+
this.branch = info.branch;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Result of the worktree cleanup — undefined until recordCleanup is called. */
|
|
27
|
+
get cleanupResult(): WorktreeCleanupResult | undefined {
|
|
28
|
+
return this._cleanupResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Record the cleanup result. Called once on agent completion or error. */
|
|
32
|
+
recordCleanup(result: WorktreeCleanupResult): void {
|
|
33
|
+
this._cleanupResult = result;
|
|
34
|
+
}
|
|
35
|
+
}
|