@evandu/super-pi 0.1.129 → 0.1.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/super-pi-documents/index.ts +92 -0
- package/agents/super-pi-knowledge/index.ts +32 -1
- package/agents/super-pi-task/core.ts +40 -0
- package/agents/super-pi-task/index.ts +15 -1
- package/agents/super-pi-web/index.ts +32 -0
- package/assets/fonts/LICENSE +92 -0
- package/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +5 -5
- package/dist/client/assets/{CodeMirrorEditor-BNVBZZSS.js → CodeMirrorEditor-BlQhH3vs.js} +2 -2
- package/dist/client/assets/{dist-DmAOqr0M.js → dist-B1_1Ui9E.js} +1 -1
- package/dist/client/assets/{dist-GkIV1LTX.js → dist-B448EWTM.js} +1 -1
- package/dist/client/assets/{dist-CiSGSVBZ.js → dist-B68Q8uXX.js} +1 -1
- package/dist/client/assets/{dist-DB5M5fZG.js → dist-BFlq-kiF.js} +1 -1
- package/dist/client/assets/{dist-BykVFExd.js → dist-BXVFgriA.js} +1 -1
- package/dist/client/assets/{dist-CZ9I2JCd.js → dist-BcE6tJvc.js} +1 -1
- package/dist/client/assets/{dist-BhJGK8-b.js → dist-ByIqsb5-.js} +1 -1
- package/dist/client/assets/{dist-DZnUERoW.js → dist-C6qSkkeN.js} +1 -1
- package/dist/client/assets/{dist-BKp4AWEJ.js → dist-C9AkgNbt.js} +1 -1
- package/dist/client/assets/{dist-CRzRB9k3.js → dist-COp25nhe.js} +1 -1
- package/dist/client/assets/{dist-CMnvChDX.js → dist-CpuexUlh.js} +1 -1
- package/dist/client/assets/{dist-ChszWGDv.js → dist-CtdlYgGc.js} +1 -1
- package/dist/client/assets/{dist-Dq4-QFAJ.js → dist-Cybsoapc.js} +1 -1
- package/dist/client/assets/{dist-Ur37340K.js → dist-D6PoSxR5.js} +1 -1
- package/dist/client/assets/{dist-C90MPTnk.js → dist-D7bj81KQ.js} +1 -1
- package/dist/client/assets/{dist-BBW2SE_R.js → dist-DP2ygAFF.js} +1 -1
- package/dist/client/assets/{dist-B61RSa-K.js → dist-DXNDtyHP.js} +1 -1
- package/dist/client/assets/{dist-CWYRjnj-.js → dist-DxoWS5MH.js} +1 -1
- package/dist/client/assets/{dist-Cxerc7Bb.js → dist-Fhkmvjdx.js} +1 -1
- package/dist/client/assets/{dist-BDygQgZD.js → dist-hfpd-aWa.js} +1 -1
- package/dist/client/assets/{dist-B2nPVdKh.js → dist-lQWWWxh9.js} +1 -1
- package/dist/client/assets/{dist-HdeIBl2J.js → dist-r5Af94tH.js} +1 -1
- package/dist/client/assets/{index-D_c-CYAr.js → index-BApJ3Zbq.js} +3 -3
- package/dist/client/index.html +1 -1
- package/dist/server/active-memory.d.ts +12 -0
- package/dist/server/active-memory.js +25 -1
- package/dist/server/application/conversation-service.js +5 -0
- package/dist/server/application/document-service.d.ts +68 -0
- package/dist/server/application/document-service.js +120 -0
- package/dist/server/application/run-service.js +49 -7
- package/dist/server/documents.d.ts +152 -0
- package/dist/server/documents.js +998 -0
- package/dist/server/file-changes.js +6 -4
- package/dist/server/files.d.ts +32 -0
- package/dist/server/files.js +28 -0
- package/dist/server/http-server.js +113 -2
- package/dist/server/index.js +13 -1
- package/dist/server/inline-thinking.d.ts +90 -0
- package/dist/server/inline-thinking.js +181 -0
- package/dist/server/knowledge.d.ts +24 -14
- package/dist/server/knowledge.js +47 -32
- package/dist/server/pi-session-pool.js +2 -1
- package/dist/server/pi-session.d.ts +20 -6
- package/dist/server/pi-session.js +43 -11
- package/dist/server/presentations/schema.d.ts +120 -0
- package/dist/server/presentations/schema.js +128 -0
- package/dist/server/runtime.d.ts +46 -0
- package/dist/server/runtime.js +150 -16
- package/dist/server/server-context.d.ts +3 -0
- package/dist/server/subagents.js +33 -6
- package/dist/server/web.d.ts +24 -0
- package/dist/server/web.js +180 -0
- package/package.json +33 -5
- package/skills/presentation/SKILL.md +30 -0
- package/skills/presentation/references/data.md +10 -0
- package/skills/presentation/references/quality.md +12 -0
- package/skills/presentation/references/story.md +12 -0
- package/skills/presentation/references/templates-and-editing.md +11 -0
- package/skills/presentation/references/visual-system.md +10 -0
|
@@ -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 } : {}),
|
|
@@ -184,6 +184,13 @@ export interface ExtensionEnvironment {
|
|
|
184
184
|
* concurrent session's environment would otherwise land in the middle of this one.
|
|
185
185
|
*/
|
|
186
186
|
export declare function withEnvironment<T>(values: Record<string, string>, work: () => Promise<T>): Promise<T>;
|
|
187
|
+
/**
|
|
188
|
+
* `withEnvironment` for one session's extension load, serialized across all session creations.
|
|
189
|
+
*
|
|
190
|
+
* The extension factories run inside `work` and capture the values into their closures; after the
|
|
191
|
+
* window closes they never read `process.env` again, so only the WINDOW needs to be exclusive.
|
|
192
|
+
*/
|
|
193
|
+
export declare function withIsolatedSessionEnv<T>(values: Record<string, string>, work: () => Promise<T>): Promise<T>;
|
|
187
194
|
/**
|
|
188
195
|
* The live session for one conversation.
|
|
189
196
|
*
|
|
@@ -336,6 +343,8 @@ export interface SessionCreation {
|
|
|
336
343
|
sessionDir: string;
|
|
337
344
|
/** Super Pi's extensions to mount, as absolute paths. */
|
|
338
345
|
extensionPaths: string[];
|
|
346
|
+
/** Super Pi's built-in skills to expose beside the user's and workspace's skills. */
|
|
347
|
+
skillPaths?: string[];
|
|
339
348
|
/** The environment those extensions read when they are loaded. */
|
|
340
349
|
extensionEnv: Record<string, string>;
|
|
341
350
|
/** The agent's system prompt file, or undefined for Pi's own defaults. */
|
|
@@ -363,14 +372,17 @@ export interface SessionCreation {
|
|
|
363
372
|
*
|
|
364
373
|
* The order of operations here IS the isolation mechanism, and each step is load-bearing:
|
|
365
374
|
*
|
|
366
|
-
* 1. The
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
375
|
+
* 1. The loader is created with Super Pi's extension paths, for PI to load - through its jiti
|
|
376
|
+
* transpiler and virtual modules, the same machinery the spawned CLI used. Nothing here imports a
|
|
377
|
+
* Super Pi extension file directly; see the comment at the loader below for why that must never
|
|
378
|
+
* come back.
|
|
379
|
+
* 2. The reload runs inside this session's env window (`withIsolatedSessionEnv`), which is when Pi
|
|
380
|
+
* calls the extension factories. The factories CAPTURE the values; they do not read them later.
|
|
370
381
|
* 3. The session is created with that loader.
|
|
371
382
|
*
|
|
372
|
-
* Step 2 happening outside
|
|
373
|
-
*
|
|
383
|
+
* Step 2 happening outside its env window would give every session the same configuration, and the
|
|
384
|
+
* window would give every session no configuration at all if two creations could interleave - which
|
|
385
|
+
* the serialization in `withIsolatedSessionEnv` prevents.
|
|
374
386
|
*
|
|
375
387
|
* THE FACTORY IS INJECTABLE. A test needs to drive a session without a real Pi behind it - to check what
|
|
376
388
|
* a session was BUILT with (its working directory, its allowlist) rather than what it eventually
|
|
@@ -410,3 +422,5 @@ export interface PromptTemplateSummary {
|
|
|
410
422
|
}
|
|
411
423
|
export declare function loadPromptTemplateSummaries(cwd: string): Promise<PromptTemplateSummary[]>;
|
|
412
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[];
|
|
@@ -225,20 +225,39 @@ export async function withEnvironment(values, work) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The env window of the session currently being built, so the next one queues behind it.
|
|
230
|
+
*
|
|
231
|
+
* `withEnvironment` alone is not safe here: its window is process-global, and two sessions created
|
|
232
|
+
* concurrently would interleave - A applies its capability, B applies its own, and A's extension
|
|
233
|
+
* factory captures B's credential. Chaining the windows means a concurrent creation waits for the
|
|
234
|
+
* other's `reload()` - milliseconds - instead of silently wearing its capability.
|
|
235
|
+
*/
|
|
236
|
+
let extensionEnvWindow = Promise.resolve();
|
|
237
|
+
/**
|
|
238
|
+
* `withEnvironment` for one session's extension load, serialized across all session creations.
|
|
239
|
+
*
|
|
240
|
+
* The extension factories run inside `work` and capture the values into their closures; after the
|
|
241
|
+
* window closes they never read `process.env` again, so only the WINDOW needs to be exclusive.
|
|
242
|
+
*/
|
|
243
|
+
export function withIsolatedSessionEnv(values, work) {
|
|
244
|
+
const run = extensionEnvWindow.then(() => withEnvironment(values, work));
|
|
245
|
+
extensionEnvWindow = run.then(() => undefined, () => undefined);
|
|
246
|
+
return run;
|
|
247
|
+
}
|
|
228
248
|
export async function createPiSession(creation) {
|
|
229
249
|
const startedAt = performance.now();
|
|
230
250
|
traceSession(creation.sessionId, "sdk.load.begin", startedAt);
|
|
231
251
|
const sdk = await loadSdk();
|
|
232
252
|
traceSession(creation.sessionId, "sdk.load.end", startedAt);
|
|
233
253
|
const agentDir = creation.agentDir ?? defaultAgentDir();
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}));
|
|
254
|
+
// THE EXTENSIONS ARE PI'S TO LOAD. Every path goes to Pi's own resource loader, which imports them
|
|
255
|
+
// through its jiti transpiler with its virtual modules - and both of those are load-bearing:
|
|
256
|
+
// `@sinclair/typebox` is a Pi virtual module that NO node_modules anywhere can resolve, and Node's
|
|
257
|
+
// type stripping refuses .ts under any `node_modules` segment (which is where a published package
|
|
258
|
+
// lives). A native `import()` here survived neither - it broke every installed layout the moment it
|
|
259
|
+
// shipped, invisibly, because nothing in `npm run check` runs from an installed layout.
|
|
260
|
+
// `scripts/installed-layout.test.mjs` exists to keep that class of breakage visible.
|
|
242
261
|
return (async () => {
|
|
243
262
|
// THE AGENT'S PROMPT. Pi's loader takes the same two shapes the command line took: a `systemPrompt`
|
|
244
263
|
// that REPLACES Pi's own, and an `appendSystemPrompt` list of files that are ADDED to it. An agent
|
|
@@ -249,15 +268,18 @@ export async function createPiSession(creation) {
|
|
|
249
268
|
const loader = new sdk.DefaultResourceLoader({
|
|
250
269
|
cwd: creation.cwd,
|
|
251
270
|
agentDir,
|
|
252
|
-
...(
|
|
253
|
-
...(
|
|
271
|
+
...(creation.extensionPaths.length ? { additionalExtensionPaths: creation.extensionPaths } : {}),
|
|
272
|
+
...(creation.skillPaths?.length ? { additionalSkillPaths: creation.skillPaths } : {}),
|
|
254
273
|
...(creation.systemPromptFile && creation.systemPromptMode === "replace"
|
|
255
274
|
? { systemPrompt: creation.systemPromptFile }
|
|
256
275
|
: creation.systemPromptFile
|
|
257
276
|
? { appendSystemPrompt: [creation.systemPromptFile] }
|
|
258
277
|
: {}),
|
|
259
278
|
});
|
|
260
|
-
|
|
279
|
+
// The extension factories run inside `reload()` and capture this session's env values into their
|
|
280
|
+
// closures; the window is serialized (`withIsolatedSessionEnv`) so a concurrent creation cannot
|
|
281
|
+
// land its own capability in the middle.
|
|
282
|
+
await withIsolatedSessionEnv(creation.extensionEnv, () => loader.reload());
|
|
261
283
|
traceSession(creation.sessionId, "sdk.loader.reload", startedAt, `extensions=${creation.extensionPaths.length}`);
|
|
262
284
|
const sessionManager = creation.persist === false
|
|
263
285
|
? sdk.SessionManager.inMemory(creation.cwd, { id: creation.sessionId })
|
|
@@ -392,6 +414,16 @@ export function superPiExtensionPaths(repositoryRoot) {
|
|
|
392
414
|
join(repositoryRoot, "agents", "super-pi-task", "index.ts"),
|
|
393
415
|
join(repositoryRoot, "agents", "super-pi-memory", "index.ts"),
|
|
394
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"),
|
|
395
422
|
];
|
|
396
423
|
return paths.filter((path) => existsSync(path));
|
|
397
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); }
|