agentfootprint 9.25.0 → 9.26.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 (189) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +7 -1
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/code/local.js +102 -2
  5. package/dist/adapters/code/local.js.map +1 -1
  6. package/dist/adapters/hosting/agentcore.js +11 -0
  7. package/dist/adapters/hosting/agentcore.js.map +1 -1
  8. package/dist/adapters/identity/jwks.js +277 -0
  9. package/dist/adapters/identity/jwks.js.map +1 -0
  10. package/dist/adapters/types.js +18 -1
  11. package/dist/adapters/types.js.map +1 -1
  12. package/dist/artifacts/index.js +6 -1
  13. package/dist/artifacts/index.js.map +1 -1
  14. package/dist/artifacts/recordingArtifact.js +88 -0
  15. package/dist/artifacts/recordingArtifact.js.map +1 -0
  16. package/dist/core/Agent.js +176 -0
  17. package/dist/core/Agent.js.map +1 -1
  18. package/dist/core/agent/repeatedCall.js +190 -0
  19. package/dist/core/agent/repeatedCall.js.map +1 -0
  20. package/dist/core/agent/stages/toolCalls.js +50 -0
  21. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  22. package/dist/core/codeRunnerTool.js +149 -12
  23. package/dist/core/codeRunnerTool.js.map +1 -1
  24. package/dist/esm/adapters/code/local.d.ts +16 -0
  25. package/dist/esm/adapters/code/local.js +102 -2
  26. package/dist/esm/adapters/code/local.js.map +1 -1
  27. package/dist/esm/adapters/hosting/agentcore.js +11 -0
  28. package/dist/esm/adapters/hosting/agentcore.js.map +1 -1
  29. package/dist/esm/adapters/identity/jwks.d.ts +124 -0
  30. package/dist/esm/adapters/identity/jwks.js +249 -0
  31. package/dist/esm/adapters/identity/jwks.js.map +1 -0
  32. package/dist/esm/adapters/types.d.ts +86 -0
  33. package/dist/esm/adapters/types.js +16 -0
  34. package/dist/esm/adapters/types.js.map +1 -1
  35. package/dist/esm/artifacts/index.d.ts +1 -0
  36. package/dist/esm/artifacts/index.js +1 -0
  37. package/dist/esm/artifacts/index.js.map +1 -1
  38. package/dist/esm/artifacts/recordingArtifact.d.ts +76 -0
  39. package/dist/esm/artifacts/recordingArtifact.js +83 -0
  40. package/dist/esm/artifacts/recordingArtifact.js.map +1 -0
  41. package/dist/esm/core/Agent.d.ts +66 -2
  42. package/dist/esm/core/Agent.js +176 -0
  43. package/dist/esm/core/Agent.js.map +1 -1
  44. package/dist/esm/core/agent/repeatedCall.d.ts +150 -0
  45. package/dist/esm/core/agent/repeatedCall.js +184 -0
  46. package/dist/esm/core/agent/repeatedCall.js.map +1 -0
  47. package/dist/esm/core/agent/stages/toolCalls.d.ts +12 -0
  48. package/dist/esm/core/agent/stages/toolCalls.js +50 -0
  49. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  50. package/dist/esm/core/agent/types.d.ts +82 -0
  51. package/dist/esm/core/codeRunnerTool.d.ts +31 -0
  52. package/dist/esm/core/codeRunnerTool.js +149 -12
  53. package/dist/esm/core/codeRunnerTool.js.map +1 -1
  54. package/dist/esm/events/payloads.d.ts +22 -0
  55. package/dist/esm/events/registry.d.ts +3 -1
  56. package/dist/esm/events/registry.js +2 -0
  57. package/dist/esm/events/registry.js.map +1 -1
  58. package/dist/esm/hosting/admission.d.ts +180 -0
  59. package/dist/esm/hosting/admission.js +188 -0
  60. package/dist/esm/hosting/admission.js.map +1 -0
  61. package/dist/esm/hosting/artifactWire.d.ts +8 -5
  62. package/dist/esm/hosting/artifactWire.js +14 -9
  63. package/dist/esm/hosting/artifactWire.js.map +1 -1
  64. package/dist/esm/hosting/envelope.d.ts +34 -0
  65. package/dist/esm/hosting/envelope.js +81 -0
  66. package/dist/esm/hosting/envelope.js.map +1 -1
  67. package/dist/esm/hosting/errors.d.ts +131 -0
  68. package/dist/esm/hosting/errors.js +195 -0
  69. package/dist/esm/hosting/errors.js.map +1 -1
  70. package/dist/esm/hosting/httpHost.d.ts +21 -0
  71. package/dist/esm/hosting/httpHost.js +44 -2
  72. package/dist/esm/hosting/httpHost.js.map +1 -1
  73. package/dist/esm/hosting/identityVerification.d.ts +154 -0
  74. package/dist/esm/hosting/identityVerification.js +135 -0
  75. package/dist/esm/hosting/identityVerification.js.map +1 -0
  76. package/dist/esm/hosting/index.d.ts +12 -3
  77. package/dist/esm/hosting/index.js +16 -2
  78. package/dist/esm/hosting/index.js.map +1 -1
  79. package/dist/esm/hosting/memorySessions.d.ts +9 -0
  80. package/dist/esm/hosting/memorySessions.js +86 -0
  81. package/dist/esm/hosting/memorySessions.js.map +1 -1
  82. package/dist/esm/hosting/nodeHost.js +8 -0
  83. package/dist/esm/hosting/nodeHost.js.map +1 -1
  84. package/dist/esm/hosting/sessionWire.d.ts +126 -0
  85. package/dist/esm/hosting/sessionWire.js +95 -0
  86. package/dist/esm/hosting/sessionWire.js.map +1 -0
  87. package/dist/esm/hosting/sqliteSessions.js +122 -5
  88. package/dist/esm/hosting/sqliteSessions.js.map +1 -1
  89. package/dist/esm/hosting/standingAgent.js +299 -8
  90. package/dist/esm/hosting/standingAgent.js.map +1 -1
  91. package/dist/esm/hosting/types.d.ts +166 -6
  92. package/dist/esm/hosting/types.js.map +1 -1
  93. package/dist/esm/hosting/wireOps.d.ts +52 -0
  94. package/dist/esm/hosting/wireOps.js +60 -0
  95. package/dist/esm/hosting/wireOps.js.map +1 -0
  96. package/dist/esm/identity.d.ts +1 -0
  97. package/dist/esm/identity.js +6 -0
  98. package/dist/esm/identity.js.map +1 -1
  99. package/dist/esm/index.d.ts +2 -2
  100. package/dist/esm/index.js +5 -1
  101. package/dist/esm/index.js.map +1 -1
  102. package/dist/events/registry.js +2 -0
  103. package/dist/events/registry.js.map +1 -1
  104. package/dist/hosting/admission.js +194 -0
  105. package/dist/hosting/admission.js.map +1 -0
  106. package/dist/hosting/artifactWire.js +14 -9
  107. package/dist/hosting/artifactWire.js.map +1 -1
  108. package/dist/hosting/envelope.js +84 -1
  109. package/dist/hosting/envelope.js.map +1 -1
  110. package/dist/hosting/errors.js +203 -1
  111. package/dist/hosting/errors.js.map +1 -1
  112. package/dist/hosting/httpHost.js +43 -1
  113. package/dist/hosting/httpHost.js.map +1 -1
  114. package/dist/hosting/identityVerification.js +140 -0
  115. package/dist/hosting/identityVerification.js.map +1 -0
  116. package/dist/hosting/index.js +38 -1
  117. package/dist/hosting/index.js.map +1 -1
  118. package/dist/hosting/memorySessions.js +86 -0
  119. package/dist/hosting/memorySessions.js.map +1 -1
  120. package/dist/hosting/nodeHost.js +8 -0
  121. package/dist/hosting/nodeHost.js.map +1 -1
  122. package/dist/hosting/sessionWire.js +100 -0
  123. package/dist/hosting/sessionWire.js.map +1 -0
  124. package/dist/hosting/sqliteSessions.js +121 -4
  125. package/dist/hosting/sqliteSessions.js.map +1 -1
  126. package/dist/hosting/standingAgent.js +297 -6
  127. package/dist/hosting/standingAgent.js.map +1 -1
  128. package/dist/hosting/types.js.map +1 -1
  129. package/dist/hosting/wireOps.js +65 -0
  130. package/dist/hosting/wireOps.js.map +1 -0
  131. package/dist/identity.js +9 -1
  132. package/dist/identity.js.map +1 -1
  133. package/dist/index.js +9 -2
  134. package/dist/index.js.map +1 -1
  135. package/dist/types/adapters/code/local.d.ts +16 -0
  136. package/dist/types/adapters/code/local.d.ts.map +1 -1
  137. package/dist/types/adapters/hosting/agentcore.d.ts.map +1 -1
  138. package/dist/types/adapters/identity/jwks.d.ts +125 -0
  139. package/dist/types/adapters/identity/jwks.d.ts.map +1 -0
  140. package/dist/types/adapters/types.d.ts +86 -0
  141. package/dist/types/adapters/types.d.ts.map +1 -1
  142. package/dist/types/artifacts/index.d.ts +1 -0
  143. package/dist/types/artifacts/index.d.ts.map +1 -1
  144. package/dist/types/artifacts/recordingArtifact.d.ts +77 -0
  145. package/dist/types/artifacts/recordingArtifact.d.ts.map +1 -0
  146. package/dist/types/core/Agent.d.ts +66 -2
  147. package/dist/types/core/Agent.d.ts.map +1 -1
  148. package/dist/types/core/agent/repeatedCall.d.ts +151 -0
  149. package/dist/types/core/agent/repeatedCall.d.ts.map +1 -0
  150. package/dist/types/core/agent/stages/toolCalls.d.ts +12 -0
  151. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  152. package/dist/types/core/agent/types.d.ts +82 -0
  153. package/dist/types/core/agent/types.d.ts.map +1 -1
  154. package/dist/types/core/codeRunnerTool.d.ts +31 -0
  155. package/dist/types/core/codeRunnerTool.d.ts.map +1 -1
  156. package/dist/types/events/payloads.d.ts +22 -0
  157. package/dist/types/events/payloads.d.ts.map +1 -1
  158. package/dist/types/events/registry.d.ts +3 -1
  159. package/dist/types/events/registry.d.ts.map +1 -1
  160. package/dist/types/hosting/admission.d.ts +181 -0
  161. package/dist/types/hosting/admission.d.ts.map +1 -0
  162. package/dist/types/hosting/artifactWire.d.ts +8 -5
  163. package/dist/types/hosting/artifactWire.d.ts.map +1 -1
  164. package/dist/types/hosting/envelope.d.ts +34 -0
  165. package/dist/types/hosting/envelope.d.ts.map +1 -1
  166. package/dist/types/hosting/errors.d.ts +131 -0
  167. package/dist/types/hosting/errors.d.ts.map +1 -1
  168. package/dist/types/hosting/httpHost.d.ts +21 -0
  169. package/dist/types/hosting/httpHost.d.ts.map +1 -1
  170. package/dist/types/hosting/identityVerification.d.ts +155 -0
  171. package/dist/types/hosting/identityVerification.d.ts.map +1 -0
  172. package/dist/types/hosting/index.d.ts +12 -3
  173. package/dist/types/hosting/index.d.ts.map +1 -1
  174. package/dist/types/hosting/memorySessions.d.ts +9 -0
  175. package/dist/types/hosting/memorySessions.d.ts.map +1 -1
  176. package/dist/types/hosting/nodeHost.d.ts.map +1 -1
  177. package/dist/types/hosting/sessionWire.d.ts +127 -0
  178. package/dist/types/hosting/sessionWire.d.ts.map +1 -0
  179. package/dist/types/hosting/sqliteSessions.d.ts.map +1 -1
  180. package/dist/types/hosting/standingAgent.d.ts.map +1 -1
  181. package/dist/types/hosting/types.d.ts +166 -6
  182. package/dist/types/hosting/types.d.ts.map +1 -1
  183. package/dist/types/hosting/wireOps.d.ts +53 -0
  184. package/dist/types/hosting/wireOps.d.ts.map +1 -0
  185. package/dist/types/identity.d.ts +1 -0
  186. package/dist/types/identity.d.ts.map +1 -1
  187. package/dist/types/index.d.ts +2 -2
  188. package/dist/types/index.d.ts.map +1 -1
  189. package/package.json +6 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * core/agent/repeatedCall — tell a model when it has already asked this.
