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:
@@ -13,6 +13,8 @@ import type { ChildProcess } from 'child_process'
|
||||
import { WorkerProcess, type WorkerExitCode } from './WorkerProcess.js'
|
||||
import { WorkerProtocol, type WorkerMessage, type WorkerMessageType } from './WorkerProtocol.js'
|
||||
import type { WorkerResult, WorkerStatus, AgentType } from '@aircoding/contracts'
|
||||
import { eventIngestor } from '../events/EventIngestor.js'
|
||||
import { eventSchemaRegistry } from '../events/EventSchemaRegistry.js'
|
||||
import type { ToolRegistry } from '../tools/ToolRegistry.js'
|
||||
import type { ProviderManager } from '@aircoding/llm'
|
||||
|
||||
@@ -85,6 +87,7 @@ export class WorkerManager {
|
||||
state: 'starting',
|
||||
started_at: new Date().toISOString()
|
||||
}
|
||||
this.workers.set(config.agent_id, handle)
|
||||
|
||||
// Spawn worker process using Bun
|
||||
// Worker must run from AirCoding repo root so Bun can resolve modules
|
||||
@@ -126,7 +129,6 @@ export class WorkerManager {
|
||||
})
|
||||
|
||||
handle.state = 'ready'
|
||||
this.workers.set(config.agent_id, handle)
|
||||
|
||||
// Set up timeout
|
||||
if (config.timeout_ms) {
|
||||
@@ -238,6 +240,40 @@ export class WorkerManager {
|
||||
}
|
||||
})
|
||||
|
||||
// Handle worker-emitted RuntimeEvent payloads through the single EventIngestor entry point.
|
||||
proc.on_message('event', async (msg) => {
|
||||
try {
|
||||
const event_type = (msg.payload.event_type || msg.payload.type) as string
|
||||
if (!event_type || !eventSchemaRegistry.isRegistered(event_type, 1)) {
|
||||
console.error(`[WM] ignoring unregistered worker event: ${event_type || '(missing)'}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { event_type: _eventType, ...restPayload } = msg.payload
|
||||
const payload = _eventType ? restPayload : (() => {
|
||||
const { type: _legacyType, ...legacyPayload } = restPayload
|
||||
return legacyPayload
|
||||
})()
|
||||
const event = {
|
||||
id: (payload.event_id as string) || msg.id,
|
||||
type: event_type,
|
||||
version: 1,
|
||||
timestamp: msg.timestamp || new Date().toISOString(),
|
||||
session_id: this.execution_context?.session_id || msg.session_id,
|
||||
project_id: this.execution_context?.project_id || '',
|
||||
source: { kind: 'agent', id: agent_id, agent_type: this.worker_agent_type(agent_id) },
|
||||
route: ['worker', agent_id, event_type],
|
||||
payload,
|
||||
}
|
||||
|
||||
const persistence = eventSchemaRegistry.getPersistence(event_type, 1)
|
||||
if (persistence === 'durable') await eventIngestor.ingest(event as any)
|
||||
else if (persistence === 'ephemeral') await eventIngestor.ingest_ephemeral(event as any)
|
||||
} catch (e: any) {
|
||||
console.error('[WM] worker event ingest error:', e.message)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle worker.result → update handle
|
||||
proc.on_message('worker.result', (msg) => {
|
||||
const handle = this.workers.get(agent_id)
|
||||
@@ -324,20 +360,32 @@ export class WorkerManager {
|
||||
* Get result for a task.
|
||||
*/
|
||||
get_result_for_task(task_id: string): WorkerResult<unknown> | undefined {
|
||||
return this.list().find(w => w.config.task_spec?.id === task_id)?.result
|
||||
return this.list().find(w => w.config.task_spec?.id === task_id || w.config.task_spec?.task_id === task_id || w.worker_id === `agent_${task_id}`)?.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get handle for a task.
|
||||
*/
|
||||
get_handle_for_task(task_id: string): WorkerHandle | undefined {
|
||||
return this.list().find(w => w.config.task_spec?.id === task_id)
|
||||
return this.list().find(w => w.config.task_spec?.id === task_id || w.config.task_spec?.task_id === task_id || w.worker_id === `agent_${task_id}`)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private
|
||||
// ============================================================================
|
||||
|
||||
private worker_agent_type(agent_id: string): AgentType {
|
||||
const handle = this.workers.get(agent_id)
|
||||
const task_type = handle?.config.task_type || 'execute'
|
||||
switch (task_type) {
|
||||
case 'review': return 'reviewer' as AgentType
|
||||
case 'debug': return 'debugger' as AgentType
|
||||
case 'compact': return 'compactor' as AgentType
|
||||
case 'mine_experience': return 'experience_miner' as AgentType
|
||||
default: return 'executor' as AgentType
|
||||
}
|
||||
}
|
||||
|
||||
private handle_worker_exit(agent_id: string, exit: { code: number | null; signal: NodeJS.Signals | null; semantic: string; description: string }): void {
|
||||
const handle = this.workers.get(agent_id)
|
||||
if (!handle) return
|
||||
@@ -371,9 +419,11 @@ export class WorkerManager {
|
||||
const raw_status = (payload.status as string) || 'completed'
|
||||
const status = raw_status === 'completed' || raw_status === 'cancelled' || raw_status === 'blocked' || raw_status === 'failed'
|
||||
? raw_status
|
||||
: raw_status === 'fixed' || raw_status === 'pass'
|
||||
: raw_status === 'fixed' || raw_status === 'cannot_reproduce' || raw_status === 'pass' || raw_status === 'compacted' || raw_status === 'skipped' || raw_status === 'no_patterns'
|
||||
? 'completed'
|
||||
: 'failed'
|
||||
: raw_status === 'escalated'
|
||||
? 'blocked'
|
||||
: 'failed'
|
||||
const changes = Array.isArray((payload as any).changes) ? (payload as any).changes : []
|
||||
const changed_files = (payload.changed_files as string[] | undefined) || changes.map((c: any) => String(c.file)).filter(Boolean)
|
||||
const verification_payload = payload.verification as any
|
||||
@@ -381,13 +431,15 @@ export class WorkerManager {
|
||||
: verification_payload ? [{ command: 'worker verification', passed: Boolean(verification_payload.passed), output: String(verification_payload.output || '') }] as any[]
|
||||
: []
|
||||
const summary = (payload.summary as string)
|
||||
|| (payload.summary_content as string)
|
||||
|| (payload.root_cause as string)
|
||||
|| (payload.error ? String(payload.error) : '')
|
||||
|| (changed_files.length > 0 ? `Changed files: ${changed_files.join(', ')}` : `Worker ${status}`)
|
||||
|
||||
return {
|
||||
task_id: (payload.task_id as string) || (handle.config.task_spec?.id as string) || '' as any,
|
||||
task_id: (payload.task_id as string) || (handle.config.task_spec?.id as string) || (handle.config.task_spec?.task_id as string) || '' as any,
|
||||
agent_id: (payload.agent_id as string) || handle.config.agent_id as any,
|
||||
agent_type: (payload.agent_type as AgentType) || 'executor',
|
||||
agent_type: (payload.agent_type as AgentType) || this.worker_agent_type(handle.config.agent_id),
|
||||
status: status as WorkerStatus,
|
||||
summary,
|
||||
changed_files,
|
||||
|
||||
Reference in New Issue
Block a user