@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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-connection pi SDK lifecycle.
|
|
3
3
|
*
|
|
4
|
-
* One `
|
|
4
|
+
* One `AgentBridge` instance per active WebSocket connection. Wraps:
|
|
5
5
|
* - `createAgentSession` (in-memory session manager — we own persistence in
|
|
6
6
|
* SQLite; pi doesn't need to write its own JSONL files).
|
|
7
7
|
* - `subscribe` listener that translates pi `AgentSessionEvent`s into our
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* History rehydration:
|
|
42
42
|
* On first attach to a previously-created session (e.g. after a server
|
|
43
43
|
* restart), the SessionStreamManager passes the full SQLite transcript
|
|
44
|
-
* to the
|
|
44
|
+
* to the AgentBridge via `AgentBridgeOptions.history`. Before
|
|
45
45
|
* `createAgentSession` is called, each message is appended to the
|
|
46
46
|
* in-memory SessionManager so the LLM sees the full conversation
|
|
47
47
|
* context from the very first prompt. Multi-turn conversations within
|
|
@@ -49,12 +49,13 @@
|
|
|
49
49
|
* instance is reused across `prompt()` calls).
|
|
50
50
|
*/
|
|
51
51
|
import { createJiti } from "@mariozechner/jiti";
|
|
52
|
-
import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "../
|
|
52
|
+
import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "../sdk/coding-agent/index.js";
|
|
53
53
|
import { randomUUID } from "node:crypto";
|
|
54
54
|
import { existsSync, statSync } from "node:fs";
|
|
55
55
|
import { dirname, join, resolve } from "node:path";
|
|
56
56
|
import { fileURLToPath } from "node:url";
|
|
57
57
|
import aexolMcpExtension from "../extensions/aexol-mcp.js";
|
|
58
|
+
import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
|
|
58
59
|
import subagentExt from "../agent/index.js";
|
|
59
60
|
import designerExtension from "../designer/index.js";
|
|
60
61
|
import observationalMemory from "../memory/index.js";
|
|
@@ -133,7 +134,7 @@ function extractTextFromContent(content) {
|
|
|
133
134
|
*/
|
|
134
135
|
function resolveMcpAdapterEntry() {
|
|
135
136
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
136
|
-
//
|
|
137
|
+
// agent-bridge.ts compiles to dist/server/agent-bridge.js;
|
|
137
138
|
// the bundled MCP adapter sits next door at dist/mcp/index.js.
|
|
138
139
|
const bundledIndex = resolve(__dirname, "..", "mcp", "index.js");
|
|
139
140
|
if (existsSync(bundledIndex))
|
|
@@ -195,6 +196,27 @@ function bareModelId(modelId) {
|
|
|
195
196
|
const idx = modelId.lastIndexOf("/");
|
|
196
197
|
return idx === -1 ? modelId : modelId.slice(idx + 1);
|
|
197
198
|
}
|
|
199
|
+
/** Get model cost from backend credit rates, falling back to hardcoded pricing table. */
|
|
200
|
+
function getModelCost(m) {
|
|
201
|
+
// Prefer backend credit rates when configured
|
|
202
|
+
const hasCredits = m.creditInputPer1M != null ||
|
|
203
|
+
m.creditOutputPer1M != null ||
|
|
204
|
+
m.creditCacheReadPer1M != null ||
|
|
205
|
+
m.creditCacheWritePer1M != null;
|
|
206
|
+
if (hasCredits) {
|
|
207
|
+
return {
|
|
208
|
+
input: m.creditInputPer1M ?? 0,
|
|
209
|
+
output: m.creditOutputPer1M ?? 0,
|
|
210
|
+
cacheRead: m.creditCacheReadPer1M ?? 0,
|
|
211
|
+
cacheWrite: m.creditCacheWritePer1M ?? 0,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Fall back to hardcoded pricing table when credits aren't configured
|
|
215
|
+
const pricing = lookupPricing(m.modelId);
|
|
216
|
+
return pricing
|
|
217
|
+
? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
|
|
218
|
+
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
219
|
+
}
|
|
198
220
|
/** Look up pricing for a modelId. Returns null when unknown. */
|
|
199
221
|
function lookupPricing(modelId) {
|
|
200
222
|
const bare = bareModelId(modelId);
|
|
@@ -210,21 +232,28 @@ function lookupPricing(modelId) {
|
|
|
210
232
|
}
|
|
211
233
|
return null;
|
|
212
234
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
function inferSyntheticOpenAICompat(model) {
|
|
236
|
+
const bare = bareModelId(model.modelId);
|
|
237
|
+
const isDeepSeek = model.provider === "deepseek" ||
|
|
238
|
+
model.modelId.startsWith("deepseek/") ||
|
|
239
|
+
bare.startsWith("deepseek");
|
|
240
|
+
if (isDeepSeek) {
|
|
241
|
+
return {
|
|
242
|
+
thinkingFormat: "deepseek",
|
|
243
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
244
|
+
supportsDeveloperRole: false,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (model.provider === "openrouter") {
|
|
248
|
+
const compat = {
|
|
249
|
+
thinkingFormat: "openrouter",
|
|
250
|
+
};
|
|
251
|
+
if (model.modelId.startsWith("anthropic/")) {
|
|
252
|
+
compat.cacheControlFormat = "anthropic";
|
|
253
|
+
}
|
|
254
|
+
return compat;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
228
257
|
}
|
|
229
258
|
/**
|
|
230
259
|
* Calculate credits from token usage using per-model credit rates.
|
|
@@ -348,7 +377,7 @@ function toRestoredMemorySnapshot(snapshot) {
|
|
|
348
377
|
details: normalizeSnapshotDetails(snapshot.details),
|
|
349
378
|
};
|
|
350
379
|
}
|
|
351
|
-
export class
|
|
380
|
+
export class AgentBridge {
|
|
352
381
|
session;
|
|
353
382
|
sessionManager;
|
|
354
383
|
unsubscribe;
|
|
@@ -388,8 +417,8 @@ export class PiBridge {
|
|
|
388
417
|
*/
|
|
389
418
|
async start() {
|
|
390
419
|
if (this.disposed)
|
|
391
|
-
throw new Error("
|
|
392
|
-
const extensionFactories = [aexolMcpExtension, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
|
|
420
|
+
throw new Error("AgentBridge already disposed");
|
|
421
|
+
const extensionFactories = [aexolMcpExtension, async (pi) => { spectralVisionExtension(pi); }, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
|
|
393
422
|
// Load pi-mcp-adapter via jiti so tsc never crawls its .ts files in
|
|
394
423
|
// node_modules. The static `import` was causing tsc to type-check
|
|
395
424
|
// pi-mcp-adapter's source and fail the build on its type errors.
|
|
@@ -404,25 +433,25 @@ export class PiBridge {
|
|
|
404
433
|
}
|
|
405
434
|
}
|
|
406
435
|
catch {
|
|
407
|
-
console.info("[
|
|
436
|
+
console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
|
|
408
437
|
}
|
|
409
438
|
}
|
|
410
439
|
else {
|
|
411
|
-
console.info("[
|
|
440
|
+
console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
|
|
412
441
|
}
|
|
413
442
|
// ResourceLoader with extensions wired in via factories.
|
|
414
443
|
// Each factory's signature `(pi: ExtensionAPI) => Promise<void>` matches
|
|
415
444
|
// the ExtensionFactory type exactly, so we can pass them directly.
|
|
416
445
|
//
|
|
417
|
-
// Skill discovery: pi's defaults scan ~/.
|
|
418
|
-
// .
|
|
446
|
+
// Skill discovery: pi's defaults scan ~/.spectral/agent/skills/ (user),
|
|
447
|
+
// .spectral/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
|
|
419
448
|
// (ancestor-walked). We additionally walk ancestors for
|
|
420
449
|
// .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
|
|
421
450
|
// work out of the box.
|
|
422
451
|
const extraSkillPaths = collectAncestorSkillDirs(this.opts.cwd, [".opencode/skills", ".aexol/skills"]);
|
|
423
452
|
const resourceLoader = new DefaultResourceLoader({
|
|
424
453
|
cwd: this.opts.cwd,
|
|
425
|
-
agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.
|
|
454
|
+
agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.spectral/agent`,
|
|
426
455
|
extensionFactories,
|
|
427
456
|
noExtensions: false,
|
|
428
457
|
noSkills: false,
|
|
@@ -528,7 +557,7 @@ export class PiBridge {
|
|
|
528
557
|
}
|
|
529
558
|
// system messages are informational only; skip for LLM context
|
|
530
559
|
}
|
|
531
|
-
console.info(`[
|
|
560
|
+
console.info(`[AgentBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
|
|
532
561
|
}
|
|
533
562
|
if (this.opts.memorySnapshot) {
|
|
534
563
|
const restoredSnapshot = toRestoredMemorySnapshot(this.opts.memorySnapshot);
|
|
@@ -544,10 +573,10 @@ export class PiBridge {
|
|
|
544
573
|
coveredSourceCount: restoredSnapshot.coveredSourceCount,
|
|
545
574
|
});
|
|
546
575
|
}
|
|
547
|
-
console.info(`[
|
|
576
|
+
console.info(`[AgentBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
|
|
548
577
|
}
|
|
549
|
-
// Build a model registry that does NOT touch ~/.
|
|
550
|
-
// ~/.
|
|
578
|
+
// Build a model registry that does NOT touch ~/.spectral/agent/auth.json or
|
|
579
|
+
// ~/.spectral/agent/models.json — the backend is now the only source of
|
|
551
580
|
// provider credentials and the only allowed inference target. We then
|
|
552
581
|
// register synthetic providers (`spectral-proxy-anthropic` /
|
|
553
582
|
// `spectral-proxy-openai`) whose `baseUrl` points at the backend's
|
|
@@ -560,21 +589,34 @@ export class PiBridge {
|
|
|
560
589
|
// disk-based auth: this is the single path for `spectral serve`.
|
|
561
590
|
const authStorage = AuthStorage.inMemory();
|
|
562
591
|
this.modelRegistry = ModelRegistry.inMemory(authStorage);
|
|
563
|
-
const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
|
|
564
592
|
let allowedModels;
|
|
565
593
|
try {
|
|
566
|
-
allowedModels = await
|
|
567
|
-
backendUrl: this.opts.backendUrl,
|
|
568
|
-
machineJwt: this.opts.machineJwt,
|
|
569
|
-
});
|
|
594
|
+
allowedModels = await this.refreshAllowedModels();
|
|
570
595
|
}
|
|
571
596
|
catch (err) {
|
|
572
597
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
573
598
|
throw new Error(`Failed to fetch allowed models from backend; check SPECTRAL_BACKEND_URL ` +
|
|
574
599
|
`and machine JWT. Underlying error: ${e.message}`);
|
|
575
600
|
}
|
|
576
|
-
|
|
577
|
-
|
|
601
|
+
// Build an in-memory SettingsManager seeded with admin-configured
|
|
602
|
+
// defaults from the backend. findInitialModel() will pick up the
|
|
603
|
+
// isDefault model; the vision extension can query isVisionDefault.
|
|
604
|
+
const settingsOverrides = {};
|
|
605
|
+
const defaultModel = allowedModels.find((m) => m.isDefault);
|
|
606
|
+
if (defaultModel) {
|
|
607
|
+
const proxyProvider = this.proxyProviderForAllowedModel(defaultModel);
|
|
608
|
+
settingsOverrides.defaultProvider = proxyProvider;
|
|
609
|
+
settingsOverrides.defaultModel = defaultModel.modelId;
|
|
610
|
+
console.info(`✓ Default model from backend: ${proxyProvider}/${defaultModel.modelId}`);
|
|
611
|
+
}
|
|
612
|
+
const defaultVisionModel = allowedModels.find((m) => m.isVisionDefault);
|
|
613
|
+
if (defaultVisionModel) {
|
|
614
|
+
const proxyProvider = this.proxyProviderForAllowedModel(defaultVisionModel);
|
|
615
|
+
settingsOverrides.defaultVisionProvider = proxyProvider;
|
|
616
|
+
settingsOverrides.defaultVisionModel = defaultVisionModel.modelId;
|
|
617
|
+
console.info(`✓ Default vision model from backend: ${proxyProvider}/${defaultVisionModel.modelId}`);
|
|
618
|
+
}
|
|
619
|
+
const settingsManager = SettingsManager.inMemory(settingsOverrides);
|
|
578
620
|
console.info(`✓ Inference routed via backend proxy (${allowedModels.length} model(s) available)`);
|
|
579
621
|
const result = await createAgentSession({
|
|
580
622
|
cwd: this.opts.cwd,
|
|
@@ -582,6 +624,7 @@ export class PiBridge {
|
|
|
582
624
|
sessionManager,
|
|
583
625
|
authStorage,
|
|
584
626
|
modelRegistry: this.modelRegistry,
|
|
627
|
+
settingsManager,
|
|
585
628
|
});
|
|
586
629
|
this.session = result.session;
|
|
587
630
|
// Headless UI context: forwards extension notify() calls as wire events
|
|
@@ -601,15 +644,38 @@ export class PiBridge {
|
|
|
601
644
|
// registration.
|
|
602
645
|
try {
|
|
603
646
|
await this.session.bindExtensions({ uiContext });
|
|
604
|
-
console.info("[
|
|
647
|
+
console.info("[AgentBridge] session_start emitted; extensions initialized.");
|
|
605
648
|
}
|
|
606
649
|
catch (err) {
|
|
607
650
|
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
-
console.warn(`[
|
|
651
|
+
console.warn(`[AgentBridge] session_start failed (extension init error): ${msg}`);
|
|
609
652
|
}
|
|
610
653
|
// Subscribe BEFORE any prompt fires.
|
|
611
654
|
this.unsubscribe = this.session.subscribe((ev) => this.handleEvent(ev));
|
|
612
655
|
}
|
|
656
|
+
proxyProviderForAllowedModel(model) {
|
|
657
|
+
if (model.provider === "anthropic") {
|
|
658
|
+
return SPECTRAL_PROXY_ANTHROPIC;
|
|
659
|
+
}
|
|
660
|
+
if (model.provider === "built-in") {
|
|
661
|
+
return SPECTRAL_PROXY_USER_MODEL;
|
|
662
|
+
}
|
|
663
|
+
return SPECTRAL_PROXY_OPENAI;
|
|
664
|
+
}
|
|
665
|
+
async refreshAllowedModels(opts) {
|
|
666
|
+
if (!this.modelRegistry) {
|
|
667
|
+
throw new Error("AgentBridge model registry unavailable");
|
|
668
|
+
}
|
|
669
|
+
const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
|
|
670
|
+
const allowedModels = await fetchModels({
|
|
671
|
+
backendUrl: this.opts.backendUrl,
|
|
672
|
+
machineJwt: this.opts.machineJwt,
|
|
673
|
+
bypassCache: opts?.bypassCache,
|
|
674
|
+
});
|
|
675
|
+
this.allowedModels = allowedModels;
|
|
676
|
+
this.registerSyntheticProviders(allowedModels);
|
|
677
|
+
return allowedModels;
|
|
678
|
+
}
|
|
613
679
|
/**
|
|
614
680
|
* Register one synthetic provider per upstream API shape. Anthropic models
|
|
615
681
|
* go to `${backendUrl}/v1/messages` (Messages API); everything else (OpenAI,
|
|
@@ -636,7 +702,6 @@ export class PiBridge {
|
|
|
636
702
|
authHeader: true,
|
|
637
703
|
api: "anthropic-messages",
|
|
638
704
|
models: anthropicModels.map((m) => {
|
|
639
|
-
const pricing = lookupPricing(m.modelId);
|
|
640
705
|
return {
|
|
641
706
|
id: m.modelId,
|
|
642
707
|
name: m.displayName,
|
|
@@ -649,12 +714,9 @@ export class PiBridge {
|
|
|
649
714
|
// at our synthetic proxy provider so auth resolves to the machine JWT.
|
|
650
715
|
provider: SPECTRAL_PROXY_ANTHROPIC,
|
|
651
716
|
baseUrl,
|
|
652
|
-
reasoning:
|
|
717
|
+
reasoning: m.supportsReasoning ?? false,
|
|
653
718
|
input: m.supportsImages !== false ? ["text", "image"] : ["text"],
|
|
654
|
-
|
|
655
|
-
cost: pricing
|
|
656
|
-
? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
|
|
657
|
-
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
719
|
+
cost: getModelCost(m),
|
|
658
720
|
contextWindow: m.contextWindow ?? 0,
|
|
659
721
|
maxTokens: 0,
|
|
660
722
|
};
|
|
@@ -668,7 +730,6 @@ export class PiBridge {
|
|
|
668
730
|
authHeader: true,
|
|
669
731
|
api: "openai-completions",
|
|
670
732
|
models: openaiCompatModels.map((m) => {
|
|
671
|
-
const pricing = lookupPricing(m.modelId);
|
|
672
733
|
return {
|
|
673
734
|
id: m.modelId,
|
|
674
735
|
name: m.displayName,
|
|
@@ -679,14 +740,12 @@ export class PiBridge {
|
|
|
679
740
|
// breaking auth lookup against our synthetic proxy provider.
|
|
680
741
|
provider: SPECTRAL_PROXY_OPENAI,
|
|
681
742
|
baseUrl,
|
|
682
|
-
reasoning:
|
|
743
|
+
reasoning: m.supportsReasoning ?? false,
|
|
683
744
|
input: m.supportsImages !== false ? ["text", "image"] : ["text"],
|
|
684
|
-
|
|
685
|
-
cost: pricing
|
|
686
|
-
? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
|
|
687
|
-
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
745
|
+
cost: getModelCost(m),
|
|
688
746
|
contextWindow: m.contextWindow ?? 0,
|
|
689
747
|
maxTokens: 0,
|
|
748
|
+
compat: inferSyntheticOpenAICompat(m),
|
|
690
749
|
};
|
|
691
750
|
}),
|
|
692
751
|
});
|
|
@@ -703,20 +762,18 @@ export class PiBridge {
|
|
|
703
762
|
authHeader: true,
|
|
704
763
|
api: "openai-completions",
|
|
705
764
|
models: userModelEntries.map((m) => {
|
|
706
|
-
const pricing = lookupPricing(m.modelId);
|
|
707
765
|
return {
|
|
708
766
|
id: m.modelId,
|
|
709
767
|
name: m.displayName,
|
|
710
768
|
api: "openai-completions",
|
|
711
769
|
provider: SPECTRAL_PROXY_USER_MODEL,
|
|
712
770
|
baseUrl,
|
|
713
|
-
reasoning:
|
|
771
|
+
reasoning: m.supportsReasoning ?? false,
|
|
714
772
|
input: m.supportsImages !== false ? ["text", "image"] : ["text"],
|
|
715
|
-
cost:
|
|
716
|
-
? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
|
|
717
|
-
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
773
|
+
cost: getModelCost(m),
|
|
718
774
|
contextWindow: m.contextWindow ?? 0,
|
|
719
775
|
maxTokens: 0,
|
|
776
|
+
compat: inferSyntheticOpenAICompat(m),
|
|
720
777
|
};
|
|
721
778
|
}),
|
|
722
779
|
});
|
|
@@ -794,7 +851,7 @@ export class PiBridge {
|
|
|
794
851
|
if (!modelId)
|
|
795
852
|
return true; // nothing to apply — pi keeps its current model
|
|
796
853
|
if (!this.session)
|
|
797
|
-
throw new Error("
|
|
854
|
+
throw new Error("AgentBridge.start() not called");
|
|
798
855
|
if (this.lastAppliedModelId === modelId)
|
|
799
856
|
return true; // idempotent: same model already in effect
|
|
800
857
|
if (!this.modelRegistry) {
|
|
@@ -807,13 +864,26 @@ export class PiBridge {
|
|
|
807
864
|
});
|
|
808
865
|
return false;
|
|
809
866
|
}
|
|
810
|
-
|
|
867
|
+
let model = this.modelRegistry
|
|
811
868
|
.getAvailable()
|
|
812
869
|
.find((m) => m.id === modelId);
|
|
870
|
+
let refreshError;
|
|
871
|
+
if (!model) {
|
|
872
|
+
try {
|
|
873
|
+
await this.refreshAllowedModels({ bypassCache: true });
|
|
874
|
+
model = this.modelRegistry
|
|
875
|
+
.getAvailable()
|
|
876
|
+
.find((m) => m.id === modelId);
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
refreshError = err instanceof Error ? err : new Error(String(err));
|
|
880
|
+
this.opts.onError?.(refreshError);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
813
883
|
if (!model) {
|
|
814
884
|
this.opts.emit({
|
|
815
885
|
type: "error",
|
|
816
|
-
message: `Unknown modelId "${modelId}" — not found in pi model registry`,
|
|
886
|
+
message: `Unknown modelId "${modelId}" — not found in pi model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
|
|
817
887
|
});
|
|
818
888
|
return false;
|
|
819
889
|
}
|
|
@@ -915,16 +985,14 @@ export class PiBridge {
|
|
|
915
985
|
*/
|
|
916
986
|
async prompt(text, images) {
|
|
917
987
|
if (!this.session)
|
|
918
|
-
throw new Error("
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
// and
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
: undefined;
|
|
925
|
-
const modelSupportsImages = currentModel?.supportsImages === true;
|
|
988
|
+
throw new Error("AgentBridge.start() not called");
|
|
989
|
+
// Always pass images through to the session.
|
|
990
|
+
// The spectral-vision-fallback extension intercepts images via the
|
|
991
|
+
// `context` event and replaces them with text descriptions using a
|
|
992
|
+
// vision-capable model BEFORE they reach the LLM. This means images
|
|
993
|
+
// work regardless of whether the main model supports them.
|
|
926
994
|
try {
|
|
927
|
-
if (images && images.length > 0
|
|
995
|
+
if (images && images.length > 0) {
|
|
928
996
|
const imageContents = images.map((img) => ({
|
|
929
997
|
type: "image",
|
|
930
998
|
data: img.data,
|
|
@@ -932,20 +1000,6 @@ export class PiBridge {
|
|
|
932
1000
|
}));
|
|
933
1001
|
await this.session.prompt(text, { images: imageContents });
|
|
934
1002
|
}
|
|
935
|
-
else if (images && images.length > 0 && !modelSupportsImages) {
|
|
936
|
-
// Model doesn't support images — convert them to text descriptions
|
|
937
|
-
// so the conversation can continue instead of hanging.
|
|
938
|
-
const imageDescriptions = images
|
|
939
|
-
.map((img, i) => `[Image ${i + 1}: ${img.mimeType}, ${img.data.length.toLocaleString()} bytes base64]`)
|
|
940
|
-
.join("\n");
|
|
941
|
-
const augmentedText = `${text}\n\n---\nThe following image(s) were attached but the current model does not support image input:\n${imageDescriptions}\n(Describe what you see or ask the user to switch to a model that supports images.)`;
|
|
942
|
-
this.opts.emit({
|
|
943
|
-
type: "agent_notification",
|
|
944
|
-
message: `The current model does not support image input. ${images.length} image(s) were converted to text descriptions.`,
|
|
945
|
-
level: "warning",
|
|
946
|
-
});
|
|
947
|
-
await this.session.prompt(augmentedText);
|
|
948
|
-
}
|
|
949
1003
|
else {
|
|
950
1004
|
await this.session.prompt(text);
|
|
951
1005
|
}
|
|
@@ -967,7 +1021,7 @@ export class PiBridge {
|
|
|
967
1021
|
*/
|
|
968
1022
|
async compact(customInstructions) {
|
|
969
1023
|
if (!this.session)
|
|
970
|
-
throw new Error("
|
|
1024
|
+
throw new Error("AgentBridge.start() not called");
|
|
971
1025
|
this.memoryPhase = "compacting";
|
|
972
1026
|
try {
|
|
973
1027
|
await this.session.compact(customInstructions);
|
|
@@ -1056,13 +1110,13 @@ export class PiBridge {
|
|
|
1056
1110
|
e.type === "tool_call" ||
|
|
1057
1111
|
e.type === "tool_result");
|
|
1058
1112
|
if (!finalContent && !hasMeaningfulEvent) {
|
|
1059
|
-
console.debug("[
|
|
1113
|
+
console.debug("[agent-bridge] skipping empty intermediate message");
|
|
1060
1114
|
try {
|
|
1061
1115
|
this.opts.onAssistantMessageSkipped?.(messageId);
|
|
1062
1116
|
}
|
|
1063
1117
|
catch (err) {
|
|
1064
1118
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
1065
|
-
console.error(`[
|
|
1119
|
+
console.error(`[agent-bridge] onAssistantMessageSkipped failed: ${e.message}`);
|
|
1066
1120
|
}
|
|
1067
1121
|
return;
|
|
1068
1122
|
}
|
|
@@ -1177,6 +1231,8 @@ export class PiBridge {
|
|
|
1177
1231
|
const done = details.results.filter(r => r.exitCode !== undefined && r.exitCode !== -1).length;
|
|
1178
1232
|
const running = total - done;
|
|
1179
1233
|
const currentAgent = details.results.find(r => r.exitCode === -1 || r.exitCode === undefined)?.agent;
|
|
1234
|
+
// Extract streaming text from partialResult content (set by text_delta forwarding).
|
|
1235
|
+
const streamingText = extractTextFromContent(partialResult?.content);
|
|
1180
1236
|
const se = {
|
|
1181
1237
|
type: "subagent_progress",
|
|
1182
1238
|
toolCallId: ev.toolCallId,
|
|
@@ -1190,6 +1246,7 @@ export class PiBridge {
|
|
|
1190
1246
|
: mode === "chain"
|
|
1191
1247
|
? `Chain: step ${details.results.length}/${total}`
|
|
1192
1248
|
: `Running ${currentAgent ?? "subagent"}...`,
|
|
1249
|
+
streamingText: mode === "single" ? streamingText : undefined,
|
|
1193
1250
|
};
|
|
1194
1251
|
try {
|
|
1195
1252
|
this.opts.emit(se);
|
|
@@ -1387,6 +1444,15 @@ function detectMemorySystem(message) {
|
|
|
1387
1444
|
// Keep generic observational-memory messages visible in the landing badge.
|
|
1388
1445
|
return "memory_observer";
|
|
1389
1446
|
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Detect subsystem from notification messages by known prefix.
|
|
1449
|
+
* Returns undefined for generic extension messages without a matching prefix.
|
|
1450
|
+
*/
|
|
1451
|
+
function detectNotifSystem(message) {
|
|
1452
|
+
if (message.startsWith("[spectral-vision]"))
|
|
1453
|
+
return "vision";
|
|
1454
|
+
return detectMemorySystem(message);
|
|
1455
|
+
}
|
|
1390
1456
|
/**
|
|
1391
1457
|
* Create a minimal ExtensionUIContext that forwards `notify()` calls as
|
|
1392
1458
|
* `agent_notification` wire events. All other UI methods are no-ops —
|
|
@@ -1397,15 +1463,18 @@ function detectMemorySystem(message) {
|
|
|
1397
1463
|
function createHeadlessUIContext(emit) {
|
|
1398
1464
|
// Defer to a Proxy so we don't need to stub every method.
|
|
1399
1465
|
// `notify` is the only method called by extensions in serve mode
|
|
1400
|
-
// (observational memory, MCP status bar updates).
|
|
1466
|
+
// (observational memory, MCP status bar updates, spectral-vision).
|
|
1401
1467
|
const handler = {
|
|
1402
1468
|
get(_target, prop) {
|
|
1403
1469
|
if (prop === "notify") {
|
|
1404
1470
|
return (message, type) => {
|
|
1405
1471
|
const level = type ?? "info";
|
|
1406
|
-
const system =
|
|
1472
|
+
const system = detectNotifSystem(message);
|
|
1407
1473
|
if (system?.startsWith("memory_")) {
|
|
1408
|
-
console.info(`[
|
|
1474
|
+
console.info(`[AgentBridge][memory][${level}] ${message}`);
|
|
1475
|
+
}
|
|
1476
|
+
else if (system === "vision") {
|
|
1477
|
+
console.info(`[AgentBridge][vision][${level}] ${message}`);
|
|
1409
1478
|
}
|
|
1410
1479
|
emit({
|
|
1411
1480
|
type: "agent_notification",
|
|
@@ -65,6 +65,27 @@ export async function handleCompactSession(store, manager, id) {
|
|
|
65
65
|
}
|
|
66
66
|
return { ok: true };
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Remember & delete: compact the session (which persists observations as
|
|
70
|
+
* project memory via the compaction hook), then delete the session.
|
|
71
|
+
*
|
|
72
|
+
* This gives the user a way to keep a session's reflections as durable
|
|
73
|
+
* cross-session memory even for short sessions that never hit the compaction
|
|
74
|
+
* threshold naturally.
|
|
75
|
+
*/
|
|
76
|
+
export async function handleRememberAndDeleteSession(store, manager, id) {
|
|
77
|
+
const detail = store.getSession(id);
|
|
78
|
+
if (!detail)
|
|
79
|
+
throw new NotFoundError("Session not found");
|
|
80
|
+
try {
|
|
81
|
+
await manager.compactSession(id);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
throw new BadRequestError(message);
|
|
86
|
+
}
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
68
89
|
/**
|
|
69
90
|
* Fork a session: create a new session copying all messages from the
|
|
70
91
|
* source, with the `fork_compact_source_id` flag set so the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session-scoped streaming layer for `spectral serve`.
|
|
3
3
|
*
|
|
4
|
-
* Background: prior to this module each WebSocket owned its own `
|
|
4
|
+
* Background: prior to this module each WebSocket owned its own `AgentBridge`
|
|
5
5
|
* instance and the routes layer enforced single-writer-wins (4001 eviction)
|
|
6
6
|
* to keep that bridge unique per session. That model lost data on browser
|
|
7
7
|
* refresh — the WS close torn down the pi process mid-stream, and a re-open
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
* for now — streams accumulate for the lifetime of the server process.
|
|
38
38
|
*/
|
|
39
39
|
import { randomUUID } from "node:crypto";
|
|
40
|
-
import {
|
|
40
|
+
import { AgentBridge } from "./agent-bridge.js";
|
|
41
41
|
import { getMemoryState, isSourceEntry, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
|
|
42
42
|
import { observationPoolTokens, renderSummary } from "../memory/compaction.js";
|
|
43
43
|
import { loadConfig } from "../memory/config.js";
|
|
44
44
|
import { setProjectObsStore } from "../memory/project-observations-store.js";
|
|
45
45
|
import { estimateStringTokens } from "../memory/tokens.js";
|
|
46
46
|
import { reflectionContent, reflectionId } from "../memory/types.js";
|
|
47
|
-
const DEFAULT_BRIDGE_FACTORY = (args) => new
|
|
47
|
+
const DEFAULT_BRIDGE_FACTORY = (args) => new AgentBridge(args);
|
|
48
48
|
/** Safety limit for autonomous loop iterations per session. */
|
|
49
49
|
const MAX_LOOP_ITERATIONS = 100;
|
|
50
50
|
/**
|
|
@@ -646,7 +646,7 @@ export class SessionStreamManager {
|
|
|
646
646
|
assistantText: "",
|
|
647
647
|
};
|
|
648
648
|
// 4. Fire pi. `prompt` resolves on agent_end; errors are handled inside
|
|
649
|
-
//
|
|
649
|
+
// AgentBridge (it emits `error` for us). We don't await — broadcast is
|
|
650
650
|
// driven by the bridge's emit callback.
|
|
651
651
|
void stream.bridge.prompt(content, images);
|
|
652
652
|
}
|
|
@@ -1127,7 +1127,7 @@ export class SessionStreamManager {
|
|
|
1127
1127
|
}
|
|
1128
1128
|
// Broadcast first, then maybe close out the turn. agent_end clears the
|
|
1129
1129
|
// buffer because by that point the assistant message is already in
|
|
1130
|
-
// SQLite (
|
|
1130
|
+
// SQLite (AgentBridge calls onAssistantMessageComplete on message_end,
|
|
1131
1131
|
// which fires before agent_end).
|
|
1132
1132
|
//
|
|
1133
1133
|
// Track context window state from token_usage events — the bridge emits
|
|
@@ -1173,6 +1173,10 @@ export class SessionStreamManager {
|
|
|
1173
1173
|
prunePersistedHistoryAfterCompaction(this.store, stream.sessionId, stream.bridge);
|
|
1174
1174
|
}
|
|
1175
1175
|
persistObservationalMemorySnapshot(this.store, stream.sessionId, stream.bridge);
|
|
1176
|
+
// Drain the prompt queue after compaction finishes. During
|
|
1177
|
+
// compaction, auto-dequeue in agent_end is skipped to prevent the
|
|
1178
|
+
// lossy prompt() guard from dropping queued items.
|
|
1179
|
+
this.maybeAutoDequeue(stream);
|
|
1176
1180
|
// After compaction the session context has been reduced; push updated
|
|
1177
1181
|
// context-window stats to all subscribers so the frontend's context
|
|
1178
1182
|
// bar refreshes immediately instead of waiting for the next turn.
|
|
@@ -1287,8 +1291,10 @@ export class SessionStreamManager {
|
|
|
1287
1291
|
// pending, check the persistent prompt queue. If there's a queued
|
|
1288
1292
|
// prompt, start it immediately without broadcasting agent_end —
|
|
1289
1293
|
// the frontend transitions seamlessly to the next turn.
|
|
1294
|
+
// Skip if compaction is in-flight (e.g. fork-compact just started
|
|
1295
|
+
// above) — compaction_end will drain the queue when it finishes.
|
|
1290
1296
|
if (!stream.loopActive || !stream.loopOriginalPrompt) {
|
|
1291
|
-
if (this.maybeAutoDequeue(stream))
|
|
1297
|
+
if (!stream.compacting && this.maybeAutoDequeue(stream))
|
|
1292
1298
|
return;
|
|
1293
1299
|
}
|
|
1294
1300
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexol/spectral",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "AI coding agent for Aexol with relay-based browser access.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"coding-agent",
|
|
31
31
|
"ai",
|
|
32
32
|
"cli",
|
|
33
|
-
"pi",
|
|
34
33
|
"claude",
|
|
35
34
|
"openai",
|
|
36
35
|
"agent",
|
|
@@ -46,6 +45,10 @@
|
|
|
46
45
|
"url": "https://gitlab.aexol.com/aexol/spectral/-/issues"
|
|
47
46
|
},
|
|
48
47
|
"license": "MIT",
|
|
48
|
+
"spectralConfig": {
|
|
49
|
+
"name": "spectral",
|
|
50
|
+
"configDir": ".spectral"
|
|
51
|
+
},
|
|
49
52
|
"publishConfig": {
|
|
50
53
|
"access": "public"
|
|
51
54
|
},
|