@oh-my-pi/pi-coding-agent 17.1.1 → 17.1.2
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 +31 -0
- package/dist/cli.js +6216 -3984
- package/dist/types/cli/bench-cli.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +6 -3
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/launch/broker-list-order.test.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -6
- package/dist/types/modes/types.d.ts +8 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/structured-subagent.d.ts +3 -0
- package/dist/types/task/types.d.ts +11 -11
- package/dist/types/thinking.d.ts +13 -0
- package/dist/types/tools/eval-format/index.d.ts +7 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/julia.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval-format/ruby.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +4 -1
- package/dist/types/web/search/providers/base.d.ts +16 -0
- package/dist/types/web/search/providers/brave.d.ts +2 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
- package/dist/types/web/search/providers/gemini.d.ts +3 -0
- package/dist/types/web/search/providers/jina.d.ts +2 -0
- package/dist/types/web/search/providers/kagi.d.ts +2 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/parallel.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +3 -0
- package/dist/types/web/search/providers/searxng.d.ts +10 -0
- package/dist/types/web/search/providers/tavily.d.ts +8 -0
- package/dist/types/web/search/query.d.ts +190 -0
- package/package.json +12 -12
- package/src/cli/bench-cli.ts +12 -1
- package/src/cli/web-search-cli.ts +7 -0
- package/src/config/model-resolver.ts +17 -6
- package/src/config/settings-schema.ts +5 -0
- package/src/debug/raw-sse-buffer.ts +157 -30
- package/src/export/share.ts +4 -3
- package/src/launch/broker-list-order.test.ts +89 -0
- package/src/launch/broker.ts +49 -8
- package/src/modes/controllers/input-controller.ts +8 -8
- package/src/modes/interactive-mode.ts +60 -5
- package/src/modes/types.ts +9 -4
- package/src/modes/utils/ui-helpers.ts +7 -8
- package/src/prompts/system/resolve-device-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +5 -2
- package/src/prompts/tools/web-search.md +2 -0
- package/src/sdk.ts +8 -4
- package/src/session/agent-session.ts +14 -0
- package/src/session/queued-messages.ts +7 -1
- package/src/task/executor.ts +17 -9
- package/src/task/index.ts +14 -34
- package/src/task/structured-subagent.ts +4 -0
- package/src/task/types.ts +15 -13
- package/src/thinking.ts +27 -0
- package/src/tools/ast-edit.ts +4 -1
- package/src/tools/bash.ts +13 -0
- package/src/tools/browser/render.ts +2 -1
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +12 -6
- package/src/tools/resolve.ts +10 -1
- package/src/tools/todo.ts +58 -6
- package/src/web/search/index.ts +28 -5
- package/src/web/search/providers/anthropic.ts +62 -4
- package/src/web/search/providers/base.ts +16 -0
- package/src/web/search/providers/brave.ts +30 -3
- package/src/web/search/providers/codex.ts +14 -1
- package/src/web/search/providers/duckduckgo.ts +23 -1
- package/src/web/search/providers/ecosia.ts +5 -1
- package/src/web/search/providers/exa.ts +28 -1
- package/src/web/search/providers/firecrawl.ts +37 -3
- package/src/web/search/providers/gemini.ts +12 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +31 -8
- package/src/web/search/providers/kagi.ts +10 -1
- package/src/web/search/providers/kimi.ts +16 -1
- package/src/web/search/providers/mojeek.ts +14 -1
- package/src/web/search/providers/parallel.ts +48 -2
- package/src/web/search/providers/perplexity.ts +94 -6
- package/src/web/search/providers/searxng.ts +145 -9
- package/src/web/search/providers/startpage.ts +8 -2
- package/src/web/search/providers/synthetic.ts +7 -1
- package/src/web/search/providers/tavily.ts +52 -3
- package/src/web/search/providers/tinyfish.ts +6 -1
- package/src/web/search/providers/xai.ts +49 -2
- package/src/web/search/providers/zai.ts +19 -1
- package/src/web/search/query.ts +850 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { createDaemonBrokerClient, type DaemonBrokerClient } from "./client";
|
|
6
|
+
import type { DaemonSnapshot, DaemonSpec } from "./protocol";
|
|
7
|
+
|
|
8
|
+
const TERMINAL_HISTORY_LIMIT = 10;
|
|
9
|
+
|
|
10
|
+
function spec(name: string, cwd: string): DaemonSpec {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
application: process.execPath,
|
|
14
|
+
args: [],
|
|
15
|
+
env: {},
|
|
16
|
+
cwd,
|
|
17
|
+
pty: false,
|
|
18
|
+
restart: "no",
|
|
19
|
+
persist: false,
|
|
20
|
+
detached: false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function terminalSnapshot(index: number): DaemonSnapshot {
|
|
25
|
+
const name = `exited-${index}`;
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
id: name,
|
|
29
|
+
state: index % 2 === 0 ? "exited" : "failed",
|
|
30
|
+
createdAt: index * 10,
|
|
31
|
+
startedAt: index * 10,
|
|
32
|
+
exitedAt: index * 10 + 1,
|
|
33
|
+
exitReason: `historical exit ${index}`,
|
|
34
|
+
restartCount: 0,
|
|
35
|
+
outputBytes: 0,
|
|
36
|
+
persist: false,
|
|
37
|
+
detached: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function seedTerminalRecord(runtimeDir: string, cwd: string, snapshot: DaemonSnapshot): Promise<void> {
|
|
42
|
+
const metaPath = path.join(runtimeDir, "daemons", snapshot.name, "meta.json");
|
|
43
|
+
await Bun.write(metaPath, JSON.stringify({ daemon: snapshot, spec: spec(snapshot.name, cwd) }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function shutdown(client: DaemonBrokerClient, activeName: string): Promise<void> {
|
|
47
|
+
await client.request({ op: "stop", name: activeName, timeoutMs: 2_000 }).catch(() => undefined);
|
|
48
|
+
await client.request({ op: "shutdown" }).catch(() => undefined);
|
|
49
|
+
client.close();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("broker list", () => {
|
|
53
|
+
it("returns active daemons first and caps recovered terminal history by real exit time", async () => {
|
|
54
|
+
using tempDir = TempDir.createSync("@omp-launch-list-");
|
|
55
|
+
const projectDir = path.join(tempDir.path(), "project");
|
|
56
|
+
const runtimeDir = path.join(tempDir.path(), "runtime");
|
|
57
|
+
await fs.mkdir(projectDir);
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < TERMINAL_HISTORY_LIMIT + 5; index++) {
|
|
60
|
+
await seedTerminalRecord(runtimeDir, projectDir, terminalSnapshot(index));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const client = await createDaemonBrokerClient(projectDir, { runtimeDir, idleGraceMs: 5_000 });
|
|
64
|
+
const activeName = "active-server";
|
|
65
|
+
try {
|
|
66
|
+
const started = await client.request({
|
|
67
|
+
op: "start",
|
|
68
|
+
spec: {
|
|
69
|
+
...spec(activeName, projectDir),
|
|
70
|
+
args: ["-e", "process.stdin.resume()"],
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(started.op).toBe("start");
|
|
74
|
+
|
|
75
|
+
const listed = await client.request({ op: "list" });
|
|
76
|
+
expect(listed.op).toBe("list");
|
|
77
|
+
if (listed.op !== "list") throw new Error(`Unexpected broker result: ${listed.op}`);
|
|
78
|
+
|
|
79
|
+
expect(listed.daemons.map(daemon => daemon.name)).toEqual([
|
|
80
|
+
activeName,
|
|
81
|
+
...Array.from({ length: TERMINAL_HISTORY_LIMIT }, (_, offset) => `exited-${14 - offset}`),
|
|
82
|
+
]);
|
|
83
|
+
expect(listed.daemons[0]?.state).toBe("running");
|
|
84
|
+
expect(listed.daemons.at(-1)?.exitedAt).toBe(51);
|
|
85
|
+
} finally {
|
|
86
|
+
await shutdown(client, activeName);
|
|
87
|
+
}
|
|
88
|
+
}, 20_000);
|
|
89
|
+
});
|
package/src/launch/broker.ts
CHANGED
|
@@ -33,6 +33,12 @@ const MAX_LOG_BYTES = 25 * 1024 * 1024;
|
|
|
33
33
|
const LOG_READ_BYTES = 2 * 1024 * 1024;
|
|
34
34
|
const READINESS_BUFFER_CHARS = 64 * 1024;
|
|
35
35
|
const RESTART_MAX_DELAY_MS = 30_000;
|
|
36
|
+
/**
|
|
37
|
+
* Cap on terminal (exited/failed) daemons surfaced by `list`. Active daemons
|
|
38
|
+
* are always shown in full; older history is truncated so the response stays
|
|
39
|
+
* bounded over a long-lived project (issue #6517).
|
|
40
|
+
*/
|
|
41
|
+
const MAX_TERMINAL_DAEMONS_LISTED = 10;
|
|
36
42
|
const TOKEN_FILE = "broker.token";
|
|
37
43
|
const PID_FILE = "broker.pid";
|
|
38
44
|
const META_FILE = "meta.json";
|
|
@@ -95,6 +101,41 @@ function terminalState(state: DaemonSnapshot["state"]): boolean {
|
|
|
95
101
|
return state === "exited" || state === "failed";
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Order daemons for the `list` response: non-terminal (active) daemons first,
|
|
106
|
+
* oldest to newest, so the process the user is acting on is immediately visible
|
|
107
|
+
* instead of buried behind exited history; then the most recently exited/failed
|
|
108
|
+
* ones, capped at {@link MAX_TERMINAL_DAEMONS_LISTED} to keep the response from
|
|
109
|
+
* growing without bound. Truncated terminal records stay addressable by name
|
|
110
|
+
* via `describe`/`logs`/`restart`.
|
|
111
|
+
*/
|
|
112
|
+
function orderDaemonsForListing(snapshots: DaemonSnapshot[]): DaemonSnapshot[] {
|
|
113
|
+
const active: DaemonSnapshot[] = [];
|
|
114
|
+
const terminal: DaemonSnapshot[] = [];
|
|
115
|
+
for (const snapshot of snapshots) {
|
|
116
|
+
(terminalState(snapshot.state) ? terminal : active).push(snapshot);
|
|
117
|
+
}
|
|
118
|
+
active.sort((left, right) => left.createdAt - right.createdAt);
|
|
119
|
+
terminal.sort((left, right) => (right.exitedAt ?? right.createdAt) - (left.exitedAt ?? left.createdAt));
|
|
120
|
+
return [...active, ...terminal.slice(0, MAX_TERMINAL_DAEMONS_LISTED)];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reap a recovered non-detached daemon snapshot in place. Already-terminal
|
|
125
|
+
* records are left untouched so `list` keeps their real {@link DaemonSnapshot.exitedAt}
|
|
126
|
+
* for recency ranking; records that were still alive when the previous broker
|
|
127
|
+
* exited are marked `exited` at `now`, since their process died with that broker
|
|
128
|
+
* (issue #6517). Returns whether the record was reaped.
|
|
129
|
+
*/
|
|
130
|
+
function reapRecoveredSnapshot(snapshot: DaemonSnapshot, now: number): boolean {
|
|
131
|
+
if (terminalState(snapshot.state)) return false;
|
|
132
|
+
snapshot.pid = undefined;
|
|
133
|
+
snapshot.state = "exited";
|
|
134
|
+
snapshot.exitedAt = now;
|
|
135
|
+
snapshot.exitReason = "previous broker exited";
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
98
139
|
/** Mirror per-condition readiness progress into the snapshot so clients can see which condition is unmet. */
|
|
99
140
|
function syncReadyPending(record: ManagedDaemon): void {
|
|
100
141
|
if (record.snapshot.state !== "starting") {
|
|
@@ -402,9 +443,7 @@ class DaemonBroker {
|
|
|
402
443
|
await Promise.all([...this.#records.values()].map(record => this.#refreshDetached(record)));
|
|
403
444
|
return {
|
|
404
445
|
op: "list",
|
|
405
|
-
daemons: [...this.#records.values()]
|
|
406
|
-
.sort((left, right) => left.snapshot.createdAt - right.snapshot.createdAt)
|
|
407
|
-
.map(record => record.snapshot),
|
|
446
|
+
daemons: orderDaemonsForListing([...this.#records.values()].map(record => record.snapshot)),
|
|
408
447
|
};
|
|
409
448
|
}
|
|
410
449
|
case "logs":
|
|
@@ -973,11 +1012,13 @@ class DaemonBroker {
|
|
|
973
1012
|
snapshot.state !== "stopping" &&
|
|
974
1013
|
processRef?.status() === "running";
|
|
975
1014
|
if (!detached) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
snapshot.
|
|
980
|
-
|
|
1015
|
+
// Reap only records that were still alive when the previous broker
|
|
1016
|
+
// exited; already-terminal records keep their real exit time so
|
|
1017
|
+
// `list` ranks exited history by true recency (issue #6517).
|
|
1018
|
+
if (!terminalState(snapshot.state) && processRef) {
|
|
1019
|
+
await processRef.terminate({ group: true, gracefulMs: 500, timeoutMs: 2_000 });
|
|
1020
|
+
}
|
|
1021
|
+
reapRecoveredSnapshot(snapshot, Date.now());
|
|
981
1022
|
} else if (snapshot.state === "restarting") {
|
|
982
1023
|
snapshot.state = spec.ready ? "starting" : "running";
|
|
983
1024
|
}
|
|
@@ -179,9 +179,9 @@ export class InputController {
|
|
|
179
179
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
180
180
|
// #detectLeftDoubleTap.
|
|
181
181
|
#leftTapCount = 0;
|
|
182
|
-
// Sequential index for `local://
|
|
183
|
-
//
|
|
184
|
-
#
|
|
182
|
+
// Sequential index for `local://paste-N.md` references created by the large-paste
|
|
183
|
+
// flow. Seeded from 0 and bumped past existing paste files.
|
|
184
|
+
#pasteCounter = 0;
|
|
185
185
|
|
|
186
186
|
#showTinyTitleDownloadProgress(modelKey: string): void {
|
|
187
187
|
if (!isTinyTitleLocalModelKey(modelKey)) return;
|
|
@@ -1711,7 +1711,7 @@ export class InputController {
|
|
|
1711
1711
|
`Pasted ${lineCount} lines`,
|
|
1712
1712
|
[
|
|
1713
1713
|
{ label: WRAPPED_BLOCK, description: "Wrap the text in <attachment> tags, collapsed to a marker" },
|
|
1714
|
-
{ label: LOCAL_FILE, description: "Save the text to a local://
|
|
1714
|
+
{ label: LOCAL_FILE, description: "Save the text to a local://paste file" },
|
|
1715
1715
|
{ label: INLINE, description: "Collapse the text to an inline paste marker" },
|
|
1716
1716
|
],
|
|
1717
1717
|
{ helpText: "Esc to paste inline" },
|
|
@@ -1740,7 +1740,7 @@ export class InputController {
|
|
|
1740
1740
|
}
|
|
1741
1741
|
|
|
1742
1742
|
/**
|
|
1743
|
-
* Save a large paste to the session's `local://` store and insert a clean `local://
|
|
1743
|
+
* Save a large paste to the session's `local://` store and insert a clean `local://paste-N.md`
|
|
1744
1744
|
* reference into the editor so the agent can `read` it on demand — instead of inlining the text or
|
|
1745
1745
|
* leaking a raw temp path. Falls back to an inline paste marker when the write fails, so the
|
|
1746
1746
|
* content is never lost.
|
|
@@ -1748,7 +1748,7 @@ export class InputController {
|
|
|
1748
1748
|
async #attachPasteAsFile(text: string, lineCount: number): Promise<void> {
|
|
1749
1749
|
try {
|
|
1750
1750
|
// Mirror the exact mapping the read tool's local:// resolver uses so a later
|
|
1751
|
-
// `read local://
|
|
1751
|
+
// `read local://paste-N.md` lands on the file written here.
|
|
1752
1752
|
const localRoot = resolveLocalRoot({
|
|
1753
1753
|
getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
|
|
1754
1754
|
getSessionId: () => this.ctx.sessionManager.getSessionId(),
|
|
@@ -1756,8 +1756,8 @@ export class InputController {
|
|
|
1756
1756
|
let name: string;
|
|
1757
1757
|
let filePath: string;
|
|
1758
1758
|
do {
|
|
1759
|
-
this.#
|
|
1760
|
-
name = `
|
|
1759
|
+
this.#pasteCounter++;
|
|
1760
|
+
name = `paste-${this.#pasteCounter}.md`;
|
|
1761
1761
|
filePath = path.join(localRoot, name);
|
|
1762
1762
|
} while (await Bun.file(filePath).exists());
|
|
1763
1763
|
await Bun.write(filePath, text);
|
|
@@ -209,6 +209,7 @@ import type {
|
|
|
209
209
|
InteractiveModeContext,
|
|
210
210
|
InteractiveModeInitOptions,
|
|
211
211
|
InteractiveSelectorDialogOptions,
|
|
212
|
+
RenderSessionContextOptions,
|
|
212
213
|
SubmittedUserInput,
|
|
213
214
|
TodoItem,
|
|
214
215
|
TodoPhase,
|
|
@@ -1740,6 +1741,60 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1740
1741
|
const context = this.viewSession.buildTranscriptSessionContext({
|
|
1741
1742
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
1742
1743
|
});
|
|
1744
|
+
const preservedLiveToolCallIds = new Set<string>();
|
|
1745
|
+
// A preserved pending-tool component whose result has already landed in
|
|
1746
|
+
// the replayed transcript is re-rendered by `renderSessionContext` itself
|
|
1747
|
+
// (the toolResult message reconstructs the block with its output). Keeping
|
|
1748
|
+
// it in the live set too re-appends a second identical block below the
|
|
1749
|
+
// replayed one — the tool call renders twice (#6516). The preservation
|
|
1750
|
+
// above assumes every pending-tool component is still dangling (its result
|
|
1751
|
+
// lives outside `state.messages`), which stops holding the instant the
|
|
1752
|
+
// result is persisted while the component lingers in `pendingTools` (a
|
|
1753
|
+
// rebuild racing tool-completion, a background/displaceable snapshot).
|
|
1754
|
+
// Drop the already-resolved ones and let the replay own them; only
|
|
1755
|
+
// genuinely in-flight (dangling, replay-stripped) calls still need
|
|
1756
|
+
// preserving.
|
|
1757
|
+
for (const message of context.messages) {
|
|
1758
|
+
if (message.role !== "toolResult") continue;
|
|
1759
|
+
const resolved = livePendingTools.get(message.toolCallId);
|
|
1760
|
+
if (!resolved) continue;
|
|
1761
|
+
// A background task's initial `async.state === "running"` result is
|
|
1762
|
+
// persisted while `EventController#handleToolExecutionEnd` deliberately
|
|
1763
|
+
// keeps its component in `pendingTools` so a later
|
|
1764
|
+
// `tool_execution_update`/`_end` settles it. Such a handle is still
|
|
1765
|
+
// live — dropping it would strand those updates on the running snapshot
|
|
1766
|
+
// — so keep it and let the live component retain ownership; only
|
|
1767
|
+
// terminal results are owned by the replay. (Cast mirrors the async
|
|
1768
|
+
// detail reads in tool-execution.ts / event-controller.ts.)
|
|
1769
|
+
const details = message.details as { async?: { state?: string } } | undefined;
|
|
1770
|
+
if (details?.async?.state === "running") {
|
|
1771
|
+
preservedLiveToolCallIds.add(message.toolCallId);
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
livePendingTools.delete(message.toolCallId);
|
|
1775
|
+
// A `ReadToolGroupComponent` is shared by every read id it renders
|
|
1776
|
+
// (ui-helpers sets the same group for each collapsed read call). While a
|
|
1777
|
+
// sibling read id still points at it the component must stay on screen
|
|
1778
|
+
// and preserved — splicing it here would detach the pending read's
|
|
1779
|
+
// display and strand its future result on an off-screen component.
|
|
1780
|
+
// Splice only once no remaining pending id shares it.
|
|
1781
|
+
let stillShared = false;
|
|
1782
|
+
for (const other of livePendingTools.values()) {
|
|
1783
|
+
if (other === resolved) {
|
|
1784
|
+
stillShared = true;
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (stillShared) {
|
|
1789
|
+
// The shared component still owns this completed member as well as
|
|
1790
|
+
// its pending sibling. Suppress the replay copy so the group remains
|
|
1791
|
+
// a single on-screen block while future results keep routing to it.
|
|
1792
|
+
preservedLiveToolCallIds.add(message.toolCallId);
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const index = liveComponents.indexOf(resolved as unknown as Component);
|
|
1796
|
+
if (index >= 0) liveComponents.splice(index, 1);
|
|
1797
|
+
}
|
|
1743
1798
|
// Prune the settled-component cache to the messages this rebuild will
|
|
1744
1799
|
// actually render. Message objects stay strongly reachable through
|
|
1745
1800
|
// session entries for the whole session, so entries for compacted-away
|
|
@@ -1751,7 +1806,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1751
1806
|
if (component) retained.set(message, component);
|
|
1752
1807
|
}
|
|
1753
1808
|
this.transcriptMessageComponents = retained;
|
|
1754
|
-
this.renderSessionContext(context, {
|
|
1809
|
+
this.renderSessionContext(context, {
|
|
1810
|
+
reuseSettledComponents: options.reuseSettledComponents,
|
|
1811
|
+
preservedLiveToolCallIds,
|
|
1812
|
+
});
|
|
1755
1813
|
for (const child of liveComponents) {
|
|
1756
1814
|
this.chatContainer.addChild(child);
|
|
1757
1815
|
}
|
|
@@ -4294,10 +4352,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4294
4352
|
return this.#uiHelpers.addMessageToChat(message, options);
|
|
4295
4353
|
}
|
|
4296
4354
|
|
|
4297
|
-
renderSessionContext(
|
|
4298
|
-
sessionContext: SessionContext,
|
|
4299
|
-
options?: { updateFooter?: boolean; populateHistory?: boolean; reuseSettledComponents?: boolean },
|
|
4300
|
-
): void {
|
|
4355
|
+
renderSessionContext(sessionContext: SessionContext, options?: RenderSessionContextOptions): void {
|
|
4301
4356
|
for (const message of sessionContext.messages) {
|
|
4302
4357
|
this.noteDisplayableThinkingContent(message);
|
|
4303
4358
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -92,6 +92,14 @@ export interface InteractiveModeInitOptions {
|
|
|
92
92
|
|
|
93
93
|
export type InteractiveSelectorDialogOptions = ExtensionUIDialogOptions & Pick<HookSelectorOptions, "disabledIndices">;
|
|
94
94
|
|
|
95
|
+
export interface RenderSessionContextOptions {
|
|
96
|
+
updateFooter?: boolean;
|
|
97
|
+
populateHistory?: boolean;
|
|
98
|
+
reuseSettledComponents?: boolean;
|
|
99
|
+
/** Tool calls whose existing live component remains the sole render owner across a rebuild. */
|
|
100
|
+
preservedLiveToolCallIds?: ReadonlySet<string>;
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
export interface InteractiveModeContext {
|
|
96
104
|
// UI access
|
|
97
105
|
ui: TUI;
|
|
@@ -310,10 +318,7 @@ export interface InteractiveModeContext {
|
|
|
310
318
|
reuseSettledComponent?: boolean;
|
|
311
319
|
},
|
|
312
320
|
): Component[];
|
|
313
|
-
renderSessionContext(
|
|
314
|
-
sessionContext: SessionContext,
|
|
315
|
-
options?: { updateFooter?: boolean; populateHistory?: boolean; reuseSettledComponents?: boolean },
|
|
316
|
-
): void;
|
|
321
|
+
renderSessionContext(sessionContext: SessionContext, options?: RenderSessionContextOptions): void;
|
|
317
322
|
renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void;
|
|
318
323
|
getUserMessageText(message: Message): string;
|
|
319
324
|
findLastAssistantMessage(): AssistantMessage | undefined;
|
|
@@ -33,7 +33,7 @@ import { UserMessageComponent } from "../../modes/components/user-message";
|
|
|
33
33
|
import { decodeStreamedToolArgs, streamingStringKeysForTool } from "../../modes/controllers/tool-args-reveal";
|
|
34
34
|
import { materializeImageReferenceLinksSync } from "../../modes/image-references";
|
|
35
35
|
import { theme } from "../../modes/theme/theme";
|
|
36
|
-
import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
|
|
36
|
+
import type { CompactionQueuedMessage, InteractiveModeContext, RenderSessionContextOptions } from "../../modes/types";
|
|
37
37
|
import {
|
|
38
38
|
BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE,
|
|
39
39
|
type CustomMessage,
|
|
@@ -72,12 +72,6 @@ type AddMessageOptions = {
|
|
|
72
72
|
reuseSettledComponent?: boolean;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
type RenderSessionContextOptions = {
|
|
76
|
-
updateFooter?: boolean;
|
|
77
|
-
populateHistory?: boolean;
|
|
78
|
-
reuseSettledComponents?: boolean;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
75
|
function imageLinksForMessage(
|
|
82
76
|
message: Extract<AgentMessage, { role: "developer" | "user" }>,
|
|
83
77
|
putBlobSync: InteractiveModeContext["sessionManager"]["putBlobSync"],
|
|
@@ -422,8 +416,12 @@ export class UiHelpers {
|
|
|
422
416
|
if (content.type !== "toolCall") {
|
|
423
417
|
continue;
|
|
424
418
|
}
|
|
425
|
-
resolveWaitingPoll(content.name);
|
|
426
419
|
const afterToolSegment = timeline.afterToolCalls.get(content.id);
|
|
420
|
+
if (options.preservedLiveToolCallIds?.has(content.id)) {
|
|
421
|
+
appendAssistantSegment(afterToolSegment);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
resolveWaitingPoll(content.name);
|
|
427
425
|
|
|
428
426
|
if (content.name === "read" && readArgsCollapseIntoGroup(content.arguments)) {
|
|
429
427
|
if (hasErrorStop && errorMessage) {
|
|
@@ -538,6 +536,7 @@ export class UiHelpers {
|
|
|
538
536
|
pendingUsageTtft = message.ttft;
|
|
539
537
|
pendingUsageTimestamp = message.timestamp;
|
|
540
538
|
} else if (message.role === "toolResult") {
|
|
539
|
+
if (options.preservedLiveToolCallIds?.has(message.toolCallId)) continue;
|
|
541
540
|
const pendingReadComponent = this.ctx.pendingTools.get(message.toolCallId);
|
|
542
541
|
const isReadGroupResult =
|
|
543
542
|
message.toolName === "read" &&
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<system-reminder>
|
|
2
|
-
|
|
2
|
+
The `{{toolName}}` result above is a PREVIEW — no files were changed. Finalize it now with the `write` tool: write a one-sentence reason as plain text to `xd://resolve` to APPLY it, or to `xd://reject` to DISCARD it.
|
|
3
3
|
</system-reminder>
|
|
@@ -13,7 +13,7 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
|
|
|
13
13
|
<helpers>
|
|
14
14
|
State persists across eval calls, so scout in one call and fan out in the next. Every eval call has:
|
|
15
15
|
|
|
16
|
-
- `agent(prompt, *, agent="task",
|
|
16
|
+
- `agent(prompt, *, agent="task", label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("scout", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes. Recursion follows `task.maxRecursionDepth` (default 2; a negative value disables the cap); deeper calls raise an error.
|
|
17
17
|
- `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
|
|
18
18
|
- `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
|
|
19
19
|
- `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
|
|
@@ -7,4 +7,5 @@ Structural AST-aware rewrites via ast-grep. Use for codemods where text replace
|
|
|
7
7
|
- Rewrite patterns MUST parse as single AST node. Non-standalone → wrap: `class $_ { … }`.
|
|
8
8
|
- TS: tolerate annotations — `async function $NAME($$$ARGS): $_ { $$$BODY }`. Delete with empty `out`: `{"pat":"console.log($$$)","out":""}`.
|
|
9
9
|
- 1:1 substitution — no splitting/merging captures.
|
|
10
|
+
- Matches are STAGED as a proposal, not applied: finalize by writing a one-sentence reason to `xd://resolve` (apply) or `xd://reject` (discard).
|
|
10
11
|
- Parse issues → malformed rewrite, not clean no-op. For one-off text edits, prefer the Edit tool.
|
|
@@ -9,6 +9,7 @@ Use ONLY for: single binary call or short pipeline that COMPUTES a fact (`wc -l`
|
|
|
9
9
|
- `pty: true` only for real terminal needs (`sudo`, `ssh`); default `false`.
|
|
10
10
|
- Multiple calls run concurrently; NEVER split order-dependent commands — chain with `&&` in one call (`;` only to continue past failure).
|
|
11
11
|
- Internal URIs (`skill://`, `agent://`, …) auto-resolve to FS paths.
|
|
12
|
+
{{#if hasShellBuiltins}}- aux utils available: mkdir, head, tail, wc, sort, ls, find, grep, rg, fd, cat, uniq, base64, cmp, md5sum, sha{1,224,256,384,512}sum, b2sum, basename, dirname, readlink, realpath, touch, stat, date, mktemp, seq, yes, printenv, truncate, tac, nproc, uname, whoami, hostname, which, diff, cut, tee, tr, paste, comm, sed, xargs, jq, rm, mv, ln, ts, sponge, ifne, isutf8, combine{{#unless isWindows}}, errno{{/unless}}{{/if}}
|
|
12
13
|
{{#if asyncEnabled}}- `async: true` defers reporting for finite commands needing no later input.{{/if}}
|
|
13
14
|
</instruction>
|
|
14
15
|
|
|
@@ -20,9 +20,9 @@ tool.<name>(args) → unknown
|
|
|
20
20
|
Invoke any session tool; `args` = its parameter object.
|
|
21
21
|
completion(prompt, model?="default"|"smol"|"slow", system?=None, schema?=None) → str | dict
|
|
22
22
|
Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → parsed object.
|
|
23
|
-
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}",
|
|
23
|
+
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", label?=None, schema?=None, schema{{#if js}}Mode{{else}}_mode{{/if}}?="permissive", isolated?=None, apply?=None, merge?=None, handle?=False) → str | dict
|
|
24
24
|
Run a subagent → final output. `agent` selects a discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` overrides agent/session schemas; `schemaMode`/`schema_mode`: "permissive" | "strict". Effective schemas return parsed data. `isolated` requests a worktree; `apply`/`merge` control its changes. Background via `local://` files named in the prompt. `handle` → { text, output, handle: "agent://<id>", id, agent }, parsed `data` when structured.
|
|
25
|
-
{{#if js}} JS: ONE trailing object — agent(prompt, { agent,
|
|
25
|
+
{{#if js}} JS: ONE trailing object — agent(prompt, { agent, label, schema, schemaMode, isolated, apply, merge, handle }).{{/if}}
|
|
26
26
|
{{/if}}
|
|
27
27
|
parallel(thunks) → list pipeline(items, ...stages) → list
|
|
28
28
|
log(message) → None phase(title) → None
|
|
@@ -14,6 +14,9 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
|
|
|
14
14
|
- **Agent typing:** Pick each item's `agent` type. Read-only research MUST use `agent: "scout"` (faster model). Use default worker only when no specialist fits.
|
|
15
15
|
- **No overhead:** Each `task` MUST instruct its agent to skip formatters, linters, and project-wide test suites. Run those once at the end.
|
|
16
16
|
- **One-pass:** Prefer agents that investigate AND edit in one pass; spin a read-only scout only when affected files are genuinely unknown.
|
|
17
|
+
- **Overlap is safe:** Concurrent edits to the same files auto-resolve{{#if ircEnabled}}; worst case, agents coordinate directly over IRC{{/if}}. NEVER shrink or serialize a batch to avoid file overlap. Two prerequisites:
|
|
18
|
+
1. Every task MUST skip validation (build/lint/tests) — validating mid-flight blocks agents on each other's edits.
|
|
19
|
+
2. Decide cross-task contracts up front (e.g. the interface A implements and B consumes) and state them in the {{#if batchEnabled}}batch `context`{{else}}task{{/if}}, not left for agents to negotiate.
|
|
17
20
|
|
|
18
21
|
# Inputs
|
|
19
22
|
{{#if batchEnabled}}
|
|
@@ -22,7 +25,7 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
|
|
|
22
25
|
- `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
|
|
23
26
|
- `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
|
24
27
|
- `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
25
|
-
- `
|
|
28
|
+
- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
|
|
26
29
|
- `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
|
|
27
30
|
- `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
|
|
28
31
|
{{#if isolationEnabled}}
|
|
@@ -36,7 +39,7 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
|
|
|
36
39
|
- `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
|
|
37
40
|
- `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
|
38
41
|
- `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
39
|
-
- `
|
|
42
|
+
- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
|
|
40
43
|
- `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
|
|
41
44
|
- `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
|
|
42
45
|
{{#if isolationEnabled}}
|
|
@@ -3,4 +3,6 @@ Searches the web for up-to-date information beyond knowledge cutoff.
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- You SHOULD prefer primary sources (papers, official docs) and corroborate key claims with multiple sources
|
|
5
5
|
- You MUST include links for cited sources in the final response
|
|
6
|
+
- NEVER use for content that is programmatically accessible or whose URL you already know (GitHub repos/issues, a known arXiv paper, a Wikipedia page, official docs) — `read` the URL directly instead
|
|
7
|
+
- `query` supports Google-style directives on every provider: `site:`/`-site:`, `after:`/`before:` (`YYYY-MM-DD`), `inurl:`, `intitle:`, `filetype:`, `"exact phrase"`, `-term`, `OR`. Constraints map to native provider filters where available; otherwise results are filtered leniently — a constraint matching nothing is relaxed and reported instead of returning zero results.
|
|
6
8
|
</instruction>
|
package/src/sdk.ts
CHANGED
|
@@ -2594,9 +2594,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2594
2594
|
// ExtensionToolWrapper, so the registry holds them unwrapped. The normal
|
|
2595
2595
|
// `write xd://<tool>` path runs approval through the wrapped `write` tool's
|
|
2596
2596
|
// tier gate, but Cursor invokes advertised devices via `tool.execute()`
|
|
2597
|
-
// directly
|
|
2598
|
-
//
|
|
2599
|
-
|
|
2597
|
+
// directly, and the agent loop's fallback resolver executes mounted
|
|
2598
|
+
// devices the model called by their top-level name — so wrap unwrapped
|
|
2599
|
+
// devices here to keep the approval/deny/prompt gate. Dynamic mounts
|
|
2600
|
+
// (custom/MCP) already come from the wrapped registry.
|
|
2601
|
+
const resolveDeviceTool = (name: string): AgentTool | undefined => {
|
|
2600
2602
|
const device = toolSession.xdevRegistry?.get(name);
|
|
2601
2603
|
if (!device) return undefined;
|
|
2602
2604
|
return device instanceof ExtensionToolWrapper ? device : new ExtensionToolWrapper(device, extensionRunner);
|
|
@@ -2604,7 +2606,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2604
2606
|
const cursorExecHandlers = new CursorExecHandlers({
|
|
2605
2607
|
cwd,
|
|
2606
2608
|
tools: toolRegistry,
|
|
2607
|
-
getTool:
|
|
2609
|
+
getTool: resolveDeviceTool,
|
|
2608
2610
|
getToolContext: () => toolContextStore.getContext(),
|
|
2609
2611
|
emitEvent: event => cursorEventEmitter?.(event),
|
|
2610
2612
|
});
|
|
@@ -3012,6 +3014,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3012
3014
|
cursorExecHandlers,
|
|
3013
3015
|
getCursorTools: () => [...(toolSession.xdevRegistry?.list() ?? [])],
|
|
3014
3016
|
transformToolCallArguments,
|
|
3017
|
+
resolveFallbackTool: resolveDeviceTool,
|
|
3015
3018
|
intentTracing: !!intentField,
|
|
3016
3019
|
pruneToolDescriptions: inlineToolDescriptors,
|
|
3017
3020
|
dialect: resolveDialect(settings.get("tools.format"), model),
|
|
@@ -3360,6 +3363,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3360
3363
|
getToolContext: toolCall => toolContextStore.getContext(toolCall),
|
|
3361
3364
|
streamFn: settingsAwareStreamFn,
|
|
3362
3365
|
transformToolCallArguments,
|
|
3366
|
+
resolveFallbackTool: resolveDeviceTool,
|
|
3363
3367
|
intentTracing: !!intentField,
|
|
3364
3368
|
pruneToolDescriptions: inlineToolDescriptors,
|
|
3365
3369
|
dialect: resolveDialect(settings.get("tools.format"), captureModel),
|
|
@@ -2406,6 +2406,20 @@ export class AgentSession {
|
|
|
2406
2406
|
baseUrl: this.#modelRegistry.getProviderBaseUrl?.(assistantMsg.provider),
|
|
2407
2407
|
});
|
|
2408
2408
|
}
|
|
2409
|
+
// Broker deployments: report this request's burn so the broker can
|
|
2410
|
+
// attribute token usage per install. No-op with a local auth store.
|
|
2411
|
+
this.#modelRegistry.authStorage.recordObservedUsage({
|
|
2412
|
+
provider: assistantMsg.provider,
|
|
2413
|
+
model: assistantMsg.model,
|
|
2414
|
+
at: assistantMsg.timestamp,
|
|
2415
|
+
usage: {
|
|
2416
|
+
input: assistantMsg.usage.input,
|
|
2417
|
+
output: assistantMsg.usage.output,
|
|
2418
|
+
cacheRead: assistantMsg.usage.cacheRead,
|
|
2419
|
+
cacheWrite: assistantMsg.usage.cacheWrite,
|
|
2420
|
+
},
|
|
2421
|
+
costUsd: assistantMsg.usage.cost.total,
|
|
2422
|
+
});
|
|
2409
2423
|
}
|
|
2410
2424
|
if (event.message.role === "toolResult") {
|
|
2411
2425
|
const { toolName, toolCallId, isError, content } = event.message;
|
|
@@ -44,7 +44,13 @@ export function isTerminalTextAssistantAnswer(message: AgentMessage | undefined)
|
|
|
44
44
|
if (part.text.trim().length > 0) hasText = true;
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
-
if (
|
|
47
|
+
if (
|
|
48
|
+
part.type === "thinking" ||
|
|
49
|
+
part.type === "redactedThinking" ||
|
|
50
|
+
part.type === "fallback" ||
|
|
51
|
+
part.type === "anthropicServerTool"
|
|
52
|
+
)
|
|
53
|
+
continue;
|
|
48
54
|
return false;
|
|
49
55
|
}
|
|
50
56
|
return hasText;
|
package/src/task/executor.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type { AuthStorage } from "../session/auth-storage";
|
|
|
46
46
|
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
47
47
|
import { SessionManager } from "../session/session-manager";
|
|
48
48
|
import { truncateTail } from "../session/streaming-output";
|
|
49
|
-
import type
|
|
49
|
+
import { type ConfiguredThinkingLevel, resolveTaskEffortLevel, type TaskEffort } from "../thinking";
|
|
50
50
|
import type { ContextFileEntry, ToolSession } from "../tools";
|
|
51
51
|
import { resolveEvalBackends } from "../tools/eval-backends";
|
|
52
52
|
import { isIrcEnabled } from "../tools/hub";
|
|
@@ -323,6 +323,8 @@ export interface ExecutorOptions {
|
|
|
323
323
|
*/
|
|
324
324
|
parentActiveModelPattern?: string;
|
|
325
325
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
326
|
+
/** Caller-requested coarse effort (`lo`/`med`/`hi`); maps onto the resolved model's supported thinking range and wins over {@link thinkingLevel}. */
|
|
327
|
+
effort?: TaskEffort;
|
|
326
328
|
/** Schema used to validate the final structured completion. */
|
|
327
329
|
outputSchema?: unknown;
|
|
328
330
|
/** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
|
|
@@ -2644,16 +2646,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2644
2646
|
if (model?.contextWindow && model.contextWindow > 0) {
|
|
2645
2647
|
progress.contextWindow = model.contextWindow;
|
|
2646
2648
|
}
|
|
2649
|
+
// Caller-requested coarse effort maps onto the resolved model's
|
|
2650
|
+
// supported range; undefined (no effort, or no controllable effort
|
|
2651
|
+
// surface) falls through to the normal selectors below.
|
|
2652
|
+
const effortLevel = options.effort !== undefined ? resolveTaskEffortLevel(model, options.effort) : undefined;
|
|
2647
2653
|
if (model) {
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2654
|
+
const displayLevel = effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : undefined);
|
|
2655
|
+
progress.resolvedModel =
|
|
2656
|
+
displayLevel !== undefined
|
|
2657
|
+
? formatModelSelectorValue(formatModelStringWithRouting(model), displayLevel)
|
|
2658
|
+
: formatModelStringWithRouting(model);
|
|
2651
2659
|
}
|
|
2652
|
-
// Precedence: explicit `:level` suffix on the resolved
|
|
2653
|
-
// agent-definition default (e.g. task's `auto`) >
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
2660
|
+
// Precedence: caller `effort` > explicit `:level` suffix on the resolved
|
|
2661
|
+
// model pattern > agent-definition default (e.g. task's `auto`) >
|
|
2662
|
+
// pattern-derived level.
|
|
2663
|
+
const effectiveThinkingLevel =
|
|
2664
|
+
effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : (thinkingLevel ?? resolvedThinkingLevel));
|
|
2657
2665
|
resolvedAt = performance.now();
|
|
2658
2666
|
// Per-agent prewalk: the agent definition's `prewalk` frontmatter or the
|
|
2659
2667
|
// `task.agentPrewalk` settings override hands the subagent off to a
|