@avocadostudio-ai/orchestrator-core 0.3.1 → 0.3.3

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.
Files changed (73) hide show
  1. package/dist/chat/anthropic-planner.d.ts +8 -0
  2. package/dist/chat/anthropic-planner.js +166 -12
  3. package/dist/chat/chat-pipeline-translation.d.ts +13 -0
  4. package/dist/chat/chat-pipeline-translation.js +109 -45
  5. package/dist/chat/chat-pipeline.d.ts +1 -1
  6. package/dist/chat/chat-pipeline.js +297 -53
  7. package/dist/chat/gemini-planner.d.ts +2 -0
  8. package/dist/chat/gemini-planner.js +2 -1
  9. package/dist/chat/hallucination-validator.d.ts +6 -0
  10. package/dist/chat/hallucination-validator.js +49 -8
  11. package/dist/chat/planner-types.d.ts +15 -0
  12. package/dist/chat/planner-types.js +2 -2
  13. package/dist/chat/planner.d.ts +12 -0
  14. package/dist/chat/planner.js +16 -2
  15. package/dist/chat/translation-chunking.d.ts +124 -0
  16. package/dist/chat/translation-chunking.js +371 -0
  17. package/dist/checks/field-walk.d.ts +25 -0
  18. package/dist/checks/field-walk.js +152 -0
  19. package/dist/checks/index.d.ts +5 -0
  20. package/dist/checks/index.js +4 -0
  21. package/dist/checks/page-weight.d.ts +22 -0
  22. package/dist/checks/page-weight.js +200 -0
  23. package/dist/checks/rules-draft.d.ts +2 -0
  24. package/dist/checks/rules-draft.js +375 -0
  25. package/dist/checks/run-checks.d.ts +32 -0
  26. package/dist/checks/run-checks.js +152 -0
  27. package/dist/checks/session-runner.d.ts +19 -0
  28. package/dist/checks/session-runner.js +95 -0
  29. package/dist/checks/types.d.ts +65 -0
  30. package/dist/checks/types.js +1 -0
  31. package/dist/cms/adapter.d.ts +1 -0
  32. package/dist/durable/durable-store-singleton.d.ts +37 -0
  33. package/dist/durable/durable-store-singleton.js +179 -0
  34. package/dist/durable/finding-impact.d.ts +30 -0
  35. package/dist/durable/finding-impact.js +53 -0
  36. package/dist/durable/in-memory-durable-store.d.ts +203 -0
  37. package/dist/durable/in-memory-durable-store.js +363 -0
  38. package/dist/durable/index.d.ts +5 -0
  39. package/dist/durable/index.js +4 -0
  40. package/dist/durable/pending-plan-store.d.ts +28 -0
  41. package/dist/durable/pending-plan-store.js +156 -0
  42. package/dist/durable/sqlite-durable-store.d.ts +71 -0
  43. package/dist/durable/sqlite-durable-store.js +631 -0
  44. package/dist/durable/types.d.ts +265 -0
  45. package/dist/durable/types.js +1 -0
  46. package/dist/handler/create-orchestrator.d.ts +4 -0
  47. package/dist/handler/create-orchestrator.js +85 -9
  48. package/dist/http/audio-actions.d.ts +1 -1
  49. package/dist/http/checks-actions.d.ts +39 -0
  50. package/dist/http/checks-actions.js +122 -0
  51. package/dist/http/history-actions.d.ts +1 -1
  52. package/dist/http/image-generate-actions.d.ts +2 -2
  53. package/dist/http/ops-actions.d.ts +2 -2
  54. package/dist/http/publish-actions.d.ts +4 -4
  55. package/dist/http/restore-actions.d.ts +3 -3
  56. package/dist/http/screenshot-actions.d.ts +2 -2
  57. package/dist/http/session-actions.d.ts +1 -1
  58. package/dist/http/telemetry-feedback-actions.d.ts +2 -2
  59. package/dist/http/unsplash-actions.d.ts +2 -2
  60. package/dist/http/variations-actions.d.ts +2 -2
  61. package/dist/index.d.ts +7 -0
  62. package/dist/index.js +27 -0
  63. package/dist/nlp/deterministic-planner-context.d.ts +16 -0
  64. package/dist/nlp/deterministic-planner-context.js +33 -7
  65. package/dist/nlp/deterministic-planner-suggestions.js +1 -1
  66. package/dist/nlp/plan-normalizer.js +193 -56
  67. package/dist/ops/destructive-action-gate.js +7 -2
  68. package/dist/ops/ops-engine.d.ts +12 -1
  69. package/dist/ops/ops-engine.js +41 -14
  70. package/dist/publish/publish-target-registry.js +1 -1
  71. package/dist/publish/publish-target.d.ts +1 -1
  72. package/dist/state/session-state.js +8 -1
  73. package/package.json +3 -3
