diff --git a/.agents/skills/mastra/SKILL.md b/.agents/skills/mastra/SKILL.md new file mode 100644 index 0000000..786224b --- /dev/null +++ b/.agents/skills/mastra/SKILL.md @@ -0,0 +1,204 @@ +--- +name: mastra +description: "Comprehensive Mastra framework guide. Teaches how to find current documentation, verify API signatures, and build agents and workflows. Covers documentation lookup strategies (embedded docs, remote docs), core concepts (agents vs workflows, tools, memory, RAG), TypeScript requirements, and common patterns. Use this skill for all Mastra development to ensure you're using current APIs from the installed version or latest documentation." +license: Apache-2.0 +metadata: + author: Mastra + version: "2.0.0" + repository: https://github.com/mastra-ai/skills +--- + +# Mastra Framework Guide + +Build AI applications with Mastra. This skill teaches you how to find current documentation and build agents and workflows. + +## ⚠️ Critical: Do not trust internal knowledge + +Everything you know about Mastra is likely outdated or wrong. Never rely on memory. Always verify against current documentation. + +Your training data contains obsolete APIs, deprecated patterns, and incorrect usage. Mastra evolves rapidly - APIs change between versions, constructor signatures shift, and patterns get refactored. + +## Prerequisites + +Before writing any Mastra code, check if packages are installed: + +```bash +ls node_modules/@mastra/ +``` + +- **If packages exist:** Use embedded docs first (most reliable) +- **If no packages:** Install first or use remote docs + +## Available files + +### References + +| User Question | First Check | How To | +| ----------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | +| "Create/install Mastra project" | [`references/create-mastra.md`](references/create-mastra.md) | Setup guide with CLI and manual steps | +| "How do I use Agent/Workflow/Tool?" | [`references/embedded-docs.md`](references/embedded-docs.md) | Look up in `node_modules/@mastra/*/dist/docs/` | +| "How do I use X?" (no packages) | [`references/remote-docs.md`](references/remote-docs.md) | Fetch from `https://mastra.ai/llms.txt` | +| "I'm getting an error..." | [`references/common-errors.md`](references/common-errors.md) | Common errors and solutions | +| "Upgrade from v0.x to v1.x" | [`references/migration-guide.md`](references/migration-guide.md) | Version upgrade workflows | + +### Scripts + +- `scripts/provider-registry.mjs`: Look up current providers and models available in the model router. Always run this before using a model to verify provider keys and model names. + +## Priority order for writing code + +⚠️ Never write code without checking current docs first. + +1. **Embedded docs first** (if packages installed) + + Look up current docs in `node_modules` for a package. Example of looking up "Agent" docs in `@mastra/core`: + + ```bash + grep -r "Agent" node_modules/@mastra/core/dist/docs/references + ``` + + - **Why:** Matches your EXACT installed version + - **Most reliable source of truth** + - **More information:** [`references/embedded-docs.md`](references/embedded-docs.md) + +2. **Source code second** (if packages installed) + + If you can't find what you need in the embedded docs, look directly at the source code. This is more time consuming but can provide insights into implementation details. + + ```bash + # Check what's available + cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json | grep '"Agent"' + + # Read the actual type definition + cat node_modules/@mastra/core/dist/[path-from-source-map] + ``` + + - **Why:** Ultimate source of truth if docs are missing or unclear + - **Use when:** Embedded docs don't cover your question + - **More information:** [`references/embedded-docs.md`](references/embedded-docs.md) + +3. **Remote docs third** (if packages not installed) + + You can fetch the latest docs from the Mastra website: + + ```bash + https://mastra.ai/llms.txt + ``` + + - **Why:** Latest published docs (may be ahead of installed version) + - **Use when:** Packages not installed or exploring new features + - **More information:** [`references/remote-docs.md`](references/remote-docs.md) + +## Core concepts + +### Agents vs workflows + +**Agent**: Autonomous, makes decisions, uses tools +Use for: Open-ended tasks (support, research, analysis) + +**Workflow**: Structured sequence of steps +Use for: Defined processes (pipelines, approvals, ETL) + +### Key components + +- **Tools**: Extend agent capabilities (APIs, databases, external services) +- **Memory**: Maintain context (message history, working memory, semantic recall, observational memory) +- **RAG**: Query external knowledge (vector stores, graph relationships) +- **Storage**: Persist data (Postgres, LibSQL, MongoDB) + +### Mastra Studio + +Studio provides an interactive UI for building, testing, and managing agents, workflows, and tools. It helps with debugging and improving your applications iteratively. + +Inside a Mastra project, run: + +```bash +npm run dev +``` + +Then open `http://localhost:4111` in your browser to access Mastra Studio. + +## Critical requirements + +### TypeScript config + +Mastra requires **ES2022 modules**. CommonJS will fail. + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } +} +``` + +### Model format + +Always use `"provider/model-name"` when defining models using Mastra's model router. + +Use the provider registry script to look up available providers and models: + +```bash +# List all available providers +node scripts/provider-registry.mjs --list + +# List all models for a specific provider (sorted newest first) +node scripts/provider-registry.mjs --provider openai +node scripts/provider-registry.mjs --provider anthropic +``` + +When the user asks to use a model or provider, **always run the script first** to verify the provider key and model name are valid. Do not guess model names from memory as they change frequently. + +Example model strings: + +- `"openai/gpt-5.4"` +- `"anthropic/claude-sonnet-4-5"` +- `"google/gemini-2.5-pro"` + +## When you see errors + +**Type errors often mean your knowledge is outdated.** + +**Common signs of outdated knowledge:** + +- `Property X does not exist on type Y` +- `Cannot find module` +- `Type mismatch` errors +- Constructor parameter errors + +**What to do:** + +1. Check [`references/common-errors.md`](references/common-errors.md) +2. Verify current API in embedded docs +3. Don't assume the error is a user mistake - it might be your outdated knowledge + +## Development workflow + +**Always verify before writing code:** + +1. **Check packages installed** + + ```bash + ls node_modules/@mastra/ + ``` + +2. **Look up current API** + - If installed → Use embedded docs [`references/embedded-docs.md`](references/embedded-docs.md) + - If not → Use remote docs [`references/remote-docs.md`](references/remote-docs.md) + +3. **Write code based on current docs** + +4. **Test in Studio** + ```bash + npm run dev # http://localhost:4111 + ``` + +## Resources + +- **Setup**: [`references/create-mastra.md`](references/create-mastra.md) +- **Embedded docs lookup**: [`references/embedded-docs.md`](references/embedded-docs.md) - Start here if packages are installed +- **Remote docs lookup**: [`references/remote-docs.md`](references/remote-docs.md) +- **Common errors**: [`references/common-errors.md`](references/common-errors.md) +- **Migrations**: [`references/migration-guide.md`](references/migration-guide.md) diff --git a/.agents/skills/mastra/references/common-errors.md b/.agents/skills/mastra/references/common-errors.md new file mode 100644 index 0000000..d81dc08 --- /dev/null +++ b/.agents/skills/mastra/references/common-errors.md @@ -0,0 +1,537 @@ +# Common errors and troubleshooting + +Comprehensive guide to common Mastra errors and their solutions. + +## Quickstart + +In a lot of cases, debugging errors can be greatly simplified by first checking the behavior in Mastra Studio. This allows you to interactively test agents and workflows, inspect logs, and see real-time error messages. + +```bash +npm run dev +``` + +Open `http://localhost:4111` in your browser to access Mastra Studio. + +## Build and configuration errors + +### "Cannot find module" or import errors + +**Symptoms**: + +```bash +Error: Cannot find module '@mastra/core' +SyntaxError: Cannot use import statement outside a module +``` + +**Causes**: + +- CommonJS configuration in `tsconfig.json` +- Missing `"type": "module"` in `package.json` +- Incorrect module resolution + +**Solutions**: + +1. Update `tsconfig.json`: + + ```json + { + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } + } + ``` + +2. Add to `package.json`: + + ```json + { + "type": "module" + } + ``` + +3. Ensure imports use `.js` extensions for local files (if needed by your bundler) + +### "Property X does not exist on type Y" + +**Symptoms**: + +```bash +Property 'tools' does not exist on type 'Agent' +Property 'memory' does not exist on type 'AgentConfig' +``` + +**Causes**: + +- Outdated API usage (Mastra is actively developed) +- Incorrect import or type +- Version mismatch between docs and installed package + +**Solutions**: + +1. Check embedded docs (see `embedded-docs.md`) to check current API +2. Check `node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json` for current exports +3. Verify package versions: `npm list @mastra/core` +4. Update dependencies: `npm update @mastra/core` + +## Agent errors + +### Agent not using assigned tools + +**Symptoms**: + +- Agent responds "I don't have access to that tool" +- Tools never get called despite being relevant + +**Causes**: + +- Tools not registered in Mastra instance +- Tools not passed to Agent constructor +- Tool IDs don't match + +**Solutions**: + +**Correct pattern**: + +```typescript +// 1. Create tool +const weatherTool = createTool({ + id: "get-weather", + // ... tool config +}); + +// 2. Register in Mastra instance +const mastra = new Mastra({ + tools: { + weatherTool, // or 'weatherTool': weatherTool + }, +}); + +// 3. Assign to agent +const agent = new Agent({ + id: "weather-agent", + tools: { weatherTool }, // Reference the tool + // ... other config +}); +``` + +**Alternative pattern (direct assignment)**: + +```typescript +const agent = new Agent({ + id: "weather-agent", + tools: { + weatherTool: createTool({ id: "get-weather" /* ... */ }), + }, +}); +``` + +### Agent memory not persisting + +**Symptoms**: + +- Agent doesn't remember previous messages +- Conversation history is lost between calls + +**Causes**: + +- No storage backend configured +- Missing or inconsistent `threadId` +- Memory not assigned to agent + +**Solutions**: + +```typescript +// 1. Configure storage +const storage = new PostgresStore({ + connectionString: process.env.DATABASE_URL, +}); + +// 2. Create memory with storage +const memory = new Memory({ + id: "chat-memory", + storage, + options: { + lastMessages: 10, // How many messages to retrieve + }, +}); + +// 3. Assign memory to agent +const agent = new Agent({ + id: "chat-agent", + memory, +}); + +// 4. Use consistent threadId +await agent.generate("Hello", { + threadId: "user-123-conversation", // Same threadId for entire conversation + resourceId: "user-123", +}); +``` + +## Workflow errors + +### "Cannot read property 'then' of undefined" + +**Symptoms**: + +```bash +TypeError: Cannot read property 'then' of undefined +Workflow execution fails immediately +``` + +**Causes**: + +- Forgot to call `.commit()` on workflow +- Step returns undefined + +**Solutions**: + +**Correct pattern**: + +```typescript +const workflow = createWorkflow({ + id: "my-workflow", + inputSchema: z.object({ data: z.string() }), + outputSchema: z.object({ result: z.string() }), +}) + .then(step1) + .then(step2) + .commit(); // REQUIRED! + +// Then execute +const run = await workflow.createRun(); +const result = await run.start({ inputData: { data: "test" } }); +``` + +### Workflow state not updating + +**Symptoms**: + +- State changes don't persist across steps +- `getStepResult()` returns undefined + +**Causes**: + +- Not using `setState` to update state +- Accessing state before step completes + +**Solutions**: + +```typescript +const step1 = createStep({ + id: "step1", + execute: async ({ state, setState }) => { + // Update state + await setState({ ...state, counter: (state.counter || 0) + 1 }); + return { result: "done" }; + }, +}); + +// Access state in subsequent steps +const step2 = createStep({ + id: "step2", + execute: async ({ state }) => { + console.log(state.counter); // Access updated state + return { result: "complete" }; + }, +}); +``` + +## Memory errors + +### "Storage is required for Memory" + +**Symptoms**: + +```bash +Error: Storage is required for Memory +Memory instantiation fails +``` + +**Causes**: + +- Memory created without storage backend + +**Solutions**: + +```typescript +// Always provide storage when creating Memory +const memory = new Memory({ + id: "my-memory", + storage: postgresStore, // REQUIRED + options: { + lastMessages: 10, + }, +}); +``` + +### Semantic recall not working + +**Symptoms**: + +- Memory doesn't retrieve semantically similar messages +- Only recent messages are returned + +**Causes**: + +- No vector store configured +- No embedder configured +- `semanticRecall` not enabled + +**Solutions**: + +```typescript +const memory = new Memory({ + id: "semantic-memory", + storage: postgresStore, + vector: chromaVectorStore, // REQUIRED for semantic recall + embedder: openaiEmbedder, // REQUIRED for semantic recall + options: { + lastMessages: 10, + semanticRecall: true, // REQUIRED + }, +}); +``` + +## Tool errors + +### "Tool validation failed" + +**Symptoms**: + +```bash +Error: Input validation failed for tool 'my-tool' +ZodError: Expected string, received number +``` + +**Causes**: + +- Input doesn't match inputSchema +- Missing required fields +- Type mismatch + +**Solutions**: + +```typescript +const tool = createTool({ + id: "my-tool", + inputSchema: z.object({ + name: z.string(), + age: z.number().optional(), // Make optional fields explicit + }), + execute: async (input) => { + // input is validated and typed + return { result: `Hello ${input.name}` }; + }, +}); + +// Correct usage +await tool.execute({ name: "Alice" }); // Works +await tool.execute({ name: "Bob", age: 30 }); // Works +await tool.execute({ age: 30 }); // ERROR: name is required +``` + +### Tool suspension not resuming + +**Symptoms**: + +- Tool suspends but never resumes +- resumeData is undefined + +**Causes**: + +- Not calling workflow.resume() or agent.generate() with resumeData +- Incorrect resumeSchema + +**Solutions**: + +```typescript +const approvalTool = createTool({ + id: "approval", + inputSchema: z.object({ request: z.string() }), + outputSchema: z.object({ approved: z.boolean() }), + suspendSchema: z.object({ requestId: z.string() }), + resumeSchema: z.object({ approved: z.boolean() }), + execute: async (input, context) => { + if (!context.resumeData) { + // First call - suspend + const requestId = generateId(); + context.suspend({ requestId }); + return; // Execution pauses here + } + + // Resumed - use resumeData + return { approved: context.resumeData.approved }; + }, +}); + +// Resume the workflow/agent +await run.resume({ + resumeData: { approved: true }, +}); +``` + +## Storage errors + +### "Connection refused" or "Database does not exist" + +**Symptoms**: + +```bash +Error: connect ECONNREFUSED 127.0.0.1:5432 +Error: database "mastra" does not exist +``` + +**Causes**: + +- Database not running +- Incorrect connection string +- Database not created + +**Solutions**: + +1. Start database (Postgres example): + +```bash +docker run -d \ + --name mastra-postgres \ + -e POSTGRES_PASSWORD=password \ + -e POSTGRES_DB=mastra \ + -p 5432:5432 \ + postgres:16 +``` + +2. Verify connection string: + +```env +DATABASE_URL=postgresql://postgres:password@localhost:5432/mastra +``` + +3. Initialize storage: + +```typescript +const storage = new PostgresStore({ + connectionString: process.env.DATABASE_URL, +}); +await storage.init(); // Creates tables if needed +``` + +## Environment variable errors + +### "API key not found" + +**Symptoms**: + +```bash +Error: OPENAI_API_KEY environment variable is not set +401 Unauthorized +``` + +**Causes**: + +- Missing .env file +- Environment variables not loaded +- Incorrect variable name + +**Solutions**: + +1. Create .env file: + +```env +OPENAI_API_KEY=sk-... +ANTHROPIC_API_KEY=sk-ant-... +GOOGLE_GENERATIVE_AI_API_KEY=... +``` + +2. Load environment variables (for Node.js): + +```typescript +import "dotenv/config"; // At top of entry file +``` + +3. Verify variable is loaded: + +```typescript +if (!process.env.OPENAI_API_KEY) { + throw new Error("OPENAI_API_KEY is required"); +} +``` + +## Model errors + +### "Model not found" or "Invalid model" + +**Symptoms**: + +```bash +Error: Model 'gpt-4' not found +Error: Invalid model format +``` + +**Causes**: + +- Incorrect model format (should be `provider/model`) +- Unsupported model +- Missing provider API key + +**Solutions**: + +**Correct model format**: + +```typescript +const agent = new Agent({ + model: "openai/gpt-5.4", // ✅ Correct + // NOT: model: 'gpt-5.4' // ❌ Missing provider +}); +``` + +**Common models**: + +- OpenAI: `openai/gpt-5.4`, `openai/gpt-5-mini` +- Anthropic: `anthropic/claude-sonnet-4-5`, `anthropic/claude-haiku-4-5`, `anthropic/claude-opus-4-6` +- Google: `google/gemini-2.5-pro`, `google/gemini-2.5-flash` + +**Use embedded docs to verify**: + +```bash +# Check supported models +ls node_modules/@mastra/core/dist/docs/ +# See embedded-docs.md for lookup instructions +``` + +## Debugging tips + +### Enable verbose logging + +```typescript +const mastra = new Mastra({ + logger: new PinoLogger({ + name: "mastra", + level: "debug", // or 'trace' for even more detail + }), +}); +``` + +### Check package versions + +```bash +npm list @mastra/core +npm list @mastra/memory +npm list @mastra/rag +``` + +### Validate TypeScript config + +```bash +npx tsc --showConfig +# Verify target: ES2022, module: ES2022 +``` + +## Getting help + +1. **Check embedded docs**: Check embedded docs (see `embedded-docs.md`) +2. **Search documentation**: [mastra.ai/docs](https://mastra.ai/docs) +3. **Check version compatibility**: Ensure all @mastra packages are same version +4. **File an issue**: [github.com/mastra-ai/mastra](https://github.com/mastra-ai/mastra) diff --git a/.agents/skills/mastra/references/create-mastra.md b/.agents/skills/mastra/references/create-mastra.md new file mode 100644 index 0000000..a4ee7d9 --- /dev/null +++ b/.agents/skills/mastra/references/create-mastra.md @@ -0,0 +1,222 @@ +# Create Mastra Reference + +Complete guide for creating new Mastra projects. Includes both quickstart CLI method and detailed manual installation. + +**Official documentation: [mastra.ai/docs](https://mastra.ai/docs)** + +## Get started + +Ask: **"How would you like to create your Mastra project?"** + +1. **Quick Setup**: Copy and run: `npm create mastra@latest` +2. **Guided Setup**: I walk you through each step, you approve commands +3. **Automatic Setup**: I create everything, just give me your API key + +> **For AI agents:** The CLI is interactive. Use **Automatic Setup** to create files using the steps in "Automatic Setup / Manual Installation" below. + +## Prerequisites + +- An API key from a supported model provider (OpenAI, Anthropic, Google, etc.) + +## Quick Setup (user runs CLI) + +Create a new Mastra project with one command: + +```bash +npm create mastra@latest +``` + +**Other package managers:** + +```bash +pnpm create mastra@latest +yarn create mastra@latest +bun create mastra@latest +``` + +## CLI flags + +**Skip the example agent:** + +```bash +npm create mastra@latest --no-example +``` + +**Use a specific template:** + +```bash +npm create mastra@latest --template +``` + +## Automatic setup / manual installation + +**Use this for automatic setup** (AI creates all files) or when you prefer manual control. + +Follow these steps to create a complete Mastra project: + +### Step 1: Create project directory + +```bash +mkdir my-first-agent && cd my-first-agent +npm init -y +``` + +### Step 2: Install dependencies + +```bash +npm install -D typescript @types/node mastra@latest +npm install @mastra/core@latest zod@^4 +``` + +### Step 3: Configure package scripts + +Add to `package.json`: + +```json +{ + "scripts": { + "dev": "mastra dev", + "build": "mastra build" + } +} +``` + +### Step 4: Configure TypeScript + +Create `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "outDir": "dist" + }, + "include": ["src/**/*"] +} +``` + +**Important:** Mastra requires `"module": "ES2022"` and `"moduleResolution": "bundler"`. CommonJS will cause errors. + +### Step 5: Create environment file + +Create `.env` with your API key: + +```env +GOOGLE_GENERATIVE_AI_API_KEY= +``` + +Or use `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc. + +### Step 6: Create weather tool + +Create `src/mastra/tools/weather-tool.ts`: + +```typescript +import { createTool } from "@mastra/core/tools"; +import { z } from "zod"; + +export const weatherTool = createTool({ + id: "get-weather", + description: "Get current weather for a location", + inputSchema: z.object({ + location: z.string().describe("City name"), + }), + outputSchema: z.object({ + output: z.string(), + }), + execute: async () => { + return { output: "The weather is sunny" }; + }, +}); +``` + +### Step 7: Create weather agent + +Create `src/mastra/agents/weather-agent.ts`: + +```typescript +import { Agent } from "@mastra/core/agent"; +import { weatherTool } from "../tools/weather-tool"; + +export const weatherAgent = new Agent({ + id: "weather-agent", + name: "Weather Agent", + instructions: ` + You are a helpful weather assistant that provides accurate weather information. + + Your primary function is to help users get weather details for specific locations. When responding: + - Always ask for a location if none is provided + - If the location name isn't in English, please translate it + - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York") + - Include relevant details like humidity, wind conditions, and precipitation + - Keep responses concise but informative + + Use the weatherTool to fetch current weather data. +`, + model: "google/gemini-2.5-pro", + tools: { weatherTool }, +}); +``` + +**Note:** Model format is `"provider/model-name"`. Examples: + +- `"google/gemini-2.5-pro"` +- `"openai/gpt-5.4"` +- `"anthropic/claude-sonnet-4-5"` + +### Step 8: Create mastra entry point + +Create `src/mastra/index.ts`: + +```typescript +import { Mastra } from "@mastra/core"; +import { weatherAgent } from "./agents/weather-agent"; + +export const mastra = new Mastra({ + agents: { weatherAgent }, +}); +``` + +### Step 9: Launch Mastra Studio + +Launch the development server: + +```bash +npm run dev +``` + +Access Studio at `http://localhost:4111` to test your agent. + +## Next steps + +After creating your project with `create mastra`: + +- **Customize the example agent** in `src/mastra/agents/weather-agent.ts` +- **Add new agents** - see [Agents documentation](https://mastra.ai/docs/agents/overview) +- **Create workflows** - see [Workflows documentation](https://mastra.ai/docs/workflows/overview) +- **Add more tools** to extend agent capabilities +- **Integrate into your app** - see framework guides at [mastra.ai/docs](https://mastra.ai/docs) + +## Troubleshooting + +| Issue | Solution | +| ------------------ | ------------------------------------------------------------------------------------ | +| API key not found | Make sure your `.env` file has the correct key | +| Studio won't start | Check that port 4111 is available | +| CommonJS errors | Ensure `tsconfig.json` uses `"module": "ES2022"` and `"moduleResolution": "bundler"` | +| Command not found | Ensure you're using Node.js 20+ | + +## Resources + +- [Docs](https://mastra.ai/docs) +- [Installation](https://mastra.ai/docs/getting-started/installation) +- [Agents](https://mastra.ai/docs/agents/overview) +- [Workflows](https://mastra.ai/docs/workflows/overview) +- [GitHub](https://github.com/mastra-ai/mastra) diff --git a/.agents/skills/mastra/references/embedded-docs.md b/.agents/skills/mastra/references/embedded-docs.md new file mode 100644 index 0000000..64e58dc --- /dev/null +++ b/.agents/skills/mastra/references/embedded-docs.md @@ -0,0 +1,103 @@ +# Embedded Docs Reference + +Look up API signatures from embedded docs in `node_modules/@mastra/*/dist/docs/` - these match the installed version. + +**Use this FIRST** when Mastra packages are installed locally. Embedded docs are always accurate for the installed version. + +## Why use embedded docs + +- **Version accuracy**: Embedded docs match the exact installed version +- **No network required**: All docs are local in `node_modules/` +- **Mastra evolves quickly**: APIs change rapidly, embedded docs stay in sync +- **TypeScript definitions**: Includes JSDoc, type signatures, and examples +- **Training data may be outdated**: Claude's knowledge cutoff may not reflect latest APIs + +## Documentation structure + +``` +node_modules/@mastra/core/dist/docs/ +├── SKILL.md # Package overview, exports +├── assets/ +│ └── SOURCE_MAP.json # Export -> file mappings +└── references/ # Individual topic docs +``` + +## Lookup process + +### 1. Check if packages are installed + +```bash +ls node_modules/@mastra/ +``` + +If you see packages like `core`, `memory`, `rag`, etc., proceed with embedded docs lookup. + +### 2. Look through topic docs + +Use `grep` to find relevant docs in `references/`: + +```bash +grep -r "Agent" node_modules/@mastra/core/dist/docs/references +``` + +### Naming convention + +Documents are typically formatted as `-.md` where category is one of: `"docs", "reference", "guides", "models"`. + +### Optional: Check source code for type definitions / additional details + +Look at the `SOURCE_MAP.json` to find the file path for the export: + +```bash +cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json | grep '"Agent"' +``` + +Returns: `{ "Agent": { "types": "dist/agent/agent.d.ts", ... } }` + +Read the type definition for exact constructor parameters, types, and JSDoc: + +```bash +cat node_modules/@mastra/core/dist/agent/agent.d.ts +``` + +## Common packages + +| Package | Path | Contains | +| ---------------- | ---------------------------------------- | ----------------------------------------- | +| `@mastra/core` | `node_modules/@mastra/core/dist/docs/` | Agents, Workflows, Tools, Mastra instance | +| `@mastra/memory` | `node_modules/@mastra/memory/dist/docs/` | Memory systems, conversation history | +| `@mastra/rag` | `node_modules/@mastra/rag/dist/docs/` | RAG features, vector stores | +| `@mastra/pg` | `node_modules/@mastra/pg/dist/docs/` | PostgreSQL storage | +| `@mastra/libsql` | `node_modules/@mastra/libsql/dist/docs/` | LibSQL/SQLite storage | + +## Quick commands reference + +```bash +# List installed @mastra packages +ls node_modules/@mastra/ + +# List available topic documentation +ls node_modules/@mastra/core/dist/docs/references/ + +# Find specific export in SOURCE_MAP +cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json | grep '"ExportName"' + +# Read type definition from path +cat node_modules/@mastra/core/dist/[path-from-source-map] + +# View package overview +cat node_modules/@mastra/core/dist/docs/SKILL.md +``` + +## When embedded docs are not available + +If packages aren't installed or `dist/docs/` doesn't exist: + +1. **Recommend installation**: Suggest installing packages to access embedded docs +2. **Fall back to remote docs**: See `references/remote-docs.md` + +## Best Practices + +1. **Check topic docs** for conceptual understanding and patterns +2. **Search source code** if docs don't answer the question +3. **Verify imports** match what's exported in the type definitions diff --git a/.agents/skills/mastra/references/migration-guide.md b/.agents/skills/mastra/references/migration-guide.md new file mode 100644 index 0000000..de4e998 --- /dev/null +++ b/.agents/skills/mastra/references/migration-guide.md @@ -0,0 +1,180 @@ +# Migration Guide + +Guide for upgrading Mastra versions using official documentation and current API verification. + +## Migration strategy + +For version upgrades, follow this process: + +### 1. Check official migration docs + +**Always start with the official migration documentation:** `https://mastra.ai/llms.txt` + +Look for the **Migrations** or **Guides** section, which will have: + +- Breaking changes for each version +- Automated migration tools +- Step-by-step upgrade instructions + +**Example sections to look for:** + +- `/guides/migrations/upgrade-to-v1/` +- `/guides/migrations/upgrade-to-v2/` +- Breaking changes lists + +### 2. Use embedded docs for current APIs + +After identifying breaking changes, verify the new APIs: + +**Check your installed version:** + +```bash +cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json | grep '"ApiName"' +cat node_modules/@mastra/core/dist/[path-from-source-map] +``` + +See [`embedded-docs.md`](embedded-docs.md) for detailed lookup instructions. + +### 3. Use remote docs for latest info + +If packages aren't updated yet, check what APIs will look like: `https://mastra.ai/reference/[topic]` + +See [`remote-docs.md`](remote-docs.md) for detailed lookup instructions. + +## Quick migration workflow + +```bash +# 1. Check current version +npm list @mastra/core + +# 2. Fetch migration guide from official docs +# Use WebFetch: https://mastra.ai/llms.txt +# Find relevant migration section + +# 3. Update dependencies +npm install @mastra/core@latest @mastra/memory@latest @mastra/rag@latest mastra@latest + +# 4. Run automated migration (if available) +npx @mastra/codemod@latest v1 # or whatever version + +# 5. Check embedded docs for new APIs +cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json + +# 6. Fix breaking changes using embedded docs lookup +# See embedded-docs.md for how to look up each API + +# 7. Test +npm run dev +npm test +``` + +## Common migration patterns + +### Finding what changed + +**Check official migration docs:** `https://mastra.ai/guides/migrations/upgrade-to-v1/overview.md` + +This will list: + +- Breaking changes +- Deprecated APIs +- New features +- Migration tools + +### Updating API usage + +**For each breaking change:** + +1. **Find the old API** in your code +2. **Look up the new API** using embedded docs: + ```bash + cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json | grep '"NewApi"' + cat node_modules/@mastra/core/dist/[path] + ``` +3. **Update your code** based on the type signatures +4. **Test** the change + +### Example: Tool execute signature change + +**Official docs say:** "Tool execute signature changed" + +**Look up current signature:** + +```bash +cat node_modules/@mastra/core/dist/docs/assets/SOURCE_MAP.json | grep '"createTool"' +cat node_modules/@mastra/core/dist/tools/tool.d.ts +``` + +**Update based on type definition:** + +```typescript +// Old (from docs) +execute: async (input) => { ... } + +// New (from embedded docs) +execute: async (inputData, context) => { ... } +``` + +## Pre-migration checklist + +- [ ] Backup code (git commit) +- [ ] Check official migration docs: `https://mastra.ai/llms.txt` +- [ ] Note current version: `npm list @mastra/core` +- [ ] Read breaking changes list +- [ ] Tests are passing + +## Post-migration checklist + +- [ ] All dependencies updated together +- [ ] TypeScript compiles: `npx tsc --noEmit` +- [ ] Tests pass: `npm test` +- [ ] Studio works: `npm run dev` +- [ ] No console warnings +- [ ] APIs verified against embedded docs + +## Migration resources + +| Resource | Use For | +| -------------------------------------- | --------------------------------------------- | +| `https://mastra.ai/llms.txt` | Finding migration guides and breaking changes | +| [`embedded-docs.md`](embedded-docs.md) | Looking up new API signatures after updating | +| [`remote-docs.md`](remote-docs.md) | Checking latest docs before updating | +| [`common-errors.md`](common-errors.md) | Fixing migration errors | + +## Version-specific notes + +### General principles + +1. **Always update all @mastra packages together** + + ```bash + npm install @mastra/core@latest @mastra/memory@latest @mastra/rag@latest mastra@latest + ``` + +2. **Check for automated migration tools** + + ```bash + npx @mastra/codemod@latest [version] + ``` + +3. **Verify Node.js version requirements** + - Check official migration docs for minimum Node version + +4. **Run database migrations if using storage** + - Follow storage migration guide in official docs + +## Getting help + +1. **Check official migration docs**: `https://mastra.ai/llms.txt` → Migrations section +2. **Look up new APIs**: See [`embedded-docs.md`](embedded-docs.md) +3. **Check for errors**: See [`common-errors.md`](common-errors.md) +4. **Ask in Discord**: https://discord.gg/BTYqqHKUrf +5. **File issues**: https://github.com/mastra-ai/mastra/issues + +## Key principles + +1. **Official docs are source of truth** - Start with `https://mastra.ai/llms.txt` +2. **Verify with embedded docs** - Check installed version APIs +3. **Update incrementally** - Don't skip major versions +4. **Test thoroughly** - Run tests after each change +5. **Use automation** - Use codemods when available diff --git a/.agents/skills/mastra/references/remote-docs.md b/.agents/skills/mastra/references/remote-docs.md new file mode 100644 index 0000000..dbd587b --- /dev/null +++ b/.agents/skills/mastra/references/remote-docs.md @@ -0,0 +1,193 @@ +# Remote Docs Reference + +How to look up current documentation from https://mastra.ai when local packages aren't available or you need conceptual guidance. + +**Use this when:** + +- Mastra packages aren't installed locally +- You need conceptual explanations or guides +- You want the latest documentation (may be ahead of installed version) + +## Documentation site structure + +Mastra docs are organized at **https://mastra.ai**: + +- **Docs**: Core documentation covering concepts, features, and implementation details +- **Models**: Mastra provides a unified interface for working with LLMs across multiple providers +- **Guides**: Step-by-step tutorials for building specific applications +- **Reference**: API reference documentation + +## Finding relevant documentation + +### Method 1: Use llms.txt (Recommended) + +The main llms.txt file provides an agent-friendly overview of all documentation: https://mastra.ai/llms.txt + +This returns a structured markdown document with: + +- Documentation organization and hierarchy +- All available topics and sections +- Direct links to relevant documentation +- Agent-optimized content structure + +**Use this first** to understand what documentation is available and where to find specific topics. + +### Method 2: Direct URL patterns + +Documentation follows predictable URL patterns: + +- Overview pages: `https://mastra.ai/docs/{topic}/overview` +- API reference: `https://mastra.ai/reference/{topic}/` +- Guides: `https://mastra.ai/guides/{topic}/` + +**Examples:** + +- `https://mastra.ai/docs/agents/overview` +- `https://mastra.ai/docs/workflows/overview` +- `https://mastra.ai/reference/workflows/workflow-methods/` + +## Agent-friendly documentation + +**Critical feature**: Send the `text-markdown` request header or add `.md` to any documentation URL to get clean, agent-friendly markdown. + +### Standard URL: + +``` +https://mastra.ai/reference/workflows/workflow-methods/then +``` + +### Agent-friendly URL (Markdown): + +``` +https://mastra.ai/reference/workflows/workflow-methods/then.md +``` + +The `.md` version: + +- Removes navigation, headers, footers +- Returns pure markdown content +- Optimized for LLM consumption +- Includes all code examples and explanations + +## Lookup Workflow + +### 1. Check the main documentation index + +**Start here** to understand what's available: + +``` +https://mastra.ai/llms.txt +``` + +This provides: + +- Complete documentation structure +- Available topics and sections +- Links to relevant documentation pages + +### 2. Find relevant documentation + +**Option A: Use information from llms.txt** +The main llms.txt will guide you to the right section. + +**Option B: Construct URL directly** + +``` +https://mastra.ai/docs/{topic}/overview +https://mastra.ai/reference/{topic}/ +``` + +### 3. Fetch agent-friendly version + +Add `.md` to the end of any documentation URL: + +``` +https://mastra.ai/reference/workflows/workflow-methods/then.md +``` + +### 4. Extract relevant information + +The markdown will include: + +- Function signatures +- Parameter descriptions +- Return types +- Usage examples +- Best practices + +## Common documentation paths + +### Agents + +- Overview: `https://mastra.ai/docs/agents/overview` +- Creating agents: `https://mastra.ai/docs/agents/creating-agents` +- Agent tools: `https://mastra.ai/docs/agents/tools` +- Memory: `https://mastra.ai/docs/agents/memory` + +### Workflows + +- Overview: `https://mastra.ai/docs/workflows/overview` +- Creating workflows: `https://mastra.ai/docs/workflows/creating-workflows` +- Workflow methods: `https://mastra.ai/reference/workflows/workflow-methods/` + +### Tools + +- Overview: `https://mastra.ai/docs/tools/overview` +- Creating tools: `https://mastra.ai/docs/tools/creating-tools` + +### Memory + +- Overview: `https://mastra.ai/docs/memory/overview` +- Configuration: `https://mastra.ai/docs/memory/configuration` + +### RAG + +- Overview: `https://mastra.ai/docs/rag/overview` +- Vector stores: `https://mastra.ai/docs/rag/vector-stores` + +## Example: Looking up workflow .then() method + +### 1. Check main documentation index + +``` +WebFetch({ + url: "https://mastra.ai/llms.txt", + prompt: "Where can I find documentation about workflow methods like .then()?" +}) +``` + +This will point you to the workflows reference section. + +### 2. Fetch specific method documentation + +``` +https://mastra.ai/reference/workflows/workflow-methods/then.md +``` + +### 3. Use WebFetch tool + +``` +WebFetch({ + url: "https://mastra.ai/reference/workflows/workflow-methods/then.md", + prompt: "What are the parameters for the .then() method and how do I use it?" +}) +``` + +## When to use remote vs embedded docs + +| Situation | Use | +| -------------------------- | --------------------------------------------------- | +| Packages installed locally | **Embedded docs** (guaranteed version match) | +| Packages not installed | **Remote docs** | +| Need conceptual guides | **Remote docs** | +| Need exact API signatures | **Embedded docs** (if available) | +| Exploring new features | **Remote docs** (may be ahead of installed version) | +| Need working examples | **Both** (embedded for types, remote for guides) | + +## Best practices + +1. **Always use .md** for fetching documentation +2. **Check sitemap.xml** when unsure about URL structure +3. **Prefer embedded docs** when packages are installed (version accuracy) +4. **Use remote docs** for conceptual understanding and guides +5. **Combine both** for comprehensive understanding diff --git a/.agents/skills/mastra/scripts/provider-registry.mjs b/.agents/skills/mastra/scripts/provider-registry.mjs new file mode 100755 index 0000000..11dee37 --- /dev/null +++ b/.agents/skills/mastra/scripts/provider-registry.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function findRegistryPath() { + const rel = join('node_modules', '@mastra', 'core', 'dist', 'provider-registry.json'); + // Walk up from script location to find project root with node_modules + let dir = __dirname; + for (let i = 0; i < 10; i++) { + try { + const p = join(dir, rel); + readFileSync(p, "utf-8"); + return p; + } catch { + dir = dirname(dir); + } + } + // Fall back to cwd + return join(process.cwd(), rel); +} + +function loadRegistry() { + const path = findRegistryPath(); + try { + return JSON.parse(readFileSync(path, "utf-8")); + } catch (e) { + console.error(`Error: Could not load provider registry at ${path}`); + console.error(e.message); + process.exit(1); + } +} + +/** + * Extract version numbers from a model name for sorting. + * Returns an array of numeric segments, e.g. "gpt-5.4" → [5, 4]. + * Handles dot-separated (3.5), hyphen-separated (3-7), and mixed formats. + * Models without detectable version numbers return null. + */ +function extractVersion(name) { + // Use named capture to grab version-like sequences along with their context. + // We capture digits separated by dots/hyphens, plus any trailing letter for filtering. + const regex = /(\d+(?:[.\-]\d+)*)([a-zA-Z])?/g; + const candidates = []; + let match; + while ((match = regex.exec(name)) !== null) { + const numStr = match[1]; + const suffix = match[2] || ""; + candidates.push({ numStr, suffix, index: match.index }); + } + if (candidates.length === 0) return null; + + // Process candidates: filter and clean up non-version parts + const processed = []; + for (const c of candidates) { + let parts = c.numStr.split(/[.\-]/).map(Number); + // If followed by a size suffix (b/B/k/K/m/M/t/T) — e.g. "8b", "70B", "1t" — + // strip the last numeric part (param count) but keep earlier parts as the version + if (/^[bBkKmMtT]$/.test(c.suffix)) { + parts = parts.slice(0, -1); + if (parts.length === 0) continue; + } + // Strip date-like segments (>= 2020 or YYYYMMDD-style 8-digit numbers) + parts = parts.filter((p) => p < 2020); + if (parts.length === 0) continue; + // Skip very large standalone numbers (parameter counts, IDs) + if (parts.length === 1 && parts[0] >= 100 && candidates.length > 1) continue; + // Skip trailing date-like patterns (MM-DD) in the latter half of the name + if ( + parts.length === 2 && + parts[0] >= 1 && parts[0] <= 12 && + parts[1] >= 1 && parts[1] <= 31 && + c.index > name.length / 2 && + candidates.length > 1 + ) continue; + processed.push(parts); + } + + if (processed.length === 0) return null; + + // Return the first valid version candidate (versions appear early in model names) + return processed[0]; +} + +function compareVersionsDesc(a, b) { + const va = extractVersion(a); + const vb = extractVersion(b); + + // Models without versions go to the end + if (!va && !vb) return a.localeCompare(b); + if (!va) return 1; + if (!vb) return -1; + + // Compare version tuples numerically, descending + const len = Math.max(va.length, vb.length); + for (let i = 0; i < len; i++) { + const ai = va[i] ?? 0; + const bi = vb[i] ?? 0; + if (bi !== ai) return bi - ai; + } + // Same version — secondary sort by full name descending + return b.localeCompare(a); +} + +function printUsage() { + console.log(`Usage: provider-registry.mjs [options] + +Options: + --list List all available model providers + --provider List all models for a provider (sorted newest first) + --help Show this help message + +Examples: + node provider-registry.mjs --list + node provider-registry.mjs --provider openai + node provider-registry.mjs --provider anthropic`); +} + +function listProviders(registry) { + const entries = Object.entries(registry.providers) + .map(([key, val]) => ({ key, name: val.name || key })) + .sort((a, b) => a.key.localeCompare(b.key)); + + const maxKey = Math.max(...entries.map((e) => e.key.length)); + const maxName = Math.max(...entries.map((e) => e.name.length)); + + console.log(`${"PROVIDER".padEnd(maxKey)} ${"NAME".padEnd(maxName)} MODELS`); + console.log(`${"─".repeat(maxKey)} ${"─".repeat(maxName)} ${"─".repeat(6)}`); + for (const entry of entries) { + const modelCount = registry.providers[entry.key].models.length; + console.log(`${entry.key.padEnd(maxKey)} ${entry.name.padEnd(maxName)} ${modelCount}`); + } + console.log(`\n${entries.length} providers`); +} + +function listModels(registry, providerName) { + const provider = registry.providers[providerName]; + if (!provider) { + console.error(`Error: Provider "${providerName}" not found.`); + console.error(`Run with --list to see available providers.`); + process.exit(1); + } + + const models = [...provider.models].sort(compareVersionsDesc); + + console.log(`${provider.name || providerName} — ${models.length} models\n`); + for (const model of models) { + console.log(` ${model}`); + } +} + +const args = process.argv.slice(2); + +if (args.includes("--help") || args.length === 0) { + printUsage(); + process.exit(0); +} + +if (args.includes("--list")) { + listProviders(loadRegistry()); + process.exit(0); +} + +const providerIdx = args.indexOf("--provider"); +if (providerIdx !== -1) { + const name = args[providerIdx + 1]; + if (!name) { + console.error("Error: --provider requires a provider name."); + process.exit(1); + } + listModels(loadRegistry(), name); + process.exit(0); +} + +console.error("Error: Unknown arguments:", args.join(" ")); +printUsage(); +process.exit(1); diff --git a/.claude/skills/mastra b/.claude/skills/mastra new file mode 120000 index 0000000..2d4ffc1 --- /dev/null +++ b/.claude/skills/mastra @@ -0,0 +1 @@ +../../.agents/skills/mastra \ No newline at end of file diff --git a/.env.example b/.env.example index a27fd80..da4665b 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,9 @@ -OPENAI_API_KEY=UPDATE_ME_TO_YOUR_OWN_KEY \ No newline at end of file +OPENAI_API_KEY=UPDATE_ME_TO_YOUR_OWN_KEY +DATABASE_URL="postgresql://neondb_owner:npg_DO0und9NBrUM@ep-sweet-brook-a7517b66-pooler.ap-southeast-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require" +# Leave unset on Vercel to default to file:/tmp/secondorder.db for serverless compatibility. +# Leave DATABASE_URL unset to use local libsql storage instead. +MASTRA_STORAGE_URL=file:./.mastra/secondorder.db +MASTRA_STORAGE_AUTH_TOKEN= +SECONDORDER_AGENT_MODEL=openai/gpt-5.1 +SECONDORDER_PLANNER_MODEL= +SECONDORDER_CRITIC_MODEL= diff --git a/.gitignore b/.gitignore index de6c8c5..bed7124 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ playwright-report/ coverage .env.local tsconfig.tsbuildinfo +.mastra diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..3402434 --- /dev/null +++ b/PRD.md @@ -0,0 +1,329 @@ +# PRD: Milestone 1.5 - Structured Meta Chat + +**Product**: SecondOrder Web +**Date**: March 8, 2026 +**Status**: Draft +**Document owner**: Product / Engineering + +## 1. Summary + +The next milestone should productize the meta-thinking system that already exists in the codebase. + +Today, SecondOrder has: + +- a polished marketing page at [`/Users/henry/workspace/secondorder-web/app/page.tsx`](/Users/henry/workspace/secondorder-web/app/page.tsx) +- a thread-based chat experience at [`/Users/henry/workspace/secondorder-web/app/chat/[threadId]/page.tsx`](/Users/henry/workspace/secondorder-web/app/chat/[threadId]/page.tsx) +- a Mastra-backed workflow that already classifies tasks, generates plans, drafts responses, and critiques them before the final answer at [`/Users/henry/workspace/secondorder-web/mastra/workflows/meta-chat-workflow.ts`](/Users/henry/workspace/secondorder-web/mastra/workflows/meta-chat-workflow.ts) +- memory-backed thread history and resource scoping in [`/Users/henry/workspace/secondorder-web/app/api/chat/route.ts`](/Users/henry/workspace/secondorder-web/app/api/chat/route.ts) + +What is missing is the actual product layer that makes this differentiation visible and useful to the user. + +This milestone should ship a clearer, more trustworthy chat product with: + +- visible task framing +- optional plan preview +- confidence and limitation signals +- structured feedback capture +- basic instrumentation for evaluation + +This is the narrowest milestone that turns SecondOrder from "chat with hidden orchestration" into "a meta-thinking assistant users can understand and trust." + +## 2. Problem + +The current app has meaningful backend meta-orchestration, but the user experience still feels like a standard chat interface. + +Current gaps: + +- users cannot see how SecondOrder interpreted their task +- plans and critiques are used internally but never surfaced +- there is no confidence or uncertainty signaling +- there is no structured feedback loop to improve future responses +- success is not measured with product-level analytics or evaluation events + +As a result, the product promise on the landing page is ahead of the in-product experience. + +## 3. Goal + +Ship the first user-visible version of SecondOrder's meta-thinking experience inside `/chat`. + +By the end of this milestone, a user should be able to: + +- ask a complex question +- see how SecondOrder framed the task +- optionally inspect the plan before or alongside the answer +- understand when the assistant is confident vs uncertain +- give simple feedback on whether the response was useful + +## 4. Non-Goals + +This milestone should not include: + +- broad tool-calling beyond what Mastra already supports internally +- multi-agent UI visualizations or raw chain-of-thought exposure +- long-term personalized memory controls +- judge-agent or multi-model orchestration UI +- large marketing-site redesign work + +## 5. User Segments + +Primary users: + +- founders, operators, and technical users exploring SecondOrder's core differentiation +- early adopters evaluating whether the assistant is better than generic chat for planning, analysis, decisions, and troubleshooting + +Secondary users: + +- internal team members using the product to validate reasoning quality and product positioning + +## 6. Current State Analysis + +### What is already implemented + +- New thread creation via `/chat` redirect to unique thread URLs +- Thread-scoped message history and resource isolation +- Request validation and oversized-input rejection +- Task classification into `simple_chat`, `analysis`, `planning`, `decision`, and `troubleshooting` +- Planner and critic agents +- Request-context injection into the final agent +- Memory-backed chat history +- Mastra storage, logging, and observability wiring +- Unit coverage for chat utilities, registry logic, and API route behavior + +### What is not yet productized + +- task-type display in the UI +- user-visible plan summaries +- confidence badges or uncertainty messaging +- feedback controls per answer +- conversation-level outcome tracking +- instrumentation tied to chat behavior +- clear empty-state onboarding for "how to use SecondOrder differently" + +## 7. Milestone Thesis + +The next milestone is not "build more intelligence." It is "make existing intelligence legible, controllable, and measurable." + +The product should expose enough of the meta layer to create trust and differentiation without exposing raw internal reasoning. + +## 8. Scope + +### In scope + +#### A. Visible Meta Mode + +For non-`simple_chat` requests, the chat experience should show a compact task-framing block that includes: + +- [x] detected task type +- [x] short goal summary +- [x] optional constraints summary if available +- [x] whether SecondOrder is using a structured meta pass + +This should appear as a compact system-style card above the assistant response or as a collapsible pre-answer block. + +#### B. Plan Preview + +For complex tasks, users should be able to view a compact plan summary generated by the planner workflow. + +Requirements: + +- [x] default to compact, not verbose +- [x] avoid exposing chain-of-thought or raw internal prompts +- [x] support a collapsed and expanded state +- [x] never block the final answer if the plan preview fails + +#### C. Confidence and Limitation Signals + +Each assistant response for meta-routed tasks should include lightweight trust signals such as: + +- [x] confidence level: low, medium, high +- [x] explicit note when assumptions are weak +- [x] explicit note when more context would improve the answer + +These signals should come from structured workflow output, not hardcoded UI copy. + +#### D. Feedback Capture + +Users should be able to provide structured feedback on assistant messages. + +Initial feedback schema: + +- [x] helpful +- [x] not helpful +- [x] needs more depth +- [x] missed constraints + +Feedback should be stored as an event with thread ID, message ID, task type, and timestamp. + +#### E. Instrumentation and Evaluation Baseline + +Track enough events to evaluate whether visible meta behavior improves user outcomes. + +Minimum events: + +- [x] thread started +- [x] message submitted +- [x] task classified +- [x] meta mode used +- [x] plan preview expanded +- [x] response completed +- [x] feedback submitted + +Minimum metrics: + +- share of conversations routed to meta mode +- feedback positivity rate +- response completion rate +- average turns per successful thread +- percentage of meta-routed responses where plan preview is viewed + +#### F. Better Chat Onboarding + +The empty state in chat should explain what makes SecondOrder different and suggest task types it handles well: + +- [x] planning +- [x] analysis +- [x] decisions +- [x] troubleshooting + +This should improve first-message quality and align product experience with landing-page claims. + +### Out of scope + +- persistent user profile settings +- memory inspection/deletion UI +- external connectors or retrieval systems +- pricing, auth, billing, or team collaboration +- extensive redesign of the visual system + +## 9. Product Requirements + +### Functional requirements + +1. [x] The system must show visible task framing for meta-routed requests. +2. [x] The system must expose a compact plan preview for meta-routed requests. +3. [x] The system must show confidence or limitation signals alongside the assistant answer. +4. [x] The system must allow users to submit structured feedback on individual assistant responses. +5. [x] The system must emit analytics and evaluation events for the full chat lifecycle. +6. [x] The system must preserve the current thread-based URL model and history loading behavior. +7. [x] The system must continue hiding raw internal reasoning and prompt text. + +### UX requirements + +1. The chat must still feel fast and conversational. +2. Meta information must be skimmable and collapsible. +3. Simple-chat requests should remain lightweight and should not show unnecessary framing chrome. +4. Visible trust signals should be informative, not alarmist. +5. The interface should work cleanly on desktop and mobile. + +### Technical requirements + +1. [x] Extend workflow output schemas rather than inferring UI state from freeform text. +2. [x] Keep UI concerns in chat route components, not in shared primitives unless reuse is justified. +3. [x] Preserve strict TypeScript and current testing patterns. +4. [x] Add focused Vitest coverage for new structured chat state logic. +5. [x] Add Playwright coverage for the visible meta-mode flow. + +## 10. User Stories + +1. As a user asking for a plan, I want to see how the assistant framed my request so I can trust that it understood the job. +2. As a user working on a hard problem, I want to inspect a compact plan so I can judge whether the reasoning direction is sound. +3. As a cautious user, I want clear confidence and limitation signals so I know when to trust the answer and when to add more context. +4. As a product team member, I want feedback and event data so I can tell whether the meta-thinking layer is improving outcomes. + +## 11. Success Metrics + +### Primary success metrics + +- At least 60% of meta-routed conversations receive a user feedback event +- Helpful feedback rate is at least 20 points higher for meta-routed threads than baseline generic threads +- At least 40% of meta-routed responses have the plan preview opened +- Chat completion rate improves relative to the current baseline + +### Secondary metrics + +- Reduced follow-up turns caused by misunderstanding the task +- Increased repeat usage of `/chat` +- Higher share of conversations in planning, analysis, decision, and troubleshooting categories + +## 12. Release Criteria + +The milestone is complete when: + +1. [x] Meta-routed tasks show visible task framing in the shipped UI. +2. [x] Plan preview is available and collapsible. +3. [x] Confidence and limitation signals are displayed for meta-routed responses. +4. [x] Structured feedback events are captured. +5. [x] Core analytics events are emitted. +6. [x] New unit and E2E coverage pass. +7. [x] `npm test` and `npm run ts-check` pass in the target branch. + +## 13. Risks + +### Risk: exposing too much internal reasoning + +Mitigation: + +- surface summaries, not raw prompts or chain-of-thought +- keep plan previews compact and product-shaped + +### Risk: added UI makes chat feel slower or heavier + +Mitigation: + +- only show meta chrome for meta-routed tasks +- default cards to compact collapsed states where appropriate + +### Risk: workflow outputs are not structured enough for UI + +Mitigation: + +- formalize schema fields for goal, constraints, plan summary, and confidence +- avoid parsing freeform assistant text for product state + +### Risk: instrumentation exists technically but is not actionable + +Mitigation: + +- define the event list and success metrics before implementation +- keep the first milestone event model intentionally small + +## 14. Suggested Delivery Plan + +### Phase A: Schema and backend contract + +- extend workflow result schema for visible task framing and confidence +- ensure API and request context return stable fields for UI rendering +- add event hooks for analytics and feedback + +### Phase B: Chat UX + +- add empty-state onboarding improvements +- add meta summary card and plan preview UI +- add confidence and limitation presentation +- add message-level feedback controls + +### Phase C: Validation + +- add targeted Vitest coverage +- add Playwright coverage for meta-routed flows +- verify `npm test` and `npm run ts-check` + +## 15. Recommended File Targets + +Likely implementation areas: + +- [`/Users/henry/workspace/secondorder-web/mastra/workflows/meta-chat-workflow.ts`](/Users/henry/workspace/secondorder-web/mastra/workflows/meta-chat-workflow.ts) +- [`/Users/henry/workspace/secondorder-web/lib/chat/contracts.ts`](/Users/henry/workspace/secondorder-web/lib/chat/contracts.ts) +- [`/Users/henry/workspace/secondorder-web/app/api/chat/route.ts`](/Users/henry/workspace/secondorder-web/app/api/chat/route.ts) +- [`/Users/henry/workspace/secondorder-web/app/chat/_components/chat-page-client.tsx`](/Users/henry/workspace/secondorder-web/app/chat/_components/chat-page-client.tsx) +- [`/Users/henry/workspace/secondorder-web/app/chat/_components/chat-message-list.tsx`](/Users/henry/workspace/secondorder-web/app/chat/_components/chat-message-list.tsx) +- [`/Users/henry/workspace/secondorder-web/app/chat/_components/chat-message.tsx`](/Users/henry/workspace/secondorder-web/app/chat/_components/chat-message.tsx) +- [`/Users/henry/workspace/secondorder-web/e2e/chat.spec.ts`](/Users/henry/workspace/secondorder-web/e2e/chat.spec.ts) + +## 16. Final Recommendation + +The clearest next milestone is: + +**Make SecondOrder's hidden meta workflow visible, trustworthy, and measurable in chat.** + +That is the highest-leverage step because it builds directly on infrastructure already present in the repo, closes the gap between marketing promise and product reality, and creates the baseline needed for later milestones like tools, memory controls, and judge-agent orchestration. diff --git a/afk-ralph.sh b/afk-ralph.sh new file mode 100755 index 0000000..43bc708 --- /dev/null +++ b/afk-ralph.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +for ((i=1; i<=$1; i++)); do + tmpfile=$(mktemp) + + codex exec \ + --full-auto \ + --output-last-message "$tmpfile" \ + "PRD.md progress.md + +1. Read PRD.md and progress.md. +2. Find the highest-priority incomplete task and implement it. +3. Run relevant tests and type checks. +4. Update progress.md with exactly what you completed. +5. Update PRD.md to put a checkbox next to the task item you have implemented. + +ONLY WORK ON A SINGLE TASK. +If the PRD is complete, output exactly: COMPLETE." + + result=$(cat "$tmpfile") + rm -f "$tmpfile" + + echo "$result" + + if [[ "$result" == *"COMPLETE"* ]]; then + echo "PRD complete after $i iterations." + exit 0 + fi +done \ No newline at end of file diff --git a/app/api/chat/events/route.test.ts b/app/api/chat/events/route.test.ts new file mode 100644 index 0000000..09d7f97 --- /dev/null +++ b/app/api/chat/events/route.test.ts @@ -0,0 +1,105 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const cookiesMock = vi.fn(); +const getThreadMock = vi.fn(); +const recordChatEventMock = vi.fn(); + +vi.mock('next/headers', () => ({ + cookies: cookiesMock, +})); + +vi.mock('@/lib/chat/history', () => ({ + getThread: getThreadMock, +})); + +vi.mock('@/lib/chat/events', () => ({ + recordChatEvent: recordChatEventMock, +})); + +describe('/api/chat/events route', () => { + beforeEach(() => { + cookiesMock.mockResolvedValue({ + get() { + return undefined; + }, + }); + + getThreadMock.mockResolvedValue(null); + recordChatEventMock.mockResolvedValue({ + id: 'event-1', + createdAt: '2026-03-08T00:00:00.000Z', + }); + }); + + afterEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it('returns 400 for invalid payloads', async () => { + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat/events', { + method: 'POST', + body: JSON.stringify({ threadId: 'bad-id' }), + }), + ); + + expect(response.status).toBe(400); + expect(recordChatEventMock).not.toHaveBeenCalled(); + }); + + it('returns 404 when the thread is not accessible', async () => { + getThreadMock.mockResolvedValue({ + id: 'thread-1', + resourceId: '22222222-2222-4222-8222-222222222222', + }); + + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat/events', { + method: 'POST', + body: JSON.stringify({ + eventType: 'plan_preview_expanded', + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + }), + }), + ); + + expect(response.status).toBe(404); + expect(recordChatEventMock).not.toHaveBeenCalled(); + }); + + it('stores instrumentation events for valid requests', async () => { + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat/events', { + method: 'POST', + body: JSON.stringify({ + eventType: 'plan_preview_expanded', + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + metadata: { + planLength: 2, + }, + }), + }), + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(recordChatEventMock).toHaveBeenCalledWith({ + eventType: 'plan_preview_expanded', + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + metadata: { + planLength: 2, + }, + }); + expect(payload.success).toBe(true); + }); +}); diff --git a/app/api/chat/events/route.ts b/app/api/chat/events/route.ts new file mode 100644 index 0000000..4948a83 --- /dev/null +++ b/app/api/chat/events/route.ts @@ -0,0 +1,98 @@ +import { cookies } from 'next/headers'; +import { chatEventBodySchema } from '@/lib/chat/contracts'; +import { recordChatEvent } from '@/lib/chat/events'; +import { getThread } from '@/lib/chat/history'; +import { + createResourceCookieHeader, + getOrCreateResourceId, +} from '@/lib/chat/session'; + +export const runtime = 'nodejs'; + +function jsonResponse( + body: Record, + init?: ResponseInit & { setCookie?: string }, +) { + const headers = new Headers(init?.headers); + headers.set('Content-Type', 'application/json'); + + if (init?.setCookie) { + headers.append('Set-Cookie', init.setCookie); + } + + return new Response(JSON.stringify(body), { + status: init?.status ?? 200, + headers, + }); +} + +async function validateThreadAccess(threadId: string, resourceId: string) { + const thread = await getThread(threadId); + + if (thread && thread.resourceId && thread.resourceId !== resourceId) { + return false; + } + + return true; +} + +export async function POST(request: Request) { + const cookieStore = await cookies(); + const { resourceId, shouldSetCookie } = getOrCreateResourceId(cookieStore); + + try { + const body = chatEventBodySchema.safeParse(await request.json()); + + if (!body.success) { + return jsonResponse( + { error: 'Invalid request body' }, + { + status: 400, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const hasAccess = await validateThreadAccess(body.data.threadId, resourceId); + + if (!hasAccess) { + return jsonResponse( + { error: 'Thread not found' }, + { + status: 404, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const event = await recordChatEvent(body.data); + + return jsonResponse( + { success: true, event }, + { + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } catch (error) { + const message = + error instanceof Error + ? error.message + : 'An unexpected error occurred'; + + return jsonResponse( + { error: message }, + { + status: 500, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } +} diff --git a/app/api/chat/feedback/route.test.ts b/app/api/chat/feedback/route.test.ts new file mode 100644 index 0000000..2af1f78 --- /dev/null +++ b/app/api/chat/feedback/route.test.ts @@ -0,0 +1,117 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const cookiesMock = vi.fn(); +const getThreadMock = vi.fn(); +const recordChatFeedbackMock = vi.fn(); +const recordChatEventMock = vi.fn(); + +vi.mock('next/headers', () => ({ + cookies: cookiesMock, +})); + +vi.mock('@/lib/chat/history', () => ({ + getThread: getThreadMock, +})); + +vi.mock('@/lib/chat/feedback', () => ({ + recordChatFeedback: recordChatFeedbackMock, +})); + +vi.mock('@/lib/chat/events', () => ({ + recordChatEvent: recordChatEventMock, +})); + +describe('/api/chat/feedback route', () => { + beforeEach(() => { + cookiesMock.mockResolvedValue({ + get() { + return undefined; + }, + }); + + getThreadMock.mockResolvedValue(null); + recordChatFeedbackMock.mockResolvedValue({ + id: 'feedback-1', + createdAt: '2026-03-08T00:00:00.000Z', + }); + recordChatEventMock.mockResolvedValue({ + id: 'event-1', + createdAt: '2026-03-08T00:00:00.000Z', + }); + }); + + afterEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it('returns 400 for invalid payloads', async () => { + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat/feedback', { + method: 'POST', + body: JSON.stringify({ threadId: 'bad-id' }), + }), + ); + + expect(response.status).toBe(400); + expect(recordChatFeedbackMock).not.toHaveBeenCalled(); + }); + + it('returns 404 when the thread is not accessible', async () => { + getThreadMock.mockResolvedValue({ + id: 'thread-1', + resourceId: '22222222-2222-4222-8222-222222222222', + }); + + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat/feedback', { + method: 'POST', + body: JSON.stringify({ + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + feedback: 'helpful', + }), + }), + ); + + expect(response.status).toBe(404); + expect(recordChatFeedbackMock).not.toHaveBeenCalled(); + }); + + it('stores structured feedback events for valid requests', async () => { + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat/feedback', { + method: 'POST', + body: JSON.stringify({ + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + feedback: 'missed_constraints', + }), + }), + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(recordChatFeedbackMock).toHaveBeenCalledWith({ + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + feedback: 'missed_constraints', + }); + expect(recordChatEventMock).toHaveBeenCalledWith({ + eventType: 'feedback_submitted', + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'assistant-1', + taskType: 'planning', + metadata: { + feedback: 'missed_constraints', + }, + }); + expect(payload.success).toBe(true); + }); +}); diff --git a/app/api/chat/feedback/route.ts b/app/api/chat/feedback/route.ts new file mode 100644 index 0000000..1fec069 --- /dev/null +++ b/app/api/chat/feedback/route.ts @@ -0,0 +1,108 @@ +import { cookies } from 'next/headers'; +import { chatFeedbackBodySchema } from '@/lib/chat/contracts'; +import { recordChatEvent } from '@/lib/chat/events'; +import { recordChatFeedback } from '@/lib/chat/feedback'; +import { getThread } from '@/lib/chat/history'; +import { + createResourceCookieHeader, + getOrCreateResourceId, +} from '@/lib/chat/session'; + +export const runtime = 'nodejs'; + +function jsonResponse( + body: Record, + init?: ResponseInit & { setCookie?: string }, +) { + const headers = new Headers(init?.headers); + headers.set('Content-Type', 'application/json'); + + if (init?.setCookie) { + headers.append('Set-Cookie', init.setCookie); + } + + return new Response(JSON.stringify(body), { + status: init?.status ?? 200, + headers, + }); +} + +async function validateThreadAccess(threadId: string, resourceId: string) { + const thread = await getThread(threadId); + + if (thread && thread.resourceId && thread.resourceId !== resourceId) { + return false; + } + + return true; +} + +export async function POST(request: Request) { + const cookieStore = await cookies(); + const { resourceId, shouldSetCookie } = getOrCreateResourceId(cookieStore); + + try { + const body = chatFeedbackBodySchema.safeParse(await request.json()); + + if (!body.success) { + return jsonResponse( + { error: 'Invalid request body' }, + { + status: 400, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const hasAccess = await validateThreadAccess(body.data.threadId, resourceId); + + if (!hasAccess) { + return jsonResponse( + { error: 'Thread not found' }, + { + status: 404, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const event = await recordChatFeedback(body.data); + void recordChatEvent({ + eventType: 'feedback_submitted', + threadId: body.data.threadId, + messageId: body.data.messageId, + taskType: body.data.taskType, + metadata: { + feedback: body.data.feedback, + }, + }).catch(() => {}); + + return jsonResponse( + { success: true, event }, + { + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } catch (error) { + const message = + error instanceof Error + ? error.message + : 'An unexpected error occurred'; + + return jsonResponse( + { error: message }, + { + status: 500, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } +} diff --git a/app/api/chat/route.test.ts b/app/api/chat/route.test.ts new file mode 100644 index 0000000..7faae8a --- /dev/null +++ b/app/api/chat/route.test.ts @@ -0,0 +1,164 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const cookiesMock = vi.fn(); +const handleChatStreamMock = vi.fn(); +const createUIMessageStreamResponseMock = vi.fn(); +const getThreadMock = vi.fn(); +const getChatHistoryMock = vi.fn(); +const recordChatEventMock = vi.fn(); + +vi.mock('next/headers', () => ({ + cookies: cookiesMock, +})); + +vi.mock('@mastra/ai-sdk', () => ({ + handleChatStream: handleChatStreamMock, +})); + +vi.mock('ai', () => ({ + createUIMessageStreamResponse: createUIMessageStreamResponseMock, +})); + +vi.mock('@/lib/chat/history', () => ({ + getThread: getThreadMock, + getChatHistory: getChatHistoryMock, +})); + +vi.mock('@/lib/chat/events', () => ({ + recordChatEvent: recordChatEventMock, +})); + +vi.mock('@/mastra', () => ({ + mastra: { + getWorkflow() { + return { + async createRun() { + return { + async start() { + return { + status: 'success', + result: { + taskType: 'planning', + shouldUseMeta: true, + selectedSkillIds: ['interpret-task', 'build-plan'], + selectedSkillNames: ['Interpret Task', 'Build Plan'], + meta: { + goal: 'Create a migration plan.', + constraints: ['Avoid downtime'], + plan: ['Assess current state', 'Sequence the migration'], + responseStrategy: 'Lead with sequencing and risk mitigation.', + confidence: 'medium', + limitations: ['Current deployment details are missing'], + contextGaps: ['Unknown database size'], + }, + }, + }; + }, + }; + }, + }; + }, + }, +})); + +describe('/api/chat route', () => { + beforeEach(() => { + cookiesMock.mockResolvedValue({ + get() { + return undefined; + }, + }); + + handleChatStreamMock.mockResolvedValue( + new ReadableStream({ + start(controller) { + controller.close(); + }, + }), + ); + + createUIMessageStreamResponseMock.mockImplementation( + () => new Response('streamed'), + ); + + getThreadMock.mockResolvedValue(null); + getChatHistoryMock.mockResolvedValue([]); + recordChatEventMock.mockResolvedValue({ + id: 'event-1', + createdAt: '2026-03-08T00:00:00.000Z', + }); + }); + + afterEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it('returns 400 for invalid POST payloads', async () => { + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat', { + method: 'POST', + body: JSON.stringify({ threadId: 'bad-id', messages: 'nope' }), + }), + ); + + expect(response.status).toBe(400); + }); + + it('returns stored messages for GET history requests', async () => { + getChatHistoryMock.mockResolvedValue([ + { + id: 'msg-1', + role: 'assistant', + parts: [{ type: 'text', text: 'Hello again' }], + }, + ]); + + const { GET } = await import('./route'); + const response = await GET( + new Request('http://localhost:3000/api/chat?threadId=11111111-1111-4111-8111-111111111111'), + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload.messages).toHaveLength(1); + }); + + it('streams chat responses for valid POST requests', async () => { + const { POST } = await import('./route'); + const response = await POST( + new Request('http://localhost:3000/api/chat', { + method: 'POST', + body: JSON.stringify({ + threadId: '11111111-1111-4111-8111-111111111111', + messages: [ + { + id: 'user-1', + role: 'user', + parts: [{ type: 'text', text: 'Create a migration plan.' }], + }, + ], + }), + }), + ); + + expect(response.status).toBe(200); + expect(handleChatStreamMock).toHaveBeenCalledTimes(1); + expect(createUIMessageStreamResponseMock).toHaveBeenCalledTimes(1); + expect(recordChatEventMock).toHaveBeenCalledWith({ + eventType: 'thread_started', + threadId: '11111111-1111-4111-8111-111111111111', + }); + expect(recordChatEventMock).toHaveBeenCalledWith({ + eventType: 'message_submitted', + threadId: '11111111-1111-4111-8111-111111111111', + messageId: 'user-1', + }); + expect(recordChatEventMock).toHaveBeenCalledWith({ + eventType: 'task_classified', + threadId: '11111111-1111-4111-8111-111111111111', + taskType: 'planning', + }); + }); +}); diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index b157f38..46f99a0 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,99 +1,300 @@ -import { openai } from '@ai-sdk/openai'; -import { streamText } from 'ai'; +import { handleChatStream } from '@mastra/ai-sdk'; +import { createUIMessageStreamResponse } from 'ai'; +import { + MASTRA_RESOURCE_ID_KEY, + MASTRA_THREAD_ID_KEY, + RequestContext, +} from '@mastra/core/request-context'; +import { cookies } from 'next/headers'; +import { chatPostBodySchema, historyQuerySchema } from '@/lib/chat/contracts'; +import { recordChatEvent } from '@/lib/chat/events'; +import { getChatHistory, getThread } from '@/lib/chat/history'; +import { isMessageTooLong, getLastUserMessageText } from '@/lib/chat/messages'; +import { + createResourceCookieHeader, + getOrCreateResourceId, +} from '@/lib/chat/session'; +import { mastra } from '@/mastra'; -export const runtime = 'edge'; +export const runtime = 'nodejs'; -const SYSTEM_PROMPT = `You are SecondOrder, a meta-thinking AI assistant that embodies the principles of meta-cognition for LLM systems. +function normalizeMessages( + messages: Array<{ + id?: string; + role: 'system' | 'user' | 'assistant'; + parts: Array<{ type: string; text?: string }>; + metadata?: unknown; + }>, +) { + return messages.map((message) => ({ + ...message, + id: message.id ?? crypto.randomUUID(), + })); +} -Core Capabilities: -- Meta thinking layer: Analyze goals, prompts, and constraints to generate sharper context -- Self-improving loop: Generate answers, absorb feedback, audit progress, and iterate -- Adaptive learning: Modify strategies for each new problem +function jsonResponse( + body: Record, + init?: ResponseInit & { setCookie?: string }, +) { + const headers = new Headers(init?.headers); + headers.set('Content-Type', 'application/json'); -Your responses should: -1. Be concise and analytical -2. Demonstrate self-monitoring and self-evaluation -3. Reference meta-cognitive principles when relevant -4. Acknowledge uncertainty and suggest iterative improvements + if (init?.setCookie) { + headers.append('Set-Cookie', init.setCookie); + } -You help users understand and apply meta-thinking principles to their problems.`; + return new Response(JSON.stringify(body), { + status: init?.status ?? 200, + headers, + }); +} -const MAX_MESSAGE_LENGTH = 4000; +async function validateThreadAccess(threadId: string, resourceId: string) { + const thread = await getThread(threadId); -interface IncomingMessage { - role: 'user' | 'assistant'; - content?: string; - parts?: Array<{ type: string; text: string }>; + if (thread && thread.resourceId && thread.resourceId !== resourceId) { + return false; + } + + return true; } -interface NormalizedMessage { - role: 'user' | 'assistant'; - content: string; +function recordChatEventSafely( + input: Parameters[0], +) { + void recordChatEvent(input).catch(() => {}); } -function normalizeMessages(messages: IncomingMessage[]): NormalizedMessage[] { - return messages.map((msg) => { - // If message has parts array (e.g., from useChat), extract text content - if (msg.parts && Array.isArray(msg.parts)) { - const textContent = msg.parts - .filter((part) => part.type === 'text') - .map((part) => part.text) - .join(''); - return { - role: msg.role, - content: textContent, - }; - } - // Otherwise use content directly - return { - role: msg.role, - content: msg.content || '', - }; - }); +export async function GET(request: Request) { + const cookieStore = await cookies(); + const { resourceId, shouldSetCookie } = getOrCreateResourceId(cookieStore); + const query = historyQuerySchema.safeParse( + Object.fromEntries(new URL(request.url).searchParams), + ); + + if (!query.success) { + return jsonResponse( + { error: 'Invalid thread ID' }, + { + status: 400, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const hasAccess = await validateThreadAccess(query.data.threadId, resourceId); + + if (!hasAccess) { + return jsonResponse( + { error: 'Thread not found' }, + { + status: 404, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const messages = await getChatHistory(query.data.threadId, resourceId); + + return jsonResponse( + { messages }, + { + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); } -export async function POST(req: Request) { +export async function POST(request: Request) { + const cookieStore = await cookies(); + const { resourceId, shouldSetCookie } = getOrCreateResourceId(cookieStore); + try { - const { messages } = await req.json(); + const body = chatPostBodySchema.safeParse(await request.json()); - if (!Array.isArray(messages)) { - return new Response(JSON.stringify({ error: 'Invalid request body' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); + if (!body.success) { + return jsonResponse( + { error: 'Invalid request body' }, + { + status: 400, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); } - // Normalize messages to CoreMessage format - const normalizedMessages = normalizeMessages(messages); + const normalizedMessages = normalizeMessages(body.data.messages); + const latestUserMessage = getLastUserMessageText(normalizedMessages); - const lastMessage = normalizedMessages[normalizedMessages.length - 1]; - if ( - typeof lastMessage?.content === 'string' && - lastMessage.content.length > MAX_MESSAGE_LENGTH - ) { - return new Response(JSON.stringify({ error: 'Message too long' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, + if (isMessageTooLong(latestUserMessage)) { + return jsonResponse( + { error: 'Message too long' }, + { + status: 400, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const existingThread = await getThread(body.data.threadId); + + if (existingThread && existingThread.resourceId && existingThread.resourceId !== resourceId) { + return jsonResponse( + { error: 'Thread not found' }, + { + status: 404, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + const latestMessage = normalizedMessages.at(-1); + + if (!existingThread) { + recordChatEventSafely({ + eventType: 'thread_started', + threadId: body.data.threadId, }); } - const result = streamText({ - model: openai('gpt-5.4'), - system: SYSTEM_PROMPT, - messages: normalizedMessages, + if (latestMessage?.role === 'user') { + recordChatEventSafely({ + eventType: 'message_submitted', + threadId: body.data.threadId, + messageId: latestMessage.id, + }); + } + + const workflow = mastra.getWorkflow('metaChatWorkflow'); + const run = await workflow.createRun({ resourceId }); + const workflowResult = await run.start({ + inputData: { + messages: normalizedMessages, + threadId: body.data.threadId, + resourceId, + }, + }); + + if (workflowResult.status !== 'success') { + return jsonResponse( + { error: 'Unable to prepare chat response' }, + { + status: 500, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, + ); + } + + recordChatEventSafely({ + eventType: 'task_classified', + threadId: body.data.threadId, + taskType: workflowResult.result.taskType, + }); + + const requestContext = new RequestContext>( + Object.entries({ + ...workflowResult.result, + threadId: body.data.threadId, + resourceId, + }), + ); + requestContext.set(MASTRA_THREAD_ID_KEY, body.data.threadId); + requestContext.set(MASTRA_RESOURCE_ID_KEY, resourceId); + + const stream = await handleChatStream({ + mastra, + agentId: 'secondOrderAgent', + params: { + messages: normalizedMessages as never, + memory: { + thread: body.data.threadId, + resource: resourceId, + }, + requestContext, + }, }); + const assistantMessageMetadata = { + taskType: workflowResult.result.taskType, + shouldUseMeta: workflowResult.result.shouldUseMeta, + meta: workflowResult.result.meta, + }; + let assistantMessageId: string | null = null; + const streamWithMetadata = stream.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + if (chunk.type === 'text-start' && !assistantMessageId) { + assistantMessageId = chunk.id; + + if (workflowResult.result.shouldUseMeta) { + recordChatEventSafely({ + eventType: 'meta_mode_used', + threadId: body.data.threadId, + messageId: assistantMessageId, + taskType: workflowResult.result.taskType, + }); + } + } + + if (chunk.type === 'start' || chunk.type === 'finish') { + if (chunk.type === 'finish') { + recordChatEventSafely({ + eventType: 'response_completed', + threadId: body.data.threadId, + messageId: assistantMessageId ?? undefined, + taskType: workflowResult.result.taskType, + }); + } + + controller.enqueue({ + ...chunk, + messageMetadata: assistantMessageMetadata, + }); + return; + } - return result.toUIMessageStreamResponse(); + controller.enqueue(chunk); + }, + }), + ); + + const response = createUIMessageStreamResponse({ + stream: streamWithMetadata as never, + }); + + if (shouldSetCookie) { + response.headers.append( + 'Set-Cookie', + createResourceCookieHeader(resourceId), + ); + } + + return response; } catch (error) { - if (error instanceof Error) { - return new Response(JSON.stringify({ error: error.message }), { + const message = + error instanceof Error + ? error.message + : 'An unexpected error occurred'; + + return jsonResponse( + { error: message }, + { status: 500, - headers: { 'Content-Type': 'application/json' }, - }); - } - return new Response( - JSON.stringify({ error: 'An unexpected error occurred' }), - { status: 500, headers: { 'Content-Type': 'application/json' } }, + setCookie: shouldSetCookie + ? createResourceCookieHeader(resourceId) + : undefined, + }, ); } } diff --git a/app/chat/[threadId]/page.tsx b/app/chat/[threadId]/page.tsx new file mode 100644 index 0000000..da5e98e --- /dev/null +++ b/app/chat/[threadId]/page.tsx @@ -0,0 +1,22 @@ +import { notFound } from 'next/navigation'; +import { threadIdSchema } from '@/lib/chat/contracts'; +import { ChatPageClient } from '../_components/chat-page-client'; + +interface ChatThreadPageProps { + params: Promise<{ + threadId: string; + }>; +} + +export default async function ChatThreadPage({ + params, +}: ChatThreadPageProps) { + const { threadId } = await params; + const parsedThreadId = threadIdSchema.safeParse(threadId); + + if (!parsedThreadId.success) { + notFound(); + } + + return ; +} diff --git a/app/chat/_components/chat-input.tsx b/app/chat/_components/chat-input.tsx index 08944b6..47b7791 100644 --- a/app/chat/_components/chat-input.tsx +++ b/app/chat/_components/chat-input.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { ChangeEvent, FormEvent } from 'react'; +import type { ChangeEvent, FormEvent, RefObject } from 'react'; import { Button } from '@/components/ui/button'; interface ChatInputProps { @@ -8,6 +8,8 @@ interface ChatInputProps { handleInputChange: (e: ChangeEvent) => void; handleSubmit: (e: FormEvent) => void; isLoading: boolean; + placeholder?: string; + textareaRef?: RefObject; } export function ChatInput({ @@ -15,6 +17,8 @@ export function ChatInput({ handleInputChange, handleSubmit, isLoading, + placeholder = 'Ask for a plan, analysis, decision, or troubleshooting help...', + textareaRef, }: ChatInputProps) { const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -33,12 +37,14 @@ export function ChatInput({ className="mx-auto flex max-w-3xl items-end gap-3" >