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_fncallbacks 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#
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.
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:
nameUnique identifier for the step. It is used in
results[name], checkpoint filenames, logs, and preview output.agentA
BaseAgentinstance. Construct the agent before registration so it resolves the desired LLM provider and model.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.
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>.pklPickled
AgentResult, used byresume=True.<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:
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.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.
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_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 unit tests and documentation examples.
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 |
Agents do not use the expected LLM provider |
Agents were constructed before |
Configure LLM settings first, then instantiate agents. |