@oh-my-pi/pi-coding-agent 16.3.14 → 16.4.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +3053 -3158
  3. package/dist/types/advisor/config.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/config/models-config-schema.d.ts +28 -15
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +7 -2
  9. package/dist/types/dap/config.d.ts +13 -1
  10. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +7 -1
  12. package/dist/types/main.d.ts +2 -0
  13. package/dist/types/mcp/transports/stdio.d.ts +8 -3
  14. package/dist/types/modes/components/hook-selector.d.ts +2 -0
  15. package/dist/types/modes/theme/defaults/index.d.ts +2 -0
  16. package/dist/types/modes/theme/theme.d.ts +3 -2
  17. package/dist/types/sdk.d.ts +2 -0
  18. package/dist/types/session/agent-session.d.ts +5 -3
  19. package/dist/types/session/session-context.test.d.ts +1 -0
  20. package/dist/types/session/session-entries.d.ts +4 -0
  21. package/dist/types/thinking.d.ts +6 -3
  22. package/dist/types/utils/file-display-mode.d.ts +1 -1
  23. package/package.json +12 -12
  24. package/src/advisor/__tests__/config.test.ts +84 -0
  25. package/src/advisor/config.ts +28 -0
  26. package/src/cli/args.ts +1 -0
  27. package/src/cli/flag-tables.ts +3 -0
  28. package/src/config/model-registry.ts +1 -1
  29. package/src/config/model-resolver.ts +14 -12
  30. package/src/config/models-config-schema.ts +3 -2
  31. package/src/config/settings-schema.ts +5 -2
  32. package/src/dap/config.ts +136 -39
  33. package/src/dap/defaults.json +1 -1
  34. package/src/eval/js/shared/runtime.ts +20 -4
  35. package/src/eval/js/worker-core.ts +9 -2
  36. package/src/exec/bash-executor.ts +8 -1
  37. package/src/extensibility/extensions/types.ts +6 -0
  38. package/src/internal-urls/docs-index.generated.txt +1 -1
  39. package/src/lsp/config.ts +27 -13
  40. package/src/lsp/index.ts +114 -29
  41. package/src/main.ts +21 -1
  42. package/src/mcp/transports/stdio.test.ts +12 -0
  43. package/src/mcp/transports/stdio.ts +14 -8
  44. package/src/modes/components/hook-selector.ts +9 -2
  45. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  46. package/src/modes/controllers/input-controller.ts +4 -4
  47. package/src/modes/controllers/selector-controller.ts +1 -1
  48. package/src/modes/theme/defaults/dark-poimandres.json +6 -5
  49. package/src/modes/theme/defaults/light-poimandres.json +6 -5
  50. package/src/modes/theme/theme-schema.json +6 -2
  51. package/src/modes/theme/theme.ts +24 -18
  52. package/src/prompts/agents/init.md +1 -1
  53. package/src/prompts/agents/plan.md +2 -2
  54. package/src/prompts/agents/reviewer.md +1 -1
  55. package/src/prompts/agents/{explore.md → scout.md} +3 -2
  56. package/src/prompts/system/plan-mode-active.md +2 -2
  57. package/src/prompts/system/system-prompt.md +5 -3
  58. package/src/prompts/tools/ast-grep.md +1 -1
  59. package/src/prompts/tools/debug.md +4 -4
  60. package/src/prompts/tools/grep.md +1 -1
  61. package/src/prompts/tools/task.md +2 -2
  62. package/src/sdk.ts +68 -45
  63. package/src/session/agent-session.ts +215 -64
  64. package/src/session/session-context.test.ts +83 -0
  65. package/src/session/session-context.ts +1 -0
  66. package/src/session/session-entries.ts +4 -0
  67. package/src/session/session-manager.ts +9 -1
  68. package/src/system-prompt.test.ts +20 -11
  69. package/src/task/agents.ts +2 -5
  70. package/src/task/executor.ts +111 -74
  71. package/src/thinking.ts +16 -7
  72. package/src/tools/ask.ts +51 -13
  73. package/src/tools/browser/cmux/cmux-tab.ts +21 -12
  74. package/src/tools/debug.ts +41 -11
  75. package/src/utils/file-display-mode.ts +1 -1
  76. package/src/prompts/agents/tester.md +0 -111
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import * as snapcompact from "@oh-my-pi/snapcompact";
4
+ import type { CompactionSummaryMessage } from "./messages";
5
+ import { buildSessionContext } from "./session-context";
6
+ import type { SessionEntry } from "./session-entries";
7
+
8
+ const timestamp = "2026-07-09T00:00:00.000Z";
9
+
10
+ const compactedEntries = [
11
+ {
12
+ type: "message",
13
+ id: "m1",
14
+ parentId: null,
15
+ timestamp,
16
+ message: { role: "user", content: [{ type: "text", text: "before compaction" }], timestamp: 1 },
17
+ },
18
+ {
19
+ type: "compaction",
20
+ id: "c1",
21
+ parentId: "m1",
22
+ timestamp,
23
+ summary: "summary",
24
+ firstKeptEntryId: "m1",
25
+ tokensBefore: 123,
26
+ preserveData: {
27
+ [snapcompact.PRESERVE_KEY]: {
28
+ frames: [{ data: "base64-frame", mimeType: "image/png", cols: 10, rows: 10, chars: 100 }],
29
+ totalChars: 100,
30
+ truncatedChars: 0,
31
+ textHead: "head",
32
+ textTail: "tail",
33
+ },
34
+ },
35
+ },
36
+ {
37
+ type: "message",
38
+ id: "m2",
39
+ parentId: "c1",
40
+ timestamp,
41
+ message: { role: "user", content: [{ type: "text", text: "after compaction" }], timestamp: 2 },
42
+ },
43
+ ] satisfies SessionEntry[];
44
+
45
+ function compactionSummary(messages: AgentMessage[]): CompactionSummaryMessage {
46
+ const summary = messages.find(
47
+ (message): message is CompactionSummaryMessage => message.role === "compactionSummary",
48
+ );
49
+ if (!summary) throw new Error("Expected a compaction summary message");
50
+ return summary;
51
+ }
52
+
53
+ describe("buildSessionContext snapcompact archives", () => {
54
+ it("omits snapcompact archive blocks from collapsed transcript summaries", () => {
55
+ const context = buildSessionContext(compactedEntries, undefined, undefined, {
56
+ transcript: true,
57
+ collapseCompactedHistory: true,
58
+ });
59
+
60
+ const summary = compactionSummary(context.messages);
61
+
62
+ expect(summary.images).toBeUndefined();
63
+ expect(summary.blocks).toBeUndefined();
64
+ });
65
+
66
+ it("keeps snapcompact archive blocks in full transcript summaries", () => {
67
+ const context = buildSessionContext(compactedEntries, undefined, undefined, { transcript: true });
68
+
69
+ const summary = compactionSummary(context.messages);
70
+
71
+ expect(summary.images?.map(image => image.data)).toEqual(["base64-frame"]);
72
+ expect(summary.blocks?.map(block => block.type)).toEqual(["text", "image", "text"]);
73
+ });
74
+
75
+ it("keeps snapcompact archive blocks in provider context summaries", () => {
76
+ const context = buildSessionContext(compactedEntries);
77
+
78
+ const summary = compactionSummary(context.messages);
79
+
80
+ expect(summary.images?.map(image => image.data)).toEqual(["base64-frame"]);
81
+ expect(summary.blocks?.map(block => block.type)).toEqual(["text", "image", "text"]);
82
+ });
83
+ });
@@ -132,6 +132,7 @@ function snapcompactHistoryBlocksForContext(
132
132
  options: BuildSessionContextOptions | undefined,
133
133
  ) {
134
134
  if (!archive) return undefined;
135
+ if (options?.transcript && options.collapseCompactedHistory) return undefined;
135
136
  return snapcompact.historyBlocks(archive, snapcompactHistoryBlockOptions(archive, options));
136
137
  }
