|
| 1 | +/** |
| 2 | + * Built-in tools for Cortex. |
| 3 | + * |
| 4 | + * Built-in tools are first-party primitives that run inside the agent loop |
| 5 | + * with direct access to cortex internals (outbox, channels, config). |
| 6 | + * They are NOT namespaced — tool names are bare (e.g. "send_message"). |
| 7 | + * |
| 8 | + * External skills loaded from skillDirs are namespaced as "skillId.toolName". |
| 9 | + */ |
| 10 | + |
| 11 | +import type { Result } from "@shetty4l/core/result"; |
| 12 | +import type { CortexConfig } from "../config"; |
| 13 | +import type { SkillRegistry, SkillToolResult, ToolDefinition } from "../skills"; |
| 14 | + |
| 15 | +// --- Types --- |
| 16 | + |
| 17 | +/** Mutable context updated per-message in the processing loop. */ |
| 18 | +export interface BuiltinToolContext { |
| 19 | + /** Topic key of the message currently being processed. */ |
| 20 | + topicKey: string; |
| 21 | +} |
| 22 | + |
| 23 | +/** A built-in tool with direct access to cortex internals. */ |
| 24 | +export interface BuiltinTool { |
| 25 | + readonly definition: ToolDefinition; |
| 26 | + execute( |
| 27 | + argsJson: string, |
| 28 | + ctx: BuiltinToolContext, |
| 29 | + ): Promise<Result<SkillToolResult>>; |
| 30 | +} |
| 31 | + |
| 32 | +// --- Output channel resolution --- |
| 33 | + |
| 34 | +/** Channels that are internal (system) and should not receive user-facing responses. */ |
| 35 | +const SYSTEM_CHANNELS = new Set(["thalamus", "calendar"]); |
| 36 | + |
| 37 | +/** |
| 38 | + * Resolve the output channel for a response. |
| 39 | + * |
| 40 | + * - If the input channel is a system channel, route to "silent". |
| 41 | + * - If the channel is "silent" and an alias is configured, redirect to the alias. |
| 42 | + * - Otherwise, return the channel unchanged (echo back to sender). |
| 43 | + */ |
| 44 | +export function resolveOutputChannel( |
| 45 | + inputChannel: string, |
| 46 | + config: Pick<CortexConfig, "silentChannelAlias">, |
| 47 | +): string { |
| 48 | + const channel = SYSTEM_CHANNELS.has(inputChannel) ? "silent" : inputChannel; |
| 49 | + if (channel === "silent" && config.silentChannelAlias) { |
| 50 | + return config.silentChannelAlias; |
| 51 | + } |
| 52 | + return channel; |
| 53 | +} |
| 54 | + |
| 55 | +// --- Combined registry --- |
| 56 | + |
| 57 | +/** |
| 58 | + * Create a unified SkillRegistry that dispatches to built-in tools first, |
| 59 | + * then falls through to the external skill registry. |
| 60 | + * |
| 61 | + * Built-in tools receive a BuiltinToolContext (topicKey, etc.) instead of |
| 62 | + * the SkillRuntimeContext used by external skills. |
| 63 | + * |
| 64 | + * @param builtinTools Array of built-in tool definitions + executors. |
| 65 | + * @param skillRegistry External skill registry loaded from skillDirs. |
| 66 | + * @param getContext Closure that returns the current per-message context. |
| 67 | + */ |
| 68 | +export function createCombinedRegistry( |
| 69 | + builtinTools: BuiltinTool[], |
| 70 | + skillRegistry: SkillRegistry, |
| 71 | + getContext: () => BuiltinToolContext, |
| 72 | +): SkillRegistry { |
| 73 | + const builtinMap = new Map(builtinTools.map((t) => [t.definition.name, t])); |
| 74 | + |
| 75 | + const allTools: ReadonlyArray<ToolDefinition> = [ |
| 76 | + ...builtinTools.map((t) => t.definition), |
| 77 | + ...skillRegistry.tools, |
| 78 | + ]; |
| 79 | + |
| 80 | + return { |
| 81 | + tools: allTools, |
| 82 | + |
| 83 | + async executeTool(name, argumentsJson, ctx) { |
| 84 | + const builtin = builtinMap.get(name); |
| 85 | + if (builtin) { |
| 86 | + return builtin.execute(argumentsJson, getContext()); |
| 87 | + } |
| 88 | + return skillRegistry.executeTool(name, argumentsJson, ctx); |
| 89 | + }, |
| 90 | + |
| 91 | + isMutating(name) { |
| 92 | + const builtin = builtinMap.get(name); |
| 93 | + if (builtin) return builtin.definition.mutatesState; |
| 94 | + return skillRegistry.isMutating(name); |
| 95 | + }, |
| 96 | + }; |
| 97 | +} |
0 commit comments