soothe-cli 0.6.16__tar.gz → 0.6.17__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_cli-0.6.16 → soothe_cli-0.6.17}/PKG-INFO +1 -1
- soothe_cli-0.6.17/src/soothe_cli/runtime/__init__.py +17 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/parse/message_processing.py +0 -6
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/parse/tool_call_resolution.py +32 -6
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/parse/tool_result.py +0 -2
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/state/step_router.py +68 -8
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_messages_mixin.py +26 -9
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/config.py +0 -141
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/media_utils.py +0 -20
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/model_config.py +0 -10
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/textual_adapter.py +499 -125
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/unicode_security.py +0 -38
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/chat_input.py +1 -1
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/file_change_preview.py +122 -34
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/__init__.py +2 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/cognition_step.py +74 -110
- soothe_cli-0.6.17/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +552 -0
- soothe_cli-0.6.17/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +122 -0
- soothe_cli-0.6.16/src/soothe_cli/runtime/__init__.py +0 -82
- soothe_cli-0.6.16/src/soothe_cli/runtime/parse/_utils.py +0 -14
- soothe_cli-0.6.16/src/soothe_cli/runtime/parse/tool_message_format.py +0 -17
- soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -947
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/.gitignore +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/README.md +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/pyproject.toml +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Daemon event processing and display state for the CLI (source of truth for TUI).
|
|
2
|
+
|
|
3
|
+
Bridges daemon events/messages to in-memory state. ``soothe_cli.tui`` owns widgets
|
|
4
|
+
and layout only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from soothe_cli.config.loader import load_config
|
|
8
|
+
from soothe_cli.config.logging_setup import setup_logging
|
|
9
|
+
from soothe_cli.runtime.headless.processor import EventProcessor
|
|
10
|
+
from soothe_cli.runtime.headless.processor_state import ProcessorState
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"EventProcessor",
|
|
14
|
+
"ProcessorState",
|
|
15
|
+
"load_config",
|
|
16
|
+
"setup_logging",
|
|
17
|
+
]
|
|
@@ -16,17 +16,14 @@ from soothe_sdk.display.message_processing import (
|
|
|
16
16
|
_resolve_pending_lookup_tool_name,
|
|
17
17
|
accumulate_tool_call_chunks,
|
|
18
18
|
coerce_tool_call_args_to_dict,
|
|
19
|
-
coerce_tool_call_entry_to_dict,
|
|
20
19
|
extract_tool_args_dict,
|
|
21
20
|
extract_tool_brief,
|
|
22
21
|
finalize_pending_tool_call,
|
|
23
22
|
ingest_tool_call_stream_state,
|
|
24
23
|
normalize_tool_calls_list,
|
|
25
24
|
richest_pending_args_for_lookup,
|
|
26
|
-
seed_pending_tool_calls_from_message,
|
|
27
25
|
tool_calls_have_any_arg_dict,
|
|
28
26
|
tool_ids_touched_by_stream_message,
|
|
29
|
-
tool_lookup_step_id,
|
|
30
27
|
try_parse_pending_tool_call_args,
|
|
31
28
|
)
|
|
32
29
|
|
|
@@ -36,16 +33,13 @@ __all__ = [
|
|
|
36
33
|
"_resolve_pending_lookup_tool_name",
|
|
37
34
|
"accumulate_tool_call_chunks",
|
|
38
35
|
"coerce_tool_call_args_to_dict",
|
|
39
|
-
"coerce_tool_call_entry_to_dict",
|
|
40
36
|
"extract_tool_args_dict",
|
|
41
37
|
"extract_tool_brief",
|
|
42
38
|
"finalize_pending_tool_call",
|
|
43
39
|
"ingest_tool_call_stream_state",
|
|
44
40
|
"normalize_tool_calls_list",
|
|
45
41
|
"richest_pending_args_for_lookup",
|
|
46
|
-
"seed_pending_tool_calls_from_message",
|
|
47
42
|
"tool_calls_have_any_arg_dict",
|
|
48
43
|
"tool_ids_touched_by_stream_message",
|
|
49
|
-
"tool_lookup_step_id",
|
|
50
44
|
"try_parse_pending_tool_call_args",
|
|
51
45
|
]
|
{soothe_cli-0.6.16 → soothe_cli-0.6.17}/src/soothe_cli/runtime/parse/tool_call_resolution.py
RENAMED
|
@@ -17,7 +17,15 @@ from collections.abc import Mapping
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
-
from soothe_sdk.ux.
|
|
20
|
+
from soothe_sdk.ux.execute_namespace import (
|
|
21
|
+
is_root_execute_namespace_key,
|
|
22
|
+
is_step_level_execute_namespace_key,
|
|
23
|
+
)
|
|
24
|
+
from soothe_sdk.ux.task_namespace import (
|
|
25
|
+
TaskScope,
|
|
26
|
+
parse_unified_tool_call_id,
|
|
27
|
+
row_key_for_subgraph_tool,
|
|
28
|
+
)
|
|
21
29
|
|
|
22
30
|
from soothe_cli.runtime.parse.message_processing import (
|
|
23
31
|
extract_tool_args_dict,
|
|
@@ -40,15 +48,32 @@ def tool_args_meaningful(raw: Any) -> bool:
|
|
|
40
48
|
|
|
41
49
|
|
|
42
50
|
def is_execute_step_namespace(ns_key: tuple[str, ...]) -> bool:
|
|
43
|
-
"""True for CoreAgent execute root namespace (``execute:{run_id}``), not nested ``tools:`` subgraphs."""
|
|
44
|
-
|
|
45
|
-
return False
|
|
46
|
-
return str(ns_key[0] or "").startswith("execute:")
|
|
51
|
+
"""True for CoreAgent execute root namespace (``execute:{run_id}``), not nested ``/N`` or ``tools:`` subgraphs."""
|
|
52
|
+
return is_root_execute_namespace_key(ns_key)
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
def is_step_card_tool_scope(*, ns_key: tuple[str, ...]) -> bool:
|
|
50
56
|
"""True when tool activity belongs on the step card as main execute-graph tools."""
|
|
51
|
-
return ns_key == () or
|
|
57
|
+
return ns_key == () or is_step_level_execute_namespace_key(ns_key)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_tool_result_row_key(
|
|
61
|
+
*,
|
|
62
|
+
ns_key: tuple[str, ...],
|
|
63
|
+
tool_call_id: str,
|
|
64
|
+
task_scope: TaskScope | None = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Row key for completing a tool row from a streamed ``ToolMessage``.
|
|
67
|
+
|
|
68
|
+
Execute-graph namespaces (``()`` or ``execute:…``) keep the unified ``s:`` id.
|
|
69
|
+
Subagent ``tools:…`` namespaces remap via :func:`row_key_for_subgraph_tool`.
|
|
70
|
+
"""
|
|
71
|
+
sid = str(tool_call_id or "").strip()
|
|
72
|
+
if not sid:
|
|
73
|
+
return ""
|
|
74
|
+
if is_step_card_tool_scope(ns_key=ns_key):
|
|
75
|
+
return sid
|
|
76
|
+
return row_key_for_subgraph_tool(ns_key, sid, task_scope=task_scope)
|
|
52
77
|
|
|
53
78
|
|
|
54
79
|
def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
|
|
@@ -546,6 +571,7 @@ __all__ = [
|
|
|
546
571
|
"merge_tool_display_args",
|
|
547
572
|
"resolve_stream_tool_name",
|
|
548
573
|
"resolve_tool_invocations_for_display",
|
|
574
|
+
"resolve_tool_result_row_key",
|
|
549
575
|
"should_ingest_tool_for_step_stats",
|
|
550
576
|
"tool_args_meaningful",
|
|
551
577
|
]
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from soothe_sdk.display.tool_result import (
|
|
6
|
-
ToolResultPayload,
|
|
7
6
|
extract_tool_result_payload,
|
|
8
7
|
infer_tool_output_suggests_error,
|
|
9
8
|
)
|
|
10
9
|
|
|
11
10
|
__all__ = [
|
|
12
|
-
"ToolResultPayload",
|
|
13
11
|
"extract_tool_result_payload",
|
|
14
12
|
"infer_tool_output_suggests_error",
|
|
15
13
|
]
|
|
@@ -165,6 +165,47 @@ class StepTaskRouter:
|
|
|
165
165
|
if sid:
|
|
166
166
|
self.active_step_ids.add(sid)
|
|
167
167
|
|
|
168
|
+
def maybe_promote_step_to_running(
|
|
169
|
+
self,
|
|
170
|
+
step_w: StepWidget,
|
|
171
|
+
tool_call_id: str,
|
|
172
|
+
*,
|
|
173
|
+
step_cards: dict[str, StepWidget],
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Promote a pending step card to running only when it is executing (RFC-628).
|
|
176
|
+
|
|
177
|
+
Pre-mounted future steps must stay ``pending`` until ``step.started`` even when
|
|
178
|
+
tools are stamped with their unified step id. The pre-``step.started`` race for
|
|
179
|
+
the active step is allowed only while no sibling step card is already running.
|
|
180
|
+
"""
|
|
181
|
+
if getattr(step_w, "_status", "") != "pending":
|
|
182
|
+
return
|
|
183
|
+
step_id = str(getattr(step_w, "_step_id", "") or "").strip()
|
|
184
|
+
if not step_id:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
tcid = str(tool_call_id or "").strip()
|
|
188
|
+
parsed_sid, _, _, _ = parse_unified_tool_call_id(tcid)
|
|
189
|
+
if parsed_sid and parsed_sid != step_id:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if step_id in self.active_step_ids:
|
|
193
|
+
allowed = True
|
|
194
|
+
elif self.active_step_ids:
|
|
195
|
+
allowed = False
|
|
196
|
+
else:
|
|
197
|
+
allowed = not any(
|
|
198
|
+
getattr(w, "_status", "") == "running"
|
|
199
|
+
and str(getattr(w, "_step_id", "") or "").strip() != step_id
|
|
200
|
+
for w in step_cards.values()
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not allowed:
|
|
204
|
+
return
|
|
205
|
+
promote = getattr(step_w, "promote_to_running_if_pending", None)
|
|
206
|
+
if callable(promote):
|
|
207
|
+
promote()
|
|
208
|
+
|
|
168
209
|
def on_step_completed(self, step_id: str) -> None:
|
|
169
210
|
"""Drop step from the active set and spawn registry."""
|
|
170
211
|
sid = step_id.strip()
|
|
@@ -353,6 +394,11 @@ class StepTaskRouter:
|
|
|
353
394
|
item.args,
|
|
354
395
|
raw_args=item.raw_args,
|
|
355
396
|
)
|
|
397
|
+
self.maybe_promote_step_to_running(
|
|
398
|
+
step_w,
|
|
399
|
+
item.tool_call_id,
|
|
400
|
+
step_cards=step_cards,
|
|
401
|
+
)
|
|
356
402
|
tool_to_step[item.tool_call_id] = step_w
|
|
357
403
|
existing = tool_display_by_call_id.get(item.tool_call_id)
|
|
358
404
|
if existing is None:
|
|
@@ -394,6 +440,8 @@ class StepTaskRouter:
|
|
|
394
440
|
parent: ParentWidget,
|
|
395
441
|
scope: TaskScope,
|
|
396
442
|
tool_to_step: dict[str, ParentWidget],
|
|
443
|
+
*,
|
|
444
|
+
step_cards: dict[str, StepWidget] | None = None,
|
|
397
445
|
) -> bool:
|
|
398
446
|
"""Register one subgraph tool row on an already-resolved parent step card."""
|
|
399
447
|
if _is_task_metadata_subgraph_tool(item):
|
|
@@ -432,7 +480,6 @@ class StepTaskRouter:
|
|
|
432
480
|
resolved_args = parsed
|
|
433
481
|
update(row_id, resolved_args)
|
|
434
482
|
else:
|
|
435
|
-
parent_task_id = str(scope[0]).strip()
|
|
436
483
|
resolved_args = dict(item.args or {})
|
|
437
484
|
from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
|
|
438
485
|
|
|
@@ -446,9 +493,10 @@ class StepTaskRouter:
|
|
|
446
493
|
item.tool_name,
|
|
447
494
|
resolved_args,
|
|
448
495
|
raw_args=item.raw_args,
|
|
449
|
-
parent_tool_call_id=parent_task_id or None,
|
|
450
496
|
)
|
|
451
497
|
tool_to_step[row_id] = parent
|
|
498
|
+
if step_cards is not None:
|
|
499
|
+
self.maybe_promote_step_to_running(parent, row_id, step_cards=step_cards)
|
|
452
500
|
return True
|
|
453
501
|
|
|
454
502
|
def try_route_subgraph_tool(
|
|
@@ -522,7 +570,13 @@ class StepTaskRouter:
|
|
|
522
570
|
return False
|
|
523
571
|
pending_key = _subgraph_pending_key(ns_key, item.lookup_id)
|
|
524
572
|
self._pending_subgraph_tools.pop(pending_key, None)
|
|
525
|
-
return self._ingest_subgraph_tool_on_parent(
|
|
573
|
+
return self._ingest_subgraph_tool_on_parent(
|
|
574
|
+
item,
|
|
575
|
+
parent,
|
|
576
|
+
scope,
|
|
577
|
+
tool_to_step,
|
|
578
|
+
step_cards=step_cards,
|
|
579
|
+
)
|
|
526
580
|
|
|
527
581
|
def route_pending_subgraph_tools(
|
|
528
582
|
self,
|
|
@@ -575,7 +629,13 @@ class StepTaskRouter:
|
|
|
575
629
|
if parent is None:
|
|
576
630
|
still[key] = item
|
|
577
631
|
continue
|
|
578
|
-
if self._ingest_subgraph_tool_on_parent(
|
|
632
|
+
if self._ingest_subgraph_tool_on_parent(
|
|
633
|
+
item,
|
|
634
|
+
parent,
|
|
635
|
+
scope,
|
|
636
|
+
tool_to_step,
|
|
637
|
+
step_cards=step_cards,
|
|
638
|
+
):
|
|
579
639
|
routed += 1
|
|
580
640
|
else:
|
|
581
641
|
still[key] = item
|
|
@@ -586,15 +646,15 @@ class StepTaskRouter:
|
|
|
586
646
|
"""Snapshot of subgraph tools still awaiting parent resolution."""
|
|
587
647
|
return list(self._pending_subgraph_tools.values())
|
|
588
648
|
|
|
649
|
+
def discard_pending_subgraph_tool(self, ns_key: tuple[str, ...], lookup_id: str) -> None:
|
|
650
|
+
"""Drop a buffered subgraph tool after routing it to a SubAgent card."""
|
|
651
|
+
self._pending_subgraph_tools.pop(_subgraph_pending_key(ns_key, lookup_id), None)
|
|
652
|
+
|
|
589
653
|
@property
|
|
590
654
|
def pending_main_tool_count(self) -> int:
|
|
591
655
|
"""Number of root tools still awaiting step card routing."""
|
|
592
656
|
return len(self._pending_main_tools)
|
|
593
657
|
|
|
594
|
-
def clear_step_tool_bindings(self, step_id: str) -> None:
|
|
595
|
-
"""No-op: step routing uses unified tool_call_id encoding only."""
|
|
596
|
-
_ = step_id
|
|
597
|
-
|
|
598
658
|
|
|
599
659
|
__all__ = [
|
|
600
660
|
"ParentWidget",
|
|
@@ -393,25 +393,42 @@ class _MessagesMixin:
|
|
|
393
393
|
copy_selection_to_clipboard(self, notify_if_empty=True)
|
|
394
394
|
|
|
395
395
|
def action_quit_or_interrupt(self) -> None:
|
|
396
|
-
"""Handle Ctrl+C - interrupt agent or quit on double press.
|
|
396
|
+
"""Handle Ctrl+C - clear input, interrupt agent, or quit on double press.
|
|
397
397
|
|
|
398
|
-
Priority order:
|
|
399
|
-
1. If
|
|
400
|
-
2. If
|
|
401
|
-
|
|
402
|
-
|
|
398
|
+
Priority order when task is running (agent/shell):
|
|
399
|
+
1. If input has pending text, clear it (first Ctrl+C)
|
|
400
|
+
2. If input is empty, interrupt the running task (second Ctrl+C)
|
|
401
|
+
|
|
402
|
+
Priority order when idle:
|
|
403
|
+
1. If double press (quit_pending), quit
|
|
404
|
+
2. Otherwise clear pending input and show quit hint
|
|
403
405
|
|
|
404
406
|
Note: Copying selected text is bound to Ctrl+Y (`action_copy_selection`)
|
|
405
407
|
so Ctrl+C is reserved for interrupt/quit only.
|
|
406
408
|
"""
|
|
407
|
-
#
|
|
409
|
+
# Check if input has pending content (text, mode, or completion)
|
|
410
|
+
has_pending_input = self._chat_input and (
|
|
411
|
+
self._chat_input.value.strip()
|
|
412
|
+
or self._chat_input.mode != "normal"
|
|
413
|
+
or self._chat_input._current_suggestions
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# If shell command is running: clear input first, then kill shell
|
|
408
417
|
if self._shell_running and self._shell_worker:
|
|
418
|
+
if has_pending_input:
|
|
419
|
+
self._chat_input.clear_input()
|
|
420
|
+
self._quit_pending = False
|
|
421
|
+
return
|
|
409
422
|
self._cancel_worker(self._shell_worker)
|
|
410
423
|
self._quit_pending = False
|
|
411
424
|
return
|
|
412
425
|
|
|
413
|
-
# If agent is running
|
|
426
|
+
# If agent is running: clear input first, then interrupt
|
|
414
427
|
if self._agent_running and self._agent_worker:
|
|
428
|
+
if has_pending_input:
|
|
429
|
+
self._chat_input.clear_input()
|
|
430
|
+
self._quit_pending = False
|
|
431
|
+
return
|
|
415
432
|
if self._daemon_session is not None:
|
|
416
433
|
self.run_worker(
|
|
417
434
|
self._interrupt_daemon_agent_turn(),
|
|
@@ -430,7 +447,7 @@ class _MessagesMixin:
|
|
|
430
447
|
self._arm_quit_pending("Ctrl+C")
|
|
431
448
|
|
|
432
449
|
def _arm_quit_pending(self, shortcut: str) -> None:
|
|
433
|
-
"""Set the pending-quit flag, clear
|
|
450
|
+
"""Set the pending-quit flag, clear pending input, and show a matching hint.
|
|
434
451
|
|
|
435
452
|
Args:
|
|
436
453
|
shortcut: The key chord to show in the quit hint.
|
|
@@ -7,7 +7,6 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
-
import shlex
|
|
11
10
|
import sys
|
|
12
11
|
import threading
|
|
13
12
|
from dataclasses import dataclass
|
|
@@ -499,13 +498,6 @@ def get_banner() -> str:
|
|
|
499
498
|
return banner
|
|
500
499
|
|
|
501
500
|
|
|
502
|
-
MAX_ARG_LENGTH = 150
|
|
503
|
-
"""Character limit for tool argument values in the UI.
|
|
504
|
-
|
|
505
|
-
Longer values are truncated with an ellipsis by `truncate_value`
|
|
506
|
-
in `tool_display`.
|
|
507
|
-
"""
|
|
508
|
-
|
|
509
501
|
config: RunnableConfig = {
|
|
510
502
|
"recursion_limit": 1000,
|
|
511
503
|
}
|
|
@@ -1426,87 +1418,6 @@ def contains_dangerous_patterns(command: str) -> bool:
|
|
|
1426
1418
|
return bool(re.search(r"(?<![&])&(?![&])", command))
|
|
1427
1419
|
|
|
1428
1420
|
|
|
1429
|
-
def is_shell_command_allowed(command: str, allow_list: list[str] | None) -> bool:
|
|
1430
|
-
"""Check if a shell command is in the allow-list.
|
|
1431
|
-
|
|
1432
|
-
The allow-list matches against the first token of the command (the executable
|
|
1433
|
-
name). This allows read-only commands like ls, cat, grep, etc. to be
|
|
1434
|
-
auto-approved.
|
|
1435
|
-
|
|
1436
|
-
When `allow_list` is the `SHELL_ALLOW_ALL` sentinel, all non-empty commands
|
|
1437
|
-
are approved unconditionally — dangerous pattern checks are skipped.
|
|
1438
|
-
|
|
1439
|
-
SECURITY: For regular allow-lists, this function rejects commands containing
|
|
1440
|
-
dangerous shell patterns (command substitution, redirects, process
|
|
1441
|
-
substitution, etc.) BEFORE parsing, to prevent injection attacks that could
|
|
1442
|
-
bypass the allow-list.
|
|
1443
|
-
|
|
1444
|
-
Args:
|
|
1445
|
-
command: The full shell command to check.
|
|
1446
|
-
allow_list: List of allowed command names (e.g., `["ls", "cat", "grep"]`),
|
|
1447
|
-
the `SHELL_ALLOW_ALL` sentinel to allow any command, or `None`.
|
|
1448
|
-
|
|
1449
|
-
Returns:
|
|
1450
|
-
`True` if the command is allowed, `False` otherwise.
|
|
1451
|
-
"""
|
|
1452
|
-
if not allow_list or not command or not command.strip():
|
|
1453
|
-
return False
|
|
1454
|
-
|
|
1455
|
-
# SHELL_ALLOW_ALL sentinel — skip pattern and token checks
|
|
1456
|
-
if isinstance(allow_list, _ShellAllowAll):
|
|
1457
|
-
return True
|
|
1458
|
-
|
|
1459
|
-
# SECURITY: Check for dangerous patterns BEFORE any parsing
|
|
1460
|
-
# This prevents injection attacks like: ls "$(rm -rf /)"
|
|
1461
|
-
if contains_dangerous_patterns(command):
|
|
1462
|
-
return False
|
|
1463
|
-
|
|
1464
|
-
allow_set = set(allow_list)
|
|
1465
|
-
|
|
1466
|
-
# Extract the first command token
|
|
1467
|
-
# Handle pipes and other shell operators by checking each command in the pipeline
|
|
1468
|
-
# Split by compound operators first (&&, ||), then single-char operators (|, ;).
|
|
1469
|
-
# Note: standalone & (background) is blocked by contains_dangerous_patterns above.
|
|
1470
|
-
segments = re.split(r"&&|\|\||[|;]", command)
|
|
1471
|
-
|
|
1472
|
-
# Track if we found at least one valid command
|
|
1473
|
-
found_command = False
|
|
1474
|
-
|
|
1475
|
-
for raw_segment in segments:
|
|
1476
|
-
segment = raw_segment.strip()
|
|
1477
|
-
if not segment:
|
|
1478
|
-
continue
|
|
1479
|
-
|
|
1480
|
-
try:
|
|
1481
|
-
# Try to parse as shell command to extract the executable name
|
|
1482
|
-
tokens = shlex.split(segment)
|
|
1483
|
-
if tokens:
|
|
1484
|
-
found_command = True
|
|
1485
|
-
cmd_name = tokens[0]
|
|
1486
|
-
# Check if this command is in the allow set
|
|
1487
|
-
if cmd_name not in allow_set:
|
|
1488
|
-
return False
|
|
1489
|
-
except ValueError:
|
|
1490
|
-
# If we can't parse it, be conservative and require approval
|
|
1491
|
-
return False
|
|
1492
|
-
|
|
1493
|
-
# All segments are allowed (and we found at least one command)
|
|
1494
|
-
return found_command
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
def get_default_coding_instructions() -> str:
|
|
1498
|
-
"""Get the default coding agent instructions.
|
|
1499
|
-
|
|
1500
|
-
These are the immutable base instructions that cannot be modified by the agent.
|
|
1501
|
-
Long-term memory (AGENTS.md) is handled separately by the middleware.
|
|
1502
|
-
|
|
1503
|
-
Returns:
|
|
1504
|
-
The default agent instructions as a string.
|
|
1505
|
-
"""
|
|
1506
|
-
default_prompt_path = Path(__file__).parent / "default_agent_prompt.md"
|
|
1507
|
-
return default_prompt_path.read_text()
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
1421
|
def detect_provider(model_name: str) -> str | None:
|
|
1511
1422
|
"""Auto-detect provider from model name.
|
|
1512
1423
|
|
|
@@ -2027,58 +1938,6 @@ def create_model(
|
|
|
2027
1938
|
)
|
|
2028
1939
|
|
|
2029
1940
|
|
|
2030
|
-
def validate_model_capabilities(model: BaseChatModel, model_name: str) -> None:
|
|
2031
|
-
"""Validate that the model has required capabilities for `Soothe`.
|
|
2032
|
-
|
|
2033
|
-
Checks the model's profile (if available) to ensure it supports tool calling, which
|
|
2034
|
-
is required for agent functionality. Issues warnings for models without profiles or
|
|
2035
|
-
with limited context windows.
|
|
2036
|
-
|
|
2037
|
-
Args:
|
|
2038
|
-
model: The instantiated model to validate.
|
|
2039
|
-
model_name: Model name for error/warning messages.
|
|
2040
|
-
|
|
2041
|
-
Note:
|
|
2042
|
-
This validation is best-effort. Models without profiles will pass with
|
|
2043
|
-
a warning. Calls `sys.exit(1)` if the model's profile explicitly
|
|
2044
|
-
indicates `tool_calling=False`.
|
|
2045
|
-
"""
|
|
2046
|
-
console = _get_console()
|
|
2047
|
-
profile = getattr(model, "profile", None)
|
|
2048
|
-
|
|
2049
|
-
if profile is None:
|
|
2050
|
-
# Model doesn't have profile data - warn but allow
|
|
2051
|
-
console.print(
|
|
2052
|
-
f"[dim][yellow]Note:[/yellow] No capability profile for "
|
|
2053
|
-
f"'{model_name}'. Cannot verify tool calling support.[/dim]"
|
|
2054
|
-
)
|
|
2055
|
-
return
|
|
2056
|
-
|
|
2057
|
-
if not isinstance(profile, dict):
|
|
2058
|
-
return
|
|
2059
|
-
|
|
2060
|
-
# Check required capability: tool_calling
|
|
2061
|
-
tool_calling = profile.get("tool_calling")
|
|
2062
|
-
if tool_calling is False:
|
|
2063
|
-
console.print(
|
|
2064
|
-
f"[bold red]Error:[/bold red] Model '{model_name}' does not support tool calling."
|
|
2065
|
-
)
|
|
2066
|
-
console.print(
|
|
2067
|
-
"\nDeep Agents requires tool calling for agent functionality. "
|
|
2068
|
-
"Please choose a model that supports tool calling."
|
|
2069
|
-
)
|
|
2070
|
-
console.print("\nSee MODELS.md for supported models.")
|
|
2071
|
-
sys.exit(1)
|
|
2072
|
-
|
|
2073
|
-
# Warn about potentially limited context (< 8k tokens)
|
|
2074
|
-
max_input_tokens = profile.get("max_input_tokens")
|
|
2075
|
-
if max_input_tokens and max_input_tokens < 8000: # noqa: PLR2004 # Model context window default
|
|
2076
|
-
console.print(
|
|
2077
|
-
f"[dim][yellow]Warning:[/yellow] Model '{model_name}' has limited context "
|
|
2078
|
-
f"({max_input_tokens:,} tokens). Agent performance may be affected.[/dim]"
|
|
2079
|
-
)
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
1941
|
def _get_console() -> Console:
|
|
2083
1942
|
"""Return the lazily-initialized global `Console` instance.
|
|
2084
1943
|
|
|
@@ -9,7 +9,6 @@ import shutil
|
|
|
9
9
|
|
|
10
10
|
# S404: subprocess needed for clipboard access via pngpaste/osascript
|
|
11
11
|
import subprocess # noqa: S404
|
|
12
|
-
import sys
|
|
13
12
|
import tempfile
|
|
14
13
|
from dataclasses import dataclass
|
|
15
14
|
from typing import TYPE_CHECKING
|
|
@@ -118,25 +117,6 @@ class VideoData:
|
|
|
118
117
|
)
|
|
119
118
|
|
|
120
119
|
|
|
121
|
-
def get_clipboard_image() -> ImageData | None:
|
|
122
|
-
"""Attempt to read an image from the system clipboard.
|
|
123
|
-
|
|
124
|
-
Supports macOS via `pngpaste` or `osascript`.
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
ImageData if an image is found, None otherwise.
|
|
128
|
-
"""
|
|
129
|
-
if sys.platform == "darwin":
|
|
130
|
-
return _get_macos_clipboard_image()
|
|
131
|
-
logger.warning(
|
|
132
|
-
"Clipboard image paste is not supported on %s. "
|
|
133
|
-
"Only macOS is currently supported. "
|
|
134
|
-
"You can still attach images by dragging and dropping file paths.",
|
|
135
|
-
sys.platform,
|
|
136
|
-
)
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
|
|
140
120
|
def get_image_from_path(path: pathlib.Path) -> ImageData | None:
|
|
141
121
|
"""Read and encode an image file from disk.
|
|
142
122
|
|
|
@@ -381,16 +381,6 @@ def save_default_model(model_spec: ModelSpec) -> None:
|
|
|
381
381
|
pass
|
|
382
382
|
|
|
383
383
|
|
|
384
|
-
def save_recent_model(model_spec: ModelSpec) -> None:
|
|
385
|
-
"""Save model to recent models list.
|
|
386
|
-
|
|
387
|
-
Args:
|
|
388
|
-
model_spec: Model specification to add to recent models.
|
|
389
|
-
"""
|
|
390
|
-
# Stub - should maintain recent models list in SootheConfig
|
|
391
|
-
pass
|
|
392
|
-
|
|
393
|
-
|
|
394
384
|
def clear_default_model() -> None:
|
|
395
385
|
"""Clear saved default model preference."""
|
|
396
386
|
# Stub
|