ask user
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@inquirer/prompts": "^7.5.1",
|
"@inquirer/prompts": "^7.5.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"dotenv": "^18.0.2",
|
"dotenv": "^18.0.2",
|
||||||
|
"ignore": "^7.0.10",
|
||||||
"llmorbiter": "file:../llmorbiter"
|
"llmorbiter": "file:../llmorbiter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -462,6 +463,15 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ignore": {
|
||||||
|
"version": "7.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz",
|
||||||
|
"integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@inquirer/prompts": "^7.5.1",
|
"@inquirer/prompts": "^7.5.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"dotenv": "^18.0.2",
|
"dotenv": "^18.0.2",
|
||||||
|
"ignore": "^7.0.10",
|
||||||
"llmorbiter": "file:../llmorbiter"
|
"llmorbiter": "file:../llmorbiter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
222
src/fido.js
222
src/fido.js
@@ -22,9 +22,10 @@ import { fileURLToPath } from 'url';
|
|||||||
import { createInterface } from 'readline';
|
import { createInterface } from 'readline';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { input } from '@inquirer/prompts';
|
import { input, select } from '@inquirer/prompts';
|
||||||
|
|
||||||
import { LLMOrbiter, OpenRouterProvider, defineTool, ToolError } from 'llmorbiter';
|
import { LLMOrbiter, OpenRouterProvider, defineTool, ToolError } from 'llmorbiter';
|
||||||
|
import ignore from 'ignore';
|
||||||
|
|
||||||
// ─── Bootstrap ─────────────────────────────────────────────────────────────────
|
// ─── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -35,8 +36,8 @@ dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
|
|||||||
// Also try cwd-relative .env
|
// Also try cwd-relative .env
|
||||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||||
|
|
||||||
const API_KEY = process.env.OPENROUTER_API_KEY;
|
const API_KEY = process.env.OPENROUTER_API_KEY;
|
||||||
const MODEL = process.env.MODEL || 'xiaomi/mimo-v2.6-flash';
|
const MODEL = process.env.MODEL || 'xiaomi/mimo-v2.6-flash';
|
||||||
const UNDO_FILE = process.env.UNDO_FILE || '.fido-undo.json';
|
const UNDO_FILE = process.env.UNDO_FILE || '.fido-undo.json';
|
||||||
|
|
||||||
// Root dir: --dir flag wins, then env ROOT_DIR, then cwd
|
// Root dir: --dir flag wins, then env ROOT_DIR, then cwd
|
||||||
@@ -78,12 +79,50 @@ function resolveSafe(userPath) {
|
|||||||
// ─── Directory listing helper ──────────────────────────────────────────────────
|
// ─── Directory listing helper ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively list files under a directory, relative to ROOT_DIR.
|
* Build an `ignore` filter that respects all .gitignore files found between
|
||||||
* Returns { files: string[], total: number, truncated: number }
|
* ROOT_DIR and `dir` (inclusive), plus a hardcoded node_modules rule.
|
||||||
|
* Paths fed to ig.ignores() must be relative to ROOT_DIR and use forward slashes.
|
||||||
*/
|
*/
|
||||||
function listFilesRecursive(dir, seek = 0, limit = 50) {
|
function buildIgnoreFilter(dir) {
|
||||||
const allFiles = [];
|
const ig = ignore().add('node_modules/');
|
||||||
const walk = (current) => {
|
// Collect .gitignore files from ROOT_DIR down to dir
|
||||||
|
const rel = path.relative(ROOT_DIR, dir);
|
||||||
|
const parts = rel === '' ? [] : rel.split(path.sep);
|
||||||
|
const ancestors = [ROOT_DIR];
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
ancestors.push(path.join(ROOT_DIR, ...parts.slice(0, i + 1)));
|
||||||
|
}
|
||||||
|
for (const ancestor of ancestors) {
|
||||||
|
const gi = path.join(ancestor, '.gitignore');
|
||||||
|
if (fs.existsSync(gi)) {
|
||||||
|
try {
|
||||||
|
const rules = fs.readFileSync(gi, 'utf8');
|
||||||
|
// Rules in a nested .gitignore are relative to that directory;
|
||||||
|
// prefix them so they work relative to ROOT_DIR.
|
||||||
|
const prefix = path.relative(ROOT_DIR, ancestor).replace(/\\/g, '/');
|
||||||
|
const prefixed = rules
|
||||||
|
.split('\n')
|
||||||
|
.filter(l => l.trim() && !l.startsWith('#'))
|
||||||
|
.map(l => (prefix ? `${prefix}/${l.trim()}` : l.trim()));
|
||||||
|
ig.add(prefixed);
|
||||||
|
} catch { /* ignore read errors */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List entries of a directory, optionally recursive, respecting .gitignore.
|
||||||
|
* Returns { entries: Array<{path,type,size}>, total, truncated }
|
||||||
|
* - type: 'd' for directory, 'f' for file
|
||||||
|
* - size: file size in bytes (null for directories)
|
||||||
|
* All paths are relative to ROOT_DIR and use forward slashes.
|
||||||
|
*/
|
||||||
|
function listDir(dir, { seek = 0, limit = 50, recursive = false } = {}) {
|
||||||
|
const ig = buildIgnoreFilter(dir);
|
||||||
|
const allEntries = [];
|
||||||
|
|
||||||
|
const collect = (current) => {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||||
@@ -92,47 +131,50 @@ function listFilesRecursive(dir, seek = 0, limit = 50) {
|
|||||||
}
|
}
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
const full = path.join(current, e.name);
|
const full = path.join(current, e.name);
|
||||||
const rel = path.relative(ROOT_DIR, full).replace(/\\/g, '/');
|
const rel = path.relative(ROOT_DIR, full).replace(/\\/g, '/');
|
||||||
|
if (ig.ignores(e.isDirectory() ? rel + '/' : rel)) continue;
|
||||||
if (e.isDirectory()) {
|
if (e.isDirectory()) {
|
||||||
allFiles.push(rel + '/');
|
allEntries.push({ path: rel + '/', type: 'd', size: null });
|
||||||
walk(full);
|
if (recursive) collect(full);
|
||||||
} else {
|
} else {
|
||||||
allFiles.push(rel);
|
let size = null;
|
||||||
|
try { size = fs.statSync(full).size; } catch { /* skip */ }
|
||||||
|
allEntries.push({ path: rel, type: 'f', size });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
walk(dir);
|
collect(dir);
|
||||||
const total = allFiles.length;
|
|
||||||
const sliced = allFiles.slice(seek, seek + limit);
|
const total = allEntries.length;
|
||||||
|
const sliced = allEntries.slice(seek, seek + limit);
|
||||||
const truncated = total - (seek + sliced.length);
|
const truncated = total - (seek + sliced.length);
|
||||||
return { files: sliced, total, truncated: Math.max(0, truncated) };
|
return { entries: sliced, total, truncated: Math.max(0, truncated) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── System prompt builder ─────────────────────────────────────────────────────
|
// ─── System prompt builder ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function buildSystemPrompt() {
|
function buildSystemPrompt() {
|
||||||
const { files, total, truncated } = listFilesRecursive(ROOT_DIR, 0, 50);
|
const { entries, total, truncated } = listDir(ROOT_DIR, { seek: 0, limit: 50, recursive: false });
|
||||||
let dirSection = files.map(f => ` ${f}`).join('\n');
|
const fmt = (e) => e.type === 'd' ? `d ${e.path}` : `f ${e.size ?? '?'} ${e.path}`;
|
||||||
|
let dirSection = entries.map(fmt).join('\n');
|
||||||
if (truncated > 0) {
|
if (truncated > 0) {
|
||||||
dirSection += `\n ... (${truncated} more files not shown — use list_files to explore)`;
|
dirSection += `\n ... (${truncated} more entries not shown — use list_files to explore)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `You are Fido, a minimal but capable coding assistant.
|
return `You are Fido, a coding assistant.
|
||||||
You can read files, list directories, and apply code patches within the workspace.
|
You can read files, list directories, and apply code patches within the workspace.
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
Root directory: ${ROOT_DIR}
|
Root directory: ${ROOT_DIR}
|
||||||
Total files: ${total}
|
Total files: ${total}
|
||||||
|
|
||||||
## Current directory listing (first 50 of ${total}):
|
## Workspace root (${total} entries):
|
||||||
${dirSection}
|
${dirSection}
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
- You may ONLY access files within the workspace root. Never use ".." or absolute paths.
|
- You may ONLY access files within the workspace root. Never use ".." or absolute paths.
|
||||||
- When editing files, prefer targeted SEARCH/REPLACE patches over full rewrites.
|
- Undo is handled externally by the user
|
||||||
- After applying a patch, briefly confirm what you did.
|
- When the user's intent is ambiguous or you need a decision to proceed, use the ask_user tool — don't guess.
|
||||||
- Keep responses concise and focused.
|
|
||||||
- Use the tools; do not emit raw file contents unless the user asks.
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,10 +232,8 @@ function pushUndo(entry) {
|
|||||||
*/
|
*/
|
||||||
function parsePatch(patchStr) {
|
function parsePatch(patchStr) {
|
||||||
const ops = [];
|
const ops = [];
|
||||||
// Split on operation boundaries: lines starting with "<<<<<"
|
// Normalise line endings so the regex always works regardless of CRLF/LF/CR
|
||||||
const opPattern = /^<<<<< (\w+) (.+)$/m;
|
const lines = patchStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
||||||
// We'll scan line by line
|
|
||||||
const lines = patchStr.split('\n');
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
@@ -225,7 +265,7 @@ function parsePatch(patchStr) {
|
|||||||
ops.push({ type: 'DELETE_DIR', path: opPath });
|
ops.push({ type: 'DELETE_DIR', path: opPath });
|
||||||
} else if (opType === 'SEARCH') {
|
} else if (opType === 'SEARCH') {
|
||||||
// Parse inner <<<<<<< SEARCH / ======= / >>>>>>> REPLACE block
|
// Parse inner <<<<<<< SEARCH / ======= / >>>>>>> REPLACE block
|
||||||
const searchMatch = body.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
|
const searchMatch = body.match(/<<<<<<< SEARCH\r?\n([\s\S]*?)\r?\n=======\r?\n([\s\S]*?)\r?\n>>>>>>> REPLACE/);
|
||||||
if (!searchMatch) {
|
if (!searchMatch) {
|
||||||
ops.push({ type: 'SEARCH', path: opPath, error: 'Malformed SEARCH block — expected <<<<<<< SEARCH / ======= / >>>>>>> REPLACE markers' });
|
ops.push({ type: 'SEARCH', path: opPath, error: 'Malformed SEARCH block — expected <<<<<<< SEARCH / ======= / >>>>>>> REPLACE markers' });
|
||||||
} else {
|
} else {
|
||||||
@@ -358,8 +398,10 @@ function applyPatch(patchStr) {
|
|||||||
const listFilesTool = defineTool({
|
const listFilesTool = defineTool({
|
||||||
name: 'list_files',
|
name: 'list_files',
|
||||||
description:
|
description:
|
||||||
'List files and directories within the workspace. Use seek/limit for pagination. ' +
|
'List the immediate contents of a directory within the workspace (non-recursive by default). ' +
|
||||||
'Paths are relative to the workspace root. Returns truncation info when results are cut.',
|
'Hidden files and entries matching .gitignore patterns are excluded automatically. ' +
|
||||||
|
'Set recursive=true to walk the full subtree. ' +
|
||||||
|
'Use seek/limit for pagination. Paths are relative to the workspace root.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -369,6 +411,10 @@ const listFilesTool = defineTool({
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Directory path relative to workspace root. Use "." for root.',
|
description: 'Directory path relative to workspace root. Use "." for root.',
|
||||||
},
|
},
|
||||||
|
recursive: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'If true, walk the full subtree. Default: false (immediate children only).',
|
||||||
|
},
|
||||||
seek: {
|
seek: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Number of entries to skip (for pagination). Default: 0.',
|
description: 'Number of entries to skip (for pagination). Default: 0.',
|
||||||
@@ -379,7 +425,7 @@ const listFilesTool = defineTool({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
invoke: async ({ dirname, seek = 0, limit = 50 }) => {
|
invoke: async ({ dirname, recursive = false, seek = 0, limit = 50 }) => {
|
||||||
const absDir = resolveSafe(dirname);
|
const absDir = resolveSafe(dirname);
|
||||||
if (!fs.existsSync(absDir)) {
|
if (!fs.existsSync(absDir)) {
|
||||||
throw new ToolError(`Directory not found: "${dirname}"`);
|
throw new ToolError(`Directory not found: "${dirname}"`);
|
||||||
@@ -387,11 +433,11 @@ const listFilesTool = defineTool({
|
|||||||
if (!fs.statSync(absDir).isDirectory()) {
|
if (!fs.statSync(absDir).isDirectory()) {
|
||||||
throw new ToolError(`"${dirname}" is not a directory.`);
|
throw new ToolError(`"${dirname}" is not a directory.`);
|
||||||
}
|
}
|
||||||
const { files, total, truncated } = listFilesRecursive(absDir, seek, limit);
|
const { entries, total, truncated } = listDir(absDir, { seek, limit, recursive });
|
||||||
const result = { dirname, files, total, returned: files.length, seek };
|
const result = { dirname, recursive, entries, total, returned: entries.length, seek };
|
||||||
if (truncated > 0) {
|
if (truncated > 0) {
|
||||||
result.truncated = truncated;
|
result.truncated = truncated;
|
||||||
result.truncationNote = `${truncated} entries not shown. Use seek=${seek + files.length} to continue.`;
|
result.truncationNote = `${truncated} entries not shown. Use seek=${seek + entries.length} to continue.`;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
@@ -430,8 +476,8 @@ const readFileTool = defineTool({
|
|||||||
throw new ToolError(`"${filename}" is not a file.`);
|
throw new ToolError(`"${filename}" is not a file.`);
|
||||||
}
|
}
|
||||||
const allLines = fs.readFileSync(absPath, 'utf8').split('\n');
|
const allLines = fs.readFileSync(absPath, 'utf8').split('\n');
|
||||||
const total = allLines.length;
|
const total = allLines.length;
|
||||||
const slice = allLines.slice(seek, seek + limit);
|
const slice = allLines.slice(seek, seek + limit);
|
||||||
const truncated = total - (seek + slice.length);
|
const truncated = total - (seek + slice.length);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
@@ -449,6 +495,92 @@ const readFileTool = defineTool({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const askUserTool = defineTool({
|
||||||
|
name: 'ask_user',
|
||||||
|
description:
|
||||||
|
'Ask the user a question when you need clarification or a decision. ' +
|
||||||
|
'Provide a concise question and up to 3 labelled options (A, B, C). ' +
|
||||||
|
'The user can pick an option or type a free-form answer (D). ' +
|
||||||
|
'Use this instead of guessing whenever intent is ambiguous.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['question'],
|
||||||
|
properties: {
|
||||||
|
question: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The question to ask the user.',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Up to 3 short option labels for the user to choose from.',
|
||||||
|
items: { type: 'string' },
|
||||||
|
maxItems: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invoke: async ({ question, options = [] }) => {
|
||||||
|
const capped = options.slice(0, 3);
|
||||||
|
const letters = ['A', 'B', 'C'];
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.magenta.bold(' ❓ Fido asks:'));
|
||||||
|
console.log(chalk.white(' ' + question));
|
||||||
|
|
||||||
|
if (capped.length === 0) {
|
||||||
|
// No options — just free text
|
||||||
|
const answer = await input({
|
||||||
|
message: chalk.bold.magenta('Your answer'),
|
||||||
|
prefix: chalk.magenta('›'),
|
||||||
|
theme: {
|
||||||
|
prefix: chalk.magenta('›'),
|
||||||
|
style: { answer: chalk.white, message: chalk.bold.magenta },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
return { answer };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build select choices: one per option + free-text
|
||||||
|
const choices = [
|
||||||
|
...capped.map((opt, i) => ({
|
||||||
|
name: `${letters[i]}) ${opt}`,
|
||||||
|
value: letters[i],
|
||||||
|
short: letters[i],
|
||||||
|
})),
|
||||||
|
{ name: 'D) Type my own answer…', value: '__custom__', short: 'D' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selection = await select({
|
||||||
|
message: question,
|
||||||
|
choices,
|
||||||
|
prefix: chalk.magenta('›'),
|
||||||
|
theme: {
|
||||||
|
prefix: chalk.magenta('›'),
|
||||||
|
style: { message: chalk.bold.magenta },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let answer;
|
||||||
|
if (selection === '__custom__') {
|
||||||
|
answer = await input({
|
||||||
|
message: chalk.bold.magenta('Your answer'),
|
||||||
|
prefix: chalk.magenta('›'),
|
||||||
|
theme: {
|
||||||
|
prefix: chalk.magenta('›'),
|
||||||
|
style: { answer: chalk.white, message: chalk.bold.magenta },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const idx = letters.indexOf(selection);
|
||||||
|
answer = capped[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
return { answer };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const patchTool = defineTool({
|
const patchTool = defineTool({
|
||||||
name: 'patch',
|
name: 'patch',
|
||||||
description: `Apply file operations to the workspace. Supports CREATE, CREATE_DIR, SEARCH/REPLACE, DELETE, DELETE_DIR.
|
description: `Apply file operations to the workspace. Supports CREATE, CREATE_DIR, SEARCH/REPLACE, DELETE, DELETE_DIR.
|
||||||
@@ -559,7 +691,7 @@ function printToolError(name, error) {
|
|||||||
function printUsage(u) {
|
function printUsage(u) {
|
||||||
const parts = [`in=${u.input}`, `out=${u.output}`];
|
const parts = [`in=${u.input}`, `out=${u.output}`];
|
||||||
if (u.cached) parts.push(`cached=${u.cached}`);
|
if (u.cached) parts.push(`cached=${u.cached}`);
|
||||||
if (u.cost) parts.push(`cost=$${u.cost.toFixed(5)}`);
|
if (u.cost) parts.push(`cost=$${u.cost.toFixed(5)}`);
|
||||||
console.log(chalk.dim(` 📊 ${parts.join(' ')}`));
|
console.log(chalk.dim(` 📊 ${parts.join(' ')}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,11 +704,11 @@ async function main() {
|
|||||||
const provider = new OpenRouterProvider({ apiKey: API_KEY });
|
const provider = new OpenRouterProvider({ apiKey: API_KEY });
|
||||||
|
|
||||||
const orbiter = new LLMOrbiter(provider, {
|
const orbiter = new LLMOrbiter(provider, {
|
||||||
onStatus: (s) => printStatus(s),
|
onStatus: (s) => printStatus(s),
|
||||||
onToolCall: (c) => printToolCall(c.name, c.args),
|
onToolCall: (c) => printToolCall(c.name, c.args),
|
||||||
onToolResult: (r) => printToolResult(r.name, r.result),
|
onToolResult: (r) => printToolResult(r.name, r.result),
|
||||||
onToolError: (e) => printToolError(e.name, e.error),
|
onToolError: (e) => printToolError(e.name, e.error),
|
||||||
onUsage: (u) => printUsage(u),
|
onUsage: (u) => printUsage(u),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Conversation state: persisted across turns
|
// Conversation state: persisted across turns
|
||||||
@@ -635,7 +767,7 @@ async function main() {
|
|||||||
prompt: userInput,
|
prompt: userInput,
|
||||||
messages,
|
messages,
|
||||||
model: MODEL,
|
model: MODEL,
|
||||||
tools: [listFilesTool, readFileTool, patchTool],
|
tools: [listFilesTool, readFileTool, patchTool, askUserTool],
|
||||||
maxIterations: 20,
|
maxIterations: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user