feat: enhance function calling with richer schemas, validation, parallel execution, and Ollama native support (#51)

* feat: add richer JSON Schema types and AJV validation utility

- Extended ToolParameter with enum, items, properties, additionalProperties (max depth 3)
- Added ToolError class with ToolErrorCategory enum
- Added ToolValidationError interface
- Created AJV-based validateToolArgs() with lazy loading and schema depth enforcement
- 57 new tests covering types and validation

* feat: add validation gate, parallel execution, and structured error feedback

- Arsenal.execute() validates args against JSON Schema before handler execution
- Tool execution errors categorized (scope_denied, tool_not_found, timeout, execution_error, validation_error)
- ToolError import fix in agent (was missing, causing silent ReferenceError)
- Semaphore-based parallel tool execution with configurable concurrency (default 5)
- Results maintain original call order regardless of completion order
- Structured error feedback to LLM with category only (no raw error messages)
- Scope/approval gates checked per-call before execution
- 35 new tests covering validation, parallel execution, and error feedback

* feat: add Ollama native function calling with probe + cache + fallback

- Added nativeTools config option to LLMConfig
- LocalAdapter probes model support for native tools on first call
- Probe result cached per model name to avoid re-probing
- Native tools formatted as Ollama /api/chat tools parameter
- Tool calls parsed from Ollama's tool_calls response format
- Falls back to text-based renderToolContract when native not supported
- OpenAI-compatible wire format also supported (for vLLM, LM Studio)
- 10 new tests covering probe, cache, fallback, and config overrides

* fix(tools): harden validation and native fallback

---------

Co-authored-by: sronaal <sronaal@users.noreply.github.com>
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-07-20 11:45:52 -04:00
1 parent 151d3555c6
commit df08627cb9
14 files changed
+3019 -63

No files matched your search

+54
View File
@@ -16,6 +16,12 @@ export interface LLMConfig {
maxTokens?: number;
temperature?: number;
timeout?: number;
/**
* When true, force native Ollama / OpenAI-compatible function calling. When
* false, always use text-based tool contract injection. When unset, probe
* on the first call and cache per-model.
*/
nativeTools?: boolean;
/**
* Ordered model/provider ladder to fall back to when the PRIMARY model fails for
* ANY reason it can't self-recover from — hard errors after same-model retries
@@ -79,6 +85,13 @@ export interface LLMToolDefinition {
enum?: string[];
items?: { type: string };
default?: unknown;
properties?: Record<string, {
type: string;
description?: string;
enum?: string[];
}>;
required?: string[];
additionalProperties?: boolean;
}>;
required?: string[];
};
@@ -391,6 +404,27 @@ export interface Message {
* additionally fire a loud warning. Absent/safe/active tools are ungated. */
export type RiskTier = 'local_read' | 'passive' | 'active' | 'intrusive' | 'credential' | 'dangerous';
/** Structured error categories for tool failures — enables LLM feedback without leaking internals. */
export enum ToolErrorCategory {
ScopeDenied = 'scope_denied',
ToolNotFound = 'tool_not_found',
Timeout = 'timeout',
ExecutionError = 'execution_error',
ValidationError = 'validation_error',
}
/** Structured error thrown by tool execution — carries category for LLM feedback and optional tool name. */
export class ToolError extends Error {
constructor(
public readonly category: ToolErrorCategory,
message: string,
public readonly toolName?: string,
) {
super(message);
this.name = 'ToolError';
}
}
export interface CustomTool {
name: string;
description: string;
@@ -408,6 +442,26 @@ export interface ToolParameter {
description: string;
required: boolean;
default?: unknown;
/** Allowed values — constrains the parameter to a fixed set. */
enum?: (string | number)[];
/** Schema for array items (when type === 'array'). */
items?: {
type: string;
description?: string;
properties?: Record<string, ToolParameter>;
};
/** Nested object properties (when type === 'object'). */
properties?: Record<string, ToolParameter>;
/** Whether additional properties are allowed on object parameters. */
additionalProperties?: boolean;
}
/** A single validation error produced by validateToolArgs. */
export interface ToolValidationError {
field: string;
message: string;
received: unknown;
expected: string;
}
export interface ToolContext {