FAQ - joeylking/agent-runtime GitHub Wiki

FAQ

Is this an agent framework? No. It is the loop and the controls around it. It has no prompts, memory, orchestration, or provider layer.

Which model providers does it support? Any, through a two-method interface. Three adapters ship, each a nested module so its dependencies stay out of the core: Ollama, any OpenAI-compatible endpoint, and the Claude API through the official SDK. The runtime also ships scripted, recording, and replaying models. See Provider Adapters.

Can the agent be a framework? Yes. Implement Decide by calling into it and return the chosen tool call as a decision. Or, since v0.4.0, keep the framework's loop and have each action it proposes go through a Gate, which executes the tool itself.

Why does the agent get all prior steps every time? So it is stateless, so a resumed run in another process sees exactly what a continuous one would, and so the audit log and the agent's view are the same data.

Why is a denied request not the end of the run? Because the agent may correct it. The consecutive-failure limit ends the run if denials repeat, and the policy can answer abort when a request shows the run cannot succeed.

What happens if a tool panics or hangs? A panic is recovered and a hang is cancelled at the tool's timeout. Both become failure observations.

Can two processes run the same run? No. Since v0.3.0 a running run is leased to the driver executing it: another process's Resume is refused with ErrRunLeased until the lease is released or expires, and a loop that loses its lease starts no further tool or model call. Status transitions are also compare-and-set, so two processes resuming one approved run execute it once. The lease does not stop a process of an older version, which ignores it. See Run Lease.

After a crash, Resume says the run is leased. Why? The dead process's lease has not expired yet. It lasts Config.LeaseTTL, 30 seconds by default, and a restarted process is not recognized as the same owner, because nothing proves the old one is gone. Retry after the ExpiresAt the error carries, or lower LeaseTTL for faster recovery.

What is an ambiguous model call? One that timed out or lost its connection after dispatch. The provider may have completed it, so it is counted and charged conservatively.

I approved a run and no longer want it. Now what? An approval cannot be un-approved. Call Cancel on the run (or agentrt cancel); it ends as operator_cancelled and the recorded request never executes. To have stale grants expire on their own, set Limits.GrantTTL.

Why are approvals hashed? So that a modified approval row cannot be granted, and so that an approval for one request can never authorize a different one.

Can it use MCP servers? Yes, through the mcp module, which exposes a server's tools as runtime tools. An operator classifies each one, the tool set is pinned by schema and description, and the server's own annotations are never a source of permission. See MCP Tools.

Can I put the controls in front of an MCP host that is not written in Go? Yes, with agentrt-proxy, a local MCP proxy over stdio that the host starts as one of its MCP servers and that routes every tool call through a gate. It governs only calls that go through it, so a host that also gives the model a shell or file access lets the model go around it, and it does not separate the host's user from the operator. Read the scope first: MCP Proxy.

Does the runtime have to run my loop? No. Since v0.4.0 the Driver is the reference loop, and a gate offers the same controls to a loop you own. The project owns the effect, not the loop.

Does the runtime retry a tool after a crash? Never on its own. It marks the step interrupted and asks the consumer's reconciliation what happened. Without a reconciliation, a call that was executing a tool that is not read-only pauses on an interrupted_side_effect approval: it runs again only if an operator approves, once, on the same step, knowing the first attempt may have taken effect. See Interruption and Reconciliation.

Why SQLite? One file, transactions, no service to run, and the consumer can keep its own tables beside the runtime's. It is not supported on a network filesystem, whose locking SQLite cannot rely on.

Why is each step slower in v0.3? Every commit is synced to disk, so a power loss cannot lose the record that a tool started. It costs about a third more per step on an SSD. See Performance.

Is the API stable? It is pre-1.0 and changes when its consumer needs it to, under a written promise: additive within a minor version; a minor bump's release notes say which symbols changed and what to do instead; nested modules version independently; the schema only migrates forward; the replay key's shape does not change. Changes come with tests and status rows, and CHANGELOG.md lists them. v0.3.0 was such a minor bump; see Upgrading to v0.3.

