Commit graph

82 commits

Author SHA1 Message Date
Bryan Helmkamp
9c152ccddf
refactor(agent): simplify task reminder staging and test fixtures
Stage the pending task reminder as a Message and add
Message::to_llm_message so durable history and the round-staged turn
share one turn-to-wire conversion. Replace the one-off
BlockingAfterFirstOutputProvider with request capture and an
EventsThenPending variant on ScriptedStreamProvider, add a shared
make_session_with_provider_and_tools helper, and assert the reminder
tests against task_reminder::TASK_REMINDER_TEXT instead of a
substring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 14:20:32 -04:00
Bryan Helmkamp
b6fdd9df72
Keep task reminders transactional across interrupts 2026-08-03 16:56:54 -04:00
Bryan Helmkamp
f212594875
Merge pull request #694 from fabro-sh/feat/reusable-subagent-sessions
Reuse completed subagent sessions
2026-08-01 10:21:41 -04:00
Bryan Helmkamp
a617eaa5eb
docs: record why a resumed turn's send cannot hang an agent
Review asked twice whether `permit.send` can leave an agent Running with
no turn on its way. It cannot, and the reasoning is not local to the call,
so state it there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 10:17:10 -04:00
Bryan Helmkamp
8c19b1e94a
fix: keep forwarding a reused child's events after a broadcast lag
The subagent event forwarder left its loop on any `recv` error, including
`Lagged`. A lagged broadcast receiver stays usable, so one transient lag
silenced the child for the rest of its life while the task completed
normally and shutdown joined it without noticing. Session reuse widens
that window from a single turn to the whole parent session.

Also borrow each result's output when rendering a parent notification
instead of cloning it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 10:13:27 -04:00
Bryan Helmkamp
1bb70adafd
refactor: simplify subagent session reuse
Review pass over the reuse change. No intended behavior changes.

- share one definition of the initial generation from fabro-types instead
  of three copies across fabro-types, fabro-agent, and the supervisor
- give each child one SubAgentHandle instead of threading the supervisor's
  state, callback, and notification sender through five functions, and
  collapse the repeated signal-then-drain pairs into publish()
- move `reusable` inside SubAgentStatus::Finished so a closed agent can no
  longer be marked reusable
- clear the lifecycle draining flag with an RAII guard, so one panicking
  callback cannot silence every later lifecycle event
- tear down a session that failed to initialize right away rather than
  holding it and its sandbox until the parent closes the agent
- look agents up through SupervisorState::agent/agent_mut instead of five
  copies of the same not-found error
- drop the unreachable cleanup_started branch and the test-only emit_event
  whose only caller was its own test
- render subagent starts from one ProgressEvent and one display method,
  deriving the spawn/turn distinction from the generation
- set projected subagent status through one helper instead of four
  identical reducer arms
- drive the generation-pinned wait test through spawn/send_input rather
  than hand-writing private supervisor state

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 10:09:07 -04:00
Bryan Helmkamp
24f9ac6bd1
refactor(model): rename Kimi provider to Moonshot 2026-07-31 09:00:52 -04:00
Bryan Helmkamp
8b5106902c
feat: reuse completed subagent sessions 2026-07-31 08:19:10 -04:00
Bryan Helmkamp
0eda219376
Simplify stdin_source plumbing after review
- ExecStreamingRequest: drop #[non_exhaustive] and the six Option-taking
  builder setters; call sites use struct literals over ::new(), matching
  GrepOptions/WalkOptions, and providers can destructure exhaustively
- Docker: pass ExecStreamingRequest through docker_exec_shell_streaming
  instead of seven positional args; revert the no-op StartExecOptions
- Daytona: stdin temp-file cleanup is now best-effort (mirrors
  DaytonaSession::close) so a failed delete cannot fail a completed
  command or double-delete from Drop; upload overlaps session creation;
  one shared DAYTONA_CLEANUP_TIMEOUT
- write_process_stdin tolerates ConnectionReset/ConnectionAborted so a
  command that stops reading stdin does not fail on TCP Docker daemons
- Local sandbox aborts the stdin writer after process exit instead of
  joining unbounded
