// Evidence projection for the completion judge.
//
// Split out of std/agent/judge so the adjudication loop and the packet that
// feeds it live in separate files. The judge decides; this module decides what
// the judge is allowed to see. They change for different reasons, and were only
// together because they started together.
//
// The public surface is unchanged. __judge_evidence_projection still lives in
// std/agent/judge and still calls into here, so every existing importer is
// untouched by the move.
import { canon_event_paths } from "std/agent/canon"
import { completion_claim_evidence_refs } from "std/agent/completion_claim"
import {
COMPLETION_MUTATION_TARGET_LIMIT,
CompletionEvidenceAction,
CompletionEvidenceIssue,
CompletionEvidenceProjection,
CompletionEvidenceRole,
CompletionEvidenceSelection,
CompletionReadOnlyEvidence,
CompletionToolSemantics,
completion_evidence_action_authorship,
completion_read_only_evidence,
completion_read_target,
completion_research_summary,
} from "std/agent/completion_evidence"
import { agent_tool_annotations, agent_tool_is_read_only } from "std/agent/tool_annotations"
import {
AgentTranscriptIssue,
AgentTranscriptToolLifecycle,
AgentTranscriptToolLifecycleReport,
} from "std/agent/transcript"
// The completion judge is a terminal checkpoint, not a second agent loop. Give
// it the latest effect-bearing actions and their observations without replaying
// an unbounded session or the full callable schemas it cannot use.
const __JUDGE_EVIDENCE_FIELD_CHAR_LIMIT = 4500
const __JUDGE_EVIDENCE_LABEL_CHAR_LIMIT = 160
// Negative indices are reserved for Harn-owned session summaries. Transcript
// lifecycle indices are non-negative, so the two namespaces cannot alias.
const __JUDGE_MUTATION_SUMMARY_EVIDENCE_INDEX = -3
const __JUDGE_MUTATION_ARTIFACT_SAMPLE_LIMIT = 12
fn __judge_tool_entries(opts: dict) {
const registry = opts?.tools
return if type_of(registry) == "dict" {
registry?.tools ?? []
} else if type_of(registry) == "list" {
registry
} else {
[]
}
}
fn __judge_tool_name(entry: dict) -> string {
return to_string(entry?.name ?? entry?.function?.name ?? "")
}
fn __judge_tool_entry(opts: dict, name: any) {
for entry in __judge_tool_entries(opts) {
if __judge_tool_name(entry) == name {
return entry
}
}
return nil
}
fn __judge_declared_evidence_role(value: string?) -> CompletionEvidenceRole? {
const role = lowercase(to_string(value ?? ""))
if role == "observation" || role == "mutation" || role == "verification" {
return role
}
return nil
}
fn __judge_evidence_role(
declared: CompletionEvidenceRole?,
kind: string,
effect: string,
read_only: bool,
) -> CompletionEvidenceRole {
if declared != nil {
return declared
}
if kind == "edit" || effect == "workspace_write" {
return "mutation"
}
if read_only {
return "observation"
}
return "other"
}
pub fn __judge_tool_descriptor(opts: dict, name: any) -> CompletionToolSemantics {
const entry = __judge_tool_entry(opts, name)
const annotations = agent_tool_annotations(entry, opts?.policy, name)
const read_only = entry != nil && agent_tool_is_read_only(entry, opts?.policy, name)
const kind = lowercase(to_string(annotations?.kind ?? ""))
const effect = lowercase(to_string(annotations?.side_effect_level ?? ""))
const declared_role = __judge_declared_evidence_role(annotations?.completion_evidence_role)
const evidence_role = __judge_evidence_role(declared_role, kind, effect, read_only)
return {
name: __judge_clip_evidence(name, __JUDGE_EVIDENCE_LABEL_CHAR_LIMIT),
kind: __judge_clip_evidence(
annotations?.kind ?? annotations?.tool_kind ?? annotations?.toolKind ?? "",
__JUDGE_EVIDENCE_LABEL_CHAR_LIMIT,
),
side_effect_level: __judge_clip_evidence(
annotations?.side_effect_level ?? annotations?.sideEffectLevel ?? annotations?.side_effect
?? annotations?.sideEffect
?? "",
__JUDGE_EVIDENCE_LABEL_CHAR_LIMIT,
),
read_only: read_only,
evidence_role: evidence_role,
}
}
fn __judge_clip_evidence(value: any, limit: int) -> string {
const text = if type_of(value) == "string" {
value
} else {
json_stringify(value ?? "")
}
if len(text) <= limit {
return text
}
const tail_chars = limit / 3
const head_chars = limit - tail_chars
const omitted = len(text) - limit
return substring(text, 0, head_chars)
+ "\n…["
+ to_string(omitted)
+ " chars elided — middle of evidence]…\n"
+ substring(text, len(text) - tail_chars, len(text))
}
fn __judge_bounded_artifact_id(value: unknown) -> string {
const identity = trim(to_string(value ?? ""))
if identity == "" {
return ""
}
if len(identity) <= __JUDGE_EVIDENCE_LABEL_CHAR_LIMIT {
return identity
}
// Keep a readable prefix and a collision-resistant identity for paths longer
// than the evidence envelope. No changed artifact silently disappears.
return substring(identity, 0, 80) + "…#sha256:" + sha256(identity)
}
fn __judge_push_artifact_ids(items: list<string>, bucket: unknown) -> list<string> {
let out = items
if type_of(bucket) != "list" {
return out
}
for value in bucket {
const identity = trim(to_string(value ?? ""))
if identity != "" && !out.contains(identity) {
out = out + [identity]
}
}
return out
}
fn __judge_mutation_artifact_ids(
lifecycle: AgentTranscriptToolLifecycleReport,
opts: dict,
) -> list<string> {
let artifact_ids: list<string> = []
for call in lifecycle.calls {
const descriptor = __judge_tool_descriptor(opts, call.name)
if !__judge_descriptor_is_mutation(descriptor) || call.status != "completed" {
continue
}
for result in call.results {
// Requirement evidence is consumed independently of `packet.actions`, so
// enforce the same authorship boundary here rather than relying on the
// action projection to have removed injected harness feedback already.
if result.origin == "harness_repair" || result.outcome != "ok" {
continue
}
artifact_ids = __judge_push_artifact_ids(artifact_ids, canon_event_paths(result.data))
}
}
return artifact_ids
}
fn __judge_mutation_requirement_evidence(
lifecycle: AgentTranscriptToolLifecycleReport,
opts: dict,
) -> list<CompletionRequirementEvidenceRecord> {
const artifact_ids = __judge_mutation_artifact_ids(lifecycle, opts).sorted()
if len(artifact_ids) == 0 {
return []
}
const sample_count = if len(artifact_ids) < __JUDGE_MUTATION_ARTIFACT_SAMPLE_LIMIT {
len(artifact_ids)
} else {
__JUDGE_MUTATION_ARTIFACT_SAMPLE_LIMIT
}
let representative_ids: list<string> = []
for identity in artifact_ids.slice(0, sample_count) {
representative_ids = representative_ids + [__judge_bounded_artifact_id(identity)]
}
const record: CompletionRequirementEvidenceRecord = {
evidence_index: __JUDGE_MUTATION_SUMMARY_EVIDENCE_INDEX,
role: "mutation_summary",
supports_completion: true,
summary: "Changed " + to_string(len(artifact_ids)) + " artifact(s) across this session.",
representative_artifact_ids: representative_ids,
artifact_count: len(artifact_ids),
artifact_set_digest: "sha256:" + sha256(json_stringify(artifact_ids)),
measurement: "observed",
}
return [record]
}
fn __judge_descriptor_is_verifier(descriptor: dict) -> bool {
return descriptor?.evidence_role == "verification"
}
fn __judge_descriptor_is_mutation(descriptor: dict) -> bool {
return descriptor?.evidence_role == "mutation"
}
fn __judge_descriptor_is_observation(descriptor: dict) -> bool {
return descriptor?.evidence_role == "observation"
}
fn __judge_lifecycle_action(
lifecycle: AgentTranscriptToolLifecycle,
opts: dict,
) -> CompletionEvidenceAction {
const descriptor = __judge_tool_descriptor(opts, lifecycle.name)
// A `harness_repair` result is the harness's own injected feedback, written
// to close an orphaned `tool_use` block under that call's name and id. It is
// not an observation of anything, and reading it as one lets a completion
// veto cite itself as proof of the gap it asserted (harn#7757).
const dispatched = lifecycle.results.filter({ matched -> matched.origin != "harness_repair" })
.to_list()
const result = if len(dispatched) == 0 {
nil
} else {
dispatched[len(dispatched) - 1]
}
let observations = []
for matched in dispatched {
observations = observations + [matched.text]
}
const action: CompletionEvidenceAction = {
ordinal: lifecycle.ordinal,
evidence_index: lifecycle.evidence_index,
tool_call_id: __judge_clip_evidence(lifecycle.tool_call_id, __JUDGE_EVIDENCE_LABEL_CHAR_LIMIT),
name: descriptor.name,
semantics: descriptor,
lifecycle_status: lifecycle.status,
arguments: __judge_clip_evidence(lifecycle.args, __JUDGE_EVIDENCE_FIELD_CHAR_LIMIT),
observation: __judge_clip_evidence(join(observations, "\n"), __JUDGE_EVIDENCE_FIELD_CHAR_LIMIT),
result_facts: __judge_clip_evidence(result?.data ?? {}, __JUDGE_EVIDENCE_FIELD_CHAR_LIMIT),
has_error_result: dispatched.any({ matched -> matched.outcome == "error" }),
}
return completion_evidence_action_authorship(action, len(lifecycle.results), len(dispatched))
}
fn __judge_latest(
current: CompletionEvidenceAction?,
candidate: CompletionEvidenceAction?,
) -> CompletionEvidenceAction? {
if candidate == nil {
return current
}
if current == nil || candidate.evidence_index >= current.evidence_index {
return candidate
}
return current
}
fn __judge_unique_slot_actions(
candidates: list<CompletionEvidenceAction?>,
) -> list<CompletionEvidenceAction> {
let selected: list<CompletionEvidenceAction> = []
let seen = {}
for candidate in candidates {
if candidate == nil {
continue
}
const key = to_string(candidate.ordinal) + ":" + to_string(candidate.evidence_index)
if seen[key] ?? false {
continue
}
seen = seen + {[key]: true}
selected = selected + [candidate]
}
return selected
}
/**
* The latest mutation for EACH touched path, not the latest mutation overall.
*
* A single `latest_mutation` slot cannot represent a two-file change. The
* second edit displaces the first, so a task that asks for a production change
* plus a test hands the judge exactly one of them and reports the other as
* absent — and the judge, reasoning from the packet, refuses work that is
* present and verified (#8090).
*
* Keyed by the call's own locator, so the packet describes workspace STATE
* ("what does each touched file look like now") rather than narrative ("what
* happened last"). Chronological, because the calls arrive that way and a
* judge reads a sequence of edits more easily than a set.
*
* Bounded by `COMPLETION_MUTATION_TARGET_LIMIT` distinct paths, keeping the
* most recently touched, so a wide refactor degrades to fewer files rather
* than to an unbounded packet. Whatever is dropped is still counted in
* `omitted_older_action_count`, so the judge is never shown a window narrower
* than it believes.
*/
fn __judge_latest_mutation_per_target(
calls: list<AgentTranscriptToolLifecycle>,
opts: dict,
) -> list<CompletionEvidenceAction> {
let latest_by_target = {}
for call in calls {
const action = __judge_lifecycle_action(call, opts)
if (action.harness_authored_only ?? false)
|| action.semantics.read_only
|| !__judge_descriptor_is_mutation(action.semantics) {
continue
}
const target = completion_read_target(call.args)
latest_by_target = latest_by_target
+ {[target]: __judge_latest(latest_by_target[target], action)}
}
// Second pass so the result is chronological without sorting: a call is the
// one to keep exactly when it IS its target's latest.
let ordered: list<CompletionEvidenceAction> = []
for call in calls {
const action = __judge_lifecycle_action(call, opts)
if (action.harness_authored_only ?? false)
|| action.semantics.read_only
|| !__judge_descriptor_is_mutation(action.semantics) {
continue
}
const winner = latest_by_target[completion_read_target(call.args)]
if winner != nil && winner.evidence_index == action.evidence_index {
ordered = ordered + [action]
}
}
if len(ordered) > COMPLETION_MUTATION_TARGET_LIMIT {
return ordered.slice(len(ordered) - COMPLETION_MUTATION_TARGET_LIMIT, len(ordered))
}
return ordered
}
fn __judge_issue_evidence(issue: AgentTranscriptIssue) -> CompletionEvidenceIssue {
return {
code: __judge_clip_evidence(issue.code, __JUDGE_EVIDENCE_LABEL_CHAR_LIMIT),
record_index: issue.record_index,
message: __judge_clip_evidence(issue.message, __JUDGE_EVIDENCE_LABEL_CHAR_LIMIT),
details: __judge_clip_evidence(issue.details, __JUDGE_EVIDENCE_FIELD_CHAR_LIMIT),
}
}
/**
* Build the bounded evidence packet sent to the completion-judge LLM. The raw
* transcript remains on the canonical payload for deterministic gates, replay,
* and audit. Selection is structural: explicit mutation and verification roles
* are retained as actions, and read-only calls are reduced to the bounded
* `research_summary` instead of being dropped. Canonical edit and
* workspace-write annotations imply mutation; execute tools must declare a
* completion evidence role instead of being guessed from provider prose or
* process capability.
*
* @effects: []
* @errors: []
* @api_stability: internal
*/
pub fn __judge_evidence_projection_from_lifecycle(
messages: string | bytes | list | dict | set | range | nil,
lifecycle: AgentTranscriptToolLifecycleReport,
opts: dict,
raw_transcript_digest: any = nil,
) -> CompletionEvidenceProjection {
const claim_refs = completion_claim_evidence_refs(opts?._completion_claim)
const evidence_lifecycle = if len(claim_refs) == 0 {
lifecycle
} else {
lifecycle
+ {
calls: lifecycle.calls.filter({ call -> claim_refs.contains(call.tool_call_id) }).to_list(),
}
}
let relevant_call_count = 0
let read_only_evidence: list<CompletionReadOnlyEvidence> = []
let latest_verification = nil
let latest_problem = nil
let latest_observation = nil
let claimed_actions: list<CompletionEvidenceAction> = []
let omitted_harness_authored_result_count = 0
for call in evidence_lifecycle.calls {
const action = __judge_lifecycle_action(call, opts)
omitted_harness_authored_result_count = omitted_harness_authored_result_count
+ (action.omitted_harness_authored_result_count ?? 0)
if action.harness_authored_only ?? false {
// Every result on this call was written by the harness, so the call was
// never dispatched. Counted rather than silently dropped: a judge that
// sees fewer actions than the run made must be able to tell why.
continue
}
if action.semantics.read_only {
read_only_evidence = read_only_evidence + [completion_read_only_evidence(call, action)]
latest_observation = __judge_latest(latest_observation, action)
continue
}
relevant_call_count = relevant_call_count + 1
if len(claim_refs) > 0 {
claimed_actions = claimed_actions + [action]
}
if __judge_descriptor_is_observation(action.semantics) {
latest_observation = __judge_latest(latest_observation, action)
}
if __judge_descriptor_is_verifier(action.semantics) {
latest_verification = __judge_latest(latest_verification, action)
}
if action.lifecycle_status != "completed" || action.has_error_result {
latest_problem = __judge_latest(latest_problem, action)
}
}
let issue_tail = lifecycle.issues
if len(issue_tail) > 3 {
issue_tail = issue_tail.slice(len(issue_tail) - 3, len(issue_tail))
}
let latest_issues = []
for issue in issue_tail {
latest_issues = latest_issues + [__judge_issue_evidence(issue)]
}
// Every touched path, then the verifier and the latest problem. State first,
// narrative second: the judge is being asked what the workspace looks like
// now, not which call happened last (#8090).
const selected = if len(claim_refs) > 0 {
claimed_actions
} else {
__judge_unique_slot_actions(
__judge_latest_mutation_per_target(evidence_lifecycle.calls, opts)
+ [latest_verification, latest_problem],
)
}
const observation_fallback = if len(selected) == 0 && latest_observation != nil {
[latest_observation]
} else {
[]
}
const raw_digest = raw_transcript_digest
?? ("sha256:" + sha256(json_stringify(messages)))
const research_summary = completion_research_summary(read_only_evidence)
let packet: CompletionEvidencePacket = {
schema: "harn.completion_judge_evidence.v2",
raw_message_count: len(messages),
raw_transcript_digest: raw_digest,
relevant_action_count: relevant_call_count,
omitted_read_only_call_count: research_summary.omitted_count,
omitted_older_action_count: max(relevant_call_count - len(selected), 0),
lifecycle_issue_count: lifecycle.invalid_count,
lifecycle_issues: latest_issues,
actions: selected,
research_summary: research_summary,
fallback_observations: observation_fallback,
requirement_evidence: __judge_mutation_requirement_evidence(evidence_lifecycle, opts),
}
if opts?._completion_claim != nil {
packet = packet + {completion_claim: opts._completion_claim}
}
if omitted_harness_authored_result_count > 0 {
packet = packet
+ {omitted_harness_authored_result_count: omitted_harness_authored_result_count}
}
let selection: list<CompletionEvidenceSelection> = []
for action in selected {
selection = selection
+ [
{
name: action.name,
evidence_role: action.semantics.evidence_role,
evidence_index: action.evidence_index,
},
]
}
const serialized = json_stringify(packet)
return {
packet: packet,
serialized: serialized,
stats: {
schema: "harn.completion_judge_evidence_projection.v1",
raw_message_count: len(messages),
relevant_call_count: relevant_call_count,
included_action_count: len(selected),
included_fallback_observation_count: len(observation_fallback),
included_read_only_call_count: research_summary.included_count,
omitted_read_only_call_count: research_summary.omitted_count,
omitted_older_action_count: max(relevant_call_count - len(selected), 0),
omitted_harness_authored_result_count: omitted_harness_authored_result_count,
lifecycle_issue_count: lifecycle.invalid_count,
serialized_chars: len(serialized),
raw_transcript_digest: packet.raw_transcript_digest,
selected_actions: selection,
},
}
}