pi-delegation-policy 0.4.1 → 0.6.0
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 +22 -0
- package/README.md +19 -19
- package/SECURITY.md +1 -1
- package/examples/global.json +2 -5
- package/package.json +1 -1
- package/schema/delegation-policy.schema.json +7 -4
- package/src/config.ts +234 -74
- package/src/delegate-panel.ts +22 -16
- package/src/index.ts +41 -23
- package/src/prompt.ts +77 -22
- package/src/runtime.ts +41 -27
- package/src/types.ts +20 -15
- package/src/ui.ts +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.0 - 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Allow Small, Medium, and Large to be explicitly disabled independently while requiring one enabled ordinary role for an active policy.
|
|
10
|
+
- Add schema 3 defaults and session state, with in-memory schema 2 migration and guarded session writes for safer downgrade behavior. Downgrading after saving schema 3 defaults or manually editing them requires `/delegate off` in each active branch and manual conversion of ordinary `null` values.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Select only enabled ordinary roles that can satisfy the task, leaving work with the main agent when none can; Small/Medium preferences are inactive when either role is disabled.
|
|
15
|
+
|
|
16
|
+
## 0.5.0 - 2026-08-28
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Expanded the optional visual specialist to create assets and complete bounded presentation-layer changes when behavior and data contracts are already defined.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Renamed the user-facing UI Design role to Visual Design while preserving the `uiDesign` configuration key and `ui-design` status token.
|
|
25
|
+
- Clarified routing boundaries for application behavior, accessibility, checks, integration, and final acceptance.
|
|
26
|
+
|
|
5
27
|
## 0.4.1 - 2026-08-28
|
|
6
28
|
|
|
7
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# pi-delegation-policy
|
|
2
2
|
|
|
3
|
-
A local Pi extension that helps the main agent decide **when delegation is worth it** and which exact models to use for
|
|
3
|
+
A local Pi extension that helps the main agent decide **when delegation is worth it** and which exact models to use for Small, Medium, Large, and optional Visual Design. It provides guidance; it is not a subagent runner.
|
|
4
4
|
|
|
5
|
-
> **Status:** Version **0.
|
|
5
|
+
> **Status:** Version **0.6.0** is the latest published package. The package requires Pi `>=0.84.3`; Pi `0.84.3` is the explicitly checked baseline.
|
|
6
6
|
>
|
|
7
7
|
> **Docs:** [Read the documentation site](https://yivas.github.io/pi-delegation-policy/).
|
|
8
8
|
|
|
9
9
|
## Value and boundary
|
|
10
10
|
|
|
11
|
-
- Choose `off`, `normal`, or `aggressive`
|
|
12
|
-
- Configure exact provider/model
|
|
11
|
+
- Choose `off`, `normal`, or `aggressive` globally or for the current session branch.
|
|
12
|
+
- Configure an exact provider/model reference or explicitly disable each ordinary role.
|
|
13
13
|
- Keep global defaults and session-branch overrides across reload, resume, and tree navigation.
|
|
14
14
|
- Validate active configurations before injecting one policy block through Pi's public `before_agent_start` event.
|
|
15
15
|
|
|
16
16
|
The extension guides the main agent. It never creates, launches, routes, supervises, or blocks subagents; changes Pi's main model or thinking; stores credentials; intercepts tools; or makes its own network requests. It has no model fallback, telemetry, project configuration, presets, or external skill loading.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
A valid active policy requires an explicit decision for Small, Medium, and Large: an exact model reference or disabled. At least one ordinary role must remain enabled. A disabled role is not validated. An absent role, an invalid enabled reference, or no enabled ordinary role produces `D:ERR` and injects no policy. `off` always produces `D:OFF` without injection.
|
|
19
|
+
|
|
20
|
+
The policy considers only enabled ordinary roles, chooses the least costly role that can satisfy the task's acceptance criteria and evidence, and keeps work with the main agent when none can. It never invents a model or role. `efficient` and `intensive` are tie-breaks only when both Small and Medium are enabled; otherwise their bias is inactive.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
Visual Design is an independent optional specialist for direction, assets, bounded presentation-layer implementation, and visual review. Use it only when behavior and data contracts are already defined and unchanged, the affected surface is bounded, and visual quality or user experience is the primary acceptance criterion. It does not count as an ordinary role or replace one. Route business logic, data, APIs, routes, application architecture, tooling, interaction behavior, and cross-system integration to an enabled ordinary role by task fit. The main agent retains final integration and acceptance.
|
|
23
|
+
|
|
24
|
+
## Install and start
|
|
21
25
|
|
|
22
26
|
```bash
|
|
23
27
|
pi install npm:pi-delegation-policy
|
|
@@ -25,12 +29,14 @@ pi install npm:pi-delegation-policy
|
|
|
25
29
|
```
|
|
26
30
|
|
|
27
31
|
1. Open `/delegate` (or press `Alt+G` in Pi's TUI).
|
|
28
|
-
2.
|
|
32
|
+
2. For Small, Medium, and Large, select an exact authenticated provider/model or **Disable for this session**. Keep at least one enabled.
|
|
29
33
|
3. Select `normal` or `aggressive`, then choose **Apply changes**.
|
|
30
|
-
4. Run `/delegate status`
|
|
31
|
-
5. The applied state affects the **next** agent run.
|
|
34
|
+
4. Run `/delegate status`. `disabled`, `not configured`, and exact references remain distinct. `D:ERR` means no policy is injected.
|
|
35
|
+
5. The applied state affects the **next** agent run.
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
Global defaults are stored at `~/.pi/agent/delegation-policy.json` and use schema version 3. Schema 2 defaults and session entries are read and normalized in memory; they are not rewritten on read. Schema 3 stores `null` for an explicitly disabled ordinary role. Session changes write a schema 2 `off` guard before the schema 3 state so older versions restore off. Saving global defaults is global-only: before downgrading after that action or manually writing schema 3, run `/delegate off` in every active branch, restore schema 2 references manually, then install the older package.
|
|
38
|
+
|
|
39
|
+
See the [getting-started guide](https://yivas.github.io/pi-delegation-policy/getting-started/) and [configuration reference](https://yivas.github.io/pi-delegation-policy/configuration/).
|
|
34
40
|
|
|
35
41
|
## Essential commands
|
|
36
42
|
|
|
@@ -43,13 +49,7 @@ See the [end-to-end getting-started guide](https://yivas.github.io/pi-delegation
|
|
|
43
49
|
/delegate reset Reset this branch to off and other fields to global defaults
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
The editor is a bounded, keyboard-first panel. It shows model ID first and `[provider]` last, fuzzy-searches provider, model ID, and display name, and shows at most 10 model rows. It also shows a compact effective-policy preview, field explanations, and public model metadata when Pi supplies it. Changes are drafts until **Apply changes**; saving effective configuration as defaults updates the global file without applying the draft, and closing a modified draft requires explicit discard.
|
|
47
|
-
|
|
48
|
-
## Configuration and safety
|
|
49
|
-
|
|
50
|
-
Global defaults are stored at `~/.pi/agent/delegation-policy.json` (schema version 2). The file stores optional intensity, preference, exact role references, and an optional UI Design reference; it never stores thinking. A valid active mode requires exact, available, in-scope, authenticated Small, Medium, and Large references. Before every delegated launch, the policy requires the selected role's exact `provider/model` base and a thinking level chosen for that task instead of relying on ambient defaults. With `pi-subagents`, the main agent passes both as `model: "provider/model:LEVEL"`; other launchers may expose a separate per-run thinking field. Invalid active configuration fails closed as `D:ERR` with no policy injection; `off` is always `D:OFF`.
|
|
51
|
-
|
|
52
|
-
The extension stores only policy settings and provider/model identifiers in local defaults and session entries. Review local configuration before sharing diagnostics, and remove credentials, prompts, personal paths, session files, and unredacted logs from reports. See the [limits and privacy reference](https://yivas.github.io/pi-delegation-policy/limits-and-privacy/) and [security policy](https://github.com/Yivas/pi-delegation-policy/blob/main/SECURITY.md).
|
|
52
|
+
The editor is a bounded, keyboard-first panel. Every model selector pins **Use global default** and **Disable for this session** before searchable models. It shows model ID first and `[provider]` last, fuzzy-searches provider, model ID, and display name, and shows at most 10 model rows. It also shows a compact effective-policy preview, field explanations, and public model metadata when Pi supplies it. Changes are drafts until **Apply changes**; saving effective configuration as defaults updates only the global file without applying the draft, and closing a modified draft requires explicit discard.
|
|
53
53
|
|
|
54
54
|
## Development
|
|
55
55
|
|
|
@@ -63,8 +63,8 @@ npm run build
|
|
|
63
63
|
npm run pack:check
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Tests use local mocks and do not make paid model calls or network requests. See [CONTRIBUTING.md](https://github.com/Yivas/pi-delegation-policy/blob/main/CONTRIBUTING.md)
|
|
66
|
+
Tests use local mocks and do not make paid model calls or network requests. See [CONTRIBUTING.md](https://github.com/Yivas/pi-delegation-policy/blob/main/CONTRIBUTING.md).
|
|
67
67
|
|
|
68
68
|
## License
|
|
69
69
|
|
|
70
|
-
MIT. See [LICENSE](
|
|
70
|
+
MIT. See [LICENSE](LICENSE).
|
package/SECURITY.md
CHANGED
|
@@ -14,4 +14,4 @@ Include the affected version or commit, operating system, Pi version, reproducti
|
|
|
14
14
|
|
|
15
15
|
## Supported versions
|
|
16
16
|
|
|
17
|
-
Only the latest published version is supported. Version 0.
|
|
17
|
+
Only the latest published version is supported. Version 0.6.0 is the current supported release.
|
package/examples/global.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"intensity": "normal",
|
|
4
4
|
"preference": "standard",
|
|
5
5
|
"small": {
|
|
@@ -10,10 +10,7 @@
|
|
|
10
10
|
"provider": "example-provider",
|
|
11
11
|
"model": "example-medium"
|
|
12
12
|
},
|
|
13
|
-
"large":
|
|
14
|
-
"provider": "example-provider",
|
|
15
|
-
"model": "example-large"
|
|
16
|
-
},
|
|
13
|
+
"large": null,
|
|
17
14
|
"uiDesign": {
|
|
18
15
|
"provider": "example-provider",
|
|
19
16
|
"model": "example-ui-design"
|
package/package.json
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["schemaVersion"],
|
|
7
7
|
"properties": {
|
|
8
|
-
"schemaVersion": { "const":
|
|
8
|
+
"schemaVersion": { "const": 3 },
|
|
9
9
|
"intensity": { "enum": ["off", "normal", "aggressive"] },
|
|
10
10
|
"preference": { "enum": ["efficient", "standard", "intensive"] },
|
|
11
|
-
"small": { "$ref": "#/$defs/
|
|
12
|
-
"medium": { "$ref": "#/$defs/
|
|
13
|
-
"large": { "$ref": "#/$defs/
|
|
11
|
+
"small": { "$ref": "#/$defs/ordinaryRole" },
|
|
12
|
+
"medium": { "$ref": "#/$defs/ordinaryRole" },
|
|
13
|
+
"large": { "$ref": "#/$defs/ordinaryRole" },
|
|
14
14
|
"uiDesign": { "$ref": "#/$defs/modelRef" }
|
|
15
15
|
},
|
|
16
16
|
"$defs": {
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"model": { "type": "string", "minLength": 1 }
|
|
23
23
|
},
|
|
24
24
|
"additionalProperties": false
|
|
25
|
+
},
|
|
26
|
+
"ordinaryRole": {
|
|
27
|
+
"anyOf": [{ "$ref": "#/$defs/modelRef" }, { "type": "null" }]
|
|
25
28
|
}
|
|
26
29
|
},
|
|
27
30
|
"additionalProperties": false
|
package/src/config.ts
CHANGED
|
@@ -2,12 +2,15 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
CURRENT_SCHEMA_VERSION,
|
|
5
6
|
emptyGlobalDefaults,
|
|
6
7
|
emptySessionState,
|
|
7
8
|
type EffectiveDelegateState,
|
|
8
9
|
type GlobalDefaults,
|
|
9
10
|
type Intensity,
|
|
10
11
|
type ModelRef,
|
|
12
|
+
type ModelRole,
|
|
13
|
+
type OrdinaryRoleSetting,
|
|
11
14
|
type Preference,
|
|
12
15
|
type SessionDelegateState,
|
|
13
16
|
type ValueSource,
|
|
@@ -19,11 +22,23 @@ export const SESSION_ENTRY_TYPE = "pi-delegation-policy:session";
|
|
|
19
22
|
export const GLOBAL_CONFIG_NAME = "delegation-policy.json";
|
|
20
23
|
|
|
21
24
|
const LEGACY_SCHEMA_MESSAGE =
|
|
22
|
-
"Global defaults use schema version 1. Configure schema version
|
|
25
|
+
"Global defaults use schema version 1. Configure schema version 3 with /delegate before activating delegation.";
|
|
23
26
|
const INVALID_CONFIG_MESSAGE = "Global defaults are invalid. Configure them again with /delegate.";
|
|
27
|
+
const INVALID_SESSION_MESSAGE =
|
|
28
|
+
"The latest delegation session state is invalid or unsupported. Delegation is off for safety.";
|
|
29
|
+
const CONFIG_KEYS = [
|
|
30
|
+
"schemaVersion",
|
|
31
|
+
"intensity",
|
|
32
|
+
"preference",
|
|
33
|
+
"small",
|
|
34
|
+
"medium",
|
|
35
|
+
"large",
|
|
36
|
+
"uiDesign",
|
|
37
|
+
] as const;
|
|
24
38
|
|
|
25
39
|
export type ConfigDiagnostic = {
|
|
26
40
|
message: string;
|
|
41
|
+
reportWhenOff?: boolean;
|
|
27
42
|
};
|
|
28
43
|
|
|
29
44
|
export type LoadedDefaults = {
|
|
@@ -31,6 +46,26 @@ export type LoadedDefaults = {
|
|
|
31
46
|
diagnostics: ConfigDiagnostic[];
|
|
32
47
|
};
|
|
33
48
|
|
|
49
|
+
export type RestoredSessionState = {
|
|
50
|
+
session: SessionDelegateState;
|
|
51
|
+
diagnostics: ConfigDiagnostic[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type SessionEntryWriter = { appendEntry: (type: string, data?: unknown) => void };
|
|
55
|
+
export type GuardedAppendResult = "success" | "guard-failed" | "state-failed";
|
|
56
|
+
|
|
57
|
+
type Schema2GlobalDefaults = {
|
|
58
|
+
schemaVersion: 2;
|
|
59
|
+
intensity?: Intensity;
|
|
60
|
+
preference?: Preference;
|
|
61
|
+
small?: ModelRef;
|
|
62
|
+
medium?: ModelRef;
|
|
63
|
+
large?: ModelRef;
|
|
64
|
+
uiDesign?: ModelRef;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type Schema2SessionState = Omit<Schema2GlobalDefaults, "uiDesign"> & { uiDesign?: ModelRef | null };
|
|
68
|
+
|
|
34
69
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
70
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
71
|
}
|
|
@@ -60,33 +95,27 @@ function parseModelRef(value: unknown): ModelRef | undefined {
|
|
|
60
95
|
return { provider: value.provider, model: value.model };
|
|
61
96
|
}
|
|
62
97
|
|
|
63
|
-
function
|
|
64
|
-
return value ? { ...value } : undefined;
|
|
98
|
+
function copyRoleSetting(value: OrdinaryRoleSetting | undefined): OrdinaryRoleSetting | undefined {
|
|
99
|
+
return value === null ? null : value ? { ...value } : undefined;
|
|
65
100
|
}
|
|
66
101
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"large",
|
|
77
|
-
"uiDesign",
|
|
78
|
-
]) ||
|
|
79
|
-
value.schemaVersion !== 2 ||
|
|
80
|
-
(value.intensity !== undefined && !isIntensity(value.intensity)) ||
|
|
81
|
-
(value.preference !== undefined && !isPreference(value.preference))
|
|
82
|
-
)
|
|
83
|
-
return undefined;
|
|
102
|
+
function hasValidEnvelope(value: unknown, schemaVersion: 2 | 3): value is Record<string, unknown> {
|
|
103
|
+
return (
|
|
104
|
+
isRecord(value) &&
|
|
105
|
+
hasOnlyKeys(value, CONFIG_KEYS) &&
|
|
106
|
+
value.schemaVersion === schemaVersion &&
|
|
107
|
+
(value.intensity === undefined || isIntensity(value.intensity)) &&
|
|
108
|
+
(value.preference === undefined || isPreference(value.preference))
|
|
109
|
+
);
|
|
110
|
+
}
|
|
84
111
|
|
|
112
|
+
function parseSchema2Roles(
|
|
113
|
+
value: Record<string, unknown>,
|
|
114
|
+
): Pick<Schema2GlobalDefaults, "small" | "medium" | "large" | "uiDesign"> | undefined {
|
|
85
115
|
const small = value.small === undefined ? undefined : parseModelRef(value.small);
|
|
86
116
|
const medium = value.medium === undefined ? undefined : parseModelRef(value.medium);
|
|
87
117
|
const large = value.large === undefined ? undefined : parseModelRef(value.large);
|
|
88
118
|
const uiDesign = value.uiDesign === undefined ? undefined : parseModelRef(value.uiDesign);
|
|
89
|
-
|
|
90
119
|
if (
|
|
91
120
|
(value.small !== undefined && !small) ||
|
|
92
121
|
(value.medium !== undefined && !medium) ||
|
|
@@ -94,11 +123,7 @@ export function parseConfig(value: unknown): GlobalDefaults | undefined {
|
|
|
94
123
|
(value.uiDesign !== undefined && !uiDesign)
|
|
95
124
|
)
|
|
96
125
|
return undefined;
|
|
97
|
-
|
|
98
126
|
return {
|
|
99
|
-
schemaVersion: 2,
|
|
100
|
-
...(value.intensity ? { intensity: value.intensity } : {}),
|
|
101
|
-
...(value.preference ? { preference: value.preference } : {}),
|
|
102
127
|
...(small ? { small } : {}),
|
|
103
128
|
...(medium ? { medium } : {}),
|
|
104
129
|
...(large ? { large } : {}),
|
|
@@ -106,51 +131,139 @@ export function parseConfig(value: unknown): GlobalDefaults | undefined {
|
|
|
106
131
|
};
|
|
107
132
|
}
|
|
108
133
|
|
|
109
|
-
export function
|
|
134
|
+
export function parseSchema2Config(value: unknown): Schema2GlobalDefaults | undefined {
|
|
135
|
+
if (!hasValidEnvelope(value, 2)) return undefined;
|
|
136
|
+
const roles = parseSchema2Roles(value);
|
|
137
|
+
if (!roles) return undefined;
|
|
138
|
+
return {
|
|
139
|
+
schemaVersion: 2,
|
|
140
|
+
...(value.intensity ? { intensity: value.intensity as Intensity } : {}),
|
|
141
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
142
|
+
...roles,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function parseSchema3Config(value: unknown): GlobalDefaults | undefined {
|
|
147
|
+
if (!hasValidEnvelope(value, CURRENT_SCHEMA_VERSION)) return undefined;
|
|
148
|
+
const parseOrdinary = (setting: unknown): OrdinaryRoleSetting | undefined =>
|
|
149
|
+
setting === null ? null : parseModelRef(setting);
|
|
150
|
+
const small = value.small === undefined ? undefined : parseOrdinary(value.small);
|
|
151
|
+
const medium = value.medium === undefined ? undefined : parseOrdinary(value.medium);
|
|
152
|
+
const large = value.large === undefined ? undefined : parseOrdinary(value.large);
|
|
153
|
+
const uiDesign = value.uiDesign === undefined ? undefined : parseModelRef(value.uiDesign);
|
|
110
154
|
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"preference",
|
|
116
|
-
"small",
|
|
117
|
-
"medium",
|
|
118
|
-
"large",
|
|
119
|
-
"uiDesign",
|
|
120
|
-
]) ||
|
|
121
|
-
value.schemaVersion !== 2 ||
|
|
122
|
-
(value.intensity !== undefined && !isIntensity(value.intensity)) ||
|
|
123
|
-
(value.preference !== undefined && !isPreference(value.preference))
|
|
155
|
+
(value.small !== undefined && small === undefined) ||
|
|
156
|
+
(value.medium !== undefined && medium === undefined) ||
|
|
157
|
+
(value.large !== undefined && large === undefined) ||
|
|
158
|
+
(value.uiDesign !== undefined && !uiDesign)
|
|
124
159
|
)
|
|
125
160
|
return undefined;
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
163
|
+
...(value.intensity ? { intensity: value.intensity as Intensity } : {}),
|
|
164
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
165
|
+
...(small !== undefined ? { small } : {}),
|
|
166
|
+
...(medium !== undefined ? { medium } : {}),
|
|
167
|
+
...(large !== undefined ? { large } : {}),
|
|
168
|
+
...(uiDesign ? { uiDesign } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
126
171
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
172
|
+
function migrateSchema2Config(value: Schema2GlobalDefaults): GlobalDefaults {
|
|
173
|
+
return {
|
|
174
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
175
|
+
...(value.intensity ? { intensity: value.intensity as Intensity } : {}),
|
|
176
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
177
|
+
...(value.small ? { small: { ...value.small } } : {}),
|
|
178
|
+
...(value.medium ? { medium: { ...value.medium } } : {}),
|
|
179
|
+
...(value.large ? { large: { ...value.large } } : {}),
|
|
180
|
+
...(value.uiDesign ? { uiDesign: { ...value.uiDesign } } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseConfig(value: unknown): GlobalDefaults | undefined {
|
|
185
|
+
return (
|
|
186
|
+
parseSchema3Config(value) ??
|
|
187
|
+
(() => {
|
|
188
|
+
const schema2 = parseSchema2Config(value);
|
|
189
|
+
return schema2 ? migrateSchema2Config(schema2) : undefined;
|
|
190
|
+
})()
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseSchema2SessionState(value: unknown): Schema2SessionState | undefined {
|
|
195
|
+
if (!hasValidEnvelope(value, 2)) return undefined;
|
|
130
196
|
const uiDesign =
|
|
131
197
|
value.uiDesign === undefined || value.uiDesign === null
|
|
132
198
|
? value.uiDesign
|
|
133
199
|
: parseModelRef(value.uiDesign);
|
|
200
|
+
const roles = parseSchema2Roles({ ...value, uiDesign: undefined });
|
|
201
|
+
if (!roles || (value.uiDesign !== undefined && value.uiDesign !== null && !uiDesign))
|
|
202
|
+
return undefined;
|
|
203
|
+
return {
|
|
204
|
+
schemaVersion: 2,
|
|
205
|
+
...(value.intensity ? { intensity: value.intensity as Intensity } : {}),
|
|
206
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
207
|
+
...roles,
|
|
208
|
+
...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
134
211
|
|
|
212
|
+
function parseSchema3SessionState(value: unknown): SessionDelegateState | undefined {
|
|
213
|
+
if (!hasValidEnvelope(value, CURRENT_SCHEMA_VERSION)) return undefined;
|
|
214
|
+
const parseOrdinary = (setting: unknown): OrdinaryRoleSetting | undefined =>
|
|
215
|
+
setting === null ? null : parseModelRef(setting);
|
|
216
|
+
const small = value.small === undefined ? undefined : parseOrdinary(value.small);
|
|
217
|
+
const medium = value.medium === undefined ? undefined : parseOrdinary(value.medium);
|
|
218
|
+
const large = value.large === undefined ? undefined : parseOrdinary(value.large);
|
|
219
|
+
const uiDesign =
|
|
220
|
+
value.uiDesign === undefined || value.uiDesign === null
|
|
221
|
+
? value.uiDesign
|
|
222
|
+
: parseModelRef(value.uiDesign);
|
|
135
223
|
if (
|
|
136
|
-
(value.small !== undefined &&
|
|
137
|
-
(value.medium !== undefined &&
|
|
138
|
-
(value.large !== undefined &&
|
|
224
|
+
(value.small !== undefined && small === undefined) ||
|
|
225
|
+
(value.medium !== undefined && medium === undefined) ||
|
|
226
|
+
(value.large !== undefined && large === undefined) ||
|
|
139
227
|
(value.uiDesign !== undefined && value.uiDesign !== null && !uiDesign)
|
|
140
228
|
)
|
|
141
229
|
return undefined;
|
|
142
|
-
|
|
143
230
|
return {
|
|
144
|
-
schemaVersion:
|
|
145
|
-
...(value.intensity ? { intensity: value.intensity } : {}),
|
|
146
|
-
...(value.preference ? { preference: value.preference } : {}),
|
|
147
|
-
...(small ? { small } : {}),
|
|
148
|
-
...(medium ? { medium } : {}),
|
|
149
|
-
...(large ? { large } : {}),
|
|
231
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
232
|
+
...(value.intensity ? { intensity: value.intensity as Intensity } : {}),
|
|
233
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
234
|
+
...(small !== undefined ? { small } : {}),
|
|
235
|
+
...(medium !== undefined ? { medium } : {}),
|
|
236
|
+
...(large !== undefined ? { large } : {}),
|
|
150
237
|
...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
|
|
151
238
|
};
|
|
152
239
|
}
|
|
153
240
|
|
|
241
|
+
function migrateSchema2SessionState(value: Schema2SessionState): SessionDelegateState {
|
|
242
|
+
return {
|
|
243
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
244
|
+
...(value.intensity ? { intensity: value.intensity as Intensity } : {}),
|
|
245
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
246
|
+
...(value.small ? { small: { ...value.small } } : {}),
|
|
247
|
+
...(value.medium ? { medium: { ...value.medium } } : {}),
|
|
248
|
+
...(value.large ? { large: { ...value.large } } : {}),
|
|
249
|
+
...(value.uiDesign === null
|
|
250
|
+
? { uiDesign: null }
|
|
251
|
+
: value.uiDesign
|
|
252
|
+
? { uiDesign: { ...value.uiDesign } }
|
|
253
|
+
: {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function parseSessionState(value: unknown): SessionDelegateState | undefined {
|
|
258
|
+
return (
|
|
259
|
+
parseSchema3SessionState(value) ??
|
|
260
|
+
(() => {
|
|
261
|
+
const schema2 = parseSchema2SessionState(value);
|
|
262
|
+
return schema2 ? migrateSchema2SessionState(schema2) : undefined;
|
|
263
|
+
})()
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
154
267
|
function isLegacyConfig(value: unknown): boolean {
|
|
155
268
|
return isRecord(value) && value.schemaVersion === 1;
|
|
156
269
|
}
|
|
@@ -195,64 +308,111 @@ async function atomicWrite(path: string, value: unknown): Promise<void> {
|
|
|
195
308
|
}
|
|
196
309
|
|
|
197
310
|
export async function writeConfig(path: string, defaults: GlobalDefaults): Promise<void> {
|
|
198
|
-
const parsed =
|
|
311
|
+
const parsed = parseSchema3Config(defaults);
|
|
199
312
|
if (!parsed) throw new Error("Refusing to write invalid delegation policy defaults.");
|
|
200
313
|
await atomicWrite(path, parsed);
|
|
201
314
|
}
|
|
202
315
|
|
|
203
|
-
export function
|
|
316
|
+
export function restoreSessionStateWithDiagnostics(entries: unknown[]): RestoredSessionState {
|
|
204
317
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
205
318
|
const entry = entries[index] as Record<string, unknown> | undefined;
|
|
206
319
|
if (entry?.type !== "custom" || entry.customType !== SESSION_ENTRY_TYPE) continue;
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
320
|
+
const session = parseSessionState(entry.data);
|
|
321
|
+
if (session) return { session, diagnostics: [] };
|
|
322
|
+
return {
|
|
323
|
+
session: { schemaVersion: CURRENT_SCHEMA_VERSION, intensity: "off" },
|
|
324
|
+
diagnostics: [{ message: INVALID_SESSION_MESSAGE, reportWhenOff: true }],
|
|
325
|
+
};
|
|
209
326
|
}
|
|
210
|
-
return emptySessionState();
|
|
327
|
+
return { session: emptySessionState(), diagnostics: [] };
|
|
211
328
|
}
|
|
212
329
|
|
|
213
|
-
function
|
|
214
|
-
|
|
330
|
+
export function restoreSessionState(entries: unknown[]): SessionDelegateState {
|
|
331
|
+
return restoreSessionStateWithDiagnostics(entries).session;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function sourceFor<T>(
|
|
335
|
+
session: Record<string, unknown>,
|
|
336
|
+
globalValue: T | undefined,
|
|
337
|
+
key: string,
|
|
338
|
+
): ValueSource {
|
|
339
|
+
if (Object.hasOwn(session, key)) return "session";
|
|
215
340
|
if (globalValue !== undefined) return "global";
|
|
216
341
|
return "default";
|
|
217
342
|
}
|
|
218
343
|
|
|
344
|
+
function resolveRole(
|
|
345
|
+
globalValue: OrdinaryRoleSetting | undefined,
|
|
346
|
+
session: SessionDelegateState,
|
|
347
|
+
role: ModelRole,
|
|
348
|
+
): OrdinaryRoleSetting | undefined {
|
|
349
|
+
return copyRoleSetting(Object.hasOwn(session, role) ? session[role] : globalValue);
|
|
350
|
+
}
|
|
351
|
+
|
|
219
352
|
export function resolveDelegateState(
|
|
220
353
|
defaults: GlobalDefaults,
|
|
221
354
|
session: SessionDelegateState,
|
|
222
355
|
): EffectiveDelegateState {
|
|
223
356
|
const uiDesign =
|
|
224
357
|
session.uiDesign === undefined
|
|
225
|
-
?
|
|
358
|
+
? defaults.uiDesign
|
|
359
|
+
? { ...defaults.uiDesign }
|
|
360
|
+
: undefined
|
|
226
361
|
: session.uiDesign === null
|
|
227
362
|
? undefined
|
|
228
|
-
:
|
|
363
|
+
: { ...session.uiDesign };
|
|
364
|
+
const small = resolveRole(defaults.small, session, "small");
|
|
365
|
+
const medium = resolveRole(defaults.medium, session, "medium");
|
|
366
|
+
const large = resolveRole(defaults.large, session, "large");
|
|
229
367
|
|
|
230
368
|
return {
|
|
231
369
|
intensity: session.intensity ?? defaults.intensity ?? "off",
|
|
232
370
|
preference: session.preference ?? defaults.preference ?? "standard",
|
|
233
|
-
small
|
|
234
|
-
medium
|
|
235
|
-
large
|
|
371
|
+
...(small !== undefined ? { small } : {}),
|
|
372
|
+
...(medium !== undefined ? { medium } : {}),
|
|
373
|
+
...(large !== undefined ? { large } : {}),
|
|
236
374
|
...(uiDesign ? { uiDesign } : {}),
|
|
237
375
|
source: {
|
|
238
|
-
intensity: sourceFor(session
|
|
239
|
-
preference: sourceFor(session
|
|
240
|
-
small: sourceFor(session
|
|
241
|
-
medium: sourceFor(session
|
|
242
|
-
large: sourceFor(session
|
|
243
|
-
uiDesign: sourceFor(session
|
|
376
|
+
intensity: sourceFor(session, defaults.intensity, "intensity"),
|
|
377
|
+
preference: sourceFor(session, defaults.preference, "preference"),
|
|
378
|
+
small: sourceFor(session, defaults.small, "small"),
|
|
379
|
+
medium: sourceFor(session, defaults.medium, "medium"),
|
|
380
|
+
large: sourceFor(session, defaults.large, "large"),
|
|
381
|
+
uiDesign: sourceFor(session, defaults.uiDesign, "uiDesign"),
|
|
244
382
|
},
|
|
245
383
|
};
|
|
246
384
|
}
|
|
247
385
|
|
|
248
386
|
export function defaultsFromEffectiveState(state: EffectiveDelegateState): GlobalDefaults {
|
|
387
|
+
const role = (setting: OrdinaryRoleSetting | undefined) =>
|
|
388
|
+
setting === undefined ? {} : setting === null ? { value: null } : { value: { ...setting } };
|
|
389
|
+
const small = role(state.small);
|
|
390
|
+
const medium = role(state.medium);
|
|
391
|
+
const large = role(state.large);
|
|
249
392
|
return {
|
|
250
|
-
schemaVersion:
|
|
393
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
251
394
|
intensity: state.intensity,
|
|
252
395
|
preference: state.preference,
|
|
253
|
-
...(
|
|
254
|
-
...(
|
|
255
|
-
...(
|
|
396
|
+
...("value" in small ? { small: small.value } : {}),
|
|
397
|
+
...("value" in medium ? { medium: medium.value } : {}),
|
|
398
|
+
...("value" in large ? { large: large.value } : {}),
|
|
256
399
|
...(state.uiDesign ? { uiDesign: { ...state.uiDesign } } : {}),
|
|
257
400
|
};
|
|
258
401
|
}
|
|
402
|
+
|
|
403
|
+
export function appendGuardedSessionState(
|
|
404
|
+
pi: SessionEntryWriter,
|
|
405
|
+
session: SessionDelegateState,
|
|
406
|
+
): GuardedAppendResult {
|
|
407
|
+
try {
|
|
408
|
+
pi.appendEntry(SESSION_ENTRY_TYPE, { schemaVersion: 2, intensity: "off" });
|
|
409
|
+
} catch {
|
|
410
|
+
return "guard-failed";
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
pi.appendEntry(SESSION_ENTRY_TYPE, session);
|
|
414
|
+
return "success";
|
|
415
|
+
} catch {
|
|
416
|
+
return "state-failed";
|
|
417
|
+
}
|
|
418
|
+
}
|
package/src/delegate-panel.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { buildPolicyPreview } from "./prompt.ts";
|
|
|
16
16
|
import {
|
|
17
17
|
INTENSITIES,
|
|
18
18
|
PREFERENCES,
|
|
19
|
+
CURRENT_SCHEMA_VERSION,
|
|
19
20
|
type GlobalDefaults,
|
|
20
21
|
type Intensity,
|
|
21
22
|
type ModelConfigKey,
|
|
@@ -73,7 +74,7 @@ const FIELD_LABELS: Record<DelegateField, string> = {
|
|
|
73
74
|
small: "Small model",
|
|
74
75
|
medium: "Medium model",
|
|
75
76
|
large: "Large model",
|
|
76
|
-
uiDesign: "
|
|
77
|
+
uiDesign: "Visual Design",
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
const FIELD_DESCRIPTIONS: Record<DelegateField, string> = {
|
|
@@ -82,7 +83,7 @@ const FIELD_DESCRIPTIONS: Record<DelegateField, string> = {
|
|
|
82
83
|
small: "Bounded, planned, and verifiable execution.",
|
|
83
84
|
medium: "Planning, ambiguity, synthesis, and coordination.",
|
|
84
85
|
large: "Exceptional unblocker for persistent hard problems.",
|
|
85
|
-
uiDesign: "
|
|
86
|
+
uiDesign: "Design, assets, and bounded presentation work; no app behavior.",
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
const ACTION_LABELS: Record<PanelAction, string> = {
|
|
@@ -260,7 +261,7 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
260
261
|
const safeWidth = Math.max(1, width);
|
|
261
262
|
this.renderWidth = safeWidth;
|
|
262
263
|
const rows = Math.max(1, this.tui.terminal.rows);
|
|
263
|
-
if (safeWidth <
|
|
264
|
+
if (safeWidth < 26 || rows < 9) return this.renderCompact(safeWidth, rows);
|
|
264
265
|
|
|
265
266
|
const title = this.renderTitle(safeWidth);
|
|
266
267
|
const footer = this.renderFooter(safeWidth);
|
|
@@ -281,7 +282,7 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
private isCompact(): boolean {
|
|
284
|
-
return this.renderWidth <
|
|
285
|
+
return this.renderWidth < 26 || this.tui.terminal.rows < 9;
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
private handleCompactInput(data: string): void {
|
|
@@ -400,7 +401,7 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
400
401
|
? effective.uiDesign
|
|
401
402
|
? modelText(effective.uiDesign)
|
|
402
403
|
: "disabled"
|
|
403
|
-
:
|
|
404
|
+
: rawModelText(effective[field], "not configured");
|
|
404
405
|
const details = this.sourceDetails(field);
|
|
405
406
|
if (width < 48) {
|
|
406
407
|
return [
|
|
@@ -473,7 +474,7 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
473
474
|
];
|
|
474
475
|
}
|
|
475
476
|
return [
|
|
476
|
-
"built-in
|
|
477
|
+
"built-in not configured",
|
|
477
478
|
`global ${rawModelText(this.global[field], "—")}`,
|
|
478
479
|
`session ${rawModelText(this.draft[field], "inherit")}`,
|
|
479
480
|
];
|
|
@@ -522,7 +523,7 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
522
523
|
const [input = ""] = this.searchInput.render(inputWidth);
|
|
523
524
|
const inputText = input.startsWith("> ") ? input.slice(2) : input;
|
|
524
525
|
const choices = this.modelChoices(mode.field, mode.query);
|
|
525
|
-
const pinnedCount =
|
|
526
|
+
const pinnedCount = 2;
|
|
526
527
|
const pinned = choices.slice(0, pinnedCount);
|
|
527
528
|
const models = choices.slice(pinnedCount);
|
|
528
529
|
const pinnedLines = pinned.map((choice, index) =>
|
|
@@ -544,8 +545,9 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
544
545
|
),
|
|
545
546
|
)
|
|
546
547
|
: [];
|
|
547
|
-
const fixedRows =
|
|
548
|
-
const
|
|
548
|
+
const fixedRows = 1 + pinnedLines.length + dividerRows;
|
|
549
|
+
const reservedModelRows = models.length > 0 && budget > fixedRows ? 1 : 0;
|
|
550
|
+
const availableDetailRows = Math.max(0, budget - fixedRows - reservedModelRows - 1);
|
|
549
551
|
const details = metadata.slice(0, availableDetailRows);
|
|
550
552
|
const detailRows = details.length > 0 ? details.length + 1 : 0;
|
|
551
553
|
const availableListRows = Math.max(0, budget - fixedRows - detailRows);
|
|
@@ -577,7 +579,6 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
577
579
|
}
|
|
578
580
|
return [
|
|
579
581
|
truncateToWidth(`Search: ${inputText}`, width, ""),
|
|
580
|
-
"",
|
|
581
582
|
...pinnedLines,
|
|
582
583
|
...(dividerRows ? [this.theme.fg("borderMuted", "─".repeat(width))] : []),
|
|
583
584
|
...modelLines,
|
|
@@ -656,7 +657,8 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
656
657
|
}
|
|
657
658
|
if (item === "apply") void this.applyDraft();
|
|
658
659
|
else if (item === "save-defaults") void this.saveDefaults();
|
|
659
|
-
else if (item === "reset")
|
|
660
|
+
else if (item === "reset")
|
|
661
|
+
this.draft = { schemaVersion: CURRENT_SCHEMA_VERSION, intensity: "off" };
|
|
660
662
|
else this.requestClose();
|
|
661
663
|
}
|
|
662
664
|
|
|
@@ -730,24 +732,28 @@ export class DelegatePanel implements Component, Focusable {
|
|
|
730
732
|
if (!choice || this.mode.kind !== "model") return;
|
|
731
733
|
const field = this.mode.field;
|
|
732
734
|
if (choice.kind === "global") delete this.draft[field];
|
|
733
|
-
else if (choice.kind === "disabled"
|
|
735
|
+
else if (choice.kind === "disabled") this.draft[field] = null;
|
|
734
736
|
else if (choice.kind === "model") this.draft[field] = { ...choice.reference };
|
|
735
737
|
this.mode = { kind: "settings" };
|
|
736
738
|
}
|
|
737
739
|
|
|
738
740
|
private modelChoices(field: ModelConfigKey, query: string): ModelChoice[] {
|
|
739
741
|
const global = this.global[field];
|
|
742
|
+
const globalDescription =
|
|
743
|
+
field === "uiDesign"
|
|
744
|
+
? global
|
|
745
|
+
? modelText(global)
|
|
746
|
+
: "disabled"
|
|
747
|
+
: rawModelText(global, "not configured");
|
|
740
748
|
const pinned: ModelChoice[] = [
|
|
741
749
|
{
|
|
742
750
|
kind: "global",
|
|
743
751
|
key: "global",
|
|
744
752
|
label: USE_GLOBAL_DEFAULT,
|
|
745
|
-
|
|
753
|
+
description: globalDescription,
|
|
746
754
|
},
|
|
755
|
+
{ kind: "disabled", key: "disabled", label: DISABLE_FOR_SESSION },
|
|
747
756
|
];
|
|
748
|
-
if (field === "uiDesign") {
|
|
749
|
-
pinned.push({ kind: "disabled", key: "disabled", label: DISABLE_FOR_SESSION });
|
|
750
|
-
}
|
|
751
757
|
const filtered = query
|
|
752
758
|
? fuzzyFilter(this.candidates, query, (model) =>
|
|
753
759
|
`${model.provider} ${model.provider}/${model.id} ${model.provider} ${model.id} ${model.name ?? ""}`.trim(),
|
package/src/index.ts
CHANGED
|
@@ -5,16 +5,16 @@ import type {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
7
7
|
import { buildDelegationPolicy } from "./prompt.ts";
|
|
8
|
+
import { appendGuardedSessionState, type GuardedAppendResult } from "./config.ts";
|
|
8
9
|
import {
|
|
9
10
|
formatModelRef,
|
|
10
11
|
hasRuntimeError,
|
|
11
12
|
loadRuntime,
|
|
12
|
-
sessionEntry,
|
|
13
13
|
statusLabel,
|
|
14
14
|
type RuntimeState,
|
|
15
15
|
} from "./runtime.ts";
|
|
16
16
|
import { openDelegateEditor } from "./ui.ts";
|
|
17
|
-
import { INTENSITIES, type Intensity } from "./types.ts";
|
|
17
|
+
import { CURRENT_SCHEMA_VERSION, INTENSITIES, type Intensity } from "./types.ts";
|
|
18
18
|
|
|
19
19
|
const STATUS_KEY = "pi-delegation-policy";
|
|
20
20
|
|
|
@@ -42,17 +42,13 @@ export function getArgumentCompletions(prefix: string): AutocompleteItem[] | nul
|
|
|
42
42
|
return matches.length ? matches.map((value) => ({ value, label: value })) : null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async function refresh(ctx: ExtensionContext): Promise<RuntimeState> {
|
|
46
|
-
return loadRuntime(ctx);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
45
|
function updateStatus(ctx: ExtensionContext, state: RuntimeState): void {
|
|
50
46
|
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", statusLabel(state)));
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
async function openEditor(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
54
50
|
await openDelegateEditor(ctx, pi);
|
|
55
|
-
updateStatus(ctx, await
|
|
51
|
+
updateStatus(ctx, await loadRuntime(ctx));
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
export function statusText(state: RuntimeState): string {
|
|
@@ -63,34 +59,56 @@ export function statusText(state: RuntimeState): string {
|
|
|
63
59
|
`small=${formatModelRef(effective.small)} (${effective.source.small})`,
|
|
64
60
|
`medium=${formatModelRef(effective.medium)} (${effective.source.medium})`,
|
|
65
61
|
`large=${formatModelRef(effective.large)} (${effective.source.large})`,
|
|
66
|
-
`ui-design=${formatModelRef(effective.uiDesign)} (${effective.source.uiDesign})`,
|
|
62
|
+
`ui-design=${effective.uiDesign ? formatModelRef(effective.uiDesign) : "disabled"} (${effective.source.uiDesign})`,
|
|
67
63
|
];
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
const diagnosticMessages = state.diagnostics.map(({ message }) => message);
|
|
66
|
+
const errorDetails =
|
|
67
|
+
effective.intensity === "off"
|
|
68
|
+
? state.diagnostics.filter(({ reportWhenOff }) => reportWhenOff).map(({ message }) => message)
|
|
69
|
+
: [
|
|
70
|
+
...diagnosticMessages,
|
|
71
|
+
...state.runtimeErrors.filter((message) => !diagnosticMessages.includes(message)),
|
|
72
|
+
];
|
|
73
|
+
if (errorDetails.length > 0) details.push(`details=${errorDetails.join("; ")}`);
|
|
72
74
|
return details.join(" | ");
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
function notifyAppendFailure(ctx: ExtensionCommandContext, result: GuardedAppendResult): void {
|
|
78
|
+
ctx.ui.notify(
|
|
79
|
+
result === "guard-failed"
|
|
80
|
+
? "Could not save session settings. No change was applied."
|
|
81
|
+
: "Could not save session settings. Delegation is off for safety.",
|
|
82
|
+
"error",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
async function setSessionIntensity(
|
|
76
87
|
pi: ExtensionAPI,
|
|
77
88
|
ctx: ExtensionCommandContext,
|
|
78
89
|
intensity: Intensity,
|
|
79
90
|
): Promise<void> {
|
|
80
|
-
const state = await
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const updated = await
|
|
91
|
+
const state = await loadRuntime(ctx);
|
|
92
|
+
const session = { ...state.session, schemaVersion: CURRENT_SCHEMA_VERSION, intensity };
|
|
93
|
+
const result = appendGuardedSessionState(pi, session);
|
|
94
|
+
const updated = await loadRuntime(ctx);
|
|
84
95
|
updateStatus(ctx, updated);
|
|
96
|
+
if (result !== "success") {
|
|
97
|
+
notifyAppendFailure(ctx, result);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
85
100
|
ctx.ui.notify(`Session delegation intensity: ${intensity}.`, "info");
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
async function resetSession(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const updated = await refresh(ctx);
|
|
104
|
+
const session = { schemaVersion: CURRENT_SCHEMA_VERSION, intensity: "off" as const };
|
|
105
|
+
const result = appendGuardedSessionState(pi, session);
|
|
106
|
+
const updated = await loadRuntime(ctx);
|
|
93
107
|
updateStatus(ctx, updated);
|
|
108
|
+
if (result !== "success") {
|
|
109
|
+
notifyAppendFailure(ctx, result);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
94
112
|
ctx.ui.notify("Session delegation settings reset to off.", "info");
|
|
95
113
|
}
|
|
96
114
|
|
|
@@ -105,7 +123,7 @@ export default function piDelegationPolicy(pi: ExtensionAPI): void {
|
|
|
105
123
|
return;
|
|
106
124
|
}
|
|
107
125
|
if (action.kind === "status") {
|
|
108
|
-
const state = await
|
|
126
|
+
const state = await loadRuntime(ctx);
|
|
109
127
|
updateStatus(ctx, state);
|
|
110
128
|
ctx.ui.notify(statusText(state), hasRuntimeError(state) ? "error" : "info");
|
|
111
129
|
return;
|
|
@@ -128,13 +146,13 @@ export default function piDelegationPolicy(pi: ExtensionAPI): void {
|
|
|
128
146
|
});
|
|
129
147
|
|
|
130
148
|
pi.on("session_start", async (_event, ctx) => {
|
|
131
|
-
updateStatus(ctx, await
|
|
149
|
+
updateStatus(ctx, await loadRuntime(ctx));
|
|
132
150
|
});
|
|
133
151
|
pi.on("session_tree", async (_event, ctx) => {
|
|
134
|
-
updateStatus(ctx, await
|
|
152
|
+
updateStatus(ctx, await loadRuntime(ctx));
|
|
135
153
|
});
|
|
136
154
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
137
|
-
const state = await
|
|
155
|
+
const state = await loadRuntime(ctx);
|
|
138
156
|
updateStatus(ctx, state);
|
|
139
157
|
const policy = buildDelegationPolicy(state);
|
|
140
158
|
return policy ? { systemPrompt: `${event.systemPrompt}\n\n${policy}` } : undefined;
|
package/src/prompt.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
enabledOrdinaryRoles,
|
|
3
|
+
hasRuntimeError,
|
|
4
|
+
isRoleDisabled,
|
|
5
|
+
type RuntimeState,
|
|
6
|
+
} from "./runtime.ts";
|
|
7
|
+
import type { EffectiveDelegateState, ModelRef, ModelRole, Preference } from "./types.ts";
|
|
3
8
|
|
|
4
9
|
const NORMAL_POLICY =
|
|
5
10
|
"Delegate substantial, separable work only when the expected benefit clearly outweighs briefing, supervision, review, and integration cost. Count parallelism as a benefit only when valuable work can advance now or elapsed time matters. A merely possible fresh perspective is not enough by itself. Keep borderline work with the main agent.";
|
|
@@ -11,19 +16,36 @@ const ROLE_SELECTION_POLICY = `Choose the role by task fit before considering mo
|
|
|
11
16
|
- difficulty: clarity, ambiguity, dependencies, competing hypotheses, and risk;
|
|
12
17
|
- quantity: files, modules, systems, sources, and context volume;
|
|
13
18
|
- error and review cost: what can go wrong, how costly it is to detect, and what evidence is needed.
|
|
14
|
-
No single factor decides the role.
|
|
19
|
+
No single factor decides the role. First remove disabled roles, then discard enabled roles that cannot satisfy the acceptance criteria and evidence requirements. Select the least costly remaining role that can satisfy them. Keep the work with the main agent if no enabled role can satisfy them.
|
|
15
20
|
|
|
16
21
|
Use Small for bounded, planned, and verifiable execution: concrete searches, scoped exploration, defined implementation, focused documentation, tests, reviews, mechanical changes, evident bugs, and bounded UI implementation whose design and stack are decided. Difficult but well-defined execution can remain Small with higher thinking.
|
|
17
22
|
|
|
18
23
|
Use Medium directly when the combined task fit materially requires planning, reducing meaningful ambiguity, broad synthesis, tracing several modules, comparing sources or options, coordinating substantial context, or making difficult decisions. Small does not need to fail first.
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
When Small and Medium are enabled alternatives, use Large only to unblock genuinely stuck work: persistent failures, severe framework conflicts, contradictory hypotheses, or reliable prior evidence that ordinary roles have not produced a trustworthy answer. Do not require ceremonial failed attempts. Large remains exceptional in a complete ordinary-role configuration.
|
|
21
26
|
|
|
22
|
-
Large quantities of repetitive, independent work favor multiple Small delegations; volume alone does not justify Medium or Large. Agent type does not determine the model role. Apply preference only when Small and Medium are comparably credible fits.
|
|
27
|
+
A more capable enabled role may cover work normally suited to a disabled role only when it can satisfy the same acceptance and evidence. Never choose a less capable role merely because it is the only enabled role. Large quantities of repetitive, independent work favor multiple Small delegations; volume alone does not justify Medium or Large. Agent type does not determine the model role. Apply preference only when Small and Medium are comparably credible fits. That tie-break applies only when both are enabled.
|
|
23
28
|
|
|
24
29
|
In every intensity, keep global strategy, coordination, integration, final review, and work whose essential context is too costly or risky to transfer with the main agent.`;
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
const VISUAL_DESIGN_POLICY = `Visual Design is an optional specialist role. Use it only when all four conditions hold:
|
|
32
|
+
1. the primary acceptance criterion is a visual or user-experience result;
|
|
33
|
+
2. product behavior and data contracts are already defined and remain unchanged;
|
|
34
|
+
3. the patch is bounded to an identifiable surface, component, or set of assets;
|
|
35
|
+
4. it requires no business logic, data flow, APIs, routes, application architecture, tooling, or cross-system coordination.
|
|
36
|
+
|
|
37
|
+
When eligible, Visual Design may design, create, implement, and review scoped presentation code and visual assets, including layout, styles, responsive presentation, typography, images, icons, logos, SVGs, diagrams, and documentation visuals. It may address visual accessibility such as contrast and focus visibility. It must run and report the relevant existing checks for its patch.
|
|
38
|
+
|
|
39
|
+
Route interaction behavior, state, validation, semantic HTML changes, keyboard mechanics, ARIA behavior, authentication, permissions, persistence, test infrastructure, and behavior-test ownership to an enabled ordinary role that fits, or keep it with the main agent. If any eligibility condition fails, use an enabled ordinary role or split the visual portion from the broader task. The main agent retains cross-domain integration and final acceptance.`;
|
|
40
|
+
|
|
41
|
+
function hasSmallMedium(enabled: readonly ModelRole[]): boolean {
|
|
42
|
+
return enabled.includes("small") && enabled.includes("medium");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function preferenceGuidance(preference: Preference, enabled: readonly ModelRole[]): string {
|
|
46
|
+
if (!hasSmallMedium(enabled)) {
|
|
47
|
+
return `${preference} is inactive because Small or Medium is disabled.`;
|
|
48
|
+
}
|
|
27
49
|
if (preference === "efficient") {
|
|
28
50
|
return "Use efficient only as a Small tie-break when Small and Medium are comparably credible. Do not choose Small when Medium is a materially better task fit.";
|
|
29
51
|
}
|
|
@@ -33,7 +55,8 @@ function preferenceGuidance(preference: Preference): string {
|
|
|
33
55
|
return "Standard adds no Small or Medium bias; follow task fit.";
|
|
34
56
|
}
|
|
35
57
|
|
|
36
|
-
function preferencePreview(preference: Preference): string {
|
|
58
|
+
function preferencePreview(preference: Preference, enabled: readonly ModelRole[]): string {
|
|
59
|
+
if (!hasSmallMedium(enabled)) return `${preference} inactive (Small or Medium disabled)`;
|
|
37
60
|
if (preference === "efficient") return "efficient breaks comparable fits toward Small";
|
|
38
61
|
if (preference === "intensive") return "intensive breaks comparable fits toward Medium";
|
|
39
62
|
return "standard has no extra bias";
|
|
@@ -58,18 +81,43 @@ function formatThinkingLaunchModel(reference: ModelRef): string {
|
|
|
58
81
|
return promptString(`${reference.provider}/${reference.model}:LEVEL`);
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
function roleName(role: ModelRole): string {
|
|
85
|
+
return role[0]!.toUpperCase() + role.slice(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rolesByState(effective: EffectiveDelegateState): {
|
|
89
|
+
enabled: ModelRole[];
|
|
90
|
+
disabled: ModelRole[];
|
|
91
|
+
unconfigured: ModelRole[];
|
|
92
|
+
} {
|
|
93
|
+
return {
|
|
94
|
+
enabled: enabledOrdinaryRoles(effective),
|
|
95
|
+
disabled: (["small", "medium", "large"] as const).filter((role) =>
|
|
96
|
+
isRoleDisabled(effective[role]),
|
|
97
|
+
),
|
|
98
|
+
unconfigured: (["small", "medium", "large"] as const).filter(
|
|
99
|
+
(role) => effective[role] === undefined,
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
61
104
|
export function buildPolicyPreview(effective: EffectiveDelegateState): string[] {
|
|
62
105
|
if (effective.intensity === "off") return ["off · no policy injected"];
|
|
63
106
|
|
|
64
|
-
const {
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
const { enabled, disabled, unconfigured } = rolesByState(effective);
|
|
108
|
+
if (unconfigured.length > 0) {
|
|
109
|
+
return [`active · ${roleName(unconfigured[0]!)} not configured · no policy can be injected`];
|
|
110
|
+
}
|
|
111
|
+
if (enabled.length === 0)
|
|
112
|
+
return ["active · no ordinary role enabled · no policy can be injected"];
|
|
68
113
|
|
|
114
|
+
const references = enabled
|
|
115
|
+
.map((role) => `${roleName(role)} ${formatLaunchModel(effective[role] as ModelRef)}`)
|
|
116
|
+
.join(" · ");
|
|
69
117
|
return [
|
|
70
|
-
`${effective.intensity} · task fit first · ${preferencePreview(effective.preference)}`,
|
|
71
|
-
`
|
|
72
|
-
|
|
118
|
+
`${effective.intensity} · task fit first · ${preferencePreview(effective.preference, enabled)}`,
|
|
119
|
+
`Enabled: ${enabled.map(roleName).join(", ")}${disabled.length ? ` · Disabled: ${disabled.map(roleName).join(", ")}` : ""}`,
|
|
120
|
+
`${references} · exact model plus per-task thinking required; neither uses an ambient default.`,
|
|
73
121
|
];
|
|
74
122
|
}
|
|
75
123
|
|
|
@@ -77,12 +125,19 @@ export function buildDelegationPolicy(state: RuntimeState): string | undefined {
|
|
|
77
125
|
if (state.effective.intensity === "off" || hasRuntimeError(state)) return undefined;
|
|
78
126
|
|
|
79
127
|
const { effective } = state;
|
|
80
|
-
|
|
128
|
+
const { enabled, disabled } = rolesByState(effective);
|
|
129
|
+
if (enabled.length === 0) return undefined;
|
|
81
130
|
|
|
82
131
|
const intensityPolicy = effective.intensity === "normal" ? NORMAL_POLICY : AGGRESSIVE_POLICY;
|
|
83
132
|
const uiDesign = effective.uiDesign
|
|
84
|
-
? `\n-
|
|
133
|
+
? `\n- Visual Design: ${formatReference(effective.uiDesign)}; exact model base: ${formatLaunchModel(effective.uiDesign)}; pi-subagents form: ${formatThinkingLaunchModel(effective.uiDesign)}`
|
|
85
134
|
: "";
|
|
135
|
+
const roleLines = enabled
|
|
136
|
+
.map((role) => {
|
|
137
|
+
const reference = effective[role] as ModelRef;
|
|
138
|
+
return `- ${roleName(role)}: ${formatReference(reference)}; exact model base: ${formatLaunchModel(reference)}; pi-subagents form: ${formatThinkingLaunchModel(reference)}`;
|
|
139
|
+
})
|
|
140
|
+
.join("\n");
|
|
86
141
|
|
|
87
142
|
return `<delegation_policy>
|
|
88
143
|
Intensity: ${effective.intensity}.
|
|
@@ -90,15 +145,15 @@ ${intensityPolicy}
|
|
|
90
145
|
|
|
91
146
|
${ROLE_SELECTION_POLICY}
|
|
92
147
|
|
|
93
|
-
|
|
148
|
+
Enabled ordinary roles: ${enabled.map(roleName).join(", ")}.${disabled.length ? `\nDisabled ordinary roles: ${disabled.map(roleName).join(", ")}.` : ""}
|
|
94
149
|
|
|
95
|
-
|
|
150
|
+
Model preference: ${effective.preference}. ${preferenceGuidance(effective.preference, enabled)}
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
- Small: ${formatReference(effective.small)}; exact model base: ${formatLaunchModel(effective.small)}; pi-subagents form: ${formatThinkingLaunchModel(effective.small)}
|
|
99
|
-
- Medium: ${formatReference(effective.medium)}; exact model base: ${formatLaunchModel(effective.medium)}; pi-subagents form: ${formatThinkingLaunchModel(effective.medium)}
|
|
100
|
-
- Large: ${formatReference(effective.large)}; exact model base: ${formatLaunchModel(effective.large)}; pi-subagents form: ${formatThinkingLaunchModel(effective.large)}${uiDesign}
|
|
152
|
+
Before every delegated launch, name the selected role and take its exact combined provider/model base below. Choose thinking dynamically for that run from task demand, difficulty, quantity, risk, review cost, and the selected model's capabilities. Then transmit both through the launcher's per-run mechanism without changing the provider/model base. When the launcher encodes thinking as a model suffix, replace LEVEL in the shown pi-subagents form and pass model: "provider/model:LEVEL". Do not omit the model or thinking choice, inherit an ambient launcher default for either, substitute an unlisted model, persist the thinking level, launch a disabled or unconfigured role, invent a role, or use an unsupported thinking level.
|
|
101
153
|
|
|
154
|
+
Roles:
|
|
155
|
+
${roleLines}${uiDesign}
|
|
156
|
+
${effective.uiDesign ? `\n${VISUAL_DESIGN_POLICY}\n` : ""}
|
|
102
157
|
This is guidance for the main agent. It does not create, execute, route, supervise, or enforce delegated work.
|
|
103
158
|
</delegation_policy>`;
|
|
104
159
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -3,9 +3,8 @@ import type { Api, Model } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import {
|
|
4
4
|
getGlobalConfigPath,
|
|
5
5
|
readConfig,
|
|
6
|
-
SESSION_ENTRY_TYPE,
|
|
7
6
|
resolveDelegateState,
|
|
8
|
-
|
|
7
|
+
restoreSessionStateWithDiagnostics,
|
|
9
8
|
type ConfigDiagnostic,
|
|
10
9
|
} from "./config.ts";
|
|
11
10
|
import {
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
type ModelRef,
|
|
18
17
|
type ModelRole,
|
|
19
18
|
type ModelStatus,
|
|
19
|
+
type OrdinaryRoleSetting,
|
|
20
20
|
type SessionDelegateState,
|
|
21
21
|
} from "./types.ts";
|
|
22
22
|
|
|
@@ -29,6 +29,18 @@ export type RuntimeState = {
|
|
|
29
29
|
runtimeErrors: string[];
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
export function isRoleEnabled(setting: OrdinaryRoleSetting | undefined): setting is ModelRef {
|
|
33
|
+
return setting !== undefined && setting !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isRoleDisabled(setting: OrdinaryRoleSetting | undefined): setting is null {
|
|
37
|
+
return setting === null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function enabledOrdinaryRoles(effective: EffectiveDelegateState): ModelRole[] {
|
|
41
|
+
return MODEL_ROLES.filter((role) => isRoleEnabled(effective[role]));
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function matchesReference(model: Model<Api>, reference: ModelRef): boolean {
|
|
33
45
|
return model.provider === reference.provider && model.id === reference.model;
|
|
34
46
|
}
|
|
@@ -88,17 +100,12 @@ function statusDetail(status: ModelStatus): string {
|
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!reference) {
|
|
98
|
-
state.runtimeErrors.push(`${ROLE_LABELS[role]} model is not configured.`);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
103
|
+
function validateEnabledRole(
|
|
104
|
+
ctx: ExtensionContext,
|
|
105
|
+
state: RuntimeState,
|
|
106
|
+
role: ModelConfigKey,
|
|
107
|
+
reference: ModelRef,
|
|
108
|
+
): void {
|
|
102
109
|
const status = validateModelReference(ctx, reference);
|
|
103
110
|
state.modelStatuses.set(role, status);
|
|
104
111
|
if (status.kind !== "available") {
|
|
@@ -108,12 +115,12 @@ function validateRole(ctx: ExtensionContext, state: RuntimeState, role: ModelCon
|
|
|
108
115
|
|
|
109
116
|
export async function loadRuntime(ctx: ExtensionContext): Promise<RuntimeState> {
|
|
110
117
|
const loaded = await readConfig(getGlobalConfigPath());
|
|
111
|
-
const
|
|
118
|
+
const restored = restoreSessionStateWithDiagnostics(ctx.sessionManager.getBranch());
|
|
112
119
|
const state: RuntimeState = {
|
|
113
|
-
effective: resolveDelegateState(loaded.defaults, session),
|
|
120
|
+
effective: resolveDelegateState(loaded.defaults, restored.session),
|
|
114
121
|
global: loaded.defaults,
|
|
115
|
-
session,
|
|
116
|
-
diagnostics: loaded.diagnostics,
|
|
122
|
+
session: restored.session,
|
|
123
|
+
diagnostics: [...loaded.diagnostics, ...restored.diagnostics],
|
|
117
124
|
modelStatuses: new Map(),
|
|
118
125
|
runtimeErrors: [],
|
|
119
126
|
};
|
|
@@ -129,8 +136,21 @@ export function validateRuntime(ctx: ExtensionContext, state: RuntimeState): voi
|
|
|
129
136
|
if (state.effective.intensity === "off") return;
|
|
130
137
|
|
|
131
138
|
for (const diagnostic of state.diagnostics) state.runtimeErrors.push(diagnostic.message);
|
|
132
|
-
for (const role of MODEL_ROLES)
|
|
133
|
-
|
|
139
|
+
for (const role of MODEL_ROLES) {
|
|
140
|
+
const setting = state.effective[role];
|
|
141
|
+
if (setting === undefined) {
|
|
142
|
+
state.runtimeErrors.push(
|
|
143
|
+
`${ROLE_LABELS[role]} model is not configured; configure it or explicitly disable it.`,
|
|
144
|
+
);
|
|
145
|
+
} else if (isRoleEnabled(setting)) {
|
|
146
|
+
validateEnabledRole(ctx, state, role, setting);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (enabledOrdinaryRoles(state.effective).length === 0) {
|
|
150
|
+
state.runtimeErrors.push("At least one ordinary role must be enabled.");
|
|
151
|
+
}
|
|
152
|
+
if (state.effective.uiDesign)
|
|
153
|
+
validateEnabledRole(ctx, state, "uiDesign", state.effective.uiDesign);
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
export function hasRuntimeError(state: RuntimeState): boolean {
|
|
@@ -143,13 +163,7 @@ export function statusLabel(state: RuntimeState): string {
|
|
|
143
163
|
return state.effective.intensity === "normal" ? "D:NORM" : "D:AGG";
|
|
144
164
|
}
|
|
145
165
|
|
|
146
|
-
export function formatModelRef(reference: ModelRef | undefined): string {
|
|
166
|
+
export function formatModelRef(reference: ModelRef | null | undefined): string {
|
|
167
|
+
if (reference === null) return "disabled";
|
|
147
168
|
return reference ? `${reference.provider}/${reference.model}` : "not configured";
|
|
148
169
|
}
|
|
149
|
-
|
|
150
|
-
export function sessionEntry(
|
|
151
|
-
pi: { appendEntry: (type: string, data?: unknown) => void },
|
|
152
|
-
state: RuntimeState,
|
|
153
|
-
): void {
|
|
154
|
-
pi.appendEntry(SESSION_ENTRY_TYPE, state.session);
|
|
155
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
|
+
export const CURRENT_SCHEMA_VERSION = 3 as const;
|
|
4
|
+
|
|
3
5
|
export const INTENSITIES = ["off", "normal", "aggressive"] as const;
|
|
4
6
|
export type Intensity = (typeof INTENSITIES)[number];
|
|
5
7
|
|
|
@@ -14,7 +16,7 @@ export const ROLE_LABELS: Record<ModelConfigKey, string> = {
|
|
|
14
16
|
small: "Small",
|
|
15
17
|
medium: "Medium",
|
|
16
18
|
large: "Large",
|
|
17
|
-
uiDesign: "
|
|
19
|
+
uiDesign: "Visual Design",
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export type ModelRef = {
|
|
@@ -22,24 +24,27 @@ export type ModelRef = {
|
|
|
22
24
|
model: string;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
// Absent session properties inherit. Null explicitly disables an ordinary role.
|
|
28
|
+
export type OrdinaryRoleSetting = ModelRef | null;
|
|
29
|
+
|
|
25
30
|
export type GlobalDefaults = {
|
|
26
|
-
schemaVersion:
|
|
31
|
+
schemaVersion: typeof CURRENT_SCHEMA_VERSION;
|
|
27
32
|
intensity?: Intensity;
|
|
28
33
|
preference?: Preference;
|
|
29
|
-
small?:
|
|
30
|
-
medium?:
|
|
31
|
-
large?:
|
|
34
|
+
small?: OrdinaryRoleSetting;
|
|
35
|
+
medium?: OrdinaryRoleSetting;
|
|
36
|
+
large?: OrdinaryRoleSetting;
|
|
32
37
|
uiDesign?: ModelRef;
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
export type SessionDelegateState = {
|
|
36
|
-
schemaVersion:
|
|
41
|
+
schemaVersion: typeof CURRENT_SCHEMA_VERSION;
|
|
37
42
|
intensity?: Intensity;
|
|
38
43
|
preference?: Preference;
|
|
39
|
-
small?:
|
|
40
|
-
medium?:
|
|
41
|
-
large?:
|
|
42
|
-
// Null explicitly disables a global
|
|
44
|
+
small?: OrdinaryRoleSetting;
|
|
45
|
+
medium?: OrdinaryRoleSetting;
|
|
46
|
+
large?: OrdinaryRoleSetting;
|
|
47
|
+
// Null explicitly disables a global Visual Design role for this session branch.
|
|
43
48
|
uiDesign?: ModelRef | null;
|
|
44
49
|
};
|
|
45
50
|
|
|
@@ -48,9 +53,9 @@ export type ValueSource = "default" | "global" | "session";
|
|
|
48
53
|
export type EffectiveDelegateState = {
|
|
49
54
|
intensity: Intensity;
|
|
50
55
|
preference: Preference;
|
|
51
|
-
small?:
|
|
52
|
-
medium?:
|
|
53
|
-
large?:
|
|
56
|
+
small?: OrdinaryRoleSetting;
|
|
57
|
+
medium?: OrdinaryRoleSetting;
|
|
58
|
+
large?: OrdinaryRoleSetting;
|
|
54
59
|
uiDesign?: ModelRef;
|
|
55
60
|
source: {
|
|
56
61
|
intensity: ValueSource;
|
|
@@ -70,9 +75,9 @@ export type ModelStatus =
|
|
|
70
75
|
| { kind: "no-credentials" };
|
|
71
76
|
|
|
72
77
|
export function emptyGlobalDefaults(): GlobalDefaults {
|
|
73
|
-
return { schemaVersion:
|
|
78
|
+
return { schemaVersion: CURRENT_SCHEMA_VERSION };
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
export function emptySessionState(): SessionDelegateState {
|
|
77
|
-
return { schemaVersion:
|
|
82
|
+
return { schemaVersion: CURRENT_SCHEMA_VERSION };
|
|
78
83
|
}
|
package/src/ui.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
appendGuardedSessionState,
|
|
3
4
|
defaultsFromEffectiveState,
|
|
4
5
|
getGlobalConfigPath,
|
|
5
6
|
resolveDelegateState,
|
|
6
7
|
writeConfig,
|
|
7
8
|
} from "./config.ts";
|
|
8
9
|
import { DelegatePanel, type DelegatePanelResult } from "./delegate-panel.ts";
|
|
9
|
-
import { hasRuntimeError, loadRuntime, modelCandidates
|
|
10
|
+
import { hasRuntimeError, loadRuntime, modelCandidates } from "./runtime.ts";
|
|
10
11
|
|
|
11
12
|
export async function openDelegateEditor(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
|
12
13
|
if (!ctx.hasUI) return;
|
|
@@ -29,13 +30,23 @@ export async function openDelegateEditor(ctx: ExtensionContext, pi: ExtensionAPI
|
|
|
29
30
|
global: state.global,
|
|
30
31
|
session: state.session,
|
|
31
32
|
candidates,
|
|
32
|
-
diagnostics:
|
|
33
|
+
diagnostics:
|
|
34
|
+
state.effective.intensity === "off"
|
|
35
|
+
? state.diagnostics
|
|
36
|
+
.filter(({ reportWhenOff }) => reportWhenOff)
|
|
37
|
+
.map(({ message }) => message)
|
|
38
|
+
: [...state.runtimeErrors],
|
|
33
39
|
hasRuntimeError: hasRuntimeError(state),
|
|
34
40
|
onApply: async (draft) => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
state.
|
|
38
|
-
|
|
41
|
+
const result = appendGuardedSessionState(pi, structuredClone(draft));
|
|
42
|
+
const refreshed = await loadRuntime(ctx);
|
|
43
|
+
state.global = refreshed.global;
|
|
44
|
+
state.session = refreshed.session;
|
|
45
|
+
state.diagnostics = refreshed.diagnostics;
|
|
46
|
+
state.effective = refreshed.effective;
|
|
47
|
+
state.modelStatuses = refreshed.modelStatuses;
|
|
48
|
+
state.runtimeErrors = refreshed.runtimeErrors;
|
|
49
|
+
return result === "success";
|
|
39
50
|
},
|
|
40
51
|
onSaveDefaults: async (draft) => {
|
|
41
52
|
try {
|