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.

referencestreaming.md

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

Streaming

Agent responses are streamed as NDJSON (newline-delimited JSON) — one JSON object per line, emitted incrementally as the model produces output.

Server-Side: Emitting Chunks

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

// Helper: serialize one object as a line
const emit = (obj: object) => res.write(JSON.stringify(obj) + "\n")

Chunk Types

type When emitted Shape
session_id Immediately at start { type: "session_id", sessionId: string }
text Each word/token from the model { type: "text", content: string }
tool_call When a tool fires { type: "tool_call", tool: string, args: object }
tool_result When a tool call completes { type: "tool_result", tool: string }

Iterating the Full Stream

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 })
  }
}

res.end()  // REQUIRED — closes the HTTP response

Note: The Vercel AI SDK uses inconsistent field names across versions. Always fall back through text ?? textDelta ?? delta and args ?? input to be safe.

Client-Side: Parsing the NDJSON Stream

async function sendMessage(messages: any[], sessionId: string | null) {
  const response = await fetch("/admin/my-agent", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages, session_id: sessionId }),
    credentials: "include",  // sends admin session cookie
  })

  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ""

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split("\n")
    // Keep the last (potentially incomplete) line in the buffer
    buffer = lines.pop() ?? ""

    for (const line of lines) {
      if (!line.trim()) continue
      try {
        const chunk = JSON.parse(line)
        handleChunk(chunk)
      } catch {
        // incomplete JSON line — wait for more data
      }
    }
  }
}

function handleChunk(chunk: any) {
  if (chunk.type === "session_id") {
    // Persist session ID in state for subsequent messages
    setSessionId(chunk.sessionId)
  } else if (chunk.type === "text") {
    // Append text to the current assistant message
    appendToLastMessage(chunk.content)
  } else if (chunk.type === "tool_call") {
    // Show a "running" indicator for this tool
    addToolCall({ tool: chunk.tool, status: "running" })
  } else if (chunk.type === "tool_result") {
    // Mark the tool call as done
    markToolDone(chunk.tool)
  }
}

Key Rules

  • Buffer incomplete lines — network packets may split a JSON object across two reads. Always accumulate a buffer and split on \n.
  • credentials: "include" — Medusa admin uses cookie-based auth; without this the request is rejected as unauthenticated.
  • Emit session_id first — the client needs this before any other chunk so it can link subsequent user messages to the same session.
  • Call res.end() — without it, the client's reader.read() never returns done: true and the connection hangs.
  • Transfer-Encoding: chunked — tells the HTTP layer not to buffer the response body; required for true streaming.

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