Agent tools
The platform tools every seat can call, how they are authorised, and the fake harness script language used in tests.
Agents interact with the platform only through tools. Every call goes to the platform service, which authenticates the seat from its identity token. The platform checks the seat's current execution lease and its capability manifest, built from your grants, before acting. A seat cannot claim to be someone else, and a tool is never less restricted because it was called from a shell instead of through MCP.
Transports
| Transport | How | Names |
|---|---|---|
| MCP (Claude Code) | steadmesh-tools mcp runs as a stdio MCP server inside the seat | Dots become underscores: memory_search, or mcp__steadmesh__memory_search in Claude Code |
| CLI | steadmesh-tools call <name> '<json>', steadmesh-tools list | Dotted names. Exit code 1 means a tool error; 4 means the caller's lease was fenced. |
| HTTP | GET /v1/tools, POST /v1/tools/{name} on the platform service | Dotted names |
Each seat sees only the tools it can use. Results are JSON, capped at 16 KiB, and lists are paginated with cursor.
Identity and status
| Tool | Arguments | Behaviour |
|---|---|---|
self | none | The seat's identity, instruction sources, memory stores, recipients and connections. |
status | seats? (up to 20) | Technical state (running, stopped, queued work) of this seat and the seats it talks to. Representatives use it to explain real delays instead of inventing progress. |
handoff.update | objective, unresolved[], record_ids[], pending_message_ids[], operation_ids[], notes | Replaces the seat's portable handoff, which is restored after restarts and harness changes. |
Memory
Memory stores are scopes the platform enforces. Inside a store, records are addressed like files: each has a path that is unique among the store's live records, for example notes/plan.md or decisions/auth.md. Agents decide how to organise them. Search and listing are authorised before they run, so counts, paths and snippets from stores a seat cannot read are never revealed.
| Tool | Arguments | Behaviour |
|---|---|---|
memory.stores | none | Stores available to this seat and the operations allowed on each. |
memory.list | store?, prefix?, order_by (name, created_at, updated_at), order, max_results (≤100), cursor | Records under a path prefix: path, kind, size, revision and author, without contents. |
memory.read | store + path, or record_id; revision?, start_line?, stop_line? | A record or a past revision. Line numbers are 1-based and inclusive; negative ones count back from the last line. Long reads return next_start_line. |
memory.search | query (full text, web-search syntax) or queries[] (literal substrings) with match_mode (any, all_on_same_line, all_within_lines + line_count), case_sensitive, context_lines (≤5); stores[], path_prefix, tags[], max_results, cursor | Full-text results with snippets, or literal matches line by line with surrounding lines. |
memory.write | store, path, text, tags[]?, source_refs[]?, expected_revision? | Creates the note at the path, or replaces its text. With expected_revision it is a compare-and-swap (0 means it must not exist yet): if someone else changed it first the result is a conflict with the current revision, and nothing is overwritten. Replacing needs the revise permission. |
memory.append | store, path, text | Appends to a note, creating it if needed. Concurrent appends never overwrite each other, which suits logs and running notes. |
memory.archive | locator, expected_revision | Hides a note from listing and search and keeps its history. |
memory.publish | locator, destination, destination_path?, text? (a summary) | An attributed copy into another store the seat can write. Provenance is recorded, and access to the original never changes. |
memory.history | locator, max_results, cursor | Revisions with their authors and lease generations, newest first. |
Paths under work/ hold work items (below). They can be read and searched like any record, but only the work tools change them, so ownership and status rules cannot be bypassed with memory.write.
Work items (optional)
Shared memory and messages are enough to coordinate. When explicit ownership helps, for example with several people on one piece of work or work that runs for days, a seat can record a work item in a shared store. It has an objective, acceptance criteria, an owner, a status, a plan, dependencies and evidence. Its id is <store>/W-<n>. Nothing requires a work item: no workflow is imposed and no status has to be passed through. Every change is revision-checked.
| Tool | Arguments | Behaviour |
|---|---|---|
work.create | store, objective, acceptance[], depends_on[], links[], parent?, owner? | Creates the item as ready, optionally assigned. Personal stores are refused. |
work.get, work.list | work_id; or store?, owner? (seat key or me), status? | The item with its log and current revision, or summaries with the current step. |
work.claim | work_id, expected_revision, note? | Takes an unowned item. If someone else owns it, the result is a conflict naming the owner; nothing changes. |
work.update_plan | work_id, expected_revision, plan[] of {step, status} (pending, in_progress, completed), explanation? | Replaces the plan. At most one step is in progress. Only the owner, or the creator while the item is unowned, can change it. |
work.update | work_id, expected_revision, status?, note?, evidence[]?, blocker? | Anyone who can write the store may add notes and evidence. Only the owner, or the creator while unowned, changes status: ready, in_progress, in_review, blocked (needs a blocker), done (needs evidence) or cancelled. A finished item reopens as ready. |
work.release | work_id, expected_revision, note | The owner gives the item up for someone else to claim. |
A restarted seat finds its unfinished work items in the recovery section of its bootstrap context. The organisation can optionally publish work items to a tracker (work_publication, see Configuration). The tracker is a view for people. Agents never depend on it.
Messages
| Tool | Arguments | Behaviour |
|---|---|---|
messages.recipients | none | Seats this seat may message, and whether they can reply. |
messages.send | to (seat key), body (≤12 KiB), conversation_id?, correlation_id?, wake? (default true) | Sends over a declared route. By default the recipient gets a turn, and is woken if it was asleep. With wake: false the message is queued instead and handed over with the recipient's next turn, whatever starts it, for information that needs no action now. The recipient sees the sender identity the platform assigned, never anything taken from the text. Fails with inbox_full when the recipient has 1000 waiting messages. |
messages.reply | message_id or binding, body, wake? | Replies to a received message. With binding, a representative sends an unprompted update to its own human. |
messages.inbox | max_results? | Reads the messages queued with wake: false that have not been handed over yet. |
messages.history | conversation_id?, limit, cursor | Lists conversations, or reads one this seat takes part in. Human conversations are visible only to their representative. |
Automations and time
An automation is a named instruction a seat carries out for itself on a schedule: a reminder, a follow-up or a recurring check. It is stored by the platform, outside Terraform, and survives restarts. Each run queues a turn for the seat carrying the instruction, waking the seat if it has stopped.
| Tool | Arguments | Behaviour |
|---|---|---|
automations.create | name, instruction, and one schedule: in (a delay, e.g. 20m), at (local 2026-10-09T15:00 or RFC 3339), every (an interval, at least 1m) or time (HH:MM) with days? (mon…sun, weekdays, weekends, daily); timezone?, until?, max_runs? | Saves the automation and returns it with a readable schedule and its next three runs in local time with their UTC offsets. A name already used by one of the seat's live automations is a conflict naming its id, so agents update instead of duplicating. At most 50 active or paused automations per seat. |
automations.list | include_completed? | The seat's automations: schedule, status, next runs, last run and counts. |
automations.update | id; any of name, instruction, status (active/paused), the schedule fields, until, max_runs (null removes a limit) | Changes the automation. in, at or every replace the schedule; time, days and timezone adjust a time-of-day schedule and keep what is left out, so moving a weekday check to 10:00 keeps it on weekdays. Resuming does not replay runs that fell due while paused. |
automations.delete | id | Stops it for good. A run already queued still arrives. |
clock.now | timezone? | The current time in the seat's time zone or another. |
Times of day are read in the seat's time zone: the organisation's timezone (default UTC), or for a representative its human's, set on the channel binding. Every turn's envelope also carries current_time in that zone, since a message may have waited before the turn starts.
How runs behave:
- Who runs it. The seat that created it, with its own harness, model and permissions.
- Busy seat. A run is queued like any message; if the seat is busy it runs afterwards.
- Overlap. While the previous run is still waiting or in progress, the next occurrence is skipped rather than piled up, and counted in
skipped_runs. - Downtime. Occurrences missed while the platform was down are not replayed. One catch-up run says how many were missed, then the schedule resumes at its next future occurrence.
- Daylight saving.
timefollows the wall clock. A time that happens twice runs once, at the first; a time skipped when clocks go forward runs that much later (02:30 becomes 03:30).everycounts elapsed time and ignores the clock. - Ending. A one-off completes after its run.
max_runscounts runs queued; the automation completes after the last. No runs happen afteruntil. - Duplicates. Each occurrence is queued at most once, even if the scheduler retries.
- Retirement. A retiring seat's automations stop.
External connections
| Tool | Arguments | Behaviour |
|---|---|---|
connections.list | none | Connections this seat may use, with the granted operations and targets. |
connections.invoke | connection, operation, params, idempotency_key? | Runs a granted operation on a tracker or code host, such as project.create on Linear or pull_request.create on GitHub (platform delivery). Each call is recorded in the operation ledger. Reusing an idempotency key returns the recorded operation instead of repeating it. |
operations.get | id | One recorded operation: status, receipt and result. |
Linear parameters:
project.create:{name, description, content, team_ids}. Team IDs default to the connection'steam_id, anddescriptionis limited to 255 characters.project.read:{id}or{query, first}.task.write:{title, description, team_id, project_id, state_id, assignee_id, priority}. Passing anidturns a create into an update.task.read: exactly one of{id},{query}or{project_id}.comment.write:{issue_id, body}.comment.read:{issue_id, first}. The issue's comments with author and time, so agents can act on what people write in Linear.
Operation outcomes
| Status | Meaning |
|---|---|
succeeded | Applied, with an external receipt such as a project ID or issue identifier. |
failed | Rejected and not applied. |
unknown | The request may have been applied, but the response was lost. The platform tried to read the effect back and could not confirm it. It is never repeated automatically: the agent should check the tracker. Unknown operations are listed in the seat's recovery context after a restart. |
Because Linear has no idempotency keys, created records carry a marker steadmesh-op:<operation id>. After a lost response, the gateway searches for that marker to find out whether the first attempt landed.
The fake harness script language
The fake harness never calls a model. It reads each message body as a small script, which makes infrastructure tests and the quickstart deterministic. Lines that start with / are commands; a body without any commands gets the reply ack: <text>. Every tool call goes through the steadmesh-tools binary, the same path and authority a real harness has.
| Command | Effect |
|---|---|
/tool <name> [json] | Calls a platform tool and records the result as output. |
/reply <text> | Replies to the triggering message. |
/report | Replies with all output collected so far. |
/file write <path> <text>, /file read <path> | Writes or reads a file in the seat's workspace. |
/delegate <seat> <text…> | Sends the rest of the message to another seat. When the reply comes back, it is forwarded to the original human. |
/task (as the first line of a message from a seat) | Runs the remaining lines and replies to the sender with their output. If the task delegates further with /delegate, the downstream result is forwarded back instead, so a chain such as representative, lead, engineer, reviewer reports the end result. |
/sleep <duration> | Waits (up to 10m); useful for testing interruption. |
/claim <text> | Records an agent claim that a task is done. The platform treats this as information only. |
/fail <text> | Fails the turn technically. |
Messages queued with wake: false that arrive with a turn appear in its output as queued message from <seat>: ….
For example, to delegate a tracker task from a human message:
/delegate eng_lead /task
/tool connections.invoke {"connection":"linear","operation":"project.create","params":{"name":"Onboarding"}}