@evandu/super-pi 0.1.138 → 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-knowledge/index.ts +11 -13
- package/agents/super-pi-memory/index.ts +11 -5
- package/agents/super-pi-task/core.ts +418 -1
- package/agents/super-pi-task/index.ts +115 -0
- package/bin/super-pi.js +7 -11
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +7 -6
- package/dist/client/assets/{CodeMirrorEditor-CVImD0nh.js → CodeMirrorEditor-DvZeOLny.js} +2 -2
- package/dist/client/assets/{dist-CiEW7LQW.js → dist-3BuyVG3H.js} +1 -1
- package/dist/client/assets/{dist-faxwvvOU.js → dist-BCUfH4nP.js} +1 -1
- package/dist/client/assets/{dist-yONALz0V.js → dist-BIIMtU9W.js} +1 -1
- package/dist/client/assets/{dist-CKI1xVeE.js → dist-C-QmRwbd.js} +1 -1
- package/dist/client/assets/{dist-4uPxkRdw.js → dist-CTkzLBdb.js} +1 -1
- package/dist/client/assets/{dist-CUACXe1Y.js → dist-C_VuSpx8.js} +1 -1
- package/dist/client/assets/{dist-BwQM8_Mk.js → dist-CaUsvSz1.js} +1 -1
- package/dist/client/assets/{dist-Clf26yld.js → dist-Cb1gxS57.js} +1 -1
- package/dist/client/assets/{dist-YB4GJD3u.js → dist-CqDSeoUk.js} +1 -1
- package/dist/client/assets/{dist-DozjvPAZ.js → dist-D7yfnKNC.js} +1 -1
- package/dist/client/assets/{dist-Cocw5XJ_.js → dist-DA8yW4lK.js} +1 -1
- package/dist/client/assets/{dist-amySbADh.js → dist-DAcS4Kj8.js} +1 -1
- package/dist/client/assets/{dist-B75OT6A9.js → dist-DN0M4DAX.js} +1 -1
- package/dist/client/assets/{dist-At94VK1P.js → dist-DRgWZqFH.js} +1 -1
- package/dist/client/assets/{dist-BIgRwYsI.js → dist-DdfW8Gjv.js} +1 -1
- package/dist/client/assets/{dist-DhEa5hHR.js → dist-DlQ8PMvv.js} +1 -1
- package/dist/client/assets/{dist-quQ6u6Rw.js → dist-Dvca0LeR.js} +1 -1
- package/dist/client/assets/{dist-BqblSnWL.js → dist-HJdP1yxN.js} +1 -1
- package/dist/client/assets/{dist-pnGHl3nq.js → dist-gBAr9yXF.js} +1 -1
- package/dist/client/assets/{dist-1H_gIENV.js → dist-gyzurUDR.js} +1 -1
- package/dist/client/assets/{dist-DSCjkO-y.js → dist-qG53hT1b.js} +1 -1
- package/dist/client/assets/{dist-CNP-Q5Gs.js → dist-r4Kq1ABA.js} +1 -1
- package/dist/client/assets/{index-CNoiRdJC.js → index-CNbh1fL2.js} +14 -14
- package/dist/client/assets/index-DCSFD4VV.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/conversation-service.d.ts +13 -4
- package/dist/server/application/conversation-service.js +112 -61
- package/dist/server/application/run-service.d.ts +13 -0
- package/dist/server/application/run-service.js +255 -69
- package/dist/server/application/scheduled-task-service.js +8 -1
- package/dist/server/application/session-orchestrator-service.d.ts +113 -0
- package/dist/server/application/session-orchestrator-service.js +234 -0
- package/dist/server/application/workspace-service.js +16 -1
- package/dist/server/feishu-cli.js +15 -4
- package/dist/server/file-index.d.ts +30 -0
- package/dist/server/file-index.js +49 -4
- package/dist/server/files.d.ts +21 -0
- package/dist/server/files.js +38 -0
- package/dist/server/http-server.js +1580 -1402
- package/dist/server/index.js +23 -6
- package/dist/server/logging.js +56 -3
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +1 -0
- package/dist/server/pi-session.d.ts +118 -0
- package/dist/server/pi-session.js +132 -10
- package/dist/server/runtime.d.ts +48 -0
- package/dist/server/runtime.js +189 -86
- 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/store.d.ts +44 -1
- package/dist/server/store.js +114 -12
- package/dist/server/subagent-runs.d.ts +31 -1
- package/dist/server/subagent-runs.js +48 -20
- package/dist/server/subagents.js +7 -0
- package/dist/server/task-tool.d.ts +18 -0
- package/dist/server/task-tool.js +85 -0
- package/dist/server/tool-runner.js +13 -4
- package/dist/server/types.d.ts +50 -0
- package/dist/server/ws-server.d.ts +86 -6
- package/dist/server/ws-server.js +291 -51
- package/package.json +16 -2
- package/scripts/diagnose.mjs +161 -58
- package/dist/client/assets/index-FxTrUapG.css +0 -1
package/dist/server/store.d.ts
CHANGED
|
@@ -119,6 +119,29 @@ export declare class Store {
|
|
|
119
119
|
*/
|
|
120
120
|
workspaceForConversation(conversationId: string): Workspace | undefined;
|
|
121
121
|
workspace(workspaceId: string): Workspace | undefined;
|
|
122
|
+
/** Read the app-wide default without cloning conversations or transcripts. */
|
|
123
|
+
defaultModel(): ModelSelection | undefined;
|
|
124
|
+
/** Read one detached conversation for lifecycle operations. */
|
|
125
|
+
conversation(conversationId: string): Conversation | undefined;
|
|
126
|
+
/** Return every pending question in one conversation without cloning unrelated history. */
|
|
127
|
+
pendingQuestions(conversationId: string): Array<{
|
|
128
|
+
messageId: string;
|
|
129
|
+
index: number;
|
|
130
|
+
question: ToolCallQuestion;
|
|
131
|
+
}>;
|
|
132
|
+
/**
|
|
133
|
+
* THE SCHEDULED TASKS, WITHOUT CLONING THE STATE TO FIND THEM.
|
|
134
|
+
*
|
|
135
|
+
* The same argument as the readers above, with a sharper edge: the scheduled-task clock sweeps
|
|
136
|
+
* every fifteen seconds and used to read its three-to-zero tasks through `snapshot()` - a deep
|
|
137
|
+
* clone of the whole application, every fifteen seconds, whether the user has any scheduled task or
|
|
138
|
+
* not. On a server holding a long transcript that is a full copy of the app four times a minute,
|
|
139
|
+
* produced for a list that is usually empty.
|
|
140
|
+
*
|
|
141
|
+
* The tasks are returned as a shallow copy of the array: the sweep reads `enabled` and
|
|
142
|
+
* `nextRunAt` and then hands the whole task back to the store, which owns the real one.
|
|
143
|
+
*/
|
|
144
|
+
scheduledTasks(): ScheduledTask[];
|
|
122
145
|
/** Small detached rows used when refreshing each conversation's scoped model catalog. */
|
|
123
146
|
catalogConversationTargets(): Array<Pick<Conversation, "id" | "archivedAt" | "model">>;
|
|
124
147
|
/** Clone only one workspace's transcripts, never unrelated workspaces or app state. */
|
|
@@ -239,11 +262,14 @@ export declare class Store {
|
|
|
239
262
|
* Filtering HERE, in one place, rather than in every reader, is what keeps the client dumb: the
|
|
240
263
|
* sidebar renders `state.workspaces` and never has to know that a closed workspace exists.
|
|
241
264
|
*/
|
|
242
|
-
clientState(
|
|
265
|
+
clientState(scope?: {
|
|
266
|
+
conversationId?: string;
|
|
267
|
+
}): AppState;
|
|
243
268
|
createConversation(workspaceId: string, configuration?: {
|
|
244
269
|
agentId?: string;
|
|
245
270
|
model?: ModelSelection | null;
|
|
246
271
|
thinkingLevel?: string | null;
|
|
272
|
+
modelNotice?: string;
|
|
247
273
|
}): Promise<Conversation>;
|
|
248
274
|
/**
|
|
249
275
|
* Copies a conversation into a NEW one, under an id the caller has already minted.
|
|
@@ -480,6 +506,20 @@ export declare class Store {
|
|
|
480
506
|
* Every field is `undefined`-tested for the same reason `settleToolCall` does it: `""` and `[]` are
|
|
481
507
|
* values a tool can genuinely produce, and treating them as "not reported" would erase a real answer.
|
|
482
508
|
*/
|
|
509
|
+
/**
|
|
510
|
+
* THE TOOL-CALL WRITES, PERSISTED AS ONE MESSAGE INSTEAD OF THE WHOLE APPLICATION.
|
|
511
|
+
*
|
|
512
|
+
* A tool call's lifecycle touches exactly ONE thing: the `toolCalls` array of ONE message. The
|
|
513
|
+
* write used to be `this.persist()`, which hands the ENTIRE state to the repository - every
|
|
514
|
+
* workspace, every conversation, every message of every conversation - to be serialized and
|
|
515
|
+
* written. A turn that calls five tools therefore wrote the whole application ten to fifteen
|
|
516
|
+
* times, and every one of those writes was proportional to the whole history rather than to the
|
|
517
|
+
* one row that changed (LAT-020).
|
|
518
|
+
*
|
|
519
|
+
* `message.replace` is the transaction that means 「this one message now reads like this」, and it
|
|
520
|
+
* is exactly the write this is: the row's content did not change, only its tool calls did.
|
|
521
|
+
*/
|
|
522
|
+
private persistToolCallMessage;
|
|
483
523
|
updateToolCall(conversationId: string, messageId: string, index: number, patch: {
|
|
484
524
|
label?: string;
|
|
485
525
|
detail?: string;
|
|
@@ -487,6 +527,9 @@ export declare class Store {
|
|
|
487
527
|
live?: string;
|
|
488
528
|
preparing?: boolean;
|
|
489
529
|
question?: ToolCallQuestion;
|
|
530
|
+
at?: number;
|
|
531
|
+
thinkingAt?: number;
|
|
532
|
+
thinkingBlock?: number | null;
|
|
490
533
|
backgroundTask?: ToolCallBlock["backgroundTask"];
|
|
491
534
|
}): Promise<ToolCallBlock[]>;
|
|
492
535
|
/**
|
package/dist/server/store.js
CHANGED
|
@@ -721,6 +721,47 @@ export class Store {
|
|
|
721
721
|
const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
|
|
722
722
|
return workspace ? { ...workspace } : undefined;
|
|
723
723
|
}
|
|
724
|
+
/** Read the app-wide default without cloning conversations or transcripts. */
|
|
725
|
+
defaultModel() {
|
|
726
|
+
const model = this.state.defaultModel;
|
|
727
|
+
return model ? { ...model } : undefined;
|
|
728
|
+
}
|
|
729
|
+
/** Read one detached conversation for lifecycle operations. */
|
|
730
|
+
conversation(conversationId) {
|
|
731
|
+
const conversation = this.state.conversations.find((item) => item.id === conversationId);
|
|
732
|
+
return conversation ? structuredClone(conversation) : undefined;
|
|
733
|
+
}
|
|
734
|
+
/** Return every pending question in one conversation without cloning unrelated history. */
|
|
735
|
+
pendingQuestions(conversationId) {
|
|
736
|
+
const conversation = this.state.conversations.find((item) => item.id === conversationId);
|
|
737
|
+
if (!conversation)
|
|
738
|
+
return [];
|
|
739
|
+
const pending = [];
|
|
740
|
+
for (const message of conversation.messages) {
|
|
741
|
+
for (let index = 0; index < (message.toolCalls?.length ?? 0); index += 1) {
|
|
742
|
+
const question = message.toolCalls?.[index]?.question;
|
|
743
|
+
if (message.toolCalls?.[index]?.label === "question" && question?.status === "pending") {
|
|
744
|
+
pending.push({ messageId: message.id, index, question: structuredClone(question) });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return pending;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* THE SCHEDULED TASKS, WITHOUT CLONING THE STATE TO FIND THEM.
|
|
752
|
+
*
|
|
753
|
+
* The same argument as the readers above, with a sharper edge: the scheduled-task clock sweeps
|
|
754
|
+
* every fifteen seconds and used to read its three-to-zero tasks through `snapshot()` - a deep
|
|
755
|
+
* clone of the whole application, every fifteen seconds, whether the user has any scheduled task or
|
|
756
|
+
* not. On a server holding a long transcript that is a full copy of the app four times a minute,
|
|
757
|
+
* produced for a list that is usually empty.
|
|
758
|
+
*
|
|
759
|
+
* The tasks are returned as a shallow copy of the array: the sweep reads `enabled` and
|
|
760
|
+
* `nextRunAt` and then hands the whole task back to the store, which owns the real one.
|
|
761
|
+
*/
|
|
762
|
+
scheduledTasks() {
|
|
763
|
+
return this.state.scheduledTasks ? [...this.state.scheduledTasks] : [];
|
|
764
|
+
}
|
|
724
765
|
/** Small detached rows used when refreshing each conversation's scoped model catalog. */
|
|
725
766
|
catalogConversationTargets() {
|
|
726
767
|
return this.state.conversations.map(({ id, archivedAt, model }) => ({
|
|
@@ -788,8 +829,10 @@ export class Store {
|
|
|
788
829
|
snapshot() {
|
|
789
830
|
const startedAt = process.env.SUPER_PI_TRACE_LATENCY === "1" ? performance.now() : 0;
|
|
790
831
|
const value = structuredClone(this.state);
|
|
832
|
+
// Do not stringify here just to count bytes: that makes tracing add a second full serialization
|
|
833
|
+
// and mislabels that extra work as clone time. The broadcaster measures the actual JSON payload.
|
|
791
834
|
if (startedAt)
|
|
792
|
-
console.warn(`[super-pi latency] phase=state.snapshot
|
|
835
|
+
console.warn(`[super-pi latency] phase=state.snapshot cloneMs=${Math.round(performance.now() - startedAt)}`);
|
|
793
836
|
return value;
|
|
794
837
|
}
|
|
795
838
|
/** Cheap scalar/entity reads for hot paths. Never expose the live mutable state object. */
|
|
@@ -981,14 +1024,32 @@ export class Store {
|
|
|
981
1024
|
* Filtering HERE, in one place, rather than in every reader, is what keeps the client dumb: the
|
|
982
1025
|
* sidebar renders `state.workspaces` and never has to know that a closed workspace exists.
|
|
983
1026
|
*/
|
|
984
|
-
clientState() {
|
|
985
|
-
const
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1027
|
+
clientState(scope) {
|
|
1028
|
+
const closed = new Set(this.state.workspaces.filter((item) => item.closedAt).map((item) => item.id));
|
|
1029
|
+
const workspaces = this.state.workspaces.filter((item) => !closed.has(item.id));
|
|
1030
|
+
const visible = this.state.conversations.filter((item) => !closed.has(item.workspaceId));
|
|
1031
|
+
// Legacy sockets still receive complete visible transcripts. Project before cloning so a closed
|
|
1032
|
+
// workspace's retained history is never copied just to discard it afterwards.
|
|
1033
|
+
if (scope === undefined)
|
|
1034
|
+
return structuredClone({ ...this.state, workspaces, conversations: visible });
|
|
1035
|
+
// A focused socket keeps every sidebar row, but owns only the selected transcript. An empty scope
|
|
1036
|
+
// deliberately loads none; a missing/closed selection must not expose retained workspace history.
|
|
1037
|
+
const selectedId = visible.find((item) => item.id === scope.conversationId)?.id;
|
|
1038
|
+
const conversations = visible.map(({ messages, ...metadata }) => ({
|
|
1039
|
+
...metadata,
|
|
1040
|
+
messages: metadata.id === selectedId ? messages : [],
|
|
1041
|
+
messagesLoaded: metadata.id === selectedId,
|
|
1042
|
+
hasPendingQuestion: messages.some((message) => message.toolCalls?.some((call) => call.question?.status === "pending")),
|
|
1043
|
+
}));
|
|
1044
|
+
return structuredClone({
|
|
1045
|
+
...this.state,
|
|
1046
|
+
workspaces,
|
|
1047
|
+
conversations,
|
|
1048
|
+
subagentRuns: this.state.subagentRuns.filter((run) => run.conversationId === selectedId),
|
|
1049
|
+
...(this.state.workflows ? { workflows: this.state.workflows.filter((run) => run.conversationId === selectedId) } : {}),
|
|
1050
|
+
...(this.state.todosByConversation ? { todosByConversation: selectedId && this.state.todosByConversation[selectedId]
|
|
1051
|
+
? { [selectedId]: this.state.todosByConversation[selectedId] } : {} } : {}),
|
|
1052
|
+
});
|
|
992
1053
|
}
|
|
993
1054
|
async createConversation(workspaceId, configuration) {
|
|
994
1055
|
const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
|
|
@@ -1007,6 +1068,10 @@ export class Store {
|
|
|
1007
1068
|
if (workspace.defaultAgentId)
|
|
1008
1069
|
conversation.agentId = workspace.defaultAgentId;
|
|
1009
1070
|
if (configuration) {
|
|
1071
|
+
// WHY THIS CONVERSATION IS NOT ON THE MODEL THE USER SET. Set once, at birth, and never
|
|
1072
|
+
// rewritten: it describes how this conversation came to be, not what is true right now.
|
|
1073
|
+
if (configuration.modelNotice !== undefined)
|
|
1074
|
+
conversation.modelNotice = configuration.modelNotice;
|
|
1010
1075
|
if (configuration.model !== undefined) {
|
|
1011
1076
|
if (configuration.model)
|
|
1012
1077
|
conversation.model = { ...configuration.model };
|
|
@@ -1477,7 +1542,7 @@ export class Store {
|
|
|
1477
1542
|
};
|
|
1478
1543
|
calls.push(structuredClone(placed));
|
|
1479
1544
|
conversation.updatedAt = now();
|
|
1480
|
-
await this.
|
|
1545
|
+
await this.persistToolCallMessage(conversationId, messageId);
|
|
1481
1546
|
return structuredClone(calls);
|
|
1482
1547
|
}
|
|
1483
1548
|
/**
|
|
@@ -1521,7 +1586,7 @@ export class Store {
|
|
|
1521
1586
|
// finished tool block that vanishes the moment the call ends is a block that taught the reader
|
|
1522
1587
|
// nothing. The client prefers `live` when both are present, falling back to `result`.
|
|
1523
1588
|
conversation.updatedAt = now();
|
|
1524
|
-
await this.
|
|
1589
|
+
await this.persistToolCallMessage(conversationId, messageId);
|
|
1525
1590
|
return structuredClone(calls);
|
|
1526
1591
|
}
|
|
1527
1592
|
/**
|
|
@@ -1533,6 +1598,26 @@ export class Store {
|
|
|
1533
1598
|
* Every field is `undefined`-tested for the same reason `settleToolCall` does it: `""` and `[]` are
|
|
1534
1599
|
* values a tool can genuinely produce, and treating them as "not reported" would erase a real answer.
|
|
1535
1600
|
*/
|
|
1601
|
+
/**
|
|
1602
|
+
* THE TOOL-CALL WRITES, PERSISTED AS ONE MESSAGE INSTEAD OF THE WHOLE APPLICATION.
|
|
1603
|
+
*
|
|
1604
|
+
* A tool call's lifecycle touches exactly ONE thing: the `toolCalls` array of ONE message. The
|
|
1605
|
+
* write used to be `this.persist()`, which hands the ENTIRE state to the repository - every
|
|
1606
|
+
* workspace, every conversation, every message of every conversation - to be serialized and
|
|
1607
|
+
* written. A turn that calls five tools therefore wrote the whole application ten to fifteen
|
|
1608
|
+
* times, and every one of those writes was proportional to the whole history rather than to the
|
|
1609
|
+
* one row that changed (LAT-020).
|
|
1610
|
+
*
|
|
1611
|
+
* `message.replace` is the transaction that means 「this one message now reads like this」, and it
|
|
1612
|
+
* is exactly the write this is: the row's content did not change, only its tool calls did.
|
|
1613
|
+
*/
|
|
1614
|
+
async persistToolCallMessage(conversationId, messageId) {
|
|
1615
|
+
const conversation = this.requireConversation(conversationId);
|
|
1616
|
+
const message = conversation.messages.find((item) => item.id === messageId);
|
|
1617
|
+
if (!message)
|
|
1618
|
+
return;
|
|
1619
|
+
await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: conversation.updatedAt });
|
|
1620
|
+
}
|
|
1536
1621
|
async updateToolCall(conversationId, messageId, index, patch) {
|
|
1537
1622
|
const conversation = this.requireConversation(conversationId);
|
|
1538
1623
|
const message = conversation.messages.find((item) => item.id === messageId);
|
|
@@ -1569,11 +1654,28 @@ export class Store {
|
|
|
1569
1654
|
// the call was being written are gone by the time it runs.
|
|
1570
1655
|
if (patch.live !== undefined)
|
|
1571
1656
|
call.live = patch.live;
|
|
1657
|
+
// WHERE THE CALL HAPPENED, corrected when the call is announced. The placeholder stamped the
|
|
1658
|
+
// offsets of the moment it was made, and that moment is not the moment the call belongs to
|
|
1659
|
+
// (UX-008).
|
|
1660
|
+
if (patch.at !== undefined)
|
|
1661
|
+
call.at = patch.at;
|
|
1662
|
+
if (patch.thinkingAt !== undefined)
|
|
1663
|
+
call.thinkingAt = patch.thinkingAt;
|
|
1664
|
+
// WHICH REASONING BLOCK THIS CALL BELONGS TO. It is written when the call is ANNOUNCED, not
|
|
1665
|
+
// when its placeholder was made, and it is written HERE because the placeholder row already
|
|
1666
|
+
// exists by then - a placeholder created while the model was still writing the arguments
|
|
1667
|
+
// cannot know whether the reasoning was over by the time the call was made (UX-008).
|
|
1668
|
+
//
|
|
1669
|
+
// `null` IS A VALUE, NOT AN ABSENCE: it says the call is outside every reasoning block, and
|
|
1670
|
+
// the browser needs to hear that difference from "not decided yet". So the check is
|
|
1671
|
+
// `!== undefined`, never a truthiness test.
|
|
1672
|
+
if (patch.thinkingBlock !== undefined)
|
|
1673
|
+
call.thinkingBlock = patch.thinkingBlock;
|
|
1572
1674
|
// The question rides on the call rather than beside it, so this is the only write it needs.
|
|
1573
1675
|
if (patch.question !== undefined)
|
|
1574
1676
|
call.question = patch.question;
|
|
1575
1677
|
conversation.updatedAt = now();
|
|
1576
|
-
await this.
|
|
1678
|
+
await this.persistToolCallMessage(conversationId, messageId);
|
|
1577
1679
|
return structuredClone(calls);
|
|
1578
1680
|
}
|
|
1579
1681
|
/**
|
|
@@ -15,7 +15,8 @@ import type { ModelSelection, ServerEvent, SubagentRun } from "./types.js";
|
|
|
15
15
|
* mid-run leaves an orphan - and `Store.open` fails that orphan with a reason on the next start.
|
|
16
16
|
* 2. **A snapshot on every delta.** `subagent.event` carries the run as it stands, not just at the end,
|
|
17
17
|
* because streaming a sub-agent is the entire reason Super Pi spawns its own instead of shipping Pi's
|
|
18
|
-
* extension (design §2, reason 1).
|
|
18
|
+
* extension (design §2, reason 1). The snapshot's `output` is the TAIL of the accumulated text
|
|
19
|
+
* ({@link LIVE_OUTPUT_TAIL_CAP}); the whole text is in the store and in the settle event.
|
|
19
20
|
*
|
|
20
21
|
* What deliberately does NOT go into either is the child's transcript. See `SubagentRun`.
|
|
21
22
|
*/
|
|
@@ -29,6 +30,21 @@ import type { ModelSelection, ServerEvent, SubagentRun } from "./types.js";
|
|
|
29
30
|
* what was said. The parent model is handed the capped copy, because that is the window it actually has.
|
|
30
31
|
*/
|
|
31
32
|
export declare const PER_TASK_OUTPUT_CAP: number;
|
|
33
|
+
/**
|
|
34
|
+
* How much of the ACCUMULATED output a delta-phase `subagent.event` may carry.
|
|
35
|
+
*
|
|
36
|
+
* A delta event is a SNAPSHOT the card replaces itself with, and it fires PER DELTA. Carrying the whole
|
|
37
|
+
* accumulated text makes the Nth event N characters long - the traffic is quadratic in the output length
|
|
38
|
+
* (the flood behind "连接中 / 断连 / 连接中", see docs/SUBAGENT-WS-FLOOD-FIX-2026-10-29.md). So a live
|
|
39
|
+
* event carries only the TAIL of what the child has said; the run's full output still reaches everyone -
|
|
40
|
+
* the STORE record is written unabridged on every delta (a mid-run reload rebuilds from `state`, not from
|
|
41
|
+
* events), and the settle event carries the final text in full. A watching human needs the last few
|
|
42
|
+
* hundred characters, not the first ones, so the tail is the useful end.
|
|
43
|
+
*
|
|
44
|
+
* THIS CHANGES WHAT AN OLD CLIENT SEES: a card whose child has said more than this shows the latest 8 KB
|
|
45
|
+
* (sliding) instead of the whole text, then the full text at settle. The field shape is unchanged.
|
|
46
|
+
*/
|
|
47
|
+
export declare const LIVE_OUTPUT_TAIL_CAP: number;
|
|
32
48
|
/** What a caller supplies to dispatch one run. The workspace directory is required, like `AnswerInput.cwd`. */
|
|
33
49
|
export type SubagentDispatch = {
|
|
34
50
|
id?: string;
|
|
@@ -75,6 +91,20 @@ export type SubagentRunHooks = {
|
|
|
75
91
|
* attached without changing the recorder.
|
|
76
92
|
*/
|
|
77
93
|
broadcast: (event: ServerEvent) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Fires ONCE per run, AFTER the terminal `subagent.event` has been broadcast from `settle()`. This -
|
|
96
|
+
* not the delta phase - is when a caller should broadcast a full `state` snapshot: the run's final
|
|
97
|
+
* record is on disk by then, so a browser that reloads after it sees the finished card. Broadcasting
|
|
98
|
+
* state on every DELTA is what flooded the socket (each delta demanded a full state frame; a burst of
|
|
99
|
+
* deltas demanded hundreds).
|
|
100
|
+
*/
|
|
101
|
+
onSettled?: () => void;
|
|
102
|
+
/**
|
|
103
|
+
* Cap on `run.output` in delta-phase events, in characters; defaults to {@link LIVE_OUTPUT_TAIL_CAP}.
|
|
104
|
+
* A settle event always carries the full output regardless. `Number.POSITIVE_INFINITY` disables the
|
|
105
|
+
* cap - the diagnostic "legacy flood" mode uses that to reproduce the pre-fix wire shape.
|
|
106
|
+
*/
|
|
107
|
+
liveTailCap?: number;
|
|
78
108
|
/**
|
|
79
109
|
* Builds the host that answers a CHILD's questions, keyed by the run as well as the conversation
|
|
80
110
|
* (design §4). Optional because a headless caller has nobody to ask; a child that asks in that case is
|
|
@@ -12,7 +12,8 @@ import { SubagentCancelled, newSubagentRunId } from "./subagents.js";
|
|
|
12
12
|
* mid-run leaves an orphan - and `Store.open` fails that orphan with a reason on the next start.
|
|
13
13
|
* 2. **A snapshot on every delta.** `subagent.event` carries the run as it stands, not just at the end,
|
|
14
14
|
* because streaming a sub-agent is the entire reason Super Pi spawns its own instead of shipping Pi's
|
|
15
|
-
* extension (design §2, reason 1).
|
|
15
|
+
* extension (design §2, reason 1). The snapshot's `output` is the TAIL of the accumulated text
|
|
16
|
+
* ({@link LIVE_OUTPUT_TAIL_CAP}); the whole text is in the store and in the settle event.
|
|
16
17
|
*
|
|
17
18
|
* What deliberately does NOT go into either is the child's transcript. See `SubagentRun`.
|
|
18
19
|
*/
|
|
@@ -26,6 +27,21 @@ import { SubagentCancelled, newSubagentRunId } from "./subagents.js";
|
|
|
26
27
|
* what was said. The parent model is handed the capped copy, because that is the window it actually has.
|
|
27
28
|
*/
|
|
28
29
|
export const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
|
30
|
+
/**
|
|
31
|
+
* How much of the ACCUMULATED output a delta-phase `subagent.event` may carry.
|
|
32
|
+
*
|
|
33
|
+
* A delta event is a SNAPSHOT the card replaces itself with, and it fires PER DELTA. Carrying the whole
|
|
34
|
+
* accumulated text makes the Nth event N characters long - the traffic is quadratic in the output length
|
|
35
|
+
* (the flood behind "连接中 / 断连 / 连接中", see docs/SUBAGENT-WS-FLOOD-FIX-2026-10-29.md). So a live
|
|
36
|
+
* event carries only the TAIL of what the child has said; the run's full output still reaches everyone -
|
|
37
|
+
* the STORE record is written unabridged on every delta (a mid-run reload rebuilds from `state`, not from
|
|
38
|
+
* events), and the settle event carries the final text in full. A watching human needs the last few
|
|
39
|
+
* hundred characters, not the first ones, so the tail is the useful end.
|
|
40
|
+
*
|
|
41
|
+
* THIS CHANGES WHAT AN OLD CLIENT SEES: a card whose child has said more than this shows the latest 8 KB
|
|
42
|
+
* (sliding) instead of the whole text, then the full text at settle. The field shape is unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export const LIVE_OUTPUT_TAIL_CAP = 8 * 1024;
|
|
29
45
|
export class SubagentRunRecorder {
|
|
30
46
|
supervisor;
|
|
31
47
|
hooks;
|
|
@@ -125,7 +141,22 @@ export class SubagentRunRecorder {
|
|
|
125
141
|
// Generated HERE, not by `run()`, because the record has to exist first - see this file's header.
|
|
126
142
|
const id = input.id ?? newSubagentRunId();
|
|
127
143
|
const started = { id, conversationId, agentId, task, dispatchedBy, parentRunId: input.parentRunId, depth: input.depth ?? 0, loopId: input.loopId, role: input.role, originMessageId: input.originMessageId, originCallRow: input.originCallRow, status: "running", startedAt: new Date().toISOString(), sessionId: id };
|
|
128
|
-
await
|
|
144
|
+
// Own cancellation before the first persistence await. A conversation stop must also reach a
|
|
145
|
+
// human dispatch whose initial row is still being written, before a supervisor/child exists.
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
this.controllers.set(id, { controller, conversationId });
|
|
148
|
+
const onCallerAbort = () => controller.abort();
|
|
149
|
+
input.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
150
|
+
if (input.signal?.aborted)
|
|
151
|
+
controller.abort();
|
|
152
|
+
try {
|
|
153
|
+
await this.hooks.store.beginSubagentRun(started);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
input.signal?.removeEventListener("abort", onCallerAbort);
|
|
157
|
+
this.controllers.delete(id);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
129
160
|
// The recorder's own copy, held separately from the store's. Mutating the object `beginSubagentRun`
|
|
130
161
|
// was handed would reach into the store's state behind its back, and every snapshot it hands out
|
|
131
162
|
// would then move under its reader.
|
|
@@ -159,24 +190,22 @@ export class SubagentRunRecorder {
|
|
|
159
190
|
* and is what a card should say about a run whose child never existed. See `SubagentFailure`.
|
|
160
191
|
*/
|
|
161
192
|
let stage = "dispatch";
|
|
193
|
+
// Live events carry only the tail of the accumulated output (see LIVE_OUTPUT_TAIL_CAP) so a
|
|
194
|
+
// per-delta snapshot stays small no matter how much the child has said.
|
|
195
|
+
const liveTailCap = this.hooks.liveTailCap ?? LIVE_OUTPUT_TAIL_CAP;
|
|
162
196
|
const publish = () => {
|
|
163
|
-
record.output = streamed;
|
|
197
|
+
record.output = streamed.length > liveTailCap ? streamed.slice(streamed.length - liveTailCap) : streamed;
|
|
164
198
|
record.toolCalls = toolCalls;
|
|
165
199
|
record.activity = activity;
|
|
166
200
|
// WRITE BEFORE ANNOUNCE, and this is the whole defect. The order of the two statements below is
|
|
167
201
|
// load-bearing; swapping them back reintroduces a card that loses text it already streamed.
|
|
168
202
|
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// `state`
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// Why some cards lived anyway and this one did not: a child that keeps talking recovers by accident,
|
|
176
|
-
// because its NEXT delta publishes a `state` once the write queue has drained. A child that says one
|
|
177
|
-
// thing and goes quiet never publishes again, so nothing restores it - and it is not only that child's
|
|
178
|
-
// card. Every later `state` broadcast from any command, including ones about an unrelated
|
|
179
|
-
// conversation, reads that same stale record.
|
|
203
|
+
// Announcing before the store write means any `state` snapshot taken after the announce - from
|
|
204
|
+
// whatever command, this run's or an unrelated conversation's - still reads the PRE-delta record.
|
|
205
|
+
// The browser REPLACES on `state` (`App.tsx`), so a card that had just been handed the right text
|
|
206
|
+
// would have it taken back one frame later. This run's own delta phase no longer triggers a
|
|
207
|
+
// `state` broadcast at all (the caller hooks that via `onSettled` at settle time only), but OTHER
|
|
208
|
+
// commands broadcast `state` while this child streams, and every one of them reads this record.
|
|
180
209
|
//
|
|
181
210
|
// The write is still not awaited, and still must not be: `onTextDelta` is synchronous, and a drain loop
|
|
182
211
|
// that flushed to disk per token would throttle the child. Reordering buys the correctness anyway,
|
|
@@ -207,6 +236,9 @@ export class SubagentRunRecorder {
|
|
|
207
236
|
// the correct trade: never announce state that a reload would contradict.
|
|
208
237
|
const settled = await this.hooks.store.setSubagentRun(id, changes);
|
|
209
238
|
this.hooks.broadcast({ type: "subagent.event", run: structuredClone(record) });
|
|
239
|
+
// The ONE moment a full `state` broadcast is needed: the final record is on disk NOW, so a browser
|
|
240
|
+
// that reloads after this point rebuilds the finished card from `state`. See the `onSettled` doc.
|
|
241
|
+
this.hooks.onSettled?.();
|
|
210
242
|
return settled;
|
|
211
243
|
};
|
|
212
244
|
/**
|
|
@@ -219,13 +251,9 @@ export class SubagentRunRecorder {
|
|
|
219
251
|
* run happened.
|
|
220
252
|
*/
|
|
221
253
|
const elapsedMs = () => Math.max(0, Date.now() - new Date(started.startedAt).getTime());
|
|
222
|
-
// One controller for both abort sources: the caller's signal and `abortAll` are the same button from
|
|
223
|
-
// the child's point of view, and `run()` takes exactly one.
|
|
224
|
-
const controller = new AbortController();
|
|
225
|
-
this.controllers.set(id, { controller, conversationId });
|
|
226
|
-
const onCallerAbort = () => controller.abort();
|
|
227
|
-
input.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
228
254
|
try {
|
|
255
|
+
if (controller.signal.aborted)
|
|
256
|
+
throw new SubagentCancelled(id);
|
|
229
257
|
const result = await this.supervisor.run({
|
|
230
258
|
id,
|
|
231
259
|
conversationId,
|
package/dist/server/subagents.js
CHANGED
|
@@ -265,6 +265,13 @@ export class SubagentSupervisor {
|
|
|
265
265
|
args.push("--provider", chosen.provider, "--model", chosen.modelId);
|
|
266
266
|
if (agent.thinkingLevel)
|
|
267
267
|
args.push("--thinking", agent.thinkingLevel);
|
|
268
|
+
// Cancellation may arrive during filesystem/config/model preparation, before the child or its
|
|
269
|
+
// abort listener exists. Refuse the spawn instead of starting work after the user pressed stop.
|
|
270
|
+
if (signal?.aborted) {
|
|
271
|
+
if (memoryToken)
|
|
272
|
+
this.memory?.release(memoryToken);
|
|
273
|
+
throw new SubagentCancelled(id);
|
|
274
|
+
}
|
|
268
275
|
// npm exposes global CLIs as .cmd shims on Windows; Node requires a shell to launch those shims.
|
|
269
276
|
const child = spawn(shellQuote(this.pi.command), args, { cwd: childCwd, stdio: ["pipe", "pipe", "pipe"], shell: process.platform === "win32", ...(memoryToken ? { env: { ...process.env, SUPER_PI_MEMORY_ENDPOINT: process.env.SUPER_PI_MEMORY_ENDPOINT, SUPER_PI_MEMORY_TOKEN: memoryToken, SUPER_PI_MEMORY_READ_ONLY: "1" } } : {}) });
|
|
270
277
|
if (memoryToken)
|
|
@@ -244,3 +244,21 @@ export declare function normalizeTodos(raw: unknown): {
|
|
|
244
244
|
* header are computed from the same stored list and cannot disagree.
|
|
245
245
|
*/
|
|
246
246
|
export declare function todoSummary(todos: readonly TodoItem[]): string;
|
|
247
|
+
/**
|
|
248
|
+
* The orchestrator extension's half of the environment contract.
|
|
249
|
+
* `agents/super-pi-task/core.ts` is the other end; `scripts/task-tool.test.mjs` asserts the two copies agree.
|
|
250
|
+
*/
|
|
251
|
+
export declare const ORCHESTRATOR_ENDPOINT_ENV = "SUPER_PI_ORCHESTRATOR_ENDPOINT";
|
|
252
|
+
/** Tool names registered with Pi. */
|
|
253
|
+
export declare const SESSION_SPAWN_TOOL_NAME = "session_spawn";
|
|
254
|
+
export declare const SESSION_STATUS_TOOL_NAME = "session_status";
|
|
255
|
+
export declare const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
|
|
256
|
+
export declare const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
257
|
+
/** Model-facing description for session_spawn. */
|
|
258
|
+
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.";
|
|
259
|
+
/** Model-facing description for session_status. */
|
|
260
|
+
export declare const SESSION_STATUS_DESCRIPTION = "Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.\n\nWHEN TO USE THIS:\n- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.\n- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.\n- To list all sibling conversations under your coordination when conversationId is omitted.\n\nUsage notes:\n1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).\n2. When 'conversationId' is omitted: returns a summary list of ALL sibling conversations spawned by this orchestrator session.\n3. This tool never returns full transcripts; it returns concise summaries and snippets to keep your context window lean.";
|
|
261
|
+
/** Model-facing description for session_dispatch. */
|
|
262
|
+
export declare const SESSION_DISPATCH_DESCRIPTION = "Send a message, follow-up instructions, or steering guidance to an existing sibling conversation.\n\nWHEN TO USE THIS:\n- To follow up with a sibling session after reading its progress with session_status.\n- To give feedback, provide additional requirements, or answer a question from a sibling session.\n- To kick off a new turn on an idle or stopped sibling session.\n\nUsage notes:\n1. 'conversationId' is required and must identify an existing sibling conversation.\n2. 'content' is the text instruction sent to that session, just as if typed by a user in that session's chat.\n3. Dispatch is ASYNCHRONOUS by default: it enqueues the message into the session's queue and returns immediately.\n4. If the sibling session is currently busy running a turn, the dispatched message is placed in its waiting queue and processed when the active turn completes.";
|
|
263
|
+
/** Model-facing description for session_stop. */
|
|
264
|
+
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.";
|
package/dist/server/task-tool.js
CHANGED
|
@@ -333,3 +333,88 @@ export function todoSummary(todos) {
|
|
|
333
333
|
return `全部完成:${todos.length} 个待办任务`;
|
|
334
334
|
return `${open} 个待办未完成,共 ${todos.length} 个`;
|
|
335
335
|
}
|
|
336
|
+
// ---------------------------------------------------------------------------------------------
|
|
337
|
+
// The `session_*` orchestrator tools: manage sibling conversations in the current workspace.
|
|
338
|
+
//
|
|
339
|
+
// WHY THESE TOOLS EXIST. A primary agent can already delegate a single transient job to a subagent
|
|
340
|
+
// via `task` (a one-shot Pi process), or pipeline 2-8 steps via `workflow`. What was missing was the
|
|
341
|
+
// ability to spin up INDEPENDENT, PERSISTENT sibling conversations in Super Pi — sessions that appear
|
|
342
|
+
// in the workspace sidebar, carry their own full message history, run concurrently in the background,
|
|
343
|
+
// and can be conversed with over multiple turns via `session_dispatch`, monitored via `session_status`,
|
|
344
|
+
// or cancelled via `session_stop`.
|
|
345
|
+
//
|
|
346
|
+
// WHY THEY DIFFER FROM `task`. `task` runs an ephemeral child process: it has no conversation history,
|
|
347
|
+
// cannot be conversed with across turns, does not appear in the sidebar, and dies upon returning its
|
|
348
|
+
// final text. Sibling conversations are first-class Super Pi conversations: they retain their own
|
|
349
|
+
// transcripts, can be opened and viewed by the user in the UI, and can execute complex multi-phase
|
|
350
|
+
// work in parallel with the orchestrator.
|
|
351
|
+
// ---------------------------------------------------------------------------------------------
|
|
352
|
+
/**
|
|
353
|
+
* The orchestrator extension's half of the environment contract.
|
|
354
|
+
* `agents/super-pi-task/core.ts` is the other end; `scripts/task-tool.test.mjs` asserts the two copies agree.
|
|
355
|
+
*/
|
|
356
|
+
export const ORCHESTRATOR_ENDPOINT_ENV = "SUPER_PI_ORCHESTRATOR_ENDPOINT";
|
|
357
|
+
/** Tool names registered with Pi. */
|
|
358
|
+
export const SESSION_SPAWN_TOOL_NAME = "session_spawn";
|
|
359
|
+
export const SESSION_STATUS_TOOL_NAME = "session_status";
|
|
360
|
+
export const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
|
|
361
|
+
export const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
362
|
+
/** Model-facing description for session_spawn. */
|
|
363
|
+
export const SESSION_SPAWN_DESCRIPTION = `Create a new sibling conversation in the current workspace and immediately dispatch its initial task.
|
|
364
|
+
|
|
365
|
+
DIFFERENCE FROM THE 'task' TOOL:
|
|
366
|
+
- 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.
|
|
367
|
+
- '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.
|
|
368
|
+
|
|
369
|
+
WHEN TO USE THIS:
|
|
370
|
+
- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.
|
|
371
|
+
- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.
|
|
372
|
+
- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.
|
|
373
|
+
|
|
374
|
+
WHEN NOT TO USE THIS:
|
|
375
|
+
- 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.
|
|
376
|
+
- For running shell commands or build watchers: use 'run_async'.
|
|
377
|
+
- For a linear multi-step subagent pipeline: use 'workflow'.
|
|
378
|
+
|
|
379
|
+
Usage notes:
|
|
380
|
+
1. 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.
|
|
381
|
+
2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.
|
|
382
|
+
3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.`;
|
|
383
|
+
/** Model-facing description for session_status. */
|
|
384
|
+
export const SESSION_STATUS_DESCRIPTION = `Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.
|
|
385
|
+
|
|
386
|
+
WHEN TO USE THIS:
|
|
387
|
+
- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.
|
|
388
|
+
- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.
|
|
389
|
+
- To list all sibling conversations under your coordination when conversationId is omitted.
|
|
390
|
+
|
|
391
|
+
Usage notes:
|
|
392
|
+
1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).
|
|
393
|
+
2. When 'conversationId' is omitted: returns a summary list of ALL sibling conversations spawned by this orchestrator session.
|
|
394
|
+
3. This tool never returns full transcripts; it returns concise summaries and snippets to keep your context window lean.`;
|
|
395
|
+
/** Model-facing description for session_dispatch. */
|
|
396
|
+
export const SESSION_DISPATCH_DESCRIPTION = `Send a message, follow-up instructions, or steering guidance to an existing sibling conversation.
|
|
397
|
+
|
|
398
|
+
WHEN TO USE THIS:
|
|
399
|
+
- To follow up with a sibling session after reading its progress with session_status.
|
|
400
|
+
- To give feedback, provide additional requirements, or answer a question from a sibling session.
|
|
401
|
+
- To kick off a new turn on an idle or stopped sibling session.
|
|
402
|
+
|
|
403
|
+
Usage notes:
|
|
404
|
+
1. 'conversationId' is required and must identify an existing sibling conversation.
|
|
405
|
+
2. 'content' is the text instruction sent to that session, just as if typed by a user in that session's chat.
|
|
406
|
+
3. Dispatch is ASYNCHRONOUS by default: it enqueues the message into the session's queue and returns immediately.
|
|
407
|
+
4. If the sibling session is currently busy running a turn, the dispatched message is placed in its waiting queue and processed when the active turn completes.`;
|
|
408
|
+
/** Model-facing description for session_stop. */
|
|
409
|
+
export const SESSION_STOP_DESCRIPTION = `Stop execution in a sibling conversation.
|
|
410
|
+
|
|
411
|
+
WHEN TO USE THIS:
|
|
412
|
+
- 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.
|
|
413
|
+
- To clear pending queued messages before they start running.
|
|
414
|
+
|
|
415
|
+
Usage notes:
|
|
416
|
+
1. 'mode' controls what to stop:
|
|
417
|
+
- 'all' (default): stops the currently running turn and cancels all pending queued messages in the session.
|
|
418
|
+
- 'active': only interrupts the active running turn; leaves any pending queued messages untouched.
|
|
419
|
+
- 'queued': only cancels pending queued messages; lets the currently running turn complete naturally.
|
|
420
|
+
2. Use 'messageId' if you want to cancel or interrupt one specific queued message instead of all queued messages.`;
|
|
@@ -156,8 +156,15 @@ export class ToolRunner {
|
|
|
156
156
|
// -I ignores user Python configuration and user site packages. No shell means params never become code.
|
|
157
157
|
const child = spawn(python.path, ["-I", "-u", input.script], { cwd: input.cwd, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, TEMP: process.env.TEMP, TMP: process.env.TMP, PYTHONIOENCODING: "utf-8", PYTHONNOUSERSITE: "1", SUPER_PI_TOOL_ID: input.toolId } });
|
|
158
158
|
active.child = child;
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// A PIPE CARRIES BYTES, AND A READ BOUNDARY CAN LAND INSIDE A CHARACTER. A Chinese character is
|
|
160
|
+
// three bytes, so decoding one chunk at a time turns every character that straddles a boundary
|
|
161
|
+
// into U+FFFD - the same output comes out damaged on one machine and whole on another. The
|
|
162
|
+
// stream decodes instead, and Node keeps an incomplete character until the rest of its bytes
|
|
163
|
+
// arrive.
|
|
164
|
+
child.stdout.setEncoding("utf8");
|
|
165
|
+
child.stderr.setEncoding("utf8");
|
|
166
|
+
child.stdout.on("data", (text) => { active.stdout = clipped(`${active.stdout}${text}`); });
|
|
167
|
+
child.stderr.on("data", (text) => logLine(active, text));
|
|
161
168
|
child.once("error", (error) => this.finish(id, "failed", error.message));
|
|
162
169
|
child.once("close", (code) => {
|
|
163
170
|
if (active.run.status !== "running")
|
|
@@ -210,8 +217,10 @@ function runCapture(python, args, timeoutMs, onOutput) {
|
|
|
210
217
|
}
|
|
211
218
|
catch { } resolve({ ok: false, code: null, stdout, error: `timed out after ${timeoutMs}ms` }); }, timeoutMs);
|
|
212
219
|
timer.unref();
|
|
213
|
-
child.stdout.
|
|
214
|
-
child.stderr.
|
|
220
|
+
child.stdout.setEncoding("utf8");
|
|
221
|
+
child.stderr.setEncoding("utf8");
|
|
222
|
+
child.stdout.on("data", (text) => { stdout += text; onOutput?.(text); });
|
|
223
|
+
child.stderr.on("data", (text) => { stderr += text; onOutput?.(text); });
|
|
215
224
|
child.once("error", (error) => { if (settled)
|
|
216
225
|
return; settled = true; clearTimeout(timer); resolve({ ok: false, code: null, stdout, error: error.message }); });
|
|
217
226
|
child.once("close", (code) => { if (settled)
|