@amodalai/runtime 0.2.0 → 0.2.1
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/src/__fixtures__/README.md +4 -0
- package/dist/src/__fixtures__/e2e.test.d.ts +6 -0
- package/dist/src/__fixtures__/e2e.test.js +211 -0
- package/dist/src/__fixtures__/e2e.test.js.map +1 -0
- package/dist/src/__fixtures__/smoke-agent/automations/delivery-callback-test.json +9 -0
- package/dist/src/__fixtures__/smoke-agent/connections/mock-mcp/spec.json +1 -1
- package/dist/src/__fixtures__/smoke.test.js +715 -29
- package/dist/src/__fixtures__/smoke.test.js.map +1 -1
- package/dist/src/__fixtures__/test-env.d.ts +27 -0
- package/dist/src/__fixtures__/test-env.js +64 -0
- package/dist/src/__fixtures__/test-env.js.map +1 -0
- package/dist/src/__fixtures__/test-helpers.d.ts +30 -0
- package/dist/src/__fixtures__/test-helpers.js +120 -0
- package/dist/src/__fixtures__/test-helpers.js.map +1 -0
- package/dist/src/agent/agent-types.d.ts +22 -0
- package/dist/src/agent/agent-types.js.map +1 -1
- package/dist/src/agent/automation-bridge.d.ts +9 -0
- package/dist/src/agent/automation-bridge.js +26 -0
- package/dist/src/agent/automation-bridge.js.map +1 -1
- package/dist/src/agent/automation-bridge.test.js +63 -0
- package/dist/src/agent/automation-bridge.test.js.map +1 -1
- package/dist/src/agent/local-server.d.ts +0 -7
- package/dist/src/agent/local-server.js +230 -86
- package/dist/src/agent/local-server.js.map +1 -1
- package/dist/src/agent/local-server.test.js +14 -8
- package/dist/src/agent/local-server.test.js.map +1 -1
- package/dist/src/agent/loop-types.d.ts +81 -2
- package/dist/src/agent/loop-types.js +4 -0
- package/dist/src/agent/loop-types.js.map +1 -1
- package/dist/src/agent/loop.js +16 -3
- package/dist/src/agent/loop.js.map +1 -1
- package/dist/src/agent/loop.test.js +572 -8
- package/dist/src/agent/loop.test.js.map +1 -1
- package/dist/src/agent/proactive/delivery-router.d.ts +68 -0
- package/dist/src/agent/proactive/delivery-router.js +337 -0
- package/dist/src/agent/proactive/delivery-router.js.map +1 -0
- package/dist/src/agent/proactive/delivery-router.test.d.ts +6 -0
- package/dist/src/agent/proactive/delivery-router.test.js +455 -0
- package/dist/src/agent/proactive/delivery-router.test.js.map +1 -0
- package/dist/src/agent/proactive/proactive-runner.d.ts +23 -1
- package/dist/src/agent/proactive/proactive-runner.js +42 -10
- package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
- package/dist/src/agent/proactive/proactive-runner.test.js +0 -2
- package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
- package/dist/src/agent/routes/admin-chat-abort.test.d.ts +6 -0
- package/dist/src/agent/routes/admin-chat-abort.test.js +206 -0
- package/dist/src/agent/routes/admin-chat-abort.test.js.map +1 -0
- package/dist/src/agent/routes/admin-chat.js +0 -2
- package/dist/src/agent/routes/admin-chat.js.map +1 -1
- package/dist/src/agent/routes/task.test.js +0 -2
- package/dist/src/agent/routes/task.test.js.map +1 -1
- package/dist/src/agent/snapshot-server.js +0 -2
- package/dist/src/agent/snapshot-server.js.map +1 -1
- package/dist/src/agent/states/compacting.js +5 -3
- package/dist/src/agent/states/compacting.js.map +1 -1
- package/dist/src/agent/states/confirming.js +3 -0
- package/dist/src/agent/states/confirming.js.map +1 -1
- package/dist/src/agent/states/dispatching.js +45 -1
- package/dist/src/agent/states/dispatching.js.map +1 -1
- package/dist/src/agent/states/executing.js +225 -81
- package/dist/src/agent/states/executing.js.map +1 -1
- package/dist/src/agent/states/streaming.js +14 -0
- package/dist/src/agent/states/streaming.js.map +1 -1
- package/dist/src/agent/states/thinking.d.ts +1 -1
- package/dist/src/agent/states/thinking.js +246 -29
- package/dist/src/agent/states/thinking.js.map +1 -1
- package/dist/src/agent/token-estimate.d.ts +20 -6
- package/dist/src/agent/token-estimate.js +24 -3
- package/dist/src/agent/token-estimate.js.map +1 -1
- package/dist/src/agent/token-estimate.test.d.ts +6 -0
- package/dist/src/agent/token-estimate.test.js +44 -0
- package/dist/src/agent/token-estimate.test.js.map +1 -0
- package/dist/src/api/create-agent.js +0 -3
- package/dist/src/api/create-agent.js.map +1 -1
- package/dist/src/api/types.d.ts +0 -2
- package/dist/src/env-ref.d.ts +13 -0
- package/dist/src/env-ref.js +31 -0
- package/dist/src/env-ref.js.map +1 -0
- package/dist/src/env-ref.test.d.ts +6 -0
- package/dist/src/env-ref.test.js +34 -0
- package/dist/src/env-ref.test.js.map +1 -0
- package/dist/src/errors.d.ts +15 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/errors.test.js +2 -2
- package/dist/src/errors.test.js.map +1 -1
- package/dist/src/events/event-bus.d.ts +54 -0
- package/dist/src/events/event-bus.js +84 -0
- package/dist/src/events/event-bus.js.map +1 -0
- package/dist/src/events/event-bus.test.d.ts +6 -0
- package/dist/src/events/event-bus.test.js +112 -0
- package/dist/src/events/event-bus.test.js.map +1 -0
- package/dist/src/events/events-route.d.ts +36 -0
- package/dist/src/events/events-route.js +80 -0
- package/dist/src/events/events-route.js.map +1 -0
- package/dist/src/events/events-route.test.d.ts +6 -0
- package/dist/src/events/events-route.test.js +134 -0
- package/dist/src/events/events-route.test.js.map +1 -0
- package/dist/src/events/store-event-wrapper.d.ts +19 -0
- package/dist/src/events/store-event-wrapper.js +57 -0
- package/dist/src/events/store-event-wrapper.js.map +1 -0
- package/dist/src/events/store-event-wrapper.test.d.ts +6 -0
- package/dist/src/events/store-event-wrapper.test.js +91 -0
- package/dist/src/events/store-event-wrapper.test.js.map +1 -0
- package/dist/src/middleware/auth.d.ts +0 -2
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/providers/search-provider.d.ts +64 -0
- package/dist/src/providers/search-provider.js +174 -0
- package/dist/src/providers/search-provider.js.map +1 -0
- package/dist/src/providers/types.d.ts +8 -0
- package/dist/src/routes/ai-stream.d.ts +15 -0
- package/dist/src/routes/ai-stream.js +9 -0
- package/dist/src/routes/ai-stream.js.map +1 -1
- package/dist/src/routes/chat-stream.d.ts +6 -0
- package/dist/src/routes/chat-stream.js +2 -0
- package/dist/src/routes/chat-stream.js.map +1 -1
- package/dist/src/routes/chat.d.ts +6 -0
- package/dist/src/routes/chat.js +2 -0
- package/dist/src/routes/chat.js.map +1 -1
- package/dist/src/routes/session-resolver.d.ts +5 -0
- package/dist/src/routes/session-resolver.js +1 -15
- package/dist/src/routes/session-resolver.js.map +1 -1
- package/dist/src/routes/session-resolver.test.js +7 -6
- package/dist/src/routes/session-resolver.test.js.map +1 -1
- package/dist/src/server.d.ts +6 -0
- package/dist/src/server.js +2 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/session/drizzle-session-store.d.ts +56 -0
- package/dist/src/session/drizzle-session-store.js +203 -0
- package/dist/src/session/drizzle-session-store.js.map +1 -0
- package/dist/src/session/manager.d.ts +6 -3
- package/dist/src/session/manager.js +46 -16
- package/dist/src/session/manager.js.map +1 -1
- package/dist/src/session/manager.test.js +12 -18
- package/dist/src/session/manager.test.js.map +1 -1
- package/dist/src/session/pglite-session-store.d.ts +23 -0
- package/dist/src/session/pglite-session-store.js +70 -0
- package/dist/src/session/pglite-session-store.js.map +1 -0
- package/dist/src/session/postgres-session-store.d.ts +44 -0
- package/dist/src/session/postgres-session-store.js +138 -0
- package/dist/src/session/postgres-session-store.js.map +1 -0
- package/dist/src/session/session-builder.d.ts +0 -2
- package/dist/src/session/session-builder.js +22 -2
- package/dist/src/session/session-builder.js.map +1 -1
- package/dist/src/session/session-builder.test.js +0 -2
- package/dist/src/session/session-builder.test.js.map +1 -1
- package/dist/src/session/session-store-selector.d.ts +49 -0
- package/dist/src/session/session-store-selector.js +60 -0
- package/dist/src/session/session-store-selector.js.map +1 -0
- package/dist/src/session/session-store-selector.test.d.ts +6 -0
- package/dist/src/session/session-store-selector.test.js +79 -0
- package/dist/src/session/session-store-selector.test.js.map +1 -0
- package/dist/src/session/store.d.ts +146 -32
- package/dist/src/session/store.js +126 -138
- package/dist/src/session/store.js.map +1 -1
- package/dist/src/session/store.test.js +385 -107
- package/dist/src/session/store.test.js.map +1 -1
- package/dist/src/session/tool-context-factory.d.ts +3 -2
- package/dist/src/session/tool-context-factory.js +1 -2
- package/dist/src/session/tool-context-factory.js.map +1 -1
- package/dist/src/session/tool-context-factory.test.js +1 -4
- package/dist/src/session/tool-context-factory.test.js.map +1 -1
- package/dist/src/session/types.d.ts +13 -6
- package/dist/src/stores/schema.d.ts +0 -34
- package/dist/src/stores/schema.js +6 -4
- package/dist/src/stores/schema.js.map +1 -1
- package/dist/src/tools/admin-file-tools.d.ts +29 -0
- package/dist/src/tools/admin-file-tools.js +525 -11
- package/dist/src/tools/admin-file-tools.js.map +1 -1
- package/dist/src/tools/admin-file-tools.test.js +373 -4
- package/dist/src/tools/admin-file-tools.test.js.map +1 -1
- package/dist/src/tools/custom-tool-adapter.test.js +0 -1
- package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
- package/dist/src/tools/dispatch-tool.d.ts +4 -4
- package/dist/src/tools/fetch-url-tool.d.ts +23 -0
- package/dist/src/tools/fetch-url-tool.js +333 -0
- package/dist/src/tools/fetch-url-tool.js.map +1 -0
- package/dist/src/tools/fetch-url-tool.test.d.ts +6 -0
- package/dist/src/tools/fetch-url-tool.test.js +228 -0
- package/dist/src/tools/fetch-url-tool.test.js.map +1 -0
- package/dist/src/tools/mcp-tool-adapter.test.js +0 -1
- package/dist/src/tools/mcp-tool-adapter.test.js.map +1 -1
- package/dist/src/tools/registry.test.js +0 -1
- package/dist/src/tools/registry.test.js.map +1 -1
- package/dist/src/tools/request-tool.test.js +0 -1
- package/dist/src/tools/request-tool.test.js.map +1 -1
- package/dist/src/tools/store-tools.test.js +0 -1
- package/dist/src/tools/store-tools.test.js.map +1 -1
- package/dist/src/tools/types.d.ts +20 -2
- package/dist/src/tools/web-search-tool.d.ts +31 -0
- package/dist/src/tools/web-search-tool.js +170 -0
- package/dist/src/tools/web-search-tool.js.map +1 -0
- package/dist/src/tools/web-search-tool.test.d.ts +6 -0
- package/dist/src/tools/web-search-tool.test.js +153 -0
- package/dist/src/tools/web-search-tool.test.js.map +1 -0
- package/dist/src/tools/web-tools-shared.d.ts +21 -0
- package/dist/src/tools/web-tools-shared.js +32 -0
- package/dist/src/tools/web-tools-shared.js.map +1 -0
- package/dist/src/types.d.ts +20 -0
- package/dist/src/types.js +13 -0
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -3
- package/dist/src/agent/session-store.d.ts +0 -71
- package/dist/src/agent/session-store.js +0 -151
- package/dist/src/agent/session-store.js.map +0 -1
- package/dist/src/session/admin-file-tools.d.ts +0 -136
- package/dist/src/session/admin-file-tools.js +0 -240
- package/dist/src/session/admin-file-tools.js.map +0 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* web_search tool — grounded search via Gemini Flash.
|
|
8
|
+
*
|
|
9
|
+
* The tool delegates to `ctx.searchProvider.search()` (a dedicated Gemini
|
|
10
|
+
* provider configured via `webTools` in amodal.json), then formats the
|
|
11
|
+
* synthesized answer with cited source URLs as markdown.
|
|
12
|
+
*
|
|
13
|
+
* Works for agents on any main model (Anthropic/OpenAI/Google) — search
|
|
14
|
+
* always runs through the Gemini backend regardless of main provider.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { WEB_SEARCH_TOOL_NAME } from '@amodalai/core';
|
|
18
|
+
import { log } from '../logger.js';
|
|
19
|
+
import { ProviderError, ToolExecutionError } from '../errors.js';
|
|
20
|
+
import { truncateToTokens, MAX_WEB_TOOL_RESULT_TOKENS } from './web-tools-shared.js';
|
|
21
|
+
export { WEB_SEARCH_TOOL_NAME };
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Params schema
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
const DEFAULT_MAX_RESULTS = 5;
|
|
26
|
+
const MAX_ALLOWED_RESULTS = 10;
|
|
27
|
+
const WebSearchParamsSchema = z.object({
|
|
28
|
+
query: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.describe('Search query. Be specific — include dates, names, error messages as relevant.'),
|
|
32
|
+
max_results: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(MAX_ALLOWED_RESULTS)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(`Maximum source citations to include (default: ${String(DEFAULT_MAX_RESULTS)}, max: ${String(MAX_ALLOWED_RESULTS)}).`),
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Map a provider error to an actionable message for the agent. The goal is
|
|
42
|
+
* to let the model know whether retrying will help, so it doesn't burn
|
|
43
|
+
* turns retrying something that's permanently broken.
|
|
44
|
+
*/
|
|
45
|
+
function classifyProviderError(err) {
|
|
46
|
+
const status = err.statusCode;
|
|
47
|
+
if (status === 400 || status === 401 || status === 403) {
|
|
48
|
+
return {
|
|
49
|
+
content: 'Web search is not authorized. The Google API key is missing, invalid, or not permitted for this model. ' +
|
|
50
|
+
'DO NOT retry. Tell the user to check the GOOGLE_API_KEY configured for webTools in amodal.json.',
|
|
51
|
+
retryable: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (status === 429) {
|
|
55
|
+
return {
|
|
56
|
+
content: 'Web search is rate-limited or the Gemini grounding quota is exhausted. ' +
|
|
57
|
+
'DO NOT retry this search in the current turn. Continue with other tools or finish the task without search.',
|
|
58
|
+
retryable: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (status !== undefined && status >= 500) {
|
|
62
|
+
return {
|
|
63
|
+
content: `Web search failed transiently (status ${String(status)}). ` +
|
|
64
|
+
'You may retry once with the same or a slightly different query. If it fails again, continue without search.',
|
|
65
|
+
retryable: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: `Web search failed: ${err.message}. Do not retry without changing the query.`,
|
|
70
|
+
retryable: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Formatting
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
function formatResult(text, sources, maxResults) {
|
|
77
|
+
const capped = sources.slice(0, maxResults);
|
|
78
|
+
if (capped.length === 0) {
|
|
79
|
+
return `${text}\n\n_(no sources cited)_`;
|
|
80
|
+
}
|
|
81
|
+
const lines = [text.trim(), '', 'Sources:'];
|
|
82
|
+
for (let i = 0; i < capped.length; i++) {
|
|
83
|
+
const source = capped[i];
|
|
84
|
+
if (!source)
|
|
85
|
+
continue;
|
|
86
|
+
const titlePart = source.title ? ` — ${source.title}` : '';
|
|
87
|
+
lines.push(`[${String(i + 1)}] ${source.uri}${titlePart}`);
|
|
88
|
+
}
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Factory
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
export function createWebSearchTool() {
|
|
95
|
+
return {
|
|
96
|
+
description: `Search the web for current information. Use when the user asks about recent events, current versions of libraries, news, or any fact you do not already know with confidence. Returns a synthesized answer with cited source URLs.
|
|
97
|
+
|
|
98
|
+
When to use:
|
|
99
|
+
- User asks "what is the latest/current X"
|
|
100
|
+
- Question about events after your knowledge cutoff
|
|
101
|
+
- Looking up specific facts, documentation, or error messages
|
|
102
|
+
- Verifying a claim with external sources
|
|
103
|
+
|
|
104
|
+
When NOT to use:
|
|
105
|
+
- Questions fully answerable from conversation history or knowledge files
|
|
106
|
+
- Internal agent workflows (use connections instead)
|
|
107
|
+
- Retrieving a specific URL (use fetch_url instead)
|
|
108
|
+
|
|
109
|
+
Query strategy (write queries that steer search toward authoritative sources):
|
|
110
|
+
- Code / library questions → include "github" or the package name (e.g. "nextjs app router github docs")
|
|
111
|
+
- API documentation → include the vendor name + "docs" (e.g. "stripe docs customer object")
|
|
112
|
+
- Version / release lookups → add "release notes" or "changelog" and the repo (e.g. "nodejs release notes site:github.com/nodejs/node")
|
|
113
|
+
- Error messages → paste the exact error text verbatim, no rephrasing
|
|
114
|
+
- Recent events → include the **current year or month** (from the currentDate in your context, not your pretraining era) to anchor the timeframe
|
|
115
|
+
- Ambiguous names → add a qualifier ("Python library", "JavaScript", the vendor) so the model searches the right thing
|
|
116
|
+
|
|
117
|
+
If the first query returns off-topic results, rewrite more specifically and search again — don't guess.`,
|
|
118
|
+
parameters: WebSearchParamsSchema,
|
|
119
|
+
readOnly: true,
|
|
120
|
+
metadata: { category: 'system' },
|
|
121
|
+
async execute(params, ctx) {
|
|
122
|
+
const maxResults = params.max_results ?? DEFAULT_MAX_RESULTS;
|
|
123
|
+
if (!ctx.searchProvider) {
|
|
124
|
+
return {
|
|
125
|
+
status: 'error',
|
|
126
|
+
content: 'Web search is not configured. Set `webTools.apiKey` in amodal.json to enable web_search.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const started = Date.now();
|
|
130
|
+
try {
|
|
131
|
+
const result = await ctx.searchProvider.search(params.query, { signal: ctx.signal });
|
|
132
|
+
const formatted = formatResult(result.text, result.sources, maxResults);
|
|
133
|
+
const content = truncateToTokens(formatted, MAX_WEB_TOOL_RESULT_TOKENS);
|
|
134
|
+
log.info('web_search', {
|
|
135
|
+
session: ctx.sessionId,
|
|
136
|
+
query_length: params.query.length,
|
|
137
|
+
result_count: result.sources.length,
|
|
138
|
+
duration_ms: Date.now() - started,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
status: 'ok',
|
|
142
|
+
content,
|
|
143
|
+
source_count: Math.min(result.sources.length, maxResults),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
log.error('web_search_failed', {
|
|
148
|
+
session: ctx.sessionId,
|
|
149
|
+
query_length: params.query.length,
|
|
150
|
+
duration_ms: Date.now() - started,
|
|
151
|
+
status_code: err instanceof ProviderError ? err.statusCode : undefined,
|
|
152
|
+
error: err instanceof Error ? err.message : String(err),
|
|
153
|
+
});
|
|
154
|
+
// Return structured guidance for provider errors so the agent
|
|
155
|
+
// knows whether to retry. Unexpected errors still throw — those
|
|
156
|
+
// are bugs, not runtime conditions.
|
|
157
|
+
if (err instanceof ProviderError) {
|
|
158
|
+
const { content, retryable } = classifyProviderError(err);
|
|
159
|
+
return { status: 'error', content, retryable };
|
|
160
|
+
}
|
|
161
|
+
throw new ToolExecutionError('web_search failed', {
|
|
162
|
+
toolName: WEB_SEARCH_TOOL_NAME,
|
|
163
|
+
callId: ctx.sessionId,
|
|
164
|
+
cause: err,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=web-search-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-search-tool.js","sourceRoot":"","sources":["../../../src/tools/web-search-tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { createWebSearchTool } from './web-search-tool.js';
|
|
8
|
+
import { ProviderError } from '../errors.js';
|
|
9
|
+
function makeCtx(overrides) {
|
|
10
|
+
return {
|
|
11
|
+
request: vi.fn(),
|
|
12
|
+
store: vi.fn(),
|
|
13
|
+
env: vi.fn(),
|
|
14
|
+
log: vi.fn(),
|
|
15
|
+
user: { roles: [] },
|
|
16
|
+
signal: AbortSignal.timeout(5000),
|
|
17
|
+
sessionId: 'test-session',
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function makeProvider(result) {
|
|
22
|
+
return {
|
|
23
|
+
model: 'gemini-2.5-flash',
|
|
24
|
+
search: vi.fn().mockResolvedValue(result),
|
|
25
|
+
fetchUrl: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Helper to execute the tool and get a typed result.
|
|
29
|
+
async function runTool(ctx, params) {
|
|
30
|
+
const tool = createWebSearchTool();
|
|
31
|
+
const raw = await tool.execute(params, ctx);
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
describe('web_search tool', () => {
|
|
35
|
+
it('returns a friendly error when no searchProvider is configured', async () => {
|
|
36
|
+
const ctx = makeCtx();
|
|
37
|
+
const result = await runTool(ctx, { query: 'anything' });
|
|
38
|
+
expect(result.status).toBe('error');
|
|
39
|
+
expect(result.content).toContain('not configured');
|
|
40
|
+
expect(result.content).toContain('webTools');
|
|
41
|
+
});
|
|
42
|
+
it('formats synthesized answer with numbered source citations', async () => {
|
|
43
|
+
const provider = makeProvider({
|
|
44
|
+
text: 'Node.js 22 is the current LTS.',
|
|
45
|
+
sources: [
|
|
46
|
+
{ uri: 'https://nodejs.org/en/blog/release/v22.0.0', title: 'Node.js 22 Release' },
|
|
47
|
+
{ uri: 'https://en.wikipedia.org/wiki/Node.js' },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
const ctx = makeCtx({ searchProvider: provider });
|
|
51
|
+
const result = await runTool(ctx, { query: 'latest node version' });
|
|
52
|
+
expect(result.status).toBe('ok');
|
|
53
|
+
expect(result.content).toContain('Node.js 22 is the current LTS.');
|
|
54
|
+
expect(result.content).toContain('[1] https://nodejs.org/en/blog/release/v22.0.0 — Node.js 22 Release');
|
|
55
|
+
expect(result.content).toContain('[2] https://en.wikipedia.org/wiki/Node.js');
|
|
56
|
+
expect(result.source_count).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
it('notes when no sources were returned', async () => {
|
|
59
|
+
const provider = makeProvider({ text: 'Answer without sources.', sources: [] });
|
|
60
|
+
const ctx = makeCtx({ searchProvider: provider });
|
|
61
|
+
const result = await runTool(ctx, { query: 'something' });
|
|
62
|
+
expect(result.status).toBe('ok');
|
|
63
|
+
expect(result.content).toContain('(no sources cited)');
|
|
64
|
+
expect(result.source_count).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
it('caps source list at max_results', async () => {
|
|
67
|
+
const sources = Array.from({ length: 8 }, (_, i) => ({ uri: `https://example.com/${String(i)}` }));
|
|
68
|
+
const provider = makeProvider({ text: 'answer', sources });
|
|
69
|
+
const ctx = makeCtx({ searchProvider: provider });
|
|
70
|
+
const result = await runTool(ctx, { query: 'q', max_results: 3 });
|
|
71
|
+
expect(result.source_count).toBe(3);
|
|
72
|
+
expect(result.content).toContain('[1]');
|
|
73
|
+
expect(result.content).toContain('[3]');
|
|
74
|
+
expect(result.content).not.toContain('[4]');
|
|
75
|
+
});
|
|
76
|
+
it('passes ctx.signal through to the provider', async () => {
|
|
77
|
+
const searchMock = vi.fn().mockResolvedValue({ text: 'x', sources: [] });
|
|
78
|
+
const provider = {
|
|
79
|
+
model: 'test',
|
|
80
|
+
search: searchMock,
|
|
81
|
+
fetchUrl: vi.fn(),
|
|
82
|
+
};
|
|
83
|
+
const signal = AbortSignal.timeout(1000);
|
|
84
|
+
const ctx = makeCtx({ searchProvider: provider, signal });
|
|
85
|
+
await runTool(ctx, { query: 'q' });
|
|
86
|
+
expect(searchMock).toHaveBeenCalledWith('q', { signal });
|
|
87
|
+
});
|
|
88
|
+
it('truncates very long output with a marker', async () => {
|
|
89
|
+
const huge = 'a'.repeat(50_000);
|
|
90
|
+
const provider = makeProvider({ text: huge, sources: [] });
|
|
91
|
+
const ctx = makeCtx({ searchProvider: provider });
|
|
92
|
+
const result = await runTool(ctx, { query: 'q' });
|
|
93
|
+
expect(result.status).toBe('ok');
|
|
94
|
+
expect(result.content.length).toBeLessThan(9_000); // 2000 tokens × 4 chars/token = 8000
|
|
95
|
+
expect(result.content).toContain('(truncated)');
|
|
96
|
+
});
|
|
97
|
+
describe('provider error classification (retry guidance)', () => {
|
|
98
|
+
function makeProviderErrorProvider(status) {
|
|
99
|
+
const err = new ProviderError('Grounded search failed', {
|
|
100
|
+
provider: 'google',
|
|
101
|
+
statusCode: status,
|
|
102
|
+
retryable: status >= 500,
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
model: 'test',
|
|
106
|
+
search: vi.fn().mockRejectedValue(err),
|
|
107
|
+
fetchUrl: vi.fn(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
it('tells the agent NOT to retry on 401 (auth)', async () => {
|
|
111
|
+
const ctx = makeCtx({ searchProvider: makeProviderErrorProvider(401) });
|
|
112
|
+
const result = await runTool(ctx, { query: 'q' });
|
|
113
|
+
expect(result.status).toBe('error');
|
|
114
|
+
expect(result.content).toContain('DO NOT retry');
|
|
115
|
+
expect(result.content).toContain('GOOGLE_API_KEY');
|
|
116
|
+
expect(result.retryable).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('tells the agent NOT to retry on 400 (bad key)', async () => {
|
|
119
|
+
const ctx = makeCtx({ searchProvider: makeProviderErrorProvider(400) });
|
|
120
|
+
const result = await runTool(ctx, { query: 'q' });
|
|
121
|
+
expect(result.content).toContain('DO NOT retry');
|
|
122
|
+
expect(result.retryable).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it('tells the agent NOT to retry on 429 (quota)', async () => {
|
|
125
|
+
const ctx = makeCtx({ searchProvider: makeProviderErrorProvider(429) });
|
|
126
|
+
const result = await runTool(ctx, { query: 'q' });
|
|
127
|
+
expect(result.content).toContain('rate-limited');
|
|
128
|
+
expect(result.content).toContain('DO NOT retry');
|
|
129
|
+
expect(result.retryable).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
it('says retry is OK on 5xx (transient)', async () => {
|
|
132
|
+
const ctx = makeCtx({ searchProvider: makeProviderErrorProvider(503) });
|
|
133
|
+
const result = await runTool(ctx, { query: 'q' });
|
|
134
|
+
expect(result.content).toContain('transient');
|
|
135
|
+
expect(result.content).toContain('may retry');
|
|
136
|
+
expect(result.retryable).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('wraps unexpected (non-ProviderError) errors in ToolExecutionError', async () => {
|
|
140
|
+
const provider = {
|
|
141
|
+
model: 'test',
|
|
142
|
+
search: vi.fn().mockRejectedValue(new Error('boom')),
|
|
143
|
+
fetchUrl: vi.fn(),
|
|
144
|
+
};
|
|
145
|
+
const ctx = makeCtx({ searchProvider: provider });
|
|
146
|
+
const tool = createWebSearchTool();
|
|
147
|
+
await expect(tool.execute({ query: 'q' }, ctx)).rejects.toMatchObject({
|
|
148
|
+
name: 'ToolExecutionError',
|
|
149
|
+
toolName: 'web_search',
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=web-search-tool.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-search-tool.test.js","sourceRoot":"","sources":["../../../src/tools/web-search-tool.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Shared helpers for the `web_search` and `fetch_url` tools.
|
|
8
|
+
*
|
|
9
|
+
* - `MAX_WEB_TOOL_RESULT_TOKENS` — uniform 2000-token cap on tool output.
|
|
10
|
+
* - `truncateToTokens()` — token-estimate truncation using the 4 chars/token
|
|
11
|
+
* heuristic (matches `packages/runtime/src/agent/token-estimate.ts`).
|
|
12
|
+
*/
|
|
13
|
+
/** Max token budget for any single web-tool result, before truncation. */
|
|
14
|
+
export declare const MAX_WEB_TOOL_RESULT_TOKENS = 2000;
|
|
15
|
+
/**
|
|
16
|
+
* Truncate a string to fit within `maxTokens` tokens, using the same
|
|
17
|
+
* 4-chars-per-token heuristic the rest of the runtime uses. When the
|
|
18
|
+
* input fits, it is returned unchanged; otherwise it is trimmed and a
|
|
19
|
+
* truncation marker is appended so the model sees that content was cut.
|
|
20
|
+
*/
|
|
21
|
+
export declare function truncateToTokens(text: string, maxTokens: number): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Shared helpers for the `web_search` and `fetch_url` tools.
|
|
8
|
+
*
|
|
9
|
+
* - `MAX_WEB_TOOL_RESULT_TOKENS` — uniform 2000-token cap on tool output.
|
|
10
|
+
* - `truncateToTokens()` — token-estimate truncation using the 4 chars/token
|
|
11
|
+
* heuristic (matches `packages/runtime/src/agent/token-estimate.ts`).
|
|
12
|
+
*/
|
|
13
|
+
/** Max token budget for any single web-tool result, before truncation. */
|
|
14
|
+
export const MAX_WEB_TOOL_RESULT_TOKENS = 2000;
|
|
15
|
+
/** Chars-per-token approximation — matches `estimateTokenCount()`. */
|
|
16
|
+
const CHARS_PER_TOKEN = 4;
|
|
17
|
+
/** Suffix appended when content is truncated, so the model knows it is clipped. */
|
|
18
|
+
const TRUNCATION_SUFFIX = '\n\n…(truncated)';
|
|
19
|
+
/**
|
|
20
|
+
* Truncate a string to fit within `maxTokens` tokens, using the same
|
|
21
|
+
* 4-chars-per-token heuristic the rest of the runtime uses. When the
|
|
22
|
+
* input fits, it is returned unchanged; otherwise it is trimmed and a
|
|
23
|
+
* truncation marker is appended so the model sees that content was cut.
|
|
24
|
+
*/
|
|
25
|
+
export function truncateToTokens(text, maxTokens) {
|
|
26
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
27
|
+
if (text.length <= maxChars)
|
|
28
|
+
return text;
|
|
29
|
+
const roomForSuffix = Math.max(0, maxChars - TRUNCATION_SUFFIX.length);
|
|
30
|
+
return text.slice(0, roomForSuffix) + TRUNCATION_SUFFIX;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=web-tools-shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-tools-shared.js","sourceRoot":"","sources":["../../../src/tools/web-tools-shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAE/C,sEAAsE;AACtE,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,mFAAmF;AACnF,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,SAAiB;IAC9D,MAAM,QAAQ,GAAG,SAAS,GAAG,eAAe,CAAC;IAC7C,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,GAAG,iBAAiB,CAAC;AAC1D,CAAC"}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -16,18 +16,33 @@ export declare const ChatRequestSchema: z.ZodObject<{
|
|
|
16
16
|
session_type: z.ZodOptional<z.ZodEnum<["chat", "admin", "automation"]>>;
|
|
17
17
|
/** Optional deployment ID — load a specific snapshot instead of the active one */
|
|
18
18
|
deploy_id: z.ZodOptional<z.ZodString>;
|
|
19
|
+
/**
|
|
20
|
+
* Optional session-wide **token** budget cap (not dollars; cost varies
|
|
21
|
+
* by model). When cumulative usage reaches this value the loop
|
|
22
|
+
* terminates with `reason: 'budget_exceeded'`. Absent = no cap.
|
|
23
|
+
*
|
|
24
|
+
* This is a **soft ceiling** — the check runs after each turn, so a
|
|
25
|
+
* single in-flight turn can overshoot by up to `maxOutputTokens` +
|
|
26
|
+
* tool result sizes. Size the cap ~20% below your hard limit.
|
|
27
|
+
*
|
|
28
|
+
* Distinct from the LLM-API `max_tokens` (per-call output cap) —
|
|
29
|
+
* this is a session-wide cumulative total across all turns.
|
|
30
|
+
*/
|
|
31
|
+
max_session_tokens: z.ZodOptional<z.ZodNumber>;
|
|
19
32
|
}, "strip", z.ZodTypeAny, {
|
|
20
33
|
message: string;
|
|
21
34
|
session_id?: string | undefined;
|
|
22
35
|
role?: string | undefined;
|
|
23
36
|
session_type?: "chat" | "admin" | "automation" | undefined;
|
|
24
37
|
deploy_id?: string | undefined;
|
|
38
|
+
max_session_tokens?: number | undefined;
|
|
25
39
|
}, {
|
|
26
40
|
message: string;
|
|
27
41
|
session_id?: string | undefined;
|
|
28
42
|
role?: string | undefined;
|
|
29
43
|
session_type?: "chat" | "admin" | "automation" | undefined;
|
|
30
44
|
deploy_id?: string | undefined;
|
|
45
|
+
max_session_tokens?: number | undefined;
|
|
31
46
|
}>;
|
|
32
47
|
export type ChatRequest = z.infer<typeof ChatRequestSchema>;
|
|
33
48
|
export declare const WebhookPayloadSchema: z.ZodObject<{
|
|
@@ -164,6 +179,11 @@ export interface SSEApprovedEvent {
|
|
|
164
179
|
export interface SSEDoneEvent {
|
|
165
180
|
type: SSEEventType.Done;
|
|
166
181
|
timestamp: string;
|
|
182
|
+
/**
|
|
183
|
+
* Why the loop stopped. Consumers use this to distinguish normal
|
|
184
|
+
* termination from enforced caps (budget, turns, loop detection).
|
|
185
|
+
*/
|
|
186
|
+
reason?: 'model_stop' | 'max_turns' | 'user_abort' | 'error' | 'budget_exceeded' | 'loop_detected';
|
|
167
187
|
usage?: {
|
|
168
188
|
input_tokens: number;
|
|
169
189
|
output_tokens: number;
|
package/dist/src/types.js
CHANGED
|
@@ -18,6 +18,19 @@ export const ChatRequestSchema = z.object({
|
|
|
18
18
|
session_type: z.enum(['chat', 'admin', 'automation']).optional(),
|
|
19
19
|
/** Optional deployment ID — load a specific snapshot instead of the active one */
|
|
20
20
|
deploy_id: z.string().optional(),
|
|
21
|
+
/**
|
|
22
|
+
* Optional session-wide **token** budget cap (not dollars; cost varies
|
|
23
|
+
* by model). When cumulative usage reaches this value the loop
|
|
24
|
+
* terminates with `reason: 'budget_exceeded'`. Absent = no cap.
|
|
25
|
+
*
|
|
26
|
+
* This is a **soft ceiling** — the check runs after each turn, so a
|
|
27
|
+
* single in-flight turn can overshoot by up to `maxOutputTokens` +
|
|
28
|
+
* tool result sizes. Size the cap ~20% below your hard limit.
|
|
29
|
+
*
|
|
30
|
+
* Distinct from the LLM-API `max_tokens` (per-call output cap) —
|
|
31
|
+
* this is a session-wide cumulative total across all turns.
|
|
32
|
+
*/
|
|
33
|
+
max_session_tokens: z.number().int().positive().optional(),
|
|
21
34
|
});
|
|
22
35
|
export const WebhookPayloadSchema = z.object({
|
|
23
36
|
/** Optional event data passed to the automation prompt */
|
package/dist/src/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,4CAA4C;IAC5C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,qDAAqD;IACrD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,8CAA8C;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,yEAAyE;IACzE,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChE,kFAAkF;IAClF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,4CAA4C;IAC5C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,qDAAqD;IACrD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,8CAA8C;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,yEAAyE;IACzE,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChE,kFAAkF;IAClF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC;;;;;;;;;;;OAWG;IACH,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC3D,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,0DAA0D;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAoCH,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,MAAM,CAAN,IAAY,YAqBX;AArBD,WAAY,YAAY;IACtB,6BAAa,CAAA;IACb,wCAAwB,CAAA;IACxB,iDAAiC,CAAA;IACjC,mDAAmC,CAAA;IACnC,gDAAgC,CAAA;IAChC,kDAAkC,CAAA;IAClC,iCAAiB,CAAA;IACjB,0CAA0B,CAAA;IAC1B,oDAAoC,CAAA;IACpC,qCAAqB,CAAA;IACrB,8CAA8B,CAAA;IAC9B,0CAA0B,CAAA;IAC1B,sCAAsB,CAAA;IACtB,0CAA0B,CAAA;IAC1B,8DAA8C,CAAA;IAC9C,oDAAoC,CAAA;IACpC,gDAAgC,CAAA;IAChC,oCAAoB,CAAA;IACpB,+BAAe,CAAA;IACf,6BAAa,CAAA;AACf,CAAC,EArBW,YAAY,KAAZ,YAAY,QAqBvB"}
|