Skip to content

feat(sdk): introduce agent + engine runtime, unify graph around engine.Host#45

Merged
lIang70 merged 8 commits into
mainfrom
feature/agent-engine-runtime
Apr 28, 2026
Merged

feat(sdk): introduce agent + engine runtime, unify graph around engine.Host#45
lIang70 merged 8 commits into
mainfrom
feature/agent-engine-runtime

Conversation

@lIang70

@lIang70 lIang70 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces the next-generation runtime layer (sdk/agent + sdk/engine) and refactors sdk/graph and sdk/script/bindings to drive every cross-cutting concern (events, interrupts, user prompts, checkpointing, usage reporting) through a single engine.Host interface. Marks the legacy sdk/workflow package deprecated with a full migration map. Removes the unused sdk/actor package.

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.0 so downstream code can migrate at its own pace.

What changed

New packages

  • sdk/engine — runtime-agnostic primitives: Host (composed of Publisher / Interrupter / UserPrompter / Checkpointer / UsageReporter), Board (thread-safe blackboard with vars + channels), Interrupt / InterruptedError, Checkpoint, UserPrompt / UserReply. Comes with enginetest.MockHost for downstream tests.
  • sdk/agent — application-layer surface: Agent, Run, Decider, Disposition, Observer, Seeder, Request, Result. Replaces the workflow Runtime + Strategy + Memory triad with a leaner pipeline.
  • sdk/graph/runner — graph assembly + lifecycle, lifted from graph/executor so 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 monolithic graph/node/llm.go. Carries its own Config / runRound / vars; runRound owns the streaming loop, observes host.Interrupts(), commits partial assistant messages + synthetic [cancelled by interrupt] tool_results on cancellation.
  • sdk/graph/node/knowledgenode — moved out of sdk/knowledge/node.go so the graph wiring lives next to the rest of the node implementations.
  • sdk/script/bindings/bridge_host.go — exposes host.publish / host.checkInterrupt / host.askUser / host.reportUsage to scripts. checkInterrupt uses 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.ExecutionContext carries engine.Host so nodes call ctx.Host.{Publish,Interrupts,AskUser,Checkpoint,ReportUsage} directly. The previous Stream callback field is kept but Deprecated:.
  • Executor single-publisher model: runConfig exposes one canonical event sink, cfg.publisher, built once via resolvePublisher(cfg.host, cfg.bus). The deprecated WithEventBus is folded in but no longer touched by the main loop.
  • Executor single-checkpoint model: WithCheckpointStore is folded into cfg.host via storeOnlyHost (parallel to busOnlyHost in the runner). The main loop calls host.Checkpoint(ctx, cp.toEngine()) exclusively. When both WithHost and WithCheckpointStore are supplied the host wins — checkpointing is state, not observability, so unlike events we don't fan out to two backends.
  • Runner single-host model: Runner keeps only r.host. WithEventBus(bus) wraps the bus into a busOnlyHost adapter at construction time so Run() only ever ships one event sink to the executor.
  • graph root reorganised: graph.gocore.go, schema system removed (UI metadata is no longer SDK-owned), validate.go merged into port.go, meta.go + raw.go merged into definition.go, vars.go rewritten to separate engine-produced keys from chat-application conventions (the latter all Deprecated:).
  • scriptnode.signal.interrupt(msg) now surfaces as engine.Interrupted{Cause: CauseCustom, Detail: msg} so the agent layer can recognise the pause via errdefs.IsInterrupted and read the script-supplied detail.
  • llmnode reports per-round delta usage via ctx.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 in VarInternalUsage.
  • script/bindings wave: bridges decoupled from workflow (board / expr / fs / shell / tools / runtime). Workflow-bound bridges (stream, run, agent_step) collected in bindings/deprecated.go. New bridge_llm_facade covers the multimodal LLM path the legacy bridge_stream couldn't.
  • Knowledge node migration: sdk/knowledge/node.go (358 lines) → sdk/graph/node/knowledgenode/. Legacy schema / RegisterNode / KnowledgeConfigFromMap collapsed into sdk/knowledge/deprecated.go with no workflow imports.

Deprecations

Every legacy surface gets a standard // Deprecated: godoc with Scheduled for removal in v0.3.0 and an explicit migration target:

  • sdk/workflow (whole package)doc.go now carries a per-symbol migration map covering Board / Runtime / Agent / Strategy / Request / Result / Memory / RunOption / StreamEvent / Task. Enables staticcheck SA1019 for direct importers.
  • graph/adapter (whole package) — only exists to keep workflow.Strategy callers compiling against graph definitions.
  • Field- and option-level: 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.RunRound family, 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:

Package Coverage
sdk/agent 96.4%
sdk/engine 86.8%
sdk/graph/executor 81.8%
sdk/graph/runner 80.9%
sdk/graph/node/llmnode 89.4%
sdk/graph/node/knowledgenode 82.4%
sdk/script/bindings 92.5%
sdk/graph/node 100%

Notable new test suites:

  • agent/run_test.go — full single-shot agent runs incl. retry, observer, decider routing
  • engine/board_test.go + engine/host_test.go + engine/interrupt_test.go — board cloning, host contracts, interrupt classification
  • engine/enginetest/suite.go — reusable conformance suite for downstream Host implementations
  • graph/node/llmnode/interrupt_test.go — partial commit + cancelled tool_results invariants
  • graph/node/llmnode/host_test.go — delta-usage reporting, zero-usage skip, nil-host safety
  • graph/executor/checkpoint_test.go — host vs store priority, store-only fallback through storeOnlyHost
  • script/bindings/bridge_host_test.go — publish / sticky checkInterrupt / askUser round-trip / reportUsage parsing

Verification

go vet ./... && go test ./... -count=1   # green in sdk, sdkx, voice

sdkx and voice build and test without changes (no voice migration 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.go migration to agent — the only non-deprecated workflow consumer left in the repo. Required before workflow can actually be deleted.
  • sdk/llm/deprecated.go workflow import — keeps a transitive workflow dependency for sdkx/llm/* providers; clearable once the bridge_llm_round migration is fully adopted.
  • historyllmnode constants alignmentVarPrevMessageCount / VarSummaryIndex / VarInternalUsage are duplicated as string constants between workflow/keys.go and llmnode/vars.go. They share the same value so today this works, but history will need to switch to the llmnode constants when workflow is removed.

Test plan

  • go vet ./... clean across sdk, sdkx, voice
  • go test ./... -count=1 clean across sdk, sdkx, voice
  • WithHost(h) actually receives every envelope, interrupt observation, AskUser call, Checkpoint, ReportUsage
  • WithEventBus(bus) keeps working through the deprecated path
  • WithCheckpointStore(s) keeps working through storeOnlyHost; ignored when WithHost is also set
  • llmnode commits partial assistant message + cancelled tool_results on host interrupt and on ctx.Done()
  • scriptnode signal.interrupt(msg) surfaces as engine.InterruptedError{Cause: CauseCustom, Detail: msg}
  • bridge_host exposes host.publish / host.checkInterrupt (sticky latch) / host.askUser (multimodal) / host.reportUsage
  • staticcheck SA1019 flags direct sdk/workflow importers (verified via doc.go package-level Deprecated)

Made with Cursor

lIang70 added 8 commits April 27, 2026 18:02
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
… 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
@lIang70
lIang70 merged commit 491f603 into main Apr 28, 2026
9 checks passed
@lIang70
lIang70 deleted the feature/agent-engine-runtime branch April 28, 2026 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant