import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; const DEFAULT_COROS_AGENT_URL = "http://127.0.0.1:8787"; const DEFAULT_SESSION_ID = "pi-coros"; interface CorosAgentResponse { message?: string; capability?: string; confidence?: number; tools?: string[]; graph_steps?: string[]; citations?: string[]; memory?: string[]; error?: string; } function baseUrl(): string { return (process.env.COROS_RUNNING_AGENT_URL || DEFAULT_COROS_AGENT_URL).replace(/\/+$/, ""); } function defaultSessionId(): string { return process.env.COROS_RUNNING_AGENT_SESSION_ID || DEFAULT_SESSION_ID; } async function askCorosAgent(message: string, sessionId: string): Promise { const headers: Record = { "content-type": "application/json", }; const token = process.env.COROS_RUNNING_AGENT_TOKEN; if (token) { headers.authorization = `Bearer ${token}`; } const response = await fetch(`${baseUrl()}/api/chat`, { method: "POST", headers, body: JSON.stringify({ message, session_id: sessionId, }), }); const raw = await response.text(); let payload: CorosAgentResponse; try { payload = JSON.parse(raw) as CorosAgentResponse; } catch { payload = { error: raw }; } if (!response.ok) { throw new Error(payload.error || `COROS Running Agent request failed: ${response.status}`); } return payload; } function formatResponse(payload: CorosAgentResponse): string { const parts: string[] = []; if (payload.message?.trim()) { parts.push(payload.message.trim()); } const meta: string[] = []; if (payload.capability) meta.push(`capability=${payload.capability}`); if (typeof payload.confidence === "number") meta.push(`confidence=${payload.confidence.toFixed(2)}`); if (payload.tools?.length) meta.push(`tools=${payload.tools.join(", ")}`); if (payload.graph_steps?.length) meta.push(`steps=${payload.graph_steps.join(" -> ")}`); if (meta.length > 0) { parts.push(`\n\n---\n${meta.join("\n")}`); } if (payload.citations?.length) { parts.push(`\n\nCitations:\n${payload.citations.map((item) => `- ${item}`).join("\n")}`); } if (payload.memory?.length) { parts.push(`\n\nMemory used:\n${payload.memory.map((item) => `- ${item}`).join("\n")}`); } return parts.join("").trim() || "COROS Running Agent returned no message."; } export default function corosRunningAgentExtension(pi: ExtensionAPI): void { pi.registerTool({ name: "coros_running_agent", label: "COROS Running Agent", description: "Ask a COROS Running Agent HTTP backend about workouts, recent activities, running reports, personal bests, race context, photos, training plans, and running knowledge-base questions.", promptSnippet: "Ask COROS Running Agent for workout analysis, running coaching, COROS activity data, PBs, race photos, and running knowledge-base answers.", promptGuidelines: [ "Use coros_running_agent when the user asks about COROS workouts, recent training, running reports, personal bests, running photos, race context, or running training plans.", "Use coros_running_agent when a running coaching question may need personal memory, COROS data, RAG running books/videos, or backend web search.", "Do not invent workout metrics. If workout data is needed, call coros_running_agent and base the answer on its result.", ], parameters: Type.Object({ message: Type.String({ description: "The user's full running or COROS question. Preserve dates, activity numbers, race names, goals, and constraints.", }), sessionId: Type.Optional( Type.String({ description: "Stable conversation id for follow-up context. Omit to use COROS_RUNNING_AGENT_SESSION_ID.", }), ), }), async execute(_toolCallId, params, _signal, onUpdate) { onUpdate?.({ content: [{ type: "text", text: "Calling COROS Running Agent..." }], details: { status: "calling", baseUrl: baseUrl() }, }); const payload = await askCorosAgent(params.message, params.sessionId || defaultSessionId()); return { content: [{ type: "text", text: formatResponse(payload) }], details: payload, }; }, }); pi.registerCommand("coros", { description: "Ask the configured COROS Running Agent", handler: async (args, ctx) => { const message = args.trim(); if (!message) { ctx.ui.notify("Usage: /coros ", "warning"); return; } if (!ctx.isIdle()) { ctx.ui.notify("Agent is busy. Try again after the current response finishes.", "warning"); return; } pi.sendUserMessage(`Call coros_running_agent to answer this. Do not invent workout data: ${message}`); }, }); }