Skip to content

Files, tools, and events ​

Files in ​

Seed the workspace before a run starts. Text and binary both work:

ts
await agent.run({
  prompt: 'Read feedback.txt and create report.md.',
  files: [
    { path: 'feedback.txt', text: feedback },
    { path: 'assets/data.bin', bytes: new Uint8Array([...]) },
  ],
  schema,
});

Paths must be relative workspace paths. Runcell rejects absolute paths, .. segments, backslashes, and drive letters before the run starts.

Files out ​

The result returns created and modified files:

ts
const result = await agent.run({ prompt, schema });

for (const file of result.files) {
  file.path; // workspace-relative
  file.change; // 'create' | 'modify'
  file.bytes; // Uint8Array
}

const report = result.files.find(f => f.path === 'report.md');
const text = report ? new TextDecoder().decode(report.bytes) : undefined;

With a caller-owned sandbox you can also just read the workspace directly (sandbox.readTextFile(...)) after (or during) runs.

Host tools ​

Tools are functions the agent can call in your process, such as application lookups and internal APIs that the sandbox cannot reach:

ts
import { z } from 'zod';

const agent = createAgent({
  model,
  tools: {
    lookupCustomer: {
      description: 'Look up customer account details by customer id.',
      schema: z.object({ id: z.string() }),
      execute: ({ id }) => db.customers.find(id), // sync or async
    },
  },
});
  • schema accepts any Standard Schema validator. Runcell validates the input before execute runs, and the schema sets the input type.
  • Runcell serializes the return value back to the model.
  • Reserved names (used by the runtime): read, write, edit, bash, grep, glob, ls, submitResult, fileChange, readPdfPages. Registering one throws at createAgent time.

Built-in PDF tool ​

When a run seeds a PDF and the optional peers pdfjs-dist and @napi-rs/canvas are installed, runcell registers readPdfPages. This tool renders pages as images that the model can view. See Streaming for the limits. You do not need to write your own renderer for this case.

Multi-part and image results ​

Use toolContent() when a host tool needs to return real content blocks to the model, such as a rendered PDF page:

ts
import { createAgent, toolContent } from 'runcell';
import { z } from 'zod';

const agent = createAgent({
  model,
  tools: {
    renderPdfPage: {
      description: 'Render one page of a PDF for visual inspection.',
      schema: z.object({ page: z.number().int().positive() }),
      execute: async ({ page }) => {
        const png: Uint8Array = await renderPage(page);
        return toolContent([
          { type: 'text', text: `Rendered PDF page ${page}:` },
          { type: 'image', data: png, mediaType: 'image/png' },
        ]);
      },
    },
  },
});
Tool return valueWhat the model receives
toolContent([...])Verbatim text parts and real image blocks
A stringThe string verbatim, unchanged from ordinary tool results
Anything elseJSON.stringify text (values that serialize to undefined become 'null'), unchanged from ordinary tool results

toolContent() accepts a non-empty array of text and image parts. Image data can be a Uint8Array or a base64 string. A base64 string must be standard padded canonical base64: no whitespace, no data: URL prefix, no base64url alphabet. The helper base64-encodes raw bytes once. It matches media types case-insensitively and normalizes image/jpg to image/jpeg. Supported types are image/png, image/jpeg, image/gif, and image/webp, with a 5 MB decoded limit per image. Invalid input throws eagerly from toolContent().

The helper is an explicit opt-in. Returning a bare array that looks like content parts still follows the ordinary JSON-stringified path. Text-only toolContent([...]) is also valid. For these results, onToolResult and result tool projections expose the normalized, JSON-safe content array with base64 image data. Runcell does not pre-check whether a model supports vision. A provider rejection surfaces through onError like other model errors.

See the runnable examples/11-image-tool-results.ts with npm run example:11.

Events ​

Lifecycle callbacks support logging, UIs, and metrics. They are optional and best-effort: runcell ignores callback errors, and they do not affect the run. Register callbacks at the agent level for every run or per run via agent.run({ ..., events }). If both are set, both fire.

ts
const agent = createAgent({
  model,
  events: {
    onText: delta => process.stdout.write(delta),
    onToolCall: call => log('tool call', call.name, call.input),
    onToolResult: result => log('tool result', result.name),
    onFileChange: file => log('changed', file.path, file.change),
    onRepair: info => log('repair attempt', info.attempt, info.reason),
    onFinish: info => log('turn finished', info.finishReason),
    onError: error => log('error', error),
  },
});
EventFires when
onTexta text delta streams from the model
onToolCallthe agent invokes one of your tools
onToolResultone of your tools returns
onFileChangethe agent creates/modifies a workspace file
onRepaira repair turn starts (structured runs only)
onFinisha turn completes, with its finish reason
onErrorthe run fails after the session starts. Runcell attaches usage before the callback. The same object rejects the run
onCredentialFallbacka credentials chain moves to its next source after a credential error, with from, to, and cause

Runcell ignores callback exceptions. In particular, an exception from onError does not replace the enriched failure object used to reject the run.

Events fire for run() and stream() alike. For streaming text to a client, prefer agent.stream()'s textStream over onText. See Streaming.