rlmness-cli 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.
- rlmagent/__init__.py +1 -0
- rlmagent/__main__.py +8 -0
- rlmagent/acp/__init__.py +1 -0
- rlmagent/acp/runtime.py +71 -0
- rlmagent/acp/server.py +598 -0
- rlmagent/acp/updates.py +128 -0
- rlmagent/agentnet.py +237 -0
- rlmagent/agents/__init__.py +1 -0
- rlmagent/agents/approval.py +90 -0
- rlmagent/agents/bridge.py +109 -0
- rlmagent/agents/budget.py +212 -0
- rlmagent/agents/child_handles.py +46 -0
- rlmagent/agents/child_mailbox.py +30 -0
- rlmagent/agents/kernel_api.py +156 -0
- rlmagent/agents/output.py +34 -0
- rlmagent/agents/patch.py +107 -0
- rlmagent/agents/prompts.py +156 -0
- rlmagent/agents/trace.py +290 -0
- rlmagent/agents/tree.py +682 -0
- rlmagent/agents/wasm_api.py +105 -0
- rlmagent/agents/workspace.py +357 -0
- rlmagent/autopilot.py +447 -0
- rlmagent/backend.py +946 -0
- rlmagent/backend_context.py +74 -0
- rlmagent/cli/__init__.py +5 -0
- rlmagent/cli/config_cmd.py +177 -0
- rlmagent/cli/health.py +156 -0
- rlmagent/cli/main.py +1194 -0
- rlmagent/cli/update.py +188 -0
- rlmagent/clipboard.py +63 -0
- rlmagent/config/__init__.py +37 -0
- rlmagent/config/config.py +297 -0
- rlmagent/config/loader.py +119 -0
- rlmagent/config/prompts.py +124 -0
- rlmagent/config/secure_input.py +213 -0
- rlmagent/config/store.py +275 -0
- rlmagent/context/__init__.py +47 -0
- rlmagent/context/budget.py +557 -0
- rlmagent/contract.py +311 -0
- rlmagent/contract_run.py +232 -0
- rlmagent/conversation.py +2547 -0
- rlmagent/cron.py +77 -0
- rlmagent/directives.py +893 -0
- rlmagent/discovery.py +356 -0
- rlmagent/execution_log.py +66 -0
- rlmagent/factory.py +189 -0
- rlmagent/factory_graph.py +413 -0
- rlmagent/harness/__init__.py +63 -0
- rlmagent/harness/contracts/__init__.py +1 -0
- rlmagent/harness/contracts/stream.py +129 -0
- rlmagent/harness/contracts/tooling.py +127 -0
- rlmagent/harness/contracts/transcript/__init__.py +68 -0
- rlmagent/harness/contracts/transcript/_base.py +29 -0
- rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
- rlmagent/harness/contracts/transcript/entries.py +180 -0
- rlmagent/harness/contracts/transcript/segments.py +64 -0
- rlmagent/harness/contracts/values.py +6 -0
- rlmagent/harness/driver.py +397 -0
- rlmagent/harness/engine.py +489 -0
- rlmagent/harness/mending.py +153 -0
- rlmagent/harness/provider/__init__.py +1 -0
- rlmagent/harness/provider/base.py +27 -0
- rlmagent/harness/provider/wire.py +189 -0
- rlmagent/harness/py.typed +0 -0
- rlmagent/harness/session/__init__.py +5 -0
- rlmagent/harness/session/index.py +218 -0
- rlmagent/harness/session/lease.py +66 -0
- rlmagent/harness/session/records.py +118 -0
- rlmagent/harness/session/replay.py +165 -0
- rlmagent/harness/session/store.py +155 -0
- rlmagent/harness/session/summary.py +46 -0
- rlmagent/hints.py +47 -0
- rlmagent/history.py +92 -0
- rlmagent/hooks/__init__.py +1 -0
- rlmagent/hooks/hooks.py +1 -0
- rlmagent/instructions.py +157 -0
- rlmagent/job_runner.py +117 -0
- rlmagent/jobs.py +365 -0
- rlmagent/kept_agents.py +397 -0
- rlmagent/kernel/__init__.py +5 -0
- rlmagent/kernel/session.py +205 -0
- rlmagent/kernel/wasm.py +169 -0
- rlmagent/logs.py +208 -0
- rlmagent/managed_release.py +368 -0
- rlmagent/mcp.py +411 -0
- rlmagent/mcp_oauth.py +213 -0
- rlmagent/model/__init__.py +29 -0
- rlmagent/model/_claude/__init__.py +1 -0
- rlmagent/model/_claude/courier.py +117 -0
- rlmagent/model/_claude/emitter.py +378 -0
- rlmagent/model/_oai/__init__.py +1 -0
- rlmagent/model/_oai/helpers.py +256 -0
- rlmagent/model/_oai/normalize.py +263 -0
- rlmagent/model/_oai/parsers.py +317 -0
- rlmagent/model/_oai/payloads.py +226 -0
- rlmagent/model/_oai/transport.py +127 -0
- rlmagent/model/claude.py +552 -0
- rlmagent/model/copilot.py +142 -0
- rlmagent/model/correlation.py +21 -0
- rlmagent/model/deadline.py +19 -0
- rlmagent/model/discovery.py +181 -0
- rlmagent/model/external/__init__.py +11 -0
- rlmagent/model/external/acp.py +202 -0
- rlmagent/model/external/acp_install.py +109 -0
- rlmagent/model/external/cli.py +248 -0
- rlmagent/model/external/textcalls.py +57 -0
- rlmagent/model/failover.py +129 -0
- rlmagent/model/google.py +199 -0
- rlmagent/model/limits.py +46 -0
- rlmagent/model/oai_compatible.py +414 -0
- rlmagent/model/preferences.py +56 -0
- rlmagent/model/py.typed +0 -0
- rlmagent/model/registered_oauth.py +195 -0
- rlmagent/model/scripted.py +55 -0
- rlmagent/model/settings.py +271 -0
- rlmagent/model/subscription.py +398 -0
- rlmagent/model/token_bounds.py +15 -0
- rlmagent/model/transport/__init__.py +1 -0
- rlmagent/model/transport/backoff.py +87 -0
- rlmagent/model/transport/client.py +87 -0
- rlmagent/model/transport/faults.py +65 -0
- rlmagent/model/transport/responses_socket.py +223 -0
- rlmagent/model/vertex.py +93 -0
- rlmagent/network.py +123 -0
- rlmagent/packages.py +246 -0
- rlmagent/planning.py +84 -0
- rlmagent/playbook/__init__.py +1 -0
- rlmagent/playbook/apply.py +106 -0
- rlmagent/playbook/digest.py +63 -0
- rlmagent/playbook/improver.py +272 -0
- rlmagent/playbook/plan.py +99 -0
- rlmagent/playbook/store.py +67 -0
- rlmagent/plugins.py +327 -0
- rlmagent/policy.py +105 -0
- rlmagent/prompt_refs.py +57 -0
- rlmagent/prompts.py +331 -0
- rlmagent/prune.py +60 -0
- rlmagent/py.typed +0 -0
- rlmagent/reasoning.py +135 -0
- rlmagent/refinement/__init__.py +1 -0
- rlmagent/refinement/controller.py +448 -0
- rlmagent/refinement/engine.py +220 -0
- rlmagent/refinement/evidence.py +131 -0
- rlmagent/refinement/scoped.py +90 -0
- rlmagent/refinement/store.py +193 -0
- rlmagent/refresh.py +202 -0
- rlmagent/routing.py +52 -0
- rlmagent/runtime.py +575 -0
- rlmagent/safety.py +267 -0
- rlmagent/session_html.py +57 -0
- rlmagent/session_import.py +52 -0
- rlmagent/session_share.py +55 -0
- rlmagent/sessions.py +249 -0
- rlmagent/shell_sandbox.py +88 -0
- rlmagent/skillset.py +320 -0
- rlmagent/steering.py +148 -0
- rlmagent/stops.py +62 -0
- rlmagent/subscriptions.py +219 -0
- rlmagent/tasks.py +98 -0
- rlmagent/toolhost.py +310 -0
- rlmagent/tools/__init__.py +56 -0
- rlmagent/tools/_shared.py +210 -0
- rlmagent/tools/files.py +462 -0
- rlmagent/tools/kernel.py +93 -0
- rlmagent/tools/patch_tool.py +68 -0
- rlmagent/tools/search.py +188 -0
- rlmagent/trust.py +383 -0
- rlmagent/tui/__init__.py +1 -0
- rlmagent/tui/adapter.py +506 -0
- rlmagent/tui/app.py +711 -0
- rlmagent/tui/approval_screen.py +57 -0
- rlmagent/tui/choice_screen.py +47 -0
- rlmagent/tui/completions.py +116 -0
- rlmagent/tui/components.py +25 -0
- rlmagent/tui/composer.py +40 -0
- rlmagent/tui/factory_view.py +86 -0
- rlmagent/tui/inspect.py +193 -0
- rlmagent/tui/messages.py +215 -0
- rlmagent/tui/model_screen.py +307 -0
- rlmagent/tui/prompt.py +98 -0
- rlmagent/tui/question_screen.py +52 -0
- rlmagent/tui/replay.py +141 -0
- rlmagent/tui/rlm_agent.tcss +405 -0
- rlmagent/tui/search_box.py +91 -0
- rlmagent/tui/sidebar.py +182 -0
- rlmagent/tui/status.py +49 -0
- rlmagent/tui/steps.py +283 -0
- rlmagent/tui/suggest.py +778 -0
- rlmagent/tui/transcript.py +172 -0
- rlmagent/tui/trust_dialog.py +58 -0
- rlmagent/tui/worker_attach.py +141 -0
- rlmagent/ui/__init__.py +1 -0
- rlmagent/ui/render.py +211 -0
- rlmagent/undo.py +109 -0
- rlmagent/utils/__init__.py +1 -0
- rlmagent/websearch.py +75 -0
- rlmagent/worker.py +1275 -0
- rlmagent/worker_service.py +203 -0
- rlmness_cli-0.2.0.dist-info/METADATA +882 -0
- rlmness_cli-0.2.0.dist-info/RECORD +203 -0
- rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
- rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
- rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""HTTP transport for the Anthropic Messages API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from rlmagent.harness.contracts.tooling import CancelToken
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Retriable status codes
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
_TRANSIENT_CODES = frozenset({408, 409, 425, 429})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Error type
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApiRejection(Exception):
|
|
26
|
+
"""Non-200 HTTP response from the Anthropic Messages API."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
status: int,
|
|
31
|
+
detail: str,
|
|
32
|
+
*,
|
|
33
|
+
wait_hint: float | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.status = status
|
|
36
|
+
self.detail = detail
|
|
37
|
+
self.wait_hint = wait_hint
|
|
38
|
+
super().__init__(f"HTTP {status}")
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def retriable(self) -> bool:
|
|
42
|
+
"""Return whether this status code warrants an automatic reattempt."""
|
|
43
|
+
return self.status in _TRANSIENT_CODES or self.status >= 500
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Internal helpers
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_object(text: str) -> dict[str, Any] | None:
|
|
52
|
+
try:
|
|
53
|
+
obj = json.loads(text)
|
|
54
|
+
return obj if isinstance(obj, dict) else None
|
|
55
|
+
except (json.JSONDecodeError, ValueError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _read_wait_hint(response: httpx.Response) -> float | None:
|
|
60
|
+
raw = response.headers.get("retry-after")
|
|
61
|
+
if raw is None:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
return max(0.0, float(raw))
|
|
65
|
+
except (ValueError, TypeError):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Streaming transport
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def relay_sse(
|
|
75
|
+
client: httpx.AsyncClient,
|
|
76
|
+
url: str,
|
|
77
|
+
body: dict[str, Any],
|
|
78
|
+
headers: dict[str, str],
|
|
79
|
+
*,
|
|
80
|
+
signal: CancelToken | None = None,
|
|
81
|
+
) -> AsyncIterator[tuple[str, dict[str, Any]]]:
|
|
82
|
+
"""POST *body* as streaming JSON and yield ``(event_type, payload)`` pairs."""
|
|
83
|
+
async with client.stream("POST", url, json=body, headers=headers) as response:
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
raw = (await response.aread()).decode("utf-8", errors="replace")
|
|
86
|
+
raise ApiRejection(
|
|
87
|
+
response.status_code,
|
|
88
|
+
raw,
|
|
89
|
+
wait_hint=_read_wait_hint(response),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
event_name = ""
|
|
93
|
+
data_parts: list[str] = []
|
|
94
|
+
|
|
95
|
+
async for raw_line in response.aiter_lines():
|
|
96
|
+
if signal is not None and signal.is_cancelled():
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
line = raw_line.rstrip("\r\n")
|
|
100
|
+
|
|
101
|
+
if not line:
|
|
102
|
+
# Empty line fires the accumulated event
|
|
103
|
+
if data_parts:
|
|
104
|
+
payload = _parse_object("\n".join(data_parts))
|
|
105
|
+
if payload is not None:
|
|
106
|
+
yield (event_name, payload)
|
|
107
|
+
event_name = ""
|
|
108
|
+
data_parts = []
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if line.startswith(":"):
|
|
112
|
+
continue # comment / keep-alive
|
|
113
|
+
|
|
114
|
+
if line.startswith("event:"):
|
|
115
|
+
event_name = line[6:].strip()
|
|
116
|
+
elif line.startswith("data:"):
|
|
117
|
+
data_parts.append(line[5:].lstrip())
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""State-machine parser that converts Anthropic SSE events into rlm-agent wire events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum, auto
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rlmagent.harness.contracts.transcript import (
|
|
11
|
+
CallBlock,
|
|
12
|
+
ModelEntry,
|
|
13
|
+
ReplyContent,
|
|
14
|
+
TextSegment,
|
|
15
|
+
ThoughtSegment,
|
|
16
|
+
UsageStats,
|
|
17
|
+
)
|
|
18
|
+
from rlmagent.harness.provider.wire import (
|
|
19
|
+
CallChunkEvent,
|
|
20
|
+
CallCloseEvent,
|
|
21
|
+
CallOpenEvent,
|
|
22
|
+
CompletionCause,
|
|
23
|
+
ContentChunkEvent,
|
|
24
|
+
ContentCloseEvent,
|
|
25
|
+
ContentOpenEvent,
|
|
26
|
+
FaultCause,
|
|
27
|
+
ReasoningChunkEvent,
|
|
28
|
+
ReasoningCloseEvent,
|
|
29
|
+
ReasoningOpenEvent,
|
|
30
|
+
StreamCloseEvent,
|
|
31
|
+
StreamFaultEvent,
|
|
32
|
+
StreamOpenEvent,
|
|
33
|
+
WireEvent,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Phase enum
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Phase(Enum):
|
|
42
|
+
"""Lifecycle phase of the streaming response."""
|
|
43
|
+
|
|
44
|
+
PENDING = auto()
|
|
45
|
+
ACTIVE = auto()
|
|
46
|
+
SEALED = auto()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Content accumulator
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(slots=True)
|
|
55
|
+
class _ContentAccumulator:
|
|
56
|
+
"""Mutable tracker for one in-progress content block."""
|
|
57
|
+
|
|
58
|
+
slot: int
|
|
59
|
+
variety: str # "text" | "thinking" | "tool_use"
|
|
60
|
+
chunks: list[str] = field(default_factory=list)
|
|
61
|
+
tool_id: str = ""
|
|
62
|
+
tool_name: str = ""
|
|
63
|
+
# Opaque proof the API attaches to thinking (or the whole payload of a
|
|
64
|
+
# redacted block). It must be sent back verbatim on the next request.
|
|
65
|
+
signature: str | None = None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def assembled(self) -> str:
|
|
69
|
+
"""Return the concatenated content gathered so far."""
|
|
70
|
+
return "".join(self.chunks)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Helpers
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _map_stop_reason(reason: str) -> str:
|
|
79
|
+
"""Translate an Anthropic stop reason to rlm-agent's halt vocabulary."""
|
|
80
|
+
match reason:
|
|
81
|
+
case "end_turn" | "stop_sequence":
|
|
82
|
+
return "stop"
|
|
83
|
+
case "max_tokens":
|
|
84
|
+
return "length"
|
|
85
|
+
case "tool_use":
|
|
86
|
+
return "toolUse"
|
|
87
|
+
case _:
|
|
88
|
+
return "stop"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _safe_json_args(text: str) -> dict[str, Any]:
|
|
92
|
+
"""Parse tool-call input JSON, falling back to an empty dict."""
|
|
93
|
+
if not text or not text.strip():
|
|
94
|
+
return {}
|
|
95
|
+
try:
|
|
96
|
+
obj = json.loads(text)
|
|
97
|
+
return obj if isinstance(obj, dict) else {}
|
|
98
|
+
except (json.JSONDecodeError, ValueError):
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _tally_usage(
|
|
103
|
+
*,
|
|
104
|
+
input_tokens: int = 0,
|
|
105
|
+
output_tokens: int = 0,
|
|
106
|
+
cache_read: int = 0,
|
|
107
|
+
cache_write: int = 0,
|
|
108
|
+
) -> UsageStats:
|
|
109
|
+
"""Build a ``UsageStats`` from Anthropic token counts."""
|
|
110
|
+
return UsageStats(
|
|
111
|
+
total_tokens=input_tokens + output_tokens,
|
|
112
|
+
input=input_tokens,
|
|
113
|
+
output=output_tokens,
|
|
114
|
+
cache_read=cache_read,
|
|
115
|
+
cache_write=cache_write,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# State machine
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ResponseMachine:
|
|
125
|
+
"""Stateful parser that ingests Anthropic SSE events and emits wire events."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, *, model: str, provider: str) -> None:
|
|
128
|
+
self._model = model
|
|
129
|
+
self._provider = provider
|
|
130
|
+
self._phase = Phase.PENDING
|
|
131
|
+
|
|
132
|
+
# Response-level metadata
|
|
133
|
+
self._response_id: str | None = None
|
|
134
|
+
self._response_model: str | None = None
|
|
135
|
+
self._halt: str = "stop"
|
|
136
|
+
|
|
137
|
+
# Token accounting
|
|
138
|
+
self._input_tokens = 0
|
|
139
|
+
self._output_tokens = 0
|
|
140
|
+
self._cache_read = 0
|
|
141
|
+
self._cache_write = 0
|
|
142
|
+
|
|
143
|
+
# Completed content blocks
|
|
144
|
+
self._thoughts: list[ThoughtSegment] = []
|
|
145
|
+
self._texts: list[TextSegment] = []
|
|
146
|
+
self._calls: list[CallBlock] = []
|
|
147
|
+
|
|
148
|
+
# Active block (at most one at a time)
|
|
149
|
+
self._accumulator: _ContentAccumulator | None = None
|
|
150
|
+
|
|
151
|
+
# --- snapshot -----------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def _snapshot(self) -> ModelEntry:
|
|
154
|
+
"""Build a ``ModelEntry`` reflecting the current accumulated state."""
|
|
155
|
+
blocks: list[ReplyContent] = []
|
|
156
|
+
blocks.extend(self._thoughts)
|
|
157
|
+
blocks.extend(self._texts)
|
|
158
|
+
blocks.extend(self._calls)
|
|
159
|
+
|
|
160
|
+
# Include partial content from the active block
|
|
161
|
+
if self._accumulator is not None:
|
|
162
|
+
body = self._accumulator.assembled
|
|
163
|
+
if self._accumulator.variety == "thinking" and body:
|
|
164
|
+
blocks.append(ThoughtSegment(thinking=body))
|
|
165
|
+
elif self._accumulator.variety == "text" and body:
|
|
166
|
+
blocks.append(TextSegment(text=body))
|
|
167
|
+
|
|
168
|
+
return ModelEntry(
|
|
169
|
+
model=self._response_model or self._model,
|
|
170
|
+
provider=self._provider,
|
|
171
|
+
api="messages",
|
|
172
|
+
response_id=self._response_id,
|
|
173
|
+
response_model=self._response_model,
|
|
174
|
+
content=blocks,
|
|
175
|
+
stop_reason=self._halt, # type: ignore[arg-type]
|
|
176
|
+
usage=_tally_usage(
|
|
177
|
+
input_tokens=self._input_tokens,
|
|
178
|
+
output_tokens=self._output_tokens,
|
|
179
|
+
cache_read=self._cache_read,
|
|
180
|
+
cache_write=self._cache_write,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# --- public interface ---------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def ingest(self, kind: str, data: dict[str, Any]) -> list[WireEvent]:
|
|
187
|
+
"""Process one SSE event and return any resulting wire events."""
|
|
188
|
+
match kind:
|
|
189
|
+
case "message_start":
|
|
190
|
+
return self._on_message_start(data)
|
|
191
|
+
case "content_block_start":
|
|
192
|
+
return self._on_block_open(data)
|
|
193
|
+
case "content_block_delta":
|
|
194
|
+
return self._on_block_delta(data)
|
|
195
|
+
case "content_block_stop":
|
|
196
|
+
return self._on_block_close(data)
|
|
197
|
+
case "message_delta":
|
|
198
|
+
return self._on_message_delta(data)
|
|
199
|
+
case "message_stop":
|
|
200
|
+
return self.seal()
|
|
201
|
+
case "ping":
|
|
202
|
+
return []
|
|
203
|
+
case "error":
|
|
204
|
+
return self._on_error(data)
|
|
205
|
+
case _:
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
def seal(self) -> list[WireEvent]:
|
|
209
|
+
"""Emit the terminal event if the machine has not already been sealed."""
|
|
210
|
+
if self._phase == Phase.SEALED:
|
|
211
|
+
return []
|
|
212
|
+
self._phase = Phase.SEALED
|
|
213
|
+
|
|
214
|
+
message = self._snapshot()
|
|
215
|
+
|
|
216
|
+
if self._halt in ("error", "aborted"):
|
|
217
|
+
cause: FaultCause = "aborted" if self._halt == "aborted" else "error"
|
|
218
|
+
return [StreamFaultEvent(reason=cause, error=message)]
|
|
219
|
+
|
|
220
|
+
reason: CompletionCause = "stop"
|
|
221
|
+
if self._halt == "length":
|
|
222
|
+
reason = "length"
|
|
223
|
+
elif self._halt == "toolUse":
|
|
224
|
+
reason = "toolUse"
|
|
225
|
+
return [StreamCloseEvent(reason=reason, message=message)]
|
|
226
|
+
|
|
227
|
+
# --- event handlers -----------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def _on_message_start(self, data: dict[str, Any]) -> list[WireEvent]:
|
|
230
|
+
msg = data.get("message", {})
|
|
231
|
+
self._response_id = msg.get("id")
|
|
232
|
+
self._response_model = msg.get("model")
|
|
233
|
+
|
|
234
|
+
usage = msg.get("usage", {})
|
|
235
|
+
self._input_tokens = usage.get("input_tokens", 0)
|
|
236
|
+
self._cache_read = usage.get("cache_read_input_tokens", 0)
|
|
237
|
+
self._cache_write = usage.get("cache_creation_input_tokens", 0)
|
|
238
|
+
|
|
239
|
+
self._phase = Phase.ACTIVE
|
|
240
|
+
return [StreamOpenEvent(partial=self._snapshot())]
|
|
241
|
+
|
|
242
|
+
def _on_block_open(self, data: dict[str, Any]) -> list[WireEvent]:
|
|
243
|
+
block = data.get("content_block", {})
|
|
244
|
+
slot = data.get("index", 0)
|
|
245
|
+
variety = block.get("type", "text")
|
|
246
|
+
|
|
247
|
+
if variety == "tool_use":
|
|
248
|
+
self._accumulator = _ContentAccumulator(
|
|
249
|
+
slot=slot,
|
|
250
|
+
variety="tool_use",
|
|
251
|
+
tool_id=block.get("id", ""),
|
|
252
|
+
tool_name=block.get("name", ""),
|
|
253
|
+
)
|
|
254
|
+
return [CallOpenEvent(content_index=slot, partial=self._snapshot())]
|
|
255
|
+
|
|
256
|
+
if variety == "thinking":
|
|
257
|
+
self._accumulator = _ContentAccumulator(
|
|
258
|
+
slot=slot, variety="thinking", signature=block.get("signature") or None,
|
|
259
|
+
)
|
|
260
|
+
return [ReasoningOpenEvent(content_index=slot, partial=self._snapshot())]
|
|
261
|
+
|
|
262
|
+
if variety == "redacted_thinking":
|
|
263
|
+
self._accumulator = _ContentAccumulator(
|
|
264
|
+
slot=slot, variety="redacted_thinking", signature=block.get("data") or None,
|
|
265
|
+
)
|
|
266
|
+
return [ReasoningOpenEvent(content_index=slot, partial=self._snapshot())]
|
|
267
|
+
|
|
268
|
+
# Default: text
|
|
269
|
+
self._accumulator = _ContentAccumulator(slot=slot, variety="text")
|
|
270
|
+
return [ContentOpenEvent(content_index=slot, partial=self._snapshot())]
|
|
271
|
+
|
|
272
|
+
def _on_block_delta(self, data: dict[str, Any]) -> list[WireEvent]:
|
|
273
|
+
if self._accumulator is None:
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
delta = data.get("delta", {})
|
|
277
|
+
delta_type = delta.get("type", "")
|
|
278
|
+
slot = data.get("index", self._accumulator.slot)
|
|
279
|
+
|
|
280
|
+
if delta_type == "text_delta":
|
|
281
|
+
fragment = delta.get("text", "")
|
|
282
|
+
if fragment:
|
|
283
|
+
self._accumulator.chunks.append(fragment)
|
|
284
|
+
return [ContentChunkEvent(
|
|
285
|
+
content_index=slot, delta=fragment, partial=self._snapshot(),
|
|
286
|
+
)]
|
|
287
|
+
|
|
288
|
+
elif delta_type == "thinking_delta":
|
|
289
|
+
fragment = delta.get("thinking", "")
|
|
290
|
+
if fragment:
|
|
291
|
+
self._accumulator.chunks.append(fragment)
|
|
292
|
+
return [ReasoningChunkEvent(
|
|
293
|
+
content_index=slot, delta=fragment, partial=self._snapshot(),
|
|
294
|
+
)]
|
|
295
|
+
|
|
296
|
+
elif delta_type == "signature_delta":
|
|
297
|
+
self._accumulator.signature = (
|
|
298
|
+
self._accumulator.signature or ""
|
|
299
|
+
) + delta.get("signature", "")
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
elif delta_type == "input_json_delta":
|
|
303
|
+
fragment = delta.get("partial_json", "")
|
|
304
|
+
if fragment:
|
|
305
|
+
self._accumulator.chunks.append(fragment)
|
|
306
|
+
return [CallChunkEvent(
|
|
307
|
+
content_index=slot, delta=fragment, partial=self._snapshot(),
|
|
308
|
+
)]
|
|
309
|
+
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
def _on_block_close(self, data: dict[str, Any]) -> list[WireEvent]:
|
|
313
|
+
acc = self._accumulator
|
|
314
|
+
if acc is None:
|
|
315
|
+
return []
|
|
316
|
+
self._accumulator = None
|
|
317
|
+
slot = data.get("index", acc.slot)
|
|
318
|
+
body = acc.assembled
|
|
319
|
+
|
|
320
|
+
if acc.variety == "text":
|
|
321
|
+
self._texts.append(TextSegment(text=body))
|
|
322
|
+
return [ContentCloseEvent(
|
|
323
|
+
content_index=slot, content=body, partial=self._snapshot(),
|
|
324
|
+
)]
|
|
325
|
+
|
|
326
|
+
if acc.variety in ("thinking", "redacted_thinking"):
|
|
327
|
+
self._thoughts.append(ThoughtSegment(
|
|
328
|
+
thinking=body,
|
|
329
|
+
redacted=acc.variety == "redacted_thinking",
|
|
330
|
+
thinking_signature=acc.signature,
|
|
331
|
+
))
|
|
332
|
+
return [ReasoningCloseEvent(
|
|
333
|
+
content_index=slot, content=body, partial=self._snapshot(),
|
|
334
|
+
)]
|
|
335
|
+
|
|
336
|
+
if acc.variety == "tool_use":
|
|
337
|
+
args = _safe_json_args(body)
|
|
338
|
+
call = CallBlock(id=acc.tool_id, name=acc.tool_name, arguments=args)
|
|
339
|
+
self._calls.append(call)
|
|
340
|
+
return [CallCloseEvent(
|
|
341
|
+
content_index=slot, tool_call=call, partial=self._snapshot(),
|
|
342
|
+
)]
|
|
343
|
+
|
|
344
|
+
return []
|
|
345
|
+
|
|
346
|
+
def _on_message_delta(self, data: dict[str, Any]) -> list[WireEvent]:
|
|
347
|
+
delta = data.get("delta", {})
|
|
348
|
+
stop = delta.get("stop_reason")
|
|
349
|
+
if stop:
|
|
350
|
+
self._halt = _map_stop_reason(stop)
|
|
351
|
+
|
|
352
|
+
usage = data.get("usage", {})
|
|
353
|
+
out = usage.get("output_tokens")
|
|
354
|
+
if out is not None:
|
|
355
|
+
self._output_tokens = int(out)
|
|
356
|
+
return []
|
|
357
|
+
|
|
358
|
+
def _on_error(self, data: dict[str, Any]) -> list[WireEvent]:
|
|
359
|
+
was_pending = self._phase == Phase.PENDING
|
|
360
|
+
self._phase = Phase.SEALED
|
|
361
|
+
self._halt = "error"
|
|
362
|
+
|
|
363
|
+
error_obj = data.get("error", {})
|
|
364
|
+
msg = error_obj.get("message", str(data))
|
|
365
|
+
entry = ModelEntry(
|
|
366
|
+
model=self._response_model or self._model,
|
|
367
|
+
provider=self._provider,
|
|
368
|
+
api="messages",
|
|
369
|
+
content=[],
|
|
370
|
+
stop_reason="error",
|
|
371
|
+
error_message=msg,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
events: list[WireEvent] = []
|
|
375
|
+
if was_pending:
|
|
376
|
+
events.append(StreamOpenEvent(partial=entry))
|
|
377
|
+
events.append(StreamFaultEvent(reason="error", error=entry))
|
|
378
|
+
return events
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal implementation package for the OpenAI-compatible provider adapter."""
|