@@ -45,6 +45,22 @@ function findBlockType(args) {
45
45
  }
46
46
  return undefined;
47
47
  }
48
+ /*
49
+ * Does this key read as a visual/presentational one?
50
+ *
51
+ * Deliberately a small allow-list of stems rather than a clever rule: the
52
+ * default has to be "content", because the expensive mistake is telling
53
+ * somebody their content edit was a styling limitation, not the reverse.
54
+ */
55
+ const VISUAL_PROP_STEMS = [
56
+ "color", "colour", "background", "gradient", "animation", "animate", "shadow",
57
+ "font", "size", "spacing", "padding", "margin", "border", "radius", "opacity",
58
+ "align", "theme", "style", "variant", "width", "height", "position", "layout"
59
+ ];
60
+ function isVisualPropName(prop) {
61
+ const lower = prop.toLowerCase();
62
+ return VISUAL_PROP_STEMS.some((stem) => lower.includes(stem));
63
+ }
48
64
  function humanBlockName(blockType) {
49
65
  const meta = getBlockMeta(blockType);
50
66
  return meta?.displayName ?? blockType;
@@ -81,15 +97,33 @@ export function validateAndStripHallucinatedProps(args) {
81
97
  if (allowedKeys.has(key))
82
98
  continue;
83
99
  delete patchCandidate[key];
84
- hallucinatedProps.push({ blockId: op.blockId, blockType, propName: key });
100
+ hallucinatedProps.push({
101
+ blockId: op.blockId,
102
+ blockType,
103
+ propName: key,
104
+ allowedProps: [...allowedKeys].sort()
105
+ });
85
106
  }
86
107
  }
87
108
  if (hallucinatedProps.length > 0) {
88
- // Merge duplicates into a single readable note keyed by blockType. We
89
- // intentionally avoid echoing the raw prop name back to the user —
90
- // doing so is (a) jargon-y (users don't think in prop keys), and (b)
91
- // makes the note trivially collide with eval banned-word checks that
92
- // try to prove the planner didn't promise the unsupported behavior.
109
+ /*
110
+ * Two different events used to share one sentence, and the wrong one was
111
+ * the default.
112
+ *
113
+ * "Some requested styling isn't available" is true when the planner
114
+ * promised a colour, a gradient or an animation the block has no field
115
+ * for. It is false — and actively misleading — when the planner simply
116
+ * used the wrong *name* for a field the block does have under another
117
+ * name: nothing about that is styling, and the sentence sends the reader
118
+ * to look at their design system. An integrator lost nine minutes to
119
+ * exactly that, on a block whose schema, registry and ops path were all
120
+ * correct.
121
+ *
122
+ * So classify the stripped key. A visual key keeps the original wording,
123
+ * because that is the case it was written for and the evals that check it
124
+ * are checking that case. A content key gets a sentence that names it,
125
+ * which is the one piece of information that ends the search.
126
+ */
93
127
  const byBlockType = new Map();
94
128
  for (const entry of hallucinatedProps) {
95
129
  const bucket = byBlockType.get(entry.blockType) ?? new Set();
@@ -97,9 +131,16 @@ export function validateAndStripHallucinatedProps(args) {
97
131
  byBlockType.set(entry.blockType, bucket);
98
132
  }
99
133
  const noteParts = [];
100
- for (const [blockType] of byBlockType) {
134
+ for (const [blockType, props] of byBlockType) {
101
135
  const name = humanBlockName(blockType);
102
- noteParts.push(`Some requested styling isn't available on the ${name} block — applied the supported parts.`);
136
+ const visual = [...props].filter(isVisualPropName);
137
+ const content = [...props].filter((prop) => !isVisualPropName(prop));
138
+ if (visual.length > 0) {
139
+ noteParts.push(`Some requested styling isn't available on the ${name} block — applied the supported parts.`);
140
+ }
141
+ for (const prop of content) {
142
+ noteParts.push(`The ${name} block has no “${prop}” field, so that part wasn't applied.`);
143
+ }
103
144
  }
104
145
  const note = noteParts.join(" ");
105
146
  const summary = plan.summary_for_user?.trimEnd() ?? "";
@@ -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
  };
@@ -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;
@@ -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,
@@ -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;