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 +20 -1
- package/package.json +1 -1
- package/src/commands.ts +1 -1
- package/src/tool.ts +179 -49
- package/tests/commands.test.ts +6 -1
- package/tests/tool.test.ts +1 -2
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
|
-
|
|
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
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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
22
|
+
Type.Number(),
|
|
23
|
+
Type.Boolean(),
|
|
24
|
+
Type.Null(),
|
|
31
25
|
Type.Array(Type.Any()),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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?: {
|
|
42
|
-
|
|
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: {
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
52
|
-
"Use this when the
|
|
53
|
-
"
|
|
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
|
-
"
|
|
172
|
+
"Evaluate bounded choices, yes/no likelihoods, and ordered scores from shared state",
|
|
173
|
+
|
|
57
174
|
promptGuidelines: [
|
|
58
|
-
"Use system_one
|
|
59
|
-
"
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
69
|
-
questions: params.questions as
|
|
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
|
-
|
|
198
|
+
|
|
75
199
|
return {
|
|
76
|
-
content: [
|
|
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
|
}
|
package/tests/commands.test.ts
CHANGED
|
@@ -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(
|
|
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();
|
package/tests/tool.test.ts
CHANGED
|
@@ -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
|
});
|