feat: add timeline backed demo agent

This commit is contained in:
lda
2026-07-09 06:12:58 +07:00 Verified
parent c332e58697
commit 6d5ab3a0c5
2 changed files with 192 additions and 0 deletions
@@ -0,0 +1,82 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { DemoTimelineController } from "../useDemoTimeline.js";
import { initialDemoTimelineState } from "../timeline/reducer.js";
import { useTimelineAgent } from "./timelineAgent.js";
const demoController = (
overrides: Partial<DemoTimelineController> = {},
): DemoTimelineController => ({
state: initialDemoTimelineState,
inFlight: false,
interruptPayload: null,
output: null,
trace: null,
missingDeploymentMessage: null,
recordingId: null,
canStart: true,
setMode: vi.fn(),
start: vi.fn(),
pause: vi.fn(),
play: vi.fn(),
next: vi.fn(async () => {}),
submitSelectedIssues: vi.fn(async () => {}),
cancelReview: vi.fn(async () => {}),
restart: vi.fn(),
...overrides,
});
describe("useTimelineAgent", () => {
it("starts the prepared workflow through the timeline", async () => {
const start = vi.fn();
const demo = demoController({ start });
const { result } = renderHook(() => useTimelineAgent(demo, "live"));
await act(async () => result.current.runPreparedWorkflow());
expect(start).toHaveBeenCalledTimes(1);
expect(result.current.messages.at(-1)?.parts).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "tool-result" }),
]),
);
});
it("submits selected issues from the current interrupt payload", async () => {
const submitSelectedIssues = vi.fn(async () => {});
const demo = demoController({
state: { ...initialDemoTimelineState, phase: "review" },
interruptPayload: {
report_markdown: "# Report",
proposed_issues: [
{ id: "risk-1", title: "Risk", body: "Body", severity: "medium" },
],
},
submitSelectedIssues,
});
const { result } = renderHook(() => useTimelineAgent(demo, "replay"));
await act(async () => result.current.submitSelectedIssues());
expect(submitSelectedIssues).toHaveBeenCalledWith(["risk-1"], "Create the selected issue.");
});
it("cancels review through the timeline", async () => {
const cancelReview = vi.fn(async () => {});
const demo = demoController({
state: { ...initialDemoTimelineState, phase: "review" },
cancelReview,
});
const { result } = renderHook(() => useTimelineAgent(demo, "live"));
await act(async () => result.current.cancelReview());
expect(cancelReview).toHaveBeenCalledWith("Cancelled by operator.");
});
it("disables run when the timeline cannot start", () => {
const demo = demoController({ canStart: false });
const { result } = renderHook(() => useTimelineAgent(demo, "live"));
expect(result.current.canRun).toBe(false);
});
});
@@ -0,0 +1,110 @@
import { useCallback, useMemo, useState } from "react";
import type { DemoTimelineController } from "../useDemoTimeline.js";
import {
agentTextMessage,
agentToolCallPart,
agentToolResultPart,
type AgentMessage,
} from "./events.js";
import type { AgentToolName } from "./tools.js";
export type TimelineAgentMode = "live" | "replay";
export type TimelineAgentController = {
readonly messages: ReadonlyArray<AgentMessage>;
readonly canRun: boolean;
readonly runLabel: string;
readonly runPreparedWorkflow: () => Promise<void>;
readonly submitSelectedIssues: () => Promise<void>;
readonly cancelReview: () => Promise<void>;
};
const DEFAULT_COMMENT = "Create the selected issue.";
const appendToolMessage = (
messages: ReadonlyArray<AgentMessage>,
id: string,
name: AgentToolName,
input: unknown,
output: unknown,
): ReadonlyArray<AgentMessage> => [
...messages,
{
id,
role: "assistant",
parts: [
agentToolCallPart(`${id}-call`, name, input),
agentToolResultPart(`${id}-call`, name, "success", output),
],
},
];
export const useTimelineAgent = (
demo: DemoTimelineController,
modeLabel: TimelineAgentMode,
): TimelineAgentController => {
const [messages, setMessages] = useState<ReadonlyArray<AgentMessage>>([
agentTextMessage(
"timeline-agent-intro",
"assistant",
modeLabel === "live"
? "Live workflow server is available. I can run the prepared workflow now."
: "Replay fallback is active. I can still walk through the prepared workflow evidence.",
),
]);
const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough";
const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running";
const runPreparedWorkflow = useCallback(async () => {
if (!demo.canStart || demo.inFlight) return;
demo.restart();
demo.start();
setMessages((current) => appendToolMessage(
current,
"timeline-agent-start",
"startPreparedReportRun",
{ mode: modeLabel },
{ phase: "started" },
));
}, [demo, modeLabel]);
const selectedIssueIds = useMemo(
() => demo.interruptPayload?.proposed_issues.map((issue) => issue.id) ?? [],
[demo.interruptPayload],
);
const submitSelectedIssues = useCallback(async () => {
if (selectedIssueIds.length === 0) return;
await demo.submitSelectedIssues(selectedIssueIds, DEFAULT_COMMENT);
await demo.next();
setMessages((current) => appendToolMessage(
current,
"timeline-agent-submit",
"resumeIssueReview",
{ selectedIssueIds },
{ outcome: "submitted" },
));
}, [demo, selectedIssueIds]);
const cancelReview = useCallback(async () => {
await demo.cancelReview("Cancelled by operator.");
await demo.next();
setMessages((current) => appendToolMessage(
current,
"timeline-agent-cancel",
"resumeIssueReview",
{},
{ outcome: "cancelled" },
));
}, [demo]);
return {
messages,
canRun,
runLabel,
runPreparedWorkflow,
submitSelectedIssues,
cancelReview,
};
};