@evandu/super-pi 0.1.130 → 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 (67) 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/application/document-service.d.ts +68 -0
  37. package/dist/server/application/document-service.js +120 -0
  38. package/dist/server/application/run-service.js +49 -7
  39. package/dist/server/documents.d.ts +152 -0
  40. package/dist/server/documents.js +998 -0
  41. package/dist/server/file-changes.js +6 -4
  42. package/dist/server/files.d.ts +32 -0
  43. package/dist/server/files.js +28 -0
  44. package/dist/server/http-server.js +113 -2
  45. package/dist/server/index.js +13 -1
  46. package/dist/server/inline-thinking.d.ts +90 -0
  47. package/dist/server/inline-thinking.js +181 -0
  48. package/dist/server/knowledge.d.ts +24 -14
  49. package/dist/server/knowledge.js +47 -32
  50. package/dist/server/pi-session-pool.js +2 -1
  51. package/dist/server/pi-session.d.ts +4 -0
  52. package/dist/server/pi-session.js +11 -0
  53. package/dist/server/presentations/schema.d.ts +120 -0
  54. package/dist/server/presentations/schema.js +128 -0
  55. package/dist/server/runtime.d.ts +46 -0
  56. package/dist/server/runtime.js +150 -16
  57. package/dist/server/server-context.d.ts +3 -0
  58. package/dist/server/subagents.js +33 -6
  59. package/dist/server/web.d.ts +24 -0
  60. package/dist/server/web.js +180 -0
  61. package/package.json +31 -5
  62. package/skills/presentation/SKILL.md +30 -0
  63. package/skills/presentation/references/data.md +10 -0
  64. package/skills/presentation/references/quality.md +12 -0
  65. package/skills/presentation/references/story.md +12 -0
  66. package/skills/presentation/references/templates-and-editing.md +11 -0
  67. package/skills/presentation/references/visual-system.md +10 -0