137
138
 
@@ -32,10 +32,14 @@ export interface SessionHeader {
32
32
  timestamp: string;
33
33
  cwd: string;
34
34
  parentSession?: string;
35
+ /** Provider prompt-cache identity inherited by exact-route full forks. */
36
+ providerPromptCacheKey?: string;
35
37
  }
36
38
 
37
39
  export interface NewSessionOptions {
38
40
  parentSession?: string;
41
+ /** Provider prompt-cache identity to seed on the new session header. */
42
+ providerPromptCacheKey?: string;
39
43
  /** Skip flushing the current session and delete it instead of saving. */
40
44
  drop?: boolean;
41
45
  }
@@ -783,6 +783,7 @@ export class SessionManager {
783
783
  timestamp,
784
784
  cwd: this.#cwd,
785
785
  parentSession: options?.parentSession,
786
+ providerPromptCacheKey: options?.providerPromptCacheKey,
786
787
  };
787
788
  this.#titleUpdatedAt = timestamp;
788
789
 
@@ -1050,6 +1051,7 @@ export class SessionManager {
1050
1051
  timestamp,
1051
1052
  cwd: this.#cwd,
1052
1053
  parentSession: parentSessionId,
1054
+ providerPromptCacheKey: this.#header.providerPromptCacheKey ?? parentSessionId,
1053
1055
  };
