@geminilight/mindos 0.6.29 → 0.6.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.
- package/README.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +116 -13
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/page.tsx +7 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/FileTree.tsx +21 -10
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +177 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +481 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/i18n/modules/knowledge.ts +4 -0
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +3 -1
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -8,8 +8,17 @@ import type {
|
|
|
8
8
|
AcpJsonRpcRequest,
|
|
9
9
|
AcpJsonRpcResponse,
|
|
10
10
|
AcpRegistryEntry,
|
|
11
|
-
AcpTransportType,
|
|
12
11
|
} from './types';
|
|
12
|
+
import { resolveAgentCommand } from './agent-descriptors';
|
|
13
|
+
import { readSettings } from '../settings';
|
|
14
|
+
|
|
15
|
+
/** Incoming JSON-RPC request from agent (bidirectional — agent asks US for permission). */
|
|
16
|
+
export interface AcpIncomingRequest {
|
|
17
|
+
jsonrpc: '2.0';
|
|
18
|
+
id: string | number;
|
|
19
|
+
method: string;
|
|
20
|
+
params?: Record<string, unknown>;
|
|
21
|
+
}
|
|
13
22
|
|
|
14
23
|
/* ── Types ─────────────────────────────────────────────────────────────── */
|
|
15
24
|
|
|
@@ -21,11 +30,13 @@ export interface AcpProcess {
|
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
type MessageCallback = (msg: AcpJsonRpcResponse) => void;
|
|
33
|
+
type RequestCallback = (req: AcpIncomingRequest) => void;
|
|
24
34
|
|
|
25
35
|
/* ── State ─────────────────────────────────────────────────────────────── */
|
|
26
36
|
|
|
27
37
|
const processes = new Map<string, AcpProcess>();
|
|
28
38
|
const messageListeners = new Map<string, Set<MessageCallback>>();
|
|
39
|
+
const requestListeners = new Map<string, Set<RequestCallback>>();
|
|
29
40
|
let rpcIdCounter = 1;
|
|
30
41
|
|
|
31
42
|
/* ── Public API ────────────────────────────────────────────────────────── */
|
|
@@ -35,13 +46,17 @@ let rpcIdCounter = 1;
|
|
|
35
46
|
*/
|
|
36
47
|
export function spawnAcpAgent(
|
|
37
48
|
entry: AcpRegistryEntry,
|
|
38
|
-
options?: { env?: Record<string, string
|
|
49
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
39
50
|
): AcpProcess {
|
|
40
|
-
const
|
|
51
|
+
const settings = readSettings();
|
|
52
|
+
const userOverride = settings.acpAgents?.[entry.id];
|
|
53
|
+
const resolved = resolveAgentCommand(entry.id, entry, userOverride);
|
|
54
|
+
const { cmd, args } = { cmd: resolved.cmd, args: resolved.args };
|
|
41
55
|
|
|
42
56
|
const mergedEnv = {
|
|
43
57
|
...process.env,
|
|
44
58
|
...(entry.env ?? {}),
|
|
59
|
+
...(resolved.env ?? {}),
|
|
45
60
|
...(options?.env ?? {}),
|
|
46
61
|
};
|
|
47
62
|
|
|
@@ -49,6 +64,7 @@ export function spawnAcpAgent(
|
|
|
49
64
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
50
65
|
env: mergedEnv,
|
|
51
66
|
shell: false,
|
|
67
|
+
...(options?.cwd ? { cwd: options.cwd } : {}),
|
|
52
68
|
});
|
|
53
69
|
|
|
54
70
|
const id = `acp-${entry.id}-${Date.now()}`;
|
|
@@ -56,6 +72,7 @@ export function spawnAcpAgent(
|
|
|
56
72
|
|
|
57
73
|
processes.set(id, acpProc);
|
|
58
74
|
messageListeners.set(id, new Set());
|
|
75
|
+
requestListeners.set(id, new Set());
|
|
59
76
|
|
|
60
77
|
// Parse newline-delimited JSON from stdout
|
|
61
78
|
let buffer = '';
|
|
@@ -68,10 +85,23 @@ export function spawnAcpAgent(
|
|
|
68
85
|
const trimmed = line.trim();
|
|
69
86
|
if (!trimmed) continue;
|
|
70
87
|
try {
|
|
71
|
-
const msg = JSON.parse(trimmed)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
const msg = JSON.parse(trimmed);
|
|
89
|
+
|
|
90
|
+
// Distinguish incoming requests (agent → client) from responses (to our requests).
|
|
91
|
+
// Requests have `method` and `id` but no `result`/`error`.
|
|
92
|
+
const isRequest = msg.method && msg.id !== undefined
|
|
93
|
+
&& !('result' in msg) && !('error' in msg);
|
|
94
|
+
|
|
95
|
+
if (isRequest) {
|
|
96
|
+
const reqListeners = requestListeners.get(id);
|
|
97
|
+
if (reqListeners) {
|
|
98
|
+
for (const cb of reqListeners) cb(msg as AcpIncomingRequest);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const listeners = messageListeners.get(id);
|
|
102
|
+
if (listeners) {
|
|
103
|
+
for (const cb of listeners) cb(msg as AcpJsonRpcResponse);
|
|
104
|
+
}
|
|
75
105
|
}
|
|
76
106
|
} catch {
|
|
77
107
|
// Not valid JSON — skip (could be agent debug output)
|
|
@@ -79,13 +109,26 @@ export function spawnAcpAgent(
|
|
|
79
109
|
}
|
|
80
110
|
});
|
|
81
111
|
|
|
82
|
-
|
|
112
|
+
// Capture stderr for debugging (agents may log startup errors there)
|
|
113
|
+
let stderrBuf = '';
|
|
114
|
+
proc.stderr?.on('data', (chunk: Buffer) => {
|
|
115
|
+
stderrBuf += chunk.toString();
|
|
116
|
+
// Keep only last 4KB to avoid unbounded memory
|
|
117
|
+
if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-4096);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
proc.on('close', (code) => {
|
|
83
121
|
acpProc.alive = false;
|
|
122
|
+
if (code && code !== 0 && stderrBuf.trim()) {
|
|
123
|
+
console.error(`[ACP] ${entry.id} exited with code ${code}: ${stderrBuf.trim().slice(0, 500)}`);
|
|
124
|
+
}
|
|
84
125
|
messageListeners.delete(id);
|
|
126
|
+
requestListeners.delete(id);
|
|
85
127
|
});
|
|
86
128
|
|
|
87
|
-
proc.on('error', () => {
|
|
129
|
+
proc.on('error', (err) => {
|
|
88
130
|
acpProc.alive = false;
|
|
131
|
+
console.error(`[ACP] ${entry.id} spawn error:`, err.message);
|
|
89
132
|
});
|
|
90
133
|
|
|
91
134
|
return acpProc;
|
|
@@ -166,6 +209,15 @@ export function killAgent(acpProc: AcpProcess): void {
|
|
|
166
209
|
acpProc.alive = false;
|
|
167
210
|
processes.delete(acpProc.id);
|
|
168
211
|
messageListeners.delete(acpProc.id);
|
|
212
|
+
requestListeners.delete(acpProc.id);
|
|
213
|
+
// Clean up any terminals spawned by this process
|
|
214
|
+
const terms = terminalMaps.get(acpProc.id);
|
|
215
|
+
if (terms) {
|
|
216
|
+
for (const entry of terms.values()) {
|
|
217
|
+
if (entry.child.exitCode === null) entry.child.kill('SIGTERM');
|
|
218
|
+
}
|
|
219
|
+
terminalMaps.delete(acpProc.id);
|
|
220
|
+
}
|
|
169
221
|
}
|
|
170
222
|
|
|
171
223
|
/**
|
|
@@ -191,19 +243,253 @@ export function killAllAgents(): void {
|
|
|
191
243
|
}
|
|
192
244
|
}
|
|
193
245
|
|
|
194
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Register a callback for incoming JSON-RPC REQUESTS from the agent
|
|
248
|
+
* (bidirectional: agent asks client for permission / capability).
|
|
249
|
+
* Returns an unsubscribe function.
|
|
250
|
+
*/
|
|
251
|
+
export function onRequest(acpProc: AcpProcess, callback: RequestCallback): () => void {
|
|
252
|
+
const listeners = requestListeners.get(acpProc.id);
|
|
253
|
+
if (!listeners) throw new Error(`ACP process ${acpProc.id} not found`);
|
|
254
|
+
|
|
255
|
+
listeners.add(callback);
|
|
256
|
+
return () => { listeners.delete(callback); };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Send a raw JSON-RPC response back to the agent's stdin.
|
|
261
|
+
* Used for replying to incoming requests (e.g. permission approvals).
|
|
262
|
+
*/
|
|
263
|
+
export function sendResponse(
|
|
264
|
+
acpProc: AcpProcess,
|
|
265
|
+
id: string | number,
|
|
266
|
+
result: unknown,
|
|
267
|
+
): void {
|
|
268
|
+
if (!acpProc.alive || !acpProc.proc.stdin?.writable) {
|
|
269
|
+
throw new Error(`ACP process ${acpProc.id} is not alive`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const response: AcpJsonRpcResponse = {
|
|
273
|
+
jsonrpc: '2.0',
|
|
274
|
+
id,
|
|
275
|
+
result,
|
|
276
|
+
};
|
|
277
|
+
acpProc.proc.stdin.write(JSON.stringify(response) + '\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Install auto-approval for all incoming permission/capability requests.
|
|
282
|
+
* Agents in ACP mode send requests like fs/read, fs/write, terminal/execute etc.
|
|
283
|
+
* Without approval, the agent hangs waiting for TTY input that never comes.
|
|
284
|
+
* Returns an unsubscribe function.
|
|
285
|
+
*/
|
|
286
|
+
export function installAutoApproval(acpProc: AcpProcess): () => void {
|
|
287
|
+
return onRequest(acpProc, (req) => {
|
|
288
|
+
const method = req.method;
|
|
289
|
+
const params = (req.params ?? {}) as Record<string, unknown>;
|
|
290
|
+
|
|
291
|
+
switch (method) {
|
|
292
|
+
// ── File system: read ──
|
|
293
|
+
case 'fs/read_text_file': {
|
|
294
|
+
const filePath = String(params.path ?? '');
|
|
295
|
+
if (!filePath) {
|
|
296
|
+
sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const fs = require('fs');
|
|
301
|
+
const line = typeof params.line === 'number' ? params.line : undefined;
|
|
302
|
+
const limit = typeof params.limit === 'number' ? params.limit : undefined;
|
|
303
|
+
let content = fs.readFileSync(filePath, 'utf-8') as string;
|
|
304
|
+
if (line !== undefined || limit !== undefined) {
|
|
305
|
+
const lines = content.split('\n');
|
|
306
|
+
const start = (line ?? 1) - 1; // 1-based to 0-based
|
|
307
|
+
const end = limit !== undefined ? start + limit : lines.length;
|
|
308
|
+
content = lines.slice(Math.max(0, start), end).join('\n');
|
|
309
|
+
}
|
|
310
|
+
sendResponse(acpProc, req.id, { content });
|
|
311
|
+
} catch (err) {
|
|
312
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: (err as Error).message } });
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── File system: write ──
|
|
318
|
+
case 'fs/write_text_file': {
|
|
319
|
+
const filePath = String(params.path ?? '');
|
|
320
|
+
const content = String(params.content ?? '');
|
|
321
|
+
if (!filePath) {
|
|
322
|
+
sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const fs = require('fs');
|
|
327
|
+
const path = require('path');
|
|
328
|
+
const dir = path.dirname(filePath);
|
|
329
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
330
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
331
|
+
sendResponse(acpProc, req.id, {});
|
|
332
|
+
} catch (err) {
|
|
333
|
+
sendResponse(acpProc, req.id, { error: { code: -32603, message: (err as Error).message } });
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── Terminal: create ──
|
|
339
|
+
case 'terminal/create': {
|
|
340
|
+
const command = String(params.command ?? '');
|
|
341
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
342
|
+
const cwd = typeof params.cwd === 'string' ? params.cwd : undefined;
|
|
343
|
+
const env = (params.env && typeof params.env === 'object') ? params.env as Record<string, string> : undefined;
|
|
344
|
+
const outputByteLimit = typeof params.outputByteLimit === 'number' ? params.outputByteLimit : 1_000_000;
|
|
345
|
+
|
|
346
|
+
if (!command) {
|
|
347
|
+
sendResponse(acpProc, req.id, { error: { code: -32602, message: 'command is required' } });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
352
|
+
try {
|
|
353
|
+
const { spawn: spawnChild } = require('child_process');
|
|
354
|
+
const child = spawnChild(command, args, {
|
|
355
|
+
cwd,
|
|
356
|
+
env: { ...process.env, ...(env ?? {}) },
|
|
357
|
+
shell: true,
|
|
358
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
let output = '';
|
|
362
|
+
let truncated = false;
|
|
363
|
+
|
|
364
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
365
|
+
if (output.length < outputByteLimit) {
|
|
366
|
+
output += chunk.toString();
|
|
367
|
+
if (output.length > outputByteLimit) {
|
|
368
|
+
output = output.slice(0, outputByteLimit);
|
|
369
|
+
truncated = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
374
|
+
if (output.length < outputByteLimit) {
|
|
375
|
+
output += chunk.toString();
|
|
376
|
+
if (output.length > outputByteLimit) {
|
|
377
|
+
output = output.slice(0, outputByteLimit);
|
|
378
|
+
truncated = true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Store terminal in process-scoped map
|
|
384
|
+
const terminalMap = getOrCreateTerminalMap(acpProc.id);
|
|
385
|
+
terminalMap.set(terminalId, { child, output: () => output, truncated: () => truncated });
|
|
386
|
+
|
|
387
|
+
sendResponse(acpProc, req.id, { terminalId });
|
|
388
|
+
} catch (err) {
|
|
389
|
+
sendResponse(acpProc, req.id, { error: { code: -32603, message: (err as Error).message } });
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Terminal: output ──
|
|
395
|
+
case 'terminal/output': {
|
|
396
|
+
const terminalId = String(params.terminalId ?? '');
|
|
397
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
398
|
+
if (!terminal) {
|
|
399
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const exitStatus = terminal.child.exitCode !== null ? { exitCode: terminal.child.exitCode } : undefined;
|
|
403
|
+
sendResponse(acpProc, req.id, { output: terminal.output(), truncated: terminal.truncated(), exitStatus });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Terminal: kill ──
|
|
408
|
+
case 'terminal/kill': {
|
|
409
|
+
const terminalId = String(params.terminalId ?? '');
|
|
410
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
411
|
+
if (!terminal) {
|
|
412
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
terminal.child.kill('SIGTERM');
|
|
416
|
+
sendResponse(acpProc, req.id, {});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Terminal: wait_for_exit ──
|
|
421
|
+
case 'terminal/wait_for_exit': {
|
|
422
|
+
const terminalId = String(params.terminalId ?? '');
|
|
423
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
424
|
+
if (!terminal) {
|
|
425
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (terminal.child.exitCode !== null) {
|
|
429
|
+
sendResponse(acpProc, req.id, { exitCode: terminal.child.exitCode, signal: terminal.child.signalCode });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
terminal.child.on('exit', (code: number | null, signal: string | null) => {
|
|
433
|
+
sendResponse(acpProc, req.id, { exitCode: code, signal });
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── Terminal: release ──
|
|
439
|
+
case 'terminal/release': {
|
|
440
|
+
const terminalId = String(params.terminalId ?? '');
|
|
441
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
442
|
+
if (!terminal) {
|
|
443
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (terminal.child.exitCode === null) terminal.child.kill('SIGTERM');
|
|
447
|
+
removeTerminal(acpProc.id, terminalId);
|
|
448
|
+
sendResponse(acpProc, req.id, {});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Permission requests (auto-approve all) ──
|
|
453
|
+
case 'session/request_permission': {
|
|
454
|
+
console.log(`[ACP] Auto-approving permission: ${JSON.stringify(params.toolCall ?? {}).slice(0, 200)}`);
|
|
455
|
+
sendResponse(acpProc, req.id, { outcome: { selected: { optionId: 'allow_once' } } });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Unknown methods: auto-approve for backwards compat ──
|
|
460
|
+
default: {
|
|
461
|
+
console.log(`[ACP] Auto-approving unknown agent request: ${method} (id=${req.id})`);
|
|
462
|
+
sendResponse(acpProc, req.id, {});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ── Terminal management (per ACP process) ─────────────────────────────── */
|
|
469
|
+
|
|
470
|
+
interface TerminalEntry {
|
|
471
|
+
child: import('child_process').ChildProcess;
|
|
472
|
+
output: () => string;
|
|
473
|
+
truncated: () => boolean;
|
|
474
|
+
}
|
|
195
475
|
|
|
196
|
-
|
|
197
|
-
const transport: AcpTransportType = entry.transport;
|
|
476
|
+
const terminalMaps = new Map<string, Map<string, TerminalEntry>>();
|
|
198
477
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
case 'binary':
|
|
205
|
-
case 'stdio':
|
|
206
|
-
default:
|
|
207
|
-
return { cmd: entry.command, args: entry.args ?? [] };
|
|
478
|
+
function getOrCreateTerminalMap(procId: string): Map<string, TerminalEntry> {
|
|
479
|
+
let map = terminalMaps.get(procId);
|
|
480
|
+
if (!map) {
|
|
481
|
+
map = new Map();
|
|
482
|
+
terminalMaps.set(procId, map);
|
|
208
483
|
}
|
|
484
|
+
return map;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getTerminal(procId: string, terminalId: string): TerminalEntry | undefined {
|
|
488
|
+
return terminalMaps.get(procId)?.get(terminalId);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function removeTerminal(procId: string, terminalId: string): void {
|
|
492
|
+
terminalMaps.get(procId)?.delete(terminalId);
|
|
209
493
|
}
|
|
494
|
+
|
|
495
|
+
/* ── Internal — agent command resolution moved to agent-descriptors.ts ─ */
|
package/app/lib/acp/types.ts
CHANGED
|
@@ -10,14 +10,63 @@
|
|
|
10
10
|
/** How an ACP agent is spawned */
|
|
11
11
|
export type AcpTransportType = 'stdio' | 'npx' | 'uvx' | 'binary';
|
|
12
12
|
|
|
13
|
+
/* ── ContentBlock (ACP prompt format) ─────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
export type AcpContentBlock =
|
|
16
|
+
| { type: 'text'; text: string }
|
|
17
|
+
| { type: 'image'; data: string; mimeType: string }
|
|
18
|
+
| { type: 'audio'; data: string; mimeType: string }
|
|
19
|
+
| { type: 'resource_link'; uri: string; name: string }
|
|
20
|
+
| { type: 'resource'; resource: { uri: string; text?: string; blob?: string } };
|
|
21
|
+
|
|
22
|
+
/* ── StopReason ───────────────────────────────────────────────────────── */
|
|
23
|
+
|
|
24
|
+
export type AcpStopReason = 'end_turn' | 'max_tokens' | 'max_turn_requests' | 'refusal' | 'cancelled';
|
|
25
|
+
|
|
26
|
+
/* ── Modes & Config ───────────────────────────────────────────────────── */
|
|
27
|
+
|
|
28
|
+
export interface AcpMode {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AcpConfigOptionEntry {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AcpConfigOption {
|
|
40
|
+
type: 'select';
|
|
41
|
+
configId: string;
|
|
42
|
+
category: 'mode' | 'model' | 'thought_level' | 'other' | string;
|
|
43
|
+
label?: string;
|
|
44
|
+
currentValue: string;
|
|
45
|
+
options: AcpConfigOptionEntry[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── Auth ──────────────────────────────────────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
export interface AcpAuthMethod {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
/* ── Capabilities ─────────────────────────────────────────────────────── */
|
|
14
57
|
|
|
15
|
-
/** What
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
58
|
+
/** What the agent declares it supports (from initialize response). */
|
|
59
|
+
export interface AcpAgentCapabilities {
|
|
60
|
+
loadSession?: boolean;
|
|
61
|
+
mcpCapabilities?: { http?: boolean; sse?: boolean };
|
|
62
|
+
promptCapabilities?: { audio?: boolean; embeddedContext?: boolean; image?: boolean };
|
|
63
|
+
sessionCapabilities?: { list?: boolean };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What MindOS declares as a client (sent in initialize request). */
|
|
67
|
+
export interface AcpClientCapabilities {
|
|
68
|
+
fs?: { readTextFile?: boolean; writeTextFile?: boolean };
|
|
69
|
+
terminal?: boolean;
|
|
21
70
|
}
|
|
22
71
|
|
|
23
72
|
/* ── Session ──────────────────────────────────────────────────────────── */
|
|
@@ -28,8 +77,25 @@ export interface AcpSession {
|
|
|
28
77
|
id: string;
|
|
29
78
|
agentId: string;
|
|
30
79
|
state: AcpSessionState;
|
|
80
|
+
cwd?: string;
|
|
31
81
|
createdAt: string;
|
|
32
82
|
lastActivityAt: string;
|
|
83
|
+
/** Agent capabilities from initialize response */
|
|
84
|
+
agentCapabilities?: AcpAgentCapabilities;
|
|
85
|
+
/** Modes available from session/new or session/load response */
|
|
86
|
+
modes?: AcpMode[];
|
|
87
|
+
/** Config options from session/new or session/load response */
|
|
88
|
+
configOptions?: AcpConfigOption[];
|
|
89
|
+
/** Auth methods from initialize response */
|
|
90
|
+
authMethods?: AcpAuthMethod[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Lightweight session info returned by session/list. */
|
|
94
|
+
export interface AcpSessionInfo {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
title?: string;
|
|
97
|
+
cwd?: string;
|
|
98
|
+
updatedAt?: string;
|
|
33
99
|
}
|
|
34
100
|
|
|
35
101
|
/* ── JSON-RPC (ACP uses JSON-RPC 2.0 over stdio) ─────────────────────── */
|
|
@@ -58,32 +124,27 @@ export interface AcpJsonRpcError {
|
|
|
58
124
|
|
|
59
125
|
export interface AcpPromptRequest {
|
|
60
126
|
sessionId: string;
|
|
61
|
-
|
|
62
|
-
|
|
127
|
+
prompt: AcpContentBlock[];
|
|
128
|
+
context?: { cwd?: string };
|
|
129
|
+
stream?: boolean;
|
|
63
130
|
}
|
|
64
131
|
|
|
65
132
|
export interface AcpPromptResponse {
|
|
66
133
|
sessionId: string;
|
|
67
134
|
text: string;
|
|
68
135
|
done: boolean;
|
|
136
|
+
stopReason?: AcpStopReason;
|
|
69
137
|
toolCalls?: AcpToolCall[];
|
|
70
138
|
metadata?: Record<string, unknown>;
|
|
71
139
|
}
|
|
72
140
|
|
|
73
|
-
/* ──
|
|
74
|
-
|
|
75
|
-
export type AcpUpdateType = 'text' | 'tool_call' | 'tool_result' | 'done' | 'error';
|
|
141
|
+
/* ── ToolCall (full ACP model) ────────────────────────────────────────── */
|
|
76
142
|
|
|
77
|
-
export
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
text?: string;
|
|
81
|
-
toolCall?: AcpToolCall;
|
|
82
|
-
toolResult?: AcpToolResult;
|
|
83
|
-
error?: string;
|
|
84
|
-
}
|
|
143
|
+
export type AcpToolCallKind =
|
|
144
|
+
| 'read' | 'edit' | 'delete' | 'move' | 'search'
|
|
145
|
+
| 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
|
85
146
|
|
|
86
|
-
|
|
147
|
+
export type AcpToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
87
148
|
|
|
88
149
|
export interface AcpToolCall {
|
|
89
150
|
id: string;
|
|
@@ -91,12 +152,85 @@ export interface AcpToolCall {
|
|
|
91
152
|
arguments: Record<string, unknown>;
|
|
92
153
|
}
|
|
93
154
|
|
|
155
|
+
/** Full tool call with status, kind, and content — used in session updates. */
|
|
156
|
+
export interface AcpToolCallFull {
|
|
157
|
+
toolCallId: string;
|
|
158
|
+
title?: string;
|
|
159
|
+
kind?: AcpToolCallKind;
|
|
160
|
+
status: AcpToolCallStatus;
|
|
161
|
+
rawInput?: string;
|
|
162
|
+
rawOutput?: string;
|
|
163
|
+
content?: AcpContentBlock[];
|
|
164
|
+
locations?: { path: string; line?: number }[];
|
|
165
|
+
}
|
|
166
|
+
|
|
94
167
|
export interface AcpToolResult {
|
|
95
168
|
callId: string;
|
|
96
169
|
result: string;
|
|
97
170
|
isError?: boolean;
|
|
98
171
|
}
|
|
99
172
|
|
|
173
|
+
/* ── Plan ──────────────────────────────────────────────────────────────── */
|
|
174
|
+
|
|
175
|
+
export type AcpPlanEntryStatus = 'pending' | 'in_progress' | 'completed';
|
|
176
|
+
export type AcpPlanEntryPriority = 'high' | 'medium' | 'low';
|
|
177
|
+
|
|
178
|
+
export interface AcpPlanEntry {
|
|
179
|
+
content: string;
|
|
180
|
+
status: AcpPlanEntryStatus;
|
|
181
|
+
priority: AcpPlanEntryPriority;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface AcpPlan {
|
|
185
|
+
entries: AcpPlanEntry[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ── Session Updates (streaming) — Full ACP spec ──────────────────────── */
|
|
189
|
+
|
|
190
|
+
export type AcpUpdateType =
|
|
191
|
+
| 'user_message_chunk'
|
|
192
|
+
| 'agent_message_chunk'
|
|
193
|
+
| 'agent_thought_chunk'
|
|
194
|
+
| 'tool_call'
|
|
195
|
+
| 'tool_call_update'
|
|
196
|
+
| 'plan'
|
|
197
|
+
| 'available_commands_update'
|
|
198
|
+
| 'current_mode_update'
|
|
199
|
+
| 'config_option_update'
|
|
200
|
+
| 'session_info_update'
|
|
201
|
+
// Legacy compat (mapped internally)
|
|
202
|
+
| 'text'
|
|
203
|
+
| 'tool_result'
|
|
204
|
+
| 'done'
|
|
205
|
+
| 'error';
|
|
206
|
+
|
|
207
|
+
export interface AcpSessionUpdate {
|
|
208
|
+
sessionId: string;
|
|
209
|
+
type: AcpUpdateType;
|
|
210
|
+
/** Text content for message chunk types */
|
|
211
|
+
text?: string;
|
|
212
|
+
/** Structured tool call data */
|
|
213
|
+
toolCall?: AcpToolCallFull;
|
|
214
|
+
/** Tool result (legacy) */
|
|
215
|
+
toolResult?: AcpToolResult;
|
|
216
|
+
/** Plan entries */
|
|
217
|
+
plan?: AcpPlan;
|
|
218
|
+
/** Available commands (opaque to client) */
|
|
219
|
+
availableCommands?: unknown[];
|
|
220
|
+
/** Current mode ID */
|
|
221
|
+
currentModeId?: string;
|
|
222
|
+
/** Updated config options */
|
|
223
|
+
configOptions?: AcpConfigOption[];
|
|
224
|
+
/** Session info update */
|
|
225
|
+
sessionInfo?: { title?: string; updatedAt?: string };
|
|
226
|
+
/** Error message */
|
|
227
|
+
error?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ── Permission ───────────────────────────────────────────────────────── */
|
|
231
|
+
|
|
232
|
+
export type AcpPermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
|
|
233
|
+
|
|
100
234
|
/* ── Registry ─────────────────────────────────────────────────────────── */
|
|
101
235
|
|
|
102
236
|
/** An entry from the ACP registry (registry.json) */
|
|
@@ -107,6 +241,8 @@ export interface AcpRegistryEntry {
|
|
|
107
241
|
version?: string;
|
|
108
242
|
transport: AcpTransportType;
|
|
109
243
|
command: string;
|
|
244
|
+
/** npm package name for npx-based agents (e.g. "@google/gemini-cli") */
|
|
245
|
+
packageName?: string;
|
|
110
246
|
args?: string[];
|
|
111
247
|
env?: Record<string, string>;
|
|
112
248
|
tags?: string[];
|
|
@@ -128,6 +264,8 @@ export const ACP_ERRORS = {
|
|
|
128
264
|
AGENT_NOT_FOUND: { code: -32003, message: 'Agent not found in registry' },
|
|
129
265
|
SPAWN_FAILED: { code: -32004, message: 'Failed to spawn agent process' },
|
|
130
266
|
TRANSPORT_ERROR: { code: -32005, message: 'Transport error' },
|
|
267
|
+
AUTH_REQUIRED: { code: -32000, message: 'Authentication required' },
|
|
268
|
+
RESOURCE_NOT_FOUND: { code: -32002, message: 'Resource not found' },
|
|
131
269
|
PARSE_ERROR: { code: -32700, message: 'Parse error' },
|
|
132
270
|
INVALID_REQUEST: { code: -32600, message: 'Invalid request' },
|
|
133
271
|
METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
|
package/app/lib/agent/model.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { getModel as piGetModel, type Model } from '@mariozechner/pi-ai';
|
|
2
2
|
import { effectiveAiConfig } from '@/lib/settings';
|
|
3
3
|
|
|
4
|
+
/** Check if any message in the conversation contains images */
|
|
5
|
+
export function hasImages(messages: Array<{ images?: unknown[] }>): boolean {
|
|
6
|
+
return messages.some(m => m.images && m.images.length > 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Ensure model input includes 'image' when images are present */
|
|
10
|
+
function ensureVisionCapable(model: Model<any>): Model<any> {
|
|
11
|
+
const inputs = model.input as readonly string[];
|
|
12
|
+
if (inputs.includes('image')) return model;
|
|
13
|
+
// Upgrade input to include image — most modern models support it
|
|
14
|
+
return { ...model, input: [...inputs, 'image'] as any };
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
/**
|
|
5
18
|
* Build a pi-ai Model for the configured provider.
|
|
6
19
|
*
|
|
@@ -11,7 +24,7 @@ import { effectiveAiConfig } from '@/lib/settings';
|
|
|
11
24
|
*
|
|
12
25
|
* Returns { model, modelName, apiKey } — Agent needs model + apiKey via getApiKey hook.
|
|
13
26
|
*/
|
|
14
|
-
export function getModelConfig(): {
|
|
27
|
+
export function getModelConfig(options?: { hasImages?: boolean }): {
|
|
15
28
|
model: Model<any>;
|
|
16
29
|
modelName: string;
|
|
17
30
|
apiKey: string;
|
|
@@ -77,7 +90,8 @@ export function getModelConfig(): {
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
const finalModel = options?.hasImages ? ensureVisionCapable(model) : model;
|
|
94
|
+
return { model: finalModel, modelName, apiKey: cfg.openaiApiKey, provider: 'openai' };
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
// Anthropic
|
|
@@ -104,5 +118,6 @@ export function getModelConfig(): {
|
|
|
104
118
|
};
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
|
|
121
|
+
const finalModel = options?.hasImages ? ensureVisionCapable(model) : model;
|
|
122
|
+
return { model: finalModel, modelName, apiKey: cfg.anthropicApiKey, provider: 'anthropic' };
|
|
108
123
|
}
|