@oisin-ee/oisinpi 0.10.2 → 0.11.1
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/CHANGELOG.md +20 -0
- package/node_modules/@oisin-ee/pi-guardrails/README.md +8 -0
- package/node_modules/@oisin-ee/pi-guardrails/dist/shell-parser.js +65 -2
- package/node_modules/@oisin-ee/pi-guardrails/package.json +1 -1
- package/node_modules/@oisin-ee/pi-guardrails/src/pi/index.ts +21 -7
- package/node_modules/@oisin-ee/pi-guardrails/src/shell-parser.ts +74 -2
- package/node_modules/@oisin-ee/pi-prewalk/package.json +1 -1
- package/node_modules/@oisin-ee/pi-tiers/package.json +1 -1
- package/node_modules/@oisin-ee/pi-todo/README.md +8 -0
- package/node_modules/@oisin-ee/pi-todo/package.json +1 -1
- package/node_modules/@oisin-ee/pi-todo/src/gate.ts +12 -3
- package/node_modules/@oisin-ee/pi-todo/src/guidance.ts +3 -1
- package/node_modules/@oisin-ee/pi-todo/src/runtime-fixture.ts +93 -42
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @oisin-ee/oisinpi
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`f1837f2`](https://github.com/oisin-ee/oisinpi/commit/f1837f28bc25fb00feebf7c47509db9bbd3b897c)]:
|
|
8
|
+
- @oisin-ee/pi-guardrails@0.11.1
|
|
9
|
+
- @oisin-ee/pi-prewalk@0.11.1
|
|
10
|
+
- @oisin-ee/pi-tiers@0.11.1
|
|
11
|
+
- @oisin-ee/pi-todo@0.11.1
|
|
12
|
+
|
|
13
|
+
## 0.11.0
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [[`49b00f1`](https://github.com/oisin-ee/oisinpi/commit/49b00f117d4d64a51cdb2b502ac505a99ffe83dc), [`0e37d82`](https://github.com/oisin-ee/oisinpi/commit/0e37d8259d4a7cb4cffbeccfce2a36caa0274b6e)]:
|
|
18
|
+
- @oisin-ee/pi-guardrails@0.11.0
|
|
19
|
+
- @oisin-ee/pi-todo@0.11.0
|
|
20
|
+
- @oisin-ee/pi-prewalk@0.11.0
|
|
21
|
+
- @oisin-ee/pi-tiers@0.11.0
|
|
22
|
+
|
|
3
23
|
## 0.10.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -13,6 +13,14 @@ Agent guardrails for [pi](https://pi.dev), Claude Code, and Codex. One set of po
|
|
|
13
13
|
In pi, it also fills a missing shell timeout (120 s) and blocks one above 10 minutes, naming each tool's unit.
|
|
14
14
|
Shell text is parsed with `shfmt`, which must be on `PATH`. A command that cannot be parsed is blocked.
|
|
15
15
|
|
|
16
|
+
## Planning models
|
|
17
|
+
|
|
18
|
+
The guardrails do not restrict a model by its identity. The planning selections `tier/plan-claude` and
|
|
19
|
+
`tier/plan-gpt` keep the full policy set: a planning model may run Git commands, write and edit files,
|
|
20
|
+
and use the workflow tools.
|
|
21
|
+
|
|
22
|
+
Planning restrictions belong to Plannotator. It owns plan mode, and it blocks writes while it plans.
|
|
23
|
+
|
|
16
24
|
## pi
|
|
17
25
|
|
|
18
26
|
```bash
|
|
@@ -95,7 +95,7 @@ pipelineStages = (stmt, source) => {
|
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
97
|
visit(tree);
|
|
98
|
-
return { calls, guards };
|
|
98
|
+
return { tree, calls, guards };
|
|
99
99
|
}, parsedOf = (command) => {
|
|
100
100
|
if (cache.command !== command || cache.parsed === undefined) {
|
|
101
101
|
cache.parsed = parse(command);
|
|
@@ -117,4 +117,67 @@ shellCalls = (command) => parsedOf(command).calls.map((words) => [...words]),
|
|
|
117
117
|
* @returns {(string[][] | null)[]} One guard per call.
|
|
118
118
|
*/
|
|
119
119
|
shellCallGuards = (command) => parsedOf(command).guards.map((stages) => stages?.map((words) => [...words]) ?? null);
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Whether a literal contains a glob character that the shell would expand. An
|
|
122
|
+
* escaped character is literal, so it does not expand.
|
|
123
|
+
* @param {string} value The raw literal text.
|
|
124
|
+
* @returns {boolean} True for an active glob character.
|
|
125
|
+
*/
|
|
126
|
+
function hasActiveGlob(value) {
|
|
127
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
128
|
+
const character = value[index] ?? "";
|
|
129
|
+
if (!"*?[]{}".includes(character)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let backslashes = 0;
|
|
133
|
+
for (let before = index - 1; before >= 0 && value[before] === "\\"; before -= 1) {
|
|
134
|
+
backslashes += 1;
|
|
135
|
+
}
|
|
136
|
+
if (backslashes % 2 === 0) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Returns literal calls only when the complete AST has read-only shell syntax.
|
|
144
|
+
* @param {string} command Shell text.
|
|
145
|
+
* @returns {string[][] | null} Literal argv, or null for unsupported syntax.
|
|
146
|
+
*/
|
|
147
|
+
function literalShellCalls(command) {
|
|
148
|
+
const parsed = parsedOf(command), allowed = new Set([
|
|
149
|
+
"File",
|
|
150
|
+
"Stmt",
|
|
151
|
+
"CallExpr",
|
|
152
|
+
"Word",
|
|
153
|
+
"Lit",
|
|
154
|
+
"SglQuoted",
|
|
155
|
+
"DblQuoted",
|
|
156
|
+
"BinaryCmd",
|
|
157
|
+
]), safe = (node, quoted = false) => {
|
|
158
|
+
if (Array.isArray(node)) {
|
|
159
|
+
return node.every((item) => safe(item, quoted));
|
|
160
|
+
}
|
|
161
|
+
if (!isNode(node)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (node.Type !== undefined && !allowed.has(node.Type)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (node.Dollar === true ||
|
|
168
|
+
node.Background === true ||
|
|
169
|
+
(node.Redirs ?? []).length > 0 ||
|
|
170
|
+
(node.Assigns ?? []).length > 0 ||
|
|
171
|
+
(node.Type === "CallExpr" && (node.Args ?? []).length === 0) ||
|
|
172
|
+
(node.Type === "BinaryCmd" && !["&&", "||", "|"].includes(node.Op ?? "")) ||
|
|
173
|
+
(node.Type === "Lit" && !quoted && hasActiveGlob(node.Value ?? "")) ||
|
|
174
|
+
(node.Type === "Lit" && !quoted && (node.Value ?? "").startsWith("~"))) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return Object.values(node).every((value) => safe(value, quoted || node.Type === "DblQuoted"));
|
|
178
|
+
};
|
|
179
|
+
return safe(parsed.tree) && parsed.calls.length > 0
|
|
180
|
+
? parsed.calls.map((words) => [...words])
|
|
181
|
+
: null;
|
|
182
|
+
}
|
|
183
|
+
export { literalShellCalls, shellCallGuards, shellCalls };
|
|
@@ -49,15 +49,29 @@ const MAX_NESTING = 8,
|
|
|
49
49
|
),
|
|
50
50
|
editViolation = (call: ToolCall, cwd: string): string | null =>
|
|
51
51
|
classifyEdits(editInputsFromPayload({ tool_input: call.input, tool_name: call.toolName }), cwd),
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The nested calls of a codemode input.
|
|
54
|
+
* @param {Readonly<Record<string, unknown>>} input The tool input.
|
|
55
|
+
* @returns {readonly ToolCall[]} The calls of the envelope.
|
|
56
|
+
*/
|
|
57
|
+
nestedCalls = (input: Readonly<Record<string, unknown>>): readonly ToolCall[] => {
|
|
53
58
|
const uses = input["tool_uses"];
|
|
54
59
|
if (!Array.isArray(uses)) {
|
|
55
60
|
return [];
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const calls: ToolCall[] = [];
|
|
63
|
+
for (const use of uses) {
|
|
64
|
+
if (!isRecord(use)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const toolName = use["recipient_name"];
|
|
68
|
+
const parameters = use["parameters"];
|
|
69
|
+
calls.push({
|
|
70
|
+
input: isRecord(parameters) ? parameters : {},
|
|
71
|
+
toolName: typeof toolName === "string" ? toolName : "",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return calls;
|
|
61
75
|
},
|
|
62
76
|
toolViolation = (event: ToolCall, cwd: string, depth = 0): string | null => {
|
|
63
77
|
if (depth > MAX_NESTING) {
|
|
@@ -72,8 +86,8 @@ const MAX_NESTING = 8,
|
|
|
72
86
|
if (own !== null) {
|
|
73
87
|
return own;
|
|
74
88
|
}
|
|
75
|
-
for (const
|
|
76
|
-
const message = toolViolation(
|
|
89
|
+
for (const child of nestedCalls(call.input)) {
|
|
90
|
+
const message = toolViolation(child, cwd, depth + 1);
|
|
77
91
|
if (message !== null) {
|
|
78
92
|
return message;
|
|
79
93
|
}
|
|
@@ -32,6 +32,7 @@ interface ShNode {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface Parsed {
|
|
35
|
+
readonly tree: unknown;
|
|
35
36
|
readonly calls: readonly Words[];
|
|
36
37
|
readonly guards: readonly Guard[];
|
|
37
38
|
}
|
|
@@ -145,7 +146,7 @@ const MAX_BUFFER = 16 * 1024 * 1024,
|
|
|
145
146
|
}
|
|
146
147
|
};
|
|
147
148
|
visit(tree);
|
|
148
|
-
return { calls, guards };
|
|
149
|
+
return { tree, calls, guards };
|
|
149
150
|
},
|
|
150
151
|
parsedOf = (command: string): Parsed => {
|
|
151
152
|
if (cache.command !== command || cache.parsed === undefined) {
|
|
@@ -170,4 +171,75 @@ const MAX_BUFFER = 16 * 1024 * 1024,
|
|
|
170
171
|
shellCallGuards = (command: string): (string[][] | null)[] =>
|
|
171
172
|
parsedOf(command).guards.map((stages) => stages?.map((words) => [...words]) ?? null);
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Whether a literal contains a glob character that the shell would expand. An
|
|
176
|
+
* escaped character is literal, so it does not expand.
|
|
177
|
+
* @param {string} value The raw literal text.
|
|
178
|
+
* @returns {boolean} True for an active glob character.
|
|
179
|
+
*/
|
|
180
|
+
function hasActiveGlob(value: string): boolean {
|
|
181
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
182
|
+
const character = value[index] ?? "";
|
|
183
|
+
if (!"*?[]{}".includes(character)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
let backslashes = 0;
|
|
187
|
+
for (let before = index - 1; before >= 0 && value[before] === "\\"; before -= 1) {
|
|
188
|
+
backslashes += 1;
|
|
189
|
+
}
|
|
190
|
+
if (backslashes % 2 === 0) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns literal calls only when the complete AST has read-only shell syntax.
|
|
199
|
+
* @param {string} command Shell text.
|
|
200
|
+
* @returns {string[][] | null} Literal argv, or null for unsupported syntax.
|
|
201
|
+
*/
|
|
202
|
+
function literalShellCalls(command: string): string[][] | null {
|
|
203
|
+
const parsed = parsedOf(command),
|
|
204
|
+
allowed = new Set([
|
|
205
|
+
"File",
|
|
206
|
+
"Stmt",
|
|
207
|
+
"CallExpr",
|
|
208
|
+
"Word",
|
|
209
|
+
"Lit",
|
|
210
|
+
"SglQuoted",
|
|
211
|
+
"DblQuoted",
|
|
212
|
+
"BinaryCmd",
|
|
213
|
+
]),
|
|
214
|
+
safe = (node: unknown, quoted = false): boolean => {
|
|
215
|
+
if (Array.isArray(node)) {
|
|
216
|
+
return node.every((item: unknown) => safe(item, quoted));
|
|
217
|
+
}
|
|
218
|
+
if (!isNode(node)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (node.Type !== undefined && !allowed.has(node.Type)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
node.Dollar === true ||
|
|
226
|
+
node.Background === true ||
|
|
227
|
+
(node.Redirs ?? []).length > 0 ||
|
|
228
|
+
(node.Assigns ?? []).length > 0 ||
|
|
229
|
+
(node.Type === "CallExpr" && (node.Args ?? []).length === 0) ||
|
|
230
|
+
(node.Type === "BinaryCmd" && !["&&", "||", "|"].includes(node.Op ?? "")) ||
|
|
231
|
+
(node.Type === "Lit" && !quoted && hasActiveGlob(node.Value ?? "")) ||
|
|
232
|
+
(node.Type === "Lit" && !quoted && (node.Value ?? "").startsWith("~"))
|
|
233
|
+
) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return Object.values(node).every((value: unknown) =>
|
|
237
|
+
safe(value, quoted || node.Type === "DblQuoted"),
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
return safe(parsed.tree) && parsed.calls.length > 0
|
|
241
|
+
? parsed.calls.map((words) => [...words])
|
|
242
|
+
: null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { literalShellCalls, shellCallGuards, shellCalls };
|
|
@@ -64,6 +64,14 @@ Untargeted `done` and `drop` affect open work only. A parent without `task` is n
|
|
|
64
64
|
Appending children to a closed parent reopens it as pending. An active parent stays active.
|
|
65
65
|
Outcome and subtask progress remain separate. Abandoned work never counts as completed.
|
|
66
66
|
|
|
67
|
+
## Activation gate
|
|
68
|
+
|
|
69
|
+
Shell, code, and mutation tools need one active local unit. Structured reads, workflow control, and `todo` calls stay available without one.
|
|
70
|
+
The gate checks the selected model at each call. The planner selections `tier/plan-claude` and `tier/plan-gpt` skip the activation check, so a planning session can read and inspect without a unit.
|
|
71
|
+
Every other selection needs an active unit, including a tier with another name and a physical model that only looks like a planner.
|
|
72
|
+
The exemption is a workflow prerequisite, not a permission. It does not grant write access, and other policies such as guardrails still apply.
|
|
73
|
+
An active unit stays required for the sessions that implement code.
|
|
74
|
+
|
|
67
75
|
## State
|
|
68
76
|
|
|
69
77
|
Each task change is saved as a `todo-state` entry on the session branch. A resumed session, a `/tree` move,
|
|
@@ -2,8 +2,17 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { TaskProvider } from "./provider.ts";
|
|
3
3
|
import { ownsTodo } from "./lifecycle.ts";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
const
|
|
5
|
+
/** Planning selections that skip the activation check, exact provider and model IDs. */
|
|
6
|
+
const planningSelections = new Set(["plan-claude", "plan-gpt"]),
|
|
7
|
+
/**
|
|
8
|
+
* Whether the selected model is a planning selection.
|
|
9
|
+
* @param {Readonly<object>} model Selected model, when the session has one.
|
|
10
|
+
* @returns {boolean} Whether the activation check does not apply.
|
|
11
|
+
*/
|
|
12
|
+
isPlanning = (model: { readonly provider?: string; readonly id?: string } | undefined): boolean =>
|
|
13
|
+
model?.provider === "tier" && planningSelections.has(model.id ?? ""),
|
|
14
|
+
/** Exact structured reads and workflow controls, never command-text inference. */
|
|
15
|
+
controls = new Set([
|
|
7
16
|
"ask",
|
|
8
17
|
"codemode",
|
|
9
18
|
"interview",
|
|
@@ -57,7 +66,7 @@ const controls = new Set([
|
|
|
57
66
|
return { block: false };
|
|
58
67
|
}
|
|
59
68
|
provider.context(ctx);
|
|
60
|
-
if (controls.has(event.toolName)) {
|
|
69
|
+
if (controls.has(event.toolName) || isPlanning(ctx.model)) {
|
|
61
70
|
return { block: false };
|
|
62
71
|
}
|
|
63
72
|
const active = provider.capability().active();
|
|
@@ -9,6 +9,8 @@ const workUnitGuidance =
|
|
|
9
9
|
"Use returned IDs after creation. Update drafts before start; only one complete self leaf executes locally. " +
|
|
10
10
|
"The lead closes any open leaf with done and a short note, or with drop. " +
|
|
11
11
|
"Keep content a short headline (aim under 60 characters, imperative, no trailing detail); " +
|
|
12
|
-
"put context, approach, or acceptance detail in the optional description (one or two sentences)."
|
|
12
|
+
"put context, approach, or acceptance detail in the optional description (one or two sentences). " +
|
|
13
|
+
"A planning session selected as tier/plan-claude or tier/plan-gpt needs no active unit. " +
|
|
14
|
+
"Every other session needs one active local unit before shell, code, or mutation tools.";
|
|
13
15
|
|
|
14
16
|
export { workUnitGuidance };
|
|
@@ -27,6 +27,12 @@ interface Startup {
|
|
|
27
27
|
readonly cwd: string;
|
|
28
28
|
readonly seedPath: string;
|
|
29
29
|
}
|
|
30
|
+
/** One selectable model in the runtime catalog. */
|
|
31
|
+
interface Selection {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly kind?: "physical" | "virtual";
|
|
34
|
+
readonly provider: string;
|
|
35
|
+
}
|
|
30
36
|
interface RuntimeFixture {
|
|
31
37
|
readonly session: Readonly<Pick<Session, "dispose" | "abort" | "messages">> & {
|
|
32
38
|
readonly extensionRunner: Readonly<Pick<Session["extensionRunner"], "emitToolResult" | "emit">>;
|
|
@@ -35,27 +41,32 @@ interface RuntimeFixture {
|
|
|
35
41
|
readonly provider: () => TaskProviderV1;
|
|
36
42
|
readonly call: (name: string, args: Readonly<Record<string, string>>) => Promise<void>;
|
|
37
43
|
readonly create: () => Promise<void>;
|
|
44
|
+
readonly select: (provider: string, id: string) => Promise<void>;
|
|
38
45
|
readonly todo: (params: TodoParams) => Promise<unknown>;
|
|
39
46
|
}
|
|
47
|
+
/** Plain description of one runtime fixture. */
|
|
48
|
+
interface FixtureRequest {
|
|
49
|
+
readonly collision: boolean;
|
|
50
|
+
readonly later: ExtensionFactory | undefined;
|
|
51
|
+
readonly selections: readonly Selection[];
|
|
52
|
+
readonly startup: Startup | undefined;
|
|
53
|
+
}
|
|
40
54
|
|
|
41
55
|
/**
|
|
42
56
|
* Creates resources without user configuration or external services.
|
|
43
|
-
* @param {
|
|
44
|
-
* @param {boolean} collision Whether the other todo owner loads first.
|
|
45
|
-
* @param {Startup | undefined} startup Optional worker startup configuration.
|
|
57
|
+
* @param {Readonly<FixtureRequest>} request Fixture description.
|
|
46
58
|
* @returns {object} Local resource owners.
|
|
47
59
|
*/
|
|
48
60
|
const resources = (
|
|
49
|
-
|
|
50
|
-
collision: boolean,
|
|
51
|
-
startup: Startup | undefined,
|
|
61
|
+
request: Readonly<FixtureRequest>,
|
|
52
62
|
): {
|
|
53
63
|
events: ReturnType<typeof createEventBus>;
|
|
54
64
|
manager: SessionManager;
|
|
55
65
|
settings: SettingsManager;
|
|
56
66
|
loader: DefaultResourceLoader;
|
|
57
67
|
} => {
|
|
58
|
-
const
|
|
68
|
+
const { collision, later, startup } = request,
|
|
69
|
+
events = createEventBus(),
|
|
59
70
|
manager = SessionManager.inMemory(),
|
|
60
71
|
settings = SettingsManager.inMemory({
|
|
61
72
|
compaction: { enabled: false },
|
|
@@ -130,47 +141,55 @@ const resources = (
|
|
|
130
141
|
);
|
|
131
142
|
},
|
|
132
143
|
/**
|
|
133
|
-
*
|
|
134
|
-
* @param {
|
|
135
|
-
* @
|
|
136
|
-
* @returns {RuntimeFixture["call"]} Request function.
|
|
144
|
+
* Creates the model catalog with the requested physical and virtual entries.
|
|
145
|
+
* @param {Readonly<FixtureRequest>} request Fixture description.
|
|
146
|
+
* @returns {object} Scripted provider and model catalog owner.
|
|
137
147
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
fauxAssistantMessage(fauxToolCall(name, args)),
|
|
146
|
-
fauxAssistantMessage("Done."),
|
|
147
|
-
]);
|
|
148
|
-
await session.prompt("Exercise one tool.");
|
|
149
|
-
},
|
|
150
|
-
/**
|
|
151
|
-
* Loads the real runner and codemode with a scripted model.
|
|
152
|
-
* @param {ExtensionFactory} later Additional extension.
|
|
153
|
-
* @param {boolean} collision Loads a competing owner first.
|
|
154
|
-
* @param {Startup | undefined} startup Optional worker startup configuration.
|
|
155
|
-
* @returns {Promise<RuntimeFixture>} Isolated runtime and callers.
|
|
156
|
-
*/
|
|
157
|
-
runtimeFixture = async (
|
|
158
|
-
later?: ExtensionFactory,
|
|
159
|
-
collision = false,
|
|
160
|
-
startup?: Startup,
|
|
161
|
-
): Promise<RuntimeFixture> => {
|
|
162
|
-
const setup = resources(later, collision, startup),
|
|
163
|
-
modelRuntime = await ModelRuntime.create({
|
|
148
|
+
catalog = async (
|
|
149
|
+
request: Readonly<FixtureRequest>,
|
|
150
|
+
): Promise<{
|
|
151
|
+
faux: ReturnType<typeof fauxProvider>;
|
|
152
|
+
modelRuntime: ModelRuntime;
|
|
153
|
+
}> => {
|
|
154
|
+
const modelRuntime = await ModelRuntime.create({
|
|
164
155
|
credentials: new InMemoryCredentialStore(),
|
|
165
156
|
modelsPath: null,
|
|
166
157
|
modelsStore: new InMemoryModelsStore(),
|
|
167
158
|
refreshOnCreate: false,
|
|
168
159
|
}),
|
|
169
|
-
faux = fauxProvider({
|
|
160
|
+
faux = fauxProvider({
|
|
161
|
+
models: [
|
|
162
|
+
...request.selections
|
|
163
|
+
.filter((selection) => selection.kind === "physical")
|
|
164
|
+
.map((selection) => ({ id: selection.id })),
|
|
165
|
+
{ id: "faux-1" },
|
|
166
|
+
],
|
|
167
|
+
tokensPerSecond: 0,
|
|
168
|
+
});
|
|
170
169
|
modelRuntime.registerNativeProvider(faux.provider);
|
|
170
|
+
for (const selection of request.selections.filter((entry) => entry.kind !== "physical")) {
|
|
171
|
+
modelRuntime.registerVirtualModel({
|
|
172
|
+
id: selection.id,
|
|
173
|
+
name: selection.id,
|
|
174
|
+
provider: selection.provider,
|
|
175
|
+
route: () => ({ model: faux.getModel(), thinkingLevel: "off" }),
|
|
176
|
+
thinkingLevels: ["off"],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
180
|
+
return { faux, modelRuntime };
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* Builds the real session and its callers over prepared resources.
|
|
184
|
+
* @param {Readonly<FixtureRequest>} request Fixture description.
|
|
185
|
+
* @returns {Promise<RuntimeFixture>} Isolated runtime and callers.
|
|
186
|
+
*/
|
|
187
|
+
buildFixture = async (request: Readonly<FixtureRequest>): Promise<RuntimeFixture> => {
|
|
188
|
+
const setup = resources(request),
|
|
189
|
+
{ faux, modelRuntime } = await catalog(request);
|
|
171
190
|
await setup.loader.reload();
|
|
172
191
|
const { session } = await createAgentSession({
|
|
173
|
-
cwd: startup?.cwd ?? "/tmp",
|
|
192
|
+
cwd: request.startup?.cwd ?? "/tmp",
|
|
174
193
|
model: faux.getModel(),
|
|
175
194
|
modelRuntime,
|
|
176
195
|
resourceLoader: setup.loader,
|
|
@@ -181,7 +200,13 @@ const resources = (
|
|
|
181
200
|
await session.bindExtensions({});
|
|
182
201
|
session.setActiveToolsByName([...session.getActiveToolNames(), "codemode"]);
|
|
183
202
|
return {
|
|
184
|
-
call:
|
|
203
|
+
call: async (name, args): Promise<void> => {
|
|
204
|
+
faux.setResponses([
|
|
205
|
+
fauxAssistantMessage(fauxToolCall(name, args)),
|
|
206
|
+
fauxAssistantMessage("Done."),
|
|
207
|
+
]);
|
|
208
|
+
await session.prompt("Exercise one tool.");
|
|
209
|
+
},
|
|
185
210
|
create: async (): Promise<void> => {
|
|
186
211
|
await todoCall(session, {
|
|
187
212
|
items: [{ content: "Build", contract: contract(), key: "build" }],
|
|
@@ -191,10 +216,36 @@ const resources = (
|
|
|
191
216
|
},
|
|
192
217
|
manager: setup.manager,
|
|
193
218
|
provider: () => acquire(setup.events),
|
|
219
|
+
select: async (provider, id): Promise<void> => {
|
|
220
|
+
const model = modelRuntime.getModel(provider, id);
|
|
221
|
+
if (model === undefined) {
|
|
222
|
+
throw new Error(`Unknown selection: ${provider}/${id}`);
|
|
223
|
+
}
|
|
224
|
+
await session.setModel(model);
|
|
225
|
+
},
|
|
194
226
|
session,
|
|
195
227
|
todo: async (params): Promise<unknown> => todoCall(session, params),
|
|
196
228
|
};
|
|
197
229
|
},
|
|
230
|
+
/**
|
|
231
|
+
* Loads the real runner and codemode with a scripted model.
|
|
232
|
+
* @param {ExtensionFactory} later Optional additional guard or test tool.
|
|
233
|
+
* @param {boolean} collision Whether the other todo owner loads first.
|
|
234
|
+
* @param {Startup | undefined} startup Optional worker startup configuration.
|
|
235
|
+
* @returns {Promise<RuntimeFixture>} Isolated runtime and callers.
|
|
236
|
+
*/
|
|
237
|
+
runtimeFixture = async (
|
|
238
|
+
later?: ExtensionFactory,
|
|
239
|
+
collision = false,
|
|
240
|
+
startup?: Startup,
|
|
241
|
+
): Promise<RuntimeFixture> => buildFixture({ collision, later, selections: [], startup }),
|
|
242
|
+
/**
|
|
243
|
+
* Loads the real runtime with selectable virtual and physical models.
|
|
244
|
+
* @param {readonly Selection[]} selections Selectable catalog entries.
|
|
245
|
+
* @returns {Promise<RuntimeFixture>} Runtime with the given selections.
|
|
246
|
+
*/
|
|
247
|
+
selectableFixture = async (selections: readonly Selection[]): Promise<RuntimeFixture> =>
|
|
248
|
+
buildFixture({ collision: false, later: undefined, selections, startup: undefined }),
|
|
198
249
|
/**
|
|
199
250
|
* Produces a deterministic tool result.
|
|
200
251
|
* @returns {object} Successful content.
|
|
@@ -205,5 +256,5 @@ const resources = (
|
|
|
205
256
|
}),
|
|
206
257
|
emptyParameters = Type.Object({});
|
|
207
258
|
|
|
208
|
-
export { emptyParameters, emptyResult, runtimeFixture };
|
|
209
|
-
export type { RuntimeFixture };
|
|
259
|
+
export { emptyParameters, emptyResult, runtimeFixture, selectableFixture };
|
|
260
|
+
export type { RuntimeFixture, Selection };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oisin-ee/oisinpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Oisin's pi packages in one install: prewalk, todo, tiers, and guardrails.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"guardrails",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
},
|
|
18
18
|
"type": "module",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@oisin-ee/pi-guardrails": "0.
|
|
21
|
-
"@oisin-ee/pi-prewalk": "0.
|
|
22
|
-
"@oisin-ee/pi-tiers": "0.
|
|
23
|
-
"@oisin-ee/pi-todo": "0.
|
|
20
|
+
"@oisin-ee/pi-guardrails": "0.11.1",
|
|
21
|
+
"@oisin-ee/pi-prewalk": "0.11.1",
|
|
22
|
+
"@oisin-ee/pi-tiers": "0.11.1",
|
|
23
|
+
"@oisin-ee/pi-todo": "0.11.1"
|
|
24
24
|
},
|
|
25
25
|
"bundleDependencies": [
|
|
26
26
|
"@oisin-ee/pi-guardrails",
|