loopengine 0.1.51 → 0.1.53
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/dist/core/run-agent.js +18 -0
- package/dist/web/playground.js +74 -1
- package/package.json +1 -1
package/dist/core/run-agent.js
CHANGED
|
@@ -13,6 +13,7 @@ import { BudgetTracker } from './budget.js';
|
|
|
13
13
|
import { Compactor } from './compaction.js';
|
|
14
14
|
import { ToolLane } from './toollane.js';
|
|
15
15
|
import { Recovery } from './recovery.js';
|
|
16
|
+
import { collectKnownUrls, correctToolUseInput, recordKnownUrlsFromResult } from './known-urls.js';
|
|
16
17
|
import { loadAgentModule } from './discover-agents.js';
|
|
17
18
|
import { agentAsTool } from './agent-as-tool.js';
|
|
18
19
|
import { loadGatewayToolsFromDir } from './gateway-tools.js';
|
|
@@ -458,6 +459,12 @@ async function runLoop(ctx, messages, newMessages, starterMessage) {
|
|
|
458
459
|
messages.push(message);
|
|
459
460
|
newMessages.push(message);
|
|
460
461
|
}
|
|
462
|
+
// See known-urls.ts's own doc comment — seeded once from this
|
|
463
|
+
// session's full prior history (a resumed session's own tool results,
|
|
464
|
+
// not just this run's), then kept current live as this run's own new
|
|
465
|
+
// toollane:result events come in below. One map for the whole run, not
|
|
466
|
+
// re-derived every turn.
|
|
467
|
+
const knownUrlsByBase = collectKnownUrls(messages);
|
|
461
468
|
const recovery = new Recovery({
|
|
462
469
|
onPromptTooLong: async (currentMessages) => {
|
|
463
470
|
// newMessages (this turn's own content — not durably stored
|
|
@@ -566,6 +573,12 @@ async function runLoop(ctx, messages, newMessages, starterMessage) {
|
|
|
566
573
|
// changing toollane's own types.
|
|
567
574
|
const approvedMetaById = new Map();
|
|
568
575
|
for (const block of toolUseBlocks) {
|
|
576
|
+
// Before anything else looks at this block's own input at all
|
|
577
|
+
// (Skill's own args included) — see known-urls.ts's own doc
|
|
578
|
+
// comment: a URL the model just copied from an earlier tool
|
|
579
|
+
// result into this call's own arguments gets snapped back to the
|
|
580
|
+
// known-good one sharing its base, if they differ.
|
|
581
|
+
correctToolUseInput(block, knownUrlsByBase);
|
|
569
582
|
// Skill invocation injects instructions into context; it isn't a
|
|
570
583
|
// real tool call and doesn't need ActAuth gating or ToolLane
|
|
571
584
|
// scheduling — but it's still a tool_use block the model emitted,
|
|
@@ -737,6 +750,11 @@ async function runLoop(ctx, messages, newMessages, starterMessage) {
|
|
|
737
750
|
const summary = result.status === 'fulfilled' ? JSON.stringify(result.value) : `ERROR: ${result.error}`;
|
|
738
751
|
const meta = approvedMetaById.get(result.id);
|
|
739
752
|
log({ type: 'toollane:result', name: result.name, summary });
|
|
753
|
+
// A later tool call this same run (or a follow-up message in
|
|
754
|
+
// this same session) can reference a URL from this result —
|
|
755
|
+
// see known-urls.ts's own doc comment. Harmless no-op for a
|
|
756
|
+
// non-JSON/error summary.
|
|
757
|
+
recordKnownUrlsFromResult(summary, knownUrlsByBase);
|
|
740
758
|
resultBlocks.push({
|
|
741
759
|
type: 'tool_result',
|
|
742
760
|
tool_use_id: result.id,
|
package/dist/web/playground.js
CHANGED
|
@@ -412,6 +412,42 @@ export const playgroundHtml = `<!doctype html>
|
|
|
412
412
|
// target for one that happens to reuse an id namespace.
|
|
413
413
|
var toolCallCardsById = {};
|
|
414
414
|
|
|
415
|
+
// Keyed by a signed URL's own base (everything before its "?") —
|
|
416
|
+
// populated from every 'toollane:result' event's own 'summary' (see
|
|
417
|
+
// core/loop-events.ts's own ToolLaneResultEvent doc comment: the raw,
|
|
418
|
+
// unparsed tool return value, straight from execute() with nothing
|
|
419
|
+
// reshaped in between). A long signed-URL query string (a GCS
|
|
420
|
+
// Signature param can run ~300 opaque base64 characters) is exactly
|
|
421
|
+
// the kind of thing an LLM can flip one character of while generating
|
|
422
|
+
// its own reply text from the same value earlier in its context — see
|
|
423
|
+
// correctKnownUrls below, which snaps a written URL back to this
|
|
424
|
+
// known-good one whenever the base (the object path — descriptive,
|
|
425
|
+
// not random, so far less likely to get mistyped) matches but the
|
|
426
|
+
// full string doesn't. Cleared on every conversation switch, same as
|
|
427
|
+
// toolCallCardsById above — a base from an unrelated earlier session
|
|
428
|
+
// is never a valid correction target (and its signed URL has likely
|
|
429
|
+
// expired anyway).
|
|
430
|
+
var knownUrlsByBase = {};
|
|
431
|
+
|
|
432
|
+
// Recursively walks an arbitrary JSON value (a tool's own summary,
|
|
433
|
+
// parsed) collecting every string that looks like a signed URL — has
|
|
434
|
+
// a "?" (nothing to correct in a bare URL with no query to corrupt).
|
|
435
|
+
// Last one seen for a given base wins, which is fine: a re-signed URL
|
|
436
|
+
// for the same object is just as valid a correction target as the
|
|
437
|
+
// first one seen.
|
|
438
|
+
function recordKnownUrls(value) {
|
|
439
|
+
if (typeof value === 'string') {
|
|
440
|
+
if (value.indexOf('https://') === 0 || value.indexOf('http://') === 0) {
|
|
441
|
+
var qIndex = value.indexOf('?');
|
|
442
|
+
if (qIndex !== -1) knownUrlsByBase[value.slice(0, qIndex)] = value;
|
|
443
|
+
}
|
|
444
|
+
} else if (Array.isArray(value)) {
|
|
445
|
+
for (var i = 0; i < value.length; i++) recordKnownUrls(value[i]);
|
|
446
|
+
} else if (value && typeof value === 'object') {
|
|
447
|
+
for (var key in value) recordKnownUrls(value[key]);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
415
451
|
// A page refresh only ever loses *this tab's* JS state — the actual
|
|
416
452
|
// conversation is durably stored server-side (see session-store.ts).
|
|
417
453
|
// What's missing after a refresh is just "which session ids did I
|
|
@@ -463,8 +499,30 @@ export const playgroundHtml = `<!doctype html>
|
|
|
463
499
|
// attachment — a real HTTP response header, so a plain click already
|
|
464
500
|
// downloads correctly regardless of origin, no JS fetch/blob trick or
|
|
465
501
|
// same-origin requirement needed.
|
|
502
|
+
//
|
|
503
|
+
// Before any of that: correctKnownUrls (see knownUrlsByBase's own doc
|
|
504
|
+
// comment) snaps any signed URL the model wrote back to the exact
|
|
505
|
+
// known-good one whenever they share a base but differ — recovers a
|
|
506
|
+
// broken image/download link from a transcription slip without
|
|
507
|
+
// needing the model to get a ~300-character signature exactly right.
|
|
508
|
+
//
|
|
509
|
+
// Built from a runtime backslash character, never a literal
|
|
510
|
+
// backslash-letter sequence in this file's own source — same gotcha
|
|
511
|
+
// renderMessageMarkdown's own patterns below document in full (this
|
|
512
|
+
// page is one big outer template literal whose own output is itself
|
|
513
|
+
// parsed as JS a second time).
|
|
514
|
+
var urlBs = String.fromCharCode(92);
|
|
515
|
+
var urlPattern = new RegExp('https?:' + urlBs + '/' + urlBs + '/[^' + urlBs + 's)' + urlBs + ']]+', 'g');
|
|
516
|
+
function correctKnownUrls(md) {
|
|
517
|
+
return md.replace(urlPattern, function (url) {
|
|
518
|
+
var qIndex = url.indexOf('?');
|
|
519
|
+
if (qIndex === -1) return url;
|
|
520
|
+
var known = knownUrlsByBase[url.slice(0, qIndex)];
|
|
521
|
+
return known || url;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
466
524
|
function renderMessageMarkdown(md) {
|
|
467
|
-
var lines = escapeHtml(md).split('\\n');
|
|
525
|
+
var lines = escapeHtml(correctKnownUrls(md)).split('\\n');
|
|
468
526
|
var html = '';
|
|
469
527
|
var inCode = false;
|
|
470
528
|
var codeBuffer = '';
|
|
@@ -1184,6 +1242,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
1184
1242
|
sessionLabel.classList.add('copyable');
|
|
1185
1243
|
chatPane.textContent = '';
|
|
1186
1244
|
toolCallCardsById = {};
|
|
1245
|
+
knownUrlsByBase = {};
|
|
1187
1246
|
setEmptyHint(timelinePane, 'Loop events (tool calls, permission checks, budget checks) will appear here as the agent runs.');
|
|
1188
1247
|
renderSessionsPane();
|
|
1189
1248
|
|
|
@@ -1334,6 +1393,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
1334
1393
|
function resetConversation() {
|
|
1335
1394
|
sessionId = null;
|
|
1336
1395
|
toolCallCardsById = {};
|
|
1396
|
+
knownUrlsByBase = {};
|
|
1337
1397
|
removeThinking();
|
|
1338
1398
|
sessionLabel.textContent = 'session: (new)';
|
|
1339
1399
|
sessionLabel.classList.remove('copyable');
|
|
@@ -1467,6 +1527,19 @@ export const playgroundHtml = `<!doctype html>
|
|
|
1467
1527
|
removeThinking();
|
|
1468
1528
|
appendQuestionCard(data);
|
|
1469
1529
|
appendTimelineEntry(eventName, data);
|
|
1530
|
+
} else if (eventName === 'toollane:result') {
|
|
1531
|
+
// data.summary is a tool's raw, unreshaped return value (see
|
|
1532
|
+
// knownUrlsByBase's own doc comment above) — a JSON string for
|
|
1533
|
+
// this ability, but tools generally can return plain text too, so
|
|
1534
|
+
// a parse failure here just means nothing to record, not an error.
|
|
1535
|
+
if (typeof data.summary === 'string') {
|
|
1536
|
+
try {
|
|
1537
|
+
recordKnownUrls(JSON.parse(data.summary));
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
// Not JSON — nothing to correct against.
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
appendTimelineEntry(eventName, data);
|
|
1470
1543
|
} else {
|
|
1471
1544
|
appendTimelineEntry(eventName, data);
|
|
1472
1545
|
}
|