/** * ExperienceMinerRole - Pattern extraction worker * Analyzes completed tasks for reusable patterns. * DD ยง8.4. * * @module packages/workers/src/roles/ExperienceMinerRole */ import { WorkerRuntime } from '../WorkerRuntime.js' const MEMORY_TYPES = new Set(['project_rule', 'toolchain_rule', 'skill_update', 'debug_experience']) export interface ExperienceMinerResult { status: 'completed' | 'no_patterns' | 'blocked' entries: Array<{ category: string pattern: string source_task_id: string description: string }> summary: string } export class ExperienceMinerRole { private runtime: WorkerRuntime constructor(runtime: WorkerRuntime) { this.runtime = runtime } async run(mine_spec: { task_ids?: string[]; focus_categories?: string[]; source_refs?: Array>; evidence_refs?: string[] }): Promise { const result: ExperienceMinerResult = { status: 'no_patterns', entries: [], summary: '' } try { const task_ids = mine_spec.task_ids ?? [] const evidence_refs = mine_spec.evidence_refs ?? task_ids.map((task_id) => `task:${task_id}`) const focus = mine_spec.focus_categories?.length ? mine_spec.focus_categories : ['project_rule', 'toolchain_rule', 'debug_experience'] if (task_ids.length === 0 && evidence_refs.length === 0) { result.summary = 'No task or evidence refs available for memory mining' return result } const messages = [ { role: 'system', content: 'Extract durable learning candidates only when supported by evidence. Output one candidate per line as memory_type: concise summary. Valid memory_type values: project_rule, toolchain_rule, skill_update, debug_experience. Do not promote or archive memories.' }, { role: 'user', content: `Evidence refs:\n${evidence_refs.join('\n')}\n\nTask ids: ${task_ids.join(', ') || '(none)'}\nFocus categories: ${focus.join(', ')}` } ] try { const analysis = await this.runtime.call_llm({ messages, max_tokens: 2048, temperature: 0.2 }) for (const line of (analysis.content || '').split('\n')) { const colon_idx = line.indexOf(':') if (colon_idx <= 0) continue const category = line.slice(0, colon_idx).trim().toLowerCase() const memory_type = MEMORY_TYPES.has(category) ? category : 'project_rule' const pattern = line.slice(colon_idx + 1).trim() if (pattern.length < 8) continue result.entries.push({ category: memory_type, pattern, source_task_id: task_ids[0] || '', description: pattern }) } } catch { result.entries.push({ category: 'project_rule', pattern: `Review evidence before promoting memory from ${evidence_refs[0] || task_ids[0]}`, source_task_id: task_ids[0] || '', description: 'LLM unavailable; created a conservative candidate that requires human/runtime review before promotion.', }) } if (result.entries.length === 0 && evidence_refs.length > 0) { const category = focus.find((item) => MEMORY_TYPES.has(item)) || 'project_rule' result.entries.push({ category, pattern: `Review evidence before promoting memory from ${evidence_refs[0]}`, source_task_id: task_ids[0] || '', description: 'Created a conservative candidate because no structured LLM-supported pattern was returned.', }) } for (const entry of result.entries) { const candidate_id = `mem_${crypto.randomUUID()}` this.runtime.emit('memory.candidate.created', { event_id: `evt_${candidate_id}`, candidate_id, source_ref: { entity_type: entry.source_task_id ? 'task' : 'evidence', entity_id: entry.source_task_id || evidence_refs[0] || '', }, memory_type: entry.category, summary: entry.pattern, evidence_refs, }) } if (result.entries.length === 0) { result.summary = `No supported memory candidates extracted from ${task_ids.length} tasks` } else { result.status = 'completed' result.summary = `Created ${result.entries.length} memory candidates from ${task_ids.length} tasks` } this.runtime.checkpoint('experience_mining_completed', { candidates: result.entries.length }) return result } catch (error) { result.status = 'blocked' result.summary = error instanceof Error ? error.message : String(error) return result } } }