@evandu/super-pi 0.1.139 → 0.1.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/super-pi-task/core.ts +418 -1
- package/agents/super-pi-task/index.ts +115 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +7 -5
- package/dist/client/assets/{CodeMirrorEditor-H7SHGhdS.js → CodeMirrorEditor-Dm5IDKy9.js} +2 -2
- package/dist/client/assets/{dist-CZ9Krsno.js → dist-4baaMdqn.js} +1 -1
- package/dist/client/assets/{dist-Jw-GQ-re.js → dist-B-edBMDa.js} +1 -1
- package/dist/client/assets/{dist-zUJqdbfo.js → dist-BGaKp7BG.js} +1 -1
- package/dist/client/assets/{dist-2XXLO-IZ.js → dist-BMbwbGH9.js} +1 -1
- package/dist/client/assets/{dist-NlopeJ5R.js → dist-BqXU6S8h.js} +1 -1
- package/dist/client/assets/{dist-BEM6tttY.js → dist-BvCNiV_z.js} +1 -1
- package/dist/client/assets/{dist-CukPKrEn.js → dist-C7UQGfmz.js} +1 -1
- package/dist/client/assets/{dist-BfUeRJKr.js → dist-CMDW84Eh.js} +1 -1
- package/dist/client/assets/{dist-C8NllalC.js → dist-CN1j5LO2.js} +1 -1
- package/dist/client/assets/{dist-CUNctZ5v.js → dist-CNgQgdyS.js} +1 -1
- package/dist/client/assets/{dist-C_ftUQZN.js → dist-CSDLiQKt.js} +1 -1
- package/dist/client/assets/{dist-cP2vnTP_.js → dist-D0-9TI_k.js} +1 -1
- package/dist/client/assets/{dist-POoQ4j29.js → dist-D0eIQjoI.js} +1 -1
- package/dist/client/assets/{dist-BoucVzKA.js → dist-D7YZUqfA.js} +1 -1
- package/dist/client/assets/{dist-BokzcFLE.js → dist-DQWPdnen.js} +1 -1
- package/dist/client/assets/{dist-DJ8SwQl_.js → dist-DWj0wg8M.js} +1 -1
- package/dist/client/assets/{dist-BxTGjU84.js → dist-DtO-7WEZ.js} +1 -1
- package/dist/client/assets/{dist-D3Ta0y4e.js → dist-F7KOq8Xm.js} +1 -1
- package/dist/client/assets/{dist-DLKVFZhO.js → dist-GAPsnCM8.js} +1 -1
- package/dist/client/assets/{dist-CuMnZOGE.js → dist-LCDxbTTI.js} +1 -1
- package/dist/client/assets/{dist-CSdzvQnf.js → dist-M3BFsjal.js} +1 -1
- package/dist/client/assets/{dist-C-MUXK_t.js → dist-PQZBepmy.js} +1 -1
- package/dist/client/assets/index-BJIfLtDs.css +1 -0
- package/dist/client/assets/{index-B32VZVYx.js → index-DHsjaI7m.js} +13 -13
- package/dist/client/index.html +2 -2
- package/dist/server/agents.d.ts +2 -0
- package/dist/server/agents.js +10 -3
- package/dist/server/application/conversation-service.d.ts +15 -1
- package/dist/server/application/conversation-service.js +18 -2
- package/dist/server/application/run-service.d.ts +1 -0
- package/dist/server/application/run-service.js +121 -20
- package/dist/server/application/session-orchestrator-service.d.ts +113 -0
- package/dist/server/application/session-orchestrator-service.js +241 -0
- package/dist/server/command-handler.js +4 -0
- package/dist/server/http-server.js +135 -0
- package/dist/server/index.js +20 -2
- package/dist/server/runtime.d.ts +1 -0
- package/dist/server/runtime.js +6 -2
- package/dist/server/server-context.d.ts +2 -0
- package/dist/server/session-ledger.d.ts +60 -0
- package/dist/server/session-ledger.js +76 -0
- package/dist/server/store.d.ts +10 -0
- package/dist/server/store.js +38 -0
- package/dist/server/subagent-runs.d.ts +31 -1
- package/dist/server/subagent-runs.js +30 -13
- package/dist/server/task-tool.d.ts +18 -0
- package/dist/server/task-tool.js +85 -0
- package/dist/server/types.d.ts +56 -1
- package/dist/server/ws-server.d.ts +52 -1
- package/dist/server/ws-server.js +151 -35
- package/package.json +4 -2
- package/dist/client/assets/index-Bdym8BPo.css +0 -1
package/dist/server/store.d.ts
CHANGED
|
@@ -270,7 +270,17 @@ export declare class Store {
|
|
|
270
270
|
model?: ModelSelection | null;
|
|
271
271
|
thinkingLevel?: string | null;
|
|
272
272
|
modelNotice?: string;
|
|
273
|
+
parentConversationId?: string;
|
|
274
|
+
origin?: "user" | "orchestrator";
|
|
273
275
|
}): Promise<Conversation>;
|
|
276
|
+
/**
|
|
277
|
+
* Records (or clears) one conversation as collapsed in the sidebar's session tree. The list is a
|
|
278
|
+
* SET in array clothing - order means nothing, duplicates must not accumulate - and it follows the
|
|
279
|
+
* assign -> persist -> rollback pattern `setTerminalShell` established: the in-memory value is the
|
|
280
|
+
* previous value again the moment persistence fails, so a rejected write can never leave the
|
|
281
|
+
* screen and the disk disagreeing.
|
|
282
|
+
*/
|
|
283
|
+
setConversationCollapsed(conversationId: string, collapsed: boolean): Promise<void>;
|
|
274
284
|
/**
|
|
275
285
|
* Copies a conversation into a NEW one, under an id the caller has already minted.
|
|
276
286
|
*
|
package/dist/server/store.js
CHANGED
|
@@ -1086,11 +1086,49 @@ export class Store {
|
|
|
1086
1086
|
}
|
|
1087
1087
|
if (configuration.agentId)
|
|
1088
1088
|
conversation.agentId = configuration.agentId;
|
|
1089
|
+
// THE SESSION TREE'S EDGE, WRITTEN AT BIRTH. Both fields describe how this conversation CAME TO
|
|
1090
|
+
// BE, exactly like modelNotice above, so they are set once here and never rewritten: the tree
|
|
1091
|
+
// the sidebar draws after a restart is the tree that was true when each session was opened.
|
|
1092
|
+
// Written only when present, so a conversation created without them (the user's own "+") reads
|
|
1093
|
+
// as a top-level one on every code path, including every state.json from before this field.
|
|
1094
|
+
if (configuration.parentConversationId)
|
|
1095
|
+
conversation.parentConversationId = configuration.parentConversationId;
|
|
1096
|
+
if (configuration.origin)
|
|
1097
|
+
conversation.origin = configuration.origin;
|
|
1089
1098
|
}
|
|
1090
1099
|
this.state.conversations.unshift(conversation);
|
|
1091
1100
|
await this.persist();
|
|
1092
1101
|
return structuredClone(conversation);
|
|
1093
1102
|
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Records (or clears) one conversation as collapsed in the sidebar's session tree. The list is a
|
|
1105
|
+
* SET in array clothing - order means nothing, duplicates must not accumulate - and it follows the
|
|
1106
|
+
* assign -> persist -> rollback pattern `setTerminalShell` established: the in-memory value is the
|
|
1107
|
+
* previous value again the moment persistence fails, so a rejected write can never leave the
|
|
1108
|
+
* screen and the disk disagreeing.
|
|
1109
|
+
*/
|
|
1110
|
+
async setConversationCollapsed(conversationId, collapsed) {
|
|
1111
|
+
const previous = this.state.collapsedConversationIds;
|
|
1112
|
+
const next = new Set(previous ?? []);
|
|
1113
|
+
if (collapsed)
|
|
1114
|
+
next.add(conversationId);
|
|
1115
|
+
else
|
|
1116
|
+
next.delete(conversationId);
|
|
1117
|
+
if (next.size === 0)
|
|
1118
|
+
delete this.state.collapsedConversationIds;
|
|
1119
|
+
else
|
|
1120
|
+
this.state.collapsedConversationIds = [...next];
|
|
1121
|
+
try {
|
|
1122
|
+
await this.persist();
|
|
1123
|
+
}
|
|
1124
|
+
catch (error) {
|
|
1125
|
+
if (previous === undefined)
|
|
1126
|
+
delete this.state.collapsedConversationIds;
|
|
1127
|
+
else
|
|
1128
|
+
this.state.collapsedConversationIds = previous;
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1094
1132
|
/**
|
|
1095
1133
|
* Copies a conversation into a NEW one, under an id the caller has already minted.
|
|
1096
1134
|
*
|
|
@@ -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;
|
|
@@ -174,24 +190,22 @@ export class SubagentRunRecorder {
|
|
|
174
190
|
* and is what a card should say about a run whose child never existed. See `SubagentFailure`.
|
|
175
191
|
*/
|
|
176
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;
|
|
177
196
|
const publish = () => {
|
|
178
|
-
record.output = streamed;
|
|
197
|
+
record.output = streamed.length > liveTailCap ? streamed.slice(streamed.length - liveTailCap) : streamed;
|
|
179
198
|
record.toolCalls = toolCalls;
|
|
180
199
|
record.activity = activity;
|
|
181
200
|
// WRITE BEFORE ANNOUNCE, and this is the whole defect. The order of the two statements below is
|
|
182
201
|
// load-bearing; swapping them back reintroduces a card that loses text it already streamed.
|
|
183
202
|
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// `state`
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// Why some cards lived anyway and this one did not: a child that keeps talking recovers by accident,
|
|
191
|
-
// because its NEXT delta publishes a `state` once the write queue has drained. A child that says one
|
|
192
|
-
// thing and goes quiet never publishes again, so nothing restores it - and it is not only that child's
|
|
193
|
-
// card. Every later `state` broadcast from any command, including ones about an unrelated
|
|
194
|
-
// 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.
|
|
195
209
|
//
|
|
196
210
|
// The write is still not awaited, and still must not be: `onTextDelta` is synchronous, and a drain loop
|
|
197
211
|
// that flushed to disk per token would throttle the child. Reordering buys the correctness anyway,
|
|
@@ -222,6 +236,9 @@ export class SubagentRunRecorder {
|
|
|
222
236
|
// the correct trade: never announce state that a reload would contradict.
|
|
223
237
|
const settled = await this.hooks.store.setSubagentRun(id, changes);
|
|
224
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?.();
|
|
225
242
|
return settled;
|
|
226
243
|
};
|
|
227
244
|
/**
|
|
@@ -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.`;
|
package/dist/server/types.d.ts
CHANGED
|
@@ -516,6 +516,21 @@ export type Conversation = {
|
|
|
516
516
|
* deleted agent degrades to "no role" instead of leaving a stale snapshot in state.json.
|
|
517
517
|
*/
|
|
518
518
|
agentId?: string;
|
|
519
|
+
/**
|
|
520
|
+
* The conversation that opened this one, when an orchestrator agent created it. Absent for a
|
|
521
|
+
* conversation the user started. This is the session tree's edge, and it PERSISTS: the sidebar
|
|
522
|
+
* draws the hierarchy from it after a restart, which a server-side in-memory ledger cannot do.
|
|
523
|
+
* Absence also reads correctly on every state.json written before this field existed - no parent
|
|
524
|
+
* recorded simply means "top level", which is what those conversations genuinely are.
|
|
525
|
+
*/
|
|
526
|
+
parentConversationId?: string;
|
|
527
|
+
/**
|
|
528
|
+
* Who opened this conversation. "user" for one the user started, "orchestrator" for one an agent
|
|
529
|
+
* created. Kept beside the parent id because a conversation can be an orchestrator's without
|
|
530
|
+
* having a parent (the top-level orchestrator), and because the context menu's "stop the session
|
|
531
|
+
* AND its queue" needs to know a queue exists without polling the orchestrator's ledger.
|
|
532
|
+
*/
|
|
533
|
+
origin?: "user" | "orchestrator";
|
|
519
534
|
};
|
|
520
535
|
/**
|
|
521
536
|
* One parsed agent definition, as read from an `agents/*.md` file. `prompt` is the file body and is the
|
|
@@ -863,7 +878,13 @@ export type AppState = {
|
|
|
863
878
|
todosByConversation?: Record<string, TodoItem[]>;
|
|
864
879
|
workflows?: WorkflowRun[];
|
|
865
880
|
scheduledTasks?: ScheduledTask[];
|
|
866
|
-
customAgentDirs?: string[];
|
|
881
|
+
customAgentDirs?: string[]; /**
|
|
882
|
+
* Which conversations the reader has collapsed in the sidebar's session tree, by conversation id.
|
|
883
|
+
* Server-side so the tree looks the same after a restart and on another machine - the sidebar is a
|
|
884
|
+
* view of the PRODUCT's state, not of one browser's session. Absent (older state.json) means
|
|
885
|
+
* "nothing collapsed", which is the correct reading of a preference that did not exist yet.
|
|
886
|
+
*/
|
|
887
|
+
collapsedConversationIds?: string[];
|
|
867
888
|
};
|
|
868
889
|
export type TodoItem = {
|
|
869
890
|
id?: string;
|
|
@@ -871,6 +892,29 @@ export type TodoItem = {
|
|
|
871
892
|
status: "pending" | "in_progress" | "completed";
|
|
872
893
|
priority?: "high" | "medium" | "low";
|
|
873
894
|
};
|
|
895
|
+
/** Summary of an orchestrated sibling session, returned by orchestrator queries. */
|
|
896
|
+
export type OrchestratorSessionSummary = {
|
|
897
|
+
conversationId: string;
|
|
898
|
+
workspaceId: string;
|
|
899
|
+
title: string;
|
|
900
|
+
status: RunStatus;
|
|
901
|
+
origin: "user" | "orchestrator";
|
|
902
|
+
parentConversationId?: string;
|
|
903
|
+
task?: string;
|
|
904
|
+
messageCount: number;
|
|
905
|
+
turnCount: number;
|
|
906
|
+
lastAssistantSnippet?: string;
|
|
907
|
+
lastUserSnippet?: string;
|
|
908
|
+
hasPendingQuestion: boolean;
|
|
909
|
+
activeTool?: {
|
|
910
|
+
label: string;
|
|
911
|
+
detail?: string;
|
|
912
|
+
};
|
|
913
|
+
error?: string;
|
|
914
|
+
updatedAt: string;
|
|
915
|
+
createdAt?: number;
|
|
916
|
+
lastProgress?: string;
|
|
917
|
+
};
|
|
874
918
|
/**
|
|
875
919
|
* What Pi reports about ITS OWN queue, forwarded to the browser as it arrives.
|
|
876
920
|
*
|
|
@@ -1135,6 +1179,17 @@ export type ClientCommandBody =
|
|
|
1135
1179
|
} | {
|
|
1136
1180
|
type: "conversation.unarchive";
|
|
1137
1181
|
conversationId: string;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Collapses or expands one conversation's whole subtree in the sidebar's session tree. The state
|
|
1185
|
+
* lives SERVER-SIDE (`AppState.collapsedConversationIds`), not in the browser: the tree must look
|
|
1186
|
+
* the same after a restart and on another machine, and a bare WS client must not be able to leave
|
|
1187
|
+
* the server's truth and the screen's truth disagreeing about a preference the product owns.
|
|
1188
|
+
*/
|
|
1189
|
+
| {
|
|
1190
|
+
type: "conversation.collapse";
|
|
1191
|
+
conversationId: string;
|
|
1192
|
+
collapsed: boolean;
|
|
1138
1193
|
} | {
|
|
1139
1194
|
type: "conversation.rename";
|
|
1140
1195
|
conversationId: string;
|
|
@@ -10,6 +10,21 @@ import type { CatalogService } from "./application/catalog-service.js";
|
|
|
10
10
|
* a network peer that has disappeared can therefore make the buffer grow
|
|
11
11
|
* without throwing. Once this threshold is crossed we drop that connection
|
|
12
12
|
* and let the normal close path clean up its resources.
|
|
13
|
+
*
|
|
14
|
+
* AND YET DROPPING IT IS THE WRONG ANSWER FOR A BUSY SERVER. The threshold was
|
|
15
|
+
* written for a peer that has gone away - a frozen tab, a pulled cable. A server
|
|
16
|
+
* that is merely DELIVERING TOO MUCH hits the same number: a sub-agent that
|
|
17
|
+
* streams text emits one `subagent.event` per delta, each of which asks for a
|
|
18
|
+
* full state broadcast, and a browser that cannot parse frames as fast as they
|
|
19
|
+
* arrive lets the buffer grow past the threshold while it is perfectly alive.
|
|
20
|
+
* The browser then reports code 1006 and reconnects, only to be dropped again -
|
|
21
|
+
* "连接中 / 断连 / 连接中" forever - and the user's work is interrupted rather
|
|
22
|
+
* than slowed.
|
|
23
|
+
*
|
|
24
|
+
* So a frame that does not fit is DROPPED, not the connection: the next frame
|
|
25
|
+
* carries the same state, and a browser that is behind will catch up on the next
|
|
26
|
+
* one instead of being disconnected and losing its place. The connection is
|
|
27
|
+
* only terminated when the socket itself is gone (readyState is not OPEN).
|
|
13
28
|
*/
|
|
14
29
|
export declare const MAX_WS_BUFFERED_BYTES: number;
|
|
15
30
|
/** Maximum size of one inbound WebSocket message. Attachments are uploaded
|
|
@@ -34,7 +49,19 @@ export declare class WsBroadcaster implements EventBroadcaster {
|
|
|
34
49
|
private readonly fileIndexScopes;
|
|
35
50
|
private readonly coalescedSockets;
|
|
36
51
|
private readonly pendingStreams;
|
|
37
|
-
|
|
52
|
+
/**
|
|
53
|
+
* DIAGNOSTIC ONLY - sends every requested state frame immediately instead of merging them.
|
|
54
|
+
*
|
|
55
|
+
* The legacy flood switch (`SUPER_PI_LEGACY_SUBAGENT_FLOOD=1`) restores the pre-fix WIRING: one
|
|
56
|
+
* full state broadcast per sub-agent event. But the 150ms merge window would still fold those
|
|
57
|
+
* broadcasts into one frame, so the "before" measurement would not actually be "before" - it
|
|
58
|
+
* would show a flood that the fixed server also survives, and the A/B comparison would prove
|
|
59
|
+
* nothing. This flag removes the window as well, so the probe can reproduce the real pre-fix
|
|
60
|
+
* wire shape: one frame per request, sent at once.
|
|
61
|
+
*
|
|
62
|
+
* Never set outside a probe or a regression test that is asserting the old behaviour.
|
|
63
|
+
*/
|
|
64
|
+
private readonly sendStateImmediately;
|
|
38
65
|
constructor(getState: (scope?: {
|
|
39
66
|
conversationId?: string;
|
|
40
67
|
}) => AppState);
|
|
@@ -51,7 +78,31 @@ export declare class WsBroadcaster implements EventBroadcaster {
|
|
|
51
78
|
private enqueueStreaming;
|
|
52
79
|
getSockets(): Set<WebSocket>;
|
|
53
80
|
broadcast(event: ServerEvent): void;
|
|
81
|
+
/**
|
|
82
|
+
* BROADCAST STATES IN A TIME WINDOW, NOT IN A MICROTASK.
|
|
83
|
+
*
|
|
84
|
+
* The microtask flag this replaces merged only the calls that arrived within the
|
|
85
|
+
* SAME tick. A sub-agent that streams text emits one `subagent.event` per delta,
|
|
86
|
+
* each asking for a full state broadcast, and those deltas arrive across many
|
|
87
|
+
* ticks - so a single sub-agent run could ask for HUNDREDS of full state
|
|
88
|
+
* broadcasts. On a machine whose state is large, that is a flood the browser
|
|
89
|
+
* cannot drain: its parse queue falls behind, the socket buffer grows, and the
|
|
90
|
+
* connection is dropped (the browser reports 1006).
|
|
91
|
+
*
|
|
92
|
+
* A window turns a burst into a handful of frames. The state a browser receives
|
|
93
|
+
* is the LATEST state - the intermediate ones carried nothing the last one did
|
|
94
|
+
* not - so nothing is lost by sending fewer of them. The only thing that
|
|
95
|
+
* changes is how promptly the browser sees a change, and 150ms is below the
|
|
96
|
+
* threshold at which a person notices.
|
|
97
|
+
*/
|
|
98
|
+
private stateFlushScheduled;
|
|
54
99
|
broadcastState(): void;
|
|
100
|
+
private flushState;
|
|
101
|
+
/**
|
|
102
|
+
* Releases the pending state window, so a shutting-down server does not hold the event loop
|
|
103
|
+
* open on a timer whose flush would send to sockets that are already closed.
|
|
104
|
+
*/
|
|
105
|
+
dispose(): void;
|
|
55
106
|
getStateSnapshot(scope?: {
|
|
56
107
|
conversationId?: string;
|
|
57
108
|
}): AppState;
|