agentlink-cli 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/PKG-INFO +1 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/codex/adapter.py +33 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/codex/reducer.py +61 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/version.py +1 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/pyproject.toml +1 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_codex_adapter.py +165 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_version.py +1 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/uv.lock +1 -1
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/.gitignore +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/README.md +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/adapter.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/config_options.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/discovery.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/manifest.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/manifests/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/manifests/codebuddy.json +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/manifests/cursor.json +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/manifests/gemini.json +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/manifests/grok_build.json +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/reducer.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/acp/rpc.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/adapter.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/attachments.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/capabilities.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/history_adapter.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/normalized.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/normalizers.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/path_utils.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/preferences.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/sdk_adapter.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/timeline_identity.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/timeline_reducer.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/claude/trust.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/cli.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/codex/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/codex/collaboration.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/codex/history.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/codex/rpc.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/control.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/json_rpc.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/launch.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local/common.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local/file_ops.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local/ops.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local/shell.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local/terminal.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local_ops.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/local_runtime.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/logging.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/perf.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/protocol.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/registry.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/runtime.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/sync_state.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/connector/time.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/run.sh +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/acp/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/acp/fake_agent/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/acp/fake_agent/__main__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/acp/test_acp_adapter_fake.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/acp/test_acp_reducer_golden.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/acp/test_config_options.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/contract/__init__.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/contract/helpers.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_claude_history_adapter.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_claude_preferences.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_claude_sdk_adapter.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_claude_timeline_parity.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_claude_trust.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_connector_capabilities.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_connector_cli.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_connector_control.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_connector_json_rpc.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_connector_logging.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_connector_runtime.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_launch_command.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_local_runtime.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_perf.py +0 -0
- {agentlink_cli-0.1.1 → agentlink_cli-0.1.2}/tests/test_terminal_backend.py +0 -0
|
@@ -463,9 +463,19 @@ class CodexAdapter:
|
|
|
463
463
|
settings = collaboration["settings"]
|
|
464
464
|
turn_params["model"] = settings["model"]
|
|
465
465
|
turn_params["effort"] = settings["reasoning_effort"]
|
|
466
|
+
if requested_collaboration_mode is not None:
|
|
467
|
+
self.reducer.register_collaboration_mode(
|
|
468
|
+
session_id=session_id,
|
|
469
|
+
thread_id=thread_id,
|
|
470
|
+
mode=requested_collaboration_mode,
|
|
471
|
+
)
|
|
466
472
|
try:
|
|
467
473
|
result = await self.rpc.request("turn/start", turn_params)
|
|
468
474
|
except RuntimeError as exc:
|
|
475
|
+
self.reducer.cancel_pending_collaboration_mode(
|
|
476
|
+
session_id=session_id,
|
|
477
|
+
thread_id=thread_id,
|
|
478
|
+
)
|
|
469
479
|
recovered = await self._recover_missing_thread_for_turn_start(
|
|
470
480
|
params,
|
|
471
481
|
session_id=session_id,
|
|
@@ -483,6 +493,12 @@ class CodexAdapter:
|
|
|
483
493
|
"thread/settings/update",
|
|
484
494
|
{"threadId": thread_id, "collaborationMode": collaboration},
|
|
485
495
|
)
|
|
496
|
+
if requested_collaboration_mode is not None:
|
|
497
|
+
self.reducer.register_collaboration_mode(
|
|
498
|
+
session_id=session_id,
|
|
499
|
+
thread_id=thread_id,
|
|
500
|
+
mode=requested_collaboration_mode,
|
|
501
|
+
)
|
|
486
502
|
if client_message_id:
|
|
487
503
|
self.reducer.register_client_message(
|
|
488
504
|
session_id=session_id,
|
|
@@ -491,8 +507,24 @@ class CodexAdapter:
|
|
|
491
507
|
text=text_content,
|
|
492
508
|
attachments=timeline_attachments,
|
|
493
509
|
)
|
|
494
|
-
|
|
510
|
+
retry_succeeded = False
|
|
511
|
+
try:
|
|
512
|
+
result = await self.rpc.request("turn/start", turn_params)
|
|
513
|
+
retry_succeeded = True
|
|
514
|
+
finally:
|
|
515
|
+
if not retry_succeeded:
|
|
516
|
+
self.reducer.cancel_pending_collaboration_mode(
|
|
517
|
+
session_id=session_id,
|
|
518
|
+
thread_id=thread_id,
|
|
519
|
+
)
|
|
495
520
|
turn_id = _turn_id_from_result(result)
|
|
521
|
+
if requested_collaboration_mode is not None and turn_id:
|
|
522
|
+
self.reducer.register_collaboration_mode(
|
|
523
|
+
session_id=session_id,
|
|
524
|
+
thread_id=thread_id,
|
|
525
|
+
turn_id=turn_id,
|
|
526
|
+
mode=requested_collaboration_mode,
|
|
527
|
+
)
|
|
496
528
|
if client_message_id and turn_id:
|
|
497
529
|
self.reducer.register_client_message(
|
|
498
530
|
session_id=session_id,
|
|
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
from collections import OrderedDict
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from connector.time import utc_now
|
|
9
10
|
|
|
10
|
-
|
|
11
11
|
CODEX_APPROVAL_METHODS = {
|
|
12
12
|
"item/commandExecution/requestApproval",
|
|
13
13
|
"item/fileChange/requestApproval",
|
|
@@ -16,6 +16,7 @@ CODEX_APPROVAL_METHODS = {
|
|
|
16
16
|
CODEX_INPUT_REQUEST_METHOD = "item/tool/requestUserInput"
|
|
17
17
|
|
|
18
18
|
OUTPUT_PREVIEW_CHARS = 4000
|
|
19
|
+
COLLABORATION_TURN_CACHE_SIZE = 512
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@dataclass(slots=True)
|
|
@@ -35,6 +36,8 @@ class TimelineReducer:
|
|
|
35
36
|
self._client_message_by_turn: dict[tuple[str, str | None, str], dict[str, Any]] = {}
|
|
36
37
|
self._pending_client_messages: dict[tuple[str, str | None], list[dict[str, Any]]] = {}
|
|
37
38
|
self._reasoning_index_by_turn: dict[tuple[str, str], int] = {}
|
|
39
|
+
self._pending_collaboration_mode: dict[tuple[str, str], str] = {}
|
|
40
|
+
self._collaboration_mode_by_turn: OrderedDict[tuple[str, str, str], str] = OrderedDict()
|
|
38
41
|
self._next_order = 1
|
|
39
42
|
|
|
40
43
|
def bind_session(self, session_id: str, thread_id: str) -> None:
|
|
@@ -47,6 +50,52 @@ class TimelineReducer:
|
|
|
47
50
|
def session_for_thread(self, thread_id: str) -> str | None:
|
|
48
51
|
return self._session_by_thread.get(thread_id)
|
|
49
52
|
|
|
53
|
+
def register_collaboration_mode(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
session_id: str,
|
|
57
|
+
thread_id: str,
|
|
58
|
+
mode: str,
|
|
59
|
+
turn_id: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
if turn_id is None:
|
|
62
|
+
self._pending_collaboration_mode[(session_id, thread_id)] = mode
|
|
63
|
+
return
|
|
64
|
+
self._pending_collaboration_mode.pop((session_id, thread_id), None)
|
|
65
|
+
self._bind_collaboration_turn(session_id, thread_id, turn_id, mode)
|
|
66
|
+
|
|
67
|
+
def cancel_pending_collaboration_mode(self, *, session_id: str, thread_id: str) -> None:
|
|
68
|
+
self._pending_collaboration_mode.pop((session_id, thread_id), None)
|
|
69
|
+
|
|
70
|
+
def _bind_pending_collaboration_turn(
|
|
71
|
+
self,
|
|
72
|
+
session_id: str,
|
|
73
|
+
thread_id: str | None,
|
|
74
|
+
turn_id: str | None,
|
|
75
|
+
) -> None:
|
|
76
|
+
if thread_id is None or turn_id is None:
|
|
77
|
+
return
|
|
78
|
+
mode = self._pending_collaboration_mode.pop((session_id, thread_id), None)
|
|
79
|
+
if mode is not None:
|
|
80
|
+
self._bind_collaboration_turn(session_id, thread_id, turn_id, mode)
|
|
81
|
+
|
|
82
|
+
def _bind_collaboration_turn(self, session_id: str, thread_id: str, turn_id: str, mode: str) -> None:
|
|
83
|
+
key = (session_id, thread_id, turn_id)
|
|
84
|
+
self._collaboration_mode_by_turn[key] = mode
|
|
85
|
+
self._collaboration_mode_by_turn.move_to_end(key)
|
|
86
|
+
while len(self._collaboration_mode_by_turn) > COLLABORATION_TURN_CACHE_SIZE:
|
|
87
|
+
self._collaboration_mode_by_turn.popitem(last=False)
|
|
88
|
+
|
|
89
|
+
def _collaboration_mode_for_turn(
|
|
90
|
+
self,
|
|
91
|
+
session_id: str,
|
|
92
|
+
thread_id: str | None,
|
|
93
|
+
turn_id: str | None,
|
|
94
|
+
) -> str | None:
|
|
95
|
+
if thread_id is None or turn_id is None:
|
|
96
|
+
return None
|
|
97
|
+
return self._collaboration_mode_by_turn.get((session_id, thread_id, turn_id))
|
|
98
|
+
|
|
50
99
|
def _session_update(
|
|
51
100
|
self,
|
|
52
101
|
*,
|
|
@@ -264,6 +313,10 @@ class TimelineReducer:
|
|
|
264
313
|
return ReductionResult()
|
|
265
314
|
if thread_id:
|
|
266
315
|
self.bind_session(session_id, thread_id)
|
|
316
|
+
# Codex does not guarantee that turn/started is the first event for a
|
|
317
|
+
# turn. Bind on the first notification carrying the real turn id so an
|
|
318
|
+
# early item or turn/completed event receives the requested mode too.
|
|
319
|
+
self._bind_pending_collaboration_turn(session_id, thread_id, turn_id)
|
|
267
320
|
|
|
268
321
|
if method == "thread/name/updated":
|
|
269
322
|
return ReductionResult(
|
|
@@ -916,6 +969,13 @@ class TimelineReducer:
|
|
|
916
969
|
}
|
|
917
970
|
if source_extra:
|
|
918
971
|
source.update(source_extra)
|
|
972
|
+
collaboration_mode = self._collaboration_mode_for_turn(
|
|
973
|
+
session_id,
|
|
974
|
+
source_session_id,
|
|
975
|
+
turn_id,
|
|
976
|
+
)
|
|
977
|
+
if collaboration_mode is not None:
|
|
978
|
+
source.setdefault("collaborationMode", collaboration_mode)
|
|
919
979
|
source = {key: value for key, value in source.items() if value is not None}
|
|
920
980
|
content_hash = _content_hash(item_type, status, role, content, source)
|
|
921
981
|
if existing and existing.get("contentHash") == content_hash:
|
|
@@ -182,6 +182,17 @@ class MissingAfterUnloadedThreadStartRpc(FakeCodexRpc):
|
|
|
182
182
|
return {}
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
class FailingRecoveredTurnStartRpc(CollaborationCodexRpc):
|
|
186
|
+
async def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
187
|
+
if method == "thread/resume":
|
|
188
|
+
self.requests.append((method, params))
|
|
189
|
+
return {"thread": {"id": (params or {}).get("threadId"), "status": {"type": "loaded"}}}
|
|
190
|
+
if method == "turn/start":
|
|
191
|
+
self.requests.append((method, params))
|
|
192
|
+
raise RuntimeError(json.dumps({"code": -32600, "message": "id not found"}))
|
|
193
|
+
return await super().request(method, params)
|
|
194
|
+
|
|
195
|
+
|
|
185
196
|
class MissingModelProviderResumeRpc(FakeCodexRpc):
|
|
186
197
|
async def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
187
198
|
self.requests.append((method, params))
|
|
@@ -429,6 +440,122 @@ def test_reducer_maps_codex_turn_and_message_notifications() -> None:
|
|
|
429
440
|
assert [item["type"] for item in completed.timeline_items] == ["turn.start", "turn.end"]
|
|
430
441
|
|
|
431
442
|
|
|
443
|
+
def test_reducer_persists_collaboration_mode_across_live_and_snapshot_items() -> None:
|
|
444
|
+
reducer = TimelineReducer()
|
|
445
|
+
reducer.bind_session("sess_1", "thr_1")
|
|
446
|
+
reducer.register_collaboration_mode(
|
|
447
|
+
session_id="sess_1",
|
|
448
|
+
thread_id="thr_1",
|
|
449
|
+
mode="plan",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
started = reducer.reduce_notification(
|
|
453
|
+
{
|
|
454
|
+
"method": "turn/started",
|
|
455
|
+
"params": {"threadId": "thr_1", "turnId": "turn_plan", "turn": {"input": "plan"}},
|
|
456
|
+
}
|
|
457
|
+
).timeline_items[0]
|
|
458
|
+
message = reducer.reduce_notification(
|
|
459
|
+
{
|
|
460
|
+
"method": "item/agentMessage/delta",
|
|
461
|
+
"params": {
|
|
462
|
+
"threadId": "thr_1",
|
|
463
|
+
"turnId": "turn_plan",
|
|
464
|
+
"itemId": "msg_plan",
|
|
465
|
+
"delta": "# Final plan",
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
).timeline_items[0]
|
|
469
|
+
completed = reducer.reduce_notification(
|
|
470
|
+
{
|
|
471
|
+
"method": "turn/completed",
|
|
472
|
+
"params": {
|
|
473
|
+
"threadId": "thr_1",
|
|
474
|
+
"turnId": "turn_plan",
|
|
475
|
+
"turn": {"status": "completed"},
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
).timeline_items
|
|
479
|
+
snapshot = reducer.reduce_thread_snapshot(
|
|
480
|
+
"sess_1",
|
|
481
|
+
{
|
|
482
|
+
"id": "thr_1",
|
|
483
|
+
"status": {"type": "idle"},
|
|
484
|
+
"turns": [
|
|
485
|
+
{
|
|
486
|
+
"id": "turn_plan",
|
|
487
|
+
"status": "completed",
|
|
488
|
+
"items": [
|
|
489
|
+
{
|
|
490
|
+
"id": "msg_plan",
|
|
491
|
+
"type": "agentMessage",
|
|
492
|
+
"text": "# Final plan",
|
|
493
|
+
"status": "completed",
|
|
494
|
+
}
|
|
495
|
+
],
|
|
496
|
+
}
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
).timeline_items
|
|
500
|
+
|
|
501
|
+
for item in [started, message, *completed, *snapshot]:
|
|
502
|
+
assert item["source"]["collaborationMode"] == "plan"
|
|
503
|
+
|
|
504
|
+
reducer.register_collaboration_mode(
|
|
505
|
+
session_id="sess_1",
|
|
506
|
+
thread_id="thr_1",
|
|
507
|
+
turn_id="turn_default",
|
|
508
|
+
mode="default",
|
|
509
|
+
)
|
|
510
|
+
default_message = reducer.reduce_notification(
|
|
511
|
+
{
|
|
512
|
+
"method": "item/agentMessage/delta",
|
|
513
|
+
"params": {
|
|
514
|
+
"threadId": "thr_1",
|
|
515
|
+
"turnId": "turn_default",
|
|
516
|
+
"itemId": "msg_default",
|
|
517
|
+
"delta": "Implemented",
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
).timeline_items[0]
|
|
521
|
+
assert default_message["source"]["collaborationMode"] == "default"
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def test_reducer_binds_pending_collaboration_mode_on_first_turn_event() -> None:
|
|
525
|
+
reducer = TimelineReducer()
|
|
526
|
+
reducer.bind_session("sess_1", "thr_1")
|
|
527
|
+
reducer.register_collaboration_mode(
|
|
528
|
+
session_id="sess_1",
|
|
529
|
+
thread_id="thr_1",
|
|
530
|
+
mode="plan",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
message = reducer.reduce_notification(
|
|
534
|
+
{
|
|
535
|
+
"method": "item/agentMessage/delta",
|
|
536
|
+
"params": {
|
|
537
|
+
"threadId": "thr_1",
|
|
538
|
+
"turnId": "turn_out_of_order",
|
|
539
|
+
"itemId": "msg_1",
|
|
540
|
+
"delta": "# Final plan",
|
|
541
|
+
},
|
|
542
|
+
}
|
|
543
|
+
).timeline_items[0]
|
|
544
|
+
completed = reducer.reduce_notification(
|
|
545
|
+
{
|
|
546
|
+
"method": "turn/completed",
|
|
547
|
+
"params": {
|
|
548
|
+
"threadId": "thr_1",
|
|
549
|
+
"turnId": "turn_out_of_order",
|
|
550
|
+
"turn": {"status": "completed"},
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
).timeline_items
|
|
554
|
+
|
|
555
|
+
assert message["source"]["collaborationMode"] == "plan"
|
|
556
|
+
assert all(item["source"]["collaborationMode"] == "plan" for item in completed)
|
|
557
|
+
|
|
558
|
+
|
|
432
559
|
def test_reducer_keeps_agent_delta_items_separate_by_item_id() -> None:
|
|
433
560
|
reducer = TimelineReducer()
|
|
434
561
|
reducer.bind_session("sess_1", "thr_1")
|
|
@@ -951,6 +1078,10 @@ def test_adapter_replaces_thread_when_unloaded_codex_thread_rollout_is_missing()
|
|
|
951
1078
|
asyncio.run(_exercise_unloaded_thread_missing_rollout_replacement())
|
|
952
1079
|
|
|
953
1080
|
|
|
1081
|
+
def test_adapter_clears_pending_mode_when_recovered_turn_start_retry_fails() -> None:
|
|
1082
|
+
asyncio.run(_exercise_failed_recovered_turn_start_clears_pending_mode())
|
|
1083
|
+
|
|
1084
|
+
|
|
954
1085
|
def test_codex_commands_use_native_app_server_methods() -> None:
|
|
955
1086
|
async def exercise() -> None:
|
|
956
1087
|
rpc = FakeCodexRpc()
|
|
@@ -1218,6 +1349,40 @@ async def _exercise_unloaded_thread_missing_rollout_replacement() -> None:
|
|
|
1218
1349
|
]
|
|
1219
1350
|
|
|
1220
1351
|
|
|
1352
|
+
async def _exercise_failed_recovered_turn_start_clears_pending_mode() -> None:
|
|
1353
|
+
rpc = FailingRecoveredTurnStartRpc()
|
|
1354
|
+
adapter = CodexAdapter(rpc=rpc) # type: ignore[arg-type]
|
|
1355
|
+
adapter.reducer.bind_session("sess_1", "thr_1")
|
|
1356
|
+
adapter._loaded_thread_ids.add("thr_1")
|
|
1357
|
+
|
|
1358
|
+
try:
|
|
1359
|
+
await adapter.start_turn(
|
|
1360
|
+
{
|
|
1361
|
+
"sessionId": "sess_1",
|
|
1362
|
+
"externalSessionId": "thr_1",
|
|
1363
|
+
"content": "make a plan",
|
|
1364
|
+
"collaborationMode": "plan",
|
|
1365
|
+
}
|
|
1366
|
+
)
|
|
1367
|
+
except RuntimeError:
|
|
1368
|
+
pass
|
|
1369
|
+
else:
|
|
1370
|
+
raise AssertionError("expected recovered turn/start retry to fail")
|
|
1371
|
+
|
|
1372
|
+
later = adapter.reducer.reduce_notification(
|
|
1373
|
+
{
|
|
1374
|
+
"method": "item/agentMessage/delta",
|
|
1375
|
+
"params": {
|
|
1376
|
+
"threadId": "thr_1",
|
|
1377
|
+
"turnId": "turn_later",
|
|
1378
|
+
"itemId": "msg_later",
|
|
1379
|
+
"delta": "normal answer",
|
|
1380
|
+
},
|
|
1381
|
+
}
|
|
1382
|
+
).timeline_items[0]
|
|
1383
|
+
assert "collaborationMode" not in later["source"]
|
|
1384
|
+
|
|
1385
|
+
|
|
1221
1386
|
def test_adapter_sync_session_refreshes_loaded_codex_thread() -> None:
|
|
1222
1387
|
async def exercise() -> None:
|
|
1223
1388
|
rpc = FakeCodexRpc()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|