@avocadostudio-ai/orchestrator-core 0.3.2 → 0.4.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/chat/anthropic-planner.d.ts +8 -0
- package/dist/chat/anthropic-planner.js +166 -12
- package/dist/chat/chat-pipeline-translation.d.ts +13 -0
- package/dist/chat/chat-pipeline-translation.js +109 -45
- package/dist/chat/chat-pipeline.d.ts +1 -1
- package/dist/chat/chat-pipeline.js +312 -54
- package/dist/chat/gemini-planner.d.ts +2 -0
- package/dist/chat/gemini-planner.js +2 -1
- package/dist/chat/planner-types.d.ts +15 -0
- package/dist/chat/planner-types.js +2 -2
- package/dist/chat/planner.d.ts +12 -0
- package/dist/chat/planner.js +16 -2
- package/dist/chat/prompts.d.ts +5 -0
- package/dist/chat/prompts.js +92 -9
- package/dist/chat/translation-chunking.d.ts +124 -0
- package/dist/chat/translation-chunking.js +371 -0
- package/dist/checks/field-walk.d.ts +42 -0
- package/dist/checks/field-walk.js +198 -0
- package/dist/checks/index.d.ts +5 -0
- package/dist/checks/index.js +4 -0
- package/dist/checks/page-weight.d.ts +22 -0
- package/dist/checks/page-weight.js +200 -0
- package/dist/checks/rules-draft.d.ts +2 -0
- package/dist/checks/rules-draft.js +439 -0
- package/dist/checks/run-checks.d.ts +42 -0
- package/dist/checks/run-checks.js +159 -0
- package/dist/checks/session-runner.d.ts +19 -0
- package/dist/checks/session-runner.js +99 -0
- package/dist/checks/types.d.ts +109 -0
- package/dist/checks/types.js +1 -0
- package/dist/cms/adapter.d.ts +74 -1
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/index.d.ts +1 -1
- package/dist/cms/index.js +1 -1
- package/dist/cms/media-sources.d.ts +29 -1
- package/dist/cms/media-sources.js +188 -7
- package/dist/durable/durable-store-singleton.d.ts +37 -0
- package/dist/durable/durable-store-singleton.js +179 -0
- package/dist/durable/finding-impact.d.ts +30 -0
- package/dist/durable/finding-impact.js +53 -0
- package/dist/durable/in-memory-durable-store.d.ts +203 -0
- package/dist/durable/in-memory-durable-store.js +363 -0
- package/dist/durable/index.d.ts +5 -0
- package/dist/durable/index.js +4 -0
- package/dist/durable/pending-plan-store.d.ts +28 -0
- package/dist/durable/pending-plan-store.js +156 -0
- package/dist/durable/sqlite-durable-store.d.ts +71 -0
- package/dist/durable/sqlite-durable-store.js +631 -0
- package/dist/durable/types.d.ts +265 -0
- package/dist/durable/types.js +1 -0
- package/dist/handler/create-orchestrator.d.ts +4 -0
- package/dist/handler/create-orchestrator.js +283 -32
- package/dist/http/audio-actions.d.ts +1 -1
- package/dist/http/checks-actions.d.ts +39 -0
- package/dist/http/checks-actions.js +122 -0
- package/dist/http/history-actions.d.ts +44 -1
- package/dist/http/history-actions.js +122 -0
- package/dist/http/image-generate-actions.d.ts +2 -2
- package/dist/http/ops-actions.d.ts +2 -2
- package/dist/http/publish-actions.d.ts +15 -4
- package/dist/http/publish-actions.js +3 -3
- package/dist/http/restore-actions.d.ts +3 -3
- package/dist/http/screenshot-actions.d.ts +2 -2
- package/dist/http/session-actions.d.ts +1 -1
- package/dist/http/telemetry-feedback-actions.d.ts +2 -2
- package/dist/http/unsplash-actions.d.ts +2 -2
- package/dist/http/variations-actions.d.ts +2 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +28 -1
- package/dist/nlp/deterministic-planner-context.d.ts +16 -0
- package/dist/nlp/deterministic-planner-context.js +33 -7
- package/dist/nlp/intent-detection.d.ts +16 -0
- package/dist/nlp/intent-detection.js +15 -1
- package/dist/nlp/plan-normalizer.js +66 -32
- package/dist/ops/destructive-action-gate.js +7 -2
- package/dist/ops/ops-engine.d.ts +12 -1
- package/dist/ops/ops-engine.js +41 -14
- package/dist/publish/publish-helpers.d.ts +12 -2
- package/dist/publish/publish-helpers.js +10 -3
- package/dist/publish/publish-selection.d.ts +84 -0
- package/dist/publish/publish-selection.js +113 -0
- package/dist/publish/publish-target-registry.js +1 -1
- package/dist/publish/publish-target.d.ts +1 -1
- package/dist/publish/targets/git.js +2 -2
- package/dist/state/session-state.js +8 -1
- package/dist/state/site-assets.d.ts +41 -0
- package/dist/state/site-assets.js +40 -0
- package/package.json +3 -3
|
@@ -87,6 +87,21 @@ export type CommonGeneratePlanArgs = {
|
|
|
87
87
|
thinking?: {
|
|
88
88
|
effort: PlannerEffort;
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* Multiplier on the planner's output-token budget (Anthropic). The pipeline
|
|
92
|
+
* raises it on a retry that followed a `max_tokens` truncation, so the next
|
|
93
|
+
* attempt has room the previous one lacked. Ignored by other providers.
|
|
94
|
+
*/
|
|
95
|
+
outputTokenScale?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Restrict the block-schema contracts sent to the model to these types.
|
|
98
|
+
*
|
|
99
|
+
* Set by the translation chunker: a chunk holding a Hero and a CTA cannot
|
|
100
|
+
* legitimately emit an op for a Gallery, so shipping every block's contract to
|
|
101
|
+
* every chunk repeats the largest part of the request for nothing. Leave unset
|
|
102
|
+
* anywhere the model may reference a type that isn't already on the page.
|
|
103
|
+
*/
|
|
104
|
+
contractBlockTypeAllowlist?: string[];
|
|
90
105
|
};
|
|
91
106
|
/**
|
|
92
107
|
* Effort levels accepted by the Messages API `output_config.effort` field on the
|
|
@@ -29,7 +29,7 @@ const openAIPlanner = {
|
|
|
29
29
|
},
|
|
30
30
|
async generatePlan(args) {
|
|
31
31
|
// OpenAI doesn't yet support Anthropic-style thinking events — drop them.
|
|
32
|
-
const { onStatusUpdate: _s, onImageProgress: _i, log: _l, onThinking: _t, thinking: _th, ...rest } = args;
|
|
32
|
+
const { onStatusUpdate: _s, onImageProgress: _i, log: _l, onThinking: _t, thinking: _th, outputTokenScale: _o, ...rest } = args;
|
|
33
33
|
return generatePlanWithOpenAI(rest);
|
|
34
34
|
},
|
|
35
35
|
};
|
|
@@ -44,7 +44,7 @@ const geminiPlanner = {
|
|
|
44
44
|
supportsNativeTools: true,
|
|
45
45
|
parseIntent: parseIntentWithGemini,
|
|
46
46
|
async generatePlan(args) {
|
|
47
|
-
const { onThinking: _t, thinking: _th, ...rest } = args;
|
|
47
|
+
const { onThinking: _t, thinking: _th, outputTokenScale: _o, ...rest } = args;
|
|
48
48
|
return generatePlanWithGemini(rest);
|
|
49
49
|
},
|
|
50
50
|
};
|
package/dist/chat/planner.d.ts
CHANGED
|
@@ -45,6 +45,16 @@ export declare function buildPlannerSchemaContext(args: {
|
|
|
45
45
|
forceFullContracts?: boolean;
|
|
46
46
|
componentsManifest?: BlockManifest;
|
|
47
47
|
effectiveBlockTypes?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Hard ceiling on which block contracts may be sent, whatever mode is chosen.
|
|
50
|
+
*
|
|
51
|
+
* A page-wide translation asks for `full` contracts because it edits every
|
|
52
|
+
* block on the page — but a *chunk* of one edits only the blocks it holds, and
|
|
53
|
+
* shipping all eighteen contracts to each of six chunks repeats ~9 KB six times
|
|
54
|
+
* for no gain. Set only where the caller can guarantee the model cannot
|
|
55
|
+
* legitimately reference a type outside the list.
|
|
56
|
+
*/
|
|
57
|
+
contractBlockTypeAllowlist?: string[];
|
|
48
58
|
}): {
|
|
49
59
|
payload: PlannerSchemaContextPayload;
|
|
50
60
|
meta: PlannerSchemaContextMeta;
|
|
@@ -136,6 +146,8 @@ export declare function generatePlanWithOpenAI(args: {
|
|
|
136
146
|
client?: PlannerOpenAIClient;
|
|
137
147
|
siteContextBlock?: string | null;
|
|
138
148
|
forceFullSchemaContracts?: boolean;
|
|
149
|
+
/** Restrict block-schema contracts to these types. See CommonGeneratePlanArgs. */
|
|
150
|
+
contractBlockTypeAllowlist?: string[];
|
|
139
151
|
componentsManifest?: BlockManifest;
|
|
140
152
|
lightweight?: boolean;
|
|
141
153
|
signal?: AbortSignal;
|
package/dist/chat/planner.js
CHANGED
|
@@ -191,13 +191,26 @@ function buildContractsForMode(args) {
|
|
|
191
191
|
payload.pageMetaContract = pageMetaContractSummary();
|
|
192
192
|
return payload;
|
|
193
193
|
}
|
|
194
|
+
/** Narrow the contract set to an allowlist, when the caller supplied a non-empty one. */
|
|
195
|
+
function restrictContracts(allContracts, allowlist) {
|
|
196
|
+
if (!allowlist || allowlist.length === 0)
|
|
197
|
+
return allContracts;
|
|
198
|
+
const restricted = {};
|
|
199
|
+
for (const type of allowlist) {
|
|
200
|
+
if (type in allContracts)
|
|
201
|
+
restricted[type] = allContracts[type];
|
|
202
|
+
}
|
|
203
|
+
// An allowlist that matches nothing is a caller bug, not an instruction to
|
|
204
|
+
// send the model a page it has no schema for — fall back to everything.
|
|
205
|
+
return Object.keys(restricted).length > 0 ? restricted : allContracts;
|
|
206
|
+
}
|
|
194
207
|
export function buildPlannerSchemaContext(args) {
|
|
195
208
|
const strictJsonEnabled = isStrictJsonResponseEnabled();
|
|
196
209
|
const targetBlockTypes = pickTargetBlockTypes({ message: args.message, contextPack: args.contextPack });
|
|
197
210
|
const includePageMetaContract = /\b(seo|meta|metadata|og\s*image|open\s*graph|description|structured\s*data|schema\.org)\b/i.test(args.message) || /\d{2,3}\s*char/i.test(args.message);
|
|
198
211
|
const knownTypes = args.effectiveBlockTypes
|
|
199
212
|
?? (args.componentsManifest ? args.componentsManifest.blocks.map(c => c.type) : Object.keys(blockSchemas));
|
|
200
|
-
const allContracts = blockContractsSummary(args.componentsManifest);
|
|
213
|
+
const allContracts = restrictContracts(blockContractsSummary(args.componentsManifest), args.contractBlockTypeAllowlist);
|
|
201
214
|
if (!isAdaptiveSchemaContextEnabled()) {
|
|
202
215
|
const payload = args.legacyIncludeContracts
|
|
203
216
|
? {
|
|
@@ -1162,7 +1175,8 @@ export async function generatePlanWithOpenAI(args) {
|
|
|
1162
1175
|
pageWideTranslation,
|
|
1163
1176
|
legacyIncludeContracts: includeContracts,
|
|
1164
1177
|
forceFullContracts: args.forceFullSchemaContracts,
|
|
1165
|
-
componentsManifest: args.componentsManifest
|
|
1178
|
+
componentsManifest: args.componentsManifest,
|
|
1179
|
+
contractBlockTypeAllowlist: args.contractBlockTypeAllowlist
|
|
1166
1180
|
});
|
|
1167
1181
|
const user = {
|
|
1168
1182
|
request: args.message,
|
package/dist/chat/prompts.d.ts
CHANGED
|
@@ -65,3 +65,8 @@ export declare function buildPlannerSystemPromptSegments(opts: PlannerPromptOpti
|
|
|
65
65
|
stable: string;
|
|
66
66
|
dynamic: string;
|
|
67
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* The subset of the corrections above that is true of THIS site's catalogue.
|
|
70
|
+
* Returns one prompt line per surviving correction, or an empty array.
|
|
71
|
+
*/
|
|
72
|
+
export declare function propNameCorrectionLines(effectiveBlockTypes: string[]): string[];
|
package/dist/chat/prompts.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Eliminates duplication between OpenAI and Anthropic planner modules.
|
|
5
5
|
* Provider-specific extensions are injected via the `provider` option.
|
|
6
6
|
*/
|
|
7
|
+
import { blockAcceptsProp, blockListItemAcceptsKey } from "@avocadostudio-ai/shared";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Intent parser
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -154,7 +155,13 @@ const ANTHROPIC_IMAGE_TOOL_LINES = [
|
|
|
154
155
|
"When using image.generate, write the returned imageUrl into the relevant imageUrl field and set imageAlt from the returned alt text.",
|
|
155
156
|
];
|
|
156
157
|
const BLOCK_NAME_PRIVACY_OPENAI = "Never mention internal block IDs (b_hero_*, b_featuregrid_*, etc.), prop names (imageUrl, imageAlt), or system settings in summary_for_user or change_log. Use human-friendly descriptions instead (e.g. 'Update the Hero image' not 'Update imageUrl on b_hero_123').";
|
|
157
|
-
|
|
158
|
+
/*
|
|
159
|
+
* Stated as a rule about identifiers rather than as a list of our block names:
|
|
160
|
+
* a site with its own catalogue has its own CamelCase type names, and an
|
|
161
|
+
* allowlist of ours neither covers them nor describes them. The test is whether
|
|
162
|
+
* the word reads as English to someone who has never seen the schema.
|
|
163
|
+
*/
|
|
164
|
+
const BLOCK_NAME_PRIVACY_ANTHROPIC = "Never mention internal block IDs (b_hero_*, b_featuregrid_*, etc.), prop names (imageUrl, imageAlt), or system settings in summary_for_user, change_log, or suggested_next_actions. Also avoid raw block type names — they are schema identifiers, not words users know. Describe the section by what it is: a type named 'RichText' is a 'text section', 'FeatureGrid' a 'features grid', 'FAQAccordion' an 'FAQ section', 'heroSplit' or 'PageHeader' just 'the hero' or 'the page header'. The test is whether the word reads as ordinary English rather than as a name from a schema; where it does — 'Hero', 'CTA', 'Testimonials', 'Gallery' — using it as-is is fine.";
|
|
158
165
|
// ---------------------------------------------------------------------------
|
|
159
166
|
// Full planner prompt — composed from section builders.
|
|
160
167
|
// Each section becomes a ## HEADER in the emitted prompt so the LLM can
|
|
@@ -168,16 +175,18 @@ function joinSections(sections) {
|
|
|
168
175
|
}
|
|
169
176
|
function buildFullPlannerSegments(opts) {
|
|
170
177
|
const hasNativeTools = opts.provider === "anthropic" || opts.provider === "gemini";
|
|
171
|
-
// Stable sections —
|
|
172
|
-
//
|
|
173
|
-
// the
|
|
174
|
-
//
|
|
178
|
+
// Stable sections — the part Anthropic caches. "Stable" means stable across
|
|
179
|
+
// *requests*, not constant: IMAGES varies with hasNativeTools and OPERATION
|
|
180
|
+
// CATALOG with the site's block catalogue, and both are fixed for a given
|
|
181
|
+
// provider and site. Two sites with different catalogues get two cache
|
|
182
|
+
// entries, which is the correct trade — a prompt that tells the model the
|
|
183
|
+
// wrong prop names caches beautifully and edits the wrong field.
|
|
175
184
|
const stableSections = [
|
|
176
185
|
sectionRole(),
|
|
177
186
|
sectionOutputContract(),
|
|
178
187
|
sectionIntentDecisionTree(),
|
|
179
188
|
sectionVoice(opts, hasNativeTools),
|
|
180
|
-
sectionOperationCatalog(),
|
|
189
|
+
sectionOperationCatalog(opts),
|
|
181
190
|
sectionSchemaDiscipline(),
|
|
182
191
|
sectionImages(hasNativeTools),
|
|
183
192
|
];
|
|
@@ -241,14 +250,88 @@ function sectionVoice(opts, hasNativeTools) {
|
|
|
241
250
|
if (hasNativeTools) {
|
|
242
251
|
lines.push("For edit_plan intent: summary_for_user must be ONE short sentence (max ~20 words) describing what the plan will do. Do NOT elaborate, explain why, or describe the content being added — let change_log carry the detail. Bad: 'Updated the hero heading with a punchier tone.' Good: 'Will add a **text section** about blueberry varieties after the features grid.'", "change_log coverage is MANDATORY: emit exactly one change_log entry per op, in the same order as ops[], describing what that specific op does. If ops has N entries, change_log must have N entries — never cluster multiple ops into one entry, never skip an op, never leave an op undescribed. The user reads change_log to decide whether to approve; a missing entry is a silent bait-and-switch.", "change_log entries should add specific detail NOT already in summary_for_user — e.g. list the actual content, items, or values being set. Do not paraphrase the summary.");
|
|
243
252
|
}
|
|
244
|
-
lines.push("In summary_for_user, use simple markdown for readability: **bold** for key terms or labels, and bullet lists (- item) when listing multiple items, recommendations, or observations. Keep it scannable — avoid walls of text.", "When rewriting text, return plain text unless the prop is a rich-text prop (see below) or the user explicitly asks for markdown formatting. Do not wrap the entire rewrite in **bold** markers.", RULE_RICH_TEXT_PROPS, "For copy in German or similar long-compound languages, insert soft hyphen opportunities in long compounds where helpful for responsive line wrapping. Use the Unicode soft hyphen character (U+00AD), never HTML entities like ­ or ­.", opts.provider !== "openai" ? BLOCK_NAME_PRIVACY_ANTHROPIC : BLOCK_NAME_PRIVACY_OPENAI, "", "### suggested_next_actions", "2-4 short imperative phrases the user could type next (max 6 words each). Each MUST be a logical follow-up to the specific change just made — not a generic action. NEVER suggest 'Open /X' or any navigation to a page that the current plan is creating, duplicating, or otherwise still pending — the page won't exist until the user approves the plan, and there is no special navigation chip handler (suggestions are sent verbatim as the next chat command). Suggest plan refinements instead (e.g. 'Use a punchier hero headline', 'Add a card grid for spotlights', 'Drop the FAQ section'). Ask yourself: 'what would the user likely want to do next given THIS edit?' When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', 'Revert to previous'). For example, after rewriting stats labels, suggest refining the same section ('Make the numbers bigger', 'Add a stat about X') — not unrelated actions like 'Change title' or 'Add a Testimonials section'. For needs_clarification, suggest the most likely concrete answers. Omit suggested_next_actions entirely if no contextual follow-up is obvious. Every suggestion must be an action the user can perform inside this editor — restricted to the block types in blockContracts / blockCatalogue
|
|
253
|
+
lines.push("In summary_for_user, use simple markdown for readability: **bold** for key terms or labels, and bullet lists (- item) when listing multiple items, recommendations, or observations. Keep it scannable — avoid walls of text.", "When rewriting text, return plain text unless the prop is a rich-text prop (see below) or the user explicitly asks for markdown formatting. Do not wrap the entire rewrite in **bold** markers.", RULE_RICH_TEXT_PROPS, "For copy in German or similar long-compound languages, insert soft hyphen opportunities in long compounds where helpful for responsive line wrapping. Use the Unicode soft hyphen character (U+00AD), never HTML entities like ­ or ­.", opts.provider !== "openai" ? BLOCK_NAME_PRIVACY_ANTHROPIC : BLOCK_NAME_PRIVACY_OPENAI, "", "### suggested_next_actions", "2-4 short imperative phrases the user could type next (max 6 words each). Each MUST be a logical follow-up to the specific change just made — not a generic action. NEVER suggest 'Open /X' or any navigation to a page that the current plan is creating, duplicating, or otherwise still pending — the page won't exist until the user approves the plan, and there is no special navigation chip handler (suggestions are sent verbatim as the next chat command). Suggest plan refinements instead (e.g. 'Use a punchier hero headline', 'Add a card grid for spotlights', 'Drop the FAQ section'). Ask yourself: 'what would the user likely want to do next given THIS edit?' When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', 'Revert to previous'). For example, after rewriting stats labels, suggest refining the same section ('Make the numbers bigger', 'Add a stat about X') — not unrelated actions like 'Change title' or 'Add a Testimonials section'. For needs_clarification, suggest the most likely concrete answers. Omit suggested_next_actions entirely if no contextual follow-up is obvious. Every suggestion must be an action the user can perform inside this editor — restricted to the block types listed in blockContracts / blockCatalogue for THIS site, or SEO/site-config edits. Never suggest adding a section this site has no block for; the catalogue is the whole list, not a sample of a larger one. NEVER suggest unsupported features: no forms, no email capture, no contact forms, no subscribe boxes, no newsletter signups, no popups/modals, no chat widgets, no live video, no payment/checkout — these require custom code the editor cannot produce. Never suggest actions outside the editor's scope such as A/B testing, analytics, performance monitoring, user research, or marketing strategy.");
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
256
|
+
/*
|
|
257
|
+
* Prop-name corrections the model actually needs, and the reason they are not
|
|
258
|
+
* written as prose.
|
|
259
|
+
*
|
|
260
|
+
* Models reach for the obvious English word — `heading` for a section title,
|
|
261
|
+
* `question`/`answer` for an FAQ entry, `testimonial` for a quote — and several
|
|
262
|
+
* of Avocado's built-in blocks chose a different name. Telling the model so is
|
|
263
|
+
* worth real accuracy, and the line that did it read: "use 'title' not
|
|
264
|
+
* 'heading' for section titles (except Hero which uses 'heading')".
|
|
265
|
+
*
|
|
266
|
+
* That is a fact about Avocado's own catalogue stated as a fact about section
|
|
267
|
+
* titles. A site that brings its own blocks and names a text prop `heading` was
|
|
268
|
+
* being instructed, in the system prompt, to emit a prop its schema does not
|
|
269
|
+
* have. It is the same mistake the plan normalizer was fixed for in 0.3.3 — the
|
|
270
|
+
* normalizer no longer *creates* the wrong prop name, but nothing had stopped
|
|
271
|
+
* the prompt from *asking* for it, so the bug simply moved one layer up.
|
|
272
|
+
*
|
|
273
|
+
* So each correction now names the block type it is about, and is emitted only
|
|
274
|
+
* when the registry confirms it is still true: the type is in this site's
|
|
275
|
+
* catalogue, it really does reject the wrong key, and it really does accept the
|
|
276
|
+
* right one. A site that registers its own `Hero` with a `title` prop gets no
|
|
277
|
+
* line about Hero; a site with no FAQAccordion gets no line about `q`/`a`.
|
|
278
|
+
*/
|
|
279
|
+
const PROP_NAME_CORRECTIONS = [
|
|
280
|
+
{ type: "Hero", wrong: "title", right: "heading", what: "the headline" },
|
|
281
|
+
{ type: "FeatureGrid", wrong: "heading", right: "title", what: "the section title" },
|
|
282
|
+
{ type: "CardGrid", wrong: "heading", right: "title", what: "the section title" },
|
|
283
|
+
{ type: "Testimonials", wrong: "heading", right: "title", what: "the section title" },
|
|
284
|
+
{ type: "FAQAccordion", wrong: "heading", right: "title", what: "the section title" },
|
|
285
|
+
{ type: "CTA", wrong: "title", right: "heading", what: "the headline" },
|
|
286
|
+
];
|
|
287
|
+
const LIST_ITEM_CORRECTIONS = [
|
|
288
|
+
{ type: "FAQAccordion", list: "items", wrong: "question", right: "q" },
|
|
289
|
+
{ type: "FAQAccordion", list: "items", wrong: "answer", right: "a" },
|
|
290
|
+
{ type: "Testimonials", list: "items", wrong: "testimonial", right: "quote" },
|
|
291
|
+
];
|
|
292
|
+
/**
|
|
293
|
+
* The subset of the corrections above that is true of THIS site's catalogue.
|
|
294
|
+
* Returns one prompt line per surviving correction, or an empty array.
|
|
295
|
+
*/
|
|
296
|
+
export function propNameCorrectionLines(effectiveBlockTypes) {
|
|
297
|
+
const present = new Set(effectiveBlockTypes);
|
|
298
|
+
const lines = [];
|
|
299
|
+
for (const c of PROP_NAME_CORRECTIONS) {
|
|
300
|
+
if (!present.has(c.type))
|
|
301
|
+
continue;
|
|
302
|
+
if (blockAcceptsProp(c.type, c.wrong))
|
|
303
|
+
continue;
|
|
304
|
+
if (!blockAcceptsProp(c.type, c.right))
|
|
305
|
+
continue;
|
|
306
|
+
lines.push(`${c.type} names ${c.what} '${c.right}', not '${c.wrong}'.`);
|
|
307
|
+
}
|
|
308
|
+
for (const c of LIST_ITEM_CORRECTIONS) {
|
|
309
|
+
if (!present.has(c.type))
|
|
310
|
+
continue;
|
|
311
|
+
if (blockListItemAcceptsKey(c.type, c.list, c.wrong))
|
|
312
|
+
continue;
|
|
313
|
+
if (!blockListItemAcceptsKey(c.type, c.list, c.right))
|
|
314
|
+
continue;
|
|
315
|
+
lines.push(`${c.type} \`${c.list}\` entries use '${c.right}', not '${c.wrong}'.`);
|
|
316
|
+
}
|
|
245
317
|
return lines;
|
|
246
318
|
}
|
|
247
|
-
function sectionOperationCatalog() {
|
|
319
|
+
function sectionOperationCatalog(opts) {
|
|
320
|
+
/*
|
|
321
|
+
* The universal rule first, so it governs even when no correction below
|
|
322
|
+
* applies: a prop name is a property of one block type's schema, never of the
|
|
323
|
+
* English word for the thing it holds.
|
|
324
|
+
*/
|
|
325
|
+
const corrections = propNameCorrectionLines(opts.effectiveBlockTypes);
|
|
326
|
+
const addBlockPropRule = "add_block: use the exact prop names blockContracts lists for THAT block type. A prop name is a fact about one block's schema, not about the kind of content it holds — never carry a prop name across from another block type, and never infer one from the English word for the field ('heading', 'question', 'subtitle'). If blockContracts does not list a prop, the block does not have it: omit it rather than inventing a plausible name." +
|
|
327
|
+
(corrections.length > 0
|
|
328
|
+
? ` Corrections for blocks on this site that models routinely get wrong: ${corrections.join(" ")}`
|
|
329
|
+
: "") +
|
|
330
|
+
" Always populate block.props with REAL content matching the user's request (headlines, body copy, list items grounded in the user's topic) — never emit add_block with empty or missing props; the system falls back to demo template defaults if you omit props, and that template will mislead the user. Placement: set `afterBlockId` to the id of the block the new section should follow — e.g. to add a section 'below the hero' / 'under the hero' / 'after the hero', set afterBlockId to the hero block's id from the pageOutline. Honor any position the user names ('below the hero', 'above the footer', 'after the pricing'). Omit afterBlockId only when no position is implied — it then appends at the very end of the page.";
|
|
248
331
|
return [
|
|
249
332
|
"## OPERATION CATALOG",
|
|
250
333
|
"update_props: blockId is required and must target an existing block id (b_*). Never use a page route/path as blockId or path. Use blockId values from the pageOutline — never invent block IDs. Set patch to changed props only; use existing prop keys for the target block type. Emit keys in this exact order: op, pageSlug (if present), blockId, patch.",
|
|
251
|
-
|
|
334
|
+
addBlockPropRule,
|
|
252
335
|
"remove_block: delete an ENTIRE block/section from the page (blockId required, no listKey). Use this — NOT remove_item — whenever the user removes a whole section, INCLUDING when they pick it by POSITION or TYPE: 'delete the second card grid', 'remove the first feature grid', 'hide the third section', 'remove the FAQ', 'get rid of the testimonials'. For 'remove ALL the <type>' / 'delete every <type>', emit one remove_block per matching block. remove_item is ONLY for deleting ONE entry inside a block's list and is signalled by an item word scoped INTO a container ('remove the last card IN the grid', 'delete a question FROM the FAQ'). A bare '[the Nth] <block-or-section-noun>' with no into-container preposition is ALWAYS a remove_block — never remove an item to satisfy it.",
|
|
253
336
|
"add_item / update_item / remove_item / move_item: edit ONE entry inside a block's list prop (e.g. FeatureGrid `features`, FAQAccordion `items`, Testimonials `items`, CardGrid `cards`). Required on every item op: pageSlug, blockId, listKey. Address the target entry by its stable `itemId` (the item's `id` field) WHEN the block's full props are in your context — never guess or invent an itemId you cannot see. Otherwise use the 0-based `index`; the system resolves it against the current page state, so an index is safe too. add_item: supply `item` with the entry's props (do NOT invent an `id`, one is assigned); optional `afterItemId` (or `afterIndex`) positions it, omit ⇒ append. update_item: `patch` is a merge-patch over that entry's fields. move_item: `afterItemId` (or `afterIndex`) sets the new position, omit ⇒ move to front. When changing SEVERAL entries of one list in a single plan, replacing the whole list with one update_props op is also fine.",
|
|
254
337
|
"update_page_meta: set SEO metadata (title, description, ogImage) on a page. Patch is merge-patch: only supplied keys update. Set a field to empty string to clear it.",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { EditPlan, PageDoc } from "@avocadostudio-ai/shared";
|
|
2
|
+
import type { plannerContextPack } from "../nlp/deterministic-planner.ts";
|
|
3
|
+
import type { CommonGeneratePlanArgs, GeneratePlanResult } from "./planner-types.ts";
|
|
4
|
+
export declare function translationChunkingConfig(): {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Translatable bytes to aim for per chunk.
|
|
8
|
+
*
|
|
9
|
+
* Chunk wall clock is roughly a fixed time-to-first-token plus streaming time
|
|
10
|
+
* proportional to this. Measured on Sonnet 5: ~5s to first token, then ~130
|
|
11
|
+
* output tokens/second, and a chunk's output runs a little over one token per
|
|
12
|
+
* source byte. ~900 bytes puts streaming (~8s) in the same range as the fixed
|
|
13
|
+
* cost, which is where splitting further stops buying much and starts paying
|
|
14
|
+
* another chunk's input tokens for nothing.
|
|
15
|
+
*/
|
|
16
|
+
targetBytes: number;
|
|
17
|
+
/** Upper bound on parallel planner calls — one wave, not a stampede. */
|
|
18
|
+
maxChunks: number;
|
|
19
|
+
/** Below these, a single request is already fast and chunking only adds input cost. */
|
|
20
|
+
minBlocks: number;
|
|
21
|
+
minBytes: number;
|
|
22
|
+
};
|
|
23
|
+
export type TranslationChunk = {
|
|
24
|
+
blockIds: string[];
|
|
25
|
+
/** Translatable bytes carried by this chunk — the driver of its output length. */
|
|
26
|
+
bytes: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Partition the page's translatable blocks into balanced chunks.
|
|
30
|
+
*
|
|
31
|
+
* Wall clock is the *heaviest* chunk, not the average one, so balance is the
|
|
32
|
+
* whole game: a first live run split a page into 1604 / 1010 / 172 / 769 bytes
|
|
33
|
+
* and spent 17 of its 23 seconds waiting on the first bin while a 172-byte bin
|
|
34
|
+
* sat finished. Packing blocks in page order is what produces that — it has to
|
|
35
|
+
* close a chunk before it can see what comes next.
|
|
36
|
+
*
|
|
37
|
+
* So blocks are packed heaviest-first into whichever chunk is currently lightest
|
|
38
|
+
* (longest-processing-time-first), which bounds the heaviest chunk far better,
|
|
39
|
+
* and page order is restored afterwards so the change log still reads top to
|
|
40
|
+
* bottom. Blocks are never split: an `update_props` op carries a whole block.
|
|
41
|
+
*
|
|
42
|
+
* Returns a single chunk when the page is too small to be worth splitting; the
|
|
43
|
+
* caller treats that as "don't chunk".
|
|
44
|
+
*/
|
|
45
|
+
export declare function planTranslationChunks(args: {
|
|
46
|
+
page: PageDoc;
|
|
47
|
+
targetBytes?: number;
|
|
48
|
+
maxChunks?: number;
|
|
49
|
+
}): TranslationChunk[];
|
|
50
|
+
/** Whether this page is worth fanning out, per the configured thresholds. */
|
|
51
|
+
export declare function shouldChunkTranslation(page: PageDoc, chunks: TranslationChunk[]): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The page as this chunk should see it: same id/slug/meta, only its own blocks.
|
|
54
|
+
*
|
|
55
|
+
* Everything downstream in the planner derives from `currentPage` — the output
|
|
56
|
+
* token budget, and the enumerated translation checklist handed to the model —
|
|
57
|
+
* so narrowing the page is what makes a chunk a chunk. No extra plumbing.
|
|
58
|
+
*/
|
|
59
|
+
export declare function subsetPageForChunk(page: PageDoc, blockIds: Set<string>): PageDoc;
|
|
60
|
+
/** The context pack narrowed to the chunk's blocks, so input cost doesn't repeat the whole page N times. */
|
|
61
|
+
export declare function subsetContextPackForChunk(pack: ReturnType<typeof plannerContextPack>, blockIds: Set<string>): ReturnType<typeof plannerContextPack>;
|
|
62
|
+
/**
|
|
63
|
+
* Keep only what this chunk was asked for.
|
|
64
|
+
*
|
|
65
|
+
* A chunk sees a slice of the page, but nothing stops a model from volunteering
|
|
66
|
+
* an op for a block it half-remembers from the site context, or from restructuring
|
|
67
|
+
* a page it was asked to translate. Ops outside the chunk's blocks are dropped
|
|
68
|
+
* rather than merged: a translation that also deletes a section is not a
|
|
69
|
+
* translation, and two chunks editing the same block would silently race.
|
|
70
|
+
*/
|
|
71
|
+
export declare function filterChunkOps(args: {
|
|
72
|
+
ops: EditPlan["ops"];
|
|
73
|
+
blockIds: Set<string>;
|
|
74
|
+
isFirstChunk: boolean;
|
|
75
|
+
}): {
|
|
76
|
+
kept: EditPlan["ops"];
|
|
77
|
+
droppedCount: number;
|
|
78
|
+
};
|
|
79
|
+
export type ChunkOutcome = {
|
|
80
|
+
index: number;
|
|
81
|
+
blockIds: string[];
|
|
82
|
+
ok: boolean;
|
|
83
|
+
opCount: number;
|
|
84
|
+
droppedOpCount: number;
|
|
85
|
+
attempts: number;
|
|
86
|
+
/** Wall clock for this chunk. The slowest one is the request's latency — the number to tune against. */
|
|
87
|
+
durationMs: number;
|
|
88
|
+
bytes: number;
|
|
89
|
+
outputTokens: number;
|
|
90
|
+
reason?: string;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Run every chunk in parallel and merge the results into one plan.
|
|
94
|
+
*
|
|
95
|
+
* A chunk that dies on a token ceiling is retried once on its own, with double
|
|
96
|
+
* the budget — retrying one chunk is cheap where retrying the page is not. A
|
|
97
|
+
* chunk that still fails is left out: the merged plan is short those blocks, and
|
|
98
|
+
* the pipeline's translation coverage gate is what notices and repairs that. The
|
|
99
|
+
* whole call only fails when every chunk failed, so the caller's own retry loop
|
|
100
|
+
* sees a normal planner failure.
|
|
101
|
+
*/
|
|
102
|
+
export declare function generateChunkedTranslationPlan(args: {
|
|
103
|
+
plannerArgs: CommonGeneratePlanArgs;
|
|
104
|
+
chunks: TranslationChunk[];
|
|
105
|
+
generate: (plannerArgs: CommonGeneratePlanArgs) => Promise<GeneratePlanResult>;
|
|
106
|
+
log?: {
|
|
107
|
+
warn: (obj: Record<string, unknown>, msg: string) => void;
|
|
108
|
+
info?: (obj: Record<string, unknown>, msg: string) => void;
|
|
109
|
+
};
|
|
110
|
+
onChunksSettled?: (outcomes: ChunkOutcome[]) => void;
|
|
111
|
+
}): Promise<GeneratePlanResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Fold the chunk plans back into one.
|
|
114
|
+
*
|
|
115
|
+
* Chunks are balanced by weight rather than page position, so their ops come
|
|
116
|
+
* back shuffled; `blockOrder` puts them back the way the page reads, which is
|
|
117
|
+
* the order the change log and the plan preview are reviewed in.
|
|
118
|
+
*
|
|
119
|
+
* The user-facing summary is not concatenated: every chunk was given the user's
|
|
120
|
+
* original message, so every chunk wrote a summary of the same request
|
|
121
|
+
* ("Translated the page into Russian"), and stacking six of those reads like a
|
|
122
|
+
* stutter. The first one stands for all.
|
|
123
|
+
*/
|
|
124
|
+
export declare function mergeChunkResults(results: GeneratePlanResult[], blockOrder?: string[]): GeneratePlanResult;
|