pi-system-one 1.2.1 → 1.2.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
@@ -22,10 +22,13 @@ omp --extension /path/to/pi-system-one
22
22
 
23
23
  Set `SYSTEM_ONE_BASE_URL` before launch, or run `/so config`; the `system_one` tool is registered after the endpoint is configured.
24
24
 
25
- <img src="./demo.gif" alt="Description" width="640" >
25
+ <!-- <img src="./demo.gif" alt="Description" width="640" > -->
26
26
 
27
27
  ---
28
28
 
29
+ <img width="1836" height="1092" alt="demo" src="https://github.com/user-attachments/assets/341c0e4a-d0e5-4df0-85d2-74ca2f316db5" />
30
+
31
+
29
32
  Setup Provider:
30
33
  ##### locally hosted model e.g Reflex, von, laya etc, any model that supports `POST /v1/systemone`
31
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.1",
3
+ "version": "1.2.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",
@@ -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 + per-option probabilities +
42
- confidence. Add an `other` option when the set might not cover the
43
- input, and use `null` criteria values for self-explanatory labels.
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 name them for your own branching.
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
- export function renderSystemOneResult(response: {
4
- answers: Record<string, any>;
5
- }): string {
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(` ${k}: ${v}`);
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(` ${k}: ${v}`);
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
  }