@ggui-ai/negotiator 0.24.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 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
- * `salvaged-subset` they include one finding per dropped entry.
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;;;;;;OAMG;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,CAkHjC"}
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: errorFindings,
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: [...errorFindings, ...salvaged.dropped],
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: errorFindings,
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,8 +25,8 @@
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 { llmRerankJudge, rerankCandidates } 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
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';
@@ -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;AAC9E,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,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"}
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 { llmRerankJudge, 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';
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"llm-caller.d.ts","sourceRoot":"","sources":["../src/llm-caller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;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,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;CACrB;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"}
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"}
@@ -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
@@ -1,4 +1,4 @@
1
- import type { LLMCaller, ToolSchema } from './llm-caller.js';
1
+ import type { LLMCaller, TokenUsage, ToolSchema } from './llm-caller.js';
2
2
  /**
3
3
  * One candidate blueprint for the LLM judge to consider.
4
4
  *
@@ -53,15 +53,14 @@ export interface RerankDecision {
53
53
  /** Wall-clock latency of the LLM call. */
54
54
  readonly latencyMs: number;
55
55
  /**
56
- * Token cost of the call — for the cache-trace sink and cost
57
- * accounting. Implementations that can't surface token counts may
58
- * report `{input: 0, output: 0}` and the cost-per-call gate will
59
- * have to be measured externally.
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).
60
62
  */
61
- readonly tokenCost: {
62
- readonly input: number;
63
- readonly output: number;
64
- };
63
+ readonly tokenCost?: TokenUsage;
65
64
  }
66
65
  /** Query the user's request the judge is matching against. */
