@agent-native/core 0.196.0 → 0.196.1-nightly-20260928204657
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/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
- package/corpus/templates/clips/actions/create-recording.ts +1 -1
- package/corpus/templates/clips/actions/lib/auto-title-candidates.ts +42 -0
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +1 -1
- package/corpus/templates/clips/actions/lib/transcript-text.ts +16 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +49 -31
- package/corpus/templates/clips/actions/list-recordings.ts +2 -4
- package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +12 -6
- package/corpus/templates/clips/actions/regenerate-title.ts +4 -1
- package/corpus/templates/clips/actions/request-transcript.ts +1 -1
- package/corpus/templates/clips/actions/save-browser-transcript.ts +1 -1
- package/corpus/templates/clips/app/components/editable-recording-title.tsx +1 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +1 -1
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +194 -219
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-09-28-clip-share-previews-show-a-branded-image-while-the-recording.md +6 -0
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +2 -0
- package/corpus/templates/clips/server/lib/transactional-email-store.ts +42 -2
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +27 -15
- package/corpus/templates/clips/shared/ai-request-status.ts +6 -0
- package/corpus/templates/clips/shared/share-meta.ts +2 -27
- package/corpus/templates/clips/{actions/lib → shared}/title-source.ts +9 -2
- package/corpus/templates/slides/changelog/2026-09-28-typing-in-slides-agent-chat-keeps-every-character-in-order.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/README.md +11 -2
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +135 -4
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/run-manager.d.ts +2 -0
- package/dist/agent/run-manager.js +9 -2
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +148 -0
- package/dist/chat-threads/store.d.ts +1 -0
- package/dist/chat-threads/store.js +244 -21
- package/dist/client/AgentKitAssistantChat.js +23 -12
- package/dist/client/chat/agentkit-agent-native.js +409 -8
- package/dist/client/chat/agentkit-protocol.js +3 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +6 -6
- package/dist/org/handlers.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.js +12 -7
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
signShortLivedToken,
|
|
11
11
|
verifyShortLivedToken,
|
|
12
12
|
} from "@agent-native/core/server";
|
|
13
|
+
import { AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE } from "@agent-native/core/shared";
|
|
13
14
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
14
15
|
import { eq } from "drizzle-orm";
|
|
15
16
|
import {
|
|
@@ -159,6 +160,18 @@ function imageResponse(
|
|
|
159
160
|
return new Response(body, { status, headers });
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
function defaultSocialImageResponse(): Response {
|
|
164
|
+
return new Response(null, {
|
|
165
|
+
status: 302,
|
|
166
|
+
headers: {
|
|
167
|
+
Location: AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE,
|
|
168
|
+
"Cache-Control": "private, max-age=0, no-store",
|
|
169
|
+
"Referrer-Policy": "no-referrer",
|
|
170
|
+
"X-Content-Type-Options": "nosniff",
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
162
175
|
async function fetchThumbnail(sourceUrl: string): Promise<Response> {
|
|
163
176
|
let currentUrl = sourceUrl;
|
|
164
177
|
const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
|
|
@@ -325,20 +338,13 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
325
338
|
query.animated === "1"
|
|
326
339
|
? recording.animatedThumbnailUrl || recording.thumbnailUrl
|
|
327
340
|
: recording.thumbnailUrl || recording.animatedThumbnailUrl;
|
|
328
|
-
if (!sourceUrl)
|
|
329
|
-
setResponseStatus(event, 404);
|
|
330
|
-
return { error: "Thumbnail not found" };
|
|
331
|
-
}
|
|
341
|
+
if (!sourceUrl) return defaultSocialImageResponse();
|
|
332
342
|
|
|
333
343
|
if (sourceUrl.startsWith("data:")) {
|
|
334
|
-
return (
|
|
335
|
-
dataUrlResponse(sourceUrl) ??
|
|
336
|
-
imageResponse(null, "text/plain; charset=utf-8", 415)
|
|
337
|
-
);
|
|
344
|
+
return dataUrlResponse(sourceUrl) ?? defaultSocialImageResponse();
|
|
338
345
|
}
|
|
339
346
|
if (isRecursiveThumbnailUrl(sourceUrl, recordingId)) {
|
|
340
|
-
|
|
341
|
-
return { error: "Thumbnail not found" };
|
|
347
|
+
return defaultSocialImageResponse();
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
let resolvedSourceUrl = sourceUrl;
|
|
@@ -360,17 +366,23 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
360
366
|
extra: { recordingId },
|
|
361
367
|
},
|
|
362
368
|
);
|
|
363
|
-
setResponseStatus(event, 502);
|
|
364
|
-
return { error: "The recording thumbnail could not be loaded." };
|
|
365
369
|
}
|
|
366
|
-
|
|
370
|
+
const contentType = response.headers
|
|
371
|
+
.get("content-type")
|
|
372
|
+
?.split(";", 1)[0]
|
|
373
|
+
?.trim()
|
|
374
|
+
.toLowerCase();
|
|
375
|
+
return response.ok &&
|
|
376
|
+
contentType &&
|
|
377
|
+
SAFE_RASTER_IMAGE_TYPES.has(contentType)
|
|
378
|
+
? response
|
|
379
|
+
: defaultSocialImageResponse();
|
|
367
380
|
} catch (error) {
|
|
368
381
|
captureRouteError(error, {
|
|
369
382
|
route: "api/thumbnail",
|
|
370
383
|
extra: { recordingId },
|
|
371
384
|
});
|
|
372
|
-
|
|
373
|
-
return { error: "The recording thumbnail could not be loaded." };
|
|
385
|
+
return defaultSocialImageResponse();
|
|
374
386
|
}
|
|
375
387
|
},
|
|
376
388
|
);
|
|
@@ -10,9 +10,6 @@ import {
|
|
|
10
10
|
type SocialMetaDescriptor,
|
|
11
11
|
} from "@agent-native/core/shared";
|
|
12
12
|
|
|
13
|
-
import { buildAgentApiUrls } from "./agent-context";
|
|
14
|
-
import { isLoomEmbedBackedRecording } from "./loom";
|
|
15
|
-
|
|
16
13
|
export const CLIPS_DEFAULT_TITLE = "Untitled recording";
|
|
17
14
|
|
|
18
15
|
export type ClipsShareMetaRecording = {
|
|
@@ -32,8 +29,6 @@ export type ClipsShareMetaRecording = {
|
|
|
32
29
|
isLoomEmbedBacked?: boolean;
|
|
33
30
|
};
|
|
34
31
|
|
|
35
|
-
const SOCIAL_FRAME_AT_MS = 350;
|
|
36
|
-
|
|
37
32
|
export type PreferredThumbnailVariant = "still" | "animated";
|
|
38
33
|
|
|
39
34
|
export function preferredThumbnailVariant(
|
|
@@ -146,31 +141,11 @@ export function resolveClipsSocialImageUrl(options: {
|
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
if (!canUseSocialImage(recording)) return undefined;
|
|
149
|
-
if (
|
|
150
|
-
!origin ||
|
|
151
|
-
recording.isLoomEmbedBacked === true ||
|
|
152
|
-
isLoomEmbedBackedRecording(recording)
|
|
153
|
-
) {
|
|
154
|
-
console.warn("clips.thumbnail.publish_fallback", {
|
|
155
|
-
recordingId: recording.id,
|
|
156
|
-
fallback: "default-image",
|
|
157
|
-
});
|
|
158
|
-
return AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
144
|
console.warn("clips.thumbnail.publish_fallback", {
|
|
162
145
|
recordingId: recording.id,
|
|
163
|
-
fallback: "
|
|
146
|
+
fallback: "default-image",
|
|
164
147
|
});
|
|
165
|
-
|
|
166
|
-
buildAgentApiUrls(recording.id, {
|
|
167
|
-
origin,
|
|
168
|
-
basePath,
|
|
169
|
-
}).frameUrl(SOCIAL_FRAME_AT_MS),
|
|
170
|
-
);
|
|
171
|
-
const version = recording.updatedAt?.trim();
|
|
172
|
-
if (version) frameUrl.searchParams.set("v", version);
|
|
173
|
-
return frameUrl.toString();
|
|
148
|
+
return AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE;
|
|
174
149
|
}
|
|
175
150
|
|
|
176
151
|
export function buildClipsShareMeta(options: {
|
|
@@ -15,13 +15,20 @@ export function isDefaultTitle(title: string | null | undefined): boolean {
|
|
|
15
15
|
return !trimmed || trimmed === DEFAULT_RECORDING_TITLE;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Title sources an auto-generated title may overwrite. */
|
|
19
|
+
export const AUTO_REPLACEABLE_TITLE_SOURCES = [
|
|
20
|
+
"default",
|
|
21
|
+
"context",
|
|
22
|
+
] as const satisfies readonly RecordingTitleSource[];
|
|
23
|
+
|
|
18
24
|
export function isAutoTitleReplaceable(
|
|
19
25
|
title: string | null | undefined,
|
|
20
26
|
titleSource: string | null | undefined,
|
|
21
27
|
): boolean {
|
|
22
28
|
return (
|
|
23
29
|
isDefaultTitle(title) ||
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
(AUTO_REPLACEABLE_TITLE_SOURCES as readonly string[]).includes(
|
|
31
|
+
titleSource ?? "",
|
|
32
|
+
)
|
|
26
33
|
);
|
|
27
34
|
}
|
|
@@ -15,6 +15,13 @@ Run the script from `templates/slides`:
|
|
|
15
15
|
pnpm exec tsx scripts/edit-fidelity/run.ts [case-filter] [options]
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Run the focused chat caret regression while a synthetic slide text edit session
|
|
19
|
+
is open in Chromium:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm exec tsx scripts/edit-fidelity/run.ts --typing-chat
|
|
23
|
+
```
|
|
24
|
+
|
|
18
25
|
By default the harness starts its own scratch dev server with this command,
|
|
19
26
|
run from the repo root:
|
|
20
27
|
|
|
@@ -47,6 +54,7 @@ because it creates and rewrites decks.
|
|
|
47
54
|
| `--resume <run>` | Reuse `<run>`'s output and keep every result that did not error |
|
|
48
55
|
| `--cpu-throttle N` | Slow each editor page's CPU N times, to reproduce timing-dependent saves |
|
|
49
56
|
| `--headed` | Show the browser |
|
|
57
|
+
| `--typing-chat` | Run only the Agent chat typing regression with a slide text edit session open |
|
|
50
58
|
|
|
51
59
|
Exit codes:
|
|
52
60
|
|
|
@@ -136,8 +144,9 @@ them per slide. For each target and scenario:
|
|
|
136
144
|
stylesheet per slide font, so `fonts.ready` alone can resolve before the
|
|
137
145
|
slide's font is requested. Scenarios never contaminate each other: a
|
|
138
146
|
`pagehide` keepalive write from the previous page that carries other
|
|
139
|
-
content for this slide could land after the restore
|
|
140
|
-
|
|
147
|
+
content for this slide could land after the restore. The harness retains
|
|
148
|
+
those request bodies, waits for the restored content to settle when one
|
|
149
|
+
could overwrite it, and errors if the fixture did not survive.
|
|
141
150
|
2. **View.** Capture `view.png` and a style snapshot of the slide.
|
|
142
151
|
3. **Enter edit.** Try click, then a second click, then double-click. The
|
|
143
152
|
gesture that worked is recorded. A click must leave a caret within one
|
|
@@ -134,6 +134,7 @@ const cpuThrottle = numOpt("--cpu-throttle", 1);
|
|
|
134
134
|
const update = argv.includes("--update");
|
|
135
135
|
const acceptFailing = argv.includes("--accept-failing");
|
|
136
136
|
const headed = argv.includes("--headed");
|
|
137
|
+
const typingChatOnly = argv.includes("--typing-chat");
|
|
137
138
|
for (const s of scenarios) {
|
|
138
139
|
if (!SCENARIOS.includes(s))
|
|
139
140
|
fatal(`unknown scenario ${s}; expected ${SCENARIOS.join(",")}`);
|
|
@@ -532,6 +533,101 @@ async function exitEdit(
|
|
|
532
533
|
return waitFor(async () => !(await editorState(page, slideId)).editing, 5000);
|
|
533
534
|
}
|
|
534
535
|
|
|
536
|
+
async function runChatTypingRegression(page: Page, base: string) {
|
|
537
|
+
await page.route("**/_agent-native/agent-engine/status", (route: any) =>
|
|
538
|
+
route.fulfill({
|
|
539
|
+
status: 200,
|
|
540
|
+
contentType: "application/json",
|
|
541
|
+
body: JSON.stringify({ configured: true, chatEligible: true }),
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
await page.goto(`${base}/home`, { waitUntil: "domcontentloaded" });
|
|
545
|
+
await ensureSignedIn(page);
|
|
546
|
+
const created = await action(page, "create-deck", {
|
|
547
|
+
title: "[edit-fidelity] chat typing regression",
|
|
548
|
+
slides: [
|
|
549
|
+
{
|
|
550
|
+
id: "chat-typing-slide",
|
|
551
|
+
content:
|
|
552
|
+
'<div class="fmd-slide"><p>Slide text edit stays open</p></div>',
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
});
|
|
556
|
+
const deckId = String(created.id ?? created.deckId);
|
|
557
|
+
await openSlide(page, base, deckId, 0, "chat-typing-slide");
|
|
558
|
+
const [target] = await listTargets(page, "chat-typing-slide");
|
|
559
|
+
if (!target) throw new Error("synthetic slide has no editable text target");
|
|
560
|
+
if (!(await enterEdit(page, "chat-typing-slide", target.point, []))) {
|
|
561
|
+
throw new Error("could not open the synthetic slide text edit session");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
await page.evaluate(() =>
|
|
565
|
+
window.dispatchEvent(
|
|
566
|
+
new CustomEvent("agent-panel:open", { detail: { focus: true } }),
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
const selector =
|
|
570
|
+
'.agent-sidebar-panel[data-agent-sidebar-state="open"] [data-agent-composer-slot="editor-input"]';
|
|
571
|
+
const composer = page.locator(selector);
|
|
572
|
+
await composer.waitFor({ state: "visible", timeout: 45_000 });
|
|
573
|
+
if (!(await editorState(page, "chat-typing-slide")).editing) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
"opening the Agent sidebar ended the slide text edit session",
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
await page.waitForFunction(
|
|
579
|
+
(inputSelector: string) =>
|
|
580
|
+
document
|
|
581
|
+
.querySelector<HTMLElement>(inputSelector)
|
|
582
|
+
?.getAttribute("contenteditable") === "true",
|
|
583
|
+
selector,
|
|
584
|
+
{ timeout: 20_000 },
|
|
585
|
+
);
|
|
586
|
+
await composer.focus();
|
|
587
|
+
|
|
588
|
+
await composer.evaluate((element: HTMLElement) => {
|
|
589
|
+
(window as any).__typingRegressionComposer = element;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const first = "Fast typing should keep every character in order.";
|
|
593
|
+
const second = " A pause must not reset the caret either.";
|
|
594
|
+
await composer.pressSequentially(first);
|
|
595
|
+
await sleep(400);
|
|
596
|
+
await composer.pressSequentially(second);
|
|
597
|
+
|
|
598
|
+
const expected = first + second;
|
|
599
|
+
const result = await page.evaluate((selector: string) => {
|
|
600
|
+
const editor = document.querySelector<HTMLElement>(selector);
|
|
601
|
+
const selection = window.getSelection();
|
|
602
|
+
const focusNode = selection?.focusNode;
|
|
603
|
+
return {
|
|
604
|
+
text: editor?.innerText ?? null,
|
|
605
|
+
sameNode: editor === (window as any).__typingRegressionComposer,
|
|
606
|
+
focused: document.activeElement === editor,
|
|
607
|
+
caretOffset:
|
|
608
|
+
editor && focusNode && editor.contains(focusNode)
|
|
609
|
+
? (selection?.focusOffset ?? null)
|
|
610
|
+
: null,
|
|
611
|
+
};
|
|
612
|
+
}, selector);
|
|
613
|
+
const editAfterTyping = await editorState(page, "chat-typing-slide");
|
|
614
|
+
const problems: string[] = [];
|
|
615
|
+
if (result.text !== expected) {
|
|
616
|
+
problems.push(`text mismatch: ${JSON.stringify(result.text)}`);
|
|
617
|
+
}
|
|
618
|
+
if (!result.sameNode) problems.push("composer remounted while typing");
|
|
619
|
+
if (!result.focused) problems.push("composer lost focus while typing");
|
|
620
|
+
if (result.caretOffset !== expected.length) {
|
|
621
|
+
problems.push(
|
|
622
|
+
`caret ended at ${result.caretOffset}, expected ${expected.length}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (!editAfterTyping.editing) {
|
|
626
|
+
problems.push("slide text edit session ended while typing in chat");
|
|
627
|
+
}
|
|
628
|
+
return problems;
|
|
629
|
+
}
|
|
630
|
+
|
|
535
631
|
/**
|
|
536
632
|
* Polls the stored slide until it stops changing and no write the page sent
|
|
537
633
|
* is still in flight. Saves are debounced, so "no change yet" is only trusted
|
|
@@ -897,11 +993,30 @@ async function runScenario(
|
|
|
897
993
|
page.on("requestfailed", onWriteDone);
|
|
898
994
|
|
|
899
995
|
try {
|
|
996
|
+
await page.goto(`${ctx.base}/home`, { waitUntil: "domcontentloaded" });
|
|
997
|
+
const priorPagehideWrites = await takeKeepaliveWrites(page);
|
|
998
|
+
const priorPagehideMismatches = keepaliveMismatches(
|
|
999
|
+
priorPagehideWrites,
|
|
1000
|
+
slideId,
|
|
1001
|
+
ctx.stored,
|
|
1002
|
+
);
|
|
900
1003
|
await restoreSlide(page, deckId, slideId, ctx.stored);
|
|
1004
|
+
if (priorPagehideMismatches.length) {
|
|
1005
|
+
const restored = await settleSaved(
|
|
1006
|
+
page,
|
|
1007
|
+
deckId,
|
|
1008
|
+
slideId,
|
|
1009
|
+
() => inFlight.size,
|
|
1010
|
+
15_000,
|
|
1011
|
+
);
|
|
1012
|
+
if (restored !== ctx.stored) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
`${priorPagehideMismatches.length} prior pagehide write(s) overwrote the restored slide`,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
901
1018
|
await openSlide(page, ctx.base, deckId, ctx.slideIndex, slideId);
|
|
902
|
-
//
|
|
903
|
-
// keepalive flush it sent can land after the restore and change the
|
|
904
|
-
// slide this scenario starts from.
|
|
1019
|
+
// Earlier writes were checked after settling; only new writes can still overwrite the fixture.
|
|
905
1020
|
const leftBehind = keepaliveMismatches(
|
|
906
1021
|
await takeKeepaliveWrites(page),
|
|
907
1022
|
slideId,
|
|
@@ -909,7 +1024,7 @@ async function runScenario(
|
|
|
909
1024
|
);
|
|
910
1025
|
if (leftBehind.length) {
|
|
911
1026
|
throw new Error(
|
|
912
|
-
|
|
1027
|
+
`${leftBehind.length} pagehide keepalive write(s) can overwrite the restored slide`,
|
|
913
1028
|
);
|
|
914
1029
|
}
|
|
915
1030
|
const current = (await listTargets(page, slideId))[target.index];
|
|
@@ -1815,6 +1930,22 @@ async function main() {
|
|
|
1815
1930
|
await warmUp(warm, base);
|
|
1816
1931
|
await warm.close();
|
|
1817
1932
|
|
|
1933
|
+
if (typingChatOnly) {
|
|
1934
|
+
const page = await context.newPage();
|
|
1935
|
+
const problems = await runChatTypingRegression(page, base);
|
|
1936
|
+
await page.close();
|
|
1937
|
+
if (problems.length) {
|
|
1938
|
+
console.error(
|
|
1939
|
+
`[edit-fidelity] chat typing regression: ${problems.join("; ")}`,
|
|
1940
|
+
);
|
|
1941
|
+
return 1;
|
|
1942
|
+
}
|
|
1943
|
+
console.log(
|
|
1944
|
+
"[edit-fidelity] chat typing regression passed with a slide edit session open",
|
|
1945
|
+
);
|
|
1946
|
+
return 0;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1818
1949
|
console.log(
|
|
1819
1950
|
`[edit-fidelity] ${base} · ${cases.length} case(s) · scenarios ${scenarios.join(",")} · out ${outRoot}`,
|
|
1820
1951
|
);
|
|
@@ -7657,6 +7657,8 @@ export function createProductionAgentHandler(options) {
|
|
|
7657
7657
|
backgroundFunction: runsInBackgroundFunction,
|
|
7658
7658
|
noProgressTimeoutMs: options.runNoProgressTimeoutMs,
|
|
7659
7659
|
turnId: effectiveTurnId,
|
|
7660
|
+
agentKitApprovalContinuation: internalContinuation &&
|
|
7661
|
+
Boolean(approvedToolCallsForExecution?.length),
|
|
7660
7662
|
turnInitiator,
|
|
7661
7663
|
parentId: requestParentId,
|
|
7662
7664
|
waitUntil: getRequestRunContext()?.waitUntil,
|
|
@@ -6,6 +6,7 @@ export interface ActiveRun {
|
|
|
6
6
|
threadId: string;
|
|
7
7
|
parentId?: string | null;
|
|
8
8
|
turnId: string;
|
|
9
|
+
agentKitApprovalContinuation?: boolean;
|
|
9
10
|
events: RunEvent[];
|
|
10
11
|
status: RunStatus;
|
|
11
12
|
subscribers: Set<(event: RunEvent) => void>;
|
|
@@ -54,6 +55,7 @@ export interface StartRunOptions {
|
|
|
54
55
|
softTimeoutMs?: number;
|
|
55
56
|
useHostedSoftTimeoutDefault?: boolean;
|
|
56
57
|
turnId?: string;
|
|
58
|
+
agentKitApprovalContinuation?: boolean;
|
|
57
59
|
parentId?: string | null;
|
|
58
60
|
backgroundFunction?: boolean;
|
|
59
61
|
noProgressTimeoutMs?: number;
|
|
@@ -486,6 +486,9 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
486
486
|
threadId,
|
|
487
487
|
...(options?.parentId !== undefined ? { parentId: options.parentId } : {}),
|
|
488
488
|
turnId: options?.turnId ?? runId,
|
|
489
|
+
...(options?.agentKitApprovalContinuation
|
|
490
|
+
? { agentKitApprovalContinuation: true }
|
|
491
|
+
: {}),
|
|
489
492
|
events: [],
|
|
490
493
|
status: "running",
|
|
491
494
|
subscribers: new Set(),
|
|
@@ -1795,7 +1798,8 @@ export async function getActiveRunForThreadAsync(threadId) {
|
|
|
1795
1798
|
const memRun = getActiveRunForThread(threadId);
|
|
1796
1799
|
if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
|
|
1797
1800
|
const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
|
|
1798
|
-
if (
|
|
1801
|
+
if (memRun.status !== "running" &&
|
|
1802
|
+
(!sqlSnapshot || sqlSnapshot.status === "running")) {
|
|
1799
1803
|
const successor = await fetchNewerNonTerminalRunForSameTurn(threadId, memRun);
|
|
1800
1804
|
if (successor) {
|
|
1801
1805
|
return {
|
|
@@ -1813,7 +1817,10 @@ export async function getActiveRunForThreadAsync(threadId) {
|
|
|
1813
1817
|
};
|
|
1814
1818
|
}
|
|
1815
1819
|
}
|
|
1816
|
-
const
|
|
1820
|
+
const sqlStatus = sqlSnapshot?.status === "running" && memRun.status !== "running"
|
|
1821
|
+
? memRun.status
|
|
1822
|
+
: (sqlSnapshot?.status ?? memRun.status);
|
|
1823
|
+
const status = legacyWireRunStatus(sqlStatus);
|
|
1817
1824
|
const heartbeatAt = status === "running"
|
|
1818
1825
|
? Date.now()
|
|
1819
1826
|
: (sqlSnapshot?.heartbeatAt ?? memRun.startedAt);
|
|
@@ -107,6 +107,7 @@ export declare function foldAssistantTurn(repo: any, assistantMsg: AssistantMess
|
|
|
107
107
|
turnId?: string;
|
|
108
108
|
runId?: string;
|
|
109
109
|
parentId?: string | null;
|
|
110
|
+
agentKitOwnsContinuation?: boolean;
|
|
110
111
|
}): any;
|
|
111
112
|
export declare function normalizeThreadTitle(value: unknown): string;
|
|
112
113
|
export declare function extractThreadMeta(repo: any): {
|
|
@@ -1039,6 +1039,149 @@ export function claimQueuedMessage(repo, messageId) {
|
|
|
1039
1039
|
normalized[CLAIMED_QUEUED_MESSAGE_IDS_KEY] = [...claimed, messageId].slice(-MAX_CLAIMED_QUEUED_MESSAGE_IDS);
|
|
1040
1040
|
return pruneClaimedQueuedMessages(normalized);
|
|
1041
1041
|
}
|
|
1042
|
+
function snapshotEntryId(entry, kind) {
|
|
1043
|
+
if (!entry || typeof entry !== "object")
|
|
1044
|
+
return undefined;
|
|
1045
|
+
if (kind === "widget") {
|
|
1046
|
+
const messageId = entry.messageId;
|
|
1047
|
+
const widgetId = entry.widget?.id;
|
|
1048
|
+
return typeof messageId === "string" && typeof widgetId === "string"
|
|
1049
|
+
? JSON.stringify([messageId, widgetId])
|
|
1050
|
+
: undefined;
|
|
1051
|
+
}
|
|
1052
|
+
return typeof entry.id === "string" ? entry.id : undefined;
|
|
1053
|
+
}
|
|
1054
|
+
function snapshotMessageRunIds(agentKit) {
|
|
1055
|
+
const runIds = new Map();
|
|
1056
|
+
for (const event of Array.isArray(agentKit?.events) ? agentKit.events : []) {
|
|
1057
|
+
if ((event?.type === "message.created" ||
|
|
1058
|
+
event?.type === "message.completed") &&
|
|
1059
|
+
event.message?.role === "assistant" &&
|
|
1060
|
+
typeof event.message.id === "string" &&
|
|
1061
|
+
typeof event.runId === "string") {
|
|
1062
|
+
runIds.set(event.message.id, event.runId);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
for (const run of Array.isArray(agentKit?.runs) ? agentKit.runs : []) {
|
|
1066
|
+
if (typeof run?.activeMessageId === "string" &&
|
|
1067
|
+
typeof run.id === "string") {
|
|
1068
|
+
runIds.set(run.activeMessageId, run.id);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return runIds;
|
|
1072
|
+
}
|
|
1073
|
+
function snapshotAssistantTextKey(message, runIds) {
|
|
1074
|
+
const runId = (typeof message?.id === "string" ? runIds.get(message.id) : undefined) ??
|
|
1075
|
+
message?.metadata?.runId ??
|
|
1076
|
+
message?.metadata?.custom?.runId;
|
|
1077
|
+
if (message?.role !== "assistant" ||
|
|
1078
|
+
typeof runId !== "string" ||
|
|
1079
|
+
!Array.isArray(message.parts) ||
|
|
1080
|
+
message.parts.length === 0 ||
|
|
1081
|
+
!message.parts.every((part) => part?.type === "text" && typeof part.text === "string")) {
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
return JSON.stringify([
|
|
1085
|
+
runId,
|
|
1086
|
+
message.parts.map((part) => [part.text, part.format ?? null]),
|
|
1087
|
+
]);
|
|
1088
|
+
}
|
|
1089
|
+
function preferIncomingSnapshotEntry(kind, existing, incoming) {
|
|
1090
|
+
if (kind === "message") {
|
|
1091
|
+
const rank = (message) => message.status === "complete" ? 2 : message.status === "error" ? 1 : 0;
|
|
1092
|
+
if (rank(existing) !== rank(incoming))
|
|
1093
|
+
return rank(incoming) > rank(existing);
|
|
1094
|
+
const partCount = (message) => Array.isArray(message.parts) ? message.parts.length : 0;
|
|
1095
|
+
if (partCount(existing) !== partCount(incoming)) {
|
|
1096
|
+
return partCount(incoming) > partCount(existing);
|
|
1097
|
+
}
|
|
1098
|
+
const textLength = (message) => (Array.isArray(message.parts) ? message.parts : []).reduce((total, part) => total + (typeof part?.text === "string" ? part.text.length : 0), 0);
|
|
1099
|
+
return textLength(incoming) > textLength(existing);
|
|
1100
|
+
}
|
|
1101
|
+
if (kind === "toolCall") {
|
|
1102
|
+
return existing.status === "running" && incoming.status !== "running";
|
|
1103
|
+
}
|
|
1104
|
+
return (existing.widget?.state === "active" && incoming.widget?.state !== "active");
|
|
1105
|
+
}
|
|
1106
|
+
function mergeAgentKitHistoryArray(existing, incoming, kind, existingMessageRunIds, incomingMessageRunIds) {
|
|
1107
|
+
if (!Array.isArray(existing) && !Array.isArray(incoming))
|
|
1108
|
+
return undefined;
|
|
1109
|
+
const merged = Array.isArray(existing) ? [...existing] : [];
|
|
1110
|
+
const positions = new Map();
|
|
1111
|
+
const assistantTextPositions = new Map();
|
|
1112
|
+
merged.forEach((entry, index) => {
|
|
1113
|
+
const id = snapshotEntryId(entry, kind);
|
|
1114
|
+
if (id && !positions.has(id))
|
|
1115
|
+
positions.set(id, index);
|
|
1116
|
+
if (kind === "message") {
|
|
1117
|
+
const key = snapshotAssistantTextKey(entry, existingMessageRunIds);
|
|
1118
|
+
if (key) {
|
|
1119
|
+
assistantTextPositions.set(key, [
|
|
1120
|
+
...(assistantTextPositions.get(key) ?? []),
|
|
1121
|
+
index,
|
|
1122
|
+
]);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
const matchedAssistantTextPositions = new Set();
|
|
1127
|
+
for (const entry of Array.isArray(incoming) ? incoming : []) {
|
|
1128
|
+
const id = snapshotEntryId(entry, kind);
|
|
1129
|
+
const idIndex = id ? positions.get(id) : undefined;
|
|
1130
|
+
const textKey = kind === "message"
|
|
1131
|
+
? snapshotAssistantTextKey(entry, incomingMessageRunIds)
|
|
1132
|
+
: undefined;
|
|
1133
|
+
const textIndex = idIndex === undefined && textKey
|
|
1134
|
+
? assistantTextPositions
|
|
1135
|
+
.get(textKey)
|
|
1136
|
+
?.find((candidate) => !matchedAssistantTextPositions.has(candidate))
|
|
1137
|
+
: undefined;
|
|
1138
|
+
const index = idIndex ?? textIndex;
|
|
1139
|
+
if (index === undefined) {
|
|
1140
|
+
if (id)
|
|
1141
|
+
positions.set(id, merged.length);
|
|
1142
|
+
merged.push(entry);
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
if (kind === "message")
|
|
1146
|
+
matchedAssistantTextPositions.add(index);
|
|
1147
|
+
if (id && idIndex === undefined)
|
|
1148
|
+
positions.set(id, index);
|
|
1149
|
+
const preferIncoming = preferIncomingSnapshotEntry(kind, merged[index], entry);
|
|
1150
|
+
const preferCurrentMessageId = kind === "message" &&
|
|
1151
|
+
textIndex !== undefined &&
|
|
1152
|
+
!preferIncomingSnapshotEntry(kind, entry, merged[index]);
|
|
1153
|
+
if (preferIncoming || preferCurrentMessageId) {
|
|
1154
|
+
merged[index] = entry;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return merged;
|
|
1159
|
+
}
|
|
1160
|
+
function mergeAgentKitHistory(existing, incoming) {
|
|
1161
|
+
if (!existing ||
|
|
1162
|
+
typeof existing !== "object" ||
|
|
1163
|
+
Array.isArray(existing) ||
|
|
1164
|
+
!incoming ||
|
|
1165
|
+
typeof incoming !== "object" ||
|
|
1166
|
+
Array.isArray(incoming)) {
|
|
1167
|
+
return incoming ?? existing;
|
|
1168
|
+
}
|
|
1169
|
+
const previous = existing;
|
|
1170
|
+
const next = incoming;
|
|
1171
|
+
const merged = { ...previous, ...next };
|
|
1172
|
+
const existingMessageRunIds = snapshotMessageRunIds(previous);
|
|
1173
|
+
const incomingMessageRunIds = snapshotMessageRunIds(next);
|
|
1174
|
+
for (const [key, kind] of [
|
|
1175
|
+
["messages", "message"],
|
|
1176
|
+
["toolCalls", "toolCall"],
|
|
1177
|
+
["widgets", "widget"],
|
|
1178
|
+
]) {
|
|
1179
|
+
const entries = mergeAgentKitHistoryArray(previous[key], next[key], kind, existingMessageRunIds, incomingMessageRunIds);
|
|
1180
|
+
if (entries)
|
|
1181
|
+
merged[key] = entries;
|
|
1182
|
+
}
|
|
1183
|
+
return merged;
|
|
1184
|
+
}
|
|
1042
1185
|
function pruneClaimedQueuedMessages(repo) {
|
|
1043
1186
|
if (!Array.isArray(repo?.queuedMessages))
|
|
1044
1187
|
return repo;
|
|
@@ -1079,6 +1222,9 @@ export function mergeThreadDataForClientSave(existingRepo, incomingRepo, options
|
|
|
1079
1222
|
merged.queuedMessages === undefined) {
|
|
1080
1223
|
merged.queuedMessages = existingNormalized.queuedMessages;
|
|
1081
1224
|
}
|
|
1225
|
+
if (merged.agentKit !== undefined) {
|
|
1226
|
+
merged.agentKit = mergeAgentKitHistory(existingNormalized?.agentKit, merged.agentKit);
|
|
1227
|
+
}
|
|
1082
1228
|
const existingMessages = Array.isArray(existingNormalized?.messages)
|
|
1083
1229
|
? existingNormalized.messages
|
|
1084
1230
|
: null;
|
|
@@ -1509,6 +1655,8 @@ function appendFoldedContent(existing, incoming) {
|
|
|
1509
1655
|
}).content;
|
|
1510
1656
|
}
|
|
1511
1657
|
export function foldAssistantTurn(repo, assistantMsg, options) {
|
|
1658
|
+
if (options.agentKitOwnsContinuation)
|
|
1659
|
+
return repo;
|
|
1512
1660
|
const turnId = options.turnId;
|
|
1513
1661
|
const runId = options.runId;
|
|
1514
1662
|
if (!turnId)
|