agentfootprint 9.18.0 → 9.19.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 (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +4 -2
  3. package/README.md +2 -0
  4. package/ai-instructions/claude-code/SKILL.md +1 -1
  5. package/dist/core/Agent.js +71 -1
  6. package/dist/core/Agent.js.map +1 -1
  7. package/dist/core/agent/AgentBuilder.js +66 -1
  8. package/dist/core/agent/AgentBuilder.js.map +1 -1
  9. package/dist/core/agent/buildAgentChart.js +19 -0
  10. package/dist/core/agent/buildAgentChart.js.map +1 -1
  11. package/dist/core/agent/buildDynamicAgentChart.js +42 -0
  12. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  13. package/dist/core/agent/skillBrains.js +216 -0
  14. package/dist/core/agent/skillBrains.js.map +1 -0
  15. package/dist/core/agent/stages/callLLM.js +29 -9
  16. package/dist/core/agent/stages/callLLM.js.map +1 -1
  17. package/dist/core/agent/stages/routeTurn.js +123 -3
  18. package/dist/core/agent/stages/routeTurn.js.map +1 -1
  19. package/dist/core/agent/stages/seed.js +9 -0
  20. package/dist/core/agent/stages/seed.js.map +1 -1
  21. package/dist/core/agent/stages/toolCalls.js +326 -8
  22. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  23. package/dist/core/agent/toolEffects.js +242 -0
  24. package/dist/core/agent/toolEffects.js.map +1 -0
  25. package/dist/esm/core/Agent.d.ts +6 -1
  26. package/dist/esm/core/Agent.js +71 -1
  27. package/dist/esm/core/Agent.js.map +1 -1
  28. package/dist/esm/core/agent/AgentBuilder.d.ts +43 -3
  29. package/dist/esm/core/agent/AgentBuilder.js +66 -1
  30. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  31. package/dist/esm/core/agent/buildAgentChart.d.ts +11 -0
  32. package/dist/esm/core/agent/buildAgentChart.js +19 -0
  33. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  34. package/dist/esm/core/agent/buildDynamicAgentChart.js +42 -0
  35. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  36. package/dist/esm/core/agent/skillBrains.d.ts +144 -0
  37. package/dist/esm/core/agent/skillBrains.js +209 -0
  38. package/dist/esm/core/agent/skillBrains.js.map +1 -0
  39. package/dist/esm/core/agent/stages/callLLM.d.ts +24 -0
  40. package/dist/esm/core/agent/stages/callLLM.js +29 -9
  41. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  42. package/dist/esm/core/agent/stages/routeTurn.d.ts +23 -0
  43. package/dist/esm/core/agent/stages/routeTurn.js +123 -3
  44. package/dist/esm/core/agent/stages/routeTurn.js.map +1 -1
  45. package/dist/esm/core/agent/stages/seed.d.ts +8 -0
  46. package/dist/esm/core/agent/stages/seed.js +9 -0
  47. package/dist/esm/core/agent/stages/seed.js.map +1 -1
  48. package/dist/esm/core/agent/stages/toolCalls.d.ts +33 -0
  49. package/dist/esm/core/agent/stages/toolCalls.js +326 -8
  50. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  51. package/dist/esm/core/agent/toolEffects.d.ts +202 -0
  52. package/dist/esm/core/agent/toolEffects.js +234 -0
  53. package/dist/esm/core/agent/toolEffects.js.map +1 -0
  54. package/dist/esm/core/agent/types.d.ts +35 -0
  55. package/dist/esm/events/payloads.d.ts +123 -2
  56. package/dist/esm/events/registry.d.ts +5 -1
  57. package/dist/esm/events/registry.js +4 -0
  58. package/dist/esm/events/registry.js.map +1 -1
  59. package/dist/esm/index.d.ts +2 -1
  60. package/dist/esm/index.js +6 -0
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js +57 -1
  63. package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
  64. package/dist/esm/lib/injection-engine/constrainedEnumPick.d.ts +52 -0
  65. package/dist/esm/lib/injection-engine/constrainedEnumPick.js +101 -0
  66. package/dist/esm/lib/injection-engine/constrainedEnumPick.js.map +1 -0
  67. package/dist/esm/lib/injection-engine/evaluator.d.ts +7 -0
  68. package/dist/esm/lib/injection-engine/evaluator.js +16 -0
  69. package/dist/esm/lib/injection-engine/evaluator.js.map +1 -1
  70. package/dist/esm/lib/injection-engine/factories/defineSkill.d.ts +22 -0
  71. package/dist/esm/lib/injection-engine/factories/defineSkill.js +7 -0
  72. package/dist/esm/lib/injection-engine/factories/defineSkill.js.map +1 -1
  73. package/dist/esm/lib/injection-engine/index.d.ts +1 -0
  74. package/dist/esm/lib/injection-engine/index.js +3 -0
  75. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  76. package/dist/esm/lib/injection-engine/llmClassifier.d.ts +11 -8
  77. package/dist/esm/lib/injection-engine/llmClassifier.js +27 -84
  78. package/dist/esm/lib/injection-engine/llmClassifier.js.map +1 -1
  79. package/dist/esm/lib/injection-engine/routingPolicy.d.ts +10 -3
  80. package/dist/esm/lib/injection-engine/routingPolicy.js.map +1 -1
  81. package/dist/esm/lib/injection-engine/skillGraph.d.ts +28 -3
  82. package/dist/esm/lib/injection-engine/skillGraph.js +80 -12
  83. package/dist/esm/lib/injection-engine/skillGraph.js.map +1 -1
  84. package/dist/esm/lib/injection-engine/types.d.ts +30 -0
  85. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  86. package/dist/esm/recorders/observability/RouteRecorder.d.ts +6 -1
  87. package/dist/esm/recorders/observability/RouteRecorder.js +10 -0
  88. package/dist/esm/recorders/observability/RouteRecorder.js.map +1 -1
  89. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +58 -0
  90. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  91. package/dist/events/registry.js +4 -0
  92. package/dist/events/registry.js.map +1 -1
  93. package/dist/index.js +11 -2
  94. package/dist/index.js.map +1 -1
  95. package/dist/lib/injection-engine/buildInjectionEngineSubflow.js +57 -1
  96. package/dist/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
  97. package/dist/lib/injection-engine/constrainedEnumPick.js +105 -0
  98. package/dist/lib/injection-engine/constrainedEnumPick.js.map +1 -0
  99. package/dist/lib/injection-engine/evaluator.js +16 -0
  100. package/dist/lib/injection-engine/evaluator.js.map +1 -1
  101. package/dist/lib/injection-engine/factories/defineSkill.js +7 -0
  102. package/dist/lib/injection-engine/factories/defineSkill.js.map +1 -1
  103. package/dist/lib/injection-engine/index.js +5 -1
  104. package/dist/lib/injection-engine/index.js.map +1 -1
  105. package/dist/lib/injection-engine/llmClassifier.js +27 -84
  106. package/dist/lib/injection-engine/llmClassifier.js.map +1 -1
  107. package/dist/lib/injection-engine/routingPolicy.js.map +1 -1
  108. package/dist/lib/injection-engine/skillGraph.js +80 -12
  109. package/dist/lib/injection-engine/skillGraph.js.map +1 -1
  110. package/dist/lib/injection-engine/types.js.map +1 -1
  111. package/dist/recorders/observability/RouteRecorder.js +10 -0
  112. package/dist/recorders/observability/RouteRecorder.js.map +1 -1
  113. package/dist/recorders/observability/commentary/commentaryTemplates.js +58 -0
  114. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  115. package/dist/types/core/Agent.d.ts +6 -1
  116. package/dist/types/core/Agent.d.ts.map +1 -1
  117. package/dist/types/core/agent/AgentBuilder.d.ts +43 -3
  118. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  119. package/dist/types/core/agent/buildAgentChart.d.ts +11 -0
  120. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  121. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  122. package/dist/types/core/agent/skillBrains.d.ts +145 -0
  123. package/dist/types/core/agent/skillBrains.d.ts.map +1 -0
  124. package/dist/types/core/agent/stages/callLLM.d.ts +24 -0
  125. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  126. package/dist/types/core/agent/stages/routeTurn.d.ts +23 -0
  127. package/dist/types/core/agent/stages/routeTurn.d.ts.map +1 -1
  128. package/dist/types/core/agent/stages/seed.d.ts +8 -0
  129. package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
  130. package/dist/types/core/agent/stages/toolCalls.d.ts +33 -0
  131. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  132. package/dist/types/core/agent/toolEffects.d.ts +203 -0
  133. package/dist/types/core/agent/toolEffects.d.ts.map +1 -0
  134. package/dist/types/core/agent/types.d.ts +35 -0
  135. package/dist/types/core/agent/types.d.ts.map +1 -1
  136. package/dist/types/events/payloads.d.ts +123 -2
  137. package/dist/types/events/payloads.d.ts.map +1 -1
  138. package/dist/types/events/registry.d.ts +5 -1
  139. package/dist/types/events/registry.d.ts.map +1 -1
  140. package/dist/types/index.d.ts +2 -1
  141. package/dist/types/index.d.ts.map +1 -1
  142. package/dist/types/lib/injection-engine/buildInjectionEngineSubflow.d.ts.map +1 -1
  143. package/dist/types/lib/injection-engine/constrainedEnumPick.d.ts +53 -0
  144. package/dist/types/lib/injection-engine/constrainedEnumPick.d.ts.map +1 -0
  145. package/dist/types/lib/injection-engine/evaluator.d.ts +7 -0
  146. package/dist/types/lib/injection-engine/evaluator.d.ts.map +1 -1
  147. package/dist/types/lib/injection-engine/factories/defineSkill.d.ts +22 -0
  148. package/dist/types/lib/injection-engine/factories/defineSkill.d.ts.map +1 -1
  149. package/dist/types/lib/injection-engine/index.d.ts +1 -0
  150. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  151. package/dist/types/lib/injection-engine/llmClassifier.d.ts +11 -8
  152. package/dist/types/lib/injection-engine/llmClassifier.d.ts.map +1 -1
  153. package/dist/types/lib/injection-engine/routingPolicy.d.ts +10 -3
  154. package/dist/types/lib/injection-engine/routingPolicy.d.ts.map +1 -1
  155. package/dist/types/lib/injection-engine/skillGraph.d.ts +28 -3
  156. package/dist/types/lib/injection-engine/skillGraph.d.ts.map +1 -1
  157. package/dist/types/lib/injection-engine/types.d.ts +30 -0
  158. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  159. package/dist/types/recorders/observability/RouteRecorder.d.ts +6 -1
  160. package/dist/types/recorders/observability/RouteRecorder.d.ts.map +1 -1
  161. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  162. package/package.json +1 -1
@@ -45,6 +45,7 @@
45
45
  */
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
47
  exports.buildToolCallsHandler = void 0;
48
+ const footprintjs_1 = require("footprintjs");
48
49
  const types_js_1 = require("../../../adapters/types.js");
49
50
  const typedEmit_js_1 = require("../../../recorders/core/typedEmit.js");
50
51
  const extractSequence_js_1 = require("../../../security/extractSequence.js");
@@ -60,6 +61,7 @@ const toolArgsValidation_js_1 = require("../toolArgsValidation.js");
60
61
  const validators_js_1 = require("../validators.js");
61
62
  const toolResultCap_js_1 = require("../toolResultCap.js");
62
63
  const skillSteps_js_1 = require("../../../lib/injection-engine/skillSteps.js");
64
+ const toolEffects_js_1 = require("../toolEffects.js");
63
65
  /** Declaration order preserved, ids de-duplicated — the shape the gate's re-prompt
64
66
  * and the `skill.rejected` payload both report as "what this gate accepts". */
65
67
  function dedupeIds(ids) {
@@ -351,6 +353,195 @@ function buildToolCallsHandler(deps) {
351
353
  const plan = deps.stepPlanFor?.(pickedId);
352
354
  return plan ? (0, skillSteps_js_1.readSkillStepIntro)(plan) : '';
353
355
  };
356
+ // ── Escalate-on-evidence (9.19.0) — the refusal budget ─────────────────
357
+ // Called beside BOTH `skill.rejected` emit sites (reachability + posture):
358
+ // real recorded refusals are the only evidence that counts. At the
359
+ // declared threshold the flip is committed (`skillEscalated` — the fact
360
+ // callLLM's brainFor reads) and `skill.escalated` fires ONCE; the counter
361
+ // keeps counting so the record shows the whole loop. Gated on the policy
362
+ // being declared — otherwise not one key is read or written.
363
+ const noteSkillRefusal = (scope, iteration) => {
364
+ const escalation = deps.escalation;
365
+ if (!escalation)
366
+ return;
367
+ const refusals = (scope.skillRefusalsThisTurn ?? 0) + 1;
368
+ scope.skillRefusalsThisTurn = refusals;
369
+ if (refusals >= escalation.afterRefusals && scope.skillEscalated !== true) {
370
+ scope.skillEscalated = true;
371
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.skill.escalated', {
372
+ iteration,
373
+ afterRefusals: escalation.afterRefusals,
374
+ refusals,
375
+ from: escalation.describeFrom(scope.currentSkillId, scope.resolvedModel),
376
+ to: escalation.to,
377
+ });
378
+ }
379
+ };
380
+ const warnEffect = (toolName, sentence) => {
381
+ if ((0, footprintjs_1.isDevMode)()) {
382
+ // eslint-disable-next-line no-console
383
+ console.warn(`agentfootprint tool-effects: tool '${toolName}' — ${sentence}`);
384
+ }
385
+ };
386
+ /**
387
+ * Judge one finalized result's effects, in declaration order. Returns the
388
+ * model-visible NOTE (teaching refusals only — an accepted effect speaks
389
+ * through what it does: the cursor move, the delivered instruction).
390
+ * Every judgment is a `tools.effect` event; nothing is half-applied.
391
+ */
392
+ const applyToolEffects = (scope, call, envelope, state) => {
393
+ let note = '';
394
+ const refuse = (kind, refusalReason, fields = {}) => {
395
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.tools.effect', {
396
+ kind,
397
+ outcome: 'refused',
398
+ toolName: call.toolName,
399
+ toolCallId: call.toolCallId,
400
+ iteration: call.iteration,
401
+ ...(fields.targetSkillId !== undefined && { targetSkillId: fields.targetSkillId }),
402
+ ...(fields.reason !== undefined && { reason: fields.reason }),
403
+ ...(fields.instructionId !== undefined && { instructionId: fields.instructionId }),
404
+ ...(fields.deliveryLease !== undefined && { deliveryLease: fields.deliveryLease }),
405
+ refusalReason,
406
+ });
407
+ note += ` [tool effect refused: ${refusalReason}]`;
408
+ warnEffect(call.toolName, refusalReason);
409
+ };
410
+ for (const bad of envelope.malformed)
411
+ refuse(bad.kind, bad.refusalReason);
412
+ for (const effect of envelope.effects) {
413
+ if (effect.kind === 'propose-transition') {
414
+ const target = effect.targetSkillId;
415
+ if (!deps.allowedSkillIds) {
416
+ refuse('propose-transition', `propose-transition → '${target}' needs a mounted skill graph: without one there ` +
417
+ `is no routing law to check the proposal against and no cursor to move. Mount ` +
418
+ `.skillGraph(), or drop the effect.`, { targetSkillId: target, reason: effect.reason });
419
+ continue;
420
+ }
421
+ const currentSkillId = scope.currentSkillId;
422
+ const hops = deps.allowedSkillIds(currentSkillId);
423
+ if (!hops.includes(target)) {
424
+ refuse('propose-transition', `propose-transition → '${target}' was refused: '${target}' is not reachable from ` +
425
+ `${currentSkillId !== undefined ? `'${currentSkillId}'` : 'the turn start'} per ` +
426
+ `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 });
427
+ continue;
428
+ }
429
+ if (state.winner !== undefined && state.winner.targetSkillId !== target) {
430
+ // A later proposal to a DIFFERENT target — suppressed under the
431
+ // route_conflict law (first in call order wins), on the record.
432
+ state.losers.push({
433
+ toolCallId: call.toolCallId,
434
+ toolName: call.toolName,
435
+ target,
436
+ });
437
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.tools.effect', {
438
+ kind: 'propose-transition',
439
+ outcome: 'superseded',
440
+ toolName: call.toolName,
441
+ toolCallId: call.toolCallId,
442
+ iteration: call.iteration,
443
+ targetSkillId: target,
444
+ reason: effect.reason,
445
+ supersededBy: 'earlier-proposal',
446
+ });
447
+ continue;
448
+ }
449
+ if (state.winner === undefined) {
450
+ state.winner = {
451
+ targetSkillId: target,
452
+ toolName: call.toolName,
453
+ toolCallId: call.toolCallId,
454
+ reason: effect.reason,
455
+ iteration: call.iteration,
456
+ };
457
+ // Committed AT ACCEPTANCE (not batch end) so a pause later in the
458
+ // batch checkpoints the accepted move instead of dropping it.
459
+ scope.pendingToolTransition = state.winner;
460
+ // The activation promise a gate-accepted read_skill pick makes,
461
+ // made here too: a model-edge-only target keeps its llm-activated
462
+ // trigger, so the ledger entry is what makes the body actually
463
+ // load when the cursor lands.
464
+ const activated = scope.activatedInjectionIds;
465
+ if (!activated.includes(target)) {
466
+ scope.activatedInjectionIds = [...activated, target];
467
+ }
468
+ }
469
+ // First acceptance AND a same-target repeat both land here: every
470
+ // proposal that asked for the move that happens is 'accepted'.
471
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.tools.effect', {
472
+ kind: 'propose-transition',
473
+ outcome: 'accepted',
474
+ toolName: call.toolName,
475
+ toolCallId: call.toolCallId,
476
+ iteration: call.iteration,
477
+ targetSkillId: target,
478
+ reason: effect.reason,
479
+ });
480
+ continue;
481
+ }
482
+ // require-instruction
483
+ const known = deps.leaseTargets?.get(effect.instructionId);
484
+ if (known === undefined) {
485
+ refuse('require-instruction', `require-instruction '${effect.instructionId}' was refused: no registered ` +
486
+ `injection carries that id — the push door serves the declared catalog only ` +
487
+ `(read_skill stays the pull door).`, { instructionId: effect.instructionId, deliveryLease: effect.deliveryLease });
488
+ continue;
489
+ }
490
+ if (known.surfaceMode === 'tool-only') {
491
+ refuse('require-instruction', `require-instruction '${effect.instructionId}' was refused: that skill declares ` +
492
+ `surfaceMode 'tool-only', whose body travels only as a read_skill result — the ` +
493
+ `system slot suppresses it, so the lease would be granted and never delivered. ` +
494
+ `Pull it with read_skill, or change the skill's surfaceMode.`, { instructionId: effect.instructionId, deliveryLease: effect.deliveryLease });
495
+ continue;
496
+ }
497
+ const tenant = (0, toolEffects_js_1.tenantOf)(scope.currentSkillId, scope.activatedInjectionIds);
498
+ const lease = {
499
+ instructionId: effect.instructionId,
500
+ deliveryLease: effect.deliveryLease,
501
+ ...(tenant !== undefined && { skillId: tenant }),
502
+ toolName: call.toolName,
503
+ toolCallId: call.toolCallId,
504
+ iteration: call.iteration,
505
+ };
506
+ // Prune-as-we-write: spent grants leave with the same write that adds
507
+ // the new one, so a long turn never accumulates dead leases.
508
+ scope.instructionLeases = [
509
+ ...(0, toolEffects_js_1.pruneLeases)(scope.instructionLeases, call.iteration, tenant),
510
+ lease,
511
+ ];
512
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.tools.effect', {
513
+ kind: 'require-instruction',
514
+ outcome: 'accepted',
515
+ toolName: call.toolName,
516
+ toolCallId: call.toolCallId,
517
+ iteration: call.iteration,
518
+ instructionId: effect.instructionId,
519
+ deliveryLease: effect.deliveryLease,
520
+ });
521
+ }
522
+ return note;
523
+ };
524
+ /** The batch-end aggregate for conflicting proposals — the
525
+ * `route_conflict` reuse, additive `source` says what conflicted. The
526
+ * per-effect `tools.effect` events above already carried every fact;
527
+ * this is the one-event-per-batch view the edge law already ships. */
528
+ const emitTransitionConflict = (scope, iteration, state) => {
529
+ if (state.winner === undefined || state.losers.length === 0)
530
+ return;
531
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.skill.route_conflict', {
532
+ iteration,
533
+ ...(scope.currentSkillId !== undefined && {
534
+ fromSkillId: scope.currentSkillId,
535
+ }),
536
+ winner: {
537
+ ...(state.winner.toolCallId !== undefined && { toolCallId: state.winner.toolCallId }),
538
+ toolName: state.winner.toolName,
539
+ target: state.winner.targetSkillId,
540
+ },
541
+ losers: state.losers.map((l) => ({ ...l })),
542
+ source: 'tool-proposal',
543
+ });
544
+ };
354
545
  /**
355
546
  * Every teardown scope an Agent run can honour (9.7.0).
356
547
  *
@@ -596,6 +787,18 @@ function buildToolCallsHandler(deps) {
596
787
  ...sessionContext(scope, toolName, toolCallId),
597
788
  });
598
789
  await endCall(toolCallId);
790
+ // The typed effects channel (9.19.0) — same unwrap as the batch loop,
791
+ // at this path's own execute boundary, so a resumed call's envelope is
792
+ // judged exactly as an inline one's.
793
+ const envelope = (0, toolEffects_js_1.readToolResultEnvelope)(result);
794
+ if (envelope !== undefined) {
795
+ return { result: envelope.content, executed: true, envelope };
796
+ }
797
+ // A status-only shape missing its `effects: []` marker is DATA (bytes
798
+ // unchanged) — but never silently: name the dropped marker in dev mode.
799
+ const nearMiss = (0, toolEffects_js_1.explainStatusOnlyNearMiss)(result);
800
+ if (nearMiss !== undefined)
801
+ warnEffect(toolName, nearMiss);
599
802
  return { result, executed: true };
600
803
  }
601
804
  catch (err) {
@@ -678,6 +881,10 @@ function buildToolCallsHandler(deps) {
678
881
  // `on-tool-return` triggers and skill-graph routes read the batch.
679
882
  const batchResults = [];
680
883
  scope.toolResults = [];
884
+ // ── The batch's transition bookkeeping (9.19.0) ───────────────────
885
+ // First ACCEPTED `propose-transition` wins (committed at acceptance);
886
+ // later ones to other targets are suppressed + aggregated at batch end.
887
+ const transitionState = { losers: [] };
681
888
  // Capture run identity from scope for the enriched permission ctx.
682
889
  // Same value the Tools slot passes to ToolProvider.list(ctx) so the
683
890
  // checker sees consistent identity across both gates.
@@ -694,6 +901,11 @@ function buildToolCallsHandler(deps) {
694
901
  const startMs = Date.now();
695
902
  let result;
696
903
  let error;
904
+ /** The typed effects channel (9.19.0) — set only when the tool
905
+ * returned a recognized result envelope; everything else keeps
906
+ * today's path byte for byte. */
907
+ let toolEnvelope;
908
+ let toolStatus;
697
909
  // Permission gate — when a checker is configured, evaluate BEFORE
