Repository navigation
Expand file tree
/
Copy pathscripted.ts
More file actions
225 lines (202 loc) · 6.07 KB
/
Copy pathscripted.ts
File metadata and controls
225 lines (202 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Decision: `ScriptedTool` ships from the `@everruns/bashkit/scripted` entry,
// mirroring the Rust `bashkit-scripted-tool` crate; the root entry keeps the
// interpreter and `BashTool`. The root still re-exports it as a deprecated
// alias during the transition.
/**
* Multi-tool orchestration for Bashkit (`ScriptedTool`).
*
* Register JS callbacks as bash builtins; one bash script composes them with
* pipes, loops, and `jq` in a logic-only shell.
*
* @packageDocumentation
*/
import type {
ExecResult,
ScriptedTool as NativeScriptedToolType,
} from "./index.cjs";
import {
BashError,
DEFAULT_MAX_INPUT_BYTES,
inputTooLargeExecResult,
native,
queueAsyncExecute,
validateJsonNestingDepth,
} from "./internal.js";
export type { ExecResult };
const NativeScriptedTool: typeof NativeScriptedToolType = native.ScriptedTool;
/**
* Options for creating a ScriptedTool instance.
*/
export interface ScriptedToolOptions {
name: string;
shortDescription?: string;
maxCommands?: number;
maxLoopIterations?: number;
}
/**
* Callback type for ScriptedTool tool commands.
*
* Receives parsed `--key value` flags as `params` and optional piped input as `stdin`.
* Must return a string.
*/
export type ToolCallback = (
params: Record<string, unknown>,
stdin: string | null,
) => string;
/**
* Compose JS callbacks as bash builtins for multi-tool orchestration.
*
* Each registered tool becomes a bash builtin command. An LLM (or user) writes
* a single bash script that pipes, loops, and branches across all tools.
*
* @example
* ```typescript
* import { ScriptedTool } from '@everruns/bashkit/scripted';
*
* const tool = new ScriptedTool({ name: "api" });
* tool.addTool("greet", "Greet user",
* (params) => `hello ${params.name ?? "world"}\n`
* );
* const result = tool.executeSync("greet --name Alice");
* console.log(result.stdout); // hello Alice\n
* ```
*/
export class ScriptedTool {
private native: NativeScriptedToolType;
// Keep strong JS refs while native TSFN callbacks are weak.
private callbackRefs: Array<(requestJson: string) => string> = [];
constructor(options: ScriptedToolOptions) {
this.native = new NativeScriptedTool({
name: options.name,
shortDescription: options.shortDescription,
maxCommands: options.maxCommands,
maxLoopIterations: options.maxLoopIterations,
});
}
/**
* Register a tool command.
*
* @param name - Command name (becomes a bash builtin)
* @param description - Human-readable description
* @param callback - JS function `(params, stdin) => string`
* @param schema - Optional JSON Schema for input parameters
*/
addTool(
name: string,
description: string,
callback: ToolCallback,
schema?: Record<string, unknown>,
): void {
if (schema) {
validateJsonNestingDepth(schema);
}
// Wrap the user callback to handle JSON serialization protocol
const wrappedCallback = (requestJson: string): string => {
const request = JSON.parse(requestJson) as {
params: Record<string, unknown>;
stdin: string | null;
};
return callback(request.params, request.stdin);
};
this.callbackRefs.push(wrappedCallback);
this.native.addTool(
name,
description,
wrappedCallback,
schema ? JSON.stringify(schema) : undefined,
);
}
/**
* Add an environment variable visible inside scripts.
*/
env(key: string, value: string): void {
this.native.env(key, value);
}
/**
* Execute a bash script synchronously.
*
* Note: ScriptedTool callbacks run asynchronously via Node's event loop.
* If a registered tool is invoked, this method returns a non-zero result
* instead of queueing a callback that would deadlock. Use `execute()`
* (async) for scripts that call registered tools. Only use this for scripts
* that don't invoke any registered tools (e.g., pure bash).
*/
executeSync(commands: string): ExecResult {
return this.native.executeSync(commands);
}
/**
* Execute a bash script asynchronously, returning a Promise.
*
* This is the recommended execution method for ScriptedTool since
* tool callbacks require the Node.js event loop to be running.
*/
async execute(commands: string): Promise<ExecResult> {
const inputLimitResult = inputTooLargeExecResult(
commands,
DEFAULT_MAX_INPUT_BYTES,
);
if (inputLimitResult) {
return inputLimitResult;
}
return queueAsyncExecute(this, () => this.native.execute(commands));
}
/**
* Execute synchronously. Throws `BashError` on non-zero exit.
*
* Same caveats as `executeSync()` — throws when a registered tool would
* require the blocked Node event loop. Use `executeOrThrow()` instead.
*/
executeSyncOrThrow(commands: string): ExecResult {
const result = this.native.executeSync(commands);
if (result.exitCode !== 0) {
throw new BashError(result);
}
return result;
}
/**
* Execute asynchronously. Throws `BashError` on non-zero exit.
*/
async executeOrThrow(commands: string): Promise<ExecResult> {
const result = await this.execute(commands);
if (result.exitCode !== 0) {
throw new BashError(result);
}
return result;
}
/** Tool name. */
get name(): string {
return this.native.name;
}
/** Short description. */
get shortDescription(): string {
return this.native.shortDescription;
}
/** Number of registered tools. */
toolCount(): number {
return this.native.toolCount();
}
/** Token-efficient tool description. */
description(): string {
return this.native.description();
}
/** Markdown help document. */
help(): string {
return this.native.help();
}
/** Compact system prompt for orchestration. */
systemPrompt(): string {
return this.native.systemPrompt();
}
/** JSON input schema as string. */
inputSchema(): string {
return this.native.inputSchema();
}
/** JSON output schema as string. */
outputSchema(): string {
return this.native.outputSchema();
}
/** Tool version. */
get version(): string {
return this.native.version;
}
}