@evandu/super-pi 0.1.90 → 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 +9 -0
- package/dist/server/application/run-service.js +41 -0
- package/dist/server/command-handler.js +4 -0
- package/dist/server/store.d.ts +5 -4
- package/dist/server/store.js +29 -9
- package/dist/server/types.d.ts +19 -1
- package/package.json +1 -1
- package/dist/client/assets/index-DgQRMp3j.css +0 -1
- package/dist/client/assets/index-p1BjYE_T.js +0 -73
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-DYjBTgl4.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BcHIMxNF.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -37,9 +37,35 @@ export class ConversationService {
|
|
|
37
37
|
const workspace = this.store.snapshot().workspaces.find(item => item.id === workspaceId);
|
|
38
38
|
if (!workspace)
|
|
39
39
|
throw new Error("Workspace not found");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// Workspace defaults are copied at birth, but Pi's model list can change after the
|
|
41
|
+
// setting was saved (for example when a model is removed from Pi's config). Do not
|
|
42
|
+
// stamp a dead selection onto every new conversation: the scoped catalog probe would
|
|
43
|
+
// fail and leave the model/thinking controls unavailable. A missing or invalid default
|
|
44
|
+
// means "follow Pi's current default" for this newly-created conversation.
|
|
45
|
+
let model = workspace.defaultModel;
|
|
46
|
+
let thinkingLevel = workspace.defaultThinkingLevel;
|
|
47
|
+
const catalog = model || thinkingLevel ? await this.runtime.catalog() : undefined;
|
|
48
|
+
if (catalog?.source === "pi" && model) {
|
|
49
|
+
const option = catalog.models.find(item => item.provider === model?.provider && item.modelId === model?.modelId);
|
|
50
|
+
if (!option) {
|
|
51
|
+
model = undefined;
|
|
52
|
+
thinkingLevel = undefined;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
model = { provider: option.provider, modelId: option.modelId, label: option.label };
|
|
56
|
+
if (thinkingLevel) {
|
|
57
|
+
const scoped = await this.runtime.catalog(model);
|
|
58
|
+
if (scoped.source === "pi" && !scoped.levels.includes(thinkingLevel))
|
|
59
|
+
thinkingLevel = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const resolved = workspace.defaultAgentId
|
|
64
|
+
? await this.resolveAgentConfiguration({ id: workspaceId, model, thinkingLevel }, workspace.defaultAgentId, workspaceId)
|
|
65
|
+
: { model, thinkingLevel };
|
|
66
|
+
// `null` is intentional here: Store uses it to remove a workspace value that it
|
|
67
|
+
// would otherwise have copied before applying the resolved configuration.
|
|
68
|
+
const defaults = { ...resolved, model: resolved.model ?? null, thinkingLevel: resolved.thinkingLevel ?? null };
|
|
43
69
|
const created = await this.store.createConversation(workspaceId, defaults);
|
|
44
70
|
this.broadcaster.broadcastState();
|
|
45
71
|
return created.id;
|
|
@@ -239,6 +239,15 @@ export declare class RunService {
|
|
|
239
239
|
steer(conversationId: string, messageId: string): Promise<void>;
|
|
240
240
|
/** Starts a chosen waiting message as the next turn, only when the conversation is idle. */
|
|
241
241
|
sendQueued(conversationId: string, messageId: string): Promise<void>;
|
|
242
|
+
/**
|
|
243
|
+
* Stops the current turn and immediately gives one waiting message its own fresh turn.
|
|
244
|
+
*
|
|
245
|
+
* `steer` cannot provide this meaning: Pi only reads its steering queue after the current tool batch
|
|
246
|
+
* finishes. Stopping first is what makes a long-running tool give way now. `sendQueued` already owns
|
|
247
|
+
* the hand-off window after a stop, so it waits until the old run has released its slot before opening
|
|
248
|
+
* the selected message. That preserves the one-Pi-per-conversation invariant.
|
|
249
|
+
*/
|
|
250
|
+
interruptQueued(conversationId: string, messageId: string): Promise<void>;
|
|
242
251
|
/**
|
|
243
252
|
* The lower-level "wait for run slot, open the turn on a specific waiting message, run it" sequence.
|
|
244
253
|
*
|
|
@@ -542,6 +542,22 @@ export class RunService {
|
|
|
542
542
|
let assistant = assistantMessage;
|
|
543
543
|
let toolCalls = [];
|
|
544
544
|
let pendingTools = Promise.resolve();
|
|
545
|
+
// A TURN MOVES FROM `waiting` TO `running` ONCE THE MODEL HAS PRODUCED ANY SIGNAL: a text delta
|
|
546
|
+
// OR a tool-call notice. The `running` state means "we have data flowing", and that is true the
|
|
547
|
+
// moment Pi yields anything back, regardless of shape. Without this, a pure tool-call turn would
|
|
548
|
+
// stay in `waiting` until the very end and the reader would see a stuck turn for the whole of a
|
|
549
|
+
// long tool execution. The flag is the only way to make the flip one-shot: calling `setStatus` on
|
|
550
|
+
// every delta would be a per-token status write.
|
|
551
|
+
let firstSignal = true;
|
|
552
|
+
const promoteToRunning = async () => {
|
|
553
|
+
if (!firstSignal)
|
|
554
|
+
return;
|
|
555
|
+
firstSignal = false;
|
|
556
|
+
if (this.store.snapshot().conversations.find((item) => item.id === conversation.id)?.status === "waiting") {
|
|
557
|
+
await this.store.setStatus(conversation.id, "running");
|
|
558
|
+
this.broadcaster.broadcastState();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
545
561
|
// Live events are independent from durable checkpoints. Persisting every token made the stdout
|
|
546
562
|
// reader wait for one disk transaction per token; over a remote browser connection that backlog
|
|
547
563
|
// looked exactly like a slow model. Keep the in-memory transcript current, publish immediately, and
|
|
@@ -570,6 +586,12 @@ export class RunService {
|
|
|
570
586
|
};
|
|
571
587
|
const enqueueTools = (what, work) => {
|
|
572
588
|
pendingTools = pendingTools.then(async () => {
|
|
589
|
+
// A TOOL CALL IS A SIGNAL THE MODEL IS ALIVE. Promote `waiting` → `running` on the same
|
|
590
|
+
// path as text deltas: a tool row appearing on screen is the same "we have data" event
|
|
591
|
+
// the status needs to reflect. Doing it BEFORE the tool write keeps the order
|
|
592
|
+
// honest - a client that saw the new tool row in the same frame also saw the running
|
|
593
|
+
// status, instead of flashing back to a still-spinning waiting mark for the next paint.
|
|
594
|
+
await promoteToRunning();
|
|
573
595
|
toolCalls = await work();
|
|
574
596
|
this.broadcaster.broadcast({ type: "message.tools", conversationId: conversation.id, messageId: assistant.id, toolCalls });
|
|
575
597
|
}).catch((error) => {
|
|
@@ -853,6 +875,12 @@ export class RunService {
|
|
|
853
875
|
// publish a new message underneath it. One `await` at the top covers both, and costs nothing:
|
|
854
876
|
// the lane is empty most of the time.
|
|
855
877
|
await pendingTools;
|
|
878
|
+
// `waiting` → `running` ON THE FIRST DELTA, before the live write. The conversation was
|
|
879
|
+
// opened in `waiting` (see `openTurn` / `openTurnOnSpecificMessage` / `openTurnOnMessage`),
|
|
880
|
+
// and the first response token is the observable fact that says the LLM is actually
|
|
881
|
+
// answering rather than hanging on auth / quota / network. `promoteToRunning` is a no-op
|
|
882
|
+
// after the first signal so this stays a one-shot status write, not a per-token one.
|
|
883
|
+
await promoteToRunning();
|
|
856
884
|
this.store.appendAssistantDeltaLive(conversation.id, assistant.id, delta);
|
|
857
885
|
spoken += delta.length;
|
|
858
886
|
// Tool events are structural transcript boundaries. Publish any boundary Pi announced before
|
|
@@ -1088,6 +1116,19 @@ export class RunService {
|
|
|
1088
1116
|
}
|
|
1089
1117
|
await this.processWaitingMessage(conversationId, messageId);
|
|
1090
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Stops the current turn and immediately gives one waiting message its own fresh turn.
|
|
1121
|
+
*
|
|
1122
|
+
* `steer` cannot provide this meaning: Pi only reads its steering queue after the current tool batch
|
|
1123
|
+
* finishes. Stopping first is what makes a long-running tool give way now. `sendQueued` already owns
|
|
1124
|
+
* the hand-off window after a stop, so it waits until the old run has released its slot before opening
|
|
1125
|
+
* the selected message. That preserves the one-Pi-per-conversation invariant.
|
|
1126
|
+
*/
|
|
1127
|
+
async interruptQueued(conversationId, messageId) {
|
|
1128
|
+
if (this.activeRuns.has(conversationId))
|
|
1129
|
+
await this.stopRun(conversationId);
|
|
1130
|
+
await this.sendQueued(conversationId, messageId);
|
|
1131
|
+
}
|
|
1091
1132
|
/**
|
|
1092
1133
|
* The lower-level "wait for run slot, open the turn on a specific waiting message, run it" sequence.
|
|
1093
1134
|
*
|
|
@@ -169,6 +169,10 @@ export class CommandHandler {
|
|
|
169
169
|
await this.runService.steer(command.conversationId, command.messageId);
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
|
+
if (command.type === "message.interrupt") {
|
|
173
|
+
await this.runService.interruptQueued(command.conversationId, command.messageId);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
172
176
|
if (command.type === "message.sendQueued") {
|
|
173
177
|
await this.runService.sendQueued(command.conversationId, command.messageId);
|
|
174
178
|
return;
|
package/dist/server/store.d.ts
CHANGED
|
@@ -107,8 +107,8 @@ export declare class Store {
|
|
|
107
107
|
deleteWorkspace(workspaceId: string): Promise<string[]>;
|
|
108
108
|
createConversation(workspaceId: string, configuration?: {
|
|
109
109
|
agentId?: string;
|
|
110
|
-
model?: ModelSelection;
|
|
111
|
-
thinkingLevel?: string;
|
|
110
|
+
model?: ModelSelection | null;
|
|
111
|
+
thinkingLevel?: string | null;
|
|
112
112
|
}): Promise<Conversation>;
|
|
113
113
|
/**
|
|
114
114
|
* Copies a conversation into a NEW one, under an id the caller has already minted.
|
|
@@ -549,9 +549,10 @@ export declare class Store {
|
|
|
549
549
|
* question that is still waiting - a transcript lying about its own order. Splicing at
|
|
550
550
|
* `indexOf(user) + 1` keeps every waiting message below the reply that belongs to the one above it.
|
|
551
551
|
*
|
|
552
|
-
* The side effects - title, `updatedAt`, `status: "
|
|
552
|
+
* The side effects - title, `updatedAt`, `status: "waiting"`, clearing a stale failure - all describe a
|
|
553
553
|
* TURN rather than a message, which is why `addUserMessage` skips them for a message that is only
|
|
554
|
-
* waiting. They belong here, at the moment the turn genuinely begins
|
|
554
|
+
* waiting. They belong here, at the moment the turn genuinely begins - the model still has not
|
|
555
|
+
* returned a token, and that is the shape the row carries until `streamTurn` sees the first delta.
|
|
555
556
|
*/
|
|
556
557
|
openTurnOnMessage(conversationId: string, messageId: string, agentIdentity?: Message["agentIdentity"]): Promise<{
|
|
557
558
|
user: Message;
|
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;
|
|
@@ -690,10 +694,18 @@ export class Store {
|
|
|
690
694
|
if (workspace.defaultAgentId)
|
|
691
695
|
conversation.agentId = workspace.defaultAgentId;
|
|
692
696
|
if (configuration) {
|
|
693
|
-
if (configuration.model)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
+
}
|
|
697
709
|
if (configuration.agentId)
|
|
698
710
|
conversation.agentId = configuration.agentId;
|
|
699
711
|
}
|
|
@@ -876,8 +888,13 @@ export class Store {
|
|
|
876
888
|
: (firstUser?.images?.length ? "【图片】" : DEFAULT_CONVERSATION_TITLE);
|
|
877
889
|
if (!conversation.titleLocked)
|
|
878
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.
|
|
879
896
|
conversation.updatedAt = message.createdAt;
|
|
880
|
-
conversation.status = "
|
|
897
|
+
conversation.status = "waiting";
|
|
881
898
|
// The reason this conversation was failed - startup reconciliation's note that a previous run was cut
|
|
882
899
|
// short - describes THAT run, and this line is the moment it stops being the current one. Deleting the
|
|
883
900
|
// key rather than overwriting it keeps the file byte-identical to a conversation that was never
|
|
@@ -1610,9 +1627,10 @@ export class Store {
|
|
|
1610
1627
|
* question that is still waiting - a transcript lying about its own order. Splicing at
|
|
1611
1628
|
* `indexOf(user) + 1` keeps every waiting message below the reply that belongs to the one above it.
|
|
1612
1629
|
*
|
|
1613
|
-
* The side effects - title, `updatedAt`, `status: "
|
|
1630
|
+
* The side effects - title, `updatedAt`, `status: "waiting"`, clearing a stale failure - all describe a
|
|
1614
1631
|
* TURN rather than a message, which is why `addUserMessage` skips them for a message that is only
|
|
1615
|
-
* 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.
|
|
1616
1634
|
*/
|
|
1617
1635
|
async openTurnOnMessage(conversationId, messageId, agentIdentity) {
|
|
1618
1636
|
const conversation = this.requireConversation(conversationId);
|
|
@@ -1629,8 +1647,10 @@ export class Store {
|
|
|
1629
1647
|
: (firstUser?.images?.length ? "【图片】" : DEFAULT_CONVERSATION_TITLE);
|
|
1630
1648
|
if (!conversation.titleLocked)
|
|
1631
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".
|
|
1632
1652
|
conversation.updatedAt = user.createdAt;
|
|
1633
|
-
conversation.status = "
|
|
1653
|
+
conversation.status = "waiting";
|
|
1634
1654
|
delete conversation.error;
|
|
1635
1655
|
await this.persistMessages(conversation);
|
|
1636
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";
|