import { type SearchResult } from "../memory/archive-search.js"; /** * v0.5 — frontmatter-driven agent router. * * Per docs/plan/v0.5-workflow-maker.md §7. The pre-v0.5 router shipped a * hardcoded 60+ keyword → 25 agent map. v0.5 replaces it with a 3-tier * filesystem scan + 4-channel resolver (slash > explicit > keyword > freq). * `AGENT_ROUTES` is gone — keyword routing lives in each SKILL.md's * `triggers.keyword` frontmatter. * * 3-tier search (lowest priority first; higher tier overrides): * 3. /.solosquad/agents/{team}/{agent}/SKILL.md (bundled, init) * 2. ~/.solosquad/agents/{team}/{agent}/SKILL.md (user global) * 1. //.agents/{team}/{agent}/SKILL.md (org local — top) * * SKILL.md without frontmatter (pre-S5 migration state) is silently * skipped. Once S5 lands the 25 bundled SKILLs get auto-backfilled and * routing returns to coverage. * * Hot-reload contract: `buildRoutes()` is pure — call it from a wrapper * that swaps `routeIndexRef` atomically. While one call builds, a previous * index keeps serving. See `src/bot/index.ts` for the swap site. */ export type TriggerChannel = "slash" | "keyword" | "freq" | "explicit"; export interface AgentRef { team: string; name: string; source_path: string; /** Which tier resolved this — for diagnostics + duplicate-name reporting. */ tier: "org" | "user" | "workspace"; stateful: boolean; } export interface FreqRoute { ref: AgentRef; keywords: string[]; window_turns: number; threshold: number; cooldown_turns: number; } export interface RouteIndex { slash: Record; /** Keys lowercased. */ keyword: Record; freq: FreqRoute[]; /** Keyed by SkillSpec.name — case-sensitive (matches PM Task tool naming). */ explicit: Record; } export interface BuildRoutesOpts { /** Override workspace agents dir (test fixtures). */ agents_root?: string; /** Override user-global agents dir (test fixtures). */ user_root?: string; /** Org slug — when set, scans `//.agents/` as top-priority tier. */ org?: string; /** Override workspace root — defaults to getWorkspaceRoot(). */ workspace_root?: string; /** * v0.6 — when true, `resolveWithArchive()` falls back to FTS5 search on * router miss. Off by default to preserve v0.5 behavior; the message * dispatcher in `src/bot/index.ts` opts in. */ archive_fallback?: boolean; } export declare function buildRoutes(opts?: BuildRoutesOpts): RouteIndex; export interface ResolveCtx { /** Recent messages, oldest first. Used for freq scoring. */ history?: { text: string; }[]; /** Map of skill name → turns remaining in cooldown (router checks ≥1). */ freq_cooldowns?: Record; } export interface ResolveResult { ref: AgentRef; channel: TriggerChannel; /** What text token caused the match — for the "🧠 X auto-loaded" notice. */ matched: string; /** Only set when channel === "freq" — score that crossed the threshold. */ freq_score?: number; /** * When the caller should bump session-store cooldowns for this skill. * Only emitted on a freq match. */ start_cooldown?: { skill_name: string; turns: number; }; } /** * Pure resolver: given a message + history + cooldown state, return the * highest-priority match. Side-effect free — the caller updates cooldowns. */ export declare function resolve(message: string, idx: RouteIndex, ctx?: ResolveCtx): ResolveResult | null; /** * Single-shot keyword lookup. Builds the index per call — slow for hot * paths. The npm package surface (src/index.ts) re-exports this so existing * external callers keep working post-v0.5. Bot internals call `resolve()` * with a pre-built `RouteIndex`. */ export declare function findAgent(userInput: string): [string, string] | null; /** Read a SKILL.md from the workspace agents dir (unchanged from pre-v0.5). */ export declare function loadAgentSkill(team: string, agent: string): string; /** * Apply one turn's worth of decay to a freq_cooldowns map. Returns a new * map with zero-or-negative entries removed. Pure — no mutation. */ export declare function tickCooldowns(cooldowns: Record): Record; /** * Build a fresh RouteIndex and atomically install it as the current one. * Returns the installed index for diagnostics. */ export declare function rebuildRoutes(opts?: BuildRoutesOpts): RouteIndex; /** Swap the module-private ref. Exposed for tests that build offline. */ export declare function installRoutes(idx: RouteIndex): void; /** * Read the currently installed index. Returns null until `rebuildRoutes()` * (or `installRoutes()`) is called at least once. */ export declare function getCurrentRoutes(): RouteIndex | null; export interface ArchiveRecallNotice { /** Short, prompt-cache-safe inline string for the user message. */ inline: string; /** PM notification: "🧠 과거 N건 회상 (날짜: ...)" — printed once per miss. */ notice: string; /** Raw FTS5 hits — for diagnostics / tests. */ hits: SearchResult[]; } export interface ResolveWithArchiveOpts extends ResolveCtx { workspace: string; orgSlug: string; /** Default 3 — matches §4.3 (`ORDER BY rank LIMIT 3`). */ recall_limit?: number; /** Max characters in the inline recall payload. §4.4: ≤ 500. */ inline_char_cap?: number; } export interface ResolveWithArchiveResult { /** Non-null when the 4-channel router matched. */ resolved: ResolveResult | null; /** Set only on miss when `archive_fallback` recalled anything. */ recall: ArchiveRecallNotice | null; } /** * v0.6 router fallback. The normal `resolve()` runs first; on miss the * caller can opt into an FTS5 recall to surface past similar messages. * Pure with respect to side effects — the caller decides what to do with * the recall (inline into the next prompt + send notice). */ export declare function resolveWithArchive(message: string, idx: RouteIndex, opts: ResolveWithArchiveOpts): ResolveWithArchiveResult;