@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +15 -0
- package/corpus/core/src/agent/production-agent.ts +215 -19
- package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
- package/corpus/core/src/client/sse-event-processor.ts +36 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
- package/corpus/core/src/coding-tools/run-code.ts +11 -5
- package/corpus/core/src/deploy/build.ts +78 -25
- package/corpus/core/src/extensions/actions.ts +15 -0
- package/corpus/core/src/extensions/url-safety.ts +11 -1
- package/corpus/core/src/notifications/channels.ts +61 -30
- package/corpus/core/src/server/action-discovery.ts +4 -1
- package/corpus/core/src/server/agent-chat/context-tools.ts +4 -0
- package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
- package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
- package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
- package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
- package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
- package/corpus/templates/slides/actions/import-docx.ts +3 -9
- package/corpus/templates/slides/actions/import-file.ts +11 -14
- package/corpus/templates/slides/actions/import-pptx.ts +3 -9
- package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
- package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
- package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
- package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
- package/dist/action.d.ts +12 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +2 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +24 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +175 -18
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +9 -5
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +33 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +4 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +104 -19
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts +5 -2
- package/dist/coding-tools/run-code.d.ts.map +1 -1
- package/dist/coding-tools/run-code.js +11 -5
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +15 -14
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +64 -23
- package/dist/deploy/build.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +14 -0
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/url-safety.d.ts +6 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +6 -0
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +38 -22
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -1
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
- package/dist/server/agent-chat/context-tools.js +4 -0
- package/dist/server/agent-chat/context-tools.js.map +1 -1
- 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
|
|
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("
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
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
|
}
|