- Cap stdin_source payloads at 10 MiB, mirroring the for_each bound
- Add Node::context_key_attr() tri-state so the handler and lint rule
  share one definition of a valid context-key attribute
- inert_attribute canonicalizes handler types via StageHandler, fixing
  false warnings for command attrs on tool nodes
- Share resolve_flat_context_value between command stdin and for_each;
  resolve_json_value takes Value by value, removing a deep clone
- Reuse MockSandbox in command handler stdin tests instead of extending
  SpySandbox with a hand-rolled streaming override

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 11:14:48 -04:00
Bryan Helmkamp
5d72f9a538
Add context-sourced command stdin 2026-07-29 10:25:51 -04:00
Bryan Helmkamp
f4c09867e5
Merge origin/main into refactor/remove-env-interpolation 2026-07-28 18:31:16 -04:00
Bryan Helmkamp
fa85bc42a2
Merge pull request #643 from fabro-sh/feat/claude-5-profile
feat(agent): add Claude 5 profile
2026-07-28 18:25:21 -04:00
Bryan Helmkamp
68b90ccc61
fix(agent): honor the TaskOutput schema defaults
The schema advertises defaults for `block` and `timeout`, but the
executor errored when either was absent. Apply the advertised defaults
instead, and keep the type check for values that are present.

The required list stays as the Claude 5 contract declares it. Constants
now hold the defaults and the maximum so the schema and the executor
cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:21:01 -04:00
Bryan Helmkamp
6226c8c517
fix: address env interpolation review findings
Restore the documented SDK env credential facade without reintroducing run fallback behavior. Fail closed on GitHub permission resolution, require worker storage at the CLI boundary, and align interpolation names and generated docs.
2026-07-28 17:30:50 -04:00
Bryan Helmkamp
e54fef760a
refactor(auth): remove EnvCredentialSource and make the run vault required
`EnvCredentialSource` resolved provider credentials from the process
environment. It had no production entry point of its own — it was only
ever reached as the `None` arm of an `Option<Vault>` in three places:
`build_llm_source`, `configured_providers_for_start`, and
`configured_providers_from_process_env`.

That optional vault is not a state the product can be in. Every run has a
server behind it, the server always spawns workers with `--storage-dir`
(`worker_runtime.rs`), and `SqlVaultCredentialSource` backs both the
server and the CLI. So the fallback only served to silently degrade
credential resolution to whatever the worker process happened to have in
its environment.

Make the vault required across the run path — `RunOptions`,
`StartServices`, `build_llm_source`, `tool_secrets_from_configured_sources`,
`vault_token_lookup`, and the CLI GitHub helpers — so the invariant is
enforced by types rather than assumed. A worker spawned without
`--storage-dir` now fails with a clear message instead of quietly
continuing without a vault.

`configured_providers_from_process_env` had no callers at all and is
deleted. `AgentApiBackend::new_from_env` was public but only ever called
from its own tests; it is deleted too.

Test-only credential sources move to a feature-gated
`fabro_auth::test_support`, wired through dev-dependencies so they never
link into production builds. The CLI worker tests now pass
`--storage-dir`, matching what the server actually does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:36:54 -04:00
Bryan Helmkamp
73051c9a8a
refactor(agent): share one normalizer between the question tools
`Claude5QuestionToolArgs`/`Claude5Question`/`Claude5Option` differed from
the Anthropic trio only in required-ness -- `header: String` rather than
`Option<String>`, same for each option's `description`. The JSON Schema
already enforces that at the model boundary, so the lenient structs
deserialize the strict payload unchanged.

`normalize_claude5_questions` then reproduced `normalize_anthropic_questions`
plus an inlined copy of `options_from_anthropic`, so `option_key`,
`display_text`, and `bounded_display_field` were each applied in two
places and could drift.

Replace both with one normalizer taking a `QuestionLimits`. The genuine
Claude 5 deltas -- at most four questions, two to four options, a
twelve-character header cap, required header and option descriptions, and
no previews on multi-select -- become data rather than a second code path.

Two rules serde used to enforce are now the normalizer's: a missing header
and a missing option description. Both are still rejected, with a clearer
message than serde's "missing field". `multiSelect` now defaults to false
instead of being a deserialization error; the schema still marks it
required, which is where that contract belongs.

