chore: remove unused code paths
This commit is contained in:
parent
062e8e3c9f
commit
0c2e5ee256
13 changed files with 2 additions and 854 deletions
|
|
@ -169,7 +169,6 @@
|
|||
| types.ts | AI Providers | Core types for models, APIs, streaming options |
|
||||
| env-api-keys.ts | AI Providers, Auth/OAuth | Environment variable API key resolution |
|
||||
| web-runtime-env-api-keys.ts | AI Providers, Auth/OAuth | Web runtime API key handling |
|
||||
| web-runtime-oauth.ts | AI Providers, Auth/OAuth | Web runtime OAuth token management |
|
||||
| providers/register-builtins.ts | AI Providers | Registration of built-in provider implementations |
|
||||
| providers/anthropic.ts | AI Providers | Anthropic API provider |
|
||||
| providers/anthropic-shared.ts | AI Providers | Shared utilities for Anthropic provider variants |
|
||||
|
|
@ -192,7 +191,6 @@
|
|||
| providers/transform-messages.ts | AI Providers | Message transformation for provider compatibility |
|
||||
| utils/oauth/index.ts | Auth/OAuth | OAuth utilities export hub |
|
||||
| utils/oauth/types.ts | Auth/OAuth | OAuth credential and prompt types |
|
||||
| utils/oauth/pkce.ts | Auth/OAuth | PKCE flow implementation |
|
||||
| utils/oauth/github-copilot.ts | Auth/OAuth | GitHub Copilot OAuth flow |
|
||||
| utils/oauth/google-oauth-utils.ts | Auth/OAuth | Shared Google OAuth utilities |
|
||||
| utils/oauth/google-gemini-cli.ts | Auth/OAuth | Google Gemini CLI OAuth flow |
|
||||
|
|
@ -373,10 +371,8 @@
|
|||
| core/discovery-cache.ts | Model System | Model discovery result caching |
|
||||
| core/keybindings.ts | TUI Components | Keybinding definitions |
|
||||
| core/footer-data-provider.ts | TUI Components | Footer information provider |
|
||||
| core/index.ts | Agent Core | Core module exports |
|
||||
| index.ts | Agent Core | Package exports |
|
||||
| utils/clipboard.ts | Tool System | Clipboard read/write |
|
||||
| utils/clipboard-native.ts | Tool System | Native clipboard implementation |
|
||||
| utils/clipboard-image.ts | Tool System | Clipboard image support |
|
||||
| utils/error.ts | Agent Core | Error message extraction/formatting |
|
||||
| utils/frontmatter.ts | Config | YAML frontmatter parsing |
|
||||
|
|
@ -385,7 +381,6 @@
|
|||
| utils/image-resize.ts | Image Processing | Image resizing and optimization |
|
||||
| utils/mime.ts | Tool System | MIME type detection |
|
||||
| utils/path-display.ts | TUI Components | Path formatting for display |
|
||||
| utils/photon.ts | Agent Core | Photon scripting runtime support |
|
||||
| utils/shell.ts | Tool System | Shell detection and execution |
|
||||
| utils/changelog.ts | CLI | Changelog parsing |
|
||||
| utils/sleep.ts | Agent Core | Async sleep/delay utility |
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"**/*.d.ts",
|
||||
"packages/coding-agent/src/core/export-html/**",
|
||||
"packages/coding-agent/src/resources/extensions/**",
|
||||
"packages/daemon/src/cli-dev.ts",
|
||||
"scripts/tmp-check-test-imports/**",
|
||||
"src/resources/extensions/**/dist/**",
|
||||
"src/resources/extensions/**",
|
||||
|
|
@ -28,7 +29,6 @@
|
|||
"@mariozechner/jiti",
|
||||
"@mistralai/mistralai",
|
||||
"@octokit/rest",
|
||||
"@silvia-odwyer/photon-node",
|
||||
"@smithy/node-http-handler",
|
||||
"@types/diff",
|
||||
"@types/express",
|
||||
|
|
@ -79,6 +79,7 @@
|
|||
"src/headless.ts",
|
||||
"src/headless*.ts",
|
||||
"src/web-mode.ts",
|
||||
"packages/daemon/src/cli-dev.ts",
|
||||
"scripts/**/*.{js,cjs,mjs,ts}",
|
||||
"tests/**/*.{js,cjs,mjs,ts}",
|
||||
"src/tests/**/*.{js,cjs,mjs,ts}",
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -27,7 +27,6 @@
|
|||
"@mistralai/mistralai": "^2.2.1",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@smithy/node-http-handler": "^4.7.3",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
|
|
@ -5886,12 +5885,6 @@
|
|||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/@silvia-odwyer/photon-node": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz",
|
||||
"integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@simple-git/args-pathspec": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz",
|
||||
|
|
@ -15324,7 +15317,6 @@
|
|||
"version": "2.75.4",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"diff": "^9.0.0",
|
||||
"express": "^5.2.1",
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@
|
|||
"@mistralai/mistralai": "^2.2.1",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@smithy/node-http-handler": "^4.7.3",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* PKCE utilities using Web Crypto API.
|
||||
* Works in both Node.js 20+ and browsers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Encode bytes as base64url string.
|
||||
*/
|
||||
function base64urlEncode(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (const byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PKCE code verifier and challenge.
|
||||
* Uses Web Crypto API for cross-platform compatibility.
|
||||
*/
|
||||
export async function generatePKCE(): Promise<{
|
||||
verifier: string;
|
||||
challenge: string;
|
||||
}> {
|
||||
// Generate random verifier
|
||||
const verifierBytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(verifierBytes);
|
||||
const verifier = base64urlEncode(verifierBytes);
|
||||
|
||||
// Compute SHA-256 challenge
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(verifier);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const challenge = base64urlEncode(new Uint8Array(hashBuffer));
|
||||
|
||||
return { verifier, challenge };
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export {
|
||||
getOAuthProvider,
|
||||
getOAuthProviders,
|
||||
type OAuthAuthInfo,
|
||||
type OAuthCredentials,
|
||||
type OAuthLoginCallbacks,
|
||||
type OAuthPrompt,
|
||||
type OAuthProviderInterface,
|
||||
} from "./oauth.js";
|
||||
|
|
@ -21,7 +21,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"diff": "^9.0.0",
|
||||
"express": "^5.2.1",
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
* Core modules shared between all run modes.
|
||||
*/
|
||||
|
||||
export {
|
||||
AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentSessionEvent,
|
||||
type AgentSessionEventListener,
|
||||
type ModelCycleResult,
|
||||
type PromptOptions,
|
||||
type SessionStats,
|
||||
} from "./agent-session.js";
|
||||
export {
|
||||
type BashExecutorOptions,
|
||||
type BashResult,
|
||||
executeBash,
|
||||
executeBashWithOperations,
|
||||
} from "./bash-executor.js";
|
||||
export type { CompactionResult } from "./compaction/index.js";
|
||||
export { ContextualTips, type TipContext } from "./contextual-tips.js";
|
||||
export {
|
||||
createEventBus,
|
||||
type EventBus,
|
||||
type EventBusController,
|
||||
} from "./event-bus.js";
|
||||
|
||||
// Extensions system
|
||||
export {
|
||||
type AgentEndEvent,
|
||||
type AgentStartEvent,
|
||||
type AgentToolResult,
|
||||
type AgentToolUpdateCallback,
|
||||
type BeforeAgentStartEvent,
|
||||
type ContextEvent,
|
||||
discoverAndLoadExtensions,
|
||||
type ExecOptions,
|
||||
type ExecResult,
|
||||
type Extension,
|
||||
type ExtensionAPI,
|
||||
type ExtensionCommandContext,
|
||||
type ExtensionContext,
|
||||
type ExtensionError,
|
||||
type ExtensionEvent,
|
||||
type ExtensionFactory,
|
||||
type ExtensionFlag,
|
||||
type ExtensionHandler,
|
||||
type ExtensionManifest,
|
||||
ExtensionRunner,
|
||||
type ExtensionShortcut,
|
||||
type ExtensionUIContext,
|
||||
type LoadExtensionsResult,
|
||||
type MessageRenderer,
|
||||
type RegisteredCommand,
|
||||
readManifest,
|
||||
readManifestFromEntryPath,
|
||||
type SessionBeforeCompactEvent,
|
||||
type SessionBeforeForkEvent,
|
||||
type SessionBeforeSwitchEvent,
|
||||
type SessionBeforeTreeEvent,
|
||||
type SessionCompactEvent,
|
||||
type SessionForkEvent,
|
||||
type SessionShutdownEvent,
|
||||
type SessionStartEvent,
|
||||
type SessionSwitchEvent,
|
||||
type SessionTreeEvent,
|
||||
type SortResult,
|
||||
type SortWarning,
|
||||
sortExtensionPaths,
|
||||
type ToolCallEvent,
|
||||
type ToolDefinition,
|
||||
type ToolRenderResultOptions,
|
||||
type ToolResultEvent,
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
wrapToolsWithExtensions,
|
||||
} from "./extensions/index.js";
|
||||
export { FallbackResolver, type FallbackResult } from "./fallback-resolver.js";
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/**
|
||||
* Re-export native clipboard utilities from "@singularity-forge/native.
|
||||
*
|
||||
* This module exists for backward compatibility. Prefer importing
|
||||
* directly from "@singularity-forge/native/clipboard" in new code.
|
||||
*/
|
||||
export {
|
||||
copyToClipboard,
|
||||
readImageFromClipboard,
|
||||
readTextFromClipboard,
|
||||
} from "@singularity-forge/native/clipboard";
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// Re-export types from the main package
|
||||
export type { PhotonImage as PhotonImageType } from "@silvia-odwyer/photon-node";
|
||||
|
|
@ -1,313 +0,0 @@
|
|||
import type { Server } from "node:http";
|
||||
import {
|
||||
type Context,
|
||||
getModels,
|
||||
type StreamOptions,
|
||||
stream,
|
||||
} from "@singularity-forge/ai";
|
||||
import express from "express";
|
||||
import type { AuthStorage } from "../core/auth-storage.js";
|
||||
import type { ModelRegistry } from "../core/model-registry.js";
|
||||
|
||||
export type ProxyServerOptions = {
|
||||
port: number;
|
||||
authStorage: AuthStorage;
|
||||
modelRegistry: ModelRegistry;
|
||||
/** Per-family provider priority overrides from settings.proxy.providerPriority */
|
||||
priorityOverrides?: Record<string, string[]>;
|
||||
onLog?: (msg: string) => void;
|
||||
};
|
||||
|
||||
// Per-family provider priority for bare model ID resolution. When the same model ID
|
||||
// exists across multiple providers, the first matching family rule wins; within that
|
||||
// rule providers are tried in order, preferring those with auth configured. Providers
|
||||
// not listed in any rule fall back to insertion order.
|
||||
const PROXY_FAMILY_PRIORITY: Array<{ match: RegExp; providers: string[] }> = [
|
||||
// MiniMax: international direct > CN endpoint
|
||||
{ match: /^MiniMax-/i, providers: ["minimax", "minimax-cn"] },
|
||||
// GLM: zai is the canonical direct provider > opencode aggregators
|
||||
{ match: /^glm-/i, providers: ["zai", "opencode", "opencode-go"] },
|
||||
// Kimi: kimi-coding direct > opencode aggregators
|
||||
{ match: /^kimi-/i, providers: ["kimi-coding", "opencode", "opencode-go"] },
|
||||
// Gemini/Gemma: proxy bare model IDs through cli-core only.
|
||||
{
|
||||
match: /^gemini-|^gemma-/i,
|
||||
providers: ["google-gemini-cli"],
|
||||
},
|
||||
// Claude: anthropic direct > opencode. Copilot is disabled.
|
||||
{
|
||||
match: /^claude-/i,
|
||||
providers: ["anthropic", "opencode"],
|
||||
},
|
||||
// GPT/OpenAI: openai direct > azure. Copilot is disabled.
|
||||
{
|
||||
match: /^gpt-|^o[0-9]|^codex-/i,
|
||||
providers: ["openai", "azure-openai-responses"],
|
||||
},
|
||||
];
|
||||
|
||||
function _sortByFamilyPriority<T extends { id: string; provider: string }>(
|
||||
models: T[],
|
||||
): T[] {
|
||||
if (models.length <= 1) return models;
|
||||
const [first] = models;
|
||||
const rule = PROXY_FAMILY_PRIORITY.find((r) => r.match.test(first.id));
|
||||
const order = rule?.providers ?? [];
|
||||
return [...models].sort((a, b) => {
|
||||
const pa = order.indexOf(a.provider);
|
||||
const pb = order.indexOf(b.provider);
|
||||
return (pa === -1 ? Infinity : pa) - (pb === -1 ? Infinity : pb);
|
||||
});
|
||||
}
|
||||
|
||||
export class ProxyServer {
|
||||
private server: Server | null = null;
|
||||
|
||||
constructor(private options: ProxyServerOptions) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.server) return;
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const { authStorage, modelRegistry, onLog } = this.options;
|
||||
const priorityOverrides = this.options.priorityOverrides ?? {};
|
||||
|
||||
const log = (msg: string) => onLog?.(msg);
|
||||
|
||||
// 1. Model Listing
|
||||
app.get(["/v1/models", "/v1beta/models"], async (req, res) => {
|
||||
const providers = ["google-gemini-cli", "anthropic", "openai"];
|
||||
const allModels = providers.flatMap((p) => getModels(p as any));
|
||||
|
||||
const formatted = allModels.map((m) => ({
|
||||
id: m.id,
|
||||
object: "model",
|
||||
created: 1677610602,
|
||||
owned_by: m.provider,
|
||||
name: m.name,
|
||||
capabilities: m.capabilities,
|
||||
}));
|
||||
|
||||
if (req.path.startsWith("/v1beta")) {
|
||||
res.json({ models: formatted });
|
||||
} else {
|
||||
res.json({ data: formatted, object: "list" });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Chat Completions (OpenAI & GenAI)
|
||||
const handleChat = async (req: express.Request, res: express.Response) => {
|
||||
const body = req.body;
|
||||
const isOpenAi = req.path.includes("/v1/chat/completions");
|
||||
const modelId = isOpenAi
|
||||
? body.model
|
||||
: req.params.modelId?.replace(/:streamGenerateContent$/, "");
|
||||
|
||||
if (!modelId) {
|
||||
return res.status(400).json({ error: "Model ID is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const candidates = modelRegistry.getModelsForProxy(
|
||||
modelId,
|
||||
priorityOverrides,
|
||||
);
|
||||
if (candidates.length === 0) {
|
||||
return res.status(404).json({ error: `Model ${modelId} not found` });
|
||||
}
|
||||
|
||||
// Normalize messages once — shared across retry attempts
|
||||
const context: Context = isOpenAi
|
||||
? this.normalizeOpenAi(body)
|
||||
: this.normalizeGoogle(body);
|
||||
|
||||
const streamOptions: StreamOptions = {
|
||||
temperature: body.temperature,
|
||||
maxTokens: isOpenAi
|
||||
? body.max_tokens
|
||||
: body.generationConfig?.maxOutputTokens,
|
||||
};
|
||||
|
||||
for (const resolvedModel of candidates) {
|
||||
const apiKey = await authStorage.getApiKey(resolvedModel.provider);
|
||||
if (!apiKey) continue; // no credentials — try next
|
||||
|
||||
const streamOptionsWithKey: StreamOptions = {
|
||||
...streamOptions,
|
||||
apiKey,
|
||||
};
|
||||
|
||||
try {
|
||||
const eventStream = stream(
|
||||
resolvedModel as any,
|
||||
context,
|
||||
streamOptionsWithKey as any,
|
||||
);
|
||||
|
||||
if (body.stream) {
|
||||
this.handleStreamingResponse(eventStream, res, isOpenAi, modelId);
|
||||
} else {
|
||||
await this.handleStaticResponse(
|
||||
eventStream,
|
||||
res,
|
||||
isOpenAi,
|
||||
modelId,
|
||||
);
|
||||
}
|
||||
return; // success
|
||||
} catch (err: any) {
|
||||
const status = err?.status ?? err?.statusCode;
|
||||
if (status === 429) {
|
||||
log(
|
||||
`Provider ${resolvedModel.provider} rate-limited (429), trying next candidate`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// All candidates exhausted
|
||||
res
|
||||
.status(429)
|
||||
.json({ error: `All providers rate-limited for model ${modelId}` });
|
||||
} catch (err: any) {
|
||||
log(`Proxy error: ${err.message}`);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
app.post("/v1/chat/completions", handleChat);
|
||||
app.post("/v1beta/models/:modelId\\:streamGenerateContent", handleChat);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.server = app.listen(this.options.port, () => {
|
||||
log(`Proxy Server running on http://localhost:${this.options.port}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeOpenAi(body: any): Context {
|
||||
const messages = body.messages || [];
|
||||
const system = messages.find((m: any) => m.role === "system")?.content;
|
||||
const history = messages
|
||||
.filter((m: any) => m.role !== "system")
|
||||
.map((m: any) => ({
|
||||
role: m.role === "user" ? "user" : "assistant",
|
||||
content:
|
||||
typeof m.content === "string"
|
||||
? [{ type: "text", text: m.content }]
|
||||
: m.content,
|
||||
}));
|
||||
return { messages: history, systemPrompt: system };
|
||||
}
|
||||
|
||||
private normalizeGoogle(body: any): Context {
|
||||
const contents = body.contents || [];
|
||||
const history = contents.map((c: any) => ({
|
||||
role: c.role === "user" ? "user" : "assistant",
|
||||
content: (c.parts || []).map((p: any) => ({
|
||||
type: "text",
|
||||
text: p.text,
|
||||
})),
|
||||
}));
|
||||
const system = body.systemInstruction?.parts?.[0]?.text;
|
||||
return { messages: history, systemPrompt: system };
|
||||
}
|
||||
|
||||
private handleStreamingResponse(
|
||||
eventStream: any,
|
||||
res: express.Response,
|
||||
isOpenAi: boolean,
|
||||
modelId: string,
|
||||
) {
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
isOpenAi ? "text/event-stream" : "application/json",
|
||||
);
|
||||
|
||||
eventStream.on("data", (ev: any) => {
|
||||
if (ev.type === "text_delta") {
|
||||
if (isOpenAi) {
|
||||
const chunk = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [
|
||||
{ index: 0, delta: { content: ev.delta }, finish_reason: null },
|
||||
],
|
||||
};
|
||||
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||
} else {
|
||||
const chunk = {
|
||||
candidates: [{ content: { parts: [{ text: ev.delta }] } }],
|
||||
};
|
||||
res.write(JSON.stringify(chunk) + "\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eventStream.on("done", () => {
|
||||
if (isOpenAi) res.write("data: [DONE]\n\n");
|
||||
res.end();
|
||||
});
|
||||
|
||||
eventStream.on("error", (ev: any) => {
|
||||
if (!res.headersSent)
|
||||
res.status(500).json({ error: ev.error.errorMessage });
|
||||
else res.end();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleStaticResponse(
|
||||
eventStream: any,
|
||||
res: express.Response,
|
||||
isOpenAi: boolean,
|
||||
modelId: string,
|
||||
) {
|
||||
let fullContent = "";
|
||||
eventStream.on("data", (ev: any) => {
|
||||
if (ev.type === "text_delta") fullContent += ev.delta;
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
eventStream.on("done", () => {
|
||||
if (isOpenAi) {
|
||||
res.json({
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: fullContent },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
candidates: [{ content: { parts: [{ text: fullContent }] } }],
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
eventStream.on("error", (ev: any) => {
|
||||
res.status(500).json({ error: ev.error.errorMessage });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/** Result of a process tree kill operation. */
|
||||
export interface KillTreeResult {
|
||||
/** Number of processes successfully killed. */
|
||||
killed: number;
|
||||
}
|
||||
383
src/traces.ts
383
src/traces.ts
|
|
@ -1,383 +0,0 @@
|
|||
/**
|
||||
* traces.ts — Structured trace data model and export utilities for autonomous mode execution.
|
||||
*
|
||||
* Purpose: provide a lightweight, hierarchical span model that captures the
|
||||
* full lifecycle of an autonomous mode session (session → units → tools) so that
|
||||
* post-hoc analysis, debugging, and cost attribution can be done from a
|
||||
* single JSON artifact instead of piecing together scattered logs.
|
||||
*
|
||||
* Consumer: headless.ts (creates and finalizes traces), trace-collector.ts
|
||||
* (appends spans and events), and any external tool that reads .sf/traces/.
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Classify the role of a span in the trace hierarchy.
|
||||
*
|
||||
* Purpose: distinguish session roots, milestone/slice/task units, and
|
||||
* individual tool calls so that renderers and aggregators can group or
|
||||
* filter spans by semantic category.
|
||||
*
|
||||
* Consumer: trace-collector.ts when creating spans, and trace visualizers
|
||||
* that colour-code or collapse spans by kind.
|
||||
*/
|
||||
export type SpanKind = "session" | "unit" | "tool";
|
||||
|
||||
/**
|
||||
* Terminal state of a span.
|
||||
*
|
||||
* Purpose: capture whether a span finished successfully, failed, was
|
||||
* cancelled, or is still running so that trace consumers can compute
|
||||
* success rates and identify hung operations.
|
||||
*
|
||||
* Consumer: trace-collector.ts on unit/tool end, and trace analysis scripts
|
||||
* that aggregate outcomes across sessions.
|
||||
*/
|
||||
export type SpanStatus =
|
||||
| "ok"
|
||||
| "error"
|
||||
| "cancelled"
|
||||
| "timeout"
|
||||
| "in_progress";
|
||||
|
||||
/**
|
||||
* A discrete event attached to a span, such as a checkpoint or decision.
|
||||
*
|
||||
* Purpose: record semantically meaningful moments (e.g. "planning meeting
|
||||
* started", "model switched") inside a span without creating a child span
|
||||
* for every micro-step.
|
||||
*
|
||||
* Consumer: trace-collector.ts when recording model switches, gate results,
|
||||
* or other non-span lifecycle events.
|
||||
*/
|
||||
export interface TraceEvent {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
attributes?: Record<string, string | number | boolean | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional metadata attached to a span.
|
||||
*
|
||||
* Purpose: carry dimensional data (tokens, cost, model, file paths) that
|
||||
* lets downstream tools attribute spend and latency to specific units or
|
||||
* tools without parsing free-form log lines.
|
||||
*
|
||||
* Consumer: trace-collector.ts when enriching spans after LLM responses,
|
||||
* and cost-dashboard scripts that sum inputTokens / outputTokens.
|
||||
*/
|
||||
export interface SpanAttributes {
|
||||
// Session-level
|
||||
projectRoot?: string;
|
||||
sessionId?: string;
|
||||
cwd?: string;
|
||||
command?: string;
|
||||
model?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
cacheReadTokens?: number;
|
||||
cacheWriteTokens?: number;
|
||||
costUsd?: number;
|
||||
exitCode?: number;
|
||||
|
||||
// Unit-level
|
||||
unitType?: "milestone" | "slice" | "task";
|
||||
unitId?: string;
|
||||
unitStatus?: SpanStatus;
|
||||
unitErrorReason?: string;
|
||||
|
||||
// Tool-level
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
toolStatus?: SpanStatus;
|
||||
toolError?: string;
|
||||
toolDurationMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single node in the trace tree.
|
||||
*
|
||||
* Purpose: represent one scoped operation (session, unit, or tool call) with
|
||||
* timing, status, attributes, nested children, and a timeline of events so
|
||||
* that the full execution graph can be reconstructed from the trace file.
|
||||
*
|
||||
* Consumer: trace-collector.ts, headless.ts, and any trace reader/visualizer.
|
||||
*/
|
||||
export interface Span {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: SpanKind;
|
||||
status: SpanStatus;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
attributes: SpanAttributes;
|
||||
children: Span[];
|
||||
events: TraceEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level trace container.
|
||||
*
|
||||
* Purpose: hold the root span and session metadata so that a single file
|
||||
* contains everything needed to replay or analyse an autonomous mode session.
|
||||
*
|
||||
* Consumer: headless.ts (creates and finalizes), exportTrace/exportTraceToProject
|
||||
* (serializes), and external trace consumers.
|
||||
*/
|
||||
export interface Trace {
|
||||
id: string;
|
||||
version: number;
|
||||
projectRoot: string;
|
||||
sessionId?: string;
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
rootSpan: Span;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Span helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new span with a random UUID and current timestamp.
|
||||
*
|
||||
* Purpose: provide a single, correct construction site for spans so that
|
||||
* every span has a stable ID and a consistent start-time baseline.
|
||||
*
|
||||
* Consumer: trace-collector.ts when starting a session, unit, or tool span.
|
||||
*/
|
||||
export function createSpan(
|
||||
name: string,
|
||||
kind: SpanKind,
|
||||
attributes: SpanAttributes = {},
|
||||
): Span {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
name,
|
||||
kind,
|
||||
status: "in_progress",
|
||||
startTime: Date.now(),
|
||||
attributes,
|
||||
children: [],
|
||||
events: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a span as complete and record end time.
|
||||
*
|
||||
* Purpose: ensure every finished span carries both a terminal status and an
|
||||
* end timestamp so that duration calculations and success-rate metrics are
|
||||
* accurate.
|
||||
*
|
||||
* Consumer: trace-collector.ts when a unit or tool finishes.
|
||||
*/
|
||||
export function endSpan(span: Span, status: SpanStatus = "ok"): Span {
|
||||
span.status = status;
|
||||
span.endTime = Date.now();
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a named event to a span with optional attributes.
|
||||
*
|
||||
* Purpose: let collectors record semantically rich checkpoints (model
|
||||
* switches, gate completions) inside an existing span without mutating the
|
||||
* span's own fields.
|
||||
*
|
||||
* Consumer: trace-collector.ts during autonomous mode phase transitions.
|
||||
*/
|
||||
export function addEvent(
|
||||
span: Span,
|
||||
name: string,
|
||||
attributes?: Record<string, string | number | boolean | null>,
|
||||
): void {
|
||||
span.events.push({
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
attributes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an error event to a span with message and optional stack.
|
||||
*
|
||||
* Purpose: capture failure details (including stack traces when available)
|
||||
* inside the trace so that debugging can be done from the trace file alone
|
||||
* without cross-referencing separate log files.
|
||||
*
|
||||
* Consumer: trace-collector.ts when a tool call or unit throws.
|
||||
*/
|
||||
export function addError(span: Span, message: string, stack?: string): void {
|
||||
span.events.push({
|
||||
name: "error",
|
||||
timestamp: Date.now(),
|
||||
attributes: {
|
||||
message,
|
||||
...(stack ? { stack } : {}),
|
||||
},
|
||||
});
|
||||
span.status = "error";
|
||||
if (!span.endTime) span.endTime = Date.now();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trace helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new trace with a root session span.
|
||||
*
|
||||
* Purpose: establish the top-level trace container and its root session span
|
||||
* in one call so that headless.ts never creates a trace without a valid root.
|
||||
*
|
||||
* Consumer: headless.ts at the start of an autonomous mode session.
|
||||
*/
|
||||
export function createTrace(
|
||||
projectRoot: string,
|
||||
sessionId?: string,
|
||||
command?: string,
|
||||
model?: string,
|
||||
): Trace {
|
||||
const rootSpan = createSpan(`session:${sessionId ?? "unknown"}`, "session", {
|
||||
sessionId,
|
||||
projectRoot,
|
||||
command,
|
||||
model,
|
||||
});
|
||||
return {
|
||||
id: randomUUID(),
|
||||
version: 1,
|
||||
projectRoot,
|
||||
sessionId,
|
||||
startedAt: new Date().toISOString(),
|
||||
rootSpan,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize a trace: set completedAt timestamp.
|
||||
*
|
||||
* Purpose: mark the trace as closed so that readers know the tree is
|
||||
* complete and can safely compute session duration and aggregate costs.
|
||||
*
|
||||
* Consumer: headless.ts in the normal exit path and signal handlers.
|
||||
*/
|
||||
export function finalizeTrace(trace: Trace): Trace {
|
||||
trace.completedAt = new Date().toISOString();
|
||||
return trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a span in the tree by ID (linear walk).
|
||||
*
|
||||
* Purpose: let collectors locate an existing span (e.g. to attach a child
|
||||
* or end it) without maintaining a separate ID-to-span map.
|
||||
*
|
||||
* Consumer: trace-collector.ts when bridging async tool-call results back
|
||||
* to their original span.
|
||||
*/
|
||||
export function findSpan(span: Span, id: string): Span | undefined {
|
||||
if (span.id === id) return span;
|
||||
for (const child of span.children) {
|
||||
const found = findSpan(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a child span to a parent.
|
||||
*
|
||||
* Purpose: build the hierarchical tree (session → unit → tool) so that
|
||||
* trace readers can collapse, expand, or aggregate by level.
|
||||
*
|
||||
* Consumer: trace-collector.ts when starting a unit or tool inside an
|
||||
* already-running parent span.
|
||||
*/
|
||||
export function addChildSpan(parent: Span, child: Span): void {
|
||||
parent.children.push(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk all spans in a trace (root first, depth-first). Yields each span.
|
||||
*
|
||||
* Purpose: provide a simple, reusable traversal for aggregators, exporters,
|
||||
* and debug printers that need to visit every span without writing recursive
|
||||
* loops in every consumer.
|
||||
*
|
||||
* Consumer: trace analysis scripts, cost aggregators, and test assertions
|
||||
* that verify span tree shape.
|
||||
*/
|
||||
export function* walkSpans(span: Span): Generator<Span, void, unknown> {
|
||||
yield span;
|
||||
for (const child of span.children) {
|
||||
yield* walkSpans(child);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Serialize and write a trace to an arbitrary path.
|
||||
* Creates parent directories as needed.
|
||||
*
|
||||
* Purpose: allow trace consumers (tests, CI scripts, manual debugging) to
|
||||
* persist a trace anywhere on disk without hard-coding .sf/traces/ logic.
|
||||
*
|
||||
* Consumer: test suites that write traces to temp directories, and custom
|
||||
* integrations that ship traces to external observability platforms.
|
||||
*/
|
||||
export function exportTrace(trace: Trace, path: string): void {
|
||||
const dir = join(path, "..");
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(path, JSON.stringify(trace, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and write a trace to .sf/traces/ in the project root.
|
||||
* Filename: trace-<timestamp>.json
|
||||
*
|
||||
* Purpose: provide the standard, project-local trace sink so that every
|
||||
* autonomous mode session leaves a discoverable artifact in a known location.
|
||||
*
|
||||
* Consumer: headless.ts in the normal exit path and signal handlers.
|
||||
*/
|
||||
export function exportTraceToProject(
|
||||
trace: Trace,
|
||||
projectRoot: string,
|
||||
): string {
|
||||
const tracesDir = join(projectRoot, ".sf", "traces");
|
||||
if (!existsSync(tracesDir)) {
|
||||
mkdirSync(tracesDir, { recursive: true });
|
||||
}
|
||||
const filename = `trace-${Date.now()}.json`;
|
||||
const path = join(tracesDir, filename);
|
||||
writeFileSync(path, JSON.stringify(trace, null, 2), "utf-8");
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a trace from disk.
|
||||
*
|
||||
* Purpose: round-trip a trace file back into the typed model so that
|
||||
* analysis tools, test assertions, and replay utilities can work with
|
||||
* structured data instead of raw JSON.
|
||||
*
|
||||
* Consumer: trace analysis scripts, test helpers, and any tool that reads
|
||||
* .sf/traces/ for post-session inspection.
|
||||
*/
|
||||
export function readTrace(path: string): Trace {
|
||||
return JSON.parse(readFileSync(path, "utf-8")) as Trace;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue