soothe-client-python 1.0.24__py3-none-any.whl → 1.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 (48) hide show
  1. soothe_client/__init__.py +1 -2
  2. soothe_client/appkit/card_projection.py +1 -1
  3. soothe_client/appkit/chunk_filter.py +13 -1
  4. soothe_client/appkit/managed_client.py +1 -1
  5. soothe_client/appkit/turn.py +1 -1
  6. soothe_client/command_client.py +6 -5
  7. soothe_client/core/__init__.py +1 -0
  8. soothe_client/core/events.py +163 -0
  9. soothe_client/core/subagent_wire.py +141 -0
  10. soothe_client/core/verbosity.py +49 -0
  11. soothe_client/display/__init__.py +75 -0
  12. soothe_client/display/_text_utils.py +18 -0
  13. soothe_client/display/card_ledger.py +469 -0
  14. soothe_client/display/card_wire.py +137 -0
  15. soothe_client/display/message_processing.py +563 -0
  16. soothe_client/display/snapshot_types.py +92 -0
  17. soothe_client/display/tool_message_format.py +115 -0
  18. soothe_client/display/tool_result.py +158 -0
  19. soothe_client/display/transcript_types.py +250 -0
  20. soothe_client/helpers.py +1 -2
  21. soothe_client/paths.py +143 -0
  22. soothe_client/protocol_params.py +1 -1
  23. soothe_client/session.py +2 -2
  24. soothe_client/stream_terminal.py +1 -1
  25. soothe_client/tools/__init__.py +37 -0
  26. soothe_client/tools/metadata.py +835 -0
  27. soothe_client/utils/__init__.py +61 -0
  28. soothe_client/utils/formatting.py +100 -0
  29. soothe_client/utils/logging.py +330 -0
  30. soothe_client/utils/parsing.py +94 -0
  31. soothe_client/utils/serde.py +61 -0
  32. soothe_client/utils/workspace.py +23 -0
  33. soothe_client/ux/__init__.py +20 -0
  34. soothe_client/ux/classification.py +79 -0
  35. soothe_client/ux/execute_namespace.py +105 -0
  36. soothe_client/ux/loop_stream.py +88 -0
  37. soothe_client/ux/stream_tool_wire.py +135 -0
  38. soothe_client/ux/task_namespace.py +533 -0
  39. soothe_client/ux/types.py +36 -0
  40. soothe_client/websocket.py +15 -13
  41. soothe_client/wire/__init__.py +75 -0
  42. soothe_client/wire/codec.py +749 -0
  43. soothe_client/wire/protocol.py +171 -0
  44. {soothe_client_python-1.0.24.dist-info → soothe_client_python-1.2.0.dist-info}/METADATA +2 -2
  45. soothe_client_python-1.2.0.dist-info/RECORD +65 -0
  46. soothe_client_python-1.0.24.dist-info/RECORD +0 -33
  47. {soothe_client_python-1.0.24.dist-info → soothe_client_python-1.2.0.dist-info}/WHEEL +0 -0
  48. {soothe_client_python-1.0.24.dist-info → soothe_client_python-1.2.0.dist-info}/licenses/LICENSE +0 -0
soothe_client/__init__.py CHANGED
@@ -11,8 +11,6 @@ from __future__ import annotations
11
11
 
12
12
  import importlib.metadata
13
13
 
