Skip to content

feat(pipeline)!: require explicit prefixes for built-in LLM access - #60

Merged
BegoniaHe merged 4 commits into
masterfrom
feat/explicit-llm-prefix-58
Sep 6, 2026
Merged

feat(pipeline)!: require explicit prefixes for built-in LLM access#60
BegoniaHe merged 4 commits into
masterfrom
feat/explicit-llm-prefix-58

Conversation

@BegoniaHe

Copy link
Copy Markdown
Collaborator

Summary

Built-in LLM new turns now require an explicit aim: command match, LLM prefix, continuation, reply_to_bot, or extras explicit_surface. Mentions stay on the message chain and are not an LLM gate. Dashboard WebChat still chats without a prefix because it stamps explicit_surface; it does not inject / into message_str.

Related issue

Fixes #58

Behavior

  • Default llm_access.private is prefix. open remains a legal opt-in.
  • llm_access.group is only open / prefix / off. Saved mention / prefix_or_mention fall back to prefix at runtime; no config-file migrator.
  • Mention / MentionAll do not extra-admit the built-in LLM. @other still blocks prefix/command.
  • reply_to_bot remains an explicit OR, including when group=off.
  • Continuations still admit fragments without repeating the prefix.
  • Extras key is explicit_surface (Discord slash / interaction follow-up, WebChat, WeCom AI Bot, cron, PlatformManager.create_event). Discord user/role mentions do not stamp it.
  • Empty @bot no longer request_llm and no longer reinserts Mention(self). A lone command prefix (/) may still wait for the next message.
  • Plugin EventMessageType.ALL / PRIVATE_MESSAGE listeners still activate when the LLM is dropped.

Non-goals

  • Restoring platform_settings.group_wake_policy or implicit mention/reply wakeups
  • Splitting WakingCheckStage
  • Injecting / into WebChat message_str
  • Silencing unmatched private/group text for plugins
  • Publishing an OpenAPI enum for llm_access.group
  • Save-time rejection of old mention / prefix_or_mention values

Implementation notes

Admit sources stay on route_turn / _llm_gate. Mentions are not read for LLM admission. Stamp sites share one extras bool; there is no second LLM gate. Runtime code and tests have zero adapter_preconfigured hits.

Bilingual docs updated under docs/zh/ and docs/en/ (group-wake, astrbot-config, architecture, faq, platform-settings). AGENTS.md and the archify/audit-product skill bullets now say mention is not a wake policy.

Validation

uv run ruff format --check <touched Python>
uv run ruff check <touched Python>
uv run pytest tests/unit/test_turn_router.py tests/unit/test_waking_check_stage.py tests/unit/test_turn_routing_current.py tests/unit/test_aiocqhttp_adapter.py tests/unit/platform/test_discord_adapter.py tests/unit/platform/test_wecom_ai_bot_adapter.py tests/unit/test_platform_manager.py tests/unit/test_group_chat_context_wiring.py tests/unit/test_webchat_message_parts.py tests/unit/test_cron_manager.py::test_cron_message_event_stamps_explicit_surface
cd docs && pnpm run docs:build
make check-md

208 pytest items passed. adapter_preconfigured has no runtime or test hits.

Compatibility and risk

Breaking for operators on the previous default private=open: IM DMs now need an LLM prefix unless they opt into open. Existing on-disk private=open is kept. Saved group=mention / prefix_or_mention silently become prefix at runtime. Dashboard WebChat is covered by explicit_surface. Empty @ no longer talks to the model.

Checklist

  • A Feature request Issue exists for large work, or this is a small, obvious addition.
  • The change is focused and does not include unrelated refactoring.
  • I added or updated tests, or explained why tests are not practical.
  • User-visible behavior updates both docs/zh/ and docs/en/.
  • OpenAPI, generated client, docs/public/openapi.json, and tests change together when routes or schemas change.
  • No secrets committed. Runtime Python deps update pyproject.toml, requirements.txt, and uv.lock together.
  • I did not restore legacy shims, Python <3.14 fallbacks, or upstream publish/docs URLs as fork artifacts.
  • Breaking API or behavior changes use ! and a BREAKING CHANGE: footer.
  • I will not merge this PR myself. Merge needs a human maintainer review plus a separate AI-assisted review (AI_POLICY.md).
  • AI use follows AI_POLICY.md. Keep exactly one author note below. Do not fabricate the other.

Agent note

Implemented Issue #58 Card A (admit-source redesign) on route_turn without splitting WakingCheckStage. Touched the router, waking stage, defaults/metadata, Discord/WebChat/WeCom/cron/manager stamp sites, handle_empty_mention, bilingual docs, and AGENTS/skill copy. Ran the PLAN pytest set (208 passed), Ruff on touched Python, docs:build, and make check-md. Residual risk: existing profiles that already stored private=open keep chatting without a prefix; operators who set group=mention fall back to prefix with no save-time warning. Tools: local git/pytest/ruff/prettier/vitepress/gh.

