@selesai/code 0.13.26 → 0.13.27
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 +74 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/session-picker.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/core/agent-session-auto-handoff.test.js +2 -1
- package/dist/core/agent-session-length-continuation.test.js +2 -1
- package/dist/core/agent-session.d.ts +58 -7
- package/dist/core/agent-session.js +334 -167
- package/dist/core/bug-report-upload.d.ts +11 -0
- package/dist/core/bug-report-upload.js +21 -0
- package/dist/core/bug-report.d.ts +175 -0
- package/dist/core/bug-report.js +291 -0
- package/dist/core/cache-stats.js +12 -1
- package/dist/core/cache-warmer.d.ts +101 -0
- package/dist/core/cache-warmer.js +341 -0
- package/dist/core/compaction/branch-summarization.js +2 -2
- package/dist/core/compaction/compaction.d.ts +6 -5
- package/dist/core/compaction/compaction.js +49 -42
- package/dist/core/crash-log.d.ts +21 -0
- package/dist/core/crash-log.js +70 -0
- package/dist/core/experimental.d.ts +0 -4
- package/dist/core/experimental.js +0 -4
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/jiti-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-loader.js +3 -0
- package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-static-loader.js +3 -0
- package/dist/core/extensions/loader.js +39 -49
- package/dist/core/extensions/runner.d.ts +10 -7
- package/dist/core/extensions/runner.js +85 -67
- package/dist/core/extensions/types.d.ts +66 -52
- package/dist/core/extensions/virtual-modules.d.ts +2 -0
- package/dist/core/extensions/virtual-modules.js +39 -0
- package/dist/core/extensions/wrapper.js +1 -20
- package/dist/core/index.d.ts +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +1 -1
- package/dist/core/messages.js +1 -0
- package/dist/core/model-config.d.ts +101 -20
- package/dist/core/model-config.js +25 -18
- package/dist/core/model-resolver.js +2 -1
- package/dist/core/model-runtime.js +5 -3
- package/dist/core/provider-composer.d.ts +7 -2
- package/dist/core/provider-composer.js +10 -5
- package/dist/core/radius.d.ts +3 -0
- package/dist/core/radius.js +6 -0
- package/dist/core/sdk.js +60 -38
- package/dist/core/session-export.d.ts +6 -2
- package/dist/core/session-export.js +14 -13
- package/dist/core/session-format.test.d.ts +1 -0
- package/dist/core/session-format.test.js +96 -0
- package/dist/core/session-manager.d.ts +29 -6
- package/dist/core/session-manager.js +159 -96
- package/dist/core/settings-manager.d.ts +22 -5
- package/dist/core/settings-manager.js +56 -23
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/system-prompt.d.ts +49 -4
- package/dist/core/system-prompt.js +155 -102
- package/dist/core/system-prompt.test.js +61 -23
- package/dist/core/tools/bash.d.ts +2 -1
- package/dist/core/tools/bash.js +10 -4
- package/dist/core/tools/edit.js +1 -2
- package/dist/core/tools/read.js +1 -2
- package/dist/core/tools/renderers/bash.js +9 -1
- package/dist/core/tools/write.js +1 -2
- package/dist/core/usage-totals.d.ts +1 -1
- package/dist/core/usage-totals.js +5 -1
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
- package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
- package/dist/extensions/pi-web-agent/src/types.ts +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/main.js +13 -9
- package/dist/migrations.js +2 -2
- package/dist/modes/interactive/bug-report.d.ts +15 -0
- package/dist/modes/interactive/bug-report.js +205 -0
- package/dist/modes/interactive/chat-viewport.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +12 -5
- package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
- package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
- package/dist/modes/interactive/components/extension-editor.js +7 -2
- package/dist/modes/interactive/components/extension-input.d.ts +2 -0
- package/dist/modes/interactive/components/extension-input.js +6 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
- package/dist/modes/interactive/components/extension-selector.js +4 -0
- package/dist/modes/interactive/components/footer.js +4 -1
- package/dist/modes/interactive/components/session-selector.d.ts +5 -5
- package/dist/modes/interactive/components/session-selector.js +74 -48
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
- package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
- package/dist/modes/interactive/components/status-indicator.js +7 -7
- package/dist/modes/interactive/components/tool-execution.js +18 -9
- package/dist/modes/interactive/components/tree-selector.js +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts +15 -2
- package/dist/modes/interactive/interactive-mode.js +257 -81
- package/dist/modes/interactive/session-share.d.ts +2 -0
- package/dist/modes/interactive/session-share.js +8 -4
- package/dist/modes/interactive/tui-renderer.js +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/utils/clipboard-command.d.ts +6 -0
- package/dist/utils/clipboard-command.js +44 -0
- package/dist/utils/clipboard-image.js +55 -89
- package/dist/utils/clipboard.d.ts +1 -1
- package/dist/utils/clipboard.js +118 -108
- package/dist/utils/wsl.d.ts +2 -0
- package/dist/utils/wsl.js +14 -0
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +59 -0
- package/package.json +9 -9
- package/dist/utils/clipboard-native.d.ts +0 -10
- package/dist/utils/clipboard-native.js +0 -19
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System prompt construction and project context loading
|
|
3
3
|
*/
|
|
4
|
+
import { getSystemMessageText } from "@earendil-works/pi-ai";
|
|
4
5
|
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
|
|
5
6
|
import { formatAgentsForPrompt } from "./agents.js";
|
|
6
7
|
import { formatSkillsForPrompt } from "./skills.js";
|
|
@@ -10,120 +11,172 @@ import { formatSkillsForPrompt } from "./skills.js";
|
|
|
10
11
|
* sessions without such a tool are unaffected.
|
|
11
12
|
*/
|
|
12
13
|
const DELEGATION_ROUTING_GUIDELINE = "When a delegation/subagent tool is available: keep tiny targeted reads and simple answers local; send broad local investigation, external research, and mutation/implementation work to a capable delegated agent, inspecting the delegation catalog/list before selecting; the parent remains the decision-maker and normally the sole writer.";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
14
|
+
const SYSTEM_PROMPT_SECTION_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
15
|
+
/** Normalize prompt input into the mutable, collection-complete shape exposed to extensions. */
|
|
16
|
+
export function normalizeBuildSystemPromptOptions(input) {
|
|
17
|
+
return {
|
|
18
|
+
customPrompt: input.customPrompt,
|
|
19
|
+
forceSystemPrompt: input.forceSystemPrompt,
|
|
20
|
+
selectedTools: [...(input.selectedTools ?? ["read", "bash", "edit", "write"])],
|
|
21
|
+
toolSnippets: { ...(input.toolSnippets ?? {}) },
|
|
22
|
+
toolGuidelines: Object.fromEntries(Object.entries(input.toolGuidelines ?? {}).map(([name, guidelines]) => [name, [...guidelines]])),
|
|
23
|
+
promptGuidelines: [...(input.promptGuidelines ?? [])],
|
|
24
|
+
appendSystemPrompt: input.appendSystemPrompt ?? "",
|
|
25
|
+
sections: { ...(input.sections ?? {}) },
|
|
26
|
+
cwd: input.cwd,
|
|
27
|
+
contextFiles: (input.contextFiles ?? []).map((file) => ({ ...file })),
|
|
28
|
+
skills: (input.skills ?? []).map((skill) => ({ ...skill })),
|
|
29
|
+
agents: (input.agents ?? []).map((agent) => ({ ...agent })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function renderProjectContext(contextFiles) {
|
|
33
|
+
return [
|
|
34
|
+
"Project-specific instructions and guidelines:",
|
|
35
|
+
...contextFiles.map(({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`),
|
|
36
|
+
].join("\n\n");
|
|
37
|
+
}
|
|
38
|
+
export function buildRules(selectedTools, toolGuidelines, promptGuidelines) {
|
|
39
|
+
const rules = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const addRule = (rule) => {
|
|
42
|
+
const normalized = rule.trim();
|
|
43
|
+
if (!normalized || seen.has(normalized))
|
|
44
|
+
return;
|
|
45
|
+
seen.add(normalized);
|
|
46
|
+
rules.push(normalized);
|
|
47
|
+
};
|
|
48
|
+
const hasBash = selectedTools.includes("bash");
|
|
49
|
+
const hasPowerShell = selectedTools.includes("powershell");
|
|
50
|
+
const hasGrep = selectedTools.includes("grep");
|
|
51
|
+
const hasFind = selectedTools.includes("find");
|
|
52
|
+
const hasLs = selectedTools.includes("ls");
|
|
53
|
+
if ((hasBash || hasPowerShell) && !hasGrep && !hasFind && !hasLs) {
|
|
54
|
+
if (hasBash && hasPowerShell) {
|
|
55
|
+
addRule("Use bash or PowerShell for file operations like listing, searching, and finding files");
|
|
41
56
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
57
|
+
else if (hasPowerShell) {
|
|
58
|
+
addRule("Use PowerShell for file operations like listing, searching, and finding files");
|
|
44
59
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// custom prompt text stays unchanged; a clearly delimited section is appended
|
|
48
|
-
// only when non-empty guidelines exist.
|
|
49
|
-
const activeGuidelines = (promptGuidelines ?? [])
|
|
50
|
-
.map((guideline) => guideline.trim())
|
|
51
|
-
.filter((guideline) => guideline.length > 0)
|
|
52
|
-
.filter((guideline, index, all) => all.indexOf(guideline) === index);
|
|
53
|
-
if (activeGuidelines.length > 0) {
|
|
54
|
-
prompt += `\n\nActive tool guidelines:\n${activeGuidelines.map((guideline) => `- ${guideline}`).join("\n")}`;
|
|
60
|
+
else {
|
|
61
|
+
addRule("Use bash for file operations like ls, rg, find");
|
|
55
62
|
}
|
|
56
|
-
prompt += `\nCurrent working directory: ${promptCwd}\n`;
|
|
57
|
-
return prompt;
|
|
58
63
|
}
|
|
59
|
-
|
|
64
|
+
for (const name of selectedTools) {
|
|
65
|
+
for (const rule of toolGuidelines[name] ?? [])
|
|
66
|
+
addRule(rule);
|
|
67
|
+
}
|
|
68
|
+
for (const rule of promptGuidelines)
|
|
69
|
+
addRule(rule);
|
|
70
|
+
// Selesai fork: delegation routing is part of the default rule set.
|
|
71
|
+
addRule(DELEGATION_ROUTING_GUIDELINE);
|
|
72
|
+
addRule("Be concise in your responses");
|
|
73
|
+
addRule("Show file paths clearly when working with files");
|
|
74
|
+
return rules.map((rule) => `- ${rule}`).join("\n");
|
|
75
|
+
}
|
|
76
|
+
/** Selesai fork: the fork names itself rather than upstream, and keeps its own docs pointers. */
|
|
77
|
+
function buildForkPreamble() {
|
|
78
|
+
return "You are an expert coding assistant operating inside SelesaiCode fork of Pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
79
|
+
}
|
|
80
|
+
function buildForkDocsSection() {
|
|
60
81
|
const readmePath = getReadmePath();
|
|
61
82
|
const docsPath = getDocsPath();
|
|
62
83
|
const examplesPath = getExamplesPath();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const normalized = guideline.trim();
|
|
87
|
-
if (normalized.length > 0) {
|
|
88
|
-
addGuideline(normalized);
|
|
84
|
+
return `Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI): main docs ${readmePath}, additional docs ${docsPath}, examples ${examplesPath}.
|
|
85
|
+
When working on pi topics, resolve docs/... and examples/... under those paths (not cwd), read the relevant .md files completely, and follow cross-references before implementing.`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Active-tool guidelines for a custom-prompt session: the caller's custom prompt text stays
|
|
89
|
+
* unchanged and a clearly delimited section is appended only when non-empty guidelines exist.
|
|
90
|
+
*/
|
|
91
|
+
function buildActiveToolGuidelines(promptGuidelines) {
|
|
92
|
+
const activeGuidelines = promptGuidelines
|
|
93
|
+
.map((guideline) => guideline.trim())
|
|
94
|
+
.filter((guideline) => guideline.length > 0)
|
|
95
|
+
.filter((guideline, index, all) => all.indexOf(guideline) === index);
|
|
96
|
+
if (activeGuidelines.length === 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
return `Active tool guidelines:\n${activeGuidelines.map((guideline) => `- ${guideline}`).join("\n")}`;
|
|
99
|
+
}
|
|
100
|
+
/** Build the ordered, independently replaceable sections of the structured system prompt. */
|
|
101
|
+
export function buildSystemPromptSections(input) {
|
|
102
|
+
const options = normalizeBuildSystemPromptOptions(input);
|
|
103
|
+
const { customPrompt, selectedTools, toolSnippets, toolGuidelines, promptGuidelines, appendSystemPrompt, sections: customSections, cwd, contextFiles, skills, agents, } = options;
|
|
104
|
+
for (const name of Object.keys(customSections)) {
|
|
105
|
+
if (!SYSTEM_PROMPT_SECTION_NAME.test(name) || name === "preamble") {
|
|
106
|
+
throw new Error(`Invalid system prompt section name: ${name}`);
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Available tools:
|
|
99
|
-
${toolsList}
|
|
100
|
-
|
|
101
|
-
In addition to the tools above, you may have access to other custom tools depending on the project.
|
|
102
|
-
|
|
103
|
-
Guidelines:
|
|
104
|
-
${guidelines}
|
|
105
|
-
|
|
106
|
-
Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI): main docs ${readmePath}, additional docs ${docsPath}, examples ${examplesPath}.
|
|
107
|
-
When working on pi topics, resolve docs/... and examples/... under those paths (not cwd), read the relevant .md files completely, and follow cross-references before implementing.`;
|
|
108
|
-
if (appendSection) {
|
|
109
|
-
prompt += appendSection;
|
|
109
|
+
const promptSections = {};
|
|
110
|
+
if (customPrompt) {
|
|
111
|
+
promptSections.preamble = customPrompt;
|
|
112
|
+
// Selesai fork: custom-prompt sessions still render active-tool guidelines.
|
|
113
|
+
const activeGuidelines = buildActiveToolGuidelines(promptGuidelines);
|
|
114
|
+
if (activeGuidelines)
|
|
115
|
+
promptSections.active_tool_guidelines = activeGuidelines;
|
|
110
116
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
prompt += "</project_context>\n";
|
|
117
|
+
else {
|
|
118
|
+
promptSections.preamble = buildForkPreamble();
|
|
119
|
+
const visibleTools = selectedTools.filter((name) => !!toolSnippets[name]);
|
|
120
|
+
const tools = visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets[name]}`).join("\n") : "(none)";
|
|
121
|
+
promptSections.tools = `${tools}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.`;
|
|
122
|
+
promptSections.rules = buildRules(selectedTools, toolGuidelines, promptGuidelines);
|
|
123
|
+
promptSections.docs = buildForkDocsSection();
|
|
119
124
|
}
|
|
120
|
-
|
|
125
|
+
if (appendSystemPrompt)
|
|
126
|
+
promptSections.addendum = appendSystemPrompt;
|
|
127
|
+
if (contextFiles.length > 0)
|
|
128
|
+
promptSections.project_context = renderProjectContext(contextFiles);
|
|
129
|
+
const skillFileReadTool = ["read", "bash"].find((tool) => selectedTools.includes(tool));
|
|
121
130
|
if (skillFileReadTool && skills.length > 0) {
|
|
122
|
-
|
|
131
|
+
const skillsPrompt = formatSkillsForPrompt(skills, skillFileReadTool).trim();
|
|
132
|
+
if (skillsPrompt)
|
|
133
|
+
promptSections.skills = skillsPrompt;
|
|
134
|
+
}
|
|
135
|
+
// Selesai fork: the available-agents section belongs to the fork's delegation story.
|
|
136
|
+
if (selectedTools.includes("read") && agents.length > 0) {
|
|
137
|
+
const agentsPrompt = formatAgentsForPrompt(agents).trim();
|
|
138
|
+
if (agentsPrompt)
|
|
139
|
+
promptSections.agents = agentsPrompt;
|
|
140
|
+
}
|
|
141
|
+
promptSections.cwd = cwd.replace(/\\/g, "/");
|
|
142
|
+
for (const [name, content] of Object.entries(customSections)) {
|
|
143
|
+
if (content)
|
|
144
|
+
promptSections[name] = content;
|
|
145
|
+
}
|
|
146
|
+
const sections = { preamble: promptSections.preamble };
|
|
147
|
+
for (const [name, content] of Object.entries(promptSections)) {
|
|
148
|
+
if (name !== "preamble")
|
|
149
|
+
sections[name] = `<${name}>\n${content}\n</${name}>`;
|
|
150
|
+
}
|
|
151
|
+
return sections;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The complete prompt state for `input`. A forced prompt is opaque and lives in `content`
|
|
155
|
+
* with no sections; otherwise `content` is empty and the structured sections carry the prompt.
|
|
156
|
+
*/
|
|
157
|
+
export function buildSystemPromptState(input) {
|
|
158
|
+
if (input.forceSystemPrompt !== undefined)
|
|
159
|
+
return { content: input.forceSystemPrompt };
|
|
160
|
+
return { content: "", sections: buildSystemPromptSections(input) };
|
|
161
|
+
}
|
|
162
|
+
/** Build the system prompt text, rendered exactly as the transcript's system message replays it. */
|
|
163
|
+
export function buildSystemPrompt(input) {
|
|
164
|
+
return getSystemMessageText({ role: "system", ...buildSystemPromptState(input), timestamp: 0 });
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Diff the sections the model currently has (replayed from the transcript, so never null)
|
|
168
|
+
* against the desired ones. Returns a `SystemMessage.sections` patch, or undefined when
|
|
169
|
+
* nothing changed.
|
|
170
|
+
*/
|
|
171
|
+
export function diffSystemPromptSections(previous, current) {
|
|
172
|
+
const patch = {};
|
|
173
|
+
for (const [name, text] of Object.entries(current)) {
|
|
174
|
+
if (previous[name] !== text)
|
|
175
|
+
patch[name] = text;
|
|
123
176
|
}
|
|
124
|
-
|
|
125
|
-
|
|
177
|
+
for (const name of Object.keys(previous)) {
|
|
178
|
+
if (current[name] === undefined)
|
|
179
|
+
patch[name] = null;
|
|
126
180
|
}
|
|
127
|
-
|
|
128
|
-
return prompt;
|
|
181
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
129
182
|
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { buildSystemPrompt } from "./system-prompt.js";
|
|
2
|
+
import { buildSystemPrompt, buildSystemPromptSections, buildSystemPromptState } from "./system-prompt.js";
|
|
3
|
+
const skill = {
|
|
4
|
+
name: "test-skill",
|
|
5
|
+
description: "Test skill description",
|
|
6
|
+
filePath: "/tmp/skills/test-skill/SKILL.md",
|
|
7
|
+
baseDir: "/tmp/skills/test-skill",
|
|
8
|
+
sourceInfo: { type: "file", path: "/tmp/skills/test-skill/SKILL.md" },
|
|
9
|
+
disableModelInvocation: false,
|
|
10
|
+
};
|
|
11
|
+
const agent = {
|
|
12
|
+
name: "test-agent",
|
|
13
|
+
description: "Test agent description",
|
|
14
|
+
filePath: "/tmp/agents/test-agent.md",
|
|
15
|
+
baseDir: "/tmp/agents",
|
|
16
|
+
sourceInfo: { type: "file", path: "/tmp/agents/test-agent.md" },
|
|
17
|
+
frontmatter: {},
|
|
18
|
+
};
|
|
3
19
|
describe("buildSystemPrompt", () => {
|
|
4
20
|
it("includes the generic delegation-routing threshold in the default prompt", () => {
|
|
5
21
|
const prompt = buildSystemPrompt({ cwd: "/tmp/project" });
|
|
@@ -9,8 +25,9 @@ describe("buildSystemPrompt", () => {
|
|
|
9
25
|
expect(prompt).toMatch(/capable delegated agent/i);
|
|
10
26
|
expect(prompt).toMatch(/inspecting the delegation catalog\/list before selecting/i);
|
|
11
27
|
expect(prompt).toMatch(/parent remains the decision-maker and normally the sole writer/i);
|
|
12
|
-
expect(prompt).
|
|
13
|
-
expect(prompt).toContain("
|
|
28
|
+
expect(prompt).toContain("<rules>");
|
|
29
|
+
expect(prompt).toContain("- Be concise in your responses");
|
|
30
|
+
expect(prompt).toContain("<cwd>\n/tmp/project\n</cwd>");
|
|
14
31
|
});
|
|
15
32
|
it("keeps the collapsed Pi-docs block without the per-topic filename map", () => {
|
|
16
33
|
const prompt = buildSystemPrompt({ cwd: "/tmp/project" });
|
|
@@ -19,31 +36,23 @@ describe("buildSystemPrompt", () => {
|
|
|
19
36
|
expect(prompt).not.toContain("(docs/extensions.md");
|
|
20
37
|
});
|
|
21
38
|
it("keeps the default tool snippets, skills, and agents XML read-gated", () => {
|
|
22
|
-
const skill = {
|
|
23
|
-
name: "test-skill",
|
|
24
|
-
description: "Test skill description",
|
|
25
|
-
filePath: "/tmp/skills/test-skill/SKILL.md",
|
|
26
|
-
baseDir: "/tmp/skills/test-skill",
|
|
27
|
-
sourceInfo: { type: "file", path: "/tmp/skills/test-skill/SKILL.md" },
|
|
28
|
-
disableModelInvocation: false,
|
|
29
|
-
};
|
|
30
|
-
const agent = {
|
|
31
|
-
name: "test-agent",
|
|
32
|
-
description: "Test agent description",
|
|
33
|
-
filePath: "/tmp/agents/test-agent.md",
|
|
34
|
-
baseDir: "/tmp/agents",
|
|
35
|
-
sourceInfo: { type: "file", path: "/tmp/agents/test-agent.md" },
|
|
36
|
-
frontmatter: {},
|
|
37
|
-
};
|
|
38
39
|
const withoutRead = buildSystemPrompt({
|
|
39
40
|
cwd: "/tmp/project",
|
|
40
|
-
selectedTools: ["
|
|
41
|
+
selectedTools: ["edit"],
|
|
41
42
|
skills: [skill],
|
|
42
43
|
agents: [agent],
|
|
43
44
|
});
|
|
44
45
|
expect(withoutRead).not.toContain("<available_skills>");
|
|
45
46
|
expect(withoutRead).not.toContain("<available_agents>");
|
|
46
|
-
expect(withoutRead).toContain("
|
|
47
|
+
expect(withoutRead).toContain("<cwd>\n/tmp/project\n</cwd>");
|
|
48
|
+
const withBash = buildSystemPrompt({
|
|
49
|
+
cwd: "/tmp/project",
|
|
50
|
+
selectedTools: ["bash"],
|
|
51
|
+
skills: [skill],
|
|
52
|
+
agents: [agent],
|
|
53
|
+
});
|
|
54
|
+
expect(withBash).toContain("<available_skills>");
|
|
55
|
+
expect(withBash).not.toContain("<available_agents>");
|
|
47
56
|
const withRead = buildSystemPrompt({
|
|
48
57
|
cwd: "/tmp/project",
|
|
49
58
|
selectedTools: ["read", "bash"],
|
|
@@ -64,7 +73,7 @@ describe("buildSystemPrompt", () => {
|
|
|
64
73
|
expect(prompt).toContain("You are my custom assistant.");
|
|
65
74
|
expect(prompt).toContain("Active tool guidelines:");
|
|
66
75
|
expect(prompt).toContain('- Before executing, call { action: "list" }.');
|
|
67
|
-
expect(prompt).toContain("
|
|
76
|
+
expect(prompt).toContain("<cwd>\n/tmp/project\n</cwd>");
|
|
68
77
|
});
|
|
69
78
|
it("deduplicates and trims active promptGuidelines in custom prompts", () => {
|
|
70
79
|
const prompt = buildSystemPrompt({
|
|
@@ -79,7 +88,7 @@ describe("buildSystemPrompt", () => {
|
|
|
79
88
|
const prompt = buildSystemPrompt({ cwd: "/tmp/project", customPrompt: "Custom text." });
|
|
80
89
|
expect(prompt).toContain("Custom text.");
|
|
81
90
|
expect(prompt).not.toContain("Active tool guidelines:");
|
|
82
|
-
expect(prompt).toContain("
|
|
91
|
+
expect(prompt).toContain("<cwd>\n/tmp/project\n</cwd>");
|
|
83
92
|
});
|
|
84
93
|
it("renders active promptGuidelines in custom prompts without requiring read", () => {
|
|
85
94
|
const prompt = buildSystemPrompt({
|
|
@@ -92,3 +101,32 @@ describe("buildSystemPrompt", () => {
|
|
|
92
101
|
expect(prompt).not.toContain("<available_agents>");
|
|
93
102
|
});
|
|
94
103
|
});
|
|
104
|
+
describe("system prompt sections", () => {
|
|
105
|
+
it("tags every non-preamble section with its own name so a later update can address it", () => {
|
|
106
|
+
const sections = buildSystemPromptSections({
|
|
107
|
+
cwd: "/tmp/project",
|
|
108
|
+
selectedTools: ["read", "bash"],
|
|
109
|
+
skills: [skill],
|
|
110
|
+
});
|
|
111
|
+
expect(sections.preamble).toContain("SelesaiCode fork of Pi");
|
|
112
|
+
expect(sections.preamble.startsWith("<")).toBe(false);
|
|
113
|
+
for (const [name, content] of Object.entries(sections)) {
|
|
114
|
+
if (name === "preamble")
|
|
115
|
+
continue;
|
|
116
|
+
expect(content).toBe(`<${name}>\n${content.slice(name.length + 3, -1 * (name.length + 4))}\n</${name}>`);
|
|
117
|
+
}
|
|
118
|
+
expect(Object.keys(sections)).toContain("tools");
|
|
119
|
+
expect(Object.keys(sections)).toContain("rules");
|
|
120
|
+
expect(Object.keys(sections)).toContain("docs");
|
|
121
|
+
expect(Object.keys(sections)).toContain("skills");
|
|
122
|
+
expect(Object.keys(sections)).toContain("cwd");
|
|
123
|
+
});
|
|
124
|
+
it("carries a forced prompt as opaque content with no sections", () => {
|
|
125
|
+
expect(buildSystemPromptState({ forceSystemPrompt: "exact", cwd: "/tmp" })).toEqual({ content: "exact" });
|
|
126
|
+
expect(buildSystemPrompt({ forceSystemPrompt: "exact", cwd: "/tmp" })).toBe("exact");
|
|
127
|
+
});
|
|
128
|
+
it("rejects an invalid section name rather than producing a malformed prompt", () => {
|
|
129
|
+
expect(() => buildSystemPromptSections({ cwd: "/tmp", sections: { "Bad Name": "x" } })).toThrow(/Invalid system prompt section name/);
|
|
130
|
+
expect(() => buildSystemPromptSections({ cwd: "/tmp", sections: { preamble: "x" } })).toThrow(/Invalid system prompt section name/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -26,7 +26,8 @@ export interface BashOperations {
|
|
|
26
26
|
* @param command The command to execute
|
|
27
27
|
* @param cwd Working directory
|
|
28
28
|
* @param options Execution options
|
|
29
|
-
* @returns Promise resolving to exit code
|
|
29
|
+
* @returns Promise resolving to the exit code. Report signal terminations as 128 + signal number;
|
|
30
|
+
* a null exit code is treated as a failed command.
|
|
30
31
|
*/
|
|
31
32
|
exec: (command: string, cwd: string, options: {
|
|
32
33
|
onData: (data: Buffer) => void;
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { access as fsAccess } from "node:fs/promises";
|
|
3
|
+
import { constants as osConstants } from "node:os";
|
|
3
4
|
import { spawn } from "child_process";
|
|
4
5
|
import { Type } from "typebox";
|
|
5
6
|
import { waitForChildProcess } from "../../utils/child-process.js";
|
|
6
7
|
import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
|
|
7
|
-
import { getExperimentalToolSampling } from "../experimental.js";
|
|
8
8
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
9
9
|
import { BASH_UPDATE_THROTTLE_MS, createShellRenderers } from "./renderers/bash.js";
|
|
10
10
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
@@ -94,7 +94,10 @@ export function createLocalShellOperations(shellName, resolveShellConfig) {
|
|
|
94
94
|
if (timedOut) {
|
|
95
95
|
throw new Error(`timeout:${timeout}`);
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
// A signal-killed shell has no exit code. Use the standard shell convention so
|
|
98
|
+
// callers do not mistake the termination for a successful command.
|
|
99
|
+
const signalCode = child.signalCode;
|
|
100
|
+
return { exitCode: exitCode ?? (signalCode ? 128 + (osConstants.signals[signalCode] ?? 0) : 1) };
|
|
98
101
|
}
|
|
99
102
|
finally {
|
|
100
103
|
if (child.pid)
|
|
@@ -151,7 +154,7 @@ export function createShellToolDefinition(cwd, config, options) {
|
|
|
151
154
|
promptSnippet: config.promptSnippet,
|
|
152
155
|
promptGuidelines: exposeSessionEnvironment && config.promptGuidelines ? [...config.promptGuidelines] : undefined,
|
|
153
156
|
parameters: bashSchema,
|
|
154
|
-
constrainedSampling:
|
|
157
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
155
158
|
async execute(_toolCallId, { command, timeout }, signal, onUpdate, ctx) {
|
|
156
159
|
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
|
157
160
|
const spawnContext = resolveSpawnContext(resolvedCommand, ctx?.cwd || cwd, spawnHook, exposeSessionEnvironment, ctx);
|
|
@@ -260,7 +263,10 @@ export function createShellToolDefinition(cwd, config, options) {
|
|
|
260
263
|
}
|
|
261
264
|
const snapshot = await finishOutput();
|
|
262
265
|
const { text: outputText, details } = formatOutput(snapshot);
|
|
263
|
-
if (exitCode
|
|
266
|
+
if (exitCode === null) {
|
|
267
|
+
throw new Error(appendStatus(outputText, "Command terminated without an exit code"));
|
|
268
|
+
}
|
|
269
|
+
if (exitCode !== 0) {
|
|
264
270
|
throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));
|
|
265
271
|
}
|
|
266
272
|
return { content: [{ type: "text", text: outputText }], details };
|
package/dist/core/tools/edit.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { constants } from "fs";
|
|
2
2
|
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { getExperimentalToolSampling } from "../experimental.js";
|
|
5
4
|
import { applyEditsToNormalizedContent, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
|
|
6
5
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
7
6
|
import { resolveToCwd } from "./path-utils.js";
|
|
@@ -67,7 +66,7 @@ export function createEditToolDefinition(cwd, options) {
|
|
|
67
66
|
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
68
67
|
],
|
|
69
68
|
parameters: editSchema,
|
|
70
|
-
constrainedSampling:
|
|
69
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
71
70
|
renderShell: "self",
|
|
72
71
|
prepareArguments: prepareEditArguments,
|
|
73
72
|
async execute(_toolCallId, input, signal, _onUpdate, ctx) {
|
package/dist/core/tools/read.js
CHANGED
|
@@ -3,7 +3,6 @@ import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { processImage } from "../../utils/image-process.js";
|
|
5
5
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
6
|
-
import { getExperimentalToolSampling } from "../experimental.js";
|
|
7
6
|
import { captionImageWithModel } from "../vision-caption.js";
|
|
8
7
|
import { resolveReadPathAsync } from "./path-utils.js";
|
|
9
8
|
import { readRenderers } from "./renderers/read.js";
|
|
@@ -70,7 +69,7 @@ export function createReadToolDefinition(cwd, options) {
|
|
|
70
69
|
promptSnippet: readToolSystemPromptContribution.snippet,
|
|
71
70
|
promptGuidelines: [...readToolSystemPromptContribution.guidelines],
|
|
72
71
|
parameters: readSchema,
|
|
73
|
-
constrainedSampling:
|
|
72
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
74
73
|
async execute(_toolCallId, { path, offset, limit }, signal, _onUpdate, ctx) {
|
|
75
74
|
return new Promise((resolve, reject) => {
|
|
76
75
|
if (signal?.aborted) {
|
|
@@ -21,7 +21,15 @@ class BashResultRenderComponent extends Container {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
function formatDuration(ms) {
|
|
24
|
-
|
|
24
|
+
const seconds = ms / 1000;
|
|
25
|
+
if (seconds < 60)
|
|
26
|
+
return `${seconds.toFixed(1)}s`;
|
|
27
|
+
const totalSeconds = Math.floor(seconds);
|
|
28
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
29
|
+
const remainder = totalSeconds % 60;
|
|
30
|
+
if (minutes < 60)
|
|
31
|
+
return `${minutes}m ${remainder}s`;
|
|
32
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
|
|
25
33
|
}
|
|
26
34
|
function formatShellCall(args, prompt) {
|
|
27
35
|
const command = str(args?.command);
|
package/dist/core/tools/write.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
|
|
2
2
|
import { dirname } from "path";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { getExperimentalToolSampling } from "../experimental.js";
|
|
5
4
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
6
5
|
import { resolveToCwd } from "./path-utils.js";
|
|
7
6
|
import { writeRenderers } from "./renderers/write.js";
|
|
@@ -23,7 +22,7 @@ export function createWriteToolDefinition(cwd, options) {
|
|
|
23
22
|
promptSnippet: "Create or overwrite files",
|
|
24
23
|
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
25
24
|
parameters: writeSchema,
|
|
26
|
-
constrainedSampling:
|
|
25
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
27
26
|
async execute(_toolCallId, { path, content }, signal, _onUpdate, ctx) {
|
|
28
27
|
const absolutePath = resolveToCwd(path, ctx?.cwd || cwd);
|
|
29
28
|
const dir = dirname(absolutePath);
|
|
@@ -16,5 +16,5 @@ export interface UsageCostBreakdownEntry {
|
|
|
16
16
|
cost: number;
|
|
17
17
|
tokens: number;
|
|
18
18
|
}
|
|
19
|
-
/** Group
|
|
19
|
+
/** Group model-attributed usage by model and all other usage into a separate bucket. */
|
|
20
20
|
export declare function getUsageCostBreakdown(entries: SessionEntry[]): UsageCostBreakdownEntry[];
|
|
@@ -24,7 +24,7 @@ export function addUsageToTotals(totals, usage) {
|
|
|
24
24
|
totals.cacheWrite += usage.cacheWrite;
|
|
25
25
|
totals.cost += usage.cost.total;
|
|
26
26
|
}
|
|
27
|
-
/** Group
|
|
27
|
+
/** Group model-attributed usage by model and all other usage into a separate bucket. */
|
|
28
28
|
export function getUsageCostBreakdown(entries) {
|
|
29
29
|
const totalsByKey = new Map();
|
|
30
30
|
for (const entry of entries) {
|
|
@@ -34,6 +34,10 @@ export function getUsageCostBreakdown(entries) {
|
|
|
34
34
|
key = `${entry.message.provider}/${entry.message.responseModel ?? entry.message.model}`;
|
|
35
35
|
usage = entry.message.usage;
|
|
36
36
|
}
|
|
37
|
+
else if (entry.type === "usage") {
|
|
38
|
+
key = `${entry.provider}/${entry.model}`;
|
|
39
|
+
usage = entry.usage;
|
|
40
|
+
}
|
|
37
41
|
else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
|
|
38
42
|
key = "Tools/summaries";
|
|
39
43
|
usage = entry.message.usage;
|
|
@@ -213,13 +213,21 @@ export function createBackendSet(
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
216
|
+
// The keyless Tavily safety net, kept even under fanout — it wraps whatever search ended up
|
|
217
|
+
// being so a total failure still has somewhere to go. It used to arm only for the bare
|
|
218
|
+
// no-account DuckDuckGo default, which skipped every hosted provider configured with
|
|
219
|
+
// `fallback: duckduckgo`: when that fallback then hit a DNS-blocked or bot-walled DuckDuckGo
|
|
220
|
+
// the whole search died even though keyless Tavily was sitting right there. Arm it whenever
|
|
221
|
+
// DuckDuckGo is reachable in the chain. Opt out with PI_WEB_AGENT_DISABLE_KEYLESS_FALLBACK=1.
|
|
219
222
|
const keylessFallbackDisabled = process.env.PI_WEB_AGENT_DISABLE_KEYLESS_FALLBACK === '1';
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
// Fanout with no explicit provider list means "all usable", which always includes DuckDuckGo.
|
|
224
|
+
const fanoutActive = config.search.fanout?.mode !== undefined && config.search.fanout.mode !== 'off';
|
|
225
|
+
const duckDuckGoInChain =
|
|
226
|
+
!config.search.provider ||
|
|
227
|
+
config.search.provider === 'duckduckgo' ||
|
|
228
|
+
config.search.fallback === 'duckduckgo' ||
|
|
229
|
+
(fanoutActive && (config.search.fanout?.providers?.includes('duckduckgo') ?? true));
|
|
230
|
+
if (duckDuckGoInChain && !keylessFallbackDisabled) {
|
|
223
231
|
search = withSearchFallback(search, createTavilySearch({ keyless: true }), 'duckduckgo');
|
|
224
232
|
}
|
|
225
233
|
|
|
@@ -23,7 +23,9 @@ function serializeForModel(result: WebExploreResponse): string {
|
|
|
23
23
|
return `Research failed: ${result.error?.message ?? 'Unknown research failure.'}`;
|
|
24
24
|
}
|
|
25
25
|
if (result.findings.length === 0 && result.sources.length === 0) {
|
|
26
|
-
return
|
|
26
|
+
return result.metadata?.failureReason
|
|
27
|
+
? `No usable evidence found: ${result.metadata.failureReason}`
|
|
28
|
+
: 'No usable evidence found.';
|
|
27
29
|
}
|
|
28
30
|
return [
|
|
29
31
|
result.findings.join('\n\n'),
|