Commit graph

694 commits

Author SHA1 Message Date
Bryan Helmkamp
5dae891a98
Merge branch 'petri-integration-read' into petri-integration
# Conflicts:
#	Cargo.lock
#	lib/components/fabro-petri/Cargo.toml
#	lib/components/fabro-petri/README.md
#	lib/components/fabro-petri/src/lib.rs
2026-09-17 23:48:36 -04:00
Bryan Helmkamp
53b16b591d
Merge branch 'petri-integration-adapters' into petri-integration 2026-09-17 22:40:30 -04:00
Bryan Helmkamp
e2bf05c0f0
Project a Petri run's records into Fabro's run view
The projection folds Petri's public events (replay_since over the run's
stored records) and Fabro's platform records into the RunProjection the
API serves, row by row as VIEWS.md maps them. The stage key is the
execution and firing; the StageId label is node@visit, made unique with
the execution when two child invocations would share one. A stage's
first_event_seq is the milliseconds from the run's creation to its
visit.started, so the view built live equals the view rebuilt from the
records whatever order two logs' records were committed in.

The projector is the view pass and its wake-up. Records first: an append
returns before any view work; a pass reads what is committed, folds the
items past the committed positions, and writes the projection document,
the ordered stream (one stream_seq per Petri event or platform record,
with the item's own identity beside it) and the narrowed runs row in one
later transaction. Signals coalesce per run, a lost signal costs only
latency, the startup pass folds every run the view trails, and a pass
that races a platform record leaves the view alone and runs again. A
torn tail holds the view where it stands and reports the run incomplete
with the replay's error; inspect_run decides completeness once the run
recorded its finish.

The tests build the view live for the hello bundle, a command workflow
and a two-branch parallel workflow and compare it with the rebuild; drop
every wake-up and catch up by a signal and by the startup pass; crash
between the record commit and the view transaction and apply only the
suffix; restart the projector over child executions; and hold at a torn
tail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 22:32:23 -04:00
Bryan Helmkamp
223e10ea20
Install Petri's interview, secret, blob and home adapters in a Fabro run
A Petri run in the worker, and in the server under its test override, now
gets Fabro's platform adapters instead of the standalone defaults:

- `fabro_petri::interview`: Petri's `Interviewer` over the questions API
  and the worker's control channel. A human gate's question is posted as
  the `interview.started` event a legacy stage emits, keyed by an id
  derived from Petri's identity (node, execution, firing, occurrence,
  ask), so the API, the web app and Slack list it; the answer posted to
  the questions endpoint reaches the control interviewer the adapter waits
  on and is mapped onto Petri's answer. An expiry the gate reports is
  completed as `interview.timeout`, a cancel as `interview.interrupted`,
  and an auto-approved run answers itself. The hook points the read side
  takes over are marked.
- `fabro_petri::secrets`: Petri's `SecretProvider` over the vault's token
  entries, so `{{ secrets.NAME }}` resolves at spawn and is masked in every
  record; a sensitive answer registers as a dynamic secret.
- `fabro_petri::blobs`: Petri's `OutputStore` over Fabro's `blobs` table,
  through the server's blob store or the worker's client.
- The Fabro home the server resolved travels to the worker as
  `--fabro-home`, so the skills step reads it whatever the worker's
  environment says.

`engine::RunRequest` takes the interviewer, its observers, the secret
provider and the blob table from the caller; `interviewer::Unattended` is
gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 22:31:35 -04:00
Bryan Helmkamp
f4fe505bac
Move the Petri pin to 83345a8
Petri's `fabro-integration-p1` branch at 83345a8 adds
`Runtime::check_source`, the in-memory check entry point over a
`frontend::FileSource`, and makes `[workflow] engine` a known
`workflow.toml` key in its Fabro frontend, refusing any other unknown
`[workflow]` key with `unsupported.workflow_toml.key`.

