@agent-native/core 0.203.0 → 0.203.1-nightly-20261007183952

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 (50) hide show
  1. package/dist/action.d.ts +34 -0
  2. package/dist/agent/clip-text.d.ts +4 -0
  3. package/dist/agent/clip-text.js +17 -0
  4. package/dist/agent/follow-up-suggestions.d.ts +16 -2
  5. package/dist/agent/follow-up-suggestions.js +69 -1
  6. package/dist/agent/production-agent.d.ts +20 -2
  7. package/dist/agent/production-agent.js +322 -132
  8. package/dist/agent/provider-model-selection.d.ts +21 -2
  9. package/dist/agent/provider-model-selection.js +19 -2
  10. package/dist/agent/run-loop-with-resume.js +2 -16
  11. package/dist/agent/tool-search.d.ts +31 -1
  12. package/dist/agent/tool-search.js +196 -15
  13. package/dist/agent/write-receipts.d.ts +27 -0
  14. package/dist/agent/write-receipts.js +181 -0
  15. package/dist/automation/index.d.ts +1 -1
  16. package/dist/collab/awareness.d.ts +2 -2
  17. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  18. package/dist/observability/routes.d.ts +6 -6
  19. package/dist/observability/trace-error.d.ts +5 -3
  20. package/dist/observability/trace-error.js +29 -4
  21. package/dist/observability/traces.js +6 -0
  22. package/dist/progress/routes.d.ts +1 -1
  23. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  24. package/dist/resources/handlers.d.ts +1 -1
  25. package/dist/secrets/routes.d.ts +11 -11
  26. package/dist/server/action-routes.js +3 -0
  27. package/dist/server/agent-chat/prompt-resources.d.ts +13 -0
  28. package/dist/server/agent-chat/prompt-resources.js +21 -12
  29. package/dist/server/agent-chat-plugin.js +25 -9
  30. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  31. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  32. package/dist/server/email-auth-links.js +98 -2
  33. package/dist/server/embed-route.js +2 -1
  34. package/dist/server/index.d.ts +3 -3
  35. package/dist/server/index.js +3 -3
  36. package/dist/server/realtime-voice.js +2 -4
  37. package/dist/shared/embed-auth.d.ts +1 -0
  38. package/dist/shared/embed-auth.js +1 -0
  39. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  40. package/dist/templates/chat/.agents/skills/actions/SKILL.md +25 -0
  41. package/dist/templates/default/.agents/skills/actions/SKILL.md +25 -0
  42. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +26 -39
  43. package/dist/templates/headless/.agents/skills/actions/SKILL.md +25 -0
  44. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +25 -0
  45. package/package.json +3 -3
  46. package/src/templates/chat/.agents/skills/actions/SKILL.md +25 -0
  47. package/src/templates/default/.agents/skills/actions/SKILL.md +25 -0
  48. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +26 -39
  49. package/src/templates/headless/.agents/skills/actions/SKILL.md +25 -0
  50. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +25 -0
@@ -105,10 +105,29 @@ export declare function resolveProviderModelSelectionAtScope(provider: ProviderM
105
105
  export declare function applyProviderModelSelection(catalog: readonly string[], selection: EffectiveProviderModelSelection | null): string[];
106
106
  /**
107
107
  * The model to run when nothing picked one and the engine's default is no
108
- * longer checked: the first checked model. `undefined` keeps the engine
109
- * default, which is also what happens when the selection can't be read.
108
+ * longer checked: the first checked model in the order it was saved, which is
109
+ * not a ranking of the models. `undefined` keeps the engine default, which is
110
+ * also what happens when the selection can't be read.
110
111
  */
111
112
  export declare function resolveUncheckedDefaultModelReplacement(engine: {
112
113
  name: string;
113
114
  defaultModel: string;
114
115
  }): Promise<string | undefined>;
116
+ /**
117
+ * `selection` with the unchecked-default replacement applied. Only the engine
118
+ * default yields to the provider's checked models: a model the request or a
119
+ * stored default names still runs after it is unchecked, so chats already on
120
+ * it keep working. A replaced model is labelled `provider-selection-fallback`,
121
+ * never `default`, so logs and traces can tell a checkbox swap from the
122
+ * engine's own default.
123
+ */
124
+ export declare function applyUncheckedDefaultModelReplacement<Source extends string>(engine: {
125
+ name: string;
126
+ defaultModel: string;
127
+ }, selection: {
128
+ model: string;
129
+ source: Source;
130
+ }): Promise<{
131
+ model: string;
132
+ source: Source | "provider-selection-fallback";
133
+ }>;
@@ -381,8 +381,9 @@ export function applyProviderModelSelection(catalog, selection) {
381
381
  }
382
382
  /**
383
383
  * The model to run when nothing picked one and the engine's default is no
384
- * longer checked: the first checked model. `undefined` keeps the engine
385
- * default, which is also what happens when the selection can't be read.
384
+ * longer checked: the first checked model in the order it was saved, which is
385
+ * not a ranking of the models. `undefined` keeps the engine default, which is
386
+ * also what happens when the selection can't be read.
386
387
  */
387
388
  export async function resolveUncheckedDefaultModelReplacement(engine) {
388
389
  const provider = providerForEngineName(engine.name);
@@ -402,3 +403,19 @@ export async function resolveUncheckedDefaultModelReplacement(engine) {
402
403
  return undefined;
403
404
  return selection.models[0];
404
405
  }
406
+ /**
407
+ * `selection` with the unchecked-default replacement applied. Only the engine
408
+ * default yields to the provider's checked models: a model the request or a
409
+ * stored default names still runs after it is unchecked, so chats already on
410
+ * it keep working. A replaced model is labelled `provider-selection-fallback`,
411
+ * never `default`, so logs and traces can tell a checkbox swap from the
412
+ * engine's own default.
413
+ */
414
+ export async function applyUncheckedDefaultModelReplacement(engine, selection) {
415
+ if (selection.source !== "default")
416
+ return selection;
417
+ const replacement = await resolveUncheckedDefaultModelReplacement(engine);
418
+ return replacement
419
+ ? { model: replacement, source: "provider-selection-fallback" }
420
+ : selection;
421
+ }
@@ -2,7 +2,7 @@ import { MAX_BACKGROUND_RUN_LOOP_CONTINUATIONS, MAX_RUN_LOOP_CONTINUATIONS, } fr
2
2
  import { SERVER_OWNED_ABORT_REASONS, clientAbortReason, } from "./abort-reasons.js";
3
3
  import { PROVIDER_RATE_LIMITED_ERROR_CODE } from "./engine/error-detail.js";
4
4
  import { EngineError } from "./engine/types.js";
5
- import { runAgentLoop, appendAgentLoopContinuation, isResumableEngineError, isTransientProviderRateLimitError, continuationReasonForResumableError, lastUnfinishedPreparingActionToolFromEvents, resolveFinalResponseGuardRequestText, SELF_CHAIN_MIN_CONTINUATION_BUDGET_MS, PROVIDER_RATE_LIMITED_TERMINAL_MESSAGE, } from "./production-agent.js";
5
+ import { runAgentLoop, mergeAgentLoopUsage, appendAgentLoopContinuation, isResumableEngineError, isTransientProviderRateLimitError, continuationReasonForResumableError, lastUnfinishedPreparingActionToolFromEvents, resolveFinalResponseGuardRequestText, SELF_CHAIN_MIN_CONTINUATION_BUDGET_MS, PROVIDER_RATE_LIMITED_TERMINAL_MESSAGE, } from "./production-agent.js";
6
6
  import { resolveRunSoftTimeoutMs } from "./run-manager.js";
7
7
  import { getCurrentTurnEventsForThread } from "./run-store.js";
8
8
  import { classifyToolCallJournal, buildResumeJournalNote, } from "./tool-call-journal.js";
@@ -239,21 +239,7 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
239
239
  engineName: opts.engine.name,
240
240
  model: opts.model,
241
241
  };
242
- const addUsage = (next) => {
243
- usage.inputTokens += next.inputTokens;
244
- usage.outputTokens += next.outputTokens;
245
- usage.cacheReadTokens += next.cacheReadTokens;
246
- usage.cacheWriteTokens += next.cacheWriteTokens;
247
- if (next.builderCreditsUsed !== undefined) {
248
- usage.builderCreditsUsed =
249
- (usage.builderCreditsUsed ?? 0) + next.builderCreditsUsed;
250
- }
251
- usage.engineName = next.engineName ?? usage.engineName;
252
- usage.model = next.model;
253
- if (next.usageReported)
254
- usage.usageReported = true;
255
- usage.firstEngineEventAtMs ??= next.firstEngineEventAtMs;
256
- };
242
+ const addUsage = (next) => mergeAgentLoopUsage(usage, next);
257
243
  const localTurnEvents = [];
258
244
  const continueFromChunkBoundary = async (reason, attemptEvents) => {
259
245
  if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
@@ -2,6 +2,8 @@ import type { ActionEntry } from "./production-agent.js";
2
2
  export declare const TOOL_SEARCH_ACTION_NAME = "tool-search";
3
3
  type ToolSearchArgs = {
4
4
  query?: unknown;
5
+ queries?: unknown;
6
+ names?: unknown;
5
7
  limit?: unknown;
6
8
  includeSchemas?: unknown;
7
9
  readOnlyOnly?: unknown;
@@ -30,7 +32,7 @@ type ToolSearchOptions = {
30
32
  export declare function createToolSearchEntry(getRegistry: () => Record<string, ActionEntry>, options?: ToolSearchOptions): ActionEntry;
31
33
  export declare function attachToolSearch(registry: Record<string, ActionEntry>, options?: ToolSearchOptions): Record<string, ActionEntry>;
32
34
  export declare function filterActionsForAgentDiscovery(registry: Record<string, ActionEntry>, context?: import("../action.js").ActionRunContext): Promise<Record<string, ActionEntry>>;
33
- export declare function searchToolRegistry(registry: Record<string, ActionEntry>, args?: ToolSearchArgs, options?: ToolSearchOptions): {
35
+ type ToolSearchOutput = {
34
36
  query: string;
35
37
  totalTools: number;
36
38
  count: number;
@@ -38,5 +40,33 @@ export declare function searchToolRegistry(registry: Record<string, ActionEntry>
38
40
  message?: string;
39
41
  results: ToolSearchResult[];
40
42
  };
43
+ /**
44
+ * One call may carry several capability queries and exact names; every match
45
+ * loads on the same next step. Each query keeps its own repeat guard.
46
+ */
47
+ export declare function searchToolRegistry(registry: Record<string, ActionEntry>, args?: ToolSearchArgs, options?: ToolSearchOptions): ToolSearchOutput;
41
48
  export declare function searchToolRegistryForRequest(registry: Record<string, ActionEntry>, args?: ToolSearchArgs, options?: ToolSearchOptions, context?: import("../action.js").ActionRunContext): Promise<ReturnType<typeof searchToolRegistry>>;
49
+ /**
50
+ * Whether a search names anything to look for. A menu search (no query,
51
+ * queries or names) only lists, so a caller that grants tools from a search
52
+ * result must not grant from it. Reads the lists exactly as the search does,
53
+ * so a form the search accepts is never read as a menu search.
54
+ */
55
+ export declare function isTargetedToolSearch(args: object): boolean;
56
+ /** Callable matches of a targeted search; empty for a menu or an error. */
57
+ export declare function extractToolSearchResultNames(value: unknown): string[];
58
+ /**
59
+ * The tools a search made callable lead its stored result. Replayed history
60
+ * clips a result mid-object, and the loaded list must outlive that clip, so it
61
+ * is read before the rest of the object is parsed.
62
+ */
63
+ export declare function withLoadedToolNames<T extends object>(output: T, names: string[]): {
64
+ loadedForNextStep: string[];
65
+ } & T;
66
+ /**
67
+ * Tools a stored tool-search result made callable. `[]` means it loaded
68
+ * nothing, which includes a failed or interrupted search (not an object);
69
+ * `null` means it is a search result that cannot be read, clipped or corrupt.
70
+ */
71
+ export declare function readLoadedToolNames(content: string): string[] | null;
42
72
  export {};
@@ -2,9 +2,13 @@ import { isActionHiddenFromEveryAgentSurface } from "../action.js";
2
2
  import { parseMcpToolName } from "../mcp-client/manager.js";
3
3
  import { isMcpToolAllowedForRequest } from "../mcp-client/visibility.js";
4
4
  import { getRequestRunContext } from "../server/request-context.js";
5
+ import { clipHead } from "./clip-text.js";
5
6
  export const TOOL_SEARCH_ACTION_NAME = "tool-search";
6
7
  const DEFAULT_LIMIT = 8;
7
8
  const MAX_LIMIT = 10;
9
+ const MAX_QUERIES = 5;
10
+ const MAX_NAMES = 20;
11
+ const EXACT_NAME_SCORE = 100;
8
12
  const MAX_MENU_DESCRIPTION_CHARS = 140;
9
13
  const MAX_DESCRIPTION_CHARS = 220;
10
14
  const MAX_PARAMETER_COUNT = 8;
@@ -16,7 +20,7 @@ export function createToolSearchEntry(getRegistry, options = {}) {
16
20
  const defaultLimit = Math.max(1, Math.min(options.defaultLimit ?? DEFAULT_LIMIT, maxLimit));
17
21
  return {
18
22
  tool: {
19
- description: "Find actions and connected MCP tools named `mcp__<server>__<tool>` by capability or name. A targeted query returns concise details and adds matches allowed in the current mode, with their full schemas, to the next model step. Omit query for a bounded alphabetical menu; menu entries are informational until you search for a capability or name.",
23
+ description: "Find actions and connected MCP tools named `mcp__<server>__<tool>` by capability or name. A targeted search returns concise details and adds matches allowed in the current mode, with their full schemas, to the NEXT model step, not this one, so request every tool you need in one call (`queries` and/or `names`) and call them on the next step. If every match is already callable, call it directly instead of searching again. Omit query for a bounded alphabetical menu; menu entries are informational until you search for a capability or name.",
20
24
  parameters: {
21
25
  type: "object",
22
26
  properties: {
@@ -24,9 +28,21 @@ export function createToolSearchEntry(getRegistry, options = {}) {
24
28
  type: "string",
25
29
  description: "Capability or exact tool name to find. A targeted search adds matches allowed in the current mode, with their full schemas, to the next model step. Omit for a bounded alphabetical inventory.",
26
30
  },
31
+ queries: {
32
+ type: "array",
33
+ items: { type: "string" },
34
+ maxItems: MAX_QUERIES,
35
+ description: `Up to ${MAX_QUERIES} capability searches run together; all matches load on the next step.`,
36
+ },
37
+ names: {
38
+ type: "array",
39
+ items: { type: "string" },
40
+ maxItems: MAX_NAMES,
41
+ description: "Exact tool names to load on the next step, together with any queries.",
42
+ },
27
43
  limit: {
28
44
  type: "number",
29
- description: `Maximum results to return, including menu mode. Defaults to ${defaultLimit}; capped at ${maxLimit}.`,
45
+ description: `Maximum results to return per query, including menu mode. Defaults to ${defaultLimit}; capped at ${maxLimit}.`,
30
46
  },
31
47
  includeSchemas: {
32
48
  type: "boolean",
@@ -75,8 +91,72 @@ export async function filterActionsForAgentDiscovery(registry, context) {
75
91
  }
76
92
  return filtered;
77
93
  }
94
+ /**
95
+ * One call may carry several capability queries and exact names; every match
96
+ * loads on the same next step. Each query keeps its own repeat guard.
97
+ */
78
98
  export function searchToolRegistry(registry, args = {}, options = {}) {
79
- const query = String(args.query ?? "").trim();
99
+ const { parsedQueries, parsedNames } = parseToolSearchLists(args);
100
+ const queries = parsedQueries.kept;
101
+ const names = parsedNames.kept;
102
+ // Providers rarely enforce `maxItems`; a silently dropped entry reads as a
103
+ // tool that loaded.
104
+ const truncationNote = [
105
+ droppedEntriesNote("queries", MAX_QUERIES, parsedQueries.dropped),
106
+ droppedEntriesNote("names", MAX_NAMES, parsedNames.dropped),
107
+ ]
108
+ .filter(Boolean)
109
+ .join(" ");
110
+ const withTruncationNote = (output) => truncationNote
111
+ ? {
112
+ ...output,
113
+ message: [output.message, truncationNote].filter(Boolean).join(" "),
114
+ }
115
+ : output;
116
+ const targets = [
117
+ ...queries.map((query) => ({ query })),
118
+ ...(names.length > 0 ? [{ names }] : []),
119
+ ];
120
+ if (targets.length <= 1) {
121
+ return withTruncationNote(searchOnce(registry, targets[0] ?? { query: "" }, args, options));
122
+ }
123
+ const parts = targets.map((target) => searchOnce(registry, target, args, options));
124
+ const byName = new Map();
125
+ for (const part of parts) {
126
+ for (const result of part.results) {
127
+ const existing = byName.get(result.name);
128
+ if (!existing || result.score > existing.score) {
129
+ byName.set(result.name, result);
130
+ }
131
+ }
132
+ }
133
+ const results = [...byName.values()].sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
134
+ const allRepeated = parts.every((part) => part.repeated);
135
+ const message = allRepeated
136
+ ? parts[0].message
137
+ : parts
138
+ .filter((part) => !part.repeated && part.message)
139
+ .map((part) => part.message)
140
+ .join(" ");
141
+ return withTruncationNote({
142
+ query: parts.map((part) => part.query).join(" | "),
143
+ totalTools: Math.max(...parts.map((part) => part.totalTools)),
144
+ count: results.length,
145
+ ...(allRepeated ? { repeated: true } : {}),
146
+ ...(message ? { message } : {}),
147
+ results,
148
+ });
149
+ }
150
+ function searchOnce(registry, target, args, options) {
151
+ const names = "names" in target ? target.names : undefined;
152
+ const exactNames = names
153
+ ? new Set(names.map((name) => name.toLowerCase()))
154
+ : undefined;
155
+ const query = names
156
+ ? `names: ${names.join(", ")}`
157
+ : "query" in target
158
+ ? target.query
159
+ : "";
80
160
  const listAll = query.length === 0;
81
161
  const readOnlyOnly = parseBoolean(args.readOnlyOnly);
82
162
  const limit = parseLimit(args.limit, options.defaultLimit ?? DEFAULT_LIMIT, Math.min(options.maxLimit ?? MAX_LIMIT, MAX_LIMIT));
@@ -144,15 +224,19 @@ export function searchToolRegistry(registry, args = {}, options = {}) {
144
224
  continue;
145
225
  }
146
226
  const parameters = summarizeParameters(entry.tool.parameters);
147
- const score = scoreTool({
148
- query,
149
- queryTokens,
150
- name,
151
- source,
152
- description,
153
- parameters,
154
- kind,
155
- });
227
+ const score = exactNames
228
+ ? exactNames.has(name.toLowerCase())
229
+ ? EXACT_NAME_SCORE
230
+ : 0
231
+ : scoreTool({
232
+ query,
233
+ queryTokens,
234
+ name,
235
+ source,
236
+ description,
237
+ parameters,
238
+ kind,
239
+ });
156
240
  if (score <= 0)
157
241
  continue;
158
242
  candidates.push({
@@ -185,11 +269,19 @@ export function searchToolRegistry(registry, args = {}, options = {}) {
185
269
  return b.score - a.score;
186
270
  return a.name.localeCompare(b.name);
187
271
  });
272
+ const cap = exactNames ? MAX_NAMES : limit;
273
+ const found = new Set(candidates.map((candidate) => candidate.name.toLowerCase()));
274
+ const missing = names?.filter((name) => !found.has(name.toLowerCase()));
188
275
  const result = {
189
276
  query,
190
277
  totalTools,
191
- count: Math.min(candidates.length, limit),
192
- results: candidates.slice(0, limit),
278
+ count: Math.min(candidates.length, cap),
279
+ ...(missing && missing.length > 0
280
+ ? {
281
+ message: `No tool named ${missing.join(", ")} is available in the current mode.`,
282
+ }
283
+ : {}),
284
+ results: candidates.slice(0, cap),
193
285
  };
194
286
  rememberToolSearchResult(cacheKey, result);
195
287
  return result;
@@ -241,7 +333,96 @@ function rememberToolSearchResult(cacheKey, result) {
241
333
  function truncate(value, max) {
242
334
  if (value.length <= max)
243
335
  return value;
244
- return `${value.slice(0, max - 1).trimEnd()}…`;
336
+ return `${clipHead(value, max - 1).trimEnd()}…`;
337
+ }
338
+ function parseToolSearchLists(args) {
339
+ return {
340
+ parsedQueries: parseStringList([String(args.query ?? ""), args.queries], MAX_QUERIES),
341
+ parsedNames: parseStringList([args.names], MAX_NAMES),
342
+ };
343
+ }
344
+ /**
345
+ * Whether a search names anything to look for. A menu search (no query,
346
+ * queries or names) only lists, so a caller that grants tools from a search
347
+ * result must not grant from it. Reads the lists exactly as the search does,
348
+ * so a form the search accepts is never read as a menu search.
349
+ */
350
+ export function isTargetedToolSearch(args) {
351
+ const { parsedQueries, parsedNames } = parseToolSearchLists(args);
352
+ return parsedQueries.kept.length > 0 || parsedNames.kept.length > 0;
353
+ }
354
+ /** Callable matches of a targeted search; empty for a menu or an error. */
355
+ export function extractToolSearchResultNames(value) {
356
+ if (!value || typeof value !== "object")
357
+ return [];
358
+ const result = value;
359
+ if (typeof result.query !== "string" || result.query.trim().length === 0) {
360
+ return [];
361
+ }
362
+ if (!Array.isArray(result.results))
363
+ return [];
364
+ const names = [];
365
+ for (const item of result.results) {
366
+ if (!item || typeof item !== "object")
367
+ continue;
368
+ const record = item;
369
+ if (record.callable === false)
370
+ continue;
371
+ const name = record.name;
372
+ if (typeof name === "string" && name.trim())
373
+ names.push(name);
374
+ }
375
+ return names;
376
+ }
377
+ /**
378
+ * The tools a search made callable lead its stored result. Replayed history
379
+ * clips a result mid-object, and the loaded list must outlive that clip, so it
380
+ * is read before the rest of the object is parsed.
381
+ */
382
+ export function withLoadedToolNames(output, names) {
383
+ return { loadedForNextStep: names, ...output };
384
+ }
385
+ const LEADING_LOADED_TOOL_NAMES = /^\s*\{\s*"loadedForNextStep"\s*:\s*(\[\s*(?:"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*)?\s*\])/;
386
+ /**
387
+ * Tools a stored tool-search result made callable. `[]` means it loaded
388
+ * nothing, which includes a failed or interrupted search (not an object);
389
+ * `null` means it is a search result that cannot be read, clipped or corrupt.
390
+ */
391
+ export function readLoadedToolNames(content) {
392
+ try {
393
+ const leading = LEADING_LOADED_TOOL_NAMES.exec(content);
394
+ if (leading)
395
+ return JSON.parse(leading[1]);
396
+ if (!content.trimStart().startsWith("{"))
397
+ return [];
398
+ // Results stored before the loaded list existed carry every callable
399
+ // match. Pretty-printed JSON has no blank line, so notes appended after
400
+ // the object start at the first one.
401
+ const end = content.indexOf("\n\n");
402
+ return extractToolSearchResultNames(JSON.parse(end < 0 ? content : content.slice(0, end)));
403
+ // coercion-ok: unreadable returns null, which callers tell apart from [] (loaded nothing) and warn about
404
+ }
405
+ catch {
406
+ return null;
407
+ }
408
+ }
409
+ function parseStringList(values, max) {
410
+ const unique = new Map();
411
+ for (const item of values.flat()) {
412
+ if (typeof item !== "string")
413
+ continue;
414
+ const trimmed = item.trim();
415
+ if (trimmed && !unique.has(trimmed.toLowerCase())) {
416
+ unique.set(trimmed.toLowerCase(), trimmed);
417
+ }
418
+ }
419
+ const all = [...unique.values()];
420
+ return { kept: all.slice(0, max), dropped: all.slice(max) };
421
+ }
422
+ function droppedEntriesNote(kind, max, dropped) {
423
+ if (dropped.length === 0)
424
+ return "";
425
+ return `Only the first ${max} ${kind} were used; ${dropped.length} more were ignored (${truncate(dropped.join(", "), 200)}). Request them in another call.`;
245
426
  }
246
427
  function parseLimit(value, fallback, max) {
247
428
  const n = typeof value === "number"
@@ -0,0 +1,27 @@
1
+ import type { WriteReceipt } from "../action.js";
2
+ import type { AgentLoopFinalResponseGuardResult } from "./production-agent.js";
3
+ export type ToolWriteReceipt = WriteReceipt & {
4
+ tool: string;
5
+ };
6
+ export interface WriteReceiptGuard {
7
+ retryMessage: string;
8
+ fallbackMessage: string;
9
+ exhaustedDraftPrefix: string;
10
+ maxRetries: number;
11
+ }
12
+ /**
13
+ * Reads the reserved `_receipt` off an action's raw result. A receipt that is
14
+ * present but unreadable is `unverified`, never silently clean.
15
+ */
16
+ export declare function readWriteReceipt(result: unknown): WriteReceipt | undefined;
17
+ /**
18
+ * A receipt that says nothing was written, or that the effect did not hold,
19
+ * forces one retry per turn. `unverified` alone only annotates the answer.
20
+ */
21
+ export declare function writeReceiptGuard(receipts: readonly ToolWriteReceipt[], alreadyRetried: boolean): WriteReceiptGuard | null;
22
+ /**
23
+ * One guard result from the receipt guard and the app's guard. The retry
24
+ * counter is shared, so the larger budget wins and both messages are sent in
25
+ * the same retry.
26
+ */
27
+ export declare function mergeFinalResponseGuards(receipt: WriteReceiptGuard, app: AgentLoopFinalResponseGuardResult): AgentLoopFinalResponseGuardResult;
@@ -0,0 +1,181 @@
1
+ import { clipHead } from "./clip-text.js";
2
+ const MAX_SUMMARY_CHARS = 200;
3
+ const MAX_SUBJECT_CHARS = 200;
4
+ const MAX_DETAIL_CHARS = 160;
5
+ const MAX_CHECKS = 8;
6
+ const MAX_WARNINGS = 5;
7
+ // About 120 tokens per receipt in the retry block.
8
+ const MAX_RECEIPT_LINE_CHARS = 480;
9
+ const MAX_RECEIPT_LINES = 6;
10
+ const clip = (text, max) => text.length > max ? `${clipHead(text, max - 1)}…` : text;
11
+ // Bidi overrides and isolates reorder the line the user reads, and zero-width
12
+ // space, word joiner, BOM and tag characters hide text. ZWJ, ZWNJ and the
13
+ // directional marks stay: they change the meaning of Persian, Indic and emoji
14
+ // text.
15
+ const HIDING_CHARS = /[\u200b\u2060\ufeff\u202a-\u202e\u2066-\u2069\u{E0000}-\u{E007F}]/gu;
16
+ /**
17
+ * Receipt text can carry strings an author of the written record controls
18
+ * (panel titles, column aliases). It reaches a user-role retry message and
19
+ * a user-visible block, so it is one line with no control characters and no
20
+ * angle brackets that could close the `<write-receipts>` tag.
21
+ */
22
+ function sanitizeText(text, max) {
23
+ return clip(text
24
+ .replace(/[\p{Cc}\u2028\u2029]/gu, " ")
25
+ .replace(HIDING_CHARS, "")
26
+ .replace(/</g, "‹")
27
+ .replace(/>/g, "›")
28
+ .replace(/\s+/g, " ")
29
+ .trim(), max);
30
+ }
31
+ function isRecord(value) {
32
+ return !!value && typeof value === "object" && !Array.isArray(value);
33
+ }
34
+ function parseReceipt(raw) {
35
+ if (!isRecord(raw))
36
+ return null;
37
+ const { changed, verified, summary, subject, checks, warnings } = raw;
38
+ if (typeof changed !== "boolean" || typeof summary !== "string")
39
+ return null;
40
+ if (subject !== undefined && typeof subject !== "string")
41
+ return null;
42
+ if (verified !== true && verified !== false && verified !== "unverified") {
43
+ return null;
44
+ }
45
+ let parsedChecks;
46
+ if (checks !== undefined) {
47
+ if (!Array.isArray(checks))
48
+ return null;
49
+ parsedChecks = [];
50
+ for (const check of checks.slice(0, MAX_CHECKS)) {
51
+ if (!isRecord(check) ||
52
+ typeof check.id !== "string" ||
53
+ typeof check.ok !== "boolean" ||
54
+ (check.detail !== undefined && typeof check.detail !== "string")) {
55
+ return null;
56
+ }
57
+ parsedChecks.push({
58
+ id: check.id,
59
+ ok: check.ok,
60
+ ...(check.detail === undefined
61
+ ? {}
62
+ : { detail: clip(check.detail, MAX_DETAIL_CHARS) }),
63
+ });
64
+ }
65
+ }
66
+ if (warnings !== undefined &&
67
+ (!Array.isArray(warnings) ||
68
+ !warnings.every((warning) => typeof warning === "string"))) {
69
+ return null;
70
+ }
71
+ return {
72
+ changed,
73
+ verified,
74
+ summary: clip(summary, MAX_SUMMARY_CHARS),
75
+ ...(subject?.trim()
76
+ ? { subject: clip(subject.trim(), MAX_SUBJECT_CHARS) }
77
+ : {}),
78
+ ...(parsedChecks ? { checks: parsedChecks } : {}),
79
+ ...(warnings
80
+ ? {
81
+ warnings: warnings
82
+ .slice(0, MAX_WARNINGS)
83
+ .map((warning) => clip(warning, MAX_DETAIL_CHARS)),
84
+ }
85
+ : {}),
86
+ };
87
+ }
88
+ /**
89
+ * Reads the reserved `_receipt` off an action's raw result. A receipt that is
90
+ * present but unreadable is `unverified`, never silently clean.
91
+ */
92
+ export function readWriteReceipt(result) {
93
+ if (!isRecord(result) || result._receipt === undefined)
94
+ return undefined;
95
+ return (parseReceipt(result._receipt) ?? {
96
+ changed: true,
97
+ verified: "unverified",
98
+ summary: "malformed receipt",
99
+ });
100
+ }
101
+ function receiptLine(receipt) {
102
+ const failed = (receipt.checks ?? [])
103
+ .filter((check) => !check.ok)
104
+ .map((check) => check.detail
105
+ ? `${sanitizeText(check.id, MAX_DETAIL_CHARS)} (${sanitizeText(check.detail, MAX_DETAIL_CHARS)})`
106
+ : sanitizeText(check.id, MAX_DETAIL_CHARS));
107
+ return clip(`- ${receipt.tool}: changed=${receipt.changed} verified=${receipt.verified}. ${sanitizeText(receipt.summary, MAX_SUMMARY_CHARS)}${failed.length > 0 ? ` Failed checks: ${failed.join("; ")}.` : ""}`, MAX_RECEIPT_LINE_CHARS);
108
+ }
109
+ /**
110
+ * A flagged receipt is dropped once later verified changes to the same subject
111
+ * re-checked everything it failed on. A verified receipt speaks only for the
112
+ * parts it checked, so one with no matching `ok` checks (an edit that touched
113
+ * nothing the failure was about) supersedes nothing that carried a failing
114
+ * check. A later receipt from another tool counts only against a failing
115
+ * check, because that check id is what ties the two tools' receipts to the
116
+ * same fault; a flagged receipt with no failing checks is dropped only by a
117
+ * later change from its own tool. A receipt with no subject is never
118
+ * superseded.
119
+ */
120
+ function stillFlaggedReceipts(receipts) {
121
+ return receipts.filter((receipt, index) => {
122
+ if (receipt.verified === true && receipt.changed)
123
+ return false;
124
+ const { subject } = receipt;
125
+ if (!subject)
126
+ return true;
127
+ const failing = (receipt.checks ?? []).filter((check) => !check.ok);
128
+ const verifiedLater = receipts
129
+ .slice(index + 1)
130
+ .filter((later) => (later.tool === receipt.tool || failing.length > 0) &&
131
+ later.subject === subject &&
132
+ later.changed &&
133
+ later.verified === true);
134
+ if (verifiedLater.length === 0)
135
+ return true;
136
+ const rechecked = new Set(verifiedLater.flatMap((later) => (later.checks ?? []).filter((check) => check.ok).map(({ id }) => id)));
137
+ return failing.some((check) => !rechecked.has(check.id));
138
+ });
139
+ }
140
+ /**
141
+ * A receipt that says nothing was written, or that the effect did not hold,
142
+ * forces one retry per turn. `unverified` alone only annotates the answer.
143
+ */
144
+ export function writeReceiptGuard(receipts, alreadyRetried) {
145
+ const flagged = stillFlaggedReceipts(receipts);
146
+ if (flagged.length === 0)
147
+ return null;
148
+ const lines = flagged.slice(0, MAX_RECEIPT_LINES).map(receiptLine);
149
+ if (flagged.length > MAX_RECEIPT_LINES) {
150
+ lines.push(`- (+${flagged.length - MAX_RECEIPT_LINES} more)`);
151
+ }
152
+ const block = lines.join("\n");
153
+ const needsRetry = flagged.some((receipt) => receipt.verified === false || !receipt.changed);
154
+ return {
155
+ retryMessage: `<write-receipts>\n${block}\n</write-receipts>\nState what these receipts show. Do not say a change is visible or working unless verified=true.`,
156
+ fallbackMessage: `Write check:\n${block}`,
157
+ exhaustedDraftPrefix: `Write check:\n${block}`,
158
+ maxRetries: needsRetry && !alreadyRetried ? 1 : 0,
159
+ };
160
+ }
161
+ /**
162
+ * One guard result from the receipt guard and the app's guard. The retry
163
+ * counter is shared, so the larger budget wins and both messages are sent in
164
+ * the same retry.
165
+ */
166
+ export function mergeFinalResponseGuards(receipt, app) {
167
+ const appGuard = typeof app === "string" ? { retryMessage: app, fallbackMessage: app } : app;
168
+ return {
169
+ retryMessage: `${receipt.retryMessage}\n\n${appGuard.retryMessage}`,
170
+ maxRetries: Math.max(receipt.maxRetries, appGuard.maxRetries ?? 1),
171
+ ...(appGuard.expandToolSurface ? { expandToolSurface: true } : {}),
172
+ ...(appGuard.exhaustedDraftPrefix
173
+ ? {
174
+ exhaustedDraftPrefix: `${receipt.exhaustedDraftPrefix}\n\n${appGuard.exhaustedDraftPrefix}`,
175
+ fallbackMessage: appGuard.fallbackMessage ?? receipt.fallbackMessage,
176
+ }
177
+ : {
178
+ fallbackMessage: `${receipt.fallbackMessage}\n\n${appGuard.fallbackMessage ?? appGuard.retryMessage}`,
179
+ }),
180
+ };
181
+ }
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
+ error?: undefined;
142
143
  accepted: boolean;
143
144
  duplicate: boolean;
144
145
  eventId: string;
145
- error?: undefined;
146
146
  }>;
@@ -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;
41
42
  states: {
42
43
  clientId: number;
43
44
  state: string;
44
45
  }[];
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;
51
52
  users: {
52
53
  clientId: number;
53
54
  lastSeen: number;
54
55
  }[];
55
- error?: undefined;
56
56
  }>>;
@@ -39,11 +39,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
39
39
  provider?: undefined;
40
40
  configured?: undefined;
41
41
  } | {
42
+ error?: undefined;
42
43
  url: string;
43
44
  id: string;
44
45
  provider: string;
45
46
  configured?: undefined;
46
- error?: undefined;
47
47
  } | {
48
48
  url?: undefined;
49
49
  id?: undefined;