made tiny updates
This commit is contained in:
@@ -2,9 +2,10 @@ import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
// ── Model constants ───────────────────────────────────────────────────────────
|
||||
|
||||
// claude-opus-4-7 uses adaptive thinking (thinking.type = 'adaptive').
|
||||
// It does NOT accept temperature, top_p, or top_k — those are omitted in gateway.ts.
|
||||
export const MODEL = 'claude-opus-4-7' as const;
|
||||
// Spec Layer 6: "Claude Sonnet 4 API is the only AI provider."
|
||||
// claude-sonnet-4-6 supports per-call temperature (0.1 / 0.2 / 0.3 per prompt)
|
||||
// which the spec requires for diff-summary, completion-zone, and agent-query.
|
||||
export const MODEL = 'claude-sonnet-4-6' as const;
|
||||
|
||||
// ── Singleton client ──────────────────────────────────────────────────────────
|
||||
// The client is created once and shared. API key is injected from the server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildSystemPrompt } from '../prompts/system.js';
|
||||
import { buildAgentQueryMessages } from '../prompts/agent-query.prompt.js';
|
||||
|
||||
export interface AgentQAInput {
|
||||
/** Question from the coding agent (e.g. Cursor or Claude Code) */
|
||||
@@ -10,6 +10,10 @@ export interface AgentQAInput {
|
||||
artboardContextJson: string;
|
||||
/** Active DLF */
|
||||
dlfJson?: string;
|
||||
/** Before-state screenshot as base64 data URL (spec Layer 6.3-R3) */
|
||||
beforeScreenshotBase64?: string;
|
||||
/** After-state screenshot as base64 data URL (spec Layer 6.3-R3) */
|
||||
afterScreenshotBase64?: string;
|
||||
}
|
||||
|
||||
export interface AgentQAOutput {
|
||||
@@ -22,20 +26,14 @@ export async function answerAgentQuestion(
|
||||
gateway: AIGateway,
|
||||
input: AgentQAInput
|
||||
): Promise<AgentQAOutput> {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a design agent answering questions from a coding agent implementing a design diff',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
const { system, userContent, maxTokens } = buildAgentQueryMessages(input);
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `<artboard_context>\n${input.artboardContextJson}\n</artboard_context>\n\n<diff_id>${input.diffId}</diff_id>\n\n<question>\n${input.question}\n</question>\n\nAnswer the coding agent's question concisely and precisely. If the answer references a specific design token, component, or visual region, include a "visualReference" field. Return JSON:\n{\n "answer": "...",\n "visualReference": "..." (optional)\n}\n\nRespond ONLY with valid JSON.`,
|
||||
},
|
||||
],
|
||||
maxTokens: 1024,
|
||||
// userContent is ContentBlockParam[] — may include image blocks for screenshots
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.1, // spec Layer 6.3: 0.1 for agent-query (factual, authoritative)
|
||||
});
|
||||
|
||||
let parsed: unknown;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildSystemPrompt } from '../prompts/system.js';
|
||||
import { buildArtboardQueryMessages } from '../prompts/artboard-query.prompt.js';
|
||||
|
||||
export interface ArtboardQueryInput {
|
||||
/** Natural language query from the user */
|
||||
@@ -23,17 +23,13 @@ export async function queryCrossArtboard(
|
||||
gateway: AIGateway,
|
||||
input: ArtboardQueryInput
|
||||
): Promise<ArtboardQueryOutput> {
|
||||
const system = buildSystemPrompt({ role: 'a search agent filtering artboards by design intent' });
|
||||
const { system, userContent, maxTokens } = buildArtboardQueryMessages(input);
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `<artboards>\n${input.artboardsJson}\n</artboards>\n\n<query>\n${input.query}\n</query>\n\nReturn a JSON object:\n{\n "results": [{"artboardId": "...", "relevanceScore": 0-1, "reason": "..."}],\n "reasoning": "brief explanation"\n}\n\nOnly include artboards with relevanceScore > 0.3. Respond ONLY with valid JSON.`,
|
||||
},
|
||||
],
|
||||
maxTokens: 1024,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.1, // spec Layer 10.2: 0.1 for workspace queries (factual)
|
||||
});
|
||||
|
||||
// Returning empty results on parse failure is indistinguishable from "no match".
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { z } from 'zod';
|
||||
import { AIGateway } from '../gateway.js';
|
||||
import { buildSystemPrompt } from '../prompts/system.js';
|
||||
import { buildCompletionZoneMessages } from '../prompts/completion-zone.prompt.js';
|
||||
import type { GatewayResponse } from '../gateway.js';
|
||||
|
||||
// ── Output schema (spec Layer 6.3-R2: validated with Zod before accepting) ────
|
||||
// The model MUST return exactly these two fields. We validate the shape before
|
||||
// returning to callers so a malformed AI response never reaches the canvas store.
|
||||
|
||||
const CompletionZoneResultSchema = z.object({
|
||||
proposedTree: z.unknown(), // arbitrary component tree — shape validated downstream
|
||||
description: z.string().min(1), // non-empty natural-language summary
|
||||
});
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CompletionZoneInput {
|
||||
@@ -14,10 +23,12 @@ export interface CompletionZoneInput {
|
||||
dlfJson?: string;
|
||||
/** Before screenshot as base64 data URL (optional) */
|
||||
screenshotBase64?: string;
|
||||
/** Workspace ID for per-workspace cost attribution (spec Layer 6) */
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export interface CompletionZoneOutput {
|
||||
/** Proposed component tree changes as structured JSON */
|
||||
/** Proposed component tree changes as structured JSON — Zod-validated shape */
|
||||
proposedTree: unknown;
|
||||
/** Natural language description of the proposed change */
|
||||
description: string;
|
||||
@@ -31,45 +42,50 @@ export async function fillCompletionZone(
|
||||
input: CompletionZoneInput,
|
||||
maxRetries = 3
|
||||
): Promise<CompletionZoneOutput> {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a Completion Zone design agent',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
const { system, userContent, maxTokens } = buildCompletionZoneMessages(input);
|
||||
|
||||
const userContent: Anthropic.Messages.ContentBlockParam[] = [
|
||||
{
|
||||
type: 'text',
|
||||
text: `<component_tree>\n${input.componentTreeJson}\n</component_tree>\n\n<intent>\n${input.intent}\n</intent>\n\nReturn a JSON object with two fields:\n- "proposedTree": the updated component tree matching the intent\n- "description": a one-sentence description of the change\n\nRespond ONLY with valid JSON.`,
|
||||
},
|
||||
];
|
||||
|
||||
if (input.screenshotBase64) {
|
||||
userContent.unshift({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: input.screenshotBase64.replace(/^data:image\/\w+;base64,/, '') },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
// Retry up to maxRetries times on invalid JSON output
|
||||
// Retry up to maxRetries times on invalid JSON or Zod-invalid output
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens: 4096,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.3, // spec Layer 6.3: 0.3 for completion-zone (creative latitude)
|
||||
...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}),
|
||||
});
|
||||
|
||||
// Strip markdown fences the model occasionally adds despite instructions
|
||||
const raw = response.text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
const parsed = JSON.parse(response.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim());
|
||||
return { proposedTree: parsed.proposedTree as unknown, description: String(parsed.description ?? ''), raw: response };
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (parseErr) {
|
||||
lastError = new Error(
|
||||
`AI returned invalid JSON on attempt ${attempt + 1}: ${String(parseErr)}. ` +
|
||||
`Raw response (first 200 chars): ${response.text.slice(0, 200)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate shape with Zod before accepting (spec Layer 6.3-R2)
|
||||
const validated = CompletionZoneResultSchema.safeParse(parsed);
|
||||
if (!validated.success) {
|
||||
lastError = new Error(
|
||||
`AI response failed schema validation on attempt ${attempt + 1}: ` +
|
||||
validated.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ') +
|
||||
`. Raw response (first 200 chars): ${response.text.slice(0, 200)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
proposedTree: validated.data.proposedTree,
|
||||
description: validated.data.description,
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
throw lastError ?? new Error('Completion zone fill failed after retries');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildSystemPrompt } from '../prompts/system.js';
|
||||
import { buildDiffSummaryMessages, buildAggregateSummaryMessages } from '../prompts/diff-summary.prompt.js';
|
||||
|
||||
export interface DiffSummaryInput {
|
||||
/** Serialized component-level changes (JSON) */
|
||||
changesJson: string;
|
||||
/** Component name */
|
||||
componentName: string;
|
||||
/** Active DLF as JSON string — passed through for rule-violation annotation */
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
export interface DiffSummaryOutput {
|
||||
@@ -16,17 +18,54 @@ export async function generateDiffSummary(
|
||||
gateway: AIGateway,
|
||||
input: DiffSummaryInput
|
||||
): Promise<DiffSummaryOutput> {
|
||||
const system = buildSystemPrompt({ role: 'a technical writer summarizing UI component changes' });
|
||||
const { system, userContent, maxTokens } = buildDiffSummaryMessages(input);
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Summarize the following component changes for "${input.componentName}" in one concise sentence (max 20 words) suitable for a developer reviewing a pull request. Focus on what changed and its purpose.\n\n<changes>\n${input.changesJson}\n</changes>\n\nRespond with only the summary sentence.`,
|
||||
},
|
||||
],
|
||||
maxTokens: 128,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.2, // spec Layer 6.3: 0.2 for diff-summary (consistent output)
|
||||
});
|
||||
|
||||
return { summary: response.text.trim() };
|
||||
}
|
||||
|
||||
// ── Aggregate summary ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface AggregateSummaryInput {
|
||||
/** All changes in the diff as serialized JSON */
|
||||
changesJson: string;
|
||||
/** Human-readable artboard name, e.g. "Homepage Hero" */
|
||||
artboardName: string;
|
||||
/** Active DLF as JSON string — used to flag rule violations in the summary */
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
export interface AggregateSummaryOutput {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 2-3 sentence summary of ALL changes across a single diff
|
||||
* (artboard-level granularity, suitable for the diff card header and PR body).
|
||||
* Uses buildAggregateSummaryMessages — the session-level counterpart of
|
||||
* buildDiffSummaryMessages (component-level).
|
||||
*/
|
||||
export async function generateAggregateSummary(
|
||||
gateway: AIGateway,
|
||||
input: AggregateSummaryInput,
|
||||
): Promise<AggregateSummaryOutput> {
|
||||
const { system, userContent, maxTokens } = buildAggregateSummaryMessages({
|
||||
changesJson: input.changesJson,
|
||||
artboardName: input.artboardName,
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.2, // same as per-component summary — deterministic aggregate
|
||||
});
|
||||
|
||||
return { summary: response.text.trim() };
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { getClient, MODEL } from './client.js';
|
||||
import { generateDiffSummary, generateAggregateSummary } from './features/diff-summary.js';
|
||||
import { fillCompletionZone } from './features/completion-zone.js';
|
||||
import { queryCrossArtboard } from './features/artboard-query.js';
|
||||
import { answerAgentQuestion } from './features/agent-qa.js';
|
||||
import type { DiffSummaryInput, DiffSummaryOutput, AggregateSummaryInput, AggregateSummaryOutput } from './features/diff-summary.js';
|
||||
import type { CompletionZoneInput, CompletionZoneOutput } from './features/completion-zone.js';
|
||||
import type { ArtboardQueryInput, ArtboardQueryOutput } from './features/artboard-query.js';
|
||||
import type { AgentQAInput, AgentQAOutput } from './features/agent-qa.js';
|
||||
|
||||
// ── Gateway config ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -21,21 +29,22 @@ export interface RequestCost {
|
||||
estimatedCentsCost: number;
|
||||
}
|
||||
|
||||
const OPUS_4_INPUT_COST_PER_M = 500; // $5.00 / 1M
|
||||
const OPUS_4_OUTPUT_COST_PER_M = 2500; // $25.00 / 1M
|
||||
const OPUS_4_CACHE_READ_PER_M = 50; // $0.50 / 1M (estimated)
|
||||
// Spec Layer 6: claude-sonnet-4-6 pricing (used for cost attribution logging)
|
||||
const SONNET_4_INPUT_COST_PER_M = 300; // $3.00 / 1M input tokens
|
||||
const SONNET_4_OUTPUT_COST_PER_M = 1500; // $15.00 / 1M output tokens
|
||||
const SONNET_4_CACHE_READ_PER_M = 30; // $0.30 / 1M cache-read tokens
|
||||
|
||||
function computeCost(usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens?: number | null; cache_creation_input_tokens?: number | null }): RequestCost {
|
||||
const inputTokens = usage.input_tokens;
|
||||
const outputTokens = usage.output_tokens;
|
||||
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
||||
const inputTokens = usage.input_tokens;
|
||||
const outputTokens = usage.output_tokens;
|
||||
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
||||
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
||||
|
||||
const billableInput = inputTokens - cacheReadTokens;
|
||||
const estimatedCentsCost = Math.round(
|
||||
(billableInput / 1_000_000) * OPUS_4_INPUT_COST_PER_M +
|
||||
(outputTokens / 1_000_000) * OPUS_4_OUTPUT_COST_PER_M +
|
||||
(cacheReadTokens / 1_000_000) * OPUS_4_CACHE_READ_PER_M
|
||||
(billableInput / 1_000_000) * SONNET_4_INPUT_COST_PER_M +
|
||||
(outputTokens / 1_000_000) * SONNET_4_OUTPUT_COST_PER_M +
|
||||
(cacheReadTokens / 1_000_000) * SONNET_4_CACHE_READ_PER_M
|
||||
);
|
||||
|
||||
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, estimatedCentsCost };
|
||||
@@ -70,11 +79,22 @@ function sleep(ms: number): Promise<void> {
|
||||
// ── Gateway ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface GatewayRequest {
|
||||
messages: Anthropic.Messages.MessageParam[];
|
||||
system?: Anthropic.Messages.TextBlockParam[];
|
||||
maxTokens?: number;
|
||||
// NOTE: temperature is intentionally omitted — claude-opus-4-7 with
|
||||
// adaptive thinking rejects temperature, top_p, and top_k with a 400 error.
|
||||
messages: Anthropic.Messages.MessageParam[];
|
||||
system?: Anthropic.Messages.TextBlockParam[];
|
||||
maxTokens?: number;
|
||||
/**
|
||||
* Sampling temperature — spec Layer 6.3 per-prompt values:
|
||||
* diff-summary: 0.2 (consistent, low-creativity summaries)
|
||||
* completion-zone: 0.3 (slight creative latitude for UI generation)
|
||||
* agent-query: 0.1 (factual/authoritative answers)
|
||||
* Defaults to 0.2 if omitted. Range [0, 1].
|
||||
*/
|
||||
temperature?: number;
|
||||
/**
|
||||
* Workspace ID for per-workspace cost attribution (spec Layer 6.2-R2).
|
||||
* Logged on every request so cost can be aggregated per workspace.
|
||||
*/
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export interface GatewayResponse {
|
||||
@@ -98,15 +118,18 @@ export class AIGateway {
|
||||
async complete(req: GatewayRequest): Promise<GatewayResponse> {
|
||||
await this.rateLimiter.acquire();
|
||||
|
||||
const requestId = `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
const startMs = Date.now();
|
||||
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: MODEL,
|
||||
max_tokens: req.maxTokens ?? 4096,
|
||||
thinking: { type: 'adaptive' },
|
||||
model: MODEL,
|
||||
max_tokens: req.maxTokens ?? 4096,
|
||||
temperature: req.temperature ?? 0.2,
|
||||
...(req.system !== undefined ? { system: req.system } : {}),
|
||||
messages: req.messages,
|
||||
messages: req.messages,
|
||||
});
|
||||
|
||||
const text = response.content
|
||||
@@ -114,20 +137,55 @@ export class AIGateway {
|
||||
.map(b => b.text)
|
||||
.join('');
|
||||
|
||||
const cost = computeCost(response.usage);
|
||||
const cost = computeCost(response.usage);
|
||||
const latencyMs = Date.now() - startMs;
|
||||
this.totalCost += cost.estimatedCentsCost;
|
||||
|
||||
// ── Structured request log (spec Layer 6.2-R4) ──────────────────────
|
||||
// Every AI call is logged with: requestId, model, token counts, cost
|
||||
// estimate, latency, and workspace ID for per-workspace attribution.
|
||||
console.log(JSON.stringify({
|
||||
level: 'info',
|
||||
event: 'ai_request',
|
||||
requestId,
|
||||
model: MODEL,
|
||||
workspaceId: req.workspaceId ?? null,
|
||||
inputTokens: cost.inputTokens,
|
||||
outputTokens: cost.outputTokens,
|
||||
cacheReadTokens: cost.cacheReadTokens,
|
||||
cacheWriteTokens: cost.cacheWriteTokens,
|
||||
estimatedCents: cost.estimatedCentsCost,
|
||||
latencyMs,
|
||||
attempt,
|
||||
}));
|
||||
|
||||
return { content: response.content, text, cost };
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
const latencyMs = Date.now() - startMs;
|
||||
|
||||
// Log every failed attempt for observability
|
||||
console.error(JSON.stringify({
|
||||
level: 'error',
|
||||
event: 'ai_request_error',
|
||||
requestId,
|
||||
model: MODEL,
|
||||
workspaceId: req.workspaceId ?? null,
|
||||
attempt,
|
||||
latencyMs,
|
||||
error: lastError.message,
|
||||
}));
|
||||
|
||||
// Retry on 429, 529, 5xx, and network errors (no HTTP status).
|
||||
// Break immediately on client errors (4xx that aren't rate-limits).
|
||||
if (err instanceof Anthropic.APIError) {
|
||||
const { status } = err;
|
||||
if (status !== 429 && status !== 529 && status < 500) break;
|
||||
}
|
||||
// Non-APIError (network timeout, DNS failure, etc.) → always retry
|
||||
await sleep(2 ** attempt * 1000);
|
||||
// Non-APIError (network timeout, DNS failure, etc.) → always retry.
|
||||
// Cap at 30s so a long retry sequence doesn't wedge the server forever.
|
||||
const delayMs = Math.min(2 ** attempt * 1000, 30_000);
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,4 +196,28 @@ export class AIGateway {
|
||||
getCumulativeCost(): number {
|
||||
return this.totalCost;
|
||||
}
|
||||
|
||||
// ── Spec Layer 5 convenience methods ─────────────────────────────────────────
|
||||
// Thin wrappers that delegate to the standalone feature functions, making the
|
||||
// gateway usable as a single dependency injection point across the app layer.
|
||||
|
||||
generateDiffSummary(input: DiffSummaryInput): Promise<DiffSummaryOutput> {
|
||||
return generateDiffSummary(this, input);
|
||||
}
|
||||
|
||||
generateAggregateSummary(input: AggregateSummaryInput): Promise<AggregateSummaryOutput> {
|
||||
return generateAggregateSummary(this, input);
|
||||
}
|
||||
|
||||
fillCompletionZone(input: CompletionZoneInput): Promise<CompletionZoneOutput> {
|
||||
return fillCompletionZone(this, input);
|
||||
}
|
||||
|
||||
queryArtboards(input: ArtboardQueryInput): Promise<ArtboardQueryOutput> {
|
||||
return queryCrossArtboard(this, input);
|
||||
}
|
||||
|
||||
answerAgentQuery(input: AgentQAInput): Promise<AgentQAOutput> {
|
||||
return answerAgentQuestion(this, input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,24 @@ export { getClient, MODEL } from './client.js';
|
||||
export { AIGateway } from './gateway.js';
|
||||
export type { GatewayRequest, GatewayResponse, RequestCost } from './gateway.js';
|
||||
|
||||
// ── Versioned prompt builders ─────────────────────────────────────────────────
|
||||
export { DIFF_SUMMARY_PROMPT_VERSION, buildDiffSummaryMessages, buildAggregateSummaryMessages } from './prompts/diff-summary.prompt.js';
|
||||
export type { DiffSummaryPromptInput, AggregateSummaryPromptInput } from './prompts/diff-summary.prompt.js';
|
||||
|
||||
export { COMPLETION_ZONE_PROMPT_VERSION, buildCompletionZoneMessages } from './prompts/completion-zone.prompt.js';
|
||||
export type { CompletionZonePromptInput } from './prompts/completion-zone.prompt.js';
|
||||
|
||||
export { ARTBOARD_QUERY_PROMPT_VERSION, buildArtboardQueryMessages } from './prompts/artboard-query.prompt.js';
|
||||
export type { ArtboardQueryPromptInput } from './prompts/artboard-query.prompt.js';
|
||||
|
||||
export { AGENT_QUERY_PROMPT_VERSION, buildAgentQueryMessages } from './prompts/agent-query.prompt.js';
|
||||
export type { AgentQueryPromptInput } from './prompts/agent-query.prompt.js';
|
||||
|
||||
export { fillCompletionZone } from './features/completion-zone.js';
|
||||
export type { CompletionZoneInput, CompletionZoneOutput } from './features/completion-zone.js';
|
||||
|
||||
export { generateDiffSummary } from './features/diff-summary.js';
|
||||
export type { DiffSummaryInput, DiffSummaryOutput } from './features/diff-summary.js';
|
||||
export { generateDiffSummary, generateAggregateSummary } from './features/diff-summary.js';
|
||||
export type { DiffSummaryInput, DiffSummaryOutput, AggregateSummaryInput, AggregateSummaryOutput } from './features/diff-summary.js';
|
||||
|
||||
export { queryCrossArtboard } from './features/artboard-query.js';
|
||||
export type { ArtboardQueryInput, ArtboardQueryOutput, ArtboardQueryResult } from './features/artboard-query.js';
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// ── Agent Query Prompt ────────────────────────────────────────────────────────
|
||||
// Versioned prompt for answering coding-agent questions about design diffs.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 1024 — JSON with answer + optional visual reference
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) + optional DLF constraint block (cached)
|
||||
// User: [before screenshot] + [after screenshot] + artboard context +
|
||||
// diff ID + agent question (spec Layer 6.3-R3: screenshots required)
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const AGENT_QUERY_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface AgentQueryPromptInput {
|
||||
/** Question from the coding agent (e.g. Cursor or Claude Code) */
|
||||
question: string;
|
||||
/** The diff ID this question is about */
|
||||
diffId: string;
|
||||
/** Full artboard context (IntentDiff + artboard metadata) as JSON */
|
||||
artboardContextJson: string;
|
||||
/** Active DLF as JSON string (optional — cached in system when present) */
|
||||
dlfJson?: string;
|
||||
/**
|
||||
* Before-state screenshot as base64 data URL (spec Layer 6.3-R3).
|
||||
* Gives the model visual ground truth of what the artboard looked like
|
||||
* before the diff was applied.
|
||||
*/
|
||||
beforeScreenshotBase64?: string;
|
||||
/**
|
||||
* After-state screenshot as base64 data URL (spec Layer 6.3-R3).
|
||||
* Gives the model the proposed post-diff visual state.
|
||||
*/
|
||||
afterScreenshotBase64?: string;
|
||||
}
|
||||
|
||||
function stripDataUrl(dataUrl: string): string {
|
||||
return dataUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user content blocks for a coding-agent design question.
|
||||
* When screenshots are provided they precede the text context so the model can
|
||||
* visually ground its answer before reading the structured JSON.
|
||||
* Max 1024 tokens — the answer must be precise and actionable.
|
||||
*/
|
||||
export function buildAgentQueryMessages(input: AgentQueryPromptInput): {
|
||||
system: Anthropic.Messages.TextBlockParam[];
|
||||
userContent: Anthropic.Messages.ContentBlockParam[];
|
||||
maxTokens: number;
|
||||
} {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a design agent answering questions from a coding agent implementing a design diff',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const userContent: Anthropic.Messages.ContentBlockParam[] = [];
|
||||
|
||||
// Screenshots first — visual context before text, per multimodal best practice.
|
||||
if (input.beforeScreenshotBase64) {
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text: 'Before state (artboard before this diff was applied):',
|
||||
});
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: stripDataUrl(input.beforeScreenshotBase64) },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
if (input.afterScreenshotBase64) {
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text: 'After state (proposed artboard after this diff is applied):',
|
||||
});
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: stripDataUrl(input.afterScreenshotBase64) },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text:
|
||||
`<artboard_context>\n${input.artboardContextJson}\n</artboard_context>\n\n` +
|
||||
`<diff_id>${input.diffId}</diff_id>\n\n` +
|
||||
`<question>\n${input.question}\n</question>\n\n` +
|
||||
`Answer the coding agent's question concisely and precisely. ` +
|
||||
`If the answer references a specific design token, component, or visual region, ` +
|
||||
`include a "visualReference" field. Return JSON:\n` +
|
||||
`{\n "answer": "...",\n "visualReference": "..." (optional)\n}\n\n` +
|
||||
`Respond ONLY with valid JSON.`,
|
||||
});
|
||||
|
||||
return { system, userContent, maxTokens: 1024 };
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// ── Artboard Query Prompt ─────────────────────────────────────────────────────
|
||||
// Versioned prompt for cross-artboard design intent search.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 1024 — output is a JSON results array ranked by relevance
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) — no DLF needed for cross-artboard search
|
||||
// User: artboard metadata array + query string (not cached — varies per call)
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const ARTBOARD_QUERY_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface ArtboardQueryPromptInput {
|
||||
/** Natural language query from the user */
|
||||
query: string;
|
||||
/** Array of artboard metadata objects serialized as JSON */
|
||||
artboardsJson: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user message pair for a cross-artboard relevance search.
|
||||
* Only artboards with relevanceScore > 0.3 should be returned by the model.
|
||||
*/
|
||||
export function buildArtboardQueryMessages(input: ArtboardQueryPromptInput): {
|
||||
system: Anthropic.Messages.TextBlockParam[];
|
||||
userContent: string;
|
||||
maxTokens: number;
|
||||
} {
|
||||
const system = buildSystemPrompt({ role: 'a search agent filtering artboards by design intent' });
|
||||
|
||||
const userContent =
|
||||
`<artboards>\n${input.artboardsJson}\n</artboards>\n\n` +
|
||||
`<query>\n${input.query}\n</query>\n\n` +
|
||||
`Return a JSON object:\n` +
|
||||
`{\n` +
|
||||
` "results": [{"artboardId": "...", "relevanceScore": 0-1, "reason": "..."}],\n` +
|
||||
` "reasoning": "brief explanation"\n` +
|
||||
`}\n\n` +
|
||||
`Only include artboards with relevanceScore > 0.3. Respond ONLY with valid JSON.`;
|
||||
|
||||
return { system, userContent, maxTokens: 1024 };
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// ── Completion Zone Prompt ────────────────────────────────────────────────────
|
||||
// Versioned prompt for filling AI Completion Zones.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 4096 — output is a structured JSON tree
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) + DLF as a hard constraint block (cached)
|
||||
// User: component tree context + optional screenshot + zone intent string
|
||||
//
|
||||
// The DLF is placed first (before the component tree) so it is cached across
|
||||
// all completion zone calls in the same workspace session. The model must treat
|
||||
// the DLF as a hard constraint: any proposed component must exist in the DLF's
|
||||
// component list, and all props must pass the DLF's allowedProps rules.
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const COMPLETION_ZONE_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface CompletionZonePromptInput {
|
||||
componentTreeJson: string;
|
||||
intent: string;
|
||||
dlfJson?: string;
|
||||
screenshotBase64?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full message array for a completion zone generation request.
|
||||
* Returns a multi-part user message that includes an optional image block.
|
||||
*/
|
||||
export function buildCompletionZoneMessages(input: CompletionZonePromptInput): {
|
||||
system: Anthropic.Messages.TextBlockParam[];
|
||||
userContent: Anthropic.Messages.ContentBlockParam[];
|
||||
maxTokens: number;
|
||||
} {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a Completion Zone design agent that generates UI component trees to fill empty design regions',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const textBlock: Anthropic.Messages.TextBlockParam = {
|
||||
type: 'text',
|
||||
text:
|
||||
`<component_tree>\n${input.componentTreeJson}\n</component_tree>\n\n` +
|
||||
`<intent>\n${input.intent}\n</intent>\n\n` +
|
||||
(input.dlfJson
|
||||
? `The DLF in the system prompt is a HARD CONSTRAINT. Only use components and prop values defined there.\n\n`
|
||||
: '') +
|
||||
`Return a JSON object with exactly two fields:\n` +
|
||||
`- "proposedTree": the updated component tree matching the intent (same shape as the input tree)\n` +
|
||||
`- "description": one sentence describing what was generated and why\n\n` +
|
||||
`Respond ONLY with valid JSON. No markdown, no prose outside the JSON.`,
|
||||
};
|
||||
|
||||
const userContent: Anthropic.Messages.ContentBlockParam[] = [];
|
||||
|
||||
// Screenshot goes first if provided (visual context before textual context).
|
||||
if (input.screenshotBase64) {
|
||||
const base64Data = input.screenshotBase64.replace(/^data:image\/\w+;base64,/, '');
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: base64Data },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
userContent.push(textBlock);
|
||||
|
||||
return { system, userContent, maxTokens: 4096 };
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// ── Diff Summary Prompt ───────────────────────────────────────────────────────
|
||||
// Versioned prompt for generating human-readable summaries of ComponentChange
|
||||
// records and aggregate IntentDiff records.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 300 per change summary, 500 for aggregate summary
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) + optional DLF constraint block (cached)
|
||||
// User: component changes + output instruction (not cached — varies per call)
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const DIFF_SUMMARY_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface DiffSummaryPromptInput {
|
||||
changesJson: string;
|
||||
componentName: string;
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
export interface AggregateSummaryPromptInput {
|
||||
changesJson: string;
|
||||
artboardName: string;
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user message pair for a single-component change summary.
|
||||
* Max 300 tokens — one tight sentence for PR review readability.
|
||||
*/
|
||||
export function buildDiffSummaryMessages(
|
||||
input: DiffSummaryPromptInput,
|
||||
): { system: Anthropic.Messages.TextBlockParam[]; userContent: string; maxTokens: number } {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a technical writer summarizing UI component changes for pull request reviewers',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const userContent =
|
||||
`Summarize the following changes to "${input.componentName}" in one concise sentence ` +
|
||||
`(max 20 words). Focus on the user-visible impact, not technical prop names. ` +
|
||||
`If the change violates a design system rule, note it.\n\n` +
|
||||
`<changes>\n${input.changesJson}\n</changes>\n\n` +
|
||||
`Respond with ONLY the summary sentence.`;
|
||||
|
||||
return { system, userContent, maxTokens: 300 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user message pair for an aggregate IntentDiff summary.
|
||||
* Max 500 tokens — a short paragraph describing all changes together.
|
||||
*/
|
||||
export function buildAggregateSummaryMessages(
|
||||
input: AggregateSummaryPromptInput,
|
||||
): { system: Anthropic.Messages.TextBlockParam[]; userContent: string; maxTokens: number } {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a technical writer summarizing a design session for engineering handoff',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const userContent =
|
||||
`Write a 2-3 sentence summary of the following design changes made to the "${input.artboardName}" artboard. ` +
|
||||
`Describe the combined intent — what changed and why — in terms a developer reading a PR can understand. ` +
|
||||
`Do not list individual prop changes; synthesize them.\n\n` +
|
||||
`<changes>\n${input.changesJson}\n</changes>\n\n` +
|
||||
`Respond with ONLY the summary paragraph.`;
|
||||
|
||||
return { system, userContent, maxTokens: 500 };
|
||||
}
|
||||
Reference in New Issue
Block a user