@principles/host-runtime 0.7.10 → 0.7.12
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActivatedPrinciple } from '@principles/core/runtime-v2';
|
|
1
|
+
import { type ActivatedPrinciple, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
2
2
|
export interface ActivePrinciplePromptResult {
|
|
3
3
|
additionalContext: string;
|
|
4
4
|
principleIds: string[];
|
|
@@ -11,6 +11,16 @@ export interface ActivePrinciplePromptResult {
|
|
|
11
11
|
excludedCount: number;
|
|
12
12
|
exclusionReason?: 'host_principle_overlap';
|
|
13
13
|
allValidatedPrinciplesExcluded: boolean;
|
|
14
|
+
/** PRI-904: budget-packing policy that produced this selection. */
|
|
15
|
+
selectionPolicy?: PromptSelectionPolicy;
|
|
16
|
+
/** PRI-904: candidates that reached the budget selector. */
|
|
17
|
+
eligibleCount?: number;
|
|
18
|
+
/** PRI-904: circular scan start; present only under fair_rotation_v1. */
|
|
19
|
+
rotationStartIndex?: number;
|
|
20
|
+
/** PRI-904 (bounded, max 16): activation ids dropped for insufficient remaining budget. */
|
|
21
|
+
droppedActivationIds?: string[];
|
|
22
|
+
/** PRI-904 (bounded, max 16): activation ids that cannot fit even in an empty payload. */
|
|
23
|
+
oversizedActivationIds?: string[];
|
|
14
24
|
}
|
|
15
25
|
export interface PromptActivationCandidates {
|
|
16
26
|
principles: ActivatedPrinciple[];
|
|
@@ -34,4 +44,10 @@ export declare function readPromptActivationCandidates(input: {
|
|
|
34
44
|
export declare function buildActivePrinciplePromptContext(input: {
|
|
35
45
|
workspaceDir: string;
|
|
36
46
|
excludePrincipleIds?: ReadonlySet<string>;
|
|
47
|
+
/**
|
|
48
|
+
* PRI-904 fair-rotation round key (derive via roundKeyFromRunIdentity from
|
|
49
|
+
* the host run/turn id). Absent → legacy FIFO prefix policy (rollback /
|
|
50
|
+
* characterization baseline).
|
|
51
|
+
*/
|
|
52
|
+
roundKey?: number;
|
|
37
53
|
}): Promise<ActivePrinciplePromptResult>;
|
|
@@ -3,6 +3,8 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
5
5
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
6
|
+
/** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
|
|
7
|
+
const MAX_SHARED_DIAGNOSTIC_IDS = 16;
|
|
6
8
|
/**
|
|
7
9
|
* PR #1844 follow-up: the activation READ half of buildActivePrinciplePromptContext,
|
|
8
10
|
* exported so the console budget projection can share the exact same input
|
|
@@ -85,17 +87,49 @@ export async function buildActivePrinciplePromptContext(input) {
|
|
|
85
87
|
additionalContext: '', principleIds: [], activationIds: [], artifactIds: [], warnings,
|
|
86
88
|
budget: RUNTIME_V2_PRINCIPLE_BUDGET, truncated: false, excludedPrincipleIds,
|
|
87
89
|
excludedCount: excludedPrincipleIds.length, allValidatedPrinciplesExcluded: false,
|
|
90
|
+
selectionPolicy: 'legacy_fifo_prefix_v1', eligibleCount: 0,
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
93
|
const included = [];
|
|
91
94
|
let additionalContext = '';
|
|
92
95
|
let truncated = false;
|
|
93
|
-
|
|
96
|
+
// PRI-904: same fair-rotation semantics as the plugin route's trimToBudget —
|
|
97
|
+
// deterministic rotating start over the base (ASC) candidate order, circular
|
|
98
|
+
// scan, continue-on-non-fit. Costs are measured with the REAL serializer
|
|
99
|
+
// (renderPrinciplesToDirectives) so budget accounting stays exact here too.
|
|
100
|
+
const { roundKey } = input;
|
|
101
|
+
const selectionPolicy = roundKey === undefined || principles.length === 0 ? 'legacy_fifo_prefix_v1' : 'fair_rotation_v1';
|
|
102
|
+
const n = principles.length;
|
|
103
|
+
// Guard on the policy, not just the key: an all-excluded candidate list has
|
|
104
|
+
// n === 0 and must not emit a NaN start (core trimToBudget omits the field
|
|
105
|
+
// for empty input — parity requires the same here).
|
|
106
|
+
const rotationStartIndex = selectionPolicy === 'fair_rotation_v1' && roundKey !== undefined
|
|
107
|
+
? ((roundKey % n) + n) % n
|
|
108
|
+
: undefined;
|
|
109
|
+
const droppedActivationIds = [];
|
|
110
|
+
const oversizedActivationIds = [];
|
|
111
|
+
const standaloneFits = (principle) => renderPrinciplesToDirectives([principle], new Set([principle.principleId]), { escapeFn: escapeXml, selfReportInstruction: selfReportEnabled }).length
|
|
112
|
+
<= RUNTIME_V2_PRINCIPLE_BUDGET;
|
|
113
|
+
const scanOrder = rotationStartIndex === undefined
|
|
114
|
+
? principles
|
|
115
|
+
: principles.slice(rotationStartIndex).concat(principles.slice(0, rotationStartIndex));
|
|
116
|
+
for (const principle of scanOrder) {
|
|
94
117
|
const candidate = [...included, principle];
|
|
95
118
|
const candidateContext = renderPrinciplesToDirectives(candidate, new Set(candidate.map((entry) => entry.principleId)), { escapeFn: escapeXml, selfReportInstruction: selfReportEnabled });
|
|
96
119
|
if (candidateContext.length > RUNTIME_V2_PRINCIPLE_BUDGET) {
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
if (standaloneFits(principle)) {
|
|
121
|
+
// Fits alone but not now — normal budget truncation.
|
|
122
|
+
truncated = true;
|
|
123
|
+
if (droppedActivationIds.length < MAX_SHARED_DIAGNOSTIC_IDS)
|
|
124
|
+
droppedActivationIds.push(principle.activationId);
|
|
125
|
+
}
|
|
126
|
+
else if (oversizedActivationIds.length < MAX_SHARED_DIAGNOSTIC_IDS) {
|
|
127
|
+
// Can never fit alone — oversize, not starvation (PRI-904 SPEC §6.6).
|
|
128
|
+
oversizedActivationIds.push(principle.activationId);
|
|
129
|
+
}
|
|
130
|
+
if (selectionPolicy === 'legacy_fifo_prefix_v1')
|
|
131
|
+
break;
|
|
132
|
+
continue;
|
|
99
133
|
}
|
|
100
134
|
included.push(principle);
|
|
101
135
|
additionalContext = candidateContext;
|
|
@@ -116,5 +150,10 @@ export async function buildActivePrinciplePromptContext(input) {
|
|
|
116
150
|
excludedCount: excludedPrincipleIds.length,
|
|
117
151
|
allValidatedPrinciplesExcluded: excludedPrincipleIds.length > 0 && principles.length === 0,
|
|
118
152
|
...(excludedPrincipleIds.length > 0 ? { exclusionReason: 'host_principle_overlap' } : {}),
|
|
153
|
+
selectionPolicy,
|
|
154
|
+
eligibleCount: principles.length,
|
|
155
|
+
...(rotationStartIndex !== undefined ? { rotationStartIndex } : {}),
|
|
156
|
+
...(droppedActivationIds.length > 0 ? { droppedActivationIds } : {}),
|
|
157
|
+
...(oversizedActivationIds.length > 0 ? { oversizedActivationIds } : {}),
|
|
119
158
|
};
|
|
120
159
|
}
|
package/dist/index.js
CHANGED
|
@@ -177,6 +177,16 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
177
177
|
}),
|
|
178
178
|
beforeToolCall: options.beforeToolCall ?? productionGate,
|
|
179
179
|
async beforePromptBuild(event) {
|
|
180
|
+
// PRI-904 Phase-1 (Option 3): this production path is ALSO the Codex
|
|
181
|
+
// route, and Codex never writes trajectory.db::user_turns — its
|
|
182
|
+
// ingestion writes governance_* rows instead. Reading a session turn
|
|
183
|
+
// ordinal here would therefore be a false round authority: on Codex it
|
|
184
|
+
// either throws (→ legacy) or returns a constant 1 (→ fixed start, a
|
|
185
|
+
// different positional starvation while the event still claimed a fair
|
|
186
|
+
// rotation). So the shared production path passes NO round key and
|
|
187
|
+
// reports `legacy_fifo_prefix_v1` honestly. The selector's fair-rotation
|
|
188
|
+
// capability is retained and stays reachable for callers that DO hold a
|
|
189
|
+
// legitimate advancing round authority.
|
|
180
190
|
const prompt = await buildActivePrinciplePromptContext({
|
|
181
191
|
workspaceDir: event.context.workspaceDir,
|
|
182
192
|
excludePrincipleIds: options.promptExcludePrincipleIds?.(event),
|
|
@@ -203,6 +213,14 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
203
213
|
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
204
214
|
...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
|
|
205
215
|
...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
|
|
216
|
+
// PRI-904: selection diagnostics (bounded, optional). On this route
|
|
217
|
+
// the policy is legacy_fifo_prefix_v1 and NO rotation provenance is
|
|
218
|
+
// emitted — a fair claim would be untrue here (see above).
|
|
219
|
+
...(prompt.selectionPolicy !== undefined ? { selectionPolicy: prompt.selectionPolicy } : {}),
|
|
220
|
+
...(prompt.eligibleCount !== undefined ? { eligibleCount: prompt.eligibleCount } : {}),
|
|
221
|
+
...(prompt.rotationStartIndex !== undefined ? { rotationStartIndex: prompt.rotationStartIndex } : {}),
|
|
222
|
+
...(prompt.droppedActivationIds !== undefined && prompt.droppedActivationIds.length > 0 ? { droppedActivationIds: prompt.droppedActivationIds } : {}),
|
|
223
|
+
...(prompt.oversizedActivationIds !== undefined && prompt.oversizedActivationIds.length > 0 ? { oversizedActivationIds: prompt.oversizedActivationIds } : {}),
|
|
206
224
|
});
|
|
207
225
|
}
|
|
208
226
|
catch (err) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
1
2
|
export interface PromptInjectionProjection {
|
|
2
3
|
/** Which real injection route the workspace is on (abstraction_layer_v1 flag). */
|
|
3
4
|
route: 'legacy_trim' | 'shared_render';
|
|
@@ -13,6 +14,38 @@ export interface PromptInjectionProjection {
|
|
|
13
14
|
injectedPrincipleIds: string[];
|
|
14
15
|
injectedActivationIds: string[];
|
|
15
16
|
warnings: string[];
|
|
17
|
+
/**
|
|
18
|
+
* PRI-935: the budget-packing policy that produced THIS projection
|
|
19
|
+
* (`legacy_fifo_prefix_v1` | `fair_rotation_v1`). The console must not
|
|
20
|
+
* describe a fair-rotation workspace in FIFO terms, so the policy travels
|
|
21
|
+
* with the projection instead of being re-derived by each consumer.
|
|
22
|
+
*/
|
|
23
|
+
selectionPolicy: PromptSelectionPolicy;
|
|
24
|
+
/**
|
|
25
|
+
* PRI-935: activations that the production route will inject under SOME
|
|
26
|
+
* round key, i.e. activations that are merely rotated out of the current
|
|
27
|
+
* window rather than structurally starved.
|
|
28
|
+
*
|
|
29
|
+
* Reachability is a property of the SELECTION POLICY, not of any single
|
|
30
|
+
* round: a fair-rotation workspace reaches every non-oversized eligible
|
|
31
|
+
* entry within N turns. So this is computed under the production policy
|
|
32
|
+
* (fair rotation whenever the route actually rotates) and is therefore
|
|
33
|
+
* non-empty even for a caller that supplied no round key — which is
|
|
34
|
+
* exactly the console's situation. Empty only when the production route
|
|
35
|
+
* genuinely does not rotate (shared route) or nothing is eligible.
|
|
36
|
+
*/
|
|
37
|
+
eventuallyInjectedActivationIds: string[];
|
|
38
|
+
/** Number of eligible candidates that reached the selector. */
|
|
39
|
+
eligibleCount: number;
|
|
40
|
+
/**
|
|
41
|
+
* PRI-935: whether the PRODUCTION route for this workspace rotates.
|
|
42
|
+
* The forecast's own `selectionPolicy` answers "which policy did THIS
|
|
43
|
+
* projection run", which is legacy for a console that holds no session
|
|
44
|
+
* round key; this answers "will the agent's real injection rotate",
|
|
45
|
+
* which is what determines whether an out-of-window activation is queued
|
|
46
|
+
* or starved. They differ exactly in the case this whole fix is about.
|
|
47
|
+
*/
|
|
48
|
+
productionRotates: boolean;
|
|
16
49
|
}
|
|
17
50
|
/**
|
|
18
51
|
* PR #1844 follow-up: forecast the prompt-injection budget from the SAME
|
|
@@ -31,8 +64,25 @@ export interface PromptInjectionProjection {
|
|
|
31
64
|
* candidates against the legacy evolution ledger before trimming; the
|
|
32
65
|
* console cannot replay that reducer read-only, so this projection may
|
|
33
66
|
* forecast slightly MORE consumption than the real injection (never less).
|
|
67
|
+
*
|
|
68
|
+
* PRI-935 — round-key alignment. The plugin derives its fair-rotation round
|
|
69
|
+
* key from the CURRENT session's turn ordinal
|
|
70
|
+
* (`nextSessionTurnOrdinal`), which the console has no access to. Passing no
|
|
71
|
+
* key therefore does NOT make the forecast "more conservative": it selects a
|
|
72
|
+
* DIFFERENT policy (`legacy_fifo_prefix_v1`) than the one production runs, and
|
|
73
|
+
* legacy FIFO structurally excludes the newest activation from every
|
|
74
|
+
* truncated selection. The console must either supply the round key or
|
|
75
|
+
* report the policy honestly, so this function takes the key as an explicit
|
|
76
|
+
* input and exposes `selectionPolicy` on the result.
|
|
34
77
|
*/
|
|
35
78
|
export declare function buildLivePromptInjectionProjection(input: {
|
|
36
79
|
workspaceDir: string;
|
|
37
80
|
excludePrincipleIds?: ReadonlySet<string>;
|
|
81
|
+
/**
|
|
82
|
+
* PRI-935: the fair-rotation round key the production route is using this
|
|
83
|
+
* turn. Callers that hold one (the plugin) pass it; callers that do not
|
|
84
|
+
* (the console) omit it and MUST read `selectionPolicy` off the result
|
|
85
|
+
* rather than assuming FIFO.
|
|
86
|
+
*/
|
|
87
|
+
roundKey?: number;
|
|
38
88
|
}): Promise<PromptInjectionProjection>;
|
|
@@ -2,6 +2,28 @@ import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, trimToBudge
|
|
|
2
2
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
3
3
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
4
4
|
import { buildActivePrinciplePromptContext, readPromptActivationCandidates } from './active-principle-prompt.js';
|
|
5
|
+
/**
|
|
6
|
+
* PRI-935: which activations a fair-rotation selector injects across the
|
|
7
|
+
* whole ring. The plugin's round key advances by one per recorded user turn
|
|
8
|
+
* within a continuously advancing session, so N consecutive turns cover all
|
|
9
|
+
* N ring positions: an activation absent from the current window but present
|
|
10
|
+
* in ANY round is reachable, not starved.
|
|
11
|
+
*
|
|
12
|
+
* Kept here (not in the console) so the production selector's own arithmetic
|
|
13
|
+
* stays the single authority for what "reachable" means.
|
|
14
|
+
*/
|
|
15
|
+
function collectReachableActivationIds(principles, budget) {
|
|
16
|
+
const reachable = new Set();
|
|
17
|
+
for (let roundKey = 0; roundKey < principles.length; roundKey += 1) {
|
|
18
|
+
const result = trimToBudget(principles, budget, escapeXml, roundKey);
|
|
19
|
+
for (const principleId of result.injectedIds) {
|
|
20
|
+
const match = principles.find((p) => p.principleId === principleId);
|
|
21
|
+
if (match)
|
|
22
|
+
reachable.add(match.activationId);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...reachable];
|
|
26
|
+
}
|
|
5
27
|
/**
|
|
6
28
|
* PR #1844 follow-up: forecast the prompt-injection budget from the SAME
|
|
7
29
|
* projection the agent injection itself uses, routed by the same
|
|
@@ -19,6 +41,16 @@ import { buildActivePrinciplePromptContext, readPromptActivationCandidates } fro
|
|
|
19
41
|
* candidates against the legacy evolution ledger before trimming; the
|
|
20
42
|
* console cannot replay that reducer read-only, so this projection may
|
|
21
43
|
* forecast slightly MORE consumption than the real injection (never less).
|
|
44
|
+
*
|
|
45
|
+
* PRI-935 — round-key alignment. The plugin derives its fair-rotation round
|
|
46
|
+
* key from the CURRENT session's turn ordinal
|
|
47
|
+
* (`nextSessionTurnOrdinal`), which the console has no access to. Passing no
|
|
48
|
+
* key therefore does NOT make the forecast "more conservative": it selects a
|
|
49
|
+
* DIFFERENT policy (`legacy_fifo_prefix_v1`) than the one production runs, and
|
|
50
|
+
* legacy FIFO structurally excludes the newest activation from every
|
|
51
|
+
* truncated selection. The console must either supply the round key or
|
|
52
|
+
* report the policy honestly, so this function takes the key as an explicit
|
|
53
|
+
* input and exposes `selectionPolicy` on the result.
|
|
22
54
|
*/
|
|
23
55
|
export async function buildLivePromptInjectionProjection(input) {
|
|
24
56
|
const sharedRoute = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
|
|
@@ -32,6 +64,13 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
32
64
|
injectedPrincipleIds: context.principleIds,
|
|
33
65
|
injectedActivationIds: context.activationIds,
|
|
34
66
|
warnings: context.warnings,
|
|
67
|
+
// The shared production path deliberately passes no round key
|
|
68
|
+
// (host-runtime/src/index.ts), so this route genuinely does not rotate
|
|
69
|
+
// and claims nothing about eventual reachability.
|
|
70
|
+
selectionPolicy: context.selectionPolicy ?? 'legacy_fifo_prefix_v1',
|
|
71
|
+
eventuallyInjectedActivationIds: [],
|
|
72
|
+
eligibleCount: context.eligibleCount ?? 0,
|
|
73
|
+
productionRotates: false,
|
|
35
74
|
};
|
|
36
75
|
}
|
|
37
76
|
const candidates = await readPromptActivationCandidates(input);
|
|
@@ -45,10 +84,23 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
45
84
|
injectedPrincipleIds: [],
|
|
46
85
|
injectedActivationIds: [],
|
|
47
86
|
warnings,
|
|
87
|
+
selectionPolicy: 'legacy_fifo_prefix_v1',
|
|
88
|
+
eventuallyInjectedActivationIds: [],
|
|
89
|
+
eligibleCount: 0,
|
|
90
|
+
productionRotates: false,
|
|
48
91
|
};
|
|
49
92
|
}
|
|
50
|
-
|
|
93
|
+
// PRI-935: pass the caller's round key so the forecast runs the SAME
|
|
94
|
+
// packing policy production runs this turn.
|
|
95
|
+
const trimmed = trimToBudget(principles, RUNTIME_V2_PRINCIPLE_BUDGET, escapeXml, input.roundKey);
|
|
51
96
|
const injected = principles.filter((p) => trimmed.injectedIds.has(p.principleId));
|
|
97
|
+
// PRI-935: the legacy_trim route IS the OpenClaw plugin-local route, which
|
|
98
|
+
// rotates on every recorded user turn (PRI-904). Reachability is therefore
|
|
99
|
+
// computed under fair rotation regardless of whether THIS forecast holds a
|
|
100
|
+
// round key — a console without a session key must still be able to tell
|
|
101
|
+
// "queued behind rotation" apart from "structurally starved", otherwise it
|
|
102
|
+
// keeps telling the Owner to deactivate healthy principles.
|
|
103
|
+
const eventuallyInjectedActivationIds = collectReachableActivationIds(principles, RUNTIME_V2_PRINCIPLE_BUDGET);
|
|
52
104
|
return {
|
|
53
105
|
route: 'legacy_trim',
|
|
54
106
|
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
@@ -57,5 +109,9 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
57
109
|
injectedPrincipleIds: [...trimmed.injectedIds],
|
|
58
110
|
injectedActivationIds: injected.map((p) => p.activationId),
|
|
59
111
|
warnings,
|
|
112
|
+
selectionPolicy: trimmed.selectionPolicy,
|
|
113
|
+
eventuallyInjectedActivationIds,
|
|
114
|
+
eligibleCount: trimmed.eligibleCount,
|
|
115
|
+
productionRotates: true,
|
|
60
116
|
};
|
|
61
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"lint": "eslint \"src/**/*.ts\""
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@principles/core": "^1.287.
|
|
24
|
+
"@principles/core": "^1.287.7",
|
|
25
25
|
"@principles/install-layout": "^0.2.7",
|
|
26
26
|
"better-sqlite3": "^13.0.3",
|
|
27
27
|
"js-yaml": "^5.4.1"
|