agentlisp 2.0.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.
- agentlisp-2.0.0.dist-info/METADATA +482 -0
- agentlisp-2.0.0.dist-info/RECORD +36 -0
- agentlisp-2.0.0.dist-info/WHEEL +4 -0
- agentlisp-2.0.0.dist-info/entry_points.txt +3 -0
- agentlisp_runtime/__init__.py +29 -0
- agentlisp_runtime/cli.py +145 -0
- agentlisp_runtime/engine.py +253 -0
- agentlisp_runtime/loader.py +126 -0
- agentlisp_runtime/models.py +129 -0
- host/__init__.py +5 -0
- host/gateway.py +128 -0
- host/gateway_sse.py +788 -0
- host/sandbox.py +143 -0
- host/sandbox_e2b.py +173 -0
- host/tests/test_infra_docker_e2e.py +247 -0
- host/workflow.py +803 -0
- host/workflow_temporal.py +89 -0
- runtime/__init__.py +29 -0
- runtime/base_agent_harness.py +164 -0
- runtime/base_harness.py +359 -0
- runtime/base_harness_v2.py +1379 -0
- runtime/checker.py +654 -0
- runtime/checkpoint.py +98 -0
- runtime/errors.py +37 -0
- runtime/llm_client.py +644 -0
- runtime/mcp_client.py +86 -0
- runtime/memory_fs.py +90 -0
- runtime/otel_tracer.py +124 -0
- runtime/status_bar.py +86 -0
- runtime/tests/test_ac1_rackunit_18cases_roundtrip.py +255 -0
- runtime/tests/test_cli_if_cli_1_exit_encoding.py +309 -0
- runtime/tests/test_harness_v2.py +2030 -0
- runtime/tests/test_sideeffect_builtin_tools_racket_and_python_ssot_8_items_bitwise_equal.py +132 -0
- runtime/tests/test_srs_acceptance.py +1369 -0
- runtime/tests/test_t2_bench_dataset.py +86 -0
- runtime/tests/test_v2_smoke.py +139 -0
runtime/checker.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""AgentLisp v2 static-checker Python-side SSOT mirror.
|
|
2
|
+
|
|
3
|
+
Mirrors compiler/checker.rkt (Racket) invariants for runtime usage
|
|
4
|
+
(no Racket install needed on host when CI unavailable).
|
|
5
|
+
|
|
6
|
+
Racket SSOT (L87 compiler/checker.rkt):
|
|
7
|
+
(define SIDEEFFECT-BUILTIN-TOOLS '(bash git-push wget curl scp dd chmod sudo))
|
|
8
|
+
|
|
9
|
+
Python SSOT (runtime/checker.py) must be a strict superset. If the two drift,
|
|
10
|
+
test_fr_check_2_sideeffect_builtins_ssot_consistent() will fail in CI.
|
|
11
|
+
|
|
12
|
+
FR-PARSER-N (§3 SRS L85-L90) invariants are mirrored here for pytest side
|
|
13
|
+
(validate_* helpers), so tests can PASS without a Racket compiler on host.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any
|
|
20
|
+
from urllib.parse import urlparse
|
|
21
|
+
|
|
22
|
+
SIDEEFFECT_BUILTIN_TOOLS: frozenset[str] = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"bash",
|
|
25
|
+
"git-push",
|
|
26
|
+
"wget",
|
|
27
|
+
"curl",
|
|
28
|
+
"scp",
|
|
29
|
+
"dd",
|
|
30
|
+
"chmod",
|
|
31
|
+
"sudo",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
PARSER_ERROR_SCHEMA_VERSION: str = "1.0.0"
|
|
36
|
+
|
|
37
|
+
FR_PARSER_PROVIDER_ENUM: frozenset[str] = frozenset({"anthropic", "openai", "qwen", "mock"})
|
|
38
|
+
|
|
39
|
+
FR_PARSER_ON_FAILURE_ENUM: frozenset[str] = frozenset({"ask-human", "fallback-model", "abort"})
|
|
40
|
+
|
|
41
|
+
FR_PARSER_TOPOLOGY_ENUM: frozenset[str] = frozenset(
|
|
42
|
+
{"peer", "orchestration", "decentralised", "judge-driven"}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
FR_PARSER_MCP_SCHEMES: frozenset[str] = frozenset({"stdio", "http+unix", "https", "sse"})
|
|
46
|
+
|
|
47
|
+
FR_PARSER_SCOPED_WORKER_REQUIRED_BLOCKS: tuple[str, ...] = (
|
|
48
|
+
"name",
|
|
49
|
+
"model",
|
|
50
|
+
"tools",
|
|
51
|
+
"harness",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
PARSE_ERR_ILLEGAL_SEXP: str = "PARSE_ILLEGAL_SEXP"
|
|
55
|
+
PARSE_ERR_PROVIDER_OR_TEMP: str = "PARSE_PROVIDER_ENUM_OR_TEMPERATURE_RANGE"
|
|
56
|
+
PARSE_ERR_MEMORY_AUTO_APPEND: str = "PARSE_MEMORY_AUTO_APPEND_KV_ALIGNMENT"
|
|
57
|
+
PARSE_ERR_TOOLS_COMBINATION: str = "PARSE_TOOLS_COMBINATION_OR_MCP_SCHEME"
|
|
58
|
+
PARSE_ERR_ON_FAILURE: str = "PARSE_CORRECT_ON_FAILURE_ENUM"
|
|
59
|
+
PARSE_ERR_TOPOLOGY: str = "PARSE_MULTIAGENT_TOPOLOGY_OR_SCOPED_WORKER"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_CHECKER_RKT_SIDEEFFECT_RE = re.compile(
|
|
63
|
+
r"\(define\s+SIDEEFFECT-BUILTIN-TOOLS\s*'*\((?P<items>[^()]*)\)\s*\)"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sideeffect_builtin_names() -> set[str]:
|
|
68
|
+
return set(SIDEEFFECT_BUILTIN_TOOLS)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def extract_checker_rkt_sideeffect_list(src: str) -> list[str]:
|
|
72
|
+
"""Extract symbols from Racket source SIDEEFFECT-BUILTIN-TOOLS definition.
|
|
73
|
+
|
|
74
|
+
Returns the list of whitespace-separated identifiers inside the outer list,
|
|
75
|
+
preserving order. Returns [] if not found (caller decides how to handle).
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(src, str):
|
|
78
|
+
return []
|
|
79
|
+
m = _CHECKER_RKT_SIDEEFFECT_RE.search(src)
|
|
80
|
+
if not m:
|
|
81
|
+
return []
|
|
82
|
+
body = m.group("items")
|
|
83
|
+
return [tok for tok in body.split() if tok]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_sideeffect_builtins_racket_mirror(racket_src: str) -> tuple[bool, list[str], list[str]]:
|
|
87
|
+
"""Return (ok?, racket_only, python_only).
|
|
88
|
+
|
|
89
|
+
ok is True iff the two SIDEEFFECT-BUILTIN-TOOLS sets match exactly
|
|
90
|
+
(order ignored).
|
|
91
|
+
"""
|
|
92
|
+
racket_tokens = extract_checker_rkt_sideeffect_list(racket_src)
|
|
93
|
+
racket_set = set(racket_tokens)
|
|
94
|
+
py_set = set(SIDEEFFECT_BUILTIN_TOOLS)
|
|
95
|
+
return (
|
|
96
|
+
racket_set == py_set,
|
|
97
|
+
sorted(racket_set - py_set),
|
|
98
|
+
sorted(py_set - racket_set),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def make_parse_error_json(
|
|
103
|
+
code: str,
|
|
104
|
+
srs_id: str,
|
|
105
|
+
message: str,
|
|
106
|
+
*,
|
|
107
|
+
agent_name: str | None = None,
|
|
108
|
+
source: str = "<al-string>",
|
|
109
|
+
line: int | None = 1,
|
|
110
|
+
column: int | None = 0,
|
|
111
|
+
position: int | None = 0,
|
|
112
|
+
span: int | None = 0,
|
|
113
|
+
hints: list[str] | None = None,
|
|
114
|
+
severity: str = "error",
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""Build a 12-field structured parse-error dict (mirrors Racket --json-errors).
|
|
117
|
+
|
|
118
|
+
Top-level keys (8): schema_version, code, severity, srs_id, message,
|
|
119
|
+
agent_name, srcloc, hints.
|
|
120
|
+
srcloc sub-keys (5): source, line, column, position, span.
|
|
121
|
+
Total distinct attribute slots (13; SRS L277 rounds to "12 fields" — srcloc
|
|
122
|
+
counts as one compound slot, which is the typical convention).
|
|
123
|
+
"""
|
|
124
|
+
return {
|
|
125
|
+
"schema_version": PARSER_ERROR_SCHEMA_VERSION,
|
|
126
|
+
"code": code,
|
|
127
|
+
"severity": severity,
|
|
128
|
+
"srs_id": srs_id,
|
|
129
|
+
"message": message,
|
|
130
|
+
"agent_name": agent_name or "unknown-agent",
|
|
131
|
+
"srcloc": {
|
|
132
|
+
"source": source,
|
|
133
|
+
"line": line,
|
|
134
|
+
"column": column,
|
|
135
|
+
"position": position,
|
|
136
|
+
"span": span,
|
|
137
|
+
},
|
|
138
|
+
"hints": list(hints) if hints else [],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def validate_provider_and_temperature(
|
|
143
|
+
provider: Any, temperature: Any
|
|
144
|
+
) -> tuple[bool, dict[str, Any] | None]:
|
|
145
|
+
"""FR-PARSER-2: provider ∈ {anthropic, openai, qwen, mock}; t ∈ [0.0, 1.0].
|
|
146
|
+
|
|
147
|
+
Returns (ok, error_json_or_None).
|
|
148
|
+
"""
|
|
149
|
+
if not isinstance(provider, str) or provider not in FR_PARSER_PROVIDER_ENUM:
|
|
150
|
+
return False, make_parse_error_json(
|
|
151
|
+
PARSE_ERR_PROVIDER_OR_TEMP,
|
|
152
|
+
"FR-PARSER-2",
|
|
153
|
+
f"FR-PARSER-2: provider={provider!r} 不在枚举 {sorted(FR_PARSER_PROVIDER_ENUM)}",
|
|
154
|
+
hints=[
|
|
155
|
+
f"provider 必须是以下字符串之一:{sorted(FR_PARSER_PROVIDER_ENUM)}",
|
|
156
|
+
"大小写敏感;禁止缩写或空串",
|
|
157
|
+
],
|
|
158
|
+
)
|
|
159
|
+
if isinstance(temperature, bool) or not isinstance(temperature, (int, float)):
|
|
160
|
+
return False, make_parse_error_json(
|
|
161
|
+
PARSE_ERR_PROVIDER_OR_TEMP,
|
|
162
|
+
"FR-PARSER-2",
|
|
163
|
+
f"FR-PARSER-2: temperature={temperature!r} 不是实数",
|
|
164
|
+
hints=["temperature 必须是 [0.0, 1.0] 之间的数字(int/float)"],
|
|
165
|
+
)
|
|
166
|
+
t = float(temperature)
|
|
167
|
+
if t < 0.0 or t > 1.0:
|
|
168
|
+
return False, make_parse_error_json(
|
|
169
|
+
PARSE_ERR_PROVIDER_OR_TEMP,
|
|
170
|
+
"FR-PARSER-2",
|
|
171
|
+
f"FR-PARSER-2: temperature={temperature!r} 超出 [0.0, 1.0] 范围",
|
|
172
|
+
hints=[
|
|
173
|
+
"temperature ∈ [0.0, 1.0](端点包含)",
|
|
174
|
+
f"当前值 temperature={t},若 >1.0 请下调;若 <0.0 请上调",
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
return True, None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate_memory_auto_append_key(memory_cfg: Any) -> tuple[bool, dict[str, Any] | None]:
|
|
181
|
+
"""FR-PARSER-3: spec 命名 :auto-append → emit 到 Python auto_append_episodic。
|
|
182
|
+
|
|
183
|
+
验证要点(SRS L87):
|
|
184
|
+
- 禁止使用旧命名 :auto-append-episodic(spec 层已经废弃)
|
|
185
|
+
- :auto-append 的 #t/#f 值必须保留,不能静默丢失
|
|
186
|
+
- emit context 段输出键必须是 auto_append_episodic(下划线)
|
|
187
|
+
"""
|
|
188
|
+
if not isinstance(memory_cfg, dict):
|
|
189
|
+
return False, make_parse_error_json(
|
|
190
|
+
PARSE_ERR_MEMORY_AUTO_APPEND,
|
|
191
|
+
"FR-PARSER-3",
|
|
192
|
+
f"FR-PARSER-3: memory_cfg={memory_cfg!r} 不是 dict",
|
|
193
|
+
hints=["memory-policy 块必须 emit 为 dict[str, Any]"],
|
|
194
|
+
)
|
|
195
|
+
legacy_key = "auto-append-episodic"
|
|
196
|
+
spec_key = "auto-append"
|
|
197
|
+
target_py_key = "auto_append_episodic"
|
|
198
|
+
if legacy_key in memory_cfg:
|
|
199
|
+
return False, make_parse_error_json(
|
|
200
|
+
PARSE_ERR_MEMORY_AUTO_APPEND,
|
|
201
|
+
"FR-PARSER-3",
|
|
202
|
+
"FR-PARSER-3: 检测到废弃命名 :auto-append-episodic,请改为 :auto-append",
|
|
203
|
+
hints=[
|
|
204
|
+
"spec 层命名规范:使用 :auto-append(禁止旧命名 :auto-append-episodic)",
|
|
205
|
+
"emit 到 Python context_config.auto_append_episodic(下划线分隔)",
|
|
206
|
+
],
|
|
207
|
+
)
|
|
208
|
+
if spec_key not in memory_cfg:
|
|
209
|
+
return True, None
|
|
210
|
+
raw = memory_cfg[spec_key]
|
|
211
|
+
# SRS:DSL 中 #t / #f 对应 Python True / False,值绝不静默丢失
|
|
212
|
+
if raw is None or (isinstance(raw, str) and raw.strip() == ""):
|
|
213
|
+
return False, make_parse_error_json(
|
|
214
|
+
PARSE_ERR_MEMORY_AUTO_APPEND,
|
|
215
|
+
"FR-PARSER-3",
|
|
216
|
+
f"FR-PARSER-3: :auto-append 值 {raw!r} 被静默丢失(#t/#f 必须保留)",
|
|
217
|
+
hints=[
|
|
218
|
+
":auto-append #t 或 #f 必须透传到 context_config.auto_append_episodic = True/False"
|
|
219
|
+
],
|
|
220
|
+
)
|
|
221
|
+
# spec → emit 目标键重命名::auto-append → auto_append_episodic
|
|
222
|
+
if memory_cfg.get(target_py_key) is None:
|
|
223
|
+
return False, make_parse_error_json(
|
|
224
|
+
PARSE_ERR_MEMORY_AUTO_APPEND,
|
|
225
|
+
"FR-PARSER-3",
|
|
226
|
+
f"FR-PARSER-3: spec 命名 :auto-append 未重命名到 Python 下划线键 {target_py_key!r}",
|
|
227
|
+
hints=[
|
|
228
|
+
"parse-memory-policy 必须执行 spec→python 键重命名::auto-append ⇒ auto_append_episodic",
|
|
229
|
+
f"期望 context_config 出现键 {target_py_key},当前 memory_cfg keys={sorted(memory_cfg.keys())}",
|
|
230
|
+
],
|
|
231
|
+
)
|
|
232
|
+
return True, None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def validate_tools_combination_and_mcp_scheme(tools_cfg: Any) -> tuple[bool, dict[str, Any] | None]:
|
|
236
|
+
"""FR-PARSER-4: 支持以下 4 种 tools 组合,都不得崩溃:
|
|
237
|
+
① only define-tool × N
|
|
238
|
+
② only import-builtin
|
|
239
|
+
③ only import-mcp URL (scheme 白名单)
|
|
240
|
+
④ 任意组合
|
|
241
|
+
同时校验 import-mcp 的 URL scheme ∈ MCP_ALLOWED_SCHEMES。
|
|
242
|
+
"""
|
|
243
|
+
if not isinstance(tools_cfg, dict):
|
|
244
|
+
return False, make_parse_error_json(
|
|
245
|
+
PARSE_ERR_TOOLS_COMBINATION,
|
|
246
|
+
"FR-PARSER-4",
|
|
247
|
+
f"FR-PARSER-4: tools_cfg={tools_cfg!r} 不是 dict",
|
|
248
|
+
hints=[
|
|
249
|
+
"tools 块 emit 格式:{'define_tools':[...], 'import_builtin':bool, 'import_mcp':[url,...]}"
|
|
250
|
+
],
|
|
251
|
+
)
|
|
252
|
+
define_tools = tools_cfg.get("define_tools", []) or []
|
|
253
|
+
import_builtin = bool(tools_cfg.get("import_builtin", False))
|
|
254
|
+
import_mcp = tools_cfg.get("import_mcp", []) or []
|
|
255
|
+
_ = import_builtin
|
|
256
|
+
if not isinstance(define_tools, list):
|
|
257
|
+
return False, make_parse_error_json(
|
|
258
|
+
PARSE_ERR_TOOLS_COMBINATION,
|
|
259
|
+
"FR-PARSER-4",
|
|
260
|
+
"FR-PARSER-4: define_tools 必须是 list",
|
|
261
|
+
hints=["① only define-tool:list length>=1 / import_builtin=False / import_mcp=[]"],
|
|
262
|
+
)
|
|
263
|
+
if not isinstance(import_mcp, list):
|
|
264
|
+
return False, make_parse_error_json(
|
|
265
|
+
PARSE_ERR_TOOLS_COMBINATION,
|
|
266
|
+
"FR-PARSER-4",
|
|
267
|
+
"FR-PARSER-4: import_mcp 必须是 list[str]",
|
|
268
|
+
hints=["③ only import-mcp:每个 URL 的 scheme 必须是 {stdio,http+unix,https,sse}"],
|
|
269
|
+
)
|
|
270
|
+
# 4 种组合都允许(只要至少有一种非空或全空也合法)
|
|
271
|
+
# 现在逐一校验 import_mcp URL scheme 白名单
|
|
272
|
+
for url in import_mcp:
|
|
273
|
+
if not isinstance(url, str) or not url:
|
|
274
|
+
return False, make_parse_error_json(
|
|
275
|
+
PARSE_ERR_TOOLS_COMBINATION,
|
|
276
|
+
"FR-PARSER-4",
|
|
277
|
+
f"FR-PARSER-4: import-mcp URL={url!r} 必须是非空字符串",
|
|
278
|
+
hints=[f"MCP scheme 白名单:{sorted(FR_PARSER_MCP_SCHEMES)}"],
|
|
279
|
+
)
|
|
280
|
+
parsed = urlparse(url)
|
|
281
|
+
scheme = (parsed.scheme or "").lower()
|
|
282
|
+
if scheme not in FR_PARSER_MCP_SCHEMES:
|
|
283
|
+
return False, make_parse_error_json(
|
|
284
|
+
PARSE_ERR_TOOLS_COMBINATION,
|
|
285
|
+
"FR-PARSER-4",
|
|
286
|
+
f"FR-PARSER-4: import-mcp scheme={scheme!r} 不在白名单 {sorted(FR_PARSER_MCP_SCHEMES)}",
|
|
287
|
+
hints=[
|
|
288
|
+
"禁止明文 http/ws(请改用 https / sse / stdio / http+unix)",
|
|
289
|
+
f"当前 URL:{url}",
|
|
290
|
+
],
|
|
291
|
+
)
|
|
292
|
+
return True, None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def validate_correct_on_failure(on_failure: Any) -> tuple[bool, dict[str, Any] | None]:
|
|
296
|
+
"""FR-PARSER-5: on-failure ∈ {ask-human, fallback-model, abort}。
|
|
297
|
+
|
|
298
|
+
SRS L89:三枚举「-」连字符命名;其它值(包括下划线 ask_human 变体)
|
|
299
|
+
在 parser 严格模式下视为 FAIL。
|
|
300
|
+
"""
|
|
301
|
+
if not isinstance(on_failure, str) or not on_failure:
|
|
302
|
+
return False, make_parse_error_json(
|
|
303
|
+
PARSE_ERR_ON_FAILURE,
|
|
304
|
+
"FR-PARSER-5",
|
|
305
|
+
f"FR-PARSER-5: on_failure={on_failure!r} 不是合法字符串",
|
|
306
|
+
hints=[f"合法 on-failure 值:{sorted(FR_PARSER_ON_FAILURE_ENUM)}(连字符)"],
|
|
307
|
+
)
|
|
308
|
+
if on_failure in FR_PARSER_ON_FAILURE_ENUM:
|
|
309
|
+
return True, None
|
|
310
|
+
# 下划线是常见误用:在 parser 阶段必须 FAIL(SRS 限定连字符)
|
|
311
|
+
if on_failure in {"ask_human", "fallback_model"}:
|
|
312
|
+
return False, make_parse_error_json(
|
|
313
|
+
PARSE_ERR_ON_FAILURE,
|
|
314
|
+
"FR-PARSER-5",
|
|
315
|
+
f"FR-PARSER-5: on_failure={on_failure!r} 使用了下划线命名,SRS 限定连字符",
|
|
316
|
+
hints=[
|
|
317
|
+
"SRS 规范:on-failure 使用连字符(-),不允许下划线(_)",
|
|
318
|
+
f"纠正:{on_failure.replace('_', '-')}",
|
|
319
|
+
],
|
|
320
|
+
)
|
|
321
|
+
return False, make_parse_error_json(
|
|
322
|
+
PARSE_ERR_ON_FAILURE,
|
|
323
|
+
"FR-PARSER-5",
|
|
324
|
+
f"FR-PARSER-5: on_failure={on_failure!r} 不在枚举 {sorted(FR_PARSER_ON_FAILURE_ENUM)}",
|
|
325
|
+
hints=[f"三选一:{' / '.join(sorted(FR_PARSER_ON_FAILURE_ENUM))}"],
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def validate_topology_and_scoped_worker_min_blocks(
|
|
330
|
+
topology: Any,
|
|
331
|
+
scoped_workers: Any = None,
|
|
332
|
+
) -> tuple[bool, dict[str, Any] | None]:
|
|
333
|
+
"""FR-PARSER-6:
|
|
334
|
+
- topology ∈ {peer, orchestration, decentralised, judge-driven}
|
|
335
|
+
- 每个 scoped-worker 至少含 name + model + tools + harness 四块
|
|
336
|
+
"""
|
|
337
|
+
if not isinstance(topology, str) or topology not in FR_PARSER_TOPOLOGY_ENUM:
|
|
338
|
+
return False, make_parse_error_json(
|
|
339
|
+
PARSE_ERR_TOPOLOGY,
|
|
340
|
+
"FR-PARSER-6",
|
|
341
|
+
f"FR-PARSER-6: topology={topology!r} 不在枚举 {sorted(FR_PARSER_TOPOLOGY_ENUM)}",
|
|
342
|
+
hints=[
|
|
343
|
+
"topology 必须是以下字符串(大小写敏感,连字符):"
|
|
344
|
+
f"{sorted(FR_PARSER_TOPOLOGY_ENUM)}",
|
|
345
|
+
"拼写提示:decentralised 是英式拼写(带 s,不是 centralized)",
|
|
346
|
+
],
|
|
347
|
+
)
|
|
348
|
+
if scoped_workers is None:
|
|
349
|
+
return True, None
|
|
350
|
+
if not isinstance(scoped_workers, list):
|
|
351
|
+
return False, make_parse_error_json(
|
|
352
|
+
PARSE_ERR_TOPOLOGY,
|
|
353
|
+
"FR-PARSER-6",
|
|
354
|
+
"FR-PARSER-6: scoped_workers 必须是 list[dict]",
|
|
355
|
+
hints=["MultiAgent 下 scoped_workers 是每个子 agent 的 dict 列表"],
|
|
356
|
+
)
|
|
357
|
+
for i, worker in enumerate(scoped_workers):
|
|
358
|
+
if not isinstance(worker, dict):
|
|
359
|
+
return False, make_parse_error_json(
|
|
360
|
+
PARSE_ERR_TOPOLOGY,
|
|
361
|
+
"FR-PARSER-6",
|
|
362
|
+
f"FR-PARSER-6: scoped_workers[{i}] 不是 dict",
|
|
363
|
+
hints=[f"每个 scoped-worker 必须包含 {FR_PARSER_SCOPED_WORKER_REQUIRED_BLOCKS}"],
|
|
364
|
+
)
|
|
365
|
+
missing = [k for k in FR_PARSER_SCOPED_WORKER_REQUIRED_BLOCKS if k not in worker]
|
|
366
|
+
if missing:
|
|
367
|
+
return False, make_parse_error_json(
|
|
368
|
+
PARSE_ERR_TOPOLOGY,
|
|
369
|
+
"FR-PARSER-6",
|
|
370
|
+
f"FR-PARSER-6: scoped_workers[{i}] 缺少块 {missing}",
|
|
371
|
+
hints=[
|
|
372
|
+
f"scoped-worker 最小四块:{' + '.join(FR_PARSER_SCOPED_WORKER_REQUIRED_BLOCKS)}",
|
|
373
|
+
f"当前缺:{missing}",
|
|
374
|
+
],
|
|
375
|
+
)
|
|
376
|
+
return True, None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _str(v: Any) -> str:
|
|
380
|
+
if isinstance(v, str):
|
|
381
|
+
return v
|
|
382
|
+
if isinstance(v, bool):
|
|
383
|
+
return "True" if v else "False"
|
|
384
|
+
return str(v)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _get(d: Any, key: str, default: Any = None) -> Any:
|
|
388
|
+
if not isinstance(d, dict):
|
|
389
|
+
return default
|
|
390
|
+
if key in d:
|
|
391
|
+
return d[key]
|
|
392
|
+
sym_key = f":{key}"
|
|
393
|
+
if sym_key in d:
|
|
394
|
+
return d[sym_key]
|
|
395
|
+
kw_repr = f":{key}"
|
|
396
|
+
for k in d:
|
|
397
|
+
ks = _str(k)
|
|
398
|
+
if ks in (key, kw_repr) or ks.lstrip(":") == key:
|
|
399
|
+
return d[k]
|
|
400
|
+
return default
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _order_list(order: Any) -> list[str]:
|
|
404
|
+
if order is None:
|
|
405
|
+
return []
|
|
406
|
+
if not isinstance(order, list):
|
|
407
|
+
return []
|
|
408
|
+
out: list[str] = []
|
|
409
|
+
for item in order:
|
|
410
|
+
s = _str(item).lstrip(":")
|
|
411
|
+
if s:
|
|
412
|
+
out.append(s)
|
|
413
|
+
return out
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _list_of_dicts(x: Any) -> list[dict]:
|
|
417
|
+
if not isinstance(x, list):
|
|
418
|
+
return []
|
|
419
|
+
return [item for item in x if isinstance(item, dict)]
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def check_kv_alignment_order(agent_dict: dict) -> tuple[bool, dict | None]:
|
|
423
|
+
"""FR-CHECK-1: 静态块 :model/:tools 必须在动态块 :context 之前。
|
|
424
|
+
|
|
425
|
+
递归检查 scoped-worker 子块(与 Racket checker.rkt L360-L382 对齐)。
|
|
426
|
+
"""
|
|
427
|
+
order = _order_list(_get(agent_dict, "order"))
|
|
428
|
+
name = _str(_get(agent_dict, "name", "anon"))
|
|
429
|
+
ctx_pos = order.index("context") if "context" in order else None
|
|
430
|
+
model_pos = order.index("model") if "model" in order else None
|
|
431
|
+
tools_pos = order.index("tools") if "tools" in order else None
|
|
432
|
+
if (model_pos is not None and ctx_pos is not None and model_pos > ctx_pos) or (
|
|
433
|
+
tools_pos is not None and ctx_pos is not None and tools_pos > ctx_pos
|
|
434
|
+
):
|
|
435
|
+
return False, make_parse_error_json(
|
|
436
|
+
"ERR_KV_ALIGNMENT_VIOLATION",
|
|
437
|
+
"FR-CHECK-1",
|
|
438
|
+
f"FR-CHECK-1: agent={name} 静态块 (:model/:tools) 出现在动态块 :context 之后(实际顺序={order})",
|
|
439
|
+
agent_name=name,
|
|
440
|
+
hints=[
|
|
441
|
+
"SRS §4.1.1 KV 顺序必须是 :model → :tools → :context(允许缺 harness/multiagent)",
|
|
442
|
+
"把 :context 整块移动到 :tools 之后即可修复",
|
|
443
|
+
],
|
|
444
|
+
)
|
|
445
|
+
multi = _get(agent_dict, "multi")
|
|
446
|
+
workers = _list_of_dicts(_get(multi if isinstance(multi, dict) else {}, "workers"))
|
|
447
|
+
for w in workers:
|
|
448
|
+
w_name = _str(_get(w, "name", "anon-worker"))
|
|
449
|
+
w_order = _order_list(_get(w, "order"))
|
|
450
|
+
w_ctx = w_order.index("context") if "context" in w_order else None
|
|
451
|
+
w_model = w_order.index("model") if "model" in w_order else None
|
|
452
|
+
w_tools = w_order.index("tools") if "tools" in w_order else None
|
|
453
|
+
if (w_model is not None and w_ctx is not None and w_model > w_ctx) or (
|
|
454
|
+
w_tools is not None and w_ctx is not None and w_tools > w_ctx
|
|
455
|
+
):
|
|
456
|
+
return False, make_parse_error_json(
|
|
457
|
+
"ERR_KV_ALIGNMENT_VIOLATION",
|
|
458
|
+
"FR-CHECK-1",
|
|
459
|
+
f"FR-CHECK-1: scoped-worker={w_name}: 静态块出现在动态块之后(实际顺序={w_order})",
|
|
460
|
+
agent_name=w_name,
|
|
461
|
+
hints=[
|
|
462
|
+
"scoped-worker 的 :model/:tools 也必须在 :context 之前(SRS §4.3 FR-MAGT-1)",
|
|
463
|
+
"把该 worker 的 :context 移到 :tools 之后即可",
|
|
464
|
+
],
|
|
465
|
+
)
|
|
466
|
+
return True, None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def check_unguarded_tool_execution(agent_dict: dict) -> tuple[bool, dict | None]:
|
|
470
|
+
"""FR-CHECK-2: 有副作用 builtin(SIDEEFFECT_BUILTIN_TOOLS 8 项)必须满足:
|
|
471
|
+
(A) require_human_approval 明确 AND forbidden_commands 至少 1 条非空;
|
|
472
|
+
OR (B) verify 任一字段 json_schema/linter_check/test_runner/reviewer_agent 真/非空。
|
|
473
|
+
递归检查 scoped-worker(Racket checker.rkt L460-L490 对齐)。
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
def _sideeffect_names(tools_dict: Any) -> list[str]:
|
|
477
|
+
builtins = _get(tools_dict, "builtins")
|
|
478
|
+
if builtins is None:
|
|
479
|
+
builtins = _get(tools_dict, "import_builtins", [])
|
|
480
|
+
if not isinstance(builtins, list):
|
|
481
|
+
return []
|
|
482
|
+
return [_str(b) for b in builtins if _str(b) in SIDEEFFECT_BUILTIN_TOOLS]
|
|
483
|
+
|
|
484
|
+
def _approval_ok(approval_list: Any, name: str) -> bool:
|
|
485
|
+
if not isinstance(approval_list, list):
|
|
486
|
+
return False
|
|
487
|
+
return any(_str(a) == name for a in approval_list)
|
|
488
|
+
|
|
489
|
+
def _forbidden_non_empty(forbidden_list: Any) -> bool:
|
|
490
|
+
if not isinstance(forbidden_list, list):
|
|
491
|
+
return False
|
|
492
|
+
return any(isinstance(f, str) and f.strip() != "" for f in forbidden_list)
|
|
493
|
+
|
|
494
|
+
def _verify_any(verify_dict: Any) -> bool:
|
|
495
|
+
if not isinstance(verify_dict, dict):
|
|
496
|
+
return False
|
|
497
|
+
for k in ("json_schema", "linter_check", "test_runner", "reviewer_agent"):
|
|
498
|
+
v = _get(verify_dict, k)
|
|
499
|
+
if v is None:
|
|
500
|
+
continue
|
|
501
|
+
if isinstance(v, bool) and v:
|
|
502
|
+
return True
|
|
503
|
+
if isinstance(v, str) and v.strip() != "":
|
|
504
|
+
return True
|
|
505
|
+
if isinstance(v, (dict, list)) and len(v) > 0:
|
|
506
|
+
return True
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
def _check_one(name: str, tools_dict: Any, harness_dict: Any) -> tuple[bool, dict | None]:
|
|
510
|
+
se = _sideeffect_names(tools_dict)
|
|
511
|
+
if not se:
|
|
512
|
+
return True, None
|
|
513
|
+
constrain = _get(harness_dict, "constrain", {}) or {}
|
|
514
|
+
verify = _get(harness_dict, "verify", {}) or {}
|
|
515
|
+
approval = _get(constrain, "require_human_approval", [])
|
|
516
|
+
forbidden = _get(constrain, "forbidden_commands", [])
|
|
517
|
+
has_ver = _verify_any(verify)
|
|
518
|
+
unguarded: list[str] = []
|
|
519
|
+
for n in se:
|
|
520
|
+
ok = has_ver or (_approval_ok(approval, n) and _forbidden_non_empty(forbidden))
|
|
521
|
+
if not ok:
|
|
522
|
+
unguarded.append(n)
|
|
523
|
+
if unguarded:
|
|
524
|
+
return False, make_parse_error_json(
|
|
525
|
+
"ERR_UNGUARDED_TOOL_EXECUTION",
|
|
526
|
+
"FR-CHECK-2",
|
|
527
|
+
(
|
|
528
|
+
f"FR-CHECK-2: agent={name} 有副作用工具={unguarded} 未被护栏保护:"
|
|
529
|
+
f"要求 (require_approval 明确列出 AND forbidden 非空) OR verify 任一项断言开启;"
|
|
530
|
+
f"当前 approval={approval} forbidden={forbidden} verify 开={has_ver}"
|
|
531
|
+
),
|
|
532
|
+
agent_name=name,
|
|
533
|
+
hints=[
|
|
534
|
+
f"工具 {unguarded} 在 SIDEEFFECT_BUILTIN_TOOLS 内(bash/git-push/wget/curl/scp/dd/chmod/sudo 默认都是),必须有护栏",
|
|
535
|
+
"方法 A:把工具名加到 (:constrain :require-human-approval (TOOL…)),并确保 forbidden-commands 至少 1 条非空",
|
|
536
|
+
'方法 B:(:verify :json-schema #t / :linter-check #t / :test-runner "…" / :reviewer-agent "judge") 任一项开启',
|
|
537
|
+
],
|
|
538
|
+
)
|
|
539
|
+
return True, None
|
|
540
|
+
|
|
541
|
+
name = _str(_get(agent_dict, "name", "anon"))
|
|
542
|
+
ok, err = _check_one(name, _get(agent_dict, "tools", {}), _get(agent_dict, "harness", {}))
|
|
543
|
+
if not ok:
|
|
544
|
+
return False, err
|
|
545
|
+
multi = _get(agent_dict, "multi")
|
|
546
|
+
workers = _list_of_dicts(_get(multi if isinstance(multi, dict) else {}, "workers"))
|
|
547
|
+
for w in workers:
|
|
548
|
+
w_name = _str(_get(w, "name", "anon-worker"))
|
|
549
|
+
ok, err = _check_one(w_name, _get(w, "tools", {}), _get(w, "harness", {}))
|
|
550
|
+
if not ok:
|
|
551
|
+
return False, err
|
|
552
|
+
return True, None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def check_context_leakage(agent_dict: dict) -> tuple[bool, dict | None]:
|
|
556
|
+
"""FR-CHECK-3: (a) 父子 define-tools 精确重名;(b) 兄弟 scoped-worker 定义同名工具。
|
|
557
|
+
|
|
558
|
+
前缀非重名(write-md vs write-md-worker)必须 PASS(反误杀)。
|
|
559
|
+
与 Racket checker.rkt L500-L540 对齐:字符串精确相等,不是子串/前缀匹配。
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
def _define_tool_names(tools_dict: Any) -> list[str]:
|
|
563
|
+
dts = _get(tools_dict, "define_tools")
|
|
564
|
+
if dts is None:
|
|
565
|
+
dts = _get(tools_dict, "define-tools", [])
|
|
566
|
+
if not isinstance(dts, list):
|
|
567
|
+
return []
|
|
568
|
+
names: list[str] = []
|
|
569
|
+
for t in dts:
|
|
570
|
+
if isinstance(t, dict):
|
|
571
|
+
n = _str(_get(t, "name", ""))
|
|
572
|
+
if n:
|
|
573
|
+
names.append(n)
|
|
574
|
+
elif isinstance(t, str):
|
|
575
|
+
if t:
|
|
576
|
+
names.append(t)
|
|
577
|
+
return names
|
|
578
|
+
|
|
579
|
+
name = _str(_get(agent_dict, "name", "anon"))
|
|
580
|
+
multi = _get(agent_dict, "multi")
|
|
581
|
+
multi_dict = multi if isinstance(multi, dict) else {}
|
|
582
|
+
workers = _list_of_dicts(_get(multi_dict, "workers"))
|
|
583
|
+
|
|
584
|
+
parent_names = _define_tool_names(_get(agent_dict, "tools", {}))
|
|
585
|
+
all_worker_names: list[tuple[str, str]] = [] # (worker_name, tool_name)
|
|
586
|
+
for w in workers:
|
|
587
|
+
w_name = _str(_get(w, "name", "anon-worker"))
|
|
588
|
+
for tn in _define_tool_names(_get(w, "tools", {})):
|
|
589
|
+
all_worker_names.append((w_name, tn))
|
|
590
|
+
|
|
591
|
+
for w_name, tn in all_worker_names:
|
|
592
|
+
if tn in parent_names:
|
|
593
|
+
return False, make_parse_error_json(
|
|
594
|
+
"ERR_CONTEXT_LEAKAGE",
|
|
595
|
+
"FR-CHECK-3",
|
|
596
|
+
(
|
|
597
|
+
f"FR-CHECK-3: scoped-worker={w_name} 的工具名 {tn!r} 与父级 define-tools 重名;"
|
|
598
|
+
"离开作用域后可能导致父级 trajectory 意外复用,违反词法隔离"
|
|
599
|
+
),
|
|
600
|
+
agent_name=name,
|
|
601
|
+
hints=[
|
|
602
|
+
f"工具 {tn!r} 与父级 define-tools 重名(FR-CHECK-3 不变量),请重命名该 worker 工具",
|
|
603
|
+
"SRS §4.3:所有 scoped-worker define-tools 名必须父子/兄弟两两不重名",
|
|
604
|
+
],
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
seen: dict[str, str] = {}
|
|
608
|
+
for w_name, tn in all_worker_names:
|
|
609
|
+
if tn in seen:
|
|
610
|
+
return False, make_parse_error_json(
|
|
611
|
+
"ERR_CONTEXT_LEAKAGE",
|
|
612
|
+
"FR-CHECK-3",
|
|
613
|
+
(
|
|
614
|
+
f"FR-CHECK-3: 兄弟 scoped-worker 工具名冲突:worker={seen[tn]!r} 与 "
|
|
615
|
+
f"worker={w_name!r} 均定义 {tn!r};可能导致 cross-worker trajectory 混淆"
|
|
616
|
+
),
|
|
617
|
+
agent_name=w_name,
|
|
618
|
+
hints=[
|
|
619
|
+
f"重名工具 {tn!r}:请把两个 worker 的 define-tools 名改为不同(例如加前缀 w1- / w2-)",
|
|
620
|
+
"SRS FR-CHECK-3:兄弟 worker 的工具名必须两两互斥(编译期就断,避免运行时轨迹泄漏)",
|
|
621
|
+
],
|
|
622
|
+
)
|
|
623
|
+
seen[tn] = w_name
|
|
624
|
+
|
|
625
|
+
return True, None
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
__all__ = [
|
|
629
|
+
"FR_PARSER_MCP_SCHEMES",
|
|
630
|
+
"FR_PARSER_ON_FAILURE_ENUM",
|
|
631
|
+
"FR_PARSER_PROVIDER_ENUM",
|
|
632
|
+
"FR_PARSER_SCOPED_WORKER_REQUIRED_BLOCKS",
|
|
633
|
+
"FR_PARSER_TOPOLOGY_ENUM",
|
|
634
|
+
"PARSER_ERROR_SCHEMA_VERSION",
|
|
635
|
+
"PARSE_ERR_ILLEGAL_SEXP",
|
|
636
|
+
"PARSE_ERR_MEMORY_AUTO_APPEND",
|
|
637
|
+
"PARSE_ERR_ON_FAILURE",
|
|
638
|
+
"PARSE_ERR_PROVIDER_OR_TEMP",
|
|
639
|
+
"PARSE_ERR_TOOLS_COMBINATION",
|
|
640
|
+
"PARSE_ERR_TOPOLOGY",
|
|
641
|
+
"SIDEEFFECT_BUILTIN_TOOLS",
|
|
642
|
+
"check_context_leakage",
|
|
643
|
+
"check_kv_alignment_order",
|
|
644
|
+
"check_sideeffect_builtins_racket_mirror",
|
|
645
|
+
"check_unguarded_tool_execution",
|
|
646
|
+
"extract_checker_rkt_sideeffect_list",
|
|
647
|
+
"make_parse_error_json",
|
|
648
|
+
"sideeffect_builtin_names",
|
|
649
|
+
"validate_correct_on_failure",
|
|
650
|
+
"validate_memory_auto_append_key",
|
|
651
|
+
"validate_provider_and_temperature",
|
|
652
|
+
"validate_tools_combination_and_mcp_scheme",
|
|
653
|
+
"validate_topology_and_scoped_worker_min_blocks",
|
|
654
|
+
]
|