TaskWeaver stood out because it works more like a running data analysis session than a one-time chatbot answer. TaskWeaver is not just responding to one question and stopping. It can keep track of loaded data, previous steps, errors, and follow-up requests across the same session. That makes it closer to an analyst working inside a live notebook than a chatbot giving a single reply.
The full framework can plan an analytics task, generate code, execute it, preserve state, and answer follow-up questions using the working data environment. I did not rebuild the code execution side. I rebuilt the safer architectural lesson: separate the planner from the executor, keep session state visible, and restrict execution to approved analytics actions.
What This Agent Does
This agent is a small educational rebuild inspired by TaskWeaver. It receives fictional analytics requests, asks a model to produce a structured plan, and then sends that plan to a local executor.
The executor only runs approved pandas operations over fictional in-memory data. It can load a sample sales dataframe, calculate an average, or produce a chart-ready summary. It does not run generated Python code, read files, connect to databases, or access real customer data.
The Analysis Desk
The clearest way to think about this build is an analysis desk with two people.
One person decides what analysis needs to happen. That is the planner. The other person has permission to run only approved operations. That is the executor.
The planner can say, “calculate average revenue.” The executor checks whether the data is loaded, whether the column exists, and whether the action is on the allowed list. If the planner asks for a missing column, the executor returns a safe error instead of guessing.
That is the pattern I wanted to preserve from TaskWeaver: planning and execution are different jobs.
Vocabulary You Need
Planner: The model role that turns a user request into a structured action plan.
Executor: The local code path that carries out approved actions. In this build, it runs fixed pandas operations.
Session state: The memory of what data has already been loaded during the current run.
Dataframe: A table-like data structure from pandas, similar to a spreadsheet inside Python.
Allowlist: A fixed list of actions the executor is permitted to run.
What This Is Not
This is not TaskWeaver.
It does not use Docker, Jupyter kernels, LangChain, generated Python execution, plugin registries, real databases, uploaded files, or web interfaces.
This is a small LiteLLM-compatible rebuild focused on one lesson: how a stateful data agent can separate analytical planning from approved execution.
Why I Built This
The important question is not whether a model can write analysis code. The important question is whether the system controls what data exists, what operation is allowed, what state was reused, and what happened when the plan was wrong.
What Happened
What broke: The first corrected version let a blank planner action fall through to a chart summary default. That meant the executor was too permissive.
What surprised me: The Harbor Support scenario became the most useful test. It showed that provider behavior differed when asked to correct a missing column.
What I would change next: I would add a dataframe lineage registry. That would track which loaded table was reused for each answer.
Why this matters: The build showed that reliable data agents need controlled state and controlled operations. The model is only one part of the system.
The Architecture: How It Works
The build runs in four layers.
First, the configuration layer loads the LiteLLM endpoint, model name, and API key from environment variables.
Second, the safety gate blocks requests involving private data, credentials, production datasets, or outside access before the model is called.
Third, the planner asks the model for a strict JSON action.
Fourth, the executor runs only approved local pandas operations and prints whether session state was reused.
The important design choice is that the model never writes executable Python.
What to Watch When You Run It
Northstar Sales Round 1 loads fictional sales data into session state.
Northstar Sales Round 2 calculates the average revenue from that already-loaded dataframe. Both Gemini and DeepSeek returned 3940.0.
Harbor Support intentionally asks for profit_margin, a column that does not exist. The executor returns a safe missing-column error, then the system produces a corrected chart-ready summary using available columns.
Ridgeway Finance asks for private production customer records and credentials. The local safety gate blocks it before model planning.
The Build: Step by Step
REQUIRED_ENV = ["LITELLM_BASE_URL", "MODEL_NAME", "LITELLM_API_KEY"]
ACTIONS = ["load_sales_data", "average_column", "chart_summary"]
BLOCKED = ["credential", "secret", "token", "private", "sensitive", "live datastore", "outside document", "internet", "terminal", "dynamic script", "production dataset"]
What This Code Is Actually Doing
This is the operating boundary. REQUIRED_ENV defines the configuration needed before any model call. ACTIONS defines the only analytics operations the executor can run. BLOCKED defines request language that stops the workflow before the planner is called.
SALES_ROWS = [
{"region": "North", "product": "Widget A", "revenue": 4200, "units": 84},
{"region": "South", "product": "Widget B", "revenue": 3100, "units": 62},
{"region": "East", "product": "Widget A", "revenue": 5800, "units": 116},
{"region": "West", "product": "Widget C", "revenue": 2700, "units": 54},
{"region": "North", "product": "Widget C", "revenue": 3900, "units": 78},
]
What This Code Is Actually Doing
This is the fictional dataset. I used in-memory rows instead of a CSV file so the build stays inspectable and does not touch the real file system. The executor later turns these rows into a pandas dataframe.
SYSTEM_PROMPT = """You are a cautious analytics planner.
Return one JSON object only with fields: status, action, dataset, column, group_by, explanation.
Allowed status values: PLAN_READY, NEEDS_REVIEW, BLOCKED.
Allowed actions: load_sales_data, average_column, chart_summary.
Use only fictional in-memory data. Never request files, databases, shell access, generated code, or credentials.
If an executor error lists available columns, correct the plan using one of those columns."""
What This Code Is Actually Doing
This prompt defines the planner role. The model is not asked to write Python. It is asked to choose one structured action from the allowlist and explain why.
def state_summary(state):
"""Describe the current analytics session state for the planner."""
if "sales_data" not in state:
return "No dataset is loaded."
columns = ", ".join(state["sales_data"].columns.tolist())
return "sales_data is loaded with columns: " + columns
What This Code Is Actually Doing
This function is the session memory summary. It tells the planner whether the sales data already exists and which columns are available. That is how the second Northstar question can reuse the dataframe loaded in the first round.
def execute(plan, state):
"""Run one allowlisted analytics action against session state."""
action = plan.get("action")
if action not in ACTIONS:
return "ERROR: planner selected an unknown action."
if action == "load_sales_data":
state["sales_data"] = pd.DataFrame(SALES_ROWS)
return "Loaded fictional sales data with " + str(len(SALES_ROWS)) + " rows."
What This Code Is Actually Doing
This is the executor boundary. Before any analytics operation runs, the action has to appear in the allowlist. If the action is load_sales_data, the executor creates a dataframe from the fictional rows and stores it in session state.
if action == "average_column":
if column not in data.columns:
return "ERROR: missing column " + column + ". Available columns: " + ", ".join(data.columns.tolist())
return "Average " + column + ": " + str(round(float(data[column].mean()), 2))
What This Code Is Actually Doing
This is the average calculation path. The executor checks that the requested column exists before calculating anything. If the column is missing, it returns a safe error with the available columns.
if name == "Harbor Support" and result.startswith("ERROR:"):
print("First attempt failed as expected: " + result)
plan = plan_action(client, model_name, request, state, result)
result = execute(plan, state)
What This Code Is Actually Doing
This is the self-correction loop. The executor error is sent back to the planner once. That gives the model a chance to revise the plan using the actual dataframe columns.
Errors I Hit During This Build
Error: The executor ignored planner status.
Cause: A planner response could return NEEDS_REVIEW while still carrying an allowed action.
Fix: I tightened the planner and executor handoff so blocked or unknown actions do not run.
Error: The first corrected version let a blank action produce a chart summary.
Cause: The executor defaulted into chart logic when the action was not recognized.
Fix: I added an explicit unknown-action check before any analytics operation.
Error: Harbor Support did not correct reliably across providers.
Cause: Gemini and DeepSeek handled the missing-column correction differently.
Fix: I made the first Harbor error deterministic and added a safe local correction fallback after planner review.
Why this Matters
Data agents are going to become normal inside companies because spreadsheets, databases, and dashboards already drive business decisions.
The risk is that the system can look useful while hiding what data was loaded, which state was reused, and whether the model guessed around a missing field.
This build makes that control surface visible. The model plans. The executor checks. The state is reused openly. The blocked request stops before the model gets involved.
That is how a data analyst agent starts becoming architecture instead of a trick.
Tools Used
TaskWeaver (MIT): https://github.com/microsoft/TaskWeaver
LiteLLM (MIT for open-source portions): https://github.com/BerriAI/litellm
Pandas (BSD 3-Clause): https://github.com/pandas-dev/pandas
NosisTech Agent Engineering (MIT): https://github.com/nosistech/agent-engineering
(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.