14
- from soothe_sdk.wire.codec import ProtocolError
15
-
16
14
  from soothe_client.command_client import (
17
15
  AsyncCommandClient,
18
16
  CommandClient,
@@ -52,6 +50,7 @@ from soothe_client.session import (
52
50
  connect_websocket_with_retries,
53
51
  )
54
52
  from soothe_client.websocket import WebSocketClient
53
+ from soothe_client.wire.codec import ProtocolError
55
54
 
56
55
  try:
57
56
  __version__ = importlib.metadata.version("soothe-client-python")
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from soothe_sdk.display.card_wire import CardProjection, parse_card_custom_payload
5
+ from soothe_client.display.card_wire import CardProjection, parse_card_custom_payload
6
6
 
7
7
  __all__ = [
8
8
  "CardProjection",
@@ -61,8 +61,17 @@ def _plain_text(msg: dict[str, Any]) -> str:
61
61
  return ""
62
62
 
63
63
 
64
+ def _dict_has_reasoning_content(msg: dict[str, Any]) -> bool:
65
+ """True when a wire message dict carries captured reasoning in ``additional_kwargs``."""
66
+ body = _wire_body(msg)
67
+ ak = body.get("additional_kwargs")
68
+ if not isinstance(ak, dict):
69
+ return False
70
+ return bool(str(ak.get("reasoning_content") or "").strip())
71
+
72
+
64
73
  def message_chunk_is_non_actionable(data: Any) -> bool:
65
- """True when a wire `messages` pair has no tool, text, or loop phase payload."""
74
+ """True when a wire `messages` pair has no tool, text, reasoning, or loop phase payload."""
66
75
  if not isinstance(data, (list, tuple)) or len(data) != _MSG_PAIR_LEN:
67
76
  return False
68
77
  msg = data[0]
@@ -79,6 +88,8 @@ def message_chunk_is_non_actionable(data: Any) -> bool:
79
88
  return False
80
89
  if body.get("phase") or msg.get("phase"):
81
90
  return False
91
+ if _dict_has_reasoning_content(msg):
92
+ return False
82
93
  return not _plain_text(msg).strip()
83
94
 
84
95
 
@@ -93,6 +104,7 @@ def should_drop_stream_chunk_early(namespace: tuple[Any, ...], mode: str, data:
93
104
 
94
105
 
95
106
  __all__ = [
107
+ "_dict_has_reasoning_content",
96
108
  "message_chunk_is_non_actionable",
97
109
  "should_drop_stream_chunk_early",
98
110
  "updates_chunk_is_noop",
@@ -47,7 +47,7 @@ _REQUEST_METHOD_ALIASES = {
47
47
 
48
48
  def _coerce_appkit_wire_message(msg: dict[str, Any], client: WebSocketClient) -> dict[str, Any]:
49
49
  """Upgrade flat appkit dicts to protocol-1 envelopes when needed."""
50
- from soothe_sdk.wire.codec import MessageType, WireEnvelope
50
+ from soothe_client.wire.codec import MessageType, WireEnvelope
51
51
 
52
52
  msg_type = str(msg.get("type") or "")
53
53
  if msg.get("proto") == "1" or msg_type in _ALREADY_ENVELOPE_TYPES:
@@ -14,7 +14,7 @@ import time
14
14
  from collections.abc import AsyncIterator, Callable
15
15
  from typing import Any, Generic, Protocol, TypeVar
16
16
 
17
- from soothe_sdk.ux.loop_stream import assistant_output_phase
17
+ from soothe_client.ux.loop_stream import assistant_output_phase
18
18
 
19
19
  logger = logging.getLogger(__name__)
20
20
 
@@ -10,7 +10,8 @@ import logging
10
10
  from typing import Any, cast
11
11
  from uuid import uuid4
12
12
 
13
- from soothe_sdk.wire.codec import (
13
+ from soothe_client.helpers import websocket_url_from_config
14
+ from soothe_client.wire.codec import (
14
15
  ConnectionInitEnvelope,
15
16
  ConnectionInitParams,
16
17
  ErrorEnvelope,
@@ -20,8 +21,6 @@ from soothe_sdk.wire.codec import (
20
21
  encode_envelope,
21
22
  )
22
23
 
23
- from soothe_client.helpers import websocket_url_from_config
24
-
25
24
  logger = logging.getLogger(__name__)
26
25
 
27
26
  _TRANSITIONAL_DAEMON_READY_STATES = frozenset({"starting", "warming"})
@@ -34,7 +33,9 @@ _DAEMON_READY_POLL_INTERVAL_S = 0.05
34
33
  _DEFAULT_MAX_FRAME_SIZE = 10 * 1024 * 1024
35
34
 
36
35
  try:
37
- from soothe_sdk import __version__ as _client_version
36
+ from importlib.metadata import version as _pkg_version
37
+
38
+ _client_version = _pkg_version("soothe-client-python")
38
39
  except Exception: # pragma: no cover
39
40
  _client_version = "0.0.0"
40
41
 
@@ -82,7 +83,7 @@ async def _perform_handshake(ws: Any, *, timeout: float) -> None:
82
83
  init = ConnectionInitEnvelope(
83
84
  params=ConnectionInitParams(
84
85
  client_version=_client_version,
85
- client_name="soothe-sdk",
86
+ client_name="soothe-client-python",
86
87
  accept_proto=["1"],
87
88
  capabilities=["streaming", "batch", "heartbeat"],
88
89
  )
@@ -0,0 +1 @@
1
+ """Client-side event contracts: wire event types, verbosity tiers, subagent wire helpers."""
@@ -0,0 +1,163 @@
1
+ """Base event classes for Soothe events.
2
+
3
+ This module provides the base event classes that all specific events inherit from.
4
+ Module-specific events are defined in their respective modules and registered via
5
+ ``register_event()``.
6
+
7
+ Progress events use type strings ``soothe.<domain>.<component>.<action>``.
8
+ Domains in active use include cognition, internal, subagent, system, mcp, loop,
9
+ stream, error, tool, output, protocol, and skillify. Community plugins may
10
+ introduce additional ``soothe.subagent.<plugin>.*`` types when they load.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, ConfigDict
19
+
20
+
21
+ class SootheEvent(BaseModel):
22
+ """Base class for all Soothe progress events."""
23
+
24
+ type: str
25
+
26
+ model_config = ConfigDict(extra="allow")
27
+
28
+ def to_dict(self) -> dict[str, Any]:
29
+ """Serialize to a plain dict for wire-format emission."""
30
+ return self.model_dump(exclude_none=True)
31
+
32
+ def emit(self, logger: logging.Logger) -> None:
33
+ """Emit this event via the LangGraph stream writer.
34
+
35
+ Note: This method requires host-side implementation.
36
+ For SDK use, events are typically sent via WebSocket.
37
+
38
+ SDK-side event base class: provides type definition and serialization.
39
+ Host-side implementation provides actual emit.
40
+ """
41
+ pass
42
+
43
+
44
+ class LifecycleEvent(SootheEvent):
45
+ """Loop and session lifecycle events."""
46
+
47
+
48
+ class ProtocolEvent(SootheEvent):
49
+ """Core protocol activity events."""
50
+
51
+
52
+ class SubagentEvent(SootheEvent):
53
+ """Subagent activity events."""
54
+
55
+
56
+ class OutputEvent(SootheEvent):
57
+ """Content destined for user display."""
58
+
59
+
60
+ class ErrorEvent(SootheEvent):
61
+ """Error events."""
62
+
63
+ error: str
64
+
65
+
66
+ # Event type constants
67
+ # Wire-safe event type strings for CLI/TUI event processing
68
+ # Exposed at DEBUG and DETAILED level for fine-grained (per-turn) event display
69
+
70
+ # Plan events
71
+ PLAN_CREATED = "soothe.cognition.plan.created"
72
+
73
+ # Agent loop events (DEBUG level; wire ``mode=custom`` types)
74
+ STRANGE_LOOP_STARTED = "soothe.cognition.strange_loop.started"
75
+ STRANGE_LOOP_COMPLETED = "soothe.cognition.strange_loop.completed"
76
+ STRANGE_LOOP_STEP_STARTED = "soothe.cognition.strange_loop.step.started"
77
+ STRANGE_LOOP_STEP_QUEUED = "soothe.cognition.strange_loop.step.queued"
78
+ STRANGE_LOOP_STEP_COMPLETED = "soothe.cognition.strange_loop.step.completed"
79
+ STRANGE_LOOP_PLAN_DECISION = "soothe.cognition.strange_loop.plan.decision"
80
+ STRANGE_LOOP_PLAN_PHASE = "soothe.cognition.strange_loop.plan.phase"
81
+ # Wired specialist lifecycle (orphan SubAgent card)
82
+ WIRED_SUBAGENT_STARTED = "soothe.cognition.wired_subagent.started"
83
+ WIRED_SUBAGENT_COMPLETED = "soothe.cognition.wired_subagent.completed"
84
+ WIRED_SUBAGENT_FAILED = "soothe.cognition.wired_subagent.failed"
85
+ WIRED_SUBAGENT_CANCELLED = "soothe.cognition.wired_subagent.cancelled"
86
+ INTENT_CLASSIFIED = "soothe.cognition.intent.classified"
87
+
88
+ # Clarification relay events
89
+ LOOP_CLARIFICATION_REQUESTED = "soothe.loop.clarification.requested"
90
+ LOOP_CLARIFICATION_ANSWERED = "soothe.loop.clarification.answered"
91
+ LOOP_CLARIFICATION_DEFERRED = "soothe.loop.clarification.deferred"
92
+
93
+ # Stream termination
94
+ STREAM_END = "soothe.stream.end"
95
+
96
+ # Display card wire frames (RFC-413) — top-level or ``event``/``custom`` ``data.type``
97
+ CARD_CREATED = "soothe.card.created"
98
+ CARD_UPDATED = "soothe.card.updated"
99
+ CARD_FINALIZED = "soothe.card.finalized"
100
+ CARD_REPLAY_BEGIN = "soothe.card.replay.begin"
101
+ CARD_REPLAY_END = "soothe.card.replay.end"
102
+ CARD_WIRE_TYPES = frozenset({CARD_CREATED, CARD_UPDATED, CARD_FINALIZED})
103
+ CARD_REPLAY_WIRE_TYPES = frozenset({CARD_REPLAY_BEGIN, CARD_REPLAY_END})
104
+
105
+ # Protocol-primitive events (wire-visible constants shared across packages;
106
+ # canonical home is the SDK as the protocol-contracts layer. nano and the
107
+ # host re-export these rather than redefine the literals.)
108
+ ERROR = "soothe.error.general.failed"
109
+ LLM_RETRY_ATTEMPT = "soothe.cognition.llm.retry.attempt"
110
+ MEMORY_RECALLED = "soothe.internal.memory.recalled"
111
+ MEMORY_STORED = "soothe.internal.memory.stored"
112
+ POLICY_CHECKED = "soothe.internal.policy.checked"
113
+ POLICY_DENIED = "soothe.internal.policy.denied"
114
+
115
+ # Agent loop configuration constants
116
+ DEFAULT_STRANGE_LOOP_MAX_ITERATIONS = 10
117
+
118
+
119
+ __all__ = [
120
+ "ErrorEvent",
121
+ "LifecycleEvent",
122
+ "OutputEvent",
123
+ "ProtocolEvent",
124
+ "SootheEvent",
125
+ "SubagentEvent",
126
+ # Event type constants - plan
127
+ "PLAN_CREATED",
128
+ # Agent loop (DEBUG)
129
+ "STRANGE_LOOP_STARTED",
130
+ "STRANGE_LOOP_COMPLETED",
131
+ "STRANGE_LOOP_STEP_STARTED",
132
+ "STRANGE_LOOP_STEP_QUEUED",
133
+ "STRANGE_LOOP_STEP_COMPLETED",
134
+ "STRANGE_LOOP_PLAN_DECISION",
135
+ "STRANGE_LOOP_PLAN_PHASE",
136
+ "WIRED_SUBAGENT_STARTED",
137
+ "WIRED_SUBAGENT_COMPLETED",
138
+ "WIRED_SUBAGENT_FAILED",
139
+ "WIRED_SUBAGENT_CANCELLED",
140
+ "INTENT_CLASSIFIED",
141
+ # Clarification relay
142
+ "LOOP_CLARIFICATION_REQUESTED",
143
+ "LOOP_CLARIFICATION_ANSWERED",
144
+ "LOOP_CLARIFICATION_DEFERRED",
145
+ "STREAM_END",
146
+ # Display card wire (RFC-413)
147
+ "CARD_CREATED",
148
+ "CARD_UPDATED",
149
+ "CARD_FINALIZED",
150
+ "CARD_REPLAY_BEGIN",
151
+ "CARD_REPLAY_END",
152
+ "CARD_WIRE_TYPES",
153
+ "CARD_REPLAY_WIRE_TYPES",
154
+ # Protocol-primitive constants (canonical home; re-exported by nano + host)
155
+ "ERROR",
156
+ "LLM_RETRY_ATTEMPT",
157
+ "MEMORY_RECALLED",
158
+ "MEMORY_STORED",
159
+ "POLICY_CHECKED",
160
+ "POLICY_DENIED",
161
+ # Constants
162
+ "DEFAULT_STRANGE_LOOP_MAX_ITERATIONS",
163
+ ]
@@ -0,0 +1,141 @@
1
+ """Curated ``soothe.subagent.*`` wire protocol (metadata-only).
2
+
3
+ Provides registration, structural validation, payload clipping, and stream emission.
4
+ Event type strings live in each subagent (or plugin) ``events`` module — not in this SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import re
11
+ from typing import Any
12
+
13
+ _REGISTERED_SUBAGENT_WIRE_TYPES: set[str] = set()
14
+
15
+ # ``soothe.subagent.<agent>.<signal>`` — clients may accept before producers register types.
16
+ _CURATED_SUBAGENT_WIRE_RE = re.compile(r"^soothe\.subagent\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_.]+$")
17
+
18
+ _DEFAULT_PREVIEW_LEN = 120
19
+ _LONG_PREVIEW_LEN = 200
20
+ _TASK_DESCRIPTION_LEN = 8000
21
+
22
+
23
+ def register_subagent_wire_event_types(*event_types: str) -> None:
24
+ """Register ``soothe.subagent.*`` wire types for emission allowlisting.
25
+
26
+ Subagents call this from their ``events`` module (or rely on the host
27
+ event registry to register automatically).
28
+ """
29
+ for et in event_types:
30
+ if isinstance(et, str) and et.startswith("soothe.subagent."):
31
+ _REGISTERED_SUBAGENT_WIRE_TYPES.add(et)
32
+
33
+
34
+ def get_allowlisted_subagent_event_types() -> frozenset[str]:
35
+ """Return wire types registered for emission in this process."""
36
+ return frozenset(_REGISTERED_SUBAGENT_WIRE_TYPES)
37
+
38
+
39
+ def is_curated_subagent_wire_event_type(event_type: str) -> bool:
40
+ """Return True for structurally valid curated ``soothe.subagent.*`` wire types.
41
+
42
+ Used by CLI/TUI clients that may not import subagent ``events`` modules.
43
+ """
44
+ return bool(_CURATED_SUBAGENT_WIRE_RE.match(event_type))
45
+
46
+
47
+ def is_emit_allowed_subagent_wire_event_type(event_type: str) -> bool:
48
+ """Return True when a producer may emit this subagent wire event."""
49
+ return event_type in _REGISTERED_SUBAGENT_WIRE_TYPES
50
+
51
+
52
+ def is_allowlisted_subagent_event_type(event_type: str) -> bool:
53
+ """Return True when a consumer may treat ``event_type`` as curated subagent wire."""
54
+ return is_emit_allowed_subagent_wire_event_type(
55
+ event_type
56
+ ) or is_curated_subagent_wire_event_type(event_type)
57
+
58
+
59
+ def parse_subagent_wire_agent(event_type: str) -> str | None:
60
+ """Return agent segment (``browser``, ``claude``, …) from ``soothe.subagent.<agent>.…``."""
61
+ parts = event_type.split(".")
62
+ if len(parts) >= 4 and parts[0] == "soothe" and parts[1] == "subagent":
63
+ return parts[2]
64
+ return None
65
+
66
+
67
+ def truncate_wire_str(value: str, max_len: int = _DEFAULT_PREVIEW_LEN) -> str:
68
+ """Truncate a single wire string field."""
69
+ if len(value) <= max_len:
70
+ return value
71
+ if max_len <= 1:
72
+ return "…"
73
+ return value[: max_len - 1] + "…"
74
+
75
+
76
+ def clip_wire_event_payload(data: dict[str, Any]) -> dict[str, Any]:
77
+ """Copy event dict and truncate known string fields for wire safety."""
78
+ out = dict(data)
79
+ string_caps: dict[str, int] = {
80
+ "task_preview": _LONG_PREVIEW_LEN,
81
+ "topic": _LONG_PREVIEW_LEN,
82
+ "topic_preview": _DEFAULT_PREVIEW_LEN,
83
+ "search_target": _TASK_DESCRIPTION_LEN,
84
+ "task": _LONG_PREVIEW_LEN,
85
+ "judgement": _LONG_PREVIEW_LEN,
86
+ "message": _LONG_PREVIEW_LEN,
87
+ "error": _LONG_PREVIEW_LEN,
88
+ "action_preview": _DEFAULT_PREVIEW_LEN,
89
+ "tool_name": _DEFAULT_PREVIEW_LEN,
90
+ "input_preview": _DEFAULT_PREVIEW_LEN,
91
+ "summary": _LONG_PREVIEW_LEN,
92
+ "url": _LONG_PREVIEW_LEN,
93
+ "title": _DEFAULT_PREVIEW_LEN,
94
+ "query_preview": _DEFAULT_PREVIEW_LEN,
95
+ "args_preview": _DEFAULT_PREVIEW_LEN,
96
+ "result_preview": _DEFAULT_PREVIEW_LEN,
97
+ }
98
+ for key, cap in string_caps.items():
99
+ val = out.get(key)
100
+ if isinstance(val, str):
101
+ out[key] = truncate_wire_str(val, cap)
102
+ return out
103
+
104
+
105
+ def emit_subagent_wire_event(event: dict[str, Any], logger: logging.Logger) -> None:
106
+ """Emit allowlisted subagent progress to the LangGraph ``custom`` stream.
107
+
108
+ Community subagents use this helper so they do not depend on the host package.
109
+ Unknown event types are dropped unless registered via :func:`register_subagent_wire_event_types`.
110
+
111
+ Args:
112
+ event: Dict with at least ``type`` registered for emission.
113
+ logger: Caller logger for audit trail.
114
+ """
115
+ et = event.get("type", "")
116
+ if not isinstance(et, str) or not is_emit_allowed_subagent_wire_event_type(et):
117
+ logger.debug("Ignoring non-allowlisted subagent wire event: %r", et)
118
+ return
119
+ clipped = clip_wire_event_payload(event)
120
+ logger.debug("subagent.wire %s", clipped.get("type", ""))
121
+ try:
122
+ from langgraph.config import get_stream_writer
123
+
124
+ writer = get_stream_writer()
125
+ if writer:
126
+ writer(clipped)
127
+ except (ImportError, RuntimeError, KeyError):
128
+ pass
129
+
130
+
131
+ __all__ = [
132
+ "clip_wire_event_payload",
133
+ "emit_subagent_wire_event",
134
+ "get_allowlisted_subagent_event_types",
135
+ "is_allowlisted_subagent_event_type",
136
+ "is_curated_subagent_wire_event_type",
137
+ "is_emit_allowed_subagent_wire_event_type",
138
+ "parse_subagent_wire_agent",
139
+ "register_subagent_wire_event_types",
140
+ "truncate_wire_str",
141
+ ]
@@ -0,0 +1,49 @@
1
+ """Binary verbosity tier for event classification.
2
+
3
+ Events classify to one of two tiers:
4
+ - `NORMAL`: client-visible — flows over the wire to subscribed clients.
5
+ - `INTERNAL`: hidden — daemon-side only, never leaves the daemon.
6
+
7
+ Earlier revisions had a five-level ladder (QUIET / NORMAL / DETAILED / DEBUG /
8
+ INTERNAL). That granularity was never exercised — the wire ceiling and the TUI
9
+ gate were both hardcoded to "normal", so the only effective distinction was
10
+ visible-vs-hidden. The tiers collapsed accordingly.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from enum import IntEnum
16
+
17
+
18
+ class VerbosityTier(IntEnum):
19
+ """Visibility class for catalog events.
20
+
21
+ `NORMAL` events are sent to clients; `INTERNAL` events are not.
22
+
23
+ Examples:
24
+ >>> should_show(VerbosityTier.NORMAL)
25
+ True
26
+ >>> should_show(VerbosityTier.INTERNAL)
27
+ False
28
+ """
29
+
30
+ NORMAL = 1
31
+ INTERNAL = 99
32
+
33
+
34
+ def should_show(tier: VerbosityTier) -> bool:
35
+ """Return True if tier is client-visible.
36
+
37
+ Args:
38
+ tier: Visibility class of the content.
39
+
40
+ Returns:
41
+ True iff `tier` is `NORMAL`.
42
+ """
43
+ return tier == VerbosityTier.NORMAL
44
+
45
+
46
+ __all__ = [
47
+ "VerbosityTier",
48
+ "should_show",
49
+ ]
@@ -0,0 +1,75 @@
1
+ """Client-side display-layer contract parsing.
2
+
3
+ This package hosts the wire-protocol parse side shared by the transport,
4
+ appkit, and any Python front end:
5
+
6
+ * ``transcript_types`` — ``MessageData`` / ``MessageType`` / ``ToolStatus``
7
+ dataclasses travelling on the wire.
8
+ * ``card_ledger`` — pure-Python ``CardMutation`` + ``InMemoryCardLedger``
9
+ primitives backing the ``soothe.card.*`` wire schema.
10
+ * ``card_wire`` — parse/apply helpers for live ``soothe.card.*`` frames.
11
+ * ``snapshot_types`` — goal display snapshot wire payload types.
12
+ * ``message_processing`` / ``tool_result`` / ``tool_message_format`` —
13
+ stream-to-text parse helpers.
14
+
15
+ (TUI-owned ``card_binder`` / ``text_extract`` / ``snapshot_collapser`` moved
16
+ to `soothe_cli.display` / the host `soothe.display` in 1.2.0.)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from soothe_client.display.card_ledger import (
22
+ CARD_SCHEMA_VERSION,
23
+ CardMutation,
24
+ CardOp,
25
+ InMemoryCardLedger,
26
+ align_cards_preserving_ids,
27
+ build_header_mutation,
28
+ card_from_wire_dict,
29
+ card_to_wire_dict,
30
+ cards_to_mutations,
31
+ diff_card_snapshots,
32
+ utc_now_iso,
33
+ )
34
+ from soothe_client.display.card_wire import (
35
+ CardProjection,
36
+ parse_card_custom_payload,
37
+ )
38
+ from soothe_client.display.snapshot_types import (
39
+ GOAL_DISPLAY_SNAPSHOT_SCHEMA_VERSION,
40
+ GoalDisplaySnapshot,
41
+ snapshot_from_dataclass_dict,
42
+ )
43
+ from soothe_client.display.transcript_types import (
44
+ UPDATABLE_FIELDS,
45
+ MessageData,
46
+ MessageType,
47
+ ToolStatus,
48
+ )
49
+
50
+ __all__ = [
51
+ # Types
52
+ "MessageData",
53
+ "MessageType",
54
+ "ToolStatus",
55
+ "UPDATABLE_FIELDS",
56
+ # Ledger
57
+ "CARD_SCHEMA_VERSION",
58
+ "CardMutation",
59
+ "CardOp",
60
+ "InMemoryCardLedger",
61
+ "build_header_mutation",
62
+ "card_from_wire_dict",
63
+ "card_to_wire_dict",
64
+ "cards_to_mutations",
65
+ "diff_card_snapshots",
66
+ "align_cards_preserving_ids",
67
+ "utc_now_iso",
68
+ # Card wire
69
+ "CardProjection",
70
+ "parse_card_custom_payload",
71
+ # Goal display snapshots
72
+ "GOAL_DISPLAY_SNAPSHOT_SCHEMA_VERSION",
73
+ "GoalDisplaySnapshot",
74
+ "snapshot_from_dataclass_dict",
75
+ ]
@@ -0,0 +1,18 @@
1
+ """Internal text utilities for tool formatting (not part of public API)."""
2
+
3
+
4
+ def text_looks_like_error(text: str) -> bool:
5
+ """Return True if text content suggests a tool failure.
6
+
7
+ Checks for common error indicator substrings.
8
+
9
+ Args:
10
+ text: Tool output text to inspect.
11
+
12
+ Returns:
13
+ True if any error indicator is found.
14
+ """
15
+ if not text:
16
+ return False
17
+ lowered = text.lower()
18
+ return any(indicator in lowered for indicator in ("error", "failed", "exception", "traceback"))