-
Notifications
You must be signed in to change notification settings - Fork 579
Option B: typed MessageNode.kind for replay resume points #1896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
a8df793
ab09c16
4f7e820
4a65a3d
21c1545
8a1ab79
ecddd73
e3e6262
7b79984
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| """Replay resume-point annotation + selection (Option B: typed ``MessageNode.kind`` field). | ||
|
|
||
| During generation the harness tags interesting nodes; the replay-buffer selector reads the | ||
| tags back. Tool *identity* (which tool) and "is this a tool call" come from the typed graph, | ||
| so only two things need a tag: branch provenance (compaction/subagent) and tool failure | ||
| status (``failed_tool_call``), which the trace otherwise drops. | ||
|
|
||
| Option B stores the tag on the node itself (``MessageNode.kind``; see ``verifiers/v1/graph.py``). | ||
| The label rides *with* the node — no keying, typed/validated, and visible anywhere traces are | ||
| inspected — at the cost of a change to the core ``MessageNode`` schema. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from verifiers.v1.graph import MessageNode | ||
| from verifiers.v1.trace import Trace | ||
| from verifiers.v1.types import AssistantMessage, ToolMessage | ||
|
|
||
|
|
||
| def branch_start_nodes(trace: Trace) -> list[int]: | ||
| """First node of each forked branch. A node with >1 child is a fork point; ``children[0]`` | ||
| is the original line and ``children[1:]`` are the forks. The compacting harness rewrites | ||
| context every turn, so each fork start is a compaction boundary.""" | ||
| children: dict[int | None, list[int]] = {} | ||
| for nid, node in enumerate(trace.nodes): | ||
| children.setdefault(node.parent, []).append(nid) | ||
| starts: list[int] = [] | ||
| for kids in children.values(): | ||
| if len(kids) > 1: | ||
| starts.extend(kids[1:]) | ||
| return starts | ||
|
|
||
|
|
||
| def tool_nodes(trace: Trace) -> dict[str, int]: | ||
| """``tool_call_id -> node_id`` for every tool result — the key for joining the program's | ||
| per-call failure log back onto the graph.""" | ||
| return { | ||
| node.message.tool_call_id: nid | ||
| for nid, node in enumerate(trace.nodes) | ||
| if isinstance(node.message, ToolMessage) | ||
| } | ||
|
|
||
|
|
||
| def tool_name(trace: Trace, node: MessageNode) -> str | None: | ||
| """The tool/function name for a tool-result node: ``ToolMessage.name`` when the dialect | ||
| recovered it, else the issuing assistant's matching ``tool_calls[].name``.""" | ||
| m = node.message | ||
| if not isinstance(m, ToolMessage): | ||
| return None | ||
| if m.name: | ||
| return m.name | ||
| nid = node.parent | ||
| while nid is not None: | ||
| parent = trace.nodes[nid].message | ||
| if isinstance(parent, AssistantMessage): | ||
| for tc in parent.tool_calls or []: | ||
| if tc.id == m.tool_call_id: | ||
| return tc.name | ||
| break | ||
| nid = trace.nodes[nid].parent | ||
| return None | ||
|
|
||
|
|
||
| def get_tag(trace: Trace, node_id: int) -> str | None: | ||
| """Read a node's replay tag (Option B: from the typed ``MessageNode.kind`` field).""" | ||
| return trace.nodes[node_id].kind | ||
|
|
||
|
|
||
| def resume_points(trace: Trace, *, kinds: set[str]) -> list[dict]: | ||
| """Enumerate replay resume points, filtered to ``kinds``. Tool calls come from the typed | ||
| graph; compaction comes from the node tag. Each point carries its ``node`` id, ``kind``, | ||
| and for tool calls the ``tool`` name and whether it ``failed``.""" | ||
| points: list[dict] = [] | ||
| for nid, node in enumerate(trace.nodes): | ||
| if isinstance(node.message, ToolMessage): | ||
| points.append( | ||
| { | ||
| "node": nid, | ||
| "kind": "tool_call", | ||
| "tool": tool_name(trace, node), | ||
| "failed": get_tag(trace, nid) == "failed_tool_call", | ||
| } | ||
| ) | ||
| elif get_tag(trace, nid) == "compaction": | ||
| points.append({"node": nid, "kind": "compaction"}) | ||
| return [p for p in points if p["kind"] in kinds] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium verifiers/verifiers/v1/graph.py Line 128 in a8df793
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ | |
| import hashlib | ||
| import json | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Any | ||
| from typing import TYPE_CHECKING, Any, Literal | ||
|
|
||
| import numpy as np | ||
| from pydantic import ConfigDict, Field, field_serializer, field_validator | ||
|
|
@@ -60,6 +60,13 @@ def _decode_ndarray(d: dict) -> np.ndarray: | |
| return np.frombuffer(d["data"], dtype=np.dtype(d["dtype"])).reshape(d["shape"]) | ||
|
|
||
|
|
||
| NodeTag = Literal["compaction", "subagent", "failed_tool_call"] | None | ||
| """Replay resume-point tag for a node: branch provenance (compaction/subagent) or tool failure | ||
| status (failed_tool_call) — the two things the typed graph can't otherwise express. Written by | ||
| the harness during generation, read by the replay-buffer resume-point selector. None for an | ||
| ordinary node.""" | ||
|
|
||
|
|
||
| class MessageNode(StrictBaseModel): | ||
| """One message in the graph: a message plus the tokens it adds to the cumulative | ||
| sequence. Concatenating a root→leaf path's nodes reconstructs that branch's full token | ||
|
|
@@ -73,6 +80,12 @@ class MessageNode(StrictBaseModel): | |
| """True iff a model call produced this message (the response passed to `commit`); False for | ||
| every prompt-supplied message — including assistant/tool messages fabricated as context | ||
| the model never generated, which role alone can't tell apart from real turns.""" | ||
| kind: NodeTag = None | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium Because 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| """Replay resume-point tag, written by the harness during generation: branch provenance | ||
| (compaction/subagent) or tool failure status (failed_tool_call); None for an ordinary node. | ||
| Read by the replay-buffer resume-point selector. Tool identity and "is this a tool call" | ||
| come from the typed message, so they need no tag. A plain str/None, so it rides the wire and | ||
| the JSON dump automatically (no `_NODE_DUMP_EXCLUDE` entry needed).""" | ||
| token_ids: list[int] = Field(default_factory=list) | ||
| """This message's delta contribution to the cumulative token sequence: its leading | ||
| template scaffold + its own tokens — for an assistant, the generation-prompt scaffold | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.