Code Mode
pkg/codemode is a Go port of the TypeScript AI SDK's @ai-sdk/code-mode
package. It lets a model write JavaScript that calls host tools
programmatically instead of making one tool call per model turn ("code
mode"). The model's source runs in an isolated QuickJS sandbox compiled to
WebAssembly and executed with wazero — no cgo
required.
Experimental: this package is experimental, mirroring the TypeScript
package's own experimental_ naming convention on every exported entry
point. Its API may change in a minor version. Go names drop the
experimental_ prefix.
See Known differences: code-mode never-settling promises and in-flight limits for the behavior differences from the TypeScript (V8-based) runtime.
Package
import "github.com/digitallysavvy/go-ai/pkg/codemode"
Entry points
RunCodeMode
func RunCodeMode(ctx context.Context, input RunInput) (interface{}, error)
Runs code-mode JavaScript directly, without wrapping it as an AI SDK tool.
The source runs as the body of an async function (top-level await/return
are supported) inside a fresh QuickJS sandbox with input.Options'
execution limits applied. Returns the sandboxed program's JSON-decoded
return value on completion, a *Interrupt (as the returned interface{},
with a nil error) if execution paused on a host tool call, or an error.
type RunInput struct {
JS string
Tools ToolSet // map[string]types.Tool, reachable as tools.<name>(input)
ToolExecutionOptions *types.ToolExecutionOptions
Options *Options
Continuation *Continuation // resumes a prior interrupt
InterruptResolution *InterruptResolution // resolution for Continuation's next pending interrupt
}
CreateCodeModeTool
func CreateCodeModeTool(tools ToolSet, options Options) types.Tool
Creates an AI SDK types.Tool (name "codeMode") that executes code-mode
JavaScript in an isolated sandbox, with tools reachable inside the
sandbox as tools.<name>(input). Drop the result straight into
GenerateTextOptions.Tools.
CodeModeTool (tool caller)
func CodeModeTool(options ToolCallerOptions) types.Tool
Creates a code-mode tool for use with ai.ExperimentalToolCallers, so other
tools can invoke code-mode as a caller rather than the model calling it
directly.
Messages a tool caller adds to the conversation (for example the code-mode
tool catalog with ToolDiscoveryConversation) persist across steps in
GenerateText, StreamText and ToolLoopAgent, and PrepareStep's
InitialMessages stays the raw prompt, as in TS.
TypeScript in model-written code
Before running, code-mode strips TypeScript annotations from the source with
Node stripTypeScriptTypes semantics, as TS code-mode does:
- type annotations, generics (including generic arrow functions such as
<T,>(x: T) => x) and default type parameters asandsatisfiesexpressions, and non-null assertions (x!)interfaceandtypedeclarations,import type/export type- class modifiers (
public,private,protected,readonly,override,declare,abstract),declarestatements, and a function'sthisparameter
Syntax that has no type-only erasure, such as enum, isn't rewritten. If
stripping fails for any reason, the source runs unchanged, and the
JavaScript engine reports a syntax error if it isn't valid JavaScript.
Plain JavaScript is never modified.
Tool catalog order
The tool catalog and prompt that code-mode generates list tools in
declaration order, matching TS. With CodeModeTool and
ai.ExperimentalToolCallers, that is the order of the tools passed to the
call: types.ToolCallerDefinition.Bind and PrepareModelMessage receive
an ordered []types.Tool. CreateCodeModeTool, BuildCodeModeToolDescription
and BuildCodeModeToolCatalogMessage take a ToolSet (a Go map, which has
no order), so they list tools alphabetically.
Interrupts and approvals (Code-mode part 2)
Host tool calls made from inside the sandbox can pause execution (an interrupt) or require approval before running. These functions manage that lifecycle; see the package's Go doc comments for full semantics.
func RequestCodeModeInterrupt(payload InterruptPayload) error
func IsCodeModeInterrupt(value interface{}, security ...ContinuationSecurityOptions) bool
func GetCodeModeInterrupt(result interface{}, security ...ContinuationSecurityOptions) *Interrupt
func UnwrapCodeModeResult(result interface{}, security ...ContinuationSecurityOptions) UnwrappedResult
func ContinueCodeModeInterrupt(ctx context.Context, interrupt Interrupt, resolution interface{}, tools ToolSet, options *Options, toolExecutionOptions *types.ToolExecutionOptions) (interface{}, error)
func IsCodeModeApprovalInterrupt(value interface{}, security ...ContinuationSecurityOptions) bool
func ContinueCodeModeApproval(ctx context.Context, interrupt ApprovalInterrupt, response ApprovalResponse, tools ToolSet, options *Options, toolExecutionOptions *types.ToolExecutionOptions) (interface{}, error)
func GetCodeModeApprovalResponse(messages []types.Message, interrupt ApprovalInterrupt) *ApprovalResponse
func ToCodeModeApprovalMessages(interrupt ApprovalInterrupt) []types.Message
func SetCodeModeContinuationSigningKey(key []byte, maxAge time.Duration) error
Concurrent approvals (Promise.all)
With Options.Approval set to &ApprovalOptions{Mode: ApprovalModeInterrupt},
tool calls that need approval and start concurrently (for example inside one
Promise.all) are batched, as in TS. Each such call stays pending until the
sandbox's job queue is idle, and then all of them are returned together:
RunCodeMode(or the tool) returns an approval*InterruptwhoseContinuation.PendingInterruptionslists every call in the batch.- Resolve the entries one at a time with
ContinueCodeModeApproval, passingApprovalResponse{ApprovalID: interrupt.InterruptID, Approved: ...}. Each call returns the next pending*Interruptuntil the batch is complete. - No tool in the batch runs until every entry is resolved. If any entry is
denied, none of them run, and the result is a
*ToolApprovalDeniedError.
Calls that don't need approval run immediately and aren't part of the
batch. MaxInFlightBridgeRequests counts the calls in a batch. A promise
that never settles while no tool call is pending fails fast with a
*ProtocolError instead of waiting for the execution timeout.
Continuations are signed (HMAC-SHA256, TS-compatible canonical envelope) so
they can safely round-trip through client-visible state between requests.
SetCodeModeContinuationSigningKey sets the process-wide signing key; a
random key is generated at process start if it's never called.
Vendored dependency
pkg/codemode vendors a patched copy of fastschema/qjs v0.0.6 at
pkg/internal/third_party/qjs (MIT license) to fix two memory-read bugs
and to add a job-queue quiescence hook (used to batch concurrent approval
requests) and an option that disables module imports in the engine (code-mode
sandboxes run with it on, so import() and the native qjs:* modules are
unavailable). qjs.wasm is rebuilt from pinned, checksum-verified sources by
pkg/internal/third_party/qjs/build/build.sh.
github.com/tetratelabs/wazero is a direct dependency.