Commit graph

314 commits

Author SHA1 Message Date
Bryan Helmkamp
c08e5c5490
feat(agent): add a Kimi agent profile for Moonshot and gateway routes
Kimi models ran on the OpenAI profile, which exists to look like Codex. Give
them their own profile derived from Kimi Code's system prompt.

Routing is per model, not per provider, because Kimi models are served both
directly by Moonshot and through gateways. `kimi` sets agent_profile at the
provider level; the Kimi model rows on `openrouter` set it individually, so a
gateway route behaves like the direct one while other OpenRouter models keep
the provider's OpenAI profile.

The profile targets a measured failure. Across two observed K3 implementation
stages, 32 of 35 tool failures were the same thing: writes to files the model
had not read, rejected by the workspace read-before-write guard, or
`old_string` values reconstructed from memory rather than taken from a read.
Kimi Code drills this rule in its own tool descriptions, so the profile does
too -- `edit_file` and `write_file` carry Kimi-specific descriptions naming the
guard and the failure text the model will see, alongside a "Reading Before
Writing" section in the system prompt. Profiles own their tool registries, so
this re-describes the tools for Kimi only; every other profile is untouched and
the executors and JSON schemas are shared unchanged.

Tool names stay fabro's existing snake_case. Whether Kimi Code's PascalCase
vocabulary measurably helps is untested, and renaming would also mean updating
the name-keyed categories in tool_permissions.rs, where an unknown tool falls
back to Shell. That is a separate change to make on evidence.

The prompt is a subtractive port: capabilities fabro does not have -- plan
mode, background tasks, cron, subagent swarms, the cwd tree listing -- are
dropped rather than promised. The shell timeout default matches Kimi Code's 60s
and memory discovery reads AGENTS.md, which is the only instruction file Kimi
Code looks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 19:50:58 -04:00
Bryan Helmkamp
63c952e380
Merge pull request #628 from fabro-sh/refactor/agent-md-prompts
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
refactor(agent): render profile system prompts with minijinja templates
2026-07-24 19:31:48 -04:00
Bryan Helmkamp
f94955ede5
fix(agent): budget compaction summaries for reasoning models
Compaction summarizes the conversation with the session's own model, but
hard-coded `max_tokens: Some(4096)` and sent no `reasoning_effort`. On a
reasoning model that ceiling covers thinking *and* visible output, so a
long conversation can exhaust it on reasoning alone and return a
successful response with empty content — silently replacing the compacted
history with an empty summary.

The Anthropic codec's existing clamp does not cover this path: it only
runs when the request carries a `reasoning_effort` and the model has no
native effort parameter. Compaction sends `reasoning_effort: None`, so
encoding falls through to the branch that injects `{"type": "adaptive"}`
for `levels` models with no clamp at all, and the openai_compatible and
openai_responses codecs pass `max_tokens` straight through.

