Skip to content

feat(provider): add IBM Bob, read from its database - #29

Open
gillesdandrea wants to merge 4 commits into
furkankly:mainfrom
gillesdandrea:bob-provider
Open

gillesdandrea wants to merge 4 commits into
furkankly:mainfrom
gillesdandrea:bob-provider

Conversation

@gillesdandrea

Copy link
Copy Markdown

Adds IBM Bob (the IDE and the bob shell) as a third provider.

Bob is the first agent here that keeps no files: every session is rows of one SQLite database (~/.bob/db/bob.db, WAL mode), a tasks row per session and per child it spawns, a messages row per turn. DISCOVERY.md §7 scoped that class out as "a different kind of feeder reading rows, not a wider SessionFile". This PR takes exactly that path.

Shape

  • The provider stays a line provider. provider/bob/ reads JSON lines, one per row (wire.rs), through the same Stream::push as the others. An export of a session is a file the stream reads like any transcript, the conformance fixture (assets/bob/shell-2.0.3.jsonl, with its two goldens) is such a file, and the browser opens one.
  • The feeder is what differs. A root read as ReadMode::Rows names the database and the task. provider/bob/db.rs (native only) presents the task tree's rows as the provider's lines; tailer/rows.rs polls it in place of the byte tailer. sweep lists stored sessions through one new primitive, stored_sessions, so zoe <id> and zoe <dir> find Bob sessions beside Claude's and Codex's, a followed directory attaches to a Bob task that starts later, and re-attach and auto-switch go by id.
  • zoe export <id|dir> writes a stored session as those lines: the file the browser reads.
  • One dependency, rusqlite with bundled, optional behind the native feature; the portable core does not see it. Bundled rather than the system library so nothing is needed on the machine and the crate builds the same everywhere; opens are read-only, one read transaction per poll.

What the format needed the provider to know

Verified against a real database (586 sessions) and three live probes, all recorded in the module docs:

  • Bob deletes and re-inserts a session's message rows when the task ends or is reopened: rowids reused, every created_at restamped, order changed. So the reader keys messages by id, task rows by their state, and a turn's time is _meta.timestamp inside its blob.
  • A child task keeps no rows of its own. Its row is born at created_at and ended by status at updated_at, presented as two observations so the fold never learns an ending at a birth (§1.2). The spawn's result names the child (_meta.subagentId) and carries its whole transcript; the reader lifts that out as the child's own lines, each at its own time.
  • A /skill launch stores the skill's text as the user turn and what was typed in _meta.mask; a tool result the harness marks hide is an error kept from the user, and still answers its call.

Checks

CI on the fork is green on every job, Windows and Linux included: gillesdandrea#1. Beyond cargo fmt, clippy, test, the wasm frontend lint and the MSRV build, the stream is folded under forty interleavings of the fixture's task rows with its turns, and harness::snapshot now compares tokens, labels and agent types.

Not in this PR

The web site copy, a demo recording, and the Herdr plugin (Bob runs inside its IDE, not a pane). Two soft spots the format exposed are recorded in ARCHITECTURE.md §7 rather than fixed: an ending cannot be retracted when Bob reopens a task, and a tool end read before its start stays pending.

This touches the same seams as #25 and #26 (Provider arms, the spawn mark, usage text); happy to rebase over whichever lands first.

IBM Bob keeps every session as rows of one SQLite database rather than
files: a `tasks` row per session and per child it spawns, a `messages`
row per turn with the turn in a JSON blob. This provider reads those rows
as JSON lines, one per row (`wire.rs`), so a session exported that way is
a file the stream reads like any other transcript, the conformance
fixture (`assets/bob/shell-2.0.3.jsonl`, a probe run on Bob Shell 2.0.3,
harness text elided) is such a file, and the browser opens one.

What the format needs the provider to know: a child's row is born at
`created_at` and ended by its `status` at `updated_at`, presented as two
observations (`seen`) so the fold never learns an ending at a birth; the
join from a spawning call to its child is the child's id on the call's
result (`_meta.subagentId`), with the child's `title`, which is the call's
prompt, as the fallback for a result that lacks it, and an errored spawn
joins nothing; a spawn's result carries the child's whole transcript,
stated as the child's turns whose tokens the child's row already totals;
a turn's time is `_meta.timestamp` inside its blob, never the row's
`created_at`, which Bob restamps when it rewrites a session's rows; a
`/skill` launch stores the skill's text as content and what was typed in
`_meta.mask`; a tool result the harness marks `hide` is an error it keeps
from the user, and still answers its call.

The stream is checked against forty interleavings of the fixture's task
rows with its turns, and the harness's snapshot now compares tokens,
labels and agent types, not only structure.
The row feeder DISCOVERY.md §7 predicted for an agent that keeps a
database. A root read as `ReadMode::Rows` names the database and the
task; `provider/bob/db.rs` (native, rusqlite bundled, read-only opens)
presents the task tree's rows as the provider's lines, and
`tailer/rows.rs` polls it in place of the byte tailer. The reader keys
messages by id and task rows by their state, since Bob deletes and
re-inserts a session's rows when the task ends or is reopened (rowids
reused, `created_at` restamped, order changed); it reads each poll in
one transaction, detects change cheaply before fetching, walks the task
tree to any depth, and lifts the transcript a spawn's result carries out
as the child's own lines, each at its own time, so a replay shows a
child working when it worked.

`sweep` lists stored sessions through a new `stored_sessions` primitive,
so `zoe <id>` and `zoe <dir>` find Bob sessions beside the others; a
followed directory attaches to a Bob task that starts later, and
re-attach and auto-switch go by id, since every stored session shares
the database's path. `inspect` folds a stored session the same way. The
portable core does not see rusqlite.
`zoe export <id|dir>` writes a Bob session's rows as the lines its
provider reads, to stdout: the file the browser opens, and the shape
the fixture has. A directory means its newest stored session. A session
kept as files needs no export and is refused with its files listed.
README lists Bob among the supported agents and the export command;
DISCOVERY.md moves the database class of §7 from out of scope to
supported, with Bob's answers in the primitives table and the
`stored_sessions` primitive; ARCHITECTURE.md §0 covers the row feeder
and §7 records two soft spots it exposed (an ending cannot be retracted
when Bob reopens a task; a tool end read before its start stays
pending); DESIGN.md's module map and CLI cover the new files and the
subcommand.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant