npm package: @gizmo-ai/agent-plugin
Description: Agent middleware for Gizmo runtime - orchestrates model calls and tool execution
Latest version: 0.2.1

# @gizmo-ai/agent-plugin

Agent runtime plugin for Gizmo – orchestrates model calls and tool execution.

## Overview

This package provides a runtime plugin for building AI agents with Gizmo. It composes four middleware layers:

1. **Agent orchestration** - Manages the agentic loop (model → tools → model)
2. **Model driver** - Calls the LLM provider
3. **Tools executor** - Executes tools with concurrency control
4. **Output truncation** - Truncates large tool outputs to prevent context overflow

All agent actions are prefixed with `AGENT_…` (for example `AGENT_MODEL_REQUESTED`, `AGENT_TOOL_CALL_EXECUTED`). When creating your own plugin, follow the same pattern with a unique prefix so runtime diagnostics clearly show where each action originates.

> First time with the runtime itself? Walk through [`packages/runtime/README.md`](../runtime/README.md) to understand the execution lifecycle and middleware helpers this plugin relies on.

**Key difference from v1**: `agentPlugin()` returns a runtime plugin, not a runtime. You can register it alongside your own plugins and middleware.

## Installation

```bash
bun add @gizmo-ai/agent-plugin @gizmo-ai/runtime
```

## Quick Start

```typescript
import { createRuntime } from '@gizmo-ai/runtime';
import { agentPlugin } from '@gizmo-ai/agent-plugin';
import { openAI } from '@gizmo-ai/model-providers';

const agent = agentPlugin({
  model: openAI('gpt-4'),
  tools: [calculatorTool, searchTool],
  systemMessage: "You are a helpful assistant",
  maxLoops: 10,
});

const runtime = createRuntime({
  plugins: [agent],
});

const handle = runtime.execute("What is 2 + 2?");
await handle.completed;

// State is fully typed - no casts needed!
console.log(runtime.getState().agent.conversation);
```

## API

### `agentPlugin(config: AgentConfig)`

Creates an `AgentPlugin`. Register it with the runtime (via `plugins`) to enable agent behaviour.

**Config**:
- `model: ModelProvider` - LLM provider (required)
- `tools?: Tool[]` - Available tools (optional)
- `systemMessage?: string` - System message (optional)
- `maxLoops?: number` - Maximum agentic loops before stopping (optional)
- `initialConversation?: ConversationItem[]` - Initial conversation (optional)
- `concurrency?: number` - Tool execution concurrency limit (optional)
- `maxToolOutputChars?: number` - Maximum characters for tool output before truncation (default: 50,000)

**Returns**: `AgentPlugin`

`AgentPlugin` exposes:

- `key: "agent"` – reducer key used by the runtime
- `reducer` – manages the agent state slice
- `initialState` – bootstrapped from your config
- `middleware` – the composed orchestration → model → tools → truncation pipeline
- `stages` – individual middleware stages (`orchestration`, `model`, `tools`, `truncation`) for advanced composition

> **Need raw pieces?**
> `agentReducer` and `initialAgentState` are still exported for advanced scenarios, but most users should rely on the plugin.

## Runtime Tool Registration

Plugins that need to register or remove tools at runtime can dispatch the provided helpers:

```ts
import { registerAgentTool, deregisterAgentTool } from "@gizmo-ai/agent-plugin";

runtime.dispatch(
  registerAgentTool({
    tool: {
      name: "feature_flag_tool",
      description: "Only available for beta users",
      parameters: {},
      execute: featureFlaggedExecute,
    },
  })
);

runtime.dispatch(deregisterAgentTool({ name: "feature_flag_tool" }));
```

The helpers simply emit `AGENT_REGISTER_TOOL` / `AGENT_DEREGISTER_TOOL` actions, so the standard tooling middleware sees the updated registry immediately.

## How It Works

### 1. Execution Flow

