Skip to content

INTERACTION Worker API ​

Worker API

INTERACTION executes external actions, normalizes observations, and delivers final messages. Graph defines dependencies; Loop owns iteration and termination; Worker injects tools, MCP clients, and output sinks. No separate Agent manager, automatic command registration, or plugin scanner is needed.

Functional selection and parameter effects ​

NodeSuitable tasksHow inputs change execution
INTERACTION.ACT.TOOLRegistered file, API, database, code or business operationscall.name selects a tool, arguments supplies structured inputs, call.id correlates the result. Executes one tool per call
INTERACTION.ACT.MCPDiscover capabilities or invoke tools on connected MCP servicesoperation chooses discover/invoke, server scopes the service, and invoke call specifies tool/arguments
INTERACTION.OBSERVEConvert raw tool results into observations for reasoningresult status/content/structuredContent/references/error determine the tool message and retained data; no automatic retry or state update
INTERACTION.OUTPUTDeliver a validated answer and artifacts to a UI, queue or other sinkdeliveryId correlates receipts, message carries content, artifacts supplies references. OutputSink defines actual delivery semantics

Parameters affect action scope and result completeness ​

Parameter / designNode impactWorker/business impact
Tool description / inputSchemaHelps the model understand when to invoke and which fields to supplyDescriptions are not execution policy. ToolRegistry calls RegisteredTool.validate; it does not automatically run a complete JSON Schema validator
Tool argumentsDetermines query scope, paths, object IDs or write contentValidate business rules and permissions in validate/execute. Larger batches/scopes can increase database/API/output load
Tool effects / requiresApprovalDescribes effects and approval intentDoes not automatically request confirmation. Application Graph/Loop checks approval before writing; the tool also validates approval and object version
call.idCorrelates actions, results and observationsDoes not create idempotent transactions or suppress repeated effects. Check backend idempotency keys/receipts before retry
MCP discover serverExplicit server discovers one service; omission traverses registered servicesMore services can increase pages and capability size. Registration and Sandbox permission must both hold; no automatic new connections
MCP operation:"invoke"Calls a specific tool on a connected serverThe server enforces business permissions/input constraints; isError becomes a failed result rather than successful prose
OBSERVE structuredContent / referencesRetains machine-readable data and provenance, plus failure status/errorsHelps subsequent parsing, but large objects expand model input. Select/trim and explicitly UPDATE Context; no automatic conversation insertion
OUTPUT deliveryId / artifactsChecks receipt ID consistency and retains artifact referencesStable deliveryId supports sink idempotency, but the Worker does not deduplicate on its behalf. References do not automatically upload, copy or publish files

Map messages, action requests and observations explicitly between Contracts and Infer. Keep call IDs and tool sources; OpenAI-compatible Infer tool messages require metadata.actionRequestId. Concatenating a tool string into a user message loses call identity and failure semantics. Construct messages for the actual model protocol.

Numerical limits and resource tradeoffs ​

SettingDefault / rangeEffect of adjustment
Read-only command maxEntries / maxOutputBytes / maxErrorBytes1000 entries / 64 KiB / 8 KiB; hard caps 10000 / 1 MiB / 64 KiBLarger values preserve more output and increase transfer/Context cost. Smaller values can hide errors; inspect structuredContent.truncated
web_search arguments.limitDefault 5, range 1–20More search coverage and more reading/filtering work. Returns title/url/snippet without opening page bodies
Brave Provider timeoutMs / maxResponseBytesConfigured on the provider, detailed belowLonger waits allow slow responses but hold Worker capacity; larger responses consume more memory. Limit violations fail explicitly
McpRegistry maxDiscoveryPages / maxCapabilitiesDefault 100 pages / 1000 capabilities; positive integersTotal budget across services for one discover call. Larger catalogs increase discovery latency and model-visible tool directory size
Worker concurrencyPositive integer; unlimited by defaultBounds entry calls without adding MCP connections, API quota, tool pools or sink throughput

Inject tools, MCP and output resources at construction. ACT.TOOL/OBSERVE exist by default; MCP/OUTPUT are registered only when their resources are injected. Pass YAML settings to the matching helper/provider; configuration alone does not create tools or connections.

Continue, retry or finish ​

Read ExternalResult.status / OutputReceipt.status before choosing the next Graph. OBSERVE describes failed, timeout, cancelled and unknown without promoting them to success. Unknown calls for verification. OUTPUT accepted establishes sink acceptance under its contract; actual delivery/file availability needs sink-defined semantics. Shorter timeouts do not undo external effects. SDKs/executors must cooperate with cancellation/deadlines, and retries still require checking actual state.

1. Imports and API inventory ​

ts
import { createBraveWebSearchProvider, createInteractionWorker, createReadOnlyCommandTools, createWebSearchTool, ToolRegistry, McpRegistry } from "@codesoul-co/ditto/worker/interaction";
// Also exported by @codesoul-co/ditto.
APIReturn / purpose
createInteractionWorker(options?)WorkerDefinition; standard setup
createInteractionNodes(options?)WorkerNodes for custom defineWorker
ToolRegistry.register / list / callRegister/remove, list permitted tools, invoke one tool
createReadOnlyCommandTools(options?)14 optional read-only command registrations with structured inputs and bounded output
createWebSearchTool({ provider })Optional provider-neutral web_search registration with bounded normalized results
createBraveWebSearchProvider(options)Native-fetch Brave Web Search adapter; application supplies credentials
McpRegistry.register / executeRegister/remove clients, discover or invoke MCP tools
createToolHandler / createMcpHandler / createOutputHandlerConstruct individual leaf NodeHandlers
observeExternalResult(input)Synchronously returns Observation; pure normalization
RegisteredTool.validate / executeApplication tool port; execute returns ToolExecutionOutcome
McpClient.listTools / callToolApplication-owned SDK client port
OutputSink.deliverPromise<OutputReceipt>; delivery port
NodeInputOutput
INTERACTION.ACT.TOOL{ call: ToolCall }ExternalResult
INTERACTION.ACT.MCPdiscover / invoke uniondiscover / invoke union
INTERACTION.OBSERVE{ result: ExternalResult }Observation
INTERACTION.OUTPUT{ deliveryId, message, artifacts? }OutputReceipt

These Nodes return the listed Output directly, without NodeResult, executionId, or an output wrapper. Actions correlate through callId; delivery through deliveryId. Registration/validation errors, permission denial, and unclassified SDK errors reject the Promise; business failures use structured results.

2. Complete examples and setup ​

Examples share the imports from examples/interaction.ts. The complete file is checked by npm run typecheck; importing executes no examples. Connect SDK resources in your application before injecting them.

ts
import { createDitto, defineWorker, graph, loop, type WorkerContext } from "@codesoul-co/ditto";
import {
  createInteractionWorker, createInteractionNodes, createToolHandler, createMcpHandler,
  createOutputHandler, observeExternalResult, ToolRegistry, McpRegistry,
  interactionObserveNode, type RegisteredTool, type McpClient, type OutputSink,
} from "@codesoul-co/ditto/worker/interaction";

RegisteredTool.validate / execute ​

Schema describes the capability; Core installs no JSON Schema validator. validate must enforce the actual business input contract. execute performs one action, without owning an Agent loop.

FieldRequirement and behavior
nameRequired; 1–64 letters, digits, underscores, or hyphens; unique per registry
description?Capability description
inputSchemaRequired JsonObject; provided to callers/models, not automatically enforced
effects?Array of read / write / execute / network descriptors
requiresApproval?Descriptive flag; Core does not open approval UI or change Sandbox permissions
validate(args)Synchronous validation; a thrown error prevents execute
execute(args, context)Returns Promise<Omit<ExternalResult, "callId" | "source">>
ts
export const readTextTool: RegisteredTool = {
  name: "read_text", description: "Read a workspace text file",
  inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
  effects: ["read"], requiresApproval: false,
  validate(args) {
    if (!args || typeof args.path !== "string" || !args.path.trim()) throw new Error("path must be a nonempty string");
  },
  async execute(args, context) {
    return { status: "success", content: await context.services.sandbox.readText(args.path as string) };
  },
};

context provides services.sandbox/config/providers, Worker/execution identity, invoke/emit, and internal Graph execution. Use Sandbox for file, command, and network access; it is a cooperative guard, not isolation against arbitrary application JS.

createReadOnlyCommandTools(options?) ​

This helper returns 14 ordinary RegisteredTool values; it does not register them or create a process executor. Inputs expose only common read operations rather than arbitrary flags:

ToolStructured argumentsFixed command shape
greppattern, paths, optional recursive, ignoreCase, fixedStringsgrep -n ... -- pattern paths...
lsoptional path, allls -1 [-a] -- path
catpathcat -- path
findoptional path, name, type, maxDepthfind path -maxdepth ... [-type ...] [-name ...] -print
head / tailpath, optional lines (1–1,000; default 20)head/tail -n lines -- path
wcpath, optional metric (lines / words / bytes)wc -l/-w/-c -- path
sortpath, optional reverse, numeric, uniquesort [-r] [-n] [-u] -- path
uniqpath, optional count, ignoreCaseuniq [-c] [-i] -- path
cutpath, fields, optional one-character delimitercut [-d delimiter] -f fields -- path
stat / filepathstat/file -- path
dupath, optional maxDepth (0–32; default 1)du -k --max-depth=N -- path
pwdno fieldspwd

Paths must be relative POSIX workspace paths; absolute paths, backslashes, traversal segments, unknown fields, and raw find expressions are rejected before execution. Defaults are 1,000 returned lines, 64 KiB stdout, and 8 KiB stderr. Options may lower or raise them only within hard caps of 10,000 lines, 1 MiB stdout, and 64 KiB stderr. Output truncation is UTF-8 safe and reported in structuredContent.truncated.

ts
const commandTools = createReadOnlyCommandTools({ maxEntries: 200, maxOutputBytes: 32 * 1024 });
const runtime = createDitto({
  sandbox: { tools: commandTools.map(tool => tool.name), execute: true },
  sandboxExecutor,
  workers: [createInteractionWorker({ tools: commandTools })],
});

Each invocation calls sandbox.run({ command, args }) once. A nonzero exit becomes a failed ExternalResult with COMMAND_EXIT_NONZERO; startup, permission, transport, or executor exceptions still reject and are never retried automatically. Relative-path validation is a cooperative API boundary, not protection against a malicious executor or workspace symlink; use OS/container isolation for untrusted workloads.

createWebSearchTool({ provider }) ​

web_search accepts { query, limit? }; query is a nonempty single line of at most 600 characters and 75 words, and limit defaults to 5 with a hard maximum of 20. The Tool normalizes at most the requested results to title / url / snippet, bounds titles to 256 characters and snippets to 2,048 characters, accepts only HTTP(S) URLs without embedded credentials, and emits each URL as a Reference.

ts
const provider = createBraveWebSearchProvider({ apiKey: process.env.DITTO_WORKER_INTERACTION_BRAVE_SEARCH_API_KEY! });
const webSearch = createWebSearchTool({ provider });
const runtime = createDitto({
  sandbox: { tools: [webSearch.name], network: [provider.origin] },
  workers: [createInteractionWorker({ tools: [webSearch] })],
});

The registry checks Tool permission before validation; the Tool checks the exact Provider origin before calling it. Provider errors and malformed results become one sanitized failed / WEB_SEARCH_FAILED result without retry. Credentials, quotas and retry/lifecycle policies remain application responsibilities and never enter the ToolCall. The Brave adapter uses GET /res/v1/web/search, X-Subscription-Token, native fetch and a configurable timeout; it is not registered automatically.

OutputSink.deliver ​

