@oh-my-pi/pi-coding-agent 14.5.2 → 14.5.5
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 +70 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +104 -6
- package/src/config/settings-schema.ts +14 -13
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +4 -4
- package/src/edit/index.ts +111 -109
- package/src/edit/line-hash.ts +33 -3
- package/src/edit/modes/apply-patch.ts +6 -4
- package/src/edit/modes/atom.lark +27 -0
- package/src/edit/modes/atom.ts +1094 -642
- package/src/edit/modes/hashline.ts +9 -10
- package/src/edit/modes/patch.ts +23 -19
- package/src/edit/modes/replace.ts +19 -15
- package/src/edit/renderer.ts +65 -8
- package/src/edit/streaming.ts +47 -77
- package/src/extensibility/extensions/types.ts +11 -11
- package/src/extensibility/hooks/types.ts +6 -6
- package/src/lsp/edits.ts +8 -5
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +13 -43
- package/src/mcp/discoverable-tool-metadata.ts +1 -1
- package/src/mcp/manager.ts +3 -3
- package/src/mcp/tool-bridge.ts +4 -4
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-defs.ts +3 -3
- package/src/modes/components/tree-selector.ts +2 -2
- package/src/modes/controllers/event-controller.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +31 -7
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/system-prompt.md +34 -31
- package/src/prompts/tools/apply-patch.md +0 -2
- package/src/prompts/tools/atom.md +88 -97
- package/src/prompts/tools/bash.md +7 -4
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/find.md +6 -1
- package/src/prompts/tools/hashline.md +10 -11
- package/src/prompts/tools/patch.md +13 -13
- package/src/prompts/tools/read.md +5 -5
- package/src/prompts/tools/replace.md +3 -3
- package/src/prompts/tools/{grep.md → search.md} +4 -4
- package/src/sdk.ts +19 -9
- package/src/session/agent-session.ts +69 -1
- package/src/system-prompt.ts +15 -5
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +10 -1
- package/src/tools/ast-edit.ts +27 -50
- package/src/tools/ast-grep.ts +22 -48
- package/src/tools/bash.ts +1 -1
- package/src/tools/file-recorder.ts +6 -6
- package/src/tools/find.ts +11 -13
- package/src/tools/grouped-file-output.ts +96 -0
- package/src/tools/index.ts +7 -7
- package/src/tools/path-utils.ts +31 -4
- package/src/tools/read.ts +12 -6
- package/src/tools/renderers.ts +2 -2
- package/src/tools/{grep.ts → search.ts} +43 -86
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/write.ts +8 -4
- package/src/web/search/index.ts +1 -1
|
@@ -1,103 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</operations>
|
|
29
|
-
|
|
30
|
-
<examples>
|
|
31
|
-
All examples below reference the same file:
|
|
32
|
-
|
|
33
|
-
```ts title="a.ts"
|
|
34
|
-
{{hline 1 "const tag = \"BAD\";"}}
|
|
1
|
+
Your patch language is a compact, file-oriented edit format.
|
|
2
|
+
|
|
3
|
+
When emitting a patch, the first non-blank line **MUST** be `---PATH`.
|
|
4
|
+
A Lid is the anchor emitted in read/grep etc. (line number + id, e.g. `5th`).
|
|
5
|
+
|
|
6
|
+
<ops>
|
|
7
|
+
---PATH start editing PATH with cursor at EOF
|
|
8
|
+
!rm delete PATH
|
|
9
|
+
!mv X move file to X
|
|
10
|
+
$ move cursor to BOF
|
|
11
|
+
^ move cursor to EOF
|
|
12
|
+
@Lid move cursor after Lid
|
|
13
|
+
+X insert X at the cursor; `+` alone inserts a blank line
|
|
14
|
+
Lid=X replace whole line with X; `Lid=` blanks it out
|
|
15
|
+
-Lid delete line (repeat for multi)
|
|
16
|
+
</ops>
|
|
17
|
+
|
|
18
|
+
<rules>
|
|
19
|
+
- You may have multiple `---PATH` sections to edit multiple files at once.
|
|
20
|
+
- Ops starting with `$` / `^` / `@Lid` do not alter lines; you must still issue an op like `+` afterwards.
|
|
21
|
+
- Consecutive `+X` ops insert consecutive lines.
|
|
22
|
+
- `Lid=X` replaces the whole line. X must be the complete new line, not a fragment.
|
|
23
|
+
- To rewrite multiple adjacent lines, delete each with `-Lid` then emit the new content as `+TEXT` lines. Do not stack `Lid=X` over a contiguous range — it requires the new block to match the old line count and silently corrupts the file when they differ.
|
|
24
|
+
</rules>
|
|
25
|
+
|
|
26
|
+
<case file="a.ts">
|
|
27
|
+
{{hline 1 "const DEF = \"guest\";"}}
|
|
35
28
|
{{hline 2 ""}}
|
|
36
|
-
{{hline 3 "function
|
|
37
|
-
{{hline 4 "\
|
|
38
|
-
{{hline 5 "\
|
|
39
|
-
{{hline 6 "
|
|
40
|
-
|
|
41
|
-
{{hline 8 "}"}}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
# Replace a line with `splice`
|
|
45
|
-
`{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},splice:["const tag = \"OK\";"]}]}`
|
|
46
|
-
|
|
47
|
-
# Combine `pre` + `splice` + `post` in one entry
|
|
48
|
-
`{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],splice:["\tif (!x) {"],post:["\t\tlog();"]}]}`
|
|
29
|
+
{{hline 3 "export function label(name) {"}}
|
|
30
|
+
{{hline 4 "\tconst clean = name || DEF;"}}
|
|
31
|
+
{{hline 5 "\treturn clean.trim();"}}
|
|
32
|
+
{{hline 6 "}"}}
|
|
33
|
+
</case>
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
35
|
+
<examples>
|
|
36
|
+
# Replace line
|
|
37
|
+
---a.ts
|
|
38
|
+
{{hrefr 5}}= return clean.trim().toUpperCase();
|
|
39
|
+
|
|
40
|
+
# Rewrite multiple adjacent lines (delete the old, insert the new)
|
|
41
|
+
---a.ts
|
|
42
|
+
-{{hrefr 3}}
|
|
43
|
+
-{{hrefr 4}}
|
|
44
|
+
-{{hrefr 5}}
|
|
45
|
+
-{{hrefr 6}}
|
|
46
|
+
+export function label(name: string): string {
|
|
47
|
+
+ return (name || DEF).trim().toUpperCase();
|
|
48
|
+
+}
|
|
49
|
+
|
|
50
|
+
# Append after
|
|
51
|
+
---a.ts
|
|
52
|
+
@{{hrefr 4}}
|
|
53
|
+
+ const suffix = "";
|
|
54
|
+
|
|
55
|
+
# Delete a line
|
|
56
|
+
---a.ts
|
|
57
|
+
-{{hrefr 2}}
|
|
58
|
+
|
|
59
|
+
# Prepend and append
|
|
60
|
+
---a.ts
|
|
61
|
+
$
|
|
62
|
+
+// Copyright (c) 2026
|
|
63
|
+
+
|
|
64
|
+
^
|
|
65
|
+
+export { DEF };
|
|
66
|
+
|
|
67
|
+
# File ops
|
|
68
|
+
---a.ts
|
|
69
|
+
!rm
|
|
70
|
+
---b.ts
|
|
71
|
+
!mv a.ts
|
|
72
|
+
|
|
73
|
+
# Wrong: `@Lid=TEXT` is not replacement syntax
|
|
74
|
+
---a.ts
|
|
75
|
+
@{{hrefr 5}}= return clean.trim().toUpperCase();
|
|
76
|
+
|
|
77
|
+
# Wrong: do not split `Lid=TEXT` across lines
|
|
78
|
+
---a.ts
|
|
79
|
+
{{hrefr 5}}=
|
|
80
|
+
return clean.trim().toUpperCase();
|
|
81
|
+
|
|
82
|
+
# Wrong: do not replace by deleting then adding
|
|
83
|
+
---a.ts
|
|
84
|
+
-{{hrefr 5}}
|
|
85
|
+
+{{hrefr 5}}= return clean.trim().toUpperCase();
|
|
85
86
|
</examples>
|
|
86
87
|
|
|
87
88
|
<critical>
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
- `
|
|
93
|
-
- `splice: []` deletes the anchored line. `splice:[""]` preserves a blank line.
|
|
94
|
-
- Within a single request you may submit edits in any order — the runtime applies them bottom-up so they don't shift each other. After any request that mutates a file, anchors below the mutation are stale on disk; re-read before issuing more edits to that file.
|
|
95
|
-
- `splice` operations target the current file content only. Do not try to reference old line text after the file has changed.
|
|
96
|
-
- For **small** in-line edits (renaming a token, flipping an operator, tweaking a literal), prefer `sed` over `splice`. The `loc` anchor already pins the line — repeating the entire line in a `splice` array invites hallucinated content. Use the smallest `pat` that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe. When `pat` contains regex metacharacters you mean literally (e.g. `||`, `.`, `(`, `?`, `\`), set `F:true` to disable regex. `g` is `false` by default — pass `g:true` to replace every occurrence. For multi-line restructuring (wrapping logic, adding new branches, inserting blocks), use `splice`/`pre`/`post` — do **not** stretch `sed` into a rewrite tool.
|
|
97
|
-
- When you do use `splice`, re-read the anchored line first and copy it verbatim, changing only the required token(s). Anchor identity does not verify line content, so a hallucinated replacement will silently corrupt the file.
|
|
98
|
-
- Anchors are pin points, not region markers. One anchor pins exactly one line. If your change touches N distinct source lines, that is N edits with N anchors — not one big `splice` array intended to cover the whole region. `splice` cannot "replace lines 4 through 7"; it can only splice content in at one anchor.
|
|
99
|
-
- You **MUST NOT** include lines in `splice`/`pre`/`post` that already exist immediately adjacent to the anchor in the current file. `splice` does not overwrite the lines below — they shift down — so any neighbor you re-type in your array becomes a duplicate. If your intended replacement contains content that is already on neighboring source lines, split into multiple edits at each real change site instead of one fat `splice`.
|
|
100
|
-
- Before issuing a multi-line `splice`, mentally diff each array element against the current file lines at and just below the anchor. Any element that matches a line within ~5 lines of the anchor will become a duplicate after the splice. If you find a match, drop that element and use a separate edit (or `pre`/`post`) at the real change point.
|
|
101
|
-
- Text content must be literal file content with matching indentation. If the file uses tabs, use real tabs.
|
|
102
|
-
- You **MUST NOT** use this tool to reformat or clean up unrelated code.
|
|
89
|
+
- Copy Lids **EXACTLY** from prior tool output. Never guess, shorten, or omit the letters.
|
|
90
|
+
- Only emit lines that change. Never repeat unchanged context — anchors imply it.
|
|
91
|
+
- This is **NOT** unified diff. Never send `@@`, `-OLD` / `+NEW` pairs, or unchanged context.
|
|
92
|
+
- Never split `Lid=TEXT` across two physical lines.
|
|
93
|
+
- Never stack `Lid=X` over a contiguous range. Use `-Lid`+`+TEXT` for block rewrites.
|
|
103
94
|
</critical>
|
|
@@ -29,22 +29,25 @@ Returns output and exit code.
|
|
|
29
29
|
</output>
|
|
30
30
|
|
|
31
31
|
<critical>
|
|
32
|
-
You **MUST
|
|
32
|
+
You **MUST** use specialized tools instead of bash for any file, directory, or text-search operation. Do **NOT** use Bash to run commands when a relevant dedicated tool is provided — dedicated tools are faster, render diffs, respect `.gitignore`, and let the user review your work. Bash commands matching the patterns below are intercepted and blocked at runtime.
|
|
33
33
|
|
|
34
34
|
|Instead of (WRONG)|Use (CORRECT)|
|
|
35
35
|
|---|---|
|
|
36
36
|
|`cat file`, `head -n N file`|`read(path="file", limit=N)`|
|
|
37
37
|
|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`|
|
|
38
|
-
{{#if
|
|
39
|
-
|`grep -rn 'pat' dir/`|`
|
|
40
|
-
|`rg 'pattern' dir/`|`
|
|
38
|
+
{{#if hasSearch}}|`grep -A 20 'pat' file`|`search(pattern="pat", path="file", post=20)`|
|
|
39
|
+
|`grep -rn 'pat' dir/`|`search(pattern="pat", path="dir/")`|
|
|
40
|
+
|`rg 'pattern' dir/`|`search(pattern="pattern", path="dir/")`|{{/if}}
|
|
41
41
|
{{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}}
|
|
42
42
|
|`ls dir/`|`read(path="dir/")`|
|
|
43
43
|
|`cat <<'EOF' > file`|`write(path="file", content="…")`|
|
|
44
44
|
|`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`|
|
|
45
45
|
{{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}}
|
|
46
|
+
- You **MUST NOT** create files with `cat <<EOF`, `echo > file`, or `printf > file`. Use `write` — heredoc content cannot be cached for permission reuse, every revision triggers a fresh review, and there is no diff. This is the most-violated rule.
|
|
47
|
+
- You **MUST NOT** read line ranges with `sed -n 'A,Bp'`, `awk 'NR≥A && NR≤B'`, or `head | tail` pipelines. Use `read` with `offset`/`limit` (or `sel` if available).
|
|
46
48
|
{{#if hasAstGrep}}- You **MUST** use `ast_grep` for structural code search instead of bash `grep`/`awk`/`perl` pipelines{{/if}}
|
|
47
49
|
{{#if hasAstEdit}}- You **MUST** use `ast_edit` for structural rewrites instead of bash `sed`/`awk`/`perl` pipelines{{/if}}
|
|
48
50
|
- You **MUST NOT** use `2>&1` or `2>/dev/null` — stdout and stderr are already merged
|
|
49
51
|
- You **MUST NOT** use `| head -n 50` or `| tail -n 100` — use `head`/`tail` parameters instead
|
|
52
|
+
- If you catch yourself typing `cat`, `head`, `tail`, `less`, `more`, `ls`, `grep`, `rg`, `find`, `fd`, `sed -i`, `awk -i`, or a heredoc redirect inside a Bash call, stop and switch to the dedicated tool. There is no scenario where bash is preferable for these operations.
|
|
50
53
|
</critical>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Creates a context checkpoint before exploratory work so you can later rewind and keep only a concise report.
|
|
2
2
|
|
|
3
|
-
Use this when you need to investigate with many intermediate tool calls (read/
|
|
3
|
+
Use this when you need to investigate with many intermediate tool calls (read/search/find/lsp/etc.) and want to minimize context cost afterward.
|
|
4
4
|
|
|
5
5
|
Rules:
|
|
6
6
|
- You **MUST** call `rewind` before yielding after starting a checkpoint.
|
|
@@ -14,5 +14,10 @@ Matching file paths sorted by modification time (most recent first). Truncated a
|
|
|
14
14
|
</examples>
|
|
15
15
|
|
|
16
16
|
<avoid>
|
|
17
|
-
For open-ended searches requiring multiple rounds of globbing and
|
|
17
|
+
For open-ended searches requiring multiple rounds of globbing and searching, you **MUST** use Task tool instead.
|
|
18
18
|
</avoid>
|
|
19
|
+
|
|
20
|
+
<critical>
|
|
21
|
+
- You **MUST** use the built-in Find tool for every file-name lookup. Do **NOT** shell out to `find`, `fd`, `locate`, `ls`, or `git ls-files` via Bash — they ignore `.gitignore`, blow past result limits, and waste tokens.
|
|
22
|
+
- If you catch yourself typing `find -name`, `fd`, or `ls **/*.ext` in a Bash command, stop and re-issue the lookup through the Find tool with a glob pattern instead.
|
|
23
|
+
</critical>
|
|
@@ -5,10 +5,9 @@ Read the file first. Copy the full anchors exactly as shown by `read`.
|
|
|
5
5
|
<operations>
|
|
6
6
|
**Top level**
|
|
7
7
|
- `edits` — array of edit entries
|
|
8
|
-
- `path` (
|
|
8
|
+
- `path` (required) — file path for all edits in this request
|
|
9
9
|
|
|
10
|
-
**Edit entry**: `{
|
|
11
|
-
- `path` — file path (omit to fall back to the request-level `path`)
|
|
10
|
+
**Edit entry**: `{ loc, content }`
|
|
12
11
|
- `loc` — where to apply the edit (see below)
|
|
13
12
|
- `content` — replacement/inserted lines (`string[]`, one element per line; `null` to delete)
|
|
14
13
|
|
|
@@ -44,24 +43,24 @@ All examples below reference the same file:
|
|
|
44
43
|
|
|
45
44
|
# Replace a block body
|
|
46
45
|
Replace only the catch body. Do not target the shared boundary line `} catch (err) {`.
|
|
47
|
-
`{
|
|
46
|
+
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 15}},end:{{href 16}}}},content:["\t\tif (isEnoent(err)) return null;","\t\tthrow err;"]}]}`
|
|
48
47
|
# Replace whole block including closing brace
|
|
49
|
-
Replace `alpha`'s entire body including the closing `}`. `end` **MUST** be {{href 7
|
|
50
|
-
`{
|
|
51
|
-
**Wrong**: `end: {{href 6
|
|
48
|
+
Replace `alpha`'s entire body including the closing `}`. `end` **MUST** be {{href 7}} because `content` includes `}`.
|
|
49
|
+
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 6}},end:{{href 7}}}},content:["\tvalidate();","\tlog();","}"]}]}`
|
|
50
|
+
**Wrong**: `end: {{href 6}}` — line 7 (`}`) survives AND content emits `}`, producing two closing braces.
|
|
52
51
|
# Replace one line
|
|
53
52
|
Single-line replace uses `pos == end`.
|
|
54
|
-
`{
|
|
53
|
+
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 2}},end:{{href 2}}}},content:["const timeout = 30_000;"]}]}`
|
|
55
54
|
# Delete a range
|
|
56
|
-
`{
|
|
55
|
+
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 10}},end:{{href 11}}}},content:null}]}`
|
|
57
56
|
# Insert before a sibling
|
|
58
57
|
When adding a sibling declaration, prefer `prepend` on the next declaration.
|
|
59
|
-
`{
|
|
58
|
+
`{path:"a.ts",edits:[{loc:{prepend:{{href 9}}},content:["function gamma() {","\tvalidate();","}",""]}]}`
|
|
60
59
|
</examples>
|
|
61
60
|
|
|
62
61
|
<critical>
|
|
63
62
|
- Make the minimum exact edit.
|
|
64
|
-
- Copy the full anchors exactly as shown by `read/
|
|
63
|
+
- Copy the full anchors exactly as shown by `read/search` (for example `160sr`, not just `sr`).
|
|
65
64
|
- `range` requires both `pos` and `end`.
|
|
66
65
|
- **Closing-delimiter check**: when your replacement `content` ends with a closing delimiter (`}`, `*/`, `)`, `]`), compare it against the line immediately after `end` in the file. If they match, extend `end` to include that line — otherwise the original delimiter survives and `content` adds a second copy.
|
|
67
66
|
- For a range, replace only the body or the whole range — don't split range boundaries.
|
|
@@ -18,19 +18,19 @@ When editing structured blocks (nested braces, tags, indented regions), include
|
|
|
18
18
|
|
|
19
19
|
<parameters>
|
|
20
20
|
```ts
|
|
21
|
-
// Input is { edits: Entry[] }
|
|
21
|
+
// Input is { path: string, edits: Entry[] }. `path` is required and applies to every entry.
|
|
22
22
|
type Entry =
|
|
23
|
-
// Diff is one or more hunks
|
|
23
|
+
// Diff is one or more hunks for the top-level path.
|
|
24
24
|
// - Each hunk begins with "@@" (anchor optional).
|
|
25
25
|
// - Each hunk body only has lines starting with ' ' | '+' | '-'.
|
|
26
26
|
// - Each hunk includes at least one change (+ or -).
|
|
27
|
-
| {
|
|
27
|
+
| { op: "update", diff: string }
|
|
28
28
|
// Diff is full file content, no prefixes.
|
|
29
|
-
| {
|
|
29
|
+
| { op: "create", diff: string }
|
|
30
30
|
// No diff for delete.
|
|
31
|
-
| {
|
|
32
|
-
// New path for update+move.
|
|
33
|
-
| {
|
|
31
|
+
| { op: "delete" }
|
|
32
|
+
// New path for update+move from the top-level path.
|
|
33
|
+
| { op: "update", rename: string, diff: string }
|
|
34
34
|
```
|
|
35
35
|
</parameters>
|
|
36
36
|
|
|
@@ -52,15 +52,15 @@ Returns success/failure; on failure, error message indicates:
|
|
|
52
52
|
|
|
53
53
|
<examples>
|
|
54
54
|
# Create
|
|
55
|
-
`edit {"
|
|
55
|
+
`edit {"path":"hello.txt","edits":[{"op":"create","diff":"Hello\n"}]}`
|
|
56
56
|
# Update
|
|
57
|
-
`edit {"
|
|
57
|
+
`edit {"path":"src/app.py","edits":[{"op":"update","diff":"@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n"}]}`
|
|
58
58
|
# Rename
|
|
59
|
-
`edit {"
|
|
59
|
+
`edit {"path":"src/app.py","edits":[{"op":"update","rename":"src/main.py","diff":"@@\n …\n"}]}`
|
|
60
60
|
# Delete
|
|
61
|
-
`edit {"
|
|
62
|
-
#
|
|
63
|
-
|
|
61
|
+
`edit {"path":"obsolete.txt","edits":[{"op":"delete"}]}`
|
|
62
|
+
# Multiple entries
|
|
63
|
+
All entries in one call apply to the top-level `path`; use separate calls for different files.
|
|
64
64
|
</examples>
|
|
65
65
|
|
|
66
66
|
<avoid>
|
|
@@ -23,7 +23,7 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
|
|
|
23
23
|
# Filesystem
|
|
24
24
|
- Reading a directory path returns a list of dirents.
|
|
25
25
|
{{#if IS_HASHLINE_MODE}}
|
|
26
|
-
- Reading a file returns lines prefixed with anchors (line
|
|
26
|
+
- Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():`
|
|
27
27
|
{{else}}
|
|
28
28
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
29
29
|
- Reading a file returns lines prefixed with line numbers: `41|def alpha():`
|
|
@@ -50,9 +50,9 @@ Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, R
|
|
|
50
50
|
</instruction>
|
|
51
51
|
|
|
52
52
|
<critical>
|
|
53
|
-
- You **MUST** use `read` for
|
|
54
|
-
You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if
|
|
55
|
-
- You **MUST** always include the `path` parameter
|
|
56
|
-
- For specific line ranges, use `sel
|
|
53
|
+
- You **MUST** use `read` for every file, directory, archive, and URL read. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, and `wget` are **FORBIDDEN** for inspection — any such Bash call is a bug, regardless of how short or convenient it looks.
|
|
54
|
+
- You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if `read` fails to deliver reasonable content.
|
|
55
|
+
- You **MUST** always include the `path` parameter — never call `read` with an empty argument object `{}`.
|
|
56
|
+
- For specific line ranges, use `sel` (e.g. `sel="50-200"`, `sel="50+150"`) — do **NOT** reach for `sed -n`, `awk NR`, or `head`/`tail` pipelines.
|
|
57
57
|
- You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
|
|
58
58
|
</critical>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Performs string replacements in files with fuzzy whitespace matching.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
4
|
+
- Params **MUST** be `{ path, edits }`; `path` is required at the top level and applies to every replacement
|
|
5
|
+
- You **MUST** use the smallest `old_text` that uniquely identifies the change
|
|
6
|
+
- If `old_text` is not unique, you **MUST** expand it with more context or use `all: true` to replace all occurrences
|
|
7
7
|
- You **SHOULD** prefer editing existing files over creating new ones
|
|
8
8
|
</instruction>
|
|
9
9
|
|
|
@@ -17,8 +17,8 @@ Searches files using powerful regex matching.
|
|
|
17
17
|
</output>
|
|
18
18
|
|
|
19
19
|
<critical>
|
|
20
|
-
- You **MUST** use the built-in
|
|
21
|
-
- Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The
|
|
22
|
-
- If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the
|
|
23
|
-
- If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining
|
|
20
|
+
- You **MUST** use the built-in `search` tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
|
|
21
|
+
- Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The `search` tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
|
|
22
|
+
- If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the lookup through the `search` tool instead.
|
|
23
|
+
- If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining `search` calls yourself.
|
|
24
24
|
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
InternalUrlRouter,
|
|
67
67
|
JobsProtocolHandler,
|
|
68
68
|
LocalProtocolHandler,
|
|
69
|
+
type LocalProtocolOptions,
|
|
69
70
|
McpProtocolHandler,
|
|
70
71
|
MemoryProtocolHandler,
|
|
71
72
|
PiProtocolHandler,
|
|
@@ -111,7 +112,6 @@ import {
|
|
|
111
112
|
discoverStartupLspServers,
|
|
112
113
|
EditTool,
|
|
113
114
|
FindTool,
|
|
114
|
-
GrepTool,
|
|
115
115
|
getSearchTools,
|
|
116
116
|
HIDDEN_TOOLS,
|
|
117
117
|
isSearchProviderPreference,
|
|
@@ -121,10 +121,12 @@ import {
|
|
|
121
121
|
ReadTool,
|
|
122
122
|
ResolveTool,
|
|
123
123
|
renderSearchToolBm25Description,
|
|
124
|
+
SearchTool,
|
|
124
125
|
setPreferredImageProvider,
|
|
125
126
|
setPreferredSearchProvider,
|
|
126
127
|
type Tool,
|
|
127
128
|
type ToolSession,
|
|
129
|
+
WebSearchTool,
|
|
128
130
|
WriteTool,
|
|
129
131
|
warmupLspServers,
|
|
130
132
|
} from "./tools";
|
|
@@ -226,6 +228,9 @@ export interface CreateAgentSessionOptions {
|
|
|
226
228
|
/** Session manager. Default: session stored under the configured agentDir sessions root */
|
|
227
229
|
sessionManager?: SessionManager;
|
|
228
230
|
|
|
231
|
+
/** Override local:// protocol options for subagent local:// sharing. Default: uses the session's own artifacts dir and session ID. */
|
|
232
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
233
|
+
|
|
229
234
|
/** Settings instance. Default: Settings.init({ cwd, agentDir }) */
|
|
230
235
|
settings?: Settings;
|
|
231
236
|
|
|
@@ -271,13 +276,14 @@ export {
|
|
|
271
276
|
createTools,
|
|
272
277
|
EditTool,
|
|
273
278
|
FindTool,
|
|
274
|
-
GrepTool,
|
|
275
279
|
HIDDEN_TOOLS,
|
|
276
280
|
loadSshTool,
|
|
277
281
|
PythonTool,
|
|
278
282
|
ReadTool,
|
|
279
283
|
ResolveTool,
|
|
284
|
+
SearchTool,
|
|
280
285
|
type ToolSession,
|
|
286
|
+
WebSearchTool,
|
|
281
287
|
WriteTool,
|
|
282
288
|
};
|
|
283
289
|
|
|
@@ -996,10 +1002,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
996
1002
|
}),
|
|
997
1003
|
);
|
|
998
1004
|
internalRouter.register(
|
|
999
|
-
new LocalProtocolHandler(
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1005
|
+
new LocalProtocolHandler(
|
|
1006
|
+
options.localProtocolOptions ?? {
|
|
1007
|
+
getArtifactsDir,
|
|
1008
|
+
getSessionId: () => sessionManager.getSessionId(),
|
|
1009
|
+
},
|
|
1010
|
+
),
|
|
1003
1011
|
);
|
|
1004
1012
|
internalRouter.register(
|
|
1005
1013
|
new SkillProtocolHandler({
|
|
@@ -1386,7 +1394,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1386
1394
|
? requestedActiveToolNames
|
|
1387
1395
|
: requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
|
|
1388
1396
|
const explicitlyRequestedMCPToolNames = options.toolNames
|
|
1389
|
-
? requestedActiveToolNames.filter(name => name.startsWith("
|
|
1397
|
+
? requestedActiveToolNames.filter(name => name.startsWith("mcp__"))
|
|
1390
1398
|
: [];
|
|
1391
1399
|
const discoveryDefaultServers = new Set(
|
|
1392
1400
|
(settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
|
|
@@ -1412,7 +1420,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1412
1420
|
: [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
|
|
1413
1421
|
initialToolNames = [
|
|
1414
1422
|
...new Set([
|
|
1415
|
-
...initialRequestedActiveToolNames.filter(name => !name.startsWith("
|
|
1423
|
+
...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp__")),
|
|
1416
1424
|
...initialSelectedMCPToolNames,
|
|
1417
1425
|
]),
|
|
1418
1426
|
];
|
|
@@ -1424,7 +1432,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1424
1432
|
...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
|
|
1425
1433
|
];
|
|
1426
1434
|
for (const name of alwaysInclude) {
|
|
1427
|
-
if (mcpDiscoveryEnabled && name.startsWith("
|
|
1435
|
+
if (mcpDiscoveryEnabled && name.startsWith("mcp__")) {
|
|
1428
1436
|
continue;
|
|
1429
1437
|
}
|
|
1430
1438
|
if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
|
|
@@ -1601,6 +1609,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1601
1609
|
ttsrManager,
|
|
1602
1610
|
obfuscator,
|
|
1603
1611
|
asyncJobManager,
|
|
1612
|
+
agentId: resolvedAgentId,
|
|
1613
|
+
agentRegistry,
|
|
1604
1614
|
});
|
|
1605
1615
|
hasSession = true;
|
|
1606
1616
|
|
|
@@ -130,6 +130,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
130
130
|
type: "text",
|
|
131
131
|
};
|
|
132
132
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
133
|
+
import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
133
134
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
134
135
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
135
136
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
@@ -196,7 +197,8 @@ export type AgentSessionEvent =
|
|
|
196
197
|
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
197
198
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
198
199
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
199
|
-
| { type: "todo_auto_clear" }
|
|
200
|
+
| { type: "todo_auto_clear" }
|
|
201
|
+
| { type: "irc_message"; message: CustomMessage };
|
|
200
202
|
|
|
201
203
|
/** Listener function for agent session events */
|
|
202
204
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
@@ -262,6 +264,10 @@ export interface AgentSessionConfig {
|
|
|
262
264
|
obfuscator?: SecretObfuscator;
|
|
263
265
|
/** Logical owner for retained Python kernels created by this session. */
|
|
264
266
|
pythonKernelOwnerId?: string;
|
|
267
|
+
/** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
|
|
268
|
+
agentId?: string;
|
|
269
|
+
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
270
|
+
agentRegistry?: AgentRegistry;
|
|
265
271
|
}
|
|
266
272
|
|
|
267
273
|
/** Options for AgentSession.prompt() */
|
|
@@ -477,6 +483,9 @@ export class AgentSession {
|
|
|
477
483
|
// Drained into history (via emitExternalEvent) once the recipient becomes idle.
|
|
478
484
|
#pendingBackgroundExchanges: CustomMessage[][] = [];
|
|
479
485
|
#scheduledBackgroundExchangeFlush = false;
|
|
486
|
+
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
487
|
+
#agentId: string | undefined;
|
|
488
|
+
#agentRegistry: AgentRegistry | undefined;
|
|
480
489
|
// Extension system
|
|
481
490
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
482
491
|
#turnIndex = 0;
|
|
@@ -610,6 +619,8 @@ export class AgentSession {
|
|
|
610
619
|
);
|
|
611
620
|
this.#ttsrManager = config.ttsrManager;
|
|
612
621
|
this.#obfuscator = config.obfuscator;
|
|
622
|
+
this.#agentId = config.agentId;
|
|
623
|
+
this.#agentRegistry = config.agentRegistry;
|
|
613
624
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
614
625
|
const event: AgentEvent = {
|
|
615
626
|
type: "message_update",
|
|
@@ -5997,6 +6008,14 @@ export class AgentSession {
|
|
|
5997
6008
|
attribution: "agent",
|
|
5998
6009
|
timestamp: incomingTimestamp,
|
|
5999
6010
|
};
|
|
6011
|
+
void this.#emitSessionEvent({ type: "irc_message", message: incomingRecord });
|
|
6012
|
+
this.#forwardIrcRelayToMain({
|
|
6013
|
+
from: args.from,
|
|
6014
|
+
to: this.#agentId ?? "?",
|
|
6015
|
+
body: args.message,
|
|
6016
|
+
kind: "message",
|
|
6017
|
+
timestamp: incomingTimestamp,
|
|
6018
|
+
});
|
|
6000
6019
|
|
|
6001
6020
|
if (!awaitReply) {
|
|
6002
6021
|
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
@@ -6021,11 +6040,60 @@ export class AgentSession {
|
|
|
6021
6040
|
attribution: "agent",
|
|
6022
6041
|
timestamp: Date.now(),
|
|
6023
6042
|
};
|
|
6043
|
+
void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
|
|
6044
|
+
this.#forwardIrcRelayToMain({
|
|
6045
|
+
from: this.#agentId ?? "?",
|
|
6046
|
+
to: args.from,
|
|
6047
|
+
body: replyText,
|
|
6048
|
+
kind: "reply",
|
|
6049
|
+
timestamp: replyRecord.timestamp,
|
|
6050
|
+
});
|
|
6024
6051
|
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
|
|
6025
6052
|
|
|
6026
6053
|
return { replyText };
|
|
6027
6054
|
}
|
|
6028
6055
|
|
|
6056
|
+
/**
|
|
6057
|
+
* Forward an IRC exchange observation to the main agent's session UI so the
|
|
6058
|
+
* user can see every IRC conversation in the main transcript, even when the
|
|
6059
|
+
* main agent is not a direct participant. The relay record is display-only:
|
|
6060
|
+
* it is NOT injected into the main agent's persisted history.
|
|
6061
|
+
*/
|
|
6062
|
+
#forwardIrcRelayToMain(args: {
|
|
6063
|
+
from: string;
|
|
6064
|
+
to: string;
|
|
6065
|
+
body: string;
|
|
6066
|
+
kind: "message" | "reply";
|
|
6067
|
+
timestamp: number;
|
|
6068
|
+
}): void {
|
|
6069
|
+
const registry = this.#agentRegistry;
|
|
6070
|
+
if (!registry) return;
|
|
6071
|
+
// If this session is the main agent, the local emit already reached the main UI.
|
|
6072
|
+
if (this.#agentId === MAIN_AGENT_ID) return;
|
|
6073
|
+
const mainRef = registry.get(MAIN_AGENT_ID);
|
|
6074
|
+
const mainSession = mainRef?.session;
|
|
6075
|
+
if (!mainSession || mainSession === this) return;
|
|
6076
|
+
const arrow = args.kind === "reply" ? "\u2192 (auto)" : "\u2192";
|
|
6077
|
+
const relayRecord: CustomMessage = {
|
|
6078
|
+
role: "custom",
|
|
6079
|
+
customType: "irc:relay",
|
|
6080
|
+
content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
|
|
6081
|
+
display: true,
|
|
6082
|
+
details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
|
|
6083
|
+
attribution: "agent",
|
|
6084
|
+
timestamp: args.timestamp,
|
|
6085
|
+
};
|
|
6086
|
+
mainSession.emitIrcRelayObservation(relayRecord);
|
|
6087
|
+
}
|
|
6088
|
+
|
|
6089
|
+
/**
|
|
6090
|
+
* Emit an IRC relay observation event on this session for UI rendering only.
|
|
6091
|
+
* Does not persist the record to history. Public so other sessions can forward.
|
|
6092
|
+
*/
|
|
6093
|
+
emitIrcRelayObservation(record: CustomMessage): void {
|
|
6094
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
6095
|
+
}
|
|
6096
|
+
|
|
6029
6097
|
/**
|
|
6030
6098
|
* Run a single ephemeral side-channel turn against this session's current
|
|
6031
6099
|
* model + system prompt + history. No tools are used; the side request
|