clink-cli 0.2.1__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.
Files changed (141) hide show
  1. clink/__init__.py +19 -0
  2. clink/agents/__init__.py +30 -0
  3. clink/agents/base.py +70 -0
  4. clink/agents/claude.py +7 -0
  5. clink/agents/codex.py +7 -0
  6. clink/agents/gemini.py +7 -0
  7. clink/allowed_roots.py +356 -0
  8. clink/backend.py +765 -0
  9. clink/cli.py +594 -0
  10. clink/constants.py +78 -0
  11. clink/contracts.py +167 -0
  12. clink/files.py +70 -0
  13. clink/generation.py +274 -0
  14. clink/generation_capabilities.py +207 -0
  15. clink/generation_contracts.py +214 -0
  16. clink/generation_policy.py +174 -0
  17. clink/models.py +243 -0
  18. clink/native.py +409 -0
  19. clink/native_adapters.py +244 -0
  20. clink/native_contracts.py +68 -0
  21. clink/native_generation.py +421 -0
  22. clink/native_generation_claude_policy.py +59 -0
  23. clink/native_generation_contracts.py +264 -0
  24. clink/native_generation_families.py +218 -0
  25. clink/native_generation_family_policy.py +14 -0
  26. clink/native_generation_files.py +153 -0
  27. clink/native_generation_policy.py +82 -0
  28. clink/native_policy.py +86 -0
  29. clink/parsers/__init__.py +34 -0
  30. clink/parsers/agy.py +53 -0
  31. clink/parsers/agy_native.py +161 -0
  32. clink/parsers/agy_native_generation.py +189 -0
  33. clink/parsers/base.py +209 -0
  34. clink/parsers/claude.py +104 -0
  35. clink/parsers/claude_native.py +113 -0
  36. clink/parsers/claude_native_generation.py +346 -0
  37. clink/parsers/codex.py +172 -0
  38. clink/parsers/codex_generation.py +212 -0
  39. clink/parsers/codex_native.py +236 -0
  40. clink/parsers/codex_native_generation.py +367 -0
  41. clink/parsers/gemini.py +110 -0
  42. clink/parsers/grok.py +63 -0
  43. clink/parsers/grok_native.py +91 -0
  44. clink/parsers/grok_native_generation.py +157 -0
  45. clink/policy.py +180 -0
  46. clink/registry.py +544 -0
  47. clink/resources/CODEX-LICENSE +201 -0
  48. clink/resources/CODEX-NOTICE +6 -0
  49. clink/resources/PROVENANCE.md +16 -0
  50. clink/resources/codex-0.166.0-models-direct.json +1284 -0
  51. clink/resources/codex-0.166.0-models-generation.json +1269 -0
  52. clink/resources/native-agy-1.4.12-admission.json +746 -0
  53. clink/resources/native-claude-2.1.304-admission.json +99 -0
  54. clink/resources/native-generation-claude-2.1.304-admission.json +277 -0
  55. clink/resources/native-generation-codex-0.166.0-admission.json +309 -0
  56. clink/resources/native-generation-grok-1.0.70-admission.json +329 -0
  57. clink/resources/native-grok-1.0.70-admission.json +644 -0
  58. clink/runner.py +303 -0
  59. clink/setup.py +399 -0
  60. clink/tasks.py +695 -0
  61. clink_cli-0.2.1.dist-info/METADATA +225 -0
  62. clink_cli-0.2.1.dist-info/RECORD +141 -0
  63. clink_cli-0.2.1.dist-info/WHEEL +5 -0
  64. clink_cli-0.2.1.dist-info/entry_points.txt +3 -0
  65. clink_cli-0.2.1.dist-info/licenses/LICENSE +197 -0
  66. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-LICENSE +201 -0
  67. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-NOTICE +6 -0
  68. clink_cli-0.2.1.dist-info/top_level.txt +7 -0
  69. conf/__init__.py +1 -0
  70. conf/cli_clients/agy.json +40 -0
  71. conf/cli_clients/claude.json +39 -0
  72. conf/cli_clients/codex.json +40 -0
  73. conf/cli_clients/gemini.json +23 -0
  74. conf/cli_clients/grok.json +40 -0
  75. config.py +40 -0
  76. server.py +896 -0
  77. systemprompts/__init__.py +35 -0
  78. systemprompts/analyze_prompt.py +87 -0
  79. systemprompts/chat_prompt.py +57 -0
  80. systemprompts/clink/codex_codereviewer.txt +8 -0
  81. systemprompts/clink/default.txt +8 -0
  82. systemprompts/clink/default_codereviewer.txt +8 -0
  83. systemprompts/clink/default_planner.txt +7 -0
  84. systemprompts/clink/native_codereviewer.txt +1 -0
  85. systemprompts/clink/native_default.txt +1 -0
  86. systemprompts/clink/native_planner.txt +1 -0
  87. systemprompts/codereview_prompt.py +91 -0
  88. systemprompts/consensus_prompt.py +120 -0
  89. systemprompts/debug_prompt.py +150 -0
  90. systemprompts/docgen_prompt.py +252 -0
  91. systemprompts/generate_code_prompt.py +181 -0
  92. systemprompts/planner_prompt.py +127 -0
  93. systemprompts/precommit_prompt.py +179 -0
  94. systemprompts/refactor_prompt.py +323 -0
  95. systemprompts/secaudit_prompt.py +420 -0
  96. systemprompts/testgen_prompt.py +133 -0
  97. systemprompts/thinkdeep_prompt.py +55 -0
  98. systemprompts/tracer_prompt.py +145 -0
  99. tools/__init__.py +43 -0
  100. tools/analyze.py +583 -0
  101. tools/apilookup.py +97 -0
  102. tools/challenge.py +198 -0
  103. tools/chat.py +187 -0
  104. tools/clink.py +130 -0
  105. tools/codereview.py +724 -0
  106. tools/consensus.py +790 -0
  107. tools/debug.py +573 -0
  108. tools/docgen.py +577 -0
  109. tools/listmodels.py +48 -0
  110. tools/models.py +373 -0
  111. tools/planner.py +533 -0
  112. tools/precommit.py +776 -0
  113. tools/refactor.py +645 -0
  114. tools/secaudit.py +685 -0
  115. tools/shared/__init__.py +19 -0
  116. tools/shared/base_models.py +167 -0
  117. tools/shared/base_tool.py +659 -0
  118. tools/shared/exceptions.py +20 -0
  119. tools/shared/schema_builders.py +159 -0
  120. tools/simple/__init__.py +18 -0
  121. tools/simple/base.py +539 -0
  122. tools/testgen.py +513 -0
  123. tools/thinkdeep.py +586 -0
  124. tools/tracer.py +763 -0
  125. tools/version.py +53 -0
  126. tools/workflow/__init__.py +22 -0
  127. tools/workflow/base.py +448 -0
  128. tools/workflow/schema_builders.py +166 -0
  129. tools/workflow/workflow_mixin.py +1195 -0
  130. utils/__init__.py +21 -0
  131. utils/call_state.py +349 -0
  132. utils/client_info.py +293 -0
  133. utils/consensus_manifest.py +131 -0
  134. utils/conversation_memory.py +582 -0
  135. utils/env.py +111 -0
  136. utils/file_types.py +271 -0
  137. utils/file_utils.py +803 -0
  138. utils/image_utils.py +94 -0
  139. utils/security_config.py +163 -0
  140. utils/storage_backend.py +177 -0
  141. utils/token_utils.py +54 -0
