gbs-add-block 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +190 -134
  2. package/.gbs/skills/gbs-components/references/install.md +25 -3
  3. package/.gbs/skills/gbs-components/references/styling.md +246 -207
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +154 -23
  6. package/index.cjs +212 -3
  7. package/package.json +41 -10
  8. package/schema/passport-v1.schema.json +204 -0
  9. package/source/beta-components/accordion/passport.json +259 -0
  10. package/source/beta-components/accordion/styles.css +207 -208
  11. package/source/beta-components/alert/passport.json +250 -0
  12. package/source/beta-components/alert/styles.css +154 -155
  13. package/source/beta-components/avatar/passport.json +294 -0
  14. package/source/beta-components/avatar/styles.css +225 -226
  15. package/source/beta-components/badge/passport.json +332 -0
  16. package/source/beta-components/badge/styles.css +203 -204
  17. package/source/beta-components/breadcrumb/passport.json +243 -0
  18. package/source/beta-components/breadcrumb/styles.css +138 -140
  19. package/source/beta-components/button/passport.json +402 -0
  20. package/source/beta-components/button/passport.manual.json +31 -0
  21. package/source/beta-components/button/styles.css +232 -234
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -232
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -213
  26. package/source/beta-components/combobox/passport.json +456 -0
  27. package/source/beta-components/combobox/styles.css +419 -417
  28. package/source/beta-components/data-grid/agent/coerce.ts +368 -0
  29. package/source/beta-components/data-grid/agent/contract.ts +410 -0
  30. package/source/beta-components/data-grid/agent/dataset.ts +92 -0
  31. package/source/beta-components/data-grid/agent/engine.ts +470 -0
  32. package/source/beta-components/data-grid/agent/executors.ts +155 -0
  33. package/source/beta-components/data-grid/agent/index.ts +79 -0
  34. package/source/beta-components/data-grid/agent/intent.ts +324 -0
  35. package/source/beta-components/data-grid/agent/operations.ts +335 -0
  36. package/source/beta-components/data-grid/agent/validate.ts +630 -0
  37. package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
  38. package/source/beta-components/data-grid/index.ts +14 -7
  39. package/source/beta-components/data-grid/passport.json +1051 -0
  40. package/source/beta-components/data-grid/passport.manual.json +255 -0
  41. package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
  42. package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
  43. package/source/beta-components/data-grid/styles.css +874 -716
  44. package/source/beta-components/date-picker/passport.json +407 -0
  45. package/source/beta-components/date-picker/styles.css +445 -446
  46. package/source/beta-components/dialog/passport.json +344 -0
  47. package/source/beta-components/dialog/styles.css +280 -279
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -396
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -297
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -223
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -240
  56. package/source/beta-components/number-input/passport.json +541 -0
  57. package/source/beta-components/number-input/styles.css +230 -231
  58. package/source/beta-components/popover/passport.json +238 -0
  59. package/source/beta-components/popover/styles.css +148 -147
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -201
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +269 -270
  64. package/source/beta-components/shared/core/agent/adapter.ts +65 -0
  65. package/source/beta-components/shared/core/agent/history.ts +120 -0
  66. package/source/beta-components/shared/core/agent/index.ts +46 -0
  67. package/source/beta-components/shared/core/agent/numbers.ts +217 -0
  68. package/source/beta-components/shared/core/agent/schema.ts +180 -0
  69. package/source/beta-components/shared/core/agent/types.ts +169 -0
  70. package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
  71. package/source/beta-components/shared/index.ts +9 -0
  72. package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
  73. package/source/beta-components/shared/react/useAskAgent.ts +217 -0
  74. package/source/beta-components/shared/styles.css +79 -0
  75. package/source/beta-components/shared/version.json +4 -4
  76. package/source/beta-components/shared/version.ts +6 -6
  77. package/source/beta-components/skeleton/passport.json +251 -0
  78. package/source/beta-components/skeleton/styles.css +185 -187
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +173 -174
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +227 -229
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +263 -264
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -160
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -282
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -73
  91. package/tools/env.cjs +61 -0
  92. package/tools/passport/cli.cjs +79 -0
  93. package/tools/passport/extract.cjs +493 -0
  94. package/tools/passport/index.cjs +185 -0
  95. package/tools/passport/merge.cjs +131 -0
  96. package/tools/passport/policy.cjs +65 -0
  97. package/tools/passport/validate.cjs +277 -0
  98. package/tools/ts-require.cjs +79 -0
  99. package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
  100. package/source/beta-components/alert/__tests__/core.test.ts +0 -17
  101. package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
  102. package/source/beta-components/badge/__tests__/core.test.ts +0 -46
  103. package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
  104. package/source/beta-components/button/__tests__/core.test.ts +0 -31
  105. package/source/beta-components/card/__tests__/core.test.ts +0 -57
  106. package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
  107. package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
  108. package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
  109. package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
  110. package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
  111. package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
  112. package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
  113. package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
  114. package/source/beta-components/input/__tests__/core.test.ts +0 -75
  115. package/source/beta-components/menu/__tests__/core.test.ts +0 -120
  116. package/source/beta-components/modal/__tests__/core.test.ts +0 -55
  117. package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
  118. package/source/beta-components/progress/__tests__/core.test.ts +0 -56
  119. package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
  120. package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
  121. package/source/beta-components/shared/__tests__/core.test.ts +0 -55
  122. package/source/beta-components/shared/__tests__/position.test.ts +0 -143
  123. package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
  124. package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
  125. package/source/beta-components/switch/__tests__/core.test.ts +0 -64
  126. package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
  127. package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
  128. package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
  129. package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
