@evandu/super-pi 0.1.76 → 0.1.78
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/README.md +16 -7
- package/agents/super-pi-task/core.ts +23 -0
- package/agents/super-pi-task/index.ts +8 -6
- package/dist/client/.super-pi-client-sources.json +25 -0
- package/dist/client/assets/index-C7wCG7Jn.css +1 -0
- package/dist/client/assets/index-DCtDhAuw.js +58 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.d.ts +45 -0
- package/dist/server/application/run-service.js +139 -5
- package/dist/server/http-server.js +158 -75
- package/dist/server/runtime.d.ts +5 -2
- package/dist/server/runtime.js +7 -1
- package/dist/server/store.d.ts +3 -1
- package/dist/server/store.js +9 -2
- package/dist/server/subagent-runs.d.ts +8 -0
- package/dist/server/subagent-runs.js +1 -1
- package/dist/server/tool-runner.js +3 -3
- package/dist/server/types.d.ts +42 -0
- package/dist/server/ws-server.js +11 -4
- package/package.json +4 -2
- package/dist/client/assets/index-CHRkbLTs.js +0 -59
- package/dist/client/assets/index-CKdlMXJt.css +0 -1
package/dist/server/runtime.d.ts
CHANGED
|
@@ -100,8 +100,11 @@ export interface AnswerInput {
|
|
|
100
100
|
* replacement text, and one that shows the call arriving.
|
|
101
101
|
*/
|
|
102
102
|
onToolPreparing?(call: ToolCallPreparing): void;
|
|
103
|
-
/** Called while the primary model is emitting reasoning, so the UI can show live progress.
|
|
103
|
+
/** Called while the primary model is emitting reasoning, so the UI can show live progress. A call means
|
|
104
|
+
* the model is STILL THINKING - there is no separate flag, because Pi does not send one. */
|
|
104
105
|
onThinking?(delta: string): void;
|
|
106
|
+
/** Called when the reasoning phase ends - the only signal that it has. */
|
|
107
|
+
onThinkingEnd?(): void;
|
|
105
108
|
}
|
|
106
109
|
/**
|
|
107
110
|
* What Pi answered when a `steer` was written to it, from the `disposition` of that command's response
|
|
@@ -492,7 +495,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
492
495
|
* own address, so it is the same string on every turn and is read once from the constructor's options.
|
|
493
496
|
*/
|
|
494
497
|
private taskExtensionEnv;
|
|
495
|
-
answer({ conversation, prompt, signal, cwd, host, onPiQueue, onUserEntry, onSessionFile, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, images, memoryReview, memorySourceConversationId }: AnswerInput): AsyncIterable<string>;
|
|
498
|
+
answer({ conversation, prompt, signal, cwd, host, onPiQueue, onUserEntry, onSessionFile, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, onThinkingEnd, images, memoryReview, memorySourceConversationId }: AnswerInput): AsyncIterable<string>;
|
|
496
499
|
/**
|
|
497
500
|
* Reads back the id of the user message this turn just created, and hands it to the caller.
|
|
498
501
|
*
|
package/dist/server/runtime.js
CHANGED
|
@@ -398,7 +398,7 @@ export class PiRpcRuntime {
|
|
|
398
398
|
[TASK_CAPABILITY_ENV]: capability,
|
|
399
399
|
};
|
|
400
400
|
}
|
|
401
|
-
async *answer({ conversation, prompt, signal, cwd, host, onPiQueue, onUserEntry, onSessionFile, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, images, memoryReview, memorySourceConversationId }) {
|
|
401
|
+
async *answer({ conversation, prompt, signal, cwd, host, onPiQueue, onUserEntry, onSessionFile, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, onThinkingEnd, images, memoryReview, memorySourceConversationId }) {
|
|
402
402
|
const latencyStartedAt = performance.now();
|
|
403
403
|
traceLatency(conversation.id, "answer.begin", latencyStartedAt);
|
|
404
404
|
await mkdir(this.sessionDir, { recursive: true });
|
|
@@ -723,8 +723,14 @@ export class PiRpcRuntime {
|
|
|
723
723
|
const detail = describeToolCallInProgress(name, text);
|
|
724
724
|
onToolPreparing?.({ contentIndex: event.contentIndex, name, ...(text ? { live: text } : {}), ...(detail ? { detail } : {}) });
|
|
725
725
|
}
|
|
726
|
+
// Pi's reasoning lifecycle is three events, not one flag: `thinking_delta` means the model is
|
|
727
|
+
// STILL THINKING RIGHT NOW, and `thinking_end` is the only thing that says it stopped. There is
|
|
728
|
+
// deliberately no `active` field read here - every `thinking_delta` in Pi's wire shape carries
|
|
729
|
+
// `{ type, contentIndex, delta, partial }` and nothing else, so a delta is itself the evidence.
|
|
726
730
|
else if (event?.type === "thinking_delta")
|
|
727
731
|
onThinking?.(typeof event.delta === "string" ? event.delta : "");
|
|
732
|
+
else if (event?.type === "thinking_end")
|
|
733
|
+
onThinkingEnd?.();
|
|
728
734
|
else {
|
|
729
735
|
noteUnhandled(`message_update:${JSON.stringify(event?.type ?? "(missing)")}`);
|
|
730
736
|
}
|
package/dist/server/store.d.ts
CHANGED
|
@@ -241,7 +241,8 @@ export declare class Store {
|
|
|
241
241
|
* to go back to. Persisting here rather than only in the browser is the point of its living on the
|
|
242
242
|
* message: a reasoning stream that vanishes on reload was never a record of anything.
|
|
243
243
|
*/
|
|
244
|
-
appendThinkingDeltaLive(conversationId: string, messageId: string, delta: string, at?: number): void;
|
|
244
|
+
appendThinkingDeltaLive(conversationId: string, messageId: string, delta: string, at?: number, newBlock?: boolean): void;
|
|
245
|
+
setThinkingActiveLive(conversationId: string, messageId: string, active: boolean): void;
|
|
245
246
|
appendThinkingDelta(conversationId: string, messageId: string, delta: string): Promise<void>;
|
|
246
247
|
/** Persists the message exactly as it stands now, including any live text, thinking and tool updates. */
|
|
247
248
|
checkpointMessage(conversationId: string, messageId: string): Promise<void>;
|
|
@@ -304,6 +305,7 @@ export declare class Store {
|
|
|
304
305
|
live?: string;
|
|
305
306
|
preparing?: boolean;
|
|
306
307
|
question?: ToolCallQuestion;
|
|
308
|
+
backgroundTask?: ToolCallBlock["backgroundTask"];
|
|
307
309
|
}): Promise<ToolCallBlock[]>;
|
|
308
310
|
setStatus(conversationId: string, status: Conversation["status"]): Promise<void>;
|
|
309
311
|
/** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
|
package/dist/server/store.js
CHANGED
|
@@ -907,7 +907,7 @@ export class Store {
|
|
|
907
907
|
* to go back to. Persisting here rather than only in the browser is the point of its living on the
|
|
908
908
|
* message: a reasoning stream that vanishes on reload was never a record of anything.
|
|
909
909
|
*/
|
|
910
|
-
appendThinkingDeltaLive(conversationId, messageId, delta, at) {
|
|
910
|
+
appendThinkingDeltaLive(conversationId, messageId, delta, at, newBlock = false) {
|
|
911
911
|
const conversation = this.requireConversation(conversationId);
|
|
912
912
|
const message = conversation.messages.find((item) => item.id === messageId);
|
|
913
913
|
if (!message)
|
|
@@ -915,12 +915,17 @@ export class Store {
|
|
|
915
915
|
message.thinking = `${message.thinking ?? ""}${delta}`;
|
|
916
916
|
const blocks = (message.thinkingBlocks ??= []);
|
|
917
917
|
const last = blocks.at(-1);
|
|
918
|
-
if (last && last.at === (at ?? last.at))
|
|
918
|
+
if (!newBlock && last && last.at === (at ?? last.at))
|
|
919
919
|
last.text += delta;
|
|
920
920
|
else
|
|
921
921
|
blocks.push({ at: at ?? message.content.length, text: delta });
|
|
922
922
|
conversation.updatedAt = now();
|
|
923
923
|
}
|
|
924
|
+
setThinkingActiveLive(conversationId, messageId, active) {
|
|
925
|
+
const message = this.requireConversation(conversationId).messages.find((item) => item.id === messageId);
|
|
926
|
+
if (message)
|
|
927
|
+
message.thinkingActive = active;
|
|
928
|
+
}
|
|
924
929
|
async appendThinkingDelta(conversationId, messageId, delta) {
|
|
925
930
|
this.appendThinkingDeltaLive(conversationId, messageId, delta);
|
|
926
931
|
await this.checkpointMessage(conversationId, messageId);
|
|
@@ -1050,6 +1055,8 @@ export class Store {
|
|
|
1050
1055
|
// whatever the latest update happened to carry loses every key the update left out, which on the very
|
|
1051
1056
|
// first update is all of them. Merging can only ever add, so a sparse update can no longer erase what
|
|
1052
1057
|
// an earlier one already reported - and the cap is re-applied here rather than trusted from outside.
|
|
1058
|
+
if (patch.backgroundTask !== undefined)
|
|
1059
|
+
call.backgroundTask = structuredClone(patch.backgroundTask);
|
|
1053
1060
|
if (patch.label !== undefined)
|
|
1054
1061
|
call.label = patch.label;
|
|
1055
1062
|
if (patch.detail !== undefined)
|
|
@@ -41,6 +41,14 @@ export type SubagentDispatch = {
|
|
|
41
41
|
* passes "model" explicitly.
|
|
42
42
|
*/
|
|
43
43
|
dispatchedBy?: "human" | "model";
|
|
44
|
+
/**
|
|
45
|
+
* Where in the dispatching message this run was asked for, resolved by the caller from the tool call's
|
|
46
|
+
* id. Copied straight onto the record, because a card's position is a fact about its BIRTH - it is
|
|
47
|
+
* settled here and no update has to supply it, so the card cannot drift while the child streams.
|
|
48
|
+
* Omitted means "no call asked for it" (a person pressing the button), which is not row zero and never is.
|
|
49
|
+
*/
|
|
50
|
+
originMessageId?: string;
|
|
51
|
+
originCallRow?: number;
|
|
44
52
|
parentRunId?: string;
|
|
45
53
|
depth?: number;
|
|
46
54
|
loopId?: string;
|
|
@@ -124,7 +124,7 @@ export class SubagentRunRecorder {
|
|
|
124
124
|
const { conversationId, agentId, task, cwd, model, denyTools, dispatchedBy = "human" } = input;
|
|
125
125
|
// Generated HERE, not by `run()`, because the record has to exist first - see this file's header.
|
|
126
126
|
const id = input.id ?? newSubagentRunId();
|
|
127
|
-
const started = { id, conversationId, agentId, task, dispatchedBy, parentRunId: input.parentRunId, depth: input.depth ?? 0, loopId: input.loopId, role: input.role, status: "running", startedAt: new Date().toISOString(), sessionId: id };
|
|
127
|
+
const started = { id, conversationId, agentId, task, dispatchedBy, parentRunId: input.parentRunId, depth: input.depth ?? 0, loopId: input.loopId, role: input.role, originMessageId: input.originMessageId, originCallRow: input.originCallRow, status: "running", startedAt: new Date().toISOString(), sessionId: id };
|
|
128
128
|
await this.hooks.store.beginSubagentRun(started);
|
|
129
129
|
// The recorder's own copy, held separately from the store's. Mutating the object `beginSubagentRun`
|
|
130
130
|
// was handed would reach into the store's state behind its back, and every snapshot it hands out
|
|
@@ -19,7 +19,7 @@ export class ToolRunner {
|
|
|
19
19
|
child.stdout.on("data", (data) => { active.stdout = clipped(`${active.stdout}${data.toString("utf8")}`); });
|
|
20
20
|
child.stderr.on("data", log);
|
|
21
21
|
child.once("error", (error) => this.finish(id, "failed", error.message));
|
|
22
|
-
child.once("
|
|
22
|
+
child.once("close", (code) => {
|
|
23
23
|
if (active.run.status !== "running")
|
|
24
24
|
return;
|
|
25
25
|
if (code !== 0) {
|
|
@@ -34,7 +34,6 @@ export class ToolRunner {
|
|
|
34
34
|
this.finish(id, "succeeded");
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
|
-
active.run.logs = clipped(`${active.run.logs}${active.stdout}`);
|
|
38
37
|
this.finish(id, "failed", `脚本没有输出一个 JSON 对象:${error instanceof Error ? error.message : "invalid output"}`);
|
|
39
38
|
}
|
|
40
39
|
});
|
|
@@ -48,7 +47,8 @@ export class ToolRunner {
|
|
|
48
47
|
this.finish(id, "cancelled");
|
|
49
48
|
} return copy(active.run); }
|
|
50
49
|
finish(id, status, error) { const active = this.runs.get(id); if (!active || active.run.status !== "running")
|
|
51
|
-
return; clearTimeout(active.timer);
|
|
50
|
+
return; clearTimeout(active.timer); if (status !== "succeeded" && active.stdout)
|
|
51
|
+
active.run.logs = clipped(`${active.run.logs}${active.stdout}`); active.run.status = status; active.run.finishedAt = new Date().toISOString(); if (error)
|
|
52
52
|
active.run.error = error; if (!active.child.killed) {
|
|
53
53
|
try {
|
|
54
54
|
active.child.kill();
|
package/dist/server/types.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export type Message = {
|
|
|
98
98
|
at: number;
|
|
99
99
|
text: string;
|
|
100
100
|
}[];
|
|
101
|
+
thinkingActive?: boolean;
|
|
101
102
|
images?: MessageImage[];
|
|
102
103
|
attachments?: MessageAttachments;
|
|
103
104
|
agentIdentity?: {
|
|
@@ -203,6 +204,16 @@ export type ToolCallBlock = {
|
|
|
203
204
|
*/
|
|
204
205
|
at?: number;
|
|
205
206
|
thinkingAt?: number;
|
|
207
|
+
/** Index into the message's thinkingBlocks, captured when this call was first announced.
|
|
208
|
+
* null means outside reasoning; absence is an older record with offsets only. */
|
|
209
|
+
thinkingBlock?: number | null;
|
|
210
|
+
/** A background command keeps its dispatch position and settled output after the active task is removed. */
|
|
211
|
+
backgroundTask?: {
|
|
212
|
+
id: string;
|
|
213
|
+
status: "running" | "completed" | "failed";
|
|
214
|
+
output?: string;
|
|
215
|
+
exitCode?: number;
|
|
216
|
+
};
|
|
206
217
|
question?: ToolCallQuestion;
|
|
207
218
|
};
|
|
208
219
|
/**
|
|
@@ -565,6 +576,22 @@ export type SubagentRun = {
|
|
|
565
576
|
* Absent means `human`, which is what every record written before this field existed was.
|
|
566
577
|
*/
|
|
567
578
|
dispatchedBy?: "human" | "model";
|
|
579
|
+
/**
|
|
580
|
+
* WHERE THIS RUN WAS ASKED FOR, as a position in the dispatching assistant message: the message id, and
|
|
581
|
+
* the index of the tool call inside its `toolCalls`.
|
|
582
|
+
*
|
|
583
|
+
* This exists because a card anchored to nothing is anchored to a CLOCK. `startedAt` is when the child
|
|
584
|
+
* was spawned, and the message that dispatched it goes on streaming for seconds afterwards, so sorting
|
|
585
|
+
* cards against messages by time dropped every card below every message still being written - which is
|
|
586
|
+
* why sub-agent cards appeared at the bottom of the transcript instead of at the call that asked for
|
|
587
|
+
* them. Ordering by call position fixes that in the only direction a reader means: later call, later
|
|
588
|
+
* card, regardless of which child finishes first.
|
|
589
|
+
*
|
|
590
|
+
* ABSENT IS NORMAL AND MEANS "no anchor", never "row zero": a person pressing 派子智能体 has no tool
|
|
591
|
+
* call, and neither does a record written before this field existed. Both render as they always did.
|
|
592
|
+
*/
|
|
593
|
+
originMessageId?: string;
|
|
594
|
+
originCallRow?: number;
|
|
568
595
|
/** Authoritative dispatch ancestry, assigned by the server. */
|
|
569
596
|
parentRunId?: string;
|
|
570
597
|
depth: number;
|
|
@@ -628,6 +655,11 @@ export type BackgroundTaskSummary = {
|
|
|
628
655
|
status: "running" | "completed" | "failed";
|
|
629
656
|
startedAt: string;
|
|
630
657
|
resultBlockId: string;
|
|
658
|
+
/** The same call-position anchor a `SubagentRun` carries, and for the same reason: a background command
|
|
659
|
+
* asked for inside a turn belongs at that call, not wherever its output happened to land. Absent when
|
|
660
|
+
* the task was not started by a tool call. */
|
|
661
|
+
originMessageId?: string;
|
|
662
|
+
originCallRow?: number;
|
|
631
663
|
};
|
|
632
664
|
/**
|
|
633
665
|
* Orchestrated plans, optional for the same reason `todos` is: a `state.json` written before this
|
|
@@ -1075,7 +1107,16 @@ export type ServerVersionInfo = {
|
|
|
1075
1107
|
latestVersion?: string;
|
|
1076
1108
|
updateAvailable?: boolean;
|
|
1077
1109
|
};
|
|
1110
|
+
export type MountedToolSummary = {
|
|
1111
|
+
id: string;
|
|
1112
|
+
name: string;
|
|
1113
|
+
description?: string;
|
|
1114
|
+
enabled: boolean;
|
|
1115
|
+
};
|
|
1078
1116
|
export type ServerEvent = {
|
|
1117
|
+
type: "tools.updated";
|
|
1118
|
+
tools: MountedToolSummary[];
|
|
1119
|
+
} | {
|
|
1079
1120
|
type: "operation.result";
|
|
1080
1121
|
requestId: string;
|
|
1081
1122
|
ok: boolean;
|
|
@@ -1099,6 +1140,7 @@ export type ServerEvent = {
|
|
|
1099
1140
|
delta: string;
|
|
1100
1141
|
active: boolean;
|
|
1101
1142
|
at?: number;
|
|
1143
|
+
blockIndex?: number;
|
|
1102
1144
|
}
|
|
1103
1145
|
/**
|
|
1104
1146
|
* THE ANSWER TO `conversation.fork`, as ONE event with an `ok` discriminant rather than two events or a
|
package/dist/server/ws-server.js
CHANGED
|
@@ -44,13 +44,20 @@ export class WsBroadcaster {
|
|
|
44
44
|
enqueueStreaming(socket, event) {
|
|
45
45
|
let pending = this.pendingStreams.get(socket);
|
|
46
46
|
if (!pending) {
|
|
47
|
-
const events =
|
|
47
|
+
const events = [];
|
|
48
48
|
pending = { events, timer: setTimeout(() => this.flushStreaming(socket), 16) };
|
|
49
49
|
this.pendingStreams.set(socket, pending);
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Only adjacent fragments from the same stream can merge. A different kind,
|
|
52
|
+
// answer offset or lifecycle flag is an ordering boundary, even within 16ms.
|
|
53
|
+
const previous = pending.events.at(-1);
|
|
54
|
+
if (previous && previous.type === event.type && previous.conversationId === event.conversationId
|
|
55
|
+
&& previous.messageId === event.messageId && (event.type !== "message.thinking"
|
|
56
|
+
|| (previous.type === "message.thinking" && previous.at === event.at && previous.active === event.active && previous.blockIndex === event.blockIndex))) {
|
|
57
|
+
pending.events[pending.events.length - 1] = { ...event, delta: previous.delta + event.delta };
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
pending.events.push(event);
|
|
54
61
|
}
|
|
55
62
|
getSockets() {
|
|
56
63
|
return this.sockets;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dev:client": "vite --host 0.0.0.0",
|
|
29
29
|
"dev:server": "cross-env SUPER_PI_PORT=3081 tsx watch src/server/index.ts",
|
|
30
30
|
"dev:installed": "npm run build && cross-env SUPER_PI_PORT=3080 node bin/super-pi.js",
|
|
31
|
-
"build": "tsc -b && vite build",
|
|
31
|
+
"build": "tsc -b && vite build && node scripts/fresh-build.mjs",
|
|
32
32
|
"start": "node dist/server/index.js",
|
|
33
33
|
"restart:server": "node scripts/restart-server.mjs",
|
|
34
34
|
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"scripts/model-filter.test.mjs",
|
|
86
86
|
"scripts/active-memory.test.mjs",
|
|
87
87
|
"scripts/tool-runner.test.mjs",
|
|
88
|
+
"scripts/tool-runtime-api.test.mjs",
|
|
88
89
|
"scripts/extension-load.test.mjs"
|
|
89
90
|
],
|
|
90
91
|
"check:browser": [
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
"scripts/md-render-browser.test.mjs",
|
|
105
106
|
"scripts/right-sidebar-browser.test.mjs",
|
|
106
107
|
"scripts/sidebar-tools-browser.test.mjs",
|
|
108
|
+
"scripts/tool-runtime-browser.test.mjs",
|
|
107
109
|
"scripts/session-context-menu-browser.test.mjs",
|
|
108
110
|
"scripts/tool-call-blocks-browser.test.mjs",
|
|
109
111
|
"scripts/files-tab-browser.test.mjs",
|