@oh-my-pi/pi-coding-agent 14.6.5 → 14.7.0
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 +47 -0
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +7 -4
- package/examples/sdk/README.md +1 -1
- package/package.json +7 -7
- package/src/autoresearch/index.ts +48 -44
- package/src/cli/read-cli.ts +58 -0
- package/src/cli.ts +1 -0
- package/src/commands/read.ts +40 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/analysis/conventional.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/map-reduce/map-phase.ts +1 -1
- package/src/commit/map-reduce/reduce-phase.ts +1 -1
- package/src/config/settings-schema.ts +39 -0
- package/src/edit/line-hash.ts +34 -4
- package/src/edit/modes/hashline.ts +221 -7
- package/src/edit/streaming.ts +4 -1
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/extensions/runner.ts +3 -3
- package/src/extensibility/extensions/types.ts +4 -4
- package/src/main.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/custom-editor.ts +4 -5
- package/src/modes/components/read-tool-group.ts +4 -9
- package/src/modes/components/tool-execution.ts +4 -0
- package/src/modes/controllers/event-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +3 -1
- package/src/modes/interactive-mode.ts +24 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/utils/context-usage.ts +12 -5
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/project-prompt.md +36 -0
- package/src/prompts/system/system-prompt.md +0 -29
- package/src/prompts/tools/github.md +1 -0
- package/src/prompts/tools/hashline.md +24 -6
- package/src/prompts/tools/read.md +15 -14
- package/src/sdk.ts +29 -28
- package/src/session/agent-session.ts +20 -12
- package/src/session/compaction/branch-summarization.ts +1 -1
- package/src/session/compaction/compaction.ts +3 -3
- package/src/session/session-dump-format.ts +10 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +1 -1
- package/src/system-prompt.ts +35 -3
- package/src/task/executor.ts +4 -3
- package/src/tools/fetch.ts +4 -4
- package/src/tools/gh.ts +187 -0
- package/src/tools/image-gen.ts +3 -1
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/path-utils.ts +11 -0
- package/src/tools/read.ts +388 -204
- package/src/tools/search.ts +1 -1
- package/src/tools/sqlite-reader.ts +1 -1
- package/src/utils/commit-message-generator.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/workspace-tree.ts +396 -0
|
@@ -18,13 +18,13 @@ const CELL_FILLED_MESSAGES = "⛃";
|
|
|
18
18
|
const CELL_FREE = "⛶";
|
|
19
19
|
const CELL_BUFFER = "⛝";
|
|
20
20
|
|
|
21
|
-
type CategoryId = "systemPrompt" | "systemTools" | "skills" | "messages";
|
|
21
|
+
type CategoryId = "systemPrompt" | "systemContext" | "systemTools" | "skills" | "messages";
|
|
22
22
|
|
|
23
23
|
interface CategoryInfo {
|
|
24
24
|
id: CategoryId;
|
|
25
25
|
label: string;
|
|
26
26
|
tokens: number;
|
|
27
|
-
color: "accent" | "warning" | "success" | "userMessageText";
|
|
27
|
+
color: "accent" | "warning" | "success" | "userMessageText" | "customMessageLabel";
|
|
28
28
|
glyph: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -86,12 +86,19 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
|
|
|
86
86
|
// Tools = JSON tool schema sent separately on the wire
|
|
87
87
|
// Skills = the skill list embedded in the system prompt
|
|
88
88
|
// Messages = conversation messages
|
|
89
|
-
const
|
|
90
|
-
const
|
|
89
|
+
const systemPromptTokens = Math.max(0, countTokens(session.systemPrompt[0] ?? "") - skillsTokens);
|
|
90
|
+
const systemContextTokens = countTokens(session.systemPrompt.slice(1));
|
|
91
91
|
|
|
92
92
|
const categories: CategoryInfo[] = [
|
|
93
93
|
{ id: "systemPrompt", label: "System prompt", tokens: systemPromptTokens, color: "accent", glyph: CELL_FILLED },
|
|
94
94
|
{ id: "systemTools", label: "System tools", tokens: toolsTokens, color: "warning", glyph: CELL_FILLED },
|
|
95
|
+
{
|
|
96
|
+
id: "systemContext",
|
|
97
|
+
label: "System context",
|
|
98
|
+
tokens: systemContextTokens,
|
|
99
|
+
color: "customMessageLabel",
|
|
100
|
+
glyph: CELL_FILLED,
|
|
101
|
+
},
|
|
95
102
|
{ id: "skills", label: "Skills", tokens: skillsTokens, color: "success", glyph: CELL_FILLED },
|
|
96
103
|
{
|
|
97
104
|
id: "messages",
|
|
@@ -134,7 +141,7 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
|
|
|
134
141
|
|
|
135
142
|
interface CellSpec {
|
|
136
143
|
glyph: string;
|
|
137
|
-
color: "accent" | "warning" | "success" | "userMessageText" | "muted" | "dim";
|
|
144
|
+
color: "accent" | "warning" | "success" | "userMessageText" | "customMessageLabel" | "muted" | "dim";
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
function planCells(breakdown: ContextBreakdown): CellSpec[] {
|
|
@@ -336,6 +336,7 @@ export class UiHelpers {
|
|
|
336
336
|
showImages: settings.get("terminal.showImages"),
|
|
337
337
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
338
338
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
339
|
+
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
339
340
|
},
|
|
340
341
|
tool,
|
|
341
342
|
this.ctx.ui,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<workstation>
|
|
2
|
+
{{#list environment prefix="- " join="\n"}}{{label}}: {{value}}{{/list}}
|
|
3
|
+
</workstation>
|
|
4
|
+
|
|
5
|
+
{{#if contextFiles.length}}
|
|
6
|
+
<context>
|
|
7
|
+
Follow the context files below for all tasks:
|
|
8
|
+
{{#each contextFiles}}
|
|
9
|
+
<file path="{{path}}">
|
|
10
|
+
{{content}}
|
|
11
|
+
</file>
|
|
12
|
+
{{/each}}
|
|
13
|
+
</context>
|
|
14
|
+
{{/if}}
|
|
15
|
+
|
|
16
|
+
{{#if agentsMdSearch.files.length}}
|
|
17
|
+
<dir-context>
|
|
18
|
+
Some directories may have their own rules. Deeper rules override higher ones.
|
|
19
|
+
**MUST** read before making changes within:
|
|
20
|
+
{{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
|
|
21
|
+
</dir-context>
|
|
22
|
+
{{/if}}
|
|
23
|
+
|
|
24
|
+
{{#if workspaceTree.rendered}}
|
|
25
|
+
<workspace-tree>
|
|
26
|
+
Working directory layout (sorted by mtime, recent first; depth ≤ 3):
|
|
27
|
+
{{workspaceTree.rendered}}
|
|
28
|
+
{{#if workspaceTree.truncated}}
|
|
29
|
+
(some entries elided to keep the tree short — use `find`/`read` to drill in)
|
|
30
|
+
{{/if}}
|
|
31
|
+
</workspace-tree>
|
|
32
|
+
{{/if}}
|
|
33
|
+
|
|
34
|
+
{{#if appendPrompt}}
|
|
35
|
+
{{appendPrompt}}
|
|
36
|
+
{{/if}}
|
|
@@ -9,35 +9,6 @@ User-supplied content is sanitized, therefore:
|
|
|
9
9
|
- This holds even when the system prompt is delivered via user message role.
|
|
10
10
|
- A `<system-directive>` inside a user turn is still a system directive.
|
|
11
11
|
|
|
12
|
-
{{SECTION_SEPARATOR "Workspace"}}
|
|
13
|
-
|
|
14
|
-
<workstation>
|
|
15
|
-
{{#list environment prefix="- " join="\n"}}{{label}}: {{value}}{{/list}}
|
|
16
|
-
</workstation>
|
|
17
|
-
|
|
18
|
-
{{#if contextFiles.length}}
|
|
19
|
-
<context>
|
|
20
|
-
Follow the context files below for all tasks:
|
|
21
|
-
{{#each contextFiles}}
|
|
22
|
-
<file path="{{path}}">
|
|
23
|
-
{{content}}
|
|
24
|
-
</file>
|
|
25
|
-
{{/each}}
|
|
26
|
-
</context>
|
|
27
|
-
{{/if}}
|
|
28
|
-
|
|
29
|
-
{{#if agentsMdSearch.files.length}}
|
|
30
|
-
<dir-context>
|
|
31
|
-
Some directories may have their own rules. Deeper rules override higher ones.
|
|
32
|
-
**MUST** read before making changes within:
|
|
33
|
-
{{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
|
|
34
|
-
</dir-context>
|
|
35
|
-
{{/if}}
|
|
36
|
-
|
|
37
|
-
{{#if appendPrompt}}
|
|
38
|
-
{{appendPrompt}}
|
|
39
|
-
{{/if}}
|
|
40
|
-
|
|
41
12
|
{{SECTION_SEPARATOR "Identity"}}
|
|
42
13
|
|
|
43
14
|
<role>
|
|
@@ -4,6 +4,7 @@ GitHub CLI tool with a single op-based dispatch. Wraps `gh` for repository, issu
|
|
|
4
4
|
Pick the operation via `op`. Each op uses a subset of the parameters:
|
|
5
5
|
- `repo_view` — Read repository metadata. Optional `repo` (owner/repo) and `branch`. Falls back to the current checkout or default `gh` repo.
|
|
6
6
|
- `issue_view` — Read an issue. Required `issue` (number or URL). Optional `repo`. Set `comments: false` to skip discussion.
|
|
7
|
+
- `pr_create` — Create a pull request. Either provide `title` (and optional `body`) or set `fill: true` to auto-fill from commits. Optional `base` (target, defaults to repo default), `head` (source, defaults to current branch), `draft`, `repo`, `reviewer[]`, `assignee[]`, `label[]`. Returns the new PR URL plus a summary.
|
|
7
8
|
- `pr_view` — Read one or more pull requests, including reviews and inline review comments. Optional `pr` (number, URL, branch, or array of any — pass an array to fetch multiple PRs in one call); omitting it targets the current branch's PR. Optional `repo`. Set `comments: false` for a lighter summary.
|
|
8
9
|
- `pr_diff` — Read one or more pull request diffs. Optional `pr` (single identifier or array for batch). Optional `repo`. Set `nameOnly: true` for changed file names. Use `exclude` to drop generated paths from the diff.
|
|
9
10
|
- `pr_checkout` — Check one or more pull requests out into dedicated git worktrees. Optional `pr` (number, URL, branch, or array of any of those — pass an array to batch-check-out multiple PRs in one call), `repo`, `force` (reset existing local branch).
|
|
@@ -8,8 +8,8 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
8
8
|
|
|
9
9
|
<ops>
|
|
10
10
|
@PATH header: subsequent ops apply to PATH
|
|
11
|
-
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
11
|
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
|
+
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
13
13
|
- A..B delete the line range (inclusive); `- A` for one line
|
|
14
14
|
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows
|
|
15
15
|
</ops>
|
|
@@ -20,6 +20,7 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
20
20
|
- `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
|
|
21
21
|
- `= A..B` replaces the inclusive range with the following payload lines. `= A` (or `= A..B`) with no payload blanks the range to a single empty line.
|
|
22
22
|
- `- A..B` deletes the inclusive range; omit `..B` for one line.
|
|
23
|
+
- Pick the smallest op for the change: pure addition → `+`/`<`; pure deletion → `-`; `= A..B` ONLY when content inside `A..B` is actually being modified or removed.
|
|
23
24
|
</rules>
|
|
24
25
|
|
|
25
26
|
<case file="a.ts">
|
|
@@ -39,11 +40,9 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
39
40
|
|
|
40
41
|
# Replace a contiguous range with multiple lines
|
|
41
42
|
@a.ts
|
|
42
|
-
= {{hrefr
|
|
43
|
-
{{hsep}}export function label(name: string): string {
|
|
43
|
+
= {{hrefr 4}}..{{hrefr 5}}
|
|
44
44
|
{{hsep}} const clean = (name || DEF).trim();
|
|
45
45
|
{{hsep}} return clean.length === 0 ? DEF : clean.toUpperCase();
|
|
46
|
-
{{hsep}}}
|
|
47
46
|
|
|
48
47
|
# Insert BEFORE a line
|
|
49
48
|
@a.ts
|
|
@@ -73,11 +72,30 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
73
72
|
= {{hrefr 2}}
|
|
74
73
|
</examples>
|
|
75
74
|
|
|
75
|
+
<anti-pattern>
|
|
76
|
+
# WRONG — replaces 6 lines just to add one. Use `+` at the boundary instead.
|
|
77
|
+
@a.ts
|
|
78
|
+
= {{hrefr 1}}..{{hrefr 6}}
|
|
79
|
+
{{hsep}}const DEF = "guest";
|
|
80
|
+
{{hsep}}const DEBUG = false;
|
|
81
|
+
{{hsep}}
|
|
82
|
+
{{hsep}}export function label(name) {
|
|
83
|
+
{{hsep}} const clean = name || DEF;
|
|
84
|
+
{{hsep}} return clean.trim();
|
|
85
|
+
{{hsep}}}
|
|
86
|
+
|
|
87
|
+
# RIGHT — same effect, one-line insert
|
|
88
|
+
@a.ts
|
|
89
|
+
+ {{hrefr 1}}
|
|
90
|
+
{{hsep}}const DEBUG = false;
|
|
91
|
+
|
|
92
|
+
If your replacement payload would render with even one unchanged line in the diff, you have the wrong op or the wrong range. Stop and rewrite as `+`/`<`/`-` plus a narrower `=`.
|
|
93
|
+
</anti-pattern>
|
|
94
|
+
|
|
76
95
|
<critical>
|
|
77
96
|
- Always copy anchors exactly from tool output, but **NEVER** include line content after the `{{hsep}}` separator in the op line.
|
|
78
|
-
- Only emit changed lines. Do not restate unchanged context as payload.
|
|
79
97
|
- Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
|
|
80
98
|
- Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
|
|
81
|
-
- To replace a block, use one `= A..B` op followed by all replacement `{{hsep}}TEXT` payload lines.
|
|
82
99
|
- `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
|
|
100
|
+
- Multiple ops in one patch are cheap. Prefer two narrow ops over one wide `=`.
|
|
83
101
|
</critical>
|
|
@@ -6,29 +6,30 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
|
|
|
6
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
7
|
|
|
8
8
|
## Parameters
|
|
9
|
-
- `path` — file path or URL (required)
|
|
10
|
-
- `sel` — optional selector for line ranges or raw mode
|
|
9
|
+
- `path` — file path or URL (required). Append `:<sel>` for line ranges or raw mode (for example `src/foo.ts:50-200` or `src/foo.ts:raw`).
|
|
11
10
|
- `timeout` — seconds, for URLs only
|
|
12
11
|
|
|
13
12
|
## Selectors
|
|
14
13
|
|
|
15
|
-
|`
|
|
14
|
+
|`path` suffix|Behavior|
|
|
16
15
|
|---|---|
|
|
17
|
-
|_(omitted)_|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
|_(omitted)_|For parseable code files, return a structural summary. Otherwise read from the start (up to {{DEFAULT_LIMIT}} lines).|
|
|
17
|
+
|`:50`|Read from line 50 onward|
|
|
18
|
+
|`:50-200`|Read lines 50-200|
|
|
19
|
+
|`:50+150`|Read 150 lines starting at line 50|
|
|
20
|
+
|`:20+1`|Read exactly one line|
|
|
21
|
+
|`:raw`|Read verbatim text without anchors or summarization|
|
|
22
22
|
|
|
23
23
|
# Filesystem
|
|
24
24
|
- Reading a directory path returns a list of dirents.
|
|
25
25
|
{{#if IS_HL_MODE}}
|
|
26
|
-
- Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():`
|
|
26
|
+
- Reading a file with an explicit selector returns lines prefixed with anchors (line+hash): `41th|def alpha():`
|
|
27
27
|
{{else}}
|
|
28
28
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
29
|
-
- Reading a file returns lines prefixed with line numbers: `41|def alpha():`
|
|
29
|
+
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`
|
|
30
30
|
{{/if}}
|
|
31
31
|
{{/if}}
|
|
32
|
+
- Reading a parseable code file without a selector returns a structural summary with signatures/declarations kept and large bodies collapsed to `…`. Use `:raw` or an explicit range such as `:1-9999` for verbatim content.
|
|
32
33
|
|
|
33
34
|
# Inspection
|
|
34
35
|
|
|
@@ -36,7 +37,7 @@ Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook
|
|
|
36
37
|
|
|
37
38
|
# Directories & Archives
|
|
38
39
|
|
|
39
|
-
Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents.
|
|
40
|
+
Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents, and append a selector to the archive entry such as `archive.zip:dir/file.ts:50-60`.
|
|
40
41
|
|
|
41
42
|
# SQLite Databases
|
|
42
43
|
|
|
@@ -50,13 +51,13 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
50
51
|
|
|
51
52
|
# URLs
|
|
52
53
|
|
|
53
|
-
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
|
|
54
|
+
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 a `:raw` suffix for untouched HTML; `timeout` to override the default request timeout. URL line selectors require the `L` form, for example `https://example.com/page:L50-L60`.
|
|
54
55
|
</instruction>
|
|
55
56
|
|
|
56
57
|
<critical>
|
|
57
58
|
- You **MUST** use `read` for every file, directory, archive, and URL read. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, and `wget` are **FORBIDDEN** for inspection — any such Bash call is a bug, regardless of how short or convenient it looks.
|
|
58
59
|
- You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if `read` fails to deliver reasonable content.
|
|
59
60
|
- You **MUST** always include the `path` parameter — never call `read` with an empty argument object `{}`.
|
|
60
|
-
- For specific line ranges,
|
|
61
|
-
- You **MAY** use
|
|
61
|
+
- For specific line ranges, append the selector to `path` (e.g. `path="src/foo.ts:50-200"`, `path="src/foo.ts:50+150"`) — do **NOT** reach for `sed -n`, `awk NR`, or `head`/`tail` pipelines.
|
|
62
|
+
- You **MAY** use path suffix selectors with URL reads; the tool paginates cached fetched output.
|
|
62
63
|
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -102,6 +102,7 @@ import { closeAllConnections } from "./ssh/connection-manager";
|
|
|
102
102
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
103
103
|
import {
|
|
104
104
|
type AgentsMdSearch,
|
|
105
|
+
type BuildSystemPromptResult,
|
|
105
106
|
buildAgentsMdSearch,
|
|
106
107
|
buildSystemPrompt as buildSystemPromptInternal,
|
|
107
108
|
buildSystemPromptToolMetadata,
|
|
@@ -140,6 +141,7 @@ import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
|
140
141
|
import { queueResolveHandler } from "./tools/resolve";
|
|
141
142
|
import { EventBus } from "./utils/event-bus";
|
|
142
143
|
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
144
|
+
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
143
145
|
|
|
144
146
|
// Types
|
|
145
147
|
export interface CreateAgentSessionOptions {
|
|
@@ -165,8 +167,8 @@ export interface CreateAgentSessionOptions {
|
|
|
165
167
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
|
166
168
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
167
169
|
|
|
168
|
-
/** System prompt.
|
|
169
|
-
systemPrompt?: string | ((defaultPrompt: string) => string);
|
|
170
|
+
/** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */
|
|
171
|
+
systemPrompt?: string[] | ((defaultPrompt: string[]) => string[]);
|
|
170
172
|
/** Optional provider-facing session identifier for prompt caches and sticky auth selection.
|
|
171
173
|
* Keeps persisted session files isolated while reusing provider-side caches. */
|
|
172
174
|
providerSessionId?: string;
|
|
@@ -270,6 +272,7 @@ export type { Skill } from "./extensibility/skills";
|
|
|
270
272
|
export type { FileSlashCommand } from "./extensibility/slash-commands";
|
|
271
273
|
export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp";
|
|
272
274
|
export type { Tool } from "./tools";
|
|
275
|
+
export { buildDirectoryTree, buildWorkspaceTree, type DirectoryTree, type WorkspaceTree } from "./workspace-tree";
|
|
273
276
|
|
|
274
277
|
export {
|
|
275
278
|
// Individual tool classes (for custom usage)
|
|
@@ -399,9 +402,12 @@ export interface BuildSystemPromptOptions {
|
|
|
399
402
|
}
|
|
400
403
|
|
|
401
404
|
/**
|
|
402
|
-
* Build the default system prompt.
|
|
405
|
+
* Build the default provider-facing system prompt blocks.
|
|
406
|
+
*
|
|
407
|
+
* The returned `systemPrompt` preserves the stable harness prompt and dynamic project context
|
|
408
|
+
* as separate entries so providers can cache prompt prefixes without concatenating blocks.
|
|
403
409
|
*/
|
|
404
|
-
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<
|
|
410
|
+
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
|
|
405
411
|
return await buildSystemPromptInternal({
|
|
406
412
|
cwd: options.cwd,
|
|
407
413
|
skills: options.skills,
|
|
@@ -652,7 +658,7 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] {
|
|
|
652
658
|
* const { session } = await createAgentSession({
|
|
653
659
|
* model: myModel,
|
|
654
660
|
* getApiKey: async () => Bun.env.MY_KEY,
|
|
655
|
-
* systemPrompt: 'You are helpful.',
|
|
661
|
+
* systemPrompt: ['You are helpful.'],
|
|
656
662
|
* tools: codingTools({ cwd: getProjectDir() }),
|
|
657
663
|
* skills: [],
|
|
658
664
|
* sessionManager: SessionManager.inMemory(),
|
|
@@ -680,6 +686,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
680
686
|
// (~200ms on large repos) and only needs `cwd`, so it can overlap with everything that follows.
|
|
681
687
|
const agentsMdSearchPromise: Promise<AgentsMdSearch> = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, cwd);
|
|
682
688
|
agentsMdSearchPromise.catch(() => {});
|
|
689
|
+
const workspaceTreePromise: Promise<WorkspaceTree> = logger.time("buildWorkspaceTree", buildWorkspaceTree, cwd);
|
|
690
|
+
workspaceTreePromise.catch(() => {});
|
|
683
691
|
|
|
684
692
|
// Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
|
|
685
693
|
// at their respective consumer sites. Their work can overlap with model resolution, secret loading,
|
|
@@ -1330,7 +1338,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1330
1338
|
const repeatToolDescriptions = settings.get("repeatToolDescriptions");
|
|
1331
1339
|
const eagerTasks = settings.get("task.eager");
|
|
1332
1340
|
const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
|
|
1333
|
-
const rebuildSystemPrompt = async (
|
|
1341
|
+
const rebuildSystemPrompt = async (
|
|
1342
|
+
toolNames: string[],
|
|
1343
|
+
tools: Map<string, AgentTool>,
|
|
1344
|
+
): Promise<BuildSystemPromptResult> => {
|
|
1334
1345
|
toolContextStore.setToolNames(toolNames);
|
|
1335
1346
|
const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
|
|
1336
1347
|
const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
|
|
@@ -1380,33 +1391,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1380
1391
|
eagerTasks,
|
|
1381
1392
|
secretsEnabled,
|
|
1382
1393
|
agentsMdSearch: agentsMdSearchPromise,
|
|
1394
|
+
workspaceTree: workspaceTreePromise,
|
|
1383
1395
|
});
|
|
1384
1396
|
|
|
1385
1397
|
if (options.systemPrompt === undefined) {
|
|
1386
1398
|
return defaultPrompt;
|
|
1387
1399
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
return
|
|
1390
|
-
cwd,
|
|
1391
|
-
skills,
|
|
1392
|
-
contextFiles,
|
|
1393
|
-
tools: promptTools,
|
|
1394
|
-
toolNames,
|
|
1395
|
-
rules: rulebookRules,
|
|
1396
|
-
alwaysApplyRules,
|
|
1397
|
-
skillsSettings: settings.getGroup("skills"),
|
|
1398
|
-
customPrompt: options.systemPrompt,
|
|
1399
|
-
appendSystemPrompt: appendPrompt,
|
|
1400
|
-
repeatToolDescriptions,
|
|
1401
|
-
intentField,
|
|
1402
|
-
mcpDiscoveryMode: hasDiscoverableMCPTools,
|
|
1403
|
-
mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
|
|
1404
|
-
eagerTasks,
|
|
1405
|
-
secretsEnabled,
|
|
1406
|
-
agentsMdSearch: agentsMdSearchPromise,
|
|
1407
|
-
});
|
|
1400
|
+
if (Array.isArray(options.systemPrompt)) {
|
|
1401
|
+
return { systemPrompt: options.systemPrompt };
|
|
1408
1402
|
}
|
|
1409
|
-
return
|
|
1403
|
+
return {
|
|
1404
|
+
systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
|
|
1405
|
+
};
|
|
1410
1406
|
};
|
|
1411
1407
|
|
|
1412
1408
|
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
|
|
@@ -1472,7 +1468,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1472
1468
|
}
|
|
1473
1469
|
}
|
|
1474
1470
|
|
|
1475
|
-
const systemPrompt = await logger.time(
|
|
1471
|
+
const { systemPrompt } = await logger.time(
|
|
1472
|
+
"buildSystemPrompt",
|
|
1473
|
+
rebuildSystemPrompt,
|
|
1474
|
+
initialToolNames,
|
|
1475
|
+
toolRegistry,
|
|
1476
|
+
);
|
|
1476
1477
|
|
|
1477
1478
|
const promptTemplates = await promptTemplatesPromise;
|
|
1478
1479
|
toolSession.promptTemplates = promptTemplates;
|
|
@@ -245,8 +245,8 @@ export interface AgentSessionConfig {
|
|
|
245
245
|
onResponse?: SimpleStreamOptions["onResponse"];
|
|
246
246
|
/** Current session message-to-LLM conversion pipeline */
|
|
247
247
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
248
|
-
/** System prompt builder that can consider tool availability */
|
|
249
|
-
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
|
|
248
|
+
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
249
|
+
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
250
250
|
/**
|
|
251
251
|
* Optional accessor for live MCP server instructions. Read by the session's
|
|
252
252
|
* `rebuildSystemPrompt`-skip optimization to detect server-side instruction
|
|
@@ -520,9 +520,11 @@ export class AgentSession {
|
|
|
520
520
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
521
521
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
522
522
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
523
|
-
#rebuildSystemPrompt:
|
|
523
|
+
#rebuildSystemPrompt:
|
|
524
|
+
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
525
|
+
| undefined;
|
|
524
526
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
525
|
-
#baseSystemPrompt: string;
|
|
527
|
+
#baseSystemPrompt: string[];
|
|
526
528
|
/**
|
|
527
529
|
* Signature of the (toolNames, tool descriptions) tuple passed to the most
|
|
528
530
|
* recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
|
|
@@ -2083,8 +2085,8 @@ export class AgentSession {
|
|
|
2083
2085
|
getLastAssistantMessage(): AssistantMessage | undefined {
|
|
2084
2086
|
return this.#findLastAssistantMessage();
|
|
2085
2087
|
}
|
|
2086
|
-
/** Current effective system prompt (includes any per-turn extension modifications) */
|
|
2087
|
-
get systemPrompt(): string {
|
|
2088
|
+
/** Current effective system prompt blocks (includes any per-turn extension modifications) */
|
|
2089
|
+
get systemPrompt(): string[] {
|
|
2088
2090
|
return this.agent.state.systemPrompt;
|
|
2089
2091
|
}
|
|
2090
2092
|
|
|
@@ -2281,7 +2283,8 @@ export class AgentSession {
|
|
|
2281
2283
|
if (this.#rebuildSystemPrompt) {
|
|
2282
2284
|
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
2283
2285
|
if (signature !== this.#lastAppliedToolSignature) {
|
|
2284
|
-
|
|
2286
|
+
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
2287
|
+
this.#baseSystemPrompt = built.systemPrompt;
|
|
2285
2288
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
2286
2289
|
this.#lastAppliedToolSignature = signature;
|
|
2287
2290
|
}
|
|
@@ -2324,7 +2327,8 @@ export class AgentSession {
|
|
|
2324
2327
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
2325
2328
|
if (!this.#rebuildSystemPrompt) return;
|
|
2326
2329
|
const activeToolNames = this.getActiveToolNames();
|
|
2327
|
-
|
|
2330
|
+
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
2331
|
+
this.#baseSystemPrompt = built.systemPrompt;
|
|
2328
2332
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
2329
2333
|
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
2330
2334
|
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
@@ -2335,14 +2339,14 @@ export class AgentSession {
|
|
|
2335
2339
|
this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
|
|
2336
2340
|
}
|
|
2337
2341
|
|
|
2338
|
-
async #buildSystemPromptForAgentStart(promptText: string): Promise<string> {
|
|
2342
|
+
async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
|
|
2339
2343
|
const backend = resolveMemoryBackend(this.settings);
|
|
2340
2344
|
if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
|
|
2341
2345
|
|
|
2342
2346
|
try {
|
|
2343
2347
|
const injected = await backend.beforeAgentStartPrompt(this, promptText);
|
|
2344
2348
|
if (!injected) return this.#baseSystemPrompt;
|
|
2345
|
-
return
|
|
2349
|
+
return [...this.#baseSystemPrompt, injected];
|
|
2346
2350
|
} catch (err) {
|
|
2347
2351
|
logger.debug("Memory backend beforeAgentStartPrompt failed", {
|
|
2348
2352
|
backend: backend.id,
|
|
@@ -4215,7 +4219,11 @@ export class AgentSession {
|
|
|
4215
4219
|
apiKey,
|
|
4216
4220
|
customInstructions,
|
|
4217
4221
|
compactionAbortController.signal,
|
|
4218
|
-
{
|
|
4222
|
+
{
|
|
4223
|
+
promptOverride: hookPrompt,
|
|
4224
|
+
extraContext: hookContext,
|
|
4225
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
4226
|
+
},
|
|
4219
4227
|
);
|
|
4220
4228
|
summary = result.summary;
|
|
4221
4229
|
shortSummary = result.shortSummary;
|
|
@@ -5328,7 +5336,7 @@ export class AgentSession {
|
|
|
5328
5336
|
compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
|
|
5329
5337
|
promptOverride: hookPrompt,
|
|
5330
5338
|
extraContext: hookContext,
|
|
5331
|
-
remoteInstructions: this.#baseSystemPrompt,
|
|
5339
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
5332
5340
|
initiatorOverride: "agent",
|
|
5333
5341
|
});
|
|
5334
5342
|
break;
|
|
@@ -290,7 +290,7 @@ export async function generateBranchSummary(
|
|
|
290
290
|
// Call LLM for summarization
|
|
291
291
|
const response = await completeSimple(
|
|
292
292
|
model,
|
|
293
|
-
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
293
|
+
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
294
294
|
{ apiKey, signal, maxTokens: 2048 },
|
|
295
295
|
);
|
|
296
296
|
|
|
@@ -1019,7 +1019,7 @@ export async function generateSummary(
|
|
|
1019
1019
|
|
|
1020
1020
|
const response = await completeSimple(
|
|
1021
1021
|
model,
|
|
1022
|
-
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
1022
|
+
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
1023
1023
|
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
|
|
1024
1024
|
);
|
|
1025
1025
|
|
|
@@ -1066,7 +1066,7 @@ async function generateShortSummary(
|
|
|
1066
1066
|
const response = await completeSimple(
|
|
1067
1067
|
model,
|
|
1068
1068
|
{
|
|
1069
|
-
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
1069
|
+
systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT],
|
|
1070
1070
|
messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
|
|
1071
1071
|
},
|
|
1072
1072
|
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
|
|
@@ -1386,7 +1386,7 @@ async function generateTurnPrefixSummary(
|
|
|
1386
1386
|
|
|
1387
1387
|
const response = await completeSimple(
|
|
1388
1388
|
model,
|
|
1389
|
-
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
1389
|
+
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
1390
1390
|
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride },
|
|
1391
1391
|
);
|
|
1392
1392
|
|
|
@@ -25,7 +25,7 @@ export interface SessionDumpToolInfo {
|
|
|
25
25
|
|
|
26
26
|
export interface FormatSessionDumpTextOptions {
|
|
27
27
|
messages: readonly AgentMessage[];
|
|
28
|
-
systemPrompt?: string | null;
|
|
28
|
+
systemPrompt?: readonly string[] | null;
|
|
29
29
|
model?: Model | null;
|
|
30
30
|
thinkingLevel?: ThinkingLevel | string | null;
|
|
31
31
|
tools?: readonly SessionDumpToolInfo[];
|
|
@@ -64,11 +64,16 @@ function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
|
|
|
64
64
|
export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string {
|
|
65
65
|
const lines: string[] = [];
|
|
66
66
|
|
|
67
|
-
const systemPrompt = options.systemPrompt;
|
|
68
|
-
if (systemPrompt) {
|
|
67
|
+
const systemPrompt = options.systemPrompt?.filter(prompt => prompt.length > 0) ?? [];
|
|
68
|
+
if (systemPrompt.length > 0) {
|
|
69
69
|
lines.push("## System Prompt\n");
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
for (let index = 0; index < systemPrompt.length; index++) {
|
|
71
|
+
if (systemPrompt.length > 1) {
|
|
72
|
+
lines.push(`### System Prompt ${index + 1}\n`);
|
|
73
|
+
}
|
|
74
|
+
lines.push(systemPrompt[index]);
|
|
75
|
+
lines.push("\n");
|
|
76
|
+
}
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
const model = options.model;
|
|
@@ -2182,6 +2182,63 @@ export class SessionManager {
|
|
|
2182
2182
|
return manager.getPath(id);
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
2185
|
+
/**
|
|
2186
|
+
* Path to the unsent-input draft sidecar for the current session. Lives inside
|
|
2187
|
+
* the artifacts directory so it is removed together with the session on
|
|
2188
|
+
* `dropSession`. Returns null when the session has no on-disk identity.
|
|
2189
|
+
*/
|
|
2190
|
+
#getDraftPath(): string | null {
|
|
2191
|
+
const dir = this.getArtifactsDir();
|
|
2192
|
+
return dir ? path.join(dir, "draft.txt") : null;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Persist (or clear) the current editor draft so the next resume of this
|
|
2197
|
+
* session can restore it. Empty text deletes any stale draft. No-op when the
|
|
2198
|
+
* session is not persisted.
|
|
2199
|
+
*/
|
|
2200
|
+
async saveDraft(text: string): Promise<void> {
|
|
2201
|
+
const draftPath = this.#getDraftPath();
|
|
2202
|
+
if (!draftPath || !this.persist) return;
|
|
2203
|
+
if (text.length === 0) {
|
|
2204
|
+
try {
|
|
2205
|
+
await this.storage.unlink(draftPath);
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
if (!isEnoent(err)) throw err;
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
// Force the session header onto disk so resume can find the file we are
|
|
2212
|
+
// attaching this draft to. Without this, a session whose first message
|
|
2213
|
+
// never produced an assistant reply would persist a draft next to a
|
|
2214
|
+
// session file that does not exist on disk.
|
|
2215
|
+
await this.ensureOnDisk();
|
|
2216
|
+
await this.storage.writeText(draftPath, text);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
/**
|
|
2220
|
+
* Read and remove the saved draft. Returns the previously-saved text, or
|
|
2221
|
+
* null when no draft is pending. Single-shot: a successful read removes the
|
|
2222
|
+
* sidecar so a subsequent resume does not re-restore the same text.
|
|
2223
|
+
*/
|
|
2224
|
+
async consumeDraft(): Promise<string | null> {
|
|
2225
|
+
const draftPath = this.#getDraftPath();
|
|
2226
|
+
if (!draftPath) return null;
|
|
2227
|
+
let text: string;
|
|
2228
|
+
try {
|
|
2229
|
+
text = await this.storage.readText(draftPath);
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
if (isEnoent(err)) return null;
|
|
2232
|
+
throw err;
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
await this.storage.unlink(draftPath);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
if (!isEnoent(err)) throw err;
|
|
2238
|
+
}
|
|
2239
|
+
return text;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2185
2242
|
/** The source that set the session name: "user" (manual /rename or RPC) or "auto" (generated title). */
|
|
2186
2243
|
get titleSource(): "auto" | "user" | undefined {
|
|
2187
2244
|
return this.#titleSource;
|
|
@@ -759,7 +759,7 @@ export function formatHeadTruncationNotice(
|
|
|
759
759
|
const totalFileLines = options.totalFileLines ?? truncation.totalLines;
|
|
760
760
|
const endLineDisplay = startLineDisplay + (truncation.outputLines ?? truncation.totalLines) - 1;
|
|
761
761
|
const nextOffset = endLineDisplay + 1;
|
|
762
|
-
const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use
|
|
762
|
+
const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use :${nextOffset} to continue]`;
|
|
763
763
|
return `\n\n${notice}`;
|
|
764
764
|
}
|
|
765
765
|
|