3
+ *
4
+ * ── The measured failure ────────────────────────────────────────────────────
5
+ * A traced production run: the model called one tool three times in a row with
6
+ * byte-identical arguments and got a byte-identical result each time. The tool
7
+ * was doing its job — the arguments named a filter the backend did not honour,
8
+ * so the same rows came back — and the model read the same rows as a fresh
9
+ * answer each iteration, concluded nothing had changed, and tried again. Three
10
+ * calls, three identical results, one wasted turn, and nothing anywhere in the
11
+ * loop that could say "you have done this".
12
+ *
13
+ * That class of loop is invisible from inside the conversation, because the
14
+ * history genuinely does show three separate calls that each returned data. The
15
+ * only party with the whole picture is the framework, which watched all three
16
+ * land.
17
+ *
18
+ * ── The intervention: a NOTE, never a refusal ───────────────────────────────
19
+ * On the Nth identical (tool, args) → identical result, the framework appends
20
+ * one sentence to that result. It does not block the call, does not change the
21
+ * result, does not error, and does not stop a later identical call from
22
+ * running. That restraint is the design:
23
+ *
24
+ * • **A repeat is sometimes correct.** Polling a job until its status
25
+ * changes is a loop of identical calls returning identical results, on
26
+ * purpose. Refusing it would break a legitimate pattern to fix an
27
+ * illegitimate one, and the model is the only party that knows which it is
28
+ * doing.
29
+ * • **A note is evidence; a refusal is a wall.** The model reads what
30
+ * happened and decides. That is the same posture every teaching refusal in
31
+ * this library takes, minus the refusal.
32
+ *
33
+ * ── Why the result must match too ───────────────────────────────────────────
34
+ * Identical ARGUMENTS alone are not evidence of anything: a "check status" call
35
+ * with the same arguments returning a DIFFERENT status is progress. It is the
36
+ * identical RESULT that makes the repeat pointless, so both halves are
37
+ * required. Which also means the note can say something true and specific —
38
+ * *calling again will not change it* — rather than a vague "you seem to be
39
+ * repeating yourself".
40
+ *
41
+ * ── What is stored, and what is deliberately not ────────────────────────────
42
+ * A short non-cryptographic FINGERPRINT of the arguments and the result, never
43
+ * the values. Tool arguments routinely carry the things redaction exists for,
44
+ * and a fingerprint answers the only question this feature asks ("is this the
45
+ * same?") and answers nothing else.
46
+ *
47
+ * ── And WHERE it is stored: beside the run, never inside its state ──────────
48
+ * The counters live in {@link repeatedCallLedgers}, a run-keyed map the
49
+ * dispatch loop holds — NOT on tracked scope. That is not a tidiness
50
+ * preference, it is the zero-cost law:
51
+ *
52
+ * • Tracked scope IS the commit log, the snapshot, the narrative, every
53
+ * recording and every causal slice. A key written there on every tool
54
+ * landing would appear in all of them for every existing agent the moment
55
+ * it upgraded — a turn that never repeated anything would no longer be
56
+ * byte-identical to the release before, and a consumer asserting a state-key
57
+ * set would break for a feature it never asked for.
58
+ * • A within-turn counter is not conversation state. Nothing resumes from it,
59
+ * nothing branches on it later, and no reader of a trace needs it: the
60
+ * repeat itself lands on the record as `agentfootprint.tools.repeated_call`,
61
+ * which is the telemetry channel this library puts per-attempt facts on
62
+ * (the same reasoning as retries and streaming tokens, and the same
63
+ * reasoning that keeps a consent record off tracked state).
64
+ *
65
+ * So an agent that never repeats a call is byte-identical to 9.25 in behaviour,
66
+ * in state AND in events — the ledger exists only in memory, only for the
67
+ * duration of a run, and only where a tool actually landed.
68
+ */
69
+ /** How many identical (tool, args, result) landings trigger the note. The
70
+ * SECOND one: the first repeat is the first moment the fact exists. */
71
+ export declare const REPEATED_CALL_THRESHOLD = 2;
72
+ /**
73
+ * The ledger: fingerprint → how many times this exact (tool, args, result) has
74
+ * landed in this run.
75
+ *
76
+ * A plain record of small strings to numbers — a few dozen bytes per distinct
77
+ * call, and nothing in it that could not be printed in a bug report.
78
+ */
79
+ export type RepeatedCallLedger = Readonly<Record<string, number>>;
80
+ /**
81
+ * Where the ledgers live: one per RUN, held beside the dispatch loop.
82
+ *
83
+ * ── Why the run and not the conversation ────────────────────────────────────
84
+ * The failure this counts is a model calling the same thing twice inside ONE
85
+ * answer. A counter that spanned turns would tell somebody who asked the same
86
+ * question again tomorrow that they had already asked — which is true, and not
87
+ * what the note says. `runId` is the exact grain, and it is already the key
88
+ * every event on this run is stamped with.
89
+ *
90
+ * A resume mints a fresh `runId`, so a run that was paused for a person and
91
+ * continued starts counting again. That is the honest reading of "this turn":
92
+ * the framework watched half of it.
93
+ *
94
+ * ── Why BOUNDED ─────────────────────────────────────────────────────────────
95
+ * One agent instance serves many runs, and nothing tells this map when a run
96
+ * ended — a run can also end by throwing, by pausing forever, or by the process
97
+ * losing interest. So it keeps the most recent few and drops the rest. The cost
98
+ * of dropping one is exactly one note that is never given; it can never produce
99
+ * a note for the wrong run, because a run only ever reads counters filed under
100
+ * its own id.
101
+ */
102
+ export interface RepeatedCallLedgers {
103
+ /** This run's counters, or `undefined` before its first tool landed. */
104
+ read(runId: string): RepeatedCallLedger | undefined;
105
+ /** File this run's counters, evicting the least recently written run when
106
+ * the map is full. */
107
+ write(runId: string, ledger: RepeatedCallLedger): void;
108
+ }
109
+ /** Build the run-keyed holder. One per agent — the chart is built once, so the
110
+ * loop that owns this is built once too. */
111
+ export declare function repeatedCallLedgers(maxRuns?: number): RepeatedCallLedgers;
112
+ /**
113
+ * The two fingerprints for one landing, and the ledger key that joins them.
114
+ *
115
+ * Kept apart on the way out so a debugger reading the event can tell "same
116
+ * call, different answer" from "same call, same answer" — without either value
117
+ * being present anywhere.
118
+ */
119
+ export declare function repeatedCallKey(toolName: string, args: unknown, result: string): {
120
+ readonly key: string;
121
+ readonly argsFingerprint: string;
122
+ readonly resultFingerprint: string;
123
+ };
124
+ /** What one landing came to. `occurrences` counts THIS one. */
125
+ export interface RepeatedCallOutcome {
126
+ /** The ledger to write back to scope. */
127
+ readonly ledger: RepeatedCallLedger;
128
+ /** How many times this exact call+result has now landed this run. */
129
+ readonly occurrences: number;
130
+ /** The sentence to append, or `undefined` when this landing is not a
131
+ * repeat (or the note has already been given for it). */
132
+ readonly note?: string;
133
+ /** Digest of the arguments — never the arguments. Rides the typed event. */
134
+ readonly argsFingerprint: string;
135
+ /** Digest of the result — never the result. */
136
+ readonly resultFingerprint: string;
137
+ }
138
+ /**
139
+ * Record one landing and decide whether it earns the note.
140
+ *
141
+ * PURE — takes the ledger, returns the next one. The dispatch loop owns reading
142
+ * and writing scope, so this file never imports the agent and can be tested as
143
+ * a table.
144
+ *
145
+ * The note fires ONCE per distinct call, on the Nth landing exactly. A third
146
+ * and fourth identical call add nothing further: the model has been told, and
147
+ * repeating the lesson every iteration would be the framework doing the very
148
+ * thing it is complaining about.
149
+ */
150
+ export declare function noteRepeatedCall(ledger: RepeatedCallLedger | undefined, toolName: string, args: unknown, result: string): RepeatedCallOutcome;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * core/agent/repeatedCall — tell a model when it has already asked this.
3
+ *
4
+ * ── The measured failure ────────────────────────────────────────────────────
5
+ * A traced production run: the model called one tool three times in a row with
6
+ * byte-identical arguments and got a byte-identical result each time. The tool
7
+ * was doing its job — the arguments named a filter the backend did not honour,
8
+ * so the same rows came back — and the model read the same rows as a fresh
9
+ * answer each iteration, concluded nothing had changed, and tried again. Three
10
+ * calls, three identical results, one wasted turn, and nothing anywhere in the
11
+ * loop that could say "you have done this".
12
+ *
13
+ * That class of loop is invisible from inside the conversation, because the
14
+ * history genuinely does show three separate calls that each returned data. The
15
+ * only party with the whole picture is the framework, which watched all three
16
+ * land.
17
+ *
18
+ * ── The intervention: a NOTE, never a refusal ───────────────────────────────
19
+ * On the Nth identical (tool, args) → identical result, the framework appends
20
+ * one sentence to that result. It does not block the call, does not change the
21
+ * result, does not error, and does not stop a later identical call from
22
+ * running. That restraint is the design:
23
+ *
24
+ * • **A repeat is sometimes correct.** Polling a job until its status
25
+ * changes is a loop of identical calls returning identical results, on
26
+ * purpose. Refusing it would break a legitimate pattern to fix an
27
+ * illegitimate one, and the model is the only party that knows which it is
28
+ * doing.
29
+ * • **A note is evidence; a refusal is a wall.** The model reads what
30
+ * happened and decides. That is the same posture every teaching refusal in
31
+ * this library takes, minus the refusal.
32
+ *
33
+ * ── Why the result must match too ───────────────────────────────────────────
34
+ * Identical ARGUMENTS alone are not evidence of anything: a "check status" call
35
+ * with the same arguments returning a DIFFERENT status is progress. It is the
36
+ * identical RESULT that makes the repeat pointless, so both halves are
37
+ * required. Which also means the note can say something true and specific —
38
+ * *calling again will not change it* — rather than a vague "you seem to be
39
+ * repeating yourself".
40
+ *
41
+ * ── What is stored, and what is deliberately not ────────────────────────────
42
+ * A short non-cryptographic FINGERPRINT of the arguments and the result, never
43
+ * the values. Tool arguments routinely carry the things redaction exists for,
44
+ * and a fingerprint answers the only question this feature asks ("is this the
45
+ * same?") and answers nothing else.
46
+ *
47
+ * ── And WHERE it is stored: beside the run, never inside its state ──────────
48
+ * The counters live in {@link repeatedCallLedgers}, a run-keyed map the
49
+ * dispatch loop holds — NOT on tracked scope. That is not a tidiness
50
+ * preference, it is the zero-cost law:
51
+ *
52
+ * • Tracked scope IS the commit log, the snapshot, the narrative, every
53
+ * recording and every causal slice. A key written there on every tool
54
+ * landing would appear in all of them for every existing agent the moment
55
+ * it upgraded — a turn that never repeated anything would no longer be
56
+ * byte-identical to the release before, and a consumer asserting a state-key
57
+ * set would break for a feature it never asked for.
58
+ * • A within-turn counter is not conversation state. Nothing resumes from it,
59
+ * nothing branches on it later, and no reader of a trace needs it: the
60
+ * repeat itself lands on the record as `agentfootprint.tools.repeated_call`,
61
+ * which is the telemetry channel this library puts per-attempt facts on
62
+ * (the same reasoning as retries and streaming tokens, and the same
63
+ * reasoning that keeps a consent record off tracked state).
64
+ *
65
+ * So an agent that never repeats a call is byte-identical to 9.25 in behaviour,
66
+ * in state AND in events — the ledger exists only in memory, only for the
67
+ * duration of a run, and only where a tool actually landed.
68
+ */
69
+ /** How many identical (tool, args, result) landings trigger the note. The
70
+ * SECOND one: the first repeat is the first moment the fact exists. */
71
+ export const REPEATED_CALL_THRESHOLD = 2;
72
+ /** How many runs' counters one dispatch loop keeps at once. Small on purpose:
73
+ * concurrent runs on ONE agent instance are the only reason for more than
74
+ * one, and a deployment serving many callers at once gives each its own
75
+ * agent (that is what `agentFactory` is for). */
76
+ const LEDGER_RUNS = 8;
77
+ /** Build the run-keyed holder. One per agent — the chart is built once, so the
78
+ * loop that owns this is built once too. */
79
+ export function repeatedCallLedgers(maxRuns = LEDGER_RUNS) {
80
+ const byRun = new Map();
81
+ return {
82
+ read: (runId) => byRun.get(runId),
83
+ write: (runId, ledger) => {
84
+ // Delete-then-set makes insertion order recency order, which is what the
85
+ // eviction below reads.
86
+ byRun.delete(runId);
87
+ byRun.set(runId, ledger);
88
+ while (byRun.size > maxRuns) {
89
+ const oldest = byRun.keys().next();
90
+ if (oldest.done === true)
91
+ break;
92
+ byRun.delete(oldest.value);
93
+ }
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ * A stable, order-insensitive fingerprint of one call.
99
+ *
100
+ * Object keys are SORTED before hashing, at every depth: `{ a: 1, b: 2 }` and
101
+ * `{ b: 2, a: 1 }` are the same call, and a model that happens to emit its JSON
102
+ * in a different key order twice is repeating itself just as surely. Arrays
103
+ * keep their order, because `[1, 2]` and `[2, 1]` are not the same argument.
104
+ */
105
+ function stableString(value) {
106
+ if (value === null || typeof value !== 'object')
107
+ return JSON.stringify(value) ?? 'undefined';
108
+ if (Array.isArray(value))
109
+ return `[${value.map(stableString).join(',')}]`;
110
+ const entries = Object.entries(value)
111
+ .filter(([, v]) => v !== undefined)
112
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
113
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableString(v)}`).join(',')}}`;
114
+ }
115
+ /**
116
+ * FNV-1a, 32-bit, as 8 hex characters.
117
+ *
118
+ * Non-cryptographic ON PURPOSE and stated as such: this compares a string to
119
+ * ITSELF a few iterations later inside one run, where an adversary has nothing
120
+ * to gain from a collision and the cost of `crypto.subtle` (async, and one
121
+ * digest per tool result) buys nothing. A collision produces one unnecessary
122
+ * teaching note, which is the cheapest possible failure this could have.
123
+ */
124
+ function fingerprint(text) {
125
+ let hash = 0x811c9dc5;
126
+ for (let i = 0; i < text.length; i += 1) {
127
+ hash ^= text.charCodeAt(i);
128
+ hash = Math.imul(hash, 0x01000193) >>> 0;
129
+ }
130
+ return hash.toString(16).padStart(8, '0');
131
+ }
132
+ /**
133
+ * The two fingerprints for one landing, and the ledger key that joins them.
134
+ *
135
+ * Kept apart on the way out so a debugger reading the event can tell "same
136
+ * call, different answer" from "same call, same answer" — without either value
137
+ * being present anywhere.
138
+ */
139
+ export function repeatedCallKey(toolName, args, result) {
140
+ const argsFingerprint = fingerprint(stableString(args));
141
+ const resultFingerprint = fingerprint(result);
142
+ return {
143
+ key: `${toolName}:${argsFingerprint}:${resultFingerprint}`,
144
+ argsFingerprint,
145
+ resultFingerprint,
146
+ };
147
+ }
148
+ /**
149
+ * The teaching sentence.
150
+ *
151
+ * It says three things, in the order the model needs them: what happened (the
152
+ * fact it cannot see), what follows from it (calling again is not going to
153
+ * help), and what to do instead (two named options, neither of which is "stop"
154
+ * — a model told only to stop tends to stop answering).
155
+ */
156
+ function noteFor(toolName, occurrences) {
157
+ return (`\n\n[identical call: '${toolName}' has now returned exactly this result ${occurrences} ` +
158
+ `times this turn, for exactly these arguments. Calling it again will not change it — ` +
159
+ `act on what you have, or change the arguments (a different filter, a wider range, a ` +
160
+ `different tool). If you are waiting for something to change, say so in your answer ` +
161
+ `rather than calling again.]`);
162
+ }
163
+ /**
164
+ * Record one landing and decide whether it earns the note.
165
+ *
166
+ * PURE — takes the ledger, returns the next one. The dispatch loop owns reading
167
+ * and writing scope, so this file never imports the agent and can be tested as
168
+ * a table.
169
+ *
170
+ * The note fires ONCE per distinct call, on the Nth landing exactly. A third
171
+ * and fourth identical call add nothing further: the model has been told, and
172
+ * repeating the lesson every iteration would be the framework doing the very
173
+ * thing it is complaining about.
174
+ */
175
+ export function noteRepeatedCall(ledger, toolName, args, result) {
176
+ const { key, argsFingerprint, resultFingerprint } = repeatedCallKey(toolName, args, result);
177
+ const occurrences = (ledger?.[key] ?? 0) + 1;
178
+ const next = { ...(ledger ?? {}), [key]: occurrences };
179
+ const outcome = { ledger: next, occurrences, argsFingerprint, resultFingerprint };
180
+ if (occurrences !== REPEATED_CALL_THRESHOLD)
181
+ return outcome;
182
+ return { ...outcome, note: noteFor(toolName, occurrences) };
183
+ }
184
+ //# sourceMappingURL=repeatedCall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repeatedCall.js","sourceRoot":"","sources":["../../../../src/core/agent/repeatedCall.ts"],"names":[],"mappings":"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"}
@@ -118,6 +118,18 @@ export interface ToolCallsHandlerDeps {
118
118
  * `../toolResultCap.ts` for the shape and why it is opt-in.
119
119
  */
120
120
  readonly maxToolResultChars?: number;
121
+ /**
122
+ * Tell the model when it has already made this exact call and already got
123
+ * this exact answer (9.26.0). Threaded from `AgentOptions.repeatedCallNudge`,
124
+ * and only when the operator turned it OFF — `undefined` here means the
125
+ * default, which is on.
126
+ *
127
+ * Applied at the BATCH dispatch loop only, which is where a turn's repeats
128
+ * actually happen. The pause-resume paths deliver a call a PERSON answered
129
+ * or approved, and a note telling the model it has already done what a human
130
+ * just authorised would be the framework arguing with the human.
131
+ */
132
+ readonly repeatedCallNudge?: boolean;
121
133
  /**
122
134
  * Skill-graph read_skill GATE (`graph.reachableSkills`). When present, a
123
135
  * `read_skill('id')` whose `id` is not reachable from the current cursor is
@@ -59,6 +59,7 @@ import { assertAskComponent, InvalidAskComponentError, } from '../../askComponen
59
59
  import { shouldCheckIn, isCheckInDecision, checkInDeclined, } from '../../checkin.js';
60
60
  import { runToolChain, runToolAfterChain } from '../middleware/runChain.js';
61
61
  import { recordDecisions } from '../middleware/ledger.js';
62
+ import { noteRepeatedCall, repeatedCallLedgers } from '../repeatedCall.js';
62
63
  import { formatToolArgIssues, validateToolArgs, } from '../toolArgsValidation.js';
63
64
  import { safeStringify } from '../validators.js';
64
65
  import { capToolResult } from '../toolResultCap.js';
@@ -219,6 +220,17 @@ function argsForPausedCall(scope, toolCallId) {
219
220
  function appendBatchResult(scope, entry) {
220
221
  scope.toolResults = [...(scope.toolResults ?? []), entry];
221
222
  }
223
+ /**
224
+ * The ledger key for a handler built with no `currentRun` accessor — a
225
+ * hand-composed dispatch loop in a test, never an Agent (which always wires
226
+ * it).
227
+ *
228
+ * One shared bucket, stated rather than hidden: without a run id there is
229
+ * nothing to tell two runs apart by, and the honest consequence is that such a
230
+ * loop counts across them. It cannot leak anything — the ledger holds
231
+ * fingerprints — and the only visible effect is a note arriving one call early.
232
+ */
233
+ const UNSCOPED_RUN = '#no-run-id';
222
234
  export function buildToolCallsHandler(deps) {
223
235
  const { registryByName, externalToolProvider, providerToolCache, permissionChecker } = deps;
224
236
  const toolArgValidation = deps.toolArgValidation ?? 'enforce';
@@ -229,6 +241,10 @@ export function buildToolCallsHandler(deps) {
229
241
  // that THROWS on use (never undefined) — so a tool can't silently no-op.
230
242
  const credentials = deps.credentialProvider ?? unconfiguredCredentialProvider();
231
243
  const hasCredentials = deps.credentialProvider !== undefined;
244
+ // The repeated-call counters (9.26.0), run-keyed and OFF tracked scope — an
245
+ // empty Map here, and not one byte written anywhere a reader can see until a
246
+ // tool actually lands. See `../repeatedCall.ts` for why they are not state.
247
+ const repeatLedgers = repeatedCallLedgers();
232
248
  /**
233
249
  * The tool-result ceiling, applied at the ONE place a result leaves dispatch.
234
250
  *
@@ -2032,6 +2048,11 @@ export function buildToolCallsHandler(deps) {
2032
2048
  ...(toolStatus !== undefined && { status: toolStatus }),
2033
2049
  });
2034
2050
  let resultStr = typeof modelResult === 'string' ? modelResult : safeStringify(modelResult);
2051
+ // The tool's OWN answer, before any framework suffix joins it — what
2052
+ // the repeated-call ledger fingerprints. Fingerprinting the decorated
2053
+ // string instead would compare our own additions (a step that advanced
2054
+ // once, an effect note) and miss the repeat they decorate.
2055
+ const deliveredResult = resultStr;
2035
2056
  // ── Step boundary (9.18.0): advance decided, then decorate, THEN
2036
2057
  // push — the suffix must be part of the one past every reader sees
2037
2058
  // (history, `lastToolResult`, the batch), never spliced in later.
@@ -2068,6 +2089,35 @@ export function buildToolCallsHandler(deps) {
2068
2089
  if (toolEnvelope !== undefined && executed && !error && !denied) {
2069
2090
  resultStr += applyToolEffects(scope, { toolName: tc.name, toolCallId: tc.id, iteration }, toolEnvelope, transitionState);
2070
2091
  }
2092
+ // ── Repeated-call nudge (9.26.0) ───────────────────────────────
2093
+ // The framework is the only party that watched all three landings, so
2094
+ // it is the only one that can say so. A NOTE on the result — the call
2095
+ // ran, nothing was refused, and a later identical call is not blocked.
2096
+ // Only for a call that RAN: a permission denial, a gate rejection and
2097
+ // a ceiling refusal each already teach their own lesson, and stacking
2098
+ // a second one would bury it.
2099
+ if (deps.repeatedCallNudge !== false && executed && !denied && !skillRejected) {
2100
+ // Counted beside the run, never inside its state: a tracked write is
2101
+ // a commit-log entry, a snapshot key, a narrative line and a row in
2102
+ // every recording, and a turn that repeats NOTHING must stay
2103
+ // byte-identical to the release before this one. `currentRun()` is
2104
+ // the same accessor `ctx.runId` is composed from — one answer to
2105
+ // "which run is this", not a second spelling of it.
2106
+ const runKey = deps.currentRun?.().runId ?? UNSCOPED_RUN;
2107
+ const repeat = noteRepeatedCall(repeatLedgers.read(runKey), tc.name, callArgs, deliveredResult);
2108
+ repeatLedgers.write(runKey, repeat.ledger);
2109
+ if (repeat.note !== undefined) {
2110
+ resultStr += repeat.note;
2111
+ typedEmit(scope, 'agentfootprint.tools.repeated_call', {
2112
+ toolName: tc.name,
2113
+ toolCallId: tc.id,
2114
+ iteration,
2115
+ occurrences: repeat.occurrences,
2116
+ argsFingerprint: repeat.argsFingerprint,
2117
+ resultFingerprint: repeat.resultFingerprint,
2118
+ });
2119
+ }
2120
+ }
2071
2121
  newHistory.push({
2072
2122
  role: 'tool',
2073
2123
  content: resultStr,