agentfootprint 9.19.0 → 9.20.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.
Files changed (60) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +3 -1
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/core/agent/resultCeiling.js +65 -0
  5. package/dist/core/agent/resultCeiling.js.map +1 -0
  6. package/dist/core/agent/stages/toolCalls.js +104 -8
  7. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  8. package/dist/core/tools.js +34 -1
  9. package/dist/core/tools.js.map +1 -1
  10. package/dist/esm/core/agent/resultCeiling.d.ts +58 -0
  11. package/dist/esm/core/agent/resultCeiling.js +61 -0
  12. package/dist/esm/core/agent/resultCeiling.js.map +1 -0
  13. package/dist/esm/core/agent/stages/toolCalls.js +104 -8
  14. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  15. package/dist/esm/core/tools.d.ts +46 -0
  16. package/dist/esm/core/tools.js +32 -0
  17. package/dist/esm/core/tools.js.map +1 -1
  18. package/dist/esm/events/payloads.d.ts +29 -1
  19. package/dist/esm/events/payloads.js +3 -1
  20. package/dist/esm/events/payloads.js.map +1 -1
  21. package/dist/esm/events/registry.d.ts +3 -1
  22. package/dist/esm/events/registry.js +2 -0
  23. package/dist/esm/events/registry.js.map +1 -1
  24. package/dist/esm/index.d.ts +2 -2
  25. package/dist/esm/index.js +1 -1
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/lib/injection-engine/skillGraph.js +7 -7
  28. package/dist/esm/lib/injection-engine/skillGraph.js.map +1 -1
  29. package/dist/esm/lib/injection-engine/skillGraphCheckup.js +9 -0
  30. package/dist/esm/lib/injection-engine/skillGraphCheckup.js.map +1 -1
  31. package/dist/esm/lib/injection-engine/skillMatch.d.ts +43 -3
  32. package/dist/esm/lib/injection-engine/skillMatch.js +177 -13
  33. package/dist/esm/lib/injection-engine/skillMatch.js.map +1 -1
  34. package/dist/events/payloads.js +3 -1
  35. package/dist/events/payloads.js.map +1 -1
  36. package/dist/events/registry.js +2 -0
  37. package/dist/events/registry.js.map +1 -1
  38. package/dist/index.js +3 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/injection-engine/skillGraph.js +7 -7
  41. package/dist/lib/injection-engine/skillGraph.js.map +1 -1
  42. package/dist/lib/injection-engine/skillGraphCheckup.js +9 -0
  43. package/dist/lib/injection-engine/skillGraphCheckup.js.map +1 -1
  44. package/dist/lib/injection-engine/skillMatch.js +177 -13
  45. package/dist/lib/injection-engine/skillMatch.js.map +1 -1
  46. package/dist/types/core/agent/resultCeiling.d.ts +59 -0
  47. package/dist/types/core/agent/resultCeiling.d.ts.map +1 -0
  48. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  49. package/dist/types/core/tools.d.ts +46 -0
  50. package/dist/types/core/tools.d.ts.map +1 -1
  51. package/dist/types/events/payloads.d.ts +29 -1
  52. package/dist/types/events/payloads.d.ts.map +1 -1
  53. package/dist/types/events/registry.d.ts +3 -1
  54. package/dist/types/events/registry.d.ts.map +1 -1
  55. package/dist/types/index.d.ts +2 -2
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/lib/injection-engine/skillGraphCheckup.d.ts.map +1 -1
  58. package/dist/types/lib/injection-engine/skillMatch.d.ts +43 -3
  59. package/dist/types/lib/injection-engine/skillMatch.d.ts.map +1 -1
  60. package/package.json +1 -1
@@ -8,8 +8,37 @@
8
8
  * Emits: N/A (types only).
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.defineTool = exports.warnIfInvalidToolName = exports.assertValidToolName = void 0;
11
+ exports.defineTool = exports.warnIfInvalidToolName = exports.assertValidToolName = exports.assertResultCeiling = void 0;
12
12
  const footprintjs_1 = require("footprintjs");