Return a receipt with the same deliveryId. This sink prints JSON; applications can supply UI, HTTP, or queue delivery with their own lifecycle and retry policy.

ts
export const consoleSink: OutputSink = {
  async deliver(input) {
    console.log(JSON.stringify({ id: input.deliveryId, message: input.message, artifacts: input.artifacts }));
    return { deliveryId: input.deliveryId, status: "accepted", ...(input.artifacts ? { artifacts: input.artifacts } : {}) };
  },
};

createInteractionWorker(options?) ​

OptionType / default behavior
tools?readonly RegisteredTool[] or ToolRegistry; empty by default, TOOL remains exposed
mcp?Readonly<Record<string, McpClient>> or McpRegistry; omission leaves MCP unexposed
output?OutputSink; omission leaves OUTPUT unexposed
concurrency?Positive integer limiting concurrent calls per replica; unlimited by default

OBSERVE is always exposed. Arrays/maps are registered at construction; supplied registries retain identity for dynamic updates. The factory grants no tool or network permissions.

ts
export function setupInteraction(client: McpClient) {
  return createDitto({
    sandbox: { tools: ["read_text"], read: true, mcp: ["files"] },
    workers: [createInteractionWorker({ tools: [readTextTool], mcp: { files: client }, output: consoleSink, concurrency: 8 })],
  }); // client is already connected; the application closes it after runtime.close().
}

3. ACT.TOOL and tool registry ​

ts
interface ToolCall { id: string; name: string; arguments: JsonObject; }
interface InteractionToolInput { call: ToolCall; }
type InteractionToolOutput = ExternalResult;

call.id is nonempty; name selects a registered tool. arguments is a JSON object whose business shape is checked by validate. Execution checks ID, tools permission, registration, and arguments before execute, then validates the result. Core supplies callId=call.id and source=call.name.

ts
export async function invokeTool() {
  const runtime = createDitto({ sandbox: { tools: ["read_text"], read: true }, workers: [createInteractionWorker({ tools: [readTextTool] })] });
  try {
    const result = await runtime.invoke("INTERACTION.ACT.TOOL", { call: { id: "read-1", name: "read_text", arguments: { path: "README.md" } } });
    if (result.status !== "success") throw new Error(result.error?.code ?? result.status);
    return result.content; // Direct ExternalResult; no .output wrapper.
  } finally { await runtime.close(); }
}

ToolRegistry.register / list / call ​

new ToolRegistry() starts empty. register(tool) returns an unregister callback: true once, false afterwards. Invalid/duplicate names throw immediately. list(context) returns permitted definitions without executable functions. call(call, context) returns Promise<ExternalResult>. Use a genuine Worker context.

ts
export async function toolRegistryApis(context: WorkerContext<unknown, unknown>) {
  const tools = new ToolRegistry();
  const unregister = tools.register(readTextTool);
  const definitions = tools.list(context); // Only tools allowed by context.services.sandbox.
  try {
    const result = await tools.call({ id: "read-1", name: "read_text", arguments: { path: "README.md" } }, context);
    return { definitions, result };
  } finally { unregister(); } // true on first removal, false afterwards; does not close tool resources.
}

4. ACT.MCP and client adapters ​

ts
type InteractionMcpInput =
  | { operation: "discover"; server?: string }
  | { operation: "invoke"; server: string; call: ToolCall };
type InteractionMcpOutput =
  | { operation: "discover"; capabilities: readonly McpCapability[] }
  | { operation: "invoke"; result: ExternalResult };
interface McpClient {
  listTools(params?: { cursor?: string }, options?: McpCallOptions): Promise<{
    tools: readonly { name: string; description?: string; inputSchema: JsonObject; outputSchema?: JsonObject }[];
    nextCursor?: string;
  }>;
  callTool(params: { name: string; arguments: JsonObject }, options?: McpCallOptions): Promise<McpToolResult>;
}
interface McpToolResult {
  content?: MessageContent; structuredContent?: JsonValue;
  references?: readonly Reference[]; isError?: boolean; error?: InteractionError;
}

