@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.
Files changed (70) hide show
  1. package/agents/super-pi-documents/index.ts +92 -0
  2. package/agents/super-pi-knowledge/index.ts +32 -1
  3. package/agents/super-pi-task/core.ts +40 -0
  4. package/agents/super-pi-task/index.ts +15 -1
  5. package/agents/super-pi-web/index.ts +32 -0
  6. package/assets/fonts/LICENSE +92 -0
  7. package/assets/fonts/NotoSansSC-Bold.otf +0 -0
  8. package/assets/fonts/NotoSansSC-Regular.otf +0 -0
  9. package/dist/.super-pi-check-inputs.json +2 -2
  10. package/dist/client/.super-pi-client-sources.json +5 -5
  11. package/dist/client/assets/{CodeMirrorEditor-BNVBZZSS.js → CodeMirrorEditor-BlQhH3vs.js} +2 -2
  12. package/dist/client/assets/{dist-DmAOqr0M.js → dist-B1_1Ui9E.js} +1 -1
  13. package/dist/client/assets/{dist-GkIV1LTX.js → dist-B448EWTM.js} +1 -1
  14. package/dist/client/assets/{dist-CiSGSVBZ.js → dist-B68Q8uXX.js} +1 -1
  15. package/dist/client/assets/{dist-DB5M5fZG.js → dist-BFlq-kiF.js} +1 -1
  16. package/dist/client/assets/{dist-BykVFExd.js → dist-BXVFgriA.js} +1 -1
  17. package/dist/client/assets/{dist-CZ9I2JCd.js → dist-BcE6tJvc.js} +1 -1
  18. package/dist/client/assets/{dist-BhJGK8-b.js → dist-ByIqsb5-.js} +1 -1
  19. package/dist/client/assets/{dist-DZnUERoW.js → dist-C6qSkkeN.js} +1 -1
  20. package/dist/client/assets/{dist-BKp4AWEJ.js → dist-C9AkgNbt.js} +1 -1
  21. package/dist/client/assets/{dist-CRzRB9k3.js → dist-COp25nhe.js} +1 -1
  22. package/dist/client/assets/{dist-CMnvChDX.js → dist-CpuexUlh.js} +1 -1
  23. package/dist/client/assets/{dist-ChszWGDv.js → dist-CtdlYgGc.js} +1 -1
  24. package/dist/client/assets/{dist-Dq4-QFAJ.js → dist-Cybsoapc.js} +1 -1
  25. package/dist/client/assets/{dist-Ur37340K.js → dist-D6PoSxR5.js} +1 -1
  26. package/dist/client/assets/{dist-C90MPTnk.js → dist-D7bj81KQ.js} +1 -1
  27. package/dist/client/assets/{dist-BBW2SE_R.js → dist-DP2ygAFF.js} +1 -1
  28. package/dist/client/assets/{dist-B61RSa-K.js → dist-DXNDtyHP.js} +1 -1
  29. package/dist/client/assets/{dist-CWYRjnj-.js → dist-DxoWS5MH.js} +1 -1
  30. package/dist/client/assets/{dist-Cxerc7Bb.js → dist-Fhkmvjdx.js} +1 -1
  31. package/dist/client/assets/{dist-BDygQgZD.js → dist-hfpd-aWa.js} +1 -1
  32. package/dist/client/assets/{dist-B2nPVdKh.js → dist-lQWWWxh9.js} +1 -1
  33. package/dist/client/assets/{dist-HdeIBl2J.js → dist-r5Af94tH.js} +1 -1
  34. package/dist/client/assets/{index-D_c-CYAr.js → index-BApJ3Zbq.js} +3 -3
  35. package/dist/client/index.html +1 -1
  36. package/dist/server/active-memory.d.ts +12 -0
  37. package/dist/server/active-memory.js +25 -1
  38. package/dist/server/application/conversation-service.js +5 -0
  39. package/dist/server/application/document-service.d.ts +68 -0
  40. package/dist/server/application/document-service.js +120 -0
  41. package/dist/server/application/run-service.js +49 -7
  42. package/dist/server/documents.d.ts +152 -0
  43. package/dist/server/documents.js +998 -0
  44. package/dist/server/file-changes.js +6 -4
  45. package/dist/server/files.d.ts +32 -0
  46. package/dist/server/files.js +28 -0
  47. package/dist/server/http-server.js +113 -2
  48. package/dist/server/index.js +13 -1
  49. package/dist/server/inline-thinking.d.ts +90 -0
  50. package/dist/server/inline-thinking.js +181 -0
  51. package/dist/server/knowledge.d.ts +24 -14
  52. package/dist/server/knowledge.js +47 -32
  53. package/dist/server/pi-session-pool.js +2 -1
  54. package/dist/server/pi-session.d.ts +20 -6
  55. package/dist/server/pi-session.js +43 -11
  56. package/dist/server/presentations/schema.d.ts +120 -0
  57. package/dist/server/presentations/schema.js +128 -0
  58. package/dist/server/runtime.d.ts +46 -0
  59. package/dist/server/runtime.js +150 -16
  60. package/dist/server/server-context.d.ts +3 -0
  61. package/dist/server/subagents.js +33 -6
  62. package/dist/server/web.d.ts +24 -0
  63. package/dist/server/web.js +180 -0
  64. package/package.json +33 -5
  65. package/skills/presentation/SKILL.md +30 -0
  66. package/skills/presentation/references/data.md +10 -0
  67. package/skills/presentation/references/quality.md +12 -0
  68. package/skills/presentation/references/story.md +12 -0
  69. package/skills/presentation/references/templates-and-editing.md +11 -0
  70. package/skills/presentation/references/visual-system.md +10 -0
@@ -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
  *
@@ -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 only when the workspace actually HAS documents. An empty knowledge base would otherwise
1115
- // cost a catalogue round trip and one declared tool on every turn to tell the model there is
1116
- // nothing to find. Review turns never mount it: a background reflection has no business
1117
- // retrieving documents it was not asked about, and its `--tools` line is a closed list. A
1118
- // reranker never mounts it either - it must not be able to call the search it is ranking.
1119
- const knowledgeEnabled = !memoryReview && !knowledgeRerank && !!this.knowledge && await this.knowledge.hasDocuments(conversation.workspaceId).catch(() => false);
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, ...(extensionArgs.length ? await this.oneShotExtensionEnv(conversation.id) : {}), ...(memoryEnabled ? {
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
- const title = text
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
- if (!text.trim())
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 text;
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;
@@ -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
- streamed += delta;
424
- current += delta;
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
- onTextDelta?.(delta);
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
- current = assistantText(event.message) || current;
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
+ }