The ACP session
agentic-job run drives its agent over the
Agent Client Protocol: JSON-RPC
between a client and an agent, the same for every agent that implements
it. The session (src/session/) is the client. That gives one
integration, a transcript with the same structure whatever the agent,
permission requests as explicit messages, and a session a person could
join (interactive.md). Where its code sits and what
it is handed is in layout.md.
CI drives the scripted fake agent. Real-agent deployment evidence is
reported in the README; configuration
unit tests alone do not show that an adapter works.
What it does
It starts the agent through the wrapper run gives it and speaks ACP
over the agent's standard input and output; nothing listens on the
network. Without --runner-home the wrapper is run0 and the agent is
the sandbox user, while the session runs as the runner's user, so the
agent cannot alter the transcript or the limits. The reusable workflow,
by default (agent-user: runner), and review.yml pass
--runner-home: the agent is the runner's user
too, so it can write the session's files, the transcript and limits
included, read run's environment and stop run; the record is then
evidence of what the agent let through, not a guarantee
(requirements.md). The agent's
environment holds none of the job's ACTIONS_* variables and none of
its provider settings.
It sends the task as one prompt and advertises no filesystem or
terminal capability, so the agent uses its own tools inside the
sandbox. Every message in both directions is recorded as it passes.
That limits no tool: Claude Code still offers its own Read, Edit and
Write, in the working directory the session names, which
just claude-acp-test checks against the pinned adapter and a loopback
stub. The run's prompt tells every agent to change files with its edit
tools, and shell_edits in summary.json flags a change made without
one call of them.
It answers permission requests from a policy: allow, except a git push and an edit, delete or move outside the sandbox user's home. An
allow is the agent's "allow once", never "allow always". This is for
tidiness and for the record, since an agent only asks when it chooses
to: the sandbox is the boundary.
When the session ends it kills every process of the sandbox user, or,
with --runner-home, only the agent's process group: what the agent
detached from it runs until the job ends. What
run leaves under --out is in layout.md.
A run that allows outputs gets one reminder if it ends without a hand-back,
then fails if none is left. A fix can leave a change for the pull request's
branch, a replacement series, or a noop or missing_data request with a
reason. An unfinished rebase or unmerged index prevents completion even if
a request exists; the reminder names that state. These checks run under the
session's existing limits and do not replace output validation.
Agents
Reading a session in the summary
The session is included in the run summary by default. Set
render-session = false in [agent] in the repository-owned sandbox
configuration to opt out; there is no dispatcher input.
The trusted runner renders its redacted ACP copy,
not files written by the sandbox user. User and assistant text is prose;
reasoning and tools are collapsed separately. Tools show arguments, measured
duration, reported status and exit code when available, and at most 8192 bytes
of output with an omitted-byte count. Transcript punctuation is encoded to
prevent HTML or Markdown injection, including closing a generated details block.
Joined text is redacted again so secrets split across streamed chunks stay hidden.
session.md in the agent-run artifact contains the complete rendered session
(tool outputs are still bounded). The summary reserves space for accounting
and stays below GitHub's 1 MiB limit: assistant prose is retained first, then
head and tail blocks. If assistant prose alone exceeds the limit, each message
is abbreviated; the artifact keeps it.
A subagent's messages and tool calls render inside the call that started it,
where the agent says which that is (Claude Code does; opencode and vibe
report only the call's output). Each step shows the context's tokens as the
agent reports them. Where it reports the session's running cost in USD, each
reading shows what the turn added, and the three costliest turns are bold;
session/prompt responses show their turn's tokens. These are the agent's
figures, not the inference proxy's.
To replay a long real run in the renderer regression test, download its
*-agent-transcript artifact and extract acp.jsonl, then run
SESSION_TRANSCRIPT=/absolute/path/acp.jsonl cargo test --locked --lib real_long_session_summary_and_artifact -- --ignored --nocapture.
The test checks the full artifact and summary, then forces a smaller preview
budget to check prose retention. The ordinary suite separately covers overflow
of the 1 MiB limit with 150 tool calls. Real transcripts stay external, like
the corpus inputs, rather than committing whole task conversations.
Claude currently runs through claude-agent-acp, not native stream-json.
This renderer uses that recorded protocol. Native stream-json ingestion,
subagent-turn nesting (not exposed as separate turns by the current adapter),
and agent-specific opencode/vibe fidelity remain follow-ups. No sandbox transcript
collection or additional permissions are introduced.
Agent adapters
To the session, an agent is an entry of session/agents.toml, built into
the binary: its command, how it is told the model, and whether it takes
notices. This is not a caller-extensible registry for run: run also
has a closed Kind enum and agent-specific configuration and launcher
code. ACP makes the session transport common, not provider setup.
claude is Claude Code through claude-agent-acp, opencode is
opencode acp, and fake is fake-agent demo, which plays a recorded
session and needs no model.
vibe is Mistral Vibe 2.26.1, using its legacy Python ACP runtime.
The launcher uses its library entrypoint with only the user configuration
source and isolated Python imports: .vibe/config.toml, project profiles
and checkout modules cannot redirect inference. Both built-in provider
names are replaced with the broker's /mistral/v1 chat-completions route;
the caller's model is selected under its own name, which Vibe reports over
ACP and tells the model it is, and also replaces the built-in model, which
Vibe's default and fallback paths select. A model named devstral-2, which
Vibe renames to the built-in's, is refused.
Mid-turn notices are not supported. That broker model uses text-only tools
rather than inheriting Medium 3.5's capabilities. Its context window is
its entry of context-windows in [agent] in the repository-owned
configuration, for the main agent and the child alike, and Vibe compacts
at three quarters of it. config check refuses a window over 1,048,576,
which keeps a request under the broker's 16 MiB body limit.
Without an entry a model gets a conservative 32k, compacted at 24k, at
which a GLM research task compacted 18 times and lost its findings
(#821). Z.ai documents a 1M window for GLM-5.3; check what the broker's
provider serves before claiming it:
[agent.context-windows]"zai-glm-5-3" = 1048576Each model's reply is capped by its max_output_tokens, at most 65,536.
A model without one gets 32,000, or a quarter of its declared window if
that is less: 8,192 for the job's own model. Uncapped, one response ran
past the broker's body limit. The legacy runtime ignores the model's
max_output_tokens, so the session also sends the active model's cap as
Vibe's undeclared max_tokens session config option, which that runtime
sends with every model request. That runtime does not fail a reply cut
off at the cap; how a model recovers from one has not been tested.
Vibe 2.26.1 does not retry a model stream that ends without a finish
reason, and retries a dropped connection only before the first chunk. Only
its terminal interface retries those, and no setting enables that for ACP.
Over ACP the failure is a generic internal error, which the session cannot
tell from others to resend the turn. Such a run fails: a known limitation.
A configuration repository (agent-config-repo, agent-config-path) may
instead declare the models, in its config.toml. Only its [[models]]
(at most 32) and active_model, the alias of one of them, are taken; each
model's provider must be mistral or llamacpp, the two names the job
writes as the broker, and a model may set only the fields of the pinned
Vibe's model definition, none of them an address or a key, under an alias
that is not Vibe's own (mistral-medium-3.5, devstral-2), which Vibe
would merge with its built-in or rename. The file's
providers and every other setting are ignored: they could send inference
and the run token elsewhere, or turn on telemetry and connectors. The file
holds no key. Its models' window, compaction, thinking and output settings
apply as written; only max-requests bounds what they spend. A model the
caller names still wins over active_model: it selects the declared model
of that alias, else of that name, or, if none is declared, is added as
above. Dispatch and review always pass AGENT_MODEL, so there the file
only declares models. Vibe keeps its built-in model beside the file's, on
the same broker provider, where it may serve as the image fallback of a
text-only active model.
The run token lives only in the private generated TOML, as an Authorization
header, with no provider key or browser login. Telemetry, experiments,
connectors and update checks are disabled. Instructions are pointed out
in the task rather than automatically loaded from the project.
The credentialless hosted adapter test uses generated configuration and
a loopback model to verify ACP model selection, the output cap, a permission-requested
file write, cancellation followed by hand-back, and hostile project config
and import canaries. Run it with
AGENTIC_JOB_TEST_VIBE_PYTHON=/path/to/venv/bin/python3 cargo test --locked --test vibe -- --nocapture
after installing mistral-vibe==2.26.1 there. This is adapter evidence,
not a privileged sandbox or live broker test.
run does not start the real agents by those commands. It starts
agentic-job launch-agent NAME, a hidden command that runs as the
sandbox user, reads the one file holding the run token, sets the
agent's environment and becomes the agent. So the binary must be where
the sandbox user can run it, such as /usr/local/bin; run checks
that before it clones.
Adding an ACP agent to run
Paths here are relative to crates/agentic-job/src/. Adding a TOML entry
alone does not make a new [agent] name work. Keep provider setup in
reviewed code: an agent's configuration precedence and alternate login
paths cannot be secured by assuming it honors a base-URL variable.
First establish the adapter's contract. It must speak ACP on stdio and
use its own filesystem and terminal tools (the client advertises neither).
Identify the inference API it speaks, how it takes a base URL and run
token, and how it selects a model. Check which user, project, environment,
profile and login settings can override those choices. Determine whether
a mid-turn session/prompt steers the current turn or ends it, how it
ends a turn whose model request the provider refused, and test
cancellation followed by the hand-back prompt against the actual adapter.
The implementation recipe is:
- In
session/agents.toml, add the ACP command and non-secret defaults. Usemodel-envif the adapter selects its model with an environment variable; otherwise the session uses ACP'ssession/set_config_optionwith themodeloption, and fails the run unless the agent's answer reports that exact value as current: so opencode'sprovider/model/variant, reported without the variant, is refused. Neither a model from the environment nor a later change of model is checked. Leavenoticesfalse unless a mid-turn prompt is known to reach the next step without ending the running turn. Setprovider-errorto what the adapter's last message starts with if it answers a turn whose model request was refused as done. Registryenvvalues go on the wrapped command line: never put a token there, or incommand. - In
run/agent.rs, extendKind, its name/parser list and its matches. Choose the proxy API route, declare the one token-bearing home file, implement configuration generation and validation, and decide which instruction files need to be pointed out in the task. If configuration repositories are supported, explicitly select and validate their files; do not copy a directory of plugins or tools. Pin or disable settings that could redirect inference, enable alternate providers or load project configuration. Tokens belong only in the private home file (0600, directory 0700), never root-owned managed settings, argv or logs. If managed settings are needed, provide them throughmanaged_settingsforsandbox setup;runcannot write them after hardening. - In
run/launch.rs, add the agent's launch behavior: read the run's configuration, set only the required environment, and execute the registry command. ExtendKEPTonly for non-secret model settings that must survive filtering. Insession/process.rs, extendNOT_INHERITEDfor the new agent's provider, credential and configuration variables, so neither the wrapper nor agent inherits ambient settings. Account for configuration-location variables too, while preserving the login session variables needed by rootless containers. The launcher has no privilege or credential the agent lacks; the sandbox and egress policy remain the security boundary.
The current integrations illustrate why the last two steps are code.
Claude uses ANTHROPIC_BASE_URL, a private environment file and managed
settings that override project provider switches. With the proxy run API,
it sends a placeholder credential plus the run token in x-run-token;
with token-file registration, the token is the credential itself. Its
model is ANTHROPIC_MODEL, and mid-turn notices are disabled. opencode
instead gets a single provider's options.baseURL and options.apiKey
rewritten in its global JSON configuration; its launcher disables project
configuration and model fetching. Its model is an ACP session option and
mid-turn notices are enabled. See inference.md for the
proxy modes and routes; an ACP adapter still needs a compatible inference
API, not just a compatible session protocol.
Extend the table tests beside run/agent.rs, run/launch.rs,
session/agents.rs and session/process.rs. Cover registry/Kind
agreement, model delivery, missing inference, exactly one private token
file, token-free managed settings and argv, hostile inherited variables,
and project/configuration overrides. Check run/probe.rs's token-location
probes for the new file. Review session/digest.rs's subagent tool and cost
interpretation so task and spending limits remain meaningful. Exercise
permission requests, notices, cancellation, limits and cleanup through
the scripted session tests, then verify the real adapter in the sandbox
with the proxy: generated-config tests alone are not deployment evidence.
Install the agent and adapter where the sandbox user can execute them,
and document the tested versions and results before claiming support.
Limits
[limits] has timeout-minutes, max-requests (model requests, as
the inference proxy counts them), max-tasks (subagent
tasks) and budget (hundredths of a dollar, of the cost the agent
reports). Three tables are refused before the agent starts:
- one without a timeout;
- one with neither
max-requestsnorbudget, unless it saysuncapped = true(and one that says it together with either); - one with
max-requestswhere nothing counts requests: no proxy,register = "token-file", or a proxy that registered the run without a count. The cap is refused, not dropped.
At 60% and 80% of the more used of the time and the requests, an agent
whose entry says notices = true is told so in a prompt of its own.
Claude Code is not: its adapter would end the running turn for one. At
95% of the timeout, within ten requests of the cap, or once the agent
starts more tasks than allowed, the turn is cancelled and one more
prompt asks the agent to hand back what it has. At the limit the
session is cancelled. Spending over budget stops it at once, with no
hand-back (then). The cost
comes after each model reply, so the reply that crosses the budget is
often the last of a turn the agent has already ended. If the agent
answers the cancel with end_turn, the run ends well, exit state 0:
its harness.json message says how far over it went, the run action
warns and sets its over-budget output, and the status comment says so.
A turn answered with cancelled is a stop as before. A turn that ends
just as the timeout cancels it is treated the same way. The budget cannot
refuse a reply in advance, because no one knows its cost before it
comes; max-requests is the cap that acts before a request is made.
max-repeats (off unless set, 2 to 100) is for an agent stuck making
one tool call over and over: GLM agents have repeated one gh api
command hundreds of times, until the provider's rate limit ended the
run with nothing handed back. Once that many finished calls in a row
had the same tool, input and result, the agent is told so in a notice.
The step under way may still make the call once more; a second call
after the notice has the agent hand back as at a limit. Only a changed
result starts the count again: a poll that keeps answering "in
progress" counts, so leave room for one. The harness sees calls only as
the agent reports them, after they ran, so it cannot refuse one. An
agent without notices is not told, and hands back at the same call.
A task's turn that ends on an error is not taken for a finished task.
That is a turn the agent answers with an error, as opencode and Vibe
answer one whose model request failed and as Claude Code's adapter
answers one it gave up on (Internal error: API Error: Request rejected (429) ..., with errorKind in the error's data), or one it answers as
done after saying it gave up on a refused request, as Claude Code says
API Error: ... after its own retries (the entry's provider-error
names the words). After a pause of a minute (no longer than the
hand-back window), the agent is asked to hand back as near a limit.
If it does, the run is stopped at a limit, exit state 3, with the
error as the reason; if it doesn't, the run fails with the error. A
limit the run reaches meanwhile stops it as any other time, and a bridge
client's prompts meanwhile are refused.
The agent's word is not needed to keep its change, though: handing back
needs the model too, and two runs lost 45 minutes of work to a provider
that stayed overloaded (#1860). A run that failed on a model provider's
failure of a known class (any but other, an agent that died of one
included) and whose working tree has a change is handed back as if
stopped at a limit: exit state 3, harness.json and summary.json
with result budget and a message starting "the model provider stopped
the run" that names the class but not the error's words, the default
partial pull request with that as stopped_early, and its checkpoint
under ready_when_complete. Like a run whose agent handed back, it is
filed as budget, a partial result to continue rather than a run to
repeat, with the class in provider_error. The session's own
harness.json under work/ keeps the failure. A run
that changed nothing, whose change was not handed back, or that failed
for anything else, still fails with exit state 1.
A model provider's failure that a wait may clear gets a few retries
before that, for every agent. The session sorts the error's words, or
the agent's, into a class (session/provider.rs): rate_limited (HTTP
429, "rate limit"), overloaded (5xx, Anthropic's 529, "overloaded"),
unreachable (refused, reset, closed, EOF, a timeout), quota (a usage
or spending limit), unauthorized (401 or 403) or other. The words
are those of real runs' transcripts, in
tests/data/provider/errors.json; a status code counts only next to a
word that makes it one. Vibe's RateLimitError, code -31001, is
rate_limited by the entry's rate-limit-code whatever its words
(JSON-RPC's reserved codes are refused there). A turn that ended on one
of the first three is sent again on the same session, labelled rate limited or provider error, behind a note, after a wait of 10, 20 and
40 seconds plus up to a quarter of jitter, or as long as the provider
said to (retry-after), each no longer than the hand-back window: three
times, and then the turn is taken as one that ended on an error. An
overloaded provider is out of capacity for a while rather than
refusing the run, and is waited out for about four minutes: six times,
the waits growing to a minute each. A
provider that asks for more than two minutes is not waited for at all.
Runs had ended on a 429 that a minute's wait would have cleared (#1339).
A quota or unauthorized failure is never waited on: nothing within
the run clears it, so the agent is asked to hand back at once, without
the pause. The waits count against the run's time; a limit reached in
one stops the run, and near one the agent is asked to hand back instead
of waiting on. provider_error in harness.json and summary.json
names the class of the failure a run that did not end well ended on. An
agent that died mid-turn, after letting a failure out on its standard
error or in its words in that turn, died of it: that, not the adapter's
crash, is the class; the words of an earlier turn do not count, and the
agent's own log uses words like "timed out" or "connection closed" for
other things, so only a provider's phrasing of them does. resends and
resend_wait_s count the times the task was sent again and the seconds
spent waiting, a wait a limit interrupted included. summary.json files
the failure of a run that did not hand back under inference_ and the
class, and the run action and the status comment carry that word on
(workflow.md).
An overload that outlasts its waits can be worked around rather than
handed back: fallback-model in [agent] (for example opus for a run
of fable; Claude Code and the fake agent only, whose models are names
the provider resolves) has the session switch the agent to that model
through its model session config option (ACP's
session/set_config_option, which claude-agent-acp serves), so that it
keeps its context, and send the task again at once, the waits starting
over. It switches once; an agent that refuses the switch, reports no
model or still the one it ran is asked to hand back as without one.
fallback_model in harness.json and summary.json names the model
the agent then said it ran, and model in summary.json names it too,
so that pacing charges the pool the run went on with.
Nor is a task's turn that stalls left to the timeout. Vibe has no read
timeout on its model requests, and one waited six minutes on idle
connections before going on (#983). An agent entry with
stall-timeout (seconds; Vibe's is 120, no other has one) has a turn in
which nothing comes from the agent for that long, with no tool call
running, cancelled and the task sent again once on the same session,
labelled stalled, behind a note that the last turn stalled. A second
stall, or a cancel the agent does not answer, fails the run with a
message that starts the agent stalled, and summary.json files it as
a stall failure rather than agent_exit. stalls and stall_retries
in harness.json and summary.json count both. Only the task's turn is
watched; the hand-back, reminder and client turns are not.
Nor is a task's turn that ends well without the output the run requires.
A run whose policy allows any output requires one: a request in
out/safe-outputs.jsonl, a replacement, or, where the policy carries a
change, a change of the checkout. Agents have answered end_turn without
theirs, after a compaction of their context, and the run ended well with
nothing handed back (#607). The session then checks, as the sandbox user,
and asks once more in a prompt of its own, labelled reminder, under the
same limits. A requests file the run would not take (over its limit, or
not a regular file) is reminded of the same way, naming the file and the
limit, since the hand-back would otherwise be dropped whole without the
agent hearing of it. If the agent still left none, the run fails, exit
state 1.
reminded in harness.json and summary.json says a reminder was sent.
The check only decides the reminder; the hand-back is still collected and
checked as before. A run with a bridge client is not reminded: the
client sees what the agent left.
Where the bounds set ready_when_complete for the run's change, the
brief asks the agent to write its outcome early, with a checkpoint
object (complete, done, left, next) marked incomplete, keep it
current, and set complete to true last. Restating the hand-back's path
and what was left is what brought back a hand-back lost to a compaction,
where a bare reminder did not (#813). So the reminder quotes the
checkpoint's left and next, and after each compaction the session
sees during the task's turn (a Vibe-style compaction tool call that
completes, or the context reported in a usage_update falling below
half, as for a second agent), an agent that takes
notices is sent a compaction notice: the start of its task, the paths
it hands back in, and what its checkpoint says is left. An open
checkpoint is never taken for done: see
ready or draft. Without
ready_when_complete, the brief asks for no checkpoint and no
compaction notice is sent.
Updates of the run's item
Direction added to a run's issue used to mean cancelling the run and dispatching it again, losing its work (#925). With the repository-owned configuration's
[session]watch-item = truewatch-seconds = 60 # the default; 10 to 3600run polls the issue or pull request the run is for (--watch-item,
which the reusable workflow sets from the event or the issue input)
with the agent's own read-only token, and sends the agent each comment
made on it (not a pull request's review comments), and each edit of the
body, while it works. What came between the event and the session's
start is in the brief or not seen. Off by default; without
the token or an item it watches nothing, and says so. The polls are
conditional (If-None-Match), so an unchanged item costs no rate limit,
and a poll GitHub refuses for a limit waits as long as GitHub asks.
Every update is passed on, whoever wrote it, and none claims authority:
it is quoted with its author and link as new input on the item, which
the agent weighs as any other text it reads. Anyone who can comment on
the item could already put text before an agent that reads it with gh.
A run takes at most ten updates, each cut to 8000 characters.
Updates go into a running turn only: by claude-agent-acp's
_session/steering for an entry with steering = true, which asks the
adapter not to start a turn when none is running; otherwise as a notice
is, for an entry with notices = true. An agent that takes neither (Vibe)
is sent none, and the condensed log says so. Each update sent is a line
of the condensed log, and its link is in updates of summary.json
and in summary.md.
A second agent
A run can let its agent start a second agent, the child, and drive it over ACP turn by turn: to watch, for one, what a model does after its context is compacted. Only Vibe can be the child. It is off unless the repository-owned configuration names one:
[child-agent]name = "vibe"model = "zai-glm-5-3"sandbox setup installs the same pinned Vibe 2.26.1 runtime whether
Vibe is the main agent, the child, or both. Other child names are refused
by config check and setup: only Vibe has a supported child installer.
The child uses [inference] url plus /mistral/v1, not the main
agent's openai-url or anthropic-url. The
broker's run-token contract
admits an active run token on every injected route, including Mistral;
registration is not scoped to the main agent's provider. No child pool
list or second registration is needed by this protocol (this repository
has no pool configuration). The broker deployment must enable its Mistral
route and supply its Mistral credential. This confirms the documented
protocol, not a live deployment test. The repository-owned configuration
must explicitly set [inference] url, register (and audience for
github-oidc), allow that address in egress.direct when required, and
set [limits] timeout-minutes and max-requests.
run then writes the child a Vibe configuration of its own,
~/.config/agentic-job/child-vibe/config.toml, as it would write the
main agent's for that model: the same proxy route and run token, so the
token probes expect it in that file and the agent's, and nowhere else.
Its model is a built-in definition, with the window [agent] context-windows gives it (32k without an entry); the
main agent may edit that file, which gives it nothing it does not have
with its own token. The task is told the path of acp-drive.
The configuration is refused without limits.max-requests. The budget
reads only the cost the main agent reports, but the proxy counts every
request made with the run's token, so the request cap covers both
agents: a run capped by its budget alone could spend without bound
through the child. The timeout covers both too, since the session kills
every process of the sandbox user when it ends. run --runner-home has
no sandbox user to end, and the daemon's session of its own would outlive
the agent's, so it refuses [child-agent].
The main agent uses it through bash; each command prints JSON:
agentic-job acp-drive start --agent vibe [--model M] [--cwd DIR]starts a daemon, as the sandbox user, that starts Vibe throughlaunch-agent --child, opens a session and holds its standard streams. It prints the session and the model Vibe reports.prompt TEXT [--timeout S]sends a turn and prints its stop reason, or the error it was answered with. With--timeoutit stops waiting (exit 124) and the turn goes on.events [--since N]prints the events from the Nth on: the prompt, messages, thinking, tool calls and their results, permission answers, compactions, the end of each turn and the child's exit. An event never changes once printed; a message still streaming is cut where it stood and goes on in the next.cancelcancels the running turn;statusprints the session's state;stopkills the child and ends the daemon.
Vibe 2.26.1 reports a compaction as a tool call titled "Compacting
context", then "Context compacted"; those are compaction events. A
compaction it does not report is inferred, "inferred": true, when the
context it reports in a usage_update falls below half the last one.
The child's permission requests are answered by the main agent's policy.
The daemon talks to its commands over a socket in
~/.local/state/agentic-job/acp-drive, private to the sandbox user, and
writes there every ACP message (acp.jsonl), the child's standard
error and its own log. It holds nothing the main agent does not and is
not a boundary: the sandbox and the proxy's count are. After the
session run reads that directory and the child's Vibe logs as the
sandbox user, as it reads the agent's own logs, into
child-session.txt and child-logs.txt of the transcript, redacted
with it; child_logs in summary.json says what was taken. As there,
the last 4 MiB of each file is kept.
tests/acp_drive.rs drives the scripted agent through a prompt, a
cancel, reported and inferred compactions and a crash. Driving a real
Vibe this way has not been tested.
How a run ends
The session's result becomes run's exit state: 0 when the agent ended
its turn, 1 when it failed, exited, could not be started or cancelled,
3 when a request, task or spending limit stopped it (or it handed back
after a turn that ended on an error), and 124 for the
timeout. interface.md has the whole table,
with the states that are run's own. handed_back in summary.json
says whether a stopped agent handed back.
The transcript also holds agent-logs.txt: the agent's own log files
written during the session, which may say what its standard error and
ACP do not, such as a dropped model stream. They are read as the sandbox
user from one directory under its home: ~/.config/agentic-job/vibe/logs
(the launch command turns Vibe's log on, as its ACP entrypoint does),
~/.local/share/opencode/log, and ~/.claude/debug, which Claude Code
writes only with debugging on. Links are not followed; the newest 16
files are taken, the last 4 MiB of each and 16 MiB in all, and redacted
with the rest of the transcript. agent_logs in summary.json says what
was taken, and the summary says when the directory is missing.
Vibe sends its reasoning as message chunks holding Python reprs of thinking blocks; the condensed log and the rendered session read them as reasoning in plain text, as other agents' thought chunks.