Why did agentrt refuse my database? It says why in one sentence. The usual causes: the file, or a WAL file beside it, is writable by group or others (chmod 600 it), the path is a symbolic link (name the file itself), the file holds a trigger or a view, or it was written by a different version of the runtime than this agentrt (use the one from the same release). It also never creates a database, so a mistyped path is an error.

Why does agentrt approve ask me to confirm? Because approving is the one irreversible thing it does, and it should never happen by accident or by a script's default. It prints the request, then needs y at the prompt, -yes, or -approval <id>. The decision applies only if the stored approval is still the one it printed.

Is -by an identity? No. It is a label recorded exactly as given. Whoever can write the database can approve; there are no users or authentication.

How do I inspect a run? cmd/agentrt: runs, show, events, in aligned text or JSON, read-only and a page at a time (-limit, -offset). Under it, the view package's read models over the store's bounded page reads, and trace for live events through an observer. See The Operator Command.

How do I measure my agent? With the bench module: declare the outcomes your agent can end in, each classed success, safe non-success, or unsafe, score every trial into exactly one, and write a result file. Every count is reported with its denominator, one unsafe outcome disqualifies the mode, and Compare refuses results from different commits unless you say otherwise. The scenarios and the judging stay yours. See Measuring Agents.

How do I prove my consumer survives a crash? With testkit.Run: give it your tools, policy, and a scripted agent, and a Crash at a point such as AfterToolEffect. It parks the loop there, closes the store the way a dead process would leave it, resumes in a fresh driver after the lease expires, and asserts that nothing re-executed on its own, an approved request executed at most once, and no step or lease was left in flight. It cannot crash inside one transaction, between a tool's own journal writes, or during a model dispatch. See Testing a Consumer.

How do I know a changed policy would not have decided a past run differently? Re-check the run: agentrt.Recheck(ctx, store, runID, newPolicy, agentrt.RecheckOptions{}), or testkit.RecheckDiffers in a test, or agentrt-proxy recheck for the proxy's calls. Each recorded policy evaluation is rebuilt as the policy saw it and put to the new policy, and the report names the steps it would have decided otherwise. It proves the policy's answer, not the tool's behaviour, and a run recorded before v0.5.0 is not re-checkable. A policy that reads a clock, a file, or a service cannot be re-checked exactly. See Re-checking a Run.

Does agentrt verify prove nobody edited my database? Not by itself. The events are a hash chain, and verify finds the first event that was altered, deleted, or reordered by anything that did not rewrite the chain after it, which catches corruption and a careless edit. Whoever can write the file can recompute the hashes and cut events from the end. It shows the record unaltered only up to a head you kept where that writer cannot reach, passed back as -head SEQ:HASH. The state tables are not chained. See Re-checking a Run.

Why did my policy see different bytes after v0.5.0? It now receives the arguments and the tool's schema in PolicyJSON form on every path: keys sorted, no insignificant whitespace, numbers as written, no HTML escaping. Before, the loop and a resume handed different bytes. A policy that unmarshals them is unaffected. See Upgrading to v0.5.

How do I see runs in my tracing tool? Run export/otel/example beside the consumer, pointed at its database and at your collector through the standard OTEL_* environment. It follows the events table read-only from a separate process and sends a trace per run, with spans for the run, its steps, each model attempt, and each approval wait, and nothing in the consumer changes. What it sends is bounded, not redacted: see Exporting Events.

How do I approve from chat? Serve approver/webhook over the database and have your bot call it. The bot lists a run's pending approvals, shows one (as JSON or as sanitised text), and posts a decision carrying the hash it showed, signed with a shared secret; a decision on an approval that has since changed is refused. The handler does not resume the run: your consumer's own process does, as it does after any approval. It has no users, only the secret, and speaks plain HTTP, so it belongs behind TLS and a network boundary you control. See Approval Channels.

Is it safe to point agentrt at a database someone sent me? Safer than it was. It opens it read-only, refuses a trigger or a view, runs with SQLite's defensive mode, reads only the page it prints with every column capped, and escapes everything it prints. The database is still not a trust boundary: what is in it is what its author wrote. See Security Model.