UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

354 lines (353 loc) 14 kB
/** * MCP Handler — autosnippet_task (Intent Lifecycle + Signal Collection) * * 5 Operations: * prime — Load knowledge context + initialize intent * create — Create in-memory task anchor (generates ID) * close — Complete task + persist intent chain + trigger Guard * fail — Abandon task + persist intent chain * record_decision — Record user preference signal * * Architecture: Zero DB. Pure memory (IntentState) + SignalBus → JSONL signals. */ import { notifyTaskProgress } from '#infra/notification/LarkNotifier.js'; import { extract as extractIntent } from '#service/task/IntentExtractor.js'; import { envelope } from '../envelope.js'; import { createIdleIntent } from './types.js'; // ─── In-memory task ID counter ─────────────────────────── let _taskCounter = 0; function _generateTaskId() { _taskCounter++; return `asd-${Date.now().toString(36)}-${_taskCounter}`; } // ─── Task Rules Reminder ───────────────────────────────── const _taskRules = { reminder: [ '📋 TASK RULES (MANDATORY):', '🔑 YOU are the task operator — user speaks naturally, you translate to task operations.', '• MUST prime on EVERY message BEFORE anything else', '• MUST create task for non-trivial work (≥2 files OR ≥10 lines)', '• MUST close when done with meaningful reason', '• When user agrees/disagrees → record_decision immediately', '• NEVER tell user to run task commands', ].join('\n'), translationHint: [ 'User Says → You Run:', '"fix bug"/"implement" → create→code→close', '"continue" → resume in-progress→close', '"pause"/"abandon" → fail(id, reason)', '"agreed"/"disagree" → record_decision', 'Quick question → No task. Just answer.', ].join('\n'), }; /** * Unified entry point */ export async function taskHandler(ctx, args) { // Normalize taskId → id (schema accepts both for convenience) if (!args.id && typeof args.taskId === 'string') { args.id = args.taskId; } let result; switch (args.operation) { case 'prime': return _prime(ctx, args); case 'create': result = await _create(ctx, args); break; case 'close': result = await _close(ctx, args); break; case 'fail': result = await _fail(ctx, args); break; case 'record_decision': result = await _recordDecision(ctx, args); break; default: return envelope({ success: false, message: `Unknown operation: ${args.operation}. Valid: prime, create, close, fail, record_decision.`, meta: { tool: 'autosnippet_task' }, }); } // ── Lark notification (async, non-blocking) ── notifyTaskProgress(args.operation, args, result).catch((err) => { process.stderr.write(`[MCP/Task] Notify error: ${err instanceof Error ? err.message : String(err)}\n`); }); return result; } // ═══ prime ═══════════════════════════════════════════════ async function _prime(ctx, args) { const intent = ctx.session?.intent; // If there is an active intent, persist it as abandoned before starting fresh if (intent && intent.phase === 'active') { _persistIntentChain(ctx, intent, 'abandoned', 'New prime received'); } // ─── Intake: extract intent signals ─── const extracted = extractIntent(args.userQuery || '', args.activeFile, args.language); // ─── Enrichment: multi-query search via PrimeSearchPipeline ─── const pipeline = _getPipeline(ctx.container); let searchResult = null; if (pipeline && extracted.queries[0]?.trim()) { try { searchResult = await pipeline.search(extracted); if (!searchResult) { process.stderr.write('[MCP/Task] prime: pipeline.search returned null (all filtered)\n'); } } catch (err) { process.stderr.write(`[MCP/Task] prime search error: ${err instanceof Error ? err.stack || err.message : String(err)}\n`); } } else if (!pipeline) { process.stderr.write('[MCP/Task] prime: pipeline is null, skipping search\n'); } else { process.stderr.write(`[MCP/Task] prime: queries empty, skipping search. queries=${JSON.stringify(extracted.queries)}\n`); } // ─── Lifecycle: initialize IntentState ─── const freshIntent = createIdleIntent(); freshIntent.phase = 'active'; freshIntent.primeQuery = args.userQuery || ''; freshIntent.primeActiveFile = args.activeFile; freshIntent.primeLanguage = extracted.language; freshIntent.primeModule = extracted.module; freshIntent.primeScenario = extracted.scenario; freshIntent.primeAt = Date.now(); if (searchResult) { freshIntent.primeRecipeIds = [...searchResult.relatedKnowledge, ...searchResult.guardRules] .map((r) => r.id) .filter(Boolean); freshIntent.searchMeta = { queries: searchResult.searchMeta.queries, resultCount: searchResult.searchMeta.resultCount, filteredCount: searchResult.searchMeta.filteredCount, }; } // Bind intent to session if (ctx.session) { ctx.session.intent = freshIntent; } // ─── Delivery: build response ─── const relatedCount = searchResult?.relatedKnowledge.length ?? 0; const ruleCount = searchResult?.guardRules.length ?? 0; const lines = []; if (relatedCount > 0 || ruleCount > 0) { lines.push(`📋 Found ${relatedCount} recipe(s), ${ruleCount} guard rule(s).`); for (const r of searchResult.relatedKnowledge) { const hint = r.actionHint ? ` — ${r.actionHint}` : ''; const refs = r.sourceRefs?.length ? `\n 📍 ${r.sourceRefs.join(', ')}` : ''; lines.push(` • ${r.trigger || r.title}${hint}${refs}`); } for (const r of searchResult.guardRules) { lines.push(` • [rule] ${r.trigger || r.title}`); } } else { lines.push('No matching recipes found.'); } return envelope({ success: true, data: { knowledge: searchResult ? { relatedKnowledge: searchResult.relatedKnowledge, guardRules: searchResult.guardRules, } : null, searchMeta: searchResult?.searchMeta ?? null, _taskRules, }, message: lines.join('\n'), meta: { tool: 'autosnippet_task' }, }); } // ═══ create ═════════════════════════════════════════════ async function _create(ctx, args) { if (!args.title) { return envelope({ success: false, message: 'title is required', meta: { tool: 'autosnippet_task' }, }); } const taskId = _generateTaskId(); const intent = ctx.session?.intent; // Bind task ID to current intent if (intent && intent.phase === 'active') { intent.taskId = taskId; intent.taskTitle = args.title; } return envelope({ success: true, data: { id: taskId, title: args.title }, message: `📌 Created: ${taskId} — ${args.title}`, meta: { tool: 'autosnippet_task' }, }); } // ═══ close ══════════════════════════════════════════════ async function _close(ctx, args) { const intent = ctx.session?.intent; // Resolve id: explicit arg > session intent > fail const id = args.id || (intent?.taskId ?? ''); if (!id) { return envelope({ success: false, message: 'id is required (pass id or ensure a task was created in this session)', meta: { tool: 'autosnippet_task' }, }); } const reason = args.reason || 'Completed'; // Persist intent chain via SignalBus if (intent && intent.phase === 'active') { _persistIntentChain(ctx, intent, 'completed', reason); } // Reset intent to idle if (ctx.session) { ctx.session.intent = createIdleIntent(); } const lines = [`✅ Closed: ${id} — ${reason}`]; lines.push(''); lines.push('⚠️ REQUIRED: You MUST call autosnippet_guard (no args) NOW to review changed files for compliance violations.'); return envelope({ success: true, data: { closed: { id, reason, closedAt: Date.now() }, nextAction: { tool: 'autosnippet_guard', args: {}, required: true, reason: 'Post-close compliance review — check diff for violations before moving on.', }, }, message: lines.join('\n'), meta: { tool: 'autosnippet_task' }, }); } // ═══ fail ═══════════════════════════════════════════════ async function _fail(ctx, args) { const intent = ctx.session?.intent; // Resolve id: explicit arg > session intent > fail const id = args.id || (intent?.taskId ?? ''); if (!id) { return envelope({ success: false, message: 'id is required (pass id or ensure a task was created in this session)', meta: { tool: 'autosnippet_task' }, }); } const reason = args.reason || 'Agent execution failed'; // Persist intent chain via SignalBus if (intent && intent.phase === 'active') { _persistIntentChain(ctx, intent, 'failed', reason); } // Reset intent to idle if (ctx.session) { ctx.session.intent = createIdleIntent(); } return envelope({ success: true, data: { failed: { id, reason, failedAt: Date.now() }, }, message: `❌ Failed: ${id} — ${reason}`, meta: { tool: 'autosnippet_task' }, }); } // ═══ record_decision ════════════════════════════════════ async function _recordDecision(ctx, args) { if (!args.title) { return envelope({ success: false, message: 'title is required', meta: { tool: 'autosnippet_task' }, }); } if (!args.description) { return envelope({ success: false, message: 'description is required', meta: { tool: 'autosnippet_task' }, }); } const decisionId = `dec-${Date.now().toString(36)}`; const decision = { id: decisionId, title: args.title, description: args.description, rationale: args.rationale, tags: args.tags, recordedAt: Date.now(), }; // Push to current intent's decisions const intent = ctx.session?.intent; if (intent && intent.phase === 'active') { intent.decisions.push(decision); } return envelope({ success: true, data: { decision: { id: decisionId, title: args.title } }, message: `📌 Decision recorded: ${args.title}`, meta: { tool: 'autosnippet_task' }, }); } // ═══ Intent Chain Persistence (via SignalBus) ═══════════ function _persistIntentChain(ctx, intent, outcome, reason) { const now = Date.now(); const chain = { sessionId: ctx.session?.id || 'unknown', taskId: intent.taskId, outcome, primeQuery: intent.primeQuery, primeActiveFile: intent.primeActiveFile, primeRecipeIds: intent.primeRecipeIds, primeAt: intent.primeAt || now, primeLanguage: intent.primeLanguage ?? null, primeModule: intent.primeModule ?? null, primeScenario: intent.primeScenario ?? 'search', searchMeta: intent.searchMeta, toolCalls: intent.toolCalls, searchQueries: intent.searchQueries, mentionedFiles: intent.mentionedFiles, decisions: intent.decisions, driftEvents: intent.driftEvents, driftScore: _computeDriftScore(intent), closeReason: outcome === 'completed' ? reason : undefined, failReason: outcome !== 'completed' ? reason : undefined, startedAt: intent.primeAt || now, endedAt: now, duration: now - (intent.primeAt || now), }; // Emit via SignalBus — subscribers handle JSONL persistence try { const signalBus = ctx.container.get('signalBus'); signalBus.send('intent', 'TaskHandler', _computeDriftScore(intent), { target: intent.taskId ?? null, metadata: { chain }, }); } catch { // signalBus unavailable — silent failure, non-blocking } } function _computeDriftScore(intent) { if (intent.driftEvents.length === 0) { return 0; } const sum = intent.driftEvents.reduce((acc, d) => acc + (1 - d.primeOverlap), 0); return sum / intent.driftEvents.length; } function _getPipeline(container) { try { const p = container.get('primeSearchPipeline'); if (!p) { process.stderr.write('[MCP/Task] _getPipeline: container returned null/undefined\n'); } return p; } catch (err) { process.stderr.write(`[MCP/Task] _getPipeline failed: ${err instanceof Error ? err.message : String(err)}\n`); return null; } }