Commit graph

138 commits

Author SHA1 Message Date
Scott Werner
42dcb410f2 Remove obsolete manifest Docker image argument 2026-08-01 09:03:10 -04:00
Release Repro
799fac1d3b
style(models): group DeepSeek price digits 2026-07-31 13:17:44 -04:00
Release Repro
40391ac3ac
fix(models): use exact DeepSeek context window 2026-07-31 13:17:08 -04:00
Release Repro
1016f995c4
chore(models): pin DeepSeek agent profile 2026-07-31 13:16:37 -04:00
Release Repro
76dae568f5
feat(reasoning): expose DeepSeek effort controls 2026-07-31 13:14:11 -04:00
Release Repro
f613821bfb
feat(llm): add direct DeepSeek provider 2026-07-31 13:10:08 -04:00
Release Repro
8864e0f0cf
feat(models): refresh DeepSeek V4 Flash metadata 2026-07-31 13:02:58 -04:00
Release Repro
0afd3a43b6
feat(models): add portable DeepSeek aliases to Fireworks 2026-07-31 12:59:40 -04:00
Release Repro
8bae35398f
fix(models): point DeepSeek aliases to V4 Flash 2026-07-31 12:59:00 -04:00
Bryan Helmkamp
d4119cee77
fix: keep stored run events from older releases deserializable
Copilot review flagged two backward-compatibility breaks with events
persisted by pre-model-keyed releases; both are stored data that can
never be rewritten, so accept the old shapes on read:

- FailoverProps: original_provider/original_model/attempt are Option
  again with serde defaults. New events always set them; failover events
  recorded before model-keyed fallbacks lack them. Restores the
  historical-event test.
- RunModelSettings: temporary custom deserializer accepts the legacy
  flat-array fallbacks shape inside stored run.created events, keying
  the chain under the requested model name when one is set. Remove once
  pre-0.311 run logs are out of the support window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:24:08 -04:00
Bryan Helmkamp
e6eb36f852
refactor: simplify model-keyed fallback internals
Consolidation pass over the fallback feature, no intended behavior
changes beyond noted validation and event-shape cleanups:

- Unify the two parallel notice types: FallbackPlanNotice is gone;
  ModelFallbackNotice now owns the runtime NoNearbyReasoningLevel case
  and the shared ChainEmpty wording. Notices emit through a new
  Emitter::notice_scoped with their own level, and each distinct notice
  is emitted once per run instead of on every LLM call.
- Move canonical_model_id onto Catalog so chain keys are written and
  read through one function; reject provider-qualified fallback keys,
  which could never match at dispatch and were silently dead config.
- Type FallbackTarget as ProviderId/ModelId, removing repeated
  ProviderId::new re-wrapping at every use site.
- Derive FallbackPlan's current route from a position index instead of
  storing current/requested_controls copies; advance() no longer has
  unreachable None branches.
- Bundle the agent invocation's live state (session, bridge, lease,
  forwarder, accounting) into LiveAgentInvocation; failover_agent_session
  drops from 21 parameters to 7 and the six copies of the
  abort/discard/classify teardown collapse into two methods.
- Share one route_request builder between one_shot and its failover
  loop; complete_one_shot_request takes the request by value instead of
  deep-cloning the message payload per call.
- Event::Failover carries FailoverProps directly; the props' original
  route and attempt fields are now required, and reasoning efforts are
  typed ReasoningEffort instead of strings.
- Reuse RunModelSettings/RunModelControls in fabro-api via
  with_replacement, add the missing controls property to the OpenAPI
  schema, regenerate the TS client, and add the type-identity/JSON
  parity test.
- Smaller cleanups: ReasoningEffort::closest_supported uses enum
  discriminants; ModelFallbackPolicy gains len(); resolve_model_fallbacks
  takes a provider slice; duplicate-target filtering lives only in the
  resolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:51:36 -04:00