13
+ /**
14
+ * Refuse a `resultCeiling` this library cannot honor, at definition time —
15
+ * naming the tool and the fix, never failing at the first oversized result of
16
+ * the first run. Exported beside {@link assertValidToolName} for consumers
17
+ * assembling `Tool` objects by hand.
18
+ */
19
+ function assertResultCeiling(toolName, ceiling) {
20
+ if (ceiling === undefined)
21
+ return;
22
+ const { maxChars, narrowBy } = ceiling;
23
+ if (!Number.isFinite(maxChars) || !Number.isInteger(maxChars) || maxChars <= 0) {
24
+ throw new Error(`defineTool: tool '${toolName}' declares resultCeiling.maxChars = ${String(maxChars)}, ` +
25
+ `which is not a positive whole number of characters. The ceiling is the size over ` +
26
+ `which the model reads a refusal instead of the result — to have no ceiling, omit ` +
27
+ `\`resultCeiling\` (absent means results are never measured, exactly as before).`);
28
+ }
29
+ if (narrowBy !== undefined) {
30
+ const usable = Array.isArray(narrowBy) &&
31
+ narrowBy.length > 0 &&
32
+ narrowBy.every((n) => typeof n === 'string' && n.trim().length > 0);
33
+ if (!usable) {
34
+ throw new Error(`defineTool: tool '${toolName}' declares resultCeiling.narrowBy = ` +
35
+ `${JSON.stringify(narrowBy)}, which names nothing the refusal could suggest. ` +
36
+ `List at least one parameter name (non-empty strings, e.g. ['limit', 'fields']), ` +
37
+ `or drop the field — omitting it is how "no suggestions" is said.`);
38
+ }
39
+ }
40
+ }
41
+ exports.assertResultCeiling = assertResultCeiling;
13
42
  /**
14
43
  * Ergonomic builder for `Tool`. Equivalent to constructing an object
15
44
  * literal with `schema` + `execute`, but flatter and safer — the name
@@ -81,6 +110,9 @@ function warnIfInvalidToolName(name) {
81
110
  exports.warnIfInvalidToolName = warnIfInvalidToolName;
82
111
  function defineTool(options) {
83
112
  warnIfInvalidToolName(options.name);
113
+ // A ceiling that cannot cap fails HERE, naming the tool — not at the first
114
+ // oversized result of the first run.
115
+ assertResultCeiling(options.name, options.resultCeiling);
84
116
  return {
85
117
  schema: {
86
118
  name: options.name,
@@ -94,6 +126,7 @@ function defineTool(options) {
94
126
  // Copied verbatim, never inferred — an empty array is a tool that declared
95
127
  // it touches nothing, which is a different statement from saying nothing.
96
128
  ...(options.capabilities !== undefined && { capabilities: options.capabilities }),
129
+ ...(options.resultCeiling !== undefined && { resultCeiling: options.resultCeiling }),
97
130
  execute: options.execute,
98
131
  };
99
132
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/core/tools.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,6CAAwC;AAoNxC;;;;;;;;;;;;;;;;;;GAkBG;AACH;;;;;;;GAOG;AACH,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAEjD;;;;;;GAMG;AACH,SAAgB,mBAAmB,CAAC,IAAa;IAC/C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,yDAAyD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAClF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,MAAM,GACV,IAAI,CAAC,MAAM,GAAG,EAAE;YACd,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,iBAAiB;YACvC,CAAC,CAAC,mFAAmF,CAAC;QAC1F,MAAM,IAAI,KAAK,CACb,aAAa,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,MAAM,IAAI;YAC/C,sFAAsF;YACtF,wEAAwE;YACxE,0DAA0D,CAC7D,CAAC;IACJ,CAAC;AACH,CAAC;AAlBD,kDAkBC;AAED;;;;;;GAMG;AACH,SAAgB,qBAAqB,CAAC,IAAa;IACjD,IAAI,CAAC,IAAA,uBAAS,GAAE;QAAE,OAAO;IACzB,IAAI,CAAC;QACH,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,sCAAsC;QACtC,OAAO,CAAC,IAAI,CAAC,4BAA6B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IACnE,CAAC;AACH,CAAC;AARD,sDAQC;AAED,SAAgB,UAAU,CACxB,OAA0C;IAE1C,qBAAqB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO;QACL,MAAM,EAAE;YACN,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE;SACvE;QACD,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC9C,uEAAuE;QACvE,2DAA2D;QAC3D,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAwB,EAAE,CAAC;QACnF,2EAA2E;QAC3E,0EAA0E;QAC1E,GAAG,CAAC,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;QACjF,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC;AACJ,CAAC;AAnBD,gCAmBC"}
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/core/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * resultCeiling — the refusing ceiling a tool declares on its OWN result
3
+ * (9.20.0). One owner of the measurement and the refusal sentence; the
4
+ * tool-dispatch loop (`stages/toolCalls.ts`) calls it at every boundary where
5
+ * a handler's return becomes final, so a resumed call is refused exactly as an
6
+ * inline one.
7
+ *
8
+ * Pattern: measured refusal at the dispatch boundary — the sibling of
9
+ * `toolResultCap.ts`, with the OPPOSITE answer to overflow, because
10
+ * the two serve different owners:
11
+ *
12
+ * • `maxToolResultChars` (agent-level, 9.11.0) TRUNCATES: the operator caps
13
+ * tools they did not write, so the most honest deliverable is a marker
14
+ * plus a verbatim head of whatever came back.
15
+ * • `resultCeiling` (per-tool, this module) REFUSES: the AUTHOR declared
16
+ * what a sane result is and which parameters (`narrowBy`) make a retry
17
+ * smaller. A truncated result reads as a complete one — the model cannot
18
+ * tell the data ends where the cut happened and fabricates from the part
19
+ * it saw — while a refusal that says "No data was returned" and names the
20
+ * narrowing parameters produces a clean retry (field-verified on a
21
+ * ~191k-char return).
22
+ *
23
+ * The oversized payload never leaves the dispatch: not into history, not into
24
+ * `stream.tool_end`, not into any recorder. The RECORD keeps the truth as the
25
+ * typed `agentfootprint.tools.result_refused` event (true size, ceiling,
26
+ * suggestions), emitted by the caller beside the replacement; the delivered
27
+ * result carries status `'invalid'` — the closed-set member whose corrective
28
+ * action is "fix the call" — so `onToolStatus` edges can route on it.
29
+ *
30
+ * Emits: nothing itself (pure measurement); zero-cost-when-unused — no
31
+ * declared ceiling means one `undefined` check and byte-identical behavior.
32
+ */
33
+ import type { ToolResultCeiling } from '../tools.js';
34
+ /** What an over-ceiling measurement hands the dispatch loop: the sentence the
35
+ * model reads and the true size for the record. `undefined` from
36
+ * {@link applyResultCeiling} means no ceiling or under it — untouched path. */
37
+ export interface ResultCeilingRefusal {
38
+ /** The teaching refusal — the ONLY thing the model (and every downstream
39
+ * channel) receives in place of the oversized payload. */
40
+ readonly refusal: string;
41
+ /** The stringified result's true length — the record's fact, carried by the
42
+ * `tools.result_refused` event, never by any content channel. */
43
+ readonly sizeChars: number;
44
+ }
45
+ /**
46
+ * Measure one handler result against the tool's declared ceiling.
47
+ *
48
+ * Returns `undefined` when there is no ceiling or the result fits — the caller
49
+ * keeps every byte of today's path. Over the ceiling, returns the refusal to
50
+ * deliver INSTEAD of the payload. What is measured is the string the model
51
+ * would have read (strings as-is, everything else `safeStringify`d) — the same
52
+ * rule `toolResultCap.ts` measures by, so the two ceilings always argue about
53
+ * the same number.
54
+ */
55
+ export declare function applyResultCeiling(value: unknown, opts: {
56
+ readonly toolName: string;
57
+ readonly ceiling: ToolResultCeiling | undefined;
58
+ }): ResultCeilingRefusal | undefined;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * resultCeiling — the refusing ceiling a tool declares on its OWN result
3
+ * (9.20.0). One owner of the measurement and the refusal sentence; the
4
+ * tool-dispatch loop (`stages/toolCalls.ts`) calls it at every boundary where
5
+ * a handler's return becomes final, so a resumed call is refused exactly as an
6
+ * inline one.
7
+ *
8
+ * Pattern: measured refusal at the dispatch boundary — the sibling of
9
+ * `toolResultCap.ts`, with the OPPOSITE answer to overflow, because
10
+ * the two serve different owners:
11
+ *
12
+ * • `maxToolResultChars` (agent-level, 9.11.0) TRUNCATES: the operator caps
13
+ * tools they did not write, so the most honest deliverable is a marker
14
+ * plus a verbatim head of whatever came back.
15
+ * • `resultCeiling` (per-tool, this module) REFUSES: the AUTHOR declared
16
+ * what a sane result is and which parameters (`narrowBy`) make a retry
17
+ * smaller. A truncated result reads as a complete one — the model cannot
18
+ * tell the data ends where the cut happened and fabricates from the part
19
+ * it saw — while a refusal that says "No data was returned" and names the
20
+ * narrowing parameters produces a clean retry (field-verified on a
21
+ * ~191k-char return).
22
+ *
23
+ * The oversized payload never leaves the dispatch: not into history, not into
24
+ * `stream.tool_end`, not into any recorder. The RECORD keeps the truth as the
25
+ * typed `agentfootprint.tools.result_refused` event (true size, ceiling,
26
+ * suggestions), emitted by the caller beside the replacement; the delivered
27
+ * result carries status `'invalid'` — the closed-set member whose corrective
28
+ * action is "fix the call" — so `onToolStatus` edges can route on it.
29
+ *
30
+ * Emits: nothing itself (pure measurement); zero-cost-when-unused — no
31
+ * declared ceiling means one `undefined` check and byte-identical behavior.
32
+ */
33
+ import { safeStringify } from './validators.js';
34
+ /**
35
+ * Measure one handler result against the tool's declared ceiling.
36
+ *
37
+ * Returns `undefined` when there is no ceiling or the result fits — the caller
38
+ * keeps every byte of today's path. Over the ceiling, returns the refusal to
39
+ * deliver INSTEAD of the payload. What is measured is the string the model
40
+ * would have read (strings as-is, everything else `safeStringify`d) — the same
41
+ * rule `toolResultCap.ts` measures by, so the two ceilings always argue about
42
+ * the same number.
43
+ */
44
+ export function applyResultCeiling(value, opts) {
45
+ const ceiling = opts.ceiling;
46
+ if (ceiling === undefined)
47
+ return undefined;
48
+ const text = typeof value === 'string' ? value : safeStringify(value);
49
+ if (text.length <= ceiling.maxChars)
50
+ return undefined;
51
+ const narrow = ceiling.narrowBy !== undefined && ceiling.narrowBy.length > 0
52
+ ? ` — e.g. pass ${ceiling.narrowBy.map((n) => `'${n}'`).join(', ')}`
53
+ : '';
54
+ return {
55
+ refusal: `Result too large: ${opts.toolName} returned ${text.length} chars, over its declared ` +
56
+ `${ceiling.maxChars}-char ceiling. Narrow the request and call again${narrow}. ` +
57
+ `No data was returned.`,
58
+ sizeChars: text.length,
59
+ };
60
+ }
61
+ //# sourceMappingURL=resultCeiling.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resultCeiling.js","sourceRoot":"","sources":["../../../../src/core/agent/resultCeiling.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAchD;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAc,EACd,IAAoF;IAEpF,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC7B,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACtE,IAAI,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,MAAM,GACV,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QAC3D,CAAC,CAAC,gBAAgB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACpE,CAAC,CAAC,EAAE,CAAC;IACT,OAAO;QACL,OAAO,EACL,qBAAqB,IAAI,CAAC,QAAQ,aAAa,IAAI,CAAC,MAAM,4BAA4B;YACtF,GAAG,OAAO,CAAC,QAAQ,mDAAmD,MAAM,IAAI;YAChF,uBAAuB;QACzB,SAAS,EAAE,IAAI,CAAC,MAAM;KACvB,CAAC;AACJ,CAAC"}
@@ -57,6 +57,7 @@ import { recordDecisions } from '../middleware/ledger.js';
57
57
  import { formatToolArgIssues, validateToolArgs, } from '../toolArgsValidation.js';