```
User: runtime.execute("Hello")
  ↓
RUNTIME_EXECUTION_STARTED (runtime)
  ↓
Agent middleware:
  - Tracks execution turns and dispatches AGENT_MODEL_REQUESTED
  ↓
Model middleware:
  - Calls model provider
  - Dispatches AGENT_MODEL_RESPONDED
  ↓
Agent middleware:
  - Checks for tool calls
  - Dispatches AGENT_TOOL_CALL_REQUESTED for each tool
  ↓
Tools middleware:
  - Executes tools
  - Dispatches AGENT_TOOL_CALL_EXECUTED
  ↓
Agent middleware:
  - When all tools done, dispatches AGENT_MODEL_REQUESTED again
  - Loop continues until model responds without tool calls
  ↓
RUNTIME_EXECUTION_COMPLETED (runtime)
```

### 2. Actions

The agent middleware dispatches and listens for these actions:

**Dispatched**:
- `AGENT_MODEL_REQUESTED` - Kick off model generation
- `AGENT_TOOL_CALL_REQUESTED` - Request tool execution
- `AGENT_LOOP_INCREMENTED` - Track agentic loop count

**Listened**:
- `RUNTIME_EXECUTION_STARTED` / `RUNTIME_EXECUTION_INPUT` - From runtime
- `AGENT_MODEL_RESPONDED` - From model middleware
- `AGENT_TOOL_CALL_EXECUTED` / `AGENT_TOOL_CALL_FAILED` / `AGENT_TOOL_MISSING` - From tools middleware
- `AGENT_OUTPUT_TRUNCATED` - From truncation middleware (when tool output exceeds `maxToolOutputChars`)

See `AgentActionDescriptions` export for LLM-friendly descriptions.

### 3. MaxLoops

The `maxLoops` config prevents infinite loops:

```typescript
const agent = agentPlugin({
  model: openAI('gpt-4'),
  maxLoops: 5  // Stop after 5 agentic loops
});
```

When the limit is reached, the agent stops requesting the model, and execution completes naturally.

### 4. Async work metadata

Every asynchronous task the agent schedules (model calls, tool execution, loop bookkeeping) uses `dispatchWork` from `@gizmo-ai/runtime`. This helper wraps the thunk and guarantees that `RUNTIME_WORK_*` actions carry the triggering action’s ID/type so you can follow the work tree in logs. When you build additional middleware or extend the agent, prefer `dispatchWork(api, { action, kind, run })` over dispatching raw thunks so the runtime keeps the graph observable.

## Advanced Usage

### Custom State Composition

Combine the agent plugin with your own plugins:

```typescript
import { createRuntime, type RuntimePlugin } from '@gizmo-ai/runtime';
import { agentPlugin } from '@gizmo-ai/agent-plugin';

const agent = agentPlugin({ model, tools });

const analyticsPlugin: RuntimePlugin = {
  key: "analytics",
  reducer: analyticsReducer,
  initialState: { events: [] },
  middleware: analyticsMiddleware,
};

const runtime = createRuntime({
  plugins: [agent, analyticsPlugin],
});
```

### Manual Middleware Composition

For advanced users who want more control:

```typescript
import {
  createAgentMiddleware,
  createModelMiddleware,
  createToolsMiddleware,
  createTruncationMiddleware
} from '@gizmo-ai/agent-plugin';

const orchestration = createAgentMiddleware({ maxLoops: 10 });
const modelDriver = createModelMiddleware({ provider: openAI('gpt-4') });
const toolsExecutor = createToolsMiddleware({ concurrency: 3 });
const truncation = createTruncationMiddleware({ maxOutputChars: 50_000 });

const agent = {
  key: "agent",
  reducer: agentReducer,
  initialState: buildInitialAgentState({ tools }),
  middleware: (api) => (next) => {
    // Order: orchestration -> model -> tools -> truncation -> next
    const withTruncation = truncation(api)(next);
    const withTools = toolsExecutor(api)(withTruncation);
    const withModel = modelDriver(api)(withTools);
    return orchestration(api)(withModel);
  },
};

const runtime = createRuntime({
  plugins: [agent],
});
```

## Types

### `ConversationItem`

```typescript
type ConversationItem =
  | { type: "message"; role: "user" | "assistant" | "system"; content: string }
  | { type: "function_call"; id: string; name: string; args: Record<string, unknown> }
  | { type: "function_call_output"; call_id: string; output: string }
  | { type: "reasoning"; summary: string };
```

