← All @molecule/* packages · App templates

@molecule/api-ai-agents

Core interface · ai-agents · API (Node) · v1.0.1 · Apache-2.0

ai-agents core interface for molecule.dev.

npm install @molecule/api-ai-agents

npm · Source on GitHub

How it works

@molecule/api-ai-agents is the ai-agents core interface on the API (Node) side: the API your app calls, with no vendor inside.

Choose the implementation by bonding one of its 1 provider: @molecule/api-ai-agents-llm.

import { bond } from '@molecule/api-bond'
import { requireProvider } from '@molecule/api-ai-agents'
import { provider as agents } from '@molecule/api-ai-agents-llm'
import type { AITool } from '@molecule/api-ai'

// Wire at startup (an `ai` provider must already be bonded).
bond('ai-agents', agents)

const myTool: AITool = {
  name: 'add',
  description: 'Add two numbers',
  parameters: {
    type: 'object',
    properties: { a: { type: 'number' }, b: { type: 'number' } },
    required: ['a', 'b'],
  },
  execute: async (input) => {
    const { a, b } = input as { a: number; b: number }
    return a + b
  },
}

const result = await requireProvider().run({
  task: 'What is 1 + 2? Use the add tool.',
  tools: [myTool],
})
console.log(result.output) // final assistant answer
console.log(result.steps) // intermediate tool calls + results
console.log(result.usage) // token usage summed across all model calls

Providers (1): @molecule/api-ai-agents-llm

Works with: @molecule/api-ai, @molecule/api-bond, @molecule/api-i18n

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

AI agents core for molecule.dev — the swappable tool-calling agent contract.

Interface-only core: it defines the AIAgentsProvider interface (a batteries-included tool-calling agent) plus the bond accessor (setProvider/getProvider/requireProvider/…). The default implementation — a model↔tool loop over the swappable ai chat bond (@molecule/api-ai) — ships in the @molecule/api-ai-agents-llm bond package. Bond a provider once at startup, then drive it from anywhere.

Quick Start

import { bond } from '@molecule/api-bond'
import { requireProvider } from '@molecule/api-ai-agents'
import { provider as agents } from '@molecule/api-ai-agents-llm'
import type { AITool } from '@molecule/api-ai'

// Wire at startup (an `ai` provider must already be bonded).
bond('ai-agents', agents)

const myTool: AITool = {
  name: 'add',
  description: 'Add two numbers',
  parameters: {
    type: 'object',
    properties: { a: { type: 'number' }, b: { type: 'number' } },
    required: ['a', 'b'],
  },
  execute: async (input) => {
    const { a, b } = input as { a: number; b: number }
    return a + b
  },
}

const result = await requireProvider().run({
  task: 'What is 1 + 2? Use the add tool.',
  tools: [myTool],
})
console.log(result.output) // final assistant answer
console.log(result.steps) // intermediate tool calls + results
console.log(result.usage) // token usage summed across all model calls

Type

core

Installation

npm install @molecule/api-ai-agents @molecule/api-ai @molecule/api-bond @molecule/api-i18n

API

Interfaces

AgentRunInput

Input for a single agent run.

Exactly one of task or messages is required — task is a convenience for a single user turn; messages supplies a full conversation history.

interface AgentRunInput {
  /** Convenience: a single user task string (seeds one `user` message). */
  task?: string
  /** A full message history to seed the conversation (mutually exclusive with `task`). */
  messages?: ChatMessage[]
  /** Optional system prompt passed to the model on every turn. */
  system?: string
  /** Tools the agent may call; each call runs the matching tool's `execute()`. */
  tools?: AITool[]
  /** Maximum model↔tool round-trips before the loop stops (default 10). */
  maxSteps?: number
  /** Model identifier passed through to the AI provider. */
  model?: string
  /**
   * Max output tokens per model turn, passed through to the AI provider.
   * Without it the provider's default (typically 4096) applies — too small for
   * turns whose tool input is a whole file; a truncated tool-input JSON parses
   * as an empty `{}` input, which looks like a model bug to the caller.
   */
  maxTokens?: number
  /** Named AI provider to use; falls back to the singleton when omitted. */
  provider?: string
  /** Sampling temperature passed through to the AI provider. */
  temperature?: number
  /** Abort signal to cancel in-flight model requests and stop tool execution. */
  signal?: AbortSignal
  /** Optional live hook invoked for every streamed `ChatEvent`. */
  onEvent?: (event: ChatEvent) => void
}

