@odla-ai/ai 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.
package/dist/index.js ADDED
@@ -0,0 +1,1122 @@
1
+ import {
2
+ AuthError,
3
+ CapabilityError,
4
+ ConfigError,
5
+ ContextWindowError,
6
+ InvalidRequestError,
7
+ OdlaAIError,
8
+ ProviderError,
9
+ RateLimitError,
10
+ addUsage,
11
+ blocksOf,
12
+ emptyUsage,
13
+ extractText,
14
+ extractToolUses,
15
+ isAudioBlock,
16
+ isDocumentBlock,
17
+ isImageBlock,
18
+ isTextBlock,
19
+ isThinkingBlock,
20
+ isToolResultBlock,
21
+ isToolUseBlock,
22
+ normalizeError
23
+ } from "./chunk-LANGYP65.js";
24
+
25
+ // src/shape/capabilities.ts
26
+ function chatCapabilities(overrides = {}) {
27
+ return {
28
+ kind: "chat",
29
+ textIn: true,
30
+ imageIn: true,
31
+ audioIn: false,
32
+ documentIn: false,
33
+ toolUse: true,
34
+ thinking: false,
35
+ effort: false,
36
+ streaming: true,
37
+ structuredOutput: true,
38
+ webSearch: false,
39
+ ...overrides
40
+ };
41
+ }
42
+ function validateRequest(spec, req) {
43
+ const caps = spec.capabilities;
44
+ const fail = (what) => {
45
+ throw new CapabilityError(
46
+ `Model "${spec.id}" (${spec.provider}) does not support ${what}.`
47
+ );
48
+ };
49
+ if (caps.kind !== "chat") fail(`chat requests (it is a "${caps.kind}" model)`);
50
+ for (const msg of req.messages) {
51
+ for (const block of blocksOf(msg.content)) {
52
+ if (block.type === "image" && !caps.imageIn) fail("image input");
53
+ if (block.type === "audio" && !caps.audioIn) fail("audio input");
54
+ if (block.type === "document" && !caps.documentIn) fail("document (PDF) input");
55
+ }
56
+ }
57
+ if (req.tools && req.tools.length > 0 && !caps.toolUse) fail("tool use");
58
+ if (req.webSearch && !caps.webSearch) fail("web search");
59
+ if (req.responseFormat && !caps.structuredOutput) fail("structured output");
60
+ if (req.effort && !caps.effort) fail("the effort parameter");
61
+ if (req.thinking === "adaptive" && !caps.thinking) fail("thinking");
62
+ }
63
+
64
+ // src/catalog/anthropic.ts
65
+ var FRONTIER = () => chatCapabilities({
66
+ documentIn: true,
67
+ // PDF
68
+ thinking: true,
69
+ // adaptive
70
+ effort: true,
71
+ // output_config.effort
72
+ webSearch: true,
73
+ // web_search_20260209
74
+ audioIn: false
75
+ // Claude has no audio input
76
+ });
77
+ var ANTHROPIC_MODELS = [
78
+ {
79
+ id: "claude-opus-4-8",
80
+ provider: "anthropic",
81
+ nativeId: "claude-opus-4-8",
82
+ capabilities: FRONTIER(),
83
+ contextWindow: 1e6,
84
+ maxOutput: 128e3
85
+ },
86
+ {
87
+ id: "claude-sonnet-5",
88
+ provider: "anthropic",
89
+ nativeId: "claude-sonnet-5",
90
+ capabilities: FRONTIER(),
91
+ contextWindow: 1e6,
92
+ maxOutput: 128e3
93
+ },
94
+ {
95
+ id: "claude-fable-5",
96
+ provider: "anthropic",
97
+ nativeId: "claude-fable-5",
98
+ capabilities: FRONTIER(),
99
+ contextWindow: 1e6,
100
+ maxOutput: 128e3
101
+ },
102
+ {
103
+ // Haiku 4.5: no effort param (errors), web search needs Sonnet-4.6+, so both off.
104
+ id: "claude-haiku-4-5",
105
+ provider: "anthropic",
106
+ nativeId: "claude-haiku-4-5",
107
+ capabilities: chatCapabilities({
108
+ documentIn: true,
109
+ thinking: false,
110
+ effort: false,
111
+ webSearch: false,
112
+ audioIn: false
113
+ }),
114
+ contextWindow: 2e5,
115
+ maxOutput: 64e3
116
+ }
117
+ ];
118
+
119
+ // src/catalog/openai.ts
120
+ var OPENAI_MODELS = [
121
+ {
122
+ id: "gpt-5.5",
123
+ provider: "openai",
124
+ nativeId: "gpt-5.5",
125
+ capabilities: chatCapabilities({ effort: true }),
126
+ contextWindow: 4e5
127
+ },
128
+ {
129
+ id: "gpt-5",
130
+ provider: "openai",
131
+ nativeId: "gpt-5",
132
+ capabilities: chatCapabilities({ effort: true }),
133
+ contextWindow: 4e5
134
+ },
135
+ {
136
+ id: "gpt-5-mini",
137
+ provider: "openai",
138
+ nativeId: "gpt-5-mini",
139
+ capabilities: chatCapabilities({ effort: true }),
140
+ contextWindow: 4e5
141
+ },
142
+ {
143
+ // Non-reasoning multimodal chat (image input, no effort knob).
144
+ id: "gpt-4o",
145
+ provider: "openai",
146
+ nativeId: "gpt-4o",
147
+ capabilities: chatCapabilities(),
148
+ contextWindow: 128e3
149
+ },
150
+ {
151
+ // Audio-input model line — accepts audio, not images.
152
+ id: "gpt-audio",
153
+ provider: "openai",
154
+ nativeId: "gpt-audio",
155
+ capabilities: chatCapabilities({ imageIn: false, audioIn: true }),
156
+ contextWindow: 128e3
157
+ }
158
+ ];
159
+
160
+ // src/catalog/google.ts
161
+ var GEMINI = () => chatCapabilities({
162
+ audioIn: true,
163
+ documentIn: true,
164
+ thinking: true,
165
+ effort: true,
166
+ // mapped onto thinkingConfig
167
+ webSearch: true
168
+ // googleSearch grounding
169
+ });
170
+ var GOOGLE_MODELS = [
171
+ {
172
+ id: "gemini-2.5-pro",
173
+ provider: "google",
174
+ nativeId: "gemini-2.5-pro",
175
+ capabilities: GEMINI(),
176
+ contextWindow: 1e6
177
+ },
178
+ {
179
+ id: "gemini-2.5-flash",
180
+ provider: "google",
181
+ nativeId: "gemini-2.5-flash",
182
+ capabilities: GEMINI(),
183
+ contextWindow: 1e6
184
+ },
185
+ {
186
+ id: "gemini-2.0-flash",
187
+ provider: "google",
188
+ nativeId: "gemini-2.0-flash",
189
+ capabilities: chatCapabilities({
190
+ audioIn: true,
191
+ documentIn: true,
192
+ thinking: false,
193
+ effort: false,
194
+ webSearch: true
195
+ }),
196
+ contextWindow: 1e6
197
+ }
198
+ ];
199
+
200
+ // src/catalog/index.ts
201
+ function index(specs) {
202
+ const out = {};
203
+ for (const spec of specs) out[spec.id] = spec;
204
+ return out;
205
+ }
206
+ var DEFAULT_CATALOG = index([
207
+ ...ANTHROPIC_MODELS,
208
+ ...OPENAI_MODELS,
209
+ ...GOOGLE_MODELS
210
+ ]);
211
+ function buildCatalog(base, extra = []) {
212
+ return { ...base, ...index(extra) };
213
+ }
214
+ function resolveModel(catalog, id) {
215
+ const spec = catalog[id];
216
+ if (!spec) {
217
+ const known = Object.keys(catalog).sort().join(", ");
218
+ throw new ConfigError(`Unknown model "${id}". Known models: ${known || "(none)"}.`);
219
+ }
220
+ return spec;
221
+ }
222
+ function providersInCatalog(catalog) {
223
+ const set = /* @__PURE__ */ new Set();
224
+ for (const spec of Object.values(catalog)) set.add(spec.provider);
225
+ return [...set];
226
+ }
227
+
228
+ // src/providers/registry.ts
229
+ var cache = /* @__PURE__ */ new Map();
230
+ async function getProvider(id, apiKey) {
231
+ const cacheKey = `${id}:${apiKey}`;
232
+ const existing = cache.get(cacheKey);
233
+ if (existing) return existing;
234
+ let provider;
235
+ switch (id) {
236
+ case "anthropic": {
237
+ const { AnthropicProvider } = await import("./anthropic-JXDXR7O7.js");
238
+ provider = new AnthropicProvider(apiKey);
239
+ break;
240
+ }
241
+ case "openai": {
242
+ const { OpenAIProvider } = await import("./openai-Y3OAONAU.js");
243
+ provider = new OpenAIProvider(apiKey);
244
+ break;
245
+ }
246
+ case "google": {
247
+ const { GoogleProvider } = await import("./google-3TFOFIQO.js");
248
+ provider = new GoogleProvider(apiKey);
249
+ break;
250
+ }
251
+ }
252
+ cache.set(cacheKey, provider);
253
+ return provider;
254
+ }
255
+ function registerProvider(id, apiKey, provider) {
256
+ cache.set(`${id}:${apiKey}`, provider);
257
+ }
258
+ function clearProviderCache() {
259
+ cache.clear();
260
+ }
261
+
262
+ // src/client/keys.ts
263
+ async function resolveApiKey(provider, opts, forModel) {
264
+ let key;
265
+ if (opts.resolveKey) key = await opts.resolveKey(provider);
266
+ if (!key) key = opts.keys?.[provider];
267
+ if (!key) {
268
+ throw new ConfigError(
269
+ `No API key configured for provider "${provider}" (required by model "${forModel}"). Pass keys: { ${provider}: "..." } or a resolveKey() to init().`
270
+ );
271
+ }
272
+ return key;
273
+ }
274
+ function looksLikeKey(provider, key) {
275
+ switch (provider) {
276
+ case "anthropic":
277
+ return /^sk-ant-[a-zA-Z0-9_-]{20,}/.test(key);
278
+ case "openai":
279
+ return /^sk-(?!ant-)(?:proj-)?[a-zA-Z0-9_-]{20,}/.test(key);
280
+ case "google":
281
+ return /^AIza[0-9A-Za-z_-]{35}$/.test(key) || key.length >= 20;
282
+ }
283
+ }
284
+ function redact(text) {
285
+ return text.replace(/sk-ant-[a-zA-Z0-9_-]{6,}/g, "sk-ant-\u2026").replace(/sk-(?!ant-)(?:proj-)?[a-zA-Z0-9_-]{10,}/g, "sk-\u2026").replace(/\bAIza[0-9A-Za-z_-]{6,}/g, "AIza\u2026");
286
+ }
287
+
288
+ // src/client/init.ts
289
+ function init(opts = {}) {
290
+ const catalog = opts.catalog ?? DEFAULT_CATALOG;
291
+ const pickModel = (model) => {
292
+ const chosen = model ?? opts.defaultModel;
293
+ if (!chosen) throw new ConfigError("No model specified and no defaultModel set in init().");
294
+ return chosen;
295
+ };
296
+ const resolveProviderFor = async (model) => {
297
+ const spec = resolveModel(catalog, model);
298
+ const injected = opts.providers?.[spec.provider];
299
+ if (injected) return { spec, provider: injected };
300
+ const key = await resolveApiKey(spec.provider, opts, model);
301
+ const provider = await getProvider(spec.provider, key);
302
+ return { spec, provider };
303
+ };
304
+ const chat = async (input) => {
305
+ const model = pickModel(input.model);
306
+ const req = { ...input, model };
307
+ const { spec, provider } = await resolveProviderFor(model);
308
+ validateRequest(spec, req);
309
+ return provider.create(spec, req);
310
+ };
311
+ async function* stream(input) {
312
+ const model = pickModel(input.model);
313
+ const req = { ...input, model };
314
+ const { spec, provider } = await resolveProviderFor(model);
315
+ if (!spec.capabilities.streaming) {
316
+ throw new CapabilityError(`Model "${model}" (${spec.provider}) does not support streaming.`);
317
+ }
318
+ validateRequest(spec, req);
319
+ yield* provider.stream(spec, req);
320
+ }
321
+ const extract = async (c) => {
322
+ const model = pickModel(c.model);
323
+ const req = {
324
+ model,
325
+ system: c.system,
326
+ messages: [{ role: "user", content: c.user }],
327
+ tools: [{ name: c.tool.name, description: c.tool.description ?? "", inputSchema: c.tool.parameters }],
328
+ toolChoice: { type: "tool", name: c.tool.name },
329
+ maxTokens: c.maxTokens ?? 4096
330
+ };
331
+ const { spec, provider } = await resolveProviderFor(model);
332
+ validateRequest(spec, req);
333
+ const res = await provider.create(spec, req);
334
+ const block = res.content.find((b) => b.type === "tool_use" && b.name === c.tool.name);
335
+ if (!block || block.type !== "tool_use") {
336
+ throw new ProviderError(`Model "${model}" did not return the forced tool call "${c.tool.name}".`);
337
+ }
338
+ return { value: block.input, usage: res.usage };
339
+ };
340
+ const search = async (c) => {
341
+ const model = pickModel(c.model);
342
+ const req = {
343
+ model,
344
+ system: c.system,
345
+ messages: [{ role: "user", content: c.user }],
346
+ webSearch: true,
347
+ maxTokens: c.maxTokens ?? 8192
348
+ };
349
+ const { spec, provider } = await resolveProviderFor(model);
350
+ validateRequest(spec, req);
351
+ const res = await provider.create(spec, req);
352
+ return { text: extractText(res.content), usage: res.usage };
353
+ };
354
+ return { catalog, chat, stream, extract, search };
355
+ }
356
+
357
+ // src/doc/llms.ts
358
+ function capsSummary(c) {
359
+ const flags = ["text"];
360
+ if (c.imageIn) flags.push("image");
361
+ if (c.audioIn) flags.push("audio");
362
+ if (c.toolUse) flags.push("tools");
363
+ if (c.thinking) flags.push("thinking");
364
+ if (c.effort) flags.push("effort");
365
+ if (c.webSearch) flags.push("web-search");
366
+ if (c.structuredOutput) flags.push("structured");
367
+ return flags.join(", ");
368
+ }
369
+ function generateLlmsTxt(catalog = DEFAULT_CATALOG) {
370
+ const models = Object.values(catalog).map((s) => `- \`${s.id}\` (${s.provider}) \u2014 ${capsSummary(s.capabilities)}`).join("\n");
371
+ return `# @odla-ai/ai \u2014 LLM context
372
+
373
+ One general-purpose interface for AI inference across Anthropic (Claude), OpenAI
374
+ (GPT), and Google (Gemini) \u2014 text, image, and audio \u2014 plus a tool-use agent
375
+ engine and an eval harness. Library-only: import in-process, bring your own keys.
376
+ Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loaded.
377
+
378
+ ## Install
379
+
380
+ npm i @odla-ai/ai
381
+ # provider SDKs are peer-lazy-loaded; install those you use:
382
+ # @anthropic-ai/sdk openai @google/genai
383
+ # optional, for odla-db-backed storage + secrets:
384
+ # @odla-ai/db
385
+
386
+ ## Core call
387
+
388
+ import { init } from "@odla-ai/ai";
389
+ const ai = init({ keys: { anthropic: KEY, openai: KEY, google: KEY }, defaultModel: "claude-opus-4-8" });
390
+ const res = await ai.chat({ model: "gpt-5", messages: [{ role: "user", content: "hi" }], maxTokens: 256 });
391
+ // res.content: OracleContentBlock[] (text/tool_use/thinking)
392
+
393
+ - \`ai.stream(input)\` \u2192 async iterable of normalized events: message_start \u2192
394
+ content_block_start \u2192 content_block_delta {text_delta|thinking_delta|input_json_delta}
395
+ \u2192 content_block_stop \u2192 message_delta \u2192 message_stop (plus \`error\`).
396
+ - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns a parsed object.
397
+ - \`ai.search({ model, user })\` \u2192 web-search-grounded prose.
398
+
399
+ ## Content blocks (multimodal)
400
+
401
+ text; image { source: base64|url }; audio { source: base64|url }; document (PDF);
402
+ tool_use { id, name, input }; tool_result { toolUseId, content }; thinking.
403
+ Audio input is accepted by OpenAI (\`gpt-audio\`) and Google, NOT by Claude \u2014 an
404
+ audio block to a Claude model throws CapabilityError before any network call.
405
+
406
+ ## Agents
407
+
408
+ import { runAgent, type Persona, type ToolDef } from "@odla-ai/ai";
409
+ const add: ToolDef = { name: "add", description: "add", inputSchema: {...}, handler: (i) => ({ content: String(i.a + i.b) }) };
410
+ const persona: Persona = { name: "calc", model: "claude-opus-4-8", system: "...", tools: [add], maxSteps: 4 };
411
+ const run = await runAgent(ai, persona, { input: "what is 21+21?" });
412
+ // run.finalText, run.toolCalls, run.usage, run.steps, run.stoppedReason
413
+
414
+ Tools may carry taint labels (outputTaint / acceptsTaint) for a CaMeL-style
415
+ prompt-injection gate. Personas may carry a MemoryScope.
416
+
417
+ ## Skills
418
+
419
+ A Skill = { name, instructions?, tools: ToolDef[] } is a bundle you attach to a
420
+ persona (persona.skills); its tools merge into the model's tool list and its
421
+ instructions into the system prompt. Turn an odla-db entity into CRUD tools:
422
+
423
+ import { entityCrudSkill } from "@odla-ai/ai";
424
+ const todos = entityCrudSkill({ db, entity: "todos", fields: { text: { type: "string" }, done: { type: "boolean" } }, required: ["text"] });
425
+ // \u2192 tools: list_todos, create_todo, update_todo (partial), delete_todo \u2014 handlers write to odla-db
426
+ const run = await runAgent(ai, { name: "todo-bot", model: "gpt-5", skills: [todos] }, { input: "add buy milk" });
427
+
428
+ ## Evals
429
+
430
+ import { evaluate, exactMatch, llmJudge } from "@odla-ai/ai";
431
+ const report = await evaluate({ inference: ai, model: "claude-opus-4-8", grader: exactMatch(), cases: [{ input, expected }] });
432
+ // graders: exactMatch, includes, structuredMatch, llmJudge; pass a persona to eval an agent.
433
+
434
+ ## Storage + BYO keys via odla-db (@odla-ai/db) \u2014 the handshake
435
+
436
+ Follows odla-db's model. An agent never takes a platform secret; it does a
437
+ device-authorization handshake for a scoped, revocable token, then provisions.
438
+
439
+ import { requestToken, init as odlaInit } from "@odla-ai/db";
440
+ import { provisionAgentApp, odlaDbKeyResolver, OdlaDbMemory, persistRun, init } from "@odla-ai/ai";
441
+
442
+ // 1. Handshake: the human supplies the endpoint and approves a short code in odla-db Studio.
443
+ const { token } = await requestToken({ endpoint: ODLA_URL, onCode: ({ userCode }) => show(userCode) });
444
+
445
+ // 2. Provision: create app, mint app key, push agent schema, store BYO provider keys as secrets.
446
+ const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_URL, token, providerKeys: { openai: OPENAI_KEY } });
447
+
448
+ // 3. Runtime: read keys from odla-db secrets; persist memory + run traces.
449
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
450
+ const ai = init({ resolveKey: odlaDbKeyResolver(db) });
451
+ const persona = { name: "assistant", model: "gpt-5", memory: new OdlaDbMemory({ db, sessionId: "user-42" }) };
452
+ const run = await runAgent(ai, persona, { input: "hello" });
453
+ await persistRun(run, { db, sessionId: "user-42" });
454
+
455
+ Secrets are read-only from the app key (odlaDbKeyResolver \u2192 db.secrets.get); they
456
+ are WRITTEN only with the operator/dev token via provisionAgentApp/putSecret. Keys
457
+ are AES-GCM-encrypted at rest in odla-db and never returned to browser clients.
458
+
459
+ ## Errors
460
+
461
+ Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
462
+ config, auth, rate_limit, capability_unsupported, context_window, invalid_request,
463
+ tool_input_invalid, provider_error.
464
+
465
+ ## Models
466
+
467
+ ${models}
468
+
469
+ Model ids live in a data catalog and drift \u2014 verify against provider docs; extend
470
+ via buildCatalog / init({ catalog }).
471
+ `;
472
+ }
473
+
474
+ // src/agent/tools.ts
475
+ function toOracleTool(t) {
476
+ return { name: t.name, description: t.description, inputSchema: t.inputSchema };
477
+ }
478
+
479
+ // src/agent/skill.ts
480
+ function composeSkills(system, tools, skills) {
481
+ const composedTools = [...tools ?? []];
482
+ const sections = system ? [system] : [];
483
+ for (const skill of skills ?? []) {
484
+ composedTools.push(...skill.tools);
485
+ if (skill.instructions) sections.push(`## ${skill.name}
486
+ ${skill.instructions}`);
487
+ }
488
+ return { system: sections.length > 0 ? sections.join("\n\n") : void 0, tools: composedTools };
489
+ }
490
+
491
+ // src/agent/taint.ts
492
+ function taint(...labels) {
493
+ return new Set(labels);
494
+ }
495
+ var TaintError = class extends OdlaAIError {
496
+ constructor(message) {
497
+ super("invalid_request", message);
498
+ }
499
+ };
500
+ function assertSinkAcceptsTaint(sink, allowed, actual) {
501
+ const allow = new Set(allowed);
502
+ for (const label of actual) {
503
+ if (!allow.has(label)) {
504
+ throw new TaintError(
505
+ `Tool "${sink}" refused: conversation carries taint "${label}" which is not in its allowlist [${allowed.join(", ")}].`
506
+ );
507
+ }
508
+ }
509
+ }
510
+
511
+ // src/agent/loop.ts
512
+ async function runAgent(inference, persona, input) {
513
+ const { system, tools } = composeSkills(persona.system, persona.tools, persona.skills);
514
+ const handlerByName = /* @__PURE__ */ new Map();
515
+ for (const t of tools) if (t.handler) handlerByName.set(t.name, t);
516
+ const priorFromMemory = persona.memory ? await persona.memory.load() : [];
517
+ const startingInput = input.messages ? input.messages : input.input !== void 0 ? [{ role: "user", content: input.input }] : [];
518
+ const messages = [...priorFromMemory, ...startingInput];
519
+ const runStartIndex = messages.length - startingInput.length;
520
+ const usage = emptyUsage();
521
+ const toolCalls = [];
522
+ const accumulatedTaint = /* @__PURE__ */ new Set(["llm_inherited"]);
523
+ const maxSteps = persona.maxSteps ?? 8;
524
+ let response;
525
+ let stoppedReason = "max_steps";
526
+ let steps = 0;
527
+ while (steps < maxSteps) {
528
+ steps++;
529
+ response = await inference.chat({
530
+ model: persona.model,
531
+ system,
532
+ messages,
533
+ tools: tools.length > 0 ? tools.map(toOracleTool) : void 0,
534
+ maxTokens: persona.maxTokens ?? 4096,
535
+ effort: persona.effort,
536
+ thinking: persona.thinking,
537
+ temperature: persona.temperature,
538
+ webSearch: persona.webSearch
539
+ });
540
+ addUsage(usage, response.usage);
541
+ messages.push({ role: "assistant", content: response.content });
542
+ if (response.stopReason === "refusal") {
543
+ stoppedReason = "refusal";
544
+ break;
545
+ }
546
+ const toolUses = extractToolUses(response.content);
547
+ if (toolUses.length === 0) {
548
+ stoppedReason = "end_turn";
549
+ break;
550
+ }
551
+ const resultBlocks = [];
552
+ for (const toolUse of toolUses) {
553
+ const def = handlerByName.get(toolUse.name);
554
+ if (!def || !def.handler) {
555
+ resultBlocks.push(errorResult(toolUse.id, `No local handler for tool "${toolUse.name}".`));
556
+ continue;
557
+ }
558
+ if (def.acceptsTaint) assertSinkAcceptsTaint(def.name, def.acceptsTaint, accumulatedTaint);
559
+ let output;
560
+ try {
561
+ output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: input.signal });
562
+ } catch (err) {
563
+ output = { content: `Tool "${toolUse.name}" threw: ${err.message}`, isError: true };
564
+ }
565
+ toolCalls.push({ toolUse, output });
566
+ resultBlocks.push({ type: "tool_result", toolUseId: toolUse.id, content: output.content, isError: output.isError });
567
+ for (const label of def.outputTaint ?? []) accumulatedTaint.add(label);
568
+ }
569
+ messages.push({ role: "user", content: resultBlocks });
570
+ }
571
+ if (!response) throw new Error("runAgent: maxSteps must be at least 1");
572
+ if (persona.memory) await persona.memory.append(messages.slice(runStartIndex));
573
+ return {
574
+ finalText: extractText(response.content),
575
+ response,
576
+ messages,
577
+ usage,
578
+ steps,
579
+ toolCalls,
580
+ stoppedReason
581
+ };
582
+ }
583
+ function errorResult(toolUseId, message) {
584
+ return { type: "tool_result", toolUseId, content: message, isError: true };
585
+ }
586
+
587
+ // src/agent/memory.ts
588
+ var InMemoryScope = class {
589
+ messages = [];
590
+ load() {
591
+ return [...this.messages];
592
+ }
593
+ append(messages) {
594
+ this.messages.push(...messages);
595
+ }
596
+ clear() {
597
+ this.messages = [];
598
+ }
599
+ };
600
+
601
+ // src/eval/harness.ts
602
+ async function evaluate(opts) {
603
+ if (!opts.persona && !opts.model) {
604
+ throw new Error("evaluate() requires either a persona or a model.");
605
+ }
606
+ const results = await runPool(opts.cases, opts.concurrency ?? 5, (c) => runCase(opts, c));
607
+ const usage = emptyUsage();
608
+ let passed = 0;
609
+ for (const r of results) {
610
+ addUsage(usage, r.usage);
611
+ if (r.grade.pass) passed++;
612
+ }
613
+ const total = results.length;
614
+ return { total, passed, failed: total - passed, passRate: total ? passed / total : 0, results, usage };
615
+ }
616
+ async function runCase(opts, c) {
617
+ const messages = toMessages(c.input);
618
+ let output;
619
+ let response;
620
+ let usage;
621
+ if (opts.persona) {
622
+ const run = await runAgent(opts.inference, opts.persona, { messages });
623
+ output = run.finalText;
624
+ response = run.response;
625
+ usage = run.usage;
626
+ } else {
627
+ response = await opts.inference.chat({
628
+ model: opts.model,
629
+ system: opts.system,
630
+ messages,
631
+ maxTokens: opts.maxTokens ?? 1024
632
+ });
633
+ output = extractText(response.content);
634
+ usage = response.usage;
635
+ }
636
+ const grade = await opts.grader({ case: c, output, response });
637
+ return { case: c, output, grade, response, usage };
638
+ }
639
+ function toMessages(input) {
640
+ if (Array.isArray(input) && input.length > 0 && "role" in input[0]) {
641
+ return input;
642
+ }
643
+ return [{ role: "user", content: input }];
644
+ }
645
+ async function runPool(items, limit, fn) {
646
+ const results = new Array(items.length);
647
+ let next = 0;
648
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
649
+ for (; ; ) {
650
+ const i = next++;
651
+ if (i >= items.length) return;
652
+ results[i] = await fn(items[i]);
653
+ }
654
+ });
655
+ await Promise.all(workers);
656
+ return results;
657
+ }
658
+
659
+ // src/eval/graders.ts
660
+ function exactMatch() {
661
+ return ({ case: c, output }) => {
662
+ const expected = String(c.expected ?? "").trim();
663
+ const pass = output.trim() === expected;
664
+ return { pass, reason: pass ? "exact match" : `expected "${expected}", got "${output.trim()}"` };
665
+ };
666
+ }
667
+ function includes(opts = {}) {
668
+ const ci = opts.caseInsensitive ?? true;
669
+ return ({ case: c, output }) => {
670
+ const needle = String(c.expected ?? "");
671
+ const hay = ci ? output.toLowerCase() : output;
672
+ const pass = hay.includes(ci ? needle.toLowerCase() : needle);
673
+ return { pass, reason: pass ? `contains "${needle}"` : `missing "${needle}"` };
674
+ };
675
+ }
676
+ function structuredMatch() {
677
+ return ({ case: c, output }) => {
678
+ let actual;
679
+ try {
680
+ actual = JSON.parse(stripFences(output));
681
+ } catch {
682
+ return { pass: false, reason: "output is not valid JSON" };
683
+ }
684
+ const ok = subset(c.expected, actual);
685
+ return { pass: ok, reason: ok ? "structural subset match" : "output is missing expected fields/values" };
686
+ };
687
+ }
688
+ function llmJudge(opts) {
689
+ return async ({ case: c, output }) => {
690
+ const rubric = typeof c.expected === "string" ? c.expected : JSON.stringify(c.expected);
691
+ const system = 'You are a strict evaluation judge. Decide whether the CANDIDATE output satisfies the RUBRIC. Respond with ONLY a JSON object: {"pass": boolean, "reason": string}. No prose, no code fences.' + (opts.extraGuidance ? ` ${opts.extraGuidance}` : "");
692
+ const user = `RUBRIC:
693
+ ${rubric}
694
+
695
+ CANDIDATE:
696
+ ${output}`;
697
+ const res = await opts.inference.chat({
698
+ model: opts.model,
699
+ system,
700
+ messages: [{ role: "user", content: user }],
701
+ maxTokens: 512
702
+ });
703
+ const text = extractText(res.content);
704
+ try {
705
+ const verdict = JSON.parse(stripFences(text));
706
+ return { pass: Boolean(verdict.pass), reason: typeof verdict.reason === "string" ? verdict.reason : void 0 };
707
+ } catch {
708
+ const pass = /\bpass(ed)?\b|\btrue\b|\byes\b/i.test(text) && !/\bfail(ed)?\b|\bfalse\b|\bno\b/i.test(text);
709
+ return { pass, reason: `unparseable judge output: ${text.slice(0, 200)}` };
710
+ }
711
+ };
712
+ }
713
+ function stripFences(s) {
714
+ return s.replace(/^\s*```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
715
+ }
716
+ function subset(expected, actual) {
717
+ if (expected === null || typeof expected !== "object") return expected === actual;
718
+ if (Array.isArray(expected)) {
719
+ if (!Array.isArray(actual) || actual.length < expected.length) return false;
720
+ return expected.every((e, i) => subset(e, actual[i]));
721
+ }
722
+ if (actual === null || typeof actual !== "object" || Array.isArray(actual)) return false;
723
+ const a = actual;
724
+ return Object.entries(expected).every(([k, v]) => subset(v, a[k]));
725
+ }
726
+
727
+ // src/store/schema.ts
728
+ var NS = {
729
+ conversation: "agent_conversation",
730
+ message: "agent_message",
731
+ run: "agent_run"
732
+ };
733
+ var AGENT_SCHEMA = {
734
+ entities: {
735
+ agent_conversation: {
736
+ attrs: {
737
+ key: { type: "string", unique: true, indexed: true, optional: false },
738
+ sessionId: { type: "string", unique: false, indexed: true, optional: false },
739
+ title: { type: "string", unique: false, indexed: false, optional: true },
740
+ createdAt: { type: "date", unique: false, indexed: true, optional: false },
741
+ updatedAt: { type: "date", unique: false, indexed: true, optional: false }
742
+ }
743
+ },
744
+ agent_message: {
745
+ attrs: {
746
+ key: { type: "string", unique: true, indexed: true, optional: false },
747
+ sessionId: { type: "string", unique: false, indexed: true, optional: false },
748
+ seq: { type: "number", unique: false, indexed: true, optional: false },
749
+ role: { type: "string", unique: false, indexed: false, optional: false },
750
+ content: { type: "string", unique: false, indexed: false, optional: false },
751
+ createdAt: { type: "date", unique: false, indexed: true, optional: false }
752
+ }
753
+ },
754
+ agent_run: {
755
+ attrs: {
756
+ key: { type: "string", unique: true, indexed: true, optional: false },
757
+ sessionId: { type: "string", unique: false, indexed: true, optional: false },
758
+ persona: { type: "string", unique: false, indexed: false, optional: true },
759
+ status: { type: "string", unique: false, indexed: true, optional: false },
760
+ steps: { type: "number", unique: false, indexed: false, optional: false },
761
+ inputTokens: { type: "number", unique: false, indexed: false, optional: false },
762
+ outputTokens: { type: "number", unique: false, indexed: false, optional: false },
763
+ finalText: { type: "string", unique: false, indexed: false, optional: true },
764
+ trace: { type: "json", unique: false, indexed: false, optional: false },
765
+ createdAt: { type: "date", unique: false, indexed: true, optional: false }
766
+ }
767
+ }
768
+ },
769
+ links: {}
770
+ };
771
+
772
+ // src/store/memory.ts
773
+ var OdlaDbMemory = class {
774
+ db;
775
+ sessionId;
776
+ title;
777
+ msgNs;
778
+ convNs;
779
+ loadedCount = 0;
780
+ constructor(opts) {
781
+ this.db = opts.db;
782
+ this.sessionId = opts.sessionId;
783
+ this.title = opts.title;
784
+ this.msgNs = opts.namespaces?.message ?? NS.message;
785
+ this.convNs = opts.namespaces?.conversation ?? NS.conversation;
786
+ }
787
+ async load() {
788
+ const res = await this.db.query({
789
+ [this.msgNs]: { $: { where: { sessionId: this.sessionId }, order: { seq: "asc" }, limit: 1e5 } }
790
+ });
791
+ const rows = res[this.msgNs] ?? [];
792
+ this.loadedCount = rows.length;
793
+ return rows.map((r) => ({ role: r.role ?? "user", content: parseContent(r.content) }));
794
+ }
795
+ async append(messages) {
796
+ if (messages.length === 0) return;
797
+ const base = this.loadedCount;
798
+ const now = Date.now();
799
+ const ops = [
800
+ {
801
+ t: "update",
802
+ ns: this.convNs,
803
+ id: { ns: this.convNs, attr: "key", value: this.sessionId },
804
+ attrs: {
805
+ key: this.sessionId,
806
+ sessionId: this.sessionId,
807
+ ...this.title ? { title: this.title } : {},
808
+ ...base === 0 ? { createdAt: now } : {},
809
+ updatedAt: now
810
+ }
811
+ }
812
+ ];
813
+ messages.forEach((m, i) => {
814
+ const seq = base + i;
815
+ const key = `${this.sessionId}:${seq}`;
816
+ ops.push({
817
+ t: "update",
818
+ ns: this.msgNs,
819
+ id: { ns: this.msgNs, attr: "key", value: key },
820
+ attrs: { key, sessionId: this.sessionId, seq, role: m.role, content: JSON.stringify(m.content), createdAt: now }
821
+ });
822
+ });
823
+ await this.db.transact(ops, {
824
+ mutationId: `${this.sessionId}:append:${base}:${base + messages.length}`
825
+ });
826
+ this.loadedCount += messages.length;
827
+ }
828
+ };
829
+ function parseContent(raw) {
830
+ if (raw === void 0) return "";
831
+ try {
832
+ return JSON.parse(raw);
833
+ } catch {
834
+ return raw;
835
+ }
836
+ }
837
+
838
+ // src/store/runs.ts
839
+ async function persistRun(run, opts) {
840
+ const ns = opts.namespace ?? NS.run;
841
+ const key = opts.runId ?? `${opts.sessionId}:${Date.now()}`;
842
+ await opts.db.transact(
843
+ [
844
+ {
845
+ t: "update",
846
+ ns,
847
+ id: { ns, attr: "key", value: key },
848
+ attrs: {
849
+ key,
850
+ sessionId: opts.sessionId,
851
+ ...opts.personaName ? { persona: opts.personaName } : {},
852
+ status: run.stoppedReason,
853
+ steps: run.steps,
854
+ inputTokens: run.usage.inputTokens,
855
+ outputTokens: run.usage.outputTokens,
856
+ finalText: run.finalText,
857
+ trace: {
858
+ toolCalls: run.toolCalls.map((t) => ({ name: t.toolUse.name, input: t.toolUse.input, output: t.output })),
859
+ messages: run.messages
860
+ },
861
+ createdAt: Date.now()
862
+ }
863
+ }
864
+ ],
865
+ { mutationId: `run:${key}` }
866
+ );
867
+ return key;
868
+ }
869
+ async function queryRuns(opts) {
870
+ const ns = opts.namespace ?? NS.run;
871
+ const res = await opts.db.query({
872
+ [ns]: { $: { where: { sessionId: opts.sessionId }, order: { createdAt: "desc" }, limit: opts.limit ?? 50 } }
873
+ });
874
+ return res[ns] ?? [];
875
+ }
876
+
877
+ // src/store/keys.ts
878
+ var DEFAULT_SECRET_NAMES = {
879
+ anthropic: "anthropic_api_key",
880
+ openai: "openai_api_key",
881
+ google: "google_api_key"
882
+ };
883
+ function odlaDbKeyResolver(db, opts = {}) {
884
+ const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
885
+ const useCache = opts.cache ?? true;
886
+ const cache2 = /* @__PURE__ */ new Map();
887
+ return async (provider) => {
888
+ if (useCache) {
889
+ const hit = cache2.get(provider);
890
+ if (hit !== void 0) return hit;
891
+ }
892
+ try {
893
+ const key = await db.secrets.get(nameFor(provider));
894
+ if (useCache) cache2.set(provider, key);
895
+ return key;
896
+ } catch {
897
+ return void 0;
898
+ }
899
+ };
900
+ }
901
+
902
+ // src/store/provision.ts
903
+ async function post(ctx, path, body) {
904
+ const f = ctx.fetch ?? fetch;
905
+ const res = await f(`${ctx.endpoint.replace(/\/$/, "")}${path}`, {
906
+ method: "POST",
907
+ headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
908
+ body: JSON.stringify(body)
909
+ });
910
+ if (!res.ok) throw errorFor(res.status, await safeText(res), path);
911
+ return await res.json().catch(() => ({}));
912
+ }
913
+ async function postWithKey(ctx, path, appKey, body) {
914
+ const f = ctx.fetch ?? fetch;
915
+ const res = await f(`${ctx.endpoint.replace(/\/$/, "")}${path}`, {
916
+ method: "POST",
917
+ headers: { authorization: `Bearer ${appKey}`, "content-type": "application/json" },
918
+ body: JSON.stringify(body)
919
+ });
920
+ if (!res.ok) throw errorFor(res.status, await safeText(res), path);
921
+ return await res.json().catch(() => ({}));
922
+ }
923
+ async function createApp(ctx, opts = {}) {
924
+ const body = {};
925
+ if (opts.appId) body.appId = opts.appId;
926
+ if (opts.name) body.name = opts.name;
927
+ const json = await post(ctx, "/admin/apps", body);
928
+ const appId = json.appId ?? json.id ?? json.app?.appId ?? opts.appId;
929
+ if (!appId) throw new ProviderError("[odla-db] createApp: response did not include an appId");
930
+ return appId;
931
+ }
932
+ async function mintAppKey(ctx, appId) {
933
+ const json = await post(ctx, `/admin/apps/${encodeURIComponent(appId)}/keys`, {});
934
+ const key = json.key;
935
+ if (!key) throw new ProviderError("[odla-db] mintAppKey: response did not include a key");
936
+ return key;
937
+ }
938
+ async function pushAgentSchema(ctx, appId, appKey) {
939
+ await postWithKey(ctx, `/app/${encodeURIComponent(appId)}/schema`, appKey, { schema: AGENT_SCHEMA });
940
+ }
941
+ async function putSecret(ctx, appId, name, value) {
942
+ await post(ctx, `/admin/apps/${encodeURIComponent(appId)}/secrets`, { name, value });
943
+ }
944
+ async function provisionAgentApp(opts) {
945
+ const ctx = { endpoint: opts.endpoint, token: opts.token, fetch: opts.fetch };
946
+ const appId = await createApp(ctx, { appId: opts.appId, name: opts.appName });
947
+ const appKey = await mintAppKey(ctx, appId);
948
+ if (opts.pushSchema !== false) {
949
+ await pushAgentSchema({ endpoint: opts.endpoint, fetch: opts.fetch }, appId, appKey);
950
+ }
951
+ const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
952
+ for (const [provider, value] of Object.entries(opts.providerKeys ?? {})) {
953
+ if (!value) continue;
954
+ await putSecret(ctx, appId, nameFor(provider), value);
955
+ }
956
+ return { appId, appKey };
957
+ }
958
+ function errorFor(status, text, path) {
959
+ const msg = redact(`[odla-db] ${path} \u2192 ${status} ${text}`.trim());
960
+ if (status === 401 || status === 403) return new AuthError(msg, { providerStatus: status });
961
+ if (status === 501) return new ConfigError(`${msg} (secrets/feature not enabled on the odla-db worker)`);
962
+ if (status === 400) return new ConfigError(msg);
963
+ return new ProviderError(msg, { providerStatus: status });
964
+ }
965
+ async function safeText(res) {
966
+ try {
967
+ return (await res.text()).slice(0, 300);
968
+ } catch {
969
+ return "";
970
+ }
971
+ }
972
+
973
+ // src/store/crud.ts
974
+ function entityCrudSkill(opts) {
975
+ const { db, entity, fields } = opts;
976
+ const singular = opts.singular ?? entity.replace(/s$/, "");
977
+ const plural = opts.plural ?? entity;
978
+ const idField = opts.idField ?? "id";
979
+ const genId = opts.genId ?? (() => globalThis.crypto.randomUUID());
980
+ const stampAttr = opts.stampCreatedAt === void 0 ? "createdAt" : opts.stampCreatedAt;
981
+ const order = opts.order ?? { createdAt: "asc" };
982
+ const fieldNames = Object.keys(fields);
983
+ const pickFields = (input) => {
984
+ const out = {};
985
+ for (const k of fieldNames) if (k in input && input[k] !== void 0) out[k] = input[k];
986
+ return out;
987
+ };
988
+ const ok = (obj) => ({ content: JSON.stringify(obj) });
989
+ const listTool = {
990
+ name: `list_${plural}`,
991
+ description: `List ${plural}. Optionally filter by exact field values and/or limit the count.`,
992
+ inputSchema: {
993
+ type: "object",
994
+ properties: {
995
+ filter: { type: "object", description: `Exact-match filters, e.g. { "done": false }`, additionalProperties: true },
996
+ limit: { type: "number", description: "Max rows to return." }
997
+ }
998
+ },
999
+ handler: async (input) => {
1000
+ const filter = input.filter ?? void 0;
1001
+ const limit = typeof input.limit === "number" ? input.limit : void 0;
1002
+ const $ = { order };
1003
+ if (filter && Object.keys(filter).length > 0) $.where = filter;
1004
+ if (limit !== void 0) $.limit = limit;
1005
+ const res = await db.query({ [entity]: { $ } });
1006
+ return ok({ [plural]: res[entity] ?? [] });
1007
+ }
1008
+ };
1009
+ const createTool = {
1010
+ name: `create_${singular}`,
1011
+ description: `Create a new ${singular}.`,
1012
+ inputSchema: { type: "object", properties: fields, ...opts.required ? { required: opts.required } : {} },
1013
+ handler: async (input) => {
1014
+ const id = genId();
1015
+ const attrs = pickFields(input);
1016
+ if (stampAttr) attrs[stampAttr] = Date.now();
1017
+ await db.transact([{ t: "update", ns: entity, id, attrs }]);
1018
+ return ok({ id, created: true });
1019
+ }
1020
+ };
1021
+ const updateTool = {
1022
+ name: `update_${singular}`,
1023
+ description: `Update fields of an existing ${singular} by ${idField}. Only the fields you pass change.`,
1024
+ inputSchema: {
1025
+ type: "object",
1026
+ properties: { [idField]: { type: "string", description: `The ${singular}'s id.` }, ...fields },
1027
+ required: [idField]
1028
+ },
1029
+ handler: async (input) => {
1030
+ const id = String(input[idField] ?? "");
1031
+ if (!id) return { content: `Missing "${idField}".`, isError: true };
1032
+ const attrs = pickFields(input);
1033
+ if (Object.keys(attrs).length === 0) return { content: "No fields to update.", isError: true };
1034
+ await db.transact([{ t: "update", ns: entity, id, attrs }]);
1035
+ return ok({ id, updated: true });
1036
+ }
1037
+ };
1038
+ const deleteTool = {
1039
+ name: `delete_${singular}`,
1040
+ description: `Delete a ${singular} by ${idField}.`,
1041
+ inputSchema: { type: "object", properties: { [idField]: { type: "string" } }, required: [idField] },
1042
+ handler: async (input) => {
1043
+ const id = String(input[idField] ?? "");
1044
+ if (!id) return { content: `Missing "${idField}".`, isError: true };
1045
+ const op = { t: "delete", ns: entity, id };
1046
+ await db.transact([op]);
1047
+ return ok({ id, deleted: true });
1048
+ }
1049
+ };
1050
+ const guidance = `Manage ${plural} in the database. Use list_${plural} to read (optionally filtered), create_${singular} to add, update_${singular} to change fields (e.g. mark done), and delete_${singular} to remove. Always confirm the current list with list_${plural} before updating or deleting so you use the right id.` + (opts.instructions ? `
1051
+ ${opts.instructions}` : "");
1052
+ return {
1053
+ name: opts.name ?? `${entity} management`,
1054
+ instructions: guidance,
1055
+ tools: [listTool, createTool, updateTool, deleteTool]
1056
+ };
1057
+ }
1058
+ export {
1059
+ AGENT_SCHEMA,
1060
+ ANTHROPIC_MODELS,
1061
+ AuthError,
1062
+ CapabilityError,
1063
+ ConfigError,
1064
+ ContextWindowError,
1065
+ DEFAULT_CATALOG,
1066
+ DEFAULT_SECRET_NAMES,
1067
+ GOOGLE_MODELS,
1068
+ InMemoryScope,
1069
+ InvalidRequestError,
1070
+ NS,
1071
+ OPENAI_MODELS,
1072
+ OdlaAIError,
1073
+ OdlaDbMemory,
1074
+ ProviderError,
1075
+ RateLimitError,
1076
+ TaintError,
1077
+ addUsage,
1078
+ assertSinkAcceptsTaint,
1079
+ blocksOf,
1080
+ buildCatalog,
1081
+ chatCapabilities,
1082
+ clearProviderCache,
1083
+ composeSkills,
1084
+ createApp,
1085
+ emptyUsage,
1086
+ entityCrudSkill,
1087
+ evaluate,
1088
+ exactMatch,
1089
+ extractText,
1090
+ extractToolUses,
1091
+ generateLlmsTxt,
1092
+ getProvider,
1093
+ includes,
1094
+ init,
1095
+ isAudioBlock,
1096
+ isDocumentBlock,
1097
+ isImageBlock,
1098
+ isTextBlock,
1099
+ isThinkingBlock,
1100
+ isToolResultBlock,
1101
+ isToolUseBlock,
1102
+ llmJudge,
1103
+ looksLikeKey,
1104
+ mintAppKey,
1105
+ normalizeError,
1106
+ odlaDbKeyResolver,
1107
+ persistRun,
1108
+ providersInCatalog,
1109
+ provisionAgentApp,
1110
+ pushAgentSchema,
1111
+ putSecret,
1112
+ queryRuns,
1113
+ redact,
1114
+ registerProvider,
1115
+ resolveModel,
1116
+ runAgent,
1117
+ structuredMatch,
1118
+ taint,
1119
+ toOracleTool,
1120
+ validateRequest
1121
+ };
1122
+ //# sourceMappingURL=index.js.map