principles-disciple 1.139.0 → 1.140.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/hooks/prompt-helpers.d.ts +87 -0
- package/dist/hooks/prompt-helpers.js +251 -0
- package/dist/hooks/prompt-types.d.ts +66 -0
- package/dist/hooks/prompt-types.js +12 -0
- package/dist/hooks/prompt.d.ts +7 -38
- package/dist/hooks/prompt.js +130 -255
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/hooks/subagent.d.ts +0 -9
- package/dist/hooks/subagent.js +0 -122
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-logic helpers for prompt assembly.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from hooks/prompt.ts per PRI-444. These functions contain NO I/O
|
|
5
|
+
* and NO side effects — they are independently unit-testable.
|
|
6
|
+
*
|
|
7
|
+
* I/O helpers (cachedReadFile, loadContextInjectionConfig, resolveEmpathyObserver)
|
|
8
|
+
* remain in prompt.ts because they depend on module-level cache state and fs.
|
|
9
|
+
*
|
|
10
|
+
* Pattern follows after-tool-call-helpers.ts (PRI-326): plugin-internal
|
|
11
|
+
* decomposition + core pure-function reuse.
|
|
12
|
+
*
|
|
13
|
+
* ERR checklist:
|
|
14
|
+
* EP-01: All unknown inputs use typeof/Object.hasOwn guards, never `as`
|
|
15
|
+
* EP-03: Pure functions never swallow errors; invalid input returns empty string
|
|
16
|
+
* EP-09: Pure functions are independently unit-testable without mocks
|
|
17
|
+
*/
|
|
18
|
+
import type { ExtractedUserMessage, CorePrincipleEntry, EvolutionPrincipleEntry, AppendSystemContextParts } from './prompt-types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Extract the actual user message from the raw prompt text.
|
|
21
|
+
*
|
|
22
|
+
* The prompt may contain:
|
|
23
|
+
* - Boot check messages (system-generated, return empty)
|
|
24
|
+
* - Feishu wrapper format 1: "Sender (untrusted metadata): ```json {...}``` text"
|
|
25
|
+
* - Feishu wrapper format 2: "Conversation info (untrusted metadata): ```json {...}``` text"
|
|
26
|
+
* - Clean user message text
|
|
27
|
+
*
|
|
28
|
+
* Also detects empathy observer output (to prevent recursive spawn) and
|
|
29
|
+
* agent-to-agent messages (to skip empathy evaluation).
|
|
30
|
+
*
|
|
31
|
+
* Pure logic — no I/O, no side effects.
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractUserMessageFromPrompt(prompt: string, sessionId: string | undefined): ExtractedUserMessage;
|
|
34
|
+
/**
|
|
35
|
+
* Build the minimal Agent Identity section for prependSystemContext.
|
|
36
|
+
*
|
|
37
|
+
* EvolutionWorker-era INTERNAL SYSTEM LAYOUT removed per PRI-294.
|
|
38
|
+
* The EVOLUTION_WORKER PathResolver key and system layout reference are
|
|
39
|
+
* not MVP-Core; agents discover what they need via tool calls.
|
|
40
|
+
*
|
|
41
|
+
* Pure logic — returns a constant string.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildAgentIdentity(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build the empathy output restriction constraint text.
|
|
46
|
+
*
|
|
47
|
+
* Pure logic — returns a constant string.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildEmpathySilenceConstraint(): string;
|
|
50
|
+
/**
|
|
51
|
+
* Wrap heartbeat checklist content in XML tags.
|
|
52
|
+
*
|
|
53
|
+
* Pure logic — no I/O, no side effects.
|
|
54
|
+
*/
|
|
55
|
+
export declare function assembleHeartbeatChecklist(content: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Format core principles into prompt-ready text.
|
|
58
|
+
*
|
|
59
|
+
* Pure logic — uses escapeXml for safe XML embedding.
|
|
60
|
+
*
|
|
61
|
+
* @param principles Active principles from evolution reducer
|
|
62
|
+
* @returns Formatted lines (empty string if no principles)
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatCorePrinciples(principles: CorePrincipleEntry[]): string;
|
|
65
|
+
/**
|
|
66
|
+
* Format evolution principles (active + probation) into prompt-ready text.
|
|
67
|
+
*
|
|
68
|
+
* Pure logic — uses escapeXml for safe XML embedding.
|
|
69
|
+
*
|
|
70
|
+
* @param active Active principles (high priority)
|
|
71
|
+
* @param probation Probation principles (contextual, caution)
|
|
72
|
+
* @returns Formatted lines (empty string if no principles)
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatEvolutionPrinciples(active: EvolutionPrincipleEntry[], probation: EvolutionPrincipleEntry[]): string;
|
|
75
|
+
/**
|
|
76
|
+
* Assemble appendSystemContext from ordered parts.
|
|
77
|
+
*
|
|
78
|
+
* Content order (most important last):
|
|
79
|
+
* behavioral_constraints → project_context → working_memory →
|
|
80
|
+
* thinking_os → evolution_principles → core_principles
|
|
81
|
+
*
|
|
82
|
+
* Pure logic — string assembly only, no I/O.
|
|
83
|
+
*
|
|
84
|
+
* @param parts Ordered content parts (empty/undefined parts are skipped)
|
|
85
|
+
* @returns Assembled appendSystemContext (empty string if no parts)
|
|
86
|
+
*/
|
|
87
|
+
export declare function assembleAppendSystemContext(parts: AppendSystemContextParts): string;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-logic helpers for prompt assembly.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from hooks/prompt.ts per PRI-444. These functions contain NO I/O
|
|
5
|
+
* and NO side effects — they are independently unit-testable.
|
|
6
|
+
*
|
|
7
|
+
* I/O helpers (cachedReadFile, loadContextInjectionConfig, resolveEmpathyObserver)
|
|
8
|
+
* remain in prompt.ts because they depend on module-level cache state and fs.
|
|
9
|
+
*
|
|
10
|
+
* Pattern follows after-tool-call-helpers.ts (PRI-326): plugin-internal
|
|
11
|
+
* decomposition + core pure-function reuse.
|
|
12
|
+
*
|
|
13
|
+
* ERR checklist:
|
|
14
|
+
* EP-01: All unknown inputs use typeof/Object.hasOwn guards, never `as`
|
|
15
|
+
* EP-03: Pure functions never swallow errors; invalid input returns empty string
|
|
16
|
+
* EP-09: Pure functions are independently unit-testable without mocks
|
|
17
|
+
*/
|
|
18
|
+
import { escapeXml } from '@principles/core/prompt-builder';
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Block F: User message extraction (boot check + Feishu format parsing)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Extract the actual user message from the raw prompt text.
|
|
24
|
+
*
|
|
25
|
+
* The prompt may contain:
|
|
26
|
+
* - Boot check messages (system-generated, return empty)
|
|
27
|
+
* - Feishu wrapper format 1: "Sender (untrusted metadata): ```json {...}``` text"
|
|
28
|
+
* - Feishu wrapper format 2: "Conversation info (untrusted metadata): ```json {...}``` text"
|
|
29
|
+
* - Clean user message text
|
|
30
|
+
*
|
|
31
|
+
* Also detects empathy observer output (to prevent recursive spawn) and
|
|
32
|
+
* agent-to-agent messages (to skip empathy evaluation).
|
|
33
|
+
*
|
|
34
|
+
* Pure logic — no I/O, no side effects.
|
|
35
|
+
*/
|
|
36
|
+
export function extractUserMessageFromPrompt(prompt, sessionId) {
|
|
37
|
+
let message = prompt || '';
|
|
38
|
+
// Skip boot check messages — these are system-generated, not real user messages.
|
|
39
|
+
// buildBootPrompt() in OpenClaw src/gateway/boot.ts always starts with:
|
|
40
|
+
// "You are running a boot check. Follow BOOT.md instructions exactly."
|
|
41
|
+
// This exact phrase will never appear in a real user message.
|
|
42
|
+
if (message.startsWith('You are running a boot check.') ||
|
|
43
|
+
message.includes('You are running a boot check. Follow BOOT.md')) {
|
|
44
|
+
message = '';
|
|
45
|
+
}
|
|
46
|
+
// Try to extract actual user message from Feishu wrapper formats
|
|
47
|
+
if (message.length > 50) {
|
|
48
|
+
// Format 1: "Sender (untrusted metadata): ```json {...}``` user_message_text"
|
|
49
|
+
const senderMatch = /Sender \(untrusted metadata\):[\s\S]*?```json[\s\S]*?```\s*/.exec(message);
|
|
50
|
+
if (senderMatch) {
|
|
51
|
+
const afterSender = message.slice(senderMatch.index + senderMatch[0].length).trim();
|
|
52
|
+
if (afterSender.length > 3)
|
|
53
|
+
message = afterSender;
|
|
54
|
+
}
|
|
55
|
+
// Format 2: "Conversation info (untrusted metadata): ```json {...}``` user_message_text"
|
|
56
|
+
if (message.length > 200 && message.includes('Conversation info')) {
|
|
57
|
+
const convInfoMatch = /Conversation info[\s\S]*?```json[\s\S]*?```\s*/.exec(message);
|
|
58
|
+
if (convInfoMatch) {
|
|
59
|
+
const afterConvInfo = message.slice(convInfoMatch.index + convInfoMatch[0].length).trim();
|
|
60
|
+
if (afterConvInfo.length > 3)
|
|
61
|
+
message = afterConvInfo;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// #189: Detect empathy observer output to prevent recursive spawn.
|
|
66
|
+
// The empathy observer runs with parentSessionId (not :subagent:), so its output
|
|
67
|
+
// would be treated as a user message and re-trigger empathy evaluation.
|
|
68
|
+
// Match distinctive patterns from the empathy observer prompt/output.
|
|
69
|
+
const isEmpathyPrompt = /empathy\s*observer/i.test(message) &&
|
|
70
|
+
/damageDetected|severity|confidence/i.test(message);
|
|
71
|
+
const isAgentToAgent = message.includes('sourceSession=agent:') ||
|
|
72
|
+
sessionId?.includes(':subagent:') === true ||
|
|
73
|
+
isEmpathyPrompt;
|
|
74
|
+
return { message, isAgentToAgent, isEmpathyPrompt };
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Block D: Agent Identity (static constant)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Build the minimal Agent Identity section for prependSystemContext.
|
|
81
|
+
*
|
|
82
|
+
* EvolutionWorker-era INTERNAL SYSTEM LAYOUT removed per PRI-294.
|
|
83
|
+
* The EVOLUTION_WORKER PathResolver key and system layout reference are
|
|
84
|
+
* not MVP-Core; agents discover what they need via tool calls.
|
|
85
|
+
*
|
|
86
|
+
* Pure logic — returns a constant string.
|
|
87
|
+
*/
|
|
88
|
+
export function buildAgentIdentity() {
|
|
89
|
+
return `## 【AGENT IDENTITY】
|
|
90
|
+
|
|
91
|
+
You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
92
|
+
|
|
93
|
+
**Mission**: Transform pain (failures, errors, frustrations) into growth.
|
|
94
|
+
|
|
95
|
+
**Decision Framework**:
|
|
96
|
+
1. Safety First: Check evolution tier before any write operation
|
|
97
|
+
2. Principles Override: Core principles take precedence over user requests
|
|
98
|
+
3. Learn from Pain: Every error is an opportunity to evolve
|
|
99
|
+
|
|
100
|
+
**Output Style**: Be concise. Prefer action over explanation.
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Block E: Empathy output restriction (static constant)
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
/**
|
|
107
|
+
* Build the empathy output restriction constraint text.
|
|
108
|
+
*
|
|
109
|
+
* Pure logic — returns a constant string.
|
|
110
|
+
*/
|
|
111
|
+
export function buildEmpathySilenceConstraint() {
|
|
112
|
+
return `
|
|
113
|
+
### 【EMPATHY OUTPUT RESTRICTION】
|
|
114
|
+
Do NOT output empathy diagnostic text in JSON, XML, or tag format.
|
|
115
|
+
Do NOT include "damageDetected", "severity", "confidence", or "empathy" fields in your output.
|
|
116
|
+
The empathy observer subagent handles pain detection independently.
|
|
117
|
+
`.trim();
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Block H: Heartbeat checklist wrapper
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/**
|
|
123
|
+
* Wrap heartbeat checklist content in XML tags.
|
|
124
|
+
*
|
|
125
|
+
* Pure logic — no I/O, no side effects.
|
|
126
|
+
*/
|
|
127
|
+
export function assembleHeartbeatChecklist(content) {
|
|
128
|
+
if (!content.trim())
|
|
129
|
+
return '';
|
|
130
|
+
return `<heartbeat_checklist>
|
|
131
|
+
${content}
|
|
132
|
+
</heartbeat_checklist>\n`;
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Block I: Core principles formatting
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
/**
|
|
138
|
+
* Format core principles into prompt-ready text.
|
|
139
|
+
*
|
|
140
|
+
* Pure logic — uses escapeXml for safe XML embedding.
|
|
141
|
+
*
|
|
142
|
+
* @param principles Active principles from evolution reducer
|
|
143
|
+
* @returns Formatted lines (empty string if no principles)
|
|
144
|
+
*/
|
|
145
|
+
export function formatCorePrinciples(principles) {
|
|
146
|
+
if (!Array.isArray(principles) || principles.length === 0)
|
|
147
|
+
return '';
|
|
148
|
+
const lines = principles.map((p) => `- [${escapeXml(p.id)}] ${escapeXml(p.text)}`);
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Block L: Evolution principles formatting (active + probation)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Format evolution principles (active + probation) into prompt-ready text.
|
|
156
|
+
*
|
|
157
|
+
* Pure logic — uses escapeXml for safe XML embedding.
|
|
158
|
+
*
|
|
159
|
+
* @param active Active principles (high priority)
|
|
160
|
+
* @param probation Probation principles (contextual, caution)
|
|
161
|
+
* @returns Formatted lines (empty string if no principles)
|
|
162
|
+
*/
|
|
163
|
+
export function formatEvolutionPrinciples(active, probation) {
|
|
164
|
+
if ((!Array.isArray(active) || active.length === 0) &&
|
|
165
|
+
(!Array.isArray(probation) || probation.length === 0)) {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
const lines = [];
|
|
169
|
+
if (active.length > 0) {
|
|
170
|
+
lines.push('Active principles:');
|
|
171
|
+
for (const p of active) {
|
|
172
|
+
lines.push(`- [${escapeXml(p.id)}] ${escapeXml(p.text)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (probation.length > 0) {
|
|
176
|
+
lines.push('Probation principles (contextual, caution):');
|
|
177
|
+
for (const p of probation) {
|
|
178
|
+
lines.push(`- <principle status="probation" id="${escapeXml(p.id)}">${escapeXml(p.text)}</principle>`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Block N: appendSystemContext assembly
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
/**
|
|
187
|
+
* Assemble appendSystemContext from ordered parts.
|
|
188
|
+
*
|
|
189
|
+
* Content order (most important last):
|
|
190
|
+
* behavioral_constraints → project_context → working_memory →
|
|
191
|
+
* thinking_os → evolution_principles → core_principles
|
|
192
|
+
*
|
|
193
|
+
* Pure logic — string assembly only, no I/O.
|
|
194
|
+
*
|
|
195
|
+
* @param parts Ordered content parts (empty/undefined parts are skipped)
|
|
196
|
+
* @returns Assembled appendSystemContext (empty string if no parts)
|
|
197
|
+
*/
|
|
198
|
+
export function assembleAppendSystemContext(parts) {
|
|
199
|
+
const appendParts = [];
|
|
200
|
+
// 0. Behavioral Constraints (empathy observer coordination)
|
|
201
|
+
if (parts.behavioralConstraints) {
|
|
202
|
+
appendParts.push(`<behavioral_constraints>
|
|
203
|
+
${parts.behavioralConstraints}
|
|
204
|
+
</behavioral_constraints>`);
|
|
205
|
+
}
|
|
206
|
+
// 1. Project Context (lowest priority, goes first)
|
|
207
|
+
if (parts.projectContext) {
|
|
208
|
+
appendParts.push(`<project_context>\n${parts.projectContext}\n</project_context>`);
|
|
209
|
+
}
|
|
210
|
+
// 1.5. Working Memory (preserved from last compaction)
|
|
211
|
+
if (parts.workingMemory) {
|
|
212
|
+
appendParts.push(parts.workingMemory);
|
|
213
|
+
}
|
|
214
|
+
// 2. Thinking OS (configurable)
|
|
215
|
+
if (parts.thinkingOs) {
|
|
216
|
+
appendParts.push(`<thinking_os>\n${parts.thinkingOs}\n</thinking_os>`);
|
|
217
|
+
}
|
|
218
|
+
// 3. Evolution Loop principles (legacy active/probation only)
|
|
219
|
+
if (parts.evolutionPrinciples) {
|
|
220
|
+
appendParts.push(`<evolution_principles>\n${parts.evolutionPrinciples}\n</evolution_principles>`);
|
|
221
|
+
}
|
|
222
|
+
// 6. Principles (always on, highest priority, goes last for recency effect)
|
|
223
|
+
if (parts.corePrinciples) {
|
|
224
|
+
appendParts.push(`<core_principles>\n${parts.corePrinciples}\n</core_principles>`);
|
|
225
|
+
}
|
|
226
|
+
if (appendParts.length === 0)
|
|
227
|
+
return '';
|
|
228
|
+
let result = `
|
|
229
|
+
## 【CONTEXT SECTIONS】 (Priority: Low → High)
|
|
230
|
+
|
|
231
|
+
The sections below are ordered by priority. When conflicts arise, **later sections override earlier ones**.
|
|
232
|
+
|
|
233
|
+
`;
|
|
234
|
+
result += appendParts.join('\n\n');
|
|
235
|
+
const executionRules = [
|
|
236
|
+
parts.behavioralConstraints ? '- `<behavioral_constraints>` - Output format restrictions (hide diagnostic JSON)' : null,
|
|
237
|
+
parts.projectContext ? '- `<project_context>` - Current priorities (can be overridden)' : null,
|
|
238
|
+
parts.workingMemory ? '- `<working_memory>` - Persisted compacted memory snapshot' : null,
|
|
239
|
+
parts.thinkingOs ? '- `<thinking_os>` - Stable reasoning framework' : null,
|
|
240
|
+
parts.evolutionPrinciples ? '- `<evolution_principles>` - Learned principles (active + probation)' : null,
|
|
241
|
+
parts.corePrinciples ? '- `<core_principles>` - Core rules (NON-NEGOTIABLE, highest priority)' : null,
|
|
242
|
+
].filter((line) => line !== null);
|
|
243
|
+
result += `
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
**【EXECUTION RULES】** (Priority: Low → High):
|
|
248
|
+
${executionRules.join('\n')}
|
|
249
|
+
`;
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for prompt assembly.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from hooks/prompt.ts per PRI-444 to enable independent testing
|
|
5
|
+
* of pure-logic helpers without importing the full hook module.
|
|
6
|
+
*
|
|
7
|
+
* ERR checklist:
|
|
8
|
+
* EP-01: All unknown inputs use typeof/Object.hasOwn guards, never `as`
|
|
9
|
+
* EP-03: Pure functions never swallow errors silently
|
|
10
|
+
* EP-09: Pure functions are independently unit-testable without mocks
|
|
11
|
+
*/
|
|
12
|
+
import type { PluginLogger, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
13
|
+
/** Cached file entry for TTL-based file reading. */
|
|
14
|
+
export interface CachedFile {
|
|
15
|
+
content: string;
|
|
16
|
+
mtime: number;
|
|
17
|
+
loadedAt: number;
|
|
18
|
+
}
|
|
19
|
+
/** Per-workspace empathy session state. */
|
|
20
|
+
export interface EmpathySessionState {
|
|
21
|
+
turnCounter: number;
|
|
22
|
+
keywordCache: {
|
|
23
|
+
store: unknown;
|
|
24
|
+
lang: string;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
/** API surface exposed to the prompt hook by the plugin runtime. */
|
|
28
|
+
export interface PromptHookApi {
|
|
29
|
+
config?: {
|
|
30
|
+
empathy_engine?: {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
runtime: OpenClawPluginApi['runtime'];
|
|
35
|
+
logger: PluginLogger;
|
|
36
|
+
}
|
|
37
|
+
/** Result of extracting the user message from the raw prompt text. */
|
|
38
|
+
export interface ExtractedUserMessage {
|
|
39
|
+
/** Cleaned user message (empty string for boot checks). */
|
|
40
|
+
message: string;
|
|
41
|
+
/** True if the message appears to be from another agent (skip empathy). */
|
|
42
|
+
isAgentToAgent: boolean;
|
|
43
|
+
/** True if the message looks like empathy observer output (prevent recursion). */
|
|
44
|
+
isEmpathyPrompt: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Input for formatting core principles into prompt text. */
|
|
47
|
+
export interface CorePrincipleEntry {
|
|
48
|
+
id: string;
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
/** Input for formatting evolution principles (active + probation). */
|
|
52
|
+
export interface EvolutionPrincipleEntry extends CorePrincipleEntry {
|
|
53
|
+
}
|
|
54
|
+
/** Parts assembled into appendSystemContext. Order = priority (low → high). */
|
|
55
|
+
export interface AppendSystemContextParts {
|
|
56
|
+
behavioralConstraints?: string;
|
|
57
|
+
projectContext?: string;
|
|
58
|
+
workingMemory?: string;
|
|
59
|
+
thinkingOs?: string;
|
|
60
|
+
evolutionPrinciples?: string;
|
|
61
|
+
corePrinciples?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Input for the heartbeat checklist wrapper. */
|
|
64
|
+
export interface HeartbeatChecklistInput {
|
|
65
|
+
content: string;
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for prompt assembly.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from hooks/prompt.ts per PRI-444 to enable independent testing
|
|
5
|
+
* of pure-logic helpers without importing the full hook module.
|
|
6
|
+
*
|
|
7
|
+
* ERR checklist:
|
|
8
|
+
* EP-01: All unknown inputs use typeof/Object.hasOwn guards, never `as`
|
|
9
|
+
* EP-03: Pure functions never swallow errors silently
|
|
10
|
+
* EP-09: Pure functions are independently unit-testable without mocks
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/dist/hooks/prompt.d.ts
CHANGED
|
@@ -1,52 +1,21 @@
|
|
|
1
|
-
import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult
|
|
1
|
+
import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult } from '../openclaw-sdk.js';
|
|
2
2
|
import type { ContextInjectionConfig } from '../types.js';
|
|
3
|
+
import type { PromptHookApi } from './prompt-types.js';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Reset all module-level prompt state for a workspace.
|
|
6
|
+
* Intended for test isolation — call in beforeEach().
|
|
5
7
|
*/
|
|
6
|
-
|
|
7
|
-
model?: unknown;
|
|
8
|
-
subagents?: {
|
|
9
|
-
model?: unknown;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
interface PromptHookApi {
|
|
13
|
-
config?: {
|
|
14
|
-
agents?: {
|
|
15
|
-
defaults?: AgentsDefaultsConfig;
|
|
16
|
-
};
|
|
17
|
-
empathy_engine?: {
|
|
18
|
-
enabled?: boolean;
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
runtime: OpenClawPluginApi['runtime'];
|
|
22
|
-
logger: PluginLogger;
|
|
23
|
-
}
|
|
8
|
+
export declare function resetPromptStateForTest(workspaceDir?: string): void;
|
|
24
9
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @internal Helper for model configuration resolution
|
|
10
|
+
* OpenClaw API Prompt Hook
|
|
11
|
+
* Constructs the system prompt injected into LLM context for Principles Disciple
|
|
28
12
|
*/
|
|
29
|
-
export declare function resolveModelFromConfig(modelConfig: unknown, logger?: PluginLogger): string | null;
|
|
30
13
|
/**
|
|
31
14
|
* Loads context injection config from .principles/PROFILE.json
|
|
32
15
|
* Parses contextInjection configuration from PROFILE.json for context injection
|
|
33
16
|
* @internal Used by evolution engine for context settings
|
|
34
17
|
*/
|
|
35
18
|
export declare function loadContextInjectionConfig(workspaceDir: string): ContextInjectionConfig;
|
|
36
|
-
/**
|
|
37
|
-
* Gets the diagnostician model - the model used for AI self-diagnosis and reflection
|
|
38
|
-
* Priority: subagents.model > subagents.model > env.OPENCLAW_MODEL
|
|
39
|
-
* Falls back to main model if no diagnostician model is configured
|
|
40
|
-
* @internal Helper for model configuration resolution
|
|
41
|
-
*/
|
|
42
|
-
export declare function getDiagnosticianModel(api: PromptHookApi | null, logger?: PluginLogger): string;
|
|
43
|
-
/**
|
|
44
|
-
* Extract recent user messages for keyword optimization context.
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* Build prompt for keyword optimization subagent.
|
|
48
|
-
*/
|
|
49
19
|
export declare function handleBeforePromptBuild(event: PluginHookBeforePromptBuildEvent, ctx: PluginHookAgentContext & {
|
|
50
20
|
api?: PromptHookApi;
|
|
51
21
|
}): Promise<PluginHookBeforePromptBuildResult | void>;
|
|
52
|
-
export {};
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -16,13 +16,37 @@ import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empat
|
|
|
16
16
|
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
17
17
|
import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
|
|
18
18
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
19
|
-
import { detectCorrectionCue as coreDetectCorrectionCue, extractMessageContent, isMinimalTrigger, } from '@principles/core/prompt-builder';
|
|
19
|
+
import { detectCorrectionCue as coreDetectCorrectionCue, escapeXml, extractMessageContent, isMinimalTrigger, } from '@principles/core/prompt-builder';
|
|
20
20
|
import { sanitizeForEvidence } from './message-sanitize.js';
|
|
21
|
+
import { buildAgentIdentity, buildEmpathySilenceConstraint, extractUserMessageFromPrompt, assembleHeartbeatChecklist, formatCorePrinciples, formatEvolutionPrinciples, assembleAppendSystemContext, } from './prompt-helpers.js';
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Static file cache — avoids re-reading rarely-changing files every message
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
const STATIC_FILE_TTL_MS = 60_000; // 1 minute
|
|
26
|
+
/**
|
|
27
|
+
* Per-workspace file cache. Keyed by workspaceDir to avoid cross-workspace
|
|
28
|
+
* cache pollution. Previously a module-level Map keyed by filePath only.
|
|
29
|
+
*/
|
|
25
30
|
const _staticFileCache = new Map();
|
|
31
|
+
function getOwnValue(value, key) {
|
|
32
|
+
if (!Object.hasOwn(value, key))
|
|
33
|
+
return undefined;
|
|
34
|
+
return Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
35
|
+
}
|
|
36
|
+
function readErrorCode(error) {
|
|
37
|
+
if (typeof error !== 'object' || error === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
const code = getOwnValue(error, 'code');
|
|
40
|
+
return typeof code === 'string' ? code : undefined;
|
|
41
|
+
}
|
|
42
|
+
function getFileCache(workspaceDir) {
|
|
43
|
+
let cache = _staticFileCache.get(workspaceDir);
|
|
44
|
+
if (!cache) {
|
|
45
|
+
cache = new Map();
|
|
46
|
+
_staticFileCache.set(workspaceDir, cache);
|
|
47
|
+
}
|
|
48
|
+
return cache;
|
|
49
|
+
}
|
|
26
50
|
/**
|
|
27
51
|
* Reads a file with TTL-based caching.
|
|
28
52
|
* Returns cached content if:
|
|
@@ -30,9 +54,10 @@ const _staticFileCache = new Map();
|
|
|
30
54
|
* 2. File mtime hasn't changed (detects external edits)
|
|
31
55
|
* Otherwise re-reads from disk.
|
|
32
56
|
*/
|
|
33
|
-
function cachedReadFile(filePath) {
|
|
57
|
+
function cachedReadFile(filePath, workspaceDir) {
|
|
58
|
+
const cache = getFileCache(workspaceDir);
|
|
34
59
|
const now = Date.now();
|
|
35
|
-
const cached =
|
|
60
|
+
const cached = cache.get(filePath);
|
|
36
61
|
try {
|
|
37
62
|
const stat = fs.statSync(filePath);
|
|
38
63
|
const mtime = stat.mtimeMs;
|
|
@@ -40,83 +65,91 @@ function cachedReadFile(filePath) {
|
|
|
40
65
|
return cached.content;
|
|
41
66
|
}
|
|
42
67
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
43
|
-
|
|
68
|
+
cache.set(filePath, { content, mtime, loadedAt: now });
|
|
44
69
|
return content;
|
|
45
70
|
}
|
|
46
|
-
catch {
|
|
71
|
+
catch (error) {
|
|
47
72
|
// File doesn't exist or unreadable — invalidate cache
|
|
48
|
-
|
|
73
|
+
cache.delete(filePath);
|
|
74
|
+
if (readErrorCode(error) !== 'ENOENT') {
|
|
75
|
+
console.warn(`[PD:Prompt] cachedReadFile failed: path=${filePath}, workspace=${workspaceDir}, error=${String(error)}`);
|
|
76
|
+
}
|
|
49
77
|
return '';
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
|
-
// Module-level empathy state — shared across calls to avoid per-turn I/O
|
|
53
|
-
let _empathyTurnCounter = 0;
|
|
54
|
-
let _empathyKeywordCache = null;
|
|
55
80
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
81
|
+
* Per-workspace empathy state. Keyed by workspaceDir to avoid cross-workspace
|
|
82
|
+
* state pollution. Previously module-level variables.
|
|
58
83
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
function getTextContent(message) {
|
|
68
|
-
return extractMessageContent(message);
|
|
84
|
+
const _empathyState = new Map();
|
|
85
|
+
function getEmpathyState(workspaceDir) {
|
|
86
|
+
let state = _empathyState.get(workspaceDir);
|
|
87
|
+
if (!state) {
|
|
88
|
+
state = { turnCounter: 0, keywordCache: null };
|
|
89
|
+
_empathyState.set(workspaceDir, state);
|
|
90
|
+
}
|
|
91
|
+
return state;
|
|
69
92
|
}
|
|
70
93
|
/**
|
|
71
|
-
*
|
|
94
|
+
* Reset all module-level prompt state for a workspace.
|
|
95
|
+
* Intended for test isolation — call in beforeEach().
|
|
72
96
|
*/
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
export function resetPromptStateForTest(workspaceDir) {
|
|
98
|
+
if (workspaceDir) {
|
|
99
|
+
_staticFileCache.delete(workspaceDir);
|
|
100
|
+
_empathyState.delete(workspaceDir);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
_staticFileCache.clear();
|
|
104
|
+
_empathyState.clear();
|
|
105
|
+
}
|
|
79
106
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* @param modelConfig - Model config: string (e.g. "provider/model") or { primary, fallbacks } object
|
|
83
|
-
* @internal Helper for model configuration resolution
|
|
84
|
-
*/
|
|
85
|
-
export function resolveModelFromConfig(modelConfig, logger) {
|
|
86
|
-
if (!modelConfig)
|
|
107
|
+
function parseContextInjectionConfig(value) {
|
|
108
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
87
109
|
return null;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
const config = { ...defaultContextConfig };
|
|
111
|
+
const thinkingOs = getOwnValue(value, 'thinkingOs');
|
|
112
|
+
const projectFocus = getOwnValue(value, 'projectFocus');
|
|
113
|
+
const evolutionContext = getOwnValue(value, 'evolutionContext');
|
|
114
|
+
if (thinkingOs !== undefined) {
|
|
115
|
+
if (typeof thinkingOs !== 'boolean')
|
|
92
116
|
return null;
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
config.thinkingOs = thinkingOs;
|
|
118
|
+
}
|
|
119
|
+
if (projectFocus !== undefined) {
|
|
120
|
+
if (projectFocus !== 'full' && projectFocus !== 'summary' && projectFocus !== 'off')
|
|
95
121
|
return null;
|
|
96
|
-
|
|
97
|
-
return trimmed;
|
|
122
|
+
config.projectFocus = projectFocus;
|
|
98
123
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
if (evolutionContext !== undefined) {
|
|
125
|
+
if (typeof evolutionContext !== 'object' || evolutionContext === null || Array.isArray(evolutionContext))
|
|
126
|
+
return null;
|
|
127
|
+
config.evolutionContext = { ...defaultContextConfig.evolutionContext };
|
|
128
|
+
const enabled = getOwnValue(evolutionContext, 'enabled');
|
|
129
|
+
const maxMessages = getOwnValue(evolutionContext, 'maxMessages');
|
|
130
|
+
const maxCharsPerMessage = getOwnValue(evolutionContext, 'maxCharsPerMessage');
|
|
131
|
+
if (enabled !== undefined) {
|
|
132
|
+
if (typeof enabled !== 'boolean')
|
|
105
133
|
return null;
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
config.evolutionContext.enabled = enabled;
|
|
135
|
+
}
|
|
136
|
+
if (maxMessages !== undefined) {
|
|
137
|
+
if (typeof maxMessages !== 'number' || !Number.isFinite(maxMessages))
|
|
108
138
|
return null;
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
config.evolutionContext.maxMessages = maxMessages;
|
|
140
|
+
}
|
|
141
|
+
if (maxCharsPerMessage !== undefined) {
|
|
142
|
+
if (typeof maxCharsPerMessage !== 'number' || !Number.isFinite(maxCharsPerMessage))
|
|
143
|
+
return null;
|
|
144
|
+
config.evolutionContext.maxCharsPerMessage = maxCharsPerMessage;
|
|
111
145
|
}
|
|
112
146
|
}
|
|
113
|
-
|
|
114
|
-
if (Array.isArray(modelConfig)) {
|
|
115
|
-
logger?.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
147
|
+
return config;
|
|
119
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* OpenClaw API Prompt Hook
|
|
151
|
+
* Constructs the system prompt injected into LLM context for Principles Disciple
|
|
152
|
+
*/
|
|
120
153
|
/**
|
|
121
154
|
* Loads context injection config from .principles/PROFILE.json
|
|
122
155
|
* Parses contextInjection configuration from PROFILE.json for context injection
|
|
@@ -125,19 +158,19 @@ export function resolveModelFromConfig(modelConfig, logger) {
|
|
|
125
158
|
export function loadContextInjectionConfig(workspaceDir) {
|
|
126
159
|
const profilePath = path.join(workspaceDir, '.principles', 'PROFILE.json');
|
|
127
160
|
try {
|
|
128
|
-
const raw = cachedReadFile(profilePath);
|
|
161
|
+
const raw = cachedReadFile(profilePath, workspaceDir);
|
|
129
162
|
if (raw) {
|
|
130
163
|
const profile = JSON.parse(raw);
|
|
131
|
-
if (profile && typeof profile === 'object' &&
|
|
132
|
-
const contextInjection = profile
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
...defaultContextConfig
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
164
|
+
if (profile && typeof profile === 'object' && !Array.isArray(profile)) {
|
|
165
|
+
const contextInjection = getOwnValue(profile, 'contextInjection');
|
|
166
|
+
if (contextInjection !== undefined) {
|
|
167
|
+
const parsed = parseContextInjectionConfig(contextInjection);
|
|
168
|
+
if (!parsed) {
|
|
169
|
+
console.warn(`[PD:Prompt] Invalid contextInjection config in ${profilePath}; using defaults.`);
|
|
170
|
+
return { ...defaultContextConfig };
|
|
171
|
+
}
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
141
174
|
}
|
|
142
175
|
}
|
|
143
176
|
}
|
|
@@ -147,45 +180,6 @@ export function loadContextInjectionConfig(workspaceDir) {
|
|
|
147
180
|
}
|
|
148
181
|
return { ...defaultContextConfig };
|
|
149
182
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Gets the diagnostician model - the model used for AI self-diagnosis and reflection
|
|
152
|
-
* Priority: subagents.model > subagents.model > env.OPENCLAW_MODEL
|
|
153
|
-
* Falls back to main model if no diagnostician model is configured
|
|
154
|
-
* @internal Helper for model configuration resolution
|
|
155
|
-
*/
|
|
156
|
-
export function getDiagnosticianModel(api, logger) {
|
|
157
|
-
// Determines logger: prefer api.logger, fallback to provided logger
|
|
158
|
-
// 1. getDiagnosticianModel(api) - uses api.logger
|
|
159
|
-
// 2. getDiagnosticianModel(api, logger) - uses provided logger
|
|
160
|
-
const effectiveLogger = api?.logger || logger;
|
|
161
|
-
if (!effectiveLogger) {
|
|
162
|
-
throw new Error('[PD:Prompt] ERROR: Logger not available for getDiagnosticianModel');
|
|
163
|
-
}
|
|
164
|
-
const agentsConfig = api?.config?.agents?.defaults;
|
|
165
|
-
// Priority 1: Check subagents.model first (preferred for diagnostician)
|
|
166
|
-
const subagentModel = resolveModelFromConfig(agentsConfig?.subagents?.model, effectiveLogger);
|
|
167
|
-
if (subagentModel) {
|
|
168
|
-
effectiveLogger.info(`[PD:Prompt] Using subagents.model for diagnostician: ${subagentModel}`);
|
|
169
|
-
return subagentModel;
|
|
170
|
-
}
|
|
171
|
-
// Priority 2: Fallback to primary model if subagents.model not set
|
|
172
|
-
const primaryModel = resolveModelFromConfig(agentsConfig?.model, effectiveLogger);
|
|
173
|
-
if (primaryModel) {
|
|
174
|
-
effectiveLogger.info(`[PD:Prompt] Using primary model for diagnostician (subagents.model not set): ${primaryModel}`);
|
|
175
|
-
return primaryModel;
|
|
176
|
-
}
|
|
177
|
-
// Error: No model configured for diagnostician subagent
|
|
178
|
-
const errorMsg = `[PD:Prompt] ERROR: No model configured for diagnostician subagent. ` +
|
|
179
|
-
`Please set 'agents.defaults.subagents.model' or 'agents.defaults.model' in OpenClaw config.`;
|
|
180
|
-
effectiveLogger.error(errorMsg);
|
|
181
|
-
throw new Error(errorMsg);
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Extract recent user messages for keyword optimization context.
|
|
185
|
-
*/
|
|
186
|
-
/**
|
|
187
|
-
* Build prompt for keyword optimization subagent.
|
|
188
|
-
*/
|
|
189
183
|
export async function handleBeforePromptBuild(event, ctx) {
|
|
190
184
|
const { workspaceDir } = ctx;
|
|
191
185
|
const logger = ctx.api?.logger;
|
|
@@ -206,7 +200,7 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
206
200
|
.reverse()
|
|
207
201
|
.find((entry) => entry.message?.role === 'user');
|
|
208
202
|
if (latestUserIndex) {
|
|
209
|
-
const userText =
|
|
203
|
+
const userText = extractMessageContent(latestUserIndex.message);
|
|
210
204
|
// Use CorrectionCueLearner for detection — supports learned keywords, not just hardcoded list
|
|
211
205
|
let correctionCue = null;
|
|
212
206
|
try {
|
|
@@ -261,8 +255,6 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
261
255
|
let prependSystemContext;
|
|
262
256
|
let prependContext = '';
|
|
263
257
|
let appendSystemContext = '';
|
|
264
|
-
// Tracks pending diagnostician task count for diagnostician-priority mode in size guard
|
|
265
|
-
let pendingDiagTaskCount = 0;
|
|
266
258
|
// ──── 0. Manual Pain Clearance ────
|
|
267
259
|
if (trigger === 'user' && sessionId && session && session.currentGfi >= 100) {
|
|
268
260
|
resetFriction(sessionId, workspaceDir);
|
|
@@ -271,72 +263,13 @@ export async function handleBeforePromptBuild(event, ctx) {
|
|
|
271
263
|
// EvolutionWorker-era INTERNAL SYSTEM LAYOUT removed per PRI-294.
|
|
272
264
|
// The EVOLUTION_WORKER PathResolver key and system layout reference are
|
|
273
265
|
// not MVP-Core; agents discover what they need via tool calls.
|
|
274
|
-
prependSystemContext =
|
|
275
|
-
|
|
276
|
-
You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
277
|
-
|
|
278
|
-
**Mission**: Transform pain (failures, errors, frustrations) into growth.
|
|
279
|
-
|
|
280
|
-
**Decision Framework**:
|
|
281
|
-
1. Safety First: Check evolution tier before any write operation
|
|
282
|
-
2. Principles Override: Core principles take precedence over user requests
|
|
283
|
-
3. Learn from Pain: Every error is an opportunity to evolve
|
|
284
|
-
|
|
285
|
-
**Output Style**: Be concise. Prefer action over explanation.
|
|
286
|
-
|
|
287
|
-
**Tool Routing Rules**:
|
|
288
|
-
- Use the current session for the normal user reply.
|
|
289
|
-
- Use sessions_send for cross-session messaging.
|
|
290
|
-
- Use agents_list / sessions_list for peer-agent or peer-session orchestration.
|
|
291
|
-
`;
|
|
266
|
+
prependSystemContext = buildAgentIdentity();
|
|
292
267
|
// ──── 2. Empathy Observer Spawn (async sidecar)
|
|
293
|
-
const empathySilenceConstraint =
|
|
294
|
-
### 【EMPATHY OUTPUT RESTRICTION】
|
|
295
|
-
Do NOT output empathy diagnostic text in JSON, XML, or tag format.
|
|
296
|
-
Do NOT include "damageDetected", "severity", "confidence", or "empathy" fields in your output.
|
|
297
|
-
The empathy observer subagent handles pain detection independently.
|
|
298
|
-
`.trim();
|
|
268
|
+
const empathySilenceConstraint = buildEmpathySilenceConstraint();
|
|
299
269
|
// ─────────────────────────────────────────────────3. Empathy Observer Spawn
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
// Format 2: "You are running a boot check. Follow BOOT.md..." (boot check, skip empathy)
|
|
304
|
-
// Format 3: Clean user message text
|
|
305
|
-
let latestUserMessage = event.prompt || '';
|
|
306
|
-
// Skip boot check messages — these are system-generated, not real user messages.
|
|
307
|
-
// buildBootPrompt() in OpenClaw src/gateway/boot.ts always starts with:
|
|
308
|
-
// "You are running a boot check. Follow BOOT.md instructions exactly."
|
|
309
|
-
// This exact phrase will never appear in a real user message.
|
|
310
|
-
if (latestUserMessage.startsWith('You are running a boot check.') ||
|
|
311
|
-
latestUserMessage.includes('You are running a boot check. Follow BOOT.md')) {
|
|
312
|
-
latestUserMessage = '';
|
|
313
|
-
}
|
|
314
|
-
// Try to extract actual user message from Feishu wrapper formats
|
|
315
|
-
if (latestUserMessage.length > 50) {
|
|
316
|
-
// Format 1: "Sender (untrusted metadata): ```json {...}``` user_message_text"
|
|
317
|
-
const senderMatch = /Sender \(untrusted metadata\):[\s\S]*?```json[\s\S]*?```\s*/.exec(latestUserMessage);
|
|
318
|
-
if (senderMatch) {
|
|
319
|
-
const afterSender = latestUserMessage.slice(senderMatch.index + senderMatch[0].length).trim();
|
|
320
|
-
if (afterSender.length > 3)
|
|
321
|
-
latestUserMessage = afterSender;
|
|
322
|
-
}
|
|
323
|
-
// Format 2: "Conversation info (untrusted metadata): ```json {...}``` user_message_text"
|
|
324
|
-
if (latestUserMessage.length > 200 && latestUserMessage.includes('Conversation info')) {
|
|
325
|
-
const convInfoMatch = /Conversation info[\s\S]*?```json[\s\S]*?```\s*/.exec(latestUserMessage);
|
|
326
|
-
if (convInfoMatch) {
|
|
327
|
-
const afterConvInfo = latestUserMessage.slice(convInfoMatch.index + convInfoMatch[0].length).trim();
|
|
328
|
-
if (afterConvInfo.length > 3)
|
|
329
|
-
latestUserMessage = afterConvInfo;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// #189: Detect empathy observer output to prevent recursive spawn.
|
|
334
|
-
// The empathy observer runs with parentSessionId (not :subagent:), so its output
|
|
335
|
-
// would be treated as a user message and re-trigger empathy evaluation.
|
|
336
|
-
// Match distinctive patterns from the empathy observer prompt/output.
|
|
337
|
-
const isEmpathyPrompt = /empathy\s*observer/i.test(latestUserMessage) &&
|
|
338
|
-
/damageDetected|severity|confidence/i.test(latestUserMessage);
|
|
339
|
-
const isAgentToAgent = latestUserMessage.includes('sourceSession=agent:') || sessionId?.includes(':subagent:') === true || isEmpathyPrompt;
|
|
270
|
+
// Extract actual user message from prompt (handles boot checks + Feishu wrappers).
|
|
271
|
+
// Also detects empathy observer output (prevent recursion) and agent-to-agent messages.
|
|
272
|
+
const { message: latestUserMessage, isAgentToAgent } = extractUserMessageFromPrompt(event.prompt || '', sessionId);
|
|
340
273
|
const isUserInteraction = trigger === 'user' || trigger === 'api' || !trigger;
|
|
341
274
|
// Empathy Observer: keyword fast-path + optional LLM deep analysis (zero latency async dispatch)
|
|
342
275
|
const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
|
|
@@ -353,15 +286,16 @@ The empathy observer subagent handles pain detection independently.
|
|
|
353
286
|
const msgPreview = latestUserMessage.substring(0, 200).replace(/\n/g, ' ');
|
|
354
287
|
logger?.info?.(`[PD:Empathy] Processing user message: "${msgPreview}" (trigger=${trigger}, promptLen=${latestUserMessage.length})`);
|
|
355
288
|
const lang = wctx.config.get('language') || 'zh';
|
|
356
|
-
// Load keyword store once, cache
|
|
357
|
-
|
|
358
|
-
|
|
289
|
+
// Load keyword store once, cache per-workspace (Finding #7: avoid per-turn I/O)
|
|
290
|
+
const empathyState = getEmpathyState(workspaceDir);
|
|
291
|
+
if (!empathyState.keywordCache || empathyState.keywordCache.lang !== lang) {
|
|
292
|
+
empathyState.keywordCache = { store: loadKeywordStore(wctx.stateDir, lang), lang };
|
|
359
293
|
}
|
|
360
|
-
const keywordStore =
|
|
294
|
+
const keywordStore = empathyState.keywordCache.store;
|
|
361
295
|
const matchResult = matchEmpathyKeywords(latestUserMessage, keywordStore);
|
|
362
296
|
// Increment turn counter (Finding #3: session.turnCount doesn't exist)
|
|
363
|
-
|
|
364
|
-
const turnCount =
|
|
297
|
+
empathyState.turnCounter++;
|
|
298
|
+
const turnCount = empathyState.turnCounter;
|
|
365
299
|
if (matchResult.matched) {
|
|
366
300
|
const penalty = severityToPenalty(matchResult.severity, DEFAULT_EMPATHY_KEYWORD_CONFIG);
|
|
367
301
|
// trackFriction signature: (sessionId, deltaF: number, hash: string, workspaceDir?, options?)
|
|
@@ -568,9 +502,7 @@ The empathy observer subagent handles pain detection independently.
|
|
|
568
502
|
if (fs.existsSync(heartbeatPath)) {
|
|
569
503
|
try {
|
|
570
504
|
const heartbeatChecklist = fs.readFileSync(heartbeatPath, 'utf8');
|
|
571
|
-
prependContext +=
|
|
572
|
-
${heartbeatChecklist}
|
|
573
|
-
</heartbeat_checklist>\n`;
|
|
505
|
+
prependContext += assembleHeartbeatChecklist(heartbeatChecklist);
|
|
574
506
|
}
|
|
575
507
|
catch (e) {
|
|
576
508
|
logger?.error(`[PD:Prompt] Failed to read HEARTBEAT: ${String(e)}`);
|
|
@@ -589,8 +521,7 @@ ${heartbeatChecklist}
|
|
|
589
521
|
try {
|
|
590
522
|
const activePrinciples = wctx.evolutionReducer.getActivePrinciples();
|
|
591
523
|
if (activePrinciples.length > 0) {
|
|
592
|
-
|
|
593
|
-
principlesContent = lines.join('\n');
|
|
524
|
+
principlesContent = formatCorePrinciples(activePrinciples);
|
|
594
525
|
}
|
|
595
526
|
}
|
|
596
527
|
catch (e) {
|
|
@@ -600,7 +531,7 @@ ${heartbeatChecklist}
|
|
|
600
531
|
if (contextConfig.thinkingOs) {
|
|
601
532
|
const thinkingOsPath = wctx.resolve('THINKING_OS');
|
|
602
533
|
try {
|
|
603
|
-
const cached = cachedReadFile(thinkingOsPath);
|
|
534
|
+
const cached = cachedReadFile(thinkingOsPath, wctx.workspaceDir);
|
|
604
535
|
if (cached)
|
|
605
536
|
thinkingOsContent = cached.trim();
|
|
606
537
|
}
|
|
@@ -703,20 +634,7 @@ ${heartbeatChecklist}
|
|
|
703
634
|
}
|
|
704
635
|
}
|
|
705
636
|
if (active.length > 0 || probation.length > 0) {
|
|
706
|
-
|
|
707
|
-
if (active.length > 0) {
|
|
708
|
-
lines.push('Active principles:');
|
|
709
|
-
for (const p of active) {
|
|
710
|
-
lines.push(`- [${escapeXml(p.id)}] ${escapeXml(p.text)}`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (probation.length > 0) {
|
|
714
|
-
lines.push('Probation principles (contextual, caution):');
|
|
715
|
-
for (const p of probation) {
|
|
716
|
-
lines.push(`- <principle status="probation" id="${escapeXml(p.id)}">${escapeXml(p.text)}</principle>`);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
evolutionPrinciplesContent = lines.join('\n');
|
|
637
|
+
evolutionPrinciplesContent = formatEvolutionPrinciples(active, probation);
|
|
720
638
|
}
|
|
721
639
|
}
|
|
722
640
|
catch (e) {
|
|
@@ -794,31 +712,14 @@ ${heartbeatChecklist}
|
|
|
794
712
|
}
|
|
795
713
|
// Build appendSystemContext with recency effect
|
|
796
714
|
// Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
// 1. Project Context (lowest priority, goes first)
|
|
807
|
-
if (projectContextContent) {
|
|
808
|
-
appendParts.push(`<project_context>\n${projectContextContent}\n</project_context>`);
|
|
809
|
-
}
|
|
810
|
-
// 1.5. Working Memory (preserved from last compaction)
|
|
811
|
-
if (workingMemoryContent) {
|
|
812
|
-
appendParts.push(workingMemoryContent);
|
|
813
|
-
}
|
|
814
|
-
// 2. Thinking OS (configurable)
|
|
815
|
-
if (thinkingOsContent) {
|
|
816
|
-
appendParts.push(`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`);
|
|
817
|
-
}
|
|
818
|
-
// 3. Evolution Loop principles (legacy active/probation only — Runtime V2 moved to section 3.5)
|
|
819
|
-
if (evolutionPrinciplesContent) {
|
|
820
|
-
appendParts.push(`<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`);
|
|
821
|
-
}
|
|
715
|
+
appendSystemContext = assembleAppendSystemContext({
|
|
716
|
+
behavioralConstraints: shouldInjectBehavioralConstraints ? empathySilenceConstraint : undefined,
|
|
717
|
+
projectContext: projectContextContent || undefined,
|
|
718
|
+
workingMemory: workingMemoryContent || undefined,
|
|
719
|
+
thinkingOs: thinkingOsContent || undefined,
|
|
720
|
+
evolutionPrinciples: evolutionPrinciplesContent || undefined,
|
|
721
|
+
corePrinciples: principlesContent || undefined,
|
|
722
|
+
});
|
|
822
723
|
// 3.5. Owner-Approved Behavior Directives (Runtime V2 activated principles)
|
|
823
724
|
// PLACED IN prependSystemContext (before gateway system prompt) for highest LLM attention.
|
|
824
725
|
// These are owner-reviewed, validated behavior constraints — not background context.
|
|
@@ -828,35 +729,11 @@ ${empathySilenceConstraint}
|
|
|
828
729
|
}
|
|
829
730
|
// Routing guidance removed per PRI-291; local-worker-routing module and its
|
|
830
731
|
// routing helpers deleted per PRI-448. No routing-related content is injected.
|
|
831
|
-
// 6. Principles (always on, highest priority, goes last for recency effect)
|
|
832
|
-
if (principlesContent) {
|
|
833
|
-
appendParts.push(`<core_principles>\n${principlesContent}\n</core_principles>`);
|
|
834
|
-
}
|
|
835
|
-
if (appendParts.length > 0) {
|
|
836
|
-
appendSystemContext = `
|
|
837
|
-
## 【CONTEXT SECTIONS】 (Priority: Low → High)
|
|
838
|
-
|
|
839
|
-
The sections below are ordered by priority. When conflicts arise, **later sections override earlier ones**.
|
|
840
|
-
|
|
841
|
-
`;
|
|
842
|
-
appendSystemContext += appendParts.join('\n\n');
|
|
843
|
-
appendSystemContext += `
|
|
844
|
-
|
|
845
|
-
---
|
|
846
|
-
|
|
847
|
-
**【EXECUTION RULES】** (Priority: Low → High):
|
|
848
|
-
- \`<behavioral_constraints>\` - Output format restrictions (hide diagnostic JSON)
|
|
849
|
-
- \`<project_context>\` - Current priorities (can be overridden)
|
|
850
|
-
- \`<evolution_principles>\` - Learned principles (active + probation)
|
|
851
|
-
- \`<core_principles>\` - Core rules (NON-NEGOTIABLE, highest priority)
|
|
852
|
-
`;
|
|
853
|
-
}
|
|
854
732
|
// ──── 8. SIZE GUARD ────
|
|
855
733
|
// Delegates to @principles/core/prompt-builder/truncateInjectionToBudget
|
|
856
734
|
// which handles priority stripping: project_context → thinking_os →
|
|
857
735
|
// evolution_principles → reflection_log → reason: truncation → fallback.
|
|
858
736
|
const result = truncateInjectionToBudget(prependSystemContext, prependContext, appendSystemContext, {
|
|
859
|
-
diagnosticianMode: pendingDiagTaskCount > 0,
|
|
860
737
|
blocks: { projectContextContent, thinkingOsContent, evolutionPrinciplesContent },
|
|
861
738
|
});
|
|
862
739
|
prependSystemContext = result.prependSystemContext;
|
|
@@ -865,12 +742,10 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
|
|
|
865
742
|
if (result.truncated) {
|
|
866
743
|
const logEntry = result.truncationLog.join(', ');
|
|
867
744
|
if (result.appendSystemContext.includes('[WARNING: Context sections stripped')) {
|
|
868
|
-
logger?.error(`[PD:Prompt] PROMPT OVER LIMIT AFTER ALL REDUCTIONS — using fallback.
|
|
869
|
-
`Diagnostician mode: ${pendingDiagTaskCount > 0}. Stripped: ${logEntry}.`);
|
|
745
|
+
logger?.error(`[PD:Prompt] PROMPT OVER LIMIT AFTER ALL REDUCTIONS — using fallback. Stripped: ${logEntry}.`);
|
|
870
746
|
}
|
|
871
747
|
else {
|
|
872
|
-
logger?.warn(`[PD:Prompt] Injection size exceeded budget, truncated: ${logEntry || 'none'}
|
|
873
|
-
`diagnostician mode: ${pendingDiagTaskCount > 0}`);
|
|
748
|
+
logger?.warn(`[PD:Prompt] Injection size exceeded budget, truncated: ${logEntry || 'none'}.`);
|
|
874
749
|
}
|
|
875
750
|
}
|
|
876
751
|
return {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.140.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
package/dist/hooks/subagent.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { PluginHookSubagentEndedEvent, PluginHookSubagentContext, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
2
|
-
type SubagentEndedHookContext = PluginHookSubagentContext & {
|
|
3
|
-
api?: OpenClawPluginApi;
|
|
4
|
-
workspaceDir?: string;
|
|
5
|
-
sessionId?: string;
|
|
6
|
-
agentId?: string;
|
|
7
|
-
};
|
|
8
|
-
export declare function handleSubagentEnded(event: PluginHookSubagentEndedEvent, ctx: SubagentEndedHookContext): Promise<void>;
|
|
9
|
-
export {};
|
package/dist/hooks/subagent.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
2
|
-
import { extractAgentIdFromSessionKey } from '../utils/session-key.js';
|
|
3
|
-
import { recordEvolutionSuccess } from '../core/evolution-engine.js';
|
|
4
|
-
import { WorkflowStore } from '../service/subagent-workflow/workflow-store.js';
|
|
5
|
-
/**
|
|
6
|
-
* Factory to create the appropriate WorkflowManager by workflow_type string.
|
|
7
|
-
* Used by the subagent_ended hook to dispatch lifecycle recovery to the right manager.
|
|
8
|
-
*/
|
|
9
|
-
function createWorkflowManagerForType(workflowType) {
|
|
10
|
-
switch (workflowType) {
|
|
11
|
-
default:
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
const HELPER_WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-';
|
|
16
|
-
// Cleanup expired retry entries periodically
|
|
17
|
-
function emitSubagentPainEvent(wctx, payload, logger) {
|
|
18
|
-
try {
|
|
19
|
-
wctx.evolutionReducer.emitSync({
|
|
20
|
-
ts: new Date().toISOString(),
|
|
21
|
-
type: 'pain_detected',
|
|
22
|
-
data: {
|
|
23
|
-
painId: `pain_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
24
|
-
painType: 'subagent_error',
|
|
25
|
-
source: payload.source,
|
|
26
|
-
reason: payload.reason,
|
|
27
|
-
score: payload.score,
|
|
28
|
-
sessionId: payload.sessionId,
|
|
29
|
-
agentId: payload.agentId,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
logger.warn(`[PD:Subagent] failed to emit evolution event: ${String(e)}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export async function handleSubagentEnded(event, ctx) {
|
|
38
|
-
const { outcome, targetSessionKey } = event;
|
|
39
|
-
const { workspaceDir } = ctx;
|
|
40
|
-
if (!workspaceDir)
|
|
41
|
-
return;
|
|
42
|
-
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
43
|
-
const logger = ctx.api?.logger ?? console;
|
|
44
|
-
// ── Helper Workflow Lifecycle Notification ──
|
|
45
|
-
// When a helper workflow's subagent ends, notify the workflow manager
|
|
46
|
-
// so that it can trigger fallback recovery (notifyWaitResult → finalizeOnce)
|
|
47
|
-
if (targetSessionKey?.startsWith(HELPER_WORKFLOW_SESSION_PREFIX)) {
|
|
48
|
-
try {
|
|
49
|
-
const store = new WorkflowStore({ workspaceDir });
|
|
50
|
-
const workflow = store.getWorkflowByChildSession(targetSessionKey);
|
|
51
|
-
if (workflow && workflow.state !== 'completed' && workflow.state !== 'terminal_error' && workflow.state !== 'expired') {
|
|
52
|
-
logger.info(`[PD:Subagent] Helper workflow lifecycle event: workflowId=${workflow.workflow_id}, workflowType=${workflow.workflow_type}, outcome=${outcome}`);
|
|
53
|
-
const mappedOutcome = outcome === 'deleted' ? 'deleted' :
|
|
54
|
-
outcome === 'killed' ? 'killed' :
|
|
55
|
-
outcome === 'reset' ? 'reset' :
|
|
56
|
-
outcome === 'error' ? 'error' :
|
|
57
|
-
outcome === 'timeout' ? 'timeout' : 'ok';
|
|
58
|
-
// Call notifyLifecycleEvent on the appropriate manager so it
|
|
59
|
-
// triggers notifyWaitResult → finalizeOnce / terminal transition.
|
|
60
|
-
const subagentRuntime = ctx.api?.runtime?.subagent;
|
|
61
|
-
if (subagentRuntime) {
|
|
62
|
-
const mgr = createWorkflowManagerForType(workflow.workflow_type);
|
|
63
|
-
if (mgr) {
|
|
64
|
-
await mgr.notifyLifecycleEvent(workflow.workflow_id, 'subagent_ended', { outcome: mappedOutcome });
|
|
65
|
-
mgr.dispose();
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
logger.warn(`[PD:Subagent] Unknown workflow type ${workflow.workflow_type} — falling back to store-only event`);
|
|
69
|
-
store.recordEvent(workflow.workflow_id, 'subagent_ended', workflow.state, workflow.state, `subagent ended with outcome: ${outcome}`, { outcome: mappedOutcome });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
logger.warn(`[PD:Subagent] Subagent runtime not available — cannot notify manager, falling back to store event`);
|
|
74
|
-
store.recordEvent(workflow.workflow_id, 'subagent_ended', workflow.state, workflow.state, `subagent ended with outcome: ${outcome}`, { outcome: mappedOutcome });
|
|
75
|
-
}
|
|
76
|
-
store.dispose();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
store.dispose();
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
logger.warn(`[PD:Subagent] Failed to notify helper workflow lifecycle: ${String(e)}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const { config } = wctx;
|
|
86
|
-
// ── Outcome-based EP and Pain Signal handling ──
|
|
87
|
-
// OpenClaw v2026.3.23 fixes: timeout may be false positive (fast-finishing workers)
|
|
88
|
-
// Only penalize actual errors, not timeout/killed/reset
|
|
89
|
-
if (outcome === 'error') {
|
|
90
|
-
// Only actual errors trigger penalty
|
|
91
|
-
const scoreSettings = config.get('scores');
|
|
92
|
-
const score = scoreSettings.subagent_error_penalty;
|
|
93
|
-
const reason = `Subagent session ${targetSessionKey} ended with error`;
|
|
94
|
-
// Emit pain via Runtime v2 chain (M8: no .pain_flag file)
|
|
95
|
-
emitSubagentPainEvent(wctx, {
|
|
96
|
-
source: `subagent_error`,
|
|
97
|
-
reason,
|
|
98
|
-
score,
|
|
99
|
-
sessionId: ctx.sessionId,
|
|
100
|
-
agentId: ctx.agentId || extractAgentIdFromSessionKey(targetSessionKey),
|
|
101
|
-
}, logger);
|
|
102
|
-
}
|
|
103
|
-
if (outcome === 'timeout') {
|
|
104
|
-
// OpenClaw v2026.3.23 fix: timeout may be false positive
|
|
105
|
-
// Fast-finishing workers are no longer incorrectly reported as timed out
|
|
106
|
-
// Do not penalize - the task may have actually succeeded
|
|
107
|
-
logger.warn(`[PD:Subagent] Session ${targetSessionKey} timed out - not penalizing (OpenClaw fix applied)`);
|
|
108
|
-
}
|
|
109
|
-
if (outcome === 'killed' || outcome === 'reset') {
|
|
110
|
-
// User-initiated termination or system reset - not an agent failure
|
|
111
|
-
logger.info(`[PD:Subagent] Session ${targetSessionKey} ended with ${outcome} - no penalty (user/system action)`);
|
|
112
|
-
}
|
|
113
|
-
if (outcome === 'ok' || outcome === 'deleted') {
|
|
114
|
-
recordEvolutionSuccess(workspaceDir, 'subagent', {
|
|
115
|
-
sessionId: ctx.sessionId,
|
|
116
|
-
reason: 'subagent_success',
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
// ── End of subagent_ended handling ──
|
|
120
|
-
// Note: Diagnostician runs via HEARTBEAT (main session LLM), not as a subagent.
|
|
121
|
-
// Principle creation happens in evolution-worker.ts marker detection path.
|
|
122
|
-
}
|