AgentHarness
The AgentHarness executes agent steps, manages context, and coordinates context layers and channels.
Quick Example
import { AgentHarness, any, callModel, loop, until } from '@noetic-tools/core';
import type { ContextData } from '@noetic-tools/core';
import { z } from 'zod';
const searchTool = {
name: 'search',
description: 'Search the web for information',
input: z.object({ query: z.string() }),
output: z.object({ result: z.string() }),
execute: async (args: { query: string }) => ({ result: `Results for: ${args.query}` }),
};
const agent = loop({
id: 'researcher-loop',
steps: [callModel<ContextData, string, string>({
id: 'researcher-llm',
model: 'openai/gpt-4o',
instructions: 'You are a research assistant.',
tools: [searchTool],
})],
until: any(until.noToolCalls(), until.maxSteps(10)),
});
const harness = new AgentHarness({
name: 'researcher',
agentGraph: agent,
params: {},
callModelDefaults: { provider: 'openrouter' }, // reads OPENROUTER_API_KEY from the env
});
await harness.execute('Find recent AI news');
const response = await harness.getAgentResponse();
console.log(response.text);The AgentHarness is the execution engine at the heart of Noetic. It creates contexts, manages per-thread sessions with a message queue, dispatches steps, coordinates channels and context layers, and provides tracing infrastructure.
Calls to execute() enqueue the input on the session identified by options.threadId (a single default thread is used when omitted). Stream accessors are session-scoped — subscribe once and events flow across every turn in that session.
Constructor Options
import { AgentHarness } from '@noetic-tools/core';
const harness = new AgentHarness({
name: 'my-agent',
agentGraph: myStep,
params: { model: 'openai/gpt-4o' },
});| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Agent name (populates config.name). |
agentGraph | Step<TContext, string, string> | undefined | Root step tree executed for every turn submitted via execute(). TContext defaults to ContextData. |
params | TParams | required | Arbitrary key-value parameters (populates config.params). |
contextLayers | ContextLayer[] | undefined | Default context layers applied to every context created via createContext() / execute(). |
durability | boolean | true when environment.storage gives an adapter or store | Whether runs and sessions are checkpointed, recorded to the step ledger, and resumable on a later harness over the same storage. false turns every durability surface off; the adapter still backs context-layer persistence. See Durability. |
hooks | AgentHooks | undefined | Before/after step hooks. |
tools | Tool[] | undefined | Harness-wide tool pool, merged (identity-deduplicated) with tools collected from agentGraph to form every context's unifiedTools. |
paramsSchema | ZodType | undefined | Optional Zod schema to validate params at construction time. |
environment | AgentEnvironmentConfig | in-memory adapters | Execution environment: storage, filesystem, shell, and subprocess surfaces. See Environment below. |
callModelDefaults | LlmProviderConfig | undefined | Default provider configuration for model calls made through this harness. Supports { provider?: 'noetic' | 'openrouter', apiKey?, baseUrl?, cache? }; provider defaults to 'noetic'. apiKey defaults to NOETIC_API_KEY for 'noetic' and OPENROUTER_API_KEY for 'openrouter'. See Adapter Types. |
itemSchemas | ItemSchemaConfig | undefined | Harness-wide item schema extensions and validation strictness: { schemas?: ItemSchemaExtensions, strict?: boolean }. schemas is global by design; tool-contributed toolResults schemas are owner-scoped to each tool's own result items. Mismatches raise NoeticError kind item_schema_mismatch. strict (default true) controls whether unknown extension item types must match a registered schema. |
traceExporter | TraceExporter | NoopExporter | Where to send completed spans. |
defaultDeliveryMode | DeliveryMode | 'next-turn' | Default delivery mode for messages that don't specify one. |
initialCwd | string | process.cwd() | Initial working directory seeded into every root context this harness creates. |
connections | ConnectionEntries | undefined | The live things the agent works with — browsers, devices, apps, chat workspaces, sources — keyed by name, or as a list of named entries ([mcp({ name, … })]). Providers connect on the first execute()/run() or start(). See Connections. |
triggers | ConnectionTrigger[] | undefined | Connection events that start or join a turn. See Triggers. |
verification | Verifier | undefined | The verifier step.verify uses — verifier() from @noetic-tools/verify. See Verification. |
emitItemsOnReplay | boolean | false | When a resumed run replays a finished step, also stream the items that step emitted (emitItems) again as item_appended, so a client that reconnects mid-run sees the progress it missed. Without it, replay puts them back on the log quietly. See Emitting Items Early. |
Environment
The environment option groups the surfaces the agent runs against. Every field is optional and defaults to an in-memory adapter, so a zero-config harness is fully sandboxed.
const harness = new AgentHarness({
name: 'my-agent',
params: {},
environment: {
storage: { adapter, checkpointStore, layerStateStore, stepLedgerRetention },
fs,
shell,
subprocess,
},
});| Field | Type | Default | Description |
|---|---|---|---|
storage.adapter | StorageAdapter | in-memory | Key-value storage backing context-layer persistence and the step-completion ledger. |
storage.checkpointStore | CheckpointStore | a store over storage.adapter | Store used by checkpoint() / restore(). Defaults to one over storage.adapter when an adapter is given, so durable storage alone makes runs resumable on a new harness. The harness durability: false option turns it off; with no adapter and no store, checkpoint/restore are no-ops. See Durability. |
storage.layerStateStore | LayerStateStore | in-memory store | Backing store for context layer state. |
storage.stepLedgerRetention | StepLedgerRetention | { maxEntryBytes: 131072, maxEntries: 1000 } | Bounds on the step-completion ledger that backs step-level resume. Only meaningful when durability is on. A non-positive cap throws NoeticConfigError (STEP_LEDGER_RETENTION_INVALID) at construction. See Durability. |
fs | FsAdapter | in-memory | Filesystem adapter exposed as harness.fs. Context layers, tools, and skill discovery all route through this adapter. Pass createLocalFsAdapter() from @noetic-tools/platform-node for real disk access. |
shell | ShellAdapter | in-memory | Shell adapter exposed as harness.shell. Tools that execute commands route through this adapter. createLocalShellAdapter() runs raw sh -c; pass createLocalShellAdapter({ useRtk: true }) to wrap each command through rtk rewrite for token-efficient output. The @noetic-tools/cli package enables this by default (controlled by shell.useRtk in noetic.config.ts). |
subprocess | SubprocessAdapter | in-memory | Adapter used to dispatch every runCode, spawn, and detachedSpawn. Set to createLocalSubprocessAdapter({storage}) to run children out-of-process with durable handle manifests. See Durability. |
secrets | SecretResolver | undefined | Resolves secret('name') values immediately before a connection call is sent. See Connections. |
artifacts | ArtifactStore | in-memory | Where binary content — screenshots, recordings, verification runs — is stored. Pass createFileArtifactStore({ dir }) from @noetic-tools/platform-node for disk. |
execute()
Enqueues input on the session identified by options.threadId (or the default thread). Returns a Promise<void> that resolves once the message is accepted into the queue — not when the model finishes responding. Use the session-scoped accessors to observe the response.
declare const harness: AgentHarness;
await harness.execute('What is 2+2?');
const response = await harness.getAgentResponse();
console.log(response.text);
// Stream text across all turns in the session
for await (const delta of harness.getTextStream()) {
process.stdout.write(delta);
}
// With options
await harness.execute('Hello', {
threadId: 'thread-1',
resourceId: 'user-1',
});
// Submit a second message while the first turn is still running — it queues
// and runs as a new turn after the current turn completes.
await harness.execute('follow-up', { threadId: 'thread-1' });| Parameter | Type | Description |
|---|---|---|
input | string | Item | Item[] | The input to the agent. |
options | ExecuteOptions | Optional threadId, resourceId, state, contextLayers, connections, deliveryMode, messageId. connections attaches connections for this turn only: connected when it starts, released when it ends (see Attaching). |
Rejects with NoeticConfigError code NO_STEP_CONFIGURED if no agentGraph was provided in the constructor.
Session Accessors
Stream accessors are keyed by threadId (via SessionScope), are safe to call before the first execute(), and stay alive across every turn in the session.
| Method | Returns | Description |
|---|---|---|
getAgentResponse(scope?) | Promise<HarnessResponse> | Resolves once the session drains its queue and returns to idle. items holds final items, the graph's declared custom items included and typed. |
getTextStream(scope?) | AsyncIterable<string> | Text deltas from the model, across all turns. |
getReasoningStream(scope?) | AsyncIterable<string> | Reasoning token deltas (reasoning models). |
getItemStream(scope?) | AsyncIterable<StreamingItem> | Cumulative item snapshots with isComplete flag. Carries the full session log — framework-authored items (turn inputs, tool results, items a step emits) included. A custom item streams each event in progress, then its final item, under one id, typed by the graph's declarations. |
getFullStream(scope?) | AsyncIterable<StreamEvent> | All raw events (SDK + framework) for the session, each custom item event (custom_item_event) included. Narrow one with isCustomItemEvent(event, item). |
abort(scope?) | Promise<DrainReport | undefined> | Cancel the in-flight turn. Queued messages are preserved and trigger a fresh turn once abort completes. Pass drainMs to stop gracefully; it then resolves with the drain's report. |
getStatus(scope?) | HarnessStatus | Snapshot: { kind: 'idle' | 'generating' | 'draining' | 'aborting' }. draining carries the deadline (epoch ms) of a graceful stop. Pass { executionId } instead for a run() execution's ExecutionStatus. |
getQueueSize(scope?) | number | Count of messages queued on the session. |
Delivery Modes
Each message carries a DeliveryMode. Override per-call via options.deliveryMode, or set defaultDeliveryMode on the harness.
| Mode | Behaviour |
|---|---|
next-turn (default) | Queue until the current turn completes, then run as a new turn. |
between-rounds | Steer the running turn: add the message before its next model call. Matches Claude Code's inbox-attachment pattern. |
next-item | Steer the running turn as soon as the model finishes the item it is writing. The stream stops there, finished tool calls run, and the next round sees the message. Matches the Responses API's response.steer. |
interrupt | Abort the in-flight turn, keep the items it finished, place the message at the head of the queue, and restart. |
Steering a running turn
A between-rounds or next-item message is a steer: it joins the turn that is running instead of waiting for the next one. When no turn is running, it starts one like next-turn.
await harness.execute('Fix the failing test.');
// While the agent works:
await harness.execute('Use the staging database, not production.', {
deliveryMode: 'next-item',
});A model call offers queued steers at three points: when the stream finishes an output item (item_done), before each tool call (before_tool_call), and before each round (before_round). By default a next-item steer applies at item_done, and a between-rounds steer at before_round. Applied steers keep their content parts and role, and land in the history in the order the model saw them.
To change when a steer lands, give a context layer an onSteer hook. It can apply a steer sooner (before a tool call, skipping the rest of the round), hold it longer, rewrite it, or drop it. See Custom Layers.
Steers reach model calls on the session's own context. A spawn child takes them only with steers: 'forward'. With durability on, a turn resumed on a new harness still applies the steers it had been sent; see Durability.
HarnessResponse
interface HarnessResponse {
readonly items: ReadonlyArray<Item>;
readonly usage: { inputTokens: number; outputTokens: number; cachedTokens?: number };
readonly cost?: number;
readonly text: string;
readonly lastLayerUsage?: LastLayerUsage;
}StreamEvent
Events have a source discriminant: 'sdk' for OpenResponses SSE events, 'framework' for Noetic lifecycle events. Framework events use the harness config.name as prefix.
type StreamEvent = SdkStreamEvent | FrameworkStreamEvent;Framework events emitted during execution:
| Event | Description |
|---|---|
{name}:turn_started | A session turn began. Data: { turnId, messageIds }. |
{name}:turn_completed | A session turn completed. Data: { turnId, durationMs }. |
{name}:turn_aborted | A session turn was aborted or errored. Data: { turnId, reason }. |
{name}:steer_accepted | A steer was queued while a turn runs. Data: { messageId, turnId, deliveryMode }. |
{name}:steer_pending | A steer is waiting for the round's tool calls to finish. Emitted once per steer. Data: { messageId, waitingOn }. |
{name}:steer_dropped | A layer dropped a steer; no turn will answer it. Data: { messageId, point }. |
{name}:inbox_injected | Steers were added before a model call. Data: { round, count, messageIds }. |
{name}:model_call_steered | A steer stopped the round's stream at an item boundary. Data: { round, itemCount }. |
{name}:step_started | Before each step executes. |
{name}:step_completed | After each step completes. |
{name}:tool_round_started | Before a tool execution round. |
{name}:tool_call_started | Before each tool call. |
{name}:tool_call_completed | After each tool call. |
{name}:tool_round_completed | After all tool calls in a round. |
{name}:model_call_started | Before each provider call. Data: { round, messageCount, toolCount }. |
{name}:model_call_first_event | First SDK event received (time-to-first-token marker). Only emitted when a broadcaster is attached to the context. Data: { round }. |
{name}:model_call_completed | Provider response fully received. Data: { round, itemCount }. |
{name}:model_round_recorded | A round's record landed in the step ledger: once per finished output item while it streams (done: false), then for each round the loop goes on from (done: true). A resume will not make the items in itemIds again. Data: { round, itemIds, done }. See Durability. |
{name}:model_round_replayed | A resumed loop replayed a recorded round instead of asking the model. Data: { round }. |
{name}:model_call_stalled | Stream-idle watchdog fired; the round is about to abort. Data: { round, idleTimeoutMs }. |
{name}:spend_pending | A paid call was left with no settled bill, or restore() found one. Data: { id, kind, model, generationId?, ceiling?, unbounded? }, or { id, unreadable: true, unbounded: true } for a record that no longer reads. See Paid Calls. |
{name}:spend_settled | A pending paid call was settled or written off. Data: { id, cost, writtenOff? }. |
{name}:drain_started | A graceful stop began. Data: { executionId, deadline, inFlight }. |
{name}:drain_finished | A graceful stop ended, just before the cancel. Data: { executionId, reason, durationMs, finished, cutOff }. |
callModel steps support an emit option to control step/tool framework event emission — set false to suppress or pass a filter function. Turn-level events are emitted by the session runner and are not gated by step.emit.
Stream Idle Timeout
Each provider call is guarded by a watchdog that aborts the round if no SSE event arrives for 120 seconds. The watchdog re-arms on every stream event, so slow-but-alive responses are never cut short. On timeout, {name}:model_call_stalled is emitted and the surrounding turn fails with turn_aborted { reason: "model stream idle timeout after <N>ms" }, so upstream providers that quietly drop a connection surface as errors rather than hangs.
run()
Low-level step execution. Use this when you need full control over context creation and step dispatch.
const ctx = harness.createContext({ state: { count: 0 } });
const result = await harness.run(myStep, input, ctx);| Parameter | Type | Description |
|---|---|---|
step | Step<TContext, I, O> or an array of steps | The step to execute. An array runs in sequence, each output the next one's input; its input is the first step's and its output the last step's. |
input | I | Input value for the step. |
ctx | Context | Execution context. |
Before the first run of a root, run() checks what the root's connection handles need against each connection, so a root that needs an operation a connection lacks fails with CONNECTION_UNSATISFIED before anything executes.
When the harness is built with contextLayers + a storage adapter (environment.storage.adapter), run() runs the layers' init() hooks once per context (keyed by ctx.id) before executing — so prior layer state rehydrates from storage, recalls into the model context, and updates persist back. The same memory guarantee execute() gives, without the session/seedSessionHistory ceremony. This also makes parseAndRunWorkflow (which dispatches through run()) memory-aware. The init is idempotent: nested/repeated run() calls never re-init (re-init would clobber accumulated in-memory state).
createContext Options
| Option | Type | Description |
|---|---|---|
parent | Context | Parent context (for spawn). |
items | Item[] | Pre-seed the item log. |
state | unknown | Initial mutable state. |
threadId | string | Conversation thread identifier. |
resourceId | string | Optional resource/tenant identifier. |
contextLayers | ContextLayer[] | Context layers for this context. Overrides harness-level contextLayers if provided. |
Channel Methods
| Method | Description |
|---|---|
send(channel, value, ctx) | Push a value into a channel. |
recv(channel, ctx, opts?) | Wait for a value from a channel. Supports timeout. |
tryRecv(channel, ctx) | Non-blocking read. Returns null if nothing is available. |
getChannelHandle(channel, executionId) | Get a ChannelHandle for an external channel, enabling outside processes to push values in. |
getChannelStream(channel, executionId) | Subscribe to an external channel from outside the execution as an AsyncIterable; ends when the root execution completes. |
Context Methods
The agent harness coordinates context layer lifecycle hooks:
| Method | Description |
|---|---|
initLayers | Initialize layers at the start of an agent run. Loads persisted state from storage. |
recallLayers | Query layers for relevant items given an input string. Returns RecallLayerOutput[]. |
previewRequestItems | Return the Item[] that would be sent on the next turn — accumulated history plus harness-level layer recall outputs assembled via assembleView. Inits layers on a throwaway context so init-bearing layers contribute; safe to call between turns. |
storeLayers | Write new items from an LLM response into layers. |
disposeLayers | Clean up layers at the end of a run. |
RecallLayerOutput
interface RecallLayerOutput {
layerId: string;
items: Item[];
tokenCount: number;
}Connection Methods
| Method | Description |
|---|---|
start() | Connects every connection provider and starts triggers. The first execute() or run() calls it; call it yourself for a harness that only reacts to triggers. Idempotent. |
getConnectionStatus(name?) | Each connection's state (connecting, ready, unavailable, closed), identity, and pool usage; one entry for name, or all. |
artifacts | The artifact store (environment.artifacts, else in memory); artifacts.listRun(executionId) is a run's manifest. |
restore(executionId, { rerun? }) | rerun names connections the restored run may drive again live although they were replaced. It belongs to the returned context and ends when that context's run does. See Durability. |
close() | Shuts down everything the harness holds open: stops triggers, releases every connection and pool lease, and closes every ACP session. Closes everything even when one part throws, then rethrows the first failure. Idempotent. |
Durability Methods
When the harness is constructed with a durable environment.storage.adapter (and, typically, a durable environment.subprocess adapter), checkpoint and restore become real crash-recovery hooks — the checkpoint store defaults to one over the adapter. Without storage every call is a no-op — zero-config harnesses keep in-memory semantics.
| Method | Description |
|---|---|
checkpoint(ctx) | Snapshot execution state (frontier, layer states, cwd, channels, state, usage, and the run's item-log baseline) through the checkpoint store. Fires automatically at run start, after every step, after runAppendPipeline, and when a run finishes. Keyed by ctx.id; idempotent. |
restore(executionId, opts?) | Rebuild a Context from a previously saved snapshot and step ledger, standing where the interrupted run began; run the same step on the same input to resume. Returns null when neither exists. Throws NoeticConfigError with code: 'CHECKPOINT_SCHEMA_MISMATCH' when the persisted schemaVersion is unrecognised. opts is a RestoreContextOptions (parent / rollUpUsage / state / contextLayers) carrying back the host wiring a snapshot cannot serialise — see Restoring a decorated context. |
cancel(ctx, options?) | Cancel a running execution: abort ctx and every live descendant context (parallel paths, spawn children), then run context-layer teardown — onComplete with outcome: 'aborted', then dispose — bottom-up. Layer hooks already running on a context (recall, store, beforeToolCall, afterModelCall, onDecision) finish, up to their timeouts, before that context's teardown starts. A no-op on an already-cancelled context. options is the reason as a string, or { reason?, drainMs? }; drainMs makes it a graceful stop. Resolves with the drain's DrainReport when a drain ran, undefined otherwise. |
Cancellation reaches inside the work in flight: blocked channel recv/send calls reject with cancelled, the current provider stream and ACP agent turn are cut short, and the next step boundary throws cancelled. Tokens and cost already spent stay charged to the context. Beyond those points it is cooperative — a runCode body that ignores ctx.aborted between awaits runs to its next step boundary.
Use ctx.abort(reason) for the same signal without layer teardown (it is synchronous and returns void); use cancel when the harness owns the layer lifecycle. Note that harness.abort(scope?) is a different operation: it cancels the current session turn by thread, not a specific context tree.
Graceful stop
A plain cancel cuts the provider stream at once, and the provider still bills the cut-off round. A host that is shutting down can instead give in-flight model calls time to finish:
process.on('SIGTERM', () => {
void harness.cancel(ctx, { reason: 'SIGTERM', drainMs: 10e3 });
});With drainMs, nothing new starts beneath ctx: steps, model rounds and tool calls wait instead. callModel rounds and decide calls already in flight get up to drainMs to finish, be charged to the context, and let their step record. Then the cancel runs as without drainMs, and the waiting work throws cancelled. Calls still running at the deadline are cut off the same way a plain cancel cuts them: with durable storage their records stay pending and are priced once the provider has billed them (see Paid Calls).
For a session turn, harness.abort({ threadId, drainMs }) does the same, and getStatus() reports { kind: 'draining', turnId, deadline } while it waits. For a run() execution, getStatus({ executionId: ctx.id }) reports { kind: 'draining', deadline }, then aborting until the run returns. A second cancel or abort during the drain cancels at once.
Both resolve with the drain's report once the cancel is done, so a host can account for the drained calls without listening for events:
const report = await harness.cancel(ctx, { drainMs: 10e3 });
for (const call of report?.finished ?? []) {
budget.charge(call.cost ?? 0);
}The report is { executionId, reason, durationMs, finished, cutOff }, the same data {name}:drain_finished carries. reason is settled when nothing was left, deadline, or stopped. A second stop during the drain resolves with the same report; a stop with no drain resolves undefined. Each call is { kind: 'model_round' | 'decide', model, executionId, round?, dispatchId?, generationId?, cost?, usage? }:
- A
finishedcall the provider billed carriescostandusageexactly as charged to its context, even when its step endedcancelledand left nolastStepMeta. That includes a round rejected and asked again (listed under the sameroundas its retry) and a call that failed after it was billed. A round is finished once its response is in, even if its stream is still open at the deadline. A call the provider refused, or whose stream a steer cut before its response, has neither. - A model round carries
dispatchIdwhen the harness keeps paid-call records, andgenerationIdonce its response headers arrived, so eachcutOffcall can be matched to its entry inpendingSpend().
{name}:drain_started lists the calls in flight when the drain begins.
The subprocess adapter is the companion surface: it persists handle manifests for every long-lived child so a restarted host can call harness.subprocess.listLive() + harness.restore(executionId) per live child to rejoin the execution. Full durable-execution model, storage layout, and IPC protocol live on the Durability page.
Tracing
| Method | Description |
|---|---|
createSpan(name, parent) | Create a new tracing span. Links to a parent span when provided. |
AgentConfig
AgentConfig defines the configuration for an agent harness. It is generic over TParams, an arbitrary key-value record that steps and tools access via ctx.harness.config.params.
interface AgentConfig<TParams extends Record<string, unknown> = Record<string, unknown>> {
name: string;
storage?: StorageAdapter;
hooks?: AgentHooks;
params: TParams;
}| Field | Description |
|---|---|
name | Human-readable agent name. |
storage | Storage adapter for context persistence. |
hooks | Lifecycle hooks (see below). |
params | Arbitrary key-value parameters accessible via ctx.harness.config.params. |
AgentHooks
interface AgentHooks {
beforeStep?: (step: Step, ctx: Context) => Promise<void>;
afterStep?: (step: Step, result: unknown, ctx: Context) => Promise<void>;
}| Hook | When it fires | Typical use |
|---|---|---|
beforeStep | Before each step executes. | Logging, injecting state, guard checks. |
afterStep | After each step completes. | Metrics collection, state updates, audit logging. |
const harness = new AgentHarness({
name: 'researcher',
agentGraph: agent,
params: { model: 'openai/gpt-4o' },
hooks: {
async beforeStep(step, ctx) {
console.log(`Starting step ${step.id} (depth ${ctx.depth})`);
},
async afterStep(step, result, ctx) {
console.log(`Step ${step.id} used ${ctx.tokens.total} tokens`);
},
},
});Resuming a session from disk
harness.seedSessionHistory(threadId, items) populates a fresh session's accumulated items before the next execute() call. Pair it with append-only persistence (one Item per line in a JSONL file) to make a chat survive subprocess restarts:
import { harness } from './my-harness';
import { readPriorItems } from './my-store';
const threadId = 'task-T-abc123';
harness.seedSessionHistory(threadId, await readPriorItems(threadId));
await harness.execute('continue from here', { threadId });The Noetic CLI uses this pattern for per-task agent runners — see the "Per-Task IPC for Live Chat" section of specs/08-runtime.md for the end-to-end design.
Subprocess Adapter
The harness always holds a SubprocessAdapter. Every runCode, every spawn, and every harness.detachedSpawn routes through harness.subprocess.spawn(...) — in-process vs out-of-process is a property of the adapter, not the step.
import { AgentHarness } from '@noetic-tools/core';
import { createFileStorage, createLocalSubprocessAdapter } from '@noetic-tools/platform-node';
const subprocess = createLocalSubprocessAdapter({
storage: createFileStorage({ root: `${process.env.HOME}/.noetic/subprocess` }),
});
const harness = new AgentHarness({
name: 'durable-agent',
agentGraph: agent,
params: {},
environment: { subprocess }, // real OS subprocess with durable manifests
});Per-step and per-call overrides let you mix execution backends. A step with subprocess: createInMemorySubprocessAdapter() stays in-process even when the harness default is the local adapter; a detachedSpawn(step, input, ctx, { subprocess: otherAdapter }) overrides both. Resolution order is detachedSpawn-overrides.subprocess ?? step.subprocess ?? harness.subprocess.
Related Pages
- Context & Event Log -- the context object the agent harness creates.
- Channels -- channel send/recv methods on the agent harness.
- Context Layers -- context layers managed by the agent harness.
- Observability -- tracing spans created by
createSpan. - Durability -- checkpoint/restore, adapter durability, and the host-restart flow.