pi-delegation-policy 0.5.0 → 0.7.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 +18 -0
- package/README.md +23 -22
- package/SECURITY.md +1 -1
- package/examples/global.json +2 -5
- package/package.json +4 -4
- package/schema/delegation-policy.schema.json +8 -5
- package/src/config.ts +254 -74
- package/src/delegate-panel.ts +23 -15
- package/src/index.ts +45 -24
- package/src/prompt.ts +88 -24
- package/src/runtime.ts +47 -28
- package/src/types.ts +19 -14
- package/src/ui.ts +18 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.0 - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add the `orchestrator` intensity with `/delegate orchestrator` and `D:ORCH`. It delegates transferable research, detailed planning, implementation, testing, writing, review, and integration mechanics while keeping objectives, critical decisions, coordination, evidence, and final acceptance with the main agent.
|
|
10
|
+
- Add orchestrator guidance for batching small work, concise briefs and results with file references, and avoiding duplicate inspection without a concrete gap or risk.
|
|
11
|
+
|
|
12
|
+
## 0.6.0 - 2026-08-29
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Allow Small, Medium, and Large to be explicitly disabled independently while requiring one enabled ordinary role for an active policy.
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
5
23
|
## 0.5.0 - 2026-08-28
|
|
6
24
|
|
|
7
25
|
### Added
|
package/README.md
CHANGED
|
@@ -1,25 +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.7.0** is the latest published package and supports `off`, `normal`, `aggressive`, and `orchestrator`. 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 `
|
|
12
|
-
- Configure exact provider/model
|
|
11
|
+
- Choose `off`, `normal`, `aggressive`, or `orchestrator` 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
|
-
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.
|
|
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. `orchestrator` delegates transferable execution detail and batches small work while the main agent retains objectives, critical decisions, coordination, integration responsibility, requested evidence, and final acceptance. 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
19
|
|
|
20
|
-
|
|
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.
|
|
21
|
+
|
|
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.
|
|
21
23
|
|
|
22
|
-
Install
|
|
24
|
+
## Install and start
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
27
|
pi install npm:pi-delegation-policy
|
|
@@ -27,12 +29,16 @@ pi install npm:pi-delegation-policy
|
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
1. Open `/delegate` (or press `Alt+G` in Pi's TUI).
|
|
30
|
-
2.
|
|
31
|
-
3. Select `normal` or `
|
|
32
|
-
4. Run `/delegate status`
|
|
33
|
-
5. The applied state affects the **next** agent run.
|
|
32
|
+
2. For Small, Medium, and Large, select an exact authenticated provider/model or **Disable for this session**. Keep at least one enabled.
|
|
33
|
+
3. Select `normal`, `aggressive`, or `orchestrator`, then choose **Apply changes**.
|
|
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.
|
|
36
|
+
|
|
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 without rewriting them. Schema 3 stores `null` for an explicitly disabled ordinary role. Session changes write a schema 2 `off` guard before the schema 3 state; saving defaults changes only the global file.
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
Before downgrading to `0.6.0`, change the global intensity to `off`, `normal`, or `aggressive` and run `/delegate off` in every active branch. For `<=0.5.0`, also convert global defaults to schema 2 and replace ordinary `null` values with exact model references. Schema 2 never accepts `orchestrator`. See the configuration reference for details.
|
|
40
|
+
|
|
41
|
+
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/).
|
|
36
42
|
|
|
37
43
|
## Essential commands
|
|
38
44
|
|
|
@@ -41,17 +47,12 @@ See the [end-to-end getting-started guide](https://yivas.github.io/pi-delegation
|
|
|
41
47
|
/delegate off Disable policy for this session branch
|
|
42
48
|
/delegate normal Enable balanced delegation guidance
|
|
43
49
|
/delegate aggressive Enable delegation-first guidance
|
|
50
|
+
/delegate orchestrator Minimize main-agent execution and narration
|
|
44
51
|
/delegate status Show effective session state
|
|
45
52
|
/delegate reset Reset this branch to off and other fields to global defaults
|
|
46
53
|
```
|
|
47
54
|
|
|
48
|
-
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.
|
|
49
|
-
|
|
50
|
-
## Configuration and safety
|
|
51
|
-
|
|
52
|
-
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 Visual Design reference under the stable `uiDesign` key; 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`.
|
|
53
|
-
|
|
54
|
-
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).
|
|
55
|
+
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.
|
|
55
56
|
|
|
56
57
|
## Development
|
|
57
58
|
|
|
@@ -65,8 +66,8 @@ npm run build
|
|
|
65
66
|
npm run pack:check
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
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)
|
|
69
|
+
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).
|
|
69
70
|
|
|
70
71
|
## License
|
|
71
72
|
|
|
72
|
-
MIT. See [LICENSE](
|
|
73
|
+
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.7.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-delegation-policy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A Pi extension for configurable delegation intensity and exact subagent role model references.",
|
|
6
6
|
"type": "module",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"@earendil-works/pi-ai": "0.84.3",
|
|
45
45
|
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
46
46
|
"@earendil-works/pi-tui": "0.84.3",
|
|
47
|
-
"@eslint/js": "^
|
|
48
|
-
"@types/node": "^
|
|
47
|
+
"@eslint/js": "^10.0.1",
|
|
48
|
+
"@types/node": "^26.2.0",
|
|
49
49
|
"ajv": "^8.20.0",
|
|
50
|
-
"eslint": "^9.
|
|
50
|
+
"eslint": "^10.9.1",
|
|
51
51
|
"prettier": "^3.0.0",
|
|
52
52
|
"typescript": "^5.7.0",
|
|
53
53
|
"typescript-eslint": "^8.68.0"
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["schemaVersion"],
|
|
7
7
|
"properties": {
|
|
8
|
-
"schemaVersion": { "const":
|
|
9
|
-
"intensity": { "enum": ["off", "normal", "aggressive"] },
|
|
8
|
+
"schemaVersion": { "const": 3 },
|
|
9
|
+
"intensity": { "enum": ["off", "normal", "aggressive", "orchestrator"] },
|
|
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,16 @@ 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,
|
|
6
|
+
INTENSITIES,
|
|
5
7
|
emptyGlobalDefaults,
|
|
6
8
|
emptySessionState,
|
|
7
9
|
type EffectiveDelegateState,
|
|
8
10
|
type GlobalDefaults,
|
|
9
11
|
type Intensity,
|
|
10
12
|
type ModelRef,
|
|
13
|
+
type ModelRole,
|
|
14
|
+
type OrdinaryRoleSetting,
|
|
11
15
|
type Preference,
|
|
12
16
|
type SessionDelegateState,
|
|
13
17
|
type ValueSource,
|
|
@@ -19,11 +23,23 @@ export const SESSION_ENTRY_TYPE = "pi-delegation-policy:session";
|
|
|
19
23
|
export const GLOBAL_CONFIG_NAME = "delegation-policy.json";
|
|
20
24
|
|
|
21
25
|
const LEGACY_SCHEMA_MESSAGE =
|
|
22
|
-
"Global defaults use schema version 1. Configure schema version
|
|
26
|
+
"Global defaults use schema version 1. Configure schema version 3 with /delegate before activating delegation.";
|
|
23
27
|
const INVALID_CONFIG_MESSAGE = "Global defaults are invalid. Configure them again with /delegate.";
|
|
28
|
+
const INVALID_SESSION_MESSAGE =
|
|
29
|
+
"The latest delegation session state is invalid or unsupported. Delegation is off for safety.";
|
|
30
|
+
const CONFIG_KEYS = [
|
|
31
|
+
"schemaVersion",
|
|
32
|
+
"intensity",
|
|
33
|
+
"preference",
|
|
34
|
+
"small",
|
|
35
|
+
"medium",
|
|
36
|
+
"large",
|
|
37
|
+
"uiDesign",
|
|
38
|
+
] as const;
|
|
24
39
|
|
|
25
40
|
export type ConfigDiagnostic = {
|
|
26
41
|
message: string;
|
|
42
|
+
reportWhenOff?: boolean;
|
|
27
43
|
};
|
|
28
44
|
|
|
29
45
|
export type LoadedDefaults = {
|
|
@@ -31,6 +47,26 @@ export type LoadedDefaults = {
|
|
|
31
47
|
diagnostics: ConfigDiagnostic[];
|
|
32
48
|
};
|
|
33
49
|
|
|
50
|
+
export type RestoredSessionState = {
|
|
51
|
+
session: SessionDelegateState;
|
|
52
|
+
diagnostics: ConfigDiagnostic[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type SessionEntryWriter = { appendEntry: (type: string, data?: unknown) => void };
|
|
56
|
+
export type GuardedAppendResult = "success" | "guard-failed" | "state-failed";
|
|
57
|
+
|
|
58
|
+
type Schema2GlobalDefaults = {
|
|
59
|
+
schemaVersion: 2;
|
|
60
|
+
intensity?: LegacyIntensity;
|
|
61
|
+
preference?: Preference;
|
|
62
|
+
small?: ModelRef;
|
|
63
|
+
medium?: ModelRef;
|
|
64
|
+
large?: ModelRef;
|
|
65
|
+
uiDesign?: ModelRef;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type Schema2SessionState = Omit<Schema2GlobalDefaults, "uiDesign"> & { uiDesign?: ModelRef | null };
|
|
69
|
+
|
|
34
70
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
71
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
72
|
}
|
|
@@ -40,7 +76,14 @@ function hasOnlyKeys(value: Record<string, unknown>, allowedKeys: readonly strin
|
|
|
40
76
|
}
|
|
41
77
|
|
|
42
78
|
function isIntensity(value: unknown): value is Intensity {
|
|
43
|
-
return
|
|
79
|
+
return INTENSITIES.some((intensity) => intensity === value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const LEGACY_INTENSITIES = ["off", "normal", "aggressive"] as const;
|
|
83
|
+
type LegacyIntensity = (typeof LEGACY_INTENSITIES)[number];
|
|
84
|
+
|
|
85
|
+
function isLegacyIntensity(value: unknown): value is LegacyIntensity {
|
|
86
|
+
return LEGACY_INTENSITIES.some((intensity) => intensity === value);
|
|
44
87
|
}
|
|
45
88
|
|
|
46
89
|
function isPreference(value: unknown): value is Preference {
|
|
@@ -60,33 +103,31 @@ function parseModelRef(value: unknown): ModelRef | undefined {
|
|
|
60
103
|
return { provider: value.provider, model: value.model };
|
|
61
104
|
}
|
|
62
105
|
|
|
63
|
-
function
|
|
64
|
-
return value ? { ...value } : undefined;
|
|
106
|
+
function copyRoleSetting(value: OrdinaryRoleSetting | undefined): OrdinaryRoleSetting | undefined {
|
|
107
|
+
return value === null ? null : value ? { ...value } : undefined;
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
(value.intensity !== undefined && !isIntensity(value.intensity)) ||
|
|
81
|
-
(value.preference !== undefined && !isPreference(value.preference))
|
|
82
|
-
)
|
|
83
|
-
return undefined;
|
|
110
|
+
function hasValidEnvelope(
|
|
111
|
+
value: unknown,
|
|
112
|
+
schemaVersion: 2 | 3,
|
|
113
|
+
intensityValidator: (value: unknown) => boolean = isIntensity,
|
|
114
|
+
): value is Record<string, unknown> {
|
|
115
|
+
return (
|
|
116
|
+
isRecord(value) &&
|
|
117
|
+
hasOnlyKeys(value, CONFIG_KEYS) &&
|
|
118
|
+
value.schemaVersion === schemaVersion &&
|
|
119
|
+
(value.intensity === undefined || intensityValidator(value.intensity)) &&
|
|
120
|
+
(value.preference === undefined || isPreference(value.preference))
|
|
121
|
+
);
|
|
122
|
+
}
|
|
84
123
|
|
|
124
|
+
function parseSchema2Roles(
|
|
125
|
+
value: Record<string, unknown>,
|
|
126
|
+
): Pick<Schema2GlobalDefaults, "small" | "medium" | "large" | "uiDesign"> | undefined {
|
|
85
127
|
const small = value.small === undefined ? undefined : parseModelRef(value.small);
|
|
86
128
|
const medium = value.medium === undefined ? undefined : parseModelRef(value.medium);
|
|
87
129
|
const large = value.large === undefined ? undefined : parseModelRef(value.large);
|
|
88
130
|
const uiDesign = value.uiDesign === undefined ? undefined : parseModelRef(value.uiDesign);
|
|
89
|
-
|
|
90
131
|
if (
|
|
91
132
|
(value.small !== undefined && !small) ||
|
|
92
133
|
(value.medium !== undefined && !medium) ||
|
|
@@ -94,11 +135,7 @@ export function parseConfig(value: unknown): GlobalDefaults | undefined {
|
|
|
94
135
|
(value.uiDesign !== undefined && !uiDesign)
|
|
95
136
|
)
|
|
96
137
|
return undefined;
|
|
97
|
-
|
|
98
138
|
return {
|
|
99
|
-
schemaVersion: 2,
|
|
100
|
-
...(value.intensity ? { intensity: value.intensity } : {}),
|
|
101
|
-
...(value.preference ? { preference: value.preference } : {}),
|
|
102
139
|
...(small ? { small } : {}),
|
|
103
140
|
...(medium ? { medium } : {}),
|
|
104
141
|
...(large ? { large } : {}),
|
|
@@ -106,51 +143,147 @@ export function parseConfig(value: unknown): GlobalDefaults | undefined {
|
|
|
106
143
|
};
|
|
107
144
|
}
|
|
108
145
|
|
|
109
|
-
export function
|
|
146
|
+
export function parseSchema2Config(value: unknown): Schema2GlobalDefaults | undefined {
|
|
147
|
+
if (!hasValidEnvelope(value, 2, isLegacyIntensity)) return undefined;
|
|
148
|
+
const intensity = value.intensity;
|
|
149
|
+
if (intensity !== undefined && !isLegacyIntensity(intensity)) return undefined;
|
|
150
|
+
const roles = parseSchema2Roles(value);
|
|
151
|
+
if (!roles) return undefined;
|
|
152
|
+
return {
|
|
153
|
+
schemaVersion: 2,
|
|
154
|
+
...(intensity ? { intensity } : {}),
|
|
155
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
156
|
+
...roles,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function parseSchema3Config(value: unknown): GlobalDefaults | undefined {
|
|
161
|
+
if (!hasValidEnvelope(value, CURRENT_SCHEMA_VERSION)) return undefined;
|
|
162
|
+
const intensity = value.intensity;
|
|
163
|
+
if (intensity !== undefined && !isIntensity(intensity)) return undefined;
|
|
164
|
+
const parseOrdinary = (setting: unknown): OrdinaryRoleSetting | undefined =>
|
|
165
|
+
setting === null ? null : parseModelRef(setting);
|
|
166
|
+
const small = value.small === undefined ? undefined : parseOrdinary(value.small);
|
|
167
|
+
const medium = value.medium === undefined ? undefined : parseOrdinary(value.medium);
|
|
168
|
+
const large = value.large === undefined ? undefined : parseOrdinary(value.large);
|
|
169
|
+
const uiDesign = value.uiDesign === undefined ? undefined : parseModelRef(value.uiDesign);
|
|
110
170
|
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))
|
|
171
|
+
(value.small !== undefined && small === undefined) ||
|
|
172
|
+
(value.medium !== undefined && medium === undefined) ||
|
|
173
|
+
(value.large !== undefined && large === undefined) ||
|
|
174
|
+
(value.uiDesign !== undefined && !uiDesign)
|
|
124
175
|
)
|
|
125
176
|
return undefined;
|
|
177
|
+
return {
|
|
178
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
179
|
+
...(intensity ? { intensity } : {}),
|
|
180
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
181
|
+
...(small !== undefined ? { small } : {}),
|
|
182
|
+
...(medium !== undefined ? { medium } : {}),
|
|
183
|
+
...(large !== undefined ? { large } : {}),
|
|
184
|
+
...(uiDesign ? { uiDesign } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
126
187
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
188
|
+
function migrateSchema2Config(value: Schema2GlobalDefaults): GlobalDefaults {
|
|
189
|
+
return {
|
|
190
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
191
|
+
...(value.intensity ? { intensity: value.intensity } : {}),
|
|
192
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
193
|
+
...(value.small ? { small: { ...value.small } } : {}),
|
|
194
|
+
...(value.medium ? { medium: { ...value.medium } } : {}),
|
|
195
|
+
...(value.large ? { large: { ...value.large } } : {}),
|
|
196
|
+
...(value.uiDesign ? { uiDesign: { ...value.uiDesign } } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function parseConfig(value: unknown): GlobalDefaults | undefined {
|
|
201
|
+
return (
|
|
202
|
+
parseSchema3Config(value) ??
|
|
203
|
+
(() => {
|
|
204
|
+
const schema2 = parseSchema2Config(value);
|
|
205
|
+
return schema2 ? migrateSchema2Config(schema2) : undefined;
|
|
206
|
+
})()
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseSchema2SessionState(value: unknown): Schema2SessionState | undefined {
|
|
211
|
+
if (!hasValidEnvelope(value, 2, isLegacyIntensity)) return undefined;
|
|
212
|
+
const intensity = value.intensity;
|
|
213
|
+
if (intensity !== undefined && !isLegacyIntensity(intensity)) return undefined;
|
|
130
214
|
const uiDesign =
|
|
131
215
|
value.uiDesign === undefined || value.uiDesign === null
|
|
132
216
|
? value.uiDesign
|
|
133
217
|
: parseModelRef(value.uiDesign);
|
|
218
|
+
const roles = parseSchema2Roles({ ...value, uiDesign: undefined });
|
|
219
|
+
if (!roles || (value.uiDesign !== undefined && value.uiDesign !== null && !uiDesign))
|
|
220
|
+
return undefined;
|
|
221
|
+
return {
|
|
222
|
+
schemaVersion: 2,
|
|
223
|
+
...(intensity ? { intensity } : {}),
|
|
224
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
225
|
+
...roles,
|
|
226
|
+
...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
134
229
|
|
|
230
|
+
function parseSchema3SessionState(value: unknown): SessionDelegateState | undefined {
|
|
231
|
+
if (!hasValidEnvelope(value, CURRENT_SCHEMA_VERSION)) return undefined;
|
|
232
|
+
const intensity = value.intensity;
|
|
233
|
+
if (intensity !== undefined && !isIntensity(intensity)) return undefined;
|
|
234
|
+
const parseOrdinary = (setting: unknown): OrdinaryRoleSetting | undefined =>
|
|
235
|
+
setting === null ? null : parseModelRef(setting);
|
|
236
|
+
const small = value.small === undefined ? undefined : parseOrdinary(value.small);
|
|
237
|
+
const medium = value.medium === undefined ? undefined : parseOrdinary(value.medium);
|
|
238
|
+
const large = value.large === undefined ? undefined : parseOrdinary(value.large);
|
|
239
|
+
const uiDesign =
|
|
240
|
+
value.uiDesign === undefined || value.uiDesign === null
|
|
241
|
+
? value.uiDesign
|
|
242
|
+
: parseModelRef(value.uiDesign);
|
|
135
243
|
if (
|
|
136
|
-
(value.small !== undefined &&
|
|
137
|
-
(value.medium !== undefined &&
|
|
138
|
-
(value.large !== undefined &&
|
|
244
|
+
(value.small !== undefined && small === undefined) ||
|
|
245
|
+
(value.medium !== undefined && medium === undefined) ||
|
|
246
|
+
(value.large !== undefined && large === undefined) ||
|
|
139
247
|
(value.uiDesign !== undefined && value.uiDesign !== null && !uiDesign)
|
|
140
248
|
)
|
|
141
249
|
return undefined;
|
|
250
|
+
return {
|
|
251
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
252
|
+
...(intensity ? { intensity } : {}),
|
|
253
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
254
|
+
...(small !== undefined ? { small } : {}),
|
|
255
|
+
...(medium !== undefined ? { medium } : {}),
|
|
256
|
+
...(large !== undefined ? { large } : {}),
|
|
257
|
+
...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
142
260
|
|
|
261
|
+
function migrateSchema2SessionState(value: Schema2SessionState): SessionDelegateState {
|
|
143
262
|
return {
|
|
144
|
-
schemaVersion:
|
|
263
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
145
264
|
...(value.intensity ? { intensity: value.intensity } : {}),
|
|
146
|
-
...(value.preference ? { preference: value.preference } : {}),
|
|
147
|
-
...(small ? { small } : {}),
|
|
148
|
-
...(medium ? { medium } : {}),
|
|
149
|
-
...(large ? { large } : {}),
|
|
150
|
-
...(uiDesign === null
|
|
265
|
+
...(value.preference ? { preference: value.preference as Preference } : {}),
|
|
266
|
+
...(value.small ? { small: { ...value.small } } : {}),
|
|
267
|
+
...(value.medium ? { medium: { ...value.medium } } : {}),
|
|
268
|
+
...(value.large ? { large: { ...value.large } } : {}),
|
|
269
|
+
...(value.uiDesign === null
|
|
270
|
+
? { uiDesign: null }
|
|
271
|
+
: value.uiDesign
|
|
272
|
+
? { uiDesign: { ...value.uiDesign } }
|
|
273
|
+
: {}),
|
|
151
274
|
};
|
|
152
275
|
}
|
|
153
276
|
|
|
277
|
+
export function parseSessionState(value: unknown): SessionDelegateState | undefined {
|
|
278
|
+
return (
|
|
279
|
+
parseSchema3SessionState(value) ??
|
|
280
|
+
(() => {
|
|
281
|
+
const schema2 = parseSchema2SessionState(value);
|
|
282
|
+
return schema2 ? migrateSchema2SessionState(schema2) : undefined;
|
|
283
|
+
})()
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
154
287
|
function isLegacyConfig(value: unknown): boolean {
|
|
155
288
|
return isRecord(value) && value.schemaVersion === 1;
|
|
156
289
|
}
|
|
@@ -195,64 +328,111 @@ async function atomicWrite(path: string, value: unknown): Promise<void> {
|
|
|
195
328
|
}
|
|
196
329
|
|
|
197
330
|
export async function writeConfig(path: string, defaults: GlobalDefaults): Promise<void> {
|
|
198
|
-
const parsed =
|
|
331
|
+
const parsed = parseSchema3Config(defaults);
|
|
199
332
|
if (!parsed) throw new Error("Refusing to write invalid delegation policy defaults.");
|
|
200
333
|
await atomicWrite(path, parsed);
|
|
201
334
|
}
|
|
202
335
|
|
|
203
|
-
export function
|
|
336
|
+
export function restoreSessionStateWithDiagnostics(entries: unknown[]): RestoredSessionState {
|
|
204
337
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
205
338
|
const entry = entries[index] as Record<string, unknown> | undefined;
|
|
206
339
|
if (entry?.type !== "custom" || entry.customType !== SESSION_ENTRY_TYPE) continue;
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
340
|
+
const session = parseSessionState(entry.data);
|
|
341
|
+
if (session) return { session, diagnostics: [] };
|
|
342
|
+
return {
|
|
343
|
+
session: { schemaVersion: CURRENT_SCHEMA_VERSION, intensity: "off" },
|
|
344
|
+
diagnostics: [{ message: INVALID_SESSION_MESSAGE, reportWhenOff: true }],
|
|
345
|
+
};
|
|
209
346
|
}
|
|
210
|
-
return emptySessionState();
|
|
347
|
+
return { session: emptySessionState(), diagnostics: [] };
|
|
211
348
|
}
|
|
212
349
|
|
|
213
|
-
function
|
|
214
|
-
|
|
350
|
+
export function restoreSessionState(entries: unknown[]): SessionDelegateState {
|
|
351
|
+
return restoreSessionStateWithDiagnostics(entries).session;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sourceFor<T>(
|
|
355
|
+
session: Record<string, unknown>,
|
|
356
|
+
globalValue: T | undefined,
|
|
357
|
+
key: string,
|
|
358
|
+
): ValueSource {
|
|
359
|
+
if (Object.hasOwn(session, key)) return "session";
|
|
215
360
|
if (globalValue !== undefined) return "global";
|
|
216
361
|
return "default";
|
|
217
362
|
}
|
|
218
363
|
|
|
364
|
+
function resolveRole(
|
|
365
|
+
globalValue: OrdinaryRoleSetting | undefined,
|
|
366
|
+
session: SessionDelegateState,
|
|
367
|
+
role: ModelRole,
|
|
368
|
+
): OrdinaryRoleSetting | undefined {
|
|
369
|
+
return copyRoleSetting(Object.hasOwn(session, role) ? session[role] : globalValue);
|
|
370
|
+
}
|
|
371
|
+
|
|
219
372
|
export function resolveDelegateState(
|
|
220
373
|
defaults: GlobalDefaults,
|
|
221
374
|
session: SessionDelegateState,
|
|
222
375
|
): EffectiveDelegateState {
|
|
223
376
|
const uiDesign =
|
|
224
377
|
session.uiDesign === undefined
|
|
225
|
-
?
|
|
378
|
+
? defaults.uiDesign
|
|
379
|
+
? { ...defaults.uiDesign }
|
|
380
|
+
: undefined
|
|
226
381
|
: session.uiDesign === null
|
|
227
382
|
? undefined
|
|
228
|
-
:
|
|
383
|
+
: { ...session.uiDesign };
|
|
384
|
+
const small = resolveRole(defaults.small, session, "small");
|
|
385
|
+
const medium = resolveRole(defaults.medium, session, "medium");
|
|
386
|
+
const large = resolveRole(defaults.large, session, "large");
|
|
229
387
|
|
|
230
388
|
return {
|
|
231
389
|
intensity: session.intensity ?? defaults.intensity ?? "off",
|
|
232
390
|
preference: session.preference ?? defaults.preference ?? "standard",
|
|
233
|
-
small
|
|
234
|
-
medium
|
|
235
|
-
large
|
|
391
|
+
...(small !== undefined ? { small } : {}),
|
|
392
|
+
...(medium !== undefined ? { medium } : {}),
|
|
393
|
+
...(large !== undefined ? { large } : {}),
|
|
236
394
|
...(uiDesign ? { uiDesign } : {}),
|
|
237
395
|
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
|
|
396
|
+
intensity: sourceFor(session, defaults.intensity, "intensity"),
|
|
397
|
+
preference: sourceFor(session, defaults.preference, "preference"),
|
|
398
|
+
small: sourceFor(session, defaults.small, "small"),
|
|
399
|
+
medium: sourceFor(session, defaults.medium, "medium"),
|
|
400
|
+
large: sourceFor(session, defaults.large, "large"),
|
|
401
|
+
uiDesign: sourceFor(session, defaults.uiDesign, "uiDesign"),
|
|
244
402
|
},
|
|
245
403
|
};
|
|
246
404
|
}
|
|
247
405
|
|
|
248
406
|
export function defaultsFromEffectiveState(state: EffectiveDelegateState): GlobalDefaults {
|
|
407
|
+
const role = (setting: OrdinaryRoleSetting | undefined) =>
|
|
408
|
+
setting === undefined ? {} : setting === null ? { value: null } : { value: { ...setting } };
|
|
409
|
+
const small = role(state.small);
|
|
410
|
+
const medium = role(state.medium);
|
|
411
|
+
const large = role(state.large);
|
|
249
412
|
return {
|
|
250
|
-
schemaVersion:
|
|
413
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
251
414
|
intensity: state.intensity,
|
|
252
415
|
preference: state.preference,
|
|
253
|
-
...(
|
|
254
|
-
...(
|
|
255
|
-
...(
|
|
416
|
+
...("value" in small ? { small: small.value } : {}),
|
|
417
|
+
...("value" in medium ? { medium: medium.value } : {}),
|
|
418
|
+
...("value" in large ? { large: large.value } : {}),
|
|
256
419
|
...(state.uiDesign ? { uiDesign: { ...state.uiDesign } } : {}),
|
|
257
420
|
};
|
|
258
421
|
}
|
|
422
|
+
|
|
423
|
+
export function appendGuardedSessionState(
|
|
424
|
+
pi: SessionEntryWriter,
|
|
425
|
+
session: SessionDelegateState,
|
|
426
|
+
): GuardedAppendResult {
|
|
427
|
+
try {
|
|
428
|
+
pi.appendEntry(SESSION_ENTRY_TYPE, { schemaVersion: 2, intensity: "off" });
|
|
429
|
+
} catch {
|
|
430
|
+
return "guard-failed";
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
pi.appendEntry(SESSION_ENTRY_TYPE, session);
|
|
434
|
+
return "success";
|
|
435
|
+
} catch {
|
|
436
|
+
return "state-failed";
|
|
437
|
+
}
|
|
438
|
+
}
|