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.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +7 -1
- package/ai-instructions/claude-code/SKILL.md +1 -1
- package/dist/adapters/code/local.js +102 -2
- package/dist/adapters/code/local.js.map +1 -1
- package/dist/adapters/hosting/agentcore.js +11 -0
- package/dist/adapters/hosting/agentcore.js.map +1 -1
- package/dist/adapters/identity/jwks.js +277 -0
- package/dist/adapters/identity/jwks.js.map +1 -0
- package/dist/adapters/types.js +18 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/artifacts/index.js +6 -1
- package/dist/artifacts/index.js.map +1 -1
- package/dist/artifacts/recordingArtifact.js +88 -0
- package/dist/artifacts/recordingArtifact.js.map +1 -0
- package/dist/core/Agent.js +176 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/repeatedCall.js +190 -0
- package/dist/core/agent/repeatedCall.js.map +1 -0
- package/dist/core/agent/stages/toolCalls.js +50 -0
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/codeRunnerTool.js +149 -12
- package/dist/core/codeRunnerTool.js.map +1 -1
- package/dist/esm/adapters/code/local.d.ts +16 -0
- package/dist/esm/adapters/code/local.js +102 -2
- package/dist/esm/adapters/code/local.js.map +1 -1
- package/dist/esm/adapters/hosting/agentcore.js +11 -0
- package/dist/esm/adapters/hosting/agentcore.js.map +1 -1
- package/dist/esm/adapters/identity/jwks.d.ts +124 -0
- package/dist/esm/adapters/identity/jwks.js +249 -0
- package/dist/esm/adapters/identity/jwks.js.map +1 -0
- package/dist/esm/adapters/types.d.ts +86 -0
- package/dist/esm/adapters/types.js +16 -0
- package/dist/esm/adapters/types.js.map +1 -1
- package/dist/esm/artifacts/index.d.ts +1 -0
- package/dist/esm/artifacts/index.js +1 -0
- package/dist/esm/artifacts/index.js.map +1 -1
- package/dist/esm/artifacts/recordingArtifact.d.ts +76 -0
- package/dist/esm/artifacts/recordingArtifact.js +83 -0
- package/dist/esm/artifacts/recordingArtifact.js.map +1 -0
- package/dist/esm/core/Agent.d.ts +66 -2
- package/dist/esm/core/Agent.js +176 -0
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/agent/repeatedCall.d.ts +150 -0
- package/dist/esm/core/agent/repeatedCall.js +184 -0
- package/dist/esm/core/agent/repeatedCall.js.map +1 -0
- package/dist/esm/core/agent/stages/toolCalls.d.ts +12 -0
- package/dist/esm/core/agent/stages/toolCalls.js +50 -0
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/agent/types.d.ts +82 -0
- package/dist/esm/core/codeRunnerTool.d.ts +31 -0
- package/dist/esm/core/codeRunnerTool.js +149 -12
- package/dist/esm/core/codeRunnerTool.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +22 -0
- package/dist/esm/events/registry.d.ts +3 -1
- package/dist/esm/events/registry.js +2 -0
- package/dist/esm/events/registry.js.map +1 -1
- package/dist/esm/hosting/admission.d.ts +180 -0
- package/dist/esm/hosting/admission.js +188 -0
- package/dist/esm/hosting/admission.js.map +1 -0
- package/dist/esm/hosting/artifactWire.d.ts +8 -5
- package/dist/esm/hosting/artifactWire.js +14 -9
- package/dist/esm/hosting/artifactWire.js.map +1 -1
- package/dist/esm/hosting/envelope.d.ts +34 -0
- package/dist/esm/hosting/envelope.js +81 -0
- package/dist/esm/hosting/envelope.js.map +1 -1
- package/dist/esm/hosting/errors.d.ts +131 -0
- package/dist/esm/hosting/errors.js +195 -0
- package/dist/esm/hosting/errors.js.map +1 -1
- package/dist/esm/hosting/httpHost.d.ts +21 -0
- package/dist/esm/hosting/httpHost.js +44 -2
- package/dist/esm/hosting/httpHost.js.map +1 -1
- package/dist/esm/hosting/identityVerification.d.ts +154 -0
- package/dist/esm/hosting/identityVerification.js +135 -0
- package/dist/esm/hosting/identityVerification.js.map +1 -0
- package/dist/esm/hosting/index.d.ts +12 -3
- package/dist/esm/hosting/index.js +16 -2
- package/dist/esm/hosting/index.js.map +1 -1
- package/dist/esm/hosting/memorySessions.d.ts +9 -0
- package/dist/esm/hosting/memorySessions.js +86 -0
- package/dist/esm/hosting/memorySessions.js.map +1 -1
- package/dist/esm/hosting/nodeHost.js +8 -0
- package/dist/esm/hosting/nodeHost.js.map +1 -1
- package/dist/esm/hosting/sessionWire.d.ts +126 -0
- package/dist/esm/hosting/sessionWire.js +95 -0
- package/dist/esm/hosting/sessionWire.js.map +1 -0
- package/dist/esm/hosting/sqliteSessions.js +122 -5
- package/dist/esm/hosting/sqliteSessions.js.map +1 -1
- package/dist/esm/hosting/standingAgent.js +299 -8
- package/dist/esm/hosting/standingAgent.js.map +1 -1
- package/dist/esm/hosting/types.d.ts +166 -6
- package/dist/esm/hosting/types.js.map +1 -1
- package/dist/esm/hosting/wireOps.d.ts +52 -0
- package/dist/esm/hosting/wireOps.js +60 -0
- package/dist/esm/hosting/wireOps.js.map +1 -0
- package/dist/esm/identity.d.ts +1 -0
- package/dist/esm/identity.js +6 -0
- package/dist/esm/identity.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +5 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/events/registry.js +2 -0
- package/dist/events/registry.js.map +1 -1
- package/dist/hosting/admission.js +194 -0
- package/dist/hosting/admission.js.map +1 -0
- package/dist/hosting/artifactWire.js +14 -9
- package/dist/hosting/artifactWire.js.map +1 -1
- package/dist/hosting/envelope.js +84 -1
- package/dist/hosting/envelope.js.map +1 -1
- package/dist/hosting/errors.js +203 -1
- package/dist/hosting/errors.js.map +1 -1
- package/dist/hosting/httpHost.js +43 -1
- package/dist/hosting/httpHost.js.map +1 -1
- package/dist/hosting/identityVerification.js +140 -0
- package/dist/hosting/identityVerification.js.map +1 -0
- package/dist/hosting/index.js +38 -1
- package/dist/hosting/index.js.map +1 -1
- package/dist/hosting/memorySessions.js +86 -0
- package/dist/hosting/memorySessions.js.map +1 -1
- package/dist/hosting/nodeHost.js +8 -0
- package/dist/hosting/nodeHost.js.map +1 -1
- package/dist/hosting/sessionWire.js +100 -0
- package/dist/hosting/sessionWire.js.map +1 -0
- package/dist/hosting/sqliteSessions.js +121 -4
- package/dist/hosting/sqliteSessions.js.map +1 -1
- package/dist/hosting/standingAgent.js +297 -6
- package/dist/hosting/standingAgent.js.map +1 -1
- package/dist/hosting/types.js.map +1 -1
- package/dist/hosting/wireOps.js +65 -0
- package/dist/hosting/wireOps.js.map +1 -0
- package/dist/identity.js +9 -1
- package/dist/identity.js.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/types/adapters/code/local.d.ts +16 -0
- package/dist/types/adapters/code/local.d.ts.map +1 -1
- package/dist/types/adapters/hosting/agentcore.d.ts.map +1 -1
- package/dist/types/adapters/identity/jwks.d.ts +125 -0
- package/dist/types/adapters/identity/jwks.d.ts.map +1 -0
- package/dist/types/adapters/types.d.ts +86 -0
- package/dist/types/adapters/types.d.ts.map +1 -1
- package/dist/types/artifacts/index.d.ts +1 -0
- package/dist/types/artifacts/index.d.ts.map +1 -1
- package/dist/types/artifacts/recordingArtifact.d.ts +77 -0
- package/dist/types/artifacts/recordingArtifact.d.ts.map +1 -0
- package/dist/types/core/Agent.d.ts +66 -2
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/agent/repeatedCall.d.ts +151 -0
- package/dist/types/core/agent/repeatedCall.d.ts.map +1 -0
- package/dist/types/core/agent/stages/toolCalls.d.ts +12 -0
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/agent/types.d.ts +82 -0
- package/dist/types/core/agent/types.d.ts.map +1 -1
- package/dist/types/core/codeRunnerTool.d.ts +31 -0
- package/dist/types/core/codeRunnerTool.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +22 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/events/registry.d.ts +3 -1
- package/dist/types/events/registry.d.ts.map +1 -1
- package/dist/types/hosting/admission.d.ts +181 -0
- package/dist/types/hosting/admission.d.ts.map +1 -0
- package/dist/types/hosting/artifactWire.d.ts +8 -5
- package/dist/types/hosting/artifactWire.d.ts.map +1 -1
- package/dist/types/hosting/envelope.d.ts +34 -0
- package/dist/types/hosting/envelope.d.ts.map +1 -1
- package/dist/types/hosting/errors.d.ts +131 -0
- package/dist/types/hosting/errors.d.ts.map +1 -1
- package/dist/types/hosting/httpHost.d.ts +21 -0
- package/dist/types/hosting/httpHost.d.ts.map +1 -1
- package/dist/types/hosting/identityVerification.d.ts +155 -0
- package/dist/types/hosting/identityVerification.d.ts.map +1 -0
- package/dist/types/hosting/index.d.ts +12 -3
- package/dist/types/hosting/index.d.ts.map +1 -1
- package/dist/types/hosting/memorySessions.d.ts +9 -0
- package/dist/types/hosting/memorySessions.d.ts.map +1 -1
- package/dist/types/hosting/nodeHost.d.ts.map +1 -1
- package/dist/types/hosting/sessionWire.d.ts +127 -0
- package/dist/types/hosting/sessionWire.d.ts.map +1 -0
- package/dist/types/hosting/sqliteSessions.d.ts.map +1 -1
- package/dist/types/hosting/standingAgent.d.ts.map +1 -1
- package/dist/types/hosting/types.d.ts +166 -6
- package/dist/types/hosting/types.d.ts.map +1 -1
- package/dist/types/hosting/wireOps.d.ts +53 -0
- package/dist/types/hosting/wireOps.d.ts.map +1 -0
- package/dist/types/identity.d.ts +1 -0
- package/dist/types/identity.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- 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,
|