@evandu/super-pi 0.1.148 → 0.1.150
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 +105 -1
- package/agents/super-pi-task/index.ts +46 -0
- package/dist/client/.super-pi-client-sources.json +3 -3
- package/dist/client/assets/{CodeMirrorEditor-DQCjEnxG.js → CodeMirrorEditor-BhTJvXpU.js} +2 -2
- package/dist/client/assets/{dist-QZ-FAVJV.js → dist-0vbzT0fs.js} +1 -1
- package/dist/client/assets/{dist-CRxCwn-Z.js → dist-49TFFUQg.js} +1 -1
- package/dist/client/assets/{dist-B4pz_xdd.js → dist-80xX2Yd8.js} +1 -1
- package/dist/client/assets/{dist-F3S0t1oM.js → dist-B0gI-KRY.js} +1 -1
- package/dist/client/assets/{dist-CDBKM9tL.js → dist-B3Dy7iF_.js} +1 -1
- package/dist/client/assets/{dist-BpeClAv0.js → dist-B5nvkwVU.js} +1 -1
- package/dist/client/assets/{dist-CGzXCzCG.js → dist-BFAjSsad.js} +1 -1
- package/dist/client/assets/{dist-CTDgUnBG.js → dist-BOqRI6sA.js} +1 -1
- package/dist/client/assets/{dist-u0nBg0fZ.js → dist-BstoGC94.js} +1 -1
- package/dist/client/assets/{dist-Bk_pkBVH.js → dist-BuDFattZ.js} +1 -1
- package/dist/client/assets/{dist-B1qLZVMK.js → dist-Bz1OET8s.js} +1 -1
- package/dist/client/assets/{dist-BWvzD8Pe.js → dist-CMiCGuFr.js} +1 -1
- package/dist/client/assets/{dist-DTZLNEci.js → dist-Ce7WvAkm.js} +1 -1
- package/dist/client/assets/{dist-DuUi29km.js → dist-CkatEAPx.js} +1 -1
- package/dist/client/assets/{dist-BJVI1Unv.js → dist-Cwuref9o.js} +1 -1
- package/dist/client/assets/{dist-Bx7s4rni.js → dist-CzCFepaD.js} +1 -1
- package/dist/client/assets/{dist-1Innu17E.js → dist-DDdxjRBp.js} +1 -1
- package/dist/client/assets/{dist-D1VfMWmf.js → dist-DJdFGvR0.js} +1 -1
- package/dist/client/assets/{dist-1lynFOc5.js → dist-Dq7hLZoB.js} +1 -1
- package/dist/client/assets/{dist-BsmJg5PX.js → dist-SMKcq1ns.js} +1 -1
- package/dist/client/assets/{dist-BayWssd0.js → dist-TwlTH8F1.js} +1 -1
- package/dist/client/assets/{dist-B6KwDpeI.js → dist-aOpTOMHI.js} +1 -1
- package/dist/client/assets/{index-Dzyp_7sf.js → index-Csr3-JTY.js} +5 -5
- package/dist/client/index.html +1 -1
- package/dist/server/agents.d.ts +1 -1
- package/dist/server/agents.js +1 -1
- package/dist/server/application/session-orchestrator-service.d.ts +49 -2
- package/dist/server/application/session-orchestrator-service.js +129 -7
- package/dist/server/http-server.js +65 -0
- package/dist/server/runtime.js +3 -3
- package/dist/server/task-tool.d.ts +3 -0
- package/dist/server/task-tool.js +19 -0
- package/package.json +2 -1
package/dist/client/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
10
10
|
<title>Super Pi</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-Csr3-JTY.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-BMpqviwU.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
package/dist/server/agents.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ type Frontmatter = Record<string, unknown>;
|
|
|
38
38
|
*/
|
|
39
39
|
export declare const PI_TOOL_NAMES: readonly ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "codemode", "tool_search", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
|
|
40
40
|
/** Tools shipped by this package. Authors must be able to explicitly deny host extras. */
|
|
41
|
-
export declare const SUPER_PI_TOOL_NAMES: readonly ["task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register", "memory", "skill_manage", "session_search", "session_spawn", "session_status", "session_dispatch", "session_stop", "session_report", "knowledge_search", "knowledge_import", "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write"];
|
|
41
|
+
export declare const SUPER_PI_TOOL_NAMES: readonly ["task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register", "memory", "skill_manage", "session_search", "session_spawn", "session_status", "session_dispatch", "session_stop", "session_report", "session_stop_subtree", "knowledge_search", "knowledge_import", "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write"];
|
|
42
42
|
/**
|
|
43
43
|
* Whether a name can be a real Pi tool. A name is real if it is one of `PI_TOOL_NAMES`, an MCP
|
|
44
44
|
* server tool, or a PATTERN.
|
package/dist/server/agents.js
CHANGED
|
@@ -40,7 +40,7 @@ export const PI_TOOL_NAMES = [
|
|
|
40
40
|
export const SUPER_PI_TOOL_NAMES = [
|
|
41
41
|
"task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register",
|
|
42
42
|
"memory", "skill_manage", "session_search",
|
|
43
|
-
"session_spawn", "session_status", "session_dispatch", "session_stop", "session_report",
|
|
43
|
+
"session_spawn", "session_status", "session_dispatch", "session_stop", "session_report", "session_stop_subtree",
|
|
44
44
|
"knowledge_search", "knowledge_import",
|
|
45
45
|
"document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write",
|
|
46
46
|
];
|
|
@@ -110,16 +110,27 @@ export declare class SessionOrchestratorService {
|
|
|
110
110
|
*/
|
|
111
111
|
private readonly lengthContinuations;
|
|
112
112
|
/**
|
|
113
|
-
* THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT.
|
|
113
|
+
* THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT, AND WHEN.
|
|
114
114
|
*
|
|
115
115
|
* A child can report explicitly - the `report` action - and that report is the child's own
|
|
116
116
|
* statement of what it did. When the child's turn then ends, the automatic completion report
|
|
117
117
|
* would deliver the same thing a second time, so the automatic path steps aside for a child that
|
|
118
|
-
* is in this
|
|
118
|
+
* is in this map.
|
|
119
119
|
*
|
|
120
120
|
* The entry is added only by a TERMINAL report (`completed` / `failed`), because an interim
|
|
121
121
|
* update is not the child's final word: the task is still running, and the parent will want to
|
|
122
122
|
* hear how it ended.
|
|
123
|
+
*
|
|
124
|
+
* WHY THE MAP HOLDS A TIMESTAMP AND NOT A FLAG. A flag says "this child reported", and that is
|
|
125
|
+
* not the same question as "this child reported ABOUT THE TURN THAT JUST ENDED". A flag stays
|
|
126
|
+
* true for the rest of the child's life, so it silences every automatic report the child ever
|
|
127
|
+
* produces again: the parent gives the same child a second task, the child finishes, and the
|
|
128
|
+
* result is swallowed because of a report the parent received about the FIRST task. The parent
|
|
129
|
+
* waits for a result that was written and never arrives, and nothing in the transcript says why.
|
|
130
|
+
*
|
|
131
|
+
* The timestamp answers the real question: the report was made INSIDE this turn, so it speaks for
|
|
132
|
+
* this turn. A report made before the turn started speaks for an earlier one, and this turn's
|
|
133
|
+
* result is still its own.
|
|
123
134
|
*/
|
|
124
135
|
private readonly reportedChildren;
|
|
125
136
|
constructor(store: Store, conversationService: ConversationService, runService: RunService, ledger: SessionLedger, broadcaster?: EventBroadcaster | undefined, watchdog?: SessionWatchdog);
|
|
@@ -219,6 +230,42 @@ export declare class SessionOrchestratorService {
|
|
|
219
230
|
stopped: boolean;
|
|
220
231
|
mode: string;
|
|
221
232
|
}>;
|
|
233
|
+
/**
|
|
234
|
+
* THE SESSIONS UNDER ONE SESSION, DEEPEST LAST.
|
|
235
|
+
*
|
|
236
|
+
* The tree edge lives on the conversation itself (`parentConversationId`), so "who is under this
|
|
237
|
+
* one?" is a walk over the workspace's conversations rather than a separate structure that could
|
|
238
|
+
* disagree with it. The order is breadth-first, which is depth order - and the caller stops them
|
|
239
|
+
* in reverse, so the deepest work dies before the session that owns it.
|
|
240
|
+
*/
|
|
241
|
+
private descendantsOf;
|
|
242
|
+
/**
|
|
243
|
+
* STOPS A SESSION AND EVERYTHING UNDER IT.
|
|
244
|
+
*
|
|
245
|
+
* THIS IS NOT THE SAME THING AS STOPPING A SESSION, and it is deliberately a separate operation
|
|
246
|
+
* rather than a flag on the other one. Stopping a session stops THAT session: its turn, its queued
|
|
247
|
+
* messages, its watchdog. Nothing else. That behaviour is right for the ordinary case - a child
|
|
248
|
+
* that went down the wrong path should not take its own children down with it, because those
|
|
249
|
+
* children may be doing work the parent still wants.
|
|
250
|
+
*
|
|
251
|
+
* But there is a case where the ordinary case leaves a mess. A supervisor whose own supervisor has
|
|
252
|
+
* stopped is an ORPHAN, and the sessions it started are orphans too: they keep burning tokens,
|
|
253
|
+
* keep writing files, and report their results to a session that is no longer running - nobody is
|
|
254
|
+
* left to read them. The parent sees a child that has gone wrong, stops it, and the grandchildren
|
|
255
|
+
* keep running with no parent to stop them.
|
|
256
|
+
*
|
|
257
|
+
* So this operation takes the whole tree: the root, its children, its grandchildren, deepest
|
|
258
|
+
* first. The descendants are stopped AS A CONSEQUENCE of the root being stopped - the caller is
|
|
259
|
+
* still only managing its OWN direct child, and the cross-level reach the scope rules forbid is
|
|
260
|
+
* not what happens here: a grandchild cannot be stopped on its own, and it cannot be spared
|
|
261
|
+
* either, because it cannot outlive the session that owns it.
|
|
262
|
+
*/
|
|
263
|
+
stopSubtree(conversationId: string, options?: StopSessionOptions): Promise<{
|
|
264
|
+
stopped: boolean;
|
|
265
|
+
mode: string;
|
|
266
|
+
stoppedIds: string[];
|
|
267
|
+
descendantCount: number;
|
|
268
|
+
}>;
|
|
222
269
|
/**
|
|
223
270
|
* Resumes execution in a conversation.
|
|
224
271
|
* If prompt is provided, sends it as a new turn.
|
|
@@ -39,18 +39,29 @@ export class SessionOrchestratorService {
|
|
|
39
39
|
*/
|
|
40
40
|
lengthContinuations = new Map();
|
|
41
41
|
/**
|
|
42
|
-
* THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT.
|
|
42
|
+
* THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT, AND WHEN.
|
|
43
43
|
*
|
|
44
44
|
* A child can report explicitly - the `report` action - and that report is the child's own
|
|
45
45
|
* statement of what it did. When the child's turn then ends, the automatic completion report
|
|
46
46
|
* would deliver the same thing a second time, so the automatic path steps aside for a child that
|
|
47
|
-
* is in this
|
|
47
|
+
* is in this map.
|
|
48
48
|
*
|
|
49
49
|
* The entry is added only by a TERMINAL report (`completed` / `failed`), because an interim
|
|
50
50
|
* update is not the child's final word: the task is still running, and the parent will want to
|
|
51
51
|
* hear how it ended.
|
|
52
|
+
*
|
|
53
|
+
* WHY THE MAP HOLDS A TIMESTAMP AND NOT A FLAG. A flag says "this child reported", and that is
|
|
54
|
+
* not the same question as "this child reported ABOUT THE TURN THAT JUST ENDED". A flag stays
|
|
55
|
+
* true for the rest of the child's life, so it silences every automatic report the child ever
|
|
56
|
+
* produces again: the parent gives the same child a second task, the child finishes, and the
|
|
57
|
+
* result is swallowed because of a report the parent received about the FIRST task. The parent
|
|
58
|
+
* waits for a result that was written and never arrives, and nothing in the transcript says why.
|
|
59
|
+
*
|
|
60
|
+
* The timestamp answers the real question: the report was made INSIDE this turn, so it speaks for
|
|
61
|
+
* this turn. A report made before the turn started speaks for an earlier one, and this turn's
|
|
62
|
+
* result is still its own.
|
|
52
63
|
*/
|
|
53
|
-
reportedChildren = new
|
|
64
|
+
reportedChildren = new Map();
|
|
54
65
|
constructor(store, conversationService, runService, ledger, broadcaster, watchdog) {
|
|
55
66
|
this.store = store;
|
|
56
67
|
this.conversationService = conversationService;
|
|
@@ -180,6 +191,11 @@ export class SessionOrchestratorService {
|
|
|
180
191
|
// writes the words under the reader's own name - so the child's transcript showed an
|
|
181
192
|
// instruction the reader never typed, wearing the reader's avatar, and a reader who looked at
|
|
182
193
|
// the child session saw their own name on somebody else's order.
|
|
194
|
+
//
|
|
195
|
+
// THE TURN'S START IS RECORDED HERE, because the question "did this child already report THIS
|
|
196
|
+
// turn?" is answered by comparing the report's time against it. A report made inside the turn
|
|
197
|
+
// speaks for the turn; a report made before it speaks for an earlier one.
|
|
198
|
+
const turnStartedAt = Date.now();
|
|
183
199
|
const runPromise = this.runService.enqueueAndRunAsAgent(conversationId, content);
|
|
184
200
|
const completion = (async () => {
|
|
185
201
|
let failure;
|
|
@@ -238,9 +254,13 @@ export class SessionOrchestratorService {
|
|
|
238
254
|
// is what it produced"; the explicit report says "the child is telling its parent something
|
|
239
255
|
// now", and can be called at any point in the task's life.
|
|
240
256
|
const turnEnded = current?.status === "completed" || current?.status === "failed";
|
|
241
|
-
// A CHILD THAT REPORTED ITSELF IS NOT REPORTED TWICE
|
|
242
|
-
// child's own statement of what it did, so the
|
|
243
|
-
|
|
257
|
+
// A CHILD THAT REPORTED ITSELF IS NOT REPORTED TWICE - BUT ONLY FOR THE TURN IT REPORTED.
|
|
258
|
+
// The explicit terminal report is the child's own statement of what it did, so the
|
|
259
|
+
// automatic completion steps aside for it - when the report was made inside THIS turn.
|
|
260
|
+
// A report from an earlier turn says nothing about this one's result, and silencing this
|
|
261
|
+
// one because of that would swallow a result the parent is still waiting for.
|
|
262
|
+
const reportedAt = this.reportedChildren.get(conversationId);
|
|
263
|
+
const alreadyReported = reportedAt !== undefined && reportedAt >= turnStartedAt;
|
|
244
264
|
if (turnEnded && !alreadyReported) {
|
|
245
265
|
await this.runService.enqueueSessionResult({
|
|
246
266
|
conversationId,
|
|
@@ -260,6 +280,16 @@ export class SessionOrchestratorService {
|
|
|
260
280
|
// turn an already-finished child turn into a second failure.
|
|
261
281
|
console.warn(`Super Pi: session result callback failed for ${conversationId}:`, error instanceof Error ? error.message : String(error));
|
|
262
282
|
}
|
|
283
|
+
finally {
|
|
284
|
+
// THE RECORD IS SPENT BY THE TURN IT SPOKE FOR. Whatever it was about, that turn is over.
|
|
285
|
+
//
|
|
286
|
+
// A record left behind would be harmless - the timestamp comparison already keeps it from
|
|
287
|
+
// silencing a later turn - but it would keep saying "this child reported" to every future
|
|
288
|
+
// reader of this code, and the next thing that consults it would have to re-derive the
|
|
289
|
+
// timestamp rule. Dropping it here keeps one rule in one place: a report belongs to the
|
|
290
|
+
// turn that was running when it was made.
|
|
291
|
+
this.reportedChildren.delete(conversationId);
|
|
292
|
+
}
|
|
263
293
|
if (failure !== undefined) {
|
|
264
294
|
console.warn(`Super Pi: dispatch failed for conversation ${conversationId}:`, failure instanceof Error ? failure.message : String(failure));
|
|
265
295
|
this.ledger.update(conversationId, { status: "failed" });
|
|
@@ -391,7 +421,10 @@ export class SessionOrchestratorService {
|
|
|
391
421
|
// does not silence the automatic report: the child is still working, and the parent will want
|
|
392
422
|
// to hear how the task ended.
|
|
393
423
|
if (status === "completed" || status === "failed") {
|
|
394
|
-
|
|
424
|
+
// THE TIME OF THE REPORT IS THE PART THAT MATTERS. It is what tells the completion path
|
|
425
|
+
// whether this report spoke for the turn that is ending or for an earlier one, and it is
|
|
426
|
+
// stamped here - where the report is made - rather than where it is looked up later.
|
|
427
|
+
this.reportedChildren.set(childConversationId, Date.now());
|
|
395
428
|
await this.watchdog.cancel(childConversationId);
|
|
396
429
|
}
|
|
397
430
|
else {
|
|
@@ -662,6 +695,95 @@ export class SessionOrchestratorService {
|
|
|
662
695
|
await this.watchdog.cancel(conversationId);
|
|
663
696
|
return { stopped: true, mode };
|
|
664
697
|
}
|
|
698
|
+
/**
|
|
699
|
+
* THE SESSIONS UNDER ONE SESSION, DEEPEST LAST.
|
|
700
|
+
*
|
|
701
|
+
* The tree edge lives on the conversation itself (`parentConversationId`), so "who is under this
|
|
702
|
+
* one?" is a walk over the workspace's conversations rather than a separate structure that could
|
|
703
|
+
* disagree with it. The order is breadth-first, which is depth order - and the caller stops them
|
|
704
|
+
* in reverse, so the deepest work dies before the session that owns it.
|
|
705
|
+
*/
|
|
706
|
+
descendantsOf(rootConversationId) {
|
|
707
|
+
const root = this.store.conversation(rootConversationId);
|
|
708
|
+
if (!root)
|
|
709
|
+
return [];
|
|
710
|
+
const childrenByParent = new Map();
|
|
711
|
+
for (const conversation of this.store.conversationsForWorkspace(root.workspaceId)) {
|
|
712
|
+
const parentId = conversation.parentConversationId;
|
|
713
|
+
if (!parentId)
|
|
714
|
+
continue;
|
|
715
|
+
const siblings = childrenByParent.get(parentId);
|
|
716
|
+
if (siblings)
|
|
717
|
+
siblings.push(conversation.id);
|
|
718
|
+
else
|
|
719
|
+
childrenByParent.set(parentId, [conversation.id]);
|
|
720
|
+
}
|
|
721
|
+
const ordered = [];
|
|
722
|
+
const seen = new Set([rootConversationId]);
|
|
723
|
+
const queue = [rootConversationId];
|
|
724
|
+
while (queue.length) {
|
|
725
|
+
const current = queue.shift();
|
|
726
|
+
for (const child of childrenByParent.get(current) ?? []) {
|
|
727
|
+
// A cycle in the stored edges must not turn this into an endless walk.
|
|
728
|
+
if (seen.has(child))
|
|
729
|
+
continue;
|
|
730
|
+
seen.add(child);
|
|
731
|
+
ordered.push(child);
|
|
732
|
+
queue.push(child);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return ordered;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* STOPS A SESSION AND EVERYTHING UNDER IT.
|
|
739
|
+
*
|
|
740
|
+
* THIS IS NOT THE SAME THING AS STOPPING A SESSION, and it is deliberately a separate operation
|
|
741
|
+
* rather than a flag on the other one. Stopping a session stops THAT session: its turn, its queued
|
|
742
|
+
* messages, its watchdog. Nothing else. That behaviour is right for the ordinary case - a child
|
|
743
|
+
* that went down the wrong path should not take its own children down with it, because those
|
|
744
|
+
* children may be doing work the parent still wants.
|
|
745
|
+
*
|
|
746
|
+
* But there is a case where the ordinary case leaves a mess. A supervisor whose own supervisor has
|
|
747
|
+
* stopped is an ORPHAN, and the sessions it started are orphans too: they keep burning tokens,
|
|
748
|
+
* keep writing files, and report their results to a session that is no longer running - nobody is
|
|
749
|
+
* left to read them. The parent sees a child that has gone wrong, stops it, and the grandchildren
|
|
750
|
+
* keep running with no parent to stop them.
|
|
751
|
+
*
|
|
752
|
+
* So this operation takes the whole tree: the root, its children, its grandchildren, deepest
|
|
753
|
+
* first. The descendants are stopped AS A CONSEQUENCE of the root being stopped - the caller is
|
|
754
|
+
* still only managing its OWN direct child, and the cross-level reach the scope rules forbid is
|
|
755
|
+
* not what happens here: a grandchild cannot be stopped on its own, and it cannot be spared
|
|
756
|
+
* either, because it cannot outlive the session that owns it.
|
|
757
|
+
*/
|
|
758
|
+
async stopSubtree(conversationId, options) {
|
|
759
|
+
const root = this.store.conversation(conversationId);
|
|
760
|
+
if (!root) {
|
|
761
|
+
throw new Error(`找不到会话:${conversationId}`);
|
|
762
|
+
}
|
|
763
|
+
const descendants = this.descendantsOf(conversationId);
|
|
764
|
+
const stoppedIds = [];
|
|
765
|
+
// DEEPEST FIRST. A session that is stopped while its children still run is exactly the zombie
|
|
766
|
+
// this operation exists to prevent, so the leaves go before their parents.
|
|
767
|
+
for (const descendantId of [...descendants].reverse()) {
|
|
768
|
+
try {
|
|
769
|
+
await this.stop(descendantId, options);
|
|
770
|
+
stoppedIds.push(descendantId);
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
// ONE SESSION THAT WILL NOT STOP MUST NOT LEAVE THE REST RUNNING. The point of the cascade
|
|
774
|
+
// is that nothing survives, so a failure on one session is recorded and the sweep goes on.
|
|
775
|
+
console.warn(`Super Pi: stopping descendant ${descendantId} failed:`, error instanceof Error ? error.message : String(error));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const result = await this.stop(conversationId, options);
|
|
779
|
+
stoppedIds.push(conversationId);
|
|
780
|
+
return {
|
|
781
|
+
stopped: result.stopped,
|
|
782
|
+
mode: result.mode,
|
|
783
|
+
stoppedIds,
|
|
784
|
+
descendantCount: descendants.length,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
665
787
|
/**
|
|
666
788
|
* Resumes execution in a conversation.
|
|
667
789
|
* If prompt is provided, sends it as a new turn.
|
|
@@ -1614,6 +1614,21 @@ export function createHttpServer(options) {
|
|
|
1614
1614
|
json(200, { ok: true, ...result });
|
|
1615
1615
|
return;
|
|
1616
1616
|
}
|
|
1617
|
+
if (action === "stop_subtree") {
|
|
1618
|
+
if (!body.conversationId)
|
|
1619
|
+
throw new Error("stop_subtree requires conversationId");
|
|
1620
|
+
// THE ROOT OF THE TREE IS STILL THE CALLER'S DIRECT CHILD. The scope rule does not move:
|
|
1621
|
+
// the descendants go down with their owner, they are not reachable on their own, and a
|
|
1622
|
+
// caller still cannot stop somebody else's branch.
|
|
1623
|
+
const scope = verifyTargetScope(body.conversationId, "停止");
|
|
1624
|
+
if (!scope.ok) {
|
|
1625
|
+
json(scope.status, { error: scope.error });
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
const result = await service.stopSubtree(body.conversationId, { mode: body.mode, messageId: body.messageId });
|
|
1629
|
+
json(200, { ok: true, ...result });
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1617
1632
|
if (action === "resume") {
|
|
1618
1633
|
if (!body.conversationId)
|
|
1619
1634
|
throw new Error("resume requires conversationId");
|
|
@@ -1691,6 +1706,56 @@ export function createHttpServer(options) {
|
|
|
1691
1706
|
}
|
|
1692
1707
|
return;
|
|
1693
1708
|
}
|
|
1709
|
+
// THE PERSON'S OWN STOP.
|
|
1710
|
+
//
|
|
1711
|
+
// The endpoint above is the AGENT's path, and it authenticates with the session's capability,
|
|
1712
|
+
// because a dispatch spends model quota on somebody's behalf and the capability is what says
|
|
1713
|
+
// "this really is that session". A person clicking "stop" in their own browser is not that:
|
|
1714
|
+
// stopping REDUCES work, it grants nothing, and a browser has no capability to present - so
|
|
1715
|
+
// this path is refused by the address guard instead, the same guard every other user-facing
|
|
1716
|
+
// write in this server carries.
|
|
1717
|
+
//
|
|
1718
|
+
// WHY IT IS A SEPARATE PATH AND NOT A FLAG ON THE AGENT'S ENDPOINT. The two have different
|
|
1719
|
+
// authorities: one proves a session is speaking, the other proves a person is. Mixing them
|
|
1720
|
+
// would mean either the browser gets a dispatch credential it must not have, or the agent
|
|
1721
|
+
// loses the proof that it is the agent.
|
|
1722
|
+
if (url.pathname === "/api/orchestrator/stop") {
|
|
1723
|
+
if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
|
|
1724
|
+
response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }).end("Stopping a session requires a loopback or private network POST");
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const asked = await readRequestText(request, 64 * 1024);
|
|
1728
|
+
if (asked.tooLarge) {
|
|
1729
|
+
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
const json = (status, body) => {
|
|
1733
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1734
|
+
};
|
|
1735
|
+
try {
|
|
1736
|
+
const service = context.sessionOrchestratorService;
|
|
1737
|
+
if (!service) {
|
|
1738
|
+
json(503, { error: "编排服务未就绪" });
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const body = JSON.parse(asked.text);
|
|
1742
|
+
const conversationId = body.conversationId?.trim();
|
|
1743
|
+
if (!conversationId) {
|
|
1744
|
+
json(400, { error: "缺少 conversationId" });
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
// THE TREE GOES WITH THE ROOT WHEN THE PERSON ASKS FOR THE TREE. Stopping one session and
|
|
1748
|
+
// stopping its whole branch are two different intentions, and the person says which.
|
|
1749
|
+
const result = body.subtree
|
|
1750
|
+
? await service.stopSubtree(conversationId, { mode: body.mode, messageId: body.messageId })
|
|
1751
|
+
: await service.stop(conversationId, { mode: body.mode, messageId: body.messageId });
|
|
1752
|
+
json(200, { ok: true, ...result });
|
|
1753
|
+
}
|
|
1754
|
+
catch (error) {
|
|
1755
|
+
json(400, { error: error instanceof Error ? error.message : "stop failed" });
|
|
1756
|
+
}
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1694
1759
|
if (url.pathname === "/api/conversation/usage") {
|
|
1695
1760
|
const json = (status, payload) => {
|
|
1696
1761
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }).end(JSON.stringify(payload));
|
package/dist/server/runtime.js
CHANGED
|
@@ -24,7 +24,7 @@ import { LengthExceededError } from "./turn-failure.js";
|
|
|
24
24
|
// paths recognise a delivery identically. Re-exported because they were part of this module's surface.
|
|
25
25
|
export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
|
|
26
26
|
import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
|
|
27
|
-
import { TASK_CONVERSATION_ENV, PARENT_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, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
27
|
+
import { TASK_CONVERSATION_ENV, PARENT_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, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
28
28
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
29
29
|
import { loadConversationImages } from "./upload.js";
|
|
30
30
|
import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
|
|
@@ -1260,7 +1260,7 @@ export class PiRpcRuntime {
|
|
|
1260
1260
|
if (process.env.SUPER_PI_FEISHU_ENDPOINT)
|
|
1261
1261
|
names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
|
|
1262
1262
|
if (this.options.orchestratorEndpoint)
|
|
1263
|
-
names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME);
|
|
1263
|
+
names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME);
|
|
1264
1264
|
return names;
|
|
1265
1265
|
}
|
|
1266
1266
|
/**
|
|
@@ -1479,7 +1479,7 @@ export class PiRpcRuntime {
|
|
|
1479
1479
|
// allowlist and naming a tool Pi has not registered would change the spawn line for every other
|
|
1480
1480
|
// mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
|
|
1481
1481
|
// tool call anyway the reranker times out and falls back to the lexical order.
|
|
1482
|
-
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, SESSION_REPORT_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] : [])] });
|
|
1482
|
+
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, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_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] : [])] });
|
|
1483
1483
|
// Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
|
|
1484
1484
|
// "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
|
|
1485
1485
|
if (!this.pi.found)
|
|
@@ -258,6 +258,7 @@ export declare const SESSION_STATUS_TOOL_NAME = "session_status";
|
|
|
258
258
|
export declare const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
|
|
259
259
|
export declare const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
260
260
|
export declare const SESSION_REPORT_TOOL_NAME = "session_report";
|
|
261
|
+
export declare const SESSION_STOP_SUBTREE_TOOL_NAME = "session_stop_subtree";
|
|
261
262
|
/** Model-facing description for session_spawn. */
|
|
262
263
|
export declare const SESSION_SPAWN_DESCRIPTION = "Create a new sibling conversation in the current workspace and immediately dispatch its initial task.\n\nDIFFERENCE FROM THE 'task' TOOL:\n- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.\n- 'session_spawn' creates a real, persistent sibling CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.\n\nWHEN TO USE THIS:\n- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.\n- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.\n- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.\n\nWHEN NOT TO USE THIS:\n- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.\n- For running shell commands or build watchers: use 'run_async'.\n- For a linear multi-step subagent pipeline: use 'workflow'.\n\nUsage notes:\n1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.\n2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.\n3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.\n4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.\n5. The new session always belongs to the calling session's workspace. Pass 'cwd' as a workspace-relative subdirectory (for example, 'packages/a') when it should work below the workspace root; absolute paths and traversal are rejected.\n6. Optionally pass 'model' to choose which model the NEW session runs on: {\"provider\":\"...\",\"modelId\":\"...\"} - the same \"provider/modelId\" pair the model picker shows, and BOTH halves are required. Leave 'model' out and the new session starts on the workspace default model.\n7. Optionally pass 'watchdogTimeoutSeconds' to declare how long YOU are willing to wait for a report before you want to be reminded: a positive number of seconds. If the new session stays silent that long, you receive a '[Watchdog \u63D0\u9192]' message naming it. A report from the child (session_report) restarts the clock; the watchdog never stops the child. Leave it out to run without a watchdog.";
|
|
263
264
|
/** Model-facing description for session_status. */
|
|
@@ -268,3 +269,5 @@ export declare const SESSION_DISPATCH_DESCRIPTION = "Send a message, follow-up i
|
|
|
268
269
|
export declare const SESSION_STOP_DESCRIPTION = "Stop execution in a sibling conversation.\n\nWHEN TO USE THIS:\n- When a sibling session has gone down the wrong path, is stuck in an unwanted loop, or when the overall goal has changed and remaining work should be aborted.\n- To clear pending queued messages before they start running.\n\nUsage notes:\n1. 'mode' controls what to stop:\n - 'all' (default): stops the currently running turn and cancels all pending queued messages in the session.\n - 'active': only interrupts the active running turn; leaves any pending queued messages untouched.\n - 'queued': only cancels pending queued messages; lets the currently running turn complete naturally.\n2. Use 'messageId' if you want to cancel or interrupt one specific queued message instead of all queued messages.";
|
|
269
270
|
/** Model-facing description for session_report. */
|
|
270
271
|
export declare const SESSION_REPORT_DESCRIPTION = "Report progress from THIS session to the parent session that spawned it.\n\nWHEN TO USE THIS:\n- You are a spawned sibling session, and the parent is waiting to hear from you: a milestone is done, a roadblock appeared, or the task is finished.\n- The parent set a watchdog (session_spawn's 'watchdogTimeoutSeconds'): a report is what restarts that clock, and silence is what triggers the reminder.\n- The task is over and the parent needs the outcome NOW rather than after the automatic end-of-turn result.\n\nWHEN NOT TO USE THIS:\n- This session has no parent (the user opened it directly): there is nobody to report to, and the server refuses the call.\n- You want to ASK the parent something or send it new instructions: that is session_dispatch, and only the parent can send it. A report is one-way - the parent receives it as a message.\n\nUsage notes:\n1. 'content' is required: it is the text the parent receives. Write it for the parent - what is done, what is left, what you need.\n2. 'status' says what the report MEANS:\n - 'in_progress' (default): an interim update. The task continues, and the parent's watchdog clock restarts.\n - 'completed': the task is finished and this is your final word.\n - 'failed': the task could not be finished, and this says why.\n3. A 'completed' or 'failed' report tells the server the work is over, so the parent is NOT also sent the automatic end-of-turn result. Send it once, at the end, with the final answer in 'content'.\n4. Reporting does NOT end your turn. If the task is not done, keep working afterwards - the parent is waiting on the work, not on the report.\n5. The report reaches your DIRECT parent only. A sibling, a cousin, and the user's own session are all out of reach, and the server refuses those.";
|
|
272
|
+
/** Model-facing description for session_stop_subtree. */
|
|
273
|
+
export declare const SESSION_STOP_SUBTREE_DESCRIPTION = "Stop a sibling session AND every session underneath it.\n\nWHEN TO USE THIS:\n- The session has children of its own and you are stopping it: those children would otherwise keep running with a dead parent - zombies that burn tokens, keep writing files, and report results nobody is left to read.\n- The whole branch should go: stopping only the top leaves the work underneath it alive.\n- A child went wrong and everything it started has to go down with it.\n\nWHEN NOT TO USE THIS:\n- The session has no children: session_stop does the same thing to one session and says less.\n- The session is only misbehaving at the top and its children are doing work you still want: stop it alone with session_stop.\n- You want to cancel one queued message: session_stop's 'messageId'.\n\nUsage notes:\n1. 'conversationId' must be your own DIRECT child, exactly as for session_stop. You are not reaching across levels to manage anybody.\n2. Everything under that session goes down with it - children, grandchildren, the whole tree - deepest first, so nothing is left running under a session that is already gone.\n3. The descendants are stopped AS A CONSEQUENCE of stopping the root. They cannot be spared, because a session cannot outlive the one that owns it.\n4. The answer says how many sessions were stopped in total, including the one you named.";
|
package/dist/server/task-tool.js
CHANGED
|
@@ -363,6 +363,7 @@ export const SESSION_STATUS_TOOL_NAME = "session_status";
|
|
|
363
363
|
export const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
|
|
364
364
|
export const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
365
365
|
export const SESSION_REPORT_TOOL_NAME = "session_report";
|
|
366
|
+
export const SESSION_STOP_SUBTREE_TOOL_NAME = "session_stop_subtree";
|
|
366
367
|
/** Model-facing description for session_spawn. */
|
|
367
368
|
export const SESSION_SPAWN_DESCRIPTION = `Create a new sibling conversation in the current workspace and immediately dispatch its initial task.
|
|
368
369
|
|
|
@@ -447,3 +448,21 @@ Usage notes:
|
|
|
447
448
|
3. A 'completed' or 'failed' report tells the server the work is over, so the parent is NOT also sent the automatic end-of-turn result. Send it once, at the end, with the final answer in 'content'.
|
|
448
449
|
4. Reporting does NOT end your turn. If the task is not done, keep working afterwards - the parent is waiting on the work, not on the report.
|
|
449
450
|
5. The report reaches your DIRECT parent only. A sibling, a cousin, and the user's own session are all out of reach, and the server refuses those.`;
|
|
451
|
+
/** Model-facing description for session_stop_subtree. */
|
|
452
|
+
export const SESSION_STOP_SUBTREE_DESCRIPTION = `Stop a sibling session AND every session underneath it.
|
|
453
|
+
|
|
454
|
+
WHEN TO USE THIS:
|
|
455
|
+
- The session has children of its own and you are stopping it: those children would otherwise keep running with a dead parent - zombies that burn tokens, keep writing files, and report results nobody is left to read.
|
|
456
|
+
- The whole branch should go: stopping only the top leaves the work underneath it alive.
|
|
457
|
+
- A child went wrong and everything it started has to go down with it.
|
|
458
|
+
|
|
459
|
+
WHEN NOT TO USE THIS:
|
|
460
|
+
- The session has no children: session_stop does the same thing to one session and says less.
|
|
461
|
+
- The session is only misbehaving at the top and its children are doing work you still want: stop it alone with session_stop.
|
|
462
|
+
- You want to cancel one queued message: session_stop's 'messageId'.
|
|
463
|
+
|
|
464
|
+
Usage notes:
|
|
465
|
+
1. 'conversationId' must be your own DIRECT child, exactly as for session_stop. You are not reaching across levels to manage anybody.
|
|
466
|
+
2. Everything under that session goes down with it - children, grandchildren, the whole tree - deepest first, so nothing is left running under a session that is already gone.
|
|
467
|
+
3. The descendants are stopped AS A CONSEQUENCE of stopping the root. They cannot be spared, because a session cannot outlive the one that owns it.
|
|
468
|
+
4. The answer says how many sessions were stopped in total, including the one you named.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.150",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -217,6 +217,7 @@
|
|
|
217
217
|
"scripts/background-result-steer.test.mjs",
|
|
218
218
|
"scripts/session-report-watchdog.test.mjs",
|
|
219
219
|
"scripts/session-report-tool.test.mjs",
|
|
220
|
+
"scripts/session-stop-subtree.test.mjs",
|
|
220
221
|
"scripts/awaiting-background-status.test.mjs"
|
|
221
222
|
],
|
|
222
223
|
"check:browser": [
|