pi-system-one 1.0.0 → 1.1.1
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/README.md +8 -1
- package/package.json +31 -8
- package/src/config.ts +23 -0
- package/src/extension.ts +19 -0
- package/src/render.ts +16 -0
- package/src/tool.ts +50 -0
- package/tests/config.test.ts +20 -0
- package/tests/render.test.ts +20 -0
- package/tests/tool.test.ts +63 -0
- package/tsconfig.json +5 -0
- package/index.js +0 -2
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
# pi-system-one
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
System One decisions for Pi. Plug in TypeSafe Jev, Reflex, or your own provider.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pi install npm:pi-system-one
|
|
7
|
+
export SYSTEM_ONE_BASE_URL=http://localhost:8008
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
TypeSafe: `SYSTEM_ONE_BASE_URL=https://api.typesafe.ai SYSTEM_ONE_API_KEY=... SYSTEM_ONE_MODEL=jev-latest`.
|
package/package.json
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-system-one",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "System One decisions for Pi. Plug in TypeSafe Jev, Reflex, or your own provider.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/extension.ts"
|
|
9
|
+
},
|
|
6
10
|
"scripts": {
|
|
7
|
-
"test": "
|
|
11
|
+
"test": "node --test --experimental-strip-types \"tests/*.test.ts\"",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi-extension",
|
|
17
|
+
"model-router",
|
|
18
|
+
"model-routing",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"llm",
|
|
21
|
+
"ai",
|
|
22
|
+
"coding-agent"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"system-one-core": "^0.1.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
29
|
+
"typebox": "*"
|
|
8
30
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
31
|
+
"pi": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./src/extension.ts"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
13
36
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// pi-system-one/src/config.ts
|
|
2
|
+
export interface SystemOneEnv {
|
|
3
|
+
SYSTEM_ONE_BASE_URL?: string;
|
|
4
|
+
SYSTEM_ONE_API_KEY?: string;
|
|
5
|
+
SYSTEM_ONE_MODEL?: string;
|
|
6
|
+
SYSTEM_ONE_TIMEOUT_MS?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SystemOneExtConfig {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
}
|
|
14
|
+
export function loadSystemOneConfig(env: SystemOneEnv = process.env): SystemOneExtConfig {
|
|
15
|
+
const baseUrl = env.SYSTEM_ONE_BASE_URL;
|
|
16
|
+
if (!baseUrl) throw new Error("SYSTEM_ONE_BASE_URL is required (e.g. http://localhost:8008 or https://api.typesafe.ai)");
|
|
17
|
+
const timeoutMs = env.SYSTEM_ONE_TIMEOUT_MS ? Number(env.SYSTEM_ONE_TIMEOUT_MS) : 10_000;
|
|
18
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("SYSTEM_ONE_TIMEOUT_MS must be a positive number");
|
|
19
|
+
return { baseUrl, apiKey: env.SYSTEM_ONE_API_KEY || undefined, model: env.SYSTEM_ONE_MODEL || undefined, timeoutMs };
|
|
20
|
+
}
|
|
21
|
+
export function describeConfig(cfg: SystemOneExtConfig): string {
|
|
22
|
+
return [`Provider endpoint: ${cfg.baseUrl}`, `API key: ${cfg.apiKey ? "configured" : "absent"}`, `Model: ${cfg.model ?? "server default"}`, `Timeout: ${cfg.timeoutMs}ms`].join("\n");
|
|
23
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// pi-system-one/src/extension.ts
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { HttpSystemOneProvider } from "system-one-core";
|
|
4
|
+
import { loadSystemOneConfig } from "./config.ts";
|
|
5
|
+
import { buildSystemOneTool } from "./tool.ts";
|
|
6
|
+
|
|
7
|
+
export default function piSystemOneExtension(pi: ExtensionAPI) {
|
|
8
|
+
const cfg = loadSystemOneConfig();
|
|
9
|
+
const tool = buildSystemOneTool({
|
|
10
|
+
provider: new HttpSystemOneProvider({
|
|
11
|
+
id: "configured",
|
|
12
|
+
baseUrl: cfg.baseUrl,
|
|
13
|
+
apiKey: cfg.apiKey,
|
|
14
|
+
defaultModel: cfg.model,
|
|
15
|
+
timeoutMs: cfg.timeoutMs,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
pi.registerTool(tool);
|
|
19
|
+
}
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// pi-system-one/src/render.ts
|
|
2
|
+
export function renderSystemOneResult(response: { answers: Record<string, any> }): string {
|
|
3
|
+
const lines = ["System One result", ""];
|
|
4
|
+
for (const [id, a] of Object.entries(response.answers)) {
|
|
5
|
+
if (a?.type === "choice") {
|
|
6
|
+
lines.push(`${id}:`, ` choice: ${a.choice}`, ` confidence: ${a.confidence}`, ` probabilities:`);
|
|
7
|
+
for (const [k, v] of Object.entries(a.probabilities ?? {})) lines.push(` ${k}: ${v}`);
|
|
8
|
+
} else if (a?.type === "noul") {
|
|
9
|
+
lines.push(`${id}:`, ` noul: ${a.noul}`);
|
|
10
|
+
} else {
|
|
11
|
+
lines.push(`${id}:`, ` score: ${a.score}`, ` confidence: ${a.confidence}`);
|
|
12
|
+
}
|
|
13
|
+
lines.push("");
|
|
14
|
+
}
|
|
15
|
+
return lines.join("\n").trimEnd();
|
|
16
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// pi-system-one/src/tool.ts
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { Static } from "typebox";
|
|
4
|
+
import { SystemOne, type SystemOneProvider } from "system-one-core";
|
|
5
|
+
import { renderSystemOneResult } from "./render.ts";
|
|
6
|
+
|
|
7
|
+
const ChoiceQ = Type.Object({
|
|
8
|
+
type: Type.Literal("choice"),
|
|
9
|
+
instructions: Type.Any(),
|
|
10
|
+
criteria: Type.Record(Type.String(), Type.Union([Type.Any(), Type.Null()])),
|
|
11
|
+
});
|
|
12
|
+
const NoulQ = Type.Object({
|
|
13
|
+
type: Type.Literal("noul"),
|
|
14
|
+
instructions: Type.Any(),
|
|
15
|
+
criteria: Type.Optional(Type.Record(Type.String(), Type.Union([Type.Any(), Type.Null()]))),
|
|
16
|
+
});
|
|
17
|
+
const ScoreQ = Type.Object({
|
|
18
|
+
type: Type.Literal("score"),
|
|
19
|
+
instructions: Type.Any(),
|
|
20
|
+
criteria: Type.Array(Type.Any()),
|
|
21
|
+
});
|
|
22
|
+
export const systemOneParams = Type.Object({
|
|
23
|
+
state: Type.Union([Type.String(), Type.Record(Type.String(), Type.Any()), Type.Array(Type.Any())]),
|
|
24
|
+
questions: Type.Record(Type.String(), Type.Union([ChoiceQ, NoulQ, ScoreQ])),
|
|
25
|
+
model: Type.Optional(Type.String()),
|
|
26
|
+
});
|
|
27
|
+
export type SystemOneParams = Static<typeof systemOneParams>;
|
|
28
|
+
|
|
29
|
+
export function buildSystemOneTool(deps: { provider: SystemOneProvider }) {
|
|
30
|
+
const systemOne = new SystemOne({ provider: deps.provider });
|
|
31
|
+
return {
|
|
32
|
+
name: "system_one",
|
|
33
|
+
label: "System One",
|
|
34
|
+
description:
|
|
35
|
+
"Evaluate one or more bounded decision questions against shared state using the configured System One provider. " +
|
|
36
|
+
"Use this when the answer space is known and a fast probabilistic decision is preferable to generating free-form text. " +
|
|
37
|
+
"Multiple independent choice, noul, and score questions should be batched into one call when they share the same state.",
|
|
38
|
+
parameters: systemOneParams,
|
|
39
|
+
async execute(_toolCallId: string, params: SystemOneParams, signal: AbortSignal | undefined) {
|
|
40
|
+
// Throw on failure per Pi contract — never encode errors in content.
|
|
41
|
+
// Safe to propagate unwrapped: core error messages/codes never contain secrets.
|
|
42
|
+
const response = await systemOne.evaluate(
|
|
43
|
+
{ state: params.state as any, questions: params.questions as any, ...(params.model ? { model: params.model } : {}) },
|
|
44
|
+
{ signal }
|
|
45
|
+
);
|
|
46
|
+
const text = renderSystemOneResult(response as any);
|
|
47
|
+
return { content: [{ type: "text" as const, text }], details: { answers: response.answers, model: response.model, usage: response.usage } };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// pi-system-one/tests/config.test.ts
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { loadSystemOneConfig, describeConfig } from "../src/config.ts";
|
|
5
|
+
|
|
6
|
+
describe("config", () => {
|
|
7
|
+
it("loads from env with localhost default", () => {
|
|
8
|
+
const cfg = loadSystemOneConfig({ SYSTEM_ONE_BASE_URL: "http://localhost:8008" });
|
|
9
|
+
assert.equal(cfg.baseUrl, "http://localhost:8008");
|
|
10
|
+
assert.equal(cfg.timeoutMs, 10_000);
|
|
11
|
+
});
|
|
12
|
+
it("never exposes the key", () => {
|
|
13
|
+
const shown = describeConfig(loadSystemOneConfig({ SYSTEM_ONE_BASE_URL: "https://api.typesafe.ai", SYSTEM_ONE_API_KEY: "sk-live-123" }));
|
|
14
|
+
assert.doesNotMatch(shown, /sk-live-123/);
|
|
15
|
+
assert.match(shown, /configured/);
|
|
16
|
+
});
|
|
17
|
+
it("rejects missing baseUrl", () => {
|
|
18
|
+
assert.throws(() => loadSystemOneConfig({}), /SYSTEM_ONE_BASE_URL/);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// pi-system-one/tests/render.test.ts
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { renderSystemOneResult } from "../src/render.ts";
|
|
5
|
+
|
|
6
|
+
describe("render", () => {
|
|
7
|
+
it("renders mixed answers human-readably with full distributions", () => {
|
|
8
|
+
const text = renderSystemOneResult({
|
|
9
|
+
answers: {
|
|
10
|
+
task: { type: "choice", choice: "coding", probabilities: { coding: 0.91, research: 0.06, writing: 0.03 }, confidence: 0.91 },
|
|
11
|
+
complex: { type: "noul", noul: 0.82 },
|
|
12
|
+
difficulty: { type: "score", score: 1.7, probabilities: { "0": 0.1, "1": 0.2, "2": 0.7 }, legend: { "0": "easy", "1": "medium", "2": "hard" }, confidence: 0.84 },
|
|
13
|
+
},
|
|
14
|
+
} as any);
|
|
15
|
+
assert.match(text, /task:[\s\S]*choice: coding/);
|
|
16
|
+
assert.match(text, /probabilities:[\s\S]*coding: 0\.91/);
|
|
17
|
+
assert.match(text, /complex:[\s\S]*noul: 0\.82/);
|
|
18
|
+
assert.match(text, /difficulty:[\s\S]*score: 1\.7/);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// pi-system-one/tests/tool.test.ts
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { buildSystemOneTool } from "../src/tool.ts";
|
|
5
|
+
import { MockSystemOneProvider } from "system-one-core";
|
|
6
|
+
|
|
7
|
+
describe("system_one tool", () => {
|
|
8
|
+
it("delegates a mixed batch and preserves details", async () => {
|
|
9
|
+
const tool: any = buildSystemOneTool({
|
|
10
|
+
provider: new MockSystemOneProvider({ answers: { t: { type: "choice", choice: "a", probabilities: { a: 1 }, confidence: 1 } } }),
|
|
11
|
+
});
|
|
12
|
+
const res = await tool.execute("id-1", { state: "hi", questions: { t: { type: "choice", instructions: "W?", criteria: { a: null } } } }, undefined, undefined, {});
|
|
13
|
+
const text = JSON.stringify(res);
|
|
14
|
+
assert.match(text, /"a"/);
|
|
15
|
+
assert.equal(res.details.answers.t.choice, "a");
|
|
16
|
+
});
|
|
17
|
+
it("propagates provider errors by throwing (never error-results)", async () => {
|
|
18
|
+
const tool: any = buildSystemOneTool({
|
|
19
|
+
provider: { id: "boom", async evaluate() { throw new Error("down"); } },
|
|
20
|
+
});
|
|
21
|
+
await assert.rejects(
|
|
22
|
+
tool.execute("id-2", { state: "x", questions: { q: { type: "noul", instructions: "Is it?" } } }, undefined, undefined, {}),
|
|
23
|
+
/down/
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
it("mixed heterogeneous batch with model+signal+usage", async () => {
|
|
27
|
+
let seenRequest: any;
|
|
28
|
+
let seenOptions: any;
|
|
29
|
+
const stub = {
|
|
30
|
+
id: "stub",
|
|
31
|
+
async evaluate(req: any, opts: any) {
|
|
32
|
+
seenRequest = req;
|
|
33
|
+
seenOptions = opts;
|
|
34
|
+
return {
|
|
35
|
+
answers: {
|
|
36
|
+
c: { type: "choice", choice: "a", probabilities: { a: 0.9, b: 0.1 }, confidence: 0.9 },
|
|
37
|
+
n: { type: "noul", noul: 0.8 },
|
|
38
|
+
s: { type: "score", score: 1.5, probabilities: { "0": 0.5, "1": 0.5 }, legend: { "0": "easy", "1": "hard" }, confidence: 0.9 },
|
|
39
|
+
},
|
|
40
|
+
usage: { inputTokens: 5, outputTokens: 2 },
|
|
41
|
+
metadata: { provider: "stub" },
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const tool: any = buildSystemOneTool({ provider: stub as any });
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const res = await tool.execute("id-3", {
|
|
48
|
+
state: "hi",
|
|
49
|
+
model: "jev-latest",
|
|
50
|
+
questions: {
|
|
51
|
+
c: { type: "choice", instructions: "W?", criteria: { a: null, b: null } },
|
|
52
|
+
n: { type: "noul", instructions: "Is it?" },
|
|
53
|
+
s: { type: "score", instructions: "How hard?", criteria: ["easy", "hard"] },
|
|
54
|
+
},
|
|
55
|
+
}, controller.signal);
|
|
56
|
+
assert.equal(res.details.answers.c.choice, "a");
|
|
57
|
+
assert.equal(res.details.answers.n.noul, 0.8);
|
|
58
|
+
assert.equal(res.details.answers.s.score, 1.5);
|
|
59
|
+
assert.deepEqual(res.details.usage, { inputTokens: 5, outputTokens: 2 });
|
|
60
|
+
assert.equal(seenRequest.model, "jev-latest");
|
|
61
|
+
assert.equal(seenOptions.signal, controller.signal);
|
|
62
|
+
});
|
|
63
|
+
});
|
package/tsconfig.json
ADDED
package/index.js
DELETED