@evandu/super-pi 0.1.41 → 0.1.42
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-DTd4g0h5.css → index-C6BogHc-.css} +1 -1
- package/dist/client/assets/{index-Dihs8Ezc.js → index-DlKKZuzf.js} +3 -2
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.js +34 -25
- package/dist/server/runtime.js +5 -3
- package/dist/server/store.js +16 -1
- package/dist/server/types.d.ts +16 -0
- package/package.json +1 -1
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-DlKKZuzf.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C6BogHc-.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -314,6 +314,11 @@ export class RunService {
|
|
|
314
314
|
// an order that never happened. The runtime calls `onToolCall` synchronously between yields, so a
|
|
315
315
|
// counter advanced once per yielded delta is the real position of the call in the answer.
|
|
316
316
|
let spoken = 0;
|
|
317
|
+
// AND HOW MUCH REASONING HAD BEEN STREAMED, for the same reason and from the same place. The store's
|
|
318
|
+
// `message.thinking` is only updated at the end of the turn (in `finally`), so it cannot be read here.
|
|
319
|
+
// The running total is what lets a call made during reasoning anchor inside the thinking block rather than
|
|
320
|
+
// after it, which is the order the events had.
|
|
321
|
+
let thought = 0;
|
|
317
322
|
/**
|
|
318
323
|
* ONE COUNTER FOR EVERY ROW, AND THE ONLY WAY A ROW NUMBER IS EVER MADE.
|
|
319
324
|
*
|
|
@@ -364,6 +369,7 @@ export class RunService {
|
|
|
364
369
|
...(call.detail ? { detail: call.detail } : { detail: "正在生成参数…" }),
|
|
365
370
|
preparing: true,
|
|
366
371
|
at,
|
|
372
|
+
thinkingAt: thought,
|
|
367
373
|
}));
|
|
368
374
|
preparingRows.set(call.contentIndex, { row, name: call.name });
|
|
369
375
|
};
|
|
@@ -395,7 +401,7 @@ export class RunService {
|
|
|
395
401
|
const row = rows++;
|
|
396
402
|
if (call.id)
|
|
397
403
|
rowByToolCallId.set(call.id, row);
|
|
398
|
-
enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args, at, ...(call.question ? { question: call.question } : {}) }));
|
|
404
|
+
enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args, at, thinkingAt: thought, ...(call.question ? { question: call.question } : {}) }));
|
|
399
405
|
};
|
|
400
406
|
const onToolUpdate = (call) => {
|
|
401
407
|
const index = call.id ? rowByToolCallId.get(call.id) : undefined;
|
|
@@ -425,6 +431,7 @@ export class RunService {
|
|
|
425
431
|
// reached nobody, live or afterwards. Both halves now happen: the browser draws the delta as it
|
|
426
432
|
// arrives, and the message keeps it so the finished turn has something to go back to.
|
|
427
433
|
onThinking: (delta) => {
|
|
434
|
+
thought += delta.length;
|
|
428
435
|
this.broadcaster.broadcast({ type: "message.thinking", conversationId: conversation.id, messageId: assistant.id, delta, active: true });
|
|
429
436
|
void this.store.appendThinkingDelta(conversation.id, assistant.id, delta);
|
|
430
437
|
},
|
|
@@ -630,36 +637,38 @@ export class RunService {
|
|
|
630
637
|
const result = run.status === "completed" ? run.output ?? "" : `Background task failed: ${run.error ?? run.status}`;
|
|
631
638
|
const tag = run.status === "completed" ? "task_result" : "task_error";
|
|
632
639
|
const message = `<task id="${run.id}" state="${run.status === "completed" ? "completed" : "error"}">\n<summary>Background task ${run.status === "completed" ? "completed" : "failed"}: ${body.description?.trim() || subagentType}</summary>\n<${tag}>\n${result}\n</${tag}>\n</task>`;
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
640
|
+
// A BACKGROUND RESULT IS A PROMPT, AND DELIVERING IT IS NOT TRIGGERING IT.
|
|
641
|
+
//
|
|
642
|
+
// This used to park it in the queue until the reader sent the next prompt, which is the hole the
|
|
643
|
+
// queue exists to avoid: nothing owned it. `takeNextQueuedMessage` is only ever called from
|
|
644
|
+
// inside a turn, so a result arriving at an IDLE conversation sat in the dock forever - and when
|
|
645
|
+
// the reader tried to steer it, the runtime refused with 「会话 X 现在没有正在运行的 Pi 进程」,
|
|
646
|
+
// because of course there was no run: the message had been waiting for one nobody would start.
|
|
647
|
+
//
|
|
648
|
+
// `enqueueAndRun` is the owner this needed. It claims the run slot synchronously, so it starts a
|
|
649
|
+
// turn when the conversation is idle and enqueues when one is running - and the turn it starts
|
|
650
|
+
// drains whatever follows. One call, and no queued message is ever left without a scheduled reader.
|
|
651
|
+
// HANDED OFF, NOT AWAITED. `enqueueAndRun` resolves when the whole turn it started has FINISHED,
|
|
652
|
+
// and this handler owns the `backgroundTasks` row - so awaiting it here kept that row on screen
|
|
653
|
+
// for the entire delivered answer, a finished task wearing its own 「running」 state. The row is
|
|
654
|
+
// removed as soon as the result has a home, which is what its lifecycle means.
|
|
655
|
+
const started = this.enqueueAndRun(conversation.id, message);
|
|
656
|
+
await this.store.removeBackgroundTask(run.id);
|
|
657
|
+
this.broadcaster.broadcastState();
|
|
658
|
+
await started.catch(async (error) => {
|
|
659
|
+
// The turn could not start, so the result has nowhere to be read from. Put it back in the queue,
|
|
660
|
+
// where it is at least visible and cancellable, rather than dropping it on the floor.
|
|
643
661
|
await this.store.enqueueMessage(conversation.id, message);
|
|
644
|
-
console.warn(`Super Pi: background result could not start a turn: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
645
|
-
}
|
|
646
|
-
finally {
|
|
647
|
-
await this.store.removeBackgroundTask(run.id);
|
|
648
662
|
this.broadcaster.broadcastState();
|
|
649
|
-
|
|
663
|
+
console.warn(`Super Pi: background result could not start a turn: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
664
|
+
});
|
|
650
665
|
}).catch(async (error) => {
|
|
651
666
|
const message = `<task id="${backgroundId}" state="error">\n<summary>Background task failed: ${body.description?.trim() || subagentType}</summary>\n<task_error>Background task failed before completion: ${error instanceof Error ? error.message : "unknown error"}</task_error>\n</task>`;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
await this.enqueueAndRun(conversationId, message);
|
|
655
|
-
else
|
|
656
|
-
await this.store.enqueueMessage(conversationId, message);
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
await this.store.enqueueMessage(conversationId, message);
|
|
660
|
-
}
|
|
667
|
+
// The same rule as the task result above, and the same shape: hand it off, then drop the row.
|
|
668
|
+
const started = this.enqueueAndRun(conversationId, message);
|
|
661
669
|
await this.store.removeBackgroundTask(backgroundId);
|
|
662
670
|
this.broadcaster.broadcastState();
|
|
671
|
+
await started.catch(async () => { await this.store.enqueueMessage(conversationId, message); this.broadcaster.broadcastState(); });
|
|
663
672
|
console.warn(`Super Pi: background sub-agent dispatch failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
664
673
|
}));
|
|
665
674
|
return {
|
package/dist/server/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { resolve, join } from "node:path";
|
|
6
6
|
import { agentSpawnArgs, readAgents } from "./agents.js";
|
|
7
7
|
import { TASK_CAPABILITY_ENV, mintCapability, releaseCapability } from "./capability.js";
|
|
@@ -779,7 +779,6 @@ export class PiRpcRuntime {
|
|
|
779
779
|
} : {}) } } : {}),
|
|
780
780
|
});
|
|
781
781
|
const channel = new RpcChannel(child);
|
|
782
|
-
this.runs.set(conversation.id, { child, channel });
|
|
783
782
|
// Same guard as subagents.ts: a destroyed stdin throws synchronously on write, and an abort
|
|
784
783
|
// listener that throws inside a listener would surface as an unhandled error instead of a cancel.
|
|
785
784
|
const abort = () => {
|
|
@@ -994,6 +993,7 @@ export class PiRpcRuntime {
|
|
|
994
993
|
}
|
|
995
994
|
if (record.type === "message_update") {
|
|
996
995
|
const event = record.assistantMessageEvent;
|
|
996
|
+
console.log(`[runtime] message_update event.type=${JSON.stringify(event?.type)}`);
|
|
997
997
|
// text_delta is what drives the visible answer, and toolcall_start/delta/end are what make a
|
|
998
998
|
// call VISIBLE WHILE IT IS BEING WRITTEN.
|
|
999
999
|
//
|
|
@@ -1027,8 +1027,10 @@ export class PiRpcRuntime {
|
|
|
1027
1027
|
}
|
|
1028
1028
|
else if (event?.type === "thinking_delta")
|
|
1029
1029
|
onThinking?.(typeof event.delta === "string" ? event.delta : "");
|
|
1030
|
-
else
|
|
1030
|
+
else {
|
|
1031
1031
|
noteUnhandled(`message_update:${JSON.stringify(event?.type ?? "(missing)")}`);
|
|
1032
|
+
console.log(`[runtime] message_update not matched: event.type=${JSON.stringify(event?.type)}`);
|
|
1033
|
+
}
|
|
1032
1034
|
continue;
|
|
1033
1035
|
}
|
|
1034
1036
|
if (record.type === "extension_ui_request") {
|
package/dist/server/store.js
CHANGED
|
@@ -913,7 +913,22 @@ export class Store {
|
|
|
913
913
|
// halves: the call arrived, and the message already holds every character streamed before it. Without
|
|
914
914
|
// it the browser can only render the blocks as one list beside the text, so a turn that read a file,
|
|
915
915
|
// said something, then ran a test would show all three in the wrong order.
|
|
916
|
-
|
|
916
|
+
//
|
|
917
|
+
// AND WHERE IN THE REASONING, for the same reason and from the same place. `thinking` is the other
|
|
918
|
+
// stream a turn has, and a call made while the model was still thinking has to be placeable inside it -
|
|
919
|
+
// otherwise the transcript can only say "it reasoned, and then it called a tool", which is the one order
|
|
920
|
+
// a `hmm → read → hmm → grep → answer` turn never had. Stamped as the thinking length BEFORE this call,
|
|
921
|
+
// so a call that started before any reasoning anchors at 0 and renders first.
|
|
922
|
+
//
|
|
923
|
+
// The thinking read here is the message's own field, written synchronously as each delta arrives - not
|
|
924
|
+
// a count of how much a queued disk write had managed to flush. That distinction is the same one `at`
|
|
925
|
+
// was stamped for.
|
|
926
|
+
const placed = {
|
|
927
|
+
...call,
|
|
928
|
+
...(call.at === undefined ? { at: message.content.length } : {}),
|
|
929
|
+
...(call.thinkingAt === undefined ? { thinkingAt: (message.thinking ?? "").length } : {}),
|
|
930
|
+
};
|
|
931
|
+
calls.push(structuredClone(placed));
|
|
917
932
|
conversation.updatedAt = now();
|
|
918
933
|
await this.persist();
|
|
919
934
|
return structuredClone(calls);
|
package/dist/server/types.d.ts
CHANGED
|
@@ -167,7 +167,23 @@ export type ToolCallBlock = {
|
|
|
167
167
|
* the only thing on screen during a long parameter stream - an 'edit' block carries a whole file's
|
|
168
168
|
* replacement text, and without this the screen is blank until the last token arrives. */
|
|
169
169
|
preparing?: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* WHERE IN THE ANSWER the call happened, as a character offset into `content` - and WHERE IN THE
|
|
172
|
+
* REASONING, as an offset into `thinking`.
|
|
173
|
+
*
|
|
174
|
+
* A TURN HAS TWO STREAMS, and a call belongs to a position in each. `at` places it among the words the
|
|
175
|
+
* agent spoke; `thinkingAt` places it inside the reasoning it did before speaking, which is what lets a
|
|
176
|
+
* call that happened mid-thought render INSIDE the thinking block rather than beside it. Without the
|
|
177
|
+
* second offset a tool call can only be stacked after the reasoning, which reads as "the model thought,
|
|
178
|
+
* and then, separately, called a tool" - the order the events did not have.
|
|
179
|
+
*
|
|
180
|
+
* OPTIONAL, and absent means "this call's place in the reasoning was never recorded" - a message written
|
|
181
|
+
* before the field existed. A reader that guesses a position for such a call would be re-ordering a turn
|
|
182
|
+
* on no evidence, so it must fall back to `at` alone, which is exactly what those blocks were written to
|
|
183
|
+
* be read by.
|
|
184
|
+
*/
|
|
170
185
|
at?: number;
|
|
186
|
+
thinkingAt?: number;
|
|
171
187
|
question?: ToolCallQuestion;
|
|
172
188
|
};
|
|
173
189
|
/**
|