import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js' import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js' import { logEvent } from '../../services/analytics/index.js' import type { ValidationResult } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { isEnvTruthy } from '../../utils/envUtils.js' import { lazySchema } from '../../utils/lazySchema.js' import { plural } from '../../utils/stringUtils.js' import { resolveAttachments, validateAttachmentPaths } from './attachments.js' import { BRIEF_TOOL_NAME, BRIEF_TOOL_PROMPT, DESCRIPTION, LEGACY_BRIEF_TOOL_NAME, } from './prompt.js' import { renderToolResultMessage, renderToolUseMessage } from './UI.js' const inputSchema = lazySchema(() => z.strictObject({ message: z .string() .describe('The message for the user. Supports markdown formatting.'), attachments: z .array(z.string()) .optional() .describe( 'Optional file paths (absolute or relative to cwd) to attach. Use for photos, screenshots, diffs, logs, or any file the user should see alongside your message.', ), status: z .enum(['normal', 'proactive']) .describe( "Use 'proactive' when you're surfacing something the user hasn't asked for and needs to see now — task completion while they're away, a blocker you hit, an unsolicited status update. Use 'normal' when replying to something the user just said.", ), }), ) type InputSchema = ReturnType // attachments MUST remain optional — resumed sessions replay pre-attachment // outputs verbatim and a required field would crash the UI renderer on resume. const outputSchema = lazySchema(() => z.object({ message: z.string().describe('The message'), attachments: z .array( z.object({ path: z.string(), size: z.number(), isImage: z.boolean(), file_uuid: z.string().optional(), }), ) .optional() .describe('Resolved attachment metadata'), sentAt: z .string() .optional() .describe( 'ISO timestamp captured at tool execution on the emitting process. Optional — resumed sessions replay pre-sentAt outputs verbatim.', ), }), ) type OutputSchema = ReturnType export type Output = z.infer const KAIROS_BRIEF_REFRESH_MS = 5 * 60 * 1000 /** * Entitlement check — is the user ALLOWED to use Brief? Combines build-time * flags with runtime GB gate + assistant-mode passthrough. No opt-in check * here — this decides whether opt-in should be HONORED, not whether the user * has opted in. * * Build-time OR-gated on KAIROS || KAIROS_BRIEF (same pattern as * PROACTIVE || KAIROS): assistant mode depends on Brief, so KAIROS alone * must bundle it. KAIROS_BRIEF lets Brief ship independently. * * Use this to decide whether `--brief` / `defaultView: 'chat'` / `--tools` * listing should be honored. Use `isBriefEnabled()` to decide whether the * tool is actually active in the current session. * * CLAUDE_CODE_BRIEF env var force-grants entitlement for dev/testing — * bypasses the GB gate so you can test without being enrolled. Still * requires an opt-in action to activate (--brief, defaultView, etc.), but * the env var alone also sets userMsgOptIn via maybeActivateBrief(). */ export function isBriefEntitled(): boolean { // Positive ternary — see docs/feature-gating.md. Negative early-return // would not eliminate the GB gate string from external builds. return feature('KAIROS') || feature('KAIROS_BRIEF') ? getKairosActive() || isEnvTruthy(process.env.CLAUDE_CODE_BRIEF) || getFeatureValue_CACHED_WITH_REFRESH( 'tengu_kairos_brief', false, KAIROS_BRIEF_REFRESH_MS, ) : false } /** * Unified activation gate for the Brief tool. Governs model-facing behavior * as a unit: tool availability, system prompt section (getBriefSection), * tool-deferral bypass (isDeferredTool), and todo-nag suppression. * * Activation requires explicit opt-in (userMsgOptIn) set by one of: * - `--brief` CLI flag (maybeActivateBrief in main.tsx) * - `defaultView: 'chat'` in settings (main.tsx init) * - `/brief` slash command (brief.ts) * - `/config` defaultView picker (Config.tsx) * - SendUserMessage in `--tools` / SDK `tools` option (main.tsx) * - CLAUDE_CODE_BRIEF env var (maybeActivateBrief — dev/testing bypass) * Assistant mode (kairosActive) bypasses opt-in since its system prompt * hard-codes "you MUST use SendUserMessage" (systemPrompt.md:14). * * The GB gate is re-checked here as a kill-switch AND — flipping * tengu_kairos_brief off mid-session disables the tool on the next 5-min * refresh even for opted-in sessions. No opt-in → always false regardless * of GB (this is the fix for "brief defaults on for enrolled ants"). * * Called from Tool.isEnabled() (lazy, post-init), never at module scope. * getKairosActive() and getUserMsgOptIn() are set in main.tsx before any * caller reaches here. */ export function isBriefEnabled(): boolean { // Top-level feature() guard is load-bearing for DCE: Bun can constant-fold // the ternary to `false` in external builds and then dead-code the BriefTool // object. Composing isBriefEntitled() alone (which has its own guard) is // semantically equivalent but defeats constant-folding across the boundary. return feature('KAIROS') || feature('KAIROS_BRIEF') ? (getKairosActive() || getUserMsgOptIn()) && isBriefEntitled() : false } export const BriefTool = buildTool({ name: BRIEF_TOOL_NAME, aliases: [LEGACY_BRIEF_TOOL_NAME], searchHint: 'send a message to the user — your primary visible output channel', maxResultSizeChars: 100_000, userFacingName() { return '' }, get inputSchema(): InputSchema { return inputSchema() }, get outputSchema(): OutputSchema { return outputSchema() }, isEnabled() { return isBriefEnabled() }, isConcurrencySafe() { return true }, isReadOnly() { return true }, toAutoClassifierInput(input) { return input.message }, async validateInput({ attachments }, _context): Promise { if (!attachments || attachments.length === 0) { return { result: true } } return validateAttachmentPaths(attachments) }, async description() { return DESCRIPTION }, async prompt() { return BRIEF_TOOL_PROMPT }, mapToolResultToToolResultBlockParam(output, toolUseID) { const n = output.attachments?.length ?? 0 const suffix = n === 0 ? '' : ` (${n} ${plural(n, 'attachment')} included)` return { tool_use_id: toolUseID, type: 'tool_result', content: `Message delivered to user.${suffix}`, } }, renderToolUseMessage, renderToolResultMessage, async call({ message, attachments, status }, context) { const sentAt = new Date().toISOString() logEvent('tengu_brief_send', { proactive: status === 'proactive', attachment_count: attachments?.length ?? 0, }) if (!attachments || attachments.length === 0) { return { data: { message, sentAt } } } const appState = context.getAppState() const resolved = await resolveAttachments(attachments, { replBridgeEnabled: appState.replBridgeEnabled, signal: context.abortController.signal, }) return { data: { message, attachments: resolved, sentAt }, } }, } satisfies ToolDef)