feat(agent): add persistent runtime context providers
This commit is contained in:
@@ -11,6 +11,7 @@ if typing.TYPE_CHECKING:
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nanobot.agent.tools.context import ToolContext
|
||||
from nanobot.runtime_context import RuntimeContextProvider
|
||||
|
||||
_ToolT = TypeVar("_ToolT", bound="Tool")
|
||||
|
||||
@@ -206,6 +207,10 @@ class Tool(ABC):
|
||||
def create(cls, ctx: ToolContext) -> Tool:
|
||||
return cls()
|
||||
|
||||
def runtime_context_provider(self) -> RuntimeContextProvider | None:
|
||||
"""Return optional per-turn prompt context owned by this tool."""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, **kwargs: Any) -> Any:
|
||||
"""Run the tool; return content, or ``ToolResult.error(...)`` for failures."""
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any
|
||||
from pydantic import Field
|
||||
|
||||
from nanobot.agent.tools.base import Tool, ToolResult, tool_parameters
|
||||
from nanobot.agent.tools.context import RequestContext
|
||||
from nanobot.agent.tools.schema import (
|
||||
ArraySchema,
|
||||
BooleanSchema,
|
||||
@@ -16,7 +17,9 @@ from nanobot.agent.tools.schema import (
|
||||
tool_parameters_schema,
|
||||
)
|
||||
from nanobot.apps.cli import CliAppError, CliAppManager, CliAppsRuntimeConfig
|
||||
from nanobot.apps.cli.utils import runtime_lines_for_request
|
||||
from nanobot.config_base import Base
|
||||
from nanobot.runtime_context import RuntimeContextBlock, wrap_runtime_context_lines
|
||||
from nanobot.security.workspace_access import current_tool_workspace
|
||||
|
||||
|
||||
@@ -112,6 +115,23 @@ class CliAppsTool(Tool):
|
||||
+ installed_note
|
||||
)
|
||||
|
||||
def runtime_context_provider(self):
|
||||
return self._provide_runtime_context
|
||||
|
||||
async def _provide_runtime_context(
|
||||
self,
|
||||
request: RequestContext,
|
||||
) -> RuntimeContextBlock | None:
|
||||
lines = runtime_lines_for_request(
|
||||
request.original_user_text or "",
|
||||
request.metadata,
|
||||
request.workspace or self.workspace,
|
||||
)
|
||||
content = wrap_runtime_context_lines(lines)
|
||||
if not content:
|
||||
return None
|
||||
return RuntimeContextBlock(source="cli_apps", content=content)
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar, Token
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -25,6 +26,9 @@ class RequestContext:
|
||||
original_user_text: str | None = None
|
||||
runtime: LLMRuntime | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
sender_id: str | None = None
|
||||
turn_id: str | None = None
|
||||
workspace: Path | None = None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -138,6 +138,9 @@ class _LegacyErrorPrefixTool(Tool):
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
return self._wrapped.parameters
|
||||
|
||||
def runtime_context_provider(self):
|
||||
return self._wrapped.runtime_context_provider()
|
||||
|
||||
@property
|
||||
def read_only(self) -> bool:
|
||||
return self._wrapped.read_only
|
||||
|
||||
@@ -11,17 +11,22 @@ from nanobot.agent.goal_permission import (
|
||||
revoke_goal_mutation_permission,
|
||||
)
|
||||
from nanobot.agent.tools.base import Tool, ToolResult, tool_parameters
|
||||
from nanobot.agent.tools.context import current_request_context
|
||||
from nanobot.agent.tools.context import RequestContext, current_request_context
|
||||
from nanobot.agent.tools.schema import StringSchema, tool_parameters_schema
|
||||
from nanobot.bus.runtime_events import GoalStateChanged, RuntimeEventBus, RuntimeEventContext
|
||||
from nanobot.runtime_context import RuntimeContextBlock, wrap_runtime_context_lines
|
||||
from nanobot.session.goal_state import (
|
||||
GOAL_STATE_KEY,
|
||||
MAX_GOAL_OBJECTIVE_CHARS,
|
||||
discard_legacy_goal_state_key,
|
||||
explicit_goal_requested,
|
||||
goal_state_raw,
|
||||
goal_state_runtime_lines,
|
||||
parse_goal_state,
|
||||
sustained_goal_active,
|
||||
)
|
||||
from nanobot.session.turn_continuation import reset_goal_continuation_rounds
|
||||
from nanobot.utils.prompt_templates import render_template
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nanobot.session.manager import SessionManager
|
||||
@@ -158,6 +163,31 @@ class CreateGoalTool(Tool, _GoalToolsMixin):
|
||||
"completion criteria. Do not retry after a successful creation."
|
||||
)
|
||||
|
||||
def runtime_context_provider(self):
|
||||
return self._provide_runtime_context
|
||||
|
||||
async def _provide_runtime_context(
|
||||
self,
|
||||
request: RequestContext,
|
||||
) -> RuntimeContextBlock | None:
|
||||
if not request.session_key:
|
||||
return None
|
||||
session = self._sessions.get_or_create(request.session_key)
|
||||
goal_start_requested = explicit_goal_requested(request.metadata)
|
||||
goal_active = sustained_goal_active(session.metadata)
|
||||
if not goal_start_requested and not goal_active:
|
||||
return None
|
||||
|
||||
guidance = render_template(
|
||||
"agent/goal_runtime.md",
|
||||
strip=True,
|
||||
goal_start_requested=goal_start_requested,
|
||||
goal_active=goal_active,
|
||||
)
|
||||
state = wrap_runtime_context_lines(goal_state_runtime_lines(session.metadata))
|
||||
content = "\n\n".join(part for part in (guidance, state) if part)
|
||||
return RuntimeContextBlock(source="goal", content=content)
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
objective: str,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"""Tool registry for dynamic tool management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from nanobot.agent.tools.base import Tool, ToolResult
|
||||
from nanobot.agent.tools.context import ContextAware, current_request_context
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nanobot.runtime_context import RuntimeContextProvider
|
||||
|
||||
|
||||
def is_tool_error_result(name: str, result: Any) -> bool:
|
||||
return isinstance(result, ToolResult) and result.is_error
|
||||
@@ -36,6 +41,15 @@ class ToolRegistry:
|
||||
"""Get a tool by name."""
|
||||
return self._tools.get(name)
|
||||
|
||||
def get_runtime_context_providers(self) -> list[RuntimeContextProvider]:
|
||||
"""Return tool-owned providers in stable tool-name order."""
|
||||
providers: list[RuntimeContextProvider] = []
|
||||
for name in sorted(self._tools):
|
||||
provider = self._tools[name].runtime_context_provider()
|
||||
if provider is not None:
|
||||
providers.append(provider)
|
||||
return providers
|
||||
|
||||
@staticmethod
|
||||
def _lookup_key(name: str) -> str:
|
||||
"""Normalize names for suggestions only; never for execution."""
|
||||
|
||||
Reference in New Issue
Block a user