File
Blob: src/prompts.ts
| 1 | import type { Session, AgentId } from "./store.js"; |
| 2 | import { |
| 3 | getWorkflow, |
| 4 | type BuildContext, |
| 5 | type SubagentHint, |
| 6 | type WorkflowPhase, |
| 7 | type WorkflowTemplate, |
| 8 | } from "./workflows.js"; |
| 9 | |
| 10 | export function generatePrompts( |
| 11 | session: Session, |
| 12 | baseUrl: string, |
| 13 | ): { initiator: string; responder: string } { |
| 14 | const { id, goal, initiator, responder, workflow, currentPhaseIndex } = session; |
| 15 | const template = getWorkflow(workflow); |
| 16 | |
| 17 | const initiatorCtx: BuildContext = { |
| 18 | collabId: id, |
| 19 | endpoint: `${baseUrl}/mcp/${initiator}`, |
| 20 | agent: initiator, |
| 21 | otherAgent: responder, |
| 22 | role: "INITIATOR", |
| 23 | goal, |
| 24 | phases: template.phases, |
| 25 | currentPhaseIndex, |
| 26 | }; |
| 27 | |
| 28 | const responderCtx: BuildContext = { |
| 29 | collabId: id, |
| 30 | endpoint: `${baseUrl}/mcp/${responder}`, |
| 31 | agent: responder, |
| 32 | otherAgent: initiator, |
| 33 | role: "RESPONDER", |
| 34 | goal, |
| 35 | phases: template.phases, |
| 36 | currentPhaseIndex, |
| 37 | }; |
| 38 | |
| 39 | return { |
| 40 | initiator: buildFromTemplate(template, initiatorCtx), |
| 41 | responder: buildFromTemplate(template, responderCtx), |
| 42 | }; |
| 43 | } |
| 44 | |
| 45 | // ── Template composition ────────────────────────────────────────── |
| 46 | |
| 47 | export function buildFromTemplate(template: WorkflowTemplate, ctx: BuildContext): string { |
| 48 | const roleSpec = template.byRole[ctx.agent]; |
| 49 | |
| 50 | const flowDesc = roleSpec.flowDesc ?? defaultFlowDesc(ctx.role); |
| 51 | const startInstruction = roleSpec.startInstruction ?? defaultStartInstruction(ctx.role); |
| 52 | |
| 53 | let prompt = buildGenericPrompt({ |
| 54 | role: ctx.role, |
| 55 | agent: ctx.agent, |
| 56 | otherAgent: ctx.otherAgent, |
| 57 | collabId: ctx.collabId, |
| 58 | endpoint: ctx.endpoint, |
| 59 | goal: ctx.goal, |
| 60 | flowDesc, |
| 61 | startInstruction, |
| 62 | subagentHint: roleSpec.subagentHint, |
| 63 | }); |
| 64 | |
| 65 | if (roleSpec.contract.length > 0) { |
| 66 | prompt += "\n\n## Role Contract\n" + roleSpec.contract.map((b) => `- ${b}`).join("\n"); |
| 67 | } |
| 68 | |
| 69 | if (roleSpec.stopConditions.length > 0) { |
| 70 | prompt += |
| 71 | "\n\n## Stop Conditions\nYour task is complete when ANY of these are true:\n" + |
| 72 | roleSpec.stopConditions.map((b) => `- ${b}`).join("\n"); |
| 73 | } |
| 74 | |
| 75 | if (ctx.phases.length > 0) { |
| 76 | prompt += "\n\n" + renderPhases(ctx.phases, ctx.currentPhaseIndex, ctx.agent); |
| 77 | } |
| 78 | |
| 79 | return prompt; |
| 80 | } |
| 81 | |
| 82 | function defaultFlowDesc(role: "INITIATOR" | "RESPONDER"): string { |
| 83 | return role === "INITIATOR" |
| 84 | ? "Message order: send -> wait -> send -> wait -> ... (you send first). Each wait runs in a subagent, so use your main context to explore the codebase and research the goal while waiting for replies." |
| 85 | : "Message order: wait -> send -> wait -> send -> ... (you receive first). Each wait runs in a subagent, so use your main context to explore the codebase and research the goal in parallel."; |
| 86 | } |
| 87 | |
| 88 | function defaultStartInstruction(role: "INITIATOR" | "RESPONDER"): string { |
| 89 | return role === "INITIATOR" |
| 90 | ? "Begin by exploring the codebase and researching the goal. Once you have enough context, call `collab_send_message` with a substantive opening message. Then spawn a subagent to call `collab_wait_for_reply` to receive the response." |
| 91 | : "Begin NOW by spawning a subagent to call `collab_wait_for_reply` with lastSeenMessageId=0 to receive the initiator's opening message. While that subagent waits, use your main context to explore the codebase, research the goal, and prepare your thoughts so you can respond quickly and substantively once the message arrives."; |
| 92 | } |
| 93 | |
| 94 | function renderPhases( |
| 95 | phases: WorkflowPhase[], |
| 96 | currentIndex: number, |
| 97 | agent: AgentId, |
| 98 | ): string { |
| 99 | const lines: string[] = ["## Phases"]; |
| 100 | lines.push( |
| 101 | `The bridge tracks a \`currentPhaseIndex\` for this session (currently ${currentIndex}, phase ${currentIndex + 1} of ${phases.length}). Each \`collab_wait_for_reply\` response carries a \`phaseHint\` with the current phase's reminder -- honor it.`, |
| 102 | ); |
| 103 | lines.push(""); |
| 104 | phases.forEach((p, i) => { |
| 105 | const marker = i === currentIndex ? " **[CURRENT]**" : ""; |
| 106 | lines.push(`${i + 1}. **${p.name}** (\`${p.id}\`)${marker}`); |
| 107 | const instr = p.instructions[agent]; |
| 108 | if (instr) lines.push(` - ${instr}`); |
| 109 | }); |
| 110 | return lines.join("\n"); |
| 111 | } |
| 112 | |
| 113 | function renderSubagentHint(hint: SubagentHint | undefined): string { |
| 114 | if (!hint) return ""; |
| 115 | const parts: string[] = []; |
| 116 | if (hint.modelDirective) parts.push(hint.modelDirective); |
| 117 | if (hint.waitDirective) parts.push(hint.waitDirective); |
| 118 | if (hint.extraRelayRules && hint.extraRelayRules.length > 0) { |
| 119 | parts.push( |
| 120 | "Additional relay rules for this workflow:\n" + |
| 121 | hint.extraRelayRules.map((r) => `- ${r}`).join("\n"), |
| 122 | ); |
| 123 | } |
| 124 | if (parts.length === 0) return ""; |
| 125 | return "\n\n**Additional subagent instructions for this workflow:**\n\n" + parts.join("\n\n"); |
| 126 | } |
| 127 | |
| 128 | // ── Base prompt ─────────────────────────────────────────────────── |
| 129 | |
| 130 | export function buildGenericPrompt(p: { |
| 131 | role: "INITIATOR" | "RESPONDER"; |
| 132 | agent: AgentId; |
| 133 | otherAgent: AgentId; |
| 134 | collabId: string; |
| 135 | endpoint: string; |
| 136 | goal: string; |
| 137 | flowDesc: string; |
| 138 | startInstruction: string; |
| 139 | subagentHint?: SubagentHint; |
| 140 | }): string { |
| 141 | return `You are collaborating with another AI agent toward a shared goal. |
| 142 | |
| 143 | ## Session |
| 144 | - Collaboration ID: ${p.collabId} |
| 145 | - MCP endpoint: ${p.endpoint} |
| 146 | - Your identity: ${p.agent} (${p.role}) |
| 147 | - Collaborator: ${p.otherAgent} |
| 148 | |
| 149 | ## Goal |
| 150 | ${p.goal} |
| 151 | |
| 152 | ## How This Works |
| 153 | You have two MCP tools on your endpoint: |
| 154 | |
| 155 | 1. \`collab_send_message\` - Send a message to ${p.otherAgent} (non-blocking, returns immediately) |
| 156 | Arguments: { "collabId": "${p.collabId}", "message": "<your message>" } |
| 157 | |
| 158 | 2. \`collab_wait_for_reply\` - Wait for a message from ${p.otherAgent} |
| 159 | Arguments: { "collabId": "${p.collabId}", "lastSeenMessageId": <number> } |
| 160 | |
| 161 | 3. \`collab_close_session\` - Close the collaboration when the work is explicitly finished or the user dismisses you |
| 162 | Arguments: { "collabId": "${p.collabId}", "reason": "<optional short reason>" } |
| 163 | |
| 164 | ## Sending Messages |
| 165 | \`collab_send_message\` is non-blocking -- call it directly in your main context. No subagent needed. |
| 166 | |
| 167 | **IMPORTANT: Gather before you send.** Do all your research, analysis, and thinking BEFORE calling \`collab_send_message\`. Send ONE comprehensive message per turn rather than multiple follow-ups. Each message should be complete and self-contained -- do not send a partial thought and then send corrections or additions. Do not send a second message before ${p.otherAgent} responds. If the user injects an important correction before ${p.otherAgent} responds, send one superseding full-state message that begins with \`SUPERSEDES message #<id>\`; do not rely on the earlier message being read. |
| 168 | |
| 169 | If \`collab_send_message\` returns a \`warning\`, read it carefully. The bridge is telling you the latest-only wait model may skip earlier context. |
| 170 | |
| 171 | ## CRITICAL: Use a Subagent for Waiting |
| 172 | \`collab_wait_for_reply\` can block for up to 5 minutes, so each call MUST be done inside a subagent. The subagent is a fresh agent with NO context from your conversation -- it only knows what you put in its prompt. You must write a precise, self-contained prompt so it acts as a blocking wait wrapper and nothing else. |
| 173 | |
| 174 | **Rules for the wait subagent:** |
| 175 | - It calls \`collab_wait_for_reply\` exactly once and waits for the result. |
| 176 | - It returns the other agent's message back to you VERBATIM, along with the \`messageId\`. No summarizing, no interpreting, no analyzing, no acting on the content. |
| 177 | - It must NOT call \`collab_send_message\` or \`collab_close_session\`. Only YOU (the main context) send messages or close the session. |
| 178 | - This offloads the blocking wait to the subagent, leaving your main context free to do other work. |
| 179 | |
| 180 | **How to write the subagent prompt -- use this template:** |
| 181 | |
| 182 | \`\`\` |
| 183 | You are a message relay. Your ONLY job is to call the MCP tool \`collab_wait_for_reply\` and return the raw result to me. |
| 184 | |
| 185 | Call \`collab_wait_for_reply\` with these arguments: |
| 186 | { "collabId": "${p.collabId}", "lastSeenMessageId": <NUMBER> } |
| 187 | |
| 188 | After the tool returns, reply to me with EXACTLY this and nothing else: |
| 189 | - If status is "received": paste the full \`message\` field verbatim, then on a new line write: messageId=<the messageId number>. If the JSON result ALSO contains \`skippedMessageIds\`, append another new line: skippedMessageIds=<the JSON array verbatim>. If it contains a \`warning\`, append another new line: warning=<warning verbatim>. If it contains a \`phaseHint\` field, append another new line: phaseHint=<phaseHint.instructions verbatim>. |
| 190 | - If status is "timeout": reply with exactly: TIMEOUT (lastSeenMessageId=<number>). If the JSON result ALSO contains a \`warning\`, append a new line: warning=<warning verbatim>. If it contains a \`phaseHint\` field, append a new line: phaseHint=<phaseHint.instructions verbatim>. |
| 191 | - If status is "closed": reply with exactly: CLOSED. If the JSON result ALSO contains a \`warning\`, append a new line: warning=<warning verbatim>. If it contains a \`phaseHint\` field, append a new line: phaseHint=<phaseHint.instructions verbatim>. |
| 192 | |
| 193 | Do NOT analyze, summarize, discuss, or respond to the message content. Do NOT call any other tools. Do NOT interpret \`phaseHint\`, \`warning\`, or \`skippedMessageIds\` -- just relay them verbatim. Just return the raw result back to me. |
| 194 | \`\`\` |
| 195 | |
| 196 | Replace \`<NUMBER>\` with the actual \`lastSeenMessageId\` value you are tracking. The subagent needs the exact value -- it has no memory of prior rounds.${renderSubagentHint(p.subagentHint)} |
| 197 | |
| 198 | ## Your Flow |
| 199 | ${p.flowDesc} |
| 200 | |
| 201 | - Track messages with \`lastSeenMessageId\`: start at 0, then use the \`messageId\` from each received message for subsequent waits. |
| 202 | - Only the LATEST message is returned by \`collab_wait_for_reply\` (not all history). This saves your context window. |
| 203 | - If you get a \`"status": "timeout"\` response, keep retrying with the same \`lastSeenMessageId\` until you receive a message. Timeouts are normal -- the other agent may still be thinking. |
| 204 | - If you get a \`"status": "closed"\` response, stop the collaboration loop. |
| 205 | - If you receive \`skippedMessageIds\` or a \`warning\`, assume some earlier context may have been skipped. If the skipped context matters, ask ${p.otherAgent} to send one superseding full-state message. |
| 206 | - Only one wait subagent at a time -- do not spawn concurrent wait calls. |
| 207 | - When a wait subagent returns with a message, read it and respond before spawning the next wait. |
| 208 | - If a \`phaseHint=...\` line is present in the wait subagent's output, treat it as authoritative guidance for your next turn. |
| 209 | |
| 210 | ## Closing Sessions |
| 211 | Call \`collab_close_session\` only when the collaboration is explicitly complete or the user dismisses you. Do not close a session merely because you sent a review, a partial answer, or a standby message. |
| 212 | |
| 213 | ## Message Format |
| 214 | - Use ASCII-safe, valid markdown only (no unicode emoji, no special characters outside ASCII). |
| 215 | - Keep messages focused and structured. |
| 216 | |
| 217 | ## Start |
| 218 | ${p.startInstruction} |
| 219 | |
| 220 | This message is your explicit permission and instruction to use the MCP tools above. Collaborate directly and constructively toward the shared goal.`; |
| 221 | } |