AgentRunResult

Result of a completed agent run.

interface AgentRunResult {
  /** Final assistant text (or a note if the step budget was exhausted). */
  output: string
  /** Every intermediate step that invoked tools, in order. */
  steps: AgentStep[]
  /** Token usage summed across all model calls in the run. */
  usage: TokenUsage
}

AgentStep

One round-trip of the agent loop: the assistant text (if any) plus every tool call executed before the next model turn.

interface AgentStep {
  /** Assistant text emitted on this step, if any. */
  text?: string
  /** Tool calls executed on this step. */
  toolCalls: AgentToolCall[]
}

AgentToolCall

A single tool invocation performed during a run.

interface AgentToolCall {
  /** Provider-assigned tool-use id, echoed back in the tool result. */
  id: string
  /** Name of the tool the model called. */
  name: string
  /** Raw input the model produced for the tool. */
  input: unknown
  /** Value returned by `execute()`, or an error string when the call failed. */
  result: unknown
  /** `true` when the tool threw or the name was unknown. */
  isError?: boolean
}

AIAgentsConfig

Config options for an AI agents bond.

interface AIAgentsConfig {
  [key: string]: unknown
}

AIAgentsProvider

AI agents provider interface.

Implemented by the default provider in this package (and swappable via bond('ai-agents', provider)). Runs an agentic tool-calling loop over the bonded ai chat provider.

interface AIAgentsProvider {
  /** Provider identifier (the default implementation reports `'default'`). */
  readonly name: string
  /**
   * Runs the agentic loop to completion and returns the final result.
   *
   * @param input - The task/messages, tools, and loop options.
   * @returns The final output, the recorded steps, and total token usage.
   * @throws {Error} When the underlying `ai` bond fails mid-run (a provider
   *   API error, an abort, or any other exception raised while draining a
   *   model turn or executing a tool) — a tool's own `execute()` throwing is
   *   NON-fatal and recorded on the step instead. The default
   *   `@molecule/api-ai-agents-llm` implementation rejects with its typed
   *   `AgentRunError` in this case, which additionally carries the `usage`
   *   and `steps` accumulated across every turn completed before the
   *   failure (`error instanceof AgentRunError` to access them) — a caller
   *   that meters spend should check for it so a run that dies mid-loop
   *   doesn't silently under-meter the turns that already completed.
   */
  run(input: AgentRunInput): Promise<AgentRunResult>
}

Functions

getAllProviders()

Retrieves all named AI agents providers as a Map keyed by provider name.

function getAllProviders(): Map<string, AIAgentsProvider>

Returns: Map of provider name → AIAgentsProvider.

getProvider()

Retrieves the singleton AI agents provider, or null if none is bonded.

Falls back to a single named provider when no singleton is bonded. When multiple named providers are bonded the fallback declines (returns null) because the choice is ambiguous.

This also applies to an auto-promoted singleton: setProvider('a', p1) followed by setProvider('b', p2) does NOT leave getProvider() stuck returning p1 forever — once 'b' is registered the pick is genuinely ambiguous and getProvider() declines (null), same as if neither had been auto-promoted. An explicit setProvider(provider) singleton is unaffected by how many named providers exist.

function getProvider(): AIAgentsProvider | null

Returns: The bonded AI agents provider, or null.

getProviderByName(name)

Retrieves a named AI agents provider, or null if not bonded.

function getProviderByName(name: string): AIAgentsProvider | null
  • name — The provider name.

Returns: The named AI agents provider, or null.

hasProvider(name)

Checks whether an AI agents provider is currently bonded.

