pi-system-one 1.1.2 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-system-one",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "System One decisions for Pi. Plug in TypeSafe Jev, Reflex, or your own provider.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,126 @@
1
+ // pi-system-one/src/commands.ts
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionCommandContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { HttpSystemOneProvider } from "system-one-core";
7
+ import {
8
+ applyConfigAnswers,
9
+ blankSession,
10
+ type ConfigAnswers,
11
+ describeConfig,
12
+ type SessionConfig,
13
+ } from "./config.ts";
14
+ import { buildSystemOneTool } from "./tool.ts";
15
+
16
+ /** Mutable holder: empty until env or `/so config` provides a base URL. */
17
+ export interface SessionStore {
18
+ session?: SessionConfig;
19
+ }
20
+
21
+ export function registerSystemOneCommands(
22
+ pi: ExtensionAPI,
23
+ store: SessionStore,
24
+ ) {
25
+ function applyProvider() {
26
+ if (!store.session?.current.baseUrl) return;
27
+ const ext = store.session.current;
28
+ const provider = new HttpSystemOneProvider({
29
+ id: "configured",
30
+ baseUrl: ext.baseUrl,
31
+ apiKey: ext.apiKey,
32
+ defaultModel: ext.model,
33
+ timeoutMs: ext.timeoutMs,
34
+ });
35
+ pi.registerTool(buildSystemOneTool({ provider }));
36
+ }
37
+
38
+ async function cmdConfig(ctx: ExtensionCommandContext): Promise<void> {
39
+ const cur = store.session?.current;
40
+ const baseUrlRaw = await ctx.ui.input(
41
+ `SYSTEM_ONE_BASE_URL (current: ${cur?.baseUrl || "unset"}):`,
42
+ cur?.baseUrl ?? "",
43
+ );
44
+ if (baseUrlRaw === undefined) {
45
+ ctx.ui.notify("Cancelled.", "info");
46
+ return;
47
+ }
48
+ const baseUrl = baseUrlRaw.trim();
49
+ if (!baseUrl && !cur?.baseUrl) {
50
+ ctx.ui.notify("Cancelled (base URL is required).", "info");
51
+ return;
52
+ }
53
+ const modelRaw = await ctx.ui.input(
54
+ `SYSTEM_ONE_MODEL (current: ${cur?.model ?? "server default"}):`,
55
+ cur?.model ?? "",
56
+ );
57
+ if (modelRaw === undefined) {
58
+ ctx.ui.notify("Cancelled.", "info");
59
+ return;
60
+ }
61
+ const apiKeyRaw = await ctx.ui.input(
62
+ "SYSTEM_ONE_API_KEY (empty = keep, - = forget memory key, memory-only, gone when the session closes):",
63
+ "",
64
+ );
65
+ if (apiKeyRaw === undefined) {
66
+ ctx.ui.notify("Cancelled.", "info");
67
+ return;
68
+ }
69
+ const answers: ConfigAnswers = {
70
+ baseUrl,
71
+ model: modelRaw.trim(),
72
+ apiKey: apiKeyRaw.trim(),
73
+ timeoutMs: "",
74
+ };
75
+ if (!store.session) {
76
+ store.session = blankSession();
77
+ }
78
+ const memoryKey = applyConfigAnswers(store.session, answers);
79
+ applyProvider();
80
+ ctx.ui.notify(
81
+ memoryKey
82
+ ? "Updated for this session. API key is in memory only and will be gone when the session closes; export SYSTEM_ONE_API_KEY to persist it."
83
+ : "Updated for this session.",
84
+ "info",
85
+ );
86
+ }
87
+
88
+ async function cmdStatus(ctx: ExtensionCommandContext): Promise<void> {
89
+ if (!store.session) {
90
+ ctx.ui.notify(
91
+ "System One is not configured. Export SYSTEM_ONE_BASE_URL or run /so config.",
92
+ "info",
93
+ );
94
+ return;
95
+ }
96
+ ctx.ui.notify(describeConfig(store.session), "info");
97
+ }
98
+
99
+ const completions = [
100
+ {
101
+ value: "config",
102
+ label: "config - change endpoint, model, or key",
103
+ description: "Reconfigure System One for this session",
104
+ },
105
+ {
106
+ value: "status",
107
+ label: "status - show current config",
108
+ description: "Show current endpoint, model, and key source",
109
+ },
110
+ ];
111
+
112
+ pi.registerCommand("so", {
113
+ description: "System One config (/so status for current)",
114
+ getArgumentCompletions: (prefix: string) => {
115
+ const p = prefix.trim().toLowerCase();
116
+ return completions.filter((c) => c.value.startsWith(p));
117
+ },
118
+ handler: async (args, ctx) => {
119
+ const [cmd] = args.trim().toLowerCase().split(/\s+/);
120
+ if (cmd === "status") await cmdStatus(ctx);
121
+ else await cmdConfig(ctx);
122
+ },
123
+ });
124
+
125
+ return { applyProvider, store };
126
+ }
package/src/config.ts CHANGED
@@ -1,16 +1,27 @@
1
1
  // pi-system-one/src/config.ts