1054
1056
  this.#sessionName = this.#header.title;
1055
1057
  this.#titleSource = this.#header.titleSource;
@@ -1888,7 +1890,13 @@ export class SessionManager {
1888
1890
 
1889
1891
  const sourceHeader = sourceEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
1890
1892
  const history = sourceEntries.filter(entry => entry.type !== "session") as SessionEntry[];
1891
- manager.#resetToNewSession({ parentSession: sourceHeader?.id }, options?.sessionFile);
1893
+ manager.#resetToNewSession(
1894
+ {
1895
+ parentSession: sourceHeader?.id,
1896
+ providerPromptCacheKey: sourceHeader?.providerPromptCacheKey ?? sourceHeader?.id,
1897
+ },
1898
+ options?.sessionFile,
1899
+ );
1892
1900
  manager.#header.title = sourceHeader?.title;
1893
1901
  manager.#header.titleSource = sourceHeader?.titleSource;
1894
1902
  manager.#sessionName = manager.#header.title;
@@ -123,29 +123,36 @@ describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => {
123
123
  }, 15_000);
124
124
 
125
125
  it("kills the GPU probe at the prep deadline", async () => {
126
- const result = await runProbeScenario({ runs: 1, sleepSeconds: 7, holdStdoutOpen: true });
126
+ const result = await runProbeScenario({ runs: 1, sleepSeconds: 12, holdStdoutOpen: true });
127
127
 
128
128
  expect(result.cached).toEqual({ gpu: null });
129
+ // Probe is SIGKILLed at ~4.5s and the drain wait is bounded, so in-child
130
+ // time sits near the deadline; waiting on the descendant would push it
131
+ // past the 12s sleep.
129
132
  expect(result.elapsedMs).toBeLessThan(6500);
130
- // Codex#3838: the child process MUST exit shortly after the deadline,
131
- // not linger until a descendant holding stdout (sleep 7) exits on its own.
132
- expect(result.childElapsedMs).toBeLessThan(6500);
133
- }, 15_000);
133
+ // Codex#3838: the child process MUST exit shortly after the deadline, not
134
+ // linger until a descendant holding stdout (sleep 12) exits on its own.
135
+ // The bound over in-child time budgets bun spawn/startup on loaded runners
136
+ // while staying far below the descendant's 12s exit.
137
+ expect(result.childElapsedMs).toBeLessThan(9000);
138
+ }, 20_000);
134
139
 
135
140
  it("does not wait on stdout held by a descendant after a successful probe", async () => {
136
- const result = await runProbeScenario({ runs: 1, sleepSeconds: 3, descendantHoldsStdout: true });
141
+ const result = await runProbeScenario({ runs: 1, sleepSeconds: 8, descendantHoldsStdout: true });
137
142
 
138
143
  expect(result.cached).toEqual({ gpu: null });
139
144
  // Probe exits 0 immediately but leaves a backgrounded sleep holding the stdout
140
145
  // pipe. The success path MUST bound the drain wait, not block until sleep exits.
141
146
  expect(result.elapsedMs).toBeLessThan(2000);
142
- expect(result.childElapsedMs).toBeLessThan(2000);
143
- }, 15_000);
147
+ // Budgets bun spawn/startup overhead; blocking on the descendant would
148
+ // take at least the 8s sleep.
149
+ expect(result.childElapsedMs).toBeLessThan(5000);
150
+ }, 20_000);
144
151
 
