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",
          );
        }
      },
    );
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses key behavioral traits: that it returns multiple types of information (decisions, warnings, etc.), operates within a token budget, and serves as a preparatory step. However, it doesn't mention potential side effects, authentication needs, rate limits, or what happens if the token budget is exceeded.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is perfectly concise with two sentences that each earn their place: the first explains what the tool does and returns, the second provides crucial usage guidance. No wasted words, and the most important information (purpose and when to call it) is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 4 parameters, 100% schema coverage, and no output schema, the description provides good context about purpose and usage. However, as a preparatory tool that likely returns complex structured data, the absence of output schema means the description should ideally hint at the return format more explicitly, though it does list the types of information returned.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters well. The description adds value by explaining the overall purpose of parameter assembly ('Build tailored context for a specific task') and mentioning the token budget concept, but doesn't provide additional semantic details about individual parameters beyond what the schema offers.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with specific verbs ('Build tailored context') and resources ('relevant decisions, warnings, needs, findings, and questions'), and explicitly distinguishes it from siblings by specifying 'Call this before starting any task to get shared context from other agents' - indicating this is a preparatory tool unlike most other twining_* tools that perform actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit usage guidelines: 'Call this before starting any task' tells when to use it, and the context of getting 'shared context from other agents' distinguishes it from tools like twining_read, twining_query, or twining_search_decisions that retrieve specific information rather than assembling preparatory context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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