agentfootprint 9.86.0 → 9.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +179 -8
  2. package/canonical-notes.json +1 -1
  3. package/dist/core/agent/coverage/ledger.js +8 -2
  4. package/dist/core/agent/coverage/ledger.js.map +1 -1
  5. package/dist/core/agent/stages/toolCalls.js +109 -20
  6. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  7. package/dist/core/agent/stages/wrapUp.js +15 -4
  8. package/dist/core/agent/stages/wrapUp.js.map +1 -1
  9. package/dist/core/runbook/recording.js +3 -2
  10. package/dist/core/runbook/recording.js.map +1 -1
  11. package/dist/esm/core/agent/coverage/ledger.d.ts +5 -0
  12. package/dist/esm/core/agent/coverage/ledger.js +8 -2
  13. package/dist/esm/core/agent/coverage/ledger.js.map +1 -1
  14. package/dist/esm/core/agent/stages/toolCalls.d.ts +16 -2
  15. package/dist/esm/core/agent/stages/toolCalls.js +109 -20
  16. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  17. package/dist/esm/core/agent/stages/wrapUp.d.ts +11 -0
  18. package/dist/esm/core/agent/stages/wrapUp.js +15 -4
  19. package/dist/esm/core/agent/stages/wrapUp.js.map +1 -1
  20. package/dist/esm/core/agent/types.d.ts +5 -4
  21. package/dist/esm/core/runbook/recording.js +3 -2
  22. package/dist/esm/core/runbook/recording.js.map +1 -1
  23. package/dist/esm/events/payloads.d.ts +8 -0
  24. package/dist/esm/lib/injection-engine/skillSteps.js +6 -2
  25. package/dist/esm/lib/injection-engine/skillSteps.js.map +1 -1
  26. package/dist/esm/lib/trace-toolpack/traceToolpack.js +21 -14
  27. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
  28. package/dist/lib/injection-engine/skillSteps.js +6 -2
  29. package/dist/lib/injection-engine/skillSteps.js.map +1 -1
  30. package/dist/lib/trace-toolpack/traceToolpack.js +21 -14
  31. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
  32. package/dist/types/core/agent/coverage/ledger.d.ts +5 -0
  33. package/dist/types/core/agent/coverage/ledger.d.ts.map +1 -1
  34. package/dist/types/core/agent/stages/toolCalls.d.ts +16 -2
  35. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  36. package/dist/types/core/agent/stages/wrapUp.d.ts +11 -0
  37. package/dist/types/core/agent/stages/wrapUp.d.ts.map +1 -1
  38. package/dist/types/core/agent/types.d.ts +5 -4
  39. package/dist/types/core/agent/types.d.ts.map +1 -1
  40. package/dist/types/core/runbook/recording.d.ts.map +1 -1
  41. package/dist/types/events/payloads.d.ts +8 -0
  42. package/dist/types/events/payloads.d.ts.map +1 -1
  43. package/dist/types/lib/injection-engine/skillSteps.d.ts.map +1 -1
  44. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
  45. package/package.json +1 -1
@@ -44,6 +44,7 @@
44
44
  */
45
45
  import type { PausableHandler, TypedScope } from 'footprintjs';
46
46
  import type { PermissionChecker } from '../../../adapters/types.js';
47
+ import { type TurnRoute } from '../../../lib/injection-engine/routingPolicy.js';
47
48
  import { type SkillTargetClass } from '../../../lib/injection-engine/skillGraph.js';
48
49
  import { type SpokenIds } from '../../../lib/spokenIds.js';
49
50
  import type { ToolProvider } from '../../../tool-providers/types.js';
@@ -563,7 +564,20 @@ export declare function composeReadSkillRefusal(args: {
563
564
  * `held: true` with nothing named says a menu WAS outstanding and none of it
564
565
  * may be named, which is a third sentence, not the no-menu one. */
565
566
  readonly menuOffered?: SpokenIds;
566
- /** How the turn's start was resolved, for the `'guard'`-without-a-menu arm. */
567
- readonly routedDecisively?: boolean;
567
+ /** How the turn's start was resolved — `TurnRoute.by` — for the
568
+ * `'guard'`-without-a-menu arm. Each value is a different past fact about
569
+ * the turn, and the arm says only the one it was handed (9.86.1): it used to
570
+ * append "Declared routes moved the cursor instead." to every no-menu
571
+ * refusal, which is false for a cursor carried over from the previous turn
572
+ * (`'continuity'`) and for a menu the model's own pick had resolved
573
+ * (`'menu'`), and unprovable for `'none'`. */
574
+ readonly turnStartedBy?: TurnRoute['by'];
575
+ /** A cursor EXISTED on that call and the role filter kept its id out of the
576
+ * sentence (9.86.1). `cursorId` is absent in two situations that need two
577
+ * different sentences: a cold start, where "the turn's start" is the true
578
+ * anchor, and a hidden cursor, where the skill is real and merely unnamed.
579
+ * The description already withholds a hidden cursor's name — pinned by
580
+ * `test/security/skill-visibility.test.ts` — and the refusal printed it raw. */
581
+ readonly cursorWithheld?: boolean;
568
582
  }): string;
