@molecule/api-ai-agents-llm
Provider bond · ai-agents · API (Node) · v1.0.2 · Apache-2.0
LLM-backed agent provider for molecule.dev — composes the swappable ai chat bond into a tool-calling loop
npm install @molecule/api-ai-agents-llmnpm · Source on GitHub · Implements @molecule/api-ai-agents
How it works
@molecule/api-ai-agents-llm is a provider bond on the API (Node) side: it implements the ai-agents core interface (@molecule/api-ai-agents) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
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 callsWorks with: @molecule/api-ai, @molecule/api-ai-agents, @molecule/api-i18n
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
LLM-backed AI agents provider for molecule.dev — the batteries-included
tool-calling loop that implements the @molecule/api-ai-agents core contract.
Ships a default provider (name: 'llm') that drives a model↔tool loop over
the swappable ai chat bond (@molecule/api-ai): it calls the model,
executes any tools the model requests via each AITool.execute(), feeds the
results back, and repeats until the model stops calling tools (or the
maxSteps budget is exhausted). Bond it once at startup, then drive it from
anywhere via the @molecule/api-ai-agents accessor.
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.tsJSDoc, not this file.
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
provider
Installation
npm install @molecule/api-ai-agents-llm @molecule/api-ai @molecule/api-ai-agents @molecule/api-i18n
API
Classes
AgentRunError
Thrown by run() when the agentic loop fails partway through — a provider
API error (rate limit, auth, overload, network), an abort, or any other
exception raised while draining a model turn or executing a tool.
class AgentRunError extends Error {
/** Token usage summed across every turn that completed before the failure. */
readonly usage: TokenUsage
/** Every intermediate step (with its tool calls) recorded before the failure. */
readonly steps: AgentStep[]
/**
* Builds the error, carrying the partial run alongside the failure so a
* caller can report what the agent had already spent and done.
*
* @param message - The error message (same text a plain `Error` would carry).
* @param usage - Token usage accumulated across turns completed before the failure.
* @param steps - Steps recorded before the failure.
* @param options - Standard `Error` options — `cause` preserves the original thrown value.
*/
constructor(
message: string,
usage: TokenUsage,
steps: AgentStep[],
options?: { cause?: unknown },
) {
super(message, options)
this.name = 'AgentRunError'
this.usage = usage
this.steps = steps
}
}
Constants
provider
The LLM-backed AI agents provider: a tool-calling loop over the bonded ai
chat provider.
Bond it with bond('ai-agents', provider) and drive it with
requireProvider().run({ task, tools }). Requires a bonded ai provider
whose model supports tool use.
const provider: AIAgentsProvider
Core Interface
Implements @molecule/api-ai-agents interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-ai-agents'
import { provider } from '@molecule/api-ai-agents-llm'
export function setupAiAgentsLlm(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai^1.0.1@molecule/api-ai-agents^1.0.1@molecule/api-i18n^1.0.1
Runtime Dependencies
@molecule/api-ai@molecule/api-ai-agents@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 — this agent has no model of its own; it orchestrates the ai bond.
The loop executes your tools: each model tool_use runs the matching
AITool.execute(input) and the return value is fed back as a tool_result.
A thrown tool error or an unknown tool name becomes an error tool_result
(recorded with isError: true) instead of aborting the run, so the model can
recover. run() requires exactly one of task (a single user turn) or
messages (a full history); it throws if neither — or both — is supplied.
Every model turn streams by default (input.stream — set false to force
non-streaming) and, when input.cacheControl is set, is passed through to
ai.chat() on every turn so a provider that supports prompt caching (e.g.
@molecule/api-ai-anthropic) doesn't re-bill the identical system + tools
prefix on each of up to maxSteps turns. Note onEvent is only an
INCREMENTAL live hook while streaming — with stream: false it fires once
per turn with the whole response.
Failure semantics (how to tell failure modes apart):
- Tool failure (throwing
execute(), unknown tool name) — NON-fatal: recorded on the step withisError: trueand fed back to the model. - AI provider API failure (rate limit, bad key, overload, network — the
aibond emits an in-banderrorChatEvent), an abort, or any other mid-run exception — FATAL:run()rejects with anAgentRunError(message text unchanged from a plainError) carrying theusageandstepsaccumulated across every turn that completed before the failure — a caller that meters spend can book that partial usage instead of losing it.run()never resolves with a silently emptyoutput, so an emptyresult.outputmeans the model genuinely produced no text, not that the API failed. - Step budget exhausted — resolves normally;
outputis the last assistant text or an explicit "step budget exhausted" note.
Swappable like any bond: replace this LLM agent with your own
AIAgentsProvider via bond('ai-agents', myProvider) — nothing else changes.
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 inresult.steps, and eachtoolCalls[]names a tool the agent was actually given. Confirm each tool'sexecute()truly RAN via its own side effect / log / the resource it touched — not that the model narrated calling it — and thatresult.outputis 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 finaloutput) or hits themaxStepscap (default 10) and stops with the step-budget note asoutput. 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 matchingtoolCalls[]hasisError: true, the loop continues, and the run still returns a result. The request never crashes. (Only anai-provider failure rejects — asAgentRunErrorcarrying the partialusage/steps— and it surfaces as a handled error, not an unhandled throw.) - If the run streams (the default), the UI shows progress INCREMENTALLY:
the
onEventhook fires perChatEvent(thinkingtext,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.toolsis refused as anunknown toolerror, not executed. Tool execution stays server-side under its backend's guards (e.g. the@molecule/api-ai-toolspathGuards/redactSecrets/blockCommand), and theaiprovider 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.