discover invokes no tools, produces no Observation, and changes no Context. Omitting server visits all registered servers; every access requires mcp permission and denied servers are not silently filtered. Capabilities contain server, name, inputSchema, and optional description/outputSchema. invoke uses the application-selected server/tool and preserves callId; source is server:toolName.

ts
export async function invokeMcp(client: McpClient, absoluteFilePath: string) {
  const runtime = setupInteraction(client);
  try {
    const discovery = await runtime.invoke("INTERACTION.ACT.MCP", { operation: "discover", server: "files" });
    if (discovery.operation !== "discover") throw new Error("Expected discovery response");
    const response = await runtime.invoke("INTERACTION.ACT.MCP", {
      operation: "invoke", server: "files", call: { id: "mcp-read", name: "read_text_file", arguments: { path: absoluteFilePath } },
    });
    if (response.operation !== "invoke") throw new Error("Expected invocation response");
    return { capabilities: discovery.capabilities, result: response.result };
  } finally { await runtime.close(); }
}

McpRegistry.register / execute ​

new McpRegistry({ maxDiscoveryPages?, maxCapabilities? }) defaults to 100 pages and 1000 capabilities per discovery request; limits are positive integers. Repeated cursors, exceeded limits, or invalid schemas fail. register(server, client) requires a nonempty unique name and returns an unregister callback. execute(input, sandbox) handles discovery and invocation; removal does not close clients.

ts
export async function mcpRegistryApis(client: McpClient, absoluteFilePath: string) {
  const runtime = createDitto({ sandbox: { mcp: ["files"] } });
  const mcp = new McpRegistry({ maxDiscoveryPages: 10, maxCapabilities: 200 });
  const unregister = mcp.register("files", client);
  try {
    const discovery = await mcp.execute({ operation: "discover", server: "files" }, runtime.services.sandbox);
    const result = await mcp.execute({ operation: "invoke", server: "files", call: { id: "mcp-1", name: "read_text_file", arguments: { path: absoluteFilePath } } }, runtime.services.sandbox);
    return { discovery, result };
  } finally { unregister(); await runtime.close(); }
}

McpClient.listTools / callTool adapter example ​

ts
export function mcpClientAdapter(client: McpClient): McpClient {
  return {
    listTools: (params, options) => client.listTools(params, options),
    async callTool(params, options) {
      const result = await client.callTool(params, options);
      return {
        ...(result.content === undefined ? {} : { content: result.content }),
        ...(result.structuredContent === undefined ? {} : { structuredContent: result.structuredContent }),
        ...(result.references === undefined ? {} : { references: result.references }),
        ...(result.isError === undefined ? {} : { isError: result.isError }),
        ...(result.error === undefined ? {} : { error: result.error }),
      };
    },
  };
}

This wrapper preserves method receivers and optional fields; its client already satisfies the neutral port. Adapt native SDK result unions first. See the real MCP script and setup instructions for an executable official-SDK example. Ditto installs no MCP SDK and opens no transports automatically. Adapt SDK-specific result unions to McpToolResult. isError=true becomes failed, with MCP_TOOL_ERROR when no safe error object is available.

5. ExternalResult, OBSERVE, and messages ​

ts
interface ExternalResult {
  callId: string; source: string;
  status: "success" | "failed" | "cancelled" | "timeout" | "unknown";
  content?: MessageContent; structuredContent?: JsonValue;
  references?: readonly Reference[]; error?: InteractionError; metadata?: JsonObject;
}
interface InteractionError { code: string; message: string; retryable?: boolean; }
interface Observation extends Omit<ExternalResult, "content"> { message: Message; }
interface Reference { uri: string; mediaType?: string; digest?: string; }

Success supplies at least one of content, structuredContent, or references; other statuses require error. Content/structuredContent/metadata must be JSON-compatible: no functions, BigInt, nonfinite numbers, cycles, or Date/Map-like objects. Reference uri is nonempty.

observeExternalResult / INTERACTION.OBSERVE ​