Every `petri_*` entry moves from a0d2ceb to 83345a8. The lockfile changes
only the source line of the fifteen Petri packages; no other crate moves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 21:31:04 -04:00
Bryan Helmkamp
16354186fa
Run a Petri run in the worker process over the HTTP store
When `fabro run __run-worker` finds its run's stored spec names Petri, the
new `petri_worker` module executes it through `fabro_petri::engine` over
`HttpRunStore`, leased for a launch id the worker mints and logs at start.
`--mode start` loads the admitted graphs through the client's blob read;
`--mode resume` continues the run from its records. The worker's existing
services carry over: the control channel's cancel and SIGTERM/SIGINT cancel
Petri's root invocation politely, a lost control channel cancels the run
and is reported once it settles, and pause, unpause and steer are received
and ignored with a warning until their adapters land. The model client
comes from the worker's catalog and vault snapshot for the providers whose
credentials resolve, and the lifecycle events (`run.starting`,
`run.running`, then `run.completed` or `run.failed`) go through the client
as the legacy worker's do.

Scenario tests against the real binary: a command-only Petri run executes
in the worker a foreground server launched, its records reach
`petri_records` over the HTTP store and its lease ends with the worker; and
a run whose server and worker are both killed mid-stage resumes in a new
worker after the server restarts, with one `run.completed`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 21:20:22 -04:00
Bryan Helmkamp
832f39f704
Merge branch 'petri-integration-http' into petri-integration
# Conflicts:
#	Cargo.lock
#	lib/components/fabro-petri/Cargo.toml
#	lib/components/fabro-petri/README.md
#	lib/components/fabro-petri/src/lib.rs
2026-09-17 20:37:03 -04:00
Bryan Helmkamp
03309d4240
Let Petri compile and execute a Fabro run through fabro-petri
`check` materializes a workflow version's bundle into a temporary directory
(`Runtime::check` reads files from disk), lowers it with the run's inputs
and launch, and returns the admitted graphs or Petri's diagnostics in a
shape the server maps onto Fabro's. `admission` keeps the admitted graphs
in the blob store, named on the run spec and verified by digest on load.
`runtime` assembles the same Petri runtime at create and at execution: the
Fabro frontend with the server's settings layer, the Attractor step kinds,
the model client as the PebbleClient capability so admission pins every
model. `engine` runs the admitted graph in the server process over
SqliteRunStore under the Fabro run id, with the standalone defaults, an
interviewer that fails any question, and cancel on a token, and derives the
outcome from inspect_run over the run's record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 20:27:44 -04:00
Bryan Helmkamp
2ca1e1cda0
Serve the Petri run store to workers
The server answers the `/api/v1/runs/{id}/petri/*` endpoints from one
`SqliteRunStore` over its pool. `PetriRuns` in `AppState` keeps the
writer handle each worker opened, keyed by the run and the worker's
owner id, so the lease semantics stay the store's: the handle drops on
the worker's `release`, and every handle of a run drops when the server
observes the run's worker exit, in the subprocess wait path. Never by
timeout. A write from an owner with no held handle reopens only when
the lease row still names that owner, so a server restart or a lost
open reply recovers, and an owner the lease moved away from gets
`petri_stale_owner`.

Every endpoint is worker-scoped through the existing worker auth; a
new `RequireWorkerRunSegment` extractor covers the two-segment routes.
Store errors answer with a machine-readable code, the leased owner and
the conflict position under `meta`, and a backend failure's cause goes
to the server log rather than the worker.

A test drives a held worker through the scheduler, opens the run over
the API with its token, ends the worker, and sees the lease end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 20:12:20 -04:00
Bryan Helmkamp
3ffe7e00ce
Implement Petri's run store over Fabro's SQLite database
`SqliteRunStore` implements Petri's `RunStore` and `RunLogs` on the pool
Fabro's other stores share. A run's existence and writer lease live in
`petri_runs`; every record of every log lives in `petri_records`, keyed by
(run, log, seq) with the record stored as JSON and read back unchanged;
blobs share the `blobs` table with `BlobStore`. The lease is taken
idempotently per owner, ends when the last handle drops or when an
operator releases it, and never by timeout; every write checks it inside
its own transaction. An append is one `BEGIN IMMEDIATE` transaction per
batch: a repeated record is accepted, a different record at a taken seq or
a seq past the head is a conflict that stores nothing.

