style: Cleaned up all graph agent code

This commit is contained in:
2026-05-18 13:46:52 -06:00
parent 35e1b14843
commit 5bd0766a60
23 changed files with 560 additions and 652 deletions
+42 -43
View File
@@ -2,13 +2,14 @@
# Location: <loki-config-dir>/agents/<agent-name>/graph.yaml
#
# A graph agent is defined by THIS FILE ALONE. An agent directory contains
# EITHER a config.yaml (a normal LLM-loop agent) OR a graph.yaml (a graph
# agent) -- never both. The presence of graph.yaml is what makes the agent
# EITHER a config.yaml (a normal LLM-loop agent) or a graph.yaml (a graph
# agent), never both. The presence of graph.yaml is what makes the agent
# a graph agent.
#
# This file is a REFERENCE: it documents every available field. It is not a
# runnable agent as-is -- the `agent:`, `script:`, and `documents:` values
# point at things that would need to exist for a real agent.
# This file is meant to serve as a reference only: it documents every
# available field. It is not a runnable agent as-is. The `agent:`,
# `script:`, and `documents:` values point at things that would need to
# exist for a real agent.
#
# Full documentation:
# https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents
@@ -28,7 +29,7 @@ version: "1.0" # Graph SCHEMA version. Only "1.0" is accepte
# The same knobs a normal agent's config.yaml carries. In a graph agent they
# live here instead of in a config.yaml.
# ---------------------------------------------------------------------------
model: anthropic:claude-sonnet-4-6 # Default model for `llm` nodes that don't override it
model: claude:claude-sonnet-4-6 # Default model for `llm` nodes that don't override it
temperature: 0.0 # Default sampling temperature for `llm` nodes
top_p: null # Default sampling top-p for `llm` nodes
@@ -42,15 +43,11 @@ mcp_servers: # MCP servers an `llm` node may reference via
conversation_starters: # Suggested prompts surfaced in the UI
- "Research LOINC code 2160-0"
# NOTE: `can_spawn_agents` is NOT a field here. It is DERIVED: a graph can
# spawn child agents iff it contains at least one `agent` node (this graph
# does -- see `deep_dive`).
# ---------------------------------------------------------------------------
# Execution settings (all optional)
# ---------------------------------------------------------------------------
settings:
max_loop_iterations: 100 # PER-NODE visit cap. If one node id is entered more
max_loop_iterations: 100 # Per-node visit cap. If one node id is entered more
# than this many times, execution aborts. Default 100.
timeout: 600 # Optional wall-clock cap (seconds) on the whole run,
# checked between node transitions.
@@ -60,14 +57,16 @@ settings:
# ---------------------------------------------------------------------------
# Seed state (optional)
# Values placed into graph state before any node runs; reference anywhere via
# {{key}}. NOTE: `initial_prompt` is seeded automatically by Loki with the
# caller's prompt -- do not set it here.
# {{key}}.
#
# Note: `initial_prompt` is seeded automatically by Loki with the
# caller's prompt. So there's no need to set it here.
# ---------------------------------------------------------------------------
initial_state:
audience: "clinician"
# Seed an empty default for any key that a STRICT field (a node prompt /
# Seed an empty default for any key that a strict field (a node prompt /
# instructions / question / End output) references but that is only set on
# SOME paths. `refinement` is set only if the `refine` input node runs;
# some paths. `refinement` is set only if the `refine` input node runs;
# seeding it "" keeps `finalize`'s strict prompt from failing on the
# approve-directly path.
refinement: ""
@@ -80,7 +79,7 @@ start: triage # ID of the first node to run (must exist in `nodes
# ---------------------------------------------------------------------------
# Nodes
# Each node is keyed by its id. The `id:` inside a node must match its key
# (it may also be omitted -- Loki fills it in from the key).
# (it may also be omitted and thus Loki fills it in from the key).
#
# Node types: agent | script | approval | input | llm | rag | end
# ---------------------------------------------------------------------------
@@ -88,18 +87,18 @@ nodes:
# --- llm node -----------------------------------------------------------
# A one-shot LLM call (with an optional bounded tool-call loop). Runs in a
# fresh isolated context. Tools are STRICTLY opt-in (see `tools`).
# fresh isolated context. Tools are strictly opt-in (see `tools`).
triage:
id: triage
type: llm
description: Classify the request and extract its topic.
instructions: | # Optional system prompt (templated against state)
You triage research requests for a {{audience}} audience.
prompt: | # REQUIRED user prompt (templated against state)
prompt: | # Required user prompt (templated against state)
Classify this request and extract the key topic:
{{initial_prompt}}
tools: [] # Tool whitelist. Omitted or [] = NO tools at all.
# A list narrows to EXACTLY those entries.
tools: [] # Tool whitelist. Omitted or [] = no tools at all.
# A list narrows to exactly those entries.
output_schema: # Optional JSON Schema. The output is parsed to JSON
type: object # and its top-level object keys auto-merge into state
properties: # (so `topic` / `needs_research` become {{topic}} etc).
@@ -108,7 +107,7 @@ nodes:
required: [topic, needs_research]
state_updates: # {{output}} = this node's result (here, the parsed object)
triage_result: "{{output}}"
next: retrieve # REQUIRED for llm nodes: the success route
next: retrieve # Required for llm nodes: the success route
# --- rag node -----------------------------------------------------------
# Hybrid (vector + keyword) retrieval against a per-node knowledge base.
@@ -117,14 +116,14 @@ nodes:
retrieve:
id: retrieve
type: rag
documents: # REQUIRED. Files, directories, URLs, loader paths.
documents: # Required. Files, directories, URLs, loader paths.
- ./knowledge/ # relative paths resolve against the agent directory
- https://example.com/reference
query: "{{topic}}" # Retrieval query (templated). Default: {{initial_prompt}}.
top_k: 5 # Chunks to retrieve. Default = the KB's own top_k.
timeout: 120 # Retrieval timeout in seconds. Default 120.
# Knowledge-base BUILD config (optional; used only when the KB is first
# built). When embedding_model + chunk_size + chunk_overlap are ALL set,
# built). When embedding_model + chunk_size + chunk_overlap are all set,
# the KB builds with no interactive prompts (works in non-interactive runs).
embedding_model: openai:text-embedding-3-small
chunk_size: 1000
@@ -138,9 +137,9 @@ nodes:
# --- script node --------------------------------------------------------
# Runs a .sh / .py / .ts script. The script receives state via the
# GRAPH_STATE env var (inline JSON) OR GRAPH_STATE_FILE (path to a JSON
# file, used when state exceeds 32 KiB) -- exactly one is set. It must print
# a single JSON OBJECT on stdout: keys merge into state, and the reserved
# GRAPH_STATE env var (inline JSON) or GRAPH_STATE_FILE (path to a JSON
# file, used when state exceeds 32 KiB). Exactly one is set. It must print
# a single JSON object on stdout: keys merge into state, and the reserved
# `_next` key (if present) overrides routing.
decide:
id: decide
@@ -150,15 +149,15 @@ nodes:
state_updates: # Applied after the stdout JSON is merged
decided_for: "{{topic}}"
next: summarize # Default route if the script emits no `_next`
fallback: summarize # Route taken if the script FAILS (crash / bad JSON)
fallback: summarize # Route taken if the script fails (crash / bad JSON)
# This script is expected to emit `_next: deep_dive` (or no `_next`, in
# which case `next` is used). Because `deep_dive` is reached only via the
# script's dynamic `_next`, the startup validator will report it as an
# "unreachable" WARNING -- that is expected for `_next`-routed targets.
# "unreachable" warning. That is expected for `_next`-routed targets.
# --- agent node ---------------------------------------------------------
# Spawns a full Loki sub-agent and waits for it. The child uses ITS OWN
# tool stack -- agent nodes have NO `tools:` field. No schema hint is
# Spawns a full Loki sub-agent and waits for it. The child uses its own
# tool stack. Agent nodes have NO `tools:` field. No schema hint is
# injected even when `output_schema` is set (unlike llm nodes).
deep_dive:
id: deep_dive
@@ -168,7 +167,7 @@ nodes:
Research {{topic}} in depth. Existing context:
{{context}}
timeout: 600 # Optional wall-clock cap, seconds. Default 300.
output_schema: # Optional -- same extraction as llm nodes
output_schema: # Optional. Same extraction as llm nodes
type: object
properties:
summary: { type: string }
@@ -178,7 +177,7 @@ nodes:
required: [summary, findings]
state_updates:
research: "{{output}}"
next: review # REQUIRED for agent nodes
next: review # Required for agent nodes
# --- llm node with a narrowed tool whitelist ----------------------------
summarize:
@@ -186,22 +185,22 @@ nodes:
type: llm
instructions: "You write concise summaries for a {{audience}} audience."
prompt: "Summarize the topic {{topic}}, using your tools as needed."
tools: # Narrow whitelist: EXACTLY these entries, nothing else
tools: # Narrow whitelist: Exactly these entries, nothing else
- web_search_loki.sh # an exact global-tool / custom-tool name
- mcp:pubmed-search # `mcp:<server>` includes that server's functions
model: anthropic:claude-haiku-4-5 # Optional per-node model override
model: claude:claude-haiku-4-5 # Optional per-node model override
temperature: 0.3 # Optional per-node sampling override
max_attempts: 2 # Retry count on TRANSIENT errors only. Default 1.
max_attempts: 2 # Retry count on transient errors only. Default 1.
max_iterations: 10 # Tool-call-loop turn cap. Default 10.
fallback: review # Route here if all attempts fail
timeout: 300 # Optional node wall-clock cap, seconds (unset = no timeout)
state_updates:
research: "{{output}}"
next: review # REQUIRED for llm nodes: the success route
next: review # Required for llm nodes: the success route
# --- approval node ------------------------------------------------------
# Human-in-the-loop checkpoint. `user__ask` ALWAYS offers a free-form
# "type your own answer" option, so `on_other` is REQUIRED.
# Human-in-the-loop checkpoint. `user__ask` always offers a free-form
# "type your own answer" option, so `on_other` is required.
review:
id: review
type: approval
@@ -216,9 +215,9 @@ nodes:
routes: # Map each listed option to its next node
"yes": finalize
"no": rejected_end
on_other: refine # REQUIRED: route for ANY answer not in `routes`
on_other: refine # Required: route for ANY answer not in `routes`
state_updates:
decision: "{{choice}}" # {{choice}} = the chosen option OR the free-form text
decision: "{{choice}}" # {{choice}} = the chosen option or the free-form text
# --- input node ---------------------------------------------------------
# Collects a free-form string from the user.
@@ -227,13 +226,13 @@ nodes:
type: input
question: "What should be changed about the result?"
default: "minor wording only" # Optional: used if the user submits empty input.
# NOTE: a substituted default is NOT re-validated,
# Note: a substituted default is not re-validated,
# so make sure it would satisfy `validation`.
validation: "len(input) > 0" # Optional length predicate: len(input) <op> N,
# <op> in > >= < <= == . Length only -- no regex.
state_updates:
refinement: "{{input}}" # {{input}} = the user's text
next: finalize # REQUIRED for input nodes: the success route
next: finalize # Required for input nodes: the success route
# --- llm node (final synthesis) -----------------------------------------
finalize:
@@ -253,7 +252,7 @@ nodes:
done:
id: done
type: end
state_updates: # Optional: applied BEFORE `output` is rendered
state_updates: # Optional: applied before `output` is rendered
status: "completed"
output: |
[{{status}}] {{final_answer}}