@mikode13/harness 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/domain/agent.d.ts +7 -0
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +20 -3
- package/dist/engines/domain/model/llmAgent.js +156 -30
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/llm/domain/conversation.d.ts +4 -3
- package/dist/llm/domain/conversation.js +5 -4
- package/dist/llm/domain/conversation.js.map +1 -1
- package/dist/llm/domain/llm.d.ts +5 -1
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/message.d.ts +30 -4
- package/dist/llm/domain/message.js.map +1 -1
- package/dist/llm/domain/tool.d.ts +10 -0
- package/dist/llm/domain/tool.js +2 -0
- package/dist/llm/domain/tool.js.map +1 -0
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +5 -1
- package/dist/llm/infrastructure/claudeLLMClient.js +78 -19
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/openAILLMClient.d.ts +5 -1
- package/dist/llm/infrastructure/openAILLMClient.js +88 -12
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/tools/domain/tool.d.ts +9 -0
- package/dist/tools/domain/tool.js +2 -0
- package/dist/tools/domain/tool.js.map +1 -0
- package/dist/tools/domain/workspace.d.ts +48 -0
- package/dist/tools/domain/workspace.js +2 -0
- package/dist/tools/domain/workspace.js.map +1 -0
- package/dist/tools/infrastructure/boundedWorkspace.d.ts +66 -0
- package/dist/tools/infrastructure/boundedWorkspace.js +160 -0
- package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/createWorkspace.d.ts +14 -0
- package/dist/tools/infrastructure/createWorkspace.js +54 -0
- package/dist/tools/infrastructure/createWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/defineTool.d.ts +15 -0
- package/dist/tools/infrastructure/defineTool.js +87 -0
- package/dist/tools/infrastructure/defineTool.js.map +1 -0
- package/dist/tools/infrastructure/gitWorkspace.d.ts +13 -0
- package/dist/tools/infrastructure/gitWorkspace.js +61 -0
- package/dist/tools/infrastructure/gitWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +20 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.js +85 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/runProcess.d.ts +23 -0
- package/dist/tools/infrastructure/runProcess.js +91 -0
- package/dist/tools/infrastructure/runProcess.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +4 -0
- package/dist/tools/infrastructure/workspaceTools.js +110 -0
- package/dist/tools/infrastructure/workspaceTools.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
// What a program may print, or one line of it may hold, before it is stopped. Beyond this it
|
|
4
|
+
// has printed far more than anyone can read.
|
|
5
|
+
const maxOutputBytes = 64 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Runs a program with its arguments as an array, never through a shell, so nothing a model
|
|
8
|
+
* writes can become a command. Standard input is closed: ripgrep given no path searches its
|
|
9
|
+
* standard input whenever that is a pipe. A cancelled signal kills the process and rejects
|
|
10
|
+
* with the signal's `AbortError`.
|
|
11
|
+
*
|
|
12
|
+
* With `onLine`, each line of the output is handed over as it arrives and nothing is kept, so
|
|
13
|
+
* the caller decides what to hold; only a single line is then bound by the size limit. An
|
|
14
|
+
* error thrown by `onLine` stops the program and rejects with that error.
|
|
15
|
+
*/
|
|
16
|
+
export function runProcess(command, args, { cwd, signal, onLine }) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn(command, args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
19
|
+
const decoder = new StringDecoder('utf8');
|
|
20
|
+
const stdout = [];
|
|
21
|
+
const stderr = [];
|
|
22
|
+
let pending = '';
|
|
23
|
+
let size = 0;
|
|
24
|
+
let failure;
|
|
25
|
+
const stop = (error) => {
|
|
26
|
+
failure ??= error;
|
|
27
|
+
child.kill();
|
|
28
|
+
};
|
|
29
|
+
const deliver = (text) => {
|
|
30
|
+
if (!onLine) {
|
|
31
|
+
stdout.push(text);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const lines = (pending + text).split('\n');
|
|
35
|
+
pending = lines.pop() ?? '';
|
|
36
|
+
for (const line of lines)
|
|
37
|
+
onLine(line);
|
|
38
|
+
};
|
|
39
|
+
child.stdout.on('data', (chunk) => {
|
|
40
|
+
if (failure) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
size = onLine ? pending.length + chunk.length : size + chunk.length;
|
|
44
|
+
if (size > maxOutputBytes) {
|
|
45
|
+
stop(new Error(`The program printed more than ${String(maxOutputBytes / 1024 / 1024)} MB`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
deliver(decoder.write(chunk));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
stop(error instanceof Error ? error : new Error(String(error)));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
56
|
+
// A missing program, or the cancellation, which arrives as an `AbortError`.
|
|
57
|
+
child.on('error', reject);
|
|
58
|
+
child.on('close', (code, killedBy) => {
|
|
59
|
+
// Stopped from outside, such as by the out-of-memory killer: what it printed is not the
|
|
60
|
+
// whole answer, and an exit code would pass it off as one. Its own stops set `failure`.
|
|
61
|
+
if (code === null && !failure) {
|
|
62
|
+
failure = new Error(`${command} was stopped by ${killedBy ?? 'a signal'} before it finished`);
|
|
63
|
+
}
|
|
64
|
+
if (!failure) {
|
|
65
|
+
try {
|
|
66
|
+
deliver(decoder.end());
|
|
67
|
+
if (onLine && pending !== '')
|
|
68
|
+
onLine(pending);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (failure) {
|
|
75
|
+
reject(failure);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
resolve({
|
|
79
|
+
stdout: stdout.join(''),
|
|
80
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
81
|
+
// Not null here: a process stopped by a signal was rejected above.
|
|
82
|
+
exitCode: code ?? 1,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** The first line of a program's error output, which is the part a model can act on. */
|
|
88
|
+
export function firstLine(text) {
|
|
89
|
+
return text.trim().split('\n')[0] ?? '';
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=runProcess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runProcess.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/runProcess.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { StringDecoder } from 'node:string_decoder';\n\n// What a program may print, or one line of it may hold, before it is stopped. Beyond this it\n// has printed far more than anyone can read.\nconst maxOutputBytes = 64 * 1024 * 1024;\n\nexport interface ProcessResult {\n\t/** Empty when the output went to `onLine` instead. */\n\tstdout: string;\n\tstderr: string;\n\texitCode: number;\n}\n\n/**\n * Runs a program with its arguments as an array, never through a shell, so nothing a model\n * writes can become a command. Standard input is closed: ripgrep given no path searches its\n * standard input whenever that is a pipe. A cancelled signal kills the process and rejects\n * with the signal's `AbortError`.\n *\n * With `onLine`, each line of the output is handed over as it arrives and nothing is kept, so\n * the caller decides what to hold; only a single line is then bound by the size limit. An\n * error thrown by `onLine` stops the program and rejects with that error.\n */\nexport function runProcess(\n\tcommand: string,\n\targs: string[],\n\t{ cwd, signal, onLine }: { cwd: string; signal: AbortSignal; onLine?: (line: string) => void },\n): Promise<ProcessResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(command, args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });\n\t\tconst decoder = new StringDecoder('utf8');\n\t\tconst stdout: string[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet pending = '';\n\t\tlet size = 0;\n\t\tlet failure: Error | undefined;\n\n\t\tconst stop = (error: Error) => {\n\t\t\tfailure ??= error;\n\t\t\tchild.kill();\n\t\t};\n\n\t\tconst deliver = (text: string) => {\n\t\t\tif (!onLine) {\n\t\t\t\tstdout.push(text);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst lines = (pending + text).split('\\n');\n\t\t\tpending = lines.pop() ?? '';\n\t\t\tfor (const line of lines) onLine(line);\n\t\t};\n\n\t\tchild.stdout.on('data', (chunk: Buffer) => {\n\t\t\tif (failure) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsize = onLine ? pending.length + chunk.length : size + chunk.length;\n\t\t\tif (size > maxOutputBytes) {\n\t\t\t\tstop(new Error(`The program printed more than ${String(maxOutputBytes / 1024 / 1024)} MB`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tdeliver(decoder.write(chunk));\n\t\t\t} catch (error) {\n\t\t\t\tstop(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t\tchild.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));\n\n\t\t// A missing program, or the cancellation, which arrives as an `AbortError`.\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code, killedBy) => {\n\t\t\t// Stopped from outside, such as by the out-of-memory killer: what it printed is not the\n\t\t\t// whole answer, and an exit code would pass it off as one. Its own stops set `failure`.\n\t\t\tif (code === null && !failure) {\n\t\t\t\tfailure = new Error(\n\t\t\t\t\t`${command} was stopped by ${killedBy ?? 'a signal'} before it finished`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!failure) {\n\t\t\t\ttry {\n\t\t\t\t\tdeliver(decoder.end());\n\t\t\t\t\tif (onLine && pending !== '') onLine(pending);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailure = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failure) {\n\t\t\t\treject(failure);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout: stdout.join(''),\n\t\t\t\tstderr: Buffer.concat(stderr).toString('utf8'),\n\t\t\t\t// Not null here: a process stopped by a signal was rejected above.\n\t\t\t\texitCode: code ?? 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** The first line of a program's error output, which is the part a model can act on. */\nexport function firstLine(text: string): string {\n\treturn text.trim().split('\\n')[0] ?? '';\n}\n"]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from "./defineTool.js";
|
|
3
|
+
// What one call may return. The model can ask for fewer lines, never for more.
|
|
4
|
+
const maxFiles = 200;
|
|
5
|
+
const maxMatches = 100;
|
|
6
|
+
const maxLines = 200;
|
|
7
|
+
// One minified line can be larger than everything else the model has read.
|
|
8
|
+
const maxLineLength = 300;
|
|
9
|
+
// Strict mode has no optional fields: an argument the model may leave out is nullable.
|
|
10
|
+
const scope = {
|
|
11
|
+
path: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),
|
|
12
|
+
glob: z.string().nullable().describe('Only paths matching this glob, such as "**/*.ts".'),
|
|
13
|
+
};
|
|
14
|
+
/** One line as the model reads it: a minified bundle on a single line would flood its context. */
|
|
15
|
+
function cutLine(text) {
|
|
16
|
+
return text.length > maxLineLength ? `${text.slice(0, maxLineLength)}…` : text;
|
|
17
|
+
}
|
|
18
|
+
function assertPositiveInteger(name, value) {
|
|
19
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
20
|
+
throw new Error(`${name} must be a whole number of at least 1; got ${String(value)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The three read-only tools an agent explores a repository with. */
|
|
24
|
+
export function createWorkspaceTools(workspace) {
|
|
25
|
+
return [
|
|
26
|
+
defineTool({
|
|
27
|
+
name: 'listFiles',
|
|
28
|
+
description: 'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',
|
|
29
|
+
input: z.object(scope),
|
|
30
|
+
execute: async ({ path, glob }, signal) => {
|
|
31
|
+
// The port takes "no scope" as a missing field.
|
|
32
|
+
const { files, total, truncated } = await workspace.listFiles({ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles }, signal);
|
|
33
|
+
// Nothing found is an answer too, not an empty string.
|
|
34
|
+
if (files.length === 0) {
|
|
35
|
+
return 'No files.';
|
|
36
|
+
}
|
|
37
|
+
const notice = truncated
|
|
38
|
+
? `\n[Showing ${String(files.length)} of ${String(total)} files. Narrow the path or glob to see the rest.]`
|
|
39
|
+
: '';
|
|
40
|
+
return files.join('\n') + notice;
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
defineTool({
|
|
44
|
+
name: 'searchText',
|
|
45
|
+
description: 'Searches the repository for a regular expression and returns each matching line as path:line: text.',
|
|
46
|
+
input: z.object({
|
|
47
|
+
pattern: z.string().describe('A regular expression.'),
|
|
48
|
+
ignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),
|
|
49
|
+
...scope,
|
|
50
|
+
}),
|
|
51
|
+
execute: async ({ path, glob, ignoreCase, pattern }, signal) => {
|
|
52
|
+
const { matches, total, truncated } = await workspace.searchText({
|
|
53
|
+
path: path ?? undefined,
|
|
54
|
+
glob: glob ?? undefined,
|
|
55
|
+
ignoreCase: ignoreCase ?? false,
|
|
56
|
+
pattern,
|
|
57
|
+
limit: maxMatches,
|
|
58
|
+
}, signal);
|
|
59
|
+
if (matches.length === 0) {
|
|
60
|
+
return 'No matches.';
|
|
61
|
+
}
|
|
62
|
+
const results = matches.map(match => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`);
|
|
63
|
+
const notice = truncated
|
|
64
|
+
? `\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`
|
|
65
|
+
: '';
|
|
66
|
+
return results.join('\n') + notice;
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
defineTool({
|
|
70
|
+
name: 'readFile',
|
|
71
|
+
description: 'Reads lines of a file, each prefixed with its number.',
|
|
72
|
+
input: z.object({
|
|
73
|
+
path: z.string().describe('The file, relative to the root.'),
|
|
74
|
+
fromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),
|
|
75
|
+
lineCount: z
|
|
76
|
+
.number()
|
|
77
|
+
.nullable()
|
|
78
|
+
.describe(`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`),
|
|
79
|
+
}),
|
|
80
|
+
execute: async ({ fromLine, lineCount, path }, signal) => {
|
|
81
|
+
const from = fromLine ?? 1;
|
|
82
|
+
const requested = lineCount ?? maxLines;
|
|
83
|
+
// Checked here, before reading, because strict mode allows the schema no minimum.
|
|
84
|
+
assertPositiveInteger('fromLine', from);
|
|
85
|
+
assertPositiveInteger('lineCount', requested);
|
|
86
|
+
const { lines, totalLines, truncated } = await workspace.readFile({ path, fromLine: from, lineCount: Math.min(requested, maxLines) }, signal);
|
|
87
|
+
// The file's length tells the model where its range went wrong.
|
|
88
|
+
if (lines.length === 0) {
|
|
89
|
+
return `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;
|
|
90
|
+
}
|
|
91
|
+
const numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(text)}`);
|
|
92
|
+
const cut = lines.filter(text => text.length > maxLineLength).length;
|
|
93
|
+
const notices = [
|
|
94
|
+
...(cut > 0
|
|
95
|
+
? [
|
|
96
|
+
`[${String(cut)} lines were longer than ${String(maxLineLength)} characters and were cut.]`,
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
99
|
+
...(truncated
|
|
100
|
+
? [
|
|
101
|
+
`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,
|
|
102
|
+
]
|
|
103
|
+
: []),
|
|
104
|
+
];
|
|
105
|
+
return [...numbered, ...notices].join('\n');
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=workspaceTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/workspaceTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Tool } from '../domain/tool.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport { defineTool } from './defineTool.ts';\n\n// What one call may return. The model can ask for fewer lines, never for more.\nconst maxFiles = 200;\nconst maxMatches = 100;\nconst maxLines = 200;\n// One minified line can be larger than everything else the model has read.\nconst maxLineLength = 300;\n\n// Strict mode has no optional fields: an argument the model may leave out is nullable.\nconst scope = {\n\tpath: z.string().nullable().describe('A folder or file to stay inside, relative to the root.'),\n\tglob: z.string().nullable().describe('Only paths matching this glob, such as \"**/*.ts\".'),\n};\n\n/** One line as the model reads it: a minified bundle on a single line would flood its context. */\nfunction cutLine(text: string): string {\n\treturn text.length > maxLineLength ? `${text.slice(0, maxLineLength)}…` : text;\n}\n\nfunction assertPositiveInteger(name: string, value: number): void {\n\tif (!Number.isInteger(value) || value < 1) {\n\t\tthrow new Error(`${name} must be a whole number of at least 1; got ${String(value)}`);\n\t}\n}\n\n/** The three read-only tools an agent explores a repository with. */\nexport function createWorkspaceTools(workspace: Workspace): Tool[] {\n\treturn [\n\t\tdefineTool({\n\t\t\tname: 'listFiles',\n\t\t\tdescription:\n\t\t\t\t'Lists the files of the repository, sorted. Ignored files such as dependencies are left out.',\n\t\t\tinput: z.object(scope),\n\t\t\texecute: async ({ path, glob }, signal) => {\n\t\t\t\t// The port takes \"no scope\" as a missing field.\n\t\t\t\tconst { files, total, truncated } = await workspace.listFiles(\n\t\t\t\t\t{ path: path ?? undefined, glob: glob ?? undefined, limit: maxFiles },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// Nothing found is an answer too, not an empty string.\n\t\t\t\tif (files.length === 0) {\n\t\t\t\t\treturn 'No files.';\n\t\t\t\t}\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(files.length)} of ${String(total)} files. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn files.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'searchText',\n\t\t\tdescription:\n\t\t\t\t'Searches the repository for a regular expression and returns each matching line as path:line: text.',\n\t\t\tinput: z.object({\n\t\t\t\tpattern: z.string().describe('A regular expression.'),\n\t\t\t\tignoreCase: z.boolean().nullable().describe('Match regardless of case. Defaults to false.'),\n\t\t\t\t...scope,\n\t\t\t}),\n\t\t\texecute: async ({ path, glob, ignoreCase, pattern }, signal) => {\n\t\t\t\tconst { matches, total, truncated } = await workspace.searchText(\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: path ?? undefined,\n\t\t\t\t\t\tglob: glob ?? undefined,\n\t\t\t\t\t\tignoreCase: ignoreCase ?? false,\n\t\t\t\t\t\tpattern,\n\t\t\t\t\t\tlimit: maxMatches,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\tif (matches.length === 0) {\n\t\t\t\t\treturn 'No matches.';\n\t\t\t\t}\n\n\t\t\t\tconst results = matches.map(\n\t\t\t\t\tmatch => `${match.path}:${String(match.line)}: ${cutLine(match.text)}`,\n\t\t\t\t);\n\n\t\t\t\tconst notice = truncated\n\t\t\t\t\t? `\\n[Showing ${String(results.length)} of ${String(total)} matches. Narrow the path or glob to see the rest.]`\n\t\t\t\t\t: '';\n\t\t\t\treturn results.join('\\n') + notice;\n\t\t\t},\n\t\t}),\n\n\t\tdefineTool({\n\t\t\tname: 'readFile',\n\t\t\tdescription: 'Reads lines of a file, each prefixed with its number.',\n\t\t\tinput: z.object({\n\t\t\t\tpath: z.string().describe('The file, relative to the root.'),\n\t\t\t\tfromLine: z.number().nullable().describe('The first line to read, from 1. Defaults to 1.'),\n\t\t\t\tlineCount: z\n\t\t\t\t\t.number()\n\t\t\t\t\t.nullable()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t`How many lines to read, at most ${String(maxLines)}. Defaults to ${String(maxLines)}.`,\n\t\t\t\t\t),\n\t\t\t}),\n\t\t\texecute: async ({ fromLine, lineCount, path }, signal) => {\n\t\t\t\tconst from = fromLine ?? 1;\n\t\t\t\tconst requested = lineCount ?? maxLines;\n\t\t\t\t// Checked here, before reading, because strict mode allows the schema no minimum.\n\t\t\t\tassertPositiveInteger('fromLine', from);\n\t\t\t\tassertPositiveInteger('lineCount', requested);\n\n\t\t\t\tconst { lines, totalLines, truncated } = await workspace.readFile(\n\t\t\t\t\t{ path, fromLine: from, lineCount: Math.min(requested, maxLines) },\n\t\t\t\t\tsignal,\n\t\t\t\t);\n\n\t\t\t\t// The file's length tells the model where its range went wrong.\n\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\treturn `No lines from ${String(from)}: the file has ${String(totalLines)} lines.`;\n\t\t\t\t}\n\n\t\t\t\tconst numbered = lines.map((text, index) => `${String(from + index)}: ${cutLine(text)}`);\n\t\t\t\tconst cut = lines.filter(text => text.length > maxLineLength).length;\n\t\t\t\tconst notices = [\n\t\t\t\t\t...(cut > 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[${String(cut)} lines were longer than ${String(maxLineLength)} characters and were cut.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t...(truncated\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`[The file has ${String(totalLines)} lines. Read from line ${String(from + lines.length)} to continue.]`,\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t];\n\t\t\t\treturn [...numbered, ...notices].join('\\n');\n\t\t\t},\n\t\t}),\n\t];\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikode13/harness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Test/agent harness utility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
|
64
64
|
"@anthropic-ai/sdk": "^0.115.0",
|
|
65
65
|
"@openai/codex-sdk": "^0.154.0",
|
|
66
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
66
67
|
"openai": "^7.23.0",
|
|
67
68
|
"zod": "^4.4.3"
|
|
68
69
|
}
|