Skip to content

Tools ​

Actors are deterministic: a behavior must not reach out to the network, the clock, the filesystem, or a database directly. All of that — every effect — goes through a tool. Tools are the actor model's version of dependency injection, and they keep the functional core pure while the imperative shell owns I/O.

Using a tool ​

A behavior's handler receives a tools toolbox scoped to what the actor is allowed to use:

ts
.onMessage(async ({ message, tools }) => {
  const result = await tools.execute('verification.run', {
    taskId: message.taskId,
    patch: message.patch,
  });
  return { reply: { result } };
});

The toolbox exposes has(name), list(), and execute(name, input, options?). Calling a tool the actor wasn't granted is an error, not a silent no-op.

Timeouts and cancellation ​

Use timeoutMs when a tool can wait on an external system:

ts
.onMessage(async ({ message, tools }) => {
  try {
    const result = await tools.execute(
      'verification.run',
      {
        taskId: message.taskId,
        patch: message.patch,
      },
      { timeoutMs: 30_000 }
    );
    return { reply: { result } };
  } catch (error) {
    if ((error as { code?: string }).code === 'ACTOR_TOOL_TIMEOUT') {
      return { reply: { ok: false, reason: 'verification timed out' } };
    }
    throw error;
  }
});

Toolboxes may also be created with defaultTimeoutMs so actors inherit a deadline unless a call provides its own timeoutMs.

Each execution receives a fresh AbortSignal on the tool context. Tool adapters should pass that signal to cancellable APIs such as fetch, process runners, or model clients:

ts
const tools = {
  'verification.run': async (input, context) => {
    const response = await fetch(input.url, { signal: context.signal });
    return { ok: response.ok };
  },
};

When the deadline expires, tools.execute aborts the signal and rejects with an ActorToolTimeoutError whose code is ACTOR_TOOL_TIMEOUT. Treat that timeout as data at the actor boundary, then decide whether to retry, block, or continue.

Declaring tools ​

Tools are declared on the topology and granted to actors via an allowlist:

ts
defineActorWebTopology({
  tools: [tool('repo.diff'), tool('verification.run')],
  actors: {
    verifier: actor({
      id: 'verifier',
      node: 'local',
      behavior: createVerifier,
      tools: ['repo.diff', 'verification.run'], // least privilege
    }),
  },
});

Each actor sees only its granted tools — least privilege by construction.

Functional core / imperative shell ​

LayerResponsibility
Functional corePure decisions from message + context. No I/O, no clock.
Imperative shellCoordination, lifecycle, wiring.
Adapters (tools)The actual I/O. Return facts, don't throw expected errors.

Because effects are injected and adapters return facts, behaviors are deterministic and testable: feed a message and a context, assert the result — no network, no mocks of global state.