@dynamicagents/plugins 0.12.0 → 0.13.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 +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +53 -0
- package/dist/claude-code/cards.js +272 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +209 -112
- package/dist/claude-code/run.js +339 -138
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
- package/dist/{computer/host → workspace}/install-job.js +276 -176
- package/dist/{computer → workspace}/install.d.ts +8 -8
- package/dist/{computer → workspace}/install.js +3 -3
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +1 -1
- package/package.json +16 -36
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/container-deps.d.ts +0 -0
- /package/dist/{computer/host → workspace}/container-deps.js +0 -0
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { WorkspaceLike } from "@cloudflare/think";
|
|
2
|
+
import type { WorkspaceHost } from "../workspace/open.js";
|
|
3
|
+
import type { ComputerConfig } from "./index.js";
|
|
4
|
+
/**
|
|
5
|
+
* The container's workspace, as the agent's own `this.workspace`.
|
|
6
|
+
*
|
|
7
|
+
* Think's `read`, `write`, `edit` and `delete` run against whatever an agent's
|
|
8
|
+
* workspace is, and `computer()`'s tools replace only the ones it names. So an
|
|
9
|
+
* agent with a container and Think's default workspace would run `bash` in the
|
|
10
|
+
* container and `write` in its own SQLite, and nothing would say so. An agent
|
|
11
|
+
* that installs `computer` sets this instead:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* override workspace = computerWorkspace(config, () => this.pluginContext().runtime());
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* and `computer(config).tools(ctx)` refuses to start without it — and reaches
|
|
18
|
+
* the workspace through it, so the tools and Think's file tools cannot resolve
|
|
19
|
+
* two different ones.
|
|
20
|
+
*
|
|
21
|
+
* Built on the workspace's own filesystem, not on `@cloudflare/computer`'s
|
|
22
|
+
* Think adapter: that one's `glob` and `readDir` read every entry, and its
|
|
23
|
+
* `writeFile` knows nothing of the paths and the states this plugin refuses.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Where a {@link computerWorkspace} sends each call, carried on it under
|
|
27
|
+
* {@link ROUTE}. `computer()`'s tools route through this rather than through
|
|
28
|
+
* their own config, which is what makes a workspace built from another config,
|
|
29
|
+
* or without its runtime, a wrong workspace for both rather than a split.
|
|
30
|
+
*/
|
|
31
|
+
export interface WorkspaceRoute {
|
|
32
|
+
/** The workspace the running turn names. Read per call. */
|
|
33
|
+
name(): string;
|
|
34
|
+
host(name: string): DurableObjectStub<WorkspaceHost>;
|
|
35
|
+
}
|
|
36
|
+
/** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
|
|
37
|
+
export declare function workspaceRoute(workspace: unknown): WorkspaceRoute | undefined;
|
|
38
|
+
/** Whether `workspace` is one {@link computerWorkspace} built. */
|
|
39
|
+
export declare function isComputerWorkspace(workspace: unknown): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* @param runtime The running sub-agent's `runtime()`, read per call — how a
|
|
42
|
+
* sub-agent's file tools reach the workspace its parent prepared. A parent's
|
|
43
|
+
* is `undefined`, and {@link ComputerConfig.workspaceName} names it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function computerWorkspace(config: ComputerConfig, runtime?: () => unknown): WorkspaceLike;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { withFileLock } from "./file-lock.js";
|
|
54
|
+
import { writeGate } from "./gate.js";
|
|
55
|
+
import { openWorkspaceFs, workspaceNameFromRuntime } from "../workspace/open.js";
|
|
56
|
+
import { guardPath, WALK_SKIPS } from "./paths.js";
|
|
57
|
+
/**
|
|
58
|
+
* The key the route is carried under. `Symbol.for`, so a second copy of this
|
|
59
|
+
* module in one bundle still recognises the first's workspaces.
|
|
60
|
+
*/
|
|
61
|
+
const ROUTE = Symbol.for("@dynamicagents/plugins/computer:workspace");
|
|
62
|
+
/**
|
|
63
|
+
* The most one `glob` returns. Its caller is Think's `find`, which shows 200
|
|
64
|
+
* and says when there were more — so one past that is all it can use, and
|
|
65
|
+
* reading the rest of a large checkout into the object would buy nothing.
|
|
66
|
+
*/
|
|
67
|
+
const GLOB_LIMIT = 201;
|
|
68
|
+
/** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
|
|
69
|
+
export function workspaceRoute(workspace) {
|
|
70
|
+
if (typeof workspace !== "object" || workspace === null)
|
|
71
|
+
return undefined;
|
|
72
|
+
return workspace[ROUTE];
|
|
73
|
+
}
|
|
74
|
+
/** Whether `workspace` is one {@link computerWorkspace} built. */
|
|
75
|
+
export function isComputerWorkspace(workspace) {
|
|
76
|
+
return workspaceRoute(workspace) !== undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Missing, as the filesystem says it across the boundary. The code does not
|
|
80
|
+
* always survive RPC, so the message is read too.
|
|
81
|
+
*/
|
|
82
|
+
function isMissing(err) {
|
|
83
|
+
if (!err || typeof err !== "object")
|
|
84
|
+
return false;
|
|
85
|
+
const { code, message } = err;
|
|
86
|
+
if (code === "ENOENT")
|
|
87
|
+
return true;
|
|
88
|
+
return typeof message === "string" && /ENOENT|no such/i.test(message);
|
|
89
|
+
}
|
|
90
|
+
/** `null` for a path that is not there, which is what Think's tools expect. */
|
|
91
|
+
async function orNull(read) {
|
|
92
|
+
try {
|
|
93
|
+
return await read;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (isMissing(err))
|
|
97
|
+
return null;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `stat`, with a missing path answered as `null`.
|
|
103
|
+
*
|
|
104
|
+
* Asked of the stub's own `statOrNull` where it has one, the way
|
|
105
|
+
* {@link file://../workspace/read.ts pathExists} asks for `exists`: Think's
|
|
106
|
+
* `read` stats before it reads, so a missing file is an ordinary answer here,
|
|
107
|
+
* and a `stat` that throws across the boundary leaves the object an unhandled
|
|
108
|
+
* rejection to log for it.
|
|
109
|
+
*/
|
|
110
|
+
function statOrNull(fs, path) {
|
|
111
|
+
// Called as a method, never through `.call`: on an RPC stub `.call` is read
|
|
112
|
+
// as a remote method.
|
|
113
|
+
const remote = fs;
|
|
114
|
+
if (typeof remote.statOrNull === "function")
|
|
115
|
+
return remote.statOrNull(path);
|
|
116
|
+
return orNull(fs.stat(path));
|
|
117
|
+
}
|
|
118
|
+
async function drain(stream) {
|
|
119
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
120
|
+
}
|
|
121
|
+
function fileInfo(entry) {
|
|
122
|
+
const type = entry.isDirectory ? "directory" : "file";
|
|
123
|
+
return {
|
|
124
|
+
path: entry.path,
|
|
125
|
+
name: entry.path.slice(entry.path.lastIndexOf("/") + 1),
|
|
126
|
+
type,
|
|
127
|
+
// Generic on purpose: Think's `read` sniffs a generic type from the bytes,
|
|
128
|
+
// and an extension is not evidence of what a file holds.
|
|
129
|
+
mimeType: type === "directory" ? "inode/directory" : "application/octet-stream",
|
|
130
|
+
size: entry.size ?? 0,
|
|
131
|
+
createdAt: entry.mtime ?? 0,
|
|
132
|
+
updatedAt: entry.mtime ?? 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** The directory a glob walks from, and the pattern relative to it. */
|
|
136
|
+
function splitGlob(pattern, cwd) {
|
|
137
|
+
const absolute = pattern.startsWith("/") ? pattern : `${cwd}/${pattern}`;
|
|
138
|
+
const wildcard = absolute.search(/[*?[{]/);
|
|
139
|
+
const cut = wildcard === -1
|
|
140
|
+
? absolute.lastIndexOf("/")
|
|
141
|
+
: absolute.lastIndexOf("/", wildcard);
|
|
142
|
+
return {
|
|
143
|
+
dir: cut <= 0 ? "/" : absolute.slice(0, cut),
|
|
144
|
+
rest: absolute.slice(cut + 1)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* @param runtime The running sub-agent's `runtime()`, read per call — how a
|
|
149
|
+
* sub-agent's file tools reach the workspace its parent prepared. A parent's
|
|
150
|
+
* is `undefined`, and {@link ComputerConfig.workspaceName} names it.
|
|
151
|
+
*/
|
|
152
|
+
export function computerWorkspace(config, runtime) {
|
|
153
|
+
const cwd = config.cwd ?? "/workspace";
|
|
154
|
+
// Resolved per call, never memoized: the name belongs to the turn, and a
|
|
155
|
+
// stale one would route this caller's writes into another's files.
|
|
156
|
+
const named = () => workspaceNameFromRuntime(runtime?.()) ?? config.workspaceName();
|
|
157
|
+
const host = (name) => config.binding.get(config.binding.idFromName(name));
|
|
158
|
+
/**
|
|
159
|
+
* One call, against the workspace this turn names. Opened without starting a
|
|
160
|
+
* container — see `WorkspaceHost.__getWorkspaceFsStub`.
|
|
161
|
+
*/
|
|
162
|
+
const withFs = async (path, verb, body) => {
|
|
163
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
164
|
+
try {
|
|
165
|
+
const refusal = guardPath(path, verb);
|
|
166
|
+
if (refusal)
|
|
167
|
+
throw new Error(refusal);
|
|
168
|
+
const name = named();
|
|
169
|
+
const ws = __addDisposableResource(env_1, await openWorkspaceFs(host(name)), false);
|
|
170
|
+
return await body(ws.fs, name);
|
|
171
|
+
}
|
|
172
|
+
catch (e_1) {
|
|
173
|
+
env_1.error = e_1;
|
|
174
|
+
env_1.hasError = true;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
__disposeResources(env_1);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* A write, refused while the workspace cannot keep it. Thrown rather than
|
|
182
|
+
* returned: Think's tools report a thrown error to the model, and a write
|
|
183
|
+
* that returned would read as one that landed.
|
|
184
|
+
*/
|
|
185
|
+
const withWritableFs = (path, verb, body) => withFs(path, verb, async (fs, name) => {
|
|
186
|
+
// Fails open, for the reason the tools' own write gate does: a read of
|
|
187
|
+
// another Durable Object's state must not take out a working tool.
|
|
188
|
+
const lost = await host(name)
|
|
189
|
+
.advisories()
|
|
190
|
+
.then(writeGate, () => undefined);
|
|
191
|
+
if (lost)
|
|
192
|
+
throw new Error(lost);
|
|
193
|
+
return body(fs, name);
|
|
194
|
+
});
|
|
195
|
+
const workspace = {
|
|
196
|
+
readFile: (path) =>
|
|
197
|
+
// Whole, never truncated: Think's `edit` writes back what it read.
|
|
198
|
+
withFs(path, "read", (fs) => orNull(fs.readFile(path, "utf8"))),
|
|
199
|
+
readFileBytes: (path) => withFs(path, "read", async (fs) => {
|
|
200
|
+
const stream = await orNull(fs.readFile(path));
|
|
201
|
+
return stream === null ? null : drain(stream);
|
|
202
|
+
}),
|
|
203
|
+
stat: (path) => withFs(path, "read", async (fs) => {
|
|
204
|
+
const stat = await statOrNull(fs, path);
|
|
205
|
+
return stat === null ? null : fileInfo({ ...stat, path });
|
|
206
|
+
}),
|
|
207
|
+
readDir: (dir = cwd, opts) => withFs(dir, "list", async (fs) => (await fs.readdir(dir, opts)).map((entry) => fileInfo({
|
|
208
|
+
...entry,
|
|
209
|
+
path: dir === "/" ? `/${entry.name}` : `${dir}/${entry.name}`
|
|
210
|
+
}))),
|
|
211
|
+
// Pruned rather than filtered: at a repository root either skipped
|
|
212
|
+
// directory outnumbers everything else.
|
|
213
|
+
glob: (pattern) => {
|
|
214
|
+
const { dir, rest } = splitGlob(pattern, cwd);
|
|
215
|
+
return withFs(dir, "find", async (fs) => (await fs.find(dir, rest, {
|
|
216
|
+
limit: GLOB_LIMIT,
|
|
217
|
+
exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
|
|
218
|
+
})).map((entry) => fileInfo({ path: entry.path, isDirectory: entry.type === "dir" })));
|
|
219
|
+
},
|
|
220
|
+
// Locked per file, so a write never lands inside the `edit` tool's read and
|
|
221
|
+
// write of the same file — see `./file-lock.ts`.
|
|
222
|
+
writeFile: (path, content) => withWritableFs(path, "write", (fs, name) => withFileLock(name, path, () => fs.writeFile(path, content))),
|
|
223
|
+
mkdir: (path, opts) => withWritableFs(path, "write", (fs) => fs.mkdir(path, opts)),
|
|
224
|
+
// Locked too: a delete landing between an edit's read and its write would
|
|
225
|
+
// be undone by the write, and both would report success.
|
|
226
|
+
rm: (path, opts) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, opts)))
|
|
227
|
+
// No `writeFileBytes`. Think writes through it on its own account —
|
|
228
|
+
// evicted media, projected skills — and none of that belongs in a
|
|
229
|
+
// repository's checkout. Without it Think skips both, with a warning.
|
|
230
|
+
};
|
|
231
|
+
const route = { name: named, host };
|
|
232
|
+
Object.defineProperty(workspace, ROUTE, { value: route });
|
|
233
|
+
return workspace;
|
|
234
|
+
}
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import type { WorkspaceClient, WorkspaceRuntimeStatus } from "@cloudflare/computer";
|
|
2
|
-
/**
|
|
3
|
-
* Middle-out truncation, so both the first error and the final summary survive.
|
|
4
|
-
*
|
|
5
|
-
* The guard is not defensive padding. Without it a `max` at or below the marker's
|
|
6
|
-
* own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
|
|
7
|
-
* *whole* string — so the function returns more than it was given: 500 characters
|
|
8
|
-
* in, 543 out at `max: 80`. A silent inversion of the one thing it does,
|
|
9
|
-
* reachable from a public config field.
|
|
10
|
-
*/
|
|
11
|
-
export declare function truncateOutput(text: string, max: number): string;
|
|
12
|
-
/**
|
|
13
|
-
* A byte count in the form a model can act on.
|
|
14
|
-
*
|
|
15
|
-
* A listing that says a file is 4.2 MB tells the model the read it is about to do
|
|
16
|
-
* comes back with a hole in the middle — the one thing it cannot infer from the
|
|
17
|
-
* truncated result itself.
|
|
18
|
-
*/
|
|
19
|
-
export declare function humanBytes(bytes: number): string;
|
|
20
2
|
/**
|
|
21
3
|
* One `fs.grep` hit, derived from the client rather than imported.
|
|
22
4
|
*
|
|
23
5
|
* `@cloudflare/computer` declares `WorkspaceGrepMatch` but does not re-export it,
|
|
24
6
|
* so there is no name to import. Reading it back off the method keeps this correct
|
|
25
|
-
* by construction
|
|
7
|
+
* by construction.
|
|
26
8
|
*/
|
|
27
9
|
type GrepMatch = Awaited<ReturnType<WorkspaceClient["fs"]["grep"]>>[number];
|
|
28
10
|
/**
|
|
@@ -70,8 +52,6 @@ export declare function renderGrepMatches(matches: GrepMatch[], maxChars: number
|
|
|
70
52
|
shown: number;
|
|
71
53
|
capped: boolean;
|
|
72
54
|
};
|
|
73
|
-
/** Elapsed time in the shape a sentence wants. */
|
|
74
|
-
export declare function humanMs(ms: number): string;
|
|
75
55
|
/**
|
|
76
56
|
* Render a completed command the way a terminal would, minus the noise.
|
|
77
57
|
*
|
|
@@ -129,23 +109,4 @@ export declare function renderResult(result: {
|
|
|
129
109
|
export declare function syncPendingNote(sync: {
|
|
130
110
|
status: "complete" | "pending";
|
|
131
111
|
} | undefined): string | undefined;
|
|
132
|
-
/**
|
|
133
|
-
* Why a command has nothing to show for itself.
|
|
134
|
-
*
|
|
135
|
-
* {@link renderResult} states a non-`completed` status on its verdict line;
|
|
136
|
-
* {@link computerExec} has no verdict line, only the four fields its caller
|
|
137
|
-
* branches on. A *killed* process writes nothing, so without this a `git clone`
|
|
138
|
-
* that hit the ten-minute ceiling reaches the model through
|
|
139
|
-
* `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
|
|
140
|
-
* like a bug in the plugin rather than a limit it can work within.
|
|
141
|
-
*
|
|
142
|
-
* Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
|
|
143
|
-
* command's own stderr is the better explanation and this would be noise on top
|
|
144
|
-
* of it.
|
|
145
|
-
*
|
|
146
|
-
* Both plausible causes are named because the exit code cannot separate them:
|
|
147
|
-
* 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
|
|
148
|
-
* container that ran out of memory.
|
|
149
|
-
*/
|
|
150
|
-
export declare function cancelledNote(status: WorkspaceRuntimeStatus | undefined, exitCode: number, timeoutMs: number): string | undefined;
|
|
151
112
|
export {};
|
package/dist/computer/render.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { truncateOutput } from "../workspace/format.js";
|
|
1
2
|
/**
|
|
2
3
|
* Turning a result into the text a model reads.
|
|
3
4
|
*
|
|
@@ -15,43 +16,9 @@
|
|
|
15
16
|
* A grep match carries the whole line, and a minified bundle is one line of
|
|
16
17
|
* megabytes — a single match in `dist/` would otherwise be the entire result. The
|
|
17
18
|
* head is kept because a match's file and line number are what the model acts on;
|
|
18
|
-
* it reads the region with `
|
|
19
|
+
* it reads the region with `read` next.
|
|
19
20
|
*/
|
|
20
21
|
const MAX_MATCH_LINE_CHARS = 200;
|
|
21
|
-
/**
|
|
22
|
-
* Middle-out truncation, so both the first error and the final summary survive.
|
|
23
|
-
*
|
|
24
|
-
* The guard is not defensive padding. Without it a `max` at or below the marker's
|
|
25
|
-
* own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
|
|
26
|
-
* *whole* string — so the function returns more than it was given: 500 characters
|
|
27
|
-
* in, 543 out at `max: 80`. A silent inversion of the one thing it does,
|
|
28
|
-
* reachable from a public config field.
|
|
29
|
-
*/
|
|
30
|
-
export function truncateOutput(text, max) {
|
|
31
|
-
if (text.length <= max)
|
|
32
|
-
return text;
|
|
33
|
-
const marker = (dropped) => `\n\n… [${dropped} characters omitted from the middle] …\n\n`;
|
|
34
|
-
// No budget for two halves plus the marker: keep the head, which is where the
|
|
35
|
-
// first error is, and say nothing clever.
|
|
36
|
-
const half = Math.floor((max - marker(text.length).length) / 2);
|
|
37
|
-
if (half < 1)
|
|
38
|
-
return text.slice(0, Math.max(0, max));
|
|
39
|
-
return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* A byte count in the form a model can act on.
|
|
43
|
-
*
|
|
44
|
-
* A listing that says a file is 4.2 MB tells the model the read it is about to do
|
|
45
|
-
* comes back with a hole in the middle — the one thing it cannot infer from the
|
|
46
|
-
* truncated result itself.
|
|
47
|
-
*/
|
|
48
|
-
export function humanBytes(bytes) {
|
|
49
|
-
if (bytes < 1024)
|
|
50
|
-
return `${bytes} B`;
|
|
51
|
-
if (bytes < 1024 * 1024)
|
|
52
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
53
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
54
|
-
}
|
|
55
22
|
/** One matching line, bounded — see {@link MAX_MATCH_LINE_CHARS}. */
|
|
56
23
|
function capLine(text) {
|
|
57
24
|
// A CRLF file leaves the carriage return on every line, since the reader splits
|
|
@@ -135,13 +102,6 @@ export function renderGrepMatches(matches, maxChars) {
|
|
|
135
102
|
// Only a line that actually made it out is worth explaining.
|
|
136
103
|
return { body, shown, capped: hasLongLine.slice(0, shown).includes(true) };
|
|
137
104
|
}
|
|
138
|
-
/** Elapsed time in the shape a sentence wants. */
|
|
139
|
-
export function humanMs(ms) {
|
|
140
|
-
const s = Math.round(ms / 1000);
|
|
141
|
-
return s < 60
|
|
142
|
-
? `${s}s`
|
|
143
|
-
: `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
144
|
-
}
|
|
145
105
|
/**
|
|
146
106
|
* Render a completed command the way a terminal would, minus the noise.
|
|
147
107
|
*
|
|
@@ -212,29 +172,3 @@ export function syncPendingNote(sync) {
|
|
|
212
172
|
"in a moment, and if the workspace still disagrees with what the command " +
|
|
213
173
|
"wrote, say so in your result rather than working around it.)");
|
|
214
174
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Why a command has nothing to show for itself.
|
|
217
|
-
*
|
|
218
|
-
* {@link renderResult} states a non-`completed` status on its verdict line;
|
|
219
|
-
* {@link computerExec} has no verdict line, only the four fields its caller
|
|
220
|
-
* branches on. A *killed* process writes nothing, so without this a `git clone`
|
|
221
|
-
* that hit the ten-minute ceiling reaches the model through
|
|
222
|
-
* `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
|
|
223
|
-
* like a bug in the plugin rather than a limit it can work within.
|
|
224
|
-
*
|
|
225
|
-
* Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
|
|
226
|
-
* command's own stderr is the better explanation and this would be noise on top
|
|
227
|
-
* of it.
|
|
228
|
-
*
|
|
229
|
-
* Both plausible causes are named because the exit code cannot separate them:
|
|
230
|
-
* 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
|
|
231
|
-
* container that ran out of memory.
|
|
232
|
-
*/
|
|
233
|
-
export function cancelledNote(status, exitCode, timeoutMs) {
|
|
234
|
-
if (status !== "cancelled")
|
|
235
|
-
return undefined;
|
|
236
|
-
return (`the command was killed rather than exiting on its own (exit ${exitCode}), so ` +
|
|
237
|
-
`anything it had not yet written is gone. The two usual causes are the ` +
|
|
238
|
-
`per-command time limit — ${humanMs(timeoutMs)} here — and the container ` +
|
|
239
|
-
`running out of memory.`);
|
|
240
|
-
}
|
|
@@ -55,7 +55,7 @@ import { z } from "zod";
|
|
|
55
55
|
import { withAbort } from "@dynamicagents/core";
|
|
56
56
|
import { renderResult } from "./render.js";
|
|
57
57
|
import { execLostNote } from "./gate.js";
|
|
58
|
-
import { withShellTranscript } from "
|
|
58
|
+
import { withShellTranscript } from "../workspace/shell.js";
|
|
59
59
|
/** Running one command in the container. */
|
|
60
60
|
/**
|
|
61
61
|
* Wait for a disposable resource without outliving `signal`.
|
|
@@ -85,7 +85,7 @@ async function killLate(handle) {
|
|
|
85
85
|
export function execTools(ctx) {
|
|
86
86
|
const { config, cwd, timeoutMs, maxChars, workspace, awaitAdvisories, definedEnv } = ctx;
|
|
87
87
|
return {
|
|
88
|
-
|
|
88
|
+
bash: tool({
|
|
89
89
|
/**
|
|
90
90
|
* States what the tool guarantees, rather than what it might withhold.
|
|
91
91
|
*
|
|
@@ -131,7 +131,7 @@ export function execTools(ctx) {
|
|
|
131
131
|
* held, how long the command took, what it exited with — answer it
|
|
132
132
|
* directly.
|
|
133
133
|
*
|
|
134
|
-
* Timed around the gate as well as the command, since
|
|
134
|
+
* Timed around the gate as well as the command, since an agent blocked
|
|
135
135
|
* waiting for `npm ci` and one running a slow test suite are
|
|
136
136
|
* indistinguishable from the outside and want opposite fixes.
|
|
137
137
|
*/
|
|
@@ -143,15 +143,14 @@ export function execTools(ctx) {
|
|
|
143
143
|
* where the call stopped. A command that outran the limit once will again;
|
|
144
144
|
* one that never started because the workspace did not answer may not.
|
|
145
145
|
*
|
|
146
|
-
* `
|
|
147
|
-
* and this is why:
|
|
148
|
-
* still be running
|
|
149
|
-
* has to arrive in, and it covers sending the kill below.
|
|
146
|
+
* `bash` reads its call's signal rather than leaving the wait to Think,
|
|
147
|
+
* and this is why: an abandoned wait can say only that the command may
|
|
148
|
+
* still be running, and the kill below is what stops it.
|
|
150
149
|
*/
|
|
151
150
|
const stopped = (gateMs, started) => {
|
|
152
151
|
const timedOut = abortSignal?.reason?.name ===
|
|
153
152
|
"TimeoutError";
|
|
154
|
-
console.info("[computer]
|
|
153
|
+
console.info("[computer] bash stopped", {
|
|
155
154
|
command,
|
|
156
155
|
gateMs,
|
|
157
156
|
durationMs: Date.now() - startedAtMs - gateMs,
|
|
@@ -164,9 +163,9 @@ export function execTools(ctx) {
|
|
|
164
163
|
? "the command was stopped: it ran past this call's time limit. Anything it changed before then is still changed. Try something narrower."
|
|
165
164
|
: "the command did not run: this call reached its time limit while the workspace was still getting ready, so nothing was changed. Try again; if it happens again, the workspace is not responding.";
|
|
166
165
|
};
|
|
167
|
-
// Only `
|
|
166
|
+
// Only `bash` waits on an install. The file tools read and write
|
|
168
167
|
// source, which is in the workspace and unaffected by an install in
|
|
169
|
-
// flight — blocking them would stop the
|
|
168
|
+
// flight — blocking them would stop the agent doing the reading it
|
|
170
169
|
// could usefully do while it waits. They do consult `writeGate`, which
|
|
171
170
|
// is a different question: not "is the tree ready" but "does a write
|
|
172
171
|
// survive at all".
|
|
@@ -182,7 +181,7 @@ export function execTools(ctx) {
|
|
|
182
181
|
}
|
|
183
182
|
const gateMsWaited = Date.now() - startedAtMs;
|
|
184
183
|
if (gate.block) {
|
|
185
|
-
console.info("[computer]
|
|
184
|
+
console.info("[computer] bash blocked by a workspace advisory", {
|
|
186
185
|
command,
|
|
187
186
|
gateMs: gateMsWaited
|
|
188
187
|
});
|
|
@@ -224,7 +223,7 @@ export function execTools(ctx) {
|
|
|
224
223
|
// process are two acts. Disposing the handle is neither — it releases
|
|
225
224
|
// this side's attachment and leaves the command running.
|
|
226
225
|
const result = await withAbort(abortSignal, handle.result(), () => handle.kill("SIGTERM"));
|
|
227
|
-
console.info("[computer]
|
|
226
|
+
console.info("[computer] bash", {
|
|
228
227
|
command,
|
|
229
228
|
exitCode: result.exitCode,
|
|
230
229
|
// Split so a slow command and a slow *wait* never look alike.
|
|
@@ -248,7 +247,7 @@ export function execTools(ctx) {
|
|
|
248
247
|
if (abortSignal?.aborted)
|
|
249
248
|
return note(stopped(gateMsWaited, started));
|
|
250
249
|
const lost = execLostNote(err);
|
|
251
|
-
console.warn("[computer]
|
|
250
|
+
console.warn("[computer] bash failed", {
|
|
252
251
|
command,
|
|
253
252
|
gateMs: gateMsWaited,
|
|
254
253
|
durationMs: Date.now() - startedAtMs - gateMsWaited,
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ToolSet } from "ai";
|
|
2
2
|
import type { ComputerContext } from "./context.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Editing a file in place — the one file tool this plugin keeps. Think's
|
|
5
|
+
* `read`, `write` and `delete` reach the workspace through the agent's own
|
|
6
|
+
* `this.workspace`, which is `computerWorkspace` (`./proxy.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Not Think's `edit`, because that one is two calls through the workspace — a
|
|
9
|
+
* `readFile`, then a `writeFile` — and a lock inside the workspace cannot span
|
|
10
|
+
* the pair. See `./file-lock.ts`.
|
|
11
|
+
*/
|
|
4
12
|
export declare function fileTools(ctx: ComputerContext): ToolSet;
|