loopengine 0.1.51 → 0.1.52

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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",