gbs-add-block 2.0.4 → 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.
- package/.gbs/skills/gbs-components/SKILL.md +190 -134
- package/.gbs/skills/gbs-components/references/install.md +25 -3
- package/.gbs/skills/gbs-components/references/styling.md +246 -201
- package/CHANGELOG.md +74 -0
- package/README.md +157 -11
- package/index.cjs +212 -3
- package/package.json +41 -10
- package/schema/passport-v1.schema.json +204 -0
- package/source/beta-components/accordion/passport.json +259 -0
- package/source/beta-components/accordion/styles.css +207 -208
- package/source/beta-components/alert/passport.json +250 -0
- package/source/beta-components/alert/styles.css +154 -155
- package/source/beta-components/avatar/passport.json +294 -0
- package/source/beta-components/avatar/styles.css +201 -203
- package/source/beta-components/badge/passport.json +332 -0
- package/source/beta-components/badge/styles.css +203 -204
- package/source/beta-components/breadcrumb/passport.json +243 -0
- package/source/beta-components/breadcrumb/styles.css +138 -139
- package/source/beta-components/button/passport.json +402 -0
- package/source/beta-components/button/passport.manual.json +31 -0
- package/source/beta-components/button/styles.css +232 -233
- package/source/beta-components/card/passport.json +337 -0
- package/source/beta-components/card/styles.css +230 -231
- package/source/beta-components/checkbox/passport.json +456 -0
- package/source/beta-components/checkbox/styles.css +211 -212
- package/source/beta-components/combobox/passport.json +456 -0
- package/source/beta-components/combobox/styles.css +419 -417
- package/source/beta-components/data-grid/agent/coerce.ts +368 -0
- package/source/beta-components/data-grid/agent/contract.ts +410 -0
- package/source/beta-components/data-grid/agent/dataset.ts +92 -0
- package/source/beta-components/data-grid/agent/engine.ts +470 -0
- package/source/beta-components/data-grid/agent/executors.ts +155 -0
- package/source/beta-components/data-grid/agent/index.ts +79 -0
- package/source/beta-components/data-grid/agent/intent.ts +324 -0
- package/source/beta-components/data-grid/agent/operations.ts +335 -0
- package/source/beta-components/data-grid/agent/validate.ts +630 -0
- package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
- package/source/beta-components/data-grid/index.ts +14 -7
- package/source/beta-components/data-grid/passport.json +1051 -0
- package/source/beta-components/data-grid/passport.manual.json +255 -0
- package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
- package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
- package/source/beta-components/data-grid/styles.css +874 -717
- package/source/beta-components/date-picker/passport.json +407 -0
- package/source/beta-components/date-picker/styles.css +445 -446
- package/source/beta-components/dialog/passport.json +344 -0
- package/source/beta-components/dialog/styles.css +280 -278
- package/source/beta-components/file-uploader/passport.json +518 -0
- package/source/beta-components/file-uploader/styles.css +394 -395
- package/source/beta-components/input/passport.json +536 -0
- package/source/beta-components/input/styles.css +295 -296
- package/source/beta-components/menu/passport.json +322 -0
- package/source/beta-components/menu/styles.css +224 -222
- package/source/beta-components/modal/passport.json +289 -0
- package/source/beta-components/modal/styles.css +241 -239
- package/source/beta-components/number-input/passport.json +541 -0
- package/source/beta-components/number-input/styles.css +230 -231
- package/source/beta-components/popover/passport.json +238 -0
- package/source/beta-components/popover/styles.css +148 -146
- package/source/beta-components/progress/passport.json +270 -0
- package/source/beta-components/progress/styles.css +200 -202
- package/source/beta-components/radio-group/passport.json +477 -0
- package/source/beta-components/radio-group/styles.css +241 -242
- package/source/beta-components/shared/core/agent/adapter.ts +65 -0
- package/source/beta-components/shared/core/agent/history.ts +120 -0
- package/source/beta-components/shared/core/agent/index.ts +46 -0
- package/source/beta-components/shared/core/agent/numbers.ts +217 -0
- package/source/beta-components/shared/core/agent/schema.ts +180 -0
- package/source/beta-components/shared/core/agent/types.ts +169 -0
- package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
- package/source/beta-components/shared/index.ts +9 -0
- package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
- package/source/beta-components/shared/react/useAskAgent.ts +217 -0
- package/source/beta-components/shared/styles.css +79 -0
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/passport.json +251 -0
- package/source/beta-components/skeleton/styles.css +185 -186
- package/source/beta-components/spinner/passport.json +245 -0
- package/source/beta-components/spinner/styles.css +145 -146
- package/source/beta-components/switch/passport.json +421 -0
- package/source/beta-components/switch/styles.css +194 -196
- package/source/beta-components/tabs/passport.json +315 -0
- package/source/beta-components/tabs/styles.css +234 -235
- package/source/beta-components/textarea/passport.json +382 -0
- package/source/beta-components/textarea/styles.css +158 -159
- package/source/beta-components/toaster/passport.json +221 -0
- package/source/beta-components/toaster/styles.css +282 -283
- package/source/beta-components/tooltip/passport.json +170 -0
- package/source/beta-components/tooltip/styles.css +71 -72
- package/tools/env.cjs +61 -0
- package/tools/passport/cli.cjs +79 -0
- package/tools/passport/extract.cjs +493 -0
- package/tools/passport/index.cjs +185 -0
- package/tools/passport/merge.cjs +131 -0
- package/tools/passport/policy.cjs +65 -0
- package/tools/passport/validate.cjs +277 -0
- package/tools/ts-require.cjs +79 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
- package/source/beta-components/alert/__tests__/core.test.ts +0 -17
- package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
- package/source/beta-components/badge/__tests__/core.test.ts +0 -46
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
- package/source/beta-components/button/__tests__/core.test.ts +0 -31
- package/source/beta-components/card/__tests__/core.test.ts +0 -57
- package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
- package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
- package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
- package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
- package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
- package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
- package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
- package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
- package/source/beta-components/input/__tests__/core.test.ts +0 -75
- package/source/beta-components/menu/__tests__/core.test.ts +0 -120
- package/source/beta-components/modal/__tests__/core.test.ts +0 -55
- package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
- package/source/beta-components/progress/__tests__/core.test.ts +0 -56
- package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
- package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
- package/source/beta-components/shared/__tests__/core.test.ts +0 -55
- package/source/beta-components/shared/__tests__/position.test.ts +0 -143
- package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
- package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
- package/source/beta-components/switch/__tests__/core.test.ts +0 -64
- package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
- package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
- package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
- 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
|
+
}
|