@agent-native/core 0.101.3 → 0.101.5

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 (79) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/action.ts +15 -0
  5. package/corpus/core/src/agent/production-agent.ts +215 -19
  6. package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
  7. package/corpus/core/src/client/sse-event-processor.ts +36 -1
  8. package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
  9. package/corpus/core/src/coding-tools/run-code.ts +11 -5
  10. package/corpus/core/src/deploy/build.ts +78 -25
  11. package/corpus/core/src/extensions/actions.ts +15 -0
  12. package/corpus/core/src/extensions/url-safety.ts +11 -1
  13. package/corpus/core/src/notifications/channels.ts +61 -30
  14. package/corpus/core/src/server/action-discovery.ts +4 -1
  15. package/corpus/core/src/server/agent-chat/context-tools.ts +4 -0
  16. package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  17. package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
  18. package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
  19. package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
  20. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
  21. package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
  22. package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
  23. package/corpus/templates/slides/actions/import-docx.ts +3 -9
  24. package/corpus/templates/slides/actions/import-file.ts +11 -14
  25. package/corpus/templates/slides/actions/import-pptx.ts +3 -9
  26. package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
  27. package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
  28. package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
  29. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
  30. package/dist/action.d.ts +12 -0
  31. package/dist/action.d.ts.map +1 -1
  32. package/dist/action.js +2 -0
  33. package/dist/action.js.map +1 -1
  34. package/dist/agent/production-agent.d.ts +24 -0
  35. package/dist/agent/production-agent.d.ts.map +1 -1
  36. package/dist/agent/production-agent.js +175 -18
  37. package/dist/agent/production-agent.js.map +1 -1
  38. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  39. package/dist/client/RunStuckBanner.js +9 -5
  40. package/dist/client/RunStuckBanner.js.map +1 -1
  41. package/dist/client/sse-event-processor.d.ts.map +1 -1
  42. package/dist/client/sse-event-processor.js +33 -1
  43. package/dist/client/sse-event-processor.js.map +1 -1
  44. package/dist/client/use-run-stuck-detection.d.ts +4 -4
  45. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  46. package/dist/client/use-run-stuck-detection.js +104 -19
  47. package/dist/client/use-run-stuck-detection.js.map +1 -1
  48. package/dist/coding-tools/run-code.d.ts +5 -2
  49. package/dist/coding-tools/run-code.d.ts.map +1 -1
  50. package/dist/coding-tools/run-code.js +11 -5
  51. package/dist/coding-tools/run-code.js.map +1 -1
  52. package/dist/collab/awareness.d.ts +2 -2
  53. package/dist/collab/awareness.d.ts.map +1 -1
  54. package/dist/collab/struct-routes.d.ts +1 -1
  55. package/dist/deploy/build.d.ts +15 -14
  56. package/dist/deploy/build.d.ts.map +1 -1
  57. package/dist/deploy/build.js +64 -23
  58. package/dist/deploy/build.js.map +1 -1
  59. package/dist/extensions/actions.d.ts.map +1 -1
  60. package/dist/extensions/actions.js +14 -0
  61. package/dist/extensions/actions.js.map +1 -1
  62. package/dist/extensions/url-safety.d.ts +6 -0
  63. package/dist/extensions/url-safety.d.ts.map +1 -1
  64. package/dist/extensions/url-safety.js +6 -0
  65. package/dist/extensions/url-safety.js.map +1 -1
  66. package/dist/notifications/channels.d.ts.map +1 -1
  67. package/dist/notifications/channels.js +38 -22
  68. package/dist/notifications/channels.js.map +1 -1
  69. package/dist/notifications/routes.d.ts +3 -3
  70. package/dist/observability/routes.d.ts +1 -1
  71. package/dist/progress/routes.d.ts +1 -1
  72. package/dist/resources/handlers.d.ts +3 -3
  73. package/dist/server/action-discovery.d.ts.map +1 -1
  74. package/dist/server/action-discovery.js +4 -1
  75. package/dist/server/action-discovery.js.map +1 -1
  76. package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
  77. package/dist/server/agent-chat/context-tools.js +4 -0
  78. package/dist/server/agent-chat/context-tools.js.map +1 -1
  79. package/package.json +3 -3
