@evandu/super-pi 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -2
- package/agents/coder.md +10 -0
- package/agents/critic.md +20 -0
- package/agents/patcher.md +23 -0
- package/agents/planner.md +18 -0
- package/agents/reviewer.md +13 -0
- package/agents/scout.md +23 -0
- package/agents/super-pi-task/core.ts +485 -0
- package/agents/super-pi-task/index.ts +141 -0
- package/agents/verifier.md +21 -0
- package/bin/super-pi.js +17 -0
- package/dist/client/assets/index-BMQsEdio.js +24 -0
- package/dist/client/assets/index-CsbZzuMy.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +111 -0
- package/dist/server/agents.d.ts +177 -0
- package/dist/server/agents.js +484 -0
- package/dist/server/application/catalog-service.d.ts +44 -0
- package/dist/server/application/catalog-service.js +94 -0
- package/dist/server/application/conversation-service.d.ts +73 -0
- package/dist/server/application/conversation-service.js +202 -0
- package/dist/server/application/extension-ui-service.d.ts +28 -0
- package/dist/server/application/extension-ui-service.js +73 -0
- package/dist/server/application/run-service.d.ts +115 -0
- package/dist/server/application/run-service.js +341 -0
- package/dist/server/application/workspace-service.d.ts +45 -0
- package/dist/server/application/workspace-service.js +106 -0
- package/dist/server/command-handler.d.ts +22 -0
- package/dist/server/command-handler.js +137 -0
- package/dist/server/files.d.ts +202 -0
- package/dist/server/files.js +629 -0
- package/dist/server/http-server.d.ts +20 -0
- package/dist/server/http-server.js +313 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +103 -0
- package/dist/server/pi-command.d.ts +65 -0
- package/dist/server/pi-command.js +183 -0
- package/dist/server/runtime.d.ts +819 -0
- package/dist/server/runtime.js +1470 -0
- package/dist/server/server-context.d.ts +46 -0
- package/dist/server/server-context.js +1 -0
- package/dist/server/store.d.ts +424 -0
- package/dist/server/store.js +1121 -0
- package/dist/server/subagent-runs.d.ts +142 -0
- package/dist/server/subagent-runs.js +315 -0
- package/dist/server/subagents.d.ts +194 -0
- package/dist/server/subagents.js +542 -0
- package/dist/server/task-tool.d.ts +196 -0
- package/dist/server/task-tool.js +285 -0
- package/dist/server/tool-calls.d.ts +62 -0
- package/dist/server/tool-calls.js +141 -0
- package/dist/server/types.d.ts +920 -0
- package/dist/server/types.js +11 -0
- package/dist/server/upload.d.ts +37 -0
- package/dist/server/upload.js +169 -0
- package/dist/server/ws-server.d.ts +45 -0
- package/dist/server/ws-server.js +116 -0
- package/package.json +115 -4
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { checkSubagentDepth, parentDeniedTools } from "../task-tool.js";
|
|
2
|
+
import { newSubagentRunId } from "../subagents.js";
|
|
3
|
+
/**
|
|
4
|
+
* Service managing execution of conversation turns, message queueing,
|
|
5
|
+
* follow-ups, and sub-agent dispatching.
|
|
6
|
+
*/
|
|
7
|
+
export class RunService {
|
|
8
|
+
store;
|
|
9
|
+
runtime;
|
|
10
|
+
subagents;
|
|
11
|
+
broadcaster;
|
|
12
|
+
extensionUiService;
|
|
13
|
+
activeRuns = new Map();
|
|
14
|
+
constructor(store, runtime, subagents, broadcaster, extensionUiService) {
|
|
15
|
+
this.store = store;
|
|
16
|
+
this.runtime = runtime;
|
|
17
|
+
this.subagents = subagents;
|
|
18
|
+
this.broadcaster = broadcaster;
|
|
19
|
+
this.extensionUiService = extensionUiService;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the conversation and workspace synchronously for a run.
|
|
23
|
+
*/
|
|
24
|
+
turnContext(conversationId) {
|
|
25
|
+
const conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
|
|
26
|
+
if (!conversation)
|
|
27
|
+
throw new Error("会话不存在");
|
|
28
|
+
const workspace = this.store.snapshot().workspaces.find((item) => item.id === conversation.workspaceId);
|
|
29
|
+
if (!workspace)
|
|
30
|
+
throw new Error(`会话所属的工作区 ${conversation.workspaceId} 已不存在,拒绝运行`);
|
|
31
|
+
return { conversation, workspace };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves conversation and workspace working directory for subagent dispatch.
|
|
35
|
+
*/
|
|
36
|
+
dispatchContext(conversationId) {
|
|
37
|
+
const { conversation, workspace } = this.turnContext(conversationId);
|
|
38
|
+
return { conversation, cwd: workspace.path };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks whether a conversation has an active turn running.
|
|
42
|
+
*/
|
|
43
|
+
isRunning(conversationId) {
|
|
44
|
+
return this.activeRuns.has(conversationId);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Aborts all active conversation runs and sub-agents.
|
|
48
|
+
*/
|
|
49
|
+
abortAll() {
|
|
50
|
+
for (const controller of this.activeRuns.values()) {
|
|
51
|
+
controller.abort();
|
|
52
|
+
}
|
|
53
|
+
this.subagents.abortAll();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Stops an active turn run for a conversation.
|
|
57
|
+
*/
|
|
58
|
+
async stopRun(conversationId) {
|
|
59
|
+
this.extensionUiService.cancelPendingUi(conversationId, "aborted");
|
|
60
|
+
this.activeRuns.get(conversationId)?.abort();
|
|
61
|
+
await this.runtime.stop?.(conversationId);
|
|
62
|
+
await this.store.setStatus(conversationId, "stopped");
|
|
63
|
+
if (this.subagents.stopConversation(conversationId)) {
|
|
64
|
+
console.log(`Super Pi: stopped the sub-agent runs of conversation ${conversationId}.`);
|
|
65
|
+
}
|
|
66
|
+
this.broadcaster.broadcastState();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Handles queue snapshot updates from Pi RPC.
|
|
70
|
+
*/
|
|
71
|
+
onPiQueue(conversationId, snapshot) {
|
|
72
|
+
this.broadcaster.broadcast({ type: "pi.queue", conversationId, ...snapshot });
|
|
73
|
+
const held = this.store.listQueuedMessages(conversationId).filter((message) => message.state === "sending").length;
|
|
74
|
+
const delivered = held - snapshot.steering.length;
|
|
75
|
+
if (delivered > 0) {
|
|
76
|
+
void this.store.dropSendingMessages(conversationId, delivered).then(() => this.broadcaster.broadcastState());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Opens a turn by creating user and assistant messages in store.
|
|
81
|
+
*/
|
|
82
|
+
async openTurn(conversationId, prompt, images) {
|
|
83
|
+
const user = await this.store.addUserMessage(conversationId, prompt ?? "", images);
|
|
84
|
+
const assistant = await this.store.beginAssistantMessage(conversationId);
|
|
85
|
+
this.broadcaster.broadcastState();
|
|
86
|
+
return { user, assistant };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Streams a turn via AgentRuntime.
|
|
90
|
+
*/
|
|
91
|
+
async streamTurn(conversation, workspace, prompt, assistant, controller, userMessageId, images) {
|
|
92
|
+
this.extensionUiService.cancelPendingUi(conversation.id, "expired");
|
|
93
|
+
let toolCalls = [];
|
|
94
|
+
let pendingTools = Promise.resolve();
|
|
95
|
+
const enqueueTools = (what, work) => {
|
|
96
|
+
pendingTools = pendingTools.then(async () => {
|
|
97
|
+
toolCalls = await work();
|
|
98
|
+
this.broadcaster.broadcast({ type: "message.tools", conversationId: conversation.id, messageId: assistant.id, toolCalls });
|
|
99
|
+
}).catch((error) => {
|
|
100
|
+
console.warn(`Super Pi: recording ${what} failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const rowByToolCallId = new Map();
|
|
104
|
+
const onToolCall = (call) => {
|
|
105
|
+
if (call.id)
|
|
106
|
+
rowByToolCallId.set(call.id, rowByToolCallId.size);
|
|
107
|
+
enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args }));
|
|
108
|
+
};
|
|
109
|
+
const onToolEnd = (call) => {
|
|
110
|
+
const index = call.id ? rowByToolCallId.get(call.id) : undefined;
|
|
111
|
+
if (index === undefined)
|
|
112
|
+
return;
|
|
113
|
+
enqueueTools("a tool call outcome", () => this.store.settleToolCall(conversation.id, assistant.id, index, { status: call.status, durationMs: call.durationMs }));
|
|
114
|
+
};
|
|
115
|
+
for await (const delta of this.runtime.answer({
|
|
116
|
+
conversation,
|
|
117
|
+
prompt,
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
cwd: workspace.path,
|
|
120
|
+
host: this.extensionUiService.createHost(conversation.id),
|
|
121
|
+
onPiQueue: (snapshot) => this.onPiQueue(conversation.id, snapshot),
|
|
122
|
+
onUserEntry: (entryId) => this.store.setMessagePiEntryId(conversation.id, userMessageId, entryId),
|
|
123
|
+
onToolCall,
|
|
124
|
+
onToolEnd,
|
|
125
|
+
images,
|
|
126
|
+
})) {
|
|
127
|
+
await this.store.appendAssistantDelta(conversation.id, assistant.id, delta);
|
|
128
|
+
this.broadcaster.broadcast({ type: "message.delta", conversationId: conversation.id, messageId: assistant.id, delta });
|
|
129
|
+
}
|
|
130
|
+
await pendingTools;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Loops running turns for as long as queued follow-ups exist.
|
|
134
|
+
*/
|
|
135
|
+
async runConversationTurns(conversation, workspace, prompt, controller, initialImages) {
|
|
136
|
+
try {
|
|
137
|
+
const opened = await this.openTurn(conversation.id, prompt, initialImages);
|
|
138
|
+
await this.streamTurn(conversation, workspace, prompt, opened.assistant, controller, opened.user.id, initialImages);
|
|
139
|
+
for (;;) {
|
|
140
|
+
if (controller.signal.aborted)
|
|
141
|
+
break;
|
|
142
|
+
const next = await this.store.takeNextQueuedMessage(conversation.id);
|
|
143
|
+
if (!next)
|
|
144
|
+
break;
|
|
145
|
+
this.broadcaster.broadcastState();
|
|
146
|
+
let followUp;
|
|
147
|
+
try {
|
|
148
|
+
followUp = await this.openTurn(conversation.id, next.content, next.images);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
await this.store.enqueueMessage(conversation.id, next.content, next.images);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
const current = this.turnContext(conversation.id).conversation;
|
|
155
|
+
await this.streamTurn(current, workspace, next.content, followUp.assistant, controller, followUp.user.id, next.images);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await this.store.releaseSendingMessages(conversation.id);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Enqueues or starts execution for a user message.
|
|
164
|
+
*/
|
|
165
|
+
async enqueueAndRun(conversationId, content, images) {
|
|
166
|
+
const trimmed = content.trim();
|
|
167
|
+
const validImages = Array.isArray(images) && images.length > 0 ? images : undefined;
|
|
168
|
+
if (!trimmed && !validImages?.length)
|
|
169
|
+
return;
|
|
170
|
+
const { conversation, workspace } = this.turnContext(conversationId);
|
|
171
|
+
if (validImages?.length) {
|
|
172
|
+
const catalog = await this.runtime.catalog(conversation.model);
|
|
173
|
+
const currentModel = catalog.models.find((model) => model.provider === conversation.model?.provider && model.modelId === conversation.model?.modelId)
|
|
174
|
+
?? catalog.models.find((m) => m.provider === catalog.defaultModel?.provider && m.modelId === catalog.defaultModel?.modelId);
|
|
175
|
+
if (currentModel && !currentModel.imageInput) {
|
|
176
|
+
throw new Error(`当前选择的模型「${currentModel.label || currentModel.modelId}」不支持图片输入`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (this.activeRuns.has(conversation.id)) {
|
|
180
|
+
await this.store.enqueueMessage(conversation.id, trimmed, validImages);
|
|
181
|
+
this.broadcaster.broadcastState();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
this.activeRuns.set(conversation.id, controller);
|
|
186
|
+
try {
|
|
187
|
+
await this.runConversationTurns(conversation, workspace, trimmed, controller, validImages);
|
|
188
|
+
await this.store.setStatus(conversation.id, controller.signal.aborted ? "stopped" : "completed");
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
await this.store.setStatus(conversation.id, "failed");
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
this.activeRuns.delete(conversation.id);
|
|
196
|
+
this.extensionUiService.cancelPendingUi(conversation.id, "expired");
|
|
197
|
+
this.broadcaster.broadcastState();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Steers a queued message into the in-flight turn.
|
|
202
|
+
*/
|
|
203
|
+
async steer(conversationId, queueId) {
|
|
204
|
+
if (!this.runtime.steer) {
|
|
205
|
+
throw new Error("当前运行时没有插队能力,无法把排队消息插到正在运行的这轮里");
|
|
206
|
+
}
|
|
207
|
+
const claimed = await this.store.claimQueuedMessage(conversationId, queueId);
|
|
208
|
+
this.broadcaster.broadcastState();
|
|
209
|
+
try {
|
|
210
|
+
const disposition = await this.runtime.steer(conversationId, claimed.content, claimed.images);
|
|
211
|
+
if (disposition === "handled") {
|
|
212
|
+
await this.store.dropSendingMessages(conversationId, 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
await this.store.releaseQueuedMessage(claimed.id);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
this.broadcaster.broadcastState();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Cancels a queued message.
|
|
223
|
+
*/
|
|
224
|
+
async cancelQueued(conversationId, queueId) {
|
|
225
|
+
await this.store.cancelQueuedMessage(conversationId, queueId);
|
|
226
|
+
this.broadcaster.broadcastState();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Dispatches a subagent from client command (human dispatch).
|
|
230
|
+
*/
|
|
231
|
+
async dispatchSubagent(params) {
|
|
232
|
+
const task = params.task.trim();
|
|
233
|
+
if (!task)
|
|
234
|
+
throw new Error("子 agent 的任务不能为空");
|
|
235
|
+
const { conversation, cwd } = this.dispatchContext(params.conversationId);
|
|
236
|
+
void this.subagents.dispatch({
|
|
237
|
+
conversationId: conversation.id,
|
|
238
|
+
agentId: params.agentId,
|
|
239
|
+
task,
|
|
240
|
+
cwd,
|
|
241
|
+
model: conversation.model,
|
|
242
|
+
denyTools: parentDeniedTools(await this.runtime.agentFor?.(conversation)),
|
|
243
|
+
}).catch((error) => {
|
|
244
|
+
console.warn(`Super Pi: sub-agent dispatch of ${params.agentId} failed before it could be recorded: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
245
|
+
this.broadcaster.broadcast({
|
|
246
|
+
type: "error",
|
|
247
|
+
message: `无法派发子 agent ${JSON.stringify(params.agentId)}:${error instanceof Error ? error.message : "未知错误"}`,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Dispatches a subagent task requested by the task tool HTTP endpoint.
|
|
253
|
+
*/
|
|
254
|
+
async dispatchTask(body) {
|
|
255
|
+
const conversationId = body.conversationId?.trim();
|
|
256
|
+
const subagentType = body.subagent_type?.trim();
|
|
257
|
+
const task = body.prompt?.trim();
|
|
258
|
+
if (!conversationId || !subagentType || !task) {
|
|
259
|
+
throw new Error("task requires conversationId, subagent_type and prompt");
|
|
260
|
+
}
|
|
261
|
+
if (body.depth !== undefined)
|
|
262
|
+
checkSubagentDepth(body.depth);
|
|
263
|
+
const { conversation, cwd } = this.dispatchContext(conversationId);
|
|
264
|
+
const parent = body.parentRunId ? this.store.snapshot().subagentRuns.find((run) => run.id === body.parentRunId) : undefined;
|
|
265
|
+
if (body.parentRunId && (!parent || parent.conversationId !== conversation.id)) {
|
|
266
|
+
throw new Error("Invalid parentRunId: dispatch ancestry is not recorded for this conversation");
|
|
267
|
+
}
|
|
268
|
+
const authoritativeDepth = parent ? (parent.depth ?? 0) + 1 : 0;
|
|
269
|
+
checkSubagentDepth(authoritativeDepth);
|
|
270
|
+
if (parent)
|
|
271
|
+
checkSubagentDepth(1);
|
|
272
|
+
const backgroundId = body.background === true ? newSubagentRunId() : undefined;
|
|
273
|
+
const dispatch = this.subagents.dispatch({
|
|
274
|
+
...(backgroundId ? { id: backgroundId } : {}),
|
|
275
|
+
conversationId: conversation.id,
|
|
276
|
+
agentId: subagentType,
|
|
277
|
+
task,
|
|
278
|
+
cwd,
|
|
279
|
+
model: conversation.model,
|
|
280
|
+
dispatchedBy: "model",
|
|
281
|
+
...(parent ? { parentRunId: parent.id } : {}),
|
|
282
|
+
depth: authoritativeDepth,
|
|
283
|
+
...(body.loopId ? { loopId: body.loopId } : {}),
|
|
284
|
+
...(body.role ? { role: body.role } : {}),
|
|
285
|
+
denyTools: parentDeniedTools(await this.runtime.agentFor?.(conversation)),
|
|
286
|
+
});
|
|
287
|
+
if (body.background === true) {
|
|
288
|
+
await this.store.setBackgroundTask({
|
|
289
|
+
id: backgroundId,
|
|
290
|
+
title: body.description?.trim() || subagentType,
|
|
291
|
+
agentId: subagentType,
|
|
292
|
+
status: "running",
|
|
293
|
+
startedAt: new Date().toISOString(),
|
|
294
|
+
resultBlockId: backgroundId,
|
|
295
|
+
});
|
|
296
|
+
this.broadcaster.broadcastState();
|
|
297
|
+
void dispatch.then(async (run) => {
|
|
298
|
+
const result = run.status === "completed"
|
|
299
|
+
? run.output ?? ""
|
|
300
|
+
: `Background task failed: ${run.error ?? run.status}`;
|
|
301
|
+
const tag = run.status === "completed" ? "task_result" : "task_error";
|
|
302
|
+
const message = `<task id="${run.id}" state="${run.status === "completed" ? "completed" : "error"}">\n<summary>Background task ${run.status === "completed" ? "completed" : "failed"}: ${body.description?.trim() || subagentType}</summary>\n<${tag}>\n${result}\n</${tag}>\n</task>`;
|
|
303
|
+
await this.store.enqueueMessage(conversation.id, message);
|
|
304
|
+
await this.store.removeBackgroundTask(run.id);
|
|
305
|
+
this.broadcaster.broadcastState();
|
|
306
|
+
}).catch(async (error) => {
|
|
307
|
+
await this.store.removeBackgroundTask(backgroundId);
|
|
308
|
+
this.broadcaster.broadcastState();
|
|
309
|
+
console.warn(`Super Pi: background sub-agent dispatch failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
mode: "async",
|
|
313
|
+
status: 202,
|
|
314
|
+
body: { run: { id: backgroundId, status: "running", agentId: subagentType }, background: true },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const run = await dispatch;
|
|
318
|
+
return {
|
|
319
|
+
mode: "sync",
|
|
320
|
+
status: run.status === "completed" ? 200 : 422,
|
|
321
|
+
body: {
|
|
322
|
+
run: { id: run.id, status: run.status, agentId: run.agentId },
|
|
323
|
+
output: run.output ?? "",
|
|
324
|
+
error: run.error,
|
|
325
|
+
outputTruncated: run.outputTruncated ?? false,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Stops a specific subagent run by runId.
|
|
331
|
+
*/
|
|
332
|
+
stopSubagent(runId) {
|
|
333
|
+
this.subagents.stop(runId);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Stops all subagent runs belonging to a conversation.
|
|
337
|
+
*/
|
|
338
|
+
stopConversationSubagents(conversationId) {
|
|
339
|
+
return this.subagents.stopConversation(conversationId);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Store } from "../store.js";
|
|
2
|
+
import type { AgentRuntime } from "../runtime.js";
|
|
3
|
+
import type { EventBroadcaster } from "../server-context.js";
|
|
4
|
+
import type { CatalogService } from "./catalog-service.js";
|
|
5
|
+
import type { RunService } from "./run-service.js";
|
|
6
|
+
import type { ExtensionUiService } from "./extension-ui-service.js";
|
|
7
|
+
export interface WorkspaceSettingsUpdate {
|
|
8
|
+
model?: {
|
|
9
|
+
provider: string;
|
|
10
|
+
modelId: string;
|
|
11
|
+
} | null;
|
|
12
|
+
thinkingLevel?: string | null;
|
|
13
|
+
agentId?: string | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Service managing workspaces: creation, rename, cascade deletion, and defaults configuration.
|
|
17
|
+
*/
|
|
18
|
+
export declare class WorkspaceService {
|
|
19
|
+
private readonly store;
|
|
20
|
+
private readonly runtime;
|
|
21
|
+
private readonly broadcaster;
|
|
22
|
+
private readonly catalogService;
|
|
23
|
+
private readonly runService;
|
|
24
|
+
private readonly extensionUiService;
|
|
25
|
+
private readonly uploadsDir;
|
|
26
|
+
private readonly agentsDir;
|
|
27
|
+
constructor(store: Store, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, uploadsDir: string, agentsDir: string);
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new workspace.
|
|
30
|
+
*/
|
|
31
|
+
createWorkspace(name: string, path?: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Renames an existing workspace.
|
|
34
|
+
*/
|
|
35
|
+
renameWorkspace(workspaceId: string, name: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Deletes a workspace and cascades cleanup to active runs, subagent runs,
|
|
38
|
+
* uploaded files, and stored conversations.
|
|
39
|
+
*/
|
|
40
|
+
deleteWorkspace(workspaceId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Updates workspace default settings (model, thinking level, default agent).
|
|
43
|
+
*/
|
|
44
|
+
updateSettings(workspaceId: string, settings: WorkspaceSettingsUpdate): Promise<void>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { deleteConversationUploads } from "../upload.js";
|
|
2
|
+
/**
|
|
3
|
+
* Service managing workspaces: creation, rename, cascade deletion, and defaults configuration.
|
|
4
|
+
*/
|
|
5
|
+
export class WorkspaceService {
|
|
6
|
+
store;
|
|
7
|
+
runtime;
|
|
8
|
+
broadcaster;
|
|
9
|
+
catalogService;
|
|
10
|
+
runService;
|
|
11
|
+
extensionUiService;
|
|
12
|
+
uploadsDir;
|
|
13
|
+
agentsDir;
|
|
14
|
+
constructor(store, runtime, broadcaster, catalogService, runService, extensionUiService, uploadsDir, agentsDir) {
|
|
15
|
+
this.store = store;
|
|
16
|
+
this.runtime = runtime;
|
|
17
|
+
this.broadcaster = broadcaster;
|
|
18
|
+
this.catalogService = catalogService;
|
|
19
|
+
this.runService = runService;
|
|
20
|
+
this.extensionUiService = extensionUiService;
|
|
21
|
+
this.uploadsDir = uploadsDir;
|
|
22
|
+
this.agentsDir = agentsDir;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new workspace.
|
|
26
|
+
*/
|
|
27
|
+
async createWorkspace(name, path) {
|
|
28
|
+
await this.store.createWorkspace(name, path);
|
|
29
|
+
this.broadcaster.broadcastState();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Renames an existing workspace.
|
|
33
|
+
*/
|
|
34
|
+
async renameWorkspace(workspaceId, name) {
|
|
35
|
+
await this.store.renameWorkspace(workspaceId, name);
|
|
36
|
+
this.broadcaster.broadcastState();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Deletes a workspace and cascades cleanup to active runs, subagent runs,
|
|
40
|
+
* uploaded files, and stored conversations.
|
|
41
|
+
*/
|
|
42
|
+
async deleteWorkspace(workspaceId) {
|
|
43
|
+
const snapshot = this.store.snapshot();
|
|
44
|
+
const doomed = snapshot.conversations.filter((c) => c.workspaceId === workspaceId);
|
|
45
|
+
for (const c of doomed) {
|
|
46
|
+
this.extensionUiService.cancelPendingUi(c.id, "aborted");
|
|
47
|
+
if (this.runService.isRunning(c.id)) {
|
|
48
|
+
await this.runService.stopRun(c.id);
|
|
49
|
+
}
|
|
50
|
+
this.runService.stopConversationSubagents(c.id);
|
|
51
|
+
}
|
|
52
|
+
const doomedIds = await this.store.deleteWorkspace(workspaceId);
|
|
53
|
+
await Promise.all(doomedIds.map((id) => deleteConversationUploads(this.uploadsDir, id)));
|
|
54
|
+
this.broadcaster.broadcastState();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Updates workspace default settings (model, thinking level, default agent).
|
|
58
|
+
*/
|
|
59
|
+
async updateSettings(workspaceId, settings) {
|
|
60
|
+
const workspace = this.store.snapshot().workspaces.find((item) => item.id === workspaceId);
|
|
61
|
+
if (!workspace)
|
|
62
|
+
throw new Error("Workspace not found");
|
|
63
|
+
let selection;
|
|
64
|
+
const requested = settings.model;
|
|
65
|
+
if (requested) {
|
|
66
|
+
const catalog = await this.runtime.catalog();
|
|
67
|
+
const option = catalog.source === "pi"
|
|
68
|
+
? catalog.models.find((model) => model.provider === requested.provider && model.modelId === requested.modelId)
|
|
69
|
+
: undefined;
|
|
70
|
+
if (catalog.source === "pi" && !option) {
|
|
71
|
+
throw this.catalogService.notOffered(`模型 ${requested.provider}/${requested.modelId}`, catalog);
|
|
72
|
+
}
|
|
73
|
+
selection = option
|
|
74
|
+
? { provider: option.provider, modelId: option.modelId, label: option.label }
|
|
75
|
+
: { provider: requested.provider, modelId: requested.modelId, label: requested.modelId };
|
|
76
|
+
}
|
|
77
|
+
if (settings.thinkingLevel) {
|
|
78
|
+
const catalog = await this.runtime.catalog(selection ?? workspace.defaultModel);
|
|
79
|
+
if (catalog.source === "pi" && !catalog.levels.includes(settings.thinkingLevel)) {
|
|
80
|
+
throw this.catalogService.notOffered(`思考等级 ${settings.thinkingLevel}`, catalog);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let agentToStore;
|
|
84
|
+
if (settings.agentId !== undefined) {
|
|
85
|
+
if (settings.agentId === null) {
|
|
86
|
+
agentToStore = null;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (!this.runtime.agentFor) {
|
|
90
|
+
throw new Error("当前运行时没有 agent 机制,无法设置默认 agent");
|
|
91
|
+
}
|
|
92
|
+
const agent = await this.runtime.agentFor({ id: workspaceId, agentId: settings.agentId });
|
|
93
|
+
if (!agent) {
|
|
94
|
+
throw new Error(`Agent ${JSON.stringify(settings.agentId)} 不存在或不是可用的 primary agent(目录:${this.agentsDir})`);
|
|
95
|
+
}
|
|
96
|
+
agentToStore = agent.id;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await this.store.setWorkspaceDefaults(workspaceId, {
|
|
100
|
+
model: selection,
|
|
101
|
+
thinkingLevel: settings.thinkingLevel ?? undefined,
|
|
102
|
+
agentId: agentToStore,
|
|
103
|
+
});
|
|
104
|
+
this.broadcaster.broadcastState();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClientCommand, ServerEvent } from "./types.js";
|
|
2
|
+
import type { WorkspaceService } from "./application/workspace-service.js";
|
|
3
|
+
import type { ConversationService } from "./application/conversation-service.js";
|
|
4
|
+
import type { RunService } from "./application/run-service.js";
|
|
5
|
+
import type { ExtensionUiService } from "./application/extension-ui-service.js";
|
|
6
|
+
import type { CatalogService } from "./application/catalog-service.js";
|
|
7
|
+
export type SocketSender = (event: ServerEvent) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Routes client commands to appropriate application services and converts results or errors.
|
|
10
|
+
*/
|
|
11
|
+
export declare class CommandHandler {
|
|
12
|
+
private readonly workspaceService;
|
|
13
|
+
private readonly conversationService;
|
|
14
|
+
private readonly runService;
|
|
15
|
+
private readonly extensionUiService;
|
|
16
|
+
private readonly catalogService;
|
|
17
|
+
constructor(workspaceService: WorkspaceService, conversationService: ConversationService, runService: RunService, extensionUiService: ExtensionUiService, catalogService: CatalogService);
|
|
18
|
+
/**
|
|
19
|
+
* Processes a client command.
|
|
20
|
+
*/
|
|
21
|
+
handleCommand(command: ClientCommand, sendToSocket?: SocketSender): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes client commands to appropriate application services and converts results or errors.
|
|
3
|
+
*/
|
|
4
|
+
export class CommandHandler {
|
|
5
|
+
workspaceService;
|
|
6
|
+
conversationService;
|
|
7
|
+
runService;
|
|
8
|
+
extensionUiService;
|
|
9
|
+
catalogService;
|
|
10
|
+
constructor(workspaceService, conversationService, runService, extensionUiService, catalogService) {
|
|
11
|
+
this.workspaceService = workspaceService;
|
|
12
|
+
this.conversationService = conversationService;
|
|
13
|
+
this.runService = runService;
|
|
14
|
+
this.extensionUiService = extensionUiService;
|
|
15
|
+
this.catalogService = catalogService;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Processes a client command.
|
|
19
|
+
*/
|
|
20
|
+
async handleCommand(command, sendToSocket) {
|
|
21
|
+
if (command.type === "workspace.create") {
|
|
22
|
+
await this.workspaceService.createWorkspace(command.name, command.path);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (command.type === "workspace.rename") {
|
|
26
|
+
await this.workspaceService.renameWorkspace(command.workspaceId, command.name);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (command.type === "workspace.delete") {
|
|
30
|
+
await this.workspaceService.deleteWorkspace(command.workspaceId);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (command.type === "workspace.settings") {
|
|
34
|
+
await this.workspaceService.updateSettings(command.workspaceId, {
|
|
35
|
+
model: command.model,
|
|
36
|
+
thinkingLevel: command.thinkingLevel,
|
|
37
|
+
agentId: command.agentId,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (command.type === "conversation.create") {
|
|
42
|
+
await this.conversationService.createConversation(command.workspaceId);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (command.type === "conversation.select") {
|
|
46
|
+
await this.conversationService.selectConversation(command.conversationId);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (command.type === "conversation.archive") {
|
|
50
|
+
await this.conversationService.archiveConversation(command.conversationId);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (command.type === "conversation.unarchive") {
|
|
54
|
+
await this.conversationService.unarchiveConversation(command.conversationId);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (command.type === "conversation.rename") {
|
|
58
|
+
await this.conversationService.renameConversation(command.conversationId, command.name);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (command.type === "conversation.fork") {
|
|
62
|
+
const result = await this.conversationService.forkConversation(command.conversationId);
|
|
63
|
+
if (sendToSocket) {
|
|
64
|
+
sendToSocket({
|
|
65
|
+
type: "conversation.fork.result",
|
|
66
|
+
conversationId: command.conversationId,
|
|
67
|
+
...result,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command.type === "conversation.model") {
|
|
73
|
+
await this.conversationService.setModel(command.conversationId, command.provider, command.modelId);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (command.type === "conversation.thinking") {
|
|
77
|
+
await this.conversationService.setThinking(command.conversationId, command.level);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (command.type === "conversation.agent") {
|
|
81
|
+
await this.conversationService.setAgent(command.conversationId, command.agentId);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (command.type === "message.send") {
|
|
85
|
+
await this.runService.enqueueAndRun(command.conversationId, command.content, command.images);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (command.type === "message.steer") {
|
|
89
|
+
await this.runService.steer(command.conversationId, command.queueId);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (command.type === "message.cancel") {
|
|
93
|
+
await this.runService.cancelQueued(command.conversationId, command.queueId);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (command.type === "message.revert") {
|
|
97
|
+
await this.conversationService.revertMessage(command.conversationId, command.messageId);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (command.type === "run.stop") {
|
|
101
|
+
await this.runService.stopRun(command.conversationId);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (command.type === "subagent.run") {
|
|
105
|
+
await this.runService.dispatchSubagent({
|
|
106
|
+
conversationId: command.conversationId,
|
|
107
|
+
agentId: command.agentId,
|
|
108
|
+
task: command.task,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (command.type === "subagent.stop") {
|
|
113
|
+
this.runService.stopSubagent(command.runId);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (command.type === "extension.ui.answer") {
|
|
117
|
+
const answer = command.cancelled
|
|
118
|
+
? { cancelled: true }
|
|
119
|
+
: { value: command.value, confirmed: command.confirmed };
|
|
120
|
+
this.extensionUiService.answerUi(command.conversationId, command.requestId, answer);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (command.type === "catalog.fetch") {
|
|
124
|
+
await this.catalogService.broadcastCatalog();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (command.type === "agent.catalog.fetch") {
|
|
128
|
+
await this.catalogService.broadcastAgents();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (command.type === "subagent.catalog.fetch") {
|
|
132
|
+
await this.catalogService.broadcastSubagents();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`未知的命令类型:${JSON.stringify(command.type ?? null)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|