fix(convo): attribute harness-generated turns to the harness actor - #165
Open
akesling wants to merge 1 commit into
Open
fix(convo): attribute harness-generated turns to the harness actor#165akesling wants to merge 1 commit into
akesling wants to merge 1 commit into
Conversation
akesling
force-pushed
the
akesling/harness-generated-turns
branch
from
August 7, 2026 15:35
f183e27 to
ee0d485
Compare
|
🔍 Preview deployed: https://8f7e1587.toolpath.pages.dev |
akesling
force-pushed
the
akesling/harness-generated-turns
branch
from
August 7, 2026 15:55
ee0d485 to
50083bf
Compare
`step.actor` is a grammar — `human:alex`, `agent:gpt-5.5`, `tool:rustfmt`
— and nothing owned it. `toolpath-convo` built the string with `format!`
and read it back with a hand-rolled parser; other crates re-derived
pieces of it with `starts_with`. `toolpath`, which defines `step.actor`
and `meta.actors`, had no actor type at all.
Add `toolpath::v1::Actor`: `Human { id }`, `Agent { id }`, `Tool { id }`,
one variant per prefix rather than per semantic label — `tool:` is the
general machine prefix, so a harness is one kind of tool actor. `Display`
renders the document form and `FromStr` reads it, including the
`human:user` / `agent:unknown` placeholders and the `/`-delimited
sub-actor suffix; serde uses the same grammar, so an `Actor` on the wire
is the actor string. `Tool.id` is required because the grammar defines no
placeholder for an unnamed one, which is what makes `Display` total.
`Turn.author` is that type and replaces `Turn.model`. `role` keeps its
own meaning — where the turn sits in the conversation — and attribution
no longer consults it: `derive_path` renders the author, and
`extract_conversation` parses the actor back, so derive → extract →
derive is stable and the grammar has exactly one implementation.
That fixes a misattribution. Attribution used to be assembled from role
plus model name, which broke for messages a harness writes itself — API
errors, rate-limit notices, timeouts — occupying the assistant slot with
no model call behind them. Where a harness records a placeholder in place
of the model, `derive_path` took it at face value and produced an
`agent:` actor naming a string that is not a model. Those turns now take
`tool:<provider>`. `agent:unknown` still means "a model ran, unnamed",
distinct from "no model was involved".
Because a tool actor must be named, each provider supplies its own
provider id when it builds a harness-authored turn. `toolpath-claude`
maps its placeholder model to that actor and writes the placeholder back
when projecting, so a session survives the round trip; the placeholder
string stays owned by the crate that reads that format.
Turns reach disk nested in a step's `delegations` payload, so `author`
also accepts the bare model name older documents carry in its place — the
two are told apart by parsing, since a model name is not a valid actor
reference.
akesling
force-pushed
the
akesling/harness-generated-turns
branch
from
August 7, 2026 18:22
50083bf to
f8bc127
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A deriver bug fix. Independent of the kind work in #164 / #132 — no schema change, no kind version involved.
The bug
A harness sometimes emits an assistant message itself: an API error, a rate-limit notice, a timeout. No model call happens; the accounting is all zeros. Claude Code records a placeholder in
message.modelfor these, and the deriver took that placeholder at face value, producingagent:<placeholder>actors and matchingmeta.actorsentries.The actor convention never meant that. The agent-coding-session actor table has always defined
agent:<model>as "a model reply, named by the recorded model" — a harness notice is not a model reply, and a placeholder is not a recorded model. So this is the deriver contradicting the spec that was already written, not a change to it.These are not rare: sampling real Claude Code sessions, 14 of 40 contained at least one.
The fix
Turngains a provider-agnosticharness_generatedflag (serde-defaulted, so the wire format is unchanged when false). The shared deriver attributes such turns totool:<provider>— the same harness actor that system and other roles already take — while the turn'srolestaysassistant, so the message keeps its place in the transcript.The placeholder spelling stays where the format knowledge belongs:
toolpath-claudeowns the constant and maps it to the flag withmodel: None, so the string never enters the IR. Any other provider can set the flag from whatever signal its own format offers without touching shared code. Repo-wide, the placeholder is now spelled in exactly one file.agent:unknownis unaffected — "no model recorded" and "no model involved" stay distinct outcomes.Testing
Full workspace suite, clippy, rustdoc, and the examples gate green. Coverage: a harness-generated turn takes the harness actor with the role preserved and a
meta.actorsentry carrying provider but no model; a flagged turn's stray model is ignored; an ordinary assistant turn is untouched; a turn with no recorded model still derives asagent:unknown; and the flag round-trips on the wire in both directions.Versioning follows the deriver-behavior-fix precedent (
Derive: resolve duplicate step ids, 2026-07-01):toolpath-convotakes a minor bump because a new public field onTurnbreaks struct-literal construction downstream;toolpath-claudetakes a patch, its public API being unchanged. No CLI bump — no kind URI changes here.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.