pi-system-one 1.2.0 → 1.2.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/README.md +20 -1
- package/demo.gif +0 -0
- package/package.json +7 -2
- package/skills/system-one/SKILL.md +147 -9
- package/src/render.ts +70 -5
- package/src/tool.ts +740 -52
- package/tests/packaging.test.ts +13 -0
- package/tests/properties.test.ts +565 -0
- package/tests/render.test.ts +47 -0
- package/tests/tool.test.ts +1263 -62
package/README.md
CHANGED
|
@@ -6,10 +6,29 @@ System One decisions for Pi. Plug in TypeSafe Jev, Reflex, or your own provider.
|
|
|
6
6
|
pi install npm:pi-system-one
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
For Oh My Pi (OMP), install the published package with:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
omp install npm:pi-system-one
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For a local checkout, install or load the package directory directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
omp install /path/to/pi-system-one
|
|
19
|
+
# or
|
|
20
|
+
omp --extension /path/to/pi-system-one
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Set `SYSTEM_ONE_BASE_URL` before launch, or run `/so config`; the `system_one` tool is registered after the endpoint is configured.
|
|
24
|
+
|
|
25
|
+
<!-- <img src="./demo.gif" alt="Description" width="640" > -->
|
|
10
26
|
|
|
11
27
|
---
|
|
12
28
|
|
|
29
|
+
<img width="1836" height="1092" alt="demo" src="https://github.com/user-attachments/assets/341c0e4a-d0e5-4df0-85d2-74ca2f316db5" />
|
|
30
|
+
|
|
31
|
+
|
|
13
32
|
Setup Provider:
|
|
14
33
|
##### locally hosted model e.g Reflex, von, laya etc, any model that supports `POST /v1/systemone`
|
|
15
34
|
```bash
|
package/demo.gif
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-system-one",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"coding-agent"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"system-one-core": "^0.2.
|
|
30
|
+
"system-one-core": "^0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -37,5 +37,10 @@
|
|
|
37
37
|
"extensions": [
|
|
38
38
|
"./src/extension.ts"
|
|
39
39
|
]
|
|
40
|
+
},
|
|
41
|
+
"omp": {
|
|
42
|
+
"extensions": [
|
|
43
|
+
"./src/extension.ts"
|
|
44
|
+
]
|
|
40
45
|
}
|
|
41
46
|
}
|
|
@@ -32,50 +32,188 @@ reasoning.
|
|
|
32
32
|
One user message can hold several such judgments; batch them over the
|
|
33
33
|
same state in a single call.
|
|
34
34
|
|
|
35
|
+
## Atomic questions
|
|
36
|
+
|
|
37
|
+
Ask the most explicit, narrow, specific, atomic questions you can. This is
|
|
38
|
+
the single most important habit in the official guidance, because broad
|
|
39
|
+
questions hide several judgments behind one answer and you can neither
|
|
40
|
+
inspect nor tune what you cannot see.
|
|
41
|
+
|
|
42
|
+
"Is `message` spam?" is one broad question. It is really several:
|
|
43
|
+
|
|
44
|
+
- Does `message.body` ask the recipient to provide a password or other
|
|
45
|
+
login credential?
|
|
46
|
+
- Does `message.body` claim the recipient received an unexpected prize,
|
|
47
|
+
payment, or reward?
|
|
48
|
+
- Does `message.links[0].url` belong to a domain related to the sender?
|
|
49
|
+
|
|
50
|
+
Same call, same `state`, three answers you can weight, gate and log
|
|
51
|
+
separately. A broad question gives you one number you must trust and
|
|
52
|
+
cannot decompose.
|
|
53
|
+
|
|
54
|
+
Two habits follow from this:
|
|
55
|
+
|
|
56
|
+
- **State the exact condition.** The judge answers the question you
|
|
57
|
+
wrote, not the one you meant, and reads scoping words, negations and
|
|
58
|
+
implied conditions at face value. When you catch yourself explaining
|
|
59
|
+
what you really meant after a wrong answer, that explanation is the
|
|
60
|
+
missing half of the question. If interpretation is genuinely
|
|
61
|
+
unavoidable, ask two literal questions and combine them in code.
|
|
62
|
+
- **Reduce hops.** Questions about a property of a property lose
|
|
63
|
+
accuracy. Point at the state field by name rather than describing how
|
|
64
|
+
to find it.
|
|
65
|
+
|
|
35
66
|
## Which question type
|
|
36
67
|
|
|
37
68
|
Decide by the shape of the answer, not the difficulty of the judgment:
|
|
38
69
|
|
|
39
70
|
- **choice** — the answer is one item from a list (at least one
|
|
40
71
|
labelled option; keep sets small enough to judge — accuracy falls as
|
|
41
|
-
the set grows). Returns winner +
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
the set grows, and the ceiling is 255 options). Returns winner +
|
|
73
|
+
per-option probabilities + confidence.
|
|
74
|
+
**A choice always returns a winner**: the probabilities sum to 1, so
|
|
75
|
+
the closest irrelevant option still ranks first when nothing actually
|
|
76
|
+
fits. So when "does this exist / does anything apply" is a real
|
|
77
|
+
possibility, either add a `none`/`other` escape option, or ask that
|
|
78
|
+
separately as a `noul` in the same call and read it first. Ranked
|
|
79
|
+
alone, a choice cannot tell a real match from the nearest miss.
|
|
80
|
+
Give the candidates `null` criteria values when the labels speak for
|
|
81
|
+
themselves, but always *describe* the escape option.
|
|
44
82
|
- **score** — the answer is a position on an ordered scale (at least
|
|
45
83
|
two levels, each described concretely, low to high). Returns a fractional score
|
|
46
84
|
(probability-weighted mean — it can land between levels) + legend +
|
|
47
85
|
probabilities + confidence. Use when you will threshold, sort, or
|
|
48
|
-
average the result.
|
|
86
|
+
average the result. A scale can carry the decision outright: give it
|
|
87
|
+
a middle "let a person decide" level instead of fitting a threshold
|
|
88
|
+
to your own data, and the level descriptions are writable before you
|
|
89
|
+
have seen a single score.
|
|
49
90
|
- **noul** — the answer is yes or no. Returns one probability from 0 to
|
|
50
91
|
1 and **no confidence field**: the probability already is the
|
|
51
92
|
uncertainty measure (0.5 = maximally uncertain). Optional
|
|
52
93
|
`true`/`false` criteria spell out what yes/no mean. Ideal for gates.
|
|
53
94
|
|
|
95
|
+
A `choice` and a `noul` are not two views of the same quantity, and the
|
|
96
|
+
judge is deliberately *not* self-consistent between them. A choice over
|
|
97
|
+
options is **relative** — it settles which option, and its numbers move
|
|
98
|
+
when you add or remove an option. A noul is **absolute** — it can be low
|
|
99
|
+
for every option at once. So a noul of 0.2 and a choice of
|
|
100
|
+
`{"yes": 0.01, "no": 0.99}` are not contradictory, and
|
|
101
|
+
`P(noul) + P(not noul) != 1`. Never carry a threshold tuned on a noul
|
|
102
|
+
over to a choice, and don't expect the same question asked both ways to
|
|
103
|
+
return the same number.
|
|
104
|
+
|
|
105
|
+
If you want a safety net against a forced pick, ask a choice **and** a
|
|
106
|
+
noul existence check in the same call over the same shortlist, and read
|
|
107
|
+
the noul first.
|
|
108
|
+
|
|
109
|
+
### Describing levels and options with structure
|
|
110
|
+
|
|
111
|
+
`instructions` and the level/option descriptions may be a **string, an
|
|
112
|
+
object, an array, or null** — not just a string. Start with strings; they
|
|
113
|
+
are usually enough. When the judge keeps landing between two neighbouring
|
|
114
|
+
levels, give each level an object instead: one field for what it covers
|
|
115
|
+
and a field with a few example situations, using the **same field names
|
|
116
|
+
on every level** so the levels compare like with like.
|
|
117
|
+
|
|
118
|
+
The examples have to look like your real inputs to be worth anything. In
|
|
119
|
+
one measured case a level description with an on-topic example moved the
|
|
120
|
+
score from 1.43 to 1.03 and confidence from 0.35 to 0.96, while the same
|
|
121
|
+
level with an example unrelated to the input scored 1.43 at 0.35 —
|
|
122
|
+
identical to no example at all. Pick examples with known expected levels,
|
|
123
|
+
then test the revised descriptions on separate inputs before keeping them.
|
|
124
|
+
|
|
54
125
|
Yes/no with two named sides is a `noul`, not a two-option `choice`.
|
|
55
126
|
|
|
127
|
+
When several labels may apply at once, ask one `noul` per label rather
|
|
128
|
+
than one multi-answer `choice`: each answer is then an independent
|
|
129
|
+
P(yes) instead of a distribution that has to spend its mass.
|
|
130
|
+
|
|
131
|
+
## Letting code keep the work
|
|
132
|
+
|
|
133
|
+
The strongest recipes never ask the model to produce anything. They ask
|
|
134
|
+
it narrow questions about material that is already in `state`, and the
|
|
135
|
+
application assembles the result:
|
|
136
|
+
|
|
137
|
+
- **Pre-filter, then pick.** Find the candidate values in code first
|
|
138
|
+
(a regex, a search index, a parsed list) and offer only those, so
|
|
139
|
+
the option set is small and every option is something the model could
|
|
140
|
+
legitimately choose. The model cannot pick a value you omitted, and it
|
|
141
|
+
cannot invent or transpose one it was never shown.
|
|
142
|
+
- **Ask only what code cannot answer.** A quote missing from a document
|
|
143
|
+
is decided by a substring match, not a round trip; blank lines and
|
|
144
|
+
list markers are read in code. Spend judgments on the ambiguous part.
|
|
145
|
+
- **Select, don't generate.** To recover structure from flat text, ask
|
|
146
|
+
whether each line break split a sentence and what kind of block each
|
|
147
|
+
chunk is, then render in code — every character of the output then
|
|
148
|
+
comes from the input, and every judgment carries a probability.
|
|
149
|
+
- **Re-rank, don't search twice.** Cheap recall narrows thousands of
|
|
150
|
+
candidates to a shortlist; one judgment per shortlist entry orders it.
|
|
151
|
+
Compare each candidate against the query, not against its neighbours.
|
|
152
|
+
|
|
56
153
|
## Batching and state hygiene
|
|
57
154
|
|
|
58
155
|
- Batch every independent question over the same state into **one**
|
|
59
156
|
`system_one` call — one request reduces round-trips and lets the
|
|
60
|
-
provider batch or parallelize evaluation where supported.
|
|
157
|
+
provider batch or parallelize evaluation where supported. Adding
|
|
158
|
+
questions barely changes response time because they are evaluated in
|
|
159
|
+
parallel within the request.
|
|
160
|
+
- **Include the speculative ones.** Questions that only matter for some
|
|
161
|
+
inputs still belong in the batch: if the turn is a bug report you want
|
|
162
|
+
`bug_severity`, if it is a billing turn you want `refund_requested`.
|
|
163
|
+
An irrelevant answer costs almost nothing and is simply ignored by your
|
|
164
|
+
branch; a missing question costs a whole extra round trip. Coding
|
|
165
|
+
agents drift badly here — they fall into a one-question-per-call habit
|
|
166
|
+
much more than people writing this code by hand.
|
|
167
|
+
- Keep questions narrow enough that their answers can be recombined.
|
|
168
|
+
Batching only pays if each question is separately useful, and a batch
|
|
169
|
+
of three broad questions is three broad questions, not one decision.
|
|
61
170
|
- `state` holds the content plus supporting facts, and nothing else.
|
|
62
171
|
Judgments live in `questions`: text pasted into the state gets judged
|
|
63
172
|
as content, so never put the question itself in the state.
|
|
64
173
|
- Trim the state to what the decision needs. You pay per input token,
|
|
65
|
-
and accuracy falls as irrelevant detail grows.
|
|
174
|
+
and accuracy falls as irrelevant detail grows. Name state fields
|
|
175
|
+
rather than pasting prose, and point at them in `instructions` with a
|
|
176
|
+
backticked path (`ticket.messages[0].text`) so the judgment is about
|
|
177
|
+
the part you mean.
|
|
178
|
+
- Batching adds no run-to-run noise: the same questions answered
|
|
179
|
+
one-per-call and all-in-one return the same answers to the same
|
|
180
|
+
tolerance, so batching is a pure cost and latency win. It pays off
|
|
181
|
+
most when the state is a large document every call would resend.
|
|
182
|
+
- `state` is where the material lives, so put both sides of a comparison
|
|
183
|
+
in it (`entity_a`, `entity_b`) and phrase the questions about the
|
|
184
|
+
pair, never about one side on its own.
|
|
66
185
|
- Question ids are yours (e.g. `route`, `urgency`); the model never sees
|
|
67
|
-
them, so
|
|
186
|
+
them, so **the id is not a substitute for the question**. Write the
|
|
187
|
+
complete meaning in `instructions` even when the id is self-explanatory —
|
|
188
|
+
this is the single most common way a request goes wrong.
|
|
68
189
|
|
|
69
190
|
## Reading answers
|
|
70
191
|
|
|
71
192
|
- `confidence` (choice/score) is a spread statistic, not a feeling:
|
|
72
193
|
concentrated mass = high, flat spread = low. Low confidence means
|
|
73
194
|
ambiguous input, overlapping labels, or thin state — confirm or
|
|
74
|
-
escalate, don't silently discard.
|
|
195
|
+
escalate, don't silently discard. Across a whole call, read it as the
|
|
196
|
+
*least* certain judgment behind the answer: one wrong branch spoils
|
|
197
|
+
the call even when the rest is certain.
|
|
75
198
|
- Gate per action cost: high-confidence acts automatically, middling
|
|
76
199
|
confirms or flags, low goes to a human. Riskier actions get higher
|
|
77
200
|
bars. For `noul`, threshold on distance from 0.5 (e.g. act above 0.9
|
|
78
|
-
or below 0.1, review between).
|
|
201
|
+
or below 0.1, review between). A battery of hazard `noul`s usually
|
|
202
|
+
wants **two** thresholds each — at or above the action threshold it
|
|
203
|
+
fires, and at or above a lower one it goes to review first — so the
|
|
204
|
+
number is a policy constant in your code, not a word in a question.
|
|
205
|
+
- Check the answer's own order, too: highest confidence does not
|
|
206
|
+
override a conflicting explicit decision. If you only want the best
|
|
207
|
+
option, take the highest-confidence one instead of gating at all.
|
|
208
|
+
- **Higher confidence does not make an answer correct.** It means the
|
|
209
|
+
judge was sure, which is a statement about the distribution and not
|
|
210
|
+
about the world. A confident wrong answer is exactly what an untested
|
|
211
|
+
integration produces, so hold some back with known-answer examples.
|
|
212
|
+
- Ask each decision one way. Don't hold the judge to arithmetic identities
|
|
213
|
+
between separate questions (`P(A) + P(not A) = 1`) — the official
|
|
214
|
+
guidance treats these as structural invariants that are simply not
|
|
215
|
+
guaranteed, and the tension between them is worth surfacing in the
|
|
216
|
+
answer rather than averaging away.
|
|
79
217
|
- Start thresholds conservative, log decisions with their confidence,
|
|
80
218
|
and move the numbers once you see where wrong answers cluster. Pin
|
|
81
219
|
the backend model version once a threshold is tuned (`jev-latest`
|
package/src/render.ts
CHANGED
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
// biome-ignore-all lint/suspicious/noExplicitAny: intentional dynamic boundary over JSON protocol values
|
|
2
2
|
// pi-system-one/src/render.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* This is the only System One text the agent reads at call time. The skill
|
|
6
|
+
* explains how to read answers, but the skill loads on demand and may not be
|
|
7
|
+
* loaded at all, so anything needed to interpret a number has to appear here.
|
|
8
|
+
*
|
|
9
|
+
* The score answer is why that matters. The contract lets a score distribute
|
|
10
|
+
* over either index slots ("0".."n-1") or the level values themselves, and
|
|
11
|
+
* requires a legend labelling exactly the distributed keys (enforced in
|
|
12
|
+
* system-one-core validation). On the index-slot convention a bare
|
|
13
|
+
* `probabilities: 0: 0.57` names no level at all, so the legend is rendered
|
|
14
|
+
* inline next to its key rather than dropped.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Is this probability key an index slot rather than a self-describing label? */
|
|
18
|
+
function isSlotKey(key: string): boolean {
|
|
19
|
+
return /^\d+$/.test(key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderProbabilityLine(
|
|
23
|
+
key: string,
|
|
24
|
+
value: unknown,
|
|
25
|
+
legend: Record<string, unknown> | undefined,
|
|
26
|
+
): string {
|
|
27
|
+
// Only index slots need help identifying themselves; a key that is already
|
|
28
|
+
// the level text labels itself, and repeating it would just cost tokens.
|
|
29
|
+
const label = isSlotKey(key) ? legend?.[key] : undefined;
|
|
30
|
+
const described =
|
|
31
|
+
typeof label === "string" && label.trim() !== "" && label !== key
|
|
32
|
+
? `${key} ${label}`
|
|
33
|
+
: key;
|
|
34
|
+
return ` ${described}: ${String(value)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scoreScaleNote(
|
|
38
|
+
score: unknown,
|
|
39
|
+
probabilities: unknown,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (typeof score !== "number" || !Number.isFinite(score)) return undefined;
|
|
42
|
+
if (!probabilities || typeof probabilities !== "object") return undefined;
|
|
43
|
+
const count = Object.keys(probabilities as object).length;
|
|
44
|
+
if (count < 2) return undefined;
|
|
45
|
+
// The score is a position on the rubric, not a level index and not a
|
|
46
|
+
// percentage: it can land between levels. Say so, and give the range.
|
|
47
|
+
return ` (position on the 0..${count - 1} rubric scale; it can land between levels)`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renderSystemOneResult(
|
|
51
|
+
response: { answers: Record<string, any> },
|
|
52
|
+
discarded: string[] = [],
|
|
53
|
+
): string {
|
|
6
54
|
const lines = ["System One result", ""];
|
|
7
55
|
for (const [id, a] of Object.entries(response.answers)) {
|
|
8
56
|
if (a?.type === "choice") {
|
|
@@ -13,7 +61,7 @@ export function renderSystemOneResult(response: {
|
|
|
13
61
|
` probabilities:`,
|
|
14
62
|
);
|
|
15
63
|
for (const [k, v] of Object.entries(a.probabilities ?? {}))
|
|
16
|
-
lines.push(
|
|
64
|
+
lines.push(renderProbabilityLine(k, v, undefined));
|
|
17
65
|
} else if (a?.type === "noul") {
|
|
18
66
|
lines.push(`${id}:`, ` noul: ${a.noul}`);
|
|
19
67
|
} else if (a?.type === "score") {
|
|
@@ -22,15 +70,32 @@ export function renderSystemOneResult(response: {
|
|
|
22
70
|
` score: ${a.score}`,
|
|
23
71
|
` confidence: ${a.confidence}`,
|
|
24
72
|
);
|
|
73
|
+
const note = scoreScaleNote(a.score, a.probabilities);
|
|
74
|
+
if (note) lines.push(note);
|
|
25
75
|
if (a.probabilities && typeof a.probabilities === "object") {
|
|
26
76
|
lines.push(` probabilities:`);
|
|
27
77
|
for (const [k, v] of Object.entries(a.probabilities))
|
|
28
|
-
lines.push(
|
|
78
|
+
lines.push(renderProbabilityLine(k, v, a.legend));
|
|
29
79
|
}
|
|
30
80
|
} else {
|
|
31
81
|
lines.push(`${id}:`, ` unknown answer type: ${a?.type ?? "missing"}`);
|
|
32
82
|
}
|
|
33
83
|
lines.push("");
|
|
34
84
|
}
|
|
85
|
+
// One line, once, instead of a paragraph per answer: the three things an
|
|
86
|
+
// agent cannot infer from the numbers alone.
|
|
87
|
+
lines.push(
|
|
88
|
+
"noul is P(yes). A choice always returns a winner, so treat it as the best fit rather than proof one applies. confidence reflects the least certain judgment behind an answer and is not permission to act.",
|
|
89
|
+
);
|
|
90
|
+
if (discarded.length > 0) {
|
|
91
|
+
// Lead with this, not bury it: the answers above are complete and
|
|
92
|
+
// confident, and without this the model cannot tell that it asked for
|
|
93
|
+
// more than it got. Re-send those entries as proper questions.
|
|
94
|
+
lines.unshift(
|
|
95
|
+
"",
|
|
96
|
+
`NOTE: ${discarded.length} question entr${discarded.length === 1 ? "y was" : "ies were"} discarded during normalization and NOT judged: ${discarded.join("; ")}. Every entry above is a complete answer, but it does not cover what was dropped. Re-send ${discarded.length === 1 ? "that entry" : "those entries"} as a question object with type, instructions and criteria.`,
|
|
97
|
+
"",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
35
100
|
return lines.join("\n").trimEnd();
|
|
36
101
|
}
|