union-app-chat-stream 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/__init__.py +1 -0
- package/app/agent/__init__.py +1 -0
- package/app/agent/capabilities.py +388 -0
- package/app/agent/coordinator/__init__.py +1 -0
- package/app/agent/coordinator/definition.py +50 -0
- package/app/agent/coordinator/output_guard.py +29 -0
- package/app/agent/graph.py +95 -0
- package/app/agent/guardrails.py +30 -0
- package/app/agent/routing.py +81 -0
- package/app/agent/runtime/__init__.py +1 -0
- package/app/agent/runtime/activity.py +393 -0
- package/app/agent/runtime/delegation.py +80 -0
- package/app/agent/runtime/deps.py +34 -0
- package/app/agent/runtime/execution.py +368 -0
- package/app/agent/runtime/model.py +47 -0
- package/app/agent/runtime/model_errors.py +24 -0
- package/app/agent/runtime/session.py +154 -0
- package/app/agent/specialists/__init__.py +1 -0
- package/app/agent/specialists/behavior_risk/__init__.py +1 -0
- package/app/agent/specialists/behavior_risk/definition.py +54 -0
- package/app/agent/specialists/build.py +94 -0
- package/app/agent/specialists/knowledge/__init__.py +1 -0
- package/app/agent/specialists/knowledge/definition.py +38 -0
- package/app/agent/specialists/personal_memory/__init__.py +1 -0
- package/app/agent/specialists/personal_memory/definition.py +35 -0
- package/app/agent/specialists/personal_memory/output_guard.py +55 -0
- package/app/agent/specialists/running_analysis/__init__.py +1 -0
- package/app/agent/specialists/running_analysis/definition.py +46 -0
- package/app/agent/specialists/running_analysis/output_guard.py +38 -0
- package/app/agent/specialists/scheduled_task_draft/__init__.py +8 -0
- package/app/agent/specialists/scheduled_task_draft/definition.py +142 -0
- package/app/agent/specialists/scheduled_task_draft/output_guard.py +81 -0
- package/app/asgi.py +139 -0
- package/app/config/__init__.py +1 -0
- package/app/config/settings.py +67 -0
- package/app/memory/__init__.py +1 -0
- package/app/memory/store.py +154 -0
- package/app/service/rag_service.py +364 -0
- package/app/skills/full-chain-quality-analysis/SKILL.md +22 -0
- package/app/tools/__init__.py +1 -0
- package/app/tools/business.py +183 -0
- package/app/utils/__init__.py +1 -0
- package/app/utils/api_client.py +76 -0
- package/app/utils/control_auth.py +35 -0
- package/app/utils/state_client.py +60 -0
- package/app/views/__init__.py +1 -0
- package/app/views/auth.py +189 -0
- package/app/views/errors.py +19 -0
- package/app/views/routes.py +25 -0
- package/app/views/run_context.py +33 -0
- package/app/views/streaming_runs.py +340 -0
- package/app/views/sync_runs.py +152 -0
- package/deploy/autoconf/templates/env.j2 +23 -0
- package/deploy/autoconf.yml +15 -0
- package/deploy/scripts/healthcheck.sh +12 -0
- package/deploy/scripts/start.sh +80 -0
- package/deploy/scripts/stop.sh +35 -0
- package/knowledge/000036-scenario-offline-function-call-mock-v1.md +134 -0
- package/package.json +21 -0
- package/requirements.txt +10 -0
- package/scripts/healthcheck.sh +4 -0
- package/scripts/start-BJ11.sh +1 -0
- package/scripts/start-BJ12.sh +1 -0
- package/scripts/start-SH20.sh +1 -0
- package/scripts/start-SZ31.sh +1 -0
- package/scripts/stop.sh +4 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""收集根执行和子执行消息,并生成标准 AG-UI Activity 快照。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ag_ui.core import (
|
|
10
|
+
ActivitySnapshotEvent,
|
|
11
|
+
EventType,
|
|
12
|
+
ReasoningMessageContentEvent,
|
|
13
|
+
ReasoningMessageStartEvent,
|
|
14
|
+
TextMessageContentEvent,
|
|
15
|
+
TextMessageStartEvent,
|
|
16
|
+
ToolCallArgsEvent,
|
|
17
|
+
ToolCallResultEvent,
|
|
18
|
+
ToolCallStartEvent,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from app.agent.runtime.delegation import Delegation, PlanReferenceError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MessageReducer:
|
|
25
|
+
"""Reduce standard AG-UI message events by message/tool-call identity."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, on_new_message=None) -> None:
|
|
28
|
+
self.messages: list[dict[str, Any]] = []
|
|
29
|
+
self._by_id: dict[str, dict[str, Any]] = {}
|
|
30
|
+
self._tool_calls: dict[str, dict[str, Any]] = {}
|
|
31
|
+
self._on_new_message = on_new_message
|
|
32
|
+
|
|
33
|
+
def add_messages(self, messages: list[dict[str, Any]]) -> None:
|
|
34
|
+
for raw in messages:
|
|
35
|
+
message = dict(raw)
|
|
36
|
+
message_id = str(message.get("id") or "")
|
|
37
|
+
if not message_id:
|
|
38
|
+
continue
|
|
39
|
+
existing = self._by_id.get(message_id)
|
|
40
|
+
if existing is None:
|
|
41
|
+
self._append(message)
|
|
42
|
+
else:
|
|
43
|
+
existing.clear()
|
|
44
|
+
existing.update(message)
|
|
45
|
+
self._index_tool_calls(self._by_id[message_id])
|
|
46
|
+
|
|
47
|
+
def apply(self, event: Any) -> None:
|
|
48
|
+
if isinstance(event, TextMessageStartEvent):
|
|
49
|
+
message = {"id": event.message_id, "role": event.role, "content": ""}
|
|
50
|
+
if event.name:
|
|
51
|
+
message["name"] = event.name
|
|
52
|
+
self._ensure(message)
|
|
53
|
+
elif isinstance(event, TextMessageContentEvent):
|
|
54
|
+
self._content(event.message_id, event.delta, "assistant")
|
|
55
|
+
elif isinstance(event, ReasoningMessageStartEvent):
|
|
56
|
+
self._ensure({"id": event.message_id, "role": "reasoning", "content": ""})
|
|
57
|
+
elif isinstance(event, ReasoningMessageContentEvent):
|
|
58
|
+
self._content(event.message_id, event.delta, "reasoning")
|
|
59
|
+
elif isinstance(event, ToolCallStartEvent):
|
|
60
|
+
message_id = event.parent_message_id or f"tool-parent:{event.tool_call_id}"
|
|
61
|
+
message = self._ensure(
|
|
62
|
+
{"id": message_id, "role": "assistant", "content": "", "toolCalls": []}
|
|
63
|
+
)
|
|
64
|
+
calls = message.setdefault("toolCalls", [])
|
|
65
|
+
call = {
|
|
66
|
+
"id": event.tool_call_id,
|
|
67
|
+
"type": "function",
|
|
68
|
+
"function": {"name": event.tool_call_name, "arguments": ""},
|
|
69
|
+
}
|
|
70
|
+
calls.append(call)
|
|
71
|
+
self._tool_calls[event.tool_call_id] = call
|
|
72
|
+
elif isinstance(event, ToolCallArgsEvent):
|
|
73
|
+
call = self._tool_calls.get(event.tool_call_id)
|
|
74
|
+
if call is not None:
|
|
75
|
+
call["function"]["arguments"] += event.delta
|
|
76
|
+
elif isinstance(event, ToolCallResultEvent):
|
|
77
|
+
message = {
|
|
78
|
+
"id": event.message_id,
|
|
79
|
+
"role": "tool",
|
|
80
|
+
"toolCallId": event.tool_call_id,
|
|
81
|
+
"content": event.content,
|
|
82
|
+
}
|
|
83
|
+
self._ensure(message)
|
|
84
|
+
|
|
85
|
+
def _content(self, message_id: str, delta: str, role: str) -> None:
|
|
86
|
+
message = self._ensure({"id": message_id, "role": role, "content": ""})
|
|
87
|
+
message["content"] = str(message.get("content") or "") + delta
|
|
88
|
+
|
|
89
|
+
def _ensure(self, message: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
existing = self._by_id.get(str(message["id"]))
|
|
91
|
+
if existing is not None:
|
|
92
|
+
return existing
|
|
93
|
+
self._append(message)
|
|
94
|
+
return message
|
|
95
|
+
|
|
96
|
+
def _append(self, message: dict[str, Any]) -> None:
|
|
97
|
+
message_id = str(message["id"])
|
|
98
|
+
self.messages.append(message)
|
|
99
|
+
self._by_id[message_id] = message
|
|
100
|
+
if self._on_new_message is not None:
|
|
101
|
+
self._on_new_message(message)
|
|
102
|
+
self._index_tool_calls(message)
|
|
103
|
+
|
|
104
|
+
def _index_tool_calls(self, message: dict[str, Any]) -> None:
|
|
105
|
+
for call in message.get("toolCalls") or []:
|
|
106
|
+
call_id = call.get("id")
|
|
107
|
+
if call_id:
|
|
108
|
+
self._tool_calls[str(call_id)] = call
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class ChildExecution:
|
|
113
|
+
delegation: Delegation
|
|
114
|
+
status: str = "running"
|
|
115
|
+
error_code: str | None = None
|
|
116
|
+
reducer: MessageReducer | None = None
|
|
117
|
+
transformer: Any | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class RunCollector:
|
|
122
|
+
root_run_id: str
|
|
123
|
+
conversation_id: str
|
|
124
|
+
root_agent_name: str = "UnionCoordinatorAgent"
|
|
125
|
+
root_status: str = "running"
|
|
126
|
+
root_error_code: str | None = None
|
|
127
|
+
children: dict[str, ChildExecution] = field(default_factory=dict)
|
|
128
|
+
ordered_messages: list[tuple[str, dict[str, Any]]] = field(default_factory=list)
|
|
129
|
+
canonical_root_messages: list[dict[str, Any]] | None = None
|
|
130
|
+
plans: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
131
|
+
|
|
132
|
+
def __post_init__(self) -> None:
|
|
133
|
+
self.root = MessageReducer(
|
|
134
|
+
lambda message: self.ordered_messages.append((self.root_run_id, message))
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def start_child(self, delegation: Delegation) -> ChildExecution:
|
|
138
|
+
child = ChildExecution(delegation=delegation)
|
|
139
|
+
child.reducer = MessageReducer(
|
|
140
|
+
lambda message: self.ordered_messages.append((delegation.run_id, message))
|
|
141
|
+
)
|
|
142
|
+
self.children[delegation.run_id] = child
|
|
143
|
+
return child
|
|
144
|
+
|
|
145
|
+
def finish_child(
|
|
146
|
+
self, run_id: str, status: str, error_code: str | None = None
|
|
147
|
+
) -> ChildExecution:
|
|
148
|
+
child = self.children[run_id]
|
|
149
|
+
child.status = status
|
|
150
|
+
child.error_code = error_code
|
|
151
|
+
return child
|
|
152
|
+
|
|
153
|
+
def validate_plan_step(self, index: int) -> None:
|
|
154
|
+
plan = self.plans.get(self.root_run_id)
|
|
155
|
+
if plan is None or index < 1 or index > len(plan["items"]):
|
|
156
|
+
raise PlanReferenceError("plan step does not exist")
|
|
157
|
+
if any(
|
|
158
|
+
child.status == "running" and child.delegation.plan_index == index
|
|
159
|
+
for child in self.children.values()
|
|
160
|
+
):
|
|
161
|
+
raise PlanReferenceError("plan step is already running")
|
|
162
|
+
|
|
163
|
+
def set_root_messages(self, messages: list[dict[str, Any]]) -> None:
|
|
164
|
+
"""Combine trusted ingress users with the Agent's official message dump."""
|
|
165
|
+
ingress_users = [
|
|
166
|
+
deepcopy(message)
|
|
167
|
+
for message in self.root.messages
|
|
168
|
+
if message.get("role") == "user"
|
|
169
|
+
]
|
|
170
|
+
seen_ids = {
|
|
171
|
+
str(message["id"])
|
|
172
|
+
for message in ingress_users
|
|
173
|
+
if message.get("id") is not None
|
|
174
|
+
}
|
|
175
|
+
self.canonical_root_messages = ingress_users
|
|
176
|
+
for message in messages:
|
|
177
|
+
message_id = message.get("id")
|
|
178
|
+
if message_id is not None and str(message_id) in seen_ids:
|
|
179
|
+
continue
|
|
180
|
+
self.canonical_root_messages.append(deepcopy(message))
|
|
181
|
+
if message_id is not None:
|
|
182
|
+
seen_ids.add(str(message_id))
|
|
183
|
+
|
|
184
|
+
def snapshot(self, run_id: str) -> ActivitySnapshotEvent:
|
|
185
|
+
child = self.children[run_id]
|
|
186
|
+
metadata = child.delegation
|
|
187
|
+
return ActivitySnapshotEvent(
|
|
188
|
+
message_id=f"subagent:{run_id}",
|
|
189
|
+
activity_type="subagent_execution",
|
|
190
|
+
replace=True,
|
|
191
|
+
content={
|
|
192
|
+
"runId": run_id,
|
|
193
|
+
"parentRunId": metadata.parent_run_id,
|
|
194
|
+
"agentName": metadata.agent_name,
|
|
195
|
+
"delegationToolCallId": metadata.delegation_tool_call_id,
|
|
196
|
+
"task": metadata.task,
|
|
197
|
+
"status": child.status,
|
|
198
|
+
"messages": child.reducer.messages if child.reducer else [],
|
|
199
|
+
"errorCode": child.error_code,
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def update_plan(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
run_id: str,
|
|
207
|
+
parent_run_id: str | None,
|
|
208
|
+
agent_name: str,
|
|
209
|
+
skill_ids: list[str],
|
|
210
|
+
items: list[dict[str, str]],
|
|
211
|
+
) -> ActivitySnapshotEvent:
|
|
212
|
+
"""Project one complete plan as a replaceable AG-UI Activity."""
|
|
213
|
+
if run_id == self.root_run_id and run_id in self.plans:
|
|
214
|
+
existing_items = self.plans[run_id]["items"]
|
|
215
|
+
for child in self.children.values():
|
|
216
|
+
index = child.delegation.plan_index
|
|
217
|
+
if index is None:
|
|
218
|
+
continue
|
|
219
|
+
if (
|
|
220
|
+
index > len(items)
|
|
221
|
+
or index > len(existing_items)
|
|
222
|
+
or items[index - 1]["content"]
|
|
223
|
+
!= existing_items[index - 1]["content"]
|
|
224
|
+
):
|
|
225
|
+
raise PlanReferenceError("bound plan step changed")
|
|
226
|
+
current = next(
|
|
227
|
+
(index for index, item in enumerate(items, 1) if item["status"] == "in_progress"),
|
|
228
|
+
None,
|
|
229
|
+
)
|
|
230
|
+
if current is None:
|
|
231
|
+
current = next(
|
|
232
|
+
(index for index, item in enumerate(items, 1) if item["status"] == "pending"),
|
|
233
|
+
len(items) or None,
|
|
234
|
+
)
|
|
235
|
+
status = (
|
|
236
|
+
"running"
|
|
237
|
+
if any(item["status"] in {"pending", "in_progress"} for item in items)
|
|
238
|
+
else "completed"
|
|
239
|
+
)
|
|
240
|
+
content = {
|
|
241
|
+
"runId": run_id,
|
|
242
|
+
"parentRunId": parent_run_id,
|
|
243
|
+
"agentName": agent_name,
|
|
244
|
+
"skillIds": skill_ids,
|
|
245
|
+
"currentIndex": current,
|
|
246
|
+
"total": len(items),
|
|
247
|
+
"status": status,
|
|
248
|
+
"items": [dict(item) for item in items],
|
|
249
|
+
}
|
|
250
|
+
self.plans[run_id] = content
|
|
251
|
+
event = self.plan_snapshot(run_id)
|
|
252
|
+
assert event is not None
|
|
253
|
+
return event
|
|
254
|
+
|
|
255
|
+
def plan_snapshot(self, run_id: str) -> ActivitySnapshotEvent | None:
|
|
256
|
+
content = self.plans.get(run_id)
|
|
257
|
+
if content is None:
|
|
258
|
+
return None
|
|
259
|
+
projected = deepcopy(content)
|
|
260
|
+
if run_id == self.root_run_id:
|
|
261
|
+
for child in self.children.values():
|
|
262
|
+
index = child.delegation.plan_index
|
|
263
|
+
if index is None or index > len(projected["items"]):
|
|
264
|
+
continue
|
|
265
|
+
projected["items"][index - 1]["status"] = (
|
|
266
|
+
"in_progress" if child.status == "running" else child.status
|
|
267
|
+
)
|
|
268
|
+
current = next(
|
|
269
|
+
(
|
|
270
|
+
index
|
|
271
|
+
for index, item in enumerate(projected["items"], 1)
|
|
272
|
+
if item["status"] == "in_progress"
|
|
273
|
+
),
|
|
274
|
+
None,
|
|
275
|
+
)
|
|
276
|
+
if current is None:
|
|
277
|
+
current = next(
|
|
278
|
+
(
|
|
279
|
+
index
|
|
280
|
+
for index, item in enumerate(projected["items"], 1)
|
|
281
|
+
if item["status"] == "pending"
|
|
282
|
+
),
|
|
283
|
+
len(projected["items"]) or None,
|
|
284
|
+
)
|
|
285
|
+
statuses = {item["status"] for item in projected["items"]}
|
|
286
|
+
projected["currentIndex"] = current
|
|
287
|
+
projected["status"] = (
|
|
288
|
+
"running"
|
|
289
|
+
if statuses & {"pending", "in_progress"}
|
|
290
|
+
else "failed"
|
|
291
|
+
if "failed" in statuses
|
|
292
|
+
else "cancelled"
|
|
293
|
+
if "cancelled" in statuses
|
|
294
|
+
else "completed"
|
|
295
|
+
)
|
|
296
|
+
return ActivitySnapshotEvent(
|
|
297
|
+
message_id=f"task-plan:{run_id}",
|
|
298
|
+
activity_type="task_plan",
|
|
299
|
+
replace=True,
|
|
300
|
+
content=projected,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def finish_plan(
|
|
304
|
+
self, run_id: str, status: str
|
|
305
|
+
) -> ActivitySnapshotEvent | None:
|
|
306
|
+
content = self.plans.get(run_id)
|
|
307
|
+
if content is None or content["status"] == status:
|
|
308
|
+
return None
|
|
309
|
+
content = {**content, "status": status}
|
|
310
|
+
self.plans[run_id] = content
|
|
311
|
+
return ActivitySnapshotEvent(
|
|
312
|
+
message_id=f"task-plan:{run_id}",
|
|
313
|
+
activity_type="task_plan",
|
|
314
|
+
replace=True,
|
|
315
|
+
content=content,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def apply_root_event(self, event: Any) -> None:
|
|
319
|
+
self.root.apply(event)
|
|
320
|
+
if getattr(event, "type", None) == EventType.RUN_ERROR:
|
|
321
|
+
self.root_status = "failed"
|
|
322
|
+
self.root_error_code = getattr(event, "code", None) or "run_error"
|
|
323
|
+
elif getattr(event, "type", None) == EventType.RUN_FINISHED:
|
|
324
|
+
self.root_status = "completed"
|
|
325
|
+
|
|
326
|
+
def completion_payload(self) -> dict[str, Any]:
|
|
327
|
+
executions = [
|
|
328
|
+
{
|
|
329
|
+
"runId": self.root_run_id,
|
|
330
|
+
"parentRunId": None,
|
|
331
|
+
"agentName": self.root_agent_name,
|
|
332
|
+
"delegationToolCallId": None,
|
|
333
|
+
"task": None,
|
|
334
|
+
"status": self.root_status,
|
|
335
|
+
"errorCode": self.root_error_code,
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
executions.extend(
|
|
339
|
+
{
|
|
340
|
+
"runId": child.delegation.run_id,
|
|
341
|
+
"parentRunId": child.delegation.parent_run_id,
|
|
342
|
+
"agentName": child.delegation.agent_name,
|
|
343
|
+
"delegationToolCallId": child.delegation.delegation_tool_call_id,
|
|
344
|
+
"task": child.delegation.task,
|
|
345
|
+
"status": child.status,
|
|
346
|
+
"errorCode": child.error_code,
|
|
347
|
+
}
|
|
348
|
+
for child in self.children.values()
|
|
349
|
+
)
|
|
350
|
+
root_messages = (
|
|
351
|
+
self.canonical_root_messages
|
|
352
|
+
if self.canonical_root_messages is not None
|
|
353
|
+
else self.root.messages
|
|
354
|
+
)
|
|
355
|
+
messages = [
|
|
356
|
+
{"runId": self.root_run_id, "message": persisted}
|
|
357
|
+
for message in root_messages
|
|
358
|
+
if (
|
|
359
|
+
persisted := self._persisted_message(
|
|
360
|
+
self.root_run_id,
|
|
361
|
+
message,
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
is not None
|
|
365
|
+
]
|
|
366
|
+
messages.extend(
|
|
367
|
+
{"runId": run_id, "message": persisted}
|
|
368
|
+
for run_id, message in self.ordered_messages
|
|
369
|
+
if run_id != self.root_run_id
|
|
370
|
+
and (persisted := self._persisted_message(run_id, message)) is not None
|
|
371
|
+
)
|
|
372
|
+
return {
|
|
373
|
+
"conversationId": self.conversation_id,
|
|
374
|
+
"runId": self.root_run_id,
|
|
375
|
+
"status": self.root_status,
|
|
376
|
+
"errorCode": self.root_error_code,
|
|
377
|
+
"executions": executions,
|
|
378
|
+
"messages": messages,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def _persisted_message(
|
|
382
|
+
self, run_id: str, message: dict[str, Any]
|
|
383
|
+
) -> dict[str, Any] | None:
|
|
384
|
+
value = deepcopy(message)
|
|
385
|
+
if (
|
|
386
|
+
run_id == self.root_run_id
|
|
387
|
+
and self.root_status != "completed"
|
|
388
|
+
and value.get("role") == "assistant"
|
|
389
|
+
):
|
|
390
|
+
if not value.get("toolCalls"):
|
|
391
|
+
return None
|
|
392
|
+
value["content"] = ""
|
|
393
|
+
return value
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""定义 delegation 子执行的元数据、观察器和任务局部生命周期。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PlanReferenceError(ValueError):
|
|
14
|
+
"""Reject an invalid live binding between one root-plan step and a child."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Delegation:
|
|
19
|
+
run_id: str
|
|
20
|
+
parent_run_id: str
|
|
21
|
+
agent_name: str
|
|
22
|
+
delegation_tool_call_id: str
|
|
23
|
+
task: str
|
|
24
|
+
plan_index: int | None = None
|
|
25
|
+
terminal_status: str | None = None
|
|
26
|
+
terminal_error_code: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class DelegationObserver:
|
|
31
|
+
on_start: Callable[[Delegation], Awaitable[None]]
|
|
32
|
+
on_event: Callable[[Delegation, Any], Awaitable[None]]
|
|
33
|
+
on_finish: Callable[[Delegation, str, str | None], Awaitable[None]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
current_delegation: ContextVar[Delegation | None] = ContextVar(
|
|
37
|
+
"current_delegation",
|
|
38
|
+
default=None,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@asynccontextmanager
|
|
43
|
+
async def delegation_scope(ctx, delegation: Delegation):
|
|
44
|
+
"""Notify one observer exactly once around a delegate_task call."""
|
|
45
|
+
observer = ctx.deps.delegation_observer
|
|
46
|
+
token = current_delegation.set(delegation)
|
|
47
|
+
started = False
|
|
48
|
+
try:
|
|
49
|
+
if observer is not None:
|
|
50
|
+
await observer.on_start(delegation)
|
|
51
|
+
started = True
|
|
52
|
+
yield
|
|
53
|
+
except asyncio.CancelledError:
|
|
54
|
+
if ctx.deps.cancelled.is_set():
|
|
55
|
+
delegation.terminal_status = "cancelled"
|
|
56
|
+
delegation.terminal_error_code = "cancelled"
|
|
57
|
+
if observer is not None and started:
|
|
58
|
+
await observer.on_finish(
|
|
59
|
+
delegation,
|
|
60
|
+
delegation.terminal_status or "failed",
|
|
61
|
+
delegation.terminal_error_code or "subagent_timeout",
|
|
62
|
+
)
|
|
63
|
+
raise
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
if observer is not None and started:
|
|
66
|
+
await observer.on_finish(
|
|
67
|
+
delegation,
|
|
68
|
+
delegation.terminal_status or "failed",
|
|
69
|
+
delegation.terminal_error_code or type(exc).__name__,
|
|
70
|
+
)
|
|
71
|
+
raise
|
|
72
|
+
else:
|
|
73
|
+
if observer is not None and started:
|
|
74
|
+
await observer.on_finish(
|
|
75
|
+
delegation,
|
|
76
|
+
delegation.terminal_status or "completed",
|
|
77
|
+
delegation.terminal_error_code,
|
|
78
|
+
)
|
|
79
|
+
finally:
|
|
80
|
+
current_delegation.reset(token)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""定义服务端为一次 Agent 运行注入的可信依赖。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
|
|
11
|
+
from app.agent.runtime.delegation import DelegationObserver
|
|
12
|
+
from app.utils.api_client import ApiClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RunDeps:
|
|
17
|
+
"""Trusted, server-created dependencies for one Agent run."""
|
|
18
|
+
|
|
19
|
+
user_id: str
|
|
20
|
+
permissions: tuple[str, ...]
|
|
21
|
+
state_client: Any
|
|
22
|
+
api_client: ApiClient
|
|
23
|
+
cancelled: Any
|
|
24
|
+
authentication_type: str = "CAS"
|
|
25
|
+
org_code: str = ""
|
|
26
|
+
scheduled_run_id: int | None = None
|
|
27
|
+
rag_service: Any | None = None
|
|
28
|
+
effective_at: str | None = None
|
|
29
|
+
effective_timezone: str | None = None
|
|
30
|
+
planned_run_ids: set[str] = field(default_factory=set)
|
|
31
|
+
delegated_agents_by_run: dict[str, set[str]] = field(default_factory=dict)
|
|
32
|
+
delegation_observer: DelegationObserver | None = None
|
|
33
|
+
plan_observer: Callable[[Any, list[dict[str, str]]], Awaitable[None]] | None = None
|
|
34
|
+
delegation_plan_validator: Callable[[int], None] | None = None
|