Petri's store conformance suite passes against it, with the operator
release, lease exclusivity, a crash between appends, and blob
interoperation checked beside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 19:30:30 -04:00
Bryan Helmkamp
7eb5ca502c
Add the fabro-petri crate and pin the Petri packages
Fabro runs its workflows on Petri. The six Petri packages and the testkit
are pinned by revision in the workspace manifest under `petri_*` keys, and
`fabro-petri` is the one crate that depends on them. The crate's tests run
the `hello` bundle in memory on the stub registry and a command-only
workflow on the host sandbox; both skip without the sandbox-driver host
plugin, and the sandbox-plugins CI job requires it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 19:23:30 -04:00
fabro-releases[bot]
170291b9f7 Bump version to 0.357.0-nightly.0
Some checks failed
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
Rust / Test (macOS) (push) Has been cancelled
Rust / Sandbox plugins (stdio) (push) Has been cancelled
2026-09-15 09:36:37 +00:00
Bryan Helmkamp
7d5e696c18
Merge pull request #873 from fabro-sh/sandbox-driver-64c14b8
Pin sandbox-driver main 64c14b8 and surface dropped Daytona output
2026-09-14 16:31:07 -04:00
Bryan Helmkamp
c98785d84a
Pin pebble a39f43e and refuse an older stored session record clearly
Pebble main a39f43e26effdf99635eaf343f095c17157c9c93 (pebble #22) carries
an assistant turn's usage as Usage in the session record and moves the
record format to version 5. CodingRuntime::from_record refuses a record
in another format with UnsupportedRecord { version, supported } before
it reads the route. Fabro persists those records in SQLite for Ask Fabro
resume, and old runs get no migration, so a record written by an older
build is read back as stored and refused on the next turn.

Two tests pin that down. The store reads pebble's own version 4 fixture
back through get without a parse error and reports it unsupported. A
resumed Ask Fabro session whose stored record declares the previous
format fails its next turn with the agent_error code and the message
"session record format version 4 is not supported (this build requires
5)", runs no turn, and leaves the stored record in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 12:58:31 -06:00
Bryan Helmkamp
ef86ad278a
Carry usage as lithos-llm's Usage and rename billing to usage
Re-pin lithos-llm to 55add4596b861a0623d00c3a54aa5c147c8d504b and
pebble to c91810fe51aece80359b9cd8efea971af0c46925, where token usage
and cost travel together as Usage { tokens: TokenCounts, cost:
Option<Cost> }. Fabro now carries that one type everywhere it used to
carry BilledTokenCounts, BilledModelUsage, UsdMicros, or a token count
beside a cost_usd_micros.

fabro-types: billing.rs is usage.rs with ModelRef, ModelUsage { model,
usage }, sum_usage, and usage_is_empty; billing_rollup.rs is
usage_rollup.rs with ProjectionUsageStage, ProjectionUsageByModel,
ProjectionUsageRollup, and usage_rollup_from_projection. Every usage
field is named usage: StageProjection.usage and usage_by_model,
Outcome<Option<ModelUsage>>, stage.completed and stage.failed usage and
usage_by_model, prompt.completed usage, run.completed and run.failed
usage (total_usd_micros is gone), Conclusion.usage, StageSummary.usage,
Run.usage. RunSize buckets by Cost.

fabro-workflow: model_usage_from_llm prices tokens from the catalog with
a Catalog cost source, with_reported_cost keeps a provider cost, and the
pebble handler's stage_usage groups pebble's accounts by model and sums
rows with Usage::saturating_add, so a total has a cost only when every
priced part was priced. The store fold's live usage is the agent's
usage plus its descendants'.

