agent-accelerator 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1220 -0
  3. package/SYSTEM_PROMPT.md +14 -0
  4. package/SYSTEM_PROMPT_AGENT.md +34 -0
  5. package/SYSTEM_PROMPT_TOOLS.md +9 -0
  6. package/bunfig.toml +2 -0
  7. package/package.json +59 -0
  8. package/src/agent/agent.ts +615 -0
  9. package/src/agent/context.ts +161 -0
  10. package/src/agent/delegation.ts +481 -0
  11. package/src/agent/loop.ts +569 -0
  12. package/src/agent/subagent.ts +83 -0
  13. package/src/ai-sdk/converters.ts +342 -0
  14. package/src/ai-sdk/errors.ts +122 -0
  15. package/src/ai-sdk/executor.ts +454 -0
  16. package/src/ai-sdk/index.ts +55 -0
  17. package/src/ai-sdk/model-provider.ts +303 -0
  18. package/src/ai-sdk/options.ts +306 -0
  19. package/src/ai-sdk/provider.ts +415 -0
  20. package/src/ai-sdk/registry.ts +416 -0
  21. package/src/data/README.md +84 -0
  22. package/src/index.ts +190 -0
  23. package/src/models/catalog-cache.ts +273 -0
  24. package/src/models/catalog.ts +503 -0
  25. package/src/streaming/event-stream.ts +211 -0
  26. package/src/streaming/sse-parser.ts +97 -0
  27. package/src/tokens/counter.ts +136 -0
  28. package/src/tools/executor.ts +365 -0
  29. package/src/tools/schema.ts +221 -0
  30. package/src/tools/tool.ts +101 -0
  31. package/src/types/agent.ts +87 -0
  32. package/src/types/core.ts +86 -0
  33. package/src/types/message.ts +115 -0
  34. package/src/types/model.ts +212 -0
  35. package/src/types/provider-payloads.ts +434 -0
  36. package/src/types/response.ts +158 -0
  37. package/src/types/tool.ts +61 -0
  38. package/src/utils/base64.ts +27 -0
  39. package/src/utils/cache.ts +146 -0
  40. package/src/utils/env.ts +78 -0
  41. package/src/utils/headers.ts +110 -0
  42. package/src/utils/media.ts +137 -0
  43. package/src/utils/serialization.ts +91 -0
  44. package/src/utils/session.ts +26 -0
  45. package/src/utils/thought-signature.ts +27 -0
  46. package/tsconfig.json +31 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Battle-tested cache helper — single source for 80-90% hit rate
