@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.
Files changed (209) hide show
  1. package/dist/src/__fixtures__/README.md +4 -0
  2. package/dist/src/__fixtures__/e2e.test.d.ts +6 -0
  3. package/dist/src/__fixtures__/e2e.test.js +211 -0
  4. package/dist/src/__fixtures__/e2e.test.js.map +1 -0
  5. package/dist/src/__fixtures__/smoke-agent/automations/delivery-callback-test.json +9 -0
  6. package/dist/src/__fixtures__/smoke-agent/connections/mock-mcp/spec.json +1 -1
  7. package/dist/src/__fixtures__/smoke.test.js +715 -29
  8. package/dist/src/__fixtures__/smoke.test.js.map +1 -1
  9. package/dist/src/__fixtures__/test-env.d.ts +27 -0
  10. package/dist/src/__fixtures__/test-env.js +64 -0
  11. package/dist/src/__fixtures__/test-env.js.map +1 -0
  12. package/dist/src/__fixtures__/test-helpers.d.ts +30 -0
  13. package/dist/src/__fixtures__/test-helpers.js +120 -0
  14. package/dist/src/__fixtures__/test-helpers.js.map +1 -0
  15. package/dist/src/agent/agent-types.d.ts +22 -0
  16. package/dist/src/agent/agent-types.js.map +1 -1
  17. package/dist/src/agent/automation-bridge.d.ts +9 -0
  18. package/dist/src/agent/automation-bridge.js +26 -0
  19. package/dist/src/agent/automation-bridge.js.map +1 -1
  20. package/dist/src/agent/automation-bridge.test.js +63 -0
  21. package/dist/src/agent/automation-bridge.test.js.map +1 -1
  22. package/dist/src/agent/local-server.d.ts +0 -7
  23. package/dist/src/agent/local-server.js +230 -86
  24. package/dist/src/agent/local-server.js.map +1 -1
  25. package/dist/src/agent/local-server.test.js +14 -8
  26. package/dist/src/agent/local-server.test.js.map +1 -1
  27. package/dist/src/agent/loop-types.d.ts +81 -2
  28. package/dist/src/agent/loop-types.js +4 -0
  29. package/dist/src/agent/loop-types.js.map +1 -1
  30. package/dist/src/agent/loop.js +16 -3
  31. package/dist/src/agent/loop.js.map +1 -1
  32. package/dist/src/agent/loop.test.js +572 -8
  33. package/dist/src/agent/loop.test.js.map +1 -1
  34. package/dist/src/agent/proactive/delivery-router.d.ts +68 -0
  35. package/dist/src/agent/proactive/delivery-router.js +337 -0
  36. package/dist/src/agent/proactive/delivery-router.js.map +1 -0
  37. package/dist/src/agent/proactive/delivery-router.test.d.ts +6 -0
  38. package/dist/src/agent/proactive/delivery-router.test.js +455 -0
  39. package/dist/src/agent/proactive/delivery-router.test.js.map +1 -0
  40. package/dist/src/agent/proactive/proactive-runner.d.ts +23 -1
  41. package/dist/src/agent/proactive/proactive-runner.js +42 -10
  42. package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
  43. package/dist/src/agent/proactive/proactive-runner.test.js +0 -2
  44. package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
  45. package/dist/src/agent/routes/admin-chat-abort.test.d.ts +6 -0
  46. package/dist/src/agent/routes/admin-chat-abort.test.js +206 -0
  47. package/dist/src/agent/routes/admin-chat-abort.test.js.map +1 -0
  48. package/dist/src/agent/routes/admin-chat.js +0 -2
  49. package/dist/src/agent/routes/admin-chat.js.map +1 -1
  50. package/dist/src/agent/routes/task.test.js +0 -2
  51. package/dist/src/agent/routes/task.test.js.map +1 -1
  52. package/dist/src/agent/snapshot-server.js +0 -2
  53. package/dist/src/agent/snapshot-server.js.map +1 -1
  54. package/dist/src/agent/states/compacting.js +5 -3
  55. package/dist/src/agent/states/compacting.js.map +1 -1
  56. package/dist/src/agent/states/confirming.js +3 -0
  57. package/dist/src/agent/states/confirming.js.map +1 -1
  58. package/dist/src/agent/states/dispatching.js +45 -1
  59. package/dist/src/agent/states/dispatching.js.map +1 -1
  60. package/dist/src/agent/states/executing.js +225 -81
  61. package/dist/src/agent/states/executing.js.map +1 -1
  62. package/dist/src/agent/states/streaming.js +14 -0
  63. package/dist/src/agent/states/streaming.js.map +1 -1
  64. package/dist/src/agent/states/thinking.d.ts +1 -1
  65. package/dist/src/agent/states/thinking.js +246 -29
  66. package/dist/src/agent/states/thinking.js.map +1 -1
  67. package/dist/src/agent/token-estimate.d.ts +20 -6
  68. package/dist/src/agent/token-estimate.js +24 -3
  69. package/dist/src/agent/token-estimate.js.map +1 -1
  70. package/dist/src/agent/token-estimate.test.d.ts +6 -0
  71. package/dist/src/agent/token-estimate.test.js +44 -0
  72. package/dist/src/agent/token-estimate.test.js.map +1 -0
  73. package/dist/src/api/create-agent.js +0 -3
  74. package/dist/src/api/create-agent.js.map +1 -1
  75. package/dist/src/api/types.d.ts +0 -2
  76. package/dist/src/env-ref.d.ts +13 -0
  77. package/dist/src/env-ref.js +31 -0
  78. package/dist/src/env-ref.js.map +1 -0
  79. package/dist/src/env-ref.test.d.ts +6 -0
  80. package/dist/src/env-ref.test.js +34 -0
  81. package/dist/src/env-ref.test.js.map +1 -0
  82. package/dist/src/errors.d.ts +15 -0
  83. package/dist/src/errors.js +22 -0
  84. package/dist/src/errors.js.map +1 -1
  85. package/dist/src/errors.test.js +2 -2
  86. package/dist/src/errors.test.js.map +1 -1
  87. package/dist/src/events/event-bus.d.ts +54 -0
  88. package/dist/src/events/event-bus.js +84 -0
  89. package/dist/src/events/event-bus.js.map +1 -0
  90. package/dist/src/events/event-bus.test.d.ts +6 -0
  91. package/dist/src/events/event-bus.test.js +112 -0
  92. package/dist/src/events/event-bus.test.js.map +1 -0
  93. package/dist/src/events/events-route.d.ts +36 -0
  94. package/dist/src/events/events-route.js +80 -0
  95. package/dist/src/events/events-route.js.map +1 -0
  96. package/dist/src/events/events-route.test.d.ts +6 -0
  97. package/dist/src/events/events-route.test.js +134 -0
  98. package/dist/src/events/events-route.test.js.map +1 -0
  99. package/dist/src/events/store-event-wrapper.d.ts +19 -0
  100. package/dist/src/events/store-event-wrapper.js +57 -0
  101. package/dist/src/events/store-event-wrapper.js.map +1 -0
  102. package/dist/src/events/store-event-wrapper.test.d.ts +6 -0
  103. package/dist/src/events/store-event-wrapper.test.js +91 -0
  104. package/dist/src/events/store-event-wrapper.test.js.map +1 -0
  105. package/dist/src/middleware/auth.d.ts +0 -2
  106. package/dist/src/middleware/auth.js.map +1 -1
  107. package/dist/src/providers/search-provider.d.ts +64 -0
  108. package/dist/src/providers/search-provider.js +174 -0
  109. package/dist/src/providers/search-provider.js.map +1 -0
  110. package/dist/src/providers/types.d.ts +8 -0
  111. package/dist/src/routes/ai-stream.d.ts +15 -0
  112. package/dist/src/routes/ai-stream.js +9 -0
  113. package/dist/src/routes/ai-stream.js.map +1 -1
  114. package/dist/src/routes/chat-stream.d.ts +6 -0
  115. package/dist/src/routes/chat-stream.js +2 -0
  116. package/dist/src/routes/chat-stream.js.map +1 -1
  117. package/dist/src/routes/chat.d.ts +6 -0
  118. package/dist/src/routes/chat.js +2 -0
  119. package/dist/src/routes/chat.js.map +1 -1
  120. package/dist/src/routes/session-resolver.d.ts +5 -0
  121. package/dist/src/routes/session-resolver.js +1 -15
  122. package/dist/src/routes/session-resolver.js.map +1 -1
  123. package/dist/src/routes/session-resolver.test.js +7 -6
  124. package/dist/src/routes/session-resolver.test.js.map +1 -1
  125. package/dist/src/server.d.ts +6 -0
  126. package/dist/src/server.js +2 -0
  127. package/dist/src/server.js.map +1 -1
  128. package/dist/src/session/drizzle-session-store.d.ts +56 -0
  129. package/dist/src/session/drizzle-session-store.js +203 -0
  130. package/dist/src/session/drizzle-session-store.js.map +1 -0
  131. package/dist/src/session/manager.d.ts +6 -3
  132. package/dist/src/session/manager.js +46 -16
  133. package/dist/src/session/manager.js.map +1 -1
  134. package/dist/src/session/manager.test.js +12 -18
  135. package/dist/src/session/manager.test.js.map +1 -1
  136. package/dist/src/session/pglite-session-store.d.ts +23 -0
  137. package/dist/src/session/pglite-session-store.js +70 -0
  138. package/dist/src/session/pglite-session-store.js.map +1 -0
  139. package/dist/src/session/postgres-session-store.d.ts +44 -0
  140. package/dist/src/session/postgres-session-store.js +138 -0
  141. package/dist/src/session/postgres-session-store.js.map +1 -0
  142. package/dist/src/session/session-builder.d.ts +0 -2
  143. package/dist/src/session/session-builder.js +22 -2
  144. package/dist/src/session/session-builder.js.map +1 -1
  145. package/dist/src/session/session-builder.test.js +0 -2
  146. package/dist/src/session/session-builder.test.js.map +1 -1
  147. package/dist/src/session/session-store-selector.d.ts +49 -0
  148. package/dist/src/session/session-store-selector.js +60 -0
  149. package/dist/src/session/session-store-selector.js.map +1 -0
  150. package/dist/src/session/session-store-selector.test.d.ts +6 -0
  151. package/dist/src/session/session-store-selector.test.js +79 -0
  152. package/dist/src/session/session-store-selector.test.js.map +1 -0
  153. package/dist/src/session/store.d.ts +146 -32
  154. package/dist/src/session/store.js +126 -138
  155. package/dist/src/session/store.js.map +1 -1
  156. package/dist/src/session/store.test.js +385 -107
  157. package/dist/src/session/store.test.js.map +1 -1
  158. package/dist/src/session/tool-context-factory.d.ts +3 -2
  159. package/dist/src/session/tool-context-factory.js +1 -2
  160. package/dist/src/session/tool-context-factory.js.map +1 -1
  161. package/dist/src/session/tool-context-factory.test.js +1 -4
  162. package/dist/src/session/tool-context-factory.test.js.map +1 -1
  163. package/dist/src/session/types.d.ts +13 -6
  164. package/dist/src/stores/schema.d.ts +0 -34
  165. package/dist/src/stores/schema.js +6 -4
  166. package/dist/src/stores/schema.js.map +1 -1
  167. package/dist/src/tools/admin-file-tools.d.ts +29 -0
  168. package/dist/src/tools/admin-file-tools.js +525 -11
  169. package/dist/src/tools/admin-file-tools.js.map +1 -1
  170. package/dist/src/tools/admin-file-tools.test.js +373 -4
  171. package/dist/src/tools/admin-file-tools.test.js.map +1 -1
  172. package/dist/src/tools/custom-tool-adapter.test.js +0 -1
  173. package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
  174. package/dist/src/tools/dispatch-tool.d.ts +4 -4
  175. package/dist/src/tools/fetch-url-tool.d.ts +23 -0
  176. package/dist/src/tools/fetch-url-tool.js +333 -0
  177. package/dist/src/tools/fetch-url-tool.js.map +1 -0
  178. package/dist/src/tools/fetch-url-tool.test.d.ts +6 -0
  179. package/dist/src/tools/fetch-url-tool.test.js +228 -0
  180. package/dist/src/tools/fetch-url-tool.test.js.map +1 -0
  181. package/dist/src/tools/mcp-tool-adapter.test.js +0 -1
  182. package/dist/src/tools/mcp-tool-adapter.test.js.map +1 -1
  183. package/dist/src/tools/registry.test.js +0 -1
  184. package/dist/src/tools/registry.test.js.map +1 -1
  185. package/dist/src/tools/request-tool.test.js +0 -1
  186. package/dist/src/tools/request-tool.test.js.map +1 -1
  187. package/dist/src/tools/store-tools.test.js +0 -1
  188. package/dist/src/tools/store-tools.test.js.map +1 -1
  189. package/dist/src/tools/types.d.ts +20 -2
  190. package/dist/src/tools/web-search-tool.d.ts +31 -0
  191. package/dist/src/tools/web-search-tool.js +170 -0
  192. package/dist/src/tools/web-search-tool.js.map +1 -0
  193. package/dist/src/tools/web-search-tool.test.d.ts +6 -0
  194. package/dist/src/tools/web-search-tool.test.js +153 -0
  195. package/dist/src/tools/web-search-tool.test.js.map +1 -0
  196. package/dist/src/tools/web-tools-shared.d.ts +21 -0
  197. package/dist/src/tools/web-tools-shared.js +32 -0
  198. package/dist/src/tools/web-tools-shared.js.map +1 -0
  199. package/dist/src/types.d.ts +20 -0
  200. package/dist/src/types.js +13 -0
  201. package/dist/src/types.js.map +1 -1
  202. package/dist/tsconfig.tsbuildinfo +1 -1
  203. package/package.json +17 -3
  204. package/dist/src/agent/session-store.d.ts +0 -71
  205. package/dist/src/agent/session-store.js +0 -151
  206. package/dist/src/agent/session-store.js.map +0 -1
  207. package/dist/src/session/admin-file-tools.d.ts +0 -136
  208. package/dist/src/session/admin-file-tools.js +0 -240
  209. 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,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export {};
@@ -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"}
@@ -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 */
@@ -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;CACjC,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"}
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"}