Adds tests pinning the strict rules against the shared normalizer, and one
asserting the lenient contract still accepts optional headers and
descriptions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:04:24 -04:00
Bryan Helmkamp
8b7d07b84b
refactor(agent): deduplicate the BaseProfile accessor delegation
All six provider profiles embed a `BaseProfile` and hand-wrote the same
six delegating accessors -- 24 identical lines each. What actually
distinguishes them is `build_system_prompt`, and for Claude 5,
`register_subagent_tools`.

Replace the copies with one `impl_base_profile_accessors!()` invocation.

A macro rather than trait defaults because three implementors have no
`BaseProfile` to delegate to -- `TestProfile`, the workflow crate's
`ShutdownTestProfile`, and the server's `AskFabroProfile` -- so a default
would need a runtime fallback for a case the compiler can already rule
out. Those three keep their hand-written accessors and are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:00:21 -04:00
Bryan Helmkamp
3c755a7d4e
refactor(agent): give Claude 5 subagent tools fabro canonical names
`NativeTool` documents itself as "an identity, not a name" whose canonical
form is fabro's own vocabulary, with harness names layered on as aliases:
`to_string = "read_file", serialize = "Read"`.

The four Claude 5 subagent tools inverted that. `ClaudeAgent` declared
`to_string = "Agent"`, making the Anthropic wire name the identity and
leaving `name(ToolVocabulary::Fabro)` returning `"Agent"` -- and pairing a
provider-specific variant name with a generic wire name. It also meant the
`Claude5` arm listed none of them: they fell through to
`canonical_name()` and were correct only by accident.

Rename to `BackgroundAgent` / `AgentOutput` / `StopAgent` / `MessageAgent`
with fabro canonical names, keep the harness names as `serialize` aliases
so `from_any_name` still resolves them, and name them explicitly in the
`Claude5` vocabulary arm. Also map `Grep`/`Glob` there: that arm describes
the vocabulary rather than the profile's registry, and if either were ever
registered it would otherwise reach the harness lowercased.

Records why these are separate identities from
`spawn_agent`/`wait`/`close_agent`/`send_input` rather than aliases of
them, since the capabilities genuinely differ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:57:42 -04:00
Bryan Helmkamp
27549c2358
fix(agent): share the task runtime with every profile's children
Task tools scope their list by `root_session_id` -- `Session` documents
this as "a subagent session inherits its parent's `root_session_id` so
todo tools that scope by root (Anthropic tasks) share one list across all
subagents" -- so a root and its children address one logical list.

`build()` runs once per session, though, and `AnthropicProfile`
constructed its own `TodoRuntime` inside that call. Root and child
therefore resolved the same `list_id` through different runtimes: both ID
counters started at zero, so both emitted `todo.created` with id `1` for
the same list, and `TodoListProjection::upsert` matches on id -- the
child's task replaced the parent's in the persisted projection. `TaskGet`
and `TaskList` read the local runtime, so neither session could see the
other's tasks either.

The previous commit's shared runtime fixed this for Claude 5 only,
because `build()` passed dependencies positionally and adding a fourth
argument would have meant touching all six call sites. It grew a second
constructor for Claude 5 instead, leaving the other five on a signature
that could not carry the runtime.

Bundle them into `ProfileDeps` so every profile takes the same
`(model, &deps)`. The duplicate constructor is gone, Anthropic shares the
runtime by construction rather than by opting in, and a future dependency
reaches all six profiles or none.

The existing Claude 5 sharing test is generalized and now also runs for
Anthropic; it fails against a per-profile runtime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:37:48 -04:00
Bryan Helmkamp
f293e3de18
refactor(agent): fold parent notifications into subagent state
`ParentNotificationHub` kept a second `Mutex` and `watch` channel holding
a copy of each child's terminal result -- data `SubAgent.status` already
owns as `SubAgentStatus::Finished`, and which is never evicted, since
nothing removes entries from `SupervisorState.agents`.

