twining_assemble
Assemble tailored context packages for development tasks by gathering relevant decisions, warnings, needs, findings, and questions within a token budget.
Instructions
Build tailored context for a specific task. Returns relevant decisions, warnings, needs, findings, and questions within a token budget. Call this before starting any task to get shared context from other agents.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| task | Yes | Description of what the agent is about to do | |
| scope | Yes | File path, module, or area of codebase (e.g., "src/auth/" or "project") | |
| max_tokens | No | Token budget (default: from config, typically 4000) | |
| agent_id | No | Agent identifier for assembly tracking (default: main) |
Implementation Reference
- src/engine/context-assembler.ts:80-451 (handler)The actual implementation of the context assembly engine, which performs the scoring and filtering logic for the twining_assemble tool.
async assemble( task: string, scope: string, maxTokens?: number, agentId?: string, ): Promise<AssembledContext> { const budget = maxTokens ?? this.config.context_assembly.default_max_tokens; const weights = this.config.context_assembly.priority_weights; const now = Date.now(); // Load all data once upfront to avoid redundant disk reads const { entries: allEntries } = await this.blackboardStore.read(); const allIndex = await this.decisionStore.getIndex(); // 1. Retrieve scope-matched decisions const scopeDecisions = await this.decisionStore.getByScope(scope); const activeDecisions = scopeDecisions.filter( (d) => d.status === "active" || d.status === "provisional", ); // 2. Retrieve semantically relevant decisions (merge by ID, keep highest relevance) const decisionRelevance = new Map<string, number>(); if (this.searchEngine) { const allDecisions: Decision[] = []; for (const entry of allIndex) { if (entry.status === "active" || entry.status === "provisional") { const d = await this.decisionStore.get(entry.id); if (d) allDecisions.push(d); } } const { results: semanticDecisions } = await this.searchEngine.searchDecisions(task, allDecisions); for (const sr of semanticDecisions) { decisionRelevance.set(sr.decision.id, sr.relevance); } } // Merge scope-matched and semantic decisions (union by ID) const mergedDecisionMap = new Map<string, Decision>(); for (const d of activeDecisions) { mergedDecisionMap.set(d.id, d); if (!decisionRelevance.has(d.id)) { decisionRelevance.set(d.id, 0.5); // Default relevance for scope-only matches } } if (this.searchEngine) { for (const entry of allIndex) { if ( decisionRelevance.has(entry.id) && !mergedDecisionMap.has(entry.id) ) { const d = await this.decisionStore.get(entry.id); if (d && (d.status === "active" || d.status === "provisional")) { mergedDecisionMap.set(d.id, d); } } } } // 3. Retrieve scope-matched blackboard entries (filter from cached allEntries) const scopeEntries = allEntries.filter( (e) => e.scope.startsWith(scope) || scope.startsWith(e.scope), ); // 4. Retrieve semantically relevant findings const entryRelevance = new Map<string, number>(); if (this.searchEngine) { const { results: semanticEntries } = await this.searchEngine.searchBlackboard(task, allEntries); for (const sr of semanticEntries) { entryRelevance.set(sr.entry.id, sr.relevance); } } // Merge entries — scope + semantic const mergedEntryMap = new Map<string, BlackboardEntry>(); for (const e of scopeEntries) { mergedEntryMap.set(e.id, e); if (!entryRelevance.has(e.id)) { entryRelevance.set(e.id, 0.5); } } if (this.searchEngine) { for (const e of allEntries) { if (entryRelevance.has(e.id) && !mergedEntryMap.has(e.id)) { mergedEntryMap.set(e.id, e); } } } // 5. Compute graph reachability scores for decisions const { scores: reachabilityScores, paths: reachabilityPaths } = await this.computeGraphReachability(scope, mergedDecisionMap); // 6. Score each item const scoredItems: ScoredItem[] = []; const graphWeight = weights.graph_reachability ?? weights.graph_connectivity ?? 0; // Adaptive weight fallback: if graph returns 0 for all candidates, // redistribute graph weight proportionally to other signals const maxGraphScore = Math.max(0, ...Array.from(reachabilityScores.values())); const effectiveGraphWeight = maxGraphScore === 0 ? 0 : graphWeight; const weightScale = maxGraphScore === 0 && graphWeight > 0 ? 1.0 / (1.0 - graphWeight) : 1.0; for (const [id, decision] of mergedDecisionMap) { const recency = this.recencyScore(decision.timestamp, now); const relevance = decisionRelevance.get(id) ?? 0.5; const confidence = this.confidenceScore(decision.confidence); const warningBoost = 0; const reachability = reachabilityScores.get(id) ?? 0; const score = recency * weights.recency * weightScale + relevance * weights.relevance * weightScale + confidence * weights.decision_confidence * weightScale + warningBoost * weights.warning_boost * weightScale + reachability * effectiveGraphWeight; const text = `${decision.summary} ${decision.rationale} ${decision.confidence} ${decision.affected_files.join(", ")}`; scoredItems.push({ type: "decision", id, score, tokenCost: estimateTokens(text), data: decision, }); } for (const [id, entry] of mergedEntryMap) { const recency = this.recencyScore(entry.timestamp, now); const relevance = entryRelevance.get(id) ?? 0.5; const confidence = 0.5; // Neutral for non-decisions const warningBoost = entry.entry_type === "warning" ? 1.0 : 0.0; const score = recency * weights.recency * weightScale + relevance * weights.relevance * weightScale + confidence * weights.decision_confidence * weightScale + warningBoost * weights.warning_boost * weightScale; const text = `${entry.summary} ${entry.detail}`; scoredItems.push({ type: entry.entry_type as ScoredItem["type"], id, score, tokenCost: estimateTokens(text), data: entry, }); } // 7. Fill token budget in priority order. // Warnings get priority access — they fill first from the full budget. // Non-warnings fill the remaining budget, but are capped at 90% of the // total budget to ensure at least 10% is reserved for warnings. const warningReserve = Math.floor(budget * 0.1); const nonWarningCap = budget - warningReserve; // Separate warnings and non-warnings const warnings = scoredItems.filter((i) => i.type === "warning"); const nonWarnings = scoredItems.filter((i) => i.type !== "warning"); warnings.sort((a, b) => b.score - a.score); nonWarnings.sort((a, b) => b.score - a.score); const selected = new Set<string>(); let tokensUsed = 0; // First: fill warnings from the full budget (priority access) for (const item of warnings) { if (tokensUsed + item.tokenCost <= budget) { selected.add(item.id); tokensUsed += item.tokenCost; } } // Then: fill non-warnings, capped so they don't starve warnings let nonWarningTokens = 0; for (const item of nonWarnings) { if ( tokensUsed + item.tokenCost <= budget && nonWarningTokens + item.tokenCost <= nonWarningCap ) { selected.add(item.id); tokensUsed += item.tokenCost; nonWarningTokens += item.tokenCost; } } // Also include needs even if low-scored (safety) for (const item of scoredItems) { if ( item.type === "need" && !selected.has(item.id) && tokensUsed + item.tokenCost <= budget ) { selected.add(item.id); tokensUsed += item.tokenCost; } } // 8. Build AssembledContext const activeDecisionResults: AssembledContext["active_decisions"] = []; const openNeeds: AssembledContext["open_needs"] = []; const recentFindings: AssembledContext["recent_findings"] = []; const activeWarnings: AssembledContext["active_warnings"] = []; const recentQuestions: AssembledContext["recent_questions"] = []; for (const item of scoredItems) { if (!selected.has(item.id)) continue; if (item.type === "decision") { const d = item.data as Decision; const decisionEntry: AssembledContext["active_decisions"][number] = { id: d.id, summary: d.summary, rationale: d.rationale, confidence: d.confidence, affected_files: d.affected_files, }; const path = reachabilityPaths.get(d.id); if (path) { decisionEntry.relevance_path = path; } activeDecisionResults.push(decisionEntry); } else { const e = item.data as BlackboardEntry; switch (e.entry_type) { case "need": openNeeds.push({ id: e.id, summary: e.summary, scope: e.scope, timestamp: e.timestamp, }); break; case "warning": activeWarnings.push({ id: e.id, summary: e.summary, detail: e.detail, scope: e.scope, timestamp: e.timestamp, }); break; case "finding": recentFindings.push({ id: e.id, summary: e.summary, detail: e.detail, scope: e.scope, timestamp: e.timestamp, }); break; case "question": recentQuestions.push({ id: e.id, summary: e.summary, scope: e.scope, timestamp: e.timestamp, }); break; // Other entry types go into findings as a catch-all default: recentFindings.push({ id: e.id, summary: e.summary, detail: e.detail, scope: e.scope, timestamp: e.timestamp, }); break; } } } // 9. Populate related_entities from knowledge graph const relatedEntities = await this.getRelatedEntities(scope); const result: AssembledContext = { assembled_at: new Date().toISOString(), task, scope, token_estimate: tokensUsed, active_decisions: activeDecisionResults, open_needs: openNeeds, recent_findings: recentFindings, active_warnings: activeWarnings, recent_questions: recentQuestions, related_entities: relatedEntities, }; // 10. Integrate planning state from .planning/ directory const planningState = this.planningBridge?.readPlanningState() ?? null; if (planningState) { // Always include planning_state as metadata (not subject to token budget) result.planning_state = planningState; // Add a synthetic scored finding for planning context so it competes // for token budget alongside other items (GSDB-04) let planningText = `Planning: Phase ${planningState.current_phase}, Progress: ${planningState.progress}`; if (planningState.blockers.length > 0) { planningText += `. Blockers: ${planningState.blockers.join("; ")}`; } const planningTokenCost = estimateTokens(planningText); const planningScore = 1.0 * weights.recency + // always fresh 0.5 * weights.relevance + // moderate default relevance 0.5 * weights.decision_confidence; if (tokensUsed + planningTokenCost <= budget) { recentFindings.push({ id: "planning-state", summary: planningText, detail: planningState.open_requirements.length > 0 ? `Open requirements: ${planningState.open_requirements.join(", ")}` : "", scope: "project", timestamp: new Date().toISOString(), }); result.token_estimate += planningTokenCost; } } // 11. Include recent handoffs matching scope (HND-03) if (this.handoffStore) { const handoffEntries = await this.handoffStore.list({ scope, limit: 5 }); if (handoffEntries.length > 0) { result.recent_handoffs = handoffEntries.map((h) => ({ id: h.id, source_agent: h.source_agent, target_agent: h.target_agent ?? "", scope: h.scope ?? "", summary: h.summary, result_status: h.result_status, acknowledged: h.acknowledged, created_at: h.created_at, })); } } // 12. Include suggested agents with matching capabilities (HND-06) if (this.agentStore) { const allAgents = await this.agentStore.getAll(); const thresholds = this.config.agents?.liveness ?? DEFAULT_LIVENESS_THRESHOLDS; const taskTerms = normalizeTags(task.split(/\s+/)); const suggestedAgents: NonNullable<AssembledContext["suggested_agents"]> = []; for (const agent of allAgents) { const liveness = computeLiveness(agent.last_active, new Date(), thresholds); if (liveness === "gone") continue; const normalizedCaps = normalizeTags(agent.capabilities); const hasMatch = normalizedCaps.some((cap) => taskTerms.some((term) => term.includes(cap) || cap.includes(term)), ); if (hasMatch) { suggestedAgents.push({ agent_id: agent.agent_id, capabilities: agent.capabilities, liveness, }); } } if (suggestedAgents.length > 0) { result.suggested_agents = suggestedAgents; } } // Log assembly for assembly-before-decision tracking this.assemblyLog.set(agentId ?? "main", result.assembled_at); return result; } - src/tools/context-tools.ts:14-54 (registration)MCP tool registration for twining_assemble, which delegates the logic to the ContextAssembler class.
// twining_assemble — Build tailored context for a specific task server.registerTool( "twining_assemble", { description: "Build tailored context for a specific task. Returns relevant decisions, warnings, needs, findings, and questions within a token budget. Call this before starting any task to get shared context from other agents.", inputSchema: { task: z.string().describe("Description of what the agent is about to do"), scope: z .string() .describe('File path, module, or area of codebase (e.g., "src/auth/" or "project")'), max_tokens: z .number() .optional() .describe("Token budget (default: from config, typically 4000)"), agent_id: z .string() .optional() .describe("Agent identifier for assembly tracking (default: main)"), }, }, async (args) => { try { const result = await contextAssembler.assemble( args.task, args.scope, args.max_tokens, args.agent_id, ); return toolResult(result); } catch (e) { if (e instanceof TwiningError) { return toolError(e.message, e.code); } return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, );