feat(agent): implement phase 2 runtime and inline clarification
This commit is contained in:
77
lib/agent-models.ts
Normal file
77
lib/agent-models.ts
Normal 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
71
lib/agent-run-contract.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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.";
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user