vibecore 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of vibecore might be problematic. Click here for more details.
- vibecore/__init__.py +0 -0
- vibecore/agents/default.py +79 -0
- vibecore/agents/prompts.py +12 -0
- vibecore/agents/task_agent.py +66 -0
- vibecore/cli.py +150 -0
- vibecore/context.py +24 -0
- vibecore/handlers/__init__.py +5 -0
- vibecore/handlers/stream_handler.py +231 -0
- vibecore/main.py +506 -0
- vibecore/main.tcss +0 -0
- vibecore/mcp/__init__.py +6 -0
- vibecore/mcp/manager.py +167 -0
- vibecore/mcp/server_wrapper.py +109 -0
- vibecore/models/__init__.py +5 -0
- vibecore/models/anthropic.py +239 -0
- vibecore/prompts/common_system_prompt.txt +64 -0
- vibecore/py.typed +0 -0
- vibecore/session/__init__.py +5 -0
- vibecore/session/file_lock.py +127 -0
- vibecore/session/jsonl_session.py +236 -0
- vibecore/session/loader.py +193 -0
- vibecore/session/path_utils.py +81 -0
- vibecore/settings.py +161 -0
- vibecore/tools/__init__.py +1 -0
- vibecore/tools/base.py +27 -0
- vibecore/tools/file/__init__.py +5 -0
- vibecore/tools/file/executor.py +282 -0
- vibecore/tools/file/tools.py +184 -0
- vibecore/tools/file/utils.py +78 -0
- vibecore/tools/python/__init__.py +1 -0
- vibecore/tools/python/backends/__init__.py +1 -0
- vibecore/tools/python/backends/terminal_backend.py +58 -0
- vibecore/tools/python/helpers.py +80 -0
- vibecore/tools/python/manager.py +208 -0
- vibecore/tools/python/tools.py +27 -0
- vibecore/tools/shell/__init__.py +5 -0
- vibecore/tools/shell/executor.py +223 -0
- vibecore/tools/shell/tools.py +156 -0
- vibecore/tools/task/__init__.py +5 -0
- vibecore/tools/task/executor.py +51 -0
- vibecore/tools/task/tools.py +51 -0
- vibecore/tools/todo/__init__.py +1 -0
- vibecore/tools/todo/manager.py +31 -0
- vibecore/tools/todo/models.py +36 -0
- vibecore/tools/todo/tools.py +111 -0
- vibecore/utils/__init__.py +5 -0
- vibecore/utils/text.py +28 -0
- vibecore/widgets/core.py +332 -0
- vibecore/widgets/core.tcss +63 -0
- vibecore/widgets/expandable.py +121 -0
- vibecore/widgets/expandable.tcss +69 -0
- vibecore/widgets/info.py +25 -0
- vibecore/widgets/info.tcss +17 -0
- vibecore/widgets/messages.py +232 -0
- vibecore/widgets/messages.tcss +85 -0
- vibecore/widgets/tool_message_factory.py +121 -0
- vibecore/widgets/tool_messages.py +483 -0
- vibecore/widgets/tool_messages.tcss +289 -0
- vibecore-0.2.0.dist-info/METADATA +407 -0
- vibecore-0.2.0.dist-info/RECORD +63 -0
- vibecore-0.2.0.dist-info/WHEEL +4 -0
- vibecore-0.2.0.dist-info/entry_points.txt +2 -0
- vibecore-0.2.0.dist-info/licenses/LICENSE +21 -0
vibecore/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from agents import Agent, ModelSettings
|
|
4
|
+
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
|
|
5
|
+
from openai.types import Reasoning
|
|
6
|
+
|
|
7
|
+
from vibecore.context import VibecoreContext
|
|
8
|
+
from vibecore.settings import settings
|
|
9
|
+
from vibecore.tools.file.tools import edit, multi_edit, read, write
|
|
10
|
+
from vibecore.tools.python.tools import execute_python
|
|
11
|
+
from vibecore.tools.shell.tools import bash, glob, grep, ls
|
|
12
|
+
from vibecore.tools.task.tools import task
|
|
13
|
+
from vibecore.tools.todo.tools import todo_read, todo_write
|
|
14
|
+
|
|
15
|
+
from .prompts import COMMON_PROMPT
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from agents.mcp import MCPServer
|
|
19
|
+
|
|
20
|
+
INSTRUCTIONS = (
|
|
21
|
+
COMMON_PROMPT + "\n\n"
|
|
22
|
+
"You are a versatile AI assistant capable of helping with a wide range of tasks. "
|
|
23
|
+
"You have access to various tools including file operations, shell commands, "
|
|
24
|
+
"Python execution, and task management. "
|
|
25
|
+
"Use the appropriate tools to accomplish any task the user requests. "
|
|
26
|
+
"You can handle programming, system administration, file manipulation, "
|
|
27
|
+
"automation, and general problem-solving tasks."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_default_agent(mcp_servers: list["MCPServer"] | None = None) -> Agent[VibecoreContext]:
|
|
32
|
+
"""Create the general-purpose agent with appropriate tools.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
mcp_servers: Optional list of MCP servers to connect to.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Configured general-purpose agent.
|
|
39
|
+
"""
|
|
40
|
+
tools: list = [
|
|
41
|
+
todo_read,
|
|
42
|
+
todo_write,
|
|
43
|
+
execute_python,
|
|
44
|
+
read,
|
|
45
|
+
edit,
|
|
46
|
+
multi_edit,
|
|
47
|
+
write,
|
|
48
|
+
bash,
|
|
49
|
+
glob,
|
|
50
|
+
grep,
|
|
51
|
+
ls,
|
|
52
|
+
task,
|
|
53
|
+
]
|
|
54
|
+
instructions = INSTRUCTIONS
|
|
55
|
+
|
|
56
|
+
instructions = prompt_with_handoff_instructions(instructions)
|
|
57
|
+
|
|
58
|
+
# Configure reasoning based on settings
|
|
59
|
+
reasoning_config = Reasoning(summary="auto")
|
|
60
|
+
if settings.reasoning_effort is not None:
|
|
61
|
+
reasoning_config = Reasoning(effort=settings.reasoning_effort, summary="auto")
|
|
62
|
+
|
|
63
|
+
return Agent[VibecoreContext](
|
|
64
|
+
name="Vibecore Agent",
|
|
65
|
+
handoff_description="A versatile general-purpose assistant",
|
|
66
|
+
instructions=instructions,
|
|
67
|
+
tools=tools,
|
|
68
|
+
model=settings.model,
|
|
69
|
+
model_settings=ModelSettings(
|
|
70
|
+
include_usage=True, # Ensure token usage is tracked in streaming mode
|
|
71
|
+
reasoning=reasoning_config,
|
|
72
|
+
),
|
|
73
|
+
handoffs=[],
|
|
74
|
+
mcp_servers=mcp_servers or [],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Create default agent without MCP servers for backward compatibility
|
|
79
|
+
default_agent = create_default_agent()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Common prompts and instructions for agents."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_common_prompt() -> str:
|
|
7
|
+
"""Load the common system prompt from file."""
|
|
8
|
+
prompt_file = Path(__file__).parent.parent / "prompts" / "common_system_prompt.txt"
|
|
9
|
+
return prompt_file.read_text()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
COMMON_PROMPT = load_common_prompt()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Task-specific agent configuration for executing delegated tasks."""
|
|
2
|
+
|
|
3
|
+
from agents import Agent, ModelSettings
|
|
4
|
+
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
|
|
5
|
+
from openai.types import Reasoning
|
|
6
|
+
|
|
7
|
+
from vibecore.context import VibecoreContext
|
|
8
|
+
from vibecore.settings import settings
|
|
9
|
+
from vibecore.tools.file.tools import edit, multi_edit, read, write
|
|
10
|
+
from vibecore.tools.python.tools import execute_python
|
|
11
|
+
from vibecore.tools.shell.tools import bash, glob, grep, ls
|
|
12
|
+
from vibecore.tools.todo.tools import todo_read, todo_write
|
|
13
|
+
|
|
14
|
+
from .prompts import COMMON_PROMPT
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_task_agent(prompt: str) -> Agent[VibecoreContext]:
|
|
18
|
+
"""Create a task agent with all tools except the Task tool.
|
|
19
|
+
|
|
20
|
+
This agent is used by the Task tool to execute specific tasks.
|
|
21
|
+
It has access to all tools except the Task tool itself to prevent
|
|
22
|
+
infinite recursion.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
prompt: The task-specific instructions to add to the agent
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Configured task agent
|
|
29
|
+
"""
|
|
30
|
+
# Same tools as default agent, but excluding task tool
|
|
31
|
+
tools: list = [
|
|
32
|
+
todo_read,
|
|
33
|
+
todo_write,
|
|
34
|
+
execute_python,
|
|
35
|
+
read,
|
|
36
|
+
edit,
|
|
37
|
+
multi_edit,
|
|
38
|
+
write,
|
|
39
|
+
bash,
|
|
40
|
+
glob,
|
|
41
|
+
grep,
|
|
42
|
+
ls,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
instructions = (
|
|
46
|
+
COMMON_PROMPT + "\n\n"
|
|
47
|
+
"You are a task-specific AI agent. Your purpose is to complete the following task:\n\n"
|
|
48
|
+
f"{prompt}\n\n"
|
|
49
|
+
"Focus on completing this specific task using the tools available to you. "
|
|
50
|
+
"Provide clear results and any relevant findings."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
instructions = prompt_with_handoff_instructions(instructions)
|
|
54
|
+
|
|
55
|
+
return Agent[VibecoreContext](
|
|
56
|
+
name="Task Agent",
|
|
57
|
+
handoff_description="A task-specific agent",
|
|
58
|
+
instructions=instructions,
|
|
59
|
+
tools=tools,
|
|
60
|
+
model=settings.model,
|
|
61
|
+
model_settings=ModelSettings(
|
|
62
|
+
include_usage=True, # Ensure token usage is tracked in streaming mode
|
|
63
|
+
reasoning=Reasoning(summary="auto"),
|
|
64
|
+
),
|
|
65
|
+
handoffs=[],
|
|
66
|
+
)
|
vibecore/cli.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Vibecore CLI interface using typer."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from textual.logging import TextualHandler
|
|
9
|
+
|
|
10
|
+
from vibecore.agents.default import create_default_agent
|
|
11
|
+
from vibecore.context import VibecoreContext
|
|
12
|
+
from vibecore.main import VibecoreApp
|
|
13
|
+
from vibecore.mcp import MCPManager
|
|
14
|
+
from vibecore.settings import settings
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def version_callback(value: bool):
|
|
20
|
+
"""Handle --version flag."""
|
|
21
|
+
if value:
|
|
22
|
+
try:
|
|
23
|
+
pkg_version = version("vibecore")
|
|
24
|
+
except Exception:
|
|
25
|
+
pkg_version = "unknown"
|
|
26
|
+
typer.echo(f"vibecore {pkg_version}")
|
|
27
|
+
raise typer.Exit()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_latest_session(project_path: Path | None = None, base_dir: Path | None = None) -> str | None:
|
|
31
|
+
"""Find the most recent session file for the current project."""
|
|
32
|
+
from vibecore.session.path_utils import canonicalize_path
|
|
33
|
+
from vibecore.settings import settings
|
|
34
|
+
|
|
35
|
+
# Use provided paths or defaults
|
|
36
|
+
if project_path is None:
|
|
37
|
+
project_path = Path.cwd()
|
|
38
|
+
if base_dir is None:
|
|
39
|
+
base_dir = settings.session.base_dir
|
|
40
|
+
|
|
41
|
+
# Get the session directory for this project
|
|
42
|
+
canonical_project = canonicalize_path(project_path)
|
|
43
|
+
session_dir = base_dir / "projects" / canonical_project
|
|
44
|
+
|
|
45
|
+
if not session_dir.exists():
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
# Find all session files
|
|
49
|
+
session_files = list(session_dir.glob("chat-*.jsonl"))
|
|
50
|
+
if not session_files:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
# Sort by modification time (most recent first)
|
|
54
|
+
session_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
55
|
+
|
|
56
|
+
# Return the session ID (filename without extension)
|
|
57
|
+
return session_files[0].stem
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command()
|
|
61
|
+
def run(
|
|
62
|
+
prompt: str | None = typer.Argument(None, help="Prompt text (requires -p flag)"),
|
|
63
|
+
continue_session: bool = typer.Option(
|
|
64
|
+
False,
|
|
65
|
+
"--continue",
|
|
66
|
+
"-c",
|
|
67
|
+
help="Continue the most recent session for this project",
|
|
68
|
+
),
|
|
69
|
+
session_id: str | None = typer.Option(
|
|
70
|
+
None,
|
|
71
|
+
"--session",
|
|
72
|
+
"-s",
|
|
73
|
+
help="Continue a specific session by ID",
|
|
74
|
+
),
|
|
75
|
+
print_mode: bool = typer.Option(
|
|
76
|
+
False,
|
|
77
|
+
"--print",
|
|
78
|
+
"-p",
|
|
79
|
+
help="Print response and exit (useful for pipes)",
|
|
80
|
+
),
|
|
81
|
+
version: bool | None = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--version",
|
|
84
|
+
callback=version_callback,
|
|
85
|
+
is_eager=True,
|
|
86
|
+
help="Show version and exit",
|
|
87
|
+
),
|
|
88
|
+
):
|
|
89
|
+
"""Run the Vibecore TUI application."""
|
|
90
|
+
# Set up logging
|
|
91
|
+
logging.basicConfig(
|
|
92
|
+
level="WARNING",
|
|
93
|
+
handlers=[TextualHandler()],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
logger = logging.getLogger("openai.agents")
|
|
97
|
+
logger.addHandler(TextualHandler())
|
|
98
|
+
|
|
99
|
+
# Create context
|
|
100
|
+
ctx = VibecoreContext()
|
|
101
|
+
|
|
102
|
+
# Initialize MCP manager if configured
|
|
103
|
+
mcp_servers = []
|
|
104
|
+
if settings.mcp_servers:
|
|
105
|
+
# Create MCP manager
|
|
106
|
+
mcp_manager = MCPManager(settings.mcp_servers)
|
|
107
|
+
ctx.mcp_manager = mcp_manager
|
|
108
|
+
|
|
109
|
+
# Get the MCP servers from the manager
|
|
110
|
+
mcp_servers = mcp_manager.servers
|
|
111
|
+
|
|
112
|
+
# Create agent with MCP servers
|
|
113
|
+
agent = create_default_agent(mcp_servers=mcp_servers)
|
|
114
|
+
|
|
115
|
+
# Determine session to use
|
|
116
|
+
session_to_load = None
|
|
117
|
+
if continue_session:
|
|
118
|
+
session_to_load = find_latest_session()
|
|
119
|
+
if not session_to_load:
|
|
120
|
+
typer.echo("No existing sessions found for this project.")
|
|
121
|
+
raise typer.Exit(1)
|
|
122
|
+
typer.echo(f"Continuing session: {session_to_load}")
|
|
123
|
+
elif session_id:
|
|
124
|
+
session_to_load = session_id
|
|
125
|
+
typer.echo(f"Loading session: {session_to_load}")
|
|
126
|
+
|
|
127
|
+
# Create app
|
|
128
|
+
app_instance = VibecoreApp(ctx, agent, session_id=session_to_load, print_mode=print_mode)
|
|
129
|
+
|
|
130
|
+
if print_mode:
|
|
131
|
+
# Run in print mode
|
|
132
|
+
import asyncio
|
|
133
|
+
|
|
134
|
+
# Use provided prompt or None to read from stdin
|
|
135
|
+
result = asyncio.run(app_instance.run_print(prompt))
|
|
136
|
+
# Print raw output to stdout
|
|
137
|
+
if result:
|
|
138
|
+
print(result)
|
|
139
|
+
else:
|
|
140
|
+
# Run normal TUI mode
|
|
141
|
+
app_instance.run()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main():
|
|
145
|
+
"""Entry point for the CLI."""
|
|
146
|
+
app()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
vibecore/context.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
|
|
4
|
+
from vibecore.tools.python.manager import PythonExecutionManager
|
|
5
|
+
from vibecore.tools.todo.manager import TodoManager
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from vibecore.main import VibecoreApp
|
|
9
|
+
from vibecore.mcp import MCPManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class VibecoreContext:
|
|
14
|
+
todo_manager: TodoManager = field(default_factory=TodoManager)
|
|
15
|
+
python_manager: PythonExecutionManager = field(default_factory=PythonExecutionManager)
|
|
16
|
+
app: Optional["VibecoreApp"] = None
|
|
17
|
+
context_fullness: float = 0.0
|
|
18
|
+
mcp_manager: Optional["MCPManager"] = None
|
|
19
|
+
|
|
20
|
+
def reset_state(self) -> None:
|
|
21
|
+
"""Reset all context state for a new session."""
|
|
22
|
+
self.todo_manager = TodoManager()
|
|
23
|
+
self.python_manager = PythonExecutionManager()
|
|
24
|
+
self.context_fullness = 0.0
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Stream handler for processing agent streaming responses."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from agents import (
|
|
7
|
+
Agent,
|
|
8
|
+
AgentUpdatedStreamEvent,
|
|
9
|
+
MessageOutputItem,
|
|
10
|
+
RawResponsesStreamEvent,
|
|
11
|
+
RunItemStreamEvent,
|
|
12
|
+
RunResultStreaming,
|
|
13
|
+
StreamEvent,
|
|
14
|
+
ToolCallItem,
|
|
15
|
+
ToolCallOutputItem,
|
|
16
|
+
)
|
|
17
|
+
from openai.types.responses import (
|
|
18
|
+
ResponseFunctionToolCall,
|
|
19
|
+
ResponseOutputItemAddedEvent,
|
|
20
|
+
ResponseOutputItemDoneEvent,
|
|
21
|
+
ResponseReasoningItem,
|
|
22
|
+
ResponseReasoningSummaryPartAddedEvent,
|
|
23
|
+
ResponseReasoningSummaryTextDeltaEvent,
|
|
24
|
+
ResponseReasoningSummaryTextDoneEvent,
|
|
25
|
+
ResponseTextDeltaEvent,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from vibecore.widgets.messages import (
|
|
29
|
+
AgentMessage,
|
|
30
|
+
BaseMessage,
|
|
31
|
+
MessageStatus,
|
|
32
|
+
ReasoningMessage,
|
|
33
|
+
)
|
|
34
|
+
from vibecore.widgets.tool_message_factory import create_tool_message
|
|
35
|
+
from vibecore.widgets.tool_messages import BaseToolMessage, TaskToolMessage
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MessageHandler(Protocol):
|
|
39
|
+
"""Protocol defining the interface for message handling."""
|
|
40
|
+
|
|
41
|
+
async def handle_agent_message(self, message: BaseMessage) -> None:
|
|
42
|
+
"""Add a message to the widget's message list."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def handle_agent_update(self, new_agent: Agent) -> None:
|
|
46
|
+
"""Handle agent updates."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def handle_agent_error(self, error: Exception) -> None:
|
|
50
|
+
"""Handle errors during streaming."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def handle_agent_finished(self) -> None:
|
|
54
|
+
"""Handle when the agent has finished processing."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AgentStreamHandler:
|
|
59
|
+
"""Handles streaming responses from agents."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, message_handler: MessageHandler) -> None:
|
|
62
|
+
"""Initialize the stream handler.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
message_handler: The MessageHandler protocol instance
|
|
66
|
+
"""
|
|
67
|
+
self.message_handler = message_handler
|
|
68
|
+
self.message_content = ""
|
|
69
|
+
self.agent_message: AgentMessage | None = None
|
|
70
|
+
self.tool_messages: dict[str, BaseToolMessage] = {}
|
|
71
|
+
self.reasoning_messages: dict[str, ReasoningMessage] = {}
|
|
72
|
+
|
|
73
|
+
async def handle_text_delta(self, delta: str) -> None:
|
|
74
|
+
"""Handle incremental text updates from the agent.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
delta: The text delta to append
|
|
78
|
+
"""
|
|
79
|
+
self.message_content += delta
|
|
80
|
+
if not self.agent_message:
|
|
81
|
+
self.agent_message = AgentMessage(self.message_content, status=MessageStatus.EXECUTING)
|
|
82
|
+
await self.message_handler.handle_agent_message(self.agent_message)
|
|
83
|
+
else:
|
|
84
|
+
self.agent_message.update(self.message_content)
|
|
85
|
+
|
|
86
|
+
async def handle_tool_call(self, tool_name: str, arguments: str, call_id: str) -> None:
|
|
87
|
+
"""Create and display tool message when tool is invoked.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
tool_name: Name of the tool being called
|
|
91
|
+
arguments: JSON string of tool arguments
|
|
92
|
+
call_id: Unique identifier for this tool call
|
|
93
|
+
"""
|
|
94
|
+
# Use factory to create the appropriate tool message
|
|
95
|
+
tool_message = create_tool_message(tool_name, arguments)
|
|
96
|
+
|
|
97
|
+
self.tool_messages[call_id] = tool_message
|
|
98
|
+
await self.message_handler.handle_agent_message(tool_message)
|
|
99
|
+
|
|
100
|
+
async def handle_tool_output(self, output: str, call_id: str) -> None:
|
|
101
|
+
"""Update tool message with execution results.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
output: The tool execution output
|
|
105
|
+
call_id: The tool call identifier to match with pending calls
|
|
106
|
+
"""
|
|
107
|
+
if call_id in self.tool_messages:
|
|
108
|
+
tool_message = self.tool_messages[call_id]
|
|
109
|
+
tool_message.update(MessageStatus.SUCCESS, str(output))
|
|
110
|
+
|
|
111
|
+
async def handle_message_complete(self) -> None:
|
|
112
|
+
"""Finalize agent message when complete."""
|
|
113
|
+
if self.agent_message:
|
|
114
|
+
self.agent_message.update(self.message_content, status=MessageStatus.IDLE)
|
|
115
|
+
self.agent_message = None
|
|
116
|
+
self.message_content = ""
|
|
117
|
+
|
|
118
|
+
async def handle_event(self, event: StreamEvent) -> None:
|
|
119
|
+
"""Handle a single streaming event.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
event: The streaming event to process
|
|
123
|
+
"""
|
|
124
|
+
match event:
|
|
125
|
+
case RawResponsesStreamEvent(data=data):
|
|
126
|
+
match data:
|
|
127
|
+
case ResponseOutputItemAddedEvent(item=ResponseReasoningItem() as item):
|
|
128
|
+
reasoning_id = item.id
|
|
129
|
+
reasoning_message = ReasoningMessage("Thinking...", status=MessageStatus.EXECUTING)
|
|
130
|
+
self.reasoning_messages[reasoning_id] = reasoning_message
|
|
131
|
+
await self.message_handler.handle_agent_message(reasoning_message)
|
|
132
|
+
|
|
133
|
+
case ResponseReasoningSummaryPartAddedEvent() as e:
|
|
134
|
+
reasoning_id = e.item_id
|
|
135
|
+
reasoning_message = self.reasoning_messages[reasoning_id]
|
|
136
|
+
assert reasoning_message, f"Reasoning message with ID {reasoning_id} not found"
|
|
137
|
+
updated = reasoning_message.text + "\n\n" if reasoning_message.text != "Thinking..." else ""
|
|
138
|
+
reasoning_message.update(updated, status=MessageStatus.EXECUTING)
|
|
139
|
+
|
|
140
|
+
case ResponseReasoningSummaryTextDeltaEvent(item_id=reasoning_id, delta=delta):
|
|
141
|
+
reasoning_message = self.reasoning_messages[reasoning_id]
|
|
142
|
+
assert reasoning_message, f"Reasoning message with ID {reasoning_id} not found"
|
|
143
|
+
updated = reasoning_message.text + delta
|
|
144
|
+
reasoning_message.update(updated, status=MessageStatus.EXECUTING)
|
|
145
|
+
|
|
146
|
+
case ResponseReasoningSummaryTextDoneEvent(item_id=reasoning_id) as e:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
case ResponseOutputItemDoneEvent(item=ResponseReasoningItem() as item):
|
|
150
|
+
reasoning_id = item.id
|
|
151
|
+
reasoning_message = self.reasoning_messages[reasoning_id]
|
|
152
|
+
assert reasoning_message, f"Reasoning message with ID {reasoning_id} not found"
|
|
153
|
+
reasoning_message.update(reasoning_message.text, status=MessageStatus.IDLE)
|
|
154
|
+
|
|
155
|
+
case ResponseOutputItemAddedEvent(item=ResponseFunctionToolCall(name=tool_name, call_id=call_id)):
|
|
156
|
+
# TODO(serialx): We should move all tool call lifecycle start to here
|
|
157
|
+
# ResponseOutputItemDoneEvent can have race conditions in case of task tool calls
|
|
158
|
+
# Because we stream sub-agent events to our method: handle_task_tool_event()
|
|
159
|
+
# XXX(serialx): Just handle task tool call lifecycle here for now
|
|
160
|
+
# I'm deferring this because `arguments` is not available here... need to refactor
|
|
161
|
+
if tool_name == "task":
|
|
162
|
+
await self.handle_tool_call(tool_name, "{}", call_id)
|
|
163
|
+
|
|
164
|
+
case ResponseTextDeltaEvent(delta=delta) if delta:
|
|
165
|
+
await self.handle_text_delta(delta)
|
|
166
|
+
|
|
167
|
+
case ResponseOutputItemDoneEvent(
|
|
168
|
+
item=ResponseFunctionToolCall(name=tool_name, arguments=arguments, call_id=call_id)
|
|
169
|
+
):
|
|
170
|
+
# XXX(serialx): See above comments
|
|
171
|
+
if tool_name == "task":
|
|
172
|
+
assert call_id in self.tool_messages, f"Tool call ID {call_id} not found in tool messages"
|
|
173
|
+
task_tool_message = self.tool_messages[call_id]
|
|
174
|
+
assert isinstance(task_tool_message, TaskToolMessage), (
|
|
175
|
+
"Tool message must be a TaskToolMessage instance"
|
|
176
|
+
)
|
|
177
|
+
args = json.loads(arguments)
|
|
178
|
+
task_tool_message.description = args.get("description", "")
|
|
179
|
+
task_tool_message.prompt = args.get("prompt", "")
|
|
180
|
+
else:
|
|
181
|
+
await self.handle_tool_call(tool_name, arguments, call_id)
|
|
182
|
+
|
|
183
|
+
case RunItemStreamEvent(item=item):
|
|
184
|
+
match item:
|
|
185
|
+
case ToolCallItem():
|
|
186
|
+
pass
|
|
187
|
+
case ToolCallOutputItem(output=output, raw_item=raw_item):
|
|
188
|
+
# Find the corresponding tool message by call_id
|
|
189
|
+
if (
|
|
190
|
+
isinstance(raw_item, dict)
|
|
191
|
+
and "call_id" in raw_item
|
|
192
|
+
and raw_item["call_id"] in self.tool_messages
|
|
193
|
+
):
|
|
194
|
+
await self.handle_tool_output(output, raw_item["call_id"])
|
|
195
|
+
case MessageOutputItem():
|
|
196
|
+
await self.handle_message_complete()
|
|
197
|
+
|
|
198
|
+
case AgentUpdatedStreamEvent(new_agent=new_agent):
|
|
199
|
+
await self.message_handler.handle_agent_update(new_agent)
|
|
200
|
+
|
|
201
|
+
async def handle_task_tool_event(self, tool_name: str, tool_call_id: str, event: StreamEvent) -> None:
|
|
202
|
+
"""Handle streaming events from task tool sub-agents.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
tool_name: Name of the tool (e.g., "task")
|
|
206
|
+
tool_call_id: Unique identifier for this tool call
|
|
207
|
+
event: The streaming event from the sub-agent
|
|
208
|
+
|
|
209
|
+
Note: This is called by the main app to handle task tool events.
|
|
210
|
+
The main app receives this event from the agent's task tool handler.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
assert tool_call_id in self.tool_messages, f"Tool call ID {tool_call_id} not found in tool messages"
|
|
214
|
+
tool_message = self.tool_messages[tool_call_id]
|
|
215
|
+
assert isinstance(tool_message, TaskToolMessage), "Tool message must be a TaskToolMessage instance"
|
|
216
|
+
await tool_message.handle_task_tool_event(event)
|
|
217
|
+
|
|
218
|
+
async def process_stream(self, result: RunResultStreaming) -> None:
|
|
219
|
+
"""Process all streaming events from the agent.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
result: The streaming result to process
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
async for event in result.stream_events():
|
|
226
|
+
await self.handle_event(event)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
await self.message_handler.handle_agent_error(e)
|
|
229
|
+
finally:
|
|
230
|
+
await self.message_handler.handle_agent_finished()
|
|
231
|
+
# await self._cleanup()
|