### `Tool`

```typescript
interface Tool {
  name: string;
  description: string;
  parameters: unknown;  // JSON Schema
  execute: (args: Record<string, unknown>) => Promise<string>;
}
```

### `ModelProvider`

```typescript
interface ModelProvider {
  generate(
    conversation: ConversationItem[],
    tools: Tool[]
  ): Promise<ModelResponse>;

  generateStream?(
    conversation: ConversationItem[],
    tools: Tool[],
    onToken: (chunk: { type: "content" | "function_call"; contentChunk: string }) => void,
    onReasoning: (chunk: { type: "reasoning"; reasoningChunk: string }) => void,
    signal?: AbortSignal
  ): Promise<ModelResponse>;
}
```

### `ModelResponse`

```typescript
interface ModelResponse {
  newConversationItems: ConversationItem[];
  toolCalls?: ToolCall[];
  usage?: {
    inputTokens: number;
    outputTokens: number;
    reasoningTokens?: number;
  };
  reasoning?: string;
}
```

## Example: Building a Calculator Tool

```typescript
import type { Tool } from '@gizmo-ai/agent-plugin';

const calculatorTool: Tool = {
  name: "calculate",
  description: "Performs basic arithmetic operations",
  parameters: {
    type: "object",
    properties: {
      operation: { type: "string", enum: ["add", "subtract", "multiply", "divide"] },
      a: { type: "number" },
      b: { type: "number" }
    },
    required: ["operation", "a", "b"]
  },
  execute: async (args) => {
    const { operation, a, b } = args as { operation: string; a: number; b: number };

    switch (operation) {
      case "add": return String(a + b);
      case "subtract": return String(a - b);
      case "multiply": return String(a * b);
      case "divide": return String(a / b);
      default: throw new Error(`Unknown operation: ${operation}`);
    }
  }
};
```

## Building Plugins

When building your own runtime plugins, follow this structure:

### Plugin Structure

```typescript
import type { RuntimePlugin, Middleware, Action } from '@gizmo-ai/runtime';

// 1. Define your action types with a unique prefix
type MyAction =
  | { type: 'MY_PLUGIN_STARTED'; data: string }
  | { type: 'MY_PLUGIN_COMPLETED'; result: number };

// 2. Define your state shape
interface MyState {
  items: string[];
  count: number;
}

// 3. Create a combined action type if you handle execution actions
import type { ExecutionAction } from '@gizmo-ai/runtime';
type AllActions = ExecutionAction | MyAction;

// 4. Define your plugin with typed key for state inference
export interface MyPlugin extends RuntimePlugin<'myPlugin', MyState, AllActions> {
  key: 'myPlugin';
  reducer: (state: MyState | undefined, action: AllActions) => MyState;
  initialState: MyState;
  middleware: Middleware<Record<string, unknown>, AllActions>;
}

// 5. Export a factory function
export function createMyPlugin(config: MyConfig): MyPlugin {
  return {
    key: 'myPlugin',
    reducer: myReducer,
    initialState: { items: [], count: 0 },
    middleware: createMyMiddleware(config),
  };
}
```

### Type Inference

The runtime infers state types from plugins automatically:

```typescript
const myPlugin = createMyPlugin({ ... });
const agent = agentPlugin({ model });

const runtime = createRuntime({
  plugins: [myPlugin, agent],
});

// State is fully typed!
const state = runtime.getState();
state.myPlugin.count;        // number
state.agent.conversation;    // ConversationItem[]
```

### Action Naming Convention

- Use a unique prefix for all your actions (e.g., `MY_PLUGIN_...`)
- This makes it easy to trace action flow in logs and DevTools
- The agent uses `AGENT_...`, runtime uses `RUNTIME_...`

## Architecture

This package demonstrates Gizmo's middleware pattern:

- **Single responsibility**: Each middleware does ONE thing (orchestrate, call model, execute tools)
- **Functional style**: Pure functions, no side effects outside thunks
- **Observable**: All actions flow through the store, fully debuggable
- **Composable**: Mix with your own middleware

For detailed middleware patterns, see [packages/runtime/README.md](../runtime/README.md).

## License

MIT