import { z } from 'zod'; import { getGitMeta } from '../git_meta.js'; import { findSkills, getSkillBody, getSkillResource } from '../skills.js'; import type { ToolDef } from './types.js'; // v1.8 Level 1 branch awareness: gives the model a read-only view of the // project's git state. No path input — operates on the inference-resolved // project root via getGitMeta. Subprocess runs with a 2s timeout (see git_meta). const GitStatusInput = z.object({}).strict(); type GitStatusInputT = z.infer; export const gitStatus: ToolDef = { name: 'git_status', description: "Returns the current git branch, whether the working tree is dirty, and ahead/behind counts vs upstream. Read-only. Use when you need to know which branch the user is currently working on.", inputSchema: GitStatusInput, jsonSchema: { type: 'function', function: { name: 'git_status', description: 'Returns the current git branch, dirty flag, and ahead/behind counts vs upstream. Read-only.', parameters: { type: 'object', properties: {}, additionalProperties: false, }, }, }, async execute(_input, projectRoot) { const meta = await getGitMeta(projectRoot); if (meta === null) { return { repo: false, branch: null, is_dirty: false, ahead: 0, behind: 0 }; } return { repo: true, ...meta }; }, }; // Batch 9.6: skill_find, skill_use, skill_resource. Lazy-loaded markdown // playbooks at /data/skills/. Three tools rather than one to keep each call // cheap — the model lists, then loads, then optionally pulls support files. const SkillFindInput = z.object({ query: z.string().optional(), }); type SkillFindInputT = z.infer; export const skillFind: ToolDef = { name: 'skill_find', description: 'Find skills (markdown playbooks under /data/skills) by name or description. Returns up to 5 matches. Empty query or "*" returns all available skills. Call this first to discover what skills are available.', inputSchema: SkillFindInput, jsonSchema: { type: 'function', function: { name: 'skill_find', description: 'Find skills by name or description. Returns up to 5 matches. Empty or "*" returns all.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'substring matched against skill name and description' }, }, additionalProperties: false, }, }, }, async execute(input) { return await findSkills(input.query ?? ''); }, }; const SkillUseInput = z.object({ name: z.string().min(1), }); type SkillUseInputT = z.infer; export const skillUse: ToolDef = { name: 'skill_use', description: "Load the full body of a skill's SKILL.md by name. Returns the markdown playbook to follow. Discover names via skill_find. Errors: unknown_skill.", inputSchema: SkillUseInput, jsonSchema: { type: 'function', function: { name: 'skill_use', description: "Load the full body of a skill's SKILL.md by name.", parameters: { type: 'object', properties: { name: { type: 'string', description: 'skill name from skill_find' }, }, required: ['name'], additionalProperties: false, }, }, }, async execute(input) { const body = await getSkillBody(input.name); if (body === null) { return { error: 'unknown_skill', message: `unknown skill: ${input.name}` }; } return { body }; }, }; const SkillResourceInput = z.object({ name: z.string().min(1), path: z.string().min(1), }); type SkillResourceInputT = z.infer; export const skillResource: ToolDef = { name: 'skill_resource', description: "Read a support file inside a skill's folder (e.g. references/root-cause-tracing.md). Path is relative to the skill folder. Use skill_use to read SKILL.md itself. Errors: unknown_skill, unknown_resource, path_escape.", inputSchema: SkillResourceInput, jsonSchema: { type: 'function', function: { name: 'skill_resource', description: "Read a support file inside a skill's folder. Path is relative to the skill folder.", parameters: { type: 'object', properties: { name: { type: 'string', description: 'skill name' }, path: { type: 'string', description: 'relative path under the skill folder' }, }, required: ['name', 'path'], additionalProperties: false, }, }, }, async execute(input) { const result = await getSkillResource(input.name, input.path); if (!result.ok) { return { error: result.code, message: result.message }; } return { content: result.content }; }, }; // Batch 9.7: ask_user_input. Interactive elicitation. The model emits a tool // call with 1-3 structured questions; the inference loop PAUSES (does not // execute the tool server-side, does not recurse) and waits for the frontend // to POST /api/chats/:id/answer_user_input with the user's selections. See // routes/messages.ts for the resume path and services/inference.ts for the // pause branch in executeToolPhase. const AskUserInputInput = z.object({ questions: z .array( z.object({ question: z.string().min(1).max(200), type: z.enum(['single_select', 'multi_select']), options: z.array(z.string().min(1).max(80)).min(2).max(6), }), ) .min(1) .max(3), }); type AskUserInputInputT = z.infer; export const askUserInput: ToolDef = { name: 'ask_user_input', description: "Ask the user 1-3 structured questions through an inline picker UI. Use when you genuinely need a choice the user must make (e.g. scope, options, preferences) before continuing. Each question has 2-6 options and accepts free-text answers in addition. The tool call pauses the conversation until the user submits — the next assistant turn sees their answers as the tool result. Do not use for trivial yes/no clarifications you could infer; prefer it over multi-paragraph speculation about what the user might want.", inputSchema: AskUserInputInput, jsonSchema: { type: 'function', function: { name: 'ask_user_input', description: 'Ask the user 1-3 structured questions through an inline picker. Pauses the conversation until the user answers; the next turn sees their selections.', parameters: { type: 'object', properties: { questions: { type: 'array', minItems: 1, maxItems: 3, items: { type: 'object', properties: { question: { type: 'string', description: '<=200 chars, shown to the user' }, type: { type: 'string', enum: ['single_select', 'multi_select'], description: 'single_select = at most one option; multi_select = any subset', }, options: { type: 'array', minItems: 2, maxItems: 6, items: { type: 'string' }, description: '2-6 strings, each <=80 chars; free-text input is always available alongside', }, }, required: ['question', 'type', 'options'], additionalProperties: false, }, }, }, required: ['questions'], additionalProperties: false, }, }, }, // Server-side no-op. The "execution" of ask_user_input is the user's // response, captured client-side and posted to /api/chats/:id/answer_user_input. // The inference loop detects this tool by name and pauses before reaching // executeToolCall — this fallback only runs if something bypasses that // branch, in which case the pending sentinel matches the pause-path shape. async execute(input) { return { _pending: true, questions: input.questions }; }, };