@grampro/kit 2.7.2 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +233 -209
  2. package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
  3. package/.gbs/skills/gbs-components/references/forms.md +12 -0
  4. package/.gbs/skills/gbs-components/references/install.md +14 -3
  5. package/CHANGELOG.md +963 -861
  6. package/README.md +11 -3
  7. package/index.cjs +108 -123
  8. package/package.json +1 -1
  9. package/source/components/accordion/passport.json +1 -1
  10. package/source/components/alert/passport.json +1 -1
  11. package/source/components/avatar/passport.json +1 -1
  12. package/source/components/badge/passport.json +1 -1
  13. package/source/components/breadcrumb/passport.json +1 -1
  14. package/source/components/button/passport.json +1 -1
  15. package/source/components/card/passport.json +1 -1
  16. package/source/components/checkbox/passport.json +2 -2
  17. package/source/components/checkbox/passport.manual.json +1 -1
  18. package/source/components/checkbox/react/Checkbox.tsx +14 -0
  19. package/source/components/combobox/passport.json +3 -2
  20. package/source/components/combobox/passport.manual.json +1 -1
  21. package/source/components/combobox/react/MultiSelect.tsx +13 -0
  22. package/source/components/combobox/react/Select.tsx +13 -0
  23. package/source/components/combobox/react/surface.ts +70 -0
  24. package/source/components/data-grid/passport.json +6 -6
  25. package/source/components/data-grid/react/DataGrid.tsx +353 -351
  26. package/source/components/date-picker/passport.json +3 -2
  27. package/source/components/date-picker/passport.manual.json +1 -1
  28. package/source/components/date-picker/react/DatePicker.tsx +16 -2
  29. package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
  30. package/source/components/date-picker/react/surface.ts +68 -0
  31. package/source/components/dialog/passport.json +1 -1
  32. package/source/components/file-uploader/passport.json +1 -1
  33. package/source/components/input/passport.json +2 -2
  34. package/source/components/input/passport.manual.json +1 -1
  35. package/source/components/input/react/Input.tsx +21 -0
  36. package/source/components/menu/passport.json +1 -1
  37. package/source/components/modal/passport.json +1 -1
  38. package/source/components/number-input/passport.json +2 -2
  39. package/source/components/number-input/passport.manual.json +1 -1
  40. package/source/components/number-input/react/NumberInput.tsx +30 -0
  41. package/source/components/popover/passport.json +1 -1
  42. package/source/components/progress/passport.json +1 -1
  43. package/source/components/radio-group/passport.json +2 -2
  44. package/source/components/radio-group/passport.manual.json +1 -1
  45. package/source/components/radio-group/react/RadioGroup.tsx +33 -0
  46. package/source/components/shared/core/agent/field-executors.ts +173 -137
  47. package/source/components/shared/core/agent/index.ts +21 -0
  48. package/source/components/shared/core/agent/surface.ts +437 -0
  49. package/source/components/shared/core/agent/webmcp.ts +332 -328
  50. package/source/components/shared/core/agent/write.ts +62 -58
  51. package/source/components/shared/index.ts +7 -0
  52. package/source/components/shared/react/AgentSurface.tsx +145 -0
  53. package/source/components/shared/react/agentSurfaceContext.ts +92 -0
  54. package/source/components/shared/react/useFieldAgent.ts +62 -88
  55. package/source/components/shared/version.json +4 -4
  56. package/source/components/shared/version.ts +6 -6
  57. package/source/components/skeleton/passport.json +1 -1
  58. package/source/components/spinner/passport.json +1 -1
  59. package/source/components/switch/passport.json +2 -2
  60. package/source/components/switch/passport.manual.json +1 -1
  61. package/source/components/switch/react/Switch.tsx +16 -0
  62. package/source/components/tabs/passport.json +1 -1
  63. package/source/components/textarea/passport.json +2 -2
  64. package/source/components/textarea/passport.manual.json +1 -1
  65. package/source/components/textarea/react/Textarea.tsx +16 -0
  66. package/source/components/toaster/passport.json +1 -1
  67. package/source/components/tooltip/passport.json +1 -1
  68. package/tools/doctor/baseline.cjs +5 -4
  69. package/tools/mcp/server.cjs +13 -1
  70. package/tools/passport/extract.cjs +20 -65
  71. package/tools/passport/hash.cjs +18 -1
  72. package/tools/passport/index.cjs +4 -2
  73. package/tools/passport/typescript.cjs +220 -0
  74. package/tools/prompt.cjs +205 -0
  75. package/tools/ui.cjs +67 -0
