Files
shiro-neko/src/agents.ts
T
Muhammad Zakir Ramadhan 2fa6ee247b Add batch reads, @file completion, interruptible commands, tool sets
Tools, six built-in to fourteen:
- read_many_files: up to 20 paths read concurrently, each with its own window.
  An unreadable path is reported in its own block instead of throwing.
- multi_edit: several edits to one file, validated in memory first so a late
  failure cannot leave the file half-written.
- list_dir: ignore-aware depth-limited tree.
- git_status/diff/log/show/blame: read-only, spawned with a fixed argv rather
  than a shell string, which is what makes them safe to auto-approve.

toolSets gates them. core is always on; edit-plus and git are optional. A
disabled set reaches neither the wire nor the system prompt, since a prompt
naming an absent tool teaches calls that cannot succeed.

Interface:
- Reasoning streams to a collapsed panel, ctrl-r expands, dropped when the turn
  ends: it is progress, not the answer.
- The tool in flight is named from tool-input-start, before its arguments finish
  streaming, and cleared on its result.
- Prompts typed mid-turn queue and drain in order. esc clears the queue as well
  as aborting.
- @ opens a path picker fed by the ignore-aware walker. Prefix matches rank
  above substring matches, so @src/ means "under src/". The walk runs on the
  first @, not at startup.

ctrl-c kills the command in flight and keeps the turn. The call throws rather
than returning, so the model cannot read a killed command as one that ran and
failed on its own terms. The kill takes the whole process tree: killing cmd /c
alone left the real command holding both pipes open, so the read never returned
and the interrupt did nothing for 19 seconds.

Two pruning fixes:
- A tool result whose tool call was pruned is now dropped with it. Pruning
  counts messages, so the cut landed between an assistant tool-call and the tool
  message answering it, producing 400 "No tool call found for function call
  output with call_id ...". The reverse pairing is left alone: a call awaiting
  its result is what a suspended approval looks like.
- ignore.ts called statFs without importing it, so walk() crashed on the first
  symlink.

482 tests, up from 404. Docs synced across README, ROADMAP, TODO, and all of
docs/: tool sets, the new tools, ctrl-c semantics, the tool-start event, and the
two hand-maintained tool-name lists recorded as a known weakness.
2026-09-03 01:37:48 +07:00

113 lines
3.8 KiB
TypeScript

export type ThinkingLevel = 'off' | 'low' | 'medium' | 'high' | 'max';
/** Maps our vocabulary to the SDK's, which each provider then maps to its own knob. */
const SDK_REASONING: Record<ThinkingLevel, 'none' | 'low' | 'medium' | 'high' | 'xhigh'> = {
off: 'none',
low: 'low',
medium: 'medium',
high: 'high',
max: 'xhigh',
};
export const THINKING_LEVELS: ThinkingLevel[] = ['off', 'low', 'medium', 'high', 'max'];
export const isThinkingLevel = (v: string): v is ThinkingLevel => (THINKING_LEVELS as string[]).includes(v);
export const sdkReasoning = (level: ThinkingLevel) => SDK_REASONING[level];
export type AgentVariant = {
name: string;
summary: string;
thinking: ThinkingLevel;
/** Appended to the system prompt to shape behaviour. */
appendix: string;
/** When set, only these tools are offered. Omit to offer everything. */
allowTools?: readonly string[];
maxSteps?: number;
};
const READ_ONLY = [
'read_file',
'read_many_files',
'glob',
'grep',
'list_dir',
'git_status',
'git_diff',
'git_log',
'git_show',
'git_blame',
'task',
'todo_write',
'remember',
'recall',
'skill',
] as const;
export const VARIANTS: AgentVariant[] = [
{
name: 'default',
summary: 'balanced: full tools, medium thinking',
thinking: 'medium',
appendix: '',
},
{
name: 'quick',
summary: 'small edits: no thinking budget, act immediately',
thinking: 'off',
maxSteps: 12,
appendix:
'This is a small, well-scoped task. Do not deliberate: locate the code, make the change, verify it. ' +
'Do not write a task list. Do not explore beyond what the change requires.',
},
{
name: 'deep',
summary: 'hard problems: maximum thinking, more steps',
thinking: 'max',
maxSteps: 80,
appendix:
'This task is hard or its cause is unclear. Form more than one hypothesis before you act and say which one ' +
'you are testing. Read enough of the code to be sure rather than guessing. Record findings with remember ' +
'so they survive compaction. Report what you verified and what you could not.',
},
{
name: 'plan',
summary: 'read-only: investigate and propose, never edit',
thinking: 'high',
allowTools: READ_ONLY,
appendix:
'You are in planning mode and have no tools that change anything. Investigate, then produce a plan: ' +
'the files to touch, the change in each, the order, and how to verify. Flag anything ambiguous instead of ' +
'assuming. Do not describe edits as if you had made them.',
},
{
name: 'review',
summary: 'read-only: critique a change, find defects',
thinking: 'high',
allowTools: READ_ONLY,
appendix:
'You are reviewing code, not writing it. Look for defects in this order: incorrect behaviour, missing error ' +
'handling at trust boundaries, security issues, then clarity. For each finding give file, line, why it is ' +
'wrong, and the fix. Say plainly when something is fine. Do not invent problems to fill a report.',
},
];
export const DEFAULT_VARIANT = VARIANTS[0]!;
export const variantByName = (name: string) => VARIANTS.find((v) => v.name === name);
/** Variant with an explicit thinking override applied, for `--agent deep --think low`. */
export function resolveAgent(name: string | undefined, thinking: string | undefined): AgentVariant {
const base = name ? variantByName(name) : DEFAULT_VARIANT;
if (!base) throw new Error(`Unknown agent "${name}". Available: ${VARIANTS.map((v) => v.name).join(', ')}`);
if (thinking === undefined) return base;
if (!isThinkingLevel(thinking)) {
throw new Error(`Unknown thinking level "${thinking}". Available: ${THINKING_LEVELS.join(', ')}`);
}
return { ...base, thinking };
}
export function renderAgent(variant: AgentVariant): string {
return variant.appendix ? `\n${variant.appendix}` : '';
}