Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions loopx/capabilities/explore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ identify the current retained candidate, establish causality, satisfy acceptance
or authorize execution. Actual adoption and outcome benefit require subsequent
work and evidence; automatic branch rankings remain unchanged.

When an enabled work Yardstick has two different measured candidates on its
declared goal profile, from findings on the same node and Goal/Agent/Todo
binding, Explore context names the observed better-scoring candidate as an
available comparison base and links the scoped evidence. A
tied goal outcome calls for checking headroom. The detail read shows a proxy
ordering only when its exact profile compares the same two candidates, with
separate result references and metric directions for each profile. Weigh
continuation, a distinct mechanism or a compatible combination by expected
end-to-end gain and full implementation and validation cost. Use a bounded
check that can change the choice while honoring the accepted constraints and
budget. The compact cue keeps the recorded next-action reason and adds a
separate comparison cue. The detail read retains the complete work decision.
The comparison is advisory and carries no retention or execution obligation.

Manually captured measured findings also appear in the existing result pages
and, when linked to eligible work, its exact-source comparison cards. They need
no `writeback-result` origin tag. Read the page before optionally binding a
Expand Down
14 changes: 9 additions & 5 deletions loopx/control_plane/capabilities/explore_turn_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,15 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject {
target: {summary: "", scope: "", acceptance_ref: ""},
authoritative: {metric: "", unit: "", higher_is_better: true, comparison_scope: "", input_revision: "", evaluation_ref: ""},
proxy: null, counterevidence_refs: [], next_action: {method_id: "", reason: "", cost_bound: ""}, prior_finding_id: null}},
...(workCues.length ? {work_yardsticks: workCues.slice(0, 3).map(cue => resultNode !== null ? cue : {
source_finding_id: cue.source_finding_id, node_id: cue.node_id,
todo_id: (cue.binding as JsonObject).todo_id, target: (cue.target as JsonObject).summary,
cue: (cue.next_action as JsonObject).reason, counterevidence_refs: cue.counterevidence_refs,
details_command: resultRead("--result-node", String(cue.node_id)),
...(workCues.length ? {work_yardsticks: workCues.slice(0, 3).map(cue => {
if (resultNode !== null) return cue;
const comparison = cue.search_comparison as JsonObject | undefined;
return {source_finding_id: cue.source_finding_id, node_id: cue.node_id,
todo_id: (cue.binding as JsonObject).todo_id, target: (cue.target as JsonObject).summary,
cue: (cue.next_action as JsonObject).reason, counterevidence_refs: cue.counterevidence_refs,
...(comparison ? {comparison_cue: comparison.guidance,
comparison_candidate_ref: (comparison.goal as JsonObject).preferred_candidate_ref} : {}),
details_command: resultRead("--result-node", String(cue.node_id))};
}), omitted_work_yardsticks: Math.max(0, workCues.length - 3),
work_yardstick_command: command("turn-context", "--agent-id", agent, "--todo-id", "<todo-id>")} : {}),
} : {}),
Expand Down
73 changes: 61 additions & 12 deletions loopx/control_plane/work_items/yardstick_decision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {parseTodoTimestampMicros} from "../runtime_timestamp.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {requireJsonObject, type JsonObject} from "../runtime_decode.ts";
import {isExploreEvidenceRef} from "../capabilities/explore_evidence_ref.ts";
import type {ExploreMeasurementFeedback, ExploreMeasurementPoint} from "../capabilities/explore_measurements.ts";

export const WORK_YARDSTICK_DECISION_SCHEMA = "work_yardstick_decision_v0";
export type WorkYardstickProfile = {metric: string; unit: string; higher_is_better: boolean;
Expand Down Expand Up @@ -77,12 +78,15 @@ export function normalizeWorkYardstickCapture(params: JsonObject): WorkYardstick
}
return {decision: normalizeWorkYardstickDecision(capture.decision, params.evidence_refs), binding: normalized as WorkYardstickCapture["binding"]};
}
function belongsToWork(row: JsonObject, binding: JsonObject): boolean {
const capture = row.work_yardstick as WorkYardstickCapture | undefined;
const source = capture?.binding;
return !!source && source.goal_id === binding.goal_id && source.agent_id === binding.agent_id
&& source.todo_id === binding.todo_id;
}

function currentFinding(findings: JsonObject[], binding: JsonObject): JsonObject | undefined {
return findings.filter(row => {
const capture = row.work_yardstick as JsonObject | undefined;
const source = capture?.binding as JsonObject | undefined;
return source && ["goal_id", "agent_id", "todo_id"].every(key => source[key] === binding[key]);
}).sort((a, b) => {
return findings.filter(row => belongsToWork(row, binding)).sort((a, b) => {
// Late delivery reuses the original committed time, not append order.
const left = parseTodoTimestampMicros(String(a.last_updated_at ?? ""));
const right = parseTodoTimestampMicros(String(b.last_updated_at ?? ""));
Expand All @@ -107,15 +111,56 @@ export function validateWorkYardstickContinuation(params: JsonObject): JsonObjec
}
return {decision};
}

function measuredPair(row: ExploreMeasurementFeedback | undefined) {
const previous = row?.previous_best, latest = row?.latest;
if (!previous || !latest || !Number.isFinite(previous.value)) return null;
const {value, candidate_ref, evidence_ref} = latest.measurement;
if (value === null || !Number.isFinite(value) || candidate_ref === previous.candidate_ref) return null;
const recent: ExploreMeasurementPoint = {finding_id: latest.finding_id, candidate_ref, evidence_ref, value};
const preferred = value === previous.value ? null : (value > previous.value) === row.higher_is_better
? candidate_ref : previous.candidate_ref;
return {previous_best: previous, latest: recent, preferred_candidate_ref: preferred};
}

function matchesProfile(row: ExploreMeasurementFeedback, profile: WorkYardstickProfile): boolean {
return Object.entries(profile).every(([key, value]) => row[key as keyof WorkYardstickProfile] === value);
}

function measuredSearchComparison(feedback: ExploreMeasurementFeedback[], decision: WorkYardstickDecision,
scopedFindings: Set<unknown>) {
const pairFor = (profile: WorkYardstickProfile) => {
const pair = measuredPair(feedback.find(row => matchesProfile(row, profile)));
return pair && scopedFindings.has(pair.previous_best.finding_id) && scopedFindings.has(pair.latest.finding_id) ? pair : null;
};
const goal = pairFor(decision.authoritative);
if (!goal) return null;
const screen = decision.proxy && pairFor(decision.proxy);
// Both comparisons have two distinct candidates. Order is append/declaration
// order, so join the unordered pair instead of assuming execution chronology.
const goalCandidates = [goal.previous_best.candidate_ref, goal.latest.candidate_ref];
const proxy = screen && goalCandidates.includes(screen.previous_best.candidate_ref)
&& goalCandidates.includes(screen.latest.candidate_ref) ? screen : null;
const reversed = !!(goal.preferred_candidate_ref && proxy?.preferred_candidate_ref
&& goal.preferred_candidate_ref !== proxy.preferred_candidate_ref);
const opening = goal.preferred_candidate_ref === null
? "Observed goal results tie under this scope. Check remaining headroom before another evaluation. "
: (reversed ? "The proxy reverses the observed goal order. " : "") +
"Use the better-scoring candidate under this scope as a comparison base when available. ";
return {goal, ...(proxy ? {proxy, proxy_order_reversed: reversed} : {}),
guidance: opening + "Weigh continuing, a different mechanism or a compatible combination by expected end-to-end gain and full implementation and validation cost. Prefer a bounded check that can change the choice."};
}

export function projectWorkYardstickCue(params: JsonObject): JsonObject | null {
if (params.enabled !== true || !params.todo_id) return null;
const source = currentFinding((params.findings ?? []) as JsonObject[], params);
const findings = (params.findings ?? []) as JsonObject[];
const source = currentFinding(findings, params);
if (!source) return null;
const capture = normalizeWorkYardstickCapture({capture: source.work_yardstick, evidence_refs: source.evidence_refs,
goal_id: params.goal_id, agent_id: params.agent_id});
const decision = capture.decision as JsonObject;
const counter = decision.counterevidence_refs as string[];
const previous = ((params.findings ?? []) as JsonObject[]).find(row => row.finding_id === decision.prior_finding_id);
const decision = capture.decision;
const counter = decision.counterevidence_refs;
const previous = findings.find(row => row.finding_id === decision.prior_finding_id);
const priorCapture = previous?.work_yardstick as JsonObject | undefined;
const priorBinding = priorCapture?.binding as JsonObject | undefined;
const priorDecision = priorCapture?.decision as JsonObject | undefined;
Expand All @@ -127,11 +172,15 @@ export function projectWorkYardstickCue(params: JsonObject): JsonObject | null {
candidate_sha256: execution.candidate_sha256, evidence_refs: execution.evidence_refs,
recorded_planned_method_matches: execution.planned_method_id === (priorDecision!.next_action as JsonObject).method_id}
: null;
const profiles = [decision.authoritative, decision.proxy].filter(Boolean) as JsonObject[];
const feedback = ((params.measurement_feedback ?? []) as JsonObject[]).filter(row => row.node_id === source.node_id && profiles.some(p =>
Object.entries(p).every(([key, value]) => row[key] === value)));
const profiles = decision.proxy ? [decision.authoritative, decision.proxy] : [decision.authoritative];
const feedback = ((params.measurement_feedback ?? []) as ExploreMeasurementFeedback[]).filter(row =>
row.node_id === source.node_id && profiles.some(p => matchesProfile(row, p)));
const scopedFindings = new Set(findings.filter(row => row.node_id === source.node_id && belongsToWork(row, params))
.map(row => row.finding_id));
const searchComparison = measuredSearchComparison(feedback, decision, scopedFindings);
return {source_finding_id: source.finding_id, node_id: source.node_id, binding: capture.binding, ...decision,
measurement_feedback: feedback,
...(searchComparison ? {search_comparison: searchComparison} : {}),
...(followthrough ? {previous_decision_followthrough: followthrough} : {}),
guidance: counter.length
? "Counterevidence is recorded for this scoped proxy. Reassess its use at the next concrete choice; a local screening heuristic cannot veto an informative authoritative check by itself. Preserve accepted correctness, authorization and budget constraints."
Expand Down
Loading
Loading