@@ -1436,18 +1436,23 @@ function seedReadOnlyToolResultsFromHistory(messages, actions) {
1436
1436
  const cache = new Map();
1437
1437
  if (!isInternalContinuationTurn(messages))
1438
1438
  return cache;
1439
+ // Scoped to the current turn only (same slice as
1440
+ // seedWriteToolInterruptionsFromHistory) — reads from a prior turn are no
1441
+ // longer relevant context and must not seed skip-as-duplicate behavior.
1442
+ const turnStart = findCurrentTurnStartForContinuation(messages);
1443
+ const turnMessages = messages.slice(turnStart);
1439
1444
  const pendingToolCalls = new Map();
1440
- for (const message of messages) {
1445
+ for (const message of turnMessages) {
1441
1446
  if (message.role === "assistant") {
1442
1447
  for (const part of message.content) {
1443
1448
  if (part.type !== "tool-call")
1444
1449
  continue;
1445
1450
  const entry = actions[part.name];
1446
- if (entry?.readOnly !== true)
1447
- continue;
1448
1451
  pendingToolCalls.set(part.id, {
1449
1452
  name: part.name,
1450
1453
  input: part.input,
1454
+ readOnly: entry?.readOnly === true,
1455
+ dedupe: entry?.dedupe !== false,
1451
1456
  });
1452
1457
  }
1453
1458
  continue;
@@ -1458,6 +1463,19 @@ function seedReadOnlyToolResultsFromHistory(messages, actions) {
1458
1463
  const call = pendingToolCalls.get(part.toolCallId);
1459
1464
  if (!call)
1460
1465
  continue;
1466
+ if (!call.readOnly) {
1467
+ // Mirror the live loop: a successful write invalidates all cached
1468
+ // reads (see the `readOnlyToolResultCache.clear()` call below), so a
1469
+ // read seeded from before an intervening write must not be replayed
1470
+ // as still-fresh.
1471
+ if (part.isError !== true)
1472
+ cache.clear();
1473
+ continue;
1474
+ }
1475
+ // dedupe:false read-only tools (volatile/polling reads) are never
1476
+ // cached — every call must execute fresh, seeded or not.
1477
+ if (!call.dedupe)
1478
+ continue;
1461
1479
  if (!isReusableReadOnlyToolResult(part))
1462
1480
  continue;
1463
1481
  cache.set(toolCallCacheKey(call.name, call.input), part.content);
@@ -1465,19 +1483,135 @@ function seedReadOnlyToolResultsFromHistory(messages, actions) {
1465
1483
  }
1466
1484
  return cache;
1467
1485
  }
1486
+ function visibleDuplicateReadOnlyToolResult(toolName) {
1487
+ return (`Skipped duplicate read-only call to ${toolName}: identical input already ran in this turn. ` +
1488
+ `Use the previous result already in the conversation instead of calling this tool again.`);
1489
+ }
1490
+ function resurfacedDuplicateReadOnlyToolResultPrefix(toolName) {
1491
+ return (`Skipped duplicate read-only call to ${toolName}: identical input already ran in this turn. ` +
1492
+ `Its earlier result is no longer in view, so here it is again:\n\n`);
1493
+ }
1494
+ function resurfacedDuplicateReadOnlyToolResult(toolName, cachedResult) {
1495
+ return `${resurfacedDuplicateReadOnlyToolResultPrefix(toolName)}${cachedResult}`;
1496
+ }
1497
+ /** Restore visible-repeat strike counts for this continuation's active turn. */
1498
+ function seedDuplicateReadOnlyToolCallsFromHistory(messages, actions) {
1499
+ const repeats = new Map();
1500
+ if (!isInternalContinuationTurn(messages))
1501
+ return repeats;
1502
+ const turnStart = findCurrentTurnStartForContinuation(messages);
1503
+ const pendingToolCalls = new Map();
1504
+ const reusableReadKeys = new Set();
1505
+ for (const message of messages.slice(turnStart)) {
1506
+ if (message.role === "assistant") {
1507
+ for (const part of message.content) {
1508
+ if (part.type !== "tool-call")
1509
+ continue;
1510
+ const entry = actions[part.name];
1511
+ pendingToolCalls.set(part.id, {
1512
+ name: part.name,
1513
+ input: part.input,
1514
+ readOnly: entry?.readOnly === true,
1515
+ dedupe: entry?.dedupe !== false,
1516
+ });
1517
+ }
1518
+ continue;
1519
+ }
1520
+ for (const part of message.content) {
1521
+ if (part.type !== "tool-result")
1522
+ continue;
1523
+ const call = pendingToolCalls.get(part.toolCallId);
1524
+ if (!call)
1525
+ continue;
1526
+ if (!call.readOnly) {
1527
+ if (part.isError !== true) {
1528
+ repeats.clear();
1529
+ reusableReadKeys.clear();
1530
+ }
1531
+ continue;
1532
+ }
1533
+ if (!call.dedupe || part.isError === true)
1534
+ continue;
1535
+ const cacheKey = toolCallCacheKey(call.name, call.input);
1536
+ if (part.content === visibleDuplicateReadOnlyToolResult(call.name)) {
1537
+ if (reusableReadKeys.has(cacheKey)) {
1538
+ repeats.set(cacheKey, (repeats.get(cacheKey) ?? 0) + 1);
1539
+ }
1540
+ continue;
1541
+ }
1542
+ if (part.content.startsWith(resurfacedDuplicateReadOnlyToolResultPrefix(call.name))) {
1543
+ if (reusableReadKeys.has(cacheKey))
1544
+ repeats.set(cacheKey, 0);
1545
+ continue;
1546
+ }
1547
+ if (isReusableReadOnlyToolResult(part)) {
1548
+ reusableReadKeys.add(cacheKey);
1549
+ }
1550
+ }
1551
+ }
1552
+ return repeats;
1553
+ }
1468
1554
  function isReusableReadOnlyToolResult(part) {
1469
1555
  if (part.isError)
1470
1556
  return false;
1471
1557
  const lower = part.content.trim().toLowerCase();
1472
1558
  if (!lower)
1473
1559
  return false;
1474
- return !(lower.startsWith("invalid action parameters for ") ||
1560
+ return !(lower.startsWith("skipped duplicate read-only call to ") ||
1561
+ lower.startsWith("invalid action parameters for ") ||
1475
1562
  lower.startsWith("error running ") ||
1476
1563
  lower.includes("run aborted") ||
1477
1564
  lower.includes("tool call timed out") ||
1478
1565
  lower.includes("stale_run") ||
1479
1566
  lower.includes("connection_error"));
1480
1567
  }
1568
+ /**
1569
+ * Whether a cached read-only tool result is still something the model can
1570
+ * actually see in `contextMessages` — the trimmed/summarized view the engine
1571
+ * is streamed (NOT the raw, ever-growing `messages` array the cache is keyed
1572
+ * off of). Context-xray `evict` drops tool-result parts entirely, `summarize`
1573
+ * replaces their content with a placeholder, and observational-memory
1574
+ * trimming drops whole older messages once active. When the cached result
1575
+ * has fallen out of that view, re-serving "use the previous result" is not
1576
+ * actionable — the model has nothing to point back to.
1577
+ *
1578
+ * A visible result must belong to the same tool name + normalized input and
1579
+ * contain either the exact cached body or the exact wrapper used when that
1580
+ * body was re-served after trimming. Matching only by a content suffix is too
1581
+ * loose: short results such as "ok" can also end unrelated tool output.
1582
+ */
1583
+ export function isCachedToolResultVisibleInContext(contextMessages, toolCall, cachedResult) {
1584
+ if (cachedResult.length === 0)
1585
+ return true;
1586
+ const cacheKey = toolCallCacheKey(toolCall.name, toolCall.input);
1587
+ const matchingToolCallIds = new Set();
1588
+ for (const message of contextMessages) {
1589
+ if (message.role !== "assistant")
1590
+ continue;
1591
+ for (const part of message.content) {
1592
+ if (part.type !== "tool-call")
1593
+ continue;
1594
+ if (toolCallCacheKey(part.name, part.input) === cacheKey) {
1595
+ matchingToolCallIds.add(part.id);
1596
+ }
1597
+ }
1598
+ }
1599
+ const resurfacedResult = resurfacedDuplicateReadOnlyToolResult(toolCall.name, cachedResult);
1600
+ for (const message of contextMessages) {
1601
+ if (message.role !== "user")
1602
+ continue;
1603
+ for (const part of message.content) {
1604
+ if (part.type !== "tool-result")
1605
+ continue;
1606
+ if (!matchingToolCallIds.has(part.toolCallId))
1607
+ continue;
1608
+ if (part.content === cachedResult || part.content === resurfacedResult) {
1609
+ return true;
1610
+ }
1611
+ }
1612
+ }
1613
+ return false;
1614
+ }
1481
1615
  /**
1482
1616
  * Counts how many times each write (non-read-only) tool call was interrupted
1483
1617
  * before returning a result in the continuation history. When a connection
@@ -2126,7 +2260,7 @@ export async function runAgentLoop(opts) {
2126
2260
  runCtx.toolResults = toolResultHistory;
2127
2261
  }
2128
2262
  const readOnlyToolResultCache = seedReadOnlyToolResultsFromHistory(messages, actions);
2129
- const duplicateReadOnlyToolCalls = new Map();
2263
+ const duplicateReadOnlyToolCalls = seedDuplicateReadOnlyToolCallsFromHistory(messages, actions);
2130
2264
  const writeToolInterruptions = seedWriteToolInterruptionsFromHistory(messages, actions);
2131
2265
  const repeatedToolErrors = new Map();
2132
2266
  // Tool-call journal hard-block (resume safety). See
@@ -3206,16 +3340,41 @@ export async function runAgentLoop(opts) {
3206
3340
  isError: true,
3207
3341
  };
3208
3342
  }
3209
- const cacheKey = actionEntry.readOnly === true
3343
+ // dedupe: false opts a read-only tool out of the guard entirely — the
3344
+ // cacheKey stays null so it never gets skipped-as-duplicate and never
3345
+ // populates the cache (see the success handler below, which also
3346
+ // leaves dedupe:false results uncached and un-cleared).
3347
+ const cacheKey = actionEntry.readOnly === true && actionEntry.dedupe !== false
3210
3348
  ? toolCallCacheKey(toolCall.name, toolCall.input)
3211
3349
  : null;
3212
3350
  if (cacheKey && readOnlyToolResultCache.has(cacheKey)) {
3213
- const repeats = (duplicateReadOnlyToolCalls.get(cacheKey) ?? 0) + 1;
3214
- duplicateReadOnlyToolCalls.set(cacheKey, repeats);
3215
3351
  const previousResult = readOnlyToolResultCache.get(cacheKey) ?? "";
3216
- const result = `Skipped duplicate read-only call to ${toolCall.name}: identical input already ran in this turn. ` +
3217
- `Use the previous result already in the conversation instead of calling this tool again.\n\n` +
3218
- `Previous result:\n${previousResult}`;
3352
+ // `contextMessages` (not `messages`) is what the model actually sees
3353
+ // this iteration context-xray eviction/summarization and
3354
+ // observational-memory trimming can drop the earlier result from
3355
+ // view even though it's still cached here. Only strike-count the
3356
+ // repeat when the model could have looked back and found it itself.
3357
+ const visible = isCachedToolResultVisibleInContext(contextMessages, toolCall, previousResult);
3358
+ let result;
3359
+ if (visible) {
3360
+ const repeats = (duplicateReadOnlyToolCalls.get(cacheKey) ?? 0) + 1;
3361
+ duplicateReadOnlyToolCalls.set(cacheKey, repeats);
3362
+ result = visibleDuplicateReadOnlyToolResult(toolCall.name);
3363
+ if (repeats >= 3) {
3364
+ requestedActionStop ??= {
3365
+ message: "I stopped because the agent kept asking for the same read-only context it already had. Please send the request again if you want me to retry from a fresh turn.",
3366
+ errorCode: "duplicate_read_only_tool",
3367
+ };
3368
+ }
3369
+ }
3370
+ else {
3371
+ // The earlier result was trimmed out of the model's visible
3372
+ // context — this isn't a repetitive loop, the model legitimately
3373
+ // can't see the answer anymore. Re-serve it in full and don't
3374
+ // count a strike.
3375
+ duplicateReadOnlyToolCalls.set(cacheKey, 0);
3376
+ result = resurfacedDuplicateReadOnlyToolResult(toolCall.name, previousResult);
3377
+ }
3219
3378
  send({
3220
3379
  type: "tool_done",
3221
3380
  id: toolCall.id,
@@ -3225,12 +3384,6 @@ export async function runAgentLoop(opts) {
3225
3384
  completedSideEffect: false,
3226
3385
  });
3227
3386
  recordToolResult(result, false);
3228
- if (repeats >= 3) {
3229
- requestedActionStop ??= {
3230
- message: "I stopped because the agent kept asking for the same read-only context it already had. Please send the request again if you want me to retry from a fresh turn.",
3231
- errorCode: "duplicate_read_only_tool",
3232
- };
3233
- }
3234
3387
  return {
3235
3388
  type: "tool-result",
3236
3389
  toolCallId: toolCall.id,
@@ -3467,7 +3620,11 @@ export async function runAgentLoop(opts) {
3467
3620
  if (cacheKey) {
3468
3621
  readOnlyToolResultCache.set(cacheKey, result);
3469
3622
  }
3470
- else {
3623
+ else if (actionEntry.readOnly !== true) {
3624
+ // A genuine write invalidates all cached reads. A dedupe:false
3625
+ // read-only tool also has a null cacheKey (see above) but must NOT
3626
+ // clear the cache — it isn't a write and other tools' cached reads
3627
+ // are still valid.
3471
3628
  readOnlyToolResultCache.clear();
3472
3629
  duplicateReadOnlyToolCalls.clear();
3473
3630
  }