Orchestrate the agents. Contain the fiasco.
Fiasco is a headless multi-agent orchestrator written in Rust. It coordinates delegated agents and long-running background jobs through one durable runtime for local automation and cloud workloads.
The runtime deliberately shares one AgentRunner implementation and one tool
contract across root and child agents. Runs are portable and resumable without
a database, distributed scheduler, sandbox, or approval system. Transcript
inspection embeds fmtview behind a storage-neutral boundary; the agent runtime
itself remains headless.
- OpenAI OAuth with device login, refresh, Codex auth import, and one 401 retry
- OpenAI-compatible Responses and Chat Completions streaming APIs
- Anthropic-compatible Messages streaming API
- streamed text, fragmented tool calls, and usage/cache-token fields in events
- compact
read,write, andbashbuilt-ins plus optionalweb_search;readsupports bounded UTF-8 text and image attachments for vision-capable configured models - exact-first, atomic multi-edit writes with CRLF/BOM preservation
- run-local artifact spill for large tool results with bounded head/tail previews
- optional local context compaction recorded as ordinary messages, with read-only regex history retrieval
- Agent Skills discovery with progressive
SKILL.mdloading - progressively documented MCP stdio servers routed through one
mcpcommand - command hooks for run and tool lifecycle events
- concurrent direct-tool batches whose unfinished calls continue under process-local runtime handles
- asynchronously delegated general-task subagents that reuse the same runner
- an installable skill for graph-based orchestration with ordinary file tools
- ordinary Markdown user/project memory maintained with normal file tools
- self-contained run directories and optional NDJSON events
- tail-first transcript inspection for completed and running runs
Fiasco does not provide a security sandbox in this release. Built-in tools, MCP servers, hooks, and child agents run with the same permissions as the fiasco process. Run it only in environments where that access is appropriate.
Fiasco requires Rust and rg (ripgrep). Artifact-backed compacted-history
search invokes rg directly and expects it on PATH.
cargo install --path .The binary is named fiasco.
Without a config file, fiasco uses a deterministic echo provider:
fiasco run "hello"Run a task with machine-readable events:
fiasco run --output ndjson "inspect this repository and run its tests"Runtime output is stored beneath the current project:
.fiasco/runs/<run-id>/
run.json
messages.jsonl
events.jsonl
final.md
artifacts/
On an interactive terminal, inspect opens the current newline-complete tail and loads older messages lazily:
fiasco inspect <run-id>
fiasco inspect <run-id> --follow--follow refreshes newly completed message lines while preserving fmtview's
attached, detached, and paused browsing states. A redirected --follow is
rejected because this mode requires an interactive terminal.
When stdout is redirected, inspect instead writes exact newline-complete JSONL. The explicit form is useful in scripts. The legacy metadata/final-output view remains available separately:
fiasco inspect <run-id> | jq -c .
fiasco inspect <run-id> --output ndjson
fiasco inspect <run-id> --summaryResume an interrupted or failed run after repairing its message tail:
fiasco resume <run-id>Every complete newline is visible to inspectors, including a possible prefix of
the final assistant/tool exchange. Before resume, Fiasco matches the trailing
assistant's tool calls against ordered results and discards that exchange if
any result is missing. It then appends a user/runtime reminder that the process
stopped and workspace or external side effects may already have occurred. It
does not restore ordinary asynchronous tool jobs, active child work,
process-local input, or undelivered results. Existing child threads keep their
complete transcripts and remain discoverable through list_handles; an
explicit send_message adds a crash reminder and starts a new activity. Resume
the parent run rather than invoking fiasco resume on a child id directly.
Before resume, the process supervisor, cgroup, or container must have killed the previous fiasco process and all locally managed descendants. Remote jobs and other external side effects are not covered by that assumption.
Fiasco keeps its built-in system prompt independent of the workspace,
tool-agnostic, and unchanged across normal agent calls. Each typed tool.yaml
owns its capability's workflow guidance as well as its schema, so removing a
tool removes its model-facing instructions too. At the start of each run, the
first user message contains a synthetic <runtime-reminder> block before the
original request. The reminder snapshots the workspace path, AGENTS.md (or
lowercase agents.md when the canonical name is absent), discovered skill
metadata, memory locations, and any delegated-task instructions that apply to
the role. It also records the role and remaining
delegation depth; GeneralTask guidance lives here rather than in a second
system prompt. Built-in tool schemas are identical for Root and GeneralTask,
sorted, and frozen for the run; configuration or file changes take effect on
the next run. The environment section also states current model supported modalities: [text] (or [text, image]); the stable system prompt tells the
agent not to request an absent modality. Compaction reuses this stable
system/tool prefix and adds one final user instruction only to the compaction
request.
messages.jsonl uses the self-contained fiasco-message format. Each line has a
short ref (m1, m2, ...), created_at, role, and typed content blocks.
Those blocks are the exact provider-neutral messages replayed by the runner, so
tool failures, artifact refs, images, reasoning, and opaque provider
continuation items need no sidecar or reconstruction layout. Optional
compaction classification lives under _fiasco on the same line. The
sequence is derived from ref and the line position rather than duplicated.
The single process holding the run execution lease is the only writer; any
number of viewers may read complete message lines without taking a message-log
lock. A viewer may briefly show a prefix of a tool turn while its batch is being
written. The next writer trims a torn line and any semantically incomplete
trailing tool exchange before resuming appends. run.json declares
"message_format": "fiasco-message", retains the original user prompt, and
freezes the stored profile plus remaining delegation depth. Compaction never
rewrites or deletes committed trajectory records.
The interactive inspector delegates the physical growing-file timeline to fmtview-core and terminal lifecycle, JSON/chat rendering, search, navigation, wrap, and follow state to fmtview. Fiasco owns run lookup and terminal-state mapping; it does not duplicate newline paging or ratatui/crossterm behavior.
Stable agent instructions are folded scalar values in the typed, compile-time
prompts/agents.yaml registry. Every local model-facing tool adapter has a
typed tool.yaml beside it; standalone tools live under src/tools/<tool>/,
while handle, history, and graph families use
src/tools/<family>/<member>/. The manifest
always owns the complete model-facing name, purpose description, return
guidance, and input schema. Rust composes the two prose fields into the standard
provider description and owns validation, assembly, and execution.
Fiasco keeps graph planning as optional model guidance rather than a built-in
runtime subsystem. Install the repository's orchestrate-with-graphs Agent
Skill into a project with:
npx skills add siriusctrl/fiasco \
--skill orchestrate-with-graphs \
--agent universalThe skill teaches an orchestrator when a dependency graph is useful, how to
store it under .agents/graphs/, how to distinguish accepted knowledge from
live execution, and how to coordinate independent work. It uses ordinary file
capabilities and explicit agent controls.
Each graph is a lightweight YAML mental model with open or resolved
lifecycle. Nodes represent outcomes, questions, or decisions rather than agent
threads. A graph may be resolved with unfinished nodes when its summary explains
that the remaining work was abandoned, superseded, or deliberately narrowed.
The complete workflow and file convention live in
skills/orchestrate-with-graphs/.
If graphs later require shared remote storage, that service can expose a dedicated access capability without moving scheduling or execution state into the graph model.
Configuration is loaded from the first existing path:
--config <path><workspace>/.fiasco/config.toml$HOME/.fiasco/config.toml- built-in echo defaults
Config files are not merged, and unknown fields are rejected so misspelled settings fail at startup.
[provider]
kind = "openai-oauth"
model = "gpt-5.6-sol"
modalities = ["text"]Authenticate once:
fiasco auth loginCredentials are stored at $FIASCO_HOME/auth.json (default
$HOME/.fiasco/auth.json). If no fiasco credentials exist, the provider can
import a compatible $CODEX_HOME/auth.json or $HOME/.codex/auth.json.
[provider]
kind = "openai-compatible"
model = "my-model"
modalities = ["text"] # use ["text", "image"] only for a vision model
base_url = "http://127.0.0.1:8000/v1"
api_key = "${OPENAI_API_KEY}" # or a literal key
protocol = "chat-completions" # or "responses"
reasoning_effort = "medium" # optional; provider/model-specificapi_key accepts either a literal key or a whole environment reference such as
${OPENAI_API_KEY}. Keep a literal key in the user config at
$HOME/.fiasco/config.toml with restrictive file permissions rather than in a
workspace file that may be shared. If api_key is omitted, fiasco reads
OPENAI_API_KEY. The removed OpenAI-compatible api_key_env field is rejected;
write api_key = "${OPENAI_API_KEY}" instead.
reasoning_effort is optional. Fiasco sends it as reasoning_effort for
Chat Completions and as reasoning.effort for Responses. If omitted, the
provider's model default is used. Common values include none, minimal,
low, medium, high, and xhigh; accepted values depend on the endpoint and
model.
When Chat Completions reasoning is configured, max_output_tokens is sent as
max_completion_tokens, as required by reasoning-capable Chat endpoints.
The OpenAI-compatible adapter retries an initial HTTP 429 up to three times with bounded exponential backoff. Parent and child requests also share the runtime model-concurrency limit described below.
If a Chat Completions stream explicitly returns delta.reasoning_content,
fiasco stores it in the assistant message's optional reasoning_content
field in messages.jsonl and
emits transient model_reasoning_delta events to live event sinks. Per-chunk
text and reasoning events are not written to events.jsonl; the complete
assistant message is the durable trajectory. Reasoning token counts are
preserved in persisted model_completed events when the provider reports them.
Reasoning is kept out of the visible answer and final.md. This records only
reasoning text exposed by the compatible endpoint; it cannot recover reasoning
that the provider does not return.
Inspect the persisted reasoning for a run with:
jq -c 'select(.role == "assistant" and has("reasoning_content")) | {role, reasoning_content}' .fiasco/runs/<run-id>/messages.jsonl
jq -c '.' .fiasco/runs/<run-id>/events.jsonlreasoning_content is an OpenAI-compatible endpoint extension, not an official
OpenAI Chat Completions message field. Fiasco writes it only when the
endpoint explicitly returns reasoning text, and replays it as the same separate
field on later requests to that compatible Chat endpoint.
[provider]
kind = "anthropic-compatible"
model = "my-model"
modalities = ["text"]
base_url = "https://api.anthropic.com/v1"
api_key_env = "ANTHROPIC_API_KEY"See configuration.md for runtime, compaction, artifact, MCP, hook, and memory settings.
Automatic local compacted-state creation is disabled by default. Enable it with thresholds appropriate to the model's context window:
[runtime]
max_output_tokens = 8192
[compaction]
compact_at_tokens = 100000
context_window_tokens = 131072
keep_recent_tokens = 20000
summary_max_output_tokens = 4096
history_search_max_matches = 50Fiasco estimates the complete input from the first request and replaces that
estimate with provider-reported input usage whenever available. It can make an
additional model call when the tracked input reaches compact_at_tokens. The
call receives the original initial message, any previous compacted state, the
native older messages being replaced, and one final compaction instruction.
The successful user instruction and exact assistant response are appended to
messages.jsonl; the assistant response is the durable compacted state. Later
normal requests omit the compaction instruction and contain that exact
assistant message plus the exact recent suffix.
context_window_tokens is the model's configured nominal full window. Before
normal and compaction requests, fiasco checks its provider-neutral estimate
of system, schemas, active messages, and configured output allowance. It must
be greater than compact_at_tokens; if compaction cannot reduce the estimate
below it, the run fails locally. This is an early safety check, not a
tokenizer-exact provider guarantee. Setting the window requires an explicit
nonzero runtime.max_output_tokens; GeneralTask uses its separately configured
profile limit. Start/completion/failure records remain in events.jsonl;
compaction retries have numbered attempts, while a preflight rejection has no
started event or attempt because no provider request occurred. Compatible Chat reasoning_content and replayable opaque
provider items are included in the between-call estimate.
The normal agent receives the history_search and history_read schemas from
its first provider request whether or not compact_at_tokens is configured.
Changing the threshold controls compacted-state creation only; it does not
change the normal system prompt or tool schemas. Before anything has been
compacted, the history tools simply have no compacted prefix to search or read.
Two read-only tools recover exact details omitted from the active request:
history_search({"pattern":"..."})applies a Rust regular expression only to compacted messages and their linked textual tool-result artifacts. Results are newest-first. Each match containsref,source, andsnippet: refs are run-local sequence addresses such asm37(smaller numbers are older), whilesourceismessagefor inline content orartifactfor a linked complete spilled result.history_read({"ref":"m37","before":2,"after":2})reads a bounded chronological window around one ref. It returns JSONL records shaped as{"ref":"m<N>","message":<OpenAI Chat-compatible message>}and keeps tool calls paired with their results.
The local reader invokes rg for bounded-memory searches inside full textual
artifacts, so ripgrep must be available on PATH for that part of
history_search. A future remote reader can provide the same contract from a
database or service.
Neither tool has a cursor. history_search_max_matches limits a query to its
newest matches; if reached, older matches are omitted and the model must refine
the regex. This is distinct from artifact preview truncation: if the bounded
JSON/JSONL tool result is too large, its complete returned content is saved as
an artifact and can be inspected with read, continuing from the returned
line_offset or byte_offset; bash/rg is also useful for targeted searches. Query-limit
omissions are not present in that artifact.
Each assembled agent run has a sorted, frozen toolset. A run compacts
only when both history tools and at least one artifact inspection tool (read
or bash) are present. The compaction request reuses the same system prompt and
tool schemas; a tool-call response is rejected rather than executed.
This release implements local model-generated compacted states only. It does not use OpenAI or another provider's server-side compaction API.
Small results are returned inline. Large results are written in full under the current run and replaced in model context with:
- beginning and ending previews
- generation-time byte counts and media type
- a project-relative run-local attachment path
The model can inspect the complete output with bounded read calls or search it
with bash plus rg. The path names a mutable attachment: later inspection
observes its current contents, while the original preview remains unchanged.
Every tool result is limited independently; a previous large result never
suppresses a later small result. See
artifacts.md.
The launch tool surface is intentionally small:
read: bounded UTF-8 reads for a known path, or model attachments for jpg, jpeg, png, gif, webp, and bmp imageswrite: full-file creation/replacement or an atomic list of targeted editsbash: local discovery,rg, tests, builds, and other Bash commands; returns combined stdout/stderr and adds a status line only for unsuccessful completion. It uses a non-login shell and inherits fiasco's environment without loading profile fileshistory_search: regex search over the compacted trajectory prefixhistory_read: a bounded message window around a returned history refload_skill: progressive loading of a catalogued skill's full instructionsdelegate: asynchronously start a reusable GeneralTask agentlist_handles: discover all visible handles or inspect selected handleswait: wait until any selected handle changes or one interval expiresinspect: read a bounded window of a child agent's messagessend_message: sendsteerinput now or queuefollowupinput for laterstop: stop a tool job or the current activity of a reusable agentclose: cancel current agent activity if needed, then permanently close itweb_search: optional Brave-backed public web searchmcp: optional CLI-like access to configured MCP artifacts
Root and GeneralTask receive the same built-in schemas, including delegate
and every runtime-handle control. Remaining delegation depth is frozen in run
state and shown in the runtime reminder. At zero, delegate returns a local
tool error without creating a child; its schema does not disappear. Memory adds
paths to the reminder, not a tool schema. web_search and the single mcp
tool depend on startup configuration. Configured MCP artifacts contribute only
their namespace, description, and source-map path to the runtime reminder, not
their remote schemas. The resulting schemas are sorted and frozen before the
run's first normal provider call.
write requires every edit target to identify one non-overlapping region in
the original file. It tries exact matching first, then a conservative whole-line
indentation normalization. It does not use broad fuzzy similarity that could
silently modify the wrong code.
All direct tool calls in one assistant message start concurrently and share one
foreground window. If all finish early, fiasco returns immediately. At the
configured deadline, it preserves each unfinished exact future, moves only
those calls under process-local j_<ulid> handles, and returns those handles;
no tool is stopped or restarted. The assistant message, tool-result messages in
original call order, and any attachment message are appended in that order;
each complete line becomes visible independently.
Results retain their original tool_call_id, even though completion events can
arrive in another order. The model should put only independent calls in one
batch and issue dependent work after seeing results.
The tool result is a status-less <runtime_handle> notice containing the
handle, kind, and name; it only acknowledges that work is running.
delegate requires a non-empty model-supplied display name and starts one
isolated, reusable general-task agent asynchronously. Its handle is the child
run id, so the handle addresses the same durable transcript across activities
and process restarts. Promoted ordinary-tool handles exist only in the current
process. Each output uses the ordinary artifact policy. At the next model
boundary, one user/runtime message batches every ready
<runtime_handle status="..."> notice.
The handle-control calls are intentionally small:
delegate({"name":"inspect_tests","prompt":"inspect the failing tests and report the cause"})
wait({"handles":["01J..."]})
list_handles({"include_closed":false})
list_handles({"handles":["01J..."]})
inspect({"handle":"01J...","limit":6,"before_seq":42})
send_message({"handle":"01J...","message":"check the failing test first","mode":"steer"})
send_message({"handle":"01J...","message":"then compare the alternatives","mode":"followup"})
stop({"handle":"01J..."})
close({"handle":"01J..."})
wait returns as soon as any selected handle has a result or status change,
while its snapshot may still show other selected handles running. An omitted
or empty handles list means all visible handles. list_handles
returns all child agents owned by the current run plus current-process tool
jobs, including idle reusable agents and optionally closed ones. When handles
are named, it returns their current snapshots; include_closed affects
discovery rather than named lookup.
before_seq is exclusive and optional; inspect returns next_before_seq when
older messages exist.
Fiasco discovers Agent Skills from lowest to highest precedence:
$HOME/.agents/skills/*/SKILL.md<workspace>/.agents/skills/*/SKILL.md<workspace>/skills/*/SKILL.md
Only skill name and description enter the stable prompt prefix. load_skill
returns the instruction body without repeating that metadata, plus the absolute
skill-directory path needed to resolve referenced files.
fiasco skills listEach configured MCP source has a model-generated namespace and progressive
artifact containing MCP.md, an exact captured catalog.json, and optional
capability references. Only the namespace, short description, and absolute
MCP.md path enter the runtime reminder. The model reads that source map and
the relevant reference before using the one fixed mcp tool:
mcp("github search_code query='McpRuntime' language=rust limit=20")
The actual provider tool schema has one command string. Fiasco parses shell
quoting, resolves the first two tokens as namespace and exact remote tool name,
converts name=value arguments using the captured input schema, and calls the
configured stdio server. A tool with one input property also accepts one
positional value.
Configure the transport separately from the artifact:
[mcp.github]
artifact = ".agents/mcp/github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
[mcp.github.env]
GITHUB_TOKEN = "${GITHUB_TOKEN}"Authoring and validation use the same loader, compiler, client, and result renderer as the runtime:
fiasco mcp capture github
fiasco mcp check github
fiasco mcp check github --live
fiasco mcp compile "github search_code query='McpRuntime'"
fiasco mcp call "github search_code query='McpRuntime'"Install the repository's register-mcp Skill to have an agent capture,
document, explore, and validate an arbitrary configured stdio MCP:
npx skills add siriusctrl/fiasco \
--skill register-mcp \
--agent universalThe Skill organizes source maps around user-facing capabilities and encourages grouping commands that share objects, identifiers, or workflows. It keeps the raw per-tool catalog exact and outside model context.
delegate asynchronously starts the sole model-facing general-task role as
a reusable child agent;
there is no model-facing profile choice. The runtime reminder states the exact
remaining delegation depth. With the default max_subagent_depth = 1, the
first child has
zero remaining depth; delegate stays visible there but fails locally. A child
is another invocation of the same runner, not a second agent class. Each child:
- invokes the same
AgentRunnerand provider - has a separate run id, transcript, events, and artifacts
- records its parent run id
- shares the working project, so it can inspect and modify the same files
- uses the configured GeneralTask model and output profile
- cannot delegate another child at the default depth limit
Parent and child model requests share runtime.max_parallel_model_calls, which
defaults to one for compatibility with rate-limited endpoints. Delegated-child
capacity remains independently controlled by runtime.max_parallel_subagents.
Every delegate call starts an isolated child with only its runtime reminder
and delegated prompt. The prompt must include the complete objective and any
task-specific context; the child does not inherit the parent conversation. A
completed activity leaves that child idle. send_message resumes the same child
with an ordinary user message, followup queues that message without blocking
the parent, and a stopped agent stays paused until its next send_message.
close is the explicit end of the agent's lifetime. It cancels and waits for
current activity when needed, rejects new input once closing begins, and
discards any still-queued followups. Its
trajectory is stored in the child run, so reuse, resume, and history retrieval
do not depend on a live parent process.
runtime.model_stream_idle_timeout_seconds (default 300) stops a model stream
that produces no valid SSE event for that interval, while
runtime.model_request_deadline_seconds (default 3600) caps the complete model
API call even when the stream keeps making progress. Neither limit includes
tool execution or time spent waiting for the shared model slot.
Only child activity results return to the parent context; full child
transcripts remain in their own run directories. There is no parent-side
persistent coordination record or recovery state machine. On restart,
list_handles discovers direct child runs by parent id and presents every open
thread as idle without launching it. The first explicit send_message reuses
the child's complete transcript after adding a crash reminder. Closed children
stay closed. Tool jobs and undelivered activity outputs from the previous
process are gone; the parent crash reminder leaves any retry decision to the
model.
The parent can inspect a child's latest messages (six by default), page backward by sequence, and queue steering while it runs. Steering stays in the process-local mailbox until it is appended as an ordinary user message after the child's current assistant response and full tool-call batch, immediately before its next model request. It does not interrupt or discard in-flight tools.
Memory is durable knowledge about the user and projects, not the current conversation. It is ordinary Markdown at two locations which are included in an ordinary agent's initial runtime reminder:
$FIASCO_HOME/memory/user/for cross-project user knowledge<workspace>/.fiasco/memory/project/for project-specific knowledge
There are no special memory tools. The model uses read, write, and bash
for small focused changes. For a large independent update, it can delegate an
ordinary general-task child, continue useful work, and reconcile the child
result before finishing.
fiasco memory consolidateUse an external scheduler instead of embedding cron into the harness:
15 3 * * * /usr/local/bin/fiasco --workspace /workspace/project memory consolidateSee memory.md.
CLI/job
-> AgentRunner
-> ModelProvider
-> ToolRegistry
-> local Tool adapters grouped where related
-> one namespaced MCP command adapter
-> RuntimeHandleManager
-> promoted direct Tool future
-> delegated child AgentRunner
-> ArtifactStore
-> RunDirStore
-> EventSink
Provider wire formats never enter the loop. The MCP command adapter uses the
same Tool contract as local adapters while its artifact registry routes into
remote MCP clients. Subagents use the same runner. Large results use the same
artifact contract regardless of source.
Read architecture.md and design-choices.md for the detailed boundaries and tradeoffs.
cargo fmt --check
cargo check --all-targets
cargo clippy --all-targets -- -D warnings
cargo test --all-targetsRun a complete local smoke task:
tmp=$(mktemp -d)
target/debug/fiasco --workspace "$tmp" run --output ndjson "smoke"
find "$tmp/.fiasco/runs" -maxdepth 3 -type f -print