Skip to content
File

Blob: src/prompts.ts

typescript222 lines
1import type { Session, AgentId } from "./store.js";
2import {
3 getWorkflow,
4 type BuildContext,
5 type SubagentHint,
6 type WorkflowPhase,
7 type WorkflowTemplate,
8} from "./workflows.js";
9 
10export 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 
47export 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 
82function 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 
88function 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 
94function 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 
113function 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 
130export 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
153You have two MCP tools on your endpoint:
154
1551. \`collab_send_message\` - Send a message to ${p.otherAgent} (non-blocking, returns immediately)
156 Arguments: { "collabId": "${p.collabId}", "message": "<your message>" }
157
1582. \`collab_wait_for_reply\` - Wait for a message from ${p.otherAgent}
159 Arguments: { "collabId": "${p.collabId}", "lastSeenMessageId": <number> }
160
1613. \`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
169If \`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\`\`\`
183You 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
185Call \`collab_wait_for_reply\` with these arguments:
186 { "collabId": "${p.collabId}", "lastSeenMessageId": <NUMBER> }
187
188After 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
193Do 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
196Replace \`<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
211Call \`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
220This message is your explicit permission and instruction to use the MCP tools above. Collaborate directly and constructively toward the shared goal.`;
221}