@agent-native/core 0.211.0 → 0.212.0-nightly-20261021010049
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/README.md +1 -1
- package/dist/app-config/analytics.d.ts +1 -0
- package/dist/app-config/analytics.js +7 -0
- package/dist/app-config/schema.d.ts +1 -0
- package/dist/chat-threads/store.d.ts +28 -2
- package/dist/chat-threads/store.js +75 -13
- package/dist/cli/template-baseline.js +18 -5
- package/dist/cli/template-sync.js +100 -19
- package/dist/client/agent-chat/index.d.ts +2 -1
- package/dist/client/agent-chat/index.js +2 -1
- package/dist/client/chat/agentkit-protocol.js +21 -1
- package/dist/client/chat/run-outcome.d.ts +1 -0
- package/dist/client/chat/runtime.js +6 -0
- package/dist/client/sharing/index.d.ts +2 -1
- package/dist/client/sharing/index.js +2 -1
- package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
- package/dist/client/sharing/share-controller-helpers.js +46 -1
- package/dist/client/sharing/useShareButtonController.d.ts +12 -0
- package/dist/client/sharing/useShareButtonController.js +76 -57
- package/dist/client/sharing/useShareDialogController.d.ts +3 -0
- package/dist/client/sharing/useShareDialogController.js +77 -49
- package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
- package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
- package/dist/client/use-chat-thread-list.d.ts +40 -0
- package/dist/client/use-chat-thread-list.js +210 -0
- package/dist/client/use-chat-threads.d.ts +67 -0
- package/dist/client/use-chat-threads.js +141 -23
- package/dist/client/use-db-sync.d.ts +1 -0
- package/dist/client/use-db-sync.js +13 -1
- package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
- package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/bounded-task-bundle.d.ts +17 -0
- package/dist/collab/bounded-task-bundle.js +39 -0
- package/dist/collab/bounded-task-source.d.ts +14 -0
- package/dist/collab/bounded-task-source.js +8 -0
- package/dist/collab/bounded-tasks.d.ts +67 -0
- package/dist/collab/bounded-tasks.js +439 -0
- package/dist/collab/client.js +2 -1
- package/dist/collab/index.d.ts +5 -2
- package/dist/collab/index.js +5 -2
- package/dist/collab/live-copy-tasks.d.ts +80 -0
- package/dist/collab/live-copy-tasks.js +330 -0
- package/dist/collab/live-copy.d.ts +188 -0
- package/dist/collab/live-copy.js +415 -0
- package/dist/collab/routes.d.ts +5 -0
- package/dist/collab/routes.js +24 -8
- package/dist/collab/storage.d.ts +53 -1
- package/dist/collab/storage.js +93 -5
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/collab/ydoc-manager.d.ts +92 -3
- package/dist/collab/ydoc-manager.js +447 -38
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.js +25 -5
- package/dist/deploy/build.js +2 -0
- package/dist/extensions/actions.js +1 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/agent-connected.js +5 -1
- package/dist/mcp/build-server.js +39 -10
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/org/index.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/server/agent-chat-plugin.js +80 -17
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +1 -0
- package/dist/server/analytics.js +18 -1
- package/dist/server/collab-plugin.d.ts +10 -1
- package/dist/server/collab-plugin.js +89 -20
- package/dist/server/index.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/optional-node-builtins.d.ts +1 -0
- package/dist/shared/optional-node-builtins.js +3 -0
- package/dist/sharing/access.d.ts +1 -0
- package/dist/sharing/access.js +113 -95
- package/dist/tracking/providers.d.ts +1 -0
- package/dist/tracking/providers.js +2 -1
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/dist/vite/bounded-task-plugin.d.ts +22 -0
- package/dist/vite/bounded-task-plugin.js +44 -0
- package/dist/vite/client.js +2 -0
- package/dist/vitest-config.js +3 -0
- package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
- package/docs/content/deployment-environment-variables.mdx +1 -0
- package/docs/content/environment-variables.mdx +1 -0
- package/docs/content/sharing.mdx +36 -0
- package/docs/content/toolkit-sharing.mdx +98 -12
- package/package.json +5 -3
|
@@ -3,8 +3,10 @@ import { AGENT_CHAT_THREAD_ABSENCE_HEADER } from "../shared/agent-chat.js";
|
|
|
3
3
|
import { agentNativePath } from "./api-path.js";
|
|
4
4
|
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
5
5
|
const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
|
|
6
|
-
const
|
|
6
|
+
export const CHAT_THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
|
|
7
7
|
const THREADS_PAGE_SIZE = 50;
|
|
8
|
+
/** The server caps one page here; larger requests are silently shortened. */
|
|
9
|
+
export const CHAT_THREADS_MAX_PAGE_SIZE = 200;
|
|
8
10
|
const CLIENT_DRAFT_THREAD_PREFIX = "agent-chat-client-draft-thread:";
|
|
9
11
|
const MAX_THREAD_SAVE_RETRIES = 3;
|
|
10
12
|
const MAX_THREAD_CONFIRMATION_RETRIES = 2;
|
|
@@ -50,20 +52,126 @@ function clearClientDraftThreadMarker(id) {
|
|
|
50
52
|
function isUnconfirmedClientDraftThread(id, newlyCreated) {
|
|
51
53
|
return newlyCreated.has(id) || hasClientDraftThreadMarker(id);
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
/** The thread list endpoint answered with a failure status or a malformed body. */
|
|
56
|
+
export class ChatThreadListError extends Error {
|
|
57
|
+
status;
|
|
58
|
+
constructor(message, status = null) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "ChatThreadListError";
|
|
61
|
+
this.status = status;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const CHAT_THREAD_ACCESS_BASES = [
|
|
65
|
+
"owner",
|
|
66
|
+
"direct",
|
|
67
|
+
"organization",
|
|
68
|
+
];
|
|
69
|
+
const CHAT_THREAD_VISIBILITIES = [
|
|
70
|
+
"private",
|
|
71
|
+
"org",
|
|
72
|
+
"public",
|
|
73
|
+
];
|
|
74
|
+
function parseChatThreadListRow(row, ownership) {
|
|
75
|
+
const candidate = row;
|
|
76
|
+
if (!candidate ||
|
|
77
|
+
typeof candidate !== "object" ||
|
|
78
|
+
typeof candidate.id !== "string" ||
|
|
79
|
+
typeof candidate.ownerEmail !== "string" ||
|
|
80
|
+
!candidate.ownerEmail ||
|
|
81
|
+
!CHAT_THREAD_ACCESS_BASES.includes(candidate.accessBasis) ||
|
|
82
|
+
!CHAT_THREAD_VISIBILITIES.includes(candidate.visibility)) {
|
|
83
|
+
throw new ChatThreadListError("Chat history response included a thread without ownership metadata.");
|
|
84
|
+
}
|
|
85
|
+
// A row on the wrong side of the filter means the server and client
|
|
86
|
+
// disagree about what the list is; showing it would label someone else's
|
|
87
|
+
// chat as the caller's own, or the reverse.
|
|
88
|
+
const owned = candidate.accessBasis === "owner";
|
|
89
|
+
if ((ownership === "mine" && !owned) || (ownership === "shared" && owned)) {
|
|
90
|
+
throw new ChatThreadListError(`Chat history response included a thread outside the ${ownership} list.`);
|
|
91
|
+
}
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
async function requestChatThreadListPage(request) {
|
|
95
|
+
// Zero offsets keep their canonical URL omission, but invalid arguments
|
|
96
|
+
// must fail before serialization can turn them into a default page.
|
|
97
|
+
for (const [parameter, minimum] of [
|
|
98
|
+
["limit", 1],
|
|
99
|
+
["offset", 0],
|
|
100
|
+
]) {
|
|
101
|
+
const value = request[parameter];
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
105
|
+
throw new TypeError(`Chat history ${parameter} must be a safe integer of at least ${minimum}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const apiUrl = request.apiUrl ?? agentNativePath("/_agent-native/agent-chat");
|
|
54
109
|
const params = new URLSearchParams();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
110
|
+
const query = request.query?.trim();
|
|
111
|
+
if (query)
|
|
112
|
+
params.set("q", query);
|
|
113
|
+
if (request.limit)
|
|
114
|
+
params.set("limit", String(request.limit));
|
|
115
|
+
if (request.offset && request.offset > 0) {
|
|
116
|
+
params.set("offset", String(request.offset));
|
|
117
|
+
}
|
|
118
|
+
if (request.ownership && request.ownership !== "all") {
|
|
119
|
+
params.set("ownership", request.ownership);
|
|
120
|
+
}
|
|
121
|
+
if (request.includeExternal)
|
|
58
122
|
params.set("includeExternal", "1");
|
|
59
|
-
appendChatThreadScopeParams(params, scope);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
123
|
+
appendChatThreadScopeParams(params, request.scope);
|
|
124
|
+
const search = params.toString();
|
|
125
|
+
const res = await fetch(`${apiUrl}/threads${search ? `?${search}` : ""}`);
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
throw new ChatThreadListError(`Could not load chat history (HTTP ${res.status}).`, res.status);
|
|
128
|
+
}
|
|
129
|
+
const data = (await res.json());
|
|
130
|
+
if (!data || !Array.isArray(data.threads)) {
|
|
131
|
+
throw new ChatThreadListError("Chat history response did not include a thread list.", res.status);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
threads: data.threads,
|
|
135
|
+
hasMore: typeof data.hasMore === "boolean" ? data.hasMore : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* One page of the thread list or search, for the ownership-aware lists. Throws
|
|
140
|
+
* `ChatThreadListError` for a non-2xx response, a body that is not a thread
|
|
141
|
+
* list, or a row missing `ownerEmail` / `accessBasis` / `visibility` or sitting
|
|
142
|
+
* on the wrong side of `ownership`, so callers cannot mistake a failed page
|
|
143
|
+
* for an empty one. Invalid explicit pagination arguments reject with
|
|
144
|
+
* `TypeError` before a request is sent.
|
|
145
|
+
*
|
|
146
|
+
* The server caps each page at 200 rows. Advance offsets by the actual number
|
|
147
|
+
* of returned rows and use `hasMore`, rather than the requested limit.
|
|
148
|
+
*/
|
|
149
|
+
export async function fetchChatThreadListPage(request = {}) {
|
|
150
|
+
const page = await requestChatThreadListPage(request);
|
|
151
|
+
return {
|
|
152
|
+
threads: page.threads.map((row) => parseChatThreadListRow(row, request.ownership)),
|
|
153
|
+
hasMore: page.hasMore,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function fetchThreadListPage(apiUrl, offset, includeExternal, scope, ownership) {
|
|
157
|
+
// The lifecycle list keeps optimistic local drafts beside server rows, so it
|
|
158
|
+
// tolerates summaries without ownership metadata; the ownership-aware lists
|
|
159
|
+
// go through `fetchChatThreadListPage`, which requires it.
|
|
160
|
+
try {
|
|
161
|
+
const page = await requestChatThreadListPage({
|
|
162
|
+
apiUrl,
|
|
163
|
+
offset,
|
|
164
|
+
includeExternal,
|
|
165
|
+
scope,
|
|
166
|
+
ownership,
|
|
167
|
+
});
|
|
168
|
+
return page.threads;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (error instanceof ChatThreadListError)
|
|
63
172
|
return undefined;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
67
175
|
}
|
|
68
176
|
async function fetchThreadById(apiUrl, id, scope) {
|
|
69
177
|
try {
|
|
@@ -152,10 +260,15 @@ function withChatThreadScope(url, scope) {
|
|
|
152
260
|
return url;
|
|
153
261
|
return `${url}${url.includes("?") ? "&" : "?"}${query}`;
|
|
154
262
|
}
|
|
155
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Tell every mounted chat history list the thread set or its sharing changed,
|
|
265
|
+
* so they refetch. Anything that mutates a thread outside `useChatThreads`
|
|
266
|
+
* (a share grant, a visibility change) must call this.
|
|
267
|
+
*/
|
|
268
|
+
export function notifyChatThreadsUpdated() {
|
|
156
269
|
if (typeof window === "undefined")
|
|
157
270
|
return;
|
|
158
|
-
window.dispatchEvent(new CustomEvent(
|
|
271
|
+
window.dispatchEvent(new CustomEvent(CHAT_THREADS_UPDATED_EVENT));
|
|
159
272
|
}
|
|
160
273
|
function sortThreadSummaries(threads) {
|
|
161
274
|
return [...threads].sort((a, b) => {
|
|
@@ -240,6 +353,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
240
353
|
const autoCreate = options?.autoCreate !== false;
|
|
241
354
|
const restoreActiveThread = options?.restoreActiveThread !== false;
|
|
242
355
|
const includeExternal = options?.includeExternal === true;
|
|
356
|
+
const ownership = options?.ownership;
|
|
243
357
|
const isolateHistoryByScope = options?.isolateHistoryByScope === true;
|
|
244
358
|
const browserTabId = options?.browserTabId ??
|
|
245
359
|
(typeof window === "undefined" ? undefined : getBrowserTabId());
|
|
@@ -658,7 +772,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
658
772
|
let loadedThreads;
|
|
659
773
|
try {
|
|
660
774
|
const offset = options?.append ? nextThreadsOffsetRef.current : 0;
|
|
661
|
-
const loaded = await fetchThreadListPage(apiUrl, offset, includeExternal, historyScope);
|
|
775
|
+
const loaded = await fetchThreadListPage(apiUrl, offset, includeExternal, historyScope, ownership);
|
|
662
776
|
if (requestId !== latestFetchRequestRef.current)
|
|
663
777
|
return undefined;
|
|
664
778
|
if (!loaded) {
|
|
@@ -843,6 +957,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
843
957
|
historyScopeKey,
|
|
844
958
|
includeExternal,
|
|
845
959
|
isolateHistory,
|
|
960
|
+
ownership,
|
|
846
961
|
routeControlsActiveThread,
|
|
847
962
|
routeThreadId,
|
|
848
963
|
completeInitialThreadRestore,
|
|
@@ -1060,7 +1175,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1060
1175
|
else {
|
|
1061
1176
|
setThreads((prev) => prev.map((t) => (t.id === threadId ? { ...t, scope: null } : t)));
|
|
1062
1177
|
}
|
|
1063
|
-
|
|
1178
|
+
notifyChatThreadsUpdated();
|
|
1064
1179
|
}
|
|
1065
1180
|
catch {
|
|
1066
1181
|
await fetchThreads().catch(() => { });
|
|
@@ -1106,7 +1221,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1106
1221
|
if (pendingPinnedAtRef.current.get(threadId) === pinnedAt) {
|
|
1107
1222
|
pendingPinnedAtRef.current.delete(threadId);
|
|
1108
1223
|
}
|
|
1109
|
-
|
|
1224
|
+
notifyChatThreadsUpdated();
|
|
1110
1225
|
return true;
|
|
1111
1226
|
}
|
|
1112
1227
|
catch {
|
|
@@ -1155,7 +1270,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1155
1270
|
if (threadId === activeThreadIdRef.current) {
|
|
1156
1271
|
setActiveThreadId(null);
|
|
1157
1272
|
}
|
|
1158
|
-
|
|
1273
|
+
notifyChatThreadsUpdated();
|
|
1159
1274
|
return true;
|
|
1160
1275
|
}
|
|
1161
1276
|
catch {
|
|
@@ -1191,7 +1306,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1191
1306
|
await fetchThreads();
|
|
1192
1307
|
return false;
|
|
1193
1308
|
}
|
|
1194
|
-
|
|
1309
|
+
notifyChatThreadsUpdated();
|
|
1195
1310
|
return true;
|
|
1196
1311
|
}
|
|
1197
1312
|
catch {
|
|
@@ -1269,7 +1384,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1269
1384
|
await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(id)}`, historyScope), {
|
|
1270
1385
|
method: "DELETE",
|
|
1271
1386
|
});
|
|
1272
|
-
|
|
1387
|
+
notifyChatThreadsUpdated();
|
|
1273
1388
|
}
|
|
1274
1389
|
catch { }
|
|
1275
1390
|
clearUserRenamedThread(id);
|
|
@@ -1354,7 +1469,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1354
1469
|
if (data.threadData !== "") {
|
|
1355
1470
|
confirmThreadSnapshotPersisted(id);
|
|
1356
1471
|
}
|
|
1357
|
-
|
|
1472
|
+
notifyChatThreadsUpdated();
|
|
1358
1473
|
setThreads((prev) => {
|
|
1359
1474
|
const exists = prev.some((t) => t.id === id);
|
|
1360
1475
|
if (exists) {
|
|
@@ -1512,7 +1627,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1512
1627
|
},
|
|
1513
1628
|
...prev,
|
|
1514
1629
|
]);
|
|
1515
|
-
|
|
1630
|
+
notifyChatThreadsUpdated();
|
|
1516
1631
|
return t.id;
|
|
1517
1632
|
}
|
|
1518
1633
|
catch (err) {
|
|
@@ -1525,6 +1640,9 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1525
1640
|
const params = new URLSearchParams({ q: query });
|
|
1526
1641
|
if (includeExternal)
|
|
1527
1642
|
params.set("includeExternal", "1");
|
|
1643
|
+
if (ownership && ownership !== "all") {
|
|
1644
|
+
params.set("ownership", ownership);
|
|
1645
|
+
}
|
|
1528
1646
|
appendChatThreadScopeParams(params, historyScope);
|
|
1529
1647
|
const res = await fetch(`${apiUrl}/threads?${params.toString()}`);
|
|
1530
1648
|
if (!res.ok)
|
|
@@ -1536,7 +1654,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
1536
1654
|
catch {
|
|
1537
1655
|
return [];
|
|
1538
1656
|
}
|
|
1539
|
-
}, [apiUrl, historyScope, includeExternal, isolateHistory]);
|
|
1657
|
+
}, [apiUrl, historyScope, includeExternal, isolateHistory, ownership]);
|
|
1540
1658
|
const getThreadShareState = useCallback(async (threadId) => {
|
|
1541
1659
|
try {
|
|
1542
1660
|
const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, historyScope));
|
|
@@ -21,6 +21,7 @@ export declare function registerCollabActivityResource(resource: {
|
|
|
21
21
|
resourceType: string;
|
|
22
22
|
resourceId: string;
|
|
23
23
|
}): () => void;
|
|
24
|
+
export declare function registerOwnCollabSource(source: string): void;
|
|
24
25
|
type CollabActivityEvent = {
|
|
25
26
|
source?: string;
|
|
26
27
|
resourceType?: string;
|
|
@@ -39,6 +39,12 @@ let collabBoostActivityAt = 0;
|
|
|
39
39
|
// proves someone is editing the resource right now, so it boosts on its own.
|
|
40
40
|
const collabActivityUntilByResource = new Map();
|
|
41
41
|
const openCollabResources = new Map();
|
|
42
|
+
// The request sources this tab's collab connections have posted updates under.
|
|
43
|
+
// Apps choose their own (Content's `chat-…` tab id is not getBrowserTabId()),
|
|
44
|
+
// and a lone editor's every update comes back in its polls under that source.
|
|
45
|
+
// Never forget one when its connection closes: echoes of its last updates can
|
|
46
|
+
// arrive minutes later, while another doc on the resource is still open.
|
|
47
|
+
const ownCollabSources = new Set();
|
|
42
48
|
function collabResourceKey(resourceType, resourceId) {
|
|
43
49
|
return `${resourceType}\0${resourceId}`;
|
|
44
50
|
}
|
|
@@ -60,6 +66,9 @@ export function registerCollabActivityResource(resource) {
|
|
|
60
66
|
notifyCollabBoostChange();
|
|
61
67
|
};
|
|
62
68
|
}
|
|
69
|
+
export function registerOwnCollabSource(source) {
|
|
70
|
+
ownCollabSources.add(source);
|
|
71
|
+
}
|
|
63
72
|
function collabBoostWanted() {
|
|
64
73
|
const now = Date.now();
|
|
65
74
|
let hasRecentResourceActivity = false;
|
|
@@ -91,7 +100,9 @@ function noteCollaboratorActivity(events) {
|
|
|
91
100
|
typeof event.resourceType !== "string" ||
|
|
92
101
|
event.resourceType === "" ||
|
|
93
102
|
typeof event.resourceId !== "string" ||
|
|
94
|
-
event.requestSource === ownSource
|
|
103
|
+
event.requestSource === ownSource ||
|
|
104
|
+
(event.requestSource !== undefined &&
|
|
105
|
+
ownCollabSources.has(event.requestSource))) {
|
|
95
106
|
continue;
|
|
96
107
|
}
|
|
97
108
|
const key = collabResourceKey(event.resourceType, event.resourceId);
|
|
@@ -1144,6 +1155,7 @@ export function _resetSyncTransportRegistryForTests() {
|
|
|
1144
1155
|
collabBoostActivityAt = 0;
|
|
1145
1156
|
collabActivityUntilByResource.clear();
|
|
1146
1157
|
openCollabResources.clear();
|
|
1158
|
+
ownCollabSources.clear();
|
|
1147
1159
|
}
|
|
1148
1160
|
export function subscribeSyncEvents(options) {
|
|
1149
1161
|
const pollUrl = agentNativePath(options.pollUrl ?? "/_agent-native/poll");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const tasks: Record<string, (input: never) => Promise<import("../live-copy-tasks.js").LiveCopyTaskResult>>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { defineLiveCopyTasks } from "../live-copy-tasks.js";
|
|
3
|
+
// A tiny live-copy task module for the live-copy spec: a body is a list of
|
|
4
|
+
// paragraphs, and anything else is refused rather than dropped. Paragraphs
|
|
5
|
+
// and seed sources starting with "!" drive the failure cases.
|
|
6
|
+
function paragraph(text) {
|
|
7
|
+
const element = new Y.XmlElement("paragraph");
|
|
8
|
+
const content = new Y.XmlText();
|
|
9
|
+
content.insert(0, text);
|
|
10
|
+
element.insert(0, [content]);
|
|
11
|
+
return element;
|
|
12
|
+
}
|
|
13
|
+
/** An update whose base this document never had. */
|
|
14
|
+
function orphanUpdate() {
|
|
15
|
+
const elsewhere = new Y.Doc();
|
|
16
|
+
elsewhere.getXmlFragment("default").insert(0, [paragraph("unknown base")]);
|
|
17
|
+
const before = Y.encodeStateVector(elsewhere);
|
|
18
|
+
const fragment = elsewhere.getXmlFragment("default");
|
|
19
|
+
fragment.insert(1, [paragraph("orphan")]);
|
|
20
|
+
return Y.encodeStateAsUpdate(elsewhere, before);
|
|
21
|
+
}
|
|
22
|
+
export const tasks = defineLiveCopyTasks({
|
|
23
|
+
derive(doc, field) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
for (const node of doc.getXmlFragment(field).toArray()) {
|
|
26
|
+
if (!(node instanceof Y.XmlElement) || node.nodeName !== "paragraph") {
|
|
27
|
+
return {
|
|
28
|
+
kind: "refused",
|
|
29
|
+
reason: "unsupported_node",
|
|
30
|
+
detail: node instanceof Y.XmlElement ? node.nodeName : "text",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const text = node
|
|
34
|
+
.toArray()
|
|
35
|
+
.map((child) => child.toString())
|
|
36
|
+
.join("");
|
|
37
|
+
if (text === "!spin") {
|
|
38
|
+
for (;;) {
|
|
39
|
+
// never returns
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (text === "!html")
|
|
43
|
+
return { kind: "html" };
|
|
44
|
+
if (text === "!edit")
|
|
45
|
+
doc.getXmlFragment(field).insert(0, [paragraph("")]);
|
|
46
|
+
if (text === "!park")
|
|
47
|
+
Y.applyUpdate(doc, orphanUpdate());
|
|
48
|
+
lines.push(text);
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
},
|
|
52
|
+
seed(doc, field, source) {
|
|
53
|
+
if (source === "!refuse")
|
|
54
|
+
return { kind: "refused", reason: "too_complex" };
|
|
55
|
+
if (source === "!empty")
|
|
56
|
+
return;
|
|
57
|
+
if (source === "!junk")
|
|
58
|
+
return { kind: "update" };
|
|
59
|
+
if (source === "!stray")
|
|
60
|
+
doc.getText("other").insert(0, "hidden");
|
|
61
|
+
doc.getXmlFragment(field).insert(0, source.split("\n").map(paragraph));
|
|
62
|
+
},
|
|
63
|
+
edits: {
|
|
64
|
+
append(doc, field, input) {
|
|
65
|
+
const fragment = doc.getXmlFragment(field);
|
|
66
|
+
const text = input?.text ?? "";
|
|
67
|
+
fragment.insert(fragment.length, [paragraph(text)]);
|
|
68
|
+
},
|
|
69
|
+
clear(doc, field) {
|
|
70
|
+
const fragment = doc.getXmlFragment(field);
|
|
71
|
+
fragment.delete(0, fragment.length);
|
|
72
|
+
},
|
|
73
|
+
stray(doc) {
|
|
74
|
+
doc.getText("other").insert(0, "hidden");
|
|
75
|
+
},
|
|
76
|
+
unchanged() { },
|
|
77
|
+
refuse() {
|
|
78
|
+
return { kind: "refused", reason: "too_complex" };
|
|
79
|
+
},
|
|
80
|
+
junk() {
|
|
81
|
+
return { kind: "update" };
|
|
82
|
+
},
|
|
83
|
+
dependent(doc) {
|
|
84
|
+
Y.applyUpdate(doc, orphanUpdate());
|
|
85
|
+
},
|
|
86
|
+
dependentAndValid(doc, field) {
|
|
87
|
+
Y.applyUpdate(doc, orphanUpdate());
|
|
88
|
+
const fragment = doc.getXmlFragment(field);
|
|
89
|
+
fragment.insert(fragment.length, [paragraph("valid")]);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
@@ -38,19 +38,19 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
38
38
|
error: string;
|
|
39
39
|
states?: undefined;
|
|
40
40
|
} | {
|
|
41
|
-
error?: undefined;
|
|
42
41
|
states: {
|
|
43
42
|
clientId: number;
|
|
44
43
|
state: string;
|
|
45
44
|
}[];
|
|
45
|
+
error?: undefined;
|
|
46
46
|
}>>;
|
|
47
47
|
export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
48
48
|
error: string;
|
|
49
49
|
users?: undefined;
|
|
50
50
|
} | {
|
|
51
|
-
error?: undefined;
|
|
52
51
|
users: {
|
|
53
52
|
clientId: number;
|
|
54
53
|
lastSeen: number;
|
|
55
54
|
}[];
|
|
55
|
+
error?: undefined;
|
|
56
56
|
}>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BoundedTaskSource } from "./bounded-task-source.js";
|
|
2
|
+
export interface BundleBoundedTaskSourceOptions {
|
|
3
|
+
/** Shown in errors and runner names. Defaults to the entry file's name. */
|
|
4
|
+
name?: string;
|
|
5
|
+
/** Extra esbuild `conditions` for package resolution. */
|
|
6
|
+
conditions?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface BundledBoundedTaskSource extends BoundedTaskSource {
|
|
9
|
+
/** Every file esbuild read, for watch-mode rebuilds. */
|
|
10
|
+
readonly inputs: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Bundles a task module and everything it imports into one script a worker
|
|
14
|
+
* can evaluate. The module must export `tasks`: an object of functions that
|
|
15
|
+
* take plain data and return plain data (or a Promise of it).
|
|
16
|
+
*/
|
|
17
|
+
export declare function bundleBoundedTaskSource(entry: string | URL, options?: BundleBoundedTaskSourceOptions): Promise<BundledBoundedTaskSource>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { BOUNDED_TASK_GLOBAL, } from "./bounded-task-source.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bundles a task module and everything it imports into one script a worker
|
|
6
|
+
* can evaluate. The module must export `tasks`: an object of functions that
|
|
7
|
+
* take plain data and return plain data (or a Promise of it).
|
|
8
|
+
*/
|
|
9
|
+
export async function bundleBoundedTaskSource(entry, options = {}) {
|
|
10
|
+
const entryPath = entry instanceof URL || entry.startsWith("file:")
|
|
11
|
+
? fileURLToPath(entry)
|
|
12
|
+
: path.resolve(entry);
|
|
13
|
+
const esbuild = await import("esbuild");
|
|
14
|
+
const result = await esbuild.build({
|
|
15
|
+
entryPoints: [entryPath],
|
|
16
|
+
bundle: true,
|
|
17
|
+
write: false,
|
|
18
|
+
format: "iife",
|
|
19
|
+
globalName: BOUNDED_TASK_GLOBAL,
|
|
20
|
+
platform: "node",
|
|
21
|
+
target: "node20",
|
|
22
|
+
metafile: true,
|
|
23
|
+
sourcemap: false,
|
|
24
|
+
legalComments: "none",
|
|
25
|
+
logLevel: "silent",
|
|
26
|
+
...(options.conditions ? { conditions: options.conditions } : {}),
|
|
27
|
+
});
|
|
28
|
+
const output = result.outputFiles?.[0];
|
|
29
|
+
if (!output) {
|
|
30
|
+
throw new Error(`esbuild produced no output for bounded task ${entryPath}`);
|
|
31
|
+
}
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
return {
|
|
34
|
+
kind: "bounded-task-source",
|
|
35
|
+
name: options.name ?? path.basename(entryPath).replace(/\.[^.]+$/, ""),
|
|
36
|
+
code: output.text,
|
|
37
|
+
inputs: Object.keys(result.metafile?.inputs ?? {}).map((input) => path.resolve(cwd, input)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier the bundled task module is assigned to. `bundleBoundedTaskSource`
|
|
3
|
+
* emits an IIFE with this global name; the worker host reads it back.
|
|
4
|
+
*/
|
|
5
|
+
export declare const BOUNDED_TASK_GLOBAL = "__agentNativeBoundedTasks";
|
|
6
|
+
/**
|
|
7
|
+
* A self-contained task module, bundled to one script. Produce it with
|
|
8
|
+
* `bundleBoundedTaskSource` (Node, esbuild); never hand-write `code`.
|
|
9
|
+
*/
|
|
10
|
+
export interface BoundedTaskSource {
|
|
11
|
+
readonly kind: "bounded-task-source";
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly code: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The bundler and the vitest base config import this, not bounded-tasks.ts:
|
|
2
|
+
// packages that typecheck @agent-native/core/vitest-config from source load
|
|
3
|
+
// several @types/node versions, and node:worker_threads does not type there.
|
|
4
|
+
/**
|
|
5
|
+
* Identifier the bundled task module is assigned to. `bundleBoundedTaskSource`
|
|
6
|
+
* emits an IIFE with this global name; the worker host reads it back.
|
|
7
|
+
*/
|
|
8
|
+
export const BOUNDED_TASK_GLOBAL = "__agentNativeBoundedTasks";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BOUNDED_TASK_GLOBAL, type BoundedTaskSource } from "./bounded-task-source.js";
|
|
2
|
+
export { BOUNDED_TASK_GLOBAL, type BoundedTaskSource };
|
|
3
|
+
export type BoundedTaskErrorCode = "unavailable" | "closed" | "queue_full" | "queue_timeout" | "invalid_input" | "input_too_large" | "startup_failed" | "task_unknown" | "task_threw" | "deadline_exceeded" | "memory_exceeded" | "invalid_output" | "output_too_large" | "worker_crashed";
|
|
4
|
+
export declare class BoundedTaskError extends Error {
|
|
5
|
+
readonly code: BoundedTaskErrorCode;
|
|
6
|
+
readonly task: string;
|
|
7
|
+
/** `name` of the error the task threw, for `task_threw`. */
|
|
8
|
+
readonly taskErrorName?: string;
|
|
9
|
+
constructor(code: BoundedTaskErrorCode, task: string, message: string, options?: {
|
|
10
|
+
cause?: unknown;
|
|
11
|
+
taskErrorName?: string;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export interface BoundedTaskRunnerOptions {
|
|
15
|
+
source: BoundedTaskSource;
|
|
16
|
+
/** Wall-clock budget for one task once a worker has it. Default 2000 ms. */
|
|
17
|
+
deadlineMs?: number;
|
|
18
|
+
/** Budget for a fresh worker to evaluate the bundle. Default 10000 ms. */
|
|
19
|
+
startupTimeoutMs?: number;
|
|
20
|
+
/** Workers alive at once; also the number of tasks running at once. Default 1. */
|
|
21
|
+
maxWorkers?: number;
|
|
22
|
+
/** Tasks allowed to wait for a worker. Beyond this, `queue_full`. Default 8. */
|
|
23
|
+
maxQueue?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Longest a task may wait for a busy or terminating worker. A task waiting
|
|
26
|
+
* only for a worker to start is bounded by `startupTimeoutMs` instead.
|
|
27
|
+
* Default `deadlineMs`.
|
|
28
|
+
*/
|
|
29
|
+
queueTimeoutMs?: number;
|
|
30
|
+
/** Strings count two bytes per UTF-16 unit. Default 8 MiB. */
|
|
31
|
+
maxInputBytes?: number;
|
|
32
|
+
/** Checked inside the worker before the result is copied out. Default 16 MiB. */
|
|
33
|
+
maxOutputBytes?: number;
|
|
34
|
+
/** Worker old-generation heap. Default 128 MiB. */
|
|
35
|
+
memoryLimitMb?: number;
|
|
36
|
+
/** Default 4 MiB. */
|
|
37
|
+
stackSizeMb?: number;
|
|
38
|
+
/** An idle worker is stopped after this long. Default 30000 ms. */
|
|
39
|
+
idleTimeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface BoundedTaskRunnerStats {
|
|
42
|
+
workers: number;
|
|
43
|
+
running: number;
|
|
44
|
+
queued: number;
|
|
45
|
+
}
|
|
46
|
+
export interface BoundedTaskRunner {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
run<Output = unknown>(task: string, input: unknown): Promise<Output>;
|
|
49
|
+
stats(): BoundedTaskRunnerStats;
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Size of a structured-clone payload in bytes, or -1 when it holds anything
|
|
54
|
+
* but plain data (or more than 200,000 values). Stops counting once
|
|
55
|
+
* `limit` is passed. The worker host evaluates this function's source, so it
|
|
56
|
+
* must not reference anything outside its own body.
|
|
57
|
+
*/
|
|
58
|
+
export declare function measureBoundedPayload(value: unknown, limit: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* Runs functions from one bundled module in worker threads, each under a
|
|
61
|
+
* terminable deadline and a heap limit, with a fixed number of workers and a
|
|
62
|
+
* bounded wait queue. A task that overruns is stopped by terminating its
|
|
63
|
+
* worker, so synchronous work in the task never blocks the caller's event
|
|
64
|
+
* loop. Where worker threads do not exist (Cloudflare Workers), every run
|
|
65
|
+
* fails with `unavailable`; it never falls back to running inline.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createBoundedTaskRunner(options: BoundedTaskRunnerOptions): BoundedTaskRunner;
|