Two of the three bugs fixed in the previous commit were ordering bugs in
the coupling between those two structures: suppress-vs-commit in
`begin_shutdown`, and register-vs-publish in `spawn_inner`. Both were
fixed by ordering the steps correctly. Keeping the registration beside
the status it is delivered with makes that whole class unrepresentable
instead:

- Registration is now a field on the `SubAgent` literal `spawn_inner`
  already builds, under the lock that publishes it. There is no window
  between publishing an agent and registering its notification.
- Suppression on shutdown happens inside the critical section that
  decides the shutdown, after the status transition commits, so a
  rejected shutdown cannot discard a result the parent is owed.
- `next_parent_notification_batch` scans agents for a live registration
  whose status is `Finished`, and ignores `Closing`/`Closed` outright --
  so a shutdown racing delivery can no longer park the parent on a result
  that will never arrive, even if suppression were missed.

`spawn_result_monitor` no longer takes the hub; it bumps a single
`watch` counter after committing the status it already commits. Batch
order was the queue's insertion order, so `SubAgent` carries a
`spawn_seq` to keep delivery oldest-first.

Tests move from exercising the hub directly to the supervisor API, and
cover spawn-order batching and the shutdown-races-delivery case that the
old shape could not express.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:44:20 -04:00
Bryan Helmkamp
d669a2d55c
fix(agent): correct background-agent notification delivery
Three correctness fixes in the Claude 5 background-agent path, plus
cleanups from a reuse/quality/efficiency review pass.

Fixes:

- Background-agent output was run through skill expansion. A child that
  wrote a bare path ("cleaned up /tmp") failed the whole parent turn with
  `Unknown skill: /tmp`, and a child whose output happened to name a real
  skill had its report replaced by that skill's template. Synthesized
  harness turns now skip expansion; only text the user typed can invoke a
  skill.

- `begin_shutdown` suppressed the pending notification before deciding
  whether a shutdown would happen. Stopping an agent that had just
  finished rejected the stop *and* discarded the result the parent was
  owed. Suppression now happens only once shutdown is committed.

- `spawn_inner` registered the notification after publishing the agent in
  `state.agents`, so a concurrent `shutdown_all` in that window left a
  pending entry the monitor never completes, and the parent's drain loop
  would never see the queue as drained. Registration now precedes
  publication.

- `TaskOutput.timeout` was declared `number` but parsed with `as_u64`, so
  a schema-valid `30000.0` failed at runtime.

- Update the fabro-server alias test for the `sonnet` alias moving to
  Claude Sonnet 5.

Cleanups:

- The supervisor renders the notification turn; `Session` no longer knows
  the envelope format.
- Replace six near-identical prompt snapshots with a property test over
  all eight conditional combinations, keeping the default and
  all-conditionals snapshots for wording.
- Collapse `TodoRuntime`'s two mutexes into one.
- Read the prompt vocabulary from the registry instead of hardcoding it.
- Drop internal vocabulary from the `SendMessage` tool description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:15:39 -04:00
Release Repro
6d61c6b5e4
fix(agent): stop the Kimi leak assertion from passing vacuously
The check tested for `never reconstruct it from memory`, a phrase the
Kimi edit description no longer contains after it was reworded to match
Kimi Code. A one-sided `!contains` against a literal cannot tell "the
phrase is absent because nothing leaked" from "the phrase is absent
everywhere", so it silently stopped protecting anything.

Assert the marker is present in Kimi's own description and absent from
the stock one. Removing the marker from the description now fails the
test instead of quietly disarming it, verified by doing exactly that.

Also correct the grep docs: all three sandbox implementations probe for
`rg` and fall back to POSIX `grep`, so the page should not imply a
single engine. Pre-existing, adjacent to the lines this branch touched.

Reported by Copilot review on #646.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:13:41 -04:00
Release Repro
454f07d560
refactor(kimi): match Codex and Kimi Code on where read guidance lives
Neither harness puts read-before-edit mechanics in the system prompt.
Kimi Code's `system.md` has no such section; the rules live in
`edit.md`, `write.md`, and `read.md`. Codex's prompts say nothing about
reading before an edit at all, and its editing guidance is attached to
`apply_patch`.