2
+ //
3
+ // Configuration is environment variables. `/so config` applies overrides
4
+ // in memory for the current session only. API keys are never written to
5
+ // disk by this extension: a key typed into `/so config` lives in memory
6
+ // and is gone when the session closes. To persist, export it
7
+ // (e.g. SYSTEM_ONE_API_KEY) in the shell.
8
+
2
9
  export interface SystemOneEnv {
3
10
  SYSTEM_ONE_BASE_URL?: string;
4
11
  SYSTEM_ONE_API_KEY?: string;
5
12
  SYSTEM_ONE_MODEL?: string;
6
13
  SYSTEM_ONE_TIMEOUT_MS?: string;
7
14
  }
15
+
8
16
  export interface SystemOneExtConfig {
9
17
  baseUrl: string;
10
18
  apiKey?: string;
11
19
  model?: string;
12
20
  timeoutMs: number;
13
21
  }
22
+
23
+ export const DEFAULT_TIMEOUT_MS = 10_000;
24
+
14
25
  export function loadSystemOneConfig(
15
26
  env: SystemOneEnv = process.env,
16
27
  ): SystemOneExtConfig {
@@ -21,7 +32,7 @@ export function loadSystemOneConfig(
21
32
  );
22
33
  const timeoutMs = env.SYSTEM_ONE_TIMEOUT_MS
23
34
  ? Number(env.SYSTEM_ONE_TIMEOUT_MS)
24
- : 10_000;
35
+ : DEFAULT_TIMEOUT_MS;
25
36
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
26
37
  throw new Error("SYSTEM_ONE_TIMEOUT_MS must be a positive number");
27
38
  return {
@@ -31,11 +42,80 @@ export function loadSystemOneConfig(
31
42
  timeoutMs,
32
43
  };
33
44
  }
34
- export function describeConfig(cfg: SystemOneExtConfig): string {
45
+
46
+ export interface SessionConfig {
47
+ /** Current effective config (env + in-memory overrides). */
48
+ current: SystemOneExtConfig;
49
+ /** True when the key came from `/so config` (memory-only). */
50
+ keyInMemory: boolean;
51
+ }
52
+
53
+ export function createSessionConfig(
54
+ env: SystemOneEnv = process.env,
55
+ ): SessionConfig {
56
+ return { current: loadSystemOneConfig(env), keyInMemory: false };
57
+ }
58
+
59
+ /** Empty session for `/so config` when no env is present. */
60
+ export function blankSession(): SessionConfig {
61
+ return {
62
+ current: {
63
+ baseUrl: "",
64
+ apiKey: undefined,
65
+ model: undefined,
66
+ timeoutMs: DEFAULT_TIMEOUT_MS,
67
+ },
68
+ keyInMemory: false,
69
+ };
70
+ }
71
+
72
+ export interface ConfigAnswers {
73
+ /** Empty string keeps the existing value. */
74
+ baseUrl: string;
75
+ model: string;
76
+ /** Empty string keeps the existing key. Provided keys are memory-only. */
77
+ apiKey: string;
78
+ timeoutMs: string;
79
+ }
80
+
81
+ /** Apply `/so config` answers. Returns true when a memory-only key was set. */
82
+ export function applyConfigAnswers(
83
+ session: SessionConfig,
84
+ answers: ConfigAnswers,
85
+ env: SystemOneEnv = process.env,
86
+ ): boolean {
87
+ if (answers.baseUrl) session.current.baseUrl = answers.baseUrl;
88
+ if (answers.model) session.current.model = answers.model;
89
+ const timeoutMs = Number(answers.timeoutMs);
90
+ if (answers.timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
91
+ session.current.timeoutMs = timeoutMs;
92
+ }
93
+ if (answers.apiKey === "-") {
94
+ session.current.apiKey = env.SYSTEM_ONE_API_KEY || undefined;
95
+ session.keyInMemory = false;
96
+ return false;
97
+ }
98
+ if (answers.apiKey) {
99
+ session.current.apiKey = answers.apiKey;
100
+ session.keyInMemory = true;
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /** Human-readable summary. Shows whether a key exists, never the key. */
107
+ export function describeConfig(session: SessionConfig): string {
108
+ const c = session.current;
109
+ const keyLine = !c.apiKey
110
+ ? " api key: absent"
111
+ : session.keyInMemory
112
+ ? " api key: in memory only (gone when the session closes; export SYSTEM_ONE_API_KEY to persist)"
113
+ : " api key: from environment";
35
114
  return [
36
- `Provider endpoint: ${cfg.baseUrl}`,
37
- `API key: ${cfg.apiKey ? "configured" : "absent"}`,
38
- `Model: ${cfg.model ?? "server default"}`,
39
- `Timeout: ${cfg.timeoutMs}ms`,
115
+ "System One:",
116
+ ` endpoint: ${c.baseUrl}`,
117
+ keyLine,
118
+ ` model: ${c.model ?? "server default"}`,
119
+ ` timeout: ${c.timeoutMs}ms`,
40
120
  ].join("\n");
41
121
  }
package/src/extension.ts CHANGED
@@ -1,19 +1,25 @@
1
1
  // pi-system-one/src/extension.ts
2
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";
3
+ import { registerSystemOneCommands, type SessionStore } from "./commands.ts";
4
+ import { createSessionConfig } from "./config.ts";
6
5
 
7
6
  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);
7
+ // The /so command is always registered so config stays reachable with
8
+ // no env set. The tool appears once a base URL exists (env or /so config).
9
+ const store: SessionStore = {};
10
+ try {
11
+ store.session = createSessionConfig();
12
+ } catch {
13
+ pi.on("session_start", (event, ctx) => {
14
+ if (event.reason === "startup" || event.reason === "new") {
15
+ ctx.ui.notify(
16
+ "System One: SYSTEM_ONE_BASE_URL is not set. Export it or run /so config.",
17
+ "warning",
18
+ );
19
+ }
20
+ });
21
+ }
22
+
23
+ const { applyProvider } = registerSystemOneCommands(pi, store);
24
+ applyProvider();
19
25
  }
package/src/render.ts CHANGED
@@ -16,12 +16,19 @@ export function renderSystemOneResult(response: {
16
16
  lines.push(` ${k}: ${v}`);
17
17
  } else if (a?.type === "noul") {
18
18
  lines.push(`${id}:`, ` noul: ${a.noul}`);
19
- } else {
19
+ } else if (a?.type === "score") {
20
20
  lines.push(
21
21
  `${id}:`,
22
22
  ` score: ${a.score}`,
23
23
  ` confidence: ${a.confidence}`,
24
24
  );
25
+ if (a.probabilities && typeof a.probabilities === "object") {
26
+ lines.push(` probabilities:`);
27
+ for (const [k, v] of Object.entries(a.probabilities))
28
+ lines.push(` ${k}: ${v}`);
29
+ }
30
+ } else {
31
+ lines.push(`${id}:`, ` unknown answer type: ${a?.type ?? "missing"}`);
25
32
  }
26
33
  lines.push("");
27
34
  }
package/src/tool.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // biome-ignore-all lint/suspicious/noExplicitAny: intentional dynamic boundary over JSON protocol values
2
2
  // pi-system-one/src/tool.ts
3
3
 
4
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
5
  import { SystemOne, type SystemOneProvider } from "system-one-core";
5
6
  import type { Static } from "typebox";
6
7
  import { Type } from "typebox";
@@ -34,9 +35,16 @@ export const systemOneParams = Type.Object({
34
35
  });
35
36
  export type SystemOneParams = Static<typeof systemOneParams>;
36
37
 
38
+ export interface SystemOneDetails {
39
+ answers: Record<string, unknown>;
40
+ model?: string;
41
+ usage?: { inputTokens?: number; outputTokens?: number };
42
+ metadata: { provider: string; latencyMs?: number };
43
+ }
44
+
37
45
  export function buildSystemOneTool(deps: { provider: SystemOneProvider }) {
38
46
  const systemOne = new SystemOne({ provider: deps.provider });
39
- return {
47
+ const tool: ToolDefinition<typeof systemOneParams, SystemOneDetails> = {
40
48
  name: "system_one",
41
49
  label: "System One",
42
50
  description:
@@ -44,11 +52,15 @@ export function buildSystemOneTool(deps: { provider: SystemOneProvider }) {
44
52
  "Use this when the answer space is known and a fast probabilistic decision is preferable to generating free-form text. " +
45
53
  "Multiple independent choice, noul, and score questions should be batched into one call when they share the same state.",
46
54
  parameters: systemOneParams,
47
- async execute(
48
- _toolCallId: string,
49
- params: SystemOneParams,
50
- signal: AbortSignal | undefined,
51
- ) {
55
+ promptSnippet:
56
+ "Perform calibrated structured decisions and classifications over state",
57
+ promptGuidelines: [
58
+ "Use system_one to evaluate one or more bounded decision questions",
59
+ "Use system_one when you need structured probability, categorical choice, or scored rubric decisions rather than text generation.",
60
+ ],
61
+
62
+ async execute(toolCallId, params, signal, _onUpdate, _ctx) {
63
+ void toolCallId;
52
64
  // Throw on failure per Pi contract — never encode errors in content.
53
65
  // Safe to propagate unwrapped: core error messages/codes never contain secrets.
54
66
  const response = await systemOne.evaluate(
@@ -59,15 +71,17 @@ export function buildSystemOneTool(deps: { provider: SystemOneProvider }) {
59
71
  },
60
72
  { signal },
61
73
  );
62
- const text = renderSystemOneResult(response as any);
74
+ const text = renderSystemOneResult(response);
63
75
  return {
64
76
  content: [{ type: "text" as const, text }],
65
77
  details: {
66
78
  answers: response.answers,
67
79
  model: response.model,
68
80
  usage: response.usage,
81
+ metadata: response.metadata,
69
82
  },
70
83
  };
71
84
  },
72
85
  };
86
+ return tool;
73
87
  }
@@ -0,0 +1,130 @@
1
+ // pi-system-one/tests/commands.test.ts
2
+
3
+ import assert from "node:assert/strict";
4
+ import { describe, it } from "node:test";
5
+ import {
6
+ registerSystemOneCommands,
7
+ type SessionStore,
8
+ } from "../src/commands.ts";
9
+ import { createSessionConfig } from "../src/config.ts";
10
+
11
+ interface Captured {
12
+ tools: string[];
13
+ handler: ((args: string, ctx: never) => Promise<void>) | undefined;
14
+ completions: ((prefix: string) => { value: string }[]) | undefined;
15
+ notices: string[];
16
+ }
17
+
18
+ function harness() {
19
+ const cap: Captured = {
20
+ tools: [],
21
+ handler: undefined,
22
+ completions: undefined,
23
+ notices: [],
24
+ };
25
+ const pi = {
26
+ registerTool: (t: { name: string }) => {
27
+ cap.tools.push(t.name);
28
+ },
29
+ registerCommand: (
30
+ _n: string,
31
+ o: {
32
+ handler: (args: string, ctx: never) => Promise<void>;
33
+ getArgumentCompletions?: (prefix: string) => { value: string }[];
34
+ },
35
+ ) => {
36
+ cap.handler = o.handler;
37
+ cap.completions = o.getArgumentCompletions;
38
+ },
39
+ on: () => {},
40
+ };
41
+ return { pi: pi as never, cap };
42
+ }
43
+
44
+ function ctxFor(answers: (string | undefined)[], cap: Captured) {
45
+ return {
46
+ ui: {
47
+ input: async () => answers.shift(),
48
+ notify: (msg: string) => {
49
+ cap.notices.push(msg);
50
+ },
51
+ },
52
+ } as never;
53
+ }
54
+
55
+ describe("so command", () => {
56
+ it("completes config + status, case-insensitively", () => {
57
+ const { pi, cap } = harness();
58
+ registerSystemOneCommands(pi, {});
59
+ assert.deepEqual(
60
+ cap.completions?.("").map((c) => c.value),
61
+ ["config", "status"],
62
+ );
63
+ assert.deepEqual(
64
+ cap.completions?.("S").map((c) => c.value),
65
+ ["status"],
66
+ );
67
+ assert.deepEqual(cap.completions?.("x"), []);
68
+ });
69
+ it("bare /so configures from blank session and registers the tool", async () => {
70
+ const { pi, cap } = harness();
71
+ const store: SessionStore = {};
72
+ registerSystemOneCommands(pi, store);
73
+ await cap.handler?.("", ctxFor(["http://x/", "", ""], cap));
74
+ assert.deepEqual(cap.tools, ["system_one"]);
75
+ assert.equal(store.session?.current.baseUrl, "http://x/");
76
+ });
77
+ it("cancel (undefined) keeps everything and registers nothing", async () => {
78
+ const { pi, cap } = harness();
79
+ const store: SessionStore = {};
80
+ registerSystemOneCommands(pi, store);
81
+ await cap.handler?.("", ctxFor([undefined], cap));
82
+ assert.deepEqual(cap.tools, []);
83
+ assert.equal(store.session, undefined);
84
+ assert.match(cap.notices[0], /Cancelled/);
85
+ });
86
+ it("empty base URL keeps existing value", async () => {
87
+ const { pi, cap } = harness();
88
+ const store: SessionStore = {
89
+ session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://keep/" }),
90
+ };
91
+ registerSystemOneCommands(pi, store);
92
+ await cap.handler?.("", ctxFor(["", "", ""], cap));
93
+ assert.equal(store.session?.current.baseUrl, "http://keep/");
94
+ });
95
+ it("whitespace-only base URL is rejected", async () => {
96
+ const { pi, cap } = harness();
97
+ const store: SessionStore = {};
98
+ registerSystemOneCommands(pi, store);
99
+ await cap.handler?.("", ctxFor([" ", "", ""], cap));
100
+ assert.equal(store.session, undefined);
101
+ assert.match(cap.notices[0], /Cancelled/);
102
+ });
103
+ it("'-' forgets a memory key back to env", async () => {
104
+ const { pi, cap } = harness();
105
+ const store: SessionStore = {
106
+ session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://x/" }),
107
+ };
108
+ registerSystemOneCommands(pi, store);
109
+ await cap.handler?.("", ctxFor(["", "", "sk-mem", ""], cap));
110
+ assert.equal(store.session?.keyInMemory, true);
111
+ await cap.handler?.("", ctxFor(["", "", "-", ""], cap));
112
+ assert.equal(store.session?.keyInMemory, false);
113
+ assert.equal(store.session?.current.apiKey, undefined);
114
+ });
115
+ it("status shows current config; STATUS routes case-insensitively", async () => {
116
+ const { pi, cap } = harness();
117
+ const store: SessionStore = {
118
+ session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://x/" }),
119
+ };
120
+ registerSystemOneCommands(pi, store);
121
+ await cap.handler?.("STATUS", ctxFor([], cap));
122
+ assert.match(cap.notices[0], /http:\/\/x\//);
123
+ });
124
+ it("status with no session explains how to configure", async () => {
125
+ const { pi, cap } = harness();
126
+ registerSystemOneCommands(pi, {});
127
+ await cap.handler?.("status", ctxFor([], cap));
128
+ assert.match(cap.notices[0], /not configured/);
129
+ });
130
+ });
@@ -2,27 +2,135 @@
2
2
 
3
3
  import assert from "node:assert/strict";
4
4
  import { describe, it } from "node:test";
5
- import { describeConfig, loadSystemOneConfig } from "../src/config.ts";
5
+ import {
6
+ applyConfigAnswers,
7
+ blankSession,
8
+ createSessionConfig,
9
+ describeConfig,
10
+ loadSystemOneConfig,
11
+ } from "../src/config.ts";
6
12
 
7
13
  describe("config", () => {
8
- it("loads from env with localhost default", () => {
9
- const cfg = loadSystemOneConfig({
14
+ it("loads from env", () => {
15
+ const s = createSessionConfig({
10
16
  SYSTEM_ONE_BASE_URL: "http://localhost:8008",
11
17
  });
12
- assert.equal(cfg.baseUrl, "http://localhost:8008");
13
- assert.equal(cfg.timeoutMs, 10_000);
18
+ assert.equal(s.current.baseUrl, "http://localhost:8008");
19
+ assert.equal(s.current.timeoutMs, 10_000);
20
+ assert.equal(s.keyInMemory, false);
14
21
  });
15
- it("never exposes the key", () => {
16
- const shown = describeConfig(
17
- loadSystemOneConfig({
18
- SYSTEM_ONE_BASE_URL: "https://api.typesafe.ai",
19
- SYSTEM_ONE_API_KEY: "sk-live-123",
20
- }),
22
+ it("overrides apply in memory, empty keeps", () => {
23
+ const s = createSessionConfig({
24
+ SYSTEM_ONE_BASE_URL: "http://a/",
25
+ SYSTEM_ONE_MODEL: "m1",
26
+ });
27
+ applyConfigAnswers(s, {
28
+ baseUrl: "http://b/",
29
+ model: "",
30
+ apiKey: "",
31
+ timeoutMs: "",
32
+ });
33
+ assert.equal(s.current.baseUrl, "http://b/");
34
+ assert.equal(s.current.model, "m1");
35
+ assert.equal(s.keyInMemory, false);
36
+ });
37
+ it("typed key is memory-only and reported, never shown", () => {
38
+ const s = createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://a/" });
39
+ const memory = applyConfigAnswers(s, {
40
+ baseUrl: "",
41
+ model: "",
42
+ apiKey: "sk-typed",
43
+ timeoutMs: "",
44
+ });
45
+ assert.equal(memory, true);
46
+ assert.equal(s.keyInMemory, true);
47
+ const shown = describeConfig(s);
48
+ assert.doesNotMatch(shown, /sk-typed/);
49
+ assert.match(shown, /in memory only/);
50
+ });
51
+ it("env key is reported as from environment", () => {
52
+ const s = createSessionConfig({
53
+ SYSTEM_ONE_BASE_URL: "http://a/",
54
+ SYSTEM_ONE_API_KEY: "sk-env",
55
+ });
56
+ const shown = describeConfig(s);
57
+ assert.doesNotMatch(shown, /sk-env/);
58
+ assert.match(shown, /from environment/);
59
+ });
60
+ it("absent key is reported", () => {
61
+ const s = createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://a/" });
62
+ assert.match(describeConfig(s), /absent/);
63
+ });
64
+ it("'-' forgets a memory key back to env", () => {
65
+ const s = createSessionConfig({
66
+ SYSTEM_ONE_BASE_URL: "http://a/",
67
+ SYSTEM_ONE_API_KEY: "sk-env",
68
+ });
69
+ applyConfigAnswers(s, {
70
+ baseUrl: "",
71
+ model: "",
72
+ apiKey: "sk-mem",
73
+ timeoutMs: "",
74
+ });
75
+ assert.equal(s.keyInMemory, true);
76
+ applyConfigAnswers(
77
+ s,
78
+ { baseUrl: "", model: "", apiKey: "-", timeoutMs: "" },
79
+ {},
21
80
  );
22
- assert.doesNotMatch(shown, /sk-live-123/);
23
- assert.match(shown, /configured/);
81
+ assert.equal(s.keyInMemory, false);
82
+ assert.equal(s.current.apiKey, undefined);
24
83
  });
25
- it("rejects missing baseUrl", () => {
84
+ it("legacy env bootstrap still validates", () => {
26
85
  assert.throws(() => loadSystemOneConfig({}), /SYSTEM_ONE_BASE_URL/);
86
+ assert.throws(
87
+ () =>
88
+ loadSystemOneConfig({
89
+ SYSTEM_ONE_BASE_URL: "http://a/",
90
+ SYSTEM_ONE_TIMEOUT_MS: "nope",
91
+ }),
92
+ /positive number/,
93
+ );
94
+ });
95
+ it("blankSession starts empty with defaults", () => {
96
+ const s = blankSession();
97
+ assert.equal(s.current.baseUrl, "");
98
+ assert.equal(s.current.timeoutMs, 10_000);
99
+ assert.equal(s.keyInMemory, false);
100
+ });
101
+ it("timeout answers apply only when valid", () => {
102
+ const s = createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://a/" });
103
+ applyConfigAnswers(s, {
104
+ baseUrl: "",
105
+ model: "",
106
+ apiKey: "",
107
+ timeoutMs: "5000",
108
+ });
109
+ assert.equal(s.current.timeoutMs, 5000);
110
+ applyConfigAnswers(s, {
111
+ baseUrl: "",
112
+ model: "",
113
+ apiKey: "",
114
+ timeoutMs: "0",
115
+ });
116
+ assert.equal(s.current.timeoutMs, 5000);
117
+ applyConfigAnswers(s, {
118
+ baseUrl: "",
119
+ model: "",
120
+ apiKey: "",
121
+ timeoutMs: "junk",
122
+ });
123
+ assert.equal(s.current.timeoutMs, 5000);
124
+ });
125
+ it("describeConfig shows endpoint, model, and timeout lines", () => {
126
+ const shown = describeConfig(
127
+ createSessionConfig({
128
+ SYSTEM_ONE_BASE_URL: "http://a/",
129
+ SYSTEM_ONE_MODEL: "m1",
130
+ }),
131
+ );
132
+ assert.match(shown, /endpoint: http:\/\/a\//);
133
+ assert.match(shown, /model: m1/);
134
+ assert.match(shown, /timeout: 10000ms/);
27
135
  });
28
136
  });
@@ -29,4 +29,19 @@ describe("render", () => {
29
29
  assert.match(text, /complex:[\s\S]*noul: 0\.82/);
30
30
  assert.match(text, /difficulty:[\s\S]*score: 1\.7/);
31
31
  });
32
+ it("renders score probabilities and flags unknown types", () => {
33
+ const text = renderSystemOneResult({
34
+ answers: {
35
+ difficulty: {
36
+ type: "score",
37
+ score: 1.7,
38
+ probabilities: { "0": 0.1, "2": 0.7 },
39
+ confidence: 0.84,
40
+ },
41
+ weird: { type: "mystery", value: 1 },
42
+ },
43
+ } as any);
44
+ assert.match(text, /difficulty:[\s\S]*0: 0\.1/);
45
+ assert.match(text, /weird:[\s\S]*unknown answer type: mystery/);
46
+ });
32
47
  });
@@ -7,7 +7,7 @@ import { buildSystemOneTool } from "../src/tool.ts";
7
7
 
8
8
  describe("system_one tool", () => {
9
9
  it("delegates a mixed batch and preserves details", async () => {
10
- const tool: any = buildSystemOneTool({
10
+ const tool = buildSystemOneTool({
11
11
  provider: new MockSystemOneProvider({
12
12
  answers: {
13
13
  t: {
@@ -29,14 +29,14 @@ describe("system_one tool", () => {
29
29
  },
30
30
  undefined,
31
31
  undefined,
32
- {},
32
+ {} as never,
33
33
  );
34
34
  const text = JSON.stringify(res);
35
35
  assert.match(text, /"a"/);
36
- assert.equal(res.details.answers.t.choice, "a");
36
+ assert.equal((res.details.answers.t as { choice: string }).choice, "a");
37
37
  });
38
38
  it("propagates provider errors by throwing (never error-results)", async () => {
39
- const tool: any = buildSystemOneTool({
39
+ const tool = buildSystemOneTool({
40
40
  provider: {
41
41
  id: "boom",
42
42
  async evaluate() {
@@ -53,7 +53,7 @@ describe("system_one tool", () => {
53
53
  },
54
54
  undefined,
55
55
  undefined,
56
- {},
56
+ {} as never,
57
57
  ),
58
58
  /down/,
59
59
  );
@@ -88,7 +88,7 @@ describe("system_one tool", () => {
88
88
  };
89
89
  },
90
90
  };
91
- const tool: any = buildSystemOneTool({ provider: stub as any });
91
+ const tool = buildSystemOneTool({ provider: stub as any });
92
92
  const controller = new AbortController();
93
93
  const res = await tool.execute(
94
94
  "id-3",
@@ -110,11 +110,14 @@ describe("system_one tool", () => {
110
110
  },
111
111
  },
112
112
  controller.signal,
113
+ undefined,
114
+ {} as never,
113
115
  );
114
- assert.equal(res.details.answers.c.choice, "a");
115
- assert.equal(res.details.answers.n.noul, 0.8);
116
- assert.equal(res.details.answers.s.score, 1.5);
116
+ assert.equal((res.details.answers.c as { choice: string }).choice, "a");
117
+ assert.equal((res.details.answers.n as { noul: number }).noul, 0.8);
118
+ assert.equal((res.details.answers.s as { score: number }).score, 1.5);
117
119
  assert.deepEqual(res.details.usage, { inputTokens: 5, outputTokens: 2 });
120
+ assert.deepEqual(res.details.metadata, { provider: "stub" });
118
121
  assert.equal(seenRequest.model, "jev-latest");
119
122
  assert.equal(seenOptions.signal, controller.signal);
120
123
  });