@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.
- package/CHANGELOG.md +35 -0
- package/dist/cli.js +3053 -3158
- package/dist/types/advisor/config.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +28 -15
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +7 -2
- package/dist/types/dap/config.d.ts +13 -1
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +7 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +8 -3
- package/dist/types/modes/components/hook-selector.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/session-context.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/thinking.d.ts +6 -3
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/config.test.ts +84 -0
- package/src/advisor/config.ts +28 -0
- package/src/cli/args.ts +1 -0
- package/src/cli/flag-tables.ts +3 -0
- package/src/config/model-registry.ts +1 -1
- package/src/config/model-resolver.ts +14 -12
- package/src/config/models-config-schema.ts +3 -2
- package/src/config/settings-schema.ts +5 -2
- package/src/dap/config.ts +136 -39
- package/src/dap/defaults.json +1 -1
- package/src/eval/js/shared/runtime.ts +20 -4
- package/src/eval/js/worker-core.ts +9 -2
- package/src/exec/bash-executor.ts +8 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/config.ts +27 -13
- package/src/lsp/index.ts +114 -29
- package/src/main.ts +21 -1
- package/src/mcp/transports/stdio.test.ts +12 -0
- package/src/mcp/transports/stdio.ts +14 -8
- package/src/modes/components/hook-selector.ts +9 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/theme/defaults/dark-poimandres.json +6 -5
- package/src/modes/theme/defaults/light-poimandres.json +6 -5
- package/src/modes/theme/theme-schema.json +6 -2
- package/src/modes/theme/theme.ts +24 -18
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/{explore.md → scout.md} +3 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +5 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/debug.md +4 -4
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/task.md +2 -2
- package/src/sdk.ts +68 -45
- package/src/session/agent-session.ts +215 -64
- package/src/session/session-context.test.ts +83 -0
- package/src/session/session-context.ts +1 -0
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-manager.ts +9 -1
- package/src/system-prompt.test.ts +20 -11
- package/src/task/agents.ts +2 -5
- package/src/task/executor.ts +111 -74
- package/src/thinking.ts +16 -7
- package/src/tools/ask.ts +51 -13
- package/src/tools/browser/cmux/cmux-tab.ts +21 -12
- package/src/tools/debug.ts +41 -11
- package/src/utils/file-display-mode.ts +1 -1
- 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(
|
|
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:
|
|
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
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
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:
|
|
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
|
-
|
|
143
|
-
|
|
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:
|
|
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
|
-
|
|
158
|
-
|
|
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", () => {
|
package/src/task/agents.ts
CHANGED
|
@@ -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: "
|
|
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: {
|
package/src/task/executor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const
|
|
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
|
|
1066
|
-
return
|
|
1070
|
+
if (!isRecord(message) || !("content" in message)) {
|
|
1071
|
+
return undefined;
|
|
1067
1072
|
}
|
|
1068
|
-
return
|
|
1073
|
+
return message.content;
|
|
1069
1074
|
};
|
|
1070
1075
|
|
|
1071
1076
|
const getMessageUsage = (message: unknown): unknown => {
|
|
1072
|
-
if (message
|
|
1073
|
-
return
|
|
1077
|
+
if (!isRecord(message) || !("usage" in message)) {
|
|
1078
|
+
return undefined;
|
|
1074
1079
|
}
|
|
1075
|
-
return
|
|
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
|
-
|
|
1155
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1318
|
-
|
|
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
|
|
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(
|
|
1325
|
-
accumulatedUsage.output += getNumberField(
|
|
1326
|
-
accumulatedUsage.cacheRead += getNumberField(
|
|
1327
|
-
accumulatedUsage.cacheWrite += getNumberField(
|
|
1328
|
-
accumulatedUsage.totalTokens += getNumberField(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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: "
|
|
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
|
-
|
|
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`..`
|
|
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`..`
|
|
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.
|
|
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
|
-
|
|
226
|
+
const preferred = model.thinking?.defaultLevel ?? Effort.High;
|
|
227
|
+
return clampAutoThinkingEffort(model, preferred === Effort.Max ? Effort.XHigh : preferred);
|
|
219
228
|
}
|