Bryan Helmkamp
ba82656656
feat: add model-keyed fallback policies 2026-07-30 12:02:41 -04:00
Bryan Helmkamp
2efcc72124
Merge pull request #684 from fabro-sh/feat/portable-model-fallbacks
feat(workflow): make model fallback chains portable
2026-07-30 06:42:53 -04:00
Bryan Helmkamp
8a597ca264
fix(workflow): report the real cause when a fallback cannot resolve
Addresses review feedback on the fallback notice work.

A provider-only fallback such as `openrouter` needs the primary model's
catalog entry to find the closest capability match. When the primary is
itself a passthrough selector there is no entry, so every provider-only
candidate was skipped with "provider `X` has no compatible model" even
when that provider had plenty. Adds a `PrimaryNotInCatalog` notice that
names the missing primary instead of blaming the provider.

Also from review:

- `code()` was a wildcard fallthrough, which docs/internal/events-strategy.md
  forbids for new variants. Now exhaustive.
- `NoConfiguredOffering` discarded the `providers` list that
  `NoEligibleOffering` hands it. The notice now names the providers that do
  offer the model.
- `ModelFallbackNotice::reference` was a rendered `String`; it is now the
  `ModelRef` it came from, which also drops the per-candidate double
  allocation the previous refactor introduced.
- `ResolvedStartLlm` unpacked and repacked `ResolvedFallbackChain`
  field-for-field; it now holds it directly.
- Added `FallbackTarget: Display` as `provider:model`, replacing two
  hand-written `"{}:{}"` format strings.
- `Catalog::select` still inlined the `require_provider` body.
- Emission moved to `ModelFallbackNotice::emit_all`, covered by a new test
  proving notices reach the event stream with the right level, code, and
  message. Nothing tested that hand-off before.

Documented in `resolve_fallback_chain` why an unknown provider stays a hard
error while an unconfigured one is skipped, and that an unqualified unknown
selector pins to the primary's provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:37:51 -04:00
Bryan Helmkamp
727cd52c3b
fix(types): trim class names in Node::add_class
The doc comment promised blank names were ignored, but the guard only
rejected the empty string. Stylesheet selectors match class names exactly,
so a padded name would sit in `classes` and match no rule.

No current caller can pass one: the parser splits on whitespace, and the
subgraph and import paths strip everything but alphanumerics and hyphens.
This makes the public contract on the shared type match what it claims.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:25:42 -04:00
Bryan Helmkamp
48fd09aaab
refactor(workflow): simplify fallback chain resolution
Follow-up cleanup on the portable fallback chain work.

- Add `Catalog::require_provider` and `Catalog::provider_id`, replacing the
  `catalog_provider_id` free function in `start.rs` and two copies of the same
  `provider(..).ok_or_else(UnknownProvider)` block inside the catalog.
- Add `FallbackTarget::new` and use it for the six struct literals that each
  stringified a provider and model by hand.
- Extract per-candidate resolution into `resolve_fallback_candidate`, returning
  a `FallbackCandidate` that is either a target or the skip reason. This flattens
  `resolve_fallback_chain` from four levels of nesting to one loop and splits the
  qualified/unqualified model arms into separate match patterns.
- Drop the `seen` HashSet and its per-candidate key clones in favor of a
  `contains` check on the chain being built; fallback chains hold a handful of
  entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:23:25 -04:00
Bryan Helmkamp
01c4a4a23b
test(graphviz): table-drive the class separator cases
One row per separator rule, so a regression names the input that broke
instead of pointing at a combined fixture string. Also record why
`add_class` keeps insertion order: `fidelity` falls back to the first class
for the thread ID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:17:37 -04:00
Bryan Helmkamp
85586151e5
refactor(graphviz): parse node classes in one place
Node classes were built in two places. The parser split the `class`
attribute on commas and whitespace, but the import transform re-split the
raw attribute on commas only. A space-separated class on an import
placeholder became a single class name, so stylesheet rules did not match.
That included the `class="fast shared"` example in the imports docs.

