@geminilight/mindos 0.6.28 → 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/a2a/agents/route.ts +9 -0
- package/app/app/api/a2a/delegations/route.ts +9 -0
- package/app/app/api/a2a/discover/route.ts +2 -0
- package/app/app/api/a2a/route.ts +6 -6
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +114 -0
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +185 -0
- package/app/app/api/ask/route.ts +116 -13
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/layout.tsx +2 -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/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +40 -10
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/agents/AgentDetailContent.tsx +266 -52
- package/app/components/agents/AgentsContentPage.tsx +32 -6
- package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- 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/help/HelpContent.tsx +9 -9
- package/app/components/panels/AgentsPanel.tsx +2 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -0
- 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/components/settings/KnowledgeTab.tsx +3 -6
- package/app/components/settings/McpSkillsSection.tsx +4 -5
- package/app/components/settings/McpTab.tsx +6 -8
- package/app/components/setup/StepSecurity.tsx +4 -5
- package/app/components/setup/index.tsx +5 -11
- package/app/components/ui/Toaster.tsx +39 -0
- package/app/hooks/useA2aRegistry.ts +6 -1
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +120 -0
- package/app/hooks/useAcpRegistry.ts +86 -0
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useDelegationHistory.ts +49 -0
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/a2a/client.ts +49 -5
- package/app/lib/a2a/orchestrator.ts +0 -1
- package/app/lib/a2a/task-handler.ts +4 -4
- package/app/lib/a2a/types.ts +15 -0
- package/app/lib/acp/acp-tools.ts +95 -0
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +144 -0
- package/app/lib/acp/index.ts +40 -0
- package/app/lib/acp/registry.ts +202 -0
- package/app/lib/acp/session.ts +717 -0
- package/app/lib/acp/subprocess.ts +495 -0
- package/app/lib/acp/types.ts +274 -0
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +2 -1
- package/app/lib/i18n/_core.ts +22 -0
- package/app/lib/i18n/index.ts +35 -0
- package/app/lib/i18n/modules/ai-chat.ts +215 -0
- package/app/lib/i18n/modules/common.ts +71 -0
- package/app/lib/i18n/modules/features.ts +153 -0
- package/app/lib/i18n/modules/knowledge.ts +429 -0
- package/app/lib/i18n/modules/navigation.ts +153 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1196 -0
- package/app/lib/i18n/modules/settings.ts +585 -0
- package/app/lib/i18n-en.ts +2 -1518
- package/app/lib/i18n-zh.ts +2 -1542
- package/app/lib/i18n.ts +3 -6
- 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/toast.ts +79 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +3 -1
- package/bin/cli.js +25 -25
- package/bin/commands/file.js +29 -2
- package/bin/commands/space.js +249 -91
- 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
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Session Manager — High-level session lifecycle for ACP agents.
|
|
3
|
+
* Manages session creation, prompt turns, cancellation, and cleanup.
|
|
4
|
+
* Implements full ACP spec: initialize → session/new → session/prompt → session/cancel → close.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AcpSession,
|
|
9
|
+
AcpSessionState,
|
|
10
|
+
AcpSessionUpdate,
|
|
11
|
+
AcpPromptResponse,
|
|
12
|
+
AcpRegistryEntry,
|
|
13
|
+
AcpAgentCapabilities,
|
|
14
|
+
AcpMode,
|
|
15
|
+
AcpConfigOption,
|
|
16
|
+
AcpSessionInfo,
|
|
17
|
+
AcpStopReason,
|
|
18
|
+
AcpAuthMethod,
|
|
19
|
+
AcpContentBlock,
|
|
20
|
+
} from './types';
|
|
21
|
+
import {
|
|
22
|
+
spawnAcpAgent,
|
|
23
|
+
sendAndWait,
|
|
24
|
+
sendMessage,
|
|
25
|
+
onMessage,
|
|
26
|
+
killAgent,
|
|
27
|
+
installAutoApproval,
|
|
28
|
+
type AcpProcess,
|
|
29
|
+
} from './subprocess';
|
|
30
|
+
import { findAcpAgent } from './registry';
|
|
31
|
+
|
|
32
|
+
/* ── State ─────────────────────────────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
const sessions = new Map<string, AcpSession>();
|
|
35
|
+
const sessionProcesses = new Map<string, AcpProcess>();
|
|
36
|
+
const autoApprovalCleanups = new Map<string, () => void>();
|
|
37
|
+
|
|
38
|
+
/* ── Public API — Session Lifecycle ───────────────────────────────────── */
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new ACP session by spawning an agent process.
|
|
42
|
+
* Full ACP lifecycle: spawn → initialize → authenticate (if needed) → session/new.
|
|
43
|
+
*/
|
|
44
|
+
export async function createSession(
|
|
45
|
+
agentId: string,
|
|
46
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
47
|
+
): Promise<AcpSession> {
|
|
48
|
+
const entry = await findAcpAgent(agentId);
|
|
49
|
+
if (!entry) {
|
|
50
|
+
throw new Error(`ACP agent not found in registry: ${agentId}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return createSessionFromEntry(entry, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a session from a known registry entry (skips registry lookup).
|
|
58
|
+
*/
|
|
59
|
+
export async function createSessionFromEntry(
|
|
60
|
+
entry: AcpRegistryEntry,
|
|
61
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
62
|
+
): Promise<AcpSession> {
|
|
63
|
+
const proc = spawnAcpAgent(entry, options);
|
|
64
|
+
|
|
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
|
|
73
|
+
try {
|
|
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);
|
|
82
|
+
|
|
83
|
+
if (response.error) {
|
|
84
|
+
unsubApproval();
|
|
85
|
+
killAgent(proc);
|
|
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);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
unsubApproval();
|
|
97
|
+
killAgent(proc);
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
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
|
+
|
|
141
|
+
const sessionId = `ses-${entry.id}-${Date.now()}`;
|
|
142
|
+
const session: AcpSession = {
|
|
143
|
+
id: sessionId,
|
|
144
|
+
agentId: entry.id,
|
|
145
|
+
state: 'idle',
|
|
146
|
+
cwd: options?.cwd,
|
|
147
|
+
createdAt: new Date().toISOString(),
|
|
148
|
+
lastActivityAt: new Date().toISOString(),
|
|
149
|
+
agentCapabilities,
|
|
150
|
+
modes,
|
|
151
|
+
configOptions,
|
|
152
|
+
authMethods,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
sessions.set(sessionId, session);
|
|
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);
|
|
258
|
+
return session;
|
|
259
|
+
}
|
|
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
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Send a prompt to an active session and collect the full response.
|
|
305
|
+
* For streaming, use promptStream() instead.
|
|
306
|
+
*/
|
|
307
|
+
export async function prompt(
|
|
308
|
+
sessionId: string,
|
|
309
|
+
text: string,
|
|
310
|
+
): Promise<AcpPromptResponse> {
|
|
311
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
312
|
+
|
|
313
|
+
if (session.state === 'active') {
|
|
314
|
+
throw new Error(`Session ${sessionId} is busy processing another prompt`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
updateSessionState(session, 'active');
|
|
318
|
+
|
|
319
|
+
try {
|
|
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);
|
|
325
|
+
|
|
326
|
+
if (response.error) {
|
|
327
|
+
updateSessionState(session, 'error');
|
|
328
|
+
throw new Error(`session/prompt error: ${response.error.message}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
updateSessionState(session, 'idle');
|
|
332
|
+
const result = response.result as Record<string, unknown>;
|
|
333
|
+
return {
|
|
334
|
+
sessionId,
|
|
335
|
+
text: String(result?.text ?? ''),
|
|
336
|
+
done: true,
|
|
337
|
+
stopReason: parseStopReason(result?.stopReason),
|
|
338
|
+
toolCalls: result?.toolCalls as AcpPromptResponse['toolCalls'],
|
|
339
|
+
metadata: result?.metadata as AcpPromptResponse['metadata'],
|
|
340
|
+
};
|
|
341
|
+
} catch (err) {
|
|
342
|
+
updateSessionState(session, 'error');
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Send a prompt and receive streaming updates via callback.
|
|
349
|
+
* Handles all 10 ACP session/update types.
|
|
350
|
+
* Returns the final aggregated response.
|
|
351
|
+
*/
|
|
352
|
+
export async function promptStream(
|
|
353
|
+
sessionId: string,
|
|
354
|
+
text: string,
|
|
355
|
+
onUpdate: (update: AcpSessionUpdate) => void,
|
|
356
|
+
): Promise<AcpPromptResponse> {
|
|
357
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
358
|
+
|
|
359
|
+
if (session.state === 'active') {
|
|
360
|
+
throw new Error(`Session ${sessionId} is busy processing another prompt`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
updateSessionState(session, 'active');
|
|
364
|
+
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
let aggregatedText = '';
|
|
367
|
+
let stopReason: AcpStopReason = 'end_turn';
|
|
368
|
+
|
|
369
|
+
const unsub = onMessage(proc, (msg) => {
|
|
370
|
+
if (msg.result && typeof msg.result === 'object') {
|
|
371
|
+
const raw = msg.result as Record<string, unknown>;
|
|
372
|
+
const update = parseSessionUpdate(sessionId, raw);
|
|
373
|
+
|
|
374
|
+
onUpdate(update);
|
|
375
|
+
|
|
376
|
+
// Aggregate text from message chunk types
|
|
377
|
+
if ((update.type === 'agent_message_chunk' || update.type === 'text') && update.text) {
|
|
378
|
+
aggregatedText += update.text;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle terminal states
|
|
382
|
+
if (update.type === 'done') {
|
|
383
|
+
unsub();
|
|
384
|
+
if (raw.stopReason) {
|
|
385
|
+
stopReason = parseStopReason(raw.stopReason);
|
|
386
|
+
}
|
|
387
|
+
updateSessionState(session, 'idle');
|
|
388
|
+
resolve({
|
|
389
|
+
sessionId,
|
|
390
|
+
text: aggregatedText,
|
|
391
|
+
done: true,
|
|
392
|
+
stopReason,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (update.type === 'error') {
|
|
397
|
+
unsub();
|
|
398
|
+
updateSessionState(session, '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();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
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
|
|
433
|
+
try {
|
|
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
|
+
});
|
|
440
|
+
} catch (err) {
|
|
441
|
+
unsub();
|
|
442
|
+
updateSessionState(session, 'error');
|
|
443
|
+
reject(err);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── Public API — Session Control ─────────────────────────────────────── */
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Cancel the current prompt turn on a session.
|
|
452
|
+
*/
|
|
453
|
+
export async function cancelPrompt(sessionId: string): Promise<void> {
|
|
454
|
+
const { session, proc } = getSessionAndProc(sessionId);
|
|
455
|
+
|
|
456
|
+
if (session.state !== 'active') return;
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
await sendAndWait(proc, 'session/cancel', { sessionId }, 10_000);
|
|
460
|
+
} catch {
|
|
461
|
+
// Best-effort cancel — don't throw if the agent doesn't support it
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
updateSessionState(session, 'idle');
|
|
465
|
+
}
|
|
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
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Close a session and terminate the subprocess.
|
|
517
|
+
*/
|
|
518
|
+
export async function closeSession(sessionId: string): Promise<void> {
|
|
519
|
+
const proc = sessionProcesses.get(sessionId);
|
|
520
|
+
|
|
521
|
+
if (proc?.alive) {
|
|
522
|
+
try {
|
|
523
|
+
await sendAndWait(proc, 'session/close', { sessionId }, 5_000);
|
|
524
|
+
} catch {
|
|
525
|
+
// Best-effort close
|
|
526
|
+
}
|
|
527
|
+
killAgent(proc);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
sessions.delete(sessionId);
|
|
531
|
+
sessionProcesses.delete(sessionId);
|
|
532
|
+
const cleanup = autoApprovalCleanups.get(sessionId);
|
|
533
|
+
if (cleanup) {
|
|
534
|
+
cleanup();
|
|
535
|
+
autoApprovalCleanups.delete(sessionId);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* ── Public API — Queries ─────────────────────────────────────────────── */
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get a session by its ID.
|
|
543
|
+
*/
|
|
544
|
+
export function getSession(sessionId: string): AcpSession | undefined {
|
|
545
|
+
return sessions.get(sessionId);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get all active sessions.
|
|
550
|
+
*/
|
|
551
|
+
export function getActiveSessions(): AcpSession[] {
|
|
552
|
+
return [...sessions.values()];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Close all active sessions. Used for cleanup.
|
|
557
|
+
*/
|
|
558
|
+
export async function closeAllSessions(): Promise<void> {
|
|
559
|
+
const ids = [...sessions.keys()];
|
|
560
|
+
await Promise.allSettled(ids.map(id => closeSession(id)));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ── Internal — Session helpers ───────────────────────────────────────── */
|
|
564
|
+
|
|
565
|
+
function getSessionAndProc(sessionId: string): { session: AcpSession; proc: AcpProcess } {
|
|
566
|
+
const session = sessions.get(sessionId);
|
|
567
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
568
|
+
|
|
569
|
+
const proc = sessionProcesses.get(sessionId);
|
|
570
|
+
if (!proc?.alive) {
|
|
571
|
+
updateSessionState(session, 'error');
|
|
572
|
+
throw new Error(`Session process is dead: ${sessionId}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { session, proc };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function updateSessionState(session: AcpSession, state: AcpSessionState): void {
|
|
579
|
+
session.state = state;
|
|
580
|
+
session.lastActivityAt = new Date().toISOString();
|
|
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
|
+
}
|