feat(sdk): introduce agent + engine runtime, unify graph around engine.Host#45
Merged
Conversation
Add the new agent and engine layers so execution can be driven through a shared host, board, checkpoint, and lifecycle hook contract. Made-with: Cursor
…M facade and run-info bridge Reorient the bindings package around the engine/agent stack so it stops dragging the soon-to-be-removed workflow package into every script-host integration. Active bridges now talk to engine.Board (via a structural bindings.Board interface), llm.LLMResolver, and agent.RunInfo directly; all workflow-bound surface lives in deprecated.go and is scheduled for v0.3.0 removal. Highlights: - LLM bridge rewrite (bridge_llm.go + bridge_llm_round.go + llm_marshal.go): internal round driver replaces llm.RoundConfig / RunRound / StreamRound; exposes script-side llm.run() and iterator-based llm.stream() with multimodal-aware Part projections. Strict LLMRunOptions schema rejects unknown fields. - Board bridge: extends to typed message channels (channel / setChannel / appendChannel) backed by engine.Board, with reverse-marshal validation in llm_marshal.go (unknown fields, type mismatches, missing required fields all surface to scripts as path-prefixed errors). - New bridge_run.go: NewRunInfoBridge(agent.RunInfo) replaces the board-key dance for run/task/agent/context ids. - File renames for consistency: tools.go -> bridge_tools.go, expr.go merged into bridge_expr.go, expr_cache_test.go -> bridge_expr_cache_test.go. - Test reorganization: split monolithic bridge_test.go into bridge_<domain>_test.go files; signal tests moved to sdk/script/jsrt where they belong. Coverage for sdk/script/bindings: ~79% -> 95.2%; every non-deprecated function reaches 100% (parsePart at 98% is a Go cover-tool quirk). llm package cleanup: - Move RoundConfig / RunRound / StreamRound to sdk/llm/deprecated.go with v0.3.0 removal markers (only consumed by the legacy stream bridge). - Consolidate resolver tests into a single resolver_test.go. Made-with: Cursor
* graph.ExecutionContext now carries engine.Host; nodes use it for
publish / interrupts / user prompts instead of bespoke bus paths.
* executor/runner consolidated to a single publisher: WithEventBus is
retained as a deprecated shim that folds the bus into a busOnlyHost
adapter; Runner exposes only r.host internally.
* llmnode: streaming round observes host.Interrupts(); partial assistant
message + synthetic [cancelled by interrupt] tool_results are always
committed before propagating engine.Interrupted.
* scriptnode: signal.interrupt() surfaces as engine.Interrupted with
CauseCustom; new bindings/bridge_host.go exposes
host.{publish,checkInterrupt,askUser,reportUsage} to scripts.
* graph root reorganised: graph.go -> core.go, schema system removed,
knowledge node moved to graph/node/knowledgenode/, llm node moved to
graph/node/llmnode/.
* All overlapping options (WithEventBus / WithStreamCallback /
WithCheckpointStore) and Stream / VarMessages / VarQuery / VarAnswer
marked Deprecated, scheduled for removal in v0.3.0.
Made-with: Cursor
…n map The agent + engine + graph runtime fully supersedes workflow as of v0.2.x. Adding a package-level Deprecated note enables staticcheck SA1019 for any direct workflow import while keeping every existing symbol working until removal in v0.3.0. The doc.go now includes a one-stop migration map covering Board, Runtime, Agent, Strategy, Request/Result, Memory, RunOption, StreamEvent and Task so existing callers can plan their move without spelunking through the new packages. Made-with: Cursor
The package had no consumers anywhere in the workspace (sdk, sdkx, voice, examples, internal, cmd). The agent + engine runtime covers the use cases an actor was originally meant to fill: agent.Run is the single-shot path and engine.Host is the long-lived passive collaborator, neither of which matches the actor mailbox model. Re-introducing a generic actor on demand is cheaper than carrying ~500 lines of unowned code that confuses new contributors about which abstraction they should reach for. Made-with: Cursor
…ne.Host
The previous commit introduced engine.Host.{Checkpointer,UsageReporter}
in name only — the executor still wrote graph-format checkpoints
through cfg.checkpointStore.Save and llmnode never reported per-round
usage to the host, so any user implementing those Host methods saw
no traffic.
* checkpoint: deprecate executor.Checkpoint / executor.CheckpointStore
in favour of engine.Checkpoint / engine.CheckpointStore. The legacy
WithCheckpointStore is now folded into cfg.host via storeOnlyHost
(mirrors busOnlyHost from runner). The executor's main loop calls
host.Checkpoint exclusively. When both WithHost and
WithCheckpointStore are set the host wins (state cannot be safely
fanned out to two backends, unlike events).
* usage: llmnode.writeResults reports the per-round delta via
ctx.Host.ReportUsage. The accumulated total still lands on the
board (VarInternalUsage) for in-graph consumers; the host receives
only the delta because the contract says "each call adds delta".
Zero-usage rounds (e.g. resolver errors) are skipped to avoid
spamming budget enforcers.
* tests: TestExecutor_HostCheckpoint_PreferredOverStore proves host
shadows store when both are configured;
TestExecutor_StoreOnlyHost_ForwardsToStore proves the deprecated
path keeps working through the shim; TestNode_ReportsDeltaUsageToHost
+ companions cover the usage contract including zero-usage skip and
nil-host safety.
Made-with: Cursor
Made-with: Cursor
… runner implements engine.Engine Make graph/runner.Runner the canonical engine.Engine implementation so sdk/agent can drive graphs without the executor adapter dance. * Move sdk/graph/executor → sdk/graph/runner/internal/executor. External callers reach the executor only through runner.Runner; runner/aliases.go re-exports the small surface (PatternRun*, ParallelConfig, MergeStrategy/MergeFunc/RegisterMergeStrategy, VariableResolver/CloneableResolver, WithActorKey) needed to configure runner from outside. * Merge sdk/graph/compiler into sdk/graph: graph.Compile is now a package-level function. Drop the empty Compiler/NewCompiler/ BuildOptions/CompilerOption boilerplate. Tests move to graph_test. * Switch graph/adapter to runner.Runner; DepExecutor becomes a no-op accepted-and-ignored key (the only execution backend is runner now). Adapter test renamed accordingly. * Add sdk/graph/runner/integration_test.go: end-to-end agent.Run + runner.Runner coverage (happy path, host envelope routing, host interrupt classification, run-id propagation). Lives in runner_test to avoid pulling graph imports into the agent test binary. Behaviour-preserving for non-deprecated callers; deprecated knobs (WithEventBus, WithStreamCallback, executor.WithRunID via Run) keep working through the v0.3.0 transition. Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces the next-generation runtime layer (
sdk/agent+sdk/engine) and refactorssdk/graphandsdk/script/bindingsto drive every cross-cutting concern (events, interrupts, user prompts, checkpointing, usage reporting) through a singleengine.Hostinterface. Marks the legacysdk/workflowpackage deprecated with a full migration map. Removes the unusedsdk/actorpackage.This is the first half of the v0.2.x → v0.3.0 migration: the new runtime is fully usable today, and every workflow-bound surface is annotated with
Deprecated:+Scheduled for removal in v0.3.0so downstream code can migrate at its own pace.What changed
New packages
sdk/engine— runtime-agnostic primitives:Host(composed ofPublisher/Interrupter/UserPrompter/Checkpointer/UsageReporter),Board(thread-safe blackboard with vars + channels),Interrupt/InterruptedError,Checkpoint,UserPrompt/UserReply. Comes withenginetest.MockHostfor downstream tests.sdk/agent— application-layer surface:Agent,Run,Decider,Disposition,Observer,Seeder,Request,Result. Replaces the workflowRuntime+Strategy+Memorytriad with a leaner pipeline.sdk/graph/runner— graph assembly + lifecycle, lifted fromgraph/executorso the executor stays a single-purpose engine.runner.New(def, factory, WithHost(h))is the canonical entry point going forward.sdk/graph/node/llmnode— extracted from the old monolithicgraph/node/llm.go. Carries its ownConfig/runRound/vars;runRoundowns the streaming loop, observeshost.Interrupts(), commits partial assistant messages + synthetic[cancelled by interrupt]tool_results on cancellation.sdk/graph/node/knowledgenode— moved out ofsdk/knowledge/node.goso the graph wiring lives next to the rest of the node implementations.sdk/script/bindings/bridge_host.go— exposeshost.publish/host.checkInterrupt/host.askUser/host.reportUsageto scripts.checkInterruptuses a sticky latch so polling scripts see a consistent value.sdk/script/bindings/bridge_llm_round.go+bridge_run.go— modern script-side LLM facade and run-info bridge that don't depend on workflow.Refactors
graph.ExecutionContextcarriesengine.Hostso nodes callctx.Host.{Publish,Interrupts,AskUser,Checkpoint,ReportUsage}directly. The previousStreamcallback field is kept butDeprecated:.runConfigexposes one canonical event sink,cfg.publisher, built once viaresolvePublisher(cfg.host, cfg.bus). The deprecatedWithEventBusis folded in but no longer touched by the main loop.WithCheckpointStoreis folded intocfg.hostviastoreOnlyHost(parallel tobusOnlyHostin the runner). The main loop callshost.Checkpoint(ctx, cp.toEngine())exclusively. When bothWithHostandWithCheckpointStoreare supplied the host wins — checkpointing is state, not observability, so unlike events we don't fan out to two backends.Runnerkeeps onlyr.host.WithEventBus(bus)wraps the bus into abusOnlyHostadapter at construction time soRun()only ever ships one event sink to the executor.graphroot reorganised:graph.go→core.go, schema system removed (UI metadata is no longer SDK-owned),validate.gomerged intoport.go,meta.go+raw.gomerged intodefinition.go,vars.gorewritten to separate engine-produced keys from chat-application conventions (the latter allDeprecated:).scriptnode.signal.interrupt(msg)now surfaces asengine.Interrupted{Cause: CauseCustom, Detail: msg}so the agent layer can recognise the pause viaerrdefs.IsInterruptedand read the script-supplied detail.llmnodereports per-round delta usage viactx.Host.ReportUsage(result.Usage)— the contract is "each call adds delta", so the host receives the round usage and the board still carries the running total inVarInternalUsage.script/bindingswave: bridges decoupled from workflow (board / expr / fs / shell / tools / runtime). Workflow-bound bridges (stream,run,agent_step) collected inbindings/deprecated.go. Newbridge_llm_facadecovers the multimodal LLM path the legacybridge_streamcouldn't.sdk/knowledge/node.go(358 lines) →sdk/graph/node/knowledgenode/. Legacy schema /RegisterNode/KnowledgeConfigFromMapcollapsed intosdk/knowledge/deprecated.gowith no workflow imports.Deprecations
Every legacy surface gets a standard
// Deprecated:godoc withScheduled for removal in v0.3.0and an explicit migration target:sdk/workflow(whole package) —doc.gonow carries a per-symbol migration map covering Board / Runtime / Agent / Strategy / Request / Result / Memory / RunOption / StreamEvent / Task. Enablesstaticcheck SA1019for direct importers.graph/adapter(whole package) — only exists to keepworkflow.Strategycallers compiling against graph definitions.executor.WithEventBus,executor.WithStreamCallback,executor.WithCheckpointStore,executor.Checkpoint,executor.CheckpointStore,runner.WithEventBus,runner.Bus(),graph.ErrInterrupt,graph.ExecutionContext.Stream,graph.VarMessages,graph.VarQuery,graph.VarAnswer,graph.StreamEvent,graph.StreamCallback,llm.RunRoundfamily,bindings.NewStreamBridge,bindings.NewRunBridge,bindings.AgentStepBindings,bindings.PresetBindings.Removed
sdk/actor(~500 lines + tests) — generic actor with zero workspace consumers; removed in favour of the agent/engine model. Re-introducing on demand is cheaper than carrying unowned code.Coverage
Coverage on changed/new packages:
sdk/agentsdk/enginesdk/graph/executorsdk/graph/runnersdk/graph/node/llmnodesdk/graph/node/knowledgenodesdk/script/bindingssdk/graph/nodeNotable new test suites:
agent/run_test.go— full single-shot agent runs incl. retry, observer, decider routingengine/board_test.go+engine/host_test.go+engine/interrupt_test.go— board cloning, host contracts, interrupt classificationengine/enginetest/suite.go— reusable conformance suite for downstreamHostimplementationsgraph/node/llmnode/interrupt_test.go— partial commit + cancelled tool_results invariantsgraph/node/llmnode/host_test.go— delta-usage reporting, zero-usage skip, nil-host safetygraph/executor/checkpoint_test.go— host vs store priority, store-only fallback throughstoreOnlyHostscript/bindings/bridge_host_test.go— publish / sticky checkInterrupt / askUser round-trip / reportUsage parsingVerification
sdkxandvoicebuild and test without changes (novoicemigration in this PR — that's the v0.3.0 prerequisite that finally enables removing workflow).Known follow-ups (intentionally out of scope)
These are tracked as the v0.3.0 work that follows this PR:
voice/pipeline.gomigration toagent— the only non-deprecated workflow consumer left in the repo. Required before workflow can actually be deleted.sdk/llm/deprecated.goworkflow import — keeps a transitive workflow dependency forsdkx/llm/*providers; clearable once thebridge_llm_roundmigration is fully adopted.history↔llmnodeconstants alignment —VarPrevMessageCount/VarSummaryIndex/VarInternalUsageare duplicated as string constants betweenworkflow/keys.goandllmnode/vars.go. They share the same value so today this works, buthistorywill need to switch to thellmnodeconstants when workflow is removed.Test plan
go vet ./...clean acrosssdk,sdkx,voicego test ./... -count=1clean acrosssdk,sdkx,voiceWithHost(h)actually receives every envelope, interrupt observation, AskUser call, Checkpoint, ReportUsageWithEventBus(bus)keeps working through the deprecated pathWithCheckpointStore(s)keeps working throughstoreOnlyHost; ignored whenWithHostis also setctx.Done()signal.interrupt(msg)surfaces asengine.InterruptedError{Cause: CauseCustom, Detail: msg}bridge_hostexposeshost.publish/host.checkInterrupt(sticky latch) /host.askUser(multimodal) /host.reportUsagestaticcheck SA1019flags directsdk/workflowimporters (verified viadoc.gopackage-level Deprecated)Made with Cursor