feat(agent): implement phase 2 runtime and inline clarification

This commit is contained in:
2026-04-09 14:28:27 +02:00
parent b08e448be0
commit 29c93eeb35
18 changed files with 2376 additions and 5 deletions

77
lib/agent-models.ts Normal file
View File

@@ -0,0 +1,77 @@
export type AgentModelId =
| "openai/gpt-5.4-nano"
| "openai/gpt-5.4-mini"
| "openai/gpt-5.4"
| "openai/gpt-5.4-pro";
export type AgentModelMinTier = "starter" | "max";
export type AgentModelAccessTier = "free" | "starter" | "pro" | "max" | "business";
export interface AgentModel {
id: AgentModelId;
label: string;
minTier: AgentModelMinTier;
creditCost: number;
description: string;
}
export const AGENT_MODELS = {
"openai/gpt-5.4-nano": {
id: "openai/gpt-5.4-nano",
label: "GPT-5.4 Nano",
minTier: "starter",
creditCost: 6,
description: "Fastest option for lightweight agent runs",
},
"openai/gpt-5.4-mini": {
id: "openai/gpt-5.4-mini",
label: "GPT-5.4 Mini",
minTier: "starter",
creditCost: 15,
description: "Balanced quality and latency for default use",
},
"openai/gpt-5.4": {
id: "openai/gpt-5.4",
label: "GPT-5.4",
minTier: "starter",
creditCost: 38,
description: "Higher reasoning quality for complex tasks",
},
"openai/gpt-5.4-pro": {
id: "openai/gpt-5.4-pro",
label: "GPT-5.4 Pro",
minTier: "max",
creditCost: 180,
description: "Top-tier capability for hardest workflows",
},
} as const satisfies Record<AgentModelId, AgentModel>;
export const DEFAULT_AGENT_MODEL_ID: AgentModelId = "openai/gpt-5.4-mini";
const AGENT_MODEL_IDS = Object.keys(AGENT_MODELS) as AgentModelId[];
const AGENT_MODEL_TIER_ORDER: Record<AgentModelAccessTier, number> = {
free: 0,
starter: 1,
pro: 2,
max: 3,
business: 4,
};
export function getAgentModel(id: string): AgentModel | undefined {
return AGENT_MODELS[id as AgentModelId];
}
export function isAgentModelAvailableForTier(
tier: AgentModelAccessTier,
modelId: AgentModelId,
): boolean {
const model = AGENT_MODELS[modelId];
return AGENT_MODEL_TIER_ORDER[model.minTier] <= AGENT_MODEL_TIER_ORDER[tier];
}
export function getAvailableAgentModels(tier: AgentModelAccessTier): AgentModel[] {
return AGENT_MODEL_IDS.map((id) => AGENT_MODELS[id]).filter((model) =>
isAgentModelAvailableForTier(tier, model.id),
);
}

71
lib/agent-run-contract.ts Normal file
View File

@@ -0,0 +1,71 @@
export type AgentClarificationQuestion = {
id: string;
prompt: string;
required: boolean;
};
export type AgentClarificationAnswerMap = Partial<Record<string, string>>;
export type AgentOutputDraft = {
title?: string;
channel?: string;
outputType?: string;
body?: string;
};
export type AgentExecutionPlan = {
steps: string[];
outputs: AgentOutputDraft[];
};
export type AgentAnalyzeResult = {
clarificationQuestions: AgentClarificationQuestion[];
executionPlan: AgentExecutionPlan | null;
outputDrafts: AgentOutputDraft[];
};
const SAFE_FALLBACK_TITLE = "Untitled";
const SAFE_FALLBACK_CHANNEL = "general";
const SAFE_FALLBACK_OUTPUT_TYPE = "text";
function trimString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
export function areClarificationAnswersComplete(
questions: AgentClarificationQuestion[],
answers: AgentClarificationAnswerMap,
): boolean {
for (const question of questions) {
if (!question.required) {
continue;
}
if (trimString(answers[question.id]) === "") {
return false;
}
}
return true;
}
export function normalizeAgentOutputDraft(
draft: AgentOutputDraft,
): AgentOutputDraft & {
title: string;
channel: string;
outputType: string;
body: string;
} {
const title = trimString(draft.title) || SAFE_FALLBACK_TITLE;
const channel = trimString(draft.channel) || SAFE_FALLBACK_CHANNEL;
const outputType = trimString(draft.outputType) || SAFE_FALLBACK_OUTPUT_TYPE;
return {
...draft,
title,
channel,
outputType,
body: trimString(draft.body),
};
}

