@aexol/spectral 0.7.7 → 0.8.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/dist/agent/agents.js +4 -4
- package/dist/agent/index.js +24 -148
- package/dist/cli.js +25 -220
- package/dist/commands/serve.js +1 -1
- package/dist/extensions/spectral-vision-fallback.js +225 -0
- package/dist/mcp/agent-dir.js +1 -1
- package/dist/mcp/config.js +3 -3
- package/dist/mcp/sampling-handler.js +1 -1
- package/dist/mcp/server-manager.js +5 -1
- package/dist/memory/commands/status.js +6 -6
- package/dist/memory/commands/view.js +16 -14
- package/dist/memory/compaction.js +33 -5
- package/dist/memory/config.js +3 -3
- package/dist/memory/debug-log.js +1 -1
- package/dist/memory/observer.js +2 -2
- package/dist/memory/prompts.js +5 -5
- package/dist/memory/tokens.js +1 -1
- package/dist/memory/tools/read-project-observations.js +2 -2
- package/dist/memory/tools/recall-observation.js +4 -4
- package/dist/relay/auto-research.js +23 -23
- package/dist/relay/dispatcher.js +28 -2
- package/dist/relay/models-fetch.js +15 -3
- package/dist/{pi → sdk}/coding-agent/cli/args.js +4 -4
- package/dist/{pi → sdk}/coding-agent/config.js +9 -20
- package/dist/{pi → sdk}/coding-agent/core/agent-session.js +5 -17
- package/dist/{pi → sdk}/coding-agent/core/compaction/compaction.js +161 -5
- package/dist/{pi → sdk}/coding-agent/core/extensions/loader.js +0 -6
- package/dist/{pi → sdk}/coding-agent/core/extensions/runner.js +7 -1
- package/dist/{pi → sdk}/coding-agent/core/keybindings.js +129 -2
- package/dist/{pi → sdk}/coding-agent/core/model-registry.js +11 -4
- package/dist/{pi → sdk}/coding-agent/core/package-manager.js +5 -5
- package/dist/{pi → sdk}/coding-agent/core/sdk.js +1 -1
- package/dist/{pi → sdk}/coding-agent/core/session-manager.js +4 -4
- package/dist/{pi → sdk}/coding-agent/core/settings-manager.js +20 -0
- package/dist/{pi → sdk}/coding-agent/core/telemetry.js +1 -1
- package/dist/{pi → sdk}/coding-agent/core/tools/bash.js +17 -63
- package/dist/{pi → sdk}/coding-agent/core/tools/edit.js +4 -141
- package/dist/{pi → sdk}/coding-agent/core/tools/find.js +0 -11
- package/dist/{pi → sdk}/coding-agent/core/tools/grep.js +0 -11
- package/dist/{pi → sdk}/coding-agent/core/tools/ls.js +0 -11
- package/dist/{pi → sdk}/coding-agent/core/tools/read.js +0 -12
- package/dist/{pi → sdk}/coding-agent/core/tools/render-utils.js +1 -14
- package/dist/{pi → sdk}/coding-agent/core/tools/write.js +2 -97
- package/dist/{pi → sdk}/coding-agent/migrations.js +3 -3
- package/dist/{pi → sdk}/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
- package/dist/sdk/coding-agent/modes/interactive/components/visual-truncate.js +26 -0
- package/dist/{pi → sdk}/coding-agent/modes/interactive/theme/theme.js +1 -2
- package/dist/{pi → sdk}/coding-agent/utils/tools-manager.js +1 -1
- package/dist/{pi → sdk}/coding-agent/utils/version-check.js +2 -2
- package/dist/{pi → sdk}/coding-agent/utils/windows-self-update.js +1 -1
- package/dist/server/{pi-bridge.js → agent-bridge.js} +158 -89
- package/dist/server/handlers/sessions.js +21 -0
- package/dist/server/session-stream.js +12 -6
- package/package.json +6 -3
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
- package/dist/pi/coding-agent/core/export-html/index.js +0 -225
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +0 -32
- package/dist/pi/tui/autocomplete.js +0 -631
- package/dist/pi/tui/components/box.js +0 -103
- package/dist/pi/tui/components/cancellable-loader.js +0 -34
- package/dist/pi/tui/components/editor.js +0 -1915
- package/dist/pi/tui/components/image.js +0 -88
- package/dist/pi/tui/components/input.js +0 -425
- package/dist/pi/tui/components/loader.js +0 -68
- package/dist/pi/tui/components/markdown.js +0 -633
- package/dist/pi/tui/components/select-list.js +0 -158
- package/dist/pi/tui/components/settings-list.js +0 -184
- package/dist/pi/tui/components/spacer.js +0 -22
- package/dist/pi/tui/components/text.js +0 -88
- package/dist/pi/tui/components/truncated-text.js +0 -50
- package/dist/pi/tui/editor-component.js +0 -1
- package/dist/pi/tui/fuzzy.js +0 -109
- package/dist/pi/tui/index.js +0 -31
- package/dist/pi/tui/keybindings.js +0 -173
- package/dist/pi/tui/keys.js +0 -1172
- package/dist/pi/tui/kill-ring.js +0 -43
- package/dist/pi/tui/stdin-buffer.js +0 -360
- package/dist/pi/tui/terminal-image.js +0 -335
- package/dist/pi/tui/terminal.js +0 -324
- package/dist/pi/tui/tui.js +0 -1076
- package/dist/pi/tui/undo-stack.js +0 -24
- package/dist/pi/tui/utils.js +0 -1016
- /package/dist/{pi → sdk}/agent-core/agent-loop.js +0 -0
- /package/dist/{pi → sdk}/agent-core/agent.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/agent-harness.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/compaction/branch-summarization.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/compaction/compaction.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/compaction/utils.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/env/nodejs.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/messages.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/prompt-templates.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-repo.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-storage.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/memory-repo.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/memory-storage.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/repo-utils.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/session.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/session/uuid.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/skills.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/system-prompt.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/types.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/utils/shell-output.js +0 -0
- /package/dist/{pi → sdk}/agent-core/harness/utils/truncate.js +0 -0
- /package/dist/{pi → sdk}/agent-core/index.js +0 -0
- /package/dist/{pi → sdk}/agent-core/node.js +0 -0
- /package/dist/{pi → sdk}/agent-core/proxy.js +0 -0
- /package/dist/{pi → sdk}/agent-core/types.js +0 -0
- /package/dist/{pi → sdk}/ai/api-registry.js +0 -0
- /package/dist/{pi → sdk}/ai/cli.js +0 -0
- /package/dist/{pi → sdk}/ai/env-api-keys.js +0 -0
- /package/dist/{pi → sdk}/ai/image-models.generated.js +0 -0
- /package/dist/{pi → sdk}/ai/image-models.js +0 -0
- /package/dist/{pi → sdk}/ai/images-api-registry.js +0 -0
- /package/dist/{pi → sdk}/ai/images.js +0 -0
- /package/dist/{pi → sdk}/ai/index.js +0 -0
- /package/dist/{pi → sdk}/ai/models.generated.js +0 -0
- /package/dist/{pi → sdk}/ai/models.js +0 -0
- /package/dist/{pi → sdk}/ai/oauth.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/anthropic.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/faux.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/github-copilot-headers.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/openai-completions.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/openai-prompt-cache.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/register-builtins.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/simple-options.js +0 -0
- /package/dist/{pi → sdk}/ai/providers/transform-messages.js +0 -0
- /package/dist/{pi → sdk}/ai/session-resources.js +0 -0
- /package/dist/{pi → sdk}/ai/stream.js +0 -0
- /package/dist/{pi → sdk}/ai/types.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/diagnostics.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/event-stream.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/hash.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/headers.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/json-parse.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/node-http-proxy.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/anthropic.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/device-code.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/github-copilot.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/index.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/oauth-page.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/openai-codex.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/pkce.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/oauth/types.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/overflow.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/sanitize-unicode.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/typebox-helpers.js +0 -0
- /package/dist/{pi → sdk}/ai/utils/validation.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/bun/cli.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/bun/restore-sandbox-env.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/cli/file-processor.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/cli/initial-message.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/cli.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/agent-session-runtime.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/agent-session-services.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/auth-guidance.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/auth-storage.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/bash-executor.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/compaction/branch-summarization.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/compaction/index.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/compaction/utils.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/defaults.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/diagnostics.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/event-bus.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/exec.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/extensions/index.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/extensions/types.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/extensions/wrapper.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/footer-data-provider.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/http-dispatcher.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/index.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/messages.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/model-resolver.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/output-guard.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/prompt-templates.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/provider-display-names.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/resolve-config-value.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/resource-loader.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/session-cwd.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/skills.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/slash-commands.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/source-info.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/system-prompt.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/timings.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/edit-diff.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/file-mutation-queue.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/index.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/output-accumulator.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/path-utils.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/tool-definition-wrapper.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/core/tools/truncate.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/index.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/main.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/index.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/diff.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/interactive/interactive-mode.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/print-mode.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/rpc/jsonl.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-client.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-mode.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-types.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/ansi.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/changelog.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/child-process.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/clipboard-image.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/clipboard-native.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/clipboard.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/exif-orientation.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/frontmatter.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/fs-watch.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/git.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/html.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/image-convert.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/image-resize.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/mime.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/paths.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/photon.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/pi-user-agent.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/shell.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/sleep.js +0 -0
- /package/dist/{pi → sdk}/coding-agent/utils/syntax-highlight.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-research handler — sends an auto-research task through the existing
|
|
3
|
-
*
|
|
3
|
+
* AgentBridge (backend proxy) instead of spawning a separate pi process.
|
|
4
4
|
*
|
|
5
5
|
* This ensures auto-research uses the same model and API keys as the active
|
|
6
6
|
* session — no separate subprocess, no missing API key errors.
|
|
@@ -47,12 +47,12 @@ function makeRelaySubscriber(sessionId, relay) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Scan the project's .
|
|
50
|
+
* Scan the project's .spectral/extensions/auto-research/ directory for generated
|
|
51
51
|
* extension directories. Each subdirectory with .ts files counts as one
|
|
52
52
|
* extension.
|
|
53
53
|
*/
|
|
54
54
|
function scanGeneratedExtensions(projectPath) {
|
|
55
|
-
const arDir = path.join(projectPath, ".
|
|
55
|
+
const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
|
|
56
56
|
const extensions = [];
|
|
57
57
|
try {
|
|
58
58
|
if (!fs.existsSync(arDir))
|
|
@@ -105,7 +105,7 @@ function scanGeneratedExtensions(projectPath) {
|
|
|
105
105
|
}
|
|
106
106
|
extensions.push({
|
|
107
107
|
name: entry.name,
|
|
108
|
-
path: `.
|
|
108
|
+
path: `.spectral/extensions/auto-research/${entry.name}`,
|
|
109
109
|
description,
|
|
110
110
|
usesLLM,
|
|
111
111
|
fileCount,
|
|
@@ -137,7 +137,7 @@ function hasAgentsMdUpdate(projectPath) {
|
|
|
137
137
|
*/
|
|
138
138
|
function readManifest(projectPath) {
|
|
139
139
|
try {
|
|
140
|
-
const mPath = path.join(projectPath, ".
|
|
140
|
+
const mPath = path.join(projectPath, ".spectral", "extensions", "auto-research", "manifest.json");
|
|
141
141
|
if (!fs.existsSync(mPath))
|
|
142
142
|
return null;
|
|
143
143
|
const raw = fs.readFileSync(mPath, "utf-8");
|
|
@@ -164,7 +164,7 @@ function gatherPreRunContext(projectPath) {
|
|
|
164
164
|
const isIncremental = manifest !== null;
|
|
165
165
|
const existingExtensions = [];
|
|
166
166
|
try {
|
|
167
|
-
const arDir = path.join(projectPath, ".
|
|
167
|
+
const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
|
|
168
168
|
if (fs.existsSync(arDir)) {
|
|
169
169
|
for (const entry of fs.readdirSync(arDir, { withFileTypes: true })) {
|
|
170
170
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
@@ -209,7 +209,7 @@ function buildIncrementalSection(ctx) {
|
|
|
209
209
|
if (ctx.existingExtensions.length > 0) {
|
|
210
210
|
lines.push("### Existing extensions to review:");
|
|
211
211
|
for (const name of ctx.existingExtensions) {
|
|
212
|
-
lines.push(` - \`.
|
|
212
|
+
lines.push(` - \`.spectral/extensions/auto-research/${name}/\``);
|
|
213
213
|
}
|
|
214
214
|
lines.push("");
|
|
215
215
|
}
|
|
@@ -224,7 +224,7 @@ function buildIncrementalSection(ctx) {
|
|
|
224
224
|
lines.push(" was removed from the project, delete the extension directory entirely.", "");
|
|
225
225
|
lines.push("4. **Update AGENTS.md** — The AUTO-RESEARCH section should reflect the CURRENT");
|
|
226
226
|
lines.push(" set of extensions (remove stale entries, add new ones).", "");
|
|
227
|
-
lines.push("5. **Save manifest.json** — Write/update .
|
|
227
|
+
lines.push("5. **Save manifest.json** — Write/update .spectral/extensions/auto-research/manifest.json");
|
|
228
228
|
lines.push(` with: lastRun (ISO), lastCommit (git HEAD), runCount (${ctx.manifest ? ctx.manifest.runCount + 1 : 1}),`);
|
|
229
229
|
lines.push(" and extensions array with name/path/category for each generated extension.", "");
|
|
230
230
|
return lines.join("\n");
|
|
@@ -234,7 +234,7 @@ function buildIncrementalSection(ctx) {
|
|
|
234
234
|
*/
|
|
235
235
|
function writeManifest(projectPath, extensions) {
|
|
236
236
|
try {
|
|
237
|
-
const arDir = path.join(projectPath, ".
|
|
237
|
+
const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
|
|
238
238
|
if (!fs.existsSync(arDir))
|
|
239
239
|
fs.mkdirSync(arDir, { recursive: true });
|
|
240
240
|
let currentCommit = "unknown";
|
|
@@ -257,7 +257,7 @@ function writeManifest(projectPath, extensions) {
|
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
259
|
* Build the auto-research task prompt. This is sent as a user message
|
|
260
|
-
* through the existing
|
|
260
|
+
* through the existing AgentBridge, so the agent uses the session's model
|
|
261
261
|
* and backend proxy.
|
|
262
262
|
*
|
|
263
263
|
* @param projectPath Absolute path to the project root
|
|
@@ -297,8 +297,8 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
297
297
|
"}",
|
|
298
298
|
"```",
|
|
299
299
|
"",
|
|
300
|
-
"Extensions live in: `.
|
|
301
|
-
"Auto-research generates extensions into `.
|
|
300
|
+
"Extensions live in: `.spectral/extensions/` (project-local) or `~/.spectral/agent/extensions/` (user-global).",
|
|
301
|
+
"Auto-research generates extensions into `.spectral/extensions/auto-research/<name>/`.",
|
|
302
302
|
"",
|
|
303
303
|
"### Tools (pi.registerTool)",
|
|
304
304
|
"",
|
|
@@ -400,10 +400,10 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
400
400
|
"",
|
|
401
401
|
"### Extension File Structure",
|
|
402
402
|
"",
|
|
403
|
-
"Each extension is a directory under `.
|
|
403
|
+
"Each extension is a directory under `.spectral/extensions/auto-research/`:",
|
|
404
404
|
"",
|
|
405
405
|
"```",
|
|
406
|
-
".
|
|
406
|
+
".spectral/extensions/auto-research/",
|
|
407
407
|
" <extension-name>/",
|
|
408
408
|
" index.ts # Entry point — default export activate(pi)",
|
|
409
409
|
" utils.ts # [optional] Helper functions",
|
|
@@ -440,7 +440,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
440
440
|
"",
|
|
441
441
|
"1. **Context Collection** — Explore the project structure:",
|
|
442
442
|
" - Read package.json, tsconfig.json, deno.json (if present)",
|
|
443
|
-
" - Check existing extensions under .
|
|
443
|
+
" - Check existing extensions under .spectral/extensions/",
|
|
444
444
|
" - Review key source files to understand architecture",
|
|
445
445
|
" - Check git log for recent changes and patterns",
|
|
446
446
|
"",
|
|
@@ -454,9 +454,9 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
454
454
|
" - Use `pi.registerTool()` for simple tools with TypeBox validation",
|
|
455
455
|
" - Use `pi.registerCommand()` for custom slash commands",
|
|
456
456
|
" - For LLM-powered extensions, use `ctx.modelRegistry` to call models",
|
|
457
|
-
" - Create files under `.
|
|
457
|
+
" - Create files under `.spectral/extensions/auto-research/<name>/`",
|
|
458
458
|
" - Each extension needs an `index.ts` that registers its tools/commands",
|
|
459
|
-
" - Read `.
|
|
459
|
+
" - Read `.spectral/agents/auto-research-templates.md` for proven extension templates",
|
|
460
460
|
"",
|
|
461
461
|
"4. **Validation** — Verify generated extensions:",
|
|
462
462
|
" - Ensure all imports resolve to available packages (see reference above)",
|
|
@@ -478,7 +478,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
478
478
|
"",
|
|
479
479
|
"### Important Rules",
|
|
480
480
|
"",
|
|
481
|
-
"- Write each extension as TypeScript files under `.
|
|
481
|
+
"- Write each extension as TypeScript files under `.spectral/extensions/auto-research/<name>/`",
|
|
482
482
|
"- Every extension directory MUST have an `index.ts` entry point",
|
|
483
483
|
"- Use proper TypeScript with type annotations and error handling",
|
|
484
484
|
"- Extensions must handle errors gracefully (never crash the agent)",
|
|
@@ -491,7 +491,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
491
491
|
"agent sessions automatically know about the new capabilities.",
|
|
492
492
|
"",
|
|
493
493
|
"**How AGENTS.md works:**",
|
|
494
|
-
"-
|
|
494
|
+
"- Spectral loads AGENTS.md at startup from the project root and parent directories",
|
|
495
495
|
"- All found AGENTS.md files are concatenated and injected into the system prompt",
|
|
496
496
|
"- This means documented extensions get discovered by the agent automatically",
|
|
497
497
|
"",
|
|
@@ -502,7 +502,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
502
502
|
"## Auto-Generated Extensions",
|
|
503
503
|
"",
|
|
504
504
|
"These extensions were generated by auto-research. They are available",
|
|
505
|
-
"in every session.
|
|
505
|
+
"in every session. Spectral loads them automatically from `.spectral/extensions/`.",
|
|
506
506
|
"",
|
|
507
507
|
"### `<extension-name>`",
|
|
508
508
|
"",
|
|
@@ -539,7 +539,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
|
|
|
539
539
|
// ---------------------------------------------------------------------------
|
|
540
540
|
const AR_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes — generous for LLM-based analysis
|
|
541
541
|
/**
|
|
542
|
-
* Execute auto-research for a project through the existing
|
|
542
|
+
* Execute auto-research for a project through the existing AgentBridge.
|
|
543
543
|
*
|
|
544
544
|
* Caller (dispatcher) is fire-and-forget — this function is `void` and
|
|
545
545
|
* all errors are surfaced as `auto_research_error` events on the wire
|
|
@@ -547,7 +547,7 @@ const AR_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes — generous for LLM-based a
|
|
|
547
547
|
*
|
|
548
548
|
* This replaces the old subprocess-spawning approach. Instead of launching
|
|
549
549
|
* a separate pi process (which lacks backend proxy credentials), we send
|
|
550
|
-
* the auto-research task through the session's existing
|
|
550
|
+
* the auto-research task through the session's existing AgentBridge. The agent
|
|
551
551
|
* uses the session's model, backend proxy, and all available tools.
|
|
552
552
|
*/
|
|
553
553
|
export function handleAutoResearch(input, deps) {
|
|
@@ -700,7 +700,7 @@ export function handleAutoResearch(input, deps) {
|
|
|
700
700
|
// then timeout fires and calls finalize() again — but finalize() is
|
|
701
701
|
// gated by watcherFired, so it's a no-op. The only issue is we don't
|
|
702
702
|
// clearTimeout on watcher fire — but that's fine, the timer is harmless.
|
|
703
|
-
// --- Send the prompt through the existing
|
|
703
|
+
// --- Send the prompt through the existing AgentBridge (backend proxy) ---
|
|
704
704
|
manager.prompt(sessionId, taskContent, storedModelId).catch((err) => {
|
|
705
705
|
if (watcherFired)
|
|
706
706
|
return; // already handled by watcher
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
|
|
42
42
|
import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
|
|
43
43
|
import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
|
|
44
|
-
import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
|
|
44
|
+
import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
|
|
45
45
|
import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
|
|
46
46
|
import { shutdownState } from "../server/shutdown.js";
|
|
47
47
|
import { handleAutoResearch } from "./auto-research.js";
|
|
@@ -126,6 +126,14 @@ export function matchRoute(method, path) {
|
|
|
126
126
|
return { route: "compact_session", id };
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
|
+
// /api/sessions/:id/remember-and-delete
|
|
130
|
+
const rememberDeleteMatch = /^\/api\/sessions\/([^/]+)\/remember-and-delete$/.exec(cleanPath);
|
|
131
|
+
if (rememberDeleteMatch) {
|
|
132
|
+
const id = decodeURIComponent(rememberDeleteMatch[1]);
|
|
133
|
+
if (method === "POST")
|
|
134
|
+
return { route: "remember_and_delete_session", id };
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
129
137
|
// /api/sessions/:id/fork
|
|
130
138
|
const forkMatch = /^\/api\/sessions\/([^/]+)\/fork$/.exec(cleanPath);
|
|
131
139
|
if (forkMatch) {
|
|
@@ -341,6 +349,24 @@ async function dispatchRoute(match, body, deps) {
|
|
|
341
349
|
}
|
|
342
350
|
case "compact_session":
|
|
343
351
|
return await handleCompactSession(store, manager, id);
|
|
352
|
+
case "remember_and_delete_session": {
|
|
353
|
+
// Compact first — the compaction hook's persistProjectObservations
|
|
354
|
+
// runs inside compactSession, writing reflections to cross-session
|
|
355
|
+
// durable memory. On failure (e.g. no API key), we let the error
|
|
356
|
+
// propagate so the session is NOT deleted.
|
|
357
|
+
await handleRememberAndDeleteSession(store, manager, id);
|
|
358
|
+
const detail = store.getSession(id);
|
|
359
|
+
manager.disposeSessionStream(id);
|
|
360
|
+
handleDeleteSession(store, id);
|
|
361
|
+
if (detail) {
|
|
362
|
+
safePublish(publishMetaEvent, logger, {
|
|
363
|
+
type: "session_deleted",
|
|
364
|
+
projectId: detail.projectId,
|
|
365
|
+
sessionId: id,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return { ok: true };
|
|
369
|
+
}
|
|
344
370
|
case "fork_session": {
|
|
345
371
|
const session = handleForkSession(store, id, asObject(body));
|
|
346
372
|
safePublish(publishMetaEvent, logger, {
|
|
@@ -682,7 +708,7 @@ export function detachAllSubscribers(manager, subscribers) {
|
|
|
682
708
|
}
|
|
683
709
|
/**
|
|
684
710
|
* Dispatch an `auto_research` frame. Sends the auto-research task through
|
|
685
|
-
* the existing
|
|
711
|
+
* the existing AgentBridge (backend proxy) instead of spawning a separate pi
|
|
686
712
|
* subprocess. This ensures auto-research uses the same model and API keys
|
|
687
713
|
* as the active session.
|
|
688
714
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetch the admin-managed list of allowed base models from the backend.
|
|
3
3
|
*
|
|
4
|
-
* Used by `
|
|
4
|
+
* Used by `AgentBridge` at startup to register synthetic providers
|
|
5
5
|
* (`spectral-proxy-anthropic` / `spectral-proxy-openai`) that route every
|
|
6
6
|
* inference call through the backend's `/v1/messages` and
|
|
7
7
|
* `/v1/chat/completions` endpoints. The backend authenticates the call
|
|
@@ -28,11 +28,11 @@ const cache = new Map();
|
|
|
28
28
|
export function clearAllowedModelsCache() {
|
|
29
29
|
cache.clear();
|
|
30
30
|
}
|
|
31
|
-
const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages } }`;
|
|
31
|
+
const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages supportsReasoning isDefault isVisionDefault } }`;
|
|
32
32
|
/**
|
|
33
33
|
* Fetch the whitelist of allowed base models. Throws on any failure with a
|
|
34
34
|
* message tailored for an operator running `spectral serve` — the caller
|
|
35
|
-
* (
|
|
35
|
+
* (AgentBridge.start) lets the throw propagate so the WS subscriber sees a
|
|
36
36
|
* clear error event instead of a silent fall-through to "no models".
|
|
37
37
|
*/
|
|
38
38
|
export async function fetchAllowedModels(opts) {
|
|
@@ -104,6 +104,15 @@ export async function fetchAllowedModels(opts) {
|
|
|
104
104
|
const supportsImages = typeof row?.supportsImages === "boolean"
|
|
105
105
|
? row.supportsImages
|
|
106
106
|
: null;
|
|
107
|
+
const supportsReasoning = typeof row?.supportsReasoning === "boolean"
|
|
108
|
+
? row.supportsReasoning
|
|
109
|
+
: null;
|
|
110
|
+
const isDefault = typeof row?.isDefault === "boolean"
|
|
111
|
+
? row.isDefault
|
|
112
|
+
: null;
|
|
113
|
+
const isVisionDefault = typeof row?.isVisionDefault === "boolean"
|
|
114
|
+
? row.isVisionDefault
|
|
115
|
+
: null;
|
|
107
116
|
const model = {
|
|
108
117
|
modelId: name,
|
|
109
118
|
displayName: name,
|
|
@@ -115,6 +124,9 @@ export async function fetchAllowedModels(opts) {
|
|
|
115
124
|
creditCacheWritePer1M: asOptionalNumber(row?.creditCacheWritePer1M),
|
|
116
125
|
contextWindow,
|
|
117
126
|
supportsImages,
|
|
127
|
+
supportsReasoning,
|
|
128
|
+
isDefault,
|
|
129
|
+
isVisionDefault,
|
|
118
130
|
};
|
|
119
131
|
if (typeof row?.userModelId === "string") {
|
|
120
132
|
model.userModelId = row.userModelId;
|
|
@@ -232,7 +232,7 @@ ${chalk.bold("Options:")}
|
|
|
232
232
|
--export <file> Export session file to HTML and exit
|
|
233
233
|
--list-models [search] List available models (with optional fuzzy search)
|
|
234
234
|
--verbose Force verbose startup (overrides quietStartup setting)
|
|
235
|
-
--offline Disable startup network operations (same as
|
|
235
|
+
--offline Disable startup network operations (same as SPECTRAL_OFFLINE=1)
|
|
236
236
|
--help, -h Show this help
|
|
237
237
|
--version, -v Show version number
|
|
238
238
|
|
|
@@ -324,9 +324,9 @@ ${chalk.bold("Environment Variables:")}
|
|
|
324
324
|
${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
|
325
325
|
${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)
|
|
326
326
|
PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
SPECTRAL_OFFLINE - Disable startup network operations when set to 1/true/yes
|
|
328
|
+
SPECTRAL_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
|
|
329
|
+
SPECTRAL_SHARE_VIEWER_URL - Base URL for /share command (default: https://spectral.dev/session/)
|
|
330
330
|
|
|
331
331
|
${chalk.bold("Built-in Tool Names:")}
|
|
332
332
|
read - Read file contents
|
|
@@ -297,17 +297,6 @@ export function getThemesDir() {
|
|
|
297
297
|
/**
|
|
298
298
|
* Get path to HTML export template directory (shipped with package)
|
|
299
299
|
* - For Bun binary: export-html/ next to executable
|
|
300
|
-
* - For Node.js (dist/): dist/core/export-html/
|
|
301
|
-
* - For tsx (src/): src/core/export-html/
|
|
302
|
-
*/
|
|
303
|
-
export function getExportTemplateDir() {
|
|
304
|
-
if (isBunBinary) {
|
|
305
|
-
return join(getPackageDir(), "export-html");
|
|
306
|
-
}
|
|
307
|
-
const packageDir = getPackageDir();
|
|
308
|
-
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
|
309
|
-
return join(packageDir, srcOrDist, "core", "export-html");
|
|
310
|
-
}
|
|
311
300
|
/** Get path to package.json */
|
|
312
301
|
export function getPackageJsonPath() {
|
|
313
302
|
return join(getPackageDir(), "package.json");
|
|
@@ -347,28 +336,28 @@ export function getBundledInteractiveAssetPath(name) {
|
|
|
347
336
|
return join(getInteractiveAssetsDir(), name);
|
|
348
337
|
}
|
|
349
338
|
const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
|
|
350
|
-
const
|
|
339
|
+
const spectralConfigName = pkg.spectralConfig?.name;
|
|
351
340
|
export const PACKAGE_NAME = pkg.name || "index.ts";
|
|
352
|
-
export const APP_NAME =
|
|
353
|
-
export const APP_TITLE =
|
|
354
|
-
export const CONFIG_DIR_NAME = pkg.
|
|
341
|
+
export const APP_NAME = spectralConfigName || "spectral";
|
|
342
|
+
export const APP_TITLE = spectralConfigName ? APP_NAME : "spectral";
|
|
343
|
+
export const CONFIG_DIR_NAME = pkg.spectralConfig?.configDir || ".spectral";
|
|
355
344
|
export const VERSION = pkg.version || "0.0.0";
|
|
356
|
-
// e.g.,
|
|
345
|
+
// e.g., SPECTRAL_CODING_AGENT_DIR
|
|
357
346
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
358
347
|
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
|
359
348
|
export function expandTildePath(path) {
|
|
360
349
|
return normalizePath(path);
|
|
361
350
|
}
|
|
362
|
-
const DEFAULT_SHARE_VIEWER_URL = "https://
|
|
351
|
+
const DEFAULT_SHARE_VIEWER_URL = "https://spectral.dev/session/";
|
|
363
352
|
/** Get the share viewer URL for a gist ID */
|
|
364
353
|
export function getShareViewerUrl(gistId) {
|
|
365
|
-
const baseUrl = process.env.
|
|
354
|
+
const baseUrl = process.env.SPECTRAL_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;
|
|
366
355
|
return `${baseUrl}#${gistId}`;
|
|
367
356
|
}
|
|
368
357
|
// =============================================================================
|
|
369
|
-
// User Config Paths (~/.
|
|
358
|
+
// User Config Paths (~/.spectral/agent/*)
|
|
370
359
|
// =============================================================================
|
|
371
|
-
/** Get the agent config directory (e.g., ~/.
|
|
360
|
+
/** Get the agent config directory (e.g., ~/.spectral/agent/) */
|
|
372
361
|
export function getAgentDir() {
|
|
373
362
|
const envDir = process.env[ENV_AGENT_DIR];
|
|
374
363
|
if (envDir) {
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname } from "node:path";
|
|
17
17
|
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "../../ai/index.js";
|
|
18
|
-
import { theme } from "../modes/interactive/theme/theme.js";
|
|
19
18
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
20
19
|
import { resolvePath } from "../utils/paths.js";
|
|
21
20
|
import { sleep } from "../utils/sleep.js";
|
|
@@ -23,8 +22,6 @@ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth
|
|
|
23
22
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
24
23
|
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
25
24
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
26
|
-
import { exportSessionToHtml } from "./export-html/index.js";
|
|
27
|
-
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
28
25
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
29
26
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
30
27
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
@@ -468,6 +465,8 @@ export class AgentSession {
|
|
|
468
465
|
*/
|
|
469
466
|
dispose() {
|
|
470
467
|
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
468
|
+
this.abortRetry();
|
|
469
|
+
this.agent.abort();
|
|
471
470
|
this._disconnectFromAgent();
|
|
472
471
|
this._eventListeners = [];
|
|
473
472
|
cleanupSessionResources(this.sessionId);
|
|
@@ -1889,7 +1888,7 @@ export class AgentSession {
|
|
|
1889
1888
|
extensionsResult.runtime.flagValues.set(name, value);
|
|
1890
1889
|
}
|
|
1891
1890
|
}
|
|
1892
|
-
this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry);
|
|
1891
|
+
this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry, this.settingsManager);
|
|
1893
1892
|
if (this._extensionRunnerRef) {
|
|
1894
1893
|
this._extensionRunnerRef.current = this._extensionRunner;
|
|
1895
1894
|
}
|
|
@@ -2398,19 +2397,8 @@ export class AgentSession {
|
|
|
2398
2397
|
* @param outputPath Optional output path (defaults to session directory)
|
|
2399
2398
|
* @returns Path to exported file
|
|
2400
2399
|
*/
|
|
2401
|
-
async exportToHtml(
|
|
2402
|
-
|
|
2403
|
-
// Create tool renderer if we have an extension runner (for custom tool HTML rendering)
|
|
2404
|
-
const toolRenderer = createToolHtmlRenderer({
|
|
2405
|
-
getToolDefinition: (name) => this.getToolDefinition(name),
|
|
2406
|
-
theme,
|
|
2407
|
-
cwd: this.sessionManager.getCwd(),
|
|
2408
|
-
});
|
|
2409
|
-
return await exportSessionToHtml(this.sessionManager, this.state, {
|
|
2410
|
-
outputPath,
|
|
2411
|
-
themeName,
|
|
2412
|
-
toolRenderer,
|
|
2413
|
-
});
|
|
2400
|
+
async exportToHtml(_outputPath) {
|
|
2401
|
+
throw new Error("HTML export has been removed. Use spectral serve instead.");
|
|
2414
2402
|
}
|
|
2415
2403
|
/**
|
|
2416
2404
|
* Export the current session branch to a JSONL file.
|
|
@@ -543,6 +543,157 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
543
543
|
};
|
|
544
544
|
}
|
|
545
545
|
// ============================================================================
|
|
546
|
+
// Tool Call Deduplication
|
|
547
|
+
// ============================================================================
|
|
548
|
+
/**
|
|
549
|
+
* Tools that always return the same result for the same arguments.
|
|
550
|
+
* Same (name, args) from any point in the session = duplicate.
|
|
551
|
+
* Only the most recent call is kept.
|
|
552
|
+
*/
|
|
553
|
+
const IDEMPOTENT_READ_TOOLS = new Set(["read"]);
|
|
554
|
+
/**
|
|
555
|
+
* Tools whose output may differ between calls with the same arguments.
|
|
556
|
+
* Deduplication requires comparing actual outputs to determine equivalence.
|
|
557
|
+
*/
|
|
558
|
+
const OUTPUT_DEPENDENT_TOOLS = new Set(["bash"]);
|
|
559
|
+
/**
|
|
560
|
+
* Tools that mutate state. Never deduplicated — chronological ordering matters.
|
|
561
|
+
*/
|
|
562
|
+
const MUTABLE_TOOLS = new Set(["edit", "write"]);
|
|
563
|
+
/**
|
|
564
|
+
* Build a stable string key from a Record's sorted keys.
|
|
565
|
+
* Ensures {a:1, b:2} and {b:2, a:1} produce the same key.
|
|
566
|
+
*/
|
|
567
|
+
function stableArgs(args) {
|
|
568
|
+
const sortedKeys = Object.keys(args).sort();
|
|
569
|
+
const sorted = {};
|
|
570
|
+
for (const key of sortedKeys) {
|
|
571
|
+
sorted[key] = args[key];
|
|
572
|
+
}
|
|
573
|
+
return JSON.stringify(sorted);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Compute the deduplication key for a tool call.
|
|
577
|
+
* - Idempotent-read tools: keyed by (name, args) — same args always same result
|
|
578
|
+
* - Output-dependent tools: keyed by (name, args, output) — output comparison needed
|
|
579
|
+
*/
|
|
580
|
+
function toolCallDedupKey(toolName, args, toolCallId, resultMap) {
|
|
581
|
+
const argsKey = stableArgs(args);
|
|
582
|
+
if (OUTPUT_DEPENDENT_TOOLS.has(toolName)) {
|
|
583
|
+
const output = resultMap.get(toolCallId) ?? "";
|
|
584
|
+
return `${toolName}:${argsKey}:${output}`;
|
|
585
|
+
}
|
|
586
|
+
return `${toolName}:${argsKey}`;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Check whether a tool should be excluded from deduplication.
|
|
590
|
+
* Mutating tools and unknown tools are never deduplicated (conservative).
|
|
591
|
+
*/
|
|
592
|
+
function isMutableOrUnknownTool(toolName) {
|
|
593
|
+
if (MUTABLE_TOOLS.has(toolName))
|
|
594
|
+
return true;
|
|
595
|
+
if (IDEMPOTENT_READ_TOOLS.has(toolName))
|
|
596
|
+
return false;
|
|
597
|
+
if (OUTPUT_DEPENDENT_TOOLS.has(toolName))
|
|
598
|
+
return false;
|
|
599
|
+
// Unknown tools: conservative — never deduplicate
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Deduplicate repeated tool calls in a message array by keeping only the
|
|
604
|
+
* most recent occurrence of each (tool, args) pair.
|
|
605
|
+
*
|
|
606
|
+
* Strategy:
|
|
607
|
+
* - **Idempotent-read tools** (read, grep, glob): same (name, args) → keep last only.
|
|
608
|
+
* These tools return the same content for the same arguments.
|
|
609
|
+
* - **Output-dependent tools** (bash): same (name, args, output) → keep last only.
|
|
610
|
+
* Two bash calls with same command but different output are NOT duplicates.
|
|
611
|
+
* - **Mutating tools** (edit, write): never deduplicated. Chronological ordering
|
|
612
|
+
* of mutations matters for correctness.
|
|
613
|
+
* - **Unknown tools** (extensions, MCP, custom): never deduplicated. Conservative
|
|
614
|
+
* by default — only tools in the known sets above participate.
|
|
615
|
+
*
|
|
616
|
+
* Deduplication removes both the ToolCall block from the assistant message
|
|
617
|
+
* and the corresponding ToolResultMessage from the array.
|
|
618
|
+
*
|
|
619
|
+
* Recalculated each time compaction runs — prompt cache is only impacted
|
|
620
|
+
* alongside compression, not on every turn.
|
|
621
|
+
*/
|
|
622
|
+
export function deduplicateToolCalls(messages) {
|
|
623
|
+
if (messages.length === 0)
|
|
624
|
+
return messages;
|
|
625
|
+
// Phase 1: Build result lookup for output-dependent tools
|
|
626
|
+
const resultMap = new Map();
|
|
627
|
+
for (const msg of messages) {
|
|
628
|
+
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
|
629
|
+
const text = msg.content
|
|
630
|
+
.filter((c) => c.type === "text")
|
|
631
|
+
.map((c) => c.text)
|
|
632
|
+
.join("\n");
|
|
633
|
+
resultMap.set(msg.toolCallId, text);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Phase 2: Walk newest→oldest, collect keys. The first encounter
|
|
637
|
+
// (newest) wins; all earlier tool calls with the same key are duplicates.
|
|
638
|
+
const seen = new Map(); // key → toolCallId (keep newest)
|
|
639
|
+
const duplicateIds = new Set();
|
|
640
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
641
|
+
const msg = messages[i];
|
|
642
|
+
if (msg.role !== "assistant")
|
|
643
|
+
continue;
|
|
644
|
+
if (!("content" in msg) || !Array.isArray(msg.content))
|
|
645
|
+
continue;
|
|
646
|
+
// Iterate blocks newest-first: within a single assistant message,
|
|
647
|
+
// the rightmost tool call is the "most recent" one.
|
|
648
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
649
|
+
const block = msg.content[j];
|
|
650
|
+
if (typeof block !== "object" || block === null)
|
|
651
|
+
continue;
|
|
652
|
+
if (!("type" in block) || block.type !== "toolCall")
|
|
653
|
+
continue;
|
|
654
|
+
const toolBlock = block;
|
|
655
|
+
if (isMutableOrUnknownTool(toolBlock.name))
|
|
656
|
+
continue;
|
|
657
|
+
const key = toolCallDedupKey(toolBlock.name, toolBlock.arguments, toolBlock.id, resultMap);
|
|
658
|
+
if (seen.has(key)) {
|
|
659
|
+
duplicateIds.add(toolBlock.id);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
seen.set(key, toolBlock.id);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (duplicateIds.size === 0)
|
|
667
|
+
return messages;
|
|
668
|
+
// Phase 3: Filter out duplicate tool results and strip duplicate
|
|
669
|
+
// ToolCall blocks from assistant messages.
|
|
670
|
+
const deduped = [];
|
|
671
|
+
let modified = false;
|
|
672
|
+
for (const msg of messages) {
|
|
673
|
+
if (msg.role === "toolResult" && duplicateIds.has(msg.toolCallId)) {
|
|
674
|
+
modified = true;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (msg.role === "assistant" && "content" in msg && Array.isArray(msg.content)) {
|
|
678
|
+
const originalLength = msg.content.length;
|
|
679
|
+
const filteredContent = msg.content.filter((block) => {
|
|
680
|
+
if (typeof block !== "object" || block === null)
|
|
681
|
+
return true;
|
|
682
|
+
if (!("type" in block) || block.type !== "toolCall")
|
|
683
|
+
return true;
|
|
684
|
+
return !duplicateIds.has(block.id);
|
|
685
|
+
});
|
|
686
|
+
if (filteredContent.length < originalLength) {
|
|
687
|
+
modified = true;
|
|
688
|
+
deduped.push({ ...msg, content: filteredContent });
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
deduped.push(msg);
|
|
693
|
+
}
|
|
694
|
+
return modified ? deduped : messages;
|
|
695
|
+
}
|
|
696
|
+
// ============================================================================
|
|
546
697
|
// Main compaction function
|
|
547
698
|
// ============================================================================
|
|
548
699
|
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
|
@@ -568,22 +719,27 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
|
568
719
|
*/
|
|
569
720
|
export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn) {
|
|
570
721
|
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
|
|
722
|
+
// Remove redundant tool calls before summarization.
|
|
723
|
+
// Deduplication is applied here (not in prepareCompaction) so that
|
|
724
|
+
// the session_before_compact extension hook receives raw messages.
|
|
725
|
+
const dedupedMessages = deduplicateToolCalls(messagesToSummarize);
|
|
726
|
+
const dedupedTurnPrefix = deduplicateToolCalls(turnPrefixMessages);
|
|
571
727
|
// Generate summaries (can be parallel if both needed) and merge into one
|
|
572
728
|
let summary;
|
|
573
|
-
if (isSplitTurn &&
|
|
729
|
+
if (isSplitTurn && dedupedTurnPrefix.length > 0) {
|
|
574
730
|
// Generate both summaries in parallel
|
|
575
731
|
const [historyResult, turnPrefixResult] = await Promise.all([
|
|
576
|
-
|
|
577
|
-
? generateSummary(
|
|
732
|
+
dedupedMessages.length > 0
|
|
733
|
+
? generateSummary(dedupedMessages, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn)
|
|
578
734
|
: Promise.resolve("No prior history."),
|
|
579
|
-
generateTurnPrefixSummary(
|
|
735
|
+
generateTurnPrefixSummary(dedupedTurnPrefix, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn),
|
|
580
736
|
]);
|
|
581
737
|
// Merge into single summary
|
|
582
738
|
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
|
|
583
739
|
}
|
|
584
740
|
else {
|
|
585
741
|
// Just generate history summary
|
|
586
|
-
summary = await generateSummary(
|
|
742
|
+
summary = await generateSummary(dedupedMessages, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn);
|
|
587
743
|
}
|
|
588
744
|
// Compute file lists and append to summary
|
|
589
745
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
@@ -9,7 +9,6 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import * as _bundledPiAgentCore from "../../../agent-core/index.js";
|
|
10
10
|
import * as _bundledPiAi from "../../../ai/index.js";
|
|
11
11
|
import * as _bundledPiAiOauth from "../../../ai/oauth.js";
|
|
12
|
-
import * as _bundledPiTui from "../../../tui/index.js";
|
|
13
12
|
import { createJiti } from "@mariozechner/jiti";
|
|
14
13
|
// Static imports of packages that extensions may use.
|
|
15
14
|
// These MUST be static so Bun bundles them into the compiled binary.
|
|
@@ -34,12 +33,10 @@ const VIRTUAL_MODULES = {
|
|
|
34
33
|
"@sinclair/typebox/compile": _bundledTypeboxCompile,
|
|
35
34
|
"@sinclair/typebox/value": _bundledTypeboxValue,
|
|
36
35
|
"../../../agent-core/index.ts": _bundledPiAgentCore,
|
|
37
|
-
"../../../tui/index.ts": _bundledPiTui,
|
|
38
36
|
"../../../ai/index.ts": _bundledPiAi,
|
|
39
37
|
"../../../ai/oauth.ts": _bundledPiAiOauth,
|
|
40
38
|
"../../index.ts": _bundledPiCodingAgent,
|
|
41
39
|
"@mariozechner/pi-agent": _bundledPiAgentCore,
|
|
42
|
-
"@mariozechner/pi-tui": _bundledPiTui,
|
|
43
40
|
"@mariozechner/pi-ai": _bundledPiAi,
|
|
44
41
|
"@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
|
|
45
42
|
"@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
|
|
@@ -68,18 +65,15 @@ function getAliases() {
|
|
|
68
65
|
};
|
|
69
66
|
const piCodingAgentEntry = packageIndex;
|
|
70
67
|
const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "../../../agent-core/index.ts");
|
|
71
|
-
const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "../../../tui/index.ts");
|
|
72
68
|
const piAiEntry = resolveWorkspaceOrImport("ai/dist/index.js", "../../../ai/index.ts");
|
|
73
69
|
const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "../../../ai/oauth.ts");
|
|
74
70
|
_aliases = {
|
|
75
71
|
"../../index.ts": piCodingAgentEntry,
|
|
76
72
|
"../../../agent-core/index.ts": piAgentCoreEntry,
|
|
77
|
-
"../../../tui/index.ts": piTuiEntry,
|
|
78
73
|
"../../../ai/index.ts": piAiEntry,
|
|
79
74
|
"../../../ai/oauth.ts": piAiOauthEntry,
|
|
80
75
|
"@mariozechner/pi-coding-agent": piCodingAgentEntry,
|
|
81
76
|
"@mariozechner/pi-agent": piAgentCoreEntry,
|
|
82
|
-
"@mariozechner/pi-tui": piTuiEntry,
|
|
83
77
|
"@mariozechner/pi-ai": piAiEntry,
|
|
84
78
|
"@mariozechner/pi-ai/oauth": piAiOauthEntry,
|
|
85
79
|
typebox: typeboxEntry,
|