Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,23 @@ This is a pnpm + Turborepo monorepo. Packages are ESM TypeScript.
The agent is in `apps/agent`.

- Mastra composition: `apps/agent/src/mastra/index.ts`.
- Agent and channel setup: `apps/agent/src/mastra/agents/agent.ts`.
- Product modules: `apps/agent/src/mastra/modules`.
- Knowledge domain: `apps/agent/src/modules/knowledge`.
- Agent composition: `apps/agent/src/app/agent/index.ts`.
- Chat SDK transport: `apps/agent/src/app/bot`.
- Product modules: `apps/agent/src/app`.
- Knowledge domain: `apps/agent/src/app/knowledge`.
- Drizzle schema: `apps/agent/src/infrastructure/database`.
- Previous AI SDK implementation: `apps/agent/archive-ai-sdk`.

Keep external systems behind service boundaries. Do not call provider SDKs or database tables directly from unrelated application code.

## Chat SDK Notes

Mastra Channels normalizes platform events and owns thread continuity.
Mastra registers the agent's HTTP routes; Chat SDK owns platform transport and thread continuity,
while Mastra owns agent execution.

- The Blooio iMessage adapter resolves the canonical resource from `message.author.userId`.
- Keep webhook routes thin and signature-verified.
- Keep Mastra-registered webhook routes thin and signature-verified. Chat SDK owns deduplication, queueing,
locks, and the single platform-posting path; do not add a second transcript or posting path in Mastra.
- Keep attachment limits and normalization in the attachments module.
- Do not use Mastra's in-process scheduler on serverless deployment. Recurring definitions use
Mastra storage, while QStash owns delivery timing.
Expand Down
1 change: 1 addition & 0 deletions apps/agent/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
output.txt
.tmp
.traces
logs
node_modules
dist
Expand Down
4 changes: 4 additions & 0 deletions apps/agent/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ Load the `mastra` skill BEFORE any Mastra work. Never rely on cached knowledge
## Rules

- Register all agents, tools, workflows, and scorers in `src/mastra/index.ts`
- Keep application/runtime code under `src/app` using feature-oriented boundaries; `src/mastra`
is the framework composition root and observability setup.
- Use the `dev` and `build` scripts from `package.json` instead of running `mastra dev` / `mastra build` directly
- Keep Mastra tables owned by `PostgresStore` in the `mastra` schema.
- Keep custom Drizzle tables under `src/infrastructure/database` and use the `agent_` prefix.
- Treat `archive-ai-sdk` as read-only reference material. Do not import, build, test, or deploy it
as part of the active Mastra application.
- Register HTTP routes with Mastra's `registerApiRoute`; do not create a second Hono application in
the agent package. Chat SDK remains the sole platform transport/orchestration runtime.
- Use a pooled Neon `DATABASE_URL` for runtime database access.

## Resources
Expand Down
26 changes: 22 additions & 4 deletions apps/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

Mastra personal assistant exposed through Studio and the Blooio-backed Chat SDK iMessage channel.

Mastra is the application API layer through its registered route descriptors. Chat SDK owns the
iMessage webhook handling, signature verification, deduplication, queueing, locks, and the single
platform-posting path. The transport attachment service validates and normalizes inbound files
before Mastra sees them. Mastra owns model, tool, memory, workflow, and observability execution;
it does not create a second transport or transcript path.

The previous AI SDK implementation is preserved in `archive-ai-sdk` for presentation and
historical reference only. It is not imported by the active application, included in the runtime
bundle, uploaded to Vercel, or covered by the active package's build and test commands.

Active application/runtime code lives under `src/app` in the same feature-oriented shape as the
archived implementation. `src/mastra/index.ts` is the Mastra composition root only.

## Capabilities

- Mastra observational memory with resource-scoped continuity
Expand All @@ -32,9 +41,12 @@ pnpm --filter @labjm/agent db:push
pnpm --filter @labjm/agent dev
```

`db:push` also initializes Mastra's storage schema. Production disables automatic storage
initialization so Vercel cold starts only perform normal queries, not schema DDL. Run `db:push`
before the first deployment and after upgrading Mastra storage packages.
`db:push` first initializes Chat SDK's PostgreSQL state tables, then pushes the application schema
and initializes Mastra's storage schema. Chat SDK remains the owner of its `chat_state_*` tables and
backing sequences; the push-only Drizzle config declares those sequences only to prevent Drizzle Kit
from proposing their deletion. Production disables automatic storage initialization so Vercel cold
starts only perform normal queries, not schema DDL. Run `db:push` before the first deployment and
after upgrading Mastra storage packages.

Open `http://localhost:4111`. Studio and generic agent APIs use `AGENT_API_TOKEN`; local development
falls back to `agent-local-dev-token`.
Expand Down Expand Up @@ -113,7 +125,8 @@ Output API artifact during that build.

Mastra serves Studio at `/` and mounts built-in server routes below `/api/mastra`. Application
routes use the remaining `/api` namespace: Google OAuth uses `/api/links/google/*`, QStash
delivers to `/api/jobs/schedules/execute`, and the iMessage webhook remains below
delivers to `/api/jobs/schedules/execute` and reports exhausted delivery retries to
`/api/jobs/schedules/failure`, and the iMessage webhook remains below
`/api/agents/*`.

The deployed Studio is served at the production origin and protected by `AGENT_API_TOKEN`. The
Expand Down Expand Up @@ -147,6 +160,11 @@ Normal tests are offline. The opt-in eval suite uses the configured model and da
pnpm --filter @labjm/agent eval
```

The core evals cover scheduling tool execution and truthful confirmations, Google OAuth/read-only
boundaries, multi-turn memory continuity, and durable knowledge retrieval/writes. They use a real
evaluation Postgres schema and a mocked QStash client so they never create external reminders.
CI enables them only when `RUN_AGENT_EVALS=true` and `OPENAI_API_KEY` is available.

## Verification

```sh
Expand Down
1 change: 1 addition & 0 deletions apps/agent/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default defineConfig({
},
out: './src/infrastructure/database/drizzle',
schema: './src/infrastructure/database/schema.ts',
schemaFilter: ['public'],
tablesFilter: ['agent_*'],
strict: true,
verbose: true,
Expand Down
27 changes: 27 additions & 0 deletions apps/agent/drizzle.push.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';

config({ path: '.env', quiet: true });
config({ path: '.env.local', override: true, quiet: true });

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
throw new Error('DATABASE_URL is required to manage the agent database schema.');
}

export default defineConfig({
dialect: 'postgresql',
dbCredentials: {
url: databaseUrl,
},
out: './src/infrastructure/database/drizzle',
schema: [
'./src/infrastructure/database/schema.ts',
'./src/infrastructure/database/drizzle-chat-state-sequences.ts',
],
schemaFilter: ['public'],
tablesFilter: ['agent_*'],
strict: true,
verbose: true,
});
7 changes: 3 additions & 4 deletions apps/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"dev": "mastra dev",
"build": "mastra build",
"postbuild": "node scripts/prepare-vercel-output.mjs",
"db:push": "drizzle-kit push && node scripts/initialize-mastra-storage.mjs",
"db:push:ci": "drizzle-kit push --force && node scripts/initialize-mastra-storage.mjs",
"db:push": "node scripts/initialize-chat-state.mjs && drizzle-kit push --config drizzle.push.config.ts && node scripts/initialize-mastra-storage.mjs",
"db:push:ci": "node scripts/initialize-chat-state.mjs && drizzle-kit push --config drizzle.push.config.ts --force && node scripts/initialize-mastra-storage.mjs",
"eval": "vitest run --config vitest.evals.config.ts",
"eval:watch": "vitest --config vitest.evals.config.ts",
"lint": "mastra lint",
Expand All @@ -26,9 +26,9 @@
},
"dependencies": {
"@ai-sdk/openai": "^4.0.8",
"@chat-adapter/state-pg": "4.35.0",
"@imessage-sdk/blooio": "^0.1.2",
"@imessage-sdk/chat-adapter": "0.1.1",
"@imessage-sdk/photon": "^0.1.2",
"@mastra/core": "latest",
"@mastra/deployer-vercel": "latest",
"@mastra/loggers": "^1.2.0",
Expand All @@ -43,7 +43,6 @@
"dedent": "1.7.2",
"drizzle-orm": "^0.45.2",
"heic-decode": "^2.1.0",
"hono": "^4.12.25",
"imessage-sdk": "^0.1.3",
"pg": "^8.22.0",
"sharp": "^0.34.5",
Expand Down
19 changes: 19 additions & 0 deletions apps/agent/scripts/initialize-chat-state.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createPostgresState } from '@chat-adapter/state-pg';
import { config } from 'dotenv';

config({ path: '.env', quiet: true });
config({ path: '.env.local', override: true, quiet: true });

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
throw new Error('DATABASE_URL is required to initialize Chat SDK state.');
}

const state = createPostgresState({ url: databaseUrl, keyPrefix: 'agent' });