58
58
  import { safeStringify } from '../validators.js';
59
59
  import { capToolResult } from '../toolResultCap.js';
60
+ import { applyResultCeiling } from '../resultCeiling.js';
60
61
  import { currentStepOf, pointerOf, readSkillStepIntro, skipAdvanceSentence, skipHoldSentence, skipNeedsReasonSentence, skipNothingActiveSentence, stepAdvanceSuffix, stepInProgress, SKIP_STEP_TOOL_NAME, } from '../../../lib/injection-engine/skillSteps.js';
61
62
  import { explainStatusOnlyNearMiss, pruneLeases, readToolResultEnvelope, tenantOf, } from '../toolEffects.js';
62
63
  /** Declaration order preserved, ids de-duplicated — the shape the gate's re-prompt
@@ -252,6 +253,38 @@ export function buildToolCallsHandler(deps) {
252
253
  : capToolResult(values.modelResult, { toolName, maxChars: max }).value,
253
254
  };
254
255
  };
256
+ /**
257
+ * The tool's OWN refusing ceiling (9.20.0) — ONE implementation for every
258
+ * dispatch door, applied at the execute boundary the moment the handler's
259
+ * return lands (BEFORE the after-tool chain, the read_skill-gate precedent:
260
+ * governance composes over what the model will actually read).
261
+ *
262
+ * `undefined` = no ceiling declared or the result fits — the caller keeps
263
+ * today's path byte for byte. Otherwise: the typed `tools.result_refused`
264
+ * event records the true size (the only place it survives), and the returned
265
+ * TEACHING REFUSAL replaces the payload on every channel — history,
266
+ * `stream.tool_end`, recorders. Distinct from the agent-level
267
+ * `maxToolResultChars` (which truncates with a verbatim head, measured AFTER
268
+ * the chain): this is the tool author's own contract, and the two compose —
269
+ * the refusal sentence is far under any sane agent cap.
270
+ */
271
+ const refuseOverCeiling = (scope, call, tool, value, declaredStatus) => {
272
+ const ceiling = tool?.resultCeiling;
273
+ const verdict = applyResultCeiling(value, { toolName: call.toolName, ceiling });
274
+ if (verdict === undefined || ceiling === undefined)
275
+ return undefined;
276
+ typedEmit(scope, 'agentfootprint.tools.result_refused', {
277
+ toolName: call.toolName,
278
+ toolCallId: call.toolCallId,
279
+ iteration: call.iteration,
280
+ sizeChars: verdict.sizeChars,
281
+ maxChars: ceiling.maxChars,
282
+ // Copied — event payloads are detached plain data, never shared refs.
283
+ ...(ceiling.narrowBy !== undefined && { narrowBy: [...ceiling.narrowBy] }),
284
+ ...(declaredStatus !== undefined && { declaredStatus }),
285
+ });
286
+ return verdict.refusal;
287
+ };
255
288
  // ── Step-procedure boundary (9.18.0) — ONE implementation, five sites ──