@@ -0,0 +1,169 @@
1
+ /*
2
+ * The vocabulary an agent-operable component shares with its callers.
3
+ *
4
+ * Four things are kept apart on purpose, because they change for different
5
+ * reasons and at different times:
6
+ *
7
+ * OperationDefinition what a component knows how to do (static, from the passport)
8
+ * RuntimeContract what this instance can do, right now (per instance, per render)
9
+ * Intent what the caller is asking for (untrusted input)
10
+ * OperationExecutor how this instance actually performs it (per application)
11
+ *
12
+ * The last one is the reason this file exists. An operation must not be
13
+ * welded to an imperative handle: a grid performs `filter` by calling
14
+ * `GridApi.setFilter`, while a controlled component would perform its
15
+ * equivalent by calling the host's `onChange`. Same operation, same schema,
16
+ * different executor. Nothing here mentions either.
17
+ */
18
+
19
+ // ---------------------------------------------------------------- JSON Schema
20
+
21
+ /**
22
+ * The slice of JSON Schema this library emits and checks. Deliberately small:
23
+ * it is what a constrained decoder and a validator both need, and no more.
24
+ */
25
+ export interface JsonSchema {
26
+ $schema?: string;
27
+ title?: string;
28
+ description?: string;
29
+ type?: JsonSchemaType | readonly JsonSchemaType[];
30
+ const?: unknown;
31
+ enum?: readonly unknown[];
32
+ properties?: Readonly<Record<string, JsonSchema>>;
33
+ required?: readonly string[];
34
+ additionalProperties?: boolean | JsonSchema;
35
+ items?: JsonSchema;
36
+ minItems?: number;
37
+ maxItems?: number;
38
+ minLength?: number;
39
+ maxLength?: number;
40
+ minimum?: number;
41
+ maximum?: number;
42
+ pattern?: string;
43
+ oneOf?: readonly JsonSchema[];
44
+ anyOf?: readonly JsonSchema[];
45
+ default?: unknown;
46
+ examples?: readonly unknown[];
47
+ }
48
+
49
+ export type JsonSchemaType =
50
+ | "object"
51
+ | "array"
52
+ | "string"
53
+ | "number"
54
+ | "integer"
55
+ | "boolean"
56
+ | "null";
57
+
58
+ // ----------------------------------------------------------------- operations
59
+
60
+ /**
61
+ * One thing a component can be asked to do. This is the static half of the
62
+ * contract and mirrors the component's `passport.json` `operations` block.
63
+ */
64
+ export interface OperationDefinition {
65
+ readonly name: string;
66
+ readonly summary: string;
67
+ /** JSON Schema for the operation's input, minus the `action` discriminator. */
68
+ readonly input?: JsonSchema;
69
+ /** Plain-language consequences, for a confirmation prompt. */
70
+ readonly effects?: readonly string[];
71
+ /** Whether undoing it is a matter of restoring the previous state. */
72
+ readonly reversible: boolean;
73
+ /** Leaves the page: downloads, prints, writes to the clipboard. */
74
+ readonly requiresConfirmation?: boolean;
75
+ /**
76
+ * The imperative method that happens to back this operation, when one does.
77
+ * Metadata only — never required. An operation performed through host state
78
+ * has no API method, and that is a normal case, not a gap.
79
+ */
80
+ readonly apiMethod?: string;
81
+ }
82
+
83
+ export interface ExecutionContext<TContract = unknown> {
84
+ readonly contract: TContract;
85
+ /** Report what would happen and change nothing. */
86
+ readonly dryRun: boolean;
87
+ readonly signal?: AbortSignal;
88
+ }
89
+
90
+ /**
91
+ * How one instance performs one operation.
92
+ *
93
+ * Implementations close over whatever they need — an imperative handle, a
94
+ * state setter, a transport — and the rest of the pipeline never learns which.
95
+ */
96
+ export interface OperationExecutor<TInput, TContract = unknown, TResult = void> {
97
+ readonly operation: string;
98
+ /** Set only when `execute` really does call that method. */
99
+ readonly apiMethod?: string;
100
+ execute(input: TInput, context: ExecutionContext<TContract>): TResult | Promise<TResult>;
101
+ }
102
+
103
+ /**
104
+ * An executor in a heterogeneous registry. `never` as the input type is sound
105
+ * here: a function taking a specific input is assignable to one taking
106
+ * `never`, so concrete executors slot in without a cast. Callers cast at the
107
+ * point of dispatch, where the validator has already proved the shape.
108
+ */
109
+ export type RegisteredExecutor<TContract = unknown> = OperationExecutor<never, TContract, unknown>;
110
+
111
+ export type ExecutorRegistry<TContract = unknown> = ReadonlyMap<string, RegisteredExecutor<TContract>>;
112
+
113
+ // ----------------------------------------------------------------- validation
114
+
115
+ export type ValidationLayer =
116
+ | "schema"
117
+ | "reference"
118
+ | "coercion"
119
+ | "policy"
120
+ | "plausibility";
121
+
122
+ export interface ValidationIssue {
123
+ layer: ValidationLayer;
124
+ /** Stable, machine-readable. Messages may be reworded; codes may not. */
125
+ code: string;
126
+ message: string;
127
+ /** JSON pointer-ish path into the intent, when the issue has a location. */
128
+ path?: string;
129
+ suggestion?: string;
130
+ }
131
+
132
+ /** Something the caller should see and agree to before the command runs. */
133
+ export interface ConfirmRequest {
134
+ code: string;
135
+ message: string;
136
+ }
137
+
138
+ export type ValidationResult<TCommand> =
139
+ | {
140
+ ok: true;
141
+ command: TCommand;
142
+ /** Non-blocking: coercions applied, plausibility notes. */
143
+ warnings: ValidationIssue[];
144
+ confirm: ConfirmRequest | null;
145
+ }
146
+ | {
147
+ ok: false;
148
+ /** The issue that stopped it, flattened for display. */
149
+ reason: string;
150
+ code: string;
151
+ layer: ValidationLayer;
152
+ suggestion?: string;
153
+ issues: ValidationIssue[];
154
+ };
155
+
156
+ export const ok = <T>(
157
+ command: T,
158
+ warnings: ValidationIssue[] = [],
159
+ confirm: ConfirmRequest | null = null,
160
+ ): ValidationResult<T> => ({ ok: true, command, warnings, confirm });
161
+
162
+ export const fail = <T>(issue: ValidationIssue, issues: ValidationIssue[] = []): ValidationResult<T> => ({
163
+ ok: false,
164
+ reason: issue.message,
165
+ code: issue.code,
166
+ layer: issue.layer,
167
+ ...(issue.suggestion ? { suggestion: issue.suggestion } : {}),
168
+ issues: issues.length > 0 ? issues : [issue],
169
+ });
@@ -0,0 +1,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
+ * 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
+ }
@@ -6,3 +6,12 @@ export { AnchoredPopover, type AnchoredPopoverProps } from "./react/Popover";
6
6
  export { placePopover, type Align, type Placement, type Side } from "./core/position";
