Skip to content

Commit 3a0516a

Browse files
authored
Fix tool-call extraction and populate events on the run() path (#504)
1 parent c99e2cf commit 3a0516a

5 files changed

Lines changed: 990 additions & 296 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2929
- Worker processes killed by a signal now log a diagnostic hint (signal number, `PYTHONFAULTHANDLER=1` guidance) instead of restarting silently
3030
- Per-schedule pause/resume now work on both Conductor server families: the client sends `PUT` (the OSS Conductor dialect — the spec-generated `GET` failed there) and transparently falls back to `GET` on a 405 for Orkes servers
3131
- `Agent(model="claude-code/...")` registration no longer crashes with `SpawnSafetyError`/`PicklingError` under the `spawn` start method (top-level or nested as a sub-agent); the tool worker is now built through the same picklable passthrough path already used by other frameworks
32+
- `AgentResult.tool_calls` no longer drops or misnames tool calls. A tool task was identified by a `call_` reference-name prefix, which is the LLM provider's tool-call id format — so an Anthropic-backed agent (`toolu_…`) recorded no tool calls at all. Tools are now identified by the task type they compiled to, and named from `_agent_tool_name` (set by the server on every tool kind), `method`, or the task definition. Consequences: `http`, `api`, `mcp`, `agent_tool`, `human` and `pull_workflow_messages` tools now appear at all; media and RAG tools report their own name instead of `generate_image`/`llm_index_text`; and a camelCase worker name is no longer lowercased, so `assert_tool_used(result, "getWeather")` matches
33+
- `AgentResult.events` is now populated by `run()` and `run_async()` for natively-compiled agents, derived from the same execution already fetched for `tool_calls` rather than from an extra call. It was previously only filled in when an `on_event` callback or `stream()` was used, so the eight `conductor.ai.agents.testing` assertions that read events — `assert_no_errors`, `assert_handoff_to`, `assert_events_contain`, `assert_max_turns` and the rest — saw an empty list. Four of them check for *absence* and so passed vacuously, `assert_no_errors` on every eval case (`expect_no_errors` defaults to `True`); `expect_handoff_to`, used by the eval runner's own documented example, failed every time. Framework agents (LangChain, LangGraph, OpenAI Agents, Claude Agent SDK) also derive both fields from their execution's task list now: the whole agent runs inside one passthrough task, but the Claude Agent SDK injects a task per tool call, so those tool calls appear. LangChain and LangGraph publish their steps to the event stream rather than as tasks, so that detail still reaches `on_event` and `stream()` consumers only
34+
- Tool arguments reported on `AgentEvent.args` and in `AgentResult.tool_calls` now agree, and both strip every key Conductor injects — previously `_agent_tool_name`, `_allowed_commands`, `__humanTaskDefinition` and `__conductor_agent_ctx__` leaked into one surface or the other

‎src/conductor/ai/agents/result.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class AgentResult:
106106
error: Human-readable error message when the agent failed.
107107
token_usage: Aggregated token usage across all LLM calls.
108108
metadata: Extra data from the workflow execution.
109+
events: The execution's event history (:class:`AgentEvent`), whether the
110+
agent was streamed or run to completion.
109111
sub_results: Per-agent outputs for multi-agent strategies (parallel).
110112
Empty dict for single-agent runs. Keyed by agent name.
111113
"""
@@ -691,6 +693,20 @@ def __repr__(self) -> str:
691693
# ── AgentEvent (yielded by stream()) ───────────────────────────────────
692694

693695

696+
# Keys Conductor injects into a tool task's input, not arguments the LLM chose.
697+
# Shared with the runtime's tool_calls extraction so both report the same args.
698+
INTERNAL_ARG_KEYS = frozenset(
699+
{
700+
"_agent_state",
701+
"_agent_tool_name",
702+
"_allowed_commands",
703+
"method",
704+
"__humanTaskDefinition",
705+
"__conductor_agent_ctx__",
706+
}
707+
)
708+
709+
694710
class EventType(str, Enum):
695711
"""Types of events emitted during agent execution."""
696712

@@ -723,9 +739,6 @@ class AgentEvent:
723739
guardrail_name: Guardrail name (for ``guardrail_pass``, ``guardrail_fail``).
724740
"""
725741

726-
# Keys injected by Conductor that should not appear in user-facing args.
727-
_INTERNAL_ARG_KEYS = frozenset({"_agent_state", "method"})
728-
729742
type: str
730743
content: Optional[str] = None
731744
tool_name: Optional[str] = None
@@ -738,7 +751,7 @@ class AgentEvent:
738751

739752
def __post_init__(self):
740753
if self.args and isinstance(self.args, dict):
741-
cleaned = {k: v for k, v in self.args.items() if k not in self._INTERNAL_ARG_KEYS}
754+
cleaned = {k: v for k, v in self.args.items() if k not in INTERNAL_ARG_KEYS}
742755
object.__setattr__(self, "args", cleaned if cleaned else None)
743756

744757

0 commit comments

Comments
 (0)