glm-acp-agent 1.0.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/README.md +365 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/credentials.d.ts +15 -0
- package/dist/llm/credentials.d.ts.map +1 -0
- package/dist/llm/credentials.js +54 -0
- package/dist/llm/credentials.js.map +1 -0
- package/dist/llm/glm-client.d.ts +63 -0
- package/dist/llm/glm-client.d.ts.map +1 -0
- package/dist/llm/glm-client.js +217 -0
- package/dist/llm/glm-client.js.map +1 -0
- package/dist/llm/logger.d.ts +19 -0
- package/dist/llm/logger.d.ts.map +1 -0
- package/dist/llm/logger.js +44 -0
- package/dist/llm/logger.js.map +1 -0
- package/dist/protocol/agent.d.ts +77 -0
- package/dist/protocol/agent.d.ts.map +1 -0
- package/dist/protocol/agent.js +732 -0
- package/dist/protocol/agent.js.map +1 -0
- package/dist/protocol/connection.d.ts +9 -0
- package/dist/protocol/connection.d.ts.map +1 -0
- package/dist/protocol/connection.js +18 -0
- package/dist/protocol/connection.js.map +1 -0
- package/dist/protocol/image-preprocessor.d.ts +32 -0
- package/dist/protocol/image-preprocessor.d.ts.map +1 -0
- package/dist/protocol/image-preprocessor.js +149 -0
- package/dist/protocol/image-preprocessor.js.map +1 -0
- package/dist/protocol/session-store.d.ts +54 -0
- package/dist/protocol/session-store.d.ts.map +1 -0
- package/dist/protocol/session-store.js +109 -0
- package/dist/protocol/session-store.js.map +1 -0
- package/dist/protocol/system-prompt.d.ts +29 -0
- package/dist/protocol/system-prompt.d.ts.map +1 -0
- package/dist/protocol/system-prompt.js +103 -0
- package/dist/protocol/system-prompt.js.map +1 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +95 -0
- package/dist/setup.js.map +1 -0
- package/dist/tests/agent.test.d.ts +2 -0
- package/dist/tests/agent.test.d.ts.map +1 -0
- package/dist/tests/agent.test.js +1118 -0
- package/dist/tests/agent.test.js.map +1 -0
- package/dist/tests/credentials.test.d.ts +2 -0
- package/dist/tests/credentials.test.d.ts.map +1 -0
- package/dist/tests/credentials.test.js +112 -0
- package/dist/tests/credentials.test.js.map +1 -0
- package/dist/tests/executor.test.d.ts +2 -0
- package/dist/tests/executor.test.d.ts.map +1 -0
- package/dist/tests/executor.test.js +497 -0
- package/dist/tests/executor.test.js.map +1 -0
- package/dist/tests/glm-client.test.d.ts +2 -0
- package/dist/tests/glm-client.test.d.ts.map +1 -0
- package/dist/tests/glm-client.test.js +205 -0
- package/dist/tests/glm-client.test.js.map +1 -0
- package/dist/tests/image-preprocessor.test.d.ts +2 -0
- package/dist/tests/image-preprocessor.test.d.ts.map +1 -0
- package/dist/tests/image-preprocessor.test.js +133 -0
- package/dist/tests/image-preprocessor.test.js.map +1 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +192 -0
- package/dist/tests/integration.test.js.map +1 -0
- package/dist/tests/logger.test.d.ts +2 -0
- package/dist/tests/logger.test.d.ts.map +1 -0
- package/dist/tests/logger.test.js +14 -0
- package/dist/tests/logger.test.js.map +1 -0
- package/dist/tests/mcp-arg-remap.test.d.ts +2 -0
- package/dist/tests/mcp-arg-remap.test.d.ts.map +1 -0
- package/dist/tests/mcp-arg-remap.test.js +65 -0
- package/dist/tests/mcp-arg-remap.test.js.map +1 -0
- package/dist/tests/vision-mcp-client.test.d.ts +2 -0
- package/dist/tests/vision-mcp-client.test.d.ts.map +1 -0
- package/dist/tests/vision-mcp-client.test.js +129 -0
- package/dist/tests/vision-mcp-client.test.js.map +1 -0
- package/dist/tests/zai-mcp-client.test.d.ts +2 -0
- package/dist/tests/zai-mcp-client.test.d.ts.map +1 -0
- package/dist/tests/zai-mcp-client.test.js +386 -0
- package/dist/tests/zai-mcp-client.test.js.map +1 -0
- package/dist/tools/definitions.d.ts +18 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +149 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/executor.d.ts +48 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +670 -0
- package/dist/tools/executor.js.map +1 -0
- package/dist/tools/mcp-arg-remap.d.ts +21 -0
- package/dist/tools/mcp-arg-remap.d.ts.map +1 -0
- package/dist/tools/mcp-arg-remap.js +70 -0
- package/dist/tools/mcp-arg-remap.js.map +1 -0
- package/dist/tools/vision-mcp-client.d.ts +38 -0
- package/dist/tools/vision-mcp-client.d.ts.map +1 -0
- package/dist/tools/vision-mcp-client.js +191 -0
- package/dist/tools/vision-mcp-client.js.map +1 -0
- package/dist/tools/zai-mcp-client.d.ts +24 -0
- package/dist/tools/zai-mcp-client.d.ts.map +1 -0
- package/dist/tools/zai-mcp-client.js +189 -0
- package/dist/tools/zai-mcp-client.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { resolveApiKey } from "../llm/credentials.js";
|
|
2
|
+
import { callZaiMcpTool, ZAI_WEB_READER_MCP_ENDPOINT, ZAI_WEB_SEARCH_MCP_ENDPOINT, } from "./zai-mcp-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Executes GLM tool calls by delegating to the corresponding ACP Client methods.
|
|
5
|
+
*
|
|
6
|
+
* Permission is requested from the user before any write or execute operation.
|
|
7
|
+
* Tools that require capabilities the client did not advertise return a clear
|
|
8
|
+
* error string instead of throwing, so the model can recover by trying a
|
|
9
|
+
* different approach.
|
|
10
|
+
*/
|
|
11
|
+
export class ToolExecutor {
|
|
12
|
+
connection;
|
|
13
|
+
sessionId;
|
|
14
|
+
clientCapabilities;
|
|
15
|
+
signal;
|
|
16
|
+
visionClient;
|
|
17
|
+
constructor(connection, sessionId, clientCapabilities = null, signal, visionClient = null) {
|
|
18
|
+
this.connection = connection;
|
|
19
|
+
this.sessionId = sessionId;
|
|
20
|
+
this.clientCapabilities = clientCapabilities;
|
|
21
|
+
this.signal = signal;
|
|
22
|
+
this.visionClient = visionClient;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Dispatch a tool call from GLM to the appropriate ACP Client method.
|
|
26
|
+
*
|
|
27
|
+
* Returns a plain text result that can be fed back to GLM as a tool message.
|
|
28
|
+
*/
|
|
29
|
+
async execute(toolCallId, toolName, rawArguments) {
|
|
30
|
+
let args;
|
|
31
|
+
try {
|
|
32
|
+
args =
|
|
33
|
+
rawArguments.trim().length === 0
|
|
34
|
+
? {}
|
|
35
|
+
: JSON.parse(rawArguments);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
const message = `Error: could not parse tool arguments as JSON: ${rawArguments}`;
|
|
39
|
+
await this.failedToolCall(toolCallId, toolName, {}, message);
|
|
40
|
+
return { content: message };
|
|
41
|
+
}
|
|
42
|
+
switch (toolName) {
|
|
43
|
+
case "read_file":
|
|
44
|
+
return this.readFile(toolCallId, args);
|
|
45
|
+
case "write_file":
|
|
46
|
+
return this.writeFile(toolCallId, args);
|
|
47
|
+
case "list_files":
|
|
48
|
+
return this.listFiles(toolCallId, args);
|
|
49
|
+
case "run_command":
|
|
50
|
+
return this.runCommand(toolCallId, args);
|
|
51
|
+
case "web_search":
|
|
52
|
+
return this.webSearch(toolCallId, args);
|
|
53
|
+
case "web_reader":
|
|
54
|
+
return this.webReader(toolCallId, args);
|
|
55
|
+
case "image_analysis":
|
|
56
|
+
return this.imageAnalysis(toolCallId, args);
|
|
57
|
+
default: {
|
|
58
|
+
const message = `Error: unknown tool "${toolName}"`;
|
|
59
|
+
await this.failedToolCall(toolCallId, toolName, args, message);
|
|
60
|
+
return { content: message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Capability helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
requireCap(toolCallId, toolName, args, available, capName) {
|
|
68
|
+
if (available)
|
|
69
|
+
return null;
|
|
70
|
+
const message = `Error: client does not advertise the ${capName} capability; this tool is unavailable.`;
|
|
71
|
+
return (async () => {
|
|
72
|
+
await this.failedToolCall(toolCallId, toolName, args, message);
|
|
73
|
+
return { content: message };
|
|
74
|
+
})();
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Private tool implementations
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
async readFile(toolCallId, args) {
|
|
80
|
+
const cap = this.requireCap(toolCallId, "read_file", args, Boolean(this.clientCapabilities?.fs?.readTextFile), "fs.readTextFile");
|
|
81
|
+
if (cap)
|
|
82
|
+
return cap;
|
|
83
|
+
const path = String(args["path"] ?? "").trim();
|
|
84
|
+
if (!path) {
|
|
85
|
+
return this.failAndReturn(toolCallId, "read_file", args, "Error: `path` is required.");
|
|
86
|
+
}
|
|
87
|
+
await this.connection.sessionUpdate({
|
|
88
|
+
sessionId: this.sessionId,
|
|
89
|
+
update: {
|
|
90
|
+
sessionUpdate: "tool_call",
|
|
91
|
+
toolCallId,
|
|
92
|
+
title: `Read file: ${path}`,
|
|
93
|
+
kind: "read",
|
|
94
|
+
status: "in_progress",
|
|
95
|
+
locations: [{ path }],
|
|
96
|
+
rawInput: args,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
const response = await this.connection.readTextFile({
|
|
101
|
+
sessionId: this.sessionId,
|
|
102
|
+
path,
|
|
103
|
+
});
|
|
104
|
+
await this.connection.sessionUpdate({
|
|
105
|
+
sessionId: this.sessionId,
|
|
106
|
+
update: {
|
|
107
|
+
sessionUpdate: "tool_call_update",
|
|
108
|
+
toolCallId,
|
|
109
|
+
status: "completed",
|
|
110
|
+
content: [{ type: "content", content: { type: "text", text: response.content } }],
|
|
111
|
+
rawOutput: { content: response.content },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return { content: response.content };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
+
await this.markFailed(toolCallId, message);
|
|
119
|
+
return { content: `Error reading file: ${message}` };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async writeFile(toolCallId, args) {
|
|
123
|
+
const cap = this.requireCap(toolCallId, "write_file", args, Boolean(this.clientCapabilities?.fs?.writeTextFile), "fs.writeTextFile");
|
|
124
|
+
if (cap)
|
|
125
|
+
return cap;
|
|
126
|
+
const path = String(args["path"] ?? "").trim();
|
|
127
|
+
const content = String(args["content"] ?? "");
|
|
128
|
+
if (!path) {
|
|
129
|
+
return this.failAndReturn(toolCallId, "write_file", args, "Error: `path` is required.");
|
|
130
|
+
}
|
|
131
|
+
// Step 1: announce the pending tool call so the client can show it.
|
|
132
|
+
await this.connection.sessionUpdate({
|
|
133
|
+
sessionId: this.sessionId,
|
|
134
|
+
update: {
|
|
135
|
+
sessionUpdate: "tool_call",
|
|
136
|
+
toolCallId,
|
|
137
|
+
title: `Write file: ${path}`,
|
|
138
|
+
kind: "edit",
|
|
139
|
+
status: "pending",
|
|
140
|
+
locations: [{ path }],
|
|
141
|
+
rawInput: args,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
// Step 2: request user permission. Treat transport failures here as a
|
|
145
|
+
// failed tool call instead of letting them escape and abort the loop.
|
|
146
|
+
let permissionResponse;
|
|
147
|
+
try {
|
|
148
|
+
permissionResponse = await this.connection.requestPermission({
|
|
149
|
+
sessionId: this.sessionId,
|
|
150
|
+
toolCall: {
|
|
151
|
+
toolCallId,
|
|
152
|
+
title: `Write file: ${path}`,
|
|
153
|
+
kind: "edit",
|
|
154
|
+
status: "pending",
|
|
155
|
+
locations: [{ path }],
|
|
156
|
+
rawInput: args,
|
|
157
|
+
},
|
|
158
|
+
options: [
|
|
159
|
+
{ kind: "allow_once", name: "Allow write", optionId: "allow" },
|
|
160
|
+
{ kind: "reject_once", name: "Skip write", optionId: "reject" },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
166
|
+
await this.markFailed(toolCallId, message);
|
|
167
|
+
return { content: `Error requesting permission: ${message}` };
|
|
168
|
+
}
|
|
169
|
+
if (permissionResponse.outcome.outcome === "cancelled") {
|
|
170
|
+
await this.markFailed(toolCallId, "Cancelled by user.");
|
|
171
|
+
return { content: "Write cancelled by user." };
|
|
172
|
+
}
|
|
173
|
+
if (permissionResponse.outcome.outcome === "selected" &&
|
|
174
|
+
permissionResponse.outcome.optionId === "reject") {
|
|
175
|
+
await this.markFailed(toolCallId, "Rejected by user.");
|
|
176
|
+
return { content: "Write rejected by user." };
|
|
177
|
+
}
|
|
178
|
+
// Step 3: move to in_progress and execute.
|
|
179
|
+
await this.connection.sessionUpdate({
|
|
180
|
+
sessionId: this.sessionId,
|
|
181
|
+
update: {
|
|
182
|
+
sessionUpdate: "tool_call_update",
|
|
183
|
+
toolCallId,
|
|
184
|
+
status: "in_progress",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
await this.connection.writeTextFile({
|
|
189
|
+
sessionId: this.sessionId,
|
|
190
|
+
path,
|
|
191
|
+
content,
|
|
192
|
+
});
|
|
193
|
+
await this.connection.sessionUpdate({
|
|
194
|
+
sessionId: this.sessionId,
|
|
195
|
+
update: {
|
|
196
|
+
sessionUpdate: "tool_call_update",
|
|
197
|
+
toolCallId,
|
|
198
|
+
status: "completed",
|
|
199
|
+
rawOutput: { success: true },
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return { content: `File written successfully: ${path}` };
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
206
|
+
await this.markFailed(toolCallId, message);
|
|
207
|
+
return { content: `Error writing file: ${message}` };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async listFiles(toolCallId, args) {
|
|
211
|
+
const cap = this.requireCap(toolCallId, "list_files", args, Boolean(this.clientCapabilities?.terminal), "terminal");
|
|
212
|
+
if (cap)
|
|
213
|
+
return cap;
|
|
214
|
+
const rawPath = args["path"];
|
|
215
|
+
if (typeof rawPath !== "string" || rawPath.trim().length === 0) {
|
|
216
|
+
return this.failAndReturn(toolCallId, "list_files", args, "Error listing files: `path` must be a non-empty string.");
|
|
217
|
+
}
|
|
218
|
+
const path = rawPath;
|
|
219
|
+
await this.connection.sessionUpdate({
|
|
220
|
+
sessionId: this.sessionId,
|
|
221
|
+
update: {
|
|
222
|
+
sessionUpdate: "tool_call",
|
|
223
|
+
toolCallId,
|
|
224
|
+
title: `List files: ${path}`,
|
|
225
|
+
kind: "read",
|
|
226
|
+
status: "in_progress",
|
|
227
|
+
locations: [{ path }],
|
|
228
|
+
rawInput: args,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
let terminal;
|
|
232
|
+
try {
|
|
233
|
+
terminal = await this.connection.createTerminal({
|
|
234
|
+
sessionId: this.sessionId,
|
|
235
|
+
// Run via a shell so quoting / wildcards behave like a real ls call.
|
|
236
|
+
command: "sh",
|
|
237
|
+
args: ["-c", `ls -la -- ${shellQuote(path)}`],
|
|
238
|
+
});
|
|
239
|
+
await terminal.waitForExit();
|
|
240
|
+
const outputResponse = await terminal.currentOutput();
|
|
241
|
+
const output = outputResponse.output;
|
|
242
|
+
await this.connection.sessionUpdate({
|
|
243
|
+
sessionId: this.sessionId,
|
|
244
|
+
update: {
|
|
245
|
+
sessionUpdate: "tool_call_update",
|
|
246
|
+
toolCallId,
|
|
247
|
+
status: "completed",
|
|
248
|
+
content: [{ type: "terminal", terminalId: terminal.id }],
|
|
249
|
+
rawOutput: { output },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
return { content: output };
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
256
|
+
await this.markFailed(toolCallId, message);
|
|
257
|
+
return { content: `Error listing files: ${message}` };
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
try {
|
|
261
|
+
await terminal?.release();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// ignore release errors
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async runCommand(toolCallId, args) {
|
|
269
|
+
const cap = this.requireCap(toolCallId, "run_command", args, Boolean(this.clientCapabilities?.terminal), "terminal");
|
|
270
|
+
if (cap)
|
|
271
|
+
return cap;
|
|
272
|
+
const command = String(args["command"] ?? "").trim();
|
|
273
|
+
if (!command) {
|
|
274
|
+
return this.failAndReturn(toolCallId, "run_command", args, "Error running command: command must be a non-empty string.");
|
|
275
|
+
}
|
|
276
|
+
// Step 1: announce.
|
|
277
|
+
await this.connection.sessionUpdate({
|
|
278
|
+
sessionId: this.sessionId,
|
|
279
|
+
update: {
|
|
280
|
+
sessionUpdate: "tool_call",
|
|
281
|
+
toolCallId,
|
|
282
|
+
title: `Run command: ${command}`,
|
|
283
|
+
kind: "execute",
|
|
284
|
+
status: "pending",
|
|
285
|
+
locations: [],
|
|
286
|
+
rawInput: args,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
// Step 2: request permission. Transport failures here become a failed
|
|
290
|
+
// tool call so the agent loop can continue.
|
|
291
|
+
let permissionResponse;
|
|
292
|
+
try {
|
|
293
|
+
permissionResponse = await this.connection.requestPermission({
|
|
294
|
+
sessionId: this.sessionId,
|
|
295
|
+
toolCall: {
|
|
296
|
+
toolCallId,
|
|
297
|
+
title: `Run command: ${command}`,
|
|
298
|
+
kind: "execute",
|
|
299
|
+
status: "pending",
|
|
300
|
+
locations: [],
|
|
301
|
+
rawInput: args,
|
|
302
|
+
},
|
|
303
|
+
options: [
|
|
304
|
+
{ kind: "allow_once", name: "Run command", optionId: "allow" },
|
|
305
|
+
{ kind: "reject_once", name: "Skip command", optionId: "reject" },
|
|
306
|
+
],
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
311
|
+
await this.markFailed(toolCallId, message);
|
|
312
|
+
return { content: `Error requesting permission: ${message}` };
|
|
313
|
+
}
|
|
314
|
+
if (permissionResponse.outcome.outcome === "cancelled") {
|
|
315
|
+
await this.markFailed(toolCallId, "Cancelled by user.");
|
|
316
|
+
return { content: "Command cancelled by user." };
|
|
317
|
+
}
|
|
318
|
+
if (permissionResponse.outcome.outcome === "selected" &&
|
|
319
|
+
permissionResponse.outcome.optionId === "reject") {
|
|
320
|
+
await this.markFailed(toolCallId, "Rejected by user.");
|
|
321
|
+
return { content: "Command rejected by user." };
|
|
322
|
+
}
|
|
323
|
+
return this.runTerminalCommand(toolCallId, command);
|
|
324
|
+
}
|
|
325
|
+
async runTerminalCommand(toolCallId, command) {
|
|
326
|
+
await this.connection.sessionUpdate({
|
|
327
|
+
sessionId: this.sessionId,
|
|
328
|
+
update: {
|
|
329
|
+
sessionUpdate: "tool_call_update",
|
|
330
|
+
toolCallId,
|
|
331
|
+
status: "in_progress",
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
let terminal;
|
|
335
|
+
try {
|
|
336
|
+
// Run via `sh -c` so the command string is interpreted by a real shell –
|
|
337
|
+
// this preserves quoting, pipes, redirects and environment expansion.
|
|
338
|
+
terminal = await this.connection.createTerminal({
|
|
339
|
+
sessionId: this.sessionId,
|
|
340
|
+
command: "sh",
|
|
341
|
+
args: ["-c", command],
|
|
342
|
+
});
|
|
343
|
+
await terminal.waitForExit();
|
|
344
|
+
const outputResponse = await terminal.currentOutput();
|
|
345
|
+
const output = outputResponse.output;
|
|
346
|
+
await this.connection.sessionUpdate({
|
|
347
|
+
sessionId: this.sessionId,
|
|
348
|
+
update: {
|
|
349
|
+
sessionUpdate: "tool_call_update",
|
|
350
|
+
toolCallId,
|
|
351
|
+
status: "completed",
|
|
352
|
+
content: [{ type: "terminal", terminalId: terminal.id }],
|
|
353
|
+
rawOutput: { output },
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
return { content: output };
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
360
|
+
await this.markFailed(toolCallId, message);
|
|
361
|
+
return { content: `Error running command: ${message}` };
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
try {
|
|
365
|
+
await terminal?.release();
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// ignore release errors
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async webSearch(toolCallId, args) {
|
|
373
|
+
const query = String(args["query"] ?? "").trim();
|
|
374
|
+
const count = typeof args["count"] === "number" ? args["count"] : undefined;
|
|
375
|
+
if (!query) {
|
|
376
|
+
return this.failAndReturn(toolCallId, "web_search", args, "Error: `query` is required.");
|
|
377
|
+
}
|
|
378
|
+
await this.connection.sessionUpdate({
|
|
379
|
+
sessionId: this.sessionId,
|
|
380
|
+
update: {
|
|
381
|
+
sessionUpdate: "tool_call",
|
|
382
|
+
toolCallId,
|
|
383
|
+
title: `Web search: ${query}`,
|
|
384
|
+
kind: "fetch",
|
|
385
|
+
status: "in_progress",
|
|
386
|
+
locations: [],
|
|
387
|
+
rawInput: args,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
const apiKey = requireResolvedApiKey();
|
|
392
|
+
const toolArgs = { query };
|
|
393
|
+
if (count !== undefined)
|
|
394
|
+
toolArgs["count"] = count;
|
|
395
|
+
const mcpResult = await callZaiMcpTool({
|
|
396
|
+
endpoint: ZAI_WEB_SEARCH_MCP_ENDPOINT,
|
|
397
|
+
toolName: "webSearchPrime",
|
|
398
|
+
arguments: toolArgs,
|
|
399
|
+
apiKey,
|
|
400
|
+
signal: this.signal,
|
|
401
|
+
});
|
|
402
|
+
const { output, resultCount } = formatSearchOutput(mcpResult);
|
|
403
|
+
await this.connection.sessionUpdate({
|
|
404
|
+
sessionId: this.sessionId,
|
|
405
|
+
update: {
|
|
406
|
+
sessionUpdate: "tool_call_update",
|
|
407
|
+
toolCallId,
|
|
408
|
+
status: "completed",
|
|
409
|
+
content: [{ type: "content", content: { type: "text", text: output } }],
|
|
410
|
+
rawOutput: { resultCount },
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
return { content: output };
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
417
|
+
await this.markFailed(toolCallId, message);
|
|
418
|
+
return { content: `Error performing web search: ${message}` };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async webReader(toolCallId, args) {
|
|
422
|
+
const url = String(args["url"] ?? "").trim();
|
|
423
|
+
const returnFormat = String(args["return_format"] ?? "markdown");
|
|
424
|
+
if (!url) {
|
|
425
|
+
return this.failAndReturn(toolCallId, "web_reader", args, "Error: `url` is required.");
|
|
426
|
+
}
|
|
427
|
+
await this.connection.sessionUpdate({
|
|
428
|
+
sessionId: this.sessionId,
|
|
429
|
+
update: {
|
|
430
|
+
sessionUpdate: "tool_call",
|
|
431
|
+
toolCallId,
|
|
432
|
+
title: `Read URL: ${url}`,
|
|
433
|
+
kind: "fetch",
|
|
434
|
+
status: "in_progress",
|
|
435
|
+
locations: [{ path: url }],
|
|
436
|
+
rawInput: args,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
try {
|
|
440
|
+
const apiKey = requireResolvedApiKey();
|
|
441
|
+
const mcpResult = await callZaiMcpTool({
|
|
442
|
+
endpoint: ZAI_WEB_READER_MCP_ENDPOINT,
|
|
443
|
+
toolName: "webReader",
|
|
444
|
+
arguments: { url, return_format: returnFormat },
|
|
445
|
+
apiKey,
|
|
446
|
+
signal: this.signal,
|
|
447
|
+
});
|
|
448
|
+
const { output, title, resultUrl } = formatReaderOutput(mcpResult);
|
|
449
|
+
await this.connection.sessionUpdate({
|
|
450
|
+
sessionId: this.sessionId,
|
|
451
|
+
update: {
|
|
452
|
+
sessionUpdate: "tool_call_update",
|
|
453
|
+
toolCallId,
|
|
454
|
+
status: "completed",
|
|
455
|
+
content: [{ type: "content", content: { type: "text", text: output } }],
|
|
456
|
+
rawOutput: { title, url: resultUrl },
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
return { content: output };
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
463
|
+
await this.markFailed(toolCallId, message);
|
|
464
|
+
return { content: `Error reading URL: ${message}` };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async imageAnalysis(toolCallId, args) {
|
|
468
|
+
const imageSource = String(args["image_source"] ?? "").trim();
|
|
469
|
+
const prompt = typeof args["prompt"] === "string" ? args["prompt"] : undefined;
|
|
470
|
+
if (!imageSource) {
|
|
471
|
+
return this.failAndReturn(toolCallId, "image_analysis", args, "Error: `image_source` is required.");
|
|
472
|
+
}
|
|
473
|
+
if (!this.visionClient) {
|
|
474
|
+
return this.failAndReturn(toolCallId, "image_analysis", args, "Error: vision is not configured on this agent process. Vision MCP requires `npx` and the Z.AI Coding Plan.");
|
|
475
|
+
}
|
|
476
|
+
await this.connection.sessionUpdate({
|
|
477
|
+
sessionId: this.sessionId,
|
|
478
|
+
update: {
|
|
479
|
+
sessionUpdate: "tool_call",
|
|
480
|
+
toolCallId,
|
|
481
|
+
title: `Analyze image: ${imageSource}`,
|
|
482
|
+
kind: "fetch",
|
|
483
|
+
status: "in_progress",
|
|
484
|
+
locations: [{ path: imageSource }],
|
|
485
|
+
rawInput: args,
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
try {
|
|
489
|
+
const visionArgs = { image_source: imageSource };
|
|
490
|
+
if (prompt)
|
|
491
|
+
visionArgs["prompt"] = prompt;
|
|
492
|
+
const mcpResult = await this.visionClient.callTool("image_analysis", visionArgs, this.signal);
|
|
493
|
+
const text = unwrapVisionText(mcpResult);
|
|
494
|
+
await this.connection.sessionUpdate({
|
|
495
|
+
sessionId: this.sessionId,
|
|
496
|
+
update: {
|
|
497
|
+
sessionUpdate: "tool_call_update",
|
|
498
|
+
toolCallId,
|
|
499
|
+
status: "completed",
|
|
500
|
+
content: [{ type: "content", content: { type: "text", text } }],
|
|
501
|
+
rawOutput: { text },
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
return { content: text };
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
508
|
+
await this.markFailed(toolCallId, message);
|
|
509
|
+
return { content: `Error analyzing image: ${message}` };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Notification helpers
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
/** Mark an in-progress tool call as failed. */
|
|
516
|
+
async markFailed(toolCallId, message) {
|
|
517
|
+
await this.connection.sessionUpdate({
|
|
518
|
+
sessionId: this.sessionId,
|
|
519
|
+
update: {
|
|
520
|
+
sessionUpdate: "tool_call_update",
|
|
521
|
+
toolCallId,
|
|
522
|
+
status: "failed",
|
|
523
|
+
rawOutput: { error: message },
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Emit a brand new failed tool_call (for situations where we never made it
|
|
529
|
+
* to in_progress, e.g. invalid arguments / missing capabilities).
|
|
530
|
+
*/
|
|
531
|
+
async failedToolCall(toolCallId, toolName, rawInput, message) {
|
|
532
|
+
await this.connection.sessionUpdate({
|
|
533
|
+
sessionId: this.sessionId,
|
|
534
|
+
update: {
|
|
535
|
+
sessionUpdate: "tool_call",
|
|
536
|
+
toolCallId,
|
|
537
|
+
title: toolName,
|
|
538
|
+
kind: "other",
|
|
539
|
+
status: "failed",
|
|
540
|
+
locations: [],
|
|
541
|
+
rawInput,
|
|
542
|
+
rawOutput: { error: message },
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async failAndReturn(toolCallId, toolName, args, message) {
|
|
547
|
+
await this.failedToolCall(toolCallId, toolName, args, message);
|
|
548
|
+
return { content: message };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/** Resolve the API key from env or stored credentials, throwing a clear error if missing. */
|
|
552
|
+
function requireResolvedApiKey() {
|
|
553
|
+
const apiKey = resolveApiKey();
|
|
554
|
+
if (!apiKey) {
|
|
555
|
+
throw new Error("No API key found. Set Z_AI_API_KEY, or run `glm-acp-agent --setup` to store one.");
|
|
556
|
+
}
|
|
557
|
+
return apiKey;
|
|
558
|
+
}
|
|
559
|
+
function formatSearchOutput(mcpResult) {
|
|
560
|
+
const payload = unwrapMcpPayload(mcpResult);
|
|
561
|
+
const results = isRecord(payload) && Array.isArray(payload["search_result"])
|
|
562
|
+
? payload["search_result"]
|
|
563
|
+
: [];
|
|
564
|
+
if (results.length === 0) {
|
|
565
|
+
return {
|
|
566
|
+
output: typeof payload === "string" && payload.length > 0 ? payload : "No results found.",
|
|
567
|
+
resultCount: 0,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const output = results
|
|
571
|
+
.map((raw, i) => {
|
|
572
|
+
const r = isRecord(raw) ? raw : {};
|
|
573
|
+
const lines = [`[${i + 1}] ${stringValue(r["title"]) ?? "(no title)"}`];
|
|
574
|
+
const link = stringValue(r["link"]);
|
|
575
|
+
const media = stringValue(r["media"]);
|
|
576
|
+
const publishDate = stringValue(r["publish_date"]);
|
|
577
|
+
const content = stringValue(r["content"]);
|
|
578
|
+
if (link)
|
|
579
|
+
lines.push(`URL: ${link}`);
|
|
580
|
+
if (media)
|
|
581
|
+
lines.push(`Source: ${media}`);
|
|
582
|
+
if (publishDate)
|
|
583
|
+
lines.push(`Date: ${publishDate}`);
|
|
584
|
+
if (content)
|
|
585
|
+
lines.push(`Summary: ${content}`);
|
|
586
|
+
return lines.join("\n");
|
|
587
|
+
})
|
|
588
|
+
.join("\n\n");
|
|
589
|
+
return { output, resultCount: results.length };
|
|
590
|
+
}
|
|
591
|
+
function formatReaderOutput(mcpResult) {
|
|
592
|
+
const payload = unwrapMcpPayload(mcpResult);
|
|
593
|
+
const result = isRecord(payload) && isRecord(payload["reader_result"])
|
|
594
|
+
? payload["reader_result"]
|
|
595
|
+
: undefined;
|
|
596
|
+
if (!result) {
|
|
597
|
+
return {
|
|
598
|
+
output: typeof payload === "string" && payload.length > 0 ? payload : "No content returned.",
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const title = stringValue(result["title"]);
|
|
602
|
+
const resultUrl = stringValue(result["url"]);
|
|
603
|
+
const description = stringValue(result["description"]);
|
|
604
|
+
const content = stringValue(result["content"]);
|
|
605
|
+
const lines = [];
|
|
606
|
+
if (title)
|
|
607
|
+
lines.push(`# ${title}`);
|
|
608
|
+
if (resultUrl)
|
|
609
|
+
lines.push(`URL: ${resultUrl}`);
|
|
610
|
+
if (description)
|
|
611
|
+
lines.push(`\n${description}`);
|
|
612
|
+
if (content)
|
|
613
|
+
lines.push(`\n${content}`);
|
|
614
|
+
return { output: lines.join("\n") || "No content returned.", title, resultUrl };
|
|
615
|
+
}
|
|
616
|
+
function unwrapMcpPayload(mcpResult) {
|
|
617
|
+
if (!isRecord(mcpResult))
|
|
618
|
+
return mcpResult;
|
|
619
|
+
const content = mcpResult["content"];
|
|
620
|
+
if (!Array.isArray(content))
|
|
621
|
+
return mcpResult;
|
|
622
|
+
const texts = content
|
|
623
|
+
.map((entry) => {
|
|
624
|
+
if (!isRecord(entry))
|
|
625
|
+
return undefined;
|
|
626
|
+
const text = entry["text"];
|
|
627
|
+
return typeof text === "string" ? text : undefined;
|
|
628
|
+
})
|
|
629
|
+
.filter((text) => typeof text === "string");
|
|
630
|
+
if (texts.length === 0)
|
|
631
|
+
return mcpResult;
|
|
632
|
+
if (texts.length === 1)
|
|
633
|
+
return parseJsonIfPossible(texts[0]);
|
|
634
|
+
return texts.join("\n");
|
|
635
|
+
}
|
|
636
|
+
function parseJsonIfPossible(text) {
|
|
637
|
+
try {
|
|
638
|
+
return JSON.parse(text);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return text;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function isRecord(value) {
|
|
645
|
+
return typeof value === "object" && value !== null;
|
|
646
|
+
}
|
|
647
|
+
function stringValue(value) {
|
|
648
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Quote a string for safe inclusion in a `sh -c` command line.
|
|
652
|
+
* We use single quotes and escape any embedded single quotes via `'\''`.
|
|
653
|
+
*/
|
|
654
|
+
function shellQuote(s) {
|
|
655
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
656
|
+
}
|
|
657
|
+
function unwrapVisionText(mcpResult) {
|
|
658
|
+
if (!isRecord(mcpResult))
|
|
659
|
+
return typeof mcpResult === "string" ? mcpResult : "";
|
|
660
|
+
const content = mcpResult["content"];
|
|
661
|
+
if (Array.isArray(content)) {
|
|
662
|
+
const texts = content
|
|
663
|
+
.map((entry) => (isRecord(entry) && typeof entry["text"] === "string" ? entry["text"] : ""))
|
|
664
|
+
.filter((s) => s.length > 0);
|
|
665
|
+
if (texts.length > 0)
|
|
666
|
+
return texts.join("\n");
|
|
667
|
+
}
|
|
668
|
+
return JSON.stringify(mcpResult);
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=executor.js.map
|