@jussmor/sdk-ai 0.2.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 (111) hide show
  1. package/dist/conversation-kIkMQdYK.d.cts +105 -0
  2. package/dist/conversation-kIkMQdYK.d.ts +105 -0
  3. package/dist/conversation-store-CAyPuBjk.d.ts +10 -0
  4. package/dist/conversation-store-Cl42jpsA.d.cts +10 -0
  5. package/dist/index.cjs +1630 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +251 -0
  8. package/dist/index.d.ts +251 -0
  9. package/dist/index.js +1536 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/memory-uBLqrQRY.d.cts +28 -0
  12. package/dist/memory-uBLqrQRY.d.ts +28 -0
  13. package/dist/providers/llm/anthropic.cjs +275 -0
  14. package/dist/providers/llm/anthropic.cjs.map +1 -0
  15. package/dist/providers/llm/anthropic.d.cts +22 -0
  16. package/dist/providers/llm/anthropic.d.ts +22 -0
  17. package/dist/providers/llm/anthropic.js +240 -0
  18. package/dist/providers/llm/anthropic.js.map +1 -0
  19. package/dist/providers/llm/ollama.cjs +195 -0
  20. package/dist/providers/llm/ollama.cjs.map +1 -0
  21. package/dist/providers/llm/ollama.d.cts +23 -0
  22. package/dist/providers/llm/ollama.d.ts +23 -0
  23. package/dist/providers/llm/ollama.js +170 -0
  24. package/dist/providers/llm/ollama.js.map +1 -0
  25. package/dist/providers/llm/openai.cjs +213 -0
  26. package/dist/providers/llm/openai.cjs.map +1 -0
  27. package/dist/providers/llm/openai.d.cts +22 -0
  28. package/dist/providers/llm/openai.d.ts +22 -0
  29. package/dist/providers/llm/openai.js +178 -0
  30. package/dist/providers/llm/openai.js.map +1 -0
  31. package/dist/providers/memory/filesystem.cjs +112 -0
  32. package/dist/providers/memory/filesystem.cjs.map +1 -0
  33. package/dist/providers/memory/filesystem.d.cts +17 -0
  34. package/dist/providers/memory/filesystem.d.ts +17 -0
  35. package/dist/providers/memory/filesystem.js +87 -0
  36. package/dist/providers/memory/filesystem.js.map +1 -0
  37. package/dist/providers/store/filesystem.cjs +87 -0
  38. package/dist/providers/store/filesystem.cjs.map +1 -0
  39. package/dist/providers/store/filesystem.d.cts +14 -0
  40. package/dist/providers/store/filesystem.d.ts +14 -0
  41. package/dist/providers/store/filesystem.js +62 -0
  42. package/dist/providers/store/filesystem.js.map +1 -0
  43. package/dist/providers/thread/memory.cjs +81 -0
  44. package/dist/providers/thread/memory.cjs.map +1 -0
  45. package/dist/providers/thread/memory.d.cts +14 -0
  46. package/dist/providers/thread/memory.d.ts +14 -0
  47. package/dist/providers/thread/memory.js +56 -0
  48. package/dist/providers/thread/memory.js.map +1 -0
  49. package/dist/providers/thread/sqlite.cjs +917 -0
  50. package/dist/providers/thread/sqlite.cjs.map +1 -0
  51. package/dist/providers/thread/sqlite.d.cts +17 -0
  52. package/dist/providers/thread/sqlite.d.ts +17 -0
  53. package/dist/providers/thread/sqlite.js +911 -0
  54. package/dist/providers/thread/sqlite.js.map +1 -0
  55. package/dist/providers/tokenizers/auto.cjs +136 -0
  56. package/dist/providers/tokenizers/auto.cjs.map +1 -0
  57. package/dist/providers/tokenizers/auto.d.cts +24 -0
  58. package/dist/providers/tokenizers/auto.d.ts +24 -0
  59. package/dist/providers/tokenizers/auto.js +107 -0
  60. package/dist/providers/tokenizers/auto.js.map +1 -0
  61. package/dist/streaming-B-P6Fw_k.d.cts +372 -0
  62. package/dist/streaming-BtD23BE0.d.ts +372 -0
  63. package/dist/thread-C2b9xRMJ.d.cts +30 -0
  64. package/dist/thread-C2b9xRMJ.d.ts +30 -0
  65. package/dist/tokenizer-BhG_RGUk.d.cts +13 -0
  66. package/dist/tokenizer-BhG_RGUk.d.ts +13 -0
  67. package/package.json +84 -0
  68. package/src/agent-loop.ts +311 -0
  69. package/src/agent-source.ts +12 -0
  70. package/src/artifact.ts +31 -0
  71. package/src/compaction.ts +75 -0
  72. package/src/context-budget.ts +65 -0
  73. package/src/conversation-store.ts +8 -0
  74. package/src/conversation.ts +42 -0
  75. package/src/dispatch.ts +207 -0
  76. package/src/engine.ts +53 -0
  77. package/src/execution-context.ts +31 -0
  78. package/src/index.ts +37 -0
  79. package/src/interrupt-store.ts +25 -0
  80. package/src/interrupt.ts +55 -0
  81. package/src/llm-router.ts +34 -0
  82. package/src/llm.ts +100 -0
  83. package/src/memory-selector.ts +38 -0
  84. package/src/memory.ts +34 -0
  85. package/src/mode.ts +81 -0
  86. package/src/permissions.ts +104 -0
  87. package/src/protocol.ts +1 -0
  88. package/src/providers/llm/anthropic.ts +298 -0
  89. package/src/providers/llm/ollama.ts +219 -0
  90. package/src/providers/llm/openai.ts +215 -0
  91. package/src/providers/memory/filesystem.ts +99 -0
  92. package/src/providers/store/filesystem.ts +64 -0
  93. package/src/providers/thread/memory.ts +67 -0
  94. package/src/providers/thread/sqlite.ts +147 -0
  95. package/src/providers/tokenizers/auto.ts +26 -0
  96. package/src/providers/tokenizers/byte.ts +27 -0
  97. package/src/providers/tokenizers/tiktoken.ts +91 -0
  98. package/src/reasoning.ts +7 -0
  99. package/src/rule-matcher.ts +32 -0
  100. package/src/runtime.ts +416 -0
  101. package/src/safety.ts +56 -0
  102. package/src/sandbox.ts +23 -0
  103. package/src/session-context.ts +33 -0
  104. package/src/skill-source.ts +21 -0
  105. package/src/streaming.ts +124 -0
  106. package/src/system-prompt.ts +71 -0
  107. package/src/system-reminder.ts +9 -0
  108. package/src/thread.ts +33 -0
  109. package/src/tokenizer.ts +31 -0
  110. package/src/tool.ts +175 -0
  111. package/src/tracing.ts +63 -0
