Skip to content

fix: parse nested JSON in LLM responses + runtime bug fixes#301

Closed
lbartoszcze wants to merge 1 commit intomainfrom
fix/parse-action-nested-json-and-runtime-bugs
Closed

fix: parse nested JSON in LLM responses + runtime bug fixes#301
lbartoszcze wants to merge 1 commit intomainfrom
fix/parse-action-nested-json-and-runtime-bugs

Conversation

@lbartoszcze
Copy link
Copy Markdown
Contributor

Summary

Fixes 4 runtime bugs that affect every agent running on Singularity, with 57 tests proving correctness.

1. _parse_action silently drops params on nested JSON (CRITICAL)

The bug: The regex r'\{[^{}]*"tool"[^{}]*\}' in cognition.py:_parse_action() uses [^{}]* which rejects any JSON containing nested braces. When the LLM returns the common case:

{"tool": "shell:bash", "params": {"command": "git status"}, "reasoning": "check repo"}

...the regex fails to match because {"command": "git status"} contains braces. The fallback strips ALL params, so the agent knows what to do but loses how to do it.

The fix: Replaced regex with balanced-brace extraction (_extract_tool_json) that scans for {, counts depth to find the matching }, and attempts json.loads. Correctly handles nested objects, arrays, and braces in strings.

19 tests cover flat JSON, nested params, deeply nested objects, markdown code blocks, arrays, missing keys, invalid JSON, and fallback paths.

2. Vertex AI uses sync client in async think() (HIGH)

The bug: cognition.py imports AnthropicVertex (sync) instead of AsyncAnthropicVertex, and calls self.llm.messages.create() without await in the async think() method. This blocks the entire event loop during inference.

The fix: Import AsyncAnthropicVertex, add await to the Vertex call path.

3. Orchestrator _message() / _broadcast() KeyError (HIGH)

The bug: Both methods access _message_boxes[agent_id] without checking if the key exists. If an agent's message box wasn't pre-initialized (e.g., loaded from a previous session), this crashes with KeyError.

The fix: Guard both methods to create the queue on demand: if agent_id not in _message_boxes: _message_boxes[agent_id] = asyncio.Queue().

7 tests cover missing boxes, existing boxes, non-existent agents, self-skip, and dead agent filtering.

4. request.py Linear API call has no timeout (MEDIUM)

The bug: aiohttp.ClientSession() in _create_linear_ticket() has no timeout, risking indefinite hangs if the Linear API is unreachable.

The fix: Added aiohttp.ClientTimeout(total=30).

Files changed

File Change
singularity/cognition.py New _extract_tool_json() method, AsyncAnthropicVertex import, await on Vertex call
singularity/skills/orchestrator.py Guard _message_boxes access in _message() and _broadcast()
singularity/skills/request.py Add 30s timeout to aiohttp session
pyproject.toml Add pytest-timeout to dev deps, pytest config
.github/workflows/ci.yml CI workflow (lint + test on 3.10/3.11/3.12 + build)
tests/test_parse_action.py 19 tests for JSON parsing
tests/test_orchestrator_messaging.py 7 tests for messaging
tests/test_cognition_engine.py 14 tests for engine init, cost calc, prompts
tests/test_smoke.py 17 smoke tests for imports and skill instantiation

Test plan

  • 57 tests all pass locally (pytest tests/ -v --timeout=30)
  • Lint passes on modified files (ruff check --select F821,F811,F601)
  • Package builds and installs cleanly

🤖 Generated with Claude Code

The _parse_action regex r'\{[^{}]*"tool"[^{}]*\}' rejected any JSON with
nested braces, silently dropping all params when the LLM returned nested
objects (the common case). Agents could decide WHAT to do but lost HOW —
a shell:bash action would lose its command, email:send would lose recipients.

Replaced with balanced-brace extraction that correctly handles nested params
like {"tool": "shell:bash", "params": {"command": "ls"}, "reasoning": "..."}.

Also fixes:
- cognition.py: Vertex AI used sync AnthropicVertex in async think(),
  blocking the event loop. Replaced with AsyncAnthropicVertex + await.
- orchestrator.py: _message() and _broadcast() accessed _message_boxes
  without existence check, causing KeyError when agent's queue wasn't
  pre-initialized. Added on-demand queue creation.
- request.py: aiohttp call to Linear API had no timeout, risking
  indefinite hangs. Added 30s ClientTimeout.

57 tests covering parse logic, orchestrator messaging, cognition engine,
and skill smoke tests. CI workflow restored.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lbartoszcze lbartoszcze deleted the fix/parse-action-nested-json-and-runtime-bugs branch February 11, 2026 18:50
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.

1 participant