Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions server/api/src/agents/core/composeModelConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* `getComposeModelIdsForGraph` unit tests — BYOK validation inputs (#953).
*/
import { describe, expect, it } from "vitest";
import { getComposeModelIdsForGraph } from "./composeModelConfig.js";
import { WIKI_MAINTENANCE_GRAPH_ID } from "../graphs/wikiMaintenance/index.js";

describe("getComposeModelIdsForGraph", () => {
it("returns no model ids for wiki-maintenance (lint-only graph)", () => {
expect(getComposeModelIdsForGraph(WIKI_MAINTENANCE_GRAPH_ID)).toEqual([]);
});
});
3 changes: 3 additions & 0 deletions server/api/src/agents/core/composeModelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Wiki Compose グラフが使うモデル行 ID を解決する(BYOK 検証用)。
*/
import { WIKI_COMPOSE_GRAPH_ID } from "../graphs/wikiCompose/index.js";
import { WIKI_MAINTENANCE_GRAPH_ID } from "../graphs/wikiMaintenance/index.js";
import { getOrchestratorModelId } from "../subgraphs/research/nodes/planQueries.js";
import { RESEARCH_GRAPH_ID } from "../subgraphs/research/index.js";
import { INGEST_PLANNER_GRAPH_ID } from "../graphs/ingest/index.js";
Expand All @@ -19,6 +20,8 @@ function getDraftModelId(): string {
* `createZediChatModel` 経由で呼ばれるモデル行 ID 一覧。
*/
export function getComposeModelIdsForGraph(graphId: string): string[] {
// Lint-only graph — no `createZediChatModel` calls; BYOK must not require orchestrator keys.
if (graphId === WIKI_MAINTENANCE_GRAPH_ID) return [];
if (graphId === WIKI_COMPOSE_GRAPH_ID) {
const orchestrator = getOrchestratorModelId();
const draft = getDraftModelId();
Expand Down
43 changes: 43 additions & 0 deletions src/hooks/useWikiComposeSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,49 @@ describe("useWikiComposeSession", () => {
expect(result.current.phase).toBe("completed");
});

it("submitResearchApproval stops at conflict_resolution without POST /run", async () => {
arrangeRun([
{ type: "started", sessionId: SESSION.id, graphId: SESSION.graphId },
{ type: "done", status: "interrupted" },
]);
mocks.resumeSession.mockResolvedValue({
status: "interrupted",
output: {
__interrupt__: [
{
value: {
kind: "conflict_resolution",
conflicts: {
approved: [{ id: "src:a", title: "A" }],
rejected: [
{ id: "src:b", title: "B" },
{ id: "src:c", title: "C" },
],
rationale: "Confirm approved set.",
},
},
},
],
},
});

const { result } = renderHook(() =>
useWikiComposeSession({ pageId: "page-1", sessionId: null }),
);
await waitFor(() => expect(result.current.session).not.toBeNull());

await act(async () => {
await result.current.submitResearchApproval({
approvedSourceIds: ["src:a"],
rejectedSourceIds: ["src:b", "src:c"],
});
});

expect(mocks.runSession).toHaveBeenCalledTimes(1);
expect(result.current.researchConflictSummary?.approved).toHaveLength(1);
expect(result.current.phase).toBe("conflict");
});

it("submitBrief applies research interrupt from PATCH output without POST /run", async () => {
arrangeRun([
{ type: "started", sessionId: SESSION.id, graphId: SESSION.graphId },
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useWikiComposeSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ export interface UseWikiComposeSessionReturn extends WikiComposeSessionState {
cancel: () => Promise<void>;
}

/** First interrupt kind on a LangGraph checkpoint output, if any. */
function interruptKindFromOutput(output: unknown): string | undefined {
if (!output || typeof output !== "object") return undefined;
const interrupts = (output as { __interrupt__?: unknown }).__interrupt__;
if (!Array.isArray(interrupts) || interrupts.length === 0) return undefined;
const entry = interrupts[0];
const value =
entry && typeof entry === "object" ? (entry as { value?: unknown }).value : undefined;
if (value && typeof value === "object" && "kind" in value) {
const kind = (value as { kind?: unknown }).kind;
return typeof kind === "string" ? kind : undefined;
}
return undefined;
}

/**
* Returns a unique id for an activity row. Uses crypto.randomUUID when
* available (modern browsers); falls back to a coarse fallback for old
Expand Down Expand Up @@ -496,6 +511,7 @@ function reduceInterrupt(
return {
outlineProposal: payload.outline,
approvedSources: payload.approvedSources,
researchConflictSummary: null,
phase: "structure",
};
case "conflict_resolution":
Expand Down Expand Up @@ -649,6 +665,11 @@ export function useWikiComposeSession(
const approved = state.pendingSources.filter((s) => input.approvedSourceIds.includes(s.id));
update({ approvedSources: approved });
const result = await resumeSession({ pageId, sessionId: session.id, resume: input });
if (interruptKindFromOutput(result.output) === "conflict_resolution") {
const fromResume = reduceResumeOutput(result.output, result.status);
update({ status: result.status, ...fromResume });
return;
}
const fromResume = reduceResumeOutput(result.output, result.status);
update({ status: result.status, ...fromResume });
const needsStream =
Expand Down Expand Up @@ -758,6 +779,7 @@ export function useWikiComposeSession(
submitResearchApproval,
submitConflictAck,
submitOutline,
submitConflictAck,
cancel,
};
}
1 change: 1 addition & 0 deletions src/lib/wikiCompose/composeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export async function runSession(input: {
* - `human_review_brief` — `{ answers, appendToExisting?, researchMaxIterations? }`
* - `human_review_research` — `{ approvedSourceIds, rejectedSourceIds?, note? }`
* - `human_review_outline` — `{ sections }`
* - `conflict_resolution` — `{ acknowledged: true, note?: string }`
*
* The server returns a JSON body `{ status, output }` on resume completion (no
* SSE stream). Callers must hydrate UI state from `output` (interrupt payloads
Expand Down
1 change: 1 addition & 0 deletions src/pages/WikiComposePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ const WikiComposePage: React.FC = () => {
onSubmitResearchApproval={session.submitResearchApproval}
onSubmitConflictAck={session.submitConflictAck}
onSubmitOutline={session.submitOutline}
onSubmitConflictAck={session.submitConflictAck}
/>
);

Expand Down
Loading