@evandu/super-pi 0.1.89 → 0.1.91
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/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +5 -5
- package/dist/client/assets/index-BcHIMxNF.css +1 -0
- package/dist/client/assets/index-DYjBTgl4.js +73 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/conversation-service.js +29 -3
- package/dist/server/application/run-service.d.ts +27 -0
- package/dist/server/application/run-service.js +157 -28
- package/dist/server/command-handler.js +4 -0
- package/dist/server/store.d.ts +24 -22
- package/dist/server/store.js +74 -36
- package/dist/server/types.d.ts +19 -1
- package/package.json +1 -1
- package/dist/client/assets/index-Bpy8UeU1.js +0 -73
- package/dist/client/assets/index-CC-Q5gNQ.css +0 -1
package/dist/server/store.js
CHANGED
|
@@ -135,7 +135,11 @@ function reconcileConversations(state) {
|
|
|
135
135
|
state.conversations = conversations;
|
|
136
136
|
const orphaned = [];
|
|
137
137
|
for (const conversation of conversations) {
|
|
138
|
-
|
|
138
|
+
// `waiting` joins the orphan set for the same reason `running` does: the turn was opened (a prompt
|
|
139
|
+
// was sent) and the server died before any settled state was recorded. A conversation that was in
|
|
140
|
+
// either of those two states when the process went away has a half-open turn whose outcome nobody
|
|
141
|
+
// can determine from the file alone - exactly the situation this sweep exists to remove.
|
|
142
|
+
if (!conversation || (conversation.status !== "running" && conversation.status !== "waiting"))
|
|
139
143
|
continue;
|
|
140
144
|
conversation.status = "failed";
|
|
141
145
|
conversation.error = ORPHANED_CONVERSATION_ERROR;
|
|
@@ -358,9 +362,16 @@ function reconcileWaitingMessages(state) {
|
|
|
358
362
|
const recovered = [];
|
|
359
363
|
for (const conversation of state.conversations) {
|
|
360
364
|
for (const message of conversation.messages) {
|
|
361
|
-
// A `delivery` on
|
|
362
|
-
//
|
|
363
|
-
|
|
365
|
+
// A `delivery` on an ASSISTANT message is still a broken file: rendering a waiting state on an
|
|
366
|
+
// assistant bubble would be a claim about nothing, and nothing in the code path writes one. Tool
|
|
367
|
+
// messages with `delivery` are queue items now (exec / sub-agent background completions) - if they
|
|
368
|
+
// died in `sending`, recover them to `waiting` just like user messages, so a crashed server does
|
|
369
|
+
// not strand a 后台回传 whose delivery it had already accepted.
|
|
370
|
+
if (message.role === "assistant") {
|
|
371
|
+
delete message.delivery;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (message.role !== "user" && message.role !== "tool") {
|
|
364
375
|
delete message.delivery;
|
|
365
376
|
continue;
|
|
366
377
|
}
|
|
@@ -683,10 +694,18 @@ export class Store {
|
|
|
683
694
|
if (workspace.defaultAgentId)
|
|
684
695
|
conversation.agentId = workspace.defaultAgentId;
|
|
685
696
|
if (configuration) {
|
|
686
|
-
if (configuration.model)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
697
|
+
if (configuration.model !== undefined) {
|
|
698
|
+
if (configuration.model)
|
|
699
|
+
conversation.model = { ...configuration.model };
|
|
700
|
+
else
|
|
701
|
+
delete conversation.model;
|
|
702
|
+
}
|
|
703
|
+
if (configuration.thinkingLevel !== undefined) {
|
|
704
|
+
if (configuration.thinkingLevel)
|
|
705
|
+
conversation.thinkingLevel = configuration.thinkingLevel;
|
|
706
|
+
else
|
|
707
|
+
delete conversation.thinkingLevel;
|
|
708
|
+
}
|
|
690
709
|
if (configuration.agentId)
|
|
691
710
|
conversation.agentId = configuration.agentId;
|
|
692
711
|
}
|
|
@@ -869,8 +888,13 @@ export class Store {
|
|
|
869
888
|
: (firstUser?.images?.length ? "【图片】" : DEFAULT_CONVERSATION_TITLE);
|
|
870
889
|
if (!conversation.titleLocked)
|
|
871
890
|
conversation.title = autoTitle || DEFAULT_CONVERSATION_TITLE;
|
|
891
|
+
// A TURN STARTS IN `waiting`, NOT `running`. The first delta from the model is what flips the
|
|
892
|
+
// conversation to `running` (see `RunService.streamTurn`); opening a turn only proves a prompt was
|
|
893
|
+
// sent, and a prompt that was sent but never answered looks identical to one that is mid-stream
|
|
894
|
+
// until something tells them apart. Holding the gap open is what makes a stuck LLM endpoint
|
|
895
|
+
// visible to a person - the `waiting` mark on the sidebar row is the only honest signal.
|
|
872
896
|
conversation.updatedAt = message.createdAt;
|
|
873
|
-
conversation.status = "
|
|
897
|
+
conversation.status = "waiting";
|
|
874
898
|
// The reason this conversation was failed - startup reconciliation's note that a previous run was cut
|
|
875
899
|
// short - describes THAT run, and this line is the moment it stops being the current one. Deleting the
|
|
876
900
|
// key rather than overwriting it keeps the file byte-identical to a conversation that was never
|
|
@@ -896,22 +920,26 @@ export class Store {
|
|
|
896
920
|
return id("message");
|
|
897
921
|
}
|
|
898
922
|
/**
|
|
899
|
-
* Appends one background-result message to a conversation
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
* NO `turnContext` claim, NO `status: "running"`. The message sits in the transcript and a subsequent
|
|
908
|
-
* caller can start a turn normally if it wants the model to react to the result.
|
|
923
|
+
* Appends one background-result message to a conversation. The render column is the same as before
|
|
924
|
+
* (a `role: "tool"` row, never impersonating a user bubble) - what this method now ALSO supports is
|
|
925
|
+
* putting the message straight into the dock by passing `delivery: "waiting"`, which is what exec and
|
|
926
|
+
* sub-agent background completions do once a 后台回传 is treated as a queue item with steer priority
|
|
927
|
+
* rather than a one-way notice the model never gets to read.
|
|
928
|
+
*
|
|
929
|
+
* NO `turnContext` claim, NO `status: "running"`. The message sits in the transcript; with a delivery
|
|
930
|
+
* state it also enters `waitingMessages` and `runConversationTurns` will pick it up next.
|
|
909
931
|
*/
|
|
910
|
-
async addToolMessage(conversationId, content) {
|
|
932
|
+
async addToolMessage(conversationId, content, options) {
|
|
911
933
|
const conversation = this.requireConversation(conversationId);
|
|
912
934
|
if (conversation.archivedAt)
|
|
913
935
|
throw new Error("无法向已归档的会话追加后台回传,请先恢复该会话");
|
|
914
|
-
const message = {
|
|
936
|
+
const message = {
|
|
937
|
+
id: id("message"),
|
|
938
|
+
role: "tool",
|
|
939
|
+
content,
|
|
940
|
+
createdAt: now(),
|
|
941
|
+
...(options?.delivery ? { delivery: options.delivery } : {}),
|
|
942
|
+
};
|
|
915
943
|
conversation.messages.push(message);
|
|
916
944
|
conversation.updatedAt = message.createdAt;
|
|
917
945
|
await this.persist();
|
|
@@ -1358,14 +1386,18 @@ export class Store {
|
|
|
1358
1386
|
listWaitingMessages(conversationId) {
|
|
1359
1387
|
return structuredClone(this.waitingMessages(this.requireConversation(conversationId)));
|
|
1360
1388
|
}
|
|
1361
|
-
/** The same list by reference, for callers that only read a field and must not pay for a clone.
|
|
1389
|
+
/** The same list by reference, for callers that only read a field and must not pay for a clone.
|
|
1390
|
+
*
|
|
1391
|
+
* Includes `role: "tool"` messages now that a 后台回传 is a real queue item: same dock, same ordering,
|
|
1392
|
+
* same delivery state machine, same steer/claim semantics - the only thing different is the render
|
|
1393
|
+
* (kept as a 后台回传 block) and what the agent is told when its turn comes (see `composePrompt`). */
|
|
1362
1394
|
waitingMessages(conversation) {
|
|
1363
|
-
return conversation.messages.filter((message) => message.role === "user" && message.delivery);
|
|
1395
|
+
return conversation.messages.filter((message) => (message.role === "user" || message.role === "tool") && message.delivery);
|
|
1364
1396
|
}
|
|
1365
1397
|
requireWaitingMessage(conversationId, messageId) {
|
|
1366
1398
|
const conversation = this.requireConversation(conversationId);
|
|
1367
1399
|
const message = conversation.messages.find((item) => item.id === messageId);
|
|
1368
|
-
if (!message || message.role !== "user"
|
|
1400
|
+
if (!message || !message.delivery || (message.role !== "user" && message.role !== "tool"))
|
|
1369
1401
|
throw new Error(`等待中的消息不存在或已被处理:${messageId}`);
|
|
1370
1402
|
return { conversation, message };
|
|
1371
1403
|
}
|
|
@@ -1410,7 +1442,7 @@ export class Store {
|
|
|
1410
1442
|
async markMessageWaiting(conversationId, messageId) {
|
|
1411
1443
|
const conversation = this.requireConversation(conversationId);
|
|
1412
1444
|
const message = conversation.messages.find((item) => item.id === messageId);
|
|
1413
|
-
if (!message || message.role !== "user")
|
|
1445
|
+
if (!message || (message.role !== "user" && message.role !== "tool"))
|
|
1414
1446
|
return undefined;
|
|
1415
1447
|
if (message.delivery === "sending")
|
|
1416
1448
|
throw new Error("这条消息已经交给 Agent 了");
|
|
@@ -1479,7 +1511,7 @@ export class Store {
|
|
|
1479
1511
|
if (index < 0)
|
|
1480
1512
|
return undefined;
|
|
1481
1513
|
const message = conversation.messages[index];
|
|
1482
|
-
if (message.role !== "user" || !message.delivery)
|
|
1514
|
+
if ((message.role !== "user" && message.role !== "tool") || !message.delivery)
|
|
1483
1515
|
return undefined;
|
|
1484
1516
|
if (message.delivery === "sending")
|
|
1485
1517
|
throw new Error("这条消息已经交给 Agent 了,取消不掉(Pi 没有撤回单条队列消息的能力)");
|
|
@@ -1488,18 +1520,17 @@ export class Store {
|
|
|
1488
1520
|
return structuredClone(removed);
|
|
1489
1521
|
}
|
|
1490
1522
|
/**
|
|
1491
|
-
* Rewrites the words of one waiting message.
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
* changing it is changing the whole truth of the message - there is no second copy anywhere that could
|
|
1495
|
-
* fall out of step. `sending` means Pi already holds it, and a message the model is about to answer is
|
|
1496
|
-
* not one the user gets to rewrite after the fact.
|
|
1523
|
+
* Rewrites the words of one waiting message. User sends only - a 后台回传 carries a `<task>` envelope
|
|
1524
|
+
* the model parses for state, summary and body, and editing it would change a fact the agent has
|
|
1525
|
+
* already started reasoning from. The client dock shows no 「改字」 button for `role: "tool"` rows.
|
|
1497
1526
|
*/
|
|
1498
1527
|
async editWaitingMessage(conversationId, messageId, content) {
|
|
1499
1528
|
const trimmed = content.trim();
|
|
1500
1529
|
if (!trimmed)
|
|
1501
1530
|
throw new Error("消息不能为空");
|
|
1502
1531
|
const { conversation, message } = this.requireWaitingMessage(conversationId, messageId);
|
|
1532
|
+
if (message.role === "tool")
|
|
1533
|
+
throw new Error("后台任务结果不能改字");
|
|
1503
1534
|
if (message.delivery !== "waiting")
|
|
1504
1535
|
throw new Error("这条消息已经交给 Agent 了,改不了(Pi 已经收到了原文)");
|
|
1505
1536
|
message.content = trimmed;
|
|
@@ -1571,7 +1602,11 @@ export class Store {
|
|
|
1571
1602
|
*/
|
|
1572
1603
|
async deliverWaitingMessage(conversationId, agentIdentity) {
|
|
1573
1604
|
const conversation = this.requireConversation(conversationId);
|
|
1574
|
-
|
|
1605
|
+
// Looks for the single message Pi is currently answering. A `role: "tool"` 后台回传 that has been
|
|
1606
|
+
// steered or taken is on the same delivery state machine as a user message, so it is the answer
|
|
1607
|
+
// target too; the assistant placeholder still lands directly below it and the role/identity is read
|
|
1608
|
+
// from the conversation (not the message) when the turn opens.
|
|
1609
|
+
const index = conversation.messages.findIndex((item) => (item.role === "user" || item.role === "tool") && item.delivery === "sending");
|
|
1575
1610
|
if (index < 0)
|
|
1576
1611
|
return undefined;
|
|
1577
1612
|
const user = conversation.messages[index];
|
|
@@ -1592,9 +1627,10 @@ export class Store {
|
|
|
1592
1627
|
* question that is still waiting - a transcript lying about its own order. Splicing at
|
|
1593
1628
|
* `indexOf(user) + 1` keeps every waiting message below the reply that belongs to the one above it.
|
|
1594
1629
|
*
|
|
1595
|
-
* The side effects - title, `updatedAt`, `status: "
|
|
1630
|
+
* The side effects - title, `updatedAt`, `status: "waiting"`, clearing a stale failure - all describe a
|
|
1596
1631
|
* TURN rather than a message, which is why `addUserMessage` skips them for a message that is only
|
|
1597
|
-
* waiting. They belong here, at the moment the turn genuinely begins
|
|
1632
|
+
* waiting. They belong here, at the moment the turn genuinely begins - the model still has not
|
|
1633
|
+
* returned a token, and that is the shape the row carries until `streamTurn` sees the first delta.
|
|
1598
1634
|
*/
|
|
1599
1635
|
async openTurnOnMessage(conversationId, messageId, agentIdentity) {
|
|
1600
1636
|
const conversation = this.requireConversation(conversationId);
|
|
@@ -1611,8 +1647,10 @@ export class Store {
|
|
|
1611
1647
|
: (firstUser?.images?.length ? "【图片】" : DEFAULT_CONVERSATION_TITLE);
|
|
1612
1648
|
if (!conversation.titleLocked)
|
|
1613
1649
|
conversation.title = autoTitle || DEFAULT_CONVERSATION_TITLE;
|
|
1650
|
+
// Same `waiting` reasoning as `addUserMessage` above - the prompt was just handed off and no
|
|
1651
|
+
// token has come back yet, which is a different observable state from "we have data flowing".
|
|
1614
1652
|
conversation.updatedAt = user.createdAt;
|
|
1615
|
-
conversation.status = "
|
|
1653
|
+
conversation.status = "waiting";
|
|
1616
1654
|
delete conversation.error;
|
|
1617
1655
|
await this.persistMessages(conversation);
|
|
1618
1656
|
return { user: structuredClone(user), assistant: structuredClone(assistant) };
|
package/dist/server/types.d.ts
CHANGED
|
@@ -5,7 +5,19 @@ export type ModelFilterRule = {
|
|
|
5
5
|
label?: string;
|
|
6
6
|
};
|
|
7
7
|
import type { WorkflowRun } from "./workflow.js";
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The lifecycle states a `Conversation.status` moves through while a turn is in flight. `waiting` is the
|
|
10
|
+
* gap between "prompt handed to the LLM" and "the first response token arrived" - holding it apart from
|
|
11
|
+
* `running` is what lets the reader tell a slow or broken LLM endpoint (still waiting) apart from a
|
|
12
|
+
* streaming turn that is just slow (already running, just no new tokens in the last second). Without
|
|
13
|
+
* the gap the two states look the same on screen for as long as the model is silent, and the reader has
|
|
14
|
+
* no signal to debug from.
|
|
15
|
+
*
|
|
16
|
+
* `completed` / `stopped` / `failed` are the three settled states. `stopped` is what `run.stop` lands;
|
|
17
|
+
* `failed` is what any unhandled throw lands (with `Conversation.error` carrying the translated reason);
|
|
18
|
+
* `completed` is the clean path.
|
|
19
|
+
*/
|
|
20
|
+
export type RunStatus = "idle" | "waiting" | "running" | "completed" | "stopped" | "failed";
|
|
9
21
|
export type MessageRole = "user" | "assistant" | "tool" | "system";
|
|
10
22
|
/**
|
|
11
23
|
* One message in `state.json`.
|
|
@@ -1078,6 +1090,12 @@ export type ClientCommandBody =
|
|
|
1078
1090
|
conversationId: string;
|
|
1079
1091
|
messageId: string;
|
|
1080
1092
|
}
|
|
1093
|
+
/** Stops the active turn, then starts this waiting message as a fresh turn. */
|
|
1094
|
+
| {
|
|
1095
|
+
type: "message.interrupt";
|
|
1096
|
+
conversationId: string;
|
|
1097
|
+
messageId: string;
|
|
1098
|
+
}
|
|
1081
1099
|
/** Starts this waiting message as the next turn, only after the current turn has ended. */
|
|
1082
1100
|
| {
|
|
1083
1101
|
type: "message.sendQueued";
|