Documentation
¶
Overview ¶
Package bindings assembles host capabilities into a script.Env for any script.Runtime implementation (jsrt, luart, etc.).
Purpose ¶
bindings does two things: (1) expose Go-side interfaces as script-callable globals/tables; (2) provide common presets to reduce boilerplate. Business policy (who may call what, quotas, auth) belongs to the caller: pass pre-trimmed dependencies or tighten capabilities via Options. VM-agnostic: bindings never parses syntax; Lua/JS are handled by script sub-packages.
Dependency constraint ¶
bindings depends on llm, model, tool, engine. It must never depend on graph: graph composes bindings, not the other way around. The Board surface bindings consume is the structural bindings.Board interface, which *engine.Board satisfies directly.
Layering model ¶
- core: BindingFunc, BuildEnv — language-agnostic assembly primitives.
- atomic bridges: one file per host capability, named New<Domain>Bridge; the injected global is typically the lowercase domain name (board, fs, …).
- expr subsystem: compiled-program LRU cache (see bridge_expr.go), transparent to scripts.
- presets: common combinations as convenience functions; presets express "default assembly" and do not replace caller-level policy.
- LLM bridge (bridge_llm.go + bridge_llm_round.go + llm_marshal.go): drives an LLM round directly via llm.LLMResolver / llm.LLM / tool.Registry — fully self-contained, no host runtime dependency. Returns multimodal-aware structured results to scripts; does NOT write to the board (scripts control data flow explicitly via the board bridge). Supports blocking llm.run() and iterator-based llm.stream() modes; the iterator exposes per-chunk model.Part projections so scripts can branch on text / image / tool_call.
Directory layout ¶
doc.go package-level design notes (this file) core.go BindingFunc, BuildEnv bridge_board.go board variables + typed message channels (engine.Board) bridge_expr.go expr-lang expressions (eval) + LRU program cache bridge_shell.go sandboxed subprocesses (allowlist via ShellOption) bridge_fs.go workspace files bridge_runtime.go sub-script execScript (inherits parent bindings) bridge_llm.go NewLLMBridge facade + LLMRunOptions (script-facing options schema) bridge_llm_round.go in-bridge round driver (resolver + GenerateStream + tool.Registry) llm_marshal.go model.* ⇄ map[string]any projections (multimodal-aware) bridge_tools.go tool.Registry (deny-by-default, explicit allowlist or AllowAll) bridge_run.go run metadata exposed from agent.RunInfo (run/task/agent/context ids) *_test.go table-driven / jsrt integration tests
Global naming convention ¶
Script global names match the name returned by BindingFunc: board, stream, expr, shell, fs, runtime (injected by scriptnode), run, tools, llm. Methods exposed by each bridge use lowercase_underscore style, consistent with existing script conventions.
Integration with hosts ¶
- Graph ScriptNode: composes board+stream+expr+runtime in ExecuteBoard, then appends shell/fs per node type (see graph/node/scriptnode).
- Engine / Agent: build the env directly with BuildEnv and the bridges you need; add NewLLMBridge when LLM access is required, and use NewRunInfoBridge(runInfo) to surface run/task/agent/context ids. There is no global preset — the four lines of BuildEnv are the preset.
Checklist for adding a new bridge ¶
- Are closure-captured dependencies thread-safe and context-cancellable?
- Are defaults least-privilege (e.g. tools deny-by-default, shell recommends allowlist)?
- Are return values stable on the script side (map field names, multi-return mapping in luart/jsrt)?
- Should it be included in a preset? Document which execution paths it is compatible with.
Testing conventions ¶
Integration tests use jsrt to execute small script snippets that verify bindings; pure Go logic (e.g. LRU) is covered by *_test.go unit tests.
Index ¶
- func BuildEnv(ctx context.Context, config map[string]any, fns ...BindingFunc) *script.Env
- func RuntimeBinding(ctx context.Context, rt script.Runtime, parentBindings map[string]any) map[string]any
- type BindingFunc
- func NewBoardBridge(board Board) BindingFunc
- func NewExprBridge() BindingFunc
- func NewFSBridge(ws workspace.Workspace) BindingFunc
- func NewHostBridge(host engine.Host, source string) BindingFunc
- func NewLLMBridge(opts LLMBridgeOptions) BindingFunc
- func NewRunInfoBridge(info agent.RunInfo) BindingFunc
- func NewShellBridge(runner workspace.CommandRunner, opts ...ShellOption) BindingFunc
- func NewToolBridge(reg *tool.Registry, opts ...ToolBridgeOption) BindingFunc
- type Board
- type LLMBridgeOptions
- type LLMRunOptions
- type ShellOption
- type ToolBridgeOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type BindingFunc ¶
BindingFunc creates a named binding for script execution. The returned name becomes the global variable name in the script scope, and the value is typically a map[string]any of callable Go functions.
func NewBoardBridge ¶
func NewBoardBridge(board Board) BindingFunc
NewBoardBridge exposes board state as the global "board".
Vars (control variables, untyped):
- getVar(key) → any
- setVar(key, val)
- getVars() → map[string]any
- hasVar(key) → bool
Channels (typed conversation history; multimodal-aware via the model.Message projection in bridge_llm_marshal.go):
- channel(name) → []messageMap (read; never returns null)
- setChannel(name, msgs) → throws on validation errors
- appendChannel(name, msg) → throws on validation errors
Constants:
- MAIN_CHANNEL — the engine's reserved default channel name; scripts should reference this rather than hard-coding the literal string so future renames do not break existing scripts.
All channel APIs require an explicit name — scripts must opt into MainChannel by passing board.MAIN_CHANNEL themselves. This avoids accidentally stitching unrelated conversations together via an implicit default.
func NewExprBridge ¶
func NewExprBridge() BindingFunc
NewExprBridge exposes expr-lang as global "expr" (eval).
func NewFSBridge ¶
func NewFSBridge(ws workspace.Workspace) BindingFunc
NewFSBridge exposes workspace file ops as global "fs".
func NewHostBridge ¶ added in v0.2.3
func NewHostBridge(host engine.Host, source string) BindingFunc
NewHostBridge exposes the engine.Host control plane to scripts as the global "host". It is the script-side mirror of the small interfaces composed in engine.Host (Publisher, Interrupter, UserPrompter, Checkpointer, UsageReporter).
Script-facing API (every method returns nil / "" on a NoopHost so scripts can call it unconditionally):
host.publish(subject, payload) -> nil | error
host.checkInterrupt() -> {cause, detail} | null
host.askUser({ parts, schema, source, metadata })
-> { parts, metadata }
host.reportUsage({ input, output, total })
-> nil
Checkpointing is intentionally NOT exposed: scripts have no access to the executing engine's ExecID / Board snapshot / Step marker, so a "host.checkpoint(payload)" call would be ambiguous at best and accidentally destructive at worst. If a future use case needs script initiated checkpoints, it should land as its own typed API rather than a generic bag.
Source labels diagnostic strings (errors carry it, future tracing may too); it mirrors the bridge_llm.LLMBridgeOptions.Source convention.
Interrupt latching: the first interrupt the bridge observes is cached inside the closure so subsequent host.checkInterrupt() calls keep returning the same {cause, detail} for the lifetime of the bridge instance. This matches how scripts naturally consume the signal — "have I been told to stop?" — instead of forcing them to either save the value at first sight or risk losing it on a re-poll.
The bridge does NOT own the engine.Host; the caller (typically scriptnode.ScriptNode) feeds it whatever ctx.Host the executor installed and reuses the same Host instance for all bindings.
func NewLLMBridge ¶
func NewLLMBridge(opts LLMBridgeOptions) BindingFunc
NewLLMBridge exposes LLM calls to scripts as the global "llm":
llm.run() // blocking, returns full result map
llm.run({ model, temperature, ... }) // with per-call overrides
llm.stream() // returns iterator { next, part, text, finish, close }
llm.stream({ model, ... }) // streamed, with per-call overrides
Iterator usage (multimodal-friendly):
var s = llm.stream({ model: "..." });
while (s.next()) {
var p = s.part(); // map projection of model.Part
if (p.type === "text") write(p.text);
if (p.type === "image") show(p.image.url);
}
var r = s.finish(); // round result map
board.setVar("answer", r.content);
board.setChannel(board.MAIN_CHANNEL, r.messages);
Neither mode writes to the board; the script controls what to do with results (typically via the board bridge: board.setVar / board.setChannel). See LLMRunOptions for the per-call schema.
func NewRunInfoBridge ¶ added in v0.2.3
func NewRunInfoBridge(info agent.RunInfo) BindingFunc
NewRunInfoBridge exposes read-only run metadata to scripts as global "run", sourcing every field from a single agent.RunInfo value.
Script-facing API:
run.get_run_id() string // agent.RunInfo.RunID run.get_task_id() string // agent.RunInfo.TaskID run.get_agent_id() string // agent.RunInfo.AgentID run.get_context_id() string // agent.RunInfo.ContextID
All getters return the empty string when the corresponding field on agent.RunInfo is unset; scripts can do `if (!run.get_task_id()) { … }` to branch on absence without needing a separate "has_*" probe.
Naming: the constructor advertises its data source (RunInfo) rather than using the bare "Run" prefix. The previous workflow-coupled NewRunBridge was retired in v0.3.0; this is now the only run-metadata bridge in the package.
Design choices:
Pulls metadata from agent.RunInfo directly. No board lookup, no VarRunID convention key. The information lives on the call stack where it was minted, not in a side-channel string map.
No board reference. Run identity and board state are independent concerns; the bridge has no reason to know about the board.
Exposes AgentID and ContextID in addition to RunID/TaskID — scripts that route on multi-agent or multi-conversation identity need them.
Takes the value (not a pointer). RunInfo is a small immutable descriptor; copying it into the closure is cheaper and safer than carrying a pointer into script-controlled territory.
Typical wiring at the agent.Run boundary:
env := bindings.BuildEnv(ctx, scriptCfg,
bindings.NewBoardBridge(eng.Board()),
bindings.NewRunInfoBridge(runInfo),
bindings.NewExprBridge(),
)
func NewShellBridge ¶
func NewShellBridge(runner workspace.CommandRunner, opts ...ShellOption) BindingFunc
NewShellBridge exposes shell execution as global "shell".
func NewToolBridge ¶
func NewToolBridge(reg *tool.Registry, opts ...ToolBridgeOption) BindingFunc
NewToolBridge exposes tool execution to scripts as global "tools":
- call(name, argumentsJSON) -> { content, is_error, tool_call_id }
- list() -> []string (names the script is allowed to call)
Security: by default no tool is callable until WithAllowedToolNames or WithToolAllowAll is set.
type Board ¶ added in v0.2.3
type Board interface {
GetVar(key string) (any, bool)
SetVar(key string, value any)
Vars() map[string]any
Channel(name string) []model.Message
SetChannel(name string, msgs []model.Message)
AppendChannelMessage(name string, msg model.Message)
}
Board is the structural contract NewBoardBridge requires of any blackboard-shaped object. *engine.Board satisfies it directly; the legacy *workflow.Board also fits the same shape, which is what lets existing callers keep compiling during the v0.3.0 transition without the bridge taking on a host dependency.
Method set rationale:
- GetVar / SetVar / Vars: plain control-variable access.
- Channel / SetChannel / AppendChannelMessage: typed message channels — needed because LLM rounds keep multimodal conversation history in channels, not vars. Scripts need the ability to read the current history, replace it after a round (setChannel with r.messages), and append individual user / assistant turns (appendChannel).
type LLMBridgeOptions ¶
type LLMBridgeOptions struct {
Resolver llm.LLMResolver
Registry *tool.Registry
// Defaults are merged with the script-supplied overrides on every
// llm.run() / llm.stream() call. Any field the script omits is
// inherited from here; explicit script values win.
Defaults LLMRunOptions
// Source labels diagnostics (errors, future tracing). It is the
// bridge equivalent of "node id" / "event id" in legacy code.
Source string
// ReadMessages returns the conversation history to send to the
// LLM. Called once per script invocation so the script can mutate
// the board between rounds and have the next call see the update.
// When nil, an empty slice is used.
ReadMessages func(ctx context.Context) []model.Message
}
LLMBridgeOptions configures NewLLMBridge.
The bridge is the only consumer of these options at the moment, so the surface stays minimal: a resolver to materialize the LLM, an optional tool registry for function-calling, the per-call defaults the script can override, a source label for diagnostics, and a hook for the bridge to read the conversation history at call time.
Notably absent — these were intentional decisions during the "B-track" refactor:
- No llm.RoundConfig: scripts and the bridge interior speak the bridge's own LLMRunOptions / roundOptions types end-to-end.
- No Go-side OnEvent hook: scripts pull chunks via the iterator returned by stream(); a host-side event subscription will be added only when a real consumer needs it.
type LLMRunOptions ¶ added in v0.2.3
type LLMRunOptions struct {
Model string `json:"model,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
MaxTokens int64 `json:"max_tokens,omitempty"`
JSONMode *bool `json:"json_mode,omitempty"`
Thinking *bool `json:"thinking,omitempty"`
Tools []string `json:"tools,omitempty"`
}
LLMRunOptions is the strongly-typed object that scripts pass to llm.run() / llm.stream().
All fields are optional; any unset field inherits the bridge's LLMBridgeOptions.Defaults value. Pointer fields (Temperature, JSONMode, Thinking) distinguish "script omitted" from "script explicitly set to zero/false". Unknown JSON keys are rejected at parse time so script typos surface immediately instead of silently falling back to defaults.
Script-side schema (JS / Lua):
llm.run({
model: "openai/gpt-4o-mini", // optional, string
temperature: 0.2, // optional, number
max_tokens: 1024, // optional, integer
json_mode: true, // optional, bool
thinking: false, // optional, bool
tools: ["web_search"], // optional, []string
})
type ShellOption ¶
type ShellOption func(*shellConfig)
ShellOption configures a shell bridge.
func WithAllowedCommands ¶
func WithAllowedCommands(cmds ...string) ShellOption
WithAllowedCommands restricts the shell bridge to only execute the specified commands. When set, any command not in the list is rejected.
type ToolBridgeOption ¶
type ToolBridgeOption func(*toolBridgeConfig)
ToolBridgeOption configures NewToolBridge.
func WithAllowedToolNames ¶
func WithAllowedToolNames(names ...string) ToolBridgeOption
WithAllowedToolNames restricts script-visible tools; names must match registry entries.
func WithToolAllowAll ¶
func WithToolAllowAll() ToolBridgeOption
WithToolAllowAll allows calling any tool registered in the registry. Use only when scripts are fully trusted.