/ utils / systemPrompt.ts
systemPrompt.ts
  1  import { feature } from 'bun:bundle'
  2  import {
  3    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  4    logEvent,
  5  } from '../services/analytics/index.js'
  6  import type { ToolUseContext } from '../Tool.js'
  7  import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js'
  8  import { isBuiltInAgent } from '../tools/AgentTool/loadAgentsDir.js'
  9  import { isEnvTruthy } from './envUtils.js'
 10  import { asSystemPrompt, type SystemPrompt } from './systemPromptType.js'
 11  
 12  export { asSystemPrompt, type SystemPrompt } from './systemPromptType.js'
 13  
 14  // Dead code elimination: conditional import for proactive mode.
 15  // Same pattern as prompts.ts — lazy require to avoid pulling the module
 16  // into non-proactive builds.
 17  /* eslint-disable @typescript-eslint/no-require-imports */
 18  const proactiveModule =
 19    feature('PROACTIVE') || feature('KAIROS')
 20      ? (require('../proactive/index.js') as typeof import('../proactive/index.js'))
 21      : null
 22  /* eslint-enable @typescript-eslint/no-require-imports */
 23  
 24  function isProactiveActive_SAFE_TO_CALL_ANYWHERE(): boolean {
 25    return proactiveModule?.isProactiveActive() ?? false
 26  }
 27  
 28  /**
 29   * Builds the effective system prompt array based on priority:
 30   * 0. Override system prompt (if set, e.g., via loop mode - REPLACES all other prompts)
 31   * 1. Coordinator system prompt (if coordinator mode is active)
 32   * 2. Agent system prompt (if mainThreadAgentDefinition is set)
 33   *    - In proactive mode: agent prompt is APPENDED to default (agent adds domain
 34   *      instructions on top of the autonomous agent prompt, like teammates do)
 35   *    - Otherwise: agent prompt REPLACES default
 36   * 3. Custom system prompt (if specified via --system-prompt)
 37   * 4. Default system prompt (the standard Claude Code prompt)
 38   *
 39   * Plus appendSystemPrompt is always added at the end if specified (except when override is set).
 40   */
 41  export function buildEffectiveSystemPrompt({
 42    mainThreadAgentDefinition,
 43    toolUseContext,
 44    customSystemPrompt,
 45    defaultSystemPrompt,
 46    appendSystemPrompt,
 47    overrideSystemPrompt,
 48  }: {
 49    mainThreadAgentDefinition: AgentDefinition | undefined
 50    toolUseContext: Pick<ToolUseContext, 'options'>
 51    customSystemPrompt: string | undefined
 52    defaultSystemPrompt: string[]
 53    appendSystemPrompt: string | undefined
 54    overrideSystemPrompt?: string | null
 55  }): SystemPrompt {
 56    if (overrideSystemPrompt) {
 57      return asSystemPrompt([overrideSystemPrompt])
 58    }
 59    // Coordinator mode: use coordinator prompt instead of default
 60    // Use inline env check instead of coordinatorModule to avoid circular
 61    // dependency issues during test module loading.
 62    if (
 63      feature('COORDINATOR_MODE') &&
 64      isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) &&
 65      !mainThreadAgentDefinition
 66    ) {
 67      // Lazy require to avoid circular dependency at module load time
 68      const { getCoordinatorSystemPrompt } =
 69        // eslint-disable-next-line @typescript-eslint/no-require-imports
 70        require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js')
 71      return asSystemPrompt([
 72        getCoordinatorSystemPrompt(),
 73        ...(appendSystemPrompt ? [appendSystemPrompt] : []),
 74      ])
 75    }
 76  
 77    const agentSystemPrompt = mainThreadAgentDefinition
 78      ? isBuiltInAgent(mainThreadAgentDefinition)
 79        ? mainThreadAgentDefinition.getSystemPrompt({
 80            toolUseContext: { options: toolUseContext.options },
 81          })
 82        : mainThreadAgentDefinition.getSystemPrompt()
 83      : undefined
 84  
 85    // Log agent memory loaded event for main loop agents
 86    if (mainThreadAgentDefinition?.memory) {
 87      logEvent('tengu_agent_memory_loaded', {
 88        ...(process.env.USER_TYPE === 'ant' && {
 89          agent_type:
 90            mainThreadAgentDefinition.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 91        }),
 92        scope:
 93          mainThreadAgentDefinition.memory as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 94        source:
 95          'main-thread' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 96      })
 97    }
 98  
 99    // In proactive mode, agent instructions are appended to the default prompt
100    // rather than replacing it. The proactive default prompt is already lean
101    // (autonomous agent identity + memory + env + proactive section), and agents
102    // add domain-specific behavior on top — same pattern as teammates.
103    if (
104      agentSystemPrompt &&
105      (feature('PROACTIVE') || feature('KAIROS')) &&
106      isProactiveActive_SAFE_TO_CALL_ANYWHERE()
107    ) {
108      return asSystemPrompt([
109        ...defaultSystemPrompt,
110        `\n# Custom Agent Instructions\n${agentSystemPrompt}`,
111        ...(appendSystemPrompt ? [appendSystemPrompt] : []),
112      ])
113    }
114  
115    return asSystemPrompt([
116      ...(agentSystemPrompt
117        ? [agentSystemPrompt]
118        : customSystemPrompt
119          ? [customSystemPrompt]
120          : defaultSystemPrompt),
121      ...(appendSystemPrompt ? [appendSystemPrompt] : []),
122    ])
123  }