mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
147 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
91d11eb04d
|
fix(llm): preserve raw compatible tool arguments (#448)
## Summary Fixes #435. Preserve raw non-JSON tool-call arguments for custom/freeform tools when using the OpenAI-compatible Chat Completions adapter. This keeps `apply_patch` receiving the raw patch text instead of `{}` when LiteLLM/openai-compatible providers emit Codex-style freeform patch calls. Also extends the OpenAI twin so black-box tests can exercise the Chat Completions path with raw tool-call arguments. ## Test Plan - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-agent --test it openai_compatible_twin_preserves_raw_apply_patch_arguments --run-ignored only` - `cargo nextest run -p fabro-llm` - `cargo nextest run -p fabro-test` |
||
|
|
2e39dfc70e
|
fix(agent): align compaction preserve boundary (#449)
## Summary Follow-up to fabro-sh/fabro#447. This keeps context compaction's effective preserve boundary consistent between summary generation, history mutation, and emitted telemetry so tool-call/result pairs that remain in raw history are not also summarized. The branch also tightens the OpenAI twin support added for this regression: scripted usage is modeled as a single `TokenUsage`, SSE completion payloads reuse the canonical Responses JSON shape, and request validation now treats custom tool-call outputs as tool outputs instead of spreading raw item-type string checks. ## Verification - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-agent compaction` - `cargo nextest run -p twin-openai` - `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --profile e2e --run-ignored only --test it openai_twin_compaction_preserves_tool_call_pairs` - `git diff --check origin/main...HEAD` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
81554581ca
|
fix(agent): preserve tool-call pairs during compaction (#447)
## Summary Fixes OpenAI Responses requests after context compaction by ensuring preserved tool results are not separated from the assistant tool calls that produced them. The previous fixed-size preserved tail could retain a `function_call_output` while dropping the matching `function_call`, which OpenAI rejects as an orphaned tool result. ## Changes - Extends `History::compact` so the preserved range moves backward until every kept tool result has its matching assistant tool call. - Adds a unit invariant test for compacted histories that serialize tool results. - Adds an OpenAI twin integration regression that forces compaction during a tool-use loop. - Teaches the OpenAI twin to validate orphaned `function_call_output` items and script response usage counts for deterministic compaction tests. ## Test Plan - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --test it openai_twin_compaction_preserves_tool_call_pairs --run-ignored all` - `cargo nextest run -p fabro-agent` - `cargo nextest run -p twin-openai` - `cargo nextest run -p fabro-test` - `cargo +nightly-2026-04-14 clippy -p fabro-agent -p fabro-test -p twin-openai --all-targets --no-deps -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
ab68fc27d1
|
fix(workflow): preserve usage across session compaction (#420)
## Summary Fixes shared-thread workflow stages that compact their session before routing/audit bookkeeping finishes. The workflow backend now records token usage from each `Session::process_input` call as it happens, instead of slicing assistant turns out of the final session history after the session may have been compacted or replaced. ## Changes - Track per-input token usage inside `fabro-agent::Session` alongside the existing timing data. - Use the recorded per-input usage in the workflow LLM backend for initial prompts, retry-after-compaction prompts, and schema repair prompts. - Keep the invariant panic message for inconsistent session history explicit with `expect(...)`. - Add a black-box workflow integration test that drives a shared-thread audit through pre-routing compaction and asserts the audit still succeeds. ## Verification - `ulimit -n 4096 && cargo nextest run --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cd apps/fabro-web && bun test --isolate` - `cd apps/fabro-web && bun run typecheck` - `cd lib/packages/fabro-api-client && bun run typecheck` - After rebasing onto current `origin/main`: `ulimit -n 4096 && cargo nextest run -p fabro-workflow --test it integration::shared_thread_compaction_before_routing_audit_succeeds` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
01892185ff
|
Replace bare unwrap() with documented expect() across production runtim… (#415)
Audit and remediation pass enforcing the project's
no-panic-in-production policy. Every `unwrap()` on a mutex/RwLock in
reachable runtime code is replaced with `expect()` carrying a message
that explains *why* the lock cannot be poisoned (no code panics while
holding it). Bare `unreachable!()` and `panic!()` calls are updated with
messages that name the invariant being asserted. One genuine bug is
fixed in the process.
## What changed
**`unwrap()` → `expect()` on locks** (`fabro-core`, `fabro-oauth`,
`fabro-util`, `fabro-workflow/*`, `fabro-server`): Every
`Mutex`/`RwLock` `.unwrap()` in production paths now carries the
standard justification pattern: `"<name> mutex/RwLock should not be
poisoned: no code panics while holding this lock"`.
**`unreachable!()` and `panic!()` message quality**: Bare
`unreachable!()` calls in `subagent.rs`, `wait.rs`, `condition.rs`,
`event/convert.rs`, and `server.rs` now name the structural invariant
(e.g. "outer match arm already verified…"). The `panic!` in `tools.rs`
now includes the offending name and the expected format, making it
actionable.
**`sha_newtype` / `short_sha_newtype` in `run_files.rs` — actual bug
fix**: These helpers previously called `unwrap_or_else(|e| panic!(…))`
on git output, meaning a malformed SHA from a real git subprocess would
panic in a request handler. They now return `Result<T, ApiError>` and
propagate errors to callers, which in turn propagate with `?`. This is
the only change that alters observable behavior under failure.
**Demo-only panics in `fabro-server/src/demo/mod.rs`**: Panic messages
updated to clarify that these paths operate on hardcoded compile-time
constants, so the panic is a programming-error guard rather than a
runtime failure guard.
## Design note
The lock-poisoning `expect` messages all follow a single template so
reviewers can quickly verify the claim: if you ever add code that can
panic inside a lock guard scope, the message becomes a lie and that must
be caught in review. The uniformity is intentional.
### Fabro Details
<details>
<summary>Ran 0 stages in 64m 54s for $14.28</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **64m 54s** | **$14.28** | **0** |
</details>
<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>
```dot
digraph Goal {
graph [
goal="Complete the user-provided goal",
rankdir=LR,
max_node_visits=30
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [
label="Work",
thread_id="goal",
fidelity="full",
max_visits=12,
prompt="@prompts/continue.md"
]
audit [
label="Completion Audit",
thread_id="goal",
fidelity="full",
goal_gate=true,
retry_target="work",
output_schema="routing",
output_retries=2,
max_visits=12,
prompt="@prompts/audit.md"
]
start -> work -> audit
audit -> exit [label="Done", condition="outcome=succeeded"]
audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
audit -> work [label="No clear verdict"]
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
37f1d26d4c
|
Fix stage inference/tool timing reporting (was always zero) (#408)
## Summary Every `stage.completed` and `run.completed/failed` event has reported `inference_time_ms: 0, tool_time_ms: 0` since timing fields were wired up in #343. Two independent bugs caused this: handlers never populated `Outcome.timing`, and engine-failure terminal paths discarded the rolled-up conclusion entirely. ## What changed and why ### Bug 1 — Handlers never populated `Outcome.timing` **`fabro-agent/session.rs`**: Added `SessionInputTiming { inference: Duration, tool: Duration }` accumulators to `Session`. `run_single_input` now takes a `&mut SessionInputTiming` and records elapsed time at every exit point of the `'streamattempts` loop (stream open, retry, cancel, error, normal completion) plus a `tool_start` / `tool_elapsed` wrap around `execute_tool_calls`. The per-input total is exposed via `session.last_input_timing()` after `process_input_with_runtime` returns, even on error. **`CodergenResult::Text`**: Added a `timing: StageTiming` field. All backends now populate it: - `AgentApiBackend::run` accumulates `session.last_input_timing()` across inputs and any structured-output repair turns (repair turns now use `process_input_with_runtime` instead of `process_input` so timing is captured there too). - `AgentApiBackend::one_shot` wraps `complete_one_shot_request` with `Instant`/`elapsed` across repair iterations; all time is attributed to inference. - `AgentAcpBackend::run` uses `result.duration_ms` attributed entirely to inference (ACP is opaque about the split). **`AgentHandler`, `PromptHandler`, `FanInHandler`, `CommandHandler`**: Each now sets `outcome.timing = Some(timing)` from the backend result before returning. `CommandHandler` attributes `result.duration_ms` to tool time (`StageTiming::active_only(0, duration_ms)`). The failure branches (structured-output exhausted retries) also carry timing forward so no timing is lost on partial success. **`StageTiming::active_only`**: New constructor added to `fabro-types` for the handler→executor hop where wall time is ignored (executor's own stopwatch is authoritative for wall). ### Bug 2 — Engine-failure paths discarded the conclusion **`start.rs`**: Introduced `emit_workflow_run_failed` as a shared helper that calls `build_conclusion_from_store` (which already does the full per-stage rollup) and uses `conclusion.timing` and `conclusion.billing` when emitting `WorkflowRunFailed`, instead of `RunTiming::wall_only(...)` and `None`. All three terminal failure paths now go through this helper: - `persist_terminal_engine_failure` — main `VisitLimitExceeded`/engine-error path - `DetachedRunBootstrapGuard::drop` — takes `RunStoreHandle` as a new field (cloned in at arm time) - `DetachedRunCompletionGuard::drop` — same - `persist_detached_failure` — now accepts `&RunStoreHandle` and delegates to `emit_workflow_run_failed` ### Refactoring `test_usage` helper was duplicated across `billing_rollup` and `event/convert` test modules; both now import from `crate::test_support`. `scheduler_capacity` predicate (`counts_toward_scheduler_capacity`) was extracted from the inline closure in `spawn_scheduler` and reused in the `GET /system/info` handler for the new `scheduler_slots_used` field — a pre-existing separate fix included in this changeset. ### Plan Summary - **A1** — `SessionInputTiming` accumulators in `Session`; `last_input_timing()` getter - **A2** — `CodergenResult::Text { timing }` field; all three backends populate it - **A3** — All four active-work handlers (`agent`, `prompt`, `fan_in`, `command`) set `outcome.timing` - **B1** — `persist_terminal_engine_failure` uses conclusion's rolled-up timing + billing - **B2** — Both drop guards and `persist_detached_failure` also use `emit_workflow_run_failed` ### Fabro Details <details> <summary>Ran 8 stages in 76m 38s for $39.81</summary> | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 1s | – | 0 | | preflight_compile | 2m 3s | – | 0 | | preflight_lint | 2m 18s | – | 0 | | implement | 35m 53s | $17.74 | 0 | | simplify_opus | 22m 51s | $17.18 | 0 | | simplify_gpt | 4m 0s | $4.89 | 0 | | verify | 8m 59s | – | 0 | | **Total** | **76m 38s** | **$39.81** | **0** | </details> <details> <summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14 edges)</summary> ```dot digraph ImplementPlan { graph [ goal="Implement and simplify", model_stylesheet=" * { model: claude-opus-4-7; } " ] rankdir=LR start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0] preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0] preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0] fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3] implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"] simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"] simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"] verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"] fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3] start -> toolchain toolchain -> preflight_compile [condition="outcome=succeeded"] toolchain -> exit preflight_compile -> preflight_lint [condition="outcome=succeeded"] preflight_compile -> exit preflight_lint -> implement [condition="outcome=succeeded"] preflight_lint -> fix_lints fix_lints -> preflight_lint implement -> simplify_opus -> simplify_gpt -> verify verify -> exit [condition="outcome=succeeded"] verify -> fixup fixup -> verify } ``` </details> ⚒️ Generated with [Fabro](https://fabro.sh) --------- Co-authored-by: Fabro <noreply@fabro.sh> |
||
|
|
e8f0aceee8
|
refactor: rationalize server secret scopes (vault-only for optional int… (#401)
## Summary
Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.
## What changed
**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.
**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.
**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).
**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.
**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.
**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.
**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.
### Plan Summary
- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.
### Fabro Details
<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
7d655d7c95
|
feat: expose effective agent tool list via StageProjection.agent_tools (#388)
## Summary
Adds `StageProjection.agent_tools` — a replay-authoritative list of
every tool the model can actually call — so UI and API consumers no
longer have to infer tool availability from `permission_level`. The
field is populated by a new `agent.tools.available` durable event
emitted once per stage session after provider-profile setup, MCP
integration, and access-policy filtering are complete.
### Plan Summary
- **Types (`fabro-types`)** — new `AgentToolSummary`, `AgentToolSource`,
`AgentToolCategory`, `AgentToolsAvailableProps`, and
`EventBody::AgentToolsAvailable`; `agent_tools: Vec<AgentToolSummary>`
added to `StageProjection` with skip-serializing-if-empty semantics.
- **Tool registry (`fabro-agent`)** — `ToolSource::Mcp` gains
`original_name` (no more re-parsing the qualified name downstream);
`ToolDefinitionWithSource::to_agent_tool_summary()` maps to the public
DTO; `Session::effective_tools()` / `agent_tool_summaries()` expose the
filtered list; `tool_category` split into `tool_category` (CLI gate,
defaults `Shell`) and `known_tool_category` (projection, returns `None`
→ `Other` for unknown tools).
- **Projection reducer (`fabro-store`)** — `AgentToolsAvailable`
replaces the stage's `agent_tools`; `AgentToolStarted` flips `invoked =
true` on the matching entry; legacy runs without the event get an empty
list.
- **OpenAPI + generated clients** — `AgentToolSummary`,
`AgentToolSource`, `AgentToolCategory`, `AgentToolsAvailableProps`
schemas added; `StageProjection.agent_tools` field added; `build.rs`
replacements wire them to the `fabro-types` structs.
- **Web sidebar** — new collapsible "Tools" section renders name,
description, source/category badge, and used/available state from
`stage.agent_tools`; `permission_level` is kept as secondary fallback
metadata for legacy stages.
## Key design decisions
- **`agent_tools`, not `tools`** — avoids ambiguity with MCP nested
tools and completion API tool definitions.
- **Dedicated `agent.tools.available` event** — cleaner than overloading
`agent.session.activated`; replacement semantics on replay mean
re-emission works if tool registration ever becomes mutable.
- **`original_name` carried in `ToolSource::Mcp`** — stored by the MCP
integration at registration time so the projection never needs to
re-parse qualified names like `mcp__filesystem__read_file`.
- **`invoked` is projected state, not event state** — the availability
event always emits `false`; replay of `agent.tool.started` flips
matching entries.
- **Parameter schemas omitted** — `AgentToolSummary` carries only
`name`, `description`, `source`, `category`, and `invoked` to keep
payloads small and avoid exposing implementation detail.
- **`AgentToolCategory::Other` for unknown tools** — unlike the CLI
permission gate (which defaults to `Shell` to require approval), the
projection uses `Other` to surface unrecognized MCP/skill tools
accurately.
### Fabro Details
<details>
<summary>Ran 8 stages in 56m 37s for $58.75</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 0s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 22m 54s | $42.64 | 0 |
| simplify_opus | 16m 41s | $10.62 | 0 |
| simplify_gpt | 3m 43s | $5.49 | 0 |
| verify | 8m 33s | – | 0 |
| **Total** | **56m 37s** | **$58.75** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
98c26d5370
|
Fold context-window data into agent.message, remove snapshot event (#390)
## Summary
Removes the standalone `agent.context_window.snapshot` event and instead
attaches the context-window projection directly to `agent.message`. This
eliminates the async provider token-count API calls that the old
approach required, and simplifies the event log to a single event type
carrying all post-response agent data.
## What Changed and Why
**Before:** After each LLM turn, the agent emitted a separate
`agent.context_window.snapshot` event — first a local estimate, then
potentially a second one after an async `count_input_tokens` call
resolved (or after response usage arrived). This required fingerprint
deduplication state, a `close_token` to cancel in-flight counts, and
frontend handling for the extra event type.
**After:** The `AgentEvent::AssistantMessage` variant carries an
`Option<StageContextWindowProjection>`. The projection is computed
locally at request-build time and then refined using response token
usage when available (`ResponseUsageScaledBreakdown`), or kept as a
`LocalEstimate` when response usage is absent. No provider API calls are
made.
### Plan Summary
- **Task 1:** Added `context_window:
Option<StageContextWindowProjection>` to `AgentMessageProps` (Rust types
+ OpenAPI), removed `AgentContextWindowSnapshotProps` and
`EventBody::AgentContextWindowSnapshot`.
- **Task 2:** Removed the spawned `count_input_tokens` task,
`close_token`, fingerprint sets, and both snapshot-emit methods from
`Session`. Added `context_window_from_response_usage` to
`context_window.rs`; `BuiltRequest` now holds the local projection
instead of the tool list.
- **Task 3:** Workflow conversion copies `context_window` from
`AgentEvent::AssistantMessage` into `AgentMessageProps`; store reducer
reads it from `AgentMessage` instead of the removed snapshot variant and
stamps `event_seq`.
- **Task 4:** GET endpoint tests updated to seed data via
`agent.message` with embedded context-window; endpoint behavior
unchanged.
- **Task 5:** Frontend constant and tests for
`agent.context_window.snapshot` removed; `agent.message` already
invalidates `stageContextWindow` through existing stage-activity
handling. TypeScript client regenerated with the new `AgentMessageProps`
model.
### Key Design Decisions
- **No provider token-count API calls** during normal execution —
context-window accuracy relies on local estimates scaled by response
usage, which is always available for successful turns.
- **Failed-before-response turns** emit no context-window data
(`context_window: None`), matching the old behavior where a snapshot
would have been emitted but response-usage scaling would never arrive.
- `BuiltRequest` drops the `tools` field (only needed for the
now-removed snapshot emission path); the local projection is computed at
build time and stored directly.
### Fabro Details
<details>
<summary>Ran 8 stages in 60m 3s for $55.78</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 1s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 30m 39s | $44.82 | 0 |
| simplify_opus | 10m 48s | $4.03 | 0 |
| simplify_gpt | 5m 1s | $6.93 | 0 |
| verify | 8m 47s | – | 0 |
| **Total** | **60m 3s** | **$55.78** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
990324710c
|
Add legacy SSE MCP transport support (#386)
## Summary Adds support for MCP servers that use the SSE-based HTTP transport, including Playwright MCP. Fabro already supports stdio and Streamable HTTP MCP servers. Some MCP servers still expose the SSE transport shape where the client opens an SSE stream, receives an `endpoint` event, and sends JSON-RPC requests back to that endpoint. This PR adds an explicit `protocol = "sse"` option while keeping Streamable HTTP as the default. ## What Changed - Added `McpHttpProtocol` with `streamable_http` as the default and `sse` as an opt-in protocol. - Added an SSE MCP client transport implementation. - Wired HTTP MCP setup to choose Streamable HTTP or SSE based on config. - Added `protocol = "sse"` support for both `http` and `sandbox` MCP entries. - Updated sandbox MCP resolution so SSE sandbox servers connect through the preview `/sse` path. - Documented `protocol = "sse"` for Playwright MCP. - Added an integration test covering SSE initialize, tool listing, and tool calls. ## Example ```toml [run.agent.mcps.playwright] type = "sandbox" protocol = "sse" command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"] port = 3100 startup_timeout = "60s" tool_timeout = "2m" ``` ## Compatibility Existing MCP configs are unchanged because `protocol` defaults to `streamable_http`. ## Validation - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-mcp` - `cargo check -p fabro-agent -p fabro-workflow -p fabro-config` --------- Co-authored-by: Bryan Helmkamp <bryan@brynary.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
858904e7ac
|
Fix token count provider log context
Keep background context-window token count requests inside the current tracing span so run-scoped logs retain run_id. Label OpenAI input token count provider failures with operation=input_token_count for easier diagnosis. |
||
|
|
596b105b06
|
fix(skills): Ensure the local server can access project skills (#383)
The local server wasn't setting git_root, and wasn't defaulting to the workspace working directory, so it didn't find project-specific skills. --------- Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
846d1e91af
|
Polish stage insights sidebar
- Track which MCP servers the agent invoked. New `invoked: bool` on `McpServerProjection` (OpenAPI + Rust type + generated TS client), set by the projector when an `AgentToolStarted` event has an `mcp__<server>__*` tool_name. UI shows `used/total` in the section header, replaces the tool count with `used` on invoked rows, and dims rows that weren't invoked. Sticky across status re-reads. - Quiet noisy context-window warnings. When the snapshot's total is provider-authoritative (ProviderApiScaledBreakdown or ResponseUsageScaledBreakdown), drop local-estimator warning codes from the snapshot — they imply the user-facing total is wrong when it isn't. Also dedupe by code so a 35-turn conversation with opaque reasoning blocks no longer surfaces 35 copies of the same warning. - Reword the legitimately-local warnings. "opaque provider context estimated from JSON" → "Some content couldn't be precisely tokenized; total is approximate." Same treatment for the media, provider-options, and whole-request local-estimate messages. - Rename the sidebar header from "INSIGHTS" to "AGENT" to better describe what it shows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
39fa73d5e2
|
Add context-window snapshot API for agent stages (#378)
## Summary
Adds a best-effort `GET
/api/v1/runs/{id}/stages/{stageId}/context-window` endpoint that exposes
model-visible input-token usage, broken down by category (system prompt,
tools, MCP tools, skills, memory, conversation, other). The endpoint
degrades gracefully: it returns a stored projection snapshot when the
stage is inactive, and `available: false` when no snapshot has ever been
observed rather than surfacing count gaps as HTTP errors.
### Plan Summary
- **Unit 1** – OpenAPI schemas (`StageContextWindow`,
`StageContextWindowProjection`, breakdown/enum types) and generated Rust
+ TypeScript clients, with `fabro-api` build-time type replacements
pointing at the hand-written `fabro-types` structs.
- **Unit 2** – `ToolSource` enum on `RegisteredTool` (Native / Mcp /
Skill) + `ToolDefinitionWithSource`; new `context_window.rs` builder in
`fabro-agent` that assembles a content-free category breakdown at
request-assembly time; `fabro-llm::token_count` narrow public helpers
(`estimate_message_tokens`, `estimate_tool_definition_tokens`,
`estimate_request_control_tokens`).
- **Unit 3** – `AgentEvent::ContextWindowSnapshot` carries a
`StageContextWindowProjection`; the session emits a local snapshot
immediately, then a provider-scaled replacement (or
response-usage-scaled replacement) asynchronously; fingerprinting
prevents double-counting the same request.
- **Unit 4** – Server endpoint (stubbed routing; full handler targets a
follow-up) returning the latest projected snapshot.
- **Unit 5** – `queryKeys.runs.stageContextWindow`,
`useRunStageContextWindow` hook, and SSE invalidation for
`agent.context_window.snapshot` and all stage-lifecycle events.
### Key design decisions
**Agent-side counting, not server-side.** The exact `fabro_llm::Request`
only exists inside the active agent session. Rather than moving raw
prompt/message content into server-managed state, the session counts the
request it already has and emits content-free projection events. The
HTTP endpoint just reads the latest durable snapshot.
**Hybrid category ownership.** `fabro-agent` owns the category taxonomy
(it sees memory documents, skills, MCP registration, and session
history); `fabro-llm` exposes narrow estimation helpers. Neither crate
leaks the other's concerns.
**Provider count is async and non-blocking.** A spawned task calls
`Client::count_input_tokens(..., PreferProvider)` with a clone of the
request. It is cancelled via `close_token` when the session closes.
Failures produce a warning on the snapshot, not a stage error.
**`available: false` instead of 4xx for known-but-unobserved stages.**
The sidebar needs stable empty states; HTTP errors only mean the run or
stage doesn't exist.
```mermaid
flowchart TB
A[Session::build_request] --> B[build_local_snapshot\nLocalEstimate]
B --> C[emit ContextWindowSnapshot]
C --> D{provider count\nspawned task}
D -- success --> E[scaled_snapshot\nProviderApiScaledBreakdown]
D -- failure --> F[warning appended to local snapshot]
E --> G[emit ContextWindowSnapshot]
G --> H[run_state reducer\nupdates StageProjection.context_window]
F --> H
H --> I[GET context-window endpoint\nreturns projection]
```
**`ToolSource` on every `RegisteredTool`.** All 20+ `make_*_tool` call
sites are updated to set `ToolSource::Native`; MCP tools get
`ToolSource::Mcp { server_name }` at registration time;
`make_use_skill_tool` gets `ToolSource::Skill`. A parallel
`definitions_with_source_for_policy` method preserves existing
`definitions_for_policy` behaviour unchanged.
### Fabro Details
<details>
<summary>Ran 8 stages in 90m 57s for $70.01</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 1m 59s | – | 0 |
| preflight_lint | 2m 11s | – | 0 |
| implement | 45m 14s | $48.47 | 0 |
| simplify_opus | 25m 50s | $18.29 | 0 |
| simplify_gpt | 6m 12s | $3.25 | 0 |
| verify | 8m 59s | – | 0 |
| **Total** | **90m 57s** | **$70.01** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
def37896cd
|
Expose PermissionLevel on StageProjection and add sortable run columns (#373)
## Summary
Two independent additions landed together: surfacing `PermissionLevel`
on `StageProjection` (the primary goal), and making four previously
unsortable run-list columns (`repo`, `title`, `workflow`, `changes`)
sortable.
## Permission Level on StageProjection
`PermissionLevel` (`read-only | read-write | full`) was already resolved
at session start inside `fabro-agent` but never reached the API. This
wires it through the existing `agent.session.activated` event rather
than introducing a new event.
The data flow:
```mermaid
graph TB
A[SessionOptions.permission_level] -->|set at CLI build_tool_approval| B[Session.permission_level]
B -->|read in api.rs| C[ActivationLeaseOptions.permission_level]
C -->|emitted as| D[Event::AgentSessionActivated.permission_level]
D -->|convert.rs| E[EventBody::AgentSessionActivated.permission_level]
E -->|run_state.rs apply_event| F[StageProjection.permission_level]
F -->|OpenAPI + TS client| G[API consumers]
```
Key decisions:
- **No new event or type.** `PermissionLevel` is reused from
`fabro_types::session` directly; `AgentSessionActivatedProps` gains one
optional field with `skip_serializing_if`, so older persisted events
deserialize cleanly to `None`.
- **`Option<PermissionLevel>` on `StageProjection`** follows the same
pattern as `provider_used` — agent stages populate it, non-agent stages
leave it `None`. No migration required.
- **`AgentSessionActivatedProps` is now a progenitor type replacement**
so the API crate and the canonical type stay in sync (verified by the
new `agent_session_activated_props_round_trip` test).
### Plan Summary
- `fabro-agent` `config.rs` / `session.rs` — store and expose
`permission_level` on `SessionOptions`
- `fabro-types` `run_event/agent.rs` — add field to
`AgentSessionActivatedProps`
- `fabro-types` `run_projection.rs` — add field to `StageProjection`
- `fabro-workflow` `api.rs` / `activation_lease.rs` / `convert.rs` /
`events.rs` — thread the value to the emission site
- `fabro-store` `run_state.rs` — fold into projection on
`AgentSessionActivated`, plus new unit test
- OpenAPI schema, `fabro-api` build.rs, TS client — all
regenerated/updated
## Sortable Run Columns
`repo`, `title`, `workflow`, and `changes` columns were rendered as
plain `<th>` elements with no sort affordance. They now use `SortHeader`
in the frontend, the server-side `RunsSortKey` enum gains the four
variants, and the OpenAPI `ListRunsSortEnum` and TS client enum are
extended to match.
Sort helpers (`run_repo_key`, `run_title_key`, `run_workflow_key`,
`run_changes_total`) normalize to lowercase strings / integer totals and
compose with the existing stable ULID tiebreak.
### Fabro Details
<details>
<summary>Ran 9 stages in 53m 42s for $18.48</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 14s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 20m 37s | $11.87 | 0 |
| simplify_opus | 8m 0s | $3.61 | 0 |
| simplify_gpt | 5m 11s | $2.50 | 0 |
| verify | 4m 48s | – | 0 |
| fixup | 9m 56s | $0.50 | 0 |
| **Total** | **53m 42s** | **$18.48** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-bot <fabro-bot@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
dbe3e3966d
|
Migrate sandbox config to named environments; add InterviewOption metad… (#372)
## Summary
Two related changes land together: the sandbox configuration surface is
replaced with a named-environment model, and `InterviewOption` gains
`description` and `preview` fields needed for the mid-stage agent
interview tools described in the plan.
## What changed
### Named environments (was `[run.sandbox]`)
`[run.sandbox]` and its provider-specific sub-tables
(`[run.sandbox.daytona]`, `[run.sandbox.docker]`) are replaced by a
two-level model:
- **`[environments.<slug>]`** — reusable catalog entries with a unified
shape: `provider`, `image`, `resources`, `network`, `lifecycle`,
`labels`, `volumes`, `env`.
- **`[run.environment] id = "<slug>"`** — selects which environment a
run uses.
- **`[run.environment.<field>]`** — sparse run-level overrides applied
on top of the selected environment.
The OpenAPI schema drops `RunSandboxSettings`, `DaytonaSettings`,
`DaytonaSnapshotSettings`, `DaytonaNetworkLayer`, and `DockerSettings`
in favour of `EnvironmentSettings`, `RunEnvironmentSettings`, and the
new sub-schemas (`EnvironmentImageSettings`,
`EnvironmentResourcesSettings`, `EnvironmentNetworkSettings`,
`EnvironmentLifecycleSettings`, `EnvironmentVolumeSettings`). The
`--sandbox` CLI flag becomes `--environment`.
All docs, example configs, `.fabro/project.toml`, and the
automation-detail / run-settings UI panels are updated to the new shape.
The run-settings page renames "Sandbox" → "Environment" and reads from
the new field paths.
### `InterviewOption` metadata fields
`description` and `preview` are added to the canonical `InterviewOption`
type (OpenAPI, helpers.ts, interview-dock, human-qa renderer). Both are
treated as untrusted model-authored text — stored and displayed as plain
strings, never rendered as HTML. The `interview-dock` test asserts that
raw HTML in `preview` is not rendered. Option `description` is shown as
secondary text under the label in choice and multi-select buttons.
### `StageModelUsage` projection
`provider_used` on `RunStageInfo` and stage projections is promoted from
a freeform object to a typed `StageModelUsage` schema (with `mode`,
`provider`, `model`, `reasoning_effort`, `speed`). The
`extractStageModel` event-scraping helper is replaced by
`formatStageModelUsageLabel` and `stageModelUsageTitle`, which work
directly from the projection field. The `Stage` interface gains
`providerUsed` and the `EventsToolbar` consumes it.
### Other schema additions
`ReasoningEffort` enum, `small_default` on model info,
`SubAgentProjection`/`SkillsProjection`/`McpServerProjection` inline in
stage projections, and `TodoListProjection` moved from the run-state
top-level `todos_by_list` map into per-stage `todos`.
### Plan summary
- Replace `[run.sandbox]` config with `[environments.<slug>]` +
`[run.environment]` selection across config, OpenAPI, UI, and docs.
- Extend `InterviewOption` with `description` and `preview`; render
`description` in choice/multi-select buttons.
- Promote `provider_used` to a typed `StageModelUsage` schema; drop
event-scraping in favour of the projection field.
- Add `ReasoningEffort`, `small_default`, subagent/skills/MCP
stage-projection schemas to OpenAPI.
### Fabro Details
<details>
<summary>Ran 9 stages in 93m 2s for $48.56</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 39m 28s | $35.95 | 0 |
| simplify_opus | 22m 6s | $8.66 | 0 |
| simplify_gpt | 7m 18s | $1.66 | 0 |
| verify | 6m 33s | – | 0 |
| fixup | 12m 34s | $2.29 | 0 |
| **Total** | **93m 2s** | **$48.56** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Fabro <fabro@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
7e6052bec3
|
Surface reasoning_effort + speed in stage badge end-to-end (#363)
## Summary
The run page stage badge showed only the model name. This PR plumbs
`reasoning_effort` and `speed` from the LLM call site all the way
through the event stream, store projection, API, and UI so the badge now
renders `gpt-5.5 · high`.
### Plan Summary
- **Event props** — `AgentSessionActivatedProps` and `StagePromptProps`
gain `reasoning_effort: Option<ReasoningEffort>` and `speed:
Option<Speed>` with `serde(default, skip_serializing_if)` for
back-compat.
- **Typed projection** — `provider_used: Option<serde_json::Value>` is
replaced by `Option<StageModelUsage>`, a proper struct in `fabro-types`
with factory methods (`from_prompt_props`,
`from_agent_session_activated`). The freeform JSON bag is gone.
- **Emission sites** — `ActivationLeaseOptions` carries the new fields;
`emit_stage_prompt()` (new shared helper) resolves
`EffectiveRequestControls` via the backend and stamps them on
`Event::Prompt`. `AgentHandler` and `PromptHandler` both call this
helper instead of building the event inline.
- **ACP path** — `AgentAcpStarted` no longer writes `provider_used`; the
canonical source is the later `AgentSessionActivated` event, which is
already emitted for ACP steering sessions. Runs without a hub
legitimately leave `provider_used` unset.
- **OpenAPI** — new `StageModelUsage` and `ReasoningEffort` schemas
replace the `object | null` bag; `build.rs` maps both to the canonical
Rust types; a new `stage_model_usage_round_trip` integration test
enforces the parity requirement.
- **UI** — `extractStageModel` (event-scanning heuristic) is deleted;
replaced by `formatStageModelUsageLabel` and `stageModelUsageTitle` that
read directly off `selectedStage.providerUsed`. `parseFanInOutcome` now
sources the reducer model from `stage.prompt` instead of
`prompt.completed`.
### Key design decisions
**No type sprawl**: `fabro_model::ReasoningEffort` and `Speed` are
reused verbatim via `with_replacement` in `build.rs` — no parallel
enums.
**ACP behavior change**: previously `AgentAcpStarted` wrote a bespoke
`provider_used` blob and a later `AgentSessionActivated` would be
ignored for ACP sessions. Now `AgentSessionActivated` is the single
write path for all modes; ACP runs that never activate a steering hub
correctly leave `provider_used = null`. The integration test (`acp.rs`)
is updated to assert the new shape, and the unit test is renamed
`agent_acp_started_alone_leaves_stage_provider_used_unset` to document
intent.
**`emit_stage_prompt` helper**: both `AgentHandler` and the existing
prompt path share one function to avoid the two call sites drifting
apart again.
### Fabro Details
<details>
<summary>Ran 9 stages in 98m 44s for $65.70</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 30s | – | 0 |
| implement | 42m 19s | $26.85 | 0 |
| simplify_opus | 38m 3s | $35.17 | 0 |
| simplify_gpt | 9m 20s | $3.68 | 0 |
| verify | 3m 38s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **98m 44s** | **$65.70** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
|
||
|
|
7f84ac5e3f
|
Replace run-scoped sandbox config with named environments (#360)
## Summary
Replaces the `[run.sandbox]` configuration surface with a named,
provider-explicit environment catalog. Runs now select an environment by
slug (`[run.environment] id = "..."`) rather than configuring a sandbox
inline. Fabro resolves the catalog through normal settings precedence,
applies sparse run-level overrides, and creates a concrete sandbox from
the resolved environment.
This is a clean break — no `[run.sandbox]` compatibility layer.
### Plan Summary
- **New config shape:** Top-level `[environments.<slug>]` catalog valid
in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`. Runs
reference a slug via `[run.environment] id = "..."` with optional sparse
overrides under `[run.environment.*]`.
- **Unified environment fields:** `provider`, `image` (ref +
dockerfile), `resources` (cpu/memory/disk), `network` (mode + allow
CIDRs), `lifecycle` (preserve/stop_on_terminal/auto_stop), `labels`,
`volumes`, `env` — replacing the previous split between `[run.sandbox]`,
`[run.sandbox.docker]`, `[run.sandbox.daytona]`, and
`[run.sandbox.daytona.snapshot]`.
- **OpenAPI schema update:** `RunSandboxSettings`, `DockerSettings`,
`DaytonaSettings`, and `DaytonaNetworkLayer` replaced with
`RunEnvironmentSettings`, `EnvironmentSettings`, `EnvironmentProvider`,
`EnvironmentImageSettings`, `EnvironmentResourcesSettings`,
`EnvironmentNetworkSettings`, `EnvironmentLifecycleSettings`, and
`EnvironmentVolumeSettings`.
- **CLI flag rename:** `--sandbox <provider>` → `--environment <slug>`
on `run`, `create`, `preflight`, and `server start/restart`.
- **Provider capability model:** Hard errors for security properties a
provider cannot enforce (local with blocked/CIDR networking; docker with
CIDR allow-lists). Warnings for unsupported resource limits, volumes,
labels, auto-stop, and Docker Dockerfiles.
- **Docs and internal code updated** throughout: `.fabro/project.toml`,
workflow configs, all public docs, CLI args, manifest builders, and the
runner's GitHub credentials check.
### Provider mapping
| Environment field | Local | Docker | Daytona |
|---|---|---|---|
| `image.ref` | Ignored | Docker image | Snapshot name |
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile
(requires `image.ref`) |
| `resources.cpu/memory/disk` | Warning; ignored | cpu_quota / memory
limit / warning | Snapshot sizing |
| `network.mode = block` | **Error** | `network_mode = none` | Daytona
block |
| `network.mode = cidr_allow_list` | **Error** | **Error** | Daytona
CIDR allow-list |
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume
mounts |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona
auto-stop interval |
| `env` | Process env overlay | Container env | Sandbox env |
### Fabro Details
<details>
<summary>Ran 11 stages in 217m 39s for $129.86</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 7s | – | 0 |
| preflight_lint | 4m 9s | – | 0 |
| fix_lints | 3m 46s | $1.06 | 0 |
| implement | 76m 6s | $57.39 | 0 |
| simplify_opus | 71m 50s | $38.17 | 0 |
| simplify_gpt | 8m 27s | $2.24 | 0 |
| verify | 6m 10s | – | 0 |
| fixup | 42m 1s | $31.00 | 0 |
| fmt | 3s | – | 0 |
| **Total** | **217m 39s** | **$129.86** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
|
||
|
|
c987766641
|
agent: use API usage baseline for compaction context estimate (#366)
## Summary
Replaces the whole-history `chars / 4` compaction trigger with a Claude
Code-style hot-path estimator: find the latest assistant turn with real
provider-reported `usage.total_tokens()`, use that as the baseline, and
add local char estimates only for turns appended after it. This makes
compaction sensitive to actual provider-reported context usage
(including cache read/write and reasoning tokens) without adding any
provider token-count API calls.
### Plan Summary
- **New estimator** (`estimate_active_context_usage`): returns a
`ContextEstimate` with both a token count and an `ContextEstimateMethod`
enum tag (`ApiUsagePlusLocalDelta` or `LocalEstimate`).
- **`check_context_usage`** now returns `Option<ContextEstimate>`
instead of `bool`, and includes `estimate_method` in warning `details`.
The caller passes the estimate directly into `compact_context`, avoiding
a double-compute.
- **`compact_context`** signature drops `system_prompt` (no longer
needed) and takes the pre-computed `ContextEstimate`. The
`CompactionStarted` event is now emitted *after* the `turns.len() <=
preserve_count` no-op guard, so a no-op can never emit `Started` without
`Completed`.
- **`History::compact`** invalidates preserved assistant `usage` (resets
to `TokenCounts::default()`) so a preserved turn's pre-compaction
provider baseline never becomes the next estimate's anchor. Content,
tool calls, provider parts, and response IDs are untouched.
- **`session.compact_if_needed`** restructured to early-return on `None`
from `check_context_usage` or on compaction disabled, simplifying the
nesting.
## Key design decisions
**Why invalidate preserved assistant usage?** After compaction the prior
turns are gone, so a stored `total_tokens` from before compaction would
overstate the new context. The authoritative billing record is in
emitted run events, not in mutable runtime history.
**Why return `Option<ContextEstimate>` from `check_context_usage`?**
Avoids recomputing the estimate in `compact_context`. It also makes the
call-site idiom (`let Some(estimate) = ... else { return; }`) an
explicit gate, which is cleaner than a separate bool-then-compact
pattern.
**Why `strum::IntoStaticStr` on the method enum?** Lets the variant
serialize to a `&'static str` for the JSON `details` field without a
manual `match` or adding `serde` derives.
### Fabro Details
<details>
<summary>Ran 9 stages in 30m 54s for $7.93</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 1m 52s | – | 0 |
| preflight_lint | 2m 6s | – | 0 |
| implement | 8m 11s | $3.47 | 0 |
| simplify_opus | 11m 7s | $3.53 | 0 |
| simplify_gpt | 2m 39s | $0.93 | 0 |
| verify | 4m 4s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **30m 54s** | **$7.93** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
8f36772af3
|
feat(llm): add input token counting (#359)
## Summary Adds an optional `fabro-llm` API for counting model-visible input tokens without creating a completion, with provider-native counting where available and deterministic local estimates when exact counting is unavailable or intentionally avoided. ## Details - Adds `Client::count_input_tokens` plus `InputTokenCountPreference` modes for provider-preferred, provider-required, and estimate-only behavior. - Implements provider count endpoints for Anthropic, Gemini, and OpenAI while filtering request bodies to count-supported fields. - Adds strict fallback semantics so local estimates do not hide bad credentials, invalid requests, unsupported models, context-length/content-filter failures, or other deterministic provider errors. - Adds a deterministic local estimator with explicit warning codes for local estimates, media heuristics, opaque provider context, and provider options. - Documents privacy implications: provider-native counting sends the provider-serialized model-visible request to the upstream count endpoint, while `EstimateOnly` keeps counting local. ## Verification - `cargo nextest run -p fabro-llm` - 385 passed, 10 skipped - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warnings` - `cargo build --workspace` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context unknown, thinking not disclosed) via [Codex](https://openai.com/codex) |
||
|
|
29e2750a00
|
feat(agent): add Anthropic TaskGet and task reminders
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Expose TaskGet for Claude-style task inspection, refresh Anthropic prompt and tool guidance, and remind long-running sessions to use task tracking when those tools are available. |
||
|
|
eb4891b1b0
|
refactor(agent): simplify reviewed changes
Use raw sandbox reads for memory and skills, keep line-numbered reads focused on display, and share retry-delay handling across agent and LLM code. Trim task tool descriptions, bound multi-file read concurrency, restore Docker's text read path, and add the reviewed implementation plan docs. |
||
|
|
d1cc47324d
|
fix(agent): use raw sandbox reads for edits
Separate raw file reads from the line-numbered display API so apply_patch and edit_file operate on unformatted UTF-8 content. Keep read_file/read_many_files model-facing output numbered and cover regressions for prefix corruption. |
||
|
|
96fff07a84
|
fix(agent): retry retryable mid-stream LLM failures
Replay retryable stream failures from the last committed turn state, clear partial visible output before retry or terminal failure, and map Anthropic stream error events into structured provider errors. |
||
|
|
a308734e9b
|
feat(agent): align Anthropic task prompt guidance
Modularize the Anthropic system prompt into Claude-style sections and expand TaskCreate, TaskUpdate, and TaskList descriptions with task-management guidance adapted to Fabro's tool surface. |
||
|
|
a2e2cbc7ed
|
Add agent context observability events (memory, skills, MCP tools) (#356)
## Summary
Adds three new durable run events — `agent.memory.loaded`,
`agent.skills.discovered`, and `agent.skill.activated` — and enriches
`agent.mcp.ready` with names-only tool summaries. Consumers can now
reconstruct what memory, skills, and MCP tools were active for any agent
run by reading the event stream, without needing to inspect session
state.
### Plan Summary
- **`fabro-types`**: New prop structs (`AgentMemoryLoadedProps`,
`AgentSkillsDiscoveredProps`, `AgentSkillActivatedProps`,
`AgentMcpToolSummary`) and three new `EventBody` variants with canonical
dot-name serialization. `AgentMcpReadyProps.tools` uses
`#[serde(default, skip_serializing_if = "Vec::is_empty")]` for backwards
compatibility.
- **`fabro-agent/memory.rs`**: `discover_memory` now returns
`Vec<MemoryDocument>` carrying path, byte counts, and truncation flag
alongside content. The content itself is never put in any event payload.
- **`fabro-agent/types.rs`**: Adds `MemoryLoaded`, `SkillsDiscovered`,
`SkillActivated`, and enriched `McpServerReady` internal variants.
Removes `SkillExpanded` (replaced by `SkillActivated { source: Slash
}`). New variants are **not** classified as streaming noise, so they
persist.
- **`fabro-agent/session.rs`**: Emits `MemoryLoaded` before skills init,
`SkillsDiscovered` after skill discovery, and enriches `McpServerReady`
with summaries from `McpConnectionManager::tool_summaries_for_server`.
Slash expansion now emits `SkillActivated { source: Slash }` instead of
`SkillExpanded`.
- **`fabro-agent/skills.rs`**: `make_use_skill_tool` emits
`SkillActivated { source: Tool }` on successful lookup only.
- **`fabro-mcp/connection_manager.rs`**: New `tool_summaries_for_server`
returns sorted `(qualified_name, original_name)` pairs without leaking
descriptions or schemas.
- **`fabro-workflow/event/convert.rs` + `names.rs`**: Converts all new
agent events to their typed `fabro-types` props, including `visit`
injection. Removes dead `SkillExpanded` arm.
- **`docs/internal/events.md`**: Documents all new event shapes with
full property tables; notes that `agent.skill.expanded` is replaced.
### Key design decisions
- Both `MemoryLoaded` and `SkillsDiscovered` are emitted even when the
result is empty. This lets consumers distinguish "no memory/skills
found" from "event not yet reported."
- Memory file **contents are never included** in any event payload —
only `path`, `byte_count`, `loaded_bytes`, and `truncated`.
- `agent.mcp.ready` `tools` field is omitted from JSON when empty
(`skip_serializing_if`), preserving wire compatibility with existing
stored events.
- `SkillActivated` is persisted (not filtered as streaming noise),
unlike the former internal-only `SkillExpanded`.
### Fabro Details
<details>
<summary>Ran 9 stages in 57m 15s for $27.15</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 17s | – | 0 |
| preflight_lint | 2m 30s | – | 0 |
| implement | 21m 18s | $16.27 | 0 |
| simplify_opus | 15m 21s | $6.52 | 0 |
| simplify_gpt | 10m 33s | $4.35 | 0 |
| verify | 4m 24s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **57m 15s** | **$27.15** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
df6f62db6a
|
test(agent): pass root session id in tool execution tests
Update stale test-only call sites after execute_and_emit_one_tool gained an explicit root_session_id argument. |
||
|
|
4df9e483ac
|
Merge remote-tracking branch 'origin/main' into ask-fabro-welcome-prompts | ||
|
|
a42634cc54
|
Add event-sourced todo tools for OpenAI and Anthropic profiles (#353)
## Summary
Adds a shared todo/task engine behind two model-native tool surfaces,
with all mutations persisted as individual run events and replayed into
`RunProjection`.
### Plan Summary
- **New domain types** in `fabro-types`: `TodoStatus`, `TodoListKind`,
`TodoProjection`, `TodoListProjection`, and `todos_by_list` on
`RunProjection`.
- **New run events**: `todo.created`, `todo.updated`, `todo.deleted` —
mapped through Fabro's typed event pipeline and replayed by
`RunProjectionReducer`.
- **`TodoRuntime`** (`fabro-agent`): thread-safe in-memory projection
shared across tool closures within a profile instance; each mutation
emits the corresponding agent event.
- **`update_plan`** registered only in `OpenAiProfile`: reconciles
incoming steps by exact `step` text (sha256-derived ID), emitting
create/update/delete events to match the submitted plan.
- **`TaskCreate` / `TaskUpdate` / `TaskList`** registered only in
`AnthropicProfile`: numeric task IDs per list, metadata merge with
`null`-key deletion, `status: "deleted"` routes to `todo.deleted`.
- **Session identity threading**: `ToolContext` gains `session_id`,
`root_session_id`, `tool_call_id`, and `agent_event_emitter`;
`execute_tool_calls` threads these through to `execute_one_tool`;
`Session` tracks `root_session_id` and `spawn_agent` inherits it for
subagents.
- **Scoping**: OpenAI todos scope to `openai_plan:<session_id>`
(per-session); Anthropic todos scope to
`anthropic_tasks:<root_session_id>` (shared across subagents).
- **Web invalidation**: `todo.*` events invalidate `getRunState` and the
run events list; tested in `run-events.test.tsx`.
```mermaid
graph TB
subgraph OpenAI
UP[update_plan] -->|diff by step text| TR[TodoRuntime]
end
subgraph Anthropic
TC[TaskCreate] --> TR
TU[TaskUpdate] --> TR
TL[TaskList] -->|read-only snapshot| TR
end
TR -->|emit todo.created/updated/deleted| SE[SessionBoundEmitter]
SE --> EV[AgentEvent stream]
EV --> RP[RunProjection\ntodos_by_list]
```
### Key design decisions
- **Step identity by text, not position** (`update_plan`): a
sha256-derived ID from `list_id + step` means reordering without
renaming emits an update rather than a delete+create. Duplicate step
strings are rejected with a model-visible error because text is the
identity.
- **No plan-replace event**: the engine emits only individual mutation
events; bulk replacement is expressed as a set of create/update/delete
events produced by diffing the incoming plan against the projection
snapshot.
- **`TodoRuntime` per profile instance**: tools inside a single profile
share one runtime. OpenAI subagents each have their own `session_id` so
their plans are isolated; Anthropic subagents inherit `root_session_id`
so tasks are shared — matching upstream Codex/Claude behavior.
- **`AgentEventEmitter` trait on `ToolContext`**: a narrow interface
that lets tools publish typed events without taking a dependency on the
full `Emitter`. `SessionBoundEmitter` wraps `Emitter` and stamps
`session_id` + `tool_call_id` on each event.
### Fabro Details
<details>
<summary>Ran 9 stages in 93m 23s for $56.46</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 20s | – | 0 |
| implement | 41m 30s | $38.21 | 0 |
| simplify_opus | 29m 2s | $11.78 | 0 |
| simplify_gpt | 14m 24s | $6.46 | 0 |
| verify | 3m 2s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **93m 23s** | **$56.46** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
1930791802
|
feat(agent): gate tools by effective access policy
Expose only policy-allowed tool schemas to the model and block hidden tool calls before executor lookup. Give Ask Fabro a run-scoped read-only policy and prompt so it only advertises tools it can actually use. |
||
|
|
5d188dbe18
|
feat(agent): make OpenAI apply_patch Codex-compatible (#350)
## Summary OpenAI-profile agents now receive `apply_patch` as a Codex-compatible freeform custom tool instead of a JSON function with a `patch` field. The model sends raw patch text validated by the vendored Lark grammar, and Fabro round-trips OpenAI custom tool calls/results through the Responses API. ## What Changed - Added `fabro-llm` support for function and custom tool definitions while preserving existing JSON function behavior for normal tools. - Translated OpenAI custom tool calls, custom tool outputs, and streaming `response.custom_tool_call_input.delta` events into Fabro tool calls with raw arguments. - Ported the Codex apply-patch grammar and adapted Codex-style patch parsing/application semantics to Fabro's `Sandbox` trait, including strict envelopes, fuzzy context matching, move/delete/add/update behavior, trailing-newline normalization, and Codex-style summaries/errors. - Updated OpenAI agent prompt/tool registration so `apply_patch` is freeform, and skipped JSON-schema validation/repair for custom tool calls only. - Updated file tracking to read Codex-style `A`/`M` result lines from successful patch output. ## Verification - `cargo nextest run -p fabro-llm -p fabro-agent` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --no-deps -p fabro-llm -p fabro-agent --all-targets -- -D warnings` - `git diff --check` Note: the full non-`--no-deps` clippy command still surfaces an unrelated existing `fabro-sandbox` `large_enum_variant` warning in `lib/crates/fabro-sandbox/src/sandbox_spec.rs`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
86b1fbef7f
|
feat(api): bind sessions to runs (#336)
## Summary
Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.
## Changes
- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.
## Validation
- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && cd
../../../apps/fabro-web && bun run typecheck`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
|
||
|
|
bf96baa9f0
|
feat(server): add run pairing API (#312)
## Summary
Adds the server-side run pairing surface for joining one active API-mode
agent session, sending pair messages, reading a compact transcript, and
ending pairing explicitly before workflow release continues.
This PR wires the feature end to end:
- adds OpenAPI paths and shared `fabro-types` DTOs for pair lifecycle,
messages, transcript entries, and run event details
- adds typed `RunEvent` variants for pair lifecycle and pair-scoped
user/system messages
- extends the workflow steering hub and agent session drain path with
typed pair control items, single-target validation, pair parking, and
pair end/resume behavior
- extends worker JSONL control and server transports for pair
start/message/end while preserving existing
steer/interrupt/answer/cancel behavior
- adds Axum handlers for `/api/v1/runs/{id}/pair`, pair messages, pair
transcript, and `/api/v1/runs/{id}/events/{seq}`
- adds `fabro-client` helpers for the new endpoints
## Notes
The subprocess path does not add a bidirectional worker ack channel in
this PR. Instead, the HTTP pair handlers only return lifecycle/message
success after the corresponding durable runtime event is observed, so
mpsc enqueue success alone is not treated as API success.
The plan checklist in
`docs/superpowers/plans/2026-05-18-server-side-run-pairing-api-events.md`
is included with that distinction left visible.
## Verification
- `cargo build -p fabro-api`
- `cargo check -p fabro-api -p fabro-client -p fabro-agent -p
fabro-workflow -p fabro-interview -p fabro-server`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-api pair
run_event_round_trips_pair_lifecycle_events
run_event_round_trips_agent_pair_messages`
- `cargo nextest run -p fabro-workflow pair`
- `cargo nextest run -p fabro-interview pair`
- `cargo nextest run -p fabro-server pair
subprocess_answer_transport_pair_commands_enqueue_control_messages steer
interrupt`
|
||
|
|
5bfd115339
|
[codex] Add ACP steering support (#329)
## Summary - Adds a backend-neutral live control abstraction so steering, interrupt, and interrupt+steer no longer depend on API-only session handles. - Reworks ACP sessions into a live protocol loop that uses ACP `session/prompt` for follow-up steers and ACP `session/cancel` for interrupts without restarting the process. - Registers ACP sessions as steerable, removes the stale non-steerable server/UI/API path, preserves ACP projection metadata, and keeps unsupported backends out of the steerability gate. ## Validation - `LC_ALL=C cargo nextest run --workspace --no-fail-fast` (5,833 passed, 178 skipped) - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1 ASDF_BUN_VERSION=1.3.11 bun test` (396 passed) - `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1 ASDF_BUN_VERSION=1.3.11 bun run typecheck` - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && LC_ALL=C ASDF_BUN_VERSION=1.3.11 bun run typecheck` --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
302e2445b4
|
refactor(model): move provider facts into catalog (#298)
## Summary Moves provider-specific facts out of `AdapterKind` metadata and into provider catalog data, leaving adapters responsible for runtime protocol behavior. This makes providers that share an adapter mostly TOML-driven while still surfacing adapter construction failures during readiness checks. ## What Changed - Provider TOML now owns auth mode, API-key/header policy, billing policy, agent profile, base URLs/env overrides, extra headers, and probe markers. - Auth, install, config, diagnostics, and server flows resolve provider credentials from catalog auth config, including API-key, header-only, and no-auth providers. - LLM client registration now reports adapter construction failures, validates final adapter requests before HTTP dispatch, and preserves custom primary auth headers. - Billing and docs now use provider-owned billing policy instead of adapter metadata, and the old adapter metadata surface is removed. ## Reviewer Notes OpenAI-compatible `base_url` validation now happens during adapter/client registration rather than catalog build. That keeps catalog parsing adapter-agnostic while still letting readiness and model listing reflect providers that cannot register. ## Verification - `cargo check -p fabro-model -p fabro-auth -p fabro-llm -p fabro-server -p fabro-cli` - `cargo nextest run -p fabro-llm -- adapter_registry` - `cargo nextest run -p fabro-model -- catalog` - `cargo nextest run -p fabro-auth -- api_key` - `cargo nextest run -p fabro-server -- install` - `cargo +nightly-2026-04-14 fmt --check --all` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
93452001a1
|
feat(api): require typed PermissionLevel on session create (#300)
## Summary - `POST /api/v1/sessions` now requires `permissions` as a typed enum (`read-only` | `read-write` | `full`) instead of accepting an optional plain string. - Removes the silent fallback at `sessions.rs:906-911` where unknown values (e.g. `"readonly"`) were coerced to `read-write` — a real security footgun: a client trying to lock the agent down would get write access instead. - Invalid or missing values are now rejected by axum's `Json` extractor with `422 Unprocessable Entity`. ## Approach - New `PermissionLevel` OpenAPI schema (`type: string, enum: [...]`). - Moves `PermissionLevel` from `fabro_agent::cli` to `fabro_types::session` so `fabro-api` can `with_replacement` it without a circular dep. `fabro_agent::cli::PermissionLevel` remains as a `pub use` re-export so existing call sites keep working. - `SessionRecord.permissions` becomes required and non-nullable for coherence — every created session has a concrete level. - `build_tool_approval` in the server takes `PermissionLevel` directly; the string-match fallback is deleted. - CLI's `session_permissions` returns a concrete `PermissionLevel` (defaults to `read-write` when neither flag nor settings provide one) and is sent explicitly on every request. ## Scope notes Confirmed out of scope and not addressed here: - Mid-session model/permission switching - Interactive tool approval / HITL ## Breaking change The `permissions` field is now required on `CreateSessionRequest` and non-nullable on `SessionRecord`. Existing on-disk session records persisted with `"permissions": null` will fail to deserialize. Acceptable per project policy (no migration); local dev users may need to clear `~/.fabro/storage/sessions/` once. ## Test plan - [x] `cargo build --workspace` - [x] `cargo nextest run -p fabro-api` — 125/125 (includes new `permission_level_round_trip` parity tests) - [x] `cargo nextest run -p fabro-server` — 554/554 (includes new 422 tests for missing + invalid permissions) - [x] `cargo nextest run -p fabro-cli` — 892/892 - [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - [x] `cargo +nightly-2026-04-14 fmt --check --all` - [x] `bun run generate` on `fabro-api-client` — emits typed `PermissionLevel` union and required field on `CreateSessionRequest` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b9497517a5
|
feat(llm): support agent profile overrides (#291)
## Summary Adds catalog-level `agent_profile` overrides so custom providers and individual models can choose Anthropic, OpenAI, or Gemini agent behavior independently from their adapter default. The effective precedence is model override, then provider override, then adapter metadata. ## What Changed - Added typed provider/model `agent_profile` settings in `fabro-config` and `fabro-model`, with serde/strum support for `anthropic`, `openai`, and `gemini`. - Centralized effective profile resolution in the catalog, including provider alias canonicalization and a guard against unrelated model overrides leaking across providers. - Updated run startup, API sessions, CLI/ACP backends, prompt project-memory discovery, and standalone agent startup to use the resolved catalog profile. - Documented provider-level and model-level `agent_profile` configuration in the public model and user configuration docs. No OpenAPI or model-list response shape changes are included. ## Validation - `cargo nextest run -p fabro-model -p fabro-config -p fabro-workflow -p fabro-agent` passed: 1826 passed, 125 skipped. - `cargo +nightly-2026-04-14 fmt --check --all` passed. - `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-config -p fabro-workflow -p fabro-agent --all-targets -- -D warnings` passed. - `git diff --check` passed. - `cargo nextest list -p fabro-dev` confirmed there is no docs-options reference test target to run. ## Post-Deploy Monitoring & Validation Watch workflow and agent-session logs for provider/model resolution errors, unexpected project-memory file selection, or CLI/ACP launch command mismatches on custom catalog providers. Healthy signal: custom provider/model runs start normally and use the intended profile-specific behavior. Failure trigger: repeated `Provider ... is not configured` errors, missing expected project memory, or profile-specific agent startup failures after configuring `agent_profile`. Mitigation is to remove the override from config or revert this PR. Validation window: first deploy cycle after merge; owner: release/on-call engineer. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
56c7f627c4
|
feat(session): add server-backed agent sessions (#278)
## Summary
Adds the first server-backed Fabro agent session slice: persistent
session records, durable turn/event storage, HTTP session APIs, SSE turn
streaming, generated clients, and a new `fabro session -p <prompt>` CLI
path.
## What Changed
- Adds shared session IDs, records, statuses, event envelopes, and
message DTOs in `fabro-types`, with OpenAPI replacements in `fabro-api`.
- Renames the agent runtime transcript item from `Turn` to `Message` and
adds conversion between runtime history and persisted `SessionMessage`
records.
- Introduces a file-backed `SessionStore` for session metadata, turns,
full transcripts, and append-only events under local storage.
- Wires server session routes for create/list/read/update/delete, turn
submission, event replay, interrupt requests, and session-scoped tools.
- Implements streamed turn execution with durable events persisted
before SSE broadcast, active-turn conflict handling, local same-machine
`working_dir` validation, and noninteractive permission denials.
- Adds `fabro-client` helpers and the `fabro session -p` command, plus
regenerated TypeScript API client files.
## Notes
V1 intentionally keeps session execution local to same-machine server
targets. Remote clone-backed session sandboxes, interactive REPL/TUI
behavior, warm session pooling, and real tool discovery for
`/sessions/{id}/tools` remain follow-up work.
## Verification
- `cargo build --workspace`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-store session_store_contract_tests --lib`
- `cargo test -p fabro-agent
history::tests::session_message_roundtrip_preserves_runtime_history
--lib`
- `cargo test -p fabro-server 'session_' --lib`
- `cargo test -p fabro-server --features test-support --test it
openapi_conformance -- --nocapture`
- `cargo test -p fabro-cli --test it cmd::session:: -- --nocapture`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
|
||
|
|
87950295bd
|
refactor(llm): split provider identity from adapters (#280)
## Summary This PR separates provider identity from adapter behavior across the LLM stack. Provider IDs now represent catalog rows and provider metadata, while adapter/profile routing owns protocol behavior for Anthropic, OpenAI, Gemini, and OpenAI-compatible providers. ## Changes - Replace the shared `fabro_model::Provider` enum with open-ended `ProviderId` catalog identity and typed `AdapterKind` metadata. - Route auth, CLI, ACP, workflow, memory selection, profile construction, and LLM client registration through catalog provider rows instead of provider-ID fallbacks. - Move API-key URL/header/env metadata into provider catalog/auth flows and require configured provider rows for credential-backed clients. - Simplify billing to `algorithm`-tagged OpenAI, Anthropic, and Gemini shapes; OpenAI-compatible adapters bill through the OpenAI algorithm. - Remove greenfield compatibility paths for old provider aliases, legacy provider-tagged billing JSON, and the `openai_compatible` pseudo-provider env fallback. - Update fixtures and tests to exercise catalog-driven Kimi/Zai/Minimax/Inception/custom OpenAI-compatible routing. ## Validation - `cargo test --no-run -p fabro-model -p fabro-auth -p fabro-agent -p fabro-workflow -p fabro-server -p fabro-llm -p fabro-api -p fabro-cli -p fabro-store -p fabro-static` - `cargo nextest run -p fabro-model -p fabro-auth -p fabro-agent -p fabro-workflow -p fabro-server --no-fail-fast` - `cargo nextest run -p fabro-llm -p fabro-api -p fabro-cli -p fabro-store -p fabro-static --no-fail-fast` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
b0847180b6
|
Fix custom provider resolution in exec and model list (#276)
## Summary - allow `fabro exec` to use configured custom provider IDs from the resolved LLM catalog - route direct exec sessions through the same catalog-aware provider/profile resolution used by workflow runs - stop `fabro-client::list_models` from rejecting non-built-in provider filters client-side - update CLI snapshots and add regression tests for custom-provider exec and model listing ## Repro With a configured provider like: ```toml [llm.providers.bedrock] adapter = "openai_compatible" base_url = "https://.../v1" [cli.exec.model] provider = "bedrock" name = "bedrock-claude-sonnet-4-6" ``` these paths diverged: - `fabro run ... --model bedrock-claude-sonnet-4-6` worked - `fabro model list` showed `bedrock-*` models - `fabro exec "..."` failed with `unknown provider: bedrock` - `fabro model test --provider bedrock` failed with the same client-side error ## Root cause There were two separate built-in-only assumptions: 1. `fabro-agent` direct CLI paths parsed provider strings into the built-in `Provider` enum and built a default catalog, so configured provider IDs from `settings.toml` were invisible. 2. `fabro-client::list_models()` parsed the optional provider filter into the same built-in enum before calling the server, so custom provider filters never reached the API. ## Validation - `cargo check -p fabro-cli -p fabro-agent -p fabro-client` - `cargo test -p fabro-agent resolve_provider_accepts_custom_catalog_provider -- --nocapture` - `cargo test -p fabro-client list_models_allows_custom_provider_filters -- --nocapture` - `cargo test -p fabro-cli exec_accepts_configured_custom_provider_from_settings -- --nocapture` - `cargo test -p fabro-cli list_invalid_provider_errors -- --nocapture` - `cargo test -p fabro-cli help -- --nocapture` --------- Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
1cfe9419f5
|
refactor(llm): simplify catalog cleanup paths (#261)
## Summary This is a cleanup pass over the configurable LLM provider/catalog work from issue #210. It addresses reuse, quality, and efficiency findings from the phase 0-9 review without changing the public provider settings contract. Notable changes: - skip LLM client initialization during run preflight when the workflow has no LLM nodes - make preflight provider checks use alias-aware `Client::has_provider` - resolve `run.model.fallbacks` through the catalog instead of the old empty-key bridge - paginate `/models` before cloning returned rows - share label parsing, provider default-adapter lookup, enum expected-value formatting, and billing token formatting helpers - use catalog provider display names for OpenAI-compatible agent profiles - align process-env configured-provider discovery with `EnvCredentialSource` ## Verification - `cargo check -p fabro-config -p fabro-model -p fabro-auth -p fabro-agent -p fabro-workflow -p fabro-server -p fabro-cli` - `cargo nextest run -p fabro-config parse_labels_keeps_key_value_pairs` - `cargo nextest run -p fabro-workflow resolve_fallback_chain_resolves` - `cargo nextest run -p fabro-auth configured_providers` - `cargo nextest run -p fabro-server list_models` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-auth -p fabro-workflow -p fabro-server -p fabro-agent -p fabro-config -p fabro-cli --all-targets -- -D warnings` - `cd apps/fabro-web && bun test app/routes/run-billing.test.tsx` - `cd apps/fabro-web && bun run typecheck` - `git diff --check` --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1b6189ee32
|
docs(llm): finish configurable provider cleanup (#260)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary Finish phase 9 of the configurable LLM provider/model work by aligning public docs, release notes, and guardrails with the implementation already landed in phases 0-8. - documents settings-driven providers/models, OpenAI-compatible gateway examples, typed `extra_headers`, model `api_id`, controls, and per-speed costs - adds the 2026-05-13 changelog entry and provider string migration note - updates the internal phase plan ledger to reflect current implementation status - adds a workspace policy test blocking direct production `Catalog::builtin()` usage outside catalog owner/test code - clarifies `Provider` as a built-in compatibility enum while open-ended identity is `ProviderId` ## Verification - `cargo nextest run -p fabro-dev --features dev --test it policy` - `cargo dev docs check` - `cargo nextest run -p fabro-model -p fabro-config -p fabro-auth -p fabro-llm` - `cargo build --workspace` - `cargo nextest run --workspace` (5717 passed, 182 skipped, nextest reported 1 leaky test) - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `git diff --check` |
||
|
|
a81eb09e78
|
feat(llm): add catalog controls and speed billing (#249)
## Summary This PR advances the catalog-driven LLM work from fabro-sh/fabro#210 by making the resolved model catalog the source of truth for provider registration, request control validation, and billing identity. Runs now preserve canonical provider/model/speed identity through pricing and API responses instead of collapsing billing around provider API aliases or model IDs alone. ## What Changed - Register LLM provider adapters from the resolved catalog, including custom OpenAI-compatible providers and their credential resolution paths. - Validate effective model request controls, including run-level defaults and node overrides, before dispatching LLM requests. - Add catalog-aware billing lookup that prices canonical `ModelRef` values, uses base model costs for standard speed, applies per-speed cost overrides, and returns an unknown estimate instead of silently billing zero for unsupported combinations. - Move Anthropic Opus fast-mode pricing into the built-in catalog for `claude-opus-4-6` and `claude-opus-4-7`. - Thread the injected catalog and effective speed controls through workflow billing, including API-mode and CLI-mode handlers. - Update billing APIs, server aggregation, generated clients, and the web billing view to expose provider/model/speed billing identity and keep standard and fast usage in separate rows. ## Notes for Review Billing lookup intentionally uses canonical catalog model IDs. Provider `api_id` substitution remains limited to provider request construction, so aliases can be used on the wire without changing billing identity. Event conversion paths that do not have catalog access now preserve token counts with a null dollar estimate rather than falling back to the bootstrap catalog. ## Verification - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `ulimit -n 4096 && cargo nextest run -p fabro-model -p fabro-workflow -p fabro-server -p fabro-api -p fabro-cli --no-fail-fast` - `ulimit -n 4096 && cargo nextest run --workspace --no-fail-fast` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
34d83db801
|
feat(model): support open provider catalog data (#245)
## Summary This PR moves Fabro’s provider/model catalog toward settings-driven provider identity by replacing the closed provider schema at the API/auth/model boundary with `ProviderId`, then loading built-in provider and model metadata from embedded per-provider TOML files. The immediate result is that built-ins now use the same settings-shaped catalog data that custom providers will use later, while request-serving paths still keep the existing bootstrap/default catalog behavior until the resolved-catalog plumbing lands. ## Changes - Replaces API-facing provider enum usage with string-backed `ProviderId`, including OpenAPI/progenitor replacements and regenerated TypeScript client models. - Routes model, auth, billing, CLI, server, and workflow call sites through provider IDs where they cross product identity boundaries. - Builds `Catalog` from settings-shaped provider/model data with validation for adapter keys, OpenAI-compatible `base_url`, duplicate aliases, provider defaults, disabled entries, model controls, and per-speed cost rows. - Replaces `catalog.json` with embedded provider TOML files under `lib/crates/fabro-model/src/catalog/providers/`. - Adds an explicit `fabro_model::bootstrap_catalog` hatch for setup/install paths and extends the dev policy test to keep bootstrap access contained. - Preserves public training and knowledge-cutoff labels in LLM model settings while still accepting bare TOML dates. ## Verification - `cargo nextest run -p fabro-model -p fabro-config -p fabro-api` — 416 passed - `cargo nextest run -p fabro-dev --features dev bootstrap_catalog_references_stay_in_allowlist` — 1 passed - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cargo build --workspace` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
234bd5663e
|
Add ACP backend support (#237)
## Summary Implemented ACP support as a first-class Fabro backend alongside `api` and `cli`. This adds a new `fabro-acp` crate using the official ACP Rust crates, routes `backend=\"acp\"` for agent and prompt nodes, adds sandbox stdio support for local/Docker/test-support paths, emits ACP workflow events/projections, updates server steerability handling, validation, documentation, and black-box CLI coverage. ## Test Plan Passed strict non-live verification: - `ulimit -n 4096 && cargo nextest run -p fabro-workflow --run-ignored all --no-fail-fast` — 1162 passed, 0 skipped. - `ulimit -n 4096 && cargo nextest run -p fabro-acp -p fabro-sandbox -p fabro-workflow -p fabro-validate -p fabro-store -p fabro-server -p fabro-cli --run-ignored all --no-fail-fast -E 'not test(daytona_streaming_live_smoke)'` — 3125 passed. - `cargo build --workspace` — passed. - `ulimit -n 4096 && cargo nextest run --workspace --run-ignored all --no-fail-fast -E 'not test(daytona_streaming_live_smoke)'` — 5666 passed. - `cargo +nightly-2026-04-14 fmt --check --all` — passed. - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — passed. Live-environment tests skipped/excluded under explicit user override: - `daytona_streaming_live_smoke` was excluded from final nextest runs because it requires live Daytona infrastructure and `DAYTONA_API_KEY`. - Confirmed with `env -u DAYTONA_API_KEY cargo test -p fabro-sandbox --features daytona --test daytona_streaming_live daytona_streaming_live::daytona_streaming_live_smoke -- --ignored --exact --nocapture`: failed fast with `DAYTONA_API_KEY must be set to run this live smoke test`. |
||
|
|
a19f6dd03a
|
feat(cli): add Fabro MCP server (#236)
## Summary Adds a stdio-based Fabro MCP server so MCP clients can manage Fabro workflow runs through the authenticated `fabro` CLI, without a separate MCP auth flow. ## What Changed - Adds `fabro mcp start`, `fabro mcp config`, and `fabro mcp init <agent>` for launching and configuring the MCP server. - Introduces a new `fabro-mcp-server` crate with run-management tools: - `fabro_run_create` - `fabro_run_search` - `fabro_run_interact` - `fabro_run_gather` - `fabro_run_events` - Reuses the CLI's authenticated server connection behavior, including OAuth refresh, dev-token/local-server handling, explicit server targets, proxy behavior, and stdio env/cwd isolation. - Moves shared run-manifest construction into `fabro-manifest` so CLI runs and MCP-created runs use the same override semantics. - Extends MCP client stdio support with configured cwd and exact environment handling for reliable spawned-server tests. --------- Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro <noreply@fabro.sh> |
||
|
|
67decad7b5
|
feat(billing): project live stage token usage
Store live per-stage token counts on StageProjection, carry typed billing model identity through agent.message events, and derive billing rollups from the projection so in-flight stages can report usage before terminal events arrive. |
||
|
|
befb2e00ec
|
feat(runs): merge command output streams
Route command stderr into stdout at execution time and expose a single output log across events, projections, API clients, and the web UI. Keep replay compatibility for older command.completed events that still contain split stdout/stderr fields. |
||
|
|
5a9f568e46
|
refactor: simplify token plumbing and parallelize read_many_files
- Replace mint_github_token's hand-rolled Pat/Installation/App match with GitHubCredentials::resolve_bearer_token, removing a near-duplicate of the same logic already in run_metadata::mint_token. - Parallelize read_many_files via futures::future::join_all so the tool actually reads concurrently — previously serial despite the name. - Replace .expect() on the post-refresh GitHubTokenSource cache with a proper anyhow error so a refresh edge case can't panic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f1c247bc0f
|
fix(github): refresh installation tokens during workflows
Split PATs from installation access tokens so static configuration cannot accidentally store expiring ghs_* credentials. Workflow command and API agent stages now resolve GITHUB_TOKEN lazily from a refreshable source, while CLI agent stages surface their launch-time refresh limitation. |