698
910
  // executing the tool. Emits `permission.check` with the decision.
699
911
  //
@@ -1166,6 +1378,26 @@ function buildToolCallsHandler(deps) {
1166
1378
  ...(resolvedCredential && { credential: resolvedCredential }),
1167
1379
  ...sessionContext(scope, tc.name, tc.id),
1168
1380
  });
1381
+ // The typed effects channel (9.19.0): a recognized envelope is
1382
+ // unwrapped HERE, at the one boundary the raw return crosses —
1383
+ // everything downstream (governance rules, the cap, history,
1384
+ // `tool_end`) sees the CONTENT, exactly what a bare return
1385
+ // would have shown. The effects are judged below, after the
1386
+ // gates, in call order.
1387
+ const envelope = (0, toolEffects_js_1.readToolResultEnvelope)(result);
1388
+ if (envelope !== undefined) {
1389
+ toolEnvelope = envelope;
1390
+ result = envelope.content;
1391
+ toolStatus = envelope.status;
1392
+ }
1393
+ else {
1394
+ // A status-only shape missing its `effects: []` marker is
1395
+ // DATA (bytes unchanged) — but never silently: name the
1396
+ // dropped marker in dev mode.
1397
+ const nearMiss = (0, toolEffects_js_1.explainStatusOnlyNearMiss)(result);
1398
+ if (nearMiss !== undefined)
1399
+ warnEffect(tc.name, nearMiss);
1400
+ }
1169
1401
  await endCall(tc.id);
