soothe-client-python 1.0.24__tar.gz → 1.1.0__tar.gz
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.
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/PKG-INFO +2 -2
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/pyproject.toml +4 -2
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/__init__.py +1 -2
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/card_projection.py +1 -1
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/chunk_filter.py +13 -1
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/managed_client.py +1 -1
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/turn.py +1 -1
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/command_client.py +5 -4
- soothe_client_python-1.1.0/src/soothe_client/core/__init__.py +1 -0
- soothe_client_python-1.1.0/src/soothe_client/core/events.py +163 -0
- soothe_client_python-1.1.0/src/soothe_client/core/subagent_wire.py +141 -0
- soothe_client_python-1.1.0/src/soothe_client/core/verbosity.py +49 -0
- soothe_client_python-1.1.0/src/soothe_client/display/__init__.py +120 -0
- soothe_client_python-1.1.0/src/soothe_client/display/_text_utils.py +18 -0
- soothe_client_python-1.1.0/src/soothe_client/display/card_binder.py +787 -0
- soothe_client_python-1.1.0/src/soothe_client/display/card_ledger.py +469 -0
- soothe_client_python-1.1.0/src/soothe_client/display/card_wire.py +137 -0
- soothe_client_python-1.1.0/src/soothe_client/display/message_processing.py +563 -0
- soothe_client_python-1.1.0/src/soothe_client/display/snapshot_collapser.py +118 -0
- soothe_client_python-1.1.0/src/soothe_client/display/snapshot_types.py +92 -0
- soothe_client_python-1.1.0/src/soothe_client/display/text_extract.py +127 -0
- soothe_client_python-1.1.0/src/soothe_client/display/tool_message_format.py +115 -0
- soothe_client_python-1.1.0/src/soothe_client/display/tool_result.py +158 -0
- soothe_client_python-1.1.0/src/soothe_client/display/transcript_types.py +250 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/helpers.py +1 -2
- soothe_client_python-1.1.0/src/soothe_client/paths.py +143 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/protocol_params.py +1 -1
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/session.py +2 -2
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/stream_terminal.py +1 -1
- soothe_client_python-1.1.0/src/soothe_client/tools/__init__.py +37 -0
- soothe_client_python-1.1.0/src/soothe_client/tools/metadata.py +835 -0
- soothe_client_python-1.1.0/src/soothe_client/utils/__init__.py +61 -0
- soothe_client_python-1.1.0/src/soothe_client/utils/formatting.py +100 -0
- soothe_client_python-1.1.0/src/soothe_client/utils/logging.py +330 -0
- soothe_client_python-1.1.0/src/soothe_client/utils/parsing.py +94 -0
- soothe_client_python-1.1.0/src/soothe_client/utils/serde.py +61 -0
- soothe_client_python-1.1.0/src/soothe_client/utils/workspace.py +23 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/__init__.py +30 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/classification.py +79 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/execute_namespace.py +105 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/loop_stream.py +88 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/stream_tool_wire.py +135 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/subagent_progress.py +171 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/subagent_wire_display.py +121 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/task_namespace.py +533 -0
- soothe_client_python-1.1.0/src/soothe_client/ux/types.py +36 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/websocket.py +14 -12
- soothe_client_python-1.1.0/src/soothe_client/wire/__init__.py +75 -0
- soothe_client_python-1.1.0/src/soothe_client/wire/codec.py +749 -0
- soothe_client_python-1.1.0/src/soothe_client/wire/protocol.py +171 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/.gitignore +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/LICENSE +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/README.md +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/__init__.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/classifier.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/daemon_session.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/loop_session_store.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/pool.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/turn_boundary.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/turn_runner.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/intent_hints.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/py.typed +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/schemas.py +0 -0
- {soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/turn_boundary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: soothe-client-python
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: WebSocket client + appkit for soothe-daemon (Python)
|
|
5
5
|
Project-URL: Homepage, https://github.com/mirasoth/soothe-client-python
|
|
6
6
|
Project-URL: Documentation, https://soothe.readthedocs.io
|
|
@@ -19,9 +19,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Requires-Python: <3.15,>=3.11
|
|
22
|
+
Requires-Dist: langchain-core<2.0.0,>=1.6.0
|
|
22
23
|
Requires-Dist: pillow<12.0.0,>=10.0.0
|
|
23
24
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
24
|
-
Requires-Dist: soothe-sdk<2.0.0,>=1.0.9
|
|
25
25
|
Requires-Dist: websockets>=12.0
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
@@ -23,8 +23,8 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"soothe-sdk>=1.0.9,<2.0.0",
|
|
27
26
|
"pydantic>=2.0.0,<3.0.0",
|
|
27
|
+
"langchain-core>=1.6.0,<2.0.0", # Wire codec message serde (absorbed from soothe-sdk)
|
|
28
28
|
"websockets>=12.0",
|
|
29
29
|
"pillow>=10.0.0,<12.0.0",
|
|
30
30
|
]
|
|
@@ -77,8 +77,10 @@ disallow_untyped_defs = true
|
|
|
77
77
|
packages = ["soothe_client"]
|
|
78
78
|
mypy_path = "src"
|
|
79
79
|
|
|
80
|
+
# langgraph is an agent-axis dependency; client modules reference it only via
|
|
81
|
+
# lazy/TYPE_CHECKING imports (utils.serde allowlist, core.subagent_wire writer).
|
|
80
82
|
[[tool.mypy.overrides]]
|
|
81
|
-
module = ["
|
|
83
|
+
module = ["langgraph.*"]
|
|
82
84
|
ignore_missing_imports = true
|
|
83
85
|
|
|
84
86
|
[tool.pytest.ini_options]
|
|
@@ -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")
|
{soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
@@ -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
|
|
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
|
|
17
|
+
from soothe_client.ux.loop_stream import assistant_output_phase
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
{soothe_client_python-1.0.24 → soothe_client_python-1.1.0}/src/soothe_client/command_client.py
RENAMED
|
@@ -10,7 +10,8 @@ import logging
|
|
|
10
10
|
from typing import Any, cast
|
|
11
11
|
from uuid import uuid4
|
|
12
12
|
|
|
13
|
-
from
|
|
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
|
|
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
|
|
|
@@ -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,120 @@
|
|
|
1
|
+
"""Display-layer SDK package.
|
|
2
|
+
|
|
3
|
+
This package hosts:
|
|
4
|
+
|
|
5
|
+
* ``transcript_types`` — ``MessageData`` / ``MessageType`` / ``ToolStatus``
|
|
6
|
+
dataclasses shared between TUI rendering and the host-resident
|
|
7
|
+
``CardBinder``.
|
|
8
|
+
* ``card_binder`` — pure event → ``MessageData`` binding logic, callable
|
|
9
|
+
from either the TUI or the host.
|
|
10
|
+
* ``card_ledger`` — pure-Python ``CardMutation`` + ``InMemoryCardLedger``
|
|
11
|
+
primitives backing the display card store and the ``soothe.card.*`` wire schema.
|
|
12
|
+
* ``card_wire`` — parse/apply helpers for live ``soothe.card.*`` frames.
|
|
13
|
+
* Tool-display helpers (``text_extract``, ``message_processing``,
|
|
14
|
+
``tool_message_format``, ``tool_result``) that the binder depends on,
|
|
15
|
+
kept here so the binder has no client-side dependencies.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from soothe_client.display.card_binder import (
|
|
21
|
+
collect_cognition_card_replay,
|
|
22
|
+
conversation_rows_to_langchain_messages,
|
|
23
|
+
convert_combined_to_data,
|
|
24
|
+
convert_event_to_message_data,
|
|
25
|
+
convert_loop_events_to_data,
|
|
26
|
+
convert_messages_to_data,
|
|
27
|
+
is_loop_internal_checkpoint_message,
|
|
28
|
+
merge_history_sources,
|
|
29
|
+
merge_step_progress,
|
|
30
|
+
merge_visible_messages_with_cognition_cards,
|
|
31
|
+
parse_loop_event_timestamp,
|
|
32
|
+
sanitize_resume_display_cards,
|
|
33
|
+
)
|
|
34
|
+
from soothe_client.display.card_ledger import (
|
|
35
|
+
CARD_SCHEMA_VERSION,
|
|
36
|
+
CardMutation,
|
|
37
|
+
CardOp,
|
|
38
|
+
InMemoryCardLedger,
|
|
39
|
+
align_cards_preserving_ids,
|
|
40
|
+
build_header_mutation,
|
|
41
|
+
card_from_wire_dict,
|
|
42
|
+
card_to_wire_dict,
|
|
43
|
+
cards_to_mutations,
|
|
44
|
+
diff_card_snapshots,
|
|
45
|
+
utc_now_iso,
|
|
46
|
+
)
|
|
47
|
+
from soothe_client.display.card_wire import (
|
|
48
|
+
CardProjection,
|
|
49
|
+
parse_card_custom_payload,
|
|
50
|
+
)
|
|
51
|
+
from soothe_client.display.snapshot_collapser import (
|
|
52
|
+
build_goal_snapshot,
|
|
53
|
+
fold_display_cards,
|
|
54
|
+
split_cards_by_user_segments,
|
|
55
|
+
)
|
|
56
|
+
from soothe_client.display.snapshot_types import (
|
|
57
|
+
GOAL_DISPLAY_SNAPSHOT_SCHEMA_VERSION,
|
|
58
|
+
GoalDisplaySnapshot,
|
|
59
|
+
snapshot_from_dataclass_dict,
|
|
60
|
+
)
|
|
61
|
+
from soothe_client.display.text_extract import (
|
|
62
|
+
extract_ai_text_for_display,
|
|
63
|
+
extract_text_from_ai_message,
|
|
64
|
+
extract_user_text_for_display,
|
|
65
|
+
normalize_stream_message,
|
|
66
|
+
)
|
|
67
|
+
from soothe_client.display.transcript_types import (
|
|
68
|
+
UPDATABLE_FIELDS,
|
|
69
|
+
MessageData,
|
|
70
|
+
MessageType,
|
|
71
|
+
ToolStatus,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
# Types
|
|
76
|
+
"MessageData",
|
|
77
|
+
"MessageType",
|
|
78
|
+
"ToolStatus",
|
|
79
|
+
"UPDATABLE_FIELDS",
|
|
80
|
+
# Text extract
|
|
81
|
+
"extract_ai_text_for_display",
|
|
82
|
+
"extract_text_from_ai_message",
|
|
83
|
+
"extract_user_text_for_display",
|
|
84
|
+
"normalize_stream_message",
|
|
85
|
+
# Binder
|
|
86
|
+
"collect_cognition_card_replay",
|
|
87
|
+
"convert_combined_to_data",
|
|
88
|
+
"convert_event_to_message_data",
|
|
89
|
+
"convert_loop_events_to_data",
|
|
90
|
+
"convert_messages_to_data",
|
|
91
|
+
"conversation_rows_to_langchain_messages",
|
|
92
|
+
"is_loop_internal_checkpoint_message",
|
|
93
|
+
"merge_history_sources",
|
|
94
|
+
"merge_step_progress",
|
|
95
|
+
"merge_visible_messages_with_cognition_cards",
|
|
96
|
+
"parse_loop_event_timestamp",
|
|
97
|
+
"sanitize_resume_display_cards",
|
|
98
|
+
# Ledger
|
|
99
|
+
"CARD_SCHEMA_VERSION",
|
|
100
|
+
"CardMutation",
|
|
101
|
+
"CardOp",
|
|
102
|
+
"InMemoryCardLedger",
|
|
103
|
+
"build_header_mutation",
|
|
104
|
+
"card_from_wire_dict",
|
|
105
|
+
"card_to_wire_dict",
|
|
106
|
+
"cards_to_mutations",
|
|
107
|
+
"diff_card_snapshots",
|
|
108
|
+
"align_cards_preserving_ids",
|
|
109
|
+
"utc_now_iso",
|
|
110
|
+
# Card wire
|
|
111
|
+
"CardProjection",
|
|
112
|
+
"parse_card_custom_payload",
|
|
113
|
+
# Goal display snapshots
|
|
114
|
+
"GOAL_DISPLAY_SNAPSHOT_SCHEMA_VERSION",
|
|
115
|
+
"GoalDisplaySnapshot",
|
|
116
|
+
"build_goal_snapshot",
|
|
117
|
+
"fold_display_cards",
|
|
118
|
+
"snapshot_from_dataclass_dict",
|
|
119
|
+
"split_cards_by_user_segments",
|
|
120
|
+
]
|
|
@@ -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"))
|