Skip to content

Generalize Slack messaging into a MessagingPlatform interface#1

Draft
0xdiid wants to merge 5 commits into
mainfrom
diid/generalize-slackbot
Draft

Generalize Slack messaging into a MessagingPlatform interface#1
0xdiid wants to merge 5 commits into
mainfrom
diid/generalize-slackbot

Conversation

@0xdiid
Copy link
Copy Markdown
Collaborator

@0xdiid 0xdiid commented May 23, 2026

No description provided.

0xdiid and others added 5 commits May 22, 2026 22:54
Move every Slack-specific bit of the API into a new platforms/ package
so additional messaging integrations (Discord, etc.) can be added by
implementing a single adapter instead of branching on platform strings
across agent.py, runtime_control.py, tool_manager.py, workflow_engine.py,
and the slack_thread_turn workflow.

New api/platforms/ package:
- MessagingPlatform base class with no-op defaults for sanitization,
  identity recovery, system-prompt injection, live-streaming session
  lifecycle, outbound channel posts, and active-thread tool-call capture.
- SlackPlatform owns all existing Slack behaviour: text scrubbing
  (sanitize_for_slack), slackbot HTTP client, GitHub-from-Slack-profile
  identity extraction, Slack formatting rules, live session + harness
  events + assistant status, and slack.send_message active-thread
  capture.
- DevPlatform: no-op fallback used when delivery is missing/unknown.
- Registry + resolvers: resolve_platform(name), resolve_for_delivery,
  resolve_for_thread_key, resolve_for_metadata.

Production-code rewires:
- agent.py: _build_session_context and _resolve_requester_identity
  dispatch via resolve_platform(); removed inline Slack-formatting and
  GitHub-handle helpers (now on SlackPlatform).
- runtime_control.py: execution worker resolves platform once from
  delivery and calls platform.session_done / harness_event /
  assistant_status; dropped dead _send_slackbot_canonical_event and
  _canonical_text_blocks helpers.
- tool_manager.py: _capture_live_slack_send → generic
  _capture_active_thread_tool_call dispatching to the platform resolved
  from thread_key prefix.
- workflow_engine.py: Delivery.for_channel(platform, ...) added (with
  Delivery.slack as a thin wrapper); ctx.post_to_channel(platform, ...)
  added (with ctx.post_to_slack wrapping it); auto-handler loader now
  honors both SLACK_CHANNEL (legacy) and DELIVERY = {platform, channel}
  (forward-compat); schedule loader honors a delivery field.
- workflows/slack_thread_turn.py: canonical WORKFLOW_NAME is now
  messaging_thread_turn with WORKFLOW_ALIASES = ("slack_thread_turn",)
  so the slackbot keeps working unchanged; _load_workflow_file registers
  aliases, _workflow_request_hash treats both names identically.
- routers/agent.py: one _resolve_delivery_fallback helper for spawn +
  execute; logs a deprecation note when callers rely on the implicit
  default platform.

Back-compat shims:
- api/slack_sanitize.py and api/slackbot_client.py re-export the public
  surface from api.platforms.slack so external imports keep working.

Tests:
- New tests/test_platforms.py covers registration, resolvers, sanitizer,
  identity-line + formatting-rule generation, and Slack delivery
  predicates.
- Existing tests that patched api.workflow_engine.slackbot_client.X /
  api.runtime_control.slackbot_client.X now patch the platform method
  directly via patch.object(SLACK_PLATFORM, ...).
- test_slackbot_client.py retargeted to patch
  api.platforms.slack.{asyncio,httpx} since the HTTP client lives there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ead code

Quick-win cluster from the multi-agent review of e2a54ef:

- Promote requester identity to a frozen `RequesterIdentity` dataclass
  (platforms/__init__.py) instead of `dict[str, str | bool]`. Field names
  drop the `slack_` prefix since the data is platform-agnostic; per-platform
  rendering ("Slack user ID: ...") stays on `system_prompt_identity_lines`.
  Updates `SlackPlatform.load_requester_identity`, `system_prompt_identity_lines`,
  and the two test files that constructed the old dict shape.

- Delete dead code: `resolve_for_metadata` had zero callers; `_DEV_FALLBACK`
  was unreachable defensive code since `DevPlatform` registers eagerly.

- Centralize platform registration via `register_builtin_platforms()` in
  `platforms/__init__.py`. `platforms/dev.py` and `platforms/slack.py` now
  only expose their singleton (`DEV_PLATFORM`, `SLACK_PLATFORM`); the
  registration call lives in one place. Removes the bottom-of-module
  side-effect imports with their `noqa: E402, F401` lies.

- Document the platform-agnostic execution metadata keys
  (`slackbot_live_delivery`, `slackbot_agent_session_id`, etc.) in the
  `api.platforms` module docstring. They're Slack-named for historical
  reasons but read by all platforms; future schema cleanup will rename
  them to `live_session_*`.

- Add `MessagingPlatform.intercepts_tool_call(tool_name, method_name)`
  sync hint. `tool_manager._capture_active_thread_tool_call` now skips
  the async dispatch hop into `capture_active_thread_tool_call` for any
  tool call the active platform doesn't intercept. `SlackPlatform`
  returns `True` only for `slack.send_message`, restoring the pre-PR
  fast path for non-send-message tool calls on Slack threads.

- Dedupe schedule registration for workflows with `WORKFLOW_ALIASES`
  (workflow_engine.py:_registered_schedule_specs). The alias system
  registers the same `_RegisteredHandler` under multiple names; without
  deduping, a future workflow with both aliases and a `CRON`/`SCHEDULE`
  would fire its schedule N times per tick. Currently latent
  (`slack_thread_turn` has no schedule), but fixed proactively.

