soothe-cli 0.6.7__tar.gz → 0.6.9__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.7 → soothe_cli-0.6.9}/PKG-INFO +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +74 -13
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/loop_cmd.py +4 -4
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/daemon.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/headless.py +8 -9
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/main.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/headless/processor.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/headless/processor_state.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/essential_events.py +4 -4
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/tui_trace_log.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/step_router.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/transport/session.py +14 -6
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/turn/prepare.py +12 -12
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_app.py +3 -2
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_execution.py +3 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_model.py +57 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_module_init.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_startup.py +11 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/command_registry.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/command_router.py +2 -2
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/slash_commands.py +7 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/config.py +1 -1
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/input.py +37 -26
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/preview_limits.py +2 -2
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/sessions.py +2 -2
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/textual_adapter.py +42 -19
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/tips.py +1 -1
- soothe_cli-0.6.9/src/soothe_cli/tui/widgets/autopilot_dashboard.py +614 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/autopilot_screen.py +4 -3
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/chat_input.py +37 -5
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/messages.py +75 -7
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/welcome.py +1 -1
- soothe_cli-0.6.7/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -318
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/.gitignore +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/README.md +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/pyproject.toml +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/task_scope.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.7 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
|
@@ -10,6 +10,7 @@ import asyncio
|
|
|
10
10
|
import os
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
|
+
from collections import Counter
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import typer
|
|
@@ -124,12 +125,40 @@ def submit(
|
|
|
124
125
|
|
|
125
126
|
@app.command("status")
|
|
126
127
|
def status() -> None:
|
|
127
|
-
"""Show overall autopilot state
|
|
128
|
+
"""Show overall autopilot state and goal DAG summary."""
|
|
128
129
|
client = _require_daemon_http()
|
|
129
130
|
data = client.status()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
state = data.get("state", data.get("status", "unknown"))
|
|
132
|
+
running = data.get("running", False)
|
|
133
|
+
dreaming = data.get("dreaming", False)
|
|
134
|
+
typer.echo(f"Autopilot state: {state}")
|
|
135
|
+
typer.echo(f"Scheduling loop: {'running' if running else 'stopped'}")
|
|
136
|
+
if dreaming:
|
|
137
|
+
typer.echo("Dreaming: yes")
|
|
138
|
+
loop_pool = data.get("loop_pool")
|
|
139
|
+
if isinstance(loop_pool, dict) and loop_pool:
|
|
140
|
+
typer.echo(f"Worker pool: {loop_pool}")
|
|
141
|
+
|
|
142
|
+
jobs = client.list_jobs().get("jobs") or []
|
|
143
|
+
goals = client.list_goals().get("goals") or []
|
|
144
|
+
typer.echo(f"\nJobs (root goals): {len(jobs)}")
|
|
145
|
+
if goals:
|
|
146
|
+
counts = Counter(str(g.get("status", "pending")) for g in goals)
|
|
147
|
+
typer.echo(f"Goals in DAG: {len(goals)}")
|
|
148
|
+
for stat, count in sorted(counts.items()):
|
|
149
|
+
typer.echo(f" {stat}: {count}")
|
|
150
|
+
elif not jobs:
|
|
151
|
+
typer.echo("Goals in DAG: 0")
|
|
152
|
+
|
|
153
|
+
if jobs:
|
|
154
|
+
typer.echo("\nJobs:")
|
|
155
|
+
for j in jobs:
|
|
156
|
+
jid = str(j.get("id", "?"))
|
|
157
|
+
sid = jid[:8]
|
|
158
|
+
sstat = j.get("status", "pending")
|
|
159
|
+
sdesc = preview_first(j.get("description", ""), 50)
|
|
160
|
+
typer.echo(f" [{sid}] {sstat:10s} {sdesc}")
|
|
161
|
+
typer.echo("\nFull DAG for a job: soothe autopilot job <job_id>")
|
|
133
162
|
|
|
134
163
|
|
|
135
164
|
@app.command("list")
|
|
@@ -139,8 +168,20 @@ def list_jobs(
|
|
|
139
168
|
"""List jobs (root goals) from the daemon autopilot.
|
|
140
169
|
|
|
141
170
|
Jobs are user-submitted tasks. Subgoals created during autonomous
|
|
142
|
-
execution are not shown here; use
|
|
171
|
+
execution are not shown here; use ``goals`` or ``goal <id>`` for details.
|
|
143
172
|
"""
|
|
173
|
+
_list_jobs_impl(status_filter)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command("jobs")
|
|
177
|
+
def list_jobs_alias(
|
|
178
|
+
status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Alias for ``list`` — list root autopilot jobs."""
|
|
181
|
+
_list_jobs_impl(status_filter)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _list_jobs_impl(status_filter: str) -> None:
|
|
144
185
|
client = _require_daemon_http()
|
|
145
186
|
payload = client.list_jobs()
|
|
146
187
|
jobs = payload.get("jobs") or []
|
|
@@ -158,6 +199,29 @@ def list_jobs(
|
|
|
158
199
|
typer.echo(f" [{sid}] {sstat:10s} pri={spri:3d} {sdesc}")
|
|
159
200
|
|
|
160
201
|
|
|
202
|
+
@app.command("goals")
|
|
203
|
+
def list_goals(
|
|
204
|
+
status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
|
|
205
|
+
) -> None:
|
|
206
|
+
"""List all goals in the daemon autopilot DAG (including subgoals)."""
|
|
207
|
+
client = _require_daemon_http()
|
|
208
|
+
payload = client.list_goals()
|
|
209
|
+
goals = payload.get("goals") or []
|
|
210
|
+
if not goals:
|
|
211
|
+
typer.echo("No goals found.")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
for g in goals:
|
|
215
|
+
if status_filter and g.get("status", "") != status_filter:
|
|
216
|
+
continue
|
|
217
|
+
gid = str(g.get("id", "?"))[:8]
|
|
218
|
+
parent = g.get("parent_id")
|
|
219
|
+
parent_s = f" parent={str(parent)[:8]}" if parent else ""
|
|
220
|
+
desc = preview_first(g.get("description", ""), 50)
|
|
221
|
+
stat = g.get("status", "pending")
|
|
222
|
+
typer.echo(f" [{gid}] {stat:10s}{parent_s} {desc}")
|
|
223
|
+
|
|
224
|
+
|
|
161
225
|
def _render_dag_tree(dag: dict, root_id: str) -> None:
|
|
162
226
|
"""Render DAG as ASCII tree for job visualization."""
|
|
163
227
|
nodes = {n["id"]: n for n in dag.get("nodes", [])}
|
|
@@ -199,9 +263,9 @@ def _render_dag_tree(dag: dict, root_id: str) -> None:
|
|
|
199
263
|
|
|
200
264
|
@app.command("job")
|
|
201
265
|
def show_job(
|
|
202
|
-
job_id: str = typer.Argument(..., help="Job ID to show details and DAG."),
|
|
266
|
+
job_id: str = typer.Argument(..., help="Job ID to show details and goal DAG."),
|
|
203
267
|
) -> None:
|
|
204
|
-
"""Show job status and DAG tree visualization.
|
|
268
|
+
"""Show job status and goal DAG tree visualization.
|
|
205
269
|
|
|
206
270
|
A job is a root goal submitted by the user. This command shows
|
|
207
271
|
the job's details and the complete goal DAG under it.
|
|
@@ -211,7 +275,7 @@ def show_job(
|
|
|
211
275
|
payload = client.get_job(job_id)
|
|
212
276
|
except RuntimeError as exc:
|
|
213
277
|
typer.echo(str(exc), err=True)
|
|
214
|
-
raise typer.Exit(1)
|
|
278
|
+
raise typer.Exit(1) from exc
|
|
215
279
|
|
|
216
280
|
job = payload.get("job")
|
|
217
281
|
dag = payload.get("dag")
|
|
@@ -228,11 +292,9 @@ def show_job(
|
|
|
228
292
|
typer.echo(f"Workspace: {job['workspace']}")
|
|
229
293
|
created = job.get("created_at", "")
|
|
230
294
|
if created:
|
|
231
|
-
# Truncate timestamp for readability
|
|
232
295
|
created_short = created[:19] if len(created) > 19 else created
|
|
233
296
|
typer.echo(f"Created: {created_short}")
|
|
234
297
|
|
|
235
|
-
# DAG stats from response
|
|
236
298
|
active = payload.get("active_goals", 0)
|
|
237
299
|
completed = payload.get("completed_goals", 0)
|
|
238
300
|
total = payload.get("total_goals", 0)
|
|
@@ -240,10 +302,9 @@ def show_job(
|
|
|
240
302
|
typer.echo(f"Completed goals: {completed}")
|
|
241
303
|
typer.echo(f"Total goals: {total}")
|
|
242
304
|
|
|
243
|
-
|
|
244
|
-
typer.echo("\nDAG:")
|
|
305
|
+
typer.echo("\nGoal DAG:")
|
|
245
306
|
if dag:
|
|
246
|
-
_render_dag_tree(dag, job_id)
|
|
307
|
+
_render_dag_tree(dag, str(job.get("id") or job_id))
|
|
247
308
|
else:
|
|
248
309
|
typer.echo(" (no subgoals)")
|
|
249
310
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Loop management CLI commands for
|
|
1
|
+
"""Loop management CLI commands for StrangeLoop instances.
|
|
2
2
|
|
|
3
3
|
RFC-503: Loop-First User Experience
|
|
4
4
|
RFC-504: Loop Management CLI Commands
|
|
@@ -24,7 +24,7 @@ from soothe_cli.runtime import load_config
|
|
|
24
24
|
console = Console()
|
|
25
25
|
|
|
26
26
|
# Create loop command group
|
|
27
|
-
loop_app = typer.Typer(help="Manage
|
|
27
|
+
loop_app = typer.Typer(help="Manage StrangeLoop instances")
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _require_daemon(ws_url: str) -> None:
|
|
@@ -139,7 +139,7 @@ def list_loops(
|
|
|
139
139
|
typer.Option("--limit", "-l", help="Limit number of results."),
|
|
140
140
|
] = 20,
|
|
141
141
|
) -> None:
|
|
142
|
-
"""List all
|
|
142
|
+
"""List all StrangeLoop instances.
|
|
143
143
|
|
|
144
144
|
Examples:
|
|
145
145
|
soothe loop list
|
|
@@ -169,7 +169,7 @@ def list_loops(
|
|
|
169
169
|
return
|
|
170
170
|
|
|
171
171
|
# Render table
|
|
172
|
-
table = Table(title="
|
|
172
|
+
table = Table(title="StrangeLoops")
|
|
173
173
|
table.add_column("Loop ID", style="cyan")
|
|
174
174
|
table.add_column("Status", style="green")
|
|
175
175
|
table.add_column("Contexts", justify="right")
|
|
@@ -39,7 +39,7 @@ _CANCEL_SEND_TIMEOUT_S = 3.0
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
|
|
42
|
-
"""Return whether a daemon frame belongs to the active
|
|
42
|
+
"""Return whether a daemon frame belongs to the active StrangeLoop session."""
|
|
43
43
|
event_type = event.get("type", "")
|
|
44
44
|
if event_type not in {"status", "event"}:
|
|
45
45
|
return True
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
-
import time
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
from soothe_sdk.client import (
|
|
@@ -42,7 +41,10 @@ def run_headless(
|
|
|
42
41
|
# Auto-start daemon if not running (RFC-0013) - WebSocket RPC checks (IG-174 Phase 1)
|
|
43
42
|
async def _run_headless_pipeline() -> int:
|
|
44
43
|
"""Ensure daemon is reachable, then run the headless daemon session."""
|
|
45
|
-
|
|
44
|
+
# Check if daemon is live and ready (IG-489: wait for readiness, not just port-live)
|
|
45
|
+
daemon_live = await is_daemon_live(
|
|
46
|
+
ws_url, timeout=5.0, wait_for_ready=True, ready_timeout=30.0
|
|
47
|
+
)
|
|
46
48
|
|
|
47
49
|
if not daemon_live:
|
|
48
50
|
# Attempt cleanup if stale daemon (connection exists but daemon not responsive)
|
|
@@ -64,13 +66,10 @@ def run_headless(
|
|
|
64
66
|
stderr=subprocess.DEVNULL,
|
|
65
67
|
)
|
|
66
68
|
|
|
67
|
-
# Wait for daemon to become fully ready with timeout
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if daemon_live:
|
|
72
|
-
break
|
|
73
|
-
await asyncio.sleep(0.5)
|
|
69
|
+
# Wait for daemon to become fully ready with timeout (IG-489)
|
|
70
|
+
daemon_live = await is_daemon_live(
|
|
71
|
+
ws_url, timeout=2.0, wait_for_ready=True, ready_timeout=_DAEMON_START_WAIT_TIMEOUT
|
|
72
|
+
)
|
|
74
73
|
# Note: We don't fail here - let the connection attempt handle errors
|
|
75
74
|
# This allows tests and edge cases to proceed with mocked daemons
|
|
76
75
|
|
|
@@ -152,7 +152,7 @@ def main(
|
|
|
152
152
|
soothe -p "Research AI advances" # One-shot headless (non-TUI) query
|
|
153
153
|
soothe -p "Hello" --tui # TUI with an auto-submitted prompt
|
|
154
154
|
soothe --daemon-port 9000 loop list # Subcommands inherit global flags
|
|
155
|
-
soothe loop list # List
|
|
155
|
+
soothe loop list # List StrangeLoop instances
|
|
156
156
|
"""
|
|
157
157
|
# Handle -h/--help flag
|
|
158
158
|
if show_help:
|
|
@@ -38,7 +38,7 @@ class ProcessorState:
|
|
|
38
38
|
# Current plan state (updated on plan events)
|
|
39
39
|
current_plan: Plan | None = None
|
|
40
40
|
|
|
41
|
-
# Active
|
|
41
|
+
# Active StrangeLoop id from daemon status frames (``loop_id``)
|
|
42
42
|
loop_id: str = ""
|
|
43
43
|
|
|
44
44
|
# Internal context tracking (suppress internal LLM responses)
|
|
@@ -6,24 +6,24 @@ from typing import Final
|
|
|
6
6
|
|
|
7
7
|
GOAL_START_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
8
8
|
{
|
|
9
|
-
"soothe.cognition.
|
|
9
|
+
"soothe.cognition.strange_loop.started",
|
|
10
10
|
"soothe.cognition.plan.creating",
|
|
11
11
|
}
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
STEP_START_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
15
15
|
{
|
|
16
|
-
"soothe.cognition.
|
|
16
|
+
"soothe.cognition.strange_loop.step.started",
|
|
17
17
|
}
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
STEP_COMPLETE_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
21
21
|
{
|
|
22
|
-
"soothe.cognition.
|
|
22
|
+
"soothe.cognition.strange_loop.step.completed",
|
|
23
23
|
}
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.
|
|
26
|
+
LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.strange_loop.reasoned"
|
|
27
27
|
|
|
28
28
|
ESSENTIAL_PROGRESS_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
29
29
|
set(GOAL_START_EVENT_TYPES)
|
|
@@ -30,7 +30,7 @@ def log_tui_trace(*, tui_debug: bool, event: str, **fields: Any) -> None:
|
|
|
30
30
|
tui_debug: Whether tracing is enabled (from config).
|
|
31
31
|
event: Short event name (e.g. ``renderer.assistant_text``).
|
|
32
32
|
fields: Key/value pairs appended as ``key='value'`` (strings truncated).
|
|
33
|
-
Common fields include ``loop_id`` for the active
|
|
33
|
+
Common fields include ``loop_id`` for the active StrangeLoop id.
|
|
34
34
|
"""
|
|
35
35
|
if not tui_debug:
|
|
36
36
|
return
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Per-turn routing for
|
|
1
|
+
"""Per-turn routing for StrangeLoop steps, root tools, and subagent task namespaces.
|
|
2
2
|
|
|
3
3
|
Owns associations between execute ``step_id``, root ``tool_call_id``, LangGraph
|
|
4
4
|
subgraph ``namespace``, and ``task`` delegations. Designed for parallel execute waves
|
|
@@ -45,6 +45,7 @@ class TuiDaemonSession:
|
|
|
45
45
|
self._client = WebSocketClient(url=ws_url)
|
|
46
46
|
self._rpc_client = WebSocketClient(url=ws_url)
|
|
47
47
|
self._loop_id: str | None = None
|
|
48
|
+
self._autopilot_mode: str | None = None
|
|
48
49
|
self._read_lock = asyncio.Lock()
|
|
49
50
|
self._rpc_lock = asyncio.Lock()
|
|
50
51
|
self._rpc_connected = False
|
|
@@ -54,9 +55,14 @@ class TuiDaemonSession:
|
|
|
54
55
|
|
|
55
56
|
@property
|
|
56
57
|
def loop_id(self) -> str | None:
|
|
57
|
-
"""Active
|
|
58
|
+
"""Active StrangeLoop id for this WebSocket session."""
|
|
58
59
|
return self._loop_id
|
|
59
60
|
|
|
61
|
+
@property
|
|
62
|
+
def autopilot_mode(self) -> str | None:
|
|
63
|
+
"""Active loop Solo/Autopilot mode when known."""
|
|
64
|
+
return self._autopilot_mode
|
|
65
|
+
|
|
60
66
|
async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
|
|
61
67
|
"""Connect and bootstrap a daemon loop session."""
|
|
62
68
|
await connect_websocket_with_retries(self._client)
|
|
@@ -77,6 +83,8 @@ class TuiDaemonSession:
|
|
|
77
83
|
if status_event.get("type") == "error":
|
|
78
84
|
raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
|
|
79
85
|
self._loop_id = status_event.get("loop_id")
|
|
86
|
+
mode = status_event.get("autopilot_mode")
|
|
87
|
+
self._autopilot_mode = str(mode) if mode in ("solo", "autopilot") else None
|
|
80
88
|
return status_event
|
|
81
89
|
|
|
82
90
|
def _resolve_stream_delivery_mode(self) -> str:
|
|
@@ -100,7 +108,7 @@ class TuiDaemonSession:
|
|
|
100
108
|
return "adaptive"
|
|
101
109
|
|
|
102
110
|
async def new_loop(self) -> dict[str, Any]:
|
|
103
|
-
"""Start a new
|
|
111
|
+
"""Start a new StrangeLoop conversation."""
|
|
104
112
|
return await self._bootstrap_loop(resume_loop_id=None)
|
|
105
113
|
|
|
106
114
|
async def switch_loop(self, loop_id: str) -> dict[str, Any]:
|
|
@@ -398,7 +406,7 @@ class TuiDaemonSession:
|
|
|
398
406
|
binder that produced the original cards.
|
|
399
407
|
|
|
400
408
|
Args:
|
|
401
|
-
loop_id:
|
|
409
|
+
loop_id: StrangeLoop id.
|
|
402
410
|
|
|
403
411
|
Returns:
|
|
404
412
|
``SimpleNamespace`` with ``cards: list[dict]``, ``seq: int``,
|
|
@@ -442,14 +450,14 @@ class TuiDaemonSession:
|
|
|
442
450
|
)
|
|
443
451
|
|
|
444
452
|
async def aget_loop_state(self, loop_id: str) -> Any:
|
|
445
|
-
"""Load
|
|
453
|
+
"""Load StrangeLoop state channels from the daemon (``loop_state_get`` RPC).
|
|
446
454
|
|
|
447
455
|
Returns a namespace with a ``values`` mapping so history code can share the
|
|
448
456
|
same consumption pattern as the in-process agent snapshot, without passing
|
|
449
457
|
graph config objects over the wire.
|
|
450
458
|
|
|
451
459
|
Args:
|
|
452
|
-
loop_id:
|
|
460
|
+
loop_id: StrangeLoop id.
|
|
453
461
|
|
|
454
462
|
Returns:
|
|
455
463
|
``types.SimpleNamespace`` with ``values: dict[str, Any]``.
|
|
@@ -489,7 +497,7 @@ class TuiDaemonSession:
|
|
|
489
497
|
"""Merge partial state into the loop on the daemon host (``loop_state_update`` RPC).
|
|
490
498
|
|
|
491
499
|
Args:
|
|
492
|
-
loop_id:
|
|
500
|
+
loop_id: StrangeLoop id.
|
|
493
501
|
values: Channel updates (e.g. ``messages``) in JSON-serializable form.
|
|
494
502
|
timeout: RPC wait budget in seconds.
|
|
495
503
|
as_node: Optional LangGraph node to attribute the write to. When
|
|
@@ -11,12 +11,12 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from langchain_core.messages import ToolMessage
|
|
13
13
|
from soothe_sdk.core.events import (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
STRANGE_LOOP_COMPLETED,
|
|
15
|
+
STRANGE_LOOP_PLAN_DECISION,
|
|
16
|
+
STRANGE_LOOP_STARTED,
|
|
17
|
+
STRANGE_LOOP_STEP_COMPLETED,
|
|
18
|
+
STRANGE_LOOP_STEP_QUEUED,
|
|
19
|
+
STRANGE_LOOP_STEP_STARTED,
|
|
20
20
|
)
|
|
21
21
|
from soothe_sdk.ux.classification import classify_event_to_tier
|
|
22
22
|
from soothe_sdk.ux.loop_stream import assistant_output_phase
|
|
@@ -38,12 +38,12 @@ _MAIN_LOOP_CUSTOM_TYPES = frozenset(
|
|
|
38
38
|
{
|
|
39
39
|
STREAM_TOOL_CALL_UPDATE,
|
|
40
40
|
TOOL_CALL_UPDATES_BATCH,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
STRANGE_LOOP_STARTED,
|
|
42
|
+
STRANGE_LOOP_COMPLETED,
|
|
43
|
+
STRANGE_LOOP_PLAN_DECISION,
|
|
44
|
+
STRANGE_LOOP_STEP_STARTED,
|
|
45
|
+
STRANGE_LOOP_STEP_QUEUED,
|
|
46
|
+
STRANGE_LOOP_STEP_COMPLETED,
|
|
47
47
|
}
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -156,7 +156,7 @@ class SootheApp(
|
|
|
156
156
|
daemon_config: Loaded Soothe configuration (WebSocket URL, etc.).
|
|
157
157
|
assistant_id: Agent identifier for memory storage.
|
|
158
158
|
cwd: Current working directory to display.
|
|
159
|
-
resume_loop_id: Initial
|
|
159
|
+
resume_loop_id: Initial StrangeLoop id when attaching to an existing loop.
|
|
160
160
|
initial_prompt: Optional prompt to auto-submit when session starts.
|
|
161
161
|
initial_skill: Optional skill name to invoke when session starts.
|
|
162
162
|
mcp_server_info: MCP server metadata for the `/mcp` viewer.
|
|
@@ -174,9 +174,10 @@ class SootheApp(
|
|
|
174
174
|
|
|
175
175
|
self._cwd = str(cwd) if cwd else str(Path.cwd())
|
|
176
176
|
|
|
177
|
-
# Active
|
|
177
|
+
# Active StrangeLoop id; LangGraph stores it as configurable.thread_id.
|
|
178
178
|
# Named `_lc_loop_id` to avoid colliding with Textual's App._thread_id.
|
|
179
179
|
self._lc_loop_id = resume_loop_id
|
|
180
|
+
self._loop_autopilot_mode: str = "solo"
|
|
180
181
|
|
|
181
182
|
self._initial_prompt = initial_prompt
|
|
182
183
|
|
|
@@ -570,6 +570,7 @@ class _ExecutionMixin:
|
|
|
570
570
|
)
|
|
571
571
|
self._session_state.loop_id = new_loop_id
|
|
572
572
|
self._lc_loop_id = new_loop_id
|
|
573
|
+
self._apply_loop_autopilot_mode(status_event.get("autopilot_mode"))
|
|
573
574
|
try:
|
|
574
575
|
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
575
576
|
banner.update_loop_id(new_loop_id)
|
|
@@ -650,6 +651,8 @@ class _ExecutionMixin:
|
|
|
650
651
|
await self._submit_autopilot_job(args)
|
|
651
652
|
elif cmd == "/autopilot-dashboard":
|
|
652
653
|
await self._show_autopilot_dashboard()
|
|
654
|
+
elif cmd == "/autopilot-toggle":
|
|
655
|
+
await self._toggle_autopilot_mode()
|
|
653
656
|
elif cmd == "/mcp":
|
|
654
657
|
await self._show_mcp_viewer()
|
|
655
658
|
elif cmd == "/theme":
|
|
@@ -265,12 +265,26 @@ class _ModelMixin:
|
|
|
265
265
|
return None
|
|
266
266
|
return resp.get("servers")
|
|
267
267
|
|
|
268
|
+
def _apply_loop_autopilot_mode(self, mode: str | None) -> None:
|
|
269
|
+
"""Sync local Solo/Autopilot mode from daemon bootstrap or toggle events."""
|
|
270
|
+
if mode not in ("solo", "autopilot"):
|
|
271
|
+
return
|
|
272
|
+
self._loop_autopilot_mode = mode
|
|
273
|
+
if self._status_bar is not None:
|
|
274
|
+
label = "Autopilot" if mode == "autopilot" else "Solo"
|
|
275
|
+
self._status_bar.set_session_tip(f"Mode: {label}")
|
|
276
|
+
|
|
268
277
|
async def _show_autopilot_dashboard(self) -> None:
|
|
269
278
|
"""Show autopilot dashboard as a screen overlay."""
|
|
270
279
|
from soothe_cli.tui.widgets.autopilot_screen import AutopilotScreen
|
|
271
280
|
|
|
272
281
|
is_narrow = self.size.width < 100
|
|
273
|
-
|
|
282
|
+
mode = (
|
|
283
|
+
self._loop_autopilot_mode
|
|
284
|
+
if self._loop_autopilot_mode in ("solo", "autopilot")
|
|
285
|
+
else "solo"
|
|
286
|
+
)
|
|
287
|
+
screen = AutopilotScreen(is_narrow=is_narrow, mode=mode)
|
|
274
288
|
|
|
275
289
|
def handle_result(result: None) -> None: # noqa: ARG001
|
|
276
290
|
if self._chat_input:
|
|
@@ -278,6 +292,48 @@ class _ModelMixin:
|
|
|
278
292
|
|
|
279
293
|
self.push_screen(screen, handle_result)
|
|
280
294
|
|
|
295
|
+
async def _toggle_autopilot_mode(self) -> None:
|
|
296
|
+
"""Toggle autopilot mode (solo ↔ autopilot) via daemon RPC.
|
|
297
|
+
|
|
298
|
+
Sends command_request to daemon and updates local dashboard mode.
|
|
299
|
+
"""
|
|
300
|
+
from soothe_cli.tui.widgets.messages import ErrorMessage, UserMessage
|
|
301
|
+
|
|
302
|
+
await self._mount_message(UserMessage("/autopilot-toggle"))
|
|
303
|
+
|
|
304
|
+
lid = self._loop_id
|
|
305
|
+
if not lid:
|
|
306
|
+
await self._mount_message(ErrorMessage("No active loop for autopilot toggle"))
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if not self._client:
|
|
310
|
+
await self._mount_message(ErrorMessage("Not connected to daemon"))
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
from soothe_cli.tui.commands.command_router import handle_rpc_command
|
|
315
|
+
from soothe_cli.tui.commands.slash_commands import COMMANDS
|
|
316
|
+
|
|
317
|
+
entry = COMMANDS.get("/autopilot-toggle")
|
|
318
|
+
if not entry:
|
|
319
|
+
await self._mount_message(ErrorMessage("Command /autopilot-toggle not found"))
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
await handle_rpc_command(
|
|
323
|
+
entry,
|
|
324
|
+
"autopilot-toggle",
|
|
325
|
+
None,
|
|
326
|
+
self._console,
|
|
327
|
+
self._client,
|
|
328
|
+
loop_id=lid,
|
|
329
|
+
)
|
|
330
|
+
new_mode = "solo" if self._loop_autopilot_mode == "autopilot" else "autopilot"
|
|
331
|
+
self._apply_loop_autopilot_mode(new_mode)
|
|
332
|
+
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
logger.exception("Autopilot toggle failed")
|
|
335
|
+
await self._mount_message(ErrorMessage(f"Failed to toggle autopilot: {exc}"))
|
|
336
|
+
|
|
281
337
|
async def _submit_autopilot_job(self, task: str) -> None:
|
|
282
338
|
"""Submit an autopilot job via HTTP REST (like CLI `soothe autopilot run`).
|
|
283
339
|
|
|
@@ -355,7 +355,7 @@ class AppResult:
|
|
|
355
355
|
"""Exit code (0 for success, non-zero for error)."""
|
|
356
356
|
|
|
357
357
|
loop_id: str | None
|
|
358
|
-
"""The final
|
|
358
|
+
"""The final StrangeLoop id at shutdown (may change if the user switched loops)."""
|
|
359
359
|
|
|
360
360
|
session_stats: SessionStats = field(default_factory=SessionStats)
|
|
361
361
|
"""Cumulative usage stats across all turns in the session."""
|
|
@@ -442,7 +442,10 @@ class _StartupMixin:
|
|
|
442
442
|
ws_url = websocket_url_from_config(self._daemon_config)
|
|
443
443
|
|
|
444
444
|
# Check daemon status via WebSocket RPC (IG-174 Phase 1)
|
|
445
|
-
|
|
445
|
+
# Wait for daemon to be fully ready, not just port-live (IG-489)
|
|
446
|
+
daemon_live = await is_daemon_live(
|
|
447
|
+
ws_url, timeout=5.0, wait_for_ready=True, ready_timeout=30.0
|
|
448
|
+
)
|
|
446
449
|
|
|
447
450
|
if not daemon_live:
|
|
448
451
|
# CLI does NOT control daemon start/stop per architectural separation (IG-174/IG-175)
|
|
@@ -475,6 +478,13 @@ class _StartupMixin:
|
|
|
475
478
|
if self._session_state is not None:
|
|
476
479
|
self._session_state.loop_id = status_loop_id
|
|
477
480
|
|
|
481
|
+
autopilot_mode = event.status_event.get("autopilot_mode")
|
|
482
|
+
if autopilot_mode not in ("solo", "autopilot") and event.session is not None:
|
|
483
|
+
autopilot_mode = getattr(event.session, "autopilot_mode", None)
|
|
484
|
+
self._apply_loop_autopilot_mode(
|
|
485
|
+
str(autopilot_mode) if autopilot_mode in ("solo", "autopilot") else None
|
|
486
|
+
)
|
|
487
|
+
|
|
478
488
|
try:
|
|
479
489
|
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
480
490
|
banner.set_connected(self._mcp_tool_count)
|
|
@@ -97,7 +97,7 @@ COMMANDS: tuple[SlashCommand, ...] = (
|
|
|
97
97
|
),
|
|
98
98
|
SlashCommand(
|
|
99
99
|
name="/resume",
|
|
100
|
-
description="Browse and resume
|
|
100
|
+
description="Browse and resume StrangeLoop instances",
|
|
101
101
|
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
102
102
|
hidden_keywords="continue history sessions loops",
|
|
103
103
|
),
|
|
@@ -50,7 +50,7 @@ def validate_command(
|
|
|
50
50
|
entry: Command registry entry
|
|
51
51
|
command: Command name
|
|
52
52
|
query: Query parameter (if present)
|
|
53
|
-
loop_id: Active
|
|
53
|
+
loop_id: Active StrangeLoop id for this session
|
|
54
54
|
|
|
55
55
|
Returns:
|
|
56
56
|
Tuple of (is_valid, error_message)
|
|
@@ -245,7 +245,7 @@ async def handle_routing_command(
|
|
|
245
245
|
|
|
246
246
|
For routing commands that map to a configured subagent id (e.g. ``/research``, ``/explore``),
|
|
247
247
|
sets the WebSocket ``preferred_subagent`` field so the daemon merges a subagent hint into
|
|
248
|
-
|
|
248
|
+
StrangeLoop (IG-349). Other routing commands (e.g. ``/plan``) are sent as plain text unchanged.
|
|
249
249
|
|
|
250
250
|
Args:
|
|
251
251
|
cmd_input: Full command input (e.g., "/research topic summary")
|
|
@@ -272,6 +272,13 @@ COMMANDS: dict[str, dict[str, Any]] = {
|
|
|
272
272
|
"requires_loop": True,
|
|
273
273
|
"handler": show_autopilot_dashboard,
|
|
274
274
|
},
|
|
275
|
+
"/autopilot-toggle": {
|
|
276
|
+
"location": "daemon",
|
|
277
|
+
"type": "rpc",
|
|
278
|
+
"daemon_command": "autopilot_toggle",
|
|
279
|
+
"description": "Toggle autopilot mode (solo ↔ autopilot)",
|
|
280
|
+
"requires_loop": True,
|
|
281
|
+
},
|
|
275
282
|
# Daemon routing commands (3)
|
|
276
283
|
"/plan": {"location": "daemon", "type": "routing", "description": "Trigger plan mode"},
|
|
277
284
|
"/tacitus": {
|
|
@@ -578,7 +578,7 @@ def build_stream_config(
|
|
|
578
578
|
* Including the SDK version here ensures it survives the merge.
|
|
579
579
|
|
|
580
580
|
Args:
|
|
581
|
-
loop_id: Active
|
|
581
|
+
loop_id: Active StrangeLoop id (stored under LangGraph ``configurable.thread_id``).
|
|
582
582
|
assistant_id: The agent/assistant identifier, if any.
|
|
583
583
|
sandbox_type: Sandbox provider name for trace metadata, or `None` if no
|
|
584
584
|
sandbox is active.
|