@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
package/app/lib/acp/session.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ACP Session Manager — High-level session lifecycle for ACP agents.
|
|
3
3
|
* Manages session creation, prompt turns, cancellation, and cleanup.
|
|
4
|
+
* Implements full ACP spec: initialize → session/new → session/prompt → session/cancel → close.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type {
|
|
@@ -9,6 +10,13 @@ import type {
|
|
|
9
10
|
AcpSessionUpdate,
|
|
10
11
|
AcpPromptResponse,
|
|
11
12
|
AcpRegistryEntry,
|
|
13
|
+
AcpAgentCapabilities,
|
|
14
|
+
AcpMode,
|
|
15
|
+
AcpConfigOption,
|
|
16
|
+
AcpSessionInfo,
|
|
17
|
+
AcpStopReason,
|
|
18
|
+
AcpAuthMethod,
|
|
19
|
+
AcpContentBlock,
|
|
12
20
|
} from './types';
|
|
13
21
|
import {
|
|
14
22
|
spawnAcpAgent,
|
|
@@ -16,6 +24,7 @@ import {
|
|
|
16
24
|
sendMessage,
|
|
17
25
|
onMessage,
|
|
18
26
|
killAgent,
|
|
27
|
+
installAutoApproval,
|
|
19
28
|
type AcpProcess,
|
|
20
29
|
} from './subprocess';
|
|
21
30
|
import { findAcpAgent } from './registry';
|
|
@@ -24,15 +33,17 @@ import { findAcpAgent } from './registry';
|
|
|
24
33
|
|
|
25
34
|
const sessions = new Map<string, AcpSession>();
|
|
26
35
|
const sessionProcesses = new Map<string, AcpProcess>();
|
|
36
|
+
const autoApprovalCleanups = new Map<string, () => void>();
|
|
27
37
|
|
|
28
|
-
/* ── Public API
|
|
38
|
+
/* ── Public API — Session Lifecycle ───────────────────────────────────── */
|
|
29
39
|
|
|
30
40
|
/**
|
|
31
41
|
* Create a new ACP session by spawning an agent process.
|
|
42
|
+
* Full ACP lifecycle: spawn → initialize → authenticate (if needed) → session/new.
|
|
32
43
|
*/
|
|
33
44
|
export async function createSession(
|
|
34
45
|
agentId: string,
|
|
35
|
-
options?: { env?: Record<string, string
|
|
46
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
36
47
|
): Promise<AcpSession> {
|
|
37
48
|
const entry = await findAcpAgent(agentId);
|
|
38
49
|
if (!entry) {
|
|
@@ -47,37 +58,248 @@ export async function createSession(
|
|
|
47
58
|
*/
|
|
48
59
|
export async function createSessionFromEntry(
|
|
49
60
|
entry: AcpRegistryEntry,
|
|
50
|
-
options?: { env?: Record<string, string
|
|
61
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
51
62
|
): Promise<AcpSession> {
|
|
52
63
|
const proc = spawnAcpAgent(entry, options);
|
|
53
64
|
|
|
54
|
-
//
|
|
65
|
+
// Install auto-approval BEFORE initialize so any early permission requests
|
|
66
|
+
// from the agent don't cause a hang waiting for TTY input.
|
|
67
|
+
const unsubApproval = installAutoApproval(proc);
|
|
68
|
+
|
|
69
|
+
let agentCapabilities: AcpAgentCapabilities | undefined;
|
|
70
|
+
let authMethods: AcpAuthMethod[] | undefined;
|
|
71
|
+
|
|
72
|
+
// Phase 1: Initialize — negotiate protocol and capabilities
|
|
55
73
|
try {
|
|
56
|
-
const response = await sendAndWait(proc, '
|
|
74
|
+
const response = await sendAndWait(proc, 'initialize', {
|
|
75
|
+
protocolVersion: 1,
|
|
76
|
+
capabilities: {
|
|
77
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
78
|
+
terminal: true,
|
|
79
|
+
},
|
|
80
|
+
clientInfo: { name: 'mindos', version: '0.6.29' },
|
|
81
|
+
}, 30_000);
|
|
57
82
|
|
|
58
83
|
if (response.error) {
|
|
84
|
+
unsubApproval();
|
|
59
85
|
killAgent(proc);
|
|
60
|
-
throw new Error(`
|
|
86
|
+
throw new Error(`initialize failed: ${response.error.message}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse agent capabilities from response
|
|
90
|
+
const initResult = response.result as Record<string, unknown> | undefined;
|
|
91
|
+
if (initResult) {
|
|
92
|
+
agentCapabilities = parseAgentCapabilities(initResult.agentCapabilities);
|
|
93
|
+
authMethods = parseAuthMethods(initResult.authMethods);
|
|
61
94
|
}
|
|
62
95
|
} catch (err) {
|
|
96
|
+
unsubApproval();
|
|
63
97
|
killAgent(proc);
|
|
64
98
|
throw err;
|
|
65
99
|
}
|
|
66
100
|
|
|
101
|
+
// Phase 2: Authenticate (if agent declares auth methods)
|
|
102
|
+
if (authMethods && authMethods.length > 0) {
|
|
103
|
+
try {
|
|
104
|
+
const authResponse = await sendAndWait(proc, 'authenticate', {
|
|
105
|
+
methodId: authMethods[0].id,
|
|
106
|
+
}, 15_000);
|
|
107
|
+
|
|
108
|
+
if (authResponse.error) {
|
|
109
|
+
// Authentication failed — non-fatal, log and continue
|
|
110
|
+
console.warn(`ACP authenticate warning for ${entry.id}: ${authResponse.error.message}`);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Best-effort auth — agent may not require it
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Phase 3: session/new — create the conversation session
|
|
118
|
+
let modes: AcpMode[] | undefined;
|
|
119
|
+
let configOptions: AcpConfigOption[] | undefined;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const newResponse = await sendAndWait(proc, 'session/new', {
|
|
123
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
124
|
+
mcpServers: [],
|
|
125
|
+
}, 15_000);
|
|
126
|
+
|
|
127
|
+
if (newResponse.error) {
|
|
128
|
+
// Non-fatal: some agents may not support explicit session/new
|
|
129
|
+
console.warn(`ACP session/new warning for ${entry.id}: ${newResponse.error.message}`);
|
|
130
|
+
} else {
|
|
131
|
+
const newResult = newResponse.result as Record<string, unknown> | undefined;
|
|
132
|
+
if (newResult) {
|
|
133
|
+
modes = parseModes(newResult.modes);
|
|
134
|
+
configOptions = parseConfigOptions(newResult.configOptions);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Non-fatal: agent may not support explicit session/new (backwards compat)
|
|
139
|
+
}
|
|
140
|
+
|
|
67
141
|
const sessionId = `ses-${entry.id}-${Date.now()}`;
|
|
68
142
|
const session: AcpSession = {
|
|
69
143
|
id: sessionId,
|
|
70
144
|
agentId: entry.id,
|
|
71
145
|
state: 'idle',
|
|
146
|
+
cwd: options?.cwd,
|
|
72
147
|
createdAt: new Date().toISOString(),
|
|
73
148
|
lastActivityAt: new Date().toISOString(),
|
|
149
|
+
agentCapabilities,
|
|
150
|
+
modes,
|
|
151
|
+
configOptions,
|
|
152
|
+
authMethods,
|
|
74
153
|
};
|
|
75
154
|
|
|
76
155
|
sessions.set(sessionId, session);
|
|
77
156
|
sessionProcesses.set(sessionId, proc);
|
|
157
|
+
autoApprovalCleanups.set(sessionId, unsubApproval);
|
|
158
|
+
return session;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Load/resume an existing session on an agent.
|
|
163
|
+
* Requires agent to declare `loadSession` capability.
|
|
164
|
+
*/
|
|
165
|
+
export async function loadSession(
|
|
166
|
+
agentId: string,
|
|
167
|
+
existingSessionId: string,
|
|
168
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
169
|
+
): Promise<AcpSession> {
|
|
170
|
+
const entry = await findAcpAgent(agentId);
|
|
171
|
+
if (!entry) {
|
|
172
|
+
throw new Error(`ACP agent not found in registry: ${agentId}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const proc = spawnAcpAgent(entry, options);
|
|
176
|
+
const unsubApproval = installAutoApproval(proc);
|
|
177
|
+
|
|
178
|
+
let agentCapabilities: AcpAgentCapabilities | undefined;
|
|
179
|
+
|
|
180
|
+
// Initialize
|
|
181
|
+
try {
|
|
182
|
+
const initResponse = await sendAndWait(proc, 'initialize', {
|
|
183
|
+
protocolVersion: 1,
|
|
184
|
+
capabilities: {
|
|
185
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
186
|
+
terminal: true,
|
|
187
|
+
},
|
|
188
|
+
clientInfo: { name: 'mindos', version: '0.6.29' },
|
|
189
|
+
}, 30_000);
|
|
190
|
+
|
|
191
|
+
if (initResponse.error) {
|
|
192
|
+
unsubApproval();
|
|
193
|
+
killAgent(proc);
|
|
194
|
+
throw new Error(`initialize failed: ${initResponse.error.message}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const initResult = initResponse.result as Record<string, unknown> | undefined;
|
|
198
|
+
if (initResult) {
|
|
199
|
+
agentCapabilities = parseAgentCapabilities(initResult.agentCapabilities);
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
unsubApproval();
|
|
203
|
+
killAgent(proc);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if agent supports loadSession
|
|
208
|
+
if (!agentCapabilities?.loadSession) {
|
|
209
|
+
unsubApproval();
|
|
210
|
+
killAgent(proc);
|
|
211
|
+
throw new Error(`Agent ${agentId} does not support session/load (loadSession capability not declared)`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// session/load — resume the existing session
|
|
215
|
+
let modes: AcpMode[] | undefined;
|
|
216
|
+
let configOptions: AcpConfigOption[] | undefined;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const loadResponse = await sendAndWait(proc, 'session/load', {
|
|
220
|
+
sessionId: existingSessionId,
|
|
221
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
222
|
+
mcpServers: [],
|
|
223
|
+
}, 15_000);
|
|
224
|
+
|
|
225
|
+
if (loadResponse.error) {
|
|
226
|
+
unsubApproval();
|
|
227
|
+
killAgent(proc);
|
|
228
|
+
throw new Error(`session/load failed: ${loadResponse.error.message}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const loadResult = loadResponse.result as Record<string, unknown> | undefined;
|
|
232
|
+
if (loadResult) {
|
|
233
|
+
modes = parseModes(loadResult.modes);
|
|
234
|
+
configOptions = parseConfigOptions(loadResult.configOptions);
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
unsubApproval();
|
|
238
|
+
killAgent(proc);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Use the original sessionId since we're resuming
|
|
243
|
+
const session: AcpSession = {
|
|
244
|
+
id: existingSessionId,
|
|
245
|
+
agentId: entry.id,
|
|
246
|
+
state: 'idle',
|
|
247
|
+
cwd: options?.cwd,
|
|
248
|
+
createdAt: new Date().toISOString(),
|
|
249
|
+
lastActivityAt: new Date().toISOString(),
|
|
250
|
+
agentCapabilities,
|
|
251
|
+
modes,
|
|
252
|
+
configOptions,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
sessions.set(existingSessionId, session);
|
|
256
|
+
sessionProcesses.set(existingSessionId, proc);
|
|
257
|
+
autoApprovalCleanups.set(existingSessionId, unsubApproval);
|
|
78
258
|
return session;
|
|
79
259
|
}
|
|
80
260
|
|
|
261
|
+
/**
|
|
262
|
+
* List resumable sessions from the agent.
|
|
263
|
+
* Requires agent to declare `sessionCapabilities.list`.
|
|
264
|
+
*/
|
|
265
|
+
export async function listSessions(
|
|
266
|
+
sessionId: string,
|
|
267
|
+
options?: { cursor?: string; cwd?: string },
|
|
268
|
+
): Promise<{ sessions: AcpSessionInfo[]; nextCursor?: string }> {
|
|
269
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
270
|
+
|
|
271
|
+
if (!session.agentCapabilities?.sessionCapabilities?.list) {
|
|
272
|
+
throw new Error('Agent does not support session/list');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const response = await sendAndWait(proc, 'session/list', {
|
|
276
|
+
...(options?.cursor ? { cursor: options.cursor } : {}),
|
|
277
|
+
...(options?.cwd ? { cwd: options.cwd } : {}),
|
|
278
|
+
}, 10_000);
|
|
279
|
+
|
|
280
|
+
if (response.error) {
|
|
281
|
+
throw new Error(`session/list failed: ${response.error.message}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
285
|
+
const rawSessions = Array.isArray(result?.sessions) ? result.sessions : [];
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
sessions: rawSessions.map((s: unknown) => {
|
|
289
|
+
const obj = s as Record<string, unknown>;
|
|
290
|
+
return {
|
|
291
|
+
sessionId: String(obj.sessionId ?? ''),
|
|
292
|
+
title: typeof obj.title === 'string' ? obj.title : undefined,
|
|
293
|
+
cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined,
|
|
294
|
+
updatedAt: typeof obj.updatedAt === 'string' ? obj.updatedAt : undefined,
|
|
295
|
+
};
|
|
296
|
+
}),
|
|
297
|
+
nextCursor: typeof result?.nextCursor === 'string' ? result.nextCursor : undefined,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ── Public API — Prompt ──────────────────────────────────────────────── */
|
|
302
|
+
|
|
81
303
|
/**
|
|
82
304
|
* Send a prompt to an active session and collect the full response.
|
|
83
305
|
* For streaming, use promptStream() instead.
|
|
@@ -95,7 +317,11 @@ export async function prompt(
|
|
|
95
317
|
updateSessionState(session, 'active');
|
|
96
318
|
|
|
97
319
|
try {
|
|
98
|
-
const response = await sendAndWait(proc, 'session/prompt', {
|
|
320
|
+
const response = await sendAndWait(proc, 'session/prompt', {
|
|
321
|
+
sessionId,
|
|
322
|
+
prompt: [{ type: 'text', text }] satisfies AcpContentBlock[],
|
|
323
|
+
...(session.cwd ? { context: { cwd: session.cwd } } : {}),
|
|
324
|
+
}, 60_000);
|
|
99
325
|
|
|
100
326
|
if (response.error) {
|
|
101
327
|
updateSessionState(session, 'error');
|
|
@@ -108,6 +334,7 @@ export async function prompt(
|
|
|
108
334
|
sessionId,
|
|
109
335
|
text: String(result?.text ?? ''),
|
|
110
336
|
done: true,
|
|
337
|
+
stopReason: parseStopReason(result?.stopReason),
|
|
111
338
|
toolCalls: result?.toolCalls as AcpPromptResponse['toolCalls'],
|
|
112
339
|
metadata: result?.metadata as AcpPromptResponse['metadata'],
|
|
113
340
|
};
|
|
@@ -119,6 +346,7 @@ export async function prompt(
|
|
|
119
346
|
|
|
120
347
|
/**
|
|
121
348
|
* Send a prompt and receive streaming updates via callback.
|
|
349
|
+
* Handles all 10 ACP session/update types.
|
|
122
350
|
* Returns the final aggregated response.
|
|
123
351
|
*/
|
|
124
352
|
export async function promptStream(
|
|
@@ -136,47 +364,79 @@ export async function promptStream(
|
|
|
136
364
|
|
|
137
365
|
return new Promise((resolve, reject) => {
|
|
138
366
|
let aggregatedText = '';
|
|
367
|
+
let stopReason: AcpStopReason = 'end_turn';
|
|
139
368
|
|
|
140
369
|
const unsub = onMessage(proc, (msg) => {
|
|
141
|
-
// Handle streaming notifications (no id field, or notification pattern)
|
|
142
370
|
if (msg.result && typeof msg.result === 'object') {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
sessionId,
|
|
146
|
-
type: (update.type as AcpSessionUpdate['type']) ?? 'text',
|
|
147
|
-
text: update.text as string | undefined,
|
|
148
|
-
toolCall: update.toolCall as AcpSessionUpdate['toolCall'],
|
|
149
|
-
toolResult: update.toolResult as AcpSessionUpdate['toolResult'],
|
|
150
|
-
error: update.error as string | undefined,
|
|
151
|
-
};
|
|
371
|
+
const raw = msg.result as Record<string, unknown>;
|
|
372
|
+
const update = parseSessionUpdate(sessionId, raw);
|
|
152
373
|
|
|
153
|
-
onUpdate(
|
|
374
|
+
onUpdate(update);
|
|
154
375
|
|
|
155
|
-
|
|
156
|
-
|
|
376
|
+
// Aggregate text from message chunk types
|
|
377
|
+
if ((update.type === 'agent_message_chunk' || update.type === 'text') && update.text) {
|
|
378
|
+
aggregatedText += update.text;
|
|
157
379
|
}
|
|
158
380
|
|
|
159
|
-
|
|
381
|
+
// Handle terminal states
|
|
382
|
+
if (update.type === 'done') {
|
|
160
383
|
unsub();
|
|
384
|
+
if (raw.stopReason) {
|
|
385
|
+
stopReason = parseStopReason(raw.stopReason);
|
|
386
|
+
}
|
|
161
387
|
updateSessionState(session, 'idle');
|
|
162
388
|
resolve({
|
|
163
389
|
sessionId,
|
|
164
390
|
text: aggregatedText,
|
|
165
391
|
done: true,
|
|
392
|
+
stopReason,
|
|
166
393
|
});
|
|
167
394
|
}
|
|
168
395
|
|
|
169
|
-
if (
|
|
396
|
+
if (update.type === 'error') {
|
|
170
397
|
unsub();
|
|
171
398
|
updateSessionState(session, 'error');
|
|
172
|
-
reject(new Error(
|
|
399
|
+
reject(new Error(update.error ?? 'Unknown ACP error'));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Update session metadata from config/mode updates
|
|
403
|
+
if (update.type === 'config_option_update' && update.configOptions) {
|
|
404
|
+
session.configOptions = update.configOptions;
|
|
405
|
+
}
|
|
406
|
+
if (update.type === 'current_mode_update' && update.currentModeId) {
|
|
407
|
+
// Track current mode
|
|
408
|
+
session.lastActivityAt = new Date().toISOString();
|
|
173
409
|
}
|
|
174
410
|
}
|
|
175
411
|
});
|
|
176
412
|
|
|
177
|
-
//
|
|
413
|
+
// Guard against agent process dying unexpectedly (OOM, SIGKILL, etc.)
|
|
414
|
+
// Without this, the Promise would hang forever if the process exits
|
|
415
|
+
// without sending a done/error notification.
|
|
416
|
+
const onExit = () => {
|
|
417
|
+
unsub();
|
|
418
|
+
updateSessionState(session, 'error');
|
|
419
|
+
reject(new Error(`ACP agent process exited unexpectedly during prompt`));
|
|
420
|
+
};
|
|
421
|
+
proc.proc.once('exit', onExit);
|
|
422
|
+
|
|
423
|
+
// Clean up exit listener when Promise resolves/rejects normally
|
|
424
|
+
const cleanup = () => { proc.proc.removeListener('exit', onExit); };
|
|
425
|
+
// Wrap resolve/reject to include cleanup — but we already unsub in the message handler.
|
|
426
|
+
// The exit listener is a safety net; if done/error fires first, remove the exit listener.
|
|
427
|
+
const origResolve = resolve;
|
|
428
|
+
const origReject = reject;
|
|
429
|
+
resolve = ((val: AcpPromptResponse) => { cleanup(); origResolve(val); }) as typeof resolve;
|
|
430
|
+
reject = ((err: unknown) => { cleanup(); origReject(err); }) as typeof reject;
|
|
431
|
+
|
|
432
|
+
// Send the prompt with ContentBlock format
|
|
178
433
|
try {
|
|
179
|
-
sendMessage(proc, 'session/prompt', {
|
|
434
|
+
sendMessage(proc, 'session/prompt', {
|
|
435
|
+
sessionId,
|
|
436
|
+
prompt: [{ type: 'text', text }] satisfies AcpContentBlock[],
|
|
437
|
+
stream: true,
|
|
438
|
+
...(session.cwd ? { context: { cwd: session.cwd } } : {}),
|
|
439
|
+
});
|
|
180
440
|
} catch (err) {
|
|
181
441
|
unsub();
|
|
182
442
|
updateSessionState(session, 'error');
|
|
@@ -185,6 +445,8 @@ export async function promptStream(
|
|
|
185
445
|
});
|
|
186
446
|
}
|
|
187
447
|
|
|
448
|
+
/* ── Public API — Session Control ─────────────────────────────────────── */
|
|
449
|
+
|
|
188
450
|
/**
|
|
189
451
|
* Cancel the current prompt turn on a session.
|
|
190
452
|
*/
|
|
@@ -194,7 +456,7 @@ export async function cancelPrompt(sessionId: string): Promise<void> {
|
|
|
194
456
|
if (session.state !== 'active') return;
|
|
195
457
|
|
|
196
458
|
try {
|
|
197
|
-
await sendAndWait(proc, 'session/cancel', {},
|
|
459
|
+
await sendAndWait(proc, 'session/cancel', { sessionId }, 10_000);
|
|
198
460
|
} catch {
|
|
199
461
|
// Best-effort cancel — don't throw if the agent doesn't support it
|
|
200
462
|
}
|
|
@@ -202,6 +464,54 @@ export async function cancelPrompt(sessionId: string): Promise<void> {
|
|
|
202
464
|
updateSessionState(session, 'idle');
|
|
203
465
|
}
|
|
204
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Set the operating mode for a session.
|
|
469
|
+
*/
|
|
470
|
+
export async function setMode(sessionId: string, modeId: string): Promise<void> {
|
|
471
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
472
|
+
|
|
473
|
+
const response = await sendAndWait(proc, 'session/set_mode', {
|
|
474
|
+
sessionId,
|
|
475
|
+
modeId,
|
|
476
|
+
}, 10_000);
|
|
477
|
+
|
|
478
|
+
if (response.error) {
|
|
479
|
+
throw new Error(`session/set_mode failed: ${response.error.message}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
session.lastActivityAt = new Date().toISOString();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Set a configuration option for a session.
|
|
487
|
+
*/
|
|
488
|
+
export async function setConfigOption(
|
|
489
|
+
sessionId: string,
|
|
490
|
+
configId: string,
|
|
491
|
+
value: string,
|
|
492
|
+
): Promise<AcpConfigOption[]> {
|
|
493
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
494
|
+
|
|
495
|
+
const response = await sendAndWait(proc, 'session/set_config_option', {
|
|
496
|
+
sessionId,
|
|
497
|
+
configId,
|
|
498
|
+
value,
|
|
499
|
+
}, 10_000);
|
|
500
|
+
|
|
501
|
+
if (response.error) {
|
|
502
|
+
throw new Error(`session/set_config_option failed: ${response.error.message}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
506
|
+
const configOptions = parseConfigOptions(result?.configOptions);
|
|
507
|
+
if (configOptions) {
|
|
508
|
+
session.configOptions = configOptions;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
session.lastActivityAt = new Date().toISOString();
|
|
512
|
+
return session.configOptions ?? [];
|
|
513
|
+
}
|
|
514
|
+
|
|
205
515
|
/**
|
|
206
516
|
* Close a session and terminate the subprocess.
|
|
207
517
|
*/
|
|
@@ -210,7 +520,7 @@ export async function closeSession(sessionId: string): Promise<void> {
|
|
|
210
520
|
|
|
211
521
|
if (proc?.alive) {
|
|
212
522
|
try {
|
|
213
|
-
await sendAndWait(proc, 'session/close', {}, 5_000);
|
|
523
|
+
await sendAndWait(proc, 'session/close', { sessionId }, 5_000);
|
|
214
524
|
} catch {
|
|
215
525
|
// Best-effort close
|
|
216
526
|
}
|
|
@@ -219,8 +529,15 @@ export async function closeSession(sessionId: string): Promise<void> {
|
|
|
219
529
|
|
|
220
530
|
sessions.delete(sessionId);
|
|
221
531
|
sessionProcesses.delete(sessionId);
|
|
532
|
+
const cleanup = autoApprovalCleanups.get(sessionId);
|
|
533
|
+
if (cleanup) {
|
|
534
|
+
cleanup();
|
|
535
|
+
autoApprovalCleanups.delete(sessionId);
|
|
536
|
+
}
|
|
222
537
|
}
|
|
223
538
|
|
|
539
|
+
/* ── Public API — Queries ─────────────────────────────────────────────── */
|
|
540
|
+
|
|
224
541
|
/**
|
|
225
542
|
* Get a session by its ID.
|
|
226
543
|
*/
|
|
@@ -243,7 +560,7 @@ export async function closeAllSessions(): Promise<void> {
|
|
|
243
560
|
await Promise.allSettled(ids.map(id => closeSession(id)));
|
|
244
561
|
}
|
|
245
562
|
|
|
246
|
-
/* ── Internal
|
|
563
|
+
/* ── Internal — Session helpers ───────────────────────────────────────── */
|
|
247
564
|
|
|
248
565
|
function getSessionAndProc(sessionId: string): { session: AcpSession; proc: AcpProcess } {
|
|
249
566
|
const session = sessions.get(sessionId);
|
|
@@ -262,3 +579,139 @@ function updateSessionState(session: AcpSession, state: AcpSessionState): void {
|
|
|
262
579
|
session.state = state;
|
|
263
580
|
session.lastActivityAt = new Date().toISOString();
|
|
264
581
|
}
|
|
582
|
+
|
|
583
|
+
/* ── Internal — Parsers ───────────────────────────────────────────────── */
|
|
584
|
+
|
|
585
|
+
function parseStopReason(raw: unknown): AcpStopReason {
|
|
586
|
+
const valid: AcpStopReason[] = ['end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'];
|
|
587
|
+
return valid.includes(raw as AcpStopReason) ? (raw as AcpStopReason) : 'end_turn';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function parseAgentCapabilities(raw: unknown): AcpAgentCapabilities | undefined {
|
|
591
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
592
|
+
const obj = raw as Record<string, unknown>;
|
|
593
|
+
return {
|
|
594
|
+
loadSession: obj.loadSession === true,
|
|
595
|
+
mcpCapabilities: typeof obj.mcpCapabilities === 'object' ? obj.mcpCapabilities as AcpAgentCapabilities['mcpCapabilities'] : undefined,
|
|
596
|
+
promptCapabilities: typeof obj.promptCapabilities === 'object' ? obj.promptCapabilities as AcpAgentCapabilities['promptCapabilities'] : undefined,
|
|
597
|
+
sessionCapabilities: typeof obj.sessionCapabilities === 'object' ? obj.sessionCapabilities as AcpAgentCapabilities['sessionCapabilities'] : undefined,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function parseAuthMethods(raw: unknown): AcpAuthMethod[] | undefined {
|
|
602
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
603
|
+
return raw
|
|
604
|
+
.filter((m): m is Record<string, unknown> => !!m && typeof m === 'object')
|
|
605
|
+
.map(m => ({
|
|
606
|
+
id: String(m.id ?? ''),
|
|
607
|
+
name: String(m.name ?? ''),
|
|
608
|
+
description: typeof m.description === 'string' ? m.description : undefined,
|
|
609
|
+
}))
|
|
610
|
+
.filter(m => m.id && m.name);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function parseModes(raw: unknown): AcpMode[] | undefined {
|
|
614
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
615
|
+
return raw
|
|
616
|
+
.filter((m): m is Record<string, unknown> => !!m && typeof m === 'object')
|
|
617
|
+
.map(m => ({
|
|
618
|
+
id: String(m.id ?? ''),
|
|
619
|
+
name: String(m.name ?? ''),
|
|
620
|
+
description: typeof m.description === 'string' ? m.description : undefined,
|
|
621
|
+
}))
|
|
622
|
+
.filter(m => m.id && m.name);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function parseConfigOptions(raw: unknown): AcpConfigOption[] | undefined {
|
|
626
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
627
|
+
return raw
|
|
628
|
+
.filter((o): o is Record<string, unknown> => !!o && typeof o === 'object')
|
|
629
|
+
.map(o => ({
|
|
630
|
+
type: 'select' as const,
|
|
631
|
+
configId: String(o.configId ?? o.id ?? ''),
|
|
632
|
+
category: String(o.category ?? 'other'),
|
|
633
|
+
label: typeof o.label === 'string' ? o.label : undefined,
|
|
634
|
+
currentValue: String(o.currentValue ?? ''),
|
|
635
|
+
options: Array.isArray(o.options) ? o.options.map((opt: unknown) => {
|
|
636
|
+
const optObj = opt as Record<string, unknown>;
|
|
637
|
+
return { id: String(optObj.id ?? ''), label: String(optObj.label ?? '') };
|
|
638
|
+
}) : [],
|
|
639
|
+
}))
|
|
640
|
+
.filter(o => o.configId);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Parse a raw session/update notification into a typed AcpSessionUpdate. */
|
|
644
|
+
function parseSessionUpdate(sessionId: string, raw: Record<string, unknown>): AcpSessionUpdate {
|
|
645
|
+
const type = raw.type as AcpSessionUpdate['type'] ?? 'text';
|
|
646
|
+
|
|
647
|
+
const base: AcpSessionUpdate = { sessionId, type };
|
|
648
|
+
|
|
649
|
+
switch (type) {
|
|
650
|
+
case 'agent_message_chunk':
|
|
651
|
+
case 'user_message_chunk':
|
|
652
|
+
case 'agent_thought_chunk':
|
|
653
|
+
case 'text':
|
|
654
|
+
base.text = typeof raw.text === 'string' ? raw.text
|
|
655
|
+
: typeof raw.content === 'string' ? raw.content
|
|
656
|
+
: undefined;
|
|
657
|
+
break;
|
|
658
|
+
|
|
659
|
+
case 'tool_call':
|
|
660
|
+
case 'tool_call_update':
|
|
661
|
+
if (raw.toolCall && typeof raw.toolCall === 'object') {
|
|
662
|
+
base.toolCall = raw.toolCall as AcpSessionUpdate['toolCall'];
|
|
663
|
+
} else {
|
|
664
|
+
// Top-level tool call fields
|
|
665
|
+
base.toolCall = {
|
|
666
|
+
toolCallId: String(raw.toolCallId ?? ''),
|
|
667
|
+
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
668
|
+
kind: raw.kind as AcpSessionUpdate['toolCall'] extends { kind: infer K } ? K : undefined,
|
|
669
|
+
status: (raw.status as 'pending' | 'in_progress' | 'completed' | 'failed') ?? 'pending',
|
|
670
|
+
rawInput: typeof raw.rawInput === 'string' ? raw.rawInput : undefined,
|
|
671
|
+
rawOutput: typeof raw.rawOutput === 'string' ? raw.rawOutput : undefined,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
|
|
676
|
+
case 'plan':
|
|
677
|
+
if (raw.entries && Array.isArray(raw.entries)) {
|
|
678
|
+
base.plan = { entries: raw.entries as AcpSessionUpdate['plan'] extends { entries: infer E } ? E : never };
|
|
679
|
+
} else if (raw.plan && typeof raw.plan === 'object') {
|
|
680
|
+
base.plan = raw.plan as AcpSessionUpdate['plan'];
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
|
|
684
|
+
case 'available_commands_update':
|
|
685
|
+
base.availableCommands = Array.isArray(raw.availableCommands) ? raw.availableCommands : undefined;
|
|
686
|
+
break;
|
|
687
|
+
|
|
688
|
+
case 'current_mode_update':
|
|
689
|
+
base.currentModeId = typeof raw.currentModeId === 'string' ? raw.currentModeId : undefined;
|
|
690
|
+
break;
|
|
691
|
+
|
|
692
|
+
case 'config_option_update':
|
|
693
|
+
base.configOptions = parseConfigOptions(raw.configOptions);
|
|
694
|
+
break;
|
|
695
|
+
|
|
696
|
+
case 'session_info_update':
|
|
697
|
+
base.sessionInfo = {
|
|
698
|
+
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
699
|
+
updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : undefined,
|
|
700
|
+
};
|
|
701
|
+
break;
|
|
702
|
+
|
|
703
|
+
case 'error':
|
|
704
|
+
base.error = typeof raw.error === 'string' ? raw.error : String(raw.message ?? 'Unknown error');
|
|
705
|
+
break;
|
|
706
|
+
|
|
707
|
+
case 'done':
|
|
708
|
+
// Terminal state — no extra fields
|
|
709
|
+
break;
|
|
710
|
+
|
|
711
|
+
case 'tool_result':
|
|
712
|
+
base.toolResult = raw.toolResult as AcpSessionUpdate['toolResult'];
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return base;
|
|
717
|
+
}
|