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
23 changes: 23 additions & 0 deletions s16code/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,29 @@ async def plan(self, graph: GraphSnapshot, event: Event) -> GraphPatch:

def _parse(self, text: str, graph: GraphSnapshot) -> GraphPatch:
data = _json_object(text)
# A few local/open-weight providers label the advertised next frontier
# ``new_tasks`` even after repair. Treat that as a wire alias for
# ``add`` while keeping the strict task and capability validation below.
if "new_tasks" in data and "add" not in data:
data["add"] = data.pop("new_tasks")
additions = data.get("add")
if isinstance(additions, list):
normalized_additions: list[Any] = []
for index, raw in enumerate(additions, start=1):
if isinstance(raw, dict):
raw = dict(raw)
if "id" not in raw and isinstance(raw.get("node_id"), str):
raw["id"] = raw.pop("node_id")
if "capability" not in raw:
provider_name = raw.get("name")
if isinstance(provider_name, str) and provider_name in self.registry:
raw["capability"] = raw.pop("name")
if "id" not in raw and isinstance(raw.get("capability"), str):
raw["id"] = f"{raw['capability']}_{len(graph.nodes) + index}"
if set(raw).issubset({"id", "capability", "arguments", "depends_on"}):
raw.setdefault("depends_on", [])
normalized_additions.append(raw)
data["add"] = normalized_additions
# Normalize the provider-style {"capability": {arguments}} shorthand
# for every advertised capability. This is a protocol adapter, not a
# task, benchmark, or domain fallback.
Expand Down
46 changes: 46 additions & 0 deletions tests/test_general_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,52 @@ async def test_task_keyed_provider_json_is_normalized_by_capability_schema():
("research_github", "researcher"), ("research_gmail", "researcher")]


@pytest.mark.asyncio
async def test_new_tasks_provider_alias_is_normalized_to_add():
reply = Replies({
"new_tasks": [{
"name": "answer_with_evidence",
"arguments": {"query": "Give a concise greeting."},
}],
"cancel": [],
"finish": False,
"reason": "answer the direct request",
})
planner = GeneralAgentPlanner(
reply,
default_registry(),
goal="Give a concise greeting.",
review_terminal=False,
)
patch = await planner.plan(snapshot(), Event(1, "run_started", None, {}))
assert [(task.id, task.skill) for task in patch.add] == [
("answer_with_evidence_1", "answer_with_evidence")
]


@pytest.mark.asyncio
async def test_new_tasks_node_id_alias_and_missing_dependencies_are_normalized():
reply = Replies({
"new_tasks": [{
"node_id": "clock",
"capability": "current_datetime",
"arguments": {"timezone": "UTC"},
}],
"cancel": [],
"finish": False,
"reason": "read the clock",
})
planner = GeneralAgentPlanner(
reply,
default_registry(),
goal="What time is it?",
review_terminal=False,
)
patch = await planner.plan(snapshot(), Event(1, "run_started", None, {}))
assert [(task.id, task.skill) for task in patch.add] == [("clock", "current_datetime")]
assert patch.connect == ()


@pytest.mark.asyncio
async def test_capability_prefixed_task_key_is_normalized_without_a_channel_table():
reply = Replies({"send_channel_message_3": {
Expand Down