All skills
medusajs avatar

/creating-internal-agents

@597f4a0 official
by Medusamedusajs/medusa-agent-skills225 stars
29

Use when building an internal admin-facing AI agent in a Medusa project. These agents are operated by merchants and store operators — not customers. Covers data models, module service, agent runtime (tools, system prompt, streamText), streaming API routes (NDJSON), and admin UI chat extensions. Load for any internal agent type: store operations assistant, product audit, cohort analysis, customer service tooling for support staff, etc. Do NOT use for customer-facing agents (storefront chatbots, buyer-side assistants).

  • 8 files
  • 46.7 KB
  • Updated 3 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/medusajs/medusa-agent-skills/creating-internal-agents

This session only. Nothing lands on disk.

referenceapi-route.md

≈1.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

API Route

Each agent gets its own set of routes but they all share the same AgentModuleService. The agent's own config (system prompt + tool descriptions) is defined locally and passed into stream() at call time.

Route paths use the agent's type slug, e.g. src/api/admin/agent/customer-service/route.ts.

POST — Main Chat Endpoint

// src/api/admin/agent/customer-service/route.ts
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import AgentModuleService from "../../../../modules/agent/service"
import { AGENT_MODULE } from "../../../../modules/agent"
import agentConfig from "../../../../modules/agent/config/customer-service"  // agent-specific config

const AGENT_TYPE = "customer-service"

export async function POST(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
  const { messages, session_id } = req.body as { messages: any[]; session_id?: string }

  const agentService: AgentModuleService = req.scope.resolve(AGENT_MODULE)

  // Create or reuse a session, always scoped to this agent type
  let sessionId = session_id
  if (!sessionId) {
    const firstUserMsg = messages.find((m) => m.role === "user")
    const title = firstUserMsg
      ? String(firstUserMsg.content).slice(0, 72)
      : "New conversation"
    const session = await agentService.createAgentSessions({
      agent_type: AGENT_TYPE,
      title,
      created_by_id: req.auth_context?.actor_id ?? "unknown",
    })
    sessionId = session.id
  }

  // Persist the latest user message
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user")
  if (lastUserMsg) {
    await agentService.createAgentMessages({
      agent_session_id: sessionId,
      role: "user",
      content: String(lastUserMsg.content),
    })
  }

  // Stream — pass this agent's config so it uses the right prompt and tools
  const result = agentService.stream(messages, req.scope, agentConfig)

  res.setHeader("Content-Type", "application/x-ndjson")
  res.setHeader("Transfer-Encoding", "chunked")
  res.setHeader("Cache-Control", "no-cache")

  const emit = (obj: object) => res.write(JSON.stringify(obj) + "\n")

  emit({ type: "session_id", sessionId })

  let assistantContent = ""

  for await (const chunk of result.fullStream) {
    if (chunk.type === "text-delta") {
      const text = (chunk as any).text ?? (chunk as any).textDelta ?? (chunk as any).delta ?? ""
      if (text) {
        assistantContent += text
        emit({ type: "text", content: text })
      }
    } else if (chunk.type === "tool-call") {
      const args = (chunk as any).args ?? (chunk as any).input
      emit({ type: "tool_call", tool: chunk.toolName, args })
    } else if (chunk.type === "tool-result") {
      emit({ type: "tool_result", tool: chunk.toolName })
    }
  }

  if (assistantContent) {
    await agentService.createAgentMessages({
      agent_session_id: sessionId,
      role: "assistant",
      content: assistantContent,
    })
  }

  res.end()
}

GET — Session List

Filter by agent_type so each agent only sees its own sessions.

// src/api/admin/agent/customer-service/sessions/route.ts
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import AgentModuleService from "../../../../../modules/agent/service"
import { AGENT_MODULE } from "../../../../../modules/agent"

const AGENT_TYPE = "customer-service"

export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
  const agentService: AgentModuleService = req.scope.resolve(AGENT_MODULE)

  const sessions = await agentService.listAgentSessions(
    { agent_type: AGENT_TYPE },
    {
      select: ["id", "title", "created_at", "created_by_id"],
      order: { created_at: "DESC" },
      take: 50,
    }
  )

  res.json({ sessions })
}

GET — Session Messages

// src/api/admin/agent/customer-service/sessions/[id]/route.ts
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import AgentModuleService from "../../../../../../modules/agent/service"
import { AGENT_MODULE } from "../../../../../../modules/agent"

export async function GET(
  req: AuthenticatedMedusaRequest & { params: { id: string } },
  res: MedusaResponse
) {
  const agentService: AgentModuleService = req.scope.resolve(AGENT_MODULE)

  const messages = await agentService.listAgentMessages(
    { agent_session_id: req.params.id },
    {
      select: ["id", "role", "content", "created_at"],
      order: { created_at: "ASC" },
    }
  )

  res.json({ messages })
}

Adding a Second Agent

Add a new route directory with its own AGENT_TYPE constant and its own config import. Everything else — the service, the models, the DB tables — is reused unchanged.

src/api/admin/agent/
  customer-service/
    route.ts                  ← AGENT_TYPE = "customer-service"
    sessions/route.ts
    sessions/[id]/route.ts
  product-audit/
    route.ts                  ← AGENT_TYPE = "product-audit"
    sessions/route.ts
    sessions/[id]/route.ts

Key Points

  • All agent routes must use AuthenticatedMedusaRequest — the agent triggers MedusaExec which runs arbitrary TypeScript with full database access. An unauthenticated route is a remote code execution vulnerability. Never place agent routes outside src/api/admin/ or relax the auth middleware.
  • agent_type is the only thing that distinguishes sessions across agents — always set it on create and filter by it on list.
  • Pass agentConfig (the agent-specific config) into stream() — not a shared default. Each agent's config lives alongside its routes or in src/modules/agent/config/<agent-type>.ts.
  • req.scope is the Medusa DI container — pass it to stream() so MedusaExec can resolve services.
  • Always call res.end() after the stream loop.

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 597f4a0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 3 months ago

README badge

README badge for medusajs/medusa-agent-skills/creating-internal-agents