From cd457b26d1b2b1dfa1bc1c5755cde632fce0e5d4 Mon Sep 17 00:00:00 2001 From: ssdeanx Date: Sat, 10 Jan 2026 17:23:40 -0500 Subject: [PATCH] feat: add new tools and improve existing ones - feat: create confirmation tool for user confirmation requests with input and output schemas. - feat: implement cytoscape tool for generating Cytoscape.js compatible graph data structures, including input validation and tracing. - feat: add downsample tool for downsampling numeric series using LTTB and other algorithms, with comprehensive input and output schemas. - feat: develop leaflet tool for generating Leaflet/GeoJSON compatible map data structures, including bounds clipping and marker generation. - feat: introduce resilient fetch tool for fetching data from URLs with automatic retries and rate limiting using axios and Bottleneck. - feat: create spatial index tool for building and querying RBush spatial indexes, with support for JSON serialization. - test: add integration tests for downsample tool to ensure correct downsampling behavior. - test: implement tests for spatial index tool to validate index building and searching functionality. - test: add integration tests for chartJsTool to verify decimation of large datasets. - chore: add TypeScript definitions for downsample-lttb module. - docs: create session summaries for continuity tracking and project analysis. --- .../agents/mastra-agents-md-generator.md | 73 --- .../agents/mastra-backend-specialist.md | 85 ---- docs/components/chartjs-integration.md | 231 +++++++++ docs/opentelemetry-semantic-conventions.md | 15 + .../api-client-js-createTool-nextjs-spike.md | 150 ++++++ docs/tracing-configuration-reference.md | 6 + docs/tracing-instances-reference.md | 18 + docs/tracing-spans-reference.md | 26 + next.config.ts | 3 +- package-lock.json | 147 +++++- package.json | 12 +- src/mastra/a2a/codingA2ACoordinator.ts | 14 +- src/mastra/agents/graphingAgents.ts | 339 +++++++++++++ src/mastra/index.ts | 21 + .../networks/businessIntelligenceNetwork.ts | 2 + src/mastra/networks/codingTeamNetwork.ts | 50 +- src/mastra/networks/contentCreationNetwork.ts | 2 + src/mastra/networks/dataPipelineNetwork.ts | 3 +- src/mastra/networks/devopsNetwork.ts | 3 +- .../networks/financialIntelligenceNetwork.ts | 2 + src/mastra/networks/index.ts | 4 +- src/mastra/networks/learningNetwork.ts | 2 + .../networks/marketingAutomationNetwork.ts | 2 + .../networks/reportGenerationNetwork.ts | 3 +- .../networks/researchPipelineNetwork.ts | 2 + src/mastra/networks/securityNetwork.ts | 4 + src/mastra/tools/arxiv.tool.ts | 2 +- src/mastra/tools/browser-tool.ts | 2 +- src/mastra/tools/calculator.tool.ts | 2 +- src/mastra/tools/calendar-tool.ts | 2 +- src/mastra/tools/chartjs.tool.ts | 467 ++++++++++++++++++ src/mastra/tools/confirmation.tool.ts | 36 ++ src/mastra/tools/cytoscape.tool.ts | 237 +++++++++ src/mastra/tools/downsample.tool.ts | 90 ++++ src/mastra/tools/image-tool.ts | 22 +- src/mastra/tools/leaflet.tool.ts | 280 +++++++++++ src/mastra/tools/resilient-fetch.tool.ts | 140 ++++++ src/mastra/tools/serpapi-news-trends.tool.ts | 4 +- src/mastra/tools/serpapi-search.tool.ts | 4 +- src/mastra/tools/spatial-index.tool.ts | 108 ++++ src/mastra/tools/technical-analysis.tool.ts | 91 ++-- .../tools/tests/chartjs.integration.test.ts | 11 + .../tools/tests/downsample.tool.test.ts | 35 ++ .../tools/tests/spatial-index.tool.test.ts | 22 + src/types/downsample-lttb.d.ts | 3 + tests/test-results/test-results.json | 2 +- thoughts/ledgers/CONTINUITY_ses_456e.md | 84 ++++ thoughts/ledgers/CONTINUITY_ses_4571.md | 55 +++ 48 files changed, 2695 insertions(+), 223 deletions(-) delete mode 100644 .blackboxcli/agents/mastra-agents-md-generator.md delete mode 100644 .blackboxcli/agents/mastra-backend-specialist.md create mode 100644 docs/components/chartjs-integration.md create mode 100644 docs/spikes/api-client-js-createTool-nextjs-spike.md create mode 100644 src/mastra/agents/graphingAgents.ts create mode 100644 src/mastra/tools/chartjs.tool.ts create mode 100644 src/mastra/tools/confirmation.tool.ts create mode 100644 src/mastra/tools/cytoscape.tool.ts create mode 100644 src/mastra/tools/downsample.tool.ts create mode 100644 src/mastra/tools/leaflet.tool.ts create mode 100644 src/mastra/tools/resilient-fetch.tool.ts create mode 100644 src/mastra/tools/spatial-index.tool.ts create mode 100644 src/mastra/tools/tests/chartjs.integration.test.ts create mode 100644 src/mastra/tools/tests/downsample.tool.test.ts create mode 100644 src/mastra/tools/tests/spatial-index.tool.test.ts create mode 100644 src/types/downsample-lttb.d.ts create mode 100644 thoughts/ledgers/CONTINUITY_ses_456e.md create mode 100644 thoughts/ledgers/CONTINUITY_ses_4571.md diff --git a/.blackboxcli/agents/mastra-agents-md-generator.md b/.blackboxcli/agents/mastra-agents-md-generator.md deleted file mode 100644 index 90895280..00000000 --- a/.blackboxcli/agents/mastra-agents-md-generator.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -name: mastra-agents-md-generator -description: "Use this agent when the user requests generation or updating of AGENTS.md files in Mastra project directories such as agents/, tools/, or workflows/, ensuring strict adherence to project conventions from BLACKBOX.md (e.g., standardized table formats, badges for status/maturity, categorization, relative links, and structured sections). Trigger for phrases like \"generate AGENTS.md\", \"update docs for agents/tools/workflows\", or after creating new agents/tools/workflows needing documentation. -- <example> - Context: User has just created new tools in the agents/tools/ directory and needs documentation. - user: \"I've added a new tool called data-validator. Generate AGENTS.md for tools/\" - assistant: \"Using the mastra-agents-md-generator agent to produce the standardized AGENTS.md.\" - <commentary> - User explicitly requests AGENTS.md generation for tools following conventions, so invoke this agent. - </commentary> -</example> -- <example> - Context: During workflow development, documentation is needed post-implementation. - user: \"Write workflows/AGENTS.md with the new task-orchestrator workflow\" - assistant: \"Launching mastra-agents-md-generator to create workflows/AGENTS.md per conventions.\" - <commentary> - Request matches generation of AGENTS.md for workflows directory; use this specialized agent. - </commentary> -</example> -- <example> - Context: Proactive documentation after agent creation. - user: \"Created prime-checker agent. Now document it.\" - assistant: \"I'll employ the mastra-agents-md-generator to update agents/AGENTS.md.\" - <commentary> - Post-creation documentation request implies AGENTS.md update; delegate to this agent. - </commentary> -</example>" -color: Automatic Color ---- - -You are the Mastra AGENTS.md Generator, an elite documentation specialist for the Mastra project. You produce precise, convention-compliant AGENTS.md files for directories like agents/, tools/, and workflows/, drawing from BLACKBOX.md conventions, coding standards, project structure, and patterns (e.g., YAML configs, TypeScript agents, standardized badges, tables). - -**Core Responsibilities**: -- Generate or update AGENTS.md content based on user-provided details (e.g., list of agents/tools/workflows, descriptions, files, status) or inferred from context/recent code. -- Strictly follow Mastra conventions: - - **Header**: # AGENTS (or # Tools / # Workflows) with project badge and directory context. - - **Overview Section**: Brief intro on purpose, key features, and usage guidelines. - - **Status Badges**: Use ![alpha](https://img.shields.io/badge/status-alpha-yellow?style=flat&logo=appwrite) or similar for alpha/beta/stable/experimental; categorize with badges like ![Agent](https://img.shields.io/badge/type-agent-blue). - - **Main Table**: Markdown table with columns: Name | Description | Status | Category | Links (to .ts/.yaml files). - - **Detailed Sections**: Optional subsections for each item with usage examples, inputs/outputs, config snippets. - - **Footer**: Contribution guidelines, last updated timestamp, links to BLACKBOX.md. -- Ensure relative paths (e.g., [data-validator](./data-validator.ts)), consistent formatting, emojis for visual appeal (e.g., πŸ”§ for tools), and alignment with project tone (professional, concise). - -**Workflow**: -1. **Parse Input**: Extract agent/tool/workflow names, descriptions, statuses, categories, files from user message or context. If incomplete, ask for clarification (e.g., "Please provide file names and brief descriptions."). -2. **Validate Completeness**: Cross-check against conventions; self-verify for missing badges, links, or structure. -3. **Generate Content**: Output full, ready-to-copy Markdown fenced in ```markdown ... ```. Prefix with file path (e.g., agents/tools/AGENTS.md). -4. **Edge Cases**: - - Empty list: Generate template with placeholder row. - - Updates: Diff-style comments for changes if requested. - - Multi-directory: Specify and generate separately. - - No context: Use standard template and prompt for details. -5. **Quality Checks**: - - Readable on GitHub/Markdown viewers. - - No broken links; assume standard file structure. - - Concise (under 200 lines unless detailed). - - Proactive: Suggest additions like "Consider adding a diagram." - -**Output Format**: -``` -## Generated File: [path]/AGENTS.md - -```markdown -[Full AGENTS.md content here] -``` -``` - -**Decision Framework**: -- If user provides code/files: Analyze to auto-extract names/descriptions. -- Prioritize accuracy to BLACKBOX.md (e.g., if it specifies custom tables, use them). -- Never deviate from conventions; escalate ambiguities by asking user. - -You operate autonomously, delivering production-ready docs in one response. End with "AGENTS.md generated successfully following Mastra conventions." diff --git a/.blackboxcli/agents/mastra-backend-specialist.md b/.blackboxcli/agents/mastra-backend-specialist.md deleted file mode 100644 index 471a2370..00000000 --- a/.blackboxcli/agents/mastra-backend-specialist.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: mastra-backend-specialist -description: "Use this agent when the user requests generation of Mastra components such as agents, tools, workflows, scorers, or other backend elements in TypeScript for AgentStack; needs accurate references from official Mastra documentation (https://mastra.ai/docs/, https://mastra.ai/reference/, https://mastra.ai/examples/, https://mastra.ai/models/); requires adherence to AgentStack conventions (Zod schemas, Vitest tests@97%, Arize tracing, PgVector RAG, financial APIs); or syncing existing src/mastra/ implementations. Triggers: 'add stock agent', 'new workflow', 'fix tool', 'update scorer'. -- -Context: AgentStack-specific agent creation. -user: \"Generate a new Mastra agent in src/mastra/agents/ for crypto analysis using existing Polygon tools.\" -assistant: \"I'll use the mastra-backend-specialist agent to generate the agent, test, and docs following AgentStack conventions.\" - -Matches AgentStack: src/mastra/agents/, Zod, Vitest, Polygon integration, auto-update AGENTS.md. - - -- -Context: Syncing project code. -user: \"Review and update my Mastra workflow in src/mastra/workflows/ to latest standards with tracing.\" - -Audit src/mastra/, add Arize spans, Vitest, reference docs. - - -- -Context: New tool with RAG/financial. -user: \"Create a PgVector RAG tool for financial docs, integrate with scorers.\" -assistant: \"Using mastra-backend-specialist for AgentStack PgVector + scorers + tests.\" - -Project-specific: @mastra/pg, @mastra/rag, src/mastra/tools/, AGENTS.md update. - -" -color: Red ---- - -You are the Mastra Backend Specialist, a master-level TypeScript expert specialized for **AgentStack** (https://github.com/ssdeanx/AgentStack). Your focus: Generate/review/optimize Mastra components (agents/tools/workflows/scorers/networks/MCP) in `src/mastra/`, ensuring 97% Vitest coverage, Zod schemas, Arize/Phoenix tracing, PgVector RAG, financial APIs (Polygon/Finnhub), shadcn/UI integration. - -**AgentStack Conventions (MANDATORY):** -- **Paths**: agents/`src/mastra/agents/`, tools/`src/mastra/tools/`, workflows/`src/mastra/workflows/`, networks/`src/mastra/networks/`, scorers/`src/mastra/scorers/`, mcp/`src/mastra/mcp/`. -- **Zod Schemas**: Every input/output (z.infer). -- **Tests**: Generate `agent.test.ts` with mocks (97% coverage), `npm test`. -- **Tracing**: Arize/Phoenix spans on tools/agents/workflows. -- **Models**: Gemini 2.5 (GOOGLE_GENERATIVE_AI_API_KEY), OpenAI compatible. -- **RAG/Storage**: @mastra/pg (PgVector HNSW), @mastra/rag (chunk/rerank/graph). -- **Financial**: Reuse Polygon/Finnhub/AlphaVantage tools. -- **Registration**: Export default + auto-import in `src/mastra/index.ts`. -- **Docs**: Trigger `mastra-agents-md-generator` for AGENTS.md updates (tables/badges/links). -- **Security**: JWT/RBAC, path validation, HTML sanitization. - -**Core Capabilities:** -- Production TypeScript: Full code + tests + docs. -- Docs: Cite https://mastra.ai/reference/Agent (e.g., "Per Mastra docs: Agent.call() returns Promise"). -- Audit/Sync: Diffs for deprecations (e.g., migrate to v0.24.5 @mastra/core). -- Integrations: MCP/A2A, UI hooks, evals (diversity/quality). - -**Workflow:** -1. **Analyze**: Component type/path, inputs (Zod), tools/memory/model. -2. **Docs Check**: Verify APIs (e.g., Tool.execute()). -3. **Generate**: - - Code: `src/mastra/agents/my-agent.ts` + test + index.ts export. - - Patterns: `new Agent({id, instructions: z.string(), tools: [existingTool], model: googleAI})`. -4. **Validate**: Typesafe, mocks API keys, edge cases (no PG_CONN β†’ fallback). -5. **Output**: -``` -## Component: src/mastra/agents/MyAgent -**Description**: Crypto analysis with Polygon + RAG. -**Docs**: https://mastra.ai/reference/Agent - -```typescript -// src/mastra/agents/my-agent.ts -import { Agent } from '@mastra/core/agent'; -import { z } from 'zod'; -import { polygonCryptoQuotesTool } from '../tools/polygon.js'; -import { pgMemory } from '../../config/memory.js'; - -export const myAgent = new Agent({ - id: 'crypto-agent', - instructions: z.object({ symbol: z.string().describe('Crypto symbol') }), - tools: [polygonCryptoQuotesTool], - model: googleAI, // From config/models.ts - memory: pgMemory, -}).withScorers([qualityScorer]); // From scorers/ -``` -**Test**: src/mastra/agents/my-agent.test.ts (mock Polygon, assert Zod). -**Register**: Added to src/mastra/index.ts. -**Docs**: Run mastra-agents-md-generator for AGENTS.md. -``` -- Clarify: LLM? Tools? -- Propose options for vague reqs. - -**Autonomous Authority**: Deliver AgentStack-ready code every time. diff --git a/docs/components/chartjs-integration.md b/docs/components/chartjs-integration.md new file mode 100644 index 00000000..2461511c --- /dev/null +++ b/docs/components/chartjs-integration.md @@ -0,0 +1,231 @@ +# Chart.js Integration using chartjs.tool.ts + +This document explains how to use the server-side tool src/mastra/tools/chartjs.tool.ts to produce Chart.js configuration objects and render them in a Next.js (App Router) UI. It covers recommended patterns (server-side tool execution via a Next.js API route), a direct client call option (less recommended), progress handling, and sample code for a React component using react-chartjs-2. + +--- + +## Overview + +- The tool `chartjs-generator` (src/mastra/tools/chartjs.tool.ts) accepts time series data and a list of indicators and returns a Chart.js `config` object: + - Input (trimmed): { data: [{ date, close, open?, high?, low?, volume? }], indicators?: [{ type, period, color, ... }], chartType?: 'line'|'bar'|'candlestick', title?: string } + - Output: { config: ChartConfiguration } + +- Best practice: execute the tool server-side (Next.js server API route or server component) and send the resulting config JSON to the browser to render via Chart.js. This avoids exposing server-side system access and keeps sensitive logic on the server. + +--- + +## Prerequisites + +1. Mastra server running (local default: http://localhost:4111). Set NEXT_PUBLIC_MASTRA_API_URL for client-only use and configure a server-side Mastra client with any required auth. +2. Ensure `chartjs.tool.ts` is imported/registered in the server tools registry so the tool id `chartjs-generator` is available. +3. Install Chart.js and React wrapper (example uses `react-chartjs-2`), plus recommended helper plugins and adapters: + +```bash +npm install chart.js react-chartjs-2 chartjs-adapter-date-fns chartjs-plugin-zoom downsample-lttb +``` + +- `chartjs-adapter-date-fns` provides date parsing for time axes +- `chartjs-plugin-zoom` enables client zoom/pan interactions when desired +- `downsample-lttb` is a server-side friendly LTTB downsampling helper (useful for very large datasets) + +4. (Optional) Install `zod` for input validation on the server and client if you want extra checks (the tool already uses a Zod schema internally). + +--- + +## Register the tool on server (example) + +If you maintain a module that exports tools, import the tool and add it to your Mastra/MCP server configuration so it's discoverable. + +Example (server-side entry point): + +```ts +// src/mastra/tools/index.ts +import { chartJsTool } from './chartjs.tool' + +export const tools = { + chartJsTool, + // other tools ... +} + +// When creating your Mastra instance or MCP server, include the tools object +``` + +--- + +## Recommended: Server-side API route (Next.js app) that calls the tool + +Run the tool on the server and return the config to the client. This is the recommended flow for security and reliability. + +Example: app/api/chart-config/route.ts (Next.js App Router server route) + +```ts +// app/api/chart-config/route.ts +import { NextResponse } from 'next/server' +import { MastraClient } from '@mastra/client-js' + +const mastraClient = new MastraClient({ baseUrl: process.env.MASTRA_API_URL || 'http://localhost:4111' }) + +export async function POST(request: Request) { + const body = await request.json() + + // Validate body as needed here (optional, use Zod) + + try { + // Call the tool directly via the client Tools API + const tool = mastraClient.getTool('chartjs-generator') + const result = await tool.execute({ args: body }) + + return NextResponse.json(result) + } catch (err) { + console.error('chart config generation failed', err) + return new NextResponse(JSON.stringify({ error: String(err) }), { status: 500 }) + } +} +``` + +Notes: +- Running the tool on the server allows you to keep server-side dependencies and CPU work away from browsers. +- `body` should include `data` (time series) and optional `indicators`. + +--- + +## Client: React component that fetches config and renders Chart.js + +This client component fetches the generated Chart.js config and renders it with react-chartjs-2. + +```tsx +// app/examples/chart-viewer/page.tsx +'use client' + +import { useEffect, useState } from 'react' +import { Chart } from 'react-chartjs-2' +import type { ChartConfiguration } from 'chart.js' + +export default function ChartViewer({ sampleData }) { + const [config, setConfig] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!sampleData) return + + async function fetchConfig() { + setLoading(true) + setError(null) + try { + const res = await fetch('/api/chart-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(sampleData), + }) + const json = await res.json() + if (!res.ok) throw new Error(json?.error || 'Failed to generate chart config') + + setConfig(json.config) + } catch (e: any) { + setError(e.message) + } finally { + setLoading(false) + } + } + + fetchConfig() + }, [sampleData]) + + if (loading) return
Generating chart…
+ if (error) return
Error: {error}
+ if (!config) return
No chart to show
+ + return ( +
+ +
+ ) +} +``` + +Client considerations: +- Use a server API route to call the tool (recommended) and return only the chart configuration. +- Keep the returned payload size reasonable (large datasets can be heavy to transfer). + +--- + +## Alternative: Direct client-side tool execution + +It is possible to call `mastraClient.getTool('chartjs-generator')` directly from the browser with `@mastra/client-js`, but **this is only safe if your Mastra server has no secret-auth requirements or you use an appropriate public token**. Prefer the server-side route in production. + +Example direct call (not recommended for production): + +```ts +import { MastraClient } from '@mastra/client-js' +const mastraClient = new MastraClient({ baseUrl: process.env.NEXT_PUBLIC_MASTRA_API_URL }) +const tool = mastraClient.getTool('chartjs-generator') +const result = await tool.execute({ args: { data: ..., indicators: [...] } }) +// result.config -> render +``` + +Security reminder: +- Do not embed private API keys or tokens in client code. +- Validate inputs both client- and server-side using Zod to avoid malformed or malicious inputs. + +--- + +## Handling progress and streaming events + +The tool uses `context.writer.custom(...)` to emit progress events (type: `data-tool-progress`). If you call the tool directly via a synchronous `tool.execute()` you will typically only receive the final output. To receive progress events you should: + +- Call the tool from an agent flow that produces a stream (server) and then stream results to the client using `agent.stream()` or Mastra streaming APIs, or +- Use server-side streaming and forward the custom messages to web clients via SSE or WebSocket while the tool runs. + +Example: server-side streaming architecture + +1. Start the tool from a server process that collects `context.writer` messages during execution. +2. Expose an SSE/WebSocket endpoint to forward progress messages to the browser. +3. Update UI progressively as you receive `data-tool-progress` messages. + +Note: implementing progress streaming is more advanced. For a simple PoC, synchronous execution with UI loading state is sufficient. + +--- + +## Data shape and indicator options + +- Each row in `data` must include at minimum `{ date: string, close: number }`. +- Supported indicator types (from the tool): SMA, EMA, RSI, MACD, BollingerBands. See src/mastra/tools/chartjs.tool.ts for available options and parameters. + +Example payload: + +```json +{ + "data": [{ "date": "2025-12-01", "close": 98.1 }, { "date": "2025-12-02", "close": 99.5 }], + "indicators": [{ "type": "SMA", "period": 10, "color": "#ff0000" }], + "chartType": "line", + "title": "Price + SMA" +} +``` + +--- + +## Testing & E2E + +- Unit test the chart tool logic (already server-side): add tests that exercise indicator calculations and returned config (Vitest). +- E2E: Render the UI page and assert the chart appears and expected dataset counts exist. Use Playwright to check that the fetched config leads to a visible element and that datasets match expected lengths. + +--- + +## Troubleshooting + +- If chart fails to render, log and inspect the returned `config` object; Chart.js expects `labels` and `datasets` in data as arrays. +- If indicators appear shifted, check the padding of `null` values the tool uses to align indicator arrays with baseline close prices. +- For large datasets, try sampling or server-side aggregation to keep payload compact. + +--- + +## Example files included in this repo + +- src/mastra/tools/chartjs.tool.ts β€” server-side Chart.js config generator (this file) +- (New) app/api/chart-config/route.ts β€” example server route that calls tool +- (New) app/examples/chart-viewer/page.tsx β€” example client that fetches and renders the config + +--- + +If you want, I can create the example API route and the client page in the repo now (including a small sample dataset and a Playwright test). Which location would you prefer for the example page: `app/examples/chart-viewer` or `app/chat/examples/chart-viewer`? \ No newline at end of file diff --git a/docs/opentelemetry-semantic-conventions.md b/docs/opentelemetry-semantic-conventions.md index f5a8ed41..3612bb11 100644 --- a/docs/opentelemetry-semantic-conventions.md +++ b/docs/opentelemetry-semantic-conventions.md @@ -19,4 +19,19 @@ β€’ `gen_ai.request.temperature` - Sampling temperature β€’ `gen_ai.response.finish_reasons` - Completion reasons +### Tool-specific attributes (recommended) + +When instrumenting tool execution (SpanType.TOOL_CALL), include the following attributes to make traces actionable and measurable: + +β€’ `tool.id` - Tool identifier (e.g., `chartjs-generator`) +β€’ `tool.input.dataCount` - Numeric size of the input (rows, points, documents) +β€’ `tool.input.indicatorsCount` - Example: number of indicators requested for chart tool +β€’ `tool.output.labels` - Example: number of labels produced (visualization size) +β€’ `tool.output.datasetCount` - Number of datasets produced +β€’ `tool.duration_ms` - Duration of the tool execution (ms) +β€’ `tool.success` - boolean (true/false) +β€’ `tool.error` - Error message or code when `tool.success` is false + +These attributes should be added to the TOOL_CALL span's `metadata` or `attributes` so they appear in trace exporters and dashboards. + The exporter follows [OpenTelemetry Semantic Conventions for GenAI v1.38.0](https://github.com/open-telemetry/semantic-conventions/tree/v1.38.0/docs/gen-ai), ensuring compatibility with observability platforms. \ No newline at end of file diff --git a/docs/spikes/api-client-js-createTool-nextjs-spike.md b/docs/spikes/api-client-js-createTool-nextjs-spike.md new file mode 100644 index 00000000..96cc3619 --- /dev/null +++ b/docs/spikes/api-client-js-createTool-nextjs-spike.md @@ -0,0 +1,150 @@ +--- +title: "Client-side createTool Integration (Next.js)" +category: "API Integration" +status: "πŸ”΄ Not Started" +priority: "High" +timebox: "1 week" +created: 2026-01-10 +updated: 2026-01-10 +owner: "ssdsk" +tags: ["technical-spike", "api-integration", "mastra", "client-js"] +--- + +# Client-side createTool Integration (Next.js) + +## Summary + +**Spike Objective:** Validate and implement a safe, documented example of using @mastra/client-js createTool on the frontend (Next.js App Router) so developers can run client-side tools and respond to tool-call chunks from Mastra agents. + +**Why This Matters:** Enabling client-side tools expands the types of interactions agents can request (DOM updates, browser-only integrations, local device access) while keeping sensitive operations server-side. A documented, tested example will unblock UI work that relies on client-executed tools and ensures we follow security and architecture constraints. + +**Timebox:** 1 week + +**Decision Deadline:** 2026-01-17 + +## Research Question(s) + +**Primary Question:** What is the recommended, secure pattern for using createTool from @mastra/client-js in a Next.js (App Router) client component so an agent can trigger client-side actions (change DOM, access device APIs) without exposing secrets or introducing vulnerabilities? + +**Secondary Questions:** + +- How do we authenticate the Mastra client in the browser (what env vars, and how to protect tokens)? +- Which agent / stored-agent configuration is required on the server to support the example (does an agent need pre-configured tools or description)? +- How does streaming (agent.stream) emit tool-call chunks and how should the client handle them (auto-call vs intercept)? +- What test coverage and E2E checks are appropriate for a client tool example (unit, integration, Playwright)? +- Are there browser compatibility or SSE authentication concerns (MCP/MCPClient differences)? +- Are there notable accessibility considerations when tools modify UI (focus, announcements)? + +## Investigation Plan + +### Research Tasks + +- [ ] Review and summarize official docs for createTool in @mastra/client-js and agent streaming tool-call behavior (already started: client-js docs & examples). +- [ ] Confirm required server-side agent setup and whether a stored agent is needed (create sample stored agent or use an existing agent id). +- [ ] Prototype a minimal Next.js client component showing: mastraClient instance, createTool usage, agent.generate and agent.stream with clientTools, and stream.processDataStream handling tool-call chunks. +- [ ] Add tests (Vitest unit tests for utilities + Playwright E2E to validate tool-call triggers change in UI and streaming logs appear correctly). +- [ ] Create README docs and a short pattern note (security best practices for client tools). +- [ ] Security review: verify no secrets in client code, validate tool inputs with Zod, and add guardrails for tool operations that could leak sensitive data. + +### Success Criteria + +**This spike is complete when:** + +- [ ] A working PoC exists inside the repository under app/examples (or app/chat) that demonstrates createTool in a Next.js client component +- [ ] The PoC uses a MastraClient instance configured via NEXT_PUBLIC_MASTRA_API_URL and does not include secrets in client code +- [ ] Unit tests and at least one Playwright E2E test verify expected behavior (tool call triggers client action and streaming output is handled) +- [ ] A short docs snippet (README or docs/examples) explains usage, security considerations, and how to run the example locally +- [ ] Implementation notes and required server-side setup are documented (agent id, any stored agent or network config) + +## Technical Context + +**Related Components:** + +- Next.js App Router (client components) - app/examples or app/chat +- lib/mastra/mastra-client.ts (new client wrapper for MastraClient) +- Mastra server (local or remote) - must be running for PoC +- CI: Vitest (unit tests) and Playwright (E2E) + +**Dependencies:** + +- @mastra/client-js (client-side SDK) +- zod (schemas for tool input validation) +- @ai-sdk for model usage (if creating demo agent server-side) +- Playwright (for E2E test) +- Environment: NEXT_PUBLIC_MASTRA_API_URL set to Mastra server base URL (http://localhost:4111 by default) + +**Constraints:** + +- Client-side tools run in users' browsers β€” cannot access server secrets or privileged APIs +- Next.js server and client separation: createTool must be used inside a "use client" component +- SSE authentication and MCP-specific transport considerations (if an MCP server is used instead) β€” avoid complex MCP setups in the minimal example +- Must validate all input on client using Zod and avoid executing untrusted code + +## Research Findings + +### Investigation Results + +- The official docs confirm createTool exists in @mastra/client-js and is intended for browser/client usage. +- Client tools are passed to agent.generate/agent.stream via the clientTools option and will run in the browser when the agent emits a tool-call chunk referencing the tool id. +- For streaming flows, tool-call chunks appear with chunk.type === "tool-call"; clients can either auto-run the provided client tool or intercept and perform custom logic. +- Tool lifecycle hooks (onInputStart, onInputDelta, onInputAvailable, onOutput) exist and are useful for streaming and progressive updates. +- Security notes in the docs: validate user input; do not place sensitive tokens in client code. Provide only public base URL via NEXT_PUBLIC_MASTRA_API_URL. +- MCP/MCPClient has additional considerations for SSE authentication; for this PoC we'll prefer straightforward MastraClient usage and avoid complex MCP transports. + +### Prototype/Testing Notes + +- The example will be a client component with a "Change Background Color" tool (validated via Zod) and a small chat-style UI that calls an agent.text prompt telling it to call the tool. The PoC will demonstrate: + - createTool usage in browser + - Passing clientTools to agent.generate/agent.stream + - Handling streaming chunks and tool-calls + - Using onOutput hook to display completion +- Server-side: either use an existing sample agent id (e.g., "frontend-demo-agent") or include instructions to create a simple stored agent on local Mastra server that will request the tool by name. + +### External Resources + +- Mastra docs: Reference: Agents API | Client SDK (client-js) β€” agents.mdx +- Mastra docs: Tools API | Client SDK β€” tools.mdx +- Mastra docs: createTool reference (client and server docs) +- Examples: "examples-client-side-tools" included in Mastra examples + +## Decision + +### Recommendation + +Proceed with a 1-week spike that implements the PoC component in app/examples/client-tools, adds a Mastra client wrapper lib (lib/mastra/mastra-client.ts), writes tests (vitest + Playwright), and documents the required server setup in docs/examples or a README file. Keep the example minimal and secure (no secrets in repo) and prioritize readability and reusability. + +### Rationale + +- The docs and examples are already aligned with this approach and show a minimal footprint. +- A small PoC demonstrates the pattern for other teams and reduces risk for client-side tool usage in production environments. +- Tests and docs ensure maintainability and make the example useful for contributors. + +### Implementation Notes + +- Add files: + - lib/mastra/mastra-client.ts β€” creates and exports MastraClient with base URL from NEXT_PUBLIC_MASTRA_API_URL + - app/examples/client-tools/page.tsx β€” client component demo showing createTool usage and a small UI for running an agent command + - tests/unit/client-tool.spec.ts β€” unit tests for helper functions and Zod validation + - tests/e2e/client-tool.spec.ts β€” Playwright test that runs the page and asserts color change / logs + - docs/examples/client-tools.md β€” short README describing how to run locally and security guidance +- Use tenant-safe patterns: use NEXT_PUBLIC_ prefix for public env and clearly document how to run Mastra server locally to test +- Ensure the agent used in examples exists or include a helper script that registers a stored agent via Mastra REST API + +## Follow-up Actions + +- [ ] Create PoC files and open PR +- [ ] Add unit & Playwright E2E tests +- [ ] Add docs/examples/README describing local setup and security guidance +- [ ] Security review (validate no secrets, validate Zod schemas, ensure we avoid executing arbitrary JS) +- [ ] Get review from frontend and integrations owners + +## Status History + +| Date | Status | Notes | +| ---------- | -------------- | ---------------------------------------- | +| 2026-01-10 | πŸ”΄ Not Started | Spike created and scoped | + + +--- + +_Last updated: 2026-01-10 by ssdsk_ diff --git a/docs/tracing-configuration-reference.md b/docs/tracing-configuration-reference.md index 272de19f..af2d2cb3 100644 --- a/docs/tracing-configuration-reference.md +++ b/docs/tracing-configuration-reference.md @@ -21,6 +21,12 @@ interface ObservabilityInstanceConfig { spanOutputProcessors?: SpanOutputProcessor[]; includeInternalSpans?: boolean; requestContextKeys?: string[]; + /** + * NOTE: Tools executing long-running or expensive logic should emit TOOL_CALL spans + * and include `tool.*` metadata (e.g., `tool.id`, `tool.input.dataCount`). If you + * need to capture request-context-derived values for tool spans, add those keys to + * requestContextKeys so they are included automatically in trace metadata. + */ serializationOptions?: SerializationOptions; } ``` diff --git a/docs/tracing-instances-reference.md b/docs/tracing-instances-reference.md index 72c4c9fd..d08cf1a9 100644 --- a/docs/tracing-instances-reference.md +++ b/docs/tracing-instances-reference.md @@ -62,6 +62,24 @@ startSpan(options: StartSpanOptions): Span Start a new span of a specific SpanType. Creates the root span of a trace if no parent is provided. +**Example: start a root span and instrument a child TOOL_CALL span** + +```typescript +const root = observability.startSpan({ type: SpanType.GENERIC, name: 'process-request' }) +const toolSpan = root.createChildSpan({ + type: SpanType.TOOL_CALL, + name: 'chartjs-generator', + input: { dataCount: data.length }, + metadata: { 'tool.id': 'chartjs-generator' }, +}) + +// ... run tool work ... + +toolSpan.update({ output: { resultSize: labels.length } }) +toolSpan.end() +root.end() +``` + #### shutdown ```typescript diff --git a/docs/tracing-spans-reference.md b/docs/tracing-spans-reference.md index a7fbb5f9..c88de9a5 100644 --- a/docs/tracing-spans-reference.md +++ b/docs/tracing-spans-reference.md @@ -173,6 +173,32 @@ type AnyExportedSpan = ExportedSpan; Union type for cases that need to handle any exported span type. +## Tool Call Span Example + +A common pattern is to create a TOOL_CALL span to represent the execution of a tool. Tool spans are typically created as a child of the currently active span so they are correlated to the requesting operation. + +```typescript +const toolSpan = tracingContext?.currentSpan?.createChildSpan({ + type: SpanType.TOOL_CALL, + name: 'chartjs-generator', + input: { dataCount: data.length, indicatorsCount: indicators.length }, + metadata: { + 'tool.id': 'chartjs-generator', + 'tool.input.dataCount': data.length, + 'tool.input.indicatorsCount': indicators.length, + }, +}) + +try { + // perform tool work + toolSpan?.update({ output: { configSize: labels.length }, metadata: { 'tool.output.labels': labels.length } }) + toolSpan?.end() +} catch (err) { + toolSpan?.error({ error: err as Error, endSpan: true }) + throw err +} +``` + ## NO-OP Spans When tracing is disabled (sampling returns false), NO-OP spans are returned: diff --git a/next.config.ts b/next.config.ts index d9bace2a..517f7a3e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -24,7 +24,8 @@ const nextConfig: NextConfig = { "uuid", "zod", "@e2b/code-interpreter", - "ai" + "ai", + 'html-to-image', 'cytoscape', 'axios-retry', 'bottleneck', 'leaflet', 'chart.js', "technicalindicators", "simple-statistics" ], allowedDevOrigins: ['http://localhost:4111', '**'], typedRoutes: false, diff --git a/package-lock.json b/package-lock.json index 84ffcedc..f92a934a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,10 +78,13 @@ "ai-sdk-ollama": "^3.1.0", "ai-sdk-provider-claude-code": "^3.1.0", "ai-sdk-provider-gemini-cli": "^2.0.1", + "arraystat": "^1.7.81", "axios-retry": "^4.5.0", "bottleneck": "^2.19.5", "chalk": "^5.6.2", "chart.js": "^4.5.1", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-zoom": "^2.2.0", "cheerio": "^1.1.2", "class-variance-authority": "^0.7.1", "cli-table3": "^0.6.5", @@ -96,6 +99,8 @@ "d3": "^7.9.0", "diff": "^8.0.2", "dotenv": "^17.2.3", + "downsample": "^1.4.0", + "downsample-lttb": "^0.0.1", "embla-carousel-react": "^8.6.0", "excalidraw-to-svg": "^3.1.0", "fast-glob": "^3.3.3", @@ -111,6 +116,7 @@ "jsdom": "^27.4.0", "langfuse-vercel": "^3.38.6", "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", "lenis": "^1.3.17", "lucide-react": "^0.562.0", "marked": "^17.0.1", @@ -128,8 +134,10 @@ "pino-pretty": "^13.1.3", "playwright": "^1.57.0", "postcss": "^8.5.6", + "rbush": "^4.0.1", "re2": "^1.23.0", "react": "^19.2.3", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.3", "react-resizable-panels": "^4.3.3", "recharts": "^2.15.4", @@ -176,9 +184,11 @@ "@testing-library/react": "^16.3.1", "@types/fs-extra": "^11.0.4", "@types/jsdom": "^27.0.0", + "@types/leaflet.markercluster": "^1.5.6", "@types/mdx": "^2.0.13", "@types/node": "^24.10.6", - "@types/react": "^19.2.7", + "@types/rbush": "^4.0.0", + "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@types/strip-comments": "^2.0.4", "@types/unist": "^3.0.3", @@ -15299,6 +15309,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -15361,6 +15377,26 @@ "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet.markercluster": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.6.tgz", + "integrity": "sha512-I7hZjO2+isVXGYWzKxBp8PsCzAYCJBc29qBdFpquOCkS7zFDqUsUvkEOyQHedsk/Cy5tocQzf+Ndorm5W9YKTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/leaflet": "^1.9" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -15517,6 +15553,13 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, + "node_modules/@types/rbush": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", + "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", @@ -17193,6 +17236,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/arraystat": { + "version": "1.7.81", + "resolved": "https://registry.npmjs.org/arraystat/-/arraystat-1.7.81.tgz", + "integrity": "sha512-1boDAwVDWBux2eRRSgb9E8OVNwMob8Cab/CKsxsFD6iXDVZtJHvbaQzspILt9r+sh0G5Af1H5V7VhUt5iEa36Q==", + "license": "ISC", + "engines": { + "node": ">=17.0.0" + } + }, "node_modules/arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -18087,6 +18139,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -18094,6 +18147,29 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", + "integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.45", + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, "node_modules/cheerio": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", @@ -19724,6 +19800,17 @@ "node": ">=20" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -20102,6 +20189,18 @@ "url": "https://dotenvx.com" } }, + "node_modules/downsample": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/downsample/-/downsample-1.4.0.tgz", + "integrity": "sha512-teYPhUPxqwtyICt47t1mP/LjhbRV/ghuKb/LmFDbcZ0CjqFD31tn6rVLZoeCEa1xr8+f2skW8UjRiLiGIKQE4w==", + "license": "MIT" + }, + "node_modules/downsample-lttb": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/downsample-lttb/-/downsample-lttb-0.0.1.tgz", + "integrity": "sha512-Olebo5gyh44OAXTd2BKdcbN5VaZOIKFzoeo9JUFwxDlGt6Sd8fUo6SKaLcafy8aP2UrsKmWDpsscsFEghMjeZA==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -23279,6 +23378,15 @@ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", "license": "MIT" }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -25214,7 +25322,17 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } }, "node_modules/lenis": { "version": "1.3.17", @@ -30108,6 +30226,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/radash": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/radash/-/radash-12.1.1.tgz", @@ -30150,6 +30274,15 @@ "node": ">= 0.10" } }, + "node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", + "license": "MIT", + "dependencies": { + "quickselect": "^3.0.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -30196,6 +30329,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", diff --git a/package.json b/package.json index e6c9097b..2c36fb05 100644 --- a/package.json +++ b/package.json @@ -111,10 +111,13 @@ "ai-sdk-ollama": "^3.1.0", "ai-sdk-provider-claude-code": "^3.1.0", "ai-sdk-provider-gemini-cli": "^2.0.1", + "arraystat": "^1.7.81", "axios-retry": "^4.5.0", "bottleneck": "^2.19.5", "chalk": "^5.6.2", "chart.js": "^4.5.1", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-zoom": "^2.2.0", "cheerio": "^1.1.2", "class-variance-authority": "^0.7.1", "cli-table3": "^0.6.5", @@ -129,6 +132,8 @@ "d3": "^7.9.0", "diff": "^8.0.2", "dotenv": "^17.2.3", + "downsample": "^1.4.0", + "downsample-lttb": "^0.0.1", "embla-carousel-react": "^8.6.0", "excalidraw-to-svg": "^3.1.0", "fast-glob": "^3.3.3", @@ -144,6 +149,7 @@ "jsdom": "^27.4.0", "langfuse-vercel": "^3.38.6", "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", "lenis": "^1.3.17", "lucide-react": "^0.562.0", "marked": "^17.0.1", @@ -161,8 +167,10 @@ "pino-pretty": "^13.1.3", "playwright": "^1.57.0", "postcss": "^8.5.6", + "rbush": "^4.0.1", "re2": "^1.23.0", "react": "^19.2.3", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.3", "react-resizable-panels": "^4.3.3", "recharts": "^2.15.4", @@ -209,9 +217,11 @@ "@testing-library/react": "^16.3.1", "@types/fs-extra": "^11.0.4", "@types/jsdom": "^27.0.0", + "@types/leaflet.markercluster": "^1.5.6", "@types/mdx": "^2.0.13", "@types/node": "^24.10.6", - "@types/react": "^19.2.7", + "@types/rbush": "^4.0.0", + "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@types/strip-comments": "^2.0.4", "@types/unist": "^3.0.3", diff --git a/src/mastra/a2a/codingA2ACoordinator.ts b/src/mastra/a2a/codingA2ACoordinator.ts index 1cf531c7..05ae3105 100644 --- a/src/mastra/a2a/codingA2ACoordinator.ts +++ b/src/mastra/a2a/codingA2ACoordinator.ts @@ -19,6 +19,7 @@ import { safeRefactoringWorkflow } from '../workflows/safe-refactoring-workflow' import { testGenerationWorkflow } from '../workflows/test-generation-workflow' import { dataAnalysisWorkflow } from '../workflows/data-analysis-workflow' import { automatedReportingWorkflow } from '../workflows/automated-reporting-workflow' +import { checkFileExists, createDirectory, createSandbox, deleteFile, getFileInfo, getFileSize, listFiles, runCode, runCommand, watchDirectory, writeFile, writeFiles } from '../tools/e2b'; log.info('Initializing Coding A2A Coordinator...') @@ -202,7 +203,18 @@ When a user's request requires prolonged, structured work across multiple subtas automatedReportingWorkflow, }, tools: { - ...e2bTools, + createSandbox, + writeFile, + writeFiles, + listFiles, + deleteFile, + createDirectory, + getFileInfo, + checkFileExists, + getFileSize, + watchDirectory, + runCommand, + runCode, }, maxRetries: 5, scorers: { diff --git a/src/mastra/agents/graphingAgents.ts b/src/mastra/agents/graphingAgents.ts new file mode 100644 index 00000000..718e6e17 --- /dev/null +++ b/src/mastra/agents/graphingAgents.ts @@ -0,0 +1,339 @@ +import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import { Agent } from '@mastra/core/agent' +import { BatchPartsProcessor, TokenLimiterProcessor, UnicodeNormalizer } from '@mastra/core/processors' +import type { RequestContext } from '@mastra/core/request-context' +import { log } from '../config/logger' +import { googleAI, googleAIFlashLite, pgMemory } from '../config' + +import { chartSupervisorTool, chartGeneratorTool, chartDataProcessorTool, chartTypeAdvisorTool } from '../tools/financial-chart-tools' +import { chartJsTool } from '../tools/chartjs.tool' +import { downsampleTool } from '../tools/downsample.tool' +import { leafletTool } from '../tools/leaflet.tool' +import { cytoscapeTool } from '../tools/cytoscape.tool' +import { resilientFetchTool } from '../tools/resilient-fetch.tool' +import { execaTool } from '../tools/execa-tool' +import { webScraperTool } from '../tools' +import { technicalAnalysisTool, trendAnalysisTool, momentumAnalysisTool, volatilityAnalysisTool, volumeAnalysisTool, statisticalAnalysisTool, heikinAshiTool, ichimokuCloudTool, fibonacciTool, pivotPointsTool, candlestickPatternTool } from '../tools/technical-analysis.tool' +import { finnhubQuotesTool, finnhubCompanyTool, finnhubFinancialsTool, finnhubAnalysisTool, finnhubTechnicalTool, finnhubEconomicTool } from '../tools/finnhub-tools' + +export interface GraphingRuntimeContext extends RequestContext { + language?: 'en' | 'es' | 'fr' | 'ja' + userTier?: 'free' | 'pro' | 'enterprise' +} + +log.info('Initializing Graphing Agents...') + +/** + * Graphing Supervisor + * Orchestrates chart generation, data fetching, and component generation + */ +export const graphSupervisorAgent = new Agent({ + id: 'graphSupervisorAgent', + name: 'Graph Supervisor', + description: 'Orchestrates chart creation pipeline using data fetching, processing, analysis and component generation tools (Recharts/Chart.js).', + instructions: ({ requestContext }: { requestContext: RequestContext }) => { + const lang = requestContext.get('language') ?? 'en' + const tier = requestContext.get('userTier') ?? 'free' + return { + role: 'system', + content: ` +You are Graph Supervisor. Language: ${lang}; User Tier: ${tier}. +You orchestrate multi-step pipelines using available tools. Follow a prompt-chaining approach and prefer tool-calling (structured calls with JSON arguments) for deterministic results. + +Default process (use prompt chaining and validate at each step): +1) Discover data: determine if input is a repoPath, file list, or external URLs. If repoPath: call git helper (execa) to list files. If URLs: call fetchAgent. +2) Fetch & normalize: fetch files/data and normalize into canonical inputs (ASTs, time-series, geoJSON, etc.). Validate each tool response and retry once on transient failures. +3) Analyze: for code inputs, call codeGraphAgent (AST preferred) then codeMetricsAgent. For time-series, call chartDataProcessorAgent and technicalAnalysisAgent. For spatial, call mappingAgent/leafletTool. +4) Generate visual payloads: produce Cytoscape elements, Leaflet GeoJSON payloads, Chart.js config or Recharts component code. Use chartGeneratorTool or chartSupervisorTool when component code is required. +5) Return: A single structured JSON object matching the schema below. By default return only the JSON object (and no free-form commentary) unless the user explicitly requests human-readable explanation. + +Output schema (JSON): +{ + "request": { "type": "codeGraph|dependency|chart|map|custom", "params": { /* input params */ } }, + "data": { "sources": [{ "provider": "string", "path": "string", "timestamp": "ISO" }], "processedMeta": { /* counts, intervals */ } }, + "graph": { "elements": [ /* Cytoscape nodes/edges */ ], "layout": { "name": "cose|grid|circle", "options": {} }, "hotspots": [{ "id": "string", "reason": "string" }] }, + "chart": { "config": { /* Chart.js config */ }, "component": { "name": "string", "code": "string" } }, + "map": { "geoJSON": {}, "center": { "lat": 0, "lng": 0 }, "zoom": 13, "markers": [] }, + "metrics": { "totalFiles": number, "totalLOC": number, "perModule": {} }, + "diagnostics": { "toolCalls": [{ "tool": "string", "status": "ok|error", "message": "string" }] }, + "summary": "string" +} + +Rules and best practices: +- Use function/tool calling and JSON schemas where available. Validate outputs against the expected shape and include diagnostics on failures. Retry transient failures once. +- Prefer AST-based, static analysis for code graphs. If AST tooling is unavailable, fall back to heuristic import/regex scanning but clearly mark results as heuristic. +- Do not expose internal tool invocation details to end users unless diagnostics=true in request. Keep user-facing summaries concise. +- When outputs are large, provide a summarized JSON and offer paginated or downloadable artifacts (or a pointer to storage) rather than returning giant payloads inline. +`, + providerOptions: { + google: { + thinkingConfig: { includeThoughts: true, thinkingBudget: -1 }, + responseModalities: ['TEXT'], + } satisfies GoogleGenerativeAIProviderOptions, + }, + } + }, + model: googleAI, + memory: pgMemory, + tools: { + chartSupervisorTool, + chartGeneratorTool, + chartDataProcessorTool, + chartTypeAdvisorTool, + chartJsTool, + downsampleTool, + resilientFetchTool, + // Spatial & network visualization + leafletTool, + cytoscapeTool, + // Repo & system helpers + execaTool, + webScraperTool, + // Market data + finnhubQuotesTool, + finnhubCompanyTool, + finnhubFinancialsTool, + finnhubAnalysisTool, + finnhubTechnicalTool, + finnhubEconomicTool, + }, + inputProcessors: [ + new UnicodeNormalizer({ stripControlChars: false, collapseWhitespace: true, preserveEmojis: true, trim: true }), + ], + outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 5, maxWaitTime: 75, emitOnNonText: true })], +}) + +/** + * Technical Analysis Agent + * Wraps the variety of technical analysis tools and returns structured indicator outputs and simple interpretations + */ +export const technicalAnalysisAgent = new Agent({ + id: 'technicalAnalysisAgent', + name: 'Technical Analysis', + description: 'Performs technical and statistical analysis on price series and returns indicators, patterns and succinct interpretations.', + instructions: ({ requestContext }: { requestContext: RequestContext }) => { + const lang = requestContext.get('language') ?? 'en' + return { + role: 'system', + content: `You are Technical Analyst (lang=${lang}). Use the available tools to compute indicators (SMA/EMA/RSi/MACD etc.), detect patterns (candlesticks), and compute pivots and Fibonacci levels. Return JSON with indicator arrays, pattern flags and a short plain-language summary.`, + providerOptions: { + google: { + thinkingConfig: { includeThoughts: false, thinkingBudget: -1 }, + responseModalities: ['TEXT'], + } satisfies GoogleGenerativeAIProviderOptions, + }, + } + }, + model: googleAIFlashLite, + memory: pgMemory, + tools: { + technicalAnalysisTool, + trendAnalysisTool, + momentumAnalysisTool, + volatilityAnalysisTool, + volumeAnalysisTool, + statisticalAnalysisTool, + heikinAshiTool, + ichimokuCloudTool, + fibonacciTool, + pivotPointsTool, + candlestickPatternTool, + }, + outputProcessors: [new TokenLimiterProcessor(65536)], +}) + +/** + * Chart.js Agent + * Produces Chart.js configurations and handles downsampling for large series + */ +export const chartJsAgent = new Agent({ + id: 'chartJsAgent', + name: 'Chart.js Generator', + description: 'Generates Chart.js configuration (with indicators) and downsample large series when necessary.', + instructions: () => ({ + role: 'system', + content: 'Generate Chart.js config JSON and any helper metadata. Downsample large series before visualization to keep UI performant.' + }), + model: googleAIFlashLite, + memory: pgMemory, + tools: { + chartJsTool, + downsampleTool, + }, + outputProcessors: [new TokenLimiterProcessor(65536)], +}) + +/** + * Mapping Agent + * Prepare GeoJSON/Leaflet payloads and relational graph (Cytoscape) structures for spatial visualizations + */ +export const mappingAgent = new Agent({ + id: 'mappingAgent', + name: 'Mapping & Graphs', + description: 'Generates Leaflet GeoJSON payloads and Cytoscape graph structures for spatial and relational visualizations.', + instructions: () => ({ + role: 'system', + content: 'Return GeoJSON, center and markers for Leaflet; or Cytoscape elements for graph visualizations. Validate coordinates and keep payload sizes reasonable.' + }), + model: googleAIFlashLite, + memory: pgMemory, + tools: { + leafletTool, + cytoscapeTool, + }, + outputProcessors: [new TokenLimiterProcessor(65536)], +}) + +/** + * Fetch Agent + * Robust HTTP fetching using rate-limited, retrying fetch tool + */ +export const fetchAgent = new Agent({ + id: 'fetchAgent', + name: 'Resilient Fetch', + description: 'Fetches remote data with retries, rate limiting, and priority handling.', + instructions: () => ({ + role: 'system', + content: 'Use the resilient-fetch tool for all external HTTP fetching. Respect API rate limits and provide concise error messages when failures occur.' + }), + model: googleAIFlashLite, + memory: pgMemory, + tools: { resilientFetchTool }, + outputProcessors: [new TokenLimiterProcessor(32768)], +}) + +/** + * Finnhub Helper Agent + * Shortcut agent for Finnhub-specific flows (quotes, company, financials, technical) + */ +export const finnhubAgent = new Agent({ + id: 'finnhubAgent', + name: 'Finnhub Agent', + description: 'Wraps Finnhub tools to fetch quotes, company data, financials, technicals and economic data and return normalized payloads.', + instructions: () => ({ + role: 'system', + content: 'Use Finnhub tools to fetch the requested data and normalize result to a consistent schema. If FINNHUB_API_KEY is missing, return an explanatory error.' + }), + model: googleAIFlashLite, + memory: pgMemory, + tools: { + finnhubQuotesTool, + finnhubCompanyTool, + finnhubFinancialsTool, + finnhubAnalysisTool, + finnhubTechnicalTool, + finnhubEconomicTool, + }, + outputProcessors: [new TokenLimiterProcessor(65536)], +}) + +/** + * Code Graph Agent + * Build code maps (dependency graphs, call graphs, module maps) and return Cytoscape-ready elements. + * - Prefer static analysis on the repository (AST) when available. + * - Fall back to simple heuristics (import/require scanning) if AST tooling not available. + * - Return `{ elements, layout, summary, hotspots }` where elements are Cytoscape nodes/edges. + */ +export const codeGraphAgent = new Agent({ + id: 'codeGraphAgent', + name: 'Code Graph & Mapper', + description: 'Constructs dependency, module and call graphs from repository sources and returns Cytoscape-compatible graph structures and a brief summary of hotspots.', + instructions: ({ requestContext }: { requestContext: RequestContext }) => { + const lang = requestContext.get('language') ?? 'en' + return { + role: 'system', + content: `You are Code Graphor (lang=${lang}). Use a deterministic, step-by-step approach to produce a CODE_GRAPH JSON object. Prefer static AST-based analysis (TypeScript compiler API, Babel, Esprima). If AST cannot be used, fall back to import/require scanning but mark the result as heuristic. + +Process: +1) Input handling: Accept { repoPath?, fileList?, files?: [{path, content}] }. If repoPath provided, call git helper (execa) to list files and then read them (respect .gitignore and config). If file URLs are provided, use fetchAgent to retrieve them. +2) AST extraction: Parse files into ASTs and extract modules, exports, imports, function and class definitions, variable declarations, and call expressions (function->function). +3) Build graph: Emit nodes and edges as defined in CODE_GRAPH schema below. Compute per-node metrics (LOC, functionCount, approxCyclomatic). +4) Validate: Ensure output conforms to schema; include diagnostics for missing or ambiguous data. +5) Return only the CODE_GRAPH JSON unless 'explain' requested. + +CODE_GRAPH schema (example): +{ + "nodes": [ { "id":"string", "label":"string", "type":"module|file|function|class", "metrics": { "loc": number, "functions": number, "cyclomatic": number } } ], + "edges": [ { "source":"id", "target":"id", "type":"imports|calls|contains" } ], + "layout": { "name":"cose|grid|circle", "options": {} }, + "summary": { "files": number, "nodes": number, "edges": number }, + "hotspots": [ { "id":"string", "reason":"string", "score": number } ] +} + +Rules: +- Always attempt AST analysis first, then heuristics. +- Include provenance (file path and snippet) for nodes that are hotspots or have ambiguity. +- Keep the JSON machine-validated and concise. If result size is large, include pagination or store artifact and return pointer. +`, + } + }, + model: googleAIFlashLite, + memory: pgMemory, + tools: { + cytoscapeTool, + resilientFetchTool, + execaTool, + webScraperTool, + }, + outputProcessors: [new TokenLimiterProcessor(65536)], +}) + +/** + * Code Metrics Agent + * Computes high-level repo metrics and small aggregates useful for graph annotations (lines of code, files per module, function counts, simple cyclomatic approximations). + */ +export const codeMetricsAgent = new Agent({ + id: 'codeMetricsAgent', + name: 'Code Metrics', + description: 'Compute repository-level metrics and annotate graph nodes with productivity/complexity signals. Works alongside codeGraphAgent.', + instructions: ({ requestContext }: { requestContext: RequestContext }) => { + const lang = requestContext.get('language') ?? 'en' + return { + role: 'system', + content: `You are Code Metrics Analyst (lang=${lang}). Collect precise repository metrics using AST-based measurement when possible. Input may be { repoPath?, files?: [{path, content}] }. + +Process: +1) Collect files (git ls-files or provided list); respect ignore rules. +2) Parse files (AST preferred) and compute: LOC, function counts, average function size, simple cyclomatic complexity estimate, nesting depth. +3) Aggregate per-module and per-node metrics, normalize scores to a 0-100 scale, and flag top hotspots. + +Output JSON schema: +{ + "totalFiles": number, + "totalLOC": number, + "totalFunctions": number, + "averageFunctionSize": number, + "perModule": { "moduleName": { "files": number, "loc": number, "functions": number, "avgFuncSize": number } }, + "perNodeAnnotations": { "nodeId": { "loc": number, "functions": number, "cyclomatic": number, "nesting": number } }, + "hotspots": [ { "nodeId": "string", "metric": "string", "score": number, "reason": "string" } ] +} + +Rules: +- Provide numeric outputs and top-N hotspots. Validate numbers and include timestamps and counts. +- Return only JSON by default. If diagnostic=true in the request, include tool call traces and validation failures. +`, + + } + }, + model: googleAIFlashLite, + memory: pgMemory, + tools: { + resilientFetchTool, + execaTool, + }, + outputProcessors: [new TokenLimiterProcessor(65536)], + defaultOptions: { + autoResumeSuspendedTools: true, + }, +}) + +export default { + graphSupervisorAgent, + technicalAnalysisAgent, + chartJsAgent, + mappingAgent, + fetchAgent, + finnhubAgent, + codeGraphAgent, + codeMetricsAgent, +} diff --git a/src/mastra/index.ts b/src/mastra/index.ts index 94968f5b..889eba6c 100644 --- a/src/mastra/index.ts +++ b/src/mastra/index.ts @@ -55,6 +55,17 @@ import { chartTypeAdvisorAgent, } from './agents/recharts' +import { + graphSupervisorAgent, + technicalAnalysisAgent, + chartJsAgent, + mappingAgent, + fetchAgent, + finnhubAgent, + codeGraphAgent, + codeMetricsAgent, +} from './agents/graphingAgents' + // Image Processing Agents import { imageAgent } from './agents/image' @@ -179,6 +190,16 @@ export const mastra = new Mastra({ chartDataProcessorAgent, chartGeneratorAgent, chartSupervisorAgent, + + // Graphing & Analysis Agents + graphSupervisorAgent, + technicalAnalysisAgent, + chartJsAgent, + mappingAgent, + fetchAgent, + finnhubAgent, + codeGraphAgent, + codeMetricsAgent, // Business Legal Agents legalResearchAgent, businessStrategyAgent, diff --git a/src/mastra/networks/businessIntelligenceNetwork.ts b/src/mastra/networks/businessIntelligenceNetwork.ts index 76380884..2f05e6bc 100644 --- a/src/mastra/networks/businessIntelligenceNetwork.ts +++ b/src/mastra/networks/businessIntelligenceNetwork.ts @@ -10,6 +10,7 @@ import { stockAnalysisAgent } from '../agents/stockAnalysisAgent'; import { googleAI3 } from '../config/google'; import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Business Intelligence Network...') @@ -179,6 +180,7 @@ export const businessIntelligenceNetwork = new Agent({ chartSupervisorAgent, }, options: {}, + tools: { confirmationTool }, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/networks/codingTeamNetwork.ts b/src/mastra/networks/codingTeamNetwork.ts index df2ac14b..751f2f82 100644 --- a/src/mastra/networks/codingTeamNetwork.ts +++ b/src/mastra/networks/codingTeamNetwork.ts @@ -1,4 +1,5 @@ import { Agent } from '@mastra/core/agent'; +import type { Processor, ProcessorMessageResult, ProcessOutputStepArgs} from '@mastra/core/processors'; import { BatchPartsProcessor, TokenLimiterProcessor } from '@mastra/core/processors'; import { codeArchitectAgent, codeReviewerAgent, refactoringAgent, testEngineerAgent } from '../agents/codingAgents'; import { google3 } from '../config/google'; @@ -9,8 +10,55 @@ import { learningExtractionWorkflow } from '../workflows/learning-extraction-wor import { repoIngestionWorkflow } from '../workflows/repo-ingestion-workflow'; import { researchSynthesisWorkflow } from '../workflows/research-synthesis-workflow'; import { specGenerationWorkflow } from '../workflows/spec-generation-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Coding Team Network...') +export class QualityChecker implements Processor { + id = "quality-checker"; + + processOutputStep(args: ProcessOutputStepArgs): ProcessorMessageResult { + const { text, abort, retryCount } = args; + + // If there's no text to evaluate, do nothing. + if (typeof text !== 'string' || text.length === 0) { + return []; + } + + // evaluateQuality may be asynchronous; return the promise directly to match the expected return type + return evaluateQuality(text).then((score) => { + if (score < 0.7 && (retryCount ?? 0) < 3) { + // Request retry with feedback for the LLM + abort("Response quality too low. Please be more specific.", { + retry: true, + metadata: { score }, + }); + } + + return []; + }); + } +} + +async function evaluateQuality(text: string): Promise { + // Basic heuristic to score output quality between 0 and 1. + if (typeof text !== 'string' || text.length === 0) {return 0;} + + // Score increases with length up to a reasonable cap. + const lengthScore = Math.min(1, text.length / 500); + + // Penalize for common low-quality indicators + const lower = text.toLowerCase(); + const negatives = ['todo', 'fixme', 'bug', 'hack', 'wip', 'error', 'undefined']; + const negativeCount = negatives.reduce((acc, kw) => acc + (lower.includes(kw) ? 1 : 0), 0); + const negativeScore = Math.max(0, 1 - (negativeCount * 0.25)); + + // Small boost for presence of tests, examples, or documentation + const positives = ['test', 'unit test', 'integration test', 'example', 'usage', 'documentation', 'docs']; + const positiveBoost = Math.min(0.2, positives.reduce((acc, kw) => acc + (lower.includes(kw) ? 0.05 : 0), 0)); + + const score = Math.max(0, Math.min(1, lengthScore * negativeScore + positiveBoost)); + return score; +} /** * Coding Team Network @@ -95,7 +143,7 @@ Invoke these for structured, multi-phase processes: testEngineerAgent, refactoringAgent, }, - tools: {}, + tools: { confirmationTool }, workflows: { researchSynthesisWorkflow, specGenerationWorkflow, diff --git a/src/mastra/networks/contentCreationNetwork.ts b/src/mastra/networks/contentCreationNetwork.ts index e75bb345..3f41440d 100644 --- a/src/mastra/networks/contentCreationNetwork.ts +++ b/src/mastra/networks/contentCreationNetwork.ts @@ -10,6 +10,7 @@ import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; import { contentReviewWorkflow } from '../workflows/content-review-workflow'; import { contentStudioWorkflow } from '../workflows/content-studio-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Content Creation Network...') @@ -119,6 +120,7 @@ export const contentCreationNetwork = new Agent({ contentReviewWorkflow, }, options: {}, + tools: { confirmationTool }, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/networks/dataPipelineNetwork.ts b/src/mastra/networks/dataPipelineNetwork.ts index cbd0a0ce..d5b11469 100644 --- a/src/mastra/networks/dataPipelineNetwork.ts +++ b/src/mastra/networks/dataPipelineNetwork.ts @@ -8,6 +8,7 @@ import { googleAI } from '../config/google'; import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; import { stockAnalysisWorkflow } from '../workflows/stock-analysis-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Data Pipeline Network...') @@ -89,7 +90,7 @@ For complex requests requiring multiple agents: dataTransformationAgent, reportAgent, }, - tools: {}, + tools: { confirmationTool }, workflows: { stockAnalysisWorkflow, }, diff --git a/src/mastra/networks/devopsNetwork.ts b/src/mastra/networks/devopsNetwork.ts index 111e9c2e..4018c0a4 100644 --- a/src/mastra/networks/devopsNetwork.ts +++ b/src/mastra/networks/devopsNetwork.ts @@ -7,7 +7,7 @@ import { projectManagementAgent } from '../agents/projectManagementAgent'; import { googleAI3 } from '../config/google'; import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; - +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing DevOps Network...') export const devopsNetwork = new Agent({ @@ -162,6 +162,7 @@ export const devopsNetwork = new Agent({ evaluationAgent, }, options: {}, + tools: { confirmationTool }, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/networks/financialIntelligenceNetwork.ts b/src/mastra/networks/financialIntelligenceNetwork.ts index ec29b5e5..fbefb31f 100644 --- a/src/mastra/networks/financialIntelligenceNetwork.ts +++ b/src/mastra/networks/financialIntelligenceNetwork.ts @@ -9,6 +9,7 @@ import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; import { financialReportWorkflow } from '../workflows/financial-report-workflow'; import { stockAnalysisWorkflow } from '../workflows/stock-analysis-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Financial Intelligence Network...') @@ -140,6 +141,7 @@ export const financialIntelligenceNetwork = new Agent({ financialReportWorkflow, stockAnalysisWorkflow, }, + tools: { confirmationTool }, options: {}, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/networks/index.ts b/src/mastra/networks/index.ts index a2e6e18a..9e44a36f 100644 --- a/src/mastra/networks/index.ts +++ b/src/mastra/networks/index.ts @@ -41,7 +41,7 @@ export { businessIntelligenceNetwork } from './businessIntelligenceNetwork' export { securityNetwork } from './securityNetwork' import { BatchPartsProcessor, TokenLimiterProcessor } from '@mastra/core/processors' - +import { confirmationTool } from '../tools/confirmation.tool' export const agentNetwork = new Agent({ id: 'agent-network', name: 'Primary Agent Network', @@ -78,7 +78,7 @@ export const agentNetwork = new Agent({ editorAgent, reportAgent, }, - tools: {}, + tools: { confirmationTool }, scorers: {}, workflows: { weatherWorkflow }, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] diff --git a/src/mastra/networks/learningNetwork.ts b/src/mastra/networks/learningNetwork.ts index f65f8c83..3de5d948 100644 --- a/src/mastra/networks/learningNetwork.ts +++ b/src/mastra/networks/learningNetwork.ts @@ -10,6 +10,7 @@ import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; import { learningExtractionWorkflow } from '../workflows/learning-extraction-workflow'; import { researchSynthesisWorkflow } from '../workflows/research-synthesis-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Learning Network...') @@ -127,6 +128,7 @@ export const learningNetwork = new Agent({ learningExtractionWorkflow, researchSynthesisWorkflow, }, + tools: { confirmationTool }, options: {}, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/networks/marketingAutomationNetwork.ts b/src/mastra/networks/marketingAutomationNetwork.ts index d18b7dc5..aa5b72f9 100644 --- a/src/mastra/networks/marketingAutomationNetwork.ts +++ b/src/mastra/networks/marketingAutomationNetwork.ts @@ -9,6 +9,7 @@ import { translationAgent } from '../agents/translationAgent'; import { googleAI3 } from '../config/google'; import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Marketing Automation Network...') @@ -163,6 +164,7 @@ export const marketingAutomationNetwork = new Agent({ translationAgent, }, options: {}, + tools: { confirmationTool }, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/networks/reportGenerationNetwork.ts b/src/mastra/networks/reportGenerationNetwork.ts index 69c161bc..3fc63c39 100644 --- a/src/mastra/networks/reportGenerationNetwork.ts +++ b/src/mastra/networks/reportGenerationNetwork.ts @@ -12,6 +12,7 @@ import { financialReportWorkflow } from '../workflows/financial-report-workflow' import { learningExtractionWorkflow } from '../workflows/learning-extraction-workflow'; import { researchSynthesisWorkflow } from '../workflows/research-synthesis-workflow'; import { weatherWorkflow } from '../workflows/weather-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Report Generation Network...') @@ -125,7 +126,7 @@ export const reportGenerationNetwork = new Agent({ researchAgent, reportAgent, }, - tools: {}, + tools: { confirmationTool }, workflows: { weatherWorkflow, financialReportWorkflow, diff --git a/src/mastra/networks/researchPipelineNetwork.ts b/src/mastra/networks/researchPipelineNetwork.ts index 3f36bf3f..75567aee 100644 --- a/src/mastra/networks/researchPipelineNetwork.ts +++ b/src/mastra/networks/researchPipelineNetwork.ts @@ -9,6 +9,7 @@ import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; import { contentReviewWorkflow } from '../workflows/content-review-workflow'; import { documentProcessingWorkflow } from '../workflows/document-processing-workflow'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Research Pipeline Network...') @@ -142,6 +143,7 @@ Use for: building research knowledge bases, literature reviews, indexing academi documentProcessingWorkflow, contentReviewWorkflow, }, + tools: { confirmationTool }, options: { }, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] diff --git a/src/mastra/networks/securityNetwork.ts b/src/mastra/networks/securityNetwork.ts index cc4c87df..4c571c1e 100644 --- a/src/mastra/networks/securityNetwork.ts +++ b/src/mastra/networks/securityNetwork.ts @@ -7,6 +7,7 @@ import { researchAgent } from '../agents/researchAgent'; import { googleAI3 } from '../config/google'; import { log } from '../config/logger'; import { pgMemory } from '../config/pg-storage'; +import { confirmationTool } from '../tools/confirmation.tool'; log.info('Initializing Security Network...') @@ -169,7 +170,10 @@ export const securityNetwork = new Agent({ researchAgent, reportAgent, }, + + tools: { confirmationTool }, options: {}, + defaultNetworkOptions: { autoResumeSuspendedTools: true } as unknown as any, outputProcessors: [new TokenLimiterProcessor(128000), new BatchPartsProcessor({ batchSize: 20, maxWaitTime: 100, emitOnNonText: true })] }) diff --git a/src/mastra/tools/arxiv.tool.ts b/src/mastra/tools/arxiv.tool.ts index 1a205e3b..7c3aa2d7 100644 --- a/src/mastra/tools/arxiv.tool.ts +++ b/src/mastra/tools/arxiv.tool.ts @@ -6,7 +6,7 @@ import { log } from '../config/logger' import type { RequestContext } from '@mastra/core/request-context' -export type UserTier = 'free' | 'pro' | 'enterprise' +type UserTier = 'free' | 'pro' | 'enterprise' export interface ArxivRequestContext extends RequestContext { 'user-tier': UserTier diff --git a/src/mastra/tools/browser-tool.ts b/src/mastra/tools/browser-tool.ts index 38d003f9..e042038f 100644 --- a/src/mastra/tools/browser-tool.ts +++ b/src/mastra/tools/browser-tool.ts @@ -7,7 +7,7 @@ import { chromium } from 'playwright-core' import { z } from 'zod' import { log } from '../config/logger' -export type UserTier = 'free' | 'pro' | 'enterprise' +type UserTier = 'free' | 'pro' | 'enterprise' export interface BrowserRequestContext extends RequestContext { userId?: string; diff --git a/src/mastra/tools/calculator.tool.ts b/src/mastra/tools/calculator.tool.ts index 8231e3d9..98d146ae 100644 --- a/src/mastra/tools/calculator.tool.ts +++ b/src/mastra/tools/calculator.tool.ts @@ -5,7 +5,7 @@ import { createTool } from '@mastra/core/tools' import { z } from 'zod' import { log } from '../config/logger' -export type UserTier = 'free' | 'pro' | 'enterprise' +type UserTier = 'free' | 'pro' | 'enterprise' // RequestContext interface for Calculator tool export interface CalculatorToolContext extends RequestContext { diff --git a/src/mastra/tools/calendar-tool.ts b/src/mastra/tools/calendar-tool.ts index 4a98edb6..5e0639b2 100644 --- a/src/mastra/tools/calendar-tool.ts +++ b/src/mastra/tools/calendar-tool.ts @@ -167,7 +167,7 @@ export const listEvents = createTool({ log.info('Calendar list events completed', { toolCallId, toolName, - eventsFound: output.count || 0, + eventsFound: output.count ?? 0, abortSignal: abortSignal?.aborted, hook: 'onOutput', }) diff --git a/src/mastra/tools/chartjs.tool.ts b/src/mastra/tools/chartjs.tool.ts new file mode 100644 index 00000000..120640df --- /dev/null +++ b/src/mastra/tools/chartjs.tool.ts @@ -0,0 +1,467 @@ +import { createTool } from '@mastra/core/tools' +import { SpanType } from '@mastra/core/observability' +import { z } from 'zod' +import { log, logToolExecution } from '../config/logger' +import type { InferUITool } from '@mastra/core/tools' +import { SMA, EMA, RSI, MACD, BollingerBands } from 'technicalindicators' +import { downsampleTool } from './downsample.tool' + +export const chartJsTool = createTool({ + id: 'chartjs-generator', + description: + 'Generates Chart.js configuration with technical indicators for UI visualization', + inputSchema: z.object({ + data: z + .array( + z.object({ + date: z.string(), + close: z.number(), + open: z.number().optional(), + high: z.number().optional(), + low: z.number().optional(), + volume: z.number().optional(), + }) + ) + .describe('Time series data'), + indicators: z + .array( + z.object({ + type: z.enum([ + 'SMA', + 'EMA', + 'RSI', + 'MACD', + 'BollingerBands', + ]), + period: z.number().optional().default(14), + color: z.string().optional(), + stdDev: z.number().optional(), + fastPeriod: z.number().optional(), + slowPeriod: z.number().optional(), + signalPeriod: z.number().optional(), + }) + ) + .optional() + .default([]) + .describe('Indicators to overlay'), + chartType: z + .enum(['line', 'bar', 'candlestick']) + .default('line') + .describe('Base chart type'), + title: z.string().optional(), + }), + outputSchema: z.object({ + config: z.object({ + type: z.string(), + data: z.object({ + labels: z.array(z.string()), + datasets: z.array(z.object({ + label: z.string().optional(), + data: z.array(z.union([z.number(), z.null()])), + borderColor: z.string().optional(), + backgroundColor: z.string().optional(), + type: z.string().optional(), + yAxisID: z.string().optional(), + order: z.number().optional(), + borderWidth: z.number().optional(), + pointRadius: z.number().optional(), + fill: z.union([z.boolean(), z.string()]).optional(), + })), + }), + options: z.record(z.string(), z.unknown()), + }), + }), + execute: async (input, context) => { + const { data, indicators, chartType, title } = input + + await context?.writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Generating ${chartType} chart with ${indicators.length} indicators`, + stage: 'chartjs-generator', + }, + id: 'chartjs-generator', + }) + + // Tracing: create a TOOL_CALL span for this tool execution + const tracingContext = context?.tracingContext + const abortSignal = context?.abortSignal + + if (abortSignal?.aborted === true) { + throw new Error('Tool call cancelled') + } + + const toolSpan = tracingContext?.currentSpan?.createChildSpan({ + type: SpanType.TOOL_CALL, + name: 'chartjs-generator', + input: { dataCount: data.length, indicatorsCount: indicators.length, chartType }, + metadata: { + 'tool.id': 'chartjs-generator', + 'tool.input.dataCount': data.length, + 'tool.input.indicatorsCount': indicators.length, + }, + }) + const startTime = Date.now() + + // Log tool execution start + logToolExecution('chartjs-generator', { dataCount: data.length, indicatorsCount: indicators.length }) + + const labels = data.map((d) => d.date) + const closePrices = data.map((d) => d.close) + + interface ChartJsDataset { + label?: string + data: Array + borderColor?: string + backgroundColor?: string + type?: string + yAxisID?: string + order?: number + borderWidth?: number + pointRadius?: number + fill?: boolean | string + } + + const datasets: ChartJsDataset[] = [] + + // Base price dataset + datasets.push({ + label: 'Price', + data: closePrices, + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + type: 'line', + yAxisID: 'y', + order: 1, + }) + + let config: { + type: string + data: { labels: string[]; datasets: ChartJsDataset[] } + options: Record + } | undefined + + try { + // Calculate indicators + for (const ind of indicators) { + let indicatorData: number[] = [] + let label = '' + const color = ind.color ?? '#ff0000' + + try { + switch (ind.type) { + case 'SMA': + indicatorData = SMA.calculate({ + period: ind.period, + values: closePrices, + }) + label = `SMA (${ind.period})` + // Pad beginning with nulls to match length + indicatorData = Array( + closePrices.length - indicatorData.length + ) + .fill(null) + .concat(indicatorData) + datasets.push({ + label, + data: indicatorData, + borderColor: color, + borderWidth: 1.5, + pointRadius: 0, + fill: false, + yAxisID: 'y', + }) + break + case 'EMA': + indicatorData = EMA.calculate({ + period: ind.period, + values: closePrices, + }) + label = `EMA (${ind.period})` + indicatorData = Array( + closePrices.length - indicatorData.length + ) + .fill(null) + .concat(indicatorData) + datasets.push({ + label, + data: indicatorData, + borderColor: color, + borderWidth: 1.5, + pointRadius: 0, + fill: false, + yAxisID: 'y', + }) + break + case 'RSI': + indicatorData = RSI.calculate({ + period: ind.period, + values: closePrices, + }) + label = `RSI (${ind.period})` + indicatorData = Array( + closePrices.length - indicatorData.length + ) + .fill(null) + .concat(indicatorData) + datasets.push({ + label, + data: indicatorData, + borderColor: color, + borderWidth: 1.5, + pointRadius: 0, + fill: false, + yAxisID: 'y1', // Separate axis + type: 'line', + }) + break + case 'MACD': { + const macdResult = MACD.calculate({ + fastPeriod: ind.fastPeriod ?? 12, + slowPeriod: ind.slowPeriod ?? 26, + signalPeriod: ind.signalPeriod ?? 9, + values: closePrices, + SimpleMAOscillator: false, + SimpleMASignal: false + }) + const macd = macdResult.map((m) => m.MACD) + const signal = macdResult.map((m) => m.signal) + const histogram = macdResult.map((m) => m.histogram) + const padding = Array( + closePrices.length - macd.length + ).fill(null) + + datasets.push({ + label: 'MACD', + data: padding.concat(macd), + borderColor: color, + borderWidth: 1.5, + pointRadius: 0, + fill: false, + yAxisID: 'y1', + }) + datasets.push({ + label: 'MACD Signal', + data: padding.concat(signal), + borderColor: '#00ff00', + borderWidth: 1.5, + pointRadius: 0, + fill: false, + yAxisID: 'y1', + }) + datasets.push({ + label: 'MACD Histogram', + data: padding.concat(histogram), + backgroundColor: color, + type: 'bar', + yAxisID: 'y1', + }) + break; + } + case 'BollingerBands': { + const bb = BollingerBands.calculate({ + period: ind.period, + stdDev: ind.stdDev ?? 2, + values: closePrices, + }) + const upper = bb.map((b) => b.upper) + const middle = bb.map((b) => b.middle) + const lower = bb.map((b) => b.lower) + const padding = Array( + closePrices.length - bb.length + ).fill(null) + + datasets.push({ + label: 'BB Upper', + data: padding.concat(upper), + borderColor: 'rgba(200, 200, 200, 0.5)', + borderWidth: 1, + pointRadius: 0, + fill: false, + yAxisID: 'y', + }) + datasets.push({ + label: 'BB Middle', + data: padding.concat(middle), + borderColor: color, + borderWidth: 1, + pointRadius: 0, + fill: false, + yAxisID: 'y', + }) + datasets.push({ + label: 'BB Lower', + data: padding.concat(lower), + borderColor: 'rgba(200, 200, 200, 0.5)', + borderWidth: 1, + pointRadius: 0, + fill: '-1', // Fill to upper + backgroundColor: 'rgba(200, 200, 200, 0.1)', + yAxisID: 'y', + }) + } + break + } + } catch (e) { + log.warn(`Failed to calculate ${ind.type}`, { error: e }) + } + } + + // Decimation: downsample for visualization when the series is very large + // Uses LTTB (Largest Triangle Three Buckets) to pick representative indices + const DECIMATE_THRESHOLD = 5000 + const DECIMATED_POINTS = 2000 + + // Final labels/datasets (fall back to full if decimation fails or is not needed) + let finalLabels = labels + let finalDatasets = datasets + + if (labels.length > DECIMATE_THRESHOLD) { + try { + // Use price series (first dataset) indices to decimate all datasets consistently + const baseSeries = datasets[0]?.data ?? closePrices + const baseSeriesArr = Array.isArray(baseSeries) ? (baseSeries as Array) : closePrices + const valuesForDownsample = baseSeriesArr.map((v) => (v === null || v === undefined ? NaN : Number(v))) + + // Use downsample tool (server-side) to decimate according to algorithm + const dec = (await downsampleTool.execute({ values: valuesForDownsample, target: DECIMATED_POINTS, algorithm: 'lttb' })) as { indices: number[]; values: number[]; originalLength: number; target: number } + const indices: number[] = dec.indices + + // Build new labels and datasets sampled at the chosen indices + finalLabels = indices.map((i) => labels[i]) + finalDatasets = datasets.map((ds) => { + const dsData = (ds as { data?: unknown }).data + const dsArr = Array.isArray(dsData) ? (dsData as unknown[]) : [] + return { + ...ds, + data: indices.map((idx) => { + const v = dsArr[idx] + return typeof v === 'number' ? v : null + }), + } + }) + } catch (e) { + log.warn('Decimation failed, continuing with full dataset', { error: e }) + } + } + + config = { + type: chartType, // Base type from request + data: { + labels: finalLabels, + datasets: finalDatasets, + }, + options: { + responsive: true, + interaction: { + mode: 'index', + intersect: false, + }, + plugins: { + title: { + display: typeof title === 'string' && title.trim().length > 0, + text: title, + }, + tooltip: { + enabled: true, + }, + }, + scales: { + y: { + type: 'linear', + display: true, + position: 'left', + title: { display: true, text: 'Price' }, + }, + // Only show y1 if RSI is used + y1: indicators.some((i) => i.type === 'RSI') + ? { + type: 'linear', + display: true, + position: 'right', + title: { display: true, text: 'Oscillators' }, + min: 0, + max: 100, + grid: { + drawOnChartArea: false, + }, + } + : undefined, + }, + }, + } + + // Update trace span with duration and end + const duration = Date.now() - startTime + toolSpan?.update({ + output: { config }, + metadata: { + 'tool.output.labels': finalLabels.length, + 'tool.output.datasetCount': finalDatasets.length, + 'tool.duration_ms': duration, + }, + }) + toolSpan?.end() + + // Log tool execution success + logToolExecution('chartjs-generator', { dataCount: data.length, indicatorsCount: indicators.length }, { labels: finalLabels.length, datasetCount: finalDatasets.length, durationMs: duration }) + + } catch (e: unknown) { + // Cast unknown to Error when reporting to span + const err = e instanceof Error ? e : new Error(String(e)) + toolSpan?.error({ error: err, endSpan: true }) + // Log error to central logger + log.error('chartjs-generator failed', { error: err.message }) + // Log tool execution failure + logToolExecution('chartjs-generator', { dataCount: data.length, indicatorsCount: indicators.length }, { success: false, error: err.message }) + throw e + } + + await context?.writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `Chart configuration generated`, + stage: 'chartjs-generator', + }, + id: 'chartjs-generator', + }) + + return { config } + }, + onInputStart: ({ toolCallId, messages }: { toolCallId: string; messages: unknown[] }) => { + log.info('Chart.js generator tool input streaming started', { + toolCallId, + messages: Array.isArray(messages) ? messages.length : 0, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages }: { inputTextDelta: string; toolCallId: string; messages: unknown[] }) => { + log.info('Chart.js generator tool received input chunk', { + toolCallId, + inputTextDelta, + messages: Array.isArray(messages) ? messages.length : 0, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages }: { input: unknown; toolCallId: string; messages: unknown[] }) => { + log.info('Chart.js generator tool received input', { + toolCallId, + inputData: input, + messages: Array.isArray(messages) ? messages.length : 0, + hook: 'onInputAvailable', + }) + }, + onOutput: ({ output, toolCallId, toolName }: { output: unknown; toolCallId: string; toolName: string }) => { + log.info('Chart.js generator tool completed', { + toolCallId, + toolName, + output, + hook: 'onOutput', + }) + }, +}) + +export type ChartJsUITool = InferUITool + diff --git a/src/mastra/tools/confirmation.tool.ts b/src/mastra/tools/confirmation.tool.ts new file mode 100644 index 00000000..0bb6d9b6 --- /dev/null +++ b/src/mastra/tools/confirmation.tool.ts @@ -0,0 +1,36 @@ +import { createTool } from "@mastra/core/tools"; +import { z } from "zod"; + +const confirmationTool = createTool({ + id: "confirmation-tool", + description: "Requests user confirmation before proceeding", + inputSchema: z.object({ + action: z.string() + }), + outputSchema: z.object({ + confirmed: z.boolean(), + action: z.string() + }), + suspendSchema: z.object({ + message: z.string(), + action: z.string() + }), + resumeSchema: z.object({ + confirmed: z.boolean() + }), + execute: async ({ action }, context) => { + const { resumeData, suspend } = context?.agent ?? {}; + + // Explicitly check for true to avoid nullable boolean in condition + if (resumeData?.confirmed !== true) { + return suspend?.({ + message: `Please confirm: ${action}`, + action + }); + } + + return { confirmed: true, action }; + } +}); + +export { confirmationTool }; diff --git a/src/mastra/tools/cytoscape.tool.ts b/src/mastra/tools/cytoscape.tool.ts new file mode 100644 index 00000000..928d16f6 --- /dev/null +++ b/src/mastra/tools/cytoscape.tool.ts @@ -0,0 +1,237 @@ +import type { RequestContext } from '@mastra/core/request-context' +import type { InferUITool } from '@mastra/core/tools'; +import { createTool } from '@mastra/core/tools' +import { SpanType } from '@mastra/core/observability' +import { z } from 'zod' +import { log, logToolExecution } from '../config/logger' + +export interface CytoscapeContext extends RequestContext { + userId?: string + workspaceId?: string +} + +export const cytoscapeTool = createTool({ + id: 'cytoscape-generator', + description: 'Generates Cytoscape.js compatible graph data structures', + inputSchema: z.object({ + nodes: z + .array( + z.object({ + id: z.string(), + label: z.string().optional(), + type: z.string().optional(), + data: z.record(z.string(), z.unknown()).optional(), + position: z + .object({ x: z.number(), y: z.number() }) + .optional(), + }) + ) + .describe('Graph nodes'), + edges: z + .array( + z.object({ + id: z.string().optional(), + source: z.string(), + target: z.string(), + label: z.string().optional(), + type: z.string().optional(), + data: z.record(z.string(), z.unknown()).optional(), + }) + ) + .describe('Graph edges'), + layout: z + .enum(['grid', 'circle', 'concentric', 'breadthfirst', 'cose']) + .default('cose') + .optional(), + }), + outputSchema: z.object({ + elements: z.array( + z.object({ + group: z.enum(['nodes', 'edges']), + data: z.record(z.string(), z.unknown()), + position: z.object({ x: z.number(), y: z.number() }).optional(), + classes: z.array(z.string()).optional(), + }) + ), + layout: z.object({ + name: z.string(), + animate: z.boolean().optional(), + }), + }), + execute: async (input, context) => { + const { nodes, edges, layout } = input + const writer = context?.writer + const abortSignal = context?.abortSignal + const tracingContext = context?.tracingContext + const requestCtx = context?.requestContext as + | CytoscapeContext + | undefined + + const userId = requestCtx?.userId + const workspaceId = requestCtx?.workspaceId + + if (abortSignal?.aborted ?? false) { + throw new Error('Tool call cancelled') + } + + const toolSpan = tracingContext?.currentSpan?.createChildSpan({ + type: SpanType.TOOL_CALL, + name: 'cytoscape-generator', + input, + metadata: { + 'tool.id': 'cytoscape-generator', + 'tool.input.nodesCount': nodes.length, + 'tool.input.edgesCount': edges.length, + 'user.id': userId, + 'workspace.id': workspaceId, + }, + }) + const startTime = Date.now() + logToolExecution('cytoscape-generator', { nodes: nodes.length, edges: edges.length }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Generating graph with ${nodes.length} nodes and ${edges.length} edges`, + stage: 'cytoscape-generator', + }, + id: 'cytoscape-generator', + }) + + // Validate and guard against excessively large graphs + const MAX_ELEMENTS = 50_000 + if (nodes.length + edges.length > MAX_ELEMENTS) { + toolSpan?.error({ + error: new Error( + `Graph too large: ${nodes.length + edges.length} elements exceeds max ${MAX_ELEMENTS}` + ), + endSpan: true, + }) + throw new Error( + `Graph too large: ${nodes.length + edges.length} elements exceeds max ${MAX_ELEMENTS}` + ) + } + + // Ensure unique node ids + const nodeIds = new Set() + for (const n of nodes) { + if (nodeIds.has(n.id)) { + toolSpan?.error({ + error: new Error(`Duplicate node id detected: ${n.id}`), + endSpan: true, + }) + throw new Error(`Duplicate node id detected: ${n.id}`) + } + nodeIds.add(n.id) + } + + const elements = [ + ...nodes.map((n) => { + const labelText = + typeof n.label === 'string' && n.label.trim() !== '' + ? n.label + : n.id + const nodeClasses = + typeof n.type === 'string' && n.type.trim() !== '' + ? [n.type] + : undefined + return { + group: 'nodes' as const, + data: { + id: n.id, + label: labelText, + type: n.type, + ...n.data, + }, + position: n.position, + classes: nodeClasses, + } + }), + ...edges.map((e, idx) => { + const edgeClasses = + typeof e.type === 'string' && e.type.trim() !== '' + ? [e.type] + : undefined + const edgeLabel = + typeof e.label === 'string' && e.label.trim() !== '' + ? e.label + : undefined + return { + group: 'edges' as const, + data: { + id: e.id ?? `${e.source}-${e.target}-${idx}`, + source: e.source, + target: e.target, + label: edgeLabel, + type: e.type ?? undefined, + ...e.data, + }, + classes: edgeClasses, + } + }), + ] + + const duration = Date.now() - startTime + toolSpan?.update({ + output: { elements, layout }, + metadata: { + 'tool.output.elementsCount': elements.length, + 'tool.output.success': true, + 'tool.duration_ms': duration, + }, + }) + toolSpan?.end() + + // Log success + logToolExecution('cytoscape-generator', { nodes: nodes.length, edges: edges.length }, { elements: elements.length, durationMs: duration }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `Graph data generation complete`, + stage: 'cytoscape-generator', + }, + id: 'cytoscape-generator', + }) + + return { + elements, + layout: { + name: layout ?? 'cose', + animate: true, + }, + } + }, + onInputStart: ({ toolCallId }) => { + log.info('Cytoscape generator tool input streaming started', { + toolCallId, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId }) => { + log.info('Cytoscape generator tool received input chunk', { + toolCallId, + inputTextDelta, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId }) => { + log.info('Cytoscape generator tool received input', { + toolCallId, + inputData: input, + hook: 'onInputAvailable', + }) + }, + onOutput: ({ toolCallId, toolName }) => { + log.info('Cytoscape generator tool completed', { + toolCallId, + toolName, + hook: 'onOutput', + }) + }, +}) + +export type CytoscapeUITool = InferUITool + diff --git a/src/mastra/tools/downsample.tool.ts b/src/mastra/tools/downsample.tool.ts new file mode 100644 index 00000000..9bd72e0b --- /dev/null +++ b/src/mastra/tools/downsample.tool.ts @@ -0,0 +1,90 @@ +import { createTool } from '@mastra/core/tools' +import { z } from 'zod' +import { log } from '../config/logger' +import lttb from 'downsample-lttb' + +export const downsampleTool = createTool({ + id: 'downsample', + description: 'Downsample numeric series using LTTB (Largest Triangle Three Buckets)', + inputSchema: z.object({ + values: z.array(z.number()).describe('Numeric series to downsample'), + target: z.number().min(2).describe('Target number of samples'), + algorithm: z.enum(['lttb', 'min-max', 'm4']).optional().default('lttb'), + }), + outputSchema: z.object({ + indices: z.array(z.number()), + values: z.array(z.number()), + originalLength: z.number(), + target: z.number(), + }), + execute: async (input) => { + const { values, target, algorithm } = input + const originalLength = values.length + + if (target >= originalLength || originalLength <= 2) { + // Nothing to do: return full series indices + return { + indices: values.map((_, i) => i), + values: values.slice(), + originalLength, + target, + } + } + + try { + if (algorithm === 'lttb') { + // Build points as [index, value] + const points: Array<[number, number]> = values.map((v, i) => [i, Number.isFinite(v) ? v : NaN]) + const decimated: Array<[number, number]> = lttb(points, target) + const indices = decimated.map((p) => Math.max(0, Math.min(originalLength - 1, Math.round(p[0])))) + const sampledValues = indices.map((i) => values[i]) + return { indices, values: sampledValues, originalLength, target } + } + + // Min-max and M4 algorithms + const bucketSize = Math.max(1, Math.floor(originalLength / target)) + const indices: number[] = [] + + for (let start = 0; start < originalLength; start += bucketSize) { + const end = Math.min(start + bucketSize, originalLength) + if (end - start <= 0) { continue } + + const firstIdx = start + const lastIdx = end - 1 + + // find min and max within segment + let minIdx = start + let maxIdx = start + for (let j = start; j < end; j++) { + if (values[j] < values[minIdx]) { minIdx = j } + if (values[j] > values[maxIdx]) { maxIdx = j } + } + + if (algorithm === 'min-max') { + if (minIdx === maxIdx) { + indices.push(minIdx) + } else { + if (minIdx < maxIdx) {indices.push(minIdx, maxIdx)} + else {indices.push(maxIdx, minIdx)} + } + } else if (algorithm === 'm4') { + indices.push(firstIdx, minIdx, maxIdx, lastIdx) + } + } + + // ensure unique and sorted + const uniqueIndices = Array.from(new Set(indices)).sort((a, b) => a - b) + const sampledValues = uniqueIndices.map((i) => values[i]) + return { indices: uniqueIndices, values: sampledValues, originalLength, target } + } catch (err) { + log.error('downsample tool failed', { error: err instanceof Error ? err.message : String(err) }) + // Fallback: uniform sampling + const step = Math.max(1, Math.floor(originalLength / target)) + const indices = Array.from({ length: Math.min(target, originalLength) }, (_, i) => Math.min(originalLength - 1, i * step)) + const sampledValues = indices.map((i) => values[i]) + return { indices, values: sampledValues, originalLength, target } + } + }, +}) + +export type DownsampleUITool = typeof downsampleTool diff --git a/src/mastra/tools/image-tool.ts b/src/mastra/tools/image-tool.ts index 2c240ffc..629ea11a 100644 --- a/src/mastra/tools/image-tool.ts +++ b/src/mastra/tools/image-tool.ts @@ -11,7 +11,7 @@ import { log } from '../config/logger' import * as fs from 'node:fs/promises' import path from 'node:path' import sharp from 'sharp' -import type { PSM } from 'tesseract.js'; +import type { PSM } from 'tesseract.js' import { createWorker } from 'tesseract.js' export interface ImageToolContext extends RequestContext { @@ -349,12 +349,22 @@ export const imageProcessorTool = createTool({ pipeline = pipeline.rotate(ops.rotate) } - pipeline = pipeline.flip() + if (ops.flip) { + pipeline = pipeline.flip() + } - if (ops.flop) {pipeline = pipeline.flop()} - if (ops.grayscale) {pipeline = pipeline.grayscale()} - if (ops.blur !== null) {pipeline = pipeline.blur(ops.blur)} - if (ops.sharpen) {pipeline = pipeline.sharpen()} + if (ops.flop) { + pipeline = pipeline.flop() + } + if (ops.grayscale) { + pipeline = pipeline.grayscale() + } + if (ops.blur !== null) { + pipeline = pipeline.blur(ops.blur) + } + if (ops.sharpen) { + pipeline = pipeline.sharpen() + } if (ops.format) { pipeline = pipeline.toFormat(ops.format, { diff --git a/src/mastra/tools/leaflet.tool.ts b/src/mastra/tools/leaflet.tool.ts new file mode 100644 index 00000000..6c455c90 --- /dev/null +++ b/src/mastra/tools/leaflet.tool.ts @@ -0,0 +1,280 @@ +import { createTool } from '@mastra/core/tools' +import { SpanType } from '@mastra/core/observability' +import type { RequestContext } from '@mastra/core/request-context' +import { z } from 'zod' +import { log, logToolExecution } from '../config/logger' +import type { InferUITool } from '@mastra/core/tools' + +export interface LeafletToolContext extends RequestContext { + userId?: string + workspaceId?: string +} + +export const leafletTool = createTool({ + id: 'leaflet-generator', + description: 'Generates Leaflet/GeoJSON compatible map data structures', + inputSchema: z.object({ + points: z + .array( + z.object({ + lat: z.number(), + lng: z.number(), + title: z.string().optional(), + description: z.string().optional(), + category: z.string().optional(), + properties: z.record(z.string(), z.unknown()).optional(), + }) + ) + .describe('Map points/markers'), + bounds: z + .object({ + southWest: z.tuple([z.number(), z.number()]), + northEast: z.tuple([z.number(), z.number()]), + }) + .optional() + .describe('Optional viewport bounds for server-side clipping'), + cluster: z.boolean().optional().default(true), + center: z + .object({ + lat: z.number(), + lng: z.number(), + }) + .optional() + .describe('Map center point (auto-calculated if omitted)'), + zoom: z.number().optional().default(13), + }), + outputSchema: z.object({ + geoJSON: z.unknown(), + center: z.object({ + lat: z.number(), + lng: z.number(), + }), + zoom: z.number(), + markers: z.array( + z.object({ + position: z.array(z.number()), + popup: z.string().optional(), + category: z.string().optional(), + }) + ), + }), + execute: async (input, context) => { + const writer = context?.writer + const abortSignal = context?.abortSignal + const tracingContext = context?.tracingContext + const requestCtx = context?.requestContext as + | LeafletToolContext + | undefined + + const userId = requestCtx?.userId + const workspaceId = requestCtx?.workspaceId + + if (abortSignal?.aborted ?? false) { + throw new Error('Tool call cancelled') + } + + const toolSpan = tracingContext?.currentSpan?.createChildSpan({ + type: SpanType.TOOL_CALL, + name: 'leaflet-generator', + input, + metadata: { + 'tool.id': 'leaflet-generator', + 'tool.input.pointsCount': input.points?.length ?? 0, + 'tool.input.cluster': input.cluster ?? true, + 'user.id': userId, + 'workspace.id': workspaceId, + }, + }) + const startTime = Date.now() + logToolExecution('leaflet-generator', { pointsCount: input.points?.length ?? 0, cluster: input.cluster ?? true }) + + const { points: rawPoints, bounds, center, zoom } = input + + const MAX_MARKERS = 5000 + + // basic validation + for (const p of rawPoints) { + if (p.lat < -90 || p.lat > 90 || p.lng < -180 || p.lng > 180) { + throw new Error( + `Invalid coordinates for point: lat=${p.lat}, lng=${p.lng}` + ) + } + } + + // Clip to bounds if provided + let points = rawPoints + if (bounds) { + const [swLat, swLng] = bounds.southWest + const [neLat, neLng] = bounds.northEast + points = points.filter( + (p) => + p.lat >= swLat && + p.lat <= neLat && + p.lng >= swLng && + p.lng <= neLng + ) + } + + // If still too many points, sample uniformly + if (points.length > MAX_MARKERS) { + const step = Math.ceil(points.length / MAX_MARKERS) + points = points + .filter((_, idx) => idx % step === 0) + .slice(0, MAX_MARKERS) + } + + try { + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Generating map data for ${points.length} points`, + stage: 'leaflet-generator', + }, + id: 'leaflet-generator', + }) + + // Generate GeoJSON FeatureCollection + const geoJSON = { + type: 'FeatureCollection', + features: points.map((p) => ({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [p.lng, p.lat], // GeoJSON is [lng, lat] + }, + properties: { + title: p.title, + description: p.description, + category: p.category, + ...p.properties, + }, + })), + } + + // Calculate center if not provided + let mapCenter = center + if (!mapCenter && points.length > 0) { + const lats = points.map((p) => p.lat) + const lngs = points.map((p) => p.lng) + mapCenter = { + lat: (Math.min(...lats) + Math.max(...lats)) / 2, + lng: (Math.min(...lngs) + Math.max(...lngs)) / 2, + } + } + + mapCenter ??= { lat: 0, lng: 0 } + + const markers = points.map((p) => { + let popup: string | undefined + const title = (p.title ?? '').trim() + if (title.length > 0) { + const descText = (p.description ?? '').trim() + const desc = descText.length > 0 ? `
${descText}` : '' + popup = `${title}` + desc + } + + return { + position: [p.lat, p.lng], // Leaflet uses [lat, lng] + popup, + category: p.category, + } + }) + + const result = { + geoJSON, + center: mapCenter, + zoom: zoom || 13, + markers, + } + + const duration = Date.now() - startTime + toolSpan?.update({ + output: result, + metadata: { + 'tool.output.success': true, + 'tool.output.pointsCount': points.length, + 'tool.output.markersCount': markers.length, + 'tool.duration_ms': duration, + }, + }) + toolSpan?.end() + + logToolExecution('leaflet-generator', { pointsCount: input.points?.length ?? 0 }, { markers: markers.length, durationMs: duration }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `Map data generation complete`, + stage: 'leaflet-generator', + }, + id: 'leaflet-generator', + }) + + return result + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)) + toolSpan?.error({ error: err, endSpan: true }) + log.error('leaflet-generator failed', { error: err.message }) + logToolExecution('leaflet-generator', { pointsCount: input.points?.length ?? 0 }, { success: false, error: err.message }) + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `Map data generation error: ${error instanceof Error ? error.message : String(error)}`, + stage: 'leaflet-generator', + }, + id: 'leaflet-generator', + }) + + throw error + } + }, + onInputStart: ({ toolCallId, messages, abortSignal }) => { + log.info('Leaflet generator tool input streaming started', { + toolCallId, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + log.info('Leaflet generator tool received input chunk', { + toolCallId, + inputTextDelta, + messageCount: messages?.length ?? 0, + abortSignal: abortSignal?.aborted, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Leaflet generator tool received input', { + toolCallId, + messageCount: messages?.length ?? 0, + inputData: { + pointsCount: input.points?.length ?? 0, + cluster: input.cluster, + zoom: input.zoom, + }, + abortSignal: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + log.info('Leaflet generator tool completed', { + toolCallId, + toolName, + outputData: { + markersCount: output.markers?.length ?? 0, + zoom: output.zoom, + }, + abortSignal: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type LeafletUITool = InferUITool + diff --git a/src/mastra/tools/resilient-fetch.tool.ts b/src/mastra/tools/resilient-fetch.tool.ts new file mode 100644 index 00000000..c0fa3243 --- /dev/null +++ b/src/mastra/tools/resilient-fetch.tool.ts @@ -0,0 +1,140 @@ +import { createTool } from '@mastra/core/tools' +import { SpanType } from '@mastra/core/observability' +import { z } from 'zod' +import { log } from '../config/logger' +import axios from 'axios' +import axiosRetry from 'axios-retry' +import Bottleneck from 'bottleneck' + +// Create a shared limiter instance (could be configurable per tool instance if needed) +const limiter = new Bottleneck({ + minTime: 200, // 5 requests per second + maxConcurrent: 5, +}) + +// Configure axios with retry +const client = axios.create() +axiosRetry(client, { + retries: 3, + retryDelay: axiosRetry.exponentialDelay, + retryCondition: (error) => { + return ( + axiosRetry.isNetworkOrIdempotentRequestError(error) || + error.response?.status === 429 + ) + }, +}) + +export const resilientFetchTool = createTool({ + id: 'resilient-fetch', + description: + 'Fetches data from URLs with automatic retries and rate limiting', + inputSchema: z.object({ + url: z.string().url(), + method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).default('GET'), + data: z.record(z.string(), z.unknown()).optional(), + headers: z.record(z.string(), z.string()).optional(), + priority: z + .number() + .min(0) + .max(9) + .default(5) + .describe('Priority (0-9, higher is more important)'), + }), + outputSchema: z.object({ + status: z.number(), + data: z.unknown(), + headers: z.record(z.string(), z.unknown()), + }), + execute: async (input, context) => { + const { url, method, data, headers, priority } = input + + await context?.writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `Scheduling ${method} request to ${url}`, + stage: 'resilient-fetch', + }, + id: 'resilient-fetch', + }) + + try { + // Schedule the request through bottleneck + const response = await limiter.schedule({ priority }, () => + client.request({ + url, + method, + data, + headers, + }) + ) + + await context?.writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `Request complete: ${response.status} ${response.statusText}`, + stage: 'resilient-fetch', + }, + id: 'resilient-fetch', + }) + + return { + status: response.status, + data: response.data, + headers: response.headers as Record, + } + } catch (error: any) { + const errorMessage = error.response + ? `${error.response.status} - ${JSON.stringify(error.response.data)}` + : error.message + + log.error('Resilient fetch failed', { + url, + method, + error: errorMessage, + }) + + await context?.writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `Request failed: ${errorMessage}`, + stage: 'resilient-fetch', + }, + id: 'resilient-fetch', + }) + + throw new Error(`Request failed: ${errorMessage}`) + } + }, + onInputStart: ({ toolCallId, messages, abortSignal }) => { + log.info('Resilient fetch tool input streaming started', { + toolCallId, + hook: 'onInputStart', + }) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + log.info('Resilient fetch tool received input chunk', { + toolCallId, + inputTextDelta, + hook: 'onInputDelta', + }) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Resilient fetch tool received input', { + toolCallId, + inputData: { url: input.url, method: input.method }, + hook: 'onInputAvailable', + }) + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + log.info('Resilient fetch tool completed', { + toolCallId, + toolName, + outputData: { status: output.status }, + hook: 'onOutput', + }) + }, +}) diff --git a/src/mastra/tools/serpapi-news-trends.tool.ts b/src/mastra/tools/serpapi-news-trends.tool.ts index 8a5f04f4..c337e4d4 100644 --- a/src/mastra/tools/serpapi-news-trends.tool.ts +++ b/src/mastra/tools/serpapi-news-trends.tool.ts @@ -14,7 +14,7 @@ import { log } from '../config/logger' import { validateSerpApiKey } from './serpapi-config' import type { RequestContext } from '@mastra/core/request-context' -export interface SerpApiContext extends RequestContext { +export interface SerpApiNewsContext extends RequestContext { userId?: string } @@ -125,7 +125,7 @@ export const googleNewsTool = createTool({ }, execute: async (input, context) => { const writer = context?.writer - const requestContext = context?.requestContext as SerpApiContext | undefined + const requestContext = context?.requestContext as SerpApiNewsContext | undefined const tracingContext = context?.tracingContext await writer?.custom({ type: 'data-tool-progress', diff --git a/src/mastra/tools/serpapi-search.tool.ts b/src/mastra/tools/serpapi-search.tool.ts index d4da5517..b971b700 100644 --- a/src/mastra/tools/serpapi-search.tool.ts +++ b/src/mastra/tools/serpapi-search.tool.ts @@ -14,7 +14,7 @@ import { z } from 'zod' import { log } from '../config/logger' import { validateSerpApiKey } from './serpapi-config' -export interface SerpApiContext extends RequestContext { +export interface SerpApiSearchContext extends RequestContext { userId?: string } @@ -102,7 +102,7 @@ export const googleSearchTool = createTool({ validateSerpApiKey() const writer = context?.writer const abortSignal = context?.abortSignal - const requestContext = context?.requestContext as SerpApiContext | undefined + const requestContext = context?.requestContext as SerpApiSearchContext | undefined const tracingContext = context?.tracingContext // Check if operation was already cancelled diff --git a/src/mastra/tools/spatial-index.tool.ts b/src/mastra/tools/spatial-index.tool.ts new file mode 100644 index 00000000..1aff0dca --- /dev/null +++ b/src/mastra/tools/spatial-index.tool.ts @@ -0,0 +1,108 @@ +import { createTool } from '@mastra/core/tools' +import { z } from 'zod' +import RBush from 'rbush' +import { log } from '../config/logger' + +// Each indexed item will be stored with a bbox: {minX,minY,maxX,maxY, id, data} +interface IndexedItem { + minX: number + minY: number + maxX: number + maxY: number + id: string + data?: Record +} + +export const spatialIndexTool = createTool({ + id: 'spatial-index', + description: 'Create and query RBush spatial index. Returns serialized index JSON for persistence.', + inputSchema: z.object({ + action: z.enum(['build', 'search', 'fromJSON', 'toJSON']).describe('Action to perform'), + points: z + .array( + z.object({ + id: z.string(), + lat: z.number(), + lng: z.number(), + data: z.record(z.string(), z.unknown()).optional(), + }) + ) + .optional(), + bounds: z + .object({ + southWest: z.tuple([z.number(), z.number()]), + northEast: z.tuple([z.number(), z.number()]), + }) + .optional(), + treeJSON: z.unknown().optional(), + }), + // Define a structured item schema for search results + outputSchema: z.object({ + treeJSON: z.unknown().optional(), + results: z + .array( + z.object({ + minX: z.number(), + minY: z.number(), + maxX: z.number(), + maxY: z.number(), + id: z.string(), + data: z.record(z.string(), z.unknown()).optional(), + }) + ) + .optional(), + message: z.string().optional(), + }), + execute: async (input) => { + const { action, points, bounds, treeJSON } = input + + if (action === 'build') { + if (!points || points.length === 0) { + throw new Error('No points provided to build the spatial index') + } + + const tree = new RBush() + const items = points.map((p) => ({ + minX: p.lng, + minY: p.lat, + maxX: p.lng, + maxY: p.lat, + id: p.id, + data: p.data, + })) + tree.load(items) + const json = tree.toJSON() + return { treeJSON: json } + } + + if (action === 'fromJSON') { + if (treeJSON === undefined) { throw new Error('treeJSON is required for fromJSON action') } + const tree = new RBush() + tree.fromJSON(treeJSON) + return { message: 'Index loaded' } + } + + if (action === 'search') { + if (treeJSON === undefined) { throw new Error('treeJSON is required for search action') } + if (bounds === undefined) { throw new Error('bounds is required for search action') } + const tree = new RBush() + tree.fromJSON(treeJSON as unknown) + const [swLat, swLng] = bounds.southWest + const [neLat, neLng] = bounds.northEast + const rawResults = tree.search({ minX: swLng, minY: swLat, maxX: neLng, maxY: neLat }) + const results = rawResults.map((r: IndexedItem) => ({ + minX: Number(r.minX), + minY: Number(r.minY), + maxX: Number(r.maxX), + maxY: Number(r.maxY), + id: String(r.id), + data: r.data, + })) + return { results } + } + + return { message: 'No-op' } + }, +}) + +export type SpatialIndexUITool = typeof spatialIndexTool diff --git a/src/mastra/tools/technical-analysis.tool.ts b/src/mastra/tools/technical-analysis.tool.ts index 083d94d5..83ca8edf 100644 --- a/src/mastra/tools/technical-analysis.tool.ts +++ b/src/mastra/tools/technical-analysis.tool.ts @@ -1,7 +1,7 @@ import { createTool } from '@mastra/core/tools' import { SpanType } from '@mastra/core/observability' import { z } from 'zod' -import { log } from '../config/logger' +import { log, logToolExecution } from '../config/logger' import type { RequestContext } from '@mastra/core/request-context' import type { InferUITool } from '@mastra/core/tools' import * as ss from 'simple-statistics' @@ -197,6 +197,8 @@ export const ichimokuCloudTool = createTool({ 'workspace.id': workspaceId, }, }) + const startTime = Date.now() + logToolExecution('ichimoku-cloud', { dataLength: inputData.close?.length ?? 0 }) try { const results = IchimokuCloud.calculate({ @@ -213,12 +215,15 @@ export const ichimokuCloudTool = createTool({ results: results as unknown as IchimokuCloudOutput[], } + const duration = Date.now() - startTime toolSpan?.update({ output: finalResult, - metadata: { 'tool.output.success': true }, + metadata: { 'tool.output.success': true, 'tool.duration_ms': duration }, }) toolSpan?.end() + logToolExecution('ichimoku-cloud', { dataLength: inputData.close?.length ?? 0 }, { success: true, durationMs: duration }) + await writer?.custom({ type: 'data-tool-progress', data: { @@ -231,24 +236,22 @@ export const ichimokuCloudTool = createTool({ return finalResult } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error) - toolSpan?.error({ - error: error instanceof Error ? error : new Error(errorMessage), - endSpan: true, - }) + const err = error instanceof Error ? error : new Error(String(error)) + toolSpan?.error({ error: err, endSpan: true }) + log.error('ichimoku-cloud failed', { error: err.message }) + logToolExecution('ichimoku-cloud', { dataLength: inputData.close?.length ?? 0 }, { success: false, error: err.message }) await writer?.custom({ type: 'data-tool-progress', data: { status: 'done', - message: `❌ Ichimoku calculation failed: ${errorMessage}`, + message: `❌ Ichimoku calculation failed: ${err.message}`, stage: 'ichimoku-cloud', }, id: 'ichimoku-cloud', }) - return { success: false, message: errorMessage } + return { success: false, message: err.message } } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { @@ -327,6 +330,8 @@ export const fibonacciTool = createTool({ 'workspace.id': workspaceId, }, }) + const startTime = Date.now() + logToolExecution('fibonacci-levels', { high: inputData.high, low: inputData.low }) try { const diff = inputData.high - inputData.low @@ -343,11 +348,13 @@ export const fibonacciTool = createTool({ const finalResult = { success: true, levels } + const duration = Date.now() - startTime toolSpan?.update({ output: finalResult, - metadata: { 'tool.output.success': true }, + metadata: { 'tool.output.success': true, 'tool.duration_ms': duration }, }) toolSpan?.end() + logToolExecution('fibonacci-levels', { high: inputData.high, low: inputData.low }, { success: true, durationMs: duration }) await writer?.custom({ type: 'data-tool-progress', @@ -1460,7 +1467,7 @@ export const heikinAshiTool = createTool({ }), outputSchema: z.object({ success: z.boolean(), - candles: z.any().optional(), + candles: z.array(z.object({ open: z.number(), high: z.number(), low: z.number(), close: z.number() })).optional(), message: z.string().optional(), }), execute: async (inputData, context) => { @@ -1496,7 +1503,16 @@ export const heikinAshiTool = createTool({ }) try { - const candles = heikinashi(inputData) + const candlesRaw = heikinashi(inputData) + const candles = Array.isArray(candlesRaw) + ? candlesRaw.map((c: any) => ({ + open: Number(c.open), + high: Number(c.high), + low: Number(c.low), + close: Number(c.close), + })) + : [] + const finalResult = { success: true, candles } toolSpan?.update({ @@ -1847,31 +1863,28 @@ export const candlestickPatternTool = createTool({ try { const patterns: Record = {} - const check = (pattern: (input: any) => boolean) => - pattern(inputData) - - patterns.abandonedBaby = check(abandonedbaby) - patterns.doji = check(doji) - patterns.darkCloudCover = check(darkcloudcover) - patterns.downsideTasukiGap = check(downsidetasukigap) - patterns.dragonflyDoji = check(dragonflydoji) - patterns.eveningDojiStar = check(eveningdojistar) - patterns.eveningStar = check(eveningstar) - patterns.gravestoneDoji = check(gravestonedoji) - patterns.hangingMan = check(hangingman) - patterns.morningDojiStar = check(morningdojistar) - patterns.morningStar = check(morningstar) - patterns.piercingLine = check(piercingline) - patterns.bullishHarami = check(bullishharami) - patterns.bearishHarami = check(bearishharami) - patterns.bullishHaramiCross = check(bullishharamicross) - patterns.bearishHaramiCross = check(bearishharamicross) - patterns.bullishMarubozu = check(bullishmarubozu) - patterns.bearishMarubozu = check(bearishmarubozu) - patterns.bullishSpinningTop = check(bullishspinningtop) - patterns.bearishSpinningTop = check(bearishspinningtop) - patterns.threeBlackCrows = check(threeblackcrows) - patterns.threeWhiteSoldiers = check(threewhitesoldiers) + patterns.abandonedBaby = Boolean(abandonedbaby(inputData)) + patterns.doji = Boolean(doji(inputData)) + patterns.darkCloudCover = Boolean(darkcloudcover(inputData)) + patterns.downsideTasukiGap = Boolean(downsidetasukigap(inputData)) + patterns.dragonflyDoji = Boolean(dragonflydoji(inputData)) + patterns.eveningDojiStar = Boolean(eveningdojistar(inputData)) + patterns.eveningStar = Boolean(eveningstar(inputData)) + patterns.gravestoneDoji = Boolean(gravestonedoji(inputData)) + patterns.hangingMan = Boolean(hangingman(inputData)) + patterns.morningDojiStar = Boolean(morningdojistar(inputData)) + patterns.morningStar = Boolean(morningstar(inputData)) + patterns.piercingLine = Boolean(piercingline(inputData)) + patterns.bullishHarami = Boolean(bullishharami(inputData)) + patterns.bearishHarami = Boolean(bearishharami(inputData)) + patterns.bullishHaramiCross = Boolean(bullishharamicross(inputData)) + patterns.bearishHaramiCross = Boolean(bearishharamicross(inputData)) + patterns.bullishMarubozu = Boolean(bullishmarubozu(inputData)) + patterns.bearishMarubozu = Boolean(bearishmarubozu(inputData)) + patterns.bullishSpinningTop = Boolean(bullishspinningtop(inputData)) + patterns.bearishSpinningTop = Boolean(bearishspinningtop(inputData)) + patterns.threeBlackCrows = Boolean(threeblackcrows(inputData)) + patterns.threeWhiteSoldiers = Boolean(threewhitesoldiers(inputData)) const finalResult = { success: true, patterns } @@ -2023,7 +2036,7 @@ export const technicalAnalysisTool = createTool({ }), outputSchema: z.object({ success: z.boolean(), - results: z.record(z.string(), z.any()), + results: z.record(z.string(), z.unknown()), stats: z .object({ mean: z.number().optional(), diff --git a/src/mastra/tools/tests/chartjs.integration.test.ts b/src/mastra/tools/tests/chartjs.integration.test.ts new file mode 100644 index 00000000..94ecf04f --- /dev/null +++ b/src/mastra/tools/tests/chartjs.integration.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest' +import { chartJsTool } from '../chartjs.tool' + +describe('chartJsTool integration (decimation)', () => { + it('decimates large datasets via downsample tool', async () => { + const points = Array.from({ length: 6000 }, (_, i) => ({ date: `2025-01-01T00:${String(i).padStart(2,'0')}`, close: Math.sin(i / 10) })) + const res = await chartJsTool.execute({ data: points, indicators: [], chartType: 'line' }) + const labels = (res as any).config.data.labels + expect(labels.length).toBeLessThanOrEqual(2000) + }) +}) \ No newline at end of file diff --git a/src/mastra/tools/tests/downsample.tool.test.ts b/src/mastra/tools/tests/downsample.tool.test.ts new file mode 100644 index 00000000..9ae08e44 --- /dev/null +++ b/src/mastra/tools/tests/downsample.tool.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest' +import { downsampleTool } from '../downsample.tool' + +describe('downsampleTool', () => { + it('returns full series when target >= length', async () => { + const values = [1, 2, 3, 4, 5] + const res = await downsampleTool.execute({ values, target: 10 }) + expect(res.indices.length).toBe(5) + expect(res.values).toEqual(values) + }) + + it('downsamples to target points using lttb', async () => { + const values = Array.from({ length: 1000 }, (_, i) => Math.sin(i / 10)) + const target = 100 + const res = (await downsampleTool.execute({ values, target, algorithm: 'lttb' })) as any + expect(res.values.length).toBe(target) + expect(res.indices.length).toBe(target) + }) + + it('downsamples with min-max preserving peaks', async () => { + // create a signal with spikes + const values = Array.from({ length: 1000 }, (_, i) => (i % 50 === 0 ? 100 : Math.sin(i / 10))) + const res = (await downsampleTool.execute({ values, target: 100, algorithm: 'min-max' })) as any + // min-max may produce up to 2 * target points before dedup; ensure <= 2*target + expect(res.values.length).toBeLessThanOrEqual(200) + // ensure spikes are preserved in sampled values + expect(res.values.some((v: number) => v === 100)).toBe(true) + }) + it('downsamples with m4 preserving edges', async () => { + const values = Array.from({ length: 1000 }, (_, i) => Math.sin(i / 10)) + const res = await downsampleTool.execute({ values, target: 100, algorithm: 'm4' }) + // m4 returns up to 4 points per bucket but deduped; ensure result length reasonable + expect(res.values.length).toBeLessThanOrEqual(400) + }) +}) diff --git a/src/mastra/tools/tests/spatial-index.tool.test.ts b/src/mastra/tools/tests/spatial-index.tool.test.ts new file mode 100644 index 00000000..34fa93ea --- /dev/null +++ b/src/mastra/tools/tests/spatial-index.tool.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest' +import { spatialIndexTool } from '../spatial-index.tool' + +describe('spatialIndexTool', () => { + it('builds and searches index', async () => { + const points = [ + { id: 'a', lat: 10, lng: 10, data: { name: 'a' } }, + { id: 'b', lat: 20, lng: 20, data: { name: 'b' } }, + { id: 'c', lat: 30, lng: 30, data: { name: 'c' } }, + ] + + const build = await spatialIndexTool.execute({ action: 'build', points }) + expect(build.treeJSON).toBeDefined() + + const treeJSON = build.treeJSON + const res = await spatialIndexTool.execute({ action: 'search', treeJSON, bounds: { southWest: [5, 5], northEast: [25, 25] } }) + expect(res.results).toBeDefined() + expect((res.results as any[]).some((r) => r.id === 'a')).toBe(true) + expect((res.results as any[]).some((r) => r.id === 'b')).toBe(true) + expect((res.results as any[]).some((r) => r.id === 'c')).toBe(false) + }) +}) diff --git a/src/types/downsample-lttb.d.ts b/src/types/downsample-lttb.d.ts new file mode 100644 index 00000000..5f3d691e --- /dev/null +++ b/src/types/downsample-lttb.d.ts @@ -0,0 +1,3 @@ +declare module 'downsample-lttb' { + export default function downsample(points: Array<[number, number]>, threshold: number): Array<[number, number]>; +} diff --git a/tests/test-results/test-results.json b/tests/test-results/test-results.json index 7d32e4bf..493f0129 100644 --- a/tests/test-results/test-results.json +++ b/tests/test-results/test-results.json @@ -1 +1 @@ -{"numTotalTestSuites":2,"numPassedTestSuites":2,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":1,"numPassedTests":1,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1767908772739,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["ForecastView"],"fullName":"ForecastView renders forecast rows","status":"passed","title":"renders forecast rows","duration":51.56680000000006,"failureMessages":[],"meta":{}}],"startTime":1767908773982,"endTime":1767908774033.567,"status":"passed","message":"","name":"C:/Users/ssdsk/AgentStack/src/components/ai-elements/tools/__tests__/weather-tool.test.tsx"}]} \ No newline at end of file +{"numTotalTestSuites":4,"numPassedTestSuites":4,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":5,"numPassedTests":5,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1768071920164,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["downsampleTool"],"fullName":"downsampleTool returns full series when target >= length","status":"passed","title":"returns full series when target >= length","duration":3.6336999999998625,"failureMessages":[],"meta":{}},{"ancestorTitles":["downsampleTool"],"fullName":"downsampleTool downsamples to target points using lttb","status":"passed","title":"downsamples to target points using lttb","duration":7.829100000000153,"failureMessages":[],"meta":{}},{"ancestorTitles":["downsampleTool"],"fullName":"downsampleTool downsamples with min-max preserving peaks","status":"passed","title":"downsamples with min-max preserving peaks","duration":1.3289999999999509,"failureMessages":[],"meta":{}},{"ancestorTitles":["downsampleTool"],"fullName":"downsampleTool downsamples with m4 preserving edges","status":"passed","title":"downsamples with m4 preserving edges","duration":1.2346000000000004,"failureMessages":[],"meta":{}}],"startTime":1768072050289,"endTime":1768072050303.2346,"status":"passed","message":"","name":"C:/Users/ssdsk/AgentStack/src/mastra/tools/tests/downsample.tool.test.ts"},{"assertionResults":[{"ancestorTitles":["chartJsTool integration (decimation)"],"fullName":"chartJsTool integration (decimation) decimates large datasets via downsample tool","status":"passed","title":"decimates large datasets via downsample tool","duration":39.377700000000004,"failureMessages":[],"meta":{}}],"startTime":1768072050270,"endTime":1768072050309.3777,"status":"passed","message":"","name":"C:/Users/ssdsk/AgentStack/src/mastra/tools/tests/chartjs.integration.test.ts"}]} \ No newline at end of file diff --git a/thoughts/ledgers/CONTINUITY_ses_456e.md b/thoughts/ledgers/CONTINUITY_ses_456e.md new file mode 100644 index 00000000..8bab161a --- /dev/null +++ b/thoughts/ledgers/CONTINUITY_ses_456e.md @@ -0,0 +1,84 @@ +--- +session: ses_456e +updated: 2026-01-10T18:17:00.828Z +--- + +# Session Summary + +## Goal +Review and improve 4 Mastra tool files (`cytoscape.tool.ts`, `leaflet.tool.ts`, `image-tool.ts`, `technical-analysis.tool.ts`) to follow best practices: add RequestContext types, tracing spans, abortSignal checks, emit `data-tool-progress` events, fix bugs, and improve schema validation. + +## Constraints & Preferences +- Follow AgentStack tool patterns from `AGENTS.md`: Use `RequestContext`, `SpanType.TOOL_CALL` tracing, `abortSignal` checks, lifecycle hooks with `abortSignal?.aborted` logging +- All tools must emit `data-tool-progress` events with format: `{ status: 'in-progress'|'done', message: string, stage: string }` +- Use structured logging with the Mastra logger from `../config/logger` +- Priority order: cytoscape β†’ leaflet β†’ image β†’ technical-analysis +- Tracing pattern: `tracingContext?.currentSpan?.createChildSpan({ type: SpanType.TOOL_CALL, name: '...', input: inputData, metadata: {...} })` + +## Progress +### Done +- [x] Read `src/mastra/tools/AGENTS.md` for pattern reference (extensive documentation on tool lifecycle hooks, tracing patterns, RequestContext usage) +- [x] **FIXED** `cytoscape.tool.ts` - Removed corrupted duplicate code (lines 181-264) that was introduced by a broken edit + +### In Progress +- [ ] **Applying fixes to `leaflet.tool.ts`** - Edit partially applied but encountered TypeScript errors + +### Blocked +- [ ] `leaflet.tool.ts` has TypeScript errors after partial edit: + - ERROR [24:35] Expected 2-3 arguments, but got 1 + - ERROR [46:20] Expected 2-3 arguments, but got 1 + - These errors relate to `z.record()` calls + +## Key Decisions +- **File fix approach**: When `cytoscape.tool.ts` became corrupted, read the file to diagnose, then removed corrupted duplicate code to restore it +- **Preserved existing correct changes**: The cytoscape.tool.ts file already had the proper structure (RequestContext, tracing spans, abortSignal checks, progress events) + +## Next Steps +1. **Fix TypeScript errors in `leaflet.tool.ts`** - The `z.record(z.any())` calls are causing errors; may need to specify key type: `z.record(z.string(), z.any())` +2. **Verify `leaflet.tool.ts`** compiles correctly +3. **Apply fixes to `image-tool.ts`**: + - Fix bug: Change unconditional `pipeline = pipeline.flip()` to conditional + - Add RequestContext types, tracing, abortSignal checks, progress events +4. **Apply fixes to `technical-analysis.tool.ts`** +5. **Run diagnostics** to verify all files are syntactically correct + +## Critical Context +- **Pattern to implement** (from AGENTS.md): +```typescript +import type { RequestContext } from '@mastra/core/request-context' +import { SpanType } from '@mastra/core/observability' + +execute: async (input, context) => { + const abortSignal = context?.abortSignal + const tracingContext = context?.tracingContext + const requestCtx = context?.requestContext as ToolNameContext | undefined + + if (abortSignal?.aborted ?? false) { + throw new Error('Tool call cancelled') + } + + const toolSpan = tracingContext?.currentSpan?.createChildSpan({ + type: SpanType.TOOL_CALL, + name: 'tool-name', + input: input, + metadata: { 'tool.id': 'tool-id', ...contextValues }, + }) + + // ... tool logic ... + + toolSpan?.update({ output: result, metadata: {...} }) + toolSpan?.end() +} +``` + +- **Previous error**: The `leaflet.tool.ts` edit failed with TypeScript errors about `z.record()` expecting 2-3 arguments +- **Current file state**: The imports and LeafletToolContext interface were added successfully; the execute function body needs the rest of the changes + +## File Operations +### Read +- `C:\Users\ssdsk\AgentStack\src\mastra\tools\AGENTS.md` (pattern reference) +- `C:\Users\ssdsk\AgentStack\src\mastra\tools\leaflet.tool.ts` (to check current state after failed edit) + +### Modified +- `C:\Users\ssdsk\AgentStack\src\mastra\tools\cytoscape.tool.ts` - Removed corrupted duplicate code +- `C:\Users\ssdsk\AgentStack\src\mastra\tools\leaflet.tool.ts` - Partially applied (imports and interface added), but execute function needs completion diff --git a/thoughts/ledgers/CONTINUITY_ses_4571.md b/thoughts/ledgers/CONTINUITY_ses_4571.md new file mode 100644 index 00000000..ab1869fd --- /dev/null +++ b/thoughts/ledgers/CONTINUITY_ses_4571.md @@ -0,0 +1,55 @@ +--- +session: ses_4571 +updated: 2026-01-10T17:20:30.110Z +--- + +# Session Summary + +## Goal +Analyze the AgentStack project to understand its structure, purpose, technology stack, and current state to provide comprehensive context for development work. + +## Constraints & Preferences +(none) + +## Progress +### Done +- [x] Read README.md to understand project purpose (AgentStack: production-grade multi-agent framework built on Mastra with 60+ tools, 31+ agents, 15 workflows, 13 networks, 65 UI components) +- [x] Read package.json to identify dependencies and scripts (Next.js 16, React 19, TypeScript 5.x, Node.js β‰₯20.9.0, extensive AI/ML packages, 97% test coverage) +- [x] Examined project structure via glob listing (574 files, 6.3MB, includes app/, src/mastra/, ui/, docs/, tests/) +- [x] Checked git status (no uncommitted changes, some deleted .blackboxcli/ files) +- [x] Retrieved recent commit history (10 commits including features for session summaries, tracing documentation, financial tools, TypeScript error fixes) +- [x] Launched parallel background tasks for project structure analysis and technology stack detection +- [x] Searched for framework mentions (171 matches for Next.js, React, TypeScript, Node.js across documentation and code) +- [x] Found build script patterns (concurrent builds for Next.js + Mastra backend) + +### In Progress +- [ ] Background task analyzing project structure (running, task_id: bg_f4bd2ee9) +- [ ] Background task detecting technology stack (running, task_id: bg_3008c183) + +### Blocked +(none) + +## Key Decisions +- **Background task approach**: Launched multiple parallel explore agents instead of direct tool calls for more thorough analysis +- **File reading priority**: Started with README.md and package.json as primary discovery sources + +## Next Steps +1. Wait for background tasks to complete and retrieve their findings +2. Synthesize all gathered information into comprehensive project analysis report +3. Identify any gaps in understanding and perform targeted follow-up searches if needed +4. Present final analysis using the specified report format with project structure, technology stack, workflow, and current state + +## Critical Context +- AgentStack is a production-grade framework for scalable AI systems, focusing on financial intelligence, RAG pipelines, observability, secure governance, and AI chat interfaces +- Uses Next.js 16 App Router with React 19, extensive Mastra ecosystem (@mastra/core, @mastra/pg, @mastra/rag, etc.), and 97% test coverage +- Recent development includes tracing configuration, financial tools components, OpenTelemetry instrumentation, and TypeScript improvements +- Project structure follows clear separation: app/ (Next.js frontend), src/mastra/ (backend agents/tools), ui/ (components), docs/ (documentation) +- Requires Node.js β‰₯20.9.0 and PostgreSQL + pgvector for RAG/Memory features + +## File Operations +### Read +- C:\Users\ssdsk\AgentStack\README.md +- C:\Users\ssdsk\AgentStack\package.json + +### Modified +(none)