Admit a new built-in LLM turn only from command match, LLM prefix,
continuation, explicit reply_to_bot, or extras explicit_surface.
Mentions stay message-chain markers and no longer extra-admit.

BREAKING CHANGE: default llm_access.private is prefix; group mention and prefix_or_mention fall back to prefix; extras adapter_preconfigured is replaced by explicit_surface; empty @ no longer request_llm
Fixes #58
AI-Generated: true
Generated-At: 2026-09-06T11:26:19Z

@BegoniaHe BegoniaHe left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

AI-assisted review

Verdict: request changes (posted as Comment because GitHub rejects REQUEST_CHANGES on the authoring account).

Card A of #58 is mostly implemented on route_turn / _llm_gate as specified: default private=prefix, mention is no longer an LLM gate, Discord user/role mentions no longer stamp extras, WebChat uses explicit_surface without injecting /, plugin EventMessageType.ALL still activates, and zh/en docs plus AGENTS.md match. Commit footer, Agent note, and the listed pytest set are in good shape. Most CI jobs are green; coverage was still pending at review time.

This cannot land until the prefix-only wait follow-up is actually admitted. The old waiter re-inserted Mention(self) so the next message would pass mention policy. That injection is gone, and the resubmitted hello has no prefix, no explicit_surface, and no inbound-coalesce window (has_open_window is TurnWindowManager only, default off). Under the new default it is dropped. The new test only asserts that Mention is not synthesized.

Blocking

  1. Prefix-only wait follow-up is dropped under default prefix. handle_empty_mention still waits on a lone / and submit()s the next event through the bounded queue. WakingCheckStage then runs route_turn on plain hello. That is not an LLM admit source. Either stamp explicit_surface (or an equivalent one-shot continuation) on the resubmitted event, or delete the wait path. Add a test that the follow-up passes route_turn / WakingCheckStage under default config.

Should fix

  1. explicit_surface skips _llm_gate, including off. WebChat, WeCom AI Bot, Discord slash, cron, and PlatformManager.create_event(is_wake=True) still run the built-in LLM when the operator set private=off / group=off. If off is the kill switch, honor it after command matching. If synthesized surfaces are exempt, document that and add off + explicit_surface tests.

  2. Dashboard copy and two config keys are now lies. empty_mention_waiting is unread. ignore_at_all has no runtime reader after MentionAll extra-admit was removed. CONFIG_METADATA_* hints and dashboard/src/i18n/locales/{zh-CN,en-US}/features/config-metadata.json still describe “只 @ 机器人是否触发等待” / “Trigger Waiting on Mention-only Messages” and “忽略 @ 全体成员”. FAQ still lists ignore_at_all as a reason the bot might not reply. Gate the remaining wait on a live flag, or hide/remove the dead keys and update i18n/hints/FAQ together.

Nits

  1. WakeReason.MENTION_BOT / MENTION_ALL / PRIVATE_DEFAULT / PREFIX are never produced. Drop them or map route_turn reasons onto this enum so _detect_wake does not silently skip unknown strings.
  2. Private _llm_gate still checks blocked_by_other_mention, which _first_mention_is_other never sets for DMs.
  3. test_detect_wake_behavior_matrix still parametrizes group=mention and ignore_at_all; those rows are tautological now and hide the missing follow-up coverage.

Process

#58 is a breaking feature. GOVERNANCE.md wants maintainer agreement before the PR. The Issue has no second-maintainer GO. This review is the AI-assisted review; it is not a human maintainer approval and must not be merged on that basis.

Residual risk already named in the Agent note is real: on-disk private=open stays open; saved group=mention silently becomes prefix with no save-time warning.

yield event.plain_result("想要问什么呢?😄")

