@agentperf/react 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,49 +1,53 @@
1
- # @agentperf/react
2
-
3
- **Give your React app a second frontend — for AI agents.** Humans get your DOM;
4
- agents get a token-budgeted state snapshot and typed, schema-gated actions on
5
- the WebMCP standard (`document.modelContext`). One codebase, two doorways.
6
-
7
- ```bash
8
- npm i @agentperf/react zod
9
- ```
10
-
11
- ```tsx
12
- import { useAgentState, useAgentAction, AgentBoundary } from "@agentperf/react";
13
- import { z } from "zod";
14
-
15
- function Cart({ cart }) {
16
- // agents read this through one get_page_state tool, pruned to a token budget
17
- useAgentState("cart", cart);
18
-
19
- // agents act through a schema gate — invalid input never reaches your handler,
20
- // it gets a refusal naming the exact violated fields so it can self-correct
21
- useAgentAction("checkout", {
22
- description: "Pay for the items in the cart",
23
- input: z.object({ email: z.string().email() }),
24
- execute: ({ email }) => checkout(email)
25
- });
26
- }
27
- ```
28
-
29
- - **No WebMCP, no problem** — in browsers without `document.modelContext`
30
- everything no-ops and your app stays a normal human app. Add
31
- [`@mcp-b/global`](https://github.com/WebMCP-org/npm-packages) as a polyfill
32
- if you want tools everywhere.
33
- - **Structured refusals** — validation failures return the violated field
34
- paths and a fix-and-retry instruction, so agents correct their calls instead
35
- of corrupting state. Deterministic: same input, same verdict.
36
- - **Token budgeting** — `get_page_state` prunes long arrays and strings to a
37
- budget (default 1,000 tokens, `setAgentStateBudget` to change), because
38
- agents pay per token to read your page.
39
- - **`AgentBoundary`** scopes state keys and tool names the way your component
40
- tree scopes the UI.
41
- - **Read-only/destructive hints** (`readOnly`, `destructive`) map to WebMCP
42
- annotations; a reserved `price` field is inert today and becomes x402
43
- settlement in v2 without a breaking change.
44
-
45
- Works in Chrome 149+ (WebMCP origin trial / `chrome://flags/#enable-webmcp-testing`)
46
- and the ChatGPT desktop browser. Measure what it saves your agents with
47
- [the AgentPerf harness](https://github.com/N-45div/AgentPerf).
48
-
49
- Apache-2.0
1
+ # @agentperf/react
2
+
3
+ **Give your React app a second frontend — for AI agents.** Humans get your DOM;
4
+ agents get a token-budgeted state snapshot and typed, schema-gated actions on
5
+ the WebMCP standard (`document.modelContext`). One codebase, two doorways.
6
+
7
+ ```bash
8
+ npm i @agentperf/react zod
9
+ ```
10
+
11
+ ```tsx
12
+ import { useAgentState, useAgentAction, AgentBoundary } from "@agentperf/react";
13
+ import { z } from "zod";
14
+
15
+ function Cart({ cart }) {
16
+ // agents read this through one get_page_state tool, pruned to a token budget
17
+ useAgentState("cart", cart);
18
+
19
+ // agents act through a schema gate — invalid input never reaches your handler,
20
+ // it gets a refusal naming the exact violated fields so it can self-correct
21
+ useAgentAction("checkout", {
22
+ description: "Pay for the items in the cart",
23
+ input: z.object({ email: z.string().email() }),
24
+ execute: ({ email }) => checkout(email)
25
+ });
26
+ }
27
+ ```
28
+
29
+ - **No WebMCP, no problem** — in browsers without `document.modelContext`
30
+ everything no-ops and your app stays a normal human app. Add
31
+ [`@mcp-b/global`](https://github.com/WebMCP-org/npm-packages) as a polyfill
32
+ if you want tools everywhere.
33
+ - **Structured refusals** — validation failures return the violated field
34
+ paths and a fix-and-retry instruction, so agents correct their calls instead
35
+ of corrupting state. Deterministic: same input, same verdict.
36
+ - **Token budgeting** — `get_page_state` prunes long arrays and strings to a
37
+ budget (default 1,000 tokens, `setAgentStateBudget` to change), because
38
+ agents pay per token to read your page.
39
+ - **`AgentBoundary`** scopes state keys and tool names the way your component
40
+ tree scopes the UI.
41
+ - **Read-only, destructive and consequential hints** (`readOnly`,
42
+ `destructive`, `consequential`) map to WebMCP annotations. `consequential`
43
+ publishes `consequentialHint` (Chrome 154+), which tells an agent to get
44
+ explicit confirmation from the person before a high-stakes or irreversible
45
+ action placing an order, moving money, booking a seat.
46
+ - **A reserved `price` field** is inert today and becomes x402 settlement
47
+ later without a breaking change.
48
+
49
+ Works in Chrome 149+ (WebMCP origin trial / `chrome://flags/#enable-webmcp-testing`)
50
+ and the ChatGPT desktop browser. Measure what it saves your agents with
51
+ [the AgentPerf harness](https://github.com/N-45div/AgentPerf).
52
+
53
+ Apache-2.0
package/dist/index.cjs CHANGED
@@ -210,6 +210,9 @@ function useAgentAction(name, config) {
210
210
  readOnlyHint: registeredWith.readOnly === true,
211
211
  ...registeredWith.destructive !== void 0 && {
212
212
  destructiveHint: registeredWith.destructive
213
+ },
214
+ ...registeredWith.consequential !== void 0 && {
215
+ consequentialHint: registeredWith.consequential
213
216
  }
214
217
  },
215
218
  execute: async (args) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/boundary.tsx","../src/state.ts","../src/serialize.ts","../src/types.ts","../src/action.ts"],"sourcesContent":["export { AgentBoundary, type AgentBoundaryProps } from \"./boundary\";\nexport {\n useAgentState,\n setAgentStateBudget,\n DEFAULT_STATE_BUDGET,\n type AgentStateOptions\n} from \"./state\";\nexport {\n useAgentAction,\n type AgentActionConfig,\n type AgentActionStatus\n} from \"./action\";\nexport { estimateTokens, serializeBudgeted } from \"./serialize\";\nexport {\n webmcpSupported,\n textResult,\n type ToolDefinition,\n type ToolResult,\n type ToolAnnotations,\n type ModelContext\n} from \"./types\";\n","/**\n * Scopes the agent surface the way a component tree scopes the human one.\n * State keys inside a boundary are prefixed `name.key`; action names are\n * prefixed `name_action`. Boundaries nest.\n */\nimport { createContext, useContext, type ReactNode } from \"react\";\n\nexport const BoundaryContext = createContext<string | null>(null);\n\nexport function scopedKey(scope: string | null, key: string, separator: \".\" | \"_\"): string {\n return scope ? `${scope}${separator}${key}` : key;\n}\n\nexport interface AgentBoundaryProps {\n name: string;\n children: ReactNode;\n}\n\nexport function AgentBoundary({ name, children }: AgentBoundaryProps) {\n const parent = useContext(BoundaryContext);\n const scope = parent ? `${parent}.${name}` : name;\n return <BoundaryContext.Provider value={scope}>{children}</BoundaryContext.Provider>;\n}\n","/**\n * Live page state exposed to agents through one `get_page_state` tool.\n *\n * Components declare slices with `useAgentState(key, value)`. The store keeps\n * a getter per key — values are resolved at call time, never at registration\n * time, so re-renders never tear down and re-register the tool. The tool is\n * registered when the first slice mounts and unregistered (via its\n * AbortController) when the last one unmounts, which fires the browser's\n * `toolchange` event so an idle agent learns the surface moved.\n */\nimport { useContext, useEffect, useRef } from \"react\";\nimport { BoundaryContext, scopedKey } from \"./boundary\";\nimport { serializeBudgeted } from \"./serialize\";\nimport { textResult, webmcpSupported } from \"./types\";\n\nexport const DEFAULT_STATE_BUDGET = 1000;\n\ninterface StateEntry {\n description?: string;\n get: () => unknown;\n}\n\nclass PageStateStore {\n private entries = new Map<string, StateEntry>();\n private controller: AbortController | null = null;\n budget = DEFAULT_STATE_BUDGET;\n\n add(key: string, entry: StateEntry): void {\n this.entries.set(key, entry);\n this.ensureRegistered();\n }\n\n remove(key: string): void {\n this.entries.delete(key);\n if (this.entries.size === 0 && this.controller) {\n this.controller.abort();\n this.controller = null;\n }\n }\n\n snapshot(): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, entry] of this.entries) out[key] = entry.get();\n return out;\n }\n\n private ensureRegistered(): void {\n if (this.controller || !webmcpSupported()) return;\n const controller = new AbortController();\n this.controller = controller;\n Promise.resolve(\n document.modelContext!.registerTool(\n {\n name: \"get_page_state\",\n description:\n \"Read a live snapshot of this page's application state, as compact JSON. \" +\n \"Call this before acting so you work from what the page actually shows, \" +\n \"not from a guess. Long values are pruned to stay small; a trailing \" +\n \"'… +N more' marker means an array was elided.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n annotations: { readOnlyHint: true },\n execute: () => textResult(serializeBudgeted(this.snapshot(), this.budget))\n },\n { signal: controller.signal }\n )\n ).catch(() => {\n if (this.controller === controller) this.controller = null;\n });\n }\n\n /** Test-only: forget all slices and drop the registration. */\n resetForTests(): void {\n this.controller?.abort();\n this.controller = null;\n this.entries.clear();\n this.budget = DEFAULT_STATE_BUDGET;\n }\n}\n\nexport const pageStateStore = new PageStateStore();\n\nexport interface AgentStateOptions {\n /** What this slice means, for future per-slice docs. */\n description?: string;\n}\n\n/**\n * Expose a slice of live app state to agents under `key`. The current value\n * is captured on every render; agents always read the latest through\n * `get_page_state`. Inside an `<AgentBoundary name=\"cart\">`, `key` becomes\n * `cart.key`.\n */\nexport function useAgentState<T>(key: string, value: T, options?: AgentStateOptions): void {\n const scope = useContext(BoundaryContext);\n const fullKey = scopedKey(scope, key, \".\");\n const valueRef = useRef<T>(value);\n valueRef.current = value;\n const descriptionRef = useRef(options?.description);\n descriptionRef.current = options?.description;\n\n useEffect(() => {\n pageStateStore.add(fullKey, {\n get: () => valueRef.current,\n description: descriptionRef.current\n });\n return () => pageStateStore.remove(fullKey);\n }, [fullKey]);\n}\n\n/** Set the total token budget for the `get_page_state` snapshot. */\nexport function setAgentStateBudget(tokens: number): void {\n pageStateStore.budget = tokens;\n}\n","/**\n * Token-budgeted serialization. Agents pay per token to read state, so the\n * snapshot an agent sees is pruned — long strings truncated, long arrays\n * elided with an explicit marker, depth capped — until it fits the budget.\n * Pruning is deterministic: the same value and budget always produce the\n * same snapshot.\n */\n\nconst ELLIPSIS = \"…\";\n\n/** Rough token estimate (~4 characters per token). */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\ninterface PruneLimits {\n maxItems: number;\n maxStringLength: number;\n maxDepth: number;\n}\n\nfunction prune(value: unknown, depth: number, limits: PruneLimits): unknown {\n if (value === null || typeof value !== \"object\") {\n if (typeof value === \"string\" && value.length > limits.maxStringLength) {\n return value.slice(0, limits.maxStringLength) + ELLIPSIS;\n }\n return value;\n }\n if (depth >= limits.maxDepth) return ELLIPSIS;\n if (Array.isArray(value)) {\n const kept = value\n .slice(0, limits.maxItems)\n .map((item) => prune(item, depth + 1, limits));\n if (value.length > limits.maxItems) {\n kept.push(`${ELLIPSIS} +${value.length - limits.maxItems} more`);\n }\n return kept;\n }\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (entry === undefined || typeof entry === \"function\") continue;\n out[key] = prune(entry, depth + 1, limits);\n }\n return out;\n}\n\n/**\n * Serialize `value` as compact JSON within roughly `budget` tokens.\n * Tightens limits in deterministic steps until the snapshot fits (or the\n * limits bottom out — a pathological value can still exceed a tiny budget).\n */\nexport function serializeBudgeted(value: unknown, budget: number): string {\n let limits: PruneLimits = { maxItems: 20, maxStringLength: 200, maxDepth: 6 };\n for (let pass = 0; pass < 6; pass++) {\n const text = JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n if (estimateTokens(text) <= budget || (limits.maxItems === 1 && limits.maxStringLength <= 25)) {\n return text;\n }\n limits = {\n maxItems: Math.max(1, Math.floor(limits.maxItems / 2)),\n maxStringLength: Math.max(25, Math.floor(limits.maxStringLength / 2)),\n maxDepth: Math.max(2, limits.maxDepth - 1)\n };\n }\n return JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n}\n","/**\n * Minimal typings for the WebMCP browser API.\n *\n * The standard exposes `document.modelContext` (Chrome 149+ origin trial;\n * `navigator.modelContext` was the pre-Chrome-150 spelling and is deprecated).\n * Browsers without the API can get it from a polyfill such as `@mcp-b/global`.\n * Only the surface this library uses is typed.\n */\n\nexport interface ToolTextContent {\n type: \"text\";\n text: string;\n}\n\nexport type ToolContent = ToolTextContent;\n\nexport interface ToolResult {\n content: ToolContent[];\n isError?: boolean;\n}\n\nexport interface ToolAnnotations {\n readOnlyHint?: boolean;\n destructiveHint?: boolean;\n idempotentHint?: boolean;\n untrustedContentHint?: boolean;\n}\n\nexport interface JsonSchemaObject {\n type: \"object\";\n properties?: Record<string, unknown>;\n required?: string[];\n additionalProperties?: boolean;\n [key: string]: unknown;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n inputSchema?: JsonSchemaObject;\n annotations?: ToolAnnotations;\n execute: (args: Record<string, unknown>) => Promise<ToolResult> | ToolResult;\n}\n\nexport interface RegisterToolOptions {\n /** Aborting this signal unregisters the tool. */\n signal?: AbortSignal;\n /** Origins allowed to see this tool when the page is framed. */\n exposedTo?: string[];\n}\n\nexport interface ModelContext {\n registerTool: (\n tool: ToolDefinition,\n options?: RegisterToolOptions\n ) => Promise<void> | void;\n getTools?: () => Promise<ToolDefinition[]>;\n addEventListener?: EventTarget[\"addEventListener\"];\n}\n\ndeclare global {\n interface Document {\n modelContext?: ModelContext;\n }\n}\n\n/** True when this browser can accept WebMCP tool registrations. */\nexport function webmcpSupported(): boolean {\n return (\n typeof document !== \"undefined\" &&\n typeof document.modelContext?.registerTool === \"function\"\n );\n}\n\n/** Canonical text result — identical shape across agents. */\nexport function textResult(text: string, isError = false): ToolResult {\n return isError\n ? { content: [{ type: \"text\", text }], isError: true }\n : { content: [{ type: \"text\", text }] };\n}\n","/**\n * Schema-gated actions. Every call is validated before your handler runs;\n * invalid input gets a structured refusal naming each violated field, so an\n * agent can fix its call instead of corrupting your state. The gate is\n * deterministic — the same input always gets the same verdict.\n */\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport { z } from \"zod\";\nimport { BoundaryContext } from \"./boundary\";\nimport {\n textResult,\n webmcpSupported,\n type JsonSchemaObject,\n type ToolResult\n} from \"./types\";\n\nexport interface AgentActionConfig<Schema extends z.ZodType = z.ZodType> {\n /** What this action does, written for the agent deciding whether to call it. */\n description: string;\n /** Input contract. Omit for zero-argument actions. */\n input?: Schema;\n /** True when the action changes nothing — lets agents call it freely. */\n readOnly?: boolean;\n /** True when the action destroys something a person would miss. */\n destructive?: boolean;\n /**\n * Reserved: price per call (e.g. \"$0.001\"). Inert in v1 — declared here so\n * adding x402 settlement later is not a breaking change.\n */\n price?: string;\n /** Runs only after input passes the schema gate. */\n execute: (input: z.infer<Schema>) => Promise<unknown> | unknown;\n}\n\nexport interface AgentActionStatus {\n /** Whether this browser accepts WebMCP registrations. */\n supported: boolean;\n /** The name the tool was published under (boundary prefixes applied). */\n name: string;\n}\n\nfunction formatRefusal(toolName: string, error: z.ZodError): ToolResult {\n const violations = error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"input\";\n return `- ${path}: ${issue.message}`;\n });\n return textResult(\n `REFUSED: invalid input for \"${toolName}\".\\n` +\n `Violations (${violations.length}):\\n${violations.join(\"\\n\")}\\n\\n` +\n `Fix the specific violations above and call the tool again. ` +\n `Validation is deterministic — the same input always gets the same verdict.`,\n true\n );\n}\n\nfunction toInputSchema(schema: z.ZodType | undefined): JsonSchemaObject {\n if (!schema) return { type: \"object\", properties: {}, additionalProperties: false };\n const { $schema: _discard, ...rest } = z.toJSONSchema(schema) as Record<string, unknown>;\n return rest as JsonSchemaObject;\n}\n\n/**\n * Publish an action as a WebMCP tool for the lifetime of the component.\n * Inside an `<AgentBoundary name=\"cart\">`, `name` becomes `cart_name`.\n * The handler and description are read at call time through a ref, so\n * re-renders never re-register the tool.\n */\nexport function useAgentAction<Schema extends z.ZodType>(\n name: string,\n config: AgentActionConfig<Schema>\n): AgentActionStatus {\n const scope = useContext(BoundaryContext);\n const fullName = scope ? `${scope.replace(/\\./g, \"_\")}_${name}` : name;\n\n const configRef = useRef(config);\n configRef.current = config;\n\n const [supported, setSupported] = useState(false);\n\n useEffect(() => {\n if (!webmcpSupported()) return;\n const controller = new AbortController();\n const registeredWith = configRef.current;\n\n Promise.resolve(\n document.modelContext!.registerTool(\n {\n name: fullName,\n description: registeredWith.description,\n inputSchema: toInputSchema(registeredWith.input),\n annotations: {\n readOnlyHint: registeredWith.readOnly === true,\n ...(registeredWith.destructive !== undefined && {\n destructiveHint: registeredWith.destructive\n })\n },\n execute: async (args) => {\n const current = configRef.current;\n let input: unknown = args;\n if (current.input) {\n const parsed = current.input.safeParse(args);\n if (!parsed.success) return formatRefusal(fullName, parsed.error);\n input = parsed.data;\n }\n try {\n const out = await current.execute(input as z.infer<Schema>);\n if (out === undefined) return textResult(\"Done.\");\n return textResult(typeof out === \"string\" ? out : JSON.stringify(out));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return textResult(`Error in \"${fullName}\": ${message}`, true);\n }\n }\n },\n { signal: controller.signal }\n )\n )\n .then(() => {\n if (!controller.signal.aborted) setSupported(true);\n })\n .catch(() => {\n /* registration failed — tool stays unpublished; page works for humans */\n });\n\n return () => controller.abort();\n }, [fullName]);\n\n return { supported, name: fullName };\n}\n"],"mappings":"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","names":["import_react","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/boundary.tsx","../src/state.ts","../src/serialize.ts","../src/types.ts","../src/action.ts"],"sourcesContent":["export { AgentBoundary, type AgentBoundaryProps } from \"./boundary\";\nexport {\n useAgentState,\n setAgentStateBudget,\n DEFAULT_STATE_BUDGET,\n type AgentStateOptions\n} from \"./state\";\nexport {\n useAgentAction,\n type AgentActionConfig,\n type AgentActionStatus\n} from \"./action\";\nexport { estimateTokens, serializeBudgeted } from \"./serialize\";\nexport {\n webmcpSupported,\n textResult,\n type ToolDefinition,\n type ToolResult,\n type ToolAnnotations,\n type ModelContext\n} from \"./types\";\n","/**\n * Scopes the agent surface the way a component tree scopes the human one.\n * State keys inside a boundary are prefixed `name.key`; action names are\n * prefixed `name_action`. Boundaries nest.\n */\nimport { createContext, useContext, type ReactNode } from \"react\";\n\nexport const BoundaryContext = createContext<string | null>(null);\n\nexport function scopedKey(scope: string | null, key: string, separator: \".\" | \"_\"): string {\n return scope ? `${scope}${separator}${key}` : key;\n}\n\nexport interface AgentBoundaryProps {\n name: string;\n children: ReactNode;\n}\n\nexport function AgentBoundary({ name, children }: AgentBoundaryProps) {\n const parent = useContext(BoundaryContext);\n const scope = parent ? `${parent}.${name}` : name;\n return <BoundaryContext.Provider value={scope}>{children}</BoundaryContext.Provider>;\n}\n","/**\n * Live page state exposed to agents through one `get_page_state` tool.\n *\n * Components declare slices with `useAgentState(key, value)`. The store keeps\n * a getter per key — values are resolved at call time, never at registration\n * time, so re-renders never tear down and re-register the tool. The tool is\n * registered when the first slice mounts and unregistered (via its\n * AbortController) when the last one unmounts, which fires the browser's\n * `toolchange` event so an idle agent learns the surface moved.\n */\nimport { useContext, useEffect, useRef } from \"react\";\nimport { BoundaryContext, scopedKey } from \"./boundary\";\nimport { serializeBudgeted } from \"./serialize\";\nimport { textResult, webmcpSupported } from \"./types\";\n\nexport const DEFAULT_STATE_BUDGET = 1000;\n\ninterface StateEntry {\n description?: string;\n get: () => unknown;\n}\n\nclass PageStateStore {\n private entries = new Map<string, StateEntry>();\n private controller: AbortController | null = null;\n budget = DEFAULT_STATE_BUDGET;\n\n add(key: string, entry: StateEntry): void {\n this.entries.set(key, entry);\n this.ensureRegistered();\n }\n\n remove(key: string): void {\n this.entries.delete(key);\n if (this.entries.size === 0 && this.controller) {\n this.controller.abort();\n this.controller = null;\n }\n }\n\n snapshot(): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, entry] of this.entries) out[key] = entry.get();\n return out;\n }\n\n private ensureRegistered(): void {\n if (this.controller || !webmcpSupported()) return;\n const controller = new AbortController();\n this.controller = controller;\n Promise.resolve(\n document.modelContext!.registerTool(\n {\n name: \"get_page_state\",\n description:\n \"Read a live snapshot of this page's application state, as compact JSON. \" +\n \"Call this before acting so you work from what the page actually shows, \" +\n \"not from a guess. Long values are pruned to stay small; a trailing \" +\n \"'… +N more' marker means an array was elided.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n annotations: { readOnlyHint: true },\n execute: () => textResult(serializeBudgeted(this.snapshot(), this.budget))\n },\n { signal: controller.signal }\n )\n ).catch(() => {\n if (this.controller === controller) this.controller = null;\n });\n }\n\n /** Test-only: forget all slices and drop the registration. */\n resetForTests(): void {\n this.controller?.abort();\n this.controller = null;\n this.entries.clear();\n this.budget = DEFAULT_STATE_BUDGET;\n }\n}\n\nexport const pageStateStore = new PageStateStore();\n\nexport interface AgentStateOptions {\n /** What this slice means, for future per-slice docs. */\n description?: string;\n}\n\n/**\n * Expose a slice of live app state to agents under `key`. The current value\n * is captured on every render; agents always read the latest through\n * `get_page_state`. Inside an `<AgentBoundary name=\"cart\">`, `key` becomes\n * `cart.key`.\n */\nexport function useAgentState<T>(key: string, value: T, options?: AgentStateOptions): void {\n const scope = useContext(BoundaryContext);\n const fullKey = scopedKey(scope, key, \".\");\n const valueRef = useRef<T>(value);\n valueRef.current = value;\n const descriptionRef = useRef(options?.description);\n descriptionRef.current = options?.description;\n\n useEffect(() => {\n pageStateStore.add(fullKey, {\n get: () => valueRef.current,\n description: descriptionRef.current\n });\n return () => pageStateStore.remove(fullKey);\n }, [fullKey]);\n}\n\n/** Set the total token budget for the `get_page_state` snapshot. */\nexport function setAgentStateBudget(tokens: number): void {\n pageStateStore.budget = tokens;\n}\n","/**\n * Token-budgeted serialization. Agents pay per token to read state, so the\n * snapshot an agent sees is pruned — long strings truncated, long arrays\n * elided with an explicit marker, depth capped — until it fits the budget.\n * Pruning is deterministic: the same value and budget always produce the\n * same snapshot.\n */\n\nconst ELLIPSIS = \"…\";\n\n/** Rough token estimate (~4 characters per token). */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\ninterface PruneLimits {\n maxItems: number;\n maxStringLength: number;\n maxDepth: number;\n}\n\nfunction prune(value: unknown, depth: number, limits: PruneLimits): unknown {\n if (value === null || typeof value !== \"object\") {\n if (typeof value === \"string\" && value.length > limits.maxStringLength) {\n return value.slice(0, limits.maxStringLength) + ELLIPSIS;\n }\n return value;\n }\n if (depth >= limits.maxDepth) return ELLIPSIS;\n if (Array.isArray(value)) {\n const kept = value\n .slice(0, limits.maxItems)\n .map((item) => prune(item, depth + 1, limits));\n if (value.length > limits.maxItems) {\n kept.push(`${ELLIPSIS} +${value.length - limits.maxItems} more`);\n }\n return kept;\n }\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (entry === undefined || typeof entry === \"function\") continue;\n out[key] = prune(entry, depth + 1, limits);\n }\n return out;\n}\n\n/**\n * Serialize `value` as compact JSON within roughly `budget` tokens.\n * Tightens limits in deterministic steps until the snapshot fits (or the\n * limits bottom out — a pathological value can still exceed a tiny budget).\n */\nexport function serializeBudgeted(value: unknown, budget: number): string {\n let limits: PruneLimits = { maxItems: 20, maxStringLength: 200, maxDepth: 6 };\n for (let pass = 0; pass < 6; pass++) {\n const text = JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n if (estimateTokens(text) <= budget || (limits.maxItems === 1 && limits.maxStringLength <= 25)) {\n return text;\n }\n limits = {\n maxItems: Math.max(1, Math.floor(limits.maxItems / 2)),\n maxStringLength: Math.max(25, Math.floor(limits.maxStringLength / 2)),\n maxDepth: Math.max(2, limits.maxDepth - 1)\n };\n }\n return JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n}\n","/**\r\n * Minimal typings for the WebMCP browser API.\r\n *\r\n * The standard exposes `document.modelContext` (Chrome 149+ origin trial;\r\n * `navigator.modelContext` was the pre-Chrome-150 spelling and is deprecated).\r\n * Browsers without the API can get it from a polyfill such as `@mcp-b/global`.\r\n * Only the surface this library uses is typed.\r\n */\r\n\r\nexport interface ToolTextContent {\r\n type: \"text\";\r\n text: string;\r\n}\r\n\r\nexport type ToolContent = ToolTextContent;\r\n\r\nexport interface ToolResult {\r\n content: ToolContent[];\r\n isError?: boolean;\r\n}\r\n\r\nexport interface ToolAnnotations {\r\n readOnlyHint?: boolean;\r\n destructiveHint?: boolean;\r\n /**\r\n * High-stakes, irreversible or real-world action. Chrome 154+ uses it to\r\n * tell an agent to confirm with the person before calling the tool.\r\n */\r\n consequentialHint?: boolean;\r\n idempotentHint?: boolean;\r\n untrustedContentHint?: boolean;\r\n}\r\n\r\nexport interface JsonSchemaObject {\r\n type: \"object\";\r\n properties?: Record<string, unknown>;\r\n required?: string[];\r\n additionalProperties?: boolean;\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface ToolDefinition {\r\n name: string;\r\n description: string;\r\n inputSchema?: JsonSchemaObject;\r\n annotations?: ToolAnnotations;\r\n execute: (args: Record<string, unknown>) => Promise<ToolResult> | ToolResult;\r\n}\r\n\r\nexport interface RegisterToolOptions {\r\n /** Aborting this signal unregisters the tool. */\r\n signal?: AbortSignal;\r\n /** Origins allowed to see this tool when the page is framed. */\r\n exposedTo?: string[];\r\n}\r\n\r\nexport interface ModelContext {\r\n registerTool: (\r\n tool: ToolDefinition,\r\n options?: RegisterToolOptions\r\n ) => Promise<void> | void;\r\n getTools?: () => Promise<ToolDefinition[]>;\r\n addEventListener?: EventTarget[\"addEventListener\"];\r\n}\r\n\r\ndeclare global {\r\n interface Document {\r\n modelContext?: ModelContext;\r\n }\r\n}\r\n\r\n/** True when this browser can accept WebMCP tool registrations. */\r\nexport function webmcpSupported(): boolean {\r\n return (\r\n typeof document !== \"undefined\" &&\r\n typeof document.modelContext?.registerTool === \"function\"\r\n );\r\n}\r\n\r\n/** Canonical text result — identical shape across agents. */\r\nexport function textResult(text: string, isError = false): ToolResult {\r\n return isError\r\n ? { content: [{ type: \"text\", text }], isError: true }\r\n : { content: [{ type: \"text\", text }] };\r\n}\r\n","/**\r\n * Schema-gated actions. Every call is validated before your handler runs;\r\n * invalid input gets a structured refusal naming each violated field, so an\r\n * agent can fix its call instead of corrupting your state. The gate is\r\n * deterministic — the same input always gets the same verdict.\r\n */\r\nimport { useContext, useEffect, useRef, useState } from \"react\";\r\nimport { z } from \"zod\";\r\nimport { BoundaryContext } from \"./boundary\";\r\nimport {\r\n textResult,\r\n webmcpSupported,\r\n type JsonSchemaObject,\r\n type ToolResult\r\n} from \"./types\";\r\n\r\nexport interface AgentActionConfig<Schema extends z.ZodType = z.ZodType> {\r\n /** What this action does, written for the agent deciding whether to call it. */\r\n description: string;\r\n /** Input contract. Omit for zero-argument actions. */\r\n input?: Schema;\r\n /** True when the action changes nothing — lets agents call it freely. */\r\n readOnly?: boolean;\r\n /** True when the action destroys something a person would miss. */\r\n destructive?: boolean;\r\n /**\r\n * True for high-stakes, irreversible or real-world actions: placing an\r\n * order, moving money, booking a seat. Agents are expected to get explicit\r\n * confirmation from the person before calling one. Published as\r\n * `consequentialHint` (Chrome 154+); older browsers ignore the extra field.\r\n */\r\n consequential?: boolean;\r\n /**\r\n * Reserved: price per call (e.g. \"$0.001\"). Inert in v1 — declared here so\r\n * adding x402 settlement later is not a breaking change.\r\n */\r\n price?: string;\r\n /** Runs only after input passes the schema gate. */\r\n execute: (input: z.infer<Schema>) => Promise<unknown> | unknown;\r\n}\r\n\r\nexport interface AgentActionStatus {\r\n /** Whether this browser accepts WebMCP registrations. */\r\n supported: boolean;\r\n /** The name the tool was published under (boundary prefixes applied). */\r\n name: string;\r\n}\r\n\r\nfunction formatRefusal(toolName: string, error: z.ZodError): ToolResult {\r\n const violations = error.issues.map((issue) => {\r\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"input\";\r\n return `- ${path}: ${issue.message}`;\r\n });\r\n return textResult(\r\n `REFUSED: invalid input for \"${toolName}\".\\n` +\r\n `Violations (${violations.length}):\\n${violations.join(\"\\n\")}\\n\\n` +\r\n `Fix the specific violations above and call the tool again. ` +\r\n `Validation is deterministic — the same input always gets the same verdict.`,\r\n true\r\n );\r\n}\r\n\r\nfunction toInputSchema(schema: z.ZodType | undefined): JsonSchemaObject {\r\n if (!schema) return { type: \"object\", properties: {}, additionalProperties: false };\r\n const { $schema: _discard, ...rest } = z.toJSONSchema(schema) as Record<string, unknown>;\r\n return rest as JsonSchemaObject;\r\n}\r\n\r\n/**\r\n * Publish an action as a WebMCP tool for the lifetime of the component.\r\n * Inside an `<AgentBoundary name=\"cart\">`, `name` becomes `cart_name`.\r\n * The handler and description are read at call time through a ref, so\r\n * re-renders never re-register the tool.\r\n */\r\nexport function useAgentAction<Schema extends z.ZodType>(\r\n name: string,\r\n config: AgentActionConfig<Schema>\r\n): AgentActionStatus {\r\n const scope = useContext(BoundaryContext);\r\n const fullName = scope ? `${scope.replace(/\\./g, \"_\")}_${name}` : name;\r\n\r\n const configRef = useRef(config);\r\n configRef.current = config;\r\n\r\n const [supported, setSupported] = useState(false);\r\n\r\n useEffect(() => {\r\n if (!webmcpSupported()) return;\r\n const controller = new AbortController();\r\n const registeredWith = configRef.current;\r\n\r\n Promise.resolve(\r\n document.modelContext!.registerTool(\r\n {\r\n name: fullName,\r\n description: registeredWith.description,\r\n inputSchema: toInputSchema(registeredWith.input),\r\n annotations: {\r\n readOnlyHint: registeredWith.readOnly === true,\r\n ...(registeredWith.destructive !== undefined && {\r\n destructiveHint: registeredWith.destructive\r\n }),\r\n ...(registeredWith.consequential !== undefined && {\r\n consequentialHint: registeredWith.consequential\r\n })\r\n },\r\n execute: async (args) => {\r\n const current = configRef.current;\r\n let input: unknown = args;\r\n if (current.input) {\r\n const parsed = current.input.safeParse(args);\r\n if (!parsed.success) return formatRefusal(fullName, parsed.error);\r\n input = parsed.data;\r\n }\r\n try {\r\n const out = await current.execute(input as z.infer<Schema>);\r\n if (out === undefined) return textResult(\"Done.\");\r\n return textResult(typeof out === \"string\" ? out : JSON.stringify(out));\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n return textResult(`Error in \"${fullName}\": ${message}`, true);\r\n }\r\n }\r\n },\r\n { signal: controller.signal }\r\n )\r\n )\r\n .then(() => {\r\n if (!controller.signal.aborted) setSupported(true);\r\n })\r\n .catch(() => {\r\n /* registration failed — tool stays unpublished; page works for humans */\r\n });\r\n\r\n return () => controller.abort();\r\n }, [fullName]);\r\n\r\n return { supported, name: fullName };\r\n}\r\n"],"mappings":"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","names":["import_react","import_react"]}
package/dist/index.d.cts CHANGED
@@ -32,6 +32,13 @@ interface AgentActionConfig<Schema extends z.ZodType = z.ZodType> {
32
32
  readOnly?: boolean;
33
33
  /** True when the action destroys something a person would miss. */
34
34
  destructive?: boolean;
35
+ /**
36
+ * True for high-stakes, irreversible or real-world actions: placing an
37
+ * order, moving money, booking a seat. Agents are expected to get explicit
38
+ * confirmation from the person before calling one. Published as
39
+ * `consequentialHint` (Chrome 154+); older browsers ignore the extra field.
40
+ */
41
+ consequential?: boolean;
35
42
  /**
36
43
  * Reserved: price per call (e.g. "$0.001"). Inert in v1 — declared here so
37
44
  * adding x402 settlement later is not a breaking change.
@@ -90,6 +97,11 @@ interface ToolResult {
90
97
  interface ToolAnnotations {
91
98
  readOnlyHint?: boolean;
92
99
  destructiveHint?: boolean;
100
+ /**
101
+ * High-stakes, irreversible or real-world action. Chrome 154+ uses it to
102
+ * tell an agent to confirm with the person before calling the tool.
103
+ */
104
+ consequentialHint?: boolean;
93
105
  idempotentHint?: boolean;
94
106
  untrustedContentHint?: boolean;
95
107
  }
package/dist/index.d.ts CHANGED
@@ -32,6 +32,13 @@ interface AgentActionConfig<Schema extends z.ZodType = z.ZodType> {
32
32
  readOnly?: boolean;
33
33
  /** True when the action destroys something a person would miss. */
34
34
  destructive?: boolean;
35
+ /**
36
+ * True for high-stakes, irreversible or real-world actions: placing an
37
+ * order, moving money, booking a seat. Agents are expected to get explicit
38
+ * confirmation from the person before calling one. Published as
39
+ * `consequentialHint` (Chrome 154+); older browsers ignore the extra field.
40
+ */
41
+ consequential?: boolean;
35
42
  /**
36
43
  * Reserved: price per call (e.g. "$0.001"). Inert in v1 — declared here so
37
44
  * adding x402 settlement later is not a breaking change.
@@ -90,6 +97,11 @@ interface ToolResult {
90
97
  interface ToolAnnotations {
91
98
  readOnlyHint?: boolean;
92
99
  destructiveHint?: boolean;
100
+ /**
101
+ * High-stakes, irreversible or real-world action. Chrome 154+ uses it to
102
+ * tell an agent to confirm with the person before calling the tool.
103
+ */
104
+ consequentialHint?: boolean;
93
105
  idempotentHint?: boolean;
94
106
  untrustedContentHint?: boolean;
95
107
  }
package/dist/index.js CHANGED
@@ -176,6 +176,9 @@ function useAgentAction(name, config) {
176
176
  readOnlyHint: registeredWith.readOnly === true,
177
177
  ...registeredWith.destructive !== void 0 && {
178
178
  destructiveHint: registeredWith.destructive
179
+ },
180
+ ...registeredWith.consequential !== void 0 && {
181
+ consequentialHint: registeredWith.consequential
179
182
  }
180
183
  },
181
184
  execute: async (args) => {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/boundary.tsx","../src/state.ts","../src/serialize.ts","../src/types.ts","../src/action.ts"],"sourcesContent":["/**\n * Scopes the agent surface the way a component tree scopes the human one.\n * State keys inside a boundary are prefixed `name.key`; action names are\n * prefixed `name_action`. Boundaries nest.\n */\nimport { createContext, useContext, type ReactNode } from \"react\";\n\nexport const BoundaryContext = createContext<string | null>(null);\n\nexport function scopedKey(scope: string | null, key: string, separator: \".\" | \"_\"): string {\n return scope ? `${scope}${separator}${key}` : key;\n}\n\nexport interface AgentBoundaryProps {\n name: string;\n children: ReactNode;\n}\n\nexport function AgentBoundary({ name, children }: AgentBoundaryProps) {\n const parent = useContext(BoundaryContext);\n const scope = parent ? `${parent}.${name}` : name;\n return <BoundaryContext.Provider value={scope}>{children}</BoundaryContext.Provider>;\n}\n","/**\n * Live page state exposed to agents through one `get_page_state` tool.\n *\n * Components declare slices with `useAgentState(key, value)`. The store keeps\n * a getter per key — values are resolved at call time, never at registration\n * time, so re-renders never tear down and re-register the tool. The tool is\n * registered when the first slice mounts and unregistered (via its\n * AbortController) when the last one unmounts, which fires the browser's\n * `toolchange` event so an idle agent learns the surface moved.\n */\nimport { useContext, useEffect, useRef } from \"react\";\nimport { BoundaryContext, scopedKey } from \"./boundary\";\nimport { serializeBudgeted } from \"./serialize\";\nimport { textResult, webmcpSupported } from \"./types\";\n\nexport const DEFAULT_STATE_BUDGET = 1000;\n\ninterface StateEntry {\n description?: string;\n get: () => unknown;\n}\n\nclass PageStateStore {\n private entries = new Map<string, StateEntry>();\n private controller: AbortController | null = null;\n budget = DEFAULT_STATE_BUDGET;\n\n add(key: string, entry: StateEntry): void {\n this.entries.set(key, entry);\n this.ensureRegistered();\n }\n\n remove(key: string): void {\n this.entries.delete(key);\n if (this.entries.size === 0 && this.controller) {\n this.controller.abort();\n this.controller = null;\n }\n }\n\n snapshot(): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, entry] of this.entries) out[key] = entry.get();\n return out;\n }\n\n private ensureRegistered(): void {\n if (this.controller || !webmcpSupported()) return;\n const controller = new AbortController();\n this.controller = controller;\n Promise.resolve(\n document.modelContext!.registerTool(\n {\n name: \"get_page_state\",\n description:\n \"Read a live snapshot of this page's application state, as compact JSON. \" +\n \"Call this before acting so you work from what the page actually shows, \" +\n \"not from a guess. Long values are pruned to stay small; a trailing \" +\n \"'… +N more' marker means an array was elided.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n annotations: { readOnlyHint: true },\n execute: () => textResult(serializeBudgeted(this.snapshot(), this.budget))\n },\n { signal: controller.signal }\n )\n ).catch(() => {\n if (this.controller === controller) this.controller = null;\n });\n }\n\n /** Test-only: forget all slices and drop the registration. */\n resetForTests(): void {\n this.controller?.abort();\n this.controller = null;\n this.entries.clear();\n this.budget = DEFAULT_STATE_BUDGET;\n }\n}\n\nexport const pageStateStore = new PageStateStore();\n\nexport interface AgentStateOptions {\n /** What this slice means, for future per-slice docs. */\n description?: string;\n}\n\n/**\n * Expose a slice of live app state to agents under `key`. The current value\n * is captured on every render; agents always read the latest through\n * `get_page_state`. Inside an `<AgentBoundary name=\"cart\">`, `key` becomes\n * `cart.key`.\n */\nexport function useAgentState<T>(key: string, value: T, options?: AgentStateOptions): void {\n const scope = useContext(BoundaryContext);\n const fullKey = scopedKey(scope, key, \".\");\n const valueRef = useRef<T>(value);\n valueRef.current = value;\n const descriptionRef = useRef(options?.description);\n descriptionRef.current = options?.description;\n\n useEffect(() => {\n pageStateStore.add(fullKey, {\n get: () => valueRef.current,\n description: descriptionRef.current\n });\n return () => pageStateStore.remove(fullKey);\n }, [fullKey]);\n}\n\n/** Set the total token budget for the `get_page_state` snapshot. */\nexport function setAgentStateBudget(tokens: number): void {\n pageStateStore.budget = tokens;\n}\n","/**\n * Token-budgeted serialization. Agents pay per token to read state, so the\n * snapshot an agent sees is pruned — long strings truncated, long arrays\n * elided with an explicit marker, depth capped — until it fits the budget.\n * Pruning is deterministic: the same value and budget always produce the\n * same snapshot.\n */\n\nconst ELLIPSIS = \"…\";\n\n/** Rough token estimate (~4 characters per token). */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\ninterface PruneLimits {\n maxItems: number;\n maxStringLength: number;\n maxDepth: number;\n}\n\nfunction prune(value: unknown, depth: number, limits: PruneLimits): unknown {\n if (value === null || typeof value !== \"object\") {\n if (typeof value === \"string\" && value.length > limits.maxStringLength) {\n return value.slice(0, limits.maxStringLength) + ELLIPSIS;\n }\n return value;\n }\n if (depth >= limits.maxDepth) return ELLIPSIS;\n if (Array.isArray(value)) {\n const kept = value\n .slice(0, limits.maxItems)\n .map((item) => prune(item, depth + 1, limits));\n if (value.length > limits.maxItems) {\n kept.push(`${ELLIPSIS} +${value.length - limits.maxItems} more`);\n }\n return kept;\n }\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (entry === undefined || typeof entry === \"function\") continue;\n out[key] = prune(entry, depth + 1, limits);\n }\n return out;\n}\n\n/**\n * Serialize `value` as compact JSON within roughly `budget` tokens.\n * Tightens limits in deterministic steps until the snapshot fits (or the\n * limits bottom out — a pathological value can still exceed a tiny budget).\n */\nexport function serializeBudgeted(value: unknown, budget: number): string {\n let limits: PruneLimits = { maxItems: 20, maxStringLength: 200, maxDepth: 6 };\n for (let pass = 0; pass < 6; pass++) {\n const text = JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n if (estimateTokens(text) <= budget || (limits.maxItems === 1 && limits.maxStringLength <= 25)) {\n return text;\n }\n limits = {\n maxItems: Math.max(1, Math.floor(limits.maxItems / 2)),\n maxStringLength: Math.max(25, Math.floor(limits.maxStringLength / 2)),\n maxDepth: Math.max(2, limits.maxDepth - 1)\n };\n }\n return JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n}\n","/**\n * Minimal typings for the WebMCP browser API.\n *\n * The standard exposes `document.modelContext` (Chrome 149+ origin trial;\n * `navigator.modelContext` was the pre-Chrome-150 spelling and is deprecated).\n * Browsers without the API can get it from a polyfill such as `@mcp-b/global`.\n * Only the surface this library uses is typed.\n */\n\nexport interface ToolTextContent {\n type: \"text\";\n text: string;\n}\n\nexport type ToolContent = ToolTextContent;\n\nexport interface ToolResult {\n content: ToolContent[];\n isError?: boolean;\n}\n\nexport interface ToolAnnotations {\n readOnlyHint?: boolean;\n destructiveHint?: boolean;\n idempotentHint?: boolean;\n untrustedContentHint?: boolean;\n}\n\nexport interface JsonSchemaObject {\n type: \"object\";\n properties?: Record<string, unknown>;\n required?: string[];\n additionalProperties?: boolean;\n [key: string]: unknown;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n inputSchema?: JsonSchemaObject;\n annotations?: ToolAnnotations;\n execute: (args: Record<string, unknown>) => Promise<ToolResult> | ToolResult;\n}\n\nexport interface RegisterToolOptions {\n /** Aborting this signal unregisters the tool. */\n signal?: AbortSignal;\n /** Origins allowed to see this tool when the page is framed. */\n exposedTo?: string[];\n}\n\nexport interface ModelContext {\n registerTool: (\n tool: ToolDefinition,\n options?: RegisterToolOptions\n ) => Promise<void> | void;\n getTools?: () => Promise<ToolDefinition[]>;\n addEventListener?: EventTarget[\"addEventListener\"];\n}\n\ndeclare global {\n interface Document {\n modelContext?: ModelContext;\n }\n}\n\n/** True when this browser can accept WebMCP tool registrations. */\nexport function webmcpSupported(): boolean {\n return (\n typeof document !== \"undefined\" &&\n typeof document.modelContext?.registerTool === \"function\"\n );\n}\n\n/** Canonical text result — identical shape across agents. */\nexport function textResult(text: string, isError = false): ToolResult {\n return isError\n ? { content: [{ type: \"text\", text }], isError: true }\n : { content: [{ type: \"text\", text }] };\n}\n","/**\n * Schema-gated actions. Every call is validated before your handler runs;\n * invalid input gets a structured refusal naming each violated field, so an\n * agent can fix its call instead of corrupting your state. The gate is\n * deterministic — the same input always gets the same verdict.\n */\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport { z } from \"zod\";\nimport { BoundaryContext } from \"./boundary\";\nimport {\n textResult,\n webmcpSupported,\n type JsonSchemaObject,\n type ToolResult\n} from \"./types\";\n\nexport interface AgentActionConfig<Schema extends z.ZodType = z.ZodType> {\n /** What this action does, written for the agent deciding whether to call it. */\n description: string;\n /** Input contract. Omit for zero-argument actions. */\n input?: Schema;\n /** True when the action changes nothing — lets agents call it freely. */\n readOnly?: boolean;\n /** True when the action destroys something a person would miss. */\n destructive?: boolean;\n /**\n * Reserved: price per call (e.g. \"$0.001\"). Inert in v1 — declared here so\n * adding x402 settlement later is not a breaking change.\n */\n price?: string;\n /** Runs only after input passes the schema gate. */\n execute: (input: z.infer<Schema>) => Promise<unknown> | unknown;\n}\n\nexport interface AgentActionStatus {\n /** Whether this browser accepts WebMCP registrations. */\n supported: boolean;\n /** The name the tool was published under (boundary prefixes applied). */\n name: string;\n}\n\nfunction formatRefusal(toolName: string, error: z.ZodError): ToolResult {\n const violations = error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"input\";\n return `- ${path}: ${issue.message}`;\n });\n return textResult(\n `REFUSED: invalid input for \"${toolName}\".\\n` +\n `Violations (${violations.length}):\\n${violations.join(\"\\n\")}\\n\\n` +\n `Fix the specific violations above and call the tool again. ` +\n `Validation is deterministic — the same input always gets the same verdict.`,\n true\n );\n}\n\nfunction toInputSchema(schema: z.ZodType | undefined): JsonSchemaObject {\n if (!schema) return { type: \"object\", properties: {}, additionalProperties: false };\n const { $schema: _discard, ...rest } = z.toJSONSchema(schema) as Record<string, unknown>;\n return rest as JsonSchemaObject;\n}\n\n/**\n * Publish an action as a WebMCP tool for the lifetime of the component.\n * Inside an `<AgentBoundary name=\"cart\">`, `name` becomes `cart_name`.\n * The handler and description are read at call time through a ref, so\n * re-renders never re-register the tool.\n */\nexport function useAgentAction<Schema extends z.ZodType>(\n name: string,\n config: AgentActionConfig<Schema>\n): AgentActionStatus {\n const scope = useContext(BoundaryContext);\n const fullName = scope ? `${scope.replace(/\\./g, \"_\")}_${name}` : name;\n\n const configRef = useRef(config);\n configRef.current = config;\n\n const [supported, setSupported] = useState(false);\n\n useEffect(() => {\n if (!webmcpSupported()) return;\n const controller = new AbortController();\n const registeredWith = configRef.current;\n\n Promise.resolve(\n document.modelContext!.registerTool(\n {\n name: fullName,\n description: registeredWith.description,\n inputSchema: toInputSchema(registeredWith.input),\n annotations: {\n readOnlyHint: registeredWith.readOnly === true,\n ...(registeredWith.destructive !== undefined && {\n destructiveHint: registeredWith.destructive\n })\n },\n execute: async (args) => {\n const current = configRef.current;\n let input: unknown = args;\n if (current.input) {\n const parsed = current.input.safeParse(args);\n if (!parsed.success) return formatRefusal(fullName, parsed.error);\n input = parsed.data;\n }\n try {\n const out = await current.execute(input as z.infer<Schema>);\n if (out === undefined) return textResult(\"Done.\");\n return textResult(typeof out === \"string\" ? out : JSON.stringify(out));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return textResult(`Error in \"${fullName}\": ${message}`, true);\n }\n }\n },\n { signal: controller.signal }\n )\n )\n .then(() => {\n if (!controller.signal.aborted) setSupported(true);\n })\n .catch(() => {\n /* registration failed — tool stays unpublished; page works for humans */\n });\n\n return () => controller.abort();\n }, [fullName]);\n\n return { supported, name: fullName };\n}\n"],"mappings":"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","names":["useContext","useContext","useContext","useEffect","useRef","useContext","useRef","useEffect"]}
1
+ {"version":3,"sources":["../src/boundary.tsx","../src/state.ts","../src/serialize.ts","../src/types.ts","../src/action.ts"],"sourcesContent":["/**\n * Scopes the agent surface the way a component tree scopes the human one.\n * State keys inside a boundary are prefixed `name.key`; action names are\n * prefixed `name_action`. Boundaries nest.\n */\nimport { createContext, useContext, type ReactNode } from \"react\";\n\nexport const BoundaryContext = createContext<string | null>(null);\n\nexport function scopedKey(scope: string | null, key: string, separator: \".\" | \"_\"): string {\n return scope ? `${scope}${separator}${key}` : key;\n}\n\nexport interface AgentBoundaryProps {\n name: string;\n children: ReactNode;\n}\n\nexport function AgentBoundary({ name, children }: AgentBoundaryProps) {\n const parent = useContext(BoundaryContext);\n const scope = parent ? `${parent}.${name}` : name;\n return <BoundaryContext.Provider value={scope}>{children}</BoundaryContext.Provider>;\n}\n","/**\n * Live page state exposed to agents through one `get_page_state` tool.\n *\n * Components declare slices with `useAgentState(key, value)`. The store keeps\n * a getter per key — values are resolved at call time, never at registration\n * time, so re-renders never tear down and re-register the tool. The tool is\n * registered when the first slice mounts and unregistered (via its\n * AbortController) when the last one unmounts, which fires the browser's\n * `toolchange` event so an idle agent learns the surface moved.\n */\nimport { useContext, useEffect, useRef } from \"react\";\nimport { BoundaryContext, scopedKey } from \"./boundary\";\nimport { serializeBudgeted } from \"./serialize\";\nimport { textResult, webmcpSupported } from \"./types\";\n\nexport const DEFAULT_STATE_BUDGET = 1000;\n\ninterface StateEntry {\n description?: string;\n get: () => unknown;\n}\n\nclass PageStateStore {\n private entries = new Map<string, StateEntry>();\n private controller: AbortController | null = null;\n budget = DEFAULT_STATE_BUDGET;\n\n add(key: string, entry: StateEntry): void {\n this.entries.set(key, entry);\n this.ensureRegistered();\n }\n\n remove(key: string): void {\n this.entries.delete(key);\n if (this.entries.size === 0 && this.controller) {\n this.controller.abort();\n this.controller = null;\n }\n }\n\n snapshot(): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, entry] of this.entries) out[key] = entry.get();\n return out;\n }\n\n private ensureRegistered(): void {\n if (this.controller || !webmcpSupported()) return;\n const controller = new AbortController();\n this.controller = controller;\n Promise.resolve(\n document.modelContext!.registerTool(\n {\n name: \"get_page_state\",\n description:\n \"Read a live snapshot of this page's application state, as compact JSON. \" +\n \"Call this before acting so you work from what the page actually shows, \" +\n \"not from a guess. Long values are pruned to stay small; a trailing \" +\n \"'… +N more' marker means an array was elided.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n annotations: { readOnlyHint: true },\n execute: () => textResult(serializeBudgeted(this.snapshot(), this.budget))\n },\n { signal: controller.signal }\n )\n ).catch(() => {\n if (this.controller === controller) this.controller = null;\n });\n }\n\n /** Test-only: forget all slices and drop the registration. */\n resetForTests(): void {\n this.controller?.abort();\n this.controller = null;\n this.entries.clear();\n this.budget = DEFAULT_STATE_BUDGET;\n }\n}\n\nexport const pageStateStore = new PageStateStore();\n\nexport interface AgentStateOptions {\n /** What this slice means, for future per-slice docs. */\n description?: string;\n}\n\n/**\n * Expose a slice of live app state to agents under `key`. The current value\n * is captured on every render; agents always read the latest through\n * `get_page_state`. Inside an `<AgentBoundary name=\"cart\">`, `key` becomes\n * `cart.key`.\n */\nexport function useAgentState<T>(key: string, value: T, options?: AgentStateOptions): void {\n const scope = useContext(BoundaryContext);\n const fullKey = scopedKey(scope, key, \".\");\n const valueRef = useRef<T>(value);\n valueRef.current = value;\n const descriptionRef = useRef(options?.description);\n descriptionRef.current = options?.description;\n\n useEffect(() => {\n pageStateStore.add(fullKey, {\n get: () => valueRef.current,\n description: descriptionRef.current\n });\n return () => pageStateStore.remove(fullKey);\n }, [fullKey]);\n}\n\n/** Set the total token budget for the `get_page_state` snapshot. */\nexport function setAgentStateBudget(tokens: number): void {\n pageStateStore.budget = tokens;\n}\n","/**\n * Token-budgeted serialization. Agents pay per token to read state, so the\n * snapshot an agent sees is pruned — long strings truncated, long arrays\n * elided with an explicit marker, depth capped — until it fits the budget.\n * Pruning is deterministic: the same value and budget always produce the\n * same snapshot.\n */\n\nconst ELLIPSIS = \"…\";\n\n/** Rough token estimate (~4 characters per token). */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\ninterface PruneLimits {\n maxItems: number;\n maxStringLength: number;\n maxDepth: number;\n}\n\nfunction prune(value: unknown, depth: number, limits: PruneLimits): unknown {\n if (value === null || typeof value !== \"object\") {\n if (typeof value === \"string\" && value.length > limits.maxStringLength) {\n return value.slice(0, limits.maxStringLength) + ELLIPSIS;\n }\n return value;\n }\n if (depth >= limits.maxDepth) return ELLIPSIS;\n if (Array.isArray(value)) {\n const kept = value\n .slice(0, limits.maxItems)\n .map((item) => prune(item, depth + 1, limits));\n if (value.length > limits.maxItems) {\n kept.push(`${ELLIPSIS} +${value.length - limits.maxItems} more`);\n }\n return kept;\n }\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (entry === undefined || typeof entry === \"function\") continue;\n out[key] = prune(entry, depth + 1, limits);\n }\n return out;\n}\n\n/**\n * Serialize `value` as compact JSON within roughly `budget` tokens.\n * Tightens limits in deterministic steps until the snapshot fits (or the\n * limits bottom out — a pathological value can still exceed a tiny budget).\n */\nexport function serializeBudgeted(value: unknown, budget: number): string {\n let limits: PruneLimits = { maxItems: 20, maxStringLength: 200, maxDepth: 6 };\n for (let pass = 0; pass < 6; pass++) {\n const text = JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n if (estimateTokens(text) <= budget || (limits.maxItems === 1 && limits.maxStringLength <= 25)) {\n return text;\n }\n limits = {\n maxItems: Math.max(1, Math.floor(limits.maxItems / 2)),\n maxStringLength: Math.max(25, Math.floor(limits.maxStringLength / 2)),\n maxDepth: Math.max(2, limits.maxDepth - 1)\n };\n }\n return JSON.stringify(prune(value, 0, limits)) ?? \"null\";\n}\n","/**\r\n * Minimal typings for the WebMCP browser API.\r\n *\r\n * The standard exposes `document.modelContext` (Chrome 149+ origin trial;\r\n * `navigator.modelContext` was the pre-Chrome-150 spelling and is deprecated).\r\n * Browsers without the API can get it from a polyfill such as `@mcp-b/global`.\r\n * Only the surface this library uses is typed.\r\n */\r\n\r\nexport interface ToolTextContent {\r\n type: \"text\";\r\n text: string;\r\n}\r\n\r\nexport type ToolContent = ToolTextContent;\r\n\r\nexport interface ToolResult {\r\n content: ToolContent[];\r\n isError?: boolean;\r\n}\r\n\r\nexport interface ToolAnnotations {\r\n readOnlyHint?: boolean;\r\n destructiveHint?: boolean;\r\n /**\r\n * High-stakes, irreversible or real-world action. Chrome 154+ uses it to\r\n * tell an agent to confirm with the person before calling the tool.\r\n */\r\n consequentialHint?: boolean;\r\n idempotentHint?: boolean;\r\n untrustedContentHint?: boolean;\r\n}\r\n\r\nexport interface JsonSchemaObject {\r\n type: \"object\";\r\n properties?: Record<string, unknown>;\r\n required?: string[];\r\n additionalProperties?: boolean;\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface ToolDefinition {\r\n name: string;\r\n description: string;\r\n inputSchema?: JsonSchemaObject;\r\n annotations?: ToolAnnotations;\r\n execute: (args: Record<string, unknown>) => Promise<ToolResult> | ToolResult;\r\n}\r\n\r\nexport interface RegisterToolOptions {\r\n /** Aborting this signal unregisters the tool. */\r\n signal?: AbortSignal;\r\n /** Origins allowed to see this tool when the page is framed. */\r\n exposedTo?: string[];\r\n}\r\n\r\nexport interface ModelContext {\r\n registerTool: (\r\n tool: ToolDefinition,\r\n options?: RegisterToolOptions\r\n ) => Promise<void> | void;\r\n getTools?: () => Promise<ToolDefinition[]>;\r\n addEventListener?: EventTarget[\"addEventListener\"];\r\n}\r\n\r\ndeclare global {\r\n interface Document {\r\n modelContext?: ModelContext;\r\n }\r\n}\r\n\r\n/** True when this browser can accept WebMCP tool registrations. */\r\nexport function webmcpSupported(): boolean {\r\n return (\r\n typeof document !== \"undefined\" &&\r\n typeof document.modelContext?.registerTool === \"function\"\r\n );\r\n}\r\n\r\n/** Canonical text result — identical shape across agents. */\r\nexport function textResult(text: string, isError = false): ToolResult {\r\n return isError\r\n ? { content: [{ type: \"text\", text }], isError: true }\r\n : { content: [{ type: \"text\", text }] };\r\n}\r\n","/**\r\n * Schema-gated actions. Every call is validated before your handler runs;\r\n * invalid input gets a structured refusal naming each violated field, so an\r\n * agent can fix its call instead of corrupting your state. The gate is\r\n * deterministic — the same input always gets the same verdict.\r\n */\r\nimport { useContext, useEffect, useRef, useState } from \"react\";\r\nimport { z } from \"zod\";\r\nimport { BoundaryContext } from \"./boundary\";\r\nimport {\r\n textResult,\r\n webmcpSupported,\r\n type JsonSchemaObject,\r\n type ToolResult\r\n} from \"./types\";\r\n\r\nexport interface AgentActionConfig<Schema extends z.ZodType = z.ZodType> {\r\n /** What this action does, written for the agent deciding whether to call it. */\r\n description: string;\r\n /** Input contract. Omit for zero-argument actions. */\r\n input?: Schema;\r\n /** True when the action changes nothing — lets agents call it freely. */\r\n readOnly?: boolean;\r\n /** True when the action destroys something a person would miss. */\r\n destructive?: boolean;\r\n /**\r\n * True for high-stakes, irreversible or real-world actions: placing an\r\n * order, moving money, booking a seat. Agents are expected to get explicit\r\n * confirmation from the person before calling one. Published as\r\n * `consequentialHint` (Chrome 154+); older browsers ignore the extra field.\r\n */\r\n consequential?: boolean;\r\n /**\r\n * Reserved: price per call (e.g. \"$0.001\"). Inert in v1 — declared here so\r\n * adding x402 settlement later is not a breaking change.\r\n */\r\n price?: string;\r\n /** Runs only after input passes the schema gate. */\r\n execute: (input: z.infer<Schema>) => Promise<unknown> | unknown;\r\n}\r\n\r\nexport interface AgentActionStatus {\r\n /** Whether this browser accepts WebMCP registrations. */\r\n supported: boolean;\r\n /** The name the tool was published under (boundary prefixes applied). */\r\n name: string;\r\n}\r\n\r\nfunction formatRefusal(toolName: string, error: z.ZodError): ToolResult {\r\n const violations = error.issues.map((issue) => {\r\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"input\";\r\n return `- ${path}: ${issue.message}`;\r\n });\r\n return textResult(\r\n `REFUSED: invalid input for \"${toolName}\".\\n` +\r\n `Violations (${violations.length}):\\n${violations.join(\"\\n\")}\\n\\n` +\r\n `Fix the specific violations above and call the tool again. ` +\r\n `Validation is deterministic — the same input always gets the same verdict.`,\r\n true\r\n );\r\n}\r\n\r\nfunction toInputSchema(schema: z.ZodType | undefined): JsonSchemaObject {\r\n if (!schema) return { type: \"object\", properties: {}, additionalProperties: false };\r\n const { $schema: _discard, ...rest } = z.toJSONSchema(schema) as Record<string, unknown>;\r\n return rest as JsonSchemaObject;\r\n}\r\n\r\n/**\r\n * Publish an action as a WebMCP tool for the lifetime of the component.\r\n * Inside an `<AgentBoundary name=\"cart\">`, `name` becomes `cart_name`.\r\n * The handler and description are read at call time through a ref, so\r\n * re-renders never re-register the tool.\r\n */\r\nexport function useAgentAction<Schema extends z.ZodType>(\r\n name: string,\r\n config: AgentActionConfig<Schema>\r\n): AgentActionStatus {\r\n const scope = useContext(BoundaryContext);\r\n const fullName = scope ? `${scope.replace(/\\./g, \"_\")}_${name}` : name;\r\n\r\n const configRef = useRef(config);\r\n configRef.current = config;\r\n\r\n const [supported, setSupported] = useState(false);\r\n\r\n useEffect(() => {\r\n if (!webmcpSupported()) return;\r\n const controller = new AbortController();\r\n const registeredWith = configRef.current;\r\n\r\n Promise.resolve(\r\n document.modelContext!.registerTool(\r\n {\r\n name: fullName,\r\n description: registeredWith.description,\r\n inputSchema: toInputSchema(registeredWith.input),\r\n annotations: {\r\n readOnlyHint: registeredWith.readOnly === true,\r\n ...(registeredWith.destructive !== undefined && {\r\n destructiveHint: registeredWith.destructive\r\n }),\r\n ...(registeredWith.consequential !== undefined && {\r\n consequentialHint: registeredWith.consequential\r\n })\r\n },\r\n execute: async (args) => {\r\n const current = configRef.current;\r\n let input: unknown = args;\r\n if (current.input) {\r\n const parsed = current.input.safeParse(args);\r\n if (!parsed.success) return formatRefusal(fullName, parsed.error);\r\n input = parsed.data;\r\n }\r\n try {\r\n const out = await current.execute(input as z.infer<Schema>);\r\n if (out === undefined) return textResult(\"Done.\");\r\n return textResult(typeof out === \"string\" ? out : JSON.stringify(out));\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n return textResult(`Error in \"${fullName}\": ${message}`, true);\r\n }\r\n }\r\n },\r\n { signal: controller.signal }\r\n )\r\n )\r\n .then(() => {\r\n if (!controller.signal.aborted) setSupported(true);\r\n })\r\n .catch(() => {\r\n /* registration failed — tool stays unpublished; page works for humans */\r\n });\r\n\r\n return () => controller.abort();\r\n }, [fullName]);\r\n\r\n return { supported, name: fullName };\r\n}\r\n"],"mappings":"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","names":["useContext","useContext","useContext","useEffect","useRef","useContext","useRef","useEffect"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentperf/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Give your React app a fast lane for AI agents: token-budgeted state snapshots and schema-gated actions on the WebMCP standard (document.modelContext).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {