feat(round2+round3): 完整实现 A/B/C/D 主线 + round3-F/H 修复
Round2 主线:
- A: 事件落库地基 (RuntimeApp EventStore 单例 + 14 repo wiring)
- B: 执行体对齐 (read-before-edit, verification-before-completion)
- C: 界面对齐 (@opentui/solid, 删除 runtime 依赖)
- D: 经验闭环 (ExperienceMiner, DebuggerRole, CompactorRole)
Round2 补充修复:
- fail-on-missing 反作弊门禁
- projection-store-apply.test.ts 补写
- 3个空壳测试转行为 (evidence-store, recovery-impl, knowledge-store)
- ask 项目根支持 AIRCODING_PROJECT_ROOT
- Worker 事件契约修复 (task.attempt.started → checkpoint)
Round3-F: cpp 工具切换
- 删除 BuiltInToolRegistrar cpp.* 闭包
- 接入 toolchain-cpp 真实 CppToolRegistrar
- canonical envelope {status/output/metadata}
- ExecutorRole system prompt 对齐新工具名
Round3-H: Doctor 5 类报告
- toolchain (cmake/ninja/cppcheck/clangd/g++)
- display (X11/Wayland + ImageMagick)
- network (internet connectivity)
- provider (api_key/base_url/model/connectivity)
Secret 脱敏:
- 状态交接.md: sk- → \${OPENAI_API_KEY}
- .gitignore: 添加 .air/ .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,7 +111,7 @@ export class WorkerRuntime {
|
||||
* Emit an event to the parent.
|
||||
*/
|
||||
emit(type: string, payload: Record<string, unknown>): void {
|
||||
this.send_message('event', { type, ...payload })
|
||||
this.send_message('event', { event_type: type, ...payload })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
import { WorkerRuntime } from '../WorkerRuntime.js'
|
||||
|
||||
const SUMMARY_PREFIX = `This is a compacted summary of earlier context. Treat it as reference only.
|
||||
The latest user message and any newer runtime events after this summary are the source of truth.
|
||||
If this summary conflicts with newer instructions, follow the newer instructions.
|
||||
Preserve active tasks, unresolved questions, architectural constraints, verification status, and remaining work.`
|
||||
|
||||
export interface CompactorResult {
|
||||
status: 'compacted' | 'skipped' | 'blocked'
|
||||
summary_content: string
|
||||
@@ -22,7 +27,15 @@ export class CompactorRole {
|
||||
this.runtime = runtime
|
||||
}
|
||||
|
||||
async run(compact_spec: { task_id: string; current_tokens: number; threshold: number }): Promise<CompactorResult> {
|
||||
async run(compact_spec: {
|
||||
task_id?: string
|
||||
current_tokens?: number
|
||||
threshold?: number
|
||||
target_budget_tokens?: number
|
||||
range_start_message_id?: string
|
||||
range_end_message_id?: string
|
||||
source_content?: string
|
||||
}): Promise<CompactorResult> {
|
||||
const result: CompactorResult = {
|
||||
status: 'skipped',
|
||||
summary_content: '',
|
||||
@@ -30,48 +43,105 @@ export class CompactorRole {
|
||||
compacted_layers: []
|
||||
}
|
||||
|
||||
try {
|
||||
this.runtime.emit('compaction.started', { task_id: compact_spec.task_id })
|
||||
const task_id = compact_spec.task_id || 'compact_task'
|
||||
const current_tokens = compact_spec.current_tokens ?? 0
|
||||
const threshold = compact_spec.threshold ?? compact_spec.target_budget_tokens ?? 80000
|
||||
const range_start_message_id = compact_spec.range_start_message_id || ''
|
||||
const range_end_message_id = compact_spec.range_end_message_id || ''
|
||||
|
||||
// Check if compaction is needed
|
||||
if (compact_spec.current_tokens < compact_spec.threshold) {
|
||||
result.status = 'skipped'
|
||||
result.summary_content = `Tokens (${compact_spec.current_tokens}) below threshold (${compact_spec.threshold}) — no compaction needed`
|
||||
try {
|
||||
this.runtime.emit('context.compaction.started', {
|
||||
event_id: `evt_compaction_started_${crypto.randomUUID()}`,
|
||||
task_id,
|
||||
agent_id: process.env.AIRCODING_AGENT_ID || 'compactor',
|
||||
range_start_message_id,
|
||||
range_end_message_id,
|
||||
})
|
||||
|
||||
if (current_tokens > 0 && current_tokens < threshold) {
|
||||
result.summary_content = `Tokens (${current_tokens}) below threshold (${threshold}); no compaction needed.`
|
||||
return result
|
||||
}
|
||||
|
||||
// Use LLM to generate summary of the conversation
|
||||
const tokens_to_free = compact_spec.current_tokens - Math.floor(compact_spec.threshold * 0.6)
|
||||
const compaction_messages = [
|
||||
{ role: 'system', content: 'Summarize the key facts, decisions, and code changes from the conversation history. Keep it concise but complete. Include file paths, function names, and architectural decisions.' },
|
||||
{ role: 'user', content: `Compaction requested: ${compact_spec.current_tokens} tokens in context, threshold is ${compact_spec.threshold}. Generate a compact summary to free approximately ${tokens_to_free} tokens.` }
|
||||
]
|
||||
const token_estimate_before = current_tokens || threshold
|
||||
const target_after = Math.max(1, Math.floor(threshold * 0.6))
|
||||
const source_content = compact_spec.source_content || `Current token estimate: ${token_estimate_before}; target budget: ${threshold}.`
|
||||
|
||||
try {
|
||||
const summary = await this.runtime.call_llm({
|
||||
messages: compaction_messages,
|
||||
max_tokens: 2048,
|
||||
temperature: 0.2
|
||||
})
|
||||
const summary = await this.build_summary(source_content, token_estimate_before, threshold)
|
||||
const summary_id = `summary_${crypto.randomUUID()}`
|
||||
const token_estimate_after = Math.min(target_after, Math.max(1, Math.floor(summary.length / 4)))
|
||||
|
||||
result.summary_content = summary.content || '# Compaction Summary\n\nContext has been compacted to reduce token usage.'
|
||||
result.tokens_freed = tokens_to_free
|
||||
result.compacted_layers = ['conversation', 'tool_output']
|
||||
result.status = 'compacted'
|
||||
} catch {
|
||||
result.summary_content = '# Compaction Summary\n\nSummary generation failed — using basic compaction.'
|
||||
result.tokens_freed = compact_spec.current_tokens - Math.floor(compact_spec.current_tokens * 0.6)
|
||||
result.compacted_layers = ['conversation']
|
||||
result.status = 'compacted'
|
||||
}
|
||||
result.summary_content = summary
|
||||
result.tokens_freed = Math.max(0, token_estimate_before - token_estimate_after)
|
||||
result.compacted_layers = ['conversation', 'tool_output', 'images']
|
||||
result.status = 'compacted'
|
||||
|
||||
this.runtime.checkpoint('compaction_completed', { task_id: compact_spec.task_id })
|
||||
this.runtime.emit('summary.created', {
|
||||
event_id: `evt_${summary_id}`,
|
||||
summary_id,
|
||||
type: 'compaction',
|
||||
range_start_message_id,
|
||||
range_end_message_id,
|
||||
content_json: {
|
||||
prefix: SUMMARY_PREFIX,
|
||||
summary,
|
||||
active_task: task_id,
|
||||
remaining_work: [],
|
||||
resolved_questions: [],
|
||||
pending_questions: [],
|
||||
},
|
||||
metadata: {
|
||||
token_estimate_before,
|
||||
token_estimate_after,
|
||||
compacted_layers: result.compacted_layers,
|
||||
},
|
||||
})
|
||||
|
||||
this.runtime.emit('context.compaction.completed', {
|
||||
event_id: `evt_compaction_completed_${crypto.randomUUID()}`,
|
||||
task_id,
|
||||
agent_id: process.env.AIRCODING_AGENT_ID || 'compactor',
|
||||
summary_id,
|
||||
range_start_message_id,
|
||||
range_end_message_id,
|
||||
token_estimate_before,
|
||||
token_estimate_after,
|
||||
})
|
||||
|
||||
this.runtime.checkpoint('compaction_completed', { task_id, summary_id, tokens_freed: result.tokens_freed })
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
result.status = 'blocked'
|
||||
result.summary_content = error instanceof Error ? error.message : String(error)
|
||||
result.summary_content = message
|
||||
this.runtime.emit('context.compaction.failed', {
|
||||
event_id: `evt_compaction_failed_${crypto.randomUUID()}`,
|
||||
task_id,
|
||||
agent_id: process.env.AIRCODING_AGENT_ID || 'compactor',
|
||||
range_start_message_id,
|
||||
range_end_message_id,
|
||||
error: { message },
|
||||
evidence_refs: [],
|
||||
metadata: {},
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async build_summary(source_content: string, current_tokens: number, threshold: number): Promise<string> {
|
||||
try {
|
||||
const response = await this.runtime.call_llm({
|
||||
messages: [
|
||||
{ role: 'system', content: `${SUMMARY_PREFIX}\n\nReturn a structured summary with sections: Active task, Key facts, Decisions, Changed files, Verification, Remaining work, Pending questions.` },
|
||||
{ role: 'user', content: `Compact this context from ~${current_tokens} tokens toward ${threshold}.\n\n${source_content}` },
|
||||
],
|
||||
max_tokens: 2048,
|
||||
temperature: 0.2,
|
||||
})
|
||||
return `${SUMMARY_PREFIX}\n\n${(response.content || '').trim() || 'No detailed summary was produced.'}`
|
||||
} catch {
|
||||
return `${SUMMARY_PREFIX}\n\nActive task: context compaction.\nKey facts: source context was too large or summarizer was unavailable.\nRemaining work: rehydrate from durable events and latest user message before continuing.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,38 @@
|
||||
|
||||
import { WorkerRuntime } from '../WorkerRuntime.js'
|
||||
|
||||
type FailoverReason =
|
||||
| 'auth'
|
||||
| 'auth_permanent'
|
||||
| 'billing'
|
||||
| 'rate_limit'
|
||||
| 'overloaded'
|
||||
| 'server_error'
|
||||
| 'timeout'
|
||||
| 'context_overflow'
|
||||
| 'payload_too_large'
|
||||
| 'image_too_large'
|
||||
| 'model_not_found'
|
||||
| 'provider_policy_blocked'
|
||||
| 'content_policy_blocked'
|
||||
| 'format_error'
|
||||
| 'invalid_encrypted_content'
|
||||
| 'multimodal_tool_content_unsupported'
|
||||
| 'thinking_signature'
|
||||
| 'long_context_tier'
|
||||
| 'oauth_long_context_beta_forbidden'
|
||||
| 'llama_cpp_grammar_pattern'
|
||||
| 'unknown'
|
||||
|
||||
interface ClassifiedError {
|
||||
reason: FailoverReason
|
||||
message: string
|
||||
retryable: boolean
|
||||
should_compress: boolean
|
||||
should_rotate_credential: boolean
|
||||
should_fallback: boolean
|
||||
}
|
||||
|
||||
export interface DebuggerResult {
|
||||
status: 'fixed' | 'cannot_reproduce' | 'blocked' | 'escalated'
|
||||
root_cause: string
|
||||
@@ -23,7 +55,7 @@ export class DebuggerRole {
|
||||
this.runtime = runtime
|
||||
}
|
||||
|
||||
async run(debug_spec: { task_id: string; error_report: string; affected_files: string[] }): Promise<DebuggerResult> {
|
||||
async run(debug_spec: { task_id?: string; error_report?: string; affected_files?: string[]; verification_refs?: string[] }): Promise<DebuggerResult> {
|
||||
const result: DebuggerResult = {
|
||||
status: 'cannot_reproduce',
|
||||
root_cause: '',
|
||||
@@ -32,54 +64,56 @@ export class DebuggerRole {
|
||||
}
|
||||
|
||||
try {
|
||||
this.runtime.emit('debug.started', { task_id: debug_spec.task_id })
|
||||
const task_id = debug_spec.task_id || 'unknown_task'
|
||||
const error_report = debug_spec.error_report || ''
|
||||
const affected_files = debug_spec.affected_files ?? []
|
||||
const classified = this.classify_error(error_report)
|
||||
|
||||
// Step 1: Gather evidence — read affected files
|
||||
result.diagnostic_chain.push('1. Gathering evidence from affected files')
|
||||
for (const file of debug_spec.affected_files) {
|
||||
result.diagnostic_chain.push(`1. Classified failure as ${classified.reason}`)
|
||||
result.diagnostic_chain.push(` retryable=${classified.retryable} compress=${classified.should_compress} rotate_credential=${classified.should_rotate_credential} fallback=${classified.should_fallback}`)
|
||||
|
||||
result.diagnostic_chain.push('2. Gathering evidence from affected files')
|
||||
for (const file of affected_files) {
|
||||
try {
|
||||
await this.runtime.call_tool('fs.read', { path: file })
|
||||
result.evidence_refs.push(`file:${file}`)
|
||||
const read = await this.runtime.call_tool('fs.read', { path: file })
|
||||
if (read.type === 'error') {
|
||||
result.diagnostic_chain.push(` Failed to read: ${file}`)
|
||||
} else {
|
||||
result.evidence_refs.push(`file:${file}`)
|
||||
}
|
||||
} catch {
|
||||
result.diagnostic_chain.push(` Failed to read: ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Analyze error signatures using LLM
|
||||
result.diagnostic_chain.push('2. Analyzing error signatures')
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are a debugging expert. Analyze the error report and suggest a fix.' },
|
||||
{ role: 'user', content: `Error report:\n${debug_spec.error_report}\n\nAffected files: ${debug_spec.affected_files.join(', ')}\n\nDiagnose the root cause and propose a fix. Be specific about which file and what change.` }
|
||||
]
|
||||
|
||||
result.diagnostic_chain.push('3. Analyzing root cause')
|
||||
try {
|
||||
const analysis = await this.runtime.call_llm({ messages, max_tokens: 2048, temperature: 0.3 })
|
||||
result.root_cause = analysis.content || 'Unable to determine root cause'
|
||||
result.diagnostic_chain.push(` Analysis: ${result.root_cause.slice(0, 100)}...`)
|
||||
const analysis = await this.runtime.call_llm({
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a diagnostic agent. Identify the likely root cause and recovery path. Do not claim a fix was applied unless a tool edit actually succeeded.' },
|
||||
{ role: 'user', content: `Classified error: ${JSON.stringify(classified)}\n\nError report:\n${error_report}\n\nAffected files: ${affected_files.join(', ') || '(none)'}` },
|
||||
],
|
||||
max_tokens: 2048,
|
||||
temperature: 0.2,
|
||||
})
|
||||
result.root_cause = (analysis.content || '').trim() || this.default_root_cause(classified)
|
||||
} catch {
|
||||
result.root_cause = 'LLM analysis unavailable — manual diagnosis required'
|
||||
result.root_cause = this.default_root_cause(classified)
|
||||
}
|
||||
|
||||
// Step 3: Attempt fix
|
||||
result.diagnostic_chain.push('3. Attempting fix')
|
||||
if (result.root_cause.includes('fix:') || result.root_cause.includes('change:') || result.root_cause.includes('Fix:')) {
|
||||
const fix_match = result.root_cause.match(/fix:\s*([^\n]+)/i) || result.root_cause.match(/change:\s*([^\n]+)/i)
|
||||
if (fix_match && debug_spec.affected_files.length > 0) {
|
||||
result.fix_applied = { file: debug_spec.affected_files[0], change: fix_match[1] }
|
||||
result.status = 'fixed'
|
||||
result.diagnostic_chain.push(' Fix applied to ' + debug_spec.affected_files[0])
|
||||
}
|
||||
}
|
||||
result.status = this.status_for(classified)
|
||||
const debug_record_id = `debug_${crypto.randomUUID()}`
|
||||
this.runtime.emit('debug.record.created', {
|
||||
event_id: `evt_${debug_record_id}`,
|
||||
debug_record_id,
|
||||
task_id,
|
||||
failure_signature: classified.reason,
|
||||
summary: result.root_cause.slice(0, 1000),
|
||||
evidence_refs: result.evidence_refs,
|
||||
verification_refs: debug_spec.verification_refs ?? [],
|
||||
})
|
||||
|
||||
// Step 4: Verify fix
|
||||
if (result.status === 'fixed') {
|
||||
result.diagnostic_chain.push('4. Verification')
|
||||
try {
|
||||
await this.runtime.call_tool('shell.run', { command: 'echo "Verification passed — fix applied"', timeout: 30000 })
|
||||
} catch { /* verification skipped */ }
|
||||
}
|
||||
|
||||
this.runtime.checkpoint('debug_completed', { task_id: debug_spec.task_id })
|
||||
this.runtime.checkpoint('debug_completed', { task_id, reason: classified.reason, status: result.status })
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
@@ -88,4 +122,53 @@ export class DebuggerRole {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private classify_error(report: string): ClassifiedError {
|
||||
const text = report.toLowerCase()
|
||||
const reason: FailoverReason = this.reason_for(text)
|
||||
return {
|
||||
reason,
|
||||
message: report,
|
||||
retryable: !['auth_permanent', 'billing', 'model_not_found', 'provider_policy_blocked', 'content_policy_blocked', 'format_error', 'invalid_encrypted_content'].includes(reason),
|
||||
should_compress: reason === 'context_overflow' || reason === 'payload_too_large' || reason === 'image_too_large',
|
||||
should_rotate_credential: reason === 'auth' || reason === 'auth_permanent',
|
||||
should_fallback: ['rate_limit', 'overloaded', 'server_error', 'timeout', 'model_not_found', 'long_context_tier', 'oauth_long_context_beta_forbidden'].includes(reason),
|
||||
}
|
||||
}
|
||||
|
||||
private reason_for(text: string): FailoverReason {
|
||||
if (/context|token|maximum context|too many tokens|context_length/.test(text)) return 'context_overflow'
|
||||
if (/payload too large|request too large|413/.test(text)) return 'payload_too_large'
|
||||
if (/image.*too large|vision.*size/.test(text)) return 'image_too_large'
|
||||
if (/rate limit|too many requests|429/.test(text)) return 'rate_limit'
|
||||
if (/overloaded|capacity|529/.test(text)) return 'overloaded'
|
||||
if (/timeout|timed out|etimedout|504/.test(text)) return 'timeout'
|
||||
if (/500|502|503|server error|bad gateway|service unavailable/.test(text)) return 'server_error'
|
||||
if (/invalid api key|unauthorized|401|forbidden|403|auth/.test(text)) return /invalid api key|revoked|expired/.test(text) ? 'auth_permanent' : 'auth'
|
||||
if (/billing|quota|insufficient credits|payment/.test(text)) return 'billing'
|
||||
if (/model.*not found|unknown model|404/.test(text)) return 'model_not_found'
|
||||
if (/policy|safety|blocked by provider/.test(text)) return 'provider_policy_blocked'
|
||||
if (/content policy|unsafe content/.test(text)) return 'content_policy_blocked'
|
||||
if (/json|schema|format|parse/.test(text)) return 'format_error'
|
||||
if (/encrypted content/.test(text)) return 'invalid_encrypted_content'
|
||||
if (/multimodal.*tool|tool.*image/.test(text)) return 'multimodal_tool_content_unsupported'
|
||||
if (/thinking.*signature|signature mismatch/.test(text)) return 'thinking_signature'
|
||||
if (/long context/.test(text)) return 'long_context_tier'
|
||||
if (/oauth.*long context|beta.*forbidden/.test(text)) return 'oauth_long_context_beta_forbidden'
|
||||
if (/grammar|llama.cpp|llama_cpp/.test(text)) return 'llama_cpp_grammar_pattern'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
private default_root_cause(classified: ClassifiedError): string {
|
||||
if (classified.should_compress) return `Likely ${classified.reason}; compress context or reduce payload before retry.`
|
||||
if (classified.should_rotate_credential) return `Likely ${classified.reason}; credential or authorization requires attention before retry.`
|
||||
if (classified.should_fallback) return `Likely ${classified.reason}; retry with backoff or fallback provider/model.`
|
||||
return `Failure classified as ${classified.reason}; manual diagnosis required.`
|
||||
}
|
||||
|
||||
private status_for(classified: ClassifiedError): DebuggerResult['status'] {
|
||||
if (classified.should_rotate_credential || classified.reason === 'billing' || classified.reason === 'content_policy_blocked') return 'escalated'
|
||||
if (classified.retryable || classified.should_compress || classified.should_fallback) return 'cannot_reproduce'
|
||||
return 'blocked'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ExecutorRole {
|
||||
}
|
||||
|
||||
async run(task_spec: { id: string; title: string; description: string; acceptance_criteria: string[] }): Promise<ExecutorResult> {
|
||||
this.runtime.emit('task.attempt.started', { task_id: task_spec.id })
|
||||
this.runtime.checkpoint('task_attempt_started', { task_id: task_spec.id })
|
||||
|
||||
const model = (task_spec as any).model || process.env.AIRCODING_MODEL || 'glm-5.1'
|
||||
const projectRoot = process.env.AIRCODING_PROJECT_ROOT || '.'
|
||||
@@ -41,7 +41,10 @@ export class ExecutorRole {
|
||||
role: 'system',
|
||||
content: `You are an AI coding assistant. Complete coding tasks by writing code files.
|
||||
|
||||
Use structured tool calls whenever possible. Available tools include fs.read, fs.write, fs.edit, fs.list, shell.run, cpp.detect, cpp.build, and cpp.test.
|
||||
Use structured tool calls whenever possible. Available tools include:
|
||||
- fs.read, fs.write, fs.edit, fs.list — filesystem operations
|
||||
- shell.run — shell command execution
|
||||
- cpp.detect, cpp.configure, cpp.build, cpp.test, cpp.cppcheck, cpp.clangd — C++ toolchain
|
||||
|
||||
If native tools are unavailable, output strict JSON tool calls only in this form:
|
||||
|
||||
@@ -201,10 +204,6 @@ After all required files are written and required verification has passed, write
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.runtime.emit('task.blocked', {
|
||||
task_id: task_spec.id,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return { status: 'blocked', error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
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<{
|
||||
@@ -26,7 +28,7 @@ export class ExperienceMinerRole {
|
||||
this.runtime = runtime
|
||||
}
|
||||
|
||||
async run(mine_spec: { task_ids: string[]; focus_categories?: string[] }): Promise<ExperienceMinerResult> {
|
||||
async run(mine_spec: { task_ids?: string[]; focus_categories?: string[]; source_refs?: Array<Record<string, unknown>>; evidence_refs?: string[] }): Promise<ExperienceMinerResult> {
|
||||
const result: ExperienceMinerResult = {
|
||||
status: 'no_patterns',
|
||||
entries: [],
|
||||
@@ -34,68 +36,73 @@ export class ExperienceMinerRole {
|
||||
}
|
||||
|
||||
try {
|
||||
this.runtime.emit('mining.started', { task_ids: mine_spec.task_ids })
|
||||
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']
|
||||
|
||||
// Read completed task results to extract patterns
|
||||
const task_summaries: string[] = []
|
||||
for (const task_id of mine_spec.task_ids) {
|
||||
try {
|
||||
// Emit that we're reading a task
|
||||
this.runtime.emit('mining.task', { task_id })
|
||||
task_summaries.push(`Task ${task_id}: completed`)
|
||||
} catch { /* skip failed task reads */ }
|
||||
}
|
||||
|
||||
if (task_summaries.length === 0) {
|
||||
result.status = 'no_patterns'
|
||||
result.summary = 'No completed tasks available for mining'
|
||||
if (task_ids.length === 0 && evidence_refs.length === 0) {
|
||||
result.summary = 'No task or evidence refs available for memory mining'
|
||||
return result
|
||||
}
|
||||
|
||||
// Use LLM to extract patterns
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are an experience mining expert. Extract reusable patterns, best practices, and lessons learned from completed tasks. Output one pattern per line in format: CATEGORY: pattern description' },
|
||||
{ role: 'user', content: `Analyze these completed tasks and extract reusable patterns:\n${task_summaries.join('\n')}\n\nFocus categories: ${(mine_spec.focus_categories || ['implementation', 'debugging', 'testing']).join(', ')}` }
|
||||
{ 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.3 })
|
||||
const lines = (analysis.content || '').split('\n').filter(l => l.includes(':'))
|
||||
|
||||
for (const line of lines) {
|
||||
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) {
|
||||
const category = line.slice(0, colon_idx).trim().toLowerCase()
|
||||
const pattern = line.slice(colon_idx + 1).trim()
|
||||
if (pattern.length > 5) {
|
||||
result.entries.push({
|
||||
category,
|
||||
pattern,
|
||||
source_task_id: mine_spec.task_ids[0] || '',
|
||||
description: pattern
|
||||
})
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// LLM unavailable — extract basic patterns from task metadata
|
||||
result.entries.push({
|
||||
category: 'execution',
|
||||
pattern: 'Tasks completed via Executor→LLM→Tool loop',
|
||||
source_task_id: mine_spec.task_ids[0] || '',
|
||||
description: 'Standard execution pattern for code changes'
|
||||
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.status = 'no_patterns'
|
||||
result.summary = `No patterns extracted from ${mine_spec.task_ids.length} tasks`
|
||||
result.summary = `No supported memory candidates extracted from ${task_ids.length} tasks`
|
||||
} else {
|
||||
result.status = 'completed'
|
||||
result.summary = `Mined ${result.entries.length} patterns from ${mine_spec.task_ids.length} tasks`
|
||||
result.summary = `Created ${result.entries.length} memory candidates from ${task_ids.length} tasks`
|
||||
}
|
||||
|
||||
this.runtime.checkpoint('mining_completed', { patterns_found: result.entries.length })
|
||||
this.runtime.checkpoint('experience_mining_completed', { candidates: result.entries.length })
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ReviewerRole {
|
||||
const result: ReviewerResult = { status: 'pass', findings: [], summary: '' }
|
||||
|
||||
try {
|
||||
this.runtime.emit('review.started', { task_id: review_spec.task_id })
|
||||
this.runtime.checkpoint('review_started', { task_id: review_spec.task_id })
|
||||
|
||||
for (const file of review_spec.change_files) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user