diff --git a/agent/subagent_lifecycle.py b/agent/subagent_lifecycle.py index 4db2592af4..23e7179040 100644 --- a/agent/subagent_lifecycle.py +++ b/agent/subagent_lifecycle.py @@ -247,8 +247,7 @@ class SubagentLifecycleService: if request.correlation_id and correlation_key in _REGISTRY.correlations: raise SubagentLifecycleError("Duplicate correlation_id for this parent session.") - # Delegate construction stays internal so plugin code never imports - # private delegation helpers or touches the active-child registry. + # Delegate construction stays internal: plugins never import private delegation helpers. from tools.delegate_tool import _build_child_preserving_parent_tools, DEFAULT_MAX_ITERATIONS child = _build_child_preserving_parent_tools( @@ -318,29 +317,24 @@ class SubagentLifecycleService: agent = record.agent record.state = SubagentState.CANCEL_REQUESTED record.updated_at = time.time() - unsupported = SubagentCancelResult(False, unsupported=True, state=SubagentState.CANCEL_REQUESTED) - if agent is None: - return unsupported - try: - accepted = request_hard_interrupt( - agent, - f"Lifecycle cancellation requested: {reason[:500]}", - tool_reason="subagent cancellation requested", - ) - except Exception: - accepted = False - if not accepted: - return unsupported - return SubagentCancelResult(True, state=SubagentState.CANCEL_REQUESTED) + accepted = False + if agent is not None: + try: + accepted = request_hard_interrupt( + agent, + f"Lifecycle cancellation requested: {reason[:500]}", + tool_reason="subagent cancellation requested", + ) + except Exception: + accepted = False + return SubagentCancelResult(bool(accepted), unsupported=not accepted, state=SubagentState.CANCEL_REQUESTED) def result(self, handle: SubagentHandle) -> SubagentResult: record = self._record(handle) if record is None: return SubagentResult(handle, SubagentState.UNKNOWN, False, error_classification="UNKNOWN_HANDLE") with _REGISTRY.lock: - if record.result is not None: - return record.result - return SubagentResult(record.handle, record.state, False, error_classification="NOT_READY") + return record.result or SubagentResult(record.handle, record.state, False, error_classification="NOT_READY") def reconnect(self, handle: SubagentHandle) -> SubagentReconnectResult: record = self._record(handle) @@ -384,8 +378,7 @@ class SubagentLifecycleService: raw = _run_child_lifecycle(0, goal, record.agent, parent) is_dict = isinstance(raw, dict) - if not is_dict: - raw = {} + raw = raw if is_dict else {} status = str(raw.get("status", "error")) if status == "interrupted": cancelled = record.state == SubagentState.CANCEL_REQUESTED @@ -425,16 +418,10 @@ class SubagentLifecycleService: @staticmethod def _validate_request(request: SubagentLaunchRequest, parent: Any) -> None: - if ( - not isinstance(request, SubagentLaunchRequest) - or not isinstance(request.goal, str) - or not request.goal.strip() - or len(request.goal) > _MAX_GOAL_CHARS - ): + goal_ok = isinstance(request, SubagentLaunchRequest) and isinstance(request.goal, str) + if not goal_ok or not request.goal.strip() or len(request.goal) > _MAX_GOAL_CHARS: raise SubagentLifecycleError("goal must be a non-empty string of at most 16000 characters.") - if request.context is not None and ( - not isinstance(request.context, str) or len(request.context) > _MAX_CONTEXT_CHARS - ): + if request.context is not None and (not isinstance(request.context, str) or len(request.context) > _MAX_CONTEXT_CHARS): raise SubagentLifecycleError("context must be a string of at most 32000 characters.") if request.role not in {"leaf", "orchestrator"}: raise SubagentLifecycleError("role must be 'leaf' or 'orchestrator'.") diff --git a/agent/trace_upload.py b/agent/trace_upload.py index addb159e0c..6bda045491 100644 --- a/agent/trace_upload.py +++ b/agent/trace_upload.py @@ -72,8 +72,7 @@ def _part_to_block(part: Any, redact: bool) -> Dict[str, Any]: if ptype == "text": return _text_block(part.get("text", ""), redact) if ptype in ("image_url", "image"): - # The viewer renders text turns; don't inline base64 blobs. - return {"type": "text", "text": "[image omitted]"} + return {"type": "text", "text": "[image omitted]"} # the viewer renders text turns; no base64 return _text_block(json.dumps(part), redact) @@ -88,6 +87,15 @@ def _content_to_blocks(content: Any, redact: bool) -> List[Dict[str, Any]]: return [_text_block(json.dumps(content), redact)] +def _parse_tool_args(raw_args: Any) -> Dict[str, Any]: + if not isinstance(raw_args, str): + return raw_args if isinstance(raw_args, dict) else {} + try: + return json.loads(raw_args) if raw_args.strip() else {} + except (json.JSONDecodeError, ValueError): + return {"_raw": raw_args} + + def _tool_calls_to_blocks(tool_calls: Any, redact: bool) -> List[Dict[str, Any]]: """Convert OpenAI tool_calls into Anthropic ``tool_use`` content blocks.""" blocks: List[Dict[str, Any]] = [] @@ -95,14 +103,7 @@ def _tool_calls_to_blocks(tool_calls: Any, redact: bool) -> List[Dict[str, Any]] if not isinstance(tc, dict): continue fn = tc.get("function") or {} - raw_args = fn.get("arguments") - if isinstance(raw_args, str): - try: - parsed = json.loads(raw_args) if raw_args.strip() else {} - except (json.JSONDecodeError, ValueError): - parsed = {"_raw": raw_args} - else: - parsed = raw_args if isinstance(raw_args, dict) else {} + parsed = _parse_tool_args(fn.get("arguments")) if redact: try: parsed = json.loads(_redact(json.dumps(parsed), redact)) @@ -127,9 +128,9 @@ def _git_branch(cwd: str) -> str: ["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3, cwd=cwd, ) - return r.stdout.strip() if r.returncode == 0 else "" except Exception: return "" + return r.stdout.strip() if r.returncode == 0 else "" def _assistant_message(msg: Dict[str, Any], model: str, redact: bool) -> Dict[str, Any]: @@ -310,14 +311,10 @@ def upload_session_trace( except Exception as e: logger.warning("Failed to load session %s for trace upload: %s", session_id, e) return f"Could not load session {session_id}: {e}" - if not messages: return "No transcript to upload for this session yet." - try: - jsonl = build_trace_jsonl( - messages, session_id=session_id, model=model or meta.get("model") or "", cwd=cwd, redact=redact, - ) + jsonl = build_trace_jsonl(messages, session_id=session_id, model=model or meta.get("model") or "", cwd=cwd, redact=redact) except TraceRedactionError: return _REDACTION_BLOCKED_MESSAGE if not jsonl.strip():