@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/package.json +7 -7
  3. package/src/cli.ts +0 -1
  4. package/src/config/prompt-templates.ts +1 -31
  5. package/src/config/settings-schema.ts +27 -37
  6. package/src/config/settings.ts +1 -1
  7. package/src/edit/index.ts +1 -53
  8. package/src/edit/line-hash.ts +13 -63
  9. package/src/edit/modes/atom.ts +334 -64
  10. package/src/edit/modes/hashline.ts +19 -26
  11. package/src/edit/renderer.ts +6 -8
  12. package/src/edit/streaming.ts +90 -114
  13. package/src/export/html/template.generated.ts +1 -1
  14. package/src/export/html/template.js +10 -15
  15. package/src/internal-urls/docs-index.generated.ts +1 -2
  16. package/src/lsp/defaults.json +142 -652
  17. package/src/modes/components/session-selector.ts +3 -3
  18. package/src/modes/components/settings-defs.ts +0 -5
  19. package/src/modes/components/tool-execution.ts +2 -5
  20. package/src/modes/controllers/btw-controller.ts +17 -105
  21. package/src/modes/controllers/todo-command-controller.ts +537 -0
  22. package/src/modes/interactive-mode.ts +35 -9
  23. package/src/modes/types.ts +2 -0
  24. package/src/modes/utils/ui-helpers.ts +17 -0
  25. package/src/prompts/system/irc-incoming.md +8 -0
  26. package/src/prompts/system/subagent-system-prompt.md +8 -0
  27. package/src/prompts/tools/ast-edit.md +1 -1
  28. package/src/prompts/tools/ast-grep.md +1 -0
  29. package/src/prompts/tools/atom.md +55 -53
  30. package/src/prompts/tools/bash.md +2 -2
  31. package/src/prompts/tools/grep.md +2 -5
  32. package/src/prompts/tools/irc.md +49 -0
  33. package/src/prompts/tools/job.md +11 -0
  34. package/src/prompts/tools/read.md +12 -13
  35. package/src/prompts/tools/task.md +1 -1
  36. package/src/prompts/tools/todo-write.md +14 -5
  37. package/src/registry/agent-registry.ts +139 -0
  38. package/src/sdk.ts +35 -0
  39. package/src/session/agent-session.ts +217 -5
  40. package/src/session/session-manager.ts +4 -1
  41. package/src/session/streaming-output.ts +1 -1
  42. package/src/slash-commands/builtin-registry.ts +24 -0
  43. package/src/task/executor.ts +14 -0
  44. package/src/tools/bash.ts +1 -1
  45. package/src/tools/fetch.ts +18 -6
  46. package/src/tools/fs-cache-invalidation.ts +0 -5
  47. package/src/tools/grep.ts +5 -125
  48. package/src/tools/index.ts +12 -6
  49. package/src/tools/irc.ts +258 -0
  50. package/src/tools/job.ts +489 -0
  51. package/src/tools/match-line-format.ts +8 -7
  52. package/src/tools/output-meta.ts +1 -1
  53. package/src/tools/read.ts +37 -131
  54. package/src/tools/renderers.ts +2 -0
  55. package/src/tools/todo-write.ts +243 -12
  56. package/src/tools/write.ts +2 -2
  57. package/src/utils/edit-mode.ts +1 -2
  58. package/src/utils/file-display-mode.ts +0 -3
  59. package/src/cli/read-cli.ts +0 -67
  60. package/src/commands/read.ts +0 -33
  61. package/src/edit/modes/chunk.ts +0 -832
  62. package/src/prompts/tools/cancel-job.md +0 -5
  63. package/src/prompts/tools/chunk-edit.md +0 -158
  64. package/src/prompts/tools/poll.md +0 -5
  65. package/src/prompts/tools/read-chunk.md +0 -73
  66. package/src/tools/cancel-job.ts +0 -95
  67. package/src/tools/poll-tool.ts +0 -173
