import type { SuperagentToolCall } from '../types'; import { parseToolArgs } from './toolWidgetUtils'; /** * Pure logic for the build_mode blueprint card — native port of the web * AgentBlueprint parsing (frontend/apps/builder/.../agent-blueprint/ * useAgentBlueprint.ts + AgentBlueprint.utils.ts). */ export type AgentBlueprintArgs = { agent_name?: string; persona?: string; mission?: string; operating_rules?: string[]; data_model?: string[]; capabilities?: string[]; connectors?: string[]; skills?: string[]; automations?: string[]; memory?: string; channels?: string[]; knowledge?: string[]; coverage_notes?: string; build_sequence?: string[]; }; /** A building block: either a list of items or a single prose string. */ export type BlueprintBlock = { key: string; label: string; items?: string[]; prose?: string; }; /** * The blueprint is generated by the planner model inside the build_mode tool * and returned in the result as {blueprint, markdown}. Fall back to the tool * args for older payloads. A non-empty result that won't parse is a * truncated/garbled blueprint — the widget shows a graceful note. */ export function parseBlueprint(toolCall: SuperagentToolCall): AgentBlueprintArgs { const raw = toolCall.results; try { const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; if (parsed && typeof parsed === 'object') { const record = parsed as { blueprint?: AgentBlueprintArgs }; return record.blueprint ?? (parsed as AgentBlueprintArgs); } } catch { // Fall through to the args. } return (parseToolArgs(toolCall) as AgentBlueprintArgs | null) ?? {}; } /** * Building blocks, in the web's priority order. Only non-empty ones render as * pills; the rationale for anything intentionally omitted lives in Coverage * notes — no dead "Not needed" rows. */ export function buildBlueprintBlocks(args: AgentBlueprintArgs): BlueprintBlock[] { const all: BlueprintBlock[] = [ { key: 'persona', label: 'Persona', prose: args.persona }, { key: 'operating_rules', label: 'Operating rules', items: args.operating_rules }, { key: 'data_model', label: 'Data model', items: args.data_model }, { key: 'capabilities', label: 'Capabilities', items: args.capabilities }, { key: 'connectors', label: 'Connectors', items: args.connectors }, { key: 'skills', label: 'Skills', items: args.skills }, { key: 'automations', label: 'Automations', items: args.automations }, { key: 'channels', label: 'Channels', items: args.channels }, { key: 'knowledge', label: 'Knowledge', items: args.knowledge }, { key: 'memory', label: 'Memory', prose: args.memory }, ]; return all.filter((block) => block.prose ? block.prose.trim().length > 0 : (block.items?.length ?? 0) > 0, ); } /** * A genuine blueprint always carries agent_name, so a completed call with no * content is an anomaly (truncated payload or a call that never produced * results) — the widget shows a note instead of a blank card. */ export function hasBlueprintContent(args: AgentBlueprintArgs, blocks: BlueprintBlock[]): boolean { return blocks.length > 0 || Boolean(args.mission) || Boolean(args.agent_name); } /** * Blueprint items arrive as "Title — long description" strings. Split on the * first dash (em, en, or hyphen) padded by whitespace, so an inline hyphen * (e.g. "key fields: start-end") is never treated as the separator. */ export function splitHeadline(text: string): { title: string; description: string } { const match = text.match(/\s[—–-]\s/); if (!match || match.index === undefined) { return { title: text.trim(), description: '' }; } return { title: text.slice(0, match.index).trim(), description: text.slice(match.index + match[0].length).trim(), }; }