@evandu/super-pi 0.1.139 → 0.1.140
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/agents/super-pi-task/core.ts +418 -1
- package/agents/super-pi-task/index.ts +115 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +6 -5
- package/dist/client/assets/{CodeMirrorEditor-H7SHGhdS.js → CodeMirrorEditor-DvZeOLny.js} +2 -2
- package/dist/client/assets/{dist-CukPKrEn.js → dist-3BuyVG3H.js} +1 -1
- package/dist/client/assets/{dist-cP2vnTP_.js → dist-BCUfH4nP.js} +1 -1
- package/dist/client/assets/{dist-C-MUXK_t.js → dist-BIIMtU9W.js} +1 -1
- package/dist/client/assets/{dist-BfUeRJKr.js → dist-C-QmRwbd.js} +1 -1
- package/dist/client/assets/{dist-CZ9Krsno.js → dist-CTkzLBdb.js} +1 -1
- package/dist/client/assets/{dist-BxTGjU84.js → dist-C_VuSpx8.js} +1 -1
- package/dist/client/assets/{dist-NlopeJ5R.js → dist-CaUsvSz1.js} +1 -1
- package/dist/client/assets/{dist-zUJqdbfo.js → dist-Cb1gxS57.js} +1 -1
- package/dist/client/assets/{dist-C8NllalC.js → dist-CqDSeoUk.js} +1 -1
- package/dist/client/assets/{dist-DLKVFZhO.js → dist-D7yfnKNC.js} +1 -1
- package/dist/client/assets/{dist-CUNctZ5v.js → dist-DA8yW4lK.js} +1 -1
- package/dist/client/assets/{dist-Jw-GQ-re.js → dist-DAcS4Kj8.js} +1 -1
- package/dist/client/assets/{dist-C_ftUQZN.js → dist-DN0M4DAX.js} +1 -1
- package/dist/client/assets/{dist-2XXLO-IZ.js → dist-DRgWZqFH.js} +1 -1
- package/dist/client/assets/{dist-POoQ4j29.js → dist-DdfW8Gjv.js} +1 -1
- package/dist/client/assets/{dist-BEM6tttY.js → dist-DlQ8PMvv.js} +1 -1
- package/dist/client/assets/{dist-CSdzvQnf.js → dist-Dvca0LeR.js} +1 -1
- package/dist/client/assets/{dist-DJ8SwQl_.js → dist-HJdP1yxN.js} +1 -1
- package/dist/client/assets/{dist-CuMnZOGE.js → dist-gBAr9yXF.js} +1 -1
- package/dist/client/assets/{dist-BokzcFLE.js → dist-gyzurUDR.js} +1 -1
- package/dist/client/assets/{dist-BoucVzKA.js → dist-qG53hT1b.js} +1 -1
- package/dist/client/assets/{dist-D3Ta0y4e.js → dist-r4Kq1ABA.js} +1 -1
- package/dist/client/assets/{index-B32VZVYx.js → index-CNbh1fL2.js} +9 -9
- package/dist/client/assets/index-DCSFD4VV.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/session-orchestrator-service.d.ts +113 -0
- package/dist/server/application/session-orchestrator-service.js +234 -0
- package/dist/server/http-server.js +135 -0
- package/dist/server/index.js +20 -2
- package/dist/server/runtime.d.ts +1 -0
- package/dist/server/runtime.js +6 -2
- package/dist/server/server-context.d.ts +2 -0
- package/dist/server/session-ledger.d.ts +60 -0
- package/dist/server/session-ledger.js +76 -0
- package/dist/server/subagent-runs.d.ts +31 -1
- package/dist/server/subagent-runs.js +30 -13
- package/dist/server/task-tool.d.ts +18 -0
- package/dist/server/task-tool.js +85 -0
- package/dist/server/types.d.ts +23 -0
- package/dist/server/ws-server.d.ts +52 -1
- package/dist/server/ws-server.js +151 -35
- package/package.json +3 -2
- package/dist/client/assets/index-Bdym8BPo.css +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator service coordinating sibling and child conversations.
|
|
3
|
+
*
|
|
4
|
+
* Provides lifecycle methods (spawn, dispatch, status, list, stop, resume) without
|
|
5
|
+
* duplicating message history or inflating memory.
|
|
6
|
+
*/
|
|
7
|
+
export class SessionOrchestratorService {
|
|
8
|
+
store;
|
|
9
|
+
conversationService;
|
|
10
|
+
runService;
|
|
11
|
+
ledger;
|
|
12
|
+
broadcaster;
|
|
13
|
+
constructor(store, conversationService, runService, ledger, broadcaster) {
|
|
14
|
+
this.store = store;
|
|
15
|
+
this.conversationService = conversationService;
|
|
16
|
+
this.runService = runService;
|
|
17
|
+
this.ledger = ledger;
|
|
18
|
+
this.broadcaster = broadcaster;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Spawns a new sibling conversation and immediately dispatches its initial task.
|
|
22
|
+
*/
|
|
23
|
+
async spawn(options) {
|
|
24
|
+
let workspaceId = options.workspaceId;
|
|
25
|
+
if (!workspaceId && options.parentConversationId) {
|
|
26
|
+
const parent = this.store.conversation(options.parentConversationId);
|
|
27
|
+
workspaceId = parent?.workspaceId;
|
|
28
|
+
}
|
|
29
|
+
if (!workspaceId) {
|
|
30
|
+
throw new Error("创建兄弟会话需要指定工作区(或提供有效的父会话 ID)。");
|
|
31
|
+
}
|
|
32
|
+
const conversationId = await this.conversationService.createConversation(workspaceId);
|
|
33
|
+
// Set title if requested or derive from task
|
|
34
|
+
if (options.title) {
|
|
35
|
+
await this.conversationService.renameConversation(conversationId, options.title);
|
|
36
|
+
}
|
|
37
|
+
else if (options.task) {
|
|
38
|
+
const titleSnippet = options.task.trim().replace(/\s+/g, " ").slice(0, 30);
|
|
39
|
+
await this.conversationService.renameConversation(conversationId, `任务:${titleSnippet}`);
|
|
40
|
+
}
|
|
41
|
+
// Apply configuration overrides if provided
|
|
42
|
+
if (options.agentId) {
|
|
43
|
+
await this.conversationService.setAgent(conversationId, options.agentId);
|
|
44
|
+
}
|
|
45
|
+
if (options.model) {
|
|
46
|
+
await this.conversationService.setModel(conversationId, options.model.provider, options.model.modelId);
|
|
47
|
+
}
|
|
48
|
+
if (options.thinkingLevel) {
|
|
49
|
+
await this.conversationService.setThinking(conversationId, options.thinkingLevel);
|
|
50
|
+
}
|
|
51
|
+
// Register in relationship ledger
|
|
52
|
+
this.ledger.register({
|
|
53
|
+
conversationId,
|
|
54
|
+
workspaceId,
|
|
55
|
+
origin: options.parentConversationId ? "orchestrator" : "user",
|
|
56
|
+
parentConversationId: options.parentConversationId,
|
|
57
|
+
task: options.task,
|
|
58
|
+
status: "idle",
|
|
59
|
+
});
|
|
60
|
+
// Immediately dispatch initial work
|
|
61
|
+
const prompt = options.initialPrompt ?? options.task;
|
|
62
|
+
if (prompt && prompt.trim()) {
|
|
63
|
+
await this.dispatch(conversationId, prompt, { wait: options.wait });
|
|
64
|
+
}
|
|
65
|
+
return { conversationId };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Dispatches a message to an arbitrary session (server-to-server).
|
|
69
|
+
* By default dispatches asynchronously so the orchestrator caller is not blocked.
|
|
70
|
+
*/
|
|
71
|
+
async dispatch(conversationId, content, options) {
|
|
72
|
+
const conversation = this.store.conversation(conversationId);
|
|
73
|
+
if (!conversation) {
|
|
74
|
+
throw new Error(`找不到会话:${conversationId}`);
|
|
75
|
+
}
|
|
76
|
+
this.ledger.update(conversationId, {
|
|
77
|
+
status: "running",
|
|
78
|
+
});
|
|
79
|
+
const runPromise = this.runService.enqueueAndRun(conversationId, content);
|
|
80
|
+
if (options?.wait) {
|
|
81
|
+
await runPromise;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
void runPromise.catch((error) => {
|
|
85
|
+
console.warn(`Super Pi: dispatch failed for conversation ${conversationId}:`, error instanceof Error ? error.message : String(error));
|
|
86
|
+
this.ledger.update(conversationId, { status: "failed" });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { conversationId };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns a high-level summary of a session's progress without returning full transcripts.
|
|
93
|
+
* Uses narrow store reader `store.conversation(id)` to avoid cloning the entire state.
|
|
94
|
+
*/
|
|
95
|
+
async status(conversationId) {
|
|
96
|
+
const conversation = this.store.conversation(conversationId);
|
|
97
|
+
if (!conversation) {
|
|
98
|
+
throw new Error(`找不到会话:${conversationId}`);
|
|
99
|
+
}
|
|
100
|
+
const ledgerEntry = this.ledger.get(conversationId);
|
|
101
|
+
const messages = conversation.messages ?? [];
|
|
102
|
+
let turnCount = 0;
|
|
103
|
+
let lastAssistantSnippet;
|
|
104
|
+
let lastUserSnippet;
|
|
105
|
+
let activeTool;
|
|
106
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
107
|
+
const msg = messages[i];
|
|
108
|
+
if (!lastAssistantSnippet && msg.role === "assistant") {
|
|
109
|
+
const raw = msg.content || (msg.thinkingBlocks ? msg.thinkingBlocks.map((b) => b.text).join(" ") : "") || "";
|
|
110
|
+
lastAssistantSnippet = raw.slice(0, 300);
|
|
111
|
+
}
|
|
112
|
+
if (!lastUserSnippet && msg.role === "user") {
|
|
113
|
+
lastUserSnippet = (msg.content || "").slice(0, 300);
|
|
114
|
+
}
|
|
115
|
+
if (msg.role === "assistant") {
|
|
116
|
+
turnCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (conversation.status === "running" || conversation.status === "waiting") {
|
|
120
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
121
|
+
if (lastAssistant?.toolCalls?.length) {
|
|
122
|
+
const lastCall = lastAssistant.toolCalls[lastAssistant.toolCalls.length - 1];
|
|
123
|
+
if (!lastCall.status || lastCall.live || lastCall.preparing) {
|
|
124
|
+
activeTool = { label: lastCall.label, detail: lastCall.detail };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const hasPendingQuestion = this.store.pendingQuestions(conversationId).length > 0;
|
|
129
|
+
// Sync ledger status if present
|
|
130
|
+
if (ledgerEntry && ledgerEntry.status !== conversation.status) {
|
|
131
|
+
this.ledger.update(conversationId, {
|
|
132
|
+
status: conversation.status,
|
|
133
|
+
lastProgress: lastAssistantSnippet ?? ledgerEntry.lastProgress,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
conversationId: conversation.id,
|
|
138
|
+
workspaceId: conversation.workspaceId,
|
|
139
|
+
title: conversation.title,
|
|
140
|
+
status: conversation.status,
|
|
141
|
+
origin: ledgerEntry?.origin ?? "user",
|
|
142
|
+
parentConversationId: ledgerEntry?.parentConversationId,
|
|
143
|
+
task: ledgerEntry?.task,
|
|
144
|
+
messageCount: messages.length,
|
|
145
|
+
turnCount,
|
|
146
|
+
lastAssistantSnippet,
|
|
147
|
+
lastUserSnippet,
|
|
148
|
+
hasPendingQuestion,
|
|
149
|
+
activeTool,
|
|
150
|
+
error: conversation.error,
|
|
151
|
+
updatedAt: conversation.updatedAt,
|
|
152
|
+
createdAt: ledgerEntry?.createdAt,
|
|
153
|
+
lastProgress: ledgerEntry?.lastProgress ?? lastAssistantSnippet,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Lists summaries of all sessions, optionally filtered by parent conversation.
|
|
158
|
+
*/
|
|
159
|
+
async list(parentConversationId) {
|
|
160
|
+
const entries = parentConversationId
|
|
161
|
+
? this.ledger.listByParent(parentConversationId)
|
|
162
|
+
: this.ledger.listAll();
|
|
163
|
+
const summaries = [];
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
try {
|
|
166
|
+
const summary = await this.status(entry.conversationId);
|
|
167
|
+
summaries.push(summary);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Conversation was removed from store; prune ledger
|
|
171
|
+
this.ledger.delete(entry.conversationId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return summaries;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Stops a target session.
|
|
178
|
+
* Can distinguish between interrupting an active turn, cancelling queued messages, or both.
|
|
179
|
+
*/
|
|
180
|
+
async stop(conversationId, options) {
|
|
181
|
+
const conversation = this.store.conversation(conversationId);
|
|
182
|
+
if (!conversation) {
|
|
183
|
+
throw new Error(`找不到会话:${conversationId}`);
|
|
184
|
+
}
|
|
185
|
+
let mode = options?.mode ?? "all";
|
|
186
|
+
if (options?.messageId) {
|
|
187
|
+
await this.runService.interruptQueued(conversationId, options.messageId);
|
|
188
|
+
mode = "queued-interrupt";
|
|
189
|
+
}
|
|
190
|
+
else if (mode === "queued") {
|
|
191
|
+
const waiting = this.store.listWaitingMessages(conversationId);
|
|
192
|
+
for (const msg of waiting) {
|
|
193
|
+
await this.runService.cancelQueued(conversationId, msg.id);
|
|
194
|
+
}
|
|
195
|
+
mode = "queued-cancel";
|
|
196
|
+
}
|
|
197
|
+
else if (mode === "active") {
|
|
198
|
+
await this.runService.stopRun(conversationId);
|
|
199
|
+
mode = "active-stop";
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// mode === "all"
|
|
203
|
+
await this.runService.stopRun(conversationId);
|
|
204
|
+
const waiting = this.store.listWaitingMessages(conversationId);
|
|
205
|
+
for (const msg of waiting) {
|
|
206
|
+
await this.runService.cancelQueued(conversationId, msg.id);
|
|
207
|
+
}
|
|
208
|
+
mode = "all";
|
|
209
|
+
}
|
|
210
|
+
this.ledger.update(conversationId, { status: "stopped" });
|
|
211
|
+
return { stopped: true, mode };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Resumes execution in a conversation.
|
|
215
|
+
* If prompt is provided, sends it as a new turn.
|
|
216
|
+
* Otherwise, if there is a waiting message in the queue, kicks off its execution.
|
|
217
|
+
*/
|
|
218
|
+
async resume(conversationId, prompt, options) {
|
|
219
|
+
const conversation = this.store.conversation(conversationId);
|
|
220
|
+
if (!conversation) {
|
|
221
|
+
throw new Error(`找不到会话:${conversationId}`);
|
|
222
|
+
}
|
|
223
|
+
if (prompt && prompt.trim()) {
|
|
224
|
+
return this.dispatch(conversationId, prompt, options);
|
|
225
|
+
}
|
|
226
|
+
const waiting = this.store.listWaitingMessages(conversationId).find((item) => item.delivery === "waiting");
|
|
227
|
+
if (waiting) {
|
|
228
|
+
await this.runService.sendQueued(conversationId, waiting.id);
|
|
229
|
+
this.ledger.update(conversationId, { status: "running" });
|
|
230
|
+
return { conversationId };
|
|
231
|
+
}
|
|
232
|
+
throw new Error("该会话没有排队的消息可恢复,请提供 prompt 发送新消息。");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -1452,6 +1452,141 @@ export function createHttpServer(options) {
|
|
|
1452
1452
|
}
|
|
1453
1453
|
return;
|
|
1454
1454
|
}
|
|
1455
|
+
if (url.pathname === "/api/orchestrator") {
|
|
1456
|
+
if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
|
|
1457
|
+
response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }).end("Orchestrator requires a loopback or private network POST");
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const asked = await readRequestText(request, MAX_FILE_BYTES * 2);
|
|
1461
|
+
if (asked.tooLarge) {
|
|
1462
|
+
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const json = (status, body) => {
|
|
1466
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1467
|
+
};
|
|
1468
|
+
try {
|
|
1469
|
+
const body = JSON.parse(asked.text);
|
|
1470
|
+
const service = context.sessionOrchestratorService;
|
|
1471
|
+
if (!service) {
|
|
1472
|
+
json(503, { error: "编排服务未就绪" });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (body.capability && body.parentConversationId) {
|
|
1476
|
+
const verdict = runService.verifyPrimaryCapability(body.parentConversationId, body.capability);
|
|
1477
|
+
if (!verdict.ok) {
|
|
1478
|
+
json(403, { error: verdict.message });
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
else if (context.accessToken && !isAuthorized(request, context.accessToken)) {
|
|
1483
|
+
json(401, { error: "未授权" });
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const action = (body.action || "").trim();
|
|
1487
|
+
if (action === "spawn") {
|
|
1488
|
+
if (!body.task)
|
|
1489
|
+
throw new Error("spawn requires task");
|
|
1490
|
+
const result = await service.spawn({
|
|
1491
|
+
task: body.task,
|
|
1492
|
+
parentConversationId: body.parentConversationId,
|
|
1493
|
+
workspaceId: body.workspaceId,
|
|
1494
|
+
title: body.title,
|
|
1495
|
+
agentId: body.agentId,
|
|
1496
|
+
model: body.model,
|
|
1497
|
+
thinkingLevel: body.thinkingLevel,
|
|
1498
|
+
initialPrompt: body.initialPrompt,
|
|
1499
|
+
wait: body.wait,
|
|
1500
|
+
});
|
|
1501
|
+
json(200, { ok: true, ...result });
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (action === "dispatch") {
|
|
1505
|
+
if (!body.conversationId)
|
|
1506
|
+
throw new Error("dispatch requires conversationId");
|
|
1507
|
+
if (!body.content)
|
|
1508
|
+
throw new Error("dispatch requires content");
|
|
1509
|
+
const result = await service.dispatch(body.conversationId, body.content, { wait: body.wait });
|
|
1510
|
+
json(200, { ok: true, ...result });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (action === "status") {
|
|
1514
|
+
if (!body.conversationId)
|
|
1515
|
+
throw new Error("status requires conversationId");
|
|
1516
|
+
const summary = await service.status(body.conversationId);
|
|
1517
|
+
json(200, { ok: true, summary });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (action === "list") {
|
|
1521
|
+
const sessions = await service.list(body.parentConversationId);
|
|
1522
|
+
json(200, { ok: true, sessions });
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (action === "stop") {
|
|
1526
|
+
if (!body.conversationId)
|
|
1527
|
+
throw new Error("stop requires conversationId");
|
|
1528
|
+
const result = await service.stop(body.conversationId, { mode: body.mode, messageId: body.messageId });
|
|
1529
|
+
json(200, { ok: true, ...result });
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (action === "resume") {
|
|
1533
|
+
if (!body.conversationId)
|
|
1534
|
+
throw new Error("resume requires conversationId");
|
|
1535
|
+
const result = await service.resume(body.conversationId, body.prompt, { wait: body.wait });
|
|
1536
|
+
json(200, { ok: true, ...result });
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
json(400, { error: `不支持的编排操作:${action}` });
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
json(400, { error: error instanceof Error ? error.message : "orchestrator request failed" });
|
|
1543
|
+
}
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (url.pathname === "/api/orchestrator/status" && request.method === "GET") {
|
|
1547
|
+
if (!isLocalOrPrivateAddress(remote)) {
|
|
1548
|
+
response.writeHead(403).end();
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const service = context.sessionOrchestratorService;
|
|
1552
|
+
if (!service) {
|
|
1553
|
+
response.writeHead(503, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "编排服务未就绪" }));
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const conversationId = url.searchParams.get("conversationId");
|
|
1557
|
+
if (!conversationId) {
|
|
1558
|
+
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "缺少 conversationId" }));
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
try {
|
|
1562
|
+
const summary = await service.status(conversationId);
|
|
1563
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, summary }));
|
|
1564
|
+
}
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "status failed" }));
|
|
1567
|
+
}
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (url.pathname === "/api/orchestrator/list" && request.method === "GET") {
|
|
1571
|
+
if (!isLocalOrPrivateAddress(remote)) {
|
|
1572
|
+
response.writeHead(403).end();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const service = context.sessionOrchestratorService;
|
|
1576
|
+
if (!service) {
|
|
1577
|
+
response.writeHead(503, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "编排服务未就绪" }));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const parentId = url.searchParams.get("parentId") || undefined;
|
|
1581
|
+
try {
|
|
1582
|
+
const sessions = await service.list(parentId);
|
|
1583
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, sessions }));
|
|
1584
|
+
}
|
|
1585
|
+
catch (error) {
|
|
1586
|
+
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "list failed" }));
|
|
1587
|
+
}
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1455
1590
|
if (url.pathname === "/api/question") {
|
|
1456
1591
|
if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
|
|
1457
1592
|
response.writeHead(403).end();
|
package/dist/server/index.js
CHANGED
|
@@ -24,6 +24,8 @@ import { RunService } from "./application/run-service.js";
|
|
|
24
24
|
import { FileChangeTracker } from "./file-changes.js";
|
|
25
25
|
import { ConversationService } from "./application/conversation-service.js";
|
|
26
26
|
import { ScheduledTaskService } from "./application/scheduled-task-service.js";
|
|
27
|
+
import { SessionLedger } from "./session-ledger.js";
|
|
28
|
+
import { SessionOrchestratorService } from "./application/session-orchestrator-service.js";
|
|
27
29
|
import { WorkspaceService } from "./application/workspace-service.js";
|
|
28
30
|
import { CommandHandler } from "./command-handler.js";
|
|
29
31
|
import { TerminalService, terminalDisabledReason } from "./terminal.js";
|
|
@@ -114,6 +116,7 @@ const workflowEndpoint = `http://${internalHost}:${port}/api/workflow/run`;
|
|
|
114
116
|
const questionEndpoint = `http://${internalHost}:${port}/api/question`;
|
|
115
117
|
const toolsEndpoint = `http://${internalHost}:${port}/api/tools`;
|
|
116
118
|
const documentsEndpoint = `http://${internalHost}:${port}/api/documents/tool`;
|
|
119
|
+
const orchestratorEndpoint = `http://${internalHost}:${port}/api/orchestrator`;
|
|
117
120
|
// ------------------------------------------------------------- Core singletons
|
|
118
121
|
const store = await openStateStore(dataDir, process.env.SUPER_PI_PERSISTENCE ?? "sqlite");
|
|
119
122
|
const toolRegistry = new ToolRegistry(dataDir);
|
|
@@ -136,7 +139,7 @@ const paths = {
|
|
|
136
139
|
};
|
|
137
140
|
const runtime = process.env.SUPER_PI_RUNTIME === "welcome"
|
|
138
141
|
? new WelcomeRuntime()
|
|
139
|
-
: new PiRpcRuntime(join(dataDir, "pi-sessions"), undefined, { dir: initialAgentDirs, promptDir: agentPromptDir }, { endpoint: taskEndpoint, execEndpoint, todoEndpoint, workflowEndpoint, questionEndpoint, toolsEndpoint, documentsEndpoint, scheduledTaskEndpoint, uploadsDir });
|
|
142
|
+
: new PiRpcRuntime(join(dataDir, "pi-sessions"), undefined, { dir: initialAgentDirs, promptDir: agentPromptDir }, { endpoint: taskEndpoint, execEndpoint, todoEndpoint, workflowEndpoint, questionEndpoint, toolsEndpoint, documentsEndpoint, scheduledTaskEndpoint, orchestratorEndpoint, uploadsDir });
|
|
140
143
|
// Closed workspaces stay in the store for reopening. Focused sockets receive all visible sidebar
|
|
141
144
|
// summaries and only their selected transcript; legacy sockets retain the complete visible state.
|
|
142
145
|
const broadcaster = new WsBroadcaster((scope) => store.clientState(scope));
|
|
@@ -173,13 +176,24 @@ supervisor.setMemory(memory);
|
|
|
173
176
|
const validateModel = (conversationId, selection) => validateConversationModel(store, runtime, conversationId, selection);
|
|
174
177
|
runtime.setModelValidator?.(validateModel);
|
|
175
178
|
supervisor.setModelValidator(validateModel);
|
|
179
|
+
/**
|
|
180
|
+
* DIAGNOSTIC ONLY - reproduces the pre-fix flood wire-for-wire (a full `state` broadcast per
|
|
181
|
+
* `subagent.event`, uncapped live output) so probes and regression tests can A/B the fix. Never set
|
|
182
|
+
* outside a probe; see docs/SUBAGENT-WS-FLOOD-FIX-2026-10-29.md.
|
|
183
|
+
*/
|
|
184
|
+
const legacySubagentFlood = process.env.SUPER_PI_LEGACY_SUBAGENT_FLOOD === "1";
|
|
176
185
|
const subagents = new SubagentRunRecorder(supervisor, {
|
|
177
186
|
store,
|
|
178
187
|
broadcast: (event) => {
|
|
179
188
|
broadcaster.broadcast(event);
|
|
180
|
-
if (event.type === "subagent.event")
|
|
189
|
+
if (legacySubagentFlood && event.type === "subagent.event")
|
|
181
190
|
broadcaster.broadcastState();
|
|
182
191
|
},
|
|
192
|
+
// The full state broadcast moved from "every delta" to "on settle": a browser that reloads mid-run
|
|
193
|
+
// rebuilds the card from the store (written per delta); the settle frame is what puts the FINAL
|
|
194
|
+
// record into `state` so a reload after the finish sees it too.
|
|
195
|
+
onSettled: legacySubagentFlood ? undefined : () => broadcaster.broadcastState(),
|
|
196
|
+
liveTailCap: legacySubagentFlood ? Number.POSITIVE_INFINITY : undefined,
|
|
183
197
|
host: (conversationId, runId) => extensionUiService.createHost(conversationId, runId),
|
|
184
198
|
});
|
|
185
199
|
/**
|
|
@@ -275,6 +289,8 @@ runService.setExecService(execService);
|
|
|
275
289
|
* message takes.
|
|
276
290
|
*/
|
|
277
291
|
const scheduledTaskService = new ScheduledTaskService(store, runService, conversationService, broadcaster);
|
|
292
|
+
const sessionLedger = new SessionLedger();
|
|
293
|
+
const sessionOrchestratorService = new SessionOrchestratorService(store, conversationService, runService, sessionLedger, broadcaster);
|
|
278
294
|
const serverContext = {
|
|
279
295
|
memory,
|
|
280
296
|
knowledge,
|
|
@@ -294,6 +310,8 @@ const serverContext = {
|
|
|
294
310
|
catalogService,
|
|
295
311
|
providerService,
|
|
296
312
|
scheduledTaskService,
|
|
313
|
+
sessionLedger,
|
|
314
|
+
sessionOrchestratorService,
|
|
297
315
|
execService,
|
|
298
316
|
toolRegistry,
|
|
299
317
|
toolRunner,
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -742,6 +742,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
742
742
|
toolsEndpoint?: string;
|
|
743
743
|
documentsEndpoint?: string;
|
|
744
744
|
scheduledTaskEndpoint?: string;
|
|
745
|
+
orchestratorEndpoint?: string;
|
|
745
746
|
uploadsDir?: string;
|
|
746
747
|
sessionFactory?: PiSessionFactory;
|
|
747
748
|
sessionMode?: "sdk" | "oneshot";
|
package/dist/server/runtime.js
CHANGED
|
@@ -21,7 +21,7 @@ import { describeDeliveredUserMessage, describePiQueue, handleSessionEvent, Turn
|
|
|
21
21
|
// paths recognise a delivery identically. Re-exported because they were part of this module's surface.
|
|
22
22
|
export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
|
|
23
23
|
import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
|
|
24
|
-
import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
24
|
+
import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, ORCHESTRATOR_ENDPOINT_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
25
25
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
26
26
|
import { loadConversationImages } from "./upload.js";
|
|
27
27
|
import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
|
|
@@ -709,6 +709,8 @@ export class PiRpcRuntime {
|
|
|
709
709
|
[TODO_ENDPOINT_ENV]: this.options.todoEndpoint ?? "",
|
|
710
710
|
[SCHEDULED_TASK_ENDPOINT_ENV]: this.options.scheduledTaskEndpoint ?? "",
|
|
711
711
|
[WORKFLOW_ENDPOINT_ENV]: this.options.workflowEndpoint ?? "",
|
|
712
|
+
[ORCHESTRATOR_ENDPOINT_ENV]: this.options.orchestratorEndpoint ?? "",
|
|
713
|
+
SUPER_PI_ORCHESTRATOR_CONVERSATION: conversationId,
|
|
712
714
|
SUPER_PI_QUESTION_ENDPOINT: this.options.questionEndpoint ?? "",
|
|
713
715
|
SUPER_PI_TOOLS_ENDPOINT: this.options.toolsEndpoint ?? "",
|
|
714
716
|
// The document tools' endpoint and the conversation they act for. The CREDENTIAL is not here:
|
|
@@ -1240,6 +1242,8 @@ export class PiRpcRuntime {
|
|
|
1240
1242
|
// loaded-but-undeclared tool - broken with no error to explain it.
|
|
1241
1243
|
if (process.env.SUPER_PI_FEISHU_ENDPOINT)
|
|
1242
1244
|
names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
|
|
1245
|
+
if (this.options.orchestratorEndpoint)
|
|
1246
|
+
names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME);
|
|
1243
1247
|
return names;
|
|
1244
1248
|
}
|
|
1245
1249
|
/**
|
|
@@ -1458,7 +1462,7 @@ export class PiRpcRuntime {
|
|
|
1458
1462
|
// allowlist and naming a tool Pi has not registered would change the spawn line for every other
|
|
1459
1463
|
// mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
|
|
1460
1464
|
// tool call anyway the reranker times out and falls back to the lexical order.
|
|
1461
|
-
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
|
|
1465
|
+
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
|
|
1462
1466
|
// Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
|
|
1463
1467
|
// "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
|
|
1464
1468
|
if (!this.pi.found)
|
|
@@ -71,6 +71,8 @@ export interface ServerContext {
|
|
|
71
71
|
* task of its own.
|
|
72
72
|
*/
|
|
73
73
|
scheduledTaskService?: import("./application/scheduled-task-service.js").ScheduledTaskService;
|
|
74
|
+
sessionLedger?: import("./session-ledger.js").SessionLedger;
|
|
75
|
+
sessionOrchestratorService?: import("./application/session-orchestrator-service.js").SessionOrchestratorService;
|
|
74
76
|
execService: ExecService;
|
|
75
77
|
toolRegistry: ToolRegistry;
|
|
76
78
|
toolRunner: ToolRunner;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type SessionOrigin = "user" | "orchestrator";
|
|
2
|
+
export type SessionLedgerStatus = "idle" | "running" | "waiting" | "stopped" | "failed" | "completed";
|
|
3
|
+
/**
|
|
4
|
+
* Metadata entry for a conversation managed or observed by the orchestrator.
|
|
5
|
+
* Crucially, NO transcript or message history is retained here to keep memory footprint bounded.
|
|
6
|
+
*/
|
|
7
|
+
export interface SessionLedgerEntry {
|
|
8
|
+
conversationId: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
/** Whether created manually by user or by orchestrator agent */
|
|
11
|
+
origin: SessionOrigin;
|
|
12
|
+
/** If created by orchestrator: the parent conversation that dispatched it */
|
|
13
|
+
parentConversationId?: string;
|
|
14
|
+
/** One-line task description assigned to this session */
|
|
15
|
+
task?: string;
|
|
16
|
+
/** Current status */
|
|
17
|
+
status: SessionLedgerStatus;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
/** High-level progress snippet or latest outcome */
|
|
21
|
+
lastProgress?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* In-memory relationship ledger for sibling and child sessions.
|
|
25
|
+
* Holds only metadata (parent-child relationships, tasks, statuses) - never transcripts.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SessionLedger {
|
|
28
|
+
private readonly entries;
|
|
29
|
+
/**
|
|
30
|
+
* Registers a session into the ledger.
|
|
31
|
+
*/
|
|
32
|
+
register(entry: Omit<SessionLedgerEntry, "createdAt" | "updatedAt"> & {
|
|
33
|
+
createdAt?: number;
|
|
34
|
+
updatedAt?: number;
|
|
35
|
+
}): SessionLedgerEntry;
|
|
36
|
+
/**
|
|
37
|
+
* Updates metadata for an existing session in the ledger.
|
|
38
|
+
*/
|
|
39
|
+
update(conversationId: string, patch: Partial<Omit<SessionLedgerEntry, "conversationId" | "createdAt">>): SessionLedgerEntry | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Retrieves metadata for one session.
|
|
42
|
+
*/
|
|
43
|
+
get(conversationId: string): SessionLedgerEntry | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Lists all sessions dispatched by a given parent conversation, sorted newest first.
|
|
46
|
+
*/
|
|
47
|
+
listByParent(parentConversationId: string): SessionLedgerEntry[];
|
|
48
|
+
/**
|
|
49
|
+
* Lists all recorded sessions in the ledger, sorted newest first.
|
|
50
|
+
*/
|
|
51
|
+
listAll(): SessionLedgerEntry[];
|
|
52
|
+
/**
|
|
53
|
+
* Removes a session from the ledger.
|
|
54
|
+
*/
|
|
55
|
+
delete(conversationId: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Clears all ledger entries (primarily for test cleanup).
|
|
58
|
+
*/
|
|
59
|
+
clear(): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory relationship ledger for sibling and child sessions.
|
|
3
|
+
* Holds only metadata (parent-child relationships, tasks, statuses) - never transcripts.
|
|
4
|
+
*/
|
|
5
|
+
export class SessionLedger {
|
|
6
|
+
entries = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Registers a session into the ledger.
|
|
9
|
+
*/
|
|
10
|
+
register(entry) {
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const fullEntry = {
|
|
13
|
+
...entry,
|
|
14
|
+
createdAt: entry.createdAt ?? now,
|
|
15
|
+
updatedAt: entry.updatedAt ?? now,
|
|
16
|
+
};
|
|
17
|
+
this.entries.set(entry.conversationId, fullEntry);
|
|
18
|
+
return { ...fullEntry };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Updates metadata for an existing session in the ledger.
|
|
22
|
+
*/
|
|
23
|
+
update(conversationId, patch) {
|
|
24
|
+
const existing = this.entries.get(conversationId);
|
|
25
|
+
if (!existing)
|
|
26
|
+
return undefined;
|
|
27
|
+
const updated = {
|
|
28
|
+
...existing,
|
|
29
|
+
...patch,
|
|
30
|
+
updatedAt: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
this.entries.set(conversationId, updated);
|
|
33
|
+
return { ...updated };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Retrieves metadata for one session.
|
|
37
|
+
*/
|
|
38
|
+
get(conversationId) {
|
|
39
|
+
const entry = this.entries.get(conversationId);
|
|
40
|
+
return entry ? { ...entry } : undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lists all sessions dispatched by a given parent conversation, sorted newest first.
|
|
44
|
+
*/
|
|
45
|
+
listByParent(parentConversationId) {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const entry of this.entries.values()) {
|
|
48
|
+
if (entry.parentConversationId === parentConversationId) {
|
|
49
|
+
result.push({ ...entry });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result.sort((a, b) => b.createdAt - a.createdAt);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Lists all recorded sessions in the ledger, sorted newest first.
|
|
56
|
+
*/
|
|
57
|
+
listAll() {
|
|
58
|
+
const result = [];
|
|
59
|
+
for (const entry of this.entries.values()) {
|
|
60
|
+
result.push({ ...entry });
|
|
61
|
+
}
|
|
62
|
+
return result.sort((a, b) => b.createdAt - a.createdAt);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Removes a session from the ledger.
|
|
66
|
+
*/
|
|
67
|
+
delete(conversationId) {
|
|
68
|
+
return this.entries.delete(conversationId);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Clears all ledger entries (primarily for test cleanup).
|
|
72
|
+
*/
|
|
73
|
+
clear() {
|
|
74
|
+
this.entries.clear();
|
|
75
|
+
}
|
|
76
|
+
}
|