View File

@@ -65,7 +65,8 @@ export type CanvasConnectionValidationReason =
| "compare-incoming-limit"
| "adjustment-target-forbidden"
| "render-source-invalid"
| "agent-source-invalid";
| "agent-source-invalid"
| "agent-output-source-invalid";
export function validateCanvasConnectionPolicy(args: {
sourceType: string;
@@ -74,6 +75,10 @@ export function validateCanvasConnectionPolicy(args: {
}): CanvasConnectionValidationReason | null {
const { sourceType, targetType, targetIncomingCount } = args;
if (targetType === "agent-output" && sourceType !== "agent") {
return "agent-output-source-invalid";
}
if (targetType === "ai-video" && sourceType !== "video-prompt") {
return "ai-video-source-invalid";
}
@@ -152,6 +157,8 @@ export function getCanvasConnectionValidationMessage(
return "Render akzeptiert nur Bild-, Asset-, KI-Bild-, Crop- oder Adjustment-Input.";
case "agent-source-invalid":
return "Agent-Nodes akzeptieren nur Content- und Kontext-Inputs, keine Generierungs-Steuerknoten wie Prompt.";
case "agent-output-source-invalid":
return "Agent-Ausgabe akzeptiert nur Eingaben von Agent-Nodes.";
default:
return "Verbindung ist fuer diese Node-Typen nicht erlaubt.";
}

View File

@@ -124,7 +124,8 @@ export const NODE_CATALOG: readonly NodeCatalogEntry[] = [
type: "agent-output",
label: "Agent-Ausgabe",
category: "ai-output",
phase: 3,
phase: 2,
implemented: true,
systemOutput: true,
disabledHint: "Wird vom Agenten erzeugt",
}),

View File

@@ -12,6 +12,7 @@ import {
DEFAULT_DETAIL_ADJUST_DATA,
DEFAULT_LIGHT_ADJUST_DATA,
} from "@/lib/image-pipeline/adjustment-types";
import { DEFAULT_AGENT_MODEL_ID } from "@/lib/agent-models";
import { DEFAULT_CROP_NODE_DATA } from "@/lib/image-pipeline/crop-node-data";
/**
@@ -121,6 +122,7 @@ const SOURCE_NODE_GLOW_RGB: Record<string, readonly [number, number, number]> =
crop: [139, 92, 246],
render: [14, 165, 233],
agent: [245, 158, 11],
"agent-output": [245, 158, 11],
};
/** Compare: Ziel-Handles blau/smaragd, Quelle compare-out grau (wie in compare-node.tsx). */
@@ -229,6 +231,7 @@ export const NODE_HANDLE_MAP: Record<
crop: { source: undefined, target: undefined },
render: { source: undefined, target: undefined },
agent: { target: "agent-in" },
"agent-output": { target: "agent-output-in" },
};
/**
@@ -281,7 +284,23 @@ export const NODE_DEFAULTS: Record<
agent: {
width: 360,
height: 320,
data: { templateId: "campaign-distributor" },
data: {
templateId: "campaign-distributor",
modelId: DEFAULT_AGENT_MODEL_ID,
clarificationQuestions: [],
clarificationAnswers: {},
outputNodeIds: [],
},
},
"agent-output": {
width: 360,
height: 260,
data: {
title: "",
channel: "",
outputType: "",
body: "",
},
},
};