try {
await state.connect();
} finally {
await state.disconnect();
}
138 changes: 138 additions & 0 deletions apps/agent/src/app/agent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { TokenLimiterProcessor } from '@mastra/core/processors';
import { askUserTool } from '@mastra/core/tools';
import { Memory } from '@mastra/memory';

import {
manageCalendarTool,
manageGoogleConnectionTool,
readCalendarTool,
readGmailTool,
} from '../features/google/tools';
import { manageNutritionTool, readNutritionTool } from '../features/nutrition/tools';
import { readLocalTimeTool, readWeatherTool } from '../features/weather/tools';
import { KnowledgeContextProcessor } from '../processors/knowledge-context';
import { OpenAIPromptCachingProcessor } from '../processors/openai-prompt-caching';
import { RuntimeContextProcessor } from '../processors/runtime-context';
import { manageScheduleTool } from '../schedules/tools';
import { responseQualityScorer } from '../scorers/response-quality';
import { calendarManagementSkill } from '../skills/calendar-management';
import { calorieTrackingSkill } from '../skills/calorie-tracking';
import { gmailManagementSkill } from '../skills/gmail-management';
import { knowledgeManagementSkill } from '../skills/knowledge-management';
import { schedulingSkill } from '../skills/scheduling';
import { manageKnowledgeTool, readKnowledgeTool } from '../tools/knowledge-tools';
import { daySummaryWorkflow } from '../workflows/day-summary';
import { agentInstructions } from './prompt';
import {
createOpenAILegacyPromptCacheModel,
createOpenAIPromptCacheOptions,
OpenAIExplicitPromptCacheBreakpoint,
OpenAIPromptCacheKeys,
} from './prompt-cache';
import { AgentRequestContextSchema, resolveIdentityId } from './runtime-context';

/**
* Mastra owns only agent execution. Transport, webhook deduplication, locks,
* attachment preparation, and platform posting live in `src/app/bot`.
*/
export const agent = new Agent({
id: 'agent',
name: 'Agent',
description:
'A personal assistant, living "next" to the user, that can help with a variety of tasks, reducing switching between apps and tools. The purpose is to streamline the user\'s workflow and enhance productivity by providing a single point of interaction for various tasks.',
instructions: {
role: 'system',
content: agentInstructions,
providerOptions: OpenAIExplicitPromptCacheBreakpoint,
},
model: 'openai/gpt-5.6-luna',
maxRetries: 1,
requestContextSchema: AgentRequestContextSchema,
defaultOptions: ({ requestContext }) => ({
maxSteps: 12,
autoResumeSuspendedTools: true,
providerOptions: {
openai: {
...createOpenAIPromptCacheOptions(
OpenAIPromptCacheKeys.mainAgent,
resolveIdentityId(requestContext),
),
reasoningEffort: 'high',
},
},
}),
memory: new Memory({
options: {
generateTitle: true,
lastMessages: 20,
observationalMemory: {
scope: 'resource',
shareTokenBudget: true,
activateAfterIdle: '30m',
observation: {
model: ({ requestContext }) =>
createOpenAILegacyPromptCacheModel(
'gpt-5.4-nano',
OpenAIPromptCacheKeys.memoryObserver,
resolveIdentityId(requestContext),
),
providerOptions: {},
},
reflection: {
providerOptions: {},
model: ({ requestContext }) =>
createOpenAILegacyPromptCacheModel(
'gpt-5.4-nano',
OpenAIPromptCacheKeys.memoryReflector,
resolveIdentityId(requestContext),
),
},
},
},
}),
inputProcessors: [
new RuntimeContextProcessor(),
new KnowledgeContextProcessor(),
new TokenLimiterProcessor({
limit: 300_000,
trimMode: 'contiguous',
}),
new OpenAIPromptCachingProcessor(),
],
skills: [
knowledgeManagementSkill,
schedulingSkill,
calendarManagementSkill,
gmailManagementSkill,
calorieTrackingSkill,
],
tools: {
ask_user: askUserTool,
read_knowledge: readKnowledgeTool,
manage_knowledge: manageKnowledgeTool,
manage_schedule: manageScheduleTool,
manage_google_connection: manageGoogleConnectionTool,
read_gmail: readGmailTool,
read_calendar: readCalendarTool,
manage_calendar: manageCalendarTool,
read_nutrition: readNutritionTool,
manage_nutrition: manageNutritionTool,
read_weather: readWeatherTool,
read_local_time: readLocalTimeTool,
web_search: openai.tools.webSearch(),
},
workflows: {
day_summary: daySummaryWorkflow,
},
scorers: {
responseQuality: {
scorer: responseQualityScorer,
sampling: {
type: 'ratio',
rate: 0.1,
},
},
},
});
Loading
Loading