Merge feat/sub-agents: author-once plan protocol, steer-delivery starvation fixes, research-steer affordance
Wave 3: the plan protocol collapses to one re-callable mermaid plan_init (seven mutation tools deleted by design; statuses derive from quiz checks; live quiz-nodeId validation with mid-turn retry). The steer-starvation arc: steers deliver coalesced with the learner's answer (claimSteers), persist dedupes same-id continuation copies, jobs settle-sync defers while a turn is busy. Research steers gain the collapsed full-brief affordance. Model reasoning effort set to minimal across roles.
This commit is contained in:
@@ -15,11 +15,11 @@ import { VisualOutputCard } from "@/components/artifacts/visual-card";
|
||||
* ("data-subagent-artifact") carries the result. It renders as an artifact —
|
||||
* NEVER a learner bubble, never wrapped in a message container:
|
||||
*
|
||||
* research — a quiet "Research complete" badge. The full brief is TOO LONG
|
||||
* to inline and the teacher's wake turn weaves it into the lesson anyway;
|
||||
* it stays persisted in the transcript (the artifact part), so a
|
||||
* "view full research" affordance can be added later. Failures keep the
|
||||
* failure card (the brief explains what went wrong).
|
||||
* research — a quiet "Research complete" one-liner that expands on demand
|
||||
* (native <details>) to the full persisted brief + sources: the brief is
|
||||
* too long to inline by default and the teacher's wake turn weaves it
|
||||
* into the lesson anyway. Failures keep the failure card (the brief
|
||||
* explains what went wrong).
|
||||
* demo / visual — the artifact itself (sandboxed iframe / sanitized SVG).
|
||||
* The sub-agent's verification notes are model-facing glue and do NOT
|
||||
* render; only a failure note does.
|
||||
@@ -48,15 +48,25 @@ export function SubagentSteerCard({ message }: { message: TeacherUIMessage }) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Quiet one-liner, the dispatch-settled idiom: the brief lives in the record,
|
||||
* the lesson lives in the teacher's wake turn — the badge just marks delivery. */
|
||||
/** Quiet one-liner, the dispatch-settled idiom — the lesson lives in the
|
||||
* teacher's wake turn; the badge just marks delivery. The persisted brief
|
||||
* rides along in a collapsed <details> (the wave-1 "view full research"
|
||||
* affordance): full markdown + sources, one click away, no extra fetch —
|
||||
* the artifact part in the record is the single source of truth. */
|
||||
function ResearchCompleteBadge({ result }: { result: ResearchResult }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground" data-testid="research-complete-badge">
|
||||
<CheckIcon className="size-3.5" />
|
||||
<span className="font-medium uppercase tracking-wide">Research</span>
|
||||
<span>complete</span>
|
||||
{result.sources.length > 0 && <span>· {result.sources.length} sources</span>}
|
||||
</div>
|
||||
<details data-testid="research-complete-badge" className="group w-full text-xs text-muted-foreground">
|
||||
<summary className="flex cursor-pointer select-none list-none items-center gap-2 [&::-webkit-details-marker]:hidden">
|
||||
<CheckIcon className="size-3.5" />
|
||||
<span className="font-medium uppercase tracking-wide">Research</span>
|
||||
<span>complete</span>
|
||||
{result.sources.length > 0 && <span>· {result.sources.length} sources</span>}
|
||||
<span className="ml-1 text-[11px] text-muted-foreground/70 group-open:hidden">view brief</span>
|
||||
<span className="ml-1 hidden text-[11px] text-muted-foreground/70 group-open:inline">hide brief</span>
|
||||
</summary>
|
||||
<div className="mt-2">
|
||||
<ResearchBriefCard output={result} />
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
+29
-6
@@ -178,11 +178,15 @@ export function Chat({
|
||||
// ── Central job observability (contracts.ts "THE ASYNC PROTOCOL" §4) ──
|
||||
// ONE poll of /api/jobs drives every job surface (transcript dispatch
|
||||
// cards, the sub-agents panel) and re-syncs the transcript exactly once
|
||||
// when a job settles: job status flips done/error only AFTER the wake
|
||||
// turn's messages are persisted, so a completion means the record already
|
||||
// holds the steer + the teacher's response — one syncFromServer adopts it
|
||||
// all. Poll fast while jobs run, slow otherwise (new-dispatch discovery),
|
||||
// and not at all when this session has never dispatched a job.
|
||||
// when a job settles. A settle no longer implies the record is complete:
|
||||
// CONTINUATION COALESCING flips the job done at claim time — while the
|
||||
// learner's own answer turn is still streaming (and a settle can race a
|
||||
// second job's wake turn). Syncing mid-turn would swap in a stale snapshot
|
||||
// mid-stream, which breaks the SDK's in-place continuation merge (duplicate
|
||||
// message ids). So: a settle during a busy turn defers the sync; the next
|
||||
// idle poll tick adopts the finished record. Poll fast while jobs run,
|
||||
// slow otherwise (new-dispatch discovery), and not at all when this
|
||||
// session has never dispatched a job.
|
||||
const [jobs, setJobs] = useState<JobRecord[]>([]);
|
||||
const jobPartCount = useMemo(
|
||||
() =>
|
||||
@@ -201,6 +205,8 @@ export function Chat({
|
||||
const localStreamRef = useRef(false);
|
||||
localStreamRef.current = localStream;
|
||||
const jobStatusRef = useRef<Map<string, string>>(new Map());
|
||||
// A settle seen during a busy turn — the sync is owed, not dropped.
|
||||
const pendingSyncRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (pollMs === 0) return;
|
||||
let cancelled = false;
|
||||
@@ -214,9 +220,26 @@ export function Chat({
|
||||
const list = data.jobs ?? [];
|
||||
for (const j of list) {
|
||||
const prev = jobStatusRef.current.get(j.id);
|
||||
if (prev === "running" && j.status !== "running") void syncFromServer();
|
||||
if (prev === "running" && j.status !== "running") {
|
||||
// The record is only guaranteed complete when no turn is in
|
||||
// flight — locally or server-side. A coalesced settle fires
|
||||
// mid-turn by design; adopt the record on the first idle tick.
|
||||
if (localStreamRef.current || data.turnInFlight === true) {
|
||||
pendingSyncRef.current = true;
|
||||
} else {
|
||||
void syncFromServer();
|
||||
}
|
||||
}
|
||||
jobStatusRef.current.set(j.id, j.status);
|
||||
}
|
||||
if (
|
||||
pendingSyncRef.current &&
|
||||
!localStreamRef.current &&
|
||||
data.turnInFlight !== true
|
||||
) {
|
||||
pendingSyncRef.current = false;
|
||||
void syncFromServer();
|
||||
}
|
||||
setJobs(list);
|
||||
// Wake-turn adoption: a server-side teacher turn with no client request
|
||||
// of ours (sub-agent delivery) — the same machinery as returning
|
||||
|
||||
Reference in New Issue
Block a user