hackagent.attacks.techniques.trace
The record of what an attack did, as a tree of nodes.
A search is not a list of calls. It is a tree: BoN fans a step out into
candidates, PAIR runs independent streams, TAP expands each surviving node
into children and prunes the rest. Recording that as a flat log loses the
structure, and loses it badly as soon as anything runs concurrently —
asyncio finishes tasks in whatever order it likes, so arrival order is
not position.
So every node carries its own :attr:~TraceNode.path: (2, 0) is the
first child of the third node. Position is recorded, never inferred.
There are four kinds of node:
phase: a step of the algorithm (an iteration, a stream, a branch). Opening one with :func:phasenests everything inside it.call: one model call, in a shape that is the same for the target, a role, and the panel, so a run's cost can be read without knowing which attack produced it.decision: why the search went the way it did — stopped, pruned, backtracked, skipped. These are the algorithm, and they are also the nodes that produce no attempt and therefore have nothing else to hang on.artifact: something the attack wrote down that is not a call, such as RAG's poisoned documents. Large ones are referenced by URI, not inlined.
The campaign runner opens a :func:recording per goal and records every
call itself, so attacks mark only their own structure: a :func:phase per
iteration and a :func:decision when the search turns.
Scope
Where a node belongs. goal is one goal's search; run is work a
technique does once for the whole run (AutoDAN's strategy library,
AdvPrefix's pipeline), which belongs to no single goal.
TraceNode Objects
@dataclass(frozen=True)
class TraceNode()
One node of a search, at a known position in it.
data
Node-specific payload: the call, the decision, or the artifact.
under
def under(path: Path) -> bool
Whether this node sits at or below path.
Trace Objects
class Trace()
The nodes of one recording, each with its position.
add
def add(node: str, *, label: str = "", **fields: Any) -> TraceNode
Append a node as the next child of the current path.
recording
@contextmanager
def recording(scope: Scope = "goal") -> Iterator[Trace]
Collect the nodes recorded in this context.
phase
@contextmanager
def phase(label: str, **data: Any) -> Iterator[Path]
Open a step of the algorithm; nodes inside it nest under it.
Yields the phase's path, which is what a :class:~.iterative.Finding
carries so its attempt can be matched to the work that produced it.
decision
def decision(outcome: str, reason: str = "", **data: Any) -> None
Record why the search turned: stopped, pruned, skipped…
artifact
def artifact(kind: str, uri: str = "", **data: Any) -> None
Record something the attack produced that is not a call.
current_path
def current_path() -> Path
Where nodes are being recorded right now.
record_call
def record_call(role: str,
request: Any,
response: Any,
latency_s: float,
*,
error: Optional[str] = None,
usage: Optional[Mapping[str, Any]] = None) -> None
Record one model call. Every caller uses this same shape.
record_target_call
def record_target_call(messages: Messages, response: CompletionResult,
latency_s: float) -> None
Record a call to the model under test.
record_judge_call
def record_judge_call(sample: Sample, verdict: Verdict,
latency_s: float) -> None
Record one panel verdict.
traced_completion
def traced_completion(name: str, completion: Completion) -> Completion
completion, recording each call as a call node.