pi-system-one 1.1.3 → 1.1.4

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 CHANGED
@@ -7,4 +7,23 @@ pi install npm:pi-system-one
7
7
  export SYSTEM_ONE_BASE_URL=http://localhost:8008
8
8
  ```
9
9
 
10
- TypeSafe: `SYSTEM_ONE_BASE_URL=https://api.typesafe.ai SYSTEM_ONE_API_KEY=... SYSTEM_ONE_MODEL=jev-latest`.
10
+ **Configuration**
11
+
12
+ The extension provides a `/so` command for configuring the System One client:
13
+
14
+ - `SYSTEM_ONE_BASE_URL` – required endpoint URL.
15
+ - `SYSTEM_ONE_MODEL` – optional default model name.
16
+ - `SYSTEM_ONE_API_KEY` – optional API key.
17
+
18
+ When you run `/so config`, you can:
19
+
20
+ - **Enter a new API key** – it is stored in memory only and will be lost when the session ends.
21
+ - **Enter `-`** – forget the memory‑only key. If an `SYSTEM_ONE_API_KEY` environment variable is present, the client will fall back to it; otherwise the key becomes absent.
22
+ - **Leave the input empty** – keep the existing value.
23
+
24
+ The current configuration can be inspected with `/so status`.
25
+
26
+ ---
27
+
28
+ For further details see the source code and comments in `src/commands.ts`.
29
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-system-one",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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",
package/src/commands.ts CHANGED
@@ -59,7 +59,7 @@ export function registerSystemOneCommands(
59
59
  return;
60
60
  }
61
61
  const apiKeyRaw = await ctx.ui.input(
62
- "SYSTEM_ONE_API_KEY (empty = keep, - = forget memory key, memory-only, gone when the session closes):",
62
+ "SYSTEM_ONE_API_KEY (empty = keep, - = forget memory key (reverts to environment variable if set, otherwise becomes absent), memory-only, gone when the session closes):",
63
63
  "",
64
64
  );
