feat: preserve isolated visitor agent runtime

This commit is contained in:
张胜宇
2026-09-10 17:36:39 +08:00
parent 8e36a9941d
commit ba22a2220f
10 changed files with 154 additions and 17 deletions
+1
View File
@@ -60,6 +60,7 @@ class AgentDefinition(BaseModel):
allowed_roles: tuple[str, ...] = ()
allowed_portals: tuple[str, ...] = ()
supported_intents: tuple[str, ...] = ("general",)
requires_model_intent_classification: bool = True
class ResolvedAgentConfig(BaseModel):
+1 -1
View File
@@ -92,6 +92,6 @@ class JwtAuthenticator:
if claims.get("visitor") is True:
return RequestContext(
user_id=str(subject), trace_id=str(uuid4()), roles=("visitor",),
permissions=("agent:run",),
permissions=("agent:run",), data_scope="public",
)
return RequestContext(user_id=str(claims["sub"]), trace_id=str(uuid4()))
+5
View File
@@ -129,11 +129,16 @@ class BaseAgent(ABC):
async def recall_memory(self, request: AgentRequest, context: RequestContext) -> None:
if self._governance is None:
raise RecoverableAgentError("缺少记忆治理依赖")
if "visitor" in context.roles:
self.memories = ()
return
self.memories = await self._governance.recall(context)
if any(memory.customer_id != context.user_id for memory in self.memories):
raise RecoverableAgentError("记忆召回越过客户范围")
async def classify_intent(self, request: AgentRequest) -> IntentResult | None:
if not self.definition.requires_model_intent_classification:
return None
if self._intent_classifier is None or self._intent_endpoint_resolver is None:
return None
endpoints = await self._intent_endpoint_resolver.resolve(
+5 -1
View File
@@ -59,6 +59,10 @@ class AgentFactory:
agent.bind_model_service(self._model_service)
if self._tool_executor is not None:
agent.bind_tool_executor(self._tool_executor)
if self._intent_classifier is not None and self._intent_endpoint_resolver is not None:
if (
agent.definition.requires_model_intent_classification
and self._intent_classifier is not None
and self._intent_endpoint_resolver is not None
):
agent.bind_intent_classifier(self._intent_classifier, self._intent_endpoint_resolver)
return agent
+5 -1
View File
@@ -118,7 +118,11 @@ class AgentRunApplicationService:
await OutboxRepository(self.session).append(DomainEvent(
event_id=str(uuid4()), event_type="agent.run_requested", aggregate_type="agent_run",
aggregate_id=run_id, trace_id=trace_id,
payload={"run_id": run_id, "metadata": request.metadata.model_dump(mode="json")},
payload={
"run_id": run_id,
"actor_type": "visitor" if "visitor" in context.roles else "authenticated",
"metadata": request.metadata.model_dump(mode="json"),
},
occurred_at=now,
))
return RunAccepted(run_id, trace_id)
+43 -14
View File
@@ -102,6 +102,35 @@ class WorkerRuntime:
# episode 聚合是低频批处理,按轮次节流而不是每轮都查。
self._episode_rounds = 0
async def restore_context(
self, *, actor_type: str, actor_id: str, trace_id: str
) -> RequestContext:
"""按已验证的内部事件身份恢复执行上下文。"""
identity = RequestContext(user_id=actor_id, trace_id=trace_id)
if actor_type == "visitor":
return identity.model_copy(update={
"roles": ("visitor",),
"permissions": ("agent:run",),
"data_scope": "public",
})
return await self.resolve_identity(identity)
@staticmethod
def should_request_memory_extraction(
*, context: RequestContext, message: str, result: AgentResult,
business_events: tuple[str, ...] | list[str],
) -> bool:
"""只允许已登录用户的明确业务事实进入客户记忆抽取队列。"""
if "visitor" in context.roles:
return False
return MemoryService.should_extract_memory(
conversation_content=message,
role="user",
tool_result=any(call.status == "succeeded" for call in result.result.tool_calls),
event_type=business_events[0] if business_events else None,
signals=MemoryService.detect_memory_signals(message),
)
async def dispatch_one(self, *, run_id: str | None = None) -> bool:
# Outbox acknowledges a durable SQL queue entry, not an in-memory task.
async with SessionFactory() as session:
@@ -428,9 +457,15 @@ class WorkerRuntime:
idempotency_key=idem.idempotency_key,
metadata=AgentRequestMetadata.model_validate(metadata),
)
identity = RequestContext(user_id=str(run.user_id), trace_id=run.trace_id)
# Re-check account and permissions at execution time, including delayed jobs.
context = await self.resolve_identity(identity)
actor_type = (
str(event.payload.get("actor_type", "authenticated"))
if event else "authenticated"
)
actor_id = str(run.user_id)
trace_id = run.trace_id
context = await self.restore_context(
actor_type=actor_type, actor_id=actor_id, trace_id=trace_id
)
result: AgentResult | None = None
async for event_data in AgentExecutor(self.factory).execute(
request.agent_type, request, context, run_id
@@ -452,17 +487,11 @@ class WorkerRuntime:
async with SessionFactory() as session:
await AgentPersistenceService(session).complete_run(
run_id, result, worker_id=worker_id,
memory_extraction_requested=MemoryService.should_extract_memory(
conversation_content=request.message,
role="user",
# 工具产出的权威事实同样构成持久记忆(工具调用记录来自终态结果)。
tool_result=any(
call.status == "succeeded" for call in result.result.tool_calls
),
# 本 run 落库的业务事件(风险评估完成、交易完成等)。
event_type=business_events[0] if business_events else None,
# 用户明确陈述的偏好/约束/身份/目标,命中才触发抽取。
signals=MemoryService.detect_memory_signals(request.message),
memory_extraction_requested=self.should_request_memory_extraction(
context=context,
message=request.message,
result=result,
business_events=business_events,
),
)