@adia-ai/a2ui-mcp 0.6.4 → 0.6.6
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 +10 -0
- package/package.json +1 -1
- package/server.js +110 -547
- package/tools/corpus.js +103 -0
- package/tools/corpus.ts +112 -0
- package/tools/feedback.js +63 -0
- package/tools/feedback.ts +73 -0
- package/tools/synthesis.js +143 -174
- package/tools/validation.js +131 -0
- package/tools/validation.ts +153 -0
- package/tools/zettel.js +87 -0
- package/tools/zettel.ts +98 -0
package/tools/synthesis.js
CHANGED
|
@@ -1,51 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 '../../../llm/llm-bridge.js';
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { composeFromIntent as composeFromChunksImpl } from "../../compose/strategies/zettel/chunk-synthesizer.js";
|
|
3
|
+
import { composeFromPlan, validatePlan } from "../../compose/strategies/zettel/chunk-composer.js";
|
|
4
|
+
import { createAdapter as createLLMAdapter } from "../../../llm/llm-bridge.js";
|
|
16
5
|
import {
|
|
17
6
|
getStateCache,
|
|
18
7
|
mintStateId,
|
|
19
|
-
mintNextStateId
|
|
20
|
-
} from
|
|
8
|
+
mintNextStateId
|
|
9
|
+
} from "../../compose/strategies/zettel/state-cache.js";
|
|
21
10
|
import {
|
|
22
11
|
reportIssue as reportIssueImpl,
|
|
23
12
|
autoReport,
|
|
24
|
-
createIssueAccumulator
|
|
25
|
-
} from
|
|
13
|
+
createIssueAccumulator
|
|
14
|
+
} from "../../compose/strategies/zettel/issue-reporter.js";
|
|
26
15
|
import {
|
|
27
16
|
refineFromIntent,
|
|
28
17
|
applyOps,
|
|
29
18
|
opsToA2UI,
|
|
30
|
-
validateOps
|
|
31
|
-
} from
|
|
32
|
-
|
|
19
|
+
validateOps
|
|
20
|
+
} from "../../compose/strategies/zettel/chunk-refiner.js";
|
|
33
21
|
const stateCache = getStateCache();
|
|
34
|
-
|
|
35
22
|
const ENGINE_VERSION_INFO = {
|
|
36
|
-
mcp:
|
|
37
|
-
corpus:
|
|
38
|
-
engine:
|
|
39
|
-
llm_adapter:
|
|
40
|
-
model: process.env
|
|
23
|
+
mcp: "0.2.0",
|
|
24
|
+
corpus: "0.2.0",
|
|
25
|
+
engine: "zettel",
|
|
26
|
+
llm_adapter: "anthropic",
|
|
27
|
+
model: typeof process !== "undefined" && process.env["ANTHROPIC_MODEL"] || "claude-opus-4-7"
|
|
41
28
|
};
|
|
42
|
-
|
|
43
|
-
export { stateCache, ENGINE_VERSION_INFO, autoReport, reportIssueImpl };
|
|
44
|
-
|
|
45
|
-
export function registerSynthesisTools(server) {
|
|
29
|
+
function registerSynthesisTools(server) {
|
|
46
30
|
server.tool(
|
|
47
|
-
|
|
48
|
-
`Compose a UI page from training chunks
|
|
31
|
+
"compose_from_chunks",
|
|
32
|
+
`Compose a UI page from training chunks \u2014 retrieval-first, synthesis-fallback.
|
|
49
33
|
|
|
50
34
|
Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflow:
|
|
51
35
|
1. Pure-retrieval tier: if \`search_chunks\` returns a strong direct match, return
|
|
@@ -56,18 +40,18 @@ Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflo
|
|
|
56
40
|
|
|
57
41
|
Returns the composed HTML string + a binding plan describing which chunks plug
|
|
58
42
|
where. Useful when the prompt is novel ("dashboard with KPI grid + funnel +
|
|
59
|
-
country list") and no exact chunk has all those parts together
|
|
43
|
+
country list") and no exact chunk has all those parts together \u2014 the LLM mixes
|
|
60
44
|
and matches from the corpus.
|
|
61
45
|
|
|
62
|
-
Two-call mode also available via \`plan\` parameter
|
|
46
|
+
Two-call mode also available via \`plan\` parameter \u2014 pass a pre-baked binding
|
|
63
47
|
plan to skip the LLM call and just materialize HTML.`,
|
|
64
48
|
{
|
|
65
|
-
intent: z.string().optional().describe(
|
|
49
|
+
intent: z.string().optional().describe("Natural-language description of what to build (uses LLM synthesis)"),
|
|
66
50
|
plan: z.object({
|
|
67
51
|
page: z.string(),
|
|
68
|
-
slot_bindings: z.record(z.union([z.string(), z.array(z.string())]))
|
|
69
|
-
}).optional().describe(
|
|
70
|
-
max_attempts: z.number().int().min(1).max(5).default(2).describe(
|
|
52
|
+
slot_bindings: z.record(z.union([z.string(), z.array(z.string())]))
|
|
53
|
+
}).optional().describe("Pre-baked binding plan (skips LLM, materializes directly)"),
|
|
54
|
+
max_attempts: z.number().int().min(1).max(5).default(2).describe("LLM retry budget for synthesis")
|
|
71
55
|
},
|
|
72
56
|
async ({ intent, plan, max_attempts }) => {
|
|
73
57
|
if (plan) {
|
|
@@ -75,45 +59,43 @@ plan to skip the LLM call and just materialize HTML.`,
|
|
|
75
59
|
if (!validation.ok) {
|
|
76
60
|
return {
|
|
77
61
|
isError: true,
|
|
78
|
-
content: [{ type:
|
|
62
|
+
content: [{ type: "text", text: JSON.stringify({ error: "invalid plan", errors: validation.errors }, null, 2) }]
|
|
79
63
|
};
|
|
80
64
|
}
|
|
81
65
|
const result = composeFromPlan(plan);
|
|
82
|
-
const state_id = mintStateId(intent
|
|
66
|
+
const state_id = mintStateId(intent ?? plan.page ?? "plan", 1);
|
|
83
67
|
stateCache.set(state_id, {
|
|
84
68
|
state_id,
|
|
85
|
-
intent: intent
|
|
69
|
+
intent: intent ?? `(plan) ${plan.page}`,
|
|
86
70
|
plan: result.plan,
|
|
87
71
|
html: result.html,
|
|
88
|
-
source:
|
|
72
|
+
source: "plan",
|
|
89
73
|
ops_history: [],
|
|
90
74
|
parent_state_id: null,
|
|
91
|
-
created_at: new Date().toISOString()
|
|
75
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
92
76
|
});
|
|
93
77
|
return {
|
|
94
|
-
content: [{ type:
|
|
78
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
95
79
|
state_id,
|
|
96
80
|
html: result.html,
|
|
97
81
|
plan: result.plan,
|
|
98
82
|
warnings: result.warnings,
|
|
99
|
-
source:
|
|
100
|
-
}, null, 2) }]
|
|
83
|
+
source: "plan"
|
|
84
|
+
}, null, 2) }]
|
|
101
85
|
};
|
|
102
86
|
}
|
|
103
|
-
|
|
104
87
|
if (!intent) {
|
|
105
88
|
return {
|
|
106
89
|
isError: true,
|
|
107
|
-
content: [{ type:
|
|
90
|
+
content: [{ type: "text", text: JSON.stringify({ error: "must provide either intent or plan" }, null, 2) }]
|
|
108
91
|
};
|
|
109
92
|
}
|
|
110
|
-
|
|
111
93
|
try {
|
|
112
94
|
const llmAdapter = await createLLMAdapter();
|
|
113
95
|
const result = await composeFromChunksImpl({
|
|
114
96
|
intent,
|
|
115
97
|
llmAdapter,
|
|
116
|
-
maxAttempts: max_attempts
|
|
98
|
+
maxAttempts: max_attempts
|
|
117
99
|
});
|
|
118
100
|
const state_id = mintStateId(intent, 1);
|
|
119
101
|
stateCache.set(state_id, {
|
|
@@ -128,29 +110,23 @@ plan to skip the LLM call and just materialize HTML.`,
|
|
|
128
110
|
warnings: result.warnings,
|
|
129
111
|
synthesis: result.synthesis,
|
|
130
112
|
scopeDrift: result.scopeDrift,
|
|
131
|
-
created_at: new Date().toISOString()
|
|
113
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
132
114
|
});
|
|
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
115
|
if (result.scopeDrift?.drift) {
|
|
139
116
|
await autoReport(
|
|
140
|
-
|
|
117
|
+
"scope-drift",
|
|
141
118
|
{
|
|
142
119
|
intent,
|
|
143
120
|
state_id,
|
|
144
121
|
scopeDrift: result.scopeDrift,
|
|
145
|
-
tags: [
|
|
146
|
-
trace:
|
|
122
|
+
tags: ["canvas-drift"],
|
|
123
|
+
trace: "full"
|
|
147
124
|
},
|
|
148
125
|
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
149
126
|
);
|
|
150
127
|
}
|
|
151
|
-
|
|
152
128
|
return {
|
|
153
|
-
content: [{ type:
|
|
129
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
154
130
|
state_id,
|
|
155
131
|
html: result.html,
|
|
156
132
|
plan: result.plan,
|
|
@@ -158,180 +134,165 @@ plan to skip the LLM call and just materialize HTML.`,
|
|
|
158
134
|
score: result.score,
|
|
159
135
|
warnings: result.warnings,
|
|
160
136
|
scopeDrift: result.scopeDrift,
|
|
161
|
-
synthesis: result.synthesis ? { attempts: result.synthesis.attempts } :
|
|
162
|
-
}, null, 2) }]
|
|
137
|
+
synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : void 0
|
|
138
|
+
}, null, 2) }]
|
|
163
139
|
};
|
|
164
140
|
} catch (e) {
|
|
141
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
165
142
|
return {
|
|
166
143
|
isError: true,
|
|
167
|
-
content: [{ type:
|
|
144
|
+
content: [{ type: "text", text: JSON.stringify({ error: err.message }, null, 2) }]
|
|
168
145
|
};
|
|
169
146
|
}
|
|
170
|
-
}
|
|
147
|
+
}
|
|
171
148
|
);
|
|
172
|
-
|
|
173
|
-
// ── Multi-turn refinement (Phase A) ─────────────────────────────────
|
|
174
|
-
// Spec: docs/specs/genui-multiturn-architecture.md §3.
|
|
175
|
-
|
|
176
149
|
server.tool(
|
|
177
|
-
|
|
150
|
+
"refine_composition",
|
|
178
151
|
`Refine an existing chunk-composed UI based on a natural-language intent or an explicit op-list.
|
|
179
152
|
|
|
180
153
|
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
154
|
|
|
182
155
|
Two modes:
|
|
183
|
-
- **Intent-driven**
|
|
184
|
-
- **Explicit ops**
|
|
156
|
+
- **Intent-driven** \u2014 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.
|
|
157
|
+
- **Explicit ops** \u2014 pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
|
|
185
158
|
|
|
186
159
|
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
160
|
|
|
188
161
|
For *fresh creation* use \`compose_from_chunks\`, not this tool.`,
|
|
189
162
|
{
|
|
190
|
-
state_id: z.string().describe(
|
|
163
|
+
state_id: z.string().describe("State id from a prior compose_from_chunks or refine_composition call"),
|
|
191
164
|
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(
|
|
193
|
-
max_attempts: z.number().int().min(1).max(5).default(2).describe(
|
|
165
|
+
ops: z.array(z.any()).optional().describe("Pre-computed chunk-plan ops to apply directly (skips the LLM)"),
|
|
166
|
+
max_attempts: z.number().int().min(1).max(5).default(2).describe("Validator retry budget for synthesis")
|
|
194
167
|
},
|
|
195
168
|
async ({ state_id, intent, ops, max_attempts }) => {
|
|
196
169
|
const priorState = stateCache.get(state_id);
|
|
197
170
|
if (!priorState) {
|
|
198
171
|
await autoReport(
|
|
199
|
-
|
|
200
|
-
{ state_id, tool:
|
|
172
|
+
"cache-miss-on-known-state",
|
|
173
|
+
{ state_id, tool: "refine_composition" },
|
|
201
174
|
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
202
175
|
);
|
|
203
176
|
return {
|
|
204
177
|
isError: true,
|
|
205
|
-
content: [{ type:
|
|
206
|
-
error:
|
|
207
|
-
hint:
|
|
208
|
-
state_id
|
|
209
|
-
}, null, 2) }]
|
|
178
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
179
|
+
error: "state_id not found in cache",
|
|
180
|
+
hint: "state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id",
|
|
181
|
+
state_id
|
|
182
|
+
}, null, 2) }]
|
|
210
183
|
};
|
|
211
184
|
}
|
|
212
|
-
|
|
213
185
|
if (!intent && !ops) {
|
|
214
186
|
return {
|
|
215
187
|
isError: true,
|
|
216
|
-
content: [{ type:
|
|
188
|
+
content: [{ type: "text", text: JSON.stringify({ error: "must provide either intent or ops" }, null, 2) }]
|
|
217
189
|
};
|
|
218
190
|
}
|
|
219
|
-
|
|
220
191
|
const issueAccumulator = createIssueAccumulator();
|
|
221
192
|
const issueCtx = { cache: stateCache, versionInfo: ENGINE_VERSION_INFO };
|
|
222
193
|
const startedAt = Date.now();
|
|
223
|
-
|
|
224
194
|
try {
|
|
225
195
|
let resolvedOps;
|
|
226
|
-
let delta_summary =
|
|
196
|
+
let delta_summary = "";
|
|
227
197
|
let synthesis = null;
|
|
228
198
|
let warnings = [];
|
|
229
|
-
|
|
230
199
|
if (ops && Array.isArray(ops)) {
|
|
231
|
-
// Explicit ops path — validate then apply
|
|
232
200
|
const validation = validateOps(ops, priorState);
|
|
233
201
|
if (!validation.ok) {
|
|
234
202
|
await issueAccumulator.flush(issueCtx);
|
|
235
203
|
return {
|
|
236
204
|
isError: true,
|
|
237
|
-
content: [{ type:
|
|
238
|
-
error:
|
|
239
|
-
errors: validation.errors
|
|
240
|
-
}, null, 2) }]
|
|
205
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
206
|
+
error: "ops failed validation",
|
|
207
|
+
errors: validation.errors
|
|
208
|
+
}, null, 2) }]
|
|
241
209
|
};
|
|
242
210
|
}
|
|
243
211
|
resolvedOps = ops;
|
|
244
212
|
delta_summary = `applied ${ops.length} explicit op(s)`;
|
|
245
213
|
} else {
|
|
246
|
-
// Intent path — two-pass synthesis with stub-friendly LLM bridge
|
|
247
214
|
const llmAdapter = await createLLMAdapter();
|
|
248
215
|
const refined = await refineFromIntent({
|
|
249
216
|
priorState,
|
|
250
217
|
intent,
|
|
251
218
|
llmAdapter,
|
|
252
219
|
maxAttempts: max_attempts,
|
|
253
|
-
issueAccumulator
|
|
220
|
+
issueAccumulator
|
|
254
221
|
});
|
|
255
222
|
resolvedOps = refined.ops;
|
|
256
|
-
delta_summary = refined.delta_summary
|
|
223
|
+
delta_summary = refined.delta_summary ?? "";
|
|
257
224
|
synthesis = refined.synthesis;
|
|
258
225
|
warnings = refined.warnings;
|
|
259
|
-
|
|
260
226
|
if (resolvedOps.length === 0) {
|
|
261
|
-
// Synthesizer gave up. Auto-fires already accumulated.
|
|
262
227
|
await issueAccumulator.flush(issueCtx);
|
|
263
|
-
const childId = mintNextStateId(state_id, (priorState
|
|
228
|
+
const childId = mintNextStateId(state_id, (priorState["version"] ?? 1) + 1);
|
|
229
|
+
const synthInfo2 = synthesis;
|
|
264
230
|
return {
|
|
265
|
-
content: [{ type:
|
|
231
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
266
232
|
state_id: childId,
|
|
267
233
|
ops_applied: [],
|
|
268
234
|
ops_failed: [],
|
|
269
|
-
delta_summary:
|
|
235
|
+
delta_summary: "",
|
|
270
236
|
warnings,
|
|
271
|
-
synthesis:
|
|
272
|
-
html: priorState
|
|
273
|
-
}, null, 2) }]
|
|
237
|
+
synthesis: synthInfo2 ? { attempts: synthInfo2["attempts"], targeted: synthInfo2["targeted"] } : null,
|
|
238
|
+
html: priorState["html"]
|
|
239
|
+
}, null, 2) }]
|
|
274
240
|
};
|
|
275
241
|
}
|
|
276
242
|
}
|
|
277
|
-
|
|
278
243
|
const applied = await applyOps({ priorState, ops: resolvedOps });
|
|
279
|
-
|
|
280
244
|
if (applied.ops_failed.length > 0) {
|
|
281
|
-
issueAccumulator.add(
|
|
245
|
+
issueAccumulator.add("ops-failed-after-apply", {
|
|
282
246
|
state_id,
|
|
283
|
-
tool:
|
|
284
|
-
intent
|
|
247
|
+
tool: "refine_composition",
|
|
248
|
+
intent
|
|
285
249
|
});
|
|
286
250
|
}
|
|
287
|
-
|
|
288
251
|
const a2uiMessages = opsToA2UI(applied.ops_applied, applied.newState);
|
|
289
|
-
|
|
290
|
-
const parentVersion = priorState.version || 1;
|
|
252
|
+
const parentVersion = priorState["version"] ?? 1;
|
|
291
253
|
const newVersion = parentVersion + 1;
|
|
292
254
|
const newStateId = mintNextStateId(state_id, newVersion);
|
|
293
|
-
|
|
294
255
|
stateCache.set(newStateId, {
|
|
295
256
|
state_id: newStateId,
|
|
296
|
-
intent: intent
|
|
297
|
-
plan: applied.newState
|
|
298
|
-
html: applied.newState
|
|
299
|
-
source:
|
|
257
|
+
intent: intent ?? `(ops) ${priorState["intent"] ?? ""}`,
|
|
258
|
+
plan: applied.newState["plan"],
|
|
259
|
+
html: applied.newState["html"],
|
|
260
|
+
source: "refinement",
|
|
300
261
|
version: newVersion,
|
|
301
|
-
ops_history: [...
|
|
262
|
+
ops_history: [...priorState["ops_history"] ?? [], ...a2uiMessages],
|
|
302
263
|
parent_state_id: state_id,
|
|
303
|
-
warnings: applied.newState
|
|
264
|
+
warnings: applied.newState["warnings"],
|
|
304
265
|
delta_summary,
|
|
305
266
|
synthesis,
|
|
306
|
-
created_at: new Date().toISOString(),
|
|
307
|
-
duration_ms: Date.now() - startedAt
|
|
267
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
268
|
+
duration_ms: Date.now() - startedAt
|
|
308
269
|
});
|
|
309
|
-
|
|
310
270
|
await issueAccumulator.flush(issueCtx);
|
|
311
|
-
|
|
271
|
+
const synthInfo = synthesis;
|
|
272
|
+
const newStateWarnings = applied.newState["warnings"] ?? [];
|
|
312
273
|
return {
|
|
313
|
-
content: [{ type:
|
|
274
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
314
275
|
state_id: newStateId,
|
|
315
276
|
ops_applied: a2uiMessages,
|
|
316
277
|
ops_failed: applied.ops_failed,
|
|
317
278
|
delta_summary,
|
|
318
|
-
warnings: [...warnings, ...
|
|
319
|
-
synthesis:
|
|
320
|
-
html: applied.newState
|
|
321
|
-
}, null, 2) }]
|
|
279
|
+
warnings: [...warnings, ...newStateWarnings],
|
|
280
|
+
synthesis: synthInfo ? { attempts: synthInfo["attempts"], targeted: synthInfo["targeted"], locatedTargets: synthInfo["locatedTargets"] } : null,
|
|
281
|
+
html: applied.newState["html"]
|
|
282
|
+
}, null, 2) }]
|
|
322
283
|
};
|
|
323
284
|
} catch (e) {
|
|
324
285
|
await issueAccumulator.flush(issueCtx);
|
|
286
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
325
287
|
return {
|
|
326
288
|
isError: true,
|
|
327
|
-
content: [{ type:
|
|
289
|
+
content: [{ type: "text", text: JSON.stringify({ error: err.message }, null, 2) }]
|
|
328
290
|
};
|
|
329
291
|
}
|
|
330
|
-
}
|
|
292
|
+
}
|
|
331
293
|
);
|
|
332
|
-
|
|
333
294
|
server.tool(
|
|
334
|
-
|
|
295
|
+
"get_state",
|
|
335
296
|
`Inspect a cached composition state by state_id.
|
|
336
297
|
|
|
337
298
|
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).
|
|
@@ -340,51 +301,51 @@ Useful for debugging refinement sequences, replaying a state's history, or verif
|
|
|
340
301
|
|
|
341
302
|
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
303
|
{
|
|
343
|
-
state_id: z.string().describe(
|
|
304
|
+
state_id: z.string().describe("State id from a prior compose_from_chunks or refine_composition call")
|
|
344
305
|
},
|
|
345
306
|
async ({ state_id }) => {
|
|
346
307
|
const entry = stateCache.peek(state_id);
|
|
347
308
|
if (!entry) {
|
|
348
309
|
await autoReport(
|
|
349
|
-
|
|
350
|
-
{ state_id, tool:
|
|
310
|
+
"cache-miss-on-known-state",
|
|
311
|
+
{ state_id, tool: "get_state" },
|
|
351
312
|
{ cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
|
|
352
313
|
);
|
|
353
314
|
return {
|
|
354
315
|
isError: true,
|
|
355
|
-
content: [{ type:
|
|
356
|
-
error:
|
|
357
|
-
state_id
|
|
358
|
-
}, null, 2) }]
|
|
316
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
317
|
+
error: "state_id not found in cache",
|
|
318
|
+
state_id
|
|
319
|
+
}, null, 2) }]
|
|
359
320
|
};
|
|
360
321
|
}
|
|
322
|
+
const e = entry;
|
|
361
323
|
return {
|
|
362
|
-
content: [{ type:
|
|
363
|
-
state_id:
|
|
364
|
-
intent:
|
|
365
|
-
plan:
|
|
366
|
-
html:
|
|
367
|
-
source:
|
|
368
|
-
version:
|
|
369
|
-
parent_state_id:
|
|
370
|
-
ops_history:
|
|
371
|
-
warnings:
|
|
372
|
-
created_at:
|
|
373
|
-
}, null, 2) }]
|
|
324
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
325
|
+
state_id: e["state_id"],
|
|
326
|
+
intent: e["intent"],
|
|
327
|
+
plan: e["plan"],
|
|
328
|
+
html: e["html"],
|
|
329
|
+
source: e["source"],
|
|
330
|
+
version: e["version"] ?? 1,
|
|
331
|
+
parent_state_id: e["parent_state_id"] ?? null,
|
|
332
|
+
ops_history: e["ops_history"] ?? [],
|
|
333
|
+
warnings: e["warnings"] ?? [],
|
|
334
|
+
created_at: e["created_at"]
|
|
335
|
+
}, null, 2) }]
|
|
374
336
|
};
|
|
375
|
-
}
|
|
337
|
+
}
|
|
376
338
|
);
|
|
377
|
-
|
|
378
339
|
server.tool(
|
|
379
|
-
|
|
380
|
-
`File a structured issue ticket
|
|
340
|
+
"report_issue",
|
|
341
|
+
`File a structured issue ticket \u2014 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
342
|
|
|
382
343
|
When to call (any of these is a trigger):
|
|
383
|
-
(a) USER PHRASES
|
|
344
|
+
(a) USER PHRASES \u2014 call immediately when the user says any of:
|
|
384
345
|
"file a ticket", "log a ticket", "save a ticket",
|
|
385
346
|
"report this as a bug", "report this issue", "log this issue",
|
|
386
347
|
"save the trace", "capture the session", "save the session for review",
|
|
387
|
-
"create a session ticket", "this is broken
|
|
348
|
+
"create a session ticket", "this is broken \u2014 debug it",
|
|
388
349
|
"download the trace", "export this for review",
|
|
389
350
|
"track this regression", "open a ticket for this".
|
|
390
351
|
(b) USER COMPLAINS the output is broken / wrong / missing.
|
|
@@ -397,20 +358,20 @@ Do NOT call this for ordinary clarification or for output the user has not yet s
|
|
|
397
358
|
|
|
398
359
|
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
360
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
361
|
+
\u{1F4CB} Logged ticket \`{issue_id}\` (\`{severity}\` \xB7 owner: {suggested_owner})
|
|
362
|
+
\u2022 Trace report: \`{markdown_path}\` \u2190 human-readable, scan this first
|
|
363
|
+
\u2022 Raw JSON: \`{path}\` \u2190 machine-readable
|
|
403
364
|
|
|
404
365
|
Issue files land under \`.brain/audit-history/issues/\` (immutable; resolution lands in a sidecar file). Severity taxonomy matches the project's ui-audit-coherence vocabulary: blocker = contract violation; drift = quality erosion; nit = cosmetic.`,
|
|
405
366
|
{
|
|
406
|
-
type: z.enum([
|
|
407
|
-
severity: z.enum([
|
|
408
|
-
title: z.string().max(80).describe(
|
|
409
|
-
body: z.string().describe(
|
|
410
|
-
state_id: z.string().optional().describe(
|
|
411
|
-
trace: z.enum([
|
|
412
|
-
suggested_owner: z.enum([
|
|
413
|
-
tags: z.array(z.string()).optional().describe(
|
|
367
|
+
type: z.enum(["bug", "training-gap", "protocol-gap", "ux-feedback"]).describe("Issue category"),
|
|
368
|
+
severity: z.enum(["blocker", "drift", "nit"]).describe("Severity tier"),
|
|
369
|
+
title: z.string().max(80).describe("One-line title (\u2264 80 chars)"),
|
|
370
|
+
body: z.string().describe("Markdown body \u2014 observed vs expected, repro steps"),
|
|
371
|
+
state_id: z.string().optional().describe("State id from a prior tool call; auto-attaches the trace"),
|
|
372
|
+
trace: z.enum(["full", "summary", "none"]).optional().describe('Trace depth \u2014 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.'),
|
|
373
|
+
suggested_owner: z.enum(["synthesis", "retrieval", "validator", "chunk-corpus", "mcp-protocol", "unknown"]).optional().describe("Best-guess owner for triage"),
|
|
374
|
+
tags: z.array(z.string()).optional().describe("Free-form tags for filtering")
|
|
414
375
|
},
|
|
415
376
|
async ({ type, severity, title, body, state_id, trace, suggested_owner, tags }) => {
|
|
416
377
|
try {
|
|
@@ -419,18 +380,26 @@ Issue files land under \`.brain/audit-history/issues/\` (immutable; resolution l
|
|
|
419
380
|
{
|
|
420
381
|
cache: stateCache,
|
|
421
382
|
versionInfo: ENGINE_VERSION_INFO,
|
|
422
|
-
reporter:
|
|
383
|
+
reporter: "llm"
|
|
423
384
|
}
|
|
424
385
|
);
|
|
425
386
|
return {
|
|
426
|
-
content: [{ type:
|
|
387
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
427
388
|
};
|
|
428
389
|
} catch (e) {
|
|
390
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
429
391
|
return {
|
|
430
392
|
isError: true,
|
|
431
|
-
content: [{ type:
|
|
393
|
+
content: [{ type: "text", text: JSON.stringify({ error: err.message }, null, 2) }]
|
|
432
394
|
};
|
|
433
395
|
}
|
|
434
|
-
}
|
|
396
|
+
}
|
|
435
397
|
);
|
|
436
398
|
}
|
|
399
|
+
export {
|
|
400
|
+
ENGINE_VERSION_INFO,
|
|
401
|
+
autoReport,
|
|
402
|
+
registerSynthesisTools,
|
|
403
|
+
reportIssueImpl,
|
|
404
|
+
stateCache
|
|
405
|
+
};
|