7
7
  export * as icons from "./react/icons";
8
8
  export { version as sharedVersion } from "./version";
9
+
10
+ // Agent runtime: the component-agnostic operation/validation/history layer.
11
+ export * from "./core/agent";
12
+ export {
13
+ GramproAIProvider,
14
+ useAgentAdapter,
15
+ type GramproAIProviderProps,
16
+ } from "./react/GramproAIProvider";
17
+ export { useAskAgent, type AskableAgent, type AskExecution, type UseAskAgent } from "./react/useAskAgent";
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
4
+ import type { AgentAdapter } from "../core/agent/adapter";
5
+
6
+ /*
7
+ * Where an application plugs its own language model in.
8
+ *
9
+ * Nothing below this provider knows which one it is. A component asks the
10
+ * context for an adapter; if there is none, the component renders exactly as
11
+ * it does today and its natural-language affordance is simply absent. That is
12
+ * the whole opt-in: no provider, no AI, no bytes, no network.
13
+ */
14
+
15
+ interface AIContextValue {
16
+ adapter: AgentAdapter | null;
17
+ /** Shown above the input, e.g. "Ask about these customers". */
18
+ placeholder?: string;
19
+ /** Example utterances offered as one-click suggestions. */
20
+ suggestions?: readonly string[];
21
+ }
22
+
23
+ const AIContext = createContext<AIContextValue>({ adapter: null });
24
+
25
+ export interface GramproAIProviderProps extends AIContextValue {
26
+ children: ReactNode;
27
+ }
28
+
29
+ export function GramproAIProvider({
30
+ adapter,
31
+ placeholder,
32
+ suggestions,
33
+ children,
34
+ }: GramproAIProviderProps) {
35
+ const value = useMemo(
36
+ () => ({ adapter, placeholder, suggestions }),
37
+ [adapter, placeholder, suggestions],
38
+ );
39
+ return <AIContext.Provider value={value}>{children}</AIContext.Provider>;
40
+ }
41
+
42
+ /**
43
+ * The adapter in scope, or null.
44
+ *
45
+ * Null is an ordinary case, not an error: a component asks, gets nothing, and
46
+ * renders without the affordance.
47
+ */
48
+ export function useAgentAdapter(): AIContextValue {
49
+ return useContext(AIContext);
50
+ }