@caelo-cms/shared 0.10.22 → 0.10.24
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/ai-tools.d.ts +289 -273
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +342 -323
- package/dist/ai-tools.js.map +1 -1
- package/dist/auth-forms.d.ts.map +1 -1
- package/dist/auth-forms.js +4 -1
- package/dist/auth-forms.js.map +1 -1
- package/dist/base-css.d.ts +35 -0
- package/dist/base-css.d.ts.map +1 -0
- package/dist/base-css.js +40 -0
- package/dist/base-css.js.map +1 -0
- package/dist/build-page.d.ts +330 -0
- package/dist/build-page.d.ts.map +1 -0
- package/dist/build-page.js +282 -0
- package/dist/build-page.js.map +1 -0
- package/dist/content.d.ts +322 -9
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +354 -11
- package/dist/content.js.map +1 -1
- package/dist/css-gradient-scan.d.ts +14 -0
- package/dist/css-gradient-scan.d.ts.map +1 -0
- package/dist/css-gradient-scan.js +81 -0
- package/dist/css-gradient-scan.js.map +1 -0
- package/dist/css-var-scan.d.ts +56 -0
- package/dist/css-var-scan.d.ts.map +1 -0
- package/dist/css-var-scan.js +97 -0
- package/dist/css-var-scan.js.map +1 -0
- package/dist/design-draft-shell.d.ts +21 -0
- package/dist/design-draft-shell.d.ts.map +1 -0
- package/dist/design-draft-shell.js +81 -0
- package/dist/design-draft-shell.js.map +1 -0
- package/dist/design-manifest.d.ts +36 -0
- package/dist/design-manifest.d.ts.map +1 -0
- package/dist/design-manifest.js +90 -0
- package/dist/design-manifest.js.map +1 -0
- package/dist/fonts.d.ts +89 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +241 -0
- package/dist/fonts.js.map +1 -0
- package/dist/genesis-inventory.d.ts +32 -0
- package/dist/genesis-inventory.d.ts.map +1 -0
- package/dist/genesis-inventory.js +186 -0
- package/dist/genesis-inventory.js.map +1 -0
- package/dist/genesis.d.ts +102 -0
- package/dist/genesis.d.ts.map +1 -0
- package/dist/genesis.js +145 -0
- package/dist/genesis.js.map +1 -0
- package/dist/i18n.d.ts +29 -36
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +53 -128
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.d.ts +23 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +44 -0
- package/dist/interactions.js.map +1 -0
- package/dist/media.d.ts +101 -16
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +126 -15
- package/dist/media.js.map +1 -1
- package/dist/page-log.d.ts +94 -0
- package/dist/page-log.d.ts.map +1 -0
- package/dist/page-log.js +111 -0
- package/dist/page-log.js.map +1 -0
- package/dist/preview-compose.d.ts +85 -12
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +157 -67
- package/dist/preview-compose.js.map +1 -1
- package/dist/proposal-status.d.ts +40 -0
- package/dist/proposal-status.d.ts.map +1 -0
- package/dist/proposal-status.js +34 -0
- package/dist/proposal-status.js.map +1 -0
- package/dist/responsive-images.d.ts +64 -0
- package/dist/responsive-images.d.ts.map +1 -0
- package/dist/responsive-images.js +98 -0
- package/dist/responsive-images.js.map +1 -0
- package/dist/safe-keys.d.ts +9 -0
- package/dist/safe-keys.d.ts.map +1 -0
- package/dist/safe-keys.js +20 -0
- package/dist/safe-keys.js.map +1 -0
- package/dist/seo.d.ts +11 -16
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +7 -23
- package/dist/seo.js.map +1 -1
- package/dist/skills.d.ts +14 -68
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +19 -113
- package/dist/skills.js.map +1 -1
- package/dist/strip-cdata.d.ts +7 -0
- package/dist/strip-cdata.d.ts.map +1 -0
- package/dist/strip-cdata.js +48 -0
- package/dist/strip-cdata.js.map +1 -0
- package/dist/structured-sets.d.ts +6 -52
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +6 -68
- package/dist/structured-sets.js.map +1 -1
- package/dist/subagents.d.ts +105 -3
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +224 -41
- package/dist/subagents.js.map +1 -1
- package/dist/template-engine.d.ts +85 -0
- package/dist/template-engine.d.ts.map +1 -0
- package/dist/template-engine.js +403 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/theme-importers/auto-detect.d.ts +26 -0
- package/dist/theme-importers/auto-detect.d.ts.map +1 -0
- package/dist/theme-importers/auto-detect.js +42 -0
- package/dist/theme-importers/auto-detect.js.map +1 -0
- package/dist/theme-importers/css-comments.d.ts +12 -0
- package/dist/theme-importers/css-comments.d.ts.map +1 -0
- package/dist/theme-importers/css-comments.js +15 -0
- package/dist/theme-importers/css-comments.js.map +1 -0
- package/dist/theme-importers/dtcg.d.ts +46 -0
- package/dist/theme-importers/dtcg.d.ts.map +1 -0
- package/dist/theme-importers/dtcg.js +111 -0
- package/dist/theme-importers/dtcg.js.map +1 -0
- package/dist/theme-importers/loose.d.ts +3 -0
- package/dist/theme-importers/loose.d.ts.map +1 -0
- package/dist/theme-importers/loose.js +76 -0
- package/dist/theme-importers/loose.js.map +1 -0
- package/dist/theme-importers/shadcn.d.ts +24 -0
- package/dist/theme-importers/shadcn.d.ts.map +1 -0
- package/dist/theme-importers/shadcn.js +135 -0
- package/dist/theme-importers/shadcn.js.map +1 -0
- package/dist/theme-importers/style-dictionary.d.ts +17 -0
- package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
- package/dist/theme-importers/style-dictionary.js +125 -0
- package/dist/theme-importers/style-dictionary.js.map +1 -0
- package/dist/theme-importers/tailwind.d.ts +3 -0
- package/dist/theme-importers/tailwind.d.ts.map +1 -0
- package/dist/theme-importers/tailwind.js +218 -0
- package/dist/theme-importers/tailwind.js.map +1 -0
- package/dist/theme-literal-binding.d.ts +37 -0
- package/dist/theme-literal-binding.d.ts.map +1 -0
- package/dist/theme-literal-binding.js +138 -0
- package/dist/theme-literal-binding.js.map +1 -0
- package/dist/theme-normalize.d.ts +31 -0
- package/dist/theme-normalize.d.ts.map +1 -0
- package/dist/theme-normalize.js +587 -0
- package/dist/theme-normalize.js.map +1 -0
- package/dist/theme-ramp.d.ts +55 -0
- package/dist/theme-ramp.d.ts.map +1 -0
- package/dist/theme-ramp.js +149 -0
- package/dist/theme-ramp.js.map +1 -0
- package/dist/theme-render.d.ts +105 -0
- package/dist/theme-render.d.ts.map +1 -0
- package/dist/theme-render.js +441 -0
- package/dist/theme-render.js.map +1 -0
- package/dist/themes-errors.d.ts +109 -0
- package/dist/themes-errors.d.ts.map +1 -0
- package/dist/themes-errors.js +170 -0
- package/dist/themes-errors.js.map +1 -0
- package/dist/themes.d.ts +343 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +697 -0
- package/dist/themes.js.map +1 -0
- package/dist/version.d.ts +7 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +10 -2
- package/src/__tests__/redos-hardening.test.ts +160 -0
- package/src/ai-tools-add-module-modes.test.ts +106 -0
- package/src/ai-tools-position.test.ts +134 -0
- package/src/ai-tools.test.ts +81 -0
- package/src/ai-tools.ts +1105 -0
- package/src/auth-forms.ts +36 -0
- package/src/base-css.ts +42 -0
- package/src/build-page.test.ts +228 -0
- package/src/build-page.ts +319 -0
- package/src/cap-failures.ts +67 -0
- package/src/content.test.ts +170 -0
- package/src/content.ts +620 -0
- package/src/context.ts +43 -0
- package/src/css-gradient-scan.ts +88 -0
- package/src/css-var-scan.test.ts +96 -0
- package/src/css-var-scan.ts +144 -0
- package/src/derive-module-type.test.ts +80 -0
- package/src/design-draft-shell.test.ts +85 -0
- package/src/design-draft-shell.ts +109 -0
- package/src/design-manifest.ts +93 -0
- package/src/fonts.test.ts +157 -0
- package/src/fonts.ts +296 -0
- package/src/genesis-inventory.test.ts +86 -0
- package/src/genesis-inventory.ts +215 -0
- package/src/genesis-sanitize.test.ts +35 -0
- package/src/genesis.ts +158 -0
- package/src/i18n.test.ts +58 -0
- package/src/i18n.ts +91 -0
- package/src/index.test.ts +10 -0
- package/src/index.ts +59 -0
- package/src/interactions.ts +48 -0
- package/src/logger.ts +147 -0
- package/src/media.test.ts +160 -0
- package/src/media.ts +355 -0
- package/src/page-log.test.ts +163 -0
- package/src/page-log.ts +124 -0
- package/src/preview-compose.test.ts +637 -0
- package/src/preview-compose.ts +656 -0
- package/src/preview-scanner.test.ts +96 -0
- package/src/preview-scanner.ts +214 -0
- package/src/proposal-status.test.ts +69 -0
- package/src/proposal-status.ts +40 -0
- package/src/responsive-images.test.ts +104 -0
- package/src/responsive-images.ts +151 -0
- package/src/result.ts +29 -0
- package/src/safe-keys.ts +21 -0
- package/src/seo.test.ts +194 -0
- package/src/seo.ts +233 -0
- package/src/skills.ts +48 -0
- package/src/snapshots.test.ts +80 -0
- package/src/snapshots.ts +81 -0
- package/src/strip-cdata.test.ts +41 -0
- package/src/strip-cdata.ts +50 -0
- package/src/structured-sets.ts +114 -0
- package/src/subagents.test.ts +262 -0
- package/src/subagents.ts +432 -0
- package/src/template-engine.test.ts +379 -0
- package/src/template-engine.ts +520 -0
- package/src/theme-gradient.test.ts +92 -0
- package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
- package/src/theme-importers/auto-detect.ts +84 -0
- package/src/theme-importers/css-comments.ts +15 -0
- package/src/theme-importers/dtcg.ts +106 -0
- package/src/theme-importers/loose.ts +76 -0
- package/src/theme-importers/shadcn.ts +133 -0
- package/src/theme-importers/style-dictionary.ts +125 -0
- package/src/theme-importers/tailwind.ts +217 -0
- package/src/theme-literal-binding.test.ts +71 -0
- package/src/theme-literal-binding.ts +159 -0
- package/src/theme-motion.test.ts +115 -0
- package/src/theme-normalize-envelope.test.ts +43 -0
- package/src/theme-normalize-gradient.test.ts +135 -0
- package/src/theme-normalize.ts +661 -0
- package/src/theme-ramp.ts +187 -0
- package/src/theme-render-sanitize.test.ts +45 -0
- package/src/theme-render.test.ts +119 -0
- package/src/theme-render.ts +487 -0
- package/src/theme-shadow.test.ts +56 -0
- package/src/themes-errors.ts +199 -0
- package/src/themes.ts +842 -0
- package/src/version.ts +66 -0
- package/dist/translation.d.ts +0 -127
- package/dist/translation.d.ts.map +0 -1
- package/dist/translation.js +0 -208
- package/dist/translation.js.map +0 -1
package/src/subagents.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* P10.5 — Subagent invocation shapes (Zod) + result parser.
|
|
5
|
+
*
|
|
6
|
+
* A subagent is just a chat-runner turn. The `spawn_subagent` AI tool
|
|
7
|
+
* lets the parent kick off another chat-runner turn with a constrained
|
|
8
|
+
* tool set + a seed task message; the matcher inside that turn engages
|
|
9
|
+
* whichever skill the task wording matches. Same chat-runner code path,
|
|
10
|
+
* different inputs.
|
|
11
|
+
*
|
|
12
|
+
* This file holds:
|
|
13
|
+
* - subagentSpec — single-spawn input shape.
|
|
14
|
+
* - spawnSubagentToolInput — wraps one spec.
|
|
15
|
+
* - spawnSubagentsToolInput — wraps an array (parallel batch).
|
|
16
|
+
* - Return-shape variants the AI can request:
|
|
17
|
+
* verdict ({pass, issues[], suggestions[]})
|
|
18
|
+
* tree ({tree: [...], rationale})
|
|
19
|
+
* freeform ({text})
|
|
20
|
+
* rebuild ({pages[], contentNotes[], skipped[], summary})
|
|
21
|
+
* - parseSubagentResult — pulls JSON out of the subagent's final
|
|
22
|
+
* assistant message (handles ```json fences) and validates against
|
|
23
|
+
* the requested shape.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
|
|
28
|
+
const expectedReturnShape = z.enum(["verdict", "tree", "freeform", "rebuild"]);
|
|
29
|
+
export type ExpectedReturnShape = z.infer<typeof expectedReturnShape>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run #8 R2a — the SINGLE source of truth for the return-shape enum.
|
|
33
|
+
*
|
|
34
|
+
* The spawn tools' hand-written provider `inputSchema` (JSON Schema) must
|
|
35
|
+
* carry the same enum values as this Zod schema; in run #8 the live
|
|
36
|
+
* validator rejected `"rebuild"` because the runtime resolved a stale
|
|
37
|
+
* pre-#266 build of this module while the provider schema advertised the
|
|
38
|
+
* new value (issue #251 drift class, resolution flavour). Tool files
|
|
39
|
+
* derive their JSON-Schema enums from this array instead of re-typing the
|
|
40
|
+
* literals, and a unit test asserts the tool schema accepts every shape
|
|
41
|
+
* this parser knows.
|
|
42
|
+
*/
|
|
43
|
+
export const EXPECTED_RETURN_SHAPES: readonly ExpectedReturnShape[] = expectedReturnShape.options;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* issue #306 — model tier a subagent runs at. `inherit` (the default) is
|
|
47
|
+
* today's behaviour: the child reuses the parent chat's provider+model.
|
|
48
|
+
* `mid` / `small` route the child onto a cheaper model the Owner mapped
|
|
49
|
+
* in the active provider's config (`ai_providers.config.modelTiers`).
|
|
50
|
+
* The tier VOCABULARY is deliberately abstract — tool results and
|
|
51
|
+
* editor-facing text never name the underlying model (CLAUDE.md §2:
|
|
52
|
+
* provider brand never surfaces in the editor chat UI); concrete model
|
|
53
|
+
* ids appear only on Owner surfaces (security panel, cost dashboard).
|
|
54
|
+
*/
|
|
55
|
+
export const subagentModelTier = z.enum(["inherit", "mid", "small"]);
|
|
56
|
+
export type SubagentModelTier = z.infer<typeof subagentModelTier>;
|
|
57
|
+
/** Single source of truth for the tier enum (same #251 drift-guard pattern
|
|
58
|
+
* as EXPECTED_RETURN_SHAPES — tool JSON schemas derive from this array). */
|
|
59
|
+
export const SUBAGENT_MODEL_TIERS: readonly SubagentModelTier[] = subagentModelTier.options;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One subagent spec. The parent supplies role + task + optional
|
|
63
|
+
* narrowing. The handler creates the ephemeral chat session, appends
|
|
64
|
+
* the task as the seed user message, calls runChatTurn directly with
|
|
65
|
+
* `excludedToolNames=spawn_subagent,spawn_subagents` (depth cap) +
|
|
66
|
+
* `allowedToolNames` from the spec.
|
|
67
|
+
*/
|
|
68
|
+
export const subagentSpec = z
|
|
69
|
+
.object({
|
|
70
|
+
/** Owner-readable role label for the verdicts UI + the subagent_runs row. */
|
|
71
|
+
role: z.string().min(1).max(120),
|
|
72
|
+
/** The seed user message. The matcher engages skills based on this text. */
|
|
73
|
+
task: z.string().min(1).max(8000),
|
|
74
|
+
/**
|
|
75
|
+
* Optional tool-catalogue narrowing. When omitted, the subagent
|
|
76
|
+
* gets the default registry MINUS the spawn tools. When set, the
|
|
77
|
+
* subagent gets the INTERSECTION of (default minus spawn) and
|
|
78
|
+
* `allowedToolNames` — typically read-only ops for safety.
|
|
79
|
+
*/
|
|
80
|
+
allowedToolNames: z.array(z.string().min(1).max(120)).optional(),
|
|
81
|
+
/** Zod-validated return shape. Defaults to `verdict`. */
|
|
82
|
+
expectedReturnShape: expectedReturnShape.default("verdict"),
|
|
83
|
+
/**
|
|
84
|
+
* Per-spawn cost cap in microcents. OPTIONAL on purpose (issue #304):
|
|
85
|
+
* when omitted, the spawn orchestrator derives the cap from the armed
|
|
86
|
+
* run budget (#297) or falls back to SUBAGENT_CHILD_CAP_MICROCENTS.
|
|
87
|
+
* The old schema default (50M µ¢ = $0.50) sat BELOW the empirically
|
|
88
|
+
* observed 90–167M µ¢ per-child spend of migration page batches
|
|
89
|
+
* (runs #14/#15), so every child errored at the cap — a default here
|
|
90
|
+
* would make "AI omitted it" indistinguishable from "AI chose $0.50".
|
|
91
|
+
*/
|
|
92
|
+
maxCostMicrocents: z.number().int().nonnegative().optional(),
|
|
93
|
+
/**
|
|
94
|
+
* Per-spawn wall-clock timeout. Default 300s (5 min): the documented
|
|
95
|
+
* fan-out use cases are page BUILDS — a Genesis design draft (full page +
|
|
96
|
+
* image generation) and a migration per-type rebuild each legitimately run
|
|
97
|
+
* 2–4 min. The old 60s default aborted every build child mid-work (Genesis
|
|
98
|
+
* live-run 2026-07: all 3 draft children `timed_out` at ~60000ms, so the
|
|
99
|
+
* flow saved 0 drafts; the abort also skips the child's `ai_calls` write, so
|
|
100
|
+
* the roll-up read `$0.00` — the child WAS building, not idle). Quick
|
|
101
|
+
* reviewer children (qa/legal/menu/categorizer) finish well under this
|
|
102
|
+
* ceiling, so the higher default is harmless for them. The AI can still
|
|
103
|
+
* pass a smaller `timeoutMs` for a known-fast child.
|
|
104
|
+
*/
|
|
105
|
+
timeoutMs: z.number().int().min(1000).max(600_000).default(300_000),
|
|
106
|
+
/**
|
|
107
|
+
* issue #306 — model tier for this child. Default `inherit` keeps
|
|
108
|
+
* single-model behaviour byte-identical to pre-#306 (conservative
|
|
109
|
+
* default: nothing changes until a caller opts in per-spawn AND the
|
|
110
|
+
* Owner has mapped the tier). A requested-but-unmapped tier is a
|
|
111
|
+
* LOUD structured error at spawn time — never a silent downgrade to
|
|
112
|
+
* the parent's model (CLAUDE.md §2 no-fallbacks).
|
|
113
|
+
*/
|
|
114
|
+
tier: subagentModelTier.default("inherit"),
|
|
115
|
+
/**
|
|
116
|
+
* Optional active page id. When passed, the spawn handler routes
|
|
117
|
+
* it through to the runChatTurn invocation as `activePageId`,
|
|
118
|
+
* giving the subagent the same Current-page volatile chunk a
|
|
119
|
+
* normal /edit chat would see. The subagent still has to call
|
|
120
|
+
* `pages.get_with_modules` to pull module HTML.
|
|
121
|
+
*/
|
|
122
|
+
activePageId: z.string().uuid().optional(),
|
|
123
|
+
})
|
|
124
|
+
.strict();
|
|
125
|
+
export type SubagentSpec = z.infer<typeof subagentSpec>;
|
|
126
|
+
|
|
127
|
+
export const spawnSubagentToolInput = subagentSpec;
|
|
128
|
+
export type SpawnSubagentToolInput = SubagentSpec;
|
|
129
|
+
|
|
130
|
+
export const spawnSubagentsToolInput = z
|
|
131
|
+
.object({
|
|
132
|
+
// issue #304 — 32 matches the tool's advertised provider-schema
|
|
133
|
+
// maxItems (SUBAGENT_MAX_BATCH default). The previous max(8) silently
|
|
134
|
+
// rejected the very batches the provider schema invited (#251 drift
|
|
135
|
+
// class): a 14-page migration fan-out failed Zod validation at
|
|
136
|
+
// dispatch and fell back to serial building. SUBAGENT_MAX_BATCH must
|
|
137
|
+
// never be env-raised past this hard bound.
|
|
138
|
+
subagents: z.array(subagentSpec).min(1).max(32),
|
|
139
|
+
})
|
|
140
|
+
.strict();
|
|
141
|
+
export type SpawnSubagentsToolInput = z.infer<typeof spawnSubagentsToolInput>;
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------
|
|
144
|
+
// Return-shape variants the parent asks the subagent to emit
|
|
145
|
+
// ---------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
export const verdictReturnShape = z
|
|
148
|
+
.object({
|
|
149
|
+
pass: z.boolean(),
|
|
150
|
+
issues: z
|
|
151
|
+
.array(z.union([z.string().min(1).max(2000), z.record(z.string(), z.unknown())]))
|
|
152
|
+
.max(50),
|
|
153
|
+
suggestions: z.array(z.string().min(1).max(2000)).max(50).default([]),
|
|
154
|
+
})
|
|
155
|
+
.strict();
|
|
156
|
+
export type VerdictReturn = z.infer<typeof verdictReturnShape>;
|
|
157
|
+
|
|
158
|
+
export const treeReturnShape = z
|
|
159
|
+
.object({
|
|
160
|
+
tree: z.array(z.unknown()).max(500),
|
|
161
|
+
rationale: z.string().max(4000).default(""),
|
|
162
|
+
})
|
|
163
|
+
.strict();
|
|
164
|
+
export type TreeReturn = z.infer<typeof treeReturnShape>;
|
|
165
|
+
|
|
166
|
+
export const freeformReturnShape = z
|
|
167
|
+
.object({
|
|
168
|
+
text: z.string().min(1).max(40_000),
|
|
169
|
+
})
|
|
170
|
+
.strict();
|
|
171
|
+
export type FreeformReturn = z.infer<typeof freeformReturnShape>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* issue #264 — compact per-page rebuild summary for migration fan-out
|
|
175
|
+
* subagents. The orchestrator chat's context grows by THIS shape only
|
|
176
|
+
* (never by the subagent's full transcript), so it is deliberately
|
|
177
|
+
* bounded: per-page status + short notes, deliberate omissions with a
|
|
178
|
+
* reason, and a one-paragraph summary. `pageId`/`slug` are both
|
|
179
|
+
* optional because a subagent that failed before resolving a page can
|
|
180
|
+
* still report the slug it was briefed with (or vice versa).
|
|
181
|
+
*/
|
|
182
|
+
export const rebuildReturnShape = z
|
|
183
|
+
.object({
|
|
184
|
+
pages: z
|
|
185
|
+
.array(
|
|
186
|
+
z
|
|
187
|
+
.object({
|
|
188
|
+
pageId: z.string().uuid().optional(),
|
|
189
|
+
slug: z.string().min(1).max(500).optional(),
|
|
190
|
+
/**
|
|
191
|
+
* issue #306 — `needs_escalation`: the child detected the page
|
|
192
|
+
* needs something it is not equipped to BUILD (no matching
|
|
193
|
+
* module/pattern, a layout decision, unexpected source
|
|
194
|
+
* structure) and hands it back instead of improvising. The
|
|
195
|
+
* orchestrator re-dispatches exactly those pages one capability
|
|
196
|
+
* step up (see subagent-batch.ts escalation waves). The reason
|
|
197
|
+
* lives in `notes` and is REQUIRED for this status — a blind
|
|
198
|
+
* escalation would just re-run the same confusion at higher
|
|
199
|
+
* cost (enforced by the superRefine below).
|
|
200
|
+
*/
|
|
201
|
+
status: z.enum(["rebuilt", "skipped", "failed", "needs_escalation"]),
|
|
202
|
+
/** Content-completeness note: what was dropped/merged and why, or
|
|
203
|
+
* why skipped/failed — or, for `needs_escalation`, the REQUIRED
|
|
204
|
+
* reason the page needs a more capable pass. */
|
|
205
|
+
notes: z.string().max(2000).optional(),
|
|
206
|
+
})
|
|
207
|
+
.strict()
|
|
208
|
+
.superRefine((page, ctx) => {
|
|
209
|
+
if (page.status === "needs_escalation" && !page.notes?.trim()) {
|
|
210
|
+
ctx.addIssue({
|
|
211
|
+
code: "custom",
|
|
212
|
+
path: ["notes"],
|
|
213
|
+
message:
|
|
214
|
+
'status "needs_escalation" requires `notes` explaining WHAT new thing this page needs (missing pattern, layout decision, unexpected structure) — the escalated pass is briefed from it',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
.min(1)
|
|
220
|
+
.max(100),
|
|
221
|
+
/** Cross-page content-completeness observations (e.g. "source pricing table had a footnote row I folded into the caption"). */
|
|
222
|
+
contentNotes: z.array(z.string().min(1).max(2000)).max(50).default([]),
|
|
223
|
+
/** Items deliberately left out — the orchestrator relays these verbatim. */
|
|
224
|
+
skipped: z
|
|
225
|
+
.array(
|
|
226
|
+
z
|
|
227
|
+
.object({
|
|
228
|
+
item: z.string().min(1).max(500),
|
|
229
|
+
reason: z.string().min(1).max(1000),
|
|
230
|
+
})
|
|
231
|
+
.strict(),
|
|
232
|
+
)
|
|
233
|
+
.max(100)
|
|
234
|
+
.default([]),
|
|
235
|
+
summary: z.string().max(4000).default(""),
|
|
236
|
+
})
|
|
237
|
+
.strict();
|
|
238
|
+
export type RebuildReturn = z.infer<typeof rebuildReturnShape>;
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------
|
|
241
|
+
// Result parser — handles `````json {…} ````` fences + raw JSON.
|
|
242
|
+
// ---------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
function stripFences(text: string): string {
|
|
245
|
+
const trimmed = text.trim();
|
|
246
|
+
if (trimmed.startsWith("```")) {
|
|
247
|
+
const firstNl = trimmed.indexOf("\n");
|
|
248
|
+
const last = trimmed.lastIndexOf("```");
|
|
249
|
+
if (firstNl !== -1 && last > firstNl) {
|
|
250
|
+
return trimmed.slice(firstNl + 1, last).trim();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Common case: raw JSON wrapped in prose. Pull out the first {...}
|
|
254
|
+
// block by brace-balancing. If no braces, return as-is.
|
|
255
|
+
const firstBrace = trimmed.indexOf("{");
|
|
256
|
+
if (firstBrace === -1) return trimmed;
|
|
257
|
+
let depth = 0;
|
|
258
|
+
let inStr = false;
|
|
259
|
+
let escaped = false;
|
|
260
|
+
for (let i = firstBrace; i < trimmed.length; i++) {
|
|
261
|
+
const ch = trimmed[i];
|
|
262
|
+
if (escaped) {
|
|
263
|
+
escaped = false;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (inStr) {
|
|
267
|
+
if (ch === "\\") {
|
|
268
|
+
escaped = true;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (ch === '"') inStr = false;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (ch === '"') {
|
|
275
|
+
inStr = true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (ch === "{") depth += 1;
|
|
279
|
+
else if (ch === "}") {
|
|
280
|
+
depth -= 1;
|
|
281
|
+
if (depth === 0) return trimmed.slice(firstBrace, i + 1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return trimmed;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type ParseSuccess =
|
|
288
|
+
| { ok: true; shape: "verdict"; value: VerdictReturn }
|
|
289
|
+
| { ok: true; shape: "tree"; value: TreeReturn }
|
|
290
|
+
| { ok: true; shape: "freeform"; value: FreeformReturn }
|
|
291
|
+
| { ok: true; shape: "rebuild"; value: RebuildReturn };
|
|
292
|
+
|
|
293
|
+
export type ParseResult = ParseSuccess | { ok: false; error: string };
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Run #10 D2 — validate an ALREADY-PARSED value against the requested
|
|
297
|
+
* return shape. This is the structured half of the result channel: the
|
|
298
|
+
* `submit_result` tool hands the payload straight from the tool-call
|
|
299
|
+
* arguments (already JSON — no fence-stripping, no brace-balancing),
|
|
300
|
+
* so the "response is not valid JSON" failure class cannot occur.
|
|
301
|
+
* `parseSubagentResult` (final-text fallback) delegates here after its
|
|
302
|
+
* JSON extraction.
|
|
303
|
+
*
|
|
304
|
+
* For `freeform`, accepts `{text: "..."}` OR a bare string (wrapped as
|
|
305
|
+
* `{text}`) so the model can pass its prose directly.
|
|
306
|
+
*/
|
|
307
|
+
export function validateSubagentResultValue(
|
|
308
|
+
value: unknown,
|
|
309
|
+
shape: ExpectedReturnShape,
|
|
310
|
+
): ParseResult {
|
|
311
|
+
// Defense-in-depth for the structured shapes: a provider can
|
|
312
|
+
// double-encode the payload so `value` arrives as a JSON string even
|
|
313
|
+
// after the `submit_result` inputSchema fix (and the final-text
|
|
314
|
+
// fallback path never passes through normalize-args at all). The
|
|
315
|
+
// structured shapes want an object/array, so decode a JSON-looking
|
|
316
|
+
// string before parsing. `freeform` legitimately wants a bare string,
|
|
317
|
+
// so it is left untouched.
|
|
318
|
+
let coerced = value;
|
|
319
|
+
if (shape !== "freeform" && typeof coerced === "string") {
|
|
320
|
+
const trimmed = coerced.trim();
|
|
321
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
322
|
+
try {
|
|
323
|
+
coerced = JSON.parse(trimmed);
|
|
324
|
+
} catch {
|
|
325
|
+
// leave as-is; the shape parse below reports the real mismatch
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// v0.2.67 — when the schema rejects, include the actual top-level
|
|
331
|
+
// keys the subagent returned so the parent AI can tell whether the
|
|
332
|
+
// subagent ignored the schema entirely (returned freeform text under
|
|
333
|
+
// a different shape) vs. got close but mistyped a single field.
|
|
334
|
+
const observedKeys =
|
|
335
|
+
typeof coerced === "object" && coerced !== null && !Array.isArray(coerced)
|
|
336
|
+
? Object.keys(coerced as Record<string, unknown>)
|
|
337
|
+
: [];
|
|
338
|
+
const observedSummary =
|
|
339
|
+
observedKeys.length > 0
|
|
340
|
+
? ` got keys: [${observedKeys.slice(0, 8).join(", ")}]`
|
|
341
|
+
: ` got: ${typeof coerced} (${Array.isArray(coerced) ? "array" : "scalar"})`;
|
|
342
|
+
const issueSummary = (issues: readonly z.ZodIssue[]): string =>
|
|
343
|
+
issues
|
|
344
|
+
.slice(0, 3)
|
|
345
|
+
.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
346
|
+
.join("; ");
|
|
347
|
+
|
|
348
|
+
if (shape === "freeform") {
|
|
349
|
+
if (typeof value === "string") {
|
|
350
|
+
if (value.trim().length === 0) return { ok: false, error: "subagent returned empty text" };
|
|
351
|
+
return { ok: true, shape: "freeform", value: { text: value.trim() } };
|
|
352
|
+
}
|
|
353
|
+
const validated = freeformReturnShape.safeParse(value);
|
|
354
|
+
if (!validated.success) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
error: `freeform shape mismatch (expected {text: string} or a plain string):${observedSummary}; ${issueSummary(validated.error.issues)}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { ok: true, shape: "freeform", value: validated.data };
|
|
361
|
+
}
|
|
362
|
+
if (shape === "verdict") {
|
|
363
|
+
const validated = verdictReturnShape.safeParse(coerced);
|
|
364
|
+
if (!validated.success) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
error: `verdict shape mismatch (expected {pass: boolean, issues: array, suggestions?: array}):${observedSummary}; ${issueSummary(validated.error.issues)}`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return { ok: true, shape: "verdict", value: validated.data };
|
|
371
|
+
}
|
|
372
|
+
if (shape === "rebuild") {
|
|
373
|
+
const validated = rebuildReturnShape.safeParse(coerced);
|
|
374
|
+
if (!validated.success) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
error: `rebuild shape mismatch (expected {pages: [{pageId?, slug?, status: "rebuilt"|"skipped"|"failed"|"needs_escalation", notes?}], contentNotes?: string[], skipped?: [{item, reason}], summary?: string}):${observedSummary}; ${issueSummary(validated.error.issues)}`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { ok: true, shape: "rebuild", value: validated.data };
|
|
381
|
+
}
|
|
382
|
+
// tree
|
|
383
|
+
const validated = treeReturnShape.safeParse(coerced);
|
|
384
|
+
if (!validated.success) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
error: `tree shape mismatch (expected {tree: array, rationale?: string}):${observedSummary}; ${issueSummary(validated.error.issues)}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { ok: true, shape: "tree", value: validated.data };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Pull JSON out of the subagent's final assistant text and validate
|
|
395
|
+
* against the requested shape. On schema mismatch, returns
|
|
396
|
+
* `{ok: false, error}` so the caller can decide whether to retry.
|
|
397
|
+
*
|
|
398
|
+
* For `freeform`, accepts EITHER `{text: "..."}` JSON or raw text;
|
|
399
|
+
* raw text is wrapped as `{text: rawText}` so the caller always gets
|
|
400
|
+
* the same shape.
|
|
401
|
+
*
|
|
402
|
+
* Run #10 D2 — this is now the FALLBACK channel; the canonical path is
|
|
403
|
+
* the child calling `submit_result`, whose payload is validated by
|
|
404
|
+
* `validateSubagentResultValue` without any text extraction.
|
|
405
|
+
*/
|
|
406
|
+
export function parseSubagentResult(text: string, shape: ExpectedReturnShape): ParseResult {
|
|
407
|
+
if (shape === "freeform") {
|
|
408
|
+
// Try JSON first; on failure treat the whole thing as freeform text.
|
|
409
|
+
try {
|
|
410
|
+
const stripped = stripFences(text);
|
|
411
|
+
const parsed = JSON.parse(stripped) as unknown;
|
|
412
|
+
const validated = freeformReturnShape.safeParse(parsed);
|
|
413
|
+
if (validated.success) return { ok: true, shape: "freeform", value: validated.data };
|
|
414
|
+
} catch {
|
|
415
|
+
/* fall through */
|
|
416
|
+
}
|
|
417
|
+
if (text.trim().length === 0) return { ok: false, error: "subagent returned empty text" };
|
|
418
|
+
return { ok: true, shape: "freeform", value: { text: text.trim() } };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const stripped = stripFences(text);
|
|
422
|
+
let parsed: unknown;
|
|
423
|
+
try {
|
|
424
|
+
parsed = JSON.parse(stripped);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
error: `subagent response is not valid JSON: ${(e as Error).message}; first 200 chars: ${stripped.slice(0, 200)}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return validateSubagentResultValue(parsed, shape);
|
|
432
|
+
}
|