Skip to content

fix: ChainPlanner._parse null-guard + TaskRecorder.input default_factory#262

Open
Ricardo-M-L wants to merge 2 commits intoTencentCloudADP:mainfrom
Ricardo-M-L:fix/plan-parser-and-task-recorder-defaults
Open

fix: ChainPlanner._parse null-guard + TaskRecorder.input default_factory#262
Ricardo-M-L wants to merge 2 commits intoTencentCloudADP:mainfrom
Ricardo-M-L:fix/plan-parser-and-task-recorder-defaults

Conversation

@Ricardo-M-L
Copy link
Copy Markdown

Summary

Fixes #261. Two small defensive-coding fixes in utu/agents/:

  1. ChainPlanner._parse (utu/agents/orchestrator/chain.py) — guard the <plan> regex match so a malformed LLM output surfaces a ValueError with a clear message instead of AttributeError: 'NoneType' object has no attribute 'group'. The preceding <analysis> match on line 82 already uses the same defensive style (if match else "").

  2. TaskRecorder.input (utu/agents/common.py) — use default_factory=list so the runtime default matches the declared type str | list[TResponseInputItem]. Other list-typed fields on the same dataclass (trajectories, raw_run_results) already use default_factory=list; input was the outlier with default_factory=dict.

Changes

 # utu/agents/orchestrator/chain.py
 match = re.search(r"<plan>\s*\[(.*?)\]\s*</plan>", text, re.DOTALL)
+if match is None:
+    raise ValueError("Failed to parse plan: <plan>[...]</plan> block not found in LLM output")
 plan_content = match.group(1).strip()
 # utu/agents/common.py
-input: str | list[TResponseInputItem] = field(default_factory=dict)
+input: str | list[TResponseInputItem] = field(default_factory=list)

Net: +3/-1 lines across two files, split into two commits so each fix is easy to bisect or cherry-pick.

Test plan

  • pre-commit run (ruff check + ruff format) passes locally
  • No new tests added — each fix is a one-line behavioural tightening whose effect is observable by reading the surrounding code; happy to add targeted unit tests if you'd prefer.

🤖 Generated with Claude Code

Ricardo-M-L and others added 2 commits April 17, 2026 01:21
When the LLM output does not contain a <plan>[...]</plan> block,
re.search returns None and calling .group(1) raises AttributeError,
which is a confusing way to surface a parser/LLM-format problem.

Raise ValueError with a clear message instead. The preceding <analysis>
block (line 81-82) already uses `if match else ""` - this mirrors that
defensive style. The subsequent assert on line 92 already fails hard
when no tasks are parsed; this gives an equally clear error when the
outer block is missing entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaskRecorder.input is annotated as `str | list[TResponseInputItem]`,
but its default_factory is `dict`, so when a TaskRecorder is created
without an explicit input the runtime value is `{}` - neither a str
nor a list. Downstream code in simple_agent.py:264 and chain.py:32
uses the field as either a string message body or a list of input
items, both of which break when given an empty dict.

Other list-typed fields on the same dataclass (`trajectories`,
`raw_run_results`) already use the correct factory; this looks like
a copy-paste slip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two small bugs: ChainPlanner._parse crashes on missing <plan> block; TaskRecorder.input wrong default_factory

1 participant