14 min read

How We Built Charlie, Part 7: Task Graph vs Transcript Ledger

How separate Task graphs and transcript ledgers preserve both coordination history and the exact model-visible record.

A frontal white geometric building set against a deep blue field with dark rocks and vivid pink flowering plants

How We Built Charlie continues from Chapter 6: the Task graph coordinates work while the transcript ledger records the ordered items visible to the model.

A lead engineer opens a pull request Charlie created and asks a direct question: “Why is this compatibility branch in the parser?”

The answer is spread across more than a chat log. The root Task began with a regression report. Its initial context included repository guidance and a failing test. One model turn inspected the parser and dispatched a focused test. A delegated child Task checked behavior in an older integration. The child returned evidence that the compatibility branch was still required. The parent incorporated that result, revised the patch, and ran verification before opening the PR.

To explain the change, Charlie needs two records.

The Task graph answers who owned each piece of work, how delegation connected it, and where each Task ended. The transcript ledger is the append-only, ordered record of what one run showed the model, what the model returned, which tools were called, and which results came back.

Those records overlap and answer different questions. Using either as the whole history creates blind spots.

Side-by-side diagram showing a root and child Task coordination graph beside an ordered transcript ledger of context, model items, tool calls, and results

The graph explains ownership across Tasks. The ledger explains the ordered model-visible history inside a run.

The Task graph coordinates work

A Task is Charlie’s durable owner for an engineering objective. Each Task has its own identity and lifecycle status. A root Task has no parent. Delegation creates a child with a parentId pointing to the caller and a rootId connecting it to the larger tree.

The graph is small by design. It carries coordination facts, not every sentence or tool payload involved in the work.

type TaskStatus =
  | 'scheduled'
  | 'queued'
  | 'running'
  | 'succeeded'
  | 'failed'
  | 'canceled'
  | 'timed_out';

type TaskNode = {
  id: string;
  rootId: string;
  parentId?: string;
  status: TaskStatus;
  result?: unknown;
};

This conceptual contract supports questions such as:

  • Which Task owns the original request?
  • Which child investigated the older integration?
  • Is the parent still waiting, or has it finished?
  • Did the child fail while the parent later succeeded with a fallback?
  • Which terminal result belongs to which Task?

Status belongs to one Task. A child can succeed while its parent remains running. A child can fail and return useful evidence that lets the parent choose another path. A canceled root can have descendants that are still observing the stop at their next scheduler boundary. Summarizing the entire tree with one status would hide those distinctions.

The rootId makes tree-wide queries practical, while parentId preserves the immediate relationship. Delegation events add another useful edge: they connect the parent’s tool call to the child Task it created. That link lets an inspection UI move from “the parent called a worker” to the exact child transcript.

Graph factQuestion it answers
Task identityWhich durable owner are we discussing?
Root identityWhich larger objective does this work belong to?
Parent identityWhich Task delegated this child?
Lifecycle statusWhat is true for this Task now?
Terminal resultWhat did this Task return to its caller or origin?
Delegation edgeWhich tool call created or waited on this child?

The graph remains useful even when transcript content is compacted, filtered, or unavailable to a viewer. Coordination still needs a stable answer about ownership and lifecycle.

The transcript ledger records model-visible chronology

The transcript is an append-oriented sequence of normalized items for one run. It records model messages, reasoning items, tool calls, tool results, and compaction boundaries together with ordering metadata.

Each row has a stable sequence position and belongs to a turn. Tool rows carry a call identity that connects request and result. Message and context items have their own item identity. The exact schema is more detailed, but the conceptual shape is straightforward:

type TranscriptRow = {
  sequence: number;
  turnIndex: number;
  itemId: string;
  type: 'message' | 'reasoning' | 'tool-call' | 'tool-result' | 'compaction';
  toolCallId?: string;
  content: unknown;
};

The sequence number establishes durable chronology. The turn index groups items that belong to one model interaction. The item ID identifies the row itself. The tool-call ID relates two different rows that describe one tool lifecycle.

Keeping those identities separate avoids several ambiguities. A tool call and its result are not the same event. Two calls to the same tool are not the same call. Two rows in the same turn do not have the same position. A mailbox message incorporated into the run retains a link to its source message rather than becoming anonymous prompt text.

