From 8e18e197f8976789edfcd6932b38d9784869697e Mon Sep 17 00:00:00 2001 From: anandpant <109482096+anandpant@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:55:30 +0000 Subject: [PATCH] feat(playground): Studio chat builds sequence diagrams (#377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed - Studio chat (`/api/chat`, the playground home) exposes a `build_sequence_diagram` tool next to `build_flowchart`. Its input is the new `BuildSequenceDiagramToolInputSchema` (sequence spec without style or artifact options). Its output is the canonical `BuildSequenceDiagramResult`. It runs on the shared Code Mode `buildSequenceDiagram` vertical. - Both tools share a per-turn executor, `makeStudioBuildToolExecutor`: a three-attempt budget per tool, host-injected artifact options, reuse of the accepted result, and optional grounding (flowchart logo placement stays as is). `MAX_FLOWCHART_BUILD_ATTEMPTS` is now `MAX_DIAGRAM_BUILD_ATTEMPTS`. - The agent system prompt picks the family: sequence for time-ordered interactions, and always when a sequence diagram is asked for. It adds sequence craft rules: participants, chronological messages, and returns that close calls (which draws activation bars). - The playground UI reads either tool part for progress titles ("Drawing your sequence diagram"), participant ghost labels while drawing, result details, failure copy (`invalid_sequence`), the stage, and downloads. One starter now asks for a sequence diagram. - The chat repair retry metric and log are renamed to `sketchi_chat_build_retries` and "Retrying Studio diagram repair", with an `operation` label. The telemetry log allowlist is updated to match. ## Why #280 asks that natural-language generation in Studio chat handle sequence explicitly. Before this, the only thing Studio could build was a flowchart. ## Verification - `pnpm nx affected -t typecheck,test --base=origin/main` (18 projects) - New tests: sequence tool contracts, build with host options and accepted-result reuse against the real Code Mode runtime, repair issues and attempt cap, policy routing, and UI selectors for sequence tool parts - The live Studio flow in Chrome is checked at the end of the stack, with real model calls. Refs #280 ## Review follow-ups - **One budget per turn:** both tools share one `StudioBuildTurn`, with one attempt counter (three total), one accepted artifact, and one build at a time. A call to the other tool after acceptance returns a rejection without building. Tests: a flowchart is accepted, then a sequence call causes 0 new writes; a mixed retry sequence caps at three attempts. - **Routing:** a type the user names wins over the topic in both directions ("a flowchart of an OAuth handshake is still a flowchart"); with no type named, content decides. `pnpm eval:studio-routing` (`tools/studio-routing-eval.ts`) runs four maintained cases (named or unnamed type × either family) against a live playground's `/api/chat` and scores the first build tool called. Its stream parsing and case coverage are unit-tested. - **Size limits:** `SEQUENCE_MAX_PARTICIPANTS = 12` and `SEQUENCE_MAX_MESSAGES = 40` in core validation, reported as Code Mode `sequence_too_large` (registered as a post-baseline code in the compatibility corpus). The issue list is capped at 20. The limits sit above the prompt's craft targets (2–8 participants, ≤30 messages) so Code Mode and MCP callers keep headroom: 12 columns are about 3,800px wide and stay well under canvas element limits. The prompts now state both the target and the limit. - **Stopped calls:** a tool call left in an input state when its run is no longer active shows "Stopped drawing your …" with a Stopped badge, for both families (`diagramToolCardStatus`, tested). --- .../src/components/ai-elements/tool.tsx | 12 +- .../features/playground/build-result.test.ts | 117 ++++++++++- .../src/features/playground/build-result.ts | 110 ++++++++-- .../src/features/playground/surface.tsx | 31 ++- apps/playground/src/routes/index.tsx | 50 +++-- .../src/server/chat/agent.server.test.ts | 4 +- .../src/server/chat/agent.server.ts | 40 ++-- .../server/chat/studio-build-tool.server.ts | 168 +++++++++++++++ .../chat/studio-flowchart-tool.server.test.ts | 17 +- .../chat/studio-flowchart-tool.server.ts | 118 ++--------- .../chat/studio-sequence-tool.server.test.ts | 194 ++++++++++++++++++ .../chat/studio-sequence-tool.server.ts | 39 ++++ .../src/server/codemode/mcp-docs/catalog.ts | 1 + .../mcp-docs/code-mode-types.generated.txt | 14 +- package.json | 1 + packages/diagram/agent/README.md | 7 +- .../code-mode/compatibility-corpus.test.ts | 9 +- .../agent/src/lib/code-mode/contract.ts | 18 ++ .../agent/src/lib/code-mode/runtime.test.ts | 23 +++ .../agent/src/lib/code-mode/runtime.ts | 1 + packages/diagram/agent/src/lib/policy.test.ts | 19 +- packages/diagram/agent/src/lib/policy.ts | 22 +- .../diagram/core/src/types/sequence.test.ts | 26 +++ packages/diagram/core/src/types/sequence.ts | 24 ++- .../diagram/generation/src/lib/messages.ts | 1 + .../observability/src/lib/effect-telemetry.ts | 2 +- tools/project-graph.test.ts | 2 + tools/studio-routing-eval.test.ts | 26 +++ tools/studio-routing-eval.ts | 128 ++++++++++++ 29 files changed, 1009 insertions(+), 215 deletions(-) create mode 100644 apps/playground/src/server/chat/studio-build-tool.server.ts create mode 100644 apps/playground/src/server/chat/studio-sequence-tool.server.test.ts create mode 100644 apps/playground/src/server/chat/studio-sequence-tool.server.ts create mode 100644 tools/studio-routing-eval.test.ts create mode 100644 tools/studio-routing-eval.ts diff --git a/apps/playground/src/components/ai-elements/tool.tsx b/apps/playground/src/components/ai-elements/tool.tsx index 53200df8..d9730054 100644 --- a/apps/playground/src/components/ai-elements/tool.tsx +++ b/apps/playground/src/components/ai-elements/tool.tsx @@ -35,6 +35,8 @@ export type ToolPart = ToolUIPart | DynamicToolUIPart; export type ToolHeaderProps = { title?: string; className?: string; + /** The run ended before this call finished; show that instead of its live state. */ + stopped?: boolean; } & ( | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never } | { @@ -86,6 +88,7 @@ export const ToolHeader = ({ title, type, state, + stopped, toolName, ...props }: ToolHeaderProps) => { @@ -103,7 +106,14 @@ export const ToolHeader = ({
{title ?? derivedName} - {getStatusBadge(state)} + {stopped ? ( + + + Stopped + + ) : ( + getStatusBadge(state) + )}
diff --git a/apps/playground/src/features/playground/build-result.test.ts b/apps/playground/src/features/playground/build-result.test.ts index 29fd2d52..b2d4688e 100644 --- a/apps/playground/src/features/playground/build-result.test.ts +++ b/apps/playground/src/features/playground/build-result.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from "vitest"; import type { BuildFlowchartResult } from "@sketchi/diagram-agent"; import type { UIMessage } from "ai"; -import { buildResultOf, deriveBuildState, type FlowchartToolPart } from "./build-result"; +import { + buildResultOf, + deriveBuildState, + diagramToolCardStatus, + type DiagramToolPart, +} from "./build-result"; import { DEPLOY_PIPELINE_SCENE, DEPLOY_PIPELINE_SPEC } from "./deploy-pipeline-sample"; function pendingMessage(id: string): UIMessage { @@ -40,7 +45,7 @@ describe("build result selectors", () => { expect(state.ghostLabels).toEqual(DEPLOY_PIPELINE_SPEC.nodes.map((node) => node.label)); }); it("rejects malformed accepted payloads before selecting formats", () => { - const part: FlowchartToolPart = { + const part: DiagramToolPart = { type: "tool-build_flowchart", toolCallId: "bad", state: "output-available", @@ -131,4 +136,112 @@ describe("build result selectors", () => { }), ).toMatchObject({ ok: false, status: "storage_failed" }); }); + + it("reads sequence build parts: participant ghosts while drawing, then the accepted scene", () => { + const spec = { + title: "Checkout", + participants: [ + { id: "browser", label: "Browser" }, + { id: "api", label: "API" }, + ], + messages: [{ source: "browser", target: "api", label: "Checkout" }], + }; + const drawing: UIMessage = { + id: "drawing", + role: "assistant", + parts: [ + { + type: "tool-build_sequence_diagram", + toolCallId: "sequence", + state: "input-available", + input: { spec }, + }, + ], + }; + expect(deriveBuildState([drawing], true).ghostLabels).toEqual(["Browser", "API"]); + + const accepted: UIMessage = { + id: "accepted", + role: "assistant", + parts: [ + { + type: "tool-build_sequence_diagram", + toolCallId: "sequence", + state: "output-available", + input: { spec }, + output: { + ok: true, + status: "accepted", + buildId: "build", + issues: [], + normalizedSpec: { + id: "checkout", + title: "Checkout", + participants: spec.participants, + messages: [{ id: "m1", source: "browser", target: "api", label: "Checkout" }], + style: { accentColor: "#8f707f", backgroundColor: "#fffdf8" }, + }, + quality: { + accepted: true, + checks: [], + score: 10, + threshold: 8, + summary: { nodeCount: 2, edgeCount: 1 }, + }, + artifact: { + artifactId: "sequence-artifact", + diagramId: "checkout", + formats: [ + { format: "scene", mimeType: "application/json", inline: DEPLOY_PIPELINE_SCENE }, + { format: "excalidraw", mimeType: "application/json" }, + ], + }, + }, + }, + ], + }; + const state = deriveBuildState([accepted], false); + expect(state.buildMode).toBe(true); + expect(state.acceptedResult?.ok).toBe(true); + expect(state.scene).toEqual(DEPLOY_PIPELINE_SCENE); + expect(state.artifact?.viewUrl).toBe("/artifacts/sequence-artifact"); + expect( + buildResultOf({ + type: "tool-build_sequence_diagram", + toolCallId: "rejected", + state: "output-available", + output: { ok: false, status: "invalid_sequence", issues: [] }, + }), + ).toMatchObject({ ok: false, status: "invalid_sequence" }); + }); + + it("titles a call left waiting after Stop as stopped, for both families", () => { + for (const [type, family] of [ + ["tool-build_flowchart", "flowchart"], + ["tool-build_sequence_diagram", "sequence diagram"], + ] as const) { + for (const state of ["input-streaming", "input-available"] as const) { + const part: DiagramToolPart = { type, toolCallId: "call", state, input: {} }; + expect(diagramToolCardStatus(part, false)).toEqual({ + stopped: true, + title: `Stopped drawing your ${family}`, + }); + expect(diagramToolCardStatus(part, true)).toEqual({ + stopped: false, + title: `${state === "input-streaming" ? "Drawing" : "Checking"} your ${family}`, + }); + } + } + expect( + diagramToolCardStatus( + { + type: "tool-build_sequence_diagram", + toolCallId: "done", + state: "output-available", + output: { ok: false, status: "invalid_sequence", issues: [] }, + }, + false, + ), + ).toEqual({ stopped: false, title: "Diagram needs changes" }); + }); }); diff --git a/apps/playground/src/features/playground/build-result.ts b/apps/playground/src/features/playground/build-result.ts index 59207a92..ddd1d2e0 100644 --- a/apps/playground/src/features/playground/build-result.ts +++ b/apps/playground/src/features/playground/build-result.ts @@ -1,8 +1,11 @@ import { BuildFlowchartRequestSchema, BuildFlowchartResultSchema, + BuildSequenceDiagramResultSchema, + BuildSequenceDiagramToolInputSchema, RenderedDiagramSceneSchema, type BuildFlowchartResult, + type BuildSequenceDiagramResult, } from "@sketchi/diagram-agent"; import type { RenderedDiagramScene } from "@sketchi/diagram-renderer"; import { Result, Schema } from "effect"; @@ -11,8 +14,41 @@ import type { ReadyPlaygroundArtifact } from "./surface"; type MessagePart = UIMessage["parts"][number]; -export interface FlowchartToolPart { - type: "tool-build_flowchart"; +/** A canonical build result from any Studio build tool. */ +export type StudioBuildResult = BuildFlowchartResult | BuildSequenceDiagramResult; + +const BUILD_TOOLS = { + "tool-build_flowchart": { + family: "flowchart", + decodeResult: Schema.decodeUnknownResult(BuildFlowchartResultSchema), + labels: (input: unknown) => { + const decoded = Schema.decodeUnknownResult(BuildFlowchartRequestSchema, { + errors: "all", + reportInput: true, + })(input); + return Result.isSuccess(decoded) ? decoded.success.spec.nodes.map((node) => node.label) : []; + }, + }, + "tool-build_sequence_diagram": { + family: "sequence", + decodeResult: Schema.decodeUnknownResult(BuildSequenceDiagramResultSchema), + labels: (input: unknown) => { + const decoded = Schema.decodeUnknownResult(BuildSequenceDiagramToolInputSchema, { + errors: "all", + reportInput: true, + })(input); + return Result.isSuccess(decoded) + ? decoded.success.spec.participants.map((participant) => participant.label) + : []; + }, + }, +} as const; + +export type DiagramToolPartType = keyof typeof BUILD_TOOLS; +export type DiagramToolFamily = (typeof BUILD_TOOLS)[DiagramToolPartType]["family"]; + +export interface DiagramToolPart { + type: DiagramToolPartType; toolCallId: string; state: "input-streaming" | "input-available" | "output-available" | "output-error"; input?: unknown; @@ -20,19 +56,54 @@ export interface FlowchartToolPart { errorText?: string; } -export function isFlowchartToolPart(part: MessagePart): part is FlowchartToolPart & MessagePart { - return part.type === "tool-build_flowchart"; +export function isDiagramToolPart(part: MessagePart): part is DiagramToolPart & MessagePart { + return Object.hasOwn(BUILD_TOOLS, part.type); +} + +export function diagramToolFamily(part: DiagramToolPart): DiagramToolFamily { + return BUILD_TOOLS[part.type].family; } -const decodeBuildResult = Schema.decodeUnknownResult(BuildFlowchartResultSchema); +const FAMILY_NAMES: Readonly> = { + flowchart: "flowchart", + sequence: "sequence diagram", +}; -export function buildResultOf(part: FlowchartToolPart): BuildFlowchartResult | undefined { +/** + * The tool card heading. A call still waiting for input when its run is no + * longer active was stopped, and never finishes drawing. + */ +export function diagramToolCardStatus( + part: DiagramToolPart, + active: boolean, +): { readonly stopped: boolean; readonly title: string } { + const family = FAMILY_NAMES[diagramToolFamily(part)]; + if (part.state === "input-streaming" || part.state === "input-available") { + if (!active) return { stopped: true, title: `Stopped drawing your ${family}` }; + return { + stopped: false, + title: + part.state === "input-streaming" ? `Drawing your ${family}` : `Checking your ${family}`, + }; + } + if (part.state === "output-error") { + return { stopped: false, title: "Couldn’t finish the diagram" }; + } + return { + stopped: false, + title: buildResultOf(part)?.ok ? "Diagram ready" : "Diagram needs changes", + }; +} + +export function buildResultOf(part: DiagramToolPart): StudioBuildResult | undefined { if (part.state !== "output-available") return undefined; - const decoded = decodeBuildResult(part.output); + const decoded: Result.Result = BUILD_TOOLS[part.type].decodeResult( + part.output, + ); return Result.isSuccess(decoded) ? decoded.success : undefined; } -export function artifactFromResponse(result: BuildFlowchartResult): ReadyPlaygroundArtifact | null { +export function artifactFromResponse(result: StudioBuildResult): ReadyPlaygroundArtifact | null { if (!result.ok) { return null; } @@ -64,7 +135,7 @@ function isRenderedDiagramScene(value: unknown): value is RenderedDiagramScene { } export function sceneFromResult( - result: BuildFlowchartResult | undefined, + result: StudioBuildResult | undefined, ): RenderedDiagramScene | null { if (!result?.ok) { return null; @@ -74,9 +145,9 @@ export function sceneFromResult( } export interface PlaygroundBuildState { - activePart: FlowchartToolPart | undefined; - acceptedResult: BuildFlowchartResult | undefined; - displayResult: BuildFlowchartResult | undefined; + activePart: DiagramToolPart | undefined; + acceptedResult: StudioBuildResult | undefined; + displayResult: StudioBuildResult | undefined; buildMode: boolean; scene: RenderedDiagramScene | null; artifact: ReadyPlaygroundArtifact | null; @@ -87,23 +158,20 @@ export function deriveBuildState( messages: readonly UIMessage[], busy: boolean, ): PlaygroundBuildState { - const toolParts = messages.flatMap((message) => message.parts.filter(isFlowchartToolPart)); + const toolParts = messages.flatMap((message) => message.parts.filter(isDiagramToolPart)); const results = toolParts.map(buildResultOf).filter((result) => result !== undefined); const displayResult = results.at(-1); const acceptedResult = [...results].reverse().find((result) => result.ok); const latestAssistant = messages.findLast((message) => message.role === "assistant"); const activePart = busy ? latestAssistant?.parts - .filter(isFlowchartToolPart) + .filter(isDiagramToolPart) .find((part) => part.state === "input-streaming" || part.state === "input-available") : undefined; - const input = Schema.decodeUnknownResult(BuildFlowchartRequestSchema, { - errors: "all", - reportInput: true, - })(activePart?.input); - const ghostLabels = Result.isSuccess(input) - ? input.success.spec.nodes - .map((node) => node.label.trim()) + const ghostLabels = activePart + ? BUILD_TOOLS[activePart.type] + .labels(activePart.input) + .map((label) => label.trim()) .filter((label) => label.length > 0) .slice(0, 24) : []; diff --git a/apps/playground/src/features/playground/surface.tsx b/apps/playground/src/features/playground/surface.tsx index 25a5a51a..af33aa56 100644 --- a/apps/playground/src/features/playground/surface.tsx +++ b/apps/playground/src/features/playground/surface.tsx @@ -1,4 +1,4 @@ -import type { BuildFlowchartResult } from "@sketchi/diagram-agent"; +import type { BuildFlowchartResult, BuildSequenceDiagramResult } from "@sketchi/diagram-agent"; import { DiagramPreview } from "@sketchi/diagram-ui"; import type { ChatStatus, UIMessage } from "ai"; import type { RefObject } from "react"; @@ -19,9 +19,11 @@ import { DEPLOY_PIPELINE_SCENE } from "./deploy-pipeline-sample"; const STARTERS = [ "Sketch a login flow with retries and a fraud check", "Diagram a CI/CD pipeline from commit to production", - "Map the data flow for an AI chat app with streaming", + "Show checkout as a sequence diagram: browser, API, payments", ]; +type StudioBuildResult = BuildFlowchartResult | BuildSequenceDiagramResult; + export interface ReadyPlaygroundArtifact { artifactId: string; exportUrls: { @@ -58,7 +60,7 @@ export function ArtifactActions({ artifact }: { artifact: ReadyPlaygroundArtifac ); } -function resultSummary(result: BuildFlowchartResult): { +function resultSummary(result: StudioBuildResult): { connectionCount?: number; itemCount?: number; score?: number; @@ -70,22 +72,14 @@ function resultSummary(result: BuildFlowchartResult): { score: result.quality.score, }; } - if (result.normalizedSpec) { - return { - connectionCount: result.normalizedSpec.edges.length, - itemCount: result.normalizedSpec.nodes.length, - }; - } - return {}; + const spec = result.normalizedSpec; + if (!spec) return {}; + return "participants" in spec + ? { connectionCount: spec.messages.length, itemCount: spec.participants.length } + : { connectionCount: spec.edges.length, itemCount: spec.nodes.length }; } -export function BuildResultDetails({ - pass, - result, -}: { - pass: number; - result: BuildFlowchartResult; -}) { +export function BuildResultDetails({ pass, result }: { pass: number; result: StudioBuildResult }) { const summary = resultSummary(result); const guidance = result.ok ? null : failureCopy(result.status); @@ -137,7 +131,7 @@ export function BuildResultDetails({ ); } -function failureCopy(status: Exclude["status"]): { +function failureCopy(status: Exclude["status"]): { hint: string; message: string; } { @@ -148,6 +142,7 @@ function failureCopy(status: Exclude["status hint: "Describe the main steps and how they connect, then try again.", }; case "invalid_flowchart": + case "invalid_sequence": return { message: "This draft needs a clearer structure before it can be drawn.", hint: "Clarify the steps or simplify the connections, then try again.", diff --git a/apps/playground/src/routes/index.tsx b/apps/playground/src/routes/index.tsx index 66c3261a..f26d7578 100644 --- a/apps/playground/src/routes/index.tsx +++ b/apps/playground/src/routes/index.tsx @@ -1,5 +1,4 @@ import { useChat } from "@ai-sdk/react"; -import { type BuildFlowchartResult } from "@sketchi/diagram-agent"; import type { RenderedDiagramScene } from "@sketchi/diagram-renderer"; import { DiagramPreview } from "@sketchi/diagram-ui"; import { createFileRoute } from "@tanstack/react-router"; @@ -34,8 +33,10 @@ import { import { buildResultOf, deriveBuildState, - isFlowchartToolPart, - type FlowchartToolPart, + diagramToolCardStatus, + isDiagramToolPart, + type DiagramToolPart, + type StudioBuildResult, } from "@/features/playground/build-result"; import { cn } from "@/lib/utils"; @@ -43,22 +44,21 @@ export const Route = createFileRoute("/")({ component: StudioRoute, }); -function FlowchartToolCard({ attempt, part }: { attempt: number; part: FlowchartToolPart }) { +function DiagramToolCard({ + active, + attempt, + part, +}: { + active: boolean; + attempt: number; + part: DiagramToolPart; +}) { const result = buildResultOf(part); - const title = - part.state === "input-streaming" - ? "Drawing your flowchart" - : part.state === "input-available" - ? "Checking your flowchart" - : part.state === "output-error" - ? "Couldn’t finish the diagram" - : result?.ok - ? "Diagram ready" - : "Diagram needs changes"; + const { stopped, title } = diagramToolCardStatus(part, active); return ( - + {result ? : null} {part.input === undefined ? null : } @@ -73,7 +73,11 @@ function FlowchartToolCard({ attempt, part }: { attempt: number; part: Flowchart ); } -function renderAssistantParts(message: UIMessage, onCompose?: () => void): ReactNode[] { +function renderAssistantParts( + message: UIMessage, + active: boolean, + onCompose?: () => void, +): ReactNode[] { const nodes: ReactNode[] = []; let attempt = 0; @@ -99,9 +103,11 @@ function renderAssistantParts(message: UIMessage, onCompose?: () => void): React return; } - if (isFlowchartToolPart(part)) { + if (isDiagramToolPart(part)) { attempt += 1; - nodes.push(); + nodes.push( + , + ); } }); @@ -130,8 +136,8 @@ function StagePlaceholder({

{generating - ? "Drawing your flowchart…" - : "This version needs a few changes. Ask Sketchi to revise it or try another flow."} + ? "Drawing your diagram…" + : "This version needs a few changes. Ask Sketchi to revise it or try another request."}

{ghostLabels.length > 0 ? (
@@ -156,7 +162,7 @@ function DiagramStage({ artifact: ReadyPlaygroundArtifact | null; generating: boolean; ghostLabels: string[]; - result: BuildFlowchartResult | undefined; + result: StudioBuildResult | undefined; scene: RenderedDiagramScene | null; }) { const title = scene?.title ?? result?.normalizedSpec?.title ?? "Warming up the pencil"; @@ -285,6 +291,8 @@ function StudioRoute() { const parts = renderAssistantParts( message, + // Only the latest assistant message can still be drawing. + busy && message.id === latestMessage?.id, message.id === answerableMessageId ? focusComposer : undefined, ); return parts.length > 0 ? ( diff --git a/apps/playground/src/server/chat/agent.server.test.ts b/apps/playground/src/server/chat/agent.server.test.ts index e41d1eea..0b924af1 100644 --- a/apps/playground/src/server/chat/agent.server.test.ts +++ b/apps/playground/src/server/chat/agent.server.test.ts @@ -5,7 +5,7 @@ import type { BuildFlowchartResult } from "@sketchi/diagram-agent"; import { PlaygroundRequestMetadata } from "../runtime/context.server"; import { PlaygroundRequestCallbacks, runPlaygroundEffect } from "../runtime/runtime.server"; -import { handleStudioAgentRequest, makeStudioFlowchartToolCallback } from "./agent.server"; +import { handleStudioAgentRequest, makeStudioBuildToolCallback } from "./agent.server"; const repairResult: BuildFlowchartResult = { ok: false, @@ -45,7 +45,7 @@ describe("Studio agent request callbacks", () => { Effect.gen(function* () { const callbacks = yield* PlaygroundRequestCallbacks; const requestContext = yield* observeContext; - const execute = makeStudioFlowchartToolCallback( + const execute = makeStudioBuildToolCallback( { execute: () => observeContext.pipe( diff --git a/apps/playground/src/server/chat/agent.server.ts b/apps/playground/src/server/chat/agent.server.ts index c0280023..ce69194f 100644 --- a/apps/playground/src/server/chat/agent.server.ts +++ b/apps/playground/src/server/chat/agent.server.ts @@ -4,8 +4,6 @@ import { DIAGRAM_AGENT_TEMPERATURE, MAX_AGENT_OUTPUT_TOKENS, MAX_AGENT_STEPS, - MAX_FLOWCHART_BUILD_ATTEMPTS, - type BuildFlowchartResult, } from "@sketchi/diagram-agent"; import { convertToModelMessages, stepCountIs, streamText, tool, validateUIMessages } from "ai"; import { Effect, Schema } from "effect"; @@ -26,8 +24,15 @@ import { StudioBuildFlowchartInputSchema, StudioBuildFlowchartOutputSchema, studioSystemPrompt, - type StudioBuildFlowchartInput, } from "./studio-flowchart-tool.server"; +import { + makeStudioSequenceToolExecutor, + STUDIO_BUILD_SEQUENCE_TOOL_DESCRIPTION, + STUDIO_BUILD_SEQUENCE_TOOL_NAME, + StudioBuildSequenceInputSchema, + StudioBuildSequenceOutputSchema, +} from "./studio-sequence-tool.server"; +import { makeStudioBuildTurn } from "./studio-build-tool.server"; export class StudioAgentRequestError extends Schema.TaggedError()( "StudioAgentRequestError", @@ -45,15 +50,14 @@ const ChatRequestSchema = Schema.Struct({ messages: Schema.Array(Schema.Unknown), }); -export function makeStudioFlowchartToolCallback( +/** Run a Studio build tool's effect inside the chat request's runtime. */ +export function makeStudioBuildToolCallback( executor: { - readonly execute: ( - input: StudioBuildFlowchartInput, - ) => PlaygroundCallbackEffect; + readonly execute: (input: Input) => PlaygroundCallbackEffect; }, runToolEffect: PlaygroundRequestRunner, ) { - return (input: StudioBuildFlowchartInput) => runToolEffect(executor.execute(input)); + return (input: Input) => runToolEffect(executor.execute(input)); } function userText( @@ -128,11 +132,15 @@ const handleStudioAgentRequestWorkflow = Effect.fn("playground.http.chat")(funct }); // Logos come only from what the user wrote, across the conversation. const logos = logosNamedIn(userText(messages)); - const executor = yield* makeStudioFlowchartToolExecutor( - codeMode.buildFlowchart, - MAX_FLOWCHART_BUILD_ATTEMPTS, + // One budget for the turn: both tools share attempts and the accepted artifact. + const turn = yield* makeStudioBuildTurn(); + const flowchartExecutor = yield* makeStudioFlowchartToolExecutor(codeMode.buildFlowchart, { logos, - ); + turn, + }); + const sequenceExecutor = yield* makeStudioSequenceToolExecutor(codeMode.buildSequenceDiagram, { + turn, + }); return yield* Effect.try({ try: () => { @@ -145,7 +153,13 @@ const handleStudioAgentRequestWorkflow = Effect.fn("playground.http.chat")(funct description: STUDIO_BUILD_FLOWCHART_TOOL_DESCRIPTION, inputSchema: StudioBuildFlowchartInputSchema, outputSchema: StudioBuildFlowchartOutputSchema, - execute: makeStudioFlowchartToolCallback(executor, callbacks.runPromise), + execute: makeStudioBuildToolCallback(flowchartExecutor, callbacks.runPromise), + }), + [STUDIO_BUILD_SEQUENCE_TOOL_NAME]: tool({ + description: STUDIO_BUILD_SEQUENCE_TOOL_DESCRIPTION, + inputSchema: StudioBuildSequenceInputSchema, + outputSchema: StudioBuildSequenceOutputSchema, + execute: makeStudioBuildToolCallback(sequenceExecutor, callbacks.runPromise), }), }, stopWhen: stepCountIs(MAX_AGENT_STEPS), diff --git a/apps/playground/src/server/chat/studio-build-tool.server.ts b/apps/playground/src/server/chat/studio-build-tool.server.ts new file mode 100644 index 00000000..d10c09b1 --- /dev/null +++ b/apps/playground/src/server/chat/studio-build-tool.server.ts @@ -0,0 +1,168 @@ +import "@tanstack/react-start/server-only"; + +import { + MAX_DIAGRAM_BUILD_ATTEMPTS, + type BuildFlowchartOptions, + type CodeModeIssue, +} from "@sketchi/diagram-agent"; +import { recordMetric } from "@sketchi/observability"; +import { Effect, Metric, Ref, Semaphore } from "effect"; + +/** Studio renders the inline scene and offers the Excalidraw download. */ +export const STUDIO_ARTIFACT_OPTIONS: NonNullable = { + artifactFormats: ["scene", "excalidraw"], + inlineArtifacts: ["scene"], +}; + +const studioBuildRetries = Metric.counter("sketchi_chat_build_retries", { + description: "Studio chat diagram repair attempts", + incremental: true, +}); + +/** The shape every canonical Code Mode build result shares. */ +interface StudioBuildResult { + readonly ok: boolean; + readonly issues: readonly CodeModeIssue[]; +} + +export interface StudioBuildToolExecutor { + readonly attempts: Effect.Effect; + readonly execute: (input: Input) => Effect.Effect; +} + +type StudioBuildOperation = "buildFlowchart" | "buildSequenceDiagram"; + +/** + * One agent turn's build budget, shared by every Studio build tool: one + * attempt counter, one accepted artifact, and one build at a time. + */ +export interface StudioBuildTurn { + readonly maxAttempts: number; + readonly semaphore: Semaphore.Semaphore; + readonly state: Ref.Ref<{ + readonly accepted?: { + readonly operation: StudioBuildOperation; + readonly result: StudioBuildResult; + }; + readonly attempts: number; + }>; +} + +export function makeStudioBuildTurn( + maxAttempts: number = MAX_DIAGRAM_BUILD_ATTEMPTS, +): Effect.Effect { + return Effect.gen(function* () { + return { + maxAttempts, + semaphore: yield* Semaphore.make(1), + state: yield* Ref.make ? A : never>({ + attempts: 0, + }), + }; + }); +} + +export const attemptLimitIssue: CodeModeIssue = { + code: "quality_below_threshold", + severity: "error", + stage: "quality", + ref: { kind: "request", path: "spec" }, + message: "The diagram still needs changes before it can be shared.", + hint: "Explain that the draft needs another pass and invite the user to simplify or clarify the request.", +}; + +export const turnAlreadyAcceptedIssue: CodeModeIssue = { + code: "quality_below_threshold", + severity: "error", + stage: "quality", + ref: { kind: "request", path: "spec" }, + message: "A diagram was already saved for this request.", + hint: "Do not build another diagram this turn. Describe the saved diagram and offer one refinement.", +}; + +/** + * Per-agent-turn guard around one canonical Code Mode build. Studio owns only + * the attempt budget, artifact options, and optional input grounding; + * validation, quality, rendering, exporting, and persistence remain the shared + * build vertical. Tools that share a turn share its budget: an accepted result + * is reused by its own tool and ends building for every other tool. + */ +export function makeStudioBuildToolExecutor< + Input extends object, + Result extends StudioBuildResult, + E, + R, +>(options: { + readonly build: (input: unknown) => Effect.Effect; + /** This tool's rejected result carrying one issue. */ + readonly rejected: (issue: CodeModeIssue) => Result; + readonly operation: StudioBuildOperation; + readonly ground?: (input: Input) => { + readonly input: Input; + readonly issues: readonly CodeModeIssue[]; + }; + readonly turn?: StudioBuildTurn; +}): Effect.Effect> { + return Effect.gen(function* () { + const turn = options.turn ?? (yield* makeStudioBuildTurn()); + const { semaphore, state } = turn; + + const execute = Effect.fn(`playground.chat.${options.operation}.execute`)((input: Input) => + semaphore.withPermit( + Effect.gen(function* () { + const current = yield* Ref.get(state); + if (current.accepted) { + // The accepted result has this tool's result type when it is this tool's. + return current.accepted.operation === options.operation + ? (current.accepted.result as Result) + : options.rejected(turnAlreadyAcceptedIssue); + } + if (current.attempts >= turn.maxAttempts) { + return options.rejected(attemptLimitIssue); + } + + const attempt = current.attempts + 1; + if (attempt > 1) { + yield* recordMetric(studioBuildRetries, 1, { + operation: options.operation, + retryKind: "repair", + surface: "chat", + }); + yield* Effect.logInfo("Retrying Studio diagram repair", { + attempt, + operation: options.operation, + retry_kind: "repair", + surface: "chat", + }); + } + + yield* Ref.update(state, (value) => ({ + ...value, + attempts: value.attempts + 1, + })); + const grounded = options.ground?.(input) ?? { input, issues: [] }; + const built = yield* options.build({ + ...grounded.input, + options: STUDIO_ARTIFACT_OPTIONS, + }); + const result: Result = + grounded.issues.length > 0 + ? { ...built, issues: [...grounded.issues, ...built.issues] } + : built; + if (result.ok) { + yield* Ref.update(state, (value) => ({ + ...value, + accepted: { operation: options.operation, result }, + })); + } + return result; + }), + ), + ); + + return { + attempts: Ref.get(state).pipe(Effect.map((value) => value.attempts)), + execute, + }; + }); +} diff --git a/apps/playground/src/server/chat/studio-flowchart-tool.server.test.ts b/apps/playground/src/server/chat/studio-flowchart-tool.server.test.ts index 07183de1..f4738bb3 100644 --- a/apps/playground/src/server/chat/studio-flowchart-tool.server.test.ts +++ b/apps/playground/src/server/chat/studio-flowchart-tool.server.test.ts @@ -9,7 +9,6 @@ import { CodeModeRuntimeEnvironment, DIAGRAM_AGENT_SYSTEM_PROMPT, makeMemoryArtifactStorage, - MAX_FLOWCHART_BUILD_ATTEMPTS, type BuildFlowchartResult, type FlowchartSpec, toCodeModeJsonSchema, @@ -326,7 +325,7 @@ describe("Studio build_flowchart host", () => { status: "quality_failed", issues: [ expect.objectContaining({ - hint: "Explain that the draft needs another pass and invite the user to simplify or clarify the flow.", + hint: "Explain that the draft needs another pass and invite the user to simplify or clarify the request.", message: "The diagram still needs changes before it can be shared.", }), ], @@ -404,8 +403,7 @@ describe("Studio chat node logos", () => { received.push(input); return accepted; }), - MAX_FLOWCHART_BUILD_ATTEMPTS, - [{ name: "Github", slug: "github" }], + { logos: [{ name: "Github", slug: "github" }] }, ), ); const spec = acceptedSpec(); @@ -447,11 +445,12 @@ describe("Studio chat node logos", () => { received.push(input); return accepted; }), - MAX_FLOWCHART_BUILD_ATTEMPTS, - [ - { name: "Github", slug: "github" }, - { aliases: ["postgres"], name: "PostgreSQL", slug: "postgresql" }, - ], + { + logos: [ + { name: "Github", slug: "github" }, + { aliases: ["postgres"], name: "PostgreSQL", slug: "postgresql" }, + ], + }, ), ); const spec = acceptedSpec(); diff --git a/apps/playground/src/server/chat/studio-flowchart-tool.server.ts b/apps/playground/src/server/chat/studio-flowchart-tool.server.ts index ce5564c4..c2217e77 100644 --- a/apps/playground/src/server/chat/studio-flowchart-tool.server.ts +++ b/apps/playground/src/server/chat/studio-flowchart-tool.server.ts @@ -4,18 +4,16 @@ import { BuildFlowchartRequestSchema, BuildFlowchartResultSchema, DIAGRAM_AGENT_SYSTEM_PROMPT, - MAX_FLOWCHART_BUILD_ATTEMPTS, normalizeIconSlug, - type BuildFlowchartOptions, type BuildFlowchartResult, type BuildFlowchartToolInput, type CodeModeIssue, } from "@sketchi/diagram-agent"; import { placeNodeLogos, type OfferedLogo } from "@sketchi/diagram-generation"; -import { recordMetric } from "@sketchi/observability"; -import { Effect, Metric, Ref, Semaphore } from "effect"; +import type { Effect } from "effect"; import { toPlaygroundStandardSchema } from "../schema/effect-standard-schema.server"; +import { makeStudioBuildToolExecutor, type StudioBuildTurn } from "./studio-build-tool.server"; export const STUDIO_BUILD_FLOWCHART_TOOL_NAME = "build_flowchart" as const; @@ -31,16 +29,6 @@ export const StudioBuildFlowchartOutputSchema = toPlaygroundStandardSchema( export type StudioBuildFlowchartInput = BuildFlowchartToolInput; -export const STUDIO_FLOWCHART_ARTIFACT_OPTIONS: NonNullable = { - artifactFormats: ["scene", "excalidraw"], - inlineArtifacts: ["scene"], -}; - -const studioFlowchartRetries = Metric.counter("sketchi_chat_flowchart_retries", { - description: "Studio chat flowchart repair attempts", - incremental: true, -}); - /** * Studio's system prompt, plus the logos the user named. The model may only use * those slugs; anything else is dropped before the build. @@ -104,99 +92,17 @@ function groundSpecIcons( : { input, issues }; } -function attemptLimitResult(): BuildFlowchartResult { - return { - ok: false, - status: "quality_failed", - issues: [ - { - code: "quality_below_threshold", - severity: "error", - stage: "quality", - ref: { kind: "request", path: "spec" }, - message: "The diagram still needs changes before it can be shared.", - hint: "Explain that the draft needs another pass and invite the user to simplify or clarify the flow.", - }, - ], - }; -} - -export interface StudioFlowchartToolExecutor { - readonly attempts: Effect.Effect; - readonly execute: (input: StudioBuildFlowchartInput) => Effect.Effect; -} - -/** - * Per-agent-turn guard around the canonical runtime. Studio owns only the - * attempt budget and artifact options; validation, quality, rendering, - * exporting, and persistence remain the shared buildFlowchart vertical. - */ +/** Studio's build_flowchart tool over the shared buildFlowchart vertical. */ export function makeStudioFlowchartToolExecutor( buildFlowchart: (input: unknown) => Effect.Effect, - maxAttempts = MAX_FLOWCHART_BUILD_ATTEMPTS, - logos: readonly OfferedLogo[] = [], -): Effect.Effect> { - return Effect.gen(function* () { - const semaphore = yield* Semaphore.make(1); - const state = yield* Ref.make<{ - attempts: number; - accepted?: Extract; - }>({ attempts: 0 }); - - const execute = Effect.fn("playground.chat.buildFlowchart.execute")( - (input: StudioBuildFlowchartInput) => - semaphore.withPermit( - Effect.gen(function* () { - const current = yield* Ref.get(state); - if (current.accepted) { - return current.accepted; - } - if (current.attempts >= maxAttempts) { - return attemptLimitResult(); - } - - const attempt = current.attempts + 1; - if (attempt > 1) { - yield* recordMetric(studioFlowchartRetries, 1, { - operation: "buildFlowchart", - retryKind: "repair", - surface: "chat", - }); - yield* Effect.logInfo("Retrying Studio flowchart repair", { - attempt, - operation: "buildFlowchart", - retry_kind: "repair", - surface: "chat", - }); - } - - yield* Ref.update(state, (value) => ({ - ...value, - attempts: value.attempts + 1, - })); - const grounded = groundSpecIcons(input, logos); - const built = yield* buildFlowchart({ - ...grounded.input, - options: STUDIO_FLOWCHART_ARTIFACT_OPTIONS, - }); - const result: BuildFlowchartResult = - grounded.issues.length > 0 - ? { ...built, issues: [...grounded.issues, ...built.issues] } - : built; - if (result.ok) { - yield* Ref.update(state, (value) => ({ - ...value, - accepted: result, - })); - } - return result; - }), - ), - ); - - return { - attempts: Ref.get(state).pipe(Effect.map((value) => value.attempts)), - execute, - }; + options: { readonly logos?: readonly OfferedLogo[]; readonly turn?: StudioBuildTurn } = {}, +) { + const logos = options.logos ?? []; + return makeStudioBuildToolExecutor({ + build: buildFlowchart, + rejected: (issue) => ({ ok: false, status: "quality_failed", issues: [issue] }), + ground: (input) => groundSpecIcons(input, logos), + operation: "buildFlowchart", + ...(options.turn ? { turn: options.turn } : {}), }); } diff --git a/apps/playground/src/server/chat/studio-sequence-tool.server.test.ts b/apps/playground/src/server/chat/studio-sequence-tool.server.test.ts new file mode 100644 index 00000000..021b5318 --- /dev/null +++ b/apps/playground/src/server/chat/studio-sequence-tool.server.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it, vi } from "@effect/vitest"; +import { Effect } from "effect"; + +import { + BuildSequenceDiagramResultSchema, + BuildSequenceDiagramToolInputSchema, + buildFlowchart, + buildSequenceDiagram, + CodeModeArtifactStorage, + CodeModeRuntimeEnvironment, + makeMemoryArtifactStorage, + toCodeModeJsonSchema, +} from "@sketchi/diagram-agent"; + +import { STUDIO_ARTIFACT_OPTIONS, makeStudioBuildTurn } from "./studio-build-tool.server"; +import { + makeStudioFlowchartToolExecutor, + type StudioBuildFlowchartInput, +} from "./studio-flowchart-tool.server"; +import { + makeStudioSequenceToolExecutor, + StudioBuildSequenceInputSchema, + StudioBuildSequenceOutputSchema, + type StudioBuildSequenceInput, +} from "./studio-sequence-tool.server"; + +const checkout: StudioBuildSequenceInput = { + spec: { + id: "checkout", + title: "Checkout", + participants: [ + { id: "browser", label: "Browser" }, + { id: "api", label: "API" }, + { id: "payments", label: "Payments" }, + ], + messages: [ + { id: "submit", source: "browser", target: "api", label: "Submit order" }, + { id: "charge", source: "api", target: "payments", label: "Charge card" }, + { id: "approved", source: "payments", target: "api", label: "Approved", type: "return" }, + { id: "receipt", source: "api", target: "browser", label: "Receipt", type: "return" }, + ], + }, +}; + +function deterministicRuntime() { + const memory = makeMemoryArtifactStorage(); + const write = vi.fn(memory.write); + const provide = ( + effect: Effect.Effect, + ) => + effect.pipe( + Effect.provideService(CodeModeArtifactStorage, { ...memory, write }), + Effect.provideService(CodeModeRuntimeEnvironment, { + createId: (prefix) => `${prefix}_fixed`, + }), + ); + return { + build: (input: unknown) => provide(buildSequenceDiagram(input)), + buildFlowchart: (input: unknown) => provide(buildFlowchart(input)), + writes: () => write.mock.calls.length, + }; +} + +const releaseFlow: StudioBuildFlowchartInput = { + spec: { + title: "Release", + nodes: [ + { id: "start", label: "Open release", kind: "start" }, + { id: "ship", label: "Ship release", kind: "process" }, + { id: "done", label: "Release live", kind: "end" }, + ], + edges: [ + { source: "start", target: "ship" }, + { source: "ship", target: "done" }, + ], + layout: { direction: "TB" }, + }, +}; +describe("Studio build_sequence_diagram host", () => { + it("exposes the package-derived sequence contracts without style or artifact options", () => { + const input = StudioBuildSequenceInputSchema["~standard"].jsonSchema.input({ + target: "draft-2020-12", + }); + expect(input).toEqual(toCodeModeJsonSchema(BuildSequenceDiagramToolInputSchema)); + expect(JSON.stringify(input)).not.toMatch(/artifactFormats|accentColor/u); + expect( + StudioBuildSequenceOutputSchema["~standard"].jsonSchema.output({ + target: "draft-2020-12", + }), + ).toEqual(toCodeModeJsonSchema(BuildSequenceDiagramResultSchema)); + }); + + it("builds through the shared vertical with host artifact options and reuses the result", async () => { + const runtime = deterministicRuntime(); + const requests: unknown[] = []; + const executor = await Effect.runPromise( + makeStudioSequenceToolExecutor((input) => { + requests.push(input); + return runtime.build(input); + }), + ); + + const accepted = await Effect.runPromise(executor.execute(checkout)); + const reused = await Effect.runPromise(executor.execute(checkout)); + + expect(requests).toEqual([{ ...checkout, options: STUDIO_ARTIFACT_OPTIONS }]); + expect(accepted).toMatchObject({ ok: true, status: "accepted" }); + expect(reused).toBe(accepted); + expect(runtime.writes()).toBe(1); + if (!accepted.ok) throw new Error("Expected an accepted sequence diagram."); + expect(accepted.artifact.formats.map((format) => format.format).sort()).toEqual([ + "excalidraw", + "scene", + ]); + }); + + it("returns repairable issues, then stops after the attempt budget", async () => { + const runtime = deterministicRuntime(); + const executor = await Effect.runPromise(makeStudioSequenceToolExecutor(runtime.build)); + const selfMessage: StudioBuildSequenceInput = { + spec: { + ...checkout.spec, + messages: [{ id: "loop", source: "api", target: "api", label: "Retry" }], + }, + }; + + const rejected = await Effect.runPromise(executor.execute(selfMessage)); + expect(rejected).toMatchObject({ + ok: false, + status: "invalid_sequence", + issues: [expect.objectContaining({ code: "self_loop" })], + }); + await Effect.runPromise(executor.execute(selfMessage)); + await Effect.runPromise(executor.execute(selfMessage)); + const capped = await Effect.runPromise(executor.execute(checkout)); + expect(capped).toMatchObject({ + ok: false, + status: "quality_failed", + issues: [expect.objectContaining({ code: "quality_below_threshold" })], + }); + await expect(Effect.runPromise(executor.attempts)).resolves.toBe(3); + expect(runtime.writes()).toBe(0); + }); + + it("shares one accepted artifact per turn across both build tools", async () => { + const runtime = deterministicRuntime(); + const turn = await Effect.runPromise(makeStudioBuildTurn()); + const flowchart = await Effect.runPromise( + makeStudioFlowchartToolExecutor(runtime.buildFlowchart, { turn }), + ); + const sequence = await Effect.runPromise( + makeStudioSequenceToolExecutor(runtime.build, { turn }), + ); + + expect(await Effect.runPromise(flowchart.execute(releaseFlow))).toMatchObject({ ok: true }); + expect(runtime.writes()).toBe(1); + const second = await Effect.runPromise(sequence.execute(checkout)); + + expect(runtime.writes()).toBe(1); + expect(second).toMatchObject({ + ok: false, + status: "quality_failed", + issues: [ + expect.objectContaining({ message: "A diagram was already saved for this request." }), + ], + }); + }); + + it("shares one attempt budget per turn across both build tools", async () => { + const runtime = deterministicRuntime(); + const turn = await Effect.runPromise(makeStudioBuildTurn()); + const flowchart = await Effect.runPromise( + makeStudioFlowchartToolExecutor(runtime.buildFlowchart, { turn }), + ); + const sequence = await Effect.runPromise( + makeStudioSequenceToolExecutor(runtime.build, { turn }), + ); + const selfMessage: StudioBuildSequenceInput = { + spec: { ...checkout.spec, messages: [{ source: "api", target: "api", label: "Retry" }] }, + }; + + await Effect.runPromise(sequence.execute(selfMessage)); + await Effect.runPromise(sequence.execute(selfMessage)); + await Effect.runPromise(flowchart.execute({ spec: { ...releaseFlow.spec, edges: [] } })); + const capped = await Effect.runPromise(sequence.execute(checkout)); + + await expect(Effect.runPromise(sequence.attempts)).resolves.toBe(3); + expect(capped).toMatchObject({ + ok: false, + issues: [expect.objectContaining({ code: "quality_below_threshold" })], + }); + expect(runtime.writes()).toBe(0); + }); +}); diff --git a/apps/playground/src/server/chat/studio-sequence-tool.server.ts b/apps/playground/src/server/chat/studio-sequence-tool.server.ts new file mode 100644 index 00000000..7c2dacf6 --- /dev/null +++ b/apps/playground/src/server/chat/studio-sequence-tool.server.ts @@ -0,0 +1,39 @@ +import "@tanstack/react-start/server-only"; + +import { + BuildSequenceDiagramResultSchema, + BuildSequenceDiagramToolInputSchema, + type BuildSequenceDiagramResult, + type BuildSequenceDiagramToolInput, +} from "@sketchi/diagram-agent"; +import type { Effect } from "effect"; + +import { toPlaygroundStandardSchema } from "../schema/effect-standard-schema.server"; +import { makeStudioBuildToolExecutor, type StudioBuildTurn } from "./studio-build-tool.server"; + +export const STUDIO_BUILD_SEQUENCE_TOOL_NAME = "build_sequence_diagram" as const; + +export const STUDIO_BUILD_SEQUENCE_TOOL_DESCRIPTION = + "Build and persist one canonical sequence diagram artifact. Pass { spec: SequenceDiagramSpec } with ordered participants and chronological messages; the host supplies artifact options. If rejected, repair every structured issue and retry, up to three total attempts."; + +export const StudioBuildSequenceInputSchema = toPlaygroundStandardSchema( + BuildSequenceDiagramToolInputSchema, +); +export const StudioBuildSequenceOutputSchema = toPlaygroundStandardSchema( + BuildSequenceDiagramResultSchema, +); + +export type StudioBuildSequenceInput = BuildSequenceDiagramToolInput; + +/** Studio's build_sequence_diagram tool over the shared buildSequenceDiagram vertical. */ +export function makeStudioSequenceToolExecutor( + buildSequenceDiagram: (input: unknown) => Effect.Effect, + options: { readonly turn?: StudioBuildTurn } = {}, +) { + return makeStudioBuildToolExecutor({ + build: buildSequenceDiagram, + rejected: (issue) => ({ ok: false, status: "quality_failed", issues: [issue] }), + operation: "buildSequenceDiagram", + ...(options.turn ? { turn: options.turn } : {}), + }); +} diff --git a/apps/playground/src/server/codemode/mcp-docs/catalog.ts b/apps/playground/src/server/codemode/mcp-docs/catalog.ts index 44cb4cd9..8a717428 100644 --- a/apps/playground/src/server/codemode/mcp-docs/catalog.ts +++ b/apps/playground/src/server/codemode/mcp-docs/catalog.ts @@ -445,6 +445,7 @@ export const catalog: CatalogEntry[] = [ "Participant order is preserved left-to-right. Message order is preserved top-to-bottom.", 'A message with type "return" from the callee back to its caller answers the latest open call between them; Sketchi draws the callee\'s activation bar from that call to its return, nesting re-entrant calls. Calls nobody answers draw no bar.', "Every message source and target must reference a declared participant id. Self-referential messages are rejected with structured issues and repair hints.", + "A sequence diagram may have at most 12 participants and 40 messages; larger ones are rejected with sequence_too_large.", 'Request envelope: { spec: SequenceDiagramSpec, options?: { artifactFormats?: ["scene", "excalidraw", "png"], inlineArtifacts?: ["scene", "excalidraw"], minQualityScore?: number } }.', "Request scene, Excalidraw, and PNG for full artifact delivery; inline Excalidraw when the harness needs importable JSON in the response.", ].join("\n"), diff --git a/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt b/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt index 0b17728a..945a8d5f 100644 --- a/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt +++ b/apps/playground/src/server/codemode/mcp-docs/code-mode-types.generated.txt @@ -11,32 +11,32 @@ declare const sketchi: { type BuildFlowchartRequest = { "requestId"?: string; "spec": { "id"?: string; "title": string; "nodes": Array<{ "id": string; "label": string; "kind": "start" | "process" | "decision" | "end"; "description"?: string; "icon"?: { "slug": string; }; }>; "edges"?: Array<{ "id"?: string; "source": string; "target": string; "label"?: string; }>; "layout"?: { "direction"?: "TB" | "LR"; }; "style"?: { "accentColor"?: string; "backgroundColor"?: string; }; }; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "minQualityScore"?: number; }; }; -type BuildFlowchartResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "nodes": Array<{ "id": string; "label": string; "kind": "start" | "process" | "decision" | "end"; "description"?: string; "icon"?: { "slug": string; }; }>; "edges": Array<{ "id": string; "source": string; "target": string; "label"?: string; }>; "layout": { "direction": "TB" | "LR"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_flowchart" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "nodes": Array<{ "id": string; "label": string; "kind": "start" | "process" | "decision" | "end"; "description"?: string; "icon"?: { "slug": string; }; }>; "edges": Array<{ "id": string; "source": string; "target": string; "label"?: string; }>; "layout": { "direction": "TB" | "LR"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type BuildFlowchartResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "nodes": Array<{ "id": string; "label": string; "kind": "start" | "process" | "decision" | "end"; "description"?: string; "icon"?: { "slug": string; }; }>; "edges": Array<{ "id": string; "source": string; "target": string; "label"?: string; }>; "layout": { "direction": "TB" | "LR"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_flowchart" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "nodes": Array<{ "id": string; "label": string; "kind": "start" | "process" | "decision" | "end"; "description"?: string; "icon"?: { "slug": string; }; }>; "edges": Array<{ "id": string; "source": string; "target": string; "label"?: string; }>; "layout": { "direction": "TB" | "LR"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type BuildMindmapRequest = { "requestId"?: string; "spec": { "id"?: string; "title": string; "root": BuildMindmapRequest___schema0; "layout"?: { "direction"?: "LR" | "RL"; }; "style"?: { "accentColor"?: string; "backgroundColor"?: string; }; }; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "minQualityScore"?: number; }; }; type BuildMindmapRequest___schema0 = { "label": string; "children"?: Array; }; -type BuildMindmapResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "root": BuildMindmapResult_Objects_; "layout": { "direction": "LR" | "RL"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_mindmap" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "root": BuildMindmapResult_Objects_; "layout": { "direction": "LR" | "RL"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type BuildMindmapResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "root": BuildMindmapResult_Objects_; "layout": { "direction": "LR" | "RL"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_mindmap" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "root": BuildMindmapResult_Objects_; "layout": { "direction": "LR" | "RL"; }; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type BuildMindmapResult_Objects_ = { "id": string; "label": string; "children": Array; }; type BuildSequenceDiagramRequest = { "requestId"?: string; "spec": { "id"?: string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages"?: Array<{ "id"?: string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style"?: { "accentColor"?: string; "backgroundColor"?: string; }; }; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "minQualityScore"?: number; }; }; -type BuildSequenceDiagramResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages": Array<{ "id": string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_sequence" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages": Array<{ "id": string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type BuildSequenceDiagramResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "id": string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages": Array<{ "id": string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality": { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_sequence" | "quality_failed" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "id": string; "title": string; "participants": Array<{ "id": string; "label": string; "kind"?: string; }>; "messages": Array<{ "id": string; "source": string; "target": string; "label": string; "type"?: "message" | "return"; "style"?: "solid" | "dashed"; }>; "style": { "accentColor": string; "backgroundColor": string; }; }; "quality"?: { "accepted": boolean; "score": (number | "Infinity" | "-Infinity" | "NaN"); "threshold": (number | "Infinity" | "-Infinity" | "NaN"); "summary": { "nodeCount": (number | "Infinity" | "-Infinity" | "NaN"); "edgeCount": (number | "Infinity" | "-Infinity" | "NaN"); }; "checks": Array<{ "code": string; "passed": boolean; "severity": "error" | "warning"; "message": string; "refs": Array<{ "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }>; }>; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type CreateCanvasRequest = { "requestId"?: string; "spec": { "kind": "canvas"; "version": 1; "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers"?: Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts"?: Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder"?: Array; }; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "minQualityScore"?: number; }; }; -type CreateCanvasResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_canvas" | "limit_exceeded" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type CreateCanvasResult = ({ "ok": true; "status": "accepted"; "buildId": string; "requestId"?: string; "normalizedSpec": { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "invalid_canvas" | "limit_exceeded" | "render_failed" | "export_failed" | "storage_failed"; "buildId"?: string; "requestId"?: string; "normalizedSpec"?: { "kind": "canvas"; "version": (number | "Infinity" | "-Infinity" | "NaN"); "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers": Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts": Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder": Array; }; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type GetArtifactRequest = { "artifactId": string; "format"?: "excalidraw" | "scene" | "png"; "inline"?: boolean; }; -type GetArtifactResult = ({ "ok": true; "artifactId": string; "diagramId": string; "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); "provenance"?: { "sourceArtifactId": string; }; } | { "ok": false; "status": "invalid_input" | "not_found" | "format_unavailable" | "expired" | "storage_failed"; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type GetArtifactResult = ({ "ok": true; "artifactId": string; "diagramId": string; "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); "provenance"?: { "sourceArtifactId": string; }; } | { "ok": false; "status": "invalid_input" | "not_found" | "format_unavailable" | "expired" | "storage_failed"; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type ApplyDiagramPatchRequest = { "requestId"?: string; "source": ({ "artifactId": string; "format"?: "scene"; } | { "scene": { "kind": "canvas"; "version": 1; "diagramId": string; "title": string; "width": number; "height": number; "accentColor": string; "backgroundColor": string; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "icons"?: { [key: string]: { "name": string; "svg": string; }; }; "layers"?: Array<{ "id": string; "name"?: string; "locked"?: boolean; "visible"?: boolean; }>; "layouts"?: Array<({ "type": "row" | "column" | "stack"; "ids": Array; "x"?: number; "y"?: number; "gap"?: number; } | { "type": "grid"; "ids": Array; "columns": number; "x"?: number; "y"?: number; "columnGap"?: number; "rowGap"?: number; } | { "type": "align"; "ids": Array; "axis": "x" | "y"; "alignment": "center" | "end" | "start"; } | { "type": "distribute"; "ids": Array; "axis": "x" | "y"; "gap"?: number; })>; "zOrder"?: Array; }; }); "operations": Array<({ "op": "setDefaultStyle"; "style": { "strokeColor"?: string; "fillColor"?: string; "textColor"?: string; "backgroundColor"?: string; }; } | { "op": "setStyle"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "style": { "strokeColor"?: string; "fillColor"?: string; "textColor"?: string; "backgroundColor"?: string; }; } | { "op": "setShape"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "shape": "rectangle" | "diamond" | "ellipse" | "circle" | "polygon"; } | { "op": "translate"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "dx": number; "dy": number; } | { "op": "replaceText"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; "text": string; } | { "op": "rerouteEdges"; "selector"?: { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; } | { "op": "insert"; "elements": Array<({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; })>; "beforeId"?: string; "afterId"?: string; } | { "op": "remove"; "selector": { "ids"?: Array; "nodeIds"?: Array; "edgeIds"?: Array; "kinds"?: Array<"start" | "process" | "decision" | "end">; "labels"?: Array; "scope"?: "all" | "nodes" | "edges"; }; } | { "op": "replace"; "id": string; "element": ({ "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "node"; "id": string; "nodeId": string; "kind"?: string; "icon"?: { "slug": string; "size": number; }; "rendererRole"?: "sequence-lifeline" | "sequence-activation"; "shape": "rectangle" | "ellipse" | "diamond" | "circle" | "polygon"; "points"?: Array<{ "x": number; "y": number; }>; "textColor"?: string; "x": number; "y": number; "width": number; "height": number; "label": string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "type": "text"; "id": string; "containerId"?: string; "textColor"?: string; "x": number; "y": number; "text": string; "fontSize": number; "fontFamily"?: "hand" | "mono" | "sans"; "maxWidth"?: number; "textAlign"?: "center" | "left" | "right"; "verticalAlign"?: "bottom" | "middle" | "top"; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "arrow"; "id": string; "edgeId": string; "sourceNodeId": string; "targetNodeId": string; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "textColor"?: string; "points": Array<{ "x": number; "y": number; }>; "label"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "line"; "id": string; "points": Array<{ "x": number; "y": number; }>; "startBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "endBinding"?: { "elementId": string; "focus"?: number; "gap"?: number; }; "startArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "endArrowhead"?: ("arrow" | "bar" | "circle" | "diamond" | "triangle" | null); "label"?: string; "textColor"?: string; } | { "frameId"?: string; "groupIds"?: Array; "layerId"?: string; "locked"?: boolean; "opacity"?: number; "zIndex"?: number; "fillColor"?: string; "fillStyle"?: "cross-hatch" | "hachure" | "solid"; "roughness"?: 0 | 1 | 2; "strokeColor"?: string; "strokeStyle"?: "dashed" | "dotted" | "solid"; "strokeWidth"?: 1 | 2 | 4; "type": "frame"; "id": string; "name"?: string; "x": number; "y": number; "width": number; "height": number; }); } | { "op": "reorder"; "ids": Array; "beforeId"?: string; "afterId"?: string; } | { "op": "group"; "ids": Array; "groupId": string; } | { "op": "ungroup"; "ids": Array; "groupId"?: string; })>; "options"?: { "artifactFormats"?: Array<"excalidraw" | "scene" | "png">; "inlineArtifacts"?: Array<"excalidraw" | "scene">; "preserveConnectivity"?: boolean; }; "intent"?: string; }; -type ApplyDiagramPatchResult = ({ "ok": true; "status": "accepted"; "patchId": string; "requestId"?: string; "sourceArtifactId"?: string; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "source_unavailable" | "target_not_found" | "unsupported_operation" | "connectivity_changed" | "render_failed" | "export_failed" | "storage_failed"; "patchId"?: string; "requestId"?: string; "sourceArtifactId"?: string; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type ApplyDiagramPatchResult = ({ "ok": true; "status": "accepted"; "patchId": string; "requestId"?: string; "sourceArtifactId"?: string; "artifact": { "artifactId": string; "diagramId": string; "formats": Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; "provenance"?: { "sourceArtifactId": string; }; "preview"?: { "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": "invalid_input" | "source_unavailable" | "target_not_found" | "unsupported_operation" | "connectivity_changed" | "render_failed" | "export_failed" | "storage_failed"; "patchId"?: string; "requestId"?: string; "sourceArtifactId"?: string; "partial"?: { "artifactId"?: string; "diagramId"?: string; "formats"?: Array<{ "format": "excalidraw" | "scene" | "png"; "mimeType": string; "url"?: string; "expiresAt"?: string; "inline"?: unknown; "sizeBytes"?: (number | "Infinity" | "-Infinity" | "NaN"); }>; }; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); type SearchIconsRequest = { "q": string; "limit"?: number; }; -type SearchIconsResult = ({ "ok": true; "status": "accepted"; "query": string; "icons": Array<{ "slug": string; "name": string; "collection": string; }>; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": ("invalid_input"); "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); +type SearchIconsResult = ({ "ok": true; "status": "accepted"; "query": string; "icons": Array<{ "slug": string; "name": string; "collection": string; }>; "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; } | { "ok": false; "status": ("invalid_input"); "issues": Array<{ "code": "missing_field" | "invalid_type" | "invalid_enum" | "invalid_color" | "duplicate_node_id" | "duplicate_edge_id" | "missing_edge_source" | "missing_edge_target" | "self_loop" | "missing_start" | "multiple_starts" | "missing_end" | "start_has_incoming" | "end_has_outgoing" | "unreachable_node" | "nonterminating_node" | "missing_outgoing_edge" | "underbranched_decision" | "unlabeled_decision_branch" | "duplicate_decision_branch_label" | "disconnected_graph" | "flowchart_too_large" | "mindmap_too_deep" | "mindmap_too_large" | "sequence_too_large" | "request_too_large" | "generic_label" | "label_too_long" | "quality_below_threshold" | "render_failed" | "text_overflow" | "arrow_binding_invalid" | "arrow_overlap" | "export_invalid_scene" | "storage_read_failed" | "storage_write_failed" | "unsupported_artifact_format" | "patch_source_unavailable" | "unknown_patch_target" | "unsupported_patch_operation" | "patch_preserve_connectivity_failed" | "patch_output_invalid" | "duplicate_element_id" | "duplicate_layer_id" | "invalid_canvas_binding" | "invalid_canvas_composition" | "invalid_canvas_geometry" | "invalid_polygon" | "canvas_limit_exceeded" | "invalid_z_order" | "unknown_layout_target" | "unknown_icon" | "invalid_canvas_icon" | "icon_dropped"; "severity": "error" | "warning"; "stage": "input" | "canvas" | "flowchart" | "mindmap" | "quality" | "render" | "export" | "storage"; "ref"?: { "kind": "request" | "diagram" | "node" | "edge" | "element" | "layer" | "artifact"; "id"?: string; "path"?: string; }; "message": string; "hint": string; }>; }); diff --git a/package.json b/package.json index 5b904baf..e6f0dbc9 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dev:playground": "pnpm nx dev playground", "dev:web": "pnpm nx dev web", "eval:harness": "jiti tools/harness-eval.ts", + "eval:studio-routing": "jiti tools/studio-routing-eval.ts", "check": "pnpm run lint && pnpm run format:check && pnpm run test:lint-rules", "format": "oxfmt", "format:check": "oxfmt --check", diff --git a/packages/diagram/agent/README.md b/packages/diagram/agent/README.md index f7592d1f..db59aa5e 100644 --- a/packages/diagram/agent/README.md +++ b/packages/diagram/agent/README.md @@ -25,9 +25,10 @@ flowchart LR | Deterministic render/export/persistence pipeline | Managed chat threads | | Effect storage service, layers, and Code Mode operations | Studio presentation state | -Studio exposes the runtime to its model as `build_flowchart`. That host injects -artifact formats and a three-attempt per-turn cap; it does not map into another -flowchart schema or persist the accepted result a second time. +Studio exposes the runtime to its model as `build_flowchart` and +`build_sequence_diagram`. That host injects artifact formats and a three-attempt +per-turn cap per tool; it does not map into another schema or persist the +accepted result a second time. Node logos come from the icon catalog through `CodeModeIconCatalog`. Hosts build one with `makeCodeModeIconCatalog`, supplying SVG bytes from wherever they diff --git a/packages/diagram/agent/src/lib/code-mode/compatibility-corpus.test.ts b/packages/diagram/agent/src/lib/code-mode/compatibility-corpus.test.ts index bddb31ac..d1460561 100644 --- a/packages/diagram/agent/src/lib/code-mode/compatibility-corpus.test.ts +++ b/packages/diagram/agent/src/lib/code-mode/compatibility-corpus.test.ts @@ -112,6 +112,8 @@ const NODE_ICON_ISSUE_CODE_ADDITIONS = new Set([ "invalid_canvas_icon", "icon_dropped", ]); +// Sequence size limits (#280) add one issue code; frozen captures predate it. +const SEQUENCE_ISSUE_CODE_ADDITIONS = new Set(["sequence_too_large"]); const FLOWCHART_SPEC_NODE_ADDED_FIELDS = new Set(["icon"]); const FROZEN_FIXTURE_HASHES = { v1: "c668b53ee90043a06c640d06cc28253496d50e7431c916b523fcd4157b91ae55", @@ -190,7 +192,8 @@ function isApprovedCanvasArrayAddition(value: string): boolean { CANVAS_SCHEMA_ENUM_ADDITIONS.has(value) || CANVAS_PATCH_OPERATION_ADDITIONS.has(value) || CANVAS_ISSUE_CODE_ADDITIONS.has(value) || - NODE_ICON_ISSUE_CODE_ADDITIONS.has(value) + NODE_ICON_ISSUE_CODE_ADDITIONS.has(value) || + SEQUENCE_ISSUE_CODE_ADDITIONS.has(value) ); } @@ -1582,7 +1585,9 @@ describe("pre-Effect Code Mode compatibility corpus", () => { CodeModeIssueCodeSchema.options .filter( (code) => - !CANVAS_ISSUE_CODE_ADDITIONS.has(code) && !NODE_ICON_ISSUE_CODE_ADDITIONS.has(code), + !CANVAS_ISSUE_CODE_ADDITIONS.has(code) && + !NODE_ICON_ISSUE_CODE_ADDITIONS.has(code) && + !SEQUENCE_ISSUE_CODE_ADDITIONS.has(code), ) .toSorted(), ).toEqual([...directlyReachableCodes, ...boundaryOnlyCodes].toSorted()); diff --git a/packages/diagram/agent/src/lib/code-mode/contract.ts b/packages/diagram/agent/src/lib/code-mode/contract.ts index afac2fdd..0b3780a2 100644 --- a/packages/diagram/agent/src/lib/code-mode/contract.ts +++ b/packages/diagram/agent/src/lib/code-mode/contract.ts @@ -383,6 +383,7 @@ export const CODE_MODE_ISSUE_CODES = [ "flowchart_too_large", "mindmap_too_deep", "mindmap_too_large", + "sequence_too_large", "request_too_large", "generic_label", "label_too_long", @@ -746,6 +747,23 @@ export class BuildSequenceDiagramRequest extends Schema.Class ({ + id, + title, + participants, + messages, + })), + ), +}); +export type BuildSequenceDiagramToolInput = typeof BuildSequenceDiagramToolInputSchema.Type; + export interface MindmapTopicInput { label: string; children?: MindmapTopicInput[] | undefined; diff --git a/packages/diagram/agent/src/lib/code-mode/runtime.test.ts b/packages/diagram/agent/src/lib/code-mode/runtime.test.ts index 19889217..5d9ab612 100644 --- a/packages/diagram/agent/src/lib/code-mode/runtime.test.ts +++ b/packages/diagram/agent/src/lib/code-mode/runtime.test.ts @@ -2099,6 +2099,29 @@ describe("Code Mode runtime", () => { }); }); + it("rejects oversized sequence diagrams with sequence_too_large", async () => { + const result = await createTestRuntime().buildSequenceDiagram({ + spec: { + title: "Too many participants", + participants: Array.from({ length: 13 }, (_, index) => ({ + id: `p${index}`, + label: `Participant ${index}`, + })), + messages: [{ source: "p0", target: "p1", label: "Hello" }], + }, + }); + expect(result).toMatchObject({ + ok: false, + status: "invalid_sequence", + issues: [ + expect.objectContaining({ + code: "sequence_too_large", + ref: { kind: "request", path: "spec.participants" }, + }), + ], + }); + }); + it("returns a repairable issue for participant ids that collide with lifelines", async () => { const result = await createTestRuntime().buildSequenceDiagram({ spec: { diff --git a/packages/diagram/agent/src/lib/code-mode/runtime.ts b/packages/diagram/agent/src/lib/code-mode/runtime.ts index e90edd6c..16863771 100644 --- a/packages/diagram/agent/src/lib/code-mode/runtime.ts +++ b/packages/diagram/agent/src/lib/code-mode/runtime.ts @@ -457,6 +457,7 @@ function toSequenceDiagram(spec: NormalizedSequenceDiagramSpec): SequenceDiagram /** Code Mode's published issue codes for diagram-core's sequence invariants. */ const SEQUENCE_ISSUE_CODES = { + sequence_too_large: "sequence_too_large", duplicate_participant_id: "duplicate_node_id", lifeline_id_collision: "duplicate_node_id", duplicate_message_id: "duplicate_edge_id", diff --git a/packages/diagram/agent/src/lib/policy.test.ts b/packages/diagram/agent/src/lib/policy.test.ts index 8d3c91cd..0bbcc196 100644 --- a/packages/diagram/agent/src/lib/policy.test.ts +++ b/packages/diagram/agent/src/lib/policy.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; -import { DIAGRAM_AGENT_SYSTEM_PROMPT, MAX_FLOWCHART_BUILD_ATTEMPTS } from "./policy.js"; +import { DIAGRAM_AGENT_SYSTEM_PROMPT, MAX_DIAGRAM_BUILD_ATTEMPTS } from "./policy.js"; describe("diagram agent policy", () => { it("states the configured build budget and final-attempt stop rule", () => { expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain( - `Hard limit of ${MAX_FLOWCHART_BUILD_ATTEMPTS} attempts per turn`, + `Hard limit of ${MAX_DIAGRAM_BUILD_ATTEMPTS} attempts per turn`, ); expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain( "if the final attempt is still rejected, stop calling the tool", @@ -20,4 +20,19 @@ describe("diagram agent policy", () => { "quality check using its code, refs, and message", ); }); + + it("routes time-ordered interactions to the sequence build tool", () => { + expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain("build_flowchart with { spec: FlowchartSpec }"); + expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain( + "build_sequence_diagram with { spec: SequenceDiagramSpec }", + ); + expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain("SEQUENCE CRAFT"); + // A named type wins over the topic in both directions. + expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain( + "When the user names a diagram type, use that type's tool whatever the topic", + ); + expect(DIAGRAM_AGENT_SYSTEM_PROMPT).toContain( + "a flowchart of an OAuth handshake is still a flowchart", + ); + }); }); diff --git a/packages/diagram/agent/src/lib/policy.ts b/packages/diagram/agent/src/lib/policy.ts index 8cc48405..74104fbf 100644 --- a/packages/diagram/agent/src/lib/policy.ts +++ b/packages/diagram/agent/src/lib/policy.ts @@ -8,22 +8,32 @@ export const MAX_AGENT_STEPS = 8; export const MAX_AGENT_OUTPUT_TOKENS = 4_096; export const DIAGRAM_AGENT_TEMPERATURE = 0.4; -export const MAX_FLOWCHART_BUILD_ATTEMPTS = 3; +/** Build attempts each build tool allows per agent turn. */ +export const MAX_DIAGRAM_BUILD_ATTEMPTS = 3; export const DIAGRAM_AGENT_SYSTEM_PROMPT = `You are Sketchi, a diagramming agent with two jobs. JOB 1 — INTAKE. On a new request, decide whether you can already name the diagram's purpose, its audience, and the 4–12 things it must show. If not, ask at most 3 sharp clarifying questions in one short message and wait. If the request is already specific — or the user says to just draw — go straight to job 2. Never ask a second round of questions unless the user invites it. -JOB 2 — BUILD. Say in one short sentence what you are about to sketch, then call build_flowchart with { spec: FlowchartSpec }. The host chooses artifact formats and persists an accepted canonical artifact. Never pass artifact options, and never paste the diagram into chat as JSON, Mermaid, or ASCII art. -- Accepted (ok: true): the returned artifact is already saved and appears on the user's canvas. Close with 1–2 sentences on how to read it, then offer exactly one concrete refinement. Do not call the tool again in the same turn. -- Not accepted (ok: false): say in one clause what you are fixing, repair every structured issue using its code, ref, message, and hint and every failed quality check using its code, refs, and message, then call build_flowchart again with a complete corrected spec. Hard limit of ${MAX_FLOWCHART_BUILD_ATTEMPTS} attempts per turn; if the final attempt is still rejected, stop calling the tool and summarize the remaining issues. -- Later change requests: call build_flowchart with the complete revised FlowchartSpec. A new turn may create one new accepted artifact. +JOB 2 — BUILD. Pick the diagram family, say in one short sentence what you are about to sketch, then call its build tool. When the user names a diagram type, use that type's tool whatever the topic: "flowchart", "flow", or "process diagram" means build_flowchart, and "sequence diagram" means build_sequence_diagram, so a flowchart of an OAuth handshake is still a flowchart. Only when no type is named, choose by content: +- build_flowchart with { spec: FlowchartSpec } for processes, decisions, pipelines, and lifecycles. +- build_sequence_diagram with { spec: SequenceDiagramSpec } for time-ordered interactions between people or systems: requests and responses, handshakes, API calls, and protocols. +The host chooses artifact formats and persists an accepted canonical artifact. Never pass artifact options, and never paste the diagram into chat as JSON, Mermaid, or ASCII art. +- Accepted (ok: true): the returned artifact is already saved and appears on the user's canvas. Close with 1–2 sentences on how to read it, then offer exactly one concrete refinement. Do not call a build tool again in the same turn. +- Not accepted (ok: false): say in one clause what you are fixing, repair every structured issue using its code, ref, message, and hint and every failed quality check using its code, refs, and message, then call the same build tool again with a complete corrected spec. Hard limit of ${MAX_DIAGRAM_BUILD_ATTEMPTS} attempts per turn; if the final attempt is still rejected, stop calling the tool and summarize the remaining issues. +- Later change requests: call the same build tool with the complete revised spec, or the other one when the user asks for the other kind of diagram. A new turn may create one new accepted artifact. -DIAGRAM CRAFT +FLOWCHART CRAFT - Node ids: short kebab-case. Labels: 5 words max, specific ("Validate card details", never "Step 2"). - Use exactly one "start" node and at least one "end" node. Use "decision" for branch points and "process" for every other step. - Every decision needs at least 2 outgoing edges with distinct labels such as "yes" and "no". Every node must be reachable from start and able to reach an end. Loops are valid only when an exit path remains. - Keep the graph at 24 nodes and 64 edges or fewer. direction is "TB" for step-by-step flows and "LR" for pipelines and lifecycles. - Tool-call hygiene: every field is its own clean string. Node and edge ids must be unique, and every edge source/target must exactly equal an existing node id. +SEQUENCE CRAFT +- Participants: 2–8 read best and 12 is the limit; short kebab-case ids, ordered left to right as they first act. Labels name the actor or system ("Browser", "Auth service"). +- Messages: chronological, one row each; the label says what is sent ("POST /login", "Session token"), 6 words max. source and target are two different participant ids. +- Answer a call with a later message of type "return" from the callee back to the caller; Sketchi draws the callee's activation bar from the call to its return. Leave fire-and-forget events unanswered. +- Keep it to 30 messages or fewer; 40 is the limit. + VOICE: warm, concise, concrete. Short paragraphs, markdown only where it clarifies. You are a sketchbook companion, not a form.`; diff --git a/packages/diagram/core/src/types/sequence.test.ts b/packages/diagram/core/src/types/sequence.test.ts index 883d217f..d5d001d3 100644 --- a/packages/diagram/core/src/types/sequence.test.ts +++ b/packages/diagram/core/src/types/sequence.test.ts @@ -4,6 +4,9 @@ import { parseCanonicalDiagram } from "../diagram"; import { parseIntermediateDiagram, SKETCHI_DIAGRAM_STYLE } from "../intermediate"; import { type SequenceDiagram, + SEQUENCE_MAX_ISSUES, + SEQUENCE_MAX_MESSAGES, + SEQUENCE_MAX_PARTICIPANTS, SequenceValidationError, apiRequestSequence, getSequenceValidationIssues, @@ -294,4 +297,27 @@ describe("Sequence diagram type", () => { }), ).toEqual([["lifeline_id_collision", "participants.[1].id"]]); }); + + it("rejects oversized diagrams and caps the issue list", () => { + const participants = Array.from({ length: SEQUENCE_MAX_PARTICIPANTS + 1 }, (_, index) => ({ + id: `p${index}`, + label: `Participant ${index}`, + })); + const messages = Array.from({ length: SEQUENCE_MAX_MESSAGES + 1 }, (_, index) => ({ + id: `m${index}`, + source: "p0", + // Every message also targets a missing participant. + target: `ghost${index}`, + label: `Message ${index}`, + })); + const issues = getSequenceValidationIssues({ ...sequenceFixture, participants, messages }); + expect(issues).toHaveLength(SEQUENCE_MAX_ISSUES); + expect(issues.slice(0, 2).map((issue) => [issue.code, issue.path])).toEqual([ + ["sequence_too_large", "participants"], + ["sequence_too_large", "messages"], + ]); + expect( + issueCodes({ ...sequenceFixture, participants: participants.slice(0, -1), messages: [] }), + ).toEqual([]); + }); }); diff --git a/packages/diagram/core/src/types/sequence.ts b/packages/diagram/core/src/types/sequence.ts index 143b1268..86526f65 100644 --- a/packages/diagram/core/src/types/sequence.ts +++ b/packages/diagram/core/src/types/sequence.ts @@ -20,6 +20,11 @@ export function sequenceActivationId(participantId: string, callMessageId: strin return `${sequenceLifelineId(participantId)}:activation:${callMessageId}`; } +/** Wider or longer diagrams stop reading as one sequence; split them instead. */ +export const SEQUENCE_MAX_PARTICIPANTS = 12; +export const SEQUENCE_MAX_MESSAGES = 40; +export const SEQUENCE_MAX_ISSUES = 20; + export const SequenceMessageTypeSchema = Schema.Literals(SEQUENCE_MESSAGE_TYPES); export const SequenceMessageStyleSchema = Schema.Literals(SEQUENCE_MESSAGE_STYLES); export type SequenceMessageType = typeof SequenceMessageTypeSchema.Type; @@ -67,6 +72,7 @@ export class SequenceDiagram extends Schema.Class("SequenceDiag export const SequenceDiagramSchema = SequenceDiagram; export const SequenceValidationIssueCodeSchema = Schema.Literals([ + "sequence_too_large", "duplicate_participant_id", "lifeline_id_collision", "duplicate_message_id", @@ -88,6 +94,22 @@ export class SequenceValidationIssue extends Schema.Class SEQUENCE_MAX_PARTICIPANTS) { + issues.push({ + code: "sequence_too_large", + path: "participants", + message: `Sequence diagram has ${diagram.participants.length} participants; the supported maximum is ${SEQUENCE_MAX_PARTICIPANTS}.`, + hint: "Combine minor actors or split the interaction into several diagrams.", + }); + } + if (diagram.messages.length > SEQUENCE_MAX_MESSAGES) { + issues.push({ + code: "sequence_too_large", + path: "messages", + message: `Sequence diagram has ${diagram.messages.length} messages; the supported maximum is ${SEQUENCE_MAX_MESSAGES}.`, + hint: "Split the interaction into phases or drop lower-signal messages.", + }); + } const participantIndexById = new Map(); diagram.participants.forEach((participant, index) => { if (participantIndexById.has(participant.id)) { @@ -152,7 +174,7 @@ export function getSequenceValidationIssues(diagram: SequenceDiagram): SequenceV }); } }); - return issues; + return issues.slice(0, SEQUENCE_MAX_ISSUES); } /** diff --git a/packages/diagram/generation/src/lib/messages.ts b/packages/diagram/generation/src/lib/messages.ts index 018a0d4e..8374454a 100644 --- a/packages/diagram/generation/src/lib/messages.ts +++ b/packages/diagram/generation/src/lib/messages.ts @@ -69,6 +69,7 @@ const SEQUENCE_IR_INSTRUCTIONS = [ 'Use message type "message" for calls and "return" for responses; dashed style is appropriate for responses.', "Answer a call with a later return from the callee back to the caller; Sketchi draws the callee's activation bar from the call to its return. Leave fire-and-forget events unanswered.", "Every message source and target must reference a participant, and self-messages are not supported.", + "Use at most 12 participants and 40 messages.", ]; function expectedJsonShape(prompt: DiagramGenerationPrompt): string { diff --git a/packages/observability/src/lib/effect-telemetry.ts b/packages/observability/src/lib/effect-telemetry.ts index 14b91470..6765a8dd 100644 --- a/packages/observability/src/lib/effect-telemetry.ts +++ b/packages/observability/src/lib/effect-telemetry.ts @@ -39,7 +39,7 @@ const safeLogMessages = new Set([ "Browser Rendering session cleanup failed", "Code Mode usage capture failed", "Retrying diagram generation", - "Retrying Studio flowchart repair", + "Retrying Studio diagram repair", ]); const safeFieldNames = new Set([ diff --git a/tools/project-graph.test.ts b/tools/project-graph.test.ts index 55ec9b06..1db12588 100644 --- a/tools/project-graph.test.ts +++ b/tools/project-graph.test.ts @@ -107,6 +107,7 @@ const approvedRuntimeBoundaryFiles = [ "scripts/pipelines/r2-catalog-smoke.ts", "tools/generation-reliability-probe.ts", "tools/harness-eval.ts", + "tools/studio-routing-eval.ts", ]; const approvedManagedPromiseSiteCounts: Record = { "apps/cli/scripts/build.mjs": 27, @@ -206,6 +207,7 @@ const approvedManagedPromiseSiteCounts: Record = { "tools/sketchi-generators/src/generators/diagram-type/diagram-type.ts": 3, "tools/sketchi-generators/src/generators/ui-component/ui-component.spec.ts": 3, "tools/sketchi-generators/src/generators/ui-component/ui-component.ts": 3, + "tools/studio-routing-eval.ts": 4, }; const approvedManagedPromiseFiles = Object.keys(approvedManagedPromiseSiteCounts).sort(); const approvedEffectDependencyVersions = new Set(["4.0.1"]); diff --git a/tools/studio-routing-eval.test.ts b/tools/studio-routing-eval.test.ts new file mode 100644 index 00000000..3f572f67 --- /dev/null +++ b/tools/studio-routing-eval.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { firstBuildTool, studioRoutingCases } from "./studio-routing-eval"; + +describe("Studio routing eval", () => { + it("covers a named type overriding the topic in both directions, and unnamed content", () => { + const byId = new Map(studioRoutingCases.map((entry) => [entry.id, entry])); + expect(byId.get("named-flowchart-sequence-topic")?.expected).toBe("build_flowchart"); + expect(byId.get("named-sequence-process-topic")?.expected).toBe("build_sequence_diagram"); + expect(new Set(studioRoutingCases.map((entry) => entry.expected))).toEqual( + new Set(["build_flowchart", "build_sequence_diagram"]), + ); + }); + + it("reads the first build tool from a UI message stream", () => { + const stream = [ + 'data: {"type":"start"}', + 'data: {"type":"text-delta","id":"t","delta":"Sketching a sequence diagram."}', + 'data: {"type":"tool-input-start","toolCallId":"c1","toolName":"build_sequence_diagram"}', + 'data: {"type":"tool-input-start","toolCallId":"c2","toolName":"build_flowchart"}', + "data: [DONE]", + ].join("\n\n"); + expect(firstBuildTool(stream)).toBe("build_sequence_diagram"); + expect(firstBuildTool('data: {"type":"text-delta","delta":"Which flow?"}')).toBeUndefined(); + }); +}); diff --git a/tools/studio-routing-eval.ts b/tools/studio-routing-eval.ts new file mode 100644 index 00000000..4e960a72 --- /dev/null +++ b/tools/studio-routing-eval.ts @@ -0,0 +1,128 @@ +/** + * Studio chat routing eval: does the agent pick the build tool the user asked + * for? A named diagram type must win over the topic in both directions; with + * no type named, content decides. Each case is one live chat request. + * + * SKETCHI_PLAYGROUND_URL=http://localhost:3000 pnpm eval:studio-routing [case-id ...] + */ +import { pathToFileURL } from "node:url"; + +import { NodeRuntime } from "@effect/platform-node"; +import { Effect, Schema } from "effect"; + +export type StudioBuildTool = "build_flowchart" | "build_sequence_diagram"; + +export interface StudioRoutingCase { + readonly expected: StudioBuildTool; + readonly id: string; + readonly prompt: string; +} + +export const studioRoutingCases: readonly StudioRoutingCase[] = [ + { + id: "named-flowchart-sequence-topic", + expected: "build_flowchart", + prompt: + "Just draw it: a flowchart of our OAuth handshake. The browser redirects to the identity provider, the user signs in, the provider redirects back with a code, and the app exchanges the code for tokens.", + }, + { + id: "named-sequence-process-topic", + expected: "build_sequence_diagram", + prompt: + "Just draw it: a sequence diagram of expense approval. The employee submits the expense to the manager, the manager approves it and forwards it to finance, and finance pays the employee.", + }, + { + id: "unnamed-interaction", + expected: "build_sequence_diagram", + prompt: + "Just draw it: the browser calls the API, the API queries the database, the database returns rows, and the API returns JSON to the browser.", + }, + { + id: "unnamed-process", + expected: "build_flowchart", + prompt: + "Just draw it: our release process. Open a release, run the tests, and if they pass ship it, otherwise fix the failures and run the tests again.", + }, +]; + +class StudioRoutingEvalError extends Schema.TaggedError()( + "StudioRoutingEvalError", + { cause: Schema.optionalKey(Schema.Defect()), message: Schema.String }, +) {} + +const ToolEvent = Schema.Struct({ toolName: Schema.String }); +const isToolEvent = Schema.is(ToolEvent); + +/** The first build tool a UI message stream (server-sent events) calls. */ +export function firstBuildTool(stream: string): StudioBuildTool | undefined { + for (const line of stream.split("\n")) { + const data = line.startsWith("data:") ? line.slice("data:".length).trim() : ""; + if (!data.startsWith("{")) continue; + let event: unknown; + try { + event = JSON.parse(data); + } catch { + continue; + } + if ( + isToolEvent(event) && + (event.toolName === "build_flowchart" || event.toolName === "build_sequence_diagram") + ) { + return event.toolName; + } + } + return undefined; +} + +function chatRequest(prompt: string) { + return { + messages: [{ id: "routing-eval", role: "user", parts: [{ type: "text", text: prompt }] }], + }; +} + +const runCase = Effect.fn("studioRoutingEval.case")(function* ( + baseUrl: string, + routingCase: StudioRoutingCase, +) { + const stream = yield* Effect.tryPromise({ + try: async (signal) => { + const response = await fetch(new URL("/api/chat", baseUrl), { + body: JSON.stringify(chatRequest(routingCase.prompt)), + headers: { "Content-Type": "application/json" }, + method: "POST", + signal, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.text(); + }, + catch: (cause) => + StudioRoutingEvalError.make({ cause, message: `Chat request failed for ${routingCase.id}.` }), + }); + const actual = firstBuildTool(stream); + return { ...routingCase, actual, passed: actual === routingCase.expected }; +}); + +const main = Effect.gen(function* () { + const baseUrl = process.env["SKETCHI_PLAYGROUND_URL"]?.trim(); + if (!baseUrl) { + return yield* StudioRoutingEvalError.make({ + message: "Set SKETCHI_PLAYGROUND_URL to a running playground.", + }); + } + const ids = new Set(process.argv.slice(2)); + const cases = studioRoutingCases.filter((entry) => ids.size === 0 || ids.has(entry.id)); + const results = yield* Effect.forEach(cases, (entry) => runCase(baseUrl, entry), { + concurrency: 1, + }); + for (const result of results) { + console.log( + `${result.passed ? "PASS" : "FAIL"} ${result.id}: expected ${result.expected}, got ${result.actual ?? "no build tool"}`, + ); + } + if (results.some((result) => !result.passed)) process.exitCode = 1; +}); + +const entryPointPath = process.argv[1]; +if (entryPointPath && import.meta.url === pathToFileURL(entryPointPath).href) { + NodeRuntime.runMain(main); +}