principles-disciple 1.107.0 → 1.108.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.
Files changed (63) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +2 -2
  3. package/src/core/init.ts +3 -1
  4. package/src/core/workspace-dir-validation.ts +3 -3
  5. package/tests/core-anti-growth.test.ts +0 -13
  6. package/tests/hooks/prompt-characterization.test.ts +1 -11
  7. package/tests/hooks/prompt-diet.test.ts +3 -11
  8. package/tests/hooks/prompt-size-guard.test.ts +0 -10
  9. package/tests/hooks/runtime-v2-prompt-activation.test.ts +0 -10
  10. package/tests/index.test.ts +1 -1
  11. package/tests/runtime-v2-discovery-guard.test.ts +1 -2
  12. package/vitest.config.ts +2 -3
  13. package/vitest.unit.config.ts +12 -0
  14. package/src/core/evolution-hook.ts +0 -74
  15. package/src/core/file-storage-adapter.ts +0 -203
  16. package/src/core/merge-gate-audit.ts +0 -314
  17. package/src/core/pain-context-extractor.ts +0 -306
  18. package/src/core/pain-lifecycle.ts +0 -38
  19. package/src/core/pain-signal-adapter.ts +0 -42
  20. package/src/core/pain-signal.ts +0 -22
  21. package/src/core/principle-injector.ts +0 -84
  22. package/src/core/principle-tree-migration.ts +0 -196
  23. package/src/core/storage-adapter.ts +0 -65
  24. package/src/core/telemetry-event.ts +0 -109
  25. package/src/core/training-program.ts +0 -632
  26. package/src/core/workspace-dir-service.ts +0 -119
  27. package/src/hooks/lifecycle-routing.ts +0 -125
  28. package/src/service/event-log-auditor.ts +0 -284
  29. package/src/service/evolution-queue-lock.ts +0 -47
  30. package/src/service/failure-classifier.ts +0 -79
  31. package/src/service/internalization-trigger-adapter.ts +0 -302
  32. package/src/service/monitoring-query-service.ts +0 -67
  33. package/src/service/subagent-workflow/index.ts +0 -17
  34. package/src/tools/critique-prompt.ts +0 -1
  35. package/src/tools/model-index.ts +0 -1
  36. package/src/types/event-payload.ts +0 -16
  37. package/src/utils/glob-match.ts +0 -50
  38. package/src/utils/nlp.ts +0 -25
  39. package/src/utils/plugin-logger.ts +0 -97
  40. package/src/utils/subagent-probe.ts +0 -81
  41. package/tests/core/evolution-hook.test.ts +0 -123
  42. package/tests/core/file-storage-adapter.test.ts +0 -285
  43. package/tests/core/merge-gate-audit.test.ts +0 -117
  44. package/tests/core/pain-context-extractor.test.ts +0 -279
  45. package/tests/core/pain-lifecycle.test.ts +0 -38
  46. package/tests/core/pain-signal-adapter.test.ts +0 -116
  47. package/tests/core/pain-signal.test.ts +0 -190
  48. package/tests/core/principle-injector.test.ts +0 -90
  49. package/tests/core/principle-tree-migration.test.ts +0 -77
  50. package/tests/core/storage-conformance.test.ts +0 -429
  51. package/tests/core/telemetry-event.test.ts +0 -119
  52. package/tests/core/training-program.test.ts +0 -472
  53. package/tests/core/workspace-dir-service.test.ts +0 -68
  54. package/tests/core/workspace-dir-validation.test.ts +0 -143
  55. package/tests/integration/internalization-trigger-guard.test.ts +0 -69
  56. package/tests/integration/pain-lifecycle-e2e.test.ts +0 -75
  57. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +0 -209
  58. package/tests/service/failure-classifier.test.ts +0 -171
  59. package/tests/service/internalization-trigger-adapter.test.ts +0 -251
  60. package/tests/service/monitoring-query-service.test.ts +0 -67
  61. package/tests/utils/nlp.test.ts +0 -35
  62. package/tests/utils/plugin-logger.test.ts +0 -156
  63. package/tests/utils/subagent-probe.test.ts +0 -79
