Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c0faaaf
Integrate Embedded Process Groups with Expand/Collapse Support
Dec 7, 2025
0ce48c8
fix
Dec 7, 2025
72439d5
Add timestamps and per-step execution time
Dec 10, 2025
37af2c6
Refactor A1/A2… as Internal Tools Within A0
Dec 12, 2025
85cc64d
user message starts a new group (each user message should be separate)
keyboardstaff Dec 13, 2025
8181a82
remove bg-colors from messages; logItem updates
3clyp50 Dec 15, 2025
e275cab
status icons and formatting
3clyp50 Dec 30, 2025
ced8f87
agent no. from backend
3clyp50 Dec 30, 2025
8d94e67
revert token counting logic
3clyp50 Jan 2, 2026
b0d83a4
process groups and steps css polish
3clyp50 Jan 2, 2026
5844e58
fix: take durationms from backend
3clyp50 Jan 2, 2026
3705096
showThoughts hide/show GEN steps
3clyp50 Jan 2, 2026
3e31dcb
tool specific step badges
3clyp50 Jan 3, 2026
74595c6
streamline status badges; polish chat-history css
3clyp50 Jan 4, 2026
6ed9f06
fix: backend not setting agent no; agent no in responses
3clyp50 Jan 6, 2026
98b0ae8
user, warning msg css
3clyp50 Jan 6, 2026
9295cd3
subordinates agents nesting in process groups
3clyp50 Jan 6, 2026
6530a08
fix user-message position
3clyp50 Jan 6, 2026
9dea6ef
rm showThoughts and showJSON
3clyp50 Jan 6, 2026
ea078af
fix: browser screenshot flashing
3clyp50 Jan 6, 2026
7dcfd6a
timestamp log fix
3clyp50 Jan 6, 2026
6a21f34
restore heading logic for rate limit, error msgs
3clyp50 Jan 6, 2026
372d2bf
autoexpand groups on warnings
3clyp50 Jan 6, 2026
a96c035
chore: cleanup comments
3clyp50 Jan 6, 2026
6d41d19
restore log guid
3clyp50 Jan 6, 2026
e042f5e
fix: browser holding timestamp
3clyp50 Jan 6, 2026
98cae36
skip native reasoning
3clyp50 Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion python/extensions/agent_init/_10_initial_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ async def execute(self, **kwargs):
# Add to log (green bubble) for immediate UI display
self.agent.context.log.log(
type="response",
heading=f"{self.agent.agent_name}: Welcome",
content=initial_message_text,
finished=True,
update_progress="none",
Expand Down
6 changes: 4 additions & 2 deletions python/extensions/before_main_llm_call/_10_log_for_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ async def execute(self, loop_data: LoopData = LoopData(), text: str = "", **kwar
)
)

def build_heading(agent, text: str):
return f"icon://network_intelligence {agent.agent_name}: {text}"
def build_heading(agent, text: str, icon: str = "network_intelligence"):
# Include agent identifier for all agents (A0:, A1:, A2:, etc.)
agent_prefix = f"{agent.agent_name}: "
return f"icon://{icon} {agent_prefix}{text}"

