Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Effectful-Tech/clanka/llms.txt

Use this file to discover all available pages before exploring further.

The AgentExecutor service is responsible for running scripts in an isolated sandbox and surfacing tool capabilities to the Agent. It can operate in-process or over an RPC transport, making it suitable for both CLI tools and distributed architectures.

AgentExecutor service

export class AgentExecutor extends Context.Service<
  AgentExecutor,
  {
    readonly capabilities: Effect.Effect<Capabilities>
    execute(options: {
      readonly script: string
      readonly onTaskComplete: (summary: string) => Effect.Effect<void>
      readonly onSubagent: (message: string) => Effect.Effect<string>
    }): Stream.Stream<string>
    executeUnsafe<Tool extends keyof typeof AgentTools.tools>(options: {
      readonly tool: Tool
      readonly params: (typeof AgentTools.tools)[Tool]["parametersSchema"]["Encoded"]
    }): Effect.Effect<Schema.Json>
  }
>()("clanka/AgentExecutor") {}
AgentExecutor is an Effect Context.Service tag. The service value exposes three members — capabilities, execute, and executeUnsafe — described in detail below.

Service members

capabilities

readonly capabilities: Effect.Effect<Capabilities>
Returns the Capabilities for this executor. The effect reads the working directory to discover AGENTS.md and resolves the available tool set.

execute()

execute(options: {
  readonly script: string
  readonly onTaskComplete: (summary: string) => Effect.Effect<void>
  readonly onSubagent: (message: string) => Effect.Effect<string>
}): Stream.Stream<string>
Runs a JavaScript script in an isolated VM sandbox and streams its console output as string chunks.
options.script
string
required
The JavaScript source to execute. Top-level await is supported. The script runs inside a Node.js vm.Script sandbox with no access to require, import, process, or other Node.js globals.
options.onTaskComplete
(summary: string) => Effect.Effect<void>
required
Callback invoked when the agent’s script calls the built-in taskComplete function. Receives the final summary string produced by the agent.
options.onSubagent
(message: string) => Effect.Effect<string>
required
Callback invoked when the script delegates work to a sub-agent. Receives the prompt string and must return the sub-agent’s output string.

executeUnsafe()

executeUnsafe<Tool extends keyof typeof AgentTools.tools>(options: {
  readonly tool: Tool
  readonly params: (typeof AgentTools.tools)[Tool]["parametersSchema"]["Encoded"]
}): Effect.Effect<Schema.Json>
Calls a single registered tool by name, bypassing the script sandbox. Useful for testing tool handlers directly or for RPC-layer bridging. Returns the tool’s encoded JSON output.
options.tool
string
required
The name of the tool to invoke. Must be a key of AgentTools.tools.
options.params
object
required
Encoded parameters matching the tool’s parametersSchema.Encoded type. Validated against the schema before execution.

Capabilities model

export class Capabilities extends Schema.Class<Capabilities>("Capabilities")({
  toolsDts: Schema.String,
  agentsMd: Schema.Option(Schema.String),
  supportsSearch: Schema.Boolean,
}) {}
A schema-validated class that describes what the executor can do.
toolsDts
string
required
TypeScript declaration source (.d.ts style) rendered from the registered tool schemas. The agent embeds this verbatim in its system prompt so the model knows what functions are available.
agentsMd
Option<string>
required
The raw contents of AGENTS.md in the working directory, wrapped in Option.some. Option.none when the file does not exist. The agent surfaces this to the model as persistent project-level instructions.
true when a SemanticSearch service is present in context, enabling the search function inside scripts.

Layers

layerLocal()

export const layerLocal: <Toolkit extends Toolkit.Any = never>(options: {
  readonly directory: string
  readonly tools?: Toolkit | undefined
}) => Layer.Layer<
  AgentExecutor,
  never,
  | FileSystem.FileSystem
  | Path.Path
  | ChildProcessSpawner.ChildProcessSpawner
  | HttpClient.HttpClient
  | Exclude<
      Toolkit extends Toolkit.Toolkit<infer T>
        ? Tool.HandlersFor<T> | Tool.HandlerServices<T[keyof T]>
        : never,
      CurrentDirectory | SubagentExecutor | TaskCompleter
    >
>
Creates an AgentExecutor that runs scripts in-process using Node.js vm. This is the standard choice for single-process deployments.
options.directory
string
required
Absolute path to the working directory. Used for reading AGENTS.md and as the CurrentDirectory context for tool handlers.
options.tools
Toolkit
Optional Toolkit of additional tools to register alongside the built-in agent tools. Handlers for CurrentDirectory, SubagentExecutor, and TaskCompleter are provided automatically and must not be included.
layerLocal automatically provides AgentToolHandlers and ToolkitRenderer. You do not need to include them manually.

layerRpc

