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,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal, Protocol
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
|
|
9
|
+
from rlmagent.harness.contracts.transcript import (
|
|
10
|
+
ImageSegment,
|
|
11
|
+
StrictModel,
|
|
12
|
+
TextSegment,
|
|
13
|
+
)
|
|
14
|
+
from rlmagent.harness.contracts.values import JValue
|
|
15
|
+
|
|
16
|
+
Parallelism = Literal["parallel", "sequential"]
|
|
17
|
+
InputShaper = Callable[[object], Mapping[str, JValue]]
|
|
18
|
+
ProgressNotifier = Callable[["ToolOutcome"], None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CancelToken(Protocol):
|
|
22
|
+
def is_cancelled(self) -> bool: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvocationPrinter(Protocol):
|
|
26
|
+
def __call__(
|
|
27
|
+
self,
|
|
28
|
+
arguments: Mapping[str, JValue],
|
|
29
|
+
) -> str | None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OutcomePresenter(Protocol):
|
|
33
|
+
def __call__(
|
|
34
|
+
self,
|
|
35
|
+
result: ToolOutcome,
|
|
36
|
+
*,
|
|
37
|
+
expanded: bool,
|
|
38
|
+
) -> str | None: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RunHandler(Protocol):
|
|
42
|
+
def __call__(
|
|
43
|
+
self,
|
|
44
|
+
tool_call_id: str,
|
|
45
|
+
arguments: Mapping[str, JValue],
|
|
46
|
+
signal: CancelToken | None = None,
|
|
47
|
+
on_update: ProgressNotifier | None = None,
|
|
48
|
+
) -> Awaitable[ToolOutcome]: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ToolOutcome(StrictModel):
|
|
52
|
+
content: list[TextSegment | ImageSegment] = Field(default_factory=list)
|
|
53
|
+
details: JValue = None
|
|
54
|
+
terminate: bool | None = None
|
|
55
|
+
added_tool_names: list[str] | None = None
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def coerce_content(cls, value):
|
|
60
|
+
if not isinstance(value, dict):
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
data = dict(value)
|
|
64
|
+
|
|
65
|
+
if isinstance(data.get("content"), str):
|
|
66
|
+
text = data["content"]
|
|
67
|
+
data["content"] = [TextSegment(text=text)] if text else []
|
|
68
|
+
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def text(self) -> str:
|
|
73
|
+
return "".join(
|
|
74
|
+
block.text
|
|
75
|
+
for block in self.content
|
|
76
|
+
if isinstance(block, TextSegment)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(slots=True, frozen=True)
|
|
81
|
+
class ToolSpec:
|
|
82
|
+
name: str
|
|
83
|
+
label: str
|
|
84
|
+
description: str
|
|
85
|
+
parameters: Mapping[str, JValue]
|
|
86
|
+
run: RunHandler
|
|
87
|
+
|
|
88
|
+
parallelism: Parallelism = "parallel"
|
|
89
|
+
|
|
90
|
+
prompt_snippet: str | None = None
|
|
91
|
+
prompt_guidelines: tuple[str, ...] = ()
|
|
92
|
+
|
|
93
|
+
input_shaper: InputShaper | None = None
|
|
94
|
+
|
|
95
|
+
format_call: InvocationPrinter | None = None
|
|
96
|
+
format_result: OutcomePresenter | None = None
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def input_schema(self) -> Mapping[str, JValue]:
|
|
100
|
+
return self.parameters
|
|
101
|
+
|
|
102
|
+
async def execute(
|
|
103
|
+
self,
|
|
104
|
+
tool_call_id: str,
|
|
105
|
+
arguments: Mapping[str, JValue],
|
|
106
|
+
signal: CancelToken | None = None,
|
|
107
|
+
on_update: ProgressNotifier | None = None,
|
|
108
|
+
) -> ToolOutcome:
|
|
109
|
+
return await self.run(
|
|
110
|
+
tool_call_id,
|
|
111
|
+
arguments,
|
|
112
|
+
signal,
|
|
113
|
+
on_update,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"CancelToken",
|
|
119
|
+
"InputShaper",
|
|
120
|
+
"InvocationPrinter",
|
|
121
|
+
"OutcomePresenter",
|
|
122
|
+
"Parallelism",
|
|
123
|
+
"ProgressNotifier",
|
|
124
|
+
"RunHandler",
|
|
125
|
+
"ToolOutcome",
|
|
126
|
+
"ToolSpec",
|
|
127
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Provider-neutral content and transcript message models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rlmagent.harness.contracts.transcript._base import StrictModel, now_ms
|
|
6
|
+
from rlmagent.harness.contracts.transcript.diagnostics import (
|
|
7
|
+
CostBreakdown,
|
|
8
|
+
FaultInfo,
|
|
9
|
+
HaltReason,
|
|
10
|
+
TurnDiagnostic,
|
|
11
|
+
UsageStats,
|
|
12
|
+
)
|
|
13
|
+
from rlmagent.harness.contracts.transcript.entries import (
|
|
14
|
+
ExtensionEntry,
|
|
15
|
+
ForkSummaryEntry,
|
|
16
|
+
HumanEntry,
|
|
17
|
+
ModelEntry,
|
|
18
|
+
PruneSummaryEntry,
|
|
19
|
+
ShellResultEntry,
|
|
20
|
+
ToolOutcomeEntry,
|
|
21
|
+
TranscriptEntry,
|
|
22
|
+
entry_to_human,
|
|
23
|
+
surface_text,
|
|
24
|
+
)
|
|
25
|
+
from rlmagent.harness.contracts.transcript.segments import (
|
|
26
|
+
CallBlock,
|
|
27
|
+
ImageSegment,
|
|
28
|
+
InputContent,
|
|
29
|
+
ReplyContent,
|
|
30
|
+
ResultContent,
|
|
31
|
+
TextSegment,
|
|
32
|
+
ThoughtSegment,
|
|
33
|
+
build_model_blocks,
|
|
34
|
+
gather_text,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# base
|
|
39
|
+
"StrictModel",
|
|
40
|
+
"now_ms",
|
|
41
|
+
# segments
|
|
42
|
+
"CallBlock",
|
|
43
|
+
"ImageSegment",
|
|
44
|
+
"InputContent",
|
|
45
|
+
"ReplyContent",
|
|
46
|
+
"ResultContent",
|
|
47
|
+
"TextSegment",
|
|
48
|
+
"ThoughtSegment",
|
|
49
|
+
"build_model_blocks",
|
|
50
|
+
"gather_text",
|
|
51
|
+
# diagnostics
|
|
52
|
+
"CostBreakdown",
|
|
53
|
+
"FaultInfo",
|
|
54
|
+
"HaltReason",
|
|
55
|
+
"TurnDiagnostic",
|
|
56
|
+
"UsageStats",
|
|
57
|
+
# entries
|
|
58
|
+
"ExtensionEntry",
|
|
59
|
+
"ForkSummaryEntry",
|
|
60
|
+
"HumanEntry",
|
|
61
|
+
"ModelEntry",
|
|
62
|
+
"PruneSummaryEntry",
|
|
63
|
+
"ShellResultEntry",
|
|
64
|
+
"ToolOutcomeEntry",
|
|
65
|
+
"TranscriptEntry",
|
|
66
|
+
"entry_to_human",
|
|
67
|
+
"surface_text",
|
|
68
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Base model config and timestamp helpers for transcript types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from time import time
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _camelize(name: str) -> str:
|
|
11
|
+
parts = name.split("_")
|
|
12
|
+
return parts[0] + "".join(part.title() for part in parts[1:])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def now_ms() -> int:
|
|
16
|
+
"""Return the current Unix timestamp in milliseconds."""
|
|
17
|
+
return int(time() * 1000)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StrictModel(BaseModel):
|
|
21
|
+
"""Strict model with Python field names and wire-compatible JSON aliases."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(
|
|
24
|
+
extra="forbid",
|
|
25
|
+
populate_by_name=True,
|
|
26
|
+
validate_by_name=True,
|
|
27
|
+
serialize_by_alias=True,
|
|
28
|
+
alias_generator=_camelize,
|
|
29
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Usage, cost, and diagnostic metadata for assistant responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from rlmagent.harness.contracts.transcript._base import StrictModel, now_ms
|
|
10
|
+
from rlmagent.harness.contracts.values import JValue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CostBreakdown(StrictModel):
|
|
14
|
+
|
|
15
|
+
total: float = 0.0
|
|
16
|
+
input: float = 0.0
|
|
17
|
+
output: float = 0.0
|
|
18
|
+
cache_read: float = 0.0
|
|
19
|
+
cache_write: float = 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UsageStats(StrictModel):
|
|
23
|
+
|
|
24
|
+
total_tokens: int = 0
|
|
25
|
+
input: int = 0
|
|
26
|
+
output: int = 0
|
|
27
|
+
cache_read: int = 0
|
|
28
|
+
cache_write: int = 0
|
|
29
|
+
cache_write_1h: int | None = None
|
|
30
|
+
reasoning: int | None = None
|
|
31
|
+
cost: CostBreakdown = CostBreakdown()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FaultInfo(StrictModel):
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
message: str
|
|
38
|
+
code: str | int | None = None
|
|
39
|
+
name: str | None = None
|
|
40
|
+
stack: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TurnDiagnostic(StrictModel):
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
type: str
|
|
47
|
+
error: FaultInfo | None = None
|
|
48
|
+
details: dict[str, JValue] | None = None
|
|
49
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
HaltReason = Literal["stop", "length", "toolUse", "error", "aborted"]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Transcript entry types and the discriminated TranscriptEntry union."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
|
|
9
|
+
from rlmagent.harness.contracts.transcript._base import StrictModel, now_ms
|
|
10
|
+
from rlmagent.harness.contracts.transcript.diagnostics import HaltReason, TurnDiagnostic, UsageStats
|
|
11
|
+
from rlmagent.harness.contracts.transcript.segments import (
|
|
12
|
+
CallBlock,
|
|
13
|
+
InputContent,
|
|
14
|
+
ReplyContent,
|
|
15
|
+
ResultContent,
|
|
16
|
+
TextSegment,
|
|
17
|
+
ThoughtSegment,
|
|
18
|
+
gather_text,
|
|
19
|
+
)
|
|
20
|
+
from rlmagent.harness.contracts.values import JValue
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HumanEntry(StrictModel):
|
|
24
|
+
"""User-authored transcript entry."""
|
|
25
|
+
|
|
26
|
+
role: Literal["user"] = "user"
|
|
27
|
+
content: InputContent
|
|
28
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def text(self) -> str:
|
|
32
|
+
return gather_text(self.content)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelEntry(StrictModel):
|
|
36
|
+
"""An assistant message with ordered content blocks."""
|
|
37
|
+
|
|
38
|
+
role: Literal["assistant"] = "assistant"
|
|
39
|
+
content: list[ReplyContent] = Field(default_factory=list)
|
|
40
|
+
model: str = "unknown"
|
|
41
|
+
provider: str = "unknown"
|
|
42
|
+
api: str = "unknown"
|
|
43
|
+
response_model: str | None = None
|
|
44
|
+
response_id: str | None = None
|
|
45
|
+
stop_reason: HaltReason = "stop"
|
|
46
|
+
usage: UsageStats = UsageStats()
|
|
47
|
+
error_message: str | None = None
|
|
48
|
+
diagnostics: list[TurnDiagnostic] | None = None
|
|
49
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="before")
|
|
52
|
+
@classmethod
|
|
53
|
+
def _coerce_str_blocks(cls, value: object) -> object:
|
|
54
|
+
if not isinstance(value, dict):
|
|
55
|
+
return value
|
|
56
|
+
data = dict(value)
|
|
57
|
+
content = data.get("content")
|
|
58
|
+
if isinstance(content, str):
|
|
59
|
+
data["content"] = [TextSegment(text=content)] if content else []
|
|
60
|
+
usage = data.get("usage")
|
|
61
|
+
if usage is None:
|
|
62
|
+
data["usage"] = UsageStats()
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def text(self) -> str:
|
|
67
|
+
return "".join(block.text for block in self.content if isinstance(block, TextSegment))
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def tool_calls(self) -> tuple[CallBlock, ...]:
|
|
71
|
+
return tuple(block for block in self.content if isinstance(block, CallBlock))
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def thinking_text(self) -> str:
|
|
75
|
+
return "".join(
|
|
76
|
+
block.thinking for block in self.content if isinstance(block, ThoughtSegment)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ToolOutcomeEntry(StrictModel):
|
|
81
|
+
"""Result of a single tool invocation."""
|
|
82
|
+
|
|
83
|
+
role: Literal["toolResult"] = "toolResult"
|
|
84
|
+
tool_call_id: str
|
|
85
|
+
tool_name: str
|
|
86
|
+
content: list[ResultContent] = Field(default_factory=list)
|
|
87
|
+
is_error: bool = False
|
|
88
|
+
details: JValue = None
|
|
89
|
+
added_tool_names: list[str] | None = None
|
|
90
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="before")
|
|
93
|
+
@classmethod
|
|
94
|
+
def _coerce_str_blocks(cls, value: object) -> object:
|
|
95
|
+
if not isinstance(value, dict):
|
|
96
|
+
return value
|
|
97
|
+
data = dict(value)
|
|
98
|
+
content = data.get("content")
|
|
99
|
+
if isinstance(content, str):
|
|
100
|
+
data["content"] = [TextSegment(text=content)] if content else []
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def text(self) -> str:
|
|
105
|
+
return gather_text(self.content)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ShellResultEntry(StrictModel):
|
|
109
|
+
"""Captured output from a shell command execution."""
|
|
110
|
+
|
|
111
|
+
role: Literal["bashExecution"] = "bashExecution"
|
|
112
|
+
command: str
|
|
113
|
+
output: str
|
|
114
|
+
exit_code: int | None = None
|
|
115
|
+
truncated: bool = False
|
|
116
|
+
cancelled: bool = False
|
|
117
|
+
full_output_path: str | None = None
|
|
118
|
+
exclude_from_context: bool = False
|
|
119
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ExtensionEntry(StrictModel):
|
|
123
|
+
"""Application-defined custom transcript entry."""
|
|
124
|
+
|
|
125
|
+
role: Literal["custom"] = "custom"
|
|
126
|
+
custom_type: str
|
|
127
|
+
content: InputContent
|
|
128
|
+
details: JValue = None
|
|
129
|
+
display: bool = True
|
|
130
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def text(self) -> str:
|
|
134
|
+
return gather_text(self.content)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PruneSummaryEntry(StrictModel):
|
|
138
|
+
"""Summary produced when compacting conversation history."""
|
|
139
|
+
|
|
140
|
+
role: Literal["compactionSummary"] = "compactionSummary"
|
|
141
|
+
summary: str
|
|
142
|
+
tokens_before: int
|
|
143
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ForkSummaryEntry(StrictModel):
|
|
147
|
+
"""Summary produced when branching a conversation."""
|
|
148
|
+
|
|
149
|
+
role: Literal["branchSummary"] = "branchSummary"
|
|
150
|
+
summary: str
|
|
151
|
+
from_id: str
|
|
152
|
+
timestamp: int = Field(default_factory=now_ms)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
type TranscriptEntry = Annotated[
|
|
156
|
+
HumanEntry
|
|
157
|
+
| ModelEntry
|
|
158
|
+
| ToolOutcomeEntry
|
|
159
|
+
| ShellResultEntry
|
|
160
|
+
| ExtensionEntry
|
|
161
|
+
| PruneSummaryEntry
|
|
162
|
+
| ForkSummaryEntry,
|
|
163
|
+
Field(discriminator="role"),
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def surface_text(message: TranscriptEntry) -> str:
|
|
168
|
+
"""Return the user-visible text of a transcript entry."""
|
|
169
|
+
if isinstance(message, (HumanEntry, ModelEntry, ToolOutcomeEntry, ExtensionEntry)):
|
|
170
|
+
return message.text
|
|
171
|
+
if isinstance(message, (ForkSummaryEntry, PruneSummaryEntry)):
|
|
172
|
+
return message.summary
|
|
173
|
+
if isinstance(message, ShellResultEntry):
|
|
174
|
+
return message.output
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def entry_to_human(message: TranscriptEntry) -> HumanEntry:
|
|
179
|
+
"""Convert custom/session-only messages to provider-compatible user context."""
|
|
180
|
+
return HumanEntry(content=surface_text(message), timestamp=message.timestamp)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Content segments and blocks that make up transcript entries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from rlmagent.harness.contracts.transcript._base import StrictModel
|
|
10
|
+
from rlmagent.harness.contracts.values import JValue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextSegment(StrictModel):
|
|
14
|
+
|
|
15
|
+
type: Literal["text"] = "text"
|
|
16
|
+
text: str
|
|
17
|
+
text_signature: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ImageSegment(StrictModel):
|
|
21
|
+
|
|
22
|
+
type: Literal["image"] = "image"
|
|
23
|
+
mime_type: str
|
|
24
|
+
data: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ThoughtSegment(StrictModel):
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
type: Literal["thinking"] = "thinking"
|
|
31
|
+
thinking: str
|
|
32
|
+
redacted: bool = False
|
|
33
|
+
thinking_signature: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CallBlock(StrictModel):
|
|
37
|
+
|
|
38
|
+
type: Literal["toolCall"] = "toolCall"
|
|
39
|
+
name: str
|
|
40
|
+
id: str
|
|
41
|
+
arguments: dict[str, JValue] = Field(default_factory=dict)
|
|
42
|
+
thought_signature: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
type InputContent = str | list[TextSegment | ImageSegment]
|
|
46
|
+
type ReplyContent = TextSegment | ThoughtSegment | CallBlock
|
|
47
|
+
type ResultContent = TextSegment | ImageSegment
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def gather_text(content: str | list[Any]) -> str:
|
|
51
|
+
"""Return visible text from string or text/image content."""
|
|
52
|
+
if isinstance(content, str):
|
|
53
|
+
return content
|
|
54
|
+
return "".join(block.text for block in content if isinstance(block, TextSegment))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_model_blocks(
|
|
58
|
+
text: str,
|
|
59
|
+
tool_calls: list[CallBlock] | tuple[CallBlock, ...] = (),
|
|
60
|
+
) -> list[ReplyContent]:
|
|
61
|
+
"""Build canonical ordered assistant blocks from parser accumulators."""
|
|
62
|
+
blocks: list[ReplyContent] = [TextSegment(text=text)] if text else []
|
|
63
|
+
blocks.extend(tool_calls)
|
|
64
|
+
return blocks
|