file transfer (go): parse images/files, wire directive sink to Rust parity - #345
Merged
Conversation
…arity Bring the Go smooth-operator server to parity with Rust on the file-transfer contract (spec #342): send_message.images[] / files[] attachments and the send_file directive convention on eventual_response.directive. - Regen go/protocol/types_gen.go from the #342 spec: adds Files (and the previously-unregenerated Skill field). Generated file, not hand-edited. - Parse images[] + files[] fail-soft in handleSendMessage (a malformed array is dropped wholesale, never rejecting the turn — matching Rust's from_value(...).ok().unwrap_or_default()). - New TurnContext (go/server/turn_context.go): the Go analog of the Rust ToolProviderContext file-transfer fields. Carries the turn's images + files and a directive sink, attached to the turn's context.Context so a host tool reads it via TurnContextFrom(ctx) (the engine dispatches every tool with that ctx). Directive is last-write-wins, concurrency-safe under ParallelToolCalls. - Drain the directive sink after the turn onto eventual_response.directive (omitted when no tool wrote one — back-compat), mirroring the Rust drain. - Tests: images/files reach a tool, a tool's directive lands on eventual_response, absent-directive omission, fail-soft on malformed images/files, directive last-write-wins. Known gap (documented in turn_context.go): images are NOT attached to the model turn as OpenAI image_url content parts. The pinned Go engine core (smooth-operator-core/go) ChatMessage.Content is a plain string with no multimodal content and no AgentOptions user-images option; unlike Rust's core (ImageContent + with_user_images), that support does not exist in the Go core, which is an external, pinned dependency from a separate repo. Attaching images to the model requires a smooth-operator-core/go release adding multimodal ChatMessage content + AgentOptions.UserImages + openai.go serialization, then a pin bump — out of scope for this single-repo PR. Until then images are surfaced to host tools only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The spec file-transfer contract (#342) added
send_message.files[]and thesend_filedirective convention oneventual_response.directive, but the Go server implemented none of the behavior — it parsedmessageonly and never setdirective. This brings Go to parity with Rust (which already handlesimages+directivefrom #236).Solution
go/protocol/types_gen.go) from the protocol: file-transfer contract (send_message.files[] + send_file directive) #342 spec viascripts/generate-go.sh: addsFiles(and the previously-unregeneratedSkillfield from th-b30a6a: send_message grows an optionalskill— the engine resolves it, not the client #338). Generated file, not hand-edited.images[]+files[]fail-soft inhandleSendMessage. Captured asjson.RawMessageand unmarshalled separately so a malformed array is dropped wholesale rather than failing the whole-frame parse — matching Rust'sfrom_value(...).ok().unwrap_or_default()(Go'sUnmarshalpartially populates a slice on a bad element, so the partial result is discarded on error).TurnContext(go/server/turn_context.go) — the Go analog of the RustToolProviderContextfile-transfer fields. Carries the turn'sImages+Filesand a directive sink, attached to the turn'scontext.Context(withTurnContext). The engine dispatches every tool with that ctx (SmoothAgent.dispatchTool→Tool.Execute), so a host tool reads it viaTurnContextFrom(ctx). Directive is last-write-wins and concurrency-safe underParallelToolCalls.eventual_response.directive(double-nesteddata.data.directive), omitted when no tool wrote one (back-compat) — mirroring the Rustrunner.rsdrain +protocol.rsemit.Known gap — images not attached to the model (documented in
turn_context.go)Spec step "attach images → model turn" is blocked on an external core release, not implemented here. The pinned Go engine core (
smooth-operator-core/go, a separate repo pinned by pseudo-version) hasChatMessage.Content stringwith no multimodal content parts and noAgentOptionsuser-images option, andopenai.goserializescontentas a plain string. Unlike Rust's core (ImageContent+with_user_images), that support simply does not exist in the Go core and cannot be added from this repo. Attaching images to the model requires asmooth-operator-core/gorelease adding multimodalChatMessagecontent +AgentOptions.UserImages+openai.goserialization, then a pin bump. Until then images are surfaced to host tools only (which is itself a spec-required surface — the RustToolProviderContext.imagesfield). Files were never model-bound by design.Tests
go/server/turn_context_test.go: images/files reach a tool viaTurnContext; a tool's directive lands oneventual_response; directive omitted when none written; fail-soft on malformedimages/files(turn still completes, attachments dropped); directive last-write-wins.Verification
Both modules (
go,go/server) — thego.ymlPR lane:gofmt -l .cleango vet ./...cleango build ./...OKgo test ./... -race— 251 passed, no data racesChangeset
None — consistent with prior Go-server PRs (e.g. #324 tool-hook seam), which shipped without one. Changesets in this repo target the JS-published packages; this is a Go-only change (behavior + regenerated Go types), no spec or TS change.
🤖 Generated with Claude Code