Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions app/subagent/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,7 @@ def _fail_unparseable(self, sub: SubAgent, parse_error: Optional[str]) -> None:

async def _dispatch_action(self, sub: SubAgent, decision: Dict[str, Any]) -> None:
action_name = decision.get("action_name") or ""
parameters = decision.get("parameters") or {}
if not isinstance(parameters, dict):
parameters = {}
parameters = self._extract_parameters(decision)

# Enforce the frozen action list — refuse anything else.
if action_name not in sub.compiled_actions:
Expand Down Expand Up @@ -309,6 +307,22 @@ async def _dispatch_action(self, sub: SubAgent, decision: Dict[str, Any]) -> Non
input_data=parameters,
)

@staticmethod
def _extract_parameters(decision: Dict[str, Any]) -> Dict[str, Any]:
"""Return action inputs from the accepted decision parameter keys."""
parameters = decision.get("parameters")
if isinstance(parameters, dict):
return parameters

# The compact action list describes schemas under ``params``. Some
# providers mirror that key in their decision even though the required
# output shape says ``parameters``.
params_alias = decision.get("params")
if isinstance(params_alias, dict):
return params_alias

return {}

# ------------------------------------------------------------------
# Session-cache management
# ------------------------------------------------------------------
Expand Down
88 changes: 88 additions & 0 deletions tests/test_subagent_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-

import asyncio

from agent_core.core.action import Action
from app.subagent.runner import SubAgentRunner
from app.subagent.types import SubAgent


class _FakeActionLibrary:
def __init__(self):
self.action = Action(
name="sub_task_end",
description="End sub-agent",
action_type="atomic",
mode="CLI",
)

def retrieve_action(self, action_name):
if action_name == self.action.name:
return self.action
return None


class _FakeActionManager:
def __init__(self):
self.calls = []

async def execute_action(self, **kwargs):
self.calls.append(kwargs)
return {"status": "success"}


def _make_runner():
action_manager = _FakeActionManager()
runner = SubAgentRunner(
subagent_manager=None,
action_manager=action_manager,
action_library=_FakeActionLibrary(),
event_stream_manager=None,
llm_interface=None,
)
return runner, action_manager


def _make_subagent():
return SubAgent(
id="sub_test",
agent_type="research_agent",
parent_task_id="parent_test",
query="test query",
compiled_actions=["sub_task_end"],
)


def test_dispatch_action_accepts_params_alias():
runner, action_manager = _make_runner()
payload = {"status": "completed", "result": "done"}

asyncio.run(
runner._dispatch_action(
_make_subagent(),
{"action_name": "sub_task_end", "params": payload},
)
)

assert action_manager.calls[0]["input_data"] == payload
assert action_manager.calls[0]["session_id"] == "sub_test"


def test_dispatch_action_prefers_parameters_key():
runner, action_manager = _make_runner()

asyncio.run(
runner._dispatch_action(
_make_subagent(),
{
"action_name": "sub_task_end",
"parameters": {"status": "failed", "result": "primary"},
"params": {"status": "completed", "result": "alias"},
},
)
)

assert action_manager.calls[0]["input_data"] == {
"status": "failed",
"result": "primary",
}