API: the OpenAPI spec deletes BilledTokenCounts, BilledModelUsage,
CompletionUsage, CompletionCost, TokenUsage, and RunBillingSummary,
adds TokenCounts, Cost, Usage, and ModelUsage, and renames every
billing schema, property, tag, path, and operation to usage. fabro-api
reuses lithos-llm's and fabro-types' types through with_replacement,
with a round-trip test per replacement.

Old stored runs get no migration: their pebble events in the old shape
read back with zero usage, and their rebuilt projections lose agent
usage.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 12:31:34 -06:00
Bryan Helmkamp
5e4a3b393d
Pin sandbox-driver main 64c14b8
Moves the eight sandbox-driver crates from ddb32e1 to 64c14b8, which
brings sandbox-driver PR #22 (Daytona output resync): the Daytona
plugin's encoded-exec decoder no longer fails a command on a torn frame.
It discards through the next newline, counts the loss in the new
`sandbox_driver::OutputLoss { dropped_frames, dropped_bytes }`, and
carries it as `ExecStreamingResult::output_loss` (serde default) across
the plugin wire. A loss also sets `truncated` on both captures, so
`into_complete()` refuses while `run_streaming` completes. PR #21
(supervisor generations) was already on main under the previous pin.

Nothing in fabro needed a source change for the bump. The lockfile
changes only the eight driver source lines; the unrelated windows-sys,
windows-core, and errno flips `cargo update` proposed are not taken, and
`cargo metadata --locked` accepts the result.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 12:24:02 -06:00
Bryan Helmkamp
2c2746c762
Pin pebble main 6d802a9
Pebble #18 adds tool-result and transcript rendering options to
pebble-cli-core (RenderOptions, Renderer::options, and
session::run_prompt_with). Pebble #19 keeps a paired human's hold across
a route failover; no embedder change is needed for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 10:46:26 -06:00
fabro-releases[bot]
15198ca23a Bump version to 0.356.0-nightly.0 2026-09-14 09:42:30 +00:00
Bryan Helmkamp
49987c69cb Pin sandbox-driver main ddb32e19
The pin moves from the head of the section-4-driver-items branch (a92c0db6,
since merged as #20) to main, which adds #21: the protocol crate's
PluginSupervisor gains numbered generations, a health probe before a
generation serves, and a refusal of a replacement that reports another
resource namespace. Petri pins the same revision, so the two runners share
one sandbox-driver.

cargo build --workspace and cargo nextest run -p fabro-sandbox -p
fabro-workflow pass (1502 tests, 48 skipped).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvCtm3CHs5TFbQqUWtkoMX
2026-09-13 09:16:24 -06:00
Bryan Helmkamp
5d2b7cc4aa Describe StageProjection.agent on the API as pebble's own types
AgentSessionProjection and the schemas nested in it reuse pebble's types
through with_replacement; the AgentSession prefix marks the projection's
own types where fabro already has a schema of that name, and pebble's
event-level types keep their names. The round-trip test builds a
projection over a scripted stream, validates it against the spec with the
spec as the root document, checks every serialized key is declared, and
validates every enum variant this build knows. The TypeScript client is
regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 08:35:07 -06:00
Bryan Helmkamp
1d1cacbef3
Pin pebble main 6996942 and implement its PortRoutes trait
Pebble now owns the trait an application hands its MCP support to reach a
port inside the environment. SandboxPortRoutes implements PortRoutes over
the run sandbox's preview-URL facet: a missing facet is Unsupported, a
driver failure is Failed with the driver error as its source. The mcp
feature no longer pins sandbox-driver, so fabro's sandbox-driver pin moves
on its own from here.

The new pebble rev also puts the summary call's usage and cost on
CompactionCompleted, which the CLI progress test literal names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 07:03:36 -06:00
fabro-releases[bot]
64845fd7d4 Bump version to 0.355.0-nightly.0 2026-09-13 09:57:21 +00:00
Bryan Helmkamp
697d8622e9
Merge pull request #843 from fabro-sh/remove/run-metadata-branches
Some checks are pending
TypeScript / Build (push) Waiting to run
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 / Sandbox plugins (stdio) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
Remove Git run metadata branches
2026-09-12 16:41:24 -06:00
Bryan Helmkamp
e5d5c534ab
Merge remote-tracking branch 'origin/main' into remove/run-metadata-branches
Resolve conflicts between the metadata-branch removal and the
sandbox-driver adoption on main:

- fabro-sandbox docker.rs, sandbox.rs, daytona/mod.rs: take main's driver
  rewrite. The Sandbox trait is gone, so the PR's push_token_source
  removal now applies to RunSandbox instead; drop that accessor and the
  RepoCredentials::source helper that only served it.
- run_metadata.rs: keep deleted. Main's edits there were adaptations to
  the driver API and the run git identity field.
- lifecycle/git.rs, finalize.rs: keep the PR's removal of metadata
  snapshots and write_finalize_commit; carry main's RunSandbox,
  GitRetryPolicy, git_identity, local_sandbox, and test catalog changes.
- sandbox_git.rs: take main's version and drop the shadow_sha parameter
  and Fabro-Checkpoint trailer.
- git_integration.rs: remove meta_branch from the new git identity test.
- Cargo.toml: main's dependency set with fabro-dump kept as a
  dev-dependency.
- checkpoints.mdx: keep both the git identity paragraph and the durable
  execution state section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 16:28:01 -06:00
Bryan Helmkamp
57a746235e
Pin pebble to main after lithoscomputer/pebble#12
Pebble's RouteFailover event now describes the route that failed and how
the new route carried the prompt on. Record the continuation on fabro's
agent.failover event as an optional string (replay_prompt or
continue_turn); events written before it existed, and one-shot prompt
stages that walk the plan themselves, read as absent. The failed route's
usage, cost, and timing are not mirrored: the stage's totals already
include them through the prompt report, and no fabro run event carries
per-route usage yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 16:26:32 -06:00
Bryan Helmkamp
b2cf0514a4
Pin pebble to main after lithoscomputer/pebble#11
Same tree as the development pin e494e8e; only the three pebble source
lines in the lockfile move.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 12:57:37 -06:00
Bryan Helmkamp
3ac9c213d8
Move the temporary pebble pin to e494e8e
Pebble's mcp-embedder-events branch gained a commit that waits for an
HTTP-placed MCP server to start answering, up to its startup timeout,
before the handshake. No fabro types changed. No fabro test runs a
pebble agent against an HTTP MCP server nothing listens on, and none
pins the old handshake text, so no fixture needs a shorter timeout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 12:48:16 -06:00
Bryan Helmkamp
1aee59585b
Pin pebble to mcp-embedder-events and accept startup_ms on MCP outcomes
Pebble's McpServerReady and McpServerFailed events now carry startup_ms.
The workflow event sink destructured both variants by name, so it stops
listing every field. The pin is temporary: it moves to pebble main once
lithoscomputer/pebble merges the branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 12:29:36 -06:00
Scott Werner
2c81e81e5a
Merge pull request #832 from fabro-sh/codex/run-intent-run-create
Migrate fabro_run_create to the RunIntent API
2026-09-12 13:38:01 -04:00
Bryan Helmkamp
d407c42636
Merge pull request #855 from fabro-sh/pebble-pin-main
Pin pebble to main after lithoscomputer/pebble#10
2026-09-12 11:31:24 -06:00
Scott Werner
b38279fc44 Apply cleanup review fixes to the run-create path
Deduplicate shared-filesystem capability checks and simplify workflow-source dispatch and types. Move Git observation and local package collection onto spawn_blocking, and flush inline workflow files before collection.

Simplify validated source and input types, derive inline size-limit messages from shared constants, add target schema-parity coverage, and remove dead producer pass-through parameters.
2026-09-12 10:57:46 -06:00
Bryan Helmkamp
e51644b8d0
Pin pebble to main after lithoscomputer/pebble#10
Pebble main `222d17f` merges the sandbox-driver re-pin fabro was
pointing at by branch commit. Same tree as `9ec23d0`, so this is a
lockfile-only change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:54:59 -06:00
Scott Werner
d611ef2bb1 Port workflow version creation to Pebble native tools 2026-09-12 10:23:34 -06:00
Scott Werner
ec81170c8a Share workflow packaging results and validation across callers 2026-09-12 10:15:42 -06:00
Scott Werner
709d15f908 Keep supplied workflow source out of packaging failure logs
The packager logged the full packaging error chain at WARN. That chain
embeds caller-supplied workflow and prompt source: the graph parser's
diagnostic includes the unparsed remainder and the TOML parser prints
the offending line. The logging strategy prohibits user file contents
in tracing events at every level, and this adapter runs inside
`fabro mcp` and run workers at the default filter.

Log the collector error's own path-only message at DEBUG, since a
malformed request is an expected input error, together with the
entrypoint and file count. Wrap the blocking-task join error with
`context` instead of interpolating it. A test installs a TRACE-level
subscriber around the blocking path and checks that the fixture's
source marker, which the full chain does contain, never reaches the
log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:15:29 -06:00
Scott Werner
e6eb59537a Move the supplied-content packager next to its collector
ServerWorkflowVersionPackager was a pure adapter over
fabro_manifest::collect_supplied_workflow_versions that touched no
server state, yet it lived in fabro-server and was imported from there
by the standalone MCP server and the CLI run worker. fabro-manifest can
depend on fabro-tool without a cycle, so the adapter now lives beside
the collector as SuppliedWorkflowVersionPackager and fabro-server no
longer exports a non-server module for it.

The adapter also cloned every version's file map out of a closure it
already owned. CollectedWorkflowClosure::into_versions hands the
versions over by value inside the blocking task instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:15:29 -06:00
Scott Werner
bafdd880f5 Simplify workflow version registration tool layering
Move supplied-content packaging into fabro-manifest beside the checkout
collector, and narrow the injected seam to a packager that returns the
dependency-ordered closure so ClientBackend registers versions with the
client it already owns.

Validate the tool input once through a ValidatedWorkflowVersionCreate
newtype, matching the other tools, instead of re-validating at three
layers. Reuse the fabro-types unique-map deserializer and the shared
"not available" error helper, derive budget messages from the limit
constants, and render the tool result through the shared summary+JSON
path used by sibling tools.

Share one extension dispatch between WorkflowLocation::resolve and
from_exact_path, compute the bundler's normalized reference once, key
path-collision checks by a Cow so the canonical exact check no longer
allocates, and log the full packaging error chain before returning the
curated tool message. Replace the hand-rolled axum test server with
httpmock and declare the new unicode dependencies at the workspace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:15:29 -06:00
Scott Werner
d5dec0fffb Add content-based workflow version registration tools 2026-09-12 10:15:19 -06:00
Bryan Helmkamp
a8cc458151
Merge pull request #852 from fabro-sh/pebble-agent-loop
Run the agent loop on pebble's CodingAgent
2026-09-12 10:04:05 -06:00
Bryan Helmkamp
957fc97c5c
Merge origin/main into pebble-agent-loop
Main merged the sandbox-driver adoption (#849) in a later form than this
branch was stacked on: the driver's own exec types replace fabro-sandbox's,
shell quoting moved to fabro-util, the sandbox lifecycle collapsed, and the
driver's events are stored as run events. This branch had deleted
`fabro-agent` and put the coding agent, the environment adapter, and the
steering hub on pebble.

The resolution takes main's sandbox API and re-applies pebble on top: the
`RunSandbox` `Environment` adapter moves to `pebble_environment.rs` (main's
`environment.rs` is the sandbox spec) and runs commands through `ExecSpec`
and `ExecControls`, feeding pebble's output sink from the driver's; the
driver-era `sandbox.*` names leave the known-event list, as on main, so a
stored event with that name and no driver shape is `Unknown` rather than an
error; `program_exit_code` matches pebble's non-exhaustive termination; the
Docker and Daytona smokes use main's constructor and credentials; the
remaining `fabro_agent` paths point at fabro-sandbox.

Pebble's `mcp` feature pins sandbox-driver, and the preview-url trait
objects only cross when both sides name one revision, so pebble moved to
main's `a92c0db6` (lithoscomputer/pebble#10) and fabro pins that pebble
revision until it lands on pebble main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 09:40:00 -06:00
Scott Werner
6872eeb68e
Merge pull request #853 from fabro-sh/codex/local-subprocess-ownership
Consolidate server Git subprocess ownership
2026-09-12 11:20:10 -04:00
fabro-releases[bot]
d6fc85b9a8 Bump version to 0.354.0-nightly.0 2026-09-12 09:30:04 +00:00
Bryan Helmkamp
9e3380b90a
Pin the pebble revision whose projection counts the whole tree's children
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 21:16:49 -06:00
Bryan Helmkamp
66360c2896
Run fabro exec through pebble's command-line session
Pebble's command line is a library now. `fabro exec` builds its agent as
before, with fabro's client, sandbox, MCP servers, skills, search, and
redaction, and hands it to pebble's session: the events rendered as they
happen, the answer on standard output, the summary after it, the agent shut
down for the reason the prompt ended with, and the terminal approval prompt
for tools the permission level does not allow. Fabro's own progress printer,
approval prompt, summary, and MCP report are gone. The event stream of
`--output-format json` stays on standard output. Standard output now carries
the final answer alone rather than every assistant message; `--verbose` no
longer prints tool results, since the session's renderer shows tool failures
only. The lockfile moves tempfile to the version pebble pins, and the SQLite
backup migration uses the replacement for the constructor that version
deprecates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:51:01 -06:00
Bryan Helmkamp
a33c9069cf
Run steering over pebble's bus and keep only fabro's attribution
Pebble's `SteeringBus` now owns the map of live sessions, the buffer for
steers that arrive between sessions, fan-out of steers and interrupts, the
close-the-door detach, and the hold that keeps a paired session open. The
hub keeps what only fabro knows: pair records, principals, stage ids, and
the run events that put bus activity on the run's stream in the order its
consumers expect. `PebbleControlHandle` is gone, since the coding agent's
control handle is a bus session natively; the ACP session joins the bus
through a small adapter and carries pebble's steering message end to end,
so a human steer keeps its author on the ACP `agent.steering.injected`
event. A pair message that evicted an older steer is now accepted and the
eviction recorded, where before it was queued and reported as refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:35:31 -06:00
Bryan Helmkamp
d0f97c25f7
Pin pebble's SessionProjection and prove the stage fold agrees with it
Pebble now folds a session tree's events into a serializable projection
with a per-prompt delta. Fabro's stage projection stays as it is: it is the
wire contract the API serves and is applied to incrementally, so pebble's
value cannot stand in for it without changing that contract. The new
parity tests replay one retained session across two stages through both
folds and pin a stage's live account to the prompt delta pebble reports,
its subagent rows to pebble's, and a stored projection to a replayed one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:13:14 -06:00
Bryan Helmkamp
6592505d0e
Take environment helpers, compaction accounting, and search providers from pebble
Steps 4, 6, and 7 of .ai/plans/pebble-absorbs-embedder-concerns.md,
pinning pebble fc907a1 with its `search-providers` feature.

The sandbox's `Environment` adapter uses pebble's `environment::support`
for the glob grammar check, the tree-order sort of a listing, and the
capture accounting, in place of its own copies; the adapter itself stays.
The stage reads the compactions a prompt performed from the report, as a
breakdown of the usage it already billed. `web_search.rs` keeps the
vault-backed credentials and the Brave-over-Venice preference, and hands
pebble's `Brave` or `Venice` provider a fabro HTTP client; the providers
and their tests are pebble's now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 19:51:12 -06:00
Bryan Helmkamp
d4e483925c
Let pebble discover memory and skills, and name the reuse rules
Steps 5 and 8 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning
pebble 49da137.

Agent stages and `fabro exec` ask pebble for the profile's instruction
files from the repository root down to the working directory
(`MemoryDiscovery::from_git_root`), which fabro lacked: it read the
working directory alone. Skill directories are pebble's to resolve too:
the user's skills directory, then `.fabro/skills` and `skills` under the
repository root. Prompt stages keep reading the working directory alone,
through the same discovery and loader, so `agent_memory.rs` keeps only
that call; the filename table is pebble's now.

A retained thread's export comes from `export_for_reuse`, which closes
the session and hands back an export whose cursor is already past the
close, in place of export, shutdown, and a cursor advance by hand. Ask
Fabro resumes a stored record with `resume_after`, the rule it applied
under the older name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 19:40:36 -06:00
Bryan Helmkamp
64c578d044
Take MCP servers from pebble
Step 3 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning pebble
6cdb30a with its `mcp` feature.

Pebble starts the stage's MCP servers while the agent is built, registers
their tools under `mcp__{server}__{tool}` with `ToolSource::Mcp`, and
closes them with the agent, for all three placements: a child of the run
worker over stdio, a server over HTTP (streamable or SSE), and a server
launched in the run sandbox and reached through the sandbox's preview
URL. `fabro_mcp::pebble::pebble_server` maps `McpServerSettings` onto
pebble's `McpServer`, keeping fabro's `/sse` path for sandbox-hosted SSE
servers; `RunSandbox::port_routes` hands pebble the driver's `PreviewUrls`
facet as the route to a sandbox port. The stage's event sink mirrors
`McpServerReady` and `McpServerFailed` onto the run's `agent.mcp.ready`
and `agent.mcp.failed` events, as it mirrors `RouteFailover` onto
`agent.failover`, and stores no second copy of a mirrored fact.

Deleted: `sandbox_mcp.rs`, the MCP branches of `pebble.rs` and `fabro
exec`, and fabro-mcp's client, connection manager, HTTP helpers, and SSE
transport, whose tests moved to pebble. fabro-mcp keeps the settings
re-export, the mapping, and a stdio client behind `test-support` for the
tests of fabro's own MCP server. `fabro exec` reports each server's
outcome from the agent's snapshot. The Daytona Playwright live test now
drives the sandbox-hosted server through an agent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 19:26:41 -06:00
Bryan Helmkamp
b5fd907c3d
Take files touched and route failover from pebble
Steps 1 and 2 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning
pebble 1a5abe4.

Files touched come from `PromptReport`: pebble computes them from every
successful write, edit, and patch across the prompt, subagents included,
so the event-fed `FileTracking` and the tracking half of
`WorkflowEventSink` go. The stage unions the reports of its prompts.

Route failover is pebble's. The stage resolves its plan from the catalog
as before and hands pebble the remaining routes through
`fallback_routes`, each with its controls and the stage's output limit.
Pebble keeps the conversation, moves it to the next route, requeues
pending steering, and continues the prompt; the stage's plan follows the
route the report says the prompt ended on, re-activates the session
there, and mirrors pebble's `RouteFailover` as the run's `agent.failover`
event with the same payload as before. `prompt_with_failover`,
`resume_agent_on_route`, and the route bookkeeping in `LiveAgent` go.
One-shot prompt stages still walk the plan themselves.

`agent.route.failover` and `agent.tool.rounds.exhausted` join the derived
event names; both variants were falling back to `agent.event`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 18:47:12 -06:00