@oh-my-pi/pi-coding-agent 13.10.1 → 13.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +3 -1
- package/src/commit/agentic/index.ts +7 -1
- package/src/commit/analysis/conventional.ts +5 -1
- package/src/commit/analysis/summary.ts +5 -1
- package/src/commit/changelog/generate.ts +5 -1
- package/src/commit/changelog/index.ts +4 -0
- package/src/commit/map-reduce/index.ts +5 -0
- package/src/commit/map-reduce/map-phase.ts +17 -2
- package/src/commit/map-reduce/reduce-phase.ts +5 -1
- package/src/commit/model-selection.ts +38 -26
- package/src/commit/pipeline.ts +22 -11
- package/src/config/model-registry.ts +98 -17
- package/src/config/settings-schema.ts +31 -12
- package/src/config.ts +10 -3
- package/src/discovery/helpers.ts +10 -3
- package/src/exa/index.ts +1 -11
- package/src/exa/search.ts +1 -122
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/config.ts +1 -0
- package/src/lsp/defaults.json +3 -3
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +81 -0
- package/src/modes/components/settings-defs.ts +5 -0
- package/src/modes/components/todo-reminder.ts +8 -1
- package/src/modes/controllers/command-controller.ts +77 -3
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +2 -3
- package/src/modes/controllers/selector-controller.ts +18 -17
- package/src/modes/interactive-mode.ts +11 -7
- package/src/modes/theme/theme.ts +30 -27
- package/src/modes/types.ts +2 -1
- package/src/patch/hashline.ts +123 -22
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/code-search.md +45 -0
- package/src/prompts/tools/find.md +1 -0
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +26 -111
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/todo-write.md +11 -1
- package/src/sdk.ts +20 -16
- package/src/session/agent-session.ts +85 -7
- package/src/session/streaming-output.ts +17 -54
- package/src/slash-commands/builtin-registry.ts +10 -2
- package/src/task/executor.ts +10 -19
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +5 -10
- package/src/task/template.ts +4 -1
- package/src/task/types.ts +2 -0
- package/src/tools/ast-edit.ts +26 -7
- package/src/tools/ast-grep.ts +26 -9
- package/src/tools/exit-plan-mode.ts +6 -0
- package/src/tools/fetch.ts +37 -6
- package/src/tools/find.ts +13 -64
- package/src/tools/grep.ts +27 -10
- package/src/tools/output-meta.ts +10 -7
- package/src/tools/path-utils.ts +348 -0
- package/src/tools/read.ts +13 -26
- package/src/tools/todo-write.ts +27 -4
- package/src/utils/commit-message-generator.ts +27 -22
- package/src/utils/image-input.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/utils/title-generator.ts +36 -23
- package/src/utils/tool-choice.ts +28 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/youtube.ts +29 -0
- package/src/web/search/code-search.ts +385 -0
- package/src/web/search/index.ts +25 -280
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/types.ts +29 -0
- package/src/exa/company.ts +0 -26
- package/src/exa/linkedin.ts +0 -26
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
Applies precise, surgical file edits by referencing `LINE#ID` tags from `read` output. Each tag uniquely identifies a line, so edits remain stable even when lines shift.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Follow these steps in order for every edit:
|
|
5
|
-
1. You **SHOULD** issue a `read` call before editing to get fresh `LINE#ID` tags. Editing without current tags causes mismatches because other edits or external changes may have shifted line numbers since your last read.
|
|
6
|
-
2. You **MUST** submit one `edit` call per file with all operations. Multiple calls to the same file require re-reading between each one (tags shift after each edit), so batching avoids wasted round-trips. Think your changes through before submitting.
|
|
7
|
-
3. You **MUST** pick the operation that matches the owning structure, not merely the smallest textual diff. Use the smallest operation only when it still cleanly owns the changed syntax. If a tiny edit would patch around a block tail, delimiter, or neighboring structural line, expand it to the semantically correct `replace` span instead.
|
|
8
|
-
</workflow>
|
|
9
|
-
|
|
10
|
-
<checklist>
|
|
11
|
-
Before choosing the payload, answer these questions in order:
|
|
12
|
-
1. **Am I replacing existing lines or inserting new ones?** If any existing line changes, use `replace` for the full changed span.
|
|
13
|
-
2. **What declaration or block owns this anchor line?** Prefer declaration/header lines over blank lines or delimiters.
|
|
14
|
-
3. **Am I inserting self-contained new content, or changing an existing block?** Use `append`/`prepend` only for self-contained additions. If surrounding code, indentation, or closers also change, use `replace`.
|
|
15
|
-
4. **Am I editing near a block tail or closing delimiter?** If yes, use shape (a) or (b) from the block-boundaries rule: either stay entirely inside the body, or own the full block including header and closer. Never set `end` at a closer without re-emitting it, and never re-emit a closer without including it in `end`.
|
|
16
|
-
</checklist>
|
|
3
|
+
Read the file first to get fresh tags. Submit one `edit` call per file with all operations batched — tags shift after each edit, so multiple calls require re-reading between them.
|
|
17
4
|
|
|
18
5
|
<operations>
|
|
19
6
|
**`path`** — the path to the file to edit.
|
|
@@ -21,25 +8,20 @@ Before choosing the payload, answer these questions in order:
|
|
|
21
8
|
**`delete`** — if true, delete the file.
|
|
22
9
|
|
|
23
10
|
**`edits[n].pos`** — the anchor line. Meaning depends on `op`:
|
|
24
|
-
- if `replace`: line to rewrite
|
|
11
|
+
- if `replace`: first line to rewrite
|
|
25
12
|
- if `prepend`: line to insert new lines **before**; omit for beginning of file
|
|
26
13
|
- if `append`: line to insert new lines **after**; omit for end of file
|
|
27
14
|
**`edits[n].end`** — range replace only. The last line of the range (inclusive). Omit for single-line replace.
|
|
28
15
|
**`edits[n].lines`** — the replacement content:
|
|
29
|
-
- `[
|
|
16
|
+
- for `replace`: the exact lines that will replace `[pos, end??pos]` inclusively (or the single `pos` line when `end` is omitted)
|
|
17
|
+
- for `prepend`/`append`: the new lines to insert
|
|
30
18
|
- `[""]` — blank line
|
|
31
|
-
- `null` or `[]` — delete if replace
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
- `null` or `[]` — delete if replace
|
|
20
|
+
- If `lines` contains content that already exists after `end`, those lines **will be duplicated** in the output.
|
|
21
|
+
- Keep `lines` to exactly what belongs inside the consumed range.
|
|
22
|
+
- Ops are applied bottom-up. Tags **MUST** be referenced from the most recent `read` output.
|
|
34
23
|
</operations>
|
|
35
24
|
|
|
36
|
-
<rules>
|
|
37
|
-
1. **Use `prepend`/`append` only for self-contained additions whose surrounding structure stays unchanged.** If you are adding a sibling declaration, prefer `prepend` on the next sibling declaration instead of `append` on the previous block closer.
|
|
38
|
-
2. **If the change touches existing code near a block tail, use range `replace` over the owned span.** Do not patch just the final line(s) before a closing delimiter when the surrounding structure, indentation, or control flow is also changing.
|
|
39
|
-
3. **Match surrounding indentation for new lines.** When inserting via `prepend`/`append`, look at the anchor line and its neighbors in the `read` output. New `lines` entries **MUST** carry the same leading whitespace. If the context uses tabs at depth 1 (`\t`), your inserted declarations need `\t` and bodies need `\t\t`. Inserting at indent level 0 inside an indented block is always wrong.
|
|
40
|
-
4. **Block boundaries travel together — never split them.** See the block-boundaries rule in `<critical>`. The two valid shapes are: replace only the body (leave header and closer untouched), or replace the whole block (header through closer, re-emit all in `lines`). Do not set `end` to a closer and omit it from `lines` (deletes it). Do not emit a closer in `lines` without including it in `end` (duplicates it).
|
|
41
|
-
</rules>
|
|
42
|
-
|
|
43
25
|
<examples>
|
|
44
26
|
All examples below reference the same file, `util.ts`:
|
|
45
27
|
```ts
|
|
@@ -103,55 +85,10 @@ Range — remove the legacy block (lines 10–11):
|
|
|
103
85
|
```
|
|
104
86
|
</example>
|
|
105
87
|
|
|
106
|
-
<example name="clear text but keep the line break">
|
|
107
|
-
Blank out a line without removing it:
|
|
108
|
-
```
|
|
109
|
-
{
|
|
110
|
-
path: "util.ts",
|
|
111
|
-
edits: [{
|
|
112
|
-
op: "replace",
|
|
113
|
-
pos: {{hlineref 3 "const tag = \"DO NOT SHIP\";"}},
|
|
114
|
-
lines: [""]
|
|
115
|
-
}]
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
</example>
|
|
119
|
-
|
|
120
88
|
<example name="rewrite a block body — shape (a)">
|
|
121
89
|
Replace the catch body with smarter error handling. Shape (a): `pos` is the first body line, `end` is the last body line. The catch header (line 14) and its closer (line 17) are outside the range and stay untouched.
|
|
122
|
-
```
|
|
123
|
-
{
|
|
124
|
-
path: "util.ts",
|
|
125
|
-
edits: [{
|
|
126
|
-
op: "replace",
|
|
127
|
-
pos: {{hlineref 15 "\t\tconsole.error(err);"}},
|
|
128
|
-
end: {{hlineref 16 "\t\treturn null;"}},
|
|
129
|
-
lines: [
|
|
130
|
-
"\t\tif (isEnoent(err)) return null;",
|
|
131
|
-
"\t\tthrow err;"
|
|
132
|
-
]
|
|
133
|
-
}]
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
</example>
|
|
137
90
|
|
|
138
|
-
|
|
139
|
-
When changing body content, replace the entire body span — not just one line inside it. Patching one line leaves the rest of the body stale.
|
|
140
|
-
|
|
141
|
-
Bad — appends after one body line, leaving the original `return null` in place:
|
|
142
|
-
```
|
|
143
|
-
{
|
|
144
|
-
path: "util.ts",
|
|
145
|
-
edits: [{
|
|
146
|
-
op: "append",
|
|
147
|
-
pos: {{hlineref 15 "\t\tconsole.error(err);"}},
|
|
148
|
-
lines: [
|
|
149
|
-
"\t\treturn fallback;"
|
|
150
|
-
]
|
|
151
|
-
}]
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
Good — shape (a): replace the full body span. Header and closer stay untouched:
|
|
91
|
+
When changing body content, replace the **entire** body span — not just one line inside it. Patching one line leaves the rest of the body stale.
|
|
155
92
|
```
|
|
156
93
|
{
|
|
157
94
|
path: "util.ts",
|
|
@@ -161,7 +98,7 @@ Good — shape (a): replace the full body span. Header and closer stay untouched
|
|
|
161
98
|
end: {{hlineref 16 "\t\treturn null;"}},
|
|
162
99
|
lines: [
|
|
163
100
|
"\t\tif (isEnoent(err)) return null;",
|
|
164
|
-
"\t\
|
|
101
|
+
"\t\tthrow err;"
|
|
165
102
|
]
|
|
166
103
|
}]
|
|
167
104
|
}
|
|
@@ -205,46 +142,18 @@ Good — `end` includes the function's own `}` on line 18, so the old closer is
|
|
|
205
142
|
```
|
|
206
143
|
</example>
|
|
207
144
|
|
|
208
|
-
<example name="
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
{
|
|
212
|
-
path: "util.ts",
|
|
213
|
-
edits: [{
|
|
214
|
-
op: "prepend",
|
|
215
|
-
pos: {{hlineref 9 "function beta() {"}},
|
|
216
|
-
lines: [
|
|
217
|
-
"function gamma() {",
|
|
218
|
-
"\tvalidate();",
|
|
219
|
-
"}",
|
|
220
|
-
""
|
|
221
|
-
]
|
|
222
|
-
}]
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
Use a trailing `""` to preserve the blank line between sibling declarations.
|
|
226
|
-
</example>
|
|
145
|
+
<example name="avoid shared boundary lines">
|
|
146
|
+
Do not anchor `replace` on a mixed boundary line such as `} catch (err) {`, `} else {`, `}),`, or `},{`. Those lines belong to two adjacent structures at once.
|
|
227
147
|
|
|
228
|
-
|
|
229
|
-
When inserting a sibling declaration, do not anchor on the previous block's lone closing brace. Anchor on the next declaration instead.
|
|
148
|
+
Bad — if you need to change code on both sides of that line, replacing just the boundary span will usually leave one side's syntax behind.
|
|
230
149
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
lines: [
|
|
239
|
-
"",
|
|
240
|
-
"function gamma() {",
|
|
241
|
-
"\tvalidate();",
|
|
242
|
-
"}"
|
|
243
|
-
]
|
|
244
|
-
}]
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
Good — prepend before the next declaration so the new sibling is anchored on a declaration header, not a block tail:
|
|
150
|
+
Good — choose one of two safe shapes instead:
|
|
151
|
+
- move inward and replace only body-owned lines
|
|
152
|
+
- expand outward and replace one whole owned block, consuming its real closer/separator too
|
|
153
|
+
</example>
|
|
154
|
+
|
|
155
|
+
<example name="insert between sibling declarations">
|
|
156
|
+
Add a `gamma()` function between `alpha()` and `beta()`. Use `prepend` on the next declaration — not `append` on the previous block's closing brace — so the anchor is a stable declaration boundary.
|
|
248
157
|
```
|
|
249
158
|
{
|
|
250
159
|
path: "util.ts",
|
|
@@ -260,6 +169,7 @@ Good — prepend before the next declaration so the new sibling is anchored on a
|
|
|
260
169
|
}]
|
|
261
170
|
}
|
|
262
171
|
```
|
|
172
|
+
Use a trailing `""` to preserve the blank line between sibling declarations.
|
|
263
173
|
</example>
|
|
264
174
|
</examples>
|
|
265
175
|
|
|
@@ -271,5 +181,10 @@ Good — prepend before the next declaration so the new sibling is anchored on a
|
|
|
271
181
|
- When changing existing code near a block tail or closing delimiter, default to `replace` over the owned span instead of inserting around the boundary.
|
|
272
182
|
- When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace.
|
|
273
183
|
- **Block boundaries travel together.** For a block `{ header / body / closer }`, there are exactly two valid replace shapes: (a) replace only the body — `pos`=first body line, `end`=last body line, leave the header and closer untouched; or (b) replace the whole block — `pos`=header, `end`=closer, re-emit all three in `lines`. Never split them: do not set `end` to the closer while omitting it from `lines` (deletes it), and do not emit the closer in `lines` without including it in `end` (duplicates it). This applies to every block terminator: `}`, `continue`, `break`, `return`, `throw`.
|
|
184
|
+
- **Never target shared boundary lines.** Do not use `replace` spans that start, end, or pivot on a line that closes one construct and opens/separates another, such as `},{`, `}),`, `} else {`, or `} catch (err) {`. Those lines are not owned by a single block. Move the range inward to body-only lines, or widen it to consume one whole owned construct including its true trailing delimiter.
|
|
185
|
+
- **`lines` must not extend past `end`.** `lines` replaces exactly `pos..end`. Content after `end` survives. If you include lines in `lines` that exist after `end`, they will appear twice. Either extend `end` to cover all lines you are re-emitting, or remove the extra lines from `lines`.
|
|
274
186
|
- `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use a real tab character.
|
|
187
|
+
- After any successful `edit` call on a file, the next change to that same file **MUST** start with a fresh `read`. Do not chain a second `edit` call off stale mental state, even if the intended range is nearby.
|
|
188
|
+
- If you need a second change in the same local region, default to one wider `replace` over the whole owned block instead of a sequence of micro-edits on adjacent lines. Repeated small patches in a moving region are unstable.
|
|
189
|
+
- If a local region is already malformed or a prior patch partially landed, stop nibbling at it. Re-read the file and replace the full owned block from a stable boundary; for a small file, prefer rewriting the file over stacking more tiny repairs.
|
|
275
190
|
</critical>
|
|
@@ -4,10 +4,10 @@ Reads files from local filesystem or internal URLs.
|
|
|
4
4
|
- Reads up to {{DEFAULT_LIMIT}} lines default
|
|
5
5
|
- Use `offset` and `limit` for large files; max {{DEFAULT_MAX_LINES}} lines per call
|
|
6
6
|
{{#if IS_HASHLINE_MODE}}
|
|
7
|
-
-
|
|
7
|
+
- Filesystem output is CID prefixed: `LINE#ID:content`
|
|
8
8
|
{{else}}
|
|
9
9
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
10
|
-
-
|
|
10
|
+
- Filesystem output is line-number-prefixed
|
|
11
11
|
{{/if}}
|
|
12
12
|
{{/if}}
|
|
13
13
|
- Supports images (PNG, JPG) and PDFs
|
|
@@ -46,6 +46,11 @@ Create a todo list when:
|
|
|
46
46
|
- Multiple ops can be batched in one call (e.g., complete current + start next)
|
|
47
47
|
</protocol>
|
|
48
48
|
|
|
49
|
+
## Task Anatomy
|
|
50
|
+
- `content`: Short label (5-10 words). What is being done, not how.
|
|
51
|
+
- `details`: File paths, implementation steps, edge cases. Shown only when task is active.
|
|
52
|
+
- `notes`: Runtime observations added during execution.
|
|
53
|
+
|
|
49
54
|
<avoid>
|
|
50
55
|
- Single-step tasks — act directly
|
|
51
56
|
- Conversational or informational requests
|
|
@@ -65,11 +70,16 @@ ops: [
|
|
|
65
70
|
]
|
|
66
71
|
</example>
|
|
67
72
|
|
|
73
|
+
<example name="add_task">
|
|
74
|
+
Add a follow-up task with implementation specifics in `details`:
|
|
75
|
+
ops: [{op: "add_task", phase: "Implementation", after: "task-2", task: {content: "Handle retries", details: "Update retry.ts to cap exponential backoff and preserve AbortSignal handling", status: "pending"}}]
|
|
76
|
+
</example>
|
|
77
|
+
|
|
68
78
|
<example name="initial-setup">
|
|
69
79
|
Replace is for setup only. Prefer add_phase / add_task for incremental additions.
|
|
70
80
|
ops: [{op: "replace", phases: [
|
|
71
81
|
{name: "Investigation", tasks: [{content: "Read source"}, {content: "Map callsites"}]},
|
|
72
|
-
{name: "Implementation", tasks: [{content: "Apply fix"}, {content: "Run tests"}]}
|
|
82
|
+
{name: "Implementation", tasks: [{content: "Apply fix", details: "Update parser.ts to handle edge case in line 42"}, {content: "Run tests"}]}
|
|
73
83
|
]}]
|
|
74
84
|
</example>
|
|
75
85
|
|
package/src/sdk.ts
CHANGED
|
@@ -89,10 +89,13 @@ import {
|
|
|
89
89
|
GrepTool,
|
|
90
90
|
getSearchTools,
|
|
91
91
|
HIDDEN_TOOLS,
|
|
92
|
+
isCodeSearchProviderId,
|
|
93
|
+
isSearchProviderPreference,
|
|
92
94
|
loadSshTool,
|
|
93
95
|
PythonTool,
|
|
94
96
|
ReadTool,
|
|
95
97
|
ResolveTool,
|
|
98
|
+
setPreferredCodeSearchProvider,
|
|
96
99
|
setPreferredImageProvider,
|
|
97
100
|
setPreferredSearchProvider,
|
|
98
101
|
type Tool,
|
|
@@ -628,8 +631,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
628
631
|
options.skills === undefined ? discoverSkills(cwd, agentDir, skillsSettings) : undefined;
|
|
629
632
|
|
|
630
633
|
// Initialize provider preferences from settings
|
|
631
|
-
|
|
632
|
-
|
|
634
|
+
const webSearchProvider = settings.get("providers.webSearch");
|
|
635
|
+
if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
|
|
636
|
+
setPreferredSearchProvider(webSearchProvider);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const codeSearchProvider = settings.get("providers.codeSearch");
|
|
640
|
+
if (typeof codeSearchProvider === "string" && isCodeSearchProviderId(codeSearchProvider)) {
|
|
641
|
+
setPreferredCodeSearchProvider(codeSearchProvider);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const imageProvider = settings.get("providers.image");
|
|
645
|
+
if (imageProvider === "auto" || imageProvider === "gemini" || imageProvider === "openrouter") {
|
|
646
|
+
setPreferredImageProvider(imageProvider);
|
|
647
|
+
}
|
|
633
648
|
|
|
634
649
|
const sessionManager = options.sessionManager ?? logger.time("sessionManager", SessionManager.create, cwd);
|
|
635
650
|
const sessionId = sessionManager.getSessionId();
|
|
@@ -962,19 +977,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
962
977
|
customTools.push(...(geminiImageTools as unknown as CustomTool[]));
|
|
963
978
|
}
|
|
964
979
|
|
|
965
|
-
// Add
|
|
966
|
-
|
|
967
|
-
if (exaSettings.enabled && exaSettings.enableSearch) {
|
|
968
|
-
const exaSearchTools = await logger.timeAsync("getSearchTools", getSearchTools, {
|
|
969
|
-
enableLinkedin: exaSettings.enableLinkedin as boolean,
|
|
970
|
-
enableCompany: exaSettings.enableCompany as boolean,
|
|
971
|
-
});
|
|
972
|
-
// Filter out the base web_search (already in built-in tools), add specialized Exa tools
|
|
973
|
-
const specializedTools = exaSearchTools.filter(t => t.name !== "web_search");
|
|
974
|
-
if (specializedTools.length > 0) {
|
|
975
|
-
customTools.push(...specializedTools);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
980
|
+
// Add web search tools
|
|
981
|
+
customTools.push(...getSearchTools());
|
|
978
982
|
|
|
979
983
|
// Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
|
|
980
984
|
const builtInToolNames = builtinTools.map(t => t.name);
|
|
@@ -1409,7 +1413,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1409
1413
|
if (pendingActionStore.hasPending) {
|
|
1410
1414
|
return { type: "function", name: "resolve" };
|
|
1411
1415
|
}
|
|
1412
|
-
return
|
|
1416
|
+
return session?.consumeNextToolChoiceOverride();
|
|
1413
1417
|
},
|
|
1414
1418
|
});
|
|
1415
1419
|
cursorEventEmitter = event => agent.emitExternalEvent(event);
|
|
@@ -89,6 +89,7 @@ import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
|
89
89
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
90
90
|
import type { PlanModeState } from "../plan-mode/state";
|
|
91
91
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
92
|
+
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
92
93
|
import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
|
|
93
94
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
94
95
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
@@ -106,6 +107,7 @@ import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from ".
|
|
|
106
107
|
import { parseCommandArgs } from "../utils/command-args";
|
|
107
108
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
108
109
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
110
|
+
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
109
111
|
import {
|
|
110
112
|
type CompactionResult,
|
|
111
113
|
calculateContextTokens,
|
|
@@ -350,6 +352,7 @@ export class AgentSession {
|
|
|
350
352
|
// Todo completion reminder state
|
|
351
353
|
#todoReminderCount = 0;
|
|
352
354
|
#todoPhases: TodoPhase[] = [];
|
|
355
|
+
#nextToolChoiceOverride: ToolChoice | undefined = undefined;
|
|
353
356
|
|
|
354
357
|
// Bash execution state
|
|
355
358
|
#bashAbortController: AbortController | undefined = undefined;
|
|
@@ -457,6 +460,12 @@ export class AgentSession {
|
|
|
457
460
|
return this.#modelRegistry;
|
|
458
461
|
}
|
|
459
462
|
|
|
463
|
+
consumeNextToolChoiceOverride(): ToolChoice | undefined {
|
|
464
|
+
const toolChoice = this.#nextToolChoiceOverride;
|
|
465
|
+
this.#nextToolChoiceOverride = undefined;
|
|
466
|
+
return toolChoice;
|
|
467
|
+
}
|
|
468
|
+
|
|
460
469
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
461
470
|
get providerSessionState(): Map<string, ProviderSessionState> {
|
|
462
471
|
return this.#providerSessionState;
|
|
@@ -791,7 +800,11 @@ export class AgentSession {
|
|
|
791
800
|
const compactionTask = this.#checkCompaction(msg);
|
|
792
801
|
this.#trackPostPromptTask(compactionTask);
|
|
793
802
|
await compactionTask;
|
|
794
|
-
// Check for incomplete todos
|
|
803
|
+
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
804
|
+
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
805
|
+
if (hasToolCalls) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
795
808
|
if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
796
809
|
if (this.#enforceRewindBeforeYield()) {
|
|
797
810
|
return;
|
|
@@ -1349,6 +1362,7 @@ export class AgentSession {
|
|
|
1349
1362
|
if (!this.#extensionRunner) return;
|
|
1350
1363
|
if (event.type === "agent_start") {
|
|
1351
1364
|
this.#turnIndex = 0;
|
|
1365
|
+
this.#nextToolChoiceOverride = undefined;
|
|
1352
1366
|
await this.#extensionRunner.emit({ type: "agent_start" });
|
|
1353
1367
|
} else if (event.type === "agent_end") {
|
|
1354
1368
|
await this.#extensionRunner.emit({ type: "agent_end", messages: event.messages });
|
|
@@ -1945,6 +1959,8 @@ export class AgentSession {
|
|
|
1945
1959
|
return;
|
|
1946
1960
|
}
|
|
1947
1961
|
|
|
1962
|
+
const eagerTodoPrelude = !options?.synthetic ? this.#createEagerTodoPrelude() : undefined;
|
|
1963
|
+
|
|
1948
1964
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
1949
1965
|
if (options?.images) {
|
|
1950
1966
|
userContent.push(...options.images);
|
|
@@ -1954,7 +1970,20 @@ export class AgentSession {
|
|
|
1954
1970
|
? { role: "developer" as const, content: userContent, attribution: "agent" as const, timestamp: Date.now() }
|
|
1955
1971
|
: { role: "user" as const, content: userContent, attribution: "user" as const, timestamp: Date.now() };
|
|
1956
1972
|
|
|
1957
|
-
|
|
1973
|
+
if (eagerTodoPrelude) {
|
|
1974
|
+
this.#nextToolChoiceOverride = eagerTodoPrelude.toolChoice;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
try {
|
|
1978
|
+
await this.#promptWithMessage(message, expandedText, {
|
|
1979
|
+
...options,
|
|
1980
|
+
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
1981
|
+
});
|
|
1982
|
+
} finally {
|
|
1983
|
+
if (eagerTodoPrelude) {
|
|
1984
|
+
this.#nextToolChoiceOverride = undefined;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1958
1987
|
if (!options?.synthetic) {
|
|
1959
1988
|
await this.#enforcePlanModeToolDecision();
|
|
1960
1989
|
}
|
|
@@ -1997,6 +2026,7 @@ export class AgentSession {
|
|
|
1997
2026
|
message: AgentMessage,
|
|
1998
2027
|
expandedText: string,
|
|
1999
2028
|
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
2029
|
+
prependMessages?: AgentMessage[];
|
|
2000
2030
|
skipPostPromptRecoveryWait?: boolean;
|
|
2001
2031
|
},
|
|
2002
2032
|
): Promise<void> {
|
|
@@ -2034,7 +2064,7 @@ export class AgentSession {
|
|
|
2034
2064
|
await this.#checkCompaction(lastAssistant, false);
|
|
2035
2065
|
}
|
|
2036
2066
|
|
|
2037
|
-
// Build messages array (
|
|
2067
|
+
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
2038
2068
|
const messages: AgentMessage[] = [];
|
|
2039
2069
|
const planReferenceMessage = await this.#buildPlanReferenceMessage?.();
|
|
2040
2070
|
if (planReferenceMessage) {
|
|
@@ -2044,6 +2074,9 @@ export class AgentSession {
|
|
|
2044
2074
|
if (planModeMessage) {
|
|
2045
2075
|
messages.push(planModeMessage);
|
|
2046
2076
|
}
|
|
2077
|
+
if (options?.prependMessages) {
|
|
2078
|
+
messages.push(...options.prependMessages);
|
|
2079
|
+
}
|
|
2047
2080
|
|
|
2048
2081
|
messages.push(message);
|
|
2049
2082
|
|
|
@@ -3481,6 +3514,51 @@ export class AgentSession {
|
|
|
3481
3514
|
this.agent.setTools(previousTools);
|
|
3482
3515
|
}
|
|
3483
3516
|
}
|
|
3517
|
+
|
|
3518
|
+
#createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
|
|
3519
|
+
const eagerTodosEnabled = this.settings.get("todo.eager");
|
|
3520
|
+
const todosEnabled = this.settings.get("todo.enabled");
|
|
3521
|
+
if (!eagerTodosEnabled || !todosEnabled) {
|
|
3522
|
+
return undefined;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
if (this.#planModeState?.enabled) {
|
|
3526
|
+
return undefined;
|
|
3527
|
+
}
|
|
3528
|
+
if (this.getTodoPhases().length > 0) {
|
|
3529
|
+
return undefined;
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
if (!this.#toolRegistry.has("todo_write")) {
|
|
3533
|
+
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
3534
|
+
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
3535
|
+
});
|
|
3536
|
+
return undefined;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
const todoWriteToolChoice = buildNamedToolChoice("todo_write", this.model);
|
|
3540
|
+
if (!todoWriteToolChoice) {
|
|
3541
|
+
logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo_write", {
|
|
3542
|
+
modelApi: this.model?.api,
|
|
3543
|
+
modelId: this.model?.id,
|
|
3544
|
+
});
|
|
3545
|
+
return undefined;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt);
|
|
3549
|
+
|
|
3550
|
+
return {
|
|
3551
|
+
message: {
|
|
3552
|
+
role: "custom",
|
|
3553
|
+
customType: "eager-todo-prelude",
|
|
3554
|
+
content: eagerTodoReminder,
|
|
3555
|
+
display: false,
|
|
3556
|
+
attribution: "agent",
|
|
3557
|
+
timestamp: Date.now(),
|
|
3558
|
+
},
|
|
3559
|
+
toolChoice: todoWriteToolChoice,
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3484
3562
|
/**
|
|
3485
3563
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
3486
3564
|
*/
|
|
@@ -5191,8 +5269,8 @@ export class AgentSession {
|
|
|
5191
5269
|
}
|
|
5192
5270
|
|
|
5193
5271
|
for (const msg of this.messages) {
|
|
5194
|
-
if (msg.role === "user") {
|
|
5195
|
-
lines.push("## User\n");
|
|
5272
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
5273
|
+
lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
|
|
5196
5274
|
if (typeof msg.content === "string") {
|
|
5197
5275
|
lines.push(msg.content);
|
|
5198
5276
|
} else {
|
|
@@ -5316,8 +5394,8 @@ export class AgentSession {
|
|
|
5316
5394
|
lines.push("");
|
|
5317
5395
|
|
|
5318
5396
|
for (const msg of this.messages) {
|
|
5319
|
-
if (msg.role === "user") {
|
|
5320
|
-
lines.push("## User");
|
|
5397
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
5398
|
+
lines.push(msg.role === "developer" ? "## Developer" : "## User");
|
|
5321
5399
|
lines.push("");
|
|
5322
5400
|
if (typeof msg.content === "string") {
|
|
5323
5401
|
lines.push(msg.content);
|
|
@@ -36,16 +36,14 @@ export interface OutputSinkOptions {
|
|
|
36
36
|
|
|
37
37
|
export interface TruncationResult {
|
|
38
38
|
content: string;
|
|
39
|
-
truncated
|
|
40
|
-
truncatedBy
|
|
39
|
+
truncated?: boolean;
|
|
40
|
+
truncatedBy?: "lines" | "bytes";
|
|
41
41
|
totalLines: number;
|
|
42
42
|
totalBytes: number;
|
|
43
|
-
outputLines
|
|
44
|
-
outputBytes
|
|
45
|
-
lastLinePartial
|
|
46
|
-
firstLineExceedsLimit
|
|
47
|
-
maxLines: number;
|
|
48
|
-
maxBytes: number;
|
|
43
|
+
outputLines?: number;
|
|
44
|
+
outputBytes?: number;
|
|
45
|
+
lastLinePartial?: boolean;
|
|
46
|
+
firstLineExceedsLimit?: boolean;
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
export interface TruncationOptions {
|
|
@@ -206,26 +204,10 @@ export function truncateLine(
|
|
|
206
204
|
// =============================================================================
|
|
207
205
|
|
|
208
206
|
/** Shared helper to build a no-truncation result. */
|
|
209
|
-
function noTruncResult(
|
|
210
|
-
content
|
|
211
|
-
|
|
212
|
-
totalBytes
|
|
213
|
-
maxLines: number,
|
|
214
|
-
maxBytes: number,
|
|
215
|
-
): TruncationResult {
|
|
216
|
-
return {
|
|
217
|
-
content,
|
|
218
|
-
truncated: false,
|
|
219
|
-
truncatedBy: null,
|
|
220
|
-
totalLines,
|
|
221
|
-
totalBytes,
|
|
222
|
-
outputLines: totalLines,
|
|
223
|
-
outputBytes: totalBytes,
|
|
224
|
-
lastLinePartial: false,
|
|
225
|
-
firstLineExceedsLimit: false,
|
|
226
|
-
maxLines,
|
|
227
|
-
maxBytes,
|
|
228
|
-
};
|
|
207
|
+
export function noTruncResult(content: string, totalLines?: number, totalBytes?: number): TruncationResult {
|
|
208
|
+
if (totalLines == null) totalLines = countNewlines(content) + 1;
|
|
209
|
+
if (totalBytes == null) totalBytes = Buffer.byteLength(content, "utf-8");
|
|
210
|
+
return { content, totalLines, totalBytes };
|
|
229
211
|
}
|
|
230
212
|
|
|
231
213
|
/**
|
|
@@ -244,7 +226,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
244
226
|
const totalLines = countNewlines(content) + 1;
|
|
245
227
|
|
|
246
228
|
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
247
|
-
return noTruncResult(content, totalLines, totalBytes
|
|
229
|
+
return noTruncResult(content, totalLines, totalBytes);
|
|
248
230
|
}
|
|
249
231
|
|
|
250
232
|
let includedLines = 0;
|
|
@@ -283,8 +265,6 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
283
265
|
outputBytes: 0,
|
|
284
266
|
lastLinePartial: false,
|
|
285
267
|
firstLineExceedsLimit: true,
|
|
286
|
-
maxLines,
|
|
287
|
-
maxBytes,
|
|
288
268
|
};
|
|
289
269
|
}
|
|
290
270
|
break;
|
|
@@ -307,8 +287,6 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
307
287
|
outputBytes: 0,
|
|
308
288
|
lastLinePartial: false,
|
|
309
289
|
firstLineExceedsLimit: true,
|
|
310
|
-
maxLines,
|
|
311
|
-
maxBytes,
|
|
312
290
|
};
|
|
313
291
|
}
|
|
314
292
|
break;
|
|
@@ -335,8 +313,6 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
|
|
335
313
|
outputBytes: bytesUsed,
|
|
336
314
|
lastLinePartial: false,
|
|
337
315
|
firstLineExceedsLimit: false,
|
|
338
|
-
maxLines,
|
|
339
|
-
maxBytes,
|
|
340
316
|
};
|
|
341
317
|
}
|
|
342
318
|
|
|
@@ -354,7 +330,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
354
330
|
const totalLines = countNewlines(content) + 1;
|
|
355
331
|
|
|
356
332
|
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
357
|
-
return noTruncResult(content, totalLines, totalBytes
|
|
333
|
+
return noTruncResult(content, totalLines, totalBytes);
|
|
358
334
|
}
|
|
359
335
|
|
|
360
336
|
let includedLines = 0;
|
|
@@ -396,8 +372,6 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
396
372
|
outputBytes: tail.bytes,
|
|
397
373
|
lastLinePartial: true,
|
|
398
374
|
firstLineExceedsLimit: false,
|
|
399
|
-
maxLines,
|
|
400
|
-
maxBytes,
|
|
401
375
|
};
|
|
402
376
|
}
|
|
403
377
|
break;
|
|
@@ -420,8 +394,6 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
420
394
|
outputBytes: tail.bytes,
|
|
421
395
|
lastLinePartial: true,
|
|
422
396
|
firstLineExceedsLimit: false,
|
|
423
|
-
maxLines,
|
|
424
|
-
maxBytes,
|
|
425
397
|
};
|
|
426
398
|
}
|
|
427
399
|
break;
|
|
@@ -447,8 +419,6 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
447
419
|
outputBytes: bytesUsed,
|
|
448
420
|
lastLinePartial: false,
|
|
449
421
|
firstLineExceedsLimit: false,
|
|
450
|
-
maxLines,
|
|
451
|
-
maxBytes,
|
|
452
422
|
};
|
|
453
423
|
}
|
|
454
424
|
|
|
@@ -693,7 +663,7 @@ export function formatTailTruncationNotice(
|
|
|
693
663
|
if (!truncation.truncated) return "";
|
|
694
664
|
|
|
695
665
|
const { fullOutputPath, originalContent, suffix = "" } = options;
|
|
696
|
-
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
666
|
+
const startLine = truncation.totalLines - (truncation.outputLines ?? truncation.totalLines) + 1;
|
|
697
667
|
const endLine = truncation.totalLines;
|
|
698
668
|
const fullOutputPart = fullOutputPath ? `. Full output: ${fullOutputPath}` : "";
|
|
699
669
|
|
|
@@ -705,11 +675,9 @@ export function formatTailTruncationNotice(
|
|
|
705
675
|
const lastLine = lastNl === -1 ? originalContent : originalContent.substring(lastNl + 1);
|
|
706
676
|
lastLineSizePart = ` (line is ${formatBytes(Buffer.byteLength(lastLine, "utf-8"))})`;
|
|
707
677
|
}
|
|
708
|
-
notice = `[Showing last ${formatBytes(truncation.outputBytes)} of line ${endLine}${lastLineSizePart}${fullOutputPart}${suffix}]`;
|
|
709
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
710
|
-
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputPart}${suffix}]`;
|
|
678
|
+
notice = `[Showing last ${formatBytes(truncation.outputBytes ?? truncation.totalBytes)} of line ${endLine}${lastLineSizePart}${fullOutputPart}${suffix}]`;
|
|
711
679
|
} else {
|
|
712
|
-
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}
|
|
680
|
+
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputPart}${suffix}]`;
|
|
713
681
|
}
|
|
714
682
|
|
|
715
683
|
return `\n\n${notice}`;
|
|
@@ -727,13 +695,8 @@ export function formatHeadTruncationNotice(
|
|
|
727
695
|
|
|
728
696
|
const startLineDisplay = options.startLine ?? 1;
|
|
729
697
|
const totalFileLines = options.totalFileLines ?? truncation.totalLines;
|
|
730
|
-
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
698
|
+
const endLineDisplay = startLineDisplay + (truncation.outputLines ?? truncation.totalLines) - 1;
|
|
731
699
|
const nextOffset = endLineDisplay + 1;
|
|
732
|
-
|
|
733
|
-
const notice =
|
|
734
|
-
truncation.truncatedBy === "lines"
|
|
735
|
-
? `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`
|
|
736
|
-
: `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatBytes(truncation.maxBytes)} limit). Use offset=${nextOffset} to continue]`;
|
|
737
|
-
|
|
700
|
+
const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
738
701
|
return `\n\n${notice}`;
|
|
739
702
|
}
|