Agent Coordinator#

pycsamt.agents.AgentCoordinator is the explicit workflow runner for the agent layer. It executes a named sequence of agent steps, passes outputs from earlier steps into later steps, writes checkpoints, supports dry-run previews, and returns one workflow-level pycsamt.agents.AgentResult.

Use the coordinator when the chain is known and should be reproducible. Use WorkflowOrchestratorAgent when a natural-language request should decide which chain to build.

What the coordinator solves#

Many pyCSAMT workflows are naturally staged:

load data
-> quality control
-> correction
-> tensor diagnostics
-> inversion preparation
-> interpretation and report

Each stage can be represented by one agent. The coordinator provides the glue:

  • a stable workflow name;

  • an ordered list of named steps;

  • input_fn callbacks that map previous results into the next agent input;

  • optional steps that do not abort the workflow when they fail;

  • 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.

Core objects#

WorkflowStep

Internal descriptor for one step. It stores the step name, agent instance, optional input mapping function, description, and whether the step is required.

AgentCoordinator

Public runner that registers steps with add_step(...) and runs them with execute(...) or previews them with preview(...).

AgentResult

Standard result object returned by each step and by the coordinator itself. The coordinator result stores per-step results in result.data.

Minimal workflow#

The first step usually receives the top-level workflow configuration directly. Later steps usually receive selected outputs from earlier results.

 1from pycsamt.agents import AgentCoordinator, DataQCAgent, MTLoaderAgent
 2
 3coord = AgentCoordinator("mt_qc")
 4
 5coord.add_step(
 6    "load",
 7    MTLoaderAgent(),
 8    description="Load EDI files into a Sites object.",
 9)
10
11coord.add_step(
12    "qc",
13    DataQCAgent(),
14    input_fn=lambda results: {
15        "sites": results["load"]["sites"],
16    },
17    description="Compute survey and station QC.",
18)
19
20workflow = coord.execute({
21    "path": "data/edis",
22})
23
24print(workflow.status)
25print(workflow.summary)
26print(workflow["qc"].summary)

The coordinator’s data dictionary is keyed by step name. Each value is the AgentResult returned by that step.

Step registration#

Register steps with AgentCoordinator.add_step.

 1coord.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:

name

Unique identifier for the step. It is used in results[name], checkpoint filenames, logs, and preview output.

agent

A BaseAgent instance. Construct the agent before registration so it resolves the desired LLM provider and model.

input_fn

Optional 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.

description

Human-readable action text shown in dry-run previews and progress output.

required

True by 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.

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.

The callable receives only the accumulated step results:

1def 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, capture them with a closure:

 1workflow_config = {
 2    "path": "data/edis",
 3    "output_dir": "outputs/willy",
 4    "period_range": (0.001, 10.0),
 5}
 6
 7def 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
14coord.add_step("qc", DataQCAgent(), input_fn=qc_input)
15result = coord.execute(workflow_config)

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.

Dry-run preview#

Use dry_run=True or call preview(...) to inspect the workflow before running agents.

1preview = coord.execute(
2    {"path": "data/edis"},
3    dry_run=True,
4)
5
6print(preview["plan"])
7print(preview["steps"])

Equivalent explicit form:

1preview = coord.preview({"path": "data/edis"})

The preview includes:

  • workflow name;

  • number of registered steps;

  • formatted input config;

  • step order;

  • agent class for each step;

  • LLM provider/model or "no-LLM";

  • whether the step is required;

  • the step description.

No agent execute(...) method is called during preview.

Checkpoints and resume#

The coordinator writes checkpoints after each step. By default, checkpoints go to:

pycsamt_agent_checkpoints/<workflow_name>/

For each step, two files are written when possible:

<step>.pkl

Pickled AgentResult, used by resume=True.

<step>.json

Human-readable sidecar containing status, summary, elapsed time, cost, warnings, and error information.

At the end of the workflow, the coordinator also writes:

