@avocadostudio-ai/orchestrator-core 0.1.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/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- package/package.json +78 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { allowedBlockTypes, editPlanSchema } from "@avocadostudio-ai/shared";
|
|
3
|
+
import { buildIntentParserSystemPrompt, buildPlannerSystemPromptSegments } from "./prompts.js";
|
|
4
|
+
import { GENERATING_IMAGE_PLACEHOLDER, SEARCHING_IMAGE_PLACEHOLDER } from "./chat-pipeline.js";
|
|
5
|
+
import { extractAudienceTarget, fetchImageAsBase64, resolveAttachmentsForLlm, intentSchema } from "../nlp/deterministic-planner.js";
|
|
6
|
+
import { isBatchAddRequest, isBatchRemoveRequest, isBatchReorderRequest, isPageWideRewriteRequest, isDuplicateAndModifyRequest, requestsPlanFirst } from "../nlp/intent-detection.js";
|
|
7
|
+
import { extractJsonObject, normalizePlanCandidate, repairAndParseJsonWithMeta } from "../nlp/plan-normalizer.js";
|
|
8
|
+
import { buildPlannerSchemaContext, extractUpdatePropsFieldDraftsFromPlanBuffer, extractOpsFromPlanBuffer, extractSummaryFromPlanBuffer, isChatStrictPrimaryOpMode, isPageWideTranslationRequest } from "./planner.js";
|
|
9
|
+
import { buildEditPlanJsonSchema, intentJsonSchema } from "./plan-json-schema.js";
|
|
10
|
+
import { extractUsage, ZERO_USAGE } from "../telemetry/usage.js";
|
|
11
|
+
import { anthropicSystemPromptWithCache, anthropicSegmentedSystemPromptWithCache, anthropicToolWithCache, ANTHROPIC_FINE_GRAINED_STREAM_HEADERS } from "./anthropic-cache.js";
|
|
12
|
+
import { executeToolCall } from "../tools/runtime.js";
|
|
13
|
+
import { DEFERRABLE_IMAGE_TOOLS } from "./chat-pipeline-shared.js";
|
|
14
|
+
// Minimum text length to treat a text-only model response as meaningful
|
|
15
|
+
// content (rather than discarding it in favor of the hardcoded fallback).
|
|
16
|
+
const MIN_MEANINGFUL_RESPONSE_LENGTH = 20;
|
|
17
|
+
const LLM_TIMEOUT_MS = 120_000;
|
|
18
|
+
/** Detect API-level errors (rate limits, auth failures, quota exhaustion) that
|
|
19
|
+
* should NOT be treated as recoverable stream parse errors. */
|
|
20
|
+
function isApiLevelError(err) {
|
|
21
|
+
if (!(err instanceof Error))
|
|
22
|
+
return false;
|
|
23
|
+
const msg = err.message.toLowerCase();
|
|
24
|
+
return (msg.includes("usage limit") ||
|
|
25
|
+
msg.includes("rate limit") ||
|
|
26
|
+
msg.includes("quota") ||
|
|
27
|
+
msg.includes("authentication") ||
|
|
28
|
+
msg.includes("unauthorized") ||
|
|
29
|
+
msg.includes("invalid.*api.*key") ||
|
|
30
|
+
msg.includes("billing") ||
|
|
31
|
+
/\b40[0-13]\b/.test(err.message));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Try JSON.parse, then repairAndParseJson. Returns parsed object or null.
|
|
35
|
+
*/
|
|
36
|
+
function tryParseOrRepair(buf, log, model) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(buf);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
try {
|
|
42
|
+
const meta = repairAndParseJsonWithMeta(buf);
|
|
43
|
+
log?.warn({ event: "anthropic_planner_json_repaired", model: model ?? "unknown", strategy: meta.strategy, mutationCount: meta.mutationCount, discardedBytes: meta.discardedBytes }, "Anthropic planner: repaired malformed tool JSON from stream buffer");
|
|
44
|
+
return meta.parsed;
|
|
45
|
+
}
|
|
46
|
+
catch (repairErr) {
|
|
47
|
+
log?.warn({
|
|
48
|
+
event: "anthropic_planner_repair_failed",
|
|
49
|
+
model: model ?? "unknown",
|
|
50
|
+
repairError: repairErr instanceof Error ? repairErr.message : String(repairErr),
|
|
51
|
+
}, "Anthropic planner: repairAndParseJson failed");
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Singleton Anthropic client — reuses HTTP/2 connection pool across requests.
|
|
58
|
+
// Lazily initialized on first use so module-level import doesn't throw if
|
|
59
|
+
// ANTHROPIC_API_KEY isn't set yet.
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
let _anthropicSingleton = null;
|
|
62
|
+
function getAnthropicClient() {
|
|
63
|
+
if (!_anthropicSingleton) {
|
|
64
|
+
_anthropicSingleton = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
65
|
+
}
|
|
66
|
+
return _anthropicSingleton;
|
|
67
|
+
}
|
|
68
|
+
/** Reset the singleton (useful for tests that swap API keys). */
|
|
69
|
+
export function resetAnthropicClient() {
|
|
70
|
+
_anthropicSingleton = null;
|
|
71
|
+
}
|
|
72
|
+
export async function parseIntentWithAnthropic(args) {
|
|
73
|
+
const client = getAnthropicClient();
|
|
74
|
+
const system = buildIntentParserSystemPrompt();
|
|
75
|
+
const user = {
|
|
76
|
+
request: args.message,
|
|
77
|
+
slug: args.slug,
|
|
78
|
+
activeBlockId: args.activeBlockId ?? null,
|
|
79
|
+
activeBlockType: args.activeBlockType ?? null,
|
|
80
|
+
activeEditablePath: args.activeEditablePath ?? null,
|
|
81
|
+
availableBlockTypes: allowedBlockTypes,
|
|
82
|
+
blocks: args.currentPage.blocks.map((b) => ({ id: b.id, type: b.type, props: Object.keys(b.props) }))
|
|
83
|
+
};
|
|
84
|
+
const response = await client.messages.create({
|
|
85
|
+
model: args.model,
|
|
86
|
+
max_tokens: 2048,
|
|
87
|
+
system: anthropicSystemPromptWithCache(system),
|
|
88
|
+
output_config: {
|
|
89
|
+
format: { type: "json_schema", schema: intentJsonSchema }
|
|
90
|
+
},
|
|
91
|
+
messages: [
|
|
92
|
+
{ role: "user", content: JSON.stringify(user) }
|
|
93
|
+
],
|
|
94
|
+
}, { signal: AbortSignal.timeout(LLM_TIMEOUT_MS) });
|
|
95
|
+
if (response.stop_reason === "max_tokens") {
|
|
96
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
97
|
+
const raw = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
98
|
+
args.log?.warn({
|
|
99
|
+
event: "anthropic_intent_truncated",
|
|
100
|
+
model: args.model,
|
|
101
|
+
stopReason: response.stop_reason,
|
|
102
|
+
rawPreview: raw.slice(0, 500)
|
|
103
|
+
}, "Anthropic intent parser: response truncated (max_tokens)");
|
|
104
|
+
throw new Error("Intent parser response was truncated (max_tokens reached)");
|
|
105
|
+
}
|
|
106
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
107
|
+
const raw = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
108
|
+
if (!raw.trim()) {
|
|
109
|
+
args.log?.warn({
|
|
110
|
+
event: "anthropic_intent_no_json",
|
|
111
|
+
model: args.model,
|
|
112
|
+
stopReason: response.stop_reason ?? "unknown",
|
|
113
|
+
rawPreview: raw.slice(0, 500),
|
|
114
|
+
contentBlockTypes: response.content.map((b) => b.type)
|
|
115
|
+
}, "Anthropic intent parser: model returned empty response");
|
|
116
|
+
throw new Error("Intent parser did not return JSON");
|
|
117
|
+
}
|
|
118
|
+
// output_config guarantees valid JSON matching our schema — parse directly
|
|
119
|
+
const normalized = JSON.parse(raw);
|
|
120
|
+
// Nulls from the schema become undefined for Zod optional fields
|
|
121
|
+
for (const key of Object.keys(normalized)) {
|
|
122
|
+
if (normalized[key] === null)
|
|
123
|
+
delete normalized[key];
|
|
124
|
+
}
|
|
125
|
+
const parsed = intentSchema.safeParse(normalized);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
const issue = parsed.error.issues[0];
|
|
128
|
+
const detail = issue?.message ?? "Invalid intent parser output";
|
|
129
|
+
const at = issue?.path?.length ? ` at ${issue.path.join(".")}` : "";
|
|
130
|
+
throw new Error(`${detail}${at}`);
|
|
131
|
+
}
|
|
132
|
+
return parsed.data;
|
|
133
|
+
}
|
|
134
|
+
function sumTokenUsage(a, b) {
|
|
135
|
+
return {
|
|
136
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
137
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
138
|
+
totalTokens: a.totalTokens + b.totalTokens,
|
|
139
|
+
...(a.cacheCreationInputTokens !== undefined || b.cacheCreationInputTokens !== undefined
|
|
140
|
+
? { cacheCreationInputTokens: (a.cacheCreationInputTokens ?? 0) + (b.cacheCreationInputTokens ?? 0) }
|
|
141
|
+
: {}),
|
|
142
|
+
...(a.cacheReadInputTokens !== undefined || b.cacheReadInputTokens !== undefined
|
|
143
|
+
? { cacheReadInputTokens: (a.cacheReadInputTokens ?? 0) + (b.cacheReadInputTokens ?? 0) }
|
|
144
|
+
: {})
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function asToolUseBlock(block) {
|
|
148
|
+
if (block.type !== "tool_use")
|
|
149
|
+
return null;
|
|
150
|
+
return block;
|
|
151
|
+
}
|
|
152
|
+
function toAnthropicToolAlias(name, used) {
|
|
153
|
+
let alias = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
154
|
+
if (!alias)
|
|
155
|
+
alias = "tool";
|
|
156
|
+
if (/^[0-9]/.test(alias))
|
|
157
|
+
alias = `tool_${alias}`;
|
|
158
|
+
alias = alias.slice(0, 120);
|
|
159
|
+
let unique = alias;
|
|
160
|
+
let counter = 1;
|
|
161
|
+
while (used.has(unique)) {
|
|
162
|
+
unique = `${alias}_${counter}`;
|
|
163
|
+
counter += 1;
|
|
164
|
+
}
|
|
165
|
+
used.add(unique);
|
|
166
|
+
return unique;
|
|
167
|
+
}
|
|
168
|
+
export async function generatePlanWithAnthropic(args) {
|
|
169
|
+
const client = args.client ?? getAnthropicClient();
|
|
170
|
+
const effectiveSignal = args.signal
|
|
171
|
+
? AbortSignal.any([args.signal, AbortSignal.timeout(LLM_TIMEOUT_MS)])
|
|
172
|
+
: AbortSignal.timeout(LLM_TIMEOUT_MS);
|
|
173
|
+
const effectiveBlockTypes = args.componentsManifest ? args.componentsManifest.blocks.map(c => c.type) : allowedBlockTypes;
|
|
174
|
+
const batchOverride = isBatchAddRequest(args.message) || isBatchRemoveRequest(args.message) || isBatchReorderRequest(args.message) || isPageWideRewriteRequest(args.message) || isDuplicateAndModifyRequest(args.message);
|
|
175
|
+
const pageWideRewrite = isPageWideRewriteRequest(args.message);
|
|
176
|
+
const planFirstMode = requestsPlanFirst(args.message);
|
|
177
|
+
const pageWideTranslation = isPageWideTranslationRequest(args.message);
|
|
178
|
+
const chatStrictPrimaryOpMode = isChatStrictPrimaryOpMode() && !batchOverride && !pageWideTranslation;
|
|
179
|
+
const selectedBlockId = String(args.contextPack.selected.blockId ?? "");
|
|
180
|
+
const audienceHint = extractAudienceTarget(args.message);
|
|
181
|
+
const explicitOtherReference = selectedBlockId.length > 0 &&
|
|
182
|
+
Array.isArray(args.contextPack.resolvedReferences.mentionedBlocks) &&
|
|
183
|
+
args.contextPack.resolvedReferences.mentionedBlocks.some((entry) => entry && typeof entry === "object" && "id" in entry && entry.id !== selectedBlockId);
|
|
184
|
+
const systemSegments = buildPlannerSystemPromptSegments({
|
|
185
|
+
provider: "anthropic",
|
|
186
|
+
lightweight: !!args.lightweight,
|
|
187
|
+
selectedBlockId,
|
|
188
|
+
explicitOtherReference: !!explicitOtherReference,
|
|
189
|
+
chatStrictPrimaryOpMode,
|
|
190
|
+
pageWideTranslation,
|
|
191
|
+
pageWideRewrite,
|
|
192
|
+
planFirstMode,
|
|
193
|
+
effectiveBlockTypes,
|
|
194
|
+
siteContextBlock: args.siteContextBlock,
|
|
195
|
+
imageUrlForVision: args.contextPack.selected?.imageUrlForVision,
|
|
196
|
+
editablePath: args.contextPack.selected?.editablePath,
|
|
197
|
+
blockId: args.contextPack.selected?.blockId,
|
|
198
|
+
locale: args.locale,
|
|
199
|
+
imageSourceChoiceOpen: args.imageSourceChoiceOpen,
|
|
200
|
+
attachmentCount: args.contextPack.attachments?.length,
|
|
201
|
+
});
|
|
202
|
+
// Stable prefix carries cache_control; dynamic tail is sent as a second
|
|
203
|
+
// text block without it, so per-request flags don't bust the cached prefix.
|
|
204
|
+
const cachedSystem = anthropicSegmentedSystemPromptWithCache(systemSegments);
|
|
205
|
+
const lowerMsg = args.message.toLowerCase();
|
|
206
|
+
const includeContracts = !args.lightweight && (batchOverride ||
|
|
207
|
+
pageWideTranslation ||
|
|
208
|
+
/\b(create|add|insert|build|generate)\b/.test(lowerMsg) ||
|
|
209
|
+
/\b(seo|meta|metadata|og\s*image|open\s*graph|description|structured\s*data|schema\.org)\b/.test(lowerMsg) ||
|
|
210
|
+
/\d{2,3}\s*char/i.test(args.message) ||
|
|
211
|
+
// Multi-field updates need block contracts to know valid prop names
|
|
212
|
+
(lowerMsg.match(/['''"""\u201C\u201D\u2018\u2019]/g)?.length ?? 0) >= 4);
|
|
213
|
+
const schemaContext = args.lightweight
|
|
214
|
+
? {
|
|
215
|
+
payload: {},
|
|
216
|
+
meta: {
|
|
217
|
+
contractMode: "minimal",
|
|
218
|
+
contractBytes: 0,
|
|
219
|
+
contractBlockCount: 0,
|
|
220
|
+
targetBlockTypes: [],
|
|
221
|
+
strictJsonEnabled: false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
: buildPlannerSchemaContext({
|
|
225
|
+
message: args.message,
|
|
226
|
+
contextPack: args.contextPack,
|
|
227
|
+
batchOverride,
|
|
228
|
+
pageWideTranslation,
|
|
229
|
+
legacyIncludeContracts: includeContracts,
|
|
230
|
+
forceFullContracts: args.forceFullSchemaContracts,
|
|
231
|
+
componentsManifest: args.componentsManifest
|
|
232
|
+
});
|
|
233
|
+
const user = {
|
|
234
|
+
request: args.message,
|
|
235
|
+
audienceHint: audienceHint ?? null,
|
|
236
|
+
slug: args.slug,
|
|
237
|
+
contextPack: args.contextPack,
|
|
238
|
+
...schemaContext.payload,
|
|
239
|
+
feedback: args.feedback ?? null
|
|
240
|
+
};
|
|
241
|
+
const imageUrlForVision = typeof args.contextPack.selected?.imageUrlForVision === "string"
|
|
242
|
+
? args.contextPack.selected.imageUrlForVision
|
|
243
|
+
: null;
|
|
244
|
+
const imageBase64 = imageUrlForVision ? await fetchImageAsBase64(imageUrlForVision) : null;
|
|
245
|
+
// User-attached files (images + PDFs) become native content blocks: images as
|
|
246
|
+
// `image` blocks, PDFs as `document` blocks (Claude reads text + per-page visuals).
|
|
247
|
+
const resolvedAttachments = await resolveAttachmentsForLlm(args.contextPack.attachments);
|
|
248
|
+
const attachmentBlocks = resolvedAttachments.map((attachment) => attachment.kind === "pdf"
|
|
249
|
+
? { type: "document", source: { type: "base64", media_type: "application/pdf", data: attachment.base64 } }
|
|
250
|
+
: { type: "image", source: { type: "base64", media_type: attachment.mediaType, data: attachment.base64 } });
|
|
251
|
+
const visionBlocks = imageUrlForVision
|
|
252
|
+
? [
|
|
253
|
+
imageBase64
|
|
254
|
+
? { type: "image", source: { type: "base64", media_type: imageBase64.mediaType, data: imageBase64.base64 } }
|
|
255
|
+
: { type: "image", source: { type: "url", url: imageUrlForVision } }
|
|
256
|
+
]
|
|
257
|
+
: [];
|
|
258
|
+
const userContent = visionBlocks.length > 0 || attachmentBlocks.length > 0
|
|
259
|
+
? [...visionBlocks, ...attachmentBlocks, { type: "text", text: JSON.stringify(user) }]
|
|
260
|
+
: JSON.stringify(user);
|
|
261
|
+
// Anthropic rejects the WHOLE request ("text content blocks must contain non-whitespace
|
|
262
|
+
// text") if any message carries an empty/whitespace text block — e.g. a blank chat turn
|
|
263
|
+
// that slipped into history. Drop those so one stray turn can't fail the entire plan.
|
|
264
|
+
const historyMessages = (args.history ?? [])
|
|
265
|
+
.filter((h) => typeof h.content === "string" && h.content.trim().length > 0)
|
|
266
|
+
.map((h) => ({
|
|
267
|
+
role: h.role,
|
|
268
|
+
content: h.content
|
|
269
|
+
}));
|
|
270
|
+
const submitPlanToolDef = {
|
|
271
|
+
name: "submit_edit_plan",
|
|
272
|
+
description: "Submit the structured EditPlan JSON.",
|
|
273
|
+
input_schema: buildEditPlanJsonSchema({ planFirstMode }),
|
|
274
|
+
eager_input_streaming: true
|
|
275
|
+
};
|
|
276
|
+
const runtimeToolNameByAlias = new Map();
|
|
277
|
+
const usedAliases = new Set(["submit_edit_plan"]);
|
|
278
|
+
const runtimeTools = args.toolRuntime
|
|
279
|
+
? args.toolRuntime.registry.listEnabled().map((entry) => {
|
|
280
|
+
const alias = toAnthropicToolAlias(entry.manifest.name, usedAliases);
|
|
281
|
+
runtimeToolNameByAlias.set(alias, entry.manifest.name);
|
|
282
|
+
return {
|
|
283
|
+
name: alias,
|
|
284
|
+
description: entry.manifest.description,
|
|
285
|
+
input_schema: entry.manifest.inputSchema
|
|
286
|
+
};
|
|
287
|
+
})
|
|
288
|
+
: [];
|
|
289
|
+
// Anthropic allows max 4 cache_control breakpoints. Cache the *first* tool
|
|
290
|
+
// (submitPlanToolDef) — it carries the large editPlanJsonSchema and is
|
|
291
|
+
// present on every request. Caching the first tool means the cached prefix
|
|
292
|
+
// stays a hit even when runtime tools (gdrive, unsplash, image.generate)
|
|
293
|
+
// come and go between requests.
|
|
294
|
+
const toolDefs = [
|
|
295
|
+
anthropicToolWithCache(submitPlanToolDef),
|
|
296
|
+
...runtimeTools,
|
|
297
|
+
];
|
|
298
|
+
let parsed;
|
|
299
|
+
let usage = { ...ZERO_USAGE };
|
|
300
|
+
const deferredNativeImageCalls = [];
|
|
301
|
+
let streamedOpsCount = 0;
|
|
302
|
+
let lastSummaryLen = 0;
|
|
303
|
+
let emittedChangeLogCount = 0;
|
|
304
|
+
const emittedFieldDraftByKey = new Map();
|
|
305
|
+
const maxToolTurns = 6;
|
|
306
|
+
// Anthropic adaptive thinking — added to every messages.create/stream call when
|
|
307
|
+
// args.thinking is set. Adaptive thinking + `output_config.effort` is the current
|
|
308
|
+
// API; the legacy `budget_tokens` shape is rejected (400) on Opus 4.8 / Sonnet 5.
|
|
309
|
+
// `display: "summarized"` keeps streamed thinking tokens visible (the default is
|
|
310
|
+
// "omitted", which streams empty thinking blocks and would blank the reasoning UI).
|
|
311
|
+
// Plan-first review mode disables thinking: thinking forces the tool_choice="auto"
|
|
312
|
+
// path, where the model can emit free-form prose instead of calling
|
|
313
|
+
// submit_edit_plan — and a text-only response is wrapped as needs_clarification
|
|
314
|
+
// (see fallback below). With thinking OFF we route to constrained decoding
|
|
315
|
+
// (output_config.format) / forced tool_choice, both of which honor the narrowed
|
|
316
|
+
// intent enum and guarantee a reviewable edit_plan.
|
|
317
|
+
const thinkingParam = !planFirstMode && args.thinking
|
|
318
|
+
? { type: "adaptive", display: "summarized" }
|
|
319
|
+
: undefined;
|
|
320
|
+
const effortParam = thinkingParam ? args.thinking.effort : undefined;
|
|
321
|
+
// Adaptive thinking counts thinking tokens against max_tokens; give headroom so
|
|
322
|
+
// the actual response isn't truncated. Effort (not max_tokens) governs spend.
|
|
323
|
+
const plannerMaxTokens = thinkingParam ? 16000 : 8192;
|
|
324
|
+
// Collapsed request params spread into every tool-loop create/stream call.
|
|
325
|
+
// effort rides inside output_config alongside thinking; only the constrained-
|
|
326
|
+
// decoding fallback (thinking OFF) sets output_config.format instead.
|
|
327
|
+
const thinkingRequestParams = thinkingParam
|
|
328
|
+
? { thinking: thinkingParam, output_config: { effort: effortParam } }
|
|
329
|
+
: {};
|
|
330
|
+
// Plan-first review mode forces the submit_edit_plan tool so the model cannot
|
|
331
|
+
// emit free-form prose (which the text-only fallback wraps as
|
|
332
|
+
// needs_clarification). Combined with the narrowed intent enum, this
|
|
333
|
+
// guarantees a reviewable edit_plan. Safe here because planFirstMode also
|
|
334
|
+
// disables thinking above, and forced tool_choice is incompatible with thinking.
|
|
335
|
+
const submitToolChoice = planFirstMode
|
|
336
|
+
? { type: "tool", name: "submit_edit_plan" }
|
|
337
|
+
: { type: "auto" };
|
|
338
|
+
// Track thinking lifecycle for callbacks (Path 1 tool-loop and Path 2 stream)
|
|
339
|
+
let thinkingStartedAt = null;
|
|
340
|
+
const emitThinkingStart = () => {
|
|
341
|
+
if (!args.onThinking || thinkingStartedAt !== null)
|
|
342
|
+
return;
|
|
343
|
+
thinkingStartedAt = Date.now();
|
|
344
|
+
args.onThinking({ type: "start" });
|
|
345
|
+
};
|
|
346
|
+
const emitThinkingToken = (text) => {
|
|
347
|
+
if (!args.onThinking || !text)
|
|
348
|
+
return;
|
|
349
|
+
emitThinkingStart();
|
|
350
|
+
args.onThinking({ type: "token", text });
|
|
351
|
+
};
|
|
352
|
+
const emitThinkingEnd = () => {
|
|
353
|
+
if (!args.onThinking || thinkingStartedAt === null)
|
|
354
|
+
return;
|
|
355
|
+
const durationMs = Date.now() - thinkingStartedAt;
|
|
356
|
+
thinkingStartedAt = null;
|
|
357
|
+
args.onThinking({ type: "end", durationMs });
|
|
358
|
+
};
|
|
359
|
+
const emitProgressFromToolJson = (toolJsonBuf) => {
|
|
360
|
+
if (args.onFieldDraft) {
|
|
361
|
+
const fieldDrafts = extractUpdatePropsFieldDraftsFromPlanBuffer(toolJsonBuf);
|
|
362
|
+
for (const draft of fieldDrafts) {
|
|
363
|
+
const key = `${draft.opIndex}:${draft.blockId}:${draft.editablePath}`;
|
|
364
|
+
const prev = emittedFieldDraftByKey.get(key);
|
|
365
|
+
if (prev === draft.value)
|
|
366
|
+
continue;
|
|
367
|
+
emittedFieldDraftByKey.set(key, draft.value);
|
|
368
|
+
args.onFieldDraft({ blockId: draft.blockId, editablePath: draft.editablePath, value: draft.value, ...(draft.pageSlug ? { pageSlug: draft.pageSlug } : {}) });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (args.onSummaryChunk || args.onChangeLogEntry) {
|
|
372
|
+
const extracted = extractSummaryFromPlanBuffer(toolJsonBuf);
|
|
373
|
+
if (extracted.summary && extracted.summary.length > lastSummaryLen) {
|
|
374
|
+
args.onSummaryChunk?.(extracted.summary.slice(lastSummaryLen));
|
|
375
|
+
lastSummaryLen = extracted.summary.length;
|
|
376
|
+
}
|
|
377
|
+
for (let i = emittedChangeLogCount; i < extracted.changeLog.length; i++) {
|
|
378
|
+
args.onChangeLogEntry?.(extracted.changeLog[i]);
|
|
379
|
+
}
|
|
380
|
+
emittedChangeLogCount = extracted.changeLog.length;
|
|
381
|
+
}
|
|
382
|
+
if (args.onPlannedOp) {
|
|
383
|
+
const next = extractOpsFromPlanBuffer(toolJsonBuf, streamedOpsCount);
|
|
384
|
+
streamedOpsCount = next.nextEmittedCount;
|
|
385
|
+
for (let idx = 0; idx < next.newOps.length; idx += 1) {
|
|
386
|
+
args.onPlannedOp(next.newOps[idx], streamedOpsCount - next.newOps.length + idx + 1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
if (runtimeTools.length > 0) {
|
|
391
|
+
const loopMessages = [
|
|
392
|
+
...historyMessages,
|
|
393
|
+
{ role: "user", content: userContent }
|
|
394
|
+
];
|
|
395
|
+
for (let turn = 0; turn < maxToolTurns; turn += 1) {
|
|
396
|
+
let response;
|
|
397
|
+
let emittedTextDeltas = false;
|
|
398
|
+
if (client.messages.stream) {
|
|
399
|
+
const stream = client.messages.stream({
|
|
400
|
+
model: args.model,
|
|
401
|
+
max_tokens: plannerMaxTokens,
|
|
402
|
+
system: cachedSystem,
|
|
403
|
+
tools: toolDefs,
|
|
404
|
+
tool_choice: submitToolChoice,
|
|
405
|
+
messages: loopMessages,
|
|
406
|
+
...thinkingRequestParams
|
|
407
|
+
}, {
|
|
408
|
+
headers: ANTHROPIC_FINE_GRAINED_STREAM_HEADERS,
|
|
409
|
+
signal: effectiveSignal,
|
|
410
|
+
});
|
|
411
|
+
const toolNameByIndex = new Map();
|
|
412
|
+
const submitToolJsonByIndex = new Map();
|
|
413
|
+
const thinkingBlockIndexes = new Set();
|
|
414
|
+
let path1TextBuf = "";
|
|
415
|
+
// Wrap stream iteration — SDK may throw on message_stop if tool JSON is malformed
|
|
416
|
+
let path1StreamError;
|
|
417
|
+
try {
|
|
418
|
+
for await (const event of stream) {
|
|
419
|
+
if (event.type === "content_block_start") {
|
|
420
|
+
const idx = typeof event.index === "number" ? event.index : -1;
|
|
421
|
+
if (idx >= 0 && event.content_block?.type === "tool_use" && typeof event.content_block.name === "string") {
|
|
422
|
+
toolNameByIndex.set(idx, event.content_block.name);
|
|
423
|
+
}
|
|
424
|
+
if (idx >= 0 && event.content_block?.type === "thinking") {
|
|
425
|
+
thinkingBlockIndexes.add(idx);
|
|
426
|
+
emitThinkingStart();
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (event.type === "content_block_stop") {
|
|
431
|
+
const idx = typeof event.index === "number" ? event.index : -1;
|
|
432
|
+
if (idx >= 0 && thinkingBlockIndexes.has(idx)) {
|
|
433
|
+
thinkingBlockIndexes.delete(idx);
|
|
434
|
+
if (thinkingBlockIndexes.size === 0)
|
|
435
|
+
emitThinkingEnd();
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (event.type !== "content_block_delta")
|
|
440
|
+
continue;
|
|
441
|
+
if (event.delta?.type === "thinking_delta") {
|
|
442
|
+
const thinkingText = event.delta.thinking ?? "";
|
|
443
|
+
if (thinkingText.length > 0)
|
|
444
|
+
emitThinkingToken(thinkingText);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (event.delta?.type === "text_delta") {
|
|
448
|
+
const text = event.delta.text ?? "";
|
|
449
|
+
if (text.length > 0) {
|
|
450
|
+
emittedTextDeltas = true;
|
|
451
|
+
path1TextBuf += text;
|
|
452
|
+
args.onToken?.(text);
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (event.delta?.type !== "input_json_delta")
|
|
457
|
+
continue;
|
|
458
|
+
const idx = typeof event.index === "number" ? event.index : -1;
|
|
459
|
+
if (idx < 0 || toolNameByIndex.get(idx) !== "submit_edit_plan")
|
|
460
|
+
continue;
|
|
461
|
+
const nextBuf = (submitToolJsonByIndex.get(idx) ?? "") + (event.delta.partial_json ?? "");
|
|
462
|
+
submitToolJsonByIndex.set(idx, nextBuf);
|
|
463
|
+
emitProgressFromToolJson(nextBuf);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
// Re-throw API-level errors (rate limits, auth failures, quota exhaustion)
|
|
468
|
+
// instead of treating them as recoverable stream parse errors.
|
|
469
|
+
if (isApiLevelError(err))
|
|
470
|
+
throw err;
|
|
471
|
+
path1StreamError = err;
|
|
472
|
+
}
|
|
473
|
+
// If stream threw, try to parse from accumulated buffers instead of finalMessage
|
|
474
|
+
if (path1StreamError) {
|
|
475
|
+
args.log?.warn({
|
|
476
|
+
event: "anthropic_path1_stream_loop_error",
|
|
477
|
+
model: args.model,
|
|
478
|
+
error: path1StreamError instanceof Error ? path1StreamError.message : String(path1StreamError),
|
|
479
|
+
submitToolJsonEntries: submitToolJsonByIndex.size,
|
|
480
|
+
textBufLength: path1TextBuf.length,
|
|
481
|
+
}, "Anthropic Path 1: stream loop threw (SDK JSON parse) — will attempt buffer repair");
|
|
482
|
+
// Find the submit_edit_plan buffer and attempt repair
|
|
483
|
+
for (const [idx, buf] of submitToolJsonByIndex) {
|
|
484
|
+
if (toolNameByIndex.get(idx) === "submit_edit_plan" && buf.length > 0) {
|
|
485
|
+
parsed = tryParseOrRepair(buf, args.log, args.model) ?? undefined;
|
|
486
|
+
if (parsed)
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (parsed)
|
|
491
|
+
break;
|
|
492
|
+
// Buffer repair failed. If we accumulated meaningful text, use it as
|
|
493
|
+
// an info response rather than falling through to the hardcoded fallback.
|
|
494
|
+
if (path1TextBuf.trim().length > MIN_MEANINGFUL_RESPONSE_LENGTH) {
|
|
495
|
+
emitThinkingEnd();
|
|
496
|
+
return {
|
|
497
|
+
plan: {
|
|
498
|
+
intent: "needs_clarification",
|
|
499
|
+
summary_for_user: path1TextBuf.trim(),
|
|
500
|
+
change_log: [],
|
|
501
|
+
ops: []
|
|
502
|
+
},
|
|
503
|
+
usage,
|
|
504
|
+
schemaContext: schemaContext.meta
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// If we couldn't parse, build a synthetic response to let the outer logic handle it
|
|
508
|
+
response = { content: [], stop_reason: "end_turn", usage: { input_tokens: 0, output_tokens: 0 } };
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
const finalMessage = await stream.finalMessage();
|
|
512
|
+
response = finalMessage;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
response = await client.messages.create({
|
|
517
|
+
model: args.model,
|
|
518
|
+
max_tokens: plannerMaxTokens,
|
|
519
|
+
system: cachedSystem,
|
|
520
|
+
tools: toolDefs,
|
|
521
|
+
tool_choice: submitToolChoice,
|
|
522
|
+
messages: loopMessages,
|
|
523
|
+
...thinkingRequestParams
|
|
524
|
+
}, { signal: effectiveSignal });
|
|
525
|
+
}
|
|
526
|
+
usage = sumTokenUsage(usage, extractUsage(response));
|
|
527
|
+
if (response.stop_reason === "max_tokens") {
|
|
528
|
+
args.log?.warn({
|
|
529
|
+
event: "anthropic_planner_truncated",
|
|
530
|
+
model: args.model,
|
|
531
|
+
stopReason: "max_tokens",
|
|
532
|
+
turn,
|
|
533
|
+
contentBlockTypes: response.content.map((b) => b.type)
|
|
534
|
+
}, "Anthropic planner: response truncated (max_tokens) during tool loop");
|
|
535
|
+
throw new Error("Model response was truncated (max_tokens reached)");
|
|
536
|
+
}
|
|
537
|
+
if (args.onToken && !emittedTextDeltas) {
|
|
538
|
+
for (const block of response.content) {
|
|
539
|
+
if (block.type === "text" && "text" in block && typeof block.text === "string") {
|
|
540
|
+
args.onToken(block.text);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const submitToolUse = response.content
|
|
545
|
+
.map((block) => asToolUseBlock(block))
|
|
546
|
+
.find((block) => block?.name === "submit_edit_plan");
|
|
547
|
+
if (submitToolUse && submitToolUse.input && typeof submitToolUse.input === "object") {
|
|
548
|
+
parsed = submitToolUse.input;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
const runtimeToolCalls = response.content
|
|
552
|
+
.map((block) => asToolUseBlock(block))
|
|
553
|
+
.filter((block) => Boolean(block && block.name !== "submit_edit_plan"));
|
|
554
|
+
if (runtimeToolCalls.length === 0) {
|
|
555
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
556
|
+
const raw = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
557
|
+
const jsonText = extractJsonObject(raw);
|
|
558
|
+
if (jsonText) {
|
|
559
|
+
try {
|
|
560
|
+
parsed = JSON.parse(jsonText);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
try {
|
|
564
|
+
const meta = repairAndParseJsonWithMeta(jsonText);
|
|
565
|
+
parsed = meta.parsed;
|
|
566
|
+
args.log?.warn({ event: "anthropic_planner_json_repaired", model: args.model, strategy: meta.strategy, mutationCount: meta.mutationCount, discardedBytes: meta.discardedBytes }, "Anthropic planner: repaired malformed text-block JSON (non-streaming tool loop)");
|
|
567
|
+
}
|
|
568
|
+
catch { /* fall through */ }
|
|
569
|
+
}
|
|
570
|
+
if (parsed)
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
// Model responded with text only (no tool call). If we have meaningful
|
|
574
|
+
// text content, treat it as an "info" response rather than discarding it.
|
|
575
|
+
const trimmed = raw.trim();
|
|
576
|
+
if (trimmed.length > MIN_MEANINGFUL_RESPONSE_LENGTH) {
|
|
577
|
+
emitThinkingEnd();
|
|
578
|
+
return {
|
|
579
|
+
plan: {
|
|
580
|
+
intent: "needs_clarification",
|
|
581
|
+
summary_for_user: trimmed,
|
|
582
|
+
change_log: [],
|
|
583
|
+
ops: []
|
|
584
|
+
},
|
|
585
|
+
usage,
|
|
586
|
+
schemaContext: schemaContext.meta
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
emitThinkingEnd();
|
|
590
|
+
return {
|
|
591
|
+
plan: {
|
|
592
|
+
intent: "needs_clarification",
|
|
593
|
+
summary_for_user: "I'm not sure what to do with that. You can try:",
|
|
594
|
+
change_log: [],
|
|
595
|
+
ops: [],
|
|
596
|
+
suggested_next_actions: [
|
|
597
|
+
"Create a new page",
|
|
598
|
+
"Add a section to the page",
|
|
599
|
+
"Rewrite the copy",
|
|
600
|
+
"Change the images"
|
|
601
|
+
]
|
|
602
|
+
},
|
|
603
|
+
usage,
|
|
604
|
+
schemaContext: schemaContext.meta
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
loopMessages.push({ role: "assistant", content: response.content });
|
|
608
|
+
const toolResults = [];
|
|
609
|
+
for (const toolCall of runtimeToolCalls) {
|
|
610
|
+
const input = "input" in toolCall ? toolCall.input : {};
|
|
611
|
+
const runtimeToolName = runtimeToolNameByAlias.get(toolCall.name) ?? toolCall.name;
|
|
612
|
+
// Defer slow image tools — return a placeholder so text ops stream immediately
|
|
613
|
+
if (DEFERRABLE_IMAGE_TOOLS.has(runtimeToolName)) {
|
|
614
|
+
const placeholderUrl = runtimeToolName === "image.generate"
|
|
615
|
+
? GENERATING_IMAGE_PLACEHOLDER
|
|
616
|
+
: SEARCHING_IMAGE_PLACEHOLDER;
|
|
617
|
+
const placeholderData = runtimeToolName === "image.generate"
|
|
618
|
+
? { imageUrl: placeholderUrl, alt: String(input.prompt ?? "Generating image…"), width: 768, height: 512 }
|
|
619
|
+
: { items: [{ id: "placeholder", imageUrl: placeholderUrl, thumbUrl: placeholderUrl, alt: String(input.query ?? "Searching…"), author: "Placeholder", sourceUrl: "" }] };
|
|
620
|
+
deferredNativeImageCalls.push({
|
|
621
|
+
toolName: runtimeToolName,
|
|
622
|
+
input: input,
|
|
623
|
+
placeholderUrl
|
|
624
|
+
});
|
|
625
|
+
args.onToolExecution?.({
|
|
626
|
+
toolName: runtimeToolName,
|
|
627
|
+
ok: true,
|
|
628
|
+
latencyMs: 0,
|
|
629
|
+
attempts: 0,
|
|
630
|
+
traceId: args.toolCallContext?.traceId ?? "tool-call",
|
|
631
|
+
sessionId: args.toolCallContext?.sessionId ?? "dev",
|
|
632
|
+
siteId: args.toolCallContext?.siteId ?? "default",
|
|
633
|
+
plannerProvider: "anthropic",
|
|
634
|
+
deferred: true
|
|
635
|
+
});
|
|
636
|
+
toolResults.push({
|
|
637
|
+
type: "tool_result",
|
|
638
|
+
tool_use_id: toolCall.id,
|
|
639
|
+
content: JSON.stringify(placeholderData)
|
|
640
|
+
});
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const result = await executeToolCall({
|
|
644
|
+
runtime: args.toolRuntime,
|
|
645
|
+
toolName: runtimeToolName,
|
|
646
|
+
input,
|
|
647
|
+
context: {
|
|
648
|
+
siteId: args.toolCallContext?.siteId ?? "default",
|
|
649
|
+
sessionId: args.toolCallContext?.sessionId ?? "dev",
|
|
650
|
+
userId: args.toolCallContext?.userId,
|
|
651
|
+
traceId: args.toolCallContext?.traceId ?? "tool-call",
|
|
652
|
+
plannerProvider: "anthropic",
|
|
653
|
+
gdriveFolderId: args.toolCallContext?.gdriveFolderId,
|
|
654
|
+
onStatusUpdate: args.onStatusUpdate,
|
|
655
|
+
onImageProgress: args.onImageProgress
|
|
656
|
+
},
|
|
657
|
+
policy: args.toolRuntime?.defaultPolicy
|
|
658
|
+
});
|
|
659
|
+
args.onToolExecution?.({
|
|
660
|
+
toolName: runtimeToolName,
|
|
661
|
+
ok: result.ok,
|
|
662
|
+
latencyMs: result.latencyMs,
|
|
663
|
+
attempts: result.attempts,
|
|
664
|
+
errorCode: result.error?.code,
|
|
665
|
+
traceId: args.toolCallContext?.traceId ?? "tool-call",
|
|
666
|
+
sessionId: args.toolCallContext?.sessionId ?? "dev",
|
|
667
|
+
siteId: args.toolCallContext?.siteId ?? "default",
|
|
668
|
+
plannerProvider: "anthropic"
|
|
669
|
+
});
|
|
670
|
+
toolResults.push({
|
|
671
|
+
type: "tool_result",
|
|
672
|
+
tool_use_id: toolCall.id,
|
|
673
|
+
content: JSON.stringify(result.ok ? result.data : { error: result.error }),
|
|
674
|
+
...(result.ok ? {} : { is_error: true })
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
loopMessages.push({
|
|
678
|
+
role: "user",
|
|
679
|
+
content: toolResults
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else if (args.onToken) {
|
|
684
|
+
let toolJsonBuf = "";
|
|
685
|
+
let textBuf = "";
|
|
686
|
+
if (client.messages.stream) {
|
|
687
|
+
// NOTE: forced tool_choice is incompatible with extended thinking on Anthropic;
|
|
688
|
+
// when thinking is enabled we relax to `auto` so the model can think freely
|
|
689
|
+
// before emitting the tool call.
|
|
690
|
+
const stream = client.messages.stream({
|
|
691
|
+
model: args.model,
|
|
692
|
+
max_tokens: plannerMaxTokens,
|
|
693
|
+
system: cachedSystem,
|
|
694
|
+
tools: [anthropicToolWithCache(submitPlanToolDef)],
|
|
695
|
+
tool_choice: thinkingParam
|
|
696
|
+
? { type: "auto" }
|
|
697
|
+
: { type: "tool", name: "submit_edit_plan" },
|
|
698
|
+
messages: [
|
|
699
|
+
...historyMessages,
|
|
700
|
+
{ role: "user", content: userContent }
|
|
701
|
+
],
|
|
702
|
+
...thinkingRequestParams
|
|
703
|
+
}, {
|
|
704
|
+
headers: ANTHROPIC_FINE_GRAINED_STREAM_HEADERS,
|
|
705
|
+
signal: effectiveSignal,
|
|
706
|
+
});
|
|
707
|
+
// Wrap stream iteration — SDK may throw on message_stop if tool JSON is malformed
|
|
708
|
+
let streamLoopError;
|
|
709
|
+
const path2ThinkingBlockIndexes = new Set();
|
|
710
|
+
try {
|
|
711
|
+
for await (const event of stream) {
|
|
712
|
+
if (event.type === "content_block_start") {
|
|
713
|
+
const idx = typeof event.index === "number" ? event.index : -1;
|
|
714
|
+
if (idx >= 0 && event.content_block?.type === "thinking") {
|
|
715
|
+
path2ThinkingBlockIndexes.add(idx);
|
|
716
|
+
emitThinkingStart();
|
|
717
|
+
}
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (event.type === "content_block_stop") {
|
|
721
|
+
const idx = typeof event.index === "number" ? event.index : -1;
|
|
722
|
+
if (idx >= 0 && path2ThinkingBlockIndexes.has(idx)) {
|
|
723
|
+
path2ThinkingBlockIndexes.delete(idx);
|
|
724
|
+
if (path2ThinkingBlockIndexes.size === 0)
|
|
725
|
+
emitThinkingEnd();
|
|
726
|
+
}
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (event.type === "content_block_delta") {
|
|
730
|
+
if (event.delta?.type === "thinking_delta") {
|
|
731
|
+
const thinkingText = event.delta.thinking ?? "";
|
|
732
|
+
if (thinkingText.length > 0)
|
|
733
|
+
emitThinkingToken(thinkingText);
|
|
734
|
+
}
|
|
735
|
+
else if (event.delta?.type === "input_json_delta") {
|
|
736
|
+
toolJsonBuf += event.delta.partial_json ?? "";
|
|
737
|
+
emitProgressFromToolJson(toolJsonBuf);
|
|
738
|
+
}
|
|
739
|
+
else if (event.delta?.type === "text_delta") {
|
|
740
|
+
textBuf += event.delta.text ?? "";
|
|
741
|
+
args.onToken(event.delta.text ?? "");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
if (isApiLevelError(err))
|
|
748
|
+
throw err;
|
|
749
|
+
streamLoopError = err;
|
|
750
|
+
args.log?.warn({
|
|
751
|
+
event: "anthropic_path2_stream_loop_error",
|
|
752
|
+
model: args.model,
|
|
753
|
+
error: err instanceof Error ? err.message : String(err),
|
|
754
|
+
toolJsonBufLength: toolJsonBuf.length,
|
|
755
|
+
toolJsonBufPreview: toolJsonBuf.slice(0, 500),
|
|
756
|
+
}, "Anthropic Path 2: stream loop threw (SDK JSON parse) — will attempt toolJsonBuf repair");
|
|
757
|
+
}
|
|
758
|
+
// If stream completed normally, try finalMessage() for validated tool input.
|
|
759
|
+
// Skip if stream threw — finalMessage() would also fail.
|
|
760
|
+
if (!streamLoopError) {
|
|
761
|
+
let finalMessage;
|
|
762
|
+
try {
|
|
763
|
+
finalMessage = await stream.finalMessage();
|
|
764
|
+
usage = extractUsage(finalMessage);
|
|
765
|
+
const streamStopReason = finalMessage?.stop_reason;
|
|
766
|
+
if (streamStopReason === "max_tokens") {
|
|
767
|
+
args.log?.warn({
|
|
768
|
+
event: "anthropic_planner_truncated",
|
|
769
|
+
model: args.model,
|
|
770
|
+
stopReason: streamStopReason,
|
|
771
|
+
toolJsonBufLength: toolJsonBuf.length,
|
|
772
|
+
textBufLength: textBuf.length
|
|
773
|
+
}, "Anthropic planner: response truncated (max_tokens)");
|
|
774
|
+
throw new Error("Model response was truncated (max_tokens reached)");
|
|
775
|
+
}
|
|
776
|
+
const finalToolBlock = finalMessage.content
|
|
777
|
+
?.map((block) => asToolUseBlock(block))
|
|
778
|
+
.find((block) => block?.name === "submit_edit_plan");
|
|
779
|
+
if (finalToolBlock?.input && typeof finalToolBlock.input === "object") {
|
|
780
|
+
parsed = finalToolBlock.input;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
catch (finalMsgErr) {
|
|
784
|
+
if (finalMsgErr instanceof Error && finalMsgErr.message.includes("max_tokens"))
|
|
785
|
+
throw finalMsgErr;
|
|
786
|
+
args.log?.warn({
|
|
787
|
+
event: "anthropic_planner_final_message_failed",
|
|
788
|
+
model: args.model,
|
|
789
|
+
error: finalMsgErr instanceof Error ? finalMsgErr.message : String(finalMsgErr),
|
|
790
|
+
toolJsonBufLength: toolJsonBuf.length,
|
|
791
|
+
}, "Anthropic planner: finalMessage() threw — falling back to streamed buffer repair");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Fallback: streamed buffer + repair (if finalMessage didn't yield tool input)
|
|
795
|
+
if (!parsed && toolJsonBuf.length > 0) {
|
|
796
|
+
parsed = tryParseOrRepair(toolJsonBuf, args.log, args.model) ?? undefined;
|
|
797
|
+
}
|
|
798
|
+
if (!parsed && textBuf.length > 0) {
|
|
799
|
+
const jsonText = extractJsonObject(textBuf);
|
|
800
|
+
if (jsonText) {
|
|
801
|
+
try {
|
|
802
|
+
parsed = JSON.parse(jsonText);
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
try {
|
|
806
|
+
const meta = repairAndParseJsonWithMeta(jsonText);
|
|
807
|
+
parsed = meta.parsed;
|
|
808
|
+
args.log?.warn({ event: "anthropic_planner_json_repaired", model: args.model, strategy: meta.strategy, mutationCount: meta.mutationCount, discardedBytes: meta.discardedBytes }, "Anthropic planner: repaired malformed text-block JSON (streaming)");
|
|
809
|
+
}
|
|
810
|
+
catch { /* fall through to non-parsed state */ }
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
const response = await client.messages.create({
|
|
817
|
+
model: args.model,
|
|
818
|
+
max_tokens: plannerMaxTokens,
|
|
819
|
+
system: cachedSystem,
|
|
820
|
+
tools: [anthropicToolWithCache(submitPlanToolDef)],
|
|
821
|
+
tool_choice: thinkingParam
|
|
822
|
+
? { type: "auto" }
|
|
823
|
+
: { type: "tool", name: "submit_edit_plan" },
|
|
824
|
+
messages: [
|
|
825
|
+
...historyMessages,
|
|
826
|
+
{ role: "user", content: userContent }
|
|
827
|
+
],
|
|
828
|
+
...thinkingRequestParams
|
|
829
|
+
}, { signal: effectiveSignal });
|
|
830
|
+
usage = extractUsage(response);
|
|
831
|
+
if (response.stop_reason === "max_tokens") {
|
|
832
|
+
args.log?.warn({
|
|
833
|
+
event: "anthropic_planner_truncated",
|
|
834
|
+
model: args.model,
|
|
835
|
+
stopReason: response.stop_reason,
|
|
836
|
+
contentBlockTypes: response.content.map((b) => b.type)
|
|
837
|
+
}, "Anthropic planner: response truncated (max_tokens)");
|
|
838
|
+
throw new Error("Model response was truncated (max_tokens reached)");
|
|
839
|
+
}
|
|
840
|
+
const toolBlock = response.content.find((b) => b.type === "tool_use");
|
|
841
|
+
if (toolBlock && "input" in toolBlock && toolBlock.input && typeof toolBlock.input === "object") {
|
|
842
|
+
parsed = toolBlock.input;
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
846
|
+
const raw = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
847
|
+
const jsonText = extractJsonObject(raw);
|
|
848
|
+
if (jsonText) {
|
|
849
|
+
try {
|
|
850
|
+
parsed = JSON.parse(jsonText);
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
try {
|
|
854
|
+
const meta = repairAndParseJsonWithMeta(jsonText);
|
|
855
|
+
parsed = meta.parsed;
|
|
856
|
+
args.log?.warn({ event: "anthropic_planner_json_repaired", model: args.model, strategy: meta.strategy, mutationCount: meta.mutationCount, discardedBytes: meta.discardedBytes }, "Anthropic planner: repaired malformed text-block JSON (non-streaming)");
|
|
857
|
+
}
|
|
858
|
+
catch { /* fall through */ }
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
// No runtime tools — try output_config.format for constrained decoding first,
|
|
866
|
+
// then fall back to tool_choice if the model doesn't return parseable JSON
|
|
867
|
+
// (some models like Haiku 4.5 may not reliably support output_config).
|
|
868
|
+
// NOTE: output_config is incompatible with extended thinking. When thinking
|
|
869
|
+
// is requested, skip straight to the tool_choice fallback below.
|
|
870
|
+
if (!thinkingParam)
|
|
871
|
+
try {
|
|
872
|
+
const response = await client.messages.create({
|
|
873
|
+
model: args.model,
|
|
874
|
+
max_tokens: plannerMaxTokens,
|
|
875
|
+
system: cachedSystem,
|
|
876
|
+
output_config: {
|
|
877
|
+
format: { type: "json_schema", schema: buildEditPlanJsonSchema({ planFirstMode }) }
|
|
878
|
+
},
|
|
879
|
+
messages: [
|
|
880
|
+
...historyMessages,
|
|
881
|
+
{ role: "user", content: userContent }
|
|
882
|
+
],
|
|
883
|
+
}, { signal: effectiveSignal });
|
|
884
|
+
usage = extractUsage(response);
|
|
885
|
+
if (response.stop_reason === "max_tokens") {
|
|
886
|
+
args.log?.warn({
|
|
887
|
+
event: "anthropic_planner_truncated",
|
|
888
|
+
model: args.model,
|
|
889
|
+
stopReason: response.stop_reason,
|
|
890
|
+
contentBlockTypes: response.content.map((b) => b.type)
|
|
891
|
+
}, "Anthropic planner: response truncated (max_tokens)");
|
|
892
|
+
throw new Error("Model response was truncated (max_tokens reached)");
|
|
893
|
+
}
|
|
894
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
895
|
+
const raw = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
896
|
+
if (raw.trim()) {
|
|
897
|
+
try {
|
|
898
|
+
parsed = JSON.parse(raw);
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
try {
|
|
902
|
+
const meta = repairAndParseJsonWithMeta(raw);
|
|
903
|
+
parsed = meta.parsed;
|
|
904
|
+
args.log?.warn({ event: "anthropic_planner_json_repaired", model: args.model, strategy: meta.strategy, mutationCount: meta.mutationCount, discardedBytes: meta.discardedBytes }, "Anthropic planner: repaired malformed output_config JSON");
|
|
905
|
+
}
|
|
906
|
+
catch { /* fall through to tool_choice fallback below */ }
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (outputConfigErr) {
|
|
911
|
+
// output_config may not be supported by this model — log and fall through
|
|
912
|
+
// to the tool_choice fallback below (don't re-throw truncation errors).
|
|
913
|
+
if (outputConfigErr instanceof Error && outputConfigErr.message.includes("truncated"))
|
|
914
|
+
throw outputConfigErr;
|
|
915
|
+
args.log?.warn({
|
|
916
|
+
event: "anthropic_planner_output_config_failed",
|
|
917
|
+
model: args.model,
|
|
918
|
+
error: outputConfigErr instanceof Error ? outputConfigErr.message : String(outputConfigErr)
|
|
919
|
+
}, "Anthropic planner: output_config call failed, falling back to tool_choice");
|
|
920
|
+
}
|
|
921
|
+
// Fallback: if output_config didn't produce JSON, retry with tool_choice
|
|
922
|
+
if (!parsed) {
|
|
923
|
+
args.log?.warn({
|
|
924
|
+
event: "anthropic_planner_output_config_fallback",
|
|
925
|
+
model: args.model
|
|
926
|
+
}, "Anthropic planner: output_config returned no JSON, retrying with tool_choice");
|
|
927
|
+
const fallbackResponse = await client.messages.create({
|
|
928
|
+
model: args.model,
|
|
929
|
+
max_tokens: plannerMaxTokens,
|
|
930
|
+
system: cachedSystem,
|
|
931
|
+
tools: [anthropicToolWithCache(submitPlanToolDef)],
|
|
932
|
+
tool_choice: thinkingParam
|
|
933
|
+
? { type: "auto" }
|
|
934
|
+
: { type: "tool", name: "submit_edit_plan" },
|
|
935
|
+
messages: [
|
|
936
|
+
...historyMessages,
|
|
937
|
+
{ role: "user", content: userContent }
|
|
938
|
+
],
|
|
939
|
+
...thinkingRequestParams
|
|
940
|
+
}, { signal: effectiveSignal });
|
|
941
|
+
const fallbackUsage = extractUsage(fallbackResponse);
|
|
942
|
+
usage = {
|
|
943
|
+
inputTokens: usage.inputTokens + fallbackUsage.inputTokens,
|
|
944
|
+
outputTokens: usage.outputTokens + fallbackUsage.outputTokens,
|
|
945
|
+
totalTokens: usage.totalTokens + fallbackUsage.totalTokens,
|
|
946
|
+
cacheCreationInputTokens: (usage.cacheCreationInputTokens ?? 0) + (fallbackUsage.cacheCreationInputTokens ?? 0),
|
|
947
|
+
cacheReadInputTokens: (usage.cacheReadInputTokens ?? 0) + (fallbackUsage.cacheReadInputTokens ?? 0)
|
|
948
|
+
};
|
|
949
|
+
if (fallbackResponse.stop_reason === "max_tokens") {
|
|
950
|
+
throw new Error("Model response was truncated (max_tokens reached)");
|
|
951
|
+
}
|
|
952
|
+
const toolBlock = fallbackResponse.content.find((b) => b.type === "tool_use");
|
|
953
|
+
if (toolBlock && "input" in toolBlock && toolBlock.input && typeof toolBlock.input === "object") {
|
|
954
|
+
parsed = toolBlock.input;
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
const textBlock = fallbackResponse.content.find((b) => b.type === "text");
|
|
958
|
+
const raw = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
959
|
+
const jsonText = extractJsonObject(raw);
|
|
960
|
+
if (jsonText) {
|
|
961
|
+
try {
|
|
962
|
+
parsed = JSON.parse(jsonText);
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
try {
|
|
966
|
+
const meta = repairAndParseJsonWithMeta(jsonText);
|
|
967
|
+
parsed = meta.parsed;
|
|
968
|
+
}
|
|
969
|
+
catch { /* fall through */ }
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// Safety: close any dangling thinking event before exit paths
|
|
976
|
+
emitThinkingEnd();
|
|
977
|
+
if (!parsed) {
|
|
978
|
+
args.log?.warn({
|
|
979
|
+
event: "anthropic_planner_no_json",
|
|
980
|
+
model: args.model,
|
|
981
|
+
hasHistory: (args.history?.length ?? 0) > 0,
|
|
982
|
+
hasFeedback: !!args.feedback
|
|
983
|
+
}, "Anthropic planner: model did not return parseable JSON");
|
|
984
|
+
throw new Error(`Model did not return JSON (model=${args.model})`);
|
|
985
|
+
}
|
|
986
|
+
const normalized = normalizePlanCandidate(parsed, {
|
|
987
|
+
defaultSlug: args.slug,
|
|
988
|
+
currentPage: args.currentPage,
|
|
989
|
+
userMessage: args.message
|
|
990
|
+
});
|
|
991
|
+
const planResult = editPlanSchema.safeParse(normalized);
|
|
992
|
+
if (!planResult.success) {
|
|
993
|
+
const first = planResult.error.issues[0];
|
|
994
|
+
const message = first?.message ?? "Invalid model output";
|
|
995
|
+
const path = first?.path?.length ? ` at ${first.path.join(".")}` : "";
|
|
996
|
+
const sample = JSON.stringify(normalized).slice(0, 700);
|
|
997
|
+
throw new Error(`${message}${path}. Parsed sample: ${sample}`);
|
|
998
|
+
}
|
|
999
|
+
const deferredImageMeta = deferredNativeImageCalls.length > 0 ? { deferredNativeImageCalls } : {};
|
|
1000
|
+
if (chatStrictPrimaryOpMode && planResult.data.intent === "edit_plan" && planResult.data.ops.length > 1) {
|
|
1001
|
+
return {
|
|
1002
|
+
plan: {
|
|
1003
|
+
...planResult.data,
|
|
1004
|
+
ops: [planResult.data.ops[0]]
|
|
1005
|
+
},
|
|
1006
|
+
usage,
|
|
1007
|
+
schemaContext: schemaContext.meta,
|
|
1008
|
+
...deferredImageMeta
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
return { plan: planResult.data, usage, schemaContext: schemaContext.meta, ...deferredImageMeta };
|
|
1012
|
+
}
|