cortex-loop 0.1.0a1__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.
- cortex/__init__.py +7 -0
- cortex/adapters.py +339 -0
- cortex/blocklist.py +51 -0
- cortex/challenges.py +210 -0
- cortex/cli.py +7 -0
- cortex/core.py +601 -0
- cortex/core_helpers.py +190 -0
- cortex/data/identity_preamble.md +5 -0
- cortex/data/layer1_part_a.md +65 -0
- cortex/data/layer1_part_b.md +17 -0
- cortex/executive.py +295 -0
- cortex/foundation.py +185 -0
- cortex/genome.py +348 -0
- cortex/graveyard.py +226 -0
- cortex/hooks/__init__.py +27 -0
- cortex/hooks/_shared.py +167 -0
- cortex/hooks/post_tool_use.py +13 -0
- cortex/hooks/pre_tool_use.py +13 -0
- cortex/hooks/session_start.py +13 -0
- cortex/hooks/stop.py +13 -0
- cortex/invariants.py +258 -0
- cortex/packs.py +118 -0
- cortex/repomap.py +6 -0
- cortex/requirements.py +497 -0
- cortex/retry.py +312 -0
- cortex/stop_contract.py +217 -0
- cortex/stop_payload.py +122 -0
- cortex/stop_policy.py +100 -0
- cortex/stop_runtime.py +400 -0
- cortex/stop_signals.py +75 -0
- cortex/store.py +793 -0
- cortex/templates/__init__.py +10 -0
- cortex/utils.py +58 -0
- cortex_loop-0.1.0a1.dist-info/METADATA +121 -0
- cortex_loop-0.1.0a1.dist-info/RECORD +52 -0
- cortex_loop-0.1.0a1.dist-info/WHEEL +5 -0
- cortex_loop-0.1.0a1.dist-info/entry_points.txt +3 -0
- cortex_loop-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- cortex_loop-0.1.0a1.dist-info/top_level.txt +3 -0
- cortex_ops_cli/__init__.py +3 -0
- cortex_ops_cli/_adapter_validation.py +119 -0
- cortex_ops_cli/_check_report.py +454 -0
- cortex_ops_cli/_check_report_output.py +270 -0
- cortex_ops_cli/_openai_bridge_probe.py +241 -0
- cortex_ops_cli/_openai_bridge_protocol.py +469 -0
- cortex_ops_cli/_runtime_profile_templates.py +341 -0
- cortex_ops_cli/_runtime_profiles.py +445 -0
- cortex_ops_cli/gemini_hooks.py +301 -0
- cortex_ops_cli/main.py +911 -0
- cortex_ops_cli/openai_app_server_bridge.py +375 -0
- cortex_repomap/__init__.py +1 -0
- cortex_repomap/engine.py +1201 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import select
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
COMMAND_APPROVAL_METHOD = "item/commandExecution/requestApproval"
|
|
11
|
+
FILE_CHANGE_APPROVAL_METHOD = "item/fileChange/requestApproval"
|
|
12
|
+
NOTIFICATION_ITEM_COMPLETED = "item/completed"
|
|
13
|
+
NOTIFICATION_TURN_COMPLETED = "turn/completed"
|
|
14
|
+
NOTIFICATION_TASK_COMPLETE = "codex/event/task_complete"
|
|
15
|
+
NOTIFICATION_THREAD_STATUS_CHANGED = "thread/status/changed"
|
|
16
|
+
DEFAULT_APPROVAL_POLICY_CANDIDATES = ("untrusted", "unlessTrusted", "on-request")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BridgeError(RuntimeError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AppServerClient:
|
|
24
|
+
def __init__(self, *, codex_bin: str, cwd: Path, timeout_seconds: float) -> None:
|
|
25
|
+
cmd = [codex_bin, "app-server", "--listen", "stdio://"]
|
|
26
|
+
self.proc = subprocess.Popen(
|
|
27
|
+
cmd,
|
|
28
|
+
cwd=str(cwd),
|
|
29
|
+
stdin=subprocess.PIPE,
|
|
30
|
+
stdout=subprocess.PIPE,
|
|
31
|
+
stderr=subprocess.PIPE,
|
|
32
|
+
text=True,
|
|
33
|
+
bufsize=1,
|
|
34
|
+
)
|
|
35
|
+
self.timeout_seconds = timeout_seconds
|
|
36
|
+
self._next_request_id = 1
|
|
37
|
+
self._pending_messages: list[dict[str, Any]] = []
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
if self.proc.poll() is not None:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
self.proc.terminate()
|
|
44
|
+
self.proc.wait(timeout=1.0)
|
|
45
|
+
except Exception:
|
|
46
|
+
self.proc.kill()
|
|
47
|
+
self.proc.wait(timeout=1.0)
|
|
48
|
+
|
|
49
|
+
def _write(self, payload: dict[str, Any]) -> None:
|
|
50
|
+
if self.proc.stdin is None:
|
|
51
|
+
raise BridgeError("codex app-server stdin unavailable")
|
|
52
|
+
self.proc.stdin.write(json.dumps(payload) + "\n")
|
|
53
|
+
self.proc.stdin.flush()
|
|
54
|
+
|
|
55
|
+
def _read_line(self, timeout_seconds: float) -> str:
|
|
56
|
+
if self.proc.stdout is None:
|
|
57
|
+
raise BridgeError("codex app-server stdout unavailable")
|
|
58
|
+
fd = self.proc.stdout.fileno()
|
|
59
|
+
ready, _, _ = select.select([fd], [], [], timeout_seconds)
|
|
60
|
+
if not ready:
|
|
61
|
+
raise BridgeError("Timed out waiting for codex app-server response")
|
|
62
|
+
line = self.proc.stdout.readline()
|
|
63
|
+
if not line:
|
|
64
|
+
stderr = ""
|
|
65
|
+
if self.proc.stderr is not None:
|
|
66
|
+
try:
|
|
67
|
+
stderr = self.proc.stderr.read().strip()
|
|
68
|
+
except Exception:
|
|
69
|
+
stderr = ""
|
|
70
|
+
raise BridgeError(f"codex app-server closed stdout. stderr={stderr[:4000]}")
|
|
71
|
+
return line
|
|
72
|
+
|
|
73
|
+
def _read_message(self, timeout_seconds: float, *, allow_pending: bool = True) -> dict[str, Any]:
|
|
74
|
+
if allow_pending and self._pending_messages:
|
|
75
|
+
return self._pending_messages.pop(0)
|
|
76
|
+
line = self._read_line(timeout_seconds)
|
|
77
|
+
try:
|
|
78
|
+
decoded = json.loads(line)
|
|
79
|
+
except json.JSONDecodeError as exc:
|
|
80
|
+
raise BridgeError(f"Invalid JSON-RPC payload from app-server: {exc}") from exc
|
|
81
|
+
if not isinstance(decoded, dict):
|
|
82
|
+
raise BridgeError("Invalid JSON-RPC payload from app-server: expected object")
|
|
83
|
+
return decoded
|
|
84
|
+
|
|
85
|
+
def _pop_pending_response(self, request_id: int) -> dict[str, Any] | None:
|
|
86
|
+
for idx, msg in enumerate(self._pending_messages):
|
|
87
|
+
if msg.get("id") == request_id and ("result" in msg or "error" in msg):
|
|
88
|
+
return self._pending_messages.pop(idx)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def request(
|
|
92
|
+
self,
|
|
93
|
+
method: str,
|
|
94
|
+
params: dict[str, Any] | None = None,
|
|
95
|
+
*,
|
|
96
|
+
timeout_seconds: float | None = None,
|
|
97
|
+
) -> Any:
|
|
98
|
+
request_id = self._next_request_id
|
|
99
|
+
self._next_request_id += 1
|
|
100
|
+
self._write({"id": request_id, "method": method, "params": params or {}})
|
|
101
|
+
deadline = time.time() + (self.timeout_seconds if timeout_seconds is None else timeout_seconds)
|
|
102
|
+
while True:
|
|
103
|
+
pending_response = self._pop_pending_response(request_id)
|
|
104
|
+
if pending_response is not None:
|
|
105
|
+
if "error" in pending_response:
|
|
106
|
+
raise BridgeError(f"JSON-RPC {method} failed: {pending_response['error']}")
|
|
107
|
+
return pending_response.get("result")
|
|
108
|
+
|
|
109
|
+
remaining = deadline - time.time()
|
|
110
|
+
if remaining <= 0:
|
|
111
|
+
raise BridgeError(f"Timed out waiting for JSON-RPC response to {method}")
|
|
112
|
+
|
|
113
|
+
msg = self._read_message(max(0.01, remaining), allow_pending=False)
|
|
114
|
+
if msg.get("id") == request_id and ("result" in msg or "error" in msg):
|
|
115
|
+
if "error" in msg:
|
|
116
|
+
raise BridgeError(f"JSON-RPC {method} failed: {msg['error']}")
|
|
117
|
+
return msg.get("result")
|
|
118
|
+
self._pending_messages.append(msg)
|
|
119
|
+
|
|
120
|
+
def send_notification(self, method: str, params: dict[str, Any] | None = None) -> None:
|
|
121
|
+
self._write({"method": method, "params": params or {}})
|
|
122
|
+
|
|
123
|
+
def send_server_request_result(self, request_id: Any, result: dict[str, Any]) -> None:
|
|
124
|
+
self._write({"id": request_id, "result": result})
|
|
125
|
+
|
|
126
|
+
def next_message(self, *, timeout_seconds: float | None = None) -> dict[str, Any]:
|
|
127
|
+
return self._read_message(self.timeout_seconds if timeout_seconds is None else timeout_seconds)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class BridgeRunResult(dict):
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def classify_command_surface(
|
|
135
|
+
*,
|
|
136
|
+
command_items: list[dict[str, Any]],
|
|
137
|
+
approval_requests: list[dict[str, Any]],
|
|
138
|
+
) -> dict[str, Any]:
|
|
139
|
+
approved_item_ids = {
|
|
140
|
+
str(item.get("item_id") or "").strip()
|
|
141
|
+
for item in approval_requests
|
|
142
|
+
if isinstance(item, dict) and str(item.get("method")) == COMMAND_APPROVAL_METHOD
|
|
143
|
+
}
|
|
144
|
+
approved_item_ids.discard("")
|
|
145
|
+
command_item_ids = {
|
|
146
|
+
str(item.get("item_id") or "").strip()
|
|
147
|
+
for item in command_items
|
|
148
|
+
if isinstance(item, dict)
|
|
149
|
+
}
|
|
150
|
+
command_item_ids.discard("")
|
|
151
|
+
|
|
152
|
+
with_approval = sorted(command_item_ids & approved_item_ids)
|
|
153
|
+
without_approval = sorted(command_item_ids - approved_item_ids)
|
|
154
|
+
declined_item_ids = {
|
|
155
|
+
str(item.get("item_id") or "").strip()
|
|
156
|
+
for item in approval_requests
|
|
157
|
+
if isinstance(item, dict)
|
|
158
|
+
and str(item.get("method")) == COMMAND_APPROVAL_METHOD
|
|
159
|
+
and str(item.get("decision")) == "decline"
|
|
160
|
+
}
|
|
161
|
+
declined_item_ids.discard("")
|
|
162
|
+
|
|
163
|
+
nonblocking_declines: list[str] = []
|
|
164
|
+
for item in command_items:
|
|
165
|
+
if not isinstance(item, dict):
|
|
166
|
+
continue
|
|
167
|
+
item_id = str(item.get("item_id") or "").strip()
|
|
168
|
+
if item_id not in declined_item_ids:
|
|
169
|
+
continue
|
|
170
|
+
status = str(item.get("status") or "").strip().lower()
|
|
171
|
+
has_exec_payload = item.get("exit_code") is not None or bool(
|
|
172
|
+
str(item.get("aggregated_output") or "").strip()
|
|
173
|
+
)
|
|
174
|
+
if status != "declined" or has_exec_payload:
|
|
175
|
+
nonblocking_declines.append(item_id or "<missing_item_id>")
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"command_item_ids": sorted(command_item_ids),
|
|
179
|
+
"approved_item_ids": sorted(approved_item_ids),
|
|
180
|
+
"command_items_with_approval": with_approval,
|
|
181
|
+
"command_items_without_approval": without_approval,
|
|
182
|
+
"declined_item_ids": sorted(declined_item_ids),
|
|
183
|
+
"nonblocking_declines": sorted(set(nonblocking_declines)),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def initialize_app_server(*, client: AppServerClient) -> None:
|
|
188
|
+
init_result = client.request(
|
|
189
|
+
"initialize",
|
|
190
|
+
{
|
|
191
|
+
"clientInfo": {
|
|
192
|
+
"name": "cortex-openai-bridge",
|
|
193
|
+
"title": "Cortex OpenAI Bridge",
|
|
194
|
+
"version": "1.0.0",
|
|
195
|
+
},
|
|
196
|
+
"capabilities": {
|
|
197
|
+
"experimentalApi": False,
|
|
198
|
+
"optOutNotificationMethods": [],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
if not isinstance(init_result, dict):
|
|
203
|
+
raise BridgeError("initialize did not return a JSON object")
|
|
204
|
+
client.send_notification("initialized", {})
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def is_policy_compat_error(exc: BridgeError) -> bool:
|
|
208
|
+
message = str(exc).lower()
|
|
209
|
+
return (
|
|
210
|
+
"approvalpolicy" in message
|
|
211
|
+
or "askforapproval" in message
|
|
212
|
+
or "unknown variant" in message
|
|
213
|
+
or "invalid type" in message
|
|
214
|
+
or "invalid value" in message
|
|
215
|
+
or "unlesstrusted" in message
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def start_thread_with_policy_fallback(
|
|
220
|
+
*,
|
|
221
|
+
client: AppServerClient,
|
|
222
|
+
cwd: Path,
|
|
223
|
+
model: str | None,
|
|
224
|
+
approval_policy_candidates: tuple[str, ...] = DEFAULT_APPROVAL_POLICY_CANDIDATES,
|
|
225
|
+
) -> tuple[dict[str, Any], str]:
|
|
226
|
+
if not approval_policy_candidates:
|
|
227
|
+
raise BridgeError("No approval policy candidates configured.")
|
|
228
|
+
|
|
229
|
+
last_error: BridgeError | None = None
|
|
230
|
+
for idx, policy in enumerate(approval_policy_candidates):
|
|
231
|
+
params: dict[str, Any] = {
|
|
232
|
+
"cwd": str(cwd),
|
|
233
|
+
"approvalPolicy": policy,
|
|
234
|
+
"sandbox": "workspace-write",
|
|
235
|
+
}
|
|
236
|
+
if model:
|
|
237
|
+
params["model"] = model
|
|
238
|
+
try:
|
|
239
|
+
result = client.request("thread/start", params)
|
|
240
|
+
if not isinstance(result, dict):
|
|
241
|
+
raise BridgeError("thread/start did not return a JSON object")
|
|
242
|
+
return result, policy
|
|
243
|
+
except BridgeError as exc:
|
|
244
|
+
last_error = exc
|
|
245
|
+
if idx < len(approval_policy_candidates) - 1 and is_policy_compat_error(exc):
|
|
246
|
+
continue
|
|
247
|
+
raise
|
|
248
|
+
|
|
249
|
+
if last_error is not None:
|
|
250
|
+
raise last_error
|
|
251
|
+
raise BridgeError("thread/start failed: no approval policy candidate succeeded")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _bridge_turn_result(
|
|
255
|
+
*,
|
|
256
|
+
thread_id: str,
|
|
257
|
+
turn_id: str,
|
|
258
|
+
approval_policy_used: str,
|
|
259
|
+
approval_policy_candidates: tuple[str, ...],
|
|
260
|
+
final_text: str,
|
|
261
|
+
approval_requests: list[dict[str, Any]],
|
|
262
|
+
command_completion_items: list[dict[str, Any]],
|
|
263
|
+
coverage_gaps: list[str],
|
|
264
|
+
duplicate_turn_completed_count: int,
|
|
265
|
+
elapsed_seconds: float,
|
|
266
|
+
) -> BridgeRunResult:
|
|
267
|
+
return BridgeRunResult(
|
|
268
|
+
{
|
|
269
|
+
"ok": True,
|
|
270
|
+
"thread_id": thread_id,
|
|
271
|
+
"turn_id": turn_id,
|
|
272
|
+
"approval_policy_used": approval_policy_used,
|
|
273
|
+
"approval_policy_candidates": list(approval_policy_candidates),
|
|
274
|
+
"text": final_text,
|
|
275
|
+
"response_present": bool(final_text.strip()),
|
|
276
|
+
"approval_requests": approval_requests,
|
|
277
|
+
"command_completion_items": command_completion_items,
|
|
278
|
+
"command_surface": classify_command_surface(
|
|
279
|
+
command_items=command_completion_items,
|
|
280
|
+
approval_requests=approval_requests,
|
|
281
|
+
),
|
|
282
|
+
"coverage_gaps": sorted(set(coverage_gaps)),
|
|
283
|
+
"duplicate_turn_completed_count": duplicate_turn_completed_count,
|
|
284
|
+
"elapsed_seconds": elapsed_seconds,
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def execute_turn(
|
|
290
|
+
*,
|
|
291
|
+
codex_bin: str,
|
|
292
|
+
cwd: Path,
|
|
293
|
+
prompt: str,
|
|
294
|
+
model: str | None,
|
|
295
|
+
timeout_seconds: float,
|
|
296
|
+
approval_policy_candidates: tuple[str, ...] = DEFAULT_APPROVAL_POLICY_CANDIDATES,
|
|
297
|
+
approval_handler: Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]],
|
|
298
|
+
client_cls: type[AppServerClient] = AppServerClient,
|
|
299
|
+
) -> BridgeRunResult:
|
|
300
|
+
client = client_cls(codex_bin=codex_bin, cwd=cwd, timeout_seconds=timeout_seconds)
|
|
301
|
+
start_time = time.time()
|
|
302
|
+
try:
|
|
303
|
+
initialize_app_server(client=client)
|
|
304
|
+
thread_result, approval_policy_used = start_thread_with_policy_fallback(
|
|
305
|
+
client=client,
|
|
306
|
+
cwd=cwd,
|
|
307
|
+
model=model,
|
|
308
|
+
approval_policy_candidates=approval_policy_candidates,
|
|
309
|
+
)
|
|
310
|
+
thread_id = str(((thread_result or {}).get("thread") or {}).get("id") or "").strip()
|
|
311
|
+
if not thread_id:
|
|
312
|
+
raise BridgeError("thread/start did not return thread.id")
|
|
313
|
+
|
|
314
|
+
turn_result = client.request(
|
|
315
|
+
"turn/start",
|
|
316
|
+
{
|
|
317
|
+
"threadId": thread_id,
|
|
318
|
+
"input": [{"type": "text", "text": prompt, "textElements": []}],
|
|
319
|
+
"cwd": str(cwd),
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
turn_id = str(((turn_result or {}).get("turn") or {}).get("id") or "").strip()
|
|
323
|
+
if not turn_id:
|
|
324
|
+
raise BridgeError("turn/start did not return turn.id")
|
|
325
|
+
|
|
326
|
+
final_agent_messages: dict[str, str] = {}
|
|
327
|
+
completed_turns: set[str] = set()
|
|
328
|
+
duplicate_turn_completed_count = 0
|
|
329
|
+
command_completion_items: list[dict[str, Any]] = []
|
|
330
|
+
approval_requests: list[dict[str, Any]] = []
|
|
331
|
+
coverage_gaps: list[str] = []
|
|
332
|
+
task_complete_turns: set[str] = set()
|
|
333
|
+
task_complete_last_messages: dict[str, str] = {}
|
|
334
|
+
|
|
335
|
+
while True:
|
|
336
|
+
msg = client.next_message()
|
|
337
|
+
method = str(msg.get("method") or "")
|
|
338
|
+
|
|
339
|
+
if method and "id" in msg and "result" not in msg and "error" not in msg:
|
|
340
|
+
request_id = msg.get("id")
|
|
341
|
+
params = msg.get("params") if isinstance(msg.get("params"), dict) else {}
|
|
342
|
+
if method in {COMMAND_APPROVAL_METHOD, FILE_CHANGE_APPROVAL_METHOD}:
|
|
343
|
+
decision, metadata = approval_handler(method, params)
|
|
344
|
+
if decision not in {"accept", "decline", "cancel"}:
|
|
345
|
+
decision = "decline"
|
|
346
|
+
approval_requests.append(
|
|
347
|
+
{
|
|
348
|
+
"method": method,
|
|
349
|
+
"item_id": str(params.get("itemId") or ""),
|
|
350
|
+
"command": str(params.get("command") or ""),
|
|
351
|
+
"cwd": str(params.get("cwd") or ""),
|
|
352
|
+
"decision": decision,
|
|
353
|
+
"metadata": metadata,
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
client.send_server_request_result(request_id, {"decision": decision})
|
|
357
|
+
continue
|
|
358
|
+
coverage_gaps.append(f"unhandled_server_request:{method}")
|
|
359
|
+
client.send_server_request_result(request_id, {"decision": "decline"})
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
if not method:
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
params = msg.get("params") if isinstance(msg.get("params"), dict) else {}
|
|
366
|
+
if method == NOTIFICATION_ITEM_COMPLETED:
|
|
367
|
+
item = params.get("item") if isinstance(params.get("item"), dict) else {}
|
|
368
|
+
item_type = str(item.get("type") or "")
|
|
369
|
+
item_turn_id = str(params.get("turnId") or "")
|
|
370
|
+
if item_type == "agentMessage" and item_turn_id:
|
|
371
|
+
final_agent_messages[item_turn_id] = str(item.get("text") or "")
|
|
372
|
+
if item_type == "commandExecution":
|
|
373
|
+
command_completion_items.append(
|
|
374
|
+
{
|
|
375
|
+
"item_id": str(item.get("id") or ""),
|
|
376
|
+
"turn_id": item_turn_id,
|
|
377
|
+
"status": str(item.get("status") or ""),
|
|
378
|
+
"exit_code": item.get("exitCode"),
|
|
379
|
+
"aggregated_output": item.get("aggregatedOutput"),
|
|
380
|
+
"command": str(item.get("command") or ""),
|
|
381
|
+
"cwd": str(item.get("cwd") or ""),
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
if method == NOTIFICATION_TURN_COMPLETED:
|
|
387
|
+
notification_turn = params.get("turn") if isinstance(params.get("turn"), dict) else {}
|
|
388
|
+
completed_turn_id = str(notification_turn.get("id") or "")
|
|
389
|
+
if not completed_turn_id:
|
|
390
|
+
coverage_gaps.append("turn_completed_missing_turn_id")
|
|
391
|
+
continue
|
|
392
|
+
if completed_turn_id in completed_turns:
|
|
393
|
+
duplicate_turn_completed_count += 1
|
|
394
|
+
continue
|
|
395
|
+
completed_turns.add(completed_turn_id)
|
|
396
|
+
if completed_turn_id != turn_id:
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
final_text = str(final_agent_messages.get(turn_id, "") or "")
|
|
400
|
+
elapsed_seconds = round(time.time() - start_time, 6)
|
|
401
|
+
return _bridge_turn_result(
|
|
402
|
+
thread_id=thread_id,
|
|
403
|
+
turn_id=turn_id,
|
|
404
|
+
approval_policy_used=approval_policy_used,
|
|
405
|
+
approval_policy_candidates=approval_policy_candidates,
|
|
406
|
+
final_text=final_text,
|
|
407
|
+
approval_requests=approval_requests,
|
|
408
|
+
command_completion_items=command_completion_items,
|
|
409
|
+
coverage_gaps=coverage_gaps,
|
|
410
|
+
duplicate_turn_completed_count=duplicate_turn_completed_count,
|
|
411
|
+
elapsed_seconds=elapsed_seconds,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if method == NOTIFICATION_TASK_COMPLETE:
|
|
415
|
+
task_msg = params.get("msg") if isinstance(params.get("msg"), dict) else {}
|
|
416
|
+
completed_turn_id = str(task_msg.get("turn_id") or params.get("id") or "").strip()
|
|
417
|
+
if completed_turn_id:
|
|
418
|
+
task_complete_turns.add(completed_turn_id)
|
|
419
|
+
last_message = str(task_msg.get("last_agent_message") or "")
|
|
420
|
+
if last_message:
|
|
421
|
+
task_complete_last_messages[completed_turn_id] = last_message
|
|
422
|
+
if completed_turn_id == turn_id and completed_turn_id not in completed_turns:
|
|
423
|
+
coverage_gaps.append("turn_completed_missing_used_task_complete_fallback")
|
|
424
|
+
final_text = str(
|
|
425
|
+
final_agent_messages.get(turn_id, "")
|
|
426
|
+
or task_complete_last_messages.get(turn_id, "")
|
|
427
|
+
or ""
|
|
428
|
+
)
|
|
429
|
+
elapsed_seconds = round(time.time() - start_time, 6)
|
|
430
|
+
return _bridge_turn_result(
|
|
431
|
+
thread_id=thread_id,
|
|
432
|
+
turn_id=turn_id,
|
|
433
|
+
approval_policy_used=approval_policy_used,
|
|
434
|
+
approval_policy_candidates=approval_policy_candidates,
|
|
435
|
+
final_text=final_text,
|
|
436
|
+
approval_requests=approval_requests,
|
|
437
|
+
command_completion_items=command_completion_items,
|
|
438
|
+
coverage_gaps=coverage_gaps,
|
|
439
|
+
duplicate_turn_completed_count=duplicate_turn_completed_count,
|
|
440
|
+
elapsed_seconds=elapsed_seconds,
|
|
441
|
+
)
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
if method == NOTIFICATION_THREAD_STATUS_CHANGED:
|
|
445
|
+
event_thread_id = str(params.get("threadId") or "")
|
|
446
|
+
status = params.get("status") if isinstance(params.get("status"), dict) else {}
|
|
447
|
+
status_type = str(status.get("type") or "")
|
|
448
|
+
if event_thread_id == thread_id and status_type == "idle" and turn_id in task_complete_turns:
|
|
449
|
+
coverage_gaps.append("turn_completed_missing_used_task_complete_fallback")
|
|
450
|
+
final_text = str(
|
|
451
|
+
final_agent_messages.get(turn_id, "")
|
|
452
|
+
or task_complete_last_messages.get(turn_id, "")
|
|
453
|
+
or ""
|
|
454
|
+
)
|
|
455
|
+
elapsed_seconds = round(time.time() - start_time, 6)
|
|
456
|
+
return _bridge_turn_result(
|
|
457
|
+
thread_id=thread_id,
|
|
458
|
+
turn_id=turn_id,
|
|
459
|
+
approval_policy_used=approval_policy_used,
|
|
460
|
+
approval_policy_candidates=approval_policy_candidates,
|
|
461
|
+
final_text=final_text,
|
|
462
|
+
approval_requests=approval_requests,
|
|
463
|
+
command_completion_items=command_completion_items,
|
|
464
|
+
coverage_gaps=coverage_gaps,
|
|
465
|
+
duplicate_turn_completed_count=duplicate_turn_completed_count,
|
|
466
|
+
elapsed_seconds=elapsed_seconds,
|
|
467
|
+
)
|
|
468
|
+
finally:
|
|
469
|
+
client.close()
|