@evandu/super-pi 0.1.129 → 0.1.131
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/agents/super-pi-documents/index.ts +92 -0
- package/agents/super-pi-knowledge/index.ts +32 -1
- package/agents/super-pi-task/core.ts +40 -0
- package/agents/super-pi-task/index.ts +15 -1
- package/agents/super-pi-web/index.ts +32 -0
- package/assets/fonts/LICENSE +92 -0
- package/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +5 -5
- package/dist/client/assets/{CodeMirrorEditor-BNVBZZSS.js → CodeMirrorEditor-BlQhH3vs.js} +2 -2
- package/dist/client/assets/{dist-DmAOqr0M.js → dist-B1_1Ui9E.js} +1 -1
- package/dist/client/assets/{dist-GkIV1LTX.js → dist-B448EWTM.js} +1 -1
- package/dist/client/assets/{dist-CiSGSVBZ.js → dist-B68Q8uXX.js} +1 -1
- package/dist/client/assets/{dist-DB5M5fZG.js → dist-BFlq-kiF.js} +1 -1
- package/dist/client/assets/{dist-BykVFExd.js → dist-BXVFgriA.js} +1 -1
- package/dist/client/assets/{dist-CZ9I2JCd.js → dist-BcE6tJvc.js} +1 -1
- package/dist/client/assets/{dist-BhJGK8-b.js → dist-ByIqsb5-.js} +1 -1
- package/dist/client/assets/{dist-DZnUERoW.js → dist-C6qSkkeN.js} +1 -1
- package/dist/client/assets/{dist-BKp4AWEJ.js → dist-C9AkgNbt.js} +1 -1
- package/dist/client/assets/{dist-CRzRB9k3.js → dist-COp25nhe.js} +1 -1
- package/dist/client/assets/{dist-CMnvChDX.js → dist-CpuexUlh.js} +1 -1
- package/dist/client/assets/{dist-ChszWGDv.js → dist-CtdlYgGc.js} +1 -1
- package/dist/client/assets/{dist-Dq4-QFAJ.js → dist-Cybsoapc.js} +1 -1
- package/dist/client/assets/{dist-Ur37340K.js → dist-D6PoSxR5.js} +1 -1
- package/dist/client/assets/{dist-C90MPTnk.js → dist-D7bj81KQ.js} +1 -1
- package/dist/client/assets/{dist-BBW2SE_R.js → dist-DP2ygAFF.js} +1 -1
- package/dist/client/assets/{dist-B61RSa-K.js → dist-DXNDtyHP.js} +1 -1
- package/dist/client/assets/{dist-CWYRjnj-.js → dist-DxoWS5MH.js} +1 -1
- package/dist/client/assets/{dist-Cxerc7Bb.js → dist-Fhkmvjdx.js} +1 -1
- package/dist/client/assets/{dist-BDygQgZD.js → dist-hfpd-aWa.js} +1 -1
- package/dist/client/assets/{dist-B2nPVdKh.js → dist-lQWWWxh9.js} +1 -1
- package/dist/client/assets/{dist-HdeIBl2J.js → dist-r5Af94tH.js} +1 -1
- package/dist/client/assets/{index-D_c-CYAr.js → index-BApJ3Zbq.js} +3 -3
- package/dist/client/index.html +1 -1
- package/dist/server/active-memory.d.ts +12 -0
- package/dist/server/active-memory.js +25 -1
- package/dist/server/application/conversation-service.js +5 -0
- package/dist/server/application/document-service.d.ts +68 -0
- package/dist/server/application/document-service.js +120 -0
- package/dist/server/application/run-service.js +49 -7
- package/dist/server/documents.d.ts +152 -0
- package/dist/server/documents.js +998 -0
- package/dist/server/file-changes.js +6 -4
- package/dist/server/files.d.ts +32 -0
- package/dist/server/files.js +28 -0
- package/dist/server/http-server.js +113 -2
- package/dist/server/index.js +13 -1
- package/dist/server/inline-thinking.d.ts +90 -0
- package/dist/server/inline-thinking.js +181 -0
- package/dist/server/knowledge.d.ts +24 -14
- package/dist/server/knowledge.js +47 -32
- package/dist/server/pi-session-pool.js +2 -1
- package/dist/server/pi-session.d.ts +20 -6
- package/dist/server/pi-session.js +43 -11
- package/dist/server/presentations/schema.d.ts +120 -0
- package/dist/server/presentations/schema.js +128 -0
- package/dist/server/runtime.d.ts +46 -0
- package/dist/server/runtime.js +150 -16
- package/dist/server/server-context.d.ts +3 -0
- package/dist/server/subagents.js +33 -6
- package/dist/server/web.d.ts +24 -0
- package/dist/server/web.js +180 -0
- package/package.json +33 -5
- package/skills/presentation/SKILL.md +30 -0
- package/skills/presentation/references/data.md +10 -0
- package/skills/presentation/references/quality.md +12 -0
- package/skills/presentation/references/story.md +12 -0
- package/skills/presentation/references/templates-and-editing.md +11 -0
- package/skills/presentation/references/visual-system.md +10 -0
package/dist/server/runtime.d.ts
CHANGED
|
@@ -512,6 +512,11 @@ export declare const catalogKey: (selection?: ModelSelection) => string;
|
|
|
512
512
|
* warning on a path that is genuinely optional rather than a startup crash on a build that skipped it.
|
|
513
513
|
*/
|
|
514
514
|
export declare function taskExtensionPath(from?: string): string;
|
|
515
|
+
/**
|
|
516
|
+
* The document extension's file, resolved the same way the task extension's is: two `..` segments
|
|
517
|
+
* from `import.meta.dirname` reach the repository root both after `tsc -b` and under `tsx watch`.
|
|
518
|
+
*/
|
|
519
|
+
export declare function documentExtensionPath(from?: string): string;
|
|
515
520
|
export declare class PiRpcRuntime implements AgentRuntime {
|
|
516
521
|
private readonly sessionDir;
|
|
517
522
|
/**
|
|
@@ -635,6 +640,8 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
635
640
|
private modelEpoch;
|
|
636
641
|
/** Latches the "no extension on disk" warning, so it is said once rather than once per turn. */
|
|
637
642
|
private warnedMissingExtension;
|
|
643
|
+
/** Latches the "the document extension is missing" warning, for the same reason. */
|
|
644
|
+
private warnedMissingDocumentExtension;
|
|
638
645
|
/** Latches the "Pi's SDK is not installed" warning, for the same reason. */
|
|
639
646
|
private warnedMissingSdk;
|
|
640
647
|
/**
|
|
@@ -701,6 +708,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
701
708
|
workflowEndpoint?: string;
|
|
702
709
|
questionEndpoint?: string;
|
|
703
710
|
toolsEndpoint?: string;
|
|
711
|
+
documentsEndpoint?: string;
|
|
704
712
|
uploadsDir?: string;
|
|
705
713
|
sessionFactory?: PiSessionFactory;
|
|
706
714
|
sessionMode?: "sdk" | "oneshot";
|
|
@@ -784,6 +792,18 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
784
792
|
* knowing and worth not repeating on every keystroke.
|
|
785
793
|
*/
|
|
786
794
|
private taskExtensionArgs;
|
|
795
|
+
/**
|
|
796
|
+
* The document extension's spawn argument.
|
|
797
|
+
*
|
|
798
|
+
* IT IS MOUNTED UNCONDITIONALLY, unlike the knowledge extension. The knowledge extension is only
|
|
799
|
+
* worth mounting when the workspace HAS documents, because its tool returns nothing otherwise; the
|
|
800
|
+
* document tools are about the files in the workspace, and a workspace with no PDF in it is exactly
|
|
801
|
+
* the workspace where an agent might be asked to WRITE one.
|
|
802
|
+
*
|
|
803
|
+
* The endpoint is the gate: without it the extension returns without registering anything, so a
|
|
804
|
+
* runtime with no document endpoint mounts nothing rather than a tool whose every call fails.
|
|
805
|
+
*/
|
|
806
|
+
private documentExtensionArgs;
|
|
787
807
|
/**
|
|
788
808
|
* The five values the extension reads out of its own environment. See `task-tool.ts` for why they are
|
|
789
809
|
* written out on both sides rather than imported.
|
|
@@ -906,7 +926,33 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
906
926
|
* because `toolFlags` treats an explicit false as outranking a host extra.
|
|
907
927
|
*/
|
|
908
928
|
private extensionToolNames;
|
|
929
|
+
/**
|
|
930
|
+
* A TURN, WITH THE REASONING TAKEN OUT OF THE ANSWER.
|
|
931
|
+
*
|
|
932
|
+
* This is the ONE place every turn passes through, whichever path it runs on - the persistent
|
|
933
|
+
* session or a one-shot process - so it is where the model's own text is read once and read
|
|
934
|
+
* properly.
|
|
935
|
+
*
|
|
936
|
+
* WHAT IT DOES. A model that has no reasoning channel answers in one stream of text and marks its
|
|
937
|
+
* thinking up inside it with `<think>…</think>`. Pi's adapter passes that through as ordinary
|
|
938
|
+
* text, because Pi only knows the four reasoning FIELDS a provider can use and a tag is not one
|
|
939
|
+
* of them. Left alone, the reasoning would be drawn as part of the answer, unlabelled, with the
|
|
940
|
+
* tags in it.
|
|
941
|
+
*
|
|
942
|
+
* WHY IT IS HERE AND NOT IN THE USER'S `models.json`. The alternative is to tell every reader to
|
|
943
|
+
* go and set a vendor-specific switch in a file Super Pi owns but Pi reads. A user who never
|
|
944
|
+
* hears of it sees a broken transcript and has no way to find out why. Super Pi reads the tags
|
|
945
|
+
* itself, for every provider, so the transcript reads the same way whichever endpoint is behind
|
|
946
|
+
* it - and the reasoning goes where the reasoning goes: into a thinking block, over the same
|
|
947
|
+
* channel Pi's own reasoning events use, so the fold, the live indicator and the persisted
|
|
948
|
+
* transcript all work without knowing where the reasoning came from.
|
|
949
|
+
*/
|
|
909
950
|
answer(input: AnswerInput): AsyncIterable<string>;
|
|
951
|
+
/**
|
|
952
|
+
* THE TURN ITSELF, on whichever path it runs. See `answer`, which is the seam the reasoning is
|
|
953
|
+
* separated at.
|
|
954
|
+
*/
|
|
955
|
+
private answerTurn;
|
|
910
956
|
/**
|
|
911
957
|
* The one-shot path: a fresh child process for a single turn.
|
|
912
958
|
*
|
package/dist/server/runtime.js
CHANGED
|
@@ -2,7 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { RpcChannel } from "./rpc-channel.js";
|
|
3
3
|
export { RpcChannel } from "./rpc-channel.js";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { KNOWLEDGE_TOOL_NAME } from "./knowledge.js";
|
|
5
|
+
import { KNOWLEDGE_IMPORT_TOOL_NAME, KNOWLEDGE_TOOL_NAME } from "./knowledge.js";
|
|
6
|
+
import { DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME } from "./documents.js";
|
|
7
|
+
import { WEB_FETCH_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from "./web.js";
|
|
6
8
|
import { existsSync } from "node:fs";
|
|
7
9
|
import { mkdir, stat } from "node:fs/promises";
|
|
8
10
|
import { forkSessionFile, truncateSessionFile } from "./session-repository.js";
|
|
@@ -22,6 +24,7 @@ import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_END
|
|
|
22
24
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
23
25
|
import { loadConversationImages } from "./upload.js";
|
|
24
26
|
import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
|
|
27
|
+
import { InlineThinkingSplitter, stripInlineThinking } from "./inline-thinking.js";
|
|
25
28
|
import { toolTargetPath } from "./file-changes.js";
|
|
26
29
|
const traceLatency = (conversationId, phase, startedAt, extra = "") => {
|
|
27
30
|
if (process.env.SUPER_PI_TRACE_LATENCY !== "1")
|
|
@@ -238,6 +241,13 @@ export const catalogKey = (selection) => (selection?.provider && selection.model
|
|
|
238
241
|
export function taskExtensionPath(from = import.meta.dirname) {
|
|
239
242
|
return resolve(from, "../../agents/super-pi-task/index.ts");
|
|
240
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* The document extension's file, resolved the same way the task extension's is: two `..` segments
|
|
246
|
+
* from `import.meta.dirname` reach the repository root both after `tsc -b` and under `tsx watch`.
|
|
247
|
+
*/
|
|
248
|
+
export function documentExtensionPath(from = import.meta.dirname) {
|
|
249
|
+
return resolve(from, "../../agents/super-pi-documents/index.ts");
|
|
250
|
+
}
|
|
241
251
|
/** Bridges a Pi session to Super Pi through Pi's documented JSONL RPC protocol. */
|
|
242
252
|
/**
|
|
243
253
|
* The tool's name as far as the stream has revealed it so far, or an empty string while the model is
|
|
@@ -356,6 +366,8 @@ export class PiRpcRuntime {
|
|
|
356
366
|
modelEpoch = 0;
|
|
357
367
|
/** Latches the "no extension on disk" warning, so it is said once rather than once per turn. */
|
|
358
368
|
warnedMissingExtension = false;
|
|
369
|
+
/** Latches the "the document extension is missing" warning, for the same reason. */
|
|
370
|
+
warnedMissingDocumentExtension = false;
|
|
359
371
|
/** Latches the "Pi's SDK is not installed" warning, for the same reason. */
|
|
360
372
|
warnedMissingSdk = false;
|
|
361
373
|
/**
|
|
@@ -541,6 +553,30 @@ export class PiRpcRuntime {
|
|
|
541
553
|
}
|
|
542
554
|
return ["--extension", path];
|
|
543
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* The document extension's spawn argument.
|
|
558
|
+
*
|
|
559
|
+
* IT IS MOUNTED UNCONDITIONALLY, unlike the knowledge extension. The knowledge extension is only
|
|
560
|
+
* worth mounting when the workspace HAS documents, because its tool returns nothing otherwise; the
|
|
561
|
+
* document tools are about the files in the workspace, and a workspace with no PDF in it is exactly
|
|
562
|
+
* the workspace where an agent might be asked to WRITE one.
|
|
563
|
+
*
|
|
564
|
+
* The endpoint is the gate: without it the extension returns without registering anything, so a
|
|
565
|
+
* runtime with no document endpoint mounts nothing rather than a tool whose every call fails.
|
|
566
|
+
*/
|
|
567
|
+
documentExtensionArgs() {
|
|
568
|
+
if (!this.options.documentsEndpoint)
|
|
569
|
+
return [];
|
|
570
|
+
const path = documentExtensionPath();
|
|
571
|
+
if (!existsSync(path)) {
|
|
572
|
+
if (!this.warnedMissingDocumentExtension) {
|
|
573
|
+
this.warnedMissingDocumentExtension = true;
|
|
574
|
+
console.warn(`PiRpcRuntime: ${path} does not exist, so the primary agent cannot read or write PDF, Word and Excel files.`);
|
|
575
|
+
}
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
return ["--extension", path];
|
|
579
|
+
}
|
|
544
580
|
/**
|
|
545
581
|
* The five values the extension reads out of its own environment. See `task-tool.ts` for why they are
|
|
546
582
|
* written out on both sides rather than imported.
|
|
@@ -579,6 +615,14 @@ export class PiRpcRuntime {
|
|
|
579
615
|
[WORKFLOW_ENDPOINT_ENV]: this.options.workflowEndpoint ?? "",
|
|
580
616
|
SUPER_PI_QUESTION_ENDPOINT: this.options.questionEndpoint ?? "",
|
|
581
617
|
SUPER_PI_TOOLS_ENDPOINT: this.options.toolsEndpoint ?? "",
|
|
618
|
+
// The document tools' endpoint and the conversation they act for. The CREDENTIAL is not here:
|
|
619
|
+
// it is the session's own capability, which the session pool adds to this environment when the
|
|
620
|
+
// session is born, and the document extension reads that same value. Minting a second
|
|
621
|
+
// credential for the same session would be a second thing to rotate and a second thing to
|
|
622
|
+
// forget to release.
|
|
623
|
+
SUPER_PI_DOCUMENTS_ENDPOINT: this.options.documentsEndpoint ?? "",
|
|
624
|
+
SUPER_PI_DOCUMENTS_CONVERSATION: conversationId,
|
|
625
|
+
SUPER_PI_WEB_ENDPOINT: process.env.SUPER_PI_WEB_ENDPOINT ?? "",
|
|
582
626
|
};
|
|
583
627
|
}
|
|
584
628
|
/**
|
|
@@ -590,8 +634,7 @@ export class PiRpcRuntime {
|
|
|
590
634
|
* here. A session lives for many turns, so its credential is minted when the session is created (see
|
|
591
635
|
* `PiSessionPool.acquire`) and the server separately requires a running turn for it to be usable.
|
|
592
636
|
*/
|
|
593
|
-
async oneShotExtensionEnv(conversationId) {
|
|
594
|
-
const capability = mintCapability(conversationId);
|
|
637
|
+
async oneShotExtensionEnv(conversationId, capability) {
|
|
595
638
|
return {
|
|
596
639
|
...(await this.taskExtensionEnv(conversationId)),
|
|
597
640
|
[TASK_CAPABILITY_ENV]: capability,
|
|
@@ -1016,10 +1059,65 @@ export class PiRpcRuntime {
|
|
|
1016
1059
|
if (this.memory)
|
|
1017
1060
|
names.push("memory", "skill_manage", "session_search");
|
|
1018
1061
|
if (this.knowledge)
|
|
1019
|
-
names.push(KNOWLEDGE_TOOL_NAME);
|
|
1062
|
+
names.push(KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME);
|
|
1063
|
+
if (process.env.SUPER_PI_WEB_ENDPOINT)
|
|
1064
|
+
names.push(WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME);
|
|
1065
|
+
// The document tools are named whenever the endpoint is configured, because that is what decides
|
|
1066
|
+
// whether the extension registers them. An agent that declares `tools: {"*": false, read: true}`
|
|
1067
|
+
// would otherwise get a loaded-but-undeclared tool - broken with no error to explain it.
|
|
1068
|
+
if (this.options.documentsEndpoint)
|
|
1069
|
+
names.push(DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME);
|
|
1020
1070
|
return names;
|
|
1021
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* A TURN, WITH THE REASONING TAKEN OUT OF THE ANSWER.
|
|
1074
|
+
*
|
|
1075
|
+
* This is the ONE place every turn passes through, whichever path it runs on - the persistent
|
|
1076
|
+
* session or a one-shot process - so it is where the model's own text is read once and read
|
|
1077
|
+
* properly.
|
|
1078
|
+
*
|
|
1079
|
+
* WHAT IT DOES. A model that has no reasoning channel answers in one stream of text and marks its
|
|
1080
|
+
* thinking up inside it with `<think>…</think>`. Pi's adapter passes that through as ordinary
|
|
1081
|
+
* text, because Pi only knows the four reasoning FIELDS a provider can use and a tag is not one
|
|
1082
|
+
* of them. Left alone, the reasoning would be drawn as part of the answer, unlabelled, with the
|
|
1083
|
+
* tags in it.
|
|
1084
|
+
*
|
|
1085
|
+
* WHY IT IS HERE AND NOT IN THE USER'S `models.json`. The alternative is to tell every reader to
|
|
1086
|
+
* go and set a vendor-specific switch in a file Super Pi owns but Pi reads. A user who never
|
|
1087
|
+
* hears of it sees a broken transcript and has no way to find out why. Super Pi reads the tags
|
|
1088
|
+
* itself, for every provider, so the transcript reads the same way whichever endpoint is behind
|
|
1089
|
+
* it - and the reasoning goes where the reasoning goes: into a thinking block, over the same
|
|
1090
|
+
* channel Pi's own reasoning events use, so the fold, the live indicator and the persisted
|
|
1091
|
+
* transcript all work without knowing where the reasoning came from.
|
|
1092
|
+
*/
|
|
1022
1093
|
async *answer(input) {
|
|
1094
|
+
const splitter = new InlineThinkingSplitter();
|
|
1095
|
+
for await (const chunk of this.answerTurn(input)) {
|
|
1096
|
+
for (const segment of splitter.push(chunk)) {
|
|
1097
|
+
if (segment.kind === "text")
|
|
1098
|
+
yield segment.text;
|
|
1099
|
+
else if (segment.kind === "thinking")
|
|
1100
|
+
input.onThinking?.(segment.text);
|
|
1101
|
+
else
|
|
1102
|
+
input.onThinkingEnd?.();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// THE END OF THE TURN. A model that was still thinking when the turn ended leaves an unclosed
|
|
1106
|
+
// region behind, and it belongs in the block rather than in the answer.
|
|
1107
|
+
for (const segment of splitter.flush()) {
|
|
1108
|
+
if (segment.kind === "text")
|
|
1109
|
+
yield segment.text;
|
|
1110
|
+
else if (segment.kind === "thinking")
|
|
1111
|
+
input.onThinking?.(segment.text);
|
|
1112
|
+
else
|
|
1113
|
+
input.onThinkingEnd?.();
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* THE TURN ITSELF, on whichever path it runs. See `answer`, which is the seam the reasoning is
|
|
1118
|
+
* separated at.
|
|
1119
|
+
*/
|
|
1120
|
+
async *answerTurn(input) {
|
|
1023
1121
|
// The one-shot paths keep their own child process, and they are NOT the same thing as a conversation
|
|
1024
1122
|
// turn: a memory review and a knowledge reranker have their own ids, their own tool sets and their own
|
|
1025
1123
|
// lifetimes, and folding them into a conversation's session would give them that conversation's
|
|
@@ -1111,15 +1209,30 @@ export class PiRpcRuntime {
|
|
|
1111
1209
|
const memoryToken = memoryEnabled ? this.memory.issue(validationId, { review: memoryReview }) : undefined;
|
|
1112
1210
|
const memoryExtension = resolve(import.meta.dirname, "../../agents/super-pi-memory/index.ts");
|
|
1113
1211
|
const memoryArgs = memoryEnabled ? ["--extension", memoryExtension] : [];
|
|
1114
|
-
// Mounted
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
|
|
1212
|
+
// Mounted on EVERY primary turn, empty base or not. The old condition (hasDocuments) saved a
|
|
1213
|
+
// catalogue round trip and a tool slot when there was nothing to find, but it also made the one
|
|
1214
|
+
// workspace where a user most plausibly says 「把这些文件加入知识库」 the one workspace whose
|
|
1215
|
+
// agent had no import tool. knowledge_search on an empty base answers no hits and costs nothing;
|
|
1216
|
+
// knowledge_import has to exist before the first document does. Review turns never mount it: a
|
|
1217
|
+
// background reflection has no business reading or writing documents, and its `--tools` line is a
|
|
1218
|
+
// closed list. A reranker never mounts it either - it must not be able to call what it ranks.
|
|
1219
|
+
const knowledgeEnabled = !memoryReview && !knowledgeRerank && !!this.knowledge && this.knowledge.enabled();
|
|
1120
1220
|
const knowledgeToken = knowledgeEnabled ? this.knowledge.issue(conversation.id) : undefined;
|
|
1121
1221
|
const knowledgeExtension = resolve(import.meta.dirname, "../../agents/super-pi-knowledge/index.ts");
|
|
1122
1222
|
const knowledgeArgs = knowledgeEnabled ? ["--extension", knowledgeExtension] : [];
|
|
1223
|
+
const webExtension = resolve(import.meta.dirname, "../../agents/super-pi-web/index.ts");
|
|
1224
|
+
const webEnabled = !memoryReview && !knowledgeRerank && existsSync(webExtension) && !!process.env.SUPER_PI_WEB_ENDPOINT;
|
|
1225
|
+
const webArgs = webEnabled ? ["--extension", webExtension] : [];
|
|
1226
|
+
// The document tools are mounted for every ordinary turn, and NOT for a review or a rerank: a
|
|
1227
|
+
// background reflection has no business reading a PDF it was not asked about, and a reranker
|
|
1228
|
+
// must not be able to reach the tools it is ranking.
|
|
1229
|
+
const documentsEnabled = !memoryReview && !knowledgeRerank && this.documentExtensionArgs().length > 0;
|
|
1230
|
+
const documentArgs = documentsEnabled ? this.documentExtensionArgs() : [];
|
|
1231
|
+
// THE ONE-SHOT CHILD'S CREDENTIAL, MINTED ONCE. The server holds ONE capability per
|
|
1232
|
+
// conversation, so minting a second one for the same conversation would replace the first and
|
|
1233
|
+
// leave the task dispatch holding a token the server has already forgotten. Both the task
|
|
1234
|
+
// extension and the document extension therefore share this one value.
|
|
1235
|
+
const oneShotCapability = (extensionArgs.length || documentsEnabled || webEnabled) ? mintCapability(conversation.id) : undefined;
|
|
1123
1236
|
// BOTH extension tools, or neither. Naming one while the other goes unmentioned would be a lie in a
|
|
1124
1237
|
// different direction: Pi filters its registry through `--tools`, so an agent that declares
|
|
1125
1238
|
// `tools: {"*": false, read: true}` gets whichever of the two is on this line and silently not the
|
|
@@ -1132,13 +1245,13 @@ export class PiRpcRuntime {
|
|
|
1132
1245
|
// allowlist and naming a tool Pi has not registered would change the spawn line for every other
|
|
1133
1246
|
// mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
|
|
1134
1247
|
// tool call anyway the reranker times out and falls back to the lexical order.
|
|
1135
|
-
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, "question", "tool_register"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME] : [])] });
|
|
1248
|
+
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, "question", "tool_register"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : [])] });
|
|
1136
1249
|
// Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
|
|
1137
1250
|
// "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
|
|
1138
1251
|
if (!this.pi.found)
|
|
1139
1252
|
throw new Error(this.pi.reason);
|
|
1140
1253
|
// npm exposes global CLIs as .cmd shims on Windows; Node requires a shell to launch those shims.
|
|
1141
|
-
const child = spawn(shellQuote(this.pi.command), ["--mode", "rpc", "--session-dir", this.sessionDir, "--session-id", conversation.id, ...extensionArgs, ...memoryArgs, ...knowledgeArgs, ...agentArgs], {
|
|
1254
|
+
const child = spawn(shellQuote(this.pi.command), ["--mode", "rpc", "--session-dir", this.sessionDir, "--session-id", conversation.id, ...extensionArgs, ...memoryArgs, ...knowledgeArgs, ...documentArgs, ...webArgs, ...agentArgs], {
|
|
1142
1255
|
cwd: childCwd,
|
|
1143
1256
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1144
1257
|
shell: process.platform === "win32",
|
|
@@ -1146,13 +1259,26 @@ export class PiRpcRuntime {
|
|
|
1146
1259
|
// every turn would silently replace the child's environment with whatever THIS process has at
|
|
1147
1260
|
// spawn time. Inheriting is what an omitted `env` already does, so the two paths differ in nothing
|
|
1148
1261
|
// but the four documented additions.
|
|
1149
|
-
...((extensionArgs.length || memoryEnabled || knowledgeEnabled) ? { env: { ...process.env, ...(
|
|
1262
|
+
...((extensionArgs.length || memoryEnabled || knowledgeEnabled || documentsEnabled || webEnabled) ? { env: { ...process.env, ...(oneShotCapability ? await this.oneShotExtensionEnv(conversation.id, oneShotCapability) : {}), ...(memoryEnabled ? {
|
|
1150
1263
|
SUPER_PI_MEMORY_ENDPOINT: process.env.SUPER_PI_MEMORY_ENDPOINT,
|
|
1151
1264
|
SUPER_PI_MEMORY_TOKEN: memoryToken,
|
|
1152
1265
|
SUPER_PI_MEMORY_REVIEW: memoryReview ? "1" : "0",
|
|
1153
1266
|
} : {}), ...(knowledgeEnabled ? {
|
|
1154
1267
|
SUPER_PI_KNOWLEDGE_ENDPOINT: process.env.SUPER_PI_KNOWLEDGE_ENDPOINT,
|
|
1155
1268
|
SUPER_PI_KNOWLEDGE_TOKEN: knowledgeToken,
|
|
1269
|
+
} : {}), ...(documentsEnabled ? {
|
|
1270
|
+
SUPER_PI_DOCUMENTS_ENDPOINT: this.options.documentsEndpoint ?? "",
|
|
1271
|
+
// The document tools carry the SESSION's capability, the same credential the task dispatch
|
|
1272
|
+
// uses: it proves which conversation is asking, and the server checks that the turn is
|
|
1273
|
+
// running. The conversation id travels with it, because the workspace is derived from the
|
|
1274
|
+
// conversation and never from the request.
|
|
1275
|
+
//
|
|
1276
|
+
// IT IS THE SAME VALUE THE TASK EXTENSION GETS. A one-shot child has no session to borrow a
|
|
1277
|
+
// credential from, so the token is minted for the turn - and minted ONCE, because the server
|
|
1278
|
+
// holds one capability per conversation and a second mint would replace the first in the
|
|
1279
|
+
// map, leaving the task dispatch holding a token the server had already forgotten.
|
|
1280
|
+
SUPER_PI_DOCUMENTS_TOKEN: oneShotCapability ?? "",
|
|
1281
|
+
SUPER_PI_DOCUMENTS_CONVERSATION: conversation.id,
|
|
1156
1282
|
} : {}) } } : {}),
|
|
1157
1283
|
});
|
|
1158
1284
|
const channel = new RpcChannel(child);
|
|
@@ -2070,7 +2196,11 @@ export class PiRpcRuntime {
|
|
|
2070
2196
|
// A model asked for "only the title" still wraps it now and then. Stripping the wrappers is not
|
|
2071
2197
|
// cosmetic: without it a leading quote or a `Title:` prefix becomes the first characters of every
|
|
2072
2198
|
// title in the sidebar.
|
|
2073
|
-
|
|
2199
|
+
//
|
|
2200
|
+
// AND THE MODEL'S OWN REASONING IS STRIPPED FIRST, because a model that thinks inside its
|
|
2201
|
+
// answer puts the thinking in front of the title: the title would then start with `<think>`,
|
|
2202
|
+
// and the sidebar would show the markup of a reasoning trace where a label belongs.
|
|
2203
|
+
const title = stripInlineThinking(text)
|
|
2074
2204
|
.replace(/^\s*(?:title|标题)\s*[::]\s*/iu, "")
|
|
2075
2205
|
.replace(/^[`"'“”‘’「『]+/u, "")
|
|
2076
2206
|
.replace(/[`"'“”‘’」』]+$/u, "")
|
|
@@ -2125,9 +2255,13 @@ export class PiRpcRuntime {
|
|
|
2125
2255
|
}
|
|
2126
2256
|
if (!settled)
|
|
2127
2257
|
throw new Error(`模型在 ${Math.round(input.timeoutMs / 1000)} 秒内没有返回`);
|
|
2128
|
-
|
|
2258
|
+
// THE MODEL'S OWN REASONING IS NOT THE TOOL'S OUTPUT. A model that thinks inside its answer
|
|
2259
|
+
// would otherwise hand the caller a `<think>`-prefixed string, and the caller - a tool, a
|
|
2260
|
+
// title, a card - would store the reasoning trace as its result.
|
|
2261
|
+
const answer = stripInlineThinking(text);
|
|
2262
|
+
if (!answer.trim())
|
|
2129
2263
|
throw new Error("模型没有返回任何内容");
|
|
2130
|
-
return
|
|
2264
|
+
return answer;
|
|
2131
2265
|
}
|
|
2132
2266
|
finally {
|
|
2133
2267
|
if (child.exitCode === null && child.pid !== undefined)
|
|
@@ -39,6 +39,9 @@ export interface EventBroadcaster {
|
|
|
39
39
|
export interface ServerContext {
|
|
40
40
|
memory?: import("./active-memory.js").ActiveMemory;
|
|
41
41
|
knowledge?: import("./knowledge.js").KnowledgeBase;
|
|
42
|
+
web?: import("./web.js").WebService;
|
|
43
|
+
/** The agent-facing document tools. Optional so the server can start without them. */
|
|
44
|
+
documentService?: import("./application/document-service.js").DocumentService;
|
|
42
45
|
paths: ServerPaths;
|
|
43
46
|
store: Store;
|
|
44
47
|
runtime: AgentRuntime;
|
package/dist/server/subagents.js
CHANGED
|
@@ -6,6 +6,7 @@ import { agentSpawnArgs, readAgents } from "./agents.js";
|
|
|
6
6
|
import { resolvePiCommand, shellQuote } from "./pi-command.js";
|
|
7
7
|
import { buildExtensionUiResponse, describeExtensionUiRequest, killTree, } from "./runtime.js";
|
|
8
8
|
import { RpcChannel } from "./rpc-channel.js";
|
|
9
|
+
import { InlineThinkingSplitter, stripInlineThinking } from "./inline-thinking.js";
|
|
9
10
|
import { describeToolCall, TOOL_DETAIL_CAP } from "./tool-calls.js";
|
|
10
11
|
/**
|
|
11
12
|
* The tool-call derivation now lives in `tool-calls.js`, because the primary agent's runtime needs the
|
|
@@ -284,6 +285,15 @@ export class SubagentSupervisor {
|
|
|
284
285
|
let current = "";
|
|
285
286
|
/** The last assistant message's text, which is the run's output. */
|
|
286
287
|
let finalText = "";
|
|
288
|
+
/**
|
|
289
|
+
* THE CHILD'S OWN REASONING, taken out of its answer.
|
|
290
|
+
*
|
|
291
|
+
* A model that has no reasoning channel writes its thinking into the answer, marked up with
|
|
292
|
+
* `<think>…</think>`. This supervisor's contract is `finalText` - the child's OUTPUT - and the
|
|
293
|
+
* reasoning is not part of it, so the tags and what they wrap never reach the card. The splitter
|
|
294
|
+
* is per child, because what it holds back belongs to one child's stream and to nothing else.
|
|
295
|
+
*/
|
|
296
|
+
const inlineThinking = new InlineThinkingSplitter();
|
|
287
297
|
const usage = { ...NO_USAGE };
|
|
288
298
|
/** Set rather than thrown so a child's own failure is reported AFTER its output has been drained. */
|
|
289
299
|
let failed;
|
|
@@ -420,10 +430,18 @@ export class SubagentSupervisor {
|
|
|
420
430
|
// three messages would report all three concatenated as one answer.
|
|
421
431
|
if (event?.type === "text_delta") {
|
|
422
432
|
const delta = event.delta ?? "";
|
|
423
|
-
|
|
424
|
-
|
|
433
|
+
// THE REASONING COMES OUT FIRST. What the card shows is the child's answer, and a model
|
|
434
|
+
// that thinks inside its answer would otherwise hand it a string that opens with
|
|
435
|
+
// `<think>` - the markup of a reasoning trace where the child's result belongs.
|
|
436
|
+
const answer = inlineThinking.push(delta)
|
|
437
|
+
.filter((segment) => segment.kind === "text")
|
|
438
|
+
.map((segment) => segment.text)
|
|
439
|
+
.join("");
|
|
440
|
+
streamed += answer;
|
|
441
|
+
current += answer;
|
|
425
442
|
enter("turn");
|
|
426
|
-
|
|
443
|
+
if (answer)
|
|
444
|
+
onTextDelta?.(answer);
|
|
427
445
|
}
|
|
428
446
|
else if (event?.type === "start") {
|
|
429
447
|
current = "";
|
|
@@ -432,16 +450,18 @@ export class SubagentSupervisor {
|
|
|
432
450
|
else if (event?.type === "done") {
|
|
433
451
|
// `done` carries the AUTHORITATIVE message, so its text wins over what the deltas added up
|
|
434
452
|
// to; the accumulated text stays as the fallback for a child that streams without a `done`.
|
|
435
|
-
|
|
453
|
+
// The authoritative text is stripped the same way the deltas were: a child whose answer
|
|
454
|
+
// ends in a reasoning region must not turn that region into its result.
|
|
455
|
+
current = stripInlineThinking(assistantText(event.message)) || current;
|
|
436
456
|
finalText = current;
|
|
437
457
|
addUsage(usage, record.usage);
|
|
438
458
|
enter("turn");
|
|
439
459
|
const reason = event.message?.stopReason ?? event.reason;
|
|
440
460
|
if (reason === "error" || reason === "aborted")
|
|
441
|
-
failed ??= `sub-agent ${agentId} (${id}) stopped with reason ${reason}: ${assistantText(event.message) || "no output"}`;
|
|
461
|
+
failed ??= `sub-agent ${agentId} (${id}) stopped with reason ${reason}: ${stripInlineThinking(assistantText(event.message)) || "no output"}`;
|
|
442
462
|
}
|
|
443
463
|
else if (event?.type === "error") {
|
|
444
|
-
failed ??= `sub-agent ${agentId} (${id}) reported ${event.reason ?? "an error"}: ${assistantText(event.message) || "no detail from Pi"}`;
|
|
464
|
+
failed ??= `sub-agent ${agentId} (${id}) reported ${event.reason ?? "an error"}: ${stripInlineThinking(assistantText(event.message)) || "no detail from Pi"}`;
|
|
445
465
|
}
|
|
446
466
|
else if (event?.type === "thinking_delta") {
|
|
447
467
|
// CONSUMED, not discarded. A reasoning model spends the first seconds of every turn here,
|
|
@@ -524,6 +544,13 @@ export class SubagentSupervisor {
|
|
|
524
544
|
throw new Error(failed);
|
|
525
545
|
// No `done` at all is a child that streamed and vanished; the deltas are then the best answer there
|
|
526
546
|
// is, and returning them beats returning "" for work that demonstrably happened.
|
|
547
|
+
//
|
|
548
|
+
// THE SPLITTER IS RELEASED FIRST. It may still be holding back the beginning of a tag, and that
|
|
549
|
+
// text belongs to the child: a run that ended mid-word must not lose the word.
|
|
550
|
+
for (const segment of inlineThinking.flush()) {
|
|
551
|
+
if (segment.kind === "text")
|
|
552
|
+
streamed += segment.text;
|
|
553
|
+
}
|
|
527
554
|
return { id, sessionId: id, finalText: finalText || streamed, usage };
|
|
528
555
|
}
|
|
529
556
|
finally {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
2
|
+
export declare const WEB_FETCH_TOOL_NAME = "web_fetch";
|
|
3
|
+
export declare const MAX_WEB_REQUEST_BYTES: number;
|
|
4
|
+
export type WebSearchResult = {
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
snippet: string;
|
|
8
|
+
};
|
|
9
|
+
export type WebFetchResult = {
|
|
10
|
+
title: string;
|
|
11
|
+
url: string;
|
|
12
|
+
text: string;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
contentType: string;
|
|
15
|
+
};
|
|
16
|
+
/** Parse Bing's ordinary result page. Kept separate so captured fixtures can guard markup changes. */
|
|
17
|
+
export declare function parseBingResults(html: string, limit?: number): WebSearchResult[];
|
|
18
|
+
export declare class WebService {
|
|
19
|
+
search(query: string, limit?: number, signal?: AbortSignal): Promise<{
|
|
20
|
+
query: string;
|
|
21
|
+
results: WebSearchResult[];
|
|
22
|
+
}>;
|
|
23
|
+
fetch(url: string, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
24
|
+
}
|