mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
Some checks are pending
NPM Format / Website checks (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat: refine local-first research workflows * fix: delegate structured output tool choice * refactor(auto-fin): fetch and filter rolling CLS news * fix(auto-fin): keep imports portable across platforms * feat(auto-fin): expose CLS fetch controls * fix(auto-fin): propagate configurable news window * feat(auto_fin): normalize hybrid wikilinks in report body - Add _normalize_hybrid_wikilinks method to remove redundant Markdown destinations - Use regex to identify hybrid wikilinks with optional destinations - Replace redundant destinations with simpler wikilink format for clarity - Ensure normalization is failure-safe with exception handling and logging - Update report body normalization process to apply hybrid wikilink fix - Add unit tests to verify correct normalization and failure safety behavior * fix(dream): serialize integration with application-wide asyncio lock - Add application-wide asyncio.Lock to serialize digest writes during integration - Update _snapshot_digest to capture metadata per bucket - Validate bucket association when recovering from file changes - Add tests ensuring recovery only from the correct bucket - Add tests confirming integration lock is shared across application context - Enhance strict topic YAML loading validation in dream utils - Add tests for strict topic loading rejecting invalid or lossy fields * fix(cookbook): enable configurable job_tools for digest and merge steps - Update daily_cookbook.yaml to add job_tools: [memory_search, read] in digest steps - Modify DailyPaperDigestStep to read job_tools from kwargs instead of fixed list - Modify AutoFinMergeStep to similarly read job_tools from kwargs - Update tests to pass job_tools explicitly when invoking these steps - Remove hardcoded _TOOLS constants and replace with dynamic job_tools handling * fix: retry incomplete dream receipts * perf(pdf): increase max PDF pages limit from 20 to 35 - Updated configuration max_pdf_pages from 20 to 35 in daily_cookbook.yaml - Modified code to extract up to 35 pages instead of 20 in analyze.py - Updated README and README_ZH to document the increased max_pdf_pages - Adjusted unit test assertions to reflect new max_pdf_pages limit of 35 * fix memory integration and daily paper links * docs clarify cookbook tool usage
499 lines
20 KiB
Python
499 lines
20 KiB
Python
"""AgentScope backend for the unified agent wrapper."""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
from collections.abc import AsyncGenerator
|
|
from pathlib import Path
|
|
from typing import Any, TYPE_CHECKING
|
|
from uuid import uuid4
|
|
|
|
from agentscope.agent import Agent, ContextConfig, ReActConfig
|
|
from agentscope.agent._config import ModelConfig
|
|
from agentscope.event import (
|
|
DataBlockDeltaEvent,
|
|
DataBlockEndEvent,
|
|
DataBlockStartEvent,
|
|
ExceedMaxItersEvent,
|
|
ModelCallEndEvent,
|
|
ModelCallStartEvent,
|
|
ReplyEndEvent,
|
|
ReplyStartEvent,
|
|
ThinkingBlockDeltaEvent,
|
|
ThinkingBlockEndEvent,
|
|
ThinkingBlockStartEvent,
|
|
TextBlockDeltaEvent,
|
|
TextBlockEndEvent,
|
|
TextBlockStartEvent,
|
|
ToolCallDeltaEvent,
|
|
ToolCallEndEvent,
|
|
ToolCallStartEvent,
|
|
ToolResultDataDeltaEvent,
|
|
ToolResultEndEvent,
|
|
ToolResultStartEvent,
|
|
ToolResultTextDeltaEvent,
|
|
)
|
|
from agentscope.message import TextBlock, ToolResultState, UserMsg
|
|
from agentscope.permission import PermissionBehavior, PermissionContext, PermissionDecision, PermissionMode
|
|
from agentscope.state import AgentState
|
|
from agentscope.tool import (
|
|
Bash,
|
|
Edit,
|
|
ExecResult,
|
|
FunctionTool,
|
|
Glob,
|
|
Grep,
|
|
LocalBackend,
|
|
Read,
|
|
ToolBase,
|
|
ToolChunk,
|
|
Toolkit,
|
|
Write,
|
|
)
|
|
|
|
from .base_agent_wrapper import BaseAgentWrapper
|
|
from ..as_llm import BaseAsLLM
|
|
from ..component_registry import R
|
|
from ...enumeration import ChunkEnum
|
|
from ...schema import StreamChunk, TokenUsage
|
|
from ...utils import AsStateHandler
|
|
|
|
if TYPE_CHECKING:
|
|
from ..job.base_job import BaseJob
|
|
|
|
_UUID_RE = re.compile(
|
|
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
class WorkspaceBackend(LocalBackend):
|
|
"""Local backend pinned to the agent cwd and configured environment.
|
|
|
|
Some AgentScope builtin tools use ``backend.getcwd()`` for default search
|
|
paths or safety checks. Pinning it here keeps those operations aligned with
|
|
the cwd passed to Bash. Tools that require absolute file paths still keep
|
|
their own validation behavior. Subprocesses receive the startup environment
|
|
captured in ``ApplicationConfig.environment`` in addition to the parent
|
|
process environment.
|
|
"""
|
|
|
|
def __init__(self, cwd: str, environment: dict[str, str] | None = None) -> None:
|
|
super().__init__()
|
|
self._workspace_cwd = cwd
|
|
self._environment = {**os.environ, **(environment or {})}
|
|
|
|
async def getcwd(self) -> str:
|
|
"""Return the configured workspace directory."""
|
|
return self._workspace_cwd
|
|
|
|
async def exec_shell(
|
|
self,
|
|
command: list[str],
|
|
*,
|
|
cwd: str | None = None,
|
|
timeout: float | None = None,
|
|
) -> ExecResult:
|
|
"""Run a local subprocess with the configured agent environment."""
|
|
kwargs: dict[str, Any] = {
|
|
"env": self._environment,
|
|
"stderr": asyncio.subprocess.PIPE,
|
|
"stdout": asyncio.subprocess.PIPE,
|
|
}
|
|
if cwd is not None:
|
|
kwargs["cwd"] = cwd
|
|
if os.name == "nt":
|
|
kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
|
|
|
|
try:
|
|
process = await asyncio.create_subprocess_exec(*command, **kwargs)
|
|
except (FileNotFoundError, NotADirectoryError, OSError) as exc:
|
|
return ExecResult(exit_code=127, stdout=b"", stderr=str(exc).encode("utf-8"))
|
|
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
process.kill()
|
|
await process.communicate()
|
|
return ExecResult(exit_code=-1, stdout=b"", stderr=b"timed out")
|
|
return ExecResult(exit_code=process.returncode or 0, stdout=stdout, stderr=stderr)
|
|
|
|
|
|
class BypassAnalysisBash(Bash):
|
|
"""Bash variant that delegates permission decisions to PermissionEngine.
|
|
|
|
AgentScope's built-in Bash performs bypass-immune static analysis before
|
|
the engine can apply `permission_mode: bypass`. For this app we want the
|
|
configured permission mode to be authoritative.
|
|
"""
|
|
|
|
async def check_permissions(
|
|
self,
|
|
_tool_input: dict[str, Any],
|
|
_context: PermissionContext,
|
|
) -> PermissionDecision:
|
|
"""Bypass Bash static analysis and let the permission engine decide."""
|
|
return PermissionDecision(
|
|
behavior=PermissionBehavior.PASSTHROUGH,
|
|
message="Bash static analysis skipped; delegating to permission engine.",
|
|
)
|
|
|
|
|
|
@R.register("agentscope")
|
|
class AsAgentWrapper(BaseAgentWrapper):
|
|
"""Agent wrapper backed by AgentScope framework."""
|
|
|
|
SDK_PACKAGE = "agentscope"
|
|
|
|
@staticmethod
|
|
def _agentscope_usage(usage: Any) -> TokenUsage:
|
|
"""Normalize AgentScope's portable input/output usage."""
|
|
return TokenUsage.from_provider(usage)
|
|
|
|
def __init__(self, as_llm: str = "default", session_retention_days: int = 10, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.as_llm = self.bind(as_llm, BaseAsLLM, optional=False)
|
|
self.session_retention_days = int(session_retention_days)
|
|
self._session_cleanup_done = False
|
|
|
|
@classmethod
|
|
def _make_tool(
|
|
cls,
|
|
job: "BaseJob",
|
|
tool_context_id: str | None = None,
|
|
injected_job_kwargs: dict[str, Any] | None = None,
|
|
) -> FunctionTool:
|
|
injected = cls._resolve_injected_job_kwargs(
|
|
{"tool_context_id": tool_context_id, "injected_job_kwargs": injected_job_kwargs},
|
|
)
|
|
|
|
async def run_job(**kwargs) -> ToolChunk:
|
|
response = await job(**cls._merge_injected_job_kwargs(kwargs, injected))
|
|
state = ToolResultState.SUCCESS if response.success else ToolResultState.ERROR
|
|
return ToolChunk(content=[TextBlock(text=str(response.answer))], state=state)
|
|
|
|
tool = FunctionTool(func=run_job, name=job.name, description=job.description, is_concurrency_safe=False)
|
|
if parameters := cls._strip_injected_parameters(job.parameters, injected):
|
|
tool.input_schema = parameters
|
|
return tool
|
|
|
|
def _builtin_tools(
|
|
self,
|
|
names: list[str] | str | bool | None = "all",
|
|
*,
|
|
sequential_tool_calls: bool = False,
|
|
) -> list[ToolBase]:
|
|
"""Return selected AgentScope built-in tools rooted at ``self.cwd``."""
|
|
cwd = str(self.cwd)
|
|
backend = WorkspaceBackend(cwd, self.bash_environment)
|
|
factories = {
|
|
"bash": lambda: BypassAnalysisBash(cwd=cwd, backend=backend),
|
|
"edit": lambda: Edit(backend=backend),
|
|
"glob": lambda: Glob(backend=backend),
|
|
"grep": lambda: Grep(backend=backend),
|
|
"read": lambda: Read(backend=backend),
|
|
"write": lambda: Write(backend=backend),
|
|
}
|
|
if names is False:
|
|
selected_names = []
|
|
elif names is True or names is None or names == "all":
|
|
selected_names = list(factories)
|
|
elif names in ("none", "no", "false"):
|
|
selected_names = []
|
|
elif isinstance(names, str):
|
|
selected_names = [names]
|
|
else:
|
|
selected_names = names
|
|
|
|
tools: list[ToolBase] = []
|
|
for name in selected_names:
|
|
key = name.lower()
|
|
if key not in factories:
|
|
allowed = ", ".join(factories)
|
|
raise ValueError(f"Unknown builtin tool {name!r}; expected one of: {allowed}")
|
|
tools.append(factories[key]())
|
|
|
|
if sequential_tool_calls:
|
|
for tool in tools:
|
|
tool.is_concurrency_safe = False
|
|
return tools
|
|
|
|
@property
|
|
def session_path(self) -> Path:
|
|
"""Directory used for persisted AgentScope sessions."""
|
|
if self.app_context is None:
|
|
return self.workspace_path / "mem_session" / "agentscope"
|
|
return self.workspace_path / self.app_context.app_config.mem_session_dir / "agentscope"
|
|
|
|
@staticmethod
|
|
def _validate_session_id(session_id: str, field: str = "session_id") -> str:
|
|
if not _UUID_RE.match(session_id):
|
|
raise ValueError(f"{field} must be a valid UUID: {session_id!r}")
|
|
return session_id.lower()
|
|
|
|
def _cleanup_expired_sessions(self) -> None:
|
|
"""Delete persisted session files older than ``session_retention_days``."""
|
|
if self._session_cleanup_done or self.session_retention_days <= 0:
|
|
self._session_cleanup_done = True
|
|
return
|
|
|
|
session_path = self.session_path
|
|
if not session_path.is_dir():
|
|
self._session_cleanup_done = True
|
|
return
|
|
|
|
cutoff = time.time() - self.session_retention_days * 24 * 60 * 60
|
|
removed = 0
|
|
for path in session_path.glob("*.jsonl"):
|
|
try:
|
|
if path.is_file() and path.stat().st_mtime < cutoff:
|
|
path.unlink()
|
|
removed += 1
|
|
except OSError as exc:
|
|
self.logger.warning(f"Failed to clean expired AgentScope session {path}: {exc}")
|
|
|
|
if removed:
|
|
self.logger.info(
|
|
f"Cleaned {removed} AgentScope session(s) older than {self.session_retention_days} day(s)",
|
|
)
|
|
self._session_cleanup_done = True
|
|
|
|
async def _load_state(self, kwargs: dict[str, Any], perm_mode: PermissionMode) -> AgentState:
|
|
resume = kwargs.get("resume") or ""
|
|
session_id = kwargs.get("session_id") or ""
|
|
fork_session = bool(kwargs.get("fork_session", False))
|
|
if resume:
|
|
resume = self._validate_session_id(resume, "resume")
|
|
if session_id:
|
|
session_id = self._validate_session_id(session_id)
|
|
|
|
if session_id and resume and not fork_session:
|
|
raise ValueError("session_id cannot be used with resume unless fork_session=True")
|
|
|
|
if resume:
|
|
handler = AsStateHandler.for_session(self.session_path, resume)
|
|
state = await handler.load_or_none()
|
|
if state is None:
|
|
raise FileNotFoundError(f"AgentScope session not found: {resume}")
|
|
state.permission_context = PermissionContext(mode=perm_mode)
|
|
state.session_id = resume
|
|
if fork_session:
|
|
forked = AgentState(
|
|
session_id=session_id or str(uuid4()),
|
|
summary=state.summary,
|
|
context=list(state.context),
|
|
permission_context=PermissionContext(mode=perm_mode),
|
|
)
|
|
return forked
|
|
return state
|
|
|
|
return AgentState(session_id=session_id or str(uuid4()), permission_context=PermissionContext(mode=perm_mode))
|
|
|
|
async def _dump_state(self, state: AgentState) -> None:
|
|
await AsStateHandler.for_session(self.session_path, state.session_id).dump(state)
|
|
|
|
def _resolve_skills(self, skills: list[str] | str | None) -> list[str]:
|
|
"""Resolve configured skill names to AgentScope local skill directories."""
|
|
return [str(path) for path in self._resolve_project_skills(skills).values()]
|
|
|
|
async def _build_agent(self, inputs: Any, **kwargs) -> tuple[Agent, Any]:
|
|
"""Build an Agent instance from kwargs. Returns (agent, processed_inputs)."""
|
|
model = self.as_llm.model if self.as_llm else None
|
|
if model is None:
|
|
raise ValueError("AsAgentWrapper requires a bound as_llm component with a valid model.")
|
|
|
|
self._cleanup_expired_sessions()
|
|
|
|
system_prompt = kwargs.get("system_prompt", "You are a helpful assistant.")
|
|
job_tools: list[str] = kwargs.get("job_tools", [])
|
|
resolved_jobs = self._resolve_job_tools(job_tools)
|
|
skills = self._resolve_skills(kwargs.get("skills"))
|
|
tool_context_id = kwargs.get("tool_context_id")
|
|
sequential_tool_calls = bool(kwargs.get("sequential_tool_calls", True))
|
|
builtin_tools = kwargs.get("builtin_tools", [])
|
|
if "builtin_tools" not in kwargs and bool(kwargs.get("use_builtin_tools", False)):
|
|
builtin_tools = "all"
|
|
tools: list[ToolBase] = []
|
|
tools.extend(self._builtin_tools(builtin_tools, sequential_tool_calls=sequential_tool_calls))
|
|
tools.extend(self._make_tool(job, tool_context_id, kwargs.get("injected_job_kwargs")) for job in resolved_jobs)
|
|
toolkit = kwargs.get("toolkit") or Toolkit(
|
|
tools=tools,
|
|
skills_or_loaders=skills,
|
|
)
|
|
|
|
perm_mode = PermissionMode(kwargs.get("permission_mode", "bypass"))
|
|
state = await self._load_state(kwargs, perm_mode)
|
|
|
|
agent = Agent(
|
|
name=self.name,
|
|
system_prompt=system_prompt,
|
|
model=model,
|
|
toolkit=toolkit,
|
|
state=state,
|
|
model_config=ModelConfig(**(kwargs.get("model_config") or {})),
|
|
context_config=ContextConfig(**(kwargs.get("context_config") or {})),
|
|
react_config=ReActConfig(**(kwargs.get("react_config") or {})),
|
|
)
|
|
|
|
if isinstance(inputs, str):
|
|
inputs = UserMsg(name="user", content=inputs)
|
|
|
|
return agent, inputs
|
|
|
|
async def reply(self, inputs: Any, **kwargs) -> dict:
|
|
kwargs = self._merged_kwargs(kwargs)
|
|
agent, inputs = await self._build_agent(inputs, **kwargs)
|
|
|
|
await agent.observe(inputs)
|
|
last_msg = await agent.reply()
|
|
usage = self._agentscope_usage(last_msg.usage) if last_msg.usage is not None else None
|
|
await self._dump_state(agent.state)
|
|
|
|
result = {
|
|
"session_id": agent.state.session_id,
|
|
"last_message": last_msg.model_dump(),
|
|
"result": last_msg.get_text_content(),
|
|
}
|
|
|
|
if usage is None:
|
|
self.logger.error("AgentScope did not return token usage; token accounting is unavailable for this reply.")
|
|
|
|
output_schema: dict | None = kwargs.get("output_schema")
|
|
if output_schema is not None:
|
|
assert self.as_llm is not None, "AsAgentWrapper requires a bound as_llm component with a valid model."
|
|
model = self.as_llm.model
|
|
assert model is not None, "AsAgentWrapper requires a bound as_llm component with a valid model."
|
|
res = await model.generate_structured_output(
|
|
messages=agent.state.context,
|
|
structured_model=output_schema,
|
|
)
|
|
result["structured_output"] = res.content
|
|
if res.usage is None:
|
|
usage = None
|
|
self.logger.error(
|
|
"AgentScope did not return structured-output token usage; token accounting is unavailable.",
|
|
)
|
|
elif usage is not None:
|
|
usage = TokenUsage.combine([usage, self._agentscope_usage(res.usage)])
|
|
|
|
result["usage"] = usage.model_dump() if usage is not None else None
|
|
if usage is not None:
|
|
self._record_token_usage(usage)
|
|
|
|
return result
|
|
|
|
async def compact_session(self, session_id: str) -> None:
|
|
"""Force compression of an AgentScope session."""
|
|
kwargs = self._merged_kwargs({"resume": session_id})
|
|
agent, _ = await self._build_agent(None, **kwargs)
|
|
config = {**(kwargs.get("context_config") or {}), "trigger_ratio": 1e-9}
|
|
await agent.compress_context(ContextConfig(**config))
|
|
await self._dump_state(agent.state)
|
|
|
|
# ----- StreamChunk conversion -------------------------------------------
|
|
|
|
@classmethod
|
|
# pylint: disable=too-many-return-statements
|
|
def _event_to_chunk(cls, event: Any) -> StreamChunk | None:
|
|
"""Convert an AgentScope event to a unified StreamChunk.
|
|
|
|
Returns ``None`` for events that should be silently skipped
|
|
(e.g. ``RequireUserConfirmEvent``).
|
|
"""
|
|
if isinstance(event, ReplyStartEvent):
|
|
meta = {"reply_id": event.reply_id, "name": event.name, "role": event.role}
|
|
return cls._chunk(ChunkEnum.REPLY_START, session_id=event.session_id, chunk="", metadata=meta)
|
|
if isinstance(event, ReplyEndEvent):
|
|
return cls._chunk(
|
|
ChunkEnum.REPLY_END,
|
|
session_id=event.session_id,
|
|
chunk="",
|
|
metadata={"reply_id": event.reply_id},
|
|
)
|
|
|
|
for event_cls, chunk_type, attr in (
|
|
(TextBlockStartEvent, ChunkEnum.CONTENT, None),
|
|
(TextBlockDeltaEvent, ChunkEnum.CONTENT, "delta"),
|
|
(TextBlockEndEvent, ChunkEnum.CONTENT, None),
|
|
(ThinkingBlockStartEvent, ChunkEnum.THINK, None),
|
|
(ThinkingBlockDeltaEvent, ChunkEnum.THINK, "delta"),
|
|
(ThinkingBlockEndEvent, ChunkEnum.THINK, None),
|
|
(DataBlockStartEvent, ChunkEnum.DATA, None),
|
|
(DataBlockDeltaEvent, ChunkEnum.DATA, "data"),
|
|
(DataBlockEndEvent, ChunkEnum.DATA, None),
|
|
):
|
|
if isinstance(event, event_cls):
|
|
kwargs = {"block_id": event.block_id, "chunk": getattr(event, attr) if attr else ""}
|
|
if isinstance(event, (DataBlockStartEvent, DataBlockDeltaEvent)):
|
|
kwargs["media_type"] = event.media_type
|
|
return cls._chunk(chunk_type, **kwargs)
|
|
|
|
if isinstance(event, ToolCallStartEvent):
|
|
payload = {"name": event.tool_call_name, "id": event.tool_call_id}
|
|
return cls._chunk(
|
|
ChunkEnum.TOOL_CALL,
|
|
tool_call_id=event.tool_call_id,
|
|
tool_call_name=event.tool_call_name,
|
|
chunk=json.dumps(payload),
|
|
)
|
|
if isinstance(event, ToolCallDeltaEvent):
|
|
return cls._chunk(ChunkEnum.TOOL_CALL, tool_call_id=event.tool_call_id, chunk=event.delta)
|
|
if isinstance(event, ToolCallEndEvent):
|
|
return cls._chunk(ChunkEnum.TOOL_CALL, tool_call_id=event.tool_call_id, chunk="")
|
|
if isinstance(event, ToolResultStartEvent):
|
|
return cls._chunk(
|
|
ChunkEnum.TOOL_RESULT,
|
|
tool_call_id=event.tool_call_id,
|
|
tool_call_name=event.tool_call_name,
|
|
chunk="",
|
|
)
|
|
if isinstance(event, ToolResultTextDeltaEvent):
|
|
return cls._chunk(ChunkEnum.TOOL_RESULT, tool_call_id=event.tool_call_id, chunk=event.delta)
|
|
if isinstance(event, ToolResultDataDeltaEvent):
|
|
return cls._chunk(
|
|
ChunkEnum.TOOL_RESULT,
|
|
tool_call_id=event.tool_call_id,
|
|
chunk=event.data,
|
|
media_type=event.media_type,
|
|
metadata={"url": event.url} if event.url else {},
|
|
)
|
|
if isinstance(event, ToolResultEndEvent):
|
|
return cls._chunk(
|
|
ChunkEnum.TOOL_RESULT,
|
|
tool_call_id=event.tool_call_id,
|
|
chunk="",
|
|
metadata={"state": str(event.state)},
|
|
)
|
|
if isinstance(event, ModelCallStartEvent):
|
|
return cls._chunk(ChunkEnum.USAGE, chunk="", metadata={"model_name": getattr(event, "model_name", None)})
|
|
if isinstance(event, ModelCallEndEvent):
|
|
usage = TokenUsage(input_tokens=event.input_tokens, output_tokens=event.output_tokens)
|
|
return cls._chunk(
|
|
ChunkEnum.USAGE,
|
|
chunk=json.dumps(usage.model_dump()),
|
|
input_tokens=usage.input_tokens,
|
|
output_tokens=usage.output_tokens,
|
|
metadata={
|
|
"model_name": getattr(event, "model_name", None),
|
|
"usage": usage.model_dump(),
|
|
},
|
|
)
|
|
if isinstance(event, ExceedMaxItersEvent):
|
|
return cls._chunk(ChunkEnum.ERROR, chunk="Exceeded max iterations")
|
|
return None
|
|
|
|
async def reply_stream(self, inputs: Any, **kwargs) -> AsyncGenerator[StreamChunk, None]:
|
|
"""Stream agent events as unified StreamChunk objects."""
|
|
kwargs = self._merged_stream_kwargs(kwargs)
|
|
agent, inputs = await self._build_agent(inputs, **kwargs)
|
|
|
|
async for event in agent.reply_stream(inputs):
|
|
chunk = self._event_to_chunk(event)
|
|
if chunk is not None:
|
|
chunk.session_id = chunk.session_id or agent.state.session_id
|
|
yield chunk
|
|
|
|
await self._dump_state(agent.state)
|