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
host/workflow.py
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
"""Workflow runner: DirectRunner (always available) + TemporalRunner (optional dep).
|
|
2
|
+
|
|
3
|
+
额外提供 HITL 人在回路的内存版 InMemoryHITLRunner(NFR-SEC-1b):
|
|
4
|
+
- 若约束里 require_approval 的工具被调用 → status=human_required 且停在 require_approval_queue;
|
|
5
|
+
- 调用 approve(run_id, tool_name) / reject(run_id, tool_name) 后 workflow 继续(或失败)。
|
|
6
|
+
|
|
7
|
+
新增 CR-15:§3 / §6.2 AC-2 RepairAgentPipeline(4 阶段 Submission→Constrain→Execute→Verify→Correct Loop→Final):
|
|
8
|
+
- on_failure ∈ {ask-human, fallback-model, abort}(对齐 FR-PARSER-5 枚举,自动兼容 ask_human 下划线)
|
|
9
|
+
- ExecutionTrace 落盘,trace.error 精确写明 strategy=on_failure(对齐 FR-RUN-4 Correct 熔断)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import contextlib
|
|
16
|
+
import hashlib
|
|
17
|
+
import os
|
|
18
|
+
import uuid
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from collections import deque
|
|
21
|
+
from dataclasses import asdict, dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from runtime.base_harness import BaseHarness, ExecutionTrace
|
|
26
|
+
from runtime.errors import FeatureNotInstalledError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class WorkflowRequest:
|
|
31
|
+
agent_name: str
|
|
32
|
+
harness: BaseHarness
|
|
33
|
+
inputs: dict[str, Any]
|
|
34
|
+
workflow: str | None = None
|
|
35
|
+
run_id: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class HITLSuspension:
|
|
40
|
+
run_id: str
|
|
41
|
+
tool_name: str
|
|
42
|
+
args: dict[str, Any]
|
|
43
|
+
approvable_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
44
|
+
decision: str | None = None # "approve" | "reject"
|
|
45
|
+
|
|
46
|
+
def resolve(self, decision: str) -> None:
|
|
47
|
+
self.decision = decision
|
|
48
|
+
self.approvable_event.set()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WorkflowRunner(ABC):
|
|
52
|
+
@abstractmethod
|
|
53
|
+
async def submit(self, req: WorkflowRequest) -> str: ...
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def wait(self, run_id: str, timeout: float = 300.0) -> ExecutionTrace: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DirectRunner(WorkflowRunner):
|
|
59
|
+
"""In-process async runner. Always available — zero external dependencies."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self.traces: dict[str, ExecutionTrace] = {}
|
|
63
|
+
self._tasks: dict[str, asyncio.Task[ExecutionTrace]] = {}
|
|
64
|
+
|
|
65
|
+
async def submit(self, req: WorkflowRequest) -> str:
|
|
66
|
+
run_id = req.run_id or os.urandom(8).hex()
|
|
67
|
+
|
|
68
|
+
async def _task() -> ExecutionTrace:
|
|
69
|
+
trace = await req.harness.run_async(inputs=req.inputs, workflow=req.workflow)
|
|
70
|
+
self.traces[run_id] = trace
|
|
71
|
+
return trace
|
|
72
|
+
|
|
73
|
+
self._tasks[run_id] = asyncio.create_task(_task())
|
|
74
|
+
return run_id
|
|
75
|
+
|
|
76
|
+
async def wait(self, run_id: str, timeout: float = 300.0) -> ExecutionTrace:
|
|
77
|
+
if run_id in self.traces:
|
|
78
|
+
return self.traces[run_id]
|
|
79
|
+
task = self._tasks.get(run_id)
|
|
80
|
+
if task is None:
|
|
81
|
+
raise KeyError(f"no run_id: {run_id}")
|
|
82
|
+
trace = await asyncio.wait_for(task, timeout=timeout)
|
|
83
|
+
return trace
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class InMemoryHITLRunner(WorkflowRunner):
|
|
87
|
+
"""SRS NFR-SEC-1b:内存版 HITL Runner,不依赖 temporalio,可本地 pytest。
|
|
88
|
+
|
|
89
|
+
工作流:
|
|
90
|
+
1. submit(req) 启动一个长运行 task;
|
|
91
|
+
2. task 里按 Harness 的顺序每步跑 step(1),
|
|
92
|
+
在每次 Constrain → Execute → Verify → Correct 前,若 tool_name 在 harness_config/correct/require_approval 里
|
|
93
|
+
或 constrain_config 的 require_approval 里 → 入队 suspend_queue → status_bar/human_required → 等待 approve();
|
|
94
|
+
3. approve() → 放行 execute;reject() → 立即 failed,trace.status="failed" 且 error="human_rejected"。
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self) -> None:
|
|
98
|
+
self.traces: dict[str, Any] = {}
|
|
99
|
+
self._tasks: dict[str, asyncio.Task[Any]] = {}
|
|
100
|
+
self.suspend_queue: deque[HITLSuspension] = deque()
|
|
101
|
+
self._by_run_tool: dict[tuple[str, str], HITLSuspension] = {}
|
|
102
|
+
self.events: list[dict[str, Any]] = []
|
|
103
|
+
|
|
104
|
+
def _require_approval_tools(self, harness: Any) -> list[str]:
|
|
105
|
+
tools: list[str] = []
|
|
106
|
+
hcfg = getattr(harness, "harness_config", None) or {}
|
|
107
|
+
ccfg = getattr(harness, "context_config", None) or {}
|
|
108
|
+
correct_cfg = hcfg.get("correct", {}) or {}
|
|
109
|
+
tools.extend(list(correct_cfg.get("require_approval", []) or []))
|
|
110
|
+
constrain_cfg_harness = hcfg.get("constrain", {}) or {}
|
|
111
|
+
tools.extend(list(constrain_cfg_harness.get("require_approval", []) or []))
|
|
112
|
+
constrain_cfg_context = ccfg.get("constrain", {}) or ccfg.get("constraints", {}) or {}
|
|
113
|
+
tools.extend(list(constrain_cfg_context.get("require_approval", []) or []))
|
|
114
|
+
out: list[str] = []
|
|
115
|
+
for t in tools:
|
|
116
|
+
if isinstance(t, str) and t and t not in out:
|
|
117
|
+
out.append(t)
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
async def submit(self, req: WorkflowRequest) -> str:
|
|
121
|
+
run_id = req.run_id or os.urandom(8).hex()
|
|
122
|
+
harness = req.harness
|
|
123
|
+
user_input = (req.inputs or {}).get("user_input") or (req.inputs or {}).get("query") or ""
|
|
124
|
+
|
|
125
|
+
async def _task() -> Any:
|
|
126
|
+
require_approval_set = set(self._require_approval_tools(harness))
|
|
127
|
+
trace = harness._trace
|
|
128
|
+
# v2 版 Harness 初始化 _trace
|
|
129
|
+
if trace is None and hasattr(harness, "run"):
|
|
130
|
+
trace = await harness.run(user_input)
|
|
131
|
+
self.traces[run_id] = trace
|
|
132
|
+
return trace
|
|
133
|
+
max_turns = int(getattr(harness, "max_turns", 3) or 3)
|
|
134
|
+
for _ in range(max_turns):
|
|
135
|
+
# 在 _react_step 之前:用 v2 接口先看 turn.action 是否命中 require_approval
|
|
136
|
+
# 直接把 Harness 的 _react_step 包一层:注册 approve/reject 钩子
|
|
137
|
+
if require_approval_set:
|
|
138
|
+
|
|
139
|
+
async def _wrapped_react_step(orig_step_fn):
|
|
140
|
+
# 先把 action 从下一个要调用的 turn 里预读?更稳的方法是在 constrain 之后 execute 前:
|
|
141
|
+
# 这里简化:用 status_bar events 反查(但 status_bar 是可观测,不提供 action pre-hook)
|
|
142
|
+
# 最终方法:把 harness.constrain 包一层 Wrapper
|
|
143
|
+
return await orig_step_fn()
|
|
144
|
+
|
|
145
|
+
_ = _wrapped_react_step # 占位;真实路径用另一种:直接跑 run(),在 constrain 里通过 side channel 抛 HITLNeedApproval
|
|
146
|
+
result = await harness._react_step() if hasattr(harness, "_react_step") else None
|
|
147
|
+
if result is None:
|
|
148
|
+
break
|
|
149
|
+
st = result.get("status")
|
|
150
|
+
if st in ("success", "failed", "blocked", "human_required"):
|
|
151
|
+
break
|
|
152
|
+
self.traces[run_id] = harness._trace
|
|
153
|
+
return harness._trace
|
|
154
|
+
|
|
155
|
+
async def _task_via_run() -> Any:
|
|
156
|
+
"""为了不依赖 turn.action 的先验,直接在 Constrain 阶段注入 Hook:
|
|
157
|
+
monkeypatch harness.constrain → 命中 require_approval 时放 suspension。"""
|
|
158
|
+
require_approval_set = set(self._require_approval_tools(harness))
|
|
159
|
+
orig_constrain = harness.constrain
|
|
160
|
+
run_state: dict[str, Any] = {"active_suspension": None}
|
|
161
|
+
|
|
162
|
+
def _emit_event(kind: str, **extra: Any) -> None:
|
|
163
|
+
self.events.append({"run_id": run_id, "kind": kind, **extra})
|
|
164
|
+
try:
|
|
165
|
+
sb = getattr(harness, "status_bar", None)
|
|
166
|
+
if sb is not None and hasattr(sb, "on_step"):
|
|
167
|
+
sb.on_step(
|
|
168
|
+
run_id,
|
|
169
|
+
len(getattr(harness, "_trace", None).turns if harness._trace else []),
|
|
170
|
+
kind,
|
|
171
|
+
str(extra),
|
|
172
|
+
)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
def _hooked_constrain(tool_call):
|
|
177
|
+
nonlocal run_state
|
|
178
|
+
allowed, reason = orig_constrain(tool_call)
|
|
179
|
+
tool_name = tool_call.get("tool_name", "") if isinstance(tool_call, dict) else ""
|
|
180
|
+
if not require_approval_set:
|
|
181
|
+
return allowed, reason
|
|
182
|
+
if tool_name in require_approval_set and allowed:
|
|
183
|
+
args = tool_call.get("args", {}) if isinstance(tool_call, dict) else {}
|
|
184
|
+
susp = HITLSuspension(run_id=run_id, tool_name=tool_name, args=args)
|
|
185
|
+
self.suspend_queue.append(susp)
|
|
186
|
+
self._by_run_tool[(run_id, tool_name)] = susp
|
|
187
|
+
run_state["active_suspension"] = susp
|
|
188
|
+
_emit_event("human_required", tool_name=tool_name, args=args)
|
|
189
|
+
# 同步等待审批(在 asyncio 事件循环里:必须改为 yield)。简化:抛 HITLNeedApproval 并在 catch 里 await。
|
|
190
|
+
raise _HITLNeedApproval(susp)
|
|
191
|
+
return allowed, reason
|
|
192
|
+
|
|
193
|
+
harness.constrain = _hooked_constrain # type: ignore[method-assign]
|
|
194
|
+
try:
|
|
195
|
+
while True:
|
|
196
|
+
try:
|
|
197
|
+
trace = await harness.run(user_input)
|
|
198
|
+
self.traces[run_id] = trace
|
|
199
|
+
return trace
|
|
200
|
+
except _HITLNeedApproval as hitl_exc:
|
|
201
|
+
susp = hitl_exc.suspension
|
|
202
|
+
# 等待 approve/reject
|
|
203
|
+
await asyncio.wait_for(susp.approvable_event.wait(), timeout=86_400.0)
|
|
204
|
+
if susp.decision == "reject":
|
|
205
|
+
self.events.append(
|
|
206
|
+
{
|
|
207
|
+
"run_id": run_id,
|
|
208
|
+
"kind": "human_rejected",
|
|
209
|
+
"tool_name": susp.tool_name,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
harness.status_bar.on_step(
|
|
214
|
+
run_id,
|
|
215
|
+
len(harness._trace.turns) if harness._trace else 0,
|
|
216
|
+
"rejected",
|
|
217
|
+
susp.tool_name,
|
|
218
|
+
)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
# 将 trace.status 置 failed + error=human_rejected
|
|
222
|
+
if harness._trace is not None:
|
|
223
|
+
harness._trace.status = "failed"
|
|
224
|
+
harness._trace.error = f"human_rejected: tool={susp.tool_name}"
|
|
225
|
+
return harness._trace
|
|
226
|
+
assert susp.decision == "approve"
|
|
227
|
+
self.events.append(
|
|
228
|
+
{
|
|
229
|
+
"run_id": run_id,
|
|
230
|
+
"kind": "human_approved",
|
|
231
|
+
"tool_name": susp.tool_name,
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
# 继续下一循环,此时再次调用 constrain → 同一个 tool_name 会再次命中 require_approval,需要放行一次:
|
|
235
|
+
# 使用 allow_once set
|
|
236
|
+
require_approval_set.discard(susp.tool_name)
|
|
237
|
+
continue
|
|
238
|
+
finally:
|
|
239
|
+
harness.constrain = orig_constrain # type: ignore[method-assign]
|
|
240
|
+
|
|
241
|
+
self._tasks[run_id] = asyncio.create_task(_task_via_run())
|
|
242
|
+
return run_id
|
|
243
|
+
|
|
244
|
+
async def wait(self, run_id: str, timeout: float = 300.0) -> Any:
|
|
245
|
+
if run_id in self.traces:
|
|
246
|
+
return self.traces[run_id]
|
|
247
|
+
task = self._tasks.get(run_id)
|
|
248
|
+
if task is None:
|
|
249
|
+
raise KeyError(f"no run_id: {run_id}")
|
|
250
|
+
trace = await asyncio.wait_for(task, timeout=timeout)
|
|
251
|
+
return trace
|
|
252
|
+
|
|
253
|
+
@contextlib.contextmanager
|
|
254
|
+
def patch_harness_constrain(self, harness: Any) -> Any:
|
|
255
|
+
"""用于 BDD / pytest 单测:
|
|
256
|
+
模拟 `submit(user_input, harness)` 的 Constrain Hook(不真正创建 async task,
|
|
257
|
+
同步地在 harness.constrain 里注入 Suspension + human_required 事件)。
|
|
258
|
+
用法:
|
|
259
|
+
runner = InMemoryHITLRunner()
|
|
260
|
+
with runner.patch_harness_constrain(harness):
|
|
261
|
+
harness.constrain({"tool_name": "git-push", ...}) # 抛 _HITLNeedApproval
|
|
262
|
+
"""
|
|
263
|
+
require_approval_set = set(self._require_approval_tools(harness))
|
|
264
|
+
orig_constrain = harness.constrain
|
|
265
|
+
run_state: dict[str, Any] = {"active_suspension": None}
|
|
266
|
+
run_id = f"patch-{uuid.uuid4().hex[:8]}"
|
|
267
|
+
|
|
268
|
+
def _hooked(tool_call):
|
|
269
|
+
nonlocal run_state
|
|
270
|
+
allowed, reason = orig_constrain(tool_call)
|
|
271
|
+
tool_name = tool_call.get("tool_name", "") if isinstance(tool_call, dict) else ""
|
|
272
|
+
if not require_approval_set:
|
|
273
|
+
return allowed, reason
|
|
274
|
+
if tool_name in require_approval_set and allowed:
|
|
275
|
+
args = tool_call.get("args", {}) if isinstance(tool_call, dict) else {}
|
|
276
|
+
susp = HITLSuspension(run_id=run_id, tool_name=tool_name, args=args)
|
|
277
|
+
self.suspend_queue.append(susp)
|
|
278
|
+
self._by_run_tool[(run_id, tool_name)] = susp
|
|
279
|
+
run_state["active_suspension"] = susp
|
|
280
|
+
self.events.append(
|
|
281
|
+
{
|
|
282
|
+
"run_id": run_id,
|
|
283
|
+
"kind": "human_required",
|
|
284
|
+
"tool_name": tool_name,
|
|
285
|
+
"args": args,
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
raise _HITLNeedApproval(susp)
|
|
289
|
+
return allowed, reason
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
harness.constrain = _hooked # type: ignore[method-assign]
|
|
293
|
+
yield self
|
|
294
|
+
finally:
|
|
295
|
+
harness.constrain = orig_constrain # type: ignore[method-assign]
|
|
296
|
+
|
|
297
|
+
async def run_approve(
|
|
298
|
+
self,
|
|
299
|
+
harness: Any,
|
|
300
|
+
*,
|
|
301
|
+
user_input: str,
|
|
302
|
+
llm: Any,
|
|
303
|
+
tool_name: str,
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
|
+
"""BDD 便利方法:等价于 submit → (在 HITL 挂起时立刻 approve 一次) → wait。
|
|
306
|
+
|
|
307
|
+
实现说明:submit 返回 run_id 后立即 asyncio.sleep(0) 让任务跑到第一次 Constrain,
|
|
308
|
+
接着 approve 一次 suspension,再 wait。返回 trace dict。
|
|
309
|
+
"""
|
|
310
|
+
rid = await self.submit(
|
|
311
|
+
WorkflowRequest(
|
|
312
|
+
agent_name=getattr(harness, "agent_name", "bdd-harness"),
|
|
313
|
+
harness=harness,
|
|
314
|
+
inputs={"user_input": user_input, "llm": llm},
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
# 最多等 5s 让 suspension 入队(harness 要先跑 constrain hook)
|
|
318
|
+
deadline = asyncio.get_running_loop().time() + 5.0
|
|
319
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
320
|
+
if (rid, tool_name) in self._by_run_tool:
|
|
321
|
+
break
|
|
322
|
+
await asyncio.sleep(0.02)
|
|
323
|
+
await self.approve(rid, tool_name)
|
|
324
|
+
trace = await self.wait(rid, timeout=30.0)
|
|
325
|
+
try:
|
|
326
|
+
td = trace.to_dict() if hasattr(trace, "to_dict") else trace
|
|
327
|
+
return {"status": td.get("status", "unknown"), "trace": td, "run_id": rid}
|
|
328
|
+
except Exception:
|
|
329
|
+
return {"status": getattr(trace, "status", "unknown"), "trace": trace, "run_id": rid}
|
|
330
|
+
|
|
331
|
+
async def approve(self, run_id: str, tool_name: str) -> None:
|
|
332
|
+
key = (run_id, tool_name)
|
|
333
|
+
susp = self._by_run_tool.get(key)
|
|
334
|
+
if susp is None:
|
|
335
|
+
# 兼容:等一会儿让事件产生(HITL suspend 是异步入队的)
|
|
336
|
+
deadline = asyncio.get_running_loop().time() + 5.0
|
|
337
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
338
|
+
susp = self._by_run_tool.get(key)
|
|
339
|
+
if susp is not None:
|
|
340
|
+
break
|
|
341
|
+
await asyncio.sleep(0.05)
|
|
342
|
+
if susp is None:
|
|
343
|
+
raise KeyError(f"no suspension for run_id={run_id} tool={tool_name}")
|
|
344
|
+
susp.resolve("approve")
|
|
345
|
+
|
|
346
|
+
async def reject(self, run_id: str, tool_name: str) -> None:
|
|
347
|
+
key = (run_id, tool_name)
|
|
348
|
+
susp = self._by_run_tool.get(key)
|
|
349
|
+
if susp is None:
|
|
350
|
+
deadline = asyncio.get_running_loop().time() + 5.0
|
|
351
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
352
|
+
susp = self._by_run_tool.get(key)
|
|
353
|
+
if susp is not None:
|
|
354
|
+
break
|
|
355
|
+
await asyncio.sleep(0.05)
|
|
356
|
+
if susp is None:
|
|
357
|
+
raise KeyError(f"no suspension for run_id={run_id} tool={tool_name}")
|
|
358
|
+
susp.resolve("reject")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class _HITLNeedApproval(Exception): # noqa: N818
|
|
362
|
+
def __init__(self, suspension: HITLSuspension) -> None:
|
|
363
|
+
super().__init__(f"HITL approve needed: tool={suspension.tool_name}")
|
|
364
|
+
self.suspension = suspension
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class TemporalRunner(WorkflowRunner):
|
|
368
|
+
"""Durable execution via Temporal. Requires `uv pip install 'agentlisp[durable]'`."""
|
|
369
|
+
|
|
370
|
+
def __init__(
|
|
371
|
+
self,
|
|
372
|
+
*,
|
|
373
|
+
temporal_client: Any | None = None,
|
|
374
|
+
task_queue: str = "agentlisp",
|
|
375
|
+
namespace: str = "default",
|
|
376
|
+
host_port: str = "localhost:7233",
|
|
377
|
+
) -> None:
|
|
378
|
+
try:
|
|
379
|
+
import temporalio # noqa: F401 # type: ignore[import-not-found]
|
|
380
|
+
except ImportError as exc:
|
|
381
|
+
raise FeatureNotInstalledError("Temporal runner", "durable") from exc
|
|
382
|
+
self._client = temporal_client
|
|
383
|
+
self.task_queue = task_queue
|
|
384
|
+
self.namespace = namespace
|
|
385
|
+
self.host_port = host_port
|
|
386
|
+
|
|
387
|
+
async def _get_client(self) -> Any: # pragma: no cover - temporal not in CI env
|
|
388
|
+
if self._client is None:
|
|
389
|
+
from temporalio.client import Client # type: ignore[import-not-found]
|
|
390
|
+
|
|
391
|
+
self._client = await Client.connect(target_url=self.host_port, namespace=self.namespace)
|
|
392
|
+
return self._client
|
|
393
|
+
|
|
394
|
+
async def submit(self, req: WorkflowRequest) -> str: # pragma: no cover
|
|
395
|
+
raise NotImplementedError(
|
|
396
|
+
"TemporalRunner.submit skeleton: wire Harness.run_async into a Temporal workflow "
|
|
397
|
+
"once your durable workflow/activities are defined."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
async def wait(self, run_id: str, timeout: float = 300.0) -> ExecutionTrace: # pragma: no cover
|
|
401
|
+
raise NotImplementedError("TemporalRunner.wait skeleton")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def default_runner(prefer_temporal: bool | None = None) -> WorkflowRunner:
|
|
405
|
+
want = (
|
|
406
|
+
prefer_temporal
|
|
407
|
+
if prefer_temporal is not None
|
|
408
|
+
else (os.getenv("AGENTLISP_RUNNER", "direct").lower() == "temporal")
|
|
409
|
+
)
|
|
410
|
+
if want:
|
|
411
|
+
try:
|
|
412
|
+
return TemporalRunner()
|
|
413
|
+
except FeatureNotInstalledError:
|
|
414
|
+
pass
|
|
415
|
+
return DirectRunner()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# =============================================================================
|
|
419
|
+
# CR-15 §6.2 AC-2:repair_agent 4 阶段 Pipeline 真跑
|
|
420
|
+
# =============================================================================
|
|
421
|
+
|
|
422
|
+
CORRECT_ON_FAILURE_ENUM: set[str] = {
|
|
423
|
+
"ask-human",
|
|
424
|
+
"fallback-model",
|
|
425
|
+
"abort",
|
|
426
|
+
"ask_human",
|
|
427
|
+
"fallback_model",
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _normalize_on_failure(raw: str | None) -> str:
|
|
432
|
+
"""FR-PARSER-5 三枚举对齐;ask_human 下划线兼容 ask-human 连字符(SRS 写连字符,Python 写点号易混)。
|
|
433
|
+
|
|
434
|
+
规范化:ask-human / fallback-model / abort。
|
|
435
|
+
"""
|
|
436
|
+
r = (raw or "ask-human").strip().lower().replace("_", "-")
|
|
437
|
+
if r not in CORRECT_ON_FAILURE_ENUM:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"correct.on_failure={raw!r} 不在 FR-PARSER-5 枚举 {{ask-human, fallback-model, abort}}"
|
|
440
|
+
)
|
|
441
|
+
return r
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass
|
|
445
|
+
class RepairSubmission:
|
|
446
|
+
"""4 阶段第 1 步:Submission,SRS §6.2 repair_agent task。"""
|
|
447
|
+
|
|
448
|
+
target_file: str
|
|
449
|
+
buggy_source: str
|
|
450
|
+
failing_pytest_output: str
|
|
451
|
+
expected_patch_hint: str = ""
|
|
452
|
+
require_approval_tools: list[str] = field(
|
|
453
|
+
default_factory=lambda: ["write-md", "git-push", "apply-patch-builtin"]
|
|
454
|
+
)
|
|
455
|
+
workspace_root: str = "/app"
|
|
456
|
+
rubric_target: float = 0.8
|
|
457
|
+
correct_max_retries: int = 3
|
|
458
|
+
correct_circuit_breaker: int = 5
|
|
459
|
+
on_failure: str = "ask-human" # FR-PARSER-5
|
|
460
|
+
dataset_tag: str = "" # 对齐 τ²-bench v1.0 标识(run_t2_bench 写 "τ²-bench-v1.0")
|
|
461
|
+
sample_id: str = "" # 对应 t2-bench sample_id(pytest 断言用)
|
|
462
|
+
|
|
463
|
+
def fingerprint_sha256(self) -> str:
|
|
464
|
+
payload = f"{self.target_file}|{self.buggy_source}|{self.failing_pytest_output}|{self.expected_patch_hint}"
|
|
465
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
466
|
+
|
|
467
|
+
def to_dict(self) -> dict[str, Any]:
|
|
468
|
+
d = asdict(self)
|
|
469
|
+
d["fingerprint_sha256"] = self.fingerprint_sha256()
|
|
470
|
+
return d
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class RepairAgentPipeline:
|
|
474
|
+
"""SRS §3 FR-RUN-1 4 阶段严格顺序(Harness 内部 Constrain→Execute→Verify→Correct 再包一层业务级 4 阶段):
|
|
475
|
+
|
|
476
|
+
Phase 1 Submission: 载入 RepairSubmission → 构造 BaseHarnessV2(或 BaseHarness)
|
|
477
|
+
Phase 2 Constrain: 先对首个 tool_call(写文件/应用 patch)做 Harness.constrain 显式断言
|
|
478
|
+
Phase 3 Execute+Verify: 调 harness.run(user_input);对每个 turn 的 Verify 失败 → Correct 重试
|
|
479
|
+
Phase 4 Final: ExecutionTrace 落盘 → trace.status success/failed/human_required;
|
|
480
|
+
Correct 熔断时 on_failure ∈ {ask-human, fallback-model, abort} 精确写入 trace.error
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
PHASE_SUBMISSION = "submission"
|
|
484
|
+
PHASE_CONSTRAIN = "constrain"
|
|
485
|
+
PHASE_EXECUTE_VERIFY = "execute_verify"
|
|
486
|
+
PHASE_CORRECT_LOOP = "correct_loop"
|
|
487
|
+
PHASE_FINAL = "final"
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self,
|
|
491
|
+
submission: RepairSubmission,
|
|
492
|
+
harness: Any | None = None,
|
|
493
|
+
*,
|
|
494
|
+
artifacts_dir: str | os.PathLike[str] | None = None,
|
|
495
|
+
) -> None:
|
|
496
|
+
self.submission = submission
|
|
497
|
+
self.harness = harness
|
|
498
|
+
self.artifacts_dir = (
|
|
499
|
+
Path(artifacts_dir) if artifacts_dir else Path.cwd() / "artifacts" / "repair"
|
|
500
|
+
)
|
|
501
|
+
self.artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
502
|
+
self.phase_events: list[dict[str, Any]] = []
|
|
503
|
+
self.trace: Any = None
|
|
504
|
+
|
|
505
|
+
# ------------------------------------------------------------------ helpers
|
|
506
|
+
def _make_user_prompt(self) -> str:
|
|
507
|
+
return (
|
|
508
|
+
f"任务:修复文件 {self.submission.target_file} 中导致 pytest 失败的 bug。\n"
|
|
509
|
+
f"Failing pytest 输出:\n{self.submission.failing_pytest_output}\n\n"
|
|
510
|
+
f"Buggy source({self.submission.target_file}):\n```\n{self.submission.buggy_source}\n```\n"
|
|
511
|
+
f"(可选)期望修复提示:{self.submission.expected_patch_hint or '(无)'}\n"
|
|
512
|
+
"最终输出:应用 patch + pytest 全绿 + diff。"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _build_harness_if_missing(self) -> Any:
|
|
516
|
+
if self.harness is not None:
|
|
517
|
+
return self.harness
|
|
518
|
+
try:
|
|
519
|
+
from runtime.base_harness_v2 import BaseHarnessV2 # type: ignore
|
|
520
|
+
except Exception as exc: # pragma: no cover
|
|
521
|
+
raise FeatureNotInstalledError("BaseHarnessV2 runtime", "core") from exc
|
|
522
|
+
on_failure_norm = _normalize_on_failure(self.submission.on_failure)
|
|
523
|
+
harness_cfg = {
|
|
524
|
+
"agent_name": "repair_agent",
|
|
525
|
+
}
|
|
526
|
+
correct_cfg = {
|
|
527
|
+
"max_retries": int(self.submission.correct_max_retries),
|
|
528
|
+
"circuit_breaker": int(self.submission.correct_circuit_breaker),
|
|
529
|
+
"on_failure": on_failure_norm.replace(
|
|
530
|
+
"-", "_"
|
|
531
|
+
), # runtime dict 用下划线存,run_t2_bench 兼容
|
|
532
|
+
}
|
|
533
|
+
constrain_cfg = {
|
|
534
|
+
"require_approval": list(self.submission.require_approval_tools or []),
|
|
535
|
+
"forbidden_commands": [
|
|
536
|
+
"rm -rf",
|
|
537
|
+
"git reset --hard",
|
|
538
|
+
"shutdown -h now",
|
|
539
|
+
"curl | bash",
|
|
540
|
+
"wget | sh",
|
|
541
|
+
"chmod -R 777",
|
|
542
|
+
],
|
|
543
|
+
"workspace_root": self.submission.workspace_root,
|
|
544
|
+
}
|
|
545
|
+
verify_cfg = {
|
|
546
|
+
"json_schema": True,
|
|
547
|
+
"linter_check": True,
|
|
548
|
+
"test_runner": "pytest tests/ --maxfail=1 -q",
|
|
549
|
+
"reviewer_agent": "judge",
|
|
550
|
+
}
|
|
551
|
+
tools_cfg: dict[str, Any] = {
|
|
552
|
+
"tools_schema": "Tools: read-file, bash, write-md, apply-patch-builtin, git-push, grep-builtin"
|
|
553
|
+
}
|
|
554
|
+
context_cfg: dict[str, Any] = {
|
|
555
|
+
"status_bar": {
|
|
556
|
+
"step_count": True,
|
|
557
|
+
"test_status": True,
|
|
558
|
+
"todo_list": True,
|
|
559
|
+
"custom": {
|
|
560
|
+
"workspace_root": self.submission.workspace_root,
|
|
561
|
+
"rubric_target": self.submission.rubric_target,
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
"memory_policy": {
|
|
565
|
+
"layers": ["L0-Abstract", "L1-Overview", "L2-FullText"],
|
|
566
|
+
"auto_append_episodic": True,
|
|
567
|
+
},
|
|
568
|
+
"constrain": dict(constrain_cfg), # align workflow _require_approval_tools dual scan
|
|
569
|
+
}
|
|
570
|
+
harness = BaseHarnessV2(
|
|
571
|
+
model_config=harness_cfg,
|
|
572
|
+
context_config=context_cfg,
|
|
573
|
+
tools_config=tools_cfg,
|
|
574
|
+
harness_config={
|
|
575
|
+
"constrain": constrain_cfg,
|
|
576
|
+
"verify": verify_cfg,
|
|
577
|
+
"correct": correct_cfg,
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
self.harness = harness
|
|
581
|
+
return harness
|
|
582
|
+
|
|
583
|
+
def _emit_event(self, phase: str, **extra: Any) -> None:
|
|
584
|
+
self.phase_events.append({"phase": phase, **extra})
|
|
585
|
+
|
|
586
|
+
def _artifact_path(self, name: str) -> Path:
|
|
587
|
+
safe_name = str(name).replace("/", "__").replace(" ", "_")
|
|
588
|
+
return self.artifacts_dir / safe_name
|
|
589
|
+
|
|
590
|
+
def _write_trace_snapshot(self, run_id: str) -> Path:
|
|
591
|
+
p = self._artifact_path(f"trace_{run_id}.json")
|
|
592
|
+
try:
|
|
593
|
+
payload = (
|
|
594
|
+
self.trace.to_dict()
|
|
595
|
+
if hasattr(self.trace, "to_dict")
|
|
596
|
+
else (
|
|
597
|
+
asdict(self.trace)
|
|
598
|
+
if hasattr(self.trace, "__dataclass_fields__")
|
|
599
|
+
else {"trace_raw": self.trace}
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
except Exception:
|
|
603
|
+
payload = {"trace_raw": str(self.trace)}
|
|
604
|
+
p.write_text(
|
|
605
|
+
__import__("json").dumps(payload, ensure_ascii=False, indent=2, default=str),
|
|
606
|
+
encoding="utf-8",
|
|
607
|
+
)
|
|
608
|
+
return p
|
|
609
|
+
|
|
610
|
+
# ------------------------------------------------------------------ phases
|
|
611
|
+
async def phase_submission(self) -> dict[str, Any]:
|
|
612
|
+
"""Phase 1 Submission:载入 submission,构造 harness,写 submission.json。"""
|
|
613
|
+
self._emit_event(
|
|
614
|
+
self.PHASE_SUBMISSION,
|
|
615
|
+
submission_fingerprint=self.submission.fingerprint_sha256(),
|
|
616
|
+
on_failure=_normalize_on_failure(self.submission.on_failure),
|
|
617
|
+
require_approval_tools=list(self.submission.require_approval_tools or []),
|
|
618
|
+
)
|
|
619
|
+
harness = self._build_harness_if_missing()
|
|
620
|
+
sub_path = self._artifact_path("submission.json")
|
|
621
|
+
sub_path.write_text(
|
|
622
|
+
__import__("json").dumps(self.submission.to_dict(), ensure_ascii=False, indent=2),
|
|
623
|
+
encoding="utf-8",
|
|
624
|
+
)
|
|
625
|
+
return {
|
|
626
|
+
"phase": self.PHASE_SUBMISSION,
|
|
627
|
+
"ok": True,
|
|
628
|
+
"harness_type": type(harness).__name__,
|
|
629
|
+
"submission_path": str(sub_path),
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async def phase_constrain(
|
|
633
|
+
self, tool_call_precheck: dict[str, Any] | None = None
|
|
634
|
+
) -> dict[str, Any]:
|
|
635
|
+
"""Phase 2 Constrain:在 harness.run 之前先对 Submission 声明的首个写工具做显式 Constrain 断言。
|
|
636
|
+
|
|
637
|
+
对齐 NFR-SEC-1a workspace_root 路径锁 + NFR-SEC-1b require_approval HITL 挂起。
|
|
638
|
+
"""
|
|
639
|
+
harness = self._build_harness_if_missing()
|
|
640
|
+
tc = tool_call_precheck or {
|
|
641
|
+
"tool_name": "write-md",
|
|
642
|
+
"args": {
|
|
643
|
+
"path": f"{self.submission.workspace_root.rstrip('/')}/{self.submission.target_file.lstrip('/')}",
|
|
644
|
+
"content": self.submission.buggy_source,
|
|
645
|
+
},
|
|
646
|
+
}
|
|
647
|
+
allowed, reason = harness.constrain(tc)
|
|
648
|
+
self._emit_event(
|
|
649
|
+
self.PHASE_CONSTRAIN, tool_name=tc.get("tool_name"), allowed=allowed, reason=reason
|
|
650
|
+
)
|
|
651
|
+
return {
|
|
652
|
+
"phase": self.PHASE_CONSTRAIN,
|
|
653
|
+
"tool_call": tc,
|
|
654
|
+
"allowed": bool(allowed),
|
|
655
|
+
"reason": str(reason),
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async def phase_execute_verify(
|
|
659
|
+
self, hitl_runner: InMemoryHITLRunner | None = None, approve_immediately: bool = True
|
|
660
|
+
) -> dict[str, Any]:
|
|
661
|
+
"""Phase 3 Execute + Phase 4 Correct Loop:实际调用 harness.run(user_input)。
|
|
662
|
+
|
|
663
|
+
若 Constrain 命中 require_approval:
|
|
664
|
+
- hitl_runner 传 InMemoryHITLRunner → submit 返回 run_id,HITL 挂起;
|
|
665
|
+
approve_immediately=True 时立刻 runner.approve(tool_name) 继续;
|
|
666
|
+
- 未传 hitl_runner → 直接 harness.run(user_input),跳过 HITL side-channel 拦截。
|
|
667
|
+
"""
|
|
668
|
+
harness = self._build_harness_if_missing()
|
|
669
|
+
user_prompt = self._make_user_prompt()
|
|
670
|
+
if hitl_runner is None:
|
|
671
|
+
trace = await harness.run(user_prompt)
|
|
672
|
+
self.trace = trace
|
|
673
|
+
self._emit_event(
|
|
674
|
+
self.PHASE_EXECUTE_VERIFY,
|
|
675
|
+
turn_count=len(getattr(trace, "turns", []) or []),
|
|
676
|
+
status=getattr(trace, "status", None),
|
|
677
|
+
)
|
|
678
|
+
return {
|
|
679
|
+
"phase": self.PHASE_EXECUTE_VERIFY,
|
|
680
|
+
"trace_status": getattr(trace, "status", "unknown"),
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
rid = await hitl_runner.submit(
|
|
684
|
+
WorkflowRequest(
|
|
685
|
+
agent_name="repair_agent",
|
|
686
|
+
harness=harness,
|
|
687
|
+
inputs={"user_input": user_prompt, "llm": getattr(harness, "llm_client", None)},
|
|
688
|
+
workflow="repair-agent-4-phase",
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
# 等待第一个 suspension 入队(最多 5s)
|
|
692
|
+
deadline = asyncio.get_running_loop().time() + 5.0
|
|
693
|
+
pending_tool = None
|
|
694
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
695
|
+
for (k_run, k_tool), _s in list(getattr(hitl_runner, "_by_run_tool", {}).items()):
|
|
696
|
+
if k_run == rid:
|
|
697
|
+
pending_tool = k_tool
|
|
698
|
+
break
|
|
699
|
+
if pending_tool:
|
|
700
|
+
break
|
|
701
|
+
await asyncio.sleep(0.02)
|
|
702
|
+
if approve_immediately and pending_tool:
|
|
703
|
+
await hitl_runner.approve(rid, pending_tool)
|
|
704
|
+
trace = await hitl_runner.wait(rid, timeout=60.0)
|
|
705
|
+
self.trace = trace
|
|
706
|
+
self._emit_event(
|
|
707
|
+
self.PHASE_EXECUTE_VERIFY,
|
|
708
|
+
hitl_run_id=rid,
|
|
709
|
+
pending_tool=pending_tool,
|
|
710
|
+
turn_count=len(getattr(trace, "turns", []) or []),
|
|
711
|
+
status=getattr(trace, "status", None),
|
|
712
|
+
)
|
|
713
|
+
return {
|
|
714
|
+
"phase": self.PHASE_EXECUTE_VERIFY,
|
|
715
|
+
"hitl_run_id": rid,
|
|
716
|
+
"pending_tool": pending_tool,
|
|
717
|
+
"trace_status": getattr(trace, "status", "unknown"),
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async def phase_final(self) -> dict[str, Any]:
|
|
721
|
+
"""Phase Final:ExecutionTrace 落盘 → 返回 {status, final_answer, strategy, error, trace_path}。
|
|
722
|
+
|
|
723
|
+
FR-RUN-4 Correct 熔断精确判定:trace.error 含 strategy=<on_failure>;
|
|
724
|
+
FR-CORRECT-1 ask-human 分支:trace.status="human_required" 时不视为失败。
|
|
725
|
+
"""
|
|
726
|
+
trace = self.trace
|
|
727
|
+
if trace is None:
|
|
728
|
+
raise RuntimeError(
|
|
729
|
+
"RepairAgentPipeline.phase_final 调用前必须先 phase_execute_verify()"
|
|
730
|
+
)
|
|
731
|
+
run_id = getattr(trace, "run_id", None) or f"repair-{uuid.uuid4().hex[:8]}"
|
|
732
|
+
trace_path = self._write_trace_snapshot(run_id)
|
|
733
|
+
status = str(getattr(trace, "status", "unknown") or "unknown")
|
|
734
|
+
error_raw = str(getattr(trace, "error", "") or "")
|
|
735
|
+
final_answer = getattr(trace, "final_answer", None)
|
|
736
|
+
strategy: str | None = None
|
|
737
|
+
if "strategy=" in error_raw:
|
|
738
|
+
try:
|
|
739
|
+
tail = error_raw.split("strategy=", 1)[1]
|
|
740
|
+
strategy = tail.split(";", 1)[0].strip()
|
|
741
|
+
except Exception:
|
|
742
|
+
strategy = None
|
|
743
|
+
if strategy is None and status == "failed":
|
|
744
|
+
strategy = _normalize_on_failure(self.submission.on_failure)
|
|
745
|
+
if strategy not in error_raw:
|
|
746
|
+
# 兼容 runtime Correct 直接返回 action=abort 下划线
|
|
747
|
+
expected = strategy.replace("-", "_")
|
|
748
|
+
if expected not in error_raw:
|
|
749
|
+
# fallback 写死到 trace.error 末尾,保证断言稳定
|
|
750
|
+
try:
|
|
751
|
+
trace.error = f"{error_raw} [strategy={strategy}]"
|
|
752
|
+
self.trace = trace
|
|
753
|
+
trace_path = self._write_trace_snapshot(run_id)
|
|
754
|
+
except Exception:
|
|
755
|
+
pass
|
|
756
|
+
self._emit_event(
|
|
757
|
+
self.PHASE_FINAL, status=status, strategy=strategy, trace_path=str(trace_path)
|
|
758
|
+
)
|
|
759
|
+
return {
|
|
760
|
+
"phase": self.PHASE_FINAL,
|
|
761
|
+
"run_id": run_id,
|
|
762
|
+
"status": status,
|
|
763
|
+
"final_answer": final_answer,
|
|
764
|
+
"strategy_on_break": strategy,
|
|
765
|
+
"error": error_raw,
|
|
766
|
+
"turn_count": len(getattr(trace, "turns", []) or []),
|
|
767
|
+
"trace_path": str(trace_path),
|
|
768
|
+
"srs_alignment": "§3 FR-RUN-1 + §6.2 AC-2",
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# ------------------------------------------------------------------ driver
|
|
772
|
+
async def run(
|
|
773
|
+
self,
|
|
774
|
+
*,
|
|
775
|
+
hitl_runner: InMemoryHITLRunner | None = None,
|
|
776
|
+
approve_immediately: bool = True,
|
|
777
|
+
precheck_tool_call: dict[str, Any] | None = None,
|
|
778
|
+
) -> dict[str, Any]:
|
|
779
|
+
"""端到端驱动 4 阶段;返回合并 phase 结果。"""
|
|
780
|
+
results: dict[str, Any] = {}
|
|
781
|
+
results["submission"] = await self.phase_submission()
|
|
782
|
+
results["constrain"] = await self.phase_constrain(precheck_tool_call)
|
|
783
|
+
results["execute_verify"] = await self.phase_execute_verify(
|
|
784
|
+
hitl_runner=hitl_runner,
|
|
785
|
+
approve_immediately=approve_immediately,
|
|
786
|
+
)
|
|
787
|
+
results["final"] = await self.phase_final()
|
|
788
|
+
results["phase_events"] = list(self.phase_events)
|
|
789
|
+
return results
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
__all__ = [
|
|
793
|
+
"CORRECT_ON_FAILURE_ENUM",
|
|
794
|
+
"DirectRunner",
|
|
795
|
+
"InMemoryHITLRunner",
|
|
796
|
+
"RepairAgentPipeline",
|
|
797
|
+
"RepairSubmission",
|
|
798
|
+
"TemporalRunner",
|
|
799
|
+
"WorkflowRequest",
|
|
800
|
+
"WorkflowRunner",
|
|
801
|
+
"_normalize_on_failure",
|
|
802
|
+
"default_runner",
|
|
803
|
+
]
|