Observed since 20 Oct, 22:50 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Multi-agent Python SDK with peer-to-peer agent communication
A multi-agent Python SDK — peer-to-peer free calls with no orchestrator.
Every agent is equal. No orchestrator. No hierarchy. No restrictions.
AgentOutO rejects the orchestrator pattern used by existing frameworks (CrewAI, AutoGen, etc.).
All agents are fully equal. There is no base agent.
Any agent can call any agent. There are no call restrictions.
Any agent can use any tool. There are no tool restrictions.
The message protocol has exactly two types: forward and return.
The user is just an agent without an LLM. No special interface, protocol, or tools exist for the user.
| Existing Frameworks | AgentOutO |
|---|---|
| Orchestrator-centric hierarchy | Peer-to-peer free calls |
| Base agent required | No base agent |
| Per-agent allowed-call lists | Any agent calls any agent |
| Per-agent tool assignment | All tools are global |
| Complex message protocols | Forward / Return only |
| Top-down message flow | Bidirectional free flow |
pip install agentouto
Requires Python ≥ 3.11.
from agentouto import Agent, Tool, Provider, run
# Providers — API connection info only
openai = Provider(name="openai", kind="openai", api_key="sk-...")
anthropic = Provider(name="anthropic", kind="anthropic", api_key="sk-ant-...")
google = Provider(name="google", kind="google", api_key="AIza...")
# Tool — globally available to all agents
@Tool
def search_web(query: str) -> str:
"""Search the web."""
return f"Results for: {query}"
# Agent — model settings live here
# role: short description shown to other agents in the agent list
# instructions: detailed instructions included in the agent's own system prompt
researcher = Agent(
name="researcher",
role="Research expert",
instructions="Search and organize information from multiple sources. Always verify facts before reporting.",
model="gpt-5.2",
provider="openai",
)
writer = Agent(
name="writer",
role="Skilled writer",
instructions="Turn research findings into polished, well-structured reports. Use clear language and logical flow.",
model="claude-sonnet-4-6",
provider="anthropic",
)
reviewer = Agent(
name="reviewer",
role="Critical reviewer",
instructions="Verify facts and improve quality. Check for accuracy, clarity, and completeness in all deliverables.",
model="gemini-3.1-pro",
provider="google",
)
# Run — user is just an agent without an LLM
result = run(
message="Write an AI trends report.",
starting_agents=[researcher, writer, reviewer],
tools=[search_web],
providers=[openai, anthropic, google],
)
print(result.output)
┌─────────────────────────────────────────────────────────┐
│ run() │
│ (User = LLM-less agent) │
│ │ │
│ Forward Message │
│ ▼ │
│ ┌─────────────── Agent Loop ──────────────────┐ │
│ │ │ │
│ │ ┌──→ LLM Call (via Provider Backend) │ │
│ │ │ │ │ │
│ │ │ ├── tool_call → Tool.execute() │ │
│ │ │ │ │ │ │
│ │ │ │ result back ───┐ │ │
│ │ │ │ │ │ │
│ │ │ ├── call_agent → New Loop ────┤ │ │
│ │ │ │ │ │ │ │
│ │ │ │ return back ───┐│ │ │
│ │ │ │ ││ │ │
│ │ ├── finish(message=X) ───────┐│ │ │
│ │ │ or plain text, no tools ││ │ │
│ │ │ final answer ───┘│ │ │
│ │ │ ││ │ │
│ │ └────────────── next iteration ◄──────┘┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ Return Message │
│ ▼ │
│ RunResult.output │
└─────────────────────────────────────────────────────────┘
An agent finishes by replying with plain text and no tool calls — that text becomes the result. Calling finish(message="...") does exactly the same thing, so use it when you want the conclusion to be explicit.
# Both of these end the loop with the same result:
# 1. the model replies with text and no tool calls
# 2. the model calls finish(message="Here's the report.")
A few other ways a run can end:
[User] ──(forward)──→ [Agent A]
│
├──(forward)──→ [Agent B]
│ ├──(forward)──→ [Agent C]
│ │ │
│ │←──(return)──────┘
│ │
│←──(return)─────┘
│
└──(return)──→ [User]
User→A and A→B use the exact same mechanism. There is no special user protocol.
[Agent A]
├──(forward)──→ [Agent B] ─┐
├──(forward)──→ [Agent C] ├── asyncio.gather — all run concurrently
└──(forward)──→ [Agent D] ─┘
│
←──(3 returns, batched)────┘
Providers hold API credentials. No model settings, no inference config.
from agentouto import Provider
openai = Provider(name="openai", kind="openai", api_key="sk-...") # gpt-5.2, gpt-5.3-codex, o3, o4-mini
openai_resp = Provider(name="openai-resp", kind="openai_responses", api_key="sk-...") # Responses API
anthropic = Provider(name="anthropic", kind="anthropic", api_key="sk-ant-...") # claude-opus-4-6, claude-sonnet-4-6
google = Provider(name="google", kind="google", api_key="AIza...") # gemini-3.1-pro, gemini-3-flash
# OpenAI-compatible APIs (vLLM, Ollama, LM Studio, etc.)
local = Provider(name="local", kind="openai", base_url="http://localhost:11434/v1")
| Field | Description | Required |
|---|---|---|
name | Identifier for the provider | ✅ |
kind | API type: "openai", "openai_responses", "anthropic", "google" | ✅ |
api_key | API key (not needed when auth is set) | ❌ |
base_url | Custom endpoint URL (for compatible APIs) | ❌ |
auth | AuthMethod instance for OAuth authentication | ❌ |
Providers can use OAuth 2.0 instead of static API keys via the auth parameter. Install OAuth dependencies:
pip install agentouto[oauth]
OpenAI OAuth — Use your ChatGPT Plus/Pro subscription:
from agentouto import Provider, OpenAIOAuth
auth = OpenAIOAuth(client_id="your-client-id")
await auth.ensure_authenticated() # Opens browser for login
openai = Provider(name="openai", kind="openai", auth=auth)
Claude OAuth ⚠️ — Anthropic prohibits third-party OAuth usage. Account suspension risk:
from agentouto import Provider, ClaudeOAuth
# ⚠️ TOS VIOLATION RISK — Use API keys from console.anthropic.com instead
auth = ClaudeOAuth(client_id="your-client-id")
await auth.ensure_authenticated()
anthropic = Provider(name="anthropic", kind="anthropic", auth=auth)
Google OAuth ⚠️ — Google bans accounts using Antigravity OAuth. Use your own GCP credentials:
from agentouto import Provider, GoogleOAuth
# ⚠️ Antigravity OAuth → account ban risk (Gmail, Drive, ALL services)
# Safe: Use your own GCP OAuth Client ID from console.cloud.google.com
auth = GoogleOAuth(
client_id="your-gcp-client-id.apps.googleusercontent.com",
client_secret="your-gcp-secret",
)
await auth.ensure_authenticated()
google = Provider(name="google", kind="google", auth=auth)
OAuth tokens are automatically cached in ~/.agentouto/tokens/ and refreshed when expired.
from agentouto import Agent
agent = Agent(
name="researcher",
role="Research expert",
instructions="Search and organize information from multiple sources. Always verify facts before reporting.",
model="gpt-5.2",
provider="openai",
reasoning=True,
reasoning_effort="high",
temperature=1.0,
)
| Field | Description | Default |
|---|---|---|
name | Agent name | (required) |
instructions | Detailed instructions (included in agent's system prompt) | (required) |
model | Model name | (required) |
provider | Provider name | (required) |
role | Short role description (shown in agent list) | None (uses instructions) |
max_output_tokens | Max output tokens | None (auto) |
reasoning | Enable reasoning/thinking mode | False |
reasoning_effort | Reasoning intensity | "medium" |
reasoning_budget | Thinking token budget (Anthropic) | None |
temperature | Temperature | 1.0 |
context_window | Context window tokens (auto-resolved) | None (auto) |
extra | Additional API parameters (free dict) | {} |
The SDK uses unified parameter names. Each provider backend maps them internally:
| SDK Parameter | OpenAI (Chat Completions) | OpenAI (Responses) | Anthropic | Google Gemini |
|---|---|---|---|---|
max_output_tokens | max_completion_tokens (omitted when None) | max_output_tokens (omitted when None) | max_tokens (auto-probed when None) | max_output_tokens (omitted when None) |
reasoning=True | sends reasoning_effort | reasoning={"effort": value} | thinking={"type": "enabled", "budget_tokens": ...} | thinking_config={"thinking_budget": ...} |
reasoning_effort | top-level reasoning_effort | reasoning.effort | N/A | N/A |
reasoning_budget | N/A | N/A | thinking.budget_tokens | thinking_config.thinking_budget |
temperature (reasoning=True) | not sent | not sent | forced to 1 | sent as-is |
context_window is auto-resolved from LCW API when None. Set explicitly to override. When set, self-summarization triggers at 70% of context limit.
See ai-docs/PROVIDER_BACKENDS.md for full mapping details.
from agentouto import Tool
@Tool
def search_web(query: str) -> str:
"""Search the web."""
return f"Results for: {query}"
# Async tools are supported
@Tool
async def fetch_data(url: str) -> str:
"""Fetch data from URL."""
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
Tools are automatically converted to JSON schemas from function signatures and docstrings. All agents can use all tools.
Use Annotated for parameter descriptions, Literal for allowed values, Enum for enumerated types, and default values — all reflected in the JSON schema sent to the LLM:
from typing import Annotated, Literal
from agentouto import Tool
@Tool
def search_web(
query: Annotated[str, "Search keywords or question"],
max_results: Annotated[int, "Maximum number of results to return"] = 10,
language: Literal["ko", "en", "ja"] = "ko",
) -> str:
"""Search the web for information."""
...
This generates a detailed schema that helps the LLM use tools correctly:
{
"properties": {
"query": {"type": "string", "description": "Search keywords or question"},
"max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
"language": {"type": "string", "enum": ["ko", "en", "ja"], "default": "ko"}
},
"required": ["query"]
}
Plain type hints (without Annotated) continue to work as before.
Tools can also return rich results with file attachments using ToolResult:
from agentouto import Tool, ToolResult, Attachment
@Tool
def fetch_image(url: str) -> ToolResult:
"""Fetch an image from URL."""
data = download_and_base64_encode(url)
return ToolResult(
content="Image fetched successfully.",
attachments=[Attachment(mime_type="image/png", data=data)],
)
When a tool returns ToolResult with attachments, the LLM can visually analyze the images. Regular str returns remain fully supported.
Built-in tools (call_agent, spawn_background_agent, send_message, get_messages, finish) can be overridden or disabled at the run() level.
Note: spawn_background_agent and the background parameter on call_agent are only exposed when allow_background_agents=True. By default, agents cannot spawn background agents at all.
Pass disabled_tools to exclude built-in tools from the tool schemas sent to the LLM:
result = run(
message="Do research.",
starting_agents=[researcher],
tools=[search_web],
providers=[openai],
allow_background_agents=True,
disabled_tools={"spawn_background_agent", "get_messages"},
)
# The LLM won't see spawn_background_agent or get_messages in its tool list.
Provide a user tool with the same name as a built-in to replace it:
@Tool
def finish(message: str, confidence: float = 1.0) -> str:
"""Return result with confidence score."""
return f"[{confidence}] {message}"
result = run(
message="Analyze this data.",
starting_agents=[researcher],
tools=[finish], # Replaces the built-in finish
providers=[openai],
)
# result.output will be "[0.85] Analysis complete"
Override takes precedence over disable — if a tool is both overridden and in disabled_tools, the override is used.
Note: finish cannot be disabled (raises ValueError). Override it instead if you need custom finish behavior. The override only runs when the model actually calls finish — a plain-text reply with no tool calls still ends the run with that text as the result.
Agents can receive file attachments (images, audio, video, PDFs) via the Attachment dataclass:
@dataclass
class Attachment:
mime_type: str # "image/png", "audio/mp3", "video/mp4"
data: str | None = None # base64-encoded data
url: str | None = None # URL reference (mutually exclusive with data)
name: str | None = None # optional filename
Pass attachments to run() or async_run():
from agentouto import run, Attachment
result = run(
message="Analyze this image.",
starting_agents=[vision_agent],
tools=[],
providers=[openai],
attachments=[
Attachment(mime_type="image/png", data=base64_string),
Attachment(mime_type="image/jpeg", url="https://example.com/photo.jpg"),
],
)
All three provider backends (OpenAI, Anthropic, Google) convert attachments to their native multimodal format automatically.
@dataclass
class Message:
type: Literal["forward", "return"]
sender: str
receiver: str
content: str
call_id: str # Unique tracking ID
attachments: list[Attachment] | None = None
Two types. No exceptions.
You can pass previous conversation history to an agent to maintain context across calls. Use RunResult.messages from a previous run:
from agentouto import run, Agent, Provider
# First conversation
result1 = run(
message="Research AI trends.",
starting_agents=[researcher],
tools=[],
providers=[openai],
)
# Continue with history
result2 = run(
message="Write about what you found.",
starting_agents=[writer, researcher],
tools=[],
providers=[openai],
history=result1.messages, # Pass previous messages
)
You can also use history with call_agent tool. The LLM can pass conversation history when calling another agent:
# The LLM can call:
call_agent(
agent_name="writer",
message="Continue the report.",
history=[...] # Optional array of previous Message objects
)
History is prepended to the agent's context before the new forward message, allowing the agent to have continuity with previous conversations.
You can inject additional instructions into the system prompt at runtime, without modifying agent declarations. This is useful for shared context that changes per execution — like a SOUL.md file defining current project tone.
# Load shared context that only the entry agent receives
with open("SOUL.md", "r") as f:
soul_content = f.read()
result = run(
message="Write an article about AI.",
starting_agents=[writer, researcher],
tools=[search_web],
providers=[openai],
extra_instructions=soul_content,
extra_instructions_scope="entry", # default
)
# Load project rules that ALL agents must follow
with open("SOUL.md", "r") as f:
soul_content = f.read()
result = run(
message="Write an article about AI.",
starting_agents=[writer, researcher, reviewer],
tools=[search_web],
providers=[openai],
extra_instructions=soul_content,
extra_instructions_scope="all", # propagates to call_agent calls
)
# writer, researcher, reviewer ALL get SOUL.md in their system prompts
The instructions are injected as an ADDITIONAL INSTRUCTIONS: section in the system prompt, after agent identity but before available agents list.
| Parameter | Default | Description |
|---|---|---|
extra_instructions | None | Additional text injected into system prompt |
extra_instructions_scope | "entry" | "entry" → only first agent, "all" → all agents including sub-calls |
Every agent call is automatically assigned a unique call_id (UUID), so even when the same agent name is called multiple times in parallel, each invocation is tracked separately.
task_id = run_background(
message="Research AI trends",
starting_agents=[researcher, writer, reviewer],
tools=[search_web],
providers=[openai],
)
# Returns main task_id: "bg_abc123"
# Additional agents spawned as: "bg_abc123_1", "bg_abc123_2", ...
Example output when the same agent is called in parallel:
user → researcher [call_id=a1b2c3d4] forward
researcher → researcher [call_id=e5f6g7h8] forward
researcher → researcher [call_id=i9j0k1l2] forward
researcher → user [call_id=a1b2c3d4] return
researcher → user [call_id=e5f6g7h8] return
researcher → user [call_id=i9j0k1l2] return
Filtering by receiver to see all calls to a specific agent:
for msg in result.messages:
if msg.receiver == "researcher" and msg.type == "forward":
print(f"call_id={msg.call_id[:8]}: {msg.content[:50]}...")
Agents can run in isolated loops that can receive messages while running. This enables true concurrent agents that can communicate during execution.
By default, background agent spawning is disabled. Agents can still call each other normally via call_agent, but they cannot spawn background agents unless you explicitly enable it.
To enable background agents, pass allow_background_agents=True:
result = run(
message="Research and report.",
starting_agents=[researcher, writer],
tools=[search_web],
providers=[openai],
allow_background_agents=True, # Enable background agent spawning
)
Once enabled, use call_agent with background=True, or use run_background() directly:
from agentouto import run_background
# Spawn an agent in background — returns immediately with task_id
task_id = run_background(
message="Research AI trends",
starting_agents=[researcher, writer],
tools=[search_web],
providers=[openai],
allow_background_agents=True, # Required
)
# task_id = "bg_abc123"
# Or use call_agent with background=True from within an agent
# (only works when allow_background_agents=True)
call_agent(
agent_name="researcher",
message="Research the latest in AI.",
background=True,
)
run_background() also supports starting_agents like run():
task_id = run_background(
message="Research AI trends",
run_agents=[researcher, writer, reviewer],
tools=[search_web],
providers=[openai],
starting_agents=[writer, reviewer],
)
# Returns main task_id: "bg_abc123"
# Additional agents spawned as: "bg_abc123_1", "bg_abc123_2", ...
Use send_message to inject messages into a running agent:
from agentouto import send_message
# Send a message to the running agent
send_message(
task_id="bg_abc123",
message="Add a section about GPT-5.",
)
# Returns: "Message sent to writer (task_id: bg_abc123)"
The agent receives the message as a new user input in its running loop.
Use get_agent_status to check on a running agent:
from agentouto import get_agent_status
# Retrieve status, result, and all messages
status = get_agent_status("bg_abc123")
# Returns:
# Task ID: bg_abc123
# Agent: writer
# Status: running
# Messages (3):
# [forward] user -> writer: Write a report...
# [return] writer -> user: Here's the report...
Use get_stream_events to stream events from a background agent:
from agentouto import get_stream_events
async for event in get_stream_events("bg_abc123"):
if event["type"] == "token":
print(event["data"]["text"], end="", flush=True)
elif event["type"] == "finish":
print(f"\n--- Result: {event['data']['output']} ---")
| Aspect | asyncio.gather Parallel | Isolated Loops |
|---|---|---|
| Execution | Same loop iteration | Isolated loops |
| Communication | Results only after completion | Real-time messages |
| Independence | Share context | Own context |
| Use case | Fast parallel tasks | Long-running concurrent agents |
from agentouto import run_background, send_message, get_agent_status
# Spawn researcher in background
task_id = run_background(
message="Research AI trends",
starting_agents=[researcher],
tools=[search_web],
providers=[openai],
allow_background_agents=True, # Required to enable background spawning
)
# task_id = "bg_res_001"
# Do other work...
# Send additional instructions
send_message(task_id="bg_res_001", message="Also look at GPT-5")
# Check status
print(get_agent_status("bg_res_001"))
See ai-docs/MESSAGE_PROTOCOL.md for detailed protocol documentation.
Agents and users can exchange messages in real-time during run() — no background mode needed. The only difference from run_background() is that run() blocks until the agent finishes.
Pass an on_message callback that receives (message, send):
def on_message(msg, send):
print(f"[{msg.sender}] {msg.content}")
if msg.content == "Need your input":
send("Approved, proceed.")
result = run(
message="Write a detailed report.",
starting_agents=[writer],
tools=[search_web],
providers=[openai],
on_message=on_message,
)
send_message(task_id="...", message="...") → on_message fires with the messagesend("...") inside the callback → message is queued and the agent receives it on its next iterationAgentLoopRegistrytask_idsend_message(task_id="...", message="update") — the same tool used for background agents(msg, send) — call send() to replysend() are queued and added to the agent's context before the next LLM callRunResult.messagesasync_run_stream also supports on_message, plus yields "user_message" StreamEvents:
async for event in async_run_stream(
starting_agents=[writer],
message="Write report",
on_message=lambda msg, send: send("keep going") if "stuck" in msg.content else None,
):
if event.type == "user_message":
print(f"Progress: {event.data['message']}")
elif event.type == "finish":
print(f"Done: {event.data['output']}")
caller_loop_id is included in all agent system prompts, so sub-agents can send intermediate messages to their caller (not just the user):
[User] → [Agent A] → [Agent B]
├── send_message(task_id="A's loop", message="50%")
└── finish("done")
All agents in starting_agents execute simultaneously in their own loops as equal peers:
result = run(
message="Research and write about AI trends",
starting_agents=[researcher, writer, reviewer], # all run in parallel
tools=[search_web],
providers=[openai, anthropic, google],
)
Each can call other agents via call_agent, use tools, and communicate independently.
run_agentsrun_agents defines the participant pool — agents that can execute, call, and perceive each other. If run_agents is not specified, it defaults to starting_agents.
Warning: Putting an agent in starting_agents but NOT in run_agents is bad practice. Such agents cannot participate in the run at all — they cannot execute, call other agents, or be perceived. A warning is issued in this case.
# Good: all participants can see each other
result = run(
message="Research and write about AI trends",
starting_agents=[researcher, writer, reviewer],
run_agents=[researcher, writer, reviewer], # All participate
tools=[search_web],
providers=[openai, anthropic, google],
)
# Bad: reviewer in starting_agents but not in run_agents
# This will issue a warning — reviewer cannot participate!
result = run(
message="...",
starting_agents=[researcher, writer, reviewer],
run_agents=[researcher, writer], # reviewer NOT in participants
...
)
This is useful for:
Both parameters work with run(), async_run(), run_background(), and run_background_sync().
When multiple agents run in parallel (via starting_agents or call_agent), their results are returned with XML-style tags for clear attribution:
# Single agent run — plain output
result = run(message="...", starting_agents=[researcher])
# result.output = "Research findings..."
# Multiple parallel agents — tagged output
result = run(
message="...",
starting_agents=[researcher, writer, reviewer],
)
# result.output =
# [researcher]Research findings...[/researcher]
# [writer]Written report...[/writer]
# [reviewer]Review complete...[/reviewer]
The same format applies to call_agent results:
# Within an agent, calling another agent returns tagged output
call_agent(agent_name="writer", message="Write a summary")
# Returns: "[writer]Summary text...[/writer]"
This ensures the user (treated as an agent without an LLM) can parse and understand which agent produced which output.
For structured event logs and call tree visualization, enable debug=True:
result = run(..., debug=True)
# Print the call tree
print(result.format_trace())
# Access event log for filtering by agent or event type
events = result.event_log.filter(event_type="agent_call")
for e in events:
print(f"{e.agent_name}: {e.call_id[:8]} from parent={e.parent_call_id}")
Debug mode is optional — basic call tracking via call_id in RunResult.messages works without it.
See ai-docs/MESSAGE_PROTOCOL.md for detailed tracking documentation.
| Kind | Provider | Example Models | Compatible With |
|---|---|---|---|
"openai" | OpenAI Chat Completions API | gpt-5.2, gpt-5.3-codex, o3, o4-mini | vLLM, Ollama, LM Studio, any OpenAI-compatible API |
"openai_responses" | OpenAI Responses API | gpt-5.2, gpt-5.3-codex, o3, o4-mini | — |
"anthropic" | Anthropic API | claude-opus-4-6, claude-sonnet-4-6 | AWS Bedrock, Google Vertex AI, Ollama, LiteLLM, any Anthropic-compatible API |
"google" | Google Gemini API | gemini-3.1-pro, gemini-3-flash | — |
import asyncio
from agentouto import async_run
result = await async_run(
message="Write an AI trends report.",
starting_agents=[researcher, writer, reviewer],
tools=[search_web, write_file],
providers=[openai, anthropic, google],
)
You can also pass conversation history and extra instructions:
result = await async_run(
message="Continue the report.",
starting_agents=[writer, researcher],
tools=[],
providers=[openai],
history=previous_result.messages, # Pass previous messages
extra_instructions="Use a professional tone.", # Inject into system prompt
)
from agentouto import async_run_stream
async for event in async_run_stream(
message="Write an AI trends report.",
starting_agents=[researcher, writer, reviewer],
tools=[search_web],
providers=[openai, anthropic, google],
):
if event.type == "token":
print(event.data["token"], end="", flush=True)
elif event.type == "finish":
print(f"\n--- {event.agent_name} finished ---")
# call_id and parent_call_id are available on all events for tracing
print(f"[{event.type}] call_id={event.call_id[:8]} parent={event.parent_call_id}")
Streaming also supports history and extra instructions:
async for event in async_run_stream(
message="Continue writing.",
starting_agents=[writer, researcher],
tools=[],
providers=[openai],
history=previous_result.messages,
extra_instructions="Focus on technical details.", # Inject into system prompt
):
...
agentouto/
├── __init__.py # Public API exports (Agent, Tool, Provider, Attachment, ToolResult, ...)
├── agent.py # Agent dataclass
├── tool.py # Tool decorator/class with auto JSON schema, ToolResult
├── message.py # Message dataclass (forward/return)
├── provider.py # Provider dataclass (API connection info)
├── context.py # Attachment, ContextMessage, per-agent conversation context
├── router.py # Message routing, system prompt generation, tool schema building
├── runtime.py # Orchestration, parallel execution, run()/async_run()
├── loop_manager.py # Background agent loops, message queues, AgentLoopRegistry
├── streaming.py # async_run_stream(), StreamEvent
├── event_log.py # AgentEvent, EventLog — structured event recording
├── tracing.py # Trace, Span — call tree builder from event logs
├── _constants.py # Shared constants (CALL_AGENT, FINISH)
├── _bridge/ # coreouto adapter — the only place that imports coreouto
│ ├── __init__.py # run_agent_loop() over coreouto.Agent.call()
│ ├── convert.py # Type conversion (Context ↔ Message, Attachment ↔ ContentBlock, Usage)
│ ├── state.py # RunState + ContextVar
│ ├── provider.py # DispatchProvider (coreouto Provider protocol)
│ ├── tools.py # Builtin + user tool dispatch into coreouto's tool registry
│ └── hooks.py # coreouto hooks → StreamEvent / EventLog / summarizer
├── exceptions.py # ProviderError, AgentError, ToolError, RoutingError, AuthError
├── auth/
│ ├── __init__.py # AuthMethod ABC, TokenData, TokenStore, OAuth implementations
│ ├── api_key.py # ApiKeyAuth — static API key wrapper
│ ├── openai_oauth.py # OpenAIOAuth — OpenAI ChatGPT subscription OAuth
│ ├── claude_oauth.py # ClaudeOAuth — Anthropic Claude OAuth (⚠️ TOS restricted)
│ ├── google_oauth.py # GoogleOAuth — Google Gemini/Antigravity OAuth (⚠️ TOS restricted)
│ ├── token_store.py # TokenStore — secure token persistence (~/.agentouto/tokens/)
│ └── _oauth_common.py # PKCE, local callback server, browser auth, token exchange
└── providers/
├── __init__.py # ProviderBackend ABC, LLMResponse, get_backend()
├── openai.py # OpenAI Chat Completions (+ compatible APIs) implementation
├── openai_responses.py # OpenAI Responses API implementation
├── anthropic.py # Anthropic implementation
└── google.py # Google Gemini implementation
| Phase | Description | Status |
|---|---|---|
| 1 | Core classes: Provider, Agent, Tool, Message | ✅ Done |
| 2 | Single agent execution: agent loop + tool calling | ✅ Done |
| 3 | Multi-agent: call_agent + finish + message routing | ✅ Done |
| 4 | Parallel calls: asyncio.gather concurrent execution | ✅ Done |
| 5 | Streaming, logging, tracing, debug mode | ✅ Done |
| 6 | CI/CD, tests, PyPI publish | ✅ Done |
| 7 | Multimodal attachments (Attachment, ToolResult) | ✅ Done |
| 8 | Rich parameter schemas (Annotated, Literal, Enum, default) | ✅ Done |
| 9 | Reasoning tag handling (content preservation, detection prevention) | ✅ Done |
| 10 | Auto max output tokens + safe JSON argument parsing | ✅ Done |
| 13 | OpenAI Responses API backend (openai_responses) + tool-result attachment routing | ✅ Done |
| 15 | OAuth authentication (OpenAI, Claude, Google) | ✅ Done |
| 16 | Conversation history (history parameter) | ✅ Done |
| 17 | Background execution + inter-agent messaging | ✅ Done |
| 18 | Background streaming + unified API (send_message, get_agent_status, run_background) | ✅ Done |
| 19 | Runtime extra_instructions injection (extra_instructions + extra_instructions_scope parameters) | ✅ Done |
| 20 | Starting agents (all parallel) + visibility scoping (run_agents) + tagged output format | ✅ Done |
| 21 | Built-in tool override/disable (disabled_tools parameter) | ✅ Done |
| 22 | Intermediate messages (on_message callback, user_message StreamEvent) | ✅ Done |
| 23 | Background agents disabled by default (allow_background_agents parameter) | ✅ Done |
| 24 | Self-summarization next-step planning (<summary> / <next_steps>) | ✅ Done |
| 25 | Summarizer user hook (on_summarize callback) | ✅ Done |
| 26 | API token usage tracking (RunResult.token_usage) + hybrid summarization trigger | ✅ Done |
| 27 | Internal loop rewrite onto coreouto (single loop for streaming and non-streaming) | ✅ Done |
For AI contributors and detailed technical reference, see ai-docs/:
AI_INSTRUCTIONS.md — Read this first. How to work on this project and update docs.PHILOSOPHY.md — Core philosophy and inviolable principles.ARCHITECTURE.md — Package structure, module responsibilities, data flow.PROVIDER_BACKENDS.md — Provider system, parameter mapping, API-specific behavior.MESSAGE_PROTOCOL.md — Message types, routing rules, parallel calls, agent loop.CONVENTIONS.md — Coding conventions, patterns, naming, style guide.ROADMAP.md — Current status, planned features, known issues.Apache License 2.0 — see LICENSE for details.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.28.0 | 1 Oct 2026 | 9 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 22:50 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:686d394a87a336771802b582e10009ce6b3424fabe35d78b4da0b0298e03dddf