@odla-ai/ai 0.2.2 → 0.3.1

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.cjs CHANGED
@@ -77,7 +77,7 @@ var init_events = __esm({
77
77
  });
78
78
 
79
79
  // src/shape/errors.ts
80
- var OdlaAIError, ConfigError, AuthError, RateLimitError, CapabilityError, ContextWindowError, InvalidRequestError, ProviderError;
80
+ var OdlaAIError, ConfigError, AuthError, RateLimitError, CapabilityError, ContextWindowError, InvalidRequestError, ToolInputError, CancelledError, DeadlineExceededError, ProviderError;
81
81
  var init_errors = __esm({
82
82
  "src/shape/errors.ts"() {
83
83
  "use strict";
@@ -131,6 +131,23 @@ var init_errors = __esm({
131
131
  super("invalid_request", message, opts);
132
132
  }
133
133
  };
134
+ ToolInputError = class extends OdlaAIError {
135
+ tool;
136
+ constructor(message, opts) {
137
+ super("tool_input_invalid", message, { cause: opts?.cause });
138
+ this.tool = opts?.tool;
139
+ }
140
+ };
141
+ CancelledError = class extends OdlaAIError {
142
+ constructor(message = "AI request was cancelled.", opts) {
143
+ super("cancelled", message, opts);
144
+ }
145
+ };
146
+ DeadlineExceededError = class extends OdlaAIError {
147
+ constructor(message = "AI request deadline was exceeded.", opts) {
148
+ super("deadline_exceeded", message, opts);
149
+ }
150
+ };
134
151
  ProviderError = class extends OdlaAIError {
135
152
  constructor(message, opts) {
136
153
  super("provider_error", message, opts);
@@ -176,6 +193,372 @@ var init_helpers = __esm({
176
193
  }
177
194
  });
178
195
 
196
+ // src/shape/json-schema-shape.ts
197
+ function validateSchemaShape(schema, path, root, issues) {
198
+ if (typeof schema === "boolean") return;
199
+ if (!isRecord(schema)) {
200
+ issue(issues, path, "schema must be an object or boolean");
201
+ return;
202
+ }
203
+ for (const key of Object.keys(schema)) {
204
+ if (!SUPPORTED_KEYS.has(key)) issue(issues, path, `schema uses unsupported keyword "${key}"`);
205
+ }
206
+ if (typeof schema.$ref === "string" && resolveLocalRef(root, schema.$ref) === void 0) {
207
+ issue(issues, path, `schema reference "${schema.$ref}" could not be resolved`);
208
+ } else if (schema.$ref !== void 0 && typeof schema.$ref !== "string") {
209
+ issue(issues, path, "schema $ref must be a string");
210
+ }
211
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
212
+ if (schema.type !== void 0 && (types.length === 0 || !types.every((type) => typeof type === "string" && ALLOWED_TYPES.has(type)))) {
213
+ issue(issues, path, "schema type contains an unsupported JSON type");
214
+ }
215
+ if (schema.enum !== void 0 && !Array.isArray(schema.enum)) {
216
+ issue(issues, path, "schema enum must be an array");
217
+ }
218
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((key) => typeof key === "string"))) {
219
+ issue(issues, path, "schema required must be a string array");
220
+ }
221
+ validateConstraintShapes(schema, path, issues);
222
+ validateChildSchemas(schema, path, root, issues);
223
+ }
224
+ function validateChildSchemas(schema, path, root, issues) {
225
+ for (const key of ["properties", "$defs", "definitions"]) {
226
+ const group = schema[key];
227
+ if (group === void 0) continue;
228
+ if (!isRecord(group)) {
229
+ issue(issues, path, `schema ${key} must be an object`);
230
+ continue;
231
+ }
232
+ for (const [name, child] of Object.entries(group)) {
233
+ validateSchemaShape(child, `${path}.${key}.${name}`, root, issues);
234
+ }
235
+ }
236
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
237
+ const group = schema[key];
238
+ if (group === void 0) continue;
239
+ if (!Array.isArray(group) || group.length === 0) {
240
+ issue(issues, path, `schema ${key} must be a non-empty array`);
241
+ continue;
242
+ }
243
+ group.forEach((child, index2) => validateSchemaShape(child, `${path}.${key}[${index2}]`, root, issues));
244
+ }
245
+ for (const key of ["items", "not", "additionalProperties"]) {
246
+ const child = schema[key];
247
+ if (child !== void 0) validateSchemaShape(child, `${path}.${key}`, root, issues);
248
+ }
249
+ }
250
+ function validateConstraintShapes(schema, path, issues) {
251
+ for (const key of ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"]) {
252
+ const value = schema[key];
253
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
254
+ issue(issues, path, `schema ${key} must be a non-negative integer`);
255
+ }
256
+ }
257
+ for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
258
+ const value = schema[key];
259
+ if (value !== void 0 && !isFiniteNumber(value)) {
260
+ issue(issues, path, `schema ${key} must be a finite number`);
261
+ }
262
+ }
263
+ if (schema.multipleOf !== void 0 && (!isFiniteNumber(schema.multipleOf) || schema.multipleOf <= 0)) {
264
+ issue(issues, path, "schema multipleOf must be a finite number greater than zero");
265
+ }
266
+ if (schema.uniqueItems !== void 0 && typeof schema.uniqueItems !== "boolean") {
267
+ issue(issues, path, "schema uniqueItems must be a boolean");
268
+ }
269
+ validatePatternShape(schema.pattern, path, issues);
270
+ }
271
+ function validatePatternShape(value, path, issues) {
272
+ if (value === void 0) return;
273
+ if (typeof value !== "string") {
274
+ issue(issues, path, "schema pattern must be a string");
275
+ return;
276
+ }
277
+ try {
278
+ new RegExp(value, "u");
279
+ } catch {
280
+ issue(issues, path, `schema pattern "${value}" is invalid`);
281
+ }
282
+ }
283
+ function resolveLocalRef(root, ref) {
284
+ if (ref === "#") return root;
285
+ if (!ref.startsWith("#/")) return void 0;
286
+ let current = root;
287
+ for (const raw of ref.slice(2).split("/")) {
288
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
289
+ if (!isRecord(current) || !Object.hasOwn(current, key)) return void 0;
290
+ current = current[key];
291
+ }
292
+ return current;
293
+ }
294
+ function isRecord(value) {
295
+ return value !== null && typeof value === "object" && !Array.isArray(value);
296
+ }
297
+ function isFiniteNumber(value) {
298
+ return typeof value === "number" && Number.isFinite(value);
299
+ }
300
+ function issue(issues, path, message) {
301
+ issues.push({ path, message });
302
+ }
303
+ var ANNOTATION_KEYS, SUPPORTED_KEYS, ALLOWED_TYPES;
304
+ var init_json_schema_shape = __esm({
305
+ "src/shape/json-schema-shape.ts"() {
306
+ "use strict";
307
+ ANNOTATION_KEYS = /* @__PURE__ */ new Set([
308
+ "$schema",
309
+ "$id",
310
+ "$anchor",
311
+ "title",
312
+ "description",
313
+ "default",
314
+ "examples",
315
+ "deprecated",
316
+ "readOnly",
317
+ "writeOnly",
318
+ "format"
319
+ ]);
320
+ SUPPORTED_KEYS = /* @__PURE__ */ new Set([
321
+ ...ANNOTATION_KEYS,
322
+ "$ref",
323
+ "$defs",
324
+ "definitions",
325
+ "type",
326
+ "enum",
327
+ "const",
328
+ "allOf",
329
+ "anyOf",
330
+ "oneOf",
331
+ "not",
332
+ "properties",
333
+ "required",
334
+ "additionalProperties",
335
+ "minProperties",
336
+ "maxProperties",
337
+ "items",
338
+ "minItems",
339
+ "maxItems",
340
+ "uniqueItems",
341
+ "minLength",
342
+ "maxLength",
343
+ "pattern",
344
+ "minimum",
345
+ "maximum",
346
+ "exclusiveMinimum",
347
+ "exclusiveMaximum",
348
+ "multipleOf"
349
+ ]);
350
+ ALLOWED_TYPES = /* @__PURE__ */ new Set([
351
+ "null",
352
+ "boolean",
353
+ "object",
354
+ "array",
355
+ "number",
356
+ "integer",
357
+ "string"
358
+ ]);
359
+ }
360
+ });
361
+
362
+ // src/shape/json-schema.ts
363
+ function validateJsonSchema(schema, value) {
364
+ const issues = [];
365
+ validateSchemaShape(schema, "$", schema, issues);
366
+ if (issues.length > 0) return { valid: false, issues };
367
+ visit(schema, value, "$", schema, issues, /* @__PURE__ */ new Set());
368
+ return { valid: issues.length === 0, issues };
369
+ }
370
+ function visit(schema, value, path, root, issues, refs) {
371
+ if (schema === true) return;
372
+ if (schema === false) {
373
+ issue2(issues, path, "is rejected by the schema");
374
+ return;
375
+ }
376
+ if (!isRecord2(schema)) {
377
+ issue2(issues, path, "schema must be an object or boolean");
378
+ return;
379
+ }
380
+ for (const key of Object.keys(schema)) {
381
+ if (!SUPPORTED_KEYS.has(key)) {
382
+ issue2(issues, path, `schema uses unsupported keyword "${key}"`);
383
+ return;
384
+ }
385
+ }
386
+ if (typeof schema.$ref === "string") {
387
+ const target = resolveLocalRef2(root, schema.$ref);
388
+ if (target === void 0) {
389
+ issue2(issues, path, `schema reference "${schema.$ref}" could not be resolved`);
390
+ return;
391
+ }
392
+ if (refs.has(schema.$ref)) {
393
+ issue2(issues, path, `cyclic schema reference "${schema.$ref}" is unsupported`);
394
+ return;
395
+ }
396
+ const nextRefs = new Set(refs);
397
+ nextRefs.add(schema.$ref);
398
+ visit(target, value, path, root, issues, nextRefs);
399
+ }
400
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonEqual(candidate, value))) {
401
+ issue2(issues, path, "must equal one of the allowed enum values");
402
+ }
403
+ if (Object.hasOwn(schema, "const") && !jsonEqual(schema.const, value)) {
404
+ issue2(issues, path, "must equal the constant value");
405
+ }
406
+ if (Array.isArray(schema.allOf)) {
407
+ for (const child of schema.allOf) visit(child, value, path, root, issues, refs);
408
+ }
409
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((child) => matches(child, value, root, refs))) {
410
+ issue2(issues, path, "must match at least one anyOf schema");
411
+ }
412
+ if (Array.isArray(schema.oneOf)) {
413
+ const matchesCount = schema.oneOf.filter((child) => matches(child, value, root, refs)).length;
414
+ if (matchesCount !== 1) issue2(issues, path, "must match exactly one oneOf schema");
415
+ }
416
+ if (schema.not !== void 0 && matches(schema.not, value, root, refs)) {
417
+ issue2(issues, path, "must not match the forbidden schema");
418
+ }
419
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) && schema.type.every((t) => typeof t === "string") ? schema.type : void 0;
420
+ if (schema.type !== void 0 && !types) {
421
+ issue2(issues, path, "schema type must be a string or string array");
422
+ return;
423
+ }
424
+ if (types && !types.some((type) => isType(value, type))) {
425
+ issue2(issues, path, `must be ${types.join(" or ")}`);
426
+ return;
427
+ }
428
+ if (typeof value === "string") validateString(schema, value, path, issues);
429
+ if (typeof value === "number") validateNumber(schema, value, path, issues);
430
+ if (Array.isArray(value)) validateArray(schema, value, path, root, issues, refs);
431
+ if (isRecord2(value)) validateObject(schema, value, path, root, issues, refs);
432
+ }
433
+ function validateString(schema, value, path, issues) {
434
+ if (isFiniteNumber2(schema.minLength) && [...value].length < schema.minLength) issue2(issues, path, `must have at least ${schema.minLength} characters`);
435
+ if (isFiniteNumber2(schema.maxLength) && [...value].length > schema.maxLength) issue2(issues, path, `must have at most ${schema.maxLength} characters`);
436
+ if (typeof schema.pattern === "string") {
437
+ try {
438
+ if (!new RegExp(schema.pattern, "u").test(value)) issue2(issues, path, `must match pattern ${schema.pattern}`);
439
+ } catch {
440
+ issue2(issues, path, `schema pattern "${schema.pattern}" is invalid`);
441
+ }
442
+ }
443
+ }
444
+ function validateNumber(schema, value, path, issues) {
445
+ if (!Number.isFinite(value)) {
446
+ issue2(issues, path, "must be a finite number");
447
+ return;
448
+ }
449
+ if (isFiniteNumber2(schema.minimum) && value < schema.minimum) issue2(issues, path, `must be >= ${schema.minimum}`);
450
+ if (isFiniteNumber2(schema.maximum) && value > schema.maximum) issue2(issues, path, `must be <= ${schema.maximum}`);
451
+ if (isFiniteNumber2(schema.exclusiveMinimum) && value <= schema.exclusiveMinimum) issue2(issues, path, `must be > ${schema.exclusiveMinimum}`);
452
+ if (isFiniteNumber2(schema.exclusiveMaximum) && value >= schema.exclusiveMaximum) issue2(issues, path, `must be < ${schema.exclusiveMaximum}`);
453
+ if (isFiniteNumber2(schema.multipleOf) && schema.multipleOf > 0) {
454
+ const quotient = value / schema.multipleOf;
455
+ if (Math.abs(quotient - Math.round(quotient)) > Number.EPSILON * Math.max(1, Math.abs(quotient))) {
456
+ issue2(issues, path, `must be a multiple of ${schema.multipleOf}`);
457
+ }
458
+ }
459
+ }
460
+ function validateArray(schema, value, path, root, issues, refs) {
461
+ if (isFiniteNumber2(schema.minItems) && value.length < schema.minItems) issue2(issues, path, `must contain at least ${schema.minItems} items`);
462
+ if (isFiniteNumber2(schema.maxItems) && value.length > schema.maxItems) issue2(issues, path, `must contain at most ${schema.maxItems} items`);
463
+ if (schema.uniqueItems === true) {
464
+ for (let i = 0; i < value.length; i++) {
465
+ if (value.slice(0, i).some((other) => jsonEqual(other, value[i]))) {
466
+ issue2(issues, `${path}[${i}]`, "must be unique");
467
+ break;
468
+ }
469
+ }
470
+ }
471
+ if (schema.items !== void 0) {
472
+ for (let i = 0; i < value.length; i++) visit(schema.items, value[i], `${path}[${i}]`, root, issues, refs);
473
+ }
474
+ }
475
+ function validateObject(schema, value, path, root, issues, refs) {
476
+ const keys = Object.keys(value);
477
+ if (isFiniteNumber2(schema.minProperties) && keys.length < schema.minProperties) issue2(issues, path, `must contain at least ${schema.minProperties} properties`);
478
+ if (isFiniteNumber2(schema.maxProperties) && keys.length > schema.maxProperties) issue2(issues, path, `must contain at most ${schema.maxProperties} properties`);
479
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((key) => typeof key === "string"))) {
480
+ issue2(issues, path, "schema required must be a string array");
481
+ return;
482
+ }
483
+ for (const key of schema.required ?? []) {
484
+ if (!Object.hasOwn(value, key)) issue2(issues, childPath(path, key), "is required");
485
+ }
486
+ const properties = isRecord2(schema.properties) ? schema.properties : {};
487
+ for (const [key, child] of Object.entries(properties)) {
488
+ if (Object.hasOwn(value, key)) visit(child, value[key], childPath(path, key), root, issues, refs);
489
+ }
490
+ const extras = keys.filter((key) => !Object.hasOwn(properties, key));
491
+ if (schema.additionalProperties === false) {
492
+ for (const key of extras) issue2(issues, childPath(path, key), "is not an allowed property");
493
+ } else if (schema.additionalProperties !== void 0 && schema.additionalProperties !== true) {
494
+ for (const key of extras) visit(schema.additionalProperties, value[key], childPath(path, key), root, issues, refs);
495
+ }
496
+ }
497
+ function matches(schema, value, root, refs) {
498
+ const nested = [];
499
+ visit(schema, value, "$", root, nested, refs);
500
+ return nested.length === 0;
501
+ }
502
+ function resolveLocalRef2(root, ref) {
503
+ if (ref === "#") return root;
504
+ if (!ref.startsWith("#/")) return void 0;
505
+ let current = root;
506
+ for (const raw of ref.slice(2).split("/")) {
507
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
508
+ if (!isRecord2(current) || !Object.hasOwn(current, key)) return void 0;
509
+ current = current[key];
510
+ }
511
+ return current;
512
+ }
513
+ function isRecord2(value) {
514
+ return value !== null && typeof value === "object" && !Array.isArray(value);
515
+ }
516
+ function isType(value, type) {
517
+ switch (type) {
518
+ case "null":
519
+ return value === null;
520
+ case "boolean":
521
+ return typeof value === "boolean";
522
+ case "object":
523
+ return isRecord2(value);
524
+ case "array":
525
+ return Array.isArray(value);
526
+ case "number":
527
+ return typeof value === "number" && Number.isFinite(value);
528
+ case "integer":
529
+ return typeof value === "number" && Number.isInteger(value);
530
+ case "string":
531
+ return typeof value === "string";
532
+ default:
533
+ return false;
534
+ }
535
+ }
536
+ function isFiniteNumber2(value) {
537
+ return typeof value === "number" && Number.isFinite(value);
538
+ }
539
+ function jsonEqual(a, b) {
540
+ if (Object.is(a, b)) return true;
541
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
542
+ if (isRecord2(a) && isRecord2(b)) {
543
+ const aKeys = Object.keys(a);
544
+ const bKeys = Object.keys(b);
545
+ return aKeys.length === bKeys.length && aKeys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]));
546
+ }
547
+ return false;
548
+ }
549
+ function childPath(path, key) {
550
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
551
+ }
552
+ function issue2(issues, path, message) {
553
+ issues.push({ path, message });
554
+ }
555
+ var init_json_schema = __esm({
556
+ "src/shape/json-schema.ts"() {
557
+ "use strict";
558
+ init_json_schema_shape();
559
+ }
560
+ });
561
+
179
562
  // src/shape/index.ts
