@acentric/agent
v0.6.0
Published
The Agent class: what a developer writes an agent with, and its types.
Downloads
882
Readme
@acentric/agent
The Agent class: what an agent is written with. An agent file default-exports one. The
acentric command runs it on your machine and
deploys it to a server, where each actor of the agent is a live instance with its own log.
import { Agent } from '@acentric/agent';
export default new Agent({
model: { provider: 'anthropic', modelId: 'claude-opus-5-5' },
instructions: 'You review changes.',
});The package also exports the types of everything an agent is made of: its inputs, tools, hooks, configuration, and the records of an actor's log.
An MCP server is a source of an agent's tools, from a second entry point. The model is offered
each of the server's tools as mcp__<name>__<tool>; a token for the server goes in headers,
which may be a function of the actor's context, to read it from the agent's secrets. An agent
that does not import @acentric/agent/mcp has none of the MCP client in its bundle.
import { Agent } from '@acentric/agent';
import { mcp } from '@acentric/agent/mcp';
export default new Agent({
model: { provider: 'anthropic', modelId: 'claude-opus-5-5' },
tools: [mcp('github', { url: 'https://api.githubcopilot.com/mcp/' })],
});An extension is a named bundle of tools, inputs, instructions and hooks, written once and added to any agent. Its tools and inputs join the agent's own, its instructions follow them, and its hooks are asked before the agent's own. A tool name or an event type that is there twice is an error.
import { Agent, defineExtension } from '@acentric/agent';
const guard = defineExtension({
name: 'guard',
hooks: {
beforeTool: (call) => (call.toolName === 'bash' ? { block: 'No shell here' } : undefined),
},
});
export default new Agent({
model: { provider: 'anthropic', modelId: 'claude-opus-5-5' },
extensions: [guard],
});A tool schedules an event for its own actor by returning it with its result. The actor receives
it at that time, as if it were sent then, which starts a run. The event is any of the agent's
inputs, so the agent words what the model is told. A caller schedules one the same way, with at
on the event it sends, or acentric send --in 10m.
execute: (args) => ({
content: 'Set',
schedule: [{ type: 'reminder', payload: { note: args.note }, at: Date.now() + 600_000 }],
}),An input has messages, which decides what the model is told of an event, or handle, which
acts on the event with no model turn: it checks on a job, calls a service, schedules the next
check. The model gets a turn only when handle returns messages.
inputs: {
'build.check': {
schema: CheckSchema,
handle: async (event) => {
const status = await checkBuild(event.payload.job);
if (!status.done) {
return { schedule: [{ type: 'build.check', payload: event.payload, at: Date.now() + 30_000 }] };
}
return { messages: [{ role: 'user', content: [{ type: 'text', text: status.summary }] }] };
},
},
},A tool or a handler that gives work to a service that calls back asks its context for a URL.
Whatever the service posts to it reaches the actor as an event of the type asked for. The URL needs
no other credential, so it is a secret. A server gives one under its API's address; the local
runner only with --tunnel or --public-url.
execute: async (args, context) => {
const url = await context.callbackUrl('command.finished');
const started = await startCommand(args.command, { callback: { url } });
return started.running ? `Running in the background as ${started.id}` : started.output;
},Each actor keeps a state: a small JSON object, at most 64 KB, beside its log. All of the agent's
code reads it as context.state; tools and handlers change it by returning state, a JSON Merge
Patch saved with their result. The state schema gives a new actor its first state, and runs again
when the actor moves to a new deployment. Each extension keeps its own part, under its name, and its
code sees only that part.
export default new Agent({
model: { provider: 'anthropic', modelId: 'claude-opus-5-5' },
state: z.object({ plan: z.array(z.string()).default([]) }),
instructions: (context) => `The plan so far: ${context.state.plan.join(', ') || 'none'}.`,
tools: [
{
type: 'function',
name: 'add_step',
description: 'Adds a step to the plan',
parameters: { type: 'object', properties: { step: { type: 'string' } } },
execute: (args, context) => ({
content: 'Added',
state: { plan: [...context.state.plan, String(args['step'])] },
}),
},
],
});An agent has no limit it does not set. maxTurns bounds the turns of one run and maxRunMs how
long it goes on; toolTimeoutMs bounds each tool call, and a tool's own timeoutMs takes its
place for that tool. Times are in milliseconds.
export default new Agent({
model: { provider: 'anthropic', modelId: 'claude-opus-5-5' },
maxTurns: 50,
maxRunMs: 30 * 60_000,
toolTimeoutMs: 60_000,
});A project that uses it is made with:
npx @acentric/cli init my-agentAn agent runs in Node on your machine, and with only the web platform's APIs on a server.