Resolve the budget from the catalog instead. Models whose endpoint
reasons without being asked (`always_adaptive` natively, `levels` via
default adaptive thinking or the provider's default effort) get 16K of
reasoning headroom above the 4096-token summary allowance, capped at the
model's own `max_output`. Models with no reasoning-effort feature never
reason on this path and keep the existing 4096.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 19:11:25 -04:00
Bryan Helmkamp
fd6f14f107
refactor(agent): simplify prompt template rendering 2026-07-24 19:10:48 -04:00
Bryan Helmkamp
b17b8aeaed
fix(agent): refuse to truncate history on a degenerate compaction summary
When the summarization LLM call returned an empty completion, compaction
truncated the conversation anyway. `history.compact_from` discarded the
summarized turns irreversibly, `CompactionCompleted` was emitted as if
nothing had gone wrong, and the replacement system turn contained only the
handoff preamble: "A different assistant began this task and produced the
following summary" followed by nothing.

The agent then continued with zero context while having been explicitly
told a handoff summary existed. It presents to a user as the agent
suddenly forgetting everything, and the only trace was a `debug!` line
that is off by default, so there was nothing in production logs to
correlate against.

This is provider-independent. Any completion that comes back empty
triggers it: a truncated stream, a reasoning model that spends its whole
token budget on reasoning, or a rate-limit edge.

Validate the summary before mutating history. A summary that is empty,
whitespace-only, or shorter than 32 bytes after trimming is refused: the
history is left fully intact and an error is returned instead. The
threshold is deliberately far below any genuine summary — 32 bytes is
shorter than a single source file path — because this guards against
degenerate responses, not summary quality, and a false refusal would let
the context keep growing. Structure is not validated, since a model may
legitimately vary the requested section format.

Returning `Err` is sufficient to surface the failure. `compact_if_needed`
already converts it into an `AgentEvent::Error`, which lands in the run
event stream and logs at ERROR via `AgentEvent::trace`, and the session
continues rather than dying — behavior already covered by
`compaction_failure_is_non_fatal`.

The canned summary in `compaction_includes_structured_prompt_and_file_tracking`
was 26 bytes, which the new guard rejects. That test verifies the
summarization request prompt and file tracking, not minimum summary
length, so its fixture is now a realistic summary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 19:10:13 -04:00
Bryan Helmkamp
1d685cbea4
refactor(agent): render profile prompts with minijinja via fabro-template
The previous commit introduced render_prompt and splice_optional_section, a
second templating mechanism in a workspace that already standardizes on
MiniJinja behind fabro-template. Drop both and render the profile prompts the
same way fabro-workflow and fabro-manifest render theirs.

Expressing the conditionals as {% if %} lets every profile collapse to a single
template, since the optional blocks no longer need to be separate files spliced
in from Rust:

  before: 6 files + 2 splice helpers, prompt prose split across .md and .rs
  after:  3 files, one per profile, all prose in the template

Rust now passes only facts -- provider name, which file-edit tool is active,
and whether web search and subagents are available. Values land under `vars`,
so templates read {{ vars.env_block }}. Booleans are passed as "true"/"false"
and compared explicitly via the bool_var helper, because the shared
TemplateContext types vars as strings and a bare {% if %} on the string
"false" would be truthy.

Also converts fabro-server's Ask Fabro prompt, which is assembled at runtime.
Its tool guidance now arrives as a template variable instead of being
interpolated into the template text. That guidance carries tool names and
descriptions that can originate from MCP servers, and MiniJinja does not
re-render substituted values, so a tool description containing {{ ... }} stays
inert rather than being evaluated.

Output is unchanged. Verified by diffing all ten prompt variants against the
same unmodified origin/main worktree used for the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:23:47 -04:00
Bryan Helmkamp
2c13e2da5e
refactor(agent): load profile system prompts from .md files
The three profile prompts lived as multi-line Rust string literals with
backslash continuations, which made them awkward to read, diff, and review.
Move them to profiles/prompts/*.md loaded via include_str!, matching the
existing convention in fabro-workflow's pipeline prompts and the server's
playground prompt.

Two helpers back the templates, both following the {env_block} convention
already used by assemble_system_prompt rather than adding a template engine:

- render_prompt substitutes {name} placeholders and leaves the rest intact,
  for values spliced inline (provider name, the web-search bullet)
- splice_optional_section handles whole blocks that come and go, dropping the
  blank line ahead of the placeholder when the block is empty

Gemini already carried a {web_search_section} placeholder in its literal, so
that one maps onto render_prompt unchanged. Anthropic's per-section functions
collapse into a single template plus a subagent fragment. OpenAI keeps its two
one-line file-edit failure hints inline, since they are bound to the tool name
and would not read well as standalone files; the multi-line usage blocks they
pair with become fragments.

Output is unchanged. Verified by capturing all ten prompt variants -- Anthropic
across subagent x web-search, OpenAI across apply_patch/edit_file x web-search,
Gemini across web-search -- from an unmodified worktree at origin/main, then
diffing them byte for byte against this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:55:08 -04:00
Bryan Helmkamp
7044dd57fa
Merge pull request #627 from fabro-sh/fix/openai-compatible-stream-usage
fix(llm): request streaming usage on openai_compatible providers
2026-07-24 17:53:45 -04:00
Bryan Helmkamp
ae3b6702e2
Merge pull request #626 from fabro-sh/feat/passive-reasoning-capture
feat(reasoning): passive reasoning capture in agent.message
2026-07-24 17:49:46 -04:00
Bryan Helmkamp
53a92f75f4
test(llm): model streamed usage in OpenAI twin 2026-07-24 17:47:05 -04:00
Release Repro
512ab50f9c
fix(reasoning): align stream and client invariants 2026-07-24 17:42:44 -04:00
Bryan Helmkamp
2c6fd5d798
Merge pull request #625 from fabro-sh/fix/brave-search-profile-config
Fix Brave Search tool secret propagation
2026-07-24 17:36:48 -04:00
Bryan Helmkamp
4ce57f8aae
refactor: simplify profile builder and drop dead tool plumbing
Follow-up cleanup on the profile-builder refactor.

AgentProfileBuilder::build now borrows instead of consuming, removing the
builder.clone().build() dance at all seven call sites. Deletes
with_command_timeouts, which had no caller but its own test, and the
with_summarizer constructors on all three profiles, whose only remaining
caller was each profile's own new().

Replaces the fifth copy of the profile-kind match (guardrails.rs) with the
builder, and swaps the parity matrix's hand-maintained provider list for
Catalog::effective_agent_profile so a new catalog provider cannot silently
skip the matrix. Collapses web_search_provider_test! into a secrets = arm
on provider_test! and uses EnvVars::BRAVE_SEARCH_API_KEY over a literal.

Drops the Brave key from the Ask Fabro session: AskFabroToolAccessPolicy
denies web_search, and both tools() and the prompt are filtered through
that policy, so the vault read only registered an uncallable tool.

Makes NativeToolOptions::for_profile match exhaustively so a new profile
kind must state its timeout, restores Anthropic's borrowed prompt sections
and Gemini's static prompt (placeholder substitution rather than format!
over 110 lines with doubled braces), and introduces WEB_SEARCH_TOOL_NAME
for the registry lookups that keep tool availability and prompt guidance
in sync.

Updates the product docs, which still described web_search as always
registered and as erroring at call time when unconfigured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:32:33 -04:00
Release Repro
4d3de5f564
fix(reasoning): tighten capture normalization 2026-07-24 17:31:14 -04:00
Bryan Helmkamp
0169725b4e
fix(llm): request streaming usage on openai_compatible providers
Chat Completions only emits the trailing usage chunk when the request sets
`stream_options: {"include_usage": true}`. The openai_compatible codec never
sent it, so providers that follow the spec strictly returned no usage at all
on streamed responses. Every message came back with zero tokens, and the
catalog cost estimate multiplied those zeros into $0.

Kimi is the visible case: a run's kimi-k3 stages report 0 tokens and no
dollars, while an openrouter stage in the same run bills normally because
OpenRouter volunteers usage (and an in-band cost) without being asked.

Send the opt-in whenever we stream. Providers that already volunteer usage
accept the field and are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 16:21:00 -04:00
Bryan Helmkamp
e7740b4acb
feat(reasoning): capture provider reasoning in agent.message
Normalize the readable reasoning providers already return into a
canonical `ReasoningOutput` and carry it through the `agent.message`
run event to storage, SSE, and JSONL.

The shape is derived from the final response's canonical message
content rather than stored a second time, so there is no duplicate
source of truth and retried or replaced streaming buffers never
become durable reasoning. OpenAI-compatible `reasoning_details` are
now preserved verbatim as an opaque content part; only known readable
members are normalized out of them, leaving encrypted entries for a
later provider-aware replay phase.

This phase is passive: no request parameters change, no capability
guessing, and no newly observed provider field is replayed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:21:15 -04:00
Bryan Helmkamp
e15defe4c5
refactor: centralize agent profile tool configuration 2026-07-24 14:06:29 -04:00
Release Repro
0b58d087ee
feat(model): add Claude Opus 5 2026-07-24 13:43:08 -04:00
Bryan Helmkamp
fec5021a29
fix: pass tool secrets to agent profiles 2026-07-24 13:36:45 -04:00
Bryan Helmkamp
71549f0917
Merge remote-tracking branch 'origin/feat/backward-event-pagination' into feat/backward-event-pagination 2026-07-24 10:21:40 -04:00
Bryan Helmkamp
b77116994f
Harden event pagination bounds and scope desc params to the run route
Guard EventScan seeks against sequences past MAX_EVENT_SEQ: a
seven-digit start prefix sorts below six-digit event keys, so an
unvalidated since_seq like 5000000 returned an incorrect slice of
history instead of an empty page. An end bound past MAX_EVENT_SEQ now
delegates to the unbounded scan, which is equivalent because no stored
sequence exceeds it.

Clamp the descending exclusive end to just past the newest stored
event, so an oversized before_seq cursor pages from the newest event
instead of probing empty key space and returning nothing.

Split RunEventListParams out of EventListParams so before_seq and
order are only accepted by /runs/{id}/events; the session, stage,
pair transcript, and demo endpoints go back to ignoring them instead
of accepting order=desc while returning ascending results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:21:25 -04:00
Bryan Helmkamp
c9900b2bfa
Merge branch 'main' into feat/backward-event-pagination 2026-07-24 10:14:57 -04:00
Bryan Helmkamp
eb83539a18
Merge origin/main into feat/stage-execution-identity-on-resume
Resolves conflicts with the shared-checkout parallel rewrite (#607) and the
cached-run/billing dedup (de60eb900):

- handler/parallel.rs: rebuilt on main's shared-checkout version. Branch
  ordinals are still reserved inside the branch task right before
  ParallelBranchStarted (with graph_visit/resumed_from_stage_id), and the
  reserved StageScope is shared with post-await error paths via a OnceLock
  slot instead of main's dispatch-time visit=1 scope, so completion events
  are never emitted under a guessed ordinal.
- billing.rs: keep this branch's run_stage_from_projection (RunStage grew
  graph_visit/resumed_from_stage_id and a typed id), adopt main's
  state.cached_run() and drop the removed run_stage_from_stage_id import.
- run_projection.rs: adopt main's typed parallel_results
  (Option<Vec<ParallelBranchResult>>).
- run_event/misc.rs: union of both sides' imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:07:35 -04:00
Bryan Helmkamp
7e7d7e4457
Recover latest event seq with bounded probes instead of a full scan
On a projection-cache miss, descending pagination recovered the latest
sequence by scanning the run's entire event prefix, making a cold-cache
order=desc request O(total_events). Binary-search the zero-padded
sequence key space with single-entry probes instead, bounding recovery
to O(log MAX_EVENT_SEQ) reads. The probe predicate (smallest stored
sequence at or above a bound) stays monotone across gaps left by
failed appends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:05:01 -04:00
Bryan Helmkamp
ec100fca2b
Merge remote-tracking branch 'origin/main' into feat/backward-event-pagination
Resolved conflicts against main's shared-projection-cache rework:
- projection_cache.rs: kept main's projection_snapshot and dropped this
  branch's last_seq accessor, which it subsumes; latest_event_seq now
  reads the sequence from projection_snapshot.
- run_store.rs: kept main's EventScan cursor and added a seek_before
  constructor so the backward-pagination range scan bounds its end key
  through the same abstraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:57:15 -04:00
Bryan Helmkamp
b886f82622
Clamp backward pagination end bound to the event key-order limit
Event keys zero-pad seq to six digits, so an exclusive end bound past
MAX_EVENT_SEQ formatted as a seven-digit prefix that sorts before real
event keys, producing an inverted scan range. This made the newest page
come back empty once a run reached MAX_EVENT_SEQ, and let a client
supplied before_seq beyond MAX_EVENT_SEQ garble the range. Clamp the
bound and treat anything past MAX_EVENT_SEQ as unbounded; no stored
sequence exceeds it, so the results are equivalent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:50:40 -04:00
Bryan Helmkamp
17cf8d710e
Merge pull request #618 from fabro-sh/fix/shared-run-projection-cache
Reuse shared projections for current run state
2026-07-24 09:47:29 -04:00
Bryan Helmkamp
8394eb2723
Merge pull request #619 from fabro-sh/feat/fireworks-provider
feat(llm): add Fireworks AI as an opt-in provider
2026-07-24 09:46:03 -04:00
Bryan Helmkamp
94e4a38375
Merge remote-tracking branch 'origin/main' into fix/shared-run-projection-cache
# Conflicts:
#	lib/components/fabro-store/src/slate/run_store.rs
2026-07-24 09:41:58 -04:00
Bryan Helmkamp
78fea736e3
fix: harden stage execution identity on resume 2026-07-24 09:37:05 -04:00
Bryan Helmkamp
396f75578a
Merge remote-tracking branch 'origin/main' into feat/backward-event-pagination
Resolved conflict in run_store.rs tests: kept both the new
list_events_before_with_limit tests from this branch and the
append_event_rejects_sequences_beyond_key_order_limit test from main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:35:34 -04:00
Bryan Helmkamp
415e9e5cd8
Merge pull request #617 from fabro-sh/feat/expose-model-controls
Expose model reasoning effort controls
2026-07-24 09:34:55 -04:00
Bryan Helmkamp
3c6a26e8c2
Merge pull request #607 from fabro-sh/feat/shared-checkout-parallel
Shared-checkout parallel execution
2026-07-24 09:33:03 -04:00
Bryan Helmkamp
3beaddc224
Merge remote-tracking branch 'origin/main' into feat/expose-model-controls
# Conflicts:
#	lib/foundation/fabro-api/src/lib.rs
2026-07-24 09:28:01 -04:00
Bryan Helmkamp
7e89cb2bb3
Merge pull request #611 from fabro-sh/fix/validate-model-reasoning-effort
Return 400 for unsupported reasoning effort
2026-07-24 09:27:44 -04:00
Bryan Helmkamp
ac62585eb7
fix parallel result artifact handling 2026-07-24 09:26:22 -04:00
Bryan Helmkamp
de60eb900f
refactor: deduplicate cached-run access and event scans
- Add AppState::cached_run with the standard 500/404 mapping and use it
  everywhere handlers read the shared run-projection cache. This also
  normalizes two inconsistencies: graph-source cache errors now map to
  500 (was 502), and a missing projection in PR create/unlink now maps
  to the canonical 404 (was a bespoke 500).
- Extract an EventScan cursor shared by the four run-event scan loops,
  delegate list_events_from to the paginated variant, and stop the
  stage-event scan once its page is full instead of walking the rest of
  the log.
- Hold Arc<RunProjection> in the local projection cache so opening a run
  no longer deep-copies the projection (copy-on-write via Arc::make_mut),
  and drop the now-unreachable shared-cache branch in last_event_seq.
- Trim hot-path clones: run_files serves the projection Arc directly,
  run-state serializes by reference, artifacts only checks existence, and
  the command-log handler opens a reader only for the CAS-blob branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:25:42 -04:00
Bryan Helmkamp
13876edc44
Merge pull request #608 from fabro-sh/fix/linear-event-history-pagination
Make full event history traversal linear
2026-07-24 09:25:38 -04:00
Release Repro
aa67a231c4
Merge origin/main into fix/validate-model-reasoning-effort
Resolves conflicts with main's typed reasoning_effort field (#609) and
the usage-buckets test (#616). The handler's manual string parse is
superseded by serde-level validation of the typed enum, so it is
removed along with its test; the client-side unsupported-effort
validation and 400 error mapping from this branch are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:24:07 -04:00
Bryan Helmkamp
4c7d13aff0
fix(store): enforce event sequence key limit 2026-07-24 09:18:43 -04:00
Release Repro
914383e1e0
Merge fix/linear-event-history-pagination into backward pagination 2026-07-24 09:12:38 -04:00
Bryan Helmkamp
1b9275f4a8
refactor(llm): align Fireworks provider with catalog conventions
- Rewrite the Fireworks tool round-trip E2E test on the shared
  run_model_test deep-test pattern used by the OpenRouter and Poolside
  opt-in provider tests, instead of a fourth hand-rolled copy of the
  multiply-tool scaffold.
- Drop the "(via Fireworks)" display-name suffix from slugs that have no
  first-party provider (kimi-k2.6, deepseek-v4-*, minimax-m2.7),
  matching the OpenRouter convention; rename "Qwen 3.7 Plus" to
  "Qwen3.7 Plus" to match existing Qwen entries.
- Fix kimi-k2.6 vision flag to false, matching the OpenRouter entry for
  the same slug (the portability test asserts they are the same model).
- Assert small_default_for_provider and per-model family/vision/
  reasoning in the catalog tests, mirroring sibling provider tests.
- Add Troubleshooting and Further reading sections to the Fireworks
  docs page, matching the other opt-in provider pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:08:32 -04:00
Bryan Helmkamp
cd706646c6
feat: treat resumed in-flight nodes as new stage executions
A node cancelled (or lost to a crash) mid-flight and then resumed now
starts a new stage execution with the next StageId ordinal (work@2)
instead of reusing and clearing the cancelled execution's projection.
The old execution stays immutable with its own events, session, output,
timing, billing, and termination state.

Engine:
- Add a run-scoped StageExecutionTracker on RunServices with per-node
  high-water marks. Ordinals are reserved after the StageStart hook
  passes on the first attempt (retries reuse the reservation), ensured
  at the composite checkpoint pre-step for hook-skips, and reserved in
  on_terminal_reached for terminal nodes' synthetic events.
- Keep three concepts distinct: graph visit (max_visits/checkpoints,
  unchanged), stage execution ordinal (the @N in StageId), and handler
  attempt. The tracker is not checkpointed; the append-only stage event
  history is its durable source of truth.
- resume() seeds the allocator from the run projection and computes a
  node -> StageId provenance map of executions observed after the
  selected checkpoint, threaded through execute_persisted_run,
  RunSession, and InitOptions.

Events and projections:
- stage.started, parallel.branch.started, and checkpoint.completed
  carry optional graph_visit and resumed_from_stage_id; StageProjection
  stores both. Old events deserialize with None and legacy duplicate
  stage.started replays keep last-attempt behavior.
- The CheckpointCompleted reducer is envelope-first: diffs and
  skipped-stage synthesis attach to the exact execution StageId, an
  existing Retrying projection finalizes as Skipped without losing
  identity, and historical node_outcomes no longer create or collide
  with newer ordinals (node_visits remains a legacy fallback).

Handlers:
- Parallel fan-out reserves child ordinals through the shared tracker,
  derives worktree pass{N} from the parent's execution ordinal, and
  seeds branch contexts with explicit child stage scopes so branch
  lifecycle and nested handler events agree.
- Artifact capture and manager-loop child logs follow the ordinal.

API and UI:
- RunStage documents visit as the execution ordinal and adds optional
  graph_visit and resumed_from_stage_id; Rust and TypeScript clients
  regenerated.
- The web sidebar lists both executions chronologically; resumed stages
  show a "Resumed from" link in the stage detail header and hover
  popover, with the graph visit surfaced when it diverges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:00:37 -04:00
Bryan Helmkamp
c06c60214a
refactor: simplify readiness-fallback plumbing
The fallback provider set was always catalog.all_provider_ids(), computed
at every call site and threaded through five layers alongside the catalog
itself. Fold it into Catalog::resolve_selection_with_catalog_fallback and
carry only a catalog_fallback flag through the transform/validate/
materialize entry points.

- materialize_run delegates to resolve_run_model again instead of
  re-inlining its provider normalization and selection
- run_preflight derives ready providers from llm_result instead of
  taking both, so callers cannot pass inconsistent pairs; the legacy
  tests now exercise the production ready-first routing path
- AppState::resolve_llm_client_with_ready_ids replaces three copies of
  resolve-then-extract-provider-ids, and ready_llm_provider_ids
  delegates to it
- the unreachable "model resolution failed" preflight check becomes an
  invariant error where the materialized run is produced
- validate_prepared_manifest_with_vars/_for_preflight share the
  ValidateInput construction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:49:01 -04:00
Bryan Helmkamp
c3cdefa5ea
refactor(events): simplify backward pagination internals
- Extract a shared fetch_run_events_page helper so the three client
  paging loops (full list, until, tail) no longer repeat the request/
  convert/has_more skeleton; fold the tail loop's two descending-order
  checks into one and drop its redundant had_events flag.
- Skip the latest-seq lookup in list_events_before_with_limit when the
  caller supplies a before_seq cursor, so a cold projection cache costs
  at most one full history scan per pagination session instead of one
  per page.
- Remove the dead before_seq max(1) clamp and the passthrough order()
  accessor from EventListParams.
- Document the CLI --tail 0 --follow seeding trick and the reader
  event_seq placeholder invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:34:02 -04:00
Release Repro
59d5b317dc
Simplify LLM error mapping and validate reasoning_effort parsing
- Return InvalidRequest (400) for unsupported speed too, matching the
  reasoning_effort check and the complete()/stream() doc comments
- Centralize fabro_llm::Error -> ApiError mapping in a From impl so the
  completions handler, playground handler, and Error::Llm arm agree on
  the InvalidRequest -> 400 / else -> 502 split
- Reject unparseable reasoning_effort values with 400 instead of
  silently dropping them
- Add classify_sdk_invalid_request test per fabro-workflow convention

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:29:12 -04:00
Bryan Helmkamp
0e4244a24a
refactor: simplify and harden the seek-based event listing
- Unify list_events_from with list_events_from_with_limit so projection
  replay shares the seek path instead of duplicating the decode loop
- Bound the event scan with keys::run_events_range instead of an
  unbounded range plus a manual prefix break, so slatedb never touches
  SSTs belonging to other runs or namespaces
- Store reader event_seq as None instead of a valid-looking sentinel of
  1, so appends through a reader-built inner fail as ReadOnly rather
  than writing duplicate sequences
- Borrow keys during scans instead of allocating a String per entry,
  drop a dead branch in cached_events_from, collapse recover_next_seq's
  single-caller parameters, and document the zero-padded key ordering
  invariant the seek depends on

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:29:06 -04:00
Bryan Helmkamp
af27e98e1c
refactor: simplify parallel handler and overview parsing
- Extract emit_branch_completed() to replace three near-identical
  ParallelBranchCompleted constructions; status now reads consistently
  from outcome.status
- Add context_diff_public() so parallel.rs and manager_loop.rs share the
  diff-minus-engine-internal-keys step; move context_diff tests next to
  the function in context.rs
- Replace fan_in's dead BranchShape struct with the canonical
  Vec<ParallelBranchResult> (from_value moves, so no payload cloning)
- Narrow parseParallelOverview to ParallelBranchSummary {id, status};
  its only consumer renders just those fields
- Drop helpers.test.ts's duplicate envelope() fixture in favor of the
  shared makeEventEnvelope

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:28:58 -04:00
Bryan Helmkamp
9708ca8177
feat(llm): add Fireworks AI as an opt-in provider
Adds a disabled-by-default `fireworks` provider to the built-in catalog,
served through the existing openai_compatible adapter/codec. The curated
roster covers Kimi K2.7 Code (default), Kimi K2.6, DeepSeek V4 Pro/Flash,
GLM 5.2, MiniMax M2.7, Qwen 3.7 Plus, and GPT-OSS 120B/20B (small
default + probe), with serverless pricing including cached-input rates.

All api_ids were verified live against /chat/completions (Fireworks'
GET /v1/models only returns a featured subset), and serverless responses
were confirmed to report prompt_tokens_details.cached_tokens, so cache
billing works through the existing codec path.

FIREWORKS_API_KEY is registered as an optional vault secret; provider
login, vault storage, and diagnostics probing are catalog-driven and
need no code changes. Includes catalog/install tests, two live e2e
tests, an integrations docs page, and a provider logo for the web UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:03:39 -04:00
Bryan Helmkamp
8f0ecfb170
fix(store): reuse shared run projections 2026-07-24 07:49:58 -04:00