Skip to content

feat(dsl,nodes): support agent_strategy plugin invocation#135

Open
BenjaminX wants to merge 4 commits into
langgenius:mainfrom
BenjaminX:feat/dsl-slim-invoke-agent-strategy
Open

feat(dsl,nodes): support agent_strategy plugin invocation#135
BenjaminX wants to merge 4 commits into
langgenius:mainfrom
BenjaminX:feat/dsl-slim-invoke-agent-strategy

Conversation

@BenjaminX
Copy link
Copy Markdown

@BenjaminX BenjaminX commented May 12, 2026

Summary

Implements the two integration surfaces flagged in RFC #102 so external integrators can drive Dify chatflows containing agent nodes through graphon.dsl.loads.

  • PR-Adsl/slim learns the invoke_agent_strategy slim action via a new SlimAgentStrategyClient. Reuses ToolRuntimeMessage as the wire DTO because the Dify Plugin SDK defines AgentInvokeMessage(InvokeMessage): pass — wire format is identical, the tool-side decoder is shared.
  • PR-B — Adds AgentNode, AgentNodeRuntimeProtocol, and SlimAgentNodeRuntime; routes BuiltinNodeTypes.AGENT through SlimDslNodeFactory. Mirrors the structure of ToolNode / SlimToolNodeRuntime.
  • Example — Adds examples/chatflow_dsl_runner, a canonical downstream-integration pattern using graphon.dsl.loads against a real Dify Studio chatflow export. Two fixtures bundled: one runs out of the box, one documents a known limitation (see below).
  • Chore — Bumps the pinned openai plugin in examples/slim_llm from 0.3.8 to 0.4.0 to align with the new chatflow example.

What's new

  • src/graphon/dsl/slim/agent.pySlimAgentStrategyClient, SlimAgentStrategyError, AgentRuntimeMessage alias, SlimActionInvoker DI seam.
  • src/graphon/dsl/agent_runtime.pySlimAgentNodeRuntime (AgentNodeRuntimeProtocol), mirrors SlimToolNodeRuntime.
  • src/graphon/dsl/_provider.py — extracted shared canonical_vendor helper (deduped from node_factory.py).
  • src/graphon/nodes/agent/{agent_node.py, entities.py, exc.py}AgentNode, AgentNodeData (typed-wrapper agent_parameters), AgentParameterValue, AgentNodeError.
  • src/graphon/nodes/runtime.py — new AgentNodeRuntimeProtocol.
  • src/graphon/dsl/{node_factory.py, importer.py} — route BuiltinNodeTypes.AGENT; add to _SUPPORTED_DEFAULT_FACTORY_NODES.
  • examples/chatflow_dsl_runner/main.py (CLI with inspect() / 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_id are intentionally not propagated through the slim runtime. When an agent strategy plugin does self.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 forward tenant_id / user_id would 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.yml ships 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.yml is 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 across test_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.
  • Local end-to-end: python3 examples/chatflow_dsl_runner/main.py chatflow_dsl_simple.yml "Say hi in 5 words." streams a real LLM response from langgenius/openai:0.4.0.
  • CI run on this PR. Unit tests use a fake action_invoker injection seam — no e2e slim-binary tests on CI by design.

Open questions for reviewers

  1. PR scope — RFC RFC: Slim runtime support for agent_strategy and tool plugin 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 (984a416 slim+node, 23855bd example, f439196 slim_llm bump) are natural boundaries.
  2. AgentRuntimeMessage = ToolRuntimeMessage alias — keeps a semantic name so future agent-only message variants can diverge, while structurally reusing the tool DTO and decoder. Alternative is to inline ToolRuntimeMessage at the agent call sites. The reasoning is in a comment at src/graphon/dsl/slim/agent.py:15.
  3. AgentNode.version() → "1" — matches the start / llm / tool convention. Distinct from tool_node_version: "2" in AgentNodeData, which is the Dify plugin protocol version forwarded verbatim to the strategy plugin.
  4. chatflow_dsl_agent.yml placement — kept under examples/ with a README disclaimer rather than tests/fixtures/ because it doubles as documentation for the architecture limitation. Open to relocation if examples/ is meant to be strictly run-out-of-box.
  5. slim_llm openai bump (f439196) — kept as a separate commit; trivially droppable from this PR if maintainers prefer it as a follow-up chore.

References

BenjaminX and others added 3 commits May 12, 2026 16:01
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>
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label May 12, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 12, 2026

All contributors on this pull request have signed the CLA.
Posted by the CLA Assistant Lite bot.

@dosubot dosubot Bot added the enhancement New feature or request label May 12, 2026
@BenjaminX
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant