@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -1
- package/package.json +7 -7
- package/src/autoresearch/prompt.md +1 -1
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +4 -4
- package/src/cursor.ts +3 -8
- package/src/discovery/helpers.ts +3 -3
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +86 -57
- package/src/edit/line-hash.ts +743 -24
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +893 -0
- package/src/edit/modes/chunk.ts +14 -24
- package/src/edit/modes/hashline.ts +193 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +14 -10
- package/src/edit/streaming.ts +50 -16
- package/src/exec/bash-executor.ts +2 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/defaults.json +142 -652
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/components/assistant-message.ts +4 -0
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +1 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/print-mode.ts +8 -0
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +16 -20
- package/src/prompts/tools/ast-grep.md +19 -24
- package/src/prompts/tools/atom.md +87 -0
- package/src/prompts/tools/chunk-edit.md +37 -161
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +4 -5
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +2 -3
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +14 -9
- package/src/session/agent-session.ts +25 -2
- package/src/session/session-manager.ts +4 -1
- package/src/task/executor.ts +43 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +8 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +509 -697
- package/src/tools/grep.ts +116 -131
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +8 -7
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -25
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -12
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
|
@@ -50,25 +50,18 @@ Returns success/failure; on failure, error message indicates:
|
|
|
50
50
|
- **NEVER** use edit to fix indentation, whitespace, or reformat code. Formatting is a single command run once at the end (`bun fmt`, `cargo fmt`, `prettier —write`, etc.)—not N individual edits. If you see inconsistent indentation after an edit, leave it; the formatter will fix all of it in one pass.
|
|
51
51
|
</critical>
|
|
52
52
|
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<example name="delete">
|
|
66
|
-
edit {"edits":[{"path":"obsolete.txt","op":"delete"}]}
|
|
67
|
-
</example>
|
|
68
|
-
|
|
69
|
-
<example name="multi-file">
|
|
70
|
-
edit {"edits":[{"path":"src/types.ts","op":"update","diff":"@@\n-old\n+new\n"},{"path":"src/index.ts","op":"update","diff":"@@\n-old\n+new\n"}]}
|
|
71
|
-
</example>
|
|
53
|
+
<examples>
|
|
54
|
+
# Create
|
|
55
|
+
`edit {"edits":[{"path":"hello.txt","op":"create","diff":"Hello\n"}]}`
|
|
56
|
+
# Update
|
|
57
|
+
`edit {"edits":[{"path":"src/app.py","op":"update","diff":"@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n"}]}`
|
|
58
|
+
# Rename
|
|
59
|
+
`edit {"edits":[{"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n …\n"}]}`
|
|
60
|
+
# Delete
|
|
61
|
+
`edit {"edits":[{"path":"obsolete.txt","op":"delete"}]}`
|
|
62
|
+
# Multi-file
|
|
63
|
+
`edit {"edits":[{"path":"src/types.ts","op":"update","diff":"@@\n-old\n+new\n"},{"path":"src/index.ts","op":"update","diff":"@@\n-old\n+new\n"}]}`
|
|
64
|
+
</examples>
|
|
72
65
|
|
|
73
66
|
<avoid>
|
|
74
67
|
- Generic anchors: `import`, `export`, `describe`, `function`, `const`
|
|
@@ -44,7 +44,8 @@ User sees output like Jupyter notebook; rich displays render fully:
|
|
|
44
44
|
- You **MUST** use `run()` for shell commands; you **MUST NOT** use raw `subprocess`
|
|
45
45
|
</critical>
|
|
46
46
|
|
|
47
|
-
<
|
|
47
|
+
<examples>
|
|
48
|
+
# Multiple small cells
|
|
48
49
|
```python
|
|
49
50
|
cells: [
|
|
50
51
|
{"title": "imports", "code": "import json\nfrom pathlib import Path"},
|
|
@@ -53,4 +54,4 @@ cells: [
|
|
|
53
54
|
{"title": "use helper", "code": "configs = [parse_config(p) for p in Path('.').glob('*.json')]"}
|
|
54
55
|
]
|
|
55
56
|
```
|
|
56
|
-
</
|
|
57
|
+
</examples>
|
|
@@ -2,7 +2,6 @@ Reads files using syntax-aware chunks. Also inspects directories, archives, SQLi
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
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
|
-
|
|
6
5
|
- You **MUST** parallelize calls when exploring related files
|
|
7
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.
|
|
8
7
|
|
|
@@ -17,7 +16,7 @@ The chunk-aware `read` variant returns AST-scoped chunks with current checksum I
|
|
|
17
16
|
|---|---|
|
|
18
17
|
|*(omitted)*|Read full file as chunks (up to {{DEFAULT_LIMIT}} lines)|
|
|
19
18
|
|`class_Foo`|Read a specific chunk|
|
|
20
|
-
|`class_Foo.fn_bar#
|
|
19
|
+
|`class_Foo.fn_bar#thth~`|Read a chunk region (body `~` / head `^`) by ID|
|
|
21
20
|
|`?`|List all chunk paths with IDs|
|
|
22
21
|
|`L50`|Read from line 50 onward (shorthand for L50 to EOF)|
|
|
23
22
|
|`L50-L120`|Read lines 50 through 120|
|
|
@@ -27,7 +26,7 @@ The chunk-aware `read` variant returns AST-scoped chunks with current checksum I
|
|
|
27
26
|
Max {{DEFAULT_MAX_LINES}} lines per call.
|
|
28
27
|
|
|
29
28
|
# Chunks
|
|
30
|
-
Each anchor `@full.chunk.path#
|
|
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.
|
|
31
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.
|
|
32
31
|
Line numbers in the gutter are absolute file line numbers.
|
|
33
32
|
|
|
@@ -18,13 +18,13 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
|
|
|
18
18
|
|`L50`|Read from line 50 onward (shorthand for L50 to EOF)|
|
|
19
19
|
|`L50-L120`|Read lines 50 through 120|
|
|
20
20
|
|`L20-L20`|Read exactly one line|
|
|
21
|
-
|`raw`|
|
|
21
|
+
|`raw`|Skip line-numbering / hashline / chunking; return file content as plain text. For URLs: untouched HTML.|
|
|
22
22
|
|
|
23
23
|
Max {{DEFAULT_MAX_LINES}} lines per call.
|
|
24
24
|
|
|
25
25
|
# Filesystem
|
|
26
26
|
{{#if IS_HASHLINE_MODE}}
|
|
27
|
-
- Reading from FS returns lines prefixed with anchors: `
|
|
27
|
+
- Reading from FS returns lines prefixed with anchors: `41th|def alpha():` (line number, 2-letter ID, pipe, then content)
|
|
28
28
|
{{else}}
|
|
29
29
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
30
30
|
- Reading from FS returns lines prefixed with line numbers: `41:def alpha():`
|
package/src/prompts/tools/ssh.md
CHANGED
|
@@ -25,20 +25,11 @@ You **MUST** build commands from the reference below
|
|
|
25
25
|
You **MUST** verify the shell type from "Available hosts" and use matching commands.
|
|
26
26
|
</critical>
|
|
27
27
|
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
Host: server1 (10.0.0.1) | linux/bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Host: winbox (192.168.1.5) | windows/cmd
|
|
37
|
-
Command: `tasklist /v`
|
|
38
|
-
</example>
|
|
39
|
-
|
|
40
|
-
<example name="macos">
|
|
41
|
-
Task: Get system info on "macbook"
|
|
42
|
-
Host: macbook (10.0.0.20) | macos/zsh
|
|
43
|
-
Command: `uname -a && sw_vers`
|
|
44
|
-
</example>
|
|
28
|
+
<examples>
|
|
29
|
+
# List files: Linux
|
|
30
|
+
Host: server1 (10.0.0.1) | linux/bash. Command: `ls -la /home/user`
|
|
31
|
+
# Show running processes: Windows cmd
|
|
32
|
+
Host: winbox (192.168.1.5) | windows/cmd. Command: `tasklist /v`
|
|
33
|
+
# Get system info: macOS
|
|
34
|
+
Host: macbook (10.0.0.20) | macos/zsh. Command: `uname -a && sw_vers`
|
|
35
|
+
</examples>
|
|
@@ -1,28 +1,53 @@
|
|
|
1
|
-
Manages a phased task list
|
|
1
|
+
Manages a phased task list through an `ops` array of flat operations.
|
|
2
2
|
The next pending task is auto-promoted to `in_progress` after completing the current one.
|
|
3
3
|
|
|
4
4
|
<protocol>
|
|
5
|
-
##
|
|
5
|
+
## Shape
|
|
6
|
+
|
|
7
|
+
Pass an object with an `ops` array:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
{
|
|
11
|
+
ops: [
|
|
12
|
+
{ op: "replace", phases: [...] },
|
|
13
|
+
{ op: "start", task: "task-3" },
|
|
14
|
+
{ op: "done", phase: "Implementation" },
|
|
15
|
+
{ op: "rm" },
|
|
16
|
+
{ op: "drop", task: "task-9" },
|
|
17
|
+
{ op: "append", phase: "Implementation", items: [{ id: "task-10", label: "Run tests" }] },
|
|
18
|
+
],
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Operation fields
|
|
6
23
|
|
|
7
24
|
|Field|Type|When to use|
|
|
8
25
|
|---|---|---|
|
|
9
|
-
|`
|
|
10
|
-
|`
|
|
11
|
-
|`
|
|
12
|
-
|`
|
|
13
|
-
|`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
|`op`|string|Required. One of `replace`, `start`, `done`, `rm`, `drop`, `append`|
|
|
27
|
+
|`task`|string|Task id for `start`, or a task target for `done` / `rm` / `drop`|
|
|
28
|
+
|`phase`|string|Phase target for `done` / `rm` / `drop`, or append destination for `append`|
|
|
29
|
+
|`items`|{id, label}[]|Required for `append`. If the phase does not exist, it is created at the end|
|
|
30
|
+
|`phases`|Phase[]|Only for `replace`. Keeps initial phased setup available for harness bootstrap and full restructures|
|
|
31
|
+
|
|
32
|
+
## Semantics
|
|
33
|
+
- `start`: requires `task`; sets that task to `in_progress`
|
|
34
|
+
- `done`: marks one task, one phase, or all tasks completed
|
|
35
|
+
- `rm`: removes one task, one phase's tasks, or all tasks
|
|
36
|
+
- `drop`: marks one task, one phase, or all tasks abandoned
|
|
37
|
+
- `append`: appends `items` to `phase`; creates the phase if missing
|
|
38
|
+
- `replace`: replaces the full todo list
|
|
39
|
+
|
|
40
|
+
If `done`, `rm`, or `drop` omits both `task` and `phase`, it applies to all tasks.
|
|
17
41
|
|
|
18
42
|
## Task Anatomy
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
43
|
+
- `label`: Short label (5-10 words). What is being done, not how.
|
|
44
|
+
- `replace` task `content` should stay short and specific.
|
|
21
45
|
|
|
22
46
|
## Rules
|
|
23
|
-
- Mark tasks
|
|
24
|
-
- Complete phases in order — do not skip ahead while earlier ones are pending
|
|
25
|
-
- On blockers
|
|
47
|
+
- Mark tasks done immediately after finishing — never defer.
|
|
48
|
+
- Complete phases in order — do not skip ahead while earlier ones are pending.
|
|
49
|
+
- On blockers, append a new task to the active phase.
|
|
50
|
+
- Keep ids stable once introduced.
|
|
26
51
|
</protocol>
|
|
27
52
|
|
|
28
53
|
<conditions>
|
|
@@ -33,32 +58,20 @@ Create a todo list when:
|
|
|
33
58
|
4. New instructions arrive mid-task — capture before proceeding
|
|
34
59
|
</conditions>
|
|
35
60
|
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
]}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
<example name="add-task">
|
|
52
|
-
{add_tasks: [{phase: "Implementation", content: "Handle retries", details: "Cap exponential backoff in retry.ts"}]}
|
|
53
|
-
</example>
|
|
54
|
-
|
|
55
|
-
<example name="add-phase">
|
|
56
|
-
{add_phase: {name: "Cleanup", tasks: [{content: "Remove dead code"}]}}
|
|
57
|
-
</example>
|
|
58
|
-
|
|
59
|
-
<example name="combined">
|
|
60
|
-
{complete: ["task-2"], add_notes: [{id: "task-3", notes: "Needs extra validation"}]}
|
|
61
|
-
</example>
|
|
61
|
+
<examples>
|
|
62
|
+
# Initial setup
|
|
63
|
+
`{"ops":[{"op":"replace","phases":[{"name":"Investigation","tasks":[{"content":"Read source"},{"content":"Map callsites"}]},{"name":"Implementation","tasks":[{"content":"Apply fix"},{"content":"Run tests"}]}]}]}`
|
|
64
|
+
# Complete one task
|
|
65
|
+
`{"ops":[{"op":"done","task":"task-2"}]}`
|
|
66
|
+
# Complete a whole phase
|
|
67
|
+
`{"ops":[{"op":"done","phase":"Implementation"}]}`
|
|
68
|
+
# Remove all tasks
|
|
69
|
+
`{"ops":[{"op":"rm"}]}`
|
|
70
|
+
# Drop one task
|
|
71
|
+
`{"ops":[{"op":"drop","task":"task-7"}]}`
|
|
72
|
+
# Append tasks to a phase
|
|
73
|
+
`{"ops":[{"op":"append","phase":"Implementation","items":[{"id":"task-8","label":"Handle retries"},{"id":"task-9","label":"Run tests"}]}]}`
|
|
74
|
+
</examples>
|
|
62
75
|
|
|
63
76
|
<avoid>
|
|
64
77
|
- Single-step tasks — act directly
|
package/src/sdk.ts
CHANGED
|
@@ -128,7 +128,7 @@ import {
|
|
|
128
128
|
warmupLspServers,
|
|
129
129
|
} from "./tools";
|
|
130
130
|
import { ToolContextStore } from "./tools/context";
|
|
131
|
-
import {
|
|
131
|
+
import { getImageGenTools } from "./tools/image-gen";
|
|
132
132
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
133
133
|
import { queueResolveHandler } from "./tools/resolve";
|
|
134
134
|
import { EventBus } from "./utils/event-bus";
|
|
@@ -209,8 +209,8 @@ export interface CreateAgentSessionOptions {
|
|
|
209
209
|
|
|
210
210
|
/** Output schema for structured completion (subagents) */
|
|
211
211
|
outputSchema?: unknown;
|
|
212
|
-
/** Whether to include the
|
|
213
|
-
|
|
212
|
+
/** Whether to include the yield tool by default */
|
|
213
|
+
requireYieldTool?: boolean;
|
|
214
214
|
/** Task recursion depth (for subagent sessions). Default: 0 */
|
|
215
215
|
taskDepth?: number;
|
|
216
216
|
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
|
|
@@ -673,7 +673,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
673
673
|
}
|
|
674
674
|
|
|
675
675
|
const imageProvider = settings.get("providers.image");
|
|
676
|
-
if (
|
|
676
|
+
if (
|
|
677
|
+
imageProvider === "auto" ||
|
|
678
|
+
imageProvider === "openai" ||
|
|
679
|
+
imageProvider === "gemini" ||
|
|
680
|
+
imageProvider === "openrouter"
|
|
681
|
+
) {
|
|
677
682
|
setPreferredImageProvider(imageProvider);
|
|
678
683
|
}
|
|
679
684
|
|
|
@@ -916,7 +921,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
916
921
|
skills,
|
|
917
922
|
eventBus,
|
|
918
923
|
outputSchema: options.outputSchema,
|
|
919
|
-
|
|
924
|
+
requireYieldTool: options.requireYieldTool,
|
|
920
925
|
taskDepth: options.taskDepth ?? 0,
|
|
921
926
|
getSessionFile: () => sessionManager.getSessionFile() ?? null,
|
|
922
927
|
getPythonKernelOwnerId: () => pythonKernelOwnerId,
|
|
@@ -1047,10 +1052,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1047
1052
|
}
|
|
1048
1053
|
toolSession.mcpManager = mcpManager;
|
|
1049
1054
|
|
|
1050
|
-
// Add
|
|
1051
|
-
const
|
|
1052
|
-
if (
|
|
1053
|
-
customTools.push(...(
|
|
1055
|
+
// Add image tools when the active model or configured image providers can generate images.
|
|
1056
|
+
const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
|
|
1057
|
+
if (imageGenTools.length > 0) {
|
|
1058
|
+
customTools.push(...(imageGenTools as unknown as CustomTool[]));
|
|
1054
1059
|
}
|
|
1055
1060
|
|
|
1056
1061
|
// Add web search tools
|
|
@@ -525,6 +525,7 @@ export class AgentSession {
|
|
|
525
525
|
#obfuscator: SecretObfuscator | undefined;
|
|
526
526
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
527
527
|
#pendingRewindReport: string | undefined = undefined;
|
|
528
|
+
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
528
529
|
#promptGeneration = 0;
|
|
529
530
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
530
531
|
|
|
@@ -789,6 +790,9 @@ export class AgentSession {
|
|
|
789
790
|
this.#toolChoiceQueue.resolve();
|
|
790
791
|
}
|
|
791
792
|
}
|
|
793
|
+
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
794
|
+
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
795
|
+
}
|
|
792
796
|
if (event.type === "turn_end" && this.#pendingRewindReport) {
|
|
793
797
|
const report = this.#pendingRewindReport;
|
|
794
798
|
this.#pendingRewindReport = undefined;
|
|
@@ -1026,7 +1030,10 @@ export class AgentSession {
|
|
|
1026
1030
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
1027
1031
|
const msg = this.#lastAssistantMessage ?? fallbackAssistant;
|
|
1028
1032
|
this.#lastAssistantMessage = undefined;
|
|
1029
|
-
if (!msg)
|
|
1033
|
+
if (!msg) {
|
|
1034
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1030
1037
|
|
|
1031
1038
|
// Invalidate GitHub Copilot credentials on auth failure so stale tokens
|
|
1032
1039
|
// aren't reused on the next request
|
|
@@ -1040,8 +1047,15 @@ export class AgentSession {
|
|
|
1040
1047
|
|
|
1041
1048
|
if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
1042
1049
|
this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
|
|
1050
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (this.#assistantEndedWithSuccessfulYield(msg)) {
|
|
1055
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1043
1056
|
return;
|
|
1044
1057
|
}
|
|
1058
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1045
1059
|
|
|
1046
1060
|
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
1047
1061
|
if (this.#isRetryableError(msg)) {
|
|
@@ -3227,7 +3241,6 @@ export class AgentSession {
|
|
|
3227
3241
|
id: task.id,
|
|
3228
3242
|
content: task.content,
|
|
3229
3243
|
status: task.status,
|
|
3230
|
-
notes: task.notes,
|
|
3231
3244
|
})),
|
|
3232
3245
|
}));
|
|
3233
3246
|
}
|
|
@@ -4230,6 +4243,16 @@ export class AgentSession {
|
|
|
4230
4243
|
}
|
|
4231
4244
|
}
|
|
4232
4245
|
}
|
|
4246
|
+
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
4247
|
+
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
4248
|
+
if (!toolCallId) return false;
|
|
4249
|
+
const lastToolCall = assistantMessage.content
|
|
4250
|
+
.slice()
|
|
4251
|
+
.reverse()
|
|
4252
|
+
.find((content): content is ToolCall => content.type === "toolCall");
|
|
4253
|
+
return lastToolCall?.name === "yield" && lastToolCall.id === toolCallId;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4233
4256
|
#enforceRewindBeforeYield(): boolean {
|
|
4234
4257
|
if (!this.#checkpointState || this.#pendingRewindReport) {
|
|
4235
4258
|
return false;
|
|
@@ -259,6 +259,8 @@ export interface SessionInfo {
|
|
|
259
259
|
created: Date;
|
|
260
260
|
modified: Date;
|
|
261
261
|
messageCount: number;
|
|
262
|
+
/** File size in bytes on disk; used for compact list rendering. */
|
|
263
|
+
size: number;
|
|
262
264
|
firstMessage: string;
|
|
263
265
|
allMessagesText: string;
|
|
264
266
|
}
|
|
@@ -1264,7 +1266,7 @@ function extractTextFromContent(content: Message["content"]): string {
|
|
|
1264
1266
|
.join(" ");
|
|
1265
1267
|
}
|
|
1266
1268
|
|
|
1267
|
-
const SESSION_LIST_PREFIX_BYTES =
|
|
1269
|
+
const SESSION_LIST_PREFIX_BYTES = 4096;
|
|
1268
1270
|
const SESSION_LIST_PARALLEL_THRESHOLD = 64;
|
|
1269
1271
|
const SESSION_LIST_MAX_WORKERS = 16;
|
|
1270
1272
|
const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -1466,6 +1468,7 @@ async function collectSessionFromFile(
|
|
|
1466
1468
|
created: new Date(header.timestamp ?? ""),
|
|
1467
1469
|
modified: stats.mtime,
|
|
1468
1470
|
messageCount,
|
|
1471
|
+
size: stats.size,
|
|
1469
1472
|
firstMessage: firstMessage || "(no messages)",
|
|
1470
1473
|
allMessagesText: allMessages.length > 0 ? allMessages.join(" ") : firstMessage,
|
|
1471
1474
|
};
|
package/src/task/executor.ts
CHANGED
|
@@ -18,8 +18,8 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
|
|
|
18
18
|
import type { Skill } from "../extensibility/skills";
|
|
19
19
|
import { callTool } from "../mcp/client";
|
|
20
20
|
import type { MCPManager } from "../mcp/manager";
|
|
21
|
-
import submitReminderTemplate from "../prompts/system/subagent-submit-reminder.md" with { type: "text" };
|
|
22
21
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
22
|
+
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
23
23
|
import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
24
24
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
25
25
|
import type { AuthStorage } from "../session/auth-storage";
|
|
@@ -223,7 +223,7 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
|
|
|
223
223
|
return { data: candidate };
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
export interface
|
|
226
|
+
export interface YieldItem {
|
|
227
227
|
data?: unknown;
|
|
228
228
|
status?: "success" | "aborted";
|
|
229
229
|
error?: string;
|
|
@@ -235,7 +235,7 @@ interface FinalizeSubprocessOutputArgs {
|
|
|
235
235
|
stderr: string;
|
|
236
236
|
doneAborted: boolean;
|
|
237
237
|
signalAborted: boolean;
|
|
238
|
-
|
|
238
|
+
yieldItems?: YieldItem[];
|
|
239
239
|
reportFindings?: ReviewFinding[];
|
|
240
240
|
outputSchema: unknown;
|
|
241
241
|
}
|
|
@@ -244,44 +244,42 @@ interface FinalizeSubprocessOutputResult {
|
|
|
244
244
|
rawOutput: string;
|
|
245
245
|
exitCode: number;
|
|
246
246
|
stderr: string;
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
abortedViaYield: boolean;
|
|
248
|
+
hasYield: boolean;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
export const
|
|
252
|
-
export const
|
|
253
|
-
"SYSTEM WARNING: Subagent exited without calling
|
|
251
|
+
export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
|
|
252
|
+
export const SUBAGENT_WARNING_MISSING_YIELD =
|
|
253
|
+
"SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
|
|
254
254
|
|
|
255
255
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
256
256
|
let { rawOutput, exitCode, stderr } = args;
|
|
257
|
-
const {
|
|
258
|
-
let
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
257
|
+
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
258
|
+
let abortedViaYield = false;
|
|
259
|
+
const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
|
|
260
|
+
|
|
261
|
+
if (hasYield) {
|
|
262
|
+
const lastYield = yieldItems[yieldItems.length - 1];
|
|
263
|
+
if (lastYield?.status === "aborted") {
|
|
264
|
+
abortedViaYield = true;
|
|
265
265
|
exitCode = 0;
|
|
266
|
-
stderr =
|
|
266
|
+
stderr = lastYield.error || "Subagent aborted task";
|
|
267
267
|
try {
|
|
268
|
-
rawOutput = JSON.stringify({ aborted: true, error:
|
|
268
|
+
rawOutput = JSON.stringify({ aborted: true, error: lastYield.error }, null, 2);
|
|
269
269
|
} catch {
|
|
270
|
-
rawOutput = `{"aborted":true,"error":"${
|
|
270
|
+
rawOutput = `{"aborted":true,"error":"${lastYield.error || "Unknown error"}"}`;
|
|
271
271
|
}
|
|
272
272
|
} else {
|
|
273
|
-
const submitData =
|
|
273
|
+
const submitData = lastYield?.data;
|
|
274
274
|
if (submitData === null || submitData === undefined) {
|
|
275
|
-
rawOutput = rawOutput
|
|
276
|
-
? `${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\n${rawOutput}`
|
|
277
|
-
: SUBAGENT_WARNING_NULL_SUBMIT_RESULT;
|
|
275
|
+
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
278
276
|
} else {
|
|
279
277
|
const completeData = normalizeCompleteData(submitData, reportFindings);
|
|
280
278
|
try {
|
|
281
279
|
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
282
280
|
} catch (err) {
|
|
283
281
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
284
|
-
rawOutput = `{"error":"Failed to serialize
|
|
282
|
+
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
285
283
|
}
|
|
286
284
|
exitCode = 0;
|
|
287
285
|
stderr = "";
|
|
@@ -307,17 +305,15 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
307
305
|
stderr = "";
|
|
308
306
|
} else if (exitCode === 0) {
|
|
309
307
|
const hasRawOutput = rawOutput.trim().length > 0;
|
|
310
|
-
rawOutput = rawOutput
|
|
311
|
-
? `${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\n${rawOutput}`
|
|
312
|
-
: SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
|
|
308
|
+
rawOutput = rawOutput ? `${SUBAGENT_WARNING_MISSING_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_MISSING_YIELD;
|
|
313
309
|
if (hasOutputSchema || !hasRawOutput) {
|
|
314
310
|
exitCode = 1;
|
|
315
|
-
stderr =
|
|
311
|
+
stderr = SUBAGENT_WARNING_MISSING_YIELD;
|
|
316
312
|
}
|
|
317
313
|
}
|
|
318
314
|
}
|
|
319
315
|
|
|
320
|
-
return { rawOutput, exitCode, stderr,
|
|
316
|
+
return { rawOutput, exitCode, stderr, abortedViaYield, hasYield };
|
|
321
317
|
}
|
|
322
318
|
|
|
323
319
|
/**
|
|
@@ -564,7 +560,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
564
560
|
const abortSignal = abortController.signal;
|
|
565
561
|
let activeSession: AgentSession | null = null;
|
|
566
562
|
let unsubscribe: (() => void) | null = null;
|
|
567
|
-
let
|
|
563
|
+
let yieldCalled = false;
|
|
568
564
|
|
|
569
565
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
570
566
|
const accumulatedUsage = {
|
|
@@ -789,8 +785,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
789
785
|
existing.push(data);
|
|
790
786
|
}
|
|
791
787
|
progress.extractedToolData[event.toolName] = existing;
|
|
792
|
-
if (event.toolName === "
|
|
793
|
-
|
|
788
|
+
if (event.toolName === "yield") {
|
|
789
|
+
yieldCalled = true;
|
|
794
790
|
}
|
|
795
791
|
}
|
|
796
792
|
}
|
|
@@ -955,7 +951,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
955
951
|
thinkingLevel: effectiveThinkingLevel,
|
|
956
952
|
toolNames,
|
|
957
953
|
outputSchema,
|
|
958
|
-
|
|
954
|
+
requireYieldTool: true,
|
|
959
955
|
contextFiles: options.contextFiles,
|
|
960
956
|
skills: options.skills,
|
|
961
957
|
promptTemplates: options.promptTemplates,
|
|
@@ -1070,7 +1066,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1070
1066
|
await extensionRunner.emit({ type: "session_start" });
|
|
1071
1067
|
}
|
|
1072
1068
|
|
|
1073
|
-
const
|
|
1069
|
+
const MAX_YIELD_RETRIES = 3;
|
|
1074
1070
|
unsubscribe = session.subscribe(event => {
|
|
1075
1071
|
if (isAgentEvent(event)) {
|
|
1076
1072
|
try {
|
|
@@ -1087,15 +1083,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1087
1083
|
await session.prompt(task, { attribution: "agent" });
|
|
1088
1084
|
await session.waitForIdle();
|
|
1089
1085
|
|
|
1090
|
-
const reminderToolChoice = buildNamedToolChoice("
|
|
1086
|
+
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1091
1087
|
|
|
1092
1088
|
let retryCount = 0;
|
|
1093
|
-
while (!
|
|
1089
|
+
while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1094
1090
|
try {
|
|
1095
1091
|
retryCount++;
|
|
1096
1092
|
const reminder = prompt.render(submitReminderTemplate, {
|
|
1097
1093
|
retryCount,
|
|
1098
|
-
maxRetries:
|
|
1094
|
+
maxRetries: MAX_YIELD_RETRIES,
|
|
1099
1095
|
});
|
|
1100
1096
|
|
|
1101
1097
|
await session.prompt(reminder, {
|
|
@@ -1111,7 +1107,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1111
1107
|
}
|
|
1112
1108
|
|
|
1113
1109
|
await session.waitForIdle();
|
|
1114
|
-
if (!
|
|
1110
|
+
if (!yieldCalled && !abortSignal.aborted) {
|
|
1115
1111
|
exitCode = 0;
|
|
1116
1112
|
}
|
|
1117
1113
|
|
|
@@ -1186,7 +1182,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1186
1182
|
|
|
1187
1183
|
// Use final output if available, otherwise accumulated output
|
|
1188
1184
|
let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
|
|
1189
|
-
const
|
|
1185
|
+
const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
|
|
1190
1186
|
const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
|
|
1191
1187
|
const finalized = finalizeSubprocessOutput({
|
|
1192
1188
|
rawOutput,
|
|
@@ -1194,17 +1190,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1194
1190
|
stderr,
|
|
1195
1191
|
doneAborted: Boolean(done.aborted),
|
|
1196
1192
|
signalAborted: Boolean(signal?.aborted),
|
|
1197
|
-
|
|
1193
|
+
yieldItems,
|
|
1198
1194
|
reportFindings,
|
|
1199
1195
|
outputSchema,
|
|
1200
1196
|
});
|
|
1201
1197
|
rawOutput = finalized.rawOutput;
|
|
1202
1198
|
exitCode = finalized.exitCode;
|
|
1203
1199
|
stderr = finalized.stderr;
|
|
1204
|
-
const
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
const { abortedViaSubmitResult, hasSubmitResult } = finalized;
|
|
1200
|
+
const lastYield = yieldItems?.[yieldItems.length - 1];
|
|
1201
|
+
const yieldAbortReason = lastYield?.status === "aborted" ? lastYield.error || "Subagent aborted task" : undefined;
|
|
1202
|
+
const { abortedViaYield, hasYield } = finalized;
|
|
1208
1203
|
const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
|
|
1209
1204
|
maxBytes: MAX_OUTPUT_BYTES,
|
|
1210
1205
|
maxLines: MAX_OUTPUT_LINES,
|
|
@@ -1228,16 +1223,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1228
1223
|
}
|
|
1229
1224
|
|
|
1230
1225
|
// Update final progress
|
|
1231
|
-
const wasAborted =
|
|
1226
|
+
const wasAborted = abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
|
|
1232
1227
|
const finalAbortReason = wasAborted
|
|
1233
|
-
?
|
|
1234
|
-
?
|
|
1228
|
+
? abortedViaYield
|
|
1229
|
+
? yieldAbortReason
|
|
1235
1230
|
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : "Subagent aborted task"))
|
|
1236
1231
|
: undefined;
|
|
1237
1232
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1238
1233
|
scheduleProgress(true);
|
|
1239
1234
|
|
|
1240
|
-
// Emit lifecycle end event after finalization so
|
|
1235
|
+
// Emit lifecycle end event after finalization so yield status is reflected
|
|
1241
1236
|
if (options.eventBus) {
|
|
1242
1237
|
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
1243
1238
|
id,
|