loopengine 0.1.53 → 0.1.54

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.
@@ -0,0 +1,27 @@
1
+ import type { Message, ModelContentBlock } from './run-agent.js';
2
+ /** Seeds a fresh known-URL map from a session's full message history —
3
+ * every tool_result block's own content, parsed as JSON where possible
4
+ * (plain-text tool results just contribute nothing, not an error). Call
5
+ * once at the top of a run; toollane:result handling in run-agent.ts
6
+ * itself keeps it updated live as new results come in during the run,
7
+ * same map, not a fresh one each turn. */
8
+ export declare function collectKnownUrls(messages: Message[]): Map<string, string>;
9
+ /** Same JSON.parse-then-walk as collectKnownUrls above, for a single
10
+ * fresh tool result (toollane:result's own summary) — called right
11
+ * after each call resolves so a later call in the same run sees it
12
+ * immediately, without waiting for the next collectKnownUrls seed
13
+ * (there isn't one; the same map lives for the whole run). */
14
+ export declare function recordKnownUrlsFromResult(summary: string, knownUrlsByBase: Map<string, string>): void;
15
+ /** Recursively rebuilds a tool_use block's own `input`, snapping any
16
+ * string that looks like a signed URL back to the known-good one for
17
+ * its base whenever they differ. Returns the same reference when
18
+ * nothing needed correcting (the overwhelmingly common case) rather
19
+ * than always cloning, so call sites can skip a write-back when the
20
+ * value is unchanged. */
21
+ export declare function correctKnownUrls<T>(value: T, knownUrlsByBase: Map<string, string>): T;
22
+ /** Applies correctKnownUrls to a tool_use block's own `input` in place —
23
+ * the one call site run-agent.ts actually needs, right before a block's
24
+ * input is used for anything (gate.evaluate, logging, execute alike all
25
+ * need to agree on the same, corrected value). No-ops for a block with
26
+ * no input (a tool that takes no arguments). */
27
+ export declare function correctToolUseInput(block: ModelContentBlock, knownUrlsByBase: Map<string, string>): void;
@@ -0,0 +1,136 @@
1
+ // A model reproducing a long, opaque, high-entropy string (a cloud
2
+ // storage signed URL's own Signature query param can run to ~300 base64
3
+ // characters) in its own generated output is prone to single-character
4
+ // transcription slips — no semantic pattern to anchor on, unlike normal
5
+ // prose. That's harmless when it only breaks a link the model itself
6
+ // writes into its final chat reply (cosmetic — see web/playground.ts's
7
+ // own client-side correctKnownUrls for that case), but the exact same
8
+ // slip can also land inside a *tool call's own arguments* — a model
9
+ // passing an earlier tool's result URL into a later tool call (e.g.
10
+ // handing image URLs from one ability to a zip-archiving ability's own
11
+ // `files` argument) — where it's not cosmetic at all: the later tool
12
+ // actually fetches that broken URL and fails.
13
+ //
14
+ // collectKnownUrls/correctKnownUrls fix that at the one place both
15
+ // failure modes share: run-agent.ts's own tool-dispatch loop, which has
16
+ // already seen every tool result in this session (via `messages`) by
17
+ // the time it's about to hand a new tool_use block's `input` to
18
+ // gate.evaluate()/execute(). A URL a model just wrote into a tool call's
19
+ // arguments gets snapped back to the last known-good one sharing the
20
+ // same base (the object path — descriptive, not random, so far less
21
+ // likely to get mistyped) whenever they differ, the same
22
+ // correct-the-query-not-the-path heuristic web/playground.ts's own copy
23
+ // already uses. Two independent, non-shared implementations by
24
+ // necessity — that one runs in a browser, parsing a chat reply's
25
+ // markdown text; this one runs in Node, walking structured tool_use
26
+ // input — not the same runtime, not the same data shape, nothing real
27
+ // to share between them.
28
+ const URL_PATTERN = /^https?:\/\//;
29
+ /** Recursively walks an arbitrary JSON value collecting every string
30
+ * that looks like a signed URL — has a "?" (nothing to correct in a
31
+ * bare URL with no query to corrupt). Last one seen for a given base
32
+ * wins: a re-signed URL for the same object is just as valid a
33
+ * correction target as the first one seen. */
34
+ function recordKnownUrls(value, knownUrlsByBase) {
35
+ if (typeof value === 'string') {
36
+ if (URL_PATTERN.test(value)) {
37
+ const qIndex = value.indexOf('?');
38
+ if (qIndex !== -1)
39
+ knownUrlsByBase.set(value.slice(0, qIndex), value);
40
+ }
41
+ }
42
+ else if (Array.isArray(value)) {
43
+ for (const item of value)
44
+ recordKnownUrls(item, knownUrlsByBase);
45
+ }
46
+ else if (value && typeof value === 'object') {
47
+ for (const key of Object.keys(value))
48
+ recordKnownUrls(value[key], knownUrlsByBase);
49
+ }
50
+ }
51
+ /** Seeds a fresh known-URL map from a session's full message history —
52
+ * every tool_result block's own content, parsed as JSON where possible
53
+ * (plain-text tool results just contribute nothing, not an error). Call
54
+ * once at the top of a run; toollane:result handling in run-agent.ts
55
+ * itself keeps it updated live as new results come in during the run,
56
+ * same map, not a fresh one each turn. */
57
+ export function collectKnownUrls(messages) {
58
+ const knownUrlsByBase = new Map();
59
+ for (const message of messages) {
60
+ if (!Array.isArray(message.content))
61
+ continue;
62
+ for (const block of message.content) {
63
+ if (block.type !== 'tool_result' || typeof block.content !== 'string')
64
+ continue;
65
+ try {
66
+ recordKnownUrls(JSON.parse(block.content), knownUrlsByBase);
67
+ }
68
+ catch {
69
+ // Not JSON — nothing to correct against.
70
+ }
71
+ }
72
+ }
73
+ return knownUrlsByBase;
74
+ }
75
+ /** Same JSON.parse-then-walk as collectKnownUrls above, for a single
76
+ * fresh tool result (toollane:result's own summary) — called right
77
+ * after each call resolves so a later call in the same run sees it
78
+ * immediately, without waiting for the next collectKnownUrls seed
79
+ * (there isn't one; the same map lives for the whole run). */
80
+ export function recordKnownUrlsFromResult(summary, knownUrlsByBase) {
81
+ try {
82
+ recordKnownUrls(JSON.parse(summary), knownUrlsByBase);
83
+ }
84
+ catch {
85
+ // Not JSON — nothing to correct against.
86
+ }
87
+ }
88
+ /** Recursively rebuilds a tool_use block's own `input`, snapping any
89
+ * string that looks like a signed URL back to the known-good one for
90
+ * its base whenever they differ. Returns the same reference when
91
+ * nothing needed correcting (the overwhelmingly common case) rather
92
+ * than always cloning, so call sites can skip a write-back when the
93
+ * value is unchanged. */
94
+ export function correctKnownUrls(value, knownUrlsByBase) {
95
+ if (typeof value === 'string') {
96
+ if (!URL_PATTERN.test(value))
97
+ return value;
98
+ const qIndex = value.indexOf('?');
99
+ if (qIndex === -1)
100
+ return value;
101
+ const known = knownUrlsByBase.get(value.slice(0, qIndex));
102
+ return (known !== undefined && known !== value ? known : value);
103
+ }
104
+ if (Array.isArray(value)) {
105
+ let changed = false;
106
+ const next = value.map((item) => {
107
+ const corrected = correctKnownUrls(item, knownUrlsByBase);
108
+ if (corrected !== item)
109
+ changed = true;
110
+ return corrected;
111
+ });
112
+ return (changed ? next : value);
113
+ }
114
+ if (value && typeof value === 'object') {
115
+ let changed = false;
116
+ const next = {};
117
+ for (const key of Object.keys(value)) {
118
+ const corrected = correctKnownUrls(value[key], knownUrlsByBase);
119
+ if (corrected !== value[key])
120
+ changed = true;
121
+ next[key] = corrected;
122
+ }
123
+ return (changed ? next : value);
124
+ }
125
+ return value;
126
+ }
127
+ /** Applies correctKnownUrls to a tool_use block's own `input` in place —
128
+ * the one call site run-agent.ts actually needs, right before a block's
129
+ * input is used for anything (gate.evaluate, logging, execute alike all
130
+ * need to agree on the same, corrected value). No-ops for a block with
131
+ * no input (a tool that takes no arguments). */
132
+ export function correctToolUseInput(block, knownUrlsByBase) {
133
+ if (!block.input)
134
+ return;
135
+ block.input = correctKnownUrls(block.input, knownUrlsByBase);
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",
@@ -27,6 +27,8 @@
27
27
  "dist/bin/ability-manager.d.ts",
28
28
  "dist/core/run-agent.js",
29
29
  "dist/core/run-agent.d.ts",
30
+ "dist/core/known-urls.js",
31
+ "dist/core/known-urls.d.ts",
30
32
  "dist/core/loop-events.js",
31
33
  "dist/core/loop-events.d.ts",
32
34
  "dist/core/client.js",