@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,91 @@
1
+ import type { Tokenizer } from "../../tokenizer.js";
2
+ import { ClaudeTokenizer } from "./byte.js";
3
+
4
+ // Lazy-loaded tiktoken encoding. Falls back to ClaudeTokenizer if not installed.
5
+ type TiktokenEncoding = {
6
+ encode(text: string): Uint32Array;
7
+ decode(tokens: Uint32Array): string;
8
+ };
9
+
10
+ type TiktokenModule = {
11
+ getEncoding(name: string): TiktokenEncoding;
12
+ };
13
+
14
+ let cl100k: TiktokenEncoding | undefined;
15
+ let o200k: TiktokenEncoding | undefined;
16
+ let loadAttempted = false;
17
+ let tiktokenMod: TiktokenModule | undefined;
18
+
19
+ async function loadTiktoken(): Promise<TiktokenModule | undefined> {
20
+ if (loadAttempted) return tiktokenMod;
21
+ loadAttempted = true;
22
+ try {
23
+ // Dynamic import — tiktoken is an optional peer dependency.
24
+ // Use a variable to prevent tsc from resolving the literal "tiktoken" module.
25
+ const modName = "tiktoken";
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27
+ const mod = await import(/* @vite-ignore */ modName as string) as unknown;
28
+ tiktokenMod = mod as TiktokenModule;
29
+ } catch {
30
+ // tiktoken not installed — fall back to heuristic
31
+ }
32
+ return tiktokenMod;
33
+ }
34
+
35
+ export type TiktokenEncName = "cl100k_base" | "o200k_base";
36
+
37
+ export class TiktokenTokenizer implements Tokenizer {
38
+ private encName: TiktokenEncName;
39
+ private enc?: TiktokenEncoding;
40
+ private fallback = new ClaudeTokenizer();
41
+ private initPromise: Promise<void>;
42
+
43
+ constructor(encName: TiktokenEncName = "cl100k_base") {
44
+ this.encName = encName;
45
+ this.initPromise = loadTiktoken().then((mod) => {
46
+ if (!mod) return;
47
+ try {
48
+ if (this.encName === "cl100k_base") {
49
+ cl100k ??= mod.getEncoding("cl100k_base");
50
+ this.enc = cl100k;
51
+ } else {
52
+ o200k ??= mod.getEncoding("o200k_base");
53
+ this.enc = o200k;
54
+ }
55
+ } catch {
56
+ // encoding load failed
57
+ }
58
+ });
59
+ }
60
+
61
+ async ready(): Promise<void> {
62
+ await this.initPromise;
63
+ }
64
+
65
+ count(text: string): number {
66
+ if (!this.enc) return this.fallback.count(text);
67
+ try {
68
+ return this.enc.encode(text).length;
69
+ } catch {
70
+ return this.fallback.count(text);
71
+ }
72
+ }
73
+
74
+ encode(text: string): number[] {
75
+ if (!this.enc) return [];
76
+ try {
77
+ return Array.from(this.enc.encode(text));
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ decode(tokens: number[]): string {
84
+ if (!this.enc) return "";
85
+ try {
86
+ return this.enc.decode(new Uint32Array(tokens));
87
+ } catch {
88
+ return "";
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,7 @@
1
+ export interface ReasoningStep {
2
+ id: string;
3
+ type: "thinking" | "action" | "result";
4
+ title: string;
5
+ content?: string;
6
+ details?: string[];
7
+ }
@@ -0,0 +1,32 @@
1
+ export interface RuleMatcher {
2
+ match(ruleContent: string, input: Record<string, unknown>): boolean;
3
+ }
4
+
5
+ export type RuleMatcherFunc = (
6
+ ruleContent: string,
7
+ input: Record<string, unknown>,
8
+ ) => boolean;
9
+
10
+ export function ruleMatcherFromFn(fn: RuleMatcherFunc): RuleMatcher {
11
+ return { match: fn };
12
+ }
13
+
14
+ export function prefixMatcher(argKey: string): RuleMatcher {
15
+ return {
16
+ match(rule: string, input: Record<string, unknown>): boolean {
17
+ if (!rule) return true;
18
+ const val = input[argKey];
19
+ return typeof val === "string" && val.startsWith(rule);
20
+ },
21
+ };
22
+ }
23
+
24
+ export function exactMatcher(): RuleMatcher {
25
+ return {
26
+ match(rule: string, input: Record<string, unknown>): boolean {
27
+ if (!rule) return true;
28
+ const primary = Object.values(input)[0];
29
+ return String(primary) === rule;
30
+ },
31
+ };
32
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,416 @@
1
+ import type { Engine } from "./engine.js";
2
+ import { hasLLM, hasMemory, hasPrompt, hasBudget, hasModes, hasTools } from "./engine.js";
3
+ import type { Conversation } from "./conversation.js";
4
+ import {
5
+ isCold,
6
+ appendUser,
7
+ appendAssistant,
8
+ incrementTurn,
9
+ } from "./conversation.js";
10
+ import type { Tokenizer } from "./tokenizer.js";
11
+ import { HeuristicTokenizer } from "./tokenizer.js";
12
+ import type { ConversationStore } from "./conversation-store.js";
13
+ import type { SafetyFilter } from "./safety.js";
14
+ import type { Compactor } from "./compaction.js";
15
+ import { enforceWithCompaction } from "./compaction.js";
16
+ import type { SessionContextProvider } from "./session-context.js";
17
+ import { formatSessionContext } from "./session-context.js";
18
+ import type { PermissionEngine } from "./permissions.js";
19
+ import type { InterruptGate } from "./interrupt.js";
20
+ import type { MemoryRoot } from "./memory.js";
21
+ import { DEFAULT_MEMORY_ROOTS } from "./memory.js";
22
+ import type { Span } from "./tracing.js";
23
+ import { Tracer } from "./tracing.js";
24
+ import type { AgentLoopResult } from "./agent-loop.js";
25
+ import { runAgentLoopWithEngine } from "./agent-loop.js";
26
+ import type { StreamEvent } from "./streaming.js";
27
+ import type { ChatMessage } from "./llm.js";
28
+ import { ToolDispatcher } from "./dispatch.js";
29
+ import { systemReminder, joinSystemReminders } from "./system-reminder.js";
30
+
31
+ export interface RuntimeResult {
32
+ response: string;
33
+ turns: number;
34
+ usage: { promptTokens: number; completionTokens: number; totalTokens: number };
35
+ stopReason: string;
36
+ trace: Span[];
37
+ traceId: string;
38
+ memoryRead: boolean;
39
+ memoryWritten: string[];
40
+ warnings: string[];
41
+ startedAt: Date;
42
+ completedAt?: Date;
43
+ }
44
+
45
+ export interface PlanController {
46
+ onStateChanged?: (ev: { state: string; plan?: string; reason?: string }) => void;
47
+ }
48
+
49
+ export class Runtime {
50
+ private engine: Engine;
51
+ private mode = "";
52
+ private model = "";
53
+ private safety?: SafetyFilter;
54
+ private tokenizer: Tokenizer = new HeuristicTokenizer();
55
+ private store?: ConversationStore;
56
+ private sessionContext?: SessionContextProvider;
57
+ private compactor?: Compactor;
58
+ private maxMemoryTokens = 0;
59
+ private memoryRoots: MemoryRoot[] = DEFAULT_MEMORY_ROOTS;
60
+ private thinkingBudget = 0;
61
+ private interruptGate?: InterruptGate;
62
+ private permissions?: PermissionEngine;
63
+ private planController?: PlanController;
64
+ private _emit?: (ev: StreamEvent) => void;
65
+
66
+ constructor(engine: Engine) {
67
+ this.engine = engine;
68
+ }
69
+
70
+ withMode(modeId: string): this { this.mode = modeId; return this; }
71
+ withModel(model: string): this { this.model = model; return this; }
72
+ withSafety(s: SafetyFilter): this { this.safety = s; return this; }
73
+ withPermissions(e: PermissionEngine): this { this.permissions = e; return this; }
74
+ withTokenizer(t: Tokenizer): this { this.tokenizer = t; return this; }
75
+ withConversationStore(s: ConversationStore): this { this.store = s; return this; }
76
+ withSessionContext(p: SessionContextProvider): this { this.sessionContext = p; return this; }
77
+ withCompactor(c: Compactor): this { this.compactor = c; return this; }
78
+ withMaxMemoryTokens(n: number): this { this.maxMemoryTokens = n; return this; }
79
+ withMemoryRoots(...roots: MemoryRoot[]): this { this.memoryRoots = roots; return this; }
80
+ withThinkingBudget(tokens: number): this { this.thinkingBudget = tokens; return this; }
81
+ withInterruptGate(gate: InterruptGate): this { this.interruptGate = gate; return this; }
82
+ withPlanController(pc: PlanController): this {
83
+ const prev = pc.onStateChanged;
84
+ pc.onStateChanged = (ev) => {
85
+ prev?.(ev);
86
+ const planMode = { state: ev.state as "entered" | "exited", plan: ev.plan, reason: ev.reason };
87
+ this._emit?.({ type: "plan_mode_changed", planMode });
88
+ };
89
+ this.planController = pc;
90
+ return this;
91
+ }
92
+
93
+ async run(
94
+ conv: Conversation,
95
+ userMessage: string,
96
+ signal?: AbortSignal,
97
+ ): Promise<RuntimeResult> {
98
+ if (!hasLLM(this.engine)) throw new Error("runtime: no LLM provider");
99
+ if (!conv) throw new Error("runtime: conversation is required");
100
+
101
+ const tracer = new Tracer();
102
+ const rr: RuntimeResult = {
103
+ response: "",
104
+ turns: 0,
105
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
106
+ stopReason: "",
107
+ trace: [],
108
+ traceId: tracer.traceId(),
109
+ memoryRead: false,
110
+ memoryWritten: [],
111
+ warnings: [],
112
+ startedAt: new Date(),
113
+ };
114
+
115
+ appendUser(conv, userMessage);
116
+
117
+ // Orientation
118
+ if (isCold(conv)) {
119
+ await this.orientation(conv, userMessage, rr, signal);
120
+ } else {
121
+ await this.warmRefresh(conv, userMessage, signal);
122
+ }
123
+
124
+ if (signal?.aborted) return rr;
125
+
126
+ // Preparation
127
+ await this.preparation(conv, rr, signal);
128
+
129
+ if (signal?.aborted) return rr;
130
+
131
+ // Execution
132
+ const loopResult = await this.execution(conv, rr, signal);
133
+
134
+ if (loopResult?.finalContent) {
135
+ appendAssistant(conv, loopResult.finalContent);
136
+ }
137
+
138
+ if (signal?.aborted) return rr;
139
+
140
+ // Closure (no-op in v3 — memory writes go through the tool)
141
+
142
+ incrementTurn(conv);
143
+
144
+ if (this.store) {
145
+ try {
146
+ await this.store.save(conv, signal);
147
+ } catch (e) {
148
+ rr.warnings.push(`save conversation: ${(e as Error).message}`);
149
+ }
150
+ }
151
+
152
+ rr.completedAt = new Date();
153
+ rr.trace = tracer.allSpans();
154
+ return rr;
155
+ }
156
+
157
+ async *runStream(
158
+ conv: Conversation,
159
+ userMessage: string,
160
+ signal?: AbortSignal,
161
+ ): AsyncGenerator<StreamEvent> {
162
+ const out: StreamEvent[] = [];
163
+ let resolve: (() => void) | null = null;
164
+ let done = false;
165
+
166
+ this._emit = (ev: StreamEvent) => {
167
+ out.push(ev);
168
+ resolve?.();
169
+ };
170
+
171
+ const runPromise = this.run(conv, userMessage, signal).then(
172
+ (result) => {
173
+ if (result.response) {
174
+ for (const chunk of chunkBySentence(result.response)) {
175
+ out.push({ type: "delta", delta: chunk });
176
+ }
177
+ }
178
+ out.push({ type: "done" });
179
+ done = true;
180
+ resolve?.();
181
+ },
182
+ (err: Error) => {
183
+ out.push({ type: "error", error: err });
184
+ out.push({ type: "done" });
185
+ done = true;
186
+ resolve?.();
187
+ },
188
+ ).finally(() => {
189
+ this._emit = undefined;
190
+ });
191
+
192
+ while (!done || out.length > 0) {
193
+ if (out.length === 0) {
194
+ await new Promise<void>((r) => { resolve = r; });
195
+ resolve = null;
196
+ continue;
197
+ }
198
+ yield out.shift()!;
199
+ }
200
+
201
+ await runPromise;
202
+ }
203
+
204
+ private async orientation(
205
+ conv: Conversation,
206
+ userMessage: string,
207
+ rr: RuntimeResult,
208
+ signal?: AbortSignal,
209
+ ): Promise<void> {
210
+ if (hasMemory(this.engine) && hasPrompt(this.engine)) {
211
+ const scopes = [...new Set(this.memoryRoots.map((r) => r.scope))];
212
+ const sections: string[] = [];
213
+ for (const scope of scopes) {
214
+ try {
215
+ const index = await this.engine.memory!.view(scope, "MEMORY.md", signal);
216
+ if (index.trim()) sections.push(index.trim());
217
+ } catch {
218
+ // no MEMORY.md yet — that's fine
219
+ }
220
+ }
221
+ if (sections.length > 0) {
222
+ let section = sections.join("\n\n");
223
+ if (this.maxMemoryTokens > 0 && this.tokenizer.count(section) > this.maxMemoryTokens) {
224
+ section = evictMemoryToTokenBudget(section, this.maxMemoryTokens, this.tokenizer);
225
+ }
226
+ this.engine.prompt!.set("memory", section);
227
+ rr.memoryRead = true;
228
+ conv.memoryRead = true;
229
+ }
230
+ }
231
+ await this.buildSessionLayer(conv, userMessage, signal);
232
+ }
233
+
234
+ private async warmRefresh(
235
+ conv: Conversation,
236
+ userMessage: string,
237
+ signal?: AbortSignal,
238
+ ): Promise<void> {
239
+ await this.buildSessionLayer(conv, userMessage, signal);
240
+ }
241
+
242
+ private async buildSessionLayer(
243
+ conv: Conversation,
244
+ _userMessage: string,
245
+ signal?: AbortSignal,
246
+ ): Promise<void> {
247
+ if (!hasPrompt(this.engine)) return;
248
+ if (!this.sessionContext) {
249
+ this.engine.prompt!.clear("session");
250
+ return;
251
+ }
252
+ try {
253
+ const sc = await this.sessionContext.get(conv, signal);
254
+ if (!sc) { this.engine.prompt!.clear("session"); return; }
255
+ const rendered = formatSessionContext(sc);
256
+ if (!rendered) { this.engine.prompt!.clear("session"); return; }
257
+ this.engine.prompt!.set("session", rendered);
258
+ } catch {
259
+ this.engine.prompt!.clear("session");
260
+ }
261
+ }
262
+
263
+ private async preparation(
264
+ conv: Conversation,
265
+ rr: RuntimeResult,
266
+ signal?: AbortSignal,
267
+ ): Promise<void> {
268
+ if (!hasBudget(this.engine) || !hasPrompt(this.engine)) return;
269
+ const memoryTokens = this.tokenizer.count(
270
+ this.engine.prompt!.get("memory"),
271
+ );
272
+ const compResult = await enforceWithCompaction(
273
+ this.engine.budget!,
274
+ this.compactor,
275
+ conv,
276
+ memoryTokens,
277
+ this.tokenizer,
278
+ signal,
279
+ );
280
+ const enforce = compResult.enforcementResult;
281
+ if (enforce.overflowTokens > 0) {
282
+ if (enforce.truncatedHistory) {
283
+ rr.warnings.push(`budget: dropped ${enforce.historyDropped} messages`);
284
+ }
285
+ if (enforce.stillOverflow) {
286
+ rr.warnings.push("budget: still over after enforcement");
287
+ }
288
+ }
289
+ if (compResult.summary) {
290
+ this.engine.prompt!.append(
291
+ "memory",
292
+ `\n\nContext summary (from earlier turns):\n${compResult.summary}`,
293
+ );
294
+ }
295
+ if (this._emit && enforce.truncatedHistory) {
296
+ this._emit({
297
+ type: "compaction",
298
+ compaction: {
299
+ messagesDropped: enforce.historyDropped,
300
+ overflowTokens: enforce.overflowTokens,
301
+ summary: compResult.summary,
302
+ },
303
+ });
304
+ }
305
+ }
306
+
307
+ private async execution(
308
+ conv: Conversation,
309
+ rr: RuntimeResult,
310
+ signal?: AbortSignal,
311
+ ): Promise<AgentLoopResult | undefined> {
312
+ let systemPrompt = "";
313
+ if (hasPrompt(this.engine)) {
314
+ if (this.mode && hasModes(this.engine)) {
315
+ try {
316
+ const mode = await this.engine.modes!.get(this.mode, signal);
317
+ this.engine.prompt!.set("mode", mode.promptContent ?? "");
318
+ } catch {}
319
+ }
320
+ systemPrompt = this.engine.prompt!.build();
321
+ }
322
+
323
+ // Collect dynamic reminders
324
+ if (hasTools(this.engine)) {
325
+ const blocks = await this.engine.tools!.collectDynamicReminders(signal);
326
+ if (blocks.length > 0) {
327
+ const wrapped = blocks.map(systemReminder).filter(Boolean);
328
+ const joined = joinSystemReminders(...wrapped);
329
+ if (joined) {
330
+ systemPrompt = systemPrompt ? systemPrompt + "\n\n" + joined : joined;
331
+ }
332
+ }
333
+ }
334
+
335
+ const cfg = {
336
+ systemPrompt,
337
+ model: this.model,
338
+ permissions: this.permissions,
339
+ maxTurns: 50,
340
+ onToolCall: (call: { name: string; arguments: string; id: string }) => {
341
+ if (this.safety) {
342
+ const verdict = this.safety.inspect(call);
343
+ return verdict.decision !== "block";
344
+ }
345
+ return true;
346
+ },
347
+ };
348
+
349
+ const loopResult = await runAgentLoopWithEngine(
350
+ this.engine,
351
+ this.mode,
352
+ cfg,
353
+ conv.messages,
354
+ signal,
355
+ );
356
+
357
+ rr.turns += loopResult.totalTurns;
358
+ rr.usage.promptTokens += loopResult.totalUsage.promptTokens;
359
+ rr.usage.completionTokens += loopResult.totalUsage.completionTokens;
360
+ rr.usage.totalTokens += loopResult.totalUsage.totalTokens;
361
+ rr.stopReason = loopResult.stopReason;
362
+ rr.response = loopResult.finalContent;
363
+
364
+ conv.messages = loopResult.messages;
365
+
366
+ return loopResult;
367
+ }
368
+ }
369
+
370
+ function evictMemoryToTokenBudget(
371
+ content: string,
372
+ maxTokens: number,
373
+ tok: Tokenizer,
374
+ ): string {
375
+ const paragraphs = content.trim().split(/\n\n+/);
376
+ const kept: string[] = [];
377
+ let used = 0;
378
+
379
+ for (let i = paragraphs.length - 1; i >= 0; i--) {
380
+ const p = paragraphs[i]!.trim();
381
+ if (!p) continue;
382
+ const tokens = tok.count(p);
383
+ if (used + tokens > maxTokens && kept.length > 0) break;
384
+ kept.unshift(p);
385
+ used += tokens;
386
+ }
387
+
388
+ if (kept.length < paragraphs.length) {
389
+ kept.unshift("[Earlier memory entries omitted — token budget exceeded]");
390
+ }
391
+ return kept.join("\n\n");
392
+ }
393
+
394
+ function chunkBySentence(text: string): string[] {
395
+ if (!text) return [];
396
+ const chunks: string[] = [];
397
+ let current = "";
398
+ for (let i = 0; i < text.length; i++) {
399
+ const ch = text[i]!;
400
+ current += ch;
401
+ if (ch === "." || ch === "!" || ch === "?" || ch === "\n") {
402
+ const next = text[i + 1];
403
+ if (!next || next === " " || next === "\n" || next === "\t") {
404
+ const chunk = current.trim();
405
+ if (chunk) chunks.push(chunk + " ");
406
+ current = "";
407
+ }
408
+ }
409
+ }
410
+ if (current.trim()) chunks.push(current.trim());
411
+ return chunks;
412
+ }
413
+
414
+ export function newRuntime(engine: Engine): Runtime {
415
+ return new Runtime(engine);
416
+ }
package/src/safety.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { ToolCallEntry } from "./llm.js";
2
+
3
+ export type SafetyDecision = "allow" | "block" | "transform";
4
+
5
+ export interface SafetyVerdict {
6
+ decision: SafetyDecision;
7
+ reason?: string;
8
+ newArgs?: string;
9
+ }
10
+
11
+ export interface SafetyFilter {
12
+ inspect(call: ToolCallEntry, signal?: AbortSignal): SafetyVerdict;
13
+ }
14
+
15
+ const DANGEROUS_PATTERNS = [
16
+ /rm\s+-rf\s+\/[^a-z]/i,
17
+ /curl.*\|\s*(?:bash|sh|python)/i,
18
+ /wget.*\|\s*(?:bash|sh|python)/i,
19
+ /mkfs/i,
20
+ /dd\s+if=/i,
21
+ />\s*\/dev\/sd[a-z]/i,
22
+ ];
23
+
24
+ const SECRET_PATTERNS = [
25
+ /sk-[a-zA-Z0-9]{20,}/,
26
+ /AKIA[0-9A-Z]{16}/,
27
+ /ghp_[a-zA-Z0-9]{36}/,
28
+ /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/,
29
+ /password\s*[:=]\s*['"][^'"]{8,}['"]/i,
30
+ ];
31
+
32
+ export class DefaultSafetyFilter implements SafetyFilter {
33
+ inspect(call: ToolCallEntry): SafetyVerdict {
34
+ const args = call.arguments;
35
+
36
+ for (const pattern of DANGEROUS_PATTERNS) {
37
+ if (pattern.test(args)) {
38
+ return {
39
+ decision: "block",
40
+ reason: `dangerous command pattern detected: ${pattern.source}`,
41
+ };
42
+ }
43
+ }
44
+
45
+ for (const pattern of SECRET_PATTERNS) {
46
+ if (pattern.test(args)) {
47
+ return {
48
+ decision: "block",
49
+ reason: "potential secret leak detected in tool arguments",
50
+ };
51
+ }
52
+ }
53
+
54
+ return { decision: "allow" };
55
+ }
56
+ }
package/src/sandbox.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface SandboxExecResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+
7
+ export interface SandboxDriver {
8
+ exec(
9
+ sandboxId: string,
10
+ command: string,
11
+ args?: string[],
12
+ signal?: AbortSignal,
13
+ ): Promise<SandboxExecResult>;
14
+
15
+ readFile(sandboxId: string, path: string, signal?: AbortSignal): Promise<string>;
16
+
17
+ writeFile(
18
+ sandboxId: string,
19
+ path: string,
20
+ content: string,
21
+ signal?: AbortSignal,
22
+ ): Promise<void>;
23
+ }
@@ -0,0 +1,33 @@
1
+ import type { Conversation } from "./conversation.js";
2
+
3
+ export interface SessionContext {
4
+ currentTime?: Date;
5
+ timezone?: string;
6
+ userId?: string;
7
+ username?: string;
8
+ projectId?: string;
9
+ activeArtifactId?: string;
10
+ extras?: Record<string, string>;
11
+ }
12
+
13
+ export function formatSessionContext(sc: SessionContext): string {
14
+ const parts: string[] = [];
15
+ if (sc.currentTime) {
16
+ parts.push(`Current time: ${sc.currentTime.toISOString()}`);
17
+ }
18
+ if (sc.timezone) parts.push(`Timezone: ${sc.timezone}`);
19
+ if (sc.userId) parts.push(`User ID: ${sc.userId}`);
20
+ if (sc.username) parts.push(`Username: ${sc.username}`);
21
+ if (sc.projectId) parts.push(`Project: ${sc.projectId}`);
22
+ if (sc.activeArtifactId) parts.push(`Active artifact: ${sc.activeArtifactId}`);
23
+ if (sc.extras) {
24
+ for (const [k, v] of Object.entries(sc.extras)) {
25
+ parts.push(`${k}: ${v}`);
26
+ }
27
+ }
28
+ return parts.join("\n");
29
+ }
30
+
31
+ export interface SessionContextProvider {
32
+ get(conv: Conversation, signal?: AbortSignal): Promise<SessionContext | undefined>;
33
+ }
@@ -0,0 +1,21 @@
1
+ export interface Skill {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ promptContent?: string;
6
+ sourceKind?: string;
7
+ allowedTools?: string[];
8
+ }
9
+
10
+ export interface SkillSource {
11
+ sourceName(): string;
12
+ list(signal?: AbortSignal): Promise<Skill[]>;
13
+ }
14
+
15
+ export interface SkillBashExecutor {
16
+ execute(
17
+ command: string,
18
+ allowedTools: string[],
19
+ signal?: AbortSignal,
20
+ ): Promise<string>;
21
+ }