Drop the `# Reading Before Writing` section from the Kimi prompt and
carry its content in the Edit, Write, and Read descriptions, worded as
Kimi Code words it. Nothing is lost: every bullet in the removed
section was already covered by a tool description.

Two behaviors change to match upstream. Edit now says not to issue
consecutive edits against the same file, since the first invalidates
the second's `old_string` -- Kimi Code's stated reason. Read now says
not to re-read solely to confirm a write landed, which both harnesses
call out as waste; the previous prompt asked for exactly that re-read.

The gpt56 profile already followed the Codex split and is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:07:19 -04:00
Release Repro
a925275778
fix(agent): remove the read-before-write guard
`ReadBeforeWriteSandbox` blocked writes to any existing file the agent
had not read, tracked by a session read set populated only by
`read_file`, `grep`, `read_many_files`, and the Kimi `Read`.

The gpt56 profile has none of those. It mirrors Codex's tool contract --
`shell_command`, `apply_patch`/`edit_file`, `update_plan`, `web_search`
-- and reads through the shell, so its read set stayed permanently
empty and every edit to an existing file failed. In run
01KYD4360GN6SED4BYEVGYP4XT all 28 `edit_file` calls failed, 25 of them
on the guard. The agent read `package.json` with `sed` and `cat`,
hex-dumped it trying to diagnose the rejections, then routed around the
guard with `sed -i`, which the guard never covered. It prevented no
blind write; it converted content-anchored edits into an unreviewed
in-place shell rewrite.

Neither Codex nor Kimi Code enforces read-before-write at runtime.
Codex's `apply_patch` `Add File` overwrites an existing path silently;
Kimi Code's `Write` has no check at all. Both rely on the exact-match
requirement in their edit tools, which is stronger proof of inspection
than a read set, plus per-write approval.

Tool descriptions and the Kimi prompt keep telling the model to read
before editing -- that guidance matches Kimi Code's own `edit.md` and
still prevents `old_string not found` -- but no longer claim the
workspace refuses unread writes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:53:38 -04:00
Bryan Helmkamp
4167fcd39b
feat(agent): add Claude 5 profile 2026-07-25 13:15:57 -04:00
Bryan Helmkamp
e2df6e68c7
Merge pull request #640 from fabro-sh/codex/workspace-glob-semantics
Unify workspace glob semantics across sandboxes and artifacts
2026-07-25 12:05:17 -04:00
Bryan Helmkamp
dec67ec92e
fix(glob): harden artifact traversal 2026-07-25 11:56:57 -04:00
Bryan Helmkamp
d931ae6105
fix(agent): simplify GPT-5.6 tool routing 2026-07-25 11:49:40 -04:00
Bryan Helmkamp
c7ad387d3e
feat(agent): add gpt56 profile for GPT-5.6 Sol, Terra, and Luna
Codex drives the GPT-5.6 models with a much narrower tool set than the
other OpenAI models: a shell, `apply_patch`, and `update_plan`. It has no
file-read, file-write, grep, glob, or fetch tool at all -- reading and
searching go through the shell, and every write goes through
`apply_patch`. Offering 5.6 fabro's extra tools advertises affordances its
instructions never mention, so this adds a profile that registers only
what Codex does.

The profile is selected per model via `agent_profile = "gpt56"` on the six
5.6 rows (three each on `openai` and `openrouter`), following the existing
Kimi-over-a-gateway pattern. Every other model on those providers keeps
its provider default, with no code branch and no version sniffing.

- `ToolVocabulary::Codex` renames `shell` to `shell_command`; a strum
  alias keeps `from_any_name` resolving it to `NativeTool::Shell`, so
  permissions, categories, and telemetry still key on the canonical name.
- `shell_command` gains `workdir`, passed to the `cwd` argument
  `execute_shell_command` already accepted, with Codex's "always set
  `workdir`, do not `cd`" guidance.
- `prompts/gpt56.md.j2` is adapted from Codex's 5.6 `base_instructions`,
  which are byte-identical across Sol, Terra, and Luna. A header comment
  records provenance and the departures fabro's harness forces.

This is an alignment-only pass: it matches Codex's tool contract while
keeping direct tool calls. Codex actually drives 5.6 in code mode, with a
single `exec` tool taking JavaScript and every other tool reached through
a `tools` object inside a V8 isolate. That is deliberately out of scope.

