@orkestrel/toolbox 0.0.13 → 0.0.15

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.
@@ -10,6 +10,7 @@ import type { ConversationStoreInterface } from '@orkestrel/agent';
10
10
  import type { DatabaseErrorCode } from '@orkestrel/database';
11
11
  import type { DatabaseInterface } from '@orkestrel/database';
12
12
  import type { DriverInterface } from '@orkestrel/database';
13
+ import type { Guard } from '@orkestrel/contract';
13
14
  import type { Include } from '@orkestrel/relation';
14
15
  import type { IndexMap } from '@orkestrel/database';
15
16
  import { JSONShape } from '@orkestrel/contract';
@@ -29,6 +30,7 @@ import { StringShape } from '@orkestrel/contract';
29
30
  import type { TableInterface } from '@orkestrel/database';
30
31
  import type { TableMap } from '@orkestrel/database';
31
32
  import type { TerminalManagerInterface } from '@orkestrel/terminal';
33
+ import type { ToolContext } from '@orkestrel/tool';
32
34
  import type { ToolInterface } from '@orkestrel/tool';
33
35
  import type { ToolManagerInterface } from '@orkestrel/tool';
34
36
  import { UnionShape } from '@orkestrel/contract';
@@ -571,11 +573,14 @@ export declare function createDatabaseDefinitionStore(driver?: DriverInterface):
571
573
  * import { createDatabaseTool } from '@src/core'
572
574
  *
573
575
  * const tool = createDatabaseTool()
574
- * await tool.execute({
575
- * operation: 'create',
576
- * id: 'shop',
577
- * tables: { products: { columns: { name: 'string', price: 'number' } } },
578
- * })
576
+ * await tool.execute(
577
+ * {
578
+ * operation: 'create',
579
+ * id: 'shop',
580
+ * tables: { products: { columns: { name: 'string', price: 'number' } } },
581
+ * },
582
+ * { signal: new AbortController().signal },
583
+ * )
579
584
  * ```
580
585
  */
581
586
  export declare function createDatabaseTool(options?: DatabaseToolOptions): ToolInterface;
@@ -638,8 +643,10 @@ export declare function createDescribeTool(tools: ToolManagerInterface): ToolInt
638
643
  * never asserted, and a key outside the closed inferred schema is silently dropped rather than
639
644
  * rejected (see {@link import('./types.js').EndpointToolOptions.validate}). With
640
645
  * `validate: false`, the tool's `execute` passes through the model-supplied `args` to
641
- * `definition.execute` without re-validation — the raw-passthrough opt-out. Either way, the
642
- * definition's return flows back as the tool call's plain result; a throw propagates uncaught,
646
+ * `definition.execute` without re-validation — the raw-passthrough opt-out. The execution context
647
+ * the tool's `execute` receives reaches `definition.execute` unchanged whichever way `args` are
648
+ * handled, so a handler that reads `context.signal` observes the caller's cancellation. Either way,
649
+ * the definition's return flows back as the tool call's plain result; a throw propagates uncaught,
643
650
  * isolated by the `ToolManagerInterface` (`@orkestrel/tool`) into the canonical error envelope
644
651
  * — never caught or re-wrapped here.
645
652
  *
@@ -862,7 +869,10 @@ export declare function createPromptTool(options: PromptToolOptions): ToolInterf
862
869
  * import { createRelationTool } from '@src/core'
863
870
  *
864
871
  * const tool = createRelationTool({ managers: { shop: manager } })
865
- * await tool.execute({ operation: 'load', model: 'accounts', key: 'acc1', include: ['contacts'] })
872
+ * await tool.execute(
873
+ * { operation: 'load', model: 'accounts', key: 'acc1', include: ['contacts'] },
874
+ * { signal: new AbortController().signal },
875
+ * )
866
876
  * ```
867
877
  */
868
878
  export declare function createRelationTool(options: RelationToolOptions): ToolInterface;
@@ -876,7 +886,9 @@ export declare function createRelationTool(options: RelationToolOptions): ToolIn
876
886
  * (`{ publish: createToolFunction(tools, 'publish') }`); the pure workflow runner has no
877
887
  * knowledge of tools itself. The returned function executes `name` against `tools` with the
878
888
  * task's `controller.input` as the call arguments. The adapter resolves the live tool, calls its
