> ## Documentation Index
> Fetch the complete documentation index at: https://obversa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Safe Node Attempts

> Run one bounded engine turn and record what was asked, what ran, what came back and what it cost.

Run one piece of work as one bounded engine turn, and get an honest record
of it: what was requested, what ran, what came back, and whether token
usage was reported or unknown. Use this page when you bind engines to
nodes yourself or write an engine plugin. For where these records live,
read [Events and artifacts](/docs/recording/events-and-artifacts); for what a
resumed run repeats, [The record](/docs/concepts/record).

An engine-backed node attempt is one fresh engine call. Command adapters
use one fresh CLI process, and data-only attempts use no engine process.
The graph executor connects attempts to graph dispatch. The request names
the work and its bounds:

```ts examples/safe-node-attempt.ts (excerpt) {11-14,16-17} theme={null}
function request(
  directory: string,
  model: string,
  jsonSchema: JsonValue,
): AgentRequest {
  return {
    prompt: 'Return the structured answer.',
    system: 'Follow the result contract.',
    model,
    jsonSchema,
    tools: [],
    allowedTools: [],
    cwd: directory,
    workspaceMode: 'none',
    leaf: true,
    timeoutMs: 5_000,
    timeoutGraceMs: 200,
    maxOutputBytes: 64 * 1_024,
    maxMemoryBytes: 256 * 1_024 * 1_024,
  };
}
```

A request needs:

* **`prompt` and `model`**, what to ask and which model to ask.
* **`tools`, `allowedTools` and `workspaceMode`**, the access the process
  may have.
* **`timeoutMs`, `maxOutputBytes` and `maxMemoryBytes`**, the bounds one
  turn runs inside.

## Run an adapter

Each adapter runs one attempt and returns a result the public validator
checks before its parts are used:

```ts examples/safe-node-attempt.ts (excerpt) {1,4,12,15} theme={null}
  const grok = validateAgentResult(await new GrokCliEngine({
    executable: grokExecutable,
    version: '1.0.44',
    identity: { provider: 'xai', modelFamily: 'grok-4' },
    permissionMode: 'dontAsk',
  }).run(
    request(directory, 'grok-4-example', RESULT_SCHEMA),
    () => {},
    new AbortController().signal,
  ));

  const opencode = validateAgentResult(await new OpenCodeCliEngine({
    executable: openCodeExecutable,
    version: '1.18.23',
    identity: { provider: 'opencode', modelFamily: null },
  }).run(
    request(directory, 'opencode/big-pickle', RESULT_SCHEMA),
    () => {},
    new AbortController().signal,
  ));
```

The example runs the Grok and OpenCode adapters against local scripted
executables, with no model and no network. Grok returns a native structured
result; OpenCode returns one marked text part, which the job's own parser
reads. OpenCode's fixture leaves out a valid usage receipt on purpose, so
the runtime keeps its usage as `unknown` instead of zero. Run it with
`npx tsx safe-node-attempt.ts`:

```json Output theme={null}
{
  "grok": {
    "requested": {
      "adapter": "grok-cli",
      "adapterVersion": "1.0.44",
      "provider": "xai",
      "modelFamily": "grok-4",
      "model": "grok-4-example",
      "executable": "grok-fixture.mjs",
      "capabilities": []
    },
    "effective": {
      "adapter": "grok-cli",
      "adapterVersion": "1.0.44",
      "provider": "xai",
      "modelFamily": "grok-4",
      "model": "grok-4-example",
      "executable": "grok-fixture.mjs",
      "capabilities": []
    },
    "final": {
      "answer": 42
    },
    "usage": "reported"
  },
  "opencode": {
    "requested": {
      "adapter": "opencode-cli",
      "adapterVersion": "1.18.23",
      "provider": "opencode",
      "modelFamily": "big",
      "model": "opencode/big-pickle",
      "executable": "opencode-fixture.mjs",
      "capabilities": []
    },
    "effective": {
      "adapter": "opencode-cli",
      "adapterVersion": "1.18.23",
      "provider": "opencode",
      "modelFamily": "big",
      "model": "opencode/big-pickle",
      "executable": "opencode-fixture.mjs",
      "capabilities": []
    },
    "final": {
      "answer": 42
    },
    "usage": "unknown"
  },
  "temporaryDirectoryRemoved": true
}
```

The report prints the stub executable paths relative to the temporary
directory; the exported `attemptReport` keeps the absolute paths. Both
requested and effective identities are recorded. If a CLI reports a
different model, the two records stay separate and the change stays
visible. Each identity records the adapter, adapter version, provider,
model family, model, capabilities and `executable`: the absolute path a
command adapter selected, or `null` for an engine with no child process.
When the engine runs with an `effort`, each identity also records it.
A host that writes an engine's selection itself names the `effort` in
the selection, the same way it names the model. The runtime passes that
`effort` to the engine with each request, preflight checks included. An
`effort` set on the engine itself still applies to a request that names
none, so an engine built with its own `effort` under a selection that names
a different one, or none, reports a different requested identity, and the
attempt fails. Name the effort in one place: the selection.
The path identifies the selected wrapper, not its resolved target or file
digest. When no Claude or Codex binary is configured, those plugins search
the inherited `PATH` once and record the absolute path they select.

