@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.
- 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/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 +4 -0
- package/dist/server/pi-session.js +11 -0
- 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 +31 -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
|
@@ -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
|
|
217
|
-
*
|
|
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
|
-
/**
|
|
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
|
}>;
|
package/dist/server/knowledge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
681
|
-
*
|
|
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
|
-
/**
|
|
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); }
|
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
|
*
|