@tintinweb/pi-subagents 0.6.2 → 0.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.
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
8
8
  import { extractText } from "../context.js";
9
- import { describeActivity, formatDuration, formatTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
9
+ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
10
+ import { describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
10
11
  /** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
11
12
  const CHROME_LINES = 6;
12
13
  const MIN_VIEWPORT = 3;
@@ -101,13 +102,10 @@ export class ConversationViewer {
101
102
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
102
103
  if (toolUses > 0)
103
104
  headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
104
- if (this.activity?.session) {
105
- try {
106
- const tokens = this.activity.session.getSessionStats().tokens.total;
107
- if (tokens > 0)
108
- headerParts.push(formatTokens(tokens));
109
- }
110
- catch { /* */ }
105
+ const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
106
+ if (tokens > 0) {
107
+ const percent = getSessionContextPercent(this.activity?.session);
108
+ headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
111
109
  }
112
110
  lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`));
113
111
  lines.push(hrMid);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * schedule-menu.ts — `/agents → Scheduled jobs` submenu.
3
+ *
4
+ * Minimal v1 surface: list scheduled jobs, select one to inspect details +
5
+ * confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
6
+ * is the canonical creation path), no toggle/cleanup (cancel is enough for
7
+ * "I scheduled something dumb, get rid of it"). Add management surfaces here
8
+ * if real demand emerges.
9
+ */
10
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
11
+ import type { SubagentScheduler } from "../schedule.js";
12
+ /**
13
+ * List scheduled jobs; selecting one opens a cancel-confirm with details.
14
+ * Returns when the user backs out or after a cancellation.
15
+ */
16
+ export declare function showSchedulesMenu(ctx: ExtensionCommandContext, scheduler: SubagentScheduler): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * schedule-menu.ts — `/agents → Scheduled jobs` submenu.
3
+ *
4
+ * Minimal v1 surface: list scheduled jobs, select one to inspect details +
5
+ * confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
6
+ * is the canonical creation path), no toggle/cleanup (cancel is enough for
7
+ * "I scheduled something dumb, get rid of it"). Add management surfaces here
8
+ * if real demand emerges.
9
+ */
10
+ /** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
11
+ function relTime(iso, now = Date.now()) {
12
+ if (!iso)
13
+ return "—";
14
+ const t = new Date(iso).getTime();
15
+ if (Number.isNaN(t))
16
+ return "—";
17
+ const diff = t - now;
18
+ const abs = Math.abs(diff);
19
+ const future = diff > 0;
20
+ if (abs < 60_000)
21
+ return future ? "in <1m" : "<1m ago";
22
+ const m = Math.round(abs / 60_000);
23
+ if (m < 60)
24
+ return future ? `in ${m}m` : `${m}m ago`;
25
+ const h = Math.round(abs / 3_600_000);
26
+ if (h < 24)
27
+ return future ? `in ${h}h` : `${h}h ago`;
28
+ const d = Math.round(abs / 86_400_000);
29
+ return future ? `in ${d}d` : `${d}d ago`;
30
+ }
31
+ /** One-line status icon. */
32
+ function statusIcon(j) {
33
+ if (!j.enabled)
34
+ return "✗";
35
+ if (j.lastStatus === "error")
36
+ return "!";
37
+ if (j.lastStatus === "running")
38
+ return "⋯";
39
+ return "✓";
40
+ }
41
+ /** Compact selectable row — name, schedule, agent type, next/last run, count. */
42
+ function formatJob(j, scheduler) {
43
+ const next = scheduler.getNextRun(j.id);
44
+ return [
45
+ statusIcon(j),
46
+ j.name.padEnd(18).slice(0, 18),
47
+ j.schedule.padEnd(14).slice(0, 14),
48
+ `[${j.subagent_type}]`,
49
+ `next ${relTime(next)}`,
50
+ `last ${relTime(j.lastRun)}`,
51
+ `runs ${j.runCount}`,
52
+ ].join(" ");
53
+ }
54
+ /** Multi-line details block for the cancel confirm. */
55
+ function formatDetails(j, scheduler) {
56
+ const next = scheduler.getNextRun(j.id) ?? "—";
57
+ return [
58
+ `name: ${j.name}`,
59
+ `schedule: ${j.schedule} (${j.scheduleType})`,
60
+ `agent: ${j.subagent_type}`,
61
+ `prompt: ${j.prompt.slice(0, 200)}${j.prompt.length > 200 ? "…" : ""}`,
62
+ `created: ${j.createdAt}`,
63
+ `last run: ${j.lastRun ?? "—"} (${j.lastStatus ?? "—"})`,
64
+ `next run: ${next}`,
65
+ `runs: ${j.runCount}`,
66
+ ].join("\n");
67
+ }
68
+ /**
69
+ * List scheduled jobs; selecting one opens a cancel-confirm with details.
70
+ * Returns when the user backs out or after a cancellation.
71
+ */
72
+ export async function showSchedulesMenu(ctx, scheduler) {
73
+ if (!scheduler.isActive()) {
74
+ ctx.ui.notify("Scheduler is not active in this session.", "warning");
75
+ return;
76
+ }
77
+ const jobs = scheduler.list();
78
+ if (jobs.length === 0) {
79
+ ctx.ui.notify("No scheduled jobs.", "info");
80
+ return;
81
+ }
82
+ const labels = jobs.map(j => formatJob(j, scheduler));
83
+ const choice = await ctx.ui.select(`Scheduled jobs (${jobs.length}) — select to cancel`, labels);
84
+ if (!choice)
85
+ return;
86
+ const idx = labels.indexOf(choice);
87
+ if (idx < 0)
88
+ return;
89
+ const job = jobs[idx];
90
+ const ok = await ctx.ui.confirm(`Cancel "${job.name}"?`, formatDetails(job, scheduler));
91
+ if (!ok)
92
+ return;
93
+ scheduler.removeJob(job.id);
94
+ ctx.ui.notify(`Cancelled "${job.name}".`, "info");
95
+ }
@@ -0,0 +1,50 @@
1
+ /** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
2
+ /**
3
+ * Lifetime usage components, accumulated via `message_end` events. Survives
4
+ * compaction (which replaces session.state.messages and would reset any
5
+ * stats-derived sum). cacheRead is excluded because each turn's cacheRead is
6
+ * the cumulative cached prefix re-read on that one call — summing across
7
+ * turns counts the prefix N times. See issue #38.
8
+ */
9
+ export type LifetimeUsage = {
10
+ input: number;
11
+ output: number;
12
+ cacheWrite: number;
13
+ };
14
+ /** Sum of lifetime usage components, or 0 if undefined. */
15
+ export declare function getLifetimeTotal(u?: LifetimeUsage): number;
16
+ /** Add a usage delta into a target accumulator (mutates target). */
17
+ export declare function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void;
18
+ /** Minimal shape we read from upstream `getSessionStats()`. */
19
+ export type SessionStatsLike = {
20
+ tokens: {
21
+ input: number;
22
+ output: number;
23
+ cacheWrite: number;
24
+ };
25
+ contextUsage?: {
26
+ percent: number | null;
27
+ };
28
+ };
29
+ export type SessionLike = {
30
+ getSessionStats(): SessionStatsLike;
31
+ };
32
+ /**
33
+ * Session-scoped token count: input + output + cacheWrite as reported by
34
+ * upstream `getSessionStats().tokens` for the *current* session window.
35
+ *
36
+ * RESETS at compaction — upstream replaces `session.state.messages` and the
37
+ * stats are derived from that array. For a lifetime total that survives
38
+ * compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
39
+ * from an independent accumulator fed by `message_end` events.
40
+ *
41
+ * Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
42
+ * and so counts the cumulative cached prefix N times across N turns
43
+ * (issue #38).
44
+ */
45
+ export declare function getSessionTokens(session: SessionLike | undefined): number;
46
+ /**
47
+ * Context-window utilization (0–100), or null when unavailable
48
+ * (no model contextWindow, or post-compaction before the next response).
49
+ */
50
+ export declare function getSessionContextPercent(session: SessionLike | undefined): number | null;
package/dist/usage.js ADDED
@@ -0,0 +1,49 @@
1
+ /** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
2
+ /** Sum of lifetime usage components, or 0 if undefined. */
3
+ export function getLifetimeTotal(u) {
4
+ return u ? u.input + u.output + u.cacheWrite : 0;
5
+ }
6
+ /** Add a usage delta into a target accumulator (mutates target). */
7
+ export function addUsage(into, delta) {
8
+ into.input += delta.input;
9
+ into.output += delta.output;
10
+ into.cacheWrite += delta.cacheWrite;
11
+ }
12
+ /**
13
+ * Session-scoped token count: input + output + cacheWrite as reported by
14
+ * upstream `getSessionStats().tokens` for the *current* session window.
15
+ *
16
+ * RESETS at compaction — upstream replaces `session.state.messages` and the
17
+ * stats are derived from that array. For a lifetime total that survives
18
+ * compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
19
+ * from an independent accumulator fed by `message_end` events.
20
+ *
21
+ * Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
22
+ * and so counts the cumulative cached prefix N times across N turns
23
+ * (issue #38).
24
+ */
25
+ export function getSessionTokens(session) {
26
+ if (!session)
27
+ return 0;
28
+ try {
29
+ const t = session.getSessionStats().tokens;
30
+ return t.input + t.output + t.cacheWrite;
31
+ }
32
+ catch {
33
+ return 0;
34
+ }
35
+ }
36
+ /**
37
+ * Context-window utilization (0–100), or null when unavailable
38
+ * (no model contextWindow, or post-compaction before the next response).
39
+ */
40
+ export function getSessionContextPercent(session) {
41
+ if (!session)
42
+ return null;
43
+ try {
44
+ return session.getSessionStats().contextUsage?.percent ?? null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -20,11 +20,15 @@
20
20
  "agent",
21
21
  "autonomous"
22
22
  ],
23
+ "peerDependencies": {
24
+ "@mariozechner/pi-ai": ">=0.70.5",
25
+ "@mariozechner/pi-coding-agent": ">=0.70.5",
26
+ "@mariozechner/pi-tui": ">=0.70.5"
27
+ },
23
28
  "dependencies": {
24
- "@mariozechner/pi-ai": "^0.70.5",
25
- "@mariozechner/pi-coding-agent": "^0.70.5",
26
- "@mariozechner/pi-tui": "^0.70.5",
27
- "@sinclair/typebox": "latest"
29
+ "@sinclair/typebox": "^0.34.49",
30
+ "croner": "^10.0.1",
31
+ "nanoid": "^5.0.0"
28
32
  },
29
33
  "scripts": {
30
34
  "build": "tsc",
@@ -36,7 +40,7 @@
36
40
  "lint:fix": "biome check --fix src/ test/"
37
41
  },
38
42
  "devDependencies": {
39
- "@biomejs/biome": "^2.3.5",
43
+ "@biomejs/biome": "^2.4.14",
40
44
  "@types/node": "^25.5.0",
41
45
  "typescript": "^6.0.0",
42
46
  "vitest": "^4.0.18"
@@ -11,10 +11,13 @@ import type { Model } from "@mariozechner/pi-ai";
11
11
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
12
  import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
13
13
  import type { AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
14
+ import { addUsage } from "./usage.js";
14
15
  import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
15
16
 
16
17
  export type OnAgentComplete = (record: AgentRecord) => void;
17
18
  export type OnAgentStart = (record: AgentRecord) => void;
19
+ export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void;
20
+ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };
18
21
 
19
22
  /** Default max concurrent background agents. */
20
23
  const DEFAULT_MAX_CONCURRENT = 4;
@@ -35,8 +38,16 @@ interface SpawnOptions {
35
38
  inheritContext?: boolean;
36
39
  thinkingLevel?: ThinkingLevel;
37
40
  isBackground?: boolean;
41
+ /**
42
+ * Skip the maxConcurrent queue check for this spawn — start immediately even
43
+ * if the configured concurrency limit would otherwise queue it. Used by the
44
+ * scheduler so a fired job can't be deferred past its trigger window.
45
+ */
46
+ bypassQueue?: boolean;
38
47
  /** Isolation mode — "worktree" creates a temp git worktree for the agent. */
39
48
  isolation?: IsolationMode;
49
+ /** Parent abort signal — when aborted, the subagent is also stopped. */
50
+ signal?: AbortSignal;
40
51
  /** Called on tool start/end with activity info (for streaming progress to UI). */
41
52
  onToolActivity?: (activity: ToolActivity) => void;
42
53
  /** Called on streaming text deltas from the assistant response. */
@@ -45,6 +56,10 @@ interface SpawnOptions {
45
56
  onSessionCreated?: (session: AgentSession) => void;
46
57
  /** Called at the end of each agentic turn with the cumulative count. */
47
58
  onTurnEnd?: (turnCount: number) => void;
59
+ /** Called once per assistant message_end with that message's usage delta. */
60
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
61
+ /** Called when the session successfully compacts. */
62
+ onCompaction?: (info: CompactionInfo) => void;
48
63
  }
49
64
 
50
65
  export class AgentManager {
@@ -52,6 +67,7 @@ export class AgentManager {
52
67
  private cleanupInterval: ReturnType<typeof setInterval>;
53
68
  private onComplete?: OnAgentComplete;
54
69
  private onStart?: OnAgentStart;
70
+ private onCompact?: OnAgentCompact;
55
71
  private maxConcurrent: number;
56
72
 
57
73
  /** Queue of background agents waiting to start. */
@@ -59,9 +75,15 @@ export class AgentManager {
59
75
  /** Number of currently running background agents. */
60
76
  private runningBackground = 0;
61
77
 
62
- constructor(onComplete?: OnAgentComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart?: OnAgentStart) {
78
+ constructor(
79
+ onComplete?: OnAgentComplete,
80
+ maxConcurrent = DEFAULT_MAX_CONCURRENT,
81
+ onStart?: OnAgentStart,
82
+ onCompact?: OnAgentCompact,
83
+ ) {
63
84
  this.onComplete = onComplete;
64
85
  this.onStart = onStart;
86
+ this.onCompact = onCompact;
65
87
  this.maxConcurrent = maxConcurrent;
66
88
  // Cleanup completed agents after 10 minutes (but keep sessions for resume)
67
89
  this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
@@ -99,12 +121,14 @@ export class AgentManager {
99
121
  toolUses: 0,
100
122
  startedAt: Date.now(),
101
123
  abortController,
124
+ lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
125
+ compactionCount: 0,
102
126
  };
103
127
  this.agents.set(id, record);
104
128
 
105
129
  const args: SpawnArgs = { pi, ctx, type, prompt, options };
106
130
 
107
- if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
131
+ if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
108
132
  // Queue it — will be started when a running agent completes
109
133
  this.queue.push({ id, args });
110
134
  return id;
@@ -121,6 +145,15 @@ export class AgentManager {
121
145
  if (options.isBackground) this.runningBackground++;
122
146
  this.onStart?.(record);
123
147
 
148
+ // Wire parent abort signal to stop the subagent when the parent is interrupted
149
+ let detachParentSignal: (() => void) | undefined;
150
+ if (options.signal) {
151
+ const onParentAbort = () => this.abort(id);
152
+ options.signal.addEventListener("abort", onParentAbort, { once: true });
153
+ detachParentSignal = () => options.signal!.removeEventListener("abort", onParentAbort);
154
+ }
155
+ const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
156
+
124
157
  // Worktree isolation: create a temporary git worktree if requested
125
158
  let worktreeCwd: string | undefined;
126
159
  let worktreeWarning = "";
@@ -152,6 +185,15 @@ export class AgentManager {
152
185
  },
153
186
  onTurnEnd: options.onTurnEnd,
154
187
  onTextDelta: options.onTextDelta,
188
+ onAssistantUsage: (usage) => {
189
+ addUsage(record.lifetimeUsage, usage);
190
+ options.onAssistantUsage?.(usage);
191
+ },
192
+ onCompaction: (info) => {
193
+ record.compactionCount++;
194
+ this.onCompact?.(record, info);
195
+ options.onCompaction?.(info);
196
+ },
155
197
  onSessionCreated: (session) => {
156
198
  record.session = session;
157
199
  // Flush any steers that arrived before the session was ready
@@ -173,6 +215,8 @@ export class AgentManager {
173
215
  record.session = session;
174
216
  record.completedAt ??= Date.now();
175
217
 
218
+ detach();
219
+
176
220
  // Final flush of streaming output file
177
221
  if (record.outputCleanup) {
178
222
  try { record.outputCleanup(); } catch { /* ignore */ }
@@ -204,6 +248,8 @@ export class AgentManager {
204
248
  record.error = err instanceof Error ? err.message : String(err);
205
249
  record.completedAt ??= Date.now();
206
250
 
251
+ detach();
252
+
207
253
  // Final flush of streaming output file on error
208
254
  if (record.outputCleanup) {
209
255
  try { record.outputCleanup(); } catch { /* ignore */ }
@@ -278,6 +324,13 @@ export class AgentManager {
278
324
  onToolActivity: (activity) => {
279
325
  if (activity.type === "end") record.toolUses++;
280
326
  },
327
+ onAssistantUsage: (usage) => {
328
+ addUsage(record.lifetimeUsage, usage);
329
+ },
330
+ onCompaction: (info) => {
331
+ record.compactionCount++;
332
+ this.onCompact?.(record, info);
333
+ },
281
334
  signal,
282
335
  });
283
336
  record.status = "completed";
@@ -16,6 +16,7 @@ import {
16
16
  } from "@mariozechner/pi-coding-agent";
17
17
  import { getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
18
18
  import { buildParentContext, extractText } from "./context.js";
19
+ import { DEFAULT_AGENTS } from "./default-agents.js";
19
20
  import { detectEnv } from "./env.js";
20
21
  import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
21
22
  import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
@@ -102,6 +103,17 @@ export interface RunOptions {
102
103
  onSessionCreated?: (session: AgentSession) => void;
103
104
  /** Called at the end of each agentic turn with the cumulative count. */
104
105
  onTurnEnd?: (turnCount: number) => void;
106
+ /**
107
+ * Called once per assistant message_end with that message's usage delta.
108
+ * Lets callers maintain a lifetime accumulator that survives compaction
109
+ * (which replaces session.state.messages and resets stats-derived sums).
110
+ */
111
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
112
+ /**
113
+ * Called when the session successfully compacts. `tokensBefore` is upstream's
114
+ * pre-compaction context size estimate. Aborted compactions don't fire.
115
+ */
116
+ onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
105
117
  }
106
118
 
107
119
  export interface RunResult {
@@ -212,19 +224,11 @@ export async function runAgent(
212
224
  if (agentConfig) {
213
225
  systemPrompt = buildAgentPrompt(agentConfig, effectiveCwd, env, parentSystemPrompt, extras);
214
226
  } else {
215
- // Unknown type fallback: general-purpose (defensive — unreachable in practice
216
- // since index.ts resolves unknown types to "general-purpose" before calling runAgent)
217
- systemPrompt = buildAgentPrompt({
218
- name: type,
219
- description: "General-purpose agent",
220
- systemPrompt: "",
221
- promptMode: "append",
222
- extensions: true,
223
- skills: true,
224
- inheritContext: false,
225
- runInBackground: false,
226
- isolated: false,
227
- }, effectiveCwd, env, parentSystemPrompt, extras);
227
+ // Unknown type fallback: spread the canonical general-purpose config (defensive —
228
+ // unreachable in practice since index.ts resolves unknown types before calling runAgent).
229
+ const fallback = DEFAULT_AGENTS.get("general-purpose");
230
+ if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
231
+ systemPrompt = buildAgentPrompt({ ...fallback, name: type }, effectiveCwd, env, parentSystemPrompt, extras);
228
232
  }
229
233
 
230
234
  // When skills is string[], we've already preloaded them into the prompt.
@@ -350,6 +354,17 @@ export async function runAgent(
350
354
  if (event.type === "tool_execution_end") {
351
355
  options.onToolActivity?.({ type: "end", toolName: event.toolName });
352
356
  }
357
+ if (event.type === "message_end" && event.message.role === "assistant") {
358
+ const u = (event.message as any).usage;
359
+ if (u) options.onAssistantUsage?.({
360
+ input: u.input ?? 0,
361
+ output: u.output ?? 0,
362
+ cacheWrite: u.cacheWrite ?? 0,
363
+ });
364
+ }
365
+ if (event.type === "compaction_end" && !event.aborted && event.result) {
366
+ options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
367
+ }
353
368
  });
354
369
 
355
370
  const collector = collectResponseText(session);
@@ -382,15 +397,31 @@ export async function runAgent(
382
397
  export async function resumeAgent(
383
398
  session: AgentSession,
384
399
  prompt: string,
385
- options: { onToolActivity?: (activity: ToolActivity) => void; signal?: AbortSignal } = {},
400
+ options: {
401
+ onToolActivity?: (activity: ToolActivity) => void;
402
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
403
+ onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
404
+ signal?: AbortSignal;
405
+ } = {},
386
406
  ): Promise<string> {
387
407
  const collector = collectResponseText(session);
388
408
  const cleanupAbort = forwardAbortSignal(session, options.signal);
389
409
 
390
- const unsubToolUse = options.onToolActivity
410
+ const unsubEvents = (options.onToolActivity || options.onAssistantUsage || options.onCompaction)
391
411
  ? session.subscribe((event: AgentSessionEvent) => {
392
- if (event.type === "tool_execution_start") options.onToolActivity!({ type: "start", toolName: event.toolName });
393
- if (event.type === "tool_execution_end") options.onToolActivity!({ type: "end", toolName: event.toolName });
412
+ if (event.type === "tool_execution_start") options.onToolActivity?.({ type: "start", toolName: event.toolName });
413
+ if (event.type === "tool_execution_end") options.onToolActivity?.({ type: "end", toolName: event.toolName });
414
+ if (event.type === "message_end" && event.message.role === "assistant") {
415
+ const u = (event.message as any).usage;
416
+ if (u) options.onAssistantUsage?.({
417
+ input: u.input ?? 0,
418
+ output: u.output ?? 0,
419
+ cacheWrite: u.cacheWrite ?? 0,
420
+ });
421
+ }
422
+ if (event.type === "compaction_end" && !event.aborted && event.result) {
423
+ options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
424
+ }
394
425
  })
395
426
  : () => {};
396
427
 
@@ -398,7 +429,7 @@ export async function resumeAgent(
398
429
  await session.prompt(prompt);
399
430
  } finally {
400
431
  collector.unsubscribe();
401
- unsubToolUse();
432
+ unsubEvents();
402
433
  cleanupAbort();
403
434
  }
404
435
 
@@ -16,13 +16,12 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
16
16
  displayName: "Agent",
17
17
  description: "General-purpose agent for complex, multi-step tasks",
18
18
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
+ // inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
20
+ // Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
19
21
  extensions: true,
20
22
  skills: true,
21
23
  systemPrompt: "",
22
24
  promptMode: "append",
23
- inheritContext: false,
24
- runInBackground: false,
25
- isolated: false,
26
25
  isDefault: true,
27
26
  },
28
27
  ],
@@ -65,9 +64,6 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
65
64
  - Do not use emojis
66
65
  - Be thorough and precise`,
67
66
  promptMode: "replace",
68
- inheritContext: false,
69
- runInBackground: false,
70
- isolated: false,
71
67
  isDefault: true,
72
68
  },
73
69
  ],
@@ -121,9 +117,6 @@ You are STRICTLY PROHIBITED from:
121
117
  List 3-5 files most critical for implementing this plan:
122
118
  - /absolute/path/to/file.ts - [Brief reason]`,
123
119
  promptMode: "replace",
124
- inheritContext: false,
125
- runInBackground: false,
126
- isolated: false,
127
120
  isDefault: true,
128
121
  },
129
122
  ],