@dex-ai/sdk 0.1.30

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 (85) hide show
  1. package/README.md +308 -0
  2. package/dist/agent.d.ts +181 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +41 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/context.d.ts +68 -0
  7. package/dist/context.d.ts.map +1 -0
  8. package/dist/context.js +8 -0
  9. package/dist/context.js.map +1 -0
  10. package/dist/create-agent.d.ts +7 -0
  11. package/dist/create-agent.d.ts.map +1 -0
  12. package/dist/create-agent.js +205 -0
  13. package/dist/create-agent.js.map +1 -0
  14. package/dist/extension.d.ts +162 -0
  15. package/dist/extension.d.ts.map +1 -0
  16. package/dist/extension.js +20 -0
  17. package/dist/extension.js.map +1 -0
  18. package/dist/generate.d.ts +10 -0
  19. package/dist/generate.d.ts.map +1 -0
  20. package/dist/generate.js +839 -0
  21. package/dist/generate.js.map +1 -0
  22. package/dist/index.d.ts +26 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +16 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/message.d.ts +89 -0
  27. package/dist/message.d.ts.map +1 -0
  28. package/dist/message.js +17 -0
  29. package/dist/message.js.map +1 -0
  30. package/dist/messages.d.ts +98 -0
  31. package/dist/messages.d.ts.map +1 -0
  32. package/dist/messages.js +339 -0
  33. package/dist/messages.js.map +1 -0
  34. package/dist/model.d.ts +39 -0
  35. package/dist/model.d.ts.map +1 -0
  36. package/dist/model.js +11 -0
  37. package/dist/model.js.map +1 -0
  38. package/dist/provider.d.ts +157 -0
  39. package/dist/provider.d.ts.map +1 -0
  40. package/dist/provider.js +39 -0
  41. package/dist/provider.js.map +1 -0
  42. package/dist/resolve-schema.d.ts +44 -0
  43. package/dist/resolve-schema.d.ts.map +1 -0
  44. package/dist/resolve-schema.js +367 -0
  45. package/dist/resolve-schema.js.map +1 -0
  46. package/dist/schema.d.ts +80 -0
  47. package/dist/schema.d.ts.map +1 -0
  48. package/dist/schema.js +90 -0
  49. package/dist/schema.js.map +1 -0
  50. package/dist/tool-dispatch.d.ts +24 -0
  51. package/dist/tool-dispatch.d.ts.map +1 -0
  52. package/dist/tool-dispatch.js +120 -0
  53. package/dist/tool-dispatch.js.map +1 -0
  54. package/dist/tool-result-cache.d.ts +43 -0
  55. package/dist/tool-result-cache.d.ts.map +1 -0
  56. package/dist/tool-result-cache.js +118 -0
  57. package/dist/tool-result-cache.js.map +1 -0
  58. package/dist/tool.d.ts +96 -0
  59. package/dist/tool.d.ts.map +1 -0
  60. package/dist/tool.js +29 -0
  61. package/dist/tool.js.map +1 -0
  62. package/dist/util.d.ts +26 -0
  63. package/dist/util.d.ts.map +1 -0
  64. package/dist/util.js +104 -0
  65. package/dist/util.js.map +1 -0
  66. package/package.json +41 -0
  67. package/src/agent.ts +235 -0
  68. package/src/context.ts +82 -0
  69. package/src/create-agent.ts +237 -0
  70. package/src/extension.ts +244 -0
  71. package/src/generate.ts +943 -0
  72. package/src/index.ts +113 -0
  73. package/src/message.ts +114 -0
  74. package/src/messages.test.ts +299 -0
  75. package/src/messages.ts +423 -0
  76. package/src/model.ts +43 -0
  77. package/src/provider.ts +187 -0
  78. package/src/resolve-schema.test.ts +351 -0
  79. package/src/resolve-schema.ts +426 -0
  80. package/src/schema.ts +131 -0
  81. package/src/tool-dispatch.ts +166 -0
  82. package/src/tool-result-cache.test.ts +182 -0
  83. package/src/tool-result-cache.ts +164 -0
  84. package/src/tool.ts +110 -0
  85. package/src/util.ts +110 -0
@@ -0,0 +1,166 @@
1
+ /** Dispatch one tool call: validate input → execute → return result. */
2
+
3
+ import type {
4
+ AnyTool,
5
+ Extension,
6
+ GenerateContext,
7
+ ToolCall,
8
+ ToolResult,
9
+ ToolOutput,
10
+ ExecuteResult,
11
+ ErrorSource,
12
+ } from "./index";
13
+ import { validateWithSchema } from "./util";
14
+ import { cacheIfLarge, type ResolvedCacheConfig } from "./tool-result-cache";
15
+
16
+ const MAX_TOOL_OUTPUT_LINES = 1000;
17
+ const MAX_TOOL_OUTPUT_CHARS = 40_000;
18
+
19
+ /**
20
+ * Truncate tool output text to fit within limits.
21
+ * Keeps the tail (most recent output) and prepends a truncation notice.
22
+ */
23
+ function truncateText(text: string): string {
24
+ const lines = text.split("\n");
25
+ let result = text;
26
+ let truncated = false;
27
+
28
+ if (lines.length > MAX_TOOL_OUTPUT_LINES) {
29
+ result = lines.slice(-MAX_TOOL_OUTPUT_LINES).join("\n");
30
+ truncated = true;
31
+ }
32
+
33
+ if (result.length > MAX_TOOL_OUTPUT_CHARS) {
34
+ result = result.slice(-MAX_TOOL_OUTPUT_CHARS);
35
+ truncated = true;
36
+ }
37
+
38
+ if (truncated) {
39
+ return (
40
+ "[OUTPUT TRUNCATED — showing last portion only. " +
41
+ "Use more targeted commands to reduce output if needed.]\n" +
42
+ result
43
+ );
44
+ }
45
+ return text;
46
+ }
47
+
48
+ /** Truncate a ToolOutput if it exceeds size limits. */
49
+ function truncateToolOutput(output: ToolOutput): ToolOutput {
50
+ switch (output.type) {
51
+ case "text":
52
+ return { type: "text", value: truncateText(output.value) };
53
+ case "error-text":
54
+ return { type: "error-text", value: truncateText(output.value) };
55
+ case "json": {
56
+ const serialized = JSON.stringify(output.value, null, 2);
57
+ const truncatedStr = truncateText(serialized);
58
+ if (truncatedStr !== serialized) {
59
+ // Truncated JSON is no longer valid JSON — return as text
60
+ return { type: "text", value: truncatedStr };
61
+ }
62
+ return output;
63
+ }
64
+ case "content": {
65
+ const parts = output.value.map((part) => {
66
+ if (part.type === "text") {
67
+ return { ...part, text: truncateText(part.text) };
68
+ }
69
+ return part;
70
+ });
71
+ return { type: "content", value: parts };
72
+ }
73
+ default:
74
+ return output;
75
+ }
76
+ }
77
+
78
+ /** Wrap an execute() return value into a ToolOutput tagged variant. */
79
+ export function toToolOutput(result: ExecuteResult<unknown>): ToolOutput {
80
+ if (typeof result === "object" && result !== null && "type" in result) {
81
+ const t = (result as { type: string }).type;
82
+ if (
83
+ t === "content" ||
84
+ t === "json" ||
85
+ t === "text" ||
86
+ t === "error-text" ||
87
+ t === "error-json"
88
+ ) {
89
+ return result as ToolOutput;
90
+ }
91
+ }
92
+ return { type: "json", value: result };
93
+ }
94
+
95
+ export interface DispatchOptions {
96
+ readonly call: ToolCall;
97
+ readonly tool: AnyTool;
98
+ readonly extensions: ReadonlyArray<Extension>;
99
+ readonly gctx: GenerateContext;
100
+ readonly reportError: (err: unknown, source: ErrorSource) => Promise<void>;
101
+ readonly cacheConfig?: ResolvedCacheConfig;
102
+ }
103
+
104
+ export interface DispatchOutcome {
105
+ result: ToolResult;
106
+ shortCircuited: boolean;
107
+ executeError?: unknown;
108
+ }
109
+
110
+ /**
111
+ * Validate and execute a tool call. Tool-start/tool-stop events are handled
112
+ * by the generate loop — this function only does validation + execution.
113
+ */
114
+ export async function dispatchTool(
115
+ opts: DispatchOptions,
116
+ ): Promise<DispatchOutcome> {
117
+ const { call, tool, gctx, reportError } = opts;
118
+
119
+ // 1. Validate input against tool.parameters.
120
+ const validated = await validateWithSchema(tool.parameters, call.input);
121
+ if ("issues" in validated) {
122
+ const msg = validated.issues
123
+ .map((i) => (i.path?.length ? i.path.join(".") + ": " : "") + i.message)
124
+ .join("; ");
125
+ return {
126
+ result: {
127
+ toolCallId: call.toolCallId,
128
+ toolName: call.toolName,
129
+ output: { type: "error-text", value: "Invalid tool input: " + msg },
130
+ },
131
+ shortCircuited: false,
132
+ };
133
+ }
134
+
135
+ // 2. Execute.
136
+ let executeError: unknown | undefined;
137
+ let result: ToolResult;
138
+ try {
139
+ const raw = await tool.execute(validated.value as any, gctx);
140
+ let output = toToolOutput(raw);
141
+ // Cache large results before truncation — agent can explore via `read`
142
+ if (opts.cacheConfig) {
143
+ output = await cacheIfLarge(
144
+ output,
145
+ call.toolName,
146
+ call.toolCallId,
147
+ opts.cacheConfig,
148
+ );
149
+ }
150
+ output = truncateToolOutput(output);
151
+ result = { toolCallId: call.toolCallId, toolName: call.toolName, output };
152
+ } catch (err) {
153
+ executeError = err;
154
+ await reportError(err, { kind: "tool", call });
155
+ result = {
156
+ toolCallId: call.toolCallId,
157
+ toolName: call.toolName,
158
+ output: {
159
+ type: "error-text",
160
+ value: err instanceof Error ? err.message : String(err),
161
+ },
162
+ };
163
+ }
164
+
165
+ return { result, shortCircuited: false, executeError };
166
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Tests for tool-result-cache.ts
3
+ */
4
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
5
+ import { mkdtemp, readFile, rm, readdir } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import {
9
+ cacheIfLarge,
10
+ cleanCacheDir,
11
+ resolveConfig,
12
+ type ResolvedCacheConfig,
13
+ } from "./tool-result-cache";
14
+ import type { ToolOutput } from "./message";
15
+
16
+ describe("resolveConfig", () => {
17
+ test("fills defaults when no config provided", () => {
18
+ const config = resolveConfig();
19
+ expect(config.threshold).toBe(10_000);
20
+ expect(config.cacheDir).toContain(".dex");
21
+ expect(config.ttlMs).toBe(86_400_000);
22
+ expect(config.excludedTools).toEqual(["read"]);
23
+ });
24
+
25
+ test("respects provided values", () => {
26
+ const config = resolveConfig({
27
+ threshold: 5000,
28
+ cacheDir: "/tmp/test-cache",
29
+ ttlMs: 1000,
30
+ excludedTools: ["read", "search"],
31
+ });
32
+ expect(config.threshold).toBe(5000);
33
+ expect(config.cacheDir).toBe("/tmp/test-cache");
34
+ expect(config.ttlMs).toBe(1000);
35
+ expect(config.excludedTools).toEqual(["read", "search"]);
36
+ });
37
+ });
38
+
39
+ describe("cacheIfLarge", () => {
40
+ let cacheDir: string;
41
+ let config: ResolvedCacheConfig;
42
+
43
+ beforeEach(async () => {
44
+ cacheDir = await mkdtemp(join(tmpdir(), "dex-cache-test-"));
45
+ config = {
46
+ threshold: 100,
47
+ cacheDir,
48
+ ttlMs: 86_400_000,
49
+ excludedTools: ["read"],
50
+ };
51
+ });
52
+
53
+ afterEach(async () => {
54
+ await rm(cacheDir, { recursive: true, force: true });
55
+ });
56
+
57
+ test("passes through small text output unchanged", async () => {
58
+ const output: ToolOutput = { type: "text", value: "hello world" };
59
+ const result = await cacheIfLarge(output, "bash", "call_1", config);
60
+ expect(result).toBe(output);
61
+ });
62
+
63
+ test("passes through excluded tools unchanged", async () => {
64
+ const bigText = "x".repeat(200);
65
+ const output: ToolOutput = { type: "text", value: bigText };
66
+ const result = await cacheIfLarge(output, "read", "call_2", config);
67
+ expect(result).toBe(output); // unchanged — read is excluded
68
+ });
69
+
70
+ test("caches large text output and returns tail with header", async () => {
71
+ const bigText = "a".repeat(50) + "b".repeat(150);
72
+ const output: ToolOutput = { type: "text", value: bigText };
73
+ const result = await cacheIfLarge(output, "bash", "call_3", config);
74
+
75
+ // Should be transformed
76
+ expect(result.type).toBe("text");
77
+ const value = (result as { type: "text"; value: string }).value;
78
+ expect(value).toContain("[Tool output cached");
79
+ expect(value).toContain("call_3");
80
+ expect(value).toContain("200"); // total length
81
+
82
+ // Tail should be last 100 chars
83
+ expect(value).toEndWith("b".repeat(100));
84
+
85
+ // File should exist on disk with full content
86
+ const cached = await readFile(join(cacheDir, "call_3"), "utf-8");
87
+ expect(cached).toBe(bigText);
88
+ });
89
+
90
+ test("caches large JSON output", async () => {
91
+ const bigObj = { data: "x".repeat(200) };
92
+ const output: ToolOutput = { type: "json", value: bigObj };
93
+ const result = await cacheIfLarge(output, "search", "call_4", config);
94
+
95
+ expect(result.type).toBe("text"); // JSON is serialized to text
96
+ const value = (result as { type: "text"; value: string }).value;
97
+ expect(value).toContain("[Tool output cached");
98
+
99
+ // Full JSON should be on disk
100
+ const cached = await readFile(join(cacheDir, "call_4"), "utf-8");
101
+ expect(cached).toBe(JSON.stringify(bigObj, null, 2));
102
+ });
103
+
104
+ test("preserves error-text type for large error outputs", async () => {
105
+ const bigError = "Error: " + "x".repeat(200);
106
+ const output: ToolOutput = { type: "error-text", value: bigError };
107
+ const result = await cacheIfLarge(output, "bash", "call_5", config);
108
+
109
+ expect(result.type).toBe("error-text");
110
+ });
111
+
112
+ test("handles content type with text parts", async () => {
113
+ const output: ToolOutput = {
114
+ type: "content",
115
+ value: [{ type: "text", text: "y".repeat(200) }],
116
+ };
117
+ const result = await cacheIfLarge(output, "bash", "call_6", config);
118
+
119
+ expect(result.type).toBe("text");
120
+ const value = (result as { type: "text"; value: string }).value;
121
+ expect(value).toContain("[Tool output cached");
122
+ });
123
+
124
+ test("returns unchanged if content has no text parts", async () => {
125
+ const output: ToolOutput = {
126
+ type: "content",
127
+ value: [
128
+ {
129
+ type: "image",
130
+ image: "base64data",
131
+ mediaType: "image/png",
132
+ },
133
+ ],
134
+ };
135
+ const result = await cacheIfLarge(output, "bash", "call_7", config);
136
+ expect(result).toBe(output);
137
+ });
138
+ });
139
+
140
+ describe("cleanCacheDir", () => {
141
+ let cacheDir: string;
142
+
143
+ beforeEach(async () => {
144
+ cacheDir = await mkdtemp(join(tmpdir(), "dex-cache-clean-test-"));
145
+ });
146
+
147
+ afterEach(async () => {
148
+ await rm(cacheDir, { recursive: true, force: true });
149
+ });
150
+
151
+ test("creates cache directory if it doesn't exist", async () => {
152
+ const newDir = join(cacheDir, "subdir", "cache");
153
+ const config: ResolvedCacheConfig = {
154
+ threshold: 10_000,
155
+ cacheDir: newDir,
156
+ ttlMs: 86_400_000,
157
+ excludedTools: ["read"],
158
+ };
159
+ await cleanCacheDir(config);
160
+
161
+ const entries = await readdir(newDir);
162
+ expect(entries).toEqual([]);
163
+ });
164
+
165
+ test("wipes existing cache files on clean", async () => {
166
+ // Write a file to the cache
167
+ const { writeFile } = await import("node:fs/promises");
168
+ await writeFile(join(cacheDir, "old_call"), "old content");
169
+
170
+ const config: ResolvedCacheConfig = {
171
+ threshold: 10_000,
172
+ cacheDir,
173
+ ttlMs: 86_400_000,
174
+ excludedTools: ["read"],
175
+ };
176
+ await cleanCacheDir(config);
177
+
178
+ // Directory should exist but be empty
179
+ const entries = await readdir(cacheDir);
180
+ expect(entries).toEqual([]);
181
+ });
182
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Large Tool Result Caching
3
+ *
4
+ * When a tool result exceeds a character threshold, the full output is cached
5
+ * to disk and only the tail is sent to the model — with a header noting where
6
+ * the full output can be read.
7
+ *
8
+ * This reduces context consumption while allowing the agent to explore
9
+ * cached results via the existing `read` tool.
10
+ */
11
+
12
+ import { writeFile, mkdir, readdir, stat, unlink, rm } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import type { ToolOutput } from "./message";
16
+
17
+ /* ------------------------------------------------------------------ */
18
+ /* Configuration */
19
+ /* ------------------------------------------------------------------ */
20
+
21
+ export interface ToolResultCacheConfig {
22
+ /** Character threshold above which results are cached. Default: 10_000 */
23
+ readonly threshold?: number;
24
+ /** Cache directory. Default: ~/.dex/cache */
25
+ readonly cacheDir?: string;
26
+ /** TTL in milliseconds for stale file cleanup. Default: 86_400_000 (24h) */
27
+ readonly ttlMs?: number;
28
+ /** Tool names excluded from caching. Default: ['read'] */
29
+ readonly excludedTools?: ReadonlyArray<string>;
30
+ }
31
+
32
+ /** Internal state key used to store the resolved config in AgentContext.state */
33
+ export const TOOL_RESULT_CACHE_KEY = "__toolResultCache";
34
+
35
+ /* ------------------------------------------------------------------ */
36
+ /* Defaults */
37
+ /* ------------------------------------------------------------------ */
38
+
39
+ const DEFAULT_THRESHOLD = 10_000;
40
+ const DEFAULT_CACHE_DIR = join(homedir(), ".dex", "cache");
41
+ const DEFAULT_TTL_MS = 86_400_000; // 24 hours
42
+ const DEFAULT_EXCLUDED_TOOLS: ReadonlyArray<string> = ["read"];
43
+
44
+ export interface ResolvedCacheConfig {
45
+ readonly threshold: number;
46
+ readonly cacheDir: string;
47
+ readonly ttlMs: number;
48
+ readonly excludedTools: ReadonlyArray<string>;
49
+ }
50
+
51
+ export function resolveConfig(
52
+ config?: ToolResultCacheConfig,
53
+ ): ResolvedCacheConfig {
54
+ return {
55
+ threshold: config?.threshold ?? DEFAULT_THRESHOLD,
56
+ cacheDir: config?.cacheDir ?? DEFAULT_CACHE_DIR,
57
+ ttlMs: config?.ttlMs ?? DEFAULT_TTL_MS,
58
+ excludedTools: config?.excludedTools ?? DEFAULT_EXCLUDED_TOOLS,
59
+ };
60
+ }
61
+
62
+ /* ------------------------------------------------------------------ */
63
+ /* Text extraction */
64
+ /* ------------------------------------------------------------------ */
65
+
66
+ /** Extract the text content from a ToolOutput, or null if non-textual. */
67
+ function extractText(output: ToolOutput): string | null {
68
+ switch (output.type) {
69
+ case "text":
70
+ case "error-text":
71
+ return output.value;
72
+ case "json":
73
+ return JSON.stringify(output.value, null, 2);
74
+ case "content": {
75
+ const texts = output.value
76
+ .filter((p) => p.type === "text")
77
+ .map((p) => (p as { type: "text"; text: string }).text);
78
+ return texts.length > 0 ? texts.join("\n") : null;
79
+ }
80
+ default:
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /* ------------------------------------------------------------------ */
86
+ /* Core: cacheIfLarge */
87
+ /* ------------------------------------------------------------------ */
88
+
89
+ /**
90
+ * If the tool output exceeds the threshold, cache the full result to disk
91
+ * and return a truncated version with a header pointing to the cache file.
92
+ *
93
+ * Returns the original output unchanged if below threshold or excluded.
94
+ */
95
+ export async function cacheIfLarge(
96
+ output: ToolOutput,
97
+ toolName: string,
98
+ toolCallId: string,
99
+ config: ResolvedCacheConfig,
100
+ ): Promise<ToolOutput> {
101
+ // Skip excluded tools
102
+ if (config.excludedTools.includes(toolName)) return output;
103
+
104
+ // Extract text content
105
+ const text = extractText(output);
106
+ if (!text || text.length <= config.threshold) return output;
107
+
108
+ // Write full output to cache
109
+ const cachePath = join(config.cacheDir, toolCallId);
110
+ try {
111
+ await mkdir(config.cacheDir, { recursive: true });
112
+ await writeFile(cachePath, text, "utf-8");
113
+ } catch {
114
+ // If caching fails (disk full, permissions), fall through to existing truncation
115
+ return output;
116
+ }
117
+
118
+ // Return tail with header
119
+ const tail = text.slice(-config.threshold);
120
+ const header =
121
+ `[Tool output cached — showing last ${config.threshold.toLocaleString()} of ${text.length.toLocaleString()} chars. ` +
122
+ `Full output: ${cachePath}]\n`;
123
+
124
+ // Preserve the output type for error results
125
+ if (output.type === "error-text") {
126
+ return { type: "error-text", value: header + tail };
127
+ }
128
+ return { type: "text", value: header + tail };
129
+ }
130
+
131
+ /* ------------------------------------------------------------------ */
132
+ /* Cleanup */
133
+ /* ------------------------------------------------------------------ */
134
+
135
+ /**
136
+ * Remove stale cache files older than TTL, then wipe the directory.
137
+ * Called on session start to ensure a clean slate.
138
+ */
139
+ export async function cleanCacheDir(
140
+ config: ResolvedCacheConfig,
141
+ ): Promise<void> {
142
+ const { cacheDir, ttlMs } = config;
143
+
144
+ try {
145
+ // First pass: remove stale files (safety net for orphaned files from crashed sessions)
146
+ const entries = await readdir(cacheDir).catch(() => [] as string[]);
147
+ const now = Date.now();
148
+ for (const entry of entries) {
149
+ const filePath = join(cacheDir, entry);
150
+ const fileStat = await stat(filePath).catch(() => null);
151
+ if (fileStat && now - fileStat.mtimeMs > ttlMs) {
152
+ await unlink(filePath).catch(() => {});
153
+ }
154
+ }
155
+
156
+ // Wipe the directory for a clean session start
157
+ await rm(cacheDir, { recursive: true, force: true });
158
+ } catch {
159
+ // Non-fatal — directory may not exist yet
160
+ }
161
+
162
+ // Ensure directory exists for this session
163
+ await mkdir(cacheDir, { recursive: true });
164
+ }
package/src/tool.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Tool — an action a Provider may invoke.
3
+ *
4
+ * ai-sdk aligned:
5
+ * - parameters: Standard Schema for the input
6
+ * - execute: ({input}, gctx) => ToolOutput | value
7
+ * - outputSchema: optional Standard Schema for the output value
8
+ *
9
+ * Approval is NOT a first-class tool field. Use the onToolCall hook
10
+ * (see extension.ts) — a short-circuit-capable reducer. An approver
11
+ * extension returns a ToolResult to reject, a rewritten ToolCall to
12
+ * modify, or void to pass through. A rejection is data for the LLM:
13
+ * the tool-result message goes into history and the model decides to
14
+ * retry, call a different tool, or stop.
15
+ */
16
+
17
+ import type { StandardSchemaV1, InferInput } from "./schema";
18
+ import type { ToolOutput } from "./message";
19
+ import type { GenerateContext } from "./context";
20
+
21
+ /** A pending tool invocation parsed from the model. */
22
+ export interface ToolCall<Input = unknown> {
23
+ readonly toolCallId: string;
24
+ readonly toolName: string;
25
+ readonly input: Input;
26
+ }
27
+
28
+ /**
29
+ * The outcome of a tool invocation. Wraps a tagged ToolOutput so tools
30
+ * can return rich content, structured JSON, plain text, or errors
31
+ * uniformly.
32
+ */
33
+ export interface ToolResult {
34
+ readonly toolCallId: string;
35
+ readonly toolName: string;
36
+ readonly output: ToolOutput;
37
+ }
38
+
39
+ /**
40
+ * An execute function may return either:
41
+ * - a raw Output value (wrapped by the loop into a ToolOutput.json), or
42
+ * - a ToolOutput directly (for rich content / explicit error shapes).
43
+ */
44
+ export type ExecuteResult<Output> = Output | ToolOutput;
45
+
46
+ export interface Tool<Input = unknown, Output = unknown> {
47
+ readonly name: string;
48
+ readonly description?: string;
49
+ readonly parameters: StandardSchemaV1<Input>;
50
+ readonly outputSchema?: StandardSchemaV1<Output>;
51
+ /**
52
+ * Access level hint for permission systems.
53
+ * - 'read': tool only reads/queries data (no side effects)
54
+ * - 'write': tool modifies state (files, memory, shell, etc.)
55
+ *
56
+ * If omitted, defaults to 'write' (conservative — requires approval).
57
+ */
58
+ readonly access?: "read" | "write";
59
+ /**
60
+ * Human-friendly name for UI display.
61
+ * Falls back to `name` when omitted.
62
+ * Examples: "Read", "Update", "Search", "Remember"
63
+ */
64
+ readonly displayName?: string;
65
+ /**
66
+ * Whether the tool invocation should be rendered in the UI message stream.
67
+ * Defaults to `true`. Set to `false` for tools that have dedicated UI
68
+ * (e.g. task panel) or whose invocations aren't useful to show inline.
69
+ */
70
+ readonly visible?: boolean;
71
+ execute(
72
+ input: Input,
73
+ gctx: GenerateContext,
74
+ ): Promise<ExecuteResult<Output>> | ExecuteResult<Output>;
75
+ /**
76
+ * Streaming alternative to execute. Yields Parts (tool-progress + tool-result).
77
+ * If present, the loop prefers stream() over execute().
78
+ * The loop stamps toolCallId on emitted parts.
79
+ */
80
+ stream?(input: Input, gctx: GenerateContext): AsyncIterable<ToolStreamPart>;
81
+ }
82
+
83
+ /** Parts a streaming tool can yield. */
84
+ export type ToolStreamPart =
85
+ | { readonly type: "tool-progress"; readonly text: string }
86
+ | { readonly type: "tool-result"; readonly output: ToolOutput };
87
+
88
+ export type AnyTool = Tool<any, any>;
89
+
90
+ /**
91
+ * Tool namespace — identity-typed declaration helper.
92
+ *
93
+ * Use `Tool.define({...})` at author-time to get full contextual typing
94
+ * (tool input/output inference from the Standard Schema `parameters`).
95
+ * Runtime behavior: returns the argument unchanged.
96
+ */
97
+ // eslint-disable-next-line @typescript-eslint/no-redeclare
98
+ export const Tool = {
99
+ define<ParamsSchema extends StandardSchemaV1<any, any>, Output>(
100
+ tool: Omit<
101
+ Tool<InferInput<ParamsSchema>, Output>,
102
+ "parameters" | "outputSchema"
103
+ > & {
104
+ readonly parameters: ParamsSchema;
105
+ readonly outputSchema?: StandardSchemaV1<Output>;
106
+ },
107
+ ): Tool<InferInput<ParamsSchema>, Output> {
108
+ return tool as Tool<InferInput<ParamsSchema>, Output>;
109
+ },
110
+ };