principles-disciple 1.7.3 → 1.7.5
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/commands/evolution-status.js +4 -2
- package/dist/commands/focus.js +30 -155
- package/dist/constants/diagnostician.d.ts +16 -0
- package/dist/constants/diagnostician.js +60 -0
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/config.d.ts +23 -0
- package/dist/core/config.js +26 -1
- package/dist/core/evolution-engine.js +1 -1
- package/dist/core/evolution-logger.d.ts +137 -0
- package/dist/core/evolution-logger.js +256 -0
- package/dist/core/evolution-reducer.d.ts +23 -0
- package/dist/core/evolution-reducer.js +73 -29
- package/dist/core/evolution-types.d.ts +6 -0
- package/dist/core/focus-history.d.ts +145 -0
- package/dist/core/focus-history.js +919 -0
- package/dist/core/init.js +24 -0
- package/dist/core/profile.js +1 -1
- package/dist/core/risk-calculator.d.ts +15 -0
- package/dist/core/risk-calculator.js +48 -0
- package/dist/core/trajectory.d.ts +73 -0
- package/dist/core/trajectory.js +206 -0
- package/dist/hooks/gate.js +130 -20
- package/dist/hooks/lifecycle.js +104 -0
- package/dist/hooks/pain.js +31 -0
- package/dist/hooks/prompt.js +136 -38
- package/dist/hooks/subagent.d.ts +1 -0
- package/dist/hooks/subagent.js +200 -18
- package/dist/http/principles-console-route.d.ts +7 -0
- package/dist/http/principles-console-route.js +301 -1
- package/dist/index.js +0 -2
- package/dist/service/central-database.d.ts +104 -0
- package/dist/service/central-database.js +648 -0
- package/dist/service/control-ui-query-service.d.ts +2 -0
- package/dist/service/control-ui-query-service.js +4 -0
- package/dist/service/empathy-observer-manager.d.ts +8 -0
- package/dist/service/empathy-observer-manager.js +40 -0
- package/dist/service/evolution-query-service.d.ts +155 -0
- package/dist/service/evolution-query-service.js +258 -0
- package/dist/service/evolution-worker.d.ts +4 -0
- package/dist/service/evolution-worker.js +185 -63
- package/dist/service/phase3-input-filter.d.ts +37 -0
- package/dist/service/phase3-input-filter.js +106 -0
- package/dist/service/runtime-summary-service.d.ts +15 -0
- package/dist/service/runtime-summary-service.js +111 -23
- package/dist/tools/deep-reflect.js +8 -2
- package/dist/utils/subagent-probe.d.ts +34 -0
- package/dist/utils/subagent-probe.js +81 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -4
- package/templates/langs/en/core/AGENTS.md +15 -3
- package/templates/langs/en/core/BOOTSTRAP.md +24 -1
- package/templates/langs/en/core/TOOLS.md +9 -0
- package/templates/langs/zh/core/AGENTS.md +15 -3
- package/templates/langs/zh/core/BOOTSTRAP.md +24 -1
- package/templates/langs/zh/core/TOOLS.md +9 -0
- package/templates/langs/zh/skills/pd-auditor/SKILL.md +61 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +287 -0
- package/templates/langs/zh/skills/pd-explorer/SKILL.md +65 -0
- package/templates/langs/zh/skills/pd-implementer/SKILL.md +68 -0
- package/templates/langs/zh/skills/pd-planner/SKILL.md +65 -0
- package/templates/langs/zh/skills/pd-reporter/SKILL.md +78 -0
- package/templates/langs/zh/skills/pd-reviewer/SKILL.md +66 -0
- package/dist/core/agent-loader.d.ts +0 -44
- package/dist/core/agent-loader.js +0 -147
- package/dist/tools/agent-spawn.d.ts +0 -54
- package/dist/tools/agent-spawn.js +0 -445
package/dist/hooks/subagent.js
CHANGED
|
@@ -6,6 +6,8 @@ import { acquireQueueLock } from '../service/evolution-worker.js';
|
|
|
6
6
|
const COMPLETION_RETRY_DELAY_MS = 250;
|
|
7
7
|
const COMPLETION_MAX_RETRIES = 3;
|
|
8
8
|
const COMPLETION_RETRY_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for retry entries
|
|
9
|
+
const TASK_OUTCOME_RETRY_DELAY_MS = 250;
|
|
10
|
+
const TASK_OUTCOME_MAX_RETRIES = 3;
|
|
9
11
|
const DIAGNOSTICIAN_SESSION_PREFIX = 'agent:diagnostician:';
|
|
10
12
|
const completionRetryCounts = new Map();
|
|
11
13
|
// Cleanup expired retry entries periodically
|
|
@@ -17,7 +19,7 @@ function cleanupExpiredRetryEntries() {
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
function emitSubagentPainEvent(wctx, payload) {
|
|
22
|
+
function emitSubagentPainEvent(wctx, payload, logger) {
|
|
21
23
|
try {
|
|
22
24
|
wctx.evolutionReducer.emitSync({
|
|
23
25
|
ts: new Date().toISOString(),
|
|
@@ -29,17 +31,25 @@ function emitSubagentPainEvent(wctx, payload) {
|
|
|
29
31
|
reason: payload.reason,
|
|
30
32
|
score: payload.score,
|
|
31
33
|
sessionId: payload.sessionId,
|
|
34
|
+
agentId: payload.agentId,
|
|
32
35
|
},
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
catch (e) {
|
|
36
|
-
|
|
39
|
+
logger.warn(`[PD:Subagent] failed to emit evolution event: ${String(e)}`);
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
function isDiagnosticianSession(targetSessionKey) {
|
|
40
43
|
return typeof targetSessionKey === 'string' && targetSessionKey.startsWith(DIAGNOSTICIAN_SESSION_PREFIX);
|
|
41
44
|
}
|
|
42
|
-
function
|
|
45
|
+
function extractAgentIdFromSessionKey(sessionKey) {
|
|
46
|
+
// sessionKey format: "agent:{agentId}:{type}:{uuid}" or "agent:{agentId}:{uuid}"
|
|
47
|
+
if (!sessionKey)
|
|
48
|
+
return undefined;
|
|
49
|
+
const match = sessionKey.match(/^agent:([^:]+):/);
|
|
50
|
+
return match ? match[1] : undefined;
|
|
51
|
+
}
|
|
52
|
+
function cleanupPainFlagForTask(wctx, completedTaskId, queue, logger) {
|
|
43
53
|
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
44
54
|
try {
|
|
45
55
|
const painData = fs.readFileSync(painFlagPath, 'utf8');
|
|
@@ -65,7 +75,7 @@ function cleanupPainFlagForTask(wctx, completedTaskId, queue) {
|
|
|
65
75
|
catch (e) {
|
|
66
76
|
if (e.code === 'ENOENT')
|
|
67
77
|
return; // File doesn't exist, nothing to clean up
|
|
68
|
-
|
|
78
|
+
logger.error(`[PD:Subagent] Failed to cleanup pain flag: ${String(e)}`);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
function getCompletionRetryKey(workspaceDir, targetSessionKey) {
|
|
@@ -92,34 +102,67 @@ function scheduleCompletionRetry(event, ctx, attempt) {
|
|
|
92
102
|
});
|
|
93
103
|
}, COMPLETION_RETRY_DELAY_MS);
|
|
94
104
|
}
|
|
105
|
+
function scheduleTaskOutcomeRetry(wctx, payload, attempt, logger) {
|
|
106
|
+
if (attempt > TASK_OUTCOME_MAX_RETRIES) {
|
|
107
|
+
logger.error(`[PD:Subagent] Failed to persist task outcome after ${TASK_OUTCOME_MAX_RETRIES} retries: ${payload.taskId}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
try {
|
|
112
|
+
wctx.trajectory?.recordTaskOutcome?.(payload);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.warn(`[PD:Subagent] Retrying task outcome persistence for ${payload.taskId}: ${String(error)}`);
|
|
116
|
+
scheduleTaskOutcomeRetry(wctx, payload, attempt + 1, logger);
|
|
117
|
+
}
|
|
118
|
+
}, TASK_OUTCOME_RETRY_DELAY_MS);
|
|
119
|
+
}
|
|
95
120
|
export async function handleSubagentEnded(event, ctx) {
|
|
96
121
|
const { outcome, targetSessionKey } = event;
|
|
97
122
|
const workspaceDir = ctx.workspaceDir;
|
|
98
123
|
if (!workspaceDir)
|
|
99
124
|
return;
|
|
100
125
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
126
|
+
const logger = ctx.api?.logger ?? console;
|
|
101
127
|
if (targetSessionKey?.startsWith('empathy_obs:')) {
|
|
102
128
|
await empathyObserverManager.reap(ctx.api, targetSessionKey, workspaceDir);
|
|
103
129
|
return;
|
|
104
130
|
}
|
|
105
131
|
const config = wctx.config;
|
|
106
|
-
|
|
132
|
+
// ── Outcome-based Trust Score and Pain Signal handling ──
|
|
133
|
+
// OpenClaw v2026.3.23 fixes: timeout may be false positive (fast-finishing workers)
|
|
134
|
+
// Only penalize actual errors, not timeout/killed/reset
|
|
135
|
+
if (outcome === 'error') {
|
|
136
|
+
// Only actual errors trigger penalty
|
|
107
137
|
const scoreSettings = config.get('scores');
|
|
108
|
-
const score =
|
|
109
|
-
const reason = `Subagent session ${targetSessionKey} ended with
|
|
138
|
+
const score = scoreSettings.subagent_error_penalty;
|
|
139
|
+
const reason = `Subagent session ${targetSessionKey} ended with error`;
|
|
110
140
|
writePainFlag(workspaceDir, {
|
|
111
|
-
source: `
|
|
141
|
+
source: `subagent_error`,
|
|
112
142
|
score: String(score),
|
|
113
143
|
time: new Date().toISOString(),
|
|
114
144
|
reason,
|
|
115
|
-
is_risky: 'true'
|
|
145
|
+
is_risky: 'true',
|
|
146
|
+
session_id: ctx.sessionId || '',
|
|
147
|
+
agent_id: ctx.agentId || extractAgentIdFromSessionKey(targetSessionKey) || '',
|
|
116
148
|
});
|
|
117
149
|
emitSubagentPainEvent(wctx, {
|
|
118
|
-
source: `
|
|
150
|
+
source: `subagent_error`,
|
|
119
151
|
reason,
|
|
120
152
|
score,
|
|
121
153
|
sessionId: ctx.sessionId,
|
|
122
|
-
|
|
154
|
+
agentId: ctx.agentId || extractAgentIdFromSessionKey(targetSessionKey),
|
|
155
|
+
}, logger);
|
|
156
|
+
}
|
|
157
|
+
if (outcome === 'timeout') {
|
|
158
|
+
// OpenClaw v2026.3.23 fix: timeout may be false positive
|
|
159
|
+
// Fast-finishing workers are no longer incorrectly reported as timed out
|
|
160
|
+
// Do not penalize - the task may have actually succeeded
|
|
161
|
+
logger.warn(`[PD:Subagent] Session ${targetSessionKey} timed out - not penalizing (OpenClaw fix applied)`);
|
|
162
|
+
}
|
|
163
|
+
if (outcome === 'killed' || outcome === 'reset') {
|
|
164
|
+
// User-initiated termination or system reset - not an agent failure
|
|
165
|
+
logger.info(`[PD:Subagent] Session ${targetSessionKey} ended with ${outcome} - no penalty (user/system action)`);
|
|
123
166
|
}
|
|
124
167
|
if (outcome === 'ok' || outcome === 'deleted') {
|
|
125
168
|
wctx.trust.recordSuccess('subagent_success', {
|
|
@@ -138,31 +181,170 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
138
181
|
const attempt = retryEntry?.count || 0;
|
|
139
182
|
let releaseLock = null;
|
|
140
183
|
try {
|
|
141
|
-
releaseLock = await acquireQueueLock(queuePath,
|
|
184
|
+
releaseLock = await acquireQueueLock(queuePath, logger);
|
|
142
185
|
const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
143
186
|
let completedTaskId = null;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
187
|
+
// Improved matching logic: support both direct session key match and HEARTBEAT placeholder match
|
|
188
|
+
// This fixes task_outcomes being empty for HEARTBEAT-triggered diagnostician runs
|
|
189
|
+
const matchedTask = queue.find((task) => {
|
|
190
|
+
if (task?.status !== 'in_progress')
|
|
191
|
+
return false;
|
|
192
|
+
const taskSessionKey = task?.assigned_session_key;
|
|
193
|
+
// 1. Exact match: direct session key assignment
|
|
194
|
+
if (typeof taskSessionKey === 'string' && taskSessionKey === targetSessionKey) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
// 2. HEARTBEAT placeholder match: for diagnostician sessions
|
|
198
|
+
// Tasks started via HEARTBEAT have placeholder like "heartbeat:diagnostician:{taskId}"
|
|
199
|
+
if (isDiagnosticianSession(targetSessionKey)) {
|
|
200
|
+
// Match tasks with HEARTBEAT placeholder
|
|
201
|
+
if (typeof taskSessionKey === 'string' && taskSessionKey.startsWith('heartbeat:diagnostician')) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// Backward compatibility: match tasks with no assigned_session_key (legacy behavior)
|
|
205
|
+
// Only match tasks started within 2 hours to avoid stale task matching
|
|
206
|
+
if (taskSessionKey === undefined || taskSessionKey === null) {
|
|
207
|
+
const taskStartedAt = task?.started_at ? new Date(task.started_at).getTime() : 0;
|
|
208
|
+
const taskAge = taskStartedAt > 0 ? Date.now() - taskStartedAt : Infinity;
|
|
209
|
+
const LEGACY_FALLBACK_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
210
|
+
if (taskAge < LEGACY_FALLBACK_MAX_AGE_MS) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
});
|
|
147
217
|
if (matchedTask) {
|
|
218
|
+
// Enhanced observability: log match type for debugging
|
|
219
|
+
const matchType = matchedTask.assigned_session_key === targetSessionKey
|
|
220
|
+
? 'exact'
|
|
221
|
+
: matchedTask.assigned_session_key?.startsWith('heartbeat:diagnostician')
|
|
222
|
+
? 'heartbeat_placeholder'
|
|
223
|
+
: 'legacy_fallback';
|
|
224
|
+
logger.info(`[PD:Subagent] Matched session ${targetSessionKey} to task ${matchedTask.id} (match_type: ${matchType})`);
|
|
148
225
|
matchedTask.status = 'completed';
|
|
149
226
|
matchedTask.completed_at = new Date().toISOString();
|
|
150
227
|
delete matchedTask.assigned_session_key;
|
|
151
228
|
completedTaskId = matchedTask.id;
|
|
152
229
|
}
|
|
153
230
|
else {
|
|
154
|
-
|
|
231
|
+
logger.warn(`[PD:Subagent] No in-progress evolution task matched subagent session ${targetSessionKey}`);
|
|
155
232
|
}
|
|
233
|
+
let taskOutcomePayload = null;
|
|
156
234
|
if (completedTaskId) {
|
|
157
235
|
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
158
|
-
cleanupPainFlagForTask(wctx, completedTaskId, queue);
|
|
236
|
+
cleanupPainFlagForTask(wctx, completedTaskId, queue, logger);
|
|
237
|
+
taskOutcomePayload = {
|
|
238
|
+
sessionId: targetSessionKey,
|
|
239
|
+
taskId: completedTaskId,
|
|
240
|
+
outcome,
|
|
241
|
+
summary: `Diagnostician session ${targetSessionKey} completed evolution task ${completedTaskId}.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (taskOutcomePayload) {
|
|
245
|
+
try {
|
|
246
|
+
wctx.trajectory?.recordTaskOutcome?.(taskOutcomePayload);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
logger.warn(`[PD:Subagent] Failed to persist task outcome for ${taskOutcomePayload.taskId}: ${String(error)}`);
|
|
250
|
+
scheduleTaskOutcomeRetry(wctx, taskOutcomePayload, 1, logger);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Read diagnostician output and create principle with generalized pattern
|
|
254
|
+
if (completedTaskId && ctx.api?.runtime?.subagent) {
|
|
255
|
+
try {
|
|
256
|
+
const messages = await ctx.api?.runtime?.subagent?.getSessionMessages?.({
|
|
257
|
+
sessionKey: targetSessionKey,
|
|
258
|
+
limit: 50
|
|
259
|
+
});
|
|
260
|
+
const assistantText = extractAssistantText(messages);
|
|
261
|
+
const report = parseDiagnosticianReport(assistantText);
|
|
262
|
+
if (report?.principle) {
|
|
263
|
+
const principleId = wctx.evolutionReducer.createPrincipleFromDiagnosis({
|
|
264
|
+
painId: matchedTask?.id || completedTaskId,
|
|
265
|
+
painType: 'tool_failure', // Default, could be extracted from task
|
|
266
|
+
triggerPattern: report.principle.trigger_pattern,
|
|
267
|
+
action: report.principle.action,
|
|
268
|
+
source: matchedTask?.source || 'diagnostician'
|
|
269
|
+
});
|
|
270
|
+
if (principleId) {
|
|
271
|
+
logger.warn(`[PD:Subagent] Created principle ${principleId} from diagnostician analysis for task ${completedTaskId}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
logger.warn(`[PD:Subagent] Failed to read diagnostician output: ${String(e)}`);
|
|
277
|
+
}
|
|
159
278
|
}
|
|
160
279
|
}
|
|
161
280
|
catch (e) {
|
|
162
|
-
|
|
281
|
+
logger.error(`[PD:Subagent] Failed to update evolution queue: ${String(e)}`);
|
|
163
282
|
scheduleCompletionRetry(event, ctx, attempt);
|
|
164
283
|
}
|
|
165
284
|
finally {
|
|
166
285
|
releaseLock?.();
|
|
167
286
|
}
|
|
168
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Extract text content from assistant messages
|
|
290
|
+
*/
|
|
291
|
+
function extractAssistantText(messages) {
|
|
292
|
+
if (!messages || !Array.isArray(messages))
|
|
293
|
+
return '';
|
|
294
|
+
const texts = [];
|
|
295
|
+
for (const msg of messages) {
|
|
296
|
+
if (msg?.role !== 'assistant')
|
|
297
|
+
continue;
|
|
298
|
+
const content = msg?.content;
|
|
299
|
+
if (Array.isArray(content)) {
|
|
300
|
+
for (const block of content) {
|
|
301
|
+
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
302
|
+
texts.push(block.text);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else if (typeof content === 'string') {
|
|
307
|
+
texts.push(content);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return texts.join('\n');
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse diagnostician JSON report from text
|
|
314
|
+
*/
|
|
315
|
+
function parseDiagnosticianReport(text) {
|
|
316
|
+
// Try to find JSON in markdown code block
|
|
317
|
+
const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/);
|
|
318
|
+
if (jsonMatch) {
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
321
|
+
// Support both direct principle and nested phases.principle_extraction structure
|
|
322
|
+
if (parsed?.principle) {
|
|
323
|
+
return { principle: parsed.principle };
|
|
324
|
+
}
|
|
325
|
+
if (parsed?.phases?.principle_extraction?.principle) {
|
|
326
|
+
return { principle: parsed.phases.principle_extraction.principle };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Fall through to return null
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Try to find raw JSON object
|
|
334
|
+
const objectMatch = text.match(/\{[\s\S]*"principle"[\s\S]*\}/);
|
|
335
|
+
if (objectMatch) {
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(objectMatch[0]);
|
|
338
|
+
if (parsed?.principle) {
|
|
339
|
+
return { principle: parsed.principle };
|
|
340
|
+
}
|
|
341
|
+
if (parsed?.phases?.principle_extraction?.principle) {
|
|
342
|
+
return { principle: parsed.phases.principle_extraction.principle };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Fall through to return null
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, OpenClawPluginHttpRouteParams } from '../openclaw-sdk.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create routes for Principles Console.
|
|
4
|
+
* Returns an array of routes:
|
|
5
|
+
* 1. Static files route (no auth required for HTML/CSS/JS)
|
|
6
|
+
* 2. API route (gateway auth required)
|
|
7
|
+
*/
|
|
8
|
+
export declare function createPrinciplesConsoleRoutes(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams[];
|
|
2
9
|
export declare function createPrinciplesConsoleRoute(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { ControlUiQueryService } from '../service/control-ui-query-service.js';
|
|
4
|
+
import { getEvolutionQueryService } from '../service/evolution-query-service.js';
|
|
5
|
+
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
6
|
+
import { getCentralDatabase } from '../service/central-database.js';
|
|
4
7
|
const ROUTE_PREFIX = '/plugins/principles';
|
|
5
8
|
const API_PREFIX = `${ROUTE_PREFIX}/api`;
|
|
6
9
|
const ASSETS_PREFIX = `${ROUTE_PREFIX}/assets`;
|
|
@@ -80,6 +83,11 @@ function createService(api) {
|
|
|
80
83
|
return new ControlUiQueryService(workspaceDir);
|
|
81
84
|
}
|
|
82
85
|
function handleApiRoute(api, pathname, req, res) {
|
|
86
|
+
// Check authentication for API routes
|
|
87
|
+
if (!validateGatewayAuth(req)) {
|
|
88
|
+
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
83
91
|
const service = createService(api);
|
|
84
92
|
const url = new URL(req.url || pathname, 'http://127.0.0.1');
|
|
85
93
|
const method = (req.method || 'GET').toUpperCase();
|
|
@@ -101,6 +109,153 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
101
109
|
if (pathname === `${API_PREFIX}/overview` && method === 'GET') {
|
|
102
110
|
return done(() => service.getOverview());
|
|
103
111
|
}
|
|
112
|
+
if (pathname === `${API_PREFIX}/central/overview` && method === 'GET') {
|
|
113
|
+
return done(() => {
|
|
114
|
+
const centralDb = getCentralDatabase();
|
|
115
|
+
const stats = centralDb.getOverviewStats();
|
|
116
|
+
const trend = centralDb.getDailyTrend(14);
|
|
117
|
+
const regressions = centralDb.getTopRegressions(5);
|
|
118
|
+
const thinkingStats = centralDb.getThinkingModelStats();
|
|
119
|
+
const workspaces = centralDb.getWorkspaces();
|
|
120
|
+
return {
|
|
121
|
+
workspaceDir: 'central',
|
|
122
|
+
generatedAt: new Date().toISOString(),
|
|
123
|
+
dataFreshness: workspaces.length > 0 ? (workspaces[0].lastSync ?? null) : null,
|
|
124
|
+
dataSource: 'central_aggregated_db',
|
|
125
|
+
runtimeControlPlaneSource: 'all_workspaces',
|
|
126
|
+
summary: {
|
|
127
|
+
repeatErrorRate: stats.totalToolCalls > 0
|
|
128
|
+
? stats.totalFailures / stats.totalToolCalls
|
|
129
|
+
: 0,
|
|
130
|
+
userCorrectionRate: stats.totalToolCalls > 0
|
|
131
|
+
? stats.totalCorrections / stats.totalToolCalls
|
|
132
|
+
: 0,
|
|
133
|
+
pendingSamples: stats.pendingSamples,
|
|
134
|
+
approvedSamples: stats.approvedSamples,
|
|
135
|
+
thinkingCoverageRate: stats.totalToolCalls > 0
|
|
136
|
+
? stats.totalThinkingEvents / stats.totalToolCalls
|
|
137
|
+
: 0,
|
|
138
|
+
painEvents: stats.totalPainEvents,
|
|
139
|
+
principleEventCount: 0,
|
|
140
|
+
gateBlocks: 0,
|
|
141
|
+
taskOutcomes: 0,
|
|
142
|
+
},
|
|
143
|
+
dailyTrend: trend,
|
|
144
|
+
topRegressions: regressions,
|
|
145
|
+
sampleQueue: {
|
|
146
|
+
counters: {
|
|
147
|
+
pending: stats.pendingSamples,
|
|
148
|
+
approved: stats.approvedSamples,
|
|
149
|
+
rejected: stats.rejectedSamples,
|
|
150
|
+
},
|
|
151
|
+
preview: [],
|
|
152
|
+
},
|
|
153
|
+
thinkingSummary: {
|
|
154
|
+
activeModels: thinkingStats.activeModels,
|
|
155
|
+
dormantModels: thinkingStats.totalModels - thinkingStats.activeModels,
|
|
156
|
+
effectiveModels: thinkingStats.models.filter(m => m.coverageRate > 0.1).length,
|
|
157
|
+
coverageRate: stats.totalToolCalls > 0
|
|
158
|
+
? stats.totalThinkingEvents / stats.totalToolCalls
|
|
159
|
+
: 0,
|
|
160
|
+
},
|
|
161
|
+
centralInfo: {
|
|
162
|
+
workspaceCount: stats.workspaceCount,
|
|
163
|
+
enabledWorkspaceCount: stats.enabledWorkspaceCount,
|
|
164
|
+
workspaces: stats.workspaceNames,
|
|
165
|
+
enabledWorkspaces: stats.enabledWorkspaceNames,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (pathname === `${API_PREFIX}/central/sync` && method === 'POST') {
|
|
171
|
+
return done(() => {
|
|
172
|
+
const centralDb = getCentralDatabase();
|
|
173
|
+
const results = centralDb.syncEnabled();
|
|
174
|
+
const summary = {};
|
|
175
|
+
results.forEach((count, name) => {
|
|
176
|
+
summary[name] = count;
|
|
177
|
+
});
|
|
178
|
+
return { synced: summary, timestamp: new Date().toISOString() };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (pathname === `${API_PREFIX}/central/workspaces` && method === 'GET') {
|
|
182
|
+
return done(() => {
|
|
183
|
+
const centralDb = getCentralDatabase();
|
|
184
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
185
|
+
const workspaces = centralDb.getWorkspaces();
|
|
186
|
+
return {
|
|
187
|
+
configs,
|
|
188
|
+
workspaces: workspaces.map(ws => ({
|
|
189
|
+
name: ws.name,
|
|
190
|
+
path: ws.path,
|
|
191
|
+
lastSync: ws.lastSync,
|
|
192
|
+
config: configs.find(c => c.workspaceName === ws.name) ?? null,
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const workspaceConfigMatch = pathname.match(/^\/plugins\/principles\/api\/central\/workspaces\/([^/]+)$/);
|
|
198
|
+
if (workspaceConfigMatch && method === 'GET') {
|
|
199
|
+
return done(() => {
|
|
200
|
+
const centralDb = getCentralDatabase();
|
|
201
|
+
const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
|
|
202
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
203
|
+
const config = configs.find(c => c.workspaceName === workspaceName);
|
|
204
|
+
return config ?? { workspaceName, enabled: true, displayName: workspaceName, syncEnabled: true };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (workspaceConfigMatch && method === 'PATCH') {
|
|
208
|
+
return (async () => {
|
|
209
|
+
try {
|
|
210
|
+
const body = await readJsonBody(req);
|
|
211
|
+
const centralDb = getCentralDatabase();
|
|
212
|
+
const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
|
|
213
|
+
centralDb.updateWorkspaceConfig(workspaceName, {
|
|
214
|
+
enabled: body.enabled,
|
|
215
|
+
displayName: body.displayName,
|
|
216
|
+
syncEnabled: body.syncEnabled,
|
|
217
|
+
});
|
|
218
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
219
|
+
json(res, 200, configs.find(c => c.workspaceName === workspaceName));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof Error && error.message === 'invalid_json') {
|
|
224
|
+
json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
api.logger.warn(`[PD:ControlUI] Workspace config update failed: ${String(error)}`);
|
|
228
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
}
|
|
233
|
+
if (pathname === `${API_PREFIX}/central/workspaces` && method === 'POST') {
|
|
234
|
+
return (async () => {
|
|
235
|
+
try {
|
|
236
|
+
const body = await readJsonBody(req);
|
|
237
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
238
|
+
const workspacePath = typeof body.path === 'string' ? body.path : '';
|
|
239
|
+
if (!name || !workspacePath) {
|
|
240
|
+
json(res, 400, { error: 'bad_request', message: 'name and path are required.' });
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const centralDb = getCentralDatabase();
|
|
244
|
+
centralDb.addCustomWorkspace(name, workspacePath);
|
|
245
|
+
json(res, 201, { success: true, workspace: name });
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (error instanceof Error && error.message === 'invalid_json') {
|
|
250
|
+
json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
api.logger.warn(`[PD:ControlUI] Add workspace failed: ${String(error)}`);
|
|
254
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
}
|
|
104
259
|
if (pathname === `${API_PREFIX}/samples` && method === 'GET') {
|
|
105
260
|
return done(() => service.listSamples({
|
|
106
261
|
status: url.searchParams.get('status') ?? undefined,
|
|
@@ -185,6 +340,62 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
185
340
|
service.dispose();
|
|
186
341
|
}
|
|
187
342
|
}
|
|
343
|
+
// === Evolution API ===
|
|
344
|
+
const evolutionService = () => {
|
|
345
|
+
const workspaceDir = api.resolvePath('.');
|
|
346
|
+
const trajectory = TrajectoryRegistry.get(workspaceDir);
|
|
347
|
+
return getEvolutionQueryService(trajectory);
|
|
348
|
+
};
|
|
349
|
+
if (pathname === `${API_PREFIX}/evolution/tasks` && method === 'GET') {
|
|
350
|
+
return done(() => {
|
|
351
|
+
const evoService = evolutionService();
|
|
352
|
+
return evoService.getTasks({
|
|
353
|
+
status: url.searchParams.get('status') ?? undefined,
|
|
354
|
+
dateFrom: url.searchParams.get('dateFrom') ?? undefined,
|
|
355
|
+
dateTo: url.searchParams.get('dateTo') ?? undefined,
|
|
356
|
+
page: url.searchParams.has('page') ? Number(url.searchParams.get('page')) : undefined,
|
|
357
|
+
pageSize: url.searchParams.has('pageSize') ? Number(url.searchParams.get('pageSize')) : undefined,
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (pathname === `${API_PREFIX}/evolution/events` && method === 'GET') {
|
|
362
|
+
return done(() => {
|
|
363
|
+
const evoService = evolutionService();
|
|
364
|
+
return evoService.getEvents({
|
|
365
|
+
traceId: url.searchParams.get('traceId') ?? undefined,
|
|
366
|
+
stage: url.searchParams.get('stage') ?? undefined,
|
|
367
|
+
limit: url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : undefined,
|
|
368
|
+
offset: url.searchParams.has('offset') ? Number(url.searchParams.get('offset')) : undefined,
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (pathname === `${API_PREFIX}/evolution/stats` && method === 'GET') {
|
|
373
|
+
return done(() => {
|
|
374
|
+
const evoService = evolutionService();
|
|
375
|
+
return evoService.getStats();
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const evolutionTraceMatch = pathname.match(/^\/plugins\/principles\/api\/evolution\/trace\/([^/]+)$/);
|
|
379
|
+
if (evolutionTraceMatch && method === 'GET') {
|
|
380
|
+
const evoService = evolutionService();
|
|
381
|
+
try {
|
|
382
|
+
const trace = evoService.getTrace(decodeURIComponent(evolutionTraceMatch[1]));
|
|
383
|
+
if (!trace) {
|
|
384
|
+
json(res, 404, { error: 'not_found', message: 'Evolution trace not found.' });
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
json(res, 200, trace);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
api.logger.warn(`[PD:ControlUI] Evolution trace request failed for ${pathname}: ${String(error)}`);
|
|
392
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
evoService.dispose();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
188
399
|
if (pathname === `${API_PREFIX}/export/corrections` && method === 'GET') {
|
|
189
400
|
try {
|
|
190
401
|
const mode = url.searchParams.get('mode') === 'redacted' ? 'redacted' : 'raw';
|
|
@@ -217,10 +428,93 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
217
428
|
json(res, 404, { error: 'not_found', message: 'Unknown Principles Console API route.' });
|
|
218
429
|
return true;
|
|
219
430
|
}
|
|
431
|
+
function getGatewayToken() {
|
|
432
|
+
try {
|
|
433
|
+
const configPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw.json');
|
|
434
|
+
if (!fs.existsSync(configPath))
|
|
435
|
+
return null;
|
|
436
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
437
|
+
return config?.gateway?.auth?.token || null;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function validateGatewayAuth(req) {
|
|
444
|
+
const gatewayToken = getGatewayToken();
|
|
445
|
+
if (!gatewayToken) {
|
|
446
|
+
// No token configured, allow all requests
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
const authHeader = req.headers?.['authorization'] || '';
|
|
450
|
+
const tokenMatch = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
451
|
+
const providedToken = tokenMatch?.[1];
|
|
452
|
+
return providedToken === gatewayToken;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Create routes for Principles Console.
|
|
456
|
+
* Returns an array of routes:
|
|
457
|
+
* 1. Static files route (no auth required for HTML/CSS/JS)
|
|
458
|
+
* 2. API route (gateway auth required)
|
|
459
|
+
*/
|
|
460
|
+
export function createPrinciplesConsoleRoutes(api) {
|
|
461
|
+
// Route 1: Static files (HTML, CSS, JS) - no auth check
|
|
462
|
+
const staticRoute = {
|
|
463
|
+
path: ROUTE_PREFIX,
|
|
464
|
+
auth: 'plugin',
|
|
465
|
+
match: 'prefix',
|
|
466
|
+
async handler(req, res) {
|
|
467
|
+
const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
|
|
468
|
+
const pathname = url.pathname;
|
|
469
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
470
|
+
// Skip API routes - they'll be handled by the API route
|
|
471
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
472
|
+
return false; // Let the API route handle this
|
|
473
|
+
}
|
|
474
|
+
// Serve assets
|
|
475
|
+
if (pathname.startsWith(ASSETS_PREFIX)) {
|
|
476
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
477
|
+
text(res, 405, 'Method Not Allowed');
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
const assetPath = safeStaticPath(api.rootDir, pathname);
|
|
481
|
+
if (!assetPath || !serveFile(res, assetPath)) {
|
|
482
|
+
text(res, 404, 'Asset Not Found');
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
// Serve index.html for the main route
|
|
487
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
488
|
+
text(res, 405, 'Method Not Allowed');
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
const indexPath = path.join(api.rootDir, 'dist', 'web', 'index.html');
|
|
492
|
+
if (!serveFile(res, indexPath)) {
|
|
493
|
+
text(res, 503, 'Principles Console UI is not built yet.');
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
// Route 2: API endpoints - gateway auth required
|
|
499
|
+
const apiRoute = {
|
|
500
|
+
path: API_PREFIX,
|
|
501
|
+
auth: 'gateway',
|
|
502
|
+
match: 'prefix',
|
|
503
|
+
async handler(req, res) {
|
|
504
|
+
const url = new URL(req.url || API_PREFIX, 'http://127.0.0.1');
|
|
505
|
+
const pathname = url.pathname;
|
|
506
|
+
return handleApiRoute(api, pathname, req, res);
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
return [staticRoute, apiRoute];
|
|
510
|
+
}
|
|
511
|
+
// Legacy export for backwards compatibility
|
|
220
512
|
export function createPrinciplesConsoleRoute(api) {
|
|
513
|
+
const routes = createPrinciplesConsoleRoutes(api);
|
|
514
|
+
// Return the combined behavior - this will be called from index.ts
|
|
221
515
|
return {
|
|
222
516
|
path: ROUTE_PREFIX,
|
|
223
|
-
auth: '
|
|
517
|
+
auth: 'plugin',
|
|
224
518
|
match: 'prefix',
|
|
225
519
|
async handler(req, res) {
|
|
226
520
|
const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
|
|
@@ -229,9 +523,15 @@ export function createPrinciplesConsoleRoute(api) {
|
|
|
229
523
|
if (!pathname.startsWith(ROUTE_PREFIX)) {
|
|
230
524
|
return false;
|
|
231
525
|
}
|
|
526
|
+
// For API routes, check auth manually
|
|
232
527
|
if (pathname.startsWith(API_PREFIX)) {
|
|
528
|
+
if (!validateGatewayAuth(req)) {
|
|
529
|
+
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
233
532
|
return handleApiRoute(api, pathname, req, res);
|
|
234
533
|
}
|
|
534
|
+
// Static files - no auth required
|
|
235
535
|
if (pathname.startsWith(ASSETS_PREFIX)) {
|
|
236
536
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
237
537
|
text(res, 405, 'Method Not Allowed');
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,6 @@ import { ensureWorkspaceTemplates } from './core/init.js';
|
|
|
26
26
|
import { migrateDirectoryStructure } from './core/migration.js';
|
|
27
27
|
import { SystemLogger } from './core/system-logger.js';
|
|
28
28
|
import { createDeepReflectTool } from './tools/deep-reflect.js';
|
|
29
|
-
import { createAgentSpawnTool } from './tools/agent-spawn.js';
|
|
30
29
|
import { PathResolver } from './core/path-resolver.js';
|
|
31
30
|
import { createPrinciplesConsoleRoute } from './http/principles-console-route.js';
|
|
32
31
|
// Track initialization to avoid repeated calls
|
|
@@ -476,7 +475,6 @@ const plugin = {
|
|
|
476
475
|
}
|
|
477
476
|
});
|
|
478
477
|
api.registerTool(createDeepReflectTool(api));
|
|
479
|
-
api.registerTool(createAgentSpawnTool(api));
|
|
480
478
|
}
|
|
481
479
|
};
|
|
482
480
|
export default plugin;
|