-
Notifications
You must be signed in to change notification settings - Fork 523
Expand file tree
/
Copy pathagent.ts
More file actions
187 lines (173 loc) · 7.51 KB
/
Copy pathagent.ts
File metadata and controls
187 lines (173 loc) · 7.51 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
/**
* Assistant — a minimal `@cloudflare/think` chat agent backed by a
* `@cloudflare/computer` VFS.
*
* Think gives the Durable Object a streaming chat protocol, message
* persistence, resumable streams, and the agentic tool loop. This
* example keeps the surface as small as possible: one agent, one
* Workspace, the shared `@cloudflare/computer/tools`, and nothing
* task-specific. You talk to it from a terminal (see `cli/chat.mjs`)
* and it can read, write, and edit files in its workspace and run
* shell commands through either workspace backend.
*
* Wiring:
* - `Think` (via the Durable Object base) hands us the message
* store, agentic loop, and chat protocol.
* - We own a `@cloudflare/computer.Workspace` with two backends:
* a WorkerShellBackend (`"shell"`) for fast just-bash text tooling and
* a CloudflareContainerBackend (`"container"`) for full Linux
* userland through computerd. This mirrors examples/container while
* keeping the chat surface unchanged.
* - Think consumes `workspace.fs` directly, while
* `@cloudflare/computer/tools` provides the complete file and
* `exec` tool set.
*/
import {
type DurableObjectStorageLike,
Workspace,
WorkspaceProxy,
WorkspaceServiceProxy,
type WorkspaceStub,
} from "@cloudflare/computer";
import {
CloudflareContainerBackend,
withWorkspaceContainer,
} from "@cloudflare/computer/backends/container";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";
import { createAITools } from "@cloudflare/computer/tools";
import { Think } from "@cloudflare/think";
import type { ToolSet } from "ai";
import { createWorkersAI } from "workers-ai-provider";
// Re-export so the runtime can build loopback bindings. The
// WorkerShellBackend reaches WorkspaceServiceProxy through
// `ctx.exports.WorkspaceServiceProxy(...)` so the in-isolate shell
// can call back into the host workspace. WorkspaceProxy carries the
// container's outbound /ws egress back to this DO.
export { WorkspaceProxy, WorkspaceServiceProxy };
const MODEL_ID = "@cf/zai-org/glm-5.2";
// Identifies this durable object to both backends: the worker
// backend's loopback Fetcher and the container's egress table route
// back to the Workspace by binding name and id.
function workspaceRef(ctx: DurableObjectState) {
return { binding: "Assistant", id: ctx.id.toString() };
}
// Anchor Think's generic before the mixin so withWorkspaceContainer
// sees a concrete constructor.
class AssistantBase extends Think<Env> {}
export class Assistant extends withWorkspaceContainer(AssistantBase) {
/** We have a dedicated `exec` tool; skip Think's built-in bash. */
override workspaceBash = false;
/** Plenty of budget for a chat turn that reads a few files first. */
override maxSteps = 20;
/**
* Container backend used when `exec` needs a real Linux userland.
* The DO itself owns the container binding through the
* withWorkspaceContainer mixin; CloudflareContainerBackend handles
* startup, outbound egress interception, the /ws upgrade, and the
* capnweb session.
*/
readonly #containerBackend = new CloudflareContainerBackend({
id: "container",
container: () => this,
workspace: workspaceRef(this.ctx),
egress: { mode: "direct" },
});
/**
* Think's workspace, owned outright. The first backend in the list
* is the default, so unqualified exec calls use the fast just-bash
* shell. Passing `{ backend: "container" }` routes a call to computerd in
* the Cloudflare Container.
*/
// The installed Think release still types workspace as its legacy
// root-level filesystem shape. Runtime tools use workspace.fs.
override workspace = new Workspace({
storage: this.ctx.storage as unknown as DurableObjectStorageLike,
backends: [
new WorkerShellBackend({
id: "shell",
loader: this.env.LOADER,
workspace: workspaceRef(this.ctx),
ctx: this.ctx,
}),
this.#containerBackend,
],
}) as Workspace & AssistantBase["workspace"];
/** Forwarded by WorkspaceProxy for computerd's outbound /ws upgrade. */
override async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/ws") {
return this.#containerBackend.handleFetch(request);
}
return super.fetch(request);
}
/**
* Hand out a typed RPC stub to the workspace. The worker backend's
* WorkspaceServiceProxy dispatches to this so the in-isolate shell
* can reach back into the host workspace; WorkspaceProxy uses the
* same method for the container's capnweb egress path.
*/
async __getWorkspaceStub(): Promise<WorkspaceStub> {
await this.workspace.ready();
return this.workspace.stub();
}
override getModel() {
return createWorkersAI({ binding: this.env.AI })(MODEL_ID);
}
override getSystemPrompt(): string {
return [
"You are a helpful assistant with a Cloudflare Workspace as your",
"working directory, rooted at /workspace.",
"",
"Tools, in preference order:",
" - read, ls: inspect the working tree. Prefer these over",
" `exec cat` / `exec ls`.",
" - write, edit: create and modify files. Prefer these over",
" `exec sed` / shell heredocs.",
" - exec: run shell commands. Use the default `shell`",
" backend first: it is just-bash in a Dynamic",
" Worker, cold-starts quickly, and includes `git`",
" (clone / status / diff / log) via the host",
" workspace. Only https:// git URLs are supported.",
" Use backend `container` when you need full Linux",
" userland: npm, node, python, package managers,",
" test runners, or other real binaries.",
"",
"Keep replies concise. Use the tools instead of guessing about",
"files you can read. When an `exec` command fails because the",
"selected backend lacks a tool, retry on a backend whose",
"description covers that command.",
].join("\n");
}
override getTools(): ToolSet {
return createAITools({
workspace: this.workspace,
shell: {
defaultBackend: "shell",
backends: {
shell: {
description:
"just-bash in a Dynamic Worker. Cold-start fast, no " +
"container, no public network. Good for cat / grep / sed / " +
"awk / jq / head / tail / sort / find, quick file " +
"inspection, text transformations, and `git` (clone / " +
"status / diff / log) — the shell registers a built-in " +
"`git` command that forwards to the host workspace, so " +
"network-bound subcommands like `git clone` work even " +
"though the isolate itself has no public network. Only " +
"https:// URLs are supported. Cannot run npm, node, python, " +
"or any binary outside just-bash's built-in command set.",
},
container: {
description:
"Cloudflare Container running computerd over capnweb. Full Linux " +
"userland: npm, node, python, package managers, test " +
"runners, real binaries on $PATH, and public network. Cold " +
"start is much slower because the container must boot; " +
"reach for it when the shell backend can't run the command. " +
"For git itself, prefer the shell backend.",
},
},
},
});
}
}