@evandu/super-pi 0.1.52 → 0.1.54
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/dist/client/assets/index-Cb_uyk8r.css +1 -0
- package/dist/client/assets/index-GghJcmHm.js +53 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.d.ts +3 -1
- package/dist/server/application/run-service.js +45 -0
- package/dist/server/command-handler.js +23 -0
- package/dist/server/runtime.d.ts +5 -0
- package/dist/server/runtime.js +14 -8
- package/dist/server/store.d.ts +4 -0
- package/dist/server/store.js +27 -1
- package/dist/server/types.d.ts +16 -0
- package/package.json +1 -1
- package/dist/client/assets/index-DJD8mXiK.css +0 -1
- package/dist/client/assets/index-PIHGU_8U.js +0 -53
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
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-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-GghJcmHm.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cb_uyk8r.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Store } from "../store.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentRuntime } from "../runtime.js";
|
|
3
3
|
import type { SubagentRunRecorder } from "../subagent-runs.js";
|
|
4
4
|
import type { Conversation, Message, MessageImage, PiQueueSnapshot, QuestionAnswer, Workspace } from "../types.js";
|
|
5
5
|
import type { EventBroadcaster } from "../server-context.js";
|
|
@@ -155,6 +155,8 @@ export declare class RunService {
|
|
|
155
155
|
* Steers a queued message into the in-flight turn.
|
|
156
156
|
*/
|
|
157
157
|
steer(conversationId: string, queueId: string): Promise<void>;
|
|
158
|
+
/** Starts a chosen saved message only when the conversation is idle. */
|
|
159
|
+
sendQueued(conversationId: string, queueId: string): Promise<void>;
|
|
158
160
|
private steerUnsafe;
|
|
159
161
|
/**
|
|
160
162
|
* Cancels a queued message.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SteerUnavailableError } from "../runtime.js";
|
|
1
2
|
import { checkSubagentDepth, parentDeniedTools, subagentRoster } from "../task-tool.js";
|
|
2
3
|
import { newSubagentRunId } from "../subagents.js";
|
|
3
4
|
import { readAgents } from "../agents.js";
|
|
@@ -553,10 +554,48 @@ export class RunService {
|
|
|
553
554
|
async steer(conversationId, queueId) {
|
|
554
555
|
return this.serializeConversation(conversationId, () => this.steerUnsafe(conversationId, queueId));
|
|
555
556
|
}
|
|
557
|
+
/** Starts a chosen saved message only when the conversation is idle. */
|
|
558
|
+
async sendQueued(conversationId, queueId) {
|
|
559
|
+
const waitForStop = async () => {
|
|
560
|
+
// A stop broadcasts `stopped` before the previous run has finished its finally block. Allow that
|
|
561
|
+
// short cleanup window; a click on the newly visible button must not race the old run slot.
|
|
562
|
+
const deadline = Date.now() + 15_000;
|
|
563
|
+
while (this.activeRuns.has(conversationId) && this.store.snapshot().conversations.find((item) => item.id === conversationId)?.status === "stopped" && Date.now() < deadline) {
|
|
564
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
await waitForStop();
|
|
568
|
+
return this.serializeConversation(conversationId, async () => {
|
|
569
|
+
if (this.activeRuns.has(conversationId))
|
|
570
|
+
throw new Error("当前轮仍在运行;停止或等待它结束后再发送这条消息");
|
|
571
|
+
const queued = await this.store.takeQueuedMessage(conversationId, queueId);
|
|
572
|
+
const userMessageCount = this.store.snapshot().conversations.find((item) => item.id === conversationId)?.messages.filter((item) => item.role === "user").length ?? 0;
|
|
573
|
+
// Claim the run immediately, then release the command lane. Waiting for the whole model turn here
|
|
574
|
+
// would block every later queue action from this socket until the answer completes.
|
|
575
|
+
void this.enqueueAndRun(conversationId, queued.content, queued.images).catch(async (error) => {
|
|
576
|
+
// Once the turn has a user message it is already in the transcript. Requeue only a preflight
|
|
577
|
+
// failure, or the same prompt would be shown as both sent and waiting to be sent again.
|
|
578
|
+
const sent = (this.store.snapshot().conversations.find((item) => item.id === conversationId)?.messages.filter((item) => item.role === "user").length ?? 0) > userMessageCount;
|
|
579
|
+
if (!sent)
|
|
580
|
+
await this.store.enqueueMessage(conversationId, queued.content, queued.images);
|
|
581
|
+
this.broadcaster.broadcastState();
|
|
582
|
+
console.warn(`Super Pi: explicit queued send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
583
|
+
});
|
|
584
|
+
this.broadcaster.broadcastState();
|
|
585
|
+
});
|
|
586
|
+
}
|
|
556
587
|
async steerUnsafe(conversationId, queueId) {
|
|
557
588
|
if (!this.runtime.steer) {
|
|
558
589
|
throw new Error("当前运行时没有插队能力,无法把排队消息插到正在运行的这轮里");
|
|
559
590
|
}
|
|
591
|
+
// A running conversation may be in the tiny hand-off window after Pi ended but before this service
|
|
592
|
+
// opens the next queued turn. Priority delivery has the same user meaning in both cases, so reserve
|
|
593
|
+
// the message for the next turn instead of turning a successful click into an implementation error.
|
|
594
|
+
if (this.runtime.canSteer && !this.runtime.canSteer(conversationId)) {
|
|
595
|
+
await this.store.prioritizeQueuedMessage(conversationId, queueId);
|
|
596
|
+
this.broadcaster.broadcastState();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
560
599
|
const claimed = await this.store.claimQueuedMessage(conversationId, queueId);
|
|
561
600
|
this.broadcaster.broadcastState();
|
|
562
601
|
try {
|
|
@@ -566,6 +605,12 @@ export class RunService {
|
|
|
566
605
|
}
|
|
567
606
|
}
|
|
568
607
|
catch (error) {
|
|
608
|
+
if (error instanceof SteerUnavailableError) {
|
|
609
|
+
await this.store.releaseQueuedMessage(claimed.id);
|
|
610
|
+
await this.store.prioritizeQueuedMessage(conversationId, claimed.id);
|
|
611
|
+
this.broadcaster.broadcastState();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
569
614
|
await this.store.releaseQueuedMessage(claimed.id);
|
|
570
615
|
throw error;
|
|
571
616
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
/**
|
|
2
3
|
* Routes client commands to appropriate application services and converts results or errors.
|
|
3
4
|
*/
|
|
@@ -162,6 +163,10 @@ export class CommandHandler {
|
|
|
162
163
|
await this.runService.steer(command.conversationId, command.queueId);
|
|
163
164
|
return;
|
|
164
165
|
}
|
|
166
|
+
if (command.type === "message.sendQueued") {
|
|
167
|
+
await this.runService.sendQueued(command.conversationId, command.queueId);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
165
170
|
if (command.type === "message.cancel") {
|
|
166
171
|
await this.runService.cancelQueued(command.conversationId, command.queueId);
|
|
167
172
|
return;
|
|
@@ -232,6 +237,24 @@ export class CommandHandler {
|
|
|
232
237
|
await this.catalogService.rescan();
|
|
233
238
|
return;
|
|
234
239
|
}
|
|
240
|
+
if (command.type === "super_pi.start") {
|
|
241
|
+
const port = Number(process.env.SUPER_PI_PORT ?? 3080);
|
|
242
|
+
const url = `http://127.0.0.1:${port}`;
|
|
243
|
+
const platform = process.platform;
|
|
244
|
+
if (platform === "darwin")
|
|
245
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
246
|
+
else if (platform === "win32")
|
|
247
|
+
spawn("cmd.exe", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
248
|
+
else
|
|
249
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (command.type === "super_pi.update") {
|
|
253
|
+
spawn("pi", ["update", "@evandu/super-pi"], { stdio: "inherit", shell: true }).on("error", () => {
|
|
254
|
+
console.error("Super Pi: `pi update @evandu/super-pi` failed — is the `pi` command in your PATH?");
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
235
258
|
throw new Error(`未知的命令类型:${JSON.stringify(command.type ?? null)}`);
|
|
236
259
|
}
|
|
237
260
|
}
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -112,6 +112,9 @@ export interface AnswerInput {
|
|
|
112
112
|
* in the model's context and one that is not, and the UI must not claim either without knowing which.
|
|
113
113
|
*/
|
|
114
114
|
export type SteerDisposition = "handled" | "queued";
|
|
115
|
+
/** A steer that provably never reached Pi; the caller may safely use the next turn instead. */
|
|
116
|
+
export declare class SteerUnavailableError extends Error {
|
|
117
|
+
}
|
|
115
118
|
/**
|
|
116
119
|
* What a revert has to be told, and what it is told afterwards. Declared here rather than in `types.ts`
|
|
117
120
|
* because neither travels to the browser: this is the runtime's own account of a file it changed.
|
|
@@ -185,6 +188,7 @@ export interface AgentRuntime {
|
|
|
185
188
|
* Absent on a runtime with no Pi behind it (the welcome runtime), which has nothing to steer.
|
|
186
189
|
*/
|
|
187
190
|
steer?(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
|
|
191
|
+
canSteer?(conversationId: string): boolean;
|
|
188
192
|
/**
|
|
189
193
|
* ONE-SHOT TITLE GENERATION. Spawns its own short-lived Pi child rather than borrowing a running one.
|
|
190
194
|
*
|
|
@@ -630,6 +634,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
630
634
|
* it was started.
|
|
631
635
|
*/
|
|
632
636
|
private readonly pi;
|
|
637
|
+
canSteer(conversationId: string): boolean;
|
|
633
638
|
constructor(sessionDir: string, command?: string,
|
|
634
639
|
/**
|
|
635
640
|
* Where agent definitions are read from and where their prompt bodies are written. A THIRD positional
|
package/dist/server/runtime.js
CHANGED
|
@@ -10,6 +10,9 @@ import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_END
|
|
|
10
10
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
11
11
|
import { loadConversationImages } from "./upload.js";
|
|
12
12
|
import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
|
|
13
|
+
/** A steer that provably never reached Pi; the caller may safely use the next turn instead. */
|
|
14
|
+
export class SteerUnavailableError extends Error {
|
|
15
|
+
}
|
|
13
16
|
/** Deterministic offline runtime used by tests and local UI development. */
|
|
14
17
|
export class WelcomeRuntime {
|
|
15
18
|
async *answer({ prompt, signal }) {
|
|
@@ -602,6 +605,10 @@ export class PiRpcRuntime {
|
|
|
602
605
|
* it was started.
|
|
603
606
|
*/
|
|
604
607
|
pi;
|
|
608
|
+
canSteer(conversationId) {
|
|
609
|
+
const run = this.runs.get(conversationId);
|
|
610
|
+
return Boolean(run && run.child.exitCode === null && !run.child.killed && !run.child.stdin.destroyed);
|
|
611
|
+
}
|
|
605
612
|
constructor(sessionDir, command,
|
|
606
613
|
/**
|
|
607
614
|
* Where agent definitions are read from and where their prompt bodies are written. A THIRD positional
|
|
@@ -1234,14 +1241,15 @@ export class PiRpcRuntime {
|
|
|
1234
1241
|
catch (error) {
|
|
1235
1242
|
if (child.exitCode === null && child.pid !== undefined)
|
|
1236
1243
|
await killTree(child.pid);
|
|
1237
|
-
|
|
1244
|
+
// taskkill has already terminated the process tree; do not add a five-second UI stall waiting for
|
|
1245
|
+
// Node's exit event bookkeeping on Windows.
|
|
1238
1246
|
throw error instanceof Error ? error : new Error(`无法向 Pi 询问会话文件:${String(error)}`);
|
|
1239
1247
|
}
|
|
1240
1248
|
// The lookup Pi is holding open is finished; it is killed BEFORE the file is touched, for the same
|
|
1241
1249
|
// reason `answer()` kills before returning: a process still tearing down can still be appending.
|
|
1242
1250
|
if (child.exitCode === null && child.pid !== undefined)
|
|
1243
1251
|
await killTree(child.pid);
|
|
1244
|
-
|
|
1252
|
+
// The process tree is dead after killTree; copying can proceed without waiting for a delayed exit event.
|
|
1245
1253
|
if (!sessionFile)
|
|
1246
1254
|
throw new Error("Pi 没有报告这个会话的记录文件,已放弃,没有改动任何文件。");
|
|
1247
1255
|
const exists = await stat(sessionFile).catch(() => undefined);
|
|
@@ -1325,12 +1333,12 @@ export class PiRpcRuntime {
|
|
|
1325
1333
|
catch (error) {
|
|
1326
1334
|
if (child.exitCode === null && child.pid !== undefined)
|
|
1327
1335
|
await killTree(child.pid);
|
|
1328
|
-
|
|
1336
|
+
// killTree waits for taskkill; waiting again for Node's exit event can miss the event and add 5s.
|
|
1329
1337
|
throw error instanceof Error ? error : new Error(`无法向 Pi 询问会话文件:${String(error)}`);
|
|
1330
1338
|
}
|
|
1331
1339
|
if (child.exitCode === null && child.pid !== undefined)
|
|
1332
1340
|
await killTree(child.pid);
|
|
1333
|
-
|
|
1341
|
+
// killTree waits for taskkill; do not wait for a possibly already-delivered exit event.
|
|
1334
1342
|
if (!sessionFile)
|
|
1335
1343
|
throw new Error("Pi 没有报告这个会话的记录文件,已放弃,没有复制任何文件。");
|
|
1336
1344
|
const exists = await stat(sessionFile).catch(() => undefined);
|
|
@@ -1516,10 +1524,8 @@ export class PiRpcRuntime {
|
|
|
1516
1524
|
}
|
|
1517
1525
|
async steer(conversationId, message, images) {
|
|
1518
1526
|
const run = this.runs.get(conversationId);
|
|
1519
|
-
if (!run)
|
|
1520
|
-
throw new
|
|
1521
|
-
if (run.child.exitCode !== null || run.child.killed || run.child.stdin.destroyed)
|
|
1522
|
-
throw new Error(`会话 ${conversationId} 的 Pi 进程已经结束,无法插队;这条消息仍在队列里。`);
|
|
1527
|
+
if (!run || !this.canSteer(conversationId))
|
|
1528
|
+
throw new SteerUnavailableError(`会话 ${conversationId} 的 Pi 进程已结束`);
|
|
1523
1529
|
const id = `steer_${++this.steerSeq}`;
|
|
1524
1530
|
const imagePayloads = this.options.uploadsDir && images?.length
|
|
1525
1531
|
? await loadConversationImages(this.options.uploadsDir, conversationId, images)
|
package/dist/server/store.d.ts
CHANGED
|
@@ -360,6 +360,8 @@ export declare class Store {
|
|
|
360
360
|
* same message would put the text into Pi twice.
|
|
361
361
|
*/
|
|
362
362
|
claimQueuedMessage(conversationId: string, queueId: string): Promise<QueuedMessage>;
|
|
363
|
+
/** Keep a priority request at the front even if the current Pi process has already exited. */
|
|
364
|
+
prioritizeQueuedMessage(conversationId: string, queueId: string): Promise<QueuedMessage>;
|
|
363
365
|
/**
|
|
364
366
|
* Puts a `sending` entry back into the queue.
|
|
365
367
|
*
|
|
@@ -395,6 +397,8 @@ export declare class Store {
|
|
|
395
397
|
* `open` does not put it back, because it may well have been delivered.
|
|
396
398
|
*/
|
|
397
399
|
takeNextQueuedMessage(conversationId: string): Promise<QueuedMessage | undefined>;
|
|
400
|
+
/** Removes one still-cancellable row by id so an explicit user action can start it as a new turn. */
|
|
401
|
+
takeQueuedMessage(conversationId: string, queueId: string): Promise<QueuedMessage>;
|
|
398
402
|
/**
|
|
399
403
|
* Returns every `sending` message of this conversation to the queue, for the end of a turn.
|
|
400
404
|
*
|
package/dist/server/store.js
CHANGED
|
@@ -1151,6 +1151,19 @@ export class Store {
|
|
|
1151
1151
|
if (message.state === "sending")
|
|
1152
1152
|
throw new Error(`排队消息已经交给 Agent 了:${queueId}`);
|
|
1153
1153
|
message.state = "sending";
|
|
1154
|
+
message.priority = true;
|
|
1155
|
+
await this.persistCollections("queuedMessages");
|
|
1156
|
+
return structuredClone(message);
|
|
1157
|
+
}
|
|
1158
|
+
/** Keep a priority request at the front even if the current Pi process has already exited. */
|
|
1159
|
+
async prioritizeQueuedMessage(conversationId, queueId) {
|
|
1160
|
+
const index = this.state.queuedMessages.findIndex((item) => item.id === queueId && item.conversationId === conversationId);
|
|
1161
|
+
if (index < 0)
|
|
1162
|
+
throw new Error(`排队消息不存在或已发出:${queueId}`);
|
|
1163
|
+
const message = this.state.queuedMessages[index];
|
|
1164
|
+
if (message.state === "sending")
|
|
1165
|
+
return structuredClone(message);
|
|
1166
|
+
message.priority = true;
|
|
1154
1167
|
await this.persistCollections("queuedMessages");
|
|
1155
1168
|
return structuredClone(message);
|
|
1156
1169
|
}
|
|
@@ -1207,13 +1220,26 @@ export class Store {
|
|
|
1207
1220
|
* `open` does not put it back, because it may well have been delivered.
|
|
1208
1221
|
*/
|
|
1209
1222
|
async takeNextQueuedMessage(conversationId) {
|
|
1210
|
-
|
|
1223
|
+
let index = this.state.queuedMessages.findIndex((item) => item.conversationId === conversationId && item.state === "queued" && item.priority);
|
|
1224
|
+
if (index < 0)
|
|
1225
|
+
index = this.state.queuedMessages.findIndex((item) => item.conversationId === conversationId && item.state === "queued");
|
|
1211
1226
|
if (index < 0)
|
|
1212
1227
|
return undefined;
|
|
1213
1228
|
const [taken] = this.state.queuedMessages.splice(index, 1);
|
|
1214
1229
|
await this.persistCollections("queuedMessages");
|
|
1215
1230
|
return structuredClone(taken);
|
|
1216
1231
|
}
|
|
1232
|
+
/** Removes one still-cancellable row by id so an explicit user action can start it as a new turn. */
|
|
1233
|
+
async takeQueuedMessage(conversationId, queueId) {
|
|
1234
|
+
const index = this.state.queuedMessages.findIndex((item) => item.id === queueId && item.conversationId === conversationId);
|
|
1235
|
+
if (index < 0)
|
|
1236
|
+
throw new Error("这条排队消息已经发出或不存在");
|
|
1237
|
+
if (this.state.queuedMessages[index].state !== "queued")
|
|
1238
|
+
throw new Error("当前轮还没有结束;请先停止或等待它结束,再发送这条消息");
|
|
1239
|
+
const [taken] = this.state.queuedMessages.splice(index, 1);
|
|
1240
|
+
await this.persistCollections("queuedMessages");
|
|
1241
|
+
return structuredClone(taken);
|
|
1242
|
+
}
|
|
1217
1243
|
/**
|
|
1218
1244
|
* Returns every `sending` message of this conversation to the queue, for the end of a turn.
|
|
1219
1245
|
*
|
package/dist/server/types.d.ts
CHANGED
|
@@ -670,6 +670,8 @@ export type QueuedMessage = {
|
|
|
670
670
|
conversationId: string;
|
|
671
671
|
content: string;
|
|
672
672
|
state: "queued" | "sending";
|
|
673
|
+
/** Chosen for priority delivery. Survives a Pi turn boundary and a server restart. */
|
|
674
|
+
priority?: boolean;
|
|
673
675
|
createdAt: string;
|
|
674
676
|
images?: MessageImage[];
|
|
675
677
|
};
|
|
@@ -961,6 +963,12 @@ export type ClientCommandBody =
|
|
|
961
963
|
type: "message.steer";
|
|
962
964
|
conversationId: string;
|
|
963
965
|
queueId: string;
|
|
966
|
+
}
|
|
967
|
+
/** Starts this saved queued message as the next turn, only after the current turn has ended. */
|
|
968
|
+
| {
|
|
969
|
+
type: "message.sendQueued";
|
|
970
|
+
conversationId: string;
|
|
971
|
+
queueId: string;
|
|
964
972
|
} | {
|
|
965
973
|
type: "run.stop";
|
|
966
974
|
conversationId: string;
|
|
@@ -1008,6 +1016,14 @@ export type ClientCommandBody =
|
|
|
1008
1016
|
dirs: string[];
|
|
1009
1017
|
} | {
|
|
1010
1018
|
type: "agents.rescan";
|
|
1019
|
+
}
|
|
1020
|
+
/** Opens the browser at the Super Pi web UI, starting the server if it is not running. */
|
|
1021
|
+
| {
|
|
1022
|
+
type: "super_pi.start";
|
|
1023
|
+
}
|
|
1024
|
+
/** Runs `pi self-update` to update Super Pi. */
|
|
1025
|
+
| {
|
|
1026
|
+
type: "super_pi.update";
|
|
1011
1027
|
} | ({
|
|
1012
1028
|
type: "extension.ui.answer";
|
|
1013
1029
|
conversationId: string;
|