diff --git a/apps/desktop/src/app/agents/index.tsx b/apps/desktop/src/app/agents/index.tsx index 5ac16a71fd..09d9ce88c3 100644 --- a/apps/desktop/src/app/agents/index.tsx +++ b/apps/desktop/src/app/agents/index.tsx @@ -143,23 +143,10 @@ const flatten = (nodes: readonly SubagentNode[]): SubagentNode[] => interface RootGroup { id: string delegationIndex: number - /** Short batch tag (`deleg_6a664903` → `6a66`) when the backend sent one. */ - batchTag?: string nodes: SubagentNode[] taskCount: number } -/** `deleg_6a664903` → `6a66`; mirrors tools.delegate_tool.format_batch_tag. */ -export const batchTagOf = (delegationId: string | undefined): string | undefined => { - if (!delegationId) { - return undefined - } - - const short = delegationId.split('_').at(-1)?.slice(0, 4) - - return short || undefined -} - function groupDelegations(roots: readonly SubagentNode[]): RootGroup[] { const groups: RootGroup[] = [] let n = 0 @@ -180,7 +167,6 @@ function groupDelegations(roots: readonly SubagentNode[]): RootGroup[] { groups.push({ id: `delegation:${node.delegationId}`, delegationIndex: n, - batchTag: batchTagOf(node.delegationId), nodes: [node], taskCount: node.taskCount }) @@ -192,7 +178,7 @@ function groupDelegations(roots: readonly SubagentNode[]): RootGroup[] { const prev = groups.at(-1) const prevTail = prev?.nodes.at(-1) const closeInTime = prevTail ? Math.abs(node.startedAt - prevTail.startedAt) <= 5_000 : false - const sameShape = prev && !prev.batchTag && node.taskCount > 1 && prev.taskCount === node.taskCount + const sameShape = prev && !prev.id.startsWith('delegation:') && node.taskCount > 1 && prev.taskCount === node.taskCount const uniqueStep = prev ? !prev.nodes.some(item => item.taskIndex === node.taskIndex) : false if (prev && sameShape && closeInTime && uniqueStep) { @@ -285,10 +271,7 @@ function DelegationGroup({ group, nowMs }: { group: RootGroup; nowMs: number }) return (

- {group.delegationIndex > 0 ? t.agents.delegation(group.delegationIndex) : ''} - {group.batchTag ? ( - [{group.batchTag}] - ) : null}{' '} + {group.delegationIndex > 0 ? t.agents.delegation(group.delegationIndex) : ''}{' '} · {t.agents.workers(group.nodes.length)} {activeWorkers > 0 ? · {t.agents.workersActive(activeWorkers)} : null}

diff --git a/tests/tools/test_delegate_batch_tag.py b/tests/tools/test_delegate_batch_tag.py index f47404c7c1..168298840a 100644 --- a/tests/tools/test_delegate_batch_tag.py +++ b/tests/tools/test_delegate_batch_tag.py @@ -3,7 +3,7 @@ When a parent fans out N subagents and a child fans out its own M, both batches print ``[n/N]`` completion lines to the same console. Without a batch tag ``✓ [3/3]`` and ``✓ [3/9]`` are indistinguishable. Every progress -surface must carry the short delegation id. +surface carries a human-readable ``set N`` ordinal (not a raw id slice). """ import types @@ -13,9 +13,15 @@ import tools.delegate_tool as dt from tools.delegate_tool import _batch_prefix, _build_child_progress_callback, format_batch_tag -def test_format_batch_tag_shortens_delegation_handle(): - assert format_batch_tag("deleg_6a664903") == "6a66" - assert format_batch_tag("deleg_") == "" +@pytest.fixture(autouse=True) +def _fresh_ordinals(monkeypatch): + monkeypatch.setattr(dt, "_BATCH_ORDINALS", {}) + + +def test_format_batch_tag_assigns_stable_ordinals_per_batch(): + assert format_batch_tag("deleg_6a664903") == "set 1" + assert format_batch_tag("deleg_b2ac1234") == "set 2" + assert format_batch_tag("deleg_6a664903") == "set 1" # same batch, same label assert format_batch_tag(None) == "" assert format_batch_tag("") == "" @@ -23,9 +29,9 @@ def test_format_batch_tag_shortens_delegation_handle(): @pytest.mark.parametrize( "deleg, idx, count, expected", [ - ("deleg_6a664903", 2, 9, "[6a66 3/9] "), + ("deleg_6a664903", 2, 9, "[set 1 · 3/9] "), (None, 2, 9, "[3/9] "), - ("deleg_6a664903", 0, 1, "[6a66] "), + ("deleg_6a664903", 0, 1, "[set 1] "), (None, 0, 1, ""), ], ) @@ -60,8 +66,8 @@ def test_child_tree_lines_and_relayed_events_carry_batch_tag(): cb("tool.started", "terminal", "ls") tree = parent._delegate_spinner.lines - assert tree[0].startswith(" [6a66 3/9] ├─ 🔀 triage cluster") - assert tree[1].startswith(" [6a66 3/9] ├─ ") + assert tree[0].startswith(" [set 1 · 3/9] ├─ 🔀 triage cluster") + assert tree[1].startswith(" [set 1 · 3/9] ├─ ") assert all(kw.get("delegation_id") == "deleg_6a664903" for _, kw in relayed) assert all(kw.get("child_session_id") == "child-sess" for _, kw in relayed) @@ -74,8 +80,7 @@ def test_child_tree_prefix_without_batch_id_is_unchanged(): def test_batch_completion_lines_are_attributable_across_two_batches(monkeypatch, tmp_path): - """Two interleaved batches: every ✓ line names its own batch tag, and the - tag equals the delegation_id the dispatch returns.""" + """Two interleaved batches: every ✓ line names its own ``set N``.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes")) (tmp_path / ".hermes").mkdir() lines = [] @@ -104,13 +109,11 @@ def test_batch_completion_lines_are_attributable_across_two_batches(monkeypatch, parent_agent=parent, ) assert "error" not in str(res)[:20], res - headers = [re.match(r"\s*🔀 \[([0-9a-f]{4})\] delegating (\d+) tasks", l) for l in lines] + headers = [re.match(r"\s*🔀 \[(set \d+)\] delegating (\d+) tasks", l) for l in lines] headers = [m for m in headers if m] - assert [int(m.group(2)) for m in headers] == [3, 9] - tags = [m.group(1) for m in headers] - assert len(set(tags)) == 2 + assert [(m.group(1), int(m.group(2))) for m in headers] == [("set 1", 3), ("set 2", 9)] done = [l for l in lines if "✓ [" in l] assert len(done) == 12 - assert sum(1 for l in done if f"✓ [{tags[0]} " in l and "/3]" in l) == 3 - assert sum(1 for l in done if f"✓ [{tags[1]} " in l and "/9]" in l) == 9 + assert sum(1 for l in done if "✓ [set 1 · " in l and "/3]" in l) == 3 + assert sum(1 for l in done if "✓ [set 2 · " in l and "/9]" in l) == 9 diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 1313f3e6d4..796487f041 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -1402,27 +1402,37 @@ def _blocked_toolsets_for_role(role: str) -> List[str]: ) +_BATCH_ORDINALS: Dict[str, int] = {} +_BATCH_ORDINALS_LOCK = threading.Lock() + + def format_batch_tag(delegation_id: Optional[str]) -> str: """Short human tag identifying which delegation batch a line belongs to. - ``deleg_6a664903`` → ``6a66``. Several batches (a parent's fan-out plus - a child's nested fan-out, or two concurrent tools) print interleaved - ``[n/N]`` progress lines to the same console; without a batch tag a - ``✓ [3/3]`` and a ``✓ [3/9]`` are indistinguishable. Empty string when + ``deleg_6a664903`` → ``set 1`` (first batch seen in this process), + the next distinct id → ``set 2``, and so on. Several batches (a parent's + fan-out plus a child's nested fan-out, or two concurrent tools) print + interleaved ``[n/N]`` progress lines to the same console; without a batch + tag a ``✓ [3/3]`` and a ``✓ [3/9]`` are indistinguishable, and a raw hex + slice (``[b2ac 3/9]``) is attributable but unreadable. Empty string when no id is known so callers can concatenate unconditionally. """ if not isinstance(delegation_id, str) or not delegation_id: return "" - short = delegation_id.split("_", 1)[-1][:4] - return f"{short}" if short else "" + with _BATCH_ORDINALS_LOCK: + n = _BATCH_ORDINALS.get(delegation_id) + if n is None: + n = len(_BATCH_ORDINALS) + 1 + _BATCH_ORDINALS[delegation_id] = n + return f"set {n}" def _batch_prefix(delegation_id: Optional[str], task_index: int, task_count: int) -> str: - """``[6a66 3/9] `` for batch children, ``[6a66] `` for a lone child, + """``[set 2 · 3/9] `` for batch children, ``[set 2] `` for a lone child, ``[3/9] `` / ``""`` when the batch id is unknown.""" tag = format_batch_tag(delegation_id) if task_count > 1: - inner = f"{tag} {task_index + 1}/{task_count}" if tag else f"{task_index + 1}/{task_count}" + inner = f"{tag} · {task_index + 1}/{task_count}" if tag else f"{task_index + 1}/{task_count}" return f"[{inner}] " return f"[{tag}] " if tag else "" @@ -4366,7 +4376,7 @@ def delegate_task( icon = "✓" if status == "completed" else "✗" remaining = n_tasks - completed_count _tag = format_batch_tag(live_deleg_id) - _slot = f"{_tag} {idx+1}/{n_tasks}" if _tag else f"{idx+1}/{n_tasks}" + _slot = f"{_tag} · {idx+1}/{n_tasks}" if _tag else f"{idx+1}/{n_tasks}" completion_line = f"{icon} [{_slot}] {label} ({dur}s)" # Failed/errored/timed-out children: say WHY on the # same line, cleaned to one short human-readable