@geminilight/mindos 0.6.29 → 0.6.31
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/README_zh.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 +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -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/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -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 +574 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- 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 +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- 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 +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- 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/explore/use-cases.ts +0 -58
- 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,257 @@ 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
|
+
const errMsg = newResponse.error.message ?? 'session/new failed';
|
|
129
|
+
// Authentication errors are fatal — agent cannot proceed without auth
|
|
130
|
+
if (/auth/i.test(errMsg)) {
|
|
131
|
+
unsubApproval();
|
|
132
|
+
killAgent(proc);
|
|
133
|
+
throw new Error(`${entry.id}: ${errMsg}`);
|
|
134
|
+
}
|
|
135
|
+
// Other errors are non-fatal: some agents may not support explicit session/new
|
|
136
|
+
console.warn(`ACP session/new warning for ${entry.id}: ${errMsg}`);
|
|
137
|
+
} else {
|
|
138
|
+
const newResult = newResponse.result as Record<string, unknown> | undefined;
|
|
139
|
+
if (newResult) {
|
|
140
|
+
modes = parseModes(newResult.modes);
|
|
141
|
+
configOptions = parseConfigOptions(newResult.configOptions);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (sessionErr) {
|
|
145
|
+
// Re-throw auth errors — they are fatal
|
|
146
|
+
if (sessionErr instanceof Error && /auth/i.test(sessionErr.message)) throw sessionErr;
|
|
147
|
+
// Non-fatal: agent may not support explicit session/new (backwards compat)
|
|
148
|
+
}
|
|
149
|
+
|
|
67
150
|
const sessionId = `ses-${entry.id}-${Date.now()}`;
|
|
68
151
|
const session: AcpSession = {
|
|
69
152
|
id: sessionId,
|
|
70
153
|
agentId: entry.id,
|
|
71
154
|
state: 'idle',
|
|
155
|
+
cwd: options?.cwd,
|
|
72
156
|
createdAt: new Date().toISOString(),
|
|
73
157
|
lastActivityAt: new Date().toISOString(),
|
|
158
|
+
agentCapabilities,
|
|
159
|
+
modes,
|
|
160
|
+
configOptions,
|
|
161
|
+
authMethods,
|
|
74
162
|
};
|
|
75
163
|
|
|
76
164
|
sessions.set(sessionId, session);
|
|
77
165
|
sessionProcesses.set(sessionId, proc);
|
|
166
|
+
autoApprovalCleanups.set(sessionId, unsubApproval);
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load/resume an existing session on an agent.
|
|
172
|
+
* Requires agent to declare `loadSession` capability.
|
|
173
|
+
*/
|
|
174
|
+
export async function loadSession(
|
|
175
|
+
agentId: string,
|
|
176
|
+
existingSessionId: string,
|
|
177
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
178
|
+
): Promise<AcpSession> {
|
|
179
|
+
const entry = await findAcpAgent(agentId);
|
|
180
|
+
if (!entry) {
|
|
181
|
+
throw new Error(`ACP agent not found in registry: ${agentId}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const proc = spawnAcpAgent(entry, options);
|
|
185
|
+
const unsubApproval = installAutoApproval(proc);
|
|
186
|
+
|
|
187
|
+
let agentCapabilities: AcpAgentCapabilities | undefined;
|
|
188
|
+
|
|
189
|
+
// Initialize
|
|
190
|
+
try {
|
|
191
|
+
const initResponse = await sendAndWait(proc, 'initialize', {
|
|
192
|
+
protocolVersion: 1,
|
|
193
|
+
capabilities: {
|
|
194
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
195
|
+
terminal: true,
|
|
196
|
+
},
|
|
197
|
+
clientInfo: { name: 'mindos', version: '0.6.29' },
|
|
198
|
+
}, 30_000);
|
|
199
|
+
|
|
200
|
+
if (initResponse.error) {
|
|
201
|
+
unsubApproval();
|
|
202
|
+
killAgent(proc);
|
|
203
|
+
throw new Error(`initialize failed: ${initResponse.error.message}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const initResult = initResponse.result as Record<string, unknown> | undefined;
|
|
207
|
+
if (initResult) {
|
|
208
|
+
agentCapabilities = parseAgentCapabilities(initResult.agentCapabilities);
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
unsubApproval();
|
|
212
|
+
killAgent(proc);
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if agent supports loadSession
|
|
217
|
+
if (!agentCapabilities?.loadSession) {
|
|
218
|
+
unsubApproval();
|
|
219
|
+
killAgent(proc);
|
|
220
|
+
throw new Error(`Agent ${agentId} does not support session/load (loadSession capability not declared)`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// session/load — resume the existing session
|
|
224
|
+
let modes: AcpMode[] | undefined;
|
|
225
|
+
let configOptions: AcpConfigOption[] | undefined;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const loadResponse = await sendAndWait(proc, 'session/load', {
|
|
229
|
+
sessionId: existingSessionId,
|
|
230
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
231
|
+
mcpServers: [],
|
|
232
|
+
}, 15_000);
|
|
233
|
+
|
|
234
|
+
if (loadResponse.error) {
|
|
235
|
+
unsubApproval();
|
|
236
|
+
killAgent(proc);
|
|
237
|
+
throw new Error(`session/load failed: ${loadResponse.error.message}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const loadResult = loadResponse.result as Record<string, unknown> | undefined;
|
|
241
|
+
if (loadResult) {
|
|
242
|
+
modes = parseModes(loadResult.modes);
|
|
243
|
+
configOptions = parseConfigOptions(loadResult.configOptions);
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
unsubApproval();
|
|
247
|
+
killAgent(proc);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Use the original sessionId since we're resuming
|
|
252
|
+
const session: AcpSession = {
|
|
253
|
+
id: existingSessionId,
|
|
254
|
+
agentId: entry.id,
|
|
255
|
+
state: 'idle',
|
|
256
|
+
cwd: options?.cwd,
|
|
257
|
+
createdAt: new Date().toISOString(),
|
|
258
|
+
lastActivityAt: new Date().toISOString(),
|
|
259
|
+
agentCapabilities,
|
|
260
|
+
modes,
|
|
261
|
+
configOptions,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
sessions.set(existingSessionId, session);
|
|
265
|
+
sessionProcesses.set(existingSessionId, proc);
|
|
266
|
+
autoApprovalCleanups.set(existingSessionId, unsubApproval);
|
|
78
267
|
return session;
|
|
79
268
|
}
|
|
80
269
|
|
|
270
|
+
/**
|
|
271
|
+
* List resumable sessions from the agent.
|
|
272
|
+
* Requires agent to declare `sessionCapabilities.list`.
|
|
273
|
+
*/
|
|
274
|
+
export async function listSessions(
|
|
275
|
+
sessionId: string,
|
|
276
|
+
options?: { cursor?: string; cwd?: string },
|
|
277
|
+
): Promise<{ sessions: AcpSessionInfo[]; nextCursor?: string }> {
|
|
278
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
279
|
+
|
|
280
|
+
if (!session.agentCapabilities?.sessionCapabilities?.list) {
|
|
281
|
+
throw new Error('Agent does not support session/list');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const response = await sendAndWait(proc, 'session/list', {
|
|
285
|
+
...(options?.cursor ? { cursor: options.cursor } : {}),
|
|
286
|
+
...(options?.cwd ? { cwd: options.cwd } : {}),
|
|
287
|
+
}, 10_000);
|
|
288
|
+
|
|
289
|
+
if (response.error) {
|
|
290
|
+
throw new Error(`session/list failed: ${response.error.message}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
294
|
+
const rawSessions = Array.isArray(result?.sessions) ? result.sessions : [];
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
sessions: rawSessions.map((s: unknown) => {
|
|
298
|
+
const obj = s as Record<string, unknown>;
|
|
299
|
+
return {
|
|
300
|
+
sessionId: String(obj.sessionId ?? ''),
|
|
301
|
+
title: typeof obj.title === 'string' ? obj.title : undefined,
|
|
302
|
+
cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined,
|
|
303
|
+
updatedAt: typeof obj.updatedAt === 'string' ? obj.updatedAt : undefined,
|
|
304
|
+
};
|
|
305
|
+
}),
|
|
306
|
+
nextCursor: typeof result?.nextCursor === 'string' ? result.nextCursor : undefined,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ── Public API — Prompt ──────────────────────────────────────────────── */
|
|
311
|
+
|
|
81
312
|
/**
|
|
82
313
|
* Send a prompt to an active session and collect the full response.
|
|
83
314
|
* For streaming, use promptStream() instead.
|
|
@@ -95,7 +326,11 @@ export async function prompt(
|
|
|
95
326
|
updateSessionState(session, 'active');
|
|
96
327
|
|
|
97
328
|
try {
|
|
98
|
-
const response = await sendAndWait(proc, 'session/prompt', {
|
|
329
|
+
const response = await sendAndWait(proc, 'session/prompt', {
|
|
330
|
+
sessionId,
|
|
331
|
+
prompt: [{ type: 'text', text }] satisfies AcpContentBlock[],
|
|
332
|
+
...(session.cwd ? { context: { cwd: session.cwd } } : {}),
|
|
333
|
+
}, 60_000);
|
|
99
334
|
|
|
100
335
|
if (response.error) {
|
|
101
336
|
updateSessionState(session, 'error');
|
|
@@ -108,6 +343,7 @@ export async function prompt(
|
|
|
108
343
|
sessionId,
|
|
109
344
|
text: String(result?.text ?? ''),
|
|
110
345
|
done: true,
|
|
346
|
+
stopReason: parseStopReason(result?.stopReason),
|
|
111
347
|
toolCalls: result?.toolCalls as AcpPromptResponse['toolCalls'],
|
|
112
348
|
metadata: result?.metadata as AcpPromptResponse['metadata'],
|
|
113
349
|
};
|
|
@@ -119,6 +355,7 @@ export async function prompt(
|
|
|
119
355
|
|
|
120
356
|
/**
|
|
121
357
|
* Send a prompt and receive streaming updates via callback.
|
|
358
|
+
* Handles all 10 ACP session/update types.
|
|
122
359
|
* Returns the final aggregated response.
|
|
123
360
|
*/
|
|
124
361
|
export async function promptStream(
|
|
@@ -136,47 +373,79 @@ export async function promptStream(
|
|
|
136
373
|
|
|
137
374
|
return new Promise((resolve, reject) => {
|
|
138
375
|
let aggregatedText = '';
|
|
376
|
+
let stopReason: AcpStopReason = 'end_turn';
|
|
139
377
|
|
|
140
378
|
const unsub = onMessage(proc, (msg) => {
|
|
141
|
-
// Handle streaming notifications (no id field, or notification pattern)
|
|
142
379
|
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
|
-
};
|
|
380
|
+
const raw = msg.result as Record<string, unknown>;
|
|
381
|
+
const update = parseSessionUpdate(sessionId, raw);
|
|
152
382
|
|
|
153
|
-
onUpdate(
|
|
383
|
+
onUpdate(update);
|
|
154
384
|
|
|
155
|
-
|
|
156
|
-
|
|
385
|
+
// Aggregate text from message chunk types
|
|
386
|
+
if ((update.type === 'agent_message_chunk' || update.type === 'text') && update.text) {
|
|
387
|
+
aggregatedText += update.text;
|
|
157
388
|
}
|
|
158
389
|
|
|
159
|
-
|
|
390
|
+
// Handle terminal states
|
|
391
|
+
if (update.type === 'done') {
|
|
160
392
|
unsub();
|
|
393
|
+
if (raw.stopReason) {
|
|
394
|
+
stopReason = parseStopReason(raw.stopReason);
|
|
395
|
+
}
|
|
161
396
|
updateSessionState(session, 'idle');
|
|
162
397
|
resolve({
|
|
163
398
|
sessionId,
|
|
164
399
|
text: aggregatedText,
|
|
165
400
|
done: true,
|
|
401
|
+
stopReason,
|
|
166
402
|
});
|
|
167
403
|
}
|
|
168
404
|
|
|
169
|
-
if (
|
|
405
|
+
if (update.type === 'error') {
|
|
170
406
|
unsub();
|
|
171
407
|
updateSessionState(session, 'error');
|
|
172
|
-
reject(new Error(
|
|
408
|
+
reject(new Error(update.error ?? 'Unknown ACP error'));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Update session metadata from config/mode updates
|
|
412
|
+
if (update.type === 'config_option_update' && update.configOptions) {
|
|
413
|
+
session.configOptions = update.configOptions;
|
|
414
|
+
}
|
|
415
|
+
if (update.type === 'current_mode_update' && update.currentModeId) {
|
|
416
|
+
// Track current mode
|
|
417
|
+
session.lastActivityAt = new Date().toISOString();
|
|
173
418
|
}
|
|
174
419
|
}
|
|
175
420
|
});
|
|
176
421
|
|
|
177
|
-
//
|
|
422
|
+
// Guard against agent process dying unexpectedly (OOM, SIGKILL, etc.)
|
|
423
|
+
// Without this, the Promise would hang forever if the process exits
|
|
424
|
+
// without sending a done/error notification.
|
|
425
|
+
const onExit = () => {
|
|
426
|
+
unsub();
|
|
427
|
+
updateSessionState(session, 'error');
|
|
428
|
+
reject(new Error(`ACP agent process exited unexpectedly during prompt`));
|
|
429
|
+
};
|
|
430
|
+
proc.proc.once('exit', onExit);
|
|
431
|
+
|
|
432
|
+
// Clean up exit listener when Promise resolves/rejects normally
|
|
433
|
+
const cleanup = () => { proc.proc.removeListener('exit', onExit); };
|
|
434
|
+
// Wrap resolve/reject to include cleanup — but we already unsub in the message handler.
|
|
435
|
+
// The exit listener is a safety net; if done/error fires first, remove the exit listener.
|
|
436
|
+
const origResolve = resolve;
|
|
437
|
+
const origReject = reject;
|
|
438
|
+
resolve = ((val: AcpPromptResponse) => { cleanup(); origResolve(val); }) as typeof resolve;
|
|
439
|
+
reject = ((err: unknown) => { cleanup(); origReject(err); }) as typeof reject;
|
|
440
|
+
|
|
441
|
+
// Send the prompt with ContentBlock format
|
|
178
442
|
try {
|
|
179
|
-
sendMessage(proc, 'session/prompt', {
|
|
443
|
+
sendMessage(proc, 'session/prompt', {
|
|
444
|
+
sessionId,
|
|
445
|
+
prompt: [{ type: 'text', text }] satisfies AcpContentBlock[],
|
|
446
|
+
stream: true,
|
|
447
|
+
...(session.cwd ? { context: { cwd: session.cwd } } : {}),
|
|
448
|
+
});
|
|
180
449
|
} catch (err) {
|
|
181
450
|
unsub();
|
|
182
451
|
updateSessionState(session, 'error');
|
|
@@ -185,6 +454,8 @@ export async function promptStream(
|
|
|
185
454
|
});
|
|
186
455
|
}
|
|
187
456
|
|
|
457
|
+
/* ── Public API — Session Control ─────────────────────────────────────── */
|
|
458
|
+
|
|
188
459
|
/**
|
|
189
460
|
* Cancel the current prompt turn on a session.
|
|
190
461
|
*/
|
|
@@ -194,7 +465,7 @@ export async function cancelPrompt(sessionId: string): Promise<void> {
|
|
|
194
465
|
if (session.state !== 'active') return;
|
|
195
466
|
|
|
196
467
|
try {
|
|
197
|
-
await sendAndWait(proc, 'session/cancel', {},
|
|
468
|
+
await sendAndWait(proc, 'session/cancel', { sessionId }, 10_000);
|
|
198
469
|
} catch {
|
|
199
470
|
// Best-effort cancel — don't throw if the agent doesn't support it
|
|
200
471
|
}
|
|
@@ -202,6 +473,54 @@ export async function cancelPrompt(sessionId: string): Promise<void> {
|
|
|
202
473
|
updateSessionState(session, 'idle');
|
|
203
474
|
}
|
|
204
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Set the operating mode for a session.
|
|
478
|
+
*/
|
|
479
|
+
export async function setMode(sessionId: string, modeId: string): Promise<void> {
|
|
480
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
481
|
+
|
|
482
|
+
const response = await sendAndWait(proc, 'session/set_mode', {
|
|
483
|
+
sessionId,
|
|
484
|
+
modeId,
|
|
485
|
+
}, 10_000);
|
|
486
|
+
|
|
487
|
+
if (response.error) {
|
|
488
|
+
throw new Error(`session/set_mode failed: ${response.error.message}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
session.lastActivityAt = new Date().toISOString();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Set a configuration option for a session.
|
|
496
|
+
*/
|
|
497
|
+
export async function setConfigOption(
|
|
498
|
+
sessionId: string,
|
|
499
|
+
configId: string,
|
|
500
|
+
value: string,
|
|
501
|
+
): Promise<AcpConfigOption[]> {
|
|
502
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
503
|
+
|
|
504
|
+
const response = await sendAndWait(proc, 'session/set_config_option', {
|
|
505
|
+
sessionId,
|
|
506
|
+
configId,
|
|
507
|
+
value,
|
|
508
|
+
}, 10_000);
|
|
509
|
+
|
|
510
|
+
if (response.error) {
|
|
511
|
+
throw new Error(`session/set_config_option failed: ${response.error.message}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
515
|
+
const configOptions = parseConfigOptions(result?.configOptions);
|
|
516
|
+
if (configOptions) {
|
|
517
|
+
session.configOptions = configOptions;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
session.lastActivityAt = new Date().toISOString();
|
|
521
|
+
return session.configOptions ?? [];
|
|
522
|
+
}
|
|
523
|
+
|
|
205
524
|
/**
|
|
206
525
|
* Close a session and terminate the subprocess.
|
|
207
526
|
*/
|
|
@@ -210,7 +529,7 @@ export async function closeSession(sessionId: string): Promise<void> {
|
|
|
210
529
|
|
|
211
530
|
if (proc?.alive) {
|
|
212
531
|
try {
|
|
213
|
-
await sendAndWait(proc, 'session/close', {}, 5_000);
|
|
532
|
+
await sendAndWait(proc, 'session/close', { sessionId }, 5_000);
|
|
214
533
|
} catch {
|
|
215
534
|
// Best-effort close
|
|
216
535
|
}
|
|
@@ -219,8 +538,15 @@ export async function closeSession(sessionId: string): Promise<void> {
|
|
|
219
538
|
|
|
220
539
|
sessions.delete(sessionId);
|
|
221
540
|
sessionProcesses.delete(sessionId);
|
|
541
|
+
const cleanup = autoApprovalCleanups.get(sessionId);
|
|
542
|
+
if (cleanup) {
|
|
543
|
+
cleanup();
|
|
544
|
+
autoApprovalCleanups.delete(sessionId);
|
|
545
|
+
}
|
|
222
546
|
}
|
|
223
547
|
|
|
548
|
+
/* ── Public API — Queries ─────────────────────────────────────────────── */
|
|
549
|
+
|
|
224
550
|
/**
|
|
225
551
|
* Get a session by its ID.
|
|
226
552
|
*/
|
|
@@ -243,7 +569,7 @@ export async function closeAllSessions(): Promise<void> {
|
|
|
243
569
|
await Promise.allSettled(ids.map(id => closeSession(id)));
|
|
244
570
|
}
|
|
245
571
|
|
|
246
|
-
/* ── Internal
|
|
572
|
+
/* ── Internal — Session helpers ───────────────────────────────────────── */
|
|
247
573
|
|
|
248
574
|
function getSessionAndProc(sessionId: string): { session: AcpSession; proc: AcpProcess } {
|
|
249
575
|
const session = sessions.get(sessionId);
|
|
@@ -262,3 +588,139 @@ function updateSessionState(session: AcpSession, state: AcpSessionState): void {
|
|
|
262
588
|
session.state = state;
|
|
263
589
|
session.lastActivityAt = new Date().toISOString();
|
|
264
590
|
}
|
|
591
|
+
|
|
592
|
+
/* ── Internal — Parsers ───────────────────────────────────────────────── */
|
|
593
|
+
|
|
594
|
+
function parseStopReason(raw: unknown): AcpStopReason {
|
|
595
|
+
const valid: AcpStopReason[] = ['end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'];
|
|
596
|
+
return valid.includes(raw as AcpStopReason) ? (raw as AcpStopReason) : 'end_turn';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function parseAgentCapabilities(raw: unknown): AcpAgentCapabilities | undefined {
|
|
600
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
601
|
+
const obj = raw as Record<string, unknown>;
|
|
602
|
+
return {
|
|
603
|
+
loadSession: obj.loadSession === true,
|
|
604
|
+
mcpCapabilities: typeof obj.mcpCapabilities === 'object' ? obj.mcpCapabilities as AcpAgentCapabilities['mcpCapabilities'] : undefined,
|
|
605
|
+
promptCapabilities: typeof obj.promptCapabilities === 'object' ? obj.promptCapabilities as AcpAgentCapabilities['promptCapabilities'] : undefined,
|
|
606
|
+
sessionCapabilities: typeof obj.sessionCapabilities === 'object' ? obj.sessionCapabilities as AcpAgentCapabilities['sessionCapabilities'] : undefined,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function parseAuthMethods(raw: unknown): AcpAuthMethod[] | undefined {
|
|
611
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
612
|
+
return raw
|
|
613
|
+
.filter((m): m is Record<string, unknown> => !!m && typeof m === 'object')
|
|
614
|
+
.map(m => ({
|
|
615
|
+
id: String(m.id ?? ''),
|
|
616
|
+
name: String(m.name ?? ''),
|
|
617
|
+
description: typeof m.description === 'string' ? m.description : undefined,
|
|
618
|
+
}))
|
|
619
|
+
.filter(m => m.id && m.name);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function parseModes(raw: unknown): AcpMode[] | undefined {
|
|
623
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
624
|
+
return raw
|
|
625
|
+
.filter((m): m is Record<string, unknown> => !!m && typeof m === 'object')
|
|
626
|
+
.map(m => ({
|
|
627
|
+
id: String(m.id ?? ''),
|
|
628
|
+
name: String(m.name ?? ''),
|
|
629
|
+
description: typeof m.description === 'string' ? m.description : undefined,
|
|
630
|
+
}))
|
|
631
|
+
.filter(m => m.id && m.name);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function parseConfigOptions(raw: unknown): AcpConfigOption[] | undefined {
|
|
635
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
636
|
+
return raw
|
|
637
|
+
.filter((o): o is Record<string, unknown> => !!o && typeof o === 'object')
|
|
638
|
+
.map(o => ({
|
|
639
|
+
type: 'select' as const,
|
|
640
|
+
configId: String(o.configId ?? o.id ?? ''),
|
|
641
|
+
category: String(o.category ?? 'other'),
|
|
642
|
+
label: typeof o.label === 'string' ? o.label : undefined,
|
|
643
|
+
currentValue: String(o.currentValue ?? ''),
|
|
644
|
+
options: Array.isArray(o.options) ? o.options.map((opt: unknown) => {
|
|
645
|
+
const optObj = opt as Record<string, unknown>;
|
|
646
|
+
return { id: String(optObj.id ?? ''), label: String(optObj.label ?? '') };
|
|
647
|
+
}) : [],
|
|
648
|
+
}))
|
|
649
|
+
.filter(o => o.configId);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Parse a raw session/update notification into a typed AcpSessionUpdate. */
|
|
653
|
+
function parseSessionUpdate(sessionId: string, raw: Record<string, unknown>): AcpSessionUpdate {
|
|
654
|
+
const type = raw.type as AcpSessionUpdate['type'] ?? 'text';
|
|
655
|
+
|
|
656
|
+
const base: AcpSessionUpdate = { sessionId, type };
|
|
657
|
+
|
|
658
|
+
switch (type) {
|
|
659
|
+
case 'agent_message_chunk':
|
|
660
|
+
case 'user_message_chunk':
|
|
661
|
+
case 'agent_thought_chunk':
|
|
662
|
+
case 'text':
|
|
663
|
+
base.text = typeof raw.text === 'string' ? raw.text
|
|
664
|
+
: typeof raw.content === 'string' ? raw.content
|
|
665
|
+
: undefined;
|
|
666
|
+
break;
|
|
667
|
+
|
|
668
|
+
case 'tool_call':
|
|
669
|
+
case 'tool_call_update':
|
|
670
|
+
if (raw.toolCall && typeof raw.toolCall === 'object') {
|
|
671
|
+
base.toolCall = raw.toolCall as AcpSessionUpdate['toolCall'];
|
|
672
|
+
} else {
|
|
673
|
+
// Top-level tool call fields
|
|
674
|
+
base.toolCall = {
|
|
675
|
+
toolCallId: String(raw.toolCallId ?? ''),
|
|
676
|
+
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
677
|
+
kind: raw.kind as AcpSessionUpdate['toolCall'] extends { kind: infer K } ? K : undefined,
|
|
678
|
+
status: (raw.status as 'pending' | 'in_progress' | 'completed' | 'failed') ?? 'pending',
|
|
679
|
+
rawInput: typeof raw.rawInput === 'string' ? raw.rawInput : undefined,
|
|
680
|
+
rawOutput: typeof raw.rawOutput === 'string' ? raw.rawOutput : undefined,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
|
|
685
|
+
case 'plan':
|
|
686
|
+
if (raw.entries && Array.isArray(raw.entries)) {
|
|
687
|
+
base.plan = { entries: raw.entries as AcpSessionUpdate['plan'] extends { entries: infer E } ? E : never };
|
|
688
|
+
} else if (raw.plan && typeof raw.plan === 'object') {
|
|
689
|
+
base.plan = raw.plan as AcpSessionUpdate['plan'];
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
|
|
693
|
+
case 'available_commands_update':
|
|
694
|
+
base.availableCommands = Array.isArray(raw.availableCommands) ? raw.availableCommands : undefined;
|
|
695
|
+
break;
|
|
696
|
+
|
|
697
|
+
case 'current_mode_update':
|
|
698
|
+
base.currentModeId = typeof raw.currentModeId === 'string' ? raw.currentModeId : undefined;
|
|
699
|
+
break;
|
|
700
|
+
|
|
701
|
+
case 'config_option_update':
|
|
702
|
+
base.configOptions = parseConfigOptions(raw.configOptions);
|
|
703
|
+
break;
|
|
704
|
+
|
|
705
|
+
case 'session_info_update':
|
|
706
|
+
base.sessionInfo = {
|
|
707
|
+
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
708
|
+
updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : undefined,
|
|
709
|
+
};
|
|
710
|
+
break;
|
|
711
|
+
|
|
712
|
+
case 'error':
|
|
713
|
+
base.error = typeof raw.error === 'string' ? raw.error : String(raw.message ?? 'Unknown error');
|
|
714
|
+
break;
|
|
715
|
+
|
|
716
|
+
case 'done':
|
|
717
|
+
// Terminal state — no extra fields
|
|
718
|
+
break;
|
|
719
|
+
|
|
720
|
+
case 'tool_result':
|
|
721
|
+
base.toolResult = raw.toolResult as AcpSessionUpdate['toolResult'];
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return base;
|
|
726
|
+
}
|