- add `Node::add_class`, replacing the duplicate append helpers in
  `SemanticState` and `ImportTransform`
- read `node.classes` in `placeholder_config` instead of re-parsing the raw
  attribute, so class splitting happens in exactly one place
- name the separator rule `split_class_attr`, splitting on commas and then
  whitespace so empty entries need no trimming
- drop the unused `Node::class` accessor that invited the re-parse
- keep the comma-compatibility note in the DOT attribute reference only

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:15:30 -04:00
Bryan Helmkamp
e0f73f963a
feat(workflow): make model fallback chains portable 2026-07-29 18:08:26 -04:00
Bryan Helmkamp
6e2a2ac652
Merge remote-tracking branch 'origin/main' into feat/command-stdin-source
# Conflicts:
#	docs/public/workflows/stages-and-nodes.mdx
#	lib/components/fabro-validate/src/rules/inert_attribute.rs
#	lib/components/fabro-workflow/src/handler/command.rs
2026-07-29 11:37:27 -04:00
Bryan Helmkamp
0eda219376
Simplify stdin_source plumbing after review
- ExecStreamingRequest: drop #[non_exhaustive] and the six Option-taking
  builder setters; call sites use struct literals over ::new(), matching
  GrepOptions/WalkOptions, and providers can destructure exhaustively
- Docker: pass ExecStreamingRequest through docker_exec_shell_streaming
  instead of seven positional args; revert the no-op StartExecOptions
- Daytona: stdin temp-file cleanup is now best-effort (mirrors
  DaytonaSession::close) so a failed delete cannot fail a completed
  command or double-delete from Drop; upload overlaps session creation;
  one shared DAYTONA_CLEANUP_TIMEOUT
- write_process_stdin tolerates ConnectionReset/ConnectionAborted so a
  command that stops reading stdin does not fail on TCP Docker daemons
- Local sandbox aborts the stdin writer after process exit instead of
  joining unbounded
- Cap stdin_source payloads at 10 MiB, mirroring the for_each bound
- Add Node::context_key_attr() tri-state so the handler and lint rule
  share one definition of a valid context-key attribute
- inert_attribute canonicalizes handler types via StageHandler, fixing
  false warnings for command attrs on tool nodes
- Share resolve_flat_context_value between command stdin and for_each;
  resolve_json_value takes Value by value, removing a deep clone
- Reuse MockSandbox in command handler stdin tests instead of extending
  SpySandbox with a hand-rolled streaming override

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 11:14:48 -04:00
Bryan Helmkamp
d37fc0027c
fix(workflow): align inferred command behavior 2026-07-29 10:54:43 -04:00
Bryan Helmkamp
cb24f47b59
Merge remote-tracking branch 'origin/main' into feat/infer-command-node-from-script
# Conflicts:
#	docs/public/workflows/stages-and-nodes.mdx
#	lib/foundation/fabro-types/src/graph.rs
2026-07-29 10:40:26 -04:00
Bryan Helmkamp
5d72f9a538
Add context-sourced command stdin 2026-07-29 10:25:51 -04:00
Bryan Helmkamp
a369ea7fc4
Merge remote-tracking branch 'origin/main' into feat/for-each-item-injection
# Conflicts:
#	apps/fabro-web/app/components/stage-renderers/parallel-children.tsx
2026-07-28 20:03:25 -04:00
Bryan Helmkamp
17b3223558
Neutralize runtime for_each labels before display
Addresses three Copilot review comments on #653.

`item_label` comes from a model or a workflow author, and it reaches the
terminal through the CLI progress display. A label could carry ANSI
escapes, newlines, or bidi overrides and rewrite what the operator sees.
It could also be whitespace-only, giving a branch a blank identity.

Add `text::sanitize_display_label`: strip ANSI sequences, drop control
and bidi-reordering characters, trim, and elide past 80 characters.
Return an empty string when nothing printable survives so callers fall
back to an identity they control.