1170
1402
  }
1171
1403
  catch (err) {
@@ -1235,6 +1467,7 @@ function buildToolCallsHandler(deps) {
1235
1467
  allowed,
1236
1468
  iteration,
1237
1469
  });
1470
+ noteSkillRefusal(scope, iteration);
1238
1471
  }
1239
1472
  else if (deps.skillStrictness !== undefined && skillHop) {
1240
1473
  // ── The POSTURE arm (SG-C) — after reachability, hops only. ──
@@ -1256,6 +1489,7 @@ function buildToolCallsHandler(deps) {
1256
1489
  iteration,
1257
1490
  posture: deps.skillStrictness,
1258
1491
  });
1492
+ noteSkillRefusal(scope, iteration);
1259
1493
  }
1260
1494
  }
1261
1495
  }
@@ -1300,6 +1534,9 @@ function buildToolCallsHandler(deps) {
1300
1534
  result: capped.result,
1301
1535
  durationMs,
1302
1536
  ...(error === true && { error: true }),
1537
+ // The tool's own declared outcome (9.19.0) — additive, envelope
1538
+ // tools only.
1539
+ ...(toolStatus !== undefined && { status: toolStatus }),
1303
1540
  });
1304
1541
  let resultStr = typeof modelResult === 'string' ? modelResult : (0, validators_js_1.safeStringify)(modelResult);
