@principles/host-runtime 0.10.4 → 0.12.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/dist/active-principle-prompt.d.ts +23 -1
- package/dist/active-principle-prompt.js +21 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +37 -2
- package/dist/prompt-activation-deliverability.d.ts +36 -3
- package/dist/prompt-activation-deliverability.js +44 -26
- package/dist/prompt-injection-budget-observation.d.ts +176 -0
- package/dist/prompt-injection-budget-observation.js +449 -0
- package/dist/prompt-injection-projection.d.ts +39 -1
- package/dist/prompt-injection-projection.js +43 -9
- package/dist/prompt-injection-status.d.ts +47 -5
- package/dist/prompt-injection-status.js +45 -8
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActivatedPrinciple, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
1
|
+
import { type ActivatedPrinciple, type PromptBudgetResolution, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
2
2
|
/** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
|
|
3
3
|
export declare const MAX_SHARED_DIAGNOSTIC_IDS = 16;
|
|
4
4
|
export interface ActivePrinciplePromptResult {
|
|
@@ -23,6 +23,20 @@ export interface ActivePrinciplePromptResult {
|
|
|
23
23
|
droppedActivationIds?: string[];
|
|
24
24
|
/** PRI-904 (bounded, max 16): activation ids that cannot fit even in an empty payload. */
|
|
25
25
|
oversizedActivationIds?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: provenance of the `budget` this
|
|
28
|
+
* selection ran under (capacity source/status + the resolved context
|
|
29
|
+
* window). Present on every result; `legacy_fallback` + `unknown` means
|
|
30
|
+
* no model capacity could be confirmed and the fixed budget was used.
|
|
31
|
+
*/
|
|
32
|
+
budgetCapacitySource?: PromptBudgetResolution['capacitySource'];
|
|
33
|
+
budgetCapacityStatus?: PromptBudgetResolution['capacityStatus'];
|
|
34
|
+
budgetContextWindowTokens?: number | null;
|
|
35
|
+
budgetPolicyVersion?: PromptBudgetResolution['policyVersion'];
|
|
36
|
+
/** SPEC §7 v0.2: bounded fallback reason code; present only on legacy_fallback. */
|
|
37
|
+
budgetFallbackReason?: PromptBudgetResolution['fallbackReason'];
|
|
38
|
+
/** Non-sensitive log context (provider/model ids); never emitted to events. */
|
|
39
|
+
budgetFallbackDetail?: string;
|
|
26
40
|
}
|
|
27
41
|
export interface PromptActivationCandidates {
|
|
28
42
|
principles: ActivatedPrinciple[];
|
|
@@ -52,4 +66,12 @@ export declare function buildActivePrinciplePromptContext(input: {
|
|
|
52
66
|
* characterization baseline).
|
|
53
67
|
*/
|
|
54
68
|
roundKey?: number;
|
|
69
|
+
/**
|
|
70
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1: the budget resolution for
|
|
71
|
+
* THIS request, resolved once by the entry point that holds the model
|
|
72
|
+
* facts. Absent → the resolver's no-facts default (legacy fixed 2000,
|
|
73
|
+
* capacityStatus unknown) — the historical behavior, used by callers with
|
|
74
|
+
* no model context (console projections, WorkBuddy without model facts).
|
|
75
|
+
*/
|
|
76
|
+
promptBudget?: PromptBudgetResolution;
|
|
55
77
|
}): Promise<ActivePrinciplePromptResult>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SqliteActivationStateStore, SqliteConnection, SqlitePIArtifactStore, computeFeatureFlagsFromConfig, filterPromptActivations, renderPrinciplesToDirectives, resolvePrincipleFromArtifact, resolvePromptBudget, } from '@principles/core/runtime-v2';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
@@ -81,13 +81,23 @@ export async function readPromptActivationCandidates(input) {
|
|
|
81
81
|
return { principles, excludedPrincipleIds, warnings, aborted: false, selfReportEnabled, abstractionLayerEnabled };
|
|
82
82
|
}
|
|
83
83
|
export async function buildActivePrinciplePromptContext(input) {
|
|
84
|
+
// One resolution per request (SPEC §6.1): the caller's pre-resolved budget
|
|
85
|
+
// or the fixed-fallback default — never a per-line constant read.
|
|
86
|
+
const promptBudget = input.promptBudget ?? resolvePromptBudget();
|
|
87
|
+
const { budgetChars } = promptBudget;
|
|
84
88
|
const { principles, excludedPrincipleIds, warnings, aborted, selfReportEnabled } = await readPromptActivationCandidates(input);
|
|
85
89
|
if (aborted) {
|
|
86
90
|
return {
|
|
87
91
|
additionalContext: '', principleIds: [], activationIds: [], artifactIds: [], warnings,
|
|
88
|
-
budget:
|
|
92
|
+
budget: budgetChars, truncated: false, excludedPrincipleIds,
|
|
89
93
|
excludedCount: excludedPrincipleIds.length, allValidatedPrinciplesExcluded: false,
|
|
90
94
|
selectionPolicy: 'legacy_fifo_prefix_v1', eligibleCount: 0,
|
|
95
|
+
budgetCapacitySource: promptBudget.capacitySource,
|
|
96
|
+
budgetCapacityStatus: promptBudget.capacityStatus,
|
|
97
|
+
budgetContextWindowTokens: promptBudget.contextWindowTokens,
|
|
98
|
+
budgetPolicyVersion: promptBudget.policyVersion,
|
|
99
|
+
...(promptBudget.fallbackReason !== undefined ? { budgetFallbackReason: promptBudget.fallbackReason } : {}),
|
|
100
|
+
...(promptBudget.fallbackDetail !== undefined ? { budgetFallbackDetail: promptBudget.fallbackDetail } : {}),
|
|
91
101
|
};
|
|
92
102
|
}
|
|
93
103
|
const included = [];
|
|
@@ -109,14 +119,14 @@ export async function buildActivePrinciplePromptContext(input) {
|
|
|
109
119
|
const droppedActivationIds = [];
|
|
110
120
|
const oversizedActivationIds = [];
|
|
111
121
|
const standaloneFits = (principle) => renderPrinciplesToDirectives([principle], new Set([principle.principleId]), { escapeFn: escapeXml, selfReportInstruction: selfReportEnabled }).length
|
|
112
|
-
<=
|
|
122
|
+
<= budgetChars;
|
|
113
123
|
const scanOrder = rotationStartIndex === undefined
|
|
114
124
|
? principles
|
|
115
125
|
: principles.slice(rotationStartIndex).concat(principles.slice(0, rotationStartIndex));
|
|
116
126
|
for (const principle of scanOrder) {
|
|
117
127
|
const candidate = [...included, principle];
|
|
118
128
|
const candidateContext = renderPrinciplesToDirectives(candidate, new Set(candidate.map((entry) => entry.principleId)), { escapeFn: escapeXml, selfReportInstruction: selfReportEnabled });
|
|
119
|
-
if (candidateContext.length >
|
|
129
|
+
if (candidateContext.length > budgetChars) {
|
|
120
130
|
if (standaloneFits(principle)) {
|
|
121
131
|
// Fits alone but not now — normal budget truncation.
|
|
122
132
|
truncated = true;
|
|
@@ -144,7 +154,7 @@ export async function buildActivePrinciplePromptContext(input) {
|
|
|
144
154
|
activationIds: included.map((principle) => principle.activationId),
|
|
145
155
|
artifactIds: included.map((principle) => principle.artifactId),
|
|
146
156
|
warnings,
|
|
147
|
-
budget:
|
|
157
|
+
budget: budgetChars,
|
|
148
158
|
truncated,
|
|
149
159
|
excludedPrincipleIds,
|
|
150
160
|
excludedCount: excludedPrincipleIds.length,
|
|
@@ -155,5 +165,11 @@ export async function buildActivePrinciplePromptContext(input) {
|
|
|
155
165
|
...(rotationStartIndex !== undefined ? { rotationStartIndex } : {}),
|
|
156
166
|
...(droppedActivationIds.length > 0 ? { droppedActivationIds } : {}),
|
|
157
167
|
...(oversizedActivationIds.length > 0 ? { oversizedActivationIds } : {}),
|
|
168
|
+
budgetCapacitySource: promptBudget.capacitySource,
|
|
169
|
+
budgetCapacityStatus: promptBudget.capacityStatus,
|
|
170
|
+
budgetContextWindowTokens: promptBudget.contextWindowTokens,
|
|
171
|
+
budgetPolicyVersion: promptBudget.policyVersion,
|
|
172
|
+
...(promptBudget.fallbackReason !== undefined ? { budgetFallbackReason: promptBudget.fallbackReason } : {}),
|
|
173
|
+
...(promptBudget.fallbackDetail !== undefined ? { budgetFallbackDetail: promptBudget.fallbackDetail } : {}),
|
|
158
174
|
};
|
|
159
175
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './prompt-injection-projection.js';
|
|
|
9
9
|
export * from './prompt-activation-deliverability.js';
|
|
10
10
|
export * from './prompt-injection-status.js';
|
|
11
11
|
export * from './prompt-injection-event-evidence.js';
|
|
12
|
+
export * from './prompt-injection-budget-observation.js';
|
|
12
13
|
export * from './pd-config.js';
|
|
13
14
|
export * from './production-rulehost-gate.js';
|
|
14
15
|
export * from './rule-implementation-runtime.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isHostEvent, isHostEventResult, } from '@principles/core/host';
|
|
3
|
-
import {
|
|
3
|
+
import { getPromptModelCatalog, resolvePromptBudget } from '@principles/core/runtime-v2';
|
|
4
4
|
import { buildActivePrinciplePromptContext } from './active-principle-prompt.js';
|
|
5
5
|
import { createProductionRuleHostGate } from './production-rulehost-gate.js';
|
|
6
6
|
import { createProductionPainEvidenceHandler } from './production-pain-evidence.js';
|
|
@@ -15,6 +15,10 @@ export * from './prompt-injection-status.js';
|
|
|
15
15
|
// SPEC PD_PROMPT_CAPACITY_V1 Phase C (AC-12): honest read-model over the
|
|
16
16
|
// existing injection-event JSONL — unknown vs proven-zero never conflated.
|
|
17
17
|
export * from './prompt-injection-event-evidence.js';
|
|
18
|
+
// PRI-1017: runtime budget OBSERVATION read model over the SAME injection
|
|
19
|
+
// events — console surfaces forecast under the budget the last real
|
|
20
|
+
// injection used (SPEC §6.1), labelled with source + observation time.
|
|
21
|
+
export * from './prompt-injection-budget-observation.js';
|
|
18
22
|
export * from './pd-config.js';
|
|
19
23
|
export * from './production-rulehost-gate.js';
|
|
20
24
|
export * from './rule-implementation-runtime.js';
|
|
@@ -211,6 +215,26 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
211
215
|
const prompt = await buildActivePrinciplePromptContext({
|
|
212
216
|
workspaceDir: event.context.workspaceDir,
|
|
213
217
|
excludePrincipleIds: options.promptExcludePrincipleIds?.(event),
|
|
218
|
+
promptBudget: resolvePromptBudget({
|
|
219
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: resolve ONCE per request
|
|
220
|
+
// from the host's own hook facts (OpenClaw effective window /
|
|
221
|
+
// Codex+WorkBuddy model id) against the installed local Pi AI
|
|
222
|
+
// catalog — no network, no LLM call. Missing/untrusted facts
|
|
223
|
+
// resolve to the legacy fixed budget with an observable reason.
|
|
224
|
+
hostCapacity: event.context.modelContext === undefined
|
|
225
|
+
? undefined
|
|
226
|
+
: {
|
|
227
|
+
contextTokenBudget: event.context.modelContext.contextTokenBudget,
|
|
228
|
+
contextWindowSource: event.context.modelContext.contextWindowSource,
|
|
229
|
+
},
|
|
230
|
+
modelIdentity: event.context.modelContext === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: {
|
|
233
|
+
providerId: event.context.modelContext.modelProviderId,
|
|
234
|
+
modelId: event.context.modelContext.modelId,
|
|
235
|
+
},
|
|
236
|
+
catalog: getPromptModelCatalog(),
|
|
237
|
+
}),
|
|
214
238
|
});
|
|
215
239
|
// PRI-750: record the injection event on the shared path with the host
|
|
216
240
|
// turn id (Codex turn_id → runId) so receipt events carry a turn-level
|
|
@@ -231,11 +255,22 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
231
255
|
injectedCount: prompt.principleIds.length,
|
|
232
256
|
skippedWarnings: prompt.warnings,
|
|
233
257
|
injectedCharCount: prompt.additionalContext.length,
|
|
234
|
-
budget:
|
|
258
|
+
budget: prompt.budget,
|
|
235
259
|
...(options.hostKind === 'openclaw' || options.hostKind === 'codex' || options.hostKind === 'workbuddy' ? { hostKind: options.hostKind } : {}),
|
|
236
260
|
eventId: randomUUID(),
|
|
237
261
|
...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
|
|
238
262
|
...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
|
|
263
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: budget provenance — the
|
|
264
|
+
// recorded budget is the one THIS injection actually ran under
|
|
265
|
+
// (prompt.budget), never a constant restated after the fact.
|
|
266
|
+
...(prompt.budgetPolicyVersion !== undefined ? { budgetPolicyVersion: prompt.budgetPolicyVersion } : {}),
|
|
267
|
+
...(prompt.budgetCapacitySource !== undefined ? { budgetCapacitySource: prompt.budgetCapacitySource } : {}),
|
|
268
|
+
...(prompt.budgetCapacityStatus !== undefined ? { budgetCapacityStatus: prompt.budgetCapacityStatus } : {}),
|
|
269
|
+
...(prompt.budgetContextWindowTokens != null ? { budgetContextWindowTokens: prompt.budgetContextWindowTokens } : {}),
|
|
270
|
+
// SPEC §7 v0.2: the bounded fallback reason code rides the event so
|
|
271
|
+
// operators can tell model-identity absence from catalog ambiguity
|
|
272
|
+
// etc. without touching logs. Detail stays out of events.
|
|
273
|
+
...(prompt.budgetFallbackReason !== undefined ? { budgetFallbackReason: prompt.budgetFallbackReason } : {}),
|
|
239
274
|
// PRI-904: selection diagnostics (bounded, optional). On this route
|
|
240
275
|
// the policy is legacy_fifo_prefix_v1 and NO rotation provenance is
|
|
241
276
|
// emitted — a fair claim would be untrue here (see above).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActivatedPrinciple } from '@principles/core/runtime-v2';
|
|
1
|
+
import { type ActivatedPrinciple, type PromptBudgetTierFit } from '@principles/core/runtime-v2';
|
|
2
2
|
import { PROMPT_INJECTION_UNIT, type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
3
3
|
/**
|
|
4
4
|
* SPEC PD_PROMPT_CAPACITY_V1 R-B2: ONE route-aware single-artifact capacity
|
|
@@ -25,18 +25,46 @@ export type PromptArtifactDeliverability = {
|
|
|
25
25
|
route: PromptInjectionRouteName;
|
|
26
26
|
hostKind?: PromptInjectionTargetHost;
|
|
27
27
|
basis?: PromptInjectionRouteDecision['basis'];
|
|
28
|
+
/**
|
|
29
|
+
* The budget this judgment is binding against: the MINIMUM supported
|
|
30
|
+
* tier — a deliverable here fits under EVERY supported budget
|
|
31
|
+
* (SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.3), so the strictest tier is
|
|
32
|
+
* the honest evidence.
|
|
33
|
+
*/
|
|
28
34
|
budget: number;
|
|
35
|
+
budgetMinChars: number;
|
|
36
|
+
budgetMaxChars: number;
|
|
29
37
|
/** Single-item cost in UTF-16 code units under the bound route's serializer. */
|
|
30
38
|
costChars: number;
|
|
31
39
|
budgetScope: PromptInjectionBudgetScope;
|
|
32
40
|
unit: typeof PROMPT_INJECTION_UNIT;
|
|
41
|
+
} | {
|
|
42
|
+
/**
|
|
43
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.3: the artifact fits under
|
|
44
|
+
* SOME supported budgets but not all — whether it injects depends on the
|
|
45
|
+
* runtime model's context window. NOT a promise of injection and NOT a
|
|
46
|
+
* permanent refusal; approval flow proceeds unchanged.
|
|
47
|
+
*/
|
|
48
|
+
status: 'model_dependent';
|
|
49
|
+
route: PromptInjectionRouteName;
|
|
50
|
+
hostKind?: PromptInjectionTargetHost;
|
|
51
|
+
basis?: PromptInjectionRouteDecision['basis'];
|
|
52
|
+
budgetMinChars: number;
|
|
53
|
+
budgetMaxChars: number;
|
|
54
|
+
costChars: number;
|
|
55
|
+
budgetScope: PromptInjectionBudgetScope;
|
|
56
|
+
unit: typeof PROMPT_INJECTION_UNIT;
|
|
57
|
+
nextAction: string;
|
|
33
58
|
} | {
|
|
34
59
|
status: 'undeliverable';
|
|
35
60
|
reason: 'single_item_exceeds_budget';
|
|
36
61
|
route: PromptInjectionRouteName;
|
|
37
62
|
hostKind?: PromptInjectionTargetHost;
|
|
38
63
|
basis?: PromptInjectionRouteDecision['basis'];
|
|
64
|
+
/** The budget this judgment is binding against: the MAXIMUM supported tier. */
|
|
39
65
|
budget: number;
|
|
66
|
+
budgetMinChars: number;
|
|
67
|
+
budgetMaxChars: number;
|
|
40
68
|
costChars: number;
|
|
41
69
|
overByChars: number;
|
|
42
70
|
budgetScope: PromptInjectionBudgetScope;
|
|
@@ -47,10 +75,10 @@ export type PromptArtifactDeliverability = {
|
|
|
47
75
|
/** Stable machine-readable reason (route facts or artifact content unresolvable). */
|
|
48
76
|
reason: string;
|
|
49
77
|
nextAction: string;
|
|
50
|
-
/** Route-unconfirmed case: per-host single-item fit so the UI can show both. */
|
|
78
|
+
/** Route-unconfirmed case: per-host single-item tier fit so the UI can show both. */
|
|
51
79
|
perHost?: readonly {
|
|
52
80
|
hostKind: PromptInjectionTargetHost;
|
|
53
|
-
|
|
81
|
+
tierFit: PromptBudgetTierFit;
|
|
54
82
|
costChars: number;
|
|
55
83
|
}[];
|
|
56
84
|
};
|
|
@@ -72,11 +100,16 @@ export declare function resolvePromptCandidatePrinciple(artifact: PromptDelivera
|
|
|
72
100
|
* lines). For an entry that does NOT fit, trimToBudget returns only the
|
|
73
101
|
* header line, so the would-be cost is reconstructed from the header line it
|
|
74
102
|
* DID return plus the real entry line (same format authority, listEntryLine).
|
|
103
|
+
*
|
|
104
|
+
* The budget parameter defaults to the legacy fixed budget: approval-time
|
|
105
|
+
* callers hold no runtime model facts, and SPEC §6.3 requires the fixed
|
|
106
|
+
* fallback — never an invented capacity — when none can be confirmed.
|
|
75
107
|
*/
|
|
76
108
|
export declare function computePromptSingleItemFit(input: {
|
|
77
109
|
principle: ActivatedPrinciple;
|
|
78
110
|
route: PromptInjectionRouteName;
|
|
79
111
|
selfReportEnabled: boolean;
|
|
112
|
+
budget?: number;
|
|
80
113
|
}): {
|
|
81
114
|
fits: boolean;
|
|
82
115
|
costChars: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { PROMPT_BUDGET_FALLBACK_CHARS, PROMPT_BUDGET_MAX_CHARS, PROMPT_BUDGET_MIN_CHARS, SqliteConnection, SqlitePIArtifactStore, classifySingleItemBudgetTier, computeFeatureFlagsFromConfig, listEntryLine, renderPrinciplesToDirectives, resolvePrincipleFromArtifact, trimToBudget, } from '@principles/core/runtime-v2';
|
|
4
4
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
5
5
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
6
6
|
import { PROMPT_INJECTION_TARGET_HOSTS, PROMPT_INJECTION_UNIT, promptRouteForHost, resolvePromptInjectionRouteDecision, } from './prompt-injection-projection.js';
|
|
@@ -38,13 +38,18 @@ function selfReportEnabledFor(workspaceDir) {
|
|
|
38
38
|
* lines). For an entry that does NOT fit, trimToBudget returns only the
|
|
39
39
|
* header line, so the would-be cost is reconstructed from the header line it
|
|
40
40
|
* DID return plus the real entry line (same format authority, listEntryLine).
|
|
41
|
+
*
|
|
42
|
+
* The budget parameter defaults to the legacy fixed budget: approval-time
|
|
43
|
+
* callers hold no runtime model facts, and SPEC §6.3 requires the fixed
|
|
44
|
+
* fallback — never an invented capacity — when none can be confirmed.
|
|
41
45
|
*/
|
|
42
46
|
export function computePromptSingleItemFit(input) {
|
|
47
|
+
const budget = input.budget ?? PROMPT_BUDGET_FALLBACK_CHARS;
|
|
43
48
|
if (input.route === 'shared_render') {
|
|
44
49
|
const render = renderPrinciplesToDirectives([input.principle], new Set([input.principle.principleId]), { escapeFn: escapeXml, selfReportInstruction: input.selfReportEnabled });
|
|
45
|
-
return { fits: render.length <=
|
|
50
|
+
return { fits: render.length <= budget, costChars: render.length };
|
|
46
51
|
}
|
|
47
|
-
const trimmed = trimToBudget([input.principle],
|
|
52
|
+
const trimmed = trimToBudget([input.principle], budget, escapeXml);
|
|
48
53
|
const fits = trimmed.injectedIds.has(input.principle.principleId);
|
|
49
54
|
const costChars = trimmed.lines.length > 1
|
|
50
55
|
? trimmed.lines.join('\n').length
|
|
@@ -52,6 +57,7 @@ export function computePromptSingleItemFit(input) {
|
|
|
52
57
|
return { fits, costChars };
|
|
53
58
|
}
|
|
54
59
|
const UNDELIVERABLE_NEXT_ACTION = 'shorten the principle statement and resubmit, or use the activations page "modify to injectable version" entry to create a replacement artifact (the approval facts are preserved)';
|
|
60
|
+
const MODEL_DEPENDENT_NEXT_ACTION = 'this principle fits the injection budget only on models with a larger context window; it may be excluded at runtime on smaller-context models — the approval itself is unaffected';
|
|
55
61
|
/**
|
|
56
62
|
* Precheck one artifact against the workspace's effective injection route.
|
|
57
63
|
* Pure read — no state mutation, safe to call before any governance write.
|
|
@@ -78,7 +84,7 @@ export async function checkPromptArtifactDeliverability(input) {
|
|
|
78
84
|
targetHost: input.targetHost,
|
|
79
85
|
});
|
|
80
86
|
if (decision.status === 'confirmed' && decision.route !== undefined) {
|
|
81
|
-
const {
|
|
87
|
+
const { costChars } = computePromptSingleItemFit({
|
|
82
88
|
principle: resolved.principle,
|
|
83
89
|
route: decision.route,
|
|
84
90
|
selfReportEnabled,
|
|
@@ -86,44 +92,56 @@ export async function checkPromptArtifactDeliverability(input) {
|
|
|
86
92
|
const budgetScope = decision.route === 'shared_render'
|
|
87
93
|
? 'full_directive_context'
|
|
88
94
|
: 'selected_lines';
|
|
89
|
-
|
|
95
|
+
const shared = {
|
|
96
|
+
route: decision.route,
|
|
97
|
+
...(decision.hostKind !== undefined ? { hostKind: decision.hostKind } : {}),
|
|
98
|
+
...(decision.basis !== undefined ? { basis: decision.basis } : {}),
|
|
99
|
+
budgetMinChars: PROMPT_BUDGET_MIN_CHARS,
|
|
100
|
+
budgetMaxChars: PROMPT_BUDGET_MAX_CHARS,
|
|
101
|
+
costChars,
|
|
102
|
+
budgetScope,
|
|
103
|
+
unit: PROMPT_INJECTION_UNIT,
|
|
104
|
+
};
|
|
105
|
+
// SPEC §6.3: approval time holds no runtime model identity — classify
|
|
106
|
+
// against the SUPPORTED budget range instead of pretending to know the
|
|
107
|
+
// executing model's window.
|
|
108
|
+
const tierFit = classifySingleItemBudgetTier(costChars);
|
|
109
|
+
if (tierFit === 'fits_all_tiers') {
|
|
90
110
|
return {
|
|
91
111
|
status: 'deliverable',
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
...shared,
|
|
113
|
+
budget: PROMPT_BUDGET_MIN_CHARS,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (tierFit === 'model_dependent') {
|
|
117
|
+
return {
|
|
118
|
+
status: 'model_dependent',
|
|
119
|
+
...shared,
|
|
120
|
+
nextAction: MODEL_DEPENDENT_NEXT_ACTION,
|
|
99
121
|
};
|
|
100
122
|
}
|
|
101
123
|
return {
|
|
102
124
|
status: 'undeliverable',
|
|
125
|
+
...shared,
|
|
103
126
|
reason: 'single_item_exceeds_budget',
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
...(decision.basis !== undefined ? { basis: decision.basis } : {}),
|
|
107
|
-
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
108
|
-
costChars,
|
|
109
|
-
overByChars: costChars - RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
110
|
-
budgetScope,
|
|
111
|
-
unit: PROMPT_INJECTION_UNIT,
|
|
127
|
+
budget: PROMPT_BUDGET_MAX_CHARS,
|
|
128
|
+
overByChars: costChars - PROMPT_BUDGET_MAX_CHARS,
|
|
112
129
|
nextAction: UNDELIVERABLE_NEXT_ACTION,
|
|
113
130
|
};
|
|
114
131
|
}
|
|
115
|
-
// Route unconfirmed: report per-host fit for every standard host
|
|
116
|
-
// guessing (R-A1) — the write gate refuses, the UI can show
|
|
117
|
-
// Roster derives from the single SSoT list (review P3-1: a
|
|
118
|
-
// two-host array dropped WorkBuddy from unconfirmed
|
|
132
|
+
// Route unconfirmed: report per-host tier fit for every standard host
|
|
133
|
+
// instead of guessing (R-A1) — the write gate refuses, the UI can show
|
|
134
|
+
// both facts. Roster derives from the single SSoT list (review P3-1: a
|
|
135
|
+
// hand-typed two-host array dropped WorkBuddy from unconfirmed
|
|
136
|
+
// deliverability reports).
|
|
119
137
|
const perHost = PROMPT_INJECTION_TARGET_HOSTS.map((hostKind) => {
|
|
120
138
|
const route = promptRouteForHost(hostKind, decision.flagEnabled);
|
|
121
|
-
const {
|
|
139
|
+
const { costChars } = computePromptSingleItemFit({
|
|
122
140
|
principle: resolved.principle,
|
|
123
141
|
route,
|
|
124
142
|
selfReportEnabled,
|
|
125
143
|
});
|
|
126
|
-
return { hostKind,
|
|
144
|
+
return { hostKind, tierFit: classifySingleItemBudgetTier(costChars), costChars };
|
|
127
145
|
});
|
|
128
146
|
return {
|
|
129
147
|
status: 'unconfirmed',
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { PROMPT_BUDGET_POLICY_VERSION, type PromptBudgetCapacitySource, type PromptBudgetCapacityStatus, type PromptBudgetFallbackReason, type PromptBudgetResolution } from '@principles/core/runtime-v2';
|
|
2
|
+
import { type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
3
|
+
/**
|
|
4
|
+
* PRI-1017: runtime budget OBSERVATION read model over the EXISTING
|
|
5
|
+
* injection-event JSONL (`<workspaceDir>/{.pd,.state}/logs/events_<date>.jsonl`,
|
|
6
|
+
* written by the OpenClaw EventLog and the Codex/WorkBuddy shared path via
|
|
7
|
+
* appendEventLogLine).
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 forbids the
|
|
10
|
+
* actual injection from running a dynamic budget while the Console forecasts
|
|
11
|
+
* under a different fixed 2000-char budget. The console holds no live host
|
|
12
|
+
* facts, but the RUNTIME persists the resolution it actually used —
|
|
13
|
+
* `runtime_v2_prompt_activations_injected` carries budgetChars, capacity
|
|
14
|
+
* provenance and hostKind. This module reads those facts back so console
|
|
15
|
+
* surfaces forecast under the same budget the last real injection used.
|
|
16
|
+
*
|
|
17
|
+
* Honesty rules this model enforces (SPEC §6.1/§7):
|
|
18
|
+
* - An observation is a FACT ABOUT THE PAST, never a live claim: callers must
|
|
19
|
+
* surface `observedAt`; the console labels it "most recent runtime
|
|
20
|
+
* observation", not a current confirmation.
|
|
21
|
+
* - No logs / no budget facts ⇒ absent, never a fabricated window; the caller
|
|
22
|
+
* keeps the honest fixed-fallback policy.
|
|
23
|
+
* - A latest observation older than RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS is
|
|
24
|
+
* `stale` — the caller must NOT present it as the current budget.
|
|
25
|
+
* - Observations bind to ONE host: OpenClaw's window is never used for a
|
|
26
|
+
* Codex or WorkBuddy forecast.
|
|
27
|
+
* - Untrusted input discipline: every line is parsed as `unknown` and
|
|
28
|
+
* narrowed with typeof guards + closed-set checks; structurally invalid
|
|
29
|
+
* lines are counted in `parseFailures` and skipped loudly, never guessed.
|
|
30
|
+
* - Events predating the model-aware policy carry no provenance fields; they
|
|
31
|
+
* are counted as `legacyEventsSkipped` and produce NO observation — a
|
|
32
|
+
* missing provenance field is "policy unknown", never a capacity claim.
|
|
33
|
+
* - Bounded I/O: only the newest bytes of the most recent daily files are read
|
|
34
|
+
* (tail window), the file cap applying PER candidate log directory, so a
|
|
35
|
+
* runaway or crafted log cannot make a Console request read an unbounded
|
|
36
|
+
* amount of data. Only bounded budget metadata is retained and returned;
|
|
37
|
+
* other event payloads are not cached.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Candidate directories for the runtime-v2 event log, in priority order.
|
|
41
|
+
* Shared with the RuleCode telemetry collector (ERR-031: both readers derive
|
|
42
|
+
* from one list — the v1 EventLog writer emits `.state/logs`, no production
|
|
43
|
+
* code ever created `.pd/logs`).
|
|
44
|
+
*/
|
|
45
|
+
export declare const RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS: readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* Bounded I/O: only the most recent daily files are scanned — PER candidate
|
|
48
|
+
* log directory (`RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`), not globally. The
|
|
49
|
+
* cap is deliberately per directory: both directories describe the same
|
|
50
|
+
* workspace's log convention and either can hold the newest event for a given
|
|
51
|
+
* host (the reader takes latest-wins ACROSS them), so a global cap would let
|
|
52
|
+
* one directory's files starve the other and drop a live host observation.
|
|
53
|
+
* Worst case per read is therefore `RUNTIME_BUDGET_OBSERVATION_MAX_FILES ×
|
|
54
|
+
* RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS.length` files (14 today) and the same
|
|
55
|
+
* count × `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` bytes (56 MiB today).
|
|
56
|
+
*/
|
|
57
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FILES = 7;
|
|
58
|
+
/**
|
|
59
|
+
* Bounded I/O: per file, only the newest bytes are read. The writer appends,
|
|
60
|
+
* so the latest observation per host lives at the TAIL; a file larger than
|
|
61
|
+
* this window is tail-read and its older prefix skipped (counted in
|
|
62
|
+
* `truncatedFiles`) instead of materialising an unbounded log in the console
|
|
63
|
+
* process. 4 MiB is ~3.4x the largest real day observed on 2026-10-21
|
|
64
|
+
* (WorkBuddy 1.17 MiB, Codex 0.87 MiB, OpenClaw 0.98 MiB across 7 files).
|
|
65
|
+
*/
|
|
66
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES: number;
|
|
67
|
+
/**
|
|
68
|
+
* Maximum age of an observation that may be presented as a CURRENT runtime
|
|
69
|
+
* fact. The runtime refreshes the observation on every prompt build, so an
|
|
70
|
+
* active workspace holds minute-old facts; beyond a day the model may have
|
|
71
|
+
* been switched and the window is no longer evidence about the present.
|
|
72
|
+
*/
|
|
73
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS: number;
|
|
74
|
+
/**
|
|
75
|
+
* Clock-skew allowance for event timestamps. A writer clock ahead of ours is
|
|
76
|
+
* normal; an observation further in the future is not a fact about the
|
|
77
|
+
* present and must never be selected as the current budget.
|
|
78
|
+
*/
|
|
79
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS: number;
|
|
80
|
+
/** Console-side bounded reason: a runtime observation exists but is too old. */
|
|
81
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_STALE_REASON: 'runtime_budget_observation_stale';
|
|
82
|
+
export interface RuntimePromptBudgetObservation {
|
|
83
|
+
/** The workspace the observed event was bound to (event `data.workspaceDir`). */
|
|
84
|
+
workspaceDir: string;
|
|
85
|
+
/** Host that produced the observation; observations never cross hosts. */
|
|
86
|
+
hostKind: PromptInjectionTargetHost;
|
|
87
|
+
/** Budget the real injection ran under (UTF-16 code units). */
|
|
88
|
+
budgetChars: number;
|
|
89
|
+
capacitySource: PromptBudgetCapacitySource;
|
|
90
|
+
capacityStatus: PromptBudgetCapacityStatus;
|
|
91
|
+
/** The context window the budget derived from; null when unknown. */
|
|
92
|
+
contextWindowTokens: number | null;
|
|
93
|
+
/** Bounded fallback reason carried by the event; null when confirmed. */
|
|
94
|
+
fallbackReason: PromptBudgetFallbackReason | null;
|
|
95
|
+
/** Policy version the observed budget was computed under (closed literal). */
|
|
96
|
+
policyVersion: typeof PROMPT_BUDGET_POLICY_VERSION | null;
|
|
97
|
+
/** ISO timestamp of the observed event (`ts` envelope field). */
|
|
98
|
+
observedAt: string;
|
|
99
|
+
}
|
|
100
|
+
export interface RuntimeBudgetObservationReport {
|
|
101
|
+
/** Latest observation per host (most recent by event `ts`). */
|
|
102
|
+
observations: RuntimePromptBudgetObservation[];
|
|
103
|
+
filesRead: string[];
|
|
104
|
+
lines: number;
|
|
105
|
+
parseFailures: number;
|
|
106
|
+
/** Injection events without model-aware budget provenance (pre-policy events). */
|
|
107
|
+
legacyEventsSkipped: number;
|
|
108
|
+
/** Injection events whose budget policy version differs from the current one. */
|
|
109
|
+
foreignPolicyEventsSkipped: number;
|
|
110
|
+
/**
|
|
111
|
+
* Files larger than the bounded read window whose older prefix was skipped.
|
|
112
|
+
* Non-zero means `lines`/skip counters describe the read window only.
|
|
113
|
+
*/
|
|
114
|
+
truncatedFiles: number;
|
|
115
|
+
note: string;
|
|
116
|
+
}
|
|
117
|
+
export type RuntimeBudgetObservationSelection = {
|
|
118
|
+
kind: 'usable';
|
|
119
|
+
observation: RuntimePromptBudgetObservation;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'stale';
|
|
122
|
+
observation: RuntimePromptBudgetObservation;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'absent';
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Read the most recent budget observation per host from the workspace's
|
|
128
|
+
* runtime-v2 event logs. Never throws: an unreadable log directory degrades
|
|
129
|
+
* to an empty report with an explicit note (absence is UNKNOWN, not zero).
|
|
130
|
+
*/
|
|
131
|
+
export declare function readRuntimePromptBudgetObservations(input: {
|
|
132
|
+
workspaceDir: string;
|
|
133
|
+
/** Reference clock for the future-timestamp guard (tests pass a fixed one). */
|
|
134
|
+
now?: Date;
|
|
135
|
+
}): RuntimeBudgetObservationReport;
|
|
136
|
+
/**
|
|
137
|
+
* Select the observation that may back a console forecast for `targetHost`.
|
|
138
|
+
* `absent` when the host has no observation at all; `stale` when the latest
|
|
139
|
+
* one is older than the freshness bound (the caller must fall back honestly).
|
|
140
|
+
*/
|
|
141
|
+
export declare function selectRuntimeBudgetObservation(input: {
|
|
142
|
+
observations: readonly RuntimePromptBudgetObservation[];
|
|
143
|
+
targetHost?: PromptInjectionTargetHost;
|
|
144
|
+
now?: Date;
|
|
145
|
+
}): RuntimeBudgetObservationSelection;
|
|
146
|
+
/**
|
|
147
|
+
* Console-facing selection: one call gives the confirmed-host budget basis,
|
|
148
|
+
* the honest stale marker, and the per-host bases for unconfirmed routes.
|
|
149
|
+
* Reads the workspace's event logs; never throws.
|
|
150
|
+
*/
|
|
151
|
+
export interface ConsoleRuntimeBudgetFacts {
|
|
152
|
+
/** Usable observation for the requested host (only when targetHost is given). */
|
|
153
|
+
selected?: RuntimePromptBudgetObservation;
|
|
154
|
+
/** Latest observation for the requested host that is too old to be current. */
|
|
155
|
+
stale?: RuntimePromptBudgetObservation;
|
|
156
|
+
/**
|
|
157
|
+
* Usable observations per host — used for unconfirmed per-host forecasts
|
|
158
|
+
* AND for a confirmed route whose host was resolved from workspace facts
|
|
159
|
+
* (no request-level targetHost). Each host only ever gets its own fact.
|
|
160
|
+
*/
|
|
161
|
+
perHost: Map<PromptInjectionTargetHost, RuntimePromptBudgetObservation>;
|
|
162
|
+
/** Latest per-host observation that is too old to be presented as current. */
|
|
163
|
+
stalePerHost: Map<PromptInjectionTargetHost, RuntimePromptBudgetObservation>;
|
|
164
|
+
}
|
|
165
|
+
export declare function selectConsoleRuntimeBudgetFacts(input: {
|
|
166
|
+
workspaceDir: string;
|
|
167
|
+
targetHost?: PromptInjectionTargetHost;
|
|
168
|
+
now?: Date;
|
|
169
|
+
}): ConsoleRuntimeBudgetFacts;
|
|
170
|
+
/**
|
|
171
|
+
* Rebuild the budget resolution an observation stands for, so console
|
|
172
|
+
* projections run under the SAME resolution shape the real injection used.
|
|
173
|
+
* `fallbackDetail` is deliberately dropped: it is log-only context and never
|
|
174
|
+
* rides events or console payloads.
|
|
175
|
+
*/
|
|
176
|
+
export declare function runtimeBudgetObservationToResolution(observation: RuntimePromptBudgetObservation): PromptBudgetResolution;
|