879
- * `execute` method directly, and deep-gates the returned unknown through `parseJSONValue`.
889
+ * `execute` method directly, and deep-gates the returned unknown through `parseJSONValue`. The
890
+ * execution context it passes carries the task's own `controller.signal`, so a handler that
891
+ * reads `context.signal` observes the caller's cancellation.
880
892
  * Genuine execution throws retain identity. An unregistered name or non-JSON return throws this
881
893
  * package's typed `TOOL` `ToolboxError`.
882
894
  *
@@ -1561,12 +1573,19 @@ export declare interface EndpointDefinition {
1561
1573
 
1562
1574
  /**
1563
1575
  * Represents the handler {@link EndpointDefinition.execute} implements — it mirrors
1564
- * `@orkestrel/tool`'s `ToolOptions.execute` signature exactly (the same
1565
- * `Readonly<Record<string, unknown>>` argument, the same `Promise<unknown> | unknown` return), so
1566
- * `execute: (args) => definition.execute(args)` typechecks with zero assertions in
1567
- * {@link import('./factories.js').createEndpointTool}.
1576
+ * `@orkestrel/tool`'s `ToolOptions.execute` signature exactly, so
1577
+ * {@link import('./factories.js').createEndpointTool} forwards the execution context it receives
1578
+ * to the handler unchanged and a handler that reads `context.signal` observes the caller's
1579
+ * cancellation.
1580
+ *
1581
+ * @remarks
1582
+ * The mirrored signature is the same `Readonly<Record<string, unknown>>` argument, the same
1583
+ * `ToolContext` second parameter, and the same `Promise<unknown> | unknown` return. A handler that
1584
+ * declares `args` alone still satisfies the type, the way a function declaring fewer parameters
1585
+ * than its contract does, so an endpoint definition written against the argument alone keeps
1586
+ * typechecking and reads no signal.
1568
1587
  */
1569
- export declare type EndpointHandler = (args: Readonly<Record<string, unknown>>) => Promise<unknown> | unknown;
1588
+ export declare type EndpointHandler = (args: Readonly<Record<string, unknown>>, context: ToolContext) => Promise<unknown> | unknown;
1570
1589
 
1571
1590
  /**
1572
1591
  * Represents the construction-time tuning for {@link import('./factories.js').createEndpointTool} —
@@ -1782,7 +1801,7 @@ export declare function isAgentFunction(value: unknown): value is AgentFunction;
1782
1801
  * @param value - The value to inspect
1783
1802
  * @returns True if `value` is `'string'`, `'integer'`, `'number'`, or `'boolean'`; false otherwise
1784
1803
  */
1785
- export declare function isColumnPrimitive(value: unknown): value is ColumnPrimitive;
1804
+ export declare const isColumnPrimitive: Guard<ColumnPrimitive>;
1786
1805
 
1787
1806
  /**
1788
1807
  * Narrows an unknown value to a {@link ColumnSpec}.
@@ -2349,7 +2368,7 @@ timeout: OptionalShape<NumberShape>;
2349
2368
  * @remarks
2350
2369
  * Carries a machine-readable `code` (see {@link import('./types.js').ToolboxErrorCode}) and
2351
2370
  * an optional `context` bag for structured diagnostics. They are readable through an in-process
2352
- * catch, including around a direct `tool.execute(args)` call. The `ToolManagerInterface`
2371
+ * catch, including around a direct `tool.execute(args, context)` call. The `ToolManagerInterface`
2353
2372
  * (`@orkestrel/tool`) instead flattens every throw to the canonical tool result's top-level
2354
2373
  * `error` message string under AGENTS' “narrow untrusted input with guards” rule.
2355
2374
  *
@@ -10,6 +10,7 @@ import type { ConversationStoreInterface } from '@orkestrel/agent';
10
10
  import type { DatabaseErrorCode } from '@orkestrel/database';
11
11
  import type { DatabaseInterface } from '@orkestrel/database';
12
12
  import type { DriverInterface } from '@orkestrel/database';
13
+ import type { Guard } from '@orkestrel/contract';
13
14
  import type { Include } from '@orkestrel/relation';
14
15
  import type { IndexMap } from '@orkestrel/database';
15
16
  import { JSONShape } from '@orkestrel/contract';
@@ -29,6 +30,7 @@ import { StringShape } from '@orkestrel/contract';
29
30
  import type { TableInterface } from '@orkestrel/database';
30
31
  import type { TableMap } from '@orkestrel/database';
31
32
  import type { TerminalManagerInterface } from '@orkestrel/terminal';
33
+ import type { ToolContext } from '@orkestrel/tool';
32
34
  import type { ToolInterface } from '@orkestrel/tool';
33
35
  import type { ToolManagerInterface } from '@orkestrel/tool';
34
36
  import { UnionShape } from '@orkestrel/contract';
@@ -571,11 +573,14 @@ export declare function createDatabaseDefinitionStore(driver?: DriverInterface):
571
573
  * import { createDatabaseTool } from '@src/core'
572
574
  *
573
575
  * const tool = createDatabaseTool()
574
- * await tool.execute({
575
- * operation: 'create',
576
- * id: 'shop',
577
- * tables: { products: { columns: { name: 'string', price: 'number' } } },
578
- * })
576
+ * await tool.execute(
577
+ * {
578
+ * operation: 'create',
579
+ * id: 'shop',
580
+ * tables: { products: { columns: { name: 'string', price: 'number' } } },
581
+ * },
582
+ * { signal: new AbortController().signal },
583
+ * )
579
584
  * ```
580
585
  */
581
586
  export declare function createDatabaseTool(options?: DatabaseToolOptions): ToolInterface;
@@ -638,8 +643,10 @@ export declare function createDescribeTool(tools: ToolManagerInterface): ToolInt
638
643
  * never asserted, and a key outside the closed inferred schema is silently dropped rather than
639
644
  * rejected (see {@link import('./types.js').EndpointToolOptions.validate}). With
640
645
  * `validate: false`, the tool's `execute` passes through the model-supplied `args` to
641
- * `definition.execute` without re-validation — the raw-passthrough opt-out. Either way, the
642
- * definition's return flows back as the tool call's plain result; a throw propagates uncaught,
646
+ * `definition.execute` without re-validation — the raw-passthrough opt-out. The execution context
647
+ * the tool's `execute` receives reaches `definition.execute` unchanged whichever way `args` are
648
+ * handled, so a handler that reads `context.signal` observes the caller's cancellation. Either way,
649
+ * the definition's return flows back as the tool call's plain result; a throw propagates uncaught,
643
650
  * isolated by the `ToolManagerInterface` (`@orkestrel/tool`) into the canonical error envelope
644
651
  * — never caught or re-wrapped here.
645
652
  *
@@ -862,7 +869,10 @@ export declare function createPromptTool(options: PromptToolOptions): ToolInterf
862
869
  * import { createRelationTool } from '@src/core'
863
870
  *
864
871
  * const tool = createRelationTool({ managers: { shop: manager } })
865
- * await tool.execute({ operation: 'load', model: 'accounts', key: 'acc1', include: ['contacts'] })
872
+ * await tool.execute(
873
+ * { operation: 'load', model: 'accounts', key: 'acc1', include: ['contacts'] },
874
+ * { signal: new AbortController().signal },
875
+ * )
866
876
  * ```
867
877
  */
868
878
  export declare function createRelationTool(options: RelationToolOptions): ToolInterface;
@@ -876,7 +886,9 @@ export declare function createRelationTool(options: RelationToolOptions): ToolIn
876
886
  * (`{ publish: createToolFunction(tools, 'publish') }`); the pure workflow runner has no
877
887
  * knowledge of tools itself. The returned function executes `name` against `tools` with the
878
888
  * task's `controller.input` as the call arguments. The adapter resolves the live tool, calls its
879
- * `execute` method directly, and deep-gates the returned unknown through `parseJSONValue`.
889
+ * `execute` method directly, and deep-gates the returned unknown through `parseJSONValue`. The
890
+ * execution context it passes carries the task's own `controller.signal`, so a handler that
891
+ * reads `context.signal` observes the caller's cancellation.
880
892
  * Genuine execution throws retain identity. An unregistered name or non-JSON return throws this
881
893
  * package's typed `TOOL` `ToolboxError`.
882
894
  *
@@ -1561,12 +1573,19 @@ export declare interface EndpointDefinition {
1561
1573
 
1562
1574
  /**
1563
1575
  * Represents the handler {@link EndpointDefinition.execute} implements — it mirrors
1564
- * `@orkestrel/tool`'s `ToolOptions.execute` signature exactly (the same
1565
- * `Readonly<Record<string, unknown>>` argument, the same `Promise<unknown> | unknown` return), so
1566
- * `execute: (args) => definition.execute(args)` typechecks with zero assertions in
1567
- * {@link import('./factories.js').createEndpointTool}.
1576
+ * `@orkestrel/tool`'s `ToolOptions.execute` signature exactly, so
1577
+ * {@link import('./factories.js').createEndpointTool} forwards the execution context it receives
1578
+ * to the handler unchanged and a handler that reads `context.signal` observes the caller's
1579
+ * cancellation.
1580
+ *
1581
+ * @remarks
1582
+ * The mirrored signature is the same `Readonly<Record<string, unknown>>` argument, the same
1583
+ * `ToolContext` second parameter, and the same `Promise<unknown> | unknown` return. A handler that
1584
+ * declares `args` alone still satisfies the type, the way a function declaring fewer parameters
1585
+ * than its contract does, so an endpoint definition written against the argument alone keeps
1586
+ * typechecking and reads no signal.
1568
1587
  */
1569
- export declare type EndpointHandler = (args: Readonly<Record<string, unknown>>) => Promise<unknown> | unknown;
1588
+ export declare type EndpointHandler = (args: Readonly<Record<string, unknown>>, context: ToolContext) => Promise<unknown> | unknown;
1570
1589
 
1571
1590
  /**
1572
1591
  * Represents the construction-time tuning for {@link import('./factories.js').createEndpointTool} —
@@ -1782,7 +1801,7 @@ export declare function isAgentFunction(value: unknown): value is AgentFunction;
1782
1801
  * @param value - The value to inspect
1783
1802
  * @returns True if `value` is `'string'`, `'integer'`, `'number'`, or `'boolean'`; false otherwise
1784
1803
  */
1785
- export declare function isColumnPrimitive(value: unknown): value is ColumnPrimitive;
1804
+ export declare const isColumnPrimitive: Guard<ColumnPrimitive>;
1786
1805
 
1787
1806
  /**
1788
1807
  * Narrows an unknown value to a {@link ColumnSpec}.
@@ -2349,7 +2368,7 @@ timeout: OptionalShape<NumberShape>;
2349
2368
  * @remarks
2350
2369
  * Carries a machine-readable `code` (see {@link import('./types.js').ToolboxErrorCode}) and
2351
2370
  * an optional `context` bag for structured diagnostics. They are readable through an in-process
2352
- * catch, including around a direct `tool.execute(args)` call. The `ToolManagerInterface`
2371
+ * catch, including around a direct `tool.execute(args, context)` call. The `ToolManagerInterface`
2353
2372
  * (`@orkestrel/tool`) instead flattens every throw to the canonical tool result's top-level
2354
2373
  * `error` message string under AGENTS' “narrow untrusted input with guards” rule.
2355
2374
  *
@@ -1,4 +1,4 @@
1
- import { arrayShape, attempt, booleanShape, cloneJSONRecord, createContract, integerShape, isFiniteNumber, isNonEmptyString, isRecord, isString, jsonShape, literalShape, numberShape, objectShape, optionalShape, parseJSONValue, rawShape, recordShape, samplesToSchema, schemaToObject, schemaToParameters, schemaToShape, stringShape, unionShape } from "@orkestrel/contract";
1
+ import { arrayShape, attempt, booleanShape, cloneJSONRecord, createContract, integerShape, isArray, isBoolean, isError, isFiniteNumber, isFunction, isInstance, isNonEmptyString, isNumber, isObject, isRecord, isString, jsonShape, literalOf, literalShape, numberShape, objectShape, optionalShape, parseJSONValue, rawShape, recordShape, samplesToSchema, schemaToObject, schemaToParameters, schemaToShape, stringShape, unionShape } from "@orkestrel/contract";
2
2
  import { MAX_TIMER_MS, createWorkflowContract } from "@orkestrel/workflow";
3
3
  import { isTerminalError } from "@orkestrel/terminal";
4
4
  import { createDatabase, createMemoryDriver, isDatabaseError } from "@orkestrel/database";
@@ -590,7 +590,7 @@ var INFER_TOOL_DESCRIPTION = [
590
590
  * @remarks
591
591
  * Carries a machine-readable `code` (see {@link import('./types.js').ToolboxErrorCode}) and
592
592
  * an optional `context` bag for structured diagnostics. They are readable through an in-process
593
- * catch, including around a direct `tool.execute(args)` call. The `ToolManagerInterface`
593
+ * catch, including around a direct `tool.execute(args, context)` call. The `ToolManagerInterface`
594
594
  * (`@orkestrel/tool`) instead flattens every throw to the canonical tool result's top-level
595
595
  * `error` message string under AGENTS' “narrow untrusted input with guards” rule.
596
596
  *
@@ -632,7 +632,7 @@ var ToolboxError = class extends Error {
632
632
  * ```
633
633
  */
634
634
  function isToolboxError(value) {
635
- return value instanceof ToolboxError;
635
+ return isInstance(value, ToolboxError);
636
636
  }
637
637
  //#endregion
638
638
  //#region src/core/validators.ts
@@ -644,7 +644,7 @@ function isToolboxError(value) {
644
644
  */
645
645
  function isWorkflowLineage(value) {
646
646
  const inspected = attempt(() => {
647
- if (!Array.isArray(value)) return false;
647
+ if (!isArray(value)) return false;
648
648
  const seen = /* @__PURE__ */ new Set();
649
649
  for (const [index, tag] of value.entries()) {
650
650
  if (!isString(tag)) return false;
@@ -664,7 +664,7 @@ function isWorkflowLineage(value) {
664
664
  */
665
665
  function isAgentFunction(value) {
666
666
  const inspected = attempt(() => {
667
- if (typeof value !== "function" || !Object.isFrozen(value)) return false;
667
+ if (!isFunction(value) || !Object.isFrozen(value)) return false;
668
668
  const category = Reflect.getOwnPropertyDescriptor(value, "category");
669
669
  const lineage = Reflect.getOwnPropertyDescriptor(value, "lineage");
670
670
  return category?.value === "agent" && lineage !== void 0 && "value" in lineage && Object.isFrozen(lineage.value) && isWorkflowLineage(lineage.value);
@@ -680,7 +680,7 @@ function isAgentFunction(value) {
680
680
  function isColumnSpec(value) {
681
681
  if (isColumnPrimitive(value)) return true;
682
682
  if (!isRecord(value)) return false;
683
- return isColumnPrimitive(value.primitive) && (value.optional === void 0 || typeof value.optional === "boolean");
683
+ return isColumnPrimitive(value.primitive) && (value.optional === void 0 || isBoolean(value.optional));
684
684
  }
685
685
  /**
686
686
  * Narrows an unknown value to a {@link ColumnPrimitive}.
@@ -688,9 +688,7 @@ function isColumnSpec(value) {
688
688
  * @param value - The value to inspect
689
689
  * @returns True if `value` is `'string'`, `'integer'`, `'number'`, or `'boolean'`; false otherwise
690
690
  */
691
- function isColumnPrimitive(value) {
692
- return value === "string" || value === "integer" || value === "number" || value === "boolean";
693
- }
691
+ var isColumnPrimitive = literalOf("string", "integer", "number", "boolean");
694
692
  /**
695
693
  * Narrows an unknown value to a {@link DatabaseDefinition} — a non-empty `id` and `driver`, a
696
694
  * `tables` record whose every value is `{ columns: record of valid ColumnSpec }`, plus optional
@@ -717,9 +715,9 @@ function isDatabaseDefinition(value) {
717
715
  if (value.indexes !== void 0) {
718
716
  if (!isRecord(value.indexes)) return false;
719
717
  for (const groups of Object.values(value.indexes)) {
720
- if (!Array.isArray(groups)) return false;
718
+ if (!isArray(groups)) return false;
721
719
  for (const group of groups) {
722
- if (!Array.isArray(group) || group.length === 0) return false;
720
+ if (!isArray(group) || group.length === 0) return false;
723
721
  for (const column of group) if (!isNonEmptyString(column)) return false;
724
722
  }
725
723
  }
@@ -1575,7 +1573,7 @@ function expandInclude(paths, depth) {
1575
1573
  });
1576
1574
  ancestors.push(branch);
1577
1575
  const existing = branch[segment];
1578
- branch = typeof existing === "object" ? existing : {};
1576
+ branch = isObject(existing) ? existing : {};
1579
1577
  }
1580
1578
  const leaf = segments[last];
1581
1579
  if (leaf === void 0) throw new ToolboxError("TOOL", `malformed include path '${path}'`, {
@@ -2058,7 +2056,9 @@ var DatabaseResolver = class {
2058
2056
  * (`{ publish: createToolFunction(tools, 'publish') }`); the pure workflow runner has no
2059
2057
  * knowledge of tools itself. The returned function executes `name` against `tools` with the
2060
2058
  * task's `controller.input` as the call arguments. The adapter resolves the live tool, calls its
2061
- * `execute` method directly, and deep-gates the returned unknown through `parseJSONValue`.
2059
+ * `execute` method directly, and deep-gates the returned unknown through `parseJSONValue`. The
2060
+ * execution context it passes carries the task's own `controller.signal`, so a handler that
2061
+ * reads `context.signal` observes the caller's cancellation.
2062
2062
  * Genuine execution throws retain identity. An unregistered name or non-JSON return throws this
2063
2063
  * package's typed `TOOL` `ToolboxError`.
2064
2064
  *
@@ -2083,7 +2083,7 @@ function createToolFunction(tools, name) {
2083
2083
  return async (controller) => {
2084
2084
  const tool = tools.tool(name);
2085
2085
  if (tool === void 0) throw new ToolboxError("TOOL", `tool '${name}' is not registered`, { tool: name });
2086
- const result = parseJSONValue(await tool.execute(controller.input));
2086
+ const result = parseJSONValue(await tool.execute(controller.input, { signal: controller.signal }));
2087
2087
  if (result === void 0) throw new ToolboxError("TOOL", `tool '${name}' returned a non-JSON value`, { tool: name });
2088
2088
  return result;
2089
2089
  };
@@ -2208,7 +2208,7 @@ function createWorkflowFunctions(runner, options) {
2208
2208
  const composed = {};
2209
2209
  Object.setPrototypeOf(composed, null);
2210
2210
  for (const [name, fn] of Object.entries(functions)) {
2211
- if (typeof fn !== "function") throw new ToolboxError("TOOL", `workflow function '${name}' is not callable`, { function: name });
2211
+ if (!isFunction(fn)) throw new ToolboxError("TOOL", `workflow function '${name}' is not callable`, { function: name });
2212
2212
  if (isAgentFunction(fn)) throw new ToolboxError("TOOL", `agent function '${name}' must be supplied through agents`, { function: name });
2213
2213
  composed[name] = fn;
2214
2214
  }
@@ -2337,7 +2337,7 @@ function createWorkflowTool(definition, runner, options) {
2337
2337
  const owned = cloned.value;
2338
2338
  let target;
2339
2339
  if (Object.keys(owned).length === 0) target = definition;
2340
- else if (Array.isArray(owned.steps)) {
2340
+ else if (isArray(owned.steps)) {
2341
2341
  const flat = steps.parse(owned);
2342
2342
  target = flat === void 0 ? void 0 : expandSteps(flat);
2343
2343
  } else {
@@ -2885,15 +2885,18 @@ function createDatabaseDefinitionStore(driver = createMemoryDriver()) {
2885
2885
  * import { createDatabaseTool } from '@src/core'
2886
2886
  *
2887
2887
  * const tool = createDatabaseTool()
2888
- * await tool.execute({
2889
- * operation: 'create',
2890
- * id: 'shop',
2891
- * tables: { products: { columns: { name: 'string', price: 'number' } } },
2892
- * })
2888
+ * await tool.execute(
2889
+ * {
2890
+ * operation: 'create',
2891
+ * id: 'shop',
2892
+ * tables: { products: { columns: { name: 'string', price: 'number' } } },
2893
+ * },
2894
+ * { signal: new AbortController().signal },
2895
+ * )
2893
2896
  * ```
2894
2897
  */
2895
2898
  function createDatabaseTool(options = {}) {
2896
- if (options.timeout !== void 0 && (options.timeout < 0 || !Number.isSafeInteger(options.timeout))) throw new ToolboxError("TOOL", "database timeout must be a nonnegative safe integer", { timeout: options.timeout });
2899
+ if (options.timeout !== void 0 && (options.timeout < 0 || !isNumber(options.timeout) || !Number.isSafeInteger(options.timeout))) throw new ToolboxError("TOOL", "database timeout must be a nonnegative safe integer", { timeout: options.timeout });
2897
2900
  const contract = createContract(databaseToolShape);
2898
2901
  const parameters = schemaToParameters(contract.schema);
2899
2902
  const handles = new Map(Object.entries(options.databases ?? {}));
@@ -2963,8 +2966,8 @@ function createDatabaseTool(options = {}) {
2963
2966
  }
2964
2967
  case "get": {
2965
2968
  const table = (await resolver.resolve(call.id)).table(call.table);
2966
- const many = Array.isArray(call.key);
2967
- const keys = Array.isArray(call.key) ? call.key : [call.key];
2969
+ const many = isArray(call.key);
2970
+ const keys = isArray(call.key) ? call.key : [call.key];
2968
2971
  const rows = await table.get(keys);
2969
2972
  return many ? { rows } : { row: rows[0] };
2970
2973
  }
@@ -2985,30 +2988,30 @@ function createDatabaseTool(options = {}) {
2985
2988
  case "aggregate": return { value: await (await resolver.resolve(call.id)).table(call.table).aggregate(call.function, call.column, normalizeQuery(call.query), read) };
2986
2989
  case "add": {
2987
2990
  const table = (await resolver.resolve(call.id)).table(call.table);
2988
- const many = Array.isArray(call.row);
2989
- const rows = Array.isArray(call.row) ? call.row : [call.row];
2991
+ const many = isArray(call.row);
2992
+ const rows = isArray(call.row) ? call.row : [call.row];
2990
2993
  const keys = await table.add(rows, read);
2991
2994
  return many ? { keys } : { key: keys[0] };
2992
2995
  }
2993
2996
  case "set": {
2994
2997
  const table = (await resolver.resolve(call.id)).table(call.table);
2995
- const many = Array.isArray(call.row);
2996
- const rows = Array.isArray(call.row) ? call.row : [call.row];
2998
+ const many = isArray(call.row);
2999
+ const rows = isArray(call.row) ? call.row : [call.row];
2997
3000
  const keys = await table.set(rows, read);
2998
3001
  return many ? { keys } : { key: keys[0] };
2999
3002
  }
3000
3003
  case "update": {
3001
3004
  const table = (await resolver.resolve(call.id)).table(call.table);
3002
3005
  const changes = call.changes;
3003
- const many = Array.isArray(call.key);
3004
- const keys = Array.isArray(call.key) ? call.key : [call.key];
3006
+ const many = isArray(call.key);
3007
+ const keys = isArray(call.key) ? call.key : [call.key];
3005
3008
  const updated = await table.update(keys, changes, read);
3006
3009
  return many ? { updated } : { updated: updated[0] };
3007
3010
  }
3008
3011
  case "remove": {
3009
3012
  const table = (await resolver.resolve(call.id)).table(call.table);
3010
- const many = Array.isArray(call.key);
3011
- const keys = Array.isArray(call.key) ? call.key : [call.key];
3013
+ const many = isArray(call.key);
3014
+ const keys = isArray(call.key) ? call.key : [call.key];
3012
3015
  const removed = await table.remove(keys, read);
3013
3016
  return many ? { removed } : { removed: removed[0] };
3014
3017
  }
@@ -3030,7 +3033,7 @@ function createDatabaseTool(options = {}) {
3030
3033
  if (isToolboxError(error)) throw error;
3031
3034
  const code = inferDatabaseCode(error);
3032
3035
  if (code === void 0) throw error;
3033
- throw new ToolboxError("DATABASE", error instanceof Error ? error.message : String(error), {
3036
+ throw new ToolboxError("DATABASE", isError(error) ? error.message : String(error), {
3034
3037
  code,
3035
3038
  operation: call.operation,
3036
3039
  id: call.id,
@@ -3085,7 +3088,10 @@ function createDatabaseTool(options = {}) {
3085
3088
  * import { createRelationTool } from '@src/core'
3086
3089
  *
3087
3090
  * const tool = createRelationTool({ managers: { shop: manager } })
3088
- * await tool.execute({ operation: 'load', model: 'accounts', key: 'acc1', include: ['contacts'] })
3091
+ * await tool.execute(
3092
+ * { operation: 'load', model: 'accounts', key: 'acc1', include: ['contacts'] },
3093
+ * { signal: new AbortController().signal },
3094
+ * )
3089
3095
  * ```
3090
3096
  */
3091
3097
  function createRelationTool(options) {
@@ -3106,7 +3112,7 @@ function createRelationTool(options) {
3106
3112
  switch (call.operation) {
3107
3113
  case "load": {
3108
3114
  const include = expandInclude(call.include, depth);
3109
- if (typeof call.key === "string" || typeof call.key === "number") return { row: await model.load(call.key, include) };
3115
+ if (isString(call.key) || isNumber(call.key)) return { row: await model.load(call.key, include) };
3110
3116
  return { rows: await model.load(call.key, include) };
3111
3117
  }
3112
3118
  case "find": {
@@ -3149,7 +3155,7 @@ function createRelationTool(options) {
3149
3155
  } catch (error) {
3150
3156
  if (isToolboxError(error)) throw error;
3151
3157
  const relation = inferRelationCode(error);
3152
- if (relation !== void 0) throw new ToolboxError("RELATION", error instanceof Error ? error.message : String(error), {
3158
+ if (relation !== void 0) throw new ToolboxError("RELATION", isError(error) ? error.message : String(error), {
3153
3159
  code: relation,
3154
3160
  operation: call.operation,
3155
3161
  model: call.model,
@@ -3157,7 +3163,7 @@ function createRelationTool(options) {
3157
3163
  });
3158
3164
  const database = inferDatabaseCode(error);
3159
3165
  if (database === void 0) throw error;
3160
- throw new ToolboxError("DATABASE", error instanceof Error ? error.message : String(error), {
3166
+ throw new ToolboxError("DATABASE", isError(error) ? error.message : String(error), {
3161
3167
  code: database,
3162
3168
  operation: call.operation
3163
3169
  });
@@ -3304,8 +3310,10 @@ function createInferTool(options) {
3304
3310
  * never asserted, and a key outside the closed inferred schema is silently dropped rather than
3305
3311
  * rejected (see {@link import('./types.js').EndpointToolOptions.validate}). With
3306
3312
  * `validate: false`, the tool's `execute` passes through the model-supplied `args` to
3307
- * `definition.execute` without re-validation — the raw-passthrough opt-out. Either way, the
3308
- * definition's return flows back as the tool call's plain result; a throw propagates uncaught,
3313
+ * `definition.execute` without re-validation — the raw-passthrough opt-out. The execution context
3314
+ * the tool's `execute` receives reaches `definition.execute` unchanged whichever way `args` are
3315
+ * handled, so a handler that reads `context.signal` observes the caller's cancellation. Either way,
3316
+ * the definition's return flows back as the tool call's plain result; a throw propagates uncaught,
3309
3317
  * isolated by the `ToolManagerInterface` (`@orkestrel/tool`) into the canonical error envelope
3310
3318
  * — never caught or re-wrapped here.
3311
3319
  *
@@ -3358,8 +3366,8 @@ function createEndpointTool(definition, options) {
3358
3366
  name: definition.name,
3359
3367
  description: definition.description,
3360
3368
  ...parameters === void 0 ? {} : { parameters },
3361
- execute(args) {
3362
- return definition.execute(args);
3369
+ execute(args, context) {
3370
+ return definition.execute(args, context);
3363
3371
  }
3364
3372
  });
3365
3373
  const contract = createContract(schemaToShape(objectSchema));
@@ -3367,13 +3375,13 @@ function createEndpointTool(definition, options) {
3367
3375
  name: definition.name,
3368
3376
  description: definition.description,
3369
3377
  ...parameters === void 0 ? {} : { parameters },
3370
- execute(args) {
3378
+ execute(args, context) {
3371
3379
  const parsed = contract.parse(args);
3372
3380
  if (parsed === void 0 || !isRecord(parsed)) throw new ToolboxError("TOOL", "malformed endpoint call arguments", {
3373
3381
  name: definition.name,
3374
3382
  faults: contract.explain(args)
3375
3383
  });
3376
- return definition.execute(parsed);
3384
+ return definition.execute(parsed, context);
3377
3385
  }
3378
3386
  });
3379
3387
  }