diff --git a/agent_core/core/impl/action/manager.py b/agent_core/core/impl/action/manager.py
index 75ca3091..54152a26 100644
--- a/agent_core/core/impl/action/manager.py
+++ b/agent_core/core/impl/action/manager.py
@@ -92,6 +92,33 @@ def __init__(
self._on_action_end = on_action_end
self._get_parent_id = get_parent_id
+ def _generate_unique_session_id(self) -> str:
+ """Generate a unique 6-character session ID.
+
+ Creates a short session ID using the first 6 hex characters of a UUID4.
+ Checks for duplicates against active task IDs from state_manager.
+
+ Returns:
+ A unique 6-character hex string session ID.
+ """
+ max_attempts = 100
+ for _ in range(max_attempts):
+ candidate = uuid.uuid4().hex[:6]
+
+ # Check against active task IDs from state manager
+ try:
+ main_state = self.state_manager.get_main_state()
+ existing_ids = set(main_state.active_task_ids) if main_state else set()
+ except Exception:
+ existing_ids = set()
+
+ if candidate not in existing_ids:
+ return candidate
+
+ # Fallback to full UUID hex if somehow all short IDs are taken
+ logger.warning("Could not generate unique 6-char session ID after 100 attempts, using full UUID")
+ return uuid.uuid4().hex
+
# ------------------------------------------------------------------
# Public helpers
# ------------------------------------------------------------------
@@ -397,7 +424,7 @@ async def execute_single(action: Action, input_data: Dict, action_session_id: st
for action, input_data in actions:
if action.name == "task_start":
# Generate unique session_id for each task_start to prevent overwriting
- action_session_id = str(uuid.uuid4())
+ action_session_id = self._generate_unique_session_id()
logger.info(f"[PARALLEL] Assigning unique session_id {action_session_id} to task_start")
else:
action_session_id = session_id
diff --git a/app/agent_base.py b/app/agent_base.py
index 81b3ab1b..6e1934db 100644
--- a/app/agent_base.py
+++ b/app/agent_base.py
@@ -269,6 +269,7 @@ def __init__(
# ── misc ──
self.is_running: bool = True
self._interface_mode: str = "tui" # Will be updated in run() based on selected interface
+ self.ui_controller = None # Set by interface after UIController is created
self._extra_system_prompt: str = self._load_extra_system_prompt()
# Scheduler for periodic tasks (memory processing, proactive checks, etc.)
@@ -1429,6 +1430,39 @@ def _format_sessions_for_routing(
return "\n\n".join(sections)
+ async def _generate_unique_session_id(self) -> str:
+ """Generate a unique 6-character session ID.
+
+ Creates a short session ID using the first 6 hex characters of a UUID4.
+ Checks for duplicates against running tasks and queued/active triggers.
+
+ Returns:
+ A unique 6-character hex string session ID.
+ """
+ max_attempts = 100 # Prevent infinite loop in edge cases
+ for _ in range(max_attempts):
+ candidate = uuid.uuid4().hex[:6]
+
+ # Check against running tasks
+ existing_task_ids = set(self.task_manager.tasks.keys())
+
+ # Check against queued triggers
+ queued_triggers = await self.triggers.list_triggers()
+ queued_session_ids = {t.session_id for t in queued_triggers if t.session_id}
+
+ # Check against active triggers (being processed)
+ active_session_ids = set(self.triggers._active.keys())
+
+ # Combine all existing IDs
+ all_existing_ids = existing_task_ids | queued_session_ids | active_session_ids
+
+ if candidate not in all_existing_ids:
+ return candidate
+
+ # Fallback to full UUID if somehow all short IDs are taken (extremely unlikely)
+ logger.warning("Could not generate unique 6-char session ID after 100 attempts, using full UUID")
+ return uuid.uuid4().hex
+
async def _route_to_session(
self,
item_type: str,
@@ -1526,6 +1560,49 @@ async def _handle_chat_message(self, payload: Dict):
f"[CHAT] Routed message to existing session {matched_session_id} "
f"(fired={fired}, reason: {routing_result.get('reason', 'N/A')})"
)
+
+ # Reset task status from "waiting" to "running" when user replies
+ # Update UI regardless of fire() result - user has replied so we should
+ # acknowledge it. If fire() failed, the task may be stale but we still
+ # want to reset the waiting indicator.
+ if self.ui_controller:
+ from app.ui_layer.events import UIEvent, UIEventType
+
+ self.ui_controller.event_bus.emit(
+ UIEvent(
+ type=UIEventType.TASK_UPDATE,
+ data={
+ "task_id": matched_session_id,
+ "status": "running",
+ },
+ )
+ )
+
+ # Check if there are still other tasks waiting
+ # If not, update global agent state back to working
+ triggers = await self.triggers.list_triggers()
+ has_waiting_tasks = any(
+ getattr(t, 'waiting_for_reply', False)
+ for t in triggers
+ if t.session_id != matched_session_id
+ )
+ if not has_waiting_tasks:
+ self.ui_controller.event_bus.emit(
+ UIEvent(
+ type=UIEventType.AGENT_STATE_CHANGED,
+ data={
+ "state": "working",
+ "status_message": "Agent is working...",
+ },
+ )
+ )
+
+ if not fired:
+ logger.warning(
+ f"[CHAT] Trigger not found for session {matched_session_id} - "
+ "message may not be delivered to task"
+ )
+
# Always trust routing decision - don't create new session
return
@@ -1560,7 +1637,7 @@ async def _handle_chat_message(self, payload: Dict):
"Please perform action that best suit this user chat "
f"you just received{platform_hint}: {chat_content}"
),
- session_id=str(uuid.uuid4()), # Generate unique session ID
+ session_id=await self._generate_unique_session_id(),
payload=trigger_payload,
),
skip_merge=True,
diff --git a/app/browser/interface.py b/app/browser/interface.py
index 37e5845d..1b89e73d 100644
--- a/app/browser/interface.py
+++ b/app/browser/interface.py
@@ -46,6 +46,7 @@ def __init__(
enable_action_panel=True, # Browser has action panel
)
self._controller = UIController(agent, self._config)
+ agent.ui_controller = self._controller # Back-reference for event emission
# Create browser adapter
self._adapter = BrowserAdapter(self._controller)
diff --git a/app/cli/interface.py b/app/cli/interface.py
index 7d7fc098..8af4ce66 100644
--- a/app/cli/interface.py
+++ b/app/cli/interface.py
@@ -46,6 +46,7 @@ def __init__(
enable_action_panel=False, # CLI uses inline action display
)
self._controller = UIController(agent, self._config)
+ agent.ui_controller = self._controller # Back-reference for event emission
# Create CLI adapter
self._adapter = CLIAdapter(self._controller)
diff --git a/app/config/settings.json b/app/config/settings.json
index afdf424b..403e9084 100644
--- a/app/config/settings.json
+++ b/app/config/settings.json
@@ -1,7 +1,6 @@
{
"general": {
- "agent_name": "CraftBot",
- "os_language": "en"
+ "agent_name": "CraftBot"
},
"proactive": {
"enabled": false
@@ -10,10 +9,10 @@
"enabled": true
},
"model": {
- "llm_provider": "byteplus",
- "vlm_provider": "byteplus",
- "llm_model": "kimi-k2-250905",
- "vlm_model": "seed-1-6-250915"
+ "llm_provider": "gemini",
+ "vlm_provider": "gemini",
+ "llm_model": null,
+ "vlm_model": null
},
"api_keys": {
"openai": "",
diff --git a/app/state/state_manager.py b/app/state/state_manager.py
index f03a9502..e122a1c0 100644
--- a/app/state/state_manager.py
+++ b/app/state/state_manager.py
@@ -179,6 +179,8 @@ def reset(self) -> None:
STATE.agent_properties: AgentProperties = AgentProperties(
current_task_id="", action_count=0
)
+ # Reset main state to clear active_task_ids and task_summaries
+ self._main_state = MainState()
if self.event_stream_manager:
self.event_stream_manager.clear_all()
self.clean_state()
diff --git a/app/tui/interface.py b/app/tui/interface.py
index 4919257b..f25b85a1 100644
--- a/app/tui/interface.py
+++ b/app/tui/interface.py
@@ -45,6 +45,7 @@ def __init__(
enable_action_panel=True, # TUI has action panel
)
self._controller = UIController(agent, self._config)
+ agent.ui_controller = self._controller # Back-reference for event emission
# Create TUI adapter
self._adapter = TUIAdapter(self._controller)
diff --git a/app/ui_layer/adapters/base.py b/app/ui_layer/adapters/base.py
index 9d9965af..620d074e 100644
--- a/app/ui_layer/adapters/base.py
+++ b/app/ui_layer/adapters/base.py
@@ -233,6 +233,12 @@ def _subscribe_events(self) -> None:
self._unsubscribers.append(
bus.subscribe(UIEventType.GUI_MODE_CHANGED, self._handle_gui_mode_change)
)
+ self._unsubscribers.append(
+ bus.subscribe(UIEventType.WAITING_FOR_USER, self._handle_waiting_for_user)
+ )
+ self._unsubscribers.append(
+ bus.subscribe(UIEventType.TASK_UPDATE, self._handle_task_update)
+ )
# Footage events
self._unsubscribers.append(
@@ -386,6 +392,23 @@ def _handle_gui_mode_change(self, event: UIEvent) -> None:
if self.footage_component:
self.footage_component.set_visible(event.data.get("gui_mode", False))
+ def _handle_waiting_for_user(self, event: UIEvent) -> None:
+ """Handle waiting for user event - update task status to waiting."""
+ task_id = event.data.get("task_id", "")
+ if task_id and self.action_panel:
+ asyncio.create_task(
+ self.action_panel.update_item(task_id, "waiting")
+ )
+
+ def _handle_task_update(self, event: UIEvent) -> None:
+ """Handle task update event - update task status."""
+ task_id = event.data.get("task_id", "")
+ status = event.data.get("status", "running")
+ if task_id and self.action_panel:
+ asyncio.create_task(
+ self.action_panel.update_item(task_id, status)
+ )
+
def _handle_footage_update(self, event: UIEvent) -> None:
"""Handle footage update event."""
if self.footage_component:
diff --git a/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.module.css b/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.module.css
index 602c9128..634343e7 100644
--- a/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.module.css
+++ b/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.module.css
@@ -29,11 +29,14 @@
}
.pending,
-.waiting,
.idle {
color: var(--color-gray-500);
}
+.waiting {
+ color: #3b82f6;
+}
+
/* Spinning animation for loader icon */
.spinning {
animation: spin 1s linear infinite;
@@ -90,11 +93,14 @@
background: var(--color-error);
}
-.dot_pending,
-.dot_waiting {
+.dot_pending {
background: var(--color-gray-500);
}
+.dot_waiting {
+ background: #3b82f6;
+}
+
/* Pulse animation for active agent states */
.pulse {
animation: pulse 1.5s ease-in-out infinite;
diff --git a/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.tsx b/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.tsx
index db0d665a..27ca201e 100644
--- a/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.tsx
+++ b/app/ui_layer/browser/frontend/src/components/ui/StatusIndicator.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { CheckCircle, XCircle, Loader, Clock } from 'lucide-react'
+import { CheckCircle, XCircle, Loader, Clock, MessageCircle } from 'lucide-react'
import styles from './StatusIndicator.module.css'
import type { ActionStatus, AgentState } from '../../types'
@@ -55,8 +55,9 @@ export function StatusIndicator({
case 'thinking':
case 'working':
return
- case 'pending':
case 'waiting':
+ return
+ case 'pending':
case 'idle':
default:
return
diff --git a/app/ui_layer/browser/frontend/src/types/index.ts b/app/ui_layer/browser/frontend/src/types/index.ts
index 57cbeb9c..78e99123 100644
--- a/app/ui_layer/browser/frontend/src/types/index.ts
+++ b/app/ui_layer/browser/frontend/src/types/index.ts
@@ -25,7 +25,7 @@ export interface ChatMessage {
// Action/Task Types
// ─────────────────────────────────────────────────────────────────────
-export type ActionStatus = 'running' | 'completed' | 'error' | 'pending' | 'cancelled'
+export type ActionStatus = 'running' | 'completed' | 'error' | 'pending' | 'cancelled' | 'waiting'
export type ItemType = 'task' | 'action' | 'reasoning'
export interface ActionItem {
diff --git a/app/ui_layer/controller/ui_controller.py b/app/ui_layer/controller/ui_controller.py
index 2dc41508..09e4dc05 100644
--- a/app/ui_layer/controller/ui_controller.py
+++ b/app/ui_layer/controller/ui_controller.py
@@ -237,8 +237,9 @@ async def submit_message(self, message: str, adapter_id: str = "") -> None:
return
# Not a command - send to agent
- # Update state
- self._state_store.dispatch("SET_AGENT_STATE", AgentStateType.WORKING.value)
+ # Note: Task status updates (waiting -> running) are handled in _handle_chat_message
+ # after routing determines the correct session. We don't update here to avoid
+ # incorrectly changing status of unrelated tasks.
# Emit state change event so adapters can update status immediately
self._event_bus.emit(
@@ -408,6 +409,43 @@ def _update_state_from_event(self, event: UIEvent) -> None:
"SET_GUI_MODE", event.data.get("gui_mode", False)
)
+ elif event.type == UIEventType.WAITING_FOR_USER:
+ task_id = event.data.get("task_id", "")
+ if task_id:
+ # Update specific task status to "waiting"
+ self._state_store.dispatch(
+ "UPDATE_ACTION_ITEM",
+ {
+ "id": task_id,
+ "status": "waiting",
+ },
+ )
+ # Update global agent state
+ self._state_store.dispatch(
+ "SET_AGENT_STATE", AgentStateType.WAITING_FOR_USER.value
+ )
+ # Emit state change event for status bar
+ self._event_bus.emit(
+ UIEvent(
+ type=UIEventType.AGENT_STATE_CHANGED,
+ data={
+ "state": AgentStateType.WAITING_FOR_USER.value,
+ "status_message": "Waiting for your response",
+ },
+ )
+ )
+
+ elif event.type == UIEventType.TASK_UPDATE:
+ task_id = event.data.get("task_id", "")
+ if task_id:
+ self._state_store.dispatch(
+ "UPDATE_ACTION_ITEM",
+ {
+ "id": task_id,
+ "status": event.data.get("status", "running"),
+ },
+ )
+
async def _consume_triggers(self) -> None:
"""Consume triggers and run agent reactions."""
while self._running and self._agent.is_running:
diff --git a/app/ui_layer/events/event_types.py b/app/ui_layer/events/event_types.py
index 79e38ff5..7dbd9c25 100644
--- a/app/ui_layer/events/event_types.py
+++ b/app/ui_layer/events/event_types.py
@@ -30,6 +30,7 @@ class UIEventType(Enum):
# State events
AGENT_STATE_CHANGED = auto()
GUI_MODE_CHANGED = auto()
+ WAITING_FOR_USER = auto()
# Footage events (for GUI mode screenshots)
FOOTAGE_UPDATE = auto()
diff --git a/app/ui_layer/events/transformer.py b/app/ui_layer/events/transformer.py
index de555692..e1fd6706 100644
--- a/app/ui_layer/events/transformer.py
+++ b/app/ui_layer/events/transformer.py
@@ -31,6 +31,7 @@ class EventTransformer:
SYSTEM_KINDS = {"system", "system message"}
INFO_KINDS = {"info", "note"}
REASONING_KINDS = {"agent reasoning", "reasoning"}
+ WAITING_FOR_USER_KINDS = {"waiting_for_user"}
# Actions that should be hidden from the UI (for action_start/action_end events)
HIDDEN_ACTIONS = {
@@ -105,6 +106,10 @@ def transform(
message, event.message, timestamp, task_id
)
+ # Handle waiting_for_user events
+ if kind in cls.WAITING_FOR_USER_KINDS or "waiting_for_user" in kind:
+ return cls._create_waiting_for_user_event(message, timestamp, task_id)
+
if kind in cls.USER_MESSAGE_KINDS:
# Skip - user messages are emitted directly by UIController.submit_message()
# to avoid duplicate display
@@ -444,6 +449,24 @@ def _create_reasoning_event(
task_id=task_id,
)
+ @classmethod
+ def _create_waiting_for_user_event(
+ cls,
+ message: str,
+ timestamp: datetime,
+ task_id: Optional[str],
+ ) -> UIEvent:
+ """Create a waiting_for_user event."""
+ return UIEvent(
+ type=UIEventType.WAITING_FOR_USER,
+ data={
+ "task_id": task_id or "",
+ "message": message,
+ },
+ timestamp=timestamp,
+ task_id=task_id,
+ )
+
@classmethod
def _parse_timestamp(cls, iso_ts: Any) -> datetime:
"""Parse timestamp from various formats."""