nolo-cli 0.1.39 → 0.1.40

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 (35) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/dialogWritePlan.ts +39 -0
  3. package/agent-runtime/hostAdapter.ts +4 -0
  4. package/agent-runtime/hybridRecordStore.ts +1 -1
  5. package/agent-runtime/index.ts +15 -0
  6. package/agent-runtime/localLoop.ts +87 -7
  7. package/agent-runtime/localToolPolicy.ts +2 -13
  8. package/agent-runtime/localWorkspaceTools.ts +42 -110
  9. package/agent-runtime/runtimeToolPolicy.ts +2 -11
  10. package/agent-runtime/runtimeToolSurface.ts +198 -0
  11. package/agent-runtime/types.ts +1 -0
  12. package/agentAliases.ts +2 -1
  13. package/agentRunCommand.ts +10 -32
  14. package/agentRuntimeLocal.ts +1 -0
  15. package/ai/agent/cliExecutor.ts +249 -34
  16. package/cli/agentAliases.ts +2 -1
  17. package/cli/agentRunCommand.ts +10 -32
  18. package/cli/agentRuntimeLocal.ts +1 -0
  19. package/cli/client/agentRun.ts +40 -87
  20. package/cli/client/localRuntimeAdapter.ts +179 -26
  21. package/cli/commandRegistry.ts +2 -2
  22. package/cli/machineWsRunDispatch.ts +51 -6
  23. package/cli/offlineMarxistsAgentCommand.ts +6 -4
  24. package/client/agentConfigResolver.test.ts +2 -2
  25. package/client/agentRun.test.ts +123 -57
  26. package/client/agentRun.ts +40 -87
  27. package/client/localRuntimeAdapter.test.ts +245 -31
  28. package/client/localRuntimeAdapter.ts +179 -26
  29. package/client/localRuntimeDryRun.test.ts +23 -9
  30. package/client/localToolPolicy.test.ts +4 -4
  31. package/commandRegistry.ts +2 -2
  32. package/database/server/levelAuthorityStore.ts +20 -1
  33. package/machineWsRunDispatch.ts +51 -6
  34. package/offlineMarxistsAgentCommand.ts +6 -4
  35. package/package.json +2 -1
package/README.md CHANGED
@@ -178,10 +178,10 @@ nolo dialog list --space <space>
178
178
  nolo dialog read <dialog>
179
179
  nolo dialog delete <dialog> --yes
180
180
  nolo doc list --agent <agent>