The pure helper is synchronous; Node invocation is asynchronous. Both preserve correlation, status, structured data, references, error, and metadata, and generate a role=tool, name=source Message. A single text part becomes a string; other content becomes text/json/reference parts. Failures include a safe summary without being promoted to success. Neither retries nor updates CONTEXT.

ts
export async function observeApis() {
  const result = { callId: "read-1", source: "read_text", status: "success" as const, content: "file text" };
  const local = observeExternalResult({ result });
  const runtime = createDitto({ workers: [createInteractionWorker()] });
  try {
    const routed = await runtime.invoke("INTERACTION.OBSERVE", { result });
    return { local, routed }; // message = { role: "tool", name: "read_text", content: "file text" }
  } finally { await runtime.close(); }
}

Shared JSON / Message types ​

Import these from @codesoul-co/ditto/contracts or the root package. INFER has its own model-specific Message contract; map content explicitly across Workers. MCP client discovery requires inputSchema for each tool, while the shared McpCapability type marks it optional.

ts
export type JsonValue = string | number | boolean | null
  | readonly JsonValue[] | { readonly [key: string]: JsonValue };
export type JsonObject = Readonly<Record<string, JsonValue>>;
export type MessagePart =
  | { type: "text"; text: string }
  | { type: "json"; data: JsonValue }
  | { type: "reference"; reference: Reference };
export type MessageContent = string | JsonValue | readonly MessagePart[];
export interface Message {
  role: "system" | "user" | "assistant" | "tool";
  content: MessageContent;
  name?: string;
}
export interface McpCapability {
  server: string; name: string; description?: string;
  inputSchema?: JsonObject; outputSchema?: JsonObject;
}

6. OUTPUT: requests and receipts ​

ts
interface InteractionOutputInput {
  deliveryId: string; message: Message; artifacts?: readonly Artifact[];
}
interface Artifact { name: string; reference: Reference; }
interface OutputReceipt {
  deliveryId: string; status: "accepted" | "rejected" | "unknown";
  artifacts?: readonly Artifact[]; error?: InteractionError; metadata?: JsonObject;
}

deliveryId is nonempty and should identify one application delivery; Core performs no automatic deduplication. message.role is system/user/assistant/tool, normally assistant for final output. content is required and may be JSON null, numbers, objects, arrays, or text. Optional name is a string. Artifacts require nonempty names and URIs; Core passes references without writing or uploading files.

ts
export async function outputApi() {
  const runtime = createDitto({ workers: [createInteractionWorker({ output: consoleSink })] });
  try {
    return await runtime.invoke("INTERACTION.OUTPUT", {
      deliveryId: "report-1", message: { role: "assistant", content: { summary: "Complete", count: 2 } },
      artifacts: [{ name: "report", reference: { uri: "urn:report:1", mediaType: "application/json" } }],
    }); // { deliveryId: "report-1", status: "accepted", artifacts: [...] }
  } finally { await runtime.close(); }
}

accepted means sink acceptance, not final delivery or reading. rejected/unknown require error. Invalid receipt IDs, artifacts, or metadata throw, without proving that delivery did not happen. Core does not automatically retry external effects.

7. Errors, permissions, and configuration ​

Tools can return business failures and sinks can reject delivery. Use safe structured errors rather than raw backend diagnostics:

ts
export const missingRecordTool: RegisteredTool = {
  name: "lookup", inputSchema: { type: "object" }, validate() {},
  async execute() { return { status: "failed", error: { code: "NOT_FOUND", message: "No matching record", retryable: false } }; },
};
export const rejectedSink: OutputSink = {
  async deliver(input) { return { deliveryId: input.deliveryId, status: "rejected", error: { code: "DELIVERY_REJECTED", message: "The destination rejected the result" } }; },
};

Public error.code is at most 64 characters using letters, digits, underscores, dots, or hyphens. message is at most 512 characters and excludes newlines, controls, credential markers, absolute paths, and stack frames. retryable is descriptive and triggers no retry. Unsafe MCP business diagnostics receive a fixed replacement; invalid Tool/OUTPUT diagnostics are rejected.

