Appearance
@ahpd/agent-cofold
The cofold agent runtime as a backend for @ahpd/sdk, and a plugin for the @ahpd/server daemon. It registers the engine cofold: each record of that engine is one agent, and every model an OpenAI-compatible endpoint serves is a model inside it.
A session uses the models and keys cofold already knows about, and runs cofold's file, shell, web and memory tools in the daemon's process.
Part of ahpd. The source is in packages/agent-cofold.
Install
bash
ahpd plugin install @ahpd/agent-cofold
ahpd restartUpdate, remove and one-run flags such as --plugin are in Plugins.
Options
In the configuration file, with the plugin's own options as the defaults every record of the engine starts from:
json
{
"plugins": [
{
"name": "@ahpd/agent-cofold",
"options": {
"model": "deepseek-chat",
"baseUrl": "https://api.deepseek.com/v1",
"apiKey": { "$secret": "host:deepseek" },
"store": "/var/lib/ahpd/cofold",
"tools": { "web": { "search": { "duckduckgo": true } } }
}
}
]
}With no model named anywhere, the providers, keys and model come from the configuration cofold's own app, copapo, reads, through cofold's resolver under copapo's name: $XDG_CONFIG_HOME/copapo/config.json or ~/.config/copapo/config.json, then the nearest .copapo.json at or above a session's folder, then the file $COPAPO_CONFIG names, each merging over the one before. Inside a machine COPAPO_CONFIG is what finds it.
COPAPO_BASE_URL, COPAPO_API_KEY and COPAPO_MODEL, or COFOLD_BASE_URL, COFOLD_API_KEY and COFOLD_MODEL where copapo's own are unset, are read on top of those files, as copapo reads them: the URL adds or replaces a provider called default with the key as its key, and a model with no slash is a model of default. A model id with a slash of its own, as OpenRouter's are, is written after default/, because the first slash ends the provider: default/deepseek/deepseek-chat. A machine profile names them as the needs cofoldBaseUrl, cofoldApiKey and cofoldModel, the key from the vault, so a machine on another box gets a provider with no file holding the key.
Every provider in the cofold file is asked for its models, each with its own key. Each id is offered as <provider id>/<model id>, such as open_router/moonshotai/kimi-k3.1-pro. The backend's own id is the prefix for an explicit baseUrl. On an explicit baseUrl, a model whose first segment names neither that id nor a provider of the file is the endpoint's own id, and is sent whole, as OpenRouter's deepseek/deepseek-chat is. A turn that nothing gives an endpoint, no baseUrl and no provider in the file, is refused as a turn that nothing gives a model is.
Every option below is a key under options in the plugin's entry, and the daemon checks each value against the schema before apply runs. An agent of the engine is a record on the agents channel: it sets any of these except strictTools and the keyed search providers, and wins over the plugin for the ones it sets. A record's id, name and description are the record's own, so this plugin has no option for them. There are two more options, adapter and policy, that only an embedder calling apply in code passes: a model adapter is an object of methods and a policy is a function, so neither is in the options schema and a configuration file cannot name them.
| Option | Default | What it does |
|---|---|---|
model | the cofold file's | The model id a record that names none runs on |
baseUrl | the cofold file's | The OpenAI-compatible endpoint a record that names none uses |
models | none | What the operator says of baseUrl's models, each by its id on that endpoint: supportsReasoningEffort, the thinking levels it accepts, as VS Code's Custom Endpoint models declare it. A model is offered those of cofold's levels (off, low, medium, high) this names as its own thinkingLevel, else cofold's four where the endpoint's catalogue lists reasoning among the model's parameters |
instructions | the cofold file's, else You are a helpful assistant. | The system prompt a record that names none is created with |
computerConfigDir | /ahpd/cofold | The configuration directory the harness reads inside a machine: a state volume seeded with its config.json, or that file mounted read-only in a profile with state: "host". false leaves the image's own |
store | cofold's home, $COFOLD_HOME or ~/.cofold | The folder cofold's file store lives in, shared by every record, as ahpd's Claude agents on one host share the CLI's store and each lists every session in it. Agents that must not see each other's sessions run in machines of their own, where cofold's home is the machine's |
memory | false | true to hold each record's store in memory, for a test |
tools | all four on | Which capabilities a session runs, and where web_search gets its providers |
strictTools | true | false holds tools to the loose check, so a key cofold does not know is dropped rather than refused |
apiKey | the cofold file's | The daemon's own key, for a record that names none. { "$secret": "host:<name>" } reads it from the vault; an embedder may pass a function asked once per request |
resource | the endpoint's origin when it is https | The protected resource a client authenticates against |
autoCompactTokens | the autoCompactAt share of the model's listed context, or of 32000 | The estimated history size at which a session compacts, never above that share |
autoCompactAt | 0.8, copapo's | The share of the model's listed context, or of 32000, a session compacts at when autoCompactTokens names no lower point |
catalogueTimeoutMs | 15000 | How long, in milliseconds, the endpoint is given to publish its catalogue before the configured model stands alone, the time VS Code gives a runtime's model catalogue |
hostTools | true | Whether the host's own tools reach each session. A record can turn them off for one agent |
ahpd vault set host:<name> keeps a key that apiKey or a search provider's apiKey reads with { "$secret": "host:<name>" }, from standard input. See PLUGINS.md for a $secret.
tools
An absent key is on, and false turns that capability off.
| Option | Default | What it does |
|---|---|---|
files | true | read_file, write_file and edit_file, titled by the file; list_files and search_files, titled by the pattern |
shell | true | shell_exec, titled by its command, one command at a time through the platform's shell |
memory | true | memory_read and memory_write, titled by the memory file, under <store>/memory/<workspace slug>/ |
web | true | web_fetch, titled by the URL; an object adds web_search, titled by the query, over the providers in its search |
tools.web.search
The providers web_search asks, in the order they are written. A failing one is skipped. A record may name duckduckgo alone: the channel vaults a record's top-level key fields only, so a key nested here would be kept in the record rather than in the vault.
| Option | Default | What it does |
|---|---|---|
brave | none | Brave Search, as { "apiKey": "..." } |
tavily | none | Tavily, as { "apiKey": "..." } |
duckduckgo | false | true scrapes DuckDuckGo's results page, with no key |
A record of the engine
An agent is a record on x-ahpd-agents: { id, engine, owner, name, description?, icon?, model?, config }. The channel owns name, description, icon and model, so config holds the cofold fields above, and a new record starts from the engine's one template, which sets nothing and runs on cofold's own configuration file.
computerConfigDir, store and memory are host-only: a team's or a person's record that sets one is refused. strictTools is not a field of a record at all, and neither is a keyed search provider, for the reason the tools.web.search table gives; adapter and policy are not options of the schema at all, only values an embedder passes in code. The engine names no key variable: the daemon's own COFOLD_API_KEY is the harness's, read with its file, and a record's apiKey is a secret field of its own, so the channel keeps it in the vault under host:agent/<id>/apiKey for a host record, and no client, session or log gets the value.
A record that names its own endpoint and key, with the model the channel owns:
json
{
"id": "cofold-deepseek",
"engine": "cofold",
"owner": "host",
"name": "Cofold on DeepSeek",
"model": "deepseek-chat",
"config": {
"baseUrl": "https://api.deepseek.com/v1",
"apiKey": { "$secret": "host:deepseek" }
}
}A client puts the key's value, and the channel moves it to the vault under host:agent/<id>/apiKey and keeps a { "$secret": "<name>" } in its place, so no client is shown the value again. The record above names host:deepseek, the form a record written by hand on disk uses; ahpd vault set host:deepseek keeps that entry from standard input.
A client makes one with x-ahpd/agents/put, and the daemon keeps it at agents/<id>.json in its configuration folder. A record whose config no longer fits, or whose $secret the vault cannot answer, is written and shown as invalid with one line naming it, and no value; a record whose engine no plugin registers any more is kept and shown as unavailable.
In your own host
bash
pnpm add @ahpd/agent-cofold @ahpd/sdkts
import { createHost, listen } from '@ahpd/sdk';
import { cofoldAgent } from '@ahpd/agent-cofold';
const host = createHost({
path: process.cwd(),
agents: [cofoldAgent({ model: 'deepseek-chat', baseUrl: 'https://api.deepseek.com/v1' })],
});
await listen({ port: 9187 }, (peer) => host.accept(peer));In a machine
A session in a machine runs nested: an ahpd from the ahpd part, which carries this plugin, is started inside, so any glibc image runs it with nothing installed. Its configuration is a state volume at /ahpd/cofold, seeded with this host's file, and the keys in that file are readable by anything in the machine. See Cofold in a machine.
The tools a session runs
A session gets @cofold/tools' four capabilities by default, so it can read, search, edit and write files, run one command, fetch a page and search the web when a provider is configured, and keep memory. The permission mode is what confines them rather than the workspace, and default asks before a write, a command, a web fetch and a read outside the workspace. A session that names no mode gets auto, where a read, inside or outside, and a web fetch run and only a write or a command asks. An approval is a question the host decides, saying what the call asks to do, with the host's options; cofold is answered for that call alone, whatever the host keeps of the answer.
The workspace check resolves symlinks with cofold's own resolver, including a link whose target does not exist yet, so a write through a link that leaves the workspace is outside it. web_fetch refuses loopback, private and link-local addresses on every hop, though not the machine's public address, and a name that answers with a different address at the connection is not caught.
A write or an edit needs the file to have been read in this session, and cofold refuses one it has not seen. A file that changed since it was read is refused too. What a session read is held in the process, so a restart means reading it again. "files": { "requireRead": false } turns the rule off for the session, and the rule stays on when the key is absent.
Memory is per workspace and shared by the sessions in it, under <store>/memory/<workspace slug>/. A session opened with no working directory keeps its tools and works in a scratch folder of its own, ~/.copilot/chats/<session id>, as VS Code's agents run a session with no workspace. A shell call is drawn as a terminal with its bare command, and a turn with no model configured fails with a sentence that says to add "model" to the cofold configuration file.
tools turns one off by naming it false, and gives web_search its providers:
json
{ "tools": { "shell": false, "web": { "search": { "duckduckgo": true } } } }The named providers are tried in the order the configuration lists them, and a failing one is skipped.
Messages sent while a turn runs
A steering message goes into the running turn: cofold appends it to the run's transcript before the model's next step. The session says it back as the chat's steering message as it takes it, and takes it out once cofold has written it. A run that ends before a step reads it leaves it to a turn of its own, which starts once the chat is idle, ahead of any queued message; a stop lets go of it.
A turn a client sends while another runs is said at once and takes that one's place in every client's chat, as the protocol's reducer replaces the active turn. It runs once the run in front of it is over, because one run reads the session at a time; the turn it replaced runs on, and is in no history. A stop of the turn that took its place stops that run, and the turns waiting with it go. A message queued while a stopped run still winds down is started at once the same way, because every client holds the chat idle once the stop is said.
A session that resumes, forks or rewinds a conversation reads it before any turn starts, and holds the turns it is handed meanwhile. A turn that finds another running once the read is done, the turn that opened first or a run that was waiting on a person when the daemon stopped and that the resume reopened, takes its place the same way. An answer to that run's question that arrives before the resumed run asks it again, while the session still reads the conversation or once the client resumed the turn, goes to the run through cofold's resume(), which takes a command before it has read the run, and the replay's asking of the question is passed by.
A turn that fails
A turn that fails on a model call can be resumed from the client, and the same prompt runs again under the turn's own id. cofold cuts the store through the last message of the turn before, so the failed prompt and what the run wrote after it leave the store. The conversation holds the prompt once, not once for each attempt. A conversation whose first turn failed has its session dropped, and the run makes it again on the same workspace. The calls the failed turn ran are run again, because the cut drops them with the prompt.
A turn that never ran carries no resume. This is a session with no model configured, or a cut the store refused. A turn read back after a restart carries none either, because this process did not watch it run and has no point to cut to.
Documentation
| PLUGINS.md | The whole backend, the session settings and the cofold configuration |
| AGENT.md | The Agent and Session contracts this implements |
| cofold | The agent runtime this runs |
License
MIT © Softov