256
289
  // The batch loop and the four resume paths all finalize results; each
257
290
  // calls these where its result becomes final, so a pointer can never miss
@@ -789,8 +822,37 @@ export function buildToolCallsHandler(deps) {
789
822
  // judged exactly as an inline one's.
790
823
  const envelope = readToolResultEnvelope(result);
791
824
  if (envelope !== undefined) {
825
+ // The tool's own ceiling (9.20.0) measures the CONTENT — the channel
826
+ // that can overflow a context window. The DECLARED effects are small
827
+ // validated data and are still judged by the caller: a tool that
828
+ // proposed a transition and overflowed its payload does not lose the
829
+ // transition. The delivered status becomes 'invalid' (the declared
830
+ // status described a result the model never received — the
831
+ // `result_refused` event keeps what was declared).
832
+ const refusal = refuseOverCeiling(scope, { toolName, toolCallId, iteration }, tool, envelope.content, envelope.status);
833
+ if (refusal !== undefined) {
834
+ return {
835
+ result: refusal,
836
+ executed: true,
837
+ ceilingRefused: true,
838
+ envelope: { ...envelope, content: refusal, status: 'invalid' },
839
+ };
840
+ }
792
841
  return { result: envelope.content, executed: true, envelope };
793
842
  }
843
+ const refusal = refuseOverCeiling(scope, { toolName, toolCallId, iteration }, tool, result);
844
+ if (refusal !== undefined) {
845
+ // Synthesized empty-effects envelope: the resume paths read the
846
+ // routing status off `envelope.status`, and this is how a refused
847
+ // NON-envelope result carries `'invalid'` there without every path
848
+ // learning about ceilings. Empty effects/malformed judge to nothing.
849
+ return {
850
+ result: refusal,
851
+ executed: true,
852
+ ceilingRefused: true,
853
+ envelope: { content: refusal, effects: [], status: 'invalid', malformed: [] },
854
+ };
855
+ }
794
856
  // A status-only shape missing its `effects: []` marker is DATA (bytes
795
857
  // unchanged) — but never silently: name the dropped marker in dev mode.
796
858
  const nearMiss = explainStatusOnlyNearMiss(result);
@@ -932,6 +994,10 @@ export function buildToolCallsHandler(deps) {
932
994
  let denied = false;
933
995
  /** True once `tool.execute` has been entered — see `afterMoment`. */
934
996
  let executed = false;
997
+ /** The tool's own `resultCeiling` refused this call's result (9.20.0)
998
+ * — the call ran, but it must not complete a procedure step: the
999
+ * refusal's own instruction is to call again. */
1000
+ let ceilingRefused = false;
935
1001
  let haltContext;
936
1002
  if (permissionChecker) {
937
1003
  try {
@@ -1395,6 +1461,21 @@ export function buildToolCallsHandler(deps) {
1395
1461
  if (nearMiss !== undefined)
1396
1462
  warnEffect(tc.name, nearMiss);
1397
1463
  }
1464
+ // The tool's own refusing ceiling (9.20.0) — at the execute
1465
+ // boundary, BEFORE the gates and the after-tool chain, so
1466
+ // governance and the agent-level cap compose over what the
1467
+ // model will actually read (the read_skill-gate precedent).
1468
+ // A recognized envelope's DECLARED effects (`toolEnvelope`)
1469
+ // are still judged below — the effects channel is not the
1470
+ // channel that overflowed; the delivered status becomes
1471
+ // 'invalid' so `onToolStatus` edges can route the overflow
1472
+ // (the declared one rides the `result_refused` event).
1473
+ const overflowRefusal = refuseOverCeiling(scope, { toolName: tc.name, toolCallId: tc.id, iteration }, tool, result, toolStatus);
1474
+ if (overflowRefusal !== undefined) {
1475
+ ceilingRefused = true;
1476
+ result = overflowRefusal;
1477
+ toolStatus = 'invalid';
1478
+ }
1398
1479
  await endCall(tc.id);
1399
1480
  }
1400
1481
  catch (err) {
@@ -1552,8 +1633,10 @@ export function buildToolCallsHandler(deps) {
1552
1633
  // The advance: only a call that RAN clean completes a step — an
1553
1634
  // error result never advances (the pointer holds; the model
1554
1635
  // retries, skips with a reason, or routes around), and neither
1555
- // does a permission denial or a gate refusal.
1556
- if (executed && !error && !denied && !skillRejected) {
1636
+ // does a permission denial, a gate refusal, or a ceiling refusal
1637
+ // (the model holds no data and was told to call again — advancing
1638
+ // would contradict the refusal's own instruction).
1639
+ if (executed && !error && !denied && !skillRejected && !ceilingRefused) {
1557
1640
  resultStr += applyStepReturn(scope, {
1558
1641
  toolName: tc.name,
1559
1642
  toolCallId: tc.id,
@@ -1828,9 +1911,14 @@ export function buildToolCallsHandler(deps) {
1828
1911
  const dispatched = await resolveCredentialAndExecute(scope, tool, toolName, rest.args, toolCallId, iteration, env);
1829
1912
  result = dispatched.result;
1830
1913
  error = dispatched.error;
1831
- stepToolRan = dispatched.executed === true && error !== true;
1832
- if (stepToolRan)
1914
+ // A ceiling-refused call RAN but must not advance a step (9.20.0);
1915
+ // its envelope — status 'invalid' plus any surviving declared
1916
+ // effects — is still picked up and judged below.
1917
+ stepToolRan =
1918
+ dispatched.executed === true && error !== true && dispatched.ceilingRefused !== true;
1919
+ if (dispatched.executed === true && error !== true) {
1833
1920
  resumeEnvelope = dispatched.envelope;
1921
+ }
1834
1922
  // skip_step behind a middleware ask, approved (9.18.0): the
1835
1923
  // placeholder just landed — replace it with the authoritative
1836
1924
  // sentence BEFORE the chain's last word, the execute loop's
@@ -1954,9 +2042,12 @@ export function buildToolCallsHandler(deps) {
1954
2042
  const dispatched = await resolveCredentialAndExecute(scope, tool, toolName, args, toolCallId, iteration, env);
1955
2043
  result = dispatched.result;
1956
2044
  error = dispatched.error;
1957
- stepToolRan = dispatched.executed === true && error !== true;
1958
- if (stepToolRan)
2045
+ // Same ceiling law as the ask path: ran ≠ completed a step (9.20.0).
2046
+ stepToolRan =
2047
+ dispatched.executed === true && error !== true && dispatched.ceilingRefused !== true;
2048
+ if (dispatched.executed === true && error !== true) {
1959
2049
  resumeEnvelope = dispatched.envelope;
2050
+ }
1960
2051
  // Consent moved the execution to this side of the pause; the rules
1961
2052
  // about results move with it.
1962
2053
  if (dispatched.executed === true) {
@@ -2075,8 +2166,13 @@ export function buildToolCallsHandler(deps) {
2075
2166
  modelResult = consentCapped.modelResult;
2076
2167
  let consentResultStr = typeof modelResult === 'string' ? modelResult : safeStringify(modelResult);
2077
2168
  // Step boundary (9.18.0) — a step tool that waited on 3LO consent
2078
- // advances the moment its resumed dispatch really ran, clean.
2079
- if (deps.stepPlanFor && dispatched.executed === true && error !== true) {
2169
+ // advances the moment its resumed dispatch really ran, clean. A
2170
+ // ceiling-refused result is not clean (9.20.0): the model holds no
2171
+ // data and was told to call again.
2172
+ if (deps.stepPlanFor &&
2173
+ dispatched.executed === true &&
2174
+ error !== true &&
2175
+ dispatched.ceilingRefused !== true) {
2080
2176
  consentResultStr += applyStepReturn(scope, { toolName, toolCallId, iteration });
2081
2177
  }
2082
2178
  // Typed tool effects (9.19.0) — same judge as every other path.