export const layerRpc: Layer.Layer<AgentExecutor, never, RpcClient.Protocol>
Creates an AgentExecutor that delegates all operations to a remote server over the RpcClient.Protocol transport. The client serializes execute and executeUnsafe calls and deserializes their streaming responses.
Pair this layer with layerRpcServer on the server side. The two share the Rpcs RpcGroup definition, so protocol changes are detected at compile time.

layerRpcServer()

export const layerRpcServer: <Toolkit extends Toolkit.Any = never>(options: {
  readonly directory: string
  readonly tools?: Toolkit | undefined
}) => Layer.Layer<
  never,
  never,
  | RpcServer.Protocol
  | FileSystem.FileSystem
  | HttpClient.HttpClient
  | Path.Path
  | ChildProcessSpawner.ChildProcessSpawner
  | Exclude<
      Toolkit extends Toolkit.Toolkit<infer T>
        ? Tool.HandlersFor<T> | Tool.HandlerServices<T[keyof T]>
        : never,
      CurrentDirectory | SubagentExecutor | TaskCompleter
    >
>
Creates an RPC server that exposes the AgentExecutor protocol over RpcServer.Protocol. Run this layer on the remote host where scripts should execute.
options.directory
string
required
Working directory passed to the underlying makeLocal constructor.
options.tools
Toolkit
Optional additional tools, same constraints as layerLocal.

Rpcs RpcGroup

export class Rpcs extends RpcGroup.make(
  Rpc.make("capabilities", {
    success: Capabilities,
  }),
  Rpc.make("subagentOutput", {
    payload: {
      id: Schema.Finite,
      output: Schema.String,
    },
  }),
  Rpc.make("execute", {
    payload: Schema.Struct({
      script: Schema.String,
    }),
    success: ExecuteOutput,
    stream: true,
  }),
  Rpc.make("executeUnsafe", {
    payload: Schema.Struct({
      tool: Schema.String,
      params: Schema.Json,
    }),
    success: Schema.Json,
  }),
) {}
The Rpcs class defines the full wire protocol between layerRpc (client) and layerRpcServer (server).
RPCDirectionDescription
capabilitiesclient → serverFetch Capabilities for the executor
executeclient → serverStream script output as ExecuteOutput events
subagentOutputclient → serverDeliver sub-agent result back to an awaiting execute stream
executeUnsafeclient → serverInvoke a single tool and return its JSON output

ExecuteOutput tagged union

export const ExecuteOutput = Schema.TaggedUnion({
  Text: { text: Schema.String },
  TaskComplete: { summary: Schema.String },
  Subagent: { id: Schema.Finite, prompt: Schema.String },
})
The execute RPC streams values of this union over the wire.
VariantFieldsDescription
Texttext: stringA chunk of console output from the running script
TaskCompletesummary: stringThe script called taskComplete; carries the final summary
Subagentid: number, prompt: stringThe script requested a sub-agent; the client must call subagentOutput with the result
The client is responsible for correlating Subagent events by id and sending the result back via the subagentOutput RPC before the execute stream can progress past that point.

Usage examples

Local executor (in-process)

import { AgentExecutor } from "clanka"
import { NodeServices, NodeHttpClient, NodeRuntime } from "@effect/platform-node"
import { Effect, Stream, Layer } from "effect"

const program = Effect.gen(function* () {
  const executor = yield* AgentExecutor.AgentExecutor

  const capabilities = yield* executor.capabilities
  console.log("Tools DTS length:", capabilities.toolsDts.length)
  console.log("Has AGENTS.md:", capabilities.agentsMd._tag === "Some")
  console.log("Supports search:", capabilities.supportsSearch)

  const output = yield* executor.execute({
    script: `console.log(await ls("."))`,
    onTaskComplete: (summary) => Effect.sync(() => console.log("Done:", summary)),
    onSubagent: (_prompt) => Effect.succeed(""),
  }).pipe(Stream.mkString)

  console.log(output)
})

const AppLayer = AgentExecutor.layerLocal({
  directory: process.cwd(),
}).pipe(
  Layer.provide(NodeServices.layer),
  Layer.provide(NodeHttpClient.layerUndici),
)

Effect.provide(program, AppLayer).pipe(NodeRuntime.runMain)

Remote executor via RPC

import { AgentExecutor } from "clanka"
import { Layer } from "effect"

// Client side — provide any RpcClient.Protocol implementation
// (e.g. an HTTP or WebSocket transport from the effect/unstable/rpc package)
const ClientLayer = AgentExecutor.layerRpc.pipe(
  Layer.provide(/* your RpcClient.Protocol layer */),
)

// Server side (in a separate process) — provide any RpcServer.Protocol implementation
const ServerLayer = AgentExecutor.layerRpcServer({
  directory: "/workspace",
}).pipe(
  Layer.provide(/* your RpcServer.Protocol layer */),
  // ... platform layers (NodeServices, NodeHttpClient, etc.)
)

Build docs developers (and LLMs) love