@kuralle-agents/core 0.11.0 → 0.12.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/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/runtime/buildAgentToolSurface.js +4 -0
- package/dist/runtime/ctx.js +27 -2
- package/dist/runtime/resolveAgentWorkspace.d.ts +3 -0
- package/dist/runtime/resolveAgentWorkspace.js +6 -2
- package/dist/tools/effect/ToolExecutor.js +6 -1
- package/dist/tools/effect/defineTool.d.ts +1 -0
- package/dist/tools/effect/defineTool.js +1 -0
- package/dist/tools/fs/caps.d.ts +21 -0
- package/dist/tools/fs/caps.js +59 -0
- package/dist/tools/fs/createFsTool.js +68 -7
- package/dist/tools/fs/createShellTool.d.ts +7 -0
- package/dist/tools/fs/createShellTool.js +90 -0
- package/dist/types/agentConfig.d.ts +2 -0
- package/dist/types/effectTool.d.ts +2 -0
- package/dist/types/run-context.d.ts +1 -1
- package/dist/types/shell.d.ts +15 -0
- package/dist/types/shell.js +1 -0
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -94,6 +94,9 @@ export { defineTool } from './types/effectTool.js';
|
|
|
94
94
|
export { fsErrorCode } from './types/filesystem.js';
|
|
95
95
|
export { createFsTool } from './tools/fs/createFsTool.js';
|
|
96
96
|
export type { CreateFsToolOptions, GrepHit } from './tools/fs/createFsTool.js';
|
|
97
|
+
export { createShellTool } from './tools/fs/createShellTool.js';
|
|
98
|
+
export type { CreateShellToolOptions } from './tools/fs/createShellTool.js';
|
|
99
|
+
export type { Shell, ShellResult, ShellExecOptions } from './types/shell.js';
|
|
97
100
|
export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
|
|
98
101
|
export type { WiredAgentSkills, SkillWireAgent } from './skills/index.js';
|
|
99
102
|
export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, }
|
|
|
53
53
|
export { defineTool } from './types/effectTool.js';
|
|
54
54
|
export { fsErrorCode } from './types/filesystem.js';
|
|
55
55
|
export { createFsTool } from './tools/fs/createFsTool.js';
|
|
56
|
+
export { createShellTool } from './tools/fs/createShellTool.js';
|
|
56
57
|
export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
|
|
57
58
|
export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
|
58
59
|
export { parseConfirmation } from './flow/confirmParse.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFsTool } from '../tools/fs/createFsTool.js';
|
|
2
|
+
import { createShellTool } from '../tools/fs/createShellTool.js';
|
|
2
3
|
import { wireAgentSkills } from '../skills/wireAgentSkills.js';
|
|
3
4
|
import { buildKnowledgeTool, wireWorkingMemory } from './grounding/index.js';
|
|
4
5
|
import { resolveAgentWorkspace, } from './resolveAgentWorkspace.js';
|
|
@@ -17,6 +18,9 @@ export async function buildAgentToolSurface(agent, session, deps) {
|
|
|
17
18
|
});
|
|
18
19
|
executorTools.workspace = workspaceTool;
|
|
19
20
|
}
|
|
21
|
+
if (resolvedWorkspace?.shell) {
|
|
22
|
+
executorTools.bash = createShellTool({ shell: resolvedWorkspace.shell });
|
|
23
|
+
}
|
|
20
24
|
const wiredWorkingMemory = await wireWorkingMemory(agent, session, deps.defaultWorkingMemoryStore);
|
|
21
25
|
if (wiredWorkingMemory) {
|
|
22
26
|
executorTools.memory_block = wiredWorkingMemory.memoryBlockTool;
|
package/dist/runtime/ctx.js
CHANGED
|
@@ -140,7 +140,7 @@ function makeCtx(deps) {
|
|
|
140
140
|
}
|
|
141
141
|
const callsite = consumeCallsite();
|
|
142
142
|
const key = toolEffectKey(deps.runState.runId, callsite, name, args);
|
|
143
|
-
|
|
143
|
+
const executeTool = () => deps.toolExecutor.execute({
|
|
144
144
|
name,
|
|
145
145
|
args,
|
|
146
146
|
session: deps.session,
|
|
@@ -148,7 +148,32 @@ function makeCtx(deps) {
|
|
|
148
148
|
abortSignal: deps.bargeIn ?? deps.abortSignal,
|
|
149
149
|
def: options?.def,
|
|
150
150
|
toolCtx: options?.toolCtx,
|
|
151
|
-
})
|
|
151
|
+
});
|
|
152
|
+
if (def?.replay === false) {
|
|
153
|
+
const auditKey = `${key}:${steps.length}`;
|
|
154
|
+
try {
|
|
155
|
+
const result = await executeTool();
|
|
156
|
+
await appendLiveStep(auditKey, 'tool', name, result);
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
161
|
+
const startedAt = Date.now();
|
|
162
|
+
const record = {
|
|
163
|
+
index: steps.length,
|
|
164
|
+
key: auditKey,
|
|
165
|
+
kind: 'tool',
|
|
166
|
+
name,
|
|
167
|
+
error: { name: err.name, message: err.message },
|
|
168
|
+
startedAt,
|
|
169
|
+
finishedAt: startedAt,
|
|
170
|
+
};
|
|
171
|
+
await deps.runStore.appendStep(deps.runState.runId, record);
|
|
172
|
+
steps.push(record);
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return replayOrExecute(key, 'tool', name, executeTool);
|
|
152
177
|
},
|
|
153
178
|
approve: async (req) => {
|
|
154
179
|
return pauseEffect(APPROVAL_SIGNAL, { approval: req });
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { FileSystem } from '../types/filesystem.js';
|
|
2
|
+
import type { Shell } from '../types/shell.js';
|
|
2
3
|
export type AgentWorkspaceConfig = FileSystem | {
|
|
3
4
|
fs: FileSystem;
|
|
5
|
+
shell?: Shell;
|
|
4
6
|
readOnly?: boolean;
|
|
5
7
|
};
|
|
6
8
|
export interface ResolvedAgentWorkspace {
|
|
7
9
|
fs: FileSystem;
|
|
10
|
+
shell?: Shell;
|
|
8
11
|
readOnly: boolean;
|
|
9
12
|
}
|
|
10
13
|
export declare function resolveAgentWorkspace(workspace: AgentWorkspaceConfig | undefined): ResolvedAgentWorkspace | undefined;
|
|
@@ -3,7 +3,11 @@ export function resolveAgentWorkspace(workspace) {
|
|
|
3
3
|
return undefined;
|
|
4
4
|
}
|
|
5
5
|
if (typeof workspace === 'object' && workspace !== null && 'fs' in workspace) {
|
|
6
|
-
return {
|
|
6
|
+
return {
|
|
7
|
+
fs: workspace.fs,
|
|
8
|
+
shell: workspace.shell,
|
|
9
|
+
readOnly: workspace.readOnly !== false,
|
|
10
|
+
};
|
|
7
11
|
}
|
|
8
|
-
return { fs: workspace, readOnly: true };
|
|
12
|
+
return { fs: workspace, readOnly: true, shell: undefined };
|
|
9
13
|
}
|
|
@@ -114,7 +114,12 @@ export class CoreToolExecutor {
|
|
|
114
114
|
interimTimer.unref();
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
const
|
|
117
|
+
const executeCtx = toolCtx
|
|
118
|
+
? { ...toolCtx, abortSignal }
|
|
119
|
+
: abortSignal
|
|
120
|
+
? { abortSignal }
|
|
121
|
+
: undefined;
|
|
122
|
+
const executePromise = Promise.resolve(def.execute(sanitizedArgs, executeCtx)).then(async (result) => {
|
|
118
123
|
if (result && typeof result[Symbol.asyncIterator] === 'function') {
|
|
119
124
|
const chunks = [];
|
|
120
125
|
for await (const chunk of result) {
|
|
@@ -18,6 +18,7 @@ export declare function defineTool<S extends z.ZodTypeAny | StandardSchemaV1 | u
|
|
|
18
18
|
/** @deprecated Use `interimAfterMs`. */
|
|
19
19
|
estimatedDurationMs?: number;
|
|
20
20
|
timeoutMs?: number;
|
|
21
|
+
replay?: boolean;
|
|
21
22
|
execute: (args: InferToolInput<S>, ctx?: ToolContext) => Promise<R> | AsyncIterable<R>;
|
|
22
23
|
}): Tool<InferToolInput<S>, R>;
|
|
23
24
|
export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const MAX_READ_LINES = 2000;
|
|
2
|
+
export declare const MAX_READ_BYTES: number;
|
|
3
|
+
export declare const MAX_GREP_HITS = 200;
|
|
4
|
+
export declare const MAX_GREP_LINE_LEN = 500;
|
|
5
|
+
export declare const MAX_LIST_ENTRIES = 1000;
|
|
6
|
+
export declare const MAX_SHELL_OUTPUT_BYTES: number;
|
|
7
|
+
export declare function applyReadWindow(content: string, offset?: number, limit?: number): {
|
|
8
|
+
content: string;
|
|
9
|
+
truncated: boolean;
|
|
10
|
+
note?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function capGrepHits<T extends {
|
|
13
|
+
text: string;
|
|
14
|
+
}>(hits: T[]): {
|
|
15
|
+
hits: T[];
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function capList<T>(entries: T[]): {
|
|
19
|
+
entries: T[];
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const MAX_READ_LINES = 2000;
|
|
2
|
+
export const MAX_READ_BYTES = 50 * 1024;
|
|
3
|
+
export const MAX_GREP_HITS = 200;
|
|
4
|
+
export const MAX_GREP_LINE_LEN = 500;
|
|
5
|
+
export const MAX_LIST_ENTRIES = 1000;
|
|
6
|
+
export const MAX_SHELL_OUTPUT_BYTES = 50 * 1024;
|
|
7
|
+
export function applyReadWindow(content, offset, limit) {
|
|
8
|
+
const allLines = content.split('\n');
|
|
9
|
+
let lines = allLines;
|
|
10
|
+
let truncated = false;
|
|
11
|
+
const notes = [];
|
|
12
|
+
// `offset` is a 1-indexed start line. 0, 1, and undefined all mean "from the
|
|
13
|
+
// start" — models routinely fill an optional numeric arg with 0, and that must
|
|
14
|
+
// not silently empty the read.
|
|
15
|
+
if (offset !== undefined && offset > 1) {
|
|
16
|
+
lines = lines.slice(offset - 1);
|
|
17
|
+
truncated = true;
|
|
18
|
+
}
|
|
19
|
+
// `limit` caps the returned line count only when positive. 0 and undefined
|
|
20
|
+
// both mean "no explicit limit" (still bounded by MAX_READ_LINES below).
|
|
21
|
+
if (limit !== undefined && limit > 0 && lines.length > limit) {
|
|
22
|
+
lines = lines.slice(0, limit);
|
|
23
|
+
truncated = true;
|
|
24
|
+
}
|
|
25
|
+
if (lines.length > MAX_READ_LINES) {
|
|
26
|
+
lines = lines.slice(0, MAX_READ_LINES);
|
|
27
|
+
truncated = true;
|
|
28
|
+
notes.push(`truncated at ${MAX_READ_LINES} lines; use offset/limit`);
|
|
29
|
+
}
|
|
30
|
+
let result = lines.join('\n');
|
|
31
|
+
if (result.length > MAX_READ_BYTES) {
|
|
32
|
+
result = result.slice(0, MAX_READ_BYTES);
|
|
33
|
+
truncated = true;
|
|
34
|
+
if (!notes.some((n) => n.includes('bytes'))) {
|
|
35
|
+
notes.push(`truncated at ${MAX_READ_BYTES} bytes; use offset/limit`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!truncated) {
|
|
39
|
+
return { content: result, truncated: false };
|
|
40
|
+
}
|
|
41
|
+
const note = notes.length > 0 ? notes.join('; ') : 'truncated; use offset/limit';
|
|
42
|
+
return { content: result, truncated: true, note };
|
|
43
|
+
}
|
|
44
|
+
export function capGrepHits(hits) {
|
|
45
|
+
const truncated = hits.length > MAX_GREP_HITS;
|
|
46
|
+
const capped = hits.slice(0, MAX_GREP_HITS).map((hit) => {
|
|
47
|
+
if (hit.text.length <= MAX_GREP_LINE_LEN)
|
|
48
|
+
return hit;
|
|
49
|
+
return {
|
|
50
|
+
...hit,
|
|
51
|
+
text: `${hit.text.slice(0, MAX_GREP_LINE_LEN)}…`,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
return { hits: capped, truncated };
|
|
55
|
+
}
|
|
56
|
+
export function capList(entries) {
|
|
57
|
+
const truncated = entries.length > MAX_LIST_ENTRIES;
|
|
58
|
+
return { entries: entries.slice(0, MAX_LIST_ENTRIES), truncated };
|
|
59
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { defineTool } from '../effect/defineTool.js';
|
|
3
3
|
import { fsErrorCode } from '../../types/filesystem.js';
|
|
4
|
+
import { applyReadWindow, capGrepHits, capList } from './caps.js';
|
|
4
5
|
const DEFAULT_READ_ONLY = true;
|
|
5
6
|
const workspaceInput = z.object({
|
|
6
7
|
op: z.enum(['ls', 'cat', 'grep', 'find', 'read', 'write', 'edit']),
|
|
@@ -12,6 +13,9 @@ const workspaceInput = z.object({
|
|
|
12
13
|
content: z.string().optional(),
|
|
13
14
|
find: z.string().optional(),
|
|
14
15
|
replace: z.string().optional(),
|
|
16
|
+
offset: z.number().optional(),
|
|
17
|
+
limit: z.number().optional(),
|
|
18
|
+
replaceAll: z.boolean().optional(),
|
|
15
19
|
});
|
|
16
20
|
function normalizeFsPath(path, fallback = '/') {
|
|
17
21
|
if (!path || path.trim() === '')
|
|
@@ -32,6 +36,32 @@ function assertWritable(readOnly, path) {
|
|
|
32
36
|
if (readOnly)
|
|
33
37
|
throw eroFs(path);
|
|
34
38
|
}
|
|
39
|
+
const GREP_FLAG_ORDER = ['g', 'i', 'm', 's'];
|
|
40
|
+
function parseGrepFlags(flags) {
|
|
41
|
+
if (!flags)
|
|
42
|
+
return undefined;
|
|
43
|
+
const allowed = new Set(GREP_FLAG_ORDER);
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const ch of flags) {
|
|
47
|
+
if (allowed.has(ch) && !seen.has(ch)) {
|
|
48
|
+
seen.add(ch);
|
|
49
|
+
out.push(ch);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out.length > 0 ? out.join('') : undefined;
|
|
53
|
+
}
|
|
54
|
+
function countOccurrences(haystack, needle) {
|
|
55
|
+
if (needle.length === 0)
|
|
56
|
+
return 0;
|
|
57
|
+
let count = 0;
|
|
58
|
+
let pos = 0;
|
|
59
|
+
while ((pos = haystack.indexOf(needle, pos)) !== -1) {
|
|
60
|
+
count++;
|
|
61
|
+
pos += needle.length;
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
35
65
|
async function listFiles(fs, root) {
|
|
36
66
|
const normalized = root === '' ? '/' : root;
|
|
37
67
|
const out = [];
|
|
@@ -62,7 +92,7 @@ async function listFiles(fs, root) {
|
|
|
62
92
|
async function grepFiles(fs, pattern, root, flags) {
|
|
63
93
|
let re;
|
|
64
94
|
try {
|
|
65
|
-
re = new RegExp(pattern, flags
|
|
95
|
+
re = new RegExp(pattern, parseGrepFlags(flags));
|
|
66
96
|
}
|
|
67
97
|
catch {
|
|
68
98
|
throw new Error(`EINVAL: invalid grep pattern '${pattern}'`);
|
|
@@ -84,6 +114,7 @@ async function grepFiles(fs, pattern, root, flags) {
|
|
|
84
114
|
}
|
|
85
115
|
const lines = content.split('\n');
|
|
86
116
|
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
re.lastIndex = 0;
|
|
87
118
|
if (re.test(lines[i])) {
|
|
88
119
|
hits.push({ path: filePath, line: i + 1, text: lines[i] });
|
|
89
120
|
}
|
|
@@ -105,6 +136,7 @@ async function grepFiles(fs, pattern, root, flags) {
|
|
|
105
136
|
}
|
|
106
137
|
const lines = content.split('\n');
|
|
107
138
|
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
re.lastIndex = 0;
|
|
108
140
|
if (re.test(lines[i])) {
|
|
109
141
|
hits.push({ path: filePath, line: i + 1, text: lines[i] });
|
|
110
142
|
}
|
|
@@ -133,19 +165,30 @@ export function createFsTool(opts) {
|
|
|
133
165
|
switch (args.op) {
|
|
134
166
|
case 'ls': {
|
|
135
167
|
const path = normalizeFsPath(args.path);
|
|
136
|
-
const
|
|
168
|
+
const rawEntries = await fs.readdirWithFileTypes(path);
|
|
169
|
+
const { entries, truncated } = capList(rawEntries);
|
|
137
170
|
return {
|
|
138
171
|
op: args.op,
|
|
139
172
|
ok: true,
|
|
140
173
|
path,
|
|
141
174
|
entries,
|
|
175
|
+
...(truncated ? { truncated: true } : {}),
|
|
142
176
|
};
|
|
143
177
|
}
|
|
144
178
|
case 'cat':
|
|
145
179
|
case 'read': {
|
|
146
180
|
const path = requireField(args, 'path', args.op);
|
|
147
|
-
const
|
|
148
|
-
|
|
181
|
+
const raw = await fs.readFile(path);
|
|
182
|
+
const windowed = applyReadWindow(raw, args.offset, args.limit);
|
|
183
|
+
return {
|
|
184
|
+
op: args.op,
|
|
185
|
+
ok: true,
|
|
186
|
+
path,
|
|
187
|
+
content: windowed.content,
|
|
188
|
+
...(windowed.truncated
|
|
189
|
+
? { truncated: true, note: windowed.note }
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
149
192
|
}
|
|
150
193
|
case 'find': {
|
|
151
194
|
const root = normalizeFsPath(args.root);
|
|
@@ -155,24 +198,28 @@ export function createFsTool(opts) {
|
|
|
155
198
|
const normalizedRoot = root === '/' ? '/' : root.replace(/\/$/, '');
|
|
156
199
|
return p === normalizedRoot || p.startsWith(`${normalizedRoot}/`);
|
|
157
200
|
});
|
|
201
|
+
const { entries: cappedPaths, truncated } = capList(rooted);
|
|
158
202
|
return {
|
|
159
203
|
op: args.op,
|
|
160
204
|
ok: true,
|
|
161
205
|
root,
|
|
162
206
|
glob,
|
|
163
|
-
paths:
|
|
207
|
+
paths: cappedPaths,
|
|
208
|
+
...(truncated ? { truncated: true } : {}),
|
|
164
209
|
};
|
|
165
210
|
}
|
|
166
211
|
case 'grep': {
|
|
167
212
|
const pattern = requireField(args, 'pattern', args.op);
|
|
168
213
|
const path = normalizeFsPath(args.path);
|
|
169
|
-
const
|
|
214
|
+
const rawHits = await grepFiles(fs, pattern, path, args.flags);
|
|
215
|
+
const { hits, truncated } = capGrepHits(rawHits);
|
|
170
216
|
return {
|
|
171
217
|
op: args.op,
|
|
172
218
|
ok: true,
|
|
173
219
|
pattern,
|
|
174
220
|
path,
|
|
175
221
|
hits,
|
|
222
|
+
...(truncated ? { truncated: true } : {}),
|
|
176
223
|
};
|
|
177
224
|
}
|
|
178
225
|
case 'write': {
|
|
@@ -188,9 +235,23 @@ export function createFsTool(opts) {
|
|
|
188
235
|
const replace = requireField(args, 'replace', args.op);
|
|
189
236
|
assertWritable(readOnly, path);
|
|
190
237
|
const current = await fs.readFile(path);
|
|
191
|
-
|
|
238
|
+
const occurrences = countOccurrences(current, find);
|
|
239
|
+
if (occurrences === 0) {
|
|
192
240
|
throw new Error(`ENOENT: find string not found in file, edit '${path}'`);
|
|
193
241
|
}
|
|
242
|
+
if (args.replaceAll) {
|
|
243
|
+
const next = current.replaceAll(find, replace);
|
|
244
|
+
await fs.writeFile(path, next);
|
|
245
|
+
return {
|
|
246
|
+
op: args.op,
|
|
247
|
+
ok: true,
|
|
248
|
+
path,
|
|
249
|
+
replacements: occurrences,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (occurrences > 1) {
|
|
253
|
+
throw new Error(`EAMBIG: ${occurrences} occurrences of find string in '${path}'; add surrounding context or use replaceAll`);
|
|
254
|
+
}
|
|
194
255
|
const next = current.replace(find, replace);
|
|
195
256
|
await fs.writeFile(path, next);
|
|
196
257
|
return { op: args.op, ok: true, path };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AnyTool } from '../../types/effectTool.js';
|
|
2
|
+
import type { Shell } from '../../types/shell.js';
|
|
3
|
+
export interface CreateShellToolOptions {
|
|
4
|
+
shell: Shell;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function createShellTool(opts: CreateShellToolOptions): AnyTool;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../effect/defineTool.js';
|
|
3
|
+
import { MAX_SHELL_OUTPUT_BYTES } from './caps.js';
|
|
4
|
+
const bashInput = z.object({
|
|
5
|
+
command: z.string(),
|
|
6
|
+
timeout: z.number().optional(),
|
|
7
|
+
});
|
|
8
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
9
|
+
function isTimeoutError(error) {
|
|
10
|
+
if (!(error instanceof Error))
|
|
11
|
+
return false;
|
|
12
|
+
const msg = error.message.toLowerCase();
|
|
13
|
+
return msg.includes('timeout') || msg.includes('timed out');
|
|
14
|
+
}
|
|
15
|
+
function capShellOutput(stdout, stderr) {
|
|
16
|
+
const combined = stdout.length + stderr.length;
|
|
17
|
+
if (combined <= MAX_SHELL_OUTPUT_BYTES) {
|
|
18
|
+
return { stdout, stderr };
|
|
19
|
+
}
|
|
20
|
+
const note = `\n[kuralle] output truncated at ${MAX_SHELL_OUTPUT_BYTES} bytes`;
|
|
21
|
+
const budget = MAX_SHELL_OUTPUT_BYTES - note.length;
|
|
22
|
+
if (budget <= 0) {
|
|
23
|
+
return { stdout: '', stderr: note.trim() };
|
|
24
|
+
}
|
|
25
|
+
if (stdout.length >= budget) {
|
|
26
|
+
return { stdout: stdout.slice(0, budget), stderr: note.trim() };
|
|
27
|
+
}
|
|
28
|
+
const stderrBudget = budget - stdout.length;
|
|
29
|
+
return {
|
|
30
|
+
stdout,
|
|
31
|
+
stderr: `${stderr.slice(0, stderrBudget)}${note}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function createShellTool(opts) {
|
|
35
|
+
const defaultTimeoutMs = opts.timeoutMs;
|
|
36
|
+
return defineTool({
|
|
37
|
+
name: 'bash',
|
|
38
|
+
description: 'Run a shell command in the agent workspace. Returns stdout, stderr, exitCode. Output is truncated when large.',
|
|
39
|
+
replay: false,
|
|
40
|
+
input: bashInput,
|
|
41
|
+
execute: async (args, ctx) => {
|
|
42
|
+
const timeoutMs = args.timeout !== undefined ? args.timeout * 1000 : defaultTimeoutMs;
|
|
43
|
+
if (ctx?.abortSignal?.aborted) {
|
|
44
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
45
|
+
}
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await opts.shell.exec(args.command, {
|
|
49
|
+
timeoutMs,
|
|
50
|
+
signal: ctx?.abortSignal,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (ctx?.abortSignal?.aborted) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (isTimeoutError(error)) {
|
|
58
|
+
const seconds = timeoutMs !== undefined ? Math.round(timeoutMs / 1000) : args.timeout ?? 0;
|
|
59
|
+
return {
|
|
60
|
+
op: 'bash',
|
|
61
|
+
ok: false,
|
|
62
|
+
stdout: '',
|
|
63
|
+
stderr: `[kuralle] command timed out after ${seconds}s`,
|
|
64
|
+
exitCode: TIMEOUT_EXIT_CODE,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (result.exitCode === TIMEOUT_EXIT_CODE) {
|
|
70
|
+
const seconds = timeoutMs !== undefined ? Math.round(timeoutMs / 1000) : args.timeout ?? 0;
|
|
71
|
+
return {
|
|
72
|
+
op: 'bash',
|
|
73
|
+
ok: false,
|
|
74
|
+
stdout: result.stdout,
|
|
75
|
+
stderr: result.stderr ||
|
|
76
|
+
`[kuralle] command timed out after ${seconds}s`,
|
|
77
|
+
exitCode: TIMEOUT_EXIT_CODE,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const capped = capShellOutput(result.stdout, result.stderr);
|
|
81
|
+
return {
|
|
82
|
+
op: 'bash',
|
|
83
|
+
ok: result.exitCode === 0,
|
|
84
|
+
stdout: capped.stdout,
|
|
85
|
+
stderr: capped.stderr,
|
|
86
|
+
exitCode: result.exitCode,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -8,9 +8,11 @@ import type { RefinementCapability } from '../capabilities/RefinementCapability.
|
|
|
8
8
|
import type { ValidationCapability } from '../capabilities/ValidationCapability.js';
|
|
9
9
|
import type { AnyTool } from './effectTool.js';
|
|
10
10
|
import type { FileSystem } from './filesystem.js';
|
|
11
|
+
import type { Shell } from './shell.js';
|
|
11
12
|
import type { SkillSource } from './skills.js';
|
|
12
13
|
export type AgentWorkspaceConfig = FileSystem | {
|
|
13
14
|
fs: FileSystem;
|
|
15
|
+
shell?: Shell;
|
|
14
16
|
readOnly?: boolean;
|
|
15
17
|
};
|
|
16
18
|
export type Instructions = string | AgentPrompt | ((ctx: {
|
|
@@ -10,6 +10,8 @@ export interface Tool<TInput = unknown, TOutput = unknown> {
|
|
|
10
10
|
interim?: string;
|
|
11
11
|
interimAfterMs?: number;
|
|
12
12
|
timeoutMs?: number;
|
|
13
|
+
/** When false, the durable journal always re-executes this tool instead of returning a cached step result — for observation/mutation tools (fs, shell) whose result must be fresh. Default true. */
|
|
14
|
+
replay?: boolean;
|
|
13
15
|
execute: (args: TInput, ctx?: ToolContext) => Promise<TOutput> | AsyncIterable<TOutput>;
|
|
14
16
|
}
|
|
15
17
|
export type AnyTool = Tool<any, any>;
|
|
@@ -118,5 +118,5 @@ export interface RunContext {
|
|
|
118
118
|
resetCallsites(): void;
|
|
119
119
|
}
|
|
120
120
|
export type ActionContext = Pick<RunContext, 'tool' | 'approve' | 'signal' | 'now' | 'uuid' | 'emit' | 'fs'>;
|
|
121
|
-
export type ToolContext = Pick<RunContext, 'session' | 'runState' | 'tool' | 'now' | 'uuid' | 'emit' | 'fs'>;
|
|
121
|
+
export type ToolContext = Pick<RunContext, 'session' | 'runState' | 'tool' | 'now' | 'uuid' | 'emit' | 'fs' | 'abortSignal'>;
|
|
122
122
|
export type { ModelMessage };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ShellResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ShellExecOptions {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
export interface Shell {
|
|
13
|
+
exec(command: string, options?: ShellExecOptions): Promise<ShellResult>;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-core"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.12.0",
|
|
10
10
|
"description": "A framework for structured conversational AI agents",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"typescript": "^5.3.0",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
105
|
"zod": "^4.0.0",
|
|
106
|
-
"@kuralle-agents/realtime-audio": "0.
|
|
106
|
+
"@kuralle-agents/realtime-audio": "0.12.0"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"chrono-node": "^2.6.0"
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"scripts": {
|
|
112
112
|
"prebuild": "rm -rf dist",
|
|
113
113
|
"build": "tsc -p tsconfig.json",
|
|
114
|
+
"typecheck:audit": "tsc --noEmit -p tsconfig.audit.json",
|
|
114
115
|
"typecheck:examples": "tsc --noEmit -p tsconfig.examples.json",
|
|
115
116
|
"clean": "rm -rf dist",
|
|
116
117
|
"test": "bun test ./test && vitest run --config vitest.config.ts",
|