Merge remote-tracking branch 'upstream/feat/open-hive' into feat/sub-agent-framework
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/adenhq/hive/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="Apache 2.0 License" /></a>
|
||||
<a href="https://github.com/aden-hive/hive/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="Apache 2.0 License" /></a>
|
||||
<a href="https://www.ycombinator.com/companies/aden"><img src="https://img.shields.io/badge/Y%20Combinator-Aden-orange" alt="Y Combinator" /></a>
|
||||
<a href="https://discord.com/invite/MXE49hrKDk"><img src="https://img.shields.io/discord/1172610340073242735?logo=discord&labelColor=%235462eb&logoColor=%23f5f5f5&color=%235462eb" alt="Discord" /></a>
|
||||
<a href="https://x.com/aden_hq"><img src="https://img.shields.io/twitter/follow/teamaden?logo=X&color=%23f5f5f5" alt="Twitter Follow" /></a>
|
||||
@@ -71,7 +71,7 @@ Use Hive when you need:
|
||||
|
||||
- **[Documentation](https://docs.adenhq.com/)** - Complete guides and API reference
|
||||
- **[Self-Hosting Guide](https://docs.adenhq.com/getting-started/quickstart)** - Deploy Hive on your infrastructure
|
||||
- **[Changelog](https://github.com/adenhq/hive/releases)** - Latest updates and releases
|
||||
- **[Changelog](https://github.com/aden-hive/hive/releases)** - Latest updates and releases
|
||||
- **[Roadmap](docs/roadmap.md)** - Upcoming features and plans
|
||||
- **[Report Issues](https://github.com/adenhq/hive/issues)** - Bug reports and feature requests
|
||||
- **[Contributing](CONTRIBUTING.md)** - How to contribute and submit PRs
|
||||
@@ -94,9 +94,10 @@ Use Hive when you need:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
git clone https://github.com/aden-hive/hive.git
|
||||
cd hive
|
||||
|
||||
|
||||
# Run quickstart setup
|
||||
./quickstart.sh
|
||||
```
|
||||
@@ -178,8 +179,7 @@ Skills and MCP servers are also available in [Antigravity IDE](https://antigravi
|
||||
|
||||
## Integration
|
||||
|
||||
<a href="https://github.com/adenhq/hive/tree/main/tools/src/aden_tools/tools"><img width="100%" alt="Integration" src="https://github.com/user-attachments/assets/a1573f93-cf02-4bb8-b3d5-b305b05b1e51" /></a>
|
||||
|
||||
<a href="https://github.com/aden-hive/hive/tree/main/tools/src/aden_tools/tools"><img width="100%" alt="Integration" src="https://github.com/user-attachments/assets/a1573f93-cf02-4bb8-b3d5-b305b05b1e51" /></a>
|
||||
Hive is built to be model-agnostic and system-agnostic.
|
||||
|
||||
- **LLM flexibility** - Hive Framework is designed to support various types of LLMs, including hosted and local models through LiteLLM-compatible providers.
|
||||
@@ -398,8 +398,7 @@ flowchart TB
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! We’re especially looking for help building tools, integrations, and example agents for the framework ([check #2805](https://github.com/adenhq/hive/issues/2805)). If you’re interested in extending its functionality, this is the perfect place to start. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
We welcome contributions from the community! We’re especially looking for help building tools, integrations, and example agents for the framework ([check #2805](https://github.com/aden-hive/hive/issues/2805)). If you’re interested in extending its functionality, this is the perfect place to start. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
**Important:** Please get assigned to an issue before submitting a PR. Comment on an issue to claim it, and a maintainer will assign you. Issues with reproducible steps and proposals are prioritized. This helps prevent duplicate work.
|
||||
|
||||
|
||||
@@ -395,7 +395,10 @@ def validate_agent_credentials(
|
||||
spec = CREDENTIAL_SPECS[cn]
|
||||
affected = sorted(t for t in required_tools if t in spec.tools)
|
||||
_check_credential(
|
||||
spec, cn, affected_tools=affected, affected_node_types=[],
|
||||
spec,
|
||||
cn,
|
||||
affected_tools=affected,
|
||||
affected_node_types=[],
|
||||
alternative_group=group_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -570,8 +570,7 @@ class ExecutionStream:
|
||||
if not _is_shared_session:
|
||||
await self._write_session_state(execution_id, ctx, result=result)
|
||||
|
||||
# Emit completion/failure event
|
||||
# (skip for pauses — executor already emitted execution_paused)
|
||||
# Emit completion/failure/pause event
|
||||
if self._scoped_event_bus:
|
||||
if result.success:
|
||||
await self._scoped_event_bus.emit_execution_completed(
|
||||
@@ -580,7 +579,17 @@ class ExecutionStream:
|
||||
output=result.output,
|
||||
correlation_id=ctx.correlation_id,
|
||||
)
|
||||
elif not result.paused_at:
|
||||
elif result.paused_at:
|
||||
# The executor returns paused_at on CancelledError but
|
||||
# does NOT emit execution_paused itself — we must emit
|
||||
# it here so the frontend can transition out of "running".
|
||||
await self._scoped_event_bus.emit_execution_paused(
|
||||
stream_id=self.stream_id,
|
||||
node_id=result.paused_at,
|
||||
reason=result.error or "Execution paused",
|
||||
execution_id=execution_id,
|
||||
)
|
||||
else:
|
||||
await self._scoped_event_bus.emit_execution_failed(
|
||||
stream_id=self.stream_id,
|
||||
execution_id=execution_id,
|
||||
@@ -629,6 +638,25 @@ class ExecutionStream:
|
||||
execution_id, ctx, error="Execution cancelled"
|
||||
)
|
||||
|
||||
# Emit SSE event so the frontend knows the execution stopped.
|
||||
# The executor does NOT emit on CancelledError, so there is no
|
||||
# risk of double-emitting.
|
||||
if self._scoped_event_bus:
|
||||
if has_result and result.paused_at:
|
||||
await self._scoped_event_bus.emit_execution_paused(
|
||||
stream_id=self.stream_id,
|
||||
node_id=result.paused_at,
|
||||
reason="Execution cancelled",
|
||||
execution_id=execution_id,
|
||||
)
|
||||
else:
|
||||
await self._scoped_event_bus.emit_execution_failed(
|
||||
stream_id=self.stream_id,
|
||||
execution_id=execution_id,
|
||||
error="Execution cancelled",
|
||||
correlation_id=ctx.correlation_id,
|
||||
)
|
||||
|
||||
# Don't re-raise - we've handled it and saved state
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -154,7 +154,9 @@ async def handle_check_agent(request: web.Request) -> web.Response:
|
||||
ensure_credential_key_env()
|
||||
|
||||
nodes = load_agent_nodes(agent_path)
|
||||
result = validate_agent_credentials(nodes, verify=verify, raise_on_error=False, force_refresh=True)
|
||||
result = validate_agent_credentials(
|
||||
nodes, verify=verify, raise_on_error=False, force_refresh=True
|
||||
)
|
||||
|
||||
# If any credential needs Aden, include ADEN_API_KEY as a first-class row
|
||||
if any(c.aden_supported for c in result.credentials):
|
||||
|
||||
@@ -113,27 +113,43 @@ async def handle_events(request: web.Request) -> web.StreamResponse:
|
||||
|
||||
sse = SSEResponse()
|
||||
await sse.prepare(request)
|
||||
logger.info(
|
||||
"SSE connected: session='%s', sub_id='%s', types=%d", session.id, sub_id, len(event_types)
|
||||
)
|
||||
|
||||
event_count = 0
|
||||
close_reason = "unknown"
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
data = await asyncio.wait_for(queue.get(), timeout=KEEPALIVE_INTERVAL)
|
||||
await sse.send_event(data)
|
||||
event_count += 1
|
||||
if event_count == 1:
|
||||
logger.info(
|
||||
"SSE first event: session='%s', type='%s'", session.id, data.get("type")
|
||||
)
|
||||
except TimeoutError:
|
||||
await sse.send_keepalive()
|
||||
except (ConnectionResetError, ConnectionError):
|
||||
close_reason = "client_disconnected"
|
||||
break
|
||||
except Exception as exc:
|
||||
logger.debug("SSE stream closed: %s", exc)
|
||||
close_reason = f"error: {exc}"
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
close_reason = "cancelled"
|
||||
finally:
|
||||
try:
|
||||
event_bus.unsubscribe(sub_id)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("SSE client disconnected from session '%s'", session.id)
|
||||
logger.info(
|
||||
"SSE disconnected: session='%s', events_sent=%d, reason='%s'",
|
||||
session.id,
|
||||
event_count,
|
||||
close_reason,
|
||||
)
|
||||
|
||||
return sse.response
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useState, useRef, useEffect } from "react";
|
||||
import { Send, Square, Crown, Cpu } from "lucide-react";
|
||||
import { Send, Square, Crown, Cpu, Check, ChevronRight, Loader2 } from "lucide-react";
|
||||
import { formatAgentDisplayName } from "@/lib/chat-helpers";
|
||||
import MarkdownContent from "@/components/MarkdownContent";
|
||||
|
||||
@@ -35,6 +35,75 @@ function getColor(_agent: string, role?: "queen" | "worker"): string {
|
||||
return "hsl(220,60%,55%)";
|
||||
}
|
||||
|
||||
function ToolActivityRow({ content }: { content: string }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
let tools: { name: string; done: boolean }[] = [];
|
||||
let allDone = false;
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
tools = parsed.tools || [];
|
||||
allDone = parsed.allDone ?? false;
|
||||
} catch {
|
||||
// Legacy plain-text fallback
|
||||
return (
|
||||
<div className="flex gap-3 pl-10">
|
||||
<span className="text-[11px] text-muted-foreground bg-muted/40 px-3 py-1 rounded-full border border-border/40">
|
||||
{content}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tools.length === 0) return null;
|
||||
|
||||
const total = tools.length;
|
||||
|
||||
if (allDone && !expanded) {
|
||||
return (
|
||||
<div className="flex gap-3 pl-10">
|
||||
<button
|
||||
onClick={() => setExpanded(true)}
|
||||
className="flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
<Check className="w-3 h-3 text-emerald-500" />
|
||||
<span>{total} tool{total === 1 ? "" : "s"} used</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 pl-10">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{allDone && (
|
||||
<button onClick={() => setExpanded(false)} className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<ChevronRight className="w-3 h-3 rotate-90" />
|
||||
</button>
|
||||
)}
|
||||
{tools.map((t, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full border ${
|
||||
t.done
|
||||
? "text-emerald-600 bg-emerald-500/10 border-emerald-500/20"
|
||||
: "text-muted-foreground bg-muted/40 border-border/40"
|
||||
}`}
|
||||
>
|
||||
{t.done ? (
|
||||
<Check className="w-2.5 h-2.5" />
|
||||
) : (
|
||||
<Loader2 className="w-2.5 h-2.5 animate-spin" />
|
||||
)}
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MessageBubble = memo(function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
const isUser = msg.type === "user";
|
||||
const isQueen = msg.role === "queen";
|
||||
@@ -51,13 +120,7 @@ const MessageBubble = memo(function MessageBubble({ msg }: { msg: ChatMessage })
|
||||
}
|
||||
|
||||
if (msg.type === "tool_status") {
|
||||
return (
|
||||
<div className="flex gap-3 pl-10">
|
||||
<span className="text-[11px] text-muted-foreground bg-muted/40 px-3 py-1 rounded-full border border-border/40">
|
||||
{msg.content}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return <ToolActivityRow content={msg.content} />;
|
||||
}
|
||||
|
||||
if (isUser) {
|
||||
@@ -120,7 +183,6 @@ export default function ChatPanel({ messages, onSend, isWaiting, activeThread, a
|
||||
if (m.type === "system" && !m.thread) return false;
|
||||
return m.thread === activeThread;
|
||||
});
|
||||
console.log('[ChatPanel] render: messages:', messages.length, 'threadMessages:', threadMessages.length, 'activeThread:', activeThread, 'threads:', [...new Set(messages.map(m => m.thread))]);
|
||||
|
||||
// Mark current thread as read
|
||||
useEffect(() => {
|
||||
|
||||
@@ -80,28 +80,39 @@ export function useMultiSSE({ sessions, onEvent }: UseMultiSSEOptions) {
|
||||
const onEventRef = useRef(onEvent);
|
||||
onEventRef.current = onEvent;
|
||||
|
||||
const sourcesRef = useRef(new Map<string, EventSource>());
|
||||
// Track both the EventSource and its session ID so we can detect session changes
|
||||
const sourcesRef = useRef(new Map<string, { es: EventSource; sessionId: string }>());
|
||||
|
||||
// Diff-based open/close — runs on every `sessions` change
|
||||
useEffect(() => {
|
||||
const current = sourcesRef.current;
|
||||
const desired = new Set(Object.keys(sessions));
|
||||
|
||||
// Close connections for sessions no longer in the map
|
||||
for (const [agentType, es] of current) {
|
||||
if (!desired.has(agentType)) {
|
||||
es.close();
|
||||
// Close connections for removed agents OR changed session IDs
|
||||
for (const [agentType, entry] of current) {
|
||||
if (!desired.has(agentType) || sessions[agentType] !== entry.sessionId) {
|
||||
console.log('[SSE] closing:', agentType, entry.sessionId, desired.has(agentType) ? '(session changed)' : '(removed)');
|
||||
entry.es.close();
|
||||
current.delete(agentType);
|
||||
}
|
||||
}
|
||||
|
||||
// Open connections for newly added sessions
|
||||
// Open connections for new/changed sessions
|
||||
for (const [agentType, sessionId] of Object.entries(sessions)) {
|
||||
if (!sessionId || current.has(agentType)) continue;
|
||||
|
||||
const url = `/api/sessions/${sessionId}/events`;
|
||||
console.log('[SSE] opening:', agentType, sessionId);
|
||||
const es = new EventSource(url);
|
||||
|
||||
es.onopen = () => {
|
||||
console.log('[SSE] connected:', agentType, sessionId);
|
||||
};
|
||||
|
||||
es.onerror = () => {
|
||||
console.error('[SSE] error:', agentType, sessionId, 'readyState:', es.readyState);
|
||||
};
|
||||
|
||||
es.onmessage = (e: MessageEvent) => {
|
||||
try {
|
||||
const event: AgentEvent = JSON.parse(e.data);
|
||||
@@ -112,14 +123,14 @@ export function useMultiSSE({ sessions, onEvent }: UseMultiSSEOptions) {
|
||||
}
|
||||
};
|
||||
|
||||
current.set(agentType, es);
|
||||
current.set(agentType, { es, sessionId });
|
||||
}
|
||||
}, [sessions]);
|
||||
|
||||
// Close all on unmount only
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
for (const es of sourcesRef.current.values()) es.close();
|
||||
for (const entry of sourcesRef.current.values()) entry.es.close();
|
||||
sourcesRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -286,7 +286,7 @@ interface AgentBackendState {
|
||||
isTyping: boolean;
|
||||
isStreaming: boolean;
|
||||
llmSnapshots: Record<string, string>;
|
||||
toolCallCounts: Record<string, number>;
|
||||
activeToolCalls: Record<string, { name: string; done: boolean; streamId: string }>;
|
||||
}
|
||||
|
||||
function defaultAgentState(): AgentBackendState {
|
||||
@@ -307,7 +307,7 @@ function defaultAgentState(): AgentBackendState {
|
||||
isTyping: false,
|
||||
isStreaming: false,
|
||||
llmSnapshots: {},
|
||||
toolCallCounts: {},
|
||||
activeToolCalls: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -964,7 +964,7 @@ export default function Workspace() {
|
||||
currentExecutionId: event.execution_id || agentStates[agentType]?.currentExecutionId || null,
|
||||
nodeLogs: {},
|
||||
llmSnapshots: {},
|
||||
toolCallCounts: {},
|
||||
activeToolCalls: {},
|
||||
});
|
||||
markAllNodesAs(agentType, ["running", "looping", "complete", "error"], "pending");
|
||||
}
|
||||
@@ -1054,7 +1054,7 @@ export default function Workspace() {
|
||||
|
||||
case "node_loop_started":
|
||||
turnCounterRef.current[agentType] = currentTurn + 1;
|
||||
updateAgentState(agentType, { isTyping: true });
|
||||
updateAgentState(agentType, { isTyping: true, activeToolCalls: {} });
|
||||
if (!isQueen && event.node_id) {
|
||||
const sessions = sessionsRef.current[agentType] || [];
|
||||
const activeId = activeSessionRef.current[agentType] || sessions[0]?.id;
|
||||
@@ -1070,7 +1070,7 @@ export default function Workspace() {
|
||||
|
||||
case "node_loop_iteration":
|
||||
turnCounterRef.current[agentType] = currentTurn + 1;
|
||||
updateAgentState(agentType, { isStreaming: false });
|
||||
updateAgentState(agentType, { isStreaming: false, activeToolCalls: {} });
|
||||
if (!isQueen && event.node_id) {
|
||||
const pendingText = agentStates[agentType]?.llmSnapshots[event.node_id];
|
||||
if (pendingText?.trim()) {
|
||||
@@ -1117,58 +1117,58 @@ export default function Workspace() {
|
||||
|
||||
case "tool_call_started": {
|
||||
console.log('[TOOL_PILL] tool_call_started received:', { isQueen, nodeId: event.node_id, streamId: event.stream_id, agentType, executionId: event.execution_id, toolName: event.data?.tool_name });
|
||||
if (!isQueen && event.node_id) {
|
||||
const pendingText = agentStates[agentType]?.llmSnapshots[event.node_id];
|
||||
if (pendingText?.trim()) {
|
||||
appendNodeLog(agentType, event.node_id, `${ts} INFO LLM: ${truncate(pendingText.trim(), 300)}`);
|
||||
setAgentStates(prev => {
|
||||
const state = prev[agentType];
|
||||
if (!state) return prev;
|
||||
const { [event.node_id!]: _, ...rest } = state.llmSnapshots;
|
||||
return { ...prev, [agentType]: { ...state, llmSnapshots: rest } };
|
||||
});
|
||||
if (event.node_id) {
|
||||
if (!isQueen) {
|
||||
const pendingText = agentStates[agentType]?.llmSnapshots[event.node_id];
|
||||
if (pendingText?.trim()) {
|
||||
appendNodeLog(agentType, event.node_id, `${ts} INFO LLM: ${truncate(pendingText.trim(), 300)}`);
|
||||
setAgentStates(prev => {
|
||||
const state = prev[agentType];
|
||||
if (!state) return prev;
|
||||
const { [event.node_id!]: _, ...rest } = state.llmSnapshots;
|
||||
return { ...prev, [agentType]: { ...state, llmSnapshots: rest } };
|
||||
});
|
||||
}
|
||||
appendNodeLog(agentType, event.node_id, `${ts} INFO Calling ${(event.data?.tool_name as string) || "unknown"}(${event.data?.tool_input ? truncate(JSON.stringify(event.data.tool_input), 200) : ""})`);
|
||||
}
|
||||
const toolName = (event.data?.tool_name as string) || "unknown";
|
||||
const toolInput = event.data?.tool_input;
|
||||
const argsStr = toolInput ? truncate(JSON.stringify(toolInput), 200) : "";
|
||||
appendNodeLog(agentType, event.node_id, `${ts} INFO Calling ${toolName}(${argsStr})`);
|
||||
|
||||
// Update tool call counts and upsert compact pill into chat
|
||||
let pillContent = "";
|
||||
const toolName = (event.data?.tool_name as string) || "unknown";
|
||||
const toolUseId = (event.data?.tool_use_id as string) || "";
|
||||
|
||||
// Track active (in-flight) tools and upsert activity row into chat
|
||||
const sid = event.stream_id;
|
||||
setAgentStates(prev => {
|
||||
const state = prev[agentType];
|
||||
if (!state) return prev;
|
||||
const newCounts = { ...state.toolCallCounts, [toolName]: (state.toolCallCounts[toolName] || 0) + 1 };
|
||||
pillContent = Object.entries(newCounts).map(([n, c]) => `${c} ${n}`).join(", ");
|
||||
return {
|
||||
...prev,
|
||||
[agentType]: { ...state, isStreaming: false, toolCallCounts: newCounts },
|
||||
};
|
||||
});
|
||||
console.log('[TOOL_PILL] pillContent:', pillContent, 'agentType:', agentType);
|
||||
if (pillContent) {
|
||||
const pillMsg: ChatMessage = {
|
||||
id: `tool-pill-${event.execution_id || "exec"}`,
|
||||
const newActive = { ...state.activeToolCalls, [toolUseId]: { name: toolName, done: false, streamId: sid } };
|
||||
// Only include tools from this stream in the pill
|
||||
const tools = Object.values(newActive).filter(t => t.streamId === sid).map(t => ({ name: t.name, done: t.done }));
|
||||
const allDone = tools.length > 0 && tools.every(t => t.done);
|
||||
upsertChatMessage(agentType, {
|
||||
id: `tool-pill-${sid}-${event.execution_id || "exec"}-${currentTurn}`,
|
||||
agent: agentDisplayName || event.node_id || "Agent",
|
||||
agentColor: "",
|
||||
content: pillContent,
|
||||
content: JSON.stringify({ tools, allDone }),
|
||||
timestamp: "",
|
||||
type: "tool_status",
|
||||
role: "worker",
|
||||
role,
|
||||
thread: agentType,
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
[agentType]: { ...state, isStreaming: false, activeToolCalls: newActive },
|
||||
};
|
||||
console.log('[TOOL_PILL] upserting:', pillMsg);
|
||||
upsertChatMessage(agentType, pillMsg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[TOOL_PILL] SKIPPED: isQueen=', isQueen, 'node_id=', event.node_id);
|
||||
console.log('[TOOL_PILL] SKIPPED: no node_id', event.node_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_call_completed":
|
||||
if (!isQueen && event.node_id) {
|
||||
case "tool_call_completed": {
|
||||
if (event.node_id) {
|
||||
const toolName = (event.data?.tool_name as string) || "unknown";
|
||||
const toolUseId = (event.data?.tool_use_id as string) || "";
|
||||
const isError = event.data?.is_error as boolean | undefined;
|
||||
const result = event.data?.result as string | undefined;
|
||||
if (isError) {
|
||||
@@ -1177,8 +1177,36 @@ export default function Workspace() {
|
||||
const resultStr = result ? ` (${truncate(result, 200)})` : "";
|
||||
appendNodeLog(agentType, event.node_id, `${ts} INFO ${toolName} done${resultStr}`);
|
||||
}
|
||||
|
||||
// Mark tool as done and update activity row
|
||||
const sid = event.stream_id;
|
||||
setAgentStates(prev => {
|
||||
const state = prev[agentType];
|
||||
if (!state) return prev;
|
||||
const updated = { ...state.activeToolCalls };
|
||||
if (updated[toolUseId]) {
|
||||
updated[toolUseId] = { ...updated[toolUseId], done: true };
|
||||
}
|
||||
const tools = Object.values(updated).filter(t => t.streamId === sid).map(t => ({ name: t.name, done: t.done }));
|
||||
const allDone = tools.length > 0 && tools.every(t => t.done);
|
||||
upsertChatMessage(agentType, {
|
||||
id: `tool-pill-${sid}-${event.execution_id || "exec"}-${currentTurn}`,
|
||||
agent: agentDisplayName || event.node_id || "Agent",
|
||||
agentColor: "",
|
||||
content: JSON.stringify({ tools, allDone }),
|
||||
timestamp: "",
|
||||
type: "tool_status",
|
||||
role,
|
||||
thread: agentType,
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
[agentType]: { ...state, activeToolCalls: updated },
|
||||
};
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "node_internal_output":
|
||||
if (!isQueen && event.node_id) {
|
||||
|
||||
Reference in New Issue
Block a user