When adding behavior, reduce duplication and conditional sprawl by modeling variants explicitly and centralizing shared logic.
Standards
JsonRunOutputter vs ConsoleRunOutputter) and call straight-line lifecycle methods, instead of repeatedly branching if (outputMode === 'json') throughout the run path.resumed?: 'yes' | 'no' (or agent_start/agent_resume) and discriminated unions for formatter variants.Example (centralized formatter with discriminated union)
type HookContextVariant =
| { kind: 'tool'; toolName: string; toolCallId: string }
| { kind: 'runStart'; source: 'RunStart' };
function escapeEmbeddedHookContexts(text: string) {
return text.trim().replace(/<(\/?)hook_context/gi, '<\\$1hook_context');
}
function formatHookContextBlock(body: string, v: HookContextVariant): string {
const escaped = escapeEmbeddedHookContexts(body);
if (v.kind === 'tool') {
return `<hook_context source="${v.toolName}" tool_name="${v.toolName}" tool_call_id="${v.toolCallId}">\n${escaped}\n</hook_context>`;
}
return `<hook_context source="${v.source}">\n${escaped}\n</hook_context>`;
}
Apply the same principle to run/output handling: choose the correct implementation once, then use consistent method names and straight-line lifecycle calls.