180
563
  var init_shape = __esm({
181
564
  "src/shape/index.ts"() {
@@ -186,6 +569,7 @@ var init_shape = __esm({
186
569
  init_events();
187
570
  init_errors();
188
571
  init_helpers();
572
+ init_json_schema();
189
573
  }
190
574
  });
191
575
 
@@ -216,8 +600,10 @@ function messageOf(err) {
216
600
  if (typeof err === "string") return err;
217
601
  return "unknown provider error";
218
602
  }
219
- function normalizeError(err, provider) {
603
+ function normalizeError(err, provider, signal) {
220
604
  if (err instanceof OdlaAIError) return err;
605
+ const cancellation = cancellationError(signal?.aborted ? signal.reason : err);
606
+ if (signal?.aborted || cancellation) return cancellation ?? new CancelledError(void 0, { cause: err });
221
607
  const status = statusOf(err);
222
608
  const message = messageOf(err);
223
609
  const tagged = `[${provider}] ${message}`;
@@ -233,8 +619,18 @@ function normalizeError(err, provider) {
233
619
  }
234
620
  return new ProviderError(tagged, { providerStatus: status, cause: err });
235
621
  }
236
- function mapProviderError(err, provider) {
237
- throw normalizeError(err, provider);
622
+ function mapProviderError(err, provider, signal) {
623
+ throw normalizeError(err, provider, signal);
624
+ }
625
+ function cancellationError(reason) {
626
+ if (reason instanceof DeadlineExceededError || reason instanceof CancelledError) return reason;
627
+ if (!reason || typeof reason !== "object") return void 0;
628
+ const rec = reason;
629
+ if (rec.name === "TimeoutError") return new DeadlineExceededError(void 0, { cause: reason });
630
+ if (rec.name === "AbortError" || rec.name === "APIUserAbortError" || rec.code === "ABORT_ERR") {
631
+ return new CancelledError(void 0, { cause: reason });
632
+ }
633
+ return void 0;
238
634
  }
239
635
  var init_errors2 = __esm({
240
636
  "src/providers/errors.ts"() {
@@ -337,13 +733,27 @@ var init_request2 = __esm({
337
733
  }
338
734
  });
339
735
 
736
+ // src/providers/tool-input.ts
737
+ function toolInput(value, provider, name) {
738
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
739
+ return value;
740
+ }
741
+ throw new ToolInputError(`[${provider}] tool "${name}" returned arguments that are not a JSON object.`, { tool: name });
742
+ }
743
+ var init_tool_input = __esm({
744
+ "src/providers/tool-input.ts"() {
745
+ "use strict";
746
+ init_shape();
747
+ }
748
+ });
749
+
340
750
  // src/providers/anthropic/response.ts
341
751
  function mapContent(blocks) {
342
752
  const out = [];
343
753
  for (const b of blocks) {
344
754
  if (b.type === "text") out.push({ type: "text", text: b.text });
345
755
  else if (b.type === "tool_use") {
346
- out.push({ type: "tool_use", id: b.id, name: b.name, input: b.input ?? {} });
756
+ out.push({ type: "tool_use", id: b.id, name: b.name, input: toolInput(b.input ?? {}, "anthropic", b.name) });
347
757
  } else if (b.type === "thinking") {
348
758
  out.push({ type: "thinking", thinking: b.thinking, signature: b.signature });
349
759
  }
@@ -372,6 +782,7 @@ function mapStop(reason) {
372
782
  var init_response = __esm({
373
783
  "src/providers/anthropic/response.ts"() {
374
784
  "use strict";
785
+ init_tool_input();
375
786
  }
376
787
  });
377
788
 
@@ -424,6 +835,35 @@ var init_stream = __esm({
424
835
  }
425
836
  });
426
837
 
838
+ // src/client/abort.ts
839
+ function signalWithDeadline(signal, deadline) {
840
+ if (deadline === void 0) return signal;
841
+ if (!Number.isSafeInteger(deadline)) throw new ConfigError("deadline must be a finite integer Unix timestamp in milliseconds.");
842
+ const remaining = deadline - Date.now();
843
+ if (remaining <= 0) {
844
+ const expired = new AbortController();
845
+ expired.abort(new DeadlineExceededError());
846
+ return signal ? AbortSignal.any([signal, expired.signal]) : expired.signal;
847
+ }
848
+ const timeout = AbortSignal.timeout(remaining);
849
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
850
+ }
851
+ function throwIfAborted(signal) {
852
+ if (!signal?.aborted) return;
853
+ const reason = signal.reason;
854
+ if (reason instanceof DeadlineExceededError || reason instanceof CancelledError) throw reason;
855
+ if (reason && typeof reason === "object" && reason.name === "TimeoutError") {
856
+ throw new DeadlineExceededError(void 0, { cause: reason });
857
+ }
858
+ throw new CancelledError(void 0, { cause: reason });
859
+ }
860
+ var init_abort = __esm({
861
+ "src/client/abort.ts"() {
862
+ "use strict";
863
+ init_errors();
864
+ }
865
+ });
866
+
427
867
  // src/providers/anthropic.ts
428
868
  var anthropic_exports = {};
429
869
  __export(anthropic_exports, {
@@ -439,6 +879,7 @@ var init_anthropic = __esm({
439
879
  init_request2();
440
880
  init_response();
441
881
  init_stream();
882
+ init_abort();
442
883
  AnthropicProvider = class {
443
884
  id = "anthropic";
444
885
  client;
@@ -447,6 +888,7 @@ var init_anthropic = __esm({
447
888
  this.client = client ?? new import_sdk.default({ apiKey });
448
889
  }
449
890
  async create(spec, req) {
891
+ const signal = signalWithDeadline(req.signal, req.deadline);
450
892
  try {
451
893
  let messages = toAnthropicMessages(req);
452
894
  const content = [];
@@ -455,7 +897,8 @@ var init_anthropic = __esm({
455
897
  let id = "";
456
898
  for (let i = 0; i < 8; i++) {
457
899
  const res = await this.client.messages.create(
458
- buildParams(spec, req, messages)
900
+ buildParams(spec, req, messages),
901
+ { signal }
459
902
  );
460
903
  id = res.id;
461
904
  addUsage(usage, mapUsage(res.usage));
@@ -469,20 +912,22 @@ var init_anthropic = __esm({
469
912
  }
470
913
  return { id, model: req.model, provider: "anthropic", role: "assistant", content, stopReason, usage };
471
914
  } catch (err) {
472
- mapProviderError(err, "anthropic");
915
+ mapProviderError(err, "anthropic", signal);
473
916
  }
474
917
  }
475
918
  async *stream(spec, req) {
919
+ const signal = signalWithDeadline(req.signal, req.deadline);
476
920
  try {
477
921
  const events = this.client.messages.stream(
478
- buildParams(spec, req, toAnthropicMessages(req))
922
+ buildParams(spec, req, toAnthropicMessages(req)),
923
+ { signal }
479
924
  );
480
925
  for await (const raw of events) {
481
926
  const ev = mapStreamEvent(raw, req);
482
927
  if (ev) yield ev;
483
928
  }
484
929
  } catch (err) {
485
- yield { type: "error", error: normalizeError(err, "anthropic").toShape() };
930
+ yield { type: "error", error: normalizeError(err, "anthropic", signal).toShape() };
486
931
  }
487
932
  }
488
933
  };
@@ -647,9 +1092,10 @@ function parseArgs(raw) {
647
1092
  if (!raw) return {};
648
1093
  try {
649
1094
  const parsed = JSON.parse(raw);
650
- return parsed && typeof parsed === "object" ? parsed : {};
651
- } catch {
652
- return {};
1095
+ return toolInput(parsed, "openai", "function");
1096
+ } catch (error) {
1097
+ if (error instanceof ToolInputError) throw error;
1098
+ throw new ToolInputError("[openai] tool returned malformed JSON arguments.", { cause: error });
653
1099
  }
654
1100
  }
655
1101
  function mapFinish(reason) {
@@ -676,6 +1122,8 @@ function mapUsage2(u) {
676
1122
  var init_response2 = __esm({
677
1123
  "src/providers/openai/response.ts"() {
678
1124
  "use strict";
1125
+ init_shape();
1126
+ init_tool_input();
679
1127
  }
680
1128
  });
681
1129
 
@@ -752,6 +1200,7 @@ var init_openai = __esm({
752
1200
  init_request3();
753
1201
  init_response2();
754
1202
  init_stream2();
1203
+ init_abort();
755
1204
  OpenAIProvider = class {
756
1205
  id = "openai";
757
1206
  client;
@@ -759,23 +1208,27 @@ var init_openai = __esm({
759
1208
  this.client = client ?? new import_openai2.default({ apiKey });
760
1209
  }
761
1210
  async create(spec, req) {
1211
+ const signal = signalWithDeadline(req.signal, req.deadline);
762
1212
  try {
763
1213
  const res = await this.client.chat.completions.create(
764
- buildParams2(spec, req, false)
1214
+ buildParams2(spec, req, false),
1215
+ { signal }
765
1216
  );
766
1217
  return mapResponse(res, req);
767
1218
  } catch (err) {
768
- mapProviderError(err, "openai");
1219
+ mapProviderError(err, "openai", signal);
769
1220
  }
770
1221
  }
771
1222
  async *stream(spec, req) {
1223
+ const signal = signalWithDeadline(req.signal, req.deadline);
772
1224
  try {
773
1225
  const stream = await this.client.chat.completions.create(
774
- buildParams2(spec, req, true)
1226
+ buildParams2(spec, req, true),
1227
+ { signal }
775
1228
  );
776
1229
  yield* mapStream(stream, req);
777
1230
  } catch (err) {
778
- yield { type: "error", error: normalizeError(err, "openai").toShape() };
1231
+ yield { type: "error", error: normalizeError(err, "openai", signal).toShape() };
779
1232
  }
780
1233
  }
781
1234
  };
@@ -878,7 +1331,7 @@ function mapResponse2(res, req) {
878
1331
  else if (p.functionCall) {
879
1332
  sawToolUse = true;
880
1333
  const name = p.functionCall.name ?? "";
881
- content.push({ type: "tool_use", id: p.functionCall.id ?? name, name, input: p.functionCall.args ?? {} });
1334
+ content.push({ type: "tool_use", id: p.functionCall.id ?? name, name, input: toolInput(p.functionCall.args ?? {}, "google", name) });
882
1335
  }
883
1336
  }
884
1337
  return {
@@ -919,6 +1372,7 @@ function mapUsage3(res) {
919
1372
  var init_response3 = __esm({
920
1373
  "src/providers/google/response.ts"() {
921
1374
  "use strict";
1375
+ init_tool_input();
922
1376
  }
923
1377
  });
924
1378
 
@@ -990,6 +1444,7 @@ var init_google = __esm({
990
1444
  init_request4();
991
1445
  init_response3();
992
1446
  init_stream3();
1447
+ init_abort();
993
1448
  GoogleProvider = class {
994
1449
  id = "google";
995
1450
  client;
@@ -997,19 +1452,25 @@ var init_google = __esm({
997
1452
  this.client = client ?? new import_genai2.GoogleGenAI({ apiKey });
998
1453
  }
999
1454
  async create(spec, req) {
1455
+ const signal = signalWithDeadline(req.signal, req.deadline);
1000
1456
  try {
1001
- const res = await this.client.models.generateContent(buildParams3(spec, req));
1457
+ const params = buildParams3(spec, req);
1458
+ params.config = { ...params.config, abortSignal: signal };
1459
+ const res = await this.client.models.generateContent(params);
1002
1460
  return mapResponse2(res, req);
1003
1461
  } catch (err) {
1004
- mapProviderError(err, "google");
1462
+ mapProviderError(err, "google", signal);
1005
1463
  }
1006
1464
  }
1007
1465
  async *stream(spec, req) {
1466
+ const signal = signalWithDeadline(req.signal, req.deadline);
1008
1467
  try {
1009
- const iter = await this.client.models.generateContentStream(buildParams3(spec, req));
1468
+ const params = buildParams3(spec, req);
1469
+ params.config = { ...params.config, abortSignal: signal };
1470
+ const iter = await this.client.models.generateContentStream(params);
1010
1471
  yield* mapStream2(iter, req);
1011
1472
  } catch (err) {
1012
- yield { type: "error", error: normalizeError(err, "google").toShape() };
1473
+ yield { type: "error", error: normalizeError(err, "google", signal).toShape() };
1013
1474
  }
1014
1475
  }
1015
1476
  };
@@ -1022,11 +1483,13 @@ __export(index_exports, {
1022
1483
  AGENT_SCHEMA: () => AGENT_SCHEMA,
1023
1484
  ANTHROPIC_MODELS: () => ANTHROPIC_MODELS,
1024
1485
  AuthError: () => AuthError,
1486
+ CancelledError: () => CancelledError,
1025
1487
  CapabilityError: () => CapabilityError,
1026
1488
  ConfigError: () => ConfigError,
1027
1489
  ContextWindowError: () => ContextWindowError,
1028
1490
  DEFAULT_CATALOG: () => DEFAULT_CATALOG,
1029
1491
  DEFAULT_SECRET_NAMES: () => DEFAULT_SECRET_NAMES,
1492
+ DeadlineExceededError: () => DeadlineExceededError,
1030
1493
  GOOGLE_MODELS: () => GOOGLE_MODELS,
1031
1494
  InMemoryScope: () => InMemoryScope,
1032
1495
  InvalidRequestError: () => InvalidRequestError,
@@ -1037,6 +1500,7 @@ __export(index_exports, {
1037
1500
  ProviderError: () => ProviderError,
1038
1501
  RateLimitError: () => RateLimitError,
1039
1502
  TaintError: () => TaintError,
1503
+ ToolInputError: () => ToolInputError,
1040
1504
  addUsage: () => addUsage,
1041
1505
  assertSinkAcceptsTaint: () => assertSinkAcceptsTaint,
1042
1506
  blocksOf: () => blocksOf,
@@ -1082,6 +1546,7 @@ __export(index_exports, {
1082
1546
  structuredMatch: () => structuredMatch,
1083
1547
  taint: () => taint,
1084
1548
  toOracleTool: () => toOracleTool,
1549
+ validateJsonSchema: () => validateJsonSchema,
1085
1550
  validateRequest: () => validateRequest
1086
1551
  });
1087
1552
  module.exports = __toCommonJS(index_exports);
@@ -1091,7 +1556,7 @@ init_shape();
1091
1556
 
1092
1557
  // src/shape/capabilities.ts
1093
1558
  init_shape();
1094
- function chatCapabilities(overrides = {}) {
1559
+ function chatCapabilities(overrides2 = {}) {
1095
1560
  return {
1096
1561
  kind: "chat",
1097
1562
  textIn: true,
@@ -1104,7 +1569,7 @@ function chatCapabilities(overrides = {}) {
1104
1569
  streaming: true,
1105
1570
  structuredOutput: true,
1106
1571
  webSearch: false,
1107
- ...overrides
1572
+ ...overrides2
1108
1573
  };
1109
1574
  }
1110
1575
  function validateRequest(spec, req) {
@@ -1196,6 +1661,15 @@ var OPENAI_MODELS = [
1196
1661
  capabilities: chatCapabilities({ effort: true }),
1197
1662
  contextWindow: 4e5
1198
1663
  },
1664
+ {
1665
+ // Compact GPT-5.4 reasoning model (effort knob), same 400K context as the
1666
+ // rest of the GPT-5 line. The default for odla-o11y AI triage.
1667
+ id: "gpt-5.4-mini",
1668
+ provider: "openai",
1669
+ nativeId: "gpt-5.4-mini",
1670
+ capabilities: chatCapabilities({ effort: true }),
1671
+ contextWindow: 4e5
1672
+ },
1199
1673
  {
1200
1674
  id: "gpt-5",
1201
1675
  provider: "openai",
@@ -1313,11 +1787,29 @@ function providersInCatalog(catalog) {
1313
1787
  }
1314
1788
 
1315
1789
  // src/providers/registry.ts
1790
+ var CLIENT_TTL_MS = 6e4;
1316
1791
  var cache = /* @__PURE__ */ new Map();
1792
+ var inflight = /* @__PURE__ */ new Map();
1793
+ var overrides = /* @__PURE__ */ new Map();
1317
1794
  async function getProvider(id, apiKey) {
1318
- const cacheKey = `${id}:${apiKey}`;
1319
- const existing = cache.get(cacheKey);
1320
- if (existing) return existing;
1795
+ const override = overrides.get(id);
1796
+ if (override?.apiKey === apiKey) return override.provider;
1797
+ const fingerprint = await fingerprintKey(apiKey);
1798
+ const existing = cache.get(id);
1799
+ if (existing?.fingerprint === fingerprint && existing.expiresAt > Date.now()) return existing.provider;
1800
+ if (existing) cache.delete(id);
1801
+ const pending = inflight.get(id);
1802
+ if (pending?.fingerprint === fingerprint) return pending.promise;
1803
+ const promise = createProvider(id, apiKey).then((provider) => {
1804
+ cache.set(id, { fingerprint, provider, expiresAt: Date.now() + CLIENT_TTL_MS });
1805
+ return provider;
1806
+ }).finally(() => {
1807
+ if (inflight.get(id)?.promise === promise) inflight.delete(id);
1808
+ });
1809
+ inflight.set(id, { fingerprint, promise });
1810
+ return promise;
1811
+ }
1812
+ async function createProvider(id, apiKey) {
1321
1813
  let provider;
1322
1814
  switch (id) {
1323
1815
  case "anthropic": {
@@ -1336,14 +1828,19 @@ async function getProvider(id, apiKey) {
1336
1828
  break;
1337
1829
  }
1338
1830
  }
1339
- cache.set(cacheKey, provider);
1340
1831
  return provider;
1341
1832
  }
1833
+ async function fingerprintKey(apiKey) {
1834
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
1835
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1836
+ }
1342
1837
  function registerProvider(id, apiKey, provider) {
1343
- cache.set(`${id}:${apiKey}`, provider);
1838
+ overrides.set(id, { apiKey, provider });
1344
1839
  }
1345
1840
  function clearProviderCache() {
1346
1841
  cache.clear();
1842
+ inflight.clear();
1843
+ overrides.clear();
1347
1844
  }
1348
1845
 
1349
1846
  // src/client/keys.ts
@@ -1414,7 +1911,9 @@ function init(opts = {}) {
1414
1911
  messages: [{ role: "user", content: c.user }],
1415
1912
  tools: [{ name: c.tool.name, description: c.tool.description ?? "", inputSchema: c.tool.parameters }],
1416
1913
  toolChoice: { type: "tool", name: c.tool.name },
1417
- maxTokens: c.maxTokens ?? 4096
1914
+ maxTokens: c.maxTokens ?? 4096,
1915
+ signal: c.signal,
1916
+ deadline: c.deadline
1418
1917
  };
1419
1918
  const { spec, provider } = await resolveProviderFor(model);
1420
1919
  validateRequest(spec, req);
@@ -1423,6 +1922,11 @@ function init(opts = {}) {
1423
1922
  if (!block || block.type !== "tool_use") {
1424
1923
  throw new ProviderError(`Model "${model}" did not return the forced tool call "${c.tool.name}".`);
1425
1924
  }
1925
+ const validation = validateJsonSchema(c.tool.parameters, block.input);
1926
+ if (!validation.valid) {
1927
+ const detail = validation.issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ");
1928
+ throw new ToolInputError(`Tool "${c.tool.name}" returned invalid input: ${detail}`, { tool: c.tool.name });
1929
+ }
1426
1930
  return { value: block.input, usage: res.usage };
1427
1931
  };
1428
1932
  const search = async (c) => {
@@ -1432,7 +1936,9 @@ function init(opts = {}) {
1432
1936
  system: c.system,
1433
1937
  messages: [{ role: "user", content: c.user }],
1434
1938
  webSearch: true,
1435
- maxTokens: c.maxTokens ?? 8192
1939
+ maxTokens: c.maxTokens ?? 8192,
1940
+ signal: c.signal,
1941
+ deadline: c.deadline
1436
1942
  };
1437
1943
  const { spec, provider } = await resolveProviderFor(model);
1438
1944
  validateRequest(spec, req);
@@ -1461,20 +1967,20 @@ function generateLlmsTxt(catalog = DEFAULT_CATALOG) {
1461
1967
  const models = Object.values(catalog).map((s) => `- \`${s.id}\` (${s.provider}) \u2014 ${capsSummary(s.capabilities)}`).join("\n");
1462
1968
  return `# @odla-ai/ai \u2014 LLM context
1463
1969
 
1464
- > EXPERIMENTAL \u2014 an agentic-coding experiment. This package is built and operated
1465
- > by autonomous coding agents as an experiment in agentic loops. APIs change
1466
- > without notice and nothing here is production-hardened. Use at your own risk.
1970
+ > EARLY ACCESS \u2014 pre-1.0. Agents work from bounded runbooks; humans approve
1971
+ > credentials, production changes, releases, and merges. APIs and exact package
1972
+ > availability can change. Review documented guarantees and limitations; this
1973
+ > software is MIT-licensed and provided without warranty.
1467
1974
 
1468
1975
  One general-purpose interface for AI inference across Anthropic (Claude), OpenAI
1469
1976
  (GPT), and Google (Gemini) \u2014 text, image, and audio \u2014 plus a tool-use agent
1470
1977
  engine and an eval harness. Library-only: import in-process, bring your own keys.
1471
- Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loaded.
1978
+ Targets Node 20+ and Cloudflare Workers. Provider adapters are runtime-lazy, but
1979
+ all provider SDKs are package dependencies. Never expose long-lived keys in a browser.
1472
1980
 
1473
1981
  ## Install
1474
1982
 
1475
1983
  npm i @odla-ai/ai
1476
- # provider SDKs are peer-lazy-loaded; install those you use:
1477
- # @anthropic-ai/sdk openai @google/genai
1478
1984
  # optional, for odla-db-backed storage + secrets:
1479
1985
  # @odla-ai/db
1480
1986
 
@@ -1488,7 +1994,8 @@ Isomorphic (Node 20+, Cloudflare Workers, browser). Provider SDKs are lazy-loade
1488
1994
  - \`ai.stream(input)\` \u2192 async iterable of normalized events: message_start \u2192
1489
1995
  content_block_start \u2192 content_block_delta {text_delta|thinking_delta|input_json_delta}
1490
1996
  \u2192 content_block_stop \u2192 message_delta \u2192 message_stop (plus \`error\`).
1491
- - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns a parsed object.
1997
+ - \`ai.extract<T>({ model, user, tool })\` \u2192 forced tool call; returns an object
1998
+ validated against the declared JSON Schema. Unsupported assertions fail closed.
1492
1999
  - \`ai.search({ model, user })\` \u2192 web-search-grounded prose.
1493
2000
 
1494
2001
  ## Content blocks (multimodal)
@@ -1507,7 +2014,15 @@ audio block to a Claude model throws CapabilityError before any network call.
1507
2014
  // run.finalText, run.toolCalls, run.usage, run.steps, run.stoppedReason
1508
2015
 
1509
2016
  Tools may carry taint labels (outputTaint / acceptsTaint) for a CaMeL-style
1510
- prompt-injection gate. Personas may carry a MemoryScope.
2017
+ prompt-injection gate. Personas may carry a MemoryScope. Tool inputs are schema-
2018
+ validated before handlers run. Pass signal/deadline and a run budget
2019
+ ({maxInputTokens,maxOutputTokens,maxTotalTokens,maxToolCalls}) to bound follow-up
2020
+ effects and requested output. Input and total limits are post-turn ceilings:
2021
+ provider usage is unavailable before the first response, so they cannot
2022
+ tokenizer-bound the first prompt. Output and remaining-total limits cap the next
2023
+ request's maxTokens. Token limits must be positive integers; maxToolCalls may be
2024
+ zero. Budget exits pair every requested tool_use with an error tool_result before
2025
+ memory is persisted.
1511
2026
 
1512
2027
  ## Skills
1513
2028
 
@@ -1534,14 +2049,17 @@ device-authorization handshake for a scoped, revocable token, then provisions.
1534
2049
  import { requestToken, init as odlaInit } from "@odla-ai/db";
1535
2050
  import { provisionAgentApp, odlaDbKeyResolver, OdlaDbMemory, persistRun, init } from "@odla-ai/ai";
1536
2051
 
1537
- // 1. Handshake: the human supplies the endpoint and approves a short code in odla-db Studio.
1538
- const { token } = await requestToken({ endpoint: ODLA_URL, onCode: ({ userCode }) => show(userCode) });
2052
+ const ODLA_PLATFORM = "https://odla.ai";
2053
+ const ODLA_DB_URL = "https://db.odla.ai";
2054
+
2055
+ // 1. Handshake: the human approves a short code at odla.ai.
2056
+ const { token } = await requestToken({ endpoint: ODLA_PLATFORM, onCode: ({ userCode }) => show(userCode) });
1539
2057
 
1540
2058
  // 2. Provision: create app, mint app key, push agent schema, store BYO provider keys as secrets.
1541
- const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_URL, token, providerKeys: { openai: OPENAI_KEY } });
2059
+ const { appId, appKey } = await provisionAgentApp({ endpoint: ODLA_PLATFORM, token, providerKeys: { openai: OPENAI_KEY } });
1542
2060
 
1543
2061
  // 3. Runtime: read keys from odla-db secrets; persist memory + run traces.
1544
- const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
2062
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_DB_URL });
1545
2063
  const ai = init({ resolveKey: odlaDbKeyResolver(db) });
1546
2064
  const persona = { name: "assistant", model: "gpt-5", memory: new OdlaDbMemory({ db, sessionId: "user-42" }) };
1547
2065
  const run = await runAgent(ai, persona, { input: "hello" });
@@ -1567,16 +2085,22 @@ env's odla-db tenant secrets, write-only for operators). One call reads both:
1567
2085
  platform: "https://odla.ai", appId: APP_ID, env: "prod", db });
1568
2086
  // ai.chat / ai.stream / ai.extract \u2014 provider/model from public-config
1569
2087
  // (cached ~60s, so Studio changes apply without redeploys); the key is
1570
- // fetched from the vault at call time via odlaDbKeyResolver.
2088
+ // fetched from the vault via a finite-TTL odlaDbKeyResolver.
1571
2089
 
1572
2090
  The only secret the Worker carries is its odla-db app key. Rotating the LLM
1573
- key = writing the secret again; switching provider/model = a Studio edit.
2091
+ key = writing the secret again; the default warm-isolate key TTL is 60 seconds
2092
+ and cacheVersion provides immediate invalidation. Provider SDK clients are
2093
+ fingerprinted, bounded to one generation per provider, and expire after 60
2094
+ seconds. The platform provider is enforced: models from other providers are
2095
+ absent from the facade catalog. If no default model is configured, each call
2096
+ must name a model for that provider. Only public config is shared; tenant-bound
2097
+ Ai facades are not cached globally.
1574
2098
 
1575
2099
  ## Errors
1576
2100
 
1577
2101
  Every error is an OdlaAIError subclass with a stable \`code\` \u2014 branch on err.code:
1578
2102
  config, auth, rate_limit, capability_unsupported, context_window, invalid_request,
1579
- tool_input_invalid, provider_error.
2103
+ tool_input_invalid, cancelled, deadline_exceeded, provider_error.
1580
2104
 
1581
2105
  ## Models
1582
2106
 
@@ -1592,6 +2116,7 @@ init_errors2();
1592
2116
 
1593
2117
  // src/agent/loop.ts
1594
2118
  init_shape();
2119
+ init_abort();
1595
2120
 
1596
2121
  // src/agent/tools.ts
1597
2122
  function toOracleTool(t) {
@@ -1633,6 +2158,7 @@ function assertSinkAcceptsTaint(sink, allowed, actual) {
1633
2158
 
1634
2159
  // src/agent/loop.ts
1635
2160
  async function runAgent(inference, persona, input) {
2161
+ validateRunLimits(persona.maxSteps, input.budget);
1636
2162
  const { system, tools } = composeSkills(persona.system, persona.tools, persona.skills);
1637
2163
  const handlerByName = /* @__PURE__ */ new Map();
1638
2164
  for (const t of tools) if (t.handler) handlerByName.set(t.name, t);
@@ -1644,21 +2170,26 @@ async function runAgent(inference, persona, input) {
1644
2170
  const toolCalls = [];
1645
2171
  const accumulatedTaint = /* @__PURE__ */ new Set(["llm_inherited"]);
1646
2172
  const maxSteps = persona.maxSteps ?? 8;
2173
+ const runSignal = signalWithDeadline(input.signal, input.deadline);
1647
2174
  let response;
1648
2175
  let stoppedReason = "max_steps";
1649
2176
  let steps = 0;
2177
+ let attemptedToolCalls = 0;
1650
2178
  while (steps < maxSteps) {
1651
2179
  steps++;
2180
+ const remainingOutput = remainingOutputTokens(usage, input.budget);
1652
2181
  response = await inference.chat({
1653
2182
  model: persona.model,
1654
2183
  system,
1655
2184
  messages,
1656
2185
  tools: tools.length > 0 ? tools.map(toOracleTool) : void 0,
1657
- maxTokens: persona.maxTokens ?? 4096,
2186
+ maxTokens: Math.min(persona.maxTokens ?? 4096, remainingOutput ?? Number.POSITIVE_INFINITY),
1658
2187
  effort: persona.effort,
1659
2188
  thinking: persona.thinking,
1660
2189
  temperature: persona.temperature,
1661
- webSearch: persona.webSearch
2190
+ webSearch: persona.webSearch,
2191
+ signal: runSignal,
2192
+ deadline: input.deadline
1662
2193
  });
1663
2194
  addUsage(usage, response.usage);
1664
2195
  messages.push({ role: "assistant", content: response.content });
@@ -1671,8 +2202,22 @@ async function runAgent(inference, persona, input) {
1671
2202
  stoppedReason = "end_turn";
1672
2203
  break;
1673
2204
  }
2205
+ if (budgetExceeded(usage, input.budget)) {
2206
+ stoppedReason = "budget_exhausted";
2207
+ messages.push({
2208
+ role: "user",
2209
+ content: toolUses.map((toolUse) => errorResult(toolUse.id, "Run budget exhausted before tool execution."))
2210
+ });
2211
+ break;
2212
+ }
1674
2213
  const resultBlocks = [];
1675
2214
  for (const toolUse of toolUses) {
2215
+ if (input.budget?.maxToolCalls !== void 0 && attemptedToolCalls >= input.budget.maxToolCalls) {
2216
+ stoppedReason = "budget_exhausted";
2217
+ resultBlocks.push(errorResult(toolUse.id, "Run tool-call budget exhausted before execution."));
2218
+ continue;
2219
+ }
2220
+ attemptedToolCalls++;
1676
2221
  const def = handlerByName.get(toolUse.name);
1677
2222
  if (!def || !def.handler) {
1678
2223
  resultBlocks.push(errorResult(toolUse.id, `No local handler for tool "${toolUse.name}".`));
@@ -1680,16 +2225,26 @@ async function runAgent(inference, persona, input) {
1680
2225
  }
1681
2226
  if (def.acceptsTaint) assertSinkAcceptsTaint(def.name, def.acceptsTaint, accumulatedTaint);
1682
2227
  let output;
1683
- try {
1684
- output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: input.signal });
1685
- } catch (err) {
1686
- output = { content: `Tool "${toolUse.name}" threw: ${err.message}`, isError: true };
2228
+ const validation = validateJsonSchema(def.inputSchema, toolUse.input);
2229
+ if (!validation.valid) {
2230
+ const detail = validation.issues.slice(0, 5).map((i) => `${i.path} ${i.message}`).join("; ");
2231
+ output = { content: `Tool "${toolUse.name}" input is invalid: ${detail}`, isError: true };
2232
+ } else {
2233
+ throwIfAborted(runSignal);
2234
+ try {
2235
+ output = await def.handler(toolUse.input, { taint: new Set(accumulatedTaint), signal: runSignal });
2236
+ } catch (error) {
2237
+ if (error instanceof CancelledError || error instanceof DeadlineExceededError) throw error;
2238
+ throwIfAborted(runSignal);
2239
+ output = { content: `Tool "${toolUse.name}" failed.`, isError: true };
2240
+ }
1687
2241
  }
1688
2242
  toolCalls.push({ toolUse, output });
1689
2243
  resultBlocks.push({ type: "tool_result", toolUseId: toolUse.id, content: output.content, isError: output.isError });
1690
2244
  for (const label of def.outputTaint ?? []) accumulatedTaint.add(label);
1691
2245
  }
1692
2246
  messages.push({ role: "user", content: resultBlocks });
2247
+ if (stoppedReason === "budget_exhausted") break;
1693
2248
  }
1694
2249
  if (!response) throw new Error("runAgent: maxSteps must be at least 1");
1695
2250
  if (persona.memory) await persona.memory.append(messages.slice(runStartIndex));
@@ -1703,6 +2258,36 @@ async function runAgent(inference, persona, input) {
1703
2258
  stoppedReason
1704
2259
  };
1705
2260
  }
2261
+ function budgetExceeded(usage, budget) {
2262
+ if (!budget) return false;
2263
+ if (budget.maxInputTokens !== void 0 && usage.inputTokens >= budget.maxInputTokens) return true;
2264
+ if (budget.maxOutputTokens !== void 0 && usage.outputTokens >= budget.maxOutputTokens) return true;
2265
+ if (budget.maxTotalTokens !== void 0 && usage.inputTokens + usage.outputTokens >= budget.maxTotalTokens) return true;
2266
+ return false;
2267
+ }
2268
+ function remainingOutputTokens(usage, budget) {
2269
+ if (!budget) return void 0;
2270
+ const remaining = [
2271
+ budget.maxOutputTokens === void 0 ? void 0 : budget.maxOutputTokens - usage.outputTokens,
2272
+ budget.maxTotalTokens === void 0 ? void 0 : budget.maxTotalTokens - usage.inputTokens - usage.outputTokens
2273
+ ].filter((value) => value !== void 0);
2274
+ return remaining.length === 0 ? void 0 : Math.max(1, Math.min(...remaining));
2275
+ }
2276
+ function validateRunLimits(maxSteps, budget) {
2277
+ if (maxSteps !== void 0 && (!Number.isInteger(maxSteps) || maxSteps < 1)) {
2278
+ throw new ConfigError("persona.maxSteps must be a positive integer.");
2279
+ }
2280
+ if (!budget) return;
2281
+ for (const key of ["maxInputTokens", "maxOutputTokens", "maxTotalTokens"]) {
2282
+ const value = budget[key];
2283
+ if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
2284
+ throw new ConfigError(`budget.${key} must be a positive integer.`);
2285
+ }
2286
+ }
2287
+ if (budget.maxToolCalls !== void 0 && (!Number.isInteger(budget.maxToolCalls) || budget.maxToolCalls < 0)) {
2288
+ throw new ConfigError("budget.maxToolCalls must be a non-negative integer.");
2289
+ }
2290
+ }
1706
2291
  function errorResult(toolUseId, message) {
1707
2292
  return { type: "tool_result", toolUseId, content: message, isError: true };
1708
2293
  }
@@ -2007,22 +2592,32 @@ var DEFAULT_SECRET_NAMES = {
2007
2592
  };
2008
2593
  function odlaDbKeyResolver(db, opts = {}) {
2009
2594
  const nameFor = opts.secretName ?? ((p) => DEFAULT_SECRET_NAMES[p]);
2010
- const useCache = opts.cache ?? true;
2595
+ const ttlMs = opts.cache === false ? 0 : Math.max(0, opts.ttlMs ?? 6e4);
2011
2596
  const cache3 = /* @__PURE__ */ new Map();
2012
2597
  return async (provider) => {
2013
- if (useCache) {
2598
+ const version = typeof opts.cacheVersion === "function" ? opts.cacheVersion(provider) : opts.cacheVersion ?? "";
2599
+ if (ttlMs > 0) {
2014
2600
  const hit = cache3.get(provider);
2015
- if (hit !== void 0) return hit;
2601
+ if (hit?.version === version && hit.expiresAt > Date.now()) return hit.value;
2602
+ cache3.delete(provider);
2016
2603
  }
2017
2604
  try {
2018
2605
  const key = await db.secrets.get(nameFor(provider));
2019
- if (useCache) cache3.set(provider, key);
2606
+ if (ttlMs > 0) cache3.set(provider, { version, value: key, expiresAt: Date.now() + ttlMs });
2020
2607
  return key;
2021
- } catch {
2022
- return void 0;
2608
+ } catch (error) {
2609
+ if (isMissingSecret(error)) return void 0;
2610
+ throw error;
2023
2611
  }
2024
2612
  };
2025
2613
  }
2614
+ function isMissingSecret(error) {
2615
+ if (!error || typeof error !== "object") return false;
2616
+ const rec = error;
2617
+ if (rec.status === 404 || rec.statusCode === 404 || rec.code === "not_found") return true;
2618
+ const message = error instanceof Error ? error.message : "";
2619
+ return /\b404\b/.test(message);
2620
+ }
2026
2621
 
2027
2622
  // src/store/platform.ts
2028
2623
  init_shape();
@@ -2054,19 +2649,22 @@ async function initFromPlatform(opts) {
2054
2649
  const key = `${opts.platform}|${opts.appId}|${opts.env}`;
2055
2650
  const now = Date.now();
2056
2651
  const hit = ttl > 0 ? cache2.get(key) : void 0;
2057
- if (hit && hit.expiresAt > now) return hit.value;
2058
- const { provider, model } = await fetchAiConfig(opts);
2059
- if (hit && hit.value.provider === provider && hit.value.model === model) {
2060
- hit.expiresAt = now + ttl;
2061
- return hit.value;
2062
- }
2652
+ const config = hit && hit.expiresAt > now ? hit.value : await fetchAiConfig(opts);
2653
+ const { provider, model } = config;
2654
+ if (ttl > 0 && (!hit || hit.expiresAt <= now)) cache2.set(key, { value: config, expiresAt: now + ttl });
2655
+ const sourceCatalog = opts.initOptions?.catalog ?? DEFAULT_CATALOG;
2656
+ const catalog = Object.fromEntries(
2657
+ Object.entries(sourceCatalog).filter(([, spec]) => spec.provider === provider)
2658
+ );
2659
+ const defaultModel = model ?? opts.initOptions?.defaultModel;
2660
+ if (defaultModel) resolveModel(catalog, defaultModel);
2063
2661
  const ai = init({
2064
2662
  ...opts.initOptions,
2065
- resolveKey: odlaDbKeyResolver(opts.db),
2066
- defaultModel: model ?? opts.initOptions?.defaultModel
2663
+ catalog,
2664
+ resolveKey: odlaDbKeyResolver(opts.db, opts.keyResolverOptions),
2665
+ defaultModel
2067
2666
  });
2068
2667
  const value = { ai, provider, ...model ? { model } : {} };
2069
- if (ttl > 0) cache2.set(key, { value, expiresAt: now + ttl });
2070
2668
  return value;
2071
2669
  }
2072
2670