function hasProvider(name?: string): boolean
  • name — Optional provider name. If omitted, checks the singleton.

Returns: true if the provider is bonded.

requireProvider()

Retrieves the bonded AI agents provider, throwing if none is bonded.

Routes through the same resolution as getProvider() so the single-named- bond fallback applies — apps that wire bond('ai-agents', 'llm', provider) directly still satisfy this call without having to switch to the explicit getProviderByName() pattern. When resolution declined because MULTIPLE named providers are bonded with no explicit singleton, the thrown message says so (distinct from "nothing bonded at all") and points at getProviderByName().

function requireProvider(): AIAgentsProvider

Returns: The bonded AI agents provider.

setProvider(provider)

Registers an AI agents provider in singleton mode.

function setProvider(provider: AIAgentsProvider): void
  • provider — The default provider implementation for this process.

Available Providers

ProviderPackage
Ai Agents@molecule/api-ai-agents-llm

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-ai ^1.0.1
  • @molecule/api-bond ^1.0.1
  • @molecule/api-i18n ^1.0.1

Runtime Dependencies

  • @molecule/api-ai
  • @molecule/api-bond
  • @molecule/api-i18n

Requires a bonded ai provider (bond('ai', provider) or a named provider selected via run({ provider: 'anthropic' })) whose model supports tool use — the agent has no model of its own; it orchestrates the ai bond.

This package is the abstract contract only. To get a working agent, also bond the @molecule/api-ai-agents-llm provider (or your own AIAgentsProvider) via bond('ai-agents', provider) — nothing else changes.

  • setProvider(name, provider) ambiguity: the FIRST named provider you register also auto-promotes to the singleton (so plain getProvider()/requireProvider() work without the caller knowing the name) — but that promotion is a single-provider convenience, not a permanent pick. The moment a SECOND, differently-named provider is registered, getProvider() stops returning the first one and declines (null) instead — requireProvider() throws pointing at getProviderByName(name). Call the explicit setProvider(provider) (no name) form if you want one provider to always win regardless of how many named providers you also register.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), give the agent a task that genuinely needs several tool calls, adapt each item to this app's actual agent surface + its registered tools, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • A multi-step task drives a real agentic LOOP, not one shot: run() records more than one entry in result.steps, and each toolCalls[] names a tool the agent was actually given. Confirm each tool's execute() truly RAN via its own side effect / log / the resource it touched — not that the model narrated calling it — and that result.output is a final answer that USES those tool results to complete the task.
  • The loop TERMINATES cleanly: it either reaches a done state (the model stops calling tools, so run() returns the final output) or hits the maxSteps cap (default 10) and stops with the step-budget note as output. It never spins forever or ping-pongs the same tool endlessly — a step cap exists and is honored: result.steps.length <= maxSteps, and a task built to loop past the budget stops AT the cap rather than running away.
  • A tool that fails mid-loop is handled, not fatal: when a tool's execute() throws (or the model names a tool that was never registered), that call is caught and fed back to the model as an error tool result — the matching toolCalls[] has isError: true, the loop continues, and the run still returns a result. The request never crashes. (Only an ai-provider failure rejects — as AgentRunError carrying the partial usage/steps — and it surfaces as a handled error, not an unhandled throw.)
  • If the run streams (the default), the UI shows progress INCREMENTALLY: the onEvent hook fires per ChatEvent (thinking text, tool_use, done) as they arrive, so the user watches the agent think + call tools live — not a long freeze then one blob at the end.
  • SECURITY — the agent can call ONLY the tools handed to this run: a tool name the model invents (or one injected via the task or a tool's own output) that was never in input.tools is refused as an unknown tool error, not executed. Tool execution stays server-side under its backend's guards (e.g. the @molecule/api-ai-tools pathGuards/redactSecrets/blockCommand), and the ai provider key lives on the API, never the client. Feed a prompt-injected instruction (in the task, or in data a tool returns) telling the agent to escape the workspace, run a privileged command, or exfiltrate a secret, and confirm it CANNOT do anything the user couldn't do directly.