@inixiative/foundry-core 0.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.
- package/LICENSE +21 -0
- package/package.json +39 -0
- package/src/action-prompt.ts +248 -0
- package/src/adapters/file-memory.ts +718 -0
- package/src/adapters/http-memory.ts +141 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/markdown-docs.ts +213 -0
- package/src/adapters/sqlite-memory.ts +188 -0
- package/src/base-agent.ts +137 -0
- package/src/bounded-set.ts +42 -0
- package/src/cache-lifecycle.ts +187 -0
- package/src/capability.ts +252 -0
- package/src/classifier.ts +43 -0
- package/src/context-layer.ts +706 -0
- package/src/context-stack.ts +223 -0
- package/src/credentials.ts +14 -0
- package/src/decider.ts +62 -0
- package/src/delivery-evidence.ts +85 -0
- package/src/event-stream.ts +105 -0
- package/src/executor.ts +185 -0
- package/src/harness.ts +749 -0
- package/src/hooks.ts +133 -0
- package/src/hydrator.ts +148 -0
- package/src/id.ts +39 -0
- package/src/index.ts +273 -0
- package/src/intervention.ts +134 -0
- package/src/messages.ts +315 -0
- package/src/middleware.ts +118 -0
- package/src/native-evidence.ts +140 -0
- package/src/permission-middleware.ts +92 -0
- package/src/retry.ts +91 -0
- package/src/router.ts +41 -0
- package/src/scope.ts +20 -0
- package/src/signal.ts +117 -0
- package/src/thread.ts +528 -0
- package/src/token-counts.ts +30 -0
- package/src/token-tracker.ts +433 -0
- package/src/tools.ts +714 -0
- package/src/trace.ts +261 -0
- package/src/types.ts +167 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Tools — typed interfaces for agent execution environments
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Agents need to do more than generate text. They need to search code,
|
|
6
|
+
// navigate browsers, call APIs, and execute scripts. Each of these is a
|
|
7
|
+
// "tool" with typed inputs/outputs, capability requirements, and
|
|
8
|
+
// observable side effects.
|
|
9
|
+
//
|
|
10
|
+
// This file defines the interfaces. Implementations live in adapters
|
|
11
|
+
// (core: lightweight, foundry: heavy-infra).
|
|
12
|
+
//
|
|
13
|
+
// Design principles:
|
|
14
|
+
// - Every tool operation maps to a capability for gating
|
|
15
|
+
// - Tool results are structured (JSON), not raw text — models waste
|
|
16
|
+
// fewer tokens parsing unstructured output
|
|
17
|
+
// - Tools are registry-managed so agents discover what's available
|
|
18
|
+
// without bloating their context with tool descriptions upfront
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
import type { Capability } from "./capability";
|
|
22
|
+
import type { OwnershipScope } from "./scope";
|
|
23
|
+
import type { ToolDefinition } from "./types";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Tool result — every tool returns this wrapper
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One executed tool call, as observed by the tool-use loop. Bounded and
|
|
31
|
+
* identity-bearing: the call id plus the dispatch id the loop received let
|
|
32
|
+
* downstream reviewers attribute the call to exactly the work that made it.
|
|
33
|
+
*/
|
|
34
|
+
export interface ToolCallObservation {
|
|
35
|
+
/** Provider-assigned call id, or a generated one when the provider gave none. */
|
|
36
|
+
readonly callId: string;
|
|
37
|
+
readonly tool: string;
|
|
38
|
+
/** Bounded JSON of the call input. */
|
|
39
|
+
readonly inputSummary: string;
|
|
40
|
+
/** Outcome when known. Absent means unknown: never assume success or failure. */
|
|
41
|
+
readonly ok?: boolean;
|
|
42
|
+
/** Bounded result text when ok. */
|
|
43
|
+
readonly outputSummary?: string;
|
|
44
|
+
/** Error text when not ok. */
|
|
45
|
+
readonly error?: string;
|
|
46
|
+
readonly durationMs: number;
|
|
47
|
+
/** 1-based position of this call within its dispatch. */
|
|
48
|
+
readonly sequence: number;
|
|
49
|
+
/** Original lengths of any field that was cut to its bound. */
|
|
50
|
+
readonly truncated?: { readonly input?: number; readonly output?: number; readonly error?: number };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ToolResult<T = unknown> {
|
|
54
|
+
/** Whether the operation succeeded. */
|
|
55
|
+
ok: boolean;
|
|
56
|
+
/** Structured result data. */
|
|
57
|
+
data?: T;
|
|
58
|
+
/** Human-readable summary (for injecting into agent context). */
|
|
59
|
+
summary: string;
|
|
60
|
+
/** Error message if ok=false. */
|
|
61
|
+
error?: string;
|
|
62
|
+
/** Token estimate for the result (helps agents budget context). */
|
|
63
|
+
estimatedTokens?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// BrowserTool — navigate, interact with, and extract data from web pages
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/** Accessibility tree element from a page snapshot. */
|
|
71
|
+
export interface PageElement {
|
|
72
|
+
role: string;
|
|
73
|
+
name: string;
|
|
74
|
+
ref?: string;
|
|
75
|
+
value?: string;
|
|
76
|
+
children?: PageElement[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PageSnapshot {
|
|
80
|
+
url: string;
|
|
81
|
+
title: string;
|
|
82
|
+
elements: PageElement[];
|
|
83
|
+
/** Estimated token count of the full snapshot. */
|
|
84
|
+
estimatedTokens: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface NavigateOpts {
|
|
88
|
+
/** Wait for this selector to appear before returning. */
|
|
89
|
+
waitFor?: string;
|
|
90
|
+
/** Timeout in ms. Default: 30000. */
|
|
91
|
+
timeout?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface BrowserTool {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
readonly kind: "browser";
|
|
97
|
+
|
|
98
|
+
/** Required capabilities for each operation. */
|
|
99
|
+
readonly capabilities: {
|
|
100
|
+
navigate: Capability;
|
|
101
|
+
interact: Capability;
|
|
102
|
+
execute: Capability;
|
|
103
|
+
screenshot: Capability;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Navigate to a URL. */
|
|
107
|
+
navigate(url: string, opts?: NavigateOpts): Promise<ToolResult<{ url: string; title: string }>>;
|
|
108
|
+
|
|
109
|
+
/** Get the current page's accessibility tree snapshot. */
|
|
110
|
+
snapshot(): Promise<ToolResult<PageSnapshot>>;
|
|
111
|
+
|
|
112
|
+
/** Click an element by reference ID or selector. */
|
|
113
|
+
click(ref: string): Promise<ToolResult<void>>;
|
|
114
|
+
|
|
115
|
+
/** Fill a form field by reference ID or selector. */
|
|
116
|
+
fill(ref: string, value: string): Promise<ToolResult<void>>;
|
|
117
|
+
|
|
118
|
+
/** Select an option in a dropdown. */
|
|
119
|
+
select(ref: string, value: string): Promise<ToolResult<void>>;
|
|
120
|
+
|
|
121
|
+
/** Execute JavaScript in the page context. Returns serializable result. */
|
|
122
|
+
evaluate<T = unknown>(script: string): Promise<ToolResult<T>>;
|
|
123
|
+
|
|
124
|
+
/** Take a screenshot. Returns base64-encoded image. */
|
|
125
|
+
screenshot(): Promise<ToolResult<{ base64: string; mimeType: string }>>;
|
|
126
|
+
|
|
127
|
+
/** Get current page URL. */
|
|
128
|
+
currentUrl(): Promise<string>;
|
|
129
|
+
|
|
130
|
+
/** Close the browser / release resources. */
|
|
131
|
+
close(): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// ApiTool — structured HTTP requests (better than curl)
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
export interface ApiRequest {
|
|
139
|
+
url: string;
|
|
140
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
141
|
+
headers?: Record<string, string>;
|
|
142
|
+
body?: unknown;
|
|
143
|
+
/** Timeout in ms. Default: 30000. */
|
|
144
|
+
timeout?: number;
|
|
145
|
+
/** Parse response body as this type. Default: "json". */
|
|
146
|
+
responseType?: "json" | "text" | "buffer";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ApiResponse<T = unknown> {
|
|
150
|
+
status: number;
|
|
151
|
+
statusText: string;
|
|
152
|
+
headers: Record<string, string>;
|
|
153
|
+
body: T;
|
|
154
|
+
/** Round-trip time in ms. */
|
|
155
|
+
durationMs: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ApiToolConfig {
|
|
159
|
+
/** Base URL prepended to relative paths. */
|
|
160
|
+
baseUrl?: string;
|
|
161
|
+
/** Default headers applied to every request. */
|
|
162
|
+
defaultHeaders?: Record<string, string>;
|
|
163
|
+
/** Auth token applied as Authorization: Bearer header. */
|
|
164
|
+
bearerToken?: string;
|
|
165
|
+
/** Max response body size in bytes before truncation. Default: 1MB. */
|
|
166
|
+
maxResponseSize?: number;
|
|
167
|
+
/** Allowed URL patterns (glob). Empty = allow all. */
|
|
168
|
+
allowedUrls?: string[];
|
|
169
|
+
/** Blocked URL patterns (glob). Takes precedence. */
|
|
170
|
+
blockedUrls?: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ApiTool {
|
|
174
|
+
readonly id: string;
|
|
175
|
+
readonly kind: "api";
|
|
176
|
+
|
|
177
|
+
/** Required capability. */
|
|
178
|
+
readonly capability: Capability;
|
|
179
|
+
|
|
180
|
+
/** Bind an authority-aware adapter to the caller's runtime-owned scope. */
|
|
181
|
+
scoped?(scope: OwnershipScope): ApiTool;
|
|
182
|
+
|
|
183
|
+
/** Make an HTTP request. */
|
|
184
|
+
request<T = unknown>(req: ApiRequest): Promise<ToolResult<ApiResponse<T>>>;
|
|
185
|
+
|
|
186
|
+
/** Convenience: GET request. */
|
|
187
|
+
get<T = unknown>(url: string, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
|
|
188
|
+
|
|
189
|
+
/** Convenience: POST request with JSON body. */
|
|
190
|
+
post<T = unknown>(url: string, body: unknown, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
|
|
191
|
+
|
|
192
|
+
/** Convenience: PUT request with JSON body. */
|
|
193
|
+
put<T = unknown>(url: string, body: unknown, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
|
|
194
|
+
|
|
195
|
+
/** Convenience: DELETE request. */
|
|
196
|
+
delete<T = unknown>(url: string, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// ShellTool — structured shell execution (better than raw bash)
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
export interface ShellResult {
|
|
204
|
+
exitCode: number;
|
|
205
|
+
stdout: string;
|
|
206
|
+
stderr: string;
|
|
207
|
+
/** Whether stdout was truncated to fit token budget. */
|
|
208
|
+
truncated: boolean;
|
|
209
|
+
durationMs: number;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Output filter function — transforms command output before returning.
|
|
214
|
+
* Use this for RTK-style token reduction: strip noise, compress
|
|
215
|
+
* repetitive output, remove ANSI codes, etc.
|
|
216
|
+
*/
|
|
217
|
+
export type OutputFilter = (stdout: string, command: string) => string;
|
|
218
|
+
|
|
219
|
+
export interface ShellOpts {
|
|
220
|
+
/** Working directory. */
|
|
221
|
+
cwd?: string;
|
|
222
|
+
/** Environment variables. */
|
|
223
|
+
env?: Record<string, string>;
|
|
224
|
+
/** Timeout in ms. Default: 120000. */
|
|
225
|
+
timeout?: number;
|
|
226
|
+
/** Max stdout bytes before truncation. Default: 100KB. */
|
|
227
|
+
maxOutput?: number;
|
|
228
|
+
/** Run in sandboxed mode (just-bash). */
|
|
229
|
+
sandbox?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Output filter applied before returning results.
|
|
232
|
+
* Use for RTK-style token savings — strip ANSI, compress whitespace,
|
|
233
|
+
* remove noise lines, etc. Applied after execution, before truncation.
|
|
234
|
+
*/
|
|
235
|
+
outputFilter?: OutputFilter;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface ShellTool {
|
|
239
|
+
readonly id: string;
|
|
240
|
+
readonly kind: "shell";
|
|
241
|
+
|
|
242
|
+
/** Required capability. */
|
|
243
|
+
readonly capability: Capability;
|
|
244
|
+
|
|
245
|
+
/** Execute a shell command. */
|
|
246
|
+
exec(command: string, opts?: ShellOpts): Promise<ToolResult<ShellResult>>;
|
|
247
|
+
|
|
248
|
+
/** Execute and return only stdout (convenience). */
|
|
249
|
+
run(command: string, opts?: ShellOpts): Promise<string>;
|
|
250
|
+
|
|
251
|
+
/** Check if a command exists. */
|
|
252
|
+
which(command: string): Promise<string | null>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// ScriptTool — TypeScript/JS execution in isolate
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
export interface ScriptResult<T = unknown> {
|
|
260
|
+
/** The return value of the script. */
|
|
261
|
+
result: T;
|
|
262
|
+
/** Console output captured during execution. */
|
|
263
|
+
logs: string[];
|
|
264
|
+
durationMs: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface ScriptOpts {
|
|
268
|
+
/** Timeout in ms. Default: 30000. */
|
|
269
|
+
timeout?: number;
|
|
270
|
+
/** Modules available to the script (injected into scope). */
|
|
271
|
+
modules?: Record<string, unknown>;
|
|
272
|
+
/** Whether to capture console.log output. Default: true. */
|
|
273
|
+
captureLogs?: boolean;
|
|
274
|
+
/** Working directory for the subprocess. */
|
|
275
|
+
cwd?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface ScriptTool {
|
|
279
|
+
readonly id: string;
|
|
280
|
+
readonly kind: "script";
|
|
281
|
+
|
|
282
|
+
/** Required capability. */
|
|
283
|
+
readonly capability: Capability;
|
|
284
|
+
|
|
285
|
+
/** Execute a TypeScript/JS script and return the result. */
|
|
286
|
+
evaluate<T = unknown>(code: string, opts?: ScriptOpts): Promise<ToolResult<ScriptResult<T>>>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// MemoryTool — query and store across any memory backend
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
//
|
|
293
|
+
// Wraps the common contract that all Foundry memory adapters follow:
|
|
294
|
+
// write/get/search/delete + asSource + signalWriter.
|
|
295
|
+
//
|
|
296
|
+
// Why this matters as a tool (vs just a layer source):
|
|
297
|
+
// - Layers inject context passively at warm time
|
|
298
|
+
// - MemoryTool lets agents query on demand during execution
|
|
299
|
+
// - Agent decides what to search for based on the task, not upfront config
|
|
300
|
+
// - Results go through ToolResult (structured, token-estimated, truncated)
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Who may read a memory entry.
|
|
304
|
+
* - "thread": only the owning thread.
|
|
305
|
+
* - "project": every thread of the owning project (explicitly published).
|
|
306
|
+
* - "global": every thread of every project (explicitly published).
|
|
307
|
+
* Entries without a visibility are unowned legacy records: hidden from every
|
|
308
|
+
* scoped read unless a source explicitly opts in.
|
|
309
|
+
*/
|
|
310
|
+
export type MemoryVisibility = "thread" | "project" | "global";
|
|
311
|
+
|
|
312
|
+
export interface MemoryEntry {
|
|
313
|
+
id: string;
|
|
314
|
+
kind: string;
|
|
315
|
+
content: string;
|
|
316
|
+
source?: string;
|
|
317
|
+
timestamp: number;
|
|
318
|
+
meta?: Record<string, unknown>;
|
|
319
|
+
/** Thread/project that captured this entry. */
|
|
320
|
+
owner?: OwnershipScope;
|
|
321
|
+
/** Declared read scope. Absent on unowned legacy records. */
|
|
322
|
+
visibility?: MemoryVisibility;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface MemorySearchOpts {
|
|
326
|
+
/** Filter by entry kind. */
|
|
327
|
+
kind?: string;
|
|
328
|
+
/** Max results. Default: 20. */
|
|
329
|
+
limit?: number;
|
|
330
|
+
/** Minimum relevance score (0-1) if backend supports scoring. */
|
|
331
|
+
minScore?: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface MemoryTool {
|
|
335
|
+
readonly id: string;
|
|
336
|
+
readonly kind: "memory";
|
|
337
|
+
/** The underlying system name (e.g., "file", "sqlite", "redis", "supermemory"). */
|
|
338
|
+
readonly system: string;
|
|
339
|
+
|
|
340
|
+
/** Required capabilities. */
|
|
341
|
+
readonly capabilities: {
|
|
342
|
+
read: Capability;
|
|
343
|
+
write: Capability;
|
|
344
|
+
delete: Capability;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Search memory by query string. */
|
|
348
|
+
search(query: string, opts?: MemorySearchOpts): Promise<ToolResult<MemoryEntry[]>>;
|
|
349
|
+
|
|
350
|
+
/** Get a specific entry by ID. */
|
|
351
|
+
get(id: string): Promise<ToolResult<MemoryEntry | null>>;
|
|
352
|
+
|
|
353
|
+
/** List recent entries. */
|
|
354
|
+
recent(limit?: number, kind?: string): Promise<ToolResult<MemoryEntry[]>>;
|
|
355
|
+
|
|
356
|
+
/** Write an entry to memory. */
|
|
357
|
+
write(entry: MemoryEntry): Promise<ToolResult<{ id: string }>>;
|
|
358
|
+
|
|
359
|
+
/** Delete an entry. */
|
|
360
|
+
delete(id: string): Promise<ToolResult<{ deleted: boolean }>>;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* A view of this tool restricted to one thread/project. Reads return only
|
|
364
|
+
* entries visible to that scope; writes are owned by it. Tools that cannot
|
|
365
|
+
* scope must not pretend to: the registry only trusts this method.
|
|
366
|
+
*/
|
|
367
|
+
scoped?(scope: OwnershipScope): MemoryTool;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Tool union type + registry
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
/** Any tool that can be registered. */
|
|
375
|
+
export type Tool = BrowserTool | ApiTool | ShellTool | ScriptTool | MemoryTool;
|
|
376
|
+
|
|
377
|
+
/** Tool kind discriminator. */
|
|
378
|
+
export type ToolKind = Tool["kind"];
|
|
379
|
+
|
|
380
|
+
/** Metadata about a registered tool (what agents see before using it). */
|
|
381
|
+
export interface ToolInfo {
|
|
382
|
+
id: string;
|
|
383
|
+
kind: ToolKind;
|
|
384
|
+
/** Short description for agent context. */
|
|
385
|
+
description: string;
|
|
386
|
+
/** Capabilities required to use this tool. */
|
|
387
|
+
capabilities: Capability[];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* ToolRegistry — agents discover and access tools through this.
|
|
392
|
+
*
|
|
393
|
+
* Instead of injecting all tool descriptions into agent context (which bloats
|
|
394
|
+
* the prompt), agents query the registry for tools matching their needs.
|
|
395
|
+
* The registry returns typed tool instances that go through capability gating.
|
|
396
|
+
*
|
|
397
|
+
* This is the "connector information" layer — adapters register here,
|
|
398
|
+
* agents consume from here.
|
|
399
|
+
*/
|
|
400
|
+
export class ToolRegistry {
|
|
401
|
+
private _tools = new Map<string, Tool>();
|
|
402
|
+
private _info = new Map<string, ToolInfo>();
|
|
403
|
+
|
|
404
|
+
/** Register a tool with its metadata. */
|
|
405
|
+
register(tool: Tool, description: string): void {
|
|
406
|
+
this._tools.set(tool.id, tool);
|
|
407
|
+
|
|
408
|
+
const capabilities: Capability[] = [];
|
|
409
|
+
if (tool.kind === "browser") {
|
|
410
|
+
capabilities.push(
|
|
411
|
+
tool.capabilities.navigate,
|
|
412
|
+
tool.capabilities.interact,
|
|
413
|
+
tool.capabilities.execute,
|
|
414
|
+
tool.capabilities.screenshot,
|
|
415
|
+
);
|
|
416
|
+
} else if (tool.kind === "memory") {
|
|
417
|
+
capabilities.push(
|
|
418
|
+
tool.capabilities.read,
|
|
419
|
+
tool.capabilities.write,
|
|
420
|
+
tool.capabilities.delete,
|
|
421
|
+
);
|
|
422
|
+
} else {
|
|
423
|
+
capabilities.push(tool.capability);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this._info.set(tool.id, {
|
|
427
|
+
id: tool.id,
|
|
428
|
+
kind: tool.kind,
|
|
429
|
+
description,
|
|
430
|
+
capabilities,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Unregister a tool. */
|
|
435
|
+
unregister(id: string): void {
|
|
436
|
+
this._tools.delete(id);
|
|
437
|
+
this._info.delete(id);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Get a tool by ID. */
|
|
441
|
+
get<T extends Tool = Tool>(id: string): T | undefined {
|
|
442
|
+
return this._tools.get(id) as T | undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Get a tool by kind (returns first match). */
|
|
446
|
+
byKind<K extends ToolKind>(kind: K): Extract<Tool, { kind: K }> | undefined {
|
|
447
|
+
for (const tool of this._tools.values()) {
|
|
448
|
+
if (tool.kind === kind) return tool as Extract<Tool, { kind: K }>;
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Get all tools of a given kind. */
|
|
454
|
+
allByKind<K extends ToolKind>(kind: K): Extract<Tool, { kind: K }>[] {
|
|
455
|
+
const result: Extract<Tool, { kind: K }>[] = [];
|
|
456
|
+
for (const tool of this._tools.values()) {
|
|
457
|
+
if (tool.kind === kind) result.push(tool as Extract<Tool, { kind: K }>);
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** List all registered tool metadata (cheap — for agent context injection). */
|
|
463
|
+
list(): ToolInfo[] {
|
|
464
|
+
return Array.from(this._info.values());
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** List tools matching a capability filter. */
|
|
468
|
+
listWithCapability(cap: Capability): ToolInfo[] {
|
|
469
|
+
return this.list().filter((info) => info.capabilities.includes(cap));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Get a compact summary string for injecting into agent context. */
|
|
473
|
+
summary(): string {
|
|
474
|
+
const tools = this.list();
|
|
475
|
+
if (tools.length === 0) return "No tools available.";
|
|
476
|
+
return tools
|
|
477
|
+
.map((t) => `- ${t.id} (${t.kind}): ${t.description}`)
|
|
478
|
+
.join("\n");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Generate LLM-compatible tool definitions from all registered tools.
|
|
483
|
+
* Each tool method becomes a separate function the LLM can call.
|
|
484
|
+
*/
|
|
485
|
+
toToolDefinitions(): ToolDefinition[] {
|
|
486
|
+
const defs: ToolDefinition[] = [];
|
|
487
|
+
|
|
488
|
+
for (const [id, tool] of this._tools) {
|
|
489
|
+
const desc = this._info.get(id)?.description ?? id;
|
|
490
|
+
|
|
491
|
+
switch (tool.kind) {
|
|
492
|
+
case "shell":
|
|
493
|
+
defs.push({
|
|
494
|
+
name: `${id}_exec`,
|
|
495
|
+
description: `[${id}] ${desc} — execute a shell command`,
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
500
|
+
},
|
|
501
|
+
required: ["command"],
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
break;
|
|
505
|
+
|
|
506
|
+
case "memory":
|
|
507
|
+
defs.push(
|
|
508
|
+
{
|
|
509
|
+
name: `${id}_search`,
|
|
510
|
+
description: `[${id}] Search ${desc}`,
|
|
511
|
+
inputSchema: {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties: {
|
|
514
|
+
query: { type: "string", description: "Search query" },
|
|
515
|
+
kind: { type: "string", description: "Filter by entry kind (optional)" },
|
|
516
|
+
limit: { type: "number", description: "Max results (default: 20)" },
|
|
517
|
+
},
|
|
518
|
+
required: ["query"],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: `${id}_get`,
|
|
523
|
+
description: `[${id}] Get entry by ID from ${desc}`,
|
|
524
|
+
inputSchema: {
|
|
525
|
+
type: "object",
|
|
526
|
+
properties: { id: { type: "string" } },
|
|
527
|
+
required: ["id"],
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: `${id}_write`,
|
|
532
|
+
description: `[${id}] Write an entry to ${desc}`,
|
|
533
|
+
inputSchema: {
|
|
534
|
+
type: "object",
|
|
535
|
+
properties: {
|
|
536
|
+
id: { type: "string" },
|
|
537
|
+
kind: { type: "string" },
|
|
538
|
+
content: { type: "string" },
|
|
539
|
+
visibility: {
|
|
540
|
+
type: "string",
|
|
541
|
+
enum: ["thread", "project", "global"],
|
|
542
|
+
description: "Who may read it. Default: thread (private). project/global publish it deliberately.",
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
required: ["id", "kind", "content"],
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
case "script":
|
|
552
|
+
defs.push({
|
|
553
|
+
name: `${id}_evaluate`,
|
|
554
|
+
description: `[${id}] ${desc} — execute code and return result`,
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
code: { type: "string", description: "TypeScript/JS code to execute. Use 'return' to return a value." },
|
|
559
|
+
},
|
|
560
|
+
required: ["code"],
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
case "api":
|
|
566
|
+
defs.push({
|
|
567
|
+
name: `${id}_request`,
|
|
568
|
+
description: `[${id}] ${desc} — make an HTTP request`,
|
|
569
|
+
inputSchema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
url: { type: "string" },
|
|
573
|
+
method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] },
|
|
574
|
+
body: { description: "Request body (JSON)" },
|
|
575
|
+
headers: { type: "object", description: "Additional headers" },
|
|
576
|
+
},
|
|
577
|
+
required: ["url"],
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
|
|
582
|
+
case "browser":
|
|
583
|
+
defs.push(
|
|
584
|
+
{
|
|
585
|
+
name: `${id}_navigate`,
|
|
586
|
+
description: `[${id}] Navigate to a URL`,
|
|
587
|
+
inputSchema: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: { url: { type: "string" } },
|
|
590
|
+
required: ["url"],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: `${id}_snapshot`,
|
|
595
|
+
description: `[${id}] Get page accessibility snapshot`,
|
|
596
|
+
inputSchema: { type: "object", properties: {} },
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: `${id}_click`,
|
|
600
|
+
description: `[${id}] Click an element`,
|
|
601
|
+
inputSchema: {
|
|
602
|
+
type: "object",
|
|
603
|
+
properties: { ref: { type: "string", description: "CSS selector or ref" } },
|
|
604
|
+
required: ["ref"],
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: `${id}_evaluate`,
|
|
609
|
+
description: `[${id}] Execute JavaScript in page`,
|
|
610
|
+
inputSchema: {
|
|
611
|
+
type: "object",
|
|
612
|
+
properties: { script: { type: "string" } },
|
|
613
|
+
required: ["script"],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return defs;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Dispatch a tool call by name. Routes "toolId_method" to the right tool.
|
|
626
|
+
* Returns the serialized result string.
|
|
627
|
+
*/
|
|
628
|
+
async dispatch(
|
|
629
|
+
toolName: string,
|
|
630
|
+
input: Record<string, unknown>,
|
|
631
|
+
opts?: { cwd?: string; scope?: OwnershipScope },
|
|
632
|
+
): Promise<ToolResult> {
|
|
633
|
+
// Parse "toolId_method" format
|
|
634
|
+
const lastUnderscore = toolName.lastIndexOf("_");
|
|
635
|
+
if (lastUnderscore === -1) {
|
|
636
|
+
return { ok: false, summary: `Unknown tool: ${toolName}`, error: "Invalid tool name format" };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Try progressively shorter prefixes (tool IDs can contain underscores)
|
|
640
|
+
let tool: Tool | undefined;
|
|
641
|
+
let method = "";
|
|
642
|
+
for (let i = toolName.length - 1; i >= 0; i--) {
|
|
643
|
+
if (toolName[i] === "_") {
|
|
644
|
+
const candidateId = toolName.slice(0, i);
|
|
645
|
+
const candidateMethod = toolName.slice(i + 1);
|
|
646
|
+
if (this._tools.has(candidateId)) {
|
|
647
|
+
tool = this._tools.get(candidateId);
|
|
648
|
+
method = candidateMethod;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (!tool) {
|
|
655
|
+
return { ok: false, summary: `Unknown tool: ${toolName}`, error: `No registered tool matches "${toolName}"` };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
switch (tool.kind) {
|
|
660
|
+
case "shell":
|
|
661
|
+
if (method === "exec") return await tool.exec(input.command as string, { cwd: opts?.cwd });
|
|
662
|
+
break;
|
|
663
|
+
|
|
664
|
+
case "memory": {
|
|
665
|
+
// A scoped dispatch may only reach a tool that can honor the scope.
|
|
666
|
+
const memory = opts?.scope ? tool.scoped?.(opts.scope) : tool;
|
|
667
|
+
if (!memory) {
|
|
668
|
+
return { ok: false, summary: `Memory tool "${tool.id}" cannot scope reads to a thread`, error: "Unscoped memory tool" };
|
|
669
|
+
}
|
|
670
|
+
if (method === "search") return await memory.search(input.query as string, { kind: input.kind as string, limit: input.limit as number });
|
|
671
|
+
if (method === "get") return await memory.get(input.id as string);
|
|
672
|
+
if (method === "write") {
|
|
673
|
+
const visibility = input.visibility as MemoryVisibility | undefined;
|
|
674
|
+
return await memory.write({
|
|
675
|
+
id: input.id as string,
|
|
676
|
+
kind: input.kind as string,
|
|
677
|
+
content: input.content as string,
|
|
678
|
+
timestamp: Date.now(),
|
|
679
|
+
...(visibility ? { visibility } : {}),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
case "script":
|
|
686
|
+
if (method === "evaluate") return await tool.evaluate(input.code as string, { cwd: opts?.cwd });
|
|
687
|
+
break;
|
|
688
|
+
|
|
689
|
+
case "api":
|
|
690
|
+
if (method === "request") {
|
|
691
|
+
const api = tool.scoped ? tool.scoped(opts?.scope ?? {}) : tool;
|
|
692
|
+
return await api.request({ url: input.url as string, method: (input.method as any) ?? "GET", body: input.body, headers: input.headers as Record<string, string> });
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
|
|
696
|
+
case "browser":
|
|
697
|
+
if (method === "navigate") return await tool.navigate(input.url as string);
|
|
698
|
+
if (method === "snapshot") return await tool.snapshot();
|
|
699
|
+
if (method === "click") return await tool.click(input.ref as string);
|
|
700
|
+
if (method === "evaluate") return await tool.evaluate(input.script as string);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return { ok: false, summary: `Unknown method "${method}" for tool kind "${tool.kind}"`, error: "Method not found" };
|
|
705
|
+
} catch (err) {
|
|
706
|
+
return { ok: false, summary: `Tool error: ${(err as Error).message}`, error: (err as Error).message };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Number of registered tools. */
|
|
711
|
+
get size(): number {
|
|
712
|
+
return this._tools.size;
|
|
713
|
+
}
|
|
714
|
+
}
|