67
66
  export interface RerankQuery {
@@ -1 +1 @@
1
- {"version":3,"file":"llm-rerank.d.ts","sourceRoot":"","sources":["../src/llm-rerank.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D;;;;;;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;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;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,CAwDzB;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"}
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"}
@@ -137,7 +137,8 @@ export async function rerankCandidates(deps, query, candidates) {
137
137
  }
138
138
  const userMessage = buildUserMessage(query, candidates);
139
139
  const candidateIds = new Set(candidates.map((c) => c.id));
140
- if (typeof deps.llm.callStructured !== 'function') {
140
+ const { callStructured, callStructuredMetered } = deps.llm;
141
+ if (typeof callStructuredMetered !== 'function' && typeof callStructured !== 'function') {
141
142
  return {
142
143
  matchId: null,
143
144
  confidence: 0,
@@ -146,9 +147,19 @@ export async function rerankCandidates(deps, query, candidates) {
146
147
  tokenCost: { input: 0, output: 0 },
147
148
  };
148
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).
149
152
  let toolInput;
153
+ let usage;
150
154
  try {
151
- toolInput = await deps.llm.callStructured(RERANK_SYSTEM_PROMPT, userMessage, RERANK_TOOL, 512);
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
+ }
152
163
  }
153
164
  catch (err) {
154
165
  const message = err instanceof Error ? err.message : String(err);
@@ -157,7 +168,6 @@ export async function rerankCandidates(deps, query, candidates) {
157
168
  confidence: 0,
158
169
  reason: `llm-rerank: callStructured threw — ${message}`,
159
170
  latencyMs: Date.now() - startedAt,
160
- tokenCost: { input: 0, output: 0 },
161
171
  };
162
172
  }
163
173
  const parsed = parseToolInput(toolInput, candidateIds);
@@ -166,10 +176,7 @@ export async function rerankCandidates(deps, query, candidates) {
166
176
  confidence: parsed.confidence,
167
177
  reason: parsed.reason,
168
178
  latencyMs: Date.now() - startedAt,
169
- // Token cost surfacing requires LLMCaller-level instrumentation
170
- // we don't have today. Default to zero; the cost gate is measured
171
- // out-of-band from billing data during the probe.
172
- tokenCost: { input: 0, output: 0 },
179
+ ...(usage !== undefined ? { tokenCost: usage } : {}),
173
180
  };
174
181
  }
175
182
  /**
@@ -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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA2GH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA4CtD"}
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"}
@@ -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 mirror the protocol `.strict()` schemas
27
- // (schemas/data-contract.ts). Stripping anything outside these is safe:
28
- // the strict schema would reject it as CTR_SHAPE_UNRECOGNIZED_KEYS.
29
- const PROPS_WRAPPER_KEYS = new Set(['description', 'properties']);
30
- const PROP_ENTRY_KEYS = new Set([
31
- 'description',
32
- 'schema',
33
- 'required',
34
- 'default',
35
- 'example',
36
- 'sourceTool',
37
- ]);
38
- const CONTEXT_ENTRY_KEYS = new Set([
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(out['propsSpec'])) {
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
- ps['properties'] = cleanEntryMap(ps['properties'], PROP_ENTRY_KEYS, [
137
- 'schema',
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
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Action-entry member preservation — the deterministic backstop that
3
+ * keeps a repair FAITHFUL to what the agent DECLARED on its actions
4
+ * (ggui#1421).
5
+ *
6
+ * The repair tool authors exactly two members per action, `label` and
7
+ * `schema` (`SYNTHESIZE_TOOL`); every other member of an
8
+ * {@link ActionEntry} — `description`, `example`, `icon`, `confirm`,
9
+ * `oneShot`, `nextStep` — is the agent's declaration, and the whole
10
+ * one-shot line (ggui#1108 → ggui#1223: the runtime's second-dispatch
11
+ * guard, the server's spend record, ui-gen's `useActionSpent` binding)
12
+ * keys on `oneShot === true` being ON THE SERVED CONTRACT. A repair that
13
+ * re-emits an entry from the tool's answer alone serves a card with no
14
+ * guard, silently. So the overlay below puts the draft's declarations
15
+ * back on the repaired candidate, deterministically, and NAMES whatever
16
+ * it cannot keep:
17
+ *
18
+ * - `REPAIR_MEMBER_DROPPED` at `actionSpec.<name>.<member>` — a
19
+ * declared member the merged contract cannot carry (the gate refuses
20
+ * it there, e.g. a `nextStep` whose tool the repair did not
21
+ * re-declare, or a member that fails its own schema);
22
+ * - `REPAIR_ENTRY_DROPPED` at `actionSpec.<name>` — a draft action the
23
+ * candidate no longer carries under that name (renamed or removed),
24
+ * which takes every member with it.
25
+ *
26
+ * A member whose path a DRAFT finding already names is under repair: it
27
+ * is not restored, and its absence is named by that finding, at the same
28
+ * path. Every other loss is named HERE, at the member's or the entry's
29
+ * own path, with the gate's reason in the message — so the invariant an
30
+ * agent can rely on is one sentence: a declared member or action missing
31
+ * from the served proposal always has a finding at its own path. The
32
+ * `CTR_*` codes say what the gate refused; the `REPAIR_*` codes say what
33
+ * the served proposal does not carry of the declaration, and why. The
34
+ * codes are handshake-decision codes like `COVERAGE_GAP` — declared where
35
+ * they are emitted, read by the agent as strings in `validationFindings`.
36
+ * Model-independent: the overlay works whatever the repair LLM returns.
37
+ */
38
+ import { type DataContract, type SuggestionFinding } from '@ggui-ai/protocol';
39
+ /** A declared action-entry member the repair could not keep; path `actionSpec.<name>.<member>`. */
40
+ export declare const REPAIR_MEMBER_DROPPED: "REPAIR_MEMBER_DROPPED";
41
+ /** A draft action entry the repair no longer carries under its name; path `actionSpec.<name>`. */
42
+ export declare const REPAIR_ENTRY_DROPPED: "REPAIR_ENTRY_DROPPED";
43
+ /**
44
+ * Whether `finding` is a member drop on one of `entryNames` — matched by
45
+ * the exact `actionSpec.<name>.<member>` path per declared member, never
46
+ * by a dotted prefix (an action named `a` is not a prefix of `a.b`).
47
+ */
48
+ export declare function isMemberDropOf(finding: SuggestionFinding, entryNames: readonly string[]): boolean;
49
+ /**
50
+ * Overlay the draft's declared members onto the candidate's action
51
+ * entries (same name), keeping the candidate's `label` and `schema`. A
52
+ * member whose exact path a draft finding names is under repair and
53
+ * stays out. Then lint the merged contract and un-restore, until the
54
+ * tree is stable, every restored member the gate refuses — at its own
55
+ * path, or (for `nextStep`) as `CTR_SCHEMA_INCOMPAT` at the sibling
56
+ * `.schema`, the path the compatibility check reports on — naming each
57
+ * one, so the returned contract never fails the gate BECAUSE of the
58
+ * overlay. A malformed candidate entry (no `label`) is left un-overlaid
59
+ * for the loop's own gate to retry; nothing here throws on the model's
60
+ * answer. Entries the candidate does not carry get nothing back here —
61
+ * see {@link findDroppedActionEntries}.
62
+ */
63
+ export declare function restoreDraftActionMembers(draft: unknown, candidate: DataContract, underRepair: ReadonlySet<string>): {
64
+ readonly contract: DataContract;
65
+ readonly dropped: readonly SuggestionFinding[];
66
+ };
67
+ /**
68
+ * One `REPAIR_ENTRY_DROPPED` per draft action entry the candidate no
69
+ * longer carries under that name — renamed or removed by the repair (or
70
+ * pruned by the loop's own placement pass) — always, because the members
71
+ * it carried (`oneShot` above all) went with it and nothing else names
72
+ * that. When the gate refused the NAME itself (`CTR_DUP_NAME` /
73
+ * `CTR_RESERVED_NAME` at `actionSpec.<name>`, so the path is under
74
+ * repair) the message says so and does not send the agent back to a
75
+ * name the gate will refuse again. Empty when every draft entry survived,
76
+ * and for drafts that declare no actions.
77
+ */
78
+ export declare function findDroppedActionEntries(draft: unknown, candidate: DataContract, underRepair: ReadonlySet<string>): readonly SuggestionFinding[];
79
+ //# sourceMappingURL=preserve-action-members.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preserve-action-members.d.ts","sourceRoot":"","sources":["../src/preserve-action-members.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAE3B,mGAAmG;AACnG,eAAO,MAAM,qBAAqB,EAAG,uBAAgC,CAAC;AACtE,kGAAkG;AAClG,eAAO,MAAM,oBAAoB,EAAG,sBAA+B,CAAC;AA6CpE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEjG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B;IAAE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAAE,CAuErF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,SAAS,iBAAiB,EAAE,CAc9B"}