@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.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/app-config/analytics.d.ts +1 -0
  3. package/dist/app-config/analytics.js +7 -0
  4. package/dist/app-config/schema.d.ts +1 -0
  5. package/dist/chat-threads/store.d.ts +28 -2
  6. package/dist/chat-threads/store.js +75 -13
  7. package/dist/cli/template-baseline.js +18 -5
  8. package/dist/cli/template-sync.js +100 -19
  9. package/dist/client/agent-chat/index.d.ts +2 -1
  10. package/dist/client/agent-chat/index.js +2 -1
  11. package/dist/client/chat/agentkit-protocol.js +21 -1
  12. package/dist/client/chat/run-outcome.d.ts +1 -0
  13. package/dist/client/chat/runtime.js +6 -0
  14. package/dist/client/sharing/index.d.ts +2 -1
  15. package/dist/client/sharing/index.js +2 -1
  16. package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
  17. package/dist/client/sharing/share-controller-helpers.js +46 -1
  18. package/dist/client/sharing/useShareButtonController.d.ts +12 -0
  19. package/dist/client/sharing/useShareButtonController.js +76 -57
  20. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  21. package/dist/client/sharing/useShareDialogController.js +77 -49
  22. package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
  23. package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
  24. package/dist/client/use-chat-thread-list.d.ts +40 -0
  25. package/dist/client/use-chat-thread-list.js +210 -0
  26. package/dist/client/use-chat-threads.d.ts +67 -0
  27. package/dist/client/use-chat-threads.js +141 -23
  28. package/dist/client/use-db-sync.d.ts +1 -0
  29. package/dist/client/use-db-sync.js +13 -1
  30. package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
  31. package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/bounded-task-bundle.d.ts +17 -0
  34. package/dist/collab/bounded-task-bundle.js +39 -0
  35. package/dist/collab/bounded-task-source.d.ts +14 -0
  36. package/dist/collab/bounded-task-source.js +8 -0
  37. package/dist/collab/bounded-tasks.d.ts +67 -0
  38. package/dist/collab/bounded-tasks.js +439 -0
  39. package/dist/collab/client.js +2 -1
  40. package/dist/collab/index.d.ts +5 -2
  41. package/dist/collab/index.js +5 -2
  42. package/dist/collab/live-copy-tasks.d.ts +80 -0
  43. package/dist/collab/live-copy-tasks.js +330 -0
  44. package/dist/collab/live-copy.d.ts +188 -0
  45. package/dist/collab/live-copy.js +415 -0
  46. package/dist/collab/routes.d.ts +5 -0
  47. package/dist/collab/routes.js +24 -8
  48. package/dist/collab/storage.d.ts +53 -1
  49. package/dist/collab/storage.js +93 -5
  50. package/dist/collab/struct-routes.d.ts +1 -1
  51. package/dist/collab/ydoc-manager.d.ts +92 -3
  52. package/dist/collab/ydoc-manager.js +447 -38
  53. package/dist/db/client.d.ts +6 -0
  54. package/dist/db/client.js +25 -5
  55. package/dist/deploy/build.js +2 -0
  56. package/dist/extensions/actions.js +1 -0
  57. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  58. package/dist/mcp/agent-connected.js +5 -1
  59. package/dist/mcp/build-server.js +39 -10
  60. package/dist/notifications/routes.d.ts +1 -1
  61. package/dist/observability/routes.d.ts +1 -1
  62. package/dist/org/index.d.ts +2 -2
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  65. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  66. package/dist/server/agent-chat-plugin.js +80 -17
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  69. package/dist/server/analytics.d.ts +1 -0
  70. package/dist/server/analytics.js +18 -1
  71. package/dist/server/collab-plugin.d.ts +10 -1
  72. package/dist/server/collab-plugin.js +89 -20
  73. package/dist/server/index.d.ts +1 -1
  74. package/dist/server/realtime-token.d.ts +1 -1
  75. package/dist/shared/optional-node-builtins.d.ts +1 -0
  76. package/dist/shared/optional-node-builtins.js +3 -0
  77. package/dist/sharing/access.d.ts +1 -0
  78. package/dist/sharing/access.js +113 -95
  79. package/dist/tracking/providers.d.ts +1 -0
  80. package/dist/tracking/providers.js +2 -1
  81. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  82. package/dist/vite/bounded-task-plugin.d.ts +22 -0
  83. package/dist/vite/bounded-task-plugin.js +44 -0
  84. package/dist/vite/client.js +2 -0
  85. package/dist/vitest-config.js +3 -0
  86. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
  87. package/docs/content/deployment-environment-variables.mdx +1 -0
  88. package/docs/content/environment-variables.mdx +1 -0
  89. package/docs/content/sharing.mdx +36 -0
  90. package/docs/content/toolkit-sharing.mdx +98 -12
  91. 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 THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
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
- async function fetchThreadListPage(apiUrl, offset, includeExternal, scope) {
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
- if (offset > 0)
56
- params.set("offset", String(offset));
57
- if (includeExternal)
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 query = params.toString();
61
- return fetch(`${apiUrl}/threads${query ? `?${query}` : ""}`).then(async (res) => {
62
- if (!res.ok)
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
- const data = await res.json();
65
- return (data.threads ?? []);
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
- function emitThreadsUpdated() {
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(THREADS_UPDATED_EVENT));
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
- emitThreadsUpdated();
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
- emitThreadsUpdated();
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
- emitThreadsUpdated();
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
- emitThreadsUpdated();
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
- emitThreadsUpdated();
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
- emitThreadsUpdated();
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
- emitThreadsUpdated();
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;