Common Tools¶
The functions in src/tools/common_tools.py operate at two levels — they are the concrete tools that agents call and the global state machine that controls access to them.
There are three common tools: todo, read_csv, and append_csv¶
todo¶
Read or update the task plan. This is a stateful tool — tasks persist in memory for the lifetime of the agent run.
# Write a plan
todo([
{"id": 1, "task": "Scrape LinkedIn jobs", "status": "pending"},
{"id": 2, "task": "Scrape Indeed jobs", "status": "pending"},
{"id": 3, "task": "Merge and deduplicate", "status": "pending"},
])
# Read current state
todo()
| Parameter | Default | Description |
|---|---|---|
todos |
None |
Pass a list of task dicts to write a new plan. Omit to read current state. |
Each task dict has three required keys:
| Key | Type | Values |
|---|---|---|
id |
int |
Unique task identifier |
task |
str |
Human-readable task description |
status |
str |
"pending", "in-progress", or "completed" |
Output uses unicode icons: ○ pending, ◉ in-progress, ✓ completed.
It allows the agent to remember the tasks in long runs without drifing from the goal.
read_csv¶
Read a CSV file from data/ and return it as a markdown table.
| Parameter | Default | Description |
|---|---|---|
filename |
— | Just the filename. Looked up in data/. |
max_rows |
200 | Maximum rows to return. |
The output is a formatted markdown table with aligned columns and a row count footer when results are truncated.
append_csv¶
Append rows to a CSV file in data/.
append_csv("jobs.csv", [
{"title": "Software Engineer", "company": "Acme Corp", "source": "linkedin"},
{"title": "Product Manager", "company": "Beta Inc", "source": "indeed"},
])
Rules:
- The CSV must pre-exist with a header row.
append_csvdoes not create files. - All row keys must match the existing headers exactly — unknown columns are rejected.
- Missing columns in a row are also rejected.
- Whitelist checks apply (see below).
How CSV Injection Works¶
The sub-agent gets csv_toolset — a bundle containing read_csv, append_csv, and todo — only when called through run_sub_agent().
The two call paths¶
User / Beat
│
├── Agent Mode ──→ run_sub_agent()
│ │
│ ├── injects csv_toolset ──→ sub_agent has CSV+todo tools
│ └── calls sub_agent.run_sync(prompt, toolsets=[...])
│
└── Swarm Mode ──→ run_agent() ──→ spawn_sub_agents()
│
└── calls sub_agent.run(prompt)
(no toolsets= argument)
→ sub_agent does NOT get CSV+todo tools
Why the distinction¶
| Mode | Why CSV tools are (or aren't) injected |
|---|---|
| Agent mode | One sub-agent, full task ownership. It needs to track its own plan and persist results to CSVs. |
| Swarm mode | Multiple parallel sub-agents. CSV access from concurrent workers would cause race conditions and file corruption. The orchestrator owns the plan and synthesis. |
How it's wired¶
In run_sub_agent():
csv_toolset = FunctionToolset(tools=[_read_csv, _append_csv, _todo])
return sub_agent.run_sync(
prompt,
toolsets=[csv_toolset],
...
)
FunctionToolset is the pydantic-ai mechanism for injecting plain Python functions as agent tools at call time. The tools are not registered on the agent itself — they're passed per-run, so they only appear when run_sub_agent() is the entry point.
CSV Whitelisting¶
CSV access is governed by one global variable in common_tools.py:
| Variable | Type | Meaning |
|---|---|---|
_beat_allowed_csvs |
list[str] \| None |
None = unrestricted (REPL). list = only those filenames allowed (beat). |
In beat mode the _beat_allowed_csvs is set to a list containing all of the whitelisted csv files allowed for that beat. (configured in the beat allowed_csvs). And in REPL mode it is set to NONE which allows the access to all of the csv files.
Why whitelisting exists¶
In beat mode, a single misconfigured prompt could read or corrupt any CSV on disk. Whitelisting forces the user to explicitly declare which CSVs a beat may touch. The files must pre-exist with headers so the agent never creates rogue data files.
Who Always Has These Tools¶
| Agent | read_csv |
append_csv |
todo |
How |
|---|---|---|---|---|
| Main agent (orchestrator) | Always | Always | Always (own impl) | Registered with @main_agent.tool_plain at import time — permanently attached. |
| Sub-agent (Agent mode) | Yes | Yes | Yes | Injected via csv_toolset in run_sub_agent(). |
| Sub-agent (Swarm mode) | No | No | No | Not injected — sub-agents are stateless workers. |