* S1-S3 — fork the parent's assistant message into a child-side message
* sequence so the subagent inherits the parent's reasoning trace cache-safely.
*
* The exact byte-for-byte placeholder string and message shape are critical
* for prompt-cache hits — do not casually tweak the wording or block order
* here, even when it looks harmless.
*/
import type {
CanonicalContentBlock,
CanonicalMessage,
CanonicalToolCallBlock,
} from "../../model/index.js";
* S2 — placeholder string injected into every synthetic `tool_result` so the
* fork is byte-identical across siblings (= cache hit on Anthropic / OpenAI).
*
* **Do not change** without a coordinated cache invalidation plan; both legacy
* and parity tests assert the literal value.
*/
export const FORK_PLACEHOLDER_RESULT =
"<pilotdeck-fork-placeholder>Subtask handled by forked subagent — see child transcript.</pilotdeck-fork-placeholder>";
export const FORK_BOILERPLATE_TAG = "pilotdeck-fork";
* S1 — Build the canonical message sequence handed to the subagent's
* `AgentLoop`:
*
* 1. Parent's assistant message verbatim (thinking + every tool_use + text).
* 2. A user message containing one synthetic `tool_result` (with
* `FORK_PLACEHOLDER_RESULT`) per `tool_use`, followed by the directive
* wrapped in `<pilotdeck-fork>` boilerplate.
*
* Returns a fresh array (never mutates `assistantMessage`).
*/
export function buildForkedMessages(
directive: string,
assistantMessage: CanonicalMessage,
): CanonicalMessage[] {
if (assistantMessage.role !== "assistant") {
throw new Error(
"buildForkedMessages: parent message must be role=assistant; got " +
assistantMessage.role,
);
}
const userBlocks: CanonicalContentBlock[] = [];
for (const block of assistantMessage.content) {
if (block.type === "tool_call") {
const tc = block as CanonicalToolCallBlock;
userBlocks.push({
type: "tool_result",
toolCallId: tc.id,
content: [{ type: "text", text: FORK_PLACEHOLDER_RESULT }],
});
}
}
userBlocks.push({ type: "text", text: buildChildMessage(directive) });
return [
cloneMessage(assistantMessage),
{ role: "user", content: userBlocks },
];
}
* S3 — child directive wrapped in fork boilerplate. Rules and output format
* live in the system prompt ({@link buildSubagentSystemPrompt}) and are NOT
* duplicated here — keeping the user message slim improves memory-retrieval
* signal-to-noise and saves ~200-300 tokens per sub-agent.
*/
export function buildChildMessage(directive: string): string {
return `<${FORK_BOILERPLATE_TAG}>\nDirective:\n${directive.trim()}\n</${FORK_BOILERPLATE_TAG}>`;
}
function cloneMessage(message: CanonicalMessage): CanonicalMessage {
return {
role: message.role,
content: message.content.map((block) => ({ ...block })),
};
}