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, and (for the bridges in deprecated.go) workflow. 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 both *engine.Board and *workflow.Board satisfy — so callers can pass either without conversion while we migrate fully off workflow.
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) deprecated.go v0.3.0 removal queue: NewStreamBridge, NewRunBridge, AgentStepBindings *_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.
- Legacy workflow agent step: see deprecated.go (NewRunBridge, AgentStepBindings — both slated for v0.3.0 removal).
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 AgentStepOptionsdeprecated
- type BindingFunc
- func AgentStepBindings(o AgentStepOptions) []BindingFuncdeprecated
- 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 NewRunBridge(opts RunBridgeOptions) BindingFuncdeprecated
- func NewRunInfoBridge(info agent.RunInfo) BindingFunc
- func NewShellBridge(runner workspace.CommandRunner, opts ...ShellOption) BindingFunc
- func NewStreamBridge(stream workflow.StreamCallback, nodeID string) BindingFuncdeprecated
- func NewToolBridge(reg *tool.Registry, opts ...ToolBridgeOption) BindingFunc
- type Board
- type LLMBridgeOptions
- type LLMRunOptions
- type RunBridgeOptionsdeprecated
- type ShellOption
- type ToolBridgeOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type AgentStepOptions
deprecated
type AgentStepOptions struct {
Board *workflow.Board
// TaskID / RunID mirror workflow.Request fields; RunID overrides board when non-empty.
TaskID string
RunID string
ToolRegistry *tool.Registry
// AllowedTools is passed to WithAllowedToolNames when non-empty (ignored if ToolRegistry is nil).
AllowedTools []string
}
AgentStepOptions selects common bindings for one workflow agent step (Lua/JS), without pulling in LLM streaming (add NewStreamBridge at the call site if needed).
Deprecated: scheduled for removal in v0.3.0 together with AgentStepBindings.
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 AgentStepBindings
deprecated
func AgentStepBindings(o AgentStepOptions) []BindingFunc
AgentStepBindings returns a typical binding set: board, run, expr, optional tools. Caller still supplies config + script.Runtime-specific globals (signal, etc.).
Deprecated: scheduled for removal in v0.3.0. The agent/engine stack does not need this preset — callers compose BuildEnv with the bridges they want directly (typically four lines), and binding combinations vary too much per agent for one preset to be worth maintaining.
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
All channel APIs require an explicit name — scripts must opt into MainChannel by passing "" 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("main", 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 NewRunBridge
deprecated
func NewRunBridge(opts RunBridgeOptions) BindingFunc
NewRunBridge exposes read-only run metadata to scripts as global "run":
- get_run_id() string
- get_task_id() string
Deprecated: scheduled for removal in v0.3.0. Hard-wired to *workflow.Board and the workflow.VarRunID convention. The engine/agent stack carries the same metadata as agent.RunInfo (RunID/TaskID/AgentID/ContextID) — pass it in directly without going through a board key. The replacement bridge will land alongside the rest of the agent-runtime cleanup.
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 legacy NewRunBridge in deprecated.go is kept under its original name to avoid breaking existing callers; the new constructor names its data source (RunInfo) instead. Once the legacy bridge is removed in v0.3.0, this will be renamed to NewRunBridge.
Design choices vs. the deprecated NewRunBridge in deprecated.go:
Pulls metadata from agent.RunInfo directly. No board lookup, no workflow.VarRunID convention key. The information lives on the call stack where it was minted, not in a side-channel string map.
No board reference. The previous bridge accepted a *workflow.Board so it could fall back to reading "__run_id" — pure tech debt from when run state was scattered across the blackboard. The agent runtime hands you the full RunInfo, so the bridge has no reason to know about a board at all.
Exposes AgentID and ContextID in addition to RunID/TaskID. These two fields exist on agent.RunInfo and were not surfaced by the old bridge; 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 NewStreamBridge
deprecated
func NewStreamBridge(stream workflow.StreamCallback, nodeID string) BindingFunc
NewStreamBridge exposes streaming as global "stream" (emit).
Deprecated: scheduled for removal in v0.3.0. Tied to workflow.StreamCallback and currently only consumed by graph/node/scriptnode (which itself rides on workflow's streaming model). Will be replaced once graph migrates off workflow.StreamCallback.
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 RunBridgeOptions
deprecated
type RunBridgeOptions struct {
Board *workflow.Board
// TaskID is optional (e.g. workflow.Request.TaskID).
TaskID string
// RunID, if non-empty, is returned by get_run_id instead of reading the board.
RunID string
}
RunBridgeOptions configures NewRunBridge (workflow / agent metadata on the board).
Deprecated: scheduled for removal in v0.3.0 together with NewRunBridge. See NewRunBridge for the engine/agent-era replacement plan.
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.