You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RFC: Slim runtime support for agent_strategy and tool plugin invocation
SlimRuntime currently invokes the dify-plugin-daemon-slim binary for model-only plugin actions (invoke_llm, invoke_text_embedding, invoke_rerank, invoke_tts, invoke_speech2text, invoke_moderation). The slim binary already supports two more plugin classes — agent_strategy and tool — but Graphon does not surface them. As a result:
BuiltinNodeTypes.AGENT is registered in the enum and BUILT_IN_NODE_TYPES, but no AgentNode class exists.
ToolNode requires a ToolNodeRuntimeProtocol injection at construction time and has no in-tree default implementation; downstream integrators must reimplement plugin-daemon plumbing themselves.
This RFC proposes extending SlimRuntime with two new invocation surfaces, adding the corresponding model_runtime protocols, providing a default ToolNodeRuntimeProtocol implementation backed by Slim, and shipping a first-class AgentNode.
The work splits into three independent PRs that can land in sequence.
Motivation
Dify chatflows exported as DSL today routinely include agent and tool nodes (the official langgenius/agent strategy plugin and the entire Dify Tool plugin marketplace). Any downstream Graphon integrator that wants to run those DSLs must:
Re-implement the slim subprocess protocol in user code, in two places (agent + tool).
Re-implement ToolNodeRuntimeProtocol from scratch.
Build their own AgentNode against private contracts.
This duplicates work, drifts from upstream, and forces every integrator to learn slim's stdin/stdout JSON format. Bringing these two plugin classes into SlimRuntime keeps the existing "model_runtime is the unified plugin invocation layer" invariant, makes Graphon a complete runtime for Dify-exported DSLs, and unlocks AgentNode support.
The Slim binary itself already exposes the routes — dify-plugin-daemon's pkg/slim/remote.go registers invoke_agent_strategy → /agent_strategy/invoke and invoke_tool → /tool/invoke, and cmd/slim/main.go accepts these as -action values. The capability gap is purely on the Graphon side.
Each protocol exposes the methods above with @runtime_checkable and follows the same conventions as LLMModelRuntime (kw-only, provider / credentials / structured params). Both are independent capabilities — implementations may opt into either or both.
ModelRuntime (the aggregate Protocol in model_runtime/protocols/runtime.py) remains backwards-compatible: the new protocols are not mixed into the aggregate to preserve the "split by capability" invariant established in #57. Consumers depend on the narrow protocol they actually need.
C. New AgentNode and AgentNodeRuntimeProtocol
Add a node directory mirroring nodes/tool/:
nodes/agent/
├── __init__.py
├── agent_node.py # class AgentNode(Node[AgentNodeData])
├── entities.py # AgentNodeData, AgentParameter, etc.
└── exc.py
AgentNodeData reflects the DSL shape produced by Dify Studio v1.7+:
classAgentNodeData(BaseNodeData):
type: NodeType=BuiltinNodeTypes.AGENTagent_strategy_provider_name: str# e.g. "langgenius/agent/agent"agent_strategy_name: str# e.g. "function_calling"agent_parameters: Mapping[str, AgentParameterValue]
plugin_unique_identifier: stroutput_schema: Mapping[str, Any] =Field(default_factory=dict)
tool_node_version: str="2"
AgentParameterValue is the {type: constant|variable|...; value: ...} typed-input wrapper Dify Studio emits.
Add AgentNodeRuntimeProtocol next to ToolNodeRuntimeProtocol in nodes/runtime.py:
AgentNode._run() dispatches AgentInvokeMessage to graphon's existing AgentLogEvent and StreamChunkEvent channels (the dispatch helper at nodes/base/node.py:845 is already wired).
D. Default ToolNodeRuntimeProtocol implementation backed by SlimRuntime
Provide a concrete SlimToolNodeRuntime (and SlimAgentNodeRuntime) under, e.g., model_runtime/slim/node_adapters.py:
This makes ToolNode "just work" out of the box once a SlimRuntime is configured, and resolves the # TODO: Make runtime optional once Graphon provides a default tool runtime adapter at nodes/runtime.py:68-70.
E. Test surface
Unit tests for each new SlimRuntime method using the existing slim test harness pattern (tests/model_runtime/slim/test_runtime.py).
Integration-style tests for AgentNode and SlimToolNodeRuntime using fake-slim fixtures (no real binary needed).
One end-to-end smoke test exercising AgentNode → SlimRuntime.invoke_agent_strategy → fake slim to validate the full event dispatch path.
Implementation plan (3 PRs)
PR-1 — feat(slim): support agent_strategy and tool plugin invocation
Scope:
New action strings + _invoke_unary_action / _invoke_streaming_action plumbing.
New methods on SlimRuntime: invoke_agent_strategy, invoke_tool, validate_tool_provider_credentials, get_tool_runtime_parameters.
New entities: AgentInvokeMessage, ToolInvokeMessage, request/response DTOs (mirror dify-plugin-daemon's pkg/entities/requests/agent.go and tool.go).
New protocols: model_runtime/protocols/agent_strategy_runtime.py, model_runtime/protocols/tool_runtime.py.
Unit tests (slim subprocess fakes).
Non-goals: AgentNode, ToolNode adapter — both deferred to PR-2/3.
PR-2 — feat(nodes): default ToolNodeRuntimeProtocol implementation backed by SlimRuntime
ToolNode's constructor still requires runtime: ToolNodeRuntimeProtocol; PR-2 only ships an adapter, it does not change the contract. Whether runtime becomes optional with a Slim-backed default is a separate decision and is out of scope here.
No DSL format assumptions that aren't already present in Dify Studio v1.7+ exports.
Open questions
AgentNode data-model fidelity. Dify Studio's agent_parameters uses a typed-input wrapper ({type: constant|variable|selector; value: ...}). Should AgentNodeData keep this shape (clean DSL round-trip) or pre-resolve to plain dicts (cleaner internal API)? PR-3 currently proposes "keep the wrapper, resolve at _run() boundary."
ToolNodeRuntimeProtocol default constructor argument. Once SlimToolNodeRuntime exists, should ToolNode.__init__ accept runtime=None and fall back to a global SlimRuntime set on GraphInitParams.run_context, or stay strictly explicit? Maintainers' call.
Streaming protocol for invoke_tool. Slim's tool action emits ToolInvokeMessage chunks. Should SlimRuntime.invoke_tool() return Generator[ToolInvokeMessage, None, None] directly, or pre-buffer non-streaming tools? Proposing: always-streaming, callers buffer if they need.
Plugin daemon vs slim local mode. The agent_strategy plugin (langgenius/agent) calls back into the daemon during a single invocation (e.g. to invoke tools and LLMs). In strict local-slim mode without a remote daemon, this nested-call pattern may be unsupported. Should this RFC require remote-daemon mode for AgentNode, or is local-slim sufficient? Needs clarification from maintainers familiar with pkg/slim/local.go vs pkg/slim/remote.go semantics.
Versioning of plugin schemas. Dify DSL emits tool_node_version: '2' on agent nodes. Should AgentNode validate the supported versions explicitly and reject unknown ones, or be permissive?
Acceptance criteria
PR-1 merged: SlimRuntime exposes invoke_agent_strategy and invoke_tool with passing tests.
PR-2 merged: a Graphon integrator can construct ToolNode using only SlimRuntime + the shipped adapter, no custom protocol code.
PR-3 merged: a Dify-exported chatflow YAML containing an agent node executes end-to-end via Graph.init() + GraphEngine.run() without external implementations of agent/tool runtime.
No regressions in existing slim tests or model_runtime protocols.
Existing Graphon pattern to mirror: SlimRuntime.invoke_llm (src/graphon/model_runtime/slim/runtime.py:318+) and LLMModelRuntime (src/graphon/model_runtime/protocols/llm_runtime.py).
Looking for feedback on
Whether maintainers consider AgentNode in scope for the core repo or prefer it stays in a downstream package.
Reactions to splitting the work as PR-1 → PR-2 → PR-3 vs a single bundled PR.
Open questions 1–5 above.
Whether anyone is already working on this so I can avoid duplication.
I have a working prototype against my fork that I am preparing to extract into PRs once the design direction is agreed.
RFC: Slim runtime support for
agent_strategyandtoolplugin invocationSlimRuntimecurrently invokes thedify-plugin-daemon-slimbinary for model-only plugin actions (invoke_llm,invoke_text_embedding,invoke_rerank,invoke_tts,invoke_speech2text,invoke_moderation). The slim binary already supports two more plugin classes —agent_strategyandtool— but Graphon does not surface them. As a result:BuiltinNodeTypes.AGENTis registered in the enum andBUILT_IN_NODE_TYPES, but noAgentNodeclass exists.ToolNoderequires aToolNodeRuntimeProtocolinjection at construction time and has no in-tree default implementation; downstream integrators must reimplement plugin-daemon plumbing themselves.This RFC proposes extending
SlimRuntimewith two new invocation surfaces, adding the correspondingmodel_runtimeprotocols, providing a defaultToolNodeRuntimeProtocolimplementation backed by Slim, and shipping a first-classAgentNode.The work splits into three independent PRs that can land in sequence.
Motivation
Dify chatflows exported as DSL today routinely include
agentandtoolnodes (the officiallanggenius/agentstrategy plugin and the entire Dify Tool plugin marketplace). Any downstream Graphon integrator that wants to run those DSLs must:ToolNodeRuntimeProtocolfrom scratch.AgentNodeagainst private contracts.This duplicates work, drifts from upstream, and forces every integrator to learn slim's stdin/stdout JSON format. Bringing these two plugin classes into
SlimRuntimekeeps the existing "model_runtime is the unified plugin invocation layer" invariant, makes Graphon a complete runtime for Dify-exported DSLs, and unlocks AgentNode support.The Slim binary itself already exposes the routes —
dify-plugin-daemon'spkg/slim/remote.goregistersinvoke_agent_strategy → /agent_strategy/invokeandinvoke_tool → /tool/invoke, andcmd/slim/main.goaccepts these as-actionvalues. The capability gap is purely on the Graphon side.Current state (verified)
SlimRuntime.invoke_llmsrc/graphon/model_runtime/slim/runtime.py:318SlimRuntime.invoke_text_embedding/_rerank/_tts/_speech_to_text/_moderationSlimRuntime.invoke_agent_strategySlimRuntime.invoke_toolSlimRuntime.validate_tool_provider_credentials/get_tool_runtime_parametersmodel_runtime/protocols/agent_strategy_runtime.pymodel_runtime/protocols/tool_runtime.pyToolNodeRuntimeProtocolsrc/graphon/nodes/runtime.py:21-57(TODO comment at line 68-70 explicitly anticipates a default adapter)BuiltinNodeTypes.AGENT = "agent"BUILT_IN_NODE_TYPESsrc/graphon/enums.py:47, 75nodes/agent/directory +AgentNodeclassnode_events/agent.py(AgentLogEvent) andgraph_events/agent.py(NodeRunAgentLogEvent)src/graphon/nodes/base/node.py:845The event side is already prepared for an
AgentNodeto emitAgentLogEvents — only the node class and its runtime adapter are missing.Proposed design
A. Extend
SlimRuntimewith three new actionsIntroduce three new action strings, mirroring the established pattern of
_invoke_unary_action()/_invoke_streaming_action():invoke_agent_strategy(...)"invoke_agent_strategy"invoke_llm(streaming)invoke_tool(...)"invoke_tool"invoke_llm(streaming)validate_tool_provider_credentials(...)"validate_tool_provider_credentials"validate_provider_credentialsget_tool_runtime_parameters(...)"get_tool_runtime_parameters"get_model_schemaBoth streaming endpoints yield typed message objects:
AgentInvokeMessage(text / log / tool_call / tool_call_error / final). Aligns withdify_plugin.entities.agent.AgentInvokeMessageon the plugin side.ToolInvokeMessage(text / link / image / file / json / variable / blob_chunk). Aligns withdify_plugin.entities.tool.ToolInvokeMessage.B. New
model_runtime/protocols/filesEach protocol exposes the methods above with
@runtime_checkableand follows the same conventions asLLMModelRuntime(kw-only,provider/credentials/ structured params). Both are independent capabilities — implementations may opt into either or both.ModelRuntime(the aggregateProtocolinmodel_runtime/protocols/runtime.py) remains backwards-compatible: the new protocols are not mixed into the aggregate to preserve the "split by capability" invariant established in #57. Consumers depend on the narrow protocol they actually need.C. New
AgentNodeandAgentNodeRuntimeProtocolAdd a node directory mirroring
nodes/tool/:AgentNodeDatareflects the DSL shape produced by Dify Studio v1.7+:AgentParameterValueis the{type: constant|variable|...; value: ...}typed-input wrapper Dify Studio emits.Add
AgentNodeRuntimeProtocolnext toToolNodeRuntimeProtocolinnodes/runtime.py:AgentNode._run()dispatchesAgentInvokeMessageto graphon's existingAgentLogEventandStreamChunkEventchannels (the dispatch helper atnodes/base/node.py:845is already wired).D. Default
ToolNodeRuntimeProtocolimplementation backed bySlimRuntimeProvide a concrete
SlimToolNodeRuntime(andSlimAgentNodeRuntime) under, e.g.,model_runtime/slim/node_adapters.py:This makes
ToolNode"just work" out of the box once aSlimRuntimeis configured, and resolves the# TODO: Make runtime optional once Graphon provides a default tool runtime adapteratnodes/runtime.py:68-70.E. Test surface
SlimRuntimemethod using the existing slim test harness pattern (tests/model_runtime/slim/test_runtime.py).AgentNodeandSlimToolNodeRuntimeusing fake-slim fixtures (no real binary needed).AgentNode → SlimRuntime.invoke_agent_strategy → fake slimto validate the full event dispatch path.Implementation plan (3 PRs)
PR-1 —
feat(slim): support agent_strategy and tool plugin invocationScope:
_invoke_unary_action/_invoke_streaming_actionplumbing.SlimRuntime:invoke_agent_strategy,invoke_tool,validate_tool_provider_credentials,get_tool_runtime_parameters.AgentInvokeMessage,ToolInvokeMessage, request/response DTOs (mirrordify-plugin-daemon'spkg/entities/requests/agent.goandtool.go).model_runtime/protocols/agent_strategy_runtime.py,model_runtime/protocols/tool_runtime.py.Non-goals:
AgentNode, ToolNode adapter — both deferred to PR-2/3.PR-2 —
feat(nodes): default ToolNodeRuntimeProtocol implementation backed by SlimRuntimeScope:
SlimToolNodeRuntimeadapter (depends on PR-1).ToolNodeconstructor TODO comment.examples/snippet showing tool-only DSL run.Non-goals: AgentNode.
PR-3 —
feat(nodes): introduce AgentNode and SlimAgentNodeRuntimeScope:
nodes/agent/directory +AgentNodeclass + entities.AgentNodeRuntimeProtocolinnodes/runtime.py.SlimAgentNodeRuntimeadapter.agent_parameters).AgentLogEventplumbing should require minimal touch).langgenius/agentplugin fixtures.Backwards compatibility
ModelRuntimeaggregate Protocol is unchanged: the two new capability protocols stand alongside, consistent with refactor(runtime)!: split model runtime protocols by capability #57's "split by capability" direction.ToolNode's constructor still requiresruntime: ToolNodeRuntimeProtocol; PR-2 only ships an adapter, it does not change the contract. Whetherruntimebecomes optional with a Slim-backed default is a separate decision and is out of scope here.Open questions
AgentNodedata-model fidelity. Dify Studio'sagent_parametersuses a typed-input wrapper ({type: constant|variable|selector; value: ...}). ShouldAgentNodeDatakeep this shape (clean DSL round-trip) or pre-resolve to plain dicts (cleaner internal API)? PR-3 currently proposes "keep the wrapper, resolve at_run()boundary."ToolNodeRuntimeProtocoldefault constructor argument. OnceSlimToolNodeRuntimeexists, shouldToolNode.__init__acceptruntime=Noneand fall back to a globalSlimRuntimeset onGraphInitParams.run_context, or stay strictly explicit? Maintainers' call.Streaming protocol for
invoke_tool. Slim's tool action emitsToolInvokeMessagechunks. ShouldSlimRuntime.invoke_tool()returnGenerator[ToolInvokeMessage, None, None]directly, or pre-buffer non-streaming tools? Proposing: always-streaming, callers buffer if they need.Plugin daemon vs slim local mode. The
agent_strategyplugin (langgenius/agent) calls back into the daemon during a single invocation (e.g. to invoke tools and LLMs). In strict local-slim mode without a remote daemon, this nested-call pattern may be unsupported. Should this RFC require remote-daemon mode forAgentNode, or is local-slim sufficient? Needs clarification from maintainers familiar withpkg/slim/local.govspkg/slim/remote.gosemantics.Versioning of plugin schemas. Dify DSL emits
tool_node_version: '2'on agent nodes. ShouldAgentNodevalidate the supported versions explicitly and reject unknown ones, or be permissive?Acceptance criteria
SlimRuntimeexposesinvoke_agent_strategyandinvoke_toolwith passing tests.ToolNodeusing onlySlimRuntime+ the shipped adapter, no custom protocol code.agentnode executes end-to-end viaGraph.init()+GraphEngine.run()without external implementations of agent/tool runtime.make tcandmake testpass.Prior art / references
dify-plugin-daemonrequest schemas:pkg/entities/requests/agent.go,pkg/entities/requests/tool.go.dify-plugin-daemonHTTP route table:internal/server/controllers/definitions/definitions.go.dify-plugin-daemonslim entrypoint:cmd/slim/main.go,pkg/slim/local.go,pkg/slim/remote.go.dify_pluginPython SDK Agent template:cmd/commandline/plugin/templates/python/agent_strategy.py.SlimRuntime.invoke_llm(src/graphon/model_runtime/slim/runtime.py:318+) andLLMModelRuntime(src/graphon/model_runtime/protocols/llm_runtime.py).Looking for feedback on
AgentNodein scope for the core repo or prefer it stays in a downstream package.I have a working prototype against my fork that I am preparing to extract into PRs once the design direction is agreed.