@evandu/super-pi 0.1.90 → 0.1.92
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-Db0poZWq.css +1 -0
- package/dist/client/assets/index-DxMdKg12.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 +46 -2
- package/dist/server/command-handler.js +4 -0
- package/dist/server/rpc-channel.d.ts +9 -0
- package/dist/server/runtime.js +36 -0
- package/dist/server/store.d.ts +5 -4
- package/dist/server/store.js +29 -9
- package/dist/server/subagents.js +13 -1
- 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-DxMdKg12.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Db0poZWq.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
|
*
|
|
@@ -31,7 +31,7 @@ function formatTurnFailure(error) {
|
|
|
31
31
|
// PROVIDER QUOTA / RATE LIMIT - the only reason the user is in this branch is because Pi was
|
|
32
32
|
// answering them a moment ago and now the model will not let it. The action is "wait or change
|
|
33
33
|
// model"; the wording has to make that obvious from the banner alone, without a tooltip.
|
|
34
|
-
if (
|
|
34
|
+
if (/\b429\b|quota|rate limit|usage limit|too many requests|5-hour|5h|resource_exhausted|resourc exhaust|hit your limit|usage cap|usage restrict/.test(lower)) {
|
|
35
35
|
return "模型额度已用完(5 小时限额或类似上限),Pi 已停止回答。可以等待重置后重试,或在会话里换一个模型再发。";
|
|
36
36
|
}
|
|
37
37
|
// RPC CHANNEL DEAD - Pi died without producing a `settled` event. Anything in the turn is now
|
|
@@ -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
|
*
|
|
@@ -1115,7 +1156,10 @@ export class RunService {
|
|
|
1115
1156
|
await this.store.setStatus(conversationId, controller.signal.aborted ? "stopped" : "completed");
|
|
1116
1157
|
}
|
|
1117
1158
|
catch (error) {
|
|
1118
|
-
await this.store.setStatus(conversationId, "failed");
|
|
1159
|
+
await this.store.setStatus(conversationId, controller.signal.aborted ? "stopped" : "failed");
|
|
1160
|
+
if (!controller.signal.aborted) {
|
|
1161
|
+
await this.store.setFailureReason(conversationId, formatTurnFailure(error));
|
|
1162
|
+
}
|
|
1119
1163
|
// A preflight failure must put the message BACK where the reader can see it. Re-adding it would
|
|
1120
1164
|
// duplicate the row that is still in the transcript, so this only re-marks the one that is
|
|
1121
1165
|
// there - which is what makes "the send failed, this row is still in the queue" a true statement.
|
|
@@ -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;
|
|
@@ -29,9 +29,18 @@ export type RpcRecord = {
|
|
|
29
29
|
role?: string;
|
|
30
30
|
content?: unknown;
|
|
31
31
|
stopReason?: string;
|
|
32
|
+
errorMessage?: string;
|
|
32
33
|
model?: string;
|
|
33
34
|
};
|
|
34
35
|
};
|
|
36
|
+
/** `agent_end` is Pi's terminal result for one attempt. `willRetry` keeps a transient provider failure from becoming a terminal failure. */
|
|
37
|
+
messages?: Array<{
|
|
38
|
+
role?: string;
|
|
39
|
+
content?: unknown;
|
|
40
|
+
stopReason?: string;
|
|
41
|
+
errorMessage?: string;
|
|
42
|
+
}>;
|
|
43
|
+
willRetry?: boolean;
|
|
35
44
|
/**
|
|
36
45
|
* Cumulative usage of the CURRENT assistant message, repeated on every one of that message's updates
|
|
37
46
|
* (modes/json-event.ts:56-59). Reading it anywhere but on `done` counts the same tokens per token.
|
package/dist/server/runtime.js
CHANGED
|
@@ -588,6 +588,27 @@ export class PiRpcRuntime {
|
|
|
588
588
|
channel.write({ id: "prompt", type: "prompt", message: prompt, ...(imagePayloads?.length ? { images: imagePayloads } : {}) });
|
|
589
589
|
traceLatency(conversation.id, "prompt.sent", latencyStartedAt);
|
|
590
590
|
let settled = false;
|
|
591
|
+
let finalAgentOutcome;
|
|
592
|
+
let finalDoneOutcome;
|
|
593
|
+
const terminalOutcome = (message, reason) => {
|
|
594
|
+
if (!message?.stopReason && !message?.errorMessage && !reason)
|
|
595
|
+
return undefined;
|
|
596
|
+
return { reason, stopReason: message?.stopReason, errorMessage: message?.errorMessage };
|
|
597
|
+
};
|
|
598
|
+
const throwIfTerminalFailure = () => {
|
|
599
|
+
if (signal.aborted)
|
|
600
|
+
return;
|
|
601
|
+
const outcome = finalAgentOutcome ?? finalDoneOutcome;
|
|
602
|
+
if (!outcome)
|
|
603
|
+
throw new Error("Pi 已结束本轮运行,但没有报告最终结果。");
|
|
604
|
+
const reason = outcome.stopReason ?? outcome.reason;
|
|
605
|
+
if (reason === "error")
|
|
606
|
+
throw new Error(outcome.errorMessage || "Pi 报告模型或服务提供方错误。");
|
|
607
|
+
if (reason === "aborted")
|
|
608
|
+
throw new Error(outcome.errorMessage || "Pi 意外中止了本轮回答。");
|
|
609
|
+
if (reason === "length")
|
|
610
|
+
throw new Error(outcome.errorMessage || "模型输出达到本轮长度上限,回答未完成。");
|
|
611
|
+
};
|
|
591
612
|
let firstPiRecord = true;
|
|
592
613
|
// Nothing may be dropped without a trace. Pi emits a wide, open-ended family of agent
|
|
593
614
|
// session events (tool_call, thinking, compaction_*, auto_retry_*, ...), so an unhandled
|
|
@@ -774,6 +795,9 @@ export class PiRpcRuntime {
|
|
|
774
795
|
}
|
|
775
796
|
if (record.type === "message_update") {
|
|
776
797
|
const event = record.assistantMessageEvent;
|
|
798
|
+
if (event?.type === "done") {
|
|
799
|
+
finalDoneOutcome = terminalOutcome(event.message, event.reason);
|
|
800
|
+
}
|
|
777
801
|
// text_delta is what drives the visible answer, and toolcall_start/delta/end are what make a
|
|
778
802
|
// call VISIBLE WHILE IT IS BEING WRITTEN.
|
|
779
803
|
//
|
|
@@ -854,6 +878,15 @@ export class PiRpcRuntime {
|
|
|
854
878
|
}
|
|
855
879
|
continue;
|
|
856
880
|
}
|
|
881
|
+
if (record.type === "agent_end") {
|
|
882
|
+
const message = [...(record.messages ?? [])].reverse().find((item) => item.role === "assistant");
|
|
883
|
+
const outcome = terminalOutcome(message);
|
|
884
|
+
// Pi emits `agent_end` for every attempt. A retryable error is deliberately only observed
|
|
885
|
+
// here; Pi owns the retry and will replace this with the final attempt before it settles.
|
|
886
|
+
if (record.willRetry !== true && outcome)
|
|
887
|
+
finalAgentOutcome = outcome;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
857
890
|
if (record.type === "extension_ui_request") {
|
|
858
891
|
await this.handleExtensionUi(record, child, host, ui);
|
|
859
892
|
continue;
|
|
@@ -868,6 +901,9 @@ export class PiRpcRuntime {
|
|
|
868
901
|
}
|
|
869
902
|
if (record.type === "agent_settled") {
|
|
870
903
|
traceLatency(conversation.id, "pi.settled", latencyStartedAt);
|
|
904
|
+
// Only now is it safe to inspect a provider error: before this point Pi may still be
|
|
905
|
+
// automatically retrying a 429, timeout, or temporary upstream failure.
|
|
906
|
+
throwIfTerminalFailure();
|
|
871
907
|
settled = true;
|
|
872
908
|
continue;
|
|
873
909
|
}
|
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/subagents.js
CHANGED
|
@@ -396,7 +396,19 @@ export class SubagentSupervisor {
|
|
|
396
396
|
if (signal?.aborted)
|
|
397
397
|
throw new SubagentCancelled(id);
|
|
398
398
|
for (;;) {
|
|
399
|
-
|
|
399
|
+
let record;
|
|
400
|
+
try {
|
|
401
|
+
record = await channel.next(DRAIN_POLL_MS);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
// RpcChannel surfaces EOF immediately so a caller cannot mistake a dead Pi for an
|
|
405
|
+
// idle one. Preserve this supervisor's richer contract: the agent identity, phase
|
|
406
|
+
// and exit code are what make an unexpected child exit actionable to the user.
|
|
407
|
+
if (error?.code === "CHANNEL_CLOSED") {
|
|
408
|
+
throw new Error(`sub-agent ${agentId} (${id}) exited before settling (code ${child.exitCode}). ${channel.stderr.trim()}`);
|
|
409
|
+
}
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
400
412
|
if (!record)
|
|
401
413
|
break;
|
|
402
414
|
if (record.type === "response" && record.id === "prompt" && record.success === false)
|
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";
|