1305
1542
  // ── Step boundary (9.18.0): advance decided, then decorate, THEN
@@ -1327,6 +1564,15 @@ function buildToolCallsHandler(deps) {
1327
1564
  });
1328
1565
  }
1329
1566
  }
1567
+ // ── Typed tool effects (9.19.0) — judged in call order ──────────
1568
+ // Only for a call that RAN clean and returned an envelope. Refusal
1569
+ // notes join the model-visible result AFTER the cap (framework
1570
+ // truth, the step-suffix precedent); accepted effects speak through
1571
+ // what they do — the cursor move, the delivered instruction — and
1572
+ // through `tools.effect` on the record.
1573
+ if (toolEnvelope !== undefined && executed && !error && !denied) {
1574
+ resultStr += applyToolEffects(scope, { toolName: tc.name, toolCallId: tc.id, iteration }, toolEnvelope, transitionState);
1575
+ }
1330
1576
  newHistory.push({
1331
1577
  role: 'tool',
1332
1578
  content: resultStr,
@@ -1342,8 +1588,17 @@ function buildToolCallsHandler(deps) {
1342
1588
  // (call order preserved), so an earlier parallel call's
1343
1589
  // routing implication is no longer overwritten by a
1344
1590
  // later sibling's.
1345
- scope.lastToolResult = { toolName: tc.name, result: resultStr };
1346
- batchResults.push({ toolName: tc.name, result: resultStr, toolCallId: tc.id });
1591
+ scope.lastToolResult = {
1592
+ toolName: tc.name,
1593
+ result: resultStr,
1594
+ ...(toolStatus !== undefined && { status: toolStatus }),
1595
+ };
1596
+ batchResults.push({
1597
+ toolName: tc.name,
1598
+ result: resultStr,
1599
+ toolCallId: tc.id,
1600
+ ...(toolStatus !== undefined && { status: toolStatus }),
1601
+ });
1347
1602
  scope.toolResults = [...batchResults];
1348
1603
  // (2) `read_skill` is the auto-attached activation tool.
1349
1604
  // When the LLM calls it with a valid Skill id, append
@@ -1408,6 +1663,10 @@ function buildToolCallsHandler(deps) {
1408
1663
  }
1409
1664
  }
1410
1665
  scope.history = newHistory;
1666
+ // The batch-end aggregate for conflicting transition proposals
1667
+ // (9.19.0) — fires only when ≥2 accepted proposals named different
1668
+ // targets; the per-effect events above already told each story.
1669
+ emitTransitionConflict(scope, iteration, transitionState);
1411
1670
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.agent.iteration_end', {
1412
1671
  turnIndex: 0,
1413
1672
  iterIndex: iteration,
@@ -1464,6 +1723,8 @@ function buildToolCallsHandler(deps) {
1464
1723
  /** The call REALLY ran, clean — the step-advance eligibility this
1465
1724
  * path knows about itself (a decline or a chain-deny never ran). */
1466
1725
  let stepToolRan = false;
1726
+ /** The effects envelope the resumed call returned (9.19.0). */
1727
+ let resumeEnvelope;
1467
1728
  if (!decision.approved) {
1468
1729
  result = decision.note ? `declined by human: ${decision.note}` : 'declined by human';
1469
1730
  (0, ledger_js_1.recordDecisions)(scope, [
@@ -1571,6 +1832,8 @@ function buildToolCallsHandler(deps) {
1571
1832
  result = dispatched.result;
1572
1833
  error = dispatched.error;
1573
1834
  stepToolRan = dispatched.executed === true && error !== true;
1835
+ if (stepToolRan)
1836
+ resumeEnvelope = dispatched.envelope;
1574
1837
  // skip_step behind a middleware ask, approved (9.18.0): the
1575
1838
  // placeholder just landed — replace it with the authoritative
1576
1839
  // sentence BEFORE the chain's last word, the execute loop's
@@ -1610,18 +1873,35 @@ function buildToolCallsHandler(deps) {
1610
1873
  if (deps.stepPlanFor && stepToolRan) {
1611
1874
  askResultStr += applyStepReturn(scope, { toolName, toolCallId, iteration });
1612
1875
  }
1876
+ // Typed tool effects (9.19.0) — a resumed call's envelope is judged
1877
+ // exactly as an inline one's (fresh single-call batch state).
1878
+ if (resumeEnvelope !== undefined) {
1879
+ const askTransition = { losers: [] };
1880
+ askResultStr += applyToolEffects(scope, { toolName, toolCallId, iteration }, resumeEnvelope, askTransition);
1881
+ emitTransitionConflict(scope, iteration, askTransition);
1882
+ }
1613
1883
  const askHistory = [
1614
1884
  ...scope.history,
1615
1885
  { role: 'tool', content: askResultStr, toolCallId, toolName },
1616
1886
  ];
1617
1887
  scope.history = askHistory;
1618
- scope.lastToolResult = { toolName, result: askResultStr };
1619
- appendBatchResult(scope, { toolName, result: askResultStr, toolCallId });
1888
+ scope.lastToolResult = {
1889
+ toolName,
1890
+ result: askResultStr,
1891
+ ...(resumeEnvelope?.status !== undefined && { status: resumeEnvelope.status }),
1892
+ };
1893
+ appendBatchResult(scope, {
1894
+ toolName,
1895
+ result: askResultStr,
1896
+ toolCallId,
1897
+ ...(resumeEnvelope?.status !== undefined && { status: resumeEnvelope.status }),
1898
+ });
1620
1899
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.tool_end', {
1621
1900
  toolCallId,
1622
1901
  result: askCapped.result,
1623
1902
  durationMs: Date.now() - startMs,
1624
1903
  ...(error === true && { error: true }),
1904
+ ...(resumeEnvelope?.status !== undefined && { status: resumeEnvelope.status }),
1625
1905
  });
1626
1906
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.agent.iteration_end', {
1627
1907
  turnIndex: 0,
@@ -1669,6 +1949,8 @@ function buildToolCallsHandler(deps) {
1669
1949
  let error;
1670
1950
  /** Step-advance eligibility, judged by this path (a decline ran nothing). */
1671
1951
  let stepToolRan = false;
1952
+ /** The effects envelope the approved call returned (9.19.0). */
1953
+ let resumeEnvelope;
1672
1954
  if (decision.approved) {
1673
1955
  const env = scope.$getEnv();
1674
1956
  const tool = lookupTool(toolName);
@@ -1676,6 +1958,8 @@ function buildToolCallsHandler(deps) {
1676
1958
  result = dispatched.result;
1677
1959
  error = dispatched.error;
1678
1960
  stepToolRan = dispatched.executed === true && error !== true;
1961
+ if (stepToolRan)
1962
+ resumeEnvelope = dispatched.envelope;
1679
1963
  // Consent moved the execution to this side of the pause; the rules
1680
1964
  // about results move with it.
1681
1965
  if (dispatched.executed === true) {
@@ -1706,19 +1990,36 @@ function buildToolCallsHandler(deps) {
1706
1990
  if (deps.stepPlanFor && stepToolRan) {
1707
1991
  decisionResultStr += applyStepReturn(scope, { toolName, toolCallId, iteration });
1708
1992
  }
1993
+ // Typed tool effects (9.19.0) — an approved consequential call may
1994
+ // carry them too; judged exactly as inline.
1995
+ if (resumeEnvelope !== undefined) {
1996
+ const decisionTransition = { losers: [] };
1997
+ decisionResultStr += applyToolEffects(scope, { toolName, toolCallId, iteration }, resumeEnvelope, decisionTransition);
1998
+ emitTransitionConflict(scope, iteration, decisionTransition);
1999
+ }
1709
2000
  const decisionHistory = [
1710
2001
  ...scope.history,
1711
2002
  { role: 'tool', content: decisionResultStr, toolCallId, toolName },
1712
2003
  ];
1713
2004
  scope.history = decisionHistory;
1714
2005
  // Drives `on-tool-return` triggers, same as the execute path.
1715
- scope.lastToolResult = { toolName, result: decisionResultStr };
1716
- appendBatchResult(scope, { toolName, result: decisionResultStr, toolCallId });
2006
+ scope.lastToolResult = {
2007
+ toolName,
2008
+ result: decisionResultStr,
2009
+ ...(resumeEnvelope?.status !== undefined && { status: resumeEnvelope.status }),
2010
+ };
2011
+ appendBatchResult(scope, {
2012
+ toolName,
2013
+ result: decisionResultStr,
2014
+ toolCallId,
2015
+ ...(resumeEnvelope?.status !== undefined && { status: resumeEnvelope.status }),
2016
+ });
1717
2017
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.tool_end', {
1718
2018
  toolCallId,
1719
2019
  result: decisionCapped.result,
1720
2020
  durationMs: Date.now() - startMs,
1721
2021
  ...(error === true && { error: true }),
2022
+ ...(resumeEnvelope?.status !== undefined && { status: resumeEnvelope.status }),
1722
2023
  });
1723
2024
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.agent.iteration_end', {
1724
2025
  turnIndex: 0,
@@ -1781,18 +2082,35 @@ function buildToolCallsHandler(deps) {
1781
2082
  if (deps.stepPlanFor && dispatched.executed === true && error !== true) {
1782
2083
  consentResultStr += applyStepReturn(scope, { toolName, toolCallId, iteration });
1783
2084
  }
2085
+ // Typed tool effects (9.19.0) — same judge as every other path.
2086
+ const consentEnvelope = dispatched.executed === true && error !== true ? dispatched.envelope : undefined;
2087
+ if (consentEnvelope !== undefined) {
2088
+ const consentTransition = { losers: [] };
2089
+ consentResultStr += applyToolEffects(scope, { toolName, toolCallId, iteration }, consentEnvelope, consentTransition);
2090
+ emitTransitionConflict(scope, iteration, consentTransition);
2091
+ }
1784
2092
  const consentHistory = [
1785
2093
  ...scope.history,
1786
2094
  { role: 'tool', content: consentResultStr, toolCallId, toolName },
1787
2095
  ];
1788
2096
  scope.history = consentHistory;
1789
- scope.lastToolResult = { toolName, result: consentResultStr };
1790
- appendBatchResult(scope, { toolName, result: consentResultStr, toolCallId });
2097
+ scope.lastToolResult = {
2098
+ toolName,
2099
+ result: consentResultStr,
2100
+ ...(consentEnvelope?.status !== undefined && { status: consentEnvelope.status }),
2101
+ };
2102
+ appendBatchResult(scope, {
2103
+ toolName,
2104
+ result: consentResultStr,
2105
+ toolCallId,
2106
+ ...(consentEnvelope?.status !== undefined && { status: consentEnvelope.status }),
2107
+ });
1791
2108
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.tool_end', {
1792
2109
  toolCallId,
1793
2110
  result: consentCapped.result,
1794
2111
  durationMs: Date.now() - startMs,
1795
2112
  ...(error === true && { error: true }),
2113
+ ...(consentEnvelope?.status !== undefined && { status: consentEnvelope.status }),
1796
2114
  });
1797
2115
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.agent.iteration_end', {
1798
2116
  turnIndex: 0,