65
65
  if (apiKeyRaw === undefined) {
package/src/tool.ts CHANGED
@@ -1,87 +1,217 @@
1
- // biome-ignore-all lint/suspicious/noExplicitAny: intentional dynamic boundary over JSON protocol values
2
- // pi-system-one/src/tool.ts
3
-
4
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
5
- import { SystemOne, type SystemOneProvider } from "system-one-core";
2
+ import {
3
+ type QuestionMap,
4
+ SystemOne,
5
+ type SystemOneProvider,
6
+ type SystemOneState,
7
+ } from "system-one-core";
6
8
  import type { Static } from "typebox";
7
9
  import { Type } from "typebox";
8
10
  import { renderSystemOneResult } from "./render.ts";
9
11
 
10
- const ChoiceQ = Type.Object({
11
- type: Type.Literal("choice"),
12
- instructions: Type.Any(),
13
- criteria: Type.Record(Type.String(), Type.Union([Type.Any(), Type.Null()])),
14
- });
15
- const NoulQ = Type.Object({
16
- type: Type.Literal("noul"),
17
- instructions: Type.Any(),
18
- criteria: Type.Optional(
19
- Type.Record(Type.String(), Type.Union([Type.Any(), Type.Null()])),
20
- ),
21
- });
22
- const ScoreQ = Type.Object({
23
- type: Type.Literal("score"),
24
- instructions: Type.Any(),
25
- criteria: Type.Array(Type.Any()),
26
- });
27
- export const systemOneParams = Type.Object({
28
- state: Type.Union([
12
+ /**
13
+ * JSON value accepted by System One.
14
+ *
15
+ * Arrays/objects use Type.Any() recursively because tool arguments already
16
+ * cross a JSON boundary. Keeping the schema non-recursive also tends to be
17
+ * easier for tool-calling models to consume.
18
+ */
19
+ const JsonValue = Type.Union(
20
+ [
29
21
  Type.String(),
30
- Type.Record(Type.String(), Type.Any()),
22
+ Type.Number(),
23
+ Type.Boolean(),
24
+ Type.Null(),
31
25
  Type.Array(Type.Any()),
32
- ]),
33
- questions: Type.Record(Type.String(), Type.Union([ChoiceQ, NoulQ, ScoreQ])),
34
- model: Type.Optional(Type.String()),
35
- });
26
+ Type.Record(Type.String(), Type.Any()),
27
+ ],
28
+ {
29
+ description:
30
+ "A JSON value: string, number, boolean, null, object, or array.",
31
+ },
32
+ );
33
+
34
+ const ChoiceQ = Type.Object(
35
+ {
36
+ type: Type.Literal("choice", {
37
+ description: "Select exactly one outcome from a bounded set.",
38
+ }),
39
+
40
+ instructions: JsonValue,
41
+
42
+ criteria: Type.Record(Type.String(), JsonValue, {
43
+ minProperties: 1,
44
+ description:
45
+ "Available choices keyed by choice label. Each value describes that choice; use null when the label is self-explanatory. The keys are the choices; do not provide a separate options field.",
46
+ }),
47
+ },
48
+ {
49
+ additionalProperties: false,
50
+ description:
51
+ "A closed-set categorical decision. The keys of criteria are the available choices.",
52
+ },
53
+ );
54
+
55
+ const NoulCriteria = Type.Object(
56
+ {
57
+ true: Type.Optional(JsonValue),
58
+ false: Type.Optional(JsonValue),
59
+ },
60
+ {
61
+ additionalProperties: false,
62
+ minProperties: 1,
63
+ description: "Optional descriptions of the true/yes and false/no outcomes.",
64
+ },
65
+ );
66
+
67
+ const NoulQ = Type.Object(
68
+ {
69
+ type: Type.Literal("noul", {
70
+ description: "Estimate the likelihood of a yes/true outcome.",
71
+ }),
72
+
73
+ instructions: JsonValue,
74
+
75
+ criteria: Type.Optional(NoulCriteria),
76
+ },
77
+ {
78
+ additionalProperties: false,
79
+ description:
80
+ "A probabilistic yes/no judgment. Omit criteria unless the true or false outcomes need clarification.",
81
+ },
82
+ );
83
+
84
+ const ScoreQ = Type.Object(
85
+ {
86
+ type: Type.Literal("score", {
87
+ description: "Score the state against an ordered rubric.",
88
+ }),
89
+
90
+ instructions: JsonValue,
91
+
92
+ criteria: Type.Array(JsonValue, {
93
+ minItems: 2,
94
+ description:
95
+ "Ordered score levels from lowest to highest. Each array item defines one level of the rubric.",
96
+ }),
97
+ },
98
+ {
99
+ additionalProperties: false,
100
+ description:
101
+ "An ordered rubric decision. Criteria are ordered from lowest to highest.",
102
+ },
103
+ );
104
+
105
+ const Question = Type.Union([ChoiceQ, NoulQ, ScoreQ]);
106
+
107
+ export const systemOneParams = Type.Object(
108
+ {
109
+ state: Type.Union(
110
+ [
111
+ Type.String(),
112
+ Type.Number(),
113
+ Type.Boolean(),
114
+ Type.Null(),
115
+ Type.Record(Type.String(), Type.Any()),
116
+ Type.Array(Type.Any()),
117
+ ],
118
+ {
119
+ description:
120
+ "Shared state or evidence that all questions should be evaluated against.",
121
+ },
122
+ ),
123
+
124
+ questions: Type.Record(Type.String(), Question, {
125
+ minProperties: 1,
126
+ description:
127
+ "One or more named decision questions. Questions sharing the same state should be sent together.",
128
+ }),
129
+ },
130
+ {
131
+ additionalProperties: false,
132
+ description: "Evaluate bounded decision questions against shared state.",
133
+ },
134
+ );
135
+
36
136
  export type SystemOneParams = Static<typeof systemOneParams>;
37
137
 
38
138
  export interface SystemOneDetails {
39
139
  answers: Record<string, unknown>;
40
140
  model?: string;
41
- usage?: { inputTokens?: number; outputTokens?: number };
42
- metadata: { provider: string; latencyMs?: number };
141
+ usage?: {
142
+ inputTokens?: number;
143
+ outputTokens?: number;
144
+ };
145
+ requestId?: string;
146
+ metadata: {
147
+ provider: string;
148
+ latencyMs?: number;
149
+ };
43
150
  }
44
151
 
45
- export function buildSystemOneTool(deps: { provider: SystemOneProvider }) {
46
- const systemOne = new SystemOne({ provider: deps.provider });
47
- const tool: ToolDefinition<typeof systemOneParams, SystemOneDetails> = {
152
+ export function buildSystemOneTool(deps: {
153
+ provider: SystemOneProvider;
154
+ }): ToolDefinition<typeof systemOneParams, SystemOneDetails> {
155
+ const systemOne = new SystemOne({
156
+ provider: deps.provider,
157
+ });
158
+
159
+ return {
48
160
  name: "system_one",
49
161
  label: "System One",
162
+
50
163
  description:
51
- "Evaluate one or more bounded decision questions against shared state using the configured System One provider. " +
52
- "Use this when the answer space is known and a fast probabilistic decision is preferable to generating free-form text. " +
53
- "Multiple independent choice, noul, and score questions should be batched into one call when they share the same state.",
164
+ "Evaluate bounded choice, yes/no likelihood, and ordered score questions against shared state. " +
165
+ "Use this when the possible outcomes or scoring rubric are known and structured probabilistic results are useful. " +
166
+ "Do not use it for open-ended text generation. " +
167
+ "Questions that share the same state can be evaluated together in one call.",
168
+
54
169
  parameters: systemOneParams,
170
+
55
171
  promptSnippet:
56
- "Perform calibrated structured decisions and classifications over state",
172
+ "Evaluate bounded choices, yes/no likelihoods, and ordered scores from shared state",
173
+
57
174
  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.",
175
+ "Use system_one for closed-set classification, probabilistic yes/no judgments, or ordered rubric scoring.",
176
+ "Do not use system_one for open-ended text generation or when there is no bounded answer space or rubric.",
177
+ "Preserve user-specified choice labels, criteria, and score levels instead of inventing replacements.",
178
+ "For choice questions, the keys of criteria are the available choices; never add a separate options field.",
179
+ "For self-explanatory choice labels, prefer null criteria values instead of inventing definitions that could bias the decision.",
180
+ "For noul questions, omit criteria unless the true/yes or false/no outcomes need clarification.",
181
+ "For score questions, criteria must contain at least two ordered levels from lowest to highest.",
182
+ "Batch independent questions into one system_one call when they share the same state.",
60
183
  ],
61
184
 
62
- async execute(toolCallId, params, signal, _onUpdate, _ctx) {
63
- void toolCallId;
64
- // Throw on failure per Pi contract — never encode errors in content.
65
- // Safe to propagate unwrapped: core error messages/codes never contain secrets.
185
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
186
+ /*
187
+ * The LLM-facing TypeBox schema is intentionally separate from the
188
+ * provider protocol types. Cast only at this boundary instead of
189
+ * spreading `any` throughout the implementation.
190
+ */
66
191
  const response = await systemOne.evaluate(
67
192
  {
68
- state: params.state as any,
69
- questions: params.questions as any,
70
- ...(params.model ? { model: params.model } : {}),
193
+ state: params.state as SystemOneState,
194
+ questions: params.questions as QuestionMap,
71
195
  },
72
196
  { signal },
73
197
  );
74
- const text = renderSystemOneResult(response);
198
+
75
199
  return {
76
- content: [{ type: "text" as const, text }],
200
+ content: [
201
+ {
202
+ type: "text" as const,
203
+ text: renderSystemOneResult(response),
204
+ },
205
+ ],
206
+
77
207
  details: {
78
208
  answers: response.answers,
79
209
  model: response.model,
80
210
  usage: response.usage,
211
+ requestId: response.requestId,
81
212
  metadata: response.metadata,
82
213
  },
83
214
  };
84
215
  },
85
216
  };
86
- return tool;
87
217
  }
@@ -109,8 +109,13 @@ describe("so command", () => {
109
109
  await cap.handler?.("", ctxFor(["", "", "sk-mem", ""], cap));
110
110
  assert.equal(store.session?.keyInMemory, true);
111
111
  await cap.handler?.("", ctxFor(["", "", "-", ""], cap));
112
+ // After forgetting the memory key, the API key should revert to the environment variable (if any).
113
+ // In the CI environment, SYSTEM_ONE_API_KEY may be set; otherwise it will be undefined.
112
114
  assert.equal(store.session?.keyInMemory, false);
113
- assert.equal(store.session?.current.apiKey, undefined);
115
+ assert.equal(
116
+ store.session?.current.apiKey,
117
+ process.env.SYSTEM_ONE_API_KEY || undefined,
118
+ );
114
119
  });
115
120
  it("status shows current config; STATUS routes case-insensitively", async () => {
116
121
  const { pi, cap } = harness();
@@ -94,7 +94,6 @@ describe("system_one tool", () => {
94
94
  "id-3",
95
95
  {
96
96
  state: "hi",
97
- model: "jev-latest",
98
97
  questions: {
99
98
  c: {
100
99
  type: "choice",
@@ -118,7 +117,7 @@ describe("system_one tool", () => {
118
117
  assert.equal((res.details.answers.s as { score: number }).score, 1.5);
119
118
  assert.deepEqual(res.details.usage, { inputTokens: 5, outputTokens: 2 });
120
119
  assert.deepEqual(res.details.metadata, { provider: "stub" });
121
- assert.equal(seenRequest.model, "jev-latest");
122
120
  assert.equal(seenOptions.signal, controller.signal);
121
+ assert.equal(seenRequest.state, "hi");
123
122
  });
124
123
  });