6. Agent Coordinator#
pycsamt.agents.AgentCoordinator is the explicit workflow runner for
the agent layer. Use it when the chain is already known, when every transition
between steps should be visible, and when a run must leave enough state for a
reviewer to understand what happened without re-reading a notebook. It
registers named workflow steps, executes them in order, passes
selected outputs forward through input mapping, writes
workflow checkpoints, supports dry-run previews, aggregates warnings
and LLM cost, and returns one workflow-level AgentResult.
The coordinator is deliberately less magical than
WorkflowOrchestratorAgent. The orchestrator reads a
natural-language request and decides which chain to build. The coordinator is
for the moment after that decision: the project has a known processing order,
and the order itself is part of the reproducible record.
6.1. What the coordinator solves#
Many pyCSAMT workflows are staged rather than monolithic:
load data
-> quality control
-> correction
-> tensor diagnostics
-> inversion preparation
-> interpretation and report
Each stage has its own scientific contract. Loading returns station objects, quality control returns warnings and tables, correction returns a new or modified survey object, inversion preparation writes solver files, and reporting consumes the reviewed results. The coordinator does not blur those contracts into one large function. It keeps a dictionary of named AgentResults so the run can be inspected step by step:
where \(C\) is the top-level workflow configuration, \(A_k\) is the
agent at step \(k\), \(g_k\) is the step’s input_fn, and
\(R_k\) is the returned AgentResult. This small formulation is the
heart of reproducibility: a downstream step should receive exactly the fields
you name, not an implicit grab bag of global state.
The coordinator provides:
a stable workflow name;
an ordered list of named steps;
explicit
input_fncallbacks for step-to-step data flow;required and optional failure behavior;
per-step checkpoints and a workflow summary file;
dry-run previews for inspection before data are processed;
workflow-level elapsed time, warnings, and LLM cost aggregation.
6.2. Core objects#
WorkflowStepInternal descriptor for one step. It stores the step name, agent instance, optional input mapping function, description, and whether the step is required.
AgentCoordinatorPublic runner that registers steps with
add_step(...)and runs them withexecute(...)or previews them withpreview(...).AgentResultStandard result object returned by each step and by the coordinator itself. The coordinator result stores per-step results in
result.data.
The workflow-level status is derived from the step statuses:
This means success is still an execution statement, not a scientific
approval. A workflow can complete cleanly and still require geological,
statistical, or deliverable review.
6.3. Minimal workflow#
The first step usually receives the top-level workflow configuration directly. Later steps usually receive selected outputs from earlier results. The example below uses tiny custom agents to expose the coordinator mechanics without mixing in survey loading, plotting, or optional AI dependencies.
1>>> from pycsamt.agents import AgentCoordinator, AgentResult, BaseAgent
2>>>
3>>> class ConstantAgent(BaseAgent):
4... def __init__(self, name, **payload):
5... super().__init__(name)
6... self.payload = payload
7...
8... def execute(self, input_data):
9... return AgentResult(
10... status="success",
11... summary=f"{self.name} accepted {sorted(input_data)}.",
12... data=dict(self.payload, received=dict(input_data)),
13... )
14>>>
15>>> coord = AgentCoordinator(
16... "doc_demo",
17... checkpoint_dir="outputs/docs/doc_demo_checkpoints",
18... verbose=False,
19... )
20>>> coord.add_step(
21... "load",
22... ConstantAgent("load", sites="SITES:3", n_stations=3),
23... description="Load EDI files into a Sites object.",
24... )
25>>> coord.add_step(
26... "qc",
27... ConstantAgent("qc", qc_score=0.94),
28... input_fn=lambda r: {
29... "sites": r["load"]["sites"],
30... "min_score": 0.8,
31... },
32... description="Compute survey and station QC.",
33... )
34>>> preview = coord.preview({"path": "data/AMT/WILLY_DATA/L18PLT"})
35>>> print(preview.status)
36success
37>>> print(preview.summary)
38Workflow preview: 2 steps.
39>>> print([step["name"] for step in preview["steps"]])
40['load', 'qc']
41>>> result = coord.execute({"path": "data/AMT/WILLY_DATA/L18PLT"})
42>>> print(result.status)
43success
44>>> print(result["qc"].summary)
45qc accepted ['min_score', 'sites'].
46>>> print(result["qc"]["received"])
47{'sites': 'SITES:3', 'min_score': 0.8}
The important detail is not the fake sites value; it is the data flow.
The load step received the original config. The qc step received only
the two fields returned by its input_fn. That is the pattern to preserve in
real chains: keep the first step simple, then make each transition explicit.
6.4. Step registration#
Register steps with AgentCoordinator.add_step.
1>>> coord.add_step(
2... name="static_shift",
3... agent=StaticShiftAgent(method="ama"),
4... input_fn=lambda results: {
5... "sites": results["load"]["sites"],
6... "output_dir": "outputs/static_shift",
7... },
8... description="Detect and correct static-shift effects.",
9... required=True,
10... )
The parameters are:
nameUnique identifier for the step. It is used in
results[name], checkpoint filenames, logs, preview output, and report labels. Prefer short stable names such as"load","qc","static_shift", or"occam2d". Renaming a step changes the resume key.agentA
BaseAgentinstance. Construct the agent before registration so LLM provider, model, plotting preset, and constructor parameters are fixed before the run begins.input_fnOptional callable receiving the accumulated previous step results. It must return the input dictionary for the current agent. When omitted, the coordinator passes the original workflow config directly.
descriptionHuman-readable action text shown in dry-run previews and progress output.
requiredTrueby default. If a required step fails, the workflow aborts. If an optional step fails, the coordinator records the failure and continues.
Step names must be unique. Registering the same name twice raises
ValueError.
6.5. Input mapping with input_fn#
input_fn is the most important part of a coordinator workflow. It is the
bridge between one agent’s output contract and the next agent’s input
contract. It should be short enough that a reviewer can read it as a wiring
diagram, not a hidden processing step.
1>>> def static_shift_input(results):
2... return {
3... "sites": results["load"]["sites"],
4... "method": "ama",
5... "output_dir": "outputs/static_shift",
6... }
If a later step also needs values from the original top-level config, keep that config in the surrounding scope and read the required values inside the mapping function:
1>>> workflow_config = {
2... "path": "data/AMT/WILLY_DATA/L18PLT",
3... "output_dir": "outputs/willy",
4... "period_range": (0.001, 10.0),
5... }
6>>>
7>>> def qc_input(results):
8... return {
9... "sites": results["load"]["sites"],
10... "period_range": workflow_config["period_range"],
11... "output_dir": f"{workflow_config['output_dir']}/qc",
12... }
13>>>
14>>> coord.add_step("qc", DataQCAgent(), input_fn=qc_input)
For reproducibility, keep these rules in mind:
map from documented output keys, not from private attributes;
pass corrected data to later steps only when the correction step is the intended source;
keep constants such as
period_rangeand output folders in the workflow config, not scattered across callbacks;inspect
result["step_name"].databefore wiring a new downstream step.
If an input_fn raises an exception in a required step, the workflow returns
a failed AgentResult immediately. For optional steps, the coordinator
records a warning and continues.
6.6. Dry-run preview#
Use dry_run=True or call preview(...) to inspect the workflow before
running agents.
1>>> preview = coord.execute(
2... {"path": "data/AMT/WILLY_DATA/L18PLT"},
3... dry_run=True,
4... )
5>>> print(preview["steps"][0]["name"])
6load
7>>> print(preview["steps"][0]["llm"])
8no-LLM
Equivalent explicit form:
1>>> preview = coord.preview({"path": "data/AMT/WILLY_DATA/L18PLT"})
The preview includes workflow name, number of steps, formatted input config,
step order, agent class, LLM provider/model or "no-LLM", whether the step
is required, and the step description. No agent execute(...) method is
called during preview, so it is safe for expensive, file-writing, or
network-dependent chains.
6.7. Checkpoints and resume#
The coordinator writes checkpoints after each executed step. By default, checkpoints go to:
pycsamt_agent_checkpoints/<workflow_name>/
For each step, two files are written when possible:
<step>.pklPickled
AgentResult, used byresume=True. The checkpoint copy drops Matplotlib figures and containers of figures before pickling, because figure objects often contain unpicklable display closures. Keep figure files inoutput_dirfor reporting.<step>.jsonHuman-readable sidecar containing status, summary, elapsed time, cost, warnings, and error information.
At the end of the workflow, the coordinator also writes:
workflow_state.jsonSummary of the workflow and status/cost metadata for all recorded steps.
Set a custom checkpoint directory when constructing the coordinator:
1>>> coord = AgentCoordinator(
2... "willy_qc",
3... checkpoint_dir="outputs/willy/checkpoints",
4... )
Resume from existing checkpoints:
1>>> result = coord.execute(
2... {"path": "data/AMT/WILLY_DATA/L18PLT"},
3... resume=True,
4... )
Resume is name-based. A step is skipped only when
<checkpoint_dir>/<step_name>.pkl exists and can be loaded. If the workflow
name, checkpoint directory, or step name changes, the coordinator treats the
step as new work.
Clear checkpoints for a workflow:
1>>> coord.reset_checkpoints()
Use checkpoints for long-running or expensive workflows. For quick notebook experiments, leave the default directory or reset it between runs. For formal deliverables, checkpoints are only execution state; archive exported arrays, figures, configuration files, and reports separately.
6.8. Required and optional steps#
Required steps abort the workflow when they fail. The workflow result keeps the partial results produced before the abort, which is important for debugging, review, and restart decisions.
1>>> from pycsamt.agents import AgentCoordinator, AgentResult, BaseAgent
2>>>
3>>> class FakeAgent(BaseAgent):
4... def __init__(self, name, result):
5... super().__init__(name)
6... self.result = result
7...
8... def execute(self, input_data):
9... return self.result
10>>>
11>>> ok = AgentResult(
12... status="success",
13... summary="load ok",
14... data={"sites": "SITES:3"},
15... )
16>>> bad = AgentResult.failed(
17... "qc failed",
18... hint="inspect station errors",
19... )
20>>> coord = AgentCoordinator(
21... "doc_required_failure",
22... checkpoint_dir="outputs/docs/required_failure",
23... verbose=False,
24... )
25>>> coord.add_step("load", FakeAgent("load", ok))
26>>> coord.add_step(
27... "qc",
28... FakeAgent("qc", bad),
29... input_fn=lambda r: {"sites": r["load"]["sites"]},
30... )
31>>> coord.add_step(
32... "report",
33... FakeAgent("report", AgentResult("success", "report ok")),
34... )
35>>> result = coord.execute({"path": "data/edis"})
36>>> print(result.status)
37failed
38>>> print(result.error)
39qc failed
40>>> print(result.error_fix_hint)
41inspect station errors
42>>> print(sorted(result.data))
43['load', 'qc']
Optional steps let the workflow continue, but the workflow is marked
needs_review because not all recorded steps succeeded.
1>>> coord = AgentCoordinator(
2... "doc_optional_failure",
3... checkpoint_dir="outputs/docs/optional_failure",
4... verbose=False,
5... )
6>>> coord.add_step(
7... "optional_plot",
8... FakeAgent("optional", AgentResult.failed("optional plot failed")),
9... required=False,
10... )
11>>> coord.add_step(
12... "report",
13... FakeAgent("report", AgentResult("success", "report ok")),
14... )
15>>> result = coord.execute({"path": "data/edis"})
16>>> print(result.status)
17needs_review
18>>> print(result["optional_plot"].status)
19failed
20>>> print(result["report"].status)
21success
Use optional steps for reports, secondary figures, extra exports, and experimental analyses. Keep loading, QC, correction, and inversion-input steps required when later steps depend on them.
6.9. Workflow result structure#
execute(...) returns one AgentResult.
1>>> result = coord.execute({"path": "data/AMT/WILLY_DATA/L18PLT"})
2>>> print(result.status)
3success
4>>> print(result.summary)
5Workflow 'doc_demo' complete: 2/2 steps succeeded in 0.5s ($0.000000).
6>>> print(result.elapsed_seconds >= 0)
7True
8>>> print(result.cost_estimate_usd)
90.0
10>>> load_result = result["load"]
11>>> qc_result = result["qc"]
The coordinator result has:
status"success"when every recorded step succeeded,"needs_review"when at least one optional or non-aborting step did not succeed, and"failed"when a required step aborted the workflow.summaryHuman-readable workflow completion or failure summary.
dataDictionary of
step_name -> AgentResult.warningsCombined warnings from step results plus coordinator warnings.
cost_estimate_usdSum of per-step LLM cost estimates for steps run in this execution.
The workflow-level elapsed time is wall-clock time around the coordinator execution. Per-step elapsed times remain inside each nested step result.
6.10. Complete QC and correction example#
The example below builds a practical survey workflow with loading, QC, static-shift correction, phase diagnostics, EDI export, and report generation. It is intentionally explicit: every downstream step states whether it consumes raw loaded sites or corrected sites.
1>>> from pycsamt.agents import (
2... AgentCoordinator,
3... DataQCAgent,
4... EDIExportAgent,
5... MTLoaderAgent,
6... PhaseAnalysisAgent,
7... ReportAgent,
8... StaticShiftAgent,
9... )
10>>>
11>>> config = {
12... "path": "data/AMT/WILLY_DATA/L18PLT",
13... "output_dir": "outputs/willy",
14... }
15>>>
16>>> coord = AgentCoordinator(
17... "willy_qc_correction",
18... checkpoint_dir=f"{config['output_dir']}/checkpoints",
19... )
20>>> coord.add_step("load", MTLoaderAgent(), description="Load survey files.")
21>>> coord.add_step(
22... "qc",
23... DataQCAgent(),
24... input_fn=lambda r: {
25... "sites": r["load"]["sites"],
26... "output_dir": f"{config['output_dir']}/qc",
27... },
28... description="Run data quality control.",
29... )
30>>> coord.add_step(
31... "static_shift",
32... StaticShiftAgent(method="ama"),
33... input_fn=lambda r: {
34... "sites": r["load"]["sites"],
35... "output_dir": f"{config['output_dir']}/static_shift",
36... },
37... description="Correct static-shift effects.",
38... )
39>>> coord.add_step(
40... "phase",
41... PhaseAnalysisAgent(),
42... input_fn=lambda r: {
43... "sites": r["static_shift"]["corrected_sites"],
44... "output_dir": f"{config['output_dir']}/phase",
45... },
46... description="Compute phase tensor and strike diagnostics.",
47... )
48>>> coord.add_step(
49... "export_edi",
50... EDIExportAgent(),
51... input_fn=lambda r: {
52... "sites": r["static_shift"]["corrected_sites"],
53... "output_dir": f"{config['output_dir']}/edis",
54... },
55... description="Export corrected EDI files.",
56... )
57>>> coord.add_step(
58... "report",
59... ReportAgent(formats=["md", "html"]),
60... input_fn=lambda r: {
61... "results": r,
62... "output_dir": f"{config['output_dir']}/report",
63... },
64... description="Assemble workflow report.",
65... required=False,
66... )
67>>> preview = coord.preview(config)
68>>> print(preview.summary)
69Workflow preview: 6 steps.
On a real survey, inspect the generated QC tables, static-shift warnings, phase-diagnostic figures, exported EDI paths, and report result separately. The coordinator summary tells you whether the chain completed; the nested step results tell you whether each scientific product is suitable for the next stage.
The exact output keys depend on each agent. When composing a new chain, check the agent-specific group pages:
6.11. Inversion preparation example#
Use the same pattern to prepare inversion files after correction and frequency selection.
1>>> from pycsamt.agents import (
2... AgentCoordinator,
3... DataQCAgent,
4... FrequencyDecimationAgent,
5... MTLoaderAgent,
6... Occam2DAgent,
7... StaticShiftAgent,
8... )
9>>>
10>>> config = {
11... "path": "data/AMT/WILLY_DATA/L18PLT",
12... "period_range": [0.001, 10.0],
13... "output_dir": "outputs/occam2d",
14... }
15>>> coord = AgentCoordinator("occam2d_preparation")
16>>> coord.add_step("load", MTLoaderAgent())
17>>> coord.add_step(
18... "qc",
19... DataQCAgent(),
20... input_fn=lambda r: {"sites": r["load"]["sites"]},
21... )
22>>> coord.add_step(
23... "static_shift",
24... StaticShiftAgent(method="ama"),
25... input_fn=lambda r: {"sites": r["load"]["sites"]},
26... )
27>>> coord.add_step(
28... "decimate",
29... FrequencyDecimationAgent(),
30... input_fn=lambda r: {
31... "sites": r["static_shift"]["corrected_sites"],
32... "period_range": config["period_range"],
33... "n_per_decade": 6,
34... },
35... )
36>>> coord.add_step(
37... "occam2d",
38... Occam2DAgent(),
39... input_fn=lambda r: {
40... "sites": r["static_shift"]["corrected_sites"],
41... "period_range": config["period_range"],
42... "output_dir": config["output_dir"],
43... },
44... )
45>>> preview = coord.execute(config, dry_run=True)
46>>> print([step["name"] for step in preview["steps"]])
47['load', 'qc', 'static_shift', 'decimate', 'occam2d']
Notice that occam2d uses the corrected sites and the same period range
stored in config. If frequency decimation returns a new site object or a
selected-period table in your workflow, wire that exact output key into the
Occam step instead of relying on the older config value.
6.12. Custom agents#
Custom agents inherit from pycsamt.agents.BaseAgent and return
pycsamt.agents.AgentResult. They are useful for project-specific
checks, manifests, report annotations, or validation gates that are too local
to belong in pyCSAMT core.
1>>> import time
2>>> from pycsamt.agents import AgentResult, BaseAgent
3>>>
4>>> class SurveyNoteAgent(BaseAgent):
5... SYSTEM_PROMPT = "You are a careful MT/CSAMT survey reviewer."
6...
7... def __init__(self, *, api_key=None, model=None, llm_provider="claude"):
8... super().__init__(
9... "SurveyNoteAgent",
10... api_key=api_key,
11... model=model,
12... llm_provider=llm_provider,
13... section_preset="pseudosection",
14... )
15...
16... def execute(self, input_data):
17... self._last_cost = 0.0
18... t0 = time.time()
19... qc_summary = input_data.get("qc_summary", "")
20... note = self.query_llm(
21... f"Write a concise survey QC note: {qc_summary}",
22... max_tokens=250,
23... )
24... return AgentResult(
25... status="success",
26... summary="Survey note generated.",
27... data={"note": note or "LLM note unavailable."},
28... llm_interpretation=note,
29... elapsed_seconds=time.time() - t0,
30... cost_estimate_usd=self._last_cost,
31... )
Register the custom step like any built-in agent:
1>>> coord.add_step(
2... "survey_note",
3... SurveyNoteAgent(),
4... input_fn=lambda r: {
5... "qc_summary": r["qc"].summary,
6... },
7... description="Generate a concise QC note.",
8... required=False,
9... )
For production workflows, keep deterministic validation gates separate from optional LLM narrative steps. The workflow can then fail on a numerical or data-quality condition while still treating generated prose as an optional review aid.
6.13. Best practices#
Give every step a short, stable, lowercase name such as
"load","qc","static_shift", or"occam2d".Keep the first step simple. Usually it loads data from the top-level
config.Treat
input_fnas an explicit contract between steps. Avoid hiding complex processing inside the callback.Preview workflows before long runs or file-writing steps.
Use a custom
checkpoint_dirfor project workflows so outputs are not mixed across surveys.Mark reports, extra figures, and secondary exports as optional when they should not block numerical outputs.
Configure
pycsamt.agents.AGENT_CONFIGbefore constructing agents if the workflow should use LLM assistance.Prefer deterministic/no-LLM mode for tests, validation gates, and workflows that must be repeatable without credentials.
Preserve output summaries, warnings, generated figure paths, and checkpoint locations in the final project record.
6.14. Common mistakes#
Symptom |
Cause |
Fix |
|---|---|---|
|
The previous agent did not return the expected data key, or the step name is wrong. |
Inspect |
Later steps use raw data instead of corrected data |
The |
Point it to the correction step output, for example
|
Workflow aborts at a reporting step |
The report step is marked required. |
Use |
Resume does not skip a step |
The checkpoint directory or workflow name changed. |
Reuse the same |
Figures are missing after resume |
Figures are stripped from pickled checkpoints because they may not be picklable. |
Use saved figure paths from the original output directory or regenerate display figures. |
Agents do not use the expected LLM provider |
Agents were constructed before |
Configure LLM settings first, then instantiate agents. |