vibecore 0.3.0__py3-none-any.whl → 0.6.2__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.
- vibecore/agents/default.py +3 -3
- vibecore/agents/task.py +3 -3
- vibecore/cli.py +67 -43
- vibecore/context.py +74 -11
- vibecore/flow.py +335 -73
- vibecore/handlers/stream_handler.py +35 -56
- vibecore/main.py +70 -272
- vibecore/session/jsonl_session.py +3 -1
- vibecore/session/loader.py +2 -2
- vibecore/settings.py +48 -1
- vibecore/tools/file/executor.py +59 -13
- vibecore/tools/file/tools.py +9 -9
- vibecore/tools/path_validator.py +251 -0
- vibecore/tools/python/helpers.py +2 -2
- vibecore/tools/python/tools.py +2 -2
- vibecore/tools/shell/executor.py +63 -7
- vibecore/tools/shell/tools.py +9 -9
- vibecore/tools/task/executor.py +2 -2
- vibecore/tools/task/tools.py +2 -2
- vibecore/tools/todo/manager.py +2 -10
- vibecore/tools/todo/models.py +5 -14
- vibecore/tools/todo/tools.py +5 -5
- vibecore/tools/webfetch/tools.py +1 -4
- vibecore/tools/websearch/ddgs/backend.py +1 -1
- vibecore/tools/websearch/tools.py +1 -4
- vibecore/widgets/core.py +3 -17
- vibecore/widgets/feedback.py +164 -0
- vibecore/widgets/feedback.tcss +121 -0
- vibecore/widgets/messages.py +22 -2
- vibecore/widgets/messages.tcss +28 -0
- vibecore/widgets/tool_messages.py +19 -4
- vibecore/widgets/tool_messages.tcss +23 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/METADATA +122 -29
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/RECORD +37 -34
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/WHEEL +0 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/entry_points.txt +0 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/licenses/LICENSE +0 -0
vibecore/main.py
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import traceback
|
|
3
3
|
from collections import deque
|
|
4
|
-
from typing import ClassVar, Literal
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
5
5
|
|
|
6
6
|
from agents import (
|
|
7
|
-
Agent,
|
|
8
|
-
ModelSettings,
|
|
9
|
-
Runner,
|
|
10
7
|
RunResultStreaming,
|
|
8
|
+
Session,
|
|
11
9
|
StreamEvent,
|
|
12
|
-
TResponseInputItem,
|
|
13
10
|
)
|
|
14
|
-
from openai.types import Reasoning
|
|
15
11
|
from openai.types.responses.response_output_message import Content
|
|
16
12
|
from textual import log, work
|
|
17
13
|
from textual.app import App, ComposeResult
|
|
18
14
|
from textual.binding import Binding
|
|
19
15
|
from textual.reactive import reactive
|
|
16
|
+
from textual.selection import Selection
|
|
17
|
+
from textual.widget import Widget
|
|
20
18
|
from textual.widgets import Header
|
|
21
19
|
from textual.worker import Worker
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from vibecore.flow import TWorkflowReturn, VibecoreTextualRunner
|
|
23
|
+
|
|
24
24
|
from vibecore.handlers import AgentStreamHandler
|
|
25
|
-
from vibecore.session import JSONLSession
|
|
26
25
|
from vibecore.session.loader import SessionLoader
|
|
27
|
-
from vibecore.settings import settings
|
|
28
26
|
from vibecore.utils.text import TextExtractor
|
|
29
27
|
from vibecore.widgets.core import AppFooter, MainScroll, MyTextArea
|
|
30
28
|
from vibecore.widgets.info import Welcome
|
|
@@ -37,34 +35,13 @@ class AppIsExiting(Exception):
|
|
|
37
35
|
pass
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
def detect_reasoning_effort(prompt: str) -> Literal["low", "medium", "high"] | None:
|
|
41
|
-
"""Detect reasoning effort level from user prompt keywords.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
prompt: User input text
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
Reasoning effort level or None if no keywords detected
|
|
48
|
-
"""
|
|
49
|
-
prompt_lower = prompt.lower()
|
|
50
|
-
|
|
51
|
-
# Check for highest priority keywords first
|
|
52
|
-
if "ultrathink" in prompt_lower:
|
|
53
|
-
return "high"
|
|
54
|
-
elif "think hard" in prompt_lower:
|
|
55
|
-
return "medium"
|
|
56
|
-
elif "think" in prompt_lower:
|
|
57
|
-
return "low"
|
|
58
|
-
|
|
59
|
-
return None
|
|
60
|
-
|
|
61
|
-
|
|
62
38
|
class VibecoreApp(App):
|
|
63
39
|
"""A Textual app to manage stopwatches."""
|
|
64
40
|
|
|
65
41
|
CSS_PATH: ClassVar = [
|
|
66
42
|
"widgets/core.tcss",
|
|
67
43
|
"widgets/messages.tcss",
|
|
44
|
+
"widgets/feedback.tcss",
|
|
68
45
|
"widgets/tool_messages.tcss",
|
|
69
46
|
"widgets/expandable.tcss",
|
|
70
47
|
"widgets/info.tcss",
|
|
@@ -82,10 +59,7 @@ class VibecoreApp(App):
|
|
|
82
59
|
|
|
83
60
|
def __init__(
|
|
84
61
|
self,
|
|
85
|
-
|
|
86
|
-
agent: Agent,
|
|
87
|
-
session_id: str | None = None,
|
|
88
|
-
print_mode: bool = False,
|
|
62
|
+
runner: "VibecoreTextualRunner[TWorkflowReturn]",
|
|
89
63
|
show_welcome: bool = True,
|
|
90
64
|
) -> None:
|
|
91
65
|
"""Initialize the Vibecore app with context and agent.
|
|
@@ -94,72 +68,45 @@ class VibecoreApp(App):
|
|
|
94
68
|
context: The VibecoreContext instance
|
|
95
69
|
agent: The Agent instance to use
|
|
96
70
|
session_id: Optional session ID to load existing session
|
|
97
|
-
print_mode: Whether to run in print mode (useful for pipes)
|
|
98
71
|
show_welcome: Whether to show the welcome message (default: True)
|
|
99
72
|
"""
|
|
100
|
-
self.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.input_items: list[TResponseInputItem] = []
|
|
73
|
+
self.runner = runner
|
|
74
|
+
if runner.context:
|
|
75
|
+
runner.context.app = self # Set the app reference in context
|
|
104
76
|
self.current_result: RunResultStreaming | None = None
|
|
105
77
|
self.current_worker: Worker[None] | None = None
|
|
106
|
-
self._session_id_provided = session_id is not None # Track if continuing session
|
|
107
|
-
self.print_mode = print_mode
|
|
108
78
|
self.show_welcome = show_welcome
|
|
109
79
|
self.message_queue: deque[str] = deque() # Queue for user messages
|
|
80
|
+
self.user_input_event = asyncio.Event() # Initialize event for user input coordination
|
|
110
81
|
|
|
111
|
-
|
|
112
|
-
if settings.session.storage_type == "jsonl":
|
|
113
|
-
if session_id is None:
|
|
114
|
-
# Generate a new session ID based on current date/time
|
|
115
|
-
import datetime
|
|
82
|
+
super().__init__()
|
|
116
83
|
|
|
117
|
-
|
|
84
|
+
def on_mouse_up(self) -> None:
|
|
85
|
+
if not self.screen.selections:
|
|
86
|
+
return None
|
|
118
87
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
88
|
+
widget_text: list[str] = []
|
|
89
|
+
for widget, selection in self.screen.selections.items():
|
|
90
|
+
assert isinstance(widget, Widget) and isinstance(selection, Selection)
|
|
91
|
+
if "copy-button" in widget.classes: # Skip copy buttons
|
|
92
|
+
continue
|
|
93
|
+
selected_text_in_widget = widget.get_selection(selection)
|
|
94
|
+
if selected_text_in_widget is not None:
|
|
95
|
+
widget_text.extend(selected_text_in_widget)
|
|
126
96
|
|
|
127
|
-
|
|
97
|
+
selected_text = "".join(widget_text)
|
|
98
|
+
self.copy_to_clipboard(selected_text)
|
|
99
|
+
self.notify("Copied to clipboard")
|
|
128
100
|
|
|
129
101
|
def compose(self) -> ComposeResult:
|
|
130
102
|
"""Create child widgets for the app."""
|
|
131
103
|
yield Header()
|
|
132
104
|
yield AppFooter()
|
|
133
|
-
with MainScroll(id="messages"):
|
|
105
|
+
with MainScroll(id="messages") as main_scroll:
|
|
106
|
+
main_scroll.anchor()
|
|
134
107
|
if self.show_welcome:
|
|
135
108
|
yield Welcome()
|
|
136
109
|
|
|
137
|
-
async def on_mount(self) -> None:
|
|
138
|
-
"""Called when the app is mounted."""
|
|
139
|
-
# Connect to MCP servers if configured
|
|
140
|
-
if self.context.mcp_manager:
|
|
141
|
-
try:
|
|
142
|
-
await self.context.mcp_manager.connect()
|
|
143
|
-
log(f"Connected to {len(self.context.mcp_manager.servers)} MCP servers")
|
|
144
|
-
except Exception as e:
|
|
145
|
-
log(f"Failed to connect to MCP servers: {e}")
|
|
146
|
-
# Continue without MCP servers rather than crashing
|
|
147
|
-
|
|
148
|
-
# Load session history if we're continuing from a previous session
|
|
149
|
-
if self._session_id_provided:
|
|
150
|
-
await self.load_session_history()
|
|
151
|
-
|
|
152
|
-
async def on_unmount(self) -> None:
|
|
153
|
-
"""Called when the app is being unmounted (shutdown)."""
|
|
154
|
-
# Cleanup MCP servers during unmount
|
|
155
|
-
if self.context.mcp_manager:
|
|
156
|
-
try:
|
|
157
|
-
log("Disconnecting from MCP servers...")
|
|
158
|
-
await self.context.mcp_manager.disconnect()
|
|
159
|
-
log("Disconnected from MCP servers")
|
|
160
|
-
except Exception as e:
|
|
161
|
-
log(f"Error disconnecting from MCP servers during unmount: {e}")
|
|
162
|
-
|
|
163
110
|
def extract_text_from_content(self, content: list[Content]) -> str:
|
|
164
111
|
"""Extract text from various content formats."""
|
|
165
112
|
return TextExtractor.extract_from_content(content)
|
|
@@ -179,10 +126,9 @@ class VibecoreApp(App):
|
|
|
179
126
|
"""Add a message widget to the main scroll area."""
|
|
180
127
|
await self.add_message(message)
|
|
181
128
|
|
|
182
|
-
async def
|
|
183
|
-
"""
|
|
184
|
-
|
|
185
|
-
self.agent = new_agent
|
|
129
|
+
async def handle_agent_message_update(self, message: BaseMessage) -> None:
|
|
130
|
+
"""Message in the widget's message list is updated with new delta or status"""
|
|
131
|
+
pass
|
|
186
132
|
|
|
187
133
|
async def handle_agent_error(self, error: Exception) -> None:
|
|
188
134
|
"""Handle errors during streaming."""
|
|
@@ -207,14 +153,16 @@ class VibecoreApp(App):
|
|
|
207
153
|
try:
|
|
208
154
|
last_message = main_scroll.query_one("BaseMessage:last-child", BaseMessage)
|
|
209
155
|
if last_message.status == MessageStatus.EXECUTING:
|
|
156
|
+
# XXX(serialx): Consider marking it as cancelled instead
|
|
157
|
+
# last_message.status = MessageStatus.ERROR
|
|
210
158
|
last_message.remove()
|
|
211
159
|
except Exception:
|
|
212
160
|
# No messages to clean up
|
|
213
161
|
pass
|
|
214
162
|
|
|
215
|
-
async def load_session_history(self) -> None:
|
|
163
|
+
async def load_session_history(self, session: Session) -> None:
|
|
216
164
|
"""Load and display messages from session history."""
|
|
217
|
-
loader = SessionLoader(
|
|
165
|
+
loader = SessionLoader(session)
|
|
218
166
|
messages = await loader.load_history()
|
|
219
167
|
|
|
220
168
|
# Remove Welcome widget if we have messages
|
|
@@ -237,10 +185,24 @@ class VibecoreApp(App):
|
|
|
237
185
|
|
|
238
186
|
async def wait_for_user_input(self) -> str:
|
|
239
187
|
"""Used in flow mode. See examples/basic_agent.py"""
|
|
188
|
+
if self.message_queue:
|
|
189
|
+
user_input = self.message_queue.popleft()
|
|
190
|
+
|
|
191
|
+
user_message = UserMessage(user_input)
|
|
192
|
+
await self.add_message(user_message)
|
|
193
|
+
self.get_child_by_id("messages").scroll_end()
|
|
194
|
+
|
|
195
|
+
return user_input
|
|
196
|
+
|
|
240
197
|
self.agent_status = "waiting_user_input"
|
|
241
|
-
self.user_input_event
|
|
198
|
+
self.user_input_event.clear() # Reset the event for next wait
|
|
242
199
|
await self.user_input_event.wait()
|
|
243
|
-
user_input = self.message_queue.
|
|
200
|
+
user_input = self.message_queue.popleft()
|
|
201
|
+
|
|
202
|
+
user_message = UserMessage(user_input)
|
|
203
|
+
await self.add_message(user_message)
|
|
204
|
+
self.get_child_by_id("messages").scroll_end()
|
|
205
|
+
|
|
244
206
|
return user_input
|
|
245
207
|
|
|
246
208
|
async def on_my_text_area_user_message(self, event: MyTextArea.UserMessage) -> None:
|
|
@@ -248,12 +210,8 @@ class VibecoreApp(App):
|
|
|
248
210
|
if event.text:
|
|
249
211
|
# Check for special commands
|
|
250
212
|
text_strip = event.text.strip()
|
|
251
|
-
if text_strip == "/
|
|
252
|
-
await self.handle_clear_command()
|
|
253
|
-
return
|
|
254
|
-
elif text_strip == "/help":
|
|
213
|
+
if text_strip == "/help":
|
|
255
214
|
help_text = "Available commands:\n"
|
|
256
|
-
help_text += "• /clear - Clear the current session and start a new one\n"
|
|
257
215
|
help_text += "• /help - Show this help message\n\n"
|
|
258
216
|
help_text += "Keyboard shortcuts:\n"
|
|
259
217
|
help_text += "• Esc - Cancel current agent operation\n"
|
|
@@ -262,13 +220,6 @@ class VibecoreApp(App):
|
|
|
262
220
|
await self.add_message(SystemMessage(help_text))
|
|
263
221
|
return
|
|
264
222
|
|
|
265
|
-
user_message = UserMessage(event.text)
|
|
266
|
-
await self.add_message(user_message)
|
|
267
|
-
user_message.scroll_visible()
|
|
268
|
-
|
|
269
|
-
if self.agent_status == "waiting_user_input":
|
|
270
|
-
self.message_queue.append(event.text)
|
|
271
|
-
self.user_input_event.set()
|
|
272
223
|
if self.agent_status == "running":
|
|
273
224
|
# If agent is running, queue the message
|
|
274
225
|
self.message_queue.append(event.text)
|
|
@@ -280,41 +231,8 @@ class VibecoreApp(App):
|
|
|
280
231
|
status="Generating…", metadata=f"{queued_count} message{'s' if queued_count > 1 else ''} queued"
|
|
281
232
|
)
|
|
282
233
|
else:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
reasoning_effort = detected_effort or settings.reasoning_effort
|
|
286
|
-
|
|
287
|
-
# Create agent with appropriate reasoning effort
|
|
288
|
-
agent_to_use = self.agent
|
|
289
|
-
if reasoning_effort is not None:
|
|
290
|
-
# Create a copy of the agent with updated model settings
|
|
291
|
-
current_settings = self.agent.model_settings or ModelSettings()
|
|
292
|
-
new_reasoning = Reasoning(effort=reasoning_effort, summary=settings.reasoning_summary)
|
|
293
|
-
updated_settings = ModelSettings(
|
|
294
|
-
include_usage=current_settings.include_usage,
|
|
295
|
-
reasoning=new_reasoning,
|
|
296
|
-
)
|
|
297
|
-
agent_to_use = Agent[VibecoreContext](
|
|
298
|
-
name=self.agent.name,
|
|
299
|
-
handoff_description=self.agent.handoff_description,
|
|
300
|
-
instructions=self.agent.instructions,
|
|
301
|
-
tools=self.agent.tools,
|
|
302
|
-
model=self.agent.model,
|
|
303
|
-
model_settings=updated_settings,
|
|
304
|
-
handoffs=self.agent.handoffs,
|
|
305
|
-
mcp_servers=self.agent.mcp_servers,
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
# Process the message immediately
|
|
309
|
-
result = Runner.run_streamed(
|
|
310
|
-
agent_to_use,
|
|
311
|
-
input=event.text, # Pass string directly when using session
|
|
312
|
-
context=self.context,
|
|
313
|
-
max_turns=settings.max_turns,
|
|
314
|
-
session=self.session,
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
self.current_worker = self.handle_streamed_response(result)
|
|
234
|
+
self.message_queue.append(event.text)
|
|
235
|
+
self.user_input_event.set()
|
|
318
236
|
|
|
319
237
|
@work(exclusive=True)
|
|
320
238
|
async def handle_streamed_response(self, result: RunResultStreaming) -> None:
|
|
@@ -324,45 +242,27 @@ class VibecoreApp(App):
|
|
|
324
242
|
self.agent_stream_handler = AgentStreamHandler(self)
|
|
325
243
|
await self.agent_stream_handler.process_stream(result)
|
|
326
244
|
|
|
327
|
-
|
|
245
|
+
# Determine usage based on the last model response rather than the aggregated usage
|
|
246
|
+
# from the entire session so that context fullness reflects the most recent request.
|
|
247
|
+
used_tokens: float = 0.0
|
|
248
|
+
if result.raw_responses:
|
|
249
|
+
last_response = result.raw_responses[-1]
|
|
250
|
+
last_usage = getattr(last_response, "usage", None)
|
|
251
|
+
if last_usage:
|
|
252
|
+
used_tokens = float(last_usage.total_tokens)
|
|
253
|
+
|
|
328
254
|
max_ctx = self._get_model_context_window()
|
|
329
|
-
log(f"Context usage: {
|
|
330
|
-
|
|
255
|
+
log(f"Context usage: {used_tokens} / {max_ctx} total tokens")
|
|
256
|
+
context_fullness = min(1.0, used_tokens / float(max_ctx))
|
|
331
257
|
footer = self.query_one(AppFooter)
|
|
332
|
-
footer.set_context_progress(
|
|
258
|
+
footer.set_context_progress(context_fullness)
|
|
333
259
|
|
|
334
260
|
self.agent_status = "idle"
|
|
335
261
|
self.current_result = None
|
|
336
262
|
self.current_worker = None
|
|
337
263
|
|
|
338
|
-
await self.process_message_queue()
|
|
339
|
-
|
|
340
|
-
async def process_message_queue(self) -> None:
|
|
341
|
-
"""Process any messages that were queued while the agent was running."""
|
|
342
|
-
if self.message_queue:
|
|
343
|
-
# Get the next message from the queue
|
|
344
|
-
next_message = self.message_queue.popleft()
|
|
345
|
-
log(f"Processing queued message: {next_message}")
|
|
346
|
-
|
|
347
|
-
# Process the message
|
|
348
|
-
result = Runner.run_streamed(
|
|
349
|
-
self.agent,
|
|
350
|
-
input=next_message,
|
|
351
|
-
context=self.context,
|
|
352
|
-
max_turns=settings.max_turns,
|
|
353
|
-
session=self.session,
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
self.current_worker = self.handle_streamed_response(result)
|
|
357
|
-
|
|
358
|
-
def on_click(self) -> None:
|
|
359
|
-
self.query_one("#input-textarea").focus()
|
|
360
|
-
|
|
361
264
|
def _get_model_context_window(self) -> int:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
model_name = settings.default_model
|
|
365
|
-
log(f"Getting context window for model: {model_name}")
|
|
265
|
+
# TODO(serialx): Implement later
|
|
366
266
|
return 200000
|
|
367
267
|
|
|
368
268
|
def action_toggle_dark(self) -> None:
|
|
@@ -402,7 +302,7 @@ class VibecoreApp(App):
|
|
|
402
302
|
"""Reset exit confirmation after 1 second and remove the message."""
|
|
403
303
|
try:
|
|
404
304
|
# Wait for 1 second
|
|
405
|
-
await asyncio.sleep(
|
|
305
|
+
await asyncio.sleep(2.0)
|
|
406
306
|
|
|
407
307
|
# Reset confirmation state
|
|
408
308
|
self._exit_confirmation_active = False
|
|
@@ -413,55 +313,6 @@ class VibecoreApp(App):
|
|
|
413
313
|
# Task was cancelled (new Ctrl-D pressed)
|
|
414
314
|
pass
|
|
415
315
|
|
|
416
|
-
async def run_print(self, prompt: str | None = None) -> str:
|
|
417
|
-
"""Run the agent and return the raw output for printing.
|
|
418
|
-
|
|
419
|
-
Args:
|
|
420
|
-
prompt: Optional prompt text. If not provided, reads from stdin.
|
|
421
|
-
|
|
422
|
-
Returns:
|
|
423
|
-
The agent's text output as a string
|
|
424
|
-
"""
|
|
425
|
-
import sys
|
|
426
|
-
|
|
427
|
-
# Use provided prompt or read from stdin
|
|
428
|
-
input_text = prompt.strip() if prompt else sys.stdin.read().strip()
|
|
429
|
-
|
|
430
|
-
if not input_text:
|
|
431
|
-
return ""
|
|
432
|
-
|
|
433
|
-
# Import needed event types
|
|
434
|
-
from agents import RawResponsesStreamEvent
|
|
435
|
-
from openai.types.responses import ResponseTextDeltaEvent
|
|
436
|
-
|
|
437
|
-
if self.context.mcp_manager:
|
|
438
|
-
await self.context.mcp_manager.connect()
|
|
439
|
-
|
|
440
|
-
# Run the agent
|
|
441
|
-
result = Runner.run_streamed(
|
|
442
|
-
self.agent,
|
|
443
|
-
input=input_text,
|
|
444
|
-
context=self.context,
|
|
445
|
-
max_turns=settings.max_turns,
|
|
446
|
-
session=self.session,
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
# Collect all agent text output
|
|
450
|
-
agent_output = ""
|
|
451
|
-
|
|
452
|
-
async for event in result.stream_events():
|
|
453
|
-
# Handle text output from agent
|
|
454
|
-
match event:
|
|
455
|
-
case RawResponsesStreamEvent(data=data):
|
|
456
|
-
match data:
|
|
457
|
-
case ResponseTextDeltaEvent(delta=delta) if delta:
|
|
458
|
-
agent_output += delta
|
|
459
|
-
|
|
460
|
-
if self.context.mcp_manager:
|
|
461
|
-
await self.context.mcp_manager.disconnect()
|
|
462
|
-
|
|
463
|
-
return agent_output.strip()
|
|
464
|
-
|
|
465
316
|
async def handle_task_tool_event(self, tool_name: str, tool_call_id: str, event: StreamEvent) -> None:
|
|
466
317
|
"""Handle streaming events from task tool sub-agents.
|
|
467
318
|
|
|
@@ -473,56 +324,3 @@ class VibecoreApp(App):
|
|
|
473
324
|
Note: The main app receives this event from the agent's task tool handler.
|
|
474
325
|
"""
|
|
475
326
|
await self.agent_stream_handler.handle_task_tool_event(tool_name, tool_call_id, event)
|
|
476
|
-
|
|
477
|
-
async def handle_clear_command(self) -> None:
|
|
478
|
-
"""Handle the /clear command to create a new session and clear the UI."""
|
|
479
|
-
log("Clearing session and creating new session")
|
|
480
|
-
|
|
481
|
-
# Cancel any running agent
|
|
482
|
-
if self.agent_status == "running":
|
|
483
|
-
self.action_cancel_agent()
|
|
484
|
-
|
|
485
|
-
# Clear message queue
|
|
486
|
-
self.message_queue.clear()
|
|
487
|
-
|
|
488
|
-
# Generate a new session ID
|
|
489
|
-
import datetime
|
|
490
|
-
|
|
491
|
-
new_session_id = f"chat-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
492
|
-
|
|
493
|
-
# Create new session
|
|
494
|
-
if settings.session.storage_type == "jsonl":
|
|
495
|
-
self.session = JSONLSession(
|
|
496
|
-
session_id=new_session_id,
|
|
497
|
-
project_path=None, # Will use current working directory
|
|
498
|
-
base_dir=settings.session.base_dir,
|
|
499
|
-
)
|
|
500
|
-
else:
|
|
501
|
-
raise NotImplementedError("SQLite session support will be added later")
|
|
502
|
-
|
|
503
|
-
# Reset context state
|
|
504
|
-
self.context.reset_state()
|
|
505
|
-
|
|
506
|
-
# Clear input items
|
|
507
|
-
self.input_items.clear()
|
|
508
|
-
|
|
509
|
-
# Clear the UI - remove all messages and add welcome back
|
|
510
|
-
main_scroll = self.query_one("#messages", MainScroll)
|
|
511
|
-
|
|
512
|
-
# Remove all existing messages
|
|
513
|
-
for message in main_scroll.query("BaseMessage"):
|
|
514
|
-
message.remove()
|
|
515
|
-
|
|
516
|
-
# Remove welcome if it exists
|
|
517
|
-
for welcome in main_scroll.query("Welcome"):
|
|
518
|
-
welcome.remove()
|
|
519
|
-
|
|
520
|
-
# Add welcome widget back if show_welcome is True
|
|
521
|
-
if self.show_welcome:
|
|
522
|
-
await main_scroll.mount(Welcome())
|
|
523
|
-
|
|
524
|
-
# Show system message to confirm the clear operation
|
|
525
|
-
system_message = SystemMessage(f"✨ Session cleared! Started new session: {new_session_id}")
|
|
526
|
-
await main_scroll.mount(system_message)
|
|
527
|
-
|
|
528
|
-
log(f"New session created: {new_session_id}")
|
|
@@ -5,6 +5,8 @@ import logging
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from agents import Session
|
|
9
|
+
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from openai.types.responses import ResponseInputItemParam as TResponseInputItem
|
|
10
12
|
|
|
@@ -14,7 +16,7 @@ from .path_utils import get_session_file_path
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class JSONLSession:
|
|
19
|
+
class JSONLSession(Session):
|
|
18
20
|
"""JSONL-based implementation of the agents.Session protocol.
|
|
19
21
|
|
|
20
22
|
Stores conversation history in JSON Lines format, with one JSON object
|
vibecore/session/loader.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
+
from agents import Session
|
|
5
6
|
from openai.types.responses import (
|
|
6
7
|
ResponseFunctionToolCall,
|
|
7
8
|
ResponseInputItemParam,
|
|
@@ -12,7 +13,6 @@ from openai.types.responses import (
|
|
|
12
13
|
from pydantic import TypeAdapter
|
|
13
14
|
from textual import log
|
|
14
15
|
|
|
15
|
-
from vibecore.session.jsonl_session import JSONLSession
|
|
16
16
|
from vibecore.utils.text import TextExtractor
|
|
17
17
|
from vibecore.widgets.messages import (
|
|
18
18
|
AgentMessage,
|
|
@@ -28,7 +28,7 @@ from vibecore.widgets.tool_messages import BaseToolMessage
|
|
|
28
28
|
class SessionLoader:
|
|
29
29
|
"""Loads and parses session history into message widgets."""
|
|
30
30
|
|
|
31
|
-
def __init__(self, session:
|
|
31
|
+
def __init__(self, session: Session):
|
|
32
32
|
"""Initialize SessionLoader with a session.
|
|
33
33
|
|
|
34
34
|
Args:
|
vibecore/settings.py
CHANGED
|
@@ -39,6 +39,48 @@ class SessionSettings(BaseModel):
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
class PathConfinementSettings(BaseModel):
|
|
43
|
+
"""Configuration for path confinement."""
|
|
44
|
+
|
|
45
|
+
enabled: bool = Field(
|
|
46
|
+
default=True,
|
|
47
|
+
description="Enable path confinement for file and shell tools",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
allowed_directories: list[Path] = Field(
|
|
51
|
+
default_factory=lambda: [Path.cwd()],
|
|
52
|
+
description="List of directories that tools can access",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
allow_home: bool = Field(
|
|
56
|
+
default=False,
|
|
57
|
+
description="Allow access to user's home directory",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
allow_temp: bool = Field(
|
|
61
|
+
default=True,
|
|
62
|
+
description="Allow access to system temp directories",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
strict_mode: bool = Field(
|
|
66
|
+
default=False,
|
|
67
|
+
description="Strict mode prevents any path traversal attempts",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@field_validator("allowed_directories", mode="before")
|
|
71
|
+
@classmethod
|
|
72
|
+
def resolve_paths(cls, v: list[str | Path]) -> list[Path]:
|
|
73
|
+
"""Resolve and validate directory paths."""
|
|
74
|
+
paths = []
|
|
75
|
+
for p in v:
|
|
76
|
+
path = Path(p).expanduser().resolve()
|
|
77
|
+
if not path.exists():
|
|
78
|
+
# Create directory if it doesn't exist
|
|
79
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
paths.append(path)
|
|
81
|
+
return paths
|
|
82
|
+
|
|
83
|
+
|
|
42
84
|
class MCPServerConfig(BaseModel):
|
|
43
85
|
"""Configuration for an MCP server."""
|
|
44
86
|
|
|
@@ -155,6 +197,12 @@ class Settings(BaseSettings):
|
|
|
155
197
|
description="List of MCP servers to connect to",
|
|
156
198
|
)
|
|
157
199
|
|
|
200
|
+
# Path confinement configuration
|
|
201
|
+
path_confinement: PathConfinementSettings = Field(
|
|
202
|
+
default_factory=PathConfinementSettings,
|
|
203
|
+
description="Path confinement configuration",
|
|
204
|
+
)
|
|
205
|
+
|
|
158
206
|
rich_tool_names: list[str] = Field(
|
|
159
207
|
default_factory=list,
|
|
160
208
|
description="List of tools to render with RichToolMessage (temporary settings)",
|
|
@@ -207,7 +255,6 @@ class Settings(BaseSettings):
|
|
|
207
255
|
return (
|
|
208
256
|
init_settings,
|
|
209
257
|
env_settings,
|
|
210
|
-
dotenv_settings,
|
|
211
258
|
YamlConfigSettingsSource(settings_cls),
|
|
212
259
|
file_secret_settings,
|
|
213
260
|
)
|