3
+ * Based on agent-accel: openai-prompt-cache.ts + applyAnthropicCacheControl
4
+ * - First turn already cache-optimized: system + tools + first user get cache_control
5
+ * - Stable sessionId (64 clamp) + prompt_cache_key ensures affinity
6
+ * - 4 breakpoint cap (Anthropic limit)
7
+ * - Retention mapping: short=5m (no ttl), medium=1h, long=24h (prompt) / 1h (anthropic)
8
+ */
9
+
10
+ import type { CacheRetention } from "../types/core.ts";
11
+ import type { ModelSpec } from "../types/model.ts";
12
+ import { getApiKey } from "./env.ts";
13
+
14
+ const CACHE_KEY_MAX = 64;
15
+
16
+ /** Clamps a cache/session affinity key to the provider-safe 64-character limit. */
17
+ export function clampCacheKey(key?: string): string | undefined {
18
+ if (!key) return undefined;
19
+ const chars = Array.from(key);
20
+ if (chars.length <= CACHE_KEY_MAX) return key;
21
+ return chars.slice(0, CACHE_KEY_MAX).join("");
22
+ }
23
+
24
+ /** Maps retention to explicit-cache TTL seconds. short=5m, medium=1h, long=12h. Explicit ttlSeconds wins. */
25
+ export function retentionToTtlSeconds(retention?: CacheRetention, ttlSeconds?: number): number | undefined {
26
+ if (ttlSeconds && ttlSeconds > 0) return Math.floor(ttlSeconds);
27
+ if (!retention || retention === "implicit") return undefined;
28
+ if (retention === "short") return 300;
29
+ if (retention === "medium") return 3600;
30
+ if (retention === "long") return 43200;
31
+ return undefined;
32
+ }
33
+
34
+ /** Maps retention settings to OpenCode/OpenRouter prompt-cache TTL values. */
35
+ export function getPromptCacheRetention(retention?: CacheRetention, supportsLong = true): "24h" | "1h" | undefined {
36
+ if (!retention || retention === "implicit") return undefined;
37
+ if (retention === "long" && supportsLong) return "24h";
38
+ if (retention === "medium" && supportsLong) return "1h";
39
+ if (retention === "long") return "24h"; // fallback even if supportsLong false, provider may ignore
40
+ return undefined;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Explicit Context Caching (Google cachedContents API)
45
+ // ============================================================================
46
+
47
+ export interface CachedContentMetadata {
48
+ name: string;
49
+ displayName?: string;
50
+ model: string;
51
+ createTime: string;
52
+ updateTime: string;
53
+ expireTime: string;
54
+ usageMetadata?: {
55
+ totalTokenCount?: number;
56
+ };
57
+ }
58
+
59
+ export interface CreateExplicitCacheOptions {
60
+ model: string;
61
+ contents?: any[];
62
+ systemInstruction?: string;
63
+ tools?: any[];
64
+ toolConfig?: any;
65
+ displayName?: string;
66
+ ttlSeconds?: number;
67
+ retention?: CacheRetention;
68
+ expireTime?: string | Date;
69
+ apiKey?: string;
70
+ baseUrl?: string;
71
+ }
72
+
73
+ /**
74
+ * Creates an explicit cached content object using Google Gemini's cachedContents API.
75
+ * REST: POST https://generativelanguage.googleapis.com/v1beta/cachedContents?key=...
76
+ */
77
+ /**
78
+ * Creates a Google Gemini cachedContents resource through the REST API.
79
+ *
80
+ * @example `const cache = await createExplicitCache({ model: "google/gemini-3.5-flash-lite", contents });`
81
+ */
82
+ export async function createExplicitCache(
83
+ options: CreateExplicitCacheOptions
84
+ ): Promise<CachedContentMetadata> {
85
+ const apiKey = getApiKey("google", options.apiKey);
86
+ if (!apiKey) {
87
+ throw new Error("API key is required to create explicit cache (GEMINI_API_KEY)");
88
+ }
89
+
90
+ const baseUrl = options.baseUrl || "https://generativelanguage.googleapis.com/v1beta";
91
+ const ttlSeconds = retentionToTtlSeconds(options.retention, options.ttlSeconds) ?? 3600;
92
+ const ttl = options.expireTime ? undefined : `${ttlSeconds}s`;
93
+
94
+ let modelName = options.model;
95
+ if (!modelName.startsWith("models/")) {
96
+ modelName = `models/${modelName.replace(/^google\//, "")}`;
97
+ }
98
+
99
+ const payload: Record<string, unknown> = {
100
+ model: modelName,
101
+ contents: options.contents ?? [],
102
+ ...(ttl ? { ttl } : {}),
103
+ ...(options.expireTime
104
+ ? { expireTime: options.expireTime instanceof Date ? options.expireTime.toISOString() : options.expireTime }
105
+ : {}),
106
+ };
107
+
108
+ if (options.displayName) {
109
+ payload.displayName = options.displayName;
110
+ (payload as any).display_name = options.displayName;
111
+ }
112
+
113
+ if (options.systemInstruction) {
114
+ payload.systemInstruction = {
115
+ parts: [{ text: options.systemInstruction }],
116
+ };
117
+ (payload as any).system_instruction = payload.systemInstruction;
118
+ }
119
+
120
+ if (options.tools && options.tools.length > 0) {
121
+ payload.tools = options.tools;
122
+ }
123
+
124
+ if (options.toolConfig) {
125
+ payload.toolConfig = options.toolConfig;
126
+ (payload as any).tool_config = options.toolConfig;
127
+ }
128
+
129
+ const url = `${baseUrl}/cachedContents?key=${apiKey}`;
130
+ const response = await fetch(url, {
131
+ method: "POST",
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ },
135
+ body: JSON.stringify(payload),
136
+ });
137
+
138
+ if (!response.ok) {
139
+ const errorBody = await response.text();
140
+ throw new Error(
141
+ `Failed to create explicit cache (${response.status} ${response.statusText}): ${errorBody}`
142
+ );
143
+ }
144
+
145
+ return (await response.json()) as CachedContentMetadata;
146
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Safe environment lookup for API keys and configurations
3
+ */
4
+ /**
5
+ * Reads an environment/global value without assuming a Node-only runtime.
6
+ *
7
+ * @example `const apiKey = getEnv("OPENAI_API_KEY");`
8
+ */
9
+ export function getEnv(key: string, fallback?: string): string | undefined {
10
+ if (typeof process !== "undefined" && process.env && process.env[key]) {
11
+ return process.env[key];
12
+ }
13
+ if (typeof globalThis !== "undefined" && (globalThis as any)[key]) {
14
+ return (globalThis as any)[key];
15
+ }
16
+ return fallback;
17
+ }
18
+
19
+ export type ProviderEnv = Record<string, string>;
20
+
21
+ /**
22
+ * Resolves a provider API key from explicit input, overrides, and standard aliases.
23
+ *
24
+ * @example `const key = getApiKey("google");`
25
+ */
26
+ export function getApiKey(provider: string, explicitKey?: string, env?: ProviderEnv): string | undefined {
27
+ if (explicitKey) return explicitKey;
28
+ // ProviderEnv override takes precedence (agent-accel inspiration, SDK-light)
29
+ if (env) {
30
+ const upper = `${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
31
+ if (env[upper]) return env[upper];
32
+ // google aliases
33
+ if (provider.toLowerCase().startsWith("google") || provider.toLowerCase() === "gemini") {
34
+ if (env["GEMINI_API_KEY"]) return env["GEMINI_API_KEY"];
35
+ if (env["GOOGLE_API_KEY"]) return env["GOOGLE_API_KEY"];
36
+ }
37
+ // openai aliases
38
+ if (provider.toLowerCase() === "openai") {
39
+ if (env["OPENAI_BASE_API_KEY"]) return env["OPENAI_BASE_API_KEY"];
40
+ if (env["OPENAI_API_KEY"]) return env["OPENAI_API_KEY"];
41
+ }
42
+ }
43
+
44
+ switch (provider.toLowerCase()) {
45
+ case "google":
46
+ case "gemini":
47
+ return (
48
+ getEnv("GEMINI_API_KEY") ||
49
+ getEnv("GOOGLE_API_KEY") ||
50
+ getEnv("GOOGLE_GENAI_API_KEY")
51
+ );
52
+ case "opencode":
53
+ case "opencode-go":
54
+ return (
55
+ getEnv("OPENCODE_API_KEY") ||
56
+ getEnv("OPENCODE_ZEN_API_KEY") ||
57
+ getEnv("OPENCODE_GO_API_KEY")
58
+ );
59
+ case "openrouter":
60
+ return getEnv("OPENROUTER_API_KEY");
61
+ case "openai":
62
+ return getEnv("OPENAI_BASE_API_KEY") || getEnv("OPENAI_API_KEY");
63
+ case "anthropic":
64
+ return getEnv("ANTHROPIC_API_KEY");
65
+ default:
66
+ return getEnv(`${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`);
67
+ }
68
+ }
69
+
70
+ /** Reads MODEL or MODEL_NAME, with an optional fallback. */
71
+ export function getModel(fallback?: string): string | undefined {
72
+ return getEnv("MODEL") || getEnv("MODEL_NAME") || fallback;
73
+ }
74
+
75
+ /** Reads SUB_AGENT_MODEL, with an optional fallback. */
76
+ export function getSubModel(fallback?: string): string | undefined {
77
+ return getEnv("SUB_AGENT_MODEL") || fallback;
78
+ }
@@ -0,0 +1,110 @@
1
+ import type { ProviderId } from "../types/model.ts";
2
+ import type { CacheConfig } from "../types/core.ts";
3
+ import { clampCacheKey } from "./cache.ts";
4
+
5
+ export function isBrowserRuntime(): boolean {
6
+ try {
7
+ return typeof (globalThis as any).window !== "undefined" && typeof (globalThis as any).window.document !== "undefined";
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function getAgentAccelUserAgent(): string {
14
+ try {
15
+ const os = (globalThis as any).process?.getBuiltinModule?.("node:os") ?? null;
16
+ if (os) {
17
+ return `agent-accel (${os.platform()} ${os.release()}; ${os.arch()})`;
18
+ }
19
+ } catch {}
20
+ return "agent-accel (linux; x64)";
21
+ }
22
+
23
+ const BROWSER_DROPPED = new Set([
24
+ "user-agent",
25
+ "x-session-id",
26
+ "x-client-request-id",
27
+ "session_id",
28
+ "x-opencode-session",
29
+ "x-opencode-client",
30
+ "x-goog-api-client",
31
+ ]);
32
+
33
+ function stripForBrowser(headers: Record<string, string>): Record<string, string> {
34
+ const out: Record<string, string> = {};
35
+ for (const [k, v] of Object.entries(headers)) {
36
+ if (BROWSER_DROPPED.has(k.toLowerCase())) continue;
37
+ out[k] = v;
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /**
43
+ * Builds provider-specific session, cache-affinity, and attribution headers.
44
+ *
45
+ * Browser note: custom `x-*` session/affinity headers force a CORS preflight
46
+ * (`OPTIONS`) and providers only allow-list their own documented headers, so a
47
+ * preflight failure surfaces as a bare `TypeError: Failed to fetch`. In browser
48
+ * runtimes this returns only CORS-safe attribution headers (OpenRouter
49
+ * `HTTP-Referer` / `X-Title`); session affinity still flows via `providerOptions`
50
+ * (`promptCacheKey`), never via headers.
51
+ * @example `const headers = buildSessionHeaders("google", { sessionId: "session-123" });`
52
+ */
53
+ export function buildSessionHeaders(
54
+ provider: ProviderId | string,
55
+ cache?: CacheConfig,
56
+ customHeaders?: Record<string, string>,
57
+ explicitSessionId?: string
58
+ ): Record<string, string> {
59
+ const browser = isBrowserRuntime();
60
+ const headers: Record<string, string> = {
61
+ ...(browser ? {} : { "User-Agent": "Agent-Accelerator/1.0" }),
62
+ ...(customHeaders ?? {}),
63
+ };
64
+
65
+ const rawSessionId = explicitSessionId || cache?.sessionId;
66
+ const sessionId = clampCacheKey(rawSessionId);
67
+
68
+ if (sessionId && !browser) {
69
+ if (provider === "opencode" || provider === "opencode-go") {
70
+ headers["x-opencode-session"] = sessionId;
71
+ headers["x-session-id"] = sessionId;
72
+ headers["x-client-request-id"] = sessionId;
73
+ headers["session_id"] = sessionId;
74
+ headers["x-opencode-client"] = "agent-accel";
75
+ headers["User-Agent"] = getAgentAccelUserAgent();
76
+ } else if (provider === "openrouter") {
77
+ headers["x-session-id"] = sessionId;
78
+ headers["x-client-request-id"] = sessionId;
79
+ headers["HTTP-Referer"] = "https://sashvat.com";
80
+ headers["X-Title"] = "Agent Accelerator";
81
+ } else if (provider === "google") {
82
+ headers["x-goog-api-client"] = "agent-accel/1.0";
83
+ headers["x-session-id"] = sessionId;
84
+ headers["x-client-request-id"] = sessionId;
85
+ } else {
86
+ headers["x-session-id"] = sessionId;
87
+ headers["x-client-request-id"] = sessionId;
88
+ headers["session_id"] = sessionId;
89
+ }
90
+ } else if (provider === "openrouter") {
91
+ if (!headers["HTTP-Referer"]) headers["HTTP-Referer"] = "https://sashvat.com";
92
+ if (!headers["X-Title"]) headers["X-Title"] = "Agent Accelerator";
93
+ }
94
+
95
+ if (provider === "google" && !browser && !headers["x-goog-api-client"]) {
96
+ headers["x-goog-api-client"] = "agent-accel/1.0";
97
+ }
98
+
99
+ if (
100
+ (provider === "opencode" || provider === "opencode-go") &&
101
+ !browser &&
102
+ !headers["x-opencode-client"]
103
+ ) {
104
+ headers["x-opencode-client"] = "agent-accel";
105
+ headers["User-Agent"] = getAgentAccelUserAgent();
106
+ }
107
+
108
+ if (browser) return stripForBrowser(headers);
109
+ return headers;
110
+ }
@@ -0,0 +1,137 @@
1
+ // NOTE: no static node:fs import — it breaks browser bundles. Local file
2
+ // paths are loaded via a lazy dynamic import so browsers never resolve it.
3
+ import { bytesToBase64 } from "./base64.ts";
4
+
5
+ export interface NormalizedMedia {
6
+ mimeType: string;
7
+ base64Data: string;
8
+ dataUrl: string;
9
+ }
10
+
11
+ /**
12
+ * Detects MIME type from common file signatures or extensions
13
+ */
14
+ /**
15
+ * Infers a MIME type from a URL/path extension.
16
+ *
17
+ * @example `const mime = inferMimeType("photo.webp");`
18
+ */
19
+ export function inferMimeType(input: string, fallback = "application/octet-stream"): string {
20
+ const clean = input.toLowerCase().split("?")[0]!;
21
+ if (clean.endsWith(".png")) return "image/png";
22
+ if (clean.endsWith(".jpg") || clean.endsWith(".jpeg")) return "image/jpeg";
23
+ if (clean.endsWith(".webp")) return "image/webp";
24
+ if (clean.endsWith(".gif")) return "image/gif";
25
+ if (clean.endsWith(".mp3")) return "audio/mp3";
26
+ if (clean.endsWith(".wav")) return "audio/wav";
27
+ if (clean.endsWith(".ogg")) return "audio/ogg";
28
+ if (clean.endsWith(".mp4")) return "video/mp4";
29
+ if (clean.endsWith(".webm")) return "video/webm";
30
+ if (clean.endsWith(".mov")) return "video/quicktime";
31
+ if (clean.endsWith(".pdf")) return "application/pdf";
32
+ return fallback;
33
+ }
34
+
35
+ /**
36
+ * Normalizes an image, audio, or video input into raw base64 and data URL
37
+ */
38
+ /**
39
+ * Normalizes a path, URL, data URL, base64 string, or binary value into media payloads.
40
+ *
41
+ * @example `const image = await normalizeMediaInput("https://example.com/photo.png");`
42
+ */
43
+ export async function normalizeMediaInput(
44
+ input: string | Uint8Array | ArrayBuffer,
45
+ explicitMimeType?: string
46
+ ): Promise<NormalizedMedia> {
47
+ // 1. If input is ArrayBuffer or Uint8Array
48
+ if (input instanceof Uint8Array || input instanceof ArrayBuffer) {
49
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
50
+ const mimeType = explicitMimeType || "application/octet-stream";
51
+ const base64Data = bytesToBase64(bytes);
52
+ return {
53
+ mimeType,
54
+ base64Data,
55
+ dataUrl: `data:${mimeType};base64,${base64Data}`,
56
+ };
57
+ }
58
+
59
+ // 2. If input is data URL (data:image/png;base64,...)
60
+ if (typeof input === "string" && input.startsWith("data:")) {
61
+ const match = input.match(/^data:([^;]+);base64,(.+)$/);
62
+ if (match && match[1] && match[2]) {
63
+ return {
64
+ mimeType: explicitMimeType || match[1],
65
+ base64Data: match[2],
66
+ dataUrl: input,
67
+ };
68
+ }
69
+ }
70
+
71
+ // 3. If input is a remote HTTP URL (check before base64 to avoid misclassifying URL as base64)
72
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
73
+ const response = await fetch(input);
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch media from URL: ${input} (${response.statusText})`);
76
+ }
77
+ const arrayBuffer = await response.arrayBuffer();
78
+ const buffer = new Uint8Array(arrayBuffer);
79
+ const mimeType =
80
+ explicitMimeType ||
81
+ response.headers.get("content-type") ||
82
+ inferMimeType(input);
83
+ const base64Data = bytesToBase64(buffer);
84
+ return {
85
+ mimeType,
86
+ base64Data,
87
+ dataUrl: `data:${mimeType};base64,${base64Data}`,
88
+ };
89
+ }
90
+
91
+ // 4. If input is already raw base64 string (stricter: length threshold + no file markers)
92
+ if (
93
+ typeof input === "string" &&
94
+ !input.includes("/") && // file paths contain /
95
+ !input.includes("\\") &&
96
+ input.length > 100 &&
97
+ /^[A-Za-z0-9+/=\n\r]+$/.test(input.slice(0, 200)) &&
98
+ input.length % 4 === 0
99
+ ) {
100
+ const mimeType = explicitMimeType || "image/jpeg";
101
+ return {
102
+ mimeType,
103
+ base64Data: input.replace(/\s/g, ""),
104
+ dataUrl: `data:${mimeType};base64,${input.replace(/\s/g, "")}`,
105
+ };
106
+ }
107
+
108
+ // 5. If input is local file path (Node only — browsers use data URLs / FileReader)
109
+ if (typeof input === "string") {
110
+ try {
111
+ const { promises: fs } = await import("node:fs");
112
+ const buffer = await fs.readFile(input);
113
+ const mimeType = explicitMimeType || inferMimeType(input);
114
+ const base64Data = bytesToBase64(buffer);
115
+ return {
116
+ mimeType,
117
+ base64Data,
118
+ dataUrl: `data:${mimeType};base64,${base64Data}`,
119
+ };
120
+ } catch (err) {
121
+ // S11: don't silently return path string as base64 (would 400). Re-check if it's actually base64 with "/"
122
+ const trimmed = input.replace(/\s/g, "");
123
+ const isMaybeBase64 = trimmed.length > 100 && /^[A-Za-z0-9+/=\n\r]+$/.test(trimmed.slice(0, 500)) && trimmed.length % 4 === 0;
124
+ if (isMaybeBase64) {
125
+ const mimeType = explicitMimeType || "image/jpeg";
126
+ return {
127
+ mimeType,
128
+ base64Data: trimmed,
129
+ dataUrl: `data:${mimeType};base64,${trimmed}`,
130
+ };
131
+ }
132
+ throw new Error(`Failed to normalize media input: not a valid file path, data URL, http URL, or base64 string: ${input.slice(0, 80)}`);
133
+ }
134
+ }
135
+
136
+ throw new Error(`Unsupported media input type: ${typeof input}`);
137
+ }
@@ -0,0 +1,91 @@
1
+ import { bytesToBase64 } from "./base64.ts";
2
+ /**
3
+ * Converts arbitrary tool output into values that are safe to put in model
4
+ * context or JSON responses. Circular references and unsupported primitives
5
+ * are represented instead of causing JSON.stringify to throw.
6
+ */
7
+ /**
8
+ * Converts arbitrary tool output into a circular/reference-safe JSON value.
9
+ *
10
+ * @example `const safe = toJsonSafe({ result, request });`
11
+ */
12
+ export function toJsonSafe(value: unknown, seen = new WeakSet<object>()): unknown {
13
+ if (value === null || value === undefined) return value === undefined ? null : value;
14
+ if (typeof value === "bigint") return `${value.toString()}n`;
15
+ if (typeof value === "number" && !Number.isFinite(value)) return String(value);
16
+ if (typeof value === "function") return `[Function${value.name ? `: ${value.name}` : ""}]`;
17
+ if (typeof value === "symbol") return value.toString();
18
+ if (typeof value !== "object") return value;
19
+
20
+ if (seen.has(value as object)) return "[Circular]";
21
+ seen.add(value as object);
22
+
23
+ try {
24
+ if (value instanceof Error) {
25
+ const error: Record<string, unknown> = {
26
+ name: value.name,
27
+ message: value.message,
28
+ ...(value.stack ? { stack: value.stack } : {}),
29
+ };
30
+ for (const key of Object.keys(value as any)) {
31
+ error[key] = toJsonSafe((value as any)[key], seen);
32
+ }
33
+ return error;
34
+ }
35
+
36
+ if (value instanceof Date) return value.toISOString();
37
+
38
+ if (value instanceof Uint8Array) {
39
+ return {
40
+ type: "Uint8Array",
41
+ base64: bytesToBase64(value),
42
+ byteLength: value.byteLength,
43
+ };
44
+ }
45
+
46
+ if (value instanceof ArrayBuffer) {
47
+ const bytes = new Uint8Array(value);
48
+ return {
49
+ type: "ArrayBuffer",
50
+ base64: bytesToBase64(bytes),
51
+ byteLength: bytes.byteLength,
52
+ };
53
+ }
54
+
55
+ if (value instanceof Map) {
56
+ return Array.from(value.entries()).map(([key, entry]) => [
57
+ toJsonSafe(key, seen),
58
+ toJsonSafe(entry, seen),
59
+ ]);
60
+ }
61
+
62
+ if (value instanceof Set) {
63
+ return Array.from(value.values()).map((entry) => toJsonSafe(entry, seen));
64
+ }
65
+
66
+ if (Array.isArray(value)) {
67
+ return value.map((entry) => toJsonSafe(entry, seen));
68
+ }
69
+
70
+ const output: Record<string, unknown> = {};
71
+ for (const key of Object.keys(value as any)) {
72
+ output[key] = toJsonSafe((value as any)[key], seen);
73
+ }
74
+ return output;
75
+ } finally {
76
+ seen.delete(value as object);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Safely serializes arbitrary values without throwing on circular or unsupported data.
82
+ *
83
+ * @example `logger.info(safeStringify(toolResult));`
84
+ */
85
+ export function safeStringify(value: unknown): string {
86
+ try {
87
+ return JSON.stringify(toJsonSafe(value)) ?? "null";
88
+ } catch {
89
+ return String(value);
90
+ }
91
+ }
@@ -0,0 +1,26 @@
1
+ import { clampCacheKey } from "./cache.ts";
2
+
3
+ /**
4
+ * Creates or formats a unique session ID for prompt caching affinity (clamped to 64 chars like agent-accel)
5
+ */
6
+ /**
7
+ * Creates a unique, provider-safe session ID for cache affinity.
8
+ *
9
+ * @example `const sessionId = createSessionId("checkout");`
10
+ */
11
+ export function createSessionId(prefix = "accel"): string {
12
+ let id: string;
13
+ try {
14
+ const webCrypto =
15
+ (globalThis as any)?.crypto ?? (typeof crypto !== "undefined" ? crypto : undefined);
16
+ if (webCrypto && typeof webCrypto.randomUUID === "function") {
17
+ id = `${prefix}-${webCrypto.randomUUID()}`;
18
+ } else {
19
+ throw new Error("no randomUUID");
20
+ }
21
+ } catch {
22
+ // Fallback — still clamp
23
+ id = `${prefix}-${Math.random().toString(36).slice(2, 11)}-${Date.now().toString(36)}`;
24
+ }
25
+ return clampCacheKey(id) || id;
26
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Utilities for Gemini thought signatures via OpenAI-compatible endpoints & Google AI Studio.
3
+ */
4
+
5
+ const base64SigPattern = /^[A-Za-z0-9+/]+={0,2}$/;
6
+
7
+ /**
8
+ * Checks whether a Gemini thought signature has valid base64 shape.
9
+ * @example `const valid = isValidThoughtSignature(signature);`
10
+ */
11
+ export function isValidThoughtSignature(sig?: string): boolean {
12
+ if (!sig) return false;
13
+ if (sig.length % 4 !== 0) return false;
14
+ return base64SigPattern.test(sig);
15
+ }
16
+
17
+ /** Keeps an incoming signature when present, otherwise preserves the existing one. */
18
+ export function retainThoughtSignature(existing?: string, incoming?: string): string | undefined {
19
+ if (typeof incoming === "string" && incoming.length > 0) return incoming;
20
+ return existing;
21
+ }
22
+
23
+ /** Extracts a Google thought signature from OpenAI-compatible metadata. */
24
+ export function extractGoogleThoughtSignature(obj: any): string | undefined {
25
+ const sig = obj?.extra_content?.google?.thought_signature;
26
+ return typeof sig === "string" && sig.length > 0 ? sig : undefined;
27
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+ "types": ["bun"],
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ },
29
+ "include": ["src/**/*", "examples/**/*"],
30
+ "exclude": ["references", "node_modules"]
31
+ }