workflow_state.json

Summary of the workflow and status/cost metadata for all recorded steps.

Set a custom checkpoint directory when constructing the coordinator:

1coord = AgentCoordinator(
2    "willy_qc",
3    checkpoint_dir="outputs/willy/checkpoints",
4)

Resume from existing checkpoints:

1result = coord.execute(
2    {"path": "data/edis"},
3    resume=True,
4)

Clear checkpoints for a workflow:

1coord.reset_checkpoints()

Use checkpoints for long-running or expensive workflows. For quick notebook experiments, you can leave the default directory or reset it between runs.

Required and optional steps#

Required steps abort the workflow when they fail:

1coord.add_step(
2    "qc",
3    DataQCAgent(),
4    input_fn=lambda r: {"sites": r["load"]["sites"]},
5    required=True,
6)

Optional steps let the workflow continue:

 1coord.add_step(
 2    "report",
 3    ReportAgent(formats=["md", "html"]),
 4    input_fn=lambda r: {
 5        "results": r,
 6        "output_dir": "outputs/report",
 7    },
 8    description="Build a report if report dependencies are available.",
 9    required=False,
10)

Use optional steps for report generation, extra plots, secondary exports, or experimental analyses. Keep core loading, QC, correction, and inversion-input steps required when later steps depend on them.

Workflow result structure#

execute(...) returns one AgentResult.

 1result = coord.execute({"path": "data/edis"})
 2
 3print(result.status)
 4print(result.summary)
 5print(result.elapsed_seconds)
 6print(result.cost_estimate_usd)
 7print(result.warnings)
 8
 9load_result = result["load"]
10qc_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.

summary

Human-readable workflow completion or failure summary.

data

Dictionary of step_name -> AgentResult.

warnings

Combined warnings from step results plus coordinator warnings.

cost_estimate_usd

Sum of per-step LLM cost estimates for steps run in this execution.

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.

 1from pycsamt.agents import (
 2    AgentCoordinator,
 3    DataQCAgent,
 4    EDIExportAgent,
 5    MTLoaderAgent,
 6    PhaseAnalysisAgent,
 7    ReportAgent,
 8    StaticShiftAgent,
 9)
10
11config = {
12    "path": "data/edis",
13    "output_dir": "outputs/willy",
14}
15
16coord = AgentCoordinator(
17    "willy_qc_correction",
18    checkpoint_dir=f"{config['output_dir']}/checkpoints",
19)
20
21coord.add_step(
22    "load",
23    MTLoaderAgent(),
24    description="Load survey files.",
25)
26
27coord.add_step(
28    "qc",
29    DataQCAgent(),
30    input_fn=lambda r: {
31        "sites": r["load"]["sites"],
32        "output_dir": f"{config['output_dir']}/qc",
33    },
34    description="Run data quality control.",
35)
36
37coord.add_step(
38    "static_shift",
39    StaticShiftAgent(method="ama"),
40    input_fn=lambda r: {
41        "sites": r["load"]["sites"],
42        "output_dir": f"{config['output_dir']}/static_shift",
43    },
44    description="Correct static-shift effects.",
45)
46
47coord.add_step(
48    "phase",
49    PhaseAnalysisAgent(),
50    input_fn=lambda r: {
51        "sites": r["static_shift"]["corrected_sites"],
52        "output_dir": f"{config['output_dir']}/phase",
53    },
54    description="Compute phase tensor and strike diagnostics.",
55)
56
57coord.add_step(
58    "export_edi",
59    EDIExportAgent(),
60    input_fn=lambda r: {
61        "sites": r["static_shift"]["corrected_sites"],
62        "output_dir": f"{config['output_dir']}/edis",
63    },
64    description="Export corrected EDI files.",
65)
66
67coord.add_step(
68    "report",
69    ReportAgent(formats=["md", "html"]),
70    input_fn=lambda r: {
71        "results": r,
72        "output_dir": f"{config['output_dir']}/report",
73    },
74    description="Assemble workflow report.",
75    required=False,
76)
77
78preview = coord.preview(config)
79print(preview["plan"])
80
81result = coord.execute(config)
82print(result.summary)

