Say what a tool did, not where it ran
An agent event set its label to the SSH profile's name, so the transcript read "homeserver · ls -la" -- naming the machine rather than the thing that was done. Built-in tools set no label at all and fell back to the function name, so a saved memory read "memory_add". The status line said "Running shell_run…" and the approval card had its own hand-written wording. Four places, four answers, nothing checking that any of them agreed. services/tool_labels.py is the one table all of them read now. Bash, Read, Write, List, Web search, Memory saved; an icon each, instead of everything being the sparkle. The precedence is inverted on purpose. Tool events are persisted in Message.tool_calls_json, so every agent row already on disk carries the profile name -- a resolver that preferred the stored value would fix nothing for any transcript that already exists. So a name the table knows resolves from the table, and a name it does not -- a custom HTTP tool, an MCP tool, whose labels are per row and cannot be tabulated -- keeps its own. One rule, both cases correct. The machine moves to `detail`, where "where this ran" belongs. tool_label and tool_icon are Jinja globals because a message bubble is rendered from four handlers, and a fifth thing each of them must remember to pass is a fifth thing one of them will forget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ from lembas.db.models import KIND_AGENT, ROLE_ASSISTANT, ROLE_USER, Chat, Messag
|
||||
from lembas.db.session import session_scope
|
||||
from lembas.services import chat as chat_service
|
||||
from lembas.services import compaction as compaction_service
|
||||
from lembas.services import interaction, tokens
|
||||
from lembas.services import interaction, tokens, tool_labels
|
||||
from lembas.services import metrics as metrics_service
|
||||
from lembas.services import prompts as prompts_service
|
||||
from lembas.services import tools as tools_service
|
||||
@@ -721,6 +721,26 @@ def _gave_up(generation, why: str) -> None:
|
||||
generation.touch()
|
||||
|
||||
|
||||
def _written(generation: Generation) -> int:
|
||||
"""How much this reply has written so far, in tokens, reported or estimated.
|
||||
|
||||
Both, because neither alone is enough. `completion_tokens` is only populated
|
||||
when the endpoint sends a usage block, and a good half of the ones this
|
||||
talks to -- llama.cpp, Ollama and friends -- never do; the fallback estimate
|
||||
is otherwise computed once, in `_run`'s `finally:`, long after the loop that
|
||||
needs it. A ceiling reading only the reported figure would work on OpenAI
|
||||
and silently do nothing everywhere else, which is the worst kind of limit:
|
||||
one that looks configured.
|
||||
|
||||
Reasoning counts. It was generated and it was paid for, even though it is
|
||||
deliberately never replayed as context.
|
||||
"""
|
||||
return max(
|
||||
generation.completion_tokens,
|
||||
tokens.estimate(generation.text + generation.thinking),
|
||||
)
|
||||
|
||||
|
||||
def _tool_status(calls: list[dict]) -> str:
|
||||
"""What to show while tools run.
|
||||
|
||||
@@ -728,7 +748,7 @@ def _tool_status(calls: list[dict]) -> str:
|
||||
nothing streaming, and a silent pause is exactly what a hang looks like.
|
||||
"""
|
||||
if len(calls) == 1:
|
||||
return f"Running {calls[0]['name']}…"
|
||||
return f"Running {tool_labels.label_for(calls[0]['name'])}…"
|
||||
return f"Running {len(calls)} tools…"
|
||||
|
||||
|
||||
@@ -746,17 +766,12 @@ def _describe(name: str, args: dict) -> tuple[str, str]:
|
||||
The detail is the thing being agreed to -- the command line, the path -- and
|
||||
is shown verbatim and escaped. A summary that paraphrased it would be a card
|
||||
approving something other than what runs.
|
||||
|
||||
Delegated to services/tool_labels.py, which the transcript and the status
|
||||
line read too. This used to be a hand-written if-chain and was the fourth
|
||||
place with its own wording for the same tool.
|
||||
"""
|
||||
if name == "shell_run":
|
||||
return "Run a command", str(args.get("command") or "")
|
||||
if name == "file_write":
|
||||
return "Write a file", str(args.get("path") or "")
|
||||
if name == "file_read":
|
||||
return "Read a file", str(args.get("path") or "")
|
||||
if name == "file_list":
|
||||
return "List a directory", str(args.get("path") or "")
|
||||
detail = ", ".join(f"{k}={v!r}" for k, v in list(args.items())[:4])
|
||||
return f"Use {name}", detail[:400]
|
||||
return tool_labels.describe(name, args)
|
||||
|
||||
|
||||
def _approvals(context, calls: list[dict]) -> list[interaction.Item]:
|
||||
|
||||
Reference in New Issue
Block a user