Skip to content

feat(channels): direct prompt pane + open PromptChannel Protocol (PR-B) - #12

Merged
flg77 merged 1 commit into
mainfrom
feat/prompt-pane-and-channels
Apr 30, 2026
Merged

feat(channels): direct prompt pane + open PromptChannel Protocol (PR-B)#12
flg77 merged 1 commit into
mainfrom
feat/prompt-pane-and-channels

Conversation

@flg77

@flg77 flg77 commented Apr 30, 2026

Copy link
Copy Markdown
Owner

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

File Role
`acc/channels/base.py` (new) `PromptChannel(Protocol)` + `PromptResponse` dataclass. Mirrors the shape of `acc.backends.LLMBackend`.
`acc/channels/tui.py` (new) `TUIPromptChannel` — first concrete impl, owns the in-flight future map, registers listener BEFORE publish to avoid races.
`acc/tui/screens/prompt.py` (new) `PromptScreen` — two-pane layout (form + scrollable chat history), Send dispatches via async worker, FIFO history capped at 100 entries.
`acc/tui/client.py` `NATSObserver` extended: `register_task_listener` / `unregister_task_listener` + `_route_task_complete` fan-out by `task_id`.
`acc/agent.py` `_handle_task` gains `target_agent_id` filter (None/missing = legacy broadcast). TASK_COMPLETE now echoes `task_id`.
`acc/tui/widgets/nav_bar.py` `_SCREENS` + `BINDINGS` extended to 7 entries.
`acc/tui/screens/{soma,nucleus,compliance,comms,performance,ecosystem}.py` One-line `("7", "navigate('prompt')", "Prompt")` binding added to each — keys 1–7 work uniformly.
`acc/tui/app.py` `SCREENS["prompt"] = PromptScreen`.
`acc/tui/help/prompt.md` (new) Per-screen help doc.

Wire-protocol compatibility

  • `TASK_ASSIGN` gains optional `target_agent_id` (None / missing = legacy broadcast-by-role behaviour).
  • `TASK_COMPLETE` now carries `task_id` (empty string fallback for pre-PR-B agents).
  • No new NATS subjects.

Tests (21 new, all green)

File Cases Coverage
`tests/test_prompt_channel.py` 6 Channel send/receive correlation, timeout cleanup, close cancels in-flight, target_agent_id omission
`tests/test_observer_task_listener.py` 5 Registry register/unregister/replace; route_task_complete fan-out + missing-id safety
`tests/test_agent_target_agent_id.py` 5 Filter logic + source-text smoke check that the deployed branch matches the test expectations
`tests/test_prompt_screen_pilot.py` 5 Send dispatch, empty-prompt notification, synthetic TASK_COMPLETE → history append, render markup, clear history

Combined PR-A + PR-A2 + PR-B footprint: 37 cases, all green. Existing core suite: 139 unit tests pass unchanged.

Test plan

  • `pytest tests/test_prompt_channel.py tests/test_observer_task_listener.py tests/test_agent_target_agent_id.py tests/test_prompt_screen_pilot.py -v --no-cov` → 21 passed
  • Combined `pytest tests/test_ecosystem_screen_pilot.py tests/test_file_picker_pilot.py tests/test_config.py tests/test_role_store.py tests/test_guardrails.py tests/test_compliance.py --deselect tests/test_role_store.py::TestEd25519Validation --no-cov -q` → 155 passed.
  • Manual: `acc-tui` → press `7` (or click 7 Prompt in NavBar). Type a prompt, click Send. History pane appends an operator block immediately, then an agent block when the agent replies. Repeat with the optional target_agent_id field set to pin a specific agent.

Out of scope (planned follow-ups)

  • `SlackPromptChannel` / `TelegramPromptChannel` / `WhatsAppPromptChannel` — each a separate small PR constructing the same Protocol from a bot SDK.
  • TASK_PROGRESS streaming — `supports_streaming()` returns False today; can flip once agent progress emissions stabilise.
  • Communication matrix per role / chunked work splitting — flagged in the user's notes as a separate orchestration PR.

Notes file: `C:\Users\micro\Documents\Notes\Notes\Development\AgenticCellCorpus\ACC TUI\TUI Fixes.md`

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
@flg77
flg77 merged commit a2cb0fc into main Apr 30, 2026
@flg77
flg77 deleted the feat/prompt-pane-and-channels branch April 30, 2026 15:13
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>
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