@bernierllc/ai-agent-runtime
v0.2.0
Published
Framework-agnostic streaming agent loop for multi-turn LLM tool-call orchestration
Downloads
303
Readme
@bernierllc/ai-agent-runtime
Framework-agnostic streaming agent loop for multi-turn LLM execution with any @bernierllc/ai-provider-core provider. Owns the full agent execution loop — LLM call, tool-call detection, tool dispatch, result injection, budget/turn/timeout guards — and emits every step as AsyncIterable<AgentEvent> so callers can stream output progressively.
Installation
npm install @bernierllc/ai-agent-runtimeUsage
import { createAgentRuntime } from '@bernierllc/ai-agent-runtime';
import { OpenAIProvider } from '@bernierllc/ai-provider-openai';
const provider = new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! });
const runtime = createAgentRuntime({
provider,
systemPrompt: 'You are a helpful assistant.',
maxTurns: 10,
tokenBudget: 20_000,
timeoutMs: 30_000,
});
const messages = [{ role: 'user' as const, content: 'What is the weather in Paris?' }];
const context = { conversationId: crypto.randomUUID(), messages: [] };
for await (const event of runtime.run(messages, context)) {
switch (event.type) {
case 'text-delta':
process.stdout.write(event.delta);
break;
case 'tool-call-start':
console.log(`\nCalling tool: ${event.toolName}`);
break;
case 'tool-call-result':
console.log(`Tool result:`, event.result);
break;
case 'done':
console.log(`\nFinished in ${event.totalTurns} turn(s), ${event.totalTokens} tokens`);
break;
case 'error':
console.error('Agent error:', event.error.message);
break;
}
}With tools
import {
createAgentRuntime,
type ToolExecutor,
type ToolResult,
type AgentContext,
} from '@bernierllc/ai-agent-runtime';
const weatherTool: ToolExecutor = {
name: 'get_weather',
description: 'Get current weather for a city',
schema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
},
required: ['city'],
},
async execute(input: unknown, _ctx: AgentContext): Promise<ToolResult> {
const { city } = input as { city: string };
// Real implementation would call a weather API
return {
success: true,
message: `Weather fetched for ${city}`,
data: { city, temp: 22, condition: 'Sunny' },
};
},
};
const runtime = createAgentRuntime({
provider,
systemPrompt: 'You are a weather assistant.',
tools: [weatherTool],
maxTurns: 5,
});
for await (const event of runtime.run(messages, context)) {
// handle events
}Dynamic system prompt
Pass a function to generate the system prompt per-run from the agent context:
const runtime = createAgentRuntime({
provider,
systemPrompt: async (ctx) => {
const prefs = await loadUserPreferences(ctx.conversationId);
return `You are a helpful assistant. User preferences: ${JSON.stringify(prefs)}`;
},
});Abort signal
Pass a standard AbortSignal to cancel an in-flight run:
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5_000);
for await (const event of runtime.run(messages, context, controller.signal)) {
if (event.type === 'error') {
// AgentRuntimeAbortError if cancelled by caller
// AgentRuntimeTimeoutError if per-turn timeoutMs fired
console.error(event.error.code, event.error.message);
}
}API
createAgentRuntime(config): AgentRuntime
Creates a new agent runtime instance.
| Field | Type | Default | Description |
|---|---|---|---|
| provider | AIProvider | required | Any @bernierllc/ai-provider-core provider |
| systemPrompt | string \| (ctx) => string \| Promise<string> | none | System prompt or dynamic factory |
| maxTurns | number | 10 | Maximum turns before AgentRuntimeMaxTurnsError |
| tokenBudget | number | none | Maximum cumulative tokens before AgentRuntimeTokenBudgetError |
| timeoutMs | number | none | Per-turn timeout in milliseconds before AgentRuntimeTimeoutError |
| tools | ToolExecutor[] | [] | Tool executors available to the agent |
AgentRuntime.run(messages, context, signal?)
Returns AsyncIterable<AgentEvent>. Drives the multi-turn loop until the agent produces a final text response, a safety guard fires, or the caller aborts.
AgentEvent union
| type | Fields | When emitted |
|---|---|---|
| text-delta | delta: string | Provider returns a non-empty text response |
| tool-call-start | toolName, toolCallId, input | Before a tool is executed |
| tool-call-result | toolName, toolCallId, result | After a tool returns |
| turn-complete | turnIndex, totalTokens | End of each turn |
| done | finalText, totalTurns, totalTokens | Run finished successfully |
| error | error: AgentRuntimeError | Any failure — loop terminates after this event |
Error classes
| Class | Code | When thrown |
|---|---|---|
| AgentRuntimeError | AGENT_RUNTIME_ERROR | Base class for all errors |
| AgentRuntimeMaxTurnsError | MAX_TURNS_EXCEEDED | Loop reached maxTurns without a text response |
| AgentRuntimeTokenBudgetError | TOKEN_BUDGET_EXCEEDED | Cumulative tokens exceeded tokenBudget |
| AgentRuntimeTimeoutError | TURN_TIMEOUT | A single turn took longer than timeoutMs |
| AgentRuntimeAbortError | RUN_ABORTED | Caller's AbortSignal fired |
All errors carry Error.cause chains for full stack trace preservation.
Integrations
Logger integration
@bernierllc/ai-agent-runtime uses @bernierllc/logger internally for structured logging at debug, warn, and error levels. All log output flows through the shared logger configuration — no extra setup needed.
NeverHub integration
This is a core package. NeverHub integration is optional and handled by the service layer (for example, @bernierllc/agent-service or whichever service orchestrates this runtime). This package does not register with @bernierllc/neverhub-adapter directly.
If you are building a service that wraps this runtime, wire in NeverHub at the service boundary:
import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
if (await NeverHubAdapter.detect()) {
const hub = new NeverHubAdapter();
await hub.register({ type: 'agent-runtime-service', version: '1.0.0' });
}
// Core runtime works regardless — graceful degradation is built in.Graceful degradation
All optional integrations (NeverHub, telemetry, external logging) are additive. The runtime works correctly without any of them, and callers that do not configure optional integrations will see no change in behaviour.
License
Bernier LLC limited-use license. See LICENSE.
