pi-system-one 1.1.2 → 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 +126 -0
- package/src/config.ts +86 -6
- package/src/extension.ts +20 -14
- package/src/render.ts +8 -1
- package/src/tool.ts +189 -45
- package/tests/commands.test.ts +135 -0
- package/tests/config.test.ts +122 -14
- package/tests/render.test.ts +15 -0
- package/tests/tool.test.ts +13 -11
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
ADDED
|
@@ -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 (reverts to environment variable if set, otherwise becomes absent), 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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
`
|
|
38
|
-
|
|
39
|
-
`
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,71 +1,215 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type QuestionMap,
|
|
4
|
+
SystemOne,
|
|
5
|
+
type SystemOneProvider,
|
|
6
|
+
type SystemOneState,
|
|
7
|
+
} from "system-one-core";
|
|
5
8
|
import type { Static } from "typebox";
|
|
6
9
|
import { Type } from "typebox";
|
|
7
10
|
import { renderSystemOneResult } from "./render.ts";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Type.Record(Type.String(), Type.Union([Type.Any(), Type.Null()])),
|
|
19
|
-
),
|
|
20
|
-
});
|
|
21
|
-
const ScoreQ = Type.Object({
|
|
22
|
-
type: Type.Literal("score"),
|
|
23
|
-
instructions: Type.Any(),
|
|
24
|
-
criteria: Type.Array(Type.Any()),
|
|
25
|
-
});
|
|
26
|
-
export const systemOneParams = Type.Object({
|
|
27
|
-
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
|
+
[
|
|
28
21
|
Type.String(),
|
|
29
|
-
Type.
|
|
22
|
+
Type.Number(),
|
|
23
|
+
Type.Boolean(),
|
|
24
|
+
Type.Null(),
|
|
30
25
|
Type.Array(Type.Any()),
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
|
|
35
136
|
export type SystemOneParams = Static<typeof systemOneParams>;
|
|
36
137
|
|
|
37
|
-
export
|
|
38
|
-
|
|
138
|
+
export interface SystemOneDetails {
|
|
139
|
+
answers: Record<string, unknown>;
|
|
140
|
+
model?: string;
|
|
141
|
+
usage?: {
|
|
142
|
+
inputTokens?: number;
|
|
143
|
+
outputTokens?: number;
|
|
144
|
+
};
|
|
145
|
+
requestId?: string;
|
|
146
|
+
metadata: {
|
|
147
|
+
provider: string;
|
|
148
|
+
latencyMs?: number;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function buildSystemOneTool(deps: {
|
|
153
|
+
provider: SystemOneProvider;
|
|
154
|
+
}): ToolDefinition<typeof systemOneParams, SystemOneDetails> {
|
|
155
|
+
const systemOne = new SystemOne({
|
|
156
|
+
provider: deps.provider,
|
|
157
|
+
});
|
|
158
|
+
|
|
39
159
|
return {
|
|
40
160
|
name: "system_one",
|
|
41
161
|
label: "System One",
|
|
162
|
+
|
|
42
163
|
description:
|
|
43
|
-
"Evaluate
|
|
44
|
-
"Use this when the
|
|
45
|
-
"
|
|
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
|
+
|
|
46
169
|
parameters: systemOneParams,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
170
|
+
|
|
171
|
+
promptSnippet:
|
|
172
|
+
"Evaluate bounded choices, yes/no likelihoods, and ordered scores from shared state",
|
|
173
|
+
|
|
174
|
+
promptGuidelines: [
|
|
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.",
|
|
183
|
+
],
|
|
184
|
+
|
|
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
|
+
*/
|
|
54
191
|
const response = await systemOne.evaluate(
|
|
55
192
|
{
|
|
56
|
-
state: params.state as
|
|
57
|
-
questions: params.questions as
|
|
58
|
-
...(params.model ? { model: params.model } : {}),
|
|
193
|
+
state: params.state as SystemOneState,
|
|
194
|
+
questions: params.questions as QuestionMap,
|
|
59
195
|
},
|
|
60
196
|
{ signal },
|
|
61
197
|
);
|
|
62
|
-
|
|
198
|
+
|
|
63
199
|
return {
|
|
64
|
-
content: [
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "text" as const,
|
|
203
|
+
text: renderSystemOneResult(response),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
|
|
65
207
|
details: {
|
|
66
208
|
answers: response.answers,
|
|
67
209
|
model: response.model,
|
|
68
210
|
usage: response.usage,
|
|
211
|
+
requestId: response.requestId,
|
|
212
|
+
metadata: response.metadata,
|
|
69
213
|
},
|
|
70
214
|
};
|
|
71
215
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
// 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.
|
|
114
|
+
assert.equal(store.session?.keyInMemory, false);
|
|
115
|
+
assert.equal(
|
|
116
|
+
store.session?.current.apiKey,
|
|
117
|
+
process.env.SYSTEM_ONE_API_KEY || undefined,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
it("status shows current config; STATUS routes case-insensitively", async () => {
|
|
121
|
+
const { pi, cap } = harness();
|
|
122
|
+
const store: SessionStore = {
|
|
123
|
+
session: createSessionConfig({ SYSTEM_ONE_BASE_URL: "http://x/" }),
|
|
124
|
+
};
|
|
125
|
+
registerSystemOneCommands(pi, store);
|
|
126
|
+
await cap.handler?.("STATUS", ctxFor([], cap));
|
|
127
|
+
assert.match(cap.notices[0], /http:\/\/x\//);
|
|
128
|
+
});
|
|
129
|
+
it("status with no session explains how to configure", async () => {
|
|
130
|
+
const { pi, cap } = harness();
|
|
131
|
+
registerSystemOneCommands(pi, {});
|
|
132
|
+
await cap.handler?.("status", ctxFor([], cap));
|
|
133
|
+
assert.match(cap.notices[0], /not configured/);
|
|
134
|
+
});
|
|
135
|
+
});
|
package/tests/config.test.ts
CHANGED
|
@@ -2,27 +2,135 @@
|
|
|
2
2
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
|
-
import {
|
|
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
|
|
9
|
-
const
|
|
14
|
+
it("loads from env", () => {
|
|
15
|
+
const s = createSessionConfig({
|
|
10
16
|
SYSTEM_ONE_BASE_URL: "http://localhost:8008",
|
|
11
17
|
});
|
|
12
|
-
assert.equal(
|
|
13
|
-
assert.equal(
|
|
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("
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
23
|
-
assert.
|
|
81
|
+
assert.equal(s.keyInMemory, false);
|
|
82
|
+
assert.equal(s.current.apiKey, undefined);
|
|
24
83
|
});
|
|
25
|
-
it("
|
|
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
|
});
|
package/tests/render.test.ts
CHANGED
|
@@ -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
|
});
|
package/tests/tool.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,13 +88,12 @@ describe("system_one tool", () => {
|
|
|
88
88
|
};
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
|
-
const tool
|
|
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",
|
|
95
95
|
{
|
|
96
96
|
state: "hi",
|
|
97
|
-
model: "jev-latest",
|
|
98
97
|
questions: {
|
|
99
98
|
c: {
|
|
100
99
|
type: "choice",
|
|
@@ -110,12 +109,15 @@ describe("system_one tool", () => {
|
|
|
110
109
|
},
|
|
111
110
|
},
|
|
112
111
|
controller.signal,
|
|
112
|
+
undefined,
|
|
113
|
+
{} as never,
|
|
113
114
|
);
|
|
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);
|
|
115
|
+
assert.equal((res.details.answers.c as { choice: string }).choice, "a");
|
|
116
|
+
assert.equal((res.details.answers.n as { noul: number }).noul, 0.8);
|
|
117
|
+
assert.equal((res.details.answers.s as { score: number }).score, 1.5);
|
|
117
118
|
assert.deepEqual(res.details.usage, { inputTokens: 5, outputTokens: 2 });
|
|
118
|
-
assert.
|
|
119
|
+
assert.deepEqual(res.details.metadata, { provider: "stub" });
|
|
119
120
|
assert.equal(seenOptions.signal, controller.signal);
|
|
121
|
+
assert.equal(seenRequest.state, "hi");
|
|
120
122
|
});
|
|
121
123
|
});
|