ConfigurationLocation
tools / mcp / output / concurrencycreateInteractionWorker options
tools / mcp / read / write / execute / networkcreateDitto({ sandbox }) or explicitly loaded shared Sandbox env
SDK endpoint / tokenApplication env and connection code, not Graph input
MCP discovery limitsnew McpRegistry({ maxDiscoveryPages, maxCapabilities })
workers.interaction YAMLcommands/webSearch behavior settings; explicitly pass config.interaction groups to factories

Cancellation comes from Runtime call options through WorkerContext to tools/MCP/WebSearch providers, rather than from Node payload fields. Executors/applications own deadlines, retries and SDK cleanup. Stopping Runtime waiting does not imply stopping an external action. The Linux example explicitly injects SandboxExecutor; it is not a default Core capability. See the Linux/macOS tool example.

8. Lower-level composition ​

createInteractionNodes(options?) ​

Options accept only { tools?: ToolRegistry, mcp?: McpRegistry, output?: OutputSink }, not arrays or client maps. Returns WorkerNodes for composition with defineWorker capabilities such as expose/resources/dispose.

ts
export function interactionNodes(client: McpClient) {
  const tools = new ToolRegistry(); tools.register(readTextTool);
  const mcp = new McpRegistry(); mcp.register("files", client);
  return defineWorker({ type: "INTERACTION", nodes: createInteractionNodes({ tools, mcp, output: consoleSink }) });
}

createToolHandler / createMcpHandler / createOutputHandler ​

Each takes its corresponding registry or sink and returns a typed NodeHandler. Use these for partial capabilities or custom handler composition; validation remains the same as the standard factory.

ts
export function interactionHandlers(client: McpClient) {
  const tools = new ToolRegistry(); tools.register(readTextTool);
  const mcp = new McpRegistry(); mcp.register("files", client);
  return defineWorker({ type: "INTERACTION", nodes: {
    "INTERACTION.ACT.TOOL": createToolHandler(tools),
    "INTERACTION.ACT.MCP": createMcpHandler(mcp),
    "INTERACTION.OBSERVE": async input => observeExternalResult(input),
    "INTERACTION.OUTPUT": createOutputHandler(consoleSink),
  } });
}

Node descriptors ​

interactionToolNode, interactionMcpNode, interactionObserveNode, and interactionOutputNode expose .type and .define(workerType, handler). They identify semantics without registering or executing anything. Replacement handlers own the full contract; normally prefer createInteractionWorker.

ts
export const observationDefinition = interactionObserveNode.define("INTERACTION", async input => observeExternalResult(input));

9. Graph + Loop + Worker and lifecycle ​

This Graph composes TOOL → OBSERVE → OUTPUT, while Loop iterates two files. Implementations remain inside the Worker. Paths and call IDs are Graph input; SDK clients and credentials are not.

ts
export async function interactionGraph() {
  const runtime = createDitto({ sandbox: { tools: ["read_text"], read: true }, workers: [createInteractionWorker({ tools: [readTextTool], output: consoleSink })] });
  const plan = graph<{ path: string; id: string }>("read-and-deliver")
    .node("read", "INTERACTION.ACT.TOOL", [], input => ({ call: { id: input.id, name: "read_text", arguments: { path: input.path } } }))
    .node("observe", "INTERACTION.OBSERVE", ["read"], (_input, { read }) => ({ result: read }))
    .node("output", "INTERACTION.OUTPUT", ["observe"], (input, { observe }) => {
      if (observe.status !== "success") throw new Error(observe.error?.code ?? observe.status);
      return { deliveryId: `${input.id}:delivery`, message: { role: "assistant", content: observe.message.content } };
    });
  const paths = ["README.md", "package.json"];
  try {
    return await runtime.loop(loop({ graph: plan, maxIterations: paths.length,
      bind: (index: number) => ({ path: paths[index]!, id: `read:${index}` }),
      update: index => index + 1, done: index => index === paths.length,
    }), 0);
  } finally { await runtime.close(); }
}

