@adia-ai/a2ui-mcp 0.2.0 → 0.2.2
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/CHANGELOG.md +56 -4
- package/package.json +1 -2
- package/scripts/eval-compose-from-chunks.mjs +2 -2
- package/server.js +17 -404
- package/tools/synthesis.js +436 -0
- package/evals/compose-from-chunks-holdout.jsonl +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -9,7 +9,59 @@ zettel strategies.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
_Nothing yet._
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [0.2.2] - 2026-05-02
|
|
17
|
+
|
|
18
|
+
**Lockstep cut.** All 8 published `@adia-ai/*` packages bump 0.2.1 → 0.2.2 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `version`: `0.2.1` → `0.2.2`.
|
|
23
|
+
- `dependencies["@adia-ai/a2ui-compose"]`: `^0.2.0` (covers `0.2.2`).
|
|
24
|
+
- `dependencies["@adia-ai/a2ui-corpus"]`: `^0.2.0` (covers `0.2.2`).
|
|
25
|
+
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.2`).
|
|
26
|
+
- `dependencies["@adia-ai/a2ui-retrieval"]`: `^0.2.0` (covers `0.2.2`).
|
|
27
|
+
- `dependencies["@adia-ai/a2ui-validator"]`: `^0.2.0` (covers `0.2.2`).
|
|
28
|
+
|
|
29
|
+
### No source changes
|
|
30
|
+
|
|
31
|
+
`a2ui-mcp` source is byte-identical to `0.2.1`. The cut bumps version + tracks dep ranges only. Consumers benefit from the panel-emission improvements that landed in `@adia-ai/a2ui-compose@0.2.2` and `@adia-ai/a2ui-retrieval@0.2.2` — the MCP `compose_from_chunks` and refine paths surface them automatically.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## [0.2.1] - 2026-05-02
|
|
36
|
+
|
|
37
|
+
**Lockstep cut + `report_issue` trigger phrases + scope-drift auto-fire on canvas-drift detection.** All 8 published `@adia-ai/*` packages bump 0.2.0 → 0.2.1 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `version`: `0.2.0` → `0.2.1`.
|
|
42
|
+
- `dependencies["@adia-ai/a2ui-compose"]`: `^0.2.0` (covers `0.2.1`).
|
|
43
|
+
- `dependencies["@adia-ai/a2ui-corpus"]`: `^0.2.0` (covers `0.2.1`).
|
|
44
|
+
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.1`).
|
|
45
|
+
- `dependencies["@adia-ai/a2ui-retrieval"]`: `^0.2.0` (covers `0.2.1`).
|
|
46
|
+
- `dependencies["@adia-ai/a2ui-validator"]`: `^0.2.0` (covers `0.2.1`).
|
|
47
|
+
- `tools/synthesis.js` is the surface that gained the new behavior (auto-fire + trigger phrases below). `scripts/eval-compose-from-chunks.mjs` updated to load the holdout fixture from its new home in `@adia-ai/a2ui-corpus/evals/` (the file moved out of `evals/` here in this cut).
|
|
48
|
+
- `evals/compose-from-chunks-holdout.jsonl` removed from this package; relocated to `packages/a2ui/corpus/evals/holdout-compose-from-chunks.jsonl` to live next to the corpus it tests.
|
|
49
|
+
|
|
50
|
+
### Added — `report_issue` trigger phrases + sibling Markdown ticket (2026-05-02)
|
|
51
|
+
|
|
52
|
+
`report_issue` tool description now lists explicit trigger phrases ("file a ticket", "save the trace", "report this as a bug", "track this regression", "open a ticket for this", and 9 more) so the LLM knows when to fire it without guessing. The default trace depth flipped from `'summary'` to `'full'` when `state_id` is provided — the high-resolution Markdown ticket is now the design intent.
|
|
53
|
+
|
|
54
|
+
Tool return shape extended with `markdown_path` alongside the existing `path` (.json). The tool description instructs the LLM to surface BOTH paths to the user with a marker so a maintainer can navigate to either:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
📋 Logged ticket `{issue_id}` ({severity} · owner: {suggested_owner})
|
|
58
|
+
• Trace report: `{markdown_path}` ← human-readable
|
|
59
|
+
• Raw JSON: `{path}` ← machine-readable
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Added — `compose_from_chunks` auto-fires `scope-drift` on canvas-drift detection (2026-05-02)
|
|
63
|
+
|
|
64
|
+
When the composer's scope-drift gate (a2ui-compose) flags `result.scopeDrift.drift === true`, the MCP handler auto-fires a `scope-drift` issue with `trace: 'full'`, writing both the JSON ticket and the high-res Markdown report. Tool response also echoes `scopeDrift` so the calling agent can read the ratio + decide its next move. Closes the original MCP feedback's "next time canvas-drift happens, a ticket files itself" ask.
|
|
13
65
|
|
|
14
66
|
## [0.2.0] - 2026-05-02
|
|
15
67
|
|
|
@@ -458,12 +510,12 @@ before HTML is materialized.
|
|
|
458
510
|
|
|
459
511
|
### Added (engine internals)
|
|
460
512
|
|
|
461
|
-
- `packages/a2ui/compose/
|
|
513
|
+
- `packages/a2ui/compose/strategies/zettel/chunk-composer.js` —
|
|
462
514
|
HTML-string-level composer that walks a page chunk and substitutes
|
|
463
515
|
each `data-chunk-slot` region with the bound block-level chunks'
|
|
464
516
|
HTML. Companion `validatePlan()` checks slot names + chunk-kind
|
|
465
517
|
contracts before composition.
|
|
466
|
-
- `packages/a2ui/compose/
|
|
518
|
+
- `packages/a2ui/compose/strategies/zettel/chunk-synthesizer.js` —
|
|
467
519
|
LLM-driven mix-and-match composition; pre-searches the catalog,
|
|
468
520
|
builds a prompt with a one-shot in-context example, validates the
|
|
469
521
|
LLM's binding plan against the catalog, retries with feedback on
|
|
@@ -535,7 +587,7 @@ exposes A2UI generation tools over JSON-RPC.
|
|
|
535
587
|
Initial version at the time the monorepo was established. Contains:
|
|
536
588
|
|
|
537
589
|
- MCP server (`server.js`) exposing `generate_ui`, `search_patterns`, `lookup_component`, `resolve_composition`, `validate_schema`, and related tools.
|
|
538
|
-
- Engine plugin API via in-process `registerEngine()` (see `packages/a2ui/compose/
|
|
590
|
+
- Engine plugin API via in-process `registerEngine()` (see `packages/a2ui/compose/strategies/registry.js`).
|
|
539
591
|
- Smoke tests (`scripts/smoke-*.mjs`) for engine registration, merged generation, and end-to-end A2UI tests.
|
|
540
592
|
- Eval harnesses (`scripts/test-evals.mjs`, `scripts/test-a2ui.mjs`, `scripts/eval-fix.mjs`) for corpus regression measurement.
|
|
541
593
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/a2ui-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "AdiaUI A2UI MCP server. Exposes the compose engine over MCP with an engine selector for monolithic + zettel strategies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"server.js",
|
|
11
11
|
"tools/",
|
|
12
12
|
"scripts/",
|
|
13
|
-
"evals/",
|
|
14
13
|
"personas/",
|
|
15
14
|
"README.md",
|
|
16
15
|
"CHANGELOG.md"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* eval-compose-from-chunks.mjs — Hold-out eval for the chunk-aware
|
|
4
4
|
* synthesizer. Per `docs/specs/compose-from-chunks-eval.md`.
|
|
5
5
|
*
|
|
6
|
-
* Reads `packages/a2ui/
|
|
6
|
+
* Reads `packages/a2ui/corpus/evals/holdout-compose-from-chunks.jsonl`,
|
|
7
7
|
* runs each intent through `composeFromIntent`, and emits a per-intent
|
|
8
8
|
* + aggregate report.
|
|
9
9
|
*
|
|
@@ -52,7 +52,7 @@ import { composeFromIntent } from '../../compose/strategies/zettel/chunk-synthes
|
|
|
52
52
|
import { searchChunksAsync } from '../../corpus/scripts/chunk-library.js';
|
|
53
53
|
|
|
54
54
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
55
|
-
const HOLDOUT = path.join(REPO_ROOT, 'packages/a2ui/
|
|
55
|
+
const HOLDOUT = path.join(REPO_ROOT, 'packages/a2ui/corpus/evals/holdout-compose-from-chunks.jsonl');
|
|
56
56
|
const PASS_THRESHOLD = 80;
|
|
57
57
|
|
|
58
58
|
const args = process.argv.slice(2);
|
package/server.js
CHANGED
|
@@ -74,6 +74,16 @@ import {
|
|
|
74
74
|
searchChunks as searchGenUIChunks,
|
|
75
75
|
} from '../corpus/scripts/chunk-library.js';
|
|
76
76
|
|
|
77
|
+
// ── Inline-tool deps (transpiler / wiring / patterns / feedback) ──
|
|
78
|
+
import { transpileHTML } from '../compose/transpiler/transpiler.js';
|
|
79
|
+
import { getWiringCatalog } from '../retrieval/wiring-catalog.js';
|
|
80
|
+
import { registerPattern } from '../retrieval/pattern-library.js';
|
|
81
|
+
import { FeedbackCollector } from '../retrieval/feedback/feedback.js';
|
|
82
|
+
import { feedbackStore } from '../retrieval/feedback/feedback-store.js';
|
|
83
|
+
|
|
84
|
+
// ── Tools extracted to tools/ for modularity ──
|
|
85
|
+
import { registerSynthesisTools } from './tools/synthesis.js';
|
|
86
|
+
|
|
77
87
|
const _chunkIndex = getChunkIndex();
|
|
78
88
|
if (_chunkIndex) {
|
|
79
89
|
console.error(
|
|
@@ -279,9 +289,6 @@ server.tool(
|
|
|
279
289
|
|
|
280
290
|
// ── Transpiler & Wiring Tools ──
|
|
281
291
|
|
|
282
|
-
import { transpileHTML } from '../compose/transpiler/transpiler.js';
|
|
283
|
-
import { getWiringCatalog } from '../retrieval/wiring-catalog.js';
|
|
284
|
-
|
|
285
292
|
server.tool(
|
|
286
293
|
'convert_html',
|
|
287
294
|
'Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.',
|
|
@@ -311,9 +318,6 @@ server.tool(
|
|
|
311
318
|
|
|
312
319
|
// ── Pattern & Feedback Tools ──
|
|
313
320
|
|
|
314
|
-
import { registerPattern } from '../retrieval/pattern-library.js';
|
|
315
|
-
import { FeedbackCollector } from '../retrieval/feedback/feedback.js';
|
|
316
|
-
|
|
317
321
|
const feedbackCollector = new FeedbackCollector();
|
|
318
322
|
|
|
319
323
|
server.tool(
|
|
@@ -378,8 +382,6 @@ server.tool(
|
|
|
378
382
|
|
|
379
383
|
// ── Quality metrics tool ──
|
|
380
384
|
|
|
381
|
-
import { feedbackStore } from '../retrieval/feedback/feedback-store.js';
|
|
382
|
-
|
|
383
385
|
server.tool(
|
|
384
386
|
'get_quality_metrics',
|
|
385
387
|
'Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.',
|
|
@@ -656,405 +658,16 @@ Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
|
|
|
656
658
|
},
|
|
657
659
|
);
|
|
658
660
|
|
|
659
|
-
// ── Chunk-aware composition
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
//
|
|
661
|
+
// ── Chunk-aware composition + multi-turn refinement ──────────────────
|
|
662
|
+
// `compose_from_chunks`, `refine_composition`, `get_state`, `report_issue`
|
|
663
|
+
// share the LLM bridge + state-cache + issue-reporter + chunk-refiner
|
|
664
|
+
// stack. Extracted to tools/synthesis.js.
|
|
663
665
|
//
|
|
664
|
-
// Spec: docs/specs/genui-
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
import { composeFromIntent as composeFromChunksImpl } from '../compose/strategies/zettel/chunk-synthesizer.js';
|
|
668
|
-
import { composeFromPlan, validatePlan } from '../compose/strategies/zettel/chunk-composer.js';
|
|
669
|
-
import { createAdapter as createLLMAdapter } from '../compose/llm/llm-bridge.js';
|
|
670
|
-
|
|
671
|
-
// ── Multi-turn architecture (Phase A) ────────────────────────────────
|
|
672
|
-
// Spec: docs/specs/genui-multiturn-architecture.md (Draft v0.1.0).
|
|
673
|
-
// Plan: docs/plans/genui-multiturn-rollout-2026-04-28.md (Phase A scoped).
|
|
674
|
-
|
|
675
|
-
import {
|
|
676
|
-
getStateCache,
|
|
677
|
-
mintStateId,
|
|
678
|
-
mintNextStateId,
|
|
679
|
-
} from '../compose/strategies/zettel/state-cache.js';
|
|
680
|
-
import {
|
|
681
|
-
reportIssue as reportIssueImpl,
|
|
682
|
-
autoReport,
|
|
683
|
-
createIssueAccumulator,
|
|
684
|
-
} from '../compose/strategies/zettel/issue-reporter.js';
|
|
685
|
-
import {
|
|
686
|
-
refineFromIntent,
|
|
687
|
-
applyOps,
|
|
688
|
-
opsToA2UI,
|
|
689
|
-
validateOps,
|
|
690
|
-
} from '../compose/strategies/zettel/chunk-refiner.js';
|
|
691
|
-
|
|
692
|
-
const stateCache = getStateCache();
|
|
693
|
-
|
|
694
|
-
const ENGINE_VERSION_INFO = {
|
|
695
|
-
mcp: '0.1.0',
|
|
696
|
-
corpus: '0.0.6',
|
|
697
|
-
engine: 'zettel',
|
|
698
|
-
llm_adapter: 'anthropic',
|
|
699
|
-
model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-7',
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
server.tool(
|
|
703
|
-
'compose_from_chunks',
|
|
704
|
-
`Compose a UI page from training chunks — retrieval-first, synthesis-fallback.
|
|
705
|
-
|
|
706
|
-
Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflow:
|
|
707
|
-
1. Pure-retrieval tier: if \`search_chunks\` returns a strong direct match, return
|
|
708
|
-
that chunk's HTML immediately (no LLM call).
|
|
709
|
-
2. Synthesis tier: when retrieval is weak, the LLM picks a page-kind chunk and
|
|
710
|
-
binds block/panel chunks to its named slots. Output validated against the
|
|
711
|
-
chunk catalog (slot names exist, bound chunks exist, kinds match).
|
|
712
|
-
|
|
713
|
-
Returns the composed HTML string + a binding plan describing which chunks plug
|
|
714
|
-
where. Useful when the prompt is novel ("dashboard with KPI grid + funnel +
|
|
715
|
-
country list") and no exact chunk has all those parts together — the LLM mixes
|
|
716
|
-
and matches from the corpus.
|
|
717
|
-
|
|
718
|
-
Two-call mode also available via \`plan\` parameter — pass a pre-baked binding
|
|
719
|
-
plan to skip the LLM call and just materialize HTML.`,
|
|
720
|
-
{
|
|
721
|
-
intent: z.string().optional().describe('Natural-language description of what to build (uses LLM synthesis)'),
|
|
722
|
-
plan: z.object({
|
|
723
|
-
page: z.string(),
|
|
724
|
-
slot_bindings: z.record(z.union([z.string(), z.array(z.string())])),
|
|
725
|
-
}).optional().describe('Pre-baked binding plan (skips LLM, materializes directly)'),
|
|
726
|
-
max_attempts: z.number().int().min(1).max(5).default(2).describe('LLM retry budget for synthesis'),
|
|
727
|
-
},
|
|
728
|
-
async ({ intent, plan, max_attempts }) => {
|
|
729
|
-
if (plan) {
|
|
730
|
-
const validation = validatePlan(plan);
|
|
731
|
-
if (!validation.ok) {
|
|
732
|
-
return {
|
|
733
|
-
isError: true,
|
|
734
|
-
content: [{ type: 'text', text: JSON.stringify({ error: 'invalid plan', errors: validation.errors }, null, 2) }],
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
const result = composeFromPlan(plan);
|
|
738
|
-
const state_id = mintStateId(intent || plan.page || 'plan', 1);
|
|
739
|
-
stateCache.set(state_id, {
|
|
740
|
-
state_id,
|
|
741
|
-
intent: intent || `(plan) ${plan.page}`,
|
|
742
|
-
plan: result.plan,
|
|
743
|
-
html: result.html,
|
|
744
|
-
source: 'plan',
|
|
745
|
-
ops_history: [],
|
|
746
|
-
parent_state_id: null,
|
|
747
|
-
created_at: new Date().toISOString(),
|
|
748
|
-
});
|
|
749
|
-
return {
|
|
750
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
751
|
-
state_id,
|
|
752
|
-
html: result.html,
|
|
753
|
-
plan: result.plan,
|
|
754
|
-
warnings: result.warnings,
|
|
755
|
-
source: 'plan',
|
|
756
|
-
}, null, 2) }],
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
if (!intent) {
|
|
761
|
-
return {
|
|
762
|
-
isError: true,
|
|
763
|
-
content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or plan' }, null, 2) }],
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
try {
|
|
768
|
-
const llmAdapter = await createLLMAdapter();
|
|
769
|
-
const result = await composeFromChunksImpl({
|
|
770
|
-
intent,
|
|
771
|
-
llmAdapter,
|
|
772
|
-
maxAttempts: max_attempts,
|
|
773
|
-
});
|
|
774
|
-
const state_id = mintStateId(intent, 1);
|
|
775
|
-
stateCache.set(state_id, {
|
|
776
|
-
state_id,
|
|
777
|
-
intent,
|
|
778
|
-
plan: result.plan,
|
|
779
|
-
html: result.html,
|
|
780
|
-
source: result.source,
|
|
781
|
-
score: result.score,
|
|
782
|
-
ops_history: [],
|
|
783
|
-
parent_state_id: null,
|
|
784
|
-
warnings: result.warnings,
|
|
785
|
-
synthesis: result.synthesis,
|
|
786
|
-
created_at: new Date().toISOString(),
|
|
787
|
-
});
|
|
788
|
-
return {
|
|
789
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
790
|
-
state_id,
|
|
791
|
-
html: result.html,
|
|
792
|
-
plan: result.plan,
|
|
793
|
-
source: result.source,
|
|
794
|
-
score: result.score,
|
|
795
|
-
warnings: result.warnings,
|
|
796
|
-
synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : undefined,
|
|
797
|
-
}, null, 2) }],
|
|
798
|
-
};
|
|
799
|
-
} catch (e) {
|
|
800
|
-
return {
|
|
801
|
-
isError: true,
|
|
802
|
-
content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
},
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
// ── Multi-turn refinement tools (Phase A) ─────────────────────────────
|
|
809
|
-
// Spec: docs/specs/genui-multiturn-architecture.md §3.
|
|
810
|
-
|
|
811
|
-
server.tool(
|
|
812
|
-
'refine_composition',
|
|
813
|
-
`Refine an existing chunk-composed UI based on a natural-language intent or an explicit op-list.
|
|
814
|
-
|
|
815
|
-
Use when the user wants to modify an *existing* UI. Triggers on "change", "update", "modify", "add to", "remove from", "this", "it", "the X". Requires \`state_id\` from a prior \`compose_from_chunks\` call.
|
|
816
|
-
|
|
817
|
-
Two modes:
|
|
818
|
-
- **Intent-driven** — pass \`intent\`. Engine runs two-pass synthesis (locator pass identifies which slots to modify; modifier pass emits chunk-plan ops). Validator-driven retry on op-validation failure.
|
|
819
|
-
- **Explicit ops** — pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
|
|
820
|
-
|
|
821
|
-
Returns a new \`state_id\` (versioned chain from the parent), the A2UI op-list applied, the post-op HTML, and a delta summary. Failed ops are reported in \`ops_failed\` with reasons.
|
|
822
|
-
|
|
823
|
-
For *fresh creation* use \`compose_from_chunks\`, not this tool.`,
|
|
824
|
-
{
|
|
825
|
-
state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
|
|
826
|
-
intent: z.string().optional().describe('Natural-language description of what to change (e.g. "add a country list to page-content")'),
|
|
827
|
-
ops: z.array(z.any()).optional().describe('Pre-computed chunk-plan ops to apply directly (skips the LLM)'),
|
|
828
|
-
max_attempts: z.number().int().min(1).max(5).default(2).describe('Validator retry budget for synthesis'),
|
|
829
|
-
},
|
|
830
|
-
async ({ state_id, intent, ops, max_attempts }) => {
|
|
831
|
-
const priorState = stateCache.get(state_id);
|
|
832
|
-
if (!priorState) {
|
|
833
|
-
await autoReport(
|
|
834
|
-
'cache-miss-on-known-state',
|
|
835
|
-
{ state_id, tool: 'refine_composition' },
|
|
836
|
-
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
837
|
-
);
|
|
838
|
-
return {
|
|
839
|
-
isError: true,
|
|
840
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
841
|
-
error: 'state_id not found in cache',
|
|
842
|
-
hint: 'state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id',
|
|
843
|
-
state_id,
|
|
844
|
-
}, null, 2) }],
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (!intent && !ops) {
|
|
849
|
-
return {
|
|
850
|
-
isError: true,
|
|
851
|
-
content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or ops' }, null, 2) }],
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const issueAccumulator = createIssueAccumulator();
|
|
856
|
-
const issueCtx = { cache: stateCache, versionInfo: ENGINE_VERSION_INFO };
|
|
857
|
-
const startedAt = Date.now();
|
|
858
|
-
|
|
859
|
-
try {
|
|
860
|
-
let resolvedOps;
|
|
861
|
-
let delta_summary = '';
|
|
862
|
-
let synthesis = null;
|
|
863
|
-
let warnings = [];
|
|
864
|
-
|
|
865
|
-
if (ops && Array.isArray(ops)) {
|
|
866
|
-
// Explicit ops path — validate then apply
|
|
867
|
-
const validation = validateOps(ops, priorState);
|
|
868
|
-
if (!validation.ok) {
|
|
869
|
-
await issueAccumulator.flush(issueCtx);
|
|
870
|
-
return {
|
|
871
|
-
isError: true,
|
|
872
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
873
|
-
error: 'ops failed validation',
|
|
874
|
-
errors: validation.errors,
|
|
875
|
-
}, null, 2) }],
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
resolvedOps = ops;
|
|
879
|
-
delta_summary = `applied ${ops.length} explicit op(s)`;
|
|
880
|
-
} else {
|
|
881
|
-
// Intent path — two-pass synthesis with stub-friendly LLM bridge
|
|
882
|
-
const llmAdapter = await createLLMAdapter();
|
|
883
|
-
const refined = await refineFromIntent({
|
|
884
|
-
priorState,
|
|
885
|
-
intent,
|
|
886
|
-
llmAdapter,
|
|
887
|
-
maxAttempts: max_attempts,
|
|
888
|
-
issueAccumulator,
|
|
889
|
-
});
|
|
890
|
-
resolvedOps = refined.ops;
|
|
891
|
-
delta_summary = refined.delta_summary || '';
|
|
892
|
-
synthesis = refined.synthesis;
|
|
893
|
-
warnings = refined.warnings;
|
|
894
|
-
|
|
895
|
-
if (resolvedOps.length === 0) {
|
|
896
|
-
// Synthesizer gave up. Auto-fires already accumulated.
|
|
897
|
-
await issueAccumulator.flush(issueCtx);
|
|
898
|
-
const childId = mintNextStateId(state_id, (priorState.version || 1) + 1);
|
|
899
|
-
return {
|
|
900
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
901
|
-
state_id: childId,
|
|
902
|
-
ops_applied: [],
|
|
903
|
-
ops_failed: [],
|
|
904
|
-
delta_summary: '',
|
|
905
|
-
warnings,
|
|
906
|
-
synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted } : null,
|
|
907
|
-
html: priorState.html,
|
|
908
|
-
}, null, 2) }],
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const applied = await applyOps({ priorState, ops: resolvedOps });
|
|
914
|
-
|
|
915
|
-
if (applied.ops_failed.length > 0) {
|
|
916
|
-
issueAccumulator.add('ops-failed-after-apply', {
|
|
917
|
-
state_id,
|
|
918
|
-
tool: 'refine_composition',
|
|
919
|
-
intent,
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
const a2uiMessages = opsToA2UI(applied.ops_applied, applied.newState);
|
|
924
|
-
|
|
925
|
-
const parentVersion = priorState.version || 1;
|
|
926
|
-
const newVersion = parentVersion + 1;
|
|
927
|
-
const newStateId = mintNextStateId(state_id, newVersion);
|
|
928
|
-
|
|
929
|
-
stateCache.set(newStateId, {
|
|
930
|
-
state_id: newStateId,
|
|
931
|
-
intent: intent || `(ops) ${priorState.intent}`,
|
|
932
|
-
plan: applied.newState.plan,
|
|
933
|
-
html: applied.newState.html,
|
|
934
|
-
source: 'refinement',
|
|
935
|
-
version: newVersion,
|
|
936
|
-
ops_history: [...(priorState.ops_history || []), ...a2uiMessages],
|
|
937
|
-
parent_state_id: state_id,
|
|
938
|
-
warnings: applied.newState.warnings,
|
|
939
|
-
delta_summary,
|
|
940
|
-
synthesis,
|
|
941
|
-
created_at: new Date().toISOString(),
|
|
942
|
-
duration_ms: Date.now() - startedAt,
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
await issueAccumulator.flush(issueCtx);
|
|
946
|
-
|
|
947
|
-
return {
|
|
948
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
949
|
-
state_id: newStateId,
|
|
950
|
-
ops_applied: a2uiMessages,
|
|
951
|
-
ops_failed: applied.ops_failed,
|
|
952
|
-
delta_summary,
|
|
953
|
-
warnings: [...warnings, ...(applied.newState.warnings || [])],
|
|
954
|
-
synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted, locatedTargets: synthesis.locatedTargets } : null,
|
|
955
|
-
html: applied.newState.html,
|
|
956
|
-
}, null, 2) }],
|
|
957
|
-
};
|
|
958
|
-
} catch (e) {
|
|
959
|
-
await issueAccumulator.flush(issueCtx);
|
|
960
|
-
return {
|
|
961
|
-
isError: true,
|
|
962
|
-
content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
},
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
server.tool(
|
|
969
|
-
'get_state',
|
|
970
|
-
`Inspect a cached composition state by state_id.
|
|
971
|
-
|
|
972
|
-
Returns the full cache entry including the materialized HTML, the chunk binding plan, the chronological ops history (every refinement applied to this state's lineage), and the parent state_id (chain-back to the originating compose_from_chunks call).
|
|
666
|
+
// Spec: docs/specs/genui-multiturn-architecture.md (Phase A) +
|
|
667
|
+
// docs/specs/genui-chunk-marker.md.
|
|
973
668
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
Auto-fires a low-severity \`cache-miss-on-known-state\` issue when the state_id is not in the cache (the cache is bounded LRU; long-paused conversations may evict their state).`,
|
|
977
|
-
{
|
|
978
|
-
state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
|
|
979
|
-
},
|
|
980
|
-
async ({ state_id }) => {
|
|
981
|
-
const entry = stateCache.peek(state_id);
|
|
982
|
-
if (!entry) {
|
|
983
|
-
await autoReport(
|
|
984
|
-
'cache-miss-on-known-state',
|
|
985
|
-
{ state_id, tool: 'get_state' },
|
|
986
|
-
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
987
|
-
);
|
|
988
|
-
return {
|
|
989
|
-
isError: true,
|
|
990
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
991
|
-
error: 'state_id not found in cache',
|
|
992
|
-
state_id,
|
|
993
|
-
}, null, 2) }],
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
return {
|
|
997
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
998
|
-
state_id: entry.state_id,
|
|
999
|
-
intent: entry.intent,
|
|
1000
|
-
plan: entry.plan,
|
|
1001
|
-
html: entry.html,
|
|
1002
|
-
source: entry.source,
|
|
1003
|
-
version: entry.version || 1,
|
|
1004
|
-
parent_state_id: entry.parent_state_id || null,
|
|
1005
|
-
ops_history: entry.ops_history || [],
|
|
1006
|
-
warnings: entry.warnings || [],
|
|
1007
|
-
created_at: entry.created_at,
|
|
1008
|
-
}, null, 2) }],
|
|
1009
|
-
};
|
|
1010
|
-
},
|
|
1011
|
-
);
|
|
1012
|
-
|
|
1013
|
-
server.tool(
|
|
1014
|
-
'report_issue',
|
|
1015
|
-
`File a structured issue ticket when something is wrong with the gen-UI output, the tool surface, or the training data.
|
|
669
|
+
registerSynthesisTools(server);
|
|
1016
670
|
|
|
1017
|
-
Use when:
|
|
1018
|
-
(a) the user explicitly says the output is broken / wrong / missing,
|
|
1019
|
-
(b) you cannot satisfy the user's intent after retrying,
|
|
1020
|
-
(c) you detect a mismatch between requested and produced output that you cannot fix.
|
|
1021
|
-
|
|
1022
|
-
Include \`state_id\` for full trace attachment (input + output + LLM prompts/responses + validator results, when available in the cache).
|
|
1023
|
-
|
|
1024
|
-
Do NOT call this for ordinary clarification or for output the user has not yet seen.
|
|
1025
|
-
|
|
1026
|
-
Issue files land at \`.brain/audit-history/issues/<issue_id>.json\` (immutable; resolution lands in a sidecar file). Severity taxonomy matches the project's coherence-audit vocabulary: blocker = contract violation; drift = quality erosion; nit = cosmetic.`,
|
|
1027
|
-
{
|
|
1028
|
-
type: z.enum(['bug', 'training-gap', 'protocol-gap', 'ux-feedback']).describe('Issue category'),
|
|
1029
|
-
severity: z.enum(['blocker', 'drift', 'nit']).describe('Severity tier'),
|
|
1030
|
-
title: z.string().max(80).describe('One-line title (≤ 80 chars)'),
|
|
1031
|
-
body: z.string().describe('Markdown body — observed vs expected, repro steps'),
|
|
1032
|
-
state_id: z.string().optional().describe('State id from a prior tool call; auto-attaches the trace'),
|
|
1033
|
-
trace: z.enum(['full', 'summary', 'none']).optional().describe('Trace depth (default: summary if state_id provided, else none)'),
|
|
1034
|
-
suggested_owner: z.enum(['synthesis', 'retrieval', 'validator', 'chunk-corpus', 'mcp-protocol', 'unknown']).optional().describe('Best-guess owner for triage'),
|
|
1035
|
-
tags: z.array(z.string()).optional().describe('Free-form tags for filtering'),
|
|
1036
|
-
},
|
|
1037
|
-
async ({ type, severity, title, body, state_id, trace, suggested_owner, tags }) => {
|
|
1038
|
-
try {
|
|
1039
|
-
const result = await reportIssueImpl(
|
|
1040
|
-
{ type, severity, title, body, state_id, trace, suggested_owner, tags },
|
|
1041
|
-
{
|
|
1042
|
-
cache: stateCache,
|
|
1043
|
-
versionInfo: ENGINE_VERSION_INFO,
|
|
1044
|
-
reporter: 'llm',
|
|
1045
|
-
}
|
|
1046
|
-
);
|
|
1047
|
-
return {
|
|
1048
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1049
|
-
};
|
|
1050
|
-
} catch (e) {
|
|
1051
|
-
return {
|
|
1052
|
-
isError: true,
|
|
1053
|
-
content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
},
|
|
1057
|
-
);
|
|
1058
671
|
|
|
1059
672
|
// ── Start ──
|
|
1060
673
|
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesis tools — chunk-based composition + multi-turn refinement.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the 4 tools that share the LLM bridge + state-cache +
|
|
5
|
+
* issue-reporter + chunk-refiner stack: `compose_from_chunks`,
|
|
6
|
+
* `refine_composition`, `get_state`, `report_issue`.
|
|
7
|
+
*
|
|
8
|
+
* Spec: docs/specs/genui-multiturn-architecture.md (Phase A).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
import { composeFromIntent as composeFromChunksImpl } from '../../compose/strategies/zettel/chunk-synthesizer.js';
|
|
14
|
+
import { composeFromPlan, validatePlan } from '../../compose/strategies/zettel/chunk-composer.js';
|
|
15
|
+
import { createAdapter as createLLMAdapter } from '../../compose/llm/llm-bridge.js';
|
|
16
|
+
import {
|
|
17
|
+
getStateCache,
|
|
18
|
+
mintStateId,
|
|
19
|
+
mintNextStateId,
|
|
20
|
+
} from '../../compose/strategies/zettel/state-cache.js';
|
|
21
|
+
import {
|
|
22
|
+
reportIssue as reportIssueImpl,
|
|
23
|
+
autoReport,
|
|
24
|
+
createIssueAccumulator,
|
|
25
|
+
} from '../../compose/strategies/zettel/issue-reporter.js';
|
|
26
|
+
import {
|
|
27
|
+
refineFromIntent,
|
|
28
|
+
applyOps,
|
|
29
|
+
opsToA2UI,
|
|
30
|
+
validateOps,
|
|
31
|
+
} from '../../compose/strategies/zettel/chunk-refiner.js';
|
|
32
|
+
|
|
33
|
+
const stateCache = getStateCache();
|
|
34
|
+
|
|
35
|
+
const ENGINE_VERSION_INFO = {
|
|
36
|
+
mcp: '0.2.0',
|
|
37
|
+
corpus: '0.2.0',
|
|
38
|
+
engine: 'zettel',
|
|
39
|
+
llm_adapter: 'anthropic',
|
|
40
|
+
model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-7',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { stateCache, ENGINE_VERSION_INFO, autoReport, reportIssueImpl };
|
|
44
|
+
|
|
45
|
+
export function registerSynthesisTools(server) {
|
|
46
|
+
server.tool(
|
|
47
|
+
'compose_from_chunks',
|
|
48
|
+
`Compose a UI page from training chunks — retrieval-first, synthesis-fallback.
|
|
49
|
+
|
|
50
|
+
Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflow:
|
|
51
|
+
1. Pure-retrieval tier: if \`search_chunks\` returns a strong direct match, return
|
|
52
|
+
that chunk's HTML immediately (no LLM call).
|
|
53
|
+
2. Synthesis tier: when retrieval is weak, the LLM picks a page-kind chunk and
|
|
54
|
+
binds block/panel chunks to its named slots. Output validated against the
|
|
55
|
+
chunk catalog (slot names exist, bound chunks exist, kinds match).
|
|
56
|
+
|
|
57
|
+
Returns the composed HTML string + a binding plan describing which chunks plug
|
|
58
|
+
where. Useful when the prompt is novel ("dashboard with KPI grid + funnel +
|
|
59
|
+
country list") and no exact chunk has all those parts together — the LLM mixes
|
|
60
|
+
and matches from the corpus.
|
|
61
|
+
|
|
62
|
+
Two-call mode also available via \`plan\` parameter — pass a pre-baked binding
|
|
63
|
+
plan to skip the LLM call and just materialize HTML.`,
|
|
64
|
+
{
|
|
65
|
+
intent: z.string().optional().describe('Natural-language description of what to build (uses LLM synthesis)'),
|
|
66
|
+
plan: z.object({
|
|
67
|
+
page: z.string(),
|
|
68
|
+
slot_bindings: z.record(z.union([z.string(), z.array(z.string())])),
|
|
69
|
+
}).optional().describe('Pre-baked binding plan (skips LLM, materializes directly)'),
|
|
70
|
+
max_attempts: z.number().int().min(1).max(5).default(2).describe('LLM retry budget for synthesis'),
|
|
71
|
+
},
|
|
72
|
+
async ({ intent, plan, max_attempts }) => {
|
|
73
|
+
if (plan) {
|
|
74
|
+
const validation = validatePlan(plan);
|
|
75
|
+
if (!validation.ok) {
|
|
76
|
+
return {
|
|
77
|
+
isError: true,
|
|
78
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'invalid plan', errors: validation.errors }, null, 2) }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const result = composeFromPlan(plan);
|
|
82
|
+
const state_id = mintStateId(intent || plan.page || 'plan', 1);
|
|
83
|
+
stateCache.set(state_id, {
|
|
84
|
+
state_id,
|
|
85
|
+
intent: intent || `(plan) ${plan.page}`,
|
|
86
|
+
plan: result.plan,
|
|
87
|
+
html: result.html,
|
|
88
|
+
source: 'plan',
|
|
89
|
+
ops_history: [],
|
|
90
|
+
parent_state_id: null,
|
|
91
|
+
created_at: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
95
|
+
state_id,
|
|
96
|
+
html: result.html,
|
|
97
|
+
plan: result.plan,
|
|
98
|
+
warnings: result.warnings,
|
|
99
|
+
source: 'plan',
|
|
100
|
+
}, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!intent) {
|
|
105
|
+
return {
|
|
106
|
+
isError: true,
|
|
107
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or plan' }, null, 2) }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const llmAdapter = await createLLMAdapter();
|
|
113
|
+
const result = await composeFromChunksImpl({
|
|
114
|
+
intent,
|
|
115
|
+
llmAdapter,
|
|
116
|
+
maxAttempts: max_attempts,
|
|
117
|
+
});
|
|
118
|
+
const state_id = mintStateId(intent, 1);
|
|
119
|
+
stateCache.set(state_id, {
|
|
120
|
+
state_id,
|
|
121
|
+
intent,
|
|
122
|
+
plan: result.plan,
|
|
123
|
+
html: result.html,
|
|
124
|
+
source: result.source,
|
|
125
|
+
score: result.score,
|
|
126
|
+
ops_history: [],
|
|
127
|
+
parent_state_id: null,
|
|
128
|
+
warnings: result.warnings,
|
|
129
|
+
synthesis: result.synthesis,
|
|
130
|
+
scopeDrift: result.scopeDrift,
|
|
131
|
+
created_at: new Date().toISOString(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Scope-drift auto-fire: composed HTML envelope exceeded the bound-
|
|
135
|
+
// chunk envelope by > SCOPE_DRIFT_RATIO. Fires a `scope-drift` issue
|
|
136
|
+
// (writes both .json and high-res .md) so post-mortem review can
|
|
137
|
+
// catch the canvas-drift regression class without manual reporting.
|
|
138
|
+
if (result.scopeDrift?.drift) {
|
|
139
|
+
await autoReport(
|
|
140
|
+
'scope-drift',
|
|
141
|
+
{
|
|
142
|
+
intent,
|
|
143
|
+
state_id,
|
|
144
|
+
scopeDrift: result.scopeDrift,
|
|
145
|
+
tags: ['canvas-drift'],
|
|
146
|
+
trace: 'full',
|
|
147
|
+
},
|
|
148
|
+
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
154
|
+
state_id,
|
|
155
|
+
html: result.html,
|
|
156
|
+
plan: result.plan,
|
|
157
|
+
source: result.source,
|
|
158
|
+
score: result.score,
|
|
159
|
+
warnings: result.warnings,
|
|
160
|
+
scopeDrift: result.scopeDrift,
|
|
161
|
+
synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : undefined,
|
|
162
|
+
}, null, 2) }],
|
|
163
|
+
};
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
isError: true,
|
|
167
|
+
content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── Multi-turn refinement (Phase A) ─────────────────────────────────
|
|
174
|
+
// Spec: docs/specs/genui-multiturn-architecture.md §3.
|
|
175
|
+
|
|
176
|
+
server.tool(
|
|
177
|
+
'refine_composition',
|
|
178
|
+
`Refine an existing chunk-composed UI based on a natural-language intent or an explicit op-list.
|
|
179
|
+
|
|
180
|
+
Use when the user wants to modify an *existing* UI. Triggers on "change", "update", "modify", "add to", "remove from", "this", "it", "the X". Requires \`state_id\` from a prior \`compose_from_chunks\` call.
|
|
181
|
+
|
|
182
|
+
Two modes:
|
|
183
|
+
- **Intent-driven** — pass \`intent\`. Engine runs two-pass synthesis (locator pass identifies which slots to modify; modifier pass emits chunk-plan ops). Validator-driven retry on op-validation failure.
|
|
184
|
+
- **Explicit ops** — pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
|
|
185
|
+
|
|
186
|
+
Returns a new \`state_id\` (versioned chain from the parent), the A2UI op-list applied, the post-op HTML, and a delta summary. Failed ops are reported in \`ops_failed\` with reasons.
|
|
187
|
+
|
|
188
|
+
For *fresh creation* use \`compose_from_chunks\`, not this tool.`,
|
|
189
|
+
{
|
|
190
|
+
state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
|
|
191
|
+
intent: z.string().optional().describe('Natural-language description of what to change (e.g. "add a country list to page-content")'),
|
|
192
|
+
ops: z.array(z.any()).optional().describe('Pre-computed chunk-plan ops to apply directly (skips the LLM)'),
|
|
193
|
+
max_attempts: z.number().int().min(1).max(5).default(2).describe('Validator retry budget for synthesis'),
|
|
194
|
+
},
|
|
195
|
+
async ({ state_id, intent, ops, max_attempts }) => {
|
|
196
|
+
const priorState = stateCache.get(state_id);
|
|
197
|
+
if (!priorState) {
|
|
198
|
+
await autoReport(
|
|
199
|
+
'cache-miss-on-known-state',
|
|
200
|
+
{ state_id, tool: 'refine_composition' },
|
|
201
|
+
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
202
|
+
);
|
|
203
|
+
return {
|
|
204
|
+
isError: true,
|
|
205
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
206
|
+
error: 'state_id not found in cache',
|
|
207
|
+
hint: 'state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id',
|
|
208
|
+
state_id,
|
|
209
|
+
}, null, 2) }],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!intent && !ops) {
|
|
214
|
+
return {
|
|
215
|
+
isError: true,
|
|
216
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or ops' }, null, 2) }],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const issueAccumulator = createIssueAccumulator();
|
|
221
|
+
const issueCtx = { cache: stateCache, versionInfo: ENGINE_VERSION_INFO };
|
|
222
|
+
const startedAt = Date.now();
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
let resolvedOps;
|
|
226
|
+
let delta_summary = '';
|
|
227
|
+
let synthesis = null;
|
|
228
|
+
let warnings = [];
|
|
229
|
+
|
|
230
|
+
if (ops && Array.isArray(ops)) {
|
|
231
|
+
// Explicit ops path — validate then apply
|
|
232
|
+
const validation = validateOps(ops, priorState);
|
|
233
|
+
if (!validation.ok) {
|
|
234
|
+
await issueAccumulator.flush(issueCtx);
|
|
235
|
+
return {
|
|
236
|
+
isError: true,
|
|
237
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
238
|
+
error: 'ops failed validation',
|
|
239
|
+
errors: validation.errors,
|
|
240
|
+
}, null, 2) }],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
resolvedOps = ops;
|
|
244
|
+
delta_summary = `applied ${ops.length} explicit op(s)`;
|
|
245
|
+
} else {
|
|
246
|
+
// Intent path — two-pass synthesis with stub-friendly LLM bridge
|
|
247
|
+
const llmAdapter = await createLLMAdapter();
|
|
248
|
+
const refined = await refineFromIntent({
|
|
249
|
+
priorState,
|
|
250
|
+
intent,
|
|
251
|
+
llmAdapter,
|
|
252
|
+
maxAttempts: max_attempts,
|
|
253
|
+
issueAccumulator,
|
|
254
|
+
});
|
|
255
|
+
resolvedOps = refined.ops;
|
|
256
|
+
delta_summary = refined.delta_summary || '';
|
|
257
|
+
synthesis = refined.synthesis;
|
|
258
|
+
warnings = refined.warnings;
|
|
259
|
+
|
|
260
|
+
if (resolvedOps.length === 0) {
|
|
261
|
+
// Synthesizer gave up. Auto-fires already accumulated.
|
|
262
|
+
await issueAccumulator.flush(issueCtx);
|
|
263
|
+
const childId = mintNextStateId(state_id, (priorState.version || 1) + 1);
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
266
|
+
state_id: childId,
|
|
267
|
+
ops_applied: [],
|
|
268
|
+
ops_failed: [],
|
|
269
|
+
delta_summary: '',
|
|
270
|
+
warnings,
|
|
271
|
+
synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted } : null,
|
|
272
|
+
html: priorState.html,
|
|
273
|
+
}, null, 2) }],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const applied = await applyOps({ priorState, ops: resolvedOps });
|
|
279
|
+
|
|
280
|
+
if (applied.ops_failed.length > 0) {
|
|
281
|
+
issueAccumulator.add('ops-failed-after-apply', {
|
|
282
|
+
state_id,
|
|
283
|
+
tool: 'refine_composition',
|
|
284
|
+
intent,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const a2uiMessages = opsToA2UI(applied.ops_applied, applied.newState);
|
|
289
|
+
|
|
290
|
+
const parentVersion = priorState.version || 1;
|
|
291
|
+
const newVersion = parentVersion + 1;
|
|
292
|
+
const newStateId = mintNextStateId(state_id, newVersion);
|
|
293
|
+
|
|
294
|
+
stateCache.set(newStateId, {
|
|
295
|
+
state_id: newStateId,
|
|
296
|
+
intent: intent || `(ops) ${priorState.intent}`,
|
|
297
|
+
plan: applied.newState.plan,
|
|
298
|
+
html: applied.newState.html,
|
|
299
|
+
source: 'refinement',
|
|
300
|
+
version: newVersion,
|
|
301
|
+
ops_history: [...(priorState.ops_history || []), ...a2uiMessages],
|
|
302
|
+
parent_state_id: state_id,
|
|
303
|
+
warnings: applied.newState.warnings,
|
|
304
|
+
delta_summary,
|
|
305
|
+
synthesis,
|
|
306
|
+
created_at: new Date().toISOString(),
|
|
307
|
+
duration_ms: Date.now() - startedAt,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await issueAccumulator.flush(issueCtx);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
314
|
+
state_id: newStateId,
|
|
315
|
+
ops_applied: a2uiMessages,
|
|
316
|
+
ops_failed: applied.ops_failed,
|
|
317
|
+
delta_summary,
|
|
318
|
+
warnings: [...warnings, ...(applied.newState.warnings || [])],
|
|
319
|
+
synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted, locatedTargets: synthesis.locatedTargets } : null,
|
|
320
|
+
html: applied.newState.html,
|
|
321
|
+
}, null, 2) }],
|
|
322
|
+
};
|
|
323
|
+
} catch (e) {
|
|
324
|
+
await issueAccumulator.flush(issueCtx);
|
|
325
|
+
return {
|
|
326
|
+
isError: true,
|
|
327
|
+
content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
server.tool(
|
|
334
|
+
'get_state',
|
|
335
|
+
`Inspect a cached composition state by state_id.
|
|
336
|
+
|
|
337
|
+
Returns the full cache entry including the materialized HTML, the chunk binding plan, the chronological ops history (every refinement applied to this state's lineage), and the parent state_id (chain-back to the originating compose_from_chunks call).
|
|
338
|
+
|
|
339
|
+
Useful for debugging refinement sequences, replaying a state's history, or verifying that a state_id is still cached before issuing a refine_composition call.
|
|
340
|
+
|
|
341
|
+
Auto-fires a low-severity \`cache-miss-on-known-state\` issue when the state_id is not in the cache (the cache is bounded LRU; long-paused conversations may evict their state).`,
|
|
342
|
+
{
|
|
343
|
+
state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
|
|
344
|
+
},
|
|
345
|
+
async ({ state_id }) => {
|
|
346
|
+
const entry = stateCache.peek(state_id);
|
|
347
|
+
if (!entry) {
|
|
348
|
+
await autoReport(
|
|
349
|
+
'cache-miss-on-known-state',
|
|
350
|
+
{ state_id, tool: 'get_state' },
|
|
351
|
+
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
352
|
+
);
|
|
353
|
+
return {
|
|
354
|
+
isError: true,
|
|
355
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
356
|
+
error: 'state_id not found in cache',
|
|
357
|
+
state_id,
|
|
358
|
+
}, null, 2) }],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
363
|
+
state_id: entry.state_id,
|
|
364
|
+
intent: entry.intent,
|
|
365
|
+
plan: entry.plan,
|
|
366
|
+
html: entry.html,
|
|
367
|
+
source: entry.source,
|
|
368
|
+
version: entry.version || 1,
|
|
369
|
+
parent_state_id: entry.parent_state_id || null,
|
|
370
|
+
ops_history: entry.ops_history || [],
|
|
371
|
+
warnings: entry.warnings || [],
|
|
372
|
+
created_at: entry.created_at,
|
|
373
|
+
}, null, 2) }],
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
server.tool(
|
|
379
|
+
'report_issue',
|
|
380
|
+
`File a structured issue ticket — writes BOTH a machine-readable JSON file AND a human-readable Markdown report containing the full session trace (intent, retrieval log, LLM prompts, every attempt's raw response, composer plan, generated HTML preview, component count, warnings, environment).
|
|
381
|
+
|
|
382
|
+
When to call (any of these is a trigger):
|
|
383
|
+
(a) USER PHRASES — call immediately when the user says any of:
|
|
384
|
+
"file a ticket", "log a ticket", "save a ticket",
|
|
385
|
+
"report this as a bug", "report this issue", "log this issue",
|
|
386
|
+
"save the trace", "capture the session", "save the session for review",
|
|
387
|
+
"create a session ticket", "this is broken — debug it",
|
|
388
|
+
"download the trace", "export this for review",
|
|
389
|
+
"track this regression", "open a ticket for this".
|
|
390
|
+
(b) USER COMPLAINS the output is broken / wrong / missing.
|
|
391
|
+
(c) YOU CANNOT satisfy the user's intent after retrying.
|
|
392
|
+
(d) YOU DETECT a mismatch between requested and produced output you can't fix.
|
|
393
|
+
|
|
394
|
+
ALWAYS pass \`state_id\` from the most-recent compose_from_chunks or refine_composition call when one exists. The default \`trace: "full"\` then writes the high-resolution Markdown ticket. (Pass \`trace: "summary"\` for compact tickets, or \`trace: "none"\` to suppress the trace entirely.)
|
|
395
|
+
|
|
396
|
+
Do NOT call this for ordinary clarification or for output the user has not yet seen.
|
|
397
|
+
|
|
398
|
+
The tool returns BOTH paths in its response: \`path\` (.json) and \`markdown_path\` (.md). Surface BOTH to the user so they can navigate / download either:
|
|
399
|
+
|
|
400
|
+
📋 Logged ticket \`{issue_id}\` (\`{severity}\` · owner: {suggested_owner})
|
|
401
|
+
• Trace report: \`{markdown_path}\` ← human-readable, scan this first
|
|
402
|
+
• Raw JSON: \`{path}\` ← machine-readable
|
|
403
|
+
|
|
404
|
+
Issue files land under \`.brain/audit-history/issues/\` (immutable; resolution lands in a sidecar file). Severity taxonomy matches the project's coherence-audit vocabulary: blocker = contract violation; drift = quality erosion; nit = cosmetic.`,
|
|
405
|
+
{
|
|
406
|
+
type: z.enum(['bug', 'training-gap', 'protocol-gap', 'ux-feedback']).describe('Issue category'),
|
|
407
|
+
severity: z.enum(['blocker', 'drift', 'nit']).describe('Severity tier'),
|
|
408
|
+
title: z.string().max(80).describe('One-line title (≤ 80 chars)'),
|
|
409
|
+
body: z.string().describe('Markdown body — observed vs expected, repro steps'),
|
|
410
|
+
state_id: z.string().optional().describe('State id from a prior tool call; auto-attaches the trace'),
|
|
411
|
+
trace: z.enum(['full', 'summary', 'none']).optional().describe('Trace depth — DEFAULT: "full" when state_id is provided (writes both .json + .md ticket with retrieval log, LLM prompts/attempts, plan, HTML preview). Use "summary" for compact tickets; "none" to suppress trace entirely.'),
|
|
412
|
+
suggested_owner: z.enum(['synthesis', 'retrieval', 'validator', 'chunk-corpus', 'mcp-protocol', 'unknown']).optional().describe('Best-guess owner for triage'),
|
|
413
|
+
tags: z.array(z.string()).optional().describe('Free-form tags for filtering'),
|
|
414
|
+
},
|
|
415
|
+
async ({ type, severity, title, body, state_id, trace, suggested_owner, tags }) => {
|
|
416
|
+
try {
|
|
417
|
+
const result = await reportIssueImpl(
|
|
418
|
+
{ type, severity, title, body, state_id, trace, suggested_owner, tags },
|
|
419
|
+
{
|
|
420
|
+
cache: stateCache,
|
|
421
|
+
versionInfo: ENGINE_VERSION_INFO,
|
|
422
|
+
reporter: 'llm',
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
return {
|
|
426
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
427
|
+
};
|
|
428
|
+
} catch (e) {
|
|
429
|
+
return {
|
|
430
|
+
isError: true,
|
|
431
|
+
content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{"id":"intent-001","kind":"compose","category":"data-display","intent":"kpi grid with 4 stat cards: users, revenue, sessions, churn","expected_components":["Card","Stat","Grid"],"expected_chunk":"kpi-grid-4-card"}
|
|
2
|
-
{"id":"intent-002","kind":"compose","category":"forms","intent":"sign-in form with email + password + 'forgot password' link","expected_components":["Card","Input","Button","Field"],"expected_chunk":"auth-sign-in"}
|
|
3
|
-
{"id":"intent-003","kind":"compose","category":"layout","intent":"settings page with three tabs (general, integrations, billing)","expected_components":["Tabs","Tab","Card","Section"],"expected_chunk":"settings-tabs-3"}
|
|
4
|
-
{"id":"intent-004","kind":"compose","category":"data","intent":"data table of users with role badge + last-active timestamp","expected_components":["Table","Badge"],"expected_chunk":"users-table"}
|
|
5
|
-
{"id":"intent-005","kind":"compose","category":"data-viz","intent":"conversion funnel chart over 6 stages, with drop-off labels","expected_components":["Chart","Card","ChartLegend"],"expected_chunk":"conversion-funnel"}
|
|
6
|
-
{"id":"intent-006","kind":"compose","category":"agent","intent":"agent activity feed with reasoning steps + final artifact","expected_components":["AgentTrace","AgentReasoning","AgentArtifact"],"expected_chunk":"agent-activity-feed"}
|
|
7
|
-
{"id":"intent-007","kind":"compose","category":"layout","intent":"split-pane editor: code on the left, preview on the right","expected_components":["EditorShell","Pane","Code"],"expected_chunk":"editor-split"}
|
|
8
|
-
{"id":"intent-008","kind":"compose","category":"overlay","intent":"command palette modal with grouped results (recent, suggestions)","expected_components":["Command","Modal"],"expected_chunk":"command-grouped"}
|
|
9
|
-
{"id":"intent-009","kind":"compose","category":"forms","intent":"registration step 2 of 5 — profile setup with 4 fields","expected_components":["Card","StepProgress","Field","Input"],"expected_chunk":"reg-step-shell"}
|
|
10
|
-
{"id":"intent-010","kind":"compose","category":"layout","intent":"404 error page with breadcrumb + back-to-home link","expected_components":["Card","Breadcrumb","Button"],"expected_chunk":"error-404"}
|
|
11
|
-
{"id":"intent-011","kind":"refine","category":"data-display","intent":"dashboard for project metrics","refine":"add a date-range filter at the top","expected_components":["Card","Stat","Select"],"expected_chunk":"project-dashboard"}
|
|
12
|
-
{"id":"intent-012","kind":"refine","category":"display","intent":"user profile card","refine":"make the email editable inline","expected_components":["Card","Avatar","Input"],"expected_chunk":"user-profile-card"}
|
|
13
|
-
{"id":"intent-013","kind":"refine","category":"data","intent":"kanban board with 3 columns","refine":"add a count badge to each column header","expected_components":["Card","Badge","Header"],"expected_chunk":"kanban-3col"}
|
|
14
|
-
{"id":"intent-014","kind":"refine","category":"chat","intent":"chat surface with streaming reply","refine":"add a stop button while streaming","expected_components":["ChatShell","Button","ChatInput"],"expected_chunk":"chat-streaming"}
|
|
15
|
-
{"id":"intent-015","kind":"refine","category":"forms","intent":"sign-up form with email + password","refine":"add password strength meter","expected_components":["Card","Input","Progress"],"expected_chunk":"auth-sign-up"}
|
|
16
|
-
{"id":"intent-016","kind":"refine","category":"settings","intent":"settings tab for notifications","refine":"split email + push into separate sections","expected_components":["Card","Section","Switch"],"expected_chunk":"settings-notifications"}
|
|
17
|
-
{"id":"intent-017","kind":"refine","category":"data","intent":"table of orders","refine":"add a bulk-action toolbar above the table","expected_components":["Table","TableToolbar","Button"],"expected_chunk":"orders-table"}
|
|
18
|
-
{"id":"intent-018","kind":"refine","category":"agent","intent":"agent reasoning panel","refine":"collapse intermediate steps by default, expandable","expected_components":["AgentReasoning","Accordion"],"expected_chunk":"agent-reasoning-collapsed"}
|
|
19
|
-
{"id":"intent-019","kind":"refine","category":"overlay","intent":"modal confirming destructive action","refine":"require typing the resource name to confirm","expected_components":["Modal","Input","Button"],"expected_chunk":"destructive-confirm"}
|
|
20
|
-
{"id":"intent-020","kind":"refine","category":"display","intent":"marketing landing hero","refine":"add a secondary 'see demo' CTA","expected_components":["Card","Heading","Button"],"expected_chunk":"marketing-hero"}
|