## Declare access

`tools` lists the built-in capabilities the CLI may expose. `allowedTools`
holds the narrower permission rules approved by the host. An adapter that
can't express the declared access refuses the request before it starts the
process, so no model runs.

The workspace mode is a ceiling. An approval never adds a capability the
mode withholds, and a bypass never goes past it. A reviewer with no way to
read the work is refused rather than run, so a review rests only on
material the seat could open. The example uses `workspaceMode: 'none'` and
exposes no tools. An evidence-only job can use `workspaceMode: 'read'` with
declared read tools. A writing job must name its write access and gets a
separate scratch directory for temporary data.

An action decision is made before an effect: `allow` runs it, `wait` pauses
it, and `deny` records a refusal without running it.

## Read results and usage

A successful result has ordered parts and exactly one final part. A stopped
or truncated turn can keep the parts it produced, including no parts at
all, but it's still a failed attempt.

Usage has two states. `reported` means the engine supplied a valid receipt.
`unknown` means it didn't. Unknown usage never becomes a fake zero.

Command adapters set three environment variables before they start a
process, and children inherit them unless they replace their environment:

* **`OBVERSA_HEADLESS=1`** tells hooks and child tools that the attempt is
  unattended, so they must not open interactive or desktop prompts.
* **`OBVERSA_RUN_ID`** identifies the run that owns the process.
* **`OBVERSA_ATTEMPT_ID`** identifies the exact attempt and lets cleanup
  find descendants that detach from their parent process.

## Failure

* **Fallback.** One attempt can use one declared fallback after a model
  becomes unavailable. The failed model stays in the attempt record. Both
  lanes share one clock, so a fallback receives only the time left by the
  first lane. The graph executor uses durable events to keep later graph
  work away from that model.
* **`TIMEOUT`.** `timeoutMs` is the work deadline; at that point a command
  adapter starts stopping its process tree. `timeoutGraceMs` is teardown
  time: the adapter asks the processes to stop, waits that long, then
  force-stops any that remain. The grace doesn't start more model work. A
  marked final result returned inside the deadline stays separate from a
  later transport failure. If cleanup makes the adapter return after the
  deadline, the attempt fails with `TIMEOUT` and its parsed `result` stays
  `null`, while the record keeps the returned parts, reported usage,
  effective engine and transport failure as evidence. The runtime waits up
  to seven more seconds for that cleanup evidence, and the wait can't turn
  the attempt into a success.
* **Cleanup.** The command-adapter kit cleans its process tree on normal
  completion, timeout, abort and other handled exits, and the runtime waits
  for that cleanup before it records the attempt. `temporaryDirectoryRemoved`
  in the output covers only the example's fixture files; the package test
  suite checks child-process cleanup separately.

## Limits

* **Hosts own two things.** Recovery after the supervising process dies,
  and checks against the CLIs installed on the machine. The
  [supervised runner](/docs/driving/runner) does both.

