@gotgenes/pi-subagents 6.7.0 → 6.8.1

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.
@@ -31,7 +31,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
31
31
  const status = getStatusLabel(record.status, record.error);
32
32
  const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
33
33
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
34
- const contextPercent = getSessionContextPercent(record.session);
34
+ const contextPercent = getSessionContextPercent(record.execution?.session);
35
35
  const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
36
36
  const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
37
37
 
@@ -41,11 +41,13 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
41
41
  : record.result
42
42
  : "No output.";
43
43
 
44
+ const toolCallId = record.notification?.toolCallId;
45
+ const outputFile = record.execution?.outputFile;
44
46
  return [
45
47
  "<task-notification>",
46
48
  `<task-id>${record.id}</task-id>`,
47
- record.toolCallId ? `<tool-use-id>${escapeXml(record.toolCallId)}</tool-use-id>` : null,
48
- record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
49
+ toolCallId ? `<tool-use-id>${escapeXml(toolCallId)}</tool-use-id>` : null,
50
+ outputFile ? `<output-file>${escapeXml(outputFile)}</output-file>` : null,
49
51
  `<status>${escapeXml(status)}</status>`,
50
52
  `<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
51
53
  `<result>${escapeXml(resultPreview)}</result>`,
@@ -73,7 +75,7 @@ export function buildNotificationDetails(
73
75
  maxTurns: activity?.maxTurns,
74
76
  totalTokens,
75
77
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
76
- outputFile: record.outputFile,
78
+ outputFile: record.execution?.outputFile,
77
79
  error: record.error,
78
80
  resultPreview: record.result
79
81
  ? record.result.length > resultMaxLen
@@ -154,10 +156,11 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
154
156
  }
155
157
 
156
158
  function emitIndividualNudge(record: AgentRecord) {
157
- if (record.resultConsumed) return;
159
+ if (record.notification?.resultConsumed) return;
158
160
 
159
161
  const notification = formatTaskNotification(record, 500);
160
- const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : "";
162
+ const outputFile = record.execution?.outputFile;
163
+ const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
161
164
 
162
165
  deps.sendMessage(
163
166
  {
@@ -7,7 +7,6 @@
7
7
 
8
8
  import type { CompactionInfo } from "./agent-manager.js";
9
9
  import type { AgentRecord } from "./agent-record.js";
10
- import { addUsage } from "./usage.js";
11
10
 
12
11
  /** Narrow session interface — only the subscribe method needed by the observer. */
13
12
  interface SubscribableSession {
@@ -22,9 +21,9 @@ export interface RecordObserverOptions {
22
21
  * Subscribe to session events and accumulate stats on the agent record.
23
22
  *
24
23
  * Handles:
25
- * - `tool_execution_end` → `record.toolUses++`
26
- * - `message_end` (assistant, with usage) → `addUsage(record.lifetimeUsage, …)`
27
- * - `compaction_end` (not aborted) → `record.compactionCount++`, call `onCompact`
24
+ * - `tool_execution_end` → `record.incrementToolUses()`
25
+ * - `message_end` (assistant, with usage) → `record.addUsage(…)`
26
+ * - `compaction_end` (not aborted) → `record.incrementCompactions()`, call `onCompact`
28
27
  *
29
28
  * @returns An unsubscribe function.
30
29
  */
@@ -35,13 +34,13 @@ export function subscribeRecordObserver(
35
34
  ): () => void {
36
35
  return session.subscribe((event: any) => {
37
36
  if (event.type === "tool_execution_end") {
38
- record.toolUses++;
37
+ record.incrementToolUses();
39
38
  }
40
39
 
41
40
  if (event.type === "message_end" && event.message?.role === "assistant") {
42
41
  const u = event.message.usage;
43
42
  if (u) {
44
- addUsage(record.lifetimeUsage, {
43
+ record.addUsage({
45
44
  input: u.input ?? 0,
46
45
  output: u.output ?? 0,
47
46
  cacheWrite: u.cacheWrite ?? 0,
@@ -50,7 +49,7 @@ export function subscribeRecordObserver(
50
49
  }
51
50
 
52
51
  if (event.type === "compaction_end" && !event.aborted && event.result) {
53
- record.compactionCount++;
52
+ record.incrementCompactions();
54
53
  options?.onCompact?.(record, {
55
54
  reason: event.reason,
56
55
  tokensBefore: event.result.tokensBefore,
@@ -17,6 +17,7 @@ export interface AgentManagerLike {
17
17
  abort(id: string): boolean;
18
18
  waitForAll(): Promise<void>;
19
19
  hasRunning(): boolean;
20
+ queueSteer(id: string, message: string): boolean;
20
21
  }
21
22
 
22
23
  /** Dependencies injected into the adapter factory. */
@@ -85,13 +86,12 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
85
86
  if (!record || record.status !== "running") {
86
87
  return false;
87
88
  }
88
- if (!record.session) {
89
- // Session not ready yet — queue for delivery once initialized
90
- if (!record.pendingSteers) record.pendingSteers = [];
91
- record.pendingSteers.push(message);
92
- return true;
89
+ const session = record.execution?.session;
90
+ if (!session) {
91
+ // Session not ready yet — queue via manager for delivery once initialized
92
+ return manager.queueSteer(id, message);
93
93
  }
94
- await record.session.steer(message);
94
+ await session.steer(message);
95
95
  return true;
96
96
  },
97
97
 
@@ -124,7 +124,8 @@ export function toSubagentRecord(record: AgentRecord): SubagentRecord {
124
124
  if (record.result !== undefined) out.result = record.result;
125
125
  if (record.error !== undefined) out.error = record.error;
126
126
  if (record.completedAt !== undefined) out.completedAt = record.completedAt;
127
- if (record.worktreeResult !== undefined) out.worktreeResult = record.worktreeResult;
127
+ const worktreeResult = record.worktreeState?.cleanupResult;
128
+ if (worktreeResult !== undefined) out.worktreeResult = worktreeResult;
128
129
 
129
130
  return out;
130
131
  }
@@ -7,6 +7,7 @@ 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";
11
12
  import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
12
13
  import {
@@ -381,7 +382,7 @@ Guidelines:
381
382
  `Agent not found: "${params.resume}". It may have been cleaned up.`,
382
383
  );
383
384
  }
384
- if (!existing.session) {
385
+ if (!existing.execution?.session) {
385
386
  return textResult(
386
387
  `Agent "${params.resume}" has no active session to resume.`,
387
388
  );
@@ -430,7 +431,8 @@ Guidelines:
430
431
 
431
432
  const record = deps.manager.getRecord(id);
432
433
  if (record) {
433
- record.toolCallId = toolCallId;
434
+ // Born complete: notification-state object owns toolCallId + resultConsumed.
435
+ record.notification = new NotificationState(toolCallId);
434
436
  }
435
437
 
436
438
  deps.agentActivity.set(id, bgState);
@@ -451,7 +453,7 @@ Guidelines:
451
453
  `Agent ID: ${id}\n` +
452
454
  `Type: ${displayName}\n` +
453
455
  `Description: ${params.description}\n` +
454
- (record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
456
+ (record?.execution?.outputFile ? `Output file: ${record.execution.outputFile}\n` : "") +
455
457
  (isQueued
456
458
  ? `Position: queued (max ${deps.manager.getMaxConcurrent()} concurrent)\n`
457
459
  : "") +
@@ -525,7 +527,7 @@ Guidelines:
525
527
  fgState.setSession(session);
526
528
  unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
527
529
  for (const a of deps.manager.listAgents()) {
528
- if (a.session === session) {
530
+ if (a.execution?.session === session) {
529
531
  fgId = a.id;
530
532
  deps.agentActivity.set(a.id, fgState);
531
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
- record.resultConsumed = true;
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.resultConsumed = true;
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
  }
@@ -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
- if (!record.session) {
50
- // Session not ready yet — queue the steer for delivery once initialized
51
- if (!record.pendingSteers) record.pendingSteers = [];
52
- record.pendingSteers.push(params.message);
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(record.session, params.message);
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(record.session);
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"}`);
@@ -197,7 +197,8 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
197
197
  }
198
198
 
199
199
  async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
200
- if (!record.session) {
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>(
@@ -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
+ }