The ledger answers questions the Task graph cannot:

  • What context did the model receive before proposing the compatibility branch?
  • Which file did Charlie inspect first?
  • What arguments were sent to the test command?
  • Did the worker result arrive before the next model turn?
  • Which tool failure caused the model to revise its plan?

The transcript is model-visible history, not a complete record of every event in the distributed system. Scheduler claims, cache refreshes, provider retries, and UI reads may matter operationally without becoming model input. Keeping the ledger scoped to execution makes its ordering meaningful.

One turn produces several rows

Agent interfaces often render a turn as one assistant bubble followed by a few tool cards. The durable record is more precise.

The model can emit reasoning, an assistant message, and several tool calls in one response. Each tool then settles separately. The executor records those outcomes and inserts the result rows in stable tool-call order before starting another model turn.

Ledger diagram showing one turn as separate reasoning, message, tool-call, pending-result, and tool-result rows connected by turn, item, sequence, and tool-call identities

One displayed turn is assembled from several durable rows. Sequence preserves chronology; call identity joins each tool request to its outcome.

Suppose one turn asks for three actions:

seq 40  turn 6  reasoning
seq 41  turn 6  assistant message
seq 42  turn 6  tool call: read parser
seq 43  turn 6  tool call: run focused test
seq 44  turn 6  tool call: delegate compatibility check

The read may finish first, the test second, and the child last. Pending-tool state tracks that operational completion. Once the sibling set is terminal, transcript result rows are materialized in the corresponding call order:

seq 45  turn 6  result for read parser
seq 46  turn 6  result for focused test
seq 47  turn 6  result for delegated check

The next model call sees a stable sequence regardless of network timing. An exact raw reader still sees every persisted row participating in pagination. A semantic reader can join each tool call with its result and present one understandable tool lifecycle.

Both views are valid because they answer different questions.

Raw chronology and semantic projection

Charlie exposes two transcript read models.

The exact view returns persisted items in chronological sequence. Tool calls and tool results are separate rows. Every item counts toward pagination. This view is useful when investigating ordering, incomplete writes, or a parser that may be dropping an item.

The semantic view is call-primary. Messages, reasoning, and tool calls define the main rows. A matching tool result hydrates its call when that result is available in the relevant page. This is closer to how an engineer reads execution: one tool card shows the request, outcome, duration, and links.

Read modelRepresentation and pagination
Exact chronologyStorage-oriented: call and result are separate rows, and every persisted item advances the cursor.
Semantic projectionInspection-oriented: a result joins its call, and primary semantic rows define the page.

The semantic projection must not rewrite chronology. It can merge related rows, extract previews, and hide raw payload detail by default, but it needs stable source identities and ordering underneath. Otherwise a page boundary could duplicate a tool, omit a result, or reorder turns between requests.

Stable ordering matters most while a run is active. New rows can append while an engineer is inspecting the transcript. Sequence-based pagination gives the reader a durable position. Sorting by display text, completion time, or client arrival would produce moving pages and confusing duplicates.

Semantic projections also reduce unnecessary exposure. Exact transcript items can contain long model content, file payloads, command output, and details that should not become the default debugging surface. The normalized UI shows structured previews and opens deeper detail deliberately. It is a debugging product, not a raw payload dump.

Context and mailbox input enter the ledger at boundaries

The model’s input includes more than earlier assistant messages. A run can begin with task content, fetched repository or platform context, system guidance, and source artifacts. While it is running, follow-up messages can arrive through the Task mailbox.

These inputs have different freshness and ownership.

Initial context is assembled for the run. It may include a snapshot of repository guidance, issue details, or prior durable artifacts. A mailbox message is new input delivered after the run began. The executor incorporates mailbox content at a safe boundary instead of mutating the prompt while a model call or tool batch is in flight.

The transcript preserves the resulting model-visible item and, where available, its source identity. That connection matters when an engineer asks whether the model knew about a correction before making a change.

Consider the compatibility branch again:

  1. The root Task starts with a failing test and repository context.
  2. The model proposes a parser change.
  3. A lead sends a follow-up: the legacy integration must remain supported.
  4. The mailbox stores the message against the open Task.
  5. At the next safe runtime boundary, the new input enters the transcript.
  6. The model delegates the compatibility check and revises the patch after the result.

The Task graph shows that the same root owner continued. The ledger shows when the correction became visible to the model. One without the other cannot answer the full question.

Delegation creates an edge, not shared consciousness