def build_default_heading(agent):
return build_heading(agent, "Generating...")
7 changes: 2 additions & 5 deletions python/extensions/response_stream/_10_log_from_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@ async def execute(
if "headline" in parsed:
heading = build_heading(self.agent, parsed['headline'])
elif "tool_name" in parsed:
heading = build_heading(self.agent, f"Using tool {parsed['tool_name']}") # if the llm skipped headline
heading = build_heading(self.agent, f"Using {parsed['tool_name']}") # if the llm skipped headline
elif "thoughts" in parsed:
# thought length indicator
thoughts = "\n".join(parsed["thoughts"])
pipes = "|" * math.ceil(math.sqrt(len(thoughts)))
heading = build_heading(self.agent, f"Thinking... {pipes}")
heading = build_default_heading(self.agent)

# create log message and store it in loop data temporary params
if "log_item_generating" not in loop_data.params_temporary:
Expand Down
19 changes: 19 additions & 0 deletions python/helpers/log.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass, field
import json
import time
from typing import Any, Literal, Optional, Dict, TypeVar, TYPE_CHECKING

T = TypeVar("T")
Expand Down Expand Up @@ -131,9 +132,13 @@ class LogItem:
kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps
id: Optional[str] = None # Add id field
guid: str = ""
timestamp: float = 0.0
duration_ms: Optional[int] = None
agent_number: int = 0

def __post_init__(self):
self.guid = self.log.guid
self.timestamp = self.timestamp or time.time()

def update(
self,
Expand Down Expand Up @@ -181,6 +186,9 @@ def output(self):
"content": self.content,
"temp": self.temp,
"kvps": self.kvps,
"timestamp": self.timestamp,
"duration_ms": self.duration_ms,
"agent_number": self.agent_number,
}


Expand All @@ -206,11 +214,22 @@ def log(
) -> LogItem:

# add a minimal item to the log
# Determine agent number from streaming agent
agent_number = 0
if self.context and self.context.streaming_agent:
agent_number = self.context.streaming_agent.number

item = LogItem(
log=self,
no=len(self.logs),
type=type,
agent_number=agent_number,
)
# Set duration on previous item and mark it as updated
if self.logs:
prev = self.logs[-1]
prev.duration_ms = int((item.timestamp - prev.timestamp) * 1000)
self.updates += [prev.no]
self.logs.append(item)

# and update it (to have just one implementation)
Expand Down
24 changes: 13 additions & 11 deletions python/helpers/persist_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,19 @@ def _deserialize_log(data: dict[str, Any]) -> "Log":
# Deserialize the list of LogItem objects
i = 0
for item_data in data.get("logs", []):
log.logs.append(
LogItem(
log=log, # restore the log reference
no=i, # item_data["no"],
type=item_data["type"],
heading=item_data.get("heading", ""),
content=item_data.get("content", ""),
kvps=OrderedDict(item_data["kvps"]) if item_data["kvps"] else None,
temp=item_data.get("temp", False),
)
)
log.logs.append(LogItem(
log=log, # restore the log reference
no=i, # item_data["no"],
type=item_data["type"],
heading=item_data.get("heading", ""),
content=item_data.get("content", ""),
kvps=OrderedDict(item_data["kvps"]) if item_data["kvps"] else None,
temp=item_data.get("temp", False),
# Pass metrics directly to constructor
timestamp=item_data.get("timestamp", 0.0),
duration_ms=item_data.get("duration_ms"),
agent_number=item_data.get("agent_number", 0),
))
log.updates.append(i)
i += 1

Expand Down
181 changes: 181 additions & 0 deletions webui/components/messages/process-group/process-group-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { createStore } from "/js/AlpineStore.js";

// Process Group Store - manages collapsible process groups in chat

// Unified mapping for both Tool Names and Step Types
// Specific tool names (keys) take precedence over generic types
const DISPLAY_CODES = {
// --- Specific Tools ---
'call_subordinate': 'SUB',
'search_engine': 'WEB',
'a2a_chat': 'A2A',
'behaviour_adjustment': 'ADJ',
'document_query': 'DOC',
'vision_load': 'EYE',
'notify_user': 'NTF',
'scheduler': 'SCH',
'unknown': 'UNK',
// Memory operations group
'memory_save': 'MEM',
'memory_load': 'MEM',
'memory_forget': 'MEM',
'memory_delete': 'MEM',

// --- Step Types ---
'agent': 'GEN',
'response': 'END',
'tool': 'USE', // Generic fallback for tools
'code_exe': 'EXE',
'browser': 'BRW',
'progress': 'HLD',
'subagent': 'SUB', // Type fallback if tool name missing
'mcp': 'MCP',
'info': 'INF',
'hint': 'HNT',
'warning': 'WRN',
'error': 'ERR',
'util': 'UTL',
'done': 'END'
};

const model = {
// Track which process groups are expanded (by group ID)
expandedGroups: {},

// Track which individual steps are expanded within a group
expandedSteps: {},

// Default collapsed state for new process groups
defaultCollapsed: true,

init() {
try {
// Load persisted state
const stored = localStorage.getItem("processGroupState");
if (stored) {
const parsed = JSON.parse(stored);
this.expandedGroups = parsed.expandedGroups || {};
this.expandedSteps = parsed.expandedSteps || {};
this.defaultCollapsed = parsed.defaultCollapsed ?? true;
}
} catch (e) {
console.error("Failed to load process group state", e);
}
},

_persist() {
try {
localStorage.setItem("processGroupState", JSON.stringify({
expandedGroups: this.expandedGroups,
expandedSteps: this.expandedSteps,
defaultCollapsed: this.defaultCollapsed
}));
} catch (e) {
console.error("Failed to persist process group state", e);
}
},

// Check if a process group is expanded
isGroupExpanded(groupId) {
if (groupId in this.expandedGroups) {
return this.expandedGroups[groupId];
}
return !this.defaultCollapsed;
},

// Toggle process group expansion
toggleGroup(groupId) {
const current = this.isGroupExpanded(groupId);
this.expandedGroups[groupId] = !current;
this._persist();
},

// Expand a specific group
expandGroup(groupId) {
this.expandedGroups[groupId] = true;
this._persist();
},

// Collapse a specific group
collapseGroup(groupId) {
this.expandedGroups[groupId] = false;
this._persist();
},

// Check if a step within a group is expanded
isStepExpanded(groupId, stepId) {
const key = `${groupId}:${stepId}`;
return this.expandedSteps[key] || false;
},

// Toggle step expansion
toggleStep(groupId, stepId) {
const key = `${groupId}:${stepId}`;
const currentState = this.expandedSteps[key] || false;
this.expandedSteps[key] = !currentState;
this._persist();
},

// Status code (3-4 letter) for backend log types
// Looks up tool name first (specific), then falls back to type (generic)
getStepCode(type, toolName = null) {
// Specific tool codes only apply to generic 'tool' steps
if (type === 'tool' && toolName && DISPLAY_CODES[toolName]) {
return DISPLAY_CODES[toolName];
}

return DISPLAY_CODES[type] ||
type?.toUpperCase()?.slice(0, 4) ||
'GEN';
},

// CSS color class for backend log types
// Looks up tool name first (specific), then falls back to type (generic)
getStatusColorClass(type, toolName = null) {
// Specific tool name mappings for 'tool' steps
if (type === 'tool' && toolName) {
// call_subordinate gets teal (SUB color)
if (toolName === 'call_subordinate') {
return 'status-sub';
}
// Add other specific tool mappings here if needed in the future
}

const colors = {
'agent': 'status-gen',
'response': 'status-end',
'tool': 'status-tool',
'mcp': 'status-mcp',
'subagent': 'status-sub',
'code_exe': 'status-exe',
'browser': 'status-brw',
'progress': 'status-wait',
'info': 'status-inf',
'hint': 'status-hnt',
'warning': 'status-wrn',
'error': 'status-err',
'util': 'status-utl',
'done': 'status-end'
};
return colors[type] || 'status-gen';
},

// Clear state for a specific context (when chat is reset)
clearContext(contextPrefix) {
// Clear groups matching the context
for (const key of Object.keys(this.expandedGroups)) {
if (key.startsWith(contextPrefix)) {
delete this.expandedGroups[key];
}
}
// Clear steps matching the context
for (const key of Object.keys(this.expandedSteps)) {
if (key.startsWith(contextPrefix)) {
delete this.expandedSteps[key];
}
}
this._persist();
}
};

export const store = createStore("processGroup", model);
Loading