Documentation
¶
Overview ¶
Package codemode lets an LLM agent orchestrate its own tools from a JavaScript program instead of one tool call per turn.
The model submits a program as a single tool call. Inside it, `await tools.name(args)` calls the tools the host bound, `Promise.all` fans them out in parallel, and ordinary JavaScript merges, filters and scores the results. Only what the program returns or console.logs goes back into the conversation — the intermediate results stay in the process.
Two things come out of that. Independent calls actually run concurrently instead of waiting for the model to emit another batch, and a fan-out over fifty search hits costs one tool result instead of fifty.
Layers ¶
Run is the engine: a program, a set of [Binding]s, a set of Limits. It knows nothing about agents, prompts or tool schemas.
NewTool wraps the engine into a tool you can hand to a model: a name, a description that states which tools a program may call, a JSON Schema for the arguments, and a Call method taking the raw argument JSON. Framework adapters live in ./adapters and are separate modules.
Prompt returns the system-prompt section describing how to write a program against the bound tools.
Sandboxing ¶
The program runs in-process on goja. Capability omission is the real boundary: a program sees `tools`, `ToolCallError`, `console`, `sleep` and the JavaScript built-ins, and nothing else — no filesystem, no network, no require, no process. Every external effect goes through a binding the host chose to pass in.
The resource limits are tripwires, not a hard sandbox. goja has no per-VM memory ceiling and no instruction counter, so the wall clock, the compute budget and the memory trip line are enforced by a 50ms sampler and goja's Interrupt. They catch hot loops and allocation bombs in well under a second. They do not make the VM safe against an adversary who controls the program and is willing to spend real effort on it.
Example ¶
A program calls tools, merges what comes back, and returns a digest. The nineteen candidates it looked at and threw away never reach the model.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/gtoxlili/codemode-go"
)
func main() {
fetch := codemode.Binding{
Name: "fetch_profile",
Invoke: func(_ context.Context, args string) (string, error) {
var a struct {
Handle string `json:"handle"`
}
if err := json.Unmarshal([]byte(args), &a); err != nil {
return "", err
}
return fmt.Sprintf(`{"handle":%q,"followers":%d}`, a.Handle, len(a.Handle)*1000), nil
},
}
res, failure := codemode.Run(context.Background(), `
const handles = ["ada", "grace", "barbara"];
const profiles = await Promise.all(handles.map(h => tools.fetch_profile({handle: h})));
const top = profiles.sort((a, b) => b.followers - a.followers)[0];
console.log("checked " + profiles.length + " profiles");
return {winner: top.handle, followers: top.followers};
`, []codemode.Binding{fetch}, codemode.DefaultLimits())
if failure != nil {
fmt.Println("failed:", failure)
return
}
out, _ := json.Marshal(res.Result)
fmt.Println(res.Logs[0])
fmt.Println(string(out))
}
Output: checked 3 profiles {"followers":7000,"winner":"barbara"}
Example (PartialFailure) ¶
A failed tool call rejects with a ToolCallError the program can catch, so one dead source does not throw away the work already done.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/gtoxlili/codemode-go"
)
func main() {
ok := codemode.Binding{
Name: "search_docs",
Invoke: func(context.Context, string) (string, error) { return `{"hits":["a","b"]}`, nil },
}
broken := codemode.Binding{
Name: "search_web",
Invoke: func(context.Context, string) (string, error) { return "", fmt.Errorf("rate limited (429)") },
}
res, failure := codemode.Run(context.Background(), `
const out = {hits: [], failed: []};
for (const name of ["search_docs", "search_web"]) {
try {
const r = await tools[name]({query: "changelog"});
out.hits.push(...r.hits);
} catch (e) {
if (!(e instanceof ToolCallError)) throw e;
out.failed.push(e.toolName);
}
}
return out;
`, []codemode.Binding{ok, broken}, codemode.DefaultLimits())
if failure != nil {
fmt.Println("failed:", failure)
return
}
out, _ := json.Marshal(res.Result)
fmt.Println(string(out))
}
Output: {"failed":["search_web"],"hits":["a","b"]}
Index ¶
- Constants
- func Prompt(opts PromptOptions) string
- func ReturnShape[T any]() string
- func ReturnShapeOf(t reflect.Type) string
- func Run(ctx context.Context, code string, bindings []Binding, limits Limits) (Result, *Failure)
- func TailLogs(lines []string, budget int) string
- type Binding
- type Blocked
- type CallEvent
- type Failure
- type FailureKind
- type Limits
- type Options
- type Phase
- type PromptOptions
- type Result
- type Tool
- func (t *Tool) Call(ctx context.Context, argsJSON string) (string, error)
- func (t *Tool) Description() string
- func (t *Tool) Name() string
- func (t *Tool) Parameters() map[string]any
- func (t *Tool) ParametersJSON() json.RawMessage
- func (t *Tool) Run(ctx context.Context, code string) (Result, *Failure)
Examples ¶
Constants ¶
const DefaultToolName = "run_code"
DefaultToolName is the tool's name unless Options.Name overrides it.
Variables ¶
This section is empty.
Functions ¶
func Prompt ¶
func Prompt(opts PromptOptions) string
Prompt returns the system-prompt section for the tool: how to call tools from a program, what a call resolves to, what a failed call rejects with, what runs in parallel, and that only the return value and console output come back.
It is written in the second person and formatted as a "# Heading" block, the usual shape of an agent system prompt section.
func ReturnShape ¶
ReturnShape renders a compact hint at the shape of T, e.g. `{hits: [{path, line: num, snippet}], total: num, next?}`.
Tool-calling protocols ship the input schema and nothing about the output, so a model writing `r.data.hits` is guessing. The hint is meant for the tail of a tool description:
desc += "\n\nReturns `{data: " + codemode.ReturnShape[SearchResult]() + "}`."
PromptOptions.ReturnShapes adds the matching notation guide to the prompt section. Because the hint is derived from the type, it tracks the code.
Notation, kept short to cost few tokens: a bare name is a string; anything else is annotated (`num`, `bool`, `timestamp`, `…` for unknown); `?` marks an omitempty field. Depth is capped at 3 and length at 240 characters, truncated with brackets closed so the hint still parses.
Returns the empty string for a type the notation has nothing to say about: an opaque payload (json.RawMessage, []byte, any), or a struct whose every field is opaque.
Example ¶
Append the shape of what a tool returns to its description, so the model navigates results instead of guessing field names five calls deep.
package main
import (
"fmt"
"github.com/gtoxlili/codemode-go"
)
type searchHit struct {
Path string `json:"path"`
Line int `json:"line"`
Snippet string `json:"snippet"`
}
type searchResults struct {
Query string `json:"query"`
Hits []searchHit `json:"hits"`
Truncated bool `json:"truncated"`
Cursor string `json:"cursor,omitempty"`
}
func main() {
fmt.Println("Returns `{data: " + codemode.ReturnShape[searchResults]() + "}`.")
}
Output: Returns `{data: {query, hits: [{path, line: num, snippet}], truncated: bool, cursor?}}`.
func ReturnShapeOf ¶
ReturnShapeOf is ReturnShape for a type you have at runtime.
func Run ¶
Run executes code as the body of an async function — top-level await and return both work — with bindings reachable as tools.Name(args).
Every run gets a fresh VM and no state survives it. Canceling ctx settles the run as FailureAborted and aborts sub-calls that are still in flight.
Three invariants keep this from deadlocking, and all three are load-bearing:
- vm.Interrupt is called directly from the watchdog, never queued onto the event loop. A queued interrupt never runs, because the loop is exactly what is stuck in the hot loop you are trying to kill.
- loop.Terminate runs in the background. It waits for the loop to go idle, so a binding that ignores context cancellation and spins would otherwise hang the caller's request instead of one cleanup goroutine.
- Every path that ends the run goes through one sync.Once. First one wins, the rest are no-ops.
func TailLogs ¶
TailLogs joins the last lines that fit in budget bytes, keeping the tail whole and dropping from the front. Use it to attach output to a failure message: the model needs to see what the program printed just before it died, and that budget is much tighter than the one a successful run gets.
Types ¶
type Binding ¶
type Binding struct {
Name string
Invoke func(ctx context.Context, args string) (string, error)
// Mutating declares that the tool writes something that outlives the call.
// It only matters together with ConflictKeys: two calls are serialized when
// they share a key and at least one of them mutates.
Mutating bool
// ConflictKeys reports which resources this call touches, derived from its
// own arguments — the scheduler does not know any tool's parameter schema,
// so the tool has to answer. Typical keys: a constant for a shared backend,
// "deck:"+id for a resource id, an absolute path for a file.
//
// nil means the call never conflicts with anything and always runs in the
// parallel pool. Panics are absorbed and treated as nil.
ConflictKeys func(args string) []string
}
Binding is one tool a program can call as tools.Name(args).
Invoke receives the run-scoped context — canceled when the run settles, times out or is aborted — and the arguments the program passed, serialized as a JSON object. It returns the tool's result as text. If that text parses as JSON the program sees the parsed value; otherwise it sees the raw string.
A returned error rejects the call's promise with a ToolCallError carrying .toolName, which the program can catch and keep going. A panic is absorbed and turned into the same thing.
func WithCallEvents ¶
WithCallEvents wraps every binding so that on receives a start event before each sub-call and a done or error event after it. Events carry the raw arguments, not a digest.
on is called from the goroutine running the sub-call, so it must be safe for concurrent use, and it runs inline: a slow observer slows the program down.
type Blocked ¶
Blocked names a tool the model can see in its tool list but cannot call from inside a program. It shows up in the tool description's "minus" list, and a program that calls it anyway gets Reason back as the call's error rather than an "unknown tool".
type CallEvent ¶
type CallEvent struct {
Seq int64
Tool string
Args string
Phase Phase
Duration time.Duration
Err error
}
CallEvent is one observation of a sub-call. Seq is the start order within the run, counting from 1. Duration and Err are only set on PhaseDone/PhaseError.
type Failure ¶
type Failure struct {
Kind FailureKind
Message string
}
Failure describes a failed run. Message is written for the model to read and act on, so it says what to do differently rather than what went wrong internally.
type FailureKind ¶
type FailureKind string
FailureKind classifies why a run failed. Each kind maps to one thing the model can do about it, which is the reason a run reports a kind instead of a bare error string: the model is the one reading it.
const ( // FailureException — the program did not compile, threw, or blew the call // stack. Fix the syntax or the logic. FailureException FailureKind = "exception" // FailureTimeout — the wall clock ran out. Narrow the loop, make fewer calls. FailureTimeout FailureKind = "timeout" // FailureComputeLimit — the compute budget ran out. Only time spent actually // running JavaScript counts, so this means the program computed too much, // not that a tool was slow. FailureComputeLimit FailureKind = "compute-limit" // FailureMemoryLimit — heap growth crossed the trip line while the program // was running. Stop building unbounded strings and arrays. FailureMemoryLimit FailureKind = "memory-limit" // FailureResultLimit — accumulated sub-call results crossed their budget. // Select or aggregate in code instead of holding everything. FailureResultLimit FailureKind = "result-limit" // FailureOutputLimit — console output crossed its budget. Print less, return // more. The output collected up to that point is kept. FailureOutputLimit FailureKind = "output-limit" // FailureInvalidReturn — the return value does not survive a JSON round // trip (a function, a cycle). Return plain data. FailureInvalidReturn FailureKind = "invalid-return" // FailureTooManyCalls — the program issued more sub-calls than the limit // allows. Nothing for the model to fix; the run is stopped on purpose. FailureTooManyCalls FailureKind = "too-many-calls" // FailureAborted — the caller canceled. Nothing for the model to fix. FailureAborted FailureKind = "aborted" )
type Limits ¶
type Limits struct {
// WallClock is the hard floor. On expiry the VM is interrupted, which kills
// hot loops and cannot be swallowed by JavaScript try/catch; if the run has
// not settled InterruptGrace later, it is torn down.
WallClock time.Duration
InterruptGrace time.Duration
// CPUBudget only accumulates while the program is running JavaScript — time
// spent waiting on tool calls or sleep does not count. A fan-out over
// twenty slow APIs does not burn it; a `while (true) {}` burns it in
// seconds, long before the wall clock.
CPUBudget time.Duration
// MemoryBudgetBytes trips when process heap growth relative to the run's
// baseline crosses it while the program is running. goja has no per-VM
// memory ceiling (upstream discussion #629), so this is a trip line sampled
// every 50ms, not a hard wall: it is a process-wide number, and concurrent
// allocation elsewhere counts toward it.
MemoryBudgetBytes int64
// ResultBudgetBytes caps the sub-call results a run may accumulate. Unlike
// the memory trip line this is exact — every resolved result is counted as
// it arrives. It is what stops a program from looping a thousand large
// documents into the heap.
ResultBudgetBytes int64
// MaxCallDepth bounds the JavaScript call stack. goja rejects deeper
// recursion with an uncatchable StackOverflowError instead of taking the
// process stack down with it.
MaxCallDepth int
// MaxParallel is the sub-call concurrency pool — the parallelism a
// Promise.all actually gets. MaxSubCalls is the total per run.
MaxParallel int
MaxSubCalls int
// LogBudgetBytes, LogLineRunes and MaxLogLines bound console output.
// Crossing any of them fails the run with FailureOutputLimit and keeps
// what was collected — an explicit failure the model can correct, rather
// than a silent truncation it never learns about.
LogBudgetBytes int
LogLineRunes int
MaxLogLines int
}
Limits is the resource envelope of one run. A zero field falls back to the corresponding DefaultLimits value, so you can override one knob and leave the rest alone.
func DefaultLimits ¶
func DefaultLimits() Limits
DefaultLimits are sized for an agent that calls slow external APIs: ten minutes of wall clock is a realistic ceiling for a fan-out over a few dozen of them, while two minutes of compute is far more than any honest merge or scoring pass needs.
type Options ¶
type Options struct {
// Bindings are the tools a program may call. Required.
Bindings []Binding
// Blocked are tools the model has but a program may not call.
Blocked []Blocked
// Name defaults to [DefaultToolName].
Name string
// Description overrides the generated one. The generated text states which
// tools are callable, derived from Bindings and Blocked; an override does
// not, and is not checked against them.
Description string
// Limits defaults to [DefaultLimits].
Limits Limits
// MaxConcurrentRuns bounds how many programs this tool runs at once,
// defaulting to 4. Each run is one VM plus its own pool of MaxParallel
// sub-calls. Waiting for a slot respects context cancellation.
MaxConcurrentRuns int
// OnCall observes each sub-call. See [WithCallEvents].
OnCall func(ctx context.Context, ev CallEvent)
// OnProgram fires once per run, before the program starts, with the code and
// the model's one-line description of what it does.
OnProgram func(ctx context.Context, code, description string)
// LogTailBytes is how much captured output is attached to a failure.
// Defaults to 8000.
LogTailBytes int
}
Options configures NewTool.
type PromptOptions ¶
type PromptOptions struct {
// ToolName defaults to [DefaultToolName]. Pass whatever you passed to
// [NewTool].
ToolName string
// ReturnShapes states whether the tool descriptions in this deployment end
// with a line giving the shape of what the tool returns, in the notation
// [ReturnShape] produces. When true, the section adds how to read those
// lines; when false it says nothing about them.
ReturnShapes bool
}
PromptOptions shapes the section Prompt returns.
type Result ¶
type Result struct {
Logs []string `json:"logs"`
Result any `json:"result,omitempty"`
HasResult bool `json:"-"`
}
Result is a successful run. HasResult is false when the program returned undefined — it only brought logs back.
type Tool ¶
type Tool struct {
// contains filtered or unexported fields
}
Tool is the run_code tool: a name, a description, an argument schema, and a Call that takes the model's raw argument JSON. The adapters under ./adapters map those onto specific frameworks.
func NewTool ¶
NewTool assembles the tool. The generated description enumerates Bindings and Blocked as they are at this moment, so it is a snapshot: a tool added to the model's tool list afterwards is not in it.
Example ¶
The tool form: a name, a description, an argument schema, and a Call that takes the model's raw argument JSON. Hand those to whatever tool-calling loop you already have.
package main
import (
"context"
"fmt"
"github.com/gtoxlili/codemode-go"
)
func main() {
tool := codemode.NewTool(codemode.Options{
Bindings: []codemode.Binding{{
Name: "count_words",
Invoke: func(context.Context, string) (string, error) { return `{"count":42}`, nil },
}},
Blocked: []codemode.Blocked{{
Name: "ask_user",
Reason: "ask_user ends the turn, so a program cannot call it — ask before you start the program",
}},
})
fmt.Println(tool.Name())
fmt.Println(tool.Description())
out, err := tool.Call(context.Background(),
`{"code":"const r = await tools.count_words({path:'a.md'}); return r.count * 2;","description":"Double a word count"}`)
fmt.Println(out, err)
}
Output: run_code Runs one JavaScript program that orchestrates your tools in batch; only what the program prints or returns comes back to the conversation. The tools callable in a program are exactly those in your current tool list, minus: run_code, ask_user. Reach for it when a batch of calls collapses into one digest — parallel fan-out, chained transforms, filtering bulk results down to what matters; a lone call is cheaper made directly. {"logs":[],"result":84} <nil>
func (*Tool) Call ¶
Call runs a program from the model's raw argument JSON and returns the result as JSON: {"logs": [...], "result": ...}, with result omitted when the program returned nothing.
A failed run comes back as an error whose message carries the failure kind and the tail of whatever the program printed, phrased for the model to read.
func (*Tool) Description ¶
func (*Tool) Parameters ¶
Parameters returns the JSON Schema for the tool's arguments, as a fresh map you may mutate.
func (*Tool) ParametersJSON ¶
func (t *Tool) ParametersJSON() json.RawMessage
ParametersJSON is Tool.Parameters marshaled, for the many APIs that want the schema as bytes.