@@ -82,6 +82,8 @@ export type KnowledgeRuntime = {
82
82
  export declare function parseRanking(output: string): number[] | null;
83
83
  /** Duplicated as a literal in `agents/super-pi-knowledge/index.ts`, the way `TASK_TOOL_NAME` is. */
84
84
  export declare const KNOWLEDGE_TOOL_NAME = "knowledge_search";
85
+ /** The import half of the same extension. Duplicated as a literal there, for the same reason. */
86
+ export declare const KNOWLEDGE_IMPORT_TOOL_NAME = "knowledge_import";
85
87
  /**
86
88
  * Index terms for one string: CJK bigrams plus Latin words.
87
89
  *
@@ -152,17 +154,6 @@ export declare class KnowledgeBase {
152
154
  private load;
153
155
  private invalidate;
154
156
  list(workspaceId: string): Promise<Array<Omit<KnowledgeDocument, "text">>>;
155
- /**
156
- * Whether this workspace has any document at all, which is what decides whether the turn mounts the
157
- * extension.
158
- *
159
- * WHY A DIRECTORY LISTING AND NOT `list()`. This runs once per turn, before every spawn. `list()`
160
- * would read and parse every document's full text to answer a yes/no question, so a workspace with
161
- * a dozen real documents would pay for parsing all of them on every message - including the
162
- * catalogue fetch the extension makes the moment it loads. `readdir` answers the same question for
163
- * the price of one directory entry, and a name is all this needs.
164
- */
165
- hasDocuments(workspaceId: string): Promise<boolean>;
166
157
  /**
167
158
  * Adds a document, either from a workspace file or from pasted text.
168
159
  *
@@ -171,6 +162,10 @@ export declare class KnowledgeBase {
171
162
  * newer version that diverges from the one the workspace serves. The path therefore goes through
172
163
  * the same `resolveInsideRoot` guard the file editor uses, so symlink and `..` escapes are refused
173
164
  * by construction instead of by a check someone has to remember.
165
+ *
166
+ * WHO CALLS THIS. The settings window (manage, action "add") AND, since the import tool exists,
167
+ * the primary agent mid-turn on the user's say-so - through the same guard, the same whitelist and
168
+ * the same caps, so an agent import can never do more than a person's import could.
174
169
  */
175
170
  add(workspaceId: string, body: {
176
171
  path?: string;
@@ -213,27 +208,42 @@ export declare class KnowledgeBase {
213
208
  */
214
209
  private rerank;
215
210
  /**
216
- * Mints a read-only token for one turn. The agent can retrieve, never write: everything a person
217
- * puts in a knowledge base is theirs to curate from the settings window.
211
+ * Mints a token for one turn. The agent can retrieve and IMPORT (a document the user just asked it
212
+ * to add rides through the same `add` guard the settings window uses), but never remove: curation
213
+ * that deletes a person's documents stays a settings-window act.
218
214
  */
219
215
  issue(conversationId: string): string;
220
216
  release(token: string): void;
221
217
  private identity;
222
- /** The agent-facing endpoint: a title catalogue and a search. Nothing that writes. */
218
+ /**
219
+ * The agent-facing endpoint: a title catalogue, a search, and an import. The import is the SAME
220
+ * `add` the settings window calls - path guard, format whitelist, size caps, dedupe - so an agent
221
+ * cannot do anything a person could not, and the one thing it cannot do is `remove`.
222
+ */
223
223
  tool(token: string, body: {
224
224
  kind?: string;
225
225
  query?: string;
226
226
  limit?: number;
227
+ path?: string;
228
+ title?: string;
229
+ text?: string;
227
230
  }, signal?: AbortSignal): Promise<{
228
231
  documents: {
229
232
  title: string;
230
233
  source: string;
231
234
  characters: number;
232
235
  }[];
236
+ imported?: undefined;
237
+ hits?: undefined;
238
+ reranked?: undefined;
239
+ } | {
240
+ documents?: undefined;
241
+ imported: Omit<KnowledgeDocument, "text">;
233
242
  hits?: undefined;
234
243
  reranked?: undefined;
235
244
  } | {
236
245
  documents?: undefined;
246
+ imported?: undefined;
237
247
  hits: KnowledgeHit[];
238
248
  reranked: boolean;
239
249
  }>;
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { basename, extname, join } from "node:path";
4
4
  import { looksBinary, resolveInsideRoot } from "./files.js";
5
+ import { documentFormatOf, DocumentError, extractText } from "./documents.js";
5
6
  /** 1 MB. Big enough for a design doc or a spec, small enough that chunking stays instant. */
6
7
  export const MAX_DOCUMENT_BYTES = 1024 * 1024;
7
8
  /** A pasted document rides inside a JSON body, so the request cap leaves room for escaping overhead. */
@@ -53,6 +54,8 @@ const HEADING = /^#{1,6}\s+(.*\S)\s*$/;
53
54
  const PASTED_SOURCE = "pasted";
54
55
  /** Duplicated as a literal in `agents/super-pi-knowledge/index.ts`, the way `TASK_TOOL_NAME` is. */
55
56
  export const KNOWLEDGE_TOOL_NAME = "knowledge_search";
57
+ /** The import half of the same extension. Duplicated as a literal there, for the same reason. */
58
+ export const KNOWLEDGE_IMPORT_TOOL_NAME = "knowledge_import";
56
59
  /** Extensions a document may be imported from. Mirrors `files.TEXT_EXTENSIONS`, minus the source languages. */
57
60
  const IMPORTABLE = new Set([
58
61
  ".md", ".markdown", ".mdx", ".txt", ".text", ".log", ".rst", ".org",
@@ -369,30 +372,6 @@ export class KnowledgeBase {
369
372
  const documents = (await this.readAll(workspaceId)).map(document => this.summarise(document));
370
373
  return documents.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
371
374
  }
372
- /**
373
- * Whether this workspace has any document at all, which is what decides whether the turn mounts the
374
- * extension.
375
- *
376
- * WHY A DIRECTORY LISTING AND NOT `list()`. This runs once per turn, before every spawn. `list()`
377
- * would read and parse every document's full text to answer a yes/no question, so a workspace with
378
- * a dozen real documents would pay for parsing all of them on every message - including the
379
- * catalogue fetch the extension makes the moment it loads. `readdir` answers the same question for
380
- * the price of one directory entry, and a name is all this needs.
381
- */
382
- async hasDocuments(workspaceId) {
383
- if (!this.enabled())
384
- return false;
385
- if (!this.store.snapshot().workspaces.some(workspace => workspace.id === workspaceId))
386
- return false;
387
- try {
388
- return (await readdir(this.folder(workspaceId))).some(name => name.endsWith(".json"));
389
- }
390
- catch (error) {
391
- if (error.code === "ENOENT")
392
- return false;
393
- throw error;
394
- }
395
- }
396
375
  /**
397
376
  * Adds a document, either from a workspace file or from pasted text.
398
377
  *
@@ -401,6 +380,10 @@ export class KnowledgeBase {
401
380
  * newer version that diverges from the one the workspace serves. The path therefore goes through
402
381
  * the same `resolveInsideRoot` guard the file editor uses, so symlink and `..` escapes are refused
403
382
  * by construction instead of by a check someone has to remember.
383
+ *
384
+ * WHO CALLS THIS. The settings window (manage, action "add") AND, since the import tool exists,
385
+ * the primary agent mid-turn on the user's say-so - through the same guard, the same whitelist and
386
+ * the same caps, so an agent import can never do more than a person's import could.
404
387
  */
405
388
  async add(workspaceId, body) {
406
389
  this.requireWorkspace(workspaceId);
@@ -417,17 +400,42 @@ export class KnowledgeBase {
417
400
  if (!info.isFile())
418
401
  throw new Error("只能导入文件");
419
402
  format = extname(absolute).toLocaleLowerCase();
420
- if (!IMPORTABLE.has(format))
403
+ const documentFormat = documentFormatOf(format);
404
+ if (!IMPORTABLE.has(format) && !documentFormat) {
421
405
  throw new Error(`不支持的文件类型 ${format || "(无扩展名)"};请先转换为 Markdown 或纯文本`);
406
+ }
422
407
  if (info.size > MAX_DOCUMENT_BYTES)
423
408
  throw new Error(`文件过大:${info.size} 字节超过 ${MAX_DOCUMENT_BYTES} 的上限`);
424
- const buffer = await readFile(absolute);
425
- if (looksBinary(buffer.subarray(0, 4096)))
426
- throw new Error("看起来是二进制文件,无法索引");
427
- text = buffer.toString("utf8");
428
409
  source = body.path.trim();
429
410
  if (!title)
430
411
  title = basename(absolute);
412
+ if (documentFormat) {
413
+ // A DOCUMENT IS PARSED, NOT DECODED. The bytes on disk are a container - a PDF's or a
414
+ // docx's - and what goes into the knowledge base is the TEXT they contain. The format
415
+ // recorded on the document stays the SOURCE format (`.pdf`), because that is what the next
416
+ // re-import of this path has to parse it as: the extracted text is not a PDF, and treating
417
+ // it as one would make an update fail to parse at all.
418
+ //
419
+ // A DOCUMENT THAT COULD NOT BE READ IS A REFUSAL, NOT AN EMPTY DOCUMENT. The parser's own
420
+ // reasons are already written for a person - a scan with no text layer, an encrypted file,
421
+ // a missing package - and they are passed through unchanged, because a person who is told
422
+ // "读取失败" learns nothing about what to do next.
423
+ try {
424
+ const extracted = await extractText(absolute, documentFormat);
425
+ text = extracted.text;
426
+ }
427
+ catch (error) {
428
+ if (error instanceof DocumentError)
429
+ throw new Error(error.message);
430
+ throw error;
431
+ }
432
+ }
433
+ else {
434
+ const buffer = await readFile(absolute);
435
+ if (looksBinary(buffer.subarray(0, 4096)))
436
+ throw new Error("看起来是二进制文件,无法索引");
437
+ text = buffer.toString("utf8");
438
+ }
431
439
  }
432
440
  const trimmed = text.trim();
433
441
  if (!trimmed)
@@ -677,8 +685,9 @@ export class KnowledgeBase {
677
685
  }
678
686
  }
679
687
  /**
680
- * Mints a read-only token for one turn. The agent can retrieve, never write: everything a person
681
- * puts in a knowledge base is theirs to curate from the settings window.
688
+ * Mints a token for one turn. The agent can retrieve and IMPORT (a document the user just asked it
689
+ * to add rides through the same `add` guard the settings window uses), but never remove: curation
690
+ * that deletes a person's documents stays a settings-window act.
682
691
  */
683
692
  issue(conversationId) {
684
693
  const conversation = this.store.snapshot().conversations.find(item => item.id === conversationId);
@@ -697,13 +706,19 @@ export class KnowledgeBase {
697
706
  throw new Error("知识库授权已失效");
698
707
  return identity;
699
708
  }
700
- /** The agent-facing endpoint: a title catalogue and a search. Nothing that writes. */
709
+ /**
710
+ * The agent-facing endpoint: a title catalogue, a search, and an import. The import is the SAME
711
+ * `add` the settings window calls - path guard, format whitelist, size caps, dedupe - so an agent
712
+ * cannot do anything a person could not, and the one thing it cannot do is `remove`.
713
+ */
701
714
  async tool(token, body, signal) {
702
715
  const identity = this.identity(token);
703
716
  if (!this.enabled())
704
717
  throw new Error("知识库已关闭");
705
718
  if (body.kind === "catalogue")
706
719
  return { documents: (await this.list(identity.workspaceId)).map(document => ({ title: document.title, source: document.source, characters: document.characters })) };
720
+ if (body.kind === "import")
721
+ return { imported: await this.add(identity.workspaceId, { path: body.path, title: body.title, text: body.text }) };
707
722
  if (body.kind !== "search")
708
723
  throw new Error("未知知识库操作");
709
724
  // The reranker needs the conversation it is ranking FOR: that is whose model answers, whose
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { existsSync } from "node:fs";
24
24
  import { join } from "node:path";
25
- import { createPiSession, sameFingerprint, superPiExtensionPaths, } from "./pi-session.js";
25
+ import { createPiSession, sameFingerprint, superPiExtensionPaths, superPiSkillPaths, } from "./pi-session.js";
26
26
  /** How long an idle session survives before it is evicted. */
27
27
  const IDLE_EVICTION_MS = 10 * 60_000;
28
28
  /**
@@ -170,6 +170,7 @@ export class PiSessionPool {
170
170
  sessionId: request.conversationId,
171
171
  sessionDir: this.sessionDir,
172
172
  extensionPaths: superPiExtensionPaths(this.repositoryRoot),
173
+ skillPaths: superPiSkillPaths(this.repositoryRoot),
173
174
  extensionEnv: request.extensionEnv,
174
175
  ...(request.systemPromptFile ? { systemPromptFile: request.systemPromptFile } : {}),
175
176
  ...(request.systemPromptMode ? { systemPromptMode: request.systemPromptMode } : {}),
@@ -343,6 +343,8 @@ export interface SessionCreation {
343
343
  sessionDir: string;
344
344
  /** Super Pi's extensions to mount, as absolute paths. */
345
345
  extensionPaths: string[];
346
+ /** Super Pi's built-in skills to expose beside the user's and workspace's skills. */
347
+ skillPaths?: string[];
346
348
  /** The environment those extensions read when they are loaded. */
347
349
  extensionEnv: Record<string, string>;
348
350
  /** The agent's system prompt file, or undefined for Pi's own defaults. */
@@ -420,3 +422,5 @@ export interface PromptTemplateSummary {
420
422
  }
421
423
  export declare function loadPromptTemplateSummaries(cwd: string): Promise<PromptTemplateSummary[]>;
422
424
  export declare function superPiExtensionPaths(repositoryRoot: string): string[];
425
+ /** Skills shipped by Super Pi itself. Paths are explicit so installed and development layouts agree. */
426
+ export declare function superPiSkillPaths(repositoryRoot: string): string[];
@@ -269,6 +269,7 @@ export async function createPiSession(creation) {
269
269
  cwd: creation.cwd,
270
270
  agentDir,
271
271
  ...(creation.extensionPaths.length ? { additionalExtensionPaths: creation.extensionPaths } : {}),
272
+ ...(creation.skillPaths?.length ? { additionalSkillPaths: creation.skillPaths } : {}),
272
273
  ...(creation.systemPromptFile && creation.systemPromptMode === "replace"
273
274
  ? { systemPrompt: creation.systemPromptFile }
274
275
  : creation.systemPromptFile
@@ -413,6 +414,16 @@ export function superPiExtensionPaths(repositoryRoot) {
413
414
  join(repositoryRoot, "agents", "super-pi-task", "index.ts"),
414
415
  join(repositoryRoot, "agents", "super-pi-memory", "index.ts"),
415
416
  join(repositoryRoot, "agents", "super-pi-knowledge", "index.ts"),
417
+ join(repositoryRoot, "agents", "super-pi-web", "index.ts"),
418
+ // The document tools. They are mounted for every session, and the extension itself decides
419
+ // whether to register anything: with no endpoint in its environment it returns without
420
+ // registering a tool, so a runtime without a document endpoint mounts nothing harmful.
421
+ join(repositoryRoot, "agents", "super-pi-documents", "index.ts"),
416
422
  ];
417
423
  return paths.filter((path) => existsSync(path));
418
424
  }
425
+ /** Skills shipped by Super Pi itself. Paths are explicit so installed and development layouts agree. */
426
+ export function superPiSkillPaths(repositoryRoot) {
427
+ const paths = [join(repositoryRoot, "skills", "presentation")];
428
+ return paths.filter((path) => existsSync(join(path, "SKILL.md")));
429
+ }
@@ -0,0 +1,120 @@
1
+ /** Versioned, renderer-independent contract for structured presentation tools. */
2
+ export declare const PRESENTATION_SPEC_VERSION: 1;
3
+ export declare const PRESENTATION_LIMITS: {
4
+ readonly maxSlides: 200;
5
+ readonly maxElementsPerSlide: 500;
6
+ readonly maxTextCharacters: 100000;
7
+ readonly maxTableCells: 10000;
8
+ readonly maxChartPoints: 20000;
9
+ };
10
+ export type PresentationSpec = {
11
+ version: typeof PRESENTATION_SPEC_VERSION;
12
+ metadata?: {
13
+ title?: string;
14
+ subject?: string;
15
+ author?: string;
16
+ company?: string;
17
+ language?: string;
18
+ };
19
+ canvas: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ theme?: PresentationTheme;
24
+ slides: PresentationSlide[];
25
+ };
26
+ export type PresentationTheme = {
27
+ colors?: Record<string, string>;
28
+ fonts?: {
29
+ heading?: string;
30
+ body?: string;
31
+ mono?: string;
32
+ };
33
+ };
34
+ export type PresentationSlide = {
35
+ id: string;
36
+ title?: string;
37
+ notes?: string;
38
+ background?: string;
39
+ elements: PresentationElement[];
40
+ };
41
+ type Frame = {
42
+ x: number;
43
+ y: number;
44
+ width: number;
45
+ height: number;
46
+ };
47
+ type BaseElement = Frame & {
48
+ id: string;
49
+ altText?: string;
50
+ name?: string;
51
+ hidden?: boolean;
52
+ rotation?: number;
53
+ };
54
+ export type TextElement = BaseElement & {
55
+ type: "text";
56
+ text: string;
57
+ fontFamily?: string;
58
+ fontSize?: number;
59
+ color?: string;
60
+ bold?: boolean;
61
+ italic?: boolean;
62
+ align?: "left" | "center" | "right" | "justify";
63
+ verticalAlign?: "top" | "middle" | "bottom";
64
+ link?: string;
65
+ };
66
+ export type ShapeElement = BaseElement & {
67
+ type: "shape";
68
+ shape: "rectangle" | "roundRectangle" | "ellipse" | "triangle" | "chevron";
69
+ fill?: string;
70
+ stroke?: string;
71
+ strokeWidth?: number;
72
+ };
73
+ export type LineElement = BaseElement & {
74
+ type: "line";
75
+ stroke?: string;
76
+ strokeWidth?: number;
77
+ dash?: "solid" | "dash" | "dot";
78
+ beginArrow?: boolean;
79
+ endArrow?: boolean;
80
+ };
81
+ export type ImageElement = BaseElement & {
82
+ type: "image";
83
+ source: string;
84
+ fit?: "contain" | "cover" | "stretch";
85
+ link?: string;
86
+ };
87
+ export type TableElement = BaseElement & {
88
+ type: "table";
89
+ rows: Array<Array<string | number | boolean | null>>;
90
+ headerRows?: number;
91
+ };
92
+ export type ChartElement = BaseElement & {
93
+ type: "chart";
94
+ chart: "bar" | "column" | "line" | "area" | "pie" | "doughnut" | "scatter";
95
+ categories?: string[];
96
+ series: Array<{
97
+ name: string;
98
+ values: Array<number | null>;
99
+ xValues?: Array<number | null>;
100
+ }>;
101
+ showLegend?: boolean;
102
+ showValues?: boolean;
103
+ };
104
+ export type PresentationElement = TextElement | ShapeElement | LineElement | ImageElement | TableElement | ChartElement;
105
+ export type PresentationDiagnostic = {
106
+ code: "invalid_type" | "invalid_value" | "unsupported_version" | "duplicate_id" | "limit_exceeded" | "out_of_bounds";
107
+ severity: "error" | "warning";
108
+ path: string;
109
+ message: string;
110
+ };
111
+ export type PresentationValidationResult = {
112
+ valid: true;
113
+ spec: PresentationSpec;
114
+ diagnostics: PresentationDiagnostic[];
115
+ } | {
116
+ valid: false;
117
+ diagnostics: PresentationDiagnostic[];
118
+ };
119
+ export declare function validatePresentationSpec(input: unknown): PresentationValidationResult;
120
+ export {};
@@ -0,0 +1,128 @@
1
+ /** Versioned, renderer-independent contract for structured presentation tools. */
2
+ export const PRESENTATION_SPEC_VERSION = 1;
3
+ export const PRESENTATION_LIMITS = {
4
+ maxSlides: 200,
5
+ maxElementsPerSlide: 500,
6
+ maxTextCharacters: 100_000,
7
+ maxTableCells: 10_000,
8
+ maxChartPoints: 20_000,
9
+ };
10
+ const ELEMENT_TYPES = new Set(["text", "shape", "line", "image", "table", "chart"]);
11
+ const ID_PATTERN = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;
12
+ export function validatePresentationSpec(input) {
13
+ const diagnostics = [];
14
+ const error = (code, path, message) => diagnostics.push({ code, severity: "error", path, message });
15
+ if (!isObject(input)) {
16
+ error("invalid_type", "$", "Presentation spec must be an object.");
17
+ return { valid: false, diagnostics };
18
+ }
19
+ if (input.version !== PRESENTATION_SPEC_VERSION) {
20
+ error("unsupported_version", "$.version", `Only presentation spec version ${PRESENTATION_SPEC_VERSION} is supported.`);
21
+ }
22
+ const canvas = input.canvas;
23
+ if (!isObject(canvas))
24
+ error("invalid_type", "$.canvas", "Canvas must be an object.");
25
+ else {
26
+ positiveNumber(canvas.width, "$.canvas.width", error);
27
+ positiveNumber(canvas.height, "$.canvas.height", error);
28
+ }
29
+ if (!Array.isArray(input.slides)) {
30
+ error("invalid_type", "$.slides", "Slides must be an array.");
31
+ }
32
+ else {
33
+ if (input.slides.length > PRESENTATION_LIMITS.maxSlides) {
34
+ error("limit_exceeded", "$.slides", `A deck may contain at most ${PRESENTATION_LIMITS.maxSlides} slides.`);
35
+ }
36
+ const ids = new Set();
37
+ input.slides.forEach((slide, slideIndex) => validateSlide(slide, slideIndex, canvas, ids, error));
38
+ }
39
+ if (input.theme !== undefined)
40
+ validateTheme(input.theme, error);
41
+ return diagnostics.some((item) => item.severity === "error")
42
+ ? { valid: false, diagnostics }
43
+ : { valid: true, spec: input, diagnostics };
44
+ }
45
+ function validateSlide(slide, index, canvas, ids, error) {
46
+ const path = `$.slides[${index}]`;
47
+ if (!isObject(slide))
48
+ return error("invalid_type", path, "Slide must be an object.");
49
+ validateId(slide.id, `${path}.id`, ids, error);
50
+ if (!Array.isArray(slide.elements))
51
+ return error("invalid_type", `${path}.elements`, "Elements must be an array.");
52
+ if (slide.elements.length > PRESENTATION_LIMITS.maxElementsPerSlide) {
53
+ error("limit_exceeded", `${path}.elements`, `A slide may contain at most ${PRESENTATION_LIMITS.maxElementsPerSlide} elements.`);
54
+ }
55
+ slide.elements.forEach((element, elementIndex) => validateElement(element, `${path}.elements[${elementIndex}]`, canvas, ids, error));
56
+ }
57
+ function validateElement(element, path, canvas, ids, error) {
58
+ if (!isObject(element))
59
+ return error("invalid_type", path, "Element must be an object.");
60
+ validateId(element.id, `${path}.id`, ids, error);
61
+ if (typeof element.type !== "string" || !ELEMENT_TYPES.has(element.type)) {
62
+ error("invalid_value", `${path}.type`, "Element type is not supported.");
63
+ return;
64
+ }
65
+ for (const key of ["x", "y", "width", "height"])
66
+ positiveNumber(element[key], `${path}.${key}`, error, key === "x" || key === "y");
67
+ if (isObject(canvas) && finite(element.x) && finite(element.y) && finite(element.width) && finite(element.height)
68
+ && finite(canvas.width) && finite(canvas.height)
69
+ && (element.x < 0 || element.y < 0 || element.x + element.width > canvas.width || element.y + element.height > canvas.height)) {
70
+ error("out_of_bounds", path, "Element frame must fit within the canvas.");
71
+ }
72
+ if (element.type === "text") {
73
+ if (typeof element.text !== "string")
74
+ error("invalid_type", `${path}.text`, "Text must be a string.");
75
+ else if (element.text.length > PRESENTATION_LIMITS.maxTextCharacters)
76
+ error("limit_exceeded", `${path}.text`, "Text is too long.");
77
+ }
78
+ else if (element.type === "image") {
79
+ if (typeof element.source !== "string" || !element.source.trim())
80
+ error("invalid_value", `${path}.source`, "Image source is required.");
81
+ if (!element.altText)
82
+ error("invalid_value", `${path}.altText`, "Images require alt text.");
83
+ }
84
+ else if (element.type === "table") {
85
+ if (!Array.isArray(element.rows))
86
+ error("invalid_type", `${path}.rows`, "Table rows must be an array.");
87
+ else if (element.rows.reduce((sum, row) => sum + (Array.isArray(row) ? row.length : 0), 0) > PRESENTATION_LIMITS.maxTableCells)
88
+ error("limit_exceeded", `${path}.rows`, "Table contains too many cells.");
89
+ }
90
+ else if (element.type === "chart")
91
+ validateChart(element, path, error);
92
+ }
93
+ function validateChart(chart, path, error) {
94
+ if (!Array.isArray(chart.series) || chart.series.length === 0)
95
+ return error("invalid_value", `${path}.series`, "Chart requires at least one series.");
96
+ let points = 0;
97
+ for (const [index, series] of chart.series.entries()) {
98
+ if (!isObject(series) || typeof series.name !== "string" || !Array.isArray(series.values)) {
99
+ error("invalid_type", `${path}.series[${index}]`, "Series requires a name and numeric values.");
100
+ continue;
101
+ }
102
+ if (series.values.some((value) => value !== null && !finite(value)))
103
+ error("invalid_value", `${path}.series[${index}].values`, "Chart values must be finite numbers or null.");
104
+ points += series.values.length;
105
+ }
106
+ if (points > PRESENTATION_LIMITS.maxChartPoints)
107
+ error("limit_exceeded", `${path}.series`, "Chart contains too many data points.");
108
+ }
109
+ function validateTheme(theme, error) {
110
+ if (!isObject(theme))
111
+ return error("invalid_type", "$.theme", "Theme must be an object.");
112
+ if (theme.colors !== undefined && (!isObject(theme.colors) || Object.values(theme.colors).some((value) => typeof value !== "string"))) {
113
+ error("invalid_type", "$.theme.colors", "Theme colors must map token names to color strings.");
114
+ }
115
+ }
116
+ function validateId(value, path, ids, error) {
117
+ if (typeof value !== "string" || !ID_PATTERN.test(value))
118
+ return error("invalid_value", path, "ID must start with a letter and contain only letters, digits, dot, underscore, or hyphen.");
119
+ if (ids.has(value))
120
+ error("duplicate_id", path, `ID '${value}' is already used in this deck.`);
121
+ ids.add(value);
122
+ }
123
+ function positiveNumber(value, path, error, allowZero = false) {
124
+ if (!finite(value) || (allowZero ? value < 0 : value <= 0))
125
+ error("invalid_value", path, allowZero ? "Value must be a non-negative finite number." : "Value must be a positive finite number.");
126
+ }
127
+ function finite(value) { return typeof value === "number" && Number.isFinite(value); }
128
+ function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
@@ -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
  *