<Accordion title="Full file">
  ```ts examples/safe-node-attempt.ts theme={null}
  import assert from 'node:assert/strict';
  import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
  import { existsSync } from 'node:fs';
  import { tmpdir } from 'node:os';
  import { join, relative } from 'node:path';

  import {
    finalResultPart,
    validateAgentResult,
    type AgentRequest,
    type AgentResultPart,
    type JsonValue,
  } from '@obversa/runtime';
  import { GrokCliEngine } from '@obversa/engine-grok-cli';
  import { OpenCodeCliEngine } from '@obversa/engine-opencode-cli';

  const STRUCTURED_RESULT_MARKER = 'OBVERSA_STRUCTURED_RESULT_V1\n';
  const RESULT_SCHEMA = {
    type: 'object',
    properties: { answer: { type: 'number' } },
    required: ['answer'],
    additionalProperties: false,
  } as const;

  const GROK_FIXTURE = `#!/usr/bin/env node
  if (process.argv.length === 3 && process.argv[2] === '--version') {
    process.stdout.write('grok 1.0.44\\n');
    process.exit(0);
  }
  process.stdout.write(JSON.stringify({
    text: '{"answer":42}',
    stopReason: 'end_turn',
    sessionId: 'example-session',
    requestId: 'example-request',
    usage: {
      input_tokens: 2,
      output_tokens: 5,
      cache_read_input_tokens: 0,
      cache_creation_input_tokens: 0
    },
    modelUsage: {
      'grok-4-example': {
        inputTokens: 2,
        outputTokens: 5,
        cacheReadInputTokens: 0,
        modelCalls: 1
      }
    },
    structuredOutput: { answer: 42 }
  }, null, 2) + '\\n');
  `;

  const OPENCODE_FIXTURE = `#!/usr/bin/env node
  if (process.argv.length === 3 && process.argv[2] === '--version') {
    process.stdout.write('1.18.23\\n');
    process.exit(0);
  }
  for await (const chunk of process.stdin) void chunk;
  const base = {
    timestamp: 1777777777777,
    sessionID: 'example-session'
  };
  const emit = (type, part) => {
    process.stdout.write(JSON.stringify({ ...base, type, part }) + '\\n');
  };
  emit('text', {
    id: 'example-result',
    sessionID: 'example-session',
    messageID: 'example-message',
    type: 'text',
    text: 'OBVERSA_STRUCTURED_RESULT_V1\\n{"answer":42}',
    time: { start: 1, end: 2 }
  });
  emit('step_finish', {
    id: 'example-finish',
    sessionID: 'example-session',
    messageID: 'example-message',
    type: 'step-finish',
    reason: 'stop',
    cost: 0,
    tokens: {}
  });
  `;

  function request(
    directory: string,
    model: string,
    jsonSchema: JsonValue,
  ): AgentRequest {
    return {
      prompt: 'Return the structured answer.',
      system: 'Follow the result contract.',
      model,
      jsonSchema,
      tools: [],
      allowedTools: [],
      cwd: directory,
      workspaceMode: 'none',
      leaf: true,
      timeoutMs: 5_000,
      timeoutGraceMs: 200,
      maxOutputBytes: 64 * 1_024,
      maxMemoryBytes: 256 * 1_024 * 1_024,
    };
  }

  function nativeValue(part: AgentResultPart): JsonValue {
    assert.equal(part.kind, 'structured');
    if (part.kind !== 'structured') throw new TypeError('Expected a structured result');
    return part.value;
  }

  function parsedValue(part: AgentResultPart): JsonValue {
    assert.equal(part.kind, 'assistant');
    if (part.kind !== 'assistant') throw new TypeError('Expected an assistant result');
    assert.ok(part.text.startsWith(STRUCTURED_RESULT_MARKER));
    return JSON.parse(part.text.slice(STRUCTURED_RESULT_MARKER.length)) as JsonValue;
  }

  const directory = await mkdtemp(join(tmpdir(), 'obversa-safe-attempt-'));
  const grokExecutable = join(directory, 'grok-fixture.mjs');
  const openCodeExecutable = join(directory, 'opencode-fixture.mjs');
  let report;

  try {
    await writeFile(grokExecutable, GROK_FIXTURE);
    await writeFile(openCodeExecutable, OPENCODE_FIXTURE);
    await chmod(grokExecutable, 0o700);
    await chmod(openCodeExecutable, 0o700);

    const grok = validateAgentResult(await new GrokCliEngine({
      executable: grokExecutable,
      version: '1.0.44',
      identity: { provider: 'xai', modelFamily: 'grok-4' },
      permissionMode: 'dontAsk',
    }).run(
      request(directory, 'grok-4-example', RESULT_SCHEMA),
      () => {},
      new AbortController().signal,
    ));

    const opencode = validateAgentResult(await new OpenCodeCliEngine({
      executable: openCodeExecutable,
      version: '1.18.23',
      identity: { provider: 'opencode', modelFamily: null },
    }).run(
      request(directory, 'opencode/big-pickle', RESULT_SCHEMA),
      () => {},
      new AbortController().signal,
    ));

    const grokFinal = nativeValue(finalResultPart(grok));
    const openCodeFinal = parsedValue(finalResultPart(opencode));
    assert.deepEqual(grokFinal, { answer: 42 });
    assert.deepEqual(openCodeFinal, { answer: 42 });
    assert.equal(opencode.usage.kind, 'unknown');

    report = {
      grok: {
        requested: grok.requested,
        effective: grok.effective,
        final: grokFinal,
        usage: grok.usage.kind,
      },
      opencode: {
        requested: opencode.requested,
        effective: opencode.effective,
        final: openCodeFinal,
        usage: opencode.usage.kind,
      },
    };
  } finally {
    await rm(directory, { recursive: true, force: true });
  }

  assert.ok(report);
  export const attemptReport = {
    ...report,
    temporaryDirectoryRemoved: !existsSync(directory),
  };
  console.log(JSON.stringify(attemptReport, (key, value: unknown) => (
    key === 'executable' && typeof value === 'string'
      ? relative(directory, value)
      : value
  ), 2));
  ```
</Accordion>

## Next steps

* [Supervised local runs](/docs/driving/runner): the watchdog that checks engines
  before the first step and restarts a killed worker.
* [Events and artifacts](/docs/recording/events-and-artifacts): the stores an
  attempt's events and parts land in.
* [API](/docs/packages/api): the engine contract every adapter implements.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.