TechFabricTechFabricHarness
Building Agents

Sessions and Prompts

How agent.session() and session.prompt() work, and what the harness loop does between the model and your tools.

A session is a persisted message and context thread, and inside one 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, and the framework will ask the model for a typed object and validate it against the schema before you see it.

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/sdk';

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. TechFabric 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, and 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.

Filesystem aliases follow familiar sandbox conventions: 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.