Apply it where the label is created, so events, the store, and the web
UI all get a clean value instead of each consumer having to remember.
`parallel_branch_display` sanitizes again, because a run recorded before
this commit still has raw labels in its event log.

`emit_branch_retrying` now sets `stage.retrying`'s `index` from the
branch stage's execution ordinal, matching the envelope `stage_id` and
the meaning every other emitter gives that field. The branch's position
in the fan-out is already on `parallel.branch.started`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:52:59 -04:00
Bryan Helmkamp
ee9189ddb5
Reuse existing helpers in the for_each fan-out
Simplification pass over the for_each branch. No behavior change.

Share what was duplicated:
- Node::prompt_or_label replaces the "prompt, else label" fallback that
  agent, prompt, and the for_each item injector each wrote out.
- context::lookup_flat replaces the "exact key, then strip context."
  lookup that condition.rs had twice and the for_each source had again.
- is_llm_handler_type replaces the inline agent/prompt match, so the
  runtime and the for_each_contract rule agree by construction.
- find_join_node now takes branch ids, so a for_each fan-out passes its
  template target instead of needing find_join_for_target.
- collect_events moves to test_support; parallel and integration tests
  shared one copy already.
- One ScriptedHandler replaces four test handlers that differed only in
  what they returned.

Straighten the branch retry loop:
- Reserve the branch scope once before the loop instead of guarding it
  with an Option, which removes three expect() calls.
- acquire_branch_permit and backoff_or_cancel replace the cancel-aware
  select! blocks the loop repeated verbatim.
- Keep the match arms in Executor::execute_with_retry order so the two
  loops stay easy to compare.

Drop redundant state:
- BranchPlan::is_for_each derives from template_target_id.
- for_each_contract checks node type before source shape, so one
  mistake reports one diagnostic.

Prove the new wire fields survive the OpenAPI boundary: the fabro-api
round-trip fixture now carries index and item_label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:42:23 -04:00
Bryan Helmkamp
f4c09867e5
Merge origin/main into refactor/remove-env-interpolation 2026-07-28 18:31:16 -04:00
Bryan Helmkamp
fa85bc42a2
Merge pull request #643 from fabro-sh/feat/claude-5-profile
feat(agent): add Claude 5 profile
2026-07-28 18:25:21 -04:00
Bryan Helmkamp
b611d946a6
Merge pull request #673 from fabro-sh/feat/modal-llm-provider
Add Modal as an LLM provider
2026-07-28 18:14:43 -04:00
Bryan Helmkamp
977628000b
Merge pull request #668 from fabro-sh/feat/provider-qualified-fallbacks
Support provider-qualified model fallback selectors
2026-07-28 18:13:43 -04:00
Bryan Helmkamp
6c516ec31d
Merge pull request #660 from fabro-sh/feat/parallel-branch-live-status
Show live status for parallel branches
2026-07-28 18:13:21 -04:00
Bryan Helmkamp
8e4251bb07
refactor(llm): simplify the Modal provider tests and fixtures
Reduce duplication and over-specification introduced with the Modal
provider, without changing shipped behavior.

- Extract enabled_provider_catalog and assert_deep_tool_round_trip in
  the fabro-llm integration tests. The Poolside, Fireworks, OpenRouter,
  and Modal deep round trips were four near-identical copies.
- Add ApiCredential::with_extra_headers for providers that authenticate
  with request headers instead of an API key.
- Replace the unreachable require_env guards in the Modal e2e test with
  the std::env::var form used by every sibling test, and register
  MODAL_TOKEN_ID and MODAL_TOKEN_SECRET in EnvVars.
- Collapse modal_requires_both_vault_proxy_tokens to a single case. The
  loop rebuilt the whole built-in catalog per iteration.
- Drop tautological and over-specified assertions: the api_key_url doc
  URL, the forced default/probe lookups on a single-model provider, and
  the get_on_provider loop that could not fail.
- Inline the single-use modal_env_catalog fixture and note why it
  overrides the shipped secrets templates.