569
583
  export declare function buildToolCallsHandler(deps: ToolCallsHandlerDeps): PausableHandler<TypedScope<AgentState>>;
@@ -271,7 +271,7 @@ export function unknownToolResult(toolName, dispatchable) {
271
271
  * composers rather than re-deriving what it says.
272
272
  */
273
273
  export function composeReadSkillRefusal(args) {
274
- const { requestedId, targetClass, cursorId, hops, openIds, isTree, posture, menuOffered, routedDecisively, } = args;
274
+ const { requestedId, targetClass, cursorId, hops, openIds, isTree, posture, menuOffered, turnStartedBy, cursorWithheld, } = args;
275
275
  const head = `read_skill("${requestedId}") was not granted on that call: `;
276
276
  // Open skills are the one list every arm may name: they are admitted from
277
277
  // every cursor and no posture governs them (the posture arm below is reached
@@ -302,10 +302,23 @@ export function composeReadSkillRefusal(args) {
302
302
  return (`${head}this graph's 'guard' posture admits a routing pick only from the menu the ` +
303
303
  `framework offered, and '${requestedId}' was not admitted on that call.${openClause}`);
304
304
  }
305
+ // What the gate KNOWS about why no menu was outstanding is `TurnRoute.by`,
306
+ // and each value is a different finished fact. Nothing is asserted about
307
+ // how the cursor came to be where it was beyond that — the old tail,
308
+ // "Declared routes moved the cursor instead.", was composed for every
309
+ // value and was true of none of them in particular.
310
+ const startClause = turnStartedBy === 'entry' || turnStartedBy === 'intent'
311
+ ? " — the turn's start had already been resolved decisively"
312
+ : turnStartedBy === 'continuity'
313
+ ? ' — the cursor had been carried over from the previous turn'
314
+ : turnStartedBy === 'menu'
315
+ ? ' — the menu had already been resolved by an earlier pick'
316
+ : turnStartedBy === 'decider'
317
+ ? " — the menu had been resolved by the configured decider before the turn's first call"
318
+ : '';
305
319
  return (`${head}this graph's 'guard' posture admits a routing pick only while the framework ` +
306
320
  `has declared ambiguity, and no menu was outstanding when that call was made` +
307
- `${routedDecisively === true ? " — the turn's start had already been resolved decisively" : ''}` +
308
- `. Declared routes moved the cursor instead.${openClause}`);
321
+ `${startClause}.${openClause}`);
309
322
  }