@@ -1,328 +1,332 @@
1
- /*
2
- * WebMCP projection: exposing an already-validated command boundary to
3
- * whatever agent is driving the browser.
4
- *
5
- * This is an adapter and nothing else. It owns no schema, no semantics and no
6
- * execution path. The chain stays:
7
- *
8
- * component source → passport → runtime contract → intent schema
9
- * → [this file] → the component's validator → its executors → the component
10
- *
11
- * Nothing here knows about a grid. A component's agent satisfies
12
- * `ProjectableAgent` structurally, and everything component-specific arrives
13
- * through `describe` and `summarise`.
14
- *
15
- * Why one tool and not one per operation: the operation list is not the
16
- * agent's interface — the generated intent schema is, because that is where
17
- * the contract and the validation layers already meet. Twenty-one operations
18
- * exposed separately would be twenty-one schemas to keep in step with a
19
- * contract that already describes itself.
20
- *
21
- * Surface notes, each of which cost a wrong run to learn:
22
- *
23
- * - It is `document.modelContext`. `navigator.modelContext` was the early
24
- * spelling and is deprecated since Chromium 150.
25
- * - Chrome hands `inputSchema` to the agent as a JSON *string*, and returns
26
- * a tool result as a JSON string. That is the browser's wire format, not
27
- * something to pre-serialise here.
28
- * - It needs `--enable-features=WebMCPTesting`, the
29
- * `chrome://flags/#enable-webmcp-testing` flag, or an origin-trial token.
30
- *
31
- * The specification is a Draft Community Group Report and is not on the W3C
32
- * standards track, so this module is deliberately small and replaceable.
33
- */
34
-
35
- import type { ConfirmRequest, JsonSchema, ValidationIssue, ValidationLayer } from "./types";
36
-
37
- // ----------------------------------------------------------- browser surface
38
-
39
- /** One command the validator accepted, in the form this projection reports. */
40
- export interface ProjectedCommand {
41
- readonly intent: unknown;
42
- readonly explain: { readonly summary: string };
43
- }
44
-
45
- /**
46
- * What the validator answers. Mirrors the component-level execution result;
47
- * a component may carry extra fields, which are ignored here.
48
- */
49
- export type ProjectedOutcome =
50
- | { status: "done"; commands: readonly ProjectedCommand[]; warnings: readonly ValidationIssue[] }
51
- | {
52
- status: "needs-confirmation";
53
- commands: readonly ProjectedCommand[];
54
- warnings: readonly ValidationIssue[];
55
- confirm: ConfirmRequest;
56
- }
57
- | {
58
- status: "rejected";
59
- reason: string;
60
- code: string;
61
- layer: ValidationLayer;
62
- suggestion?: string;
63
- }
64
- | { status: "clarify"; question: string; options?: readonly string[] }
65
- | { status: "declined"; reason: string };
66
-
67
- /**
68
- * The part of a component's agent this projection uses.
69
- *
70
- * Structural on purpose: a DataGrid agent, a DatePicker agent or a test double
71
- * satisfies it without importing anything from here.
72
- */
73
- export interface ProjectableAgent {
74
- /** The generated per-instance schema for one intent. */
75
- schema(): JsonSchema;
76
- /** Validate without changing anything. */
77
- validate(intent: unknown): ProjectedOutcome;
78
- execute(intent: unknown, options?: { confirm?: boolean }): Promise<ProjectedOutcome>;
79
- }
80
-
81
- /** The slice of the WebMCP surface this projection touches. */
82
- export interface ModelContext {
83
- registerTool(descriptor: {
84
- name: string;
85
- description: string;
86
- inputSchema: unknown;
87
- execute(input: unknown): Promise<unknown>;
88
- }): void | Promise<void>;
89
- getTools?(): unknown;
90
- executeTool?(tool: unknown, input: unknown): Promise<unknown>;
91
- }
92
-
93
- declare global {
94
- interface Document {
95
- modelContext?: ModelContext;
96
- }
97
- }
98
-
99
- // -------------------------------------------------------------------- result
100
-
101
- /** What the agent gets back, before the MCP content envelope is put round it. */
102
- export interface ToolResultPayload extends Record<string, unknown> {
103
- ok: boolean;
104
- status: string;
105
- message: string;
106
- }
107
-
108
- export interface ToolCallLog {
109
- readonly input: unknown;
110
- readonly result: ToolResultPayload;
111
- }
112
-
113
- export interface RegistrationResult {
114
- registered: boolean;
115
- toolName: string;
116
- /** Why not, when `registered` is false. Safe to show a developer. */
117
- reason?: string;
118
- /** The schema handed to the agent, for inspection in a test or a UI. */
119
- inputSchema?: JsonSchema;
120
- }
121
-
122
- export interface RegisterAgentToolOptions<TAgent extends ProjectableAgent> {
123
- /** The tool name the agent sees. One per page. */
124
- name: string;
125
- /** Natural-language description, built from the live contract. */
126
- describe: (agent: TAgent) => string;
127
- /** Extra fields merged into a successful result — row counts, undo state. */
128
- summarise?: (agent: TAgent) => Record<string, unknown>;
129
- /** How many operations may be sent in one call. Default 8. */
130
- maxIntents?: number;
131
- /** Description for the `intents` array itself. */
132
- intentsDescription?: string;
133
- /** Observe every call, for a UI log or a test. Must not throw. */
134
- onCall?: (entry: ToolCallLog) => void;
135
- /**
136
- * Where to register. Defaults to `document.modelContext`. Supplying one is
137
- * for tests and for hosts that polyfill the surface elsewhere.
138
- */
139
- modelContext?: ModelContext;
140
- }
141
-
142
- const UNAVAILABLE =
143
- "document.modelContext is unavailable. Chrome needs --enable-features=WebMCPTesting, " +
144
- "the chrome://flags/#enable-webmcp-testing flag, or an origin-trial token.";
145
-
146
- /**
147
- * Whether this page can register WebMCP tools at all.
148
- *
149
- * Guards `document` rather than assuming it: this module is imported by code
150
- * that also runs during server rendering, where touching `document` throws.
151
- */
152
- export function webmcpAvailable(modelContext?: ModelContext): boolean {
153
- const target = modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
154
- return typeof target?.registerTool === "function";
155
- }
156
-
157
- /**
158
- * The tool's input schema, derived from the live contract.
159
- *
160
- * Not hand-written. `agent.schema()` is the generated per-instance schema, so
161
- * a tool registered against a component with different columns, operators or
162
- * policy advertises different arguments automatically.
163
- */
164
- export function buildToolInputSchema(
165
- agent: ProjectableAgent,
166
- options: { maxIntents?: number; intentsDescription?: string } = {},
167
- ): JsonSchema {
168
- return {
169
- type: "object",
170
- additionalProperties: false,
171
- required: ["intents"],
172
- properties: {
173
- intents: {
174
- type: "array",
175
- minItems: 1,
176
- maxItems: options.maxIntents ?? 8,
177
- description:
178
- options.intentsDescription ??
179
- "Operations to apply in order. Several filters on different fields combine with AND.",
180
- items: agent.schema(),
181
- },
182
- },
183
- };
184
- }
185
-
186
- /**
187
- * Registers one tool, if the browser has WebMCP.
188
- *
189
- * `execute` runs the production pipeline and nothing else:
190
- *
191
- * validate → a refusal is returned as the validator wrote it
192
- * confirm → anything irreversible stops here and asks, rather than running
193
- * execute → only after validation passed
194
- *
195
- * There is no repair, no retry, and no path that exists only for WebMCP. An
196
- * agent sending something the component cannot do gets the same refusal, with
197
- * the same code and layer, that a form would have got.
198
- */
199
- export function registerAgentTool<TAgent extends ProjectableAgent>(
200
- agent: TAgent,
201
- options: RegisterAgentToolOptions<TAgent>,
202
- ): RegistrationResult {
203
- const modelContext =
204
- options.modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
205
-
206
- if (typeof modelContext?.registerTool !== "function") {
207
- return { registered: false, toolName: options.name, reason: UNAVAILABLE };
208
- }
209
-
210
- const inputSchema = buildToolInputSchema(agent, options);
211
-
212
- modelContext.registerTool({
213
- name: options.name,
214
- description: options.describe(agent),
215
- inputSchema,
216
-
217
- async execute(rawInput: unknown) {
218
- const intents = (rawInput as { intents?: unknown } | null | undefined)?.intents;
219
-
220
- const reply = (payload: ToolResultPayload) => {
221
- try {
222
- options.onCall?.({ input: rawInput, result: payload });
223
- } catch {
224
- /* A broken observer must not turn a good command into an error. */
225
- }
226
- return {
227
- // MCP's content shape, so an agent reading text gets something useful…
228
- content: [{ type: "text", text: payload.message }],
229
- // …and the structured result for anything that wants to inspect it.
230
- structuredContent: payload,
231
- isError: payload.ok === false,
232
- };
233
- };
234
-
235
- if (!Array.isArray(intents) || intents.length === 0) {
236
- return reply({
237
- ok: false,
238
- status: "rejected",
239
- code: "missing-intents",
240
- message: '"intents" must be a non-empty array of operations.',
241
- });
242
- }
243
-
244
- /* Layer one of the real pipeline. Nothing has touched the component yet. */
245
- let checked: ProjectedOutcome;
246
- try {
247
- checked = agent.validate(intents);
248
- } catch (error) {
249
- return reply({
250
- ok: false,
251
- status: "rejected",
252
- code: "validator-threw",
253
- message: error instanceof Error ? error.message : String(error),
254
- });
255
- }
256
-
257
- if (checked.status === "rejected") {
258
- return reply({
259
- ok: false,
260
- status: "rejected",
261
- code: checked.code,
262
- layer: checked.layer,
263
- suggestion: checked.suggestion,
264
- message: checked.reason,
265
- });
266
- }
267
-
268
- if (checked.status === "clarify") {
269
- return reply({
270
- ok: false,
271
- status: "clarify",
272
- message: checked.question,
273
- options: checked.options,
274
- });
275
- }
276
-
277
- if (checked.status === "declined") {
278
- return reply({ ok: false, status: "declined", message: checked.reason });
279
- }
280
-
281
- if (checked.status === "needs-confirmation") {
282
- /*
283
- * An agent does not get to skip a confirmation a person would see. The
284
- * component is left untouched and the request is handed back.
285
- */
286
- return reply({
287
- ok: false,
288
- status: "needs-confirmation",
289
- code: checked.confirm.code,
290
- message: checked.confirm.message,
291
- explain: checked.commands.map((command) => command.explain.summary),
292
- });
293
- }
294
-
295
- let run: ProjectedOutcome;
296
- try {
297
- run = await agent.execute(intents);
298
- } catch (error) {
299
- return reply({
300
- ok: false,
301
- status: "failed",
302
- code: "executor-threw",
303
- message: error instanceof Error ? error.message : String(error),
304
- });
305
- }
306
-
307
- if (run.status !== "done") {
308
- return reply({
309
- ok: false,
310
- status: run.status,
311
- message:
312
- "reason" in run ? run.reason : "question" in run ? run.question : run.status,
313
- });
314
- }
315
-
316
- return reply({
317
- ok: true,
318
- status: "done",
319
- message: run.commands.map((command) => command.explain.summary).join("; "),
320
- applied: run.commands.map((command) => command.intent),
321
- warnings: run.warnings.map((warning) => `${warning.code}: ${warning.message}`),
322
- ...(options.summarise?.(agent) ?? {}),
323
- });
324
- },
325
- });
326
-
327
- return { registered: true, toolName: options.name, inputSchema };
328
- }
1
+ /*
2
+ * WebMCP projection: exposing an already-validated command boundary to
3
+ * whatever agent is driving the browser.
4
+ *
5
+ * This is an adapter and nothing else. It owns no schema, no semantics and no
6
+ * execution path. The chain stays:
7
+ *
8
+ * component source → passport → runtime contract → intent schema
9
+ * → [this file] → the component's validator → its executors → the component
10
+ *
11
+ * Nothing here knows about a grid. A component's agent satisfies
12
+ * `ProjectableAgent` structurally, and everything component-specific arrives
13
+ * through `describe` and `summarise`.
14
+ *
15
+ * Why one tool and not one per operation: the operation list is not the
16
+ * agent's interface — the generated intent schema is, because that is where
17
+ * the contract and the validation layers already meet. Twenty-one operations
18
+ * exposed separately would be twenty-one schemas to keep in step with a
19
+ * contract that already describes itself.
20
+ *
21
+ * Surface notes, each of which cost a wrong run to learn:
22
+ *
23
+ * - It is `document.modelContext`. `navigator.modelContext` was the early
24
+ * spelling and is deprecated since Chromium 150.
25
+ * - Chrome hands `inputSchema` to the agent as a JSON *string*, and returns
26
+ * a tool result as a JSON string. That is the browser's wire format, not
27
+ * something to pre-serialise here.
28
+ * - It needs `--enable-features=WebMCPTesting`, the
29
+ * `chrome://flags/#enable-webmcp-testing` flag, or an origin-trial token.
30
+ *
31
+ * The specification is a Draft Community Group Report and is not on the W3C
32
+ * standards track, so this module is deliberately small and replaceable.
33
+ */
34
+
35
+ import type { ConfirmRequest, JsonSchema, ValidationIssue, ValidationLayer } from "./types";
36
+
37
+ // ----------------------------------------------------------- browser surface
38
+
39
+ /** One command the validator accepted, in the form this projection reports. */
40
+ export interface ProjectedCommand {
41
+ readonly intent: unknown;
42
+ readonly explain: { readonly summary: string };
43
+ }
44
+
45
+ /**
46
+ * What the validator answers. Mirrors the component-level execution result;
47
+ * a component may carry extra fields, which are ignored here.
48
+ */
49
+ export type ProjectedOutcome =
50
+ | { status: "done"; commands: readonly ProjectedCommand[]; warnings: readonly ValidationIssue[] }
51
+ | {
52
+ status: "needs-confirmation";
53
+ commands: readonly ProjectedCommand[];
54
+ warnings: readonly ValidationIssue[];
55
+ confirm: ConfirmRequest;
56
+ }
57
+ | {
58
+ status: "rejected";
59
+ reason: string;
60
+ code: string;
61
+ layer: ValidationLayer;
62
+ suggestion?: string;
63
+ }
64
+ | { status: "clarify"; question: string; options?: readonly string[] }
65
+ | { status: "declined"; reason: string };
66
+
67
+ /**
68
+ * The part of a component's agent this projection uses.
69
+ *
70
+ * Structural on purpose: a DataGrid agent, a DatePicker agent or a test double
71
+ * satisfies it without importing anything from here.
72
+ */
73
+ export interface ProjectableAgent {
74
+ /** The generated per-instance schema for one intent. */
75
+ schema(): JsonSchema;
76
+ /** Validate without changing anything. */
77
+ validate(intent: unknown): ProjectedOutcome;
78
+ execute(intent: unknown, options?: { confirm?: boolean }): Promise<ProjectedOutcome>;
79
+ }
80
+
81
+ /** The slice of the WebMCP surface this projection touches. */
82
+ export interface ModelContext {
83
+ registerTool(descriptor: {
84
+ name: string;
85
+ description: string;
86
+ inputSchema: unknown;
87
+ execute(input: unknown): Promise<unknown>;
88
+ }): void | Promise<void>;
89
+ getTools?(): unknown;
90
+ executeTool?(tool: unknown, input: unknown): Promise<unknown>;
91
+ }
92
+
93
+ declare global {
94
+ interface Document {
95
+ modelContext?: ModelContext;
96
+ }
97
+ }
98
+
99
+ // -------------------------------------------------------------------- result
100
+
101
+ /** What the agent gets back, before the MCP content envelope is put round it. */
102
+ export interface ToolResultPayload extends Record<string, unknown> {
103
+ ok: boolean;
104
+ status: string;
105
+ message: string;
106
+ }
107
+
108
+ export interface ToolCallLog {
109
+ readonly input: unknown;
110
+ readonly result: ToolResultPayload;
111
+ }
112
+
113
+ export interface RegistrationResult {
114
+ registered: boolean;
115
+ toolName: string;
116
+ /** Why not, when `registered` is false. Safe to show a developer. */
117
+ reason?: string;
118
+ /** The schema handed to the agent, for inspection in a test or a UI. */
119
+ inputSchema?: JsonSchema;
120
+ }
121
+
122
+ export interface RegisterAgentToolOptions<TAgent extends ProjectableAgent> {
123
+ /** The tool name the agent sees. One per page. */
124
+ name: string;
125
+ /** Natural-language description, built from the live contract. */
126
+ describe: (agent: TAgent) => string;
127
+ /** Extra fields merged into a successful result — row counts, undo state. */
128
+ summarise?: (agent: TAgent) => Record<string, unknown>;
129
+ /** How many operations may be sent in one call. Default 8. */
130
+ maxIntents?: number;
131
+ /** Description for the `intents` array itself. */
132
+ intentsDescription?: string;
133
+ /** Observe every call, for a UI log or a test. Must not throw. */
134
+ onCall?: (entry: ToolCallLog) => void;
135
+ /**
136
+ * Where to register. Defaults to `document.modelContext`. Supplying one is
137
+ * for tests and for hosts that polyfill the surface elsewhere.
138
+ */
139
+ modelContext?: ModelContext;
140
+ }
141
+
142
+ const UNAVAILABLE =
143
+ "document.modelContext is unavailable. Chrome needs --enable-features=WebMCPTesting, " +
144
+ "the chrome://flags/#enable-webmcp-testing flag, or an origin-trial token.";
145
+
146
+ /**
147
+ * Whether this page can register WebMCP tools at all.
148
+ *
149
+ * Guards `document` rather than assuming it: this module is imported by code
150
+ * that also runs during server rendering, where touching `document` throws.
151
+ */
152
+ export function webmcpAvailable(modelContext?: ModelContext): boolean {
153
+ const target = modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
154
+ return typeof target?.registerTool === "function";
155
+ }
156
+
157
+ /**
158
+ * The tool's input schema, derived from the live contract.
159
+ *
160
+ * Not hand-written. `agent.schema()` is the generated per-instance schema, so
161
+ * a tool registered against a component with different columns, operators or
162
+ * policy advertises different arguments automatically.
163
+ */
164
+ export function buildToolInputSchema(
165
+ agent: ProjectableAgent,
166
+ options: { maxIntents?: number; intentsDescription?: string } = {},
167
+ ): JsonSchema {
168
+ return {
169
+ type: "object",
170
+ additionalProperties: false,
171
+ required: ["intents"],
172
+ properties: {
173
+ intents: {
174
+ type: "array",
175
+ minItems: 1,
176
+ maxItems: options.maxIntents ?? 8,
177
+ description:
178
+ options.intentsDescription ??
179
+ "Operations to apply in order. Several filters on different fields combine with AND.",
180
+ items: agent.schema(),
181
+ },
182
+ },
183
+ };
184
+ }
185
+
186
+ /**
187
+ * The production pipeline for one batch of intents, independent of how the
188
+ * request arrived.
189
+ *
190
+ * validate → a refusal is returned as the validator wrote it
191
+ * confirm → anything irreversible stops here and asks, rather than running
192
+ * execute → only after validation passed
193
+ *
194
+ * There is no repair, no retry, and no path that exists only for one caller.
195
+ * A WebMCP tool for one component, the page-wide AgentSurface tool, and an
196
+ * app's own assistant all go through this, so an agent sending something a
197
+ * component cannot do gets the same refusal, with the same code and layer,
198
+ * whichever door it used — and the same refusal a form would have got.
199
+ */
200
+ export async function runProjected(
201
+ agent: ProjectableAgent,
202
+ intents: unknown,
203
+ options: { summarise?: () => Record<string, unknown> } = {},
204
+ ): Promise<ToolResultPayload> {
205
+ if (!Array.isArray(intents) || intents.length === 0) {
206
+ return {
207
+ ok: false,
208
+ status: "rejected",
209
+ code: "missing-intents",
210
+ message: '"intents" must be a non-empty array of operations.',
211
+ };
212
+ }
213
+
214
+ /* Layer one of the real pipeline. Nothing has touched the component yet. */
215
+ let checked: ProjectedOutcome;
216
+ try {
217
+ checked = agent.validate(intents);
218
+ } catch (error) {
219
+ return {
220
+ ok: false,
221
+ status: "rejected",
222
+ code: "validator-threw",
223
+ message: error instanceof Error ? error.message : String(error),
224
+ };
225
+ }
226
+
227
+ if (checked.status === "rejected") {
228
+ return {
229
+ ok: false,
230
+ status: "rejected",
231
+ code: checked.code,
232
+ layer: checked.layer,
233
+ suggestion: checked.suggestion,
234
+ message: checked.reason,
235
+ };
236
+ }
237
+ if (checked.status === "clarify") {
238
+ return { ok: false, status: "clarify", message: checked.question, options: checked.options };
239
+ }
240
+ if (checked.status === "declined") {
241
+ return { ok: false, status: "declined", message: checked.reason };
242
+ }
243
+ if (checked.status === "needs-confirmation") {
244
+ /*
245
+ * An agent does not get to skip a confirmation a person would see. The
246
+ * component is left untouched and the request is handed back.
247
+ */
248
+ return {
249
+ ok: false,
250
+ status: "needs-confirmation",
251
+ code: checked.confirm.code,
252
+ message: checked.confirm.message,
253
+ explain: checked.commands.map((command) => command.explain.summary),
254
+ };
255
+ }
256
+
257
+ let run: ProjectedOutcome;
258
+ try {
259
+ run = await agent.execute(intents);
260
+ } catch (error) {
261
+ return {
262
+ ok: false,
263
+ status: "failed",
264
+ code: "executor-threw",
265
+ message: error instanceof Error ? error.message : String(error),
266
+ };
267
+ }
268
+
269
+ if (run.status !== "done") {
270
+ return {
271
+ ok: false,
272
+ status: run.status,
273
+ message: "reason" in run ? run.reason : "question" in run ? run.question : run.status,
274
+ };
275
+ }
276
+
277
+ return {
278
+ ok: true,
279
+ status: "done",
280
+ message: run.commands.map((command) => command.explain.summary).join("; "),
281
+ applied: run.commands.map((command) => command.intent),
282
+ warnings: run.warnings.map((warning) => `${warning.code}: ${warning.message}`),
283
+ ...(options.summarise?.() ?? {}),
284
+ };
285
+ }
286
+
287
+ /** A payload in MCP's tool-result shape: text for a reader, structure for code. */
288
+ export function toToolResult(payload: ToolResultPayload) {
289
+ return {
290
+ content: [{ type: "text", text: payload.message }],
291
+ structuredContent: payload,
292
+ isError: payload.ok === false,
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Registers one tool for one component's agent, if the browser has WebMCP.
298
+ * Every call runs `runProjected`; see there for what that guarantees.
299
+ */
300
+ export function registerAgentTool<TAgent extends ProjectableAgent>(
301
+ agent: TAgent,
302
+ options: RegisterAgentToolOptions<TAgent>,
303
+ ): RegistrationResult {
304
+ const modelContext =
305
+ options.modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
306
+
307
+ if (typeof modelContext?.registerTool !== "function") {
308
+ return { registered: false, toolName: options.name, reason: UNAVAILABLE };
309
+ }
310
+
311
+ const inputSchema = buildToolInputSchema(agent, options);
312
+
313
+ modelContext.registerTool({
314
+ name: options.name,
315
+ description: options.describe(agent),
316
+ inputSchema,
317
+ async execute(rawInput: unknown) {
318
+ const intents = (rawInput as { intents?: unknown } | null | undefined)?.intents;
319
+ const payload = await runProjected(agent, intents, {
320
+ summarise: options.summarise ? () => options.summarise!(agent) : undefined,
321
+ });
322
+ try {
323
+ options.onCall?.({ input: rawInput, result: payload });
324
+ } catch {
325
+ /* A broken observer must not turn a good command into an error. */
326
+ }
327
+ return toToolResult(payload);
328
+ },
329
+ });
330
+
331
+ return { registered: true, toolName: options.name, inputSchema };
332
+ }