Quickstart
Run your first agent, then learn the pieces. Build from phases, strategies, and tools to a complete harness you can own and extend.
npm install anchor-ai-sdk@0.1.7No sandbox needed. Configure an OpenAI-compatible model, hand it a task, and read the result.
import { chatAgent, llmFromCatalog, runAgent } from "anchor-ai-sdk";
const { llm, compaction } = await llmFromCatalog({
baseUrl: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_KEY,
model: "z-ai/glm-5.3",
});
const result = await runAgent(
{ task: "Explain exponential backoff in plain English" },
{ llm, compaction, agent: chatAgent },
);
console.log(result.summary);Save the file as chat.mjs, then set your provider key and run it. .mjs tells Node to use JavaScript ES modules; .js also works in a project with"type": "module". Model calls may incur cost.
export OPENROUTER_KEY="your-key"
node chat.mjssummaryThe agent’s answerevidenceTool name, status, and metadatausageTokens and known costreasonWhy the run endedllmFromCatalog uses the provider catalog’s pricing and context-window metadata when available. Pass its compaction configuration into the engine, as above. Metadata failures fall back rather than breaking the run.
Want the reasoning behind these APIs? Read the visual guide to Anchor's philosophy— then return here to assemble the pieces.
The quickstart used a ready-made bundle. Now build one yourself. An agent is behavior + capabilities; a harness is the runtime wiring that lets it work with a model, resources, state, and limits. You do not need every optional feature to start.
- 01Phases → strategy
Name the kinds of work, then put their goals into the prompt.
- 02Tools → agent
Implement capabilities and bundle them with the strategy.
- 03Model + machine → run
Supply live dependencies and let the engine execute the loop.
- 04State + controls → harness
Add continuity, delivery, observability, and resource ownership.
phases + systemPrompt → strategystrategy + tools → agenttask + llm + agent + options → runAgent → resultA StrategyPhase is data, a Tool performs an operation, and the engine runs the loop. A Sandbox is an optional execution dependency used by machine tools—not a different kind of agent. Stores, traces, and budgets plug into run options rather than the reusable bundle.
- Reusable
- Phase data, strategy instructions, and tool definitions belong in your agent module.
- Per call
- A task, model client (LLMClient), optional sandbox, and EngineOptions configure this execution.
- Across calls
- A stable thread ID and your storage/worker policies connect executions without making the agent a stateful singleton.
1. Name the kinds of work
A phase needs an id and goal, with optional guidance. Start with the smallest useful vocabulary. This policy assistant only needs lookup and answer; a repository agent might use understand, inspect, validate, and respond.
export const policyPhases = [
{ id: "lookup", goal: "Find the relevant policy with lookup_policy." },
{
id: "answer",
goal: "Give a short answer grounded in the retrieved policy.",
},
];2. Make those phases part of a strategy
The strategy binds phase data to instructions. Assign the array to phases, and render its goals in systemPrompt so the model sees your intended approach. Requirements are checked at startup; they do not force a tool call or verify the answer.
// Strategy consumes the phase data; it does not execute a list of steps.
export const policyStrategy = {
name: "policy",
phases: policyPhases,
requiredTools: ["lookup_policy"],
requiresSandbox: false,
systemPrompt({ context, summary }) {
return [
"You are a store policy assistant working with demo data.",
"Use lookup_policy before making a claim about store policy.",
"If the policy is unknown, say so. Never invent terms or exceptions.",
"Keep the answer short and cite the topic you looked up.",
...policyPhases.map((phase) => `${phase.id}: ${phase.goal}`),
context ? `Authorized application context: ${context}` : "",
summary ? `Earlier thread context: ${summary}` : "",
]
.filter(Boolean)
.join("\n");
},
};phases → progress vocabularyThe engine uses phase IDs and exposes set_phase for a multi-phase strategy by default. Calling it updates the current label and emits phase_started; it does not invoke an executable phase handler.
systemPrompt → model instructionsThe engine calls this with task, application context, and an earlier thread summary. Read the values your strategy needs. Declaring a phase goal is not a substitute for including it in the prompt.
requiredTools / requiresSandbox → startup checksCatch missing dependencies before model execution. These are availability checks, not per-phase permissions or proof the model used the capability.
Phases guide the approach; they are not a fixed pipeline. The model can skip, revisit, or finish from any phase. A one-phase strategy works too. For an enforced approval or release gate, implement host-side policy rather than relying on a phase label.
Next: give this strategy a tool and assemble the agentYou now have policyPhases inside policyStrategy. Give that strategy a real capability, then export the pair as a plain agent object. This continues the same policy assistant in the same file; it uses demo data and needs no sandbox.
- Strategy
- Your system prompt, phases, and startup requirements.
- Phase
- A named kind of work, not a permission or an executable step.
- Tool
- A schema the model sees and a handler you implement.
- Agent
- A reusable name + strategy + tools bundle.
3. Implement the capability
The strategy refers to lookup_policy; a tool gives that name an input schema and executable handler. Its content becomes the model's observation. Here it reads demo policies; in your app it could call an authorized service.
// 01 / TOOL
// Demo data only. Replace this with your authorized data service.
const policies = new Map([
[
"refunds",
"Refunds are available within 30 days with the original receipt.",
],
["shipping", "Standard shipping takes 3–5 business days."],
]);
export const lookupPolicy = defineTool(
{
name: "lookup_policy",
description: "Look up a demo store policy: refunds or shipping.",
parameters: {
type: "object",
properties: { topic: { type: "string" } },
required: ["topic"],
additionalProperties: false,
},
},
async (input) => {
const topic = requireString(input, "topic").trim().toLowerCase();
const policy = policies.get(topic);
return {
ok: policy !== undefined,
content: policy ?? "No policy found. Do not invent one.",
meta: { topic, source: "demo-policies" },
};
},
{ readOnly: true },
);The defineTool and requireString helpers are root exports from anchor-ai-sdk. For stricter schema validation, service tools, and sandbox-backed handlers, see the tool reference.
4. Bundle it, then supply the runtime
agent.strategy holds the behavior; agent.toolsholds the capabilities. Neither boots a sandbox nor calls a model. Your host supplies llm, a task, and per-call options torunAgent. Read Agent → Run below to see the connection.
Export the bundle. It contains no model instance or per-run state.
// 03 / AGENT
// No model or runtime state in the bundle. Reuse it across runs.
export const policyAgent = {
name: "policy-agent",
strategy: policyStrategy,
tools: [lookupPolicy],
};Your host still owns model credentials, authorization, durable storage, and decisions such as funding revival. Anchor supplies the run loop, tool dispatch, intrinsic control tools, and runtime guardrails.
Next: give tools a shared execution environmentChoose a provider and configure its template or image. The result is the same Sandbox interface, passed to the same runAgent call. Your toolchain, credentials, and provider account are still yours to supply.
This is another dependency, not another loop: the host starts a machine, passes it as options.sandbox, and machine tools usectx.sandbox. The examples below reuse codingAgentto isolate provider setup. The final recipereplaces that bundle with an agent assembled from your own phases, strategy, and tools.
Prebuilt Heyo image artifact. Set HEYO_IMAGE and HEYO_IMAGE_STORE to an artifact and store you can access. Heyo configures its image on the provider; it rejects per-sandbox image and workspacePath. Host-built dockerfile/git image kinds are also available, with different boot requirements.
npm install anchor-ai-sdk@0.1.7 @heyocomputer/hwsimport { heyo } from "anchor-ai-sdk/sandbox/heyo";
// Configuration fragment; requiredEnv is included in the complete file.
const provider = heyo({
token: requiredEnv("HEYO_TOKEN"),
server: requiredEnv("HEYO_SERVER"),
namespace: requiredEnv("HEYO_NAMESPACE"),
image: requiredEnv("HEYO_IMAGE"),
imageKind: "artifact",
imageStore: requiredEnv("HEYO_IMAGE_STORE"),
sizeClass: "small",
diskSizeGb: 10,
});Set OPENROUTER_KEY, REPO_URL, HEYO_TOKEN, HEYO_SERVER, HEYO_NAMESPACE, HEYO_IMAGE, HEYO_IMAGE_STORE. Use a repository and ref you can access; the downloaded example defaults to main. Private repositories also need spec.git.token.
Save and run the downloaded file with node heyo-agent.mjs after setting its environment variables. .mjs is standard JavaScript using ES modules, not an Anchor requirement. With "type": "module" in your package.json, .js works too. Cloud sandbox and model calls may incur cost.
Provider peers can require a newer host runtime than Anchor's Node 20+ baseline. For example, E2B 2.58.0 (allowed by the install range above) requires Node 22.19+. Check the resolved peer's engines and pin its version in your lockfile; the sandbox's image runtime is separate.
The loop is driven by model responses and tool observations, with phases describing the current approach. The strategy defines the vocabulary; the engine runs model → tools → observations → model. It does not execute the phase array from top to bottom.
import { runAgent } from "anchor-ai-sdk";
import { reviewAgent } from "./repository-harness.mjs";
// reviewAgent is { name, strategy, tools }, assembled below.
// llm and sandbox are supplied by your host, not stored in the agent.
const result = await runAgent({ task: "Investigate the retry behavior" }, {
llm, agent: reviewAgent, sandbox,
});
// Runtime sequence (explanation, not a second loop for you to implement):
// 1. strategy.systemPrompt + thread/context + task -> model request
// 2. Model chooses tool calls -> matching tool.run(input, { sandbox, ... })
// 3. Tool content -> observations in the next model request
// 4. set_phase changes the approach label; it does not run a phase function
// 5. A final response/finish, stop condition, or budget ends this run
console.log(result.reason, result.summary, result.usage);See phases → strategy for the definition and the assembled harness for the full call. startPhase chooses a starting label; pauseAfterPhase can pause when the agent leaves a named phase. Neither makes earlier phases mandatory.
Bound the work, not the reusable agent
These are defaults, not hard-coded policy. Override budgets per call (one run, or a shared inbox chain). Dollar checks require usable pricing; step and token limits still apply without it. Usage is checked between model turns, so a call can overshoot a run budget. The output cap is a separate per-call request limit.
// Inside runAgent(task, options):
budgets: { maxSteps: 60, maxTokens: 3_000_000, maxUsd: 3 },
sampling: { temperature: 0, seed: 42 },Sampling is optional; an unset field uses the provider’s default. set_phase keeps the phase label current. Use pauseAfterPhase and startPhase for controlled pauses and resume.
Now apply the same recipe to a repository reviewer: phase data → strategy → a curated tool set → an agent. The host adds a configured provider, model client, stores, controls, and traces. This example builds its own agent rather than hiding the composition behind codingAgent.
reviewPhases → reviewStrategy → reviewAgentreviewAgent + model + started sandbox + state → reviewRepository// 04 / HARNESS — the host owns resources; runAgent owns the model/tool loop.
export async function reviewRepository({
llm, compaction, provider, repoUrl, ref = "main", task, context,
threadId, threadStore, inboxStore, traceSinks = [], signal, onEvent,
}) {
const sandbox = provider.create({ repoUrl, ref });
try {
await sandbox.start();
const result = await runAgent({ task, context }, {
llm, compaction, agent: reviewAgent, sandbox, signal,
budgets: { maxSteps: 40, maxTokens: 3_000_000, maxUsd: 2 },
guardrails: { repeatedToolCallLimit: 3, toolOnlyNudgeLimit: 8 },
thread: { store: threadStore, id: threadId },
inbox: { store: inboxStore, channel: threadId },
traceSinks, onEvent,
// Deliberately synchronous: background execution needs retention/resume
// bookkeeping, rather than adding a threshold to a disposable workspace.
});
return {
result,
// A completed loop is not proof of correctness. Host policy decides what
// evidence is sufficient; no publish/deploy operation is in this tool set.
observedChecks: result.evidence.filter(item => item.tool === "run_tests"),
};
} finally {
await sandbox.destroy();
}
}Behavior stays reusablereviewAgent holds phases, a prompt, and read/search/bash/test tools. A different model or SandboxProvider does not require a new agent loop.
Runtime options add capabilitiesThreadStore carries conversation; inbox chains new tasks; compaction bounds replay; budgets/guardrails bound execution; trace sinks journal what happened. They are independent inputs, not a giant Agent subclass.
Resource ownership stays explicitThis recipe creates and destroys one workspace per call, reusing it inside an inbox chain. It deliberately leaves background execution off. Cross-call workspace continuity and background jobs require the retained-machine/resume policy in the dedicated guides.
npm install anchor-ai-sdk@0.1.7 @heyocomputer/hws
# Set OPENROUTER_KEY and REPO_URL, plus the HEYO_* values from provider setup.
node repository-harness.mjsUse Heyo setup for credentials and an accessible toolchain image. Provision dependencies needed by the repository before asking the model to run checks. Running this file boots a cloud machine and calls a model, which may incur cost.
The prompt's “do not edit” instruction is not a read-only security boundary: shell and test commands can write files. Host authorization, worker ownership, timeouts, trusted commands, durable store adapters, trace access/retention, and release policy remain application responsibilities. Memory stores here are demo-only. Inbox runs share a budget; top-level evidence covers the final run, so use your journal for checks across the whole chain.
Grow it one capability at a time
- Incoming messages: authenticate producers, push queue/interject messages, and wake workers after a call ends.
- Long-running commands: add retained-machine bookkeeping before enabling background execution.
- Work beyond one budget: preserve a handoff and let host policy decide whether to fund another call.
- Application decisions: evaluate trusted evidence; a finished loop is not permission to publish.
SDK 0.1.7 includes native InboxStore, MemoryInboxStore, and EngineOptions.inbox. Your producer pushes messages; the SDK checks interjections at safe turn boundaries and drains pending batches between runs. The host does not need its own abort-and-restart loop for inbox delivery.
queueDoes not enter the active run. After the run ends, pending messages can be claimed and combined as the next run's task.interjectRequests a switch at the next engine boundary. The current turn is not spliced mid-tool-call; a new run follows with the claimed batch.One configured budget covers the chain; messages do not refill it. The chain stops on cancellation, pause, errors, or exhausted allowance. It can also end when no batch remains. A stalled run may continue if inbox work is available. A zero-progress safeguard limits repeated pre-model interruptions; there is no public maxRuns option.
Implement a production store
push(message)Return queued or duplicate. Store a stable ID, channel, text, optional context, delivery, and your ordering/state data. Use globally unique IDs; completion/release identify messages by ID, not a channel argument.
hasInterject(channel)Cheap existence check at run boundaries. It does not claim/delete messages. Current engine treats a lookup error as no interjection.
claimAll(channel)Atomically claim the pending batch: interjections newest-first, then queued oldest-first. Claimed items cannot be returned to competing claims until released or recovered through your lease policy.
complete(ids) / release(ids)The engine settles after the consuming run: release on error/cancel, otherwise complete. This is not an assertion that every instruction caused a model action. Retrying released work can repeat already-performed side effects; use application idempotency.
hasPending(channel) · optionalAllows a normal finish with outstanding jobs to skip its wait when inbox work is pending. Without it, normal wait-before-finish behavior remains. Pending work is not a reservation of budget or a guarantee the next run will start.
Production owns authentication/classification, durable inbox and thread stores, atomic claims/lease recovery, per-thread worker ownership, and waking a worker after a call has returned. Settlement/claim errors are not currently guaranteed to surface as run failures; instrument your adapter. MemoryInboxStore is single-process and not crash-durable. The interface alone is not exactly-once delivery.
Context, runtime facts, and continuation
Inbox delivery alone does not supply conversation persistence; pass thread with a stable ID. The SDK reuses the started sandbox within a chain. The current chain reuses run options but does not explicitly set the next run's startPhase from the preceding result; do not assume every phase label automatically carries. result.usage is cumulative; result.runs contains per-run usage deltas. Top-level evidence/messages/steps belong to the final run rather than an aggregate of every run.
origin: "runtime" marks learned background results, which are retained by ThreadStore. origin: "engine" marks transient nudges, filtered from normal cross-run history. Neither is a secrecy flag; the origin marker is not sent to the model provider. Your strategy must consume application context, and your UI must deliberately filter what it displays.
A bad turn needn’t waste a good run. The engine can redirect repeated calls, empty turns, nudge-triggered status replies, and a capped prose turn before giving up.
// Runtime options for a configured llm / agent / optional sandbox:
const result = await runAgent({ task }, {
llm, agent, sandbox,
guardrails: {
repeatedToolCallLimit: 3,
toolOnlyNudgeLimit: 8,
verboseTurnLimit: 8_000,
proseCheckpoint: true,
},
maxOutputTokens: 16_000, // provider request limit, not replay size
maxAssistantChars: 20_000, // replay limit for one assistant reply
truncatedReplyChars: 12_000, // head-only replay after an output cap
});repeatedToolCallLimitDefault 3. Byte-identical name/argument calls are counted. The repeat is skipped, one redirect is allowed, and another loop can stall the run. This is not semantic detection of every unproductive action.
toolOnlyNudgeLimitDefault 8 consecutive tool-call turns without visible text. Injects a one-shot status/action advisory; it is not a requirement to respond every eight turns.
verboseTurnLimitDefault 8,000 visible output tokens. Current source subtracts provider-reported reasoningTokens from completionTokens and clamps at zero. If reasoning usage is absent, it cannot be subtracted. Fires once per run.
proseCheckpointDefault true. With the finish intrinsic available, a prose-only reply following an engine nudge gets one confirmation/correction turn before it is accepted as an answer. Not a gate on every normal prose reply.
maxOutputTokensDefault 16,000, requested from the provider for each main generation. A capped prose turn can continue once when proseCheckpoint is enabled; repeated capped prose can still stall. A cap can interrupt or omit a tool call.
maxAssistantChars / truncatedReplyCharsDefaults 20,000 / 12,000 characters, not tokens. Bound replay of the model’s own text; they are not verbosity nudges or conversation compaction. The stricter truncated-reply limit never raises the main replay limit.
Use positive numbers or false for numeric switches; 0 is invalid. Empty-turn recovery is a fixed one-shot path in the reviewed source, not a public emptyTurnLimit option. Advisory text and retry counts are not individually configurable through this interface. onEvent observes events; it does not replace engine nudges. Put domain instructions in your strategy prompt.
Budget revival warnings use the separate opt-in revival.warnAtPercent. Phase-discipline guidance is controlled with intrinsics.phase, not a guardrail threshold. These mechanisms guide or bound execution; they do not guarantee progress, correctness, or authorization.
Three layers keep a long-running agent oriented without replaying everything it has ever seen.
Notes on disk
Agents can keep notes at .anchor/notes.md. The adapters initially exclude the reserved directory and force-add the notes file to Git checkpoints. Compaction doesn’t rewrite this file; it is not automatically populated.
Conversation across runs
ThreadStore carries the summary, recent messages, and phase when a store is supplied. MemoryThreadStore ships for single-process development; durable storage is yours to provide.
Spill, don’t discard
Oversized tool output is clipped (300 lines / 20k characters by default). The built-in tools attempt a full-output spill to sandbox scratch storage, with a pointer for later reads. Spills are best-effort and limited to 4M characters.
Before a model turn, the engine estimates the conversation size. Above the configured threshold, it asks the same model to summarize older messages and retains a recent tail. This is lossy summarization, not a durable archive of the removed detail.
import { chatAgent, llmFromCatalog, runAgent } from "anchor-ai-sdk";
const { llm, compaction } = await llmFromCatalog({
baseUrl: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_KEY,
model: "z-ai/glm-5.3",
fallbackCompaction: {
maxEstimatedTokens: 60_000,
keepRecentTokens: 15_000,
},
});
// Pass the returned config; the factory does not apply it to runAgent for you.
const result = await runAgent(
{ task: "Explain this topic" },
{ llm, agent: chatAgent, compaction },
);maxEstimatedTokensWhen to attempt compaction. Engine fallback: 80,000 estimated tokens.keepRecentTokensApproximate recent conversation to keep. Engine fallback: 30,000 estimated tokens. Boundaries are adjusted, not exact token cuts.Catalog-derived settings use the advertised context window: approximately 55%, with floor/ceiling rules and a 300k ceiling; the recent tail is 25% of that threshold. For a 128k window the helper returns 70,400 / 17,600. fallbackCompaction is used only when catalog window data is absent. The object passed torunAgent overrides the engine fallback fields; use object spread to tune the returned catalog configuration before passing it.
import { compactionForContextWindow } from "anchor-ai-sdk";
// For a known window, without fetching catalog metadata:
const compaction = compactionForContextWindow(128_000);
// { maxEstimatedTokens: 70400, keepRecentTokens: 17600 }
// Or tune the helper's ceiling/floor, then pass the result to runAgent:
const smaller = compactionForContextWindow(128_000, {
ceiling: 60_000,
floor: 24_000,
});Estimates use characters divided by four, including tool-call text; they are not a model-specific tokenizer. Reserve space for instructions, tools, and output. A threshold is not a guarantee against context overflow, and a usable fold is not always possible.
import { MemoryThreadStore, runAgent } from "anchor-ai-sdk";
const store = new MemoryThreadStore(); // create once; in-memory, not durable
const result = await runAgent(
{ task: "Continue the same task" },
{
llm,
agent,
compaction: { maxEstimatedTokens: 60_000, keepRecentTokens: 15_000 },
thread: { store, id: "investigation-42" },
onEvent(event) {
if (event.type === "compacted") {
console.log(event.removedMessages, event.keptMessages, event.usage);
}
},
},
);The summary is rebuilt into the strategy prompt; the current request is preserved. A supplied ThreadStore saves the summary, remaining messages, and phase. Use durable storage for cross-process continuity and configured trace sinks if you need an earlier journal. Summarization consumes model usage and is charged to the run budget. Disk notes, tool-output spill, and assistant-text elision are separate mechanisms.
When budget runs low, preserve the handoff — not just the failure. Revival is opt-in, and funding stays in your app.
Enable the handoff mechanism in runAgent, then make the continuation decision in your host. The engine warns near budget limits and offers request_revival; it does not fund or schedule another run.
// Inside runAgent(task, options):
revival: { warnAtPercent: 85 },
thread: { store: threadStore, id: "task-42" },
budgets: { maxSteps: 40, maxTokens: 3_000_000, maxUsd: 2 },import {
codingAgent, MemoryThreadStore, ThreadBudget, planRevival, runAgent,
} from "anchor-ai-sdk";
// Your host supplies a configured provider, model and repository.
export async function runWithRevival({ llm, compaction, provider, repoUrl, task }) {
const store = new MemoryThreadStore(); // demo only; not durable
const threadBudget = new ThreadBudget({ maxTurns: 3, maxUsd: 8 });
const runBudget = { maxSteps: 40, maxTokens: 3_000_000, maxUsd: 2 };
const sandbox = provider.create({ repoUrl, ref: "main" });
let startPhase;
try {
await sandbox.start();
for (;;) {
// Leave insufficient funding to a human, rather than assume a free run.
if (threadBudget.exhausted || threadBudget.usdLeft < runBudget.maxUsd) {
return { action: "needs_human", note: "Thread funding is insufficient." };
}
const result = await runAgent(
{ task }, // preserve the original task on every run
{
llm, compaction, agent: codingAgent, sandbox,
thread: { store, id: "task-42" },
budgets: runBudget,
revival: { warnAtPercent: 85 },
...(startPhase ? { startPhase } : {}),
},
);
// A budget ending normally returns an AgentResult; it is not a throw.
threadBudget.record({ usd: result.usage.costUsd });
if (result.usage.costUsd === undefined) {
return { action: "needs_human", note: "Cost is unknown.", result };
}
const plan = planRevival({ result, task, threadBudget });
if (plan.action === "needs_human") {
return { ...plan, result }; // your UI/worker must deliver this note
}
if (plan.action !== "revive") {
return { action: "stopped", result }; // inspect reason and evidence
}
console.log("Continuing:", plan.remaining);
startPhase = plan.startPhase;
// Same sandbox, thread, task and per-run budget; next iteration resumes.
}
} finally {
await sandbox.destroy();
}
}
// With llm / compaction from the model setup and provider from sandbox setup:
// const outcome = await runWithRevival({ llm, compaction, provider, repoUrl, task });
// Do not automatically retry thrown setup/transport exceptions.reviveA budget-limited result has handoff/checkpoint evidence and thread caps remain. The planner returns the task, phase and remaining-work text.needs_humanThe planner sees exhausted thread caps for an otherwise revivable result. Your app delivers the note and decides whether funding or scope changes.noneNo applicable budget ending or handoff evidence. This is not success; inspect result.reason, result.ok and evidence.maxTurns / maxUsdThread-wide recorded spend, supplied by your app. Missing cost defaults to zero in the tracker; the examples deliberately escalate instead.The planner reacts to max_steps/budget_exceeded and a revival request or checkpoint. Requests and retained machines can be absent; do not assume either exists. Run budgets are checked between model calls and may overshoot. ThreadBudget is bookkeeping, not a billing reservation or exactly-once scheduler. A process crash can lose in-memory stores and spend.
With a native inbox, one returned result can cover a chain. Record its cumulative usage once, or sum the per-run deltas in result.runs, not both. Outstanding background jobs returned at a final chain boundary must be persisted and supplied as resumeJobs after restoring the same retained machine. Inbox continuation is separate from funding a budget-exhausted revival.
For cross-process resume, persist conversation/summary/phase in your ThreadStore, thread spend and the completed run ID in your database, and the workspace via retention or a remote checkpoint. Pass the same thread ID and a started/adopted sandbox to the next run. Explicit startPhase selects the label; it does not restore the conversation or repository by itself.
SnapshotFn to use another persistence layer.AgentResult.ok is a runtime outcome, not permission to ship. result.evidence records tool names, status, and metadata. run_tests records its command, exit code, timeout, duration, and a post-run Git tree hash when available.
latestEvidenceForTree → passed / failed / not_verifiedThe helper stages the current tree and compares its hash with the latest run_tests event marked as executed. An inspectable matching hash and successful tool status producepassed. If it can’t inspect the tree, it reportsinspected: false.
Use onEvent for immediate text/status updates and traceSinks for sequenced records. Both can be attached to the same run. The host supplies the configured model, agent, task, and any sandbox used by these snippets.
import { runAgent } from "anchor-ai-sdk";
// llm, agent, optional sandbox, and task come from your host setup.
const result = await runAgent({ task }, {
llm, agent, sandbox,
onEvent(event) {
switch (event.type) {
case "llm_text_delta":
process.stdout.write(event.text);
break;
case "tool_call":
console.error("tool:", event.name, event.callId, event.verb);
break;
case "tool_result":
console.error("result:", event.name, event.ok, event.contentChars);
// event.content and event.meta contain the detailed result.
// Raw events are not redacted. Do not dump secrets into logs.
break;
case "advisory":
console.error(event.kind, event.detail);
break;
case "run_finished":
console.error("finished:", event.reason, event.usage);
break;
}
},
});
// onEvent is synchronous/observational. Async handlers are not awaited.
// Avoid slow blocking I/O here; use traceSinks for serialized persistence.llm_call_finishedFull assistant text for the turn, provider-returned reasoning if present, tool-call arguments, usage, latency, and model/provider identity when supplied. This is not a packet capture of the HTTP exchange or the full input prompt.
tool_call / tool_resultInputs and call ID on the call; model-facing bounded result content and structured meta on the result. Built-in spilled outputs are pointers to sandbox files, not copies of every byte in the journal.
compacted / checkpoint / revival_requestedFold summary and bounded folding preview, checkpoint references/push status, and the model’s optional remaining-work request. Relevant events appear only when that feature/path runs.
onEvent vs traceSinksonEvent receives raw events and is not an async persistence hook. Sinks receive { seq, ts, event } after pattern redaction and are serialized per sink. Redaction is not guaranteed to detect every secret; audit sensitive data before sharing.
background and inbox eventsbackground_started/completed and inbox_interject/delivered are engine events. The current live projector does not emit dedicated frames for those cases. Record them in a custom/full sink or handle selected metadata in onEvent. background_completed does not contain the full background result body; runtime facts live in conversation state.
import { projectConversation } from "anchor-ai-sdk";
// Split a chain journal into run segments before replaying step-scoped content.
const segments = [];
for (const entry of entries) {
if (entry.event.type === "run_started" || segments.length === 0) segments.push([]);
segments.at(-1).push(entry);
}
const replays = segments.map(segment => {
const finished = segment.findLast(entry => entry.event.type === "run_finished");
return projectConversation(segment.map(({ seq, event }) => ({
seq, type: event.type, payload: event,
})), finished?.event.reason ?? "running");
});
// Feed records in stored order. seq/step alone are not chain-wide identifiers.
// Reconnection transport, event storage queries, and client UI are app-owned.The built-in client streams; provider-reported reasoning may be absent. Sink exceptions are isolated, but a never-settling sink can delay flush. Configure sinks before the run if you need a journal. Filesystem storage in a short-lived/serverless host may be ephemeral; use your own durable sink where required.
Heyo currently provides the pollJob/killJob implementation used by background delivery. Any custom sandbox-exec tool can wire the opt-in contract; arbitrary host-side promises and all existing tools do not become backgroundable automatically. Built-in bash is enabled; built-in run_tests remains synchronous in 0.1.7.
import { codingAgent, runAgent } from "anchor-ai-sdk";
// sandbox is a STARTED Heyo sandbox from the provider setup section.
const result = await runAgent({ task: "Run checks and inspect related files" }, {
llm, agent: codingAgent, sandbox,
thread: { store: threadStore, id: "repo-42" },
backgroundAfterSecs: 15,
onEvent(event) {
if (event.type === "background_started") {
console.log("launched", event.jobId, event.tool, event.timeoutSecs);
}
if (event.type === "background_completed") {
console.log("finished", event.jobId, event.exitCode, event.timedOut);
}
},
});
// bash is already background-enabled. A shorter/equal explicit command deadline
// stays synchronous. Detach is an accepted launch, not successful completion.import { codingAgent, runAgent } from "anchor-ai-sdk";
import { projectChecksTool } from "./background-tool.mjs";
const result = await runAgent({ task: "Run project_checks and report its result" }, {
llm, agent: codingAgent, sandbox,
tools: [...codingAgent.tools, projectChecksTool],
backgroundAfterSecs: 15,
});
// Merely adding background: true to a copied tool object is not sufficient.
// defineTool captures its options; the handler must support detach and metadata.Tool permissionDeclare { background: true } in defineTool options. The wrapper exposes that permission as ctx.backgroundAllowed. Its handler must pass backgroundAfterSecs to sandbox.exec and return meta.background, command and timeoutSecs on detach.
Consumer threshold + provider supportSet a positive backgroundAfterSecs in run options; an unset value is synchronous. The provider needs pollJob. Tools/adapters without support stay synchronous or follow their documented behavior; do not rely on Docker/E2B/local detachment in this release.
Detach threshold ≠ deadlineThe command must be permitted to outlive the threshold. Bash uses the explicit deadline or an eligible background lifetime up to 3,600s. A deadline at/below the threshold does not detach. The custom example chooses a 600s hard lifetime; the threshold does not extend it.
Engine-owned deliveryThe dispatcher records handles, the engine polls before model turns and before normal finish, and completed output is injected as a runtime fact. Before a normal finish it waits within job deadline/grace logic unless the optional inbox hasPending indicates waiting work.
Across an inbox chainOutstanding jobs carry automatically into the next run. Interjections take precedence at the boundary, and queued work can avoid a forced pre-finish wait when hasPending is implemented. A shared budget or cancellation may still end the chain with pendingJobs.
Interjecting does not kill a guest command or undo its writes. Detached jobs can mutate the same workspace while later tools run; choose safe workloads and cancel conflicting work explicitly. Heyo marks retention on detach and may defer destroy/parking while jobs remain, so the host must persist handles, wake later work, and enforce a cleanup policy.
Delivered bodies use the normal best-effort truncate/spill path and origin: "runtime", so facts survive in a supplied thread store until later compaction. background_completed reports job/status metadata, not the full output body. Use conversation/runtime state and retained spill files where available; the event journal alone is not a byte-for-byte archive of every background result.
The sandbox layer can be used with agents or directly through SandboxProvider and Sandbox. Provider factories live on subpaths; the engine and built-in machine tools share the same interface. Configure the backend once rather than giving every tool its own vendor integration.
anchor-ai-sdk/sandbox/heyoanchor-ai-sdk/sandbox/e2banchor-ai-sdk/sandbox/dockeranchor-ai-sdk/sandbox/localE2B, Heyo, Docker, and local are implemented adapters. Daytona remains planned and is not an exported package subpath. Optional capabilities differ: E2B and local declare fork support; Heyo and Docker do not. The latest Docker source declares egress control:spec.egress: "none" disconnects networks after startup. The other adapters reject that setting rather than silently running with network access.
SandboxProvider owns create, adopt, release, and probe. create constructs a sandbox; startboots it and prepares the workspace. The common interface covers exec, Git checkpoints, tree comparison, and teardown. Adapters implement command deadlines; declared capabilities and provider behavior determine retention, fork, and remote-checkpoint support.
import { openSandbox } from "anchor-ai-sdk";
// provider is any configured SandboxProvider, from the examples above.
const sandbox = await openSandbox({
provider,
spec: { repoUrl, ref: "main" },
});
try {
const result = await sandbox.exec("git status --short", { timeoutSecs: 30 });
console.log(result.exitCode, result.stdout, result.stderr);
} finally {
await sandbox.destroy();
}retain: true changes teardown into provider-specific retention (for example, E2B pause or Docker stop). Store the handle for a later adopt; your app must set a cleanup policy and eventually call provider.release. Retention and cleanup are not free or automatic across runs. Use openSandboxSession with a store for adoption/fallback bookkeeping.
pollJob support (currently Heyo). In 0.1.7 the engine polls and delivers results during the call and carries outstanding jobs through an inbox chain. After the call returns, your host owns saving pendingJobs, waking another run with resumeJobs, and cleanup.Credentials enter through environment/adapter secret plumbing, not repository files. Keep provider capability checks in your app; don’t assume every backend supports every feature.
The SDK includes one executable conformance suite for the shared contract: exec results, command deadlines, env/cwd handling, Git checkpoints, retain/adopt, and declared optional capabilities. Scenarios run through the public interface rather than vendor internals.
For another backend, implement SandboxProvider and Sandbox, declare only the capabilities it supports, and run this suite against it. Agent code can keep using the common interface; provider-specific boot and lifecycle behavior stays in the adapter.
import { e2b } from "anchor-ai-sdk/sandbox/e2b";
import { runSandboxConformance } from "anchor-ai-sdk/testing";
// In a Vitest test file. This starts real cloud sandboxes and may incur cost.
runSandboxConformance(
e2b({ apiKey: process.env.E2B_API_KEY, template: process.env.E2B_TEMPLATE }),
{
timeoutMs: 240_000,
repo: { repoUrl: "https://github.com/octocat/Hello-World.git" },
},
);The repository configures local and Docker conformance in CI. Heyo and E2B live suites are credential-gated; the presence of a suite does not mean a given account, image, template, or provider version has passed it. Capability flags and optional repository fixtures affect which scenarios run. Conformance checks the public behavior contract, not security isolation or performance.
A tool has a model-visible JSON Schema and a handler you write. Define the allowed inputs, validate them, do the authorized operation, then return text for the model and optional metadata for the journal.
import { defineTool, requireString, optionalNumber, truncate } from "anchor-ai-sdk";
// Your app provides searchCatalog and enforces authorization for this tenant.
export function makeSearchCatalogTool(searchCatalog) {
return defineTool(
{
name: "search_catalog",
description: "Find up to 10 catalog entries by a text query.",
parameters: {
type: "object",
properties: {
query: { type: "string", minLength: 1, description: "What to find" },
limit: { type: "integer", minimum: 1, maximum: 10 },
},
required: ["query"],
additionalProperties: false,
},
},
async (input) => {
// The schema is not a runtime validator: check inputs in the handler.
if (Object.keys(input).some(key => !["query", "limit"].includes(key))) {
throw new Error("Unexpected parameter");
}
const query = requireString(input, "query").trim();
if (input.limit !== undefined && typeof input.limit !== "number") {
throw new Error("limit must be a number");
}
const limit = optionalNumber(input, "limit", 5);
if (!Number.isInteger(limit) || limit < 1 || limit > 10) {
throw new Error("limit must be an integer from 1 to 10");
}
const entries = await searchCatalog({ query, limit });
return {
ok: true,
content: truncate(JSON.stringify(entries), { maxLines: 80, maxChars: 8_000 }),
meta: { count: entries.length, query },
verb: "Searching the catalog",
};
},
{ readOnly: true }, // only if this handler cannot mutate the workspace
);
}defineTool catches thrown handler errors and returns ok: false with error text. It does not enforce the whole JSON Schema. The example validates required strings, number type/range, and unexpected keys itself; use your own schema validator if needed. optionalNumber can coerce values, so the strict type check above is deliberate.
import { chatAgent, codingAgent, runAgent } from "anchor-ai-sdk";
import { makeSearchCatalogTool } from "./catalog-tool.mjs";
// searchCatalog is your authorized application service, not an SDK export.
const searchCatalogTool = makeSearchCatalogTool(searchCatalog);
// A custom bundle, reusing the chat strategy:
const catalogAgent = {
name: "catalog-assistant",
strategy: chatAgent.strategy,
tools: [searchCatalogTool],
};
await runAgent({ task: "Find the relevant catalog entries" }, {
llm, agent: catalogAgent,
});
// Or extend a coding bundle. Explicit tools replace the bundle's list:
// tools: [...codingAgent.tools, searchCatalogTool],
// agent: codingAgent, sandbox,definition.name / description / parametersThe name the model calls, when it should use the capability, and the JSON Schema. Schema hints do not replace server-side authorization or validation.
run(input, ctx)Your async handler. It receives an input object and ToolContext, including an optional sandbox. Custom API tools need not use a machine. ToolContext does not currently supply an AbortSignal; add timeout/cancellation in your own service.
ok / content / metaok is the handler’s operation status. content is the text sent to the model, already bounded by your tool. meta is structured trace/evidence data, not a model reply. The engine does not universally truncate arbitrary custom-tool output for you.
verb / readOnlyA display label, and your assertion that the tool does not mutate the workspace. The engine may dispatch a fully read-only tool-call batch concurrently; flags do not prove handlers are safe.
Keep credentials in the host/service or sandbox environment, not model-visible schemas or tool results. The built-in finish and multi-phase set_phase tools are added by the engine unless disabled; do not add duplicates to the capability array. Tool schemas do not themselves enforce phase policy.
chatAgent and codingAgent are bundles, not modes. Compose your own strategy, prompts, and tools from the same primitives.
bashBuilds, scripts, installs, shell accessread_fileLine-numbered, paged file readsedit_fileUnique exact-match edits, write-size checkedsearchrg → git grep → grep fallbackrun_testsStructured test results and tree hashesfinishDeclared completion + optional handoffdefineTool pairs a JSON Schema definition with an implementation. Tool content goes to the model; meta stays structured. A consumer-provided handoff.schema shapes finish output — the engine carries the object without interpreting its business meaning. The schema is declared to the model; validate the object against your own schema and business rules in the consuming app.
The SDK includes engine, tool, and trace tests against fakes and fixtures, provider conformance suites, and genericity guards. Local conformance runs in CI; remote live tests are credential-gated.
Explore the test suiteAnchor is 0.x and MIT-licensed. Pin versions, read the source, and treat feature availability as version-specific. Agent teams, consumer skill bundles, and Daytona are roadmap work. These examples target the published 0.1.7 package and source193963a. Native inbox and background examples are checked against that installed package, not just an export map. Source inspection and offline tests are not a claim that every provider deployment has been certified or that all integrations will work unchanged.