@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.
- 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 +297 -53
- package/dist/chat/gemini-planner.d.ts +2 -0
- package/dist/chat/gemini-planner.js +2 -1
- package/dist/chat/hallucination-validator.d.ts +6 -0
- package/dist/chat/hallucination-validator.js +49 -8
- 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/translation-chunking.d.ts +124 -0
- package/dist/chat/translation-chunking.js +371 -0
- package/dist/checks/field-walk.d.ts +25 -0
- package/dist/checks/field-walk.js +152 -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 +375 -0
- package/dist/checks/run-checks.d.ts +32 -0
- package/dist/checks/run-checks.js +152 -0
- package/dist/checks/session-runner.d.ts +19 -0
- package/dist/checks/session-runner.js +95 -0
- package/dist/checks/types.d.ts +65 -0
- package/dist/checks/types.js +1 -0
- package/dist/cms/adapter.d.ts +1 -0
- 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 +85 -9
- 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 +1 -1
- 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 +4 -4
- 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 +7 -0
- package/dist/index.js +27 -0
- package/dist/nlp/deterministic-planner-context.d.ts +16 -0
- package/dist/nlp/deterministic-planner-context.js +33 -7
- package/dist/nlp/deterministic-planner-suggestions.js +1 -1
- package/dist/nlp/plan-normalizer.js +193 -56
- 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-target-registry.js +1 -1
- package/dist/publish/publish-target.d.ts +1 -1
- package/dist/state/session-state.js +8 -1
- 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({
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
};
|
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,
|
|
@@ -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;
|