diff --git a/plugins/Runner/LocalAgent/components/runner/default.yaml b/plugins/Runner/LocalAgent/components/runner/default.yaml
index cdf045c50..46a44619d 100644
--- a/plugins/Runner/LocalAgent/components/runner/default.yaml
+++ b/plugins/Runner/LocalAgent/components/runner/default.yaml
@@ -310,6 +310,7 @@ spec:
skill_authoring: true
interrupt: true
steering: true
+ interactions: true
permissions:
models:
- count_tokens
@@ -324,6 +325,8 @@ spec:
- retrieve
history:
- page
+ interactions:
+ - request
execution:
python:
path: default.py
diff --git a/plugins/Runner/LocalAgent/manifest.yaml b/plugins/Runner/LocalAgent/manifest.yaml
index 39b90e3d1..6c07f2482 100644
--- a/plugins/Runner/LocalAgent/manifest.yaml
+++ b/plugins/Runner/LocalAgent/manifest.yaml
@@ -3,7 +3,7 @@ kind: Plugin
metadata:
author: langbot-team
name: LocalAgent
- version: 0.2.4
+ version: 0.2.5
repository: https://github.com/langbot-app/LangBot/tree/master/plugins/Runner/LocalAgent
label:
en_US: Local Agent
diff --git a/plugins/Runner/LocalAgent/pyproject.toml b/plugins/Runner/LocalAgent/pyproject.toml
index a824c660a..2cbab7e75 100644
--- a/plugins/Runner/LocalAgent/pyproject.toml
+++ b/plugins/Runner/LocalAgent/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "langbot-local-agent"
-version = "0.2.4"
+version = "0.2.5"
description = "Built-in agent with model fallback, tools, and knowledge retrieval"
requires-python = ">=3.11,<4.0"
dependencies = [
diff --git a/src/langbot/pkg/agent/runner/orchestrator.py b/src/langbot/pkg/agent/runner/orchestrator.py
index 028640d36..d47a3a481 100644
--- a/src/langbot/pkg/agent/runner/orchestrator.py
+++ b/src/langbot/pkg/agent/runner/orchestrator.py
@@ -5,6 +5,7 @@ from __future__ import annotations
import time
import asyncio
import contextlib
+import re
import copy
import typing
from uuid import uuid4
@@ -50,6 +51,14 @@ from ...telemetry.execution import close_trace as close_execution_trace
from ...telemetry.execution import record as record_execution
+def _visible_auto_reply_text(content: str) -> str:
+ visible = re.sub(r'.*?(?:|$)', '', content, flags=re.IGNORECASE | re.DOTALL)
+ for size in range(min(7, len(visible)), 0, -1):
+ if any(tag.startswith(visible[-size:].lower()) for tag in ('', '')):
+ return visible[:-size]
+ return visible
+
+
class AgentRunOrchestrator:
"""Coordinate one Runner execution.
@@ -266,6 +275,7 @@ class AgentRunOrchestrator:
auto_reply_cumulative_text = ''
auto_reply_prefix = ''
auto_reply_deferred = False
+ explicit_event_reply_sent = False
explicit_reply_text: str | None = None
pending_explicit_replies: dict[str, str] = {}
# A run reached without a platform ingress (WebUI debug, service API)
@@ -410,6 +420,8 @@ class AgentRunOrchestrator:
and tool_definition.api == 'send_message'
and not result_data.get('error')
)
+ if auto_reply_deferred and result_data.get('tool_name') == 'event_reply':
+ explicit_event_reply_sent = True
explicit_reply_text = pending_explicit_replies.pop(
str(result_data.get('tool_call_id') or ''), None
)
@@ -500,15 +512,17 @@ class AgentRunOrchestrator:
async with binding_for(execution_query).lock:
result.attachments = exported_message(execution_query, file_ids, consume=True)
- if auto_reply_enabled and result is not None:
+ if auto_reply_enabled and not explicit_event_reply_sent and result is not None:
if result_type == 'message.delta' and isinstance(result, provider_message.MessageChunk):
cumulative_text = result.all_content or result.content
if isinstance(cumulative_text, str):
+ reply_text = _visible_auto_reply_text(cumulative_text)
if auto_reply_prefix and cumulative_text.startswith(auto_reply_prefix):
- reply_text = cumulative_text[len(auto_reply_prefix) :]
+ visible_prefix = _visible_auto_reply_text(auto_reply_prefix)
+ if reply_text.startswith(visible_prefix):
+ reply_text = reply_text[len(visible_prefix) :]
else:
auto_reply_prefix = ''
- reply_text = cumulative_text
auto_reply_cumulative_text = cumulative_text
auto_reply_stream_text = reply_text
if reply_text.strip() and not auto_reply_deferred:
@@ -523,8 +537,11 @@ class AgentRunOrchestrator:
elif has_completed_message:
reply_text = result.content
if isinstance(reply_text, str):
- if auto_reply_prefix and reply_text.startswith(auto_reply_prefix):
- reply_text = reply_text[len(auto_reply_prefix) :]
+ reply_text = _visible_auto_reply_text(reply_text)
+ if auto_reply_prefix and result.content.startswith(auto_reply_prefix):
+ visible_prefix = _visible_auto_reply_text(auto_reply_prefix)
+ if reply_text.startswith(visible_prefix):
+ reply_text = reply_text[len(visible_prefix) :]
if reply_text.strip() and (
not auto_reply_deferred
or explicit_reply_text is None
diff --git a/tests/unit_tests/agent/test_orchestrator_integration.py b/tests/unit_tests/agent/test_orchestrator_integration.py
index a49dc7702..bf1ba5bea 100644
--- a/tests/unit_tests/agent/test_orchestrator_integration.py
+++ b/tests/unit_tests/agent/test_orchestrator_integration.py
@@ -1272,8 +1272,9 @@ class TestQueryEntrySessionQueryId:
@pytest.mark.asyncio
@pytest.mark.parametrize('event_type', ['message.received', 'interaction.submitted'])
@pytest.mark.parametrize('completed_message', [False, True])
+@pytest.mark.parametrize('reasoning', [False, True])
async def test_direct_agent_streams_output_to_the_originating_platform(
- clean_agent_state, event_type, completed_message
+ clean_agent_state, event_type, completed_message, reasoning
):
from langbot.pkg.agent.runner.host_models import (
AgentBinding,
@@ -1286,14 +1287,16 @@ async def test_direct_agent_streams_output_to_the_originating_platform(
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
+ first_content = 'Checking the requesthello' if reasoning else 'hello'
+ final_content = 'Checking the requesthello world' if reasoning else 'hello world'
runner_results = [
{
'type': 'message.delta',
'data': {
'chunk': {
'role': 'assistant',
- 'content': 'hello',
- 'all_content': 'hello',
+ 'content': first_content,
+ 'all_content': first_content,
}
},
},
@@ -1302,7 +1305,7 @@ async def test_direct_agent_streams_output_to_the_originating_platform(
runner_results.append(
{
'type': 'message.completed',
- 'data': {'message': {'role': 'assistant', 'content': 'hello world'}},
+ 'data': {'message': {'role': 'assistant', 'content': final_content}},
}
)
runner_results.append({'type': 'run.completed', 'data': {'finish_reason': 'stop'}})
@@ -1363,7 +1366,9 @@ async def test_direct_agent_streams_output_to_the_originating_platform(
)
]
- assert [item.content for item in results] == (['hello', 'hello world'] if completed_message else ['hello'])
+ assert [item.content for item in results] == (
+ [first_content, final_content] if completed_message else [first_content]
+ )
if event_type == 'message.received':
adapter.create_message_card.assert_awaited_once()
chunks = adapter.reply_message_chunk.await_args_list
@@ -1398,6 +1403,27 @@ async def test_direct_agent_delivers_text_after_card_without_repeating_explicit_
results = []
if tool_name == 'present_interactive_card':
results.append({'type': 'message.delta', 'data': {'chunk': {'role': 'assistant', 'content': 'Before'}}})
+ else:
+ results.extend(
+ [
+ {
+ 'type': 'message.delta',
+ 'data': {'chunk': {'role': 'assistant', 'content': 'Checking available tools'}},
+ },
+ {
+ 'type': 'tool.call.completed',
+ 'data': {'tool_call_id': 'skill-1', 'tool_name': 'list_skills', 'result': {'skills': []}},
+ },
+ {
+ 'type': 'message.delta',
+ 'data': {'chunk': {'role': 'assistant', 'content': 'Checking available tools'}},
+ },
+ ]
+ )
results.extend(
[
{
@@ -1412,6 +1438,13 @@ async def test_direct_agent_delivers_text_after_card_without_repeating_explicit_
)
if tool_name == 'present_interactive_card':
results.append({'type': 'message.delta', 'data': {'chunk': {'role': 'assistant', 'content': 'BeforeAfter'}}})
+ else:
+ results.append(
+ {
+ 'type': 'message.delta',
+ 'data': {'chunk': {'role': 'assistant', 'content': 'Checking available toolsAfter'}},
+ }
+ )
results.extend(
[
{'type': 'message.completed', 'data': {'message': {'role': 'assistant', 'content': 'After'}}},