@@ -1,5 +0,0 @@
1
- Cancels a running background job started via async tool execution or bash auto-backgrounding.
2
-
3
- You **SHOULD** use this when a background `bash` or `task` job is no longer needed or is stuck.
4
-
5
- You **MAY** inspect jobs first with `read jobs://` or `read jobs://<job-id>`.
@@ -1,158 +0,0 @@
1
- Edits files via syntax-aware chunks. Use `read(path="file.ts")` to read and discover chunks before editing.
2
- - `read` is the canonical read path for chunk source and `sel="?"` tree listings.
3
- - `write` rewrites the entire targeted region — best for most edits.
4
- - `insert` adds content before/after a chunk.
5
- - `delete` deletes a targeted chunk and must be explicit.
6
-
7
- Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}`
8
-
9
- <rules>
10
- - **MUST** inspect first with `read`. Never invent chunk paths or IDs. Copy them from the latest `read` output or edit response.
11
- - `path` format: `file:selector` — e.g. `src/app.ts:fn_foo#thth~`. Append `~` for body, `^` for head, or nothing for the whole chunk. Include `#ID` for `write`/`delete`.
12
- - If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
13
- {{#if chunkAutoIndent}}
14
- - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `~` of a method, write the body starting at column 0:
15
- ```
16
- content: "if (x) {\n\treturn true;\n}"
17
- ```
18
- The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
19
- Multiple sibling body lines at the same level all start at column 0: `"print(a)\nprint(b)\nprint(c)\n"`. Only use `\t` when nesting deeper (e.g. `"if cond:\n\tinner\nouter\n"`).
20
- Before applying the target's base indent, the tool strips any common leading whitespace shared by all non-empty `write` lines as a safety net. Do not rely on that cleanup for mixed indentation; write `~` bodies at column 0 and use one `\t` per relative nesting level.
21
- Multi-line replacements use the same relative-indentation model: the replacement text is dedented, then re-indented to the matched source line. Do not include the chunk's base indentation in replacement text.
22
- **Common mistake** when replacing `~` of a function body: do NOT include the function's own indentation.
23
- Wrong: `"if b == 0:\n\t\treturn None\n\treturn a / b\n"` — adds the function's base `\t` to every line.
24
- Correct: `"if b == 0:\n\treturn None\nreturn a / b\n"` — `if` and `return a / b` at column 0, only `return None` gets `\t` for nesting.
25
- {{else}}
26
- - Match the file's literal tabs/spaces in `content`. Do not convert indentation to canonical `\t`.
27
- - Write content at indent-level 0 relative to the target region. For example, to replace `~` of a method, write:
28
- ```
29
- content: "if (x) {\n return true;\n}"
30
- ```
31
- The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
32
- Before applying the target's base indent, the tool strips any common leading whitespace shared by all non-empty `write` lines as a safety net. Do not rely on that cleanup for mixed indentation; write `~` bodies at column 0.
33
- Multi-line replacements use the same relative-indentation model: the replacement text is dedented, then re-indented to the matched source line. Do not include the chunk's base indentation in replacement text.
34
- {{/if}}
35
- - Region suffixes only apply to chunks with a real head/body boundary (classes, functions, impl blocks, and similar containers). On code leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` are rejected. Use the unsuffixed selector and supply the complete replacement content, or edit the parent container's `~` body.
36
- - Unsuffixed `write` on a leaf chunk uses your content verbatim after normal replacement; it is not a body-region rewrite. Include the exact indentation and punctuation the leaf needs in the file.
37
- - `^` head writes and `~` body writes use the same base-indent model: write content at column 0 relative to the target region, and the tool applies the chunk's file indentation.
38
- - `write` and `delete` require the current ID. `prepend`/`append` do not.
39
- - **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `read(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit.
40
- - Same-file edit batches are transactional: if any operation in that file fails, no changes from that file's batch are saved. Multi-file edit calls run per file, so a later file error does not roll back earlier files that already succeeded.
41
- </rules>
42
-
43
- <critical>
44
- You **MUST** use the narrowest region that covers your change. Putting without a region overwrites the **entire chunk including leading comments, decorators, and attributes** — omitting them from `content` deletes them.
45
-
46
- **`put` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before using `put` on any chunk's `~`, verify the chunk does not contain children you intend to keep. If a chunk spans hundreds of lines and your change touches only a few, target a specific child chunk — not the parent.
47
-
48
- **Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). `put` on a group chunk's `~` overwrites **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `read` output — do not `put` the parent group.
49
- </critical>
50
-
51
- <regions>
52
- In `read` output, lines marked `^` between the line number and `|` are **head** lines (doc comments, attributes/decorators, signature). Lines without `^` are **body** lines. Use this to decide which region to target:
53
- - `fn_foo#ID~` — **body only (the default choice for most edits).** Head lines (`^`) are preserved automatically — doc comments, attributes, and signature stay untouched. On code leaf chunks, this is rejected because there is no safe body boundary.
54
- - `fn_foo#ID^` — head only (decorators, attributes, doc comments, signature, opening delimiter). Body stays untouched.
55
- - `fn_foo#ID` — entire chunk including leading trivia. **You must include doc comments and attributes in `content`; omitting them deletes them.**
56
- - `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*. Appending to a container without `~` emits a warning because it lands after the closing delimiter, not before it.
57
-
58
- **Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. JSDoc directly above a plain function is more likely to be absorbed into that function's `^`. If you need to rewrite a decorated member, run `read(path="file", sel="?")` and check for a sibling `chunk#ID` directly above your target.
59
-
60
- **Python notes:** Python docstrings are body lines, not head lines. A `~` body write on a function that has a docstring deletes the docstring unless you include the docstring in `content`. Python enum members and nested functions/closures are often opaque inside their parent chunk and may not appear as addressable child chunks; rewrite the parent container body. Python decorated class/function `^` writes and Python `^` deletes are rejected because indentation-sensitive bodies can become attached to the wrong block while still parsing.
61
-
62
- **Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, frontmatter), unsupported `^` and `~` suffixes warn and fall back to whole-chunk editing. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers, table rows, headings) in your `content` — omitting them deletes them. For markdown sections (`sect_*`), prefer unsuffixed whole-chunk replace because `^`/`~` on prose sections can replace the heading and child content too; if you only need the heading, target the heading child chunk shown in `sel="?"`. Fenced code blocks with a declared language are parsed again and can expose inner chunks such as `code_py#ID.fn_gre#ID`; target those inner chunks when available. Markdown root writes preserve fenced code indentation verbatim. Recognized pipe tables expose `row_N` children for row-level edits; table cells and list items are not independently addressable, so rewrite the whole list/table chunk for those structural changes. Appending a table-row-shaped string (`| value |`) to a table chunk inserts it before the trailing blank-line separator so it remains part of the table. Otherwise read with `raw` first and preserve the exact whitespace inside fences. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) — not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
63
- </regions>
64
-
65
- <ops>
66
- Each edit entry has `path` (`file:selector`) plus **exactly one** operation field — `write`, `insert`, or `delete`. Never set more than one on the same entry. `write:null`, `write:""`, and bare `{path}` entries are rejected; they do not delete.
67
-
68
- |fields|path (selector part)|effect|
69
- |---|---|---|
70
- |`write: "content"`|`file:chunk#ID`, `file:chunk#ID~`, or `file:chunk#ID^`|write complete new content to the region|
71
- |`delete: true`|`file:chunk#ID`|delete the chunk explicitly|
72
- |`insert: {loc, body}`|`file:chunk` or `file:chunk~`|insert before/after the chunk (`loc`: `"prepend"` or `"append"`)|
73
- </ops>
74
-
75
- <examples>
76
- Given this `read` output for `counter.rs`:
77
- ```
78
- | counter.rs·62L·rust·#anth
79
- |
80
- @imp#erhe
81
- 1 |use std::fmt;
82
- |
83
- @struct_Counte#onat
84
- 3^|/// A simple counter that tracks a value and its history.
85
- 4^|#[derive(Debug, Clone)]
86
- 5^|pub struct Counter {
87
- -@struct_Counte.field_value#enth
88
- 6 | /// The current value.
89
- 7 | value: i32,
90
- -@struct_Counte.field_max#seti
91
- 8 | /// Maximum allowed value.
92
- 9 | max: i32,
93
- 10 |}
94
- |
95
- @impl_Counte#reha
96
- 12^|impl Counter {
97
- -@impl_Counte.fn_new#ndas
98
- 13^| /// Creates a new counter starting at zero.
99
- 14^| pub fn new(max: i32) -> Self {
100
- 15 | Self { value: 0, max }
101
- 16 | }
102
- 17 |
103
- -@impl_Counte.fn_increm#ouer
104
- 18^| /// Increments the counter by one, clamping at max.
105
- 19^| pub fn increment(&mut self) {
106
- 20 | if self.value < self.max {
107
- 21 | self.value += 1;
108
- 22 | }
109
- 23 | }
110
- 24 |
111
- -@impl_Counte.fn_decrem#arve
112
- 25^| /// Decrements the counter by one, clamping at zero.
113
- 26^| pub fn decrement(&mut self) {
114
- 27 | if self.value > 0 {
115
- 28 | self.value -= 1;
116
- 29 | }
117
- 30 | }
118
- 31 |
119
- -@impl_Counte.fn_get#arco
120
- 32^| /// Returns the current value.
121
- 33^| pub fn get(&self) -> i32 {
122
- 34 | self.value
123
- 35 | }
124
- 36 |}
125
- |
126
- @impl_Displa#meha
127
- 38^|impl fmt::Display for Counter {
128
- -@impl_Displa.fn_fmt#deri
129
- 39^| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130
- 40 | write!(f, "Counter({}/{})", self.value, self.max)
131
- 41 | }
132
- 42 |}
133
- ```
134
- Lines marked `^` between the line number and `|` are **head** lines (doc comments, attributes, signature). Lines without `^` are **body** lines. `~` replaces body lines only; `^` replaces head lines only.
135
-
136
- # Put body (`~` — the common case)
137
- `{ "path": "counter.rs:impl_Counte.fn_increm#ouer~", "write": "self.value = (self.value + 1).min(self.max);\n" }`
138
- Only body changes; doc comment, signature, and closing `}` are preserved.
139
- # Write whole chunk (rewrite signature + doc + body)
140
- `{ "path": "counter.rs:impl_Counte.fn_increm#ouer", "write": "/// Increments by the given step, clamping at max.\npub fn increment(&mut self, step: i32) {\n\tself.value = (self.value + step).min(self.max);\n}\n" }`
141
- Everything is rewritten. Omitting the doc comment or signature deletes them.
142
- # Write head (`^` — attributes, doc comments, signature)
143
- `{ "path": "counter.rs:impl_Counte.fn_get#arco^", "write": "/// Returns the current counter value.\n#[inline]\npub fn get(&self) → i32 {\n" }`
144
- Head changes (all `^` lines + opening brace); body untouched.
145
- # Insert before a chunk (`prepend`)
146
- `{ "path": "counter.rs:impl_Counte.fn_get", "insert": { "loc": "prepend", "body": "/// Resets the counter to zero.\npub fn reset(&mut self) {\n\tself.value = 0;\n}\n\n" } }`
147
- # Insert after a chunk (`append`)
148
- `{ "path": "counter.rs:struct_Counte", "insert": { "loc": "append", "body": "\nimpl Default for Counter {\n\tfn default() → Self {\n\t\tSelf { value: 0, max: 100 }\n\t}\n}\n" } }`
149
- # Insert at start of container body (`~` + `prepend`)
150
- `{ "path": "counter.rs:impl_Counte~", "insert": { "loc": "prepend", "body": "/// Creates a counter starting at the given value.\npub fn with_value(value: i32, max: i32) → Self {\n\tSelf { value: value.min(max), max }\n}\n\n" } }`
151
- Lands at the top of the impl body, before existing methods.
152
- # Insert at end of container body (`~` + `append`)
153
- `{ "path": "counter.rs:impl_Counte~", "insert": { "loc": "append", "body": "\n/// Returns true if the counter is at its maximum.\npub fn is_maxed(&self) → bool {\n\tself.value ≥ self.max\n}\n" } }`
154
- Lands at the end of the impl body, before the closing `}`.
155
- # Delete a chunk
156
- `{ "path": "counter.rs:impl_Counte.fn_decrem#arve", "delete": true }`
157
- Removes the method including its doc comment and signature.
158
- </examples>
@@ -1,5 +0,0 @@
1
- Blocks until one or more background jobs complete, fail, or are cancelled.
2
-
3
- You **MUST** use the `poll` tool instead of polling `read jobs://` in a loop when you need to wait for background task or bash results before continuing.
4
-
5
- Returns the status and results of all watched jobs once at least one finishes.
@@ -1,73 +0,0 @@
1
- Reads files using syntax-aware chunks. Also inspects directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**.
2
-
3
- <instruction>
4
- The chunk-aware `read` variant returns AST-scoped chunks with current checksum IDs for structural editing, and otherwise behaves like `open` for non-code content.
5
- - You **MUST** parallelize calls when exploring related files
6
- - For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content.
7
-
8
- ## Parameters
9
- - `path` — file path or URL; may include `:selector` suffix (required)
10
- - `sel` — optional selector for chunks, line ranges, listing, or raw mode
11
- - `timeout` — seconds, for URLs only
12
-
13
- ## Selectors
14
-
15
- |`sel` value|Behavior|
16
- |---|---|
17
- |*(omitted)*|Read full file as chunks (up to {{DEFAULT_LIMIT}} lines)|
18
- |`class_Foo`|Read a specific chunk|
19
- |`class_Foo.fn_bar#thth~`|Read a chunk region (body `~` / head `^`) by ID|
20
- |`?`|List all chunk paths with IDs|
21
- |`L50`|Read from line 50 onward (shorthand for L50 to EOF)|
22
- |`L50-L120`|Read lines 50 through 120|
23
- |`L20-L20`|Read exactly one line|
24
- |`raw`|Raw content without transformations (for URLs: untouched HTML)|
25
-
26
- Max {{DEFAULT_MAX_LINES}} lines per call.
27
-
28
- # Chunks
29
- Each anchor `@full.chunk.path#thth` (with `-` prefixes for nesting depth) in the output identifies a chunk. Use `full.chunk.path#thth` as-is to read truncated chunks.
30
- If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with IDs and is the safest structural discovery mode. Summary lines in this listing are orientation hints; follow a selector with `read(path="file", sel="chunk#ID")` or use `raw` when you need exact source.
31
- Line numbers in the gutter are absolute file line numbers.
32
-
33
- {{#if chunkAutoIndent}}
34
- Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
35
- {{else}}
36
- Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
37
- {{/if}}
38
- `raw` shows the file's literal whitespace. Structured chunk views may normalize or display indentation for edit round-tripping, so use `raw` when exact tabs/spaces matter, especially inside markdown fenced code blocks.
39
-
40
- IDs change after every edit. Use the new IDs from the edit response or refresh with `sel="?"` before the next `write`/`delete`. `insert` selectors may omit IDs, but still prefer fresh paths after structural edits.
41
-
42
- Parser boundaries vary by language: TypeScript/JavaScript decorators and JSDoc above decorated methods may appear as sibling `chunk#ID` entries, Python decorators are part of the function/class head, Python docstrings are body lines, and Python enum members or nested closures may remain opaque inside their parent chunk. Decorated Python `^` writes and Python `^` deletes are rejected for safety.
43
- Markdown sections, lists, and tables are structural chunks. Recognized pipe tables expose `row_N` children for row-level edits; list items and table cells are not independently addressable. Fenced code blocks with a declared language are parsed again when possible, so functions inside a markdown fence can appear as addressable nested chunks.
44
-
45
- Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
46
- # Inspection
47
- Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Can inspect images.
48
-
49
- # Directories & Archives
50
- Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents.
51
-
52
- # SQLite Databases
53
- When used against a SQLite database (`.sqlite`, `.sqlite3`, `.db`, `.db3`), returns structured database content.
54
- - `file.db` — list tables with row counts
55
- - `file.db:table` — table schema + sample rows
56
- - `file.db:table:key` — single row by primary key
57
- - `file.db:table?limit=50&offset=100` — paginated rows
58
- - `file.db:table?where=status='active'&order=created:desc` — filtered rows
59
- - `file.db?q=SELECT …` — read-only SELECT query
60
-
61
- # URLs
62
- 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. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling).
63
- </instruction>
64
-
65
- <critical>
66
- - You **MUST** `read` before editing — never invent chunk names or IDs.
67
- - Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `read` or `?` output — never construct them from source identifiers.
68
- - You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads.
69
- - You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows.
70
- - You **MUST** always include the `path` parameter; never call with `{}`.
71
- - For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`.
72
- - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
73
- </critical>
@@ -1,95 +0,0 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { prompt } from "@oh-my-pi/pi-utils";
3
- import { type Static, Type } from "@sinclair/typebox";
4
- import { isBackgroundJobSupportEnabled } from "../async";
5
- import cancelJobDescription from "../prompts/tools/cancel-job.md" with { type: "text" };
6
- import type { ToolSession } from "./index";
7
-
8
- const cancelJobSchema = Type.Object({
9
- job_id: Type.String({ description: "background job id", examples: ["job-1234"] }),
10
- });
11
-
12
- type CancelJobParams = Static<typeof cancelJobSchema>;
13
-
14
- export interface CancelJobToolDetails {
15
- status: "cancelled" | "not_found" | "already_completed";
16
- jobId: string;
17
- }
18
-
19
- export class CancelJobTool implements AgentTool<typeof cancelJobSchema, CancelJobToolDetails> {
20
- readonly name = "cancel_job";
21
- readonly label = "CancelJob";
22
- readonly description: string;
23
- readonly parameters = cancelJobSchema;
24
- readonly strict = true;
25
-
26
- constructor(private readonly session: ToolSession) {
27
- this.description = prompt.render(cancelJobDescription);
28
- }
29
-
30
- static createIf(session: ToolSession): CancelJobTool | null {
31
- if (!isBackgroundJobSupportEnabled(session.settings)) return null;
32
- return new CancelJobTool(session);
33
- }
34
-
35
- async execute(
36
- _toolCallId: string,
37
- params: CancelJobParams,
38
- _signal?: AbortSignal,
39
- _onUpdate?: AgentToolUpdateCallback<CancelJobToolDetails>,
40
- _context?: AgentToolContext,
41
- ): Promise<AgentToolResult<CancelJobToolDetails>> {
42
- const manager = this.session.asyncJobManager;
43
- if (!manager) {
44
- return {
45
- content: [
46
- { type: "text", text: "Async execution is disabled; no background jobs are available to cancel." },
47
- ],
48
- details: {
49
- status: "not_found",
50
- jobId: params.job_id,
51
- },
52
- };
53
- }
54
-
55
- const existing = manager.getJob(params.job_id);
56
- if (!existing) {
57
- return {
58
- content: [{ type: "text", text: `Background job not found: ${params.job_id}` }],
59
- details: {
60
- status: "not_found",
61
- jobId: params.job_id,
62
- },
63
- };
64
- }
65
-
66
- if (existing.status !== "running") {
67
- return {
68
- content: [{ type: "text", text: `Background job ${params.job_id} is already ${existing.status}.` }],
69
- details: {
70
- status: "already_completed",
71
- jobId: params.job_id,
72
- },
73
- };
74
- }
75
-
76
- const cancelled = manager.cancel(params.job_id);
77
- if (!cancelled) {
78
- return {
79
- content: [{ type: "text", text: `Background job ${params.job_id} is already completed.` }],
80
- details: {
81
- status: "already_completed",
82
- jobId: params.job_id,
83
- },
84
- };
85
- }
86
-
87
- return {
88
- content: [{ type: "text", text: `Cancelled background job ${params.job_id}.` }],
89
- details: {
90
- status: "cancelled",
91
- jobId: params.job_id,
92
- },
93
- };
94
- }
95
- }
@@ -1,173 +0,0 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { prompt } from "@oh-my-pi/pi-utils";
3
- import { type Static, Type } from "@sinclair/typebox";
4
- import { isBackgroundJobSupportEnabled } from "../async";
5
- import pollDescription from "../prompts/tools/poll.md" with { type: "text" };
6
- import type { ToolSession } from "./index";
7
-
8
- const pollSchema = Type.Object({
9
- jobs: Type.Optional(
10
- Type.Array(Type.String(), {
11
- description: "job ids to wait for",
12
- examples: [["job-1234"]],
13
- }),
14
- ),
15
- });
16
-
17
- type PollParams = Static<typeof pollSchema>;
18
-
19
- interface PollResult {
20
- id: string;
21
- type: "bash" | "task";
22
- status: "running" | "completed" | "failed" | "cancelled";
23
- label: string;
24
- durationMs: number;
25
- resultText?: string;
26
- errorText?: string;
27
- }
28
-
29
- export interface PollToolDetails {
30
- jobs: PollResult[];
31
- }
32
-
33
- export class PollTool implements AgentTool<typeof pollSchema, PollToolDetails> {
34
- readonly name = "poll";
35
- readonly label = "Poll";
36
- readonly description: string;
37
- readonly parameters = pollSchema;
38
- readonly strict = true;
39
-
40
- constructor(private readonly session: ToolSession) {
41
- this.description = prompt.render(pollDescription);
42
- }
43
-
44
- static createIf(session: ToolSession): PollTool | null {
45
- if (!isBackgroundJobSupportEnabled(session.settings)) return null;
46
- return new PollTool(session);
47
- }
48
-
49
- async execute(
50
- _toolCallId: string,
51
- params: PollParams,
52
- signal?: AbortSignal,
53
- _onUpdate?: AgentToolUpdateCallback<PollToolDetails>,
54
- _context?: AgentToolContext,
55
- ): Promise<AgentToolResult<PollToolDetails>> {
56
- const manager = this.session.asyncJobManager;
57
- if (!manager) {
58
- return {
59
- content: [{ type: "text", text: "Async execution is disabled; no background jobs to poll." }],
60
- details: { jobs: [] },
61
- };
62
- }
63
-
64
- const requestedIds = params.jobs;
65
-
66
- // Resolve which jobs to watch
67
- const jobsToWatch = requestedIds?.length
68
- ? requestedIds.map(id => manager.getJob(id)).filter(j => j != null)
69
- : manager.getRunningJobs();
70
-
71
- if (jobsToWatch.length === 0) {
72
- const message = requestedIds?.length
73
- ? `No matching jobs found for IDs: ${requestedIds.join(", ")}`
74
- : "No running background jobs to wait for.";
75
- return {
76
- content: [{ type: "text", text: message }],
77
- details: { jobs: [] },
78
- };
79
- }
80
-
81
- // If all watched jobs are already done, return immediately
82
- const runningJobs = jobsToWatch.filter(j => j.status === "running");
83
- if (runningJobs.length === 0) {
84
- return this.#buildResult(manager, jobsToWatch);
85
- }
86
-
87
- // Block until at least one running job finishes or the call is aborted
88
- const racePromises: Promise<unknown>[] = runningJobs.map(j => j.promise);
89
- const watchedJobIds = runningJobs.map(job => job.id);
90
- manager.watchJobs(watchedJobIds);
91
-
92
- try {
93
- if (signal) {
94
- const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<void>();
95
- const onAbort = () => abortResolve();
96
- signal.addEventListener("abort", onAbort, { once: true });
97
- racePromises.push(abortPromise);
98
- try {
99
- await Promise.race(racePromises);
100
- } finally {
101
- signal.removeEventListener("abort", onAbort);
102
- }
103
- } else {
104
- await Promise.race(racePromises);
105
- }
106
- } finally {
107
- manager.unwatchJobs(watchedJobIds);
108
- }
109
-
110
- if (signal?.aborted) {
111
- return this.#buildResult(manager, jobsToWatch);
112
- }
113
-
114
- return this.#buildResult(manager, jobsToWatch);
115
- }
116
-
117
- #buildResult(
118
- manager: NonNullable<ToolSession["asyncJobManager"]>,
119
- jobs: {
120
- id: string;
121
- type: "bash" | "task";
122
- status: string;
123
- label: string;
124
- startTime: number;
125
- resultText?: string;
126
- errorText?: string;
127
- }[],
128
- ): AgentToolResult<PollToolDetails> {
129
- const now = Date.now();
130
- const jobResults: PollResult[] = jobs.map(j => ({
131
- id: j.id,
132
- type: j.type,
133
- status: j.status as PollResult["status"],
134
- label: j.label,
135
- durationMs: Math.max(0, now - j.startTime),
136
- ...(j.resultText ? { resultText: j.resultText } : {}),
137
- ...(j.errorText ? { errorText: j.errorText } : {}),
138
- }));
139
-
140
- manager.acknowledgeDeliveries(jobResults.filter(j => j.status !== "running").map(j => j.id));
141
-
142
- const completed = jobResults.filter(j => j.status !== "running");
143
- const running = jobResults.filter(j => j.status === "running");
144
-
145
- const lines: string[] = [];
146
- if (completed.length > 0) {
147
- lines.push(`## Completed (${completed.length})\n`);
148
- for (const j of completed) {
149
- lines.push(`### ${j.id} [${j.type}] — ${j.status}`);
150
- lines.push(`Label: ${j.label}`);
151
- if (j.resultText) {
152
- lines.push("```", j.resultText, "```");
153
- }
154
- if (j.errorText) {
155
- lines.push(`Error: ${j.errorText}`);
156
- }
157
- lines.push("");
158
- }
159
- }
160
-
161
- if (running.length > 0) {
162
- lines.push(`## Still Running (${running.length})\n`);
163
- for (const j of running) {
164
- lines.push(`- \`${j.id}\` [${j.type}] — ${j.label}`);
165
- }
166
- }
167
-
168
- return {
169
- content: [{ type: "text", text: lines.join("\n") }],
170
- details: { jobs: jobResults },
171
- };
172
- }
173
- }