@@ -1,302 +0,0 @@
1
- /**
2
- * Internalization Dumb Trigger Adapter (PRI-63)
3
- *
4
- * Thin adapter that wakes periodically to probe SQLite for ready
5
- * internalization tasks. Plugin layer only — does NOT execute LLM calls,
6
- * does NOT directly chain PeerRunners, does NOT modify task status.
7
- *
8
- * Core decision functions (validateInternalizationTaskReady) determine
9
- * if a task is ready to be processed. This adapter is only responsible
10
- * for waking and logging — actual dispatch is handled by future
11
- * Orchestrator PRI.
12
- *
13
- * @see docs/adr/0003-peer-agent-state-machine-orchestration.md
14
- * @see packages/principles-core/src/runtime-v2/internalization/internalization-state-machine.ts (PRI-62)
15
- */
16
-
17
- import type { TaskRecord } from '@principles/core/runtime-v2';
18
-
19
- import {
20
- validateInternalizationTaskReady,
21
- isPeerRunnerKind,
22
- hydratePITaskRecord,
23
- type PITaskRecord,
24
- type RunnerKind,
25
- } from '@principles/core/runtime-v2';
26
-
27
- // ── Structured log event types ───────────────────────────────────────────────
28
-
29
- type TriggerWakeEvent = {
30
- event: 'INTERNALIZATION_TRIGGER_WAKE';
31
- workspaceDir: string;
32
- taskId: string;
33
- taskKind: RunnerKind;
34
- correlationId?: string;
35
- gateDecision: 'proceed';
36
- timestamp: string;
37
- };
38
-
39
- type TriggerNoopEvent = {
40
- event: 'INTERNALIZATION_TRIGGER_NOOP';
41
- workspaceDir: string;
42
- timestamp: string;
43
- };
44
-
45
- type TriggerBlockedEvent = {
46
- event: 'INTERNALIZATION_TRIGGER_BLOCKED';
47
- workspaceDir: string;
48
- taskId: string;
49
- taskKind: string;
50
- gateDecision: 'blocked' | 'dependency_failed' | 'retry_wait_pending';
51
- blockedBy?: string[];
52
- failedDependencies?: string[];
53
- retryAfter?: string;
54
- timestamp: string;
55
- };
56
-
57
- type TriggerFailedEvent = {
58
- event: 'INTERNALIZATION_TRIGGER_FAILED';
59
- workspaceDir: string;
60
- failureCategory: 'invalid_context' | 'provider_error' | 'unexpected_error';
61
- error?: string;
62
- timestamp: string;
63
- };
64
-
65
- type LogEvent = TriggerWakeEvent | TriggerNoopEvent | TriggerFailedEvent | TriggerBlockedEvent;
66
-
67
- // ── Logger interface ─────────────────────────────────────────────────────────
68
-
69
- export interface TriggerLogger {
70
- debug?(msg: string, meta?: Record<string, unknown>): void;
71
- info?(msg: string, meta?: Record<string, unknown>): void;
72
- warn?(msg: string, meta?: Record<string, unknown>): void;
73
- error?(msg: string, meta?: Record<string, unknown>): void;
74
- }
75
-
76
- // ── Task provider interface (dependency injection for testability) ───────────
77
-
78
- export interface InternalizationTaskProvider {
79
- listTasks(filter?: { status?: string; taskKind?: string }): Promise<TaskRecord[]>;
80
- getTask(taskId: string): Promise<TaskRecord | null>;
81
- }
82
-
83
- // ── Wake context ──────────────────────────────────────────────────────────────
84
-
85
- export interface TriggerContext {
86
- workspaceDir: string;
87
- stateDir: string;
88
- }
89
-
90
- // ── Adapter interface ─────────────────────────────────────────────────────────
91
-
92
- export interface InternalizationTriggerAdapter {
93
- wake(ctx: TriggerContext): Promise<void>;
94
- start(ctx: TriggerContext, intervalMs?: number): () => void;
95
- stop(): void;
96
- }
97
-
98
- // ── Internal state ─────────────────────────────────────────────────────────────
99
-
100
- interface AdapterState {
101
- intervalId: ReturnType<typeof setInterval> | null;
102
- }
103
-
104
- // ── Log helper ────────────────────────────────────────────────────────────────
105
-
106
- function emitLog(
107
- logger: TriggerLogger | undefined,
108
- event: LogEvent,
109
- ): void {
110
- const { event: eventType, ...meta } = event;
111
-
112
- switch (eventType) {
113
- case 'INTERNALIZATION_TRIGGER_WAKE':
114
- logger?.info?.('[PD:InternalizationTrigger] INTERNALIZATION_TRIGGER_WAKE', meta);
115
- break;
116
- case 'INTERNALIZATION_TRIGGER_NOOP':
117
- logger?.debug?.('[PD:InternalizationTrigger] INTERNALIZATION_TRIGGER_NOOP', meta);
118
- break;
119
- case 'INTERNALIZATION_TRIGGER_BLOCKED':
120
- logger?.debug?.('[PD:InternalizationTrigger] INTERNALIZATION_TRIGGER_BLOCKED', meta);
121
- break;
122
- case 'INTERNALIZATION_TRIGGER_FAILED':
123
- logger?.error?.('[PD:InternalizationTrigger] INTERNALIZATION_TRIGGER_FAILED', meta);
124
- break;
125
- }
126
- }
127
-
128
- // ── Core adapter factory ───────────────────────────────────────────────────────
129
-
130
- /**
131
- * Creates a dumb trigger adapter that probes for ready internalization tasks.
132
- *
133
- * Design constraints (enforced by architecture regression guards):
134
- * - PLUGIN_NO_INLINE_EXECUTION: wake() is read-only, does not await long tasks
135
- * - PEER_NO_DIRECT_CHAINING: adapter does not call Dreamer/Philosopher/Scribe
136
- * - TASK_MODEL_REUSE: reuses PITaskRecord / TaskRecord, no second task model
137
- *
138
- * @param provider - Task data access abstraction (SqliteTaskStore in production)
139
- * @param logger - Optional structured logger
140
- */
141
- export function createInternalizationTrigger(
142
- provider: InternalizationTaskProvider,
143
- logger?: TriggerLogger,
144
- ): InternalizationTriggerAdapter {
145
- const state: AdapterState = {
146
- intervalId: null,
147
- };
148
-
149
- // ── wake: single trigger cycle ─────────────────────────────────────────────
150
-
151
- async function wake(ctx: TriggerContext): Promise<void> {
152
- // Fail closed: missing workspaceDir or stateDir
153
- if (!ctx.workspaceDir || !ctx.stateDir) {
154
- emitLog(logger, {
155
- event: 'INTERNALIZATION_TRIGGER_FAILED',
156
- workspaceDir: ctx.workspaceDir ?? '(missing)',
157
- failureCategory: 'invalid_context',
158
- error: 'workspaceDir or stateDir is missing',
159
- timestamp: new Date().toISOString(),
160
- });
161
- return;
162
- }
163
-
164
- try {
165
- // Query pending and retry_wait tasks (non-terminal states that can be leased)
166
- const pendingTasks = await provider.listTasks({ status: 'pending' });
167
- const retryWaitTasks = await provider.listTasks({ status: 'retry_wait' });
168
-
169
- // Filter to only PeerRunner tasks, then hydrate PI metadata from diagnosticJson.
170
- // Tasks without valid PI metadata in diagnosticJson return null and are filtered out.
171
- const pendingPITasks = pendingTasks
172
- .filter(t => isPeerRunnerKind(t.taskKind))
173
- .map(t => hydratePITaskRecord(t))
174
- .filter((t): t is PITaskRecord => t !== null);
175
-
176
- const retryWaitPITasks = retryWaitTasks
177
- .filter(t => isPeerRunnerKind(t.taskKind))
178
- .map(t => hydratePITaskRecord(t))
179
- .filter((t): t is PITaskRecord => t !== null);
180
-
181
- const candidateTasks = [...pendingPITasks, ...retryWaitPITasks];
182
-
183
- // No candidates → NOOP
184
- if (candidateTasks.length === 0) {
185
- emitLog(logger, {
186
- event: 'INTERNALIZATION_TRIGGER_NOOP',
187
- workspaceDir: ctx.workspaceDir,
188
- timestamp: new Date().toISOString(),
189
- });
190
- return;
191
- }
192
-
193
- // Evaluate each candidate
194
- let hasReadyTask = false;
195
-
196
- for (const task of candidateTasks) {
197
- // Fetch dependency tasks for gate validation
198
- const dependencies: TaskRecord[] = [];
199
- if (task.dependencyTaskIds && task.dependencyTaskIds.length > 0) {
200
- for (const depId of task.dependencyTaskIds) {
201
- const dep = await provider.getTask(depId);
202
- if (dep) dependencies.push(dep);
203
- // Fail closed: if dep not found, dependencies array is incomplete
204
- // validateInternalizationTaskReady will return 'blocked'
205
- }
206
- }
207
-
208
- const gateResult = validateInternalizationTaskReady(task, dependencies);
209
-
210
- if (gateResult.decision === 'proceed') {
211
- // Task is ready — log WAKE event (read-only, no mutation)
212
- emitLog(logger, {
213
- event: 'INTERNALIZATION_TRIGGER_WAKE',
214
- workspaceDir: ctx.workspaceDir,
215
- taskId: task.taskId,
216
- taskKind: task.taskKind,
217
- correlationId: task.correlationId,
218
- gateDecision: gateResult.decision,
219
- timestamp: new Date().toISOString(),
220
- });
221
- hasReadyTask = true;
222
- } else {
223
- // blocked, dependency_failed, or retry_wait_pending — debug visibility for operators
224
- emitLog(logger, {
225
- event: 'INTERNALIZATION_TRIGGER_BLOCKED',
226
- workspaceDir: ctx.workspaceDir,
227
- taskId: task.taskId,
228
- taskKind: task.taskKind,
229
- gateDecision: gateResult.decision,
230
- blockedBy: gateResult.decision === 'blocked' ? gateResult.blockedBy : undefined,
231
- failedDependencies: gateResult.decision === 'dependency_failed' ? gateResult.failedDependencies : undefined,
232
- retryAfter: gateResult.decision === 'retry_wait_pending' ? gateResult.retryAfter : undefined,
233
- timestamp: new Date().toISOString(),
234
- });
235
- }
236
- }
237
-
238
- // All candidates were blocked
239
- if (!hasReadyTask) {
240
- emitLog(logger, {
241
- event: 'INTERNALIZATION_TRIGGER_NOOP',
242
- workspaceDir: ctx.workspaceDir,
243
- timestamp: new Date().toISOString(),
244
- });
245
- }
246
- } catch (err) {
247
- emitLog(logger, {
248
- event: 'INTERNALIZATION_TRIGGER_FAILED',
249
- workspaceDir: ctx.workspaceDir,
250
- failureCategory: 'provider_error',
251
- error: err instanceof Error ? err.message : String(err),
252
- timestamp: new Date().toISOString(),
253
- });
254
- }
255
- }
256
-
257
- // ── start: begin periodic wake cycles ─────────────────────────────────────
258
-
259
- function start(ctx: TriggerContext, intervalMs = 5 * 60 * 1000): () => void {
260
- // Prevent re-entrancy: if already running, warn and return existing stop
261
- if (state.intervalId !== null) {
262
- logger?.warn?.('[PD:InternalizationTrigger] start() called while already running — returning existing stop', {
263
- workspaceDir: ctx.workspaceDir,
264
- stateDir: ctx.stateDir,
265
- });
266
- return stop;
267
- }
268
-
269
- // Immediate first wake
270
- wake(ctx).catch(err => {
271
- logger?.error?.('[PD:InternalizationTrigger] start() initial wake failed', {
272
- error: err instanceof Error ? err.message : String(err),
273
- });
274
- });
275
-
276
- // Schedule periodic wake
277
- state.intervalId = setInterval(() => {
278
- wake(ctx).catch(err => {
279
- logger?.error?.('[PD:InternalizationTrigger] periodic wake failed', {
280
- error: err instanceof Error ? err.message : String(err),
281
- });
282
- });
283
- }, intervalMs);
284
-
285
- return stop;
286
- }
287
-
288
- // ── stop: halt periodic wake ──────────────────────────────────────────────
289
-
290
- function stop(): void {
291
- if (state.intervalId !== null) {
292
- clearInterval(state.intervalId);
293
- state.intervalId = null;
294
- }
295
- }
296
-
297
- return { wake, start, stop };
298
- }
299
-
300
- // ── Re-export types for consumers ────────────────────────────────────────────
301
-
302
- export type { PITaskRecord, PeerRunnerKind } from '@principles/core/runtime-v2';
@@ -1,67 +0,0 @@
1
- import { WorkflowStore } from './subagent-workflow/workflow-store.js';
2
-
3
- export class MonitoringQueryService {
4
- private readonly workspaceDir: string;
5
- private readonly store: WorkflowStore;
6
-
7
- constructor(workspaceDir: string) {
8
- this.workspaceDir = workspaceDir;
9
- this.store = new WorkflowStore({ workspaceDir });
10
- }
11
-
12
- dispose(): void {
13
- this.store.dispose();
14
- }
15
-
16
- getWorkflows(filters: { state?: string; type?: string } = {}): WorkflowListResponse {
17
- let workflows = filters.state
18
- ? this.store.listWorkflows(filters.state)
19
- : this.store.listWorkflows();
20
-
21
- if (filters.type) {
22
- workflows = workflows.filter(wf => wf.workflow_type === filters.type);
23
- }
24
-
25
- const now = Date.now();
26
- const workflowsWithStuckDetection = workflows.map(wf => {
27
- const metadata = parseWorkflowMetadata(wf.metadata_json);
28
- const timeoutMs = metadata.timeoutMs ?? 15 * 60 * 1000;
29
-
30
- const isStuck = wf.state === 'active' && (now - wf.created_at) > timeoutMs;
31
- const stuckDuration = isStuck ? now - wf.created_at : null;
32
-
33
- return {
34
- workflowId: wf.workflow_id,
35
- type: wf.workflow_type,
36
- state: isStuck ? 'stuck' : wf.state,
37
- duration: now - wf.created_at,
38
- createdAt: new Date(wf.created_at).toISOString(),
39
- stuckDuration,
40
- };
41
- });
42
-
43
- return { workflows: workflowsWithStuckDetection };
44
- }
45
- }
46
-
47
- function parseWorkflowMetadata(metadataJson: string): { timeoutMs?: number } {
48
- try {
49
- const parsed = JSON.parse(metadataJson) as { timeoutMs?: number };
50
- return parsed && typeof parsed === 'object' ? parsed : {};
51
- } catch {
52
- return {};
53
- }
54
- }
55
-
56
- export interface WorkflowListResponse {
57
- workflows: WorkflowInfo[];
58
- }
59
-
60
- export interface WorkflowInfo {
61
- workflowId: string;
62
- type: string;
63
- state: string;
64
- duration: number;
65
- createdAt: string;
66
- stuckDuration: number | null;
67
- }
@@ -1,17 +0,0 @@
1
- export { isExpectedSubagentError } from './subagent-error-utils.js';
2
-
3
- export { WorkflowStore, type WorkflowStoreOptions } from './workflow-store.js';
4
-
5
- export type {
6
- WorkflowState,
7
- WorkflowTransport,
8
- WorkflowMetadata,
9
- WorkflowResultContext,
10
- WorkflowPersistContext,
11
- WorkflowHandle,
12
- SubagentWorkflowSpec,
13
- WorkflowRow,
14
- WorkflowEventRow,
15
- WorkflowDebugSummary,
16
- } from './types.js';
17
-
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,16 +0,0 @@
1
- export type {
2
- EventLogEntry as DiscriminatedEventLogEntry,
3
- } from '@principles/core/runtime-v2';
4
-
5
- export {
6
- isToolCallEventEntry,
7
- isPainSignalEventEntry,
8
- isRuleMatchEventEntry,
9
- isRulePromotionEventEntry,
10
- isHookExecutionEventEntry,
11
- isGateBlockEventEntry,
12
- isGateBypassEventEntry,
13
- isPlanApprovalEventEntry,
14
- isEvolutionTaskEventEntry,
15
- isEmpathyRollbackEventEntry,
16
- } from '@principles/core/runtime-v2';
@@ -1,50 +0,0 @@
1
- /**
2
- * Glob pattern matching utilities using micromatch.
3
- * Provides lightweight file path pattern matching for whitelist/blacklist operations.
4
- */
5
-
6
- import micromatch from 'micromatch';
7
-
8
- /**
9
- * Checks if a file path matches any of the provided glob patterns.
10
- *
11
- * @param filePath - The file path to check (relative or absolute)
12
- * @param patterns - Array of glob patterns (supports *, **, ?, etc.)
13
- * @returns true if the path matches any pattern, false otherwise
14
- */
15
- export function matchesAnyPattern(filePath: string, patterns: string[]): boolean {
16
- if (!patterns || patterns.length === 0) return false;
17
- return micromatch.isMatch(filePath, patterns);
18
- }
19
-
20
- /**
21
- * Filters an array of file paths to only those that match any pattern.
22
- *
23
- * @param filePaths - Array of file paths to filter
24
- * @param patterns - Array of glob patterns (supports *, **, ?, !)
25
- * @returns Array of matching file paths
26
- */
27
- export function filterMatchingPaths(filePaths: string[], patterns: string[]): string[] {
28
- if (!patterns || patterns.length === 0) return [];
29
- return micromatch.match(filePaths, patterns);
30
- }
31
-
32
- /**
33
- * Returns all patterns that match the given file path.
34
- *
35
- * @param filePath - The file path to check
36
- * @param patterns - Array of glob patterns
37
- * @returns Array of patterns that matched the path
38
- */
39
- export function getMatchingPatterns(filePath: string, patterns: string[]): string[] {
40
- if (!patterns || patterns.length === 0) return [];
41
- return micromatch.match([filePath], patterns).map((match: string) => {
42
- // Find the pattern that produced this match
43
- for (const pattern of patterns) {
44
- if (micromatch.isMatch(match, pattern)) {
45
- return pattern;
46
- }
47
- }
48
- return '';
49
- }).filter(Boolean);
50
- }
package/src/utils/nlp.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Extracts common phrases (N-grams) that appear in a majority of the given samples.
3
- */
4
- export function extractCommonPhrases(samples: string[], minOccurrence = 3): string[] {
5
- if (samples.length < minOccurrence) return [];
6
-
7
- const phrases = new Map<string, number>();
8
- const n = 3; // Look for 3-word n-grams as a baseline for "phrases"
9
-
10
- for (const sample of samples) {
11
- // Keep all words, lowercased, remove basic punctuation
12
- const words = sample.toLowerCase().replace(/[.,!?;:()]/g, '').split(/\s+/).filter(w => w.length > 0);
13
-
14
- for (let i = 0; i <= words.length - n; i++) {
15
- const ngram = words.slice(i, i + n).join(' ');
16
- phrases.set(ngram, (phrases.get(ngram) || 0) + 1);
17
- }
18
- }
19
-
20
- // Filter phrases that meet the threshold
21
- return Array.from(phrases.entries())
22
-
23
- .filter(([_elem, count]) => count >= minOccurrence)
24
- .map(([phrase, _count]) => phrase);
25
- }
@@ -1,97 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
-
4
- export interface PluginLogger {
5
-
6
- info(message: string, meta?: Record<string, unknown>): void;
7
- warn(message: string, meta?: Record<string, unknown>): void;
8
- error(message: string, meta?: Record<string, unknown>): void;
9
- debug(message: string, meta?: Record<string, unknown>): void;
10
-
11
- }
12
-
13
- export interface PluginLoggerConfig {
14
- /** Directory to store log files (typically stateDir) */
15
- logDir: string;
16
- /** Plugin identifier for log file naming */
17
- pluginId: string;
18
- /** Console logger from OpenClaw API (for dual output) */
19
- consoleLogger?: PluginLogger;
20
- /** Whether to also output to console */
21
- echoToConsole?: boolean;
22
- }
23
-
24
- /**
25
- * Creates a plugin-specific logger that writes to its own log file.
26
- *
27
- * Log file location: {logDir}/logs/{pluginId}.log
28
- *
29
- * Example:
30
- * ```
31
- * const logger = createPluginLogger({
32
- * logDir: stateDir, // e.g. ~/.openclaw/
33
- * pluginId: 'principles-disciple',
34
- * consoleLogger: api.logger,
35
- * echoToConsole: true
36
- * });
37
- *
38
- * logger.info('Evolution task completed', { taskId: 'abc123' });
39
- * ```
40
- */
41
- export function createPluginLogger(config: PluginLoggerConfig): PluginLogger {
42
- const { logDir, pluginId, consoleLogger, echoToConsole = true } = config;
43
-
44
- // Ensure logs directory exists
45
- const logsDir = path.join(logDir, 'logs');
46
- if (!fs.existsSync(logsDir)) {
47
- fs.mkdirSync(logsDir, { recursive: true });
48
- }
49
-
50
- const logFile = path.join(logsDir, `${pluginId}.log`);
51
-
52
- const writeLog = (level: string, message: string, meta?: Record<string, unknown>) => {
53
- const timestamp = new Date().toISOString();
54
- const metaStr = meta ? ` ${JSON.stringify(meta)}` : '';
55
- const logLine = `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}\n`;
56
-
57
- try {
58
- fs.appendFileSync(logFile, logLine, 'utf-8');
59
- } catch {
60
- // Silently fail if log write fails
61
- }
62
-
63
- // Also echo to console if enabled
64
- if (echoToConsole && consoleLogger) {
65
- const consoleMessage = `[${pluginId}] ${message}`;
66
- switch (level) {
67
- case 'info':
68
- consoleLogger.info(consoleMessage, meta);
69
- break;
70
- case 'warn':
71
- consoleLogger.warn(consoleMessage, meta);
72
- break;
73
- case 'error':
74
- consoleLogger.error(consoleMessage, meta);
75
- break;
76
- case 'debug':
77
- consoleLogger.debug(consoleMessage, meta);
78
- break;
79
- }
80
- }
81
- };
82
-
83
- return {
84
- info: (message, meta) => writeLog('info', message, meta),
85
- warn: (message, meta) => writeLog('warn', message, meta),
86
- error: (message, meta) => writeLog('error', message, meta),
87
- debug: (message, meta) => writeLog('debug', message, meta),
88
- };
89
- }
90
-
91
- /**
92
- * Log file path for the plugin.
93
- * Returns the path where plugin logs are stored.
94
- */
95
- export function getPluginLogPath(logDir: string, pluginId: string): string {
96
- return path.join(logDir, 'logs', `${pluginId}.log`);
97
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Subagent Runtime Availability Probe
3
- *
4
- * This utility intentionally avoids inferring runtime availability from
5
- * JavaScript implementation details like constructor names. The only contract
6
- * we trust here is whether a callable `run` entrypoint exists.
7
- */
8
-
9
- import type { OpenClawPluginApi } from '../openclaw-sdk.js';
10
-
11
- type SubagentRuntime = NonNullable<OpenClawPluginApi['runtime']>['subagent'];
12
-
13
- /**
14
- * Try to access the global gateway subagent runtime.
15
- * This is a fallback for cases where the plugin was loaded with
16
- * allowGatewaySubagentBinding but the late-binding proxy isn't working.
17
- */
18
- function getGlobalGatewaySubagent(): SubagentRuntime | null {
19
- try {
20
- // Access the global symbol that OpenClaw uses for gateway subagent
21
- const symbol = Symbol.for('openclaw.plugin.gatewaySubagentRuntime');
22
- const globalState = (globalThis as Record<string, unknown>)[symbol as unknown as string] as { subagent?: SubagentRuntime } | null;
23
- return globalState?.subagent ?? null;
24
- } catch {
25
- return null;
26
- }
27
- }
28
-
29
- /**
30
- * Return a small, explicit availability assessment for the subagent runtime.
31
- * This is a shape check only. Actual invocation failures are classified by the
32
- * caller as runtime-unavailable vs downstream task failures.
33
- *
34
- * @param subagent - The subagent runtime object from api.runtime.subagent
35
- * @returns availability status and reason
36
- */
37
- export function getSubagentRuntimeAvailability(
38
- subagent: { run?: unknown } | undefined
39
- ): { available: boolean; reason: 'missing_runtime' | 'missing_run' | 'callable' } {
40
- if (!subagent) return { available: false, reason: 'missing_runtime' };
41
-
42
- try {
43
- const runFn = subagent.run;
44
- if (typeof runFn !== 'function') {
45
- return { available: false, reason: 'missing_run' };
46
- }
47
- return { available: true, reason: 'callable' };
48
- } catch {
49
- return { available: false, reason: 'missing_run' };
50
- }
51
- }
52
-
53
- export function isSubagentRuntimeAvailable(
54
- subagent: { run?: unknown } | undefined
55
- ): boolean {
56
- return getSubagentRuntimeAvailability(subagent).available;
57
- }
58
-
59
- /**
60
- * Get the actual subagent runtime, preferring the global gateway subagent
61
- * if the passed one is not available.
62
- *
63
- * This is useful for cases where the plugin was loaded with allowGatewaySubagentBinding
64
- * but the late-binding proxy isn't resolving correctly.
65
- */
66
- export function getAvailableSubagentRuntime(
67
- subagent: SubagentRuntime | undefined
68
- ): SubagentRuntime | undefined {
69
- // First check if the passed subagent is available
70
- if (isSubagentRuntimeAvailable(subagent)) {
71
- return subagent;
72
- }
73
-
74
- // Fallback to global gateway subagent
75
- const globalGateway = getGlobalGatewaySubagent();
76
- if (globalGateway && isSubagentRuntimeAvailable(globalGateway)) {
77
- return globalGateway;
78
- }
79
-
80
- return undefined;
81
- }