@@ -0,0 +1,212 @@
1
+ """Strict, text-only Codex JSONL parsing for the separate generation profile."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .base import (
11
+ MAX_METADATA_NUMBER,
12
+ BaseParser,
13
+ ParsedCLIResponse,
14
+ ParserError,
15
+ cli_failure,
16
+ load_json,
17
+ model_identifier,
18
+ )
19
+
20
+ MAX_EVENTS = 16384
21
+ _MAX_SOURCE_BYTES = 1024 * 1024
22
+ _USAGE_REQUIRED = frozenset({"input_tokens", "cached_input_tokens", "output_tokens"})
23
+ _USAGE_OPTIONAL = frozenset({"cache_write_input_tokens", "reasoning_output_tokens"})
24
+ _ITEM_EVENTS = frozenset({"item.started", "item.updated", "item.completed"})
25
+ _TOOL_ITEMS = frozenset(
26
+ {
27
+ "command_execution",
28
+ "file_change",
29
+ "mcp_tool_call",
30
+ "collab_tool_call",
31
+ "web_search",
32
+ "todo_list",
33
+ "image_generation",
34
+ }
35
+ )
36
+
37
+ # Admission binds this explicit acceptance contract independently of advisory and
38
+ # native parsers. Change it whenever generation acceptance semantics change.
39
+ PARSER_CONTRACT = (
40
+ "codex-generation-jsonl/v1\n"
41
+ "strict-json:no-duplicate-keys,no-nonfinite-numbers;jsonl-newline=LF;max-events=16384\n"
42
+ "thread.started:{type,thread_id:nonempty-utf8-text};turn.started:{type};one-thread,one-turn,in-order\n"
43
+ "item.started|item.updated|item.completed:{type,item};item:{type:reasoning|agent_message,text:utf8,id?:nonempty-utf8}\n"
44
+ "text-item-lifecycle:no-reused-completed-id,no-type-change,no-duplicate-start,no-incomplete-items\n"
45
+ "final:exactly-one-completed-nonblank-agent_message;preserve-text-exactly;no-item-after-final\n"
46
+ "turn.completed:{type,usage,model?:null|bounded-native-id};one-terminal,last-event\n"
47
+ "usage:required=input_tokens,cached_input_tokens,output_tokens;"
48
+ "optional=cache_write_input_tokens,reasoning_output_tokens;nonnegative-safe-json-integers\n"
49
+ "error:{type,message:utf8};turn.failed:{type,error:{message:utf8}};item-error:{type,message:utf8,id?}\n"
50
+ "unknown-keys,unknown-events,unknown-items,malformed,failed,incomplete=protocol_error;"
51
+ "known-tool-items=command_execution,file_change,mcp_tool_call,collab_tool_call,web_search,todo_list,image_generation:"
52
+ "unexpected_tool_event;known-failure-diagnostics=authentication_required|rate_limited\n"
53
+ "metadata:usage,actual_model=null-when-unreported;no-thread-or-item-identities\n"
54
+ "identity:sha256(canonical-json(contract,source_sha256(codex_generation.py,base.py)));source-limit=1048576-bytes-each\n"
55
+ )
56
+
57
+
58
+ def parser_contract_digest() -> str:
59
+ """Bind the acceptance contract and both parser implementations for admission."""
60
+
61
+ sources = {}
62
+ try:
63
+ for name in ("codex_generation.py", "base.py"):
64
+ with Path(__file__).with_name(name).open("rb") as source:
65
+ content = source.read(_MAX_SOURCE_BYTES + 1)
66
+ if len(content) > _MAX_SOURCE_BYTES:
67
+ raise _protocol_error("Codex generation parser source exceeds its identity limit")
68
+ sources[name] = hashlib.sha256(content).hexdigest()
69
+ except OSError:
70
+ raise _protocol_error("Codex generation parser source identity is unavailable") from None
71
+ identity = json.dumps(
72
+ {"contract": PARSER_CONTRACT, "source_sha256": sources},
73
+ sort_keys=True,
74
+ separators=(",", ":"),
75
+ ensure_ascii=False,
76
+ )
77
+ return hashlib.sha256(identity.encode("utf-8")).hexdigest()
78
+
79
+
80
+ def _protocol_error(message: str) -> ParserError:
81
+ return ParserError(message, code="protocol_error")
82
+
83
+
84
+ def _object(value: object, required: frozenset[str], optional: frozenset[str] = frozenset()) -> dict[str, Any]:
85
+ if not isinstance(value, dict) or not required <= value.keys() or value.keys() - (required | optional):
86
+ raise _protocol_error("Codex generation record has an unsupported shape")
87
+ return value
88
+
89
+
90
+ def _text(value: object, *, nonempty: bool = False) -> str:
91
+ if not isinstance(value, str) or (nonempty and not value.strip()):
92
+ raise _protocol_error("Codex generation text field is invalid")
93
+ try:
94
+ value.encode("utf-8")
95
+ except UnicodeError:
96
+ raise _protocol_error("Codex generation text is not valid UTF-8") from None
97
+ return value
98
+
99
+
100
+ def _failure(value: dict[str, Any], stderr: str) -> ParserError:
101
+ return cli_failure(value, stderr, message="Codex generation reported a failure", code="protocol_error")
102
+
103
+
104
+ class CodexGenerationParser(BaseParser):
105
+ """Require a complete, unambiguous text-only generation turn."""
106
+
107
+ name = "codex_generation_jsonl"
108
+
109
+ def parse(self, stdout: str, stderr: str = "") -> ParsedCLIResponse:
110
+ if not isinstance(stdout, str) or not isinstance(stderr, str):
111
+ raise _protocol_error("Codex generation streams must be textual")
112
+ # splitlines() also splits valid literal Unicode separators inside JSON
113
+ # strings. Only LF delimits JSONL; CRLF remains valid JSON whitespace.
114
+ lines = [line for line in stdout.split("\n") if line.strip(" \t\r")]
115
+ if not lines or len(lines) > MAX_EVENTS:
116
+ raise cli_failure(stderr, message="Codex generation output is empty or too large", code="protocol_error")
117
+
118
+ thread_started = False
119
+ turn_started = False
120
+ terminal = False
121
+ final_text: str | None = None
122
+ item_types: dict[str | None, str] = {}
123
+ active_items: set[str | None] = set()
124
+ completed_items: set[str] = set()
125
+ metadata: dict[str, Any] = {"actual_model": None}
126
+
127
+ for line in lines:
128
+ try:
129
+ event = load_json(line)
130
+ except ParserError:
131
+ raise _protocol_error("Codex generation returned malformed JSON") from None
132
+ if not isinstance(event, dict) or not isinstance(event.get("type"), str):
133
+ raise _protocol_error("Codex generation events must be typed objects")
134
+ if terminal:
135
+ raise _protocol_error("Codex generation continued after its terminal result")
136
+ event_type = event["type"]
137
+
138
+ if event_type == "error":
139
+ _object(event, frozenset({"type", "message"}))
140
+ _text(event["message"])
141
+ raise _failure(event, stderr)
142
+ if event_type == "turn.failed":
143
+ _object(event, frozenset({"type", "error"}))
144
+ error = _object(event["error"], frozenset({"message"}))
145
+ _text(error["message"])
146
+ raise _failure(event, stderr)
147
+
148
+ if event_type == "thread.started":
149
+ _object(event, frozenset({"type", "thread_id"}))
150
+ _text(event["thread_id"], nonempty=True)
151
+ if thread_started or turn_started:
152
+ raise _protocol_error("Codex generation thread lifecycle is contradictory")
153
+ thread_started = True
154
+ elif event_type == "turn.started":
155
+ _object(event, frozenset({"type"}))
156
+ if not thread_started or turn_started:
157
+ raise _protocol_error("Codex generation turn lifecycle is contradictory")
158
+ turn_started = True
159
+ elif event_type in _ITEM_EVENTS:
160
+ _object(event, frozenset({"type", "item"}))
161
+ item = event["item"]
162
+ if not isinstance(item, dict) or not isinstance(item.get("type"), str):
163
+ raise _protocol_error("Codex generation items must be typed objects")
164
+ item_type = item["type"]
165
+ if item_type in _TOOL_ITEMS:
166
+ raise ParserError("Codex generation emitted an unexpected tool item", code="unexpected_tool_event")
167
+ if item_type == "error":
168
+ _object(item, frozenset({"type", "message"}), frozenset({"id"}))
169
+ if "id" in item:
170
+ _text(item["id"], nonempty=True)
171
+ _text(item["message"])
172
+ raise _failure(item, stderr)
173
+ if item_type not in {"agent_message", "reasoning"}:
174
+ raise _protocol_error("Codex generation item type is unsupported")
175
+ _object(item, frozenset({"type", "text"}), frozenset({"id"}))
176
+ text = _text(item["text"])
177
+ item_id = _text(item["id"], nonempty=True) if "id" in item else None
178
+ if not turn_started or final_text is not None:
179
+ raise _protocol_error("Codex generation text appeared outside the active turn")
180
+ if item_id in completed_items or (item_id in item_types and item_types[item_id] != item_type):
181
+ raise _protocol_error("Codex generation item lifecycle is contradictory")
182
+ if event_type == "item.started" and item_id in active_items:
183
+ raise _protocol_error("Codex generation item was started more than once")
184
+ item_types[item_id] = item_type
185
+ if event_type == "item.completed":
186
+ active_items.discard(item_id)
187
+ if item_id is not None:
188
+ completed_items.add(item_id)
189
+ else:
190
+ item_types.pop(None, None)
191
+ if item_type == "agent_message":
192
+ final_text = _text(text, nonempty=True)
193
+ else:
194
+ active_items.add(item_id)
195
+ elif event_type == "turn.completed":
196
+ _object(event, frozenset({"type", "usage"}), frozenset({"model"}))
197
+ if not turn_started or active_items or final_text is None:
198
+ raise _protocol_error("Codex generation turn is incomplete")
199
+ usage = _object(event["usage"], _USAGE_REQUIRED, _USAGE_OPTIONAL)
200
+ if any(type(count) is not int or not 0 <= count <= MAX_METADATA_NUMBER for count in usage.values()):
201
+ raise _protocol_error("Codex generation usage is invalid")
202
+ actual_model = event.get("model")
203
+ if actual_model is not None and model_identifier(actual_model) is None:
204
+ raise _protocol_error("Codex generation actual model is invalid")
205
+ metadata.update(usage=usage.copy(), actual_model=actual_model)
206
+ terminal = True
207
+ else:
208
+ raise _protocol_error("Codex generation event type is unsupported")
209
+
210
+ if not terminal or final_text is None:
211
+ raise cli_failure(stderr, message="Codex generation output is incomplete", code="protocol_error")
212
+ return ParsedCLIResponse(content=final_text, metadata=metadata)
@@ -0,0 +1,236 @@
1
+ """Parse native Codex JSONL without retaining tool payloads or public sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from clink.native_contracts import ParsedNativeResponse
9
+ from clink.parsers.base import ParserError, bounded_counts, cli_failure, load_json
10
+
11
+ RESULT_CHARACTERS = 20000
12
+ MAX_EVENTS = 16384
13
+ _SESSION = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\Z")
14
+ _EVENT_TYPES = {
15
+ "thread.started",
16
+ "turn.started",
17
+ "item.started",
18
+ "item.updated",
19
+ "item.completed",
20
+ "turn.completed",
21
+ "turn.failed",
22
+ "error",
23
+ }
24
+ _ITEM_TYPES = {
25
+ "agent_message",
26
+ "reasoning",
27
+ "command_execution",
28
+ "file_change",
29
+ "mcp_tool_call",
30
+ "collab_tool_call",
31
+ "web_search",
32
+ "todo_list",
33
+ "error",
34
+ }
35
+ _TOOL_TYPES = {"command_execution", "file_change", "mcp_tool_call", "collab_tool_call"}
36
+ _USAGE_FIELDS = (
37
+ "input_tokens",
38
+ "cached_input_tokens",
39
+ "cache_write_input_tokens",
40
+ "output_tokens",
41
+ "total_tokens",
42
+ "reasoning_output_tokens",
43
+ )
44
+ _EXEC_POLICY_DENIAL = re.compile(
45
+ r"`(?P<command>[^`\r\n]+)` rejected: policy forbids commands starting with `(?P<prefix>[^`\r\n]+)`\Z"
46
+ )
47
+ _COMMAND_APPROVAL_DENIALS = {
48
+ "approval required by policy, but AskForApproval is set to Never",
49
+ "approval required by policy, but AskForApproval::Granular.sandbox_approval is false",
50
+ "approval required by policy rule, but AskForApproval::Granular.rules is false",
51
+ }
52
+ _MCP_APPROVAL_DENIALS = {
53
+ "MCP tool call requires approval, but approval policy is never",
54
+ "MCP tool call cancelled: interactive approval is unavailable in exec mode",
55
+ }
56
+
57
+
58
+ def validate_session_id(value: str | None) -> str | None:
59
+ """Accept only canonical UUID-shaped private Codex thread identities."""
60
+
61
+ if value is not None and (not isinstance(value, str) or not _SESSION.fullmatch(value)):
62
+ raise ParserError("Codex native session identity is invalid")
63
+ return value
64
+
65
+
66
+ def _validate_error(value: object) -> None:
67
+ if not isinstance(value, dict) or not isinstance(value.get("message"), str):
68
+ raise ParserError("Codex native error must contain a textual message")
69
+
70
+
71
+ def _validate_item(value: object) -> dict[str, Any]:
72
+ if not isinstance(value, dict) or not isinstance(value.get("type"), str) or value["type"] not in _ITEM_TYPES:
73
+ raise ParserError("Codex native item type is unsupported")
74
+ if not isinstance(value.get("id"), str) or not value["id"]:
75
+ raise ParserError("Codex native item identity is invalid")
76
+ item_type = value["type"]
77
+ if item_type in {"agent_message", "reasoning"} and not isinstance(value.get("text"), str):
78
+ raise ParserError("Codex native message must be textual")
79
+ if item_type == "error" and not isinstance(value.get("message"), str):
80
+ raise ParserError("Codex native item error must be textual")
81
+ if item_type in _TOOL_TYPES:
82
+ statuses = {"in_progress", "completed", "failed"}
83
+ if item_type == "command_execution":
84
+ # Pinned native status includes declined for both policy rejections
85
+ # and runtime setup failures, so it does not alone prove a denial.
86
+ statuses.add("declined")
87
+ if not isinstance(value.get("status"), str) or value["status"] not in statuses:
88
+ raise ParserError("Codex native tool status is invalid")
89
+ if item_type == "command_execution":
90
+ if not isinstance(value.get("command"), str) or not isinstance(value.get("aggregated_output"), str):
91
+ raise ParserError("Codex native command fields must be textual")
92
+ if "exit_code" not in value:
93
+ raise ParserError("Codex native command exit code is missing")
94
+ exit_code = value["exit_code"]
95
+ if exit_code is not None and (not isinstance(exit_code, int) or isinstance(exit_code, bool)):
96
+ raise ParserError("Codex native command exit code must be an integer")
97
+ if item_type == "mcp_tool_call":
98
+ if not isinstance(value.get("server"), str) or not isinstance(value.get("tool"), str):
99
+ raise ParserError("Codex native MCP tool identity must be textual")
100
+ if value.get("error") is not None:
101
+ _validate_error(value["error"])
102
+ if value.get("result") is not None and not isinstance(value["result"], dict):
103
+ raise ParserError("Codex native MCP result must be an object")
104
+ return value
105
+
106
+
107
+ def _permission_denial(item: dict[str, Any]) -> bool:
108
+ """Recognize a conservative subset of pinned 0.166.0 native diagnostics.
109
+
110
+ The protocol loses typed rejection provenance; generic declined items can
111
+ also represent setup failures. These exact native strings are a versioned
112
+ dependency, not a general interpretation of assistant or command prose.
113
+ """
114
+
115
+ if item["type"] == "command_execution" and item["status"] == "declined" and item["exit_code"] == -1:
116
+ if item["aggregated_output"] in _COMMAND_APPROVAL_DENIALS:
117
+ return True
118
+ marker = _EXEC_POLICY_DENIAL.fullmatch(item["aggregated_output"])
119
+ return (
120
+ marker is not None
121
+ and marker["command"] == item["command"]
122
+ and bool(marker["command"].strip())
123
+ and bool(marker["prefix"].strip())
124
+ )
125
+ if item["type"] == "mcp_tool_call" and item["status"] == "failed":
126
+ error = item.get("error")
127
+ return (
128
+ "result" in item
129
+ and item["result"] is None
130
+ and isinstance(error, dict)
131
+ and error["message"] in _MCP_APPROVAL_DENIALS
132
+ )
133
+ return False
134
+
135
+
136
+ class CodexNativeParser:
137
+ """Use the terminal turn outcome while preserving recoverable tool failures."""
138
+
139
+ def parse(self, stdout: str, stderr: str = "") -> ParsedNativeResponse:
140
+ if not isinstance(stdout, str):
141
+ raise ParserError("Codex native output must be textual")
142
+ lines = [line for line in stdout.splitlines() if line.strip()]
143
+ if not lines or len(lines) > MAX_EVENTS:
144
+ raise ParserError("Codex native output was empty or exceeded the event limit")
145
+ session_id = None
146
+ terminal: dict[str, Any] | None = None
147
+ fatal: dict[str, Any] | None = None
148
+ item_types: dict[str, str] = {}
149
+ completed_items: set[str] = set()
150
+ tool_errors = 0
151
+ permission_denials = 0
152
+ content = ""
153
+ message_count = 0
154
+
155
+ for line in lines:
156
+ event = load_json(line, allow_codex_web_search_ids=True)
157
+ if not isinstance(event, dict) or not isinstance(event.get("type"), str):
158
+ raise ParserError("Codex native events must be typed JSON objects")
159
+ event_type = event["type"]
160
+ if event_type not in _EVENT_TYPES:
161
+ raise ParserError("Codex native event type is unsupported")
162
+ # The installed CLI emits `error` followed by `turn.failed` for one
163
+ # fatal outcome. No other post-terminal event or duplicate is valid.
164
+ if terminal is not None or (fatal is not None and event_type != "turn.failed"):
165
+ raise ParserError("Codex native output continued after its terminal outcome")
166
+ if "thread_id" in event:
167
+ observed_session = event["thread_id"]
168
+ validate_session_id(observed_session)
169
+ if observed_session is None:
170
+ raise ParserError("Codex native thread identity is missing")
171
+ if session_id is not None and session_id != observed_session:
172
+ raise ParserError("Codex native output changed its session identity")
173
+ session_id = observed_session
174
+ elif event_type == "thread.started":
175
+ raise ParserError("Codex native thread identity is missing")
176
+
177
+ if event_type.startswith("item."):
178
+ item = _validate_item(event.get("item"))
179
+ item_id, item_type = item["id"], item["type"]
180
+ if item_id in completed_items or (item_id in item_types and item_types[item_id] != item_type):
181
+ raise ParserError("Codex native item lifecycle is contradictory")
182
+ item_types[item_id] = item_type
183
+ if event_type == "item.completed":
184
+ completed_items.add(item_id)
185
+ if item_type in _TOOL_TYPES and item["status"] == "in_progress":
186
+ raise ParserError("Codex native completed tool is still in progress")
187
+ failed = item_type == "error" or (
188
+ item_type in _TOOL_TYPES
189
+ and (
190
+ item["status"] in {"failed", "declined"}
191
+ or (item_type == "command_execution" and item.get("exit_code") not in (None, 0))
192
+ or (item_type == "mcp_tool_call" and item.get("error") is not None)
193
+ )
194
+ )
195
+ tool_errors += int(failed)
196
+ permission_denials += int(_permission_denial(item))
197
+ if item_type == "agent_message":
198
+ separator = "\n\n" if message_count else ""
199
+ remaining = RESULT_CHARACTERS + 1 - len(content)
200
+ if remaining > 0:
201
+ content += (separator + item["text"][:remaining])[:remaining]
202
+ message_count += 1
203
+ elif event_type == "error":
204
+ _validate_error(event)
205
+ fatal = event
206
+ elif event_type == "turn.failed":
207
+ _validate_error(event.get("error"))
208
+ terminal = event
209
+ elif event_type == "turn.completed":
210
+ if (
211
+ event.get("status", "completed") not in ("completed", "success")
212
+ or event.get("error") is not None
213
+ or event.get("errors")
214
+ or event.get("is_error", False) is not False
215
+ ):
216
+ raise ParserError("Codex native successful turn contains contradictory failure markers")
217
+ if not isinstance(event.get("usage"), dict):
218
+ raise ParserError("Codex native usage must be an object")
219
+ terminal = event
220
+
221
+ if terminal is None and fatal is None:
222
+ raise cli_failure(stderr, message="Codex native output had no terminal outcome")
223
+ success = terminal is not None and terminal["type"] == "turn.completed"
224
+ if success and session_id is None:
225
+ raise ParserError("Codex native successful turn omitted its session identity")
226
+ state = "permission_needed" if permission_denials else "completed" if success else "failed"
227
+ metadata: dict[str, Any] = {
228
+ "tool_error_count": tool_errors,
229
+ "permission_denial_count": permission_denials,
230
+ "truncated": len(content) > RESULT_CHARACTERS,
231
+ }
232
+ if success and (usage := bounded_counts(terminal.get("usage"), _USAGE_FIELDS)):
233
+ metadata["usage"] = usage
234
+ if state == "failed":
235
+ metadata["error_code"] = cli_failure(terminal, fatal, stderr, code="native_error").code
236
+ return ParsedNativeResponse(state, content[:RESULT_CHARACTERS], session_id, metadata)