Repository navigation
feat(console): add optional Agent workflow demo #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1399af6
03ceeaf
cdd4062
25f7c18
4f7b372
1afd1e1
be4c577
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { apiGet, apiPost, apiPut } from "@/utils/request"; | ||
| import { agentApi } from "@/api/agent"; | ||
|
|
||
| vi.mock("@/utils/request", () => ({ | ||
| apiGet: vi.fn(), | ||
| apiPost: vi.fn(), | ||
| apiPut: vi.fn(), | ||
| })); | ||
|
|
||
| describe("agentApi", () => { | ||
| beforeEach(() => vi.clearAllMocks()); | ||
|
|
||
| it("uses the authenticated Agent workflow endpoints and explicit confirmation payload", async () => { | ||
| const thread = { threadId: "thread-1" }; | ||
| vi.mocked(apiPost).mockResolvedValue(thread); | ||
| vi.mocked(apiPut).mockResolvedValue(thread); | ||
| vi.mocked(apiGet).mockResolvedValue(thread); | ||
|
|
||
| await agentApi.createThread("submission-1", "review this"); | ||
| expect(apiPost).toHaveBeenCalledWith("/agent/threads", { | ||
| sourceSubmissionId: "submission-1", | ||
| question: "review this", | ||
| }); | ||
| await agentApi.confirm("thread-1", { | ||
| draftVersion: 2, | ||
| title: "Title", | ||
| content: "Content", | ||
| facts: [], | ||
| hypotheses: [], | ||
| citations: [], | ||
| citationChecks: [], | ||
| sampleScope: null, | ||
| }, "digest"); | ||
| expect(apiPost).toHaveBeenLastCalledWith("/agent/threads/thread-1/confirm", { | ||
| draftVersion: 2, | ||
| paramsDigest: "digest", | ||
| confirm: true, | ||
| }); | ||
| await agentApi.save("thread-1", "confirmation-1"); | ||
| expect(apiPost).toHaveBeenLastCalledWith("/agent/threads/thread-1/save", { | ||
| confirmationId: "confirmation-1", | ||
| }); | ||
| await agentApi.recover("thread-1"); | ||
| expect(apiPost).toHaveBeenLastCalledWith("/agent/threads/thread-1/recover", { retry: false }); | ||
| await agentApi.cancel("thread-1"); | ||
| expect(apiPost).toHaveBeenLastCalledWith("/agent/threads/thread-1/cancel", {}); | ||
| await agentApi.updateDraft("thread-1", { draftVersion: 2, title: "Title", content: "Content" }); | ||
| expect(apiPut).toHaveBeenCalledWith("/agent/threads/thread-1/draft", { | ||
| draftVersion: 2, | ||
| title: "Title", | ||
| content: "Content", | ||
| }); | ||
| }); | ||
|
|
||
| it("encodes thread IDs for reads and event streams", async () => { | ||
| vi.mocked(apiGet).mockResolvedValue({}); | ||
| await agentApi.getThread("thread/with space"); | ||
| expect(apiGet).toHaveBeenCalledWith("/agent/threads/thread%2Fwith%20space"); | ||
| expect(agentApi.eventStreamUrl("thread/with space")).toBe( | ||
| "/api/agent/threads/thread%2Fwith%20space/events/stream?after=0", | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { apiGet, apiPost, apiPut } from "@/utils/request"; | ||
|
|
||
| export type AgentThreadStatus = | ||
| | "awaiting_confirmation" | ||
| | "analyzing" | ||
| | "confirmed" | ||
| | "saving" | ||
| | "saved" | ||
| | "unknown" | ||
| | "failed" | ||
| | "cancelled"; | ||
|
|
||
| export interface AgentCitation { | ||
| claim: string; | ||
| chunk_id: string; | ||
| doc_id: string; | ||
| version: string; | ||
| source_path: string; | ||
| source_position: string; | ||
| access_scope: string; | ||
| sample_kind: string; | ||
| source_trust: string; | ||
| text: string; | ||
| } | ||
|
|
||
| export interface AgentWorkflowEvent { | ||
| seq: number; | ||
| kind: string; | ||
| detail: Record<string, unknown>; | ||
| createdAt: number; | ||
| } | ||
|
|
||
| export interface AgentThread { | ||
| threadId: string; | ||
| runId: string; | ||
| status: AgentThreadStatus; | ||
| sourceSubmissionId: string; | ||
| question: string; | ||
| draft: { | ||
| draftVersion: number; | ||
| title: string; | ||
| content: string; | ||
| facts: unknown[]; | ||
| hypotheses: unknown[]; | ||
| citations: AgentCitation[]; | ||
| citationChecks: Array<Record<string, unknown>>; | ||
| sampleScope: string | null; | ||
| model_answer?: string; | ||
| }; | ||
| paramsDigest: string; | ||
| analysis: { | ||
| answer?: string; | ||
| trace?: Array<{ tool_name?: string; failed?: boolean }>; | ||
| citation_checks?: Array<Record<string, unknown>>; | ||
| } | null; | ||
| confirmation: { | ||
| id?: string; | ||
| action?: string; | ||
| draftVersion?: number; | ||
| paramsDigest?: string; | ||
| expiresAt?: number; | ||
| } | null; | ||
| receipt: { | ||
| planId: string | null; | ||
| cancelRequested: boolean; | ||
| failureReason: string | null; | ||
| }; | ||
| } | ||
|
|
||
| const threadPath = (threadId: string) => | ||
| `/agent/threads/${encodeURIComponent(threadId)}`; | ||
|
|
||
| export const agentApi = { | ||
| createThread(sourceSubmissionId: string, question: string) { | ||
| return apiPost<AgentThread>("/agent/threads", { sourceSubmissionId, question }); | ||
| }, | ||
| getThread(threadId: string) { | ||
| return apiGet<AgentThread>(threadPath(threadId)); | ||
| }, | ||
| analyze(threadId: string) { | ||
| return apiPost<AgentThread>(`${threadPath(threadId)}/analyze`, {}); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If analysis completes server-side but its response is lost or hits the shared 30-second client timeout, this call is automatically retried up to twice by Useful? React with 👍 / 👎. |
||
| }, | ||
| updateDraft( | ||
| threadId: string, | ||
| draft: { draftVersion: number; title: string; content: string }, | ||
| ) { | ||
| return apiPut<AgentThread>(`${threadPath(threadId)}/draft`, draft); | ||
| }, | ||
| confirm(threadId: string, draft: AgentThread["draft"], paramsDigest: string) { | ||
| return apiPost<AgentThread>(`${threadPath(threadId)}/confirm`, { | ||
| draftVersion: draft.draftVersion, | ||
| paramsDigest, | ||
| confirm: true, | ||
| }); | ||
| }, | ||
| save(threadId: string, confirmationId: string) { | ||
| return apiPost<AgentThread>(`${threadPath(threadId)}/save`, { confirmationId }); | ||
| }, | ||
| recover(threadId: string) { | ||
| return apiPost<AgentThread>(`${threadPath(threadId)}/recover`, { retry: false }); | ||
|
Comment on lines
+99
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a save reaches AGENTS.md reference: AGENTS.md:L39-L39 Useful? React with 👍 / 👎. |
||
| }, | ||
| cancel(threadId: string) { | ||
| return apiPost<AgentThread>(`${threadPath(threadId)}/cancel`, {}); | ||
| }, | ||
| eventStreamUrl(threadId: string) { | ||
| const base = (import.meta.env.VITE_API_BASE_URL || "/api").replace(/\/$/, ""); | ||
| return `${base}${threadPath(threadId)}/events/stream?after=0`; | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the server commits the create but its response is lost or replaced by a 5xx, the shared HTTP client automatically retries this POST up to twice. The create endpoint generates a fresh thread ID and business key on every invocation and does not deduplicate by request ID, so one click can leave multiple owner threads while the UI tracks only the response from the last successful attempt. Pass
{ retry: 0 }for creation or add a server-recognized idempotency key.Useful? React with 👍 / 👎.