"""Session-scoped roster and bounded live transcript snapshots for shared clients. Async projection adapted from JoaoMarcos44's PR #70899; controls reuse the existing subagent.steer RPC rather than introducing a second steering runtime. """ from .method_ctx import HandlerRegistry, bind_module _registry = HandlerRegistry() method = _registry.method _SUBAGENT_SNAPSHOT_FIELDS = ( "subagent_id", "parent_id", "depth", "goal", "delegation_id", "model", "started_at", "status", "tool_count", "last_tool", "accepting_steer", ) _SUBAGENT_TAIL_BYTES = 16384 def _owned_subagent_records(session_id, transport, owner): from tools.delegate_tool_registry import _active_subagents, _active_subagents_lock, _subagent_transport_matches with _active_subagents_lock: return [dict(r) for r in _active_subagents.values() if r.get("owner_session_id") == session_id and _subagent_transport_matches(r, transport) and r.get("owner_session_record") is owner] @method("subagent.list") def _(rid, params): session_id = _str_param(params, "session_id") transport, owner = _current_session_steer_authority(session_id) if transport is None or owner is None: return _err(rid, 4001, "session not found or not owned by this transport") live = _owned_subagent_records(session_id, transport, owner) return _ok(rid, { "subagents": [{key: r.get(key) for key in _SUBAGENT_SNAPSHOT_FIELDS} for r in live], "delegations": [], }) @method("subagent.interrupt") def _(rid, params): from agent.interrupt_compat import request_hard_interrupt subagent_id = _str_param(params, "subagent_id") if not subagent_id: return _err(rid, 4000, "subagent_id required") session_id = _str_param(params, "session_id") transport, owner = _current_session_steer_authority(session_id) if transport is None or owner is None: return _err(rid, 4001, "session not found or not owned by this transport") record = next((r for r in _owned_subagent_records(session_id, transport, owner) if r.get("subagent_id") == subagent_id), None) agent = record.get("agent") if record else None # Interrupt the authorized object, never re-resolve a globally recyclable id. found = False if agent is not None: try: found = bool(request_hard_interrupt(agent, f"Interrupted via TUI ({subagent_id})")) except Exception: logger.debug("subagent interrupt failed", exc_info=True) return _ok(rid, {"found": found, "subagent_id": subagent_id}) @method("subagent.tail") def _(rid, params): session_id = _str_param(params, "session_id") subagent_id = _str_param(params, "subagent_id") if not subagent_id: return _err(rid, 4000, "subagent_id required") transport, owner = _current_session_steer_authority(session_id) if transport is None or owner is None: return _err(rid, 4001, "session not found or not owned by this transport") result = {"subagent_id": subagent_id, "available": False, "text": "", "truncated": False} record = next((r for r in _owned_subagent_records(session_id, transport, owner) if r.get("subagent_id") == subagent_id), None) path = getattr(record.get("agent"), "_live_transcript_path", None) if record else None if not path: return _ok(rid, result) try: with open(path, "rb") as stream: size = stream.seek(0, 2) stream.seek(max(0, size - _SUBAGENT_TAIL_BYTES)) text = stream.read(_SUBAGENT_TAIL_BYTES).decode("utf-8", errors="ignore") except OSError: # Creation/cleanup races are normal while a child starts or ends. return _ok(rid, result) return _ok(rid, {**result, "available": True, "text": text, "truncated": size > _SUBAGENT_TAIL_BYTES}) def register(server): bind_module(globals(), server)