Skip to content
Merged
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
66 changes: 65 additions & 1 deletion agent-flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`agent-flow` is a torch-like framework for composing agent-backed layers.
Each layer implements `forward`, and each `AgentLayer.forward` performs one
Claude Code or Codex SDK execution. Modules can be composed directly or used
logical Claude Code or Codex invocation. Modules can be composed directly or used
to build small orchestration harnesses.

## Prerequisites
Expand Down Expand Up @@ -66,6 +66,70 @@ Run it directly:
python examples/quick_start.py
```

## Backend behavior

Define custom tools with `from agent_flow import tool`. Both backends accept
these definitions and existing Claude SDK tool objects. Python shorthand and
`TypedDict` inputs are normalized to JSON Schema. Explicit JSON Schemas are
normalized to the root object shape MCP tools require (a string `type`, a
`properties` key, and a root `$ref` moved into `allOf`) so both backends
advertise the same schema. Resource links and embedded text resources become
readable tool output; image results retain their MIME type. Unsupported result
types return explicit tool errors.

Declare required calls on the layer, independently of its backend:

```python
config = AgentLayerConfig(
backend=BackendConfig(kind="codex", model=CODEX_DEFAULT_MODEL, tools=tools),
required_tools=("append_reviewer_progress", "update_status"),
)
```

Every listed tool must be attempted by the root agent during that invocation.
If calls are missing, the framework sends one corrective SDK turn on the same
session, including for stateless layers. It requests only the remaining calls.
If they are still missing, `RequiredToolCallError` fails the invocation. This
policy counts attempts, not successful writes; tool handlers and workflow
validation remain responsible for the outputs. Token accounting includes both
turns, while context occupancy remains the final snapshot.

`BackendConfig.extra_mcp_servers` adds STDIO or HTTP MCP servers to one session.
Codex translates these into thread configuration without editing global config.
`AgentLayerConfig.disallowed_tools` removes matching framework tools and maps
supported native controls, including shell, web search, image tools, and
`mcp__SERVER__TOOL` exclusions. Codex rejects names it cannot enforce, such as a
generic `Write` ban. Tool exclusions are distinct from filesystem permissions.

Native hooks remain backend-specific. Claude accepts its SDK callback objects.
For Codex, configure and trust native hooks through Codex itself; per-client
hook injection is rejected because its trust cannot be verified. Prefer
`required_tools` for workflow progress/status requirements on either backend.
Claude receives tool annotations; Codex dynamic tools have no equivalent native
annotation fields.

Codex reports subagent activity, structured rate limits, cache-write tokens, and
current context usage after a turn. `UsageInfo.estimated_thread_cost_usd` is an
optional cumulative estimate, separate from reported `cost_usd`. Codex has no
pre-input context query or resolved available-agent inventory API; inspection
does not send a `ping` or another model turn to approximate either.

The adapters target the SDK versions pinned in `pyproject.toml`. The Codex
transport uses the SDK's public process/request APIs and one isolated
notification observer to include child turns and account events. This observer
is covered by transport tests against the pinned SDK.

## Tests

```bash
pip install -e '.[test]'
python -m pytest tests
pre-commit run -a
```

Live Claude context-budget checks require local account configuration and are
opt-in: `python -m pytest tests --run-live-backends -m live_backend`.

## Workflows

Ready-to-run multi-agent workflows ship as subpackages of
Expand Down
10 changes: 9 additions & 1 deletion agent-flow/agent_flow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
BackendConfig,
SessionConfig,
)
from .hooks import called_required_tool_this_turn, require_tool_call_stop_hook
from .hooks import (
RequiredToolCallError,
called_required_tool_this_turn,
require_tool_call_stop_hook,
)
from .layers import AgentLayer
from .module import Module, Sequential
from .tools import ToolDefinition, tool
from .types import (
AgentRequest,
AgentResponse,
Expand All @@ -33,13 +38,16 @@
"CompactBoundaryEvent",
"Module",
"RateLimitWarningEvent",
"RequiredToolCallError",
"Sequential",
"ServerToolCallEvent",
"SessionConfig",
"SessionInitEvent",
"ThinkingEvent",
"ToolCallEvent",
"ToolDefinition",
"UsageInfo",
"called_required_tool_this_turn",
"require_tool_call_stop_hook",
"tool",
]
33 changes: 27 additions & 6 deletions agent-flow/agent_flow/backends/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from claude_agent_sdk import (
ClaudeAgentOptions,
ClaudeSDKClient,
PermissionResultAllow,
SdkMcpTool,
ToolAnnotations,
create_sdk_mcp_server,
)
from claude_agent_sdk.types import (
Expand All @@ -28,6 +29,7 @@
ToolUseBlock,
)

from ..tools import normalize_tool, tool_result_to_claude
from ..types import (
AgentTextEvent,
CompactBoundaryEvent,
Expand Down Expand Up @@ -420,6 +422,29 @@ def _claude_backend_version() -> str:
_REASONING_EFFORT = "max"


def _sdk_tools(tools: list) -> list[SdkMcpTool]:
"""Translate framework or legacy tool definitions at the SDK boundary."""
result = []
for original in tools:
definition = normalize_tool(original)

async def handler(arguments, definition=definition):
return tool_result_to_claude(await definition.handler(arguments))

result.append(
SdkMcpTool(
name=definition.name,
description=definition.description,
input_schema=definition.input_schema,
handler=handler,
annotations=ToolAnnotations(**definition.annotations)
if definition.annotations
else None,
)
)
return result


class ClaudeCodeBackend(Backend):
def version(self) -> str:
return _claude_backend_version()
Expand Down Expand Up @@ -453,12 +478,9 @@ async def create_client(
if tools:
mcp_servers["agent-tools"] = create_sdk_mcp_server(
name="agent-tools",
tools=tools,
tools=_sdk_tools(tools),
)

async def _approve_tool(tool_name, tool_input, context):
return PermissionResultAllow()

options = ClaudeAgentOptions(
tools=ToolsPreset(type="preset", preset="claude_code"),
system_prompt=SystemPromptPreset(
Expand All @@ -472,7 +494,6 @@ async def _approve_tool(tool_name, tool_input, context):
cwd=cwd or Path.cwd(),
sandbox={"enabled": False},
permission_mode="bypassPermissions",
can_use_tool=_approve_tool,
hooks=hooks,
disallowed_tools=list(disallowed_tools or []),
)
Expand Down
Loading
Loading