capability-runtime 0.1.0__py3-none-any.whl
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.
- capability_runtime/__init__.py +90 -0
- capability_runtime/adapters/__init__.py +13 -0
- capability_runtime/adapters/agent_adapter.py +439 -0
- capability_runtime/adapters/agently_backend.py +423 -0
- capability_runtime/adapters/triggerflow_workflow_engine.py +865 -0
- capability_runtime/adapters/workflow_engine.py +43 -0
- capability_runtime/config.py +172 -0
- capability_runtime/errors.py +20 -0
- capability_runtime/guards.py +150 -0
- capability_runtime/host_protocol.py +400 -0
- capability_runtime/host_toolkit/__init__.py +55 -0
- capability_runtime/host_toolkit/approvals_profiles.py +94 -0
- capability_runtime/host_toolkit/evidence_hooks.py +65 -0
- capability_runtime/host_toolkit/history.py +74 -0
- capability_runtime/host_toolkit/invoke_capability.py +409 -0
- capability_runtime/host_toolkit/resume.py +317 -0
- capability_runtime/host_toolkit/system_prompt.py +132 -0
- capability_runtime/host_toolkit/turn_delta.py +128 -0
- capability_runtime/logging_utils.py +94 -0
- capability_runtime/manifest.py +173 -0
- capability_runtime/output_validator.py +139 -0
- capability_runtime/protocol/__init__.py +43 -0
- capability_runtime/protocol/agent.py +62 -0
- capability_runtime/protocol/capability.py +98 -0
- capability_runtime/protocol/chat_backend.py +38 -0
- capability_runtime/protocol/context.py +244 -0
- capability_runtime/protocol/workflow.py +119 -0
- capability_runtime/registry.py +287 -0
- capability_runtime/reporting/__init__.py +2 -0
- capability_runtime/reporting/node_report.py +497 -0
- capability_runtime/runtime.py +930 -0
- capability_runtime/runtime_ui_events_mixin.py +310 -0
- capability_runtime/sdk_lifecycle.py +982 -0
- capability_runtime/service_facade.py +418 -0
- capability_runtime/services.py +181 -0
- capability_runtime/structured_output.py +208 -0
- capability_runtime/structured_stream.py +38 -0
- capability_runtime/types.py +103 -0
- capability_runtime/ui_events/__init__.py +19 -0
- capability_runtime/ui_events/projector.py +617 -0
- capability_runtime/ui_events/session.py +292 -0
- capability_runtime/ui_events/store.py +127 -0
- capability_runtime/ui_events/transport.py +33 -0
- capability_runtime/ui_events/v1.py +76 -0
- capability_runtime/upstream_compat.py +182 -0
- capability_runtime/utils/__init__.py +1 -0
- capability_runtime/utils/usage.py +65 -0
- capability_runtime/workflow_runtime.py +218 -0
- capability_runtime-0.1.0.dist-info/METADATA +232 -0
- capability_runtime-0.1.0.dist-info/RECORD +52 -0
- capability_runtime-0.1.0.dist-info/WHEEL +5 -0
- capability_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agently → Skills Runtime SDK 的 LLM backend 适配器。
|
|
3
|
+
|
|
4
|
+
设计要点(非常重要):
|
|
5
|
+
- SDK agent loop 需要完整的 OpenAI wire `messages[]`(含 tool_call_id/tool_calls 等字段)。
|
|
6
|
+
- 因此桥接层不能使用 Agently 的 PromptGenerator(Prompt.to_messages)做映射,否则会丢字段导致 tool 闭环失败。
|
|
7
|
+
- 本模块复用 Agently builtins 的 OpenAICompatible ModelRequester 作为“网络/SSE 传输层”,直接发送 wire payload。
|
|
8
|
+
- 解析阶段复用 SDK `ChatCompletionsSseParser`,确保 tool_calls delta 拼接口径不分叉。
|
|
9
|
+
|
|
10
|
+
对齐规格:
|
|
11
|
+
- `docs/specs/agently-backend-stream-event-ordering-v1.md`
|
|
12
|
+
- `docs/specs/per-capability-llm-config-v1.md`
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Protocol, cast
|
|
20
|
+
|
|
21
|
+
from skills_runtime.llm.chat_sse import ChatCompletionsSseParser, ChatStreamEvent
|
|
22
|
+
from skills_runtime.llm.protocol import ChatBackend, ChatRequest
|
|
23
|
+
from skills_runtime.tools.protocol import ToolSpec, tool_spec_to_openai_tool
|
|
24
|
+
|
|
25
|
+
from ..logging_utils import log_suppressed_exception
|
|
26
|
+
from ..utils.usage import _usage_int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentlyRequester(Protocol):
|
|
30
|
+
"""
|
|
31
|
+
requester 抽象(用于测试注入)。
|
|
32
|
+
|
|
33
|
+
约束:
|
|
34
|
+
- `generate_request_data()` 返回一个具备 `.data/.request_options/.request_url/...` 字段的对象
|
|
35
|
+
- `request_model(request_data)` yield `(event, data)`,其中 data 为 SSE `data` 字符串或异常
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def generate_request_data(self) -> Any:
|
|
39
|
+
"""生成请求载体对象(需包含 `.data` 与 `.request_options` 字段)。"""
|
|
40
|
+
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
async def request_model(self, request_data: Any) -> AsyncIterator[tuple[str, Any]]:
|
|
44
|
+
"""发起流式请求并返回 `(event, data)` 迭代。"""
|
|
45
|
+
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentlyRequesterFactory(Protocol):
|
|
50
|
+
"""创建 requester 的工厂(用于测试注入与未来扩展)。"""
|
|
51
|
+
|
|
52
|
+
def __call__(self) -> AgentlyRequester:
|
|
53
|
+
"""创建并返回一个 requester 实例。"""
|
|
54
|
+
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class AgentlyBackendConfig:
|
|
60
|
+
"""
|
|
61
|
+
AgentlyChatBackend 的最小配置。
|
|
62
|
+
|
|
63
|
+
参数:
|
|
64
|
+
- `requester_factory`:默认使用 Agently OpenAICompatible;测试可注入 FakeRequester。
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
requester_factory: AgentlyRequesterFactory
|
|
68
|
+
|
|
69
|
+
def _normalize_usage_payload(*, usage: Any, model: Any = None) -> Optional[Dict[str, Any]]:
|
|
70
|
+
"""
|
|
71
|
+
把 provider usage 归一为 capability-runtime 的 `llm_usage` payload 形状。
|
|
72
|
+
|
|
73
|
+
返回:
|
|
74
|
+
- `None`:无法提取任何有效 usage 字段
|
|
75
|
+
- `dict`:`model/input_tokens/output_tokens/total_tokens`
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
if not isinstance(usage, dict):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
model_text = model.strip() if isinstance(model, str) and model.strip() else None
|
|
82
|
+
input_tokens = _usage_int(usage.get("input_tokens"))
|
|
83
|
+
if input_tokens is None:
|
|
84
|
+
input_tokens = _usage_int(usage.get("prompt_tokens"))
|
|
85
|
+
|
|
86
|
+
output_tokens = _usage_int(usage.get("output_tokens"))
|
|
87
|
+
if output_tokens is None:
|
|
88
|
+
output_tokens = _usage_int(usage.get("completion_tokens"))
|
|
89
|
+
|
|
90
|
+
total_tokens = _usage_int(usage.get("total_tokens"))
|
|
91
|
+
payload = {
|
|
92
|
+
"model": model_text,
|
|
93
|
+
"input_tokens": input_tokens,
|
|
94
|
+
"output_tokens": output_tokens,
|
|
95
|
+
"total_tokens": total_tokens,
|
|
96
|
+
}
|
|
97
|
+
return payload if any(value is not None for value in payload.values()) else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _extract_usage_payload_from_sse_data(data: str) -> Optional[Dict[str, Any]]:
|
|
101
|
+
"""
|
|
102
|
+
从原始 SSE `data` 字符串中提取 usage 摘要。
|
|
103
|
+
|
|
104
|
+
说明:
|
|
105
|
+
- bridge 模式仅做 best-effort;
|
|
106
|
+
- 解析失败/无 usage 时返回 None,不影响主链。
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
raw = str(data or "").strip()
|
|
110
|
+
if not raw or raw in ("[DONE]", "DONE"):
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
obj = json.loads(raw)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
log_suppressed_exception(
|
|
116
|
+
context="parse_usage_payload_json",
|
|
117
|
+
exc=exc,
|
|
118
|
+
extra={"raw_len": len(raw)},
|
|
119
|
+
)
|
|
120
|
+
return None
|
|
121
|
+
if not isinstance(obj, dict):
|
|
122
|
+
return None
|
|
123
|
+
return _normalize_usage_payload(usage=obj.get("usage"), model=obj.get("model"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _merge_stream_options_for_usage(value: Any) -> Dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
为 streaming 请求补齐 `include_usage=true`,同时保留已有 stream_options。
|
|
129
|
+
|
|
130
|
+
说明:
|
|
131
|
+
- OpenAICompatible provider 若不支持该字段,应在 provider/requester 侧 fail-open;
|
|
132
|
+
- 本函数只负责把请求事实补齐,不在此处做兼容分支判断。
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
merged = dict(value) if isinstance(value, dict) else {}
|
|
136
|
+
merged.setdefault("include_usage", True)
|
|
137
|
+
return merged
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _should_retry_without_stream_options(error: Any) -> bool:
|
|
141
|
+
"""判断 provider 拒绝 `stream_options/include_usage` 时是否应 fail-open 重试。"""
|
|
142
|
+
|
|
143
|
+
message = str(error or "").lower()
|
|
144
|
+
if not message:
|
|
145
|
+
return False
|
|
146
|
+
mentions_stream_options = "stream_options" in message or "include_usage" in message
|
|
147
|
+
mentions_unsupported = any(
|
|
148
|
+
token in message
|
|
149
|
+
for token in (
|
|
150
|
+
"unknown parameter",
|
|
151
|
+
"unsupported",
|
|
152
|
+
"not support",
|
|
153
|
+
"not supported",
|
|
154
|
+
"invalid parameter",
|
|
155
|
+
"extra inputs are not permitted",
|
|
156
|
+
"400",
|
|
157
|
+
"422",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return mentions_stream_options and mentions_unsupported
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class AgentlyChatBackend(ChatBackend):
|
|
164
|
+
"""
|
|
165
|
+
SDK `ChatBackend` 的 Agently 适配实现。
|
|
166
|
+
|
|
167
|
+
说明:
|
|
168
|
+
- `stream_chat` 输入为 OpenAI wire `messages[]` 与 `tools[]`(ToolSpec)。
|
|
169
|
+
- 输出为 SDK 的 `ChatStreamEvent`(text_delta/tool_calls/completed)。
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, *, config: AgentlyBackendConfig) -> None:
|
|
173
|
+
"""创建 backend。"""
|
|
174
|
+
|
|
175
|
+
self._config = config
|
|
176
|
+
|
|
177
|
+
async def stream_chat(self, request: ChatRequest) -> AsyncIterator[ChatStreamEvent]:
|
|
178
|
+
"""
|
|
179
|
+
发起一次 chat.completions streaming 并 yield `ChatStreamEvent`。
|
|
180
|
+
|
|
181
|
+
参数:
|
|
182
|
+
- `request`:上游 `ChatRequest` 参数包(包含 model/messages/tools 与可选推理参数)
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
usage_sink = None
|
|
186
|
+
if isinstance(getattr(request, "extra", None), dict):
|
|
187
|
+
candidate_sink = request.extra.get("_caprt_usage_sink")
|
|
188
|
+
if callable(candidate_sink):
|
|
189
|
+
usage_sink = candidate_sink
|
|
190
|
+
|
|
191
|
+
if not isinstance(request.messages, list):
|
|
192
|
+
raise TypeError("messages must be a list[dict]")
|
|
193
|
+
include_usage_enabled = True
|
|
194
|
+
for attempt in range(2):
|
|
195
|
+
requester = self._config.requester_factory()
|
|
196
|
+
request_data = requester.generate_request_data()
|
|
197
|
+
request_data.data["messages"] = request.messages
|
|
198
|
+
request_data.request_options["model"] = request.model
|
|
199
|
+
request_data.request_options["stream"] = True
|
|
200
|
+
request_data.stream = True
|
|
201
|
+
|
|
202
|
+
tool_specs: List[ToolSpec] = list(request.tools or [])
|
|
203
|
+
tool_choice_target_tool_name: Optional[str] = None
|
|
204
|
+
|
|
205
|
+
if request.temperature is not None:
|
|
206
|
+
request_data.request_options["temperature"] = float(request.temperature)
|
|
207
|
+
if request.max_tokens is not None:
|
|
208
|
+
request_data.request_options["max_tokens"] = int(request.max_tokens)
|
|
209
|
+
if request.top_p is not None:
|
|
210
|
+
request_data.request_options["top_p"] = float(request.top_p)
|
|
211
|
+
if request.response_format is not None:
|
|
212
|
+
request_data.request_options["response_format"] = dict(request.response_format)
|
|
213
|
+
|
|
214
|
+
# provider 特有扩展字段(best-effort 透传;冲突时以 request_options 显式字段为准)
|
|
215
|
+
#
|
|
216
|
+
# 重要:
|
|
217
|
+
# - request.extra 可能包含“运行时回调/非 JSON 值”(例如 on_retry=function),它们不属于 wire payload;
|
|
218
|
+
# - 这些值若被透传到 requester,可能导致 JSON 序列化失败并让 real 模式不可用。
|
|
219
|
+
if isinstance(request.extra, dict) and request.extra:
|
|
220
|
+
|
|
221
|
+
def _is_jsonable(value: Any, *, _seen: set[int] | None = None) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
判断 value 是否可 JSON 序列化(最小、保守、避免热路径重复 dumps)。
|
|
224
|
+
|
|
225
|
+
说明:
|
|
226
|
+
- 我们不尝试做自定义 default 编码(避免改变 wire 契约语义);
|
|
227
|
+
- 不可序列化的字段将被跳过(fail-closed),避免 real 模式因 requester 序列化失败而崩溃。
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
231
|
+
return True
|
|
232
|
+
if isinstance(value, dict):
|
|
233
|
+
seen = _seen if _seen is not None else set()
|
|
234
|
+
oid = id(value)
|
|
235
|
+
if oid in seen:
|
|
236
|
+
return False
|
|
237
|
+
seen.add(oid)
|
|
238
|
+
try:
|
|
239
|
+
return all(isinstance(k, str) and _is_jsonable(v, _seen=seen) for k, v in value.items())
|
|
240
|
+
finally:
|
|
241
|
+
seen.remove(oid)
|
|
242
|
+
if isinstance(value, (list, tuple)):
|
|
243
|
+
seen = _seen if _seen is not None else set()
|
|
244
|
+
oid = id(value)
|
|
245
|
+
if oid in seen:
|
|
246
|
+
return False
|
|
247
|
+
seen.add(oid)
|
|
248
|
+
try:
|
|
249
|
+
return all(_is_jsonable(v, _seen=seen) for v in value)
|
|
250
|
+
finally:
|
|
251
|
+
seen.remove(oid)
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
for k, v in request.extra.items():
|
|
255
|
+
# 过滤明显的非 wire 字段(以及所有不可 JSON 序列化值)
|
|
256
|
+
if k == "on_retry":
|
|
257
|
+
continue
|
|
258
|
+
if callable(v) or not _is_jsonable(v):
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
# 覆写优先级(spec 要求):
|
|
262
|
+
# - per-run llm_config 会写入 request.extra["tool_choice"]
|
|
263
|
+
# - 即使底层 requester/backend 已预置 tool_choice(例如默认 "auto"),也必须被本覆写覆盖
|
|
264
|
+
if k == "tool_choice":
|
|
265
|
+
if isinstance(v, dict):
|
|
266
|
+
# OpenAI 新格式:{"type":"function","function":{"name":"..."}}
|
|
267
|
+
# 某些 OpenAICompatible server 不支持 tool_choice.function,会直接 400;
|
|
268
|
+
# 兼容策略:归一化为 tool_choice="required",并(若可定位)过滤 tools 以避免选错工具。
|
|
269
|
+
function = v.get("function") if isinstance(v.get("function"), dict) else None
|
|
270
|
+
if function is not None:
|
|
271
|
+
name = function.get("name")
|
|
272
|
+
if isinstance(name, str) and name:
|
|
273
|
+
tool_choice_target_tool_name = name
|
|
274
|
+
request_data.request_options[k] = "required"
|
|
275
|
+
else:
|
|
276
|
+
request_data.request_options[k] = v
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
if k not in request_data.request_options:
|
|
280
|
+
request_data.request_options[k] = v
|
|
281
|
+
|
|
282
|
+
if include_usage_enabled:
|
|
283
|
+
request_data.request_options["stream_options"] = _merge_stream_options_for_usage(
|
|
284
|
+
request_data.request_options.get("stream_options")
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
request_data.request_options.pop("stream_options", None)
|
|
288
|
+
|
|
289
|
+
if tool_choice_target_tool_name:
|
|
290
|
+
matched = [spec for spec in tool_specs if spec.name == tool_choice_target_tool_name]
|
|
291
|
+
if not matched:
|
|
292
|
+
raise ValueError(f"tool_choice target tool not found: {tool_choice_target_tool_name}")
|
|
293
|
+
tool_specs = matched
|
|
294
|
+
|
|
295
|
+
tools_wire = [tool_spec_to_openai_tool(spec) for spec in tool_specs]
|
|
296
|
+
if tools_wire:
|
|
297
|
+
request_data.request_options["tools"] = tools_wire
|
|
298
|
+
request_data.request_options.setdefault("tool_choice", "auto")
|
|
299
|
+
else:
|
|
300
|
+
# 某些 provider 对 tools=[] 敏感;无工具时直接移除该字段。
|
|
301
|
+
request_data.request_options.pop("tools", None)
|
|
302
|
+
|
|
303
|
+
parser = ChatCompletionsSseParser()
|
|
304
|
+
deferred_completed: Optional[ChatStreamEvent] = None
|
|
305
|
+
|
|
306
|
+
# 兼容:不同版本/实现的 requester 可能返回:
|
|
307
|
+
# - async iterator(可直接 async for)
|
|
308
|
+
# - coroutine -> async iterator(需要 await 一次再 async for)
|
|
309
|
+
stream_or_coro = requester.request_model(request_data)
|
|
310
|
+
stream: AsyncIterator[tuple[str, Any]]
|
|
311
|
+
retry_without_stream_options = False
|
|
312
|
+
try:
|
|
313
|
+
if hasattr(stream_or_coro, "__aiter__"):
|
|
314
|
+
stream = cast(AsyncIterator[tuple[str, Any]], stream_or_coro)
|
|
315
|
+
else:
|
|
316
|
+
stream = await stream_or_coro
|
|
317
|
+
|
|
318
|
+
async for event, data in stream:
|
|
319
|
+
if event == "error":
|
|
320
|
+
error = data if isinstance(data, BaseException) else RuntimeError(f"Agently requester error: {data!r}")
|
|
321
|
+
if include_usage_enabled and attempt == 0 and _should_retry_without_stream_options(error):
|
|
322
|
+
retry_without_stream_options = True
|
|
323
|
+
log_suppressed_exception(
|
|
324
|
+
context="agently_backend_include_usage_retry",
|
|
325
|
+
exc=error,
|
|
326
|
+
extra={"retry_without_stream_options": True},
|
|
327
|
+
)
|
|
328
|
+
break
|
|
329
|
+
raise error
|
|
330
|
+
|
|
331
|
+
# OpenAICompatible requester 通常 yield ("message", <sse.data>)
|
|
332
|
+
if not isinstance(data, str):
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
usage_payload = _extract_usage_payload_from_sse_data(data)
|
|
336
|
+
if usage_payload is not None and usage_sink is not None:
|
|
337
|
+
try:
|
|
338
|
+
usage_sink(dict(usage_payload))
|
|
339
|
+
except Exception as sink_exc:
|
|
340
|
+
log_suppressed_exception(
|
|
341
|
+
context="usage_sink_callback",
|
|
342
|
+
exc=sink_exc,
|
|
343
|
+
extra={"source": "agently_backend"},
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
for ev in parser.feed_data(data):
|
|
347
|
+
# 关键不变量:
|
|
348
|
+
# 某些 OpenAICompatible server 的 SSE 序列可能是:
|
|
349
|
+
# - delta.tool_calls ...(累计中)
|
|
350
|
+
# - finish_reason="stop" → parser 先 emit completed
|
|
351
|
+
# - [DONE] → parser 才 flush tool_calls
|
|
352
|
+
#
|
|
353
|
+
# 若我们把 completed 立即 yield,上游消费端可能在 completed 后停止消费,
|
|
354
|
+
# 从而错过后续 tool_calls,最终表现为 NodeReport.tool_calls 为空。
|
|
355
|
+
#
|
|
356
|
+
# 因此:completed 事件必须延迟到“确认不会再有 tool_calls”后再产出。
|
|
357
|
+
if ev.type == "completed":
|
|
358
|
+
deferred_completed = ev
|
|
359
|
+
continue
|
|
360
|
+
yield ev
|
|
361
|
+
|
|
362
|
+
if data.strip() in ("[DONE]", "DONE"):
|
|
363
|
+
break
|
|
364
|
+
except Exception as error:
|
|
365
|
+
if include_usage_enabled and attempt == 0 and _should_retry_without_stream_options(error):
|
|
366
|
+
retry_without_stream_options = True
|
|
367
|
+
log_suppressed_exception(
|
|
368
|
+
context="agently_backend_include_usage_retry",
|
|
369
|
+
exc=error,
|
|
370
|
+
extra={"retry_without_stream_options": True, "raised_directly": True},
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
raise
|
|
374
|
+
|
|
375
|
+
if retry_without_stream_options:
|
|
376
|
+
include_usage_enabled = False
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
# 注意:即使已看到 [DONE],也必须调用 parser.finish():
|
|
380
|
+
# - 某些实现可能不会在 feed_data("[DONE]") 时 flush tool_calls;
|
|
381
|
+
# - 若跳过 finish(),会出现“tool_calls 丢失/NodeReport.tool_calls 为空”的假阴性。
|
|
382
|
+
for ev in parser.finish():
|
|
383
|
+
if ev.type == "completed":
|
|
384
|
+
# 不用 finish() 的 eof 覆盖真实 stop(若已看到 stop completed)
|
|
385
|
+
if deferred_completed is None:
|
|
386
|
+
deferred_completed = ev
|
|
387
|
+
continue
|
|
388
|
+
yield ev
|
|
389
|
+
|
|
390
|
+
if deferred_completed is not None:
|
|
391
|
+
yield deferred_completed
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def build_openai_compatible_requester_factory(*, agently_agent: Any) -> AgentlyRequesterFactory:
|
|
396
|
+
"""
|
|
397
|
+
构造默认 requester_factory(复用 Agently OpenAICompatible builtins)。
|
|
398
|
+
|
|
399
|
+
参数:
|
|
400
|
+
- `agently_agent`:宿主提供的 Agently agent(需包含 `plugin_manager` 与 `settings`)。
|
|
401
|
+
|
|
402
|
+
返回:
|
|
403
|
+
- requester_factory:无参可调用对象,返回 OpenAICompatible requester 实例。
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
from agently.core import Prompt
|
|
407
|
+
from agently.builtins.plugins.ModelRequester.OpenAICompatible import OpenAICompatible
|
|
408
|
+
|
|
409
|
+
plugin_manager = getattr(agently_agent, "plugin_manager", None)
|
|
410
|
+
settings = getattr(agently_agent, "settings", None)
|
|
411
|
+
if plugin_manager is None or settings is None:
|
|
412
|
+
raise TypeError("agently_agent must provide .plugin_manager and .settings")
|
|
413
|
+
|
|
414
|
+
def _factory() -> AgentlyRequester:
|
|
415
|
+
"""按当前 agently settings 构建一次 requester。"""
|
|
416
|
+
|
|
417
|
+
# Prompt 仅用于让 OpenAICompatible 读取 settings/plugin 配置并生成 request_data;
|
|
418
|
+
# 真实 wire messages 将在 backend 层覆盖到 request_data.data["messages"]。
|
|
419
|
+
prompt = Prompt(plugin_manager=plugin_manager, parent_settings=settings, name="capability-runtime-backend")
|
|
420
|
+
prompt.set("input", "bridge") # 避免 prompt 全空触发校验
|
|
421
|
+
return OpenAICompatible(prompt, settings)
|
|
422
|
+
|
|
423
|
+
return _factory
|