Agent primitive: AI agents as first-class Nextflow tasks - #7477
Conversation
✅ Deploy Preview for nextflow-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Please see the comments I left on the internal PR. Several of them are still unresolved I will make a PR for the docs and ADR today or tomorrow |
|
Oops, I had missed some of your comments since they were buried in the long GH conversation. I've replied already to some of them and exploring the missing improvements requested. However I'd prefer to not consider them blocking and move this forward. |
agent primitive: LLM agents as first-class Nextflow tasksThis introduces `agent` as a task-shaped language construct whose body is an
LLM call with an optional tool-calling loop. An agent takes one input record,
produces one result, and executes as an ordinary `TaskRun`, so it inherits the
machinery processes already have: parallelism, resume, lineage, tracing and
error reporting.
Language (nf-lang)
- `AGENT` lexer token and `agentDef` grammar rules; `agent` and `prompt` remain
usable as identifiers
- `AgentNode` AST, `visitAgent` on the script visitor, agent declarations on
`ScriptNode`, and inclusion in symbol resolution, include visitors and the
formatter
- `AgentToGroovyVisitor` lowers an agent definition to a runtime `agent(...)`
call; `ImplicitStagers` unifies agent and process `Path` I/O lowering
- `AgentDsl` directive scope and type checking for agent bodies
Runtime (modules/nextflow)
- `nextflow.agent`: the runtime model - typed I/O and record schemas, output
plans, tool descriptors and dispatch, module-as-tool bridging, skills,
filesystem tools and a sandbox guard
- `nextflow.agent.rpc`: driver-side RPC registration and host resolution for
out-of-process runners
- `AgentExecutor`, `AgentTaskHandler` and `AgentPollingMonitor` execute agents
through the standard task path
- `AgentDef`, `AgentBuilder` and `PromptDef` on the script side; `agent { ... }`
exposed via `BaseScript`
- `AgentRunner` SPI plus `AgentConfig` for the `agent` config scope
Runners (plugins)
- `nf-agent`: a langchain4j-backed runner with structured output via
`responseFormat`, tool and skill adapters, and a portable schema mapped to
langchain4j `JsonSchema`
- `nf-agent-pi`: an out-of-process runner distributed as a container image,
with a Go RPC proxy, a Node harness, TLS credentials and secret masking
Lineage
- `AgentRun` lineage record, with agent runs rendered in the lineage DAG
Docs and examples
- ADR `adr/20260505-llm-agent-primitive.md` records the decision
- `docs/agent.mdx` is the user guide; `adr/specs/` holds the architectural
reference for the design, the runner SPI and the RPC protocol, next to the
decision record it elaborates
- `examples/agents/` contains runnable examples covering structured output,
tools, skills, modules-as-tools, map/reduce and fan-in, with a validator
- the four examples that assemble a real genome share one fetched FASTQ
fixture through `examples/data/` and a `data` symlink, so the read set is
downloaded once rather than per example
The feature is a preview: a warning is emitted when an agent is used.
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Assisted-by: Claude Opus 5 via Claude Code
The destructured `record(...)` rejection was documented as a bare bullet, which made it read as arbitrary. Say what is required instead (a named record type), that it applies to `input:` and `output:` alike, and why: the output schema is derived by reflecting on the declared class, so a destructured record has nothing to reflect on. Assisted-by: Claude Code (Opus 5) Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Tag every environment variable introduced with the agent primitive with an `AddedInVersion` callout, matching the convention already used by `NXF_AGENT_MODE` and `COLUMNS`, and reword the `NXF_AGENT_MODE` note as suggested in review. Assisted-by: Claude Code (Opus 5) Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Co-authored-by: Ben Sherman <bentshermann@gmail.com> Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
…ast] Signed-off-by: Ben Sherman <bentshermann@gmail.com>
An agent asks the model for one answer, so the `output:` section takes exactly one declaration -- combine multiple values into a record. The check was added in 82310035c but called a `collectSyntaxError` overload that does not exist, so it never compiled; this reports it the same way every other agent diagnostic does, anchored to the `output:` label. A work-dir collector counts toward the limit: an agent either answers a value or collects a file, not both. Tests that declared multiple agent outputs are dropped, and the file()/files() lowering coverage is split into one agent per form. Note that AgentDef.buildWrapperSchema and AgentOutputMode.WRAPPED -- the N-way wrapper split -- are now unreachable from the DSL. Signed-off-by: Ben Sherman <bentshermann@gmail.com>
An agent declares a single output, so the wrapper-object schema and the per-output-name fan-out it fed are unreachable: resolveOutputPlan guarded them behind `outputs.size() > 1`. Drops AgentOutputMode.WRAPPED, AgentOutputPlan.isWrapped, the WRAPPED branches of decode/bind, and AgentDef.buildWrapperSchema. isStructured now means RECORD, which is what it always meant for a legal agent. Signed-off-by: Ben Sherman <bentshermann@gmail.com>
609fd96 to
0bee9d8
Compare
|
The main follow-up we need to make before 26.10 is to have more comprehensive support for composite outputs The current state:
I think the solution is to support destructured records: output:
record(id: id, result: file('result.txt'))This seems to be the only way to support a structured output that contains files. Destructured record input is already trivial to implement because it is semantically equivalent to a record type input. It is a different paradigm of output collection. Instead of giving the agent a record schema with which to structure its response, you simply give it the list of expected output file names. The agent need not know anything about the output structure aside from this. It does match the way users currently think about tasks though. A process output typically contains (1) metadata forwarded from the inputs and (2) files written by the task. Any output "metadata" or "scalars" must be written to a file (or |
Summary
Adds
agentas a task-shaped language construct whose body is an LLM call with an optional tool-calling loop. An agent takes one input record, produces one result, and executes as an ordinaryTaskRun— so it inherits the machinery processes already have: parallelism, resume, lineage, tracing, container provisioning and error reporting.The rationale is recorded in
adr/20260505-llm-agent-primitive.md. In short: agentic analysis is first and foremost a reproducibility problem, and the requirements it raises — versioned skill distribution, deterministic replay, audit trails, portability — are the ones Nextflow already solved for deterministic compute. Replay is caching, audit is lineage, skill distribution is module distribution. Rather than build a parallel stack outside the engine, this makes the agent a node in the workflow graph.What's in it
Language (
nf-lang) —AGENTlexer token andagentDefgrammar rules, withagentandpromptstill usable as identifiers;AgentNodeAST,visitAgenton the script visitor, agent declarations onScriptNode, and inclusion in symbol resolution, include visitors and the formatter;AgentToGroovyVisitorlowers a definition to a runtimeagent(...)call;ImplicitStagersunifies agent and processPathI/O lowering;AgentDsldirective scope and type checking.Runtime (
modules/nextflow) —nextflow.agentholds the runtime model: typed I/O and record schemas, output plans, tool descriptors and dispatch, module-as-tool bridging, skills, filesystem tools and a sandbox guard.nextflow.agent.rpchandles driver-side registration and host resolution for out-of-process runners.AgentExecutor,AgentTaskHandlerandAgentPollingMonitorrun agents through the standard task path.AgentRunneris the SPI;AgentConfigbacks theagentconfig scope.Runners (
plugins) —nf-agentis a langchain4j-backed runner with structured output viaresponseFormat, tool and skill adapters, and a portable schema mapped to langchain4jJsonSchema.nf-agent-piis an out-of-process runner distributed as a container image, with a Go RPC proxy, a Node harness, TLS credentials and secret masking.Lineage — an
AgentRunrecord, with agent runs rendered in the lineage DAG.Docs —
docs/agent.mdxis the user guide;adr/specs/holds the architectural reference for the design, the runner SPI and the RPC protocol, next to the decision record it elaborates.Examples —
examples/agents/has 18 runnable examples covering structured output, tools, skills, modules-as-tools, filesystem tools, goal-directed loops, convergence loops, map/reduce and fan-in, plusvalidate.shwhich runs them end to end (-ralso checks each replays from cache on-resume).Preview status
The feature is a preview: a warning is emitted whenever an agent is used, and the syntax and configuration surface may change before it is declared stable.
Testing
Unit and integration coverage lives beside the code — 79 new Spock spec files across
nextflow.agent,nf-langparsing/resolution/lowering, and both runner plugins.End to end, the first five examples were validated against a real model on the local executor with the containerized
pirunner:01_structured-output02_two-agents03_skills04_tool05_tool-parallelEvery task ran containerized (
remote=true) with no rejected RPC connections, and the outputs were checked for substance rather than exit status —05_tool-parallel's reverse complements are correct, and its non-ACGT input was correctly flagged.Notes for reviewers
release.shgains a step 1 that publishes thenf-agent-pirunner image topublic.cr.seqera.io/nextflow— the registry and namespace the release already pushesnextflow/nextflowto — reusing the existingSEQERA_PUBLIC_CR_*credential. It is deliberately ordered first, so a failure happens before anything irreversible is published.build.ymladds a QEMU setup step for the multi-arch build and raises the release job timeout from 10 to 45 minutes. This is the part most worth a close look, and it can be split into a follow-up PR if you'd prefer to land the language and runtime first.build.gradlegains two checks that run onupload,deployandrelease: one fails the release if thenf-agent-piimage build context drifted from itsVERSION, since the image tag is immutable once published; the other fails if any plugin declares anextflowVersionnewer than the tree's ownVERSION.examples/data/and adatasymlink, so the read set downloads once rather than per example. The fixture is gitignored; the directory and symlinks are committed so a fresh clone has the wiring in place.🤖 Generated with Claude Code