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.
Files changed (203) hide show
  1. rlmagent/__init__.py +1 -0
  2. rlmagent/__main__.py +8 -0
  3. rlmagent/acp/__init__.py +1 -0
  4. rlmagent/acp/runtime.py +71 -0
  5. rlmagent/acp/server.py +598 -0
  6. rlmagent/acp/updates.py +128 -0
  7. rlmagent/agentnet.py +237 -0
  8. rlmagent/agents/__init__.py +1 -0
  9. rlmagent/agents/approval.py +90 -0
  10. rlmagent/agents/bridge.py +109 -0
  11. rlmagent/agents/budget.py +212 -0
  12. rlmagent/agents/child_handles.py +46 -0
  13. rlmagent/agents/child_mailbox.py +30 -0
  14. rlmagent/agents/kernel_api.py +156 -0
  15. rlmagent/agents/output.py +34 -0
  16. rlmagent/agents/patch.py +107 -0
  17. rlmagent/agents/prompts.py +156 -0
  18. rlmagent/agents/trace.py +290 -0
  19. rlmagent/agents/tree.py +682 -0
  20. rlmagent/agents/wasm_api.py +105 -0
  21. rlmagent/agents/workspace.py +357 -0
  22. rlmagent/autopilot.py +447 -0
  23. rlmagent/backend.py +946 -0
  24. rlmagent/backend_context.py +74 -0
  25. rlmagent/cli/__init__.py +5 -0
  26. rlmagent/cli/config_cmd.py +177 -0
  27. rlmagent/cli/health.py +156 -0
  28. rlmagent/cli/main.py +1194 -0
  29. rlmagent/cli/update.py +188 -0
  30. rlmagent/clipboard.py +63 -0
  31. rlmagent/config/__init__.py +37 -0
  32. rlmagent/config/config.py +297 -0
  33. rlmagent/config/loader.py +119 -0
  34. rlmagent/config/prompts.py +124 -0
  35. rlmagent/config/secure_input.py +213 -0
  36. rlmagent/config/store.py +275 -0
  37. rlmagent/context/__init__.py +47 -0
  38. rlmagent/context/budget.py +557 -0
  39. rlmagent/contract.py +311 -0
  40. rlmagent/contract_run.py +232 -0
  41. rlmagent/conversation.py +2547 -0
  42. rlmagent/cron.py +77 -0
  43. rlmagent/directives.py +893 -0
  44. rlmagent/discovery.py +356 -0
  45. rlmagent/execution_log.py +66 -0
  46. rlmagent/factory.py +189 -0
  47. rlmagent/factory_graph.py +413 -0
  48. rlmagent/harness/__init__.py +63 -0
  49. rlmagent/harness/contracts/__init__.py +1 -0
  50. rlmagent/harness/contracts/stream.py +129 -0
  51. rlmagent/harness/contracts/tooling.py +127 -0
  52. rlmagent/harness/contracts/transcript/__init__.py +68 -0
  53. rlmagent/harness/contracts/transcript/_base.py +29 -0
  54. rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
  55. rlmagent/harness/contracts/transcript/entries.py +180 -0
  56. rlmagent/harness/contracts/transcript/segments.py +64 -0
  57. rlmagent/harness/contracts/values.py +6 -0
  58. rlmagent/harness/driver.py +397 -0
  59. rlmagent/harness/engine.py +489 -0
  60. rlmagent/harness/mending.py +153 -0
  61. rlmagent/harness/provider/__init__.py +1 -0
  62. rlmagent/harness/provider/base.py +27 -0
  63. rlmagent/harness/provider/wire.py +189 -0
  64. rlmagent/harness/py.typed +0 -0
  65. rlmagent/harness/session/__init__.py +5 -0
  66. rlmagent/harness/session/index.py +218 -0
  67. rlmagent/harness/session/lease.py +66 -0
  68. rlmagent/harness/session/records.py +118 -0
  69. rlmagent/harness/session/replay.py +165 -0
  70. rlmagent/harness/session/store.py +155 -0
  71. rlmagent/harness/session/summary.py +46 -0
  72. rlmagent/hints.py +47 -0
  73. rlmagent/history.py +92 -0
  74. rlmagent/hooks/__init__.py +1 -0
  75. rlmagent/hooks/hooks.py +1 -0
  76. rlmagent/instructions.py +157 -0
  77. rlmagent/job_runner.py +117 -0
  78. rlmagent/jobs.py +365 -0
  79. rlmagent/kept_agents.py +397 -0
  80. rlmagent/kernel/__init__.py +5 -0
  81. rlmagent/kernel/session.py +205 -0
  82. rlmagent/kernel/wasm.py +169 -0
  83. rlmagent/logs.py +208 -0
  84. rlmagent/managed_release.py +368 -0
  85. rlmagent/mcp.py +411 -0
  86. rlmagent/mcp_oauth.py +213 -0
  87. rlmagent/model/__init__.py +29 -0
  88. rlmagent/model/_claude/__init__.py +1 -0
  89. rlmagent/model/_claude/courier.py +117 -0
  90. rlmagent/model/_claude/emitter.py +378 -0
  91. rlmagent/model/_oai/__init__.py +1 -0
  92. rlmagent/model/_oai/helpers.py +256 -0
  93. rlmagent/model/_oai/normalize.py +263 -0
  94. rlmagent/model/_oai/parsers.py +317 -0
  95. rlmagent/model/_oai/payloads.py +226 -0
  96. rlmagent/model/_oai/transport.py +127 -0
  97. rlmagent/model/claude.py +552 -0
  98. rlmagent/model/copilot.py +142 -0
  99. rlmagent/model/correlation.py +21 -0
  100. rlmagent/model/deadline.py +19 -0
  101. rlmagent/model/discovery.py +181 -0
  102. rlmagent/model/external/__init__.py +11 -0
  103. rlmagent/model/external/acp.py +202 -0
  104. rlmagent/model/external/acp_install.py +109 -0
  105. rlmagent/model/external/cli.py +248 -0
  106. rlmagent/model/external/textcalls.py +57 -0
  107. rlmagent/model/failover.py +129 -0
  108. rlmagent/model/google.py +199 -0
  109. rlmagent/model/limits.py +46 -0
  110. rlmagent/model/oai_compatible.py +414 -0
  111. rlmagent/model/preferences.py +56 -0
  112. rlmagent/model/py.typed +0 -0
  113. rlmagent/model/registered_oauth.py +195 -0
  114. rlmagent/model/scripted.py +55 -0
  115. rlmagent/model/settings.py +271 -0
  116. rlmagent/model/subscription.py +398 -0
  117. rlmagent/model/token_bounds.py +15 -0
  118. rlmagent/model/transport/__init__.py +1 -0
  119. rlmagent/model/transport/backoff.py +87 -0
  120. rlmagent/model/transport/client.py +87 -0
  121. rlmagent/model/transport/faults.py +65 -0
  122. rlmagent/model/transport/responses_socket.py +223 -0
  123. rlmagent/model/vertex.py +93 -0
  124. rlmagent/network.py +123 -0
  125. rlmagent/packages.py +246 -0
  126. rlmagent/planning.py +84 -0
  127. rlmagent/playbook/__init__.py +1 -0
  128. rlmagent/playbook/apply.py +106 -0
  129. rlmagent/playbook/digest.py +63 -0
  130. rlmagent/playbook/improver.py +272 -0
  131. rlmagent/playbook/plan.py +99 -0
  132. rlmagent/playbook/store.py +67 -0
  133. rlmagent/plugins.py +327 -0
  134. rlmagent/policy.py +105 -0
  135. rlmagent/prompt_refs.py +57 -0
  136. rlmagent/prompts.py +331 -0
  137. rlmagent/prune.py +60 -0
  138. rlmagent/py.typed +0 -0
  139. rlmagent/reasoning.py +135 -0
  140. rlmagent/refinement/__init__.py +1 -0
  141. rlmagent/refinement/controller.py +448 -0
  142. rlmagent/refinement/engine.py +220 -0
  143. rlmagent/refinement/evidence.py +131 -0
  144. rlmagent/refinement/scoped.py +90 -0
  145. rlmagent/refinement/store.py +193 -0
  146. rlmagent/refresh.py +202 -0
  147. rlmagent/routing.py +52 -0
  148. rlmagent/runtime.py +575 -0
  149. rlmagent/safety.py +267 -0
  150. rlmagent/session_html.py +57 -0
  151. rlmagent/session_import.py +52 -0
  152. rlmagent/session_share.py +55 -0
  153. rlmagent/sessions.py +249 -0
  154. rlmagent/shell_sandbox.py +88 -0
  155. rlmagent/skillset.py +320 -0
  156. rlmagent/steering.py +148 -0
  157. rlmagent/stops.py +62 -0
  158. rlmagent/subscriptions.py +219 -0
  159. rlmagent/tasks.py +98 -0
  160. rlmagent/toolhost.py +310 -0
  161. rlmagent/tools/__init__.py +56 -0
  162. rlmagent/tools/_shared.py +210 -0
  163. rlmagent/tools/files.py +462 -0
  164. rlmagent/tools/kernel.py +93 -0
  165. rlmagent/tools/patch_tool.py +68 -0
  166. rlmagent/tools/search.py +188 -0
  167. rlmagent/trust.py +383 -0
  168. rlmagent/tui/__init__.py +1 -0
  169. rlmagent/tui/adapter.py +506 -0
  170. rlmagent/tui/app.py +711 -0
  171. rlmagent/tui/approval_screen.py +57 -0
  172. rlmagent/tui/choice_screen.py +47 -0
  173. rlmagent/tui/completions.py +116 -0
  174. rlmagent/tui/components.py +25 -0
  175. rlmagent/tui/composer.py +40 -0
  176. rlmagent/tui/factory_view.py +86 -0
  177. rlmagent/tui/inspect.py +193 -0
  178. rlmagent/tui/messages.py +215 -0
  179. rlmagent/tui/model_screen.py +307 -0
  180. rlmagent/tui/prompt.py +98 -0
  181. rlmagent/tui/question_screen.py +52 -0
  182. rlmagent/tui/replay.py +141 -0
  183. rlmagent/tui/rlm_agent.tcss +405 -0
  184. rlmagent/tui/search_box.py +91 -0
  185. rlmagent/tui/sidebar.py +182 -0
  186. rlmagent/tui/status.py +49 -0
  187. rlmagent/tui/steps.py +283 -0
  188. rlmagent/tui/suggest.py +778 -0
  189. rlmagent/tui/transcript.py +172 -0
  190. rlmagent/tui/trust_dialog.py +58 -0
  191. rlmagent/tui/worker_attach.py +141 -0
  192. rlmagent/ui/__init__.py +1 -0
  193. rlmagent/ui/render.py +211 -0
  194. rlmagent/undo.py +109 -0
  195. rlmagent/utils/__init__.py +1 -0
  196. rlmagent/websearch.py +75 -0
  197. rlmagent/worker.py +1275 -0
  198. rlmagent/worker_service.py +203 -0
  199. rlmness_cli-0.2.0.dist-info/METADATA +882 -0
  200. rlmness_cli-0.2.0.dist-info/RECORD +203 -0
  201. rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
  202. rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
  203. 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
@@ -0,0 +1,6 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ type JPrimitive = str | int | float | bool | None
5
+ type JValue = JPrimitive | list[JValue] | dict[str, JValue]
6
+ type JObject = dict[str, JValue]