@zenera/neo 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/agent.d.ts +66 -0
  4. package/dist/agent.js +97 -0
  5. package/dist/architecture.d.ts +74 -0
  6. package/dist/architecture.js +105 -0
  7. package/dist/embedding.d.ts +60 -0
  8. package/dist/embedding.js +21 -0
  9. package/dist/embeddings/gemini.d.ts +24 -0
  10. package/dist/embeddings/gemini.js +71 -0
  11. package/dist/embeddings/index.d.ts +4 -0
  12. package/dist/embeddings/index.js +4 -0
  13. package/dist/embeddings/openai.d.ts +18 -0
  14. package/dist/embeddings/openai.js +27 -0
  15. package/dist/embeddings/openrouter.d.ts +21 -0
  16. package/dist/embeddings/openrouter.js +54 -0
  17. package/dist/events.d.ts +144 -0
  18. package/dist/events.js +110 -0
  19. package/dist/ids.d.ts +14 -0
  20. package/dist/ids.js +33 -0
  21. package/dist/index.d.ts +25 -0
  22. package/dist/index.js +25 -0
  23. package/dist/inspect/client.d.ts +2 -0
  24. package/dist/inspect/client.js +1326 -0
  25. package/dist/inspect/css.d.ts +2 -0
  26. package/dist/inspect/css.js +164 -0
  27. package/dist/inspect/index.d.ts +9 -0
  28. package/dist/inspect/index.js +12 -0
  29. package/dist/inspect/page.d.ts +3 -0
  30. package/dist/inspect/page.js +93 -0
  31. package/dist/inspect/report.d.ts +54 -0
  32. package/dist/inspect/report.js +62 -0
  33. package/dist/kernel.d.ts +178 -0
  34. package/dist/kernel.js +929 -0
  35. package/dist/memory-stores/factory.d.ts +15 -0
  36. package/dist/memory-stores/factory.js +29 -0
  37. package/dist/memory-stores/file.d.ts +37 -0
  38. package/dist/memory-stores/file.js +174 -0
  39. package/dist/memory-stores/in-memory.d.ts +17 -0
  40. package/dist/memory-stores/in-memory.js +80 -0
  41. package/dist/memory-stores/index.d.ts +4 -0
  42. package/dist/memory-stores/index.js +5 -0
  43. package/dist/memory.d.ts +115 -0
  44. package/dist/memory.js +213 -0
  45. package/dist/model.d.ts +25 -0
  46. package/dist/model.js +2 -0
  47. package/dist/models/anthropic.d.ts +36 -0
  48. package/dist/models/anthropic.js +293 -0
  49. package/dist/models/factory.d.ts +202 -0
  50. package/dist/models/factory.js +533 -0
  51. package/dist/models/gemini.d.ts +45 -0
  52. package/dist/models/gemini.js +368 -0
  53. package/dist/models/index.d.ts +7 -0
  54. package/dist/models/index.js +7 -0
  55. package/dist/models/openai-chat.d.ts +19 -0
  56. package/dist/models/openai-chat.js +203 -0
  57. package/dist/models/openai-responses.d.ts +31 -0
  58. package/dist/models/openai-responses.js +258 -0
  59. package/dist/models/openrouter.d.ts +44 -0
  60. package/dist/models/openrouter.js +230 -0
  61. package/dist/payload-stores/factory.d.ts +15 -0
  62. package/dist/payload-stores/factory.js +30 -0
  63. package/dist/payload-stores/file.d.ts +29 -0
  64. package/dist/payload-stores/file.js +112 -0
  65. package/dist/payload-stores/in-memory.d.ts +17 -0
  66. package/dist/payload-stores/in-memory.js +40 -0
  67. package/dist/payload-stores/index.d.ts +4 -0
  68. package/dist/payload-stores/index.js +5 -0
  69. package/dist/payload.d.ts +89 -0
  70. package/dist/payload.js +181 -0
  71. package/dist/project/config.d.ts +302 -0
  72. package/dist/project/config.js +290 -0
  73. package/dist/project/index.d.ts +4 -0
  74. package/dist/project/index.js +4 -0
  75. package/dist/project/load.d.ts +142 -0
  76. package/dist/project/load.js +430 -0
  77. package/dist/project/refs.d.ts +23 -0
  78. package/dist/project/refs.js +70 -0
  79. package/dist/prompt.d.ts +66 -0
  80. package/dist/prompt.js +62 -0
  81. package/dist/runner.d.ts +103 -0
  82. package/dist/runner.js +537 -0
  83. package/dist/services.d.ts +23 -0
  84. package/dist/services.js +44 -0
  85. package/dist/skill-providers/factory.d.ts +16 -0
  86. package/dist/skill-providers/factory.js +26 -0
  87. package/dist/skill-providers/file.d.ts +44 -0
  88. package/dist/skill-providers/file.js +209 -0
  89. package/dist/skill-providers/index.d.ts +4 -0
  90. package/dist/skill-providers/index.js +5 -0
  91. package/dist/skill-providers/static.d.ts +13 -0
  92. package/dist/skill-providers/static.js +47 -0
  93. package/dist/skills.d.ts +102 -0
  94. package/dist/skills.js +232 -0
  95. package/dist/state.d.ts +86 -0
  96. package/dist/state.js +49 -0
  97. package/dist/tools/exa.d.ts +23 -0
  98. package/dist/tools/exa.js +389 -0
  99. package/dist/tools/index.d.ts +4 -0
  100. package/dist/tools/index.js +6 -0
  101. package/dist/tools/sandbox.d.ts +172 -0
  102. package/dist/tools/sandbox.js +602 -0
  103. package/dist/tools/workspace.d.ts +53 -0
  104. package/dist/tools/workspace.js +1034 -0
  105. package/dist/trajectory.d.ts +213 -0
  106. package/dist/trajectory.js +232 -0
  107. package/dist/types.d.ts +195 -0
  108. package/dist/types.js +134 -0
  109. package/package.json +98 -0