When a parent delegates work, Charlie creates a child Task with its own lifecycle and run. The worker receives a focused assignment and selected context. It does not automatically inherit the parent’s whole live conversation, private scratch state, or every tool result.

That boundary improves both execution and explanation. The child can work from a concise contract: investigate one compatibility question, inspect named paths, and return evidence. Its transcript records how it reached that result. The parent’s transcript records the delegation call and the returned child outcome.

Delegation diagram showing a parent tool call creating a child Task with a focused request, the child producing its own transcript and result, and the result returning to the parent ledger

Delegation links two Tasks and two transcripts through an explicit request and result. The child is related work, not an extension of the parent’s hidden memory.

Three identities connect the handoff:

IdentityRole in delegation
Parent tool-call IDLocates the request in the parent transcript
Child Task IDLocates the delegated owner and its lifecycle
Child terminal resultReturns a bounded handoff to the parent

The task-tree edge is durable even if the parent process restarts. The transcript link is durable even if the UI later changes how it renders a delegation card. The child result can be concise without erasing the child’s own inspection history.

This also explains why a shared devbox, when used, is an optimization rather than the delegation contract. Repository state may help a child start faster, but the Task and transcript identities must remain correct even when the worker starts in a fresh environment.

Why the UI shows both

An engineer debugging Charlie typically moves between two scales.

At the coordination scale, they need the root-task tree: the current Task, its parent, its children, each status, and links to related work. At the execution scale, they need the transcript for one Task: turns, messages, reasoning, tools, results, mailbox input, and run boundaries.

Charlie’s transcript UI keeps the transcript as the main reading surface and the Task tree as compact navigation and context. Selecting a child moves to that child’s transcript. Selecting a tool opens structured details. A delegated tool can link directly to the child it created. The interface does not flatten the tree into transcript rows or concatenate every descendant transcript into one feed.

That choice avoids several product failures:

  • False chronology: Parent and child transcripts have independent sequences. Interleaving them by timestamp would imply one model observed events from the other in real time.
  • False status: A successful child does not make the parent successful, and a failed child does not necessarily make the root fail.
  • Unbounded payloads: Expanding every descendant’s raw content would make one transcript difficult to page, search, and review.
  • Lost causality: Showing only a tree would reveal that delegation happened but not which model decision or tool call caused it.

The UI can still provide a joined explanation. A tool row shows its request and result. A delegation link opens the child. The tree shows where the child sits. The transcript shows what the parent did with the handoff afterward.

Reconstructing the parser change

Return to the lead engineer’s question about the pull request.

The root Task establishes that one durable objective owned the regression fix. Its transcript shows the original request, context, parser inspection, and failing focused test. A delegation tool row links to the child Task. The child transcript shows the compatibility investigation and the evidence it found. The child result returns to the parent ledger. A later parent turn explains why the branch is required, applies the change, and runs verification. The root terminal result links the completed work back to the team.

No single giant log is required. The explanation is assembled from stable records with explicit boundaries:

root Task
  -> parent transcript:
     request, context, test failure
  -> delegation edge:
     compatibility investigation
      -> child Task
      -> child transcript:
         inspection and evidence
      -> child result
  -> parent transcript:
     revision and verification
  -> root result:
     PR and test evidence

This model also supports honest gaps. A tool row can remain incomplete while work is running. A child can be unavailable to a viewer who lacks access. Transcript content can be compacted or deliberately withheld from a default payload. The graph still describes ownership; the ledger still preserves the ordered items it is allowed to expose. The UI should show an incomplete or restricted state rather than inventing continuity.

Two records, one explainable system

The Task graph and transcript ledger are both histories of Charlie’s work, but they preserve different truths.

The graph is the coordination record: Task identity, root and parent lineage, lifecycle, delegation, and result. The ledger is the execution record: model-visible context, turns, messages, reasoning, tool calls, and tool results in stable order.

Keeping them separate lets Charlie delegate without pretending that workers share one mind. It lets the executor paginate exact chronology while the product presents semantic tool lifecycles. It lets a lead engineer trace a line in a pull request through the request, the context, the tool evidence, and the child handoff that justified it.

The result is not merely more logging. It is a system whose ownership and reasoning path can be inspected without conflating them.

Continue the series

Previous: How We Built Charlie, Part 6: The Executor Loop. Next: How We Built Charlie, Part 8: Delegation Without Swarms. Browse the full How We Built Charlie series.