FabricFabricHarness
Building Agents

Sessions and Prompts

agent.session(), session.prompt(), and the harness loop.

A session is a persisted message/context thread. Inside a session, you can run prompts, skills, tasks, and shell commands.

Create a session

const fabricAgent = await init();
const session = await fabricAgent.session();           // new id
const resumed  = await fabricAgent.session('s-001');   // resume by id
const scoped   = await fabricAgent.session('s-002', {
  role: 'engineer',
  model: 'openai/gpt-5.5',
  cwd: 'project',
});

session.prompt(text, options?)

Run one harness loop turn — a single user prompt that may produce assistant messages, tool calls, and shell commands until the model returns a final answer.

const answer = await session.prompt('What is Temporal?');

Typed results

Use result to validate and type the return value. The framework asks the model for a typed object and validates it against the schema.

schema is also exported from @fabric-harness/sdk — same API in both.

import { schema } from '@fabric-harness/sdk';

const triage = await session.prompt('Triage this issue', {
  result: schema.object({
    severity: schema.enum(['low', 'medium', 'high', 'critical']),
    summary: schema.string(),
    recommendedLabels: schema.array(schema.string()),
  }),
});

Streaming

for await (const event of session.stream('Tell me about Temporal')) {
  if (event.type === 'text_delta') process.stdout.write(event.text);
}

Working directories

Set cwd at agent, session, prompt/skill/task, or shell scope. Relative cwd values are resolved inside the sandbox and cannot escape the sandbox workspace.

const fabricAgent = await init({ cwd: 'project' });
const session = await fabricAgent.session();

await session.shell('npm install');          // runs in /workspace/project
await session.shell('npm test', { cwd: 'ui' }); // runs in /workspace/project/ui

await session.prompt('Inspect the UI package', { cwd: 'ui' });
await session.skill('review', { cwd: 'api', args: { focus: 'routes' } });
await session.task('Refactor tests', { cwd: 'packages/core' });

File tools (read, write, grep, glob) use the same scoped cwd during prompts/skills/tasks, so coding agents can work in a repository subdirectory without repeating absolute paths.

Tools and commands

import { defineCommand } from '@fabric-harness/node';

const npm = defineCommand('npm');
const git = defineCommand('git');

await session.prompt('Run the failing tests and propose a fix', {
  commands: [npm, git],
  // tools: optional override of built-in tools
});

See Tools and Commands.

Reasoning streams

Reasoning-capable models (Anthropic Claude with extended thinking, OpenAI o-series, Gemini 2.5 with thought summaries) emit a separate "thinking" content channel. Fabric Harness surfaces it on ModelResponse.thinking and emits a text_delta event with kind: 'thinking' so streaming consumers can render it apart from the final answer.

const stream = session.stream('Plan the migration in detail.');
for await (const event of stream) {
  if (event.type === 'text_delta' && event.data?.kind === 'thinking') {
    renderThinking(event.data.delta);   // grey/italic in your UI
  } else if (event.type === 'text_delta') {
    renderOutput(event.data?.delta);    // normal model output
  }
}

Provider notes:

  • Anthropic: thinking is emitted when the request includes extended_thinking: true (set via headers on AnthropicModelProvider).
  • OpenAI o-series: emitted automatically as reasoning_content on the chat completion message.
  • Gemini 2.5: emitted when thinking_config is set on the request; consumed via the same thinking field.

Other providers leave ModelResponse.thinking undefined; consumers should fall back to text_delta without the thinking kind.

Token-level streaming

OpenAICompatibleModelProvider and AnthropicModelProvider both expose a stream() method that returns an AsyncIterable<ModelStreamChunk>. The loop uses it automatically when present, emitting text_delta events as tokens arrive (instead of buffering until the response completes). UIs see word-by-word output; per-call cost telemetry still lands on the final aggregated chunk.

Consumers don't need code changes — session.stream() already understands text_delta:

for await (const event of session.stream(prompt)) {
  if (event.type === 'text_delta') process.stdout.write(event.data?.delta ?? '');
}

Mid-stream failures fall through to the loop's normal error path. Retries are NOT automatic for streamed calls — partial state can't be safely re-applied.

session.skill(name, options?)

Invoke a Markdown skill from .fabricharness/skills/<name>/SKILL.md. Args interpolate into the skill body, and result validates the typed return value.

const triage = await session.skill('triage-issue', {
  args: { issueNumber: 42, repository: 'octocat/repo' },
  commands: [gh],
  result: schema.object({
    severity: schema.enum(['low', 'medium', 'high', 'critical']),
  }),
});

session.task(text, options?)

Spawn a child task. Tasks are durable when the session runs on the Temporal worker target.

const result = await session.task('Refactor the authentication middleware', {
  id: 'refactor-auth',
  result: schema.object({ filesChanged: schema.array(schema.string()) }),
});

session.shell(command, options?)

Execute a shell command in the session's sandbox.

const out = await session.shell('npm test', { cwd: '/workspace' });

session.fs and agent.fs

Use session.fs for host-side filesystem plumbing that should not appear in model history: staging input files, collecting scratch output, or checking whether generated artifacts exist. It uses the same sandbox backend and cwd as the session.

const fabric = await init({ sandbox: 'virtual' });
const session = await fabric.session('build-123');

await session.fs.writeText('input/ticket.md', ticketBody);
const exists = await session.fs.exists('input/ticket.md');
const files = await session.fs.list('input');
const text = await session.fs.readText('input/ticket.md');

agent.fs is also available for quick setup scripts. It is backed by a lazily-created default session; prefer session.fs when you need explicit session identity, audit correlation, or lifecycle control.

Aliases are provided for Flue parity and sandbox familiarity: readFile, readFileBuffer, writeFile, readdir, and rm.

Session history

const history = await session.history();
// { id, createdAt, updatedAt, entries, events }

The CLI's fh inspect, fh logs, and fh metrics are wrappers around this same data.