- Narrow over-broad `except (ValueError, TypeError)` to
  `json.JSONDecodeError` in `SlackPlatform.send_channel_message`.

Verified via uv run ruff check api/ and uv run pytest -q on
test_platforms.py (17 cases including 4 new), test_integration.py,
test_slackbot_client*, test_workflow_idempotency_unit.py, and the
CI-targeted subset of test_workflows.py and test_agent_control_plane.py.
78/78 targeted tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously SlackPlatform.capture_active_thread_tool_call reached into
``request.app.state.db_pool`` and ran a raw SQL query against
``agent_execution_requests`` to fetch the live-session id. That coupled
every platform adapter to FastAPI app state and the execution-request
schema — a second platform (Discord) would have to duplicate the query
or accept the same coupling.

Split the responsibility cleanly:

- New ``ActiveThreadCapture`` NamedTuple in ``api.platforms`` carries
  the pure-data result of a capture decision (text to forward into the
  live session + envelope returned to the agent).

- ``MessagingPlatform.match_active_thread_capture(thread_key, tool_name,
  method_name, args)`` is now the platform's only capture surface. It's
  synchronous, takes pure data, returns ``ActiveThreadCapture | None``,
  and performs zero I/O. Replaces both the old async
  ``capture_active_thread_tool_call`` and the ``intercepts_tool_call``
  sync hint introduced in the prior commit.

- ``runtime_control.get_live_session_id_for_thread(pool, thread_key)``
  centralizes the live-session lookup query (which reads the
  platform-agnostic ``slackbot_live_delivery`` / ``slackbot_agent_session_id``
  metadata keys documented on ``api.platforms``).

- ``tool_manager._capture_active_thread_tool_call`` is the only place
  that wires the two together: resolve platform, ask platform to match
  (sync), look up session_id (async DB), forward text via
  ``platform.session_text``. Logs as ``active_thread_tool_call_captured``
  with the platform name.

Adds 6 new test cases in ``test_platforms.py`` for the match function
(non-send-message tool, foreign-channel write, foreign-thread write,
empty text, non-slack thread_key, base no-op).

Verified: uv run ruff check api/ clean; 82/82 targeted tests pass
(platform suite, slackbot_client suite, workflow idempotency, integration,
CI-targeted workflows + agent_control_plane).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…deprec shim

P3 cleanup bundle from the multi-agent review:

- Drop the ``"slackbot" + "_v" + "2_live_delivery"`` Python-side concat
  and the matching SQL ``'slackbot' || '_v' || '2_live_delivery'`` trick.
  Write the legacy key as a plain string with an explanatory comment so
  future readers (and grep) see what's actually being checked.

- Simplify ``_valid_github_handle`` triple-strip + path-trim into a single
  ``.strip("@ \t\n").rstrip("/").split("/", 1)[0]`` chain with a comment
  documenting the inputs it accepts.

- Promote ``HASH_IGNORED_INPUT_KEYS`` to a workflow module attribute.
  ``_RegisteredHandler`` now carries the tuple; ``_workflow_request_hash``
  consults the registered handler instead of the hard-coded
  ``{"messaging_thread_turn", "slack_thread_turn"}`` set. The
  ``messaging_thread_turn`` workflow declares
  ``HASH_IGNORED_INPUT_KEYS = ("history_messages",)`` directly.

- Move ``_SLACK_CHANNEL_ID_RE`` next to its sole consumer; collect the
  Slack formatting-rule lines into a module-level
  ``_SLACK_FORMATTING_RULES`` tuple constant so
  ``system_prompt_rules`` shrinks to one composition expression.

- Emit a ``DeprecationWarning`` on import of ``api.slackbot_client`` and
  ``api.slack_sanitize`` so the shims don't accrete new callers before
  the planned removal PR.

- Trim "Phase 0 → Phase 1" refactor-history wording from module
  docstrings now that we're past that transition; replace with concrete
  forward-looking migration guidance.

Verified: uv run ruff check api/ clean; 82/82 targeted tests pass; full
test_workflows.py shows only the pre-existing
``test_slack_thread_turn_attachment_roundtrip_to_agent`` failure
(unrelated to this PR, fails on pristine main as well).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…schedule platform

Four post-review polish items flagged by the remote review:

- Workflow alias collision now logs ``workflow_alias_collision`` when a
  later loader's alias would clobber an existing canonical handler from
  a different module. Pure observability — no current in-tree collision,
  but the alias machinery introduced in the prior commits creates the
  surface area; load-order-dependent silent overwrites now show up in
  the logs instead of as ghost behaviour.

- Schedule auto-derivation of ``delivery.platform`` from ``thread_key``
  now treats 2-part legacy keys (``CHANNEL:TS``) as slack and only reads
  a platform namespace from 3-part keys (``platform:channel:ts``).
  Restores the back-compat the inline comment already promised but the
  previous ``":" in thread_key`` shortcut bypassed for the 2-part shape
  that ``_split_thread_key`` still accepts.

- Delete dead ``_clip_slackbot`` helper and ``_MAX_SLACKBOT_TEXT_CHARS``
  / ``_MAX_SLACKBOT_STEP_CHARS`` constants in runtime_control.py. Their
  only consumer (``_send_slackbot_canonical_event``) was removed earlier
  in the branch; clipping responsibility moved to
  ``MessagingPlatform.clip_text``.

- Fix stale ``dict[str, str | bool] | None`` annotations on
  ``_resolve_requester_identity`` and ``_build_session_context``'s
  ``requester_identity`` parameter. The dataclass migration earlier in
  the branch made the runtime type ``RequesterIdentity | None``;
  annotations now match.

Verified: uv run ruff check api/ clean; 82/82 targeted tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant