@agent-native/core 0.200.0-nightly-20261002124406 → 0.200.0

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 (48) hide show
  1. package/dist/agent/production-agent.d.ts +5 -1
  2. package/dist/agent/production-agent.js +36 -1
  3. package/dist/app-config/runtime.d.ts +1 -0
  4. package/dist/app-config/runtime.js +4 -0
  5. package/dist/app-config/schema.d.ts +1 -0
  6. package/dist/cli/design-connect.d.ts +1 -0
  7. package/dist/cli/design-connect.js +147 -7
  8. package/dist/client/analytics.js +24 -1
  9. package/dist/client/session-replay.d.ts +6 -0
  10. package/dist/client/session-replay.js +47 -5
  11. package/dist/collab/awareness.d.ts +2 -2
  12. package/dist/collab/routes.d.ts +1 -1
  13. package/dist/file-upload/builder.js +20 -2
  14. package/dist/mcp-client/app-api.d.ts +11 -1
  15. package/dist/mcp-client/app-api.js +17 -3
  16. package/dist/mcp-client/index.d.ts +1 -1
  17. package/dist/mcp-client/manager.d.ts +3 -1
  18. package/dist/mcp-client/manager.js +4 -0
  19. package/dist/notifications/routes.d.ts +3 -3
  20. package/dist/observability/metrics.js +14 -7
  21. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  22. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  23. package/dist/resource-changes/store.d.ts +166 -0
  24. package/dist/resource-changes/store.js +499 -0
  25. package/dist/search/index-store.d.ts +9 -0
  26. package/dist/search/index-store.js +50 -0
  27. package/dist/search/index.d.ts +5 -1
  28. package/dist/search/index.js +5 -0
  29. package/dist/search/indexer.d.ts +32 -0
  30. package/dist/search/indexer.js +651 -0
  31. package/dist/search/query-parser.d.ts +29 -0
  32. package/dist/search/query-parser.js +71 -0
  33. package/dist/search/query.d.ts +46 -0
  34. package/dist/search/query.js +149 -0
  35. package/dist/search/registry.d.ts +55 -0
  36. package/dist/search/registry.js +93 -0
  37. package/dist/search/tokenize.d.ts +103 -0
  38. package/dist/search/tokenize.js +367 -0
  39. package/dist/server/action-change-marker-write.js +5 -1
  40. package/dist/server/agent-chat/run-code-tools.d.ts +7 -0
  41. package/dist/server/agent-chat/run-code-tools.js +7 -0
  42. package/dist/server/agent-chat-plugin.d.ts +5 -1
  43. package/dist/server/agent-chat-plugin.js +18 -12
  44. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  45. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  46. package/dist/server/release-schema.js +8 -0
  47. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  48. package/package.json +6 -9
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Search tokens, computed in JavaScript for both the index and the query.
3
+ *
4
+ * Postgres's text parser depends on the database's locale: on some locales it
5
+ * drops Japanese text entirely, and PGlite and Neon don't agree. So core never
6
+ * asks Postgres to tokenize. It builds `tsvector` and `tsquery` literals
7
+ * itself, and the database only stores, indexes, and matches them. The same
8
+ * code tokenizes documents and queries, so they always agree.
9
+ *
10
+ * Rules:
11
+ * - Text is NFKC-normalized and lowercased. There is no stemming and there are
12
+ * no stopwords.
13
+ * - A word is a run of letters, numbers, and combining marks. Everything else
14
+ * separates words, so `snake_case`, `kebab-case`, URLs, and paths become
15
+ * their parts at consecutive positions, and a query for the same text
16
+ * matches them as a phrase.
17
+ * - A camelCase or PascalCase word is indexed whole and as its parts, so
18
+ * "camelCase", "camel", and "case camel" all find it.
19
+ * - A word longer than Postgres's 2,046-byte limit keeps its longest prefix
20
+ * that fits.
21
+ * - Chinese, Japanese, and Korean runs become overlapping character pairs,
22
+ * because they have no spaces to split on. A query becomes the same pairs
23
+ * as a phrase, which matches exactly that substring. A run's last character
24
+ * starts no pair, so it is also indexed alone, and a one-character query
25
+ * finds every character as a prefix.
26
+ */
27
+ const WORD = /[\p{L}\p{N}\p{M}]+/gu;
28
+ const CJK = /[\p{Script_Extensions=Han}\p{Script_Extensions=Hiragana}\p{Script_Extensions=Katakana}\p{Script_Extensions=Hangul}]/u;
29
+ const UPPERCASE = /\p{Lu}/u;
30
+ const CAMEL_LOWER_UPPER = /([\p{Ll}\p{N}])(\p{Lu})/gu;
31
+ const CAMEL_ACRONYM = /(\p{Lu})(\p{Lu}\p{Ll})/gu;
32
+ /** Postgres limits, from tsvector.h. */
33
+ const MAX_POSITION = 16_383;
34
+ const MAX_POSITIONS_PER_LEXEME = 255;
35
+ const MAX_LEXEME_BYTES = 2_046;
36
+ /**
37
+ * Postgres evaluates a phrase recursively, one level per word, and a phrase
38
+ * of about 10,000 words exhausts the stack: native Postgres errors and PGlite
39
+ * silently returns no rows. `termTsquery` refuses a longer term.
40
+ */
41
+ const MAX_QUERY_LEXEMES = 2_048;
42
+ /**
43
+ * Budget for a vector's estimated size. Postgres rejects a vector whose
44
+ * lexemes alone take 1 MB, so this stays well under that.
45
+ */
46
+ const MAX_VECTOR_BYTES = 900_000;
47
+ /** Per-lexeme overhead: its entry and position count. */
48
+ const LEXEME_OVERHEAD_BYTES = 6;
49
+ /** One stored position. */
50
+ const POSITION_BYTES = 2;
51
+ /** Normalized form used for title and summary comparisons. */
52
+ export function normalizeSearchText(value) {
53
+ return value.normalize("NFKC").trim().toLowerCase().replace(/\s+/g, " ");
54
+ }
55
+ function segments(word) {
56
+ if (!CJK.test(word))
57
+ return [{ text: word, cjk: false }];
58
+ const out = [];
59
+ for (const char of word) {
60
+ const cjk = CJK.test(char);
61
+ const last = out[out.length - 1];
62
+ if (last && last.cjk === cjk)
63
+ last.text += char;
64
+ else
65
+ out.push({ text: char, cjk });
66
+ }
67
+ return out;
68
+ }
69
+ function cjkPairs(text) {
70
+ const chars = Array.from(text);
71
+ if (chars.length < 2)
72
+ return chars;
73
+ const pairs = [];
74
+ for (let index = 0; index < chars.length - 1; index += 1) {
75
+ pairs.push(chars[index] + chars[index + 1]);
76
+ }
77
+ return pairs;
78
+ }
79
+ function camelParts(word) {
80
+ if (!UPPERCASE.test(word))
81
+ return [word];
82
+ return word
83
+ .replace(CAMEL_LOWER_UPPER, "$1\u0000$2")
84
+ .replace(CAMEL_ACRONYM, "$1\u0000$2")
85
+ .split("\u0000")
86
+ .filter(Boolean);
87
+ }
88
+ /** UTF-8 length without encoding: 1-3 bytes per UTF-16 unit, 4 per pair. */
89
+ export function utf8ByteLength(value) {
90
+ let bytes = 0;
91
+ for (let index = 0; index < value.length; index += 1) {
92
+ const code = value.charCodeAt(index);
93
+ if (code < 0x80)
94
+ bytes += 1;
95
+ else if (code < 0x800)
96
+ bytes += 2;
97
+ else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
98
+ bytes += 4;
99
+ index += 1;
100
+ }
101
+ else
102
+ bytes += 3;
103
+ }
104
+ return bytes;
105
+ }
106
+ /**
107
+ * A word longer than Postgres allows keeps its longest prefix that fits, the
108
+ * same in documents and queries, so it still matches and still holds its
109
+ * place in a phrase.
110
+ */
111
+ function fitLexeme(lexeme) {
112
+ if (lexeme.length * 3 <= MAX_LEXEME_BYTES)
113
+ return lexeme;
114
+ let bytes = 0;
115
+ let end = 0;
116
+ for (const char of lexeme) {
117
+ bytes += utf8ByteLength(char);
118
+ if (bytes > MAX_LEXEME_BYTES)
119
+ break;
120
+ end += char.length;
121
+ }
122
+ return lexeme.slice(0, end);
123
+ }
124
+ /**
125
+ * Document tokens with positions starting at `start`. Returns the next free
126
+ * position so several fields can share one position space.
127
+ */
128
+ export function documentTokens(text, start = 1) {
129
+ const tokens = [];
130
+ const next = eachDocumentToken(text, start, (lexeme, position) => {
131
+ tokens.push({ lexeme, position });
132
+ });
133
+ return { tokens, next };
134
+ }
135
+ /**
136
+ * Calls `visit` with each document token, in order, without collecting
137
+ * them: a large document has millions. Returns the next free position.
138
+ */
139
+ function eachDocumentToken(text, start, visit) {
140
+ let position = start;
141
+ const push = (lexeme, at) => visit(fitLexeme(lexeme), at);
142
+ for (const match of text.normalize("NFKC").matchAll(WORD)) {
143
+ for (const segment of segments(match[0])) {
144
+ if (segment.cjk) {
145
+ const pairs = cjkPairs(segment.text);
146
+ for (const pair of pairs)
147
+ push(pair, position++);
148
+ // The last character also stands alone, at the last pair's position.
149
+ const last = Array.from(segment.text).at(-1);
150
+ if (pairs.at(-1) !== last)
151
+ push(last, position - 1);
152
+ continue;
153
+ }
154
+ const parts = camelParts(segment.text);
155
+ const whole = segment.text.toLowerCase();
156
+ if (parts.length < 2) {
157
+ push(whole, position++);
158
+ continue;
159
+ }
160
+ // The whole word sits at its first and last part's positions, so a
161
+ // phrase can reach it from either side.
162
+ const first = position;
163
+ for (const part of parts)
164
+ push(part.toLowerCase(), position++);
165
+ push(whole, first);
166
+ push(whole, position - 1);
167
+ }
168
+ }
169
+ return position;
170
+ }
171
+ function queryWords(text) {
172
+ const words = [];
173
+ for (const match of text.normalize("NFKC").matchAll(WORD)) {
174
+ for (const segment of segments(match[0])) {
175
+ if (segment.cjk) {
176
+ for (const pair of cjkPairs(segment.text)) {
177
+ words.push({ lexeme: pair, parts: [] });
178
+ }
179
+ continue;
180
+ }
181
+ const parts = camelParts(segment.text);
182
+ words.push({
183
+ lexeme: fitLexeme(segment.text.toLowerCase()),
184
+ parts: parts.length < 2
185
+ ? []
186
+ : parts.map((part) => fitLexeme(part.toLowerCase())),
187
+ });
188
+ }
189
+ }
190
+ return words;
191
+ }
192
+ /**
193
+ * Query lexemes for one term, in order. Unlike documents, a query word is
194
+ * never split on case: "camelCase" looks for the whole word, which documents
195
+ * index alongside its parts.
196
+ */
197
+ export function queryLexemes(text) {
198
+ return queryWords(text).map((word) => word.lexeme);
199
+ }
200
+ function quoteLexeme(lexeme) {
201
+ return `'${lexeme.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
202
+ }
203
+ /** Whether this entry starts a run of one weight; entries are in field order. */
204
+ function startsWeightRun(list, index) {
205
+ return index === 0 || list[index].weight !== list[index - 1].weight;
206
+ }
207
+ /**
208
+ * Adds a word's next position within Postgres's 255. The word's first
209
+ * position in each field always stays, so it still counts as being in that
210
+ * field. Returns false when a position had to be dropped.
211
+ */
212
+ function addPosition(list, entry) {
213
+ const previous = list[list.length - 1];
214
+ if (previous.position === entry.position && previous.weight === entry.weight)
215
+ return true;
216
+ if (list.length < MAX_POSITIONS_PER_LEXEME) {
217
+ list.push(entry);
218
+ return true;
219
+ }
220
+ if (previous.weight === entry.weight)
221
+ return false;
222
+ for (let index = list.length - 1; index > 0; index -= 1) {
223
+ if (!startsWeightRun(list, index)) {
224
+ list.splice(index, 1);
225
+ list.push(entry);
226
+ return false;
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ /**
232
+ * A `tsvector` for the fields, in order, sharing one position space with a
233
+ * gap between fields, so a phrase never spans two of them. Postgres keeps at
234
+ * most 255 positions per word and none past 16,383. Past the last position,
235
+ * each field's words collapse onto its own position, because Postgres merges
236
+ * equal positions and keeps only the higher weight. A word over 255
237
+ * positions keeps its first position in each field. A very large document
238
+ * keeps one position per word in each field, and one with more distinct
239
+ * words than fit keeps the words that come first, so the vector stays under
240
+ * Postgres's size limit. Any of these makes `positionsComplete` false; each
241
+ * keeps which fields hold each word.
242
+ */
243
+ export function buildSearchVector(fields) {
244
+ const entries = new Map();
245
+ let position = 1;
246
+ let positionsComplete = true;
247
+ // The smallest the vector can be: each word once. A word past the budget
248
+ // can never be kept, so from then on new words aren't collected.
249
+ let minimumBytes = 0;
250
+ let full = false;
251
+ fields.forEach((field, fieldIndex) => {
252
+ if (!field.text)
253
+ return;
254
+ const ceiling = MAX_POSITION - (fields.length - 1 - fieldIndex);
255
+ const next = eachDocumentToken(field.text, position, (lexeme, at) => {
256
+ if (at > ceiling)
257
+ positionsComplete = false;
258
+ const entry = { position: Math.min(at, ceiling), weight: field.weight };
259
+ const list = entries.get(lexeme);
260
+ if (list) {
261
+ if (!addPosition(list, entry))
262
+ positionsComplete = false;
263
+ return;
264
+ }
265
+ const bytes = utf8ByteLength(lexeme) + LEXEME_OVERHEAD_BYTES + POSITION_BYTES;
266
+ if (full || minimumBytes + bytes > MAX_VECTOR_BYTES) {
267
+ full = true;
268
+ return;
269
+ }
270
+ minimumBytes += bytes;
271
+ entries.set(lexeme, [entry]);
272
+ });
273
+ if (next > position)
274
+ position = next + 1;
275
+ });
276
+ let positionCount = 0;
277
+ for (const positions of entries.values())
278
+ positionCount += positions.length;
279
+ const keepAllPositions = minimumBytes + (positionCount - entries.size) * POSITION_BYTES <=
280
+ MAX_VECTOR_BYTES;
281
+ const parts = [];
282
+ let total = 0;
283
+ let keptEveryLexeme = !full;
284
+ for (const [lexeme, positions] of entries) {
285
+ const kept = keepAllPositions
286
+ ? positions
287
+ : positions.filter((_, index) => startsWeightRun(positions, index));
288
+ total +=
289
+ utf8ByteLength(lexeme) +
290
+ LEXEME_OVERHEAD_BYTES +
291
+ kept.length * POSITION_BYTES;
292
+ if (total > MAX_VECTOR_BYTES) {
293
+ keptEveryLexeme = false;
294
+ break;
295
+ }
296
+ parts.push(`${quoteLexeme(lexeme)}:${kept
297
+ .map((entry) => `${entry.position}${entry.weight}`)
298
+ .join(",")}`);
299
+ }
300
+ return {
301
+ literal: parts.join(" "),
302
+ positionsComplete: positionsComplete && keepAllPositions && keptEveryLexeme,
303
+ };
304
+ }
305
+ /**
306
+ * A term with more words than the index can match. The query can't be
307
+ * answered from the index; the app's fallback search can answer it.
308
+ */
309
+ export class SearchTermTooLongError extends RangeError {
310
+ constructor() {
311
+ super(`A search term can have at most ${MAX_QUERY_LEXEMES} words.`);
312
+ this.name = "SearchTermTooLongError";
313
+ }
314
+ }
315
+ /**
316
+ * One term as a `tsquery` literal: a single lexeme, or a phrase of adjacent
317
+ * lexemes. Returns null when the term has nothing to match. Throws
318
+ * `SearchTermTooLongError` past `MAX_QUERY_LEXEMES`, which Postgres can't
319
+ * evaluate.
320
+ */
321
+ export function termTsquery(text, options = {}) {
322
+ const words = queryWords(text);
323
+ if (!words.length)
324
+ return null;
325
+ const size = words.reduce((total, word) => total + 1 + word.parts.length, 0);
326
+ if (size > MAX_QUERY_LEXEMES)
327
+ throw new SearchTermTooLongError();
328
+ const weights = options.weights ?? "";
329
+ const operand = (lexeme, prefix) => {
330
+ const flags = (prefix && options.prefix ? "*" : "") + weights;
331
+ return flags ? `${quoteLexeme(lexeme)}:${flags}` : quoteLexeme(lexeme);
332
+ };
333
+ if (options.anyOrder) {
334
+ return [
335
+ ...new Set(words.map((word, index) => operand(word.lexeme, index === words.length - 1))),
336
+ ].join(" & ");
337
+ }
338
+ if (words.length === 1)
339
+ return operand(words[0].lexeme, true);
340
+ // Inside a phrase a camelCase word is its whole lexeme or its parts: the
341
+ // document holds the whole word only at its first and last part, so the
342
+ // whole alone can't span the parts between its neighbors.
343
+ return words
344
+ .map((word, index) => {
345
+ const last = index === words.length - 1;
346
+ const whole = operand(word.lexeme, last);
347
+ if (!word.parts.length)
348
+ return whole;
349
+ const parts = word.parts
350
+ .map((part, partIndex) => operand(part, last && partIndex === word.parts.length - 1))
351
+ .join(" <-> ");
352
+ return `(${whole} | ${parts})`;
353
+ })
354
+ .join(" <-> ");
355
+ }
356
+ /** Whether a term is more than one lexeme, so it matches as a phrase. */
357
+ export function isPhraseTerm(text) {
358
+ return queryLexemes(text).length > 1;
359
+ }
360
+ export function anyOfTsquery(parts) {
361
+ const present = parts.filter((part) => !!part);
362
+ if (!present.length)
363
+ return null;
364
+ return present.length === 1
365
+ ? present[0]
366
+ : present.map((part) => `(${part})`).join(" | ");
367
+ }
@@ -1,7 +1,8 @@
1
1
  import { publishActionChangeFastPath } from "../action-change-fast-path.js";
2
2
  import { ACTION_CHANGE_MARKER_KEY, actionChangeMarkerSession, actionChangeMarkerValue, } from "../action-change-marker.js";
3
3
  import { appStatePut } from "../application-state/store.js";
4
- import { getRequestOrgId, getRequestUserEmail } from "./request-context.js";
4
+ import { runAfterWriteDrains } from "../resource-changes/store.js";
5
+ import { getRequestOrgId, getRequestRunContext, getRequestUserEmail, } from "./request-context.js";
5
6
  export function actionChangeTarget(options) {
6
7
  const owner = options.owner ?? getRequestUserEmail() ?? undefined;
7
8
  return {
@@ -17,6 +18,9 @@ export async function writeActionChangeMarker(options) {
17
18
  nonce: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
18
19
  };
19
20
  const sessionId = actionChangeMarkerSession(target);
21
+ // The action changed data, so the database is awake: let change-feed
22
+ // consumers such as the search index catch up without delaying the caller.
23
+ runAfterWriteDrains(getRequestRunContext()?.waitUntil);
20
24
  if (!sessionId)
21
25
  return;
22
26
  publishActionChangeFastPath(target);
@@ -3,4 +3,11 @@ import type { SandboxCodeEvaluator } from "../../coding-tools/run-code.js";
3
3
  export declare function loadRunCodeToolEntries(supplier: () => Record<string, ActionEntry>, runCodeOptions?: {
4
4
  bridgeTools?: string[];
5
5
  evaluator?: SandboxCodeEvaluator;
6
+ /**
7
+ * Names of actions the app defines itself. These entries are spread after
8
+ * the app's actions, so a same-named entry here would silently replace the
9
+ * app's version (and drop its `mcpTool` exposure). Leave those names to
10
+ * the app.
11
+ */
12
+ appActionNames?: Iterable<string>;
6
13
  }): Promise<Record<string, ActionEntry>>;
@@ -1,5 +1,12 @@
1
1
  import { getAppConfig } from "../../app-config/index.js";
2
2
  export async function loadRunCodeToolEntries(supplier, runCodeOptions) {
3
+ const entries = await createRunCodeToolEntries(supplier, runCodeOptions);
4
+ for (const name of runCodeOptions?.appActionNames ?? []) {
5
+ delete entries[name];
6
+ }
7
+ return entries;
8
+ }
9
+ async function createRunCodeToolEntries(supplier, runCodeOptions) {
3
10
  try {
4
11
  const { createRunCodeEntry, createGetCodeExecutionEntry } = await import("../../coding-tools/run-code.js");
5
12
  const { createToolOrchestrationEntry } = await import("../../coding-tools/tool-orchestration.js");
@@ -5,10 +5,14 @@ import { foldAssistantTurn, type ThreadSuggestionRun } from "../agent/thread-dat
5
5
  import type { AgentChatEvent, AgentChatScope } from "../agent/types.js";
6
6
  import { type QueuedMessage } from "../chat-threads/store.js";
7
7
  import type { ResolvedKeyReference } from "../secrets/substitution.js";
8
+ import { type TrackingMeta } from "../tracking/registry.js";
8
9
  import { handleSharedThreadRequest, type SharedThreadRouteDependencies } from "./agent-chat/shared-thread.js";
9
10
  export { handleSharedThreadRequest };
10
11
  export type { SharedThreadRouteDependencies };
11
- export declare function trackAgentChatRunLifecycle(event: "run_started" | "run_finished" | "run_no_reply", threadId: string | undefined, attemptId: string | undefined, userId?: string, properties?: Record<string, unknown>, appId?: string): void;
12
+ type AgentChatRunTrackingSource = Pick<TrackingMeta, "userId" | "authUserId" | "anonymousId" | "sessionId"> & {
13
+ isSyntheticTraffic?: boolean;
14
+ };
15
+ export declare function trackAgentChatRunLifecycle(event: "run_started" | "run_finished" | "run_no_reply", threadId: string | undefined, attemptId: string | undefined, userId?: string, properties?: Record<string, unknown>, appId?: string, trackingSource?: AgentChatRunTrackingSource): void;
12
16
  import { buildSelectedA2AReceiverContext, createA2AEngineToolSurface, DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKENS, DEFAULT_DELEGATED_MAX_TOOL_RESULT_CHARS, isSelectedA2AReceiver, runA2AAgentLoop, runMCPAgentLoop, assembleA2AFinalResponse, buildPublicAgentA2ASkills } from "./agent-chat/action-filters-a2a.js";
13
17
  import { _agentChatPromptSectionsForTests } from "./agent-chat/framework-prompts.js";
14
18
  import { type AgentChatPluginOptions, type NitroPluginDef } from "./agent-chat/plugin-options.js";
@@ -71,15 +71,18 @@ import { getModelFamilyOverlay } from "./prompts/index.js";
71
71
  import { mountRealtimeVoiceRoutes } from "./realtime-voice.js";
72
72
  import { runWithRequestContext, getRequestContext, getRequestOrgId, getRequestUserEmail, getRequestRunContext, ensureRequestRunContext, } from "./request-context.js";
73
73
  export { handleSharedThreadRequest };
74
- export function trackAgentChatRunLifecycle(event, threadId, attemptId, userId, properties = {}, appId) {
75
- if (!threadId?.trim() || !attemptId?.trim())
74
+ export function trackAgentChatRunLifecycle(event, threadId, attemptId, userId, properties = {}, appId, trackingSource) {
75
+ if (!threadId?.trim() ||
76
+ !attemptId?.trim() ||
77
+ trackingSource?.isSyntheticTraffic === true) {
76
78
  return;
79
+ }
77
80
  track(event, {
78
81
  ...properties,
79
82
  ...(appId ? { app_name: appId, template_name: appId } : {}),
80
83
  thread_id: threadId,
81
84
  attempt_id: attemptId,
82
- }, runLifecycleTrackingSource(userId));
85
+ }, trackingSource ?? runLifecycleTrackingSource(userId));
83
86
  }
84
87
  /**
85
88
  * The run's owner is set on the run context only once its system prompt is
@@ -1392,6 +1395,7 @@ export function createAgentChatPlugin(options) {
1392
1395
  () => filterRuntimeActionsToSurface(prodRunCodeToolActions), {
1393
1396
  bridgeTools: options?.codeExecution?.bridgeTools,
1394
1397
  evaluator: productionEvaluator,
1398
+ appActionNames: Object.keys(templateScriptsAll),
1395
1399
  });
1396
1400
  const leanRunCodeTool = await loadRunCodeToolEntries(
1397
1401
  // Lean prompt mode intentionally exposes a much smaller action
@@ -1399,6 +1403,7 @@ export function createAgentChatPlugin(options) {
1399
1403
  () => filterRuntimeActionsToSurface(leanRunCodeToolActions), {
1400
1404
  bridgeTools: options?.codeExecution?.bridgeTools,
1401
1405
  evaluator: productionEvaluator,
1406
+ appActionNames: Object.keys(templateScriptsAll),
1402
1407
  });
1403
1408
  // Full coding tool registry (bash/read/edit/write) for "trusted" prod.
1404
1409
  // In dev mode this is handled separately via devHandler below.
@@ -1431,6 +1436,7 @@ export function createAgentChatPlugin(options) {
1431
1436
  ? await loadRunCodeToolEntries(() => filterRuntimeActionsToSurface(devRunCodeToolActions), {
1432
1437
  bridgeTools: options?.codeExecution?.bridgeTools,
1433
1438
  evaluator: "node",
1439
+ appActionNames: Object.keys(templateScriptsAll),
1434
1440
  })
1435
1441
  : {};
1436
1442
  // Registry `generateCorpusToolsPrompt` (below) reads from to decide what
@@ -2656,7 +2662,7 @@ export function createAgentChatPlugin(options) {
2656
2662
  }
2657
2663
  // Callback to persist agent response when run finishes (even if client disconnected).
2658
2664
  // Reconstructs the assistant message from buffered events and appends to thread_data.
2659
- const onRunComplete = async (run, threadId) => {
2665
+ const onRunComplete = async (run, threadId, trackingSource) => {
2660
2666
  const runThreadId = String(run?.threadId ?? threadId ?? "");
2661
2667
  const chatScope = getRequestRunContext()?.chatScope;
2662
2668
  const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId, {
@@ -2682,9 +2688,9 @@ export function createAgentChatPlugin(options) {
2682
2688
  ...(failureCode?.type === "error"
2683
2689
  ? { failure_code: failureCode.errorCode ?? "unknown" }
2684
2690
  : {}),
2685
- }, options?.appId);
2691
+ }, options?.appId, trackingSource);
2686
2692
  if (!assistantMsg) {
2687
- trackAgentChatRunLifecycle("run_no_reply", runThreadId || undefined, run.runId, getRequestRunContext()?.owner, {}, options?.appId);
2693
+ trackAgentChatRunLifecycle("run_no_reply", runThreadId || undefined, run.runId, getRequestRunContext()?.owner, {}, options?.appId, trackingSource);
2688
2694
  }
2689
2695
  if (!threadId) {
2690
2696
  if (runThreadId)
@@ -3535,10 +3541,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
3535
3541
  trackAgentChatRunLifecycle("run_started", threadId, runId, runCtx?.owner, {}, options?.appId);
3536
3542
  await runPreAgentTurnAutosave(options?.onAgentTurnStart, runCtx?.chatScope, { threadId, runId });
3537
3543
  },
3538
- onRunComplete: async (run, threadId) => {
3544
+ onRunComplete: async (run, threadId, trackingSource) => {
3539
3545
  if (threadId)
3540
3546
  _runSendByThread.delete(threadId);
3541
- await onRunComplete(run, threadId);
3547
+ await onRunComplete(run, threadId, trackingSource);
3542
3548
  },
3543
3549
  resolveAdditionalActions: ({ ownerEmail, orgId }) => getMcpActionEntriesForPrincipal(ownerEmail, orgId),
3544
3550
  // Resolve owner from session for usage attribution in hosted prod
@@ -3588,10 +3594,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
3588
3594
  }
3589
3595
  trackAgentChatRunLifecycle("run_started", threadId, runId, runCtx?.owner, {}, options?.appId);
3590
3596
  },
3591
- onRunComplete: async (run, threadId) => {
3597
+ onRunComplete: async (run, threadId, trackingSource) => {
3592
3598
  if (threadId)
3593
3599
  _runSendByThread.delete(threadId);
3594
- await onRunComplete(run, threadId);
3600
+ await onRunComplete(run, threadId, trackingSource);
3595
3601
  },
3596
3602
  resolveOwnerEmail: getOwnerFromEvent,
3597
3603
  })
@@ -3790,10 +3796,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
3790
3796
  trackAgentChatRunLifecycle("run_started", threadId, runId, runCtx?.owner, {}, options?.appId);
3791
3797
  await runPreAgentTurnAutosave(options?.onAgentTurnStart, runCtx?.chatScope, { threadId, runId });
3792
3798
  },
3793
- onRunComplete: async (run, threadId) => {
3799
+ onRunComplete: async (run, threadId, trackingSource) => {
3794
3800
  if (threadId)
3795
3801
  _runSendByThread.delete(threadId);
3796
- await onRunComplete(run, threadId);
3802
+ await onRunComplete(run, threadId, trackingSource);
3797
3803
  },
3798
3804
  });
3799
3805
  }
@@ -66,12 +66,12 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
66
66
  code: any;
67
67
  } | {
68
68
  errorCode?: undefined;
69
- error?: undefined;
70
69
  code?: undefined;
71
70
  ok: boolean;
72
71
  key: string;
73
72
  baseUrlKey?: string;
74
73
  scope: AgentEngineApiKeyScope;
75
74
  defaultModel?: import("./agent-engine-default-model-route.js").SavedKeyDefaultModelOutcome;
75
+ error?: undefined;
76
76
  }>>;
77
77
  export {};
@@ -8,17 +8,17 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
8
8
  * resolveEngine's credential detection.
9
9
  */
10
10
  export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- error: string;
12
11
  ok?: undefined;
12
+ error: string;
13
13
  scope?: undefined;
14
14
  } | {
15
15
  ok: boolean;
16
16
  error: any;
17
17
  scope?: undefined;
18
18
  } | {
19
+ error?: undefined;
19
20
  ok: boolean;
20
21
  scope: "org" | "user";
21
- error?: undefined;
22
22
  }>>;
23
23
  export interface DefaultModelSelectionRequest {
24
24
  engine: string;
@@ -221,6 +221,10 @@ const FRAMEWORK_SCHEMA_ENSURES = [
221
221
  "RemoteRunEvents",
222
222
  () => import("../integrations/remote-run-events-store.js").then((m) => m.ensureTable()),
223
223
  ],
224
+ [
225
+ "ResourceChanges",
226
+ () => import("../resource-changes/store.js").then((m) => m.ensureResourceChangeTables()),
227
+ ],
224
228
  [
225
229
  "ResourceVersions",
226
230
  () => import("../history/store.js").then((m) => m.ensureResourceVersionsTable()),
@@ -245,6 +249,10 @@ const FRAMEWORK_SCHEMA_ENSURES = [
245
249
  "SchedulerHealth",
246
250
  () => import("../jobs/scheduler-health.js").then((m) => m.ensureHealthTable()),
247
251
  ],
252
+ [
253
+ "SearchIndex",
254
+ () => import("../search/index-store.js").then((m) => m.ensureSearchIndexTables()),
255
+ ],
248
256
  [
249
257
  "Settings",
250
258
  () => import("../settings/store.js").then((m) => m.ensureTable()),
@@ -14,7 +14,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
14
14
  executionCwd?: string;
15
15
  }, {
16
16
  deleted?: undefined;
17
- enabled?: undefined;
18
17
  created: boolean;
19
18
  name: string;
20
19
  scope: "organization" | "personal";
@@ -25,24 +24,25 @@ declare const _default: import("../../action.js").ActionDefinition<{
25
24
  reasoningEffort: "auto" | "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh";
26
25
  webhookPath: string;
27
26
  nextRun: string;
27
+ enabled?: undefined;
28
28
  executionHostId?: undefined;
29
29
  executionEngine?: undefined;
30
30
  executionCwd?: undefined;
31
31
  } | {
32
- enabled?: undefined;
33
- nextRun?: undefined;
34
32
  created?: undefined;
35
33
  scope?: undefined;
36
34
  triggerType?: undefined;
37
35
  event?: undefined;
38
36
  deleted: boolean;
39
37
  name: string;
38
+ enabled?: undefined;
40
39
  schedule?: undefined;
41
40
  timezone?: undefined;
42
41
  reasoningEffort?: undefined;
43
42
  executionHostId?: undefined;
44
43
  executionEngine?: undefined;
45
44
  executionCwd?: undefined;
45
+ nextRun?: undefined;
46
46
  webhookPath?: undefined;
47
47
  } | {
48
48
  deleted?: undefined;