@@ -0,0 +1,215 @@
1
+ import OpenAI from "openai";
2
+ import type {
3
+ LLMProvider,
4
+ ChatRequest,
5
+ ChatResponse,
6
+ ChatMessage,
7
+ } from "../../llm.js";
8
+ import type { StreamingLLMProvider, StreamEvent } from "../../streaming.js";
9
+
10
+ function toOpenAIMessages(
11
+ messages: ChatMessage[],
12
+ ): OpenAI.ChatCompletionMessageParam[] {
13
+ return messages.map((msg): OpenAI.ChatCompletionMessageParam => {
14
+ if (msg.role === "system") {
15
+ return { role: "system", content: msg.content };
16
+ }
17
+ if (msg.role === "tool") {
18
+ return {
19
+ role: "tool",
20
+ tool_call_id: msg.toolCallId ?? "",
21
+ content: msg.content,
22
+ };
23
+ }
24
+ if (msg.role === "assistant") {
25
+ const out: OpenAI.ChatCompletionAssistantMessageParam = {
26
+ role: "assistant",
27
+ content: msg.content || null,
28
+ };
29
+ if (msg.toolCalls?.length) {
30
+ out.tool_calls = msg.toolCalls.map((tc) => ({
31
+ id: tc.id,
32
+ type: "function" as const,
33
+ function: { name: tc.name, arguments: tc.arguments },
34
+ }));
35
+ }
36
+ return out;
37
+ }
38
+ if (msg.images?.length) {
39
+ const content: OpenAI.ChatCompletionContentPart[] = msg.images.map(
40
+ (img) =>
41
+ img.url
42
+ ? ({ type: "image_url", image_url: { url: img.url } } as const)
43
+ : ({
44
+ type: "image_url",
45
+ image_url: {
46
+ url: `data:${img.mediaType};base64,${img.source}`,
47
+ },
48
+ } as const),
49
+ );
50
+ if (msg.content) content.push({ type: "text", text: msg.content });
51
+ return { role: "user", content };
52
+ }
53
+ return { role: "user", content: msg.content };
54
+ });
55
+ }
56
+
57
+ export interface OpenAIProviderOptions {
58
+ apiKey?: string;
59
+ baseURL?: string;
60
+ defaultModel?: string;
61
+ defaultMaxTokens?: number;
62
+ }
63
+
64
+ export class OpenAIProvider implements StreamingLLMProvider {
65
+ private client: OpenAI;
66
+ private defaultModel: string;
67
+ private defaultMaxTokens: number;
68
+
69
+ constructor(opts: OpenAIProviderOptions = {}) {
70
+ this.client = new OpenAI({
71
+ apiKey: opts.apiKey ?? process.env["OPENAI_API_KEY"],
72
+ baseURL: opts.baseURL ?? process.env["OPENAI_BASE_URL"],
73
+ });
74
+ this.defaultModel = opts.defaultModel ?? "gpt-4o";
75
+ this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
76
+ }
77
+
78
+ async chat(req: ChatRequest, signal?: AbortSignal): Promise<ChatResponse> {
79
+ const messages = toOpenAIMessages(req.messages);
80
+ const tools = req.tools?.map((t) => ({
81
+ type: "function" as const,
82
+ function: {
83
+ name: t.function.name,
84
+ description: t.function.description,
85
+ parameters: t.function.parameters as unknown as Record<string, unknown>,
86
+ },
87
+ }));
88
+
89
+ const resp = await this.client.chat.completions.create(
90
+ {
91
+ model: req.model ?? this.defaultModel,
92
+ messages,
93
+ max_tokens: req.maxTokens ?? this.defaultMaxTokens,
94
+ ...(tools?.length ? { tools } : {}),
95
+ ...(req.temperature != null ? { temperature: req.temperature } : {}),
96
+ ...(req.topP != null ? { top_p: req.topP } : {}),
97
+ ...(req.stop?.length ? { stop: req.stop } : {}),
98
+ },
99
+ { signal },
100
+ );
101
+
102
+ const choice = resp.choices[0];
103
+ const msg = choice?.message;
104
+
105
+ return {
106
+ content: msg?.content ?? "",
107
+ toolCalls: msg?.tool_calls?.map((tc) => ({
108
+ id: tc.id,
109
+ name: tc.function.name,
110
+ arguments: tc.function.arguments,
111
+ })),
112
+ finishReason: choice?.finish_reason ?? "stop",
113
+ usage: {
114
+ promptTokens: resp.usage?.prompt_tokens ?? 0,
115
+ completionTokens: resp.usage?.completion_tokens ?? 0,
116
+ totalTokens: resp.usage?.total_tokens ?? 0,
117
+ },
118
+ model: resp.model,
119
+ };
120
+ }
121
+
122
+ async *chatStream(
123
+ req: ChatRequest,
124
+ signal?: AbortSignal,
125
+ ): AsyncGenerator<StreamEvent> {
126
+ const messages = toOpenAIMessages(req.messages);
127
+ const tools = req.tools?.map((t) => ({
128
+ type: "function" as const,
129
+ function: {
130
+ name: t.function.name,
131
+ description: t.function.description,
132
+ parameters: t.function.parameters as unknown as Record<string, unknown>,
133
+ },
134
+ }));
135
+
136
+ const stream = await this.client.chat.completions.create(
137
+ {
138
+ model: req.model ?? this.defaultModel,
139
+ messages,
140
+ max_tokens: req.maxTokens ?? this.defaultMaxTokens,
141
+ stream: true,
142
+ stream_options: { include_usage: true },
143
+ ...(tools?.length ? { tools } : {}),
144
+ ...(req.temperature != null ? { temperature: req.temperature } : {}),
145
+ ...(req.topP != null ? { top_p: req.topP } : {}),
146
+ ...(req.stop?.length ? { stop: req.stop } : {}),
147
+ },
148
+ { signal },
149
+ );
150
+
151
+ const toolCallBufs = new Map<
152
+ number,
153
+ { id: string; name: string; argsBuf: string }
154
+ >();
155
+ let promptTokens = 0;
156
+ let completionTokens = 0;
157
+
158
+ for await (const chunk of stream) {
159
+ if (signal?.aborted) break;
160
+
161
+ if (chunk.usage) {
162
+ promptTokens = chunk.usage.prompt_tokens;
163
+ completionTokens = chunk.usage.completion_tokens;
164
+ }
165
+
166
+ const delta = chunk.choices[0]?.delta;
167
+ if (!delta) continue;
168
+
169
+ if (delta.content) {
170
+ yield { type: "delta", delta: delta.content };
171
+ }
172
+
173
+ if (delta.tool_calls) {
174
+ for (const tc of delta.tool_calls) {
175
+ const idx = tc.index;
176
+ if (!toolCallBufs.has(idx)) {
177
+ toolCallBufs.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", argsBuf: "" });
178
+ }
179
+ const buf = toolCallBufs.get(idx)!;
180
+ if (tc.id) buf.id = tc.id;
181
+ if (tc.function?.name) buf.name = tc.function.name;
182
+ if (tc.function?.arguments) buf.argsBuf += tc.function.arguments;
183
+ }
184
+ }
185
+
186
+ const finishReason = chunk.choices[0]?.finish_reason;
187
+ if (finishReason) {
188
+ for (const [, tc] of toolCallBufs) {
189
+ yield {
190
+ type: "tool_call",
191
+ toolCall: { id: tc.id, name: tc.name, arguments: tc.argsBuf },
192
+ };
193
+ }
194
+ toolCallBufs.clear();
195
+
196
+ yield {
197
+ type: "done",
198
+ final: {
199
+ finalContent: "",
200
+ providerReasoning: "",
201
+ totalTurns: 1,
202
+ totalUsage: {
203
+ promptTokens,
204
+ completionTokens,
205
+ totalTokens: promptTokens + completionTokens,
206
+ },
207
+ messages: [],
208
+ reasoningTrace: [],
209
+ stopReason: "complete",
210
+ },
211
+ };
212
+ }
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,99 @@
1
+ import { readFile, writeFile, mkdir, rm, rename, readdir } from "node:fs/promises";
2
+ import { join, dirname, normalize, relative } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import type { MemoryProvider, MemoryEntry, Scope } from "../../memory.js";
5
+
6
+ export class FilesystemMemoryProvider implements MemoryProvider {
7
+ constructor(private root: string) {}
8
+
9
+ private scopeDir(scope: Scope): string {
10
+ return join(this.root, scope);
11
+ }
12
+
13
+ private resolvePath(scope: Scope, path: string): string {
14
+ const base = this.scopeDir(scope);
15
+ const resolved = normalize(join(base, path));
16
+ if (!resolved.startsWith(base)) {
17
+ throw new Error(`path traversal attempt: ${path}`);
18
+ }
19
+ return resolved;
20
+ }
21
+
22
+ async view(scope: Scope, path: string): Promise<string> {
23
+ const full = this.resolvePath(scope, path);
24
+ try {
25
+ return await readFile(full, "utf-8");
26
+ } catch (e) {
27
+ const err = e as NodeJS.ErrnoException;
28
+ if (err.code === "ENOENT") throw new Error(`not found: ${path}`);
29
+ throw e;
30
+ }
31
+ }
32
+
33
+ async create(scope: Scope, path: string, content: string): Promise<void> {
34
+ const full = this.resolvePath(scope, path);
35
+ if (existsSync(full)) {
36
+ throw new Error(`file already exists: ${path}`);
37
+ }
38
+ await mkdir(dirname(full), { recursive: true });
39
+ await writeFile(full, content, "utf-8");
40
+ }
41
+
42
+ async strReplace(
43
+ scope: Scope,
44
+ path: string,
45
+ oldStr: string,
46
+ newStr: string,
47
+ ): Promise<void> {
48
+ const full = this.resolvePath(scope, path);
49
+ const content = await readFile(full, "utf-8");
50
+ const count = (content.split(oldStr).length - 1);
51
+ if (count === 0) throw new Error(`string not found in ${path}`);
52
+ if (count > 1) throw new Error(`string appears ${count} times in ${path}, must appear exactly once`);
53
+ await writeFile(full, content.replace(oldStr, newStr), "utf-8");
54
+ }
55
+
56
+ async delete(scope: Scope, path: string): Promise<void> {
57
+ const full = this.resolvePath(scope, path);
58
+ await rm(full, { recursive: true, force: true });
59
+ }
60
+
61
+ async rename(scope: Scope, oldPath: string, newPath: string): Promise<void> {
62
+ const oldFull = this.resolvePath(scope, oldPath);
63
+ const newFull = this.resolvePath(scope, newPath);
64
+ await mkdir(dirname(newFull), { recursive: true });
65
+ await rename(oldFull, newFull);
66
+ }
67
+
68
+ async list(scope: Scope, path: string): Promise<string[]> {
69
+ const full = this.resolvePath(scope, path);
70
+ const base = this.scopeDir(scope);
71
+ try {
72
+ const entries = await readdir(full, { recursive: true });
73
+ return (entries as string[])
74
+ .map((e) => relative(base, join(full, e)))
75
+ .sort();
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ async search(scope: Scope, query: string): Promise<MemoryEntry[]> {
82
+ const files = await this.list(scope, "/");
83
+ const results: MemoryEntry[] = [];
84
+ const q = query.toLowerCase();
85
+
86
+ for (const file of files) {
87
+ if (!file.endsWith(".md")) continue;
88
+ try {
89
+ const content = await this.view(scope, file);
90
+ if (content.toLowerCase().includes(q)) {
91
+ results.push({ path: file, scope, content: content.slice(0, 500) });
92
+ }
93
+ } catch {
94
+ // skip unreadable files
95
+ }
96
+ }
97
+ return results;
98
+ }
99
+ }
@@ -0,0 +1,64 @@
1
+ import { readFile, writeFile, mkdir, rm, readdir } from "node:fs/promises";
2
+ import { join, normalize } from "node:path";
3
+ import type { ConversationStore } from "../../conversation-store.js";
4
+ import type { Conversation } from "../../conversation.js";
5
+
6
+ export class FilesystemConversationStore implements ConversationStore {
7
+ constructor(private root: string) {}
8
+
9
+ private filePath(id: string): string {
10
+ const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_");
11
+ const full = normalize(join(this.root, `${safe}.json`));
12
+ if (!full.startsWith(normalize(this.root))) {
13
+ throw new Error(`invalid conversation id: ${id}`);
14
+ }
15
+ return full;
16
+ }
17
+
18
+ async save(conv: Conversation): Promise<void> {
19
+ await mkdir(this.root, { recursive: true });
20
+ const path = this.filePath(conv.id);
21
+ await writeFile(path, JSON.stringify(conv, null, 2), "utf-8");
22
+ }
23
+
24
+ async load(id: string): Promise<Conversation | undefined> {
25
+ try {
26
+ const raw = await readFile(this.filePath(id), "utf-8");
27
+ const parsed = JSON.parse(raw) as Conversation;
28
+ parsed.createdAt = new Date(parsed.createdAt);
29
+ if (parsed.lastTurnAt) parsed.lastTurnAt = new Date(parsed.lastTurnAt);
30
+ return parsed;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ async list(threadId?: string): Promise<Conversation[]> {
37
+ try {
38
+ const files = await readdir(this.root);
39
+ const convs: Conversation[] = [];
40
+ for (const f of files) {
41
+ if (!f.endsWith(".json")) continue;
42
+ const id = f.slice(0, -5);
43
+ const conv = await this.load(id);
44
+ if (conv && (!threadId || conv.threadId === threadId)) {
45
+ convs.push(conv);
46
+ }
47
+ }
48
+ return convs.sort(
49
+ (a, b) =>
50
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
51
+ );
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ async delete(id: string): Promise<void> {
58
+ try {
59
+ await rm(this.filePath(id));
60
+ } catch {
61
+ // already gone
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,67 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type {
3
+ ThreadProvider,
4
+ MultiUserThreadProvider,
5
+ Thread,
6
+ ThreadStatus,
7
+ Message,
8
+ } from "../../thread.js";
9
+ import { ErrThreadAccessDenied } from "../../thread.js";
10
+
11
+ export class InMemoryThreadProvider implements MultiUserThreadProvider {
12
+ private threads = new Map<string, Thread>();
13
+
14
+ async create(projectId: string, modeId: string): Promise<Thread> {
15
+ const t: Thread = {
16
+ id: randomUUID(),
17
+ projectId,
18
+ modeId,
19
+ status: "active",
20
+ };
21
+ this.threads.set(t.id, t);
22
+ return t;
23
+ }
24
+
25
+ async createForUser(
26
+ userId: string,
27
+ projectId: string,
28
+ modeId: string,
29
+ ): Promise<Thread> {
30
+ const t: Thread = {
31
+ id: randomUUID(),
32
+ userId,
33
+ projectId,
34
+ modeId,
35
+ status: "active",
36
+ };
37
+ this.threads.set(t.id, t);
38
+ return t;
39
+ }
40
+
41
+ async get(threadId: string): Promise<Thread> {
42
+ const t = this.threads.get(threadId);
43
+ if (!t) throw new Error(`thread not found: ${threadId}`);
44
+ return t;
45
+ }
46
+
47
+ async getForUser(userId: string, threadId: string): Promise<Thread> {
48
+ const t = await this.get(threadId);
49
+ if (t.userId && t.userId !== userId) throw ErrThreadAccessDenied;
50
+ return t;
51
+ }
52
+
53
+ async listByUser(userId: string, status: ThreadStatus): Promise<Thread[]> {
54
+ return Array.from(this.threads.values()).filter(
55
+ (t) => t.userId === userId && (!status || t.status === status),
56
+ );
57
+ }
58
+
59
+ async archive(threadId: string): Promise<void> {
60
+ const t = await this.get(threadId);
61
+ t.status = "archived";
62
+ }
63
+
64
+ async sendMessage(_msg: Message): Promise<void> {
65
+ // in-memory: drop messages
66
+ }
67
+ }
@@ -0,0 +1,147 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type {
3
+ MultiUserThreadProvider,
4
+ Thread,
5
+ ThreadStatus,
6
+ Message,
7
+ } from "../../thread.js";
8
+ import { ErrThreadAccessDenied } from "../../thread.js";
9
+
10
+ interface BetterSqlite3Database {
11
+ prepare(sql: string): BetterSqlite3Statement;
12
+ exec(sql: string): void;
13
+ pragma(pragma: string): unknown;
14
+ }
15
+
16
+ interface BetterSqlite3Statement {
17
+ run(...params: unknown[]): { changes: number };
18
+ get(...params: unknown[]): unknown;
19
+ all(...params: unknown[]): unknown[];
20
+ }
21
+
22
+ // Import dynamically to allow the package to be optional
23
+ async function openDatabase(path: string): Promise<BetterSqlite3Database> {
24
+ const { default: Database } = await import("better-sqlite3") as { default: new (path: string) => BetterSqlite3Database };
25
+ const db = new Database(path);
26
+ db.pragma("journal_mode = WAL");
27
+ db.pragma("foreign_keys = ON");
28
+ return db;
29
+ }
30
+
31
+ const SCHEMA = `
32
+ CREATE TABLE IF NOT EXISTS threads (
33
+ id TEXT PRIMARY KEY,
34
+ user_id TEXT,
35
+ project_id TEXT NOT NULL,
36
+ mode_id TEXT NOT NULL,
37
+ status TEXT NOT NULL DEFAULT 'active',
38
+ parent_id TEXT,
39
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS thread_messages (
43
+ id TEXT PRIMARY KEY,
44
+ from_thread_id TEXT NOT NULL,
45
+ to_thread_id TEXT NOT NULL,
46
+ content TEXT NOT NULL,
47
+ sent_at TEXT NOT NULL DEFAULT (datetime('now')),
48
+ FOREIGN KEY (to_thread_id) REFERENCES threads(id) ON DELETE CASCADE
49
+ );
50
+ `;
51
+
52
+ interface ThreadRow {
53
+ id: string;
54
+ user_id: string | null;
55
+ project_id: string;
56
+ mode_id: string;
57
+ status: string;
58
+ parent_id: string | null;
59
+ }
60
+
61
+ function rowToThread(row: ThreadRow): Thread {
62
+ return {
63
+ id: row.id,
64
+ userId: row.user_id ?? undefined,
65
+ projectId: row.project_id,
66
+ modeId: row.mode_id,
67
+ status: row.status as ThreadStatus,
68
+ parentId: row.parent_id ?? undefined,
69
+ };
70
+ }
71
+
72
+ export class SqliteThreadProvider implements MultiUserThreadProvider {
73
+ private db!: BetterSqlite3Database;
74
+ private ready: Promise<void>;
75
+
76
+ constructor(dbPath: string) {
77
+ this.ready = openDatabase(dbPath).then((db) => {
78
+ this.db = db;
79
+ db.exec(SCHEMA);
80
+ });
81
+ }
82
+
83
+ private async ensureReady(): Promise<void> {
84
+ await this.ready;
85
+ }
86
+
87
+ async create(projectId: string, modeId: string): Promise<Thread> {
88
+ await this.ensureReady();
89
+ const id = randomUUID();
90
+ this.db.prepare(
91
+ `INSERT INTO threads (id, project_id, mode_id) VALUES (?, ?, ?)`,
92
+ ).run(id, projectId, modeId);
93
+ return this.get(id);
94
+ }
95
+
96
+ async createForUser(
97
+ userId: string,
98
+ projectId: string,
99
+ modeId: string,
100
+ ): Promise<Thread> {
101
+ await this.ensureReady();
102
+ const id = randomUUID();
103
+ this.db.prepare(
104
+ `INSERT INTO threads (id, user_id, project_id, mode_id) VALUES (?, ?, ?, ?)`,
105
+ ).run(id, userId, projectId, modeId);
106
+ return this.get(id);
107
+ }
108
+
109
+ async get(threadId: string): Promise<Thread> {
110
+ await this.ensureReady();
111
+ const row = this.db
112
+ .prepare(`SELECT * FROM threads WHERE id = ?`)
113
+ .get(threadId) as ThreadRow | undefined;
114
+ if (!row) throw new Error(`thread not found: ${threadId}`);
115
+ return rowToThread(row);
116
+ }
117
+
118
+ async getForUser(userId: string, threadId: string): Promise<Thread> {
119
+ const t = await this.get(threadId);
120
+ if (t.userId && t.userId !== userId) throw ErrThreadAccessDenied;
121
+ return t;
122
+ }
123
+
124
+ async listByUser(userId: string, status: ThreadStatus): Promise<Thread[]> {
125
+ await this.ensureReady();
126
+ const rows = this.db
127
+ .prepare(
128
+ `SELECT * FROM threads WHERE user_id = ? ${status ? "AND status = ?" : ""} ORDER BY created_at DESC`,
129
+ )
130
+ .all(...(status ? [userId, status] : [userId])) as ThreadRow[];
131
+ return rows.map(rowToThread);
132
+ }
133
+
134
+ async archive(threadId: string): Promise<void> {
135
+ await this.ensureReady();
136
+ this.db
137
+ .prepare(`UPDATE threads SET status = 'archived' WHERE id = ?`)
138
+ .run(threadId);
139
+ }
140
+
141
+ async sendMessage(msg: Message): Promise<void> {
142
+ await this.ensureReady();
143
+ this.db.prepare(
144
+ `INSERT INTO thread_messages (id, from_thread_id, to_thread_id, content) VALUES (?, ?, ?, ?)`,
145
+ ).run(msg.id ?? randomUUID(), msg.fromThreadId, msg.toThreadId, msg.content);
146
+ }
147
+ }
@@ -0,0 +1,26 @@
1
+ import type { Tokenizer } from "../../tokenizer.js";
2
+ import { ClaudeTokenizer } from "./byte.js";
3
+ import { TiktokenTokenizer } from "./tiktoken.js";
4
+
5
+ const cache = new Map<string, Tokenizer>();
6
+
7
+ export function autoTokenizer(model: string): Tokenizer {
8
+ const cached = cache.get(model);
9
+ if (cached) return cached;
10
+
11
+ const lower = model.toLowerCase();
12
+ let tok: Tokenizer;
13
+
14
+ if (lower.includes("gpt-4o") || lower.startsWith("o1") || lower.startsWith("o3") || lower.startsWith("gpt-5")) {
15
+ tok = new TiktokenTokenizer("o200k_base");
16
+ } else if (lower.includes("gpt-4") || lower.includes("gpt-3.5")) {
17
+ tok = new TiktokenTokenizer("cl100k_base");
18
+ } else {
19
+ tok = new ClaudeTokenizer();
20
+ }
21
+
22
+ cache.set(model, tok);
23
+ return tok;
24
+ }
25
+
26
+ export { ClaudeTokenizer, TiktokenTokenizer };
@@ -0,0 +1,27 @@
1
+ import type { Tokenizer } from "../../tokenizer.js";
2
+
3
+ export class ByteTokenizer implements Tokenizer {
4
+ private charsPerToken: number;
5
+
6
+ constructor(charsPerToken = 4.0) {
7
+ this.charsPerToken = charsPerToken;
8
+ }
9
+
10
+ count(text: string): number {
11
+ return Math.ceil([...text].length / this.charsPerToken);
12
+ }
13
+
14
+ encode(_text: string): number[] { return []; }
15
+ decode(_tokens: number[]): string { return ""; }
16
+ }
17
+
18
+ export class ClaudeTokenizer implements Tokenizer {
19
+ count(text: string): number {
20
+ const words = text.split(/\s+/).filter(Boolean).length;
21
+ const specials = (text.match(/[{}\[\]<>()]/g) ?? []).length;
22
+ return Math.ceil(words * 1.3 + specials * 0.5);
23
+ }
24
+
25
+ encode(_text: string): number[] { return []; }
26
+ decode(_tokens: number[]): string { return ""; }
27
+ }