tinker-agent 1.3.0 → 1.5.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 (49) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +271 -72
  3. package/bin/tinker.js +75 -25
  4. package/package.json +12 -3
  5. package/src/agent/runtime-session.ts +113 -15
  6. package/src/cli/command-line.ts +291 -0
  7. package/src/cli/config.ts +158 -262
  8. package/src/cli/index.ts +33 -21
  9. package/src/cli/main.ts +213 -0
  10. package/src/cli/model-profiles.ts +226 -72
  11. package/src/cli/output.ts +113 -0
  12. package/src/cli/package-metadata.ts +36 -0
  13. package/src/cli/prompt-source.ts +229 -0
  14. package/src/cli/public-cli-contract.ts +69 -0
  15. package/src/cli/public-config-contract.ts +732 -0
  16. package/src/cli/run-runner.ts +17 -12
  17. package/src/cli/runner-dependencies.ts +108 -0
  18. package/src/cli/tui-memory.ts +67 -0
  19. package/src/cli/tui-runner.tsx +79 -49
  20. package/src/context/context-policy.ts +2 -2
  21. package/src/events/stdout-event-printer.ts +1 -0
  22. package/src/mcp/mcp-manager.ts +2 -19
  23. package/src/mcp/mcp-tool-executor.ts +3 -4
  24. package/src/memory/contracts.ts +148 -0
  25. package/src/memory/embedding-client.ts +105 -0
  26. package/src/memory/memory-coordinator.ts +556 -0
  27. package/src/memory/memory-extractor.ts +231 -0
  28. package/src/memory/memory-log.ts +88 -0
  29. package/src/memory/memory-search-tool.ts +100 -0
  30. package/src/memory/memory-store.ts +687 -0
  31. package/src/memory/vector.ts +153 -0
  32. package/src/model/fake-model-client.ts +971 -3
  33. package/src/model/model-context-profile.ts +0 -30
  34. package/src/observation/observation-builder.ts +20 -0
  35. package/src/session/session-store.ts +123 -0
  36. package/src/tools/bash.ts +8 -25
  37. package/src/tools/grep.ts +9 -1
  38. package/src/tools/registry.ts +19 -1
  39. package/src/tools/ripgrep.ts +24 -27
  40. package/src/tools/types.ts +16 -0
  41. package/src/tools/web-fetch/index.ts +2 -15
  42. package/src/tui/app.tsx +72 -2
  43. package/src/tui/clipboard.ts +22 -0
  44. package/src/tui/components/footer.tsx +9 -4
  45. package/src/tui/components/memory-browser.tsx +151 -0
  46. package/src/tui/components/prompt-input.tsx +6 -3
  47. package/src/tui/event-store.ts +9 -2
  48. package/src/tui/slash-commands.ts +88 -24
  49. package/src/tui/workspace-file-search.ts +78 -71
@@ -14,21 +14,6 @@ export type ModelContextBudget = ModelContextProfile & {
14
14
  triggerTokens: number;
15
15
  };
16
16
 
17
- export function readModelContextProfileFromEnv(
18
- env: NodeJS.ProcessEnv = process.env,
19
- ): ModelContextProfile {
20
- return createModelContextProfile({
21
- contextWindowTokens: parseRequiredTokenCount(
22
- env.TINKER_CONTEXT_WINDOW_TOKENS,
23
- "TINKER_CONTEXT_WINDOW_TOKENS",
24
- ),
25
- maxSupportedOutputTokens: parseRequiredTokenCount(
26
- env.TINKER_MAX_SUPPORTED_OUTPUT_TOKENS,
27
- "TINKER_MAX_SUPPORTED_OUTPUT_TOKENS",
28
- ),
29
- });
30
- }
31
-
32
17
  export function createModelContextProfile(
33
18
  input: ModelContextProfile,
34
19
  ): ModelContextProfile {
@@ -102,21 +87,6 @@ export function assertMatchingContextBudget(
102
87
  }
103
88
  }
104
89
 