181
- nolo table query --table meta-b2e06f801f-NOLOTASKBOARD --limit 20
182
- nolo table query --table meta-b2e06f801f-NOLOTASKBOARD --columns '["title","status","owner","priority","codeStatus"]' --no-base-fields --output items
183
- nolo table update-row --table meta-b2e06f801f-NOLOTASKBOARD --row 01ROWID --changes '{"status":"已完成"}'
184
- nolo table add-column --table meta-b2e06f801f-NOLOTASKBOARD --schema-write-ok --name "blockedBy" --label "Blocked By"
181
+ nolo table query --table meta-0e95801d90-NOLOTASKBOARD --limit 20
182
+ nolo table query --table meta-0e95801d90-NOLOTASKBOARD --columns '["title","status","owner","priority","codeStatus"]' --no-base-fields --output items
183
+ nolo table update-row --table meta-0e95801d90-NOLOTASKBOARD --row 01ROWID --changes '{"status":"已完成"}'
184
+ nolo table add-column --table meta-0e95801d90-NOLOTASKBOARD --schema-write-ok --name "blockedBy" --label "Blocked By"
185
185
  ```
186
186
 
187
187
  `agent list`, `dialog list`, and record delete commands use global server
@@ -34,6 +34,28 @@ function resolveDialogTitle(args: {
34
34
  return lastUserText ? lastUserText.slice(0, 80) : "Local agent run";
35
35
  }
36
36
 
37
+ function normalizeNonEmptyString(value: unknown) {
38
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
39
+ }
40
+
41
+ function buildDialogLineageFields(args: {
42
+ input: AgentRuntimeSaveTurnInput;
43
+ existingDialog?: DialogRecord | null;
44
+ }) {
45
+ const inheritedFromDialogKey = normalizeNonEmptyString(args.input.inheritedFromDialogKey);
46
+ const parentDialogId = normalizeNonEmptyString(args.input.parentDialogId);
47
+ if (!inheritedFromDialogKey && !parentDialogId) return {};
48
+ const rootDialogId =
49
+ normalizeNonEmptyString(args.existingDialog?.rootDialogId) ??
50
+ normalizeNonEmptyString(args.existingDialog?.parentDialogId) ??
51
+ parentDialogId;
52
+ return {
53
+ ...(inheritedFromDialogKey ? { inheritedFromDialogKey } : {}),
54
+ ...(parentDialogId ? { parentDialogId } : {}),
55
+ ...(rootDialogId ? { rootDialogId } : {}),
56
+ };
57
+ }
58
+
37
59
  function buildDialogMessageWriteOps(args: {
38
60
  dialogId: string;
39
61
  input: AgentRuntimeSaveTurnInput;
@@ -102,6 +124,12 @@ export function buildAgentRuntimeDialogWritePlan(args: {
102
124
  updatedAt: nowIso,
103
125
  finishedAt: args.now,
104
126
  usage: args.input.result.usage,
127
+ ...(normalizeNonEmptyString(args.input.spaceId) ? { spaceId: normalizeNonEmptyString(args.input.spaceId) } : {}),
128
+ ...(normalizeNonEmptyString(args.input.category) ? { category: normalizeNonEmptyString(args.input.category) } : {}),
129
+ ...buildDialogLineageFields({
130
+ input: args.input,
131
+ existingDialog: args.existingDialog,
132
+ }),
105
133
  ...(typeof args.input.result.toolCallCount === "number"
106
134
  ? { toolCallCount: args.input.result.toolCallCount }
107
135
  : {}),
@@ -109,6 +137,17 @@ export function buildAgentRuntimeDialogWritePlan(args: {
109
137
  host: args.runtimeHost,
110
138
  ...(args.runtimeMetadata ?? {}),
111
139
  },
140
+ ...(args.input.result.runtimeToolSurface
141
+ ? {
142
+ runtimeCheckpoint: {
143
+ ...(args.existingDialog?.runtimeCheckpoint &&
144
+ typeof args.existingDialog.runtimeCheckpoint === "object"
145
+ ? args.existingDialog.runtimeCheckpoint
146
+ : {}),
147
+ toolSurface: args.input.result.runtimeToolSurface,
148
+ },
149
+ }
150
+ : {}),
112
151
  };
113
152
  return {
114
153
  dialogId,
@@ -56,6 +56,10 @@ export type AgentRuntimeSaveTurnInput = {
56
56
  messages: AgentRuntimeChatMessage[];
57
57
  result: AgentRuntimeResult;
58
58
  continueDialogId?: string;
59
+ spaceId?: string;
60
+ category?: string;
61
+ inheritedFromDialogKey?: string;
62
+ parentDialogId?: string;
59
63
  };
60
64
 
61
65
  export type AgentRuntimeHostAdapter = {
@@ -126,7 +126,7 @@ export function createHybridRecordStore(
126
126
  return {
127
127
  read: async (dbKey, options) => {
128
128
  const localRecord = await readHybridLocalRecord(deps.db, dbKey);
129
- if (localRecord) return { ...localRecord, dbKey };
129
+ if (localRecord) return normalizeHybridRecord(dbKey, localRecord);
130
130
  if (options?.remote === false) return null;
131
131
 
132
132
  for (const server of resolveHybridServers(
@@ -56,6 +56,14 @@ export {
56
56
  resolveLocalRuntimeEnvFromPolicy,
57
57
  resolveLocalWorkspaceExecutorOptionsFromPolicy,
58
58
  } from "./runtimeToolPolicy";
59
+ export {
60
+ DEFAULT_PRIVATE_NOLO_WORKSPACE_TOOLS,
61
+ inferOwnerIdFromRuntimeAgentKey,
62
+ isPublicRuntimeAgentRef,
63
+ redactAgentRecordForWorkspaceTool,
64
+ resolveRuntimeToolSurfaceForAgent,
65
+ resolveRuntimeToolSurface,
66
+ } from "./runtimeToolSurface";
59
67
  export {
60
68
  buildLocalWorkspaceOpenAiTools,
61
69
  buildLocalWorkspacePolicyToolNames,
@@ -111,6 +119,13 @@ export type {
111
119
  export type {
112
120
  PlatformChatProviderConfig,
113
121
  } from "./platformChatProvider";
122
+ export type {
123
+ RuntimeToolSurfaceHost,
124
+ RuntimeToolSurfaceForAgentInput,
125
+ RuntimeToolSurfaceInput,
126
+ RuntimeToolSurfaceResult,
127
+ RuntimeToolSurfaceVisibility,
128
+ } from "./runtimeToolSurface";
114
129
  export type {
115
130
  AgentRuntimeChatMessage,
116
131
  AgentRuntimeDecision,
@@ -12,7 +12,10 @@ export type LocalAgentTurnInput = {
12
12
  agentRef: string;
13
13
  input: AgentRuntimeMessageContent;
14
14
  continueDialogId?: string;
15
- maxToolRounds?: number;
15
+ spaceId?: string;
16
+ category?: string;
17
+ inheritedFromDialogKey?: string;
18
+ parentDialogId?: string;
16
19
  timeoutMs?: number;
17
20
  onToolEvent?: (event: LocalAgentToolEvent) => void;
18
21
  };
@@ -26,11 +29,13 @@ export type LocalAgentToolEvent = {
26
29
  round: number;
27
30
  toolCallId: string;
28
31
  toolName: string;
32
+ argumentsPreview?: string;
33
+ elapsedMs?: number;
34
+ summary?: string;
29
35
  message?: string;
30
36
  metadata?: Record<string, unknown>;
31
37
  };
32
38
 
33
- export const DEFAULT_LOCAL_AGENT_MAX_TOOL_ROUNDS = 128;
34
39
  export const LOCAL_AGENT_CONFIG_MISSING_CODE = "LOCAL_AGENT_CONFIG_MISSING";
35
40
 
36
41
  function formatToolExecutionError(args: {
@@ -75,6 +80,72 @@ function emitToolEvent(
75
80
  input.onToolEvent?.(event);
76
81
  }
77
82
 
83
+ function compactWhitespace(value: string) {
84
+ return value.replace(/\s+/g, " ").trim();
85
+ }
86
+
87
+ function clip(value: string, max = 240) {
88
+ const compact = compactWhitespace(value);
89
+ return compact.length > max ? `${compact.slice(0, max - 3)}...` : compact;
90
+ }
91
+
92
+ function parseToolArguments(raw: string | undefined) {
93
+ if (!raw?.trim()) return {};
94
+ try {
95
+ const parsed = JSON.parse(raw);
96
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
97
+ ? parsed as Record<string, unknown>
98
+ : {};
99
+ } catch {
100
+ return {};
101
+ }
102
+ }
103
+
104
+ function summarizeToolArguments(toolName: string, rawArgs: string | undefined) {
105
+ const args = parseToolArguments(rawArgs);
106
+ const pick = (...keys: string[]) => {
107
+ for (const key of keys) {
108
+ const value = args[key];
109
+ if (typeof value === "string" && value.trim()) return value.trim();
110
+ }
111
+ return "";
112
+ };
113
+ const command = pick("command", "cmd", "runCommand", "executeCommand", "bash");
114
+ if (command) return clip(command);
115
+ const filePath = pick("filePath", "path", "filename", "file");
116
+ if (filePath) return clip(filePath);
117
+ const query = pick("query", "pattern", "search", "q");
118
+ if (query) return clip(query);
119
+ const keys = Object.keys(args);
120
+ if (keys.length === 0) return "";
121
+ return clip(keys.slice(0, 6).map((key) => {
122
+ const value = args[key];
123
+ if (typeof value === "string") return `${key}=${clip(value, 80)}`;
124
+ if (typeof value === "number" || typeof value === "boolean") return `${key}=${String(value)}`;
125
+ if (Array.isArray(value)) return `${key}[${value.length}]`;
126
+ return `${key}=${value === null ? "null" : typeof value}`;
127
+ }).join(" "));
128
+ }
129
+
130
+ function summarizeToolResult(content: unknown, metadata?: Record<string, unknown>) {
131
+ const parts: string[] = [];
132
+ const exitCode = metadata?.exitCode;
133
+ if (typeof exitCode === "number") parts.push(`exit=${exitCode}`);
134
+ if (typeof content === "string") {
135
+ const trimmed = content.trim();
136
+ if (trimmed) {
137
+ const lines = trimmed.split(/\r?\n/).length;
138
+ parts.push(`${lines} line${lines === 1 ? "" : "s"}`);
139
+ parts.push(`${trimmed.length} chars`);
140
+ const tail = clip(trimmed.slice(-160), 160);
141
+ if (tail) parts.push(`tail="${tail}"`);
142
+ } else {
143
+ parts.push("empty");
144
+ }
145
+ }
146
+ return parts.join(" ");
147
+ }
148
+
78
149
  function buildMessages(args: {
79
150
  prompt?: string;
80
151
  history: AgentRuntimeChatMessage[];
@@ -124,19 +195,16 @@ export async function runLocalAgentTurn(
124
195
  input: input.input,
125
196
  });
126
197
  const provider = await input.adapter.resolveProvider(agentConfig);
127
- const maxToolRounds = input.maxToolRounds ?? DEFAULT_LOCAL_AGENT_MAX_TOOL_ROUNDS;
128
198
  const userInputText = extractUserInputText(input.input);
129
199
  let toolCallCount = 0;
130
200
  let result: AgentRuntimeResult;
131
- for (let round = 0; round <= maxToolRounds; round += 1) {
201
+ let round = 0;
202
+ while (true) {
132
203
  result = await provider.complete(messages, {
133
204
  ...(typeof input.timeoutMs === "number" ? { timeoutMs: input.timeoutMs } : {}),
134
205
  });
135
206
  const toolCalls = result.tool_calls ?? [];
136
207
  if (toolCalls.length === 0) break;
137
- if (round === maxToolRounds) {
138
- throw new Error(`Local agent exceeded max tool rounds: ${maxToolRounds}`);
139
- }
140
208
  toolCallCount += toolCalls.length;
141
209
  messages.push({
142
210
  role: "assistant",
@@ -147,11 +215,13 @@ export async function runLocalAgentTurn(
147
215
  for (const toolCall of toolCalls) {
148
216
  const toolName = toolCall.function.name;
149
217
  let toolResult;
218
+ const startedAt = Date.now();
150
219
  emitToolEvent(input, {
151
220
  type: "tool-call",
152
221
  round,
153
222
  toolCallId: toolCall.id,
154
223
  toolName,
224
+ argumentsPreview: summarizeToolArguments(toolName, toolCall.function.arguments),
155
225
  });
156
226
  try {
157
227
  toolResult = await input.adapter.executeTool({
@@ -165,6 +235,8 @@ export async function runLocalAgentTurn(
165
235
  round,
166
236
  toolCallId: toolCall.id,
167
237
  toolName,
238
+ elapsedMs: Math.max(0, Date.now() - startedAt),
239
+ summary: summarizeToolResult(toolResult.content, toolResult.metadata),
168
240
  metadata: toolResult.metadata,
169
241
  });
170
242
  } catch (error) {
@@ -174,6 +246,7 @@ export async function runLocalAgentTurn(
174
246
  round,
175
247
  toolCallId: toolCall.id,
176
248
  toolName,
249
+ elapsedMs: Math.max(0, Date.now() - startedAt),
177
250
  message: error instanceof Error ? error.message : String(error),
178
251
  });
179
252
  toolResult = {
@@ -201,6 +274,7 @@ export async function runLocalAgentTurn(
201
274
  ...(toolResult.metadata ? { tool_result_metadata: toolResult.metadata } : {}),
202
275
  });
203
276
  }
277
+ round += 1;
204
278
  }
205
279
  result = result!;
206
280
  messages.push({
@@ -213,13 +287,19 @@ export async function runLocalAgentTurn(
213
287
  result: {
214
288
  ...result,
215
289
  ...(toolCallCount > 0 ? { toolCallCount } : {}),
290
+ ...((agentConfig as any).toolSurface ? { runtimeToolSurface: (agentConfig as any).toolSurface } : {}),
216
291
  },
217
292
  ...(input.continueDialogId ? { continueDialogId: input.continueDialogId } : {}),
293
+ ...(input.spaceId ? { spaceId: input.spaceId } : {}),
294
+ ...(input.category ? { category: input.category } : {}),
295
+ ...(input.inheritedFromDialogKey ? { inheritedFromDialogKey: input.inheritedFromDialogKey } : {}),
296
+ ...(input.parentDialogId ? { parentDialogId: input.parentDialogId } : {}),
218
297
  });
219
298
 
220
299
  return {
221
300
  ...result,
222
301
  ...(toolCallCount > 0 ? { toolCallCount } : {}),
302
+ ...((agentConfig as any).toolSurface ? { runtimeToolSurface: (agentConfig as any).toolSurface } : {}),
223
303
  dialogId: saved.dialogId,
224
304
  };
225
305
  }
@@ -28,16 +28,12 @@ const REMOVED_LOCAL_TOOLS = new Set([
28
28
  ]);
29
29
 
30
30
  const DEFAULT_LOCAL_TOOLS = new Set([
31
- "listWorkspaceFiles",
32
- "readWorkspaceFile",
33
- "writeWorkspaceFile",
34
- "replaceWorkspaceText",
35
31
  "listFiles",
36
32
  "readFile",
37
33
  "writeFile",
38
34
  "editFile",
39
35
  "searchFiles",
40
- "searchWorkspace",
36
+ "execShell",
41
37
  ]);
42
38
 
43
39
  function parseToolAllowlist(value: string | undefined) {
@@ -96,14 +92,7 @@ export function resolveLocalToolPolicy(args: {
96
92
  }
97
93
 
98
94
  if (toolName === "execShell") {
99
- if (args.env.NOLO_LOCAL_SHELL_MODE === "worktree") {
100
- return { allowed: true, toolName };
101
- }
102
- return {
103
- allowed: false,
104
- toolName,
105
- reason: "execShell requires NOLO_LOCAL_SHELL_MODE=worktree for local runtime runs.",
106
- };
95
+ return { allowed: true, toolName };
107
96
  }
108
97
 
109
98
  if (NEVER_LOCAL_TOOLS.has(toolName)) {
@@ -43,18 +43,11 @@ type OpenAiCompatibleTool = Record<string, unknown> & {
43
43
  };
44
44
 
45
45
  const WORKSPACE_TOOL_NAMES = [
46
- "listWorkspaceFiles",
47
- "readWorkspaceFile",
48
- "writeWorkspaceFile",
49
- "replaceWorkspaceText",
50
46
  "listFiles",
51
47
  "readFile",
52
48
  "writeFile",
53
49
  "editFile",
54
50
  "searchFiles",
55
- "searchWorkspace",
56
- "applyEdit",
57
- "applyLineEdits",
58
51
  "startPreview",
59
52
  "getPreviewStatus",
60
53
  "stopPreview",
@@ -93,7 +86,7 @@ function buildListWorkspaceFilesTool(): OpenAiCompatibleTool {
93
86
  return {
94
87
  type: "function",
95
88
  function: {
96
- name: "listWorkspaceFiles",
89
+ name: "listFiles",
97
90
  description: "List files and directories inside a workspace directory.",
98
91
  parameters: {
99
92
  type: "object",
@@ -109,9 +102,9 @@ function buildReadWorkspaceFileTool(): OpenAiCompatibleTool {
109
102
  return {
110
103
  type: "function",
111
104
  function: {
112
- name: "readWorkspaceFile",
105
+ name: "readFile",
113
106
  description:
114
- "Read a UTF-8 text file inside the workspace. Read before editing when you need the current content or exact text for replaceWorkspaceText; skip unnecessary reads for trivial known writes.",
107
+ "Read a UTF-8 text file inside the workspace. Read before editing when you need the current content or exact text for editFile; skip unnecessary reads for trivial known writes.",
115
108
  parameters: {
116
109
  type: "object",
117
110
  properties: {
@@ -127,9 +120,9 @@ function buildWriteWorkspaceFileTool(): OpenAiCompatibleTool {
127
120
  return {
128
121
  type: "function",
129
122
  function: {
130
- name: "writeWorkspaceFile",
123
+ name: "writeFile",
131
124
  description:
132
- "Write full UTF-8 file content inside the workspace. Prefer replaceWorkspaceText for small edits to existing large files; use this for new files or deliberate whole-file rewrites.",
125
+ "Write full UTF-8 file content inside the workspace. For new files or deliberate whole-file rewrites only. For existing files, prefer editFile for targeted edits. Warn that whole-file rewrites can cause line-ending churn.",
133
126
  parameters: {
134
127
  type: "object",
135
128
  properties: {
@@ -149,9 +142,9 @@ function buildReplaceWorkspaceTextTool(): OpenAiCompatibleTool {
149
142
  return {
150
143
  type: "function",
151
144
  function: {
152
- name: "replaceWorkspaceText",
145
+ name: "editFile",
153
146
  description:
154
- "Use for small, exact edits in one workspace file without constructing a patch. Read the file first when you need the exact oldText; set expectedReplacements to avoid accidental broad edits.",
147
+ "Use for small, exact edits in one workspace file without constructing a patch. Read the file first when you need the exact oldText; set expectedReplacements to avoid accidental broad edits. When expected replacement count fails, report a blocker instead of falling back to a full-file rewrite.",
155
148
  parameters: {
156
149
  type: "object",
157
150
  properties: {
@@ -179,7 +172,7 @@ function buildSearchWorkspaceTool(): OpenAiCompatibleTool {
179
172
  return {
180
173
  type: "function",
181
174
  function: {
182
- name: "searchWorkspace",
175
+ name: "searchFiles",
183
176
  description: "Search text in the current workspace using ripgrep when available.",
184
177
  parameters: {
185
178
  type: "object",
@@ -337,45 +330,21 @@ function buildWorkspaceShellCommand(args: {
337
330
  : buildBashCommand(args.command);
338
331
  }
339
332
 
340
- function renameWorkspaceToolDefinition(
341
- tool: OpenAiCompatibleTool,
342
- name: string
343
- ): OpenAiCompatibleTool {
344
- const description = typeof tool.function?.description === "string"
345
- ? tool.function.description
346
- .replaceAll("replaceWorkspaceText", "editFile")
347
- .replaceAll("readWorkspaceFile", "readFile")
348
- : tool.function?.description;
349
- return {
350
- ...tool,
351
- function: {
352
- ...(tool.function ?? {}),
353
- name,
354
- description,
355
- },
356
- };
357
- }
358
-
359
333
  function buildWorkspaceToolDefinition(toolName: string) {
360
- if (toolName === "listWorkspaceFiles" || toolName === "listFiles") {
361
- return renameWorkspaceToolDefinition(buildListWorkspaceFilesTool(), toolName);
334
+ if (toolName === "listFiles") {
335
+ return buildListWorkspaceFilesTool();
362
336
  }
363
- if (toolName === "readWorkspaceFile" || toolName === "readFile") {
364
- return renameWorkspaceToolDefinition(buildReadWorkspaceFileTool(), toolName);
337
+ if (toolName === "readFile") {
338
+ return buildReadWorkspaceFileTool();
365
339
  }
366
- if (toolName === "writeWorkspaceFile" || toolName === "writeFile") {
367
- return renameWorkspaceToolDefinition(buildWriteWorkspaceFileTool(), toolName);
340
+ if (toolName === "writeFile") {
341
+ return buildWriteWorkspaceFileTool();
368
342
  }
369
- if (
370
- toolName === "replaceWorkspaceText" ||
371
- toolName === "editFile" ||
372
- toolName === "applyEdit" ||
373
- toolName === "applyLineEdits"
374
- ) {
375
- return renameWorkspaceToolDefinition(buildReplaceWorkspaceTextTool(), toolName);
343
+ if (toolName === "editFile") {
344
+ return buildReplaceWorkspaceTextTool();
376
345
  }
377
- if (toolName === "searchWorkspace" || toolName === "searchFiles") {
378
- return renameWorkspaceToolDefinition(buildSearchWorkspaceTool(), toolName);
346
+ if (toolName === "searchFiles") {
347
+ return buildSearchWorkspaceTool();
379
348
  }
380
349
  if (toolName === "startPreview" || toolName === "getPreviewStatus" || toolName === "stopPreview" || toolName === "releasePreview") {
381
350
  return buildPreviewLifecycleTool(toolName);
@@ -385,20 +354,6 @@ function buildWorkspaceToolDefinition(toolName: string) {
385
354
  return null;
386
355
  }
387
356
 
388
- function removeRedundantWorkspaceToolAliases(toolNames: Set<string>) {
389
- const next = new Set(toolNames);
390
- if (next.has("listWorkspaceFiles")) next.delete("listFiles");
391
- if (next.has("readWorkspaceFile")) next.delete("readFile");
392
- if (next.has("writeWorkspaceFile")) next.delete("writeFile");
393
- if (next.has("replaceWorkspaceText")) {
394
- next.delete("editFile");
395
- next.delete("applyEdit");
396
- next.delete("applyLineEdits");
397
- }
398
- if (next.has("searchWorkspace")) next.delete("searchFiles");
399
- return next;
400
- }
401
-
402
357
  function filterDeclaredWorkspaceToolNames(args: {
403
358
  toolNames?: string[];
404
359
  exposeShellTools: boolean;
@@ -415,14 +370,14 @@ export function buildLocalWorkspaceToolset(args: {
415
370
  exposeShellTools?: boolean;
416
371
  }) {
417
372
  const exposeShellTools = args.exposeShellTools === true;
418
- const toolNames = removeRedundantWorkspaceToolAliases(new Set([
373
+ const toolNames = new Set([
419
374
  ...DEFAULT_LOCAL_CODING_TOOL_NAMES,
420
375
  ...(exposeShellTools ? SHELL_TOOL_NAMES : []),
421
376
  ...filterDeclaredWorkspaceToolNames({
422
377
  toolNames: args.declaredToolNames,
423
378
  exposeShellTools,
424
379
  }),
425
- ]));
380
+ ]);
426
381
  return {
427
382
  toolNames: [...toolNames],
428
383
  exposeShellTools,
@@ -448,7 +403,7 @@ export function buildLocalWorkspaceOpenAiTools(args: {
448
403
  toolNames?: string[];
449
404
  exposeShellTools?: boolean;
450
405
  }) {
451
- const declaredTools = removeRedundantWorkspaceToolAliases(new Set(args.toolNames ?? []));
406
+ const declaredTools = new Set(args.toolNames ?? []);
452
407
  return WORKSPACE_TOOL_NAMES
453
408
  .filter((toolName) => {
454
409
  if (!declaredTools.has(toolName)) return false;
@@ -494,21 +449,21 @@ function requireWorkspaceToolPath(args: WorkspaceFileArgs) {
494
449
 
495
450
  function requireWorkspaceFileContent(args: WorkspaceFileArgs) {
496
451
  if (typeof args.content !== "string") {
497
- throw new Error("writeWorkspaceFile requires string content.");
452
+ throw new Error("writeFile requires string content.");
498
453
  }
499
454
  return args.content;
500
455
  }
501
456
 
502
457
  function requireWorkspaceOldText(args: WorkspaceFileArgs) {
503
458
  if (typeof args.oldText !== "string" || !args.oldText) {
504
- throw new Error("replaceWorkspaceText requires non-empty oldText.");
459
+ throw new Error("editFile requires non-empty oldText.");
505
460
  }
506
461
  return args.oldText;
507
462
  }
508
463
 
509
464
  function requireWorkspaceNewText(args: WorkspaceFileArgs) {
510
465
  if (typeof args.newText !== "string") {
511
- throw new Error("replaceWorkspaceText requires string newText.");
466
+ throw new Error("editFile requires string newText.");
512
467
  }
513
468
  return args.newText;
514
469
  }
@@ -517,7 +472,7 @@ function readExpectedReplacementCount(args: WorkspaceFileArgs) {
517
472
  if (args.expectedReplacements === undefined) return 1;
518
473
  const value = Number(args.expectedReplacements);
519
474
  if (!Number.isInteger(value) || value < 1) {
520
- throw new Error("replaceWorkspaceText expectedReplacements must be a positive integer.");
475
+ throw new Error("editFile expectedReplacements must be a positive integer.");
521
476
  }
522
477
  return value;
523
478
  }
@@ -535,7 +490,7 @@ function pluralizeReplacement(count: number) {
535
490
 
536
491
  function requireWorkspaceSearchQuery(args: WorkspaceFileArgs) {
537
492
  const query = typeof args.query === "string" ? args.query.trim() : "";
538
- if (!query) throw new Error("searchWorkspace requires a non-empty query.");
493
+ if (!query) throw new Error("searchFiles requires a non-empty query.");
539
494
  return query;
540
495
  }
541
496
 
@@ -783,7 +738,7 @@ export function resolveLocalWorkspaceToolPath(args: {
783
738
  return targetPath;
784
739
  }
785
740
 
786
- async function readWorkspaceFileTool(args: {
741
+ async function readFileTool(args: {
787
742
  call: AgentRuntimeToolCallInput;
788
743
  workspaceRoot: string;
789
744
  }): Promise<AgentRuntimeToolResult> {
@@ -806,10 +761,15 @@ async function readWorkspaceFileTool(args: {
806
761
  };
807
762
  }
808
763
 
809
- async function writeWorkspaceFileTool(args: {
764
+ async function writeFileTool(args: {
810
765
  call: AgentRuntimeToolCallInput;
811
766
  workspaceRoot: string;
812
767
  }): Promise<AgentRuntimeToolResult> {
768
+ // TODO: Explore cheaply including diff stat/EOL warning after writeFile.
769
+ // If implementation is larger than a small safe change, defer it.
770
+ // Exact boundaries: writeFileTool function in localWorkspaceTools.ts.
771
+ // Consider adding a post-write hook that runs `git diff --stat` and checks line endings.
772
+ // Keep it optional and non-blocking.
813
773
  const parsed = parseWorkspaceToolArguments(args.call.arguments);
814
774
  const requestedPath = requireWorkspaceToolPath(parsed);
815
775
  const content = requireWorkspaceFileContent(parsed);
@@ -832,7 +792,7 @@ async function writeWorkspaceFileTool(args: {
832
792
  };
833
793
  }
834
794
 
835
- async function replaceWorkspaceTextTool(args: {
795
+ async function editFileTool(args: {
836
796
  call: AgentRuntimeToolCallInput;
837
797
  workspaceRoot: string;
838
798
  }): Promise<AgentRuntimeToolResult> {
@@ -849,7 +809,7 @@ async function replaceWorkspaceTextTool(args: {
849
809
  const replacementCount = countExactTextOccurrences({ content, oldText });
850
810
  if (replacementCount !== expectedReplacements) {
851
811
  throw new Error(
852
- `replaceWorkspaceText expected ${expectedReplacements} ${pluralizeReplacement(expectedReplacements)} ` +
812
+ `editFile expected ${expectedReplacements} ${pluralizeReplacement(expectedReplacements)} ` +
853
813
  `but found ${replacementCount} in ${requestedPath}.`
854
814
  );
855
815
  }
@@ -883,7 +843,7 @@ async function formatWorkspaceDirEntry(args: {
883
843
  return info.isDirectory() ? `${relativePath}/` : relativePath;
884
844
  }
885
845
 
886
- async function listWorkspaceFilesTool(args: {
846
+ async function listFilesTool(args: {
887
847
  call: AgentRuntimeToolCallInput;
888
848
  workspaceRoot: string;
889
849
  }): Promise<AgentRuntimeToolResult> {
@@ -910,7 +870,7 @@ async function listWorkspaceFilesTool(args: {
910
870
  };
911
871
  }
912
872
 
913
- async function searchWorkspaceTool(args: {
873
+ async function searchFilesTool(args: {
914
874
  call: AgentRuntimeToolCallInput;
915
875
  workspaceRoot: string;
916
876
  }): Promise<AgentRuntimeToolResult> {
@@ -1110,51 +1070,23 @@ async function execShellTool(args: {
1110
1070
 
1111
1071
  export function createLocalWorkspaceToolExecutors(args: LocalWorkspaceToolArgs) {
1112
1072
  return {
1113
- listWorkspaceFiles: (call: AgentRuntimeToolCallInput) => listWorkspaceFilesTool({
1114
- call,
1115
- workspaceRoot: args.workspaceRoot,
1116
- }),
1117
- readWorkspaceFile: (call: AgentRuntimeToolCallInput) => readWorkspaceFileTool({
1118
- call,
1119
- workspaceRoot: args.workspaceRoot,
1120
- }),
1121
- writeWorkspaceFile: (call: AgentRuntimeToolCallInput) => writeWorkspaceFileTool({
1122
- call,
1123
- workspaceRoot: args.workspaceRoot,
1124
- }),
1125
- replaceWorkspaceText: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1126
- call,
1127
- workspaceRoot: args.workspaceRoot,
1128
- }),
1129
- editFile: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1130
- call,
1131
- workspaceRoot: args.workspaceRoot,
1132
- }),
1133
- applyEdit: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1134
- call,
1135
- workspaceRoot: args.workspaceRoot,
1136
- }),
1137
- applyLineEdits: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1138
- call,
1139
- workspaceRoot: args.workspaceRoot,
1140
- }),
1141
- listFiles: (call: AgentRuntimeToolCallInput) => listWorkspaceFilesTool({
1073
+ editFile: (call: AgentRuntimeToolCallInput) => editFileTool({
1142
1074
  call,
1143
1075
  workspaceRoot: args.workspaceRoot,
1144
1076
  }),
1145
- readFile: (call: AgentRuntimeToolCallInput) => readWorkspaceFileTool({
1077
+ listFiles: (call: AgentRuntimeToolCallInput) => listFilesTool({
1146
1078
  call,
1147
1079
  workspaceRoot: args.workspaceRoot,
1148
1080
  }),
1149
- writeFile: (call: AgentRuntimeToolCallInput) => writeWorkspaceFileTool({
1081
+ readFile: (call: AgentRuntimeToolCallInput) => readFileTool({
1150
1082
  call,
1151
1083
  workspaceRoot: args.workspaceRoot,
1152
1084
  }),
1153
- searchWorkspace: (call: AgentRuntimeToolCallInput) => searchWorkspaceTool({
1085
+ writeFile: (call: AgentRuntimeToolCallInput) => writeFileTool({
1154
1086
  call,
1155
1087
  workspaceRoot: args.workspaceRoot,
1156
1088
  }),
1157
- searchFiles: (call: AgentRuntimeToolCallInput) => searchWorkspaceTool({
1089
+ searchFiles: (call: AgentRuntimeToolCallInput) => searchFilesTool({
1158
1090
  call,
1159
1091
  workspaceRoot: args.workspaceRoot,
1160
1092
  }),