coding-agent-cli 0.1.0__py3-none-any.whl → 0.2.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.
- coding_agent_cli/__init__.py +21 -7
- coding_agent_cli/_args.py +28 -8
- coding_agent_cli/_classify.py +194 -103
- coding_agent_cli/_drivers/_common.py +12 -321
- coding_agent_cli/_drivers/agy.py +269 -293
- coding_agent_cli/_drivers/claude.py +413 -268
- coding_agent_cli/_drivers/codex.py +352 -273
- coding_agent_cli/_drivers/pi.py +205 -308
- coding_agent_cli/_platform.py +71 -1
- coding_agent_cli/_transport.py +520 -0
- coding_agent_cli/api.py +74 -74
- coding_agent_cli/driver.py +308 -0
- coding_agent_cli/events.py +50 -9
- coding_agent_cli/registry.py +15 -39
- coding_agent_cli/types.py +45 -5
- coding_agent_cli-0.2.0.dist-info/METADATA +290 -0
- coding_agent_cli-0.2.0.dist-info/RECORD +21 -0
- coding_agent_cli/_drivers/_usage.py +0 -77
- coding_agent_cli/_drivers/codex_rollout.py +0 -439
- coding_agent_cli/_process.py +0 -338
- coding_agent_cli-0.1.0.dist-info/METADATA +0 -217
- coding_agent_cli-0.1.0.dist-info/RECORD +0 -22
- {coding_agent_cli-0.1.0.dist-info → coding_agent_cli-0.2.0.dist-info}/WHEEL +0 -0
- {coding_agent_cli-0.1.0.dist-info → coding_agent_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
coding_agent_cli/__init__.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""Run coding-agent CLIs, stream typed events, and return a classified result."""
|
|
2
2
|
|
|
3
3
|
from .api import EventStream, check, run, run_sync, stream
|
|
4
|
+
from .driver import CliDriver, Facts, Launch, Parser, RequestError
|
|
4
5
|
from .events import (
|
|
5
6
|
SCHEMA_VERSION,
|
|
6
7
|
Event,
|
|
8
|
+
FileChanged,
|
|
9
|
+
JsonValue,
|
|
7
10
|
MessageCompleted,
|
|
8
11
|
MessageStarted,
|
|
12
|
+
Notice,
|
|
13
|
+
RawEvent,
|
|
14
|
+
ReasoningDelta,
|
|
9
15
|
RunCompleted,
|
|
10
16
|
SessionIdentified,
|
|
11
17
|
TextDelta,
|
|
@@ -13,28 +19,36 @@ from .events import (
|
|
|
13
19
|
ToolStarted,
|
|
14
20
|
UsageUpdated,
|
|
15
21
|
)
|
|
16
|
-
from .registry import
|
|
17
|
-
from .types import Capabilities, Diagnostic, Outcome, Request, Result, Usage
|
|
22
|
+
from .registry import DriverRegistry
|
|
23
|
+
from .types import Capabilities, Diagnostic, ErrorCode, Outcome, Request, Result, Usage
|
|
18
24
|
|
|
19
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.2.0"
|
|
20
26
|
|
|
21
27
|
__all__ = [
|
|
22
28
|
"SCHEMA_VERSION",
|
|
23
29
|
"Capabilities",
|
|
24
30
|
"Diagnostic",
|
|
25
|
-
"
|
|
26
|
-
"EventStream",
|
|
31
|
+
"CliDriver",
|
|
27
32
|
"DriverRegistry",
|
|
33
|
+
"ErrorCode",
|
|
28
34
|
"Event",
|
|
29
|
-
"
|
|
35
|
+
"EventStream",
|
|
36
|
+
"FileChanged",
|
|
37
|
+
"Facts",
|
|
38
|
+
"JsonValue",
|
|
30
39
|
"MessageCompleted",
|
|
31
40
|
"MessageStarted",
|
|
41
|
+
"Launch",
|
|
42
|
+
"Notice",
|
|
32
43
|
"Outcome",
|
|
44
|
+
"RawEvent",
|
|
45
|
+
"ReasoningDelta",
|
|
33
46
|
"Request",
|
|
34
47
|
"Result",
|
|
35
48
|
"RunCompleted",
|
|
36
49
|
"SessionIdentified",
|
|
37
|
-
"
|
|
50
|
+
"Parser",
|
|
51
|
+
"RequestError",
|
|
38
52
|
"TextDelta",
|
|
39
53
|
"ToolCompleted",
|
|
40
54
|
"ToolStarted",
|
coding_agent_cli/_args.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Iterable, Mapping, Sequence
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from .types import Diagnostic, Request
|
|
8
|
+
from .types import Diagnostic, ErrorCode, Request
|
|
9
9
|
|
|
10
10
|
PORTABLE_FIELDS = ("model", "reasoning", "system_prompt")
|
|
11
11
|
|
|
@@ -22,11 +22,21 @@ def _has_flag(args: Sequence[str], flags: Iterable[str]) -> str | None:
|
|
|
22
22
|
def validate_args(args: Sequence[Any]) -> Diagnostic | None:
|
|
23
23
|
"""Reject non-string args, NUL bytes, and the reserved bare ``--`` token."""
|
|
24
24
|
if not all(isinstance(value, str) for value in args):
|
|
25
|
-
return Diagnostic(code=
|
|
25
|
+
return Diagnostic(code=ErrorCode.INVALID_ARGS, message="args must contain only strings")
|
|
26
26
|
if any("\x00" in value for value in args):
|
|
27
|
-
return Diagnostic(code=
|
|
27
|
+
return Diagnostic(code=ErrorCode.INVALID_ARGS, message="args cannot contain NUL bytes")
|
|
28
28
|
if "--" in args:
|
|
29
|
-
return Diagnostic(code=
|
|
29
|
+
return Diagnostic(code=ErrorCode.INVALID_ARGS, message="args cannot contain the reserved '--' token")
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_request(req: Request) -> Diagnostic | None:
|
|
34
|
+
"""Validate the request fields that are common to every CLI driver."""
|
|
35
|
+
error = validate_args(req.args)
|
|
36
|
+
if error is not None:
|
|
37
|
+
return error
|
|
38
|
+
if not req.cwd.is_dir():
|
|
39
|
+
return Diagnostic(code=ErrorCode.BAD_CWD, message="cwd is not an existing directory")
|
|
30
40
|
return None
|
|
31
41
|
|
|
32
42
|
|
|
@@ -36,7 +46,8 @@ def reject_transport_flags(args: Sequence[str], owned: Iterable[str]) -> Diagnos
|
|
|
36
46
|
if token is None:
|
|
37
47
|
return None
|
|
38
48
|
return Diagnostic(
|
|
39
|
-
code=
|
|
49
|
+
code=ErrorCode.INVALID_ARGS,
|
|
50
|
+
message=f"args cannot override the driver-owned flag {token.split('=', 1)[0]!r}",
|
|
40
51
|
)
|
|
41
52
|
|
|
42
53
|
|
|
@@ -45,7 +56,9 @@ def reject_unsupported(req: Request, supported: Iterable[str]) -> Diagnostic | N
|
|
|
45
56
|
allowed = set(supported)
|
|
46
57
|
for name in PORTABLE_FIELDS:
|
|
47
58
|
if getattr(req, name) is not None and name not in allowed:
|
|
48
|
-
return Diagnostic(
|
|
59
|
+
return Diagnostic(
|
|
60
|
+
code=ErrorCode.UNSUPPORTED_OPTION, message=f"driver {req.driver!r} does not support {name}"
|
|
61
|
+
)
|
|
49
62
|
return None
|
|
50
63
|
|
|
51
64
|
|
|
@@ -57,10 +70,17 @@ def reject_conflicts(req: Request, flags: Mapping[str, Iterable[str]]) -> Diagno
|
|
|
57
70
|
token = _has_flag(req.args, field_flags)
|
|
58
71
|
if token is not None:
|
|
59
72
|
return Diagnostic(
|
|
60
|
-
code=
|
|
73
|
+
code=ErrorCode.INVALID_ARGS,
|
|
61
74
|
message=f"args flag {token.split('=', 1)[0]!r} conflicts with the {name} option",
|
|
62
75
|
)
|
|
63
76
|
return None
|
|
64
77
|
|
|
65
78
|
|
|
66
|
-
__all__ = [
|
|
79
|
+
__all__ = [
|
|
80
|
+
"PORTABLE_FIELDS",
|
|
81
|
+
"reject_conflicts",
|
|
82
|
+
"reject_transport_flags",
|
|
83
|
+
"reject_unsupported",
|
|
84
|
+
"validate_args",
|
|
85
|
+
"validate_request",
|
|
86
|
+
]
|
coding_agent_cli/_classify.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Map
|
|
1
|
+
"""Map transport ``RunFacts`` and driver ``Facts`` to a classified ``Result``.
|
|
2
2
|
|
|
3
3
|
Rows follow the design's outcome precedence; the first matching row wins.
|
|
4
4
|
"""
|
|
@@ -7,15 +7,22 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
from typing import Literal
|
|
10
11
|
|
|
11
|
-
from .
|
|
12
|
+
from .driver import Facts
|
|
13
|
+
from .types import Diagnostic, ErrorCode, Outcome, Result
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
SPAWN_CODES = frozenset({"missing_cli", "spawn_failed"})
|
|
15
|
-
CLI_CODES = ("auth", "invalid_model", "context_overflow", "service_error")
|
|
15
|
+
StopCause = Literal["exited", "cancelled", "timeout", "idle_timeout"]
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
_CLI_KEYWORD_ORDER: tuple[ErrorCode, ...] = (
|
|
18
|
+
ErrorCode.AUTH,
|
|
19
|
+
ErrorCode.INVALID_MODEL,
|
|
20
|
+
ErrorCode.CONTEXT_OVERFLOW,
|
|
21
|
+
ErrorCode.SERVICE_ERROR,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_KEYWORDS: dict[ErrorCode, tuple[str, ...]] = {
|
|
25
|
+
ErrorCode.AUTH: (
|
|
19
26
|
"authentication",
|
|
20
27
|
"unauthorized",
|
|
21
28
|
"forbidden",
|
|
@@ -25,9 +32,9 @@ _KEYWORDS: dict[str, tuple[str, ...]] = {
|
|
|
25
32
|
"login required",
|
|
26
33
|
"auth_unavailable",
|
|
27
34
|
),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
ErrorCode.INVALID_MODEL: ("invalid model", "unknown model", "not a valid model"),
|
|
36
|
+
ErrorCode.CONTEXT_OVERFLOW: ("context_length_exceeded",),
|
|
37
|
+
ErrorCode.SERVICE_ERROR: (
|
|
31
38
|
"overloaded",
|
|
32
39
|
"rate limit",
|
|
33
40
|
"rate_limit",
|
|
@@ -38,133 +45,217 @@ _KEYWORDS: dict[str, tuple[str, ...]] = {
|
|
|
38
45
|
),
|
|
39
46
|
}
|
|
40
47
|
_CONTEXT_OVERFLOW_RE = re.compile(r"\bcontext_length_exceeded\b")
|
|
48
|
+
_MODEL_NOT_FOUND_RE = re.compile(r'\bmodel\s+"[^"]+"\s+not\s+found\b')
|
|
49
|
+
_HTTP_STATUS_RE = re.compile(
|
|
50
|
+
r"""\b(?:http(?:/\d(?:\.\d)?)?(?:\s+error)?|status(?:\s+code)?)\s*[:=]?\s*"?([1-5]\d{2})\b"""
|
|
51
|
+
r"""|"(?:status|status_code|statusCode)"\s*:\s*(\d{3})\b""",
|
|
52
|
+
re.IGNORECASE,
|
|
53
|
+
)
|
|
41
54
|
|
|
42
55
|
|
|
43
56
|
def _normalize(text: str) -> str:
|
|
44
57
|
return text.lower().replace("node_tls_reject_unauthorized", "")
|
|
45
58
|
|
|
46
59
|
|
|
47
|
-
def
|
|
48
|
-
|
|
60
|
+
def _http_codes_from_text(text: str) -> set[ErrorCode]:
|
|
61
|
+
codes = (http_status_code(int(m.group(1) or m.group(2))) for m in _HTTP_STATUS_RE.finditer(text))
|
|
62
|
+
return {code for code in codes if code is not None}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def keyword_code(text: str) -> ErrorCode | None:
|
|
66
|
+
"""Return the first CLI ``ErrorCode`` whose keywords appear in ``text``, in precedence order, or ``None``."""
|
|
49
67
|
lowered = _normalize(text)
|
|
50
68
|
if not lowered.strip():
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
return None
|
|
70
|
+
http_codes = _http_codes_from_text(lowered)
|
|
71
|
+
for code in _CLI_KEYWORD_ORDER:
|
|
72
|
+
if code == ErrorCode.CONTEXT_OVERFLOW:
|
|
54
73
|
if _CONTEXT_OVERFLOW_RE.search(lowered):
|
|
55
74
|
return code
|
|
56
|
-
elif
|
|
75
|
+
elif code == ErrorCode.INVALID_MODEL and _MODEL_NOT_FOUND_RE.search(lowered):
|
|
76
|
+
return code
|
|
77
|
+
elif any(token in lowered for token in _KEYWORDS[code]) or code in http_codes:
|
|
57
78
|
return code
|
|
58
|
-
return
|
|
79
|
+
return None
|
|
59
80
|
|
|
60
81
|
|
|
61
|
-
def http_status_code(status: int | None) ->
|
|
62
|
-
"""Map a CLI-reported HTTP status to
|
|
82
|
+
def http_status_code(status: int | None) -> ErrorCode | None:
|
|
83
|
+
"""Map a CLI-reported HTTP status to an ``ErrorCode``, or ``None``."""
|
|
63
84
|
if status in (401, 403):
|
|
64
|
-
return
|
|
85
|
+
return ErrorCode.AUTH
|
|
65
86
|
if status == 429 or (status is not None and 500 <= status <= 599):
|
|
66
|
-
return
|
|
67
|
-
return
|
|
87
|
+
return ErrorCode.SERVICE_ERROR
|
|
88
|
+
return None
|
|
68
89
|
|
|
69
90
|
|
|
70
|
-
@dataclass(slots=True, kw_only=True)
|
|
71
|
-
class
|
|
72
|
-
"""
|
|
91
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
92
|
+
class RunFacts:
|
|
93
|
+
"""Transport and supervision facts collected for one run."""
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
session_id: str | None = None
|
|
76
|
-
cancelled: bool = False
|
|
77
|
-
request_error: Diagnostic | None = None
|
|
78
|
-
process_error: str = ""
|
|
79
|
-
process_message: str = ""
|
|
80
|
-
cli_error: Diagnostic | None = None
|
|
81
|
-
diagnostics: str = ""
|
|
95
|
+
stop_cause: StopCause = "exited"
|
|
82
96
|
returncode: int | None = None
|
|
97
|
+
stderr_tail: str = ""
|
|
98
|
+
request_error: Diagnostic | None = None
|
|
99
|
+
spawn_error: Diagnostic | None = None
|
|
100
|
+
stdin_failed: str | None = None
|
|
101
|
+
parser_error: str | None = None
|
|
102
|
+
cleanup_incomplete: bool = False
|
|
103
|
+
line_too_long: bool = False
|
|
104
|
+
duration_s: float = 0.0
|
|
83
105
|
|
|
84
106
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
outcome: Outcome
|
|
88
|
-
error: Diagnostic | None
|
|
89
|
-
warnings: tuple[Diagnostic, ...] = ()
|
|
107
|
+
def _message(*parts: str) -> str:
|
|
108
|
+
return "\n".join(part.strip() for part in parts if part and part.strip())
|
|
90
109
|
|
|
91
110
|
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
def _cli_diagnostic(run: RunFacts, facts: Facts) -> Diagnostic | None:
|
|
112
|
+
"""Return structured ``Facts.error`` if set, else keyword-match ``Facts.error_text`` and ``stderr_tail``."""
|
|
113
|
+
if facts.error is not None:
|
|
114
|
+
return facts.error
|
|
115
|
+
combined = _message(facts.error_text, run.stderr_tail)
|
|
116
|
+
code = keyword_code(combined)
|
|
117
|
+
if code is not None:
|
|
118
|
+
return Diagnostic(code=code, message=combined)
|
|
119
|
+
return None
|
|
98
120
|
|
|
99
121
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
def _build_warnings(run: RunFacts, extra: tuple[Diagnostic, ...] = ()) -> tuple[Diagnostic, ...]:
|
|
123
|
+
warnings: list[Diagnostic] = []
|
|
124
|
+
if run.line_too_long:
|
|
125
|
+
warnings.append(
|
|
126
|
+
Diagnostic(code=ErrorCode.LINE_TOO_LONG, message="a line exceeded max_line_bytes and was discarded")
|
|
127
|
+
)
|
|
128
|
+
if run.cleanup_incomplete:
|
|
129
|
+
warnings.append(
|
|
130
|
+
Diagnostic(
|
|
131
|
+
code=ErrorCode.CLEANUP_INCOMPLETE,
|
|
132
|
+
message="process cleanup did not complete before deadline",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
warnings.extend(extra)
|
|
136
|
+
return tuple(warnings)
|
|
112
137
|
|
|
113
138
|
|
|
114
|
-
def
|
|
115
|
-
|
|
139
|
+
def _result(
|
|
140
|
+
run: RunFacts,
|
|
141
|
+
facts: Facts,
|
|
142
|
+
*,
|
|
143
|
+
text: str,
|
|
144
|
+
outcome: Outcome,
|
|
145
|
+
error: Diagnostic | None,
|
|
146
|
+
extra_warnings: tuple[Diagnostic, ...] = (),
|
|
147
|
+
) -> Result:
|
|
148
|
+
return Result(
|
|
149
|
+
outcome=outcome,
|
|
150
|
+
text=text,
|
|
151
|
+
session_id=facts.session_id or None,
|
|
152
|
+
model=facts.model,
|
|
153
|
+
usage=facts.usage,
|
|
154
|
+
cost_usd=facts.cost_usd,
|
|
155
|
+
duration_s=run.duration_s,
|
|
156
|
+
num_turns=facts.num_turns,
|
|
157
|
+
stop_reason=facts.stop_reason,
|
|
158
|
+
returncode=run.returncode,
|
|
159
|
+
error=error,
|
|
160
|
+
warnings=_build_warnings(run, extra_warnings),
|
|
161
|
+
)
|
|
116
162
|
|
|
117
163
|
|
|
118
|
-
def classify(facts: Facts) ->
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
if
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
def classify(run: RunFacts, facts: Facts, *, text: str) -> Result:
|
|
165
|
+
"""Classify one run into a ``Result`` according to the design's 12-row precedence table."""
|
|
166
|
+
result_text = facts.final_text if facts.final_text is not None else text
|
|
167
|
+
|
|
168
|
+
# Row 1: Latched stop cause is cancelled
|
|
169
|
+
if run.stop_cause == "cancelled":
|
|
170
|
+
return _result(
|
|
171
|
+
run,
|
|
172
|
+
facts,
|
|
173
|
+
text=result_text,
|
|
174
|
+
outcome=Outcome.CANCELLED,
|
|
175
|
+
error=Diagnostic(code=ErrorCode.CANCELLED, message="run cancelled"),
|
|
128
176
|
)
|
|
129
|
-
# Row
|
|
130
|
-
if
|
|
131
|
-
return
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
177
|
+
# Row 2: Validation failure or RequestError
|
|
178
|
+
if run.request_error is not None:
|
|
179
|
+
return _result(run, facts, text=result_text, outcome=Outcome.REQUEST_FATAL, error=run.request_error)
|
|
180
|
+
# Row 3: Executable missing or spawn denied
|
|
181
|
+
if run.spawn_error is not None:
|
|
182
|
+
return _result(run, facts, text=result_text, outcome=Outcome.AGENT_FATAL, error=run.spawn_error)
|
|
183
|
+
|
|
184
|
+
cli = _cli_diagnostic(run, facts)
|
|
185
|
+
|
|
186
|
+
# Row 4: Auth failure
|
|
187
|
+
if cli is not None and cli.code == ErrorCode.AUTH:
|
|
188
|
+
return _result(run, facts, text=result_text, outcome=Outcome.AGENT_FATAL, error=cli)
|
|
189
|
+
# Row 5: Invalid model or context overflow
|
|
190
|
+
if cli is not None and cli.code in (ErrorCode.INVALID_MODEL, ErrorCode.CONTEXT_OVERFLOW):
|
|
191
|
+
return _result(run, facts, text=result_text, outcome=Outcome.REQUEST_FATAL, error=cli)
|
|
192
|
+
# Row 6: Parser raised
|
|
193
|
+
if run.parser_error is not None:
|
|
194
|
+
return _result(
|
|
195
|
+
run,
|
|
196
|
+
facts,
|
|
197
|
+
text=result_text,
|
|
198
|
+
outcome=Outcome.AGENT_FATAL,
|
|
199
|
+
error=Diagnostic(code=ErrorCode.PARSER_ERROR, message=run.parser_error),
|
|
140
200
|
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
201
|
+
# Row 7: CLI reports a caller-set limit
|
|
202
|
+
if cli is not None and cli.code in (ErrorCode.MAX_TURNS, ErrorCode.BUDGET):
|
|
203
|
+
return _result(run, facts, text=result_text, outcome=Outcome.LIMIT_REACHED, error=cli)
|
|
204
|
+
# Row 8: Latched stop cause is a deadline
|
|
205
|
+
if run.stop_cause in ("timeout", "idle_timeout"):
|
|
206
|
+
code = ErrorCode.TIMEOUT if run.stop_cause == "timeout" else ErrorCode.IDLE_TIMEOUT
|
|
207
|
+
msg = "run timed out" if code == ErrorCode.TIMEOUT else "run idle-timed out"
|
|
208
|
+
return _result(
|
|
209
|
+
run,
|
|
210
|
+
facts,
|
|
211
|
+
text=result_text,
|
|
212
|
+
outcome=Outcome.RETRYABLE,
|
|
213
|
+
error=Diagnostic(code=code, message=msg),
|
|
214
|
+
)
|
|
215
|
+
# Row 9: HTTP 5xx, overload, or rate limit
|
|
216
|
+
if cli is not None and cli.code == ErrorCode.SERVICE_ERROR:
|
|
217
|
+
return _result(run, facts, text=result_text, outcome=Outcome.RETRYABLE, error=cli)
|
|
218
|
+
# Row 10: Stdin failure, nonzero exit, or structured failure
|
|
219
|
+
if run.stdin_failed is not None:
|
|
220
|
+
return _result(
|
|
221
|
+
run,
|
|
222
|
+
facts,
|
|
223
|
+
text=result_text,
|
|
224
|
+
outcome=Outcome.RETRYABLE,
|
|
225
|
+
error=Diagnostic(
|
|
226
|
+
code=ErrorCode.STDIN_FAILED, message=run.stdin_failed or "failed to write prompt to stdin"
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
if facts.error is not None:
|
|
230
|
+
return _result(run, facts, text=result_text, outcome=Outcome.RETRYABLE, error=facts.error)
|
|
231
|
+
if run.returncode:
|
|
232
|
+
return _result(
|
|
233
|
+
run,
|
|
234
|
+
facts,
|
|
235
|
+
text=result_text,
|
|
236
|
+
outcome=Outcome.RETRYABLE,
|
|
237
|
+
error=Diagnostic(
|
|
238
|
+
code=ErrorCode.EXECUTION_ERROR,
|
|
239
|
+
message=_message(f"command exited with status {run.returncode}", facts.error_text, run.stderr_tail),
|
|
155
240
|
),
|
|
156
241
|
)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
242
|
+
# Row 11: No text
|
|
243
|
+
if not result_text.strip():
|
|
244
|
+
return _result(
|
|
245
|
+
run,
|
|
246
|
+
facts,
|
|
247
|
+
text=result_text,
|
|
248
|
+
outcome=Outcome.RETRYABLE,
|
|
249
|
+
error=Diagnostic(
|
|
250
|
+
code=ErrorCode.NO_OUTPUT,
|
|
251
|
+
message=_message("the CLI produced no assistant text", facts.error_text, run.stderr_tail),
|
|
252
|
+
),
|
|
161
253
|
)
|
|
162
|
-
# Row
|
|
254
|
+
# Row 12: Otherwise
|
|
255
|
+
extra: tuple[Diagnostic, ...] = ()
|
|
163
256
|
if not facts.session_id:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
return Classification(Outcome.SUCCESS, None)
|
|
257
|
+
extra = (Diagnostic(code=ErrorCode.NO_SESSION_ID, message="the CLI reported no session ID"),)
|
|
258
|
+
return _result(run, facts, text=result_text, outcome=Outcome.SUCCESS, error=None, extra_warnings=extra)
|
|
168
259
|
|
|
169
260
|
|
|
170
|
-
__all__ = ["
|
|
261
|
+
__all__ = ["RunFacts", "StopCause", "classify", "http_status_code", "keyword_code"]
|