105
- function parseRequiredTokenCount(value: string | undefined, name: string): number {
106
- if (value === undefined || value.trim() === "") {
107
- throw new Error(`${name} is required; received ${displayValue(value)}.`);
108
- }
109
- if (!/^\d+$/.test(value)) {
110
- throw new Error(
111
- `${name} must be a positive safe integer token count; received ${displayValue(value)}.`,
112
- );
113
- }
114
-
115
- const parsed = Number(value);
116
- requirePositiveSafeInteger(parsed, name, value);
117
- return parsed;
118
- }
119
-
120
90
  function requirePositiveSafeInteger(
121
91
  value: number,
122
92
  name: string,
@@ -5,6 +5,7 @@ import type {
5
5
  GenericToolRawResult,
6
6
  GlobRawResult,
7
7
  GrepRawResult,
8
+ MemorySearchRawResult,
8
9
  McpToolRawResult,
9
10
  ReadFileRawResult,
10
11
  RecallRawResult,
@@ -33,6 +34,8 @@ export class ObservationBuilder {
33
34
  return { content: renderReadObservation(input.raw) };
34
35
  case "recall":
35
36
  return { content: renderRecallObservation(input.raw) };
37
+ case "memory_search":
38
+ return { content: renderMemorySearchObservation(input.raw) };
36
39
  case "skill":
37
40
  return { content: renderSkillObservation(input.raw) };
38
41
  case "write":
@@ -227,6 +230,23 @@ function renderRecallObservation(raw: RecallRawResult): string {
227
230
  return [header, ...hits].join("\n\n");
228
231
  }
229
232
 
233
+ function renderMemorySearchObservation(raw: MemorySearchRawResult): string {
234
+ if (!raw.ok) {
235
+ return `MemorySearch unavailable: ${raw.error}`;
236
+ }
237
+ if (raw.matches.length === 0) {
238
+ return "MemorySearch found no stored memories.";
239
+ }
240
+ const header = `MemorySearch returned ${raw.matches.length} derived memories. They may be stale or wrong; verify current workspace facts.`;
241
+ return [
242
+ header,
243
+ ...raw.matches.map(
244
+ (match, index) =>
245
+ `${index + 1}. score=${match.score.toFixed(3)} created_at=${match.createdAt} workspace=${match.sourceWorkspace}\n ${match.text}`,
246
+ ),
247
+ ].join("\n\n");
248
+ }
249
+
230
250
  export function renderSkillObservation(raw: SkillRawResult): string {
231
251
  if (!raw.ok) {
232
252
  return `Skill failed for ${raw.name || "(unknown skill)"} (${raw.errorCode}): ${raw.error}`;
@@ -144,6 +144,29 @@ export type SessionImageInputCompatibility = {
144
144
  readonly tokenEstimator?: InputTokenEstimatorCompatibility;
145
145
  };
146
146
 
147
+ export type CompletedTurnMessageSnapshot =
148
+ | {
149
+ readonly ordinal: number;
150
+ readonly role: "user";
151
+ readonly content: string;
152
+ }
153
+ | {
154
+ readonly ordinal: number;
155
+ readonly role: "assistant";
156
+ readonly content: string | null;
157
+ readonly reasoningContent?: string | null;
158
+ }
159
+ | {
160
+ readonly ordinal: number;
161
+ readonly role: "tool";
162
+ readonly name: string;
163
+ readonly content: string;
164
+ };
165
+
166
+ export type CompletedTurnSnapshot = {
167
+ readonly messages: readonly CompletedTurnMessageSnapshot[];
168
+ };
169
+
147
170
  export type SessionCompatibilityContract = {
148
171
  modelName: string;
149
172
  profileName?: string;
@@ -2082,6 +2105,105 @@ export class SessionStore implements SessionLedgerCommitter {
2082
2105
  });
2083
2106
  }
2084
2107
 
2108
+ readCompletedTurnSnapshot(turnId: TurnId): CompletedTurnSnapshot {
2109
+ this.requireOpen();
2110
+ const turnRow = this.database
2111
+ .query("SELECT status FROM turns WHERE turn_id = ?")
2112
+ .get(turnId);
2113
+ const status = enumFromSql(
2114
+ recordFromSql(turnRow, "completed turn").status,
2115
+ ["open", "completed", "failed", "cancelled", "interrupted"] as const,
2116
+ "turn status",
2117
+ );
2118
+ if (status !== "completed") {
2119
+ throw new Error(`Turn ${turnId} is not completed.`);
2120
+ }
2121
+
2122
+ const rows = this.database
2123
+ .query(
2124
+ `SELECT ordinal, role, content, reasoning_content,
2125
+ reasoning_content_present, name
2126
+ FROM messages
2127
+ WHERE turn_id = ?
2128
+ ORDER BY ordinal`,
2129
+ )
2130
+ .all(turnId);
2131
+ if (rows.length === 0) {
2132
+ throw new Error(`Completed turn ${turnId} has no messages.`);
2133
+ }
2134
+
2135
+ let previousOrdinal = 0;
2136
+ const messages = rows.map((value): CompletedTurnMessageSnapshot => {
2137
+ const row = recordFromSql(value, "completed turn message");
2138
+ const ordinal = numberFromSql(row.ordinal, "completed turn ordinal");
2139
+ if (ordinal < 1 || ordinal <= previousOrdinal) {
2140
+ throw new Error("Completed turn message ordinals are invalid.");
2141
+ }
2142
+ previousOrdinal = ordinal;
2143
+ const role = enumFromSql(
2144
+ row.role,
2145
+ ["user", "assistant", "tool"] as const,
2146
+ "completed turn message role",
2147
+ );
2148
+ if (role === "user") {
2149
+ if (
2150
+ row.reasoning_content !== null ||
2151
+ numberFromSql(row.reasoning_content_present, "reasoning_content_present") !==
2152
+ 0 ||
2153
+ row.name !== null
2154
+ ) {
2155
+ throw new Error("Completed user message fields are invalid.");
2156
+ }
2157
+ return Object.freeze({
2158
+ ordinal,
2159
+ role,
2160
+ content: stringFromSql(row.content, "completed user content"),
2161
+ });
2162
+ }
2163
+ if (role === "assistant") {
2164
+ if (row.name !== null) {
2165
+ throw new Error("Completed assistant message name must be null.");
2166
+ }
2167
+ const reasoningPresent = numberFromSql(
2168
+ row.reasoning_content_present,
2169
+ "reasoning_content_present",
2170
+ );
2171
+ if (reasoningPresent !== 0 && reasoningPresent !== 1) {
2172
+ throw new Error("reasoning_content_present must be 0 or 1.");
2173
+ }
2174
+ if (reasoningPresent === 0 && row.reasoning_content !== null) {
2175
+ throw new Error("Absent assistant reasoning content must be null.");
2176
+ }
2177
+ return Object.freeze({
2178
+ ordinal,
2179
+ role,
2180
+ content: nullableTextFromSql(row.content, "completed assistant content"),
2181
+ ...(reasoningPresent === 0
2182
+ ? {}
2183
+ : {
2184
+ reasoningContent: nullableTextFromSql(
2185
+ row.reasoning_content,
2186
+ "completed assistant reasoning content",
2187
+ ),
2188
+ }),
2189
+ });
2190
+ }
2191
+ if (
2192
+ row.reasoning_content !== null ||
2193
+ numberFromSql(row.reasoning_content_present, "reasoning_content_present") !== 0
2194
+ ) {
2195
+ throw new Error("Completed tool message reasoning fields are invalid.");
2196
+ }
2197
+ return Object.freeze({
2198
+ ordinal,
2199
+ role,
2200
+ name: stringFromSql(row.name, "completed tool name"),
2201
+ content: stringFromSql(row.content, "completed tool content"),
2202
+ });
2203
+ });
2204
+ return Object.freeze({ messages: Object.freeze(messages) });
2205
+ }
2206
+
2085
2207
  loadProtocolView(): ProtocolContextView {
2086
2208
  this.requireOpen();
2087
2209
  const imageAttachments = loadMessageImageAttachments(this.database);
@@ -4725,6 +4847,7 @@ export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
4725
4847
  "web_search",
4726
4848
  "web_fetch",
4727
4849
  "recall",
4850
+ "memory_search",
4728
4851
  "skill",
4729
4852
  "mcp",
4730
4853
  "generic",
package/src/tools/bash.ts CHANGED
@@ -11,6 +11,7 @@ import { buildOutputSnapshotFromText } from "./task-output-snapshot";
11
11
  import { defineToolExecutor } from "./types";
12
12
  import type { TaskOutputSnapshot } from "./task-output";
13
13
  import type { BashRawResult, ToolExecutionContext, ToolExecutor } from "./types";
14
+ import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
14
15
 
15
16
  type BashArgs = {
16
17
  command: string;
@@ -27,25 +28,16 @@ export type BashToolOptions = {
27
28
  maxTimeoutMs?: number;
28
29
  };
29
30
 
30
- const defaultForegroundTimeoutMs = 5_000;
31
- const defaultMaxForegroundTimeoutMs = 600_000;
32
-
33
31
  export function createBashToolExecutor(options: BashToolOptions): ToolExecutor {
34
32
  const maxTimeoutMs =
35
- options.maxTimeoutMs ??
36
- parsePositiveInteger(
37
- process.env.TINKER_BASH_MAX_TIMEOUT_MS,
38
- defaultMaxForegroundTimeoutMs,
39
- );
33
+ options.maxTimeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.bashMaxTimeoutMs;
40
34
  const defaultTimeoutMs =
41
- options.defaultTimeoutMs ??
42
- Math.min(
43
- parsePositiveInteger(
44
- process.env.TINKER_BASH_DEFAULT_TIMEOUT_MS,
45
- defaultForegroundTimeoutMs,
46
- ),
47
- maxTimeoutMs,
35
+ options.defaultTimeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.bashDefaultTimeoutMs;
36
+ if (defaultTimeoutMs > maxTimeoutMs) {
37
+ throw new Error(
38
+ `Bash default timeout must not exceed max timeout; received ${defaultTimeoutMs} > ${maxTimeoutMs}.`,
48
39
  );
40
+ }
49
41
 
50
42
  return defineToolExecutor("bash", {
51
43
  definition: {
@@ -161,7 +153,7 @@ export function createBashToolExecutor(options: BashToolOptions): ToolExecutor {
161
153
 
162
154
  export function parseBashArgs(
163
155
  args: unknown,
164
- maxTimeoutMs = defaultMaxForegroundTimeoutMs,
156
+ maxTimeoutMs = DEFAULT_PUBLIC_TOOLING_CONFIG.bashMaxTimeoutMs,
165
157
  ): { ok: true; value: BashArgs } | { ok: false; error: string } {
166
158
  if (!isRecord(args)) {
167
159
  return { ok: false, error: "Bash arguments must be an object." };
@@ -401,15 +393,6 @@ function parseOptionalTimeout(
401
393
  return { ok: true, value };
402
394
  }
403
395
 
404
- function parsePositiveInteger(value: string | undefined, fallback: number): number {
405
- if (value === undefined) {
406
- return fallback;
407
- }
408
-
409
- const parsed = Number(value);
410
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
411
- }
412
-
413
396
  function lastPipelineCommandName(command: string): string | undefined {
414
397
  const segments = command.split("|");
415
398
  const lastSegment = segments.at(-1)?.trim();
package/src/tools/grep.ts CHANGED
@@ -32,6 +32,11 @@ type GrepArgs = {
32
32
  export type GrepToolOptions = {
33
33
  workspaceRoot: string;
34
34
  cwdState: CwdState;
35
+ ripgrep?: {
36
+ command?: string;
37
+ timeoutMs?: number;
38
+ maxBufferBytes?: number;
39
+ };
35
40
  };
36
41
 
37
42
  const ignoredDirectories = [
@@ -174,7 +179,10 @@ export function createGrepToolExecutor(options: GrepToolOptions): ToolExecutor {
174
179
  }
175
180
 
176
181
  const rgArgs = buildRipgrepArgs(input, mode, absoluteSearchPath);
177
- const rg = await ripGrep(rgArgs, { signal: context.signal });
182
+ const rg = await ripGrep(rgArgs, {
183
+ signal: context.signal,
184
+ ...options.ripgrep,
185
+ });
178
186
 
179
187
  if (!rg.ok) {
180
188
  return grepFailure({
@@ -31,6 +31,10 @@ import { ToolExecutionFatalError } from "./types";
31
31
  import type { SkillCatalogSnapshot } from "../skills/skill-loader";
32
32
  import type { SkillActivationCoordinator } from "../skills/skill-context";
33
33
  import { createSkillToolExecutor } from "../skills/skill-tool";
34
+ import {
35
+ DEFAULT_PUBLIC_TOOLING_CONFIG,
36
+ type PublicToolingConfig,
37
+ } from "../cli/public-config-contract";
34
38
 
35
39
  export class ToolRegistry {
36
40
  private readonly tools = new Map<string, ToolExecutor>();
@@ -128,10 +132,13 @@ export function createDefaultTooling(options: {
128
132
  taskStopGraceMs?: number;
129
133
  skillCatalog?: SkillCatalogSnapshot;
130
134
  skillCoordinator?: SkillActivationCoordinator;
135
+ toolingConfig?: PublicToolingConfig;
136
+ memorySearch?: ToolExecutor;
131
137
  }): DefaultTooling {
132
138
  const snapshots: FileSnapshotStore = new Map();
133
139
  const registry = new ToolRegistry();
134
140
  const runtimeSession = options.runtimeSession;
141
+ const toolingConfig = options.toolingConfig ?? DEFAULT_PUBLIC_TOOLING_CONFIG;
135
142
  const cwdState = createCwdState(options.workspaceRoot);
136
143
  const taskManager = new ShellTaskManager({
137
144
  workspaceRoot: options.workspaceRoot,
@@ -149,6 +156,11 @@ export function createDefaultTooling(options: {
149
156
  createGrepToolExecutor({
150
157
  workspaceRoot: options.workspaceRoot,
151
158
  cwdState,
159
+ ripgrep: {
160
+ command: toolingConfig.ripgrepPath,
161
+ timeoutMs: toolingConfig.grepTimeoutMs,
162
+ maxBufferBytes: toolingConfig.grepMaxBufferBytes,
163
+ },
152
164
  }),
153
165
  );
154
166
  registry.register(
@@ -159,6 +171,9 @@ export function createDefaultTooling(options: {
159
171
  }),
160
172
  );
161
173
  registry.register(createRecallToolExecutor({ historyReader: options.historyReader }));
174
+ if (options.memorySearch !== undefined) {
175
+ registry.register(options.memorySearch);
176
+ }
162
177
  if (options.skillCatalog !== undefined) {
163
178
  if (options.skillCatalog.skills.size === 0) {
164
179
  throw new Error("An empty Agent Skill catalog must not register tooling.");
@@ -192,13 +207,15 @@ export function createDefaultTooling(options: {
192
207
  workspaceRoot: options.workspaceRoot,
193
208
  cwdState,
194
209
  taskManager,
210
+ defaultTimeoutMs: toolingConfig.bashDefaultTimeoutMs,
211
+ maxTimeoutMs: toolingConfig.bashMaxTimeoutMs,
195
212
  }),
196
213
  );
197
214
  registry.register(createTaskListToolExecutor({ taskManager }));
198
215
  registry.register(createTaskOutputToolExecutor({ taskManager }));
199
216
  registry.register(createTaskStopToolExecutor({ taskManager }));
200
217
 
201
- const exaApiKey = options.exaApiKey ?? process.env.EXA_API_KEY;
218
+ const exaApiKey = options.exaApiKey ?? toolingConfig.exaApiKey;
202
219
  const hasExaKey = exaApiKey !== undefined && exaApiKey.trim() !== "";
203
220
 
204
221
  if (hasExaKey) {
@@ -209,6 +226,7 @@ export function createDefaultTooling(options: {
209
226
  createWebFetchToolExecutor({
210
227
  exaApiKey: hasExaKey ? exaApiKey : undefined,
211
228
  refiner: options.webFetchRefiner,
229
+ refineThreshold: toolingConfig.webFetchRefineThreshold,
212
230
  }),
213
231
  );
214
232
 
@@ -1,13 +1,10 @@
1
1
  import { execFile } from "node:child_process";
2
- import { rgPath } from "@vscode/ripgrep";
3
2
  import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
3
+ import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
4
4
 
5
5
  export const RIPGREP_MISSING_ERROR =
6
6
  "Tinker's bundled ripgrep executable is unavailable. Reinstall tinker-agent.";
7
7
 
8
- const defaultTimeoutMs = 20_000;
9
- const defaultMaxBufferBytes = 20_000_000;
10
-
11
8
  export type RipgrepResult = {
12
9
  ok: boolean;
13
10
  lines: string[];
@@ -18,12 +15,13 @@ export type RipgrepResult = {
18
15
 
19
16
  export type RipgrepOptions = {
20
17
  signal: AbortSignal;
18
+ command?: string;
21
19
  timeoutMs?: number;
22
20
  maxBufferBytes?: number;
23
21
  };
24
22
 
25
- export function findRipgrepCommand(): string {
26
- return process.env.TINKER_RIPGREP_PATH ?? rgPath;
23
+ export function findRipgrepCommand(command?: string): string {
24
+ return command ?? DEFAULT_PUBLIC_TOOLING_CONFIG.ripgrepPath;
27
25
  }
28
26
 
29
27
  export async function ripGrep(
@@ -31,21 +29,28 @@ export async function ripGrep(
31
29
  options: RipgrepOptions,
32
30
  ): Promise<RipgrepResult> {
33
31
  throwIfTurnCancelled(options.signal);
34
- const timeoutMs =
35
- options.timeoutMs ??
36
- parsePositiveInteger(process.env.TINKER_GREP_TIMEOUT_MS, defaultTimeoutMs);
32
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepTimeoutMs;
37
33
  const maxBufferBytes =
38
- options.maxBufferBytes ??
39
- parsePositiveInteger(
40
- process.env.TINKER_GREP_MAX_BUFFER_BYTES,
41
- defaultMaxBufferBytes,
42
- );
43
-
44
- const first = await runRipgrep(args, timeoutMs, maxBufferBytes, options.signal);
34
+ options.maxBufferBytes ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepMaxBufferBytes;
35
+ const command = findRipgrepCommand(options.command);
36
+
37
+ const first = await runRipgrep(
38
+ command,
39
+ args,
40
+ timeoutMs,
41
+ maxBufferBytes,
42
+ options.signal,
43
+ );
45
44
  if (first.retryWithSingleThread) {
46
45
  throwIfTurnCancelled(options.signal);
47
46
  return finalizeResult(
48
- await runRipgrep(["-j", "1", ...args], timeoutMs, maxBufferBytes, options.signal),
47
+ await runRipgrep(
48
+ command,
49
+ ["-j", "1", ...args],
50
+ timeoutMs,
51
+ maxBufferBytes,
52
+ options.signal,
53
+ ),
49
54
  );
50
55
  }
51
56
 
@@ -62,6 +67,7 @@ type RipgrepAttempt = {
62
67
  };
63
68
 
64
69
  function runRipgrep(
70
+ command: string,
65
71
  args: string[],
66
72
  timeoutMs: number,
67
73
  maxBufferBytes: number,
@@ -74,7 +80,7 @@ function runRipgrep(
74
80
  }
75
81
 
76
82
  execFile(
77
- findRipgrepCommand(),
83
+ command,
78
84
  args,
79
85
  { timeout: timeoutMs, maxBuffer: maxBufferBytes, signal },
80
86
  (error, stdout, stderr) => {
@@ -210,12 +216,3 @@ function isEagainError(
210
216
  stderr.includes("Resource temporarily unavailable")
211
217
  );
212
218
  }
213
-
214
- function parsePositiveInteger(value: string | undefined, fallback: number): number {
215
- if (value === undefined || value.trim() === "") {
216
- return fallback;
217
- }
218
-
219
- const parsed = Number(value);
220
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
221
- }
@@ -245,6 +245,21 @@ export type RecallGetRawResult =
245
245
 
246
246
  export type RecallRawResult = RecallSearchRawResult | RecallGetRawResult;
247
247
 
248
+ export type MemorySearchRawResult =
249
+ | {
250
+ ok: true;
251
+ matches: readonly {
252
+ text: string;
253
+ score: number;
254
+ sourceWorkspace: string;
255
+ createdAt: string;
256
+ }[];
257
+ }
258
+ | {
259
+ ok: false;
260
+ error: string;
261
+ };
262
+
248
263
  export type SkillRawResult =
249
264
  | {
250
265
  ok: true;
@@ -307,6 +322,7 @@ export type ToolRawResultByKind = {
307
322
  web_search: WebSearchRawResult;
308
323
  web_fetch: WebFetchRawResult;
309
324
  recall: RecallRawResult;
325
+ memory_search: MemorySearchRawResult;
310
326
  skill: SkillRawResult;
311
327
  mcp: McpToolRawResult;
312
328
  generic: GenericToolRawResult;
@@ -1,4 +1,5 @@
1
1
  import { cancellationError, throwIfTurnCancelled } from "../../agent/turn-cancellation";
2
+ import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../../cli/public-config-contract";
2
3
  import { defineToolExecutor } from "../types";
3
4
  import type { ToolExecutionContext, ToolExecutor, WebFetchRawResult } from "../types";
4
5
  import type { WebFetchBackend, WebFetchBackendResult, WebFetchRoute } from "./backend";
@@ -25,7 +26,6 @@ export type WebFetchToolOptions = {
25
26
  browserBackend?: WebFetchBackend | false;
26
27
  };
27
28
 
28
- export const WEB_FETCH_DEFAULT_REFINE_THRESHOLD = 2000;
29
29
  export const WEB_FETCH_DEFAULT_CACHE_TTL_MS = 15 * 60 * 1000;
30
30
 
31
31
  type CacheEntry = {
@@ -50,11 +50,7 @@ export function createWebFetchToolExecutor(
50
50
  : (options.browserBackend ??
51
51
  (isBrowserBackendAvailable() ? createBrowserWebFetchBackend() : undefined));
52
52
  const refineThreshold =
53
- options.refineThreshold ??
54
- parsePositiveInteger(
55
- process.env.TINKER_WEBFETCH_REFINE_THRESHOLD,
56
- WEB_FETCH_DEFAULT_REFINE_THRESHOLD,
57
- );
53
+ options.refineThreshold ?? DEFAULT_PUBLIC_TOOLING_CONFIG.webFetchRefineThreshold;
58
54
  const cacheTtlMs = options.cacheTtlMs ?? WEB_FETCH_DEFAULT_CACHE_TTL_MS;
59
55
  const cache = new Map<string, CacheEntry>();
60
56
 
@@ -284,15 +280,6 @@ function pruneCache(cache: Map<string, CacheEntry>): void {
284
280
  }
285
281
  }
286
282
 
287
- function parsePositiveInteger(value: string | undefined, fallback: number): number {
288
- if (value === undefined || value.trim() === "") {
289
- return fallback;
290
- }
291
-
292
- const parsed = Number(value);
293
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
294
- }
295
-
296
283
  function isRecord(value: unknown): value is Record<string, unknown> {
297
284
  return typeof value === "object" && value !== null && !Array.isArray(value);
298
285
  }