@ggui-ai/protocol 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +46 -0
- package/dist/bridge/invoke-agent.d.ts +65 -0
- package/dist/bridge/invoke-agent.d.ts.map +1 -0
- package/dist/bridge/invoke-agent.js +113 -0
- package/dist/envelope-adapters.d.ts +24 -0
- package/dist/envelope-adapters.d.ts.map +1 -0
- package/dist/envelope-adapters.js +14 -0
- package/dist/envelopes/builders.d.ts +145 -0
- package/dist/envelopes/builders.d.ts.map +1 -0
- package/dist/envelopes/builders.js +113 -0
- package/dist/errors/unknown-permission-name.d.ts +12 -0
- package/dist/errors/unknown-permission-name.d.ts.map +1 -0
- package/dist/errors/unknown-permission-name.js +29 -0
- package/dist/errors/version-mismatch.d.ts +55 -0
- package/dist/errors/version-mismatch.d.ts.map +1 -0
- package/dist/errors/version-mismatch.js +52 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts +93 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts.map +1 -0
- package/dist/gadgets/resolve-contract-gadgets.js +119 -0
- package/dist/gadgets/stdlib-gadgets.d.ts +43 -0
- package/dist/gadgets/stdlib-gadgets.d.ts.map +1 -0
- package/dist/gadgets/stdlib-gadgets.js +161 -0
- package/dist/iframe-bridge.d.ts +63 -0
- package/dist/iframe-bridge.d.ts.map +1 -0
- package/dist/iframe-bridge.js +166 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/integrations/mcp-apps.d.ts +1218 -0
- package/dist/integrations/mcp-apps.d.ts.map +1 -0
- package/dist/integrations/mcp-apps.js +427 -0
- package/dist/navigation/index.d.ts +3 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +1 -0
- package/dist/navigation/stack-navigation.d.ts +55 -0
- package/dist/navigation/stack-navigation.d.ts.map +1 -0
- package/dist/navigation/stack-navigation.js +80 -0
- package/dist/recommended-prompts.d.ts +56 -0
- package/dist/recommended-prompts.d.ts.map +1 -0
- package/dist/recommended-prompts.js +55 -0
- package/dist/registry/blueprint-key.d.ts +9 -0
- package/dist/registry/blueprint-key.d.ts.map +1 -0
- package/dist/registry/blueprint-key.js +28 -0
- package/dist/registry/canonicalize-contract.d.ts +35 -0
- package/dist/registry/canonicalize-contract.d.ts.map +1 -0
- package/dist/registry/canonicalize-contract.js +166 -0
- package/dist/registry/summarize-contract.d.ts +46 -0
- package/dist/registry/summarize-contract.d.ts.map +1 -0
- package/dist/registry/summarize-contract.js +63 -0
- package/dist/schema-learning/derive-contract.d.ts +67 -0
- package/dist/schema-learning/derive-contract.d.ts.map +1 -0
- package/dist/schema-learning/derive-contract.js +117 -0
- package/dist/schema-learning/merge.d.ts +32 -0
- package/dist/schema-learning/merge.d.ts.map +1 -0
- package/dist/schema-learning/merge.js +146 -0
- package/dist/schemas/blueprint.d.ts +32 -0
- package/dist/schemas/blueprint.d.ts.map +1 -0
- package/dist/schemas/blueprint.js +92 -0
- package/dist/schemas/data-contract.d.ts +750 -0
- package/dist/schemas/data-contract.d.ts.map +1 -0
- package/dist/schemas/data-contract.js +663 -0
- package/dist/schemas/gadget-name-grammar.d.ts +29 -0
- package/dist/schemas/gadget-name-grammar.d.ts.map +1 -0
- package/dist/schemas/gadget-name-grammar.js +28 -0
- package/dist/schemas/handshake-suggestion.d.ts +46 -0
- package/dist/schemas/handshake-suggestion.d.ts.map +1 -0
- package/dist/schemas/handshake-suggestion.js +107 -0
- package/dist/schemas/invoke.d.ts +337 -0
- package/dist/schemas/invoke.d.ts.map +1 -0
- package/dist/schemas/invoke.js +169 -0
- package/dist/schemas/mcp.d.ts +301 -0
- package/dist/schemas/mcp.d.ts.map +1 -0
- package/dist/schemas/mcp.js +373 -0
- package/dist/schemas/ops-blueprint.d.ts +176 -0
- package/dist/schemas/ops-blueprint.d.ts.map +1 -0
- package/dist/schemas/ops-blueprint.js +259 -0
- package/dist/schemas/sync-check.d.ts +11 -0
- package/dist/schemas/sync-check.d.ts.map +1 -0
- package/dist/schemas/sync-check.js +60 -0
- package/dist/screen-blueprints/define.d.ts +22 -0
- package/dist/screen-blueprints/define.d.ts.map +1 -0
- package/dist/screen-blueprints/define.js +3 -0
- package/dist/screen-blueprints/index.d.ts +4 -0
- package/dist/screen-blueprints/index.d.ts.map +1 -0
- package/dist/screen-blueprints/index.js +3 -0
- package/dist/screen-blueprints/match.d.ts +35 -0
- package/dist/screen-blueprints/match.d.ts.map +1 -0
- package/dist/screen-blueprints/match.js +51 -0
- package/dist/screen-blueprints/types.d.ts +164 -0
- package/dist/screen-blueprints/types.d.ts.map +1 -0
- package/dist/screen-blueprints/types.js +1 -0
- package/dist/stream/stream-parser.d.ts +62 -0
- package/dist/stream/stream-parser.d.ts.map +1 -0
- package/dist/stream/stream-parser.js +199 -0
- package/dist/transport/websocket.d.ts +178 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +1 -0
- package/dist/types/app-config.d.ts +61 -0
- package/dist/types/app-config.d.ts.map +1 -0
- package/dist/types/app-config.js +1 -0
- package/dist/types/auth.d.ts +61 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +1 -0
- package/dist/types/blueprint.d.ts +206 -0
- package/dist/types/blueprint.d.ts.map +1 -0
- package/dist/types/blueprint.js +1 -0
- package/dist/types/canvas-lifecycle.d.ts +105 -0
- package/dist/types/canvas-lifecycle.d.ts.map +1 -0
- package/dist/types/canvas-lifecycle.js +38 -0
- package/dist/types/capabilities.d.ts +40 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +19 -0
- package/dist/types/contract-inference.d.ts +401 -0
- package/dist/types/contract-inference.d.ts.map +1 -0
- package/dist/types/contract-inference.js +44 -0
- package/dist/types/credential.d.ts +41 -0
- package/dist/types/credential.d.ts.map +1 -0
- package/dist/types/credential.js +32 -0
- package/dist/types/data-bindings.d.ts +322 -0
- package/dist/types/data-bindings.d.ts.map +1 -0
- package/dist/types/data-bindings.js +29 -0
- package/dist/types/data-contract.d.ts +1296 -0
- package/dist/types/data-contract.d.ts.map +1 -0
- package/dist/types/data-contract.js +111 -0
- package/dist/types/events.d.ts +182 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +8 -0
- package/dist/types/feedback.d.ts +24 -0
- package/dist/types/feedback.d.ts.map +1 -0
- package/dist/types/feedback.js +7 -0
- package/dist/types/gadget.d.ts +121 -0
- package/dist/types/gadget.d.ts.map +1 -0
- package/dist/types/gadget.js +24 -0
- package/dist/types/handshake-suggestion.d.ts +264 -0
- package/dist/types/handshake-suggestion.d.ts.map +1 -0
- package/dist/types/handshake-suggestion.js +70 -0
- package/dist/types/host-context.d.ts +163 -0
- package/dist/types/host-context.d.ts.map +1 -0
- package/dist/types/host-context.js +142 -0
- package/dist/types/interface-context.d.ts +105 -0
- package/dist/types/interface-context.d.ts.map +1 -0
- package/dist/types/interface-context.js +115 -0
- package/dist/types/invoke.d.ts +28 -0
- package/dist/types/invoke.d.ts.map +1 -0
- package/dist/types/invoke.js +7 -0
- package/dist/types/live-channel.d.ts +613 -0
- package/dist/types/live-channel.d.ts.map +1 -0
- package/dist/types/live-channel.js +1 -0
- package/dist/types/llm.d.ts +61 -0
- package/dist/types/llm.d.ts.map +1 -0
- package/dist/types/llm.js +186 -0
- package/dist/types/mcp-proxy.d.ts +67 -0
- package/dist/types/mcp-proxy.d.ts.map +1 -0
- package/dist/types/mcp-proxy.js +46 -0
- package/dist/types/mcp.d.ts +637 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +30 -0
- package/dist/types/openrouter-models.d.ts +22 -0
- package/dist/types/openrouter-models.d.ts.map +1 -0
- package/dist/types/openrouter-models.js +4843 -0
- package/dist/types/region.d.ts +26 -0
- package/dist/types/region.d.ts.map +1 -0
- package/dist/types/region.js +36 -0
- package/dist/types/session.d.ts +419 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +1 -0
- package/dist/types/thread.d.ts +207 -0
- package/dist/types/thread.d.ts.map +1 -0
- package/dist/types/thread.js +57 -0
- package/dist/types/ui-generator.d.ts +100 -0
- package/dist/types/ui-generator.d.ts.map +1 -0
- package/dist/types/ui-generator.js +53 -0
- package/dist/validation/ajv-runtime.d.ts +140 -0
- package/dist/validation/ajv-runtime.d.ts.map +1 -0
- package/dist/validation/ajv-runtime.js +452 -0
- package/dist/validation/content-hash.d.ts +3 -0
- package/dist/validation/content-hash.d.ts.map +1 -0
- package/dist/validation/content-hash.js +21 -0
- package/dist/validation/contract-validator.d.ts +244 -0
- package/dist/validation/contract-validator.d.ts.map +1 -0
- package/dist/validation/contract-validator.js +711 -0
- package/dist/validation/cross-references.d.ts +105 -0
- package/dist/validation/cross-references.d.ts.map +1 -0
- package/dist/validation/cross-references.js +164 -0
- package/dist/validation/hygiene-rules.d.ts +250 -0
- package/dist/validation/hygiene-rules.d.ts.map +1 -0
- package/dist/validation/hygiene-rules.js +564 -0
- package/dist/validation/lint-contract.d.ts +130 -0
- package/dist/validation/lint-contract.d.ts.map +1 -0
- package/dist/validation/lint-contract.js +225 -0
- package/dist/validation/name-invariants.d.ts +117 -0
- package/dist/validation/name-invariants.d.ts.map +1 -0
- package/dist/validation/name-invariants.js +172 -0
- package/dist/validation/reserved-channels.d.ts +156 -0
- package/dist/validation/reserved-channels.d.ts.map +1 -0
- package/dist/validation/reserved-channels.js +356 -0
- package/dist/validation/resolve-stream-channel.d.ts +78 -0
- package/dist/validation/resolve-stream-channel.d.ts.map +1 -0
- package/dist/validation/resolve-stream-channel.js +64 -0
- package/dist/validation/sanitize-error.d.ts +46 -0
- package/dist/validation/sanitize-error.d.ts.map +1 -0
- package/dist/validation/sanitize-error.js +88 -0
- package/dist/validation/schema-compat-invariants.d.ts +140 -0
- package/dist/validation/schema-compat-invariants.d.ts.map +1 -0
- package/dist/validation/schema-compat-invariants.js +220 -0
- package/dist/validation/schema-meta-validation.d.ts +60 -0
- package/dist/validation/schema-meta-validation.d.ts.map +1 -0
- package/dist/validation/schema-meta-validation.js +131 -0
- package/dist/validation/schema-subset.d.ts +165 -0
- package/dist/validation/schema-subset.d.ts.map +1 -0
- package/dist/validation/schema-subset.js +295 -0
- package/dist/validation/ui-security.d.ts +54 -0
- package/dist/validation/ui-security.d.ts.map +1 -0
- package/dist/validation/ui-security.js +138 -0
- package/dist/validation/zod-to-json-schema.d.ts +63 -0
- package/dist/validation/zod-to-json-schema.d.ts.map +1 -0
- package/dist/validation/zod-to-json-schema.js +126 -0
- package/dist/version.d.ts +1458 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1459 -0
- package/package.json +113 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommended agent-side system prompts for hosts running ggui.
|
|
3
|
+
*
|
|
4
|
+
* The wire protocol already carries its own self-teaching surfaces:
|
|
5
|
+
*
|
|
6
|
+
* - Per-tool `description` strings on every `ggui_*` MCP tool.
|
|
7
|
+
* - The server's `InitializeResult.instructions` field (set via
|
|
8
|
+
* `@ggui-ai/mcp-server`'s `MCP_INSTRUCTIONS_PRESETS`).
|
|
9
|
+
*
|
|
10
|
+
* Those two carry the wire flow (new_session → handshake → push →
|
|
11
|
+
* consume → react), the contract-authoring rules, recovery shapes,
|
|
12
|
+
* and the mutation rule. **Agent builders should NOT replicate any
|
|
13
|
+
* of that in their system prompt.** The protocol is designed to be
|
|
14
|
+
* self-teaching from the host's side.
|
|
15
|
+
*
|
|
16
|
+
* What an agent-side system prompt SHOULD do: set the agent's role
|
|
17
|
+
* and posture — "you render UIs, you don't reply in plain text" —
|
|
18
|
+
* so the model reaches for ggui_* tools instead of conversational
|
|
19
|
+
* responses. That's a *posture cue*, not a procedural script.
|
|
20
|
+
*
|
|
21
|
+
* On hosts like claude.ai, the host's own baseline system prompt
|
|
22
|
+
* already nudges tool usage, so a one-line user instruction
|
|
23
|
+
* ("Always respond using ggui_* tools") is enough. On raw SDK hosts
|
|
24
|
+
* (Claude Agent SDK, OpenAI Assistants, etc.) that baseline is
|
|
25
|
+
* absent, so the recommended prompt below carries slightly more
|
|
26
|
+
* role context to compensate.
|
|
27
|
+
*
|
|
28
|
+
* If an agent isn't following the wire flow correctly even with
|
|
29
|
+
* this prompt, the bug lives in the protocol's tool descriptions
|
|
30
|
+
* or server instructions — fix THOSE, not this string.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The recommended one-line system prompt for ggui-aware agents.
|
|
34
|
+
*
|
|
35
|
+
* Posture-setting only. Carries no procedural detail — the wire flow
|
|
36
|
+
* is taught by the server's `InitializeResult.instructions` and
|
|
37
|
+
* per-tool `description` strings.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
42
|
+
* import { GGUI_AGENT_SYSTEM_PROMPT } from '@ggui-ai/protocol/recommended-prompts';
|
|
43
|
+
*
|
|
44
|
+
* query({
|
|
45
|
+
* prompt: userInput,
|
|
46
|
+
* options: {
|
|
47
|
+
* systemPrompt: GGUI_AGENT_SYSTEM_PROMPT,
|
|
48
|
+
* mcpServers: { ggui: { type: 'http', url: 'http://localhost:6781/mcp' } },
|
|
49
|
+
* },
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export const GGUI_AGENT_SYSTEM_PROMPT = 'You are a UI agent. Respond to every user request by rendering an interactive UI via the ggui_* MCP tools instead of by replying in plain text. The tool descriptions and server instructions explain the wire flow — follow them.';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DataContract } from '../types/data-contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compute the canonical 16-char identity hash for a contract shape.
|
|
4
|
+
*
|
|
5
|
+
* Pure function, no I/O. Deterministic across runs / processes /
|
|
6
|
+
* Node versions (sha256 + utf-8 encoding are well-specified).
|
|
7
|
+
*/
|
|
8
|
+
export declare function blueprintKey(contract: DataContract | undefined): string;
|
|
9
|
+
//# sourceMappingURL=blueprint-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blueprint-key.d.ts","sourceRoot":"","sources":["../../src/registry/blueprint-key.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAG9D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,CAGvE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic identity hash for a `DataContract` shape.
|
|
3
|
+
*
|
|
4
|
+
* `blueprintKey(contract)` is the Tier 1 exact-match key in the
|
|
5
|
+
* blueprint registry: two contract that canonicalize to the same
|
|
6
|
+
* string produce the same key, regardless of how the agent
|
|
7
|
+
* paraphrased the surrounding intent. Equal key → guaranteed
|
|
8
|
+
* registry lookup hit (no LLM rerank, no embedding similarity).
|
|
9
|
+
*
|
|
10
|
+
* 16-character sha256 prefix — matches the existing `blueprintHash`
|
|
11
|
+
* shape in `cache-trace-sink` and `generation-cache.ts`. Collision
|
|
12
|
+
* probability for 100s-of-thousands of distinct contract is ~10^-6
|
|
13
|
+
* (birthday-bound on 2^64), well below the budget for the OSS
|
|
14
|
+
* single-tenant scope. Hosted multi-tenant deployments scope keys
|
|
15
|
+
* by appId so the bound is per-tenant, never global.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { canonicalizeContracts } from './canonicalize-contract.js';
|
|
19
|
+
/**
|
|
20
|
+
* Compute the canonical 16-char identity hash for a contract shape.
|
|
21
|
+
*
|
|
22
|
+
* Pure function, no I/O. Deterministic across runs / processes /
|
|
23
|
+
* Node versions (sha256 + utf-8 encoding are well-specified).
|
|
24
|
+
*/
|
|
25
|
+
export function blueprintKey(contract) {
|
|
26
|
+
const canonical = canonicalizeContracts(contract);
|
|
27
|
+
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DataContract } from '../types/data-contract.js';
|
|
2
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Walk `value` recursively and produce a structurally-equivalent
|
|
7
|
+
* tree with stripped keys removed and all strings NFC-normalized.
|
|
8
|
+
* Arrays preserved in order; primitives unchanged except for Unicode
|
|
9
|
+
* normalization on strings; `undefined` collapses to absent (mirrors
|
|
10
|
+
* JSON behavior). Object key sorting + JSON serialization happen in
|
|
11
|
+
* the downstream JCS pass — this function handles the domain-specific
|
|
12
|
+
* strip step PLUS Unicode normalization (RFC 8785 leaves NFC out of
|
|
13
|
+
* scope; we apply it ourselves so visually-identical contracts hash
|
|
14
|
+
* identically regardless of the agent's keyboard / IME composition).
|
|
15
|
+
*
|
|
16
|
+
* Exported for tests / debugging — production callers should use
|
|
17
|
+
* `canonicalizeContracts()`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function canonicalizeValue(value: unknown): JsonValue | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Produce the canonical bytes for a `DataContract` value, suitable
|
|
22
|
+
* for hashing or external content-address lookups. Stable across
|
|
23
|
+
* paraphrase, key order, whitespace, and description-only edits.
|
|
24
|
+
*
|
|
25
|
+
* Empty / undefined / `{}` all collapse to the same canonical bytes,
|
|
26
|
+
* which produces a stable `blueprintKey` for the "no-contract" case.
|
|
27
|
+
* That key isn't used for cache lookups (registry refuses to register
|
|
28
|
+
* contract-less pushes) but stays well-defined for completeness.
|
|
29
|
+
*
|
|
30
|
+
* The output is a UTF-8-encoded JSON string per RFC 8785. External
|
|
31
|
+
* implementations using any JCS library produce the same bytes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function canonicalizeContracts(contract: DataContract | undefined): string;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=canonicalize-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonicalize-contract.d.ts","sourceRoot":"","sources":["../../src/registry/canonicalize-contract.ts"],"names":[],"mappings":"AAyEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAW9D,KAAK,SAAS,GACV,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CA4CvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,CAOhF"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical, deterministic serialization of `DataContract` per
|
|
3
|
+
* RFC 8785 (JSON Canonicalization Scheme — JCS).
|
|
4
|
+
*
|
|
5
|
+
* Goal: two contracts with the same WIRE-OBSERVABLE shape produce
|
|
6
|
+
* the same canonical bytes, regardless of key order, whitespace, or
|
|
7
|
+
* description-only differences. The canonical string is the input to
|
|
8
|
+
* `blueprintKey()`; equal canonical strings → equal cache key →
|
|
9
|
+
* exact-match hit at handshake time.
|
|
10
|
+
*
|
|
11
|
+
* # Why RFC 8785
|
|
12
|
+
*
|
|
13
|
+
* JCS is the IETF standard for deterministic JSON serialization
|
|
14
|
+
* (published 2020). External implementers can compute the same hash
|
|
15
|
+
* we do using any JCS library (`canonicalize` on npm, `gocanon` in
|
|
16
|
+
* Go, `pyjcs` in Python, `serde_jcs` in Rust). That makes the
|
|
17
|
+
* `contractHash` field a portable identity claim — agents can verify
|
|
18
|
+
* a hash off-server without knowing our internal format.
|
|
19
|
+
*
|
|
20
|
+
* # Pipeline
|
|
21
|
+
*
|
|
22
|
+
* stripDescriptions(contract) ← domain rule, OURS
|
|
23
|
+
* ↓
|
|
24
|
+
* canonicalize(value) ← RFC 8785, vendored library
|
|
25
|
+
* ↓
|
|
26
|
+
* utf-8 bytes, then sha256 + 16-char hex prefix (in `blueprint-key.ts`)
|
|
27
|
+
*
|
|
28
|
+
* # Domain rule: informational prose is stripped
|
|
29
|
+
*
|
|
30
|
+
* `description` and `usage` are informational only — they don't alter
|
|
31
|
+
* the wire surface (no React.Context is mounted from a description,
|
|
32
|
+
* no action gets routed by a usage hint). Stripping them means a
|
|
33
|
+
* documentation tweak doesn't invalidate a registered blueprint, and
|
|
34
|
+
* an agent's intent-prose override on a gadget export-use entry (the
|
|
35
|
+
* `description?` / `usage?` fields of each
|
|
36
|
+
* `clientCapabilities.gadgets[<package>][<exportName>]`, agent-authored
|
|
37
|
+
* at push time) does not pollute the cache key.
|
|
38
|
+
*
|
|
39
|
+
* The strip is keyed on `description`/`usage` as STRING-VALUED fields,
|
|
40
|
+
* never as map keys. A gadget package, an `agentCapabilities.tools`
|
|
41
|
+
* entry, or a spec slot may legitimately be NAMED `usage` (npm package
|
|
42
|
+
* names allow it) — that key maps to an object and MUST survive, or two
|
|
43
|
+
* distinct contracts collide onto one blueprint cache key and the wrong
|
|
44
|
+
* cached UI is served. Informational prose is always a string; a data
|
|
45
|
+
* map key named `usage`/`description` always maps to an object/array —
|
|
46
|
+
* so the value-type test cleanly separates the two. If QA later shows
|
|
47
|
+
* the prose affects LLM-generated copy enough to be load-bearing, flip
|
|
48
|
+
* `STRIPPED_KEYS`.
|
|
49
|
+
*
|
|
50
|
+
* # Preserved (load-bearing for behavior)
|
|
51
|
+
*
|
|
52
|
+
* - The four typed specs: `propsSpec`, `actionSpec`, `contextSpec`,
|
|
53
|
+
* `streamSpec` (including `streamSpec[*].source.tool` cross-refs).
|
|
54
|
+
* - The two catalogs: `agentCapabilities.tools[*]` (key names + their
|
|
55
|
+
* `inputSchema`/`outputSchema`) and `clientCapabilities.gadgets`
|
|
56
|
+
* (package-keyed — npm package names as outer keys, export names as
|
|
57
|
+
* the inner `exports` map keys; no `version`, no transport metadata
|
|
58
|
+
* on the wire).
|
|
59
|
+
* - Inner JSON Schema fields under every `schema:` wrapper (`type`,
|
|
60
|
+
* `enum`, `properties`, `required`, `items`, `additionalProperties`,
|
|
61
|
+
* `nullable`, `format`, …).
|
|
62
|
+
* - `default` values, `required` flags, `nextStep` hints (cross-ref
|
|
63
|
+
* identity), `confirm` / `icon` / `label` / `mode` / `replay` on
|
|
64
|
+
* their respective spec entries.
|
|
65
|
+
* - `enum` array order — order matters for select UIs; JCS preserves
|
|
66
|
+
* array order so we don't pre-sort.
|
|
67
|
+
* - Slot / action / stream channel / agent-capability tool names,
|
|
68
|
+
* plus gadget package keys and per-package export names (every key
|
|
69
|
+
* in the four specs + two catalogs).
|
|
70
|
+
*
|
|
71
|
+
* Pure function — no I/O, no globals.
|
|
72
|
+
*/
|
|
73
|
+
import canonicalize from 'canonicalize';
|
|
74
|
+
/**
|
|
75
|
+
* Informational-prose field names removed from the canonical form —
|
|
76
|
+
* but ONLY where they are string-valued (see `canonicalizeValue`); a
|
|
77
|
+
* same-named map key is data and is preserved. Stripping happens
|
|
78
|
+
* BEFORE JCS — JCS itself has no concept of "ignorable fields"; that's
|
|
79
|
+
* the protocol's domain rule, not the serialization standard's.
|
|
80
|
+
*/
|
|
81
|
+
const STRIPPED_KEYS = new Set(['description', 'usage']);
|
|
82
|
+
/**
|
|
83
|
+
* Walk `value` recursively and produce a structurally-equivalent
|
|
84
|
+
* tree with stripped keys removed and all strings NFC-normalized.
|
|
85
|
+
* Arrays preserved in order; primitives unchanged except for Unicode
|
|
86
|
+
* normalization on strings; `undefined` collapses to absent (mirrors
|
|
87
|
+
* JSON behavior). Object key sorting + JSON serialization happen in
|
|
88
|
+
* the downstream JCS pass — this function handles the domain-specific
|
|
89
|
+
* strip step PLUS Unicode normalization (RFC 8785 leaves NFC out of
|
|
90
|
+
* scope; we apply it ourselves so visually-identical contracts hash
|
|
91
|
+
* identically regardless of the agent's keyboard / IME composition).
|
|
92
|
+
*
|
|
93
|
+
* Exported for tests / debugging — production callers should use
|
|
94
|
+
* `canonicalizeContracts()`.
|
|
95
|
+
*/
|
|
96
|
+
export function canonicalizeValue(value) {
|
|
97
|
+
if (value === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (value === null)
|
|
100
|
+
return null;
|
|
101
|
+
const t = typeof value;
|
|
102
|
+
if (t === 'string')
|
|
103
|
+
return value.normalize('NFC');
|
|
104
|
+
if (t === 'number' || t === 'boolean') {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const item of value) {
|
|
110
|
+
const c = canonicalizeValue(item);
|
|
111
|
+
if (c !== undefined)
|
|
112
|
+
out.push(c);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
if (t === 'object') {
|
|
117
|
+
const obj = value;
|
|
118
|
+
// NFC-normalize keys before sort so two contracts with the same
|
|
119
|
+
// key in different normalization forms (precomposed "café" vs
|
|
120
|
+
// decomposed "café") collapse to one identity. Keep the original
|
|
121
|
+
// key for lookup; emit the normalized key in output. Keys sorted
|
|
122
|
+
// alphabetically here too — idempotent with the downstream JCS
|
|
123
|
+
// pass, but keeps the intermediate-form output predictable for
|
|
124
|
+
// tests/debugging that consume `canonicalizeValue` directly.
|
|
125
|
+
const entries = Object.keys(obj)
|
|
126
|
+
// Strip `description`/`usage` ONLY when string-valued — i.e. only
|
|
127
|
+
// where they are informational prose fields. A key that IS
|
|
128
|
+
// literally `description`/`usage` as a MAP key (a gadget package,
|
|
129
|
+
// an `agentCapabilities.tools` entry, a spec slot) maps to an
|
|
130
|
+
// object/array and MUST survive: stripping it collapses distinct
|
|
131
|
+
// contracts onto one blueprint cache key.
|
|
132
|
+
.filter((k) => !(STRIPPED_KEYS.has(k) && typeof obj[k] === 'string'))
|
|
133
|
+
.map((k) => ({ source: k, normalized: k.normalize('NFC') }))
|
|
134
|
+
.sort((a, b) => (a.normalized < b.normalized ? -1 : a.normalized > b.normalized ? 1 : 0));
|
|
135
|
+
const out = {};
|
|
136
|
+
for (const { source, normalized } of entries) {
|
|
137
|
+
const c = canonicalizeValue(obj[source]);
|
|
138
|
+
if (c !== undefined)
|
|
139
|
+
out[normalized] = c;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
// Functions, symbols, bigint — not JSON-serializable; treat as absent.
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Produce the canonical bytes for a `DataContract` value, suitable
|
|
148
|
+
* for hashing or external content-address lookups. Stable across
|
|
149
|
+
* paraphrase, key order, whitespace, and description-only edits.
|
|
150
|
+
*
|
|
151
|
+
* Empty / undefined / `{}` all collapse to the same canonical bytes,
|
|
152
|
+
* which produces a stable `blueprintKey` for the "no-contract" case.
|
|
153
|
+
* That key isn't used for cache lookups (registry refuses to register
|
|
154
|
+
* contract-less pushes) but stays well-defined for completeness.
|
|
155
|
+
*
|
|
156
|
+
* The output is a UTF-8-encoded JSON string per RFC 8785. External
|
|
157
|
+
* implementations using any JCS library produce the same bytes.
|
|
158
|
+
*/
|
|
159
|
+
export function canonicalizeContracts(contract) {
|
|
160
|
+
const stripped = canonicalizeValue(contract ?? {});
|
|
161
|
+
// `canonicalize` may return undefined if every field was stripped;
|
|
162
|
+
// fall back to the empty-object canonical form so the hash function
|
|
163
|
+
// always receives a stable string.
|
|
164
|
+
const result = canonicalize(stripped);
|
|
165
|
+
return result ?? '{}';
|
|
166
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-line, paraphrase-stable summary of a `DataContract` shape.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers share this string:
|
|
5
|
+
* 1. **Embedding input** — concatenated with the intent prose
|
|
6
|
+
* before bge-small embedding. The structured summary anchors
|
|
7
|
+
* retrieval to slot/action names that survive prose
|
|
8
|
+
* paraphrase, while the prose half feeds bge-small's
|
|
9
|
+
* topic-similarity awareness. Hybrid input.
|
|
10
|
+
* 2. **LLM rerank prompt** — shown to Haiku alongside the user's
|
|
11
|
+
* query so the judge has the structural context it needs to
|
|
12
|
+
* decide match-vs-no-match.
|
|
13
|
+
*
|
|
14
|
+
* Putting both consumers on the same string is load-bearing: if the
|
|
15
|
+
* embedded vector and the prompt-shown summary diverged, we'd get
|
|
16
|
+
* "RAG retrieved candidate X, but the prompt shows a different
|
|
17
|
+
* summary" — the judge would lose context the index used to
|
|
18
|
+
* retrieve. One source of truth eliminates that drift class.
|
|
19
|
+
*
|
|
20
|
+
* Format (deterministic):
|
|
21
|
+
*
|
|
22
|
+
* slots=<name:type,...>;
|
|
23
|
+
* actions=<name(payloadFields)?,...>; streams=<name,...>;
|
|
24
|
+
* props=<name:type,...>
|
|
25
|
+
*
|
|
26
|
+
* Format details:
|
|
27
|
+
* - slots: `name:type` (e.g. `count:number,draft:string`). Bare
|
|
28
|
+
* `name` when the schema has no `type` field.
|
|
29
|
+
* - actions: `name(field1,field2,...)` when the action's schema has
|
|
30
|
+
* non-empty `properties`; bare `name` when payload-less. This
|
|
31
|
+
* differentiation is load-bearing: a `sendChip` action with
|
|
32
|
+
* `{chipText: string}` payload must NOT match a payload-less
|
|
33
|
+
* `sendChip` cached blueprint, since the runtime-emitted action
|
|
34
|
+
* would arrive with `data: {}` instead of `data: {chipText}` —
|
|
35
|
+
* the agent loses the user's input. Surfacing payload field names
|
|
36
|
+
* in the summary lets the judge see this distinction.
|
|
37
|
+
* - streams: bare `name` (channel schemas vary a lot and are usually
|
|
38
|
+
* emitted by the agent later; including them adds noise).
|
|
39
|
+
* - props: `name:type` (initial render shape).
|
|
40
|
+
*
|
|
41
|
+
* Empty slots/actions/streams/props collapse to `∅`.
|
|
42
|
+
*/
|
|
43
|
+
import type { DataContract } from '../types/data-contract.js';
|
|
44
|
+
/** Stable summary of `contract` for embedding + rerank input. */
|
|
45
|
+
export declare function summarizeContract(contract: DataContract | undefined): string;
|
|
46
|
+
//# sourceMappingURL=summarize-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summarize-contract.d.ts","sourceRoot":"","sources":["../../src/registry/summarize-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAE9D,iEAAiE;AACjE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,GAAG,SAAS,GACjC,MAAM,CA6CR"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Stable summary of `contract` for embedding + rerank input. */
|
|
2
|
+
export function summarizeContract(contract) {
|
|
3
|
+
if (!contract)
|
|
4
|
+
return 'slots=∅; actions=∅; streams=∅';
|
|
5
|
+
const slots = contract.contextSpec
|
|
6
|
+
? Object.entries(contract.contextSpec)
|
|
7
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
8
|
+
.map(([name, entry]) => {
|
|
9
|
+
const t = readType(entry.schema);
|
|
10
|
+
return t ? `${name}:${t}` : name;
|
|
11
|
+
})
|
|
12
|
+
.join(',')
|
|
13
|
+
: '';
|
|
14
|
+
const actions = contract.actionSpec
|
|
15
|
+
? Object.entries(contract.actionSpec)
|
|
16
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
17
|
+
.map(([name, entry]) => {
|
|
18
|
+
const fields = readPayloadFields(entry.schema);
|
|
19
|
+
return fields ? `${name}(${fields})` : name;
|
|
20
|
+
})
|
|
21
|
+
.join(',')
|
|
22
|
+
: '';
|
|
23
|
+
const streams = contract.streamSpec
|
|
24
|
+
? Object.keys(contract.streamSpec).sort().join(',')
|
|
25
|
+
: '';
|
|
26
|
+
const props = contract.propsSpec?.properties
|
|
27
|
+
? Object.entries(contract.propsSpec.properties)
|
|
28
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
29
|
+
.map(([name, entry]) => {
|
|
30
|
+
const t = readType(entry.schema);
|
|
31
|
+
return t ? `${name}:${t}` : name;
|
|
32
|
+
})
|
|
33
|
+
.join(',')
|
|
34
|
+
: '';
|
|
35
|
+
const parts = [];
|
|
36
|
+
parts.push(`slots=${slots || '∅'}`);
|
|
37
|
+
parts.push(`actions=${actions || '∅'}`);
|
|
38
|
+
parts.push(`streams=${streams || '∅'}`);
|
|
39
|
+
if (props)
|
|
40
|
+
parts.push(`props=${props}`);
|
|
41
|
+
return parts.join('; ');
|
|
42
|
+
}
|
|
43
|
+
function readType(schema) {
|
|
44
|
+
if (typeof schema !== 'object' || schema === null)
|
|
45
|
+
return null;
|
|
46
|
+
const t = schema.type;
|
|
47
|
+
return typeof t === 'string' ? t : null;
|
|
48
|
+
}
|
|
49
|
+
/** Sorted comma-joined property names for an action's payload schema,
|
|
50
|
+
* or `null` when the action takes no payload. Empty `properties` →
|
|
51
|
+
* null (payload-less). */
|
|
52
|
+
function readPayloadFields(schema) {
|
|
53
|
+
if (typeof schema !== 'object' || schema === null)
|
|
54
|
+
return null;
|
|
55
|
+
const props = schema.properties;
|
|
56
|
+
if (typeof props !== 'object' || props === null || Array.isArray(props)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const keys = Object.keys(props);
|
|
60
|
+
if (keys.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
return keys.sort().join(',');
|
|
63
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic {@link DataContract} derivation from MCP tool schemas.
|
|
3
|
+
*
|
|
4
|
+
* Given a primary data tool (with `outputSchema`) and optional action tools
|
|
5
|
+
* (with `inputSchema`), produces a complete contract — no LLM needed.
|
|
6
|
+
*
|
|
7
|
+
* This is the Screen Designer's Tier 1 fast path: when a ggui-first-party MCP
|
|
8
|
+
* server ships native `outputSchema` on every tool, the contract falls out of
|
|
9
|
+
* the schema automatically. The agent supplies the wiring (data tool + action
|
|
10
|
+
* tools); ggui derives the rendering contract.
|
|
11
|
+
*
|
|
12
|
+
* Tier 2 (learned `generatedOutputSchema`) uses the same deriver — the caller
|
|
13
|
+
* passes the learned schema instead of the native one.
|
|
14
|
+
*/
|
|
15
|
+
import type { ActionSpec, DataContract, JsonSchema, PropsSpec } from "../types/data-contract.js";
|
|
16
|
+
/** An MCP tool spec, as it appears in tools/list (subset we care about). */
|
|
17
|
+
export interface McpToolSpec {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
inputSchema?: JsonSchema;
|
|
21
|
+
outputSchema?: JsonSchema;
|
|
22
|
+
}
|
|
23
|
+
export interface DeriveContractInput {
|
|
24
|
+
/** Display name of the owning server, used to build the intent string. */
|
|
25
|
+
serverName: string;
|
|
26
|
+
/** Primary tool — its outputSchema becomes the component's props. */
|
|
27
|
+
dataTool: McpToolSpec;
|
|
28
|
+
/** Optional MCP tools the UI hints at on its gestures. Each becomes an ActionEntry with `nextStep` set to the tool name (advisory hint the agent reads on `ggui_consume` to decide which tool to call next). */
|
|
29
|
+
actionTools?: McpToolSpec[];
|
|
30
|
+
/** Optional intent override. Default: derived from serverName + dataTool.name. */
|
|
31
|
+
intent?: string;
|
|
32
|
+
/** Optional tool-name prefix to strip when generating action keys and labels.
|
|
33
|
+
* Only strips when the tool name actually starts with `${toolPrefix}`. Default: none —
|
|
34
|
+
* the full tool name is used. Supply this only for servers that prefix their tools
|
|
35
|
+
* (e.g. `toolPrefix: "gmail_"` turns `gmail_search_messages` into `searchMessages`).
|
|
36
|
+
* Bare-named tools (`get_task`, `complete_task`) must NOT use this. */
|
|
37
|
+
toolPrefix?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a DataContract from native MCP tool schemas.
|
|
41
|
+
*
|
|
42
|
+
* Behavior:
|
|
43
|
+
* - `intent` — "<serverName> — <humanized tool verb>" unless overridden.
|
|
44
|
+
* - `props` — each top-level property of dataTool.outputSchema becomes a PropEntry.
|
|
45
|
+
* If outputSchema is not an object schema, a single `data` prop wraps the whole thing.
|
|
46
|
+
* - `actions` — one ActionEntry per actionTool, with `tool` wired to the MCP tool name
|
|
47
|
+
* and `schema` set to the tool's inputSchema (if present).
|
|
48
|
+
*
|
|
49
|
+
* Pure — deterministic given identical input.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deriveContract(input: DeriveContractInput): DataContract;
|
|
52
|
+
/** Convert an outputSchema into a PropsSpec. */
|
|
53
|
+
export declare function propsFromOutputSchema(outputSchema: JsonSchema | undefined, description?: string): PropsSpec | undefined;
|
|
54
|
+
/** Convert a list of action-tools into an ActionSpec, one entry per tool.
|
|
55
|
+
* Collisions (two tools camelCase-ing to the same key) are resolved by appending
|
|
56
|
+
* a numeric suffix — the raw tool name is always preserved on the
|
|
57
|
+
* entry's `nextStep` hint. */
|
|
58
|
+
export declare function actionsFromTools(actionTools: McpToolSpec[], toolPrefix?: string): ActionSpec;
|
|
59
|
+
/** Humanize a tool name, optionally stripping a known server prefix.
|
|
60
|
+
* `humanizeToolName("gmail_search_messages", "gmail_")` → "Search Messages".
|
|
61
|
+
* `humanizeToolName("get_task")` → "Get Task". */
|
|
62
|
+
export declare function humanizeToolName(name: string, toolPrefix?: string): string;
|
|
63
|
+
/** Strip `toolPrefix` iff `name` starts with it; otherwise return `name` unchanged. */
|
|
64
|
+
export declare function stripPrefix(name: string, toolPrefix?: string): string;
|
|
65
|
+
/** "search_messages" → "searchMessages". */
|
|
66
|
+
export declare function camelKey(name: string): string;
|
|
67
|
+
//# sourceMappingURL=derive-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-contract.d.ts","sourceRoot":"","sources":["../../src/schema-learning/derive-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAEV,UAAU,EACV,YAAY,EACZ,UAAU,EAEV,SAAS,EACV,MAAM,2BAA2B,CAAC;AAEnC,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,YAAY,CAAC,EAAE,UAAU,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,QAAQ,EAAE,WAAW,CAAC;IACtB,gNAAgN;IAChN,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;2EAIuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAiBvE;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,UAAU,GAAG,SAAS,EACpC,WAAW,CAAC,EAAE,MAAM,GACnB,SAAS,GAAG,SAAS,CA2BvB;AAED;;;8BAG8B;AAC9B,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CAkB5F;AAUD;;kDAEkD;AAClD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAO1E;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAGrE;AAED,4CAA4C;AAC5C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK7C"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a DataContract from native MCP tool schemas.
|
|
3
|
+
*
|
|
4
|
+
* Behavior:
|
|
5
|
+
* - `intent` — "<serverName> — <humanized tool verb>" unless overridden.
|
|
6
|
+
* - `props` — each top-level property of dataTool.outputSchema becomes a PropEntry.
|
|
7
|
+
* If outputSchema is not an object schema, a single `data` prop wraps the whole thing.
|
|
8
|
+
* - `actions` — one ActionEntry per actionTool, with `tool` wired to the MCP tool name
|
|
9
|
+
* and `schema` set to the tool's inputSchema (if present).
|
|
10
|
+
*
|
|
11
|
+
* Pure — deterministic given identical input.
|
|
12
|
+
*/
|
|
13
|
+
export function deriveContract(input) {
|
|
14
|
+
const { dataTool, actionTools = [], toolPrefix } = input;
|
|
15
|
+
// `intent` is not a contract field. `input.intent` and `serverName`
|
|
16
|
+
// stay on the input so callers can describe the contract for their
|
|
17
|
+
// own purposes (logging, embedding-search keys); the returned
|
|
18
|
+
// contract carries no intent of its own. See {@link DataContract}.
|
|
19
|
+
const contract = {};
|
|
20
|
+
const props = propsFromOutputSchema(dataTool.outputSchema, dataTool.description);
|
|
21
|
+
if (props)
|
|
22
|
+
contract.propsSpec = props;
|
|
23
|
+
if (actionTools.length > 0) {
|
|
24
|
+
contract.actionSpec = actionsFromTools(actionTools, toolPrefix);
|
|
25
|
+
}
|
|
26
|
+
return contract;
|
|
27
|
+
}
|
|
28
|
+
/** Convert an outputSchema into a PropsSpec. */
|
|
29
|
+
export function propsFromOutputSchema(outputSchema, description) {
|
|
30
|
+
if (!outputSchema)
|
|
31
|
+
return undefined;
|
|
32
|
+
// Object schema — one PropEntry per top-level property.
|
|
33
|
+
if (outputSchema.type === "object" && outputSchema.properties) {
|
|
34
|
+
const required = new Set(outputSchema.required ?? []);
|
|
35
|
+
const properties = {};
|
|
36
|
+
for (const [key, propSchema] of Object.entries(outputSchema.properties)) {
|
|
37
|
+
const entry = {
|
|
38
|
+
schema: propSchema,
|
|
39
|
+
required: required.has(key),
|
|
40
|
+
};
|
|
41
|
+
if (propSchema.description)
|
|
42
|
+
entry.description = propSchema.description;
|
|
43
|
+
if (propSchema.example !== undefined)
|
|
44
|
+
entry.example = propSchema.example;
|
|
45
|
+
properties[key] = entry;
|
|
46
|
+
}
|
|
47
|
+
const spec = { properties };
|
|
48
|
+
if (description)
|
|
49
|
+
spec.description = description;
|
|
50
|
+
return spec;
|
|
51
|
+
}
|
|
52
|
+
// Non-object schema (array, primitive, anyOf) — wrap as single `data` prop.
|
|
53
|
+
const entry = { schema: outputSchema, required: true };
|
|
54
|
+
if (outputSchema.description)
|
|
55
|
+
entry.description = outputSchema.description;
|
|
56
|
+
const spec = { properties: { data: entry } };
|
|
57
|
+
if (description)
|
|
58
|
+
spec.description = description;
|
|
59
|
+
return spec;
|
|
60
|
+
}
|
|
61
|
+
/** Convert a list of action-tools into an ActionSpec, one entry per tool.
|
|
62
|
+
* Collisions (two tools camelCase-ing to the same key) are resolved by appending
|
|
63
|
+
* a numeric suffix — the raw tool name is always preserved on the
|
|
64
|
+
* entry's `nextStep` hint. */
|
|
65
|
+
export function actionsFromTools(actionTools, toolPrefix) {
|
|
66
|
+
const actions = {};
|
|
67
|
+
for (const tool of actionTools) {
|
|
68
|
+
let actionKey = camelKey(stripPrefix(tool.name, toolPrefix));
|
|
69
|
+
if (actionKey in actions) {
|
|
70
|
+
// Fall back to the full tool name (camelCased) before disambiguating with a suffix.
|
|
71
|
+
const full = camelKey(tool.name);
|
|
72
|
+
actionKey = full in actions ? uniqueKey(actions, full) : full;
|
|
73
|
+
}
|
|
74
|
+
const entry = {
|
|
75
|
+
label: humanizeToolName(tool.name, toolPrefix),
|
|
76
|
+
nextStep: tool.name,
|
|
77
|
+
};
|
|
78
|
+
if (tool.description)
|
|
79
|
+
entry.description = tool.description;
|
|
80
|
+
if (tool.inputSchema)
|
|
81
|
+
entry.schema = tool.inputSchema;
|
|
82
|
+
actions[actionKey] = entry;
|
|
83
|
+
}
|
|
84
|
+
return actions;
|
|
85
|
+
}
|
|
86
|
+
function uniqueKey(existing, base) {
|
|
87
|
+
let i = 2;
|
|
88
|
+
while (`${base}${i}` in existing)
|
|
89
|
+
i++;
|
|
90
|
+
return `${base}${i}`;
|
|
91
|
+
}
|
|
92
|
+
// ────────────────────────── naming helpers ──────────────────────────
|
|
93
|
+
/** Humanize a tool name, optionally stripping a known server prefix.
|
|
94
|
+
* `humanizeToolName("gmail_search_messages", "gmail_")` → "Search Messages".
|
|
95
|
+
* `humanizeToolName("get_task")` → "Get Task". */
|
|
96
|
+
export function humanizeToolName(name, toolPrefix) {
|
|
97
|
+
const stripped = stripPrefix(name, toolPrefix);
|
|
98
|
+
return stripped
|
|
99
|
+
.split(/[_\-\s]+/)
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
102
|
+
.join(" ");
|
|
103
|
+
}
|
|
104
|
+
/** Strip `toolPrefix` iff `name` starts with it; otherwise return `name` unchanged. */
|
|
105
|
+
export function stripPrefix(name, toolPrefix) {
|
|
106
|
+
if (!toolPrefix)
|
|
107
|
+
return name;
|
|
108
|
+
return name.startsWith(toolPrefix) ? name.slice(toolPrefix.length) : name;
|
|
109
|
+
}
|
|
110
|
+
/** "search_messages" → "searchMessages". */
|
|
111
|
+
export function camelKey(name) {
|
|
112
|
+
const parts = name.split(/[_\-\s]+/).filter(Boolean);
|
|
113
|
+
if (parts.length === 0)
|
|
114
|
+
return name;
|
|
115
|
+
return parts[0].toLowerCase() +
|
|
116
|
+
parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental JSON Schema learning — type union over observed samples.
|
|
3
|
+
*
|
|
4
|
+
* Used by the Screen Designer to build `generatedOutputSchema` from real MCP tool
|
|
5
|
+
* responses when the server doesn't ship a native `outputSchema`. Each captured
|
|
6
|
+
* response is merged into the running schema via {@link mergeSchema}; the result
|
|
7
|
+
* converges as more samples arrive.
|
|
8
|
+
*
|
|
9
|
+
* Algorithm:
|
|
10
|
+
* - Object properties: union of keys. A property present in one sample but missing
|
|
11
|
+
* from another is marked optional (dropped from `required`).
|
|
12
|
+
* - Array items: recursive merge across element schemas from all samples.
|
|
13
|
+
* - Primitives of the same type: identity.
|
|
14
|
+
* - `null` + any type: sets `nullable: true` on that type.
|
|
15
|
+
* - Type conflicts (e.g. string + number): collapsed into `anyOf`.
|
|
16
|
+
*
|
|
17
|
+
* Pure — no I/O, no DB, no clock. Safe to call from Lambda, edge, or tests.
|
|
18
|
+
*/
|
|
19
|
+
import type { JsonSchema, JsonValue } from "../types/data-contract.js";
|
|
20
|
+
/** Infer a JSON Schema from a single JSON value. */
|
|
21
|
+
export declare function inferSchema(value: JsonValue): JsonSchema;
|
|
22
|
+
/**
|
|
23
|
+
* Merge a new observed sample into an existing schema. If `existing` is null,
|
|
24
|
+
* returns the schema inferred from `sample` alone.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mergeSchema(existing: JsonSchema | null | undefined, sample: JsonValue): JsonSchema;
|
|
27
|
+
/**
|
|
28
|
+
* Merge two schemas into one that accepts values satisfying either. Exported
|
|
29
|
+
* for the seeder path (combining two known schemas without sampling).
|
|
30
|
+
*/
|
|
31
|
+
export declare function mergeTwoSchemas(a: JsonSchema, b: JsonSchema): JsonSchema;
|
|
32
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/schema-learning/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAc,UAAU,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEnF,oDAAoD;AACpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CA4BxD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,SAAS,GAAG,UAAU,CAIlG;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CA0BxE"}
|