docs · guides
Bring your own agent#
To run your own program under Hekma, you write one file, adapter.toml, that says how to launch the program, what it supports on each OS, how its token use is counted, and how Hekma's settings reach it. Then you register it, check it with a short real run, and, if you publish the adapter, prove it with the conformance kit. This guide walks through each decision; the Adapter manifest reference lists every field.
If your agent speaks the Agent Client Protocol (ACP), you may not need a manifest at all: see Run an ACP agent. For Hermes Agent there is a built-in adapter; see Supported agents.
Before you start#
Answer four questions about the program. Each one maps to a section of the manifest:
| Question | Manifest section |
|---|---|
| What command starts it, and does it run in the foreground? | [lifecycle.start] |
| Can it be paused safely? Does it read input on stdin? | [capabilities] |
| Can it print its own token counts, or can you point its model calls at a different base URL? | [metering] |
| How does it take settings: environment variables, flags, or a config file? | [config] |
Hekma runs the program as a child process and supervises that process. The program must stay in the foreground: a command that starts a background daemon and exits looks to Hekma like an agent that finished at once.
A minimal manifest#
This is a complete, working adapter.toml for an agent that runs in the foreground, prints usage lines, and takes its model name from an environment variable:
contract_version = "1.0.0"
[adapter]
kind = "my-agent"
name = "My Agent"
[lifecycle.start]
exec = "/usr/local/bin/my-agent"
args = ["serve"]
env = { LOG_LEVEL = "info" }
[capabilities.pause]
linux = "guaranteed"
macos = "guaranteed"
windows = "best-effort"
[capabilities.interaction]
linux = "unsupported"
macos = "unsupported"
windows = "unsupported"
[metering]
source = "self-reported"
[config.model]
env = "MODEL"Write contract_version = "1.0.0", the adapter contract version Hekma 0.11.0 speaks. kind names the adapter; you can register many agents from one adapter, each under its own name. name is an optional display name. Hekma rejects keys it does not know, so a typo fails registration with the key named.
Launch#
[lifecycle.start] says how to start the program:
exec: the program. Use an absolute path. A bare name such asmy-agentis looked up on thePATHof the shell that runshekma, which may differ under a service manager. A relative path does not work, because the agent starts in its own folder.args: arguments, as a list. They are passed as they are, with no shell, so there is no quoting or$VARexpansion. If you need a shell, setexecto a small wrapper script.env: extra environment variables for the program.
The program starts in its Agent Home, a private folder Hekma creates for each registered agent, with the environment of the hekma command plus your env and any mapped config. Point the program's data and config folders into the Agent Home when it lets you (for example HOME-style or XDG_* variables), so two agents registered from the same adapter do not share state.
To stop an agent, Hekma signals the process group (SIGTERM on Linux and macOS), waits for the stop window (30 seconds unless the user passes --timeout), then kills it. Handle SIGTERM if your program needs to save work before it exits. You can leave out [lifecycle.stop]: the signal is the stop.
Hekma copies the launch command and the capability declaration when you register. After you change [lifecycle.start] or [capabilities], remove the agent and register it again. The [config] mappings are read again from the manifest at every start, from the path you registered with, so a change there applies at the next start. Register with an absolute --manifest path, or a later start from another folder cannot find the file.
Capabilities#
The [capabilities] tables are your agent's capability declaration: for each OS, what Hekma may do with it. Each entry is guaranteed, best-effort or unsupported, and an OS you leave out counts as unsupported. At least one capability must be supported on at least one OS.
| Capability | Declare it supported when | What Hekma does |
|---|---|---|
pause | Freezing the process and later resuming it is safe. | guaranteed: really suspends the process (SIGSTOP on Linux and macOS). best-effort: marks it paused and prints a note; the process may keep working. unsupported: refuses hekma agent pause with exit code 5. |
interaction | The program reads input on stdin and does something useful with it. | Supported: gives the program a stdin pipe that hekma agent send writes to. unsupported: stdin is closed, and send is refused. |
Be accurate rather than generous. Windows has no safe way to freeze an arbitrary process, so declare pause as best-effort there at most. A budget's pause action relies on this declaration too.
A note on stdin for 0.11.0: an agent started with --detach always gets stdin closed, and hekma agent send works only from a program that holds the engine, such as an embedding. If your program exits when its input ends, it cannot run detached; give it a server or daemon mode, or wrap it. See Supervision and detached runs.
Choose a metering source#
Every agent needs a metering source; registration fails without one. There are two:
Self-reported (
source = "self-reported"): the program prints a line likeKTESIO_USAGE {"sequence": 0, "input_tokens": 1200, "output_tokens": 300}on stdout after each model call. Choose it when you can change the program or wrap it, and when it must run with--detach.Engine-observed (
source = "engine-observed"): Hekma runs a local proxy that the program's model calls go through, and reads the usage from each response. Choose it for a program you cannot change but whose model base URL you can set. It needs one extra mapping:[metering] source = "engine-observed" [config."metering.base_url"] env = "OPENAI_BASE_URL"Engine-observed agents cannot run detached in 0.11.0.
Meter token usage explains the line format, the proxy setup and what each source cannot count.
Map config#
Users set config with hekma agent config set NAME KEY VALUE. A [config] entry tells Hekma how to deliver one known key to your program at start, in exactly one of three ways:
# As an environment variable: MODEL=gpt-4o
[config.model]
env = "MODEL"
# As a flag after your args: my-agent serve --model gpt-4o
[config.model]
flag = "--model"
# Into a TOML file in the Agent Home: config/agent.toml gets [llm] model = "gpt-4o"
[config.model]
file = { path = "config/agent.toml", key = "llm.model" }(Pick one; a key can have only one mapping.) The keys you can map are Hekma's known keys: model, the reserved metering.base_url and memory.dir that Hekma fills in itself, and the others in Config keys. A known key your adapter does not map is simply not delivered.
A file mapping writes a TOML file whose path is relative to the Agent Home. Hekma rewrites the file at every start, and paths that escape the Agent Home are rejected when you register.
For any other setting, users can set a key under agent. with no mapping needed. Hekma delivers it as an environment variable named after the rest of the key, exactly as written: hekma agent config set NAME agent.TEMPERATURE 0.2 gives the program TEMPERATURE=0.2. An agent. key is always delivered this way; a [config] rule for it has no effect.
Secrets work through any mapping. A value written as secret:OPENAI_KEY is replaced at start with the real value, from the environment or the secrets file, and stays masked everywhere Hekma displays it. Prefer env or file mappings for secrets, because flags are visible to other users in the process list. See Config, secrets and memory.
Try it#
Register the adapter with an absolute path, set what it needs, and run it for real:
hekma agent register my-agent --manifest /full/path/to/my-agent
hekma agent config set my-agent model gpt-4o
hekma agent config set my-agent budget.tokens.per_run 50000
hekma agent start my-agent --detach
hekma agent logs my-agent --followCheck three things while the follow runs:
- It starts and stays up.
hekma agent listin another terminal showsrunning. If it showsfailed,hekma agent show my-agentgives the cause and the logs show the program's own errors. - Config arrives. Have the program print the settings it received at start, or read the rendered file in the Agent Home.
hekma agent config get my-agentshows what Hekma resolved and where each value came from. - Usage is counted. After a model call,
hekma agent usage my-agentshows non-zero tokens.
Then stop it with hekma agent stop my-agent. Troubleshooting covers the common start failures.
Prove it with the conformance kit#
If you share your adapter, test it with Hekma's conformance kit: a Rust library that registers your adapter with a fresh, throwaway engine, runs it through eight checks, and returns a report of pass, fail or not applicable for each. It is not published on crates.io, so use it as a git dependency on the Hekma repository, tied to the release tag you target (or a path dependency on a local clone).
Make a small Rust crate next to your adapter (any crate works; the kit only needs a test). In Cargo.toml:
[dev-dependencies]
hekma-conformance = { git = "https://github.com/Ktesio/hekma", tag = "v0.11.0" }
serde_json = "1"In tests/conformance.rs, with your adapter.toml in an adapter folder of the crate:
use std::path::Path;
use hekma_conformance::run_mock_conformance;
#[test]
fn my_adapter_conforms() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("adapter");
let report = run_mock_conformance(&manifest_dir);
println!("{}", serde_json::to_string_pretty(&report).unwrap());
assert!(report.is_conformant(), "failures: {:?}", report.failures());
}Some checks use a small helper program from the kit, fake_agent, which must sit in your crate's target/debug folder. In 0.11.0 the kit cannot build it from outside Hekma's own repository, so build it from a Hekma checkout at the same release first, then run the test:
cargo build -p hekma-conformance --bin fake_agent --manifest-path /path/to/hekma/Cargo.toml --target-dir target
cargo test -- --nocaptureThis is the report for the Quickstart's stand-in agent:
{
"schema_version": 1,
"contract_version": "1.0.0",
"adapter_kind": "ticker",
"sections": [
{ "section": "capability_edges", "result": { "status": "pass" } },
{ "section": "lifecycle", "result": { "status": "pass" } },
{ "section": "pause", "result": { "status": "pass" } },
{ "section": "config_mapping", "result": { "status": "not_applicable", "reason": "the declaration declares no [config] rules, so there is no unified-key delivery to prove" } },
{ "section": "metering_self_reported", "result": { "status": "pass" } },
{ "section": "metering_engine_observed", "result": { "status": "not_applicable", "reason": "the declaration declares self-reported metering, so the engine-observed section does not apply" } },
{ "section": "memory", "result": { "status": "not_applicable", "reason": "the declaration maps no reserved 'memory.dir' key, so Memory Backing delivery is not declared (delivery is offered, not imposed)" } },
{ "section": "interaction", "result": { "status": "pass" } }
]
}
test my_adapter_conforms ... okA check is not_applicable when your declaration does not claim the feature, and the reason says why. If your manifest has env mappings, the config check needs your program (or a test wrapper) to accept a --dump PATH argument and write its environment there; see the --dump convention. The metering, memory and interaction checks run the kit's own test agents, so they prove Hekma's side and not your program's usage lines; your short real run above covers that.
Adapter contract and conformance kit is the full reference: setup, each check, run options and the report format.
Read next#
- Adapter manifest: every section and field.
- Meter token usage: the usage line and the proxy in full.
- Adapter contract and conformance kit: what the contract requires and how the kit checks it.
- Supported agents: adapters that already exist.