310
323
  if (isTree === true) {
311
324
  return (`${head}this map is a decision tree. A tree routes by predicate on every iteration ` +
@@ -316,7 +329,14 @@ export function composeReadSkillRefusal(args) {
316
329
  // this arm — 'self' is answered by the notice, 'hop' and 'open' are admitted
317
330
  // — and it is carried rather than re-derived so the sentence and the verdict
318
331
  // read the same classification.
319
- const from = cursorId !== undefined ? `'${cursorId}'` : "the turn's start";
332
+ // Three anchors, not two: a named cursor, a cursor this caller may not be
333
+ // told the name of, and a genuine cold start. The middle one used to print
334
+ // the raw id — the one name the description's own law withholds.
335
+ const from = cursorId !== undefined
336
+ ? `'${cursorId}'`
337
+ : cursorWithheld === true
338
+ ? 'the skill the cursor stood in'
339
+ : "the turn's start";
320
340
  // The gate reaches this arm only with `'unreachable'`; the weaker sentence is
321
341
  // for a caller that refused an admissible class for a reason of its own, and
322
342
  // it is deliberately not an invented explanation of one.
@@ -925,6 +945,11 @@ export function buildToolCallsHandler(deps) {
925
945
  }
926
946
  const currentSkillId = scope.currentSkillId;
927
947
  const hops = deps.allowedSkillIds(currentSkillId);
948
+ const targetClass = classifySkillTarget({
949
+ ...(currentSkillId !== undefined && { cursor: currentSkillId }),
950
+ target,
951
+ hops,
952
+ });
928
953
  // ── A PROPOSAL TO THE CURSOR'S OWN SKILL IS A STAY (9.86.0) ──────
929
954
  // `makeReachableSkills` filters the cursor out of its own successor
930
955
  // set — right for a MOVE, and this judge read it as "not reachable"
@@ -937,11 +962,47 @@ export function buildToolCallsHandler(deps) {
937
962
  // The event carries the existing `'accepted'` outcome plus an
938
963
  // ADDITIVE `stay: true`, deliberately not a new enum member: an
939
964
  // exhaustive switch over `outcome` in a consumer must keep compiling.
940
- if (classifySkillTarget({
941
- ...(currentSkillId !== undefined && { cursor: currentSkillId }),
942
- target,
943
- hops,
944
- }) === 'self') {
965
+ //
966
+ // A STAY COMPETES FOR THE SLOT LIKE ANY OTHER ACCEPTED PROPOSAL
967
+ // (9.86.1). The batch law is "first ACCEPTED proposal wins, later
968
+ // proposals to OTHER targets are superseded", and a stay is accepted
969
+ // — so a stay judged first holds the slot (with nothing written to
970
+ // `pendingToolTransition`, because a stay moves nothing) and a later
971
+ // sibling proposing a hop is `'superseded'` and lands in the
972
+ // `route_conflict` losers, exactly as two conflicting hops do. Before
973
+ // this the stay `continue`d past the bookkeeping, so the tool that
974
+ // judged its data first lost to whichever sibling came second, with
975
+ // two `'accepted'` events in one batch and no conflict on the record.
976
+ // The same law in the other order: a stay proposed AFTER an accepted
977
+ // hop is superseded by it, not accepted beside it.
978
+ if (targetClass === 'self') {
979
+ if (state.winner !== undefined && state.winner.targetSkillId !== target) {
980
+ state.losers.push({
981
+ toolCallId: call.toolCallId,
982
+ toolName: call.toolName,
983
+ target,
984
+ });
985
+ typedEmit(scope, 'agentfootprint.tools.effect', {
986
+ kind: 'propose-transition',
987
+ outcome: 'superseded',
988
+ toolName: call.toolName,
989
+ toolCallId: call.toolCallId,
990
+ iteration: call.iteration,
991
+ targetSkillId: target,
992
+ reason: effect.reason,
993
+ supersededBy: 'earlier-proposal',
994
+ });
995
+ continue;
996
+ }
997
+ if (state.winner === undefined) {
998
+ state.winner = {
999
+ targetSkillId: target,
1000
+ toolName: call.toolName,
1001
+ toolCallId: call.toolCallId,
1002
+ reason: effect.reason,
1003
+ iteration: call.iteration,
1004
+ };
1005
+ }
945
1006
  typedEmit(scope, 'agentfootprint.tools.effect', {
946
1007
  kind: 'propose-transition',
947
1008
  outcome: 'accepted',
@@ -954,10 +1015,25 @@ export function buildToolCallsHandler(deps) {
954
1015
  });
955
1016
  continue;
956
1017
  }
957
- if (!hops.includes(target)) {
1018
+ if (targetClass === 'unreachable') {
1019
+ // ── THE REFUSAL SPEAKS WITH THE FILTERED SETS (9.86.1) ──────────
1020
+ // This sentence is appended to the tool result the model reads and
1021
+ // rides the `tools.effect` payload, and it named the raw hop set and
1022
+ // the raw cursor — the leak the `read_skill` refusals closed one
1023
+ // function up. Same filter, same law: the hidden set the tools slot
1024
+ // resolved for this iteration, and OMIT, NEVER DENY — a hop set the
1025
+ // filter emptied composes no reachable clause rather than an empty
1026
+ // one. Admission is still judged on the raw `hops`.
1027
+ const hiddenIds = new Set(scope.hiddenSkillIds ?? []);
1028
+ const mayName = (id) => !hiddenIds.has(id);
1029
+ const reachable = spoken(hops, mayName);
1030
+ const from = currentSkillId === undefined
1031
+ ? 'the turn start'
1032
+ : mayName(currentSkillId)
1033
+ ? `'${currentSkillId}'`
1034
+ : 'the skill the cursor stood in';
958
1035
  refuse('propose-transition', `propose-transition → '${target}' was refused: '${target}' is not reachable from ` +
959
- `${currentSkillId !== undefined ? `'${currentSkillId}'` : 'the turn start'} per ` +
960
- `the graph's own law${hops.length > 0 ? ` (reachable: ${hops.join(', ')})` : ''}. The graph decides — a proposal is evidence, never authority.`, { targetSkillId: target, reason: effect.reason });
1036
+ `${from} per the graph's own law${reachable.named.length > 0 ? ` (reachable: ${reachable.named.join(', ')})` : ''}. The graph decides — a proposal is evidence, never authority.`, { targetSkillId: target, reason: effect.reason });
961
1037
  continue;
962
1038
  }
963
1039
  if (state.winner !== undefined && state.winner.targetSkillId !== target) {
@@ -2779,6 +2855,15 @@ export function buildToolCallsHandler(deps) {
2779
2855
  // "nothing reachable may be named", and it used to assert the first.
2780
2856
  const hopsSpoken = spoken(hops, mayName);
2781
2857
  const openSpoken = spoken(openAll, mayName);
2858
+ // The cursor through the SAME filter (9.86.1). The description
2859
+ // withholds a hidden cursor's name and every refusal below printed
2860
+ // it raw in two clauses; `cursorWithheld` keeps the sentence's
2861
+ // anchor honest ("the skill the cursor stood in") without the id.
2862
+ const cursorSpoken = currentSkillId === undefined
2863
+ ? {}
2864
+ : mayName(currentSkillId)
2865
+ ? { cursorId: currentSkillId }
2866
+ : { cursorWithheld: true };
2782
2867
  const hopsNamed = hopsSpoken.named;
2783
2868
  const openNamed = openSpoken.named;
2784
2869
  // The ONE owner of "is this target the cursor?" — the same function
@@ -2896,7 +2981,7 @@ export function buildToolCallsHandler(deps) {
2896
2981
  result = composeReadSkillRefusal({
2897
2982
  requestedId: reqId,
2898
2983
  targetClass,
2899
- ...(currentSkillId !== undefined && { cursorId: currentSkillId }),
2984
+ ...cursorSpoken,
2900
2985
  hops: hopsSpoken,
2901
2986
  openIds: openSpoken,
2902
2987
  ...(deps.skillGraphIsTree === true && { isTree: true }),
@@ -2933,7 +3018,7 @@ export function buildToolCallsHandler(deps) {
2933
3018
  result = composeReadSkillRefusal({
2934
3019
  requestedId: reqId,
2935
3020
  targetClass,
2936
- ...(currentSkillId !== undefined && { cursorId: currentSkillId }),
3021
+ ...cursorSpoken,
2937
3022
  // No hop is named under a posture, and the graph's hop set
2938
3023
  // is not this arm's subject — `held: false` is the honest
2939
3024
  // shape, not a claim that nothing was reachable (this arm
@@ -2942,9 +3027,9 @@ export function buildToolCallsHandler(deps) {
2942
3027
  openIds: openSpoken,
2943
3028
  posture: deps.skillStrictness,
2944
3029
  ...(menuNamed !== undefined && { menuOffered: menuNamed }),
2945
- ...((turnRoute?.by === 'intent' || turnRoute?.by === 'entry') && {
2946
- routedDecisively: true,
2947
- }),
3030
+ // The whole verdict, not a boolean derived from two of its
3031
+ // six values: the composer says one past fact per value.
3032
+ ...(turnRoute !== undefined && { turnStartedBy: turnRoute.by }),
2948
3033
  });
2949
3034
  typedEmit(scope, 'agentfootprint.skill.rejected', {
2950
3035
  requestedId: reqId,
@@ -3390,10 +3475,14 @@ export function buildToolCallsHandler(deps) {
3390
3475
  // silently invents a decision, for the same reason it never silently
3391
3476
  // drops one.
3392
3477
  error = true;
3478
+ // A past fact about the resumed call, not a forecast about the
3479
+ // turn (9.86.1): "cannot be retried this turn … Answer without it,
3480
+ // or finish" was a prediction plus a standing order on a result
3481
+ // that is re-read on every later call.
3393
3482
  result =
3394
- `tool '${toolName}' was not executed and cannot be retried this turn: it declares ` +
3395
- `its own checkIn consent gate, that gate trips for these arguments, and a resumed ` +
3396
- `dispatch has no second checkpoint to ask on. Answer without it, or finish. (To ` +
3483
+ `tool '${toolName}' was not executed on that call, and the resumed dispatch had ` +
3484
+ `no second checkpoint to retry it on: it declares its own checkIn consent gate, ` +
3485
+ `and that gate tripped for those arguments. (To ` +
3397
3486
  `the agent's author: the middleware '${askedBy}' and the tool's checkIn ask ` +
3398
3487
  `different questions — one is the rule's, one is the tool's with the evidence ` +
3399
3488
  `pack attached — so approving one is not answering the other. Keep one gate for ` +