Skip to main content
Glama

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
NameRequiredDescriptionDefault
taskYesDescription of what the agent is about to do
scopeYesFile path, module, or area of codebase (e.g., "src/auth/" or "project")
max_tokensNoToken budget (default: from config, typically 4000)
agent_idNoAgent identifier for assembly tracking (default: main)

Implementation Reference

  • 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;
    }
  • 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",
          );
        }
      },
    );

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/daveangulo/twining-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server