Registering one Worker definition multiple times shares supplied tools, registries, clients, and sinks. Construct separate definitions or use defineWorker resources/dispose for independent resources. unregister only affects future lookups; it neither cancels started operations nor closes SDKs. Drain runtime.close() before closing application-owned clients.

See the example guide for command, tool, and MCP composition. Database capabilities belong in MEMORY. Model action loops are documented in ReAct Graph.

Compose with CONTEXT ​

Observe tool results before updating CONTEXT through ingress. Cached Graphs pass scope to UPDATE; runToolCallFlow/runMcpFlow use explicit Context. Complete CONTEXT API and examples。

ts
export async function toolToCachedContext(
  runtime: import("@codesoul-co/ditto").RuntimeClient,
  scope: import("@codesoul-co/ditto/worker/context").ContextScope,
  call: import("@codesoul-co/ditto/contracts").ToolCall,
) {
  const result = await runtime.invoke("INTERACTION.ACT.TOOL", { call });
  const observation = await runtime.invoke("INTERACTION.OBSERVE", { result });
  // The application chooses whether failed observations should enter its working set.
  if (observation.status !== "success") throw new Error(observation.error?.code ?? observation.status);
  return runtime.invoke("CONTEXT.UPDATE", { scope, ingress: [{
    id: `observation:${call.id}`, sourceNode: "INTERACTION.OBSERVE", content: observation.message.content,
    metadata: { callId: call.id, source: observation.source, status: observation.status },
  }] });
}

Complete imports and source.

Provider cancellation and bounded web responses ​

WebSearchProvider.search(input, options?: WebSearchCallOptions) accepts { signal?: AbortSignal }; the tool supplies WorkerContext.signal. Brave's maxResponseBytes defaults to 1048576 and accepts 1–16777216. Streaming byte accounting stops oversized bodies and closes the stream before JSON parsing. timeoutMs defaults to 30000 and accepts 1–2147483647. Tool errors are sanitized: WEB_SEARCH_CANCELLED for cancellation, WEB_SEARCH_FAILED otherwise. Credentials and network permissions remain explicit.

ts
import { createBraveWebSearchProvider, createReadOnlyCommandTools, createWebSearchTool } from "@codesoul-co/ditto/worker/interaction";
import { loadRuntimeConfigFile } from "@codesoul-co/ditto";
const config = loadRuntimeConfigFile("ditto.yaml", process.env);
const provider = createBraveWebSearchProvider({
  apiKey: process.env.DITTO_WORKER_INTERACTION_BRAVE_SEARCH_API_KEY!,
  ...config.interaction.webSearch,
});
const tools = [...createReadOnlyCommandTools(config.interaction.commands), createWebSearchTool({ provider })];
// Pass tools to createInteractionWorker and explicitly configure Sandbox permissions/executor.

McpClient.listTools(params?, options?: McpCallOptions) and callTool(params, options?: McpCallOptions) accept signal; McpRegistry.execute accepts it as the third argument. Discovery checks cancellation around every page. When adapting the neutral port to the official MCP SDK, listTools takes options second, while callTool takes options third: client.callTool(params, undefined, options); the second argument is the result schema. See the real MCP example. Custom RegisteredTool implementations can forward context.signal to Sandbox.run or their SDK.

Use the public createLocalSandboxExecutor factory or replace it with an application SandboxExecutor; see Sandbox API for configuration, permissions and examples. ReAct forwards its signal to sampling, action and observation Graphs, so tools receive cancellation through context.signal.

Complete tool and system workflows: native model tool selection and argument completion, ten application adapters, persistence, recovery and installed-package task tests.

Execution result understanding workflows compose OBSERVE, model interpretation, Context/Memory checkpoints and actual follow-up tools.

Ditto · @codesoul-co/ditto · Node.js 24+