Repository navigation
think: work in a @cloudflare/computer Workspace - #2553
mattzcarey wants to merge 4 commits into
Conversation
🦋 Changeset detectedLatest commit: 5f88352 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
✅ agents import sizes: no significant changes ( |
There was a problem hiding this comment.
🔍 Package reference still advertises Shell workspace
The exports and dependency tables still name Think's removed Workspace export and bundled Shell filesystem. Update them to describe the Computer peer dependency.
(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.
| this.workspace = lazyWorkspace( | ||
| async () => | ||
| // SAFETY: The remote Workspace is Computer's Workspace behind an RPC | ||
| // stub. Its published types come from @cloudflare/workspace, which | ||
| // predates the rename, so TypeScript cannot relate the two. | ||
| (await this.#getWorkspace()) as unknown as ThinkWorkspace | ||
| ); |
There was a problem hiding this comment.
🔴 Later turns reuse a disposed workspace
After a turn disposes its workspace stub, lazyWorkspace keeps forwarding Think operations to that stub. Subsequent turns cannot reliably access skills or workspace files.
Learn more
The remote workspace client is a Workers RPC stub. #cleanupTurn disposes it after each turn, while #getWorkspace creates a new stub for the next turn. The resolver in lazyWorkspace runs only once, so its cached workspace remains the disposed stub. A failed #getWorkspace() promise is also cached permanently in #workspaceReady.
Example: The first issue comment completes and disposes client A. A second mention starts in the same ThinkAgent isolate; Think attempts to read a projected skill through client A rather than the newly available client B.
Recommended fix: Keep Think's lazy workspace in sync with stub disposal: reset or replace its resolver cache when disposing a stub, and clear rejected #workspaceReady promises so later turns can retry.
Was this helpful? React with 👍 or 👎 to provide feedback.
| bytes, | ||
| mediaType ?? "application/octet-stream" | ||
| ); | ||
| await files.write(path, bytes); |
There was a problem hiding this comment.
🟡 Evicted images lose readable media types
When files.write stores an AVIF attachment, it discards mediaType. The read tool cannot recognize AVIF bytes, so the model receives an unsupported-binary result instead of the image.
Learn more
Eviction replaces aged media with a file path so the model can reopen the image later. Previously the write supplied the original media type. WorkspaceFiles.write accepts only bytes and never records that type. detectWorkspaceMediaType then sniffs a short list of signatures before returning application/octet-stream; sniffMediaType does not recognize AVIF.
Example: An aged image/avif file part is evicted to an .avif workspace file. Reading that path yields kind: "binary", unsupported: true instead of image content.
Recommended fix: Preserve MIME metadata when writing evicted media, or add reliable media-type detection for supported input formats and honor the extension/original type in the read tool.
Was this helpful? React with 👍 or 👎 to provide feedback.
| } | ||
| const directory = `/${segments.slice(0, firstGlob).join("/")}`; | ||
| const relative = segments.slice(firstGlob).join("/"); | ||
| const matches = await this.#fs.find(directory, relative); |
There was a problem hiding this comment.
🟡 Missing directories abort glob searches
For a pattern under a missing directory, WorkspaceFiles.glob lets fs.find reject with ENOENT. The built-in find and grep tools fail instead of returning empty results.
Learn more
The first non-glob segments become the directory supplied to fs.find. glob handles missing literal paths with stat, but does not handle missing directories before find. Both createFindTool and createGrepTool call glob without catching that failure.
Example: On a workspace with no /workspace/src, a find for src/**/*.ts raises a missing-directory error instead of producing files: [].
Recommended fix: Treat ENOENT from fs.find as an empty match list, while propagating other errors.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // The turn may have written files. Let every open tab refresh its file | ||
| // browser. Best-effort, like the preview below. | ||
| try { | ||
| const directory = await this.parentAgent(AssistantDirectory); | ||
| await directory.workspaceChanged(); | ||
| } catch (err) { | ||
| console.warn("[MyAssistant] Failed to announce workspace change:", err); | ||
| } |
There was a problem hiding this comment.
🟡 Shared file browsers stay stale during turns
When a chat writes files before its turn completes, workspaceChanged waits for onChatResponse. Other open file browsers keep showing stale contents throughout long or paused turns.
Learn more
The file-browser client refreshes only when it receives a workspace-change frame from useChats. Previously the shared workspace sent one on each mutation; now the only notification occurs after the full assistant response. A long-running tool or approval pause can leave a completed write invisible to other chats until the turn finishes.
Example: Chat A writes /notes.txt and then waits for approval on an announcement. Chat B's open file panel still shows no note while the approval is pending.
Recommended fix: Broadcast after workspace-mutating tool results (and other file mutations), or offer an explicit refresh/polling mechanism for the file browser while turns are in progress.
Was this helpful? React with 👍 or 👎 to provide feedback.
| * // cdp.* if env.BROWSER is bound, | ||
| * // executor from env.LOADER | ||
| * execute: createExecuteTool(this) |
| Think's `workspace` is now a `Workspace` from `@cloudflare/computer`. By default Think creates one on the agent's own storage, with no execution backends. To add backends, mounts, or git, override `workspace` with `new Workspace({ storage: this.ctx.storage, ... })`. For a Workspace another Durable Object owns, use `lazyWorkspace(async () => getWorkspace(stub))`. | ||
|
|
||
| Breaking changes: | ||
|
|
||
| - `@cloudflare/computer` is a new peer dependency. Install it alongside `@cloudflare/think`. | ||
| - `Workspace` is no longer re-exported. Import it from `@cloudflare/computer`. | ||
| - `WorkspaceLike` is replaced by `ThinkWorkspace`, which needs only `workspace.fs`. | ||
| - Think writes its own files under `/workspace`. Evicted media goes to `/workspace/attachments/evicted/`, and the fetch tool saves responses to `/workspace/fetched/`. | ||
| - `createExecuteTool(this)` no longer builds `state.*` from the agent's workspace. Pass `state` explicitly. | ||
| - Media eviction and skill projection now work with every workspace. The `writeFileBytes` checks and their warnings are gone. | ||
|
|
||
| Files a `@cloudflare/shell` Workspace stored are not moved. See [Migrating to `@cloudflare/computer`](https://github.com/cloudflare/agents/blob/main/docs/shell/migrating-to-computer.md). |
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
Think's
workspaceis now aWorkspacefrom@cloudflare/computer. By default Think creates one on the agent's own storage, with no execution backends. Agent Skills accept the same shape, and a newlazyWorkspace()covers a Workspace another Durable Object owns.Shell is being deprecated in favour of Computer (#2552):
workspace.fs: media eviction, skills projection, fetch spill and the extension host functions. ThewriteFileByteschecks are gone, so media eviction and skills projection work with every workspace./workspace, Computer's working root.@cloudflare/computeris a new peer dependency.WorkspaceandWorkspaceLikeare no longer exported, andcreateExecuteTool(this)no longer buildsstate.*from the workspace.agent-thinkandexamples/assistantmove tolazyWorkspace(() => getWorkspace(parent)). The assistant's file browsers now refresh after each turn instead of on every write, because Computer has no change events.Tests ran: think workers suite, agents skills and skill runner, the assistant example, and the pi harness. Repo typecheck is clean, apart from the voice examples, whose packages weren't built locally.