@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.
- package/dist/action.d.ts +34 -0
- package/dist/agent/clip-text.d.ts +4 -0
- package/dist/agent/clip-text.js +17 -0
- package/dist/agent/follow-up-suggestions.d.ts +16 -2
- package/dist/agent/follow-up-suggestions.js +69 -1
- package/dist/agent/production-agent.d.ts +20 -2
- package/dist/agent/production-agent.js +322 -132
- package/dist/agent/provider-model-selection.d.ts +21 -2
- package/dist/agent/provider-model-selection.js +19 -2
- package/dist/agent/run-loop-with-resume.js +2 -16
- package/dist/agent/tool-search.d.ts +31 -1
- package/dist/agent/tool-search.js +196 -15
- package/dist/agent/write-receipts.d.ts +27 -0
- package/dist/agent/write-receipts.js +181 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +6 -6
- package/dist/observability/trace-error.d.ts +5 -3
- package/dist/observability/trace-error.js +29 -4
- package/dist/observability/traces.js +6 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +11 -11
- package/dist/server/action-routes.js +3 -0
- package/dist/server/agent-chat/prompt-resources.d.ts +13 -0
- package/dist/server/agent-chat/prompt-resources.js +21 -12
- package/dist/server/agent-chat-plugin.js +25 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/email-auth-links.js +98 -2
- package/dist/server/embed-route.js +2 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +3 -3
- package/dist/server/realtime-voice.js +2 -4
- package/dist/shared/embed-auth.d.ts +1 -0
- package/dist/shared/embed-auth.js +1 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/actions/SKILL.md +25 -0
- package/dist/templates/default/.agents/skills/actions/SKILL.md +25 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +26 -39
- package/dist/templates/headless/.agents/skills/actions/SKILL.md +25 -0
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +25 -0
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/actions/SKILL.md +25 -0
- package/src/templates/default/.agents/skills/actions/SKILL.md +25 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +26 -39
- package/src/templates/headless/.agents/skills/actions/SKILL.md +25 -0
- 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
|
|
109
|
-
*
|
|
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
|
|
385
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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,
|
|
192
|
-
|
|
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
|
|
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;
|