@agent-assistant/harness
v0.4.35
Published
Bounded iterative assistant-turn runtime for Agent Assistant SDK
Downloads
12,004
Readme
@agent-assistant/harness
@agent-assistant/harness is the bounded runtime for one assistant turn with iterative model/tool/model execution, truthful stop semantics, structured continuation payloads, and trace hooks.
It exists to fill the gap between a thin one-shot assistant runtime and an unbounded autonomous agent framework.
What It Owns
- bounded execution for a single assistant turn
- iterative model/tool/model loop
- truthful outcomes:
completed,needs_clarification,awaiting_approval,deferred,failed - explicit stop reasons
- compact continuation payloads for clarification, approval, and deferred resume
- trace/telemetry lifecycle events
- adapter seams for model, tools, approvals, and trace sinks
What It Does Not Own
- assistant lifecycle or runtime registration (
@agent-assistant/core) - sessions persistence (
@agent-assistant/sessions) - memory storage or retrieval (
@agent-assistant/memory) - routing policy ownership (
@agent-assistant/routing) - approvals policy ownership (
@agent-assistant/policy) - coordination/workflow engines (
@agent-assistant/coordination) - workforce persona definitions
Installation
npm install @agent-assistant/harnessWorkspace VFS Tools
createWorkspaceToolRegistry exposes provider-neutral workspace_search, workspace_list, workspace_read, and workspace_read_json tools for assistants that have a VfsProvider from @agent-assistant/vfs. The tools emit VFS paths, including sourcePath-style structured outputs for JSON reads, so callers can cite file paths in user-visible replies instead of answering workspace questions from memory.
Public GitHub Tools
GitHubPublicFetcher supports bounded read-only inspection of a public GitHub repo: metadata, README/package excerpts, directory listing, focused file reads, recent commits, and authenticated code search. The default fetch fallback reads globalThis.fetch at call time so Cloudflare Workers consumers can safely stub or rely on the platform fetch binding.
Use createPublicGitHubToolRegistry({ owner, repo }) when an assistant has already resolved one explicitly named public repo and needs deeper source navigation through public_repo_metadata, public_repo_list_tree, public_repo_read_file, public_repo_recent_commits, and optionally public_repo_search_code.
Quick Example
import { createHarness } from '@agent-assistant/harness';
const harness = createHarness({
model: {
async nextStep(input) {
if (input.toolCallCount === 0) {
return {
type: 'tool_request',
calls: [
{
id: 'call-1',
name: 'lookup_weather',
input: { city: 'Oslo' },
},
],
};
}
return {
type: 'final_answer',
text: 'It looks chilly in Oslo today.',
};
},
},
tools: {
async listAvailable() {
return [{ name: 'lookup_weather', description: 'Get current weather' }];
},
async execute(call) {
return {
callId: call.id,
toolName: call.name,
status: 'success',
output: '{"temperatureC":8}',
};
},
},
});
const result = await harness.runTurn({
assistantId: 'sage',
turnId: 'turn-123',
sessionId: 'session-123',
message: {
id: 'msg-1',
text: 'What is the weather in Oslo?',
receivedAt: new Date().toISOString(),
},
instructions: {
systemPrompt: 'You are Sage. Be concise and truthful.',
},
});OpenRouter execution adapter
The harness package now also exposes a bounded hosted execution adapter for OpenRouter-backed turns.
What it is
The OpenRouterExecutionAdapter is a direct hosted backend behind the existing ExecutionAdapter seam.
This means Agent Assistant still owns:
- assistant identity
- turn-context assembly
- policy
- continuation semantics
- Relay-native collaboration
The adapter only owns:
- request translation
- backend invocation
- output normalization
- truthful capability/degradation reporting
Current scope
This adapter is intentionally narrow in the current slice:
- backend id:
openrouter-api - direct hosted API execution
- no-tool turns only
- minimal trace facts
- truthful
unsupported/ degraded negotiation
Current non-goals
This adapter does not currently support:
- tool-bearing execution
- structured tool calls
- attachments
- structured continuation support
- approval interrupts
Example
import {
OpenRouterExecutionAdapter,
type ExecutionRequest,
} from '@agent-assistant/harness';
const adapter = new OpenRouterExecutionAdapter({
apiKey: process.env.OPENROUTER_API_KEY,
model: 'openai/gpt-5-mini',
});
const request: ExecutionRequest = {
assistantId: 'sage',
turnId: 'turn-456',
message: {
id: 'msg-2',
text: 'Summarize the current PR status.',
receivedAt: new Date().toISOString(),
},
instructions: {
systemPrompt: 'You are Sage. Be concise and truthful.',
developerPrompt: 'Do not invent missing GitHub state.',
},
context: {
blocks: [
{
id: 'ctx-1',
label: 'Scope',
text: 'Only summarize what is present in the supplied context.',
},
],
},
};
const negotiation = adapter.negotiate(request);
if (!negotiation.supported) {
throw new Error(negotiation.reasons.map((reason) => reason.message).join(' '));
}
const result = await adapter.execute(request);Honest usage guidance
Use this adapter when you want:
- a hosted API backend
- one bounded no-tool turn
- normalized
ExecutionResultoutput through the same execution seam as other backends
Do not treat it as a replacement for:
- the local CLI harness BYOH path
- Relay-native collaboration
- future tool-capable hosted execution work
Local command execution adapter
The harness package also exposes a reusable LocalCommandExecutionAdapter for BYOH/local
CLI execution. It is product-neutral: products keep assistant identity, policy, memory, and
turn-context assembly, while the adapter owns only local process invocation and result
normalization.
Use it when a local harness can be represented as:
- a command to spawn
- an argv builder from
ExecutionRequest - an output parser into normalized assistant output
- declared
ExecutionCapabilities
import {
LocalCommandExecutionAdapter,
type ExecutionRequest,
} from '@agent-assistant/harness';
const adapter = new LocalCommandExecutionAdapter({
backendId: 'my-local-harness',
command: 'my-harness',
capabilities: {
toolUse: 'adapter-mediated',
structuredToolCalls: true,
continuationSupport: 'none',
approvalInterrupts: 'none',
traceDepth: 'minimal',
attachments: false,
},
buildArgs(request: ExecutionRequest) {
return ['--json', '--prompt', request.message.text];
},
parseOutput(stdout) {
const parsed = JSON.parse(stdout) as { text?: string };
return parsed.text ? { text: parsed.text } : null;
},
});ClaudeCodeExecutionAdapter is now a preset over this same local-command primitive. That keeps
Claude Code support intact while allowing other local harnesses to use the same
ExecutionAdapter contract.
Agent Relay execution adapter
For long-lived local harnesses, AgentRelayExecutionAdapter uses Agent Relay as the local
control plane. It publishes a typed execution request to a Relay worker and waits for a typed
execution result on the same thread.
import {
AgentRelayExecutionAdapter,
} from '@agent-assistant/harness';
const adapter = new AgentRelayExecutionAdapter({
cwd: process.cwd(),
channelId: 'nightcto-local-byoh',
workerName: 'nightcto-local-harness',
orchestratorName: 'nightcto-local-chat',
});The request message body is JSON with type agent-assistant.execution-request.v1.
Workers reply with JSON type agent-assistant.execution-result.v1 and an executionResult
that follows the normal ExecutionResult contract. Successful turns should use
executionResult.status: "completed" and put the assistant answer in
executionResult.output.text. The adapter also tolerates common local-worker shorthands
such as status: "ok" plus answer and normalizes them into the same ExecutionResult
shape. This keeps Claude Code, Codex, OpenCode, or a custom local worker behind Relay
instead of baking provider-specific CLI argv into the product.
spawnWorker vs worker-bridge
AgentRelayExecutionAdapter's spawnWorker.enabled spawns a bare CLI as a
Relay agent and expects it to respond protocol-compliant messages on its own.
That only works if the spawned CLI has MCP tools wired to send typed Relay
messages — not the default for most CLIs. Leaving spawnWorker.enabled
false and running a worker-bridge process separately (see next section)
is the recommended shape; the bridge does the protocol translation so the
CLI is just a prompt-in, text-out subprocess.
Worker-bridge (@agent-assistant/harness/worker-bridge)
Dual of the adapter. Listens for agent-assistant.execution-request.v1
messages on a Relay channel, invokes a non-interactive CLI session
(claude -p, codex exec, opencode run, gemini -p, or a bash stub for
testing), captures stdout, and replies with a correctly-shaped
agent-assistant.execution-result.v1 message.
import { RelayAdapter } from '@agent-relay/sdk';
import {
createClaudeCliRunner,
createRelayWorkerBridge,
} from '@agent-assistant/harness/worker-bridge';
const relay = new RelayAdapter({ cwd: process.cwd(), channels: ['specialists'] });
await relay.start();
const bridge = await createRelayWorkerBridge({
relay,
channelId: 'specialists',
workerName: 'specialist-worker',
runner: createClaudeCliRunner(),
timeoutMs: 120_000,
});
// ... later
bridge.dispose();
await relay.shutdown();Runners provided: createClaudeCliRunner, createCodexCliRunner,
createOpenCodeCliRunner, createGeminiCliRunner, and
createBashCliRunner (for token-free testing). Each takes an optional
{ command, spawnFn } config for overriding the executable or injecting a
mock child_process.spawn.
CliRunner is a minimal interface ({ id, run(input) }) so you can plug in
your own CLI or embedded function.
The adapter + bridge are intentionally separate processes in production:
the adapter lives wherever your orchestrator runs, the bridge lives on the
machine/container that should actually invoke the CLI. Cross-process broker
discovery happens via {cwd}/.agent-relay/connection.json.
packages/webhook-runtime/examples/byoh-worker.ts is a thin wrapper over
this API that exposes --cli, --channel, --worker-name, --model, and
--timeout-ms flags for the two-terminal local-experimentation flow.
Public API
import {
AgentRelayExecutionAdapter,
HarnessConfigError,
LocalCommandExecutionAdapter,
OpenRouterExecutionAdapter,
createAgentRelayExecutionAdapter,
createHarness,
createLocalCommandAdapter,
createOpenRouterAdapter,
type ExecutionAdapter,
type ExecutionRequest,
type ExecutionResult,
type HarnessConfig,
type HarnessContinuation,
type HarnessModelAdapter,
type HarnessResult,
type HarnessRuntime,
type HarnessToolRegistry,
type HarnessTraceSink,
type HarnessTurnInput,
} from '@agent-assistant/harness';Runtime Behavior Notes
- v1 tool execution is sequential by default
- approvals are an adapter seam; the package does not own policy
- bounded stops return
deferredresults with continuation payloads - invalid model outputs are tolerated only within configured limits
- runtime/model/tool failures are surfaced as structured results rather than hidden behind fake completion
Development
From the package directory:
npm test
npm run buildCurrent package validation includes a non-trivial test suite covering:
- final answer execution
- sequential tool loops
- clarification and approval continuations
- invalid output recovery and failure bounds
- retryable vs unrecoverable tool errors
- deferred outcomes for iteration and budget ceilings
- runtime error surfacing
- focused OpenRouter adapter coverage for bounded no-tool hosted execution
HARNESS_PACKAGE_IMPLEMENTED