async def empty_mention_waiter(
async def prefix_only_waiter(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This waiter still submit()s the next inbound event through the bounded queue. The follow-up is a plain hello with no LLM prefix, no explicit_surface, and no coalesce window. Old code inserted Mention(self) so mention policy would admit it. After this PR, default prefix drops that follow-up, so the 60s wait cannot collect the actual prompt.

Stamp a one-shot admit on new_event (or stop waiting). Cover it with route_turn / WakingCheckStage, not only “no Mention synthesized”.

)

if inp.adapter_preconfigured:
if inp.explicit_surface:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

explicit_surface returns should_run_llm=True without _llm_gate. Command matching still wins, which is correct for WebChat /help, but private=off / group=off cannot stop WebChat, WeCom AI Bot, Discord slash, or cron.

Honor off here unless synthesized surfaces are documented as exempt; add that matrix test either way.

@BegoniaHe BegoniaHe self-assigned this Sep 6, 2026
Store the resolved schema defaults as default_config so save-time
unknown-key stripping does not drop plugin fields against DEFAULT_CONFIG.

AI-Generated: true
Generated-At: 2026-09-06T12:08:08Z
Stamp explicit_surface on the prefix-only waiter follow-up, restore
empty_mention_waiting as the wait gate, and keep private/group off as
the kill switch for synthesized surfaces.

AI-Generated: true
Generated-At: 2026-09-06T12:08:16Z

@BegoniaHe BegoniaHe left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

AI-assisted review

Verdict: request changes (posted as Comment because GitHub rejects REQUEST_CHANGES on the authoring account).

This is an independent review of dc3178d02, not the authoring write-up. The first-commit review's two blocking items are addressed in route_turn / _llm_gate and the prefix-only waiter stamp. Card A of #58 is otherwise in good shape: default private=prefix, mention is not an LLM gate, Discord user/role mentions no longer stamp extras, WebChat uses explicit_surface without injecting /, plugin EventMessageType.ALL still activates, zh/en docs plus AGENTS.md match, and CI on this SHA is green.

This still cannot land while off is documented as the kill switch but the prefix-only plugin path starts a new LLM turn anyway.

Blocking

  1. Prefix-only wait still calls request_llm when llm_access is off. handle_empty_mention is an EventMessageType.ALL handler. It does not read llm_access. A lone / under private=off / group=off still yields event.request_llm(...) (courtesy prompt) and still waits 60s. ProcessStage runs that plugin ProviderRequest even when should_run_llm is false. The follow-up stamp is then correctly dropped by route_turn, so the operator sees one LLM reply after they turned the model off. Docs (docs/zh/use/group-wake.md, docs/en/use/group-wake.md) say off blocks a new turn and that the wait path honors it. Gate the wait and the courtesy request_llm on the same mode check, and add a test that / + off does not call request_llm or wait_for.

Should fix

  1. Lone / is also an llm_prefix hit. catalog.resolve("") is UNKNOWN_ROOT, so route_turn("/") falls through to _llm_gate and returns should_run_llm=True with empty message_str. Confirmed locally against this SHA. Default empty_mention_waiting_need_reply=true hides it because the plugin request_llm wins and ProcessStage skips the built-in agent. If waiting is off, / still goes to the model as an empty prompt. Prefix-only should be owned by the waiter, not by _llm_gate. Add route_turn("/") coverage.

  2. Unrelated config commit is in this PR. d819e796c (fix(config): persist plugin schema keys on dashboard save) is a real bugfix and is tested, but it is not Card A, not in the PR body, and not required for the LLM-access change. Split it or say why it has to land here.

Nits

  1. Waiter follow-up is covered in pieces (plugin set_extra, route_turn explicit_surface, process() on a Mention-only chain). A WakingCheckStage.process() case for [Plain("hello")] + explicit_surface under default prefix would match the actual resubmit.
  2. _detect_wake still duplicates process() routing and is test-only.
  3. ignore_at_all is now invisible and documented as a persisted no-op. Acceptable residual; delete it in a later cleanup rather than keep a Dashboard lie.

Process

#58 is a breaking feature. GOVERNANCE.md wants maintainer agreement before the PR. The Issue still has no second-maintainer GO. This review is the AI-assisted review; it is not a human maintainer approval and must not be merged on that basis.

Residual risk already named in the Agent note is still real: on-disk private=open stays open; saved group=mention silently becomes prefix with no save-time warning.

Prior inline threads on the waiter stamp and explicit_surface/off in route_turn are addressed on this SHA.

@@ -78,7 +74,7 @@ async def handle_empty_mention(self, event: AstrMessageEvent):

yield event.request_llm(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

request_llm here is a plugin ProviderRequest. ProcessStage runs it whenever this handler is activated, independent of should_run_llm.

cfg is already loaded. If llm_access.private / group is off for this UMO, return before the courtesy prompt and before wait_for. Docs now call off the kill switch for this wait path; the follow-up stamp honors it, this yield does not.

Cover / + off so request_llm and wait_for are not called.

False,
)

llm_ok, reasons = _llm_gate(inp, blocked_by_other_mention)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

resolve("") is UNKNOWN_ROOT, so a lone / is not stopped as a command and _llm_gate treats it as llm_prefix with empty message_str.

Default waiting + need_reply masks this because the plugin request_llm wins. If empty_mention_waiting is false, / still starts a built-in turn with an empty prompt. Prefix-only should not admit here; the waiter owns that UX.

Add route_turn("/") under default prefix.

Do not admit a bare LLM prefix as an empty prompt. Skip the prefix-only
wait and courtesy request_llm when private or group access is off.

AI-Generated: true
Generated-At: 2026-09-06T12:27:19Z
@BegoniaHe
BegoniaHe merged commit e0fa1d5 into master Sep 6, 2026
21 checks passed
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.

[feat] require explicit prefixes for built-in LLM access

1 participant