@principles/host-runtime 0.10.3 → 0.11.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/governance-signal-admission.d.ts +15 -4
- package/dist/governance-signal-admission.js +98 -11
- package/dist/index.js +33 -2
- package/dist/prompt-activation-deliverability.d.ts +36 -3
- package/dist/prompt-activation-deliverability.js +44 -26
- package/dist/prompt-injection-projection.d.ts +23 -1
- package/dist/prompt-injection-projection.js +25 -6
- package/dist/prompt-injection-status.d.ts +20 -5
- package/dist/prompt-injection-status.js +30 -7
- 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
|
}
|
|
@@ -144,12 +144,23 @@ export type AdmitGovernanceSignalsResult = {
|
|
|
144
144
|
ok: true;
|
|
145
145
|
outcomes: readonly GovernanceAdmissionOutcome[];
|
|
146
146
|
} | Degradation;
|
|
147
|
+
/** Public generic intake stays Codex + explicit WorkBuddy only. */
|
|
148
|
+
export declare function admitGovernanceSignals(input: AdmitGovernanceSignalsInput): AdmitGovernanceSignalsResult;
|
|
149
|
+
/** Pure check: no DB, no filesystem, no classifier, no historical transcript. */
|
|
150
|
+
export declare function assessWorkbuddyLiveKeyword(text: string, sessionId: string): {
|
|
151
|
+
matched: boolean;
|
|
152
|
+
reason: 'matched' | 'not_a_signal' | 'prompt_too_large';
|
|
153
|
+
};
|
|
147
154
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* failing candidate never rolls back another's admission.
|
|
155
|
+
* Narrow native event gateway: validate true current user origin and flags.
|
|
156
|
+
* The ordinary generic admission API cannot bypass this WorkBuddy gate.
|
|
151
157
|
*/
|
|
152
|
-
export declare function
|
|
158
|
+
export declare function admitTrustedWorkbuddyLiveCorrection(input: {
|
|
159
|
+
workspaceDir: string;
|
|
160
|
+
rawEvent: unknown;
|
|
161
|
+
now?: Date;
|
|
162
|
+
databaseFactory?: ObservationDatabaseFactory;
|
|
163
|
+
}): AdmitGovernanceSignalsResult;
|
|
153
164
|
export interface EnsureGovernanceTaskInput {
|
|
154
165
|
readonly workspaceDir: string;
|
|
155
166
|
readonly logicalObservationKey: string;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import fs from 'node:fs';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
import Database from 'better-sqlite3';
|
|
37
|
-
import { collectSync, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, boundCorrectionEvidenceText, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
|
|
37
|
+
import { collectSync, computeFeatureFlagsFromConfig, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, boundCorrectionEvidenceText, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
|
|
38
38
|
import { deriveProductionCorrectionPainIdentity, deriveProductionToolPainIdentity, hasProductionPainSchema, PRODUCTION_WRITE_TOOLS, } from './production-pain-evidence.js';
|
|
39
39
|
import { promoteGovernanceEvidence, ensureGovernanceSchema } from './governance-observation-store.js';
|
|
40
40
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
@@ -432,7 +432,8 @@ function toolFailureTaskPayload(fields) {
|
|
|
432
432
|
};
|
|
433
433
|
return JSON.stringify(payload);
|
|
434
434
|
}
|
|
435
|
-
function taskSubmissionPayload(candidate, workspaceDir,
|
|
435
|
+
function taskSubmissionPayload(candidate, workspaceDir, details) {
|
|
436
|
+
const { reason, canonicalPainId } = details;
|
|
436
437
|
if (candidate.kind === 'user_correction') {
|
|
437
438
|
// P1-1 privacy (review round 2): sanitize the FULL text before any
|
|
438
439
|
// truncation — a token crossing the 200-char boundary must be redacted, not
|
|
@@ -446,9 +447,14 @@ function taskSubmissionPayload(candidate, workspaceDir, reason) {
|
|
|
446
447
|
hostKind: candidate.hostKind,
|
|
447
448
|
// WorkBuddy explicit intake has no transcript observation to promote.
|
|
448
449
|
// The classified Owner statement is its input basis, not a pseudo-trace.
|
|
449
|
-
evidence: candidate.hostKind === 'workbuddy'
|
|
450
|
-
? []
|
|
451
|
-
|
|
450
|
+
evidence: candidate.hostKind === 'workbuddy'
|
|
451
|
+
? candidate.explicitOwnerStatement === true ? [] : [{
|
|
452
|
+
// WorkBuddy has no governance_observation, anchor to the canonical
|
|
453
|
+
// Pain persisted in this same trajectory.db transaction.
|
|
454
|
+
sourceRef: 'pain_event:' + (canonicalPainId ?? ''),
|
|
455
|
+
note: sanitizeString(candidate.text, workspaceDir),
|
|
456
|
+
}]
|
|
457
|
+
: [{ sourceRef: 'governance_observation:' + candidate.logicalObservationKey, note: sanitizeString(candidate.text, workspaceDir) }],
|
|
452
458
|
// SPEC-CODEX-PAIN-CLOSURE-001 CR-02 (merged from main): an explicit
|
|
453
459
|
// Owner statement rides as classified correction evidence — never
|
|
454
460
|
// summarized by an AI, never inside evidence[] pseudo-trace entries.
|
|
@@ -544,7 +550,7 @@ function admitCorrection({ db, candidate, workspaceDir, now, nowIso, keywordStor
|
|
|
544
550
|
// detection, so `detection` is optional — no ruleVersion exists for them.
|
|
545
551
|
ruleVersion: detection?.ruleVersion ?? null,
|
|
546
552
|
reason,
|
|
547
|
-
payloadJson: taskSubmissionPayload(candidate, workspaceDir, reason),
|
|
553
|
+
payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason, canonicalPainId: painId }),
|
|
548
554
|
nowIso,
|
|
549
555
|
});
|
|
550
556
|
})();
|
|
@@ -637,7 +643,7 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
|
|
|
637
643
|
canonicalPainId: painId,
|
|
638
644
|
ruleVersion: null,
|
|
639
645
|
reason: sanitizeString(reason, workspaceDir),
|
|
640
|
-
payloadJson: taskSubmissionPayload(candidate, workspaceDir, reason),
|
|
646
|
+
payloadJson: taskSubmissionPayload(candidate, workspaceDir, { reason }),
|
|
641
647
|
nowIso,
|
|
642
648
|
});
|
|
643
649
|
}
|
|
@@ -650,8 +656,8 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
|
|
|
650
656
|
}
|
|
651
657
|
return { disposition: 'admitted', kind: 'tool_failure', logicalObservationKey: candidate.logicalObservationKey, canonicalPainId: painId, duplicate, ruleVersion: null };
|
|
652
658
|
}
|
|
653
|
-
function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation }) {
|
|
654
|
-
if (candidate.hostKind !== 'codex' && !(candidate.hostKind === 'workbuddy' && candidate.kind === 'user_correction' && candidate.explicitOwnerStatement === true)) {
|
|
659
|
+
function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDegradation, trustedWorkbuddyLive }) {
|
|
660
|
+
if (candidate.hostKind !== 'codex' && !(candidate.hostKind === 'workbuddy' && candidate.kind === 'user_correction' && (candidate.explicitOwnerStatement === true || trustedWorkbuddyLive === true))) {
|
|
655
661
|
return { ok: false, reason: 'unsupported_host_kind', nextAction: 'governance signal admission accepts Codex observations or explicit WorkBuddy Owner statements' };
|
|
656
662
|
}
|
|
657
663
|
const marker = db.prepare('SELECT * FROM governance_signal_admissions WHERE logical_observation_key = ?').get(candidate.logicalObservationKey);
|
|
@@ -676,7 +682,7 @@ function admitOne({ db, candidate, workspaceDir, now, nowIso, keywordStore, onDe
|
|
|
676
682
|
* trajectory.db transaction (marker + pain row + quota together), so one
|
|
677
683
|
* failing candidate never rolls back another's admission.
|
|
678
684
|
*/
|
|
679
|
-
|
|
685
|
+
function admitGovernanceSignalsInternal(input, trustedWorkbuddyLive = false) {
|
|
680
686
|
const opened = openAdmissionStore(input.workspaceDir, input.databaseFactory);
|
|
681
687
|
if (!('db' in opened))
|
|
682
688
|
return opened;
|
|
@@ -691,7 +697,7 @@ export function admitGovernanceSignals(input) {
|
|
|
691
697
|
const outcomes = [];
|
|
692
698
|
try {
|
|
693
699
|
for (const candidate of input.candidates) {
|
|
694
|
-
outcomes.push(admitOne({ db, candidate, workspaceDir, now, nowIso, ...(input.keywordStore ? { keywordStore: input.keywordStore } : {}), ...(input.onDegradation ? { onDegradation: input.onDegradation } : {}) }));
|
|
700
|
+
outcomes.push(admitOne({ db, candidate, workspaceDir, now, nowIso, ...(input.keywordStore ? { keywordStore: input.keywordStore } : {}), ...(input.onDegradation ? { onDegradation: input.onDegradation } : {}), trustedWorkbuddyLive }));
|
|
695
701
|
}
|
|
696
702
|
return { ok: true, outcomes };
|
|
697
703
|
}
|
|
@@ -703,6 +709,87 @@ export function admitGovernanceSignals(input) {
|
|
|
703
709
|
close();
|
|
704
710
|
}
|
|
705
711
|
}
|
|
712
|
+
/** Public generic intake stays Codex + explicit WorkBuddy only. */
|
|
713
|
+
export function admitGovernanceSignals(input) {
|
|
714
|
+
return admitGovernanceSignalsInternal(input);
|
|
715
|
+
}
|
|
716
|
+
/** P0: a four-term projection of existing canonical high-precision seeds. */
|
|
717
|
+
const WORKBUDDY_AUTO_TERMS = new Set(['这是错的', '不要自作主张', '不应该这么做', '又搞错']);
|
|
718
|
+
const WORKBUDDY_MAX_PROMPT_BYTES = 16 * 1024;
|
|
719
|
+
function workbuddyP0KeywordStore() {
|
|
720
|
+
const seed = buildSharedSeedKeywordStore();
|
|
721
|
+
const terms = {};
|
|
722
|
+
for (const [term, def] of Object.entries(seed.terms)) {
|
|
723
|
+
if (WORKBUDDY_AUTO_TERMS.has(term) && def.category === 'correction' && def.precision === 'high') {
|
|
724
|
+
terms[term] = def;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return { version: seed.version, terms };
|
|
728
|
+
}
|
|
729
|
+
/** Pure check: no DB, no filesystem, no classifier, no historical transcript. */
|
|
730
|
+
export function assessWorkbuddyLiveKeyword(text, sessionId) {
|
|
731
|
+
if (Buffer.byteLength(text, 'utf8') > WORKBUDDY_MAX_PROMPT_BYTES) {
|
|
732
|
+
return { matched: false, reason: 'prompt_too_large' };
|
|
733
|
+
}
|
|
734
|
+
const store = workbuddyP0KeywordStore();
|
|
735
|
+
// Prefer precision to recall: reject quoted/embedded/reported phrases.
|
|
736
|
+
if (!Object.keys(store.terms).some(term => text.trimStart().toLowerCase().startsWith(term.toLowerCase()))) {
|
|
737
|
+
return { matched: false, reason: 'not_a_signal' };
|
|
738
|
+
}
|
|
739
|
+
const out = collectSync(text, sessionId, store, SYNC_DETECTION_CONFIG, new Date().toISOString());
|
|
740
|
+
const matched = out.isSignal && out.strength === 'STRONG' &&
|
|
741
|
+
out.type === 'correction' && out.matchedPrecision === 'high';
|
|
742
|
+
return { matched, reason: matched ? 'matched' : 'not_a_signal' };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Narrow native event gateway: validate true current user origin and flags.
|
|
746
|
+
* The ordinary generic admission API cannot bypass this WorkBuddy gate.
|
|
747
|
+
*/
|
|
748
|
+
export function admitTrustedWorkbuddyLiveCorrection(input) {
|
|
749
|
+
const raw = input.rawEvent;
|
|
750
|
+
if (!isRecord(raw) || own(raw, 'hook_event_name') !== 'UserPromptSubmit' ||
|
|
751
|
+
own(raw, 'is_internal_continuation') !== false) {
|
|
752
|
+
return { ok: false, reason: 'native_current_user_unverified', nextAction: 'Require a real current WorkBuddy UserPromptSubmit event.' };
|
|
753
|
+
}
|
|
754
|
+
const sessionId = own(raw, 'session_id');
|
|
755
|
+
const generationId = own(raw, 'generation_id');
|
|
756
|
+
const prompt = own(raw, 'prompt');
|
|
757
|
+
if (typeof sessionId !== 'string' || !sessionId.trim() ||
|
|
758
|
+
typeof generationId !== 'string' || !generationId.trim() ||
|
|
759
|
+
typeof prompt !== 'string') {
|
|
760
|
+
return { ok: false, reason: 'native_identity_invalid', nextAction: 'Require nonblank native session_id/generation_id and prompt.' };
|
|
761
|
+
}
|
|
762
|
+
const config = loadPdConfigForPlugin(input.workspaceDir);
|
|
763
|
+
if (!config.ok)
|
|
764
|
+
return { ok: false, reason: 'workbuddy_config_invalid', nextAction: 'Repair WorkBuddy governance workspace config.' };
|
|
765
|
+
const { flags } = computeFeatureFlagsFromConfig(config.effective);
|
|
766
|
+
if (flags['host.workbuddy']?.enabled !== true || flags.workbuddy_live_keyword_pain?.enabled !== true) {
|
|
767
|
+
return { ok: false, reason: 'workbuddy_auto_disabled', nextAction: 'Explicitly enable both WorkBuddy host and live keyword Pain.' };
|
|
768
|
+
}
|
|
769
|
+
if (prompt.trimStart().startsWith('PD记录痛苦')) {
|
|
770
|
+
return { ok: false, reason: 'explicit_owner_statement_reserved', nextAction: 'Use original Owner intake; no keyword fallback.' };
|
|
771
|
+
}
|
|
772
|
+
if (!assessWorkbuddyLiveKeyword(prompt, sessionId).matched) {
|
|
773
|
+
return { ok: true, outcomes: [] };
|
|
774
|
+
}
|
|
775
|
+
const nativeIdentity = JSON.stringify(['workbuddy', sessionId, generationId]);
|
|
776
|
+
return admitGovernanceSignalsInternal({
|
|
777
|
+
workspaceDir: input.workspaceDir,
|
|
778
|
+
candidates: [{
|
|
779
|
+
kind: 'user_correction',
|
|
780
|
+
hostKind: 'workbuddy',
|
|
781
|
+
logicalObservationKey: nativeIdentity + '|user',
|
|
782
|
+
rolloutIdentity: JSON.stringify(['workbuddy', sessionId]),
|
|
783
|
+
rootSessionId: sessionId,
|
|
784
|
+
hostTurnId: nativeIdentity,
|
|
785
|
+
text: prompt,
|
|
786
|
+
observedAt: (input.now ?? new Date()).toISOString(),
|
|
787
|
+
}],
|
|
788
|
+
keywordStore: workbuddyP0KeywordStore(),
|
|
789
|
+
...(input.now ? { now: input.now } : {}),
|
|
790
|
+
...(input.databaseFactory ? { databaseFactory: input.databaseFactory } : {}),
|
|
791
|
+
}, true);
|
|
792
|
+
}
|
|
706
793
|
// ─── Diagnostician continuation (one pain → exactly one pending task) ───────
|
|
707
794
|
function isGovernanceTaskPayload(value) {
|
|
708
795
|
if (!isRecord(value))
|
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';
|
|
@@ -211,6 +211,26 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
211
211
|
const prompt = await buildActivePrinciplePromptContext({
|
|
212
212
|
workspaceDir: event.context.workspaceDir,
|
|
213
213
|
excludePrincipleIds: options.promptExcludePrincipleIds?.(event),
|
|
214
|
+
promptBudget: resolvePromptBudget({
|
|
215
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: resolve ONCE per request
|
|
216
|
+
// from the host's own hook facts (OpenClaw effective window /
|
|
217
|
+
// Codex+WorkBuddy model id) against the installed local Pi AI
|
|
218
|
+
// catalog — no network, no LLM call. Missing/untrusted facts
|
|
219
|
+
// resolve to the legacy fixed budget with an observable reason.
|
|
220
|
+
hostCapacity: event.context.modelContext === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: {
|
|
223
|
+
contextTokenBudget: event.context.modelContext.contextTokenBudget,
|
|
224
|
+
contextWindowSource: event.context.modelContext.contextWindowSource,
|
|
225
|
+
},
|
|
226
|
+
modelIdentity: event.context.modelContext === undefined
|
|
227
|
+
? undefined
|
|
228
|
+
: {
|
|
229
|
+
providerId: event.context.modelContext.modelProviderId,
|
|
230
|
+
modelId: event.context.modelContext.modelId,
|
|
231
|
+
},
|
|
232
|
+
catalog: getPromptModelCatalog(),
|
|
233
|
+
}),
|
|
214
234
|
});
|
|
215
235
|
// PRI-750: record the injection event on the shared path with the host
|
|
216
236
|
// turn id (Codex turn_id → runId) so receipt events carry a turn-level
|
|
@@ -231,11 +251,22 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
231
251
|
injectedCount: prompt.principleIds.length,
|
|
232
252
|
skippedWarnings: prompt.warnings,
|
|
233
253
|
injectedCharCount: prompt.additionalContext.length,
|
|
234
|
-
budget:
|
|
254
|
+
budget: prompt.budget,
|
|
235
255
|
...(options.hostKind === 'openclaw' || options.hostKind === 'codex' || options.hostKind === 'workbuddy' ? { hostKind: options.hostKind } : {}),
|
|
236
256
|
eventId: randomUUID(),
|
|
237
257
|
...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
|
|
238
258
|
...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
|
|
259
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: budget provenance — the
|
|
260
|
+
// recorded budget is the one THIS injection actually ran under
|
|
261
|
+
// (prompt.budget), never a constant restated after the fact.
|
|
262
|
+
...(prompt.budgetPolicyVersion !== undefined ? { budgetPolicyVersion: prompt.budgetPolicyVersion } : {}),
|
|
263
|
+
...(prompt.budgetCapacitySource !== undefined ? { budgetCapacitySource: prompt.budgetCapacitySource } : {}),
|
|
264
|
+
...(prompt.budgetCapacityStatus !== undefined ? { budgetCapacityStatus: prompt.budgetCapacityStatus } : {}),
|
|
265
|
+
...(prompt.budgetContextWindowTokens != null ? { budgetContextWindowTokens: prompt.budgetContextWindowTokens } : {}),
|
|
266
|
+
// SPEC §7 v0.2: the bounded fallback reason code rides the event so
|
|
267
|
+
// operators can tell model-identity absence from catalog ambiguity
|
|
268
|
+
// etc. without touching logs. Detail stays out of events.
|
|
269
|
+
...(prompt.budgetFallbackReason !== undefined ? { budgetFallbackReason: prompt.budgetFallbackReason } : {}),
|
|
239
270
|
// PRI-904: selection diagnostics (bounded, optional). On this route
|
|
240
271
|
// the policy is legacy_fifo_prefix_v1 and NO rotation provenance is
|
|
241
272
|
// 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',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
1
|
+
import { type PromptBudgetResolution, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
2
2
|
/** Hosts with a production prompt-injection path today (shared GovernanceHostKind boundary). */
|
|
3
3
|
export type PromptInjectionTargetHost = 'openclaw' | 'codex' | 'workbuddy';
|
|
4
4
|
/** Single host roster (review P2): every consumer derives from this list, so a
|
|
@@ -140,6 +140,20 @@ export interface PromptInjectionProjection {
|
|
|
140
140
|
* route (where `usedChars` already IS the full render).
|
|
141
141
|
*/
|
|
142
142
|
fullRenderChars?: number;
|
|
143
|
+
/**
|
|
144
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1/§7: provenance of `budget` —
|
|
145
|
+
* the same policy/resolution the real injection runs, so consumers can
|
|
146
|
+
* tell "runtime capacity confirmed" apart from "fixed-budget fallback"
|
|
147
|
+
* instead of assuming the forecast matches every model.
|
|
148
|
+
*/
|
|
149
|
+
budgetCapacitySource?: PromptBudgetResolution['capacitySource'];
|
|
150
|
+
budgetCapacityStatus?: PromptBudgetResolution['capacityStatus'];
|
|
151
|
+
budgetContextWindowTokens?: number | null;
|
|
152
|
+
budgetPolicyVersion?: PromptBudgetResolution['policyVersion'];
|
|
153
|
+
/** SPEC §7 v0.2: bounded fallback reason code; present only on legacy_fallback. */
|
|
154
|
+
budgetFallbackReason?: PromptBudgetResolution['fallbackReason'];
|
|
155
|
+
/** Non-sensitive log context; never emitted to events. */
|
|
156
|
+
budgetFallbackDetail?: string;
|
|
143
157
|
}
|
|
144
158
|
/**
|
|
145
159
|
* PR #1844 follow-up: forecast the prompt-injection budget from the SAME
|
|
@@ -186,6 +200,14 @@ export declare function buildLivePromptInjectionProjection(input: {
|
|
|
186
200
|
roundKey?: number;
|
|
187
201
|
/** AC-01: explicit route binding — 'codex'/'workbuddy' always shared, 'openclaw' by flag. */
|
|
188
202
|
targetHost?: PromptInjectionTargetHost;
|
|
203
|
+
/**
|
|
204
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1: budget resolution this
|
|
205
|
+
* projection must run under. Absent → the fixed-fallback default (the
|
|
206
|
+
* honest "no runtime capacity confirmed" policy), NOT a silent second
|
|
207
|
+
* policy. Callers holding live model facts (the plugin hook) pass the
|
|
208
|
+
* SAME resolution the injection uses.
|
|
209
|
+
*/
|
|
210
|
+
promptBudget?: PromptBudgetResolution;
|
|
189
211
|
}): Promise<PromptInjectionProjection>;
|
|
190
212
|
/**
|
|
191
213
|
* AC-01/R-A1: the host-aware entry every console/CLI consumer should use.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computeFeatureFlagsFromConfig, renderPrinciplesToDirectives, resolvePromptBudget, trimToBudget, } from '@principles/core/runtime-v2';
|
|
2
2
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
3
3
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
4
4
|
import { buildActivePrinciplePromptContext, readPromptActivationCandidates, MAX_SHARED_DIAGNOSTIC_IDS } from './active-principle-prompt.js';
|
|
@@ -223,6 +223,7 @@ function isDiagnosticCapHit(ids) {
|
|
|
223
223
|
* `selectionPolicy` on the result.
|
|
224
224
|
*/
|
|
225
225
|
export async function buildLivePromptInjectionProjection(input) {
|
|
226
|
+
const promptBudget = input.promptBudget ?? resolvePromptBudget();
|
|
226
227
|
const flagEnabled = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
|
|
227
228
|
// Route face follows THE one route formula (review P2-2: a re-typed
|
|
228
229
|
// codex-only ternary forecast WorkBuddy as legacy_trim while its adapter
|
|
@@ -232,7 +233,7 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
232
233
|
? flagEnabled
|
|
233
234
|
: promptRouteForHost(input.targetHost, flagEnabled) === 'shared_render';
|
|
234
235
|
if (sharedRoute) {
|
|
235
|
-
const context = await buildActivePrinciplePromptContext(input);
|
|
236
|
+
const context = await buildActivePrinciplePromptContext({ ...input, promptBudget });
|
|
236
237
|
const oversized = context.oversizedActivationIds ?? [];
|
|
237
238
|
const dropped = context.droppedActivationIds ?? [];
|
|
238
239
|
return {
|
|
@@ -256,6 +257,12 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
256
257
|
droppedActivationIds: dropped,
|
|
257
258
|
oversizedDiagnosticTruncated: isDiagnosticCapHit(oversized),
|
|
258
259
|
droppedDiagnosticTruncated: isDiagnosticCapHit(dropped),
|
|
260
|
+
budgetCapacitySource: context.budgetCapacitySource,
|
|
261
|
+
budgetCapacityStatus: context.budgetCapacityStatus,
|
|
262
|
+
budgetContextWindowTokens: context.budgetContextWindowTokens,
|
|
263
|
+
budgetPolicyVersion: context.budgetPolicyVersion,
|
|
264
|
+
...(context.budgetFallbackReason !== undefined ? { budgetFallbackReason: context.budgetFallbackReason } : {}),
|
|
265
|
+
...(context.budgetFallbackDetail !== undefined ? { budgetFallbackDetail: context.budgetFallbackDetail } : {}),
|
|
259
266
|
};
|
|
260
267
|
}
|
|
261
268
|
const candidates = await readPromptActivationCandidates(input);
|
|
@@ -263,7 +270,7 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
263
270
|
if (candidates.aborted || principles.length === 0) {
|
|
264
271
|
return {
|
|
265
272
|
route: 'legacy_trim',
|
|
266
|
-
budget:
|
|
273
|
+
budget: promptBudget.budgetChars,
|
|
267
274
|
usedChars: 0,
|
|
268
275
|
truncated: false,
|
|
269
276
|
injectedPrincipleIds: [],
|
|
@@ -279,11 +286,17 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
279
286
|
droppedActivationIds: [],
|
|
280
287
|
oversizedDiagnosticTruncated: false,
|
|
281
288
|
droppedDiagnosticTruncated: false,
|
|
289
|
+
budgetCapacitySource: promptBudget.capacitySource,
|
|
290
|
+
budgetCapacityStatus: promptBudget.capacityStatus,
|
|
291
|
+
budgetContextWindowTokens: promptBudget.contextWindowTokens,
|
|
292
|
+
budgetPolicyVersion: promptBudget.policyVersion,
|
|
293
|
+
...(promptBudget.fallbackReason !== undefined ? { budgetFallbackReason: promptBudget.fallbackReason } : {}),
|
|
294
|
+
...(promptBudget.fallbackDetail !== undefined ? { budgetFallbackDetail: promptBudget.fallbackDetail } : {}),
|
|
282
295
|
};
|
|
283
296
|
}
|
|
284
297
|
// PR-1894: pass the caller's round key so the forecast runs the SAME
|
|
285
298
|
// packing policy production runs this turn.
|
|
286
|
-
const trimmed = trimToBudget(principles,
|
|
299
|
+
const trimmed = trimToBudget(principles, promptBudget.budgetChars, escapeXml, input.roundKey);
|
|
287
300
|
const injected = principles.filter((p) => trimmed.injectedIds.has(p.principleId));
|
|
288
301
|
// PR-1894: the legacy_trim route IS the OpenClaw plugin-local route, which
|
|
289
302
|
// rotates on every recorded user turn (PRI-904). Reachability is therefore
|
|
@@ -291,11 +304,11 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
291
304
|
// round key — a console without a session key must still be able to tell
|
|
292
305
|
// "queued behind rotation" apart from "structurally starved", otherwise it
|
|
293
306
|
// keeps telling the Owner to deactivate healthy principles.
|
|
294
|
-
const eventuallyInjectedActivationIds = collectReachableActivationIds(principles,
|
|
307
|
+
const eventuallyInjectedActivationIds = collectReachableActivationIds(principles, promptBudget.budgetChars);
|
|
295
308
|
const fullRenderChars = renderPrinciplesToDirectives(injected, trimmed.injectedIds, { escapeFn: escapeXml, selfReportInstruction: candidates.selfReportEnabled }).length;
|
|
296
309
|
return {
|
|
297
310
|
route: 'legacy_trim',
|
|
298
|
-
budget:
|
|
311
|
+
budget: promptBudget.budgetChars,
|
|
299
312
|
usedChars: trimmed.lines.join('\n').length,
|
|
300
313
|
truncated: trimmed.truncated,
|
|
301
314
|
injectedPrincipleIds: [...trimmed.injectedIds],
|
|
@@ -312,6 +325,12 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
312
325
|
oversizedDiagnosticTruncated: isDiagnosticCapHit(trimmed.oversizedActivationIds),
|
|
313
326
|
droppedDiagnosticTruncated: isDiagnosticCapHit(trimmed.droppedActivationIds),
|
|
314
327
|
...(injected.length > 0 ? { fullRenderChars } : {}),
|
|
328
|
+
budgetCapacitySource: promptBudget.capacitySource,
|
|
329
|
+
budgetCapacityStatus: promptBudget.capacityStatus,
|
|
330
|
+
budgetContextWindowTokens: promptBudget.contextWindowTokens,
|
|
331
|
+
budgetPolicyVersion: promptBudget.policyVersion,
|
|
332
|
+
...(promptBudget.fallbackReason !== undefined ? { budgetFallbackReason: promptBudget.fallbackReason } : {}),
|
|
333
|
+
...(promptBudget.fallbackDetail !== undefined ? { budgetFallbackDetail: promptBudget.fallbackDetail } : {}),
|
|
315
334
|
};
|
|
316
335
|
}
|
|
317
336
|
export async function resolveLivePromptInjectionProjection(input) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PromptBudgetTierFit } from '@principles/core/runtime-v2';
|
|
1
2
|
import { type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
2
3
|
/**
|
|
3
4
|
* SPEC PD_PROMPT_CAPACITY_V1 R-A3: per-live-activation injection status for
|
|
@@ -9,25 +10,39 @@ import { type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, typ
|
|
|
9
10
|
* `route_unconfirmed` surfaces AC-01's honest unknown instead of a guess.
|
|
10
11
|
* No compliance/遵守 counts are produced here — absence of injection logs is
|
|
11
12
|
* NOT reported as "zero compliance" (AC-12 boundary).
|
|
13
|
+
*
|
|
14
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: "oversized" now means the entry
|
|
15
|
+
* exceeds the MAXIMUM supported budget (never injectable on any supported
|
|
16
|
+
* model); a row that misses the CURRENT projection budget but fits within
|
|
17
|
+
* the supported [min, max] range is `model_dependent` — whether it injects
|
|
18
|
+
* depends on the runtime model's context window, which this surface cannot
|
|
19
|
+
* see. Budget figures come from the projection's resolution (legacy
|
|
20
|
+
* fallback when no runtime capacity is confirmed), never a bare constant.
|
|
12
21
|
*/
|
|
13
|
-
export type PromptActivationInjectionStatusKind = 'injectable' | 'oversized' | 'window_excluded' | 'resolution_failed' | 'route_unconfirmed';
|
|
22
|
+
export type PromptActivationInjectionStatusKind = 'injectable' | 'oversized' | 'model_dependent' | 'window_excluded' | 'resolution_failed' | 'route_unconfirmed';
|
|
14
23
|
export interface PromptActivationInjectionStatus {
|
|
15
24
|
activationId: string;
|
|
16
25
|
principleId?: string;
|
|
17
26
|
status: PromptActivationInjectionStatusKind;
|
|
18
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Present for injectable/window_excluded/oversized/model_dependent under a
|
|
29
|
+
* confirmed route. For model_dependent rows it keeps the POSITIVE window
|
|
30
|
+
* fact: true = the entry fits THIS surface's (usually fallback) projection
|
|
31
|
+
* window, but still exceeds the measured floor — a smaller-context runtime
|
|
32
|
+
* model would exclude it.
|
|
33
|
+
*/
|
|
19
34
|
inCurrentWindow?: boolean;
|
|
20
35
|
/** Present when the single-item cost was computable. */
|
|
21
36
|
costChars?: number;
|
|
22
37
|
budget?: number;
|
|
23
38
|
budgetScope?: PromptInjectionBudgetScope;
|
|
24
39
|
route?: PromptInjectionRouteName;
|
|
25
|
-
/** Present for resolution_failed / route_unconfirmed (structured reason). */
|
|
40
|
+
/** Present for resolution_failed / route_unconfirmed / model_dependent (structured reason). */
|
|
26
41
|
reason?: string;
|
|
27
|
-
/** Present for route_unconfirmed: per-host single-item fit facts. */
|
|
42
|
+
/** Present for route_unconfirmed: per-host single-item tier fit facts. */
|
|
28
43
|
perHostFits?: readonly {
|
|
29
44
|
hostKind: PromptInjectionTargetHost;
|
|
30
|
-
|
|
45
|
+
tierFit: PromptBudgetTierFit;
|
|
31
46
|
}[];
|
|
32
47
|
}
|
|
33
48
|
export interface PromptActivationInjectionStatusReport {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { PROMPT_BUDGET_MAX_CHARS, PROMPT_BUDGET_MIN_CHARS, SqliteActivationStateStore, SqliteConnection, SqlitePIArtifactStore, classifySingleItemBudgetTier, computeFeatureFlagsFromConfig, filterPromptActivations, resolvePrincipleFromArtifact, } from '@principles/core/runtime-v2';
|
|
4
4
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
5
5
|
import { computePromptSingleItemFit } from './prompt-activation-deliverability.js';
|
|
6
6
|
import { PROMPT_INJECTION_TARGET_HOSTS, buildLivePromptInjectionProjection, promptRouteForHost, resolvePromptInjectionRouteDecision, } from './prompt-injection-projection.js';
|
|
@@ -86,12 +86,12 @@ export async function readPromptActivationInjectionStatuses(input) {
|
|
|
86
86
|
}
|
|
87
87
|
const perHostFits = PROMPT_INJECTION_TARGET_HOSTS.map((hostKind) => {
|
|
88
88
|
const hostRoute = promptRouteForHost(hostKind, route.flagEnabled);
|
|
89
|
-
const {
|
|
89
|
+
const { costChars } = computePromptSingleItemFit({
|
|
90
90
|
principle: { principleId: row.principleId, text: row.text, artifactId: '', activationId: row.activationId },
|
|
91
91
|
route: hostRoute,
|
|
92
92
|
selfReportEnabled,
|
|
93
93
|
});
|
|
94
|
-
return { hostKind,
|
|
94
|
+
return { hostKind, tierFit: classifySingleItemBudgetTier(costChars) };
|
|
95
95
|
});
|
|
96
96
|
return {
|
|
97
97
|
activationId: row.activationId,
|
|
@@ -116,20 +116,43 @@ export async function readPromptActivationInjectionStatuses(input) {
|
|
|
116
116
|
principle: { principleId: row.principleId, text: row.text, artifactId: '', activationId: row.activationId },
|
|
117
117
|
route: projection.route,
|
|
118
118
|
selfReportEnabled,
|
|
119
|
+
budget: projection.budget,
|
|
119
120
|
});
|
|
120
121
|
const inCurrentWindow = projection.injectedActivationIds.includes(row.activationId);
|
|
121
122
|
const budgetScope = projection.budgetScope;
|
|
122
|
-
|
|
123
|
+
const serialization = projection.route === 'shared_render' ? 'full directive render' : 'list entry';
|
|
124
|
+
// Tier verdict FIRST (SPEC §6.3): the projection budget on this surface
|
|
125
|
+
// is usually the fixed fallback (2000) — the console holds no runtime
|
|
126
|
+
// model facts. An entry in (MIN, fallback] fits THAT estimate and may
|
|
127
|
+
// even sit in the current window, but it exceeds the measured floor, so
|
|
128
|
+
// on a 32K/64K model (budget = floor) it would NOT inject. Reporting it
|
|
129
|
+
// as an unconditional `injectable` would contradict the approval-time
|
|
130
|
+
// `model_dependent` verdict for the very same artifact.
|
|
131
|
+
const tierFit = classifySingleItemBudgetTier(costChars);
|
|
132
|
+
if (tierFit === 'exceeds_max_tier') {
|
|
123
133
|
return {
|
|
124
134
|
activationId: row.activationId,
|
|
125
135
|
principleId: row.principleId,
|
|
126
136
|
status: 'oversized',
|
|
127
137
|
inCurrentWindow: false,
|
|
128
138
|
costChars,
|
|
129
|
-
budget:
|
|
139
|
+
budget: projection.budget,
|
|
130
140
|
budgetScope,
|
|
131
141
|
route: projection.route,
|
|
132
|
-
reason: `single_item_exceeds_budget: this principle's own entry is ${costChars} chars > ${
|
|
142
|
+
reason: `single_item_exceeds_budget: this principle's own entry is ${costChars} chars > the ${PROMPT_BUDGET_MAX_CHARS}-char maximum supported budget under the ${serialization} serialization`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (tierFit === 'model_dependent') {
|
|
146
|
+
return {
|
|
147
|
+
activationId: row.activationId,
|
|
148
|
+
principleId: row.principleId,
|
|
149
|
+
status: 'model_dependent',
|
|
150
|
+
inCurrentWindow,
|
|
151
|
+
costChars,
|
|
152
|
+
budget: projection.budget,
|
|
153
|
+
budgetScope,
|
|
154
|
+
route: projection.route,
|
|
155
|
+
reason: `model_dependent_capacity: this principle's own entry is ${costChars} chars — above the ${PROMPT_BUDGET_MIN_CHARS}-char measured floor, so smaller-context models exclude it${fits ? ' even though it fits this surface\'s fallback-budget estimate' : ''}; injection depends on the runtime model`,
|
|
133
156
|
};
|
|
134
157
|
}
|
|
135
158
|
return {
|
|
@@ -138,7 +161,7 @@ export async function readPromptActivationInjectionStatuses(input) {
|
|
|
138
161
|
status: inCurrentWindow ? 'injectable' : 'window_excluded',
|
|
139
162
|
inCurrentWindow,
|
|
140
163
|
costChars,
|
|
141
|
-
budget:
|
|
164
|
+
budget: projection.budget,
|
|
142
165
|
budgetScope,
|
|
143
166
|
route: projection.route,
|
|
144
167
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint": "eslint \"src/**/*.ts\""
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@principles/core": "^1.
|
|
34
|
+
"@principles/core": "^1.295.0",
|
|
35
35
|
"@principles/install-layout": "^0.4.0",
|
|
36
36
|
"better-sqlite3": "^13.0.3",
|
|
37
37
|
"js-yaml": "^5.4.3"
|