diff --git a/docs/edge/ar/guides/flows/conversational-flows.mdx b/docs/edge/ar/guides/flows/conversational-flows.mdx
index e7d4253b0..b0786414c 100644
--- a/docs/edge/ar/guides/flows/conversational-flows.mdx
+++ b/docs/edge/ar/guides/flows/conversational-flows.mdx
@@ -612,6 +612,44 @@ from crewai.flow import (
)
```
+## تجريبي: أوامر `/btw`
+
+
+`/btw` إضافة تجريبية. تتجاهل التدفقات المحادثية أسطر الأوامر حتى تفعّلها صراحة، وقد يتغيّر الـ API.
+
+
+يعامل كل `handle_turn()` سطر المستخدم كتشغيل جديد للرسم. `/btw` قناة جانبية تتيح لك إدخال ملاحظة توجيه — أو فرض مسار — **دون** إلحاق رسالة مستخدم أو تشغيل الرسم.
+
+```python
+from crewai.experimental.conversation_commands import btw_commands
+from crewai.flow import ConversationConfig, ConversationState, Flow, listen
+
+
+@btw_commands
+@ConversationConfig()
+class SupportFlow(Flow[ConversationState]):
+ @listen("RESEARCH")
+ def handle_research(self) -> str:
+ """Fresh web research."""
+ reply = "I would research that."
+ self.append_assistant_message(reply)
+ return reply
+```
+
+| السطر | الأثر |
+|------|--------|
+| `/btw keep answers under 20 words` | يحفظ ملاحظة توجيه. لا تُسجَّل جولة مستخدم. تظهر الملاحظة لاحقاً في مطالبات `converse` والموجّه. |
+| `/btw route RESEARCH` | يفرض المسار على **الجولة التالية** ثم يعود للتوجيه العادي. |
+| `/btw stay RESEARCH` أو `/btw route RESEARCH persist` | يواصل فرض ذلك المسار حتى `/btw clear`. |
+| `/btw clear` | يحذف الملاحظات والمسارات المفروضة. |
+| `/btw` أو `/btw show` | يعرض التوجيه الحالي. |
+| `/help` | يسرد الأوامر. |
+| `What's the weather /btw keep it to one sentence` | يطبّق الأمر ثم يشغّل النص المتبقي كجولة مستخدم. |
+
+لتدفق أنشأته مسبقاً (بما في ذلك `Flow.from_declaration(...)`)، استدعِ `enable_btw_commands(flow)` بدل المزيّن.
+
+أسطر `/btw` المستقلة تعيد إقراراً ولا تستدعي `kickoff()`. ولأن `chat()` وواجهة CLI TUI تغلفان `handle_turn()`، تلتقطان الإضافة تلقائياً بعد تثبيتها. التدفقات بدون الإضافة ما زالت تعامل `/btw …` كرسالة مستخدم عادية.
+
## مراجع
- [إتقان إدارة حالة Flow](/ar/guides/flows/mastering-flow-state)
diff --git a/docs/edge/en/guides/flows/conversational-flows.mdx b/docs/edge/en/guides/flows/conversational-flows.mdx
index c0ae83d74..7d4ba2e8f 100644
--- a/docs/edge/en/guides/flows/conversational-flows.mdx
+++ b/docs/edge/en/guides/flows/conversational-flows.mdx
@@ -613,6 +613,44 @@ from crewai.flow import (
)
```
+## Experimental: `/btw` commands
+
+
+`/btw` is an experimental add-on. Conversational flows ignore slash lines until you opt in, and the API may change.
+
+
+Each `handle_turn()` treats the user line as a new graph run. `/btw` is a side-channel so you can interject a steering note — or force a route — **without** appending a user message or running the graph.
+
+```python
+from crewai.experimental.conversation_commands import btw_commands
+from crewai.flow import ConversationConfig, ConversationState, Flow, listen
+
+
+@btw_commands
+@ConversationConfig()
+class SupportFlow(Flow[ConversationState]):
+ @listen("RESEARCH")
+ def handle_research(self) -> str:
+ """Fresh web research."""
+ reply = "I would research that."
+ self.append_assistant_message(reply)
+ return reply
+```
+
+| Line | Effect |
+|------|--------|
+| `/btw keep answers under 20 words` | Persist a steering note. No user turn is recorded. Later `converse` and router prompts include the note. |
+| `/btw route RESEARCH` | Force the **next** turn onto that route, then resume normal routing. |
+| `/btw stay RESEARCH` or `/btw route RESEARCH persist` | Keep forcing that route until `/btw clear`. |
+| `/btw clear` | Drop notes and forced routes. |
+| `/btw` or `/btw show` | Print the current steering. |
+| `/help` | List commands. |
+| `What's the weather /btw keep it to one sentence` | Apply the command, then run the leftover text as the user turn. |
+
+For a flow you already constructed (including `Flow.from_declaration(...)`), call `enable_btw_commands(flow)` instead of the decorator.
+
+Standalone `/btw` lines return an acknowledgement and do not call `kickoff()`. Because `chat()` and the CLI TUI wrap `handle_turn()`, they pick the add-on up automatically once it is installed. Flows without the add-on still treat `/btw …` as a normal user message.
+
## See also
- [Mastering Flow State Management](/en/guides/flows/mastering-flow-state) — persistence, Pydantic state, `@persist`
diff --git a/docs/edge/ko/guides/flows/conversational-flows.mdx b/docs/edge/ko/guides/flows/conversational-flows.mdx
index d18ba3ec8..ed2a2b6eb 100644
--- a/docs/edge/ko/guides/flows/conversational-flows.mdx
+++ b/docs/edge/ko/guides/flows/conversational-flows.mdx
@@ -608,6 +608,44 @@ from crewai.flow import (
)
```
+## 실험적: `/btw` 명령
+
+
+`/btw`는 실험적 애드온입니다. 대화형 Flow는 명시적으로 켜기 전까지 슬래시 줄을 무시하며, API는 바뀔 수 있습니다.
+
+
+각 `handle_turn()`은 사용자 줄을 새로운 그래프 실행으로 취급합니다. `/btw`는 사이드 채널이므로 사용자 메시지를 추가하거나 그래프를 실행하지 **않고** 스티어링 노트(또는 강제 라우트)를 끼워 넣을 수 있습니다.
+
+```python
+from crewai.experimental.conversation_commands import btw_commands
+from crewai.flow import ConversationConfig, ConversationState, Flow, listen
+
+
+@btw_commands
+@ConversationConfig()
+class SupportFlow(Flow[ConversationState]):
+ @listen("RESEARCH")
+ def handle_research(self) -> str:
+ """Fresh web research."""
+ reply = "I would research that."
+ self.append_assistant_message(reply)
+ return reply
+```
+
+| 줄 | 효과 |
+|------|--------|
+| `/btw keep answers under 20 words` | 스티어링 노트를 유지합니다. 사용자 턴은 기록되지 않습니다. 이후 `converse`와 라우터 프롬프트에 노트가 포함됩니다. |
+| `/btw route RESEARCH` | **다음** 턴만 해당 라우트로 강제하고 이후에는 일반 라우팅으로 돌아갑니다. |
+| `/btw stay RESEARCH` 또는 `/btw route RESEARCH persist` | `/btw clear` 전까지 그 라우트를 계속 강제합니다. |
+| `/btw clear` | 노트와 강제 라우트를 제거합니다. |
+| `/btw` 또는 `/btw show` | 현재 스티어링을 보여 줍니다. |
+| `/help` | 명령 목록을 출력합니다. |
+| `What's the weather /btw keep it to one sentence` | 명령을 적용한 뒤 남은 텍스트를 사용자 턴으로 실행합니다. |
+
+이미 만든 Flow( `Flow.from_declaration(...)` 포함)에는 데코레이터 대신 `enable_btw_commands(flow)`를 호출하세요.
+
+단독 `/btw` 줄은 확인 메시지를 반환하며 `kickoff()`를 호출하지 않습니다. `chat()`과 CLI TUI는 `handle_turn()`을 감싸므로, 애드온이 설치된 뒤에는 자동으로 동작합니다. 애드온이 없는 Flow는 `/btw …`를 일반 사용자 메시지로 취급합니다.
+
## 참고
- [Flow 상태 관리 마스터하기](/ko/guides/flows/mastering-flow-state)
diff --git a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx
index 38bf02d4c..b10a91c18 100644
--- a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx
+++ b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx
@@ -613,6 +613,44 @@ from crewai.flow import (
)
```
+## Experimental: comandos `/btw`
+
+
+`/btw` é um extra experimental. Flows conversacionais ignoram linhas com barra até você ativar o extra, e a API pode mudar.
+
+
+Cada `handle_turn()` trata a linha do usuário como uma nova execução do grafo. `/btw` é um canal lateral para intercalar uma nota de direção — ou forçar uma rota — **sem** anexar uma mensagem de usuário nem rodar o grafo.
+
+```python
+from crewai.experimental.conversation_commands import btw_commands
+from crewai.flow import ConversationConfig, ConversationState, Flow, listen
+
+
+@btw_commands
+@ConversationConfig()
+class SupportFlow(Flow[ConversationState]):
+ @listen("RESEARCH")
+ def handle_research(self) -> str:
+ """Fresh web research."""
+ reply = "I would research that."
+ self.append_assistant_message(reply)
+ return reply
+```
+
+| Linha | Efeito |
+|------|--------|
+| `/btw keep answers under 20 words` | Persiste uma nota de direção. Nenhum turno de usuário é gravado. As notas entram depois nos prompts de `converse` e do router. |
+| `/btw route RESEARCH` | Força o **próximo** turno para essa rota e depois volta ao roteamento normal. |
+| `/btw stay RESEARCH` ou `/btw route RESEARCH persist` | Continua forçando essa rota até `/btw clear`. |
+| `/btw clear` | Remove notas e rotas forçadas. |
+| `/btw` ou `/btw show` | Mostra a direção atual. |
+| `/help` | Lista os comandos. |
+| `What's the weather /btw keep it to one sentence` | Aplica o comando e executa o texto restante como turno do usuário. |
+
+Para um flow já construído (incluindo `Flow.from_declaration(...)`), chame `enable_btw_commands(flow)` em vez do decorator.
+
+Linhas `/btw` isoladas devolvem um acknowledgement e não chamam `kickoff()`. Como `chat()` e o TUI da CLI encapsulam `handle_turn()`, o extra passa a valer automaticamente depois de instalado. Flows sem o extra ainda tratam `/btw …` como mensagem normal de usuário.
+
## Veja também
- [Dominando o Gerenciamento de Estado em Flows](/pt-BR/guides/flows/mastering-flow-state) — persistência, estado Pydantic, `@persist`
diff --git a/lib/crewai/src/crewai/experimental/__init__.py b/lib/crewai/src/crewai/experimental/__init__.py
index ce2082f05..802b88875 100644
--- a/lib/crewai/src/crewai/experimental/__init__.py
+++ b/lib/crewai/src/crewai/experimental/__init__.py
@@ -20,6 +20,18 @@ from crewai.flow.conversational import (
_LAZY_FROM_AGENT_EXECUTOR = {"AgentExecutor", "CrewAgentExecutorFlow"}
+_LAZY_FROM_CONVERSATION_COMMANDS = {
+ "BtwAction",
+ "BtwKind",
+ "BtwSteering",
+ "HELP_TEXT",
+ "ParsedBtwLine",
+ "btw_commands",
+ "enable_btw_commands",
+ "get_btw_steering",
+ "parse_btw_line",
+}
+
_LAZY_FROM_EVALUATION = {
"AgentEvaluationResult",
"AgentEvaluator",
@@ -65,6 +77,13 @@ def __getattr__(name: str) -> Any:
globals()[attr] = getattr(_evaluation_mod, attr)
return globals()[name]
+ if name in _LAZY_FROM_CONVERSATION_COMMANDS:
+ from crewai.experimental import conversation_commands as _btw_mod
+
+ for attr in _LAZY_FROM_CONVERSATION_COMMANDS:
+ globals()[attr] = getattr(_btw_mod, attr)
+ return globals()[name]
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -73,7 +92,12 @@ __all__ = [
"AgentEvaluator",
"AgentExecutor",
"AgentMessage",
+ "BtwAction",
+ "BtwKind",
+ "BtwSteering",
"BaseEvaluator",
+ "HELP_TEXT",
+ "ParsedBtwLine",
"ConversationConfig",
"ConversationEvent",
"ConversationMessage",
@@ -92,6 +116,10 @@ __all__ = [
"SemanticQualityEvaluator",
"ToolInvocationEvaluator",
"ToolSelectionEvaluator",
+ "btw_commands",
"create_default_evaluator",
"create_evaluation_callbacks",
+ "enable_btw_commands",
+ "get_btw_steering",
+ "parse_btw_line",
]
diff --git a/lib/crewai/src/crewai/experimental/conversation_commands/__init__.py b/lib/crewai/src/crewai/experimental/conversation_commands/__init__.py
new file mode 100644
index 000000000..a33af860f
--- /dev/null
+++ b/lib/crewai/src/crewai/experimental/conversation_commands/__init__.py
@@ -0,0 +1,54 @@
+"""Experimental ``/btw`` commands for conversational Flows.
+
+Conversational turns treat every user line as a new graph run. This add-on
+lets you interject a side instruction that steers routing and replies
+without appending a user message. It is opt-in and not part of the stable
+``handle_turn`` / ``chat`` contract.
+
+Example::
+
+ from crewai.experimental.conversation_commands import btw_commands
+ from crewai.flow import ConversationConfig, ConversationState, Flow, listen
+
+
+ @btw_commands
+ @ConversationConfig()
+ class SupportFlow(Flow[ConversationState]):
+ @listen("RESEARCH")
+ def handle_research(self) -> str:
+ return "researching"
+
+ flow = SupportFlow()
+ flow.handle_turn("/btw keep answers under 20 words")
+ flow.handle_turn("/btw route RESEARCH")
+ flow.handle_turn("latest AI news")
+"""
+
+from crewai.experimental.conversation_commands.addon import (
+ btw_commands,
+ enable_btw_commands,
+)
+from crewai.experimental.conversation_commands.parser import (
+ HELP_TEXT,
+ BtwAction,
+ BtwKind,
+ ParsedBtwLine,
+ parse_btw_line,
+)
+from crewai.experimental.conversation_commands.steering import (
+ BtwSteering,
+ get_btw_steering,
+)
+
+
+__all__ = [
+ "HELP_TEXT",
+ "BtwAction",
+ "BtwKind",
+ "BtwSteering",
+ "ParsedBtwLine",
+ "btw_commands",
+ "enable_btw_commands",
+ "get_btw_steering",
+ "parse_btw_line",
+]
diff --git a/lib/crewai/src/crewai/experimental/conversation_commands/addon.py b/lib/crewai/src/crewai/experimental/conversation_commands/addon.py
new file mode 100644
index 000000000..e50aacbbc
--- /dev/null
+++ b/lib/crewai/src/crewai/experimental/conversation_commands/addon.py
@@ -0,0 +1,192 @@
+"""Opt-in installer for experimental ``/btw`` conversational commands."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterator
+from functools import wraps
+from typing import Any, TypeVar, cast, overload
+
+from crewai.experimental.conversation_commands.parser import parse_btw_line
+from crewai.experimental.conversation_commands.steering import (
+ ENABLED_ATTR,
+ apply_btw_action,
+ get_btw_steering,
+)
+from crewai.types.streaming import StreamFrame, StreamSession
+
+
+T = TypeVar("T")
+
+
+class _Intercept:
+ __slots__ = ("consumed", "reply", "user_message")
+
+ def __init__(
+ self,
+ *,
+ consumed: bool,
+ user_message: str | None,
+ reply: str | None = None,
+ ) -> None:
+ self.consumed = consumed
+ self.user_message = user_message
+ self.reply = reply
+
+
+def _intercept(flow: Any, message: str) -> _Intercept:
+ parsed = parse_btw_line(message)
+ if parsed.action is None:
+ return _Intercept(consumed=False, user_message=parsed.user_message)
+
+ reply = apply_btw_action(flow, parsed.action)
+ if parsed.user_message:
+ return _Intercept(
+ consumed=False,
+ user_message=parsed.user_message,
+ reply=reply,
+ )
+ return _Intercept(consumed=True, user_message=None, reply=reply)
+
+
+def _ack_stream(reply: str) -> StreamSession[str]:
+ def frames() -> Iterator[StreamFrame]:
+ return iter(())
+
+ session: StreamSession[str] = StreamSession(sync_iterator=frames())
+ session._set_result(reply)
+ return session
+
+
+def _already_enabled(target: Any) -> bool:
+ if getattr(target, ENABLED_ATTR, False):
+ return True
+ if not isinstance(target, type) and getattr(type(target), ENABLED_ATTR, False):
+ return True
+ return False
+
+
+def _wrap_handle_turn(original: Callable[..., Any]) -> Callable[..., Any]:
+ @wraps(original)
+ def handle_turn(self: Any, message: str, *args: Any, **kwargs: Any) -> Any:
+ decision = _intercept(self, message)
+ if decision.consumed:
+ return decision.reply
+ return original(self, decision.user_message, *args, **kwargs)
+
+ return handle_turn
+
+
+def _wrap_stream_turn(original: Callable[..., Any]) -> Callable[..., Any]:
+ @wraps(original)
+ def stream_turn(self: Any, message: str, *args: Any, **kwargs: Any) -> Any:
+ decision = _intercept(self, message)
+ if decision.consumed:
+ return _ack_stream(decision.reply or "")
+ return original(self, decision.user_message, *args, **kwargs)
+
+ return stream_turn
+
+
+def _wrap_route_turn(original: Callable[..., Any]) -> Callable[..., Any]:
+ @wraps(original)
+ def route_turn(self: Any, context: dict[str, Any]) -> str | None:
+ forced = get_btw_steering(self).consume_forced_route()
+ if forced:
+ return forced
+ return original(self, context)
+
+ return route_turn
+
+
+def _wrap_build_router_context(original: Callable[..., Any]) -> Callable[..., Any]:
+ @wraps(original)
+ def build_router_context(self: Any) -> dict[str, Any]:
+ return get_btw_steering(self).apply_to_router_context(original(self))
+
+ return build_router_context
+
+
+def _wrap_resolve_system_prompt(original: Callable[..., Any]) -> Callable[..., Any]:
+ @wraps(original)
+ def _resolve_system_prompt(self: Any) -> str | None:
+ return get_btw_steering(self).apply_to_system_prompt(original(self))
+
+ return _resolve_system_prompt
+
+
+def _wrap_class(cls: type[T]) -> type[T]:
+ cls.handle_turn = _wrap_handle_turn(cls.handle_turn) # type: ignore[attr-defined]
+ cls.stream_turn = _wrap_stream_turn(cls.stream_turn) # type: ignore[attr-defined]
+ cls.route_turn = _wrap_route_turn(cls.route_turn) # type: ignore[attr-defined]
+ cls.build_router_context = _wrap_build_router_context( # type: ignore[attr-defined]
+ cls.build_router_context
+ )
+ cls._resolve_system_prompt = _wrap_resolve_system_prompt( # type: ignore[attr-defined]
+ cls._resolve_system_prompt
+ )
+ setattr(cls, ENABLED_ATTR, True)
+ return cls
+
+
+def _bind_and_wrap(
+ flow: Any,
+ name: str,
+ wrapper: Callable[[Callable[..., Any]], Callable[..., Any]],
+) -> None:
+ original = getattr(type(flow), name)
+ wrapped = wrapper(original)
+
+ @wraps(original)
+ def bound(*args: Any, **kwargs: Any) -> Any:
+ return wrapped(flow, *args, **kwargs)
+
+ object.__setattr__(flow, name, bound)
+
+
+def _wrap_instance(flow: T) -> T:
+ target = cast(Any, flow)
+ _bind_and_wrap(target, "handle_turn", _wrap_handle_turn)
+ _bind_and_wrap(target, "stream_turn", _wrap_stream_turn)
+ _bind_and_wrap(target, "route_turn", _wrap_route_turn)
+ _bind_and_wrap(target, "build_router_context", _wrap_build_router_context)
+ _bind_and_wrap(target, "_resolve_system_prompt", _wrap_resolve_system_prompt)
+ object.__setattr__(target, ENABLED_ATTR, True)
+ return flow
+
+
+def enable_btw_commands(target: T) -> T:
+ """Install ``/btw`` interjections on a Flow class or instance.
+
+ Opt-in only: conversational flows ignore slash lines until this is
+ applied. Safe to call more than once.
+ """
+ if _already_enabled(target):
+ return target
+ if isinstance(target, type):
+ return _wrap_class(cast(type[T], target))
+ return _wrap_instance(target)
+
+
+@overload
+def btw_commands(flow_cls: type[T]) -> type[T]: ...
+
+
+@overload
+def btw_commands(flow_cls: None = None) -> Callable[[type[T]], type[T]]: ...
+
+
+def btw_commands(
+ flow_cls: type[T] | None = None,
+) -> type[T] | Callable[[type[T]], type[T]]:
+ """Class decorator that enables experimental ``/btw`` commands.
+
+ Use as ``@btw_commands`` or ``@btw_commands()`` above
+ ``@ConversationConfig(...)``.
+ """
+
+ def decorate(cls: type[T]) -> type[T]:
+ return enable_btw_commands(cls)
+
+ if flow_cls is not None:
+ return decorate(flow_cls)
+ return decorate
diff --git a/lib/crewai/src/crewai/experimental/conversation_commands/parser.py b/lib/crewai/src/crewai/experimental/conversation_commands/parser.py
new file mode 100644
index 000000000..cd6574247
--- /dev/null
+++ b/lib/crewai/src/crewai/experimental/conversation_commands/parser.py
@@ -0,0 +1,109 @@
+"""Parse ``/btw`` interjections from a conversational user line.
+
+A leading ``/btw`` is a side-channel command: it steers later turns and
+does not become the user utterance. Appending `` /btw …`` to a normal
+message applies the command and still runs the remaining text as a turn.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import Enum
+import re
+
+
+_LEADING_BTW = re.compile(r"^/btw(?:\s+|$)", re.IGNORECASE)
+_INLINE_BTW = re.compile(r"\s+/btw(?:\s+|$)", re.IGNORECASE)
+_HELP_LINES = frozenset({"/help", "/?"})
+
+HELP_TEXT = (
+ "Experimental /btw commands (side-channel; they do not become a user turn):\n"
+ " /btw Persist a steering note for later turns\n"
+ " /btw route