mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 15:24:06 +00:00
refactor: centralize cron session metadata keys
This commit is contained in:
parent
0e3a57b371
commit
d9d481bc15
@ -130,7 +130,7 @@ Instead, persist a readable automation trigger event, for example:
|
|||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": "Scheduled automation triggered: daily monitor\n\nCheck ...",
|
"content": "Scheduled automation triggered: daily monitor\n\nCheck ...",
|
||||||
"_automation_trigger": true,
|
"_automation_turn": true,
|
||||||
"automation_id": "abc123",
|
"automation_id": "abc123",
|
||||||
"automation_name": "daily monitor",
|
"automation_name": "daily monitor",
|
||||||
"automation_run_id": "abc123:1770000000000",
|
"automation_run_id": "abc123:1770000000000",
|
||||||
|
|||||||
@ -40,6 +40,7 @@ from nanobot.bus.runtime_events import (
|
|||||||
from nanobot.command import CommandContext, CommandRouter, register_builtin_commands
|
from nanobot.command import CommandContext, CommandRouter, register_builtin_commands
|
||||||
from nanobot.config.schema import AgentDefaults, ModelPresetConfig
|
from nanobot.config.schema import AgentDefaults, ModelPresetConfig
|
||||||
from nanobot.cron.automation import (
|
from nanobot.cron.automation import (
|
||||||
|
AUTOMATION_HISTORY_META,
|
||||||
automation_run_id,
|
automation_run_id,
|
||||||
automation_trigger,
|
automation_trigger,
|
||||||
defer_until_session_idle,
|
defer_until_session_idle,
|
||||||
@ -57,6 +58,7 @@ from nanobot.session.goal_state import (
|
|||||||
runner_wall_llm_timeout_s,
|
runner_wall_llm_timeout_s,
|
||||||
sustained_goal_active,
|
sustained_goal_active,
|
||||||
)
|
)
|
||||||
|
from nanobot.session.keys import UNIFIED_SESSION_KEY, session_key_for_channel
|
||||||
from nanobot.session.manager import Session, SessionManager
|
from nanobot.session.manager import Session, SessionManager
|
||||||
from nanobot.session.routing import persist_routing_context
|
from nanobot.session.routing import persist_routing_context
|
||||||
from nanobot.utils.document import extract_documents, reference_non_image_attachments
|
from nanobot.utils.document import extract_documents, reference_non_image_attachments
|
||||||
@ -78,8 +80,6 @@ if TYPE_CHECKING:
|
|||||||
from nanobot.cron.service import CronService
|
from nanobot.cron.service import CronService
|
||||||
|
|
||||||
|
|
||||||
UNIFIED_SESSION_KEY = "unified:default"
|
|
||||||
|
|
||||||
class TurnState(Enum):
|
class TurnState(Enum):
|
||||||
RESTORE = auto()
|
RESTORE = auto()
|
||||||
COMPACT = auto()
|
COMPACT = auto()
|
||||||
@ -522,12 +522,11 @@ class AgentLoop:
|
|||||||
"""Update context for all tools that need routing info."""
|
"""Update context for all tools that need routing info."""
|
||||||
from nanobot.agent.tools.context import ContextAware
|
from nanobot.agent.tools.context import ContextAware
|
||||||
|
|
||||||
if session_key is not None:
|
effective_key = session_key or session_key_for_channel(
|
||||||
effective_key = session_key
|
channel,
|
||||||
elif self._unified_session:
|
chat_id,
|
||||||
effective_key = UNIFIED_SESSION_KEY
|
unified_session=self._unified_session,
|
||||||
else:
|
)
|
||||||
effective_key = f"{channel}:{chat_id}"
|
|
||||||
request_ctx = RequestContext(
|
request_ctx = RequestContext(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
@ -646,7 +645,7 @@ class AgentLoop:
|
|||||||
if isinstance(persist_content, str) and persist_content.strip():
|
if isinstance(persist_content, str) and persist_content.strip():
|
||||||
text = persist_content
|
text = persist_content
|
||||||
extra.update({
|
extra.update({
|
||||||
"_automation_trigger": True,
|
AUTOMATION_HISTORY_META: True,
|
||||||
"automation_id": trigger.get("job_id"),
|
"automation_id": trigger.get("job_id"),
|
||||||
"automation_name": trigger.get("job_name"),
|
"automation_name": trigger.get("job_name"),
|
||||||
"automation_run_id": trigger.get("run_id"),
|
"automation_run_id": trigger.get("run_id"),
|
||||||
|
|||||||
@ -142,7 +142,7 @@ class CronTool(Tool, ContextAware):
|
|||||||
if action == "add":
|
if action == "add":
|
||||||
if self._in_cron_context.get():
|
if self._in_cron_context.get():
|
||||||
return "Error: cannot schedule new jobs from within a cron job execution"
|
return "Error: cannot schedule new jobs from within a cron job execution"
|
||||||
return self._add_job(name, message, every_seconds, cron_expr, tz, at, deliver)
|
return self._add_job(name, message, every_seconds, cron_expr, tz, at)
|
||||||
elif action == "list":
|
elif action == "list":
|
||||||
return self._list_jobs()
|
return self._list_jobs()
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
@ -157,7 +157,6 @@ class CronTool(Tool, ContextAware):
|
|||||||
cron_expr: str | None,
|
cron_expr: str | None,
|
||||||
tz: str | None,
|
tz: str | None,
|
||||||
at: str | None,
|
at: str | None,
|
||||||
deliver: bool = True,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
if not message:
|
if not message:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -64,6 +64,10 @@ from nanobot.utils.restart import ( # noqa: E402
|
|||||||
format_restart_completed_message,
|
format_restart_completed_message,
|
||||||
should_show_cli_restart_notice,
|
should_show_cli_restart_notice,
|
||||||
)
|
)
|
||||||
|
from nanobot.webui.metadata import ( # noqa: E402
|
||||||
|
WEBUI_MESSAGE_SOURCE_METADATA_KEY,
|
||||||
|
WEBUI_TURN_METADATA_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_surrogates(text: str) -> str:
|
def _sanitize_surrogates(text: str) -> str:
|
||||||
@ -89,8 +93,6 @@ class SafeFileHistory(FileHistory):
|
|||||||
super().store_string(_sanitize_surrogates(string))
|
super().store_string(_sanitize_surrogates(string))
|
||||||
|
|
||||||
|
|
||||||
_WEBUI_TURN_META_KEY = "webui_turn_id"
|
|
||||||
_WEBUI_MESSAGE_SOURCE_META_KEY = "_webui_message_source"
|
|
||||||
_PROACTIVE_WEBUI_METADATA: ContextVar[dict[str, Any] | None] = ContextVar(
|
_PROACTIVE_WEBUI_METADATA: ContextVar[dict[str, Any] | None] = ContextVar(
|
||||||
"proactive_webui_metadata",
|
"proactive_webui_metadata",
|
||||||
default=None,
|
default=None,
|
||||||
@ -106,13 +108,13 @@ def _proactive_delivery_metadata(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return channel metadata for a fresh proactive delivery turn."""
|
"""Return channel metadata for a fresh proactive delivery turn."""
|
||||||
out = dict(metadata or {})
|
out = dict(metadata or {})
|
||||||
out.pop(_WEBUI_TURN_META_KEY, None)
|
out.pop(WEBUI_TURN_METADATA_KEY, None)
|
||||||
if channel == "websocket":
|
if channel == "websocket":
|
||||||
out[_WEBUI_TURN_META_KEY] = f"{turn_seed}:{uuid.uuid4().hex}"
|
out[WEBUI_TURN_METADATA_KEY] = f"{turn_seed}:{uuid.uuid4().hex}"
|
||||||
source: dict[str, str] = {"kind": "cron"}
|
source: dict[str, str] = {"kind": "cron"}
|
||||||
if source_label:
|
if source_label:
|
||||||
source["label"] = source_label
|
source["label"] = source_label
|
||||||
out[_WEBUI_MESSAGE_SOURCE_META_KEY] = source
|
out[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = source
|
||||||
return out
|
return out
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
@ -1034,14 +1036,14 @@ def _run_gateway(
|
|||||||
schedule_background=lambda coro: agent._schedule_background(coro),
|
schedule_background=lambda coro: agent._schedule_background(coro),
|
||||||
).subscribe(runtime_events)
|
).subscribe(runtime_events)
|
||||||
|
|
||||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
|
||||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||||
|
from nanobot.session.keys import session_key_for_channel
|
||||||
|
|
||||||
def _channel_session_key(channel: str, chat_id: str) -> str:
|
def _channel_session_key(channel: str, chat_id: str) -> str:
|
||||||
return (
|
return session_key_for_channel(
|
||||||
UNIFIED_SESSION_KEY
|
channel,
|
||||||
if config.agents.defaults.unified_session
|
chat_id,
|
||||||
else f"{channel}:{chat_id}"
|
unified_session=config.agents.defaults.unified_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _session_metadata(session_key: str) -> dict[str, Any]:
|
def _session_metadata(session_key: str) -> dict[str, Any]:
|
||||||
@ -1125,17 +1127,20 @@ def _run_gateway(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] = True
|
metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] = True
|
||||||
|
run_record_base: dict[str, Any] = {
|
||||||
|
"job_id": job.id,
|
||||||
|
"job_name": job.name,
|
||||||
|
"session_key": session_key,
|
||||||
|
"prompt_ref": prompt_ref,
|
||||||
|
"prompt_vars": {"message": job.payload.message},
|
||||||
|
"rendered_prompt": prompt,
|
||||||
|
}
|
||||||
|
|
||||||
cron.write_run_record(
|
cron.write_run_record(
|
||||||
run_id,
|
run_id,
|
||||||
{
|
{
|
||||||
"job_id": job.id,
|
**run_record_base,
|
||||||
"job_name": job.name,
|
|
||||||
"session_key": session_key,
|
|
||||||
"status": "queued",
|
"status": "queued",
|
||||||
"prompt_ref": prompt_ref,
|
|
||||||
"prompt_vars": {"message": job.payload.message},
|
|
||||||
"rendered_prompt": prompt,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1159,14 +1164,9 @@ def _run_gateway(
|
|||||||
cron.write_run_record(
|
cron.write_run_record(
|
||||||
run_id,
|
run_id,
|
||||||
{
|
{
|
||||||
"job_id": job.id,
|
**run_record_base,
|
||||||
"job_name": job.name,
|
|
||||||
"session_key": session_key,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": error_text,
|
"error": error_text,
|
||||||
"prompt_ref": prompt_ref,
|
|
||||||
"prompt_vars": {"message": job.payload.message},
|
|
||||||
"rendered_prompt": prompt,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
@ -1178,13 +1178,8 @@ def _run_gateway(
|
|||||||
cron.write_run_record(
|
cron.write_run_record(
|
||||||
run_id,
|
run_id,
|
||||||
{
|
{
|
||||||
"job_id": job.id,
|
**run_record_base,
|
||||||
"job_name": job.name,
|
|
||||||
"session_key": session_key,
|
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"prompt_ref": prompt_ref,
|
|
||||||
"prompt_vars": {"message": job.payload.message},
|
|
||||||
"rendered_prompt": prompt,
|
|
||||||
"response": response,
|
"response": response,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from nanobot.cron.types import CronJob
|
|||||||
|
|
||||||
AUTOMATION_TRIGGER_META = "_automation_trigger"
|
AUTOMATION_TRIGGER_META = "_automation_trigger"
|
||||||
AUTOMATION_DEFER_UNTIL_IDLE_META = "_defer_until_session_idle"
|
AUTOMATION_DEFER_UNTIL_IDLE_META = "_defer_until_session_idle"
|
||||||
|
AUTOMATION_HISTORY_META = "_automation_turn"
|
||||||
|
|
||||||
|
|
||||||
def automation_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
def automation_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
||||||
|
|||||||
12
nanobot/session/keys.py
Normal file
12
nanobot/session/keys.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"""Shared session key constants and helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
UNIFIED_SESSION_KEY = "unified:default"
|
||||||
|
|
||||||
|
|
||||||
|
def session_key_for_channel(channel: str, chat_id: str, *, unified_session: bool = False) -> str:
|
||||||
|
"""Return the session key for a channel/chat pair."""
|
||||||
|
if unified_session:
|
||||||
|
return UNIFIED_SESSION_KEY
|
||||||
|
return f"{channel}:{chat_id}"
|
||||||
@ -14,6 +14,7 @@ from typing import Any
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from nanobot.config.paths import get_legacy_sessions_dir
|
from nanobot.config.paths import get_legacy_sessions_dir
|
||||||
|
from nanobot.session.metadata import SESSION_ROUTING_METADATA_KEY
|
||||||
from nanobot.utils.helpers import (
|
from nanobot.utils.helpers import (
|
||||||
ensure_dir,
|
ensure_dir,
|
||||||
estimate_message_tokens,
|
estimate_message_tokens,
|
||||||
@ -36,7 +37,7 @@ _FORK_VOLATILE_METADATA_KEYS = {
|
|||||||
"pending_user_turn",
|
"pending_user_turn",
|
||||||
"runtime_checkpoint",
|
"runtime_checkpoint",
|
||||||
"thread_goal",
|
"thread_goal",
|
||||||
"_routing_context",
|
SESSION_ROUTING_METADATA_KEY,
|
||||||
"title",
|
"title",
|
||||||
"title_user_edited",
|
"title_user_edited",
|
||||||
}
|
}
|
||||||
|
|||||||
3
nanobot/session/metadata.py
Normal file
3
nanobot/session/metadata.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Shared session metadata keys."""
|
||||||
|
|
||||||
|
SESSION_ROUTING_METADATA_KEY = "_routing_context"
|
||||||
@ -7,8 +7,7 @@ from typing import Any, Mapping
|
|||||||
from nanobot.bus.events import InboundMessage
|
from nanobot.bus.events import InboundMessage
|
||||||
from nanobot.cron.automation import is_automation_turn
|
from nanobot.cron.automation import is_automation_turn
|
||||||
from nanobot.session.manager import Session
|
from nanobot.session.manager import Session
|
||||||
|
from nanobot.session.metadata import SESSION_ROUTING_METADATA_KEY
|
||||||
SESSION_ROUTING_METADATA_KEY = "_routing_context"
|
|
||||||
|
|
||||||
_ROUTING_METADATA_KEYS = {
|
_ROUTING_METADATA_KEYS = {
|
||||||
"chat_type",
|
"chat_type",
|
||||||
|
|||||||
4
nanobot/webui/metadata.py
Normal file
4
nanobot/webui/metadata.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Shared WebUI metadata keys."""
|
||||||
|
|
||||||
|
WEBUI_TURN_METADATA_KEY = "webui_turn_id"
|
||||||
|
WEBUI_MESSAGE_SOURCE_METADATA_KEY = "_webui_message_source"
|
||||||
@ -18,6 +18,7 @@ from loguru import logger
|
|||||||
|
|
||||||
from nanobot.config.paths import get_webui_dir
|
from nanobot.config.paths import get_webui_dir
|
||||||
from nanobot.session.manager import SessionManager
|
from nanobot.session.manager import SessionManager
|
||||||
|
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
|
||||||
|
|
||||||
WEBUI_TRANSCRIPT_SCHEMA_VERSION = 3
|
WEBUI_TRANSCRIPT_SCHEMA_VERSION = 3
|
||||||
WEBUI_FORK_MARKER_EVENT = "fork_marker"
|
WEBUI_FORK_MARKER_EVENT = "fork_marker"
|
||||||
@ -29,8 +30,6 @@ _TRANSCRIPT_SEGMENT_RE = re.compile(r"^\d{6}\.jsonl$")
|
|||||||
_DEFAULT_TRANSCRIPT_PAGE_LIMIT = 160
|
_DEFAULT_TRANSCRIPT_PAGE_LIMIT = 160
|
||||||
_MAX_TRANSCRIPT_PAGE_LIMIT = 1000
|
_MAX_TRANSCRIPT_PAGE_LIMIT = 1000
|
||||||
_WEBUI_TURN_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
|
_WEBUI_TURN_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
|
||||||
WEBUI_TURN_METADATA_KEY = "webui_turn_id"
|
|
||||||
WEBUI_MESSAGE_SOURCE_METADATA_KEY = "_webui_message_source"
|
|
||||||
_MARKDOWN_LOCAL_IMAGE_RE = re.compile(
|
_MARKDOWN_LOCAL_IMAGE_RE = re.compile(
|
||||||
r"!\[([^\]]*)\]\((<[^>]+>|[^)\s]+)(\s+(?:\"[^\"]*\"|'[^']*'))?\)"
|
r"!\[([^\]]*)\]\((<[^>]+>|[^)\s]+)(\s+(?:\"[^\"]*\"|'[^']*'))?\)"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,8 +20,8 @@ from loguru import logger
|
|||||||
from websockets.http11 import Request as WsRequest
|
from websockets.http11 import Request as WsRequest
|
||||||
from websockets.http11 import Response
|
from websockets.http11 import Response
|
||||||
|
|
||||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
|
||||||
from nanobot.command.builtin import builtin_command_palette
|
from nanobot.command.builtin import builtin_command_palette
|
||||||
|
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||||
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
|
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
|
||||||
from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload
|
from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload
|
||||||
from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload
|
from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from nanobot.agent.context import ContextBuilder
|
|||||||
from nanobot.agent.loop import AgentLoop
|
from nanobot.agent.loop import AgentLoop
|
||||||
from nanobot.bus.events import InboundMessage
|
from nanobot.bus.events import InboundMessage
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
|
from nanobot.cron.automation import AUTOMATION_HISTORY_META, AUTOMATION_TRIGGER_META
|
||||||
from nanobot.providers.base import LLMResponse
|
from nanobot.providers.base import LLMResponse
|
||||||
from nanobot.session.goal_state import GOAL_STATE_KEY
|
from nanobot.session.goal_state import GOAL_STATE_KEY
|
||||||
from nanobot.session.manager import Session, SessionManager
|
from nanobot.session.manager import Session, SessionManager
|
||||||
@ -65,6 +66,41 @@ def test_agent_loop_llm_runtime_reflects_current_provider_and_model(tmp_path: Pa
|
|||||||
assert runtime.model == "next-model"
|
assert runtime.model == "next-model"
|
||||||
|
|
||||||
|
|
||||||
|
def test_persist_automation_turn_uses_distinct_history_marker(tmp_path: Path) -> None:
|
||||||
|
loop = _make_full_loop(tmp_path)
|
||||||
|
session = loop.sessions.get_or_create("websocket:auto")
|
||||||
|
prompt_ref = {"id": "cron.agent_turn.reminder", "version": 1, "sha256": "abc"}
|
||||||
|
|
||||||
|
persisted = loop._persist_user_message_early(
|
||||||
|
InboundMessage(
|
||||||
|
channel="websocket",
|
||||||
|
sender_id="cron",
|
||||||
|
chat_id="auto",
|
||||||
|
content="Automation: internal prompt",
|
||||||
|
metadata={
|
||||||
|
AUTOMATION_TRIGGER_META: {
|
||||||
|
"job_id": "job-1",
|
||||||
|
"job_name": "Daily check",
|
||||||
|
"run_id": "job-1:1",
|
||||||
|
"prompt_ref": prompt_ref,
|
||||||
|
"persist_content": "Scheduled automation triggered: Daily check",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert persisted is True
|
||||||
|
message = session.messages[-1]
|
||||||
|
assert message["content"] == "Scheduled automation triggered: Daily check"
|
||||||
|
assert message[AUTOMATION_HISTORY_META] is True
|
||||||
|
assert AUTOMATION_TRIGGER_META not in message
|
||||||
|
assert message["automation_id"] == "job-1"
|
||||||
|
assert message["automation_name"] == "Daily check"
|
||||||
|
assert message["automation_run_id"] == "job-1:1"
|
||||||
|
assert message["automation_prompt_ref"] == prompt_ref
|
||||||
|
|
||||||
|
|
||||||
def test_clean_generated_title_strips_reasoning_tags() -> None:
|
def test_clean_generated_title_strips_reasoning_tags() -> None:
|
||||||
assert clean_generated_title("<think>reasoning</think> WebUI polish") == "WebUI polish"
|
assert clean_generated_title("<think>reasoning</think> WebUI polish") == "WebUI polish"
|
||||||
assert clean_generated_title("Title: <think> The user said hello") == ""
|
assert clean_generated_title("Title: <think> The user said hello") == ""
|
||||||
|
|||||||
@ -592,8 +592,8 @@ async def test_waiting_dispatch_does_not_replace_active_pending_queue(tmp_path):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_followup_routed_to_pending_queue(tmp_path):
|
async def test_followup_routed_to_pending_queue(tmp_path):
|
||||||
"""Unified-session follow-ups should route into the active pending queue."""
|
"""Unified-session follow-ups should route into the active pending queue."""
|
||||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
|
||||||
from nanobot.bus.events import InboundMessage
|
from nanobot.bus.events import InboundMessage
|
||||||
|
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||||
|
|
||||||
loop = _make_loop(tmp_path)
|
loop = _make_loop(tmp_path)
|
||||||
loop._unified_session = True
|
loop._unified_session = True
|
||||||
|
|||||||
@ -11,10 +11,10 @@ from urllib.parse import urlencode
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
|
||||||
from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig
|
from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig
|
||||||
from nanobot.cron.service import CronService
|
from nanobot.cron.service import CronService
|
||||||
from nanobot.cron.types import CronJob, CronPayload, CronSchedule
|
from nanobot.cron.types import CronJob, CronPayload, CronSchedule
|
||||||
|
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||||
from nanobot.session.manager import Session, SessionManager
|
from nanobot.session.manager import Session, SessionManager
|
||||||
from nanobot.webui.gateway_services import GatewayServices, build_gateway_services
|
from nanobot.webui.gateway_services import GatewayServices, build_gateway_services
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,13 @@ from typer.testing import CliRunner
|
|||||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||||
from nanobot.cli.commands import _proactive_delivery_metadata, app
|
from nanobot.cli.commands import _proactive_delivery_metadata, app
|
||||||
from nanobot.config.schema import Config
|
from nanobot.config.schema import Config
|
||||||
|
from nanobot.cron.automation import AUTOMATION_DEFER_UNTIL_IDLE_META, AUTOMATION_TRIGGER_META
|
||||||
from nanobot.cron.types import CronJob, CronPayload
|
from nanobot.cron.types import CronJob, CronPayload
|
||||||
from nanobot.providers.factory import ProviderSnapshot, make_provider
|
from nanobot.providers.factory import ProviderSnapshot, make_provider
|
||||||
from nanobot.providers.openai_codex_provider import _strip_model_prefix
|
from nanobot.providers.openai_codex_provider import _strip_model_prefix
|
||||||
from nanobot.providers.registry import find_by_name
|
from nanobot.providers.registry import find_by_name
|
||||||
from nanobot.session.routing import SESSION_ROUTING_METADATA_KEY
|
from nanobot.session.routing import SESSION_ROUTING_METADATA_KEY
|
||||||
|
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ runner = CliRunner()
|
|||||||
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
|
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
|
||||||
metadata = {
|
metadata = {
|
||||||
"webui": True,
|
"webui": True,
|
||||||
"webui_turn_id": "turn-that-created-the-reminder",
|
WEBUI_TURN_METADATA_KEY: "turn-that-created-the-reminder",
|
||||||
"workspace_scope": {"mode": "default"},
|
"workspace_scope": {"mode": "default"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,9 +38,9 @@ def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
|
|||||||
|
|
||||||
assert out["webui"] is True
|
assert out["webui"] is True
|
||||||
assert out["workspace_scope"] == {"mode": "default"}
|
assert out["workspace_scope"] == {"mode": "default"}
|
||||||
assert out["webui_turn_id"].startswith("cron:drink-water:")
|
assert out[WEBUI_TURN_METADATA_KEY].startswith("cron:drink-water:")
|
||||||
assert out["webui_turn_id"] != metadata["webui_turn_id"]
|
assert out[WEBUI_TURN_METADATA_KEY] != metadata[WEBUI_TURN_METADATA_KEY]
|
||||||
assert out["_webui_message_source"] == {"kind": "cron", "label": "drink water"}
|
assert out[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {"kind": "cron", "label": "drink water"}
|
||||||
|
|
||||||
|
|
||||||
def _fake_provider():
|
def _fake_provider():
|
||||||
@ -1350,7 +1352,7 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
|
|||||||
to="chat-1",
|
to="chat-1",
|
||||||
channel_meta={
|
channel_meta={
|
||||||
"webui": True,
|
"webui": True,
|
||||||
"webui_turn_id": old_turn_id,
|
WEBUI_TURN_METADATA_KEY: old_turn_id,
|
||||||
"workspace_scope": {"mode": "default"},
|
"workspace_scope": {"mode": "default"},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1365,9 +1367,9 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
|
|||||||
assert delivered.chat_id == "chat-1"
|
assert delivered.chat_id == "chat-1"
|
||||||
assert delivered.metadata["webui"] is True
|
assert delivered.metadata["webui"] is True
|
||||||
assert delivered.metadata["workspace_scope"] == {"mode": "default"}
|
assert delivered.metadata["workspace_scope"] == {"mode": "default"}
|
||||||
assert delivered.metadata["webui_turn_id"].startswith("cron:drink-water:")
|
assert delivered.metadata[WEBUI_TURN_METADATA_KEY].startswith("cron:drink-water:")
|
||||||
assert delivered.metadata["webui_turn_id"] != old_turn_id
|
assert delivered.metadata[WEBUI_TURN_METADATA_KEY] != old_turn_id
|
||||||
assert delivered.metadata["_webui_message_source"] == {
|
assert delivered.metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {
|
||||||
"kind": "cron",
|
"kind": "cron",
|
||||||
"label": "drink water",
|
"label": "drink water",
|
||||||
}
|
}
|
||||||
@ -1653,14 +1655,17 @@ def test_gateway_bound_cron_runs_as_session_turn(
|
|||||||
assert "Automation: Check repository health." in msg.content
|
assert "Automation: Check repository health." in msg.content
|
||||||
assert msg.metadata["webui"] is True
|
assert msg.metadata["webui"] is True
|
||||||
assert msg.metadata["workspace_scope"]["project_path"] == str(tmp_path)
|
assert msg.metadata["workspace_scope"]["project_path"] == str(tmp_path)
|
||||||
assert msg.metadata["_webui_message_source"] == {"kind": "cron", "label": "Repo check"}
|
assert msg.metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {
|
||||||
trigger = msg.metadata["_automation_trigger"]
|
"kind": "cron",
|
||||||
|
"label": "Repo check",
|
||||||
|
}
|
||||||
|
trigger = msg.metadata[AUTOMATION_TRIGGER_META]
|
||||||
assert trigger["job_id"] == "repo-check"
|
assert trigger["job_id"] == "repo-check"
|
||||||
assert trigger["job_name"] == "Repo check"
|
assert trigger["job_name"] == "Repo check"
|
||||||
assert trigger["persist_content"] == (
|
assert trigger["persist_content"] == (
|
||||||
"Scheduled automation triggered: Repo check\n\nCheck repository health."
|
"Scheduled automation triggered: Repo check\n\nCheck repository health."
|
||||||
)
|
)
|
||||||
assert msg.metadata["_defer_until_session_idle"] is True
|
assert msg.metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] is True
|
||||||
statuses = [record["status"] for _run_id, record in seen["run_records"]]
|
statuses = [record["status"] for _run_id, record in seen["run_records"]]
|
||||||
assert statuses == ["queued", "ok"]
|
assert statuses == ["queued", "ok"]
|
||||||
assert seen["run_records"][0][0] == seen["run_records"][1][0]
|
assert seen["run_records"][0][0] == seen["run_records"][1][0]
|
||||||
|
|||||||
@ -347,7 +347,7 @@ def test_add_job_requires_session_key(tmp_path) -> None:
|
|||||||
tool = _make_tool(tmp_path)
|
tool = _make_tool(tmp_path)
|
||||||
tool.set_context(RequestContext(channel="telegram", chat_id="chat-1"))
|
tool.set_context(RequestContext(channel="telegram", chat_id="chat-1"))
|
||||||
|
|
||||||
result = tool._add_job(None, "Background refresh", 60, None, None, None, deliver=False)
|
result = tool._add_job(None, "Background refresh", 60, None, None, None)
|
||||||
|
|
||||||
assert result == "Error: scheduled automations must be created from a chat session"
|
assert result == "Error: scheduled automations must be created from a chat session"
|
||||||
assert tool._cron.list_jobs() == []
|
assert tool._cron.list_jobs() == []
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from nanobot.agent.tools.cron import CronTool
|
|||||||
from nanobot.agent.tools.message import MessageTool
|
from nanobot.agent.tools.message import MessageTool
|
||||||
from nanobot.agent.tools.spawn import SpawnTool
|
from nanobot.agent.tools.spawn import SpawnTool
|
||||||
from nanobot.cron.service import CronService
|
from nanobot.cron.service import CronService
|
||||||
|
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -262,7 +263,7 @@ async def test_webui_cron_tool_uses_unified_session_when_enabled(tmp_path) -> No
|
|||||||
"websocket",
|
"websocket",
|
||||||
"chat-123",
|
"chat-123",
|
||||||
metadata={"webui": True},
|
metadata={"webui": True},
|
||||||
session_key="unified:default",
|
session_key=UNIFIED_SESSION_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await tool.execute(action="add", message="standup", every_seconds=300)
|
result = await tool.execute(action="add", message="standup", every_seconds=300)
|
||||||
@ -270,7 +271,7 @@ async def test_webui_cron_tool_uses_unified_session_when_enabled(tmp_path) -> No
|
|||||||
|
|
||||||
jobs = tool._cron.list_jobs()
|
jobs = tool._cron.list_jobs()
|
||||||
assert len(jobs) == 1
|
assert len(jobs) == 1
|
||||||
assert jobs[0].payload.session_key == "unified:default"
|
assert jobs[0].payload.session_key == UNIFIED_SESSION_KEY
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user