@ggui-ai/negotiator 0.23.0 → 0.25.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 +6 -1
- package/dist/ensure-conforming-contract.d.ts +12 -1
- package/dist/ensure-conforming-contract.d.ts.map +1 -1
- package/dist/ensure-conforming-contract.js +40 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/llm-caller.d.ts +26 -0
- package/dist/llm-caller.d.ts.map +1 -1
- package/dist/llm-caller.js +9 -0
- package/dist/llm-rerank.d.ts +26 -25
- package/dist/llm-rerank.d.ts.map +1 -1
- package/dist/llm-rerank.js +42 -9
- package/dist/normalize-draft.d.ts +20 -0
- package/dist/normalize-draft.d.ts.map +1 -1
- package/dist/normalize-draft.js +63 -39
- package/dist/preserve-action-members.d.ts +79 -0
- package/dist/preserve-action-members.d.ts.map +1 -0
- package/dist/preserve-action-members.js +199 -0
- package/dist/rerank-eval/run-probe-cli.js +4 -1
- package/dist/rerank-eval/run-probe.js +1 -1
- package/dist/synthesize-contract.d.ts +11 -0
- package/dist/synthesize-contract.d.ts.map +1 -1
- package/dist/synthesize-contract.js +30 -2
- package/package.json +3 -3
- package/src/ensure-conforming-contract.ts +53 -7
- package/src/index.ts +3 -2
- package/src/llm-caller.ts +34 -0
- package/src/llm-rerank.ts +52 -21
- package/src/normalize-draft.ts +70 -39
- package/src/preserve-action-members.ts +224 -0
- package/src/rerank-eval/run-probe-cli.ts +3 -1
- package/src/rerank-eval/run-probe.ts +1 -1
- package/src/synthesize-contract.ts +47 -3
package/README.md
CHANGED
|
@@ -35,7 +35,12 @@ pnpm add @ggui-ai/negotiator
|
|
|
35
35
|
- **`ensureConformingContract(...)`** — the create-path guarantee: validates
|
|
36
36
|
an untrusted draft and, on errors, deterministically normalizes or
|
|
37
37
|
LLM-repairs it so the handshake always returns a contract that passes the
|
|
38
|
-
backstop. Never throws.
|
|
38
|
+
backstop. Never throws. A repair keeps every member the draft declared on
|
|
39
|
+
its actions (`oneShot`, `confirm`, `nextStep`, `description`, `example`,
|
|
40
|
+
`icon`) and names anything it cannot keep at its own path — the LLM
|
|
41
|
+
repair as `REPAIR_MEMBER_DROPPED` / `REPAIR_ENTRY_DROPPED` with the
|
|
42
|
+
gate's reason in the message, the salvaged subset with the gate's code
|
|
43
|
+
at the cut path — so a declaration is never lost silently.
|
|
39
44
|
- **`rerankCandidates(...)`** — LLM judge that re-ranks blueprint-match
|
|
40
45
|
retrieval candidates (the semantic-match decision used by
|
|
41
46
|
`decideHandshake`).
|
|
@@ -62,7 +62,18 @@ export interface EnsureConformingAccepted {
|
|
|
62
62
|
* warnings on the (valid) draft. On `origin: 'synth'`, the ERROR
|
|
63
63
|
* findings that rejected the agent's draft — so the agent-side model
|
|
64
64
|
* learns what it got wrong, even though we repaired it. On
|
|
65
|
-
* `
|
|
65
|
+
* `llm-repair` they additionally carry one `REPAIR_MEMBER_DROPPED` per
|
|
66
|
+
* declared action-entry member the repair could not keep and one
|
|
67
|
+
* `REPAIR_ENTRY_DROPPED` per draft action it no longer carries
|
|
68
|
+
* (ggui#1421 — a repair preserves the draft's declarations, `oneShot`
|
|
69
|
+
* above all, and names any it must drop, each at its own path with the
|
|
70
|
+
* gate's reason in the message; a member under repair is named by the
|
|
71
|
+
* gate's own finding at that same path).
|
|
72
|
+
* The error findings are the union of what the gate refused on the
|
|
73
|
+
* raw draft and on its normalized form (the gate stops after the
|
|
74
|
+
* shape phase, so the raw lint alone can hide a semantic finding). On
|
|
75
|
+
* `salvaged-subset` they include one finding per dropped entry, with
|
|
76
|
+
* the gate's own code at the cut path.
|
|
66
77
|
*/
|
|
67
78
|
readonly findings: readonly SuggestionFinding[];
|
|
68
79
|
/** Operator- + LLM-readable explanation. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-conforming-contract.d.ts","sourceRoot":"","sources":["../src/ensure-conforming-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD;;;;;;;;;GASG;AACH,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,iBAAiB,CAAC;AAEtB,4DAA4D;AAC5D,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC
|
|
1
|
+
{"version":3,"file":"ensure-conforming-contract.d.ts","sourceRoot":"","sources":["../src/ensure-conforming-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD;;;;;;;;;GASG;AACH,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,iBAAiB,CAAC;AAEtB,4DAA4D;AAC5D,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,GAAG,wBAAwB,CAAC;AAEzF,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;CAAE,EACjC,IAAI,EAAE;IACJ,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACnD,GACA,OAAO,CAAC,sBAAsB,CAAC,CAqJjC"}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { lintContract, dataContractSchema, } from '@ggui-ai/protocol';
|
|
37
37
|
import { synthesizeContract } from './synthesize-contract.js';
|
|
38
|
-
import { normalizeDraft } from './normalize-draft.js';
|
|
38
|
+
import { liftedRequiredNames, normalizeDraft } from './normalize-draft.js';
|
|
39
39
|
import { salvageConformingSubset } from './salvage-draft.js';
|
|
40
40
|
export async function ensureConformingContract(deps, args) {
|
|
41
41
|
const lint = lintContract(args.draft);
|
|
@@ -58,11 +58,22 @@ export async function ensureConformingContract(deps, args) {
|
|
|
58
58
|
reasoning: 'agent draft passed validateContract; accepted verbatim (origin: agent)',
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
// ggui#1454 — a wrapper-level `propsSpec.required: [...]` is refused by the
|
|
62
|
+
// gate as an unrecognized key and LIFTED by `normalizeDraft` onto the named
|
|
63
|
+
// entries (ggui#1432). The finding stays (the draft used a non-wire shape,
|
|
64
|
+
// and the agent should see that); its message says the key was honoured,
|
|
65
|
+
// so it reads as a repair, not a refusal to fix and re-handshake for.
|
|
66
|
+
const lifted = liftedRequiredNames(args.draft);
|
|
67
|
+
const liftNote = lifted.length > 0
|
|
68
|
+
? ` — the wrapper-level \`required\` is not a wire key; it was lifted into ${lifted
|
|
69
|
+
.map((name) => `propsSpec.properties.${name}.required = true`)
|
|
70
|
+
.join(', ')} (repaired, not refused)`
|
|
71
|
+
: '';
|
|
61
72
|
const errorFindings = lint.errors.map((e) => ({
|
|
62
73
|
code: e.code,
|
|
63
74
|
severity: 'error',
|
|
64
75
|
path: e.path,
|
|
65
|
-
message: e.message,
|
|
76
|
+
message: e.code === 'CTR_SHAPE_UNRECOGNIZED_KEYS' && e.path === 'propsSpec' ? `${e.message}${liftNote}` : e.message,
|
|
66
77
|
}));
|
|
67
78
|
// L3 — deterministic normalization tier. Most agent malformations are
|
|
68
79
|
// mechanical (stray illegal wrapper keys, non-canonical schema types).
|
|
@@ -72,6 +83,25 @@ export async function ensureConformingContract(deps, args) {
|
|
|
72
83
|
// LLM call). Semantic deficiencies fall through to the repair loop.
|
|
73
84
|
const normalized = normalizeDraft(args.draft);
|
|
74
85
|
const normLint = lintContract(normalized);
|
|
86
|
+
// The gate stops after the shape phase, so the RAW draft's lint may
|
|
87
|
+
// name only its mechanical errors while the NORMALIZED draft's lint
|
|
88
|
+
// reaches the semantic ones (a dangling `nextStep`). Every path the
|
|
89
|
+
// gate refused on either tree is the agent's to see (ggui#1421): the
|
|
90
|
+
// union, raw first, one finding per (code, path).
|
|
91
|
+
const gateFindings = [...errorFindings];
|
|
92
|
+
for (const e of normLint.errors) {
|
|
93
|
+
if (!gateFindings.some((f) => f.code === e.code && f.path === e.path)) {
|
|
94
|
+
gateFindings.push({ code: e.code, severity: 'error', path: e.path, message: e.message });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** `gateFindings` plus `more`, one finding per (code, path) — a drop the gate already named is not named twice. */
|
|
98
|
+
const withGateFindings = (more) => {
|
|
99
|
+
const out = [...gateFindings];
|
|
100
|
+
for (const f of more)
|
|
101
|
+
if (!out.some((g) => g.code === f.code && g.path === f.path))
|
|
102
|
+
out.push(f);
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
75
105
|
if (normLint.errors.length === 0) {
|
|
76
106
|
return {
|
|
77
107
|
contract: dataContractSchema.parse(normalized),
|
|
@@ -95,12 +125,16 @@ export async function ensureConformingContract(deps, args) {
|
|
|
95
125
|
});
|
|
96
126
|
if (synth.contract !== null &&
|
|
97
127
|
lintContract(synth.contract).errors.length === 0) {
|
|
128
|
+
const droppedPaths = synth.dropped.map((d) => d.path);
|
|
98
129
|
return {
|
|
99
130
|
contract: synth.contract,
|
|
100
131
|
origin: 'synth',
|
|
101
132
|
method: 'llm-repair',
|
|
102
|
-
findings:
|
|
103
|
-
reasoning: `repaired the agent draft to pass validateContract — ${synth.reason}
|
|
133
|
+
findings: withGateFindings(synth.dropped),
|
|
134
|
+
reasoning: `repaired the agent draft to pass validateContract — ${synth.reason}` +
|
|
135
|
+
(droppedPaths.length > 0
|
|
136
|
+
? `; dropped ${droppedPaths.length} declared action ${droppedPaths.length === 1 ? 'member/entry' : 'members/entries'} the repair could not keep (${droppedPaths.join(', ')}) — each is a finding`
|
|
137
|
+
: ''),
|
|
104
138
|
};
|
|
105
139
|
}
|
|
106
140
|
// Repair impossible (LLM down, provider can't synthesize, or the
|
|
@@ -115,7 +149,7 @@ export async function ensureConformingContract(deps, args) {
|
|
|
115
149
|
contract: salvaged.contract,
|
|
116
150
|
origin: 'synth',
|
|
117
151
|
method: 'salvaged-subset',
|
|
118
|
-
findings:
|
|
152
|
+
findings: withGateFindings(salvaged.dropped),
|
|
119
153
|
reasoning: `could not repair the agent draft within budget (${synth.reason}); ` +
|
|
120
154
|
`proposing the conforming SUBSET of your draft — dropped ${droppedPaths.length} ` +
|
|
121
155
|
`entr${droppedPaths.length === 1 ? 'y' : 'ies'} the protocol refused (${droppedPaths.join(', ')}). ` +
|
|
@@ -129,7 +163,7 @@ export async function ensureConformingContract(deps, args) {
|
|
|
129
163
|
contract: null,
|
|
130
164
|
origin: 'agent',
|
|
131
165
|
method: 'declined',
|
|
132
|
-
findings:
|
|
166
|
+
findings: gateFindings,
|
|
133
167
|
reasoning: `declined: could not repair the agent draft within budget (${synth.reason}) and no entry of it ` +
|
|
134
168
|
`passes the contract gate — nothing to propose. Fix the findings (every one names its path) ` +
|
|
135
169
|
`and re-handshake; do not render against this handshake.`,
|
package/dist/index.d.ts
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
* consumers need. Each additive export carries semver weight.
|
|
26
26
|
*/
|
|
27
27
|
export { hashContract, buildVariant } from './contract-hash.js';
|
|
28
|
-
export type { LLMCaller, LLMCallerConfig, ToolSchema } from './llm-caller.js';
|
|
29
|
-
export { rerankCandidates } from './llm-rerank.js';
|
|
30
|
-
export type { RerankCandidate, RerankDecision, RerankQuery, } from './llm-rerank.js';
|
|
28
|
+
export type { LLMCaller, LLMCallerConfig, Metered, TokenUsage, ToolSchema } from './llm-caller.js';
|
|
29
|
+
export { llmRerankJudge, rerankCandidates, RERANK_SYSTEM_PROMPT } from './llm-rerank.js';
|
|
30
|
+
export type { RerankCandidate, RerankDecision, RerankJudge, RerankQuery, } from './llm-rerank.js';
|
|
31
31
|
export { synthesizeContract } from './synthesize-contract.js';
|
|
32
32
|
export type { SynthesizeContractResult } from './synthesize-contract.js';
|
|
33
33
|
export { ensureConformingContract } from './ensure-conforming-contract.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACnG,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,EACX,WAAW,GACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,YAAY,EACV,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAItD,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,yBAAyB,EACzB,6BAA6B,EAC7B,wBAAwB,EACxB,6BAA6B,EAC7B,gCAAgC,GACjC,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* consumers need. Each additive export carries semver weight.
|
|
26
26
|
*/
|
|
27
27
|
export { hashContract, buildVariant } from './contract-hash.js';
|
|
28
|
-
export { rerankCandidates } from './llm-rerank.js';
|
|
28
|
+
export { llmRerankJudge, rerankCandidates, RERANK_SYSTEM_PROMPT } from './llm-rerank.js';
|
|
29
29
|
export { synthesizeContract } from './synthesize-contract.js';
|
|
30
30
|
export { ensureConformingContract } from './ensure-conforming-contract.js';
|
|
31
31
|
export { normalizeDraft } from './normalize-draft.js';
|
package/dist/llm-caller.d.ts
CHANGED
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
* that can't produce tool input at all simply omit this method;
|
|
36
36
|
* consumers fall back to `call` + regex JSON extraction. Absence is
|
|
37
37
|
* not an error.
|
|
38
|
+
* - `callStructuredMetered?(...)` is OPTIONAL: `callStructured`'s contract
|
|
39
|
+
* (the tool input, or a throw), with the call's token usage beside it
|
|
40
|
+
* as the provider reported it. `usage` is ABSENT when the provider
|
|
41
|
+
* reported none, never zeros: a consumer reads absence as "unmetered".
|
|
42
|
+
* A consumer that has it prefers it to `callStructured`; an
|
|
43
|
+
* implementation that cannot read usage omits it, and every caller
|
|
44
|
+
* written without it keeps compiling. It is a method rather than a
|
|
45
|
+
* usage callback because judges run concurrently, and a result carries
|
|
46
|
+
* its own usage where a callback would need correlating to its call.
|
|
38
47
|
* - `ToolSchema.input_schema` follows the OpenAI tool-use JSON
|
|
39
48
|
* Schema convention. Implementations that use a different
|
|
40
49
|
* tool-use protocol (e.g., Anthropic's variant) MUST translate at
|
|
@@ -46,6 +55,16 @@ export interface ToolSchema {
|
|
|
46
55
|
description: string;
|
|
47
56
|
input_schema: Record<string, unknown>;
|
|
48
57
|
}
|
|
58
|
+
/** Tokens one provider call consumed, as the provider reported them. */
|
|
59
|
+
export interface TokenUsage {
|
|
60
|
+
readonly input: number;
|
|
61
|
+
readonly output: number;
|
|
62
|
+
}
|
|
63
|
+
/** A call's result with the usage the provider reported for it; `usage` absent = unmetered. */
|
|
64
|
+
export interface Metered<T> {
|
|
65
|
+
readonly value: T;
|
|
66
|
+
readonly usage?: TokenUsage;
|
|
67
|
+
}
|
|
49
68
|
/** Chat-model dispatcher consumed by the negotiator's synthesis + judge primitives. */
|
|
50
69
|
export interface LLMCaller {
|
|
51
70
|
/**
|
|
@@ -62,6 +81,13 @@ export interface LLMCaller {
|
|
|
62
81
|
* extraction on the text path.
|
|
63
82
|
*/
|
|
64
83
|
callStructured?(systemPrompt: string, userMessage: string, tool: ToolSchema, maxTokens?: number): Promise<unknown>;
|
|
84
|
+
/**
|
|
85
|
+
* {@link callStructured}, with the call's token usage beside the tool
|
|
86
|
+
* input (see the normative semantics above). Omit it when the provider's
|
|
87
|
+
* usage cannot be read; consumers then fall back to `callStructured` and
|
|
88
|
+
* treat the call as unmetered.
|
|
89
|
+
*/
|
|
90
|
+
callStructuredMetered?(systemPrompt: string, userMessage: string, tool: ToolSchema, maxTokens?: number): Promise<Metered<unknown>>;
|
|
65
91
|
}
|
|
66
92
|
/**
|
|
67
93
|
* Provider + model selector for factory-style LLM caller
|
package/dist/llm-caller.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-caller.d.ts","sourceRoot":"","sources":["../src/llm-caller.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"llm-caller.d.ts","sourceRoot":"","sources":["../src/llm-caller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,+FAA+F;AAC/F,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;CAC7B;AAED,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;;;;;OAOG;IACH,cAAc,CAAC,CACb,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,UAAU,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB;;;;;OAKG;IACH,qBAAqB,CAAC,CACpB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,UAAU,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAC;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/dist/llm-caller.js
CHANGED
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
* that can't produce tool input at all simply omit this method;
|
|
36
36
|
* consumers fall back to `call` + regex JSON extraction. Absence is
|
|
37
37
|
* not an error.
|
|
38
|
+
* - `callStructuredMetered?(...)` is OPTIONAL: `callStructured`'s contract
|
|
39
|
+
* (the tool input, or a throw), with the call's token usage beside it
|
|
40
|
+
* as the provider reported it. `usage` is ABSENT when the provider
|
|
41
|
+
* reported none, never zeros: a consumer reads absence as "unmetered".
|
|
42
|
+
* A consumer that has it prefers it to `callStructured`; an
|
|
43
|
+
* implementation that cannot read usage omits it, and every caller
|
|
44
|
+
* written without it keeps compiling. It is a method rather than a
|
|
45
|
+
* usage callback because judges run concurrently, and a result carries
|
|
46
|
+
* its own usage where a callback would need correlating to its call.
|
|
38
47
|
* - `ToolSchema.input_schema` follows the OpenAI tool-use JSON
|
|
39
48
|
* Schema convention. Implementations that use a different
|
|
40
49
|
* tool-use protocol (e.g., Anthropic's variant) MUST translate at
|
package/dist/llm-rerank.d.ts
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* LLM rerank — Tier-2 precision oracle for the blueprint registry.
|
|
3
|
-
*
|
|
4
|
-
* Given a user's UI request (intent + contract structure) and a set
|
|
5
|
-
* of candidate cached blueprints retrieved by RAG, ask a fast LLM
|
|
6
|
-
* (Haiku 4.5) which candidate (if any) matches. Returns a structured
|
|
7
|
-
* decision so the caller can branch deterministically.
|
|
8
|
-
*
|
|
9
|
-
* This module is the precision half of the blueprint-first
|
|
10
|
-
* architecture: RAG retrieval is high-recall but low-precision (bge-
|
|
11
|
-
* small confuses topic-similar but UI-divergent prompts); the LLM
|
|
12
|
-
* judge restores precision. Combined break-even hit rate is ~10%;
|
|
13
|
-
* realistic workloads observe 30-70%.
|
|
14
|
-
*/
|
|
15
|
-
import type { LLMCaller, ToolSchema } from './llm-caller.js';
|
|
1
|
+
import type { LLMCaller, TokenUsage, ToolSchema } from './llm-caller.js';
|
|
16
2
|
/**
|
|
17
3
|
* One candidate blueprint for the LLM judge to consider.
|
|
18
4
|
*
|
|
@@ -58,23 +44,23 @@ export interface RerankDecision {
|
|
|
58
44
|
*/
|
|
59
45
|
readonly confidence: number;
|
|
60
46
|
/**
|
|
61
|
-
* Free-text reason from the judge
|
|
47
|
+
* Free-text reason from the judge, when it gives one — a judge that
|
|
48
|
+
* decides without prose sends none (ggui#1235). Surface in trace logs so
|
|
62
49
|
* operators can debug "why didn't this hit." Truncate at the
|
|
63
50
|
* persistence boundary if cardinality is a concern.
|
|
64
51
|
*/
|
|
65
|
-
readonly reason
|
|
52
|
+
readonly reason?: string;
|
|
66
53
|
/** Wall-clock latency of the LLM call. */
|
|
67
54
|
readonly latencyMs: number;
|
|
68
55
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
56
|
+
* Tokens the decision cost, as the provider reported them — for the
|
|
57
|
+
* cache-trace sink and cost accounting. ABSENT means unmetered, never
|
|
58
|
+
* zero: the judge called a provider and has no count for the call (a
|
|
59
|
+
* caller without `callStructuredMetered`, a provider that reported no
|
|
60
|
+
* usage, or a call that threw). `{ input: 0, output: 0 }` is a true
|
|
61
|
+
* zero: the decision was made without a provider call (ggui#1418).
|
|
73
62
|
*/
|
|
74
|
-
readonly tokenCost
|
|
75
|
-
readonly input: number;
|
|
76
|
-
readonly output: number;
|
|
77
|
-
};
|
|
63
|
+
readonly tokenCost?: TokenUsage;
|
|
78
64
|
}
|
|
79
65
|
/** Query the user's request the judge is matching against. */
|
|
80
66
|
export interface RerankQuery {
|
|
@@ -98,5 +84,20 @@ declare const RERANK_TOOL: ToolSchema;
|
|
|
98
84
|
export declare function rerankCandidates(deps: {
|
|
99
85
|
readonly llm: LLMCaller;
|
|
100
86
|
}, query: RerankQuery, candidates: readonly RerankCandidate[]): Promise<RerankDecision>;
|
|
87
|
+
/**
|
|
88
|
+
* The judge seam (ggui#1235): one function from a query and its candidates
|
|
89
|
+
* to a {@link RerankDecision}. The matcher takes a judge together with the
|
|
90
|
+
* confidence threshold it was measured on (the pair), so a judge on
|
|
91
|
+
* another scale never meets a cut calibrated for a different one.
|
|
92
|
+
* `matchId: null` is a judge's only decline; `confidence` is the judge's
|
|
93
|
+
* own, compared by the caller against the pair's threshold.
|
|
94
|
+
*/
|
|
95
|
+
export type RerankJudge = (query: RerankQuery, candidates: readonly RerankCandidate[]) => Promise<RerankDecision>;
|
|
96
|
+
/**
|
|
97
|
+
* Today's judge as a {@link RerankJudge}: {@link rerankCandidates} bound to
|
|
98
|
+
* an {@link LLMCaller} — the same prompt, the same tool, the same decision,
|
|
99
|
+
* so a caller that injects nothing else behaves exactly as before.
|
|
100
|
+
*/
|
|
101
|
+
export declare function llmRerankJudge(llm: LLMCaller): RerankJudge;
|
|
101
102
|
export { RERANK_SYSTEM_PROMPT, RERANK_TOOL };
|
|
102
103
|
//# sourceMappingURL=llm-rerank.d.ts.map
|
package/dist/llm-rerank.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-rerank.d.ts","sourceRoot":"","sources":["../src/llm-rerank.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"llm-rerank.d.ts","sourceRoot":"","sources":["../src/llm-rerank.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC;CACjC;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,QAAA,MAAM,oBAAoB,03DAQ6U,CAAC;AAExW,QAAA,MAAM,WAAW,EAAE,UA4BlB,CAAC;AAkFF;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;CAAE,EACjC,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,OAAO,CAAC,cAAc,CAAC,CAyDzB;AAGD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,SAAS,eAAe,EAAE,KACnC,OAAO,CAAC,cAAc,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,WAAW,CAE1D;AAED,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/llm-rerank.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM rerank — Tier-2 precision oracle for the blueprint registry.
|
|
3
|
+
*
|
|
4
|
+
* Given a user's UI request (intent + contract structure) and a set
|
|
5
|
+
* of candidate cached blueprints retrieved by RAG, ask a fast LLM
|
|
6
|
+
* (Haiku 4.5) which candidate (if any) matches. Returns a structured
|
|
7
|
+
* decision so the caller can branch deterministically.
|
|
8
|
+
*
|
|
9
|
+
* This module is the precision half of the blueprint-first
|
|
10
|
+
* architecture: RAG retrieval is high-recall but low-precision (bge-
|
|
11
|
+
* small confuses topic-similar but UI-divergent prompts); the LLM
|
|
12
|
+
* judge restores precision. Combined break-even hit rate is ~10%;
|
|
13
|
+
* realistic workloads observe 30-70%.
|
|
14
|
+
*/
|
|
15
|
+
import { MATCHED_INTENT_MAX_CHARS } from '@ggui-ai/protocol';
|
|
1
16
|
const RERANK_SYSTEM_PROMPT = `You match user UI requests against previously-generated UI blueprints. Each blueprint was produced for a past request and stored. Decide whether any candidate belongs to the SAME FAMILY as the current request — i.e. it would serve as a reasonable starting point that the requester can refine, not a pixel-exact replica.
|
|
2
17
|
|
|
3
18
|
MATCH means the candidate is the same intended user task AND the same broad UI shape (component types, layout pattern). A candidate still MATCHES when the current request adds or omits fields, slots, or actions relative to the cached blueprint — a superset, a subset, or an overlapping wire surface all still match. Added or omitted fields/slots/actions DO NOT block a match and are NOT yours to judge: those wire-surface deltas are reconciled and reported to the agent separately, after you decide. Judge similarity of task and shape, never coverage of fields.
|
|
@@ -42,7 +57,11 @@ function buildUserMessage(query, candidates) {
|
|
|
42
57
|
for (const c of candidates) {
|
|
43
58
|
lines.push('---');
|
|
44
59
|
lines.push(` id: ${c.id}`);
|
|
45
|
-
|
|
60
|
+
// The cap is the protocol's `MATCHED_INTENT_MAX_CHARS`: the same
|
|
61
|
+
// string the judge reads here is what a judged hit hands back to the
|
|
62
|
+
// agent as `blueprintMeta.matchedIntent` (ggui#1336), so the two cuts
|
|
63
|
+
// are one constant, not two literals that happen to agree.
|
|
64
|
+
lines.push(` intent: ${truncate(c.cachedIntent, MATCHED_INTENT_MAX_CHARS)}`);
|
|
46
65
|
lines.push(` contract: ${c.cachedContractSummary}`);
|
|
47
66
|
if (typeof c.cosine === 'number') {
|
|
48
67
|
lines.push(` cosine: ${c.cosine.toFixed(3)}`);
|
|
@@ -118,7 +137,8 @@ export async function rerankCandidates(deps, query, candidates) {
|
|
|
118
137
|
}
|
|
119
138
|
const userMessage = buildUserMessage(query, candidates);
|
|
120
139
|
const candidateIds = new Set(candidates.map((c) => c.id));
|
|
121
|
-
|
|
140
|
+
const { callStructured, callStructuredMetered } = deps.llm;
|
|
141
|
+
if (typeof callStructuredMetered !== 'function' && typeof callStructured !== 'function') {
|
|
122
142
|
return {
|
|
123
143
|
matchId: null,
|
|
124
144
|
confidence: 0,
|
|
@@ -127,9 +147,19 @@ export async function rerankCandidates(deps, query, candidates) {
|
|
|
127
147
|
tokenCost: { input: 0, output: 0 },
|
|
128
148
|
};
|
|
129
149
|
}
|
|
150
|
+
// The metered method when the caller has it (its usage is the decision's
|
|
151
|
+
// cost), else the plain one (unmetered: no tokenCost).
|
|
130
152
|
let toolInput;
|
|
153
|
+
let usage;
|
|
131
154
|
try {
|
|
132
|
-
|
|
155
|
+
if (typeof callStructuredMetered === 'function') {
|
|
156
|
+
const metered = await callStructuredMetered.call(deps.llm, RERANK_SYSTEM_PROMPT, userMessage, RERANK_TOOL, 512);
|
|
157
|
+
toolInput = metered.value;
|
|
158
|
+
usage = metered.usage;
|
|
159
|
+
}
|
|
160
|
+
else if (typeof callStructured === 'function') {
|
|
161
|
+
toolInput = await callStructured.call(deps.llm, RERANK_SYSTEM_PROMPT, userMessage, RERANK_TOOL, 512);
|
|
162
|
+
}
|
|
133
163
|
}
|
|
134
164
|
catch (err) {
|
|
135
165
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -138,7 +168,6 @@ export async function rerankCandidates(deps, query, candidates) {
|
|
|
138
168
|
confidence: 0,
|
|
139
169
|
reason: `llm-rerank: callStructured threw — ${message}`,
|
|
140
170
|
latencyMs: Date.now() - startedAt,
|
|
141
|
-
tokenCost: { input: 0, output: 0 },
|
|
142
171
|
};
|
|
143
172
|
}
|
|
144
173
|
const parsed = parseToolInput(toolInput, candidateIds);
|
|
@@ -147,11 +176,15 @@ export async function rerankCandidates(deps, query, candidates) {
|
|
|
147
176
|
confidence: parsed.confidence,
|
|
148
177
|
reason: parsed.reason,
|
|
149
178
|
latencyMs: Date.now() - startedAt,
|
|
150
|
-
|
|
151
|
-
// we don't have today. Default to zero; the cost gate is measured
|
|
152
|
-
// out-of-band from billing data during the probe.
|
|
153
|
-
tokenCost: { input: 0, output: 0 },
|
|
179
|
+
...(usage !== undefined ? { tokenCost: usage } : {}),
|
|
154
180
|
};
|
|
155
181
|
}
|
|
156
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Today's judge as a {@link RerankJudge}: {@link rerankCandidates} bound to
|
|
184
|
+
* an {@link LLMCaller} — the same prompt, the same tool, the same decision,
|
|
185
|
+
* so a caller that injects nothing else behaves exactly as before.
|
|
186
|
+
*/
|
|
187
|
+
export function llmRerankJudge(llm) {
|
|
188
|
+
return (query, candidates) => rerankCandidates({ llm }, query, candidates);
|
|
189
|
+
}
|
|
157
190
|
export { RERANK_SYSTEM_PROMPT, RERANK_TOOL };
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* This pass fixes the mechanical classes deterministically, preserving
|
|
13
13
|
* the agent's intent exactly:
|
|
14
|
+
* - lifts a wrapper-level `propsSpec.required: [...]` (JSON Schema's
|
|
15
|
+
* spelling of the same declaration) into each listed entry's
|
|
16
|
+
* `required: true` before the key goes — the agent said which props
|
|
17
|
+
* are required, and the served contract keeps saying it (ggui#1432);
|
|
18
|
+
* an entry's own explicit `required` wins, and a name with no entry
|
|
19
|
+
* lifts nothing;
|
|
14
20
|
* - strips keys the protocol's `.strict()` spec schemas would reject,
|
|
15
21
|
* keeping only the allowed keys at each wrapper / entry level;
|
|
16
22
|
* - canonicalizes every inner JSON Schema via {@link normalizeSchema}
|
|
@@ -29,5 +35,19 @@
|
|
|
29
35
|
* (the top-level DataContract schema is `.passthrough()`); only the
|
|
30
36
|
* `.strict()` spec wrappers and entries are cleaned.
|
|
31
37
|
*/
|
|
38
|
+
/**
|
|
39
|
+
* The prop names a wrapper-level `propsSpec.required: [...]` will be
|
|
40
|
+
* lifted onto by {@link normalizeDraft} (ggui#1432): each listed name that
|
|
41
|
+
* has an entry and whose entry declares no `required` of its own, in the
|
|
42
|
+
* wrapper's order and de-duplicated. A ghost name (no entry) and an entry
|
|
43
|
+
* with its own word are skipped, exactly as the lift skips them. Empty when
|
|
44
|
+
* nothing lifts.
|
|
45
|
+
*
|
|
46
|
+
* One source of truth for the lift and for the finding that reports it
|
|
47
|
+
* (ggui#1454): the gate's `CTR_SHAPE_UNRECOGNIZED_KEYS` at `propsSpec` is
|
|
48
|
+
* a repair here, not a refusal, and its message says which entries gained
|
|
49
|
+
* `required: true`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function liftedRequiredNames(draft: unknown): readonly string[];
|
|
32
52
|
export declare function normalizeDraft(draft: unknown): unknown;
|
|
33
53
|
//# sourceMappingURL=normalize-draft.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-draft.d.ts","sourceRoot":"","sources":["../src/normalize-draft.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"normalize-draft.d.ts","sourceRoot":"","sources":["../src/normalize-draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAgGH;;;;;;GAMG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAUrE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAuDtD"}
|
package/dist/normalize-draft.js
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* This pass fixes the mechanical classes deterministically, preserving
|
|
13
13
|
* the agent's intent exactly:
|
|
14
|
+
* - lifts a wrapper-level `propsSpec.required: [...]` (JSON Schema's
|
|
15
|
+
* spelling of the same declaration) into each listed entry's
|
|
16
|
+
* `required: true` before the key goes — the agent said which props
|
|
17
|
+
* are required, and the served contract keeps saying it (ggui#1432);
|
|
18
|
+
* an entry's own explicit `required` wins, and a name with no entry
|
|
19
|
+
* lifts nothing;
|
|
14
20
|
* - strips keys the protocol's `.strict()` spec schemas would reject,
|
|
15
21
|
* keeping only the allowed keys at each wrapper / entry level;
|
|
16
22
|
* - canonicalizes every inner JSON Schema via {@link normalizeSchema}
|
|
@@ -22,43 +28,23 @@
|
|
|
22
28
|
* cross-refs) are deliberately out of scope — those still go to the
|
|
23
29
|
* repair loop, where reasoning earns its keep.
|
|
24
30
|
*/
|
|
31
|
+
import { actionEntrySchema, agentToolEntrySchema, contextEntrySchema, propEntrySchema, propsSpecSchema, streamChannelEntrySchema, } from '@ggui-ai/protocol';
|
|
25
32
|
import { normalizeSchema } from './normalize-schema.js';
|
|
26
|
-
// Allowed-key sets
|
|
27
|
-
// (schemas/data-contract.ts)
|
|
28
|
-
// the
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
'description',
|
|
40
|
-
'schema',
|
|
41
|
-
'default',
|
|
42
|
-
'debounceMs',
|
|
43
|
-
'example',
|
|
44
|
-
]);
|
|
45
|
-
const ACTION_ENTRY_KEYS = new Set([
|
|
46
|
-
'description',
|
|
47
|
-
'label',
|
|
48
|
-
'schema',
|
|
49
|
-
'example',
|
|
50
|
-
'icon',
|
|
51
|
-
'confirm',
|
|
52
|
-
'nextStep',
|
|
53
|
-
]);
|
|
54
|
-
const STREAM_ENTRY_KEYS = new Set(['description', 'schema', 'source']);
|
|
55
|
-
const AGENT_TOOL_KEYS = new Set(['serverInfo', 'toolInfo', 'usage', 'example']);
|
|
33
|
+
// Allowed-key sets are DERIVED from the protocol's `.strict()` spec
|
|
34
|
+
// schemas (schemas/data-contract.ts) — never hand-copied. A hand-copied
|
|
35
|
+
// mirror drifts the day the schema grows: `oneShot` joined
|
|
36
|
+
// `actionEntrySchema` on 2026-09-15 (ggui#1108) and a stale mirror here
|
|
37
|
+
// stripped it from every repaired draft for eleven days (ggui#1421).
|
|
38
|
+
// Stripping anything outside these sets is safe: the strict schema
|
|
39
|
+
// would reject it as CTR_SHAPE_UNRECOGNIZED_KEYS.
|
|
40
|
+
const PROPS_WRAPPER_KEYS = new Set(Object.keys(propsSpecSchema.shape));
|
|
41
|
+
const PROP_ENTRY_KEYS = new Set(Object.keys(propEntrySchema.shape));
|
|
42
|
+
const CONTEXT_ENTRY_KEYS = new Set(Object.keys(contextEntrySchema.shape));
|
|
43
|
+
const ACTION_ENTRY_KEYS = new Set(Object.keys(actionEntrySchema.shape));
|
|
44
|
+
const STREAM_ENTRY_KEYS = new Set(Object.keys(streamChannelEntrySchema.shape));
|
|
45
|
+
const AGENT_TOOL_KEYS = new Set(Object.keys(agentToolEntrySchema.shape));
|
|
56
46
|
/** Inner keys of an {@link AgentToolEntry.toolInfo} (the MCP descriptor). */
|
|
57
|
-
const AGENT_TOOL_INFO_KEYS = new Set(
|
|
58
|
-
'inputSchema',
|
|
59
|
-
'description',
|
|
60
|
-
'outputSchema',
|
|
61
|
-
]);
|
|
47
|
+
const AGENT_TOOL_INFO_KEYS = new Set(Object.keys(agentToolEntrySchema.shape.toolInfo.shape));
|
|
62
48
|
function isRecord(value) {
|
|
63
49
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
64
50
|
}
|
|
@@ -121,21 +107,59 @@ function cleanAgentToolMap(map) {
|
|
|
121
107
|
* (the top-level DataContract schema is `.passthrough()`); only the
|
|
122
108
|
* `.strict()` spec wrappers and entries are cleaned.
|
|
123
109
|
*/
|
|
110
|
+
/**
|
|
111
|
+
* The prop names a wrapper-level `propsSpec.required: [...]` will be
|
|
112
|
+
* lifted onto by {@link normalizeDraft} (ggui#1432): each listed name that
|
|
113
|
+
* has an entry and whose entry declares no `required` of its own, in the
|
|
114
|
+
* wrapper's order and de-duplicated. A ghost name (no entry) and an entry
|
|
115
|
+
* with its own word are skipped, exactly as the lift skips them. Empty when
|
|
116
|
+
* nothing lifts.
|
|
117
|
+
*
|
|
118
|
+
* One source of truth for the lift and for the finding that reports it
|
|
119
|
+
* (ggui#1454): the gate's `CTR_SHAPE_UNRECOGNIZED_KEYS` at `propsSpec` is
|
|
120
|
+
* a repair here, not a refusal, and its message says which entries gained
|
|
121
|
+
* `required: true`.
|
|
122
|
+
*/
|
|
123
|
+
export function liftedRequiredNames(draft) {
|
|
124
|
+
if (!isRecord(draft) || !isRecord(draft['propsSpec']))
|
|
125
|
+
return [];
|
|
126
|
+
const wrapper = draft['propsSpec'];
|
|
127
|
+
if (!Array.isArray(wrapper['required']) || !isRecord(wrapper['properties']))
|
|
128
|
+
return [];
|
|
129
|
+
const entries = wrapper['properties'];
|
|
130
|
+
// De-duplicated: a name the wrapper lists twice lifts once and is reported once.
|
|
131
|
+
return [...new Set(wrapper['required'].filter((name) => typeof name === 'string'))].filter((name) => {
|
|
132
|
+
const entry = entries[name];
|
|
133
|
+
return isRecord(entry) && entry['required'] === undefined;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
124
136
|
export function normalizeDraft(draft) {
|
|
125
137
|
if (!isRecord(draft))
|
|
126
138
|
return draft;
|
|
127
139
|
const out = { ...draft };
|
|
128
140
|
// propsSpec wrapper: keep {description, properties}; clean each PropEntry.
|
|
141
|
+
// A wrapper-level `required: [...]` is the agent's declaration in JSON
|
|
142
|
+
// Schema's spelling — lift it into the listed entries before the key
|
|
143
|
+
// goes, so the served contract still says which props are required.
|
|
129
144
|
if (isRecord(out['propsSpec'])) {
|
|
145
|
+
const wrapper = out['propsSpec'];
|
|
130
146
|
const ps = {};
|
|
131
|
-
for (const [key, value] of Object.entries(
|
|
147
|
+
for (const [key, value] of Object.entries(wrapper)) {
|
|
132
148
|
if (PROPS_WRAPPER_KEYS.has(key))
|
|
133
149
|
ps[key] = value;
|
|
134
150
|
}
|
|
135
151
|
if (isRecord(ps['properties'])) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
const cleaned = cleanEntryMap(ps['properties'], PROP_ENTRY_KEYS, ['schema']);
|
|
153
|
+
// The same selection `liftedRequiredNames` reports (one source of truth);
|
|
154
|
+
// `cleanEntryMap` keeps an entry's own `required`, so the test is the same on
|
|
155
|
+
// the cleaned map as on the raw one.
|
|
156
|
+
for (const name of liftedRequiredNames(draft)) {
|
|
157
|
+
const entry = cleaned[name];
|
|
158
|
+
if (!isRecord(entry))
|
|
159
|
+
continue;
|
|
160
|
+
cleaned[name] = { ...entry, required: true };
|
|
161
|
+
}
|
|
162
|
+
ps['properties'] = cleaned;
|
|
139
163
|
}
|
|
140
164
|
out['propsSpec'] = ps;
|
|
141
165
|
}
|