feat(missions): surface goal conditions and per-pass verdicts in the UI
Makes the completion evaluator usable and observable.
- GET /api/missions/{id}/phases/{phase_id}/evaluations returns every verdict
for a phase, newest pass first, scoped like the summary endpoint.
- MissionPhase gains done_when / max_iterations / iteration, so the phase card
can show what the phase is working toward and which pass it is on.
- PhaseStatus gains 'evaluating' (amber) -- the state between "runs finished"
and "phase done" that only conditioned phases enter.
- New PhaseGoalStrip renders on the phase card, and renders NOTHING for phases
without a condition so unconditioned missions look exactly as before. It
polls only while the phase is running or being judged.
- Mission wizard step 2 gains the condition + a max-passes field.
Two deliberate emphases in the UI:
The evaluator's `reason` is the most prominent element, because it is both the
explanation of why a phase iterated and the literal text handed back to the
agents as guidance -- it is what tells an operator whether the condition is
written well.
The hint copy states the constraint that actually governs whether a condition
works: the judge cannot run commands, it only reads what the agents wrote, so
the condition has to be provable from their output. "cargo test reported 0
failures" works; "the code is well factored" does not. Getting this wrong is
the difference between a phase that converges and one that burns every pass.
An evaluator error is rendered distinctly from a negative verdict, so a judge
outage doesn't read as a judgement on the work.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f848248fac
commit
fe57ce4ed1
@@ -483,6 +483,10 @@ pub fn router(state: AppState) -> Router {
|
|||||||
"/api/missions/{id}/phases/{phase_id}/summary",
|
"/api/missions/{id}/phases/{phase_id}/summary",
|
||||||
get(routes::missions::get_phase_summary),
|
get(routes::missions::get_phase_summary),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/missions/{id}/phases/{phase_id}/evaluations",
|
||||||
|
get(routes::missions::list_phase_evaluations),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/missions/{id}/teams",
|
"/api/missions/{id}/teams",
|
||||||
get(routes::missions::list_teams),
|
get(routes::missions::list_teams),
|
||||||
|
|||||||
@@ -674,6 +674,51 @@ pub async fn retry_phase(
|
|||||||
/// card produced by `phase_summarizer` for a terminal-state phase.
|
/// card produced by `phase_summarizer` for a terminal-state phase.
|
||||||
/// Returns 404 while the phase is still running / hasn't been
|
/// Returns 404 while the phase is still running / hasn't been
|
||||||
/// summarized yet.
|
/// summarized yet.
|
||||||
|
/// `GET /api/missions/{id}/phases/{phase_id}/evaluations` — every completion
|
||||||
|
/// verdict for a phase, newest first.
|
||||||
|
///
|
||||||
|
/// One row per pass. The `reason` is the operator-facing explanation of why a
|
||||||
|
/// phase iterated (or stopped), and is the same text fed back to the agents as
|
||||||
|
/// guidance for the following pass.
|
||||||
|
pub async fn list_phase_evaluations(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authed(user): Authed,
|
||||||
|
Path((id, phase_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<Json<Vec<Value>>, ApiError> {
|
||||||
|
// Scope check — same shape as get_phase_summary.
|
||||||
|
let _ = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||||
|
.await?
|
||||||
|
.ok_or(ApiError::NotFound)?;
|
||||||
|
use sqlx::Row;
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"SELECT iteration, met, reason, model, error, created_at
|
||||||
|
FROM mission_phase_evaluations
|
||||||
|
WHERE mission_id = $1 AND phase_id = $2
|
||||||
|
ORDER BY iteration DESC",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(phase_id)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(
|
||||||
|
rows.into_iter()
|
||||||
|
.map(|r| {
|
||||||
|
let created_at: time::OffsetDateTime = r.get("created_at");
|
||||||
|
serde_json::json!({
|
||||||
|
"iteration": r.get::<i32, _>("iteration"),
|
||||||
|
"met": r.get::<bool, _>("met"),
|
||||||
|
"reason": r.get::<String, _>("reason"),
|
||||||
|
"model": r.get::<String, _>("model"),
|
||||||
|
"error": r.get::<Option<String>, _>("error"),
|
||||||
|
"created_at": created_at
|
||||||
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_phase_summary(
|
pub async fn get_phase_summary(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Authed(user): Authed,
|
Authed(user): Authed,
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ pub struct MissionPhase {
|
|||||||
pub order_idx: i32,
|
pub order_idx: i32,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub config: Value,
|
pub config: Value,
|
||||||
|
/// Completion condition. `None` = the phase completes as soon as its runs
|
||||||
|
/// finish, with no evaluation (the pre-conditions behaviour).
|
||||||
|
pub done_when: Option<String>,
|
||||||
|
/// Upper bound on passes; 1 means run once.
|
||||||
|
pub max_iterations: i32,
|
||||||
|
/// Which pass the phase is on, 0-based.
|
||||||
|
pub iteration: i32,
|
||||||
#[serde(with = "time::serde::rfc3339::option")]
|
#[serde(with = "time::serde::rfc3339::option")]
|
||||||
pub started_at: Option<OffsetDateTime>,
|
pub started_at: Option<OffsetDateTime>,
|
||||||
#[serde(with = "time::serde::rfc3339::option")]
|
#[serde(with = "time::serde::rfc3339::option")]
|
||||||
@@ -410,6 +417,7 @@ pub async fn phases_for(pool: &PgPool, mission_id: Uuid) -> Result<Vec<MissionPh
|
|||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
"SELECT id, mission_id, kind, order_idx, status, config,
|
"SELECT id, mission_id, kind, order_idx, status, config,
|
||||||
|
done_when, max_iterations, iteration,
|
||||||
started_at, completed_at
|
started_at, completed_at
|
||||||
FROM mission_phases WHERE mission_id = $1
|
FROM mission_phases WHERE mission_id = $1
|
||||||
ORDER BY order_idx ASC",
|
ORDER BY order_idx ASC",
|
||||||
@@ -426,6 +434,9 @@ pub async fn phases_for(pool: &PgPool, mission_id: Uuid) -> Result<Vec<MissionPh
|
|||||||
order_idx: r.get("order_idx"),
|
order_idx: r.get("order_idx"),
|
||||||
status: r.get("status"),
|
status: r.get("status"),
|
||||||
config: r.get("config"),
|
config: r.get("config"),
|
||||||
|
done_when: r.get("done_when"),
|
||||||
|
max_iterations: r.get("max_iterations"),
|
||||||
|
iteration: r.get("iteration"),
|
||||||
started_at: r.get("started_at"),
|
started_at: r.get("started_at"),
|
||||||
completed_at: r.get("completed_at"),
|
completed_at: r.get("completed_at"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { MissionLivePane } from "./MissionLivePane";
|
|||||||
import { MissionOutputReader } from "./MissionOutputReader";
|
import { MissionOutputReader } from "./MissionOutputReader";
|
||||||
import { MissionTeamTab } from "./MissionTeamTab";
|
import { MissionTeamTab } from "./MissionTeamTab";
|
||||||
import { MissionWizard } from "./MissionWizard";
|
import { MissionWizard } from "./MissionWizard";
|
||||||
|
import { PhaseGoalStrip } from "./PhaseGoalStrip";
|
||||||
import { PhaseRunsList } from "./PhaseRunsList";
|
import { PhaseRunsList } from "./PhaseRunsList";
|
||||||
import { PhaseSummaryCard } from "./PhaseSummaryCard";
|
import { PhaseSummaryCard } from "./PhaseSummaryCard";
|
||||||
import { RefineDiffModal } from "./RefineDiffModal";
|
import { RefineDiffModal } from "./RefineDiffModal";
|
||||||
@@ -64,6 +65,8 @@ const STATUS_COLOR: Record<MissionStatus, string> = {
|
|||||||
const PHASE_STATUS_COLOR: Record<PhaseStatus, string> = {
|
const PHASE_STATUS_COLOR: Record<PhaseStatus, string> = {
|
||||||
pending: "#6a6a72",
|
pending: "#6a6a72",
|
||||||
running: "#5ec8d8",
|
running: "#5ec8d8",
|
||||||
|
// Amber: work is done but the completion condition is being judged.
|
||||||
|
evaluating: "#e8b465",
|
||||||
completed: "#5fd08a",
|
completed: "#5fd08a",
|
||||||
failed: "#ff8a7a",
|
failed: "#ff8a7a",
|
||||||
skipped: "#8a8a92",
|
skipped: "#8a8a92",
|
||||||
@@ -834,6 +837,8 @@ export function MissionCanvas({
|
|||||||
: ""}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{/* Renders only when the phase carries a done_when. */}
|
||||||
|
<PhaseGoalStrip missionId={mission.id} phase={p} />
|
||||||
<PhaseRunsList runs={runsByPhase.get(p.id) ?? []} />
|
<PhaseRunsList runs={runsByPhase.get(p.id) ?? []} />
|
||||||
{(p.status === "completed" || p.status === "failed") && (
|
{(p.status === "completed" || p.status === "failed") && (
|
||||||
<PhaseSummaryCard missionId={mission.id} phaseId={p.id} />
|
<PhaseSummaryCard missionId={mission.id} phaseId={p.id} />
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export function MissionWizard({
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
// Completion condition. Empty = the phase completes when its runs finish,
|
||||||
|
// which is the behaviour missions had before conditions existed.
|
||||||
|
const [doneWhen, setDoneWhen] = useState("");
|
||||||
|
const [maxIterations, setMaxIterations] = useState(3);
|
||||||
const [scheduleKind, setScheduleKind] = useState<"one_shot" | "cron">("one_shot");
|
const [scheduleKind, setScheduleKind] = useState<"one_shot" | "cron">("one_shot");
|
||||||
const [cron, setCron] = useState("0 */6 * * *");
|
const [cron, setCron] = useState("0 */6 * * *");
|
||||||
const [runtimeKind, setRuntimeKind] = useState<"zeroclaw" | "local_herdr">("zeroclaw");
|
const [runtimeKind, setRuntimeKind] = useState<"zeroclaw" | "local_herdr">("zeroclaw");
|
||||||
@@ -157,7 +161,18 @@ export function MissionWizard({
|
|||||||
repo_id: repo?.repo_id,
|
repo_id: repo?.repo_id,
|
||||||
schedule,
|
schedule,
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
phases: preset.phases,
|
// The condition rides in each phase's config; the server promotes it
|
||||||
|
// into the done_when / max_iterations columns and clamps the cap.
|
||||||
|
phases: doneWhen.trim()
|
||||||
|
? preset.phases.map((p) => ({
|
||||||
|
...p,
|
||||||
|
config: {
|
||||||
|
...(p.config ?? {}),
|
||||||
|
done_when: doneWhen.trim(),
|
||||||
|
max_iterations: maxIterations,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: preset.phases,
|
||||||
runtime_kind: runtimeKind,
|
runtime_kind: runtimeKind,
|
||||||
target_node_id:
|
target_node_id:
|
||||||
runtimeKind === "local_herdr" ? targetNodeId : undefined,
|
runtimeKind === "local_herdr" ? targetNodeId : undefined,
|
||||||
@@ -340,6 +355,55 @@ export function MissionWizard({
|
|||||||
placeholder="What should the mission accomplish? The template's agents will use this as their driving prompt."
|
placeholder="What should the mission accomplish? The template's agents will use this as their driving prompt."
|
||||||
style={{ ...fieldStyle, resize: "vertical", fontFamily: "inherit" }}
|
style={{ ...fieldStyle, resize: "vertical", fontFamily: "inherit" }}
|
||||||
/>
|
/>
|
||||||
|
<label style={labelStyle} htmlFor="mission-done-when">
|
||||||
|
Done when <span style={{ color: "#6a6a72" }}>(optional)</span>
|
||||||
|
</label>
|
||||||
|
<p style={hintStyle}>
|
||||||
|
A completion condition. After each pass a model checks it and,
|
||||||
|
if it doesn't hold, the phase runs again with the reason as
|
||||||
|
guidance. Leave empty to finish after one pass.
|
||||||
|
</p>
|
||||||
|
<p style={{ ...hintStyle, color: "#e8b465" }}>
|
||||||
|
The checker can't run commands — it only reads what the
|
||||||
|
agents wrote. Phrase the condition so their own output proves
|
||||||
|
it: “cargo test was run and reported 0 failures”
|
||||||
|
works; “the code is well factored” does not.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
id="mission-done-when"
|
||||||
|
value={doneWhen}
|
||||||
|
onChange={(e) => setDoneWhen(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="e.g. a Markdown brief exists under /mission/repo/research and every INT-XX item is marked COMPLETED"
|
||||||
|
style={{ ...fieldStyle, resize: "vertical", fontFamily: "inherit" }}
|
||||||
|
/>
|
||||||
|
{doneWhen.trim() && (
|
||||||
|
<>
|
||||||
|
<label style={labelStyle} htmlFor="mission-max-iterations">
|
||||||
|
Maximum passes
|
||||||
|
</label>
|
||||||
|
<p style={hintStyle}>
|
||||||
|
Each pass is a full team run, so this bounds the cost if the
|
||||||
|
condition is never met.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="mission-max-iterations"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
value={maxIterations}
|
||||||
|
onChange={(e) =>
|
||||||
|
setMaxIterations(
|
||||||
|
Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(20, Number(e.target.value) || 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{ ...fieldStyle, width: 100 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{preset.requiresRepo && (
|
{preset.requiresRepo && (
|
||||||
<>
|
<>
|
||||||
<span style={labelStyle}>Repository</span>
|
<span style={labelStyle}>Repository</span>
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Target } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getPhaseEvaluations,
|
||||||
|
type MissionPhase,
|
||||||
|
type PhaseEvaluation,
|
||||||
|
} from "@/lib/api/missions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The completion condition on a phase, plus how the last pass was judged.
|
||||||
|
*
|
||||||
|
* Renders nothing for phases without a `done_when` — most missions don't have
|
||||||
|
* one, and an empty row per phase would be noise.
|
||||||
|
*
|
||||||
|
* The evaluator's `reason` is deliberately the most prominent thing here: it
|
||||||
|
* is both the explanation of why the phase iterated (or stopped) and the exact
|
||||||
|
* guidance handed to the agents for the next pass, so it is what an operator
|
||||||
|
* needs to decide whether the condition is written well.
|
||||||
|
*/
|
||||||
|
export function PhaseGoalStrip({
|
||||||
|
missionId,
|
||||||
|
phase,
|
||||||
|
}: {
|
||||||
|
missionId: string;
|
||||||
|
phase: MissionPhase;
|
||||||
|
}) {
|
||||||
|
const [evals, setEvals] = useState<PhaseEvaluation[]>([]);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setEvals(await getPhaseEvaluations(missionId, phase.id));
|
||||||
|
} catch {
|
||||||
|
// A phase that has never been judged has no rows; not an error state.
|
||||||
|
}
|
||||||
|
}, [missionId, phase.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!phase.done_when) return;
|
||||||
|
void load();
|
||||||
|
// Poll only while there is something to wait for.
|
||||||
|
if (phase.status !== "running" && phase.status !== "evaluating") return;
|
||||||
|
const t = setInterval(() => void load(), 5000);
|
||||||
|
return () => clearInterval(t);
|
||||||
|
}, [load, phase.done_when, phase.status]);
|
||||||
|
|
||||||
|
if (!phase.done_when) return null;
|
||||||
|
|
||||||
|
const latest = evals[0];
|
||||||
|
const pass = phase.iteration + 1;
|
||||||
|
const judging = phase.status === "evaluating";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
padding: "8px 10px",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "#0d0d10",
|
||||||
|
border: "1px solid #1c1c22",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
|
<Target aria-hidden size={12} color="#e8b465" />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: "#6a6a72",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Done when
|
||||||
|
</span>
|
||||||
|
<span style={{ marginLeft: "auto", fontSize: 10, color: "#6a6a72" }}>
|
||||||
|
pass {pass} / {phase.max_iterations}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ margin: 0, fontSize: 12, color: "#c8c8d0", lineHeight: 1.45 }}>
|
||||||
|
{phase.done_when}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{judging && (
|
||||||
|
<span style={{ fontSize: 11, color: "#e8b465" }}>
|
||||||
|
Judging this pass…
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{latest && (
|
||||||
|
<div style={{ display: "flex", gap: 6, alignItems: "flex-start" }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: latest.met ? "#5fd08a" : "#e8b465",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{latest.met ? "met" : "not met"}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 11, color: "#8a8a92", lineHeight: 1.45 }}>
|
||||||
|
{latest.reason}
|
||||||
|
{latest.error && (
|
||||||
|
// Distinguishes "judged incomplete" from "could not judge" —
|
||||||
|
// an evaluator outage should not read as a verdict on the work.
|
||||||
|
<em style={{ color: "#ff8a7a" }}> (evaluator error)</em>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{evals.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
style={{
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: 10,
|
||||||
|
color: "#6a6a72",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? "hide" : `show all ${evals.length} passes`}
|
||||||
|
</button>
|
||||||
|
{expanded && (
|
||||||
|
<ol
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
paddingLeft: 16,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{evals.map((e) => (
|
||||||
|
<li
|
||||||
|
key={e.iteration}
|
||||||
|
style={{ fontSize: 11, color: "#8a8a92", lineHeight: 1.4 }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{ color: e.met ? "#5fd08a" : "#e8b465", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
pass {e.iteration + 1} {e.met ? "met" : "not met"}
|
||||||
|
</span>{" "}
|
||||||
|
— {e.reason}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,9 @@ export type PhaseKind = "research" | "coding" | "benchmark" | "security_scan";
|
|||||||
export type PhaseStatus =
|
export type PhaseStatus =
|
||||||
| "pending"
|
| "pending"
|
||||||
| "running"
|
| "running"
|
||||||
|
// Runs finished; a completion condition is being judged. Only phases that
|
||||||
|
// declare `done_when` ever enter this state.
|
||||||
|
| "evaluating"
|
||||||
| "completed"
|
| "completed"
|
||||||
| "failed"
|
| "failed"
|
||||||
| "skipped";
|
| "skipped";
|
||||||
@@ -76,10 +79,28 @@ export interface MissionPhase {
|
|||||||
order_idx: number;
|
order_idx: number;
|
||||||
status: PhaseStatus;
|
status: PhaseStatus;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
|
/** Completion condition. null = complete as soon as the runs finish. */
|
||||||
|
done_when: string | null;
|
||||||
|
/** Upper bound on passes; 1 means run once. */
|
||||||
|
max_iterations: number;
|
||||||
|
/** Which pass the phase is on, 0-based. */
|
||||||
|
iteration: number;
|
||||||
started_at: string | null;
|
started_at: string | null;
|
||||||
completed_at: string | null;
|
completed_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One completion verdict, produced after a pass. */
|
||||||
|
export interface PhaseEvaluation {
|
||||||
|
iteration: number;
|
||||||
|
met: boolean;
|
||||||
|
/** Why. Also fed back to the agents as guidance for the next pass. */
|
||||||
|
reason: string;
|
||||||
|
model: string;
|
||||||
|
/** Set when the evaluator itself failed, vs. judging the work incomplete. */
|
||||||
|
error: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MissionTask {
|
export interface MissionTask {
|
||||||
id: string;
|
id: string;
|
||||||
mission_id: string;
|
mission_id: string;
|
||||||
@@ -333,6 +354,12 @@ export interface PhaseSummary {
|
|||||||
export const getPhaseSummary = (missionId: string, phaseId: string) =>
|
export const getPhaseSummary = (missionId: string, phaseId: string) =>
|
||||||
api<PhaseSummary>(`/api/missions/${missionId}/phases/${phaseId}/summary`);
|
api<PhaseSummary>(`/api/missions/${missionId}/phases/${phaseId}/summary`);
|
||||||
|
|
||||||
|
/** Completion verdicts for a phase, newest pass first. */
|
||||||
|
export const getPhaseEvaluations = (missionId: string, phaseId: string) =>
|
||||||
|
api<PhaseEvaluation[]>(
|
||||||
|
`/api/missions/${missionId}/phases/${phaseId}/evaluations`,
|
||||||
|
);
|
||||||
|
|
||||||
export const retryMissionPhase = (id: string, phaseId: string) =>
|
export const retryMissionPhase = (id: string, phaseId: string) =>
|
||||||
api<{ reset: boolean }>(
|
api<{ reset: boolean }>(
|
||||||
`/api/missions/${id}/phases/${phaseId}/retry`,
|
`/api/missions/${id}/phases/${phaseId}/retry`,
|
||||||
|
|||||||
Reference in New Issue
Block a user