@@ -0,0 +1,86 @@
1
+ import type { PayloadResolver } from './payload.ts';
2
+ import { type TrajectoryNode } from './trajectory.ts';
3
+ import type { JsonSchema, TokenUsage } from './types.ts';
4
+ export type RunPhase =
5
+ /** initial context built, nothing executed */
6
+ 'created'
7
+ /** next step is a model call */
8
+ | 'awaiting_llm'
9
+ /** the model returned tool calls; some results are missing */
10
+ | 'awaiting_tools'
11
+ /** a fork is in flight; some branches are unfinished */
12
+ | 'awaiting_branches' | 'done' | 'failed';
13
+ /**
14
+ * Immutable run configuration: written once by `createState`, never touched by
15
+ * `apply*`. It lives inside the state so a run is a single persistable blob
16
+ * that a generic worker can resume without application code.
17
+ */
18
+ export interface RunSpec {
19
+ startAgent: string;
20
+ /** JSON Schema projection of the caller's Zod schema */
21
+ outputSchema?: JsonSchema;
22
+ /** sha256 of `outputSchema`, for cheap mismatch detection on resume */
23
+ outputSchemaHash?: string;
24
+ /** non-object schemas travel in a single `value` property (tool params must be objects) */
25
+ outputWrapped?: boolean;
26
+ /** set on child runs created by a fork */
27
+ parent?: {
28
+ runId: string;
29
+ forkId: string;
30
+ branch: string;
31
+ };
32
+ /**
33
+ * How many leading nodes were copied from the parent when a branch was
34
+ * seeded. They already exist in the parent, so only what follows is spliced
35
+ * back at join, and only what follows counts as this run's usage.
36
+ */
37
+ prefixLength?: number;
38
+ /** 0 for a root run */
39
+ forkDepth: number;
40
+ /** structural bound on recursive forking (not a turn budget) */
41
+ maxForkDepth: number;
42
+ }
43
+ export interface AgentState {
44
+ /** schema version for forward migration */
45
+ version: 1;
46
+ runId: string;
47
+ spec: RunSpec;
48
+ /** active agent — changes on handoff */
49
+ agentName: string;
50
+ phase: RunPhase;
51
+ trajectory: TrajectoryNode[];
52
+ /** tool calls from the last llm_call still lacking a tool_result */
53
+ pendingToolCalls: string[];
54
+ /** the in-flight fork, if any, and its branches still lacking a result */
55
+ pendingFork?: {
56
+ callId: string;
57
+ branches: string[];
58
+ };
59
+ /** derived cache; always recomputable from the trajectory */
60
+ usage: TokenUsage;
61
+ /** serialized user context (must be JSON) */
62
+ context?: unknown;
63
+ /** set when phase === 'failed' */
64
+ error?: string;
65
+ }
66
+ export interface RunResult<T = string> {
67
+ /** `z.infer` of the output schema when one was given */
68
+ output: T;
69
+ agent: string;
70
+ state: AgentState;
71
+ usage: TokenUsage;
72
+ stopReason: 'final' | 'aborted' | 'failed';
73
+ }
74
+ /**
75
+ * Structural check for a rehydrated state. There is no prototype to restore —
76
+ * `JSON.parse(JSON.stringify(state))` is identity — so this replaces v1's
77
+ * `AgentState.from`.
78
+ */
79
+ export declare function validateState(json: unknown): json is AgentState;
80
+ export declare function assertState(json: unknown): AgentState;
81
+ /** Number of model calls made — v1's `state.turns`, derived instead of stored. */
82
+ export declare function turns(state: AgentState): number;
83
+ export declare function usageOf(state: AgentState): TokenUsage;
84
+ /** Most recent assistant text; async because the text lives behind a payload. */
85
+ export declare function lastText(state: AgentState, payloads: PayloadResolver): Promise<string>;
86
+ //# sourceMappingURL=state.d.ts.map
package/dist/state.js ADDED
@@ -0,0 +1,49 @@
1
+ import { lastOfType, totalUsage } from "./trajectory.js";
2
+ /**
3
+ * Structural check for a rehydrated state. There is no prototype to restore —
4
+ * `JSON.parse(JSON.stringify(state))` is identity — so this replaces v1's
5
+ * `AgentState.from`.
6
+ */
7
+ export function validateState(json) {
8
+ if (json === null || typeof json !== 'object') {
9
+ return false;
10
+ }
11
+ const s = json;
12
+ return (s.version === 1 &&
13
+ typeof s.runId === 'string' &&
14
+ typeof s.agentName === 'string' &&
15
+ typeof s.phase === 'string' &&
16
+ Array.isArray(s.trajectory) &&
17
+ Array.isArray(s.pendingToolCalls) &&
18
+ typeof s.spec === 'object' &&
19
+ s.spec !== null);
20
+ }
21
+ export function assertState(json) {
22
+ if (!validateState(json)) {
23
+ throw new TypeError('not a valid AgentState');
24
+ }
25
+ return json;
26
+ }
27
+ /** Number of model calls made — v1's `state.turns`, derived instead of stored. */
28
+ export function turns(state) {
29
+ return state.trajectory.filter((n) => n.type === 'llm_call').length;
30
+ }
31
+ export function usageOf(state) {
32
+ return totalUsage(state.trajectory);
33
+ }
34
+ /** Most recent assistant text; async because the text lives behind a payload. */
35
+ export async function lastText(state, payloads) {
36
+ const nodes = state.trajectory;
37
+ const final = lastOfType(nodes, 'final_output');
38
+ if (final) {
39
+ return payloads.get(final.output);
40
+ }
41
+ for (let i = nodes.length - 1; i >= 0; i--) {
42
+ const n = nodes[i];
43
+ if (n.type === 'llm_call' && n.text.size > 0) {
44
+ return payloads.get(n.text);
45
+ }
46
+ }
47
+ return '';
48
+ }
49
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,23 @@
1
+ import { type AnyTool } from '../types.ts';
2
+ /** The family name config selects the whole set by: `tools: [exa:*]`. */
3
+ export declare const EXA_GROUP = "exa";
4
+ /** Where the api lives. Exported so a host can probe the same address. */
5
+ export declare const EXA_BASE_URL = "https://api.exa.ai";
6
+ /** The variable the key is read from when the host does not pass one. */
7
+ export declare const EXA_API_KEY_ENV = "EXA_API_KEY";
8
+ export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
9
+ export interface ExaOptions {
10
+ /** The key. Falls back to `$EXA_API_KEY`, read when a tool is called. */
11
+ apiKey?: string;
12
+ baseUrl?: string;
13
+ /** How many results `web_search` returns when the model does not say. */
14
+ numResults?: number;
15
+ /** How much of a page `web_read` returns when the model does not say. */
16
+ maxCharacters?: number;
17
+ /** Milliseconds one request may take. */
18
+ timeout?: number;
19
+ /** How requests are made — the seam tests replace. */
20
+ fetch?: FetchLike;
21
+ }
22
+ export declare function exaTools<TCtx = unknown>(opts?: ExaOptions): AnyTool<TCtx>[];
23
+ //# sourceMappingURL=exa.d.ts.map
@@ -0,0 +1,389 @@
1
+ import { tool } from "../types.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Web tools, backed by Exa
4
+ //
5
+ // Three things an agent cannot do from a workspace and a shell: find a page it
6
+ // does not know the address of, read one, and ask a question of the open web.
7
+ // Like every other tool here, none of it is wired in by default — reaching the
8
+ // network is a deployment decision, so a host asks for it by calling
9
+ // `exaTools` and handing the result to a project.
10
+ //
11
+ // There is no SDK. The whole surface is three JSON POSTs to one host, and a
12
+ // dependency that exists to spell `fetch` differently would be a dependency
13
+ // this library has to keep in step with for nothing. What the vendor's client
14
+ // would buy — typed requests, retries, a client object — is either provided
15
+ // here or deliberately not wanted.
16
+ //
17
+ // The credential is read at *call* time, not construction. A host registers
18
+ // tool groups unconditionally so that config naming `exa:*` resolves, and a
19
+ // project on a machine with no key must still load; the failure belongs to the
20
+ // call that needed the key, in words the model can act on.
21
+ // ---------------------------------------------------------------------------
22
+ /** The family name config selects the whole set by: `tools: [exa:*]`. */
23
+ export const EXA_GROUP = 'exa';
24
+ /** Where the api lives. Exported so a host can probe the same address. */
25
+ export const EXA_BASE_URL = 'https://api.exa.ai';
26
+ const DEFAULT_BASE_URL = EXA_BASE_URL;
27
+ /** The variable the key is read from when the host does not pass one. */
28
+ export const EXA_API_KEY_ENV = 'EXA_API_KEY';
29
+ const DEFAULT_RESULTS = 8;
30
+ /** Exa's own ceiling is 100; a hundred results is a context bill, not an answer. */
31
+ const MAX_RESULTS = 25;
32
+ /** Per page. Exa refuses anything above 10000. */
33
+ const DEFAULT_CHARACTERS = 4000;
34
+ const MAX_CHARACTERS = 10_000;
35
+ /** What one call may return in total, however many pages it was asked for. */
36
+ const MAX_TOTAL_CHARACTERS = 128 * 1024;
37
+ /** Pages per `web_read` call. Exa's own limit is 100. */
38
+ const MAX_URLS = 20;
39
+ const DEFAULT_TIMEOUT_MS = 30_000;
40
+ const MAX_TIMEOUT_MS = 120_000;
41
+ /**
42
+ * A failure the model should read rather than one the run should die of.
43
+ * Module-private: it never escapes a tool, which turns it into `{error, hint}`
44
+ * the same way the workspace tools turn a bad patch into one.
45
+ */
46
+ class ExaError extends Error {
47
+ hint;
48
+ constructor(message, hint) {
49
+ super(message);
50
+ this.name = 'ExaError';
51
+ this.hint = hint;
52
+ }
53
+ }
54
+ /**
55
+ * What to say about a refusal, by the vendor's machine-readable tag. The set is
56
+ * open-ended by contract, so an unknown tag falls through to the message Exa
57
+ * sent — which is the one thing that is always about the actual problem.
58
+ */
59
+ const HINTS = {
60
+ INVALID_API_KEY: `the credential in $${EXA_API_KEY_ENV} was refused`,
61
+ NO_MORE_CREDITS: 'the Exa account is out of credit, so this tool will keep failing',
62
+ API_KEY_BUDGET_EXCEEDED: 'this key has spent its budget, so this tool will keep failing',
63
+ TEAM_BUDGET_EXCEEDED: 'the team has spent its budget, so this tool will keep failing',
64
+ RATE_LIMIT_EXCEEDED: 'wait a moment and try once more',
65
+ INVALID_URLS: 'give whole absolute urls, http:// or https://',
66
+ NO_CONTENT_FOUND: 'try a different page, or search again for another source',
67
+ };
68
+ class Exa {
69
+ #apiKey;
70
+ #baseUrl;
71
+ #timeout;
72
+ #fetch;
73
+ constructor(opts) {
74
+ this.#apiKey = opts.apiKey;
75
+ this.#baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
76
+ this.#timeout = clamp(opts.timeout ?? DEFAULT_TIMEOUT_MS, 1, MAX_TIMEOUT_MS);
77
+ this.#fetch = opts.fetch ?? ((input, init) => fetch(input, init));
78
+ }
79
+ /**
80
+ * The environment is read here rather than in the constructor, so a key
81
+ * exported after the tools were built still works — which is exactly what
82
+ * a host materialising a keyring before a run does.
83
+ */
84
+ #key() {
85
+ const key = this.#apiKey ?? process.env[EXA_API_KEY_ENV];
86
+ if (!key) {
87
+ throw new ExaError('no Exa credential', `set $${EXA_API_KEY_ENV} to a key from https://dashboard.exa.ai/api-keys`);
88
+ }
89
+ return key;
90
+ }
91
+ async post(path, body, signal) {
92
+ const key = this.#key();
93
+ const deadline = AbortSignal.timeout(this.#timeout);
94
+ let res;
95
+ try {
96
+ res = await this.#fetch(`${this.#baseUrl}${path}`, {
97
+ method: 'POST',
98
+ headers: { 'content-type': 'application/json', 'x-api-key': key },
99
+ body: JSON.stringify(body),
100
+ signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
101
+ });
102
+ }
103
+ catch (err) {
104
+ // An abort that came from the caller is the run being cancelled and
105
+ // is not this tool's to describe; one that came from the deadline is.
106
+ if (signal?.aborted) {
107
+ throw err;
108
+ }
109
+ if (deadline.aborted) {
110
+ throw new ExaError(`Exa did not answer within ${Math.round(this.#timeout / 1000)}s`, 'try once more, or ask for fewer results');
111
+ }
112
+ throw new ExaError(`could not reach Exa: ${err instanceof Error ? err.message : String(err)}`, 'the network is the problem, not the request');
113
+ }
114
+ if (!res.ok) {
115
+ throw await refusal(res);
116
+ }
117
+ return (await res.json());
118
+ }
119
+ }
120
+ /** The vendor's own words about a rejection, with ours about what to do next. */
121
+ async function refusal(res) {
122
+ let envelope = {};
123
+ try {
124
+ envelope = (await res.json());
125
+ }
126
+ catch {
127
+ // A non-JSON body from a gateway or a proxy; the status is all there is.
128
+ }
129
+ const said = envelope.error ?? res.statusText ?? 'refused';
130
+ const tag = envelope.tag;
131
+ return new ExaError(`Exa ${res.status}: ${said}`, tag ? HINTS[tag] : undefined);
132
+ }
133
+ /**
134
+ * A running character allowance. Text arrives per result, and a cap applied per
135
+ * result alone still lets twenty long pages add up to a context nobody asked
136
+ * for — so the budget is spent across the whole call and says when it ran out.
137
+ */
138
+ class Budget {
139
+ #left;
140
+ constructor(total) {
141
+ this.#left = total;
142
+ }
143
+ take(text, limit) {
144
+ if (!text) {
145
+ return { text: '', truncated: false };
146
+ }
147
+ const allowed = Math.min(limit, this.#left);
148
+ if (allowed <= 0) {
149
+ return { text: '', truncated: true };
150
+ }
151
+ this.#left -= Math.min(text.length, allowed);
152
+ return { text: text.slice(0, allowed), truncated: text.length > allowed };
153
+ }
154
+ }
155
+ const clamp = (n, low, high) => Math.min(high, Math.max(low, Math.trunc(n)));
156
+ /** `null` and `''` both mean the field was not found; neither is worth a token. */
157
+ function pick(value) {
158
+ return value === null || value === '' ? undefined : (value ?? undefined);
159
+ }
160
+ function source(r) {
161
+ return {
162
+ title: pick(r.title),
163
+ url: r.url,
164
+ published_date: pick(r.publishedDate),
165
+ author: pick(r.author),
166
+ };
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+ // The tools
170
+ // ---------------------------------------------------------------------------
171
+ export function exaTools(opts = {}) {
172
+ const exa = new Exa(opts);
173
+ const defaultResults = clamp(opts.numResults ?? DEFAULT_RESULTS, 1, MAX_RESULTS);
174
+ const defaultCharacters = clamp(opts.maxCharacters ?? DEFAULT_CHARACTERS, 1, MAX_CHARACTERS);
175
+ /**
176
+ * Every tool here reports a refusal instead of raising one. A search that
177
+ * found nothing, a page that would not load and a key that was rejected are
178
+ * all things the model can respond to — by asking differently, by choosing
179
+ * another source, or by saying plainly that it cannot look. A thrown error
180
+ * ends the run and tells the user what the model should have told them.
181
+ */
182
+ const attempt = async (run) => {
183
+ try {
184
+ return await run();
185
+ }
186
+ catch (err) {
187
+ if (err instanceof ExaError) {
188
+ return { error: err.message, hint: err.hint };
189
+ }
190
+ throw err;
191
+ }
192
+ };
193
+ const webSearch = tool({
194
+ name: 'web_search',
195
+ group: EXA_GROUP,
196
+ description: 'Searches the live web and returns ranked pages with a short relevant excerpt ' +
197
+ 'from each. Written as a description of what is wanted rather than as keywords: ' +
198
+ 'the query is read by a model, so "papers arguing that scaling laws break down" ' +
199
+ 'works better than "scaling laws paper". Excerpts are for judging which source ' +
200
+ 'to trust — read the page with web_read before quoting it.',
201
+ parameters: {
202
+ type: 'object',
203
+ properties: {
204
+ query: {
205
+ type: 'string',
206
+ description: 'What is wanted, in a sentence. Do not use site: or other ' +
207
+ 'search operators; use include_domains instead.',
208
+ },
209
+ num_results: {
210
+ type: 'integer',
211
+ description: `How many pages to return. Default ${defaultResults}, at most ${MAX_RESULTS}.`,
212
+ },
213
+ include_domains: {
214
+ type: 'array',
215
+ items: { type: 'string' },
216
+ description: 'Only return pages from these hosts, e.g. ["arxiv.org", "*.gov.uk"].',
217
+ },
218
+ exclude_domains: {
219
+ type: 'array',
220
+ items: { type: 'string' },
221
+ description: 'Never return pages from these hosts.',
222
+ },
223
+ start_published_date: {
224
+ type: 'string',
225
+ description: 'Only pages published on or after this ISO 8601 date.',
226
+ },
227
+ end_published_date: {
228
+ type: 'string',
229
+ description: 'Only pages published on or before this ISO 8601 date.',
230
+ },
231
+ category: {
232
+ type: 'string',
233
+ enum: [
234
+ 'company',
235
+ 'publication',
236
+ 'news',
237
+ 'personal site',
238
+ 'financial report',
239
+ 'people',
240
+ ],
241
+ description: 'Narrow the search to one kind of page.',
242
+ },
243
+ type: {
244
+ type: 'string',
245
+ enum: ['auto', 'fast', 'instant'],
246
+ description: 'Search depth. "auto" is the default and right for most questions; ' +
247
+ '"fast" and "instant" trade recall for latency.',
248
+ },
249
+ },
250
+ required: ['query'],
251
+ additionalProperties: false,
252
+ },
253
+ execute: ({ query, ...rest }, tc) => attempt(async () => {
254
+ const text = query?.trim();
255
+ if (!text) {
256
+ return { error: 'query is required' };
257
+ }
258
+ const body = await exa.post('/search', {
259
+ query: text,
260
+ numResults: clamp(rest.num_results ?? defaultResults, 1, MAX_RESULTS),
261
+ includeDomains: pick(rest.include_domains),
262
+ excludeDomains: pick(rest.exclude_domains),
263
+ startPublishedDate: pick(rest.start_published_date),
264
+ endPublishedDate: pick(rest.end_published_date),
265
+ category: pick(rest.category),
266
+ type: pick(rest.type),
267
+ // Highlights are the model-selected sentences that made
268
+ // the page match, which is what a search result excerpt
269
+ // is for. Full text is what web_read exists to fetch.
270
+ contents: { highlights: true },
271
+ }, tc.signal);
272
+ const budget = new Budget(MAX_TOTAL_CHARACTERS);
273
+ const results = (body.results ?? []).map((r) => {
274
+ const joined = (r.highlights ?? []).join(' … ') || r.summary;
275
+ const { text: excerpt } = budget.take(joined, defaultCharacters);
276
+ return { ...source(r), excerpt: pick(excerpt) };
277
+ });
278
+ return {
279
+ query: text,
280
+ results,
281
+ found: results.length,
282
+ cost_usd: body.costDollars?.total,
283
+ };
284
+ }),
285
+ });
286
+ const webRead = tool({
287
+ name: 'web_read',
288
+ group: EXA_GROUP,
289
+ description: 'Fetches the text of web pages, several at once. Returns the readable content ' +
290
+ 'with navigation and boilerplate stripped, served from a cache when it is fresh ' +
291
+ 'enough and crawled live when it is not. Pages that could not be fetched are ' +
292
+ 'reported alongside the ones that were, so a single bad url does not lose the rest.',
293
+ parameters: {
294
+ type: 'object',
295
+ properties: {
296
+ urls: {
297
+ type: 'array',
298
+ items: { type: 'string' },
299
+ description: `Absolute page urls, at most ${MAX_URLS} of them.`,
300
+ },
301
+ max_characters: {
302
+ type: 'integer',
303
+ description: `How much of each page to return. Default ${defaultCharacters}, ` +
304
+ `at most ${MAX_CHARACTERS}. Text is cut at the cap and \`truncated\` ` +
305
+ 'says so.',
306
+ },
307
+ max_age_hours: {
308
+ type: 'integer',
309
+ description: 'How stale a cached copy may be. 0 forces a live fetch, which is ' +
310
+ 'slower; omit it unless the page changes by the hour.',
311
+ },
312
+ },
313
+ required: ['urls'],
314
+ additionalProperties: false,
315
+ },
316
+ execute: ({ urls, max_characters, max_age_hours }, tc) => attempt(async () => {
317
+ const wanted = (urls ?? []).map((u) => u?.trim()).filter(Boolean);
318
+ if (wanted.length === 0) {
319
+ return {
320
+ error: 'urls is required',
321
+ hint: 'give at least one absolute url',
322
+ };
323
+ }
324
+ const limit = clamp(max_characters ?? defaultCharacters, 1, MAX_CHARACTERS);
325
+ const body = await exa.post('/contents', {
326
+ urls: wanted.slice(0, MAX_URLS),
327
+ text: { maxCharacters: limit },
328
+ maxAgeHours: pick(max_age_hours),
329
+ }, tc.signal);
330
+ const budget = new Budget(MAX_TOTAL_CHARACTERS);
331
+ const pages = (body.results ?? []).map((r) => {
332
+ const { text, truncated } = budget.take(r.text, limit);
333
+ return { ...source(r), text, truncated: truncated || undefined };
334
+ });
335
+ const failed = (body.statuses ?? [])
336
+ .filter((s) => s.status === 'error')
337
+ .map((s) => ({
338
+ url: s.id,
339
+ error: s.error?.tag ?? 'could not be fetched',
340
+ status: pick(s.error?.httpStatusCode),
341
+ }));
342
+ return {
343
+ pages,
344
+ failed: failed.length ? failed : undefined,
345
+ skipped: wanted.length > MAX_URLS ? wanted.slice(MAX_URLS) : undefined,
346
+ cost_usd: body.costDollars?.total,
347
+ };
348
+ }),
349
+ });
350
+ const webAnswer = tool({
351
+ name: 'web_answer',
352
+ group: EXA_GROUP,
353
+ description: 'Asks a question of the open web and returns a written answer with the sources ' +
354
+ 'it was drawn from. This runs a search and a model on the other side, so it is ' +
355
+ 'slower and more expensive than web_search — reach for it when the question is ' +
356
+ 'settled by a fact spread over several pages, not when a specific document is ' +
357
+ 'wanted. Cite from the returned sources, never from the answer alone.',
358
+ parameters: {
359
+ type: 'object',
360
+ properties: {
361
+ query: {
362
+ type: 'string',
363
+ description: 'The question, in full. Specific questions get direct answers.',
364
+ },
365
+ model: {
366
+ type: 'string',
367
+ enum: ['exa', 'exa-fast'],
368
+ description: '"exa" by default; "exa-fast" answers sooner from fewer sources.',
369
+ },
370
+ },
371
+ required: ['query'],
372
+ additionalProperties: false,
373
+ },
374
+ execute: ({ query, model }, tc) => attempt(async () => {
375
+ const text = query?.trim();
376
+ if (!text) {
377
+ return { error: 'query is required' };
378
+ }
379
+ const body = await exa.post('/answer', { query: text, model: pick(model), text: false }, tc.signal);
380
+ return {
381
+ answer: body.answer,
382
+ sources: (body.citations ?? []).map(source),
383
+ cost_usd: body.costDollars?.total,
384
+ };
385
+ }),
386
+ });
387
+ return [webSearch, webRead, webAnswer];
388
+ }
389
+ //# sourceMappingURL=exa.js.map
@@ -0,0 +1,4 @@
1
+ export * from './exa.ts';
2
+ export * from './sandbox.ts';
3
+ export * from './workspace.ts';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ // Tools a host may hand to a project. Nothing here is wired in by default:
2
+ // what an agent can reach is a deployment decision, so it is opted into.
3
+ export * from "./exa.js";
4
+ export * from "./sandbox.js";
5
+ export * from "./workspace.js";
6
+ //# sourceMappingURL=index.js.map