@dynamicagents/plugins 0.16.1 → 0.17.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/README.md +1 -1
- package/dist/claude-code/cards.d.ts +6 -1
- package/dist/claude-code/cards.js +21 -2
- package/dist/claude-code/model.d.ts +4 -0
- package/dist/claude-code/model.js +74 -7
- package/dist/claude-code/verified.d.ts +1 -1
- package/dist/claude-code/verified.js +1 -1
- package/dist/computer/client.d.ts +9 -1
- package/dist/computer/context.js +1 -1
- package/dist/computer/index.d.ts +10 -6
- package/dist/computer/index.js +34 -17
- package/dist/computer/paths.d.ts +26 -2
- package/dist/computer/paths.js +51 -5
- package/dist/computer/proxy.d.ts +10 -3
- package/dist/computer/proxy.js +77 -87
- package/dist/computer/render.d.ts +6 -35
- package/dist/computer/render.js +5 -43
- package/dist/computer/sandbox.d.ts +4 -3
- package/dist/computer/sandbox.js +9 -2
- package/dist/computer/shell.d.ts +8 -7
- package/dist/computer/shell.js +8 -7
- package/dist/computer/tools-exec.d.ts +39 -0
- package/dist/computer/tools-exec.js +357 -228
- package/dist/computer/tools-grep.js +10 -4
- package/dist/sandbox/header.d.ts +13 -0
- package/dist/sandbox/header.js +19 -4
- package/dist/sandbox/install-plan.d.ts +1 -1
- package/dist/sandbox/install-plan.js +2 -4
- package/dist/sandbox/object.d.ts +1 -0
- package/dist/sandbox/object.js +21 -13
- package/package.json +9 -7
package/dist/computer/proxy.js
CHANGED
|
@@ -52,78 +52,31 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
52
52
|
});
|
|
53
53
|
import { withFileLock } from "./file-lock.js";
|
|
54
54
|
import { writeGate } from "./gate.js";
|
|
55
|
-
import { guardPath,
|
|
55
|
+
import { guardPath, walkSkips } from "./paths.js";
|
|
56
56
|
/**
|
|
57
57
|
* The key the route is carried under. `Symbol.for`, so a second copy of this
|
|
58
58
|
* module in one bundle still recognises the first's workspaces.
|
|
59
59
|
*/
|
|
60
60
|
const ROUTE = Symbol.for("@dynamicagents/plugins/computer:workspace");
|
|
61
|
-
/**
|
|
62
|
-
* The most one `glob` returns. Its caller is Think's `find`, which shows 200
|
|
63
|
-
* and says when there were more — so one past that is all it can use, and
|
|
64
|
-
* reading the rest of a large checkout into the object would buy nothing.
|
|
65
|
-
*/
|
|
66
|
-
const GLOB_LIMIT = 201;
|
|
67
61
|
/** The route of a workspace {@link routedWorkspace} built, or `undefined`. */
|
|
68
62
|
export function workspaceRoute(workspace) {
|
|
69
63
|
if (typeof workspace !== "object" || workspace === null)
|
|
70
64
|
return undefined;
|
|
71
65
|
return workspace[ROUTE];
|
|
72
66
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Missing, as the filesystem says it across the boundary. The code does not
|
|
75
|
-
* always survive RPC, so the message is read too.
|
|
76
|
-
*/
|
|
77
|
-
function isMissing(err) {
|
|
78
|
-
if (!err || typeof err !== "object")
|
|
79
|
-
return false;
|
|
80
|
-
const { code, message } = err;
|
|
81
|
-
if (code === "ENOENT")
|
|
82
|
-
return true;
|
|
83
|
-
return typeof message === "string" && /ENOENT|no such/i.test(message);
|
|
84
|
-
}
|
|
85
|
-
/** `null` for a path that is not there, which is what Think's tools expect. */
|
|
86
|
-
async function orNull(read) {
|
|
87
|
-
try {
|
|
88
|
-
return await read;
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
if (isMissing(err))
|
|
92
|
-
return null;
|
|
93
|
-
throw err;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
67
|
+
/** The bytes of a stream, for a host RPC that takes a value rather than one. */
|
|
96
68
|
async function drain(stream) {
|
|
97
69
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
98
70
|
}
|
|
99
|
-
function fileInfo(entry) {
|
|
100
|
-
const type = entry.isDirectory ? "directory" : "file";
|
|
101
|
-
return {
|
|
102
|
-
path: entry.path,
|
|
103
|
-
name: entry.path.slice(entry.path.lastIndexOf("/") + 1),
|
|
104
|
-
type,
|
|
105
|
-
// Generic on purpose: Think's `read` sniffs a generic type from the bytes,
|
|
106
|
-
// and an extension is not evidence of what a file holds.
|
|
107
|
-
mimeType: type === "directory" ? "inode/directory" : "application/octet-stream",
|
|
108
|
-
size: entry.size ?? 0,
|
|
109
|
-
createdAt: entry.mtime ?? 0,
|
|
110
|
-
updatedAt: entry.mtime ?? 0
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
/** The directory a glob walks from, and the pattern relative to it. */
|
|
114
|
-
function splitGlob(pattern, cwd) {
|
|
115
|
-
const absolute = pattern.startsWith("/") ? pattern : `${cwd}/${pattern}`;
|
|
116
|
-
const wildcard = absolute.search(/[*?[{]/);
|
|
117
|
-
const cut = wildcard === -1
|
|
118
|
-
? absolute.lastIndexOf("/")
|
|
119
|
-
: absolute.lastIndexOf("/", wildcard);
|
|
120
|
-
return {
|
|
121
|
-
dir: cut <= 0 ? "/" : absolute.slice(0, cut),
|
|
122
|
-
rest: absolute.slice(cut + 1)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
71
|
/** Think's file tools over `route`, carrying it for the computer tools to find. */
|
|
126
|
-
|
|
72
|
+
/**
|
|
73
|
+
* `root` is the sandbox's served root and never a working directory. The
|
|
74
|
+
* directories Think writes its own files into are fixed under that root, so
|
|
75
|
+
* anchoring their exclusions anywhere else both hides a checkout's own
|
|
76
|
+
* `attachments/` and leaves Think's in a search of the root: see
|
|
77
|
+
* {@link file://./paths.ts walkSkips}.
|
|
78
|
+
*/
|
|
79
|
+
export function routedWorkspace(route, root) {
|
|
127
80
|
/** One call, against the workspace this turn names. */
|
|
128
81
|
const withFs = async (path, verb, body) => {
|
|
129
82
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
@@ -158,42 +111,79 @@ export function routedWorkspace(route, cwd) {
|
|
|
158
111
|
throw new Error(lost);
|
|
159
112
|
return body(fs, name);
|
|
160
113
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Read whole or ranged, and as text or as bytes, in the one overloaded
|
|
116
|
+
* method Computer's filesystem declares. The options go through untouched:
|
|
117
|
+
* an `edit` reads text and writes back what it read, and a `read` of an
|
|
118
|
+
* evicted image asks for the bytes.
|
|
119
|
+
*
|
|
120
|
+
* **A host's `readFile` has to materialise what it answers**, because the
|
|
121
|
+
* client is released when this call returns and a stream that outlived it
|
|
122
|
+
* would be read against a disposed one. `/sandbox`'s does: it awaits the
|
|
123
|
+
* bytes and hands back a stream over them.
|
|
124
|
+
*/
|
|
125
|
+
const readFile = (path, options) => withFs(path, "read", (fs) => options === undefined
|
|
126
|
+
? fs.readFile(path)
|
|
127
|
+
: fs.readFile(path, options));
|
|
128
|
+
const fs = {
|
|
129
|
+
readFile: readFile,
|
|
130
|
+
stat: (path) => withFs(path, "read", async (fs) => ({
|
|
131
|
+
...(await fs.stat(path)),
|
|
132
|
+
// The container has one, and the sandbox does not report it. Nothing
|
|
133
|
+
// downstream reads it: Computer's tools take `size`, `mtime`, `mode`
|
|
134
|
+
// and the three kind flags, and a number is all the type needs.
|
|
135
|
+
inode: 0
|
|
136
|
+
})),
|
|
137
|
+
readdir: (path, options) => withFs(path, "list", async (fs) => (await fs.readdir(path, options)).map((entry) => ({
|
|
174
138
|
...entry,
|
|
175
|
-
|
|
139
|
+
// The directory this listing was read from, which is what a caller
|
|
140
|
+
// joins a name onto. The sandbox lists names and kinds, so the size
|
|
141
|
+
// and the time are the entry's own where it has them and zero where
|
|
142
|
+
// it does not: `ls` shows both to the model, and a second round trip
|
|
143
|
+
// per entry to fill them in would cost a directory's worth of RPCs.
|
|
144
|
+
parentPath: path,
|
|
145
|
+
size: entry.size ?? 0,
|
|
146
|
+
mtime: entry.mtime ?? 0,
|
|
147
|
+
isSymbolicLink: entry.isSymbolicLink ?? false
|
|
176
148
|
}))),
|
|
177
149
|
// Pruned rather than filtered: at a repository root either skipped
|
|
178
|
-
// directory outnumbers everything else.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
150
|
+
// directory outnumbers everything else. No limit of this layer's own —
|
|
151
|
+
// `./sandbox.ts` caps a walk that arrives without one.
|
|
152
|
+
find: (directory, pattern, options) => withFs(directory, "find", (fs) => fs.find(directory, pattern, {
|
|
153
|
+
...options,
|
|
154
|
+
exclude: [...walkSkips(directory, root), ...(options?.exclude ?? [])]
|
|
155
|
+
})),
|
|
156
|
+
grep: (pattern, path, options) => withFs(path, "search", (fs) => fs.grep(pattern, path, {
|
|
157
|
+
...options,
|
|
158
|
+
exclude: [
|
|
159
|
+
...walkSkips(path, root),
|
|
160
|
+
...(options?.exclude ?? [])
|
|
161
|
+
]
|
|
162
|
+
})),
|
|
163
|
+
/**
|
|
164
|
+
* Locked per file, so a write never lands inside the `edit` tool's read and
|
|
165
|
+
* write of the same file — see `./file-lock.ts`.
|
|
166
|
+
*
|
|
167
|
+
* A stream is drained here because the sandbox RPC takes a value. The
|
|
168
|
+
* `mode` a caller may send is dropped: the sandbox's write has nowhere to
|
|
169
|
+
* put one, so a mode this cannot set is not one it can claim to preserve.
|
|
170
|
+
*/
|
|
171
|
+
writeFile: (path, content) => withWritableFs(path, "write", async (fs, name) => {
|
|
172
|
+
const body = content instanceof ReadableStream ? await drain(content) : content;
|
|
173
|
+
return withFileLock(name, path, () => fs.writeFile(path, body));
|
|
174
|
+
}),
|
|
175
|
+
mkdir: (path, options) => withWritableFs(path, "write", (fs) => fs.mkdir(path, options)),
|
|
190
176
|
// Locked too: a delete landing between an edit's read and its write would
|
|
191
177
|
// be undone by the write, and both would report success.
|
|
192
|
-
rm: (path,
|
|
193
|
-
// No `writeFileBytes`. Think writes through it on its own account —
|
|
194
|
-
// evicted media, projected skills — and none of that belongs in a
|
|
195
|
-
// repository's checkout. Without it Think skips both, with a warning.
|
|
178
|
+
rm: (path, options) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, options)))
|
|
196
179
|
};
|
|
180
|
+
/**
|
|
181
|
+
* `fs` alone. Computer's exec tool is offered only for a workspace carrying a
|
|
182
|
+
* `runtime`, so withholding one is how this keeps Think from building a
|
|
183
|
+
* second shell over the same container — `./tools-exec.ts` is the one the
|
|
184
|
+
* model gets, and it is this package's.
|
|
185
|
+
*/
|
|
186
|
+
const workspace = { fs };
|
|
197
187
|
Object.defineProperty(workspace, ROUTE, { value: route });
|
|
198
188
|
return workspace;
|
|
199
189
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GrepMatch } from "./client.js";
|
|
2
2
|
/**
|
|
3
3
|
* Emit whole items until the byte budget is spent, and report exactly how many.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Core's `truncateOutput` is wrong for every list this module produces, and is
|
|
6
6
|
* deliberately not used by them. It keeps both ends and drops the middle, which is
|
|
7
|
-
* right for a build log
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* right for a build log and destructive for a match list or a directory listing,
|
|
8
|
+
* where the middle is a whole file's worth of hits or a whole subtree that silently
|
|
9
|
+
* vanishes. So the budget is spent item by item, in order, and the caller learns
|
|
10
|
+
* where it stopped.
|
|
11
11
|
*
|
|
12
12
|
* `shown` is the load-bearing return value: it is what makes the next page's offset
|
|
13
13
|
* exact rather than a guess. A block is committed whole or not at all, because half
|
|
@@ -44,32 +44,3 @@ export declare function renderGrepMatches(matches: GrepMatch[], maxChars: number
|
|
|
44
44
|
shown: number;
|
|
45
45
|
capped: boolean;
|
|
46
46
|
};
|
|
47
|
-
/**
|
|
48
|
-
* Render a completed command the way a terminal would, minus the noise.
|
|
49
|
-
*
|
|
50
|
-
* ## Every command ends with its verdict, including the successful ones
|
|
51
|
-
*
|
|
52
|
-
* Printing the exit code only when it is non-zero looks like saving noise and is
|
|
53
|
-
* not: a model with no verdict appends `; echo "EXIT_CODE=$?"` itself, and
|
|
54
|
-
* reaches for `${PIPESTATUS[0]}` to read a piped one — which dash does not
|
|
55
|
-
* implement, so the whole line fails after the command it wrapped has run.
|
|
56
|
-
*
|
|
57
|
-
* ## `status` is not the exit code
|
|
58
|
-
*
|
|
59
|
-
* A `status` is `completed | failed | cancelled`. Dropping it
|
|
60
|
-
* renders a command killed at the `timeoutMs` ceiling exactly like one that ran
|
|
61
|
-
* to completion and failed — "your test suite was killed at ten minutes" and
|
|
62
|
-
* "your test suite has a failing test", which want opposite responses. Anything
|
|
63
|
-
* other than `completed` is stated.
|
|
64
|
-
*
|
|
65
|
-
* ## One budget, applied once
|
|
66
|
-
*
|
|
67
|
-
* Truncating per stream makes `maxOutputChars` mean "up to twice this". The bound
|
|
68
|
-
* is on the rendered transcript, which is what reaches the context window.
|
|
69
|
-
*/
|
|
70
|
-
export declare function renderResult(result: {
|
|
71
|
-
exitCode: number;
|
|
72
|
-
stdout: string;
|
|
73
|
-
stderr: string;
|
|
74
|
-
status?: CommandResult["status"];
|
|
75
|
-
}, maxChars: number): string;
|
package/dist/computer/render.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { truncateOutput } from "@dynamicagents/core";
|
|
2
1
|
/**
|
|
3
2
|
* Turning a result into the text a model reads.
|
|
4
3
|
*
|
|
@@ -31,12 +30,12 @@ function capLine(text) {
|
|
|
31
30
|
/**
|
|
32
31
|
* Emit whole items until the byte budget is spent, and report exactly how many.
|
|
33
32
|
*
|
|
34
|
-
*
|
|
33
|
+
* Core's `truncateOutput` is wrong for every list this module produces, and is
|
|
35
34
|
* deliberately not used by them. It keeps both ends and drops the middle, which is
|
|
36
|
-
* right for a build log
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* right for a build log and destructive for a match list or a directory listing,
|
|
36
|
+
* where the middle is a whole file's worth of hits or a whole subtree that silently
|
|
37
|
+
* vanishes. So the budget is spent item by item, in order, and the caller learns
|
|
38
|
+
* where it stopped.
|
|
40
39
|
*
|
|
41
40
|
* `shown` is the load-bearing return value: it is what makes the next page's offset
|
|
42
41
|
* exact rather than a guess. A block is committed whole or not at all, because half
|
|
@@ -102,40 +101,3 @@ export function renderGrepMatches(matches, maxChars) {
|
|
|
102
101
|
// Only a line that actually made it out is worth explaining.
|
|
103
102
|
return { body, shown, capped: hasLongLine.slice(0, shown).includes(true) };
|
|
104
103
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Render a completed command the way a terminal would, minus the noise.
|
|
107
|
-
*
|
|
108
|
-
* ## Every command ends with its verdict, including the successful ones
|
|
109
|
-
*
|
|
110
|
-
* Printing the exit code only when it is non-zero looks like saving noise and is
|
|
111
|
-
* not: a model with no verdict appends `; echo "EXIT_CODE=$?"` itself, and
|
|
112
|
-
* reaches for `${PIPESTATUS[0]}` to read a piped one — which dash does not
|
|
113
|
-
* implement, so the whole line fails after the command it wrapped has run.
|
|
114
|
-
*
|
|
115
|
-
* ## `status` is not the exit code
|
|
116
|
-
*
|
|
117
|
-
* A `status` is `completed | failed | cancelled`. Dropping it
|
|
118
|
-
* renders a command killed at the `timeoutMs` ceiling exactly like one that ran
|
|
119
|
-
* to completion and failed — "your test suite was killed at ten minutes" and
|
|
120
|
-
* "your test suite has a failing test", which want opposite responses. Anything
|
|
121
|
-
* other than `completed` is stated.
|
|
122
|
-
*
|
|
123
|
-
* ## One budget, applied once
|
|
124
|
-
*
|
|
125
|
-
* Truncating per stream makes `maxOutputChars` mean "up to twice this". The bound
|
|
126
|
-
* is on the rendered transcript, which is what reaches the context window.
|
|
127
|
-
*/
|
|
128
|
-
export function renderResult(result, maxChars) {
|
|
129
|
-
const parts = [];
|
|
130
|
-
if (result.stdout)
|
|
131
|
-
parts.push(result.stdout);
|
|
132
|
-
// Empty whenever the host configured a `shell`, which merges the streams at the
|
|
133
|
-
// source so the transcript keeps its causal order. Kept for hosts that did not:
|
|
134
|
-
// two labelled blocks beat silently dropping half the output.
|
|
135
|
-
if (result.stderr)
|
|
136
|
-
parts.push(`--- stderr ---\n${result.stderr}`);
|
|
137
|
-
const body = truncateOutput(parts.join("\n"), maxChars);
|
|
138
|
-
const state = result.status && result.status !== "completed" ? ` (${result.status})` : "";
|
|
139
|
-
const verdict = `--- exit ${result.exitCode}${state} ---`;
|
|
140
|
-
return body ? `${body}\n${verdict}` : `(no output)\n${verdict}`;
|
|
141
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ThinkWorkspace } from "@cloudflare/think";
|
|
2
2
|
import type { SandboxAdvisory } from "../sandbox/advisory.js";
|
|
3
3
|
import type { ComputerClient } from "./client.js";
|
|
4
4
|
/**
|
|
@@ -27,11 +27,12 @@ export interface SandboxComputerHost extends Rpc.DurableObjectBranded {
|
|
|
27
27
|
stderr: string;
|
|
28
28
|
}>;
|
|
29
29
|
readFile(path: string): Promise<Uint8Array | null>;
|
|
30
|
-
writeFile(path: string, content: string): Promise<void>;
|
|
30
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
31
31
|
stat(path: string): Promise<{
|
|
32
32
|
type: string;
|
|
33
33
|
size: number;
|
|
34
34
|
modifiedAt: number;
|
|
35
|
+
mode: number;
|
|
35
36
|
} | null>;
|
|
36
37
|
readDir(path: string): Promise<{
|
|
37
38
|
name: string;
|
|
@@ -50,6 +51,6 @@ export interface SandboxComputerConfig {
|
|
|
50
51
|
sandboxName: () => string;
|
|
51
52
|
cwd?: string;
|
|
52
53
|
}
|
|
53
|
-
export declare function computerWorkspace(config: SandboxComputerConfig, runtime?: () => unknown):
|
|
54
|
+
export declare function computerWorkspace(config: SandboxComputerConfig, runtime?: () => unknown): ThinkWorkspace;
|
|
54
55
|
/** The computer tools' client over one sandbox. */
|
|
55
56
|
export declare function sandboxClient(host: SandboxComputerHost): ComputerClient;
|
package/dist/computer/sandbox.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RUN_DIR } from "../sandbox/constants.js";
|
|
2
|
+
import { SANDBOX_ROOT } from "../sandbox/header.js";
|
|
2
3
|
import { sandboxNameFromRuntime } from "../sandbox/name.js";
|
|
3
4
|
import { routedWorkspace } from "./proxy.js";
|
|
4
5
|
/** After `timeout`'s TERM, how long before its KILL. */
|
|
@@ -39,7 +40,10 @@ export function computerWorkspace(config, runtime) {
|
|
|
39
40
|
name: () => sandboxNameFromRuntime(runtime?.()) ?? config.sandboxName(),
|
|
40
41
|
open: async (name) => sandboxClient(host(name)),
|
|
41
42
|
advisories: (name) => host(name).advisories()
|
|
42
|
-
},
|
|
43
|
+
},
|
|
44
|
+
// The served root, not `config.cwd`: a host may point the tools at a
|
|
45
|
+
// checkout, and Think's own directories stay where Think puts them.
|
|
46
|
+
SANDBOX_ROOT);
|
|
43
47
|
}
|
|
44
48
|
function missing(path) {
|
|
45
49
|
return Object.assign(new Error(`ENOENT: no such file or directory, ${path}`), { code: "ENOENT" });
|
|
@@ -61,8 +65,10 @@ export function sandboxClient(host) {
|
|
|
61
65
|
if (!stat)
|
|
62
66
|
throw missing(path);
|
|
63
67
|
return {
|
|
68
|
+
name: path.slice(path.lastIndexOf("/") + 1),
|
|
64
69
|
size: stat.size,
|
|
65
70
|
mtime: stat.modifiedAt,
|
|
71
|
+
mode: stat.mode,
|
|
66
72
|
isFile: stat.type === "file",
|
|
67
73
|
isDirectory: stat.type === "directory",
|
|
68
74
|
isSymbolicLink: stat.type === "symlink"
|
|
@@ -75,7 +81,8 @@ export function sandboxClient(host) {
|
|
|
75
81
|
.map((entry) => ({
|
|
76
82
|
name: entry.name,
|
|
77
83
|
isFile: entry.type === "file",
|
|
78
|
-
isDirectory: entry.type === "directory"
|
|
84
|
+
isDirectory: entry.type === "directory",
|
|
85
|
+
isSymbolicLink: entry.type === "symlink"
|
|
79
86
|
}));
|
|
80
87
|
},
|
|
81
88
|
find: (directory, pattern, options = {}) => find(host, directory, pattern, options),
|
package/dist/computer/shell.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* two output streams merged into one transcript, in the order they were
|
|
4
4
|
* written, or hand it back untouched when no shell is configured.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* The shell tool's consumer is **a model reading output**. A project's check is a
|
|
7
|
+
* chain
|
|
7
8
|
* — `wrangler types && prettier && eslint && tsc` — and *which tool spoke last*
|
|
8
9
|
* is how you know which one failed. A stdout block and a separate stderr block
|
|
9
10
|
* destroy that ordering, and a model that wants it back re-runs the whole gate
|
|
@@ -25,12 +26,12 @@
|
|
|
25
26
|
*
|
|
26
27
|
* The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
|
|
27
28
|
* `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*. The
|
|
28
|
-
* tool description
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* tool's description is Computer's and says nothing about either, so the
|
|
30
|
+
* plugin's own context block in {@link file://./index.ts} is where the model is
|
|
31
|
+
* told not to pipe into `head`/`tail`. It needs a shell that implements it —
|
|
32
|
+
* `bash`, `zsh`, `ksh`, not `sh`/dash.
|
|
31
33
|
*
|
|
32
|
-
* With no shell configured the two streams arrive separate, and
|
|
33
|
-
*
|
|
34
|
-
* `--- stderr ---` block.
|
|
34
|
+
* With no shell configured the two streams arrive separate, and the tool reports
|
|
35
|
+
* them as the `stdout` and `stderr` of its result.
|
|
35
36
|
*/
|
|
36
37
|
export declare function withShellTranscript(command: string, shell: string | undefined): string;
|
package/dist/computer/shell.js
CHANGED
|
@@ -4,7 +4,8 @@ import { shellQuote } from "../sandbox/agent-exec.js";
|
|
|
4
4
|
* two output streams merged into one transcript, in the order they were
|
|
5
5
|
* written, or hand it back untouched when no shell is configured.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* The shell tool's consumer is **a model reading output**. A project's check is a
|
|
8
|
+
* chain
|
|
8
9
|
* — `wrangler types && prettier && eslint && tsc` — and *which tool spoke last*
|
|
9
10
|
* is how you know which one failed. A stdout block and a separate stderr block
|
|
10
11
|
* destroy that ordering, and a model that wants it back re-runs the whole gate
|
|
@@ -26,13 +27,13 @@ import { shellQuote } from "../sandbox/agent-exec.js";
|
|
|
26
27
|
*
|
|
27
28
|
* The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
|
|
28
29
|
* `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*. The
|
|
29
|
-
* tool description
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* tool's description is Computer's and says nothing about either, so the
|
|
31
|
+
* plugin's own context block in {@link file://./index.ts} is where the model is
|
|
32
|
+
* told not to pipe into `head`/`tail`. It needs a shell that implements it —
|
|
33
|
+
* `bash`, `zsh`, `ksh`, not `sh`/dash.
|
|
32
34
|
*
|
|
33
|
-
* With no shell configured the two streams arrive separate, and
|
|
34
|
-
*
|
|
35
|
-
* `--- stderr ---` block.
|
|
35
|
+
* With no shell configured the two streams arrive separate, and the tool reports
|
|
36
|
+
* them as the `stdout` and `stderr` of its result.
|
|
36
37
|
*/
|
|
37
38
|
export function withShellTranscript(command, shell) {
|
|
38
39
|
return shell
|
|
@@ -1,3 +1,42 @@
|
|
|
1
1
|
import type { ToolSet } from "ai";
|
|
2
2
|
import type { ComputerContext } from "./context.js";
|
|
3
|
+
/**
|
|
4
|
+
* The container's shell, as Computer's `exec` tool.
|
|
5
|
+
*
|
|
6
|
+
* **The tool is upstream's, and deliberately not wrapped past the advisory
|
|
7
|
+
* gate.** Its schema, its description, its truncation and its result shape are
|
|
8
|
+
* `@cloudflare/computer`'s, so the model sees the same `exec` everywhere that
|
|
9
|
+
* package's tools are installed and this plugin has no second dialect to keep in
|
|
10
|
+
* step. What that costs, each of it accepted rather than overlooked:
|
|
11
|
+
*
|
|
12
|
+
* - **Truncation keeps the tail, not both ends.** Upstream cuts each stream to
|
|
13
|
+
* its last lines, so a failure printed early in a long build is gone from what
|
|
14
|
+
* the model reads. The exit code still reports it, and the model's recourse is
|
|
15
|
+
* a narrower command.
|
|
16
|
+
* - **The description promises a file this workspace does not write.** Upstream
|
|
17
|
+
* tells the model that truncated output is saved where `read` and `grep` can
|
|
18
|
+
* open it. That holds for a Workspace whose runtime saves it; this one reports
|
|
19
|
+
* no saved path, so the reply names none and the sentence is unfulfilled.
|
|
20
|
+
* Nothing can be set to turn it off: the sentence is built into the generated
|
|
21
|
+
* description.
|
|
22
|
+
* - **A command stopped at the time limit or cancelled reads as a plain
|
|
23
|
+
* failure.** Upstream's result has somewhere for an exit code and somewhere
|
|
24
|
+
* for an error, and nowhere for the difference between a command that outran
|
|
25
|
+
* its limit, one whose call was cancelled, and one that simply failed.
|
|
26
|
+
* {@link file://./client.ts CommandResult.status} still carries it for the log
|
|
27
|
+
* line below, which is the only place it is read.
|
|
28
|
+
*
|
|
29
|
+
* What stays this plugin's, because neither belongs to a tool:
|
|
30
|
+
*
|
|
31
|
+
* - **The advisory gate**, below: a command waits out a running install, is
|
|
32
|
+
* refused while something transient makes it pointless, and carries a note
|
|
33
|
+
* about anything permanent. {@link file://./gate.ts ExecGate} is the contract.
|
|
34
|
+
* - **How the container runs a command**, in
|
|
35
|
+
* {@link file://./sandbox.ts sandboxClient}: the launch-and-cancel handshake,
|
|
36
|
+
* the time limit, and the shell transcript. A configured shell merges the two
|
|
37
|
+
* streams onto one of them, so `stderr` arrives empty and the whole transcript
|
|
38
|
+
* is in `stdout` — see {@link file://./shell.ts withShellTranscript} for why a
|
|
39
|
+
* model reading output wants that, and why the plugin's context block rather
|
|
40
|
+
* than the tool description is where the model is told.
|
|
41
|
+
*/
|
|
3
42
|
export declare function execTools(ctx: ComputerContext): ToolSet;
|