feat: connect prepared run and refine presenter notes

This commit is contained in:
lda
2026-07-14 08:59:32 +07:00 Verified
parent 5ba9e55f92
commit 184e41858c
26 changed files with 296 additions and 114 deletions
@@ -80,15 +80,16 @@ export const GuidedProductMoment = ({
const facts = projectDemoRunFacts(demo);
const runResume = demo.state.events.find((event) => event.stage === "run_resume");
const revisionRequested = approvalActions?.state === "revision_requested";
const isReplay = demo.state.mode === "replay";
// Replay revision evidence has a separate recording identity; do not let
// the normal submitted-branch lens imply continuity for that branch.
const eyebrow = revisionRequested
? demo.state.mode === "replay"
? isReplay
? "Prepared branch"
: "Live branch"
: lens.eyebrow;
const headline = revisionRequested
? demo.state.mode === "replay"
? isReplay
? "Separate prepared revision recording"
: "Live revision branch resumes the run"
: lens.headline;
@@ -106,7 +107,7 @@ export const GuidedProductMoment = ({
<header className="guided-product-moment__header">
<span>{eyebrow}</span>
<strong>{headline}</strong>
<p>{statusCopy(moment, approvalActions, demo.state.mode === "replay")}</p>
<p>{statusCopy(moment, approvalActions, isReplay)}</p>
</header>
<div className="guided-product-moment__primary">
@@ -268,7 +268,8 @@ describe("PresentationRoute", () => {
.toHaveAttribute("data-phase", "deployment");
});
it("records a Deployment run request locally without an RPC", async () => {
it("starts the shared prepared workflow action without advancing the Deployment beat", async () => {
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
window.location.hash = "#scene/prepared-lifecycle/deployment";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
@@ -277,11 +278,16 @@ describe("PresentationRoute", () => {
await userEvent.click(screen.getByRole("button", { name: /send message/i }));
expect(await screen.findByRole("status")).toHaveTextContent(
"Run request prepared for the next execution slice.",
);
expect(mockedCallOperation.mock.calls.some(([operation]) => operation === "workflow.runs.start"))
.toBe(false);
expect(await screen.findByText("Prepared run receipt added. Continue when you are ready."))
.toBeInTheDocument();
expect(window.location.hash).toBe("#scene/prepared-lifecycle/deployment");
expect(screen.getByText(/three proposed issues are ready for review/i)).toBeInTheDocument();
await waitFor(() => {
expect(mockedCallOperation.mock.calls.some(([operation]) => operation === "workflow.deployments.inspect"))
.toBe(true);
expect(mockedCallOperation.mock.calls.some(([operation]) => operation === "workflow.runs.start"))
.toBe(true);
}, { timeout: 3000 });
});
it("keeps Diagnose submission on the current beat", async () => {
@@ -109,6 +109,11 @@ export const PresentationStage = ({
motionDisabled={state.motionDisabled}
approvalActions={approvalActions}
onPreparedLifecycleAdvance={onPreparedLifecycleAdvance}
onRunPreparedWorkflow={
demoRail.kind === "action" && timelineAgent
? () => timelineAgent.runPreparedWorkflow(demoRail.mode)
: undefined
}
/>
)}
</section>
@@ -30,15 +30,16 @@ describe("RunInputFileBrowser", () => {
const files = within(browser).getByRole("list", { name: /included in prepared run/i });
expect(within(files).getAllByRole("listitem")).toHaveLength(2);
expect(within(files).getAllByText("selected")).toHaveLength(2);
expect(within(files).getAllByText("selected")).toHaveLength(1);
expect(within(files).getAllByRole("button")).toHaveLength(2);
expect(within(files).queryAllByRole("link")).toHaveLength(0);
for (const path of ["docs/project-brief.md", "docs/architecture-notes.md"]) {
const row = within(files).getByText(path).closest("li");
expect(row).not.toBeNull();
expect(row).toHaveAttribute("data-file-path", path);
expect(row).toHaveTextContent(/selected/i);
}
expect(within(files).getByText("docs/project-brief.md").closest("li")).toHaveTextContent(/selected/i);
expect(within(files).getByText("docs/architecture-notes.md").closest("li")).not.toHaveTextContent(/selected/i);
const destination = within(browser).getByRole("group", { name: /workflow output/i });
expect(destination).toHaveTextContent("artifacts/issue-board.json");
@@ -57,6 +58,11 @@ describe("RunInputFileBrowser", () => {
expect(preview).toHaveTextContent(/architecture-notes\.md/i);
expect(preview).toHaveTextContent(/fixture preview/i);
expect(preview).toHaveTextContent(/not execution evidence/i);
expect(screen.getByRole("button", { name: /architecture-notes\.md/i })).toHaveAttribute(
"aria-pressed",
"true",
);
expect(screen.getByRole("button", { name: /project-brief\.md/i })).not.toHaveTextContent(/selected/i);
});
it("shows an honest empty preview for files absent from the fixture catalog", () => {
@@ -34,7 +34,9 @@ export const RunInputFileBrowser = ({
>
<span className="run-input-file-browser__icon" aria-hidden="true">md</span>
<code>{path}</code>
<span className="run-input-file-browser__marker">selected</span>
{path === visibleSelectedPath ? (
<span className="run-input-file-browser__marker">selected</span>
) : null}
</button>
</li>
))}
@@ -33,6 +33,7 @@ type SceneBodyProps = {
readonly motionDisabled: boolean;
readonly approvalActions?: DemoApprovalActions | undefined;
readonly onPreparedLifecycleAdvance?: (() => void) | undefined;
readonly onRunPreparedWorkflow?: (() => Promise<void>) | undefined;
};
const workflowDemoSceneIds = new Set([
@@ -290,7 +291,7 @@ const assertNever = (value: never): never => {
throw new Error(`Unexpected view: ${value}`);
};
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions, onPreparedLifecycleAdvance }: SceneBodyProps) => {
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions, onPreparedLifecycleAdvance, onRunPreparedWorkflow }: SceneBodyProps) => {
const sceneId = location.kind === "main" ? location.sceneId : "positioning";
const beatId = location.kind === "main" ? location.beatId : "landscape";
const scene = findScene(sceneId) ?? findScene("thesis")!;
@@ -331,6 +332,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
scene={scene}
beat={beat}
onAdvance={onPreparedLifecycleAdvance}
onRunPreparedWorkflow={onRunPreparedWorkflow}
discussionRail={scene.id === "prepared-lifecycle" ? discussionLinks : undefined}
/>
);
@@ -2,6 +2,7 @@ import { AssistantOperatorThread } from "../chat/AssistantOperatorThread.js";
import {
authoringToolGroupId,
projectPreparedAuthoringThread,
projectPreparedRunRequestExchange,
type AuthoringPhaseId,
} from "./authoring-recording.js";
import {
@@ -17,6 +18,7 @@ type AuthoringConversationProps = {
readonly requestOverride?: string | undefined;
readonly requestOverrides?: PreparedLifecycleSubmittedOverrides | undefined;
readonly scrollMode?: "active" | "start" | undefined;
readonly runRequested?: string | null | undefined;
};
/** Renders the same prepared conversation at full-stage or compact-dock scale. */
@@ -27,17 +29,25 @@ export const AuthoringConversation = ({
requestOverride,
requestOverrides,
scrollMode,
}: AuthoringConversationProps) => (
<AssistantOperatorThread
mode={surface === "stage" ? "full" : "dock"}
surface={surface}
messages={projectPreparedAuthoringThread(
runRequested,
}: AuthoringConversationProps) => {
const messages = [
...projectPreparedAuthoringThread(
recordingPhaseForStep(throughPhase),
requestOverride,
requestOverrides,
)}
activeToolGroupId={authoringToolGroupId(recordingPhaseForStep(activePhase))}
scrollMode={scrollMode}
ariaLabel="prepared authoring conversation"
/>
);
),
...(runRequested ? projectPreparedRunRequestExchange(runRequested) : []),
];
return (
<AssistantOperatorThread
mode={surface === "stage" ? "full" : "dock"}
surface={surface}
messages={messages}
activeToolGroupId={authoringToolGroupId(recordingPhaseForStep(activePhase))}
scrollMode={runRequested ? "end" : scrollMode}
ariaLabel="prepared authoring conversation"
/>
);
};
@@ -7,7 +7,12 @@ import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleSce
afterEach(() => cleanup());
const renderBeat = (beatId: string, onAdvance?: () => void, discussionRail?: ReactNode) => {
const renderBeat = (
beatId: string,
onAdvance?: () => void,
discussionRail?: ReactNode,
onRunPreparedWorkflow?: () => Promise<void>,
) => {
const scene = findScene("prepared-lifecycle");
const beat = findBeat("prepared-lifecycle", beatId);
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
@@ -16,6 +21,7 @@ const renderBeat = (beatId: string, onAdvance?: () => void, discussionRail?: Rea
scene={scene}
beat={beat}
onAdvance={onAdvance}
onRunPreparedWorkflow={onRunPreparedWorkflow}
discussionRail={discussionRail}
/>,
);
@@ -173,6 +179,21 @@ describe("PreparedAuthoringLifecycleScene", () => {
expect(onAdvance).toHaveBeenCalledTimes(advances ? 1 : 0);
});
it("answers the terminal run request without advancing the presentation", async () => {
const user = userEvent.setup();
const onAdvance = vi.fn();
const onRunPreparedWorkflow = vi.fn().mockResolvedValue(undefined);
renderBeat("deployment", onAdvance, undefined, onRunPreparedWorkflow);
await user.click(screen.getByRole("button", { name: /send message/i }));
expect(screen.getByRole("textbox", { name: /message to authoring assistant/i })).toHaveValue("");
expect(screen.getByRole("button", { name: /run.*1 tool call/i })).toBeInTheDocument();
expect(screen.getByText(/three proposed issues are ready for review/i)).toBeInTheDocument();
expect(onRunPreparedWorkflow).toHaveBeenCalledOnce();
expect(onAdvance).not.toHaveBeenCalled();
});
it.each([
["discover", "Discover"],
["draft", "Draft"],
@@ -18,6 +18,7 @@ type PreparedAuthoringLifecycleSceneProps = {
readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition;
readonly onAdvance?: (() => void) | undefined;
readonly onRunPreparedWorkflow?: (() => Promise<void>) | undefined;
readonly discussionRail?: ReactNode;
};
@@ -40,7 +41,7 @@ const steps = [
* Each beat shows a persistent prepared assistant beside one dominant phase
* projection sourced from the prepared authoring recording.
*/
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, discussionRail }: PreparedAuthoringLifecycleSceneProps) => {
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, onRunPreparedWorkflow, discussionRail }: PreparedAuthoringLifecycleSceneProps) => {
const [messageState, dispatch] = useReducer(
preparedLifecycleMessageReducer,
initialPreparedLifecycleMessageState,
@@ -81,7 +82,12 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, discus
dispatch({ type: "artifact_submitted" });
onAdvance?.();
}
if (step === "deployment") dispatch({ type: "run_requested" });
if (step === "deployment") {
dispatch({ type: "run_requested" });
// This is the same mode-aware action as the footer control; the
// presentation remains on this beat so the presenter advances it.
void onRunPreparedWorkflow?.();
}
}}
/>
<div className="prepared-lifecycle-scene__presentation">
@@ -41,7 +41,8 @@ describe("PresentationAssistantPane", () => {
);
expect(screen.getByRole("heading", { name: /authoring assistant/i })).toBeInTheDocument();
expect(screen.getByText(/current phase: validate/i)).toBeInTheDocument();
expect(screen.getByText(/prepared replay only/i)).toBeInTheDocument();
expect(screen.getByText(/prepared assistant transcript/i)).toBeInTheDocument();
expect(screen.getByText(/final run request uses the configured demo target/i)).toBeInTheDocument();
});
it("exposes an explicit support role when composed in the lifecycle scene", () => {
@@ -129,6 +130,15 @@ describe("PresentationAssistantPane", () => {
cleanup();
renderPane("deployment", { runRequested: "Run this deployment" });
expect(screen.getByRole("button", { name: /send message/i })).toBeDisabled();
expect(screen.getByText("Run this deployment")).toBeInTheDocument();
expect(screen.getByText(/starting lda_report_case_study\.default with the prepared input/i))
.toBeInTheDocument();
const runTools = screen.getByRole("button", { name: /run.*1 tool call/i });
expect(runTools).toHaveAttribute("aria-expanded", "false");
await user.click(runTools);
await user.click(screen.getByRole("button", { name: /startRun/i }));
expect(screen.getByText(/run_recorded_lda_report/i)).toBeInTheDocument();
expect(screen.getByText(/paused at the typed issue-review boundary/i)).toBeInTheDocument();
});
it("renders submitted overrides while keeping the replay tools canonical", () => {
@@ -58,7 +58,10 @@ export const PresentationAssistantPane = ({
};
const { submit, handleKeyDown } = usePreparedComposerSubmit(
canSubmit,
() => onSubmit(draft),
() => {
onSubmit(draft);
setDraft("");
},
);
return (
@@ -74,7 +77,7 @@ export const PresentationAssistantPane = ({
<h2>Authoring assistant</h2>
<p>Current phase: {phaseLabels[phase]}</p>
<p className="presentation-assistant-pane__disclosure">
Prepared replay only. No live actions or RPC calls.
Prepared assistant transcript. The final run request uses the configured demo target.
</p>
</header>
<div className="presentation-assistant-pane__conversation">
@@ -83,6 +86,7 @@ export const PresentationAssistantPane = ({
activePhase={phase}
surface="stage"
requestOverrides={submittedOverrides}
runRequested={runRequested}
/>
</div>
<form className="presentation-assistant-pane__composer" onSubmit={submit}>
@@ -106,7 +110,7 @@ export const PresentationAssistantPane = ({
</p>
{runRequested !== null ? (
<p role="status" className="presentation-assistant-pane__run-status">
Run request prepared for the next execution slice.
Prepared run receipt added. Continue when you are ready.
</p>
) : null}
</form>
@@ -4,6 +4,7 @@ import {
authoringToolGroupId,
projectPreparedAuthoring,
projectPreparedAuthoringThread,
projectPreparedRunRequestExchange,
type AuthoringPhaseId,
} from "./authoring-recording.js";
@@ -225,3 +226,27 @@ describe("projectPreparedAuthoringThread", () => {
]);
});
});
describe("projectPreparedRunRequestExchange", () => {
it("projects the prepared start receipt and typed interrupt without navigation actions", () => {
const messages = projectPreparedRunRequestExchange("Run the saved deployment.");
const parts = messages.flatMap((message) => message.parts);
expect(messages[0]).toMatchObject({ role: "user", parts: [{ text: "Run the saved deployment." }] });
expect(parts).toContainEqual(expect.objectContaining({
type: "tool-call",
call: expect.objectContaining({ name: "startRun" }),
}));
expect(parts).toContainEqual(expect.objectContaining({
type: "tool-result",
result: expect.objectContaining({
output: expect.objectContaining({
run_id: "run_recorded_lda_report",
status: "interrupted",
resume_readiness: "ready",
}),
}),
}));
expect(parts.some((part) => part.type === "presentation-action")).toBe(false);
});
});
@@ -354,5 +354,62 @@ export const projectPreparedAuthoringThread = (
});
};
/**
* Projects the terminal prepared request into the same chat vocabulary as a
* live run, while retaining the stable replay run identity used by later scenes.
*/
export const projectPreparedRunRequestExchange = (
request: string,
): readonly AgentMessage[] => {
const callId = "authoring-run-request-call";
return [
agentTextMessage("authoring-run-request-user", "user", request),
agentTextMessage(
"authoring-run-request-starting",
"assistant",
"Starting lda_report_case_study.default with the prepared input.",
),
{
// The authoring prefix reuses the compact phase tool-group treatment;
// presenters can expand the real-shaped receipt only when useful.
id: "authoring-run-tools",
role: "assistant",
parts: [
agentToolCallPart(callId, "startRun", {
operation: "workflow.runs.start",
deployment_id: "lda_report_case_study.default",
input_file: "examples/lda_report_workflow/run-input.json",
equivalent_cli:
"uv run wf --url http://127.0.0.1:8765/rpc run start lda_report_case_study.default --input-file examples/lda_report_workflow/run-input.json",
}),
agentToolResultPart(callId, "startRun", "success", {
artifact_id: "lda_report_case_study",
artifact_version: 1,
deployment_id: "lda_report_case_study.default",
diagnostics: [],
interrupt: {
id: "interrupt:review_issues",
kind: "issue_review",
node_id: "review_issues",
outcomes: ["submitted", "cancelled"],
proposed_issue_count: 3,
resumable: true,
typed: true,
},
resume_readiness: "ready",
run_id: "run_recorded_lda_report",
status: "interrupted",
trace_count: 6,
}),
],
},
agentTextMessage(
"authoring-run-request-paused",
"assistant",
"The run started and paused at the typed issue-review boundary. Three proposed issues are ready for review.",
),
];
};
/** Returns the full prepared authoring recording. */
export const projectPreparedAuthoring = (): readonly PreparedAuthoringPhase[] => recording;
@@ -43,14 +43,14 @@ describe("prepared lifecycle staged message state", () => {
});
});
it("preserves edited draft text exactly when submitting draft", () => {
it("preserves submitted text exactly and clears the composer draft", () => {
const edited = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
type: "draft_edited",
draft: " Check only the report binding. ",
});
expect(preparedLifecycleMessageReducer(edited, { type: "draft_submitted" })).toEqual({
draft: edited.draft,
draft: "",
submittedOverrides: { validate: edited.draft },
runRequested: null,
});
@@ -63,7 +63,7 @@ describe("prepared lifecycle staged message state", () => {
});
expect(preparedLifecycleMessageReducer(state, { type: "artifact_submitted" })).toEqual({
draft: state.draft,
draft: "",
submittedOverrides: { deployment: state.draft },
runRequested: null,
});
@@ -76,7 +76,7 @@ describe("prepared lifecycle staged message state", () => {
});
expect(preparedLifecycleMessageReducer(state, { type: "discover_submitted" })).toEqual({
draft: state.draft,
draft: "",
submittedOverrides: { discover: state.draft },
runRequested: null,
});
@@ -106,6 +106,7 @@ describe("prepared lifecycle staged message state", () => {
const requested = preparedLifecycleMessageReducer(state, { type: "run_requested" });
expect(requested.runRequested).toBe("Run this deployment");
expect(requested.draft).toBe("");
expect(requested.submittedOverrides).toEqual({});
expect(preparedLifecycleMessageReducer(requested, { type: "run_requested" })).toBe(requested);
});
@@ -57,6 +57,9 @@ const submitOverride = (
return {
...state,
// The submitted text is preserved in the transcript override; leaving it
// in the composer would leak the previous phase's request into the next.
draft: "",
submittedOverrides: {
...state.submittedOverrides,
[destination]: state.draft,
@@ -79,7 +82,7 @@ export const preparedLifecycleMessageReducer = (
return submitOverride(state, "deployment");
case "run_requested":
return state.runRequested === null && hasText(state.draft)
? { ...state, runRequested: state.draft }
? { ...state, draft: "", runRequested: state.draft }
: state;
}
};
@@ -6,7 +6,7 @@ type PreparedComposerSubmitHandlers = {
};
export const PREPARED_COMPOSER_HELP =
"Shift+Enter adds a new line. This is a deterministic prepared replay, not a live model request.";
"Shift+Enter adds a new line. Responses are prepared; the final run request may use the configured workflow target.";
/** Shares the prepared-replay composer keyboard and form submission contract. */
export const usePreparedComposerSubmit = (
@@ -232,7 +232,7 @@ describe("AssistantOperatorThread", () => {
}
});
it("can show the latest response in a static comparison transcript", async () => {
it("can show the latest response even when an authoring group is synchronized", async () => {
const setScrollTop = vi.fn();
const descriptors = {
scrollTop: Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "scrollTop"),
@@ -246,7 +246,14 @@ describe("AssistantOperatorThread", () => {
scrollHeight: { configurable: true, get: () => 240 },
clientHeight: { configurable: true, get: () => 80 },
});
render(<AssistantOperatorThread mode="dock" messages={[]} scrollMode="end" />);
render(
<AssistantOperatorThread
mode="dock"
messages={[]}
activeToolGroupId="authoring-deployment"
scrollMode="end"
/>,
);
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(160));
} finally {
@@ -276,9 +276,9 @@ export const AssistantOperatorThread = ({
const viewport = viewportRef.current;
if (!viewport) return;
// Static comparison transcripts should show their final answer; live
// presentation docks instead keep the beat-owned tool group in view.
if (!activeToolGroupId && scrollMode === "end") {
// A terminal exchange must override phase anchoring so its result and
// final assistant response remain visible after the composer submits.
if (scrollMode === "end") {
viewport.scrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
return;
}
@@ -25,7 +25,9 @@ describe("PresenterNote", () => {
/>,
);
expect(screen.getByRole("region", { name: "Beat goal" })).toHaveTextContent(note.goal);
const goal = screen.getByRole("region", { name: "Beat goal" });
expect(goal).toHaveTextContent(note.goal.replaceAll("**", ""));
expect(within(goal).getByText("one public operation").tagName).toBe("STRONG");
const anchors = screen.getByRole("region", { name: "Anchor terms" });
const anchorList = within(anchors).getByRole("list");
@@ -28,7 +28,7 @@ export const PresenterNote = ({ note, cumulativeSeconds, next, covered, onCovere
<section className="presenter-note__goal" aria-labelledby="presenter-goal">
<span id="presenter-goal">Beat goal</span>
<p>{note.goal}</p>
<div className="presenter-note__goal-copy"><ReactMarkdown>{note.goal}</ReactMarkdown></div>
</section>
<section className="presenter-note__anchors" aria-labelledby="presenter-anchors">
@@ -59,7 +59,7 @@ export const presenterNotes = [
"thesis",
"title",
15,
"Separate the AI-agent ambition from the implemented contribution.",
"Separate the **AI-agent ambition** from the implemented contribution.",
["AI-agent goal", "platform underneath"],
"The title describes the original goal: an AI agent for workspace automation. My contribution is the platform underneath that agent.",
["Thesis Abstract and Introduction"],
@@ -72,7 +72,7 @@ export const presenterNotes = [
"thesis",
"substrate",
15,
"State what the platform lets its users do.",
"State **what the platform lets its users do**.",
["agents and humans", "build, run, inspect"],
"It lets agents and humans build workflows, run them, and inspect what happened.",
["Thesis Abstract and Introduction", "Thesis Contributions"],
@@ -82,7 +82,7 @@ export const presenterNotes = [
"problem",
"direct-actions",
15,
"Show why one successful chat is not yet automation.",
"Show why **one successful chat is not yet automation**.",
["tool calls", "not reusable"],
"Like the chat example, an agent can call tools and finish one task. But that conversation is not yet a reusable workflow.",
["Thesis Problem Statement and Requirements"],
@@ -92,7 +92,7 @@ export const presenterNotes = [
"problem",
"missing-contracts",
15,
"Name the minimum durable properties reusable automation needs.",
"Name the **minimum durable properties** reusable automation needs.",
["saved definition", "validation", "execution records"],
"Reusable automation needs a saved definition, validation, execution records, and a clear way to pause and continue.",
["Thesis Problem Statement and Requirements"],
@@ -102,7 +102,7 @@ export const presenterNotes = [
"positioning",
"landscape",
18,
"Place the work beside familiar adjacent systems.",
"Place the work beside **familiar adjacent systems**.",
["Python / n8n / Zapier", "LangGraph", "MCP"],
"Existing systems solve different parts of this problem: Python scripts, n8n, Zapier, LangGraph, and MCP.",
["Thesis Positioning and Related Systems"],
@@ -112,7 +112,7 @@ export const presenterNotes = [
"positioning",
"lda-position",
17,
"State the platform's narrow position without a superiority claim.",
"State the platform's **narrow position**, without a superiority claim.",
["provider-neutral", "workflow layer", "not a replacement"],
"My platform does not replace them. It provides a provider-neutral workflow layer that agents and humans can operate.",
["Thesis Positioning and Related Systems", "Thesis Source Model"],
@@ -122,7 +122,7 @@ export const presenterNotes = [
"planner-runtime",
"planner",
12,
"Assign workflow decisions to an external planner.",
"Assign workflow decisions to an **external planner**.",
["human or AI planner"],
"A human or AI planner decides what workflow to build.",
["Thesis Architecture Overview"],
@@ -132,7 +132,7 @@ export const presenterNotes = [
"planner-runtime",
"runtime",
16,
"Assign execution and recording to the runtime.",
"Assign **execution and recording** to the runtime.",
["validation", "step-by-step execution", "state and traces"],
"The runtime validates the graph, executes it step by step, records state and traces, and pauses at declared boundaries.",
["Thesis Workflow Core", "Thesis Architecture Overview"],
@@ -146,7 +146,7 @@ export const presenterNotes = [
"planner-runtime",
"boundary",
12,
"Introduce the public seam between clients and runtime.",
"Introduce the **public seam** between clients and runtime.",
["Workflow API", "CLI", "JSON-RPC"],
"Both sides communicate through the Workflow API. Today, clients reach it through the CLI or JSON-RPC without accessing runtime internals directly.",
["Thesis Architecture Overview", "docs/source_architecture.md"],
@@ -156,7 +156,7 @@ export const presenterNotes = [
"lifecycle",
"draft",
9,
"Introduce the editable lifecycle state.",
"Introduce the **editable lifecycle state**.",
["Draft", "being built"],
"A workflow moves through four lifecycle stages. Draft means the workflow is still being built.",
["Thesis Workflow Lifecycle"],
@@ -166,7 +166,7 @@ export const presenterNotes = [
"lifecycle",
"artifact",
9,
"Introduce the immutable saved definition.",
"Introduce the **immutable saved definition**.",
["Artifact", "immutable version"],
"Artifact is a saved, immutable version.",
["Thesis Workflow Lifecycle"],
@@ -175,7 +175,7 @@ export const presenterNotes = [
"lifecycle",
"deployment",
9,
"Connect a saved definition to a runnable environment.",
"Connect a saved definition to a **runnable environment**.",
["Deployment", "sources", "ready"],
"Deployment connects that version to the sources it needs and checks whether it is ready.",
["Thesis Workflow Lifecycle"],
@@ -184,7 +184,7 @@ export const presenterNotes = [
"lifecycle",
"run",
9,
"Introduce one persisted execution record.",
"Introduce one **persisted execution record**.",
["Run", "status", "output and trace"],
"Run is one recorded execution, including its status, output, and trace.",
["Thesis Workflow Lifecycle"],
@@ -194,27 +194,27 @@ export const presenterNotes = [
"architecture",
"overview",
6,
"Show how the implementation realizes the earlier concepts.",
"Show how the **implementation realizes the earlier concepts**.",
["architecture spine"],
"This is how those concepts are organized in the implementation.",
"Now I will show how those concepts map to the implementation.",
["Thesis System Architecture", "docs/project_map.md"],
),
beatNote(
"architecture",
"client",
8,
"Show that humans and agents share one public surface.",
"Show that humans and agents share **one public surface**.",
["shared operations"],
"Humans and agents use the same public workflow operations.",
"At the top, humans and agents use the same public workflow operations.",
["Thesis System Architecture", "docs/project_map.md"],
),
beatNote(
"architecture",
"api",
9,
"Identify the system's public front door.",
"Identify the system's **public front door**.",
["Workflow API", "public boundary"],
"The Workflow API is the front door. It exposes lifecycle operations without exposing runtime internals.",
"Those operations enter through the Workflow API, the public boundary that keeps clients out of runtime internals.",
["Thesis System Architecture", "docs/source_architecture.md"],
{ qnaBranchIds: ["not-just-cli"] },
),
@@ -222,9 +222,9 @@ export const presenterNotes = [
"architecture",
"runtime",
9,
"Explain what the server composes behind the API.",
"Explain what the server composes **behind the API**.",
["WorkflowServer", "records and capabilities", "execution core"],
"Behind it, the workflow server brings together stored records, available capabilities, and the execution core.",
"Behind that boundary, the workflow server brings together stored records, available capabilities, and the execution core.",
["Thesis System Architecture", "docs/source_architecture.md"],
{ qnaBranchIds: ["provider-security"] },
),
@@ -232,7 +232,7 @@ export const presenterNotes = [
"agent-handoff",
"request",
12,
"Disclose the prepared demonstration before it begins.",
"Disclose the **prepared demonstration** before it begins.",
["prepared example", "not an autonomous planner"],
"This is a prepared example, not a live autonomous AI agent. It shows how an agent could use the platform to build and run a workflow.",
["Constrained demo agent and prepared replay recipe"],
@@ -245,7 +245,7 @@ export const presenterNotes = [
"prepared-lifecycle",
"discover",
7,
"Show that authoring starts with interface discovery.",
"Show that authoring starts with **interface discovery**.",
["sources", "capabilities"],
"First, the agent checks which sources and operations are available.",
["examples/lda_report_workflow", "deployment inspect replay evidence"],
@@ -255,7 +255,7 @@ export const presenterNotes = [
"prepared-lifecycle",
"draft",
7,
"Show mutable workflow authoring.",
"Show **mutable workflow authoring**.",
["Draft", "editable workflow"],
"Then it builds an editable workflow draft.",
["examples/lda_report_workflow", "deployment inspect replay evidence", "CLI documentation", "Draft authoring API"],
@@ -265,7 +265,7 @@ export const presenterNotes = [
"prepared-lifecycle",
"diagnose",
7,
"Show a concrete structured validation failure.",
"Show a **concrete structured validation failure**.",
["validation", "missing_outcome_edge"],
"Validation finds that the analyze step has no route for its ok outcome.",
["examples/lda_report_workflow", "deployment inspect replay evidence"],
@@ -275,7 +275,7 @@ export const presenterNotes = [
"prepared-lifecycle",
"repair",
7,
"Show the exact focused correction and revalidation.",
"Show the **focused correction and revalidation**.",
["set-route", "validation passes"],
"The agent adds that route, and validation passes.",
["Validation and diagnostics", "Challenge UX findings"],
@@ -285,7 +285,7 @@ export const presenterNotes = [
"prepared-lifecycle",
"artifact",
7,
"Show the transition to an immutable saved version.",
"Show the transition to an **immutable saved version**.",
["Artifact", "immutable"],
"The valid workflow is saved as an immutable artifact.",
["examples/lda_report_workflow", "deployment inspect replay evidence"],
@@ -294,7 +294,7 @@ export const presenterNotes = [
"prepared-lifecycle",
"deployment",
7,
"Show source binding and readiness before execution.",
"Show **source binding and readiness** before execution.",
["Deployment", "three local sources"],
"Finally, a deployment connects it to the three local sources it needs.",
["examples/lda_report_workflow", "deployment inspect replay evidence", "Thesis deterministic report case study"],
@@ -304,7 +304,7 @@ export const presenterNotes = [
"run-from-deployment",
"input",
11,
"Show the concrete inputs supplied before execution.",
"Show the **concrete inputs** supplied before execution.",
["run input", "selected documents"],
"The deployment receives selected local documents and an issue-board path.",
["workflow.runs.start replay evidence"],
@@ -313,7 +313,7 @@ export const presenterNotes = [
"run-from-deployment",
"operation",
12,
"Show that one public operation creates a persisted execution.",
"Show that **one public operation** creates a persisted execution.",
["workflow.runs.start", "persisted Run"],
"The public **workflow.runs.start** operation validates the deployment and input, creates a **persisted Run**, and begins the reusable graph.",
["workflow.runs.start replay evidence"],
@@ -323,7 +323,7 @@ export const presenterNotes = [
"run-from-deployment",
"graph",
12,
"Show the reusable workflow executing beyond the chat conversation.",
"Show the **reusable workflow** executing beyond the chat conversation.",
["workflow graph", "declared interrupt"],
"The graph reads documents, analyzes them, builds a report, drafts proposed issues, and pauses at a declared review interrupt before issue-board changes.",
["workflow.runs.start replay evidence", "examples/lda_report_workflow"],
@@ -333,7 +333,7 @@ export const presenterNotes = [
"typed-human-boundary",
"interrupt",
15,
"Show what the paused workflow asks from the operator.",
"Show what the **paused workflow asks** from the operator.",
["issue_review", "interrupt payload", "resume schema"],
"Execution pauses at a **typed issue_review interrupt** exposing request data, allowed outcomes, request schema, and resume schema.",
["Typed interrupt payload and resume contract"],
@@ -343,9 +343,9 @@ export const presenterNotes = [
"typed-human-boundary",
"approval",
15,
"Show that the operator chooses a declared continuation.",
"Show that the operator chooses a **declared continuation**.",
["submitted", "revision-requested", "typed resume"],
"The operator chooses submitted or revision-requested; this is a typed interrupt and resume contract, not a production approval gate, role system, or policy engine.",
"The operator can Submit or Request revision. Both continue through declared workflow branches; this typed resume contract is not a production approval system.",
["Typed interrupt payload and resume contract"],
{ warning: "Both outcomes resume through declared workflow branches; this is not production approval governance.", qnaBranchIds: ["typed-interrupts", "security-production-boundary"] },
),
@@ -353,7 +353,7 @@ export const presenterNotes = [
"resume-output-evidence",
"resume",
16,
"Show continuation of the same recorded run.",
"Show continuation of the **same recorded run**.",
["workflow.runs.resume", "same Run"],
"On the submitted path, **workflow.runs.resume continues the recorded interrupted Run**.",
["workflow.runs.resume replay evidence", "Revision replay identity"],
@@ -363,7 +363,7 @@ export const presenterNotes = [
"resume-output-evidence",
"output",
16,
"Show the persisted terminal results of the submitted path.",
"Show the **persisted terminal results** of the submitted path.",
["report output", "issue-board changes"],
"The workflow creates the report and issue-board changes, then records terminal output.",
["workflow.runs.resume replay evidence", "examples/lda_report_workflow"],
@@ -373,19 +373,19 @@ export const presenterNotes = [
"resume-output-evidence",
"trace",
18,
"Show that execution evidence remains inspectable after completion.",
"Show that **execution evidence remains inspectable** after completion.",
["trace frames", "protocol evidence"],
"Trace frames and protocol evidence remain inspectable; this is declared-boundary resumability, not arbitrary crash recovery or exactly-once execution. The revision replay is a separate prepared recording.",
["workflow.runs.resume replay evidence", "Revision replay identity"],
{ warning: "Never claim run-ID continuity for the prepared revision recording.", qnaBranchIds: ["replay-provenance", "demo-reliability"] },
"Trace frames and protocol evidence remain inspectable; this is declared-boundary resumability, not arbitrary crash recovery or exactly-once execution.",
["workflow.runs.resume replay evidence"],
{ qnaBranchIds: ["replay-provenance", "demo-reliability"] },
),
beatNote(
"evaluation",
"cohort",
40,
"Describe the external-agent evaluation design.",
"Describe the **external-agent evaluation design**.",
["36 trials", "two challenges", "three profiles"],
"The evaluation combines conformance tests, deterministic case studies, and a **manually audited external-agent campaign**: 36 trials across two challenges, two hosted models, three instruction profiles, and three waves, with three attempts per cell.",
"The evaluation combines conformance tests, deterministic case studies, and a **manually audited external-agent campaign**. Each model, under each instruction profile, attempted each challenge three times, for 36 trials in total.",
["Thesis Evaluation and Appendix C"],
{ qnaBranchIds: ["evaluation-validity"] },
),
@@ -393,9 +393,9 @@ export const presenterNotes = [
"evaluation",
"validity",
40,
"Separate audited valid evidence from contaminated samples.",
"Separate **audited valid evidence** from contaminated samples.",
["27 pass", "8 invalid", "1 fail"],
"The author audit classified 27 trials as clean product-path passes, eight as invalid samples, and one as a failure. Invalid samples included contamination such as reading implementation files, prior artifacts, adjacent attempts, or evaluator state.",
"The author audit then classified 27 trials as clean product-path passes, eight as invalid samples, and one as a failure. Invalid samples included contamination such as reading implementation files, prior artifacts, adjacent attempts, or evaluator state.",
["Thesis Evaluation and Appendix C", "Author audit"],
{ qnaBranchIds: ["evaluation-validity"] },
),
@@ -403,9 +403,9 @@ export const presenterNotes = [
"evaluation",
"findings",
40,
"State what the evaluation supports and what it cannot prove.",
["longitudinal evidence", "not a benchmark"],
"Because prompts, product snapshots, and hosted conditions changed across waves, these results are **longitudinal engineering evidence**. They expose authoring and diagnostic gaps, **not a benchmark** of model success, token reduction, retry reduction, or superiority.",
"State **what the evaluation supports** and what it cannot prove.",
["product evolved", "not a benchmark"],
"But the prompts, product, and hosted conditions changed during the campaign. These results show what we learned while improving the product. They expose authoring and diagnostic gaps; they are **not a benchmark** of model success, token reduction, retry reduction, or superiority.",
["Thesis Evaluation and Appendix C", "Thesis Threats to Validity"],
{ warning: "Use non-benchmark wording; do not report the counts as general model performance.", qnaBranchIds: ["evaluation-validity"] },
),
@@ -413,7 +413,7 @@ export const presenterNotes = [
"conclusion",
"limits",
18,
"Bound the prototype claims before the final contribution statement.",
"Bound the **prototype claims** before the final contribution statement.",
["prototype", "not production security"],
"The prototype uses trusted in-process Python and file-backed stores; it does not provide production authentication, RBAC, sandboxing, scheduling, arbitrary crash recovery, or a bundled autonomous planner.",
["Thesis Limitations"],
@@ -423,7 +423,7 @@ export const presenterNotes = [
"conclusion",
"future",
18,
"Name the surrounding layers left as future work.",
"Name the surrounding layers left as **future work**.",
["live agent", "scheduling", "controlled evaluation"],
"A live agent interface, transactional storage, richer debugging, security hardening, scheduling, and controlled comparative evaluation remain future work.",
["Thesis Future Work"],
@@ -433,7 +433,7 @@ export const presenterNotes = [
"conclusion",
"conclusion",
20,
"Restate the implemented contribution and planner-runtime boundary.",
"Restate the **implemented contribution** and planner-runtime boundary.",
["planner proposes", "platform executes"],
"The contribution is **architectural and implemented**: external planners can propose workflows while a typed platform **validates, binds, executes, persists, interrupts, resumes, and inspects** them through public operations.",
["Thesis Contributions", "Thesis Conclusion"],
@@ -443,7 +443,7 @@ export const presenterNotes = [
"conclusion",
"questions",
19,
"Open structured examiner discussion without introducing new claims.",
"Open **structured examiner discussion** without introducing new claims.",
["defense questions", "evidence"],
"That boundary is the claim I will defend: reusable agent-operated automation is inspectable because planning and execution have explicit contracts. I welcome questions.",
["Thesis Conclusion", "Defense Q&A index"],
@@ -114,7 +114,11 @@ describe("defense storyboard catalog", () => {
it("makes the evaluation beat carry the audited counts and validity boundary", () => {
expect(findBeat("evaluation", "cohort")?.caption).toMatch(/36|two challenges|two hosted models|three waves/i);
expect(findBeat("evaluation", "validity")?.caption).toMatch(/27|8|1|audit/i);
const validityCaption = findBeat("evaluation", "validity")?.caption ?? "";
expect(validityCaption).toMatch(/\b27\b/);
expect(validityCaption).toMatch(/\b8\b/);
expect(validityCaption).toMatch(/\b1\b/);
expect(validityCaption).toMatch(/audit/i);
expect(findBeat("evaluation", "findings")?.caption).toMatch(/changing|longitudinal|benchmark/i);
});
@@ -37,12 +37,23 @@ describe("presentationWorkflowPlan", () => {
expect(node.capability, node.id).toBeTruthy();
expect(node.inputSummary, node.id).toBeTruthy();
expect(node.outputSummary, node.id).toBeTruthy();
expect(node.outcomes, node.id).not.toHaveLength(0);
expect(node.evidencePointer, node.id).toBeTruthy();
if (node.type !== "end") {
expect(node.outcomes, node.id).toEqual(
presentationWorkflowPlan.edges
.filter((edge) => edge.from === node.id)
.map((edge) => edge.outcome),
);
}
}
const interrupt = presentationWorkflowPlan.nodes.find((node) => node.id === "review_issues");
expect(interrupt?.schemaSummary).toMatch(/request: issue proposals/i);
expect(interrupt?.outcomes).toEqual(["submitted", "cancelled"]);
expect(presentationWorkflowPlan.nodes.find((node) => node.id === "finalise")?.outcomes).toEqual([
"completed",
]);
expect(presentationWorkflowPlan.nodes.find((node) => node.id === "revision_requested")?.outcomes).toEqual([]);
});
});
@@ -99,7 +99,7 @@ export const presentationWorkflowPlan = {
capability: "State output",
inputSummary: "Markdown report and issue-board state",
outputSummary: "Completed run output",
outcomes: ["ok"],
outcomes: ["completed"],
evidencePointer: "local.lda_report.finalise_report",
},
{
@@ -111,7 +111,7 @@ export const presentationWorkflowPlan = {
capability: "Revision record",
inputSummary: "Issue-review decision",
outputSummary: "Recorded revision request",
outcomes: ["cancelled"],
outcomes: [],
evidencePointer: "local.lda_report.record_revision_request",
},
{