@kubuild/ai 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-27AY2KBI.js → chunk-3DRAVYU3.js} +173 -46
- package/dist/chunk-3DRAVYU3.js.map +1 -0
- package/dist/chunk-ILLNRABB.js +24 -0
- package/dist/chunk-ILLNRABB.js.map +1 -0
- package/dist/{chunk-JY4NHD3Q.js → chunk-K7CBTQCY.js} +91 -1
- package/dist/chunk-K7CBTQCY.js.map +1 -0
- package/dist/{chunk-UH24LHRY.js → chunk-YETTTWHI.js} +226 -1
- package/dist/chunk-YETTTWHI.js.map +1 -0
- package/dist/client/ai-client.d.ts +27 -1
- package/dist/client/ai-client.d.ts.map +1 -1
- package/dist/client/index.cjs +90 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/core/document-outline.d.ts +60 -0
- package/dist/core/document-outline.d.ts.map +1 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/messages.d.ts +18 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/prompt-compiler.d.ts +29 -0
- package/dist/core/prompt-compiler.d.ts.map +1 -1
- package/dist/index.cjs +261 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +31 -3
- package/dist/react/index.cjs +233 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +143 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/use-ai-agent.d.ts +47 -0
- package/dist/react/use-ai-agent.d.ts.map +1 -0
- package/dist/react/use-ai-generator.d.ts +2 -1
- package/dist/react/use-ai-generator.d.ts.map +1 -1
- package/dist/server/adapters/anthropic.d.ts.map +1 -1
- package/dist/server/adapters/custom.d.ts.map +1 -1
- package/dist/server/adapters/gemini.d.ts.map +1 -1
- package/dist/server/adapters/index.cjs +180 -45
- package/dist/server/adapters/index.cjs.map +1 -1
- package/dist/server/adapters/index.js +2 -1
- package/dist/server/adapters/openai.d.ts +25 -0
- package/dist/server/adapters/openai.d.ts.map +1 -1
- package/dist/server/agent.d.ts +59 -0
- package/dist/server/agent.d.ts.map +1 -0
- package/dist/server/engine.d.ts +5 -1
- package/dist/server/engine.d.ts.map +1 -1
- package/dist/server/handler.d.ts +18 -2
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/index.cjs +1843 -258
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1379 -133
- package/dist/server/index.js.map +1 -1
- package/dist/server/tools/helpers.d.ts +66 -0
- package/dist/server/tools/helpers.d.ts.map +1 -0
- package/dist/server/tools/index.d.ts +25 -0
- package/dist/server/tools/index.d.ts.map +1 -0
- package/dist/server/tools/read-tools.d.ts +3 -0
- package/dist/server/tools/read-tools.d.ts.map +1 -0
- package/dist/server/tools/types.d.ts +51 -0
- package/dist/server/tools/types.d.ts.map +1 -0
- package/dist/server/tools/write-tools.d.ts +3 -0
- package/dist/server/tools/write-tools.d.ts.map +1 -0
- package/dist/types.d.ts +224 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-27AY2KBI.js.map +0 -1
- package/dist/chunk-JY4NHD3Q.js.map +0 -1
- package/dist/chunk-UH24LHRY.js.map +0 -1
package/dist/server/index.cjs
CHANGED
|
@@ -23,16 +23,162 @@ __export(server_exports, {
|
|
|
23
23
|
AnthropicAdapter: () => AnthropicAdapter,
|
|
24
24
|
CustomHttpAdapter: () => CustomHttpAdapter,
|
|
25
25
|
GeminiAdapter: () => GeminiAdapter,
|
|
26
|
+
KubuildAiAgent: () => KubuildAiAgent,
|
|
26
27
|
KubuildAiEngine: () => KubuildAiEngine,
|
|
27
28
|
OpenAiAdapter: () => OpenAiAdapter,
|
|
29
|
+
READ_TOOLS: () => READ_TOOLS,
|
|
30
|
+
WRITE_TOOLS: () => WRITE_TOOLS,
|
|
31
|
+
checkComponentType: () => checkComponentType,
|
|
32
|
+
checkDocumentSecurity: () => checkDocumentSecurity,
|
|
33
|
+
checkNesting: () => checkNesting,
|
|
28
34
|
createAiHandler: () => createAiHandler,
|
|
29
|
-
|
|
35
|
+
createDocumentTools: () => createDocumentTools,
|
|
36
|
+
normalizeIncomingNode: () => normalizeIncomingNode,
|
|
37
|
+
processAiRequest: () => processAiRequest,
|
|
38
|
+
suggestNodeIds: () => suggestNodeIds,
|
|
39
|
+
toToolDefinitions: () => toToolDefinitions
|
|
30
40
|
});
|
|
31
41
|
module.exports = __toCommonJS(server_exports);
|
|
32
42
|
|
|
33
43
|
// src/server/engine.ts
|
|
34
44
|
var import_schema2 = require("@kubuild/schema");
|
|
35
45
|
|
|
46
|
+
// src/core/document-outline.ts
|
|
47
|
+
var LABEL_PROPS = ["text", "label", "title", "heading", "content", "placeholder", "alt", "src"];
|
|
48
|
+
var DEFAULT_LABEL_MAX = 60;
|
|
49
|
+
function truncate(value, max = DEFAULT_LABEL_MAX) {
|
|
50
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
51
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
|
|
52
|
+
}
|
|
53
|
+
function getNodeLabel(node, max = DEFAULT_LABEL_MAX) {
|
|
54
|
+
const props = node.props ?? {};
|
|
55
|
+
for (const key of LABEL_PROPS) {
|
|
56
|
+
const value = props[key];
|
|
57
|
+
if (typeof value === "string" && value.trim()) {
|
|
58
|
+
return truncate(value, max);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
function isOnPathTo(node, targetId) {
|
|
64
|
+
if (node.id === targetId) return true;
|
|
65
|
+
return (node.children ?? []).some((child) => isOnPathTo(child, targetId));
|
|
66
|
+
}
|
|
67
|
+
function summarizeNodeTree(root, options = {}) {
|
|
68
|
+
const { maxDepth = 3, focusNodeId, maxNodes = 200 } = options;
|
|
69
|
+
const lines = [];
|
|
70
|
+
let emitted = 0;
|
|
71
|
+
let truncated = false;
|
|
72
|
+
const walk = (node, depth) => {
|
|
73
|
+
if (emitted >= maxNodes) {
|
|
74
|
+
truncated = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const indent = " ".repeat(depth);
|
|
78
|
+
const label = getNodeLabel(node);
|
|
79
|
+
const children = node.children ?? [];
|
|
80
|
+
const parts = [`${indent}- #${node.id} (${node.type})`];
|
|
81
|
+
if (label) parts.push(`"${label}"`);
|
|
82
|
+
if (children.length > 0) parts.push(`[${children.length} children]`);
|
|
83
|
+
if (focusNodeId && node.id === focusNodeId) parts.push("\u2190 SELECTED");
|
|
84
|
+
lines.push(parts.join(" "));
|
|
85
|
+
emitted++;
|
|
86
|
+
if (children.length === 0) return;
|
|
87
|
+
const onFocusPath = Boolean(focusNodeId) && isOnPathTo(node, focusNodeId);
|
|
88
|
+
if (depth + 1 > maxDepth && !onFocusPath) {
|
|
89
|
+
lines.push(`${indent} (+${countDescendants(node)} nested nodes \u2014 use read_node to expand)`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const child of children) {
|
|
93
|
+
walk(child, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(root, 0);
|
|
97
|
+
if (truncated) {
|
|
98
|
+
lines.push(` (outline truncated at ${maxNodes} nodes \u2014 use find_nodes to locate specifics)`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
function countDescendants(node) {
|
|
103
|
+
return (node.children ?? []).reduce((total, child) => total + 1 + countDescendants(child), 0);
|
|
104
|
+
}
|
|
105
|
+
function summarizeDocument(document, options = {}) {
|
|
106
|
+
const header = [
|
|
107
|
+
`Title: ${document.metadata?.title || "Untitled Page"}`,
|
|
108
|
+
`Description: ${document.metadata?.description || "N/A"}`
|
|
109
|
+
].join("\n");
|
|
110
|
+
return `${header}
|
|
111
|
+
|
|
112
|
+
Page outline:
|
|
113
|
+
${summarizeNodeTree(document.document, options)}`;
|
|
114
|
+
}
|
|
115
|
+
function buildSelectionContext(root, nodeId) {
|
|
116
|
+
const ancestors = [];
|
|
117
|
+
const walk = (node, parent, index) => {
|
|
118
|
+
if (node.id === nodeId) {
|
|
119
|
+
return {
|
|
120
|
+
node,
|
|
121
|
+
ancestors: [...ancestors],
|
|
122
|
+
siblings: parent ? parent.children ?? [] : [node],
|
|
123
|
+
index,
|
|
124
|
+
parentId: parent?.id ?? null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
ancestors.push(node);
|
|
128
|
+
const children = node.children ?? [];
|
|
129
|
+
for (let i = 0; i < children.length; i++) {
|
|
130
|
+
const found = walk(children[i], node, i);
|
|
131
|
+
if (found) return found;
|
|
132
|
+
}
|
|
133
|
+
ancestors.pop();
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
return walk(root, null, 0);
|
|
137
|
+
}
|
|
138
|
+
function pruneNodeForPrompt(node, maxDepth = 2, depth = 0) {
|
|
139
|
+
const children = node.children ?? [];
|
|
140
|
+
if (children.length === 0) return node;
|
|
141
|
+
if (depth >= maxDepth) {
|
|
142
|
+
return {
|
|
143
|
+
...node,
|
|
144
|
+
children: [
|
|
145
|
+
{
|
|
146
|
+
id: `${node.id}__collapsed`,
|
|
147
|
+
type: "__collapsed__",
|
|
148
|
+
props: {
|
|
149
|
+
note: `${countDescendants(node)} nested nodes omitted \u2014 call read_node on #${node.id} with includeChildren to expand`
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
...node,
|
|
157
|
+
children: children.map((child) => pruneNodeForPrompt(child, maxDepth, depth + 1))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function formatSelectionContext(context) {
|
|
161
|
+
const path = context.ancestors.map((a) => `#${a.id}(${a.type})`).join(" > ");
|
|
162
|
+
const siblings = context.siblings.map((sibling, i) => {
|
|
163
|
+
const marker = sibling.id === context.node.id ? " \u2190 selected" : "";
|
|
164
|
+
const label = getNodeLabel(sibling);
|
|
165
|
+
return ` [${i}] #${sibling.id} (${sibling.type})${label ? ` "${label}"` : ""}${marker}`;
|
|
166
|
+
}).join("\n");
|
|
167
|
+
return [
|
|
168
|
+
"### Currently Selected Component",
|
|
169
|
+
`Node: #${context.node.id} (${context.node.type})`,
|
|
170
|
+
`Parent: ${context.parentId ? `#${context.parentId}` : "(root)"} \u2014 position ${context.index}`,
|
|
171
|
+
path ? `Path: ${path}` : "",
|
|
172
|
+
"Full node JSON:",
|
|
173
|
+
"```json",
|
|
174
|
+
JSON.stringify(pruneNodeForPrompt(context.node), null, 2),
|
|
175
|
+
"```",
|
|
176
|
+
"Siblings:",
|
|
177
|
+
siblings,
|
|
178
|
+
'When the user says "this", "ini", "komponen ini" or similar, they mean the selected node above.'
|
|
179
|
+
].filter(Boolean).join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
36
182
|
// src/core/prompt-compiler.ts
|
|
37
183
|
function compileComponentCatalog(registry) {
|
|
38
184
|
if (!registry) return [];
|
|
@@ -214,6 +360,100 @@ function buildJsonSchemaForMode(mode) {
|
|
|
214
360
|
}
|
|
215
361
|
return nodeSchema;
|
|
216
362
|
}
|
|
363
|
+
function formatComponentCatalog(catalog) {
|
|
364
|
+
return catalog.map((c) => {
|
|
365
|
+
let desc = `- **${c.type}** (${c.category}): ${c.label}`;
|
|
366
|
+
if (c.description) desc += ` - ${c.description}`;
|
|
367
|
+
desc += ` | acceptsChildren: ${c.acceptsChildren}`;
|
|
368
|
+
if (c.allowedChildren && c.allowedChildren.length > 0) {
|
|
369
|
+
desc += ` | allowedChildren: [${c.allowedChildren.join(", ")}]`;
|
|
370
|
+
}
|
|
371
|
+
if (c.props && c.props.length > 0) {
|
|
372
|
+
const propList = c.props.map((p) => {
|
|
373
|
+
let pStr = `${p.name} (${p.type})`;
|
|
374
|
+
if (p.options) pStr += `[${p.options.map(String).join("|")}]`;
|
|
375
|
+
return pStr;
|
|
376
|
+
}).join(", ");
|
|
377
|
+
desc += ` | props: { ${propList} }`;
|
|
378
|
+
}
|
|
379
|
+
return desc;
|
|
380
|
+
}).join("\n");
|
|
381
|
+
}
|
|
382
|
+
var AGENT_INSTRUCTIONS = `You are the KUBUILD page-editing agent. You edit an existing page by calling tools \u2014 you never output page JSON directly.
|
|
383
|
+
|
|
384
|
+
### HOW YOU WORK
|
|
385
|
+
1. The page outline below is your map. It shows every node's id, type and label, but not its props or styles.
|
|
386
|
+
2. To see a node's actual props/styles, call read_node. To locate nodes by type or text, call find_nodes. Never guess a node id \u2014 every id you pass must come from the outline or from a tool result.
|
|
387
|
+
3. Make the change with the smallest tool that expresses it, then stop and summarize what you changed, in the user's language.
|
|
388
|
+
|
|
389
|
+
### SURGICAL EDIT RULE (the most important rule)
|
|
390
|
+
Change only what the user asked for.
|
|
391
|
+
- Changing text/label/href/src -> update_node_props on that one node.
|
|
392
|
+
- Changing color/spacing/size/alignment -> update_node_styles on that one node.
|
|
393
|
+
- Adding something new -> insert_component (a single element) or insert_section (a whole new section).
|
|
394
|
+
- replace_node is a last resort, only when a node's children genuinely must be restructured. Never use it to change a prop or a style.
|
|
395
|
+
- Never rebuild a section, and never touch nodes the user did not ask about. If an instruction is ambiguous about scope, edit the narrowest node that satisfies it and say so in your summary.
|
|
396
|
+
|
|
397
|
+
### DESTRUCTIVE ACTIONS
|
|
398
|
+
delete_node and replace_node destroy user content. Only call them when the user asked unambiguously (e.g. "hapus section harga"). If the request is broad or vague ("bersihkan halaman ini", "hapus yang tidak perlu"), do not call them \u2014 ask the user which nodes to remove instead.
|
|
399
|
+
|
|
400
|
+
### STYLING RULES
|
|
401
|
+
- styles values are CSS primitives (strings/numbers). Use breakpoint ('base' | 'desktop' | 'tablet' | 'mobile') for normal styling and state (':hover', ':focus', ':active') for interactive states. Never nest a pseudo-class inside a breakpoint layer.
|
|
402
|
+
- Only use component types that exist in the catalog below, and respect acceptsChildren/allowedChildren.
|
|
403
|
+
- Never emit scripts, javascript: URIs, or event-handler attributes.
|
|
404
|
+
|
|
405
|
+
### WHEN A TOOL FAILS
|
|
406
|
+
A tool result with "ok": false means your input was wrong (unknown node id, invalid type, disallowed nesting). Read the error, fix your arguments, and retry \u2014 do not repeat the identical call, and do not give up silently.`;
|
|
407
|
+
function buildAgentSystemPrompt(options) {
|
|
408
|
+
const { catalog, document, selectedNodeId, prefix, stylePreference, additionalContext } = options;
|
|
409
|
+
const selection = selectedNodeId ? buildSelectionContext(document.document, selectedNodeId) : null;
|
|
410
|
+
const sections = [];
|
|
411
|
+
if (prefix) sections.push(prefix);
|
|
412
|
+
sections.push(AGENT_INSTRUCTIONS);
|
|
413
|
+
sections.push(
|
|
414
|
+
`### REGISTERED COMPONENT CATALOG
|
|
415
|
+
${formatComponentCatalog(catalog) || "(Standard core components: page, section, container, columns, heading, paragraph, text, button, input, textarea, select, image, video, icon, badge, code-block)"}`
|
|
416
|
+
);
|
|
417
|
+
sections.push(`### CURRENT PAGE
|
|
418
|
+
${summarizeDocument(document, { focusNodeId: selectedNodeId })}`);
|
|
419
|
+
if (selection) {
|
|
420
|
+
sections.push(formatSelectionContext(selection));
|
|
421
|
+
} else if (selectedNodeId) {
|
|
422
|
+
sections.push(
|
|
423
|
+
`### Selection
|
|
424
|
+
The editor reported node "${selectedNodeId}" as selected, but it is not present in the current page. Ignore it and ask the user what to target.`
|
|
425
|
+
);
|
|
426
|
+
} else {
|
|
427
|
+
sections.push(
|
|
428
|
+
'### Selection\nNo component is selected on the canvas. If the user says "this", ask which component they mean instead of guessing.'
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (stylePreference) {
|
|
432
|
+
sections.push(
|
|
433
|
+
`### Style Preference
|
|
434
|
+
"${stylePreference}" \u2014 follow this aesthetic for anything you create or restyle.`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (additionalContext) {
|
|
438
|
+
sections.push(`### Additional Context
|
|
439
|
+
${additionalContext}`);
|
|
440
|
+
}
|
|
441
|
+
return sections.join("\n\n");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/core/messages.ts
|
|
445
|
+
function getMessageText(message) {
|
|
446
|
+
if (typeof message.content === "string") return message.content;
|
|
447
|
+
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
448
|
+
}
|
|
449
|
+
function getToolUseBlocks(message) {
|
|
450
|
+
if (typeof message.content === "string") return [];
|
|
451
|
+
return message.content.filter((b) => b.type === "tool_use");
|
|
452
|
+
}
|
|
453
|
+
function getToolResultBlocks(message) {
|
|
454
|
+
if (typeof message.content === "string") return [];
|
|
455
|
+
return message.content.filter((b) => b.type === "tool_result");
|
|
456
|
+
}
|
|
217
457
|
|
|
218
458
|
// src/core/normalizer.ts
|
|
219
459
|
var import_schema = require("@kubuild/schema");
|
|
@@ -495,6 +735,13 @@ var KubuildAiEngine = class {
|
|
|
495
735
|
Tone: ${request.tone}`;
|
|
496
736
|
if (request.locale) userPrompt += `
|
|
497
737
|
Language/Locale: ${request.locale}`;
|
|
738
|
+
if (request.conversationHistory && request.conversationHistory.length > 0) {
|
|
739
|
+
const chatContext = request.conversationHistory.map((m) => `${m.role.toUpperCase()}: ${getMessageText(m)}`).join("\n");
|
|
740
|
+
userPrompt += `
|
|
741
|
+
|
|
742
|
+
Prior Conversation Discussion Context:
|
|
743
|
+
${chatContext}`;
|
|
744
|
+
}
|
|
498
745
|
const jsonSchema = buildJsonSchemaForMode("full-page");
|
|
499
746
|
const result = await this.options.adapter.generate({
|
|
500
747
|
systemPrompt,
|
|
@@ -633,18 +880,18 @@ ${JSON.stringify(request.node, null, 2)}`;
|
|
|
633
880
|
};
|
|
634
881
|
}
|
|
635
882
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
883
|
+
buildPlanPrompts(request) {
|
|
884
|
+
let sectionGuidance = "Plan between 4 to 6 cohesive, essential sections (e.g., hero, features, testimonials, pricing, cta, footer) that fulfill the request thoroughly.";
|
|
885
|
+
if (typeof request.sectionCount === "number") {
|
|
886
|
+
sectionGuidance = `Plan exactly ${request.sectionCount} cohesive sections that fulfill the request.`;
|
|
887
|
+
} else if (request.sectionCount?.min || request.sectionCount?.max) {
|
|
888
|
+
const min = request.sectionCount.min ?? 3;
|
|
889
|
+
const max = request.sectionCount.max ?? 8;
|
|
890
|
+
sectionGuidance = `Plan between ${min} and ${max} cohesive sections that fulfill the request.`;
|
|
891
|
+
} else {
|
|
892
|
+
sectionGuidance += " If the user request mentions a specific number or list of sections, respect the user request.";
|
|
893
|
+
}
|
|
894
|
+
const planSystemPrompt = `
|
|
648
895
|
You are a web architect for the KUBUILD page builder.
|
|
649
896
|
Given the user's prompt, plan the website structure. Output pure JSON (no markdown fences, no explanatory text):
|
|
650
897
|
{
|
|
@@ -666,44 +913,113 @@ Given the user's prompt, plan the website structure. Output pure JSON (no markdo
|
|
|
666
913
|
}
|
|
667
914
|
]
|
|
668
915
|
}
|
|
669
|
-
|
|
916
|
+
${sectionGuidance}
|
|
670
917
|
`;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
918
|
+
let planUserPrompt = `User Request: ${request.prompt}`;
|
|
919
|
+
if (request.stylePreference) {
|
|
920
|
+
planUserPrompt += `
|
|
674
921
|
Style Preference: ${request.stylePreference}`;
|
|
675
|
-
|
|
676
|
-
|
|
922
|
+
}
|
|
923
|
+
if (request.tone) planUserPrompt += `
|
|
677
924
|
Tone: ${request.tone}`;
|
|
678
|
-
|
|
925
|
+
if (request.locale) planUserPrompt += `
|
|
679
926
|
Locale: ${request.locale}`;
|
|
680
|
-
|
|
927
|
+
if (request.conversationHistory && request.conversationHistory.length > 0) {
|
|
928
|
+
const chatContext = request.conversationHistory.map((m) => `${m.role.toUpperCase()}: ${getMessageText(m)}`).join("\n");
|
|
929
|
+
planUserPrompt += `
|
|
930
|
+
|
|
931
|
+
Prior Conversation Discussion Context:
|
|
932
|
+
${chatContext}`;
|
|
933
|
+
}
|
|
934
|
+
return { systemPrompt: planSystemPrompt, userPrompt: planUserPrompt };
|
|
935
|
+
}
|
|
936
|
+
async planPage(request, context) {
|
|
937
|
+
let rawText = "";
|
|
938
|
+
try {
|
|
939
|
+
const { systemPrompt, userPrompt } = this.buildPlanPrompts(request);
|
|
940
|
+
this.log("info", `Planning website layout for: "${request.prompt}"`);
|
|
681
941
|
const planResult = await this.options.adapter.generate({
|
|
682
|
-
systemPrompt
|
|
683
|
-
userPrompt
|
|
942
|
+
systemPrompt,
|
|
943
|
+
userPrompt,
|
|
684
944
|
signal: context?.signal
|
|
685
945
|
});
|
|
686
|
-
|
|
946
|
+
rawText = planResult.text;
|
|
947
|
+
const plan = extractJsonFromResponse(rawText);
|
|
948
|
+
return {
|
|
949
|
+
success: true,
|
|
950
|
+
data: plan,
|
|
951
|
+
usage: planResult.usage,
|
|
952
|
+
rawModelResponse: rawText
|
|
953
|
+
};
|
|
954
|
+
} catch (err) {
|
|
955
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
956
|
+
this.log("warn", "Failed to generate plan JSON, using fallback plan", message);
|
|
957
|
+
const fallbackPlan = {
|
|
958
|
+
title: "AI Generated Page",
|
|
959
|
+
description: "Generated by KUBUILD AI",
|
|
960
|
+
sections: [
|
|
961
|
+
{ type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
|
|
962
|
+
{ type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
|
|
963
|
+
{ type: "testimonials", title: "Testimonials", prompt: `Social proof and customer reviews for: ${request.prompt}` },
|
|
964
|
+
{ type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` },
|
|
965
|
+
{ type: "footer", title: "Footer", prompt: `Footer navigation and copyright for: ${request.prompt}` }
|
|
966
|
+
]
|
|
967
|
+
};
|
|
968
|
+
return {
|
|
969
|
+
success: true,
|
|
970
|
+
data: fallbackPlan,
|
|
971
|
+
rawModelResponse: rawText || void 0
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Progressive Section Streaming Generator.
|
|
977
|
+
* Emits structured SSE events: status -> metadata -> section (one by one) -> complete.
|
|
978
|
+
*/
|
|
979
|
+
async *streamPage(request, context) {
|
|
980
|
+
try {
|
|
981
|
+
this.log("info", `[SSE] Starting streamPage for prompt: "${request.prompt}"`);
|
|
982
|
+
yield {
|
|
983
|
+
type: "status",
|
|
984
|
+
message: "Analyzing requirements and planning page sections..."
|
|
985
|
+
};
|
|
687
986
|
let plan;
|
|
688
|
-
|
|
689
|
-
plan =
|
|
690
|
-
this.log("
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
987
|
+
if (request.plan && Array.isArray(request.plan.sections) && request.plan.sections.length > 0) {
|
|
988
|
+
plan = request.plan;
|
|
989
|
+
this.log("info", `[SSE] Using approved pre-planned structure with ${plan.sections?.length ?? 0} sections`);
|
|
990
|
+
} else {
|
|
991
|
+
const { systemPrompt: planSystemPrompt, userPrompt: planUserPrompt } = this.buildPlanPrompts(request);
|
|
992
|
+
this.log("info", "[SSE] Generating website layout plan...");
|
|
993
|
+
const planResult = await this.options.adapter.generate({
|
|
994
|
+
systemPrompt: planSystemPrompt,
|
|
995
|
+
userPrompt: planUserPrompt,
|
|
996
|
+
signal: context?.signal
|
|
997
|
+
});
|
|
998
|
+
this.log("debug", "[SSE] Raw plan response from model", planResult.text);
|
|
999
|
+
try {
|
|
1000
|
+
plan = extractJsonFromResponse(planResult.text);
|
|
1001
|
+
this.log("debug", "[SSE] Parsed plan successfully", plan);
|
|
1002
|
+
} catch (parseErr) {
|
|
1003
|
+
this.log("warn", "[SSE] Failed to parse plan JSON, using fallback plan", parseErr);
|
|
1004
|
+
plan = {
|
|
1005
|
+
title: "AI Generated Page",
|
|
1006
|
+
description: "Generated by KUBUILD AI",
|
|
1007
|
+
sections: [
|
|
1008
|
+
{ type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
|
|
1009
|
+
{ type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
|
|
1010
|
+
{ type: "testimonials", title: "Testimonials", prompt: `Social proof for: ${request.prompt}` },
|
|
1011
|
+
{ type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` },
|
|
1012
|
+
{ type: "footer", title: "Footer", prompt: `Footer for: ${request.prompt}` }
|
|
1013
|
+
]
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
702
1016
|
}
|
|
703
1017
|
const sectionsToGenerate = Array.isArray(plan.sections) && plan.sections.length > 0 ? plan.sections : [
|
|
704
1018
|
{ type: "hero", title: "Hero Section", prompt: `Hero banner for: ${request.prompt}` },
|
|
705
1019
|
{ type: "features", title: "Features", prompt: `Features grid for: ${request.prompt}` },
|
|
706
|
-
{ type: "
|
|
1020
|
+
{ type: "testimonials", title: "Testimonials", prompt: `Social proof for: ${request.prompt}` },
|
|
1021
|
+
{ type: "cta", title: "Call To Action", prompt: `CTA section for: ${request.prompt}` },
|
|
1022
|
+
{ type: "footer", title: "Footer", prompt: `Footer section for: ${request.prompt}` }
|
|
707
1023
|
];
|
|
708
1024
|
const metadata = import_schema2.DocumentMetadataSchema.parse({
|
|
709
1025
|
title: plan.title || "AI Generated Page",
|
|
@@ -863,7 +1179,7 @@ ${sectionSummary || "(Canvas is currently empty)"}
|
|
|
863
1179
|
Currently Selected Component Node ID: "${request.selectedNodeId}"`;
|
|
864
1180
|
}
|
|
865
1181
|
const lastUserMsg = [...request.messages].reverse().find((m) => m.role === "user");
|
|
866
|
-
const userPrompt = lastUserMsg
|
|
1182
|
+
const userPrompt = lastUserMsg ? getMessageText(lastUserMsg) || "Hello" : "Hello";
|
|
867
1183
|
return { systemPrompt, userPrompt };
|
|
868
1184
|
}
|
|
869
1185
|
/**
|
|
@@ -1004,105 +1320,1154 @@ Currently Selected Component Node ID: "${request.selectedNodeId}"`;
|
|
|
1004
1320
|
}
|
|
1005
1321
|
};
|
|
1006
1322
|
|
|
1007
|
-
// src/server/
|
|
1008
|
-
|
|
1009
|
-
|
|
1323
|
+
// src/server/tools/helpers.ts
|
|
1324
|
+
var import_schema3 = require("@kubuild/schema");
|
|
1325
|
+
var import_core2 = require("@kubuild/core");
|
|
1326
|
+
function fail(summary, message, extra) {
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
summary,
|
|
1330
|
+
content: { ok: false, error: message, ...extra ?? {} }
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function succeed(summary, content, rest) {
|
|
1334
|
+
return { ok: true, summary, content: { ok: true, ...content }, ...rest ?? {} };
|
|
1335
|
+
}
|
|
1336
|
+
function readString(input, key) {
|
|
1337
|
+
const value = input[key];
|
|
1338
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1339
|
+
}
|
|
1340
|
+
function readPlainObject(input, key) {
|
|
1341
|
+
const value = input[key];
|
|
1342
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1343
|
+
return value;
|
|
1344
|
+
}
|
|
1345
|
+
function readOptionalIndex(input, key) {
|
|
1346
|
+
const value = input[key];
|
|
1347
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
1348
|
+
return void 0;
|
|
1349
|
+
}
|
|
1350
|
+
function similarity(a, b) {
|
|
1351
|
+
if (a === b) return Infinity;
|
|
1352
|
+
let shared = 0;
|
|
1353
|
+
const max = Math.min(a.length, b.length);
|
|
1354
|
+
while (shared < max && a[shared] === b[shared]) shared++;
|
|
1355
|
+
return shared + (a.includes(b) || b.includes(a) ? 3 : 0);
|
|
1356
|
+
}
|
|
1357
|
+
function suggestNodeIds(document, badId, limit = 5) {
|
|
1358
|
+
return Array.from((0, import_core2.collectNodeIdSet)(document.document)).map((id) => ({ id, score: similarity(id.toLowerCase(), badId.toLowerCase()) })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => entry.id);
|
|
1359
|
+
}
|
|
1360
|
+
function resolveNode(document, nodeId, toolName) {
|
|
1361
|
+
const node = (0, import_core2.findNodeById)(document.document, nodeId);
|
|
1362
|
+
if (!node) {
|
|
1010
1363
|
return {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1364
|
+
error: fail(
|
|
1365
|
+
`${toolName}: node "${nodeId}" tidak ditemukan`,
|
|
1366
|
+
`No node with id "${nodeId}" exists in this page. Node ids must come from the outline or from a tool result \u2014 never invented.`,
|
|
1367
|
+
{ suggestedNodeIds: suggestNodeIds(document, nodeId) }
|
|
1368
|
+
)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return { node };
|
|
1372
|
+
}
|
|
1373
|
+
function findCatalogEntry(catalog, type) {
|
|
1374
|
+
return catalog.find((entry) => entry.type === type);
|
|
1375
|
+
}
|
|
1376
|
+
function checkNesting(catalog, parent, childType) {
|
|
1377
|
+
if (catalog.length === 0) return null;
|
|
1378
|
+
const parentSpec = findCatalogEntry(catalog, parent.type);
|
|
1379
|
+
if (parentSpec && !parentSpec.acceptsChildren) {
|
|
1380
|
+
return `Component "${parent.type}" (#${parent.id}) cannot contain children. Insert into a container/section node instead.`;
|
|
1381
|
+
}
|
|
1382
|
+
if (parentSpec?.allowedChildren && parentSpec.allowedChildren.length > 0) {
|
|
1383
|
+
if (!parentSpec.allowedChildren.includes(childType)) {
|
|
1384
|
+
return `Component "${parent.type}" only accepts children of type [${parentSpec.allowedChildren.join(", ")}] \u2014 "${childType}" is not allowed.`;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const childSpec = findCatalogEntry(catalog, childType);
|
|
1388
|
+
if (childSpec?.disallowedParents?.includes(parent.type)) {
|
|
1389
|
+
return `Component "${childType}" cannot be placed inside "${parent.type}".`;
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
function checkComponentType(catalog, type) {
|
|
1394
|
+
if (catalog.length === 0) return null;
|
|
1395
|
+
if (findCatalogEntry(catalog, type)) return null;
|
|
1396
|
+
const known = catalog.map((entry) => entry.type);
|
|
1397
|
+
return `Unknown component type "${type}". Use list_component_types to see valid types. Known types: ${known.slice(0, 40).join(", ")}${known.length > 40 ? ", \u2026" : ""}`;
|
|
1398
|
+
}
|
|
1399
|
+
function checkDocumentSecurity(document, limits) {
|
|
1400
|
+
const result = (0, import_core2.validateDocumentSecurity)(document, limits);
|
|
1401
|
+
if (result.safe) return null;
|
|
1402
|
+
return result.errors.map((error) => `${error.code}: ${error.message}`).join("; ");
|
|
1403
|
+
}
|
|
1404
|
+
function normalizeIncomingNode(raw, document, options = {}) {
|
|
1405
|
+
let normalized;
|
|
1406
|
+
try {
|
|
1407
|
+
normalized = normalizeNodeTree(raw);
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
1410
|
+
}
|
|
1411
|
+
if (!options.preserveIds) {
|
|
1412
|
+
const taken = (0, import_core2.collectNodeIdSet)(document.document);
|
|
1413
|
+
const reassign = (node) => {
|
|
1414
|
+
if (!node.id || taken.has(node.id)) {
|
|
1415
|
+
let counter = 1;
|
|
1416
|
+
let candidate = `${node.type}-${counter}`;
|
|
1417
|
+
while (taken.has(candidate)) {
|
|
1418
|
+
counter++;
|
|
1419
|
+
candidate = `${node.type}-${counter}`;
|
|
1017
1420
|
}
|
|
1421
|
+
node.id = candidate;
|
|
1018
1422
|
}
|
|
1423
|
+
taken.add(node.id);
|
|
1424
|
+
for (const child of node.children ?? []) reassign(child);
|
|
1019
1425
|
};
|
|
1426
|
+
reassign(normalized);
|
|
1020
1427
|
}
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
if (mode === "full-page") {
|
|
1024
|
-
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
1025
|
-
return {
|
|
1026
|
-
status: 400,
|
|
1027
|
-
response: {
|
|
1028
|
-
success: false,
|
|
1029
|
-
error: {
|
|
1030
|
-
code: "INVALID_PROMPT",
|
|
1031
|
-
message: '"prompt" is required for full-page generation'
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
const result = await engine.generatePage(
|
|
1037
|
-
{
|
|
1038
|
-
prompt: payload.prompt,
|
|
1039
|
-
stylePreference: payload.stylePreference,
|
|
1040
|
-
tone: payload.tone,
|
|
1041
|
-
locale: payload.locale,
|
|
1042
|
-
metadata: payload.metadata
|
|
1043
|
-
},
|
|
1044
|
-
{ signal }
|
|
1045
|
-
);
|
|
1428
|
+
const parsed = import_schema3.NodeSchema.safeParse(normalized);
|
|
1429
|
+
if (!parsed.success) {
|
|
1046
1430
|
return {
|
|
1047
|
-
|
|
1048
|
-
response: result
|
|
1431
|
+
error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
1049
1432
|
};
|
|
1050
1433
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1434
|
+
return { node: parsed.data };
|
|
1435
|
+
}
|
|
1436
|
+
function describeNode(node) {
|
|
1437
|
+
const label = getNodeLabel(node, 32);
|
|
1438
|
+
return label ? `${node.type} "${label}" (#${node.id})` : `${node.type} (#${node.id})`;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/server/tools/read-tools.ts
|
|
1442
|
+
var getPageOutline = {
|
|
1443
|
+
kind: "read",
|
|
1444
|
+
definition: {
|
|
1445
|
+
name: "get_page_outline",
|
|
1446
|
+
description: "Return the page structure as an indented outline of node ids, types, labels and child counts. Use this to re-orient after edits. Does not include props or styles \u2014 call read_node for those.",
|
|
1447
|
+
inputSchema: {
|
|
1448
|
+
type: "object",
|
|
1449
|
+
properties: {
|
|
1450
|
+
maxDepth: {
|
|
1451
|
+
type: "integer",
|
|
1452
|
+
description: "How deep to expand the tree before collapsing. Default 3."
|
|
1061
1453
|
}
|
|
1062
|
-
}
|
|
1454
|
+
}
|
|
1063
1455
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
},
|
|
1071
|
-
{ signal }
|
|
1072
|
-
);
|
|
1073
|
-
return {
|
|
1074
|
-
status: result.success ? 200 : 500,
|
|
1075
|
-
response: result
|
|
1076
|
-
};
|
|
1456
|
+
},
|
|
1457
|
+
execute(input, context) {
|
|
1458
|
+
const maxDepth = typeof input.maxDepth === "number" && input.maxDepth > 0 ? Math.floor(input.maxDepth) : 3;
|
|
1459
|
+
return succeed("Membaca struktur halaman", {
|
|
1460
|
+
outline: summarizeNodeTree(context.document.document, { maxDepth })
|
|
1461
|
+
});
|
|
1077
1462
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1463
|
+
};
|
|
1464
|
+
var readNode = {
|
|
1465
|
+
kind: "read",
|
|
1466
|
+
definition: {
|
|
1467
|
+
name: "read_node",
|
|
1468
|
+
description: "Return one node with its full props and styles, plus its parent, position and siblings. Always read a node before editing it, so you change only the fields that need changing.",
|
|
1469
|
+
inputSchema: {
|
|
1470
|
+
type: "object",
|
|
1471
|
+
properties: {
|
|
1472
|
+
nodeId: { type: "string", description: "Id of the node to read (from the outline)." },
|
|
1473
|
+
includeChildren: {
|
|
1474
|
+
type: "boolean",
|
|
1475
|
+
description: "Include the node subtree. Default false \u2014 keeps the result small."
|
|
1088
1476
|
}
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
const result = await engine.refactorNode(
|
|
1092
|
-
{
|
|
1093
|
-
node: payload.node,
|
|
1094
|
-
instruction: payload.instruction,
|
|
1095
|
-
stylePreference: payload.stylePreference
|
|
1096
1477
|
},
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1478
|
+
required: ["nodeId"]
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
execute(input, context) {
|
|
1482
|
+
const nodeId = readString(input, "nodeId");
|
|
1483
|
+
if (!nodeId) return fail("read_node: nodeId kosong", 'Argument "nodeId" is required.');
|
|
1484
|
+
const resolved = resolveNode(context.document, nodeId, "read_node");
|
|
1485
|
+
if ("error" in resolved) return resolved.error;
|
|
1486
|
+
const selection = buildSelectionContext(context.document.document, nodeId);
|
|
1487
|
+
const includeChildren = input.includeChildren === true;
|
|
1488
|
+
const node = includeChildren ? pruneNodeForPrompt(resolved.node, 3) : { ...resolved.node, children: void 0 };
|
|
1489
|
+
return succeed(`Membaca node ${nodeId}`, {
|
|
1490
|
+
node,
|
|
1491
|
+
childCount: resolved.node.children?.length ?? 0,
|
|
1492
|
+
parentId: selection?.parentId ?? null,
|
|
1493
|
+
index: selection?.index ?? 0,
|
|
1494
|
+
ancestors: selection?.ancestors.map((a) => ({ id: a.id, type: a.type })) ?? [],
|
|
1495
|
+
siblings: selection?.siblings.map((sibling, i) => ({
|
|
1496
|
+
index: i,
|
|
1497
|
+
id: sibling.id,
|
|
1498
|
+
type: sibling.type,
|
|
1499
|
+
label: getNodeLabel(sibling)
|
|
1500
|
+
})) ?? []
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
var findNodes = {
|
|
1505
|
+
kind: "read",
|
|
1506
|
+
definition: {
|
|
1507
|
+
name: "find_nodes",
|
|
1508
|
+
description: 'Search the page for nodes by component type and/or visible text. Use this instead of guessing ids when the user refers to something by what it says ("the Get Started button").',
|
|
1509
|
+
inputSchema: {
|
|
1510
|
+
type: "object",
|
|
1511
|
+
properties: {
|
|
1512
|
+
type: { type: "string", description: 'Component type to match exactly, e.g. "button".' },
|
|
1513
|
+
textContains: {
|
|
1514
|
+
type: "string",
|
|
1515
|
+
description: "Case-insensitive substring to match against the node text/label props."
|
|
1516
|
+
},
|
|
1517
|
+
parentId: { type: "string", description: "Limit the search to this node subtree." },
|
|
1518
|
+
limit: { type: "integer", description: "Max results. Default 20." }
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
execute(input, context) {
|
|
1523
|
+
const type = readString(input, "type");
|
|
1524
|
+
const textContains = readString(input, "textContains");
|
|
1525
|
+
const parentId = readString(input, "parentId");
|
|
1526
|
+
const limit = typeof input.limit === "number" && input.limit > 0 ? Math.floor(input.limit) : 20;
|
|
1527
|
+
if (!type && !textContains) {
|
|
1528
|
+
return fail(
|
|
1529
|
+
"find_nodes: filter kosong",
|
|
1530
|
+
'Provide at least one of "type" or "textContains" \u2014 an unfiltered search would just return the whole outline.'
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
let root = context.document.document;
|
|
1534
|
+
if (parentId) {
|
|
1535
|
+
const resolved = resolveNode(context.document, parentId, "find_nodes");
|
|
1536
|
+
if ("error" in resolved) return resolved.error;
|
|
1537
|
+
root = resolved.node;
|
|
1538
|
+
}
|
|
1539
|
+
const needle = textContains?.toLowerCase();
|
|
1540
|
+
const matches = [];
|
|
1541
|
+
const walk = (node) => {
|
|
1542
|
+
if (matches.length >= limit) return;
|
|
1543
|
+
const label = getNodeLabel(node);
|
|
1544
|
+
const typeMatches = !type || node.type === type;
|
|
1545
|
+
const textMatches = !needle || label.toLowerCase().includes(needle);
|
|
1546
|
+
if (typeMatches && textMatches) {
|
|
1547
|
+
matches.push({ id: node.id, type: node.type, label });
|
|
1548
|
+
}
|
|
1549
|
+
for (const child of node.children ?? []) walk(child);
|
|
1102
1550
|
};
|
|
1551
|
+
walk(root);
|
|
1552
|
+
return succeed(
|
|
1553
|
+
`Mencari node (${matches.length} hasil)`,
|
|
1554
|
+
matches.length > 0 ? { matches, count: matches.length } : {
|
|
1555
|
+
matches: [],
|
|
1556
|
+
count: 0,
|
|
1557
|
+
hint: "Nothing matched. Try a broader filter, or call get_page_outline to see what exists."
|
|
1558
|
+
}
|
|
1559
|
+
);
|
|
1103
1560
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1561
|
+
};
|
|
1562
|
+
var listComponentTypes = {
|
|
1563
|
+
kind: "read",
|
|
1564
|
+
definition: {
|
|
1565
|
+
name: "list_component_types",
|
|
1566
|
+
description: "List component types available in this editor, with their props and nesting rules. Call this before inserting a component type you have not used yet.",
|
|
1567
|
+
inputSchema: {
|
|
1568
|
+
type: "object",
|
|
1569
|
+
properties: {
|
|
1570
|
+
category: { type: "string", description: 'Filter to one category, e.g. "layout".' }
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
execute(input, context) {
|
|
1575
|
+
const category = readString(input, "category");
|
|
1576
|
+
const entries = category ? context.catalog.filter((spec) => spec.category === category) : context.catalog;
|
|
1577
|
+
return succeed(`Melihat katalog komponen (${entries.length})`, {
|
|
1578
|
+
components: entries.map((spec) => ({
|
|
1579
|
+
type: spec.type,
|
|
1580
|
+
label: spec.label,
|
|
1581
|
+
category: spec.category,
|
|
1582
|
+
acceptsChildren: spec.acceptsChildren,
|
|
1583
|
+
allowedChildren: spec.allowedChildren,
|
|
1584
|
+
props: spec.props?.map((prop) => ({
|
|
1585
|
+
name: prop.name,
|
|
1586
|
+
type: prop.type,
|
|
1587
|
+
options: prop.options
|
|
1588
|
+
}))
|
|
1589
|
+
}))
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
var READ_TOOLS = [getPageOutline, readNode, findNodes, listComponentTypes];
|
|
1594
|
+
|
|
1595
|
+
// src/server/tools/write-tools.ts
|
|
1596
|
+
var import_core3 = require("@kubuild/core");
|
|
1597
|
+
var BREAKPOINTS = ["base", "desktop", "tablet", "mobile"];
|
|
1598
|
+
function applyCommand(toolName, run) {
|
|
1599
|
+
try {
|
|
1600
|
+
return run();
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1603
|
+
return { error: fail(`${toolName} gagal`, message) };
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function guardSecurity(toolName, document, context) {
|
|
1607
|
+
const violation = checkDocumentSecurity(document, context.securityLimits);
|
|
1608
|
+
if (!violation) return null;
|
|
1609
|
+
return fail(
|
|
1610
|
+
`${toolName} ditolak (security)`,
|
|
1611
|
+
`The resulting document failed the security check and was rejected: ${violation}`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
var updateNodeProps = {
|
|
1615
|
+
kind: "write",
|
|
1616
|
+
definition: {
|
|
1617
|
+
name: "update_node_props",
|
|
1618
|
+
description: "Change the props of one existing node (text, label, href, src, level, \u2026). This is the right tool for any content/copy change \u2014 it touches nothing else on the page.",
|
|
1619
|
+
inputSchema: {
|
|
1620
|
+
type: "object",
|
|
1621
|
+
properties: {
|
|
1622
|
+
nodeId: { type: "string", description: "Id of the node to edit." },
|
|
1623
|
+
props: {
|
|
1624
|
+
type: "object",
|
|
1625
|
+
description: "Props to write. Include only the props that should change."
|
|
1626
|
+
},
|
|
1627
|
+
merge: {
|
|
1628
|
+
type: "boolean",
|
|
1629
|
+
description: "true (default) merges with existing props. Only pass false to deliberately clear the other props."
|
|
1630
|
+
}
|
|
1631
|
+
},
|
|
1632
|
+
required: ["nodeId", "props"]
|
|
1633
|
+
}
|
|
1634
|
+
},
|
|
1635
|
+
execute(input, context) {
|
|
1636
|
+
const nodeId = readString(input, "nodeId");
|
|
1637
|
+
if (!nodeId) return fail("update_node_props: nodeId kosong", 'Argument "nodeId" is required.');
|
|
1638
|
+
const props = readPlainObject(input, "props");
|
|
1639
|
+
if (!props) {
|
|
1640
|
+
return fail(
|
|
1641
|
+
"update_node_props: props tidak valid",
|
|
1642
|
+
'Argument "props" must be a JSON object of prop names to values.'
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const resolved = resolveNode(context.document, nodeId, "update_node_props");
|
|
1646
|
+
if ("error" in resolved) return resolved.error;
|
|
1647
|
+
const merge = input.merge !== false;
|
|
1648
|
+
const applied = applyCommand(
|
|
1649
|
+
"update_node_props",
|
|
1650
|
+
() => (0, import_core3.updateProps)(context.document, { nodeId, props, merge })
|
|
1651
|
+
);
|
|
1652
|
+
if ("error" in applied) return applied.error;
|
|
1653
|
+
const violation = guardSecurity("update_node_props", applied.document, context);
|
|
1654
|
+
if (violation) return violation;
|
|
1655
|
+
const op = { kind: "update-props", nodeId, props, merge };
|
|
1656
|
+
const updated = (0, import_core3.findNodeById)(applied.document.document, nodeId);
|
|
1657
|
+
return succeed(
|
|
1658
|
+
`Ubah props ${describeNode(resolved.node)}`,
|
|
1659
|
+
{ nodeId, props: updated?.props ?? props, changedKeys: Object.keys(props) },
|
|
1660
|
+
{ op, document: applied.document }
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
var updateNodeStyles = {
|
|
1665
|
+
kind: "write",
|
|
1666
|
+
definition: {
|
|
1667
|
+
name: "update_node_styles",
|
|
1668
|
+
description: 'Change the styles of one existing node. Use `breakpoint` for normal styling and `state` for pseudo-classes like ":hover". This is the right tool for any color/spacing/size/typography change.',
|
|
1669
|
+
inputSchema: {
|
|
1670
|
+
type: "object",
|
|
1671
|
+
properties: {
|
|
1672
|
+
nodeId: { type: "string", description: "Id of the node to restyle." },
|
|
1673
|
+
styles: {
|
|
1674
|
+
type: "object",
|
|
1675
|
+
description: 'CSS properties in camelCase with primitive values, e.g. { "backgroundColor": "#16a34a", "paddingTop": "24px" }. Never nest objects here.'
|
|
1676
|
+
},
|
|
1677
|
+
breakpoint: {
|
|
1678
|
+
type: "string",
|
|
1679
|
+
enum: ["base", "desktop", "tablet", "mobile"],
|
|
1680
|
+
description: 'Breakpoint layer to write. Default "base".'
|
|
1681
|
+
},
|
|
1682
|
+
state: {
|
|
1683
|
+
type: "string",
|
|
1684
|
+
description: 'Pseudo-state layer such as ":hover", ":focus", ":active". Mutually exclusive with breakpoint.'
|
|
1685
|
+
},
|
|
1686
|
+
merge: {
|
|
1687
|
+
type: "boolean",
|
|
1688
|
+
description: "true (default) merges with existing styles in that layer."
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
required: ["nodeId", "styles"]
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
execute(input, context) {
|
|
1695
|
+
const nodeId = readString(input, "nodeId");
|
|
1696
|
+
if (!nodeId) return fail("update_node_styles: nodeId kosong", 'Argument "nodeId" is required.');
|
|
1697
|
+
const styles = readPlainObject(input, "styles");
|
|
1698
|
+
if (!styles) {
|
|
1699
|
+
return fail(
|
|
1700
|
+
"update_node_styles: styles tidak valid",
|
|
1701
|
+
'Argument "styles" must be a JSON object of CSS properties with primitive values.'
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
const nested = Object.entries(styles).find(
|
|
1705
|
+
([, value]) => value !== null && typeof value === "object"
|
|
1706
|
+
);
|
|
1707
|
+
if (nested) {
|
|
1708
|
+
return fail(
|
|
1709
|
+
"update_node_styles: nilai bersarang",
|
|
1710
|
+
`Style property "${nested[0]}" has an object value. Style values must be primitives \u2014 use the "state" argument for pseudo-classes instead of nesting them.`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
const rawState = readString(input, "state");
|
|
1714
|
+
const rawBreakpoint = readString(input, "breakpoint");
|
|
1715
|
+
if (rawState && rawBreakpoint) {
|
|
1716
|
+
return fail(
|
|
1717
|
+
"update_node_styles: argumen bentrok",
|
|
1718
|
+
'Pass either "breakpoint" or "state", not both \u2014 a pseudo-state layer is not per-breakpoint.'
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (rawBreakpoint && !BREAKPOINTS.includes(rawBreakpoint)) {
|
|
1722
|
+
return fail(
|
|
1723
|
+
"update_node_styles: breakpoint tidak dikenal",
|
|
1724
|
+
`Unknown breakpoint "${rawBreakpoint}". Valid values: ${BREAKPOINTS.join(", ")}.`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
const resolved = resolveNode(context.document, nodeId, "update_node_styles");
|
|
1728
|
+
if ("error" in resolved) return resolved.error;
|
|
1729
|
+
const state = rawState ? rawState.startsWith(":") ? rawState : `:${rawState}` : void 0;
|
|
1730
|
+
const breakpoint = state ? void 0 : rawBreakpoint ?? "base";
|
|
1731
|
+
const merge = input.merge !== false;
|
|
1732
|
+
const applied = applyCommand(
|
|
1733
|
+
"update_node_styles",
|
|
1734
|
+
() => (0, import_core3.updateStyle)(context.document, { nodeId, styles, breakpoint, state, merge })
|
|
1735
|
+
);
|
|
1736
|
+
if ("error" in applied) return applied.error;
|
|
1737
|
+
const violation = guardSecurity("update_node_styles", applied.document, context);
|
|
1738
|
+
if (violation) return violation;
|
|
1739
|
+
const op = { kind: "update-styles", nodeId, styles, breakpoint, state, merge };
|
|
1740
|
+
const layer = state ? `state ${state}` : `breakpoint ${breakpoint}`;
|
|
1741
|
+
return succeed(
|
|
1742
|
+
`Ubah style ${describeNode(resolved.node)} (${layer})`,
|
|
1743
|
+
{ nodeId, layer, appliedStyles: styles },
|
|
1744
|
+
{ op, document: applied.document }
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
var insertComponent = {
|
|
1749
|
+
kind: "write",
|
|
1750
|
+
definition: {
|
|
1751
|
+
name: "insert_component",
|
|
1752
|
+
description: "Insert one new component into an existing parent node. Use this for adding a single element (a button, a paragraph, an image). For a whole new page section, use insert_section instead.",
|
|
1753
|
+
inputSchema: {
|
|
1754
|
+
type: "object",
|
|
1755
|
+
properties: {
|
|
1756
|
+
parentId: { type: "string", description: "Id of the node that will contain the new component." },
|
|
1757
|
+
type: { type: "string", description: "Component type from the catalog." },
|
|
1758
|
+
index: {
|
|
1759
|
+
type: "integer",
|
|
1760
|
+
description: "Position among the parent children. Omit to append at the end."
|
|
1761
|
+
},
|
|
1762
|
+
props: { type: "object", description: "Initial props for the new component." },
|
|
1763
|
+
styles: {
|
|
1764
|
+
type: "object",
|
|
1765
|
+
description: 'Initial styles, e.g. { "base": { "marginTop": "16px" } }.'
|
|
1766
|
+
},
|
|
1767
|
+
children: {
|
|
1768
|
+
type: "array",
|
|
1769
|
+
description: "Optional nested child nodes, each { type, props?, styles?, children? }.",
|
|
1770
|
+
items: { type: "object" }
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
required: ["parentId", "type"]
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
execute(input, context) {
|
|
1777
|
+
const parentId = readString(input, "parentId");
|
|
1778
|
+
const type = readString(input, "type");
|
|
1779
|
+
if (!parentId || !type) {
|
|
1780
|
+
return fail(
|
|
1781
|
+
"insert_component: argumen kurang",
|
|
1782
|
+
'Arguments "parentId" and "type" are both required.'
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
const typeError = checkComponentType(context.catalog, type);
|
|
1786
|
+
if (typeError) return fail("insert_component: tipe tidak dikenal", typeError);
|
|
1787
|
+
const resolvedParent = resolveNode(context.document, parentId, "insert_component");
|
|
1788
|
+
if ("error" in resolvedParent) return resolvedParent.error;
|
|
1789
|
+
const nestingError = checkNesting(context.catalog, resolvedParent.node, type);
|
|
1790
|
+
if (nestingError) return fail("insert_component: nesting tidak valid", nestingError);
|
|
1791
|
+
const normalized = normalizeIncomingNode(
|
|
1792
|
+
{
|
|
1793
|
+
type,
|
|
1794
|
+
props: readPlainObject(input, "props") ?? void 0,
|
|
1795
|
+
styles: readPlainObject(input, "styles") ?? void 0,
|
|
1796
|
+
children: Array.isArray(input.children) ? input.children : void 0
|
|
1797
|
+
},
|
|
1798
|
+
context.document
|
|
1799
|
+
);
|
|
1800
|
+
if ("error" in normalized) {
|
|
1801
|
+
return fail("insert_component: node tidak valid", normalized.error);
|
|
1802
|
+
}
|
|
1803
|
+
const index = readOptionalIndex(input, "index");
|
|
1804
|
+
const applied = applyCommand(
|
|
1805
|
+
"insert_component",
|
|
1806
|
+
() => (0, import_core3.insertNode)(context.document, { parentId, node: normalized.node, index })
|
|
1807
|
+
);
|
|
1808
|
+
if ("error" in applied) return applied.error;
|
|
1809
|
+
const violation = guardSecurity("insert_component", applied.document, context);
|
|
1810
|
+
if (violation) return violation;
|
|
1811
|
+
const op = { kind: "insert-node", parentId, index, node: normalized.node };
|
|
1812
|
+
return succeed(
|
|
1813
|
+
`Tambah ${describeNode(normalized.node)} ke #${parentId}`,
|
|
1814
|
+
{ nodeId: normalized.node.id, parentId, index: index ?? null },
|
|
1815
|
+
{ op, document: applied.document }
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
var insertSection = {
|
|
1820
|
+
kind: "write",
|
|
1821
|
+
definition: {
|
|
1822
|
+
name: "insert_section",
|
|
1823
|
+
description: "Generate and insert a complete new page section (hero, features, pricing, FAQ, CTA, \u2026) from a description. Use this when the user asks to ADD a section \u2014 do not hand-build sections with insert_component.",
|
|
1824
|
+
inputSchema: {
|
|
1825
|
+
type: "object",
|
|
1826
|
+
properties: {
|
|
1827
|
+
prompt: {
|
|
1828
|
+
type: "string",
|
|
1829
|
+
description: "Detailed description of the section: purpose, content, layout and tone. Write it in the language the page uses."
|
|
1830
|
+
},
|
|
1831
|
+
index: {
|
|
1832
|
+
type: "integer",
|
|
1833
|
+
description: "Position among the page sections. Omit to append at the end."
|
|
1834
|
+
}
|
|
1835
|
+
},
|
|
1836
|
+
required: ["prompt"]
|
|
1837
|
+
}
|
|
1838
|
+
},
|
|
1839
|
+
async execute(input, context) {
|
|
1840
|
+
const prompt = readString(input, "prompt");
|
|
1841
|
+
if (!prompt) return fail("insert_section: prompt kosong", 'Argument "prompt" is required.');
|
|
1842
|
+
if (!context.generateSection) {
|
|
1843
|
+
return fail(
|
|
1844
|
+
"insert_section: tidak tersedia",
|
|
1845
|
+
"Section generation is not available in this deployment. Build the section with insert_component instead."
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const rootId = context.document.document.id;
|
|
1849
|
+
const existingSections = (context.document.document.children ?? []).map((child) => `${child.type}: ${getNodeLabel(child) || child.id}`).join(", ");
|
|
1850
|
+
let generated;
|
|
1851
|
+
try {
|
|
1852
|
+
generated = await context.generateSection({
|
|
1853
|
+
prompt,
|
|
1854
|
+
parentContext: `Page "${context.document.metadata?.title ?? "Untitled"}". Existing sections: ${existingSections || "(none)"}`
|
|
1855
|
+
});
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
return fail(
|
|
1858
|
+
"insert_section gagal",
|
|
1859
|
+
`Section generation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
const normalized = normalizeIncomingNode(generated, context.document);
|
|
1863
|
+
if ("error" in normalized) {
|
|
1864
|
+
return fail("insert_section: section tidak valid", normalized.error);
|
|
1865
|
+
}
|
|
1866
|
+
const index = readOptionalIndex(input, "index");
|
|
1867
|
+
const applied = applyCommand(
|
|
1868
|
+
"insert_section",
|
|
1869
|
+
() => (0, import_core3.insertNode)(context.document, { parentId: rootId, node: normalized.node, index })
|
|
1870
|
+
);
|
|
1871
|
+
if ("error" in applied) return applied.error;
|
|
1872
|
+
const violation = guardSecurity("insert_section", applied.document, context);
|
|
1873
|
+
if (violation) return violation;
|
|
1874
|
+
const op = { kind: "insert-node", parentId: rootId, index, node: normalized.node };
|
|
1875
|
+
return succeed(
|
|
1876
|
+
`Tambah section baru (#${normalized.node.id})`,
|
|
1877
|
+
{
|
|
1878
|
+
nodeId: normalized.node.id,
|
|
1879
|
+
index: index ?? null,
|
|
1880
|
+
childCount: normalized.node.children?.length ?? 0
|
|
1881
|
+
},
|
|
1882
|
+
{ op, document: applied.document }
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
var moveNodeTool = {
|
|
1887
|
+
kind: "write",
|
|
1888
|
+
definition: {
|
|
1889
|
+
name: "move_node",
|
|
1890
|
+
description: "Move an existing node to a different parent and/or position.",
|
|
1891
|
+
inputSchema: {
|
|
1892
|
+
type: "object",
|
|
1893
|
+
properties: {
|
|
1894
|
+
nodeId: { type: "string" },
|
|
1895
|
+
targetParentId: { type: "string", description: "Id of the new parent node." },
|
|
1896
|
+
index: { type: "integer", description: "Position within the new parent. Omit to append." }
|
|
1897
|
+
},
|
|
1898
|
+
required: ["nodeId", "targetParentId"]
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
execute(input, context) {
|
|
1902
|
+
const nodeId = readString(input, "nodeId");
|
|
1903
|
+
const targetParentId = readString(input, "targetParentId");
|
|
1904
|
+
if (!nodeId || !targetParentId) {
|
|
1905
|
+
return fail(
|
|
1906
|
+
"move_node: argumen kurang",
|
|
1907
|
+
'Arguments "nodeId" and "targetParentId" are both required.'
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
const resolved = resolveNode(context.document, nodeId, "move_node");
|
|
1911
|
+
if ("error" in resolved) return resolved.error;
|
|
1912
|
+
const resolvedParent = resolveNode(context.document, targetParentId, "move_node");
|
|
1913
|
+
if ("error" in resolvedParent) return resolvedParent.error;
|
|
1914
|
+
const nestingError = checkNesting(context.catalog, resolvedParent.node, resolved.node.type);
|
|
1915
|
+
if (nestingError) return fail("move_node: nesting tidak valid", nestingError);
|
|
1916
|
+
const index = readOptionalIndex(input, "index");
|
|
1917
|
+
const applied = applyCommand(
|
|
1918
|
+
"move_node",
|
|
1919
|
+
() => (0, import_core3.moveNode)(context.document, { nodeId, targetParentId, index })
|
|
1920
|
+
);
|
|
1921
|
+
if ("error" in applied) return applied.error;
|
|
1922
|
+
const violation = guardSecurity("move_node", applied.document, context);
|
|
1923
|
+
if (violation) return violation;
|
|
1924
|
+
const op = { kind: "move-node", nodeId, targetParentId, index };
|
|
1925
|
+
return succeed(
|
|
1926
|
+
`Pindah ${describeNode(resolved.node)} ke #${targetParentId}`,
|
|
1927
|
+
{ nodeId, targetParentId, index: index ?? null },
|
|
1928
|
+
{ op, document: applied.document }
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
var duplicateNodeTool = {
|
|
1933
|
+
kind: "write",
|
|
1934
|
+
definition: {
|
|
1935
|
+
name: "duplicate_node",
|
|
1936
|
+
description: "Duplicate an existing node and its subtree, with fresh ids. Useful for repeating an existing card/feature rather than authoring a new one from scratch.",
|
|
1937
|
+
inputSchema: {
|
|
1938
|
+
type: "object",
|
|
1939
|
+
properties: {
|
|
1940
|
+
nodeId: { type: "string" },
|
|
1941
|
+
targetParentId: {
|
|
1942
|
+
type: "string",
|
|
1943
|
+
description: "Where to put the copy. Defaults to the original node parent."
|
|
1944
|
+
},
|
|
1945
|
+
index: { type: "integer", description: "Position of the copy. Defaults to right after the original." }
|
|
1946
|
+
},
|
|
1947
|
+
required: ["nodeId"]
|
|
1948
|
+
}
|
|
1949
|
+
},
|
|
1950
|
+
execute(input, context) {
|
|
1951
|
+
const nodeId = readString(input, "nodeId");
|
|
1952
|
+
if (!nodeId) return fail("duplicate_node: nodeId kosong", 'Argument "nodeId" is required.');
|
|
1953
|
+
const resolved = resolveNode(context.document, nodeId, "duplicate_node");
|
|
1954
|
+
if ("error" in resolved) return resolved.error;
|
|
1955
|
+
const targetParentId = readString(input, "targetParentId") ?? void 0;
|
|
1956
|
+
if (targetParentId) {
|
|
1957
|
+
const resolvedParent = resolveNode(context.document, targetParentId, "duplicate_node");
|
|
1958
|
+
if ("error" in resolvedParent) return resolvedParent.error;
|
|
1959
|
+
}
|
|
1960
|
+
const index = readOptionalIndex(input, "index");
|
|
1961
|
+
const applied = applyCommand(
|
|
1962
|
+
"duplicate_node",
|
|
1963
|
+
() => (0, import_core3.duplicateNode)(context.document, { nodeId, targetParentId, index })
|
|
1964
|
+
);
|
|
1965
|
+
if ("error" in applied) return applied.error;
|
|
1966
|
+
const violation = guardSecurity("duplicate_node", applied.document, context);
|
|
1967
|
+
if (violation) return violation;
|
|
1968
|
+
const op = { kind: "duplicate-node", nodeId, targetParentId, index };
|
|
1969
|
+
return succeed(
|
|
1970
|
+
`Duplikat ${describeNode(resolved.node)}`,
|
|
1971
|
+
{ nodeId, targetParentId: targetParentId ?? null, index: index ?? null },
|
|
1972
|
+
{ op, document: applied.document }
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
var deleteNode = {
|
|
1977
|
+
kind: "write",
|
|
1978
|
+
destructive: true,
|
|
1979
|
+
definition: {
|
|
1980
|
+
name: "delete_node",
|
|
1981
|
+
description: "Permanently remove a node and everything inside it. Only call this when the user unambiguously asked to remove that specific thing. If the request is vague, ask instead.",
|
|
1982
|
+
inputSchema: {
|
|
1983
|
+
type: "object",
|
|
1984
|
+
properties: {
|
|
1985
|
+
nodeId: { type: "string" },
|
|
1986
|
+
reason: {
|
|
1987
|
+
type: "string",
|
|
1988
|
+
description: "Short justification quoting what the user asked for. Shown to the user before they apply."
|
|
1989
|
+
}
|
|
1990
|
+
},
|
|
1991
|
+
required: ["nodeId", "reason"]
|
|
1992
|
+
}
|
|
1993
|
+
},
|
|
1994
|
+
execute(input, context) {
|
|
1995
|
+
const nodeId = readString(input, "nodeId");
|
|
1996
|
+
if (!nodeId) return fail("delete_node: nodeId kosong", 'Argument "nodeId" is required.');
|
|
1997
|
+
const reason = readString(input, "reason");
|
|
1998
|
+
if (!reason) {
|
|
1999
|
+
return fail(
|
|
2000
|
+
"delete_node: alasan kosong",
|
|
2001
|
+
'Argument "reason" is required for destructive actions \u2014 state what the user asked for.'
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
const resolved = resolveNode(context.document, nodeId, "delete_node");
|
|
2005
|
+
if ("error" in resolved) return resolved.error;
|
|
2006
|
+
const applied = applyCommand("delete_node", () => (0, import_core3.removeNode)(context.document, { nodeId }));
|
|
2007
|
+
if ("error" in applied) return applied.error;
|
|
2008
|
+
const op = { kind: "delete-node", nodeId };
|
|
2009
|
+
return succeed(
|
|
2010
|
+
`Hapus ${describeNode(resolved.node)} \u2014 ${reason}`,
|
|
2011
|
+
{ nodeId, removedType: resolved.node.type },
|
|
2012
|
+
{ op, document: applied.document }
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
var replaceNodeTool = {
|
|
2017
|
+
kind: "write",
|
|
2018
|
+
destructive: true,
|
|
2019
|
+
definition: {
|
|
2020
|
+
name: "replace_node",
|
|
2021
|
+
description: "Replace a node subtree wholesale. LAST RESORT \u2014 only when the children genuinely must be restructured. Never use it to change props or styles; use update_node_props / update_node_styles for that.",
|
|
2022
|
+
inputSchema: {
|
|
2023
|
+
type: "object",
|
|
2024
|
+
properties: {
|
|
2025
|
+
nodeId: { type: "string", description: "Id of the node being replaced. The replacement keeps this id." },
|
|
2026
|
+
node: {
|
|
2027
|
+
type: "object",
|
|
2028
|
+
description: "Replacement node { type, props?, styles?, children? }. Its id is forced to nodeId."
|
|
2029
|
+
},
|
|
2030
|
+
reason: {
|
|
2031
|
+
type: "string",
|
|
2032
|
+
description: "Why a structural replacement is required instead of a props/styles edit."
|
|
2033
|
+
}
|
|
2034
|
+
},
|
|
2035
|
+
required: ["nodeId", "node", "reason"]
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
execute(input, context) {
|
|
2039
|
+
const nodeId = readString(input, "nodeId");
|
|
2040
|
+
const raw = readPlainObject(input, "node");
|
|
2041
|
+
const reason = readString(input, "reason");
|
|
2042
|
+
if (!nodeId || !raw || !reason) {
|
|
2043
|
+
return fail(
|
|
2044
|
+
"replace_node: argumen kurang",
|
|
2045
|
+
'Arguments "nodeId", "node" and "reason" are all required.'
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
const resolved = resolveNode(context.document, nodeId, "replace_node");
|
|
2049
|
+
if ("error" in resolved) return resolved.error;
|
|
2050
|
+
const normalized = normalizeIncomingNode({ ...raw, id: void 0 }, context.document);
|
|
2051
|
+
if ("error" in normalized) {
|
|
2052
|
+
return fail("replace_node: node tidak valid", normalized.error);
|
|
2053
|
+
}
|
|
2054
|
+
const replacement = { ...normalized.node, id: nodeId, type: resolved.node.type };
|
|
2055
|
+
const applied = applyCommand(
|
|
2056
|
+
"replace_node",
|
|
2057
|
+
() => (0, import_core3.replaceNode)(context.document, { nodeId, node: replacement })
|
|
2058
|
+
);
|
|
2059
|
+
if ("error" in applied) return applied.error;
|
|
2060
|
+
const violation = guardSecurity("replace_node", applied.document, context);
|
|
2061
|
+
if (violation) return violation;
|
|
2062
|
+
const op = { kind: "replace-node", nodeId, node: replacement };
|
|
2063
|
+
return succeed(
|
|
2064
|
+
`Ganti struktur ${describeNode(resolved.node)} \u2014 ${reason}`,
|
|
2065
|
+
{ nodeId, childCount: replacement.children?.length ?? 0 },
|
|
2066
|
+
{ op, document: applied.document }
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
var WRITE_TOOLS = [
|
|
2071
|
+
updateNodeProps,
|
|
2072
|
+
updateNodeStyles,
|
|
2073
|
+
insertComponent,
|
|
2074
|
+
insertSection,
|
|
2075
|
+
moveNodeTool,
|
|
2076
|
+
duplicateNodeTool,
|
|
2077
|
+
deleteNode,
|
|
2078
|
+
replaceNodeTool
|
|
2079
|
+
];
|
|
2080
|
+
|
|
2081
|
+
// src/server/tools/index.ts
|
|
2082
|
+
function createDocumentTools(options = {}) {
|
|
2083
|
+
const { allowWrites = true, exclude = [] } = options;
|
|
2084
|
+
const tools = allowWrites ? [...READ_TOOLS, ...WRITE_TOOLS] : [...READ_TOOLS];
|
|
2085
|
+
return tools.filter((tool) => !exclude.includes(tool.definition.name));
|
|
2086
|
+
}
|
|
2087
|
+
function toToolDefinitions(tools) {
|
|
2088
|
+
return tools.map((tool) => tool.definition);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// src/server/agent.ts
|
|
2092
|
+
var DEFAULT_MAX_STEPS = 8;
|
|
2093
|
+
var MAX_TOOL_CALLS_PER_STEP = 8;
|
|
2094
|
+
function toolResultMessage(blocks) {
|
|
2095
|
+
return { role: "user", content: blocks, timestamp: Date.now() };
|
|
2096
|
+
}
|
|
2097
|
+
var KubuildAiAgent = class {
|
|
2098
|
+
options;
|
|
2099
|
+
catalog;
|
|
2100
|
+
tools;
|
|
2101
|
+
constructor(options) {
|
|
2102
|
+
if (!options.adapter) {
|
|
2103
|
+
throw new Error("KubuildAiAgent requires an adapter instance");
|
|
2104
|
+
}
|
|
2105
|
+
if (!options.adapter.supportsTools) {
|
|
2106
|
+
throw new Error(
|
|
2107
|
+
`Adapter "${options.adapter.name}" does not support tool calling, which agent mode requires. Use an adapter with supportsTools === true (e.g. OpenAiAdapter).`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
this.options = options;
|
|
2111
|
+
this.catalog = compileComponentCatalog(options.registry);
|
|
2112
|
+
this.tools = options.tools ?? createDocumentTools();
|
|
2113
|
+
}
|
|
2114
|
+
get adapterName() {
|
|
2115
|
+
return this.options.adapter.name;
|
|
2116
|
+
}
|
|
2117
|
+
refreshCatalog() {
|
|
2118
|
+
this.catalog = compileComponentCatalog(this.options.registry);
|
|
2119
|
+
}
|
|
2120
|
+
log(level, message, meta) {
|
|
2121
|
+
if (this.options.logger) {
|
|
2122
|
+
this.options.logger(level, message, meta);
|
|
2123
|
+
} else if (this.options.debug) {
|
|
2124
|
+
console.log(`[KUBUILD-AGENT ${level.toUpperCase()}] ${message}`, meta !== void 0 ? meta : "");
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
findTool(name) {
|
|
2128
|
+
return this.tools.find((tool) => tool.definition.name === name);
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Runs one tool call against the current snapshot.
|
|
2132
|
+
*
|
|
2133
|
+
* An unknown tool name is answered with a normal (failed) tool result rather than an
|
|
2134
|
+
* exception, for the same reason bad arguments are: it gives the model a chance to pick a
|
|
2135
|
+
* real tool on the next step instead of killing the run.
|
|
2136
|
+
*/
|
|
2137
|
+
async executeToolCall(call, snapshot, signal) {
|
|
2138
|
+
const tool = this.findTool(call.name);
|
|
2139
|
+
if (!tool) {
|
|
2140
|
+
const available = this.tools.map((t) => t.definition.name).join(", ");
|
|
2141
|
+
return {
|
|
2142
|
+
ok: false,
|
|
2143
|
+
summary: `Tool "${call.name}" tidak dikenal`,
|
|
2144
|
+
snapshot,
|
|
2145
|
+
block: {
|
|
2146
|
+
type: "tool_result",
|
|
2147
|
+
toolUseId: call.id,
|
|
2148
|
+
isError: true,
|
|
2149
|
+
content: JSON.stringify({
|
|
2150
|
+
ok: false,
|
|
2151
|
+
error: `Unknown tool "${call.name}". Available tools: ${available}.`
|
|
2152
|
+
})
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
let result;
|
|
2157
|
+
try {
|
|
2158
|
+
result = await tool.execute(call.input ?? {}, {
|
|
2159
|
+
document: snapshot,
|
|
2160
|
+
catalog: this.catalog,
|
|
2161
|
+
securityLimits: this.options.securityLimits,
|
|
2162
|
+
signal,
|
|
2163
|
+
generateSection: this.options.engine ? async ({ prompt, parentContext }) => {
|
|
2164
|
+
const response = await this.options.engine.generateSection(
|
|
2165
|
+
{ prompt, parentContext },
|
|
2166
|
+
{ signal }
|
|
2167
|
+
);
|
|
2168
|
+
if (!response.success || !response.data) {
|
|
2169
|
+
throw new Error(response.error?.message ?? "Section generation failed");
|
|
2170
|
+
}
|
|
2171
|
+
return response.data;
|
|
2172
|
+
} : void 0
|
|
2173
|
+
});
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2176
|
+
this.log("error", `Tool "${call.name}" threw`, message);
|
|
2177
|
+
return {
|
|
2178
|
+
ok: false,
|
|
2179
|
+
summary: `Tool "${call.name}" error`,
|
|
2180
|
+
snapshot,
|
|
2181
|
+
block: {
|
|
2182
|
+
type: "tool_result",
|
|
2183
|
+
toolUseId: call.id,
|
|
2184
|
+
isError: true,
|
|
2185
|
+
content: JSON.stringify({ ok: false, error: message })
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
const op = result.ok && result.op ? {
|
|
2190
|
+
id: call.id,
|
|
2191
|
+
op: result.op,
|
|
2192
|
+
summary: result.summary,
|
|
2193
|
+
destructive: tool.destructive === true
|
|
2194
|
+
} : void 0;
|
|
2195
|
+
return {
|
|
2196
|
+
ok: result.ok,
|
|
2197
|
+
summary: result.summary,
|
|
2198
|
+
snapshot: result.document ?? snapshot,
|
|
2199
|
+
op,
|
|
2200
|
+
block: {
|
|
2201
|
+
type: "tool_result",
|
|
2202
|
+
toolUseId: call.id,
|
|
2203
|
+
isError: !result.ok,
|
|
2204
|
+
content: JSON.stringify(result.content)
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Executes one agent turn, streaming progress as `AiStreamEvent`s and ending with a
|
|
2210
|
+
* terminal `agent-complete` (or `error`) event.
|
|
2211
|
+
*
|
|
2212
|
+
* The loop stops as soon as the model returns a turn with no tool calls — that final
|
|
2213
|
+
* message is the summary shown to the user. It also stops at `maxSteps` (cost guard) or
|
|
2214
|
+
* on abort, and reports which of those happened via `stoppedBy` so the UI can say
|
|
2215
|
+
* "stopped after N steps" rather than pretending the work is finished.
|
|
2216
|
+
*/
|
|
2217
|
+
async *run(request, context) {
|
|
2218
|
+
const maxSteps = request.maxSteps ?? this.options.maxAgentSteps ?? DEFAULT_MAX_STEPS;
|
|
2219
|
+
const signal = context?.signal;
|
|
2220
|
+
if (!request.messages || request.messages.length === 0) {
|
|
2221
|
+
yield {
|
|
2222
|
+
type: "error",
|
|
2223
|
+
error: { code: "INVALID_AGENT_REQUEST", message: '"messages" must not be empty' }
|
|
2224
|
+
};
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
if (!request.document?.document) {
|
|
2228
|
+
yield {
|
|
2229
|
+
type: "error",
|
|
2230
|
+
error: { code: "INVALID_AGENT_REQUEST", message: '"document" is required for agent mode' }
|
|
2231
|
+
};
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
const systemPrompt = buildAgentSystemPrompt({
|
|
2235
|
+
catalog: this.catalog,
|
|
2236
|
+
document: request.document,
|
|
2237
|
+
selectedNodeId: request.selectedNodeId,
|
|
2238
|
+
prefix: this.options.systemPromptPrefix,
|
|
2239
|
+
stylePreference: request.stylePreference,
|
|
2240
|
+
additionalContext: request.systemPrompt
|
|
2241
|
+
});
|
|
2242
|
+
const toolDefinitions = toToolDefinitions(this.tools);
|
|
2243
|
+
const messages = [...request.messages];
|
|
2244
|
+
const ops = [];
|
|
2245
|
+
let snapshot = request.document;
|
|
2246
|
+
let promptTokens = 0;
|
|
2247
|
+
let completionTokens = 0;
|
|
2248
|
+
let summary = "";
|
|
2249
|
+
let stoppedBy = "complete";
|
|
2250
|
+
let step = 0;
|
|
2251
|
+
this.log("info", `[AGENT] run started (maxSteps=${maxSteps}, tools=${toolDefinitions.length})`);
|
|
2252
|
+
try {
|
|
2253
|
+
while (step < maxSteps) {
|
|
2254
|
+
if (signal?.aborted) {
|
|
2255
|
+
stoppedBy = "aborted";
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
step++;
|
|
2259
|
+
yield { type: "agent-step", step, maxSteps };
|
|
2260
|
+
const result = await this.options.adapter.generate({
|
|
2261
|
+
systemPrompt,
|
|
2262
|
+
userPrompt: getMessageText(request.messages[request.messages.length - 1]),
|
|
2263
|
+
messages,
|
|
2264
|
+
tools: toolDefinitions,
|
|
2265
|
+
toolChoice: "auto",
|
|
2266
|
+
signal
|
|
2267
|
+
});
|
|
2268
|
+
promptTokens += result.usage?.promptTokens ?? 0;
|
|
2269
|
+
completionTokens += result.usage?.completionTokens ?? 0;
|
|
2270
|
+
const toolCalls = result.toolCalls ?? [];
|
|
2271
|
+
if (toolCalls.length === 0) {
|
|
2272
|
+
summary = result.text.trim();
|
|
2273
|
+
stoppedBy = "complete";
|
|
2274
|
+
this.log("info", `[AGENT] finished after ${step} step(s) with ${ops.length} op(s)`);
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
const assistantBlocks = [];
|
|
2278
|
+
if (result.text.trim()) {
|
|
2279
|
+
assistantBlocks.push({ type: "text", text: result.text });
|
|
2280
|
+
}
|
|
2281
|
+
assistantBlocks.push(...toolCalls);
|
|
2282
|
+
messages.push({ role: "assistant", content: assistantBlocks, timestamp: Date.now() });
|
|
2283
|
+
const capped = toolCalls.slice(0, MAX_TOOL_CALLS_PER_STEP);
|
|
2284
|
+
const resultBlocks = [];
|
|
2285
|
+
for (const call of capped) {
|
|
2286
|
+
if (signal?.aborted) {
|
|
2287
|
+
stoppedBy = "aborted";
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
yield { type: "tool-call", id: call.id, name: call.name, input: call.input ?? {} };
|
|
2291
|
+
const executed = await this.executeToolCall(call, snapshot, signal);
|
|
2292
|
+
snapshot = executed.snapshot;
|
|
2293
|
+
resultBlocks.push(executed.block);
|
|
2294
|
+
if (executed.op) ops.push(executed.op);
|
|
2295
|
+
yield {
|
|
2296
|
+
type: "tool-result",
|
|
2297
|
+
id: call.id,
|
|
2298
|
+
name: call.name,
|
|
2299
|
+
ok: executed.ok,
|
|
2300
|
+
summary: executed.summary
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
for (const call of toolCalls.slice(capped.length)) {
|
|
2304
|
+
resultBlocks.push({
|
|
2305
|
+
type: "tool_result",
|
|
2306
|
+
toolUseId: call.id,
|
|
2307
|
+
isError: true,
|
|
2308
|
+
content: JSON.stringify({
|
|
2309
|
+
ok: false,
|
|
2310
|
+
error: `Too many tool calls in one step (limit ${MAX_TOOL_CALLS_PER_STEP}). This call was not executed \u2014 request it again in the next step.`
|
|
2311
|
+
})
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
messages.push(toolResultMessage(resultBlocks));
|
|
2315
|
+
if (stoppedBy === "aborted") break;
|
|
2316
|
+
}
|
|
2317
|
+
if (stoppedBy === "complete" && step >= maxSteps && !summary) {
|
|
2318
|
+
stoppedBy = "max-steps";
|
|
2319
|
+
summary = `Berhenti setelah ${maxSteps} langkah. ${ops.length} perubahan sudah disiapkan \u2014 periksa hasilnya lalu minta lanjutan bila perlu.`;
|
|
2320
|
+
this.log("warn", `[AGENT] hit maxSteps (${maxSteps}) with ${ops.length} op(s)`);
|
|
2321
|
+
}
|
|
2322
|
+
if (stoppedBy === "aborted" && !summary) {
|
|
2323
|
+
summary = `Dihentikan. ${ops.length} perubahan sempat disiapkan.`;
|
|
2324
|
+
}
|
|
2325
|
+
yield {
|
|
2326
|
+
type: "agent-complete",
|
|
2327
|
+
result: {
|
|
2328
|
+
ops,
|
|
2329
|
+
summary,
|
|
2330
|
+
stepsUsed: step,
|
|
2331
|
+
stoppedBy,
|
|
2332
|
+
usage: { promptTokens, completionTokens }
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2337
|
+
this.log("error", `[AGENT] run failed: ${message}`);
|
|
2338
|
+
yield {
|
|
2339
|
+
type: "agent-complete",
|
|
2340
|
+
result: {
|
|
2341
|
+
ops,
|
|
2342
|
+
summary: summary || `Terjadi error: ${message}`,
|
|
2343
|
+
stepsUsed: step,
|
|
2344
|
+
stoppedBy: "error",
|
|
2345
|
+
usage: { promptTokens, completionTokens }
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
yield { type: "error", error: { code: "AGENT_ERROR", message } };
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
/** Non-streaming convenience wrapper — drains `run()` and returns its terminal result. */
|
|
2352
|
+
async execute(request, context) {
|
|
2353
|
+
let final = null;
|
|
2354
|
+
let error = null;
|
|
2355
|
+
for await (const event of this.run(request, context)) {
|
|
2356
|
+
if (event.type === "agent-complete") final = event.result;
|
|
2357
|
+
if (event.type === "error") error = event.error;
|
|
2358
|
+
}
|
|
2359
|
+
if (final) return final;
|
|
2360
|
+
return {
|
|
2361
|
+
ops: [],
|
|
2362
|
+
summary: error?.message ?? "Agent produced no result",
|
|
2363
|
+
stepsUsed: 0,
|
|
2364
|
+
stoppedBy: "error"
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
// src/server/handler.ts
|
|
2370
|
+
async function processAiRequest(engine, body, signal, agent) {
|
|
2371
|
+
if (!body || typeof body !== "object") {
|
|
2372
|
+
return {
|
|
2373
|
+
status: 400,
|
|
2374
|
+
response: {
|
|
2375
|
+
success: false,
|
|
2376
|
+
error: {
|
|
2377
|
+
code: "BAD_REQUEST",
|
|
2378
|
+
message: "Request body must be a valid JSON object"
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
const payload = body;
|
|
2384
|
+
const mode = payload.mode || "full-page";
|
|
2385
|
+
if (mode === "full-page") {
|
|
2386
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
2387
|
+
return {
|
|
2388
|
+
status: 400,
|
|
2389
|
+
response: {
|
|
2390
|
+
success: false,
|
|
2391
|
+
error: {
|
|
2392
|
+
code: "INVALID_PROMPT",
|
|
2393
|
+
message: '"prompt" is required for full-page generation'
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
const result = await engine.generatePage(
|
|
2399
|
+
{
|
|
2400
|
+
prompt: payload.prompt,
|
|
2401
|
+
stylePreference: payload.stylePreference,
|
|
2402
|
+
tone: payload.tone,
|
|
2403
|
+
locale: payload.locale,
|
|
2404
|
+
metadata: payload.metadata,
|
|
2405
|
+
conversationHistory: payload.conversationHistory ?? payload.messages,
|
|
2406
|
+
sectionCount: payload.sectionCount,
|
|
2407
|
+
plan: payload.plan
|
|
2408
|
+
},
|
|
2409
|
+
{ signal }
|
|
2410
|
+
);
|
|
2411
|
+
return {
|
|
2412
|
+
status: result.success ? 200 : 500,
|
|
2413
|
+
response: result
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
if (mode === "section") {
|
|
2417
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
2418
|
+
return {
|
|
2419
|
+
status: 400,
|
|
2420
|
+
response: {
|
|
2421
|
+
success: false,
|
|
2422
|
+
error: {
|
|
2423
|
+
code: "INVALID_PROMPT",
|
|
2424
|
+
message: '"prompt" is required for section generation'
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
const result = await engine.generateSection(
|
|
2430
|
+
{
|
|
2431
|
+
prompt: payload.prompt,
|
|
2432
|
+
stylePreference: payload.stylePreference,
|
|
2433
|
+
targetSectionType: payload.targetSectionType,
|
|
2434
|
+
parentContext: payload.parentContext
|
|
2435
|
+
},
|
|
2436
|
+
{ signal }
|
|
2437
|
+
);
|
|
2438
|
+
return {
|
|
2439
|
+
status: result.success ? 200 : 500,
|
|
2440
|
+
response: result
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
if (mode === "refactor") {
|
|
2444
|
+
if (!payload.node || !payload.instruction) {
|
|
2445
|
+
return {
|
|
2446
|
+
status: 400,
|
|
2447
|
+
response: {
|
|
2448
|
+
success: false,
|
|
2449
|
+
error: {
|
|
2450
|
+
code: "INVALID_REFACTOR_PARAMS",
|
|
2451
|
+
message: '"node" and "instruction" are required for refactoring'
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
const result = await engine.refactorNode(
|
|
2457
|
+
{
|
|
2458
|
+
node: payload.node,
|
|
2459
|
+
instruction: payload.instruction,
|
|
2460
|
+
stylePreference: payload.stylePreference
|
|
2461
|
+
},
|
|
2462
|
+
{ signal }
|
|
2463
|
+
);
|
|
2464
|
+
return {
|
|
2465
|
+
status: result.success ? 200 : 500,
|
|
2466
|
+
response: result
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
if (mode === "chat") {
|
|
2470
|
+
if (!payload.messages || !Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
1106
2471
|
return {
|
|
1107
2472
|
status: 400,
|
|
1108
2473
|
response: {
|
|
@@ -1127,17 +2492,142 @@ async function processAiRequest(engine, body, signal) {
|
|
|
1127
2492
|
response: result
|
|
1128
2493
|
};
|
|
1129
2494
|
}
|
|
2495
|
+
if (mode === "agent") {
|
|
2496
|
+
const validationError = validateAgentPayload(payload, agent);
|
|
2497
|
+
if (validationError) {
|
|
2498
|
+
return { status: validationError.status, response: validationError.response };
|
|
2499
|
+
}
|
|
2500
|
+
const result = await agent.execute(
|
|
2501
|
+
{
|
|
2502
|
+
messages: payload.messages,
|
|
2503
|
+
document: payload.document,
|
|
2504
|
+
selectedNodeId: payload.selectedNodeId,
|
|
2505
|
+
stylePreference: payload.stylePreference,
|
|
2506
|
+
maxSteps: payload.maxSteps
|
|
2507
|
+
},
|
|
2508
|
+
{ signal }
|
|
2509
|
+
);
|
|
2510
|
+
return {
|
|
2511
|
+
status: result.stoppedBy === "error" ? 500 : 200,
|
|
2512
|
+
response: { success: result.stoppedBy !== "error", data: result }
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
if (mode === "plan") {
|
|
2516
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
2517
|
+
return {
|
|
2518
|
+
status: 400,
|
|
2519
|
+
response: {
|
|
2520
|
+
success: false,
|
|
2521
|
+
error: {
|
|
2522
|
+
code: "INVALID_PROMPT",
|
|
2523
|
+
message: '"prompt" is required for planning mode'
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
const result = await engine.planPage(
|
|
2529
|
+
{
|
|
2530
|
+
prompt: payload.prompt,
|
|
2531
|
+
stylePreference: payload.stylePreference,
|
|
2532
|
+
tone: payload.tone,
|
|
2533
|
+
locale: payload.locale,
|
|
2534
|
+
sectionCount: payload.sectionCount,
|
|
2535
|
+
conversationHistory: payload.conversationHistory ?? payload.messages
|
|
2536
|
+
},
|
|
2537
|
+
{ signal }
|
|
2538
|
+
);
|
|
2539
|
+
return {
|
|
2540
|
+
status: result.success ? 200 : 500,
|
|
2541
|
+
response: result
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
1130
2544
|
return {
|
|
1131
2545
|
status: 400,
|
|
1132
2546
|
response: {
|
|
1133
2547
|
success: false,
|
|
1134
2548
|
error: {
|
|
1135
2549
|
code: "UNKNOWN_MODE",
|
|
1136
|
-
message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat'`
|
|
2550
|
+
message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat', 'agent', 'plan'`
|
|
1137
2551
|
}
|
|
1138
2552
|
}
|
|
1139
2553
|
};
|
|
1140
2554
|
}
|
|
2555
|
+
function validateAgentPayload(payload, agent) {
|
|
2556
|
+
if (!agent) {
|
|
2557
|
+
return {
|
|
2558
|
+
status: 501,
|
|
2559
|
+
response: {
|
|
2560
|
+
success: false,
|
|
2561
|
+
error: {
|
|
2562
|
+
code: "AGENT_NOT_CONFIGURED",
|
|
2563
|
+
message: "Agent mode is not enabled on this endpoint. Pass a KubuildAiAgent to createAiHandler to enable it."
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
if (!payload.messages || !Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
2569
|
+
return {
|
|
2570
|
+
status: 400,
|
|
2571
|
+
response: {
|
|
2572
|
+
success: false,
|
|
2573
|
+
error: {
|
|
2574
|
+
code: "INVALID_AGENT_PARAMS",
|
|
2575
|
+
message: '"messages" array is required and must not be empty for agent mode'
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
if (!payload.document || !payload.document.document) {
|
|
2581
|
+
return {
|
|
2582
|
+
status: 400,
|
|
2583
|
+
response: {
|
|
2584
|
+
success: false,
|
|
2585
|
+
error: {
|
|
2586
|
+
code: "INVALID_AGENT_PARAMS",
|
|
2587
|
+
message: '"document" (the current PageDocument) is required for agent mode'
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
function sseResponse(events, errorCode) {
|
|
2595
|
+
const encoder = new TextEncoder();
|
|
2596
|
+
const readable = new ReadableStream({
|
|
2597
|
+
async start(controller) {
|
|
2598
|
+
try {
|
|
2599
|
+
for await (const event of events) {
|
|
2600
|
+
controller.enqueue(
|
|
2601
|
+
encoder.encode(`event: ${event.type}
|
|
2602
|
+
data: ${JSON.stringify(event)}
|
|
2603
|
+
|
|
2604
|
+
`)
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
} catch (err) {
|
|
2608
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2609
|
+
controller.enqueue(
|
|
2610
|
+
encoder.encode(
|
|
2611
|
+
`event: error
|
|
2612
|
+
data: ${JSON.stringify({ type: "error", error: { code: errorCode, message } })}
|
|
2613
|
+
|
|
2614
|
+
`
|
|
2615
|
+
)
|
|
2616
|
+
);
|
|
2617
|
+
} finally {
|
|
2618
|
+
controller.close();
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
return new Response(readable, {
|
|
2623
|
+
status: 200,
|
|
2624
|
+
headers: {
|
|
2625
|
+
"Content-Type": "text/event-stream",
|
|
2626
|
+
"Cache-Control": "no-cache, no-transform",
|
|
2627
|
+
Connection: "keep-alive"
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
1141
2631
|
function createAiHandler(engine, options) {
|
|
1142
2632
|
return async (request) => {
|
|
1143
2633
|
try {
|
|
@@ -1168,6 +2658,28 @@ function createAiHandler(engine, options) {
|
|
|
1168
2658
|
const body = await request.json().catch(() => null);
|
|
1169
2659
|
if (body && typeof body === "object" && body.stream === true) {
|
|
1170
2660
|
const mode = body.mode || "full-page";
|
|
2661
|
+
if (mode === "agent") {
|
|
2662
|
+
const validationError = validateAgentPayload(body, options?.agent);
|
|
2663
|
+
if (validationError) {
|
|
2664
|
+
return new Response(JSON.stringify(validationError.response), {
|
|
2665
|
+
status: validationError.status,
|
|
2666
|
+
headers: { "Content-Type": "application/json" }
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
return sseResponse(
|
|
2670
|
+
options.agent.run(
|
|
2671
|
+
{
|
|
2672
|
+
messages: body.messages,
|
|
2673
|
+
document: body.document,
|
|
2674
|
+
selectedNodeId: body.selectedNodeId,
|
|
2675
|
+
stylePreference: body.stylePreference,
|
|
2676
|
+
maxSteps: body.maxSteps
|
|
2677
|
+
},
|
|
2678
|
+
{ signal: request.signal }
|
|
2679
|
+
),
|
|
2680
|
+
"AGENT_STREAM_ERROR"
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
1171
2683
|
if (mode === "chat") {
|
|
1172
2684
|
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
|
1173
2685
|
return new Response(
|
|
@@ -1184,48 +2696,17 @@ function createAiHandler(engine, options) {
|
|
|
1184
2696
|
}
|
|
1185
2697
|
);
|
|
1186
2698
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
)) {
|
|
1199
|
-
controller.enqueue(
|
|
1200
|
-
encoder2.encode(`event: ${event.type}
|
|
1201
|
-
data: ${JSON.stringify(event)}
|
|
1202
|
-
|
|
1203
|
-
`)
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
} catch (err) {
|
|
1207
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1208
|
-
controller.enqueue(
|
|
1209
|
-
encoder2.encode(
|
|
1210
|
-
`event: error
|
|
1211
|
-
data: ${JSON.stringify({ type: "error", error: { code: "CHAT_STREAM_ERROR", message } })}
|
|
1212
|
-
|
|
1213
|
-
`
|
|
1214
|
-
)
|
|
1215
|
-
);
|
|
1216
|
-
} finally {
|
|
1217
|
-
controller.close();
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
return new Response(readable2, {
|
|
1222
|
-
status: 200,
|
|
1223
|
-
headers: {
|
|
1224
|
-
"Content-Type": "text/event-stream",
|
|
1225
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1226
|
-
Connection: "keep-alive"
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
2699
|
+
return sseResponse(
|
|
2700
|
+
engine.chatStream(
|
|
2701
|
+
{
|
|
2702
|
+
messages: body.messages,
|
|
2703
|
+
currentDocument: body.currentDocument,
|
|
2704
|
+
selectedNodeId: body.selectedNodeId
|
|
2705
|
+
},
|
|
2706
|
+
{ signal: request.signal }
|
|
2707
|
+
),
|
|
2708
|
+
"CHAT_STREAM_ERROR"
|
|
2709
|
+
);
|
|
1229
2710
|
}
|
|
1230
2711
|
if (!body.prompt || typeof body.prompt !== "string") {
|
|
1231
2712
|
return new Response(
|
|
@@ -1242,55 +2723,28 @@ data: ${JSON.stringify({ type: "error", error: { code: "CHAT_STREAM_ERROR", mess
|
|
|
1242
2723
|
}
|
|
1243
2724
|
);
|
|
1244
2725
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
data: ${JSON.stringify(event)}
|
|
1262
|
-
|
|
1263
|
-
`)
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
} catch (err) {
|
|
1267
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1268
|
-
controller.enqueue(
|
|
1269
|
-
encoder.encode(
|
|
1270
|
-
`event: error
|
|
1271
|
-
data: ${JSON.stringify({ type: "error", error: { code: "STREAM_ERROR", message } })}
|
|
1272
|
-
|
|
1273
|
-
`
|
|
1274
|
-
)
|
|
1275
|
-
);
|
|
1276
|
-
} finally {
|
|
1277
|
-
controller.close();
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
return new Response(readable, {
|
|
1282
|
-
status: 200,
|
|
1283
|
-
headers: {
|
|
1284
|
-
"Content-Type": "text/event-stream",
|
|
1285
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1286
|
-
Connection: "keep-alive"
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
2726
|
+
return sseResponse(
|
|
2727
|
+
engine.streamPage(
|
|
2728
|
+
{
|
|
2729
|
+
prompt: body.prompt,
|
|
2730
|
+
stylePreference: body.stylePreference,
|
|
2731
|
+
tone: body.tone,
|
|
2732
|
+
locale: body.locale,
|
|
2733
|
+
metadata: body.metadata,
|
|
2734
|
+
conversationHistory: body.conversationHistory ?? body.messages,
|
|
2735
|
+
sectionCount: body.sectionCount,
|
|
2736
|
+
plan: body.plan
|
|
2737
|
+
},
|
|
2738
|
+
{ signal: request.signal }
|
|
2739
|
+
),
|
|
2740
|
+
"STREAM_ERROR"
|
|
2741
|
+
);
|
|
1289
2742
|
}
|
|
1290
2743
|
const { status, response } = await processAiRequest(
|
|
1291
2744
|
engine,
|
|
1292
2745
|
body,
|
|
1293
|
-
request.signal
|
|
2746
|
+
request.signal,
|
|
2747
|
+
options?.agent
|
|
1294
2748
|
);
|
|
1295
2749
|
return new Response(JSON.stringify(response), {
|
|
1296
2750
|
status,
|
|
@@ -1368,7 +2822,7 @@ var GeminiAdapter = class {
|
|
|
1368
2822
|
const { userPrompt, messages: chatMessages } = params;
|
|
1369
2823
|
return chatMessages && chatMessages.length > 0 ? chatMessages.map((m) => ({
|
|
1370
2824
|
role: m.role === "assistant" ? "model" : "user",
|
|
1371
|
-
parts: [{ text: m
|
|
2825
|
+
parts: [{ text: getMessageText(m) }]
|
|
1372
2826
|
})) : [
|
|
1373
2827
|
{
|
|
1374
2828
|
role: "user",
|
|
@@ -1480,8 +2934,47 @@ var GeminiAdapter = class {
|
|
|
1480
2934
|
};
|
|
1481
2935
|
|
|
1482
2936
|
// src/server/adapters/openai.ts
|
|
2937
|
+
function mapStopReason(finishReason) {
|
|
2938
|
+
switch (finishReason) {
|
|
2939
|
+
case "stop":
|
|
2940
|
+
return "stop";
|
|
2941
|
+
case "tool_calls":
|
|
2942
|
+
case "function_call":
|
|
2943
|
+
return "tool_calls";
|
|
2944
|
+
case "length":
|
|
2945
|
+
return "length";
|
|
2946
|
+
case "content_filter":
|
|
2947
|
+
return "content_filter";
|
|
2948
|
+
default:
|
|
2949
|
+
return "unknown";
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
function parseToolCalls(raw) {
|
|
2953
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
2954
|
+
return raw.filter((call) => call?.function?.name).map((call, index) => {
|
|
2955
|
+
let input = {};
|
|
2956
|
+
const argsText = call.function?.arguments;
|
|
2957
|
+
if (argsText && argsText.trim()) {
|
|
2958
|
+
try {
|
|
2959
|
+
const parsed = JSON.parse(argsText);
|
|
2960
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2961
|
+
input = parsed;
|
|
2962
|
+
}
|
|
2963
|
+
} catch {
|
|
2964
|
+
input = {};
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
return {
|
|
2968
|
+
type: "tool_use",
|
|
2969
|
+
id: call.id || `call_${index}`,
|
|
2970
|
+
name: call.function.name,
|
|
2971
|
+
input
|
|
2972
|
+
};
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
1483
2975
|
var OpenAiAdapter = class {
|
|
1484
2976
|
name = "openai";
|
|
2977
|
+
supportsTools = true;
|
|
1485
2978
|
apiKey;
|
|
1486
2979
|
model;
|
|
1487
2980
|
temperature;
|
|
@@ -1497,30 +2990,95 @@ var OpenAiAdapter = class {
|
|
|
1497
2990
|
this.baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1498
2991
|
this.customHeaders = options.headers;
|
|
1499
2992
|
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Expands one `AiChatMessage` into the one-or-more OpenAI messages it corresponds to
|
|
2995
|
+
* (STORA-530).
|
|
2996
|
+
*
|
|
2997
|
+
* The block model is Anthropic-shaped — tool results ride along inside a `user` message —
|
|
2998
|
+
* while OpenAI wants each result as its own `role: 'tool'` message keyed by
|
|
2999
|
+
* `tool_call_id`. That fan-out happens here so the rest of the stack stays
|
|
3000
|
+
* provider-agnostic.
|
|
3001
|
+
*/
|
|
3002
|
+
mapMessage(message) {
|
|
3003
|
+
const toolUses = getToolUseBlocks(message);
|
|
3004
|
+
const toolResults = getToolResultBlocks(message);
|
|
3005
|
+
const text = getMessageText(message);
|
|
3006
|
+
if (toolResults.length > 0) {
|
|
3007
|
+
const results = toolResults.map((block) => ({
|
|
3008
|
+
role: "tool",
|
|
3009
|
+
content: block.content,
|
|
3010
|
+
tool_call_id: block.toolUseId
|
|
3011
|
+
}));
|
|
3012
|
+
if (text.trim()) {
|
|
3013
|
+
results.push({ role: "user", content: text });
|
|
3014
|
+
}
|
|
3015
|
+
return results;
|
|
3016
|
+
}
|
|
3017
|
+
if (toolUses.length > 0) {
|
|
3018
|
+
return [
|
|
3019
|
+
{
|
|
3020
|
+
role: "assistant",
|
|
3021
|
+
content: text.trim() ? text : null,
|
|
3022
|
+
tool_calls: toolUses.map((block) => ({
|
|
3023
|
+
id: block.id,
|
|
3024
|
+
type: "function",
|
|
3025
|
+
function: {
|
|
3026
|
+
name: block.name,
|
|
3027
|
+
arguments: JSON.stringify(block.input ?? {})
|
|
3028
|
+
}
|
|
3029
|
+
}))
|
|
3030
|
+
}
|
|
3031
|
+
];
|
|
3032
|
+
}
|
|
3033
|
+
return [{ role: message.role, content: text }];
|
|
3034
|
+
}
|
|
1500
3035
|
buildMessages(params) {
|
|
1501
3036
|
const { systemPrompt, userPrompt, messages: chatMessages } = params;
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
3037
|
+
if (chatMessages && chatMessages.length > 0) {
|
|
3038
|
+
return [
|
|
3039
|
+
{ role: "system", content: systemPrompt },
|
|
3040
|
+
...chatMessages.flatMap((m) => this.mapMessage(m))
|
|
3041
|
+
];
|
|
3042
|
+
}
|
|
3043
|
+
return [
|
|
1506
3044
|
{ role: "system", content: systemPrompt },
|
|
1507
3045
|
{ role: "user", content: userPrompt }
|
|
1508
3046
|
];
|
|
1509
3047
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
3048
|
+
/**
|
|
3049
|
+
* Shared request body builder for `generate`/`generateStream` — notably the
|
|
3050
|
+
* `response_format` decision, which must never fire in tool mode: OpenAI rejects
|
|
3051
|
+
* `json_object` combined with `tools`, and a tool-calling turn's text output is a
|
|
3052
|
+
* natural-language summary, not JSON.
|
|
3053
|
+
*/
|
|
3054
|
+
buildBody(params) {
|
|
3055
|
+
const { systemPrompt, jsonSchema, messages: chatMessages, tools, toolChoice } = params;
|
|
1515
3056
|
const body = {
|
|
1516
3057
|
model: this.model,
|
|
1517
|
-
temperature: this.temperature,
|
|
1518
|
-
messages
|
|
3058
|
+
temperature: params.temperature ?? this.temperature,
|
|
3059
|
+
messages: this.buildMessages(params)
|
|
1519
3060
|
};
|
|
1520
|
-
if (
|
|
3061
|
+
if (tools && tools.length > 0) {
|
|
3062
|
+
body.tools = tools.map((tool) => ({
|
|
3063
|
+
type: "function",
|
|
3064
|
+
function: {
|
|
3065
|
+
name: tool.name,
|
|
3066
|
+
description: tool.description,
|
|
3067
|
+
parameters: tool.inputSchema
|
|
3068
|
+
}
|
|
3069
|
+
}));
|
|
3070
|
+
if (toolChoice) {
|
|
3071
|
+
body.tool_choice = typeof toolChoice === "string" ? toolChoice : { type: "function", function: { name: toolChoice.name } };
|
|
3072
|
+
}
|
|
3073
|
+
return body;
|
|
3074
|
+
}
|
|
3075
|
+
if (jsonSchema || !chatMessages && (systemPrompt.includes("JSON") || systemPrompt.includes("json"))) {
|
|
1521
3076
|
body.response_format = { type: "json_object" };
|
|
1522
3077
|
}
|
|
1523
|
-
|
|
3078
|
+
return body;
|
|
3079
|
+
}
|
|
3080
|
+
async post(body, signal) {
|
|
3081
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
1524
3082
|
method: "POST",
|
|
1525
3083
|
headers: {
|
|
1526
3084
|
"Content-Type": "application/json",
|
|
@@ -1536,13 +3094,21 @@ var OpenAiAdapter = class {
|
|
|
1536
3094
|
`OpenAI API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1537
3095
|
);
|
|
1538
3096
|
}
|
|
3097
|
+
return res;
|
|
3098
|
+
}
|
|
3099
|
+
async generate(params) {
|
|
3100
|
+
const res = await this.post(this.buildBody(params), params.signal);
|
|
1539
3101
|
const data = await res.json();
|
|
1540
|
-
const
|
|
1541
|
-
|
|
3102
|
+
const choice = data.choices?.[0];
|
|
3103
|
+
const content = choice?.message?.content;
|
|
3104
|
+
const toolCalls = parseToolCalls(choice?.message?.tool_calls);
|
|
3105
|
+
if (!content && toolCalls.length === 0) {
|
|
1542
3106
|
throw new Error("OpenAI returned an empty message content");
|
|
1543
3107
|
}
|
|
1544
3108
|
return {
|
|
1545
|
-
text: content,
|
|
3109
|
+
text: content || "",
|
|
3110
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
3111
|
+
stopReason: mapStopReason(choice?.finish_reason),
|
|
1546
3112
|
usage: data.usage ? {
|
|
1547
3113
|
promptTokens: data.usage.prompt_tokens,
|
|
1548
3114
|
completionTokens: data.usage.completion_tokens
|
|
@@ -1553,38 +3119,23 @@ var OpenAiAdapter = class {
|
|
|
1553
3119
|
* Token-level streaming via OpenAI's `stream: true` chat completions mode (STORA-515).
|
|
1554
3120
|
* Yields each `delta.content` fragment as it arrives and returns the fully assembled
|
|
1555
3121
|
* text + usage (when the final chunk includes it) once the stream ends.
|
|
3122
|
+
*
|
|
3123
|
+
* Tool calls arrive fragmented too (STORA-530): `delta.tool_calls[].function.arguments`
|
|
3124
|
+
* is delivered as a character stream across many chunks, keyed by array `index`. They're
|
|
3125
|
+
* accumulated per index and parsed only once the stream ends — parsing a partial
|
|
3126
|
+
* arguments string would always fail. Tool call deltas are never `yield`ed as text,
|
|
3127
|
+
* since they aren't part of the assistant's visible message.
|
|
1556
3128
|
*/
|
|
1557
3129
|
async *generateStream(params) {
|
|
1558
3130
|
const { signal } = params;
|
|
1559
|
-
const
|
|
1560
|
-
const messages = this.buildMessages(params);
|
|
1561
|
-
const body = {
|
|
1562
|
-
model: this.model,
|
|
1563
|
-
temperature: this.temperature,
|
|
1564
|
-
messages,
|
|
1565
|
-
stream: true
|
|
1566
|
-
};
|
|
1567
|
-
const res = await fetch(url, {
|
|
1568
|
-
method: "POST",
|
|
1569
|
-
headers: {
|
|
1570
|
-
"Content-Type": "application/json",
|
|
1571
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
1572
|
-
...this.customHeaders
|
|
1573
|
-
},
|
|
1574
|
-
body: JSON.stringify(body),
|
|
1575
|
-
signal
|
|
1576
|
-
});
|
|
1577
|
-
if (!res.ok) {
|
|
1578
|
-
const errText = await res.text().catch(() => "");
|
|
1579
|
-
throw new Error(
|
|
1580
|
-
`OpenAI API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
3131
|
+
const res = await this.post({ ...this.buildBody(params), stream: true }, signal);
|
|
1583
3132
|
if (!res.body) {
|
|
1584
3133
|
throw new Error("OpenAI streaming response has no readable body");
|
|
1585
3134
|
}
|
|
1586
3135
|
let text = "";
|
|
1587
3136
|
let usage;
|
|
3137
|
+
let finishReason;
|
|
3138
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
1588
3139
|
for await (const raw of readSseDataLines(res.body, signal)) {
|
|
1589
3140
|
if (raw === "[DONE]") break;
|
|
1590
3141
|
let parsed;
|
|
@@ -1593,11 +3144,23 @@ var OpenAiAdapter = class {
|
|
|
1593
3144
|
} catch {
|
|
1594
3145
|
continue;
|
|
1595
3146
|
}
|
|
1596
|
-
const
|
|
3147
|
+
const choice = parsed.choices?.[0];
|
|
3148
|
+
const delta = choice?.delta?.content;
|
|
1597
3149
|
if (typeof delta === "string" && delta.length > 0) {
|
|
1598
3150
|
text += delta;
|
|
1599
3151
|
yield delta;
|
|
1600
3152
|
}
|
|
3153
|
+
for (const call of choice?.delta?.tool_calls ?? []) {
|
|
3154
|
+
const index = typeof call.index === "number" ? call.index : 0;
|
|
3155
|
+
const entry = toolCallAccumulator.get(index) ?? { id: "", name: "", arguments: "" };
|
|
3156
|
+
if (call.id) entry.id = call.id;
|
|
3157
|
+
if (call.function?.name) entry.name = call.function.name;
|
|
3158
|
+
if (call.function?.arguments) entry.arguments += call.function.arguments;
|
|
3159
|
+
toolCallAccumulator.set(index, entry);
|
|
3160
|
+
}
|
|
3161
|
+
if (choice?.finish_reason) {
|
|
3162
|
+
finishReason = choice.finish_reason;
|
|
3163
|
+
}
|
|
1601
3164
|
if (parsed.usage) {
|
|
1602
3165
|
usage = {
|
|
1603
3166
|
promptTokens: parsed.usage.prompt_tokens,
|
|
@@ -1605,7 +3168,19 @@ var OpenAiAdapter = class {
|
|
|
1605
3168
|
};
|
|
1606
3169
|
}
|
|
1607
3170
|
}
|
|
1608
|
-
|
|
3171
|
+
const toolCalls = parseToolCalls(
|
|
3172
|
+
Array.from(toolCallAccumulator.entries()).sort(([a], [b]) => a - b).map(([, entry]) => ({
|
|
3173
|
+
id: entry.id,
|
|
3174
|
+
type: "function",
|
|
3175
|
+
function: { name: entry.name, arguments: entry.arguments }
|
|
3176
|
+
}))
|
|
3177
|
+
);
|
|
3178
|
+
return {
|
|
3179
|
+
text,
|
|
3180
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
3181
|
+
stopReason: mapStopReason(finishReason),
|
|
3182
|
+
usage
|
|
3183
|
+
};
|
|
1609
3184
|
}
|
|
1610
3185
|
};
|
|
1611
3186
|
|
|
@@ -1633,7 +3208,7 @@ var AnthropicAdapter = class {
|
|
|
1633
3208
|
}
|
|
1634
3209
|
buildMessages(params) {
|
|
1635
3210
|
const { userPrompt, messages: chatMessages } = params;
|
|
1636
|
-
return chatMessages && chatMessages.length > 0 ? chatMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({ role: m.role, content: m
|
|
3211
|
+
return chatMessages && chatMessages.length > 0 ? chatMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({ role: m.role, content: getMessageText(m) })) : [
|
|
1637
3212
|
{
|
|
1638
3213
|
role: "user",
|
|
1639
3214
|
content: userPrompt
|
|
@@ -1765,7 +3340,7 @@ var CustomHttpAdapter = class {
|
|
|
1765
3340
|
this.formatRequestBody = options.formatRequestBody || ((params) => ({
|
|
1766
3341
|
messages: params.messages && params.messages.length > 0 ? [
|
|
1767
3342
|
{ role: "system", content: params.systemPrompt },
|
|
1768
|
-
...params.messages.map((m) => ({ role: m.role, content: m
|
|
3343
|
+
...params.messages.map((m) => ({ role: m.role, content: getMessageText(m) }))
|
|
1769
3344
|
] : [
|
|
1770
3345
|
{ role: "system", content: params.systemPrompt },
|
|
1771
3346
|
{ role: "user", content: params.userPrompt }
|
|
@@ -1816,9 +3391,19 @@ var CustomHttpAdapter = class {
|
|
|
1816
3391
|
AnthropicAdapter,
|
|
1817
3392
|
CustomHttpAdapter,
|
|
1818
3393
|
GeminiAdapter,
|
|
3394
|
+
KubuildAiAgent,
|
|
1819
3395
|
KubuildAiEngine,
|
|
1820
3396
|
OpenAiAdapter,
|
|
3397
|
+
READ_TOOLS,
|
|
3398
|
+
WRITE_TOOLS,
|
|
3399
|
+
checkComponentType,
|
|
3400
|
+
checkDocumentSecurity,
|
|
3401
|
+
checkNesting,
|
|
1821
3402
|
createAiHandler,
|
|
1822
|
-
|
|
3403
|
+
createDocumentTools,
|
|
3404
|
+
normalizeIncomingNode,
|
|
3405
|
+
processAiRequest,
|
|
3406
|
+
suggestNodeIds,
|
|
3407
|
+
toToolDefinitions
|
|
1823
3408
|
});
|
|
1824
3409
|
//# sourceMappingURL=index.cjs.map
|