@oh-my-pi/pi-coding-agent 14.4.0 → 14.4.3
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/package.json +7 -7
- package/src/cli.ts +0 -1
- package/src/config/prompt-templates.ts +1 -31
- package/src/config/settings-schema.ts +27 -37
- package/src/config/settings.ts +1 -1
- package/src/edit/index.ts +1 -53
- package/src/edit/line-hash.ts +13 -63
- package/src/edit/modes/atom.ts +334 -64
- package/src/edit/modes/hashline.ts +19 -26
- package/src/edit/renderer.ts +6 -8
- package/src/edit/streaming.ts +90 -114
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +10 -15
- package/src/internal-urls/docs-index.generated.ts +1 -2
- package/src/lsp/defaults.json +142 -652
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +0 -5
- package/src/modes/components/tool-execution.ts +2 -5
- package/src/modes/controllers/btw-controller.ts +17 -105
- package/src/modes/controllers/todo-command-controller.ts +537 -0
- package/src/modes/interactive-mode.ts +35 -9
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +17 -0
- package/src/prompts/system/irc-incoming.md +8 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/atom.md +55 -53
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/grep.md +2 -5
- package/src/prompts/tools/irc.md +49 -0
- package/src/prompts/tools/job.md +11 -0
- package/src/prompts/tools/read.md +12 -13
- package/src/prompts/tools/task.md +1 -1
- package/src/prompts/tools/todo-write.md +14 -5
- package/src/registry/agent-registry.ts +139 -0
- package/src/sdk.ts +35 -0
- package/src/session/agent-session.ts +217 -5
- package/src/session/session-manager.ts +4 -1
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +24 -0
- package/src/task/executor.ts +14 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/fetch.ts +18 -6
- package/src/tools/fs-cache-invalidation.ts +0 -5
- package/src/tools/grep.ts +5 -125
- package/src/tools/index.ts +12 -6
- package/src/tools/irc.ts +258 -0
- package/src/tools/job.ts +489 -0
- package/src/tools/match-line-format.ts +8 -7
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +37 -131
- package/src/tools/renderers.ts +2 -0
- package/src/tools/todo-write.ts +243 -12
- package/src/tools/write.ts +2 -2
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-display-mode.ts +0 -3
- package/src/cli/read-cli.ts +0 -67
- package/src/commands/read.ts +0 -33
- package/src/edit/modes/chunk.ts +0 -832
- package/src/prompts/tools/cancel-job.md +0 -5
- package/src/prompts/tools/chunk-edit.md +0 -158
- package/src/prompts/tools/poll.md +0 -5
- package/src/prompts/tools/read-chunk.md +0 -73
- package/src/tools/cancel-job.ts +0 -95
- package/src/tools/poll-tool.ts +0 -173
|
@@ -18,6 +18,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
18
18
|
|
|
19
19
|
<output>
|
|
20
20
|
- Grouped matches with file path, byte range, line/column ranges, metavariable captures
|
|
21
|
+
- Match lines are anchor-prefixed: `*LINE+ID|content` for the matched line and ` LINE+ID|content` (leading space) for surrounding context
|
|
21
22
|
- Summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
|
|
22
23
|
</output>
|
|
23
24
|
|
|
@@ -7,90 +7,92 @@ Read the file first. Copy the full anchors exactly as shown by `read`.
|
|
|
7
7
|
|
|
8
8
|
Each entry has one shared locator plus one or more verbs:
|
|
9
9
|
- `loc: "160sr"` — single anchored line
|
|
10
|
-
- `loc: "
|
|
11
|
-
- `loc: "$"` — end of file (only valid with `post`)
|
|
10
|
+
- `loc: "$"` — whole file: `pre` prepends, `post` appends, `sed` substitutes across every line
|
|
12
11
|
- `loc: "a.ts:160sr"` — cross-file override inside the locator
|
|
13
12
|
|
|
14
13
|
Verbs:
|
|
15
|
-
- `
|
|
16
|
-
- `pre: [
|
|
17
|
-
- `post: [
|
|
14
|
+
- `splice: […]`: lines are spliced in at the anchor.
|
|
15
|
+
- `pre: […]`: prepend before the anchor (or at BOF if `loc=$`)
|
|
16
|
+
- `post: […]`: append after the anchor (or at EOF if `loc=$`)
|
|
17
|
+
- `sed: "s/foo/bar/"` — sed-style substitution applied to the anchor line. **Prefer this over `splice` for token-level changes**
|
|
18
|
+
Flags: `g` (all occurrences), `i` (case-insensitive), `F` (literal).
|
|
19
|
+
Delimiter is whatever character follows `s`.
|
|
20
|
+
You **MUST** keep the pattern as short as possible.
|
|
18
21
|
|
|
19
22
|
Combination rules:
|
|
20
|
-
- On a single-anchor `loc`, you may combine `pre`, `
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
+
- On a single-anchor `loc`, you may combine `pre`, `splice`, and `post` in the same entry.
|
|
24
|
+
- `splice: []` on a single-anchor `loc` deletes that line.
|
|
25
|
+
- `splice:[""]` is **not** delete — it replaces the line with a blank line.
|
|
23
26
|
</operations>
|
|
24
27
|
|
|
25
28
|
<examples>
|
|
26
29
|
All examples below reference the same file:
|
|
27
30
|
|
|
28
31
|
```ts title="a.ts"
|
|
29
|
-
{{hline
|
|
30
|
-
{{hline
|
|
31
|
-
{{hline
|
|
32
|
-
{{hline
|
|
33
|
-
{{hline
|
|
34
|
-
{{hline
|
|
35
|
-
{{hline
|
|
36
|
-
{{hline
|
|
37
|
-
{{hline 9 "function beta(x) {"}}
|
|
38
|
-
{{hline 10 "\tif (x) {"}}
|
|
39
|
-
{{hline 11 "\t\treturn parse(data);"}}
|
|
40
|
-
{{hline 12 "\t}"}}
|
|
41
|
-
{{hline 13 "\treturn null;"}}
|
|
42
|
-
{{hline 14 "}"}}
|
|
32
|
+
{{hline 1 "const tag = \"BAD\";"}}
|
|
33
|
+
{{hline 2 ""}}
|
|
34
|
+
{{hline 3 "function beta(x) {"}}
|
|
35
|
+
{{hline 4 "\tif (x) {"}}
|
|
36
|
+
{{hline 5 "\t\treturn parse(data) || fallback;"}}
|
|
37
|
+
{{hline 6 "\t}"}}
|
|
38
|
+
{{hline 7 "\treturn null;"}}
|
|
39
|
+
{{hline 8 "}"}}
|
|
43
40
|
```
|
|
44
41
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
`{path:"a.ts",edits:[{loc:{{href 4 "const fallback = group.targetFramework || 'All Frameworks';"}},set:["const fallback = group.targetFramework ?? 'All Frameworks';"]}]}`
|
|
42
|
+
# Replace a line with `splice`
|
|
43
|
+
`{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},splice:["const tag = \"OK\";"]}]}`
|
|
48
44
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
`{path:"a.ts",edits:[{loc:{{href 2 "const timeout = 5000;"}},set:["const timeout = 30_000;"]}]}`
|
|
45
|
+
# Combine `pre` + `splice` + `post` in one entry
|
|
46
|
+
`{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],splice:["\tif (!x) {"],post:["\t\tlog();"]}]}`
|
|
52
47
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
`{path:"a.ts",edits:[{loc:{{href 10 "\tif (x) {"}},set:["\tif (!x) {"]}]}`
|
|
48
|
+
# Delete a line with `splice: []`
|
|
49
|
+
`{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},splice:[]}]}`
|
|
56
50
|
|
|
57
|
-
#
|
|
58
|
-
`{path:"a.ts",edits:[{loc:{{href
|
|
51
|
+
# Preserve a blank line with `splice:[""]`
|
|
52
|
+
`{path:"a.ts",edits:[{loc:{{href 2 ""}},splice:[""]}]}`
|
|
59
53
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
`{path:"a.ts",edits:[{loc:{{href 3 "const tag = \"DO NOT SHIP\";"}},set:["const tag = \"OK\";"]}]}`
|
|
63
|
-
|
|
64
|
-
# Replace multiple non-adjacent lines
|
|
65
|
-
`{path:"a.ts",edits:[{loc:{{href 11 "\t\treturn parse(data);"}},set:["\t\treturn parse(data) ?? fallback;"]},{loc:{{href 13 "\treturn null;"}},set:["\treturn fallback;"]}]}`
|
|
66
|
-
|
|
67
|
-
# Delete a line with `set: []`
|
|
68
|
-
`{path:"a.ts",edits:[{loc:{{href 11 "\t\treturn parse(data);"}},set:[]}]}`
|
|
54
|
+
# Insert before / after a line
|
|
55
|
+
`{path:"a.ts",edits:[{loc:{{href 3 "function beta(x) {"}},pre:["function gamma() {","\tvalidate();","}",""]}]}`
|
|
69
56
|
|
|
70
|
-
#
|
|
71
|
-
|
|
57
|
+
# Substitute one token with `sed` (regex) — preferred for token-level edits
|
|
58
|
+
Use the smallest pattern that uniquely identifies the change.
|
|
59
|
+
`{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:"s/\\|\\|/??/"}]}`
|
|
72
60
|
|
|
73
|
-
#
|
|
74
|
-
`
|
|
75
|
-
`{path:"a.ts",edits:[{loc:{{href
|
|
61
|
+
# Substitute every occurrence with `sed` (literal/fixed-string)
|
|
62
|
+
Use the `F` flag to disable regex; the delimiter can be any non-alphanumeric char.
|
|
63
|
+
`{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:"s|data|input|gF"}]}`
|
|
76
64
|
|
|
77
65
|
# Prepend / append at file edges
|
|
78
|
-
`{path:"a.ts",edits:[{loc:"
|
|
66
|
+
`{path:"a.ts",edits:[{loc:"$",pre:["// Copyright (c) 2026",""]}]}`
|
|
79
67
|
`{path:"a.ts",edits:[{loc:"$",post:["","export const VERSION = \"1.0.0\";"]}]}`
|
|
80
68
|
|
|
81
69
|
# Cross-file override inside `loc`
|
|
82
|
-
`{path:"a.ts",edits:[{loc:"b.ts:{{href
|
|
70
|
+
`{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",splice:["const tag = \"OK\";"]}]}`
|
|
71
|
+
|
|
72
|
+
# WRONG: retyping unchanged neighbors inside `splice` duplicates them
|
|
73
|
+
`{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {","\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}`
|
|
74
|
+
The 2nd array element matches existing line 5, which is **not** overwritten, it shifts, so return statement ends up duplicated.
|
|
75
|
+
|
|
76
|
+
# RIGHT: split into separate edits
|
|
77
|
+
- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},sed:"s/x/x \\&\\& ready/"},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},post:["\t\t//unreachable"]}]}`
|
|
78
|
+
OR
|
|
79
|
+
- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {"]},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},splice:["\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}`
|
|
83
80
|
</examples>
|
|
84
81
|
|
|
85
82
|
<critical>
|
|
86
83
|
- Make the minimum exact edit.
|
|
87
84
|
- Copy the full anchors exactly as shown by `read/grep` (for example `160sr`, not just `sr`).
|
|
88
85
|
- `loc` chooses the target. Verbs describe what to do there.
|
|
89
|
-
- On a single-anchor `loc`, you may combine `pre`, `
|
|
90
|
-
- `loc:"
|
|
91
|
-
- `
|
|
86
|
+
- On a single-anchor `loc`, you may combine `pre`, `splice`, and `post`.
|
|
87
|
+
- `loc:"$"` operates on the whole file: `pre` prepends, `post` appends, `sed` runs across every line.
|
|
88
|
+
- `splice: []` deletes the anchored line. `splice:[""]` preserves a blank line.
|
|
92
89
|
- 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.
|
|
93
|
-
- `
|
|
90
|
+
- `splice` operations target the current file content only. Do not try to reference old line text after the file has changed.
|
|
91
|
+
- 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 `sed` pattern that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe. For multi-line restructuring (wrapping logic, adding new branches, inserting blocks), use `splice`/`pre`/`post` — do **not** stretch `sed` into a rewrite tool.
|
|
92
|
+
- 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.
|
|
93
|
+
- 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.
|
|
94
|
+
- 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`.
|
|
95
|
+
- 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.
|
|
94
96
|
- Text content must be literal file content with matching indentation. If the file uses tabs, use real tabs.
|
|
95
97
|
- You **MUST NOT** use this tool to reformat or clean up unrelated code.
|
|
96
98
|
</critical>
|
|
@@ -14,10 +14,10 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
14
14
|
- Long-running non-PTY commands may auto-background after ~{{autoBackgroundThresholdSeconds}}s and continue as background jobs.
|
|
15
15
|
{{/if}}
|
|
16
16
|
{{#if asyncEnabled}}
|
|
17
|
-
- Inspect background jobs with `read jobs://` (`read jobs://<job-id>` for detail). To wait for results, call `poll` — do NOT poll `read jobs://` in a loop or yield and hope for delivery.
|
|
17
|
+
- Inspect background jobs with `read jobs://` (`read jobs://<job-id>` for detail). To wait for results, call `job` (with `poll`) — do NOT poll `read jobs://` in a loop or yield and hope for delivery.
|
|
18
18
|
{{else}}
|
|
19
19
|
{{#if autoBackgroundEnabled}}
|
|
20
|
-
- For auto-backgrounded jobs, inspect with `read jobs://` and call `poll` to wait — do NOT poll in a loop.
|
|
20
|
+
- For auto-backgrounded jobs, inspect with `read jobs://` and call `job` (with `poll`) to wait — do NOT poll in a loop.
|
|
21
21
|
{{/if}}
|
|
22
22
|
{{/if}}
|
|
23
23
|
</instruction>
|
|
@@ -8,20 +8,17 @@ Searches files using powerful regex matching.
|
|
|
8
8
|
|
|
9
9
|
<output>
|
|
10
10
|
{{#if IS_HASHLINE_MODE}}
|
|
11
|
-
- Text output is anchor-prefixed:
|
|
11
|
+
- Text output is anchor-prefixed: `*123th|content` (match) or ` 123th|content` (context, leading space). The 2-letter ID is a content fingerprint.
|
|
12
12
|
{{else}}
|
|
13
13
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
14
14
|
- Text output is line-number-prefixed
|
|
15
15
|
{{/if}}
|
|
16
16
|
{{/if}}
|
|
17
|
-
{{#if IS_CHUNK_MODE}}
|
|
18
|
-
- Text output is chunk-path-prefixed: `path:sel>123th:content`
|
|
19
|
-
{{/if}}
|
|
20
17
|
</output>
|
|
21
18
|
|
|
22
19
|
<critical>
|
|
23
20
|
- You **MUST** use the built-in Grep 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.
|
|
24
|
-
- Bash `grep`/`rg`
|
|
21
|
+
- Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The Grep tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
|
|
25
22
|
- If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the search through the Grep tool instead.
|
|
26
23
|
- If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining Grep calls yourself.
|
|
27
24
|
</critical>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Sends short text messages to other live agents in this process and receives their prose replies.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- The main agent is addressable as `0-Main`. Subagents reuse their task id (e.g. `0-AuthLoader`).
|
|
5
|
+
- `op: "list"` returns the current set of visible peers. Use it before sending if you are not sure who is live.
|
|
6
|
+
- `op: "send"` delivers `message` to `to`. `to` may be a specific id or `"all"` to broadcast.
|
|
7
|
+
- The recipient generates the reply via an ephemeral side-channel turn that uses their current model, system prompt, and history — it does **not** wait for the recipient's main loop to be free, so it is safe to IRC an agent that is currently inside a long-running tool call.
|
|
8
|
+
- The exchange (incoming question + auto-reply) is queued for injection into the recipient's persisted history; the recipient sees it on its next turn and can follow up if needed.
|
|
9
|
+
</instruction>
|
|
10
|
+
|
|
11
|
+
<when_to_use>
|
|
12
|
+
You **SHOULD** reach for `irc` proactively when continuing alone is wasteful or wrong. When in doubt, prefer messaging.
|
|
13
|
+
- **Unexpected state.** You hit something the original task did not describe — a missing file, a config that contradicts the assignment, an API behaving differently than you were told, a tool failing in a way that suggests the spec is wrong. DM `0-Main` (or the spawning agent) for guidance instead of guessing.
|
|
14
|
+
- **Blocked by another agent.** A peer holds the file/branch/resource you need, has already started the change you are about to make, or owns a decision you depend on. DM that peer (or broadcast to discover who) before duplicating or stepping on work.
|
|
15
|
+
- **Decision points outside your scope.** A genuine fork in the road that the assignment did not pre-decide (e.g. which of two viable APIs to use, whether to refactor adjacent code). Ask the requester rather than picking unilaterally.
|
|
16
|
+
- **Coordination opportunities.** You realize a peer's in-flight work would benefit from yours, or vice-versa.
|
|
17
|
+
|
|
18
|
+
Do **not** use `irc` for: routine progress updates, things you can verify with a tool call, or questions whose answer is already in your assignment / repo / docs.
|
|
19
|
+
</when_to_use>
|
|
20
|
+
|
|
21
|
+
<etiquette>
|
|
22
|
+
These rules apply to both sending and replying.
|
|
23
|
+
- **Plain prose only.** Do not send structured JSON status payloads (e.g. `{"type":"task_completed",…}`). Write a normal sentence: "Done with the auth refactor — left a TODO in `src/server/auth.ts` for the rate limiter."
|
|
24
|
+
- **Do not quote the message you are replying to.** The sender already saw it; the TUI already renders it. Lead with the answer.
|
|
25
|
+
- **Use IRC, not terminal tools, to learn about peers.** Do not `grep` artifacts, read other sessions' JSONL files, or shell-poke around to figure out what another agent is doing. DM them — they have the live answer and you do not.
|
|
26
|
+
- **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. Do not follow up with "did you get my message?" — they did. If `delivered` is empty or the result was `failed`, the peer is unavailable; move on or report the blocker, do not retry in a loop.
|
|
27
|
+
- **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `memory://` / `artifact://` URLs instead of pasting blobs.
|
|
28
|
+
- **Address peers by id.** Use the exact id from `op: "list"` (e.g. `0-AuthLoader`, `0-Main`). Do not invent friendly names.
|
|
29
|
+
- **Do not IRC for things a tool would answer.** If a `read`, `grep`, or build command would resolve the question, do that first.
|
|
30
|
+
- **When you receive an IRC message, answer it before continuing.** The recipient injects the question + your auto-reply into your history; address it directly, do not repeat it back to the user.
|
|
31
|
+
</etiquette>
|
|
32
|
+
|
|
33
|
+
<output>
|
|
34
|
+
- `send`: returns each recipient that received the message and any prose replies that arrived.
|
|
35
|
+
- `list`: returns peers and channels visible to the caller.
|
|
36
|
+
</output>
|
|
37
|
+
|
|
38
|
+
<examples>
|
|
39
|
+
# List peers
|
|
40
|
+
`{"op": "list"}`
|
|
41
|
+
# Direct message to the main agent (waits for prose reply)
|
|
42
|
+
`{"op": "send", "to": "0-Main", "message": "Should I prefer JWT or session cookies for the auth flow?"}`
|
|
43
|
+
# Unexpected state — ask the originator
|
|
44
|
+
`{"op": "send", "to": "0-Main", "message": "Assignment says edit src/auth/jwt.ts but the file does not exist. Is the new path src/server/auth/jwt.ts?"}`
|
|
45
|
+
# Blocked by a peer — ask them directly
|
|
46
|
+
`{"op": "send", "to": "0-AuthLoader", "message": "Are you still touching src/server/auth.ts? I need to add a 401 path; OK to proceed or should I wait?"}`
|
|
47
|
+
# Broadcast to discover who owns something (no replies, just informs them)
|
|
48
|
+
`{"op": "send", "to": "all", "message": "About to refactor src/server/middleware/*. Anyone already in there?", "awaitReply": false}`
|
|
49
|
+
</examples>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Manages background jobs: poll to wait for completion, cancel to stop running jobs.
|
|
2
|
+
|
|
3
|
+
You **MUST** use the `job` tool (in a loop, if necessary) instead of manually reading in a loop or issuing sleep commands.
|
|
4
|
+
|
|
5
|
+
Pass `poll` to wait for one or more background jobs to finalize. If the timeout elapses before any job changes state, it returns the current snapshot (still-running jobs and any already-completed deliveries) without erroring — call `job` again to keep waiting. Calling with no `poll` and no `cancel` waits on every running background job.
|
|
6
|
+
|
|
7
|
+
You **MUST NOT** poll the same job repeatedly without evidence of progress. Between calls, inspect `read jobs://<id>` to confirm new output or activity. If a job is stalled, has hung, or is producing nothing useful, cancel it via `cancel` and try a different approach instead of waiting indefinitely.
|
|
8
|
+
|
|
9
|
+
Pass `cancel` to stop one or more running background jobs (started via async tool execution or bash auto-backgrounding). You **SHOULD** cancel jobs that are no longer needed or stuck. You **MAY** inspect jobs first with `read jobs://` or `read jobs://<job-id>`.
|
|
10
|
+
|
|
11
|
+
`poll` and `cancel` may be combined in a single call: cancellations apply first, then polling waits on the remaining ids. When only `cancel` is provided the call returns immediately without waiting.
|
|
@@ -15,19 +15,18 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
|
|
|
15
15
|
|`sel` value|Behavior|
|
|
16
16
|
|---|---|
|
|
17
17
|
|*(omitted)*|Read full file (up to {{DEFAULT_LIMIT}} lines)|
|
|
18
|
-
|`
|
|
19
|
-
|`
|
|
20
|
-
|`
|
|
21
|
-
|`
|
|
22
|
-
|
|
23
|
-
Max {{DEFAULT_MAX_LINES}} lines per call.
|
|
18
|
+
|`50`|Read from line 50 onward|
|
|
19
|
+
|`50-200`|Read lines 50-200|
|
|
20
|
+
|`50+150`|Read 150 lines starting at line 50|
|
|
21
|
+
|`20+1`|Read exactly one line|
|
|
24
22
|
|
|
25
23
|
# Filesystem
|
|
24
|
+
- Reading a directory path returns a list of dirents.
|
|
26
25
|
{{#if IS_HASHLINE_MODE}}
|
|
27
|
-
- Reading
|
|
26
|
+
- Reading a file returns lines prefixed with anchors (line # .. hash .. | .. line content): `41th|def alpha():`
|
|
28
27
|
{{else}}
|
|
29
28
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
30
|
-
- Reading
|
|
29
|
+
- Reading a file returns lines prefixed with line numbers: `41|def alpha():`
|
|
31
30
|
{{/if}}
|
|
32
31
|
{{/if}}
|
|
33
32
|
|
|
@@ -47,13 +46,13 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
47
46
|
- `file.db?q=SELECT …` — read-only SELECT query
|
|
48
47
|
|
|
49
48
|
# URLs
|
|
50
|
-
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
|
|
49
|
+
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
|
|
51
50
|
</instruction>
|
|
52
51
|
|
|
53
52
|
<critical>
|
|
54
|
-
- You **MUST** use `read`
|
|
55
|
-
|
|
56
|
-
- You **MUST** always include the `path` parameter
|
|
57
|
-
- For specific line ranges, use `sel
|
|
53
|
+
- You **MUST** use `read` for all file, directory, archive, and URL reads; never cat/head/ls/tar/unzip/curl, etc.
|
|
54
|
+
You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if this method fails to deliver reasonable content.
|
|
55
|
+
- You **MUST** always include the `path` parameter.
|
|
56
|
+
- For specific line ranges, use `sel`.
|
|
58
57
|
- You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
|
|
59
58
|
</critical>
|
|
@@ -2,7 +2,7 @@ Launches subagents to parallelize workflows.
|
|
|
2
2
|
|
|
3
3
|
{{#if asyncEnabled}}
|
|
4
4
|
- Use `read jobs://` to inspect state; `read jobs://<job_id>` for detail.
|
|
5
|
-
- Use the `
|
|
5
|
+
- Use the `job` tool (with `poll`) to wait until completion. You **MUST NOT** poll `read jobs://` in a loop.
|
|
6
6
|
{{/if}}
|
|
7
7
|
|
|
8
8
|
{{#if defaultMode}}
|
|
@@ -23,11 +23,12 @@ Pass an object with an `ops` array:
|
|
|
23
23
|
|
|
24
24
|
|Field|Type|When to use|
|
|
25
25
|
|---|---|---|
|
|
26
|
-
|`op`|string|Required. One of `replace`, `start`, `done`, `rm`, `drop`, `append`|
|
|
26
|
+
|`op`|string|Required. One of `replace`, `start`, `done`, `rm`, `drop`, `append`, `note`|
|
|
27
27
|
|`task`|string|Task id for `start`, or a task target for `done` / `rm` / `drop`|
|
|
28
28
|
|`phase`|string|Phase target for `done` / `rm` / `drop`, or append destination for `append`|
|
|
29
29
|
|`items`|{id, label}[]|Required for `append`. If the phase does not exist, it is created at the end|
|
|
30
30
|
|`phases`|Phase[]|Only for `replace`. Keeps initial phased setup available for harness bootstrap and full restructures|
|
|
31
|
+
|`text`|string|Required for `note`. The note text appended to `task.notes` (which is a list, joined with newlines on render)|
|
|
31
32
|
|
|
32
33
|
## Semantics
|
|
33
34
|
- `start`: requires `task`; sets that task to `in_progress`
|
|
@@ -36,6 +37,7 @@ Pass an object with an `ops` array:
|
|
|
36
37
|
- `drop`: marks one task, one phase, or all tasks abandoned
|
|
37
38
|
- `append`: appends `items` to `phase`; creates the phase if missing
|
|
38
39
|
- `replace`: replaces the full todo list
|
|
40
|
+
- `note`: append `text` as a new note attached to `task`. Notes are append-only context the user added; they only render to you when the task is `in_progress`. Other tasks display only a `+N` marker. Use this when you want to leave a follow-up reminder for yourself when you reach a later task.
|
|
39
41
|
|
|
40
42
|
If `done`, `rm`, or `drop` omits both `task` and `phase`, it applies to all tasks.
|
|
41
43
|
|
|
@@ -43,6 +45,11 @@ If `done`, `rm`, or `drop` omits both `task` and `phase`, it applies to all task
|
|
|
43
45
|
- `label`: Short label (5-10 words). What is being done, not how.
|
|
44
46
|
- `replace` task `content` should stay short and specific.
|
|
45
47
|
|
|
48
|
+
## Phase Anatomy
|
|
49
|
+
- `name`: Short, human-readable noun phrase (1-3 words). Capitalize naturally.
|
|
50
|
+
- Always prefix with a roman-numeral ordinal (`I.`, `II.`, `III.`, `IV.`, …) to convey ordering — e.g. `I. Foundation`, `II. Auth`, `III. Routing`. Single-phase plans use `I.` too.
|
|
51
|
+
- You **MUST NOT** use snake_case, `Phase1_*`, arabic numerals (`1.`), or letter prefixes (`A.`) — they render as ugly identifiers.
|
|
52
|
+
|
|
46
53
|
## Rules
|
|
47
54
|
- Mark tasks done immediately after finishing — never defer.
|
|
48
55
|
- Complete phases in order — do not skip ahead while earlier ones are pending.
|
|
@@ -59,18 +66,20 @@ Create a todo list when:
|
|
|
59
66
|
</conditions>
|
|
60
67
|
|
|
61
68
|
<examples>
|
|
62
|
-
# Initial setup
|
|
63
|
-
`{"ops":[{"op":"replace","phases":[{"name":"
|
|
69
|
+
# Initial setup (multi-phase)
|
|
70
|
+
`{"ops":[{"op":"replace","phases":[{"name":"I. Foundation","tasks":[{"content":"Scaffold crate"},{"content":"Wire workspace"}]},{"name":"II. Auth","tasks":[{"content":"Port credential store"},{"content":"Wire OAuth providers"}]},{"name":"III. Verification","tasks":[{"content":"Run cargo test"}]}]}]}`
|
|
71
|
+
# Initial setup (single phase — still prefixed)
|
|
72
|
+
`{"ops":[{"op":"replace","phases":[{"name":"I. Implementation","tasks":[{"content":"Apply fix"},{"content":"Run tests"}]}]}]}`
|
|
64
73
|
# Complete one task
|
|
65
74
|
`{"ops":[{"op":"done","task":"task-2"}]}`
|
|
66
75
|
# Complete a whole phase
|
|
67
|
-
`{"ops":[{"op":"done","phase":"
|
|
76
|
+
`{"ops":[{"op":"done","phase":"II. Auth"}]}`
|
|
68
77
|
# Remove all tasks
|
|
69
78
|
`{"ops":[{"op":"rm"}]}`
|
|
70
79
|
# Drop one task
|
|
71
80
|
`{"ops":[{"op":"drop","task":"task-7"}]}`
|
|
72
81
|
# Append tasks to a phase
|
|
73
|
-
`{"ops":[{"op":"append","phase":"
|
|
82
|
+
`{"ops":[{"op":"append","phase":"II. Auth","items":[{"id":"task-8","label":"Handle retries"},{"id":"task-9","label":"Run tests"}]}]}`
|
|
74
83
|
</examples>
|
|
75
84
|
|
|
76
85
|
<avoid>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRegistry - Process-global registry of live AgentSession instances.
|
|
3
|
+
*
|
|
4
|
+
* Tracks every alive agent (the main session plus every subagent) so the
|
|
5
|
+
* `irc` tool can address peers by id. Sessions are registered explicitly at
|
|
6
|
+
* creation and removed when the owner releases them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentSession } from "../session/agent-session";
|
|
10
|
+
|
|
11
|
+
export const MAIN_AGENT_ID = "0-Main";
|
|
12
|
+
|
|
13
|
+
export type AgentStatus = "running" | "idle" | "completed" | "aborted";
|
|
14
|
+
export type AgentKind = "main" | "sub";
|
|
15
|
+
|
|
16
|
+
export interface AgentRef {
|
|
17
|
+
id: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
kind: AgentKind;
|
|
20
|
+
parentId?: string;
|
|
21
|
+
status: AgentStatus;
|
|
22
|
+
session: AgentSession | null;
|
|
23
|
+
sessionFile: string | null;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
lastActivity: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type RegistryEvent =
|
|
29
|
+
| { type: "registered"; ref: AgentRef }
|
|
30
|
+
| { type: "status_changed"; ref: AgentRef }
|
|
31
|
+
| { type: "removed"; ref: AgentRef };
|
|
32
|
+
|
|
33
|
+
type RegistryListener = (event: RegistryEvent) => void;
|
|
34
|
+
|
|
35
|
+
export interface RegisterInput {
|
|
36
|
+
id: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
kind: AgentKind;
|
|
39
|
+
parentId?: string;
|
|
40
|
+
session: AgentSession | null;
|
|
41
|
+
sessionFile?: string | null;
|
|
42
|
+
status?: AgentStatus;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class AgentRegistry {
|
|
46
|
+
static #global: AgentRegistry | undefined;
|
|
47
|
+
|
|
48
|
+
static global(): AgentRegistry {
|
|
49
|
+
if (!AgentRegistry.#global) {
|
|
50
|
+
AgentRegistry.#global = new AgentRegistry();
|
|
51
|
+
}
|
|
52
|
+
return AgentRegistry.#global;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reset the global registry. Test-only. */
|
|
56
|
+
static resetGlobalForTests(): void {
|
|
57
|
+
AgentRegistry.#global = new AgentRegistry();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
readonly #refs = new Map<string, AgentRef>();
|
|
61
|
+
readonly #listeners = new Set<RegistryListener>();
|
|
62
|
+
|
|
63
|
+
register(input: RegisterInput): AgentRef {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const ref: AgentRef = {
|
|
66
|
+
id: input.id,
|
|
67
|
+
displayName: input.displayName,
|
|
68
|
+
kind: input.kind,
|
|
69
|
+
parentId: input.parentId,
|
|
70
|
+
status: input.status ?? "running",
|
|
71
|
+
session: input.session,
|
|
72
|
+
sessionFile: input.sessionFile ?? null,
|
|
73
|
+
createdAt: now,
|
|
74
|
+
lastActivity: now,
|
|
75
|
+
};
|
|
76
|
+
this.#refs.set(ref.id, ref);
|
|
77
|
+
this.#emit({ type: "registered", ref });
|
|
78
|
+
return ref;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setStatus(id: string, status: AgentStatus): void {
|
|
82
|
+
const ref = this.#refs.get(id);
|
|
83
|
+
if (!ref || ref.status === status) return;
|
|
84
|
+
ref.status = status;
|
|
85
|
+
ref.lastActivity = Date.now();
|
|
86
|
+
this.#emit({ type: "status_changed", ref });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
attachSession(id: string, session: AgentSession): void {
|
|
90
|
+
const ref = this.#refs.get(id);
|
|
91
|
+
if (!ref) return;
|
|
92
|
+
ref.session = session;
|
|
93
|
+
ref.lastActivity = Date.now();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
detachSession(id: string): void {
|
|
97
|
+
const ref = this.#refs.get(id);
|
|
98
|
+
if (!ref) return;
|
|
99
|
+
ref.session = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
unregister(id: string): void {
|
|
103
|
+
const ref = this.#refs.get(id);
|
|
104
|
+
if (!ref) return;
|
|
105
|
+
this.#refs.delete(id);
|
|
106
|
+
this.#emit({ type: "removed", ref });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get(id: string): AgentRef | undefined {
|
|
110
|
+
return this.#refs.get(id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
list(): AgentRef[] {
|
|
114
|
+
return [...this.#refs.values()];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns every alive agent (running | idle) except the caller.
|
|
119
|
+
* Flat namespace: every agent can see every other agent.
|
|
120
|
+
*/
|
|
121
|
+
listVisibleTo(id: string): AgentRef[] {
|
|
122
|
+
return this.list().filter(ref => ref.id !== id && (ref.status === "running" || ref.status === "idle"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onChange(listener: RegistryListener): () => void {
|
|
126
|
+
this.#listeners.add(listener);
|
|
127
|
+
return () => this.#listeners.delete(listener);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#emit(event: RegistryEvent): void {
|
|
131
|
+
for (const listener of this.#listeners) {
|
|
132
|
+
try {
|
|
133
|
+
listener(event);
|
|
134
|
+
} catch {
|
|
135
|
+
// listeners must not break the dispatch loop
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
} from "./mcp/discoverable-tool-metadata";
|
|
84
84
|
import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
|
|
85
85
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
86
|
+
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
86
87
|
import {
|
|
87
88
|
collectEnvSecrets,
|
|
88
89
|
deobfuscateSessionContext,
|
|
@@ -213,6 +214,12 @@ export interface CreateAgentSessionOptions {
|
|
|
213
214
|
requireYieldTool?: boolean;
|
|
214
215
|
/** Task recursion depth (for subagent sessions). Default: 0 */
|
|
215
216
|
taskDepth?: number;
|
|
217
|
+
/** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
|
|
218
|
+
agentId?: string;
|
|
219
|
+
/** Display name for the agent in IRC. Default: "main" or "sub". */
|
|
220
|
+
agentDisplayName?: string;
|
|
221
|
+
/** Optional shared agent registry for IRC routing. Default: AgentRegistry.global(). */
|
|
222
|
+
agentRegistry?: AgentRegistry;
|
|
216
223
|
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
|
|
217
224
|
parentTaskPrefix?: string;
|
|
218
225
|
|
|
@@ -896,6 +903,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
896
903
|
})
|
|
897
904
|
: undefined;
|
|
898
905
|
|
|
906
|
+
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
907
|
+
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
908
|
+
const resolvedAgentDisplayName =
|
|
909
|
+
options.agentDisplayName ?? ((options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main");
|
|
899
910
|
const pythonKernelOwnerId = `agent-session:${Snowflake.next()}`;
|
|
900
911
|
|
|
901
912
|
try {
|
|
@@ -929,6 +940,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
929
940
|
trackPythonExecution: (execution, abortController) =>
|
|
930
941
|
session ? session.trackPythonExecution(execution, abortController) : execution,
|
|
931
942
|
getSessionId: () => sessionManager.getSessionId?.() ?? null,
|
|
943
|
+
getAgentId: () => resolvedAgentId,
|
|
944
|
+
agentRegistry,
|
|
932
945
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
933
946
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
934
947
|
getActiveModelString,
|
|
@@ -1591,6 +1604,28 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1591
1604
|
});
|
|
1592
1605
|
hasSession = true;
|
|
1593
1606
|
|
|
1607
|
+
// Register this session in the global agent registry so other agents can
|
|
1608
|
+
// address it via the irc tool. Wrap dispose to unregister on teardown.
|
|
1609
|
+
agentRegistry.register({
|
|
1610
|
+
id: resolvedAgentId,
|
|
1611
|
+
displayName: resolvedAgentDisplayName,
|
|
1612
|
+
kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
|
|
1613
|
+
parentId: options.parentTaskPrefix,
|
|
1614
|
+
session,
|
|
1615
|
+
sessionFile: sessionManager.getSessionFile() ?? null,
|
|
1616
|
+
status: "running",
|
|
1617
|
+
});
|
|
1618
|
+
{
|
|
1619
|
+
const originalDispose = session.dispose.bind(session);
|
|
1620
|
+
session.dispose = async () => {
|
|
1621
|
+
try {
|
|
1622
|
+
await originalDispose();
|
|
1623
|
+
} finally {
|
|
1624
|
+
agentRegistry.unregister(resolvedAgentId);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1594
1629
|
if (model?.api === "openai-codex-responses") {
|
|
1595
1630
|
const codexModel = model;
|
|
1596
1631
|
const codexTransport = getOpenAICodexTransportDetails(codexModel, {
|