feat(sdkserver): stream workspace file writes #13
No reviewers
Labels
No labels
area/engine
clarity/clear
enhancement
security
size/M
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
basepeak-ai/gptscript!13
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/streaming-workspace-write"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of basepeak-ai/bpai#1220 — workspace file writes materialise the file many times over; a single large file OOM-killed a tenant.
What
Engine:
engine.WithStreamedBody(ctx, toolID, contentType, r)gives one HTTP tool, by ID, a reader to send as its request body in place of the body built from its input. Nothing else about the call changes:X-GPTScript-Daemon-TokenX-GPTScript-Envvalues andX-GPTScript-Tool-NameThe body can be sent only once; a second call of the same tool fails rather than sending a drained stream.
Sent()reports whether the call was made. This uses the same context-value pattern asWithToolCategory.sdkserver: new route
POST /workspaces/write-file-stream. The body is a JSON header (thewrite-filefields minuscontents) followed immediately by the raw file bytes. The route runs the workspace tool as usual, with the content base64-encoded on the fly as the streamed body. The workspace daemon already decodes its body as a stream. The route fails if the run returned without sending the body, so a write that never happened can't report success. A workspace tool that is not an HTTP tool falls back to the buffered run.Both write routes now build the tool input with
json.Marshalinstead ofSprintf, so quotes and backslashes in a file path are escaped correctly.History of this branch
The first commit resolved the daemon URL with a blocking run and made the request itself. That skipped the token and the env headers that
runHTTPsends; today's provider ignores them, but a future one might not. The second commit moves the streaming intorunHTTPinstead. The third fixes a regression from the first commit:omitemptyonbodymade empty-file writes fail on the existing route (illegal base64 data at input byte 0), because an HTTP tool with no body argument sends its whole JSON input as the body.Validation
-race:pkg/engine/http_stream_test.go: the stream replaces the body, keeps the content type, tool name and env headers, applies only to its own tool, and is sent at most once.pkg/sdkserver/workspaces_test.go: the header/content split, the escaped tool input (includingbodypresent when empty), and the base64 reader: round-trip, read-error propagation, early close.createRevision=falseandlatestRevision=3. Path, query, header set, daemon-token match and decoded-content SHA-256 are identical. The only difference is chunked transfer encoding on the stream.make test(./pkg/...) and./integration/...pass.golangci-lintreports 32 issues, the same 32 as on main; none are in files this PR touches.Measured
Peak RSS of this process (as the sdkserver) while writing:
write-file(buffered)write-file-streamCompanion PRs: basepeak-ai/go-gptscript#2 (client), basepeak-ai/bpai-tools#360 (smb connector), basepeak-ai/bpai#1234 (pin bump).
Review (fresh-context reviewers) — outcome
Fixed
Read, so there is no goroutine (572b96b).Sent()only said the daemon had answered. The route now also requires the content to have been read to EOF, so a tool that answers success early can't have a cut file reported as written (e065f3a). There is a test with a fake daemon that ignores the body, and a mutation check confirms it.Dismissed
#!sys.daemontool: no such provider exists, and the buffered fallback is the safer default for an unknown tool shape.Follow-up, not in this PR (workspace-provider)
directoryProvider.WriteFile(also s3 and azure) writes the revision bookkeeping before it reads the body. A write that fails mid-upload therefore leavesCurrentIDbumped, and a retry with the oldlatestRevisionIDgets a spurious conflict. This only happens withcreateRevisionon; the knowledge connectors write with it off.io.ReadAllthe body, so on those backends the daemon still holds one copy of the file.Re-validated on the final commit
-race.CI note: this repo's CI is red on
maintoo (private module fetch fails atmake tidy), so these local runs are the evidence.