145
152
  it("keeps probe output captured before a descendant delays EOF", async () => {
146
153
  const result = await runProbeScenario({
147
154
  runs: 1,
148
- sleepSeconds: 3,
155
+ sleepSeconds: 8,
149
156
  descendantHoldsStdout: true,
150
157
  validOutput: "00:02.0 VGA compatible controller: NVIDIA TestGPU",
151
158
  });
@@ -154,8 +161,10 @@ describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => {
154
161
  // Captured stdout MUST be cached, not discarded as if the probe failed.
155
162
  expect(result.cached).toEqual({ gpu: "02.0 VGA compatible controller: NVIDIA TestGPU" });
156
163
  expect(result.elapsedMs).toBeLessThan(2000);
157
- expect(result.childElapsedMs).toBeLessThan(2000);
158
- }, 15_000);
164
+ // Budgets bun spawn/startup overhead; blocking on the descendant would
165
+ // take at least the 8s sleep.
166
+ expect(result.childElapsedMs).toBeLessThan(5000);
167
+ }, 20_000);
159
168
  });
160
169
 
161
170
  describe.skipIf(process.platform !== "linux")("system prompt CPU model", () => {
@@ -7,15 +7,13 @@ import { Effort } from "@oh-my-pi/pi-ai";
7
7
  import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
8
8
  import { parseAgentFields } from "../discovery/helpers";
9
9
  import designerMd from "../prompts/agents/designer.md" with { type: "text" };
10
- import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
11
10
  // Embed agent markdown files at build time
12
11
  import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
13
12
  import librarianMd from "../prompts/agents/librarian.md" with { type: "text" };
14
-
15
13
  import planMd from "../prompts/agents/plan.md" with { type: "text" };
16
14
  import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
15
+ import scoutMd from "../prompts/agents/scout.md" with { type: "text" };
17
16
  import taskMd from "../prompts/agents/task.md" with { type: "text" };
18
- import testerMd from "../prompts/agents/tester.md" with { type: "text" };
19
17
 
20
18
  import type { AgentDefinition, AgentSource } from "./types";
21
19
 
@@ -42,12 +40,11 @@ function buildAgentContent(def: EmbeddedAgentDef): string {
42
40
  }
43
41
 
44
42
  const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
45
- { fileName: "explore.md", template: exploreMd },
43
+ { fileName: "scout.md", template: scoutMd },
46
44
  { fileName: "plan.md", template: planMd },
47
45
  { fileName: "designer.md", template: designerMd },
48
46
  { fileName: "reviewer.md", template: reviewerMd },
49
47
  { fileName: "librarian.md", template: librarianMd },
50
- { fileName: "tester.md", template: testerMd },
51
48
  {
52
49
  fileName: "task.md",
53
50
  frontmatter: {
@@ -88,7 +88,7 @@ const MCP_CALL_TIMEOUT_MS = 60_000;
88
88
  * `task.softRequestBudgetNotice`.
89
89
  */
90
90
  export const SOFT_REQUEST_BUDGET: Record<string, number> = {
91
- explore: 40,
91
+ scout: 40,
92
92
  sonic: 40,
93
93
  default: 90,
94
94
  };
@@ -254,14 +254,18 @@ function withAbortTimeout<T>(
254
254
  return wrappedPromise;
255
255
  }
256
256
 
257
+ function isRecord(value: unknown): value is Record<string, unknown> {
258
+ if (!value || typeof value !== "object") return false;
259
+ return !Array.isArray(value);
260
+ }
261
+
257
262
  function getReportFindingKey(value: unknown): string | null {
258
- if (!value || typeof value !== "object") return null;
259
- const record = value as Record<string, unknown>;
260
- const title = typeof record.title === "string" ? record.title : null;
261
- const filePath = typeof record.file_path === "string" ? record.file_path : null;
262
- const lineStart = typeof record.line_start === "number" ? record.line_start : null;
263
- const lineEnd = typeof record.line_end === "number" ? record.line_end : null;
264
- const priority = typeof record.priority === "string" ? record.priority : null;
263
+ if (!isRecord(value)) return null;
264
+ const title = typeof value.title === "string" ? value.title : null;
265
+ const filePath = typeof value.file_path === "string" ? value.file_path : null;
266
+ const lineStart = typeof value.line_start === "number" ? value.line_start : null;
267
+ const lineEnd = typeof value.line_end === "number" ? value.line_end : null;
268
+ const priority = typeof value.priority === "string" ? value.priority : null;
265
269
  if (!title || !filePath || lineStart === null || lineEnd === null) {
266
270
  return null;
267
271
  }
@@ -908,6 +912,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
908
912
  const abortSignal = abortController.signal;
909
913
  let activeSession: AgentSession | null = null;
910
914
  let yieldCalled = false;
915
+ let yieldCallPending = false;
911
916
 
912
917
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
913
918
  const accumulatedUsage: Usage = {
@@ -1062,17 +1067,17 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1062
1067
  };
1063
1068
 
1064
1069
  const getMessageContent = (message: unknown): unknown => {
1065
- if (message && typeof message === "object" && "content" in message) {
1066
- return (message as { content?: unknown }).content;
1070
+ if (!isRecord(message) || !("content" in message)) {
1071
+ return undefined;
1067
1072
  }
1068
- return undefined;
1073
+ return message.content;
1069
1074
  };
1070
1075
 
1071
1076
  const getMessageUsage = (message: unknown): unknown => {
1072
- if (message && typeof message === "object" && "usage" in message) {
1073
- return (message as { usage?: unknown }).usage;
1077
+ if (!isRecord(message) || !("usage" in message)) {
1078
+ return undefined;
1074
1079
  }
1075
- return undefined;
1080
+ return message.usage;
1076
1081
  };
1077
1082
 
1078
1083
  const updateRecentOutputLines = () => {
@@ -1136,6 +1141,27 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1136
1141
  });
1137
1142
  };
1138
1143
 
1144
+ const recordExtractedToolData = (toolName: string, data: unknown): void => {
1145
+ progress.extractedToolData = progress.extractedToolData || {};
1146
+ const existing = progress.extractedToolData[toolName] || [];
1147
+ const findingKey = toolName === "report_finding" ? getReportFindingKey(data) : null;
1148
+ if (findingKey) {
1149
+ const existingIndex = existing.findIndex(item => getReportFindingKey(item) === findingKey);
1150
+ if (existingIndex >= 0) {
1151
+ existing[existingIndex] = data;
1152
+ } else {
1153
+ existing.push(data);
1154
+ }
1155
+ } else {
1156
+ existing.push(data);
1157
+ }
1158
+ progress.extractedToolData[toolName] = existing;
1159
+ if (toolName === "yield") {
1160
+ yieldCalled = true;
1161
+ yieldCallPending = false;
1162
+ }
1163
+ };
1164
+
1139
1165
  const processEvent = (event: AgentEvent) => {
1140
1166
  if (resolved) return;
1141
1167
  const now = Date.now();
@@ -1151,14 +1177,21 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1151
1177
  case "tool_execution_start": {
1152
1178
  progress.toolCount++;
1153
1179
  progress.currentTool = event.toolName;
1154
- progress.currentToolArgs = extractToolArgsPreview(
1155
- (event as { toolArgs?: Record<string, unknown> }).toolArgs || event.args || {},
1156
- );
1180
+ let startArgs: Record<string, unknown> = {};
1181
+ if ("toolArgs" in event && isRecord(event.toolArgs)) {
1182
+ startArgs = event.toolArgs;
1183
+ } else if (isRecord(event.args)) {
1184
+ startArgs = event.args;
1185
+ }
1186
+ progress.currentToolArgs = extractToolArgsPreview(startArgs);
1157
1187
  progress.currentToolStartMs = now;
1158
1188
  const intent = event.intent?.trim();
1159
1189
  if (intent) {
1160
1190
  progress.lastIntent = intent;
1161
1191
  }
1192
+ if (event.toolName === "yield" && !yieldCalled) {
1193
+ yieldCallPending = true;
1194
+ }
1162
1195
  // Reset any prior in-flight task snapshot so we don't show stale
1163
1196
  // nested progress when the agent enters a fresh `task` call.
1164
1197
  if (event.toolName === "task") {
@@ -1191,7 +1224,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1191
1224
 
1192
1225
  // Check for registered subagent tool handler
1193
1226
  const handler = subprocessToolRegistry.getHandler(event.toolName);
1194
- const eventArgs = (event as { args?: Record<string, unknown> }).args ?? {};
1227
+ const eventRecord: unknown = event;
1228
+ const eventArgs = isRecord(eventRecord) && isRecord(eventRecord.args) ? eventRecord.args : {};
1195
1229
  if (handler) {
1196
1230
  // Extract data using handler
1197
1231
  if (handler.extractData) {
@@ -1203,26 +1237,14 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1203
1237
  isError: event.isError,
1204
1238
  });
1205
1239
  if (data !== undefined) {
1206
- progress.extractedToolData = progress.extractedToolData || {};
1207
- const existing = progress.extractedToolData[event.toolName] || [];
1208
- const findingKey = event.toolName === "report_finding" ? getReportFindingKey(data) : null;
1209
- if (findingKey) {
1210
- const existingIndex = existing.findIndex(item => getReportFindingKey(item) === findingKey);
1211
- if (existingIndex >= 0) {
1212
- existing[existingIndex] = data;
1213
- } else {
1214
- existing.push(data);
1215
- }
1216
- } else {
1217
- existing.push(data);
1218
- }
1219
- progress.extractedToolData[event.toolName] = existing;
1220
- if (event.toolName === "yield") {
1221
- yieldCalled = true;
1222
- }
1240
+ recordExtractedToolData(event.toolName, data);
1223
1241
  }
1224
1242
  }
1225
1243
 
1244
+ if (event.toolName === "yield") {
1245
+ yieldCallPending = false;
1246
+ }
1247
+
1226
1248
  // Check if handler wants to terminate the session
1227
1249
  if (
1228
1250
  handler.shouldTerminate?.({
@@ -1284,7 +1306,23 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1284
1306
  const role = event.message?.role;
1285
1307
  if (role === "assistant") {
1286
1308
  progress.requests += 1;
1287
- if (softRequestBudget > 0 && !abortSent) {
1309
+ const eventContent = isRecord(event) && "content" in event ? event.content : undefined;
1310
+ const messageContent = getMessageContent(event.message) || eventContent;
1311
+ if (messageContent && Array.isArray(messageContent)) {
1312
+ for (const block of messageContent) {
1313
+ if (!isRecord(block)) continue;
1314
+ if (block.type === "text" && typeof block.text === "string") {
1315
+ outputChunks.push(block.text);
1316
+ continue;
1317
+ }
1318
+ if (block.type !== "toolCall" || typeof block.name !== "string") continue;
1319
+ if (block.name === "yield" && !yieldCalled) {
1320
+ yieldCallPending = true;
1321
+ flushProgress = true;
1322
+ }
1323
+ }
1324
+ }
1325
+ if (softRequestBudget > 0 && !abortSent && !yieldCallPending) {
1288
1326
  if (progress.requests >= softRequestBudget * 1.5) {
1289
1327
  requestAbort("budget");
1290
1328
  } else if (softRequestBudgetNotice && !budgetSteerSent && progress.requests >= softRequestBudget) {
@@ -1302,32 +1340,21 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1302
1340
  }
1303
1341
  }
1304
1342
  }
1305
- if (role === "assistant") {
1306
- const messageContent =
1307
- getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
1308
- if (messageContent && Array.isArray(messageContent)) {
1309
- for (const block of messageContent) {
1310
- if (block.type === "text" && block.text) {
1311
- outputChunks.push(block.text);
1312
- }
1313
- }
1314
- }
1315
- }
1316
1343
  // Extract and accumulate usage (prefer message.usage, fallback to event.usage)
1317
- const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
1318
- if (messageUsage && typeof messageUsage === "object") {
1344
+ const eventUsage = isRecord(event) && "usage" in event ? event.usage : undefined;
1345
+ const messageUsage = getMessageUsage(event.message) || eventUsage;
1346
+ if (isRecord(messageUsage)) {
1319
1347
  // Only count assistant messages (not tool results, etc.)
1320
1348
  if (role === "assistant") {
1321
- const usageRecord = messageUsage as Record<string, unknown>;
1322
- const costRecord = (messageUsage as { cost?: Record<string, unknown> }).cost;
1349
+ const costRecord = isRecord(messageUsage.cost) ? messageUsage.cost : undefined;
1323
1350
  hasUsage = true;
1324
- accumulatedUsage.input += getNumberField(usageRecord, "input") ?? 0;
1325
- accumulatedUsage.output += getNumberField(usageRecord, "output") ?? 0;
1326
- accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
1327
- accumulatedUsage.cacheWrite += getNumberField(usageRecord, "cacheWrite") ?? 0;
1328
- accumulatedUsage.totalTokens += getNumberField(usageRecord, "totalTokens") ?? 0;
1351
+ accumulatedUsage.input += getNumberField(messageUsage, "input") ?? 0;
1352
+ accumulatedUsage.output += getNumberField(messageUsage, "output") ?? 0;
1353
+ accumulatedUsage.cacheRead += getNumberField(messageUsage, "cacheRead") ?? 0;
1354
+ accumulatedUsage.cacheWrite += getNumberField(messageUsage, "cacheWrite") ?? 0;
1355
+ accumulatedUsage.totalTokens += getNumberField(messageUsage, "totalTokens") ?? 0;
1329
1356
  accumulatedUsage.reasoningTokens =
1330
- (accumulatedUsage.reasoningTokens ?? 0) + (getNumberField(usageRecord, "reasoningTokens") ?? 0);
1357
+ (accumulatedUsage.reasoningTokens ?? 0) + (getNumberField(messageUsage, "reasoningTokens") ?? 0);
1331
1358
  if (costRecord) {
1332
1359
  accumulatedUsage.cost.input += getNumberField(costRecord, "input") ?? 0;
1333
1360
  accumulatedUsage.cost.output += getNumberField(costRecord, "output") ?? 0;
@@ -1342,7 +1369,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1342
1369
  // Track latest per-turn context size so the UI can show
1343
1370
  // "current context", not just cumulative billing volume.
1344
1371
  if (role === "assistant") {
1345
- const perTurnTotal = getNumberField(messageUsage as Record<string, unknown>, "totalTokens");
1372
+ const perTurnTotal = getNumberField(messageUsage, "totalTokens");
1346
1373
  if (perTurnTotal !== undefined && perTurnTotal > 0) {
1347
1374
  progress.contextTokens = perTurnTotal;
1348
1375
  }
@@ -1587,35 +1614,45 @@ async function driveSessionToYield(
1587
1614
  }
1588
1615
  }
1589
1616
 
1590
- await awaitAbortable(session.waitForIdle());
1617
+ if (monitor.yieldCalled()) {
1618
+ await session.waitForIdle();
1619
+ } else {
1620
+ await awaitAbortable(session.waitForIdle());
1621
+ }
1591
1622
 
1592
1623
  const lastAssistant = session.getLastAssistantMessage();
1593
1624
  if (lastAssistant) {
1594
1625
  if (lastAssistant.stopReason === "aborted") {
1595
- aborted = monitor.isAbortedRun();
1596
- if (aborted) {
1597
- // A real caller signal or the wall-clock timer carries a precise
1598
- // reason (signal.reason / "runtime limit exceeded"). An internal
1599
- // turn abort does NOT prefer the assistant message's own
1600
- // errorMessage ("Request was aborted" or a specific stream error)
1601
- // over the misleading "Cancelled by caller".
1602
- abortReasonText ??= monitor.hasExplicitAbortReason()
1603
- ? monitor.resolveAbortReasonText()
1604
- : lastAssistant.errorMessage?.trim() || monitor.resolveAbortReasonText();
1626
+ if (!monitor.yieldCalled() || monitor.runtimeLimitExceeded()) {
1627
+ aborted = monitor.isAbortedRun();
1628
+ if (aborted) {
1629
+ // A real caller signal or the wall-clock timer carries a precise
1630
+ // reason (signal.reason / "runtime limit exceeded"). An internal
1631
+ // turn abort does NOT prefer the assistant message's own
1632
+ // errorMessage ("Request was aborted" or a specific stream error)
1633
+ // over the misleading "Cancelled by caller".
1634
+ abortReasonText ??= monitor.hasExplicitAbortReason()
1635
+ ? monitor.resolveAbortReasonText()
1636
+ : lastAssistant.errorMessage?.trim() || monitor.resolveAbortReasonText();
1637
+ }
1638
+ exitCode = 1;
1605
1639
  }
1606
- exitCode = 1;
1607
1640
  } else if (lastAssistant.stopReason === "error") {
1608
1641
  exitCode = 1;
1609
1642
  error ??= lastAssistant.errorMessage || "Subagent failed";
1610
1643
  }
1611
1644
  }
1612
1645
  } catch (err) {
1613
- exitCode = 1;
1614
- if (!abortSignal.aborted) {
1615
- error = err instanceof Error ? err.stack || err.message : String(err);
1646
+ if (abortSignal.aborted && monitor.yieldCalled() && !monitor.runtimeLimitExceeded()) {
1647
+ exitCode = 0;
1648
+ } else {
1649
+ exitCode = 1;
1650
+ if (!abortSignal.aborted) {
1651
+ error = err instanceof Error ? err.stack || err.message : String(err);
1652
+ }
1616
1653
  }
1617
1654
  } finally {
1618
- if (abortSignal.aborted) {
1655
+ if (abortSignal.aborted && (!monitor.yieldCalled() || monitor.runtimeLimitExceeded())) {
1619
1656
  aborted = monitor.isAbortedRun();
1620
1657
  if (aborted) {
1621
1658
  abortReasonText ??= monitor.resolveAbortReasonText();
package/src/thinking.ts CHANGED
@@ -33,7 +33,12 @@ const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
33
33
  [ThinkingLevel.XHigh]: {
34
34
  value: ThinkingLevel.XHigh,
35
35
  label: "xhigh",
36
- description: "Maximum reasoning (~32k tokens)",
36
+ description: "Extended reasoning (~32k tokens)",
37
+ },
38
+ [ThinkingLevel.Max]: {
39
+ value: ThinkingLevel.Max,
40
+ label: "max",
41
+ description: "Maximum reasoning the model supports",
37
42
  },
38
43
  };
39
44
 
@@ -43,7 +48,7 @@ const EFFORT_BY_SELECTOR: Readonly<Record<string, Effort>> = {
43
48
  [Effort.Medium]: Effort.Medium,
44
49
  [Effort.High]: Effort.High,
45
50
  [Effort.XHigh]: Effort.XHigh,
46
- max: Effort.XHigh,
51
+ [Effort.Max]: Effort.Max,
47
52
  };
48
53
  const THINKING_LEVEL_BY_SELECTOR: Readonly<Record<string, ThinkingLevel>> = {
49
54
  [ThinkingLevel.Inherit]: ThinkingLevel.Inherit,
@@ -53,6 +58,7 @@ const THINKING_LEVEL_BY_SELECTOR: Readonly<Record<string, ThinkingLevel>> = {
53
58
  [ThinkingLevel.Medium]: ThinkingLevel.Medium,
54
59
  [ThinkingLevel.High]: ThinkingLevel.High,
55
60
  [ThinkingLevel.XHigh]: ThinkingLevel.XHigh,
61
+ [ThinkingLevel.Max]: ThinkingLevel.Max,
56
62
  };
57
63
 
58
64
  function getOwnSelector<T>(selectors: Readonly<Record<string, T>>, value: string | null | undefined): T | undefined {
@@ -149,7 +155,6 @@ const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
149
155
  */
150
156
  export function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined {
151
157
  if (value === AUTO_THINKING) return AUTO_THINKING;
152
- if (value === "max") return ThinkingLevel.XHigh;
153
158
  return parseThinkingLevel(value);
154
159
  }
155
160
 
@@ -160,7 +165,7 @@ export function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLeve
160
165
 
161
166
  /**
162
167
  * Thinking selectors accepted by the `--thinking` CLI flag, in display order:
163
- * `off`, every concrete effort (`minimal`..`xhigh`), then `auto`. Single source
168
+ * `off`, every concrete effort (`minimal`..`max`), then `auto`. Single source
164
169
  * for the flag's `options` list, shell completions, and the "invalid level"
165
170
  * warning so all three stay in sync.
166
171
  */
@@ -168,7 +173,7 @@ export const CLI_THINKING_LEVELS: readonly string[] = [ThinkingLevel.Off, ...THI
168
173
 
169
174
  /**
170
175
  * Parses a `--thinking` CLI value. Accepts every {@link parseConfiguredThinkingLevel}
171
- * selector (`off`, `auto`, `minimal`..`xhigh`, plus the `max` alias) but rejects
176
+ * selector (`off`, `auto`, `minimal`..`max`) but rejects
172
177
  * `inherit`: an explicit `inherit` on the command line would suppress the
173
178
  * settings/scoped-model fallback during startup resolution only to resolve back
174
179
  * to the provider default, which is never what the user means.
@@ -211,9 +216,13 @@ export function clampAutoThinkingEffort(model: Model | undefined, effort: Effort
211
216
  /**
212
217
  * The provisional concrete level shown while `auto` is configured but before a
213
218
  * turn has been classified. Prefers the model's `defaultLevel`, otherwise High,
214
- * clamped into the auto range. Returns `undefined` for non-reasoning models.
219
+ * clamped into the auto range. Auto never provisions {@link Effort.Max} (the
220
+ * classifier ceiling is XHigh; only an explicit user request reaches Max), so a
221
+ * `defaultLevel` of `max` is capped at XHigh before clamping. Returns
222
+ * `undefined` for non-reasoning models.
215
223
  */
216
224
  export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
217
225
  if (!model?.reasoning) return undefined;
218
- return clampAutoThinkingEffort(model, model.thinking?.defaultLevel ?? Effort.High);
226
+ const preferred = model.thinking?.defaultLevel ?? Effort.High;
227
+ return clampAutoThinkingEffort(model, preferred === Effort.Max ? Effort.XHigh : preferred);
219
228
  }