feat(channels): direct prompt pane + open PromptChannel Protocol (PR-B) - #12
Merged
Conversation
Closes the operator-input gap from TUI Fixes.md: a TUI pane where the
operator types a prompt → an ACC agent runs it → the response streams
back as a chat-history block. The interface is generalised behind a
PromptChannel Protocol so future Slack / Telegram / WhatsApp adapters
plug in unchanged.
acc/channels/__init__.py + base.py + tui.py (NEW package)
PromptChannel(Protocol) — three async methods + supports_streaming():
send(prompt, *, target_role, target_agent_id=None) -> task_id
receive(task_id, timeout=60.0) -> PromptResponse
supports_streaming() -> bool
close() -> None
PromptResponse dataclass — task_id, agent_id, output, episode_id,
blocked, block_reason, latency_ms, invocations. Decoupled from
the wire payload so future channels can populate the same shape
from non-NATS sources.
TUIPromptChannel — first concrete impl. Owns a per-instance
in-flight future map; send() registers the listener on the
observer BEFORE publishing TASK_ASSIGN (so a fast TASK_COMPLETE
can't race the registration); receive() awaits the future with
timeout + cleanup; close() cancels every dangling future.
supports_streaming() returns False — single-shot today, future
enhancement gated behind the flag.
acc/tui/client.py
NATSObserver gains a per-task_id listener registry:
register_task_listener(task_id, future)
unregister_task_listener(task_id) — idempotent
+ _route_task_complete fan-out: pop the matching Future and
set_result(payload). Existing per-agent aggregation untouched.
acc/agent.py
_handle_task gains a target_agent_id filter at the top:
target_aid = data.get("target_agent_id")
if target_aid and target_aid != self.agent_id: return
None / missing key preserves legacy broadcast-by-role behaviour;
empty string is also treated as broadcast (defensive — a future
channel that emits "" instead of omitting the field can't pin
the task to a non-existent agent).
TASK_COMPLETE payload now echoes "task_id" so prompt-channel
listeners can correlate. Falls back to "" when the upstream
payload omitted task_id (back-compat with pre-PR-B agents).
acc/tui/screens/prompt.py (NEW screen — nav 7)
PromptScreen — two-pane layout:
Top form: target_role Select, target_agent_id Input, prompt
TextArea, Send + Clear-history buttons, status line.
Bottom: chat-history Static inside ScrollableContainer, FIFO
capped at 100 entries. Operator → coding_agent / agent-id
blocks in cyan; agent replies in green/red (blocked);
system errors / timeouts in yellow.
Send dispatches via TUIPromptChannel from a worker task so the
screen stays responsive while waiting for the reply.
Bindings: Ctrl+S=Send (priority), Ctrl+L=Clear history.
on_unmount cancels every in-flight worker so screen-switch is clean.
acc/tui/widgets/nav_bar.py
_SCREENS extended with ("7", "prompt", "7 Prompt").
BINDINGS extended with ("7", "navigate('prompt')", "Prompt").
Mechanical change — no behaviour drift on the existing 6 screens.
acc/tui/screens/{soma,nucleus,compliance,comms,performance,ecosystem}.py
Each carries the same ("7", "navigate('prompt')", "Prompt") binding
so operator keys 1–7 work uniformly from any screen. Verified by
grep: every screen has the navigation triple.
acc/tui/app.py
+ from acc.tui.screens.prompt import PromptScreen
+ SCREENS["prompt"] = PromptScreen
acc/tui/help/prompt.md (NEW)
Per-screen help doc covering: how a prompt round-trips, form fields,
buttons + keys, history pane semantics, status-line states, when to
use Prompt vs Nucleus vs PLAN, out-of-scope items.
Tests (21 new pytest-asyncio cases, all green):
tests/test_prompt_channel.py (6):
send_publishes_task_assign_with_fresh_task_id
send_omits_target_agent_id_when_none
receive_correlates_by_task_id
receive_timeout_unregisters_listener
close_cancels_inflight_futures
supports_streaming_returns_false
tests/test_observer_task_listener.py (5):
route_task_complete_resolves_matching_future
route_task_complete_ignores_unknown_task_id
route_task_complete_handles_missing_task_id
unregister_task_listener_is_idempotent
register_replaces_previous_listener_for_same_id
tests/test_agent_target_agent_id.py (5):
4 unit tests for the inline filter logic
1 source-text smoke check that the deployed branch matches what
the unit tests assert (catches refactors that drift the branch
semantics from the test expectations).
tests/test_prompt_screen_pilot.py (5):
send_publishes_task_assign_with_form_values
send_with_empty_prompt_notifies_and_skips_publish
synthetic_task_complete_appended_to_history
history_render_shows_operator_and_agent_blocks
clear_history_empties_the_pane
Combined PR-A + PR-A2 + PR-B test footprint: 37 cases. Existing
suite: 139 unit tests pass unchanged.
Wire-protocol compatibility:
* TASK_ASSIGN gains optional target_agent_id (None / missing =
legacy behaviour).
* TASK_COMPLETE now carries task_id (empty string when upstream
omitted it, never breaks).
* No new NATS subjects.
Out of scope (separate small follow-up PRs):
* SlackPromptChannel / TelegramPromptChannel / WhatsAppPromptChannel
— bot-daemon adapters constructing the same Protocol.
* TASK_PROGRESS streaming — channel.supports_streaming() flips True
once the agent's progress emissions stabilise.
* Communication matrix per role / chunked work splitting — the
user's notes flagged this as a separate orchestration concern.
Notes file: C:\Users\micro\Documents\Notes\Notes\Development\AgenticCellCorpus\ACC TUI\TUI Fixes.md
3 tasks
flg77
added a commit
that referenced
this pull request
Apr 30, 2026
…ss operator input (#13) First non-TUI implementation of the PromptChannel Protocol from PR-B. Operators @-mention an ACC bot in Slack; the daemon dispatches the message as TASK_ASSIGN, awaits the agent's TASK_COMPLETE, and posts the reply back to the same Slack thread. acc/channels/slack.py (NEW) SlackPromptChannel — owns its own nats.aio.client connection (the TUI's NATSObserver is view-coupled and can't be reused from a daemon process). Wire format mirrors NATSObserver.publish so agents on the bus can't tell which channel a TASK_ASSIGN came from. .connect() — open NATS conn + subscribe to acc.{cid}.task .send(prompt, *, target_role, target_agent_id=None) -> task_id .receive(task_id, timeout) -> PromptResponse .supports_streaming() -> False .close() — cancel inflight + drain NATS Subscription callback fans only TASK_COMPLETE into the per-task_id Future registry; TASK_ASSIGN echoes + everything else is silently dropped. Single registry (_inflight) — receive() is the sole eviction point, so set_result + lookup race-free. SlackDaemon — Slack-side glue. Uses slack_bolt's AsyncApp + AsyncSocketModeHandler so the bot works behind firewalls (no public webhook). Imports slack_bolt LAZILY inside .run() so the rest of the package stays light when the operator doesn't run Slack. Bot @-mention → strip <@U...> tokens via _MENTION_RE, optional "role=<name>" prefix via _ROLE_PREFIX_RE, channel.send → dispatched-notice say(), channel.receive → final reply say() in the SAME Slack thread (uses event.thread_ts when present, falls back to event.ts so top-level @-mentions start a new thread). Empty prompts / role= without body produce⚠️ notices. Send / receive failures + timeouts produce ❌ /⚠️ notices that include task_id[:8] for cross-referencing audit logs. main() entry point — env-driven config (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, ACC_NATS_URL, ACC_COLLECTIVE_ID, ACC_DEFAULT_TARGET_ROLE, ACC_SLACK_TIMEOUT_S). acc/channels/__init__.py + re-exports SlackPromptChannel + SlackDaemon so `from acc.channels import SlackPromptChannel` works without knowing the submodule path. pyproject.toml + [project.optional-dependencies] slack = [slack_bolt, aiohttp] (operator runs `pip install 'acc[slack]'` to enable the daemon) + [project.scripts] acc-channel-slack = "acc.channels.slack:main" docs/howto-slack-channel.md (NEW) 9-section operator guide: 1. Prerequisites 2. Slack app setup (app + bot scopes + Socket Mode + event subscriptions) 3. Install 4. Run 5. Use it (default routing + role=X prefix + threading) 6. Wire-protocol notes 7. Troubleshooting matrix 8. Out of scope (streaming, agent=X routing, slash commands) 9. See-also cross-links to acc/channels/base.py + acc/channels/tui.py Tests (tests/test_slack_channel.py — NEW, 20 cases all green): Channel-side (9): connect_subscribes_to_task_subject send_publishes_canonical_task_assign send_omits_target_agent_id_when_none receive_correlates_by_task_id receive_timeout_drops_listener close_cancels_inflight_and_drains on_message_ignores_non_task_complete supports_streaming_returns_false payload_to_response_handles_missing_fields Regex helpers (3): mention_regex_strips_user_id_tokens role_prefix_regex_captures_role role_prefix_regex_no_match_when_absent Daemon dispatch (8): app_mention_strips_bot_token_and_dispatches app_mention_role_prefix_overrides_default app_mention_empty_prompt_warns app_mention_role_prefix_without_body_warns app_mention_timeout_posts_warning app_mention_blocked_reply_renders_with_block_marker app_mention_uses_thread_ts_when_replying_in_existing_thread app_mention_send_failure_posts_error Mocking strategy: nats.aio.client is mocked via monkeypatch on the sys.modules entry so the channel's own `import nats` succeeds against a fake. slack_bolt is NOT imported by the daemon's _on_app_mention path — that's why the daemon tests run without slack_bolt installed (only `.run()` would need it, and we don't exercise that in unit tests; manual testing per docs/howto-slack-channel.md covers it). Combined regression footprint: 204 tests pass (20 new Slack + 184 existing PR-A/A2/B + PR #9 + core). TestEd25519Validation deselected for the lighthouse OpenSSL platform limit, as in PRs #7-#12. Out of scope (separate small follow-up PRs): * TASK_PROGRESS streaming into Slack (channel.supports_streaming() flips True once the protocol stabilises). * Per-agent routing via "agent=<id>" directive. * Slash commands (/acc <prompt>) — currently mention-only. * Container packaging (operator runs the daemon as a Python process for now; production image follow-up later). * TelegramPromptChannel + WhatsAppPromptChannel — same Protocol, different bot SDKs. Notes file: C:\Users\micro\Documents\Notes\Notes\Development\AgenticCellCorpus\ACC TUI\TUI Fixes.md
flg77
added a commit
that referenced
this pull request
May 20, 2026
Bumps that resolve the open Dependabot alerts on the repo: - uv.lock: urllib3 2.6.3 -> 2.7.0 (#10 decompression-bomb High, #11 cross-origin sensitive headers High) - uv.lock: idna 3.13 -> 3.15 (#13 idna.encode bypass — supersedes Dependabot PR #92) - uv.lock: pytest 8.4.2 -> 9.0.3 (#9 tmpdir-handling CVE; the patched line is pytest 9.x, so pytest-asyncio bumps to 1.3.0 and pytest-cov to 7.1.0) - pyproject.toml: pytest>=9.0,<10 + pytest-asyncio>=1.0,<2.0 widened to let the resolver onto the patched line. - webgui/package.json: vite ^5.4.0 -> ^5.4.21 (#12 .map path traversal). #8 (transformers Trainer-class arbitrary-code execution) cannot be bumped today — the patched line is transformers 5.x but no sentence-transformers release supports transformers 5 yet. We do NOT use Trainer (only the SentenceTransformer encode() API for embeddings), so the vulnerable code is not in our execution path. The sentence- transformers range is widened to `<5.0` so the bump becomes a one-line uv-lock change once ST releases support. Documented in pyproject.toml; GitHub alert #8 to be dismissed with "vulnerable code not in execution path". 42 webgui tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flg77
added a commit
that referenced
this pull request
Jun 1, 2026
) Follow-up to v0.3.45 Phase 1. Closes risk #1 from impl note 60: workspace renderer was falling back to `role.allowed_skills` verbatim because the snapshot didn't query skills. Now it does. * `PerceptionSnapshot.available_skills` — new field (same shape as available_roles / available_mcps). * `_query_capabilities` fan-out grows from 2→3 parallel queries (role + mcp + skill) under the same 100ms budget. Stale flag only trips when both roles + mcps come back empty — skills alone failing is silent (orchestrator may run without a SkillRegistry). * `_render_workspace` switched from a kludgy `available_roles[kind==skill]` scan to the dedicated `available_skills` list. Behaviour preserved for the no-catalog-skills fallback path. Tests: +1 (`test_skills_intersected_with_available_skills`); 2476 passed. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the operator-input gap from `TUI Fixes.md`: a TUI pane (screen 7) where the operator types a prompt → an ACC agent runs it → the response streams back as a chat-history block. The interface is generalised behind a `PromptChannel` Protocol so future Slack / Telegram / WhatsApp adapters plug in without re-touching the screen code.
Architecture
```
PromptScreen Agent
──────────── ─────
user types ──► action_send ──┐
│
▼
TUIPromptChannel
────────────────
send() ──── publish TASK_ASSIGN ───► subject acc.{cid}.task
│ {task_id, target_role,
│ target_agent_id?, content}
│
│ register_task_listener(task_id, future)
│
▼
_handle_task filter
(drop if mismatch)
│
▼
process_task → TASK_COMPLETE
(echoes task_id)
│
◄─────────────── observer fan-out ─────┘
(resolves Future by task_id)
receive() returns PromptResponse
```
Deliverables
Wire-protocol compatibility
Tests (21 new, all green)
Combined PR-A + PR-A2 + PR-B footprint: 37 cases, all green. Existing core suite: 139 unit tests pass unchanged.
Test plan
Out of scope (planned follow-ups)
Notes file: `C:\Users\micro\Documents\Notes\Notes\Development\AgenticCellCorpus\ACC TUI\TUI Fixes.md`