@gotgenes/pi-subagents 6.1.0 → 6.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 +28 -0
- package/docs/architecture/architecture.md +5 -3
- package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +488 -0
- package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +454 -0
- package/docs/retro/0098-extract-agent-record-state-machine.md +46 -0
- package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +37 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +30 -50
- package/src/agent-runner.ts +18 -135
- package/src/env.ts +4 -5
- package/src/index.ts +4 -3
- package/src/parent-snapshot.ts +27 -0
- package/src/record-observer.ts +60 -0
- package/src/service-adapter.ts +2 -2
- package/src/tools/agent-tool.ts +27 -64
- package/src/types.ts +30 -0
- package/src/ui/agent-menu.ts +2 -3
- package/src/ui/ui-observer.ts +83 -0
package/src/tools/agent-tool.ts
CHANGED
|
@@ -20,17 +20,15 @@ import {
|
|
|
20
20
|
SPINNER,
|
|
21
21
|
type UICtx,
|
|
22
22
|
} from "../ui/agent-widget.js";
|
|
23
|
-
import {
|
|
23
|
+
import { subscribeUIObserver } from "../ui/ui-observer.js";
|
|
24
|
+
import type { LifetimeUsage } from "../usage.js";
|
|
24
25
|
import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
25
26
|
|
|
26
27
|
// ---- Agent-tool-specific helpers ----
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*/
|
|
32
|
-
export function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
33
|
-
const state: AgentActivity = {
|
|
29
|
+
/** Create a fresh AgentActivity state for tracking UI progress. */
|
|
30
|
+
function createAgentActivity(maxTurns?: number): AgentActivity {
|
|
31
|
+
return {
|
|
34
32
|
activeTools: new Map(),
|
|
35
33
|
toolUses: 0,
|
|
36
34
|
turnCount: 1,
|
|
@@ -39,40 +37,6 @@ export function createActivityTracker(maxTurns?: number, onStreamUpdate?: () =>
|
|
|
39
37
|
session: undefined,
|
|
40
38
|
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
41
39
|
};
|
|
42
|
-
|
|
43
|
-
const callbacks = {
|
|
44
|
-
onToolActivity: (activity: { type: "start" | "end"; toolName: string }) => {
|
|
45
|
-
if (activity.type === "start") {
|
|
46
|
-
state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
|
|
47
|
-
} else {
|
|
48
|
-
for (const [key, name] of state.activeTools) {
|
|
49
|
-
if (name === activity.toolName) {
|
|
50
|
-
state.activeTools.delete(key);
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
state.toolUses++;
|
|
55
|
-
}
|
|
56
|
-
onStreamUpdate?.();
|
|
57
|
-
},
|
|
58
|
-
onTextDelta: (_delta: string, fullText: string) => {
|
|
59
|
-
state.responseText = fullText;
|
|
60
|
-
onStreamUpdate?.();
|
|
61
|
-
},
|
|
62
|
-
onTurnEnd: (turnCount: number) => {
|
|
63
|
-
state.turnCount = turnCount;
|
|
64
|
-
onStreamUpdate?.();
|
|
65
|
-
},
|
|
66
|
-
onSessionCreated: (session: any) => {
|
|
67
|
-
state.session = session;
|
|
68
|
-
},
|
|
69
|
-
onAssistantUsage: (usage: { input: number; output: number; cacheWrite: number }) => {
|
|
70
|
-
addUsage(state.lifetimeUsage, usage);
|
|
71
|
-
onStreamUpdate?.();
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return { state, callbacks };
|
|
76
40
|
}
|
|
77
41
|
|
|
78
42
|
/** Parenthetical status note for completed agent result text. */
|
|
@@ -451,8 +415,7 @@ Guidelines:
|
|
|
451
415
|
|
|
452
416
|
// Background execution
|
|
453
417
|
if (runInBackground) {
|
|
454
|
-
const
|
|
455
|
-
createActivityTracker(effectiveMaxTurns);
|
|
418
|
+
const bgState = createAgentActivity(effectiveMaxTurns);
|
|
456
419
|
|
|
457
420
|
let id: string;
|
|
458
421
|
|
|
@@ -469,7 +432,10 @@ Guidelines:
|
|
|
469
432
|
isBackground: true,
|
|
470
433
|
isolation,
|
|
471
434
|
invocation: agentInvocation,
|
|
472
|
-
|
|
435
|
+
onSessionCreated: (session: any) => {
|
|
436
|
+
bgState.session = session;
|
|
437
|
+
subscribeUIObserver(session, bgState);
|
|
438
|
+
},
|
|
473
439
|
});
|
|
474
440
|
} catch (err) {
|
|
475
441
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
@@ -521,6 +487,9 @@ Guidelines:
|
|
|
521
487
|
const startedAt = Date.now();
|
|
522
488
|
let fgId: string | undefined;
|
|
523
489
|
|
|
490
|
+
const fgState = createAgentActivity(effectiveMaxTurns);
|
|
491
|
+
let unsubUI: (() => void) | undefined;
|
|
492
|
+
|
|
524
493
|
const streamUpdate = () => {
|
|
525
494
|
const details: AgentDetails = {
|
|
526
495
|
...detailBase,
|
|
@@ -539,25 +508,6 @@ Guidelines:
|
|
|
539
508
|
});
|
|
540
509
|
};
|
|
541
510
|
|
|
542
|
-
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(
|
|
543
|
-
effectiveMaxTurns,
|
|
544
|
-
streamUpdate,
|
|
545
|
-
);
|
|
546
|
-
|
|
547
|
-
// Wire session creation to register in widget
|
|
548
|
-
const origOnSession = fgCallbacks.onSessionCreated;
|
|
549
|
-
fgCallbacks.onSessionCreated = (session: any) => {
|
|
550
|
-
origOnSession(session);
|
|
551
|
-
for (const a of deps.manager.listAgents()) {
|
|
552
|
-
if (a.session === session) {
|
|
553
|
-
fgId = a.id;
|
|
554
|
-
deps.agentActivity.set(a.id, fgState);
|
|
555
|
-
deps.widget.ensureTimer();
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
|
|
561
511
|
// Animate spinner at ~80ms (smooth rotation through 10 braille frames)
|
|
562
512
|
const spinnerInterval = setInterval(() => {
|
|
563
513
|
spinnerFrame++;
|
|
@@ -584,15 +534,28 @@ Guidelines:
|
|
|
584
534
|
signal,
|
|
585
535
|
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
586
536
|
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
587
|
-
|
|
537
|
+
onSessionCreated: (session: any) => {
|
|
538
|
+
fgState.session = session;
|
|
539
|
+
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
540
|
+
for (const a of deps.manager.listAgents()) {
|
|
541
|
+
if (a.session === session) {
|
|
542
|
+
fgId = a.id;
|
|
543
|
+
deps.agentActivity.set(a.id, fgState);
|
|
544
|
+
deps.widget.ensureTimer();
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
588
549
|
},
|
|
589
550
|
);
|
|
590
551
|
} catch (err) {
|
|
591
552
|
clearInterval(spinnerInterval);
|
|
553
|
+
unsubUI?.();
|
|
592
554
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
593
555
|
}
|
|
594
556
|
|
|
595
557
|
clearInterval(spinnerInterval);
|
|
558
|
+
unsubUI?.();
|
|
596
559
|
|
|
597
560
|
// Clean up foreground agent from widget
|
|
598
561
|
if (fgId) {
|
package/src/types.ts
CHANGED
|
@@ -83,8 +83,38 @@ export interface NotificationDetails {
|
|
|
83
83
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Plain data snapshot of the parent session state captured at spawn time.
|
|
88
|
+
* Replaces live `ExtensionContext` references so queued agents don't read stale state.
|
|
89
|
+
*/
|
|
90
|
+
export interface ParentSnapshot {
|
|
91
|
+
/** Parent working directory. */
|
|
92
|
+
cwd: string;
|
|
93
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
94
|
+
systemPrompt: string;
|
|
95
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
96
|
+
model: unknown;
|
|
97
|
+
/** Model registry for resolving config.model strings and creating sessions. */
|
|
98
|
+
modelRegistry: {
|
|
99
|
+
find(provider: string, modelId: string): unknown;
|
|
100
|
+
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
101
|
+
};
|
|
102
|
+
/** Pre-built parent conversation text (when inheritContext was requested). */
|
|
103
|
+
parentContext?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
export interface EnvInfo {
|
|
87
107
|
isGitRepo: boolean;
|
|
88
108
|
branch: string;
|
|
89
109
|
platform: string;
|
|
90
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
|
|
114
|
+
* Matches the shape of `pi.exec()` without carrying an SDK dependency.
|
|
115
|
+
*/
|
|
116
|
+
export type ShellExec = (
|
|
117
|
+
command: string,
|
|
118
|
+
args: string[],
|
|
119
|
+
options?: { cwd?: string; timeout?: number },
|
|
120
|
+
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { SpawnOptions } from "../agent-manager.js";
|
|
6
6
|
import {
|
|
7
7
|
BUILTIN_TOOL_NAMES,
|
|
@@ -21,7 +21,7 @@ export interface AgentMenuManager {
|
|
|
21
21
|
listAgents: () => AgentRecord[];
|
|
22
22
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
23
23
|
/** Used by generate wizard to spawn an agent that writes the .md file. */
|
|
24
|
-
spawnAndWait: (
|
|
24
|
+
spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
|
|
25
25
|
getMaxConcurrent: () => number;
|
|
26
26
|
setMaxConcurrent: (n: number) => void;
|
|
27
27
|
}
|
|
@@ -487,7 +487,6 @@ Guidelines for choosing settings:
|
|
|
487
487
|
Write the file using the write tool. Only write the file, nothing else.`;
|
|
488
488
|
|
|
489
489
|
const record = await deps.manager.spawnAndWait(
|
|
490
|
-
null,
|
|
491
490
|
ctx,
|
|
492
491
|
"general-purpose",
|
|
493
492
|
generatePrompt,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-observer.ts — Subscribes to session events and updates AgentActivity 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 { addUsage } from "../usage.js";
|
|
10
|
+
import type { AgentActivity } from "./agent-widget.js";
|
|
11
|
+
|
|
12
|
+
/** Narrow session interface — only the subscribe method needed by the observer. */
|
|
13
|
+
interface SubscribableSession {
|
|
14
|
+
subscribe(fn: (event: any) => void): () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Subscribe to session events and stream UI state into an AgentActivity object.
|
|
19
|
+
*
|
|
20
|
+
* Handles:
|
|
21
|
+
* - `tool_execution_start` → add to `state.activeTools`
|
|
22
|
+
* - `tool_execution_end` → remove from `state.activeTools`, `state.toolUses++`
|
|
23
|
+
* - `message_start` → reset `state.responseText`
|
|
24
|
+
* - `message_update` (text_delta) → append to `state.responseText`
|
|
25
|
+
* - `turn_end` → `state.turnCount++`
|
|
26
|
+
* - `message_end` (assistant, with usage) → `addUsage(state.lifetimeUsage, …)`
|
|
27
|
+
*
|
|
28
|
+
* Calls `onUpdate?.()` after each state mutation to trigger re-renders.
|
|
29
|
+
*
|
|
30
|
+
* @returns An unsubscribe function.
|
|
31
|
+
*/
|
|
32
|
+
export function subscribeUIObserver(
|
|
33
|
+
session: SubscribableSession,
|
|
34
|
+
state: AgentActivity,
|
|
35
|
+
onUpdate?: () => void,
|
|
36
|
+
): () => void {
|
|
37
|
+
return session.subscribe((event: any) => {
|
|
38
|
+
if (event.type === "tool_execution_start") {
|
|
39
|
+
state.activeTools.set(event.toolName + "_" + Date.now(), event.toolName);
|
|
40
|
+
onUpdate?.();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (event.type === "tool_execution_end") {
|
|
44
|
+
for (const [key, name] of state.activeTools) {
|
|
45
|
+
if (name === event.toolName) {
|
|
46
|
+
state.activeTools.delete(key);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
state.toolUses++;
|
|
51
|
+
onUpdate?.();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (event.type === "message_start") {
|
|
55
|
+
state.responseText = "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
event.type === "message_update" &&
|
|
60
|
+
event.assistantMessageEvent?.type === "text_delta"
|
|
61
|
+
) {
|
|
62
|
+
state.responseText += event.assistantMessageEvent.delta;
|
|
63
|
+
onUpdate?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (event.type === "turn_end") {
|
|
67
|
+
state.turnCount++;
|
|
68
|
+
onUpdate?.();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
72
|
+
const u = event.message.usage;
|
|
73
|
+
if (u) {
|
|
74
|
+
addUsage(state.lifetimeUsage, {
|
|
75
|
+
input: u.input ?? 0,
|
|
76
|
+
output: u.output ?? 0,
|
|
77
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
78
|
+
});
|
|
79
|
+
onUpdate?.();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|