Skip to content

feat(orchestrator): add activity, private-row and caller options to extension tools - #576

Open
ladislav-urban wants to merge 1 commit into
SenteLabsAI:mainfrom
ladislav-urban:feat/extension-tool-options
Open

ladislav-urban wants to merge 1 commit into
SenteLabsAI:mainfrom
ladislav-urban:feat/extension-tool-options

Conversation

@ladislav-urban

@ladislav-urban ladislav-urban commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

An extension's tool can't do three things a built-in tool can: name the chat's activity line while it runs, keep its dispatch audit row private when it quotes the speaker's own data, or be offered on a surface the extension adds that names its user without a verified caller (a kiosk or embedded widget). And an extension tool whose result is waiting_for_approval gets core's generic "Waiting for approval" chip, meant for actions the department gate holds, which never holds an extension tool.

Approach

ExtraTool gains activity_label, private_row and known_caller (default True). known_caller=False requires an offered(), which then decides alone; such a tool is still never offered on an unattended, private, AI-app or replay turn, and the lockdown and outside-content hold apply as before. summarize_action leaves a waiting result to the extension's own chip. agents.json and architecture-facts.yaml describe the options.

Checklist

  • Working implementation — no stubs or TODO placeholders
  • Tests added/updated for new behavior (pytest packages/core/tests/unit/)
  • ruff check and mypy pass (make lint)
  • UI builds if touched (cd packages/ui && npm run build) — UI not touched
  • Eval scenarios added for a new agent or prompt change (if applicable) — no new agent or prompt change
  • Architecture docs updated if a documented topic changed
    (see the "Architecture Docs" section in CLAUDE.md)
  • No secrets, credentials, or personal data committed

🤖 Generated with Claude Code

…p its audit row private and be asked without a known caller

Three things a tool built in core can do that an extension's tool could not:

- activity_label: the chat's activity line while it runs. Built-in labels
  still win; an unlabelled extension tool keeps the fallback line.
- private_row: its dispatch audit row is private (_private_tool_row), for
  a tool whose input or result quotes the speaker's own data, as the Act
  as me, fact and history tools' rows are.
- known_caller=False: asked on a turn with no verified caller too, for a
  surface an extension adds that names its user without proving who is
  speaking (a kiosk or embedded widget). register_tool requires such a tool
  to have an offered(), which decides alone; it is still never offered on
  an unattended, private, AI-app or replay turn, and the lockdown and
  outside-content hold apply as before.

And one fix: an extension tool whose result is waiting_for_approval got
core's generic "Waiting for approval" chip, meant for actions the
department gate or a teammate's send holds. The gate never holds an
extension tool, so what it waits on (its own approval) is now its chip's
to describe.

Left out on purpose: letting an extension chip carry a decision_id for
the chat to show the card under the message. Upstream's chat only renders
the card kinds it knows (ChatActionCards falls through to the meeting and
app-action cards), so an extension's id would render nothing until there
is a generic card renderer for extensions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ladislav-urban
ladislav-urban force-pushed the feat/extension-tool-options branch from 64b7a6c to 3cd7f23 Compare October 20, 2026 07:32
@github-actions

Copy link
Copy Markdown

This pull request now has merge conflicts with main (after 0bb0d5a). GitHub won't run CI until they're resolved: merge main into this branch, fix the conflicts and push.

assert "open_tool" not in offered[0]
# Still never on an unattended or private turn.
for kwargs in ({"unattended": True}, {"private_to_principal": True}):
_, _, offered = _run(_session(caller_person_id=None, origin_channel="kiosk", **kwargs), [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The docs and PR description promise a known_caller=False tool is "never offered on an unattended, private, AI-app (mcp_client) or replay turn", but this test only covers unattended and private_to_principal. offered_tools itself no longer filters on caller identity, so those exclusions depend entirely on the caller. Before this change the caller_person_id check masked that, because an MCP-client or replay session without a person id got nothing. Add from_mcp_client=True and the replay case here. Check that the chat loop's call site really excludes them. If it doesn't, a kiosk-style offered() that returns True for origin_channel == "kiosk" would hand the tool to an AI-app turn, which the persona section says gets no extension tools.

return None
from openexecutive.orchestrator import extensions

return extensions.activity_label(name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 extensions.activity_label calls load(), which imports every module named in OPENEXECUTIVE_EXTENSIONS and runs its register(). _label_for and _is_nameable now reach that for any tool name not in _LABELS, so the first activity line of a turn (and each _pick over the tool_uses) can run extension imports on the event loop. A raising or slow extension import would then delay the activity event. Read the registry without triggering load() here, since a tool can only be running if the registry has already loaded.

@github-actions

Copy link
Copy Markdown

Claude review of this fork PR, requested by @johnrufusone: 🔴 1 must fix · 🟡 1 optional · 🟣 0 pre-existing. Push again and it needs a fresh claude-review label to be re-reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants