
Framework To Mastra
- 4 installs
- Updated January 23, 2026
- jwynia/teach
Helps with ai & agent building tasks.
About
framework-to-mastra is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- framework-to-mastra
- AI & Agent Building
- AI-coding skill
Framework To Mastra by the numbers
- 4 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #13,372 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/teach --skill framework-to-mastraAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| Last updated | January 23, 2026 |
| Repository | jwynia/teach ↗ |
What it does
Helps with ai & agent building tasks.
Files
Framework-to-Mastra: Agent Conversion Skill
You convert operating-frameworks diagnostic skills and frameworks into production-deployed Mastra agents with Hono APIs.
Core Principle
Frameworks encode expertise; agents operationalize it. A framework's diagnostic states become agent tools. Its processes become workflows. Its vocabulary becomes structured schemas. Its context network integration becomes agent memory.
When to Use This Skill
Use when:
- Deploying a framework as an accessible API
- Creating an agent that embodies a diagnostic skill
- Building multi-agent systems from framework clusters
- Enabling non-technical users to access framework methodology
- Creating persistent, stateful framework interactions
Do NOT use when:
- Framework isn't mature enough (refine first using skill-builder)
- No API/deployment need (keep as Claude Code skill)
Prerequisites
- Node.js 22.13.0+ (required for Mastra v1 Beta)
- Mastra v1 Beta packages:
@mastra/core@beta,@mastra/hono@beta - A mature framework (score 19+ on 24-point evaluation)
The Conversion States
C1: No Framework Analysis
Symptoms: Jumping to code without understanding framework structure. Test: Can you list diagnostic states, vocabulary terms, and process phases? Intervention: Run framework analysis first. Extract structure systematically.
C2: States Without Tools
Symptoms: Agent exists but can't diagnose. Framework states not exposed. Test: Can the agent identify which diagnostic state applies to input? Intervention: Convert each diagnostic state to a tool with structured output.
C3: Tools Without Workflow
Symptoms: Tools exist but no orchestration. Multi-step processes are manual. Test: Are framework phases connected as workflow steps? Intervention: Map framework process to workflow with proper data flow.
C4: No Structured Output
Symptoms: Agent returns prose instead of actionable structure. Test: Can consumers programmatically parse agent responses? Intervention: Add Zod output schemas matching framework vocabulary.
C5: No Memory Integration
Symptoms: Agent forgets context between calls. No persistence. Test: Does agent maintain conversation context? Access prior research? Intervention: Configure memory threads, integrate RAG for knowledge persistence.
C6: No API Design
Symptoms: Agent works but has no clean API interface. Test: Can external systems easily call this agent? Intervention: Design Hono routes, document endpoints, add OpenAPI spec.
C7: Not Deployed
Symptoms: Everything works locally but isn't accessible. Test: Can others access this agent via network? Intervention: Containerize, deploy to cloud, configure hosting.
C8: Conversion Complete
Symptoms: Framework is fully operationalized as deployed agent. Indicators: API accessible, structured outputs, memory working, documented.
Framework Analysis Process
Before any code, extract these from the framework:
1. Diagnostic States
Each state becomes a potential tool or structured output:
| State ID | Name | Symptoms | Test | Intervention |
|---|---|---|---|---|
| [from framework] | [state name] | [what user notices] | [how to assess] | [what to apply] |
2. Vocabulary to Schemas
Framework terms become typed structures:
// From framework vocabulary
const VocabularyTermSchema = z.object({
term: z.string(),
definition: z.string(),
depth: z.enum(["introductory", "working", "expert"]),
domain: z.string(),
});3. Processes to Workflows
Framework phases become workflow steps:
Phase 0: Analysis → Step: analyze-input
Phase 1: Expansion → Step: expand-queries
Phase 2: Synthesis → Step: synthesize-findings4. Context Integration to Memory
Framework persistence patterns map to:
- Vocabulary maps → Vector storage (RAG)
- Conversation context → Thread-based memory
- Prior research → Knowledge retrieval
Mapping Table: Framework to Agent
| Framework Element | Mastra Equivalent | Implementation |
|---|---|---|
| Diagnostic States | Tools with structured output | One tool per state or combined assess-state tool |
| State Assessment | Tool that returns state ID + evidence | Include symptoms matched |
| Intervention Recommendations | Agent instructions + tool suggestions | Dynamic instructions |
| Process Phases | Workflow steps | Sequential with schema matching |
| Vocabulary | Zod schemas + type definitions | Shared types across tools |
| Anti-patterns | Validation logic in tools | Prevent known failure modes |
| Completion Criteria | Workflow output validation | Exit conditions |
| Context Integration | Memory + RAG | Thread + vector storage |
| Health Check Questions | Agent self-reflection tool | Metacognitive assessment |
Conversion Process
Step 1: Analyze Framework Structure
Read the framework SKILL.md and extract:
- All diagnostic states with symptoms, tests, interventions
- All vocabulary terms with definitions
- All process phases with inputs/outputs
- All integration points with other skills
Output: Framework analysis JSON (see examples/research-agent/analysis.json)
Step 2: Design Agent Architecture
Decide on structure:
- Single agent with multiple tools: For simpler frameworks
- Multiple specialized agents: For complex framework clusters
- Agent network: For frameworks that coordinate multiple perspectives
Step 3: Generate Zod Schemas
Create schemas for:
- Each diagnostic state output
- Framework vocabulary terms
- Workflow step inputs/outputs
- API request/response bodies
Step 4: Implement Diagnostic Tools
For each diagnostic state or combined assessment:
export const assessState = createTool({
id: "assess-state",
description: "Assess current state and recommend intervention",
inputSchema: z.object({
situation: z.string().describe("Description of current situation"),
}),
outputSchema: z.object({
state: StateIdSchema,
stateName: z.string(),
symptomsMatched: z.array(z.string()),
confidence: z.number(),
recommendedIntervention: z.string(),
nextActions: z.array(z.string()),
}),
execute: async (inputData, context) => {
// Assessment logic using framework criteria
},
});Step 5: Wire Workflow
Connect process phases as workflow steps:
const frameworkWorkflow = createWorkflow({
id: "framework-process",
inputSchema: WorkflowInputSchema,
outputSchema: WorkflowOutputSchema,
})
.then(phase0Step)
.then(phase1Step)
.then(synthesisStep)
.commit();Critical: Ensure schema matching between steps. See references/mastra-workflow-data-flow.md.
Step 6: Configure Memory
Set up persistence for framework context:
// Thread per topic/session
const thread = `${userId}-${frameworkId}-${topicId}`;
// Vector storage for vocabulary maps
await mastra.vectors?.default.upsert({
indexName: "vocabulary-maps",
vectors: vocabularyVectors,
});Step 7: Design API
Create endpoints for framework operations:
// Assessment endpoint
registerApiRoute("/diagnose", {
method: "POST",
handler: async (c) => {
const { situation } = await c.req.json();
const result = await assessTool.execute({ situation }, context);
return c.json(result);
},
});
// Workflow endpoint
registerApiRoute("/process", {
method: "POST",
handler: async (c) => {
const input = await c.req.json();
const run = frameworkWorkflow.createRun();
const result = await run.start({ inputData: input });
return c.json(result);
},
});Step 8: Deploy
Containerize and deploy:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]Anti-Patterns
The Monolithic Agent
Problem: Cramming entire framework into one agent's instructions. Symptoms: Instructions exceed 2000 words, agent confused about scope. Fix: Split diagnostic assessment, interventions, and processes into separate tools/workflows.
The Schema Orphan
Problem: Framework vocabulary exists but no Zod schemas defined. Symptoms: Agent returns inconsistent structures, consumers can't parse. Fix: Every framework term with semantic meaning needs a typed schema.
The State Black Box
Problem: Agent identifies states but doesn't explain reasoning. Symptoms: Users don't trust assessment, can't verify correctness. Fix: Include evidence in structured output: which symptoms matched, which didn't.
The Memory Amnesiac
Problem: Agent doesn't persist vocabulary maps or prior findings. Symptoms: Starting from scratch each session, repeating work. Fix: Configure RAG storage for accumulated knowledge, conversation threads for context.
The API Afterthought
Problem: Agent works but API is poorly designed. Symptoms: Consumers struggle to integrate, endpoints are inconsistent. Fix: Design API contracts before implementation. Consider consumer needs.
The Workflow Spaghetti
Problem: Workflow steps don't match schemas, data flow is broken. Symptoms: Runtime errors, steps receive undefined inputs. Fix: Validate schema matching at design time. Use .map() for transformations.
Available Scripts
analyze-framework.ts
Extract structure from framework/skill document.
deno run --allow-read scripts/analyze-framework.ts path/to/SKILL.mdscaffold-framework-agent.ts
Generate full project structure from framework analysis.
deno run --allow-all scripts/scaffold-framework-agent.ts \
--framework research \
--analysis ./analysis.json \
--output ./agents/research-agentgenerate-diagnostic-tools.ts
Auto-generate tool files from diagnostic states.
deno run --allow-all scripts/generate-diagnostic-tools.ts \
--states ./states.json \
--output ./src/mastra/tools/generate-schemas.ts
Generate Zod schemas from vocabulary.
deno run --allow-all scripts/generate-schemas.ts \
--vocabulary ./vocabulary.json \
--output ./src/schemas/validate-conversion.ts
Check conversion completeness against checklist.
deno run --allow-read scripts/validate-conversion.ts ./agents/research-agentExample: Research Framework Conversion
See examples/research-agent/ for complete worked example.
Framework Analysis Summary
The research skill has:
- 10 diagnostic states (R1-R10): No Analysis, No Vocabulary Map, Single-Perspective, Domain Blindness, Recency Bias, Breadth Without Depth, Completion Uncertainty, Research Complete, No Persistence, Scope Mismatch, No Confidence Signaling
- Key vocabulary: Phase 0 Analysis, Vocabulary Map, Core Terms, Depth Levels, Diminishing Returns, Single-Shot Research, Scope Calibration, Confidence Markers
- Process phases: Phase 0 (Analysis) → Phase 1.5 (Vocabulary) → Phase 2 (Query Construction) → Synthesis
- Integration points: context-networks (storage), fact-check (verification)
Generated Agent Structure
research-agent/
├── src/mastra/
│ ├── agents/research-agent.ts # Main agent with framework instructions
│ ├── tools/
│ │ ├── assess-state.ts # Diagnose research state (R1-R10)
│ │ ├── run-phase0.ts # Execute Phase 0 analysis
│ │ ├── build-vocabulary.ts # Build vocabulary map
│ │ ├── expand-queries.ts # Generate search queries
│ │ ├── retrieve-prior.ts # Get prior research/vocabulary
│ │ └── synthesize.ts # Create synthesis with confidence
│ ├── workflows/
│ │ ├── full-research.ts # Complete research workflow
│ │ └── single-shot.ts # Time-boxed research workflow
│ └── index.ts # Mastra instance
├── src/schemas/
│ ├── vocabulary.ts # VocabularyMapSchema
│ ├── analysis.ts # Phase0AnalysisSchema
│ ├── synthesis.ts # ResearchSynthesisSchema
│ └── states.ts # DiagnosticStateSchema
├── src/index.ts # Hono server
└── DockerfileAPI Endpoints
POST /api/agents/research-agent/diagnose
Body: { "situation": "I'm researching X but keep finding surface-level content" }
Response: { "state": "R1.5", "stateName": "No Vocabulary Map", "intervention": "Build vocabulary map", ... }
POST /api/agents/research-agent/start-research
Body: { "topic": "gentrification of workwear", "timeBox": "2h", "depth": "working" }
Response: { "workflowRunId": "...", "status": "started" }
GET /api/workflows/full-research/{runId}/status
Response: { "status": "running", "currentStep": "vocabulary-mapping", ... }
POST /api/agents/research-agent/synthesize
Body: { "topic": "...", "findings": [...], "confidenceLevel": "medium" }
Response: { "synthesis": {...}, "confidenceLevel": "medium", "caveats": [...] }Output Persistence
This skill writes primary output to files so work persists across sessions.
Output Discovery
Before doing any other work:
1. Check for context/output-config.md in the project 2. If found, look for this skill's entry 3. If not found or no entry for this skill, ask the user first:
- "Where should I save output from this framework-to-mastra session?"
- Suggest:
agents/or a sensible location for agent code
4. Store the user's preference:
- In
context/output-config.mdif context network exists - In
.framework-to-mastra-output.mdat project root otherwise
Primary Output
For this skill, persist:
- Framework analysis - extracted states, vocabulary, processes
- Generated agent code - full project structure
- Schema definitions - Zod schemas for framework vocabulary
- Conversion checklist - validation of completeness
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
| Analysis JSON | Discussion of framework structure |
| Generated code files | Iteration on design |
| Schema definitions | Real-time feedback |
| Validation reports | Deployment decisions |
File Naming
Pattern: {framework-name}-agent/ (directory structure) Example: agents/research-agent/
What You Do NOT Do
- You do not convert immature frameworks (refine first using skill-builder)
- You do not skip framework analysis
- You do not hard-code what should be configurable
- You do not deploy without testing
- You do not ignore schema matching in workflows
- You guide the conversion; the user decides what to deploy
Integration Points
| Skill | Connection |
|---|---|
| skill-builder | Use to refine framework before conversion |
| context-networks | Agent memory maps to context network structure |
| research | Primary worked example for conversion |
| story-sense | Complex diagnostic example with multiple states |
Additional Resources
Reference Files
- `references/framework-analysis.md` - How to analyze a framework for conversion
- `references/diagnostic-to-tool.md` - Converting diagnostic states to tools
- `references/process-to-workflow.md` - Converting processes to workflows
- `references/vocabulary-to-schema.md` - Framework vocabulary to Zod schemas
- `references/context-to-memory.md` - Context networks to agent memory
- `references/api-design-patterns.md` - API design for framework agents
- `references/deployment-patterns.md` - Hosting and containerization
- `references/mastra-core-patterns.md` - Mastra v1 Beta fundamentals
- `references/mastra-workflow-data-flow.md` - Critical workflow patterns
- `references/mastra-server-patterns.md` - Hono server setup
- `references/mastra-memory-patterns.md` - RAG and conversation memory
Asset Templates
- `assets/framework-agent-template.ts` - Agent template
- `assets/diagnostic-tool-template.ts` - Diagnostic tool template
- `assets/intervention-tool-template.ts` - Intervention tool template
- `assets/framework-workflow-template.ts` - Workflow template
- `assets/context-memory-template.ts` - Memory/RAG setup
- `assets/framework-server-template.ts` - Hono server template
- `assets/zod-schema-examples.ts` - Common schema patterns
Mastra Version Note
Target version: Mastra v1 Beta (stable release expected January 2026)
Patterns in this skill are for v1 Beta. Stable (0.24.x) patterns differ significantly - especially tool signatures and workflow data access.
/**
* Diagnostic Tool Template (Mastra v1 Beta)
*
* Template for converting framework diagnostic states into assessment tools.
*
* Customization points marked with {{PLACEHOLDER}}:
* - {{FRAMEWORK_NAME}}: kebab-case framework identifier
* - {{STATE_ENUM}}: Zod enum of state IDs
* - {{STATE_DETAILS}}: Lookup object with state details
* - {{ASSESSMENT_LOGIC}}: Custom assessment logic
*/
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
// State ID enum - replace with actual states
const StateIdSchema = z.enum([
// {{STATE_ENUM}}
"STATE_1",
"STATE_2",
"STATE_3",
// ... add all states
]);
// State details lookup
const STATE_DETAILS: Record<
z.infer<typeof StateIdSchema>,
{
name: string;
symptoms: string[];
test: string;
intervention: string;
}
> = {
// {{STATE_DETAILS}}
STATE_1: {
name: "Example State",
symptoms: ["Symptom 1", "Symptom 2"],
test: "Can you...?",
intervention: "Apply intervention X",
},
// ... add all states
};
// Assessment output schema
const AssessmentOutputSchema = z.object({
primaryState: StateIdSchema,
primaryStateName: z.string(),
confidence: z.number().min(0).max(1),
allStatesAssessed: z.array(
z.object({
stateId: StateIdSchema,
stateName: z.string(),
score: z.number().min(0).max(1),
symptomsMatched: z.array(z.string()),
})
),
recommendedIntervention: z.string(),
nextActions: z.array(z.string()),
isComplete: z.boolean(),
});
/**
* Assess current state against all diagnostic states.
* Returns the primary (highest-scoring) state with intervention recommendation.
*/
export const assess{{FRAMEWORK_NAME}}State = createTool({
id: "assess-{{FRAMEWORK_NAME}}-state",
description:
"Assess current situation against {{FRAMEWORK_NAME}} diagnostic states. " +
"Returns primary state, confidence score, matched symptoms, and recommended intervention. " +
"Use when evaluating where the user is in the {{FRAMEWORK_NAME}} process.",
inputSchema: z.object({
situation: z
.string()
.min(10)
.describe("Description of the current situation"),
workDone: z
.array(z.string())
.optional()
.describe("Actions already taken"),
priorState: StateIdSchema.optional().describe(
"Previously assessed state, if known"
),
}),
outputSchema: AssessmentOutputSchema,
execute: async (inputData, context) => {
const { situation, workDone = [], priorState } = inputData;
const { mastra, runtimeContext } = context;
// Get diagnostician agent for nuanced assessment
const diagnostician = mastra?.getAgent("{{FRAMEWORK_NAME}}-diagnostician");
if (!diagnostician) {
// Fallback: simple keyword-based assessment
return simpleAssessment(situation, workDone);
}
// Build assessment prompt with framework knowledge
const stateDescriptions = Object.entries(STATE_DETAILS)
.map(
([id, details]) =>
`${id}: ${details.name}\n Symptoms: ${details.symptoms.join(", ")}\n Test: ${details.test}`
)
.join("\n\n");
const prompt = `Assess this situation against the diagnostic states:
Situation: ${situation}
Work done: ${workDone.join(", ") || "None specified"}
${priorState ? `Prior state: ${priorState}` : ""}
Diagnostic States:
${stateDescriptions}
For each state, score 0-1 how well symptoms match.
Identify specific symptoms that match.
Return the highest-scoring non-complete state as primary.
Provide specific next actions.`;
const result = await diagnostician.generate(prompt, {
output: AssessmentOutputSchema,
runtimeContext,
});
return result.object;
},
});
/**
* Simple fallback assessment without LLM.
* Uses keyword matching for basic state detection.
*/
function simpleAssessment(
situation: string,
workDone: string[]
): z.infer<typeof AssessmentOutputSchema> {
const situationLower = situation.toLowerCase();
const scores: Array<{
stateId: z.infer<typeof StateIdSchema>;
stateName: string;
score: number;
symptomsMatched: string[];
}> = [];
for (const [stateId, details] of Object.entries(STATE_DETAILS)) {
const matched: string[] = [];
let score = 0;
for (const symptom of details.symptoms) {
// Simple keyword check
const keywords = symptom.toLowerCase().split(" ");
const matchCount = keywords.filter((k) =>
situationLower.includes(k)
).length;
if (matchCount > keywords.length * 0.3) {
matched.push(symptom);
score += 1 / details.symptoms.length;
}
}
scores.push({
stateId: stateId as z.infer<typeof StateIdSchema>,
stateName: details.name,
score,
symptomsMatched: matched,
});
}
// Sort by score descending
scores.sort((a, b) => b.score - a.score);
const primary = scores[0];
const stateDetails = STATE_DETAILS[primary.stateId];
return {
primaryState: primary.stateId,
primaryStateName: primary.stateName,
confidence: primary.score,
allStatesAssessed: scores,
recommendedIntervention: stateDetails.intervention,
nextActions: [`Apply: ${stateDetails.intervention}`],
isComplete: false, // Adjust based on "complete" state ID
};
}
/*
// Usage:
const result = await assess{{FRAMEWORK_NAME}}State.execute(
{
situation: "I'm researching X but keep finding surface-level content",
workDone: ["Searched Google", "Read Wikipedia"],
},
{ mastra }
);
console.log(`State: ${result.primaryState} - ${result.primaryStateName}`);
console.log(`Confidence: ${result.confidence}`);
console.log(`Intervention: ${result.recommendedIntervention}`);
*/
/**
* Framework Agent Template (Mastra v1 Beta)
*
* Template for converting an operating-framework skill into a Mastra agent.
*
* Customization points marked with {{PLACEHOLDER}}:
* - {{FRAMEWORK_NAME}}: kebab-case framework identifier
* - {{FRAMEWORK_TITLE}}: Human-readable framework name
* - {{DIAGNOSTIC_STATES}}: Formatted list of diagnostic states
* - {{TOOL_IMPORTS}}: Import statements for framework tools
* - {{TOOLS_OBJECT}}: Object of available tools
*/
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
// {{TOOL_IMPORTS}}
// Example:
// import { assessStateTool } from "../tools/assess-state.js";
// import { runPhase0Tool } from "../tools/run-phase0.js";
export const {{FRAMEWORK_NAME}}Agent = new Agent({
name: "{{FRAMEWORK_NAME}}-agent",
// Dynamic instructions can access runtimeContext
instructions: ({ runtimeContext }) => {
const userId = runtimeContext?.get("user-id") || "unknown";
const sessionTopic = runtimeContext?.get("topic") || "general";
return `You are a {{FRAMEWORK_TITLE}} expert agent.
Your role is to:
1. Diagnose the current state of the user's situation
2. Recommend appropriate interventions based on framework methodology
3. Guide users through the framework's process phases
4. Track progress and maintain context across sessions
## Diagnostic States
You assess situations against these states:
{{DIAGNOSTIC_STATES}}
## Process
When helping users:
1. First assess which diagnostic state applies
2. Explain the assessment with evidence (which symptoms matched)
3. Recommend the appropriate intervention
4. Guide them through the intervention steps
5. Re-assess after intervention to confirm progress
## Guidelines
- Always explain your reasoning
- Provide concrete, actionable next steps
- Use the framework's vocabulary precisely
- Check for prior work before starting fresh
- Signal confidence levels explicitly
## Current Context
User: ${userId}
Topic: ${sessionTopic}
Start by understanding the user's current situation, then diagnose and guide.`;
},
// Model configuration
model: openai("gpt-4o-mini"),
// Alternative: model with fallbacks
// model: [
// { model: "openai/gpt-4o", maxRetries: 3 },
// { model: "anthropic/claude-3-5-sonnet", maxRetries: 2 },
// ],
// Framework tools
tools: {
// {{TOOLS_OBJECT}}
// Example:
// assessStateTool,
// runPhase0Tool,
// buildVocabularyTool,
// retrievePriorWorkTool,
},
});
/*
// Usage example:
import { Mastra } from "@mastra/core/mastra";
import { RuntimeContext } from "@mastra/core";
const mastra = new Mastra({
agents: { {{FRAMEWORK_NAME}}Agent },
});
// With runtime context
const runtimeContext = new RuntimeContext();
runtimeContext.set("user-id", "user-123");
runtimeContext.set("topic", "gentrification of workwear");
const response = await {{FRAMEWORK_NAME}}Agent.generate(
"I'm researching this topic but keep finding surface-level content",
{
runtimeContext,
memory: {
thread: "research-user-123-gentrification-of-workwear",
resource: "user-123",
},
}
);
console.log(response.text);
*/
/**
* Framework Server Template (Mastra v1 Beta + Hono)
*
* Template for setting up a Hono server with framework agent endpoints.
*
* Customization points marked with {{PLACEHOLDER}}:
* - {{FRAMEWORK_NAME}}: kebab-case framework identifier
* - {{AGENT_IMPORT}}: Import statement for framework agent
* - {{WORKFLOW_IMPORT}}: Import statement for framework workflow
* - {{TOOL_IMPORTS}}: Import statements for framework tools
*/
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { bearerAuth } from "hono/bearer-auth";
import { MastraServer } from "@mastra/hono";
import { Mastra } from "@mastra/core/mastra";
import { RuntimeContext } from "@mastra/core";
import { LibSQLStore } from "@mastra/libsql";
import { z } from "zod";
// {{AGENT_IMPORT}}
// import { researchAgent } from "./mastra/agents/research-agent.js";
// {{WORKFLOW_IMPORT}}
// import { researchWorkflow } from "./mastra/workflows/research-workflow.js";
// {{TOOL_IMPORTS}}
// import { assessStateTool, runPhase0Tool } from "./mastra/tools/index.js";
// ============================================================================
// Mastra Instance Configuration
// ============================================================================
const mastra = new Mastra({
agents: {
// {{FRAMEWORK_NAME}}Agent,
},
workflows: {
// {{FRAMEWORK_NAME}}Workflow,
},
storage: new LibSQLStore({
url: process.env.LIBSQL_URL || "file:./data/mastra.db",
}),
// Optional: Vector store for RAG
// vectors: {
// default: new PgVector({ connectionString: process.env.DATABASE_URL }),
// },
});
// ============================================================================
// Hono App Setup
// ============================================================================
const app = new Hono();
// Middleware
app.use("*", logger());
app.use(
"*",
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || ["http://localhost:3000"],
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization", "X-User-Id"],
})
);
// Authentication (optional - uncomment to enable)
// app.use("/api/*", bearerAuth({ token: process.env.API_TOKEN! }));
// ============================================================================
// Initialize Mastra Server
// ============================================================================
const server = new MastraServer({
app,
mastra,
prefix: "/api",
});
await server.init();
// ============================================================================
// Custom Framework Endpoints
// ============================================================================
// Request schemas
const DiagnoseRequestSchema = z.object({
situation: z.string().min(10),
topic: z.string().optional(),
workDone: z.array(z.string()).optional(),
});
const InterveneRequestSchema = z.object({
stateId: z.string(),
topic: z.string(),
context: z.record(z.unknown()).optional(),
});
const StartProcessRequestSchema = z.object({
topic: z.string(),
depth: z.enum(["quick", "working", "expert"]).default("working"),
context: z.record(z.unknown()).optional(),
});
// Diagnose endpoint
app.post("/api/diagnose", async (c) => {
const body = await c.req.json();
const parsed = DiagnoseRequestSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: "Validation failed", details: parsed.error.issues }, 400);
}
const userId = c.req.header("x-user-id") || "anonymous";
// Create runtime context
const runtimeContext = new RuntimeContext();
runtimeContext.set("user-id", userId);
// Get assessment tool and execute
const assessTool = mastra.getTool("assess-{{FRAMEWORK_NAME}}-state");
if (!assessTool) {
return c.json({ error: "Assessment tool not found" }, 500);
}
const result = await assessTool.execute(parsed.data, { mastra, runtimeContext });
return c.json({
...result,
timestamp: new Date().toISOString(),
});
});
// Intervene endpoint
app.post("/api/intervene", async (c) => {
const body = await c.req.json();
const parsed = InterveneRequestSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: "Validation failed", details: parsed.error.issues }, 400);
}
const { stateId, topic, context } = parsed.data;
const userId = c.req.header("x-user-id") || "anonymous";
const runtimeContext = new RuntimeContext();
runtimeContext.set("user-id", userId);
runtimeContext.set("topic", topic);
// Map state to intervention tool
const interventionMap: Record<string, string> = {
// Map state IDs to intervention tool IDs
// "R1": "run-phase0-analysis",
// "R1.5": "build-vocabulary-map",
};
const toolId = interventionMap[stateId];
if (!toolId) {
return c.json({ error: `No intervention for state: ${stateId}` }, 400);
}
const tool = mastra.getTool(toolId);
if (!tool) {
return c.json({ error: `Intervention tool not found: ${toolId}` }, 500);
}
const result = await tool.execute({ topic, ...context }, { mastra, runtimeContext });
return c.json({
stateId,
interventionApplied: true,
result,
timestamp: new Date().toISOString(),
});
});
// Start workflow endpoint
app.post("/api/process/start", async (c) => {
const body = await c.req.json();
const parsed = StartProcessRequestSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: "Validation failed", details: parsed.error.issues }, 400);
}
const workflow = mastra.getWorkflow("{{FRAMEWORK_NAME}}-workflow");
if (!workflow) {
return c.json({ error: "Workflow not found" }, 500);
}
const run = workflow.createRun();
// Start async
run.start({ inputData: parsed.data }).catch((err) => {
console.error("Workflow error:", err);
});
return c.json({
runId: run.id,
status: "started",
statusUrl: `/api/process/${run.id}/status`,
});
});
// Workflow status endpoint
app.get("/api/process/:runId/status", async (c) => {
const runId = c.req.param("runId");
const workflow = mastra.getWorkflow("{{FRAMEWORK_NAME}}-workflow");
if (!workflow) {
return c.json({ error: "Workflow not found" }, 500);
}
const run = await workflow.getRun(runId);
if (!run) {
return c.json({ error: "Run not found" }, 404);
}
return c.json({
runId,
status: run.status,
currentStep: run.currentStep,
output: run.status === "completed" ? run.output : undefined,
error: run.status === "failed" ? run.error : undefined,
});
});
// Chat endpoint
app.post("/api/chat", async (c) => {
const body = await c.req.json();
const { message, threadId } = body;
const userId = c.req.header("x-user-id") || "anonymous";
const thread = threadId || `chat-${userId}-${Date.now()}`;
const runtimeContext = new RuntimeContext();
runtimeContext.set("user-id", userId);
const agent = mastra.getAgent("{{FRAMEWORK_NAME}}-agent");
if (!agent) {
return c.json({ error: "Agent not found" }, 500);
}
const result = await agent.generate(message, {
runtimeContext,
memory: {
thread,
resource: userId,
},
});
return c.json({
response: result.text,
threadId: thread,
});
});
// ============================================================================
// Health Endpoints
// ============================================================================
app.get("/health", async (c) => {
const checks: Record<string, string> = {};
// Check storage
try {
await mastra.storage?.getThreads({ limit: 1 });
checks.storage = "healthy";
} catch {
checks.storage = "unhealthy";
}
const allHealthy = Object.values(checks).every((v) => v === "healthy");
return c.json(
{
status: allHealthy ? "healthy" : "degraded",
checks,
timestamp: new Date().toISOString(),
},
allHealthy ? 200 : 503
);
});
app.get("/ready", (c) => c.json({ ready: true }));
// ============================================================================
// Error Handling
// ============================================================================
app.onError((err, c) => {
console.error("Server error:", err);
if (err.name === "ZodError") {
return c.json({ error: "Validation error", details: err.issues }, 400);
}
return c.json(
{
error: "Internal server error",
message: process.env.NODE_ENV === "development" ? err.message : undefined,
},
500
);
});
app.notFound((c) => {
return c.json({ error: "Endpoint not found" }, 404);
});
// ============================================================================
// Start Server
// ============================================================================
const port = parseInt(process.env.PORT || "3000");
serve({
fetch: app.fetch,
port,
});
console.log(`{{FRAMEWORK_NAME}} agent server running on http://localhost:${port}`);
// Graceful shutdown
const shutdown = async () => {
console.log("Shutting down gracefully...");
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
/**
* Framework Workflow Template (Mastra v1 Beta)
*
* Template for converting framework process phases into Mastra workflows.
*
* CRITICAL: Schema matching between steps is the most error-prone area!
* - Workflow inputSchema → Step 1 inputSchema: MUST match
* - Step N outputSchema → Step N+1 inputSchema: MUST match
* - Final step outputSchema → Workflow outputSchema: MUST match
*
* Customization points marked with {{PLACEHOLDER}}:
* - {{WORKFLOW_NAME}}: kebab-case workflow identifier
* - {{WORKFLOW_TITLE}}: Human-readable workflow name
* - {{STEPS}}: Workflow step definitions
*/
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";
// ============================================================================
// Schema Definitions
// ============================================================================
// Workflow input schema
const WorkflowInputSchema = z.object({
topic: z.string().describe("The subject for this workflow"),
depth: z
.enum(["quick", "working", "expert"])
.default("working")
.describe("Desired depth level"),
context: z.record(z.unknown()).optional(),
});
// Phase 1 output schema (MUST match Phase 2 input)
const Phase1OutputSchema = z.object({
topic: z.string(),
phase1Result: z.unknown(), // Replace with specific schema
});
// Phase 2 output schema (MUST match Phase 3 input)
const Phase2OutputSchema = z.object({
topic: z.string(),
phase1Result: z.unknown(), // Passed through
phase2Result: z.unknown(), // Replace with specific schema
});
// Final output schema (MUST match workflow outputSchema)
const FinalOutputSchema = z.object({
topic: z.string(),
summary: z.string(),
results: z.unknown(),
completedPhases: z.array(z.string()),
processedAt: z.string().datetime(),
});
// ============================================================================
// Step Definitions
// ============================================================================
/**
* Phase 1: [Phase Name]
* Description of what this phase does.
*/
const phase1Step = createStep({
id: "phase-1-name",
// Input MUST match workflow inputSchema (for first step)
inputSchema: WorkflowInputSchema,
outputSchema: Phase1OutputSchema,
execute: async ({ inputData, mastra, runtimeContext }) => {
const { topic, depth, context } = inputData;
// Get agent for this phase
const agent = mastra?.getAgent("phase1-agent");
// Execute phase logic
const result = await agent?.generate(`Phase 1 for: ${topic}`, {
runtimeContext,
});
// Return MUST match outputSchema
return {
topic,
phase1Result: result?.text,
};
},
});
/**
* Phase 2: [Phase Name]
* Description of what this phase does.
*/
const phase2Step = createStep({
id: "phase-2-name",
// Input MUST match previous step's output
inputSchema: Phase1OutputSchema,
outputSchema: Phase2OutputSchema,
execute: async ({ inputData, mastra, getInitData }) => {
// inputData is phase1Step's output
const { topic, phase1Result } = inputData;
// Access original workflow input if needed
const originalInput = getInitData();
// Execute phase logic
const agent = mastra?.getAgent("phase2-agent");
const result = await agent?.generate(
`Phase 2 based on: ${JSON.stringify(phase1Result)}`
);
// Pass through needed data for later steps
return {
topic,
phase1Result, // Pass through
phase2Result: result?.text,
};
},
});
/**
* Synthesis: Combine results
*/
const synthesisStep = createStep({
id: "synthesis",
inputSchema: Phase2OutputSchema,
outputSchema: FinalOutputSchema,
execute: async ({ inputData, mastra, getStepResult }) => {
const { topic, phase1Result, phase2Result } = inputData;
// Can also access any step by ID
const phase1 = getStepResult("phase-1-name");
// Synthesize results
const agent = mastra?.getAgent("synthesizer");
const synthesis = await agent?.generate(
`Synthesize: Phase 1: ${phase1Result}, Phase 2: ${phase2Result}`
);
return {
topic,
summary: synthesis?.text || "",
results: { phase1Result, phase2Result },
completedPhases: ["phase-1-name", "phase-2-name", "synthesis"],
processedAt: new Date().toISOString(),
};
},
});
// ============================================================================
// Workflow Assembly
// ============================================================================
export const {{WORKFLOW_NAME}}Workflow = createWorkflow({
id: "{{WORKFLOW_NAME}}",
inputSchema: WorkflowInputSchema,
outputSchema: FinalOutputSchema,
// Optional: retry configuration
retryConfig: {
attempts: 3,
delay: 1000,
},
})
.then(phase1Step)
.then(phase2Step)
.then(synthesisStep)
.commit();
// ============================================================================
// Parallel Execution Example
// ============================================================================
/*
const parallelStep1 = createStep({
id: "parallel-1",
inputSchema: z.object({ topic: z.string() }),
outputSchema: z.object({ result1: z.string() }),
execute: async ({ inputData }) => ({ result1: "Result 1" }),
});
const parallelStep2 = createStep({
id: "parallel-2",
inputSchema: z.object({ topic: z.string() }),
outputSchema: z.object({ result2: z.string() }),
execute: async ({ inputData }) => ({ result2: "Result 2" }),
});
// After parallel, input is keyed by step ID
const combineStep = createStep({
id: "combine",
inputSchema: z.object({
"parallel-1": z.object({ result1: z.string() }),
"parallel-2": z.object({ result2: z.string() }),
}),
outputSchema: z.object({ combined: z.string() }),
execute: async ({ inputData }) => ({
combined: `${inputData["parallel-1"].result1} + ${inputData["parallel-2"].result2}`,
}),
});
const parallelWorkflow = createWorkflow({...})
.parallel([parallelStep1, parallelStep2])
.then(combineStep)
.commit();
*/
// ============================================================================
// Conditional Branching Example
// ============================================================================
/*
const quickStep = createStep({
id: "quick-path",
outputSchema: z.object({ result: z.string(), path: z.literal("quick") }),
execute: async () => ({ result: "Quick result", path: "quick" }),
});
const deepStep = createStep({
id: "deep-path",
outputSchema: z.object({ result: z.string(), path: z.literal("deep") }),
execute: async () => ({ result: "Deep result", path: "deep" }),
});
// After branch, use .optional()
const afterBranchStep = createStep({
id: "after-branch",
inputSchema: z.object({
"quick-path": z.object({ result: z.string(), path: z.string() }).optional(),
"deep-path": z.object({ result: z.string(), path: z.string() }).optional(),
}),
outputSchema: z.object({ finalResult: z.string() }),
execute: async ({ inputData }) => {
const result = inputData["quick-path"]?.result || inputData["deep-path"]?.result;
return { finalResult: result || "No result" };
},
});
const branchingWorkflow = createWorkflow({...})
.then(assessStep)
.branch([
[async ({ inputData }) => inputData.depth === "quick", quickStep],
[async ({ inputData }) => inputData.depth !== "quick", deepStep],
])
.then(afterBranchStep)
.commit();
*/
// ============================================================================
// Usage
// ============================================================================
/*
import { Mastra } from "@mastra/core/mastra";
const mastra = new Mastra({
workflows: { {{WORKFLOW_NAME}}Workflow },
agents: { phase1Agent, phase2Agent, synthesizerAgent },
});
// Start workflow run
const run = {{WORKFLOW_NAME}}Workflow.createRun();
const result = await run.start({
inputData: {
topic: "gentrification of workwear",
depth: "working",
},
});
console.log(result.status); // "success" | "failed"
console.log(result.output); // FinalOutput type
*/
/**
* Intervention Tool Template (Mastra v1 Beta)
*
* Template for creating intervention tools that apply framework processes.
*
* Customization points marked with {{PLACEHOLDER}}:
* - {{INTERVENTION_NAME}}: kebab-case intervention identifier
* - {{INTERVENTION_TITLE}}: Human-readable intervention name
* - {{STATE_ID}}: State this intervention addresses
* - {{INPUT_SCHEMA}}: Zod schema for intervention inputs
* - {{OUTPUT_SCHEMA}}: Zod schema for intervention outputs
* - {{INTERVENTION_PROMPT}}: Prompt template for agent execution
*/
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
// Input schema for this intervention
// {{INPUT_SCHEMA}}
const InterventionInputSchema = z.object({
topic: z.string().describe("The topic or subject for this intervention"),
context: z
.record(z.unknown())
.optional()
.describe("Additional context from prior steps"),
priorOutput: z
.unknown()
.optional()
.describe("Output from previous intervention, if chaining"),
});
// Output schema for this intervention
// {{OUTPUT_SCHEMA}}
const InterventionOutputSchema = z.object({
success: z.boolean(),
result: z.unknown(), // Replace with specific schema
nextState: z.string().optional().describe("Recommended next state to assess"),
followUpActions: z.array(z.string()),
metadata: z.object({
executedAt: z.string().datetime(),
durationMs: z.number(),
}),
});
/**
* {{INTERVENTION_TITLE}} intervention tool.
* Addresses state {{STATE_ID}}.
*/
export const {{INTERVENTION_NAME}}Tool = createTool({
id: "{{INTERVENTION_NAME}}",
description:
"Execute {{INTERVENTION_TITLE}} intervention. " +
"Use when state {{STATE_ID}} is identified. " +
"Produces [describe output] which enables [next step].",
inputSchema: InterventionInputSchema,
outputSchema: InterventionOutputSchema,
execute: async (inputData, context) => {
const { topic, context: additionalContext, priorOutput } = inputData;
const { mastra, runtimeContext, abortSignal } = context;
const startTime = Date.now();
// Check abort signal
if (abortSignal?.aborted) {
throw new Error("Intervention aborted");
}
// Get specialized agent for this intervention
const agent = mastra?.getAgent("{{INTERVENTION_NAME}}-agent");
if (!agent) {
throw new Error("Intervention agent not found: {{INTERVENTION_NAME}}-agent");
}
// Build intervention prompt
// {{INTERVENTION_PROMPT}}
const prompt = `Execute {{INTERVENTION_TITLE}} for topic: ${topic}
${priorOutput ? `Prior step output:\n${JSON.stringify(priorOutput, null, 2)}` : ""}
${additionalContext ? `Additional context:\n${JSON.stringify(additionalContext, null, 2)}` : ""}
Instructions:
1. [Step 1 of intervention process]
2. [Step 2 of intervention process]
3. [Step 3 of intervention process]
Produce a structured output with:
- [Expected output component 1]
- [Expected output component 2]
- Recommended follow-up actions`;
// Execute with structured output
const response = await agent.generate(prompt, {
output: z.object({
// Define specific output schema here
result: z.unknown(),
followUpActions: z.array(z.string()),
}),
runtimeContext,
});
return {
success: true,
result: response.object.result,
nextState: undefined, // Set based on intervention outcome
followUpActions: response.object.followUpActions,
metadata: {
executedAt: new Date().toISOString(),
durationMs: Date.now() - startTime,
},
};
},
});
/*
// Example: Phase 0 Analysis Intervention
import { z } from "zod";
import { createTool } from "@mastra/core/tools";
const Phase0OutputSchema = z.object({
concepts: z.object({
primaryTerms: z.array(z.string()),
variants: z.array(z.string()),
ambiguous: z.array(z.string()),
}),
stakeholders: z.object({
primary: z.array(z.string()),
affected: z.array(z.string()),
opposing: z.array(z.string()),
}),
temporal: z.object({
origins: z.string(),
transitions: z.array(z.string()),
current: z.string(),
}),
domains: z.object({
primary: z.string(),
adjacent: z.array(z.string()),
}),
controversies: z.object({
activeDebates: z.array(z.string()),
competingFrameworks: z.array(z.string()),
}),
});
export const runPhase0Analysis = createTool({
id: "run-phase0-analysis",
description:
"Execute Phase 0 Topic Analysis. Use when R1 state is identified. " +
"Produces structured topic analysis including concepts, stakeholders, " +
"temporal scope, domains, and controversies.",
inputSchema: z.object({
topic: z.string(),
decisionContext: z.string().optional(),
}),
outputSchema: z.object({
success: z.boolean(),
analysis: Phase0OutputSchema,
followUpActions: z.array(z.string()),
}),
execute: async (inputData, context) => {
const { topic, decisionContext } = inputData;
const { mastra, runtimeContext } = context;
const analyst = mastra?.getAgent("topic-analyst");
const prompt = `Analyze the following research topic using Phase 0 Analysis Template:
Topic: ${topic}
${decisionContext ? `Decision context: ${decisionContext}` : ""}
Structure your analysis:
1. Core Concepts
- Primary terms requiring definition
- Terminology variants (synonyms, jargon, historical terms)
- Ambiguous terms with multiple meanings
2. Stakeholders
- Primary actors directly involved
- Affected groups bearing consequences
- Opposing interests benefiting from different outcomes
3. Temporal Scope
- Historical origins
- Key transitions and change points
- Current state
4. Domains
- Primary field of study
- Adjacent/overlapping fields
5. Controversies
- Active debates
- Competing frameworks or approaches`;
const response = await analyst?.generate(prompt, {
output: Phase0OutputSchema,
runtimeContext,
});
return {
success: true,
analysis: response?.object,
followUpActions: [
"Build vocabulary map from identified concepts",
"Research each identified controversy",
"Map terminology across identified domains",
],
};
},
});
*/
/**
* Zod Schema Examples
*
* Common schema patterns for framework-to-agent conversion.
* Use as reference when creating schemas from framework vocabulary.
*/
import { z } from "zod";
// ============================================================================
// Diagnostic State Schemas
// ============================================================================
/**
* State ID enum pattern
* Replace values with actual state IDs from framework
*/
export const StateIdSchema = z.enum([
"R1",
"R1.5",
"R2",
"R3",
"R4",
"R5",
"R6",
"R7",
"R8",
"R9",
"R10",
]);
/**
* State assessment result
*/
export const StateAssessmentSchema = z.object({
stateId: StateIdSchema,
stateName: z.string(),
applies: z.boolean(),
confidence: z.number().min(0).max(1),
symptomsMatched: z.array(z.string()),
symptomsMissed: z.array(z.string()),
recommendedIntervention: z.string(),
nextActions: z.array(z.string()),
});
/**
* Multi-state assessment (for combined assessment tools)
*/
export const CombinedAssessmentSchema = z.object({
primaryState: StateIdSchema,
primaryStateName: z.string(),
confidence: z.number().min(0).max(1),
allStatesAssessed: z.array(
z.object({
stateId: StateIdSchema,
stateName: z.string(),
score: z.number().min(0).max(1),
symptomsMatched: z.array(z.string()),
})
),
recommendedIntervention: z.string(),
nextActions: z.array(z.string()),
isComplete: z.boolean(),
});
// ============================================================================
// Vocabulary Schemas
// ============================================================================
/**
* Depth/expertise level
*/
export const DepthLevelSchema = z.enum(["introductory", "working", "expert"]);
/**
* Single vocabulary term
*/
export const VocabularyTermSchema = z.object({
term: z.string().describe("The vocabulary term"),
definition: z.string().describe("Precise definition"),
domain: z.string().describe("Field this term belongs to"),
depth: DepthLevelSchema.describe("Expertise level required"),
relatedTerms: z.array(z.string()).optional(),
});
/**
* Cross-domain synonym mapping
*/
export const SynonymMappingSchema = z.object({
concept: z.string().describe("The underlying concept"),
termsByDomain: z
.record(z.string())
.describe("Domain -> term mapping, e.g., { 'psychology': 'confirmation bias', 'economics': 'motivated reasoning' }"),
});
/**
* Full vocabulary map
*/
export const VocabularyMapSchema = z.object({
topic: z.string(),
coreTerms: z.array(VocabularyTermSchema),
synonyms: z.array(SynonymMappingSchema),
depthIndicators: z.object({
introductory: z.array(z.string()),
working: z.array(z.string()),
expert: z.array(z.string()),
}),
lastUpdated: z.string().datetime().optional(),
});
// ============================================================================
// Process Phase Schemas
// ============================================================================
/**
* Phase 0 analysis components (research framework example)
*/
export const CoreConceptsSchema = z.object({
primaryTerms: z.array(z.string()).describe("Key terms requiring definition"),
variants: z.array(z.string()).describe("Synonyms, jargon, historical terms"),
ambiguous: z.array(z.string()).describe("Terms with multiple meanings"),
});
export const StakeholdersSchema = z.object({
primary: z.array(z.string()).describe("Directly involved actors"),
affected: z.array(z.string()).describe("Groups bearing consequences"),
opposing: z.array(z.string()).describe("Those with competing interests"),
});
export const TemporalScopeSchema = z.object({
origins: z.string().describe("When this began"),
transitions: z.array(z.string()).describe("Key change points"),
current: z.string().describe("Present state"),
});
export const DomainsSchema = z.object({
primary: z.string().describe("Main discipline"),
adjacent: z.array(z.string()).describe("Related disciplines"),
});
export const ControversiesSchema = z.object({
activeDebates: z.array(z.string()),
competingFrameworks: z.array(z.string()),
});
/**
* Combined Phase 0 output
*/
export const Phase0AnalysisSchema = z.object({
topic: z.string(),
concepts: CoreConceptsSchema,
stakeholders: StakeholdersSchema,
temporal: TemporalScopeSchema,
domains: DomainsSchema,
controversies: ControversiesSchema,
analyzedAt: z.string().datetime(),
});
// ============================================================================
// Confidence and Source Schemas
// ============================================================================
/**
* Confidence level
*/
export const ConfidenceLevelSchema = z.enum([
"high",
"medium",
"low",
"insufficient",
]);
/**
* Confidence with justification
*/
export const ConfidenceWithDetailsSchema = z.object({
level: ConfidenceLevelSchema,
justification: z.string(),
sourceCount: z.number(),
consensusStatus: z.enum(["strong", "moderate", "weak", "contested"]),
});
/**
* Source reference
*/
export const SourceReferenceSchema = z.object({
title: z.string(),
url: z.string().url().optional(),
type: z.enum(["academic", "practitioner", "official", "encyclopedic", "primary"]),
authority: z.number().min(0).max(1),
accessedAt: z.string().datetime().optional(),
});
/**
* Supported claim (claim with evidence)
*/
export const SupportedClaimSchema = z.object({
claim: z.string(),
confidence: ConfidenceLevelSchema,
sources: z.array(SourceReferenceSchema),
counterEvidence: z.array(z.string()).optional(),
});
// ============================================================================
// Synthesis Schemas
// ============================================================================
/**
* Research synthesis output
*/
export const ResearchSynthesisSchema = z.object({
topic: z.string(),
summary: z.string().describe("Direct answer to the research question"),
confidence: ConfidenceWithDetailsSchema,
keyFindings: z.array(SupportedClaimSchema),
vocabularyMap: VocabularyMapSchema.optional(),
gaps: z.array(z.string()).describe("What remains unknown"),
recommendations: z.array(z.string()),
caveats: z.array(z.string()).describe("Limitations and assumptions"),
synthesizedAt: z.string().datetime(),
});
// ============================================================================
// API Request/Response Schemas
// ============================================================================
/**
* Diagnose request
*/
export const DiagnoseRequestSchema = z.object({
situation: z.string().min(10).describe("Description of current situation"),
topic: z.string().optional(),
workDone: z.array(z.string()).optional().describe("Actions already taken"),
});
/**
* Diagnose response
*/
export const DiagnoseResponseSchema = z.object({
state: StateIdSchema,
stateName: z.string(),
confidence: z.number(),
symptomsMatched: z.array(z.string()),
intervention: z.string(),
nextActions: z.array(z.string()),
timestamp: z.string().datetime(),
});
/**
* Workflow start request
*/
export const StartWorkflowRequestSchema = z.object({
topic: z.string().min(3),
depth: z.enum(["quick", "working", "expert"]).default("working"),
timeBox: z.string().optional(),
context: z.record(z.unknown()).optional(),
});
/**
* Workflow status response
*/
export const WorkflowStatusResponseSchema = z.object({
runId: z.string(),
status: z.enum(["pending", "running", "completed", "failed"]),
currentStep: z.string().optional(),
completedSteps: z.array(z.string()),
output: z.unknown().optional(),
error: z.string().optional(),
startedAt: z.string().datetime(),
completedAt: z.string().datetime().optional(),
});
// ============================================================================
// Type Extraction
// ============================================================================
// Extract TypeScript types from schemas
export type StateId = z.infer<typeof StateIdSchema>;
export type StateAssessment = z.infer<typeof StateAssessmentSchema>;
export type CombinedAssessment = z.infer<typeof CombinedAssessmentSchema>;
export type DepthLevel = z.infer<typeof DepthLevelSchema>;
export type VocabularyTerm = z.infer<typeof VocabularyTermSchema>;
export type VocabularyMap = z.infer<typeof VocabularyMapSchema>;
export type Phase0Analysis = z.infer<typeof Phase0AnalysisSchema>;
export type ConfidenceLevel = z.infer<typeof ConfidenceLevelSchema>;
export type ConfidenceWithDetails = z.infer<typeof ConfidenceWithDetailsSchema>;
export type SourceReference = z.infer<typeof SourceReferenceSchema>;
export type SupportedClaim = z.infer<typeof SupportedClaimSchema>;
export type ResearchSynthesis = z.infer<typeof ResearchSynthesisSchema>;
export type DiagnoseRequest = z.infer<typeof DiagnoseRequestSchema>;
export type DiagnoseResponse = z.infer<typeof DiagnoseResponseSchema>;
export type StartWorkflowRequest = z.infer<typeof StartWorkflowRequestSchema>;
export type WorkflowStatusResponse = z.infer<typeof WorkflowStatusResponseSchema>;
{
"frameworkId": "research",
"frameworkName": "Research Skill",
"states": [
{
"id": "R1",
"name": "No Analysis",
"symptoms": [
"Jumping straight to searching without analyzing the topic"
],
"test": "Can you articulate stakeholders, temporal scope, and domain mapping?",
"intervention": "Run Phase 0 Analysis Template before generating queries"
},
{
"id": "R1.5",
"name": "No Vocabulary Map",
"symptoms": [
"Using outsider/introductory terminology",
"Finding only surface-level material"
],
"test": "Have you identified expert vs. outsider terms? Terms across domains?",
"intervention": "Build vocabulary map. Hunt for 'also known as,' 'technically called' in early sources."
},
{
"id": "R2",
"name": "Single-Perspective Search",
"symptoms": [
"All queries support one viewpoint",
"Missing counterarguments"
],
"test": "Have you explicitly searched for opposing perspectives?",
"intervention": "Generate competing perspectives queries. Search for strongest counterargument."
},
{
"id": "R3",
"name": "Domain Blindness",
"symptoms": [
"Searching only in familiar field",
"Missing cross-disciplinary insights"
],
"test": "Have you mapped terminology variants across fields?",
"intervention": "Identify what adjacent fields call this topic. Search in at least 2 domains."
},
{
"id": "R4",
"name": "Recency Bias",
"symptoms": [
"Only recent sources",
"Missing historical context"
],
"test": "Can you explain when this topic emerged and how it evolved?",
"intervention": "Add historical context queries. Find seminal works."
},
{
"id": "R5",
"name": "Breadth Without Depth",
"symptoms": [
"Many tabs, no synthesis",
"Can't explain core concepts"
],
"test": "Can you define key terms in your own words?",
"intervention": "Apply 3-source rule per perspective. Summarize before searching more."
},
{
"id": "R6",
"name": "Completion Uncertainty",
"symptoms": [
"Unsure whether to continue or stop",
"Research expanding indefinitely"
],
"test": "Can you answer the tiered completion criteria?",
"intervention": "Run completion checklist. Look for diminishing returns signals."
},
{
"id": "R7",
"name": "Research Complete",
"symptoms": [
"Can explain topic, identify uncertainties, and take action"
],
"test": "",
"intervention": "Proceed to synthesis and action."
},
{
"id": "R8",
"name": "No Persistence",
"symptoms": [
"Starting from scratch each session",
"Re-discovering same vocabulary"
],
"test": "Did you check for prior research before starting? Are you storing findings?",
"intervention": "Store vocabulary map, sources, digested notes, and gaps for future use."
},
{
"id": "R9",
"name": "Scope Mismatch",
"symptoms": [
"Over-researching trivial questions",
"Under-researching critical decisions"
],
"test": "Is research depth proportional to decision stakes?",
"intervention": "Apply scope calibration. Match confidence level to decision reversibility and stakes."
},
{
"id": "R10",
"name": "No Confidence Signaling",
"symptoms": [
"Hedging language everywhere",
"Reader can't tell what's certain vs. speculative"
],
"test": "Can reader distinguish established facts from speculation?",
"intervention": "Use explicit confidence markers. State source quality and consensus status."
}
],
"vocabulary": [
{
"term": "Phase 0 Analysis",
"context": "Structured analysis of topic before searching: concepts, stakeholders, temporal scope, domains, controversies",
"depth": "expert"
},
{
"term": "Vocabulary Map",
"context": "Primary research deliverable mapping expert vs. outsider terms, cross-domain synonyms, depth indicators",
"depth": "expert"
},
{
"term": "Core Terms",
"context": "Essential vocabulary requiring precise definition within the research domain",
"depth": "working"
},
{
"term": "Depth Levels",
"context": "Introductory, working, expert - levels of terminology that surface different content",
"depth": "expert"
},
{
"term": "Diminishing Returns",
"context": "Signals that research is complete: circular references, repetitive findings, marginal additions",
"depth": "working"
},
{
"term": "Single-Shot Research",
"context": "Research completed without follow-up questions - agent-executed, time-boxed queries",
"depth": "expert"
},
{
"term": "Scope Calibration",
"context": "Matching research depth to decision stakes: reversibility, consequences, time available",
"depth": "expert"
},
{
"term": "Confidence Markers",
"context": "Explicit phrases signaling certainty level: established fact, strong evidence, limited evidence, unknown",
"depth": "working"
},
{
"term": "Stakeholder Analysis",
"context": "Mapping primary actors, affected groups, opposing interests",
"depth": "working"
},
{
"term": "Domain Mapping",
"context": "Identifying primary and adjacent fields that study this topic",
"depth": "working"
},
{
"term": "Query Types",
"context": "Foundational, historical, current, competing, evidence - different search approaches",
"depth": "working"
},
{
"term": "Completion Criteria",
"context": "Minimum viable, working knowledge, deep expertise - tiered research stopping points",
"depth": "expert"
}
],
"processes": [
{
"phaseId": "0",
"name": "Topic Analysis",
"description": "Analyze topic through multiple lenses before searching: core concepts, stakeholders, temporal context, domains, controversies"
},
{
"phaseId": "1.5",
"name": "Vocabulary Mapping",
"description": "Build vocabulary map as primary deliverable: core terms, synonyms, depth indicators"
},
{
"phaseId": "2",
"name": "Query Construction",
"description": "Generate specific search queries: foundational, historical, current, competing, evidence"
}
],
"integrations": [
{
"skill": "context-networks",
"connectionType": "storage",
"description": "Store research findings in appropriate network node"
},
{
"skill": "doppelganger",
"connectionType": "verification",
"description": "Apply /truth-check to findings"
}
],
"antiPatterns": [
{
"name": "Confirmation Trap",
"symptom": "Searching for evidence that supports existing belief",
"fix": "Explicitly search for strongest counterargument"
},
{
"name": "Authority Fallacy",
"symptom": "Accepting claims by source prestige rather than evidence",
"fix": "Evaluate evidence, not source"
},
{
"name": "Recency Trap",
"symptom": "Only recent sources, missing foundational work",
"fix": "Explicitly search historical periods"
},
{
"name": "Breadth Trap",
"symptom": "50 tabs, none read thoroughly",
"fix": "3-source rule, summarize before continuing"
},
{
"name": "Single-Source",
"symptom": "Wikipedia as final answer",
"fix": "Require 3 independent sources"
},
{
"name": "Jargon Blind Spot",
"symptom": "Missing other fields' terminology",
"fix": "Map variants, search multiple domains"
},
{
"name": "Infinite Rabbit Hole",
"symptom": "Lost original purpose",
"fix": "Write decision/action anchor, return to it"
}
],
"analyzedAt": "2025-12-28T00:00:00.000Z"
}
Research Agent - Worked Example
This example demonstrates converting the research framework into a deployed Mastra agent.
Framework Analysis
The research skill has:
Diagnostic States (10)
| ID | Name | Key Symptom |
|---|---|---|
| R1 | No Analysis | Jumping to searching without topic analysis |
| R1.5 | No Vocabulary Map | Using outsider terminology |
| R2 | Single-Perspective Search | All queries support one viewpoint |
| R3 | Domain Blindness | Searching only in familiar field |
| R4 | Recency Bias | Only recent sources |
| R5 | Breadth Without Depth | Many tabs, no synthesis |
| R6 | Completion Uncertainty | Unsure when to stop |
| R7 | Research Complete | Can explain and act |
| R8 | No Persistence | Starting from scratch each session |
| R9 | Scope Mismatch | Over/under-researching relative to stakes |
| R10 | No Confidence Signaling | Hedging everywhere |
Process Phases
1. Phase 0: Topic Analysis (concepts, stakeholders, temporal, domains, controversies) 2. Phase 1.5: Vocabulary Mapping (expert vs. outsider terms, cross-domain synonyms) 3. Phase 2: Query Construction (foundational, historical, current, competing, evidence) 4. Synthesis: Confidence-calibrated output
Key Vocabulary
- Phase 0 Analysis
- Vocabulary Map
- Core Terms
- Depth Levels (introductory, working, expert)
- Diminishing Returns
- Single-Shot Research
- Scope Calibration
- Confidence Markers
Agent Architecture
research-agent/
├── src/
│ ├── mastra/
│ │ ├── agents/
│ │ │ └── research-agent.ts # Main agent with framework instructions
│ │ ├── tools/
│ │ │ ├── assess-state.ts # Diagnose research state (R1-R10)
│ │ │ ├── run-phase0.ts # Execute Phase 0 analysis
│ │ │ ├── build-vocabulary.ts # Build vocabulary map
│ │ │ ├── expand-queries.ts # Generate search queries
│ │ │ ├── retrieve-prior.ts # Get prior research/vocabulary
│ │ │ └── synthesize.ts # Create synthesis with confidence
│ │ ├── workflows/
│ │ │ ├── full-research.ts # Complete research workflow
│ │ │ └── single-shot.ts # Time-boxed research workflow
│ │ └── index.ts # Mastra instance
│ ├── schemas/
│ │ ├── vocabulary.ts
│ │ ├── analysis.ts
│ │ ├── synthesis.ts
│ │ └── states.ts
│ └── index.ts # Hono server
├── package.json
├── tsconfig.json
└── DockerfileTool Mapping
| Framework Element | Tool |
|---|---|
| Diagnostic states R1-R10 | assess-research-state |
| Phase 0 Analysis | run-phase0-analysis |
| Vocabulary Mapping | build-vocabulary-map |
| Query Construction | expand-queries |
| Prior Research Check | retrieve-prior-research |
| Synthesis | synthesize-research |
Workflow Design
Full Research Workflow
Input: { topic, depth, decisionContext }
↓
[check-prior-research] → Has prior vocabulary?
↓ ↓
No Yes
↓ ↓
[phase-0-analysis] [load-vocabulary]
↓ ↓
[vocabulary-mapping] ←────────┘
↓
[query-expansion]
↓
[synthesize-findings]
↓
Output: { synthesis, vocabularyMap, confidence, gaps }Single-Shot Workflow
Input: { topic, timeBox, stakes }
↓
[calibrate-scope]
↓
[quick-analysis] (compressed Phase 0)
↓
[targeted-search]
↓
[synthesize-with-confidence]
↓
Output: { synthesis, confidence, caveats }API Endpoints
POST /api/diagnose
→ Assess research state, recommend intervention
POST /api/intervene
→ Apply intervention for specific state
POST /api/process/start
→ Start full research workflow
GET /api/process/:runId/status
→ Check workflow progress
POST /api/chat
→ Conversational interaction with research agent
GET /api/vocabulary/:topic
→ Retrieve prior vocabulary mapsMemory Configuration
// Thread per research topic
const thread = `research-${userId}-${topic}`;
// Vocabulary maps in vector store
indexName: "vocabulary-maps"
filter: { userId, topic }
// Research findings in vector store
indexName: "research-findings"
filter: { userId }Deployment
# Build
npm run build
# Run locally
npm run dev
# Docker
docker build -t research-agent .
docker run -p 3000:3000 -e OPENAI_API_KEY=sk-... research-agent
# Deploy to Railway/Fly.io
fly deployUsage Example
# Diagnose current research state
curl -X POST http://localhost:3000/api/diagnose \
-H "Content-Type: application/json" \
-d '{
"situation": "I am researching the gentrification of workwear but keep finding surface-level fashion articles",
"topic": "gentrification of workwear"
}'
# Response:
{
"state": "R1.5",
"stateName": "No Vocabulary Map",
"confidence": 0.85,
"symptomsMatched": ["Using outsider terminology", "Finding only surface-level material"],
"intervention": "Build vocabulary map. Hunt for expert terms in early sources.",
"nextActions": [
"Search for 'technically called' and 'also known as' in current sources",
"Map terms by domain using vocabulary template"
]
}Files in This Example
analysis.json- Framework analysis outputsrc/- Full agent implementation (reference)
To generate your own research agent:
deno run --allow-read scripts/analyze-framework.ts skills/research/SKILL.md --output analysis.jsonAPI Design Patterns
Designing Hono APIs for framework agents.
Core Endpoints
Every framework agent needs these endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/diagnose | POST | Assess current state |
/intervene | POST | Apply intervention for state |
/process/start | POST | Start full workflow |
/process/{runId}/status | GET | Check workflow status |
/health | GET | Health check |
Diagnostic Endpoint
import { registerApiRoute } from "@mastra/core/server";
import { z } from "zod";
import { DiagnoseRequestSchema, DiagnoseResponseSchema } from "../schemas/api.js";
registerApiRoute("/diagnose", {
method: "POST",
handler: async (c) => {
const mastra = c.get("mastra");
const body = await c.req.json();
// Validate input
const parsed = DiagnoseRequestSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: parsed.error.issues }, 400);
}
const { situation, topic, workDone } = parsed.data;
// Get assessment from tool
const assessTool = mastra.getTool("assess-research-state");
const result = await assessTool.execute(
{ situation, topic, workDone },
{ mastra }
);
// Validate output
const response = DiagnoseResponseSchema.parse({
...result,
timestamp: new Date().toISOString(),
});
return c.json(response);
},
});Intervention Endpoint
// Intervention request: state ID + context
const InterveneRequestSchema = z.object({
stateId: ResearchStateSchema,
topic: z.string(),
context: z.record(z.unknown()).optional(),
});
registerApiRoute("/intervene", {
method: "POST",
handler: async (c) => {
const mastra = c.get("mastra");
const body = await c.req.json();
const parsed = InterveneRequestSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: parsed.error.issues }, 400);
}
const { stateId, topic, context } = parsed.data;
// Route to appropriate intervention tool
let result;
switch (stateId) {
case "R1":
const phase0Tool = mastra.getTool("run-phase0-analysis");
result = await phase0Tool.execute({ topic }, { mastra });
break;
case "R1.5":
const vocabTool = mastra.getTool("build-vocabulary-map");
result = await vocabTool.execute({ topic, ...context }, { mastra });
break;
case "R2":
const perspectiveTool = mastra.getTool("expand-perspectives");
result = await perspectiveTool.execute({ topic }, { mastra });
break;
default:
return c.json({ error: `No intervention for state ${stateId}` }, 400);
}
return c.json({
stateId,
interventionApplied: true,
result,
timestamp: new Date().toISOString(),
});
},
});Workflow Endpoints
// Start workflow
registerApiRoute("/process/start", {
method: "POST",
handler: async (c) => {
const mastra = c.get("mastra");
const body = await c.req.json();
const parsed = StartResearchRequestSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: parsed.error.issues }, 400);
}
const workflow = mastra.getWorkflow("research-workflow");
const run = workflow.createRun();
// Start async - don't await completion
run.start({ inputData: parsed.data }).catch(console.error);
return c.json({
runId: run.id,
status: "started",
checkStatusUrl: `/process/${run.id}/status`,
});
},
});
// Check workflow status
registerApiRoute("/process/:runId/status", {
method: "GET",
handler: async (c) => {
const mastra = c.get("mastra");
const runId = c.req.param("runId");
const workflow = mastra.getWorkflow("research-workflow");
const run = await workflow.getRun(runId);
if (!run) {
return c.json({ error: "Run not found" }, 404);
}
return c.json({
runId,
status: run.status,
currentStep: run.currentStep,
completedSteps: Object.keys(run.steps || {}),
output: run.status === "completed" ? run.output : undefined,
error: run.status === "failed" ? run.error : undefined,
});
},
});
// Get workflow result (blocking)
registerApiRoute("/process/:runId/result", {
method: "GET",
handler: async (c) => {
const mastra = c.get("mastra");
const runId = c.req.param("runId");
const timeout = parseInt(c.req.query("timeout") || "30000");
const workflow = mastra.getWorkflow("research-workflow");
// Poll until complete or timeout
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const run = await workflow.getRun(runId);
if (!run) {
return c.json({ error: "Run not found" }, 404);
}
if (run.status === "completed") {
return c.json({ status: "completed", output: run.output });
}
if (run.status === "failed") {
return c.json({ status: "failed", error: run.error }, 500);
}
// Wait before polling again
await new Promise(r => setTimeout(r, 1000));
}
return c.json({ status: "timeout", message: "Workflow still running" }, 202);
},
});Agent Chat Endpoint
For conversational interaction:
registerApiRoute("/chat", {
method: "POST",
handler: async (c) => {
const mastra = c.get("mastra");
const body = await c.req.json();
const userId = c.req.header("x-user-id") || "anonymous";
const { message, topic, threadId } = body;
// Get or create thread
const thread = threadId || `chat-${userId}-${Date.now()}`;
const agent = mastra.getAgent("research-agent");
const result = await agent.generate(message, {
memory: {
thread,
resource: userId,
},
});
return c.json({
response: result.text,
threadId: thread,
usage: result.usage,
});
},
});
// Streaming chat
registerApiRoute("/chat/stream", {
method: "POST",
handler: async (c) => {
const mastra = c.get("mastra");
const body = await c.req.json();
const userId = c.req.header("x-user-id") || "anonymous";
const { message, threadId } = body;
const thread = threadId || `chat-${userId}-${Date.now()}`;
const agent = mastra.getAgent("research-agent");
const stream = await agent.stream(message, {
memory: {
thread,
resource: userId,
},
});
return new Response(stream.textStream, {
headers: {
"Content-Type": "text/event-stream",
"X-Thread-Id": thread,
},
});
},
});Memory Endpoints
For accessing stored research:
// Get prior vocabulary maps
registerApiRoute("/vocabulary/:topic", {
method: "GET",
handler: async (c) => {
const mastra = c.get("mastra");
const topic = c.req.param("topic");
const userId = c.req.header("x-user-id");
const vocabTool = mastra.getTool("retrieve-vocabulary");
const result = await vocabTool.execute({ topic, userId }, { mastra });
return c.json(result);
},
});
// Get research threads
registerApiRoute("/threads", {
method: "GET",
handler: async (c) => {
const mastra = c.get("mastra");
const userId = c.req.header("x-user-id");
const page = parseInt(c.req.query("page") || "1");
const threads = await mastra.storage?.listThreads({
resourceId: userId,
page,
perPage: 20,
});
return c.json({
threads: threads?.map(t => ({
id: t.id,
topic: t.metadata?.topic,
status: t.metadata?.status,
lastActivity: t.metadata?.updatedAt,
})),
page,
});
},
});Error Handling
// Global error handler
app.onError((err, c) => {
console.error("API error:", err);
// Zod validation errors
if (err.name === "ZodError") {
return c.json({
error: "Validation error",
details: err.issues,
}, 400);
}
// Not found
if (err.message.includes("not found")) {
return c.json({ error: err.message }, 404);
}
// Rate limiting
if (err.message.includes("rate limit")) {
return c.json({
error: "Rate limit exceeded",
retryAfter: 60,
}, 429);
}
// Generic error
return c.json({
error: "Internal server error",
message: process.env.NODE_ENV === "development" ? err.message : undefined,
}, 500);
});
// Not found handler
app.notFound((c) => {
return c.json({
error: "Endpoint not found",
availableEndpoints: [
"POST /diagnose",
"POST /intervene",
"POST /process/start",
"GET /process/:runId/status",
"POST /chat",
"GET /health",
],
}, 404);
});Authentication
import { bearerAuth } from "hono/bearer-auth";
import { jwt } from "hono/jwt";
// API key auth
app.use("/api/*", bearerAuth({
token: process.env.API_TOKEN!,
}));
// Or JWT auth
app.use("/api/*", jwt({
secret: process.env.JWT_SECRET!,
}));
// Extract user from JWT
app.use("/api/*", async (c, next) => {
const payload = c.get("jwtPayload");
c.set("userId", payload?.sub);
await next();
});OpenAPI Documentation
import { swaggerUI } from "@hono/swagger-ui";
// Serve OpenAPI spec
const server = new MastraServer({
app,
mastra,
openapiPath: "/openapi.json",
});
// Swagger UI
app.get("/docs", swaggerUI({ url: "/openapi.json" }));Best Practices
1. Validate Input: Always parse request body with Zod 2. Validate Output: Ensure responses match schema 3. Include Timestamps: Add timestamp to all responses 4. User Context: Extract userId from headers 5. Async Workflows: Return immediately, provide status endpoint 6. Error Details: In dev, include error messages 7. Rate Limiting: Protect expensive endpoints 8. OpenAPI: Document all endpoints
Context Network to Memory Conversion
Mapping operating-frameworks context network patterns to Mastra agent memory and RAG.
Core Mapping
| Context Network Element | Mastra Memory Pattern |
|---|---|
| status.md (current state) | Conversation thread metadata |
| decisions.md | Thread messages with decision metadata |
| glossary.md | Vector store for term lookup |
| Research findings | RAG with topic indexing |
| Prior vocabulary maps | Vector store with user/topic filtering |
| Session continuity | Thread-based memory |
Conversation Memory for Sessions
Context network tracks session continuity in status.md. In Mastra:
// Create thread for research session
const createResearchThread = async (userId: string, topic: string) => {
const threadId = `research-${userId}-${topic.toLowerCase().replace(/\s+/g, "-")}`;
const thread = await mastra.storage?.createThread({
resourceId: userId,
metadata: {
type: "research-session",
topic,
status: "active",
createdAt: new Date().toISOString(),
frameworkId: "research",
},
});
return threadId;
};
// Use thread in agent calls
const response = await researchAgent.generate(message, {
memory: {
thread: threadId,
resource: userId,
},
});Vocabulary Maps as RAG
Context network stores vocabulary maps for reuse. In Mastra:
import { embed } from "@ai-sdk/openai";
// Store vocabulary map in vector store
const storeVocabularyMap = async (
userId: string,
topic: string,
vocabularyMap: VocabularyMap
) => {
// Create embedding from core terms
const termsText = vocabularyMap.coreTerms
.map(t => `${t.term}: ${t.definition}`)
.join("\n");
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: `${topic}\n${termsText}`,
});
await mastra.vectors?.default.upsert({
indexName: "vocabulary-maps",
vectors: [{
id: `vocab-${userId}-${topic}`,
vector: embedding,
metadata: {
userId,
topic,
coreTerms: vocabularyMap.coreTerms,
synonyms: vocabularyMap.synonyms,
depthIndicators: vocabularyMap.depthIndicators,
lastUpdated: new Date().toISOString(),
},
}],
});
};
// Retrieve prior vocabulary for topic
const retrievePriorVocabulary = async (userId: string, topic: string) => {
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: topic,
});
const results = await mastra.vectors?.default.query({
indexName: "vocabulary-maps",
queryVector: embedding,
filter: { userId },
topK: 3,
});
return results?.map(r => r.metadata) || [];
};Research Findings as RAG
Prior research stored for retrieval:
// Store research findings
const storeResearchFindings = async (
userId: string,
topic: string,
synthesis: ResearchSynthesis
) => {
// Chunk key findings
const chunks = synthesis.keyFindings.map((finding, i) => ({
id: `finding-${userId}-${topic}-${i}`,
content: `${finding.claim}\n${finding.sources.map(s => s.title).join(", ")}`,
metadata: {
userId,
topic,
type: "finding",
confidence: finding.confidence,
claimText: finding.claim,
sources: finding.sources,
researchedAt: synthesis.synthesizedAt,
},
}));
const { embeddings } = await embedMany({
model: openai.embedding("text-embedding-3-small"),
values: chunks.map(c => c.content),
});
await mastra.vectors?.default.upsert({
indexName: "research-findings",
vectors: chunks.map((chunk, i) => ({
id: chunk.id,
vector: embeddings[i],
metadata: chunk.metadata,
})),
});
};
// Retrieve prior findings
const retrievePriorFindings = async (
userId: string,
query: string,
limit = 5
) => {
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: query,
});
const results = await mastra.vectors?.default.query({
indexName: "research-findings",
queryVector: embedding,
filter: { userId },
topK: limit,
});
return results?.map(r => ({
claim: r.metadata.claimText,
confidence: r.metadata.confidence,
sources: r.metadata.sources,
relevanceScore: r.score,
})) || [];
};Decision Logging
Context network decisions.md tracks key decisions. In Mastra:
// Log decision to thread
const logDecision = async (
threadId: string,
decision: {
question: string;
choice: string;
rationale: string;
alternatives: string[];
}
) => {
await mastra.storage?.addMessage({
threadId,
role: "system",
content: JSON.stringify(decision),
metadata: {
type: "decision",
question: decision.question,
choice: decision.choice,
decidedAt: new Date().toISOString(),
},
});
};
// Retrieve decisions from thread
const getDecisions = async (threadId: string) => {
const messages = await mastra.storage?.getMessages({
threadId,
page: 1,
perPage: 100,
});
return messages
?.filter(m => m.metadata?.type === "decision")
.map(m => JSON.parse(m.content));
};Framework Knowledge Indexing
Index framework content for agent self-awareness:
// Index framework SKILL.md sections
const indexFrameworkKnowledge = async (
frameworkId: string,
skillContent: string
) => {
// Parse into sections
const sections = parseSkillSections(skillContent);
const { embeddings } = await embedMany({
model: openai.embedding("text-embedding-3-small"),
values: sections.map(s => s.content),
});
await mastra.vectors?.default.upsert({
indexName: "framework-knowledge",
vectors: sections.map((section, i) => ({
id: `${frameworkId}-${section.id}`,
vector: embeddings[i],
metadata: {
frameworkId,
sectionType: section.type, // "state", "anti-pattern", "process"
sectionId: section.id,
content: section.content,
},
})),
});
};
// Agent can query its own framework knowledge
export const queryFrameworkKnowledge = createTool({
id: "query-framework",
description: "Search framework knowledge for patterns and guidance",
inputSchema: z.object({
query: z.string(),
sectionType: z.enum(["state", "anti-pattern", "process", "any"]).optional(),
}),
execute: async (inputData, context) => {
const { query, sectionType } = inputData;
const { mastra } = context;
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: query,
});
const filter = sectionType && sectionType !== "any"
? { sectionType }
: undefined;
const results = await mastra?.vectors?.default.query({
indexName: "framework-knowledge",
queryVector: embedding,
filter,
topK: 3,
});
return {
sections: results?.map(r => ({
type: r.metadata.sectionType,
content: r.metadata.content,
relevance: r.score,
})),
};
},
});Session State Management
Track research session state (like context network status.md):
// Session state schema
const SessionStateSchema = z.object({
currentPhase: z.string(),
completedPhases: z.array(z.string()),
diagnosticState: z.string().optional(),
vocabularyMapComplete: z.boolean(),
findingsCount: z.number(),
gaps: z.array(z.string()),
lastActivity: z.string().datetime(),
});
// Store session state
const updateSessionState = async (
threadId: string,
state: z.infer<typeof SessionStateSchema>
) => {
await mastra.storage?.updateThread(threadId, {
metadata: {
sessionState: state,
updatedAt: new Date().toISOString(),
},
});
};
// Retrieve session state
const getSessionState = async (threadId: string) => {
const thread = await mastra.storage?.getThread(threadId);
return thread?.metadata?.sessionState;
};Cross-Session Continuity
Tool for checking prior work before starting:
export const checkPriorResearch = createTool({
id: "check-prior-research",
description: "Check for prior research on topic before starting. " +
"Prevents starting from scratch when vocabulary maps or findings exist.",
inputSchema: z.object({
topic: z.string(),
userId: z.string(),
}),
outputSchema: z.object({
hasPriorWork: z.boolean(),
vocabularyMaps: z.array(VocabularyMapSummarySchema),
recentFindings: z.array(FindingSummarySchema),
lastSessionDate: z.string().optional(),
recommendation: z.string(),
}),
execute: async (inputData, context) => {
const { topic, userId } = inputData;
const { mastra } = context;
// Check for vocabulary maps
const vocabMaps = await retrievePriorVocabulary(userId, topic);
// Check for findings
const findings = await retrievePriorFindings(userId, topic, 3);
// Check for recent threads
const threads = await mastra?.storage?.listThreads({
resourceId: userId,
filter: { "metadata.topic": topic },
});
const lastSession = threads?.[0]?.metadata?.updatedAt;
const hasPriorWork = vocabMaps.length > 0 || findings.length > 0;
let recommendation: string;
if (!hasPriorWork) {
recommendation = "No prior research found. Start with Phase 0 analysis.";
} else if (vocabMaps.length > 0) {
recommendation = `Found vocabulary map from ${vocabMaps[0].lastUpdated}. ` +
"Load vocabulary and continue from last state.";
} else {
recommendation = `Found ${findings.length} prior findings. ` +
"Review findings before expanding research.";
}
return {
hasPriorWork,
vocabularyMaps: vocabMaps.map(v => ({
topic: v.topic,
termCount: v.coreTerms?.length || 0,
lastUpdated: v.lastUpdated,
})),
recentFindings: findings.map(f => ({
claim: f.claim,
confidence: f.confidence,
})),
lastSessionDate: lastSession,
recommendation,
};
},
});Memory Cleanup and Consolidation
Prevent unbounded memory growth:
// Summarize old conversations
const consolidateThread = async (threadId: string) => {
const messages = await mastra.storage?.getMessages({
threadId,
page: 1,
perPage: 100,
});
if (!messages || messages.length < 50) return;
// Summarize with agent
const summarizer = mastra.getAgent("summarizer");
const conversation = messages.map(m => `${m.role}: ${m.content}`).join("\n");
const summary = await summarizer?.generate(
`Summarize this research session, preserving key findings and decisions:\n\n${conversation}`
);
// Store summary, archive old messages
await mastra.storage?.addMessage({
threadId,
role: "system",
content: `[SESSION SUMMARY]\n${summary?.text}`,
metadata: {
type: "summary",
originalMessageCount: messages.length,
consolidatedAt: new Date().toISOString(),
},
});
};Best Practices
1. Thread per Topic: Create separate threads for each research topic 2. Filter by User: Always include userId in vector queries 3. Check Before Starting: Use checkPriorResearch tool before new research 4. Store Vocabulary: Persist vocabulary maps for future sessions 5. Log Decisions: Record key decisions with rationale 6. Consolidate Old Threads: Summarize long conversations 7. Index Framework: Make framework knowledge searchable by agent
Deployment Patterns
Containerization, hosting, and production deployment for framework agents.
Docker Configuration
Basic Dockerfile
FROM node:22-alpine
WORKDIR /app
# Install dependencies first (better caching)
COPY package*.json ./
RUN npm ci --only=production
# Copy source
COPY . .
# Build TypeScript
RUN npm run build
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Run
CMD ["node", "dist/index.js"]Multi-Stage Build (Smaller Image)
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:22-alpine AS production
WORKDIR /app
# Copy only production dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy built files
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]Docker Compose
version: "3.8"
services:
research-agent:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DATABASE_URL=${DATABASE_URL}
volumes:
- ./data:/app/data # For SQLite if used
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_USER=mastra
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=research_agent
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres_data:Environment Configuration
.env File
# Server
NODE_ENV=production
PORT=3000
# LLM Providers
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/research_agent
# Storage
LIBSQL_URL=file:./data/mastra.db
# Authentication
API_TOKEN=your-api-token
JWT_SECRET=your-jwt-secret
# Optional: Observability
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317Environment Validation
import { z } from "zod";
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.string().transform(Number).default("3000"),
OPENAI_API_KEY: z.string().min(1),
DATABASE_URL: z.string().optional(),
API_TOKEN: z.string().min(20),
});
// Validate at startup
const env = EnvSchema.parse(process.env);Cloud Deployment
Railway
# railway.toml
[build]
builder = "dockerfile"
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3Fly.io
# fly.toml
app = "research-agent"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[[services.http_checks]]
interval = "15s"
timeout = "2s"
path = "/health"# Deploy
fly deploy
fly secrets set OPENAI_API_KEY=sk-...Render
# render.yaml
services:
- type: web
name: research-agent
env: docker
dockerfilePath: ./Dockerfile
healthCheckPath: /health
envVars:
- key: NODE_ENV
value: production
- key: OPENAI_API_KEY
sync: false # Set in dashboardDatabase Setup
PostgreSQL with pgvector
-- Enable vector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create embeddings table
CREATE TABLE IF NOT EXISTS embeddings (
id TEXT PRIMARY KEY,
vector vector(1536),
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create index for similarity search
CREATE INDEX ON embeddings USING ivfflat (vector vector_cosine_ops)
WITH (lists = 100);SQLite (LibSQL) for Simple Deployments
import { LibSQLStore } from "@mastra/libsql";
const storage = new LibSQLStore({
url: process.env.LIBSQL_URL || "file:./data/mastra.db",
});Scaling Patterns
Horizontal Scaling
# kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: research-agent
spec:
replicas: 3
selector:
matchLabels:
app: research-agent
template:
metadata:
labels:
app: research-agent
spec:
containers:
- name: research-agent
image: research-agent:latest
ports:
- containerPort: 3000
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10Connection Pooling
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Max connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});Monitoring
Health Endpoints
registerApiRoute("/health", {
method: "GET",
handler: async (c) => {
const mastra = c.get("mastra");
const checks: Record<string, string> = {};
// Check storage
try {
await mastra.storage?.getThreads({ limit: 1 });
checks.storage = "healthy";
} catch {
checks.storage = "unhealthy";
}
// Check vector store
try {
await mastra.vectors?.default.query({
indexName: "test",
queryVector: new Array(1536).fill(0),
topK: 1,
});
checks.vectors = "healthy";
} catch {
checks.vectors = "unhealthy";
}
const allHealthy = Object.values(checks).every(v => v === "healthy");
return c.json({
status: allHealthy ? "healthy" : "degraded",
checks,
timestamp: new Date().toISOString(),
}, allHealthy ? 200 : 503);
},
});
registerApiRoute("/ready", {
method: "GET",
handler: async (c) => {
return c.json({ ready: true });
},
});Logging
import { logger } from "hono/logger";
// Request logging
app.use("*", logger());
// Custom structured logging
const log = (level: string, message: string, data?: Record<string, unknown>) => {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...data,
}));
};Metrics
// Simple request metrics
const metrics = {
requests: 0,
errors: 0,
latencies: [] as number[],
};
app.use("*", async (c, next) => {
const start = Date.now();
metrics.requests++;
try {
await next();
} catch (err) {
metrics.errors++;
throw err;
} finally {
metrics.latencies.push(Date.now() - start);
if (metrics.latencies.length > 1000) {
metrics.latencies = metrics.latencies.slice(-100);
}
}
});
registerApiRoute("/metrics", {
method: "GET",
handler: (c) => {
const avgLatency = metrics.latencies.length
? metrics.latencies.reduce((a, b) => a + b, 0) / metrics.latencies.length
: 0;
return c.json({
totalRequests: metrics.requests,
totalErrors: metrics.errors,
errorRate: metrics.requests ? metrics.errors / metrics.requests : 0,
avgLatencyMs: avgLatency,
p99LatencyMs: metrics.latencies.sort((a, b) => b - a)[0] || 0,
});
},
});Security
Rate Limiting
const rateLimits = new Map<string, { count: number; resetAt: number }>();
app.use("/api/*", async (c, next) => {
const ip = c.req.header("x-forwarded-for") || "unknown";
const now = Date.now();
const limit = rateLimits.get(ip);
if (limit && now < limit.resetAt) {
if (limit.count >= 100) { // 100 requests per minute
return c.json({
error: "Rate limit exceeded",
retryAfter: Math.ceil((limit.resetAt - now) / 1000),
}, 429);
}
limit.count++;
} else {
rateLimits.set(ip, { count: 1, resetAt: now + 60000 });
}
await next();
});CORS
import { cors } from "hono/cors";
app.use("*", cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || ["http://localhost:3000"],
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization", "X-User-Id"],
maxAge: 86400,
}));Input Sanitization
// Always validate with Zod
const sanitizedInput = InputSchema.parse(body);
// Limit input sizes
app.use("*", async (c, next) => {
const contentLength = c.req.header("content-length");
if (contentLength && parseInt(contentLength) > 1_000_000) {
return c.json({ error: "Request too large" }, 413);
}
await next();
});Graceful Shutdown
import { serve } from "@hono/node-server";
const server = serve({
fetch: app.fetch,
port: parseInt(process.env.PORT || "3000"),
});
// Handle shutdown signals
const shutdown = async () => {
console.log("Shutting down gracefully...");
// Stop accepting new requests
server.close();
// Close database connections
await pool?.end();
// Allow in-flight requests to complete
await new Promise(r => setTimeout(r, 5000));
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);Diagnostic State to Tool Conversion
Converting framework diagnostic states into Mastra tools with structured output.
Core Pattern
Each diagnostic state (or group of related states) becomes a tool that: 1. Accepts situation description as input 2. Assesses which state applies 3. Returns structured output with state, evidence, and recommendations
Single State Assessment Tool
When states are distinct and each needs individual assessment:
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
// Schema for state assessment output
const StateAssessmentSchema = z.object({
stateId: z.string(),
stateName: z.string(),
applies: z.boolean(),
confidence: z.number().min(0).max(1),
symptomsMatched: z.array(z.string()),
symptomsMissed: z.array(z.string()),
recommendedIntervention: z.string(),
nextActions: z.array(z.string()),
});
// Tool for assessing a specific state
export const assessNoAnalysisState = createTool({
id: "assess-no-analysis-r1",
description: "Assess whether the research situation shows R1: No Analysis state. " +
"Use when evaluating if topic analysis was skipped before searching.",
inputSchema: z.object({
situation: z.string().describe("Description of the current research situation"),
priorActions: z.array(z.string()).optional().describe("What has been done so far"),
}),
outputSchema: StateAssessmentSchema,
execute: async (inputData, context) => {
const { situation, priorActions = [] } = inputData;
const { mastra } = context;
// State-specific assessment logic
const symptoms = [
"Jumping straight to searching without analyzing the topic",
"No stakeholder identification",
"No temporal scope defined",
"No domain mapping done",
];
// Check symptoms against situation
const matched: string[] = [];
const missed: string[] = [];
// Simple keyword-based matching (in real implementation, use LLM)
for (const symptom of symptoms) {
if (situation.toLowerCase().includes("search") &&
!situation.toLowerCase().includes("analyz")) {
matched.push(symptoms[0]);
}
// ... more checks
}
const applies = matched.length >= 2;
const confidence = matched.length / symptoms.length;
return {
stateId: "R1",
stateName: "No Analysis",
applies,
confidence,
symptomsMatched: matched,
symptomsMissed: symptoms.filter(s => !matched.includes(s)),
recommendedIntervention: applies
? "Run Phase 0 Analysis Template before generating queries"
: "State does not apply",
nextActions: applies
? ["Complete Phase 0 analysis template", "Map stakeholders", "Define temporal scope"]
: [],
};
},
});Combined Assessment Tool
For most frameworks, a single tool that assesses all states is more practical:
// Schema for all states
const DiagnosticStateEnum = z.enum([
"R1", "R1.5", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10"
]);
const CombinedAssessmentSchema = z.object({
primaryState: DiagnosticStateEnum,
primaryStateName: z.string(),
confidence: z.number().min(0).max(1),
allStatesAssessed: z.array(z.object({
stateId: DiagnosticStateEnum,
stateName: z.string(),
score: z.number(),
symptomsMatched: z.array(z.string()),
})),
recommendedIntervention: z.string(),
nextActions: z.array(z.string()),
isComplete: z.boolean(),
});
export const assessResearchState = createTool({
id: "assess-research-state",
description: "Assess current research state across all diagnostic states (R1-R10). " +
"Returns primary state with highest match score and recommended intervention.",
inputSchema: z.object({
situation: z.string().describe("Description of current research situation"),
topic: z.string().optional().describe("Research topic if known"),
workDone: z.array(z.string()).optional().describe("Research activities completed"),
vocabularyMap: z.boolean().optional().describe("Whether vocabulary map exists"),
}),
outputSchema: CombinedAssessmentSchema,
execute: async (inputData, context) => {
const { situation, topic, workDone = [], vocabularyMap = false } = inputData;
const { mastra } = context;
// Use LLM agent for nuanced assessment
const assessor = mastra?.getAgent("research-diagnostician");
if (!assessor) throw new Error("Diagnostician agent not found");
const prompt = `Assess the following research situation against diagnostic states:
Situation: ${situation}
Topic: ${topic || "Not specified"}
Work done: ${workDone.join(", ") || "None specified"}
Has vocabulary map: ${vocabularyMap}
Rate each state R1-R10 on how well it matches (0-1 score).
Identify which symptoms match.
Recommend the intervention for the highest-scoring state.`;
const result = await assessor.generate(prompt, {
output: CombinedAssessmentSchema,
});
return result.object;
},
});Agent-Powered Assessment
For complex diagnostic logic, use an agent as the assessor:
// Diagnostician agent with framework knowledge in instructions
export const diagnosticianAgent = new Agent({
name: "research-diagnostician",
model: openai("gpt-4o-mini"),
instructions: `You are a research methodology diagnostician.
You assess research situations against these diagnostic states:
R1: No Analysis - Jumping to searching without topic analysis
R1.5: No Vocabulary Map - Using outsider terminology, finding surface-level material
R2: Single-Perspective - All queries support one viewpoint
R3: Domain Blindness - Searching only in familiar field
R4: Recency Bias - Only recent sources, missing historical context
R5: Breadth Without Depth - Many tabs, no synthesis
R6: Completion Uncertainty - Unsure whether to continue or stop
R7: Research Complete - Can explain topic, identify uncertainties, take action
R8: No Persistence - Starting from scratch each session
R9: Scope Mismatch - Over/under-researching relative to stakes
R10: No Confidence Signaling - Hedging everywhere, reader can't tell what's certain
For each assessment:
1. Score each state 0-1 on how well symptoms match
2. Identify specific symptoms that match
3. Return the highest-scoring non-complete state as primary
4. Provide specific intervention recommendation
5. List concrete next actions
If R7 (Complete) scores highest, acknowledge completion.`,
});
// Tool that uses the agent
export const diagnoseState = createTool({
id: "diagnose-research-state",
description: "Use diagnostician agent to assess research state",
inputSchema: z.object({
situation: z.string(),
}),
outputSchema: CombinedAssessmentSchema,
execute: async (inputData, context) => {
const { situation } = inputData;
const { mastra, runtimeContext } = context;
const agent = mastra?.getAgent("research-diagnostician");
const result = await agent?.generate(situation, {
output: CombinedAssessmentSchema,
runtimeContext,
});
return result?.object;
},
});Intervention Tools
Each intervention recommendation can link to a specific tool:
export const runPhase0Analysis = createTool({
id: "run-phase0-analysis",
description: "Execute Phase 0 topic analysis. Use when R1 state is identified.",
inputSchema: z.object({
topic: z.string().describe("Research topic to analyze"),
}),
outputSchema: Phase0AnalysisSchema,
execute: async (inputData, context) => {
const { topic } = inputData;
const { mastra } = context;
const analyst = mastra?.getAgent("topic-analyst");
const result = await analyst?.generate(
`Analyze topic for research: ${topic}
Apply Phase 0 Analysis Template:
1. Core Concepts - primary terms, variants, ambiguous terms
2. Stakeholders - primary actors, affected groups, opposing interests
3. Temporal Scope - origins, transitions, current state
4. Domains - primary field, adjacent fields
5. Controversies - active debates, competing frameworks`,
{ output: Phase0AnalysisSchema }
);
return result?.object;
},
});
export const buildVocabularyMap = createTool({
id: "build-vocabulary-map",
description: "Build vocabulary map. Use when R1.5 state is identified.",
inputSchema: z.object({
topic: z.string(),
initialTerms: z.array(z.string()).optional(),
sources: z.array(z.string()).optional(),
}),
outputSchema: VocabularyMapSchema,
execute: async (inputData, context) => {
// Build vocabulary map from topic and initial terms
},
});Chaining Assessment to Intervention
Common pattern: assess state, then run appropriate intervention:
export const diagnoseAndIntervene = createTool({
id: "diagnose-and-intervene",
description: "Assess state and automatically run recommended intervention",
inputSchema: z.object({
situation: z.string(),
topic: z.string(),
autoIntervene: z.boolean().default(true),
}),
outputSchema: z.object({
assessment: CombinedAssessmentSchema,
interventionResult: z.any().optional(),
interventionRan: z.boolean(),
}),
execute: async (inputData, context) => {
const { situation, topic, autoIntervene } = inputData;
const { mastra } = context;
// Assess state
const assessment = await diagnoseState.execute({ situation }, context);
if (!autoIntervene || assessment.isComplete) {
return { assessment, interventionRan: false };
}
// Run appropriate intervention
let interventionResult;
switch (assessment.primaryState) {
case "R1":
interventionResult = await runPhase0Analysis.execute({ topic }, context);
break;
case "R1.5":
interventionResult = await buildVocabularyMap.execute({ topic }, context);
break;
// ... other states
}
return {
assessment,
interventionResult,
interventionRan: true,
};
},
});Best Practices
1. Include Evidence
Always return which symptoms matched, not just the state ID:
symptomsMatched: ["Found only surface-level content", "Using outsider terms"]2. Confidence Scores
Provide confidence to help users evaluate:
confidence: 0.85 // High confidence
confidence: 0.4 // Multiple states could apply3. Actionable Next Steps
Don't just name the state - provide concrete actions:
nextActions: [
"Search for 'technically called' and 'also known as' in current sources",
"Map terms by domain using vocabulary template",
"Test searches with expert terms vs. current terms"
]4. Handle Ambiguity
When multiple states apply, return all with scores:
allStatesAssessed: [
{ stateId: "R1.5", score: 0.8, ... },
{ stateId: "R3", score: 0.6, ... },
{ stateId: "R4", score: 0.3, ... },
]Framework Analysis
How to analyze an operating-frameworks skill for agent conversion.
Overview
Before writing any code, systematically extract the framework's structure. This analysis becomes the blueprint for agent design.
Analysis Template
1. Extract Diagnostic States
For each state in the framework's SKILL.md:
{
"states": [
{
"id": "R1",
"name": "No Analysis",
"symptoms": [
"Jumping straight to searching without analyzing the topic"
],
"test": "Can you articulate stakeholders, temporal scope, and domain mapping?",
"intervention": "Run Phase 0 Analysis Template before generating queries",
"precedingStates": [],
"followingStates": ["R1.5", "R2"]
}
]
}Key fields:
id: State identifier from framework (R1, D2, etc.)name: Human-readable state namesymptoms: What user notices when in this statetest: How to verify this state appliesintervention: What framework recommendsprecedingStates: Which states typically lead herefollowingStates: Which states typically follow
2. Extract Vocabulary
Identify terms that need precise definitions:
{
"vocabulary": [
{
"term": "Phase 0 Analysis",
"definition": "Structured analysis of topic before searching: concepts, stakeholders, temporal scope, domains, controversies",
"domain": "research",
"depth": "expert",
"relatedTerms": ["Topic Analysis", "Pre-search Analysis"],
"usedInStates": ["R1", "R1.5"]
},
{
"term": "Vocabulary Map",
"definition": "Primary research deliverable mapping expert vs. outsider terms, cross-domain synonyms, and depth indicators",
"domain": "research",
"depth": "expert",
"relatedTerms": ["Term Map", "Terminology Mapping"],
"usedInStates": ["R1.5", "R3"]
}
]
}Depth levels:
introductory: Terms outsiders useworking: Terms practitioners useexpert: Technical/precise terminology
3. Extract Process Phases
Map the framework's workflow:
{
"processes": [
{
"phaseId": "0",
"name": "Topic Analysis",
"description": "Analyze topic through multiple lenses before searching",
"inputs": ["topic", "decision_context"],
"outputs": ["core_concepts", "stakeholders", "temporal_scope", "domains", "controversies"],
"dependencies": [],
"optional": false
},
{
"phaseId": "1.5",
"name": "Vocabulary Mapping",
"description": "Build vocabulary map as primary deliverable",
"inputs": ["core_concepts"],
"outputs": ["vocabulary_map"],
"dependencies": ["0"],
"optional": false
}
]
}4. Extract Integration Points
Identify connections to other skills:
{
"integrations": [
{
"skill": "context-networks",
"connectionType": "storage",
"description": "Store vocabulary maps and research findings in context network",
"states": ["R8"]
},
{
"skill": "fact-check",
"connectionType": "verification",
"description": "Apply truth-check to research findings",
"states": ["R10"]
}
]
}5. Extract Anti-Patterns
Document what to avoid:
{
"antiPatterns": [
{
"name": "Confirmation Trap",
"symptom": "Searching for evidence that supports existing belief",
"fix": "Explicitly search for strongest counterargument",
"relatedStates": ["R2"]
}
]
}6. Extract Completion Criteria
Define when the process is done:
{
"completionCriteria": {
"minimumViable": [
"Can define core concepts in own words",
"Know 2-3 major perspectives",
"Found authoritative source per perspective"
],
"workingKnowledge": [
"Can explain historical context",
"Understand stakeholder positions",
"Encountered counterarguments"
],
"deepExpertise": [
"Traced claims to primary sources",
"Can evaluate competing evidence"
]
}
}Analysis Process
Step 1: Read the Full Framework
Read the entire SKILL.md without extracting yet. Understand the overall purpose and flow.
Step 2: Identify State Boundaries
Look for section headers like "Diagnostic States", "The States", or numbered/lettered items with symptoms/interventions.
Step 3: Map Vocabulary
Highlight terms that:
- Have specific definitions in this framework
- Are used differently than common usage
- Connect to Zod schema needs
Step 4: Trace Process Flow
Identify the ordered steps or phases. Note:
- What each phase produces
- What each phase requires
- Optional vs required phases
Step 5: Find Integration Points
Look for:
- References to other skills
- "Integration with..." sections
- Cross-links or "see also" references
Step 6: Validate Completeness
Check that you've captured:
- [ ] All diagnostic states
- [ ] Key vocabulary (10-20 terms minimum)
- [ ] Process phases with dependencies
- [ ] Integration points
- [ ] Anti-patterns
- [ ] Completion criteria
Output Format
Save analysis as JSON:
analysis/
├── research-framework-analysis.json
├── story-sense-analysis.json
└── worldbuilding-analysis.jsonExample: Research Framework Analysis
See examples/research-agent/analysis.json for complete example.
Quick summary:
- States: 10 (R1-R10)
- Vocabulary: 15 key terms
- Phases: 4 (Phase 0, 1.5, 2, Synthesis)
- Integrations: 2 (context-networks, fact-check)
- Anti-patterns: 10 documented
Using Analysis for Agent Design
Once analysis is complete:
1. States → Tools: Each state or state group becomes a diagnostic tool 2. Vocabulary → Schemas: Each term becomes a Zod type 3. Phases → Workflow: Process becomes workflow steps 4. Integrations → Dependencies: Connected skills inform tool design 5. Anti-patterns → Validation: Prevent known failure modes
Mastra Core Patterns
Essential patterns for Mastra v1 Beta agent development.
Target version: Mastra v1 Beta (stable release expected January 2026) Required: Node.js 22.13.0+
Installation
# Install v1 Beta packages
npm install @mastra/core@beta @mastra/hono@beta
npm install @ai-sdk/openai # or other provider
npm install zod hono @hono/node-serverAgent Definition
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
export const myAgent = new Agent({
name: "my-agent", // Required: unique identifier
instructions: "You are a helpful assistant.", // Required: system prompt
model: openai("gpt-4o-mini"), // Required: LLM model
tools: { weatherTool, searchTool }, // Optional: named tools object
});Model Options
// SDK instances
model: openai("gpt-4o-mini")
model: anthropic("claude-3-5-sonnet-20241022")
// Router strings
model: "openai/gpt-4o-mini"
model: "anthropic/claude-3-5-sonnet"
// Fallback array
model: [
{ model: "openai/gpt-4o", maxRetries: 3 },
{ model: "anthropic/claude-3-5-sonnet", maxRetries: 2 },
]Dynamic Instructions
const agent = new Agent({
name: "personalized-agent",
instructions: ({ runtimeContext }) => {
const userName = runtimeContext.get("user-name");
const preferences = runtimeContext.get("preferences");
return `You are a personal assistant for ${userName}.
Their preferences:
${JSON.stringify(preferences, null, 2)}
Always address them by name and respect their preferences.`;
},
model: openai("gpt-4o-mini"),
});Tool Signatures (v1 Beta - CRITICAL)
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const myTool = createTool({
id: "my-tool",
description: "Description for LLM to understand when to use this tool",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
outputSchema: z.object({
results: z.array(z.string()),
}),
// v1 Beta signature: execute(inputData, context)
execute: async (inputData, context) => {
const { query } = inputData; // First parameter: parsed input
const { mastra, runtimeContext, abortSignal } = context; // Second: context
// Access nested agents via mastra
const helper = mastra?.getAgent("helperAgent");
// Always check abort signal for long operations
if (abortSignal?.aborted) throw new Error("Aborted");
return { results: ["result1", "result2"] };
},
});WRONG for v1 Beta:
execute: async ({ context }) => { ... } // This is stable 0.24.x signatureMemory Configuration
// Using memory in agent calls
const response = await agent.generate("Remember my name is Alex", {
memory: {
thread: "conversation-123", // Isolates conversation
resource: "user-456", // Associates with user
},
});
// Memory with storage
import { LibSQLStore } from "@mastra/libsql";
const mastra = new Mastra({
agents: { myAgent },
storage: new LibSQLStore({
url: "file:./mastra.db",
}),
});Structured Output
import { z } from "zod";
const response = await agent.generate("List three cities in Japan", {
output: z.object({
cities: z.array(z.object({
name: z.string(),
population: z.number().optional(),
})),
}),
});
// response.object is typed as { cities: { name: string; population?: number }[] }
console.log(response.object.cities);Mastra Instance
import { Mastra } from "@mastra/core/mastra";
export const mastra = new Mastra({
agents: { weatherAgent, assistantAgent },
workflows: { dataPipeline },
storage: new LibSQLStore({ url: "file:./mastra.db" }),
});
// Accessing agents
const agent = mastra.getAgent("weather-agent");
const result = await agent?.generate("Hello");Common Mistakes Quick Reference
| Topic | Wrong | Correct |
|---|---|---|
| Imports | import { Agent } from "@mastra/core" | import { Agent } from "@mastra/core/agent" |
| Tools array | tools: [tool1, tool2] | tools: { tool1, tool2 } |
| Memory context | { threadId: "123" } | { memory: { thread: "123", resource: "user" } } |
| Workflow data | context.steps.step1.output | inputData or getStepResult("step-1") |
| After parallel | inputData.result | inputData["step-id"].result |
| After branch | inputData.result | inputData["step-id"]?.result (optional) |
| Nested agents | import agent; agent.generate() | mastra.getAgent("name").generate() |
| State mutation | state.counter++ | setState({ ...state, counter: state.counter + 1 }) |
| v1 tool exec | execute: async ({ context }) | execute: async (inputData, context) |
Project Structure
my-agent/
├── src/
│ ├── mastra/
│ │ ├── agents/
│ │ │ └── my-agent.ts
│ │ ├── tools/
│ │ │ └── my-tool.ts
│ │ ├── workflows/
│ │ │ └── my-workflow.ts
│ │ └── index.ts # Mastra instance
│ ├── schemas/
│ │ └── my-schemas.ts # Zod schemas
│ └── index.ts # Hono server
├── package.json
├── tsconfig.json
└── DockerfileTypeScript Configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}Package.json Scripts
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest"
}
}