Guides: tutorials and task-oriented how-to guides
---
# How-to guides
> Task-oriented recipes for common Everruns workflows, equipping agents with tools, streaming events, deploying to channels, and operating production agents.
Source:
Each how-to here solves one concrete problem. They assume you already understand the basics (read the [Tutorials](https://docs.everruns.com/tutorials/run-an-agent/) first) and they don’t try to teach concepts (see [Explanation](https://docs.everruns.com/explanation/) for that). ## Building agents * [Equip an agent with tools](https://docs.everruns.com/how-to/equip-agents-with-tools/), pick capabilities and assign them. * [Give an agent web access](https://docs.everruns.com/how-to/give-an-agent-web-access/), `web_fetch`, network policies, allowlists. * [Define agents as files](https://docs.everruns.com/how-to/define-agents-as-files/), version-controllable agent definitions in Markdown, TOML, or YAML. * [Use AGENTS.md for project instructions](https://docs.everruns.com/how-to/use-agents-md/), inject project-level context into the system prompt. * [Customize a harness](https://docs.everruns.com/how-to/customize-a-harness/), create your own harness as a starting point for many agents. * [Share knowledge with OKF](https://docs.everruns.com/how-to/share-knowledge-with-okf/), import/export Knowledge Bases as Open Knowledge Format bundles, managed like code. * [Migrate between LLM providers](https://docs.everruns.com/how-to/migrate-providers/), swap OpenAI ↔ Anthropic ↔ Gemini without rewriting agents. ## Running agents * [Stream events with the SDK](https://docs.everruns.com/how-to/stream-events/), consume the SSE stream from Python, with reconnection and event filtering. * [Consume events via raw SSE](https://docs.everruns.com/how-to/consume-events-via-sse/), when you don’t want the SDK: curl, EventSource, or any HTTP client. * [Handle errors and cancel turns](https://docs.everruns.com/how-to/handle-errors-and-cancellation/), graceful failure paths, turn cancellation, retries. * [Orchestrate multi-agent pipelines](https://docs.everruns.com/how-to/orchestrate-multi-agent-pipelines/), chain sessions together. * [Build a foreman agent](https://docs.everruns.com/how-to/build-a-foreman-agent/), put one agent in front of a team of specialists and let it triage and delegate. ## Packaging and distribution * [Package an agent skill](https://docs.everruns.com/how-to/package-a-skill/), author a SKILL.md, bundle scripts and references. * [Publish a skill to the registry](https://docs.everruns.com/how-to/publish-a-skill-to-the-registry/), share skills across agents. * [Publish an agent as a Slack app](https://docs.everruns.com/how-to/publish-to-slack/), deploy an agent to a Slack workspace. ## Upgrading * [Migrate to 0.18](https://docs.everruns.com/how-to/migrate-to-0-18/), move Rust code off the `everruns-core` paths that changed, with a symbol-by-symbol table of where each type now lives. ## Operating * [Automate with the CLI](https://docs.everruns.com/how-to/automate-with-the-cli/), scripting against the CLI with `jq`. * [Deploy with Docker Compose](https://docs.everruns.com/getting-started/docker-compose/), bring up the full platform. * [Enforce a budget](https://docs.everruns.com/how-to/enforce-a-budget/), cap token spend per agent, session, or organization.
---
# Automate with the CLI
> Script the Everruns CLI with structured output, jq, quiet mode, and shell pipelines for CI, cron jobs, and integration with other tools.
Source:
The CLI emits structured output (JSON, YAML) for scripting. Combined with `jq` and `--quiet` mode, it composes naturally with shell pipelines. For a command reference, see [CLI](https://docs.everruns.com/features/cli/). ## Capture IDs
```bash
AGENT_ID=$(everruns agents create \
--name "assistant" \
--system-prompt "You are a helpful assistant." \
-o json | jq -r '.id')
SESSION_ID=$(everruns sessions create --agent "$AGENT_ID" -o json | jq -r '.id')
everruns chat "What time is it?" --session "$SESSION_ID"
```
## Quiet mode `--quiet` suppresses headers and tables, printing only the essential identifier:
```bash
everruns agents create -f agent.toml --quiet
# Output: agt_550e8400e29b41d4a716446655440000
```
Useful inside `$(...)` substitution when JSON parsing is overkill. ## Filter listings
```bash
# Active agents only
everruns agents list --output json | jq '.data[] | select(.status == "active")'
# Just the names of agents tagged "production"
everruns agents list -o json \
| jq -r '.data[] | select(.tags[]? == "production") | .name'
# Agents created in the last 24h
everruns agents list -o json \
| jq --arg cutoff "$(date -u -d '24 hours ago' +%FT%TZ)" \
'.data[] | select(.created_at > $cutoff)'
```
## Configure the API URL
```bash
# Per-command
everruns --api-url http://localhost:9300/api agents list
# For the whole shell
export EVERRUNS_API_URL=http://localhost:9300/api
export EVERRUNS_API_KEY=dev
```
In CI, set both via secrets and the CLI will pick them up automatically. ## Drive sessions from a file-defined agent
```bash
cat > agent.md <<'EOF'
---
name: "code-reviewer"
capabilities:
- ref: current_time
- ref: filesystem
config:
allowed_paths: ["/workspace"]
tags: [development]
---
You are an expert code reviewer.
When reviewing code:
1. Check for bugs and edge cases
2. Suggest performance improvements
3. Ensure code follows best practices
EOF
AGENT_ID=$(everruns agents create -f agent.md -o json | jq -r '.id')
SESSION_ID=$(everruns sessions create --agent "$AGENT_ID" -o json | jq -r '.id')
everruns chat "Review the diff at HEAD~1..HEAD" --session "$SESSION_ID"
```
## Send-and-exit (no streaming) `--no-stream` queues the message and returns immediately. Useful when a downstream system polls for results.
```bash
everruns chat "Process the queue" --session "$SESSION_ID" --no-stream
```
## See also * [CLI reference](https://docs.everruns.com/features/cli/), full command list and flags. * [Define agents as files](https://docs.everruns.com/how-to/define-agents-as-files/), the file formats accepted by `-f`.
---
# Build a foreman agent
> Put one agent in front of a team of specialists, so a Slack mention is triaged and delegated to the right worker instead of answered by a generalist.
Source:
A **foreman** is an agent whose job is routing, not answering. It receives an ambiguous request, decides which specialist should handle it, delegates, and reports back. The specialists are ordinary Agents with their own prompts, tools, and models. This is server-side delegation. If you want the *application* to chain agents in sequence, see [Orchestrate multi-agent pipelines](https://docs.everruns.com/how-to/orchestrate-multi-agent-pipelines/) instead — that pattern keeps control in your code. A foreman keeps control in the agent, which is what you want when the trigger is a human asking for something in Slack. ## Prerequisites * Two or more Agents to delegate to, each already working on its own. * An agent to act as the foreman. * For the Slack front door: a Slack workspace where you can create apps. ## Step 1 — Get the workers right first Build and test each specialist on its own before wiring any delegation. A foreman that routes correctly to a broken worker looks like a broken foreman, and you will debug the wrong layer. Give each one a narrow prompt and only the capabilities it needs. “Reviews Rust diffs for correctness bugs” routes better than “helps with code”, because the foreman picks targets from their descriptions. Note their agent ids (`agent_...`). ## Step 2 — Give the foreman the `agent_handoff` capability Delegation targets are an explicit allowlist on the foreman. Configure the `agent_handoff` capability with one entry per worker:
```json
{
"targets": [
{
"id": "code_reviewer",
"name": "Code Reviewer",
"description": "Reviews diffs and pull requests for correctness bugs",
"agent_id": "agent_...",
"required_connections": [],
"required_scopes": []
},
{
"id": "incident_responder",
"name": "Incident Responder",
"description": "Investigates alerts and production incidents",
"agent_id": "agent_..."
}
]
}
```
`description` is the field the model reads when choosing a target, so write it for that purpose. `id` is the stable key the foreman passes back in tool calls; keep it short. Two properties worth knowing, because they shape what the foreman can do: * The foreman **does not inherit the target’s tools**, and never receives the target’s provider credentials. It can ask a worker to act; it cannot act as the worker. * `required_connections` gates a handoff on a provider connection existing before the run starts, which turns a mid-run credential failure into an up-front refusal. `required_scopes` are audit labels in the current implementation, not enforced grants — a tool that needs hard authorization checks its own scopes before acting. ## Step 3 — Write the foreman’s prompt This is the part that decides whether the thing works, and the part no configuration can do for you. A foreman’s prompt needs four things: 1. **When to delegate and when to answer.** Without this, a foreman either delegates trivia or answers things it should have routed. Be concrete: “If the request names a file, a diff, or a PR, hand off to `code_reviewer`.” 2. **How to choose between overlapping targets**, and what to do when none fits — usually ask a clarifying question rather than guessing. 3. **What to do while work is running.** Background handoffs return immediately; the foreman must know to report that it has dispatched, not to invent a result. 4. **How to report back.** A foreman that delegates silently is worse than no foreman, because the requester cannot tell whether anything is happening. Keep it short. A long routing prompt tends to produce a foreman that reasons about routing out loud in your Slack channel. ## Step 4 — Understand what `spawn_agent` gives you The foreman delegates with `spawn_agent`, using `target.type = "agent"` and the `target.id` from your config. The parameters that matter for a foreman: | Parameter | Why a foreman cares | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `background` (default) returns a `task_id` immediately so the foreman can dispatch several workers and stay responsive. `foreground` blocks until the child finishes. `invite` joins the target into the *current* session instead of a child one. | | `result_schema` | A JSON Schema the child must satisfy. Turns “whatever the worker said” into a structured result the foreman can act on rather than re-parse. | | `public_context` | Non-secret context appended to the child task. Use it for the Slack thread reference so the worker knows where the request came from. | | `instructions` | The work request. **Must not contain credentials** — the child has its own. | Background handoffs create a task with `wake_policy = on_terminal`, so the foreman is woken when a worker finishes. It does not poll, and you should not prompt it to. While work is in flight the foreman manages it with the generic task tools: `list_tasks`, `get_task`, `message_task` to steer a worker mid-run, and `cancel_task`. These work identically for subagents, so a foreman can mix both kinds of delegation. ### Subagent or handoff? Both go through `spawn_agent`; the difference is what the child *is*. | | `target.type = "subagent"` | `target.type = "agent"` | | ------------ | ------------------------------------------------------ | ------------------------------------------------------------------------- | | Child config | Inherits the foreman’s harness and agent configuration | The target Agent’s own prompt, capabilities, MCP servers, model | | Set-up cost | None — spawn by name | Build and allowlist the Agent first | | Use when | The work is the same kind of work, just parallel | The work needs different tools, a different model, or different authority | A foreman that fans out “review these six files” wants subagents. A foreman that routes “is this a code question or an incident?” wants handoff. Most real ones use both. Subagent fan-out needs the separate `subagents` capability on the foreman — see [Sub Agents](https://docs.everruns.com/capabilities/sub-agents/). Nesting is depth-governed (`max_subagent_depth`) with root-tree caps on live and total descendant tasks, so a foreman cannot fork-bomb your org by accident. ## Step 5 — Put it in front of Slack Expose the **foreman** — and only the foreman — on Slack. The workers stay internal; they are reached through delegation, not by being mentioned. Follow [Publish an agent as a Slack app](https://docs.everruns.com/how-to/publish-to-slack/) for the mechanics. Two choices matter for a foreman: * **`session_strategy: per_thread`** (the default). Each Slack thread becomes one foreman session, which is what you want: the thread is the unit of work, and the foreman keeps its delegation state for the life of that thread. * **Enable the agent surface** (`agent_surface_enabled`) if you want the foreman available in Slack’s assistant pane as well as in channels. The pane streams replies token-by-token and shows a status line while tools run. ## Step 6 — Let the foreman act on Slack (optional) Out of the box an agent can reply in its own thread and nothing else. It cannot add a reaction, send a DM, look someone up, or post to another channel. If your foreman needs those — acknowledging a request with an emoji while work runs is the common one — attach a Slack MCP server as a capability on the foreman. MCP servers appear as virtual capabilities alongside built-in ones. Be aware this means a second Slack token, separate from the channel’s bot token, with its own scopes to manage and rotate. ## Step 7 — Test the routing, not the workers Send the foreman requests that are deliberately near the boundary between two targets, and requests that match none. Those are where routing fails. A request that obviously belongs to one worker will pass whether or not your prompt is any good. Check that a dispatched request reports back into the thread that asked. A foreman that accepts work and reports somewhere else trains people to stop using it. ## Limits worth knowing before you commit * **No approval buttons.** Slack interactivity is not wired up, so a foreman cannot ask “approve this?” with a button and act on the click. It can only ask in prose and read the reply. For a foreman that dispatches consequential work, this is the real constraint. * **Progress is per-turn, not per-task.** The Slack status line reflects the foreman’s current turn. “3 of 5 workers finished” is available to the foreman via `list_tasks` but is not rendered into Slack for you; the foreman has to say it. * **Two identities** if you use a Slack MCP server, as above. ## See also * [Sub Agents](https://docs.everruns.com/capabilities/sub-agents/) — the `subagents` capability and the shared `spawn_agent` dispatcher * [Publish an agent as a Slack app](https://docs.everruns.com/how-to/publish-to-slack/) * [Orchestrate multi-agent pipelines](https://docs.everruns.com/how-to/orchestrate-multi-agent-pipelines/) — the client-side alternative
---
# Complete a URL elicitation over the API
> Drive the pause-and-consent flow from your own client — declare the hint, read the confirm_url_elicitation event, and post the user's decision.
Source:
When an MCP server asks that a person finish something in their browser, Everruns pauses the turn and waits for a decision (see [URL mode elicitation](https://docs.everruns.com/features/mcp-url-elicitation/)). The Chat UI renders a card for this. Any client can do the same over the REST API and the SSE stream. ## 1. Declare that you can ask The pause only happens for clients that say they can answer it:
```bash
curl -X POST "$EVERRUNS/api/v1/sessions" \
-H 'content-type: application/json' \
-d '{
"agent_id": "agent_01a063c3b55f79f2b5de55fb002e0ae3",
"hints": { "url_elicitation": true }
}'
```
Without the hint the turn never pauses and the flow cannot be completed — see [Clients that cannot pause](#clients-that-cannot-pause). ## 2. Watch for the pause Send a message as usual. When a tool call hits an elicitation, the session moves to `waiting_for_tool_results` and a `tool.call_requested` event arrives on `GET /api/v1/sessions/{session_id}/sse`:
```json
{
"type": "tool.call_requested",
"data": {
"tool_calls": [
{
"id": "url_elicitation_01a063cf-c0bb-7891-926e-fd83aeb24d88",
"name": "confirm_url_elicitation",
"arguments": {
"server": "acme_analytics",
"tool": "run_revenue_report",
"retry_tool": "mcp_acme_analytics__run_revenue_report",
"message": "Acme Analytics needs your API key before it can run this report.",
"url": "https://acme-analytics.example/connect?ref=rev-2026-08",
"url_host": "acme-analytics.example",
"url_is_punycode": false
}
}
]
}
}
```
Everything needed to render your own surface is in `arguments`: the full URL, the host to emphasise, whether that host is Punycode, and which server is asking. Show the URL in full and open it only on an explicit action. Never fetch it on the user’s behalf. ## 3. Post the decision Post **when the user says they have finished**, not when they open the link. The server checks whether the out-of-band interaction completed, so consenting at open time just makes it ask again.
```bash
curl -X POST "$EVERRUNS/api/v1/sessions/$SESSION_ID/mcp-elicitation-consent" \
-H 'content-type: application/json' \
-d '{
"tool_call_id": "url_elicitation_01a063cf-c0bb-7891-926e-fd83aeb24d88",
"action": "accept"
}'
```
```json
{ "host": "acme-analytics.example", "status": "active" }
```
`"action": "decline"` records nothing and lets the agent continue without the tool. Errors worth handling: `404` when the tool call is not a pending elicitation, `409` when the session is not paused (already answered, or timed out). The request body carries only the decision. The server, tool and domain the consent applies to are read from the event Everruns emitted, so a client cannot record consent for something the user was never shown. ## 4. Nothing else to do Everruns records the consent, adds the decision to the conversation as a user turn, and resumes. Your stream then shows the tool being called again and the result arriving — the retry answers the MCP server `accept` on your behalf. ## Timing and reuse * **You have about five minutes.** A session left in `waiting_for_tool_results` is swept (`TOOL_RESULT_TIMEOUT_SECS`, default `300`), the pending call is completed as a timeout, and the turn resumes without consent. * **One consent authorises one retry.** It is deleted when used, so a second elicitation asks again. * **Consent is bound to the domain the user saw.** If the server elicits a different host on the retry, the consent is not reused and a new `confirm_url_elicitation` event arrives. ## Clients that cannot pause Without the `url_elicitation` hint the turn continues and the model relays the link, carrying this payload as the tool result:
```json
{
"code": "url_elicitation_required",
"url": "https://acme-analytics.example/connect?ref=rev-2026-08",
"url_host": "acme-analytics.example",
"url_is_punycode": false,
"server": "acme_analytics",
"tool": "run_revenue_report",
"retry_tool": "mcp_acme_analytics__run_revenue_report",
"message": "Acme Analytics needs your API key before it can run this report.",
"declined": false
}
```
That is informational only: with no pause there is no pending call to answer, the consent endpoint returns `409`, and the tool elicits again on every retry. Declare the hint for any client that needs these tools to complete. ## Calling Everruns as an MCP server The reverse direction is plain MCP. Declare the capability in `_meta`:
```json
{
"_meta": {
"io.modelcontextprotocol/clientCapabilities": { "elicitation": { "url": {} } }
}
}
```
`session_set_secret` then answers with an `input_required` result instead of taking a value:
```json
{
"resultType": "input_required",
"requestState": "eyJ1c2VyX2lkIjoi…",
"inputRequests": {
"secret": {
"method": "elicitation/create",
"params": {
"mode": "url",
"url": "https://app.example.com/api/mcp/elicitations/secret?token=eyJ1c2Vy…",
"message": "Everruns needs the value of 'STRIPE_API_KEY' for session session_…"
}
}
}
}
```
Send the user to that URL, then retry the same call with the state echoed and the answer under the server’s own key:
```json
{
"name": "session_set_secret",
"arguments": { "session_id": "session_…", "name": "STRIPE_API_KEY" },
"requestState": "eyJ1c2VyX2lkIjoi…",
"inputResponses": { "secret": { "action": "accept" } }
}
```
```json
{ "resultType": "complete", "structuredContent": { "name": "STRIPE_API_KEY", "stored": true } }
```
A client that never declared `elicitation.url` gets `-32021` with the missing capability named, rather than being asked for the value. ## A runnable version `examples/mcp-url-elicitation/` in the repository has both halves: a dependency-free MCP server that elicits, and a script that walks the flow above end to end against your deployment. ## Related * [URL mode elicitation](https://docs.everruns.com/features/mcp-url-elicitation/) * [Consume events via SSE](https://docs.everruns.com/how-to/consume-events-via-sse/)
---
# Consume events via raw SSE
> Subscribe to the Everruns event stream from any HTTP client using Server-Sent Events, with reconnection via since_id.
Source:
When you can’t use the SDK, a non-Python service, a browser client, a Postman test, the SSE protocol is available directly. This guide covers the protocol details you need. For the SDK convenience layer, see [Stream events](https://docs.everruns.com/how-to/stream-events/). ## Subscribe
```bash
curl -N "https://your-host/api/v1/sessions/$SESSION_ID/sse" \
-H "Authorization: Bearer $EVERRUNS_API_KEY"
```
Each event arrives as:
```plaintext
event: turn.completed
id: event_01933b5a00007000800000000000001
data: {"id":"event_...","type":"turn.completed","data":{...}}
```
(Blank line terminates each event, per the SSE spec.) ## Resume after disconnect Pass `since_id` to pick up where you left off:
```bash
curl -N "https://your-host/api/v1/sessions/$SESSION_ID/sse?since_id=event_..." \
-H "Authorization: Bearer $EVERRUNS_API_KEY"
```
Event IDs are UUIDv7 and the server orders them by an atomic per-session sequence number. Resumption is gap-free and duplicate-free. ## Heartbeats The server sends a heartbeat every 30 seconds as a comment line:
```plaintext
: heartbeat
```
Comments are invisible to SSE event parsers, they don’t appear as events. Their only purpose is to keep the TCP connection alive and let your client distinguish “idle” from “dead.” **Client requirement:** treat the connection as stale if no data (event or heartbeat) arrives within 45 seconds. Reconnect with the last received event ID. ## Connection cycling To avoid stale connections through proxies, the server gracefully cycles SSE connections every 5 minutes. Before closing, it sends:
```plaintext
event: disconnecting
data: {"reason":"connection_cycle","retry_ms":100}
```
Clients should reconnect immediately using `since_id` of the last event received. No events are dropped during the transition. ## Browser EventSource
```javascript
function connect(sessionId, lastEventId) {
const url = new URL(`/api/v1/sessions/${sessionId}/sse`, API_BASE);
if (lastEventId) url.searchParams.set("since_id", lastEventId);
const es = new EventSource(url, { withCredentials: true });
es.addEventListener("connected", () => console.log("SSE connected"));
es.addEventListener("disconnecting", (e) => {
const { retry_ms } = JSON.parse(e.data);
es.close();
setTimeout(() => connect(sessionId, lastEventId), retry_ms);
});
["input.message", "output.message.delta", "turn.completed"].forEach((t) => {
es.addEventListener(t, (e) => {
const data = JSON.parse(e.data);
lastEventId = data.id;
// handle event...
});
});
es.onerror = () => {
es.close();
setTimeout(() => connect(sessionId, lastEventId), 2000);
};
}
```
The native `EventSource` API uses the `retry:` field that every event includes (100ms during active streaming, up to 500ms while idle). You don’t need to set retry yourself. ## Poll as a fallback If your environment can’t hold long-lived connections (some serverless runtimes), poll instead:
```bash
curl "https://your-host/api/v1/sessions/$SESSION_ID/events?since_id=$LAST_ID" \
-H "Authorization: Bearer $EVERRUNS_API_KEY"
```
The same `since_id` resumption works; latency increases by the polling interval. ## See also * [Event Reference](https://docs.everruns.com/event-reference/), every event type and payload. * [Events as the primary store](https://docs.everruns.com/explanation/events/), why the protocol is shaped this way. * [Stream events with the SDK](https://docs.everruns.com/how-to/stream-events/), the convenient path.
---
# Customize a harness
> Create a custom harness that bundles your preferred capabilities, system prompt baseline, and default model, then use it as the starting point for many agents.
Source:
A harness is the base environment for sessions, system prompt baseline, default model, and pre-bundled capabilities. Create a custom one when you have a set of defaults you want to share across many agents. For the design rationale, see [Why three configuration layers](https://docs.everruns.com/explanation/concepts/#why-three-configuration-layers-harness-agent-session). ## Create a harness via API
```bash
curl -X POST http://localhost:9300/api/v1/harnesses \
-H "Content-Type: application/json" \
-d '{
"name": "research-assistant",
"display_name": "Research Assistant",
"description": "Harness with research capabilities",
"system_prompt": "You are a research assistant. Cite primary sources.",
"capabilities": [
{"ref": "session_file_system"},
{"ref": "web_fetch"},
{"ref": "stateless_todo_list"}
]
}'
```
Names follow `[a-z0-9]+(-[a-z0-9]+)*` (up to 64 characters, no consecutive hyphens) and are unique per organization. Use `name` in API calls; `display_name` is for the UI only. `system_prompt` is optional. Omit it when a harness exists only to bundle capabilities or MCP servers on top of a parent, the effective prompt is then composed from the parent harness, agent, session, and capabilities. For example, a capability-only harness that inherits its prompt from `generic`:
```bash
curl -X POST http://localhost:9300/api/v1/harnesses \
-H "Content-Type: application/json" \
-d '{
"name": "research-tools",
"display_name": "Research Tools",
"parent_harness_id": "harness_...",
"capabilities": [
{"ref": "web_fetch"},
{"ref": "stateless_todo_list"}
]
}'
```
## Preview before creating
```bash
curl -X POST http://localhost:9300/api/v1/harnesses/preview \
-H "Content-Type: application/json" \
-d '{
"system_prompt": "You are a helpful assistant.",
"capabilities": [
{"ref": "session_file_system"},
{"ref": "bashkit_shell"}
]
}'
```
Preview returns the merged system prompt and the tool list. Useful when capability ordering matters or when you’re not sure which capability adds which prompt fragment. ## Use the harness for sessions
```bash
# By name
curl -X POST http://localhost:9300/api/v1/sessions \
-H "Content-Type: application/json" \
-d '{
"harness_name": "research-assistant",
"agent_id": "agent_..."
}'
# Or via the CLI
everruns sessions create --harness research-assistant --agent agent_...
```
## Inheritance Harnesses support single-parent inheritance, a child harness can extend another, layering on extra capabilities or a longer system prompt. The merge is associative: a chain of N harnesses produces the same `RuntimeAgent` as a single pre-merged harness. This is useful when one team owns a base harness and other teams add their own specialisation on top. ## When to use a harness vs. an agent * **Harness**: defaults shared across *many agents* (e.g., “all our internal agents have file access, web fetch, and the company AGENTS.md”). * **Agent**: per-role configuration (system prompt, voice, role-specific tools). * **Session**: per-conversation tweaks (extra capability for this user, narrower network policy). Don’t pack agent-specific behaviour into a harness, it makes the harness a god-object and erodes the layering benefit. ## See also * [Harnesses feature page](https://docs.everruns.com/features/harnesses/) * [Built-in harnesses](https://docs.everruns.com/built-ins/harnesses/base/), the shipped baselines you can extend. * [Equip an agent with tools](https://docs.everruns.com/how-to/equip-agents-with-tools/), at the agent layer.
---
# Define agents as files
> Author agent definitions in Markdown, TOML, YAML, or JSON so they can be version-controlled, reviewed in pull requests, and imported via the SDK or CLI.
Source:
Agents can be defined as files with structured metadata and a system prompt. This makes them shareable, reviewable, and version-controllable, useful for teams that want agents in git rather than only in the API. ## Markdown with front matter The most readable format. The YAML front matter holds metadata; the body becomes the system prompt.
```markdown
---
name: "hackernews-reader"
description: "An agent that browses HackerNews autonomously"
tags:
- demo
- hackernews
capabilities:
- web_fetch
- current_time
- session_file_system
---
You are a HackerNews reader agent. You autonomously browse
Hacker News to find interesting stories, read discussions,
and research authors.
```
Import via the SDK:
```python
with open("hackernews-reader.md") as f:
agent = await client.agents.import_agent(f.read())
```
Or via the CLI:
```bash
everruns agents create -f hackernews-reader.md
```
## TOML
```toml
name = "research-assistant"
description = "Helps with research tasks"
# Base execution harness for this agent (id or name). Omit to default to the
# org's built-in `generic` harness. Sessions started from the agent inherit it.
harness_name = "generic"
system_prompt = """
You are a helpful research assistant.
Always cite your sources.
"""
tags = ["research", "assistant"]
[[capabilities]]
ref = "current_time"
[[capabilities]]
ref = "web_fetch"
```
If `./agent.toml` exists in the current directory and you don’t pass inline flags, `everruns agents create` picks it up automatically. ## YAML
```yaml
name: "research-assistant"
description: "Helps with research tasks"
# Base execution harness (id or name); omit to default to the built-in `generic`.
harness_name: "generic"
system_prompt: |
You are a helpful research assistant.
Always cite your sources.
capabilities:
- ref: current_time
config: {}
- ref: web_fetch
config: {}
tags:
- research
```
Shorthand form (capability IDs only):
```yaml
capabilities:
- current_time
- web_fetch
```
The long form (`ref` + `config`) is required for per-agent capability configuration. ## JSON JSON is supported for tooling that generates agent definitions programmatically. It has no special features over TOML/YAML, pick the format your team prefers.
```bash
everruns agents create -f agent.json
```
## Seed sessions with initial files To pre-populate the session workspace, either pass `--initial-files-dir` on the CLI or use the `initial_files` front matter field:
```markdown
---
name: "a11y-audit"
capabilities:
- daytona
initial_files:
- .
- .agents/*
---
Run axe-core audits...
```
Entries can be: * `.`, the entire current directory (non-hidden files plus `.agents/`). * A subdirectory, walked recursively. Glob suffixes like `/*` are stripped. * A single file path. Hidden files outside `.agents/` are skipped; symlinks outside the base directory are rejected; binary files are ignored. ## Update vs. create `everruns agents update` accepts the same file formats. Passing an explicit `` positional disables implicit `agent.toml` selection so you can update a different agent from the same directory. ## See also * [CLI reference](https://docs.everruns.com/features/cli/), full command surface. * [Equip an agent with tools](https://docs.everruns.com/how-to/equip-agents-with-tools/), choosing capabilities. * [Use AGENTS.md for project instructions](https://docs.everruns.com/how-to/use-agents-md/), adding per-project guidance.
---
# Enforce a budget
> Cap LLM spend per session or agent with USD, token, or credit-denominated budgets, soft pause thresholds, and stacked limits.
Source:
Budgets cap how much a session can spend on LLM calls. After every generation, Everruns debits the cost from any active budgets; when the balance reaches zero, the session stops. This guide creates and applies a budget. For the design and enforcement semantics, see [Budgets](https://docs.everruns.com/advanced/budgets/). ## Create a USD budget for a session
```bash
curl -X POST http://localhost:9300/api/v1/budgets \
-H "Content-Type: application/json" \
-d '{
"scope": "session",
"scope_id": "session_...",
"currency": "usd",
"limit": 10.00,
"soft_limit": 8.00
}'
```
When session spend exceeds `soft_limit`, the session pauses (status becomes `paused`) so a human can decide to top up or stop. When it hits `limit`, the session terminates. ## Token-denominated budget
```bash
curl -X POST http://localhost:9300/api/v1/budgets \
-H "Content-Type: application/json" \
-d '{
"scope": "agent",
"scope_id": "agent_...",
"currency": "tokens",
"limit": 2000000
}'
```
Token budgets are model-agnostic, they cap raw token usage regardless of which provider the session uses. ## Currencies at a glance | Currency | Unit | Cost basis | | --------- | ----------------------- | -------------------------------------------------------- | | `usd` | US dollars | Per-model pricing (input/output cost per million tokens) | | `tokens` | Raw tokens | Direct count of input + output tokens | | `credits` | 1 credit = 1,000 tokens | Token count ÷ 1,000 | | Custom | Any string | Falls back to raw token count | USD budgets reflect real costs: $10 lasts much longer on GPT-4o than on Claude Opus. ## Stack budgets for layered limits You can apply multiple budgets to a session at once. The **most restrictive** wins. A common pattern: * `$10 USD` session budget, caps dollar cost. * `2,000,000 tokens` agent budget, caps total tokens regardless of pricing. Both apply; whichever runs out first stops the session. ## Listen for budget events Budget thresholds emit events you can subscribe to:
```python
async for event in client.events.stream(session.id):
if event.type == "budget.warning":
print(f"Budget warning: {event.data}")
elif event.type == "budget.paused":
print(f"Session paused at soft limit")
elif event.type == "budget.exhausted":
print(f"Session stopped — budget exhausted")
```
A warning fires at 20% remaining; pause fires when crossing the soft limit; exhaustion fires at zero balance. ## Resume a paused session After a `budget.paused` event, you can: * Increase `limit` to give the session more headroom:
```bash
curl -X PATCH http://localhost:9300/api/v1/budgets/$BUDGET_ID \
-H "Content-Type: application/json" \
-d '{ "limit": 20.00, "soft_limit": 16.00 }'
```
* Or call the resume endpoint to continue against the existing limit (the next LLM call may push the budget over). ## A note on enforcement Budget checks run **after** each LLM call, not before, to avoid latency on the hot path. The last generation can slightly overshoot the limit, this is expected and by design. Treat budgets as cost caps, not hard cutoffs measured in single tokens. ## See also * [Budgets](https://docs.everruns.com/advanced/budgets/), full design, ledger semantics, custom currencies. * [Self-Budget capability](https://docs.everruns.com/capabilities/self-budget/), let agents inspect their own budget at runtime.
---
# Equip an agent with tools
> Assign capabilities to an agent so it can read files, run shell commands, fetch URLs, and track tasks.
Source:
This guide assigns common capabilities to an agent so it can interact with files, run commands, and fetch URLs. For the full catalog see the [Capabilities reference](https://docs.everruns.com/capabilities/). ## Common capabilities | Capability ID | Tools provided | What it’s for | | --------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------ | | `web_fetch` | `web_fetch` | Fetch URLs, convert HTML to markdown | | `session_file_system` | `read_file`, `write_file`, `edit_file`, `list_directory`, `grep_files`, `delete_file`, `stat_file` | Per-session virtual filesystem | | `bashkit_shell` | `bash` | Sandboxed bash shell | | `stateless_todo_list` | `write_todos` | Structured task tracking | | `current_time` | `get_current_time` | Current date/time awareness | | `session_storage` | `kv_store`, `secret_store` | Key/value and encrypted secrets | ## Assign capabilities at creation
```python
agent = await client.agents.create(
name="Researcher",
system_prompt="You research topics and save notes to /workspace.",
capabilities=["web_fetch", "session_file_system", "stateless_todo_list"],
)
```
## Update an existing agent
```python
await client.agents.update(
agent.id,
capabilities=["web_fetch", "session_file_system", "bashkit_shell"],
)
```
## Configure a capability Some capabilities accept per-agent configuration. Use the long form:
```python
await client.agents.update(
agent.id,
capabilities=[
{"ref": "web_fetch", "config": {"enable_file_download": True}},
{"ref": "session_file_system"},
],
)
```
## Verify the agent has the tools
```python
agent = await client.agents.get(agent.id)
for cap in agent.capabilities:
print(cap.ref, cap.config or "")
```
## Notes on ordering Capability order matters, capabilities earlier in the list contribute their system prompt fragments first. Put high-priority context (project conventions, AGENTS.md) before tool-specific guidance. ## See also * [Capabilities reference](https://docs.everruns.com/capabilities/), all available capabilities. * [Why capabilities are first-class](https://docs.everruns.com/explanation/concepts/#why-capabilities-are-first-class), the design rationale. * [Give an agent web access](https://docs.everruns.com/how-to/give-an-agent-web-access/), narrower task with network policies.
---
# Give an agent web access
> Enable the web_fetch capability, restrict outbound network access with allow/block lists, and verify the agent reaches only intended hosts.
Source:
`web_fetch` gives an agent the `web_fetch` tool, fetch any URL, optionally convert HTML to markdown. By default it can reach any public host, with built-in SSRF protection blocking private IPs. To restrict it further, layer **network access lists** on the harness, agent, or session. ## Enable the capability
```bash
curl -X PATCH http://localhost:9300/api/v1/agents/$AGENT_ID \
-H "Content-Type: application/json" \
-d '{
"capabilities": [
{ "ref": "web_fetch" }
]
}'
```
Or in an agent definition file:
```yaml
capabilities:
- ref: web_fetch
- ref: session_file_system # so the agent can save fetched content
```
## Restrict to specific hosts Pass `network_access` when creating or updating the agent. Patterns can be exact domains, wildcard domains, or URL prefixes:
```bash
curl -X POST http://localhost:9300/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"name": "Research Agent",
"system_prompt": "You are a research assistant.",
"capabilities": [{ "ref": "web_fetch" }],
"network_access": {
"allowed": ["*.github.com", "api.openai.com", "https://docs.python.org/3/"],
"blocked": ["evil.example.com"]
}
}'
```
| Pattern | Matches | | ----------------------------- | ----------------------- | | `api.example.com` | Exact domain | | `*.example.com` | Domain + all subdomains | | `https://api.example.com/v1/` | URL prefix | Domain matching is case-insensitive. **Blocked patterns always win** over allowed patterns. ## Layered policies The three layers (harness → agent → session) can only narrow access, never expand it: * `allowed` lists intersect across layers. * `blocked` lists union across layers. So a session can tighten what its agent allows but cannot punch a hole through the agent’s `blocked` list. ## Tighten further per-session When you don’t trust a particular session’s input, restrict more:
```bash
curl -X POST http://localhost:9300/api/v1/sessions \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agent_...",
"network_access": {
"blocked": ["internal.corp", "*.staging.example.com"]
}
}'
```
## SSRF protection Even without an explicit policy, `web_fetch` blocks private IP ranges by default, loopback, RFC1918, link-local, and CGNAT, with DNS pinning to prevent rebinding attacks. To explicitly allow a private host, set it in `allowed` and disable SSRF protection at the harness level (see [Network access control](https://docs.everruns.com/advanced/network-access/)). ## Verify Try a URL the agent should reach, then one it shouldn’t, and check the tool result events:
```python
await client.messages.create(session.id, "Fetch https://api.github.com/zen")
# Should succeed.
await client.messages.create(session.id, "Fetch https://evil.example.com/")
# Should fail with a network policy error in the tool result.
```
## See also * [Network access control](https://docs.everruns.com/advanced/network-access/), full pattern semantics and layering rules. * [Web Fetch capability](https://docs.everruns.com/capabilities/web-fetch/), tool reference. * [Equip an agent with tools](https://docs.everruns.com/how-to/equip-agents-with-tools/)
---
# Handle errors and cancel turns
> Detect and recover from failed turns, cancel a long-running turn, and react to common error events from the SSE stream.
Source:
Turns can fail (the LLM rejected the request, a tool errored repeatedly) or be cancelled by the user. The event stream tells you which. This guide covers both paths. ## Detect a failed turn Failures surface as `turn.failed` events:
```python
async for event in client.events.stream(session.id):
if event.type == "turn.completed":
break
if event.type == "turn.failed":
err = event.data.get("error", "unknown")
print(f"Turn failed: {err}")
break
```
The `error` field is a structured object, type, message, optional cause. Inspect it to decide whether to retry, surface to the user, or escalate. ## Cancel a long-running turn To cancel a turn that’s already running:
```python
import asyncio
await client.messages.create(session.id, "Analyse every Python package on PyPI")
await asyncio.sleep(2)
await client.sessions.cancel(session.id)
```
Cancellation emits a `turn.cancelled` event, appends a user message noting the cancellation, and the worker emits a final agent message confirming the work was stopped. The session itself stays open and accepts new messages. ## React to all three terminal states
```python
TERMINAL = {"turn.completed", "turn.failed", "turn.cancelled"}
async for event in client.events.stream(session.id):
if event.type in TERMINAL:
print(f"[{event.type}]")
if event.type == "turn.failed":
print(event.data.get("error"))
break
```
## Retry a failed turn Failed turns don’t auto-retry from the application’s perspective (durable execution retries individual steps inside a turn, not the whole turn). To retry, send the message again:
```python
async def send_with_retry(client, session_id, content, attempts=2):
for attempt in range(attempts):
await client.messages.create(session_id, content)
async for event in client.events.stream(session_id):
if event.type == "turn.completed":
return True
if event.type == "turn.failed":
if attempt == attempts - 1:
return False
break
return False
```
Don’t retry indefinitely, a turn that fails twice usually fails for a reason (rate limit, malformed prompt, missing capability). Surface to the user. ## Common error patterns | Event payload | Cause | Action | | ----------------------------- | ---------------------------------------- | --------------------------------------------------------------- | | `rate_limit_exceeded` | LLM provider rate-limited the worker | Wait, retry with backoff | | `request_too_large` | Context overflowed even after compaction | Trim the conversation, start a fresh session | | `tool_call_failed` (terminal) | A tool errored repeatedly | Inspect tool result events, fix the agent prompt or tool config | | `cancelled` | User or app called `sessions.cancel` | No retry, user intent | ## See also * [Stream events](https://docs.everruns.com/how-to/stream-events/) * [Event Reference](https://docs.everruns.com/event-reference/), all event types and payloads. * [The agentic loop](https://docs.everruns.com/explanation/agentic-loop/#what-happens-when-the-loop-gets-stuck), failure modes.
---
# Migrate between LLM providers
> Switch agents from OpenAI to Anthropic to Gemini (or any OpenAI-compatible provider) without rewriting prompts or capabilities.
Source:
Everruns abstracts the LLM behind a uniform interface, so the same agent can run on OpenAI, Anthropic, Gemini, or any OpenAI-compatible provider. This guide swaps providers cleanly without losing sessions or rewriting agents. ## Concepts Two pieces decide which model runs: * **LLM Provider**: a configured API provider with encrypted credentials (e.g., `openai`, `openrouter`, `anthropic`, `gemini`, `openai_completions`). * **LLM Model**: a specific model on a provider (e.g., `gpt-5.6-sol`, `claude-sonnet-5`, `gemini-3.5-flash`). Model resolution priority on each turn: 1. Message-level `model` control (if present on the incoming message). 2. Session override (`session.default_model_id`). 3. Agent default (`agent.default_model_id`). 4. System default. ## Add a new provider
```bash
curl -X POST http://localhost:9300/api/v1/providers \
-H "Content-Type: application/json" \
-d '{
"name": "anthropic",
"provider_type": "anthropic",
"api_key": "sk-ant-..."
}'
```
The API key is encrypted at rest. The provider’s models are discovered on creation; manually-added models are also supported. ## Switch an agent’s default model
```bash
curl -X PATCH http://localhost:9300/api/v1/agents/$AGENT_ID \
-H "Content-Type: application/json" \
-d '{ "default_model_id": "model_claude_sonnet_4" }'
```
New sessions inherit the new default. **Existing sessions keep running on the model they started with** unless you override per-session or per-message. ## Override per session For an A/B comparison, override at the session level:
```bash
curl -X POST http://localhost:9300/api/v1/sessions \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agent_...",
"default_model_id": "model_claude_sonnet_4"
}'
```
## Override per message The most targeted form, run a single turn on a different model:
```python
await client.messages.create(
session.id,
"Re-analyse the above with extra rigour.",
model="model_claude_opus",
)
```
## Compatibility caveats Most behaviour ports across providers, but a few things differ: * **Extended thinking / reasoning effort.** Supported on Anthropic Claude, OpenAI GPT-5.x, and o-series. Other models silently ignore the `reasoning_effort` control. * **Execution phases on the wire.** OpenAI Responses API on the GPT-5.4 and GPT-5.5 families accepts `phase` annotations on replayed messages; other providers (and earlier OpenAI models) ignore them. Internal tracking continues regardless. The authoritative list is the `supports_phases` flag in `crates/core/src/llm_model_profiles.rs`. See [Execution phases](https://docs.everruns.com/explanation/agentic-loop/#execution-phases). * **Tool call format differences.** The platform handles translation, but very large tool schemas may compress better on one provider than another. * **Per-token cost.** USD budgets adjust automatically since they use per-model pricing. Token budgets are model-agnostic. ## Verify before flipping production A safe migration sequence: 1. Add the new provider and verify discovered models. 2. Create a test agent that mirrors production, with `default_model_id` set to a model on the new provider. 3. Run an evaluation suite against the test agent. 4. Once happy, update the production agent’s `default_model_id`, new sessions migrate over. 5. Leave old sessions on the old model; they age out naturally. ## See also * [Concepts: LLM Provider and Model](https://docs.everruns.com/explanation/concepts/), entity model. * [Observability with Braintrust](https://docs.everruns.com/observability/braintrust/), evaluate cross-provider quality.
---
# Migrate to 0.18
> Move Rust code off `everruns-core` paths that changed in 0.18, with a symbol-by-symbol table of where each type now lives.
Source:
0.18 narrows `everruns-core` to the neutral execution kernel. Types that were persisted control-plane records, hosted service contracts, product composition or concrete integrations moved to the crate that owns them. The behaviour, the wire formats and the stored schema are unchanged, only the import paths. ## Retain an Engine for sessions The Framework exposes a concrete application execution owner. The 0.18 API removes `agent.session()` and `agent.resume(id)`; applications retain the engine that owns session identity and resume authority:
```diff
let session = agent.session();
use everruns::Engine;
let engine = Engine::new();
let session = engine.create(agent);
let id = session.session_id();
let resumed = agent.resume(id).await?;
let resumed = engine.resume(id).await?;
```
`Engine` volatile resume is deliberately process-local and engine-scoped. The old `InMemoryEngine` name remains a type alias. Engine retains the immutable Agent snapshot and exact Environment/WorkspaceHead; it does not serialize a Scale-compatible Agent definition. For a locally persisted session after process restart, rebuild the Agent from trusted application configuration, call `engine.attach(id, agent).await?`, and then `engine.resume(id).await?`. Attachment verifies the persisted session catalog before accepting the behavior snapshot; credentials and closures are never serialized. This affects you if your Rust code imports from `everruns_core` directly. If you use the `everruns` facade, most of this is invisible: the facade re-exports what applications need, and where a moved type is part of that surface it is re-exported from its new home under the same name. ## The quickest path Most migrations are a find-and-replace of a crate prefix. Compile, read the unresolved-import errors, and look each symbol up in the tables below.
```bash
cargo build 2>&1 | grep -E "unresolved import|no .* in"
```
Add whichever crates the table points you at:
```toml
everruns-platform = "0.18" # persisted records, hosted service contracts
everruns-host = "0.18" # execution composition and host wiring
everruns-provider = "0.18" # provider SPI, typed IDs, sqlx impls
everruns-capability = "0.18" # capability identity/configuration contract
everruns-mcp = "0.18" # MCP adapter and the OAuth protocol client
everruns-llmsim = "0.18" # deterministic production-safe simulator
```
The earlier `everruns-session-services` preview package was consolidated into `everruns-host`; use `everruns_host::session_services` for its namespaced API. ## Composition The single biggest change for embedders. `PlatformDefinition` no longer exists. | 0.17 | 0.18 | | ------------------------------------------------------ | --------------------------------------------------- | | `everruns_core::PlatformDefinition` | `everruns_host::HostComposition` | | `everruns_core::PlatformDefinitionBuilder` | `everruns_host::HostCompositionBuilder` | | `everruns_server::oss_platform_definition()` | `everruns_server::oss_host_composition()` | | `everruns_server::oss_platform_definition_for_grade()` | `everruns_server::oss_host_composition_for_grade()` | | `everruns_worker::default_platform_definition()` | `everruns_worker::default_host_composition()` | | `ServerAppBuilder::platform_definition(..)` | `ServerAppBuilder::host_composition(..)` | | `WorkerAppBuilder::platform_definition(..)` | `WorkerAppBuilder::host_composition(..)` |
```diff
use everruns_core::PlatformDefinition;
use everruns_host::HostComposition;
let platform = PlatformDefinition::builder()
let composition = HostComposition::builder()
.capability_registry(capabilities)
.driver_registry(drivers)
.build();
ServerAppBuilder::new().platform_definition(platform)
ServerAppBuilder::new().host_composition(composition)
```
The type is otherwise identical, same fields, same builder methods. It moved to the layer that executes a turn, because selecting a deployment’s capabilities and drivers is composition rather than kernel configuration. ### Input/Reason/Act execution kernel Concrete phase execution moved out of `everruns-core`. Import phase algorithms and their I/O values from `everruns-engine`; keep neutral effect contracts in core. | 0.17 | 0.18 | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | `everruns_core::atoms::{InputAtom, InputAtomInput, InputAtomResult}` | `everruns_engine::{InputAtom, InputAtomInput, InputAtomResult}` | | `everruns_core::atoms::{ReasonAtom, ReasonInput, ReasonResult}` | `everruns_engine::{ReasonAtom, ReasonInput, ReasonResult}` | | `everruns_core::atoms::{ActAtom, ActInput, ActResult, ToolCallResult}` | `everruns_engine::{ActAtom, ActInput, ActResult, ToolCallResult}` | | `everruns_core::atoms::AtomContext` | `everruns_core::ExecutionContext` (also re-exported by `everruns-engine`) | | `everruns_core::atoms::{PreToolUseHook, PostToolExecHook, PreToolUseDecision}` | `everruns_core::tool_hooks::*` | The generic `Atom` trait was removed; it had no production dynamic-dispatch use. Call the concrete executor’s inherent async `execute` method. There is no core compatibility module in 0.18. Serialized phase payloads retain the same fields, so durable records remain readable even though the Rust ownership path changed. ### Turn context and command completion Store-backed turn preparation now belongs to `everruns-host`. Core keeps the secret-free execution snapshot, pure context transformations, and narrow effects used by custom hosts. | 0.17 | 0.18 | | ---------------------------------------------------- | ---------------------------------------------------- | | `everruns_core::assemble_turn_context` | `everruns_host::assemble_turn_context` | | `everruns_core::inspect_turn_context` | `everruns_host::inspect_turn_context` | | `everruns_core::load_execution_snapshot` | `everruns_host::load_execution_snapshot` | | `everruns_core::load_execution_snapshot_for_session` | `everruns_host::load_execution_snapshot_for_session` | | `everruns_core::StoreCommandHost` | `everruns_host::StoreCommandHost` | `everruns_engine::ReasonAtom::new` no longer accepts harness, agent, session, and provider stores or a driver registry. Construct an `everruns_host::StoreTurnContextResolver` from those host services, then pass that resolver plus the narrow message, capability, and event effects to the atom. Hosts that already loaded a `ResolvedExecutionSnapshot` should call `everruns_host::assemble_turn_context_from_snapshot` and execute the atom with the resulting `AssembledTurnContext`; this avoids a second store load. For a fully custom host, implement the neutral `everruns_core::TurnContextResolver`, or provide already-resolved `ResolvedTurnContextInput` to `everruns_core::assemble_resolved_turn_context`. That input contains a secret-free model/provider identity and an opaque ready driver; provider keys and endpoints are never serializable kernel values. `CommandTurnContext` now exposes `session_id` directly instead of an `ExecutionSession`. Commands retain the same filtered messages, effective prompt, locale, model, streaming, and error-decision behavior without receiving a session record. ## Persisted records These are database and API records. Execution consumes a portable projection of each; the stored row is control-plane state. | 0.17 (`everruns_core::`) | 0.18 | | ----------------------------------------------------------------------------------------- | -------------------------------- | | `Agent`, `AgentVersion`, `AgentStatus`, `AgentVersionChangeKind` | `everruns_platform::` | | `Harness`, `HarnessStatus`, `BuiltInHarnessDefinition`, `BuiltInHarnessRole` | `everruns_platform::` | | `Session`, `SessionStatus`, `SessionSource`, `SessionActivity`, `SessionParticipant` | `everruns_platform::` | | `Workspace`, `WorkspaceStatus` | `everruns_platform::workspace::` | | `Eval`, `EvalCase`, `EvalRun`, `EvalCaseResult`, `EvalRunDataset`, `EvalTarget`, `Scorer` | `everruns_platform::` | | `Observer`, `ObserverMatch`, `LlmJudgeConfig`, `TraceScore` | `everruns_platform::` | | `FeatureFlags`, `FeatureFlagMap`, `FeatureFlagDefinition` | `everruns_platform::` | | `Budget`, `LedgerEntry` | `everruns_platform::` | If you were reading a stored record to run a turn, you probably want the portable projection instead, `AgentDefinition`, `HarnessDefinition` and `ExecutionSession` all stay in `everruns_core`, produced at the platform loading boundary by `Agent::execution_definition`, `Harness::execution_definition` and `Session::execution_session`. ## Hosted service contracts | 0.17 (`everruns_core::`) | 0.18 | | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `session_sqldb::*`, `SessionSqlDbStore`, `DatabaseInfo`, `SqlQueryResult`, `SqlExecuteResult`, `TableSchema`, `ColumnSchema`, `SessionSqlDbError` | `everruns_platform::session_sqldb::` | | `traits::SessionMutator` | `everruns_host::SessionMutator` (also re-exported by platform) | | `session_sandbox::*`, config, state, instance, exec/file payloads, `SessionSandboxProvider`, `SessionSandboxProviderPlugin` | `everruns_platform::session_sandbox::` | | `Connector`, `ConnectorRegistry`, `ConnectorPlugin` | `everruns_platform::connector::` | | `EmailSender`, `EmailMessage`, `SystemEmailConfig`, `ResendEmailSender` | `everruns_platform::email::` | | `OAuthClient`, `TokenSet`, `PkcePair` | `everruns_mcp::oauth::protocol::` | Neutral per-turn contracts remain in `everruns-core`, but the catch-all `everruns_core::traits` module is gone. Import from the owning concern instead, for example `everruns_core::tool_context::ToolContext`, `everruns_core::session_files::SessionFileSystem`, or `everruns_core::provider_resolution::ProviderStore`. The deployment-owned `SessionFileSystemFactory` and its context now come from `everruns-host`. Two of these also changed how a capability *reaches* the service. `sqldb_store` and `session_mutator` are no longer fields on `ToolContext`; they resolve from the type-keyed extension bag:
```diff
let Some(store) = &context.sqldb_store else { ... };
let Some(store) = context.extensions.get::() else { ... };
let store = &store.0;
```
If you implement a custom host, install them the way `everruns-host` does:
```rust
extensions.insert(Arc::new(SessionSqlDbStoreExt(store)));
extensions.insert(Arc::new(SessionMutatorExt(mutator)));
```
## Capabilities and implementations | what | 0.18 home | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Knowledge Bases and Indexes, Memories, delegation, subagents, background and scheduled work, user hooks, citations, model scouting, platform management | `everruns_platform::capabilities::` | | Session info and session storage | `everruns_host::session_services::capabilities::` (also re-exported by platform) | | Session SQL database and session sandbox | `everruns_platform::capabilities::` | | `spawn_background` and its runtime, event sink, admission permits, reattach | `everruns_platform::background_run::` | | Portable built-ins, human intent, infinity context, skills, UI prompts, compaction, tool search | `everruns_builtins::` | | `everruns_openui::{PromptOptions, default_library, generate_prompt}` | `everruns_builtins::openui::{PromptOptions, default_library, generate_prompt}` | | `everruns_a2ui::{PromptOptions, default_catalog, generate_prompt}` | `everruns_builtins::a2ui::{PromptOptions, default_catalog, generate_prompt}` | | OpenRouter workspace, model scout, and provider-executed server tools | `everruns_integrations_openrouter::` | | Filesystem, shell, web fetch, Lua | `everruns_integrations_*` | | MCP adapter | `everruns_mcp::` | | In-process HTTP egress transport | `everruns_host::DirectEgressService` | | Telemetry init, exporter event listeners, `CompositeEventListener` | `everruns_host::observability::` (feature `observability`) | | `llmsim` driver, configs, scripted turns, registry helpers, host-builder extension | `everruns_llmsim::` | | in-memory agentic loop, writable test doubles, fixture capabilities | `everruns_test_support::` | | `everruns_core::in_memory::{InMemoryAgentStore, InMemoryHarnessStore, InMemorySessionStore, InMemoryProviderStore}` | `everruns_host::{InMemoryAgentStore, InMemoryHarnessStore, InMemorySessionStore, InMemoryProviderStore}` | | `everruns_core::in_memory::{InMemoryMessageRetriever, InMemoryEventEmitter}` | `everruns_test_support::{InMemoryMessageRetriever, InMemoryEventEmitter}` for isolated deterministic tests | Product presets compose these explicitly. Core registries are now empty by default: use `everruns_host::runtime_capability_registry()` for the Framework preset or `everruns_platform::capabilities::hosted_capability_registry()` for the hosted product catalog. Hosted conversation history has no writable message-store replacement. Append canonical events through `everruns_host::EventLog` / `HostEventEmitter` and read messages through `EventHistory`. This avoids message/event dual writes and keeps resume and replay behavior identical across in-memory and durable hosts. `everruns-test-support` continues to re-export its 0.17 simulator paths during the 0.18 migration, so existing test suites can upgrade without an immediate import rewrite. Treat that as a migration bridge: production code, new tests, and low-level hosts should depend on `everruns-llmsim` directly. The application-facing `everruns::Model::simulated` and `Model::simulated_with_config` APIs are unchanged. ## Features | 0.17 | 0.18 | | -------------------------------------- | ------------------------------------------------------------------------- | | `everruns-core/sqlx` | removed, use `everruns-provider` with `features = ["sqlx"]` | | `everruns-core/embedded-platform-docs` | removed, it gated nothing; use `everruns-platform/embedded-platform-docs` | | `everruns-platform/sqlx` | removed, it forwarded to core’s and nothing enabled it | | `everruns-core/llm-tests` | removed, use the `everruns-llm-tests` package for live provider tests | `everruns-core` now has an empty default feature set. OpenAPI derives remain available only with `features = ["openapi"]`; structural outlines remain available only with `features = ["tree-sitter-outlines"]`. Neither subtree is present in a default core build. Concrete provider protocol and utility-model implementations also moved to their effectful owners: | 0.17 (`everruns_core::`) | 0.18 | | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `OpenAIProtocolChatDriver`, `openai_protocol` | `everruns_provider::` | | `OpenResponsesProtocolChatDriver`, `openresponses_protocol` | `everruns_provider::` | | `driver_helpers`, `stream_reconnect` | `everruns_provider::` | | `OpenAiUtilityLlmService` (now `ProviderUtilityLlmService`), `SystemUtilityLlmConfig`, `UTILITY_OPENAI_API_KEY_ENV` | `everruns_host::` with `features = ["utility-llm"]` | Core no longer initializes Rustls. Provider HTTP clients install the workspace crypto provider when they are first constructed, while server, worker, and CLI startup owners install it eagerly. Custom binaries that combine TLS stacks can depend on `everruns-provider` with `features = ["tls-aws-lc-rs"]` and call `everruns_provider::install_default_crypto_provider()` once during startup; the call is idempotent and safe under concurrent initialization. ## Provider and typed-ID imports Provider-owned modules are no longer compatibility-exported by `everruns-core`. Low-level consumers must add `everruns-provider` directly. This keeps credentials and concrete driver assembly out of the neutral kernel and makes the dependency owner visible in `Cargo.toml`. There are two common compiler-error shapes: 1. **The module moved to another crate.** Add that crate and change the prefix. 2. **The module stayed public, but its root convenience re-export was removed.** Keep the dependency and qualify the symbol through its module. The second case produces the misleading-looking `no X in the root` error. It does not necessarily mean the type moved. These replacements are deliberately literal so they can be applied with ordinary search-and-replace: | before | after | | ---------------------------------- | ---------------------------------------------------- | | `everruns_core::ProviderStore` | `everruns_core::provider_resolution::ProviderStore` | | `everruns_core::SessionStore` | `everruns_core::execution_loading::SessionStore` | | `everruns_core::MessageRetriever` | `everruns_core::message_retriever::MessageRetriever` | | `everruns_core::SessionFileSystem` | `everruns_core::session_files::SessionFileSystem` | | 0.17 core path | 0.18 direct path | | --------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `everruns_core::driver_registry::*` | `everruns_provider::driver_registry::*` | | `everruns_core::model::*` | `everruns_provider::model::*` | | `everruns_core::model_profiles::*` | `everruns_provider::model_profiles::*` | | `everruns_core::model_spec::ModelSpec` | `everruns_provider::model_spec::ModelSpec` | | `everruns_core::provider::*` | `everruns_provider::provider::*` | | `everruns_core::runtime_provider::*` | `everruns_provider::runtime_provider::*` | | `everruns_core::typed_id::*` | `everruns_provider::typed_id::*` | | `everruns_core::error::*` | `everruns_provider::error::*` | | `everruns_core::tool_types::*` | `everruns_provider::tool_types::*` | | `everruns_core::capability_types::{CapabilityId, CapabilityRef, CapabilityError}` | `everruns_capability::{CapabilityId, CapabilityRef, CapabilityError}` | | `everruns_core::AgentCapabilityConfig` | `everruns_capability::CapabilityRef` | | core plugin capability ID/validation helpers | the same symbol in `everruns_capability` | | `everruns_core::ExecutionPhase` or `message::ExecutionPhase` | `everruns_provider::execution_phase::ExecutionPhase` | | `everruns_core::ToolResultImage` or `tools::ToolResultImage` | `everruns_provider::tool_types::ToolResultImage` | | other root-level provider symbols | the same root symbol in `everruns_provider` | The credential-bearing `everruns_core::ResolvedModel` is removed. Store and transport boundaries now resolve two separate values: * `ModelSpec`, safe to serialize and pass through the kernel; and * a host-owned runtime `Provider` (or internal `ProviderConfig`) containing endpoint and authentication state. `ProviderStore::get_model_spec` and `get_default_model_spec` return only the first value. Hosts obtain provider configuration separately and join it only while constructing a non-serializable driver/provider execution value. `ProviderStore::get_provider_config` no longer has a default implementation. Every custom host must state where credentials live: return its resolved `ProviderConfig`, or explicitly return `None` when the provider was registered directly in the host registry or is selected but not configured. A missing credential no longer prevents turn-context/command assembly; the constructed driver rejects the first model/list/compact operation locally, before network I/O. This keeps recovery commands reachable without turning an empty token into an outbound authorization header. ## Simulator and compaction naming `LlmSimRuntimeExt::llm_sim` now only registers/replaces the simulator provider. It never changes the selected model. Existing compact test setups that relied on implicit selection should use the explicit name:
```diff
builder.llm_sim(config)
builder.llm_sim_as_default(config)
```
When a builder already calls `default_model(...)`, keep `.llm_sim(config)`; the selected model is preserved regardless of method order. `everruns_builtins::CompactionConfig` is the sole application-facing policy builder. The expanded implementation value previously available as `everruns_builtins::compaction::CompactionConfig` is now `everruns_builtins::compaction::RuntimeCompactionConfig` (also re-exported at the crate root). This makes an unqualified `CompactionConfig` unambiguous. The public core test/backend conveniences are gone as well: | removed core value | replacement | | ----------------------------------- | ------------------------------------------------------------------- | | `EchoTool`, `FailingTool` | define the small test `Tool` locally, or use test-support executors | | `InMemoryCompactionCheckpointStore` | `everruns_host::InMemoryCompactionCheckpointStore` | ## What deliberately did not move Worth knowing so you do not go looking: * **`SessionTask`, `TaskMessage`** and the task registry stay in `everruns_core`. They are turn-execution vocabulary, `wake_queue` decides mid-turn wakes from a task’s wake policy, and they appear in the canonical `task.created` / `task.updated` / `task.message.*` event payloads. * **`SessionSchedule`, `SessionScheduleStore`** stay. A portable built-in (`usage_limit_auto_continue`) schedules an auto-resume after a provider usage limit, and it sits below platform in the dependency graph. * Schedule quota and minimum-interval environment variables are no longer read by core. Local/server adapters resolve deployment policy and call the parameterized core validation helpers. * **`SessionResourceRegistry`** stays. `resource_ownership` and the portable skills capabilities consume it. * **`SessionFileSystem`, `SessionStorageStore`** and the other neutral store contracts stay. Core owns the contract; hosts own the backend. The rule these follow: whether something belongs in the kernel is decided by whether a portable execution path consumes it during a turn, not by whether it is persisted. All four above are persisted, and all four are essential for execution. ## Getting unstuck If a symbol is not in these tables, import it from the crate that defines it; `everruns-core` no longer acts as a compatibility facade for provider-owned APIs. Framework applications can continue to prefer the higher-level `everruns` facade. The crate-level docs on `everruns-core` record where each remaining family lives and why.
---
# Orchestrate multi-agent pipelines
> Chain multiple Everruns agents together by passing output from one session into another.
Source:
A common pattern is splitting work across specialised agents, a researcher gathers facts, a writer turns them into prose, an editor polishes the result. Each is a separate agent and session; the application chains them. This is the simplest orchestration pattern: no shared state, no subagent spawning, just sequential calls. ## Pipeline skeleton
```python
import asyncio
from everruns_sdk import Everruns
async def run_pipeline(client: Everruns, topic: str) -> str:
researcher = await client.agents.create(
name="Researcher",
system_prompt="Research the given topic thoroughly. Write detailed notes.",
capabilities=["web_fetch", "session_file_system"],
)
research_session = await client.sessions.create(agent_id=researcher.id)
await client.messages.create(research_session.id, f"Research {topic}")
research_output = await collect_final_text(client, research_session.id)
writer = await client.agents.create(
name="Writer",
system_prompt="Write clear, well-structured technical articles.",
)
writer_session = await client.sessions.create(agent_id=writer.id)
await client.messages.create(
writer_session.id,
f"Write a blog post based on this research:\n\n{research_output}",
)
return await collect_final_text(client, writer_session.id)
async def collect_final_text(client: Everruns, session_id: str) -> str:
final_text: str | None = None
async for event in client.events.stream(session_id):
if event.type == "output.message.completed":
message = event.data.get("message", {})
final_text = "\n".join(
p["text"] for p in message.get("content", []) if p.get("type") == "text"
)
elif event.type == "turn.failed":
raise RuntimeError(event.data.get("error", "turn failed"))
elif event.type == "turn.cancelled":
raise RuntimeError("turn cancelled")
elif event.type == "turn.completed":
break
if not final_text:
raise RuntimeError("turn completed without producing a final message")
return final_text
```
## Cleanup Each session and agent persists until you explicitly delete it. For ephemeral pipelines, clean up at the end:
```python
for sid in (research_session.id, writer_session.id):
await client.sessions.delete(sid)
for aid in (researcher.id, writer.id):
await client.agents.delete(aid)
```
For pipelines you’ll re-run, *don’t* recreate the agents, create them once, store the IDs, and reuse them. ## When to use subagents instead If one agent needs to delegate to another *during a turn*, use the [Sub Agents capability](https://docs.everruns.com/capabilities/sub-agents/) instead of an application-level pipeline. Subagents run inside the parent session and emit `subagent.*` events that the parent agent receives as tool results. Pick application-level pipelines when: * The stages are clearly separated and you want independent observability per stage. * Stages run at different cadences (e.g., scheduled research → on-demand writeup). * You want to reuse intermediate output across multiple downstream agents. Pick subagents when: * The parent agent decides at runtime which subagent to call. * The work feels like a single user request, not a pipeline. ## See also * [Sub Agents capability](https://docs.everruns.com/capabilities/sub-agents/) * [Stream events](https://docs.everruns.com/how-to/stream-events/)
---
# Package an agent skill
> Author a SKILL.md, bundle scripts and references, and place it in the session workspace so agents can discover and activate it on demand.
Source:
Skills are portable instruction packages following the [Agent Skills](https://agentskills.io/) open spec. They use progressive disclosure: the agent sees only names and descriptions until it activates a skill, at which point the full instructions load. This guide creates a skill in the session workspace. To share a skill across agents organization-wide, see [Publish a skill to the registry](https://docs.everruns.com/how-to/publish-a-skill-to-the-registry/). ## SKILL.md format Every skill is a directory containing a `SKILL.md` with YAML front matter:
```yaml
---
name: csv-analyzer
description: Analyze CSV files and generate summary reports.
metadata:
category: data-processing
version: "1.0"
---
# CSV Analyzer
## When to Use
Activate this skill when a user provides a CSV file and wants summary statistics.
## Instructions
1. Read the CSV file using the `read_file` tool
2. Run `scripts/analyze.py` via `bash`
3. Present findings to the user
```
Required front-matter fields: | Field | Constraint | | ------------- | -------------------------------------------- | | `name` | 1–64 chars, lowercase alphanumeric + hyphens | | `description` | 1–1024 chars, describes when to activate | Optional fields: `metadata`, `license`, `compatibility`. ## Bundle scripts and references Skills can include arbitrary files. The agent accesses them via the session filesystem after activation:
```plaintext
/.agents/skills/csv-analyzer/
├── SKILL.md
├── scripts/
│ └── analyze.py
└── references/
└── REFERENCE.md
```
After activation, bundled files mount at `/skills/csv-analyzer/` in the session VFS and the agent reads them with the existing `read_file` / `list_files` tools. ## Enable the skills capability Add the built-in `skills` capability to the agent so it can discover and activate skills from the workspace:
```bash
curl -X POST http://localhost:9300/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"name": "Data Analyst",
"capabilities": [
{ "ref": "skills" },
{ "ref": "session_file_system" }
]
}'
```
`skills` depends on `session_file_system`; the platform pulls it in automatically. ## How activation looks to the agent With the capability enabled, the system prompt includes an `` block (\~100 tokens per skill):
```xml
csv-analyzer
Analyze CSV files and generate summary reports.
```
When the user’s task matches, the agent calls `activate_skill`:
```json
{ "name": "activate_skill", "arguments": { "name": "csv-analyzer" } }
```
The tool returns the full SKILL.md instructions wrapped in `` tags. The agent now has the detailed instructions in context and can run the bundled scripts. ## Test the skill 1. Start a session with an agent that has the `skills` capability enabled. 2. Write the skill files to `/.agents/skills//` in the session. 3. Send a message that matches the skill’s “When to Use” criteria. 4. Watch the event stream for an `activate_skill` tool call. ## See also * [Agent Skills feature](https://docs.everruns.com/features/skills/) * [Skills Registry](https://docs.everruns.com/features/skills-registry/), share skills across the organization. * [Publish a skill to the registry](https://docs.everruns.com/how-to/publish-a-skill-to-the-registry/)
---
# Publish a skill to the registry
> Upload a SKILL.md or ZIP archive to the organization-wide Skills Registry so any agent can use it as a capability.
Source:
The Skills Registry stores skills at the organization level. Registry skills persist across sessions and can be assigned to any agent as a capability with ID `skill:{uuid}`. For workspace-scoped skills, see [Package an agent skill](https://docs.everruns.com/how-to/package-a-skill/) instead. ## From SKILL.md If your skill is a single Markdown file, post it directly:
```bash
curl -X POST http://localhost:9300/api/v1/skills \
-H "Content-Type: application/json" \
-d '{
"skill_md": "---\nname: hello-world\ndescription: A simple greeting skill.\n---\n\n# Hello World\n\nGreet the user warmly."
}'
```
The response includes the skill ID:
```json
{ "id": "skill_550e8400-e29b-41d4-a716-446655440000", "name": "hello-world", ... }
```
## From a ZIP archive For skills with bundled scripts, references, or assets:
```bash
curl -X POST http://localhost:9300/api/v1/skills/upload \
-F "file=@csv-analyzer.zip"
```
Archive layout:
```plaintext
csv-analyzer/
├── SKILL.md
├── scripts/analyze.py
└── references/REFERENCE.md
```
The top-level directory name is informational; the skill `name` comes from the SKILL.md front matter and must be unique per organization. ## Validate first Validate without creating:
```bash
curl -X POST http://localhost:9300/api/v1/skills/validate \
-H "Content-Type: application/json" \
-d '{"skill_md": "---\nname: my-skill\ndescription: Does things.\n---\n\n# Instructions"}'
```
Response:
```json
{ "valid": true, "name": "my-skill", "description": "Does things.", "warnings": [] }
```
## Assign to an agent Registry skills appear in the capability system as virtual capabilities with ID `skill:{uuid}`:
```bash
curl -X POST http://localhost:9300/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"name": "Analyst Agent",
"capabilities": [
{ "ref": "skill:550e8400-e29b-41d4-a716-446655440000" },
{ "ref": "session_file_system" }
]
}'
```
`session_file_system` is pulled in automatically as a dependency. ## Update or delete
```bash
# Update
curl -X PATCH http://localhost:9300/api/v1/skills/$SKILL_ID \
-H "Content-Type: application/json" \
-d '{"skill_md": "..."}'
# Delete
curl -X DELETE http://localhost:9300/api/v1/skills/$SKILL_ID
```
Deleting a skill hides it from capability listings. Agents that reference it via `ref: "skill:..."` will still resolve until you update them. ## Security notes * Archive uploads are validated for path traversal, ZIP bombs, and size limits. * Skill instructions are returned as tool results, not injected into the system prompt, they don’t bypass capability isolation. * Skill names are unique per organization. * Disabled skills are hidden from listings. ## See also * [Skills Registry feature](https://docs.everruns.com/features/skills-registry/) * [Package an agent skill](https://docs.everruns.com/how-to/package-a-skill/), author the SKILL.md.
---
# Publish an Agent to Slack
> Add a Slack endpoint to an Agent, publish it, connect a Slack workspace, and verify the first message.
Source:
This guide deploys an Agent as a Slack bot through an Agent-owned endpoint. For Slack scopes, manual setup, and troubleshooting, see [Slack Integration](https://docs.everruns.com/integrations/slack/). ## Prerequisites * An active Agent. * A public HTTPS Everruns origin configured through `PUBLIC_APP_URL`. * Permission to install an app in a Slack workspace. ## Add the Endpoint 1. Open the Agent and select **Integrations**. 2. Select **Add endpoint**, then select **Slack**. 3. Choose a session strategy and reply mode. 4. Leave the Slack credentials empty and select **Save endpoint**. ## Choose a session strategy `session_strategy` controls how incoming Slack messages map to Everruns sessions: | Strategy | Behaviour | Use when | | ---------------------- | ------------------------------------ | --------------------------------------------------------------- | | `per_thread` (default) | Each Slack thread is its own session | Support bots, Q\&A, each thread is a separate conversation | | `per_channel` | One session per channel | Persistent channel assistant, context shared across the channel | | `per_user` | One session per user | Personal assistant, each user has their own ongoing chat | ## Publish and Connect 1. Select **Publish** in the endpoint editor. 2. Select **Connect to Slack**. 3. Approve Slack’s consent screen and choose a workspace. 4. If one-click setup is unavailable, return to **Integrations**, expand the endpoint, and select **Create Slack app**. Copy the resulting signing secret and bot token back through **Configure**. Publish first because Slack verifies the manifest’s endpoint Request URL when it creates the Slack app. New installs use `/v1/e/{endpoint_id}/slack/events`. ## Verify 1. In Slack, enter `/invite @botname` in a channel. 2. Mention the bot. 3. Return to the Agent’s **Integrations** tab and expand the Slack endpoint. 4. Confirm that the checklist records the first message. To stop new Slack messages without deleting the configuration, select **Unpublish** on this endpoint. Existing sessions remain available. ## See also * [Slack Integration](https://docs.everruns.com/integrations/slack/), including scopes, manual setup, and troubleshooting. * [Agent Versions](https://docs.everruns.com/features/agent-versions/), including endpoint version selection.
---
# Share knowledge with Open Knowledge Format (OKF)
> Import and export Knowledge Bases as Open Knowledge Format bundles, portable markdown-with-frontmatter that any OKF consumer or agent can read, managed like code.
Source:
[Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) is a vendor-neutral interchange format for the metadata and curated context around your data: a bundle is just a directory of markdown files with YAML frontmatter, shippable as a tarball or git repo. everruns Knowledge Bases speak OKF on both ends, so you can keep curated knowledge as code, ingest bundles produced elsewhere, and hand your agents’ working set to any OKF consumer. This guide covers importing a bundle into a Knowledge Base, the OKF↔entry mapping, and exporting a bundle back out. ## What a bundle looks like
```plaintext
sales/
├── index.md # optional navigation (reserved file)
├── tables/
│ └── orders.md # one concept document per file
└── metrics/
└── revenue.md
```
A concept document is frontmatter plus markdown body:
```markdown
---
type: BigQuery Table
title: Orders
description: One row per completed customer order.
resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders
tags: [sales, revenue]
---
# Schema
| Column | Type | Description |
|--------|------|-------------|
| `order_id` | STRING | Globally unique order identifier. |
```
Only `type` is required. `index.md` and `log.md` are reserved navigation/history files and never become entries. ## Import a bundle `POST /v1/knowledge-bases/{kb_id}/okf_import` accepts either inline files or a base64-encoded `.tar.gz` bundle. It is **idempotent**: re-importing an updated bundle converges the Knowledge Base to the bundle’s state without creating duplicates. Create a Knowledge Base, then import inline files:
```bash
KB_ID=$(curl -s -X POST "http://localhost:9300/api/v1/knowledge-bases" \
-H "Content-Type: application/json" \
-d '{"name": "Sales Knowledge"}' | jq -r .id)
curl -s -X POST "http://localhost:9300/api/v1/knowledge-bases/$KB_ID/okf_import" \
-H "Content-Type: application/json" \
-d '{
"files": [
{
"path": "tables/orders.md",
"content": "---\ntype: BigQuery Table\ntitle: Orders\nresource: https://example.com/orders\ntags: [sales]\n---\n# Schema\nOne row per order.\n"
}
]
}'
```
Or import a tarball you already have:
```bash
curl -s -X POST "http://localhost:9300/api/v1/knowledge-bases/$KB_ID/okf_import" \
-H "Content-Type: application/json" \
-d "{\"bundle_base64\": \"$(base64 -w0 sales-bundle.tar.gz)\"}"
```
The response summarizes the run:
```json
{ "created": 1, "updated": 0, "skipped": 0, "pruned": 0, "warnings": [] }
```
### Keeping a Knowledge Base in sync Re-run the same import whenever the bundle changes, matched entries update in place (keyed on `resource`, or the bundle path when there is no `resource`). To make the Knowledge Base a strict mirror of the bundle, pass `"prune": true`; entries that came from a previous import and are absent from the new bundle are removed.
```bash
curl -s -X POST "http://localhost:9300/api/v1/knowledge-bases/$KB_ID/okf_import" \
-H "Content-Type: application/json" \
-d "{\"prune\": true, \"bundle_base64\": \"$(base64 -w0 sales-bundle.tar.gz)\"}"
```
Import is tolerant by design (per OKF conformance): reserved files are skipped, and malformed documents are reported in `warnings` rather than failing the whole bundle. ## How OKF maps onto entries | OKF frontmatter | Knowledge entry | | --------------- | ----------------------------------------------------------------------------------------------------- | | `type` | `kind` (`note`/`table`/`business`/`query`/`runbook`); the raw `type` is preserved for faithful export | | `title` | `title` (falls back to the filename) | | `description` | folded into the body lead | | `resource` | `resource` | | `tags` | `tags` (lowercased) | | markdown body | `body` | `type` is matched case-insensitively: anything containing *table/dataset/view* → `table`, *metric/business/kpi/definition* → `business`, *query/sql* → `query`, *playbook/runbook/procedure* → `runbook`, otherwise `note`. ## Export a bundle `GET /v1/knowledge-bases/{kb_id}/okf_export` streams a conformant OKF bundle as a gzipped tarball, including a root `index.md` that declares the format version.
```bash
curl -s "http://localhost:9300/api/v1/knowledge-bases/$KB_ID/okf_export" \
-o sales-bundle.tar.gz
tar tzf sales-bundle.tar.gz
```
Export reconstructs each entry’s frontmatter from the preserved raw `type` (or a default for its `kind`), its `resource`, tags, and timestamp, under a `kind`-derived directory. Export → import into a fresh Knowledge Base reproduces the same entries. ## Agents read OKF too The `data_knowledge` capability mounts a readonly `/knowledge/` scaffold shaped as an OKF bundle (frontmatter + `index.md` navigation). Agents on the **Data Analyst** harness read it as ground truth before writing SQL. Import an OKF bundle into a Knowledge Base, and that curated context is available to your agents and portable to any other OKF consumer. See also: [Data Analyst harness](https://docs.everruns.com/built-ins/harnesses/data-analyst/), and `knowledge/runtime-resources/okf-adoption.md` for the design intent.
---
# Stream events with the SDK
> Consume the SSE event stream from the Python SDK with automatic reconnection, heartbeat detection, and event filtering.
Source:
The Python SDK’s `client.events.stream(session_id)` returns an async iterator over typed events. It handles reconnection, heartbeat-based stale detection, and resumption with `since_id` automatically. ## Basic stream
```python
async for event in client.events.stream(session.id):
if event.type == "output.message.delta":
print(event.data.get("delta", ""), end="", flush=True)
elif event.type == "turn.completed":
print()
break
elif event.type == "turn.failed":
print(f"\n[failed: {event.data.get('error')}]")
break
```
## Tool visibility To show what the agent is doing while it works, listen for `tool.started` and `tool.completed`:
```python
async for event in client.events.stream(session.id):
if event.type == "tool.started":
tool_call = event.data.get("tool_call", {})
print(f" [tool] {tool_call.get('name')}")
elif event.type == "tool.completed":
status = "ok" if event.data.get("success") else "error"
print(f" [tool] {event.data.get('tool_name')}: {status}")
elif event.type == "turn.completed":
break
```
## Get the full final message `output.message.completed` carries the complete final message after streaming finishes:
```python
async for event in client.events.stream(session.id):
if event.type == "output.message.completed":
message = event.data.get("message", {})
for part in message.get("content", []):
if part.get("type") == "text":
print(part["text"])
elif event.type == "turn.completed":
break
```
## What the SDK handles for you * **Reconnection.** The control plane cycles SSE connections every 5 minutes; the SDK reconnects transparently using `since_id`. * **Stale detection.** The server sends a heartbeat every 30s; the SDK treats >45s of silence as a dead connection and reconnects. * **Backoff.** Network errors trigger exponential backoff with jitter. * **Typing.** Each event has `.type` and `.data` attributes parsed from SSE. ## Resuming with `since_id` While a stream is open the SDK manages reconnection internally. You only need `since_id` when restarting your application and resuming from a previously recorded event ID:
```python
# Persisted somewhere — file, DB, etc.
last_seen_id = load_cursor()
async for event in client.events.stream(session.id, since_id=last_seen_id):
handle(event)
save_cursor(event.id) # so the next restart can resume from here
```
Inside the loop the SDK already remembers the last ID it yielded and reconnects with it on transient failures, `save_cursor` here is for *application restart* recovery, not per-iteration SDK state. ## See also * [Event Reference](https://docs.everruns.com/event-reference/), all event types. * [Events as the primary store](https://docs.everruns.com/explanation/events/), why the protocol is shaped this way. * [Consume events via raw SSE](https://docs.everruns.com/how-to/consume-events-via-sse/), non-SDK clients.
---
# Use AGENTS.md for project instructions
> Inject project-level context, coding style, build commands, architecture notes, into an agent's leading user message by enabling the AGENTS.md capability.
Source:
`AGENTS.md` is an emerging open standard for providing project-level instructions to AI agents, backed by OpenAI, Google, Cursor, Sourcegraph, and others. Everruns ships it as the default file for its built-in agent instructions capability, which re-reads configured files on every turn. ## Enable the capability
```bash
curl -X PATCH http://localhost:9300/api/v1/agents/$AGENT_ID \
-H "Content-Type: application/json" \
-d '{
"capabilities": [
{ "ref": "agent_instructions" },
{ "ref": "session_file_system" }
]
}'
```
`session_file_system` isn’t required, but pairing it with `agent_instructions` lets the agent edit `AGENTS.md` itself. To also read another instruction file, configure `files` on the capability:
```bash
curl -X PATCH http://localhost:9300/api/v1/agents/$AGENT_ID \
-H "Content-Type: application/json" \
-d '{
"capabilities": [
{
"ref": "agent_instructions",
"config": {
"files": ["AGENTS.md", "CLAUDE.md"]
}
},
{ "ref": "session_file_system" }
]
}'
```
## Write the file Drop a plain Markdown file at `AGENTS.md` in the session workspace root for repo-wide rules. Add nested files (for example `docs/AGENTS.md`) for subdirectory-scoped rules — deeper files override shallower ones on conflict, and sibling subtrees never see each other’s files:
```markdown
## Project: Acme API
REST API built with Rust + Axum. PostgreSQL for storage.
## Style
- snake_case for variables and functions
- PascalCase for types
- Keep functions under 50 lines
## Build & Test
cargo build
cargo test --all-features
cargo clippy -- -D warnings
## Architecture
- `src/api/` — HTTP handlers
- `src/domain/` — Business logic
- `src/db/` — Database queries
## Commits
Use conventional commits: `feat(scope): description`
```
There are no required sections. Write whatever a new contributor would need to know. ## How it lands in the prompt Every turn the model sees, top-to-bottom: 1. **System prompt**: harness safety instructions, tool guidance, role. 2. **Conversation context**: your resolved `AGENTS.md` hierarchy, broadest scope first. 3. **Conversation history and your message**. Project files never enter the system prompt — system instructions always win on conflict, and your explicit message wins over project files. ## Limits and dynamics * Content is capped at **32 KiB** (32,768 bytes) per file (excess truncated with a warning), plus a **128 KiB total budget** per turn across the hierarchy. * Configured files are resolved from the filesystem root down to the working directory on every turn. Edits during a session apply on the next turn, no restart needed. * If a configured file doesn’t exist, the agent operates normally without it. ## Other tools’ instruction files Everruns reads `AGENTS.md` by default at every hierarchy level. Add other file names to `files` when an agent should also resolve `CLAUDE.md`, `.cursorrules`, or `.github/copilot-instructions.md` per level. ## See also * [AGENTS.md capability reference](https://docs.everruns.com/capabilities/agent-instructions/) * [Equip an agent with tools](https://docs.everruns.com/how-to/equip-agents-with-tools/), adding capabilities in general.
---
# Tutorial: Build your first agent
> Create an Everruns agent in Python, send a message, and stream the response.
Source:
By the end of this tutorial you will have created an Everruns agent, started a session, sent a message, and streamed the response — using the official Python SDK. This is a *tutorial*: a guided lesson. We make all the choices for you. When you want to do something different (different language, different tools, different patterns), follow up with the [How-to guides](https://docs.everruns.com/how-to/). If you prefer a Jupyter notebook walkthrough, start with [Run an Agent](https://docs.everruns.com/tutorials/run-an-agent/) instead. ## What you’ll build A research-assistant agent with web access. You’ll send it a topic and watch it answer. ## What you need * A running Everruns instance. The easiest path is the [Docker Compose quickstart](https://docs.everruns.com/getting-started/docker-compose/). * Python 3.10 or newer. * An LLM provider configured (an OpenAI or Anthropic API key set in the Everruns UI).
```bash
pip install everruns-sdk
```
## Step 1 — Connect to the server The `Everruns` client reads `EVERRUNS_API_KEY` and `EVERRUNS_API_URL` from the environment. For a local `just start-dev` deployment, API key `"dev"` works.
```python
import asyncio
from everruns_sdk import Everruns
client = Everruns(api_key="dev", base_url="http://localhost:9300/api")
```
## Step 2 — Create an agent An **agent** is the configuration: a name, a system prompt, and a set of capabilities (tools).
```python
async def main():
agent = await client.agents.create(
name="Research Assistant",
system_prompt=(
"You are a research assistant. When given a topic, you:\n"
"1. Fetch relevant web pages\n"
"2. Save your notes to /workspace\n"
"3. Produce a concise summary"
),
capabilities=["web_fetch", "session_file_system", "current_time"],
)
print(f"Agent: {agent.id}")
asyncio.run(main())
```
The capabilities give the agent its tools: `web_fetch` to retrieve URLs, `session_file_system` for an isolated workspace, `current_time` to know what day it is. ## Step 3 — Start a session A **session** is a working conversation with the agent. It owns the conversation history, an isolated virtual filesystem, and key/value storage.
```python
session = await client.sessions.create(
agent_id=agent.id,
title="Research: Durable Execution",
)
print(f"Session: {session.id}")
```
## Step 4 — Send a message Sending a user message queues a durable workflow that runs the agent’s reason–act loop. The call returns immediately — the response arrives as events.
```python
await client.messages.create(
session.id,
"Research durable execution engines. What are the main approaches?"
)
```
## Step 5 — Stream the response `client.events.stream(...)` returns an async iterator over typed events. It handles SSE reconnection, heartbeats, and resumption for you.
```python
async for event in client.events.stream(session.id):
if event.type == "output.message.delta":
print(event.data.get("delta", ""), end="", flush=True)
elif event.type == "tool.started":
tool = event.data.get("tool_call", {}).get("name", "")
print(f"\n [tool] {tool}")
elif event.type == "turn.completed":
print("\n[done]")
break
elif event.type == "turn.failed":
print(f"\n[failed: {event.data.get('error')}]")
break
```
You’ll see the agent’s reasoning stream token-by-token, with `[tool]` markers each time it fetches a URL or writes to its workspace. ## Step 6 — Put it together Here’s the complete program:
```python
import asyncio
from everruns_sdk import Everruns
async def main():
client = Everruns(api_key="dev", base_url="http://localhost:9300/api")
agent = await client.agents.create(
name="Research Assistant",
system_prompt=(
"You are a research assistant. When given a topic, you:\n"
"1. Fetch relevant web pages\n"
"2. Save your notes to /workspace\n"
"3. Produce a concise summary"
),
capabilities=["web_fetch", "session_file_system", "current_time"],
)
session = await client.sessions.create(
agent_id=agent.id,
title="Research: Durable Execution",
)
await client.messages.create(
session.id,
"Research durable execution engines. What are the main approaches?",
)
async for event in client.events.stream(session.id):
if event.type == "output.message.delta":
print(event.data.get("delta", ""), end="", flush=True)
elif event.type == "tool.started":
tool = event.data.get("tool_call", {}).get("name", "")
print(f"\n [tool] {tool}")
elif event.type == "turn.completed":
print("\n[done]")
break
elif event.type == "turn.failed":
print(f"\n[failed: {event.data.get('error')}]")
break
await client.close()
asyncio.run(main())
```
Save as `tutorial.py` and run:
```bash
python tutorial.py
```
You should see the agent stream a research response, fetching a few URLs along the way. ## What just happened You configured an agent (long-lived), started a session (per-conversation), and consumed the event stream (per-turn). Those three layers — configuration, runtime, data — are the spine of every Everruns application. The agent loop you watched run is the **reason–act cycle**. The model reasons (returns text or tool calls), tools execute, results feed back in, repeat until the model produces a final answer. See [The agentic loop](https://docs.everruns.com/explanation/agentic-loop/) for the design. ## Next steps Common follow-ups, each as a focused how-to: * [Equip an agent with tools](https://docs.everruns.com/how-to/equip-agents-with-tools/) — explore the full capability catalog. * [Define agents as files](https://docs.everruns.com/how-to/define-agents-as-files/) — version-control your agent definitions. * [Stream events with the SDK](https://docs.everruns.com/how-to/stream-events/) — richer streaming patterns. * [Handle errors and cancel turns](https://docs.everruns.com/how-to/handle-errors-and-cancellation/) — what to do when things go wrong. * [Orchestrate multi-agent pipelines](https://docs.everruns.com/how-to/orchestrate-multi-agent-pipelines/) — chain agents together. * [Publish an agent as a Slack app](https://docs.everruns.com/how-to/publish-to-slack/) — deploy to a channel. For background, read [Core concepts](https://docs.everruns.com/explanation/concepts/) and [The agentic loop](https://docs.everruns.com/explanation/agentic-loop/).