akseljoonas HF Staff commited on
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