The exact output keys depend on each agent. When composing a new chain, check the agent-specific group pages:

Inversion preparation example#

Use the same pattern to prepare inversion files after correction and frequency selection.

 1from pycsamt.agents import (
 2    AgentCoordinator,
 3    DataQCAgent,
 4    FrequencyDecimationAgent,
 5    MTLoaderAgent,
 6    Occam2DAgent,
 7    StaticShiftAgent,
 8)
 9
10coord = AgentCoordinator("occam2d_preparation")
11
12coord.add_step("load", MTLoaderAgent())
13
14coord.add_step(
15    "qc",
16    DataQCAgent(),
17    input_fn=lambda r: {"sites": r["load"]["sites"]},
18)
19
20coord.add_step(
21    "static_shift",
22    StaticShiftAgent(method="ama"),
23    input_fn=lambda r: {"sites": r["load"]["sites"]},
24)
25
26coord.add_step(
27    "decimate",
28    FrequencyDecimationAgent(),
29    input_fn=lambda r: {
30        "sites": r["static_shift"]["corrected_sites"],
31        "period_range": [0.001, 10.0],
32        "n_per_decade": 6,
33    },
34)
35
36coord.add_step(
37    "occam2d",
38    Occam2DAgent(),
39    input_fn=lambda r: {
40        "sites": r["static_shift"]["corrected_sites"],
41        "period_range": [0.001, 10.0],
42        "output_dir": "outputs/occam2d",
43    },
44)
45
46result = coord.execute({"path": "data/edis"})

Custom agents#

Custom agents inherit from pycsamt.agents.BaseAgent and return pycsamt.agents.AgentResult.

 1import time
 2
 3from pycsamt.agents import AgentResult, BaseAgent
 4
 5class SurveyNoteAgent(BaseAgent):
 6    SYSTEM_PROMPT = "You are a careful MT/CSAMT survey reviewer."
 7
 8    def __init__(self, *, api_key=None, model=None, llm_provider="claude"):
 9        super().__init__(
10            "SurveyNoteAgent",
11            api_key=api_key,
12            model=model,
13            llm_provider=llm_provider,
14            section_preset="pseudosection",
15        )
16
17    def execute(self, input_data):
18        self._last_cost = 0.0
19        t0 = time.time()
20
21        qc_summary = input_data.get("qc_summary", "")
22        note = self.query_llm(
23            f"Write a concise survey QC note: {qc_summary}",
24            max_tokens=250,
25        )
26
27        return AgentResult(
28            status="success",
29            summary="Survey note generated.",
30            data={"note": note or "LLM note unavailable."},
31            llm_interpretation=note,
32            elapsed_seconds=time.time() - t0,
33            cost_estimate_usd=self._last_cost,
34        )

Register the custom step like any built-in agent:

1coord.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)

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_fn as 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_dir for 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_CONFIG before constructing agents if the workflow should use LLM assistance.

  • Prefer deterministic/no-LLM mode for unit tests and documentation examples.

Common mistakes#

Symptom

Cause

Fix

KeyError in input_fn

The previous agent did not return the expected data key, or the step name is wrong.

Inspect result["previous_step"].data and update the mapping.

Later steps use raw data instead of corrected data

The input_fn still points to results["load"]["sites"].

Point it to the correction step output, for example results["static_shift"]["corrected_sites"].

Workflow aborts at a reporting step

The report step is marked required.

Use required=False for non-critical deliverables.

Resume does not skip a step

The checkpoint directory or workflow name changed.

Reuse the same workflow_name and checkpoint_dir.

Agents do not use the expected LLM provider

Agents were constructed before AGENT_CONFIG was configured.

Configure LLM settings first, then instantiate agents.