feat(dsl,nodes): support agent_strategy plugin invocation#135
feat(dsl,nodes): support agent_strategy plugin invocation#135BenjaminX wants to merge 5 commits into
Conversation
|
All contributors on this pull request have signed the CLA. |
|
I have read the CLA Document and I hereby sign the CLA |
Add a first-class workflow node for Dify ``agent_strategy`` plugins
plus the slim-backed runtime adapter that drives them. This unlocks
Dify Studio chatflows that contain ``type: agent`` nodes —
``graphon.dsl.loads`` now accepts and executes them end-to-end.
Three layers, all shipped together so the change is atomic:
1. **Slim layer** (``src/graphon/dsl/slim/agent.py``):
- ``SlimAgentStrategyClient`` — thin invoker scoped by
``(plugin_id, agent_strategy_provider, agent_strategy)``. Mirrors
``SlimLLM`` and ``SlimToolNodeRuntime``: ``action_invoker``
injection for tests, ``SlimClient`` for subprocess. Errors from
``action_invoker`` propagate unchanged (caller-owned); errors
from the SlimClient path are wrapped into ``SlimAgentStrategyError``
(library-owned boundary).
- ``AgentRuntimeMessage`` — PEP 695 alias of ``ToolRuntimeMessage``
because the Dify Plugin SDK defines
``AgentInvokeMessage(InvokeMessage): pass`` without adding fields.
The alias keeps call sites semantically clear and reserves room for
divergence later.
- ``SlimActionInvoker`` — DI seam.
- ``_decode_agent_message`` — dedicated decoder for the agent message
subset (text / link / json / log / variable / retriever_resources).
Independent from the tool runtime's decoder so the agent path is
not coupled to tool-specific variants (file / blob / image).
2. **Node layer** (``src/graphon/nodes/agent/``):
- ``AgentNodeData`` mirrors the Dify Studio v1.7+ export shape — the
``agent_strategy_provider_name`` / ``agent_strategy_name`` /
``plugin_unique_identifier`` triple plus the ``agent_parameters``
typed-wrapper bag.
- ``AgentParameterValue`` is the ``{type, value}`` wrapper Studio
emits for every parameter (constant / variable / mixed),
type-checked at validation time.
- ``AgentNode._run`` is a streaming generator that resolves
typed-wrapper parameters against the variable pool, forwards them
through the injected runtime, translates each runtime message into
the matching graph event — text/link → ``StreamChunkEvent``
selected by ``[node_id, "text"]``, log → ``AgentLogEvent``, json
/ variable → accumulated outputs — and emits one terminal
``StreamCompletedEvent``.
- ``AgentNodeError`` is the node-layer boundary error type.
- ``AgentNodeRuntimeProtocol`` (in ``nodes/runtime.py``) decouples
the node from any specific runtime.
3. **DSL wiring** (``src/graphon/dsl/``):
- ``SlimAgentNodeRuntime`` (``agent_runtime.py``) implements
``AgentNodeRuntimeProtocol`` by assembling a fresh
``SlimAgentStrategyClient`` per invocation. Stateless adapter that
mirrors how ``SlimDslNodeFactory`` constructs LLM and tool slim
clients per node. ``_provider_slug`` extracts the trailing segment
of ``agent_strategy_provider_name`` (e.g. "langgenius/agent/agent"
→ "agent") to match the slim payload contract.
- ``SlimDslNodeFactory.create_node`` routes ``BuiltinNodeTypes.AGENT``
to ``AgentNode`` with a slim-backed runtime built from the
factory's existing ``slim_client_config``.
- ``_SUPPORTED_DEFAULT_FACTORY_NODES`` in the importer now includes
``BuiltinNodeTypes.AGENT``.
Tests: 28 new cases across three files cover the slim adapter
(``tests/dsl/test_slim_agent.py``: 12 cases — basic / mixed-type
decoding, payload contract, meta propagation, lazy partial consumption,
both error-path contracts, alias identity), the node behavior
(``tests/nodes/agent/test_agent_node.py``: 10 cases — text streaming,
log → ``AgentLogEvent``, json / variable accumulation, all three
typed-wrapper resolution modes, runtime-error wrap), the slim runtime
adapter (``tests/dsl/test_slim_agent_node_runtime.py``: 4 cases — slug
extraction, payload forwarding, decoded message contract, per-node
client identity), and factory routing
(``tests/dsl/test_node_factory_agent.py``: 2 cases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eaming Standard downstream-integration pattern for executing Dify Studio exported chatflow / workflow DSLs through ``graphon.dsl.loads``. What ``main.py`` demonstrates end-to-end: - Static DSL inspection via ``graphon.dsl.inspect`` before execution, printing document kind, plugin dependencies, and load status. Aborts early with a readable diagnostic for non-loadable plans (unsupported node types, config-only ``app.mode`` like ``chat`` / ``completion`` / ``agent-chat``, unresolvable plugin dependencies). - Execution via ``graphon.dsl.loads`` — the canonical 4-line integration surface; everything else in main.py is decoration. - Live event consumption: ``NodeRunStreamChunkEvent`` writes chunks to stdout as they arrive, ``NodeRunStartedEvent`` shows per-node lifecycle, ``NodeRunAgentLogEvent`` exposes agent strategy inner steps, ``GraphRunSucceededEvent`` collects the final ``outputs["answer"]``. - Credential isolation: keys live only in ``credentials.json`` (gitignored via the repo-wide ``examples/*/credentials.json`` rule). No ambient environment variables are consulted for secrets. - Slim binary auto-discovery: ``SLIM_BINARY_PATH`` env var wins, then a local ``./slim`` file in the example directory, otherwise rely on ``PATH``. Includes ``credentials.example.json`` matching the upstream ``examples/slim_llm`` convention so credential plumbing is consistent across demos. Multi-vendor template lists both ``tongyi`` (DashScope) and ``openai`` (with custom ``openai_api_base`` + ``api_protocol`` fields exposed by the ``langgenius/openai`` plugin). README is a standard usage reference for downstream integrators: observed event types, the 4-line integration core, supported / unsupported node types and modes, troubleshooting table. Also extends ``.gitignore`` to cover ``.scratch/`` (local design / review notes) and ``examples/*/credentials.json`` (per-example secrets, never committed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous pin ``langgenius/openai:0.3.8@<digest>`` was rejected by the live marketplace with ``plugin package not found`` (500). The upstream ``examples/slim_llm`` demo therefore did not run end-to-end on a fresh ``.slim/plugins`` cache. Updating both the demo's ``graph.yml`` and ``settings.py`` to the current marketplace head ``0.4.0`` (digest ``beafb5a726eda839a1839f61a0456ae7e068c98624c53f59b07be9a71fbf72da``) restores marketplace download. No behavioral change in the demo itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f4a924f to
6f028a5
Compare
|
Hi @BenjaminX, thank you very much for the thoughtful work here and for the detailed RFC/prototype. I am sorry for the delayed conclusion. After aligning our DSL import roadmap in #154, we have decided not to support the Because of that, we are going to close this PR and the related RFC issue for now. This is not a reflection on the quality of your contribution; the implementation and tests gave us useful reference points, and we appreciate the time you put into it. To share a little more context: internally, we are about to start a larger refactor of Dify's agent node/runtime design. We expect that work to change the right integration surface for agent execution, so landing AgentNode support now would likely create churn for both maintainers and downstream users. Apologies again for the back-and-forth, and thank you for pushing this area forward. Once the agent refactor direction is clearer, we would be happy to revisit the support path with a more stable boundary. |
I also saw the Dify main repository, which refactored the Agent runtime. Are there any refactoring plans and roadmaps for this new Agent runtime? |
Summary
Implements the two integration surfaces flagged in RFC #102 so external integrators can drive Dify chatflows containing agent nodes through
graphon.dsl.loads.dsl/slimlearns theinvoke_agent_strategyslim action via a newSlimAgentStrategyClient. ReusesToolRuntimeMessageas the wire DTO because the Dify Plugin SDK definesAgentInvokeMessage(InvokeMessage): pass— wire format is identical, the tool-side decoder is shared.AgentNode,AgentNodeRuntimeProtocol, andSlimAgentNodeRuntime; routesBuiltinNodeTypes.AGENTthroughSlimDslNodeFactory. Mirrors the structure ofToolNode/SlimToolNodeRuntime.examples/chatflow_dsl_runner, a canonical downstream-integration pattern usinggraphon.dsl.loadsagainst a real Dify Studio chatflow export. Two fixtures bundled: one runs out of the box, one documents a known limitation (see below).examples/slim_llmfrom 0.3.8 to 0.4.0 to align with the new chatflow example.What's new
src/graphon/dsl/slim/agent.py—SlimAgentStrategyClient,SlimAgentStrategyError,AgentRuntimeMessagealias,SlimActionInvokerDI seam.src/graphon/dsl/agent_runtime.py—SlimAgentNodeRuntime(AgentNodeRuntimeProtocol), mirrorsSlimToolNodeRuntime.src/graphon/dsl/_provider.py— extracted sharedcanonical_vendorhelper (deduped fromnode_factory.py).src/graphon/nodes/agent/{agent_node.py, entities.py, exc.py}—AgentNode,AgentNodeData(typed-wrapperagent_parameters),AgentParameterValue,AgentNodeError.src/graphon/nodes/runtime.py— newAgentNodeRuntimeProtocol.src/graphon/dsl/{node_factory.py, importer.py}— routeBuiltinNodeTypes.AGENT; add to_SUPPORTED_DEFAULT_FACTORY_NODES.examples/chatflow_dsl_runner/—main.py(CLI withinspect()/loads()/ event streaming),chatflow_dsl_simple.yml(runnable),chatflow_dsl_agent.yml(reference fixture, see limitations),README.md,credentials.example.json.Out of scope (deliberate)
tenant_id/user_idare intentionally not propagated through the slim runtime. When an agent strategy plugin doesself.session.model.llm.invoke(...), the daemon currently performs backwards-invocation against the Dify Server inner API at:5001, which requires a tenant context. Teaching graphon to forwardtenant_id/user_idwould leak Dify's business-side identity model into a tenant-agnostic execution engine.The correct long-term fix is the daemon redesign outlined in the RFC #102 "long-term direction" — let plugin code perform nested invocations without depending on Dify Server's inner API. Until that lands,
chatflow_dsl_agent.ymlships as a structural reference (this is what a real Dify Studio export of a chatflow with an agent looks like) but cannot run end-to-end from a graphon-only runner.chatflow_dsl_simple.ymlis the runnable out-of-box fixture.This trade-off is documented in
examples/chatflow_dsl_runner/README.md#architecture--limitations.Test plan
pytest tests/— 348 passed (28 new tests acrosstest_slim_agent.py,test_agent_node.py,test_slim_agent_node_runtime.py,test_node_factory_agent.py).make tc— ruff format + check + ty all green.python3 examples/chatflow_dsl_runner/main.py chatflow_dsl_simple.yml "Say hi in 5 words."streams a real LLM response fromlanggenius/openai:0.4.0.action_invokerinjection seam — no e2e slim-binary tests on CI by design.Open questions for reviewers
agent_strategyandtoolplugin invocation #102 originally split the work into PR-A (slim) and PR-B (node). They're bundled here because PR-B is hard to verify without PR-A and the example is the smallest end-to-end witness we have. Happy to split into stacked PRs if preferred — the three commits (984a416slim+node,23855bdexample,f439196slim_llm bump) are natural boundaries.AgentRuntimeMessage = ToolRuntimeMessagealias — keeps a semantic name so future agent-only message variants can diverge, while structurally reusing the tool DTO and decoder. Alternative is to inlineToolRuntimeMessageat the agent call sites. The reasoning is in a comment atsrc/graphon/dsl/slim/agent.py:15.AgentNode.version() → "1"— matches thestart/llm/toolconvention. Distinct fromtool_node_version: "2"inAgentNodeData, which is the Dify plugin protocol version forwarded verbatim to the strategy plugin.chatflow_dsl_agent.ymlplacement — kept underexamples/with a README disclaimer rather thantests/fixtures/because it doubles as documentation for the architecture limitation. Open to relocation ifexamples/is meant to be strictly run-out-of-box.slim_llmopenai bump (f439196) — kept as a separate commit; trivially droppable from this PR if maintainers prefer it as a follow-up chore.References
agent_strategyandtoolplugin invocation #102