- Sort the MODAL_* keys in .env.example, and record in modal.toml why
  api_id keeps the Hugging Face capitalization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:06:07 -04:00
Bryan Helmkamp
f8325c5c48
Let the first separator decide the model reference form
Addresses Copilot review feedback on #668.

Checking for `:` before the legacy `/` form broke `provider/model`
references whose selector contains a colon, which Bedrock API IDs do:
`bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0` parsed as the
whole path up to the last colon, then `0`. On main it is a pin to
bedrock with the full ID as the selector.

Now whichever separator appears first decides. A `/` before any `:` is
the legacy pin and its selector may contain colons. Otherwise the token
stays bare and `qualify` promotes it only when the prefix names a
provider, so `openrouter:moonshotai/kimi-k3` still qualifies.

Adds the regression test Copilot asked for, covering both Bedrock-style
legacy input and the colon-before-slash case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:01:05 -04:00
Bryan Helmkamp
00228383dd
Merge remote-tracking branch 'origin/main' into refactor/remove-env-interpolation
# Conflicts:
#	lib/foundation/fabro-types/src/settings/interp.rs
2026-07-28 17:54:35 -04:00
Bryan Helmkamp
503e21fbc1
Merge origin/main into feat/parallel-branch-live-status
Conflicts were between this branch's parallel-branch identity work and
main's stage billing, review targets, and live stage timing.

- Stage fixtures: main added `billing` to each per-file `makeStage`; this
  branch had hoisted one builder into `lib/test-utils`. Kept the hoisted
  builder and gave it `billing: makeBilledTokenCounts()`, so both intents
  hold and the field list stays in one place. `stage-sidebar.test.ts` also
  builds raw `RunStage` wire payloads, so it keeps importing
  `makeBilledTokenCounts` directly.
- Import lists (`run_projection.rs`, `fabro-api/src/lib.rs`,
  `run_state.rs`, `stage_projection_round_trip.rs`): unioned both sides —
  `ParallelBranchId` alongside `timing`, `ReviewTarget`,
  `ReviewTargetKind`, `AttrValue`, `Node`, and
  `StageToolBatchProjection`.
- `fabro-server` tests: git interleaved two unrelated new tests into one
  body. Split them back into
  `list_run_stages_exposes_parallel_branch_identity` and
  `run_billing_includes_live_stage_timing_in_rows_and_totals`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:50:36 -04:00
Bryan Helmkamp
9d828a8688
Merge remote-tracking branch 'origin/main' into refactor/remove-env-interpolation
# Conflicts:
#	lib/components/fabro-workflow/src/pipeline/pull_request.rs
2026-07-28 17:44:03 -04:00
Bryan Helmkamp
239490a553
Merge pull request #664 from fabro-sh/feat/script-value-interpolation
Interpolate goal, inputs, and vars in command node scripts
2026-07-28 17:41:10 -04:00
Bryan Helmkamp
b19dda6aa3
Merge pull request #661 from fabro-sh/fix/small-default-skips-providers-without-small-model
Fix run title generation picking a flagship model, and sharpen the prompt
2026-07-28 17:38:27 -04:00
Bryan Helmkamp
6226c8c517
fix: address env interpolation review findings
Restore the documented SDK env credential facade without reintroducing run fallback behavior. Fail closed on GitHub permission resolution, require worker storage at the CLI boundary, and align interpolation names and generated docs.
2026-07-28 17:30:50 -04:00
Release Repro
81d762aa9d
Merge remote-tracking branch 'origin/main' into feat/script-value-interpolation
# Conflicts:
#	lib/components/fabro-workflow/src/pipeline/transform.rs
2026-07-28 17:27:24 -04:00
Bryan Helmkamp
690ddd2a96
Fix live parallel branch refresh and simplify branch rendering
Branches bypass the engine's stage.started/stage.completed lifecycle, so
no SWR key invalidated the stages list while a fork ran. The new live
branch rows stayed frozen at their first observed state until an
incidental refetch. Map parallel.* events to the stages list, run events,
and graph keys.

Also:
- Label branch rows with formatStageLabel so a re-entered branch renders
  as `review_glm@2`, matching the sidebar and waterfall.
- Build branch rows in one pass and count live outcomes in one loop.
- Name ParallelBranchId in the OpenAPI spec and reuse fabro_types::
  ParallelBranchId, replacing two copies of an inline string format.
- Hoist makeStage and textContent into lib/test-utils so widening Stage
  cannot leave per-file fixtures stale (tests are excluded from
  typecheck, so the two component-test copies had already gone stale).
- Query stat tiles by data-stat instead of an exact Tailwind class.
- Reuse append_scoped_stage_event's body via append_event_with_scope and
  add test_branch_event instead of poking envelope fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:26:05 -04:00
Release Repro
b04684aec4
fix(workflow): harden script value interpolation 2026-07-28 17:21:35 -04:00
Bryan Helmkamp
e91343bbeb
refactor: address run title review findings 2026-07-28 17:14:03 -04:00
Bryan Helmkamp
df0bd58819
Merge remote-tracking branch 'origin/main' into fix/small-default-skips-providers-without-small-model
# Conflicts:
#	lib/apps/fabro-server/src/test_support.rs
2026-07-28 17:06:20 -04:00
Bryan Helmkamp
1aa7a153b0
Merge pull request #652 from fabro-sh/fix/publish-failures
Make publish failures terminal
2026-07-28 17:03:18 -04:00
Bryan Helmkamp
2103e3fbde
Qualify colon model references against the provider registry
Splitting on the first colon in FromStr broke bare model IDs that
legitimately contain one. A reference like "llama3:8b" parsed as
provider "llama3" selector "8b", and since "llama3" is not a provider
the lookup failed instead of passing the ID through to the pinned
provider. Verified against origin/main: canonical_session_model with
"future-model:latest" pinned to openrouter returned the passthrough
before and a 400 after.

This is not fixable by choosing a different separator. Bedrock
inference-profile ARNs contain both colons and slashes, and
docs/public/integrations/bedrock.mdx tells users to put arbitrary
inference-profile IDs in api_id. Only the registry can tell a provider
prefix from a model ID that happens to contain the separator.

FromStr now leaves colon-bearing tokens bare, and ModelRef::qualify
promotes only those whose prefix names a known provider. resolve()
applies it, so the fallback path is covered; sessions.rs applies it
before its own match so it keeps its tailored ambiguity messages.

ModelRegistry is now implemented for Catalog in fabro-types, replacing
the CatalogModelRegistry wrapper that existed only in start.rs, so both
call sites share one registry view.

Covered by regression tests at both surfaces, plus qualify unit tests
for ollama tags and Bedrock ARNs. The pre-existing passthrough test
canonical_session_model_preserves_unknown_passthrough_on_selected_provider
passes again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:02:16 -04:00
Bryan Helmkamp
e6cf174578
Merge origin/main into feat/human-review-target
Conflict in apps/fabro-web/app/components/interview-dock.tsx. Main moved
the dock onto the shared collapsible `RunDockShell` and replaced the
local button constants with shared ones.

Kept main's structure whole and re-applied the review target rendering
onto it: the question paragraph in the shell's `body` becomes the linked
`ReviewTargetQuestion` when the target passes `safeReviewTarget`, and
plain text otherwise. Both now share main's paragraph classes through
`QUESTION_TEXT`, so the two renderings stay visually identical.

`peek` keeps using `question.text`, which is the correct plain-text
collapsed summary for a review target question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:48:02 -04:00
Bryan Helmkamp
8771c971d7
Add Modal LLM provider 2026-07-28 16:02:57 -04:00
Bryan Helmkamp
450a1701b2
Merge pull request #670 from fabro-sh/fix/sandbox-activate-before-access
Reactivate stopped sandboxes at workflow boundaries
2026-07-28 15:54:57 -04:00