Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit Β·
ae24efc
1
Parent(s): d644dff
feat: resolve sandbox file paths and show tool description in status
Browse files- Extract resolve_sandbox_script utility in sandbox_tool.py to detect
file paths and read content from sandbox via cat
- Resolve script paths before sending approval_required event so the
frontend receives actual file content for display and editing
- Show tool description arg in status chip and activity bar instead of
generic "running" / "Running bash" text
agent/core/agent_loop.py
CHANGED
|
@@ -491,6 +491,16 @@ class Handlers:
|
|
| 491 |
tool_args = json.loads(tc.function.arguments)
|
| 492 |
except (json.JSONDecodeError, TypeError):
|
| 493 |
tool_args = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
tools_data.append(
|
| 495 |
{
|
| 496 |
"tool": tool_name,
|
|
|
|
| 491 |
tool_args = json.loads(tc.function.arguments)
|
| 492 |
except (json.JSONDecodeError, TypeError):
|
| 493 |
tool_args = {}
|
| 494 |
+
|
| 495 |
+
# Resolve sandbox file paths for hf_jobs scripts so the
|
| 496 |
+
# frontend can display & edit the actual file content.
|
| 497 |
+
if tool_name == "hf_jobs" and isinstance(tool_args.get("script"), str):
|
| 498 |
+
from agent.tools.sandbox_tool import resolve_sandbox_script
|
| 499 |
+
sandbox = getattr(session, "sandbox", None)
|
| 500 |
+
content, _ = await resolve_sandbox_script(sandbox, tool_args["script"])
|
| 501 |
+
if content:
|
| 502 |
+
tool_args = {**tool_args, "script": content}
|
| 503 |
+
|
| 504 |
tools_data.append(
|
| 505 |
{
|
| 506 |
"tool": tool_name,
|
agent/tools/sandbox_tool.py
CHANGED
|
@@ -12,6 +12,7 @@ a cpu-basic sandbox is auto-created (no approval needed).
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
import asyncio
|
|
|
|
| 15 |
from typing import Any
|
| 16 |
|
| 17 |
from huggingface_hub import HfApi, SpaceHardware
|
|
@@ -19,6 +20,37 @@ from huggingface_hub import HfApi, SpaceHardware
|
|
| 19 |
from agent.core.session import Event
|
| 20 |
from agent.tools.sandbox_client import Sandbox
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# ββ Tool name mapping (short agent names β Sandbox client names) ββββββ
|
| 23 |
|
| 24 |
|
|
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
import asyncio
|
| 15 |
+
import shlex
|
| 16 |
from typing import Any
|
| 17 |
|
| 18 |
from huggingface_hub import HfApi, SpaceHardware
|
|
|
|
| 20 |
from agent.core.session import Event
|
| 21 |
from agent.tools.sandbox_client import Sandbox
|
| 22 |
|
| 23 |
+
|
| 24 |
+
def _looks_like_path(script: str) -> bool:
|
| 25 |
+
"""Return True if the script string looks like a file path (not inline code)."""
|
| 26 |
+
return (
|
| 27 |
+
isinstance(script, str)
|
| 28 |
+
and script.strip() == script
|
| 29 |
+
and not any(c in script for c in "\r\n\0")
|
| 30 |
+
and (script.startswith("/") or script.startswith("./") or script.startswith("../"))
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
async def resolve_sandbox_script(sandbox: Any, script: str) -> tuple[str | None, str | None]:
|
| 35 |
+
"""Read a file from the sandbox if *script* looks like a path.
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
(content, error) β content is the file text on success,
|
| 39 |
+
error is a message on failure. Both None means *script*
|
| 40 |
+
is not a path (caller should use it as-is).
|
| 41 |
+
"""
|
| 42 |
+
if not sandbox or not _looks_like_path(script):
|
| 43 |
+
return None, None
|
| 44 |
+
try:
|
| 45 |
+
result = await asyncio.to_thread(
|
| 46 |
+
sandbox.bash, f"cat {shlex.quote(script)}"
|
| 47 |
+
)
|
| 48 |
+
if result.success and result.output:
|
| 49 |
+
return result.output, None
|
| 50 |
+
return None, f"Failed to read {script} from sandbox: {result.error}"
|
| 51 |
+
except Exception as e:
|
| 52 |
+
return None, f"Failed to read {script} from sandbox: {e}"
|
| 53 |
+
|
| 54 |
# ββ Tool name mapping (short agent names β Sandbox client names) ββββββ
|
| 55 |
|
| 56 |
|
frontend/src/components/Chat/ActivityStatusBar.tsx
CHANGED
|
@@ -21,7 +21,7 @@ function statusLabel(status: ActivityStatus): string {
|
|
| 21 |
switch (status.type) {
|
| 22 |
case 'thinking': return 'Thinking';
|
| 23 |
case 'streaming': return 'Writing';
|
| 24 |
-
case 'tool': return TOOL_LABELS[status.toolName] || `Running ${status.toolName}`;
|
| 25 |
case 'waiting-approval': return 'Waiting for approval';
|
| 26 |
default: return '';
|
| 27 |
}
|
|
|
|
| 21 |
switch (status.type) {
|
| 22 |
case 'thinking': return 'Thinking';
|
| 23 |
case 'streaming': return 'Writing';
|
| 24 |
+
case 'tool': return status.description || TOOL_LABELS[status.toolName] || `Running ${status.toolName}`;
|
| 25 |
case 'waiting-approval': return 'Waiting for approval';
|
| 26 |
default: return '';
|
| 27 |
}
|
frontend/src/hooks/useAgentChat.ts
CHANGED
|
@@ -195,8 +195,8 @@ export function useAgentChat({ sessionId, isActive, onReady, onError, onSessionD
|
|
| 195 |
onStreaming: () => {
|
| 196 |
if (isActiveRef.current) setActivityStatus({ type: 'streaming' });
|
| 197 |
},
|
| 198 |
-
onToolRunning: (toolName: string) => {
|
| 199 |
-
if (isActiveRef.current) setActivityStatus({ type: 'tool', toolName });
|
| 200 |
},
|
| 201 |
}),
|
| 202 |
// sessionId is the only real dependency β Zustand setters are stable
|
|
|
|
| 195 |
onStreaming: () => {
|
| 196 |
if (isActiveRef.current) setActivityStatus({ type: 'streaming' });
|
| 197 |
},
|
| 198 |
+
onToolRunning: (toolName: string, description?: string) => {
|
| 199 |
+
if (isActiveRef.current) setActivityStatus({ type: 'tool', toolName, description });
|
| 200 |
},
|
| 201 |
}),
|
| 202 |
// sessionId is the only real dependency β Zustand setters are stable
|
frontend/src/lib/ws-chat-transport.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface SideChannelCallbacks {
|
|
| 35 |
/** Called when assistant text starts streaming */
|
| 36 |
onStreaming: () => void;
|
| 37 |
/** Called when a tool starts running (non-plan) */
|
| 38 |
-
onToolRunning: (toolName: string) => void;
|
| 39 |
}
|
| 40 |
|
| 41 |
// ---------------------------------------------------------------------------
|
|
@@ -499,7 +499,7 @@ export class WebSocketChatTransport implements ChatTransport<UIMessage> {
|
|
| 499 |
this.enqueue({ type: 'tool-input-start', toolCallId, toolName, dynamic: true });
|
| 500 |
this.enqueue({ type: 'tool-input-available', toolCallId, toolName, input: args, dynamic: true });
|
| 501 |
|
| 502 |
-
this.sideChannel.onToolRunning(toolName);
|
| 503 |
this.sideChannel.onToolCallPanel(toolName, args as Record<string, unknown>);
|
| 504 |
break;
|
| 505 |
}
|
|
|
|
| 35 |
/** Called when assistant text starts streaming */
|
| 36 |
onStreaming: () => void;
|
| 37 |
/** Called when a tool starts running (non-plan) */
|
| 38 |
+
onToolRunning: (toolName: string, description?: string) => void;
|
| 39 |
}
|
| 40 |
|
| 41 |
// ---------------------------------------------------------------------------
|
|
|
|
| 499 |
this.enqueue({ type: 'tool-input-start', toolCallId, toolName, dynamic: true });
|
| 500 |
this.enqueue({ type: 'tool-input-available', toolCallId, toolName, input: args, dynamic: true });
|
| 501 |
|
| 502 |
+
this.sideChannel.onToolRunning(toolName, (args as Record<string, unknown>)?.description as string | undefined);
|
| 503 |
this.sideChannel.onToolCallPanel(toolName, args as Record<string, unknown>);
|
| 504 |
break;
|
| 505 |
}
|
frontend/src/store/agentStore.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface LLMHealthError {
|
|
| 41 |
export type ActivityStatus =
|
| 42 |
| { type: 'idle' }
|
| 43 |
| { type: 'thinking' }
|
| 44 |
-
| { type: 'tool'; toolName: string }
|
| 45 |
| { type: 'waiting-approval' }
|
| 46 |
| { type: 'streaming' };
|
| 47 |
|
|
|
|
| 41 |
export type ActivityStatus =
|
| 42 |
| { type: 'idle' }
|
| 43 |
| { type: 'thinking' }
|
| 44 |
+
| { type: 'tool'; toolName: string; description?: string }
|
| 45 |
| { type: 'waiting-approval' }
|
| 46 |
| { type: 'streaming' };
|
| 47 |
|