Luna's `multi_agent_version: v1` (vs v2 on Sol and Terra) is also out of
scope. It only changes the sub-agent tool set, which fabro registers from
the caller rather than the profile, and fabro's current set matches
neither version exactly.

Two server cancel-timing tests are adjusted. `gpt-5.6-sol` is the
`openai` provider's default model, so runs that name no model now build a
3-tool profile instead of an 8-tool one and reach their first stage
sooner. `full_http_lifecycle_cancel` asserted `status.kind == "blocked"`
at the instant of cancel, which the worker is free to change the moment it
is signaled; it now accepts either live state, matching the tolerance its
own comment already documents for `pending_control`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:42:32 -04:00
Bryan Helmkamp
5980627bc8
refactor(glob): unify workspace path matching 2026-07-25 10:30:19 -04:00
Bryan Helmkamp
30b5d74495
Merge pull request #633 from fabro-sh/feat/sandbox-bash-contract
feat(sandbox): standardize command execution on non-login Bash
2026-07-24 23:06:34 -04:00
Bryan Helmkamp
c81ea69c73
Merge origin/main into feat/inference-observability 2026-07-24 22:55:15 -04:00
Bryan Helmkamp
7f436bf64c
fix(agent): avoid nested Bash for sandbox MCP scripts 2026-07-24 22:53:11 -04:00
Bryan Helmkamp
d4f619bc2a
fix: clean up inference observability 2026-07-24 22:50:00 -04:00
Bryan Helmkamp
2f84b67558
Merge latest origin/main into feat/sandbox-bash-contract 2026-07-24 22:45:35 -04:00
Bryan Helmkamp
29e408aa18
Merge origin/main into feat/sandbox-bash-contract 2026-07-24 22:42:28 -04:00
Bryan Helmkamp
1d939ca3eb
Merge pull request #632 from fabro-sh/fix/shell-process-outcome-reporting
fix(agent): report real shell process outcomes
2026-07-24 22:42:00 -04:00
Bryan Helmkamp
4d5458b64c
test(agent): honor Docker shell integration preconditions 2026-07-24 22:35:51 -04:00
Bryan Helmkamp
9083b1b035
Merge remote-tracking branch 'origin/main' into fix/shell-process-outcome-reporting
# Conflicts:
#	lib/components/fabro-agent/src/tools.rs
2026-07-24 22:29:06 -04:00
Bryan Helmkamp
c914fbbbe0
Merge pull request #630 from fabro-sh/fix/compaction-reasoning-token-budget
fix(agent): budget compaction summaries for reasoning models
2026-07-24 22:26:12 -04:00
Release Repro
bf62450a28
fix(agent): align summary prompt with output cap
Interpolate the visible summary allowance after applying the model max_output cap, so low-output models are not asked to produce more text than the request permits.
2026-07-24 22:21:52 -04:00
Bryan Helmkamp
fea249b4b6
Merge pull request #631 from fabro-sh/feat/kimi-agent-profile
feat(agent): add a Kimi agent profile for Moonshot and gateway routes
2026-07-24 22:20:50 -04:00
Bryan Helmkamp
debd612b52
docs(agent): clarify Kimi append precondition 2026-07-24 22:13:12 -04:00
Bryan Helmkamp
eddee10b35
fix(agent): harden Kimi profile tool contracts 2026-07-24 22:05:48 -04:00
Release Repro
5d0617f547
fix(agent): harden compaction reasoning budgets
Model default reasoning explicitly at the provider-route level so always-reasoning endpoints without effort controls receive summary headroom. Cap all summary requests at model output limits and bound retained visible summaries to the original allowance. Reuse builtin catalog fixtures and named budget constants in tests, and document the new model setting.
2026-07-24 21:58:23 -04:00
Bryan Helmkamp
1ca9fe977d
fix(sandbox): simplify Bash contract implementation 2026-07-24 21:55:36 -04:00
Bryan Helmkamp
c803354309
refactor(agent): streamline shell outcome reporting 2026-07-24 21:50:07 -04:00
Bryan Helmkamp
6659ae768a
feat(events): make inference in-flight state observable
During a long LLM turn the durable event stream was silent: between
`agent.tool.completed` and the next `agent.message` nothing was emitted,
so "the model is generating" and "the worker is wedged" were
indistinguishable from the run store, SSE, or the UI.

The signal already existed. `AssistantTextStart` fired at exactly the
right point — after `build_request()`, after compaction, immediately
before the stream opens — then was classified as streaming noise and
thrown away. This promotes it rather than inventing a new one.

Two events, each asserting only what is provable when it is emitted:

- `agent.llm.started` carries the *requested* provider/model. No usage,
  no cost, no context window: none of it exists yet, and failover can
  re-target, so `agent.message` stays authoritative for what answered.
- `agent.llm.first_output` is edge-triggered on the first output of an
  attempt and names what arrived. `ToolCall` is required, not optional:
  a turn that opens with a tool call produces no text or reasoning
  delta, so a latch keyed on those two would stay silent for exactly
  the tool-heavy rounds where liveness matters most.

`agent.llm.retry` now also fires on the one previously invisible
mid-turn path — a stream that ends without a finish event, which
replays the turn and discards its output with nothing to show for it.
Its `attempt` field was already fed by two independent counters, so an
optional `phase` (open | consume) names which loop it counts.

`StageProjection.inference` projects the open bracket. `Some` means
"the event log contains an unclosed inference bracket", not "the model
is computing now" — a SIGKILLed worker leaves it open, which is the
truthful statement of what we know, and `watchdog.timeout` remains the
authority on actually-stuck.

The close is the subtle part. Terminal cancel and wall-clock timeout
tear the session down through `discard_session` without emitting a
message, error, or interrupt, so a session-lifecycle backstop is
required. It has to be `agent.session.ended`, not
`agent.session.deactivated`: deactivation is emitted by `lease.release()`
*before* the forwarder drains queued agent events, so a queued
`agent.llm.started` can arrive after it and re-open the bracket. But
`agent.session.ended` carries no stage identity, so the close takes
ordering from the event and identity from the projection, scanning for
brackets the ending session opened. A normal stage lookup there finds
no target and silently no-ops.

Presentation states what the log proves and nothing more: no progress
bar or ETA (no completion estimate exists), "reasoning" only when the
provider sent reasoning output, elapsed counted since the request
opened, and no live animation once the run is terminal.

Scope is session-backed agent stages. One-shot completions call
`client.complete` directly and never build a session; covering them
means moving the emit point into `fabro-llm`, filed as a follow-up.

`agent.output.start` was never persisted — it existed in a name map,
an `unreachable!` arm, and docs — so the rename carries no migration
risk. Corrects `events.md`, which documented it as a real emitted
event, and the v2 proposal, which mapped it to `message.part.started`
despite it firing before the request opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:47:51 -04:00
Release Repro
51a775ea22
Merge remote-tracking branch 'origin/main' into fix/compaction-reasoning-token-budget 2026-07-24 21:37:05 -04:00
Bryan Helmkamp
cbd257c016
feat(agent): give the Kimi profile Grep's output modes and paging
Kimi Code's Grep returns matching lines, matching file names, or per-file
counts, and pages results with `head_limit` and `offset`. All four are shapes
of the result list the Sandbox trait already returns, so the Kimi profile gets
them without any provider work.

Scoped to the Kimi profile. The other profiles keep fabro's grep tool: these
options exist because Kimi models are trained against them, not because every
model should be handed more knobs.

Two details worth knowing when reading it. Extracting a file path means
parsing the `<path>:<line>:<content>` prefix, which the underlying search omits
when scanning a single file, so the search root is the fallback; the parser
also walks candidate separators so a colon inside matched content is not
mistaken for the line-number field. And `head_limit` is only pushed down to the
search as a result cap in `content` mode, where results and lines are the same
thing -- capping lines early would undercount files for the other two modes.

Kimi Code's `type`, `multiline`, and `include_ignored` are still absent. They
would have to reach ripgrep flags through new Sandbox trait methods
implemented across the local, Docker, and Daytona providers, and a parameter
that is advertised but ignored is worse than one that is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:35:02 -04:00