Skip to main content

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
  • as and satisfies expressions, and non-null assertions (x!)
  • interface and type declarations, import type / export type
  • class modifiers (public, private, protected, readonly, override, declare, abstract), declare statements, and a function's this parameter

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 *Interrupt whose Continuation.PendingInterruptions lists every call in the batch.
  • Resolve the entries one at a time with ContinueCodeModeApproval, passing ApprovalResponse{ApprovalID: interrupt.InterruptID, Approved: ...}. Each call returns the next pending *Interrupt until 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.

See Also​