subagent-cli 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- subagent/__init__.py +7 -0
- subagent/acp_client.py +366 -0
- subagent/approval_utils.py +57 -0
- subagent/cli.py +1125 -0
- subagent/config.py +305 -0
- subagent/constants.py +21 -0
- subagent/controller_service.py +267 -0
- subagent/daemon.py +133 -0
- subagent/errors.py +24 -0
- subagent/handoff_service.py +354 -0
- subagent/hints.py +36 -0
- subagent/input_contract.py +121 -0
- subagent/launcher_service.py +30 -0
- subagent/output.py +41 -0
- subagent/paths.py +63 -0
- subagent/prompt_service.py +114 -0
- subagent/runtime_service.py +342 -0
- subagent/simple_yaml.py +202 -0
- subagent/state.py +1049 -0
- subagent/turn_service.py +558 -0
- subagent/worker_runtime.py +758 -0
- subagent/worker_service.py +362 -0
- subagent_cli-0.1.1.dist-info/METADATA +98 -0
- subagent_cli-0.1.1.dist-info/RECORD +27 -0
- subagent_cli-0.1.1.dist-info/WHEEL +4 -0
- subagent_cli-0.1.1.dist-info/entry_points.txt +3 -0
- subagent_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
subagent/cli.py
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
"""Top-level `subagent` CLI implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from .config import SubagentConfig, load_config
|
|
13
|
+
from .controller_service import (
|
|
14
|
+
attach_controller,
|
|
15
|
+
init_controller,
|
|
16
|
+
read_env_handle,
|
|
17
|
+
recover_controllers,
|
|
18
|
+
release_controller,
|
|
19
|
+
resolve_controller_id,
|
|
20
|
+
shell_env_exports,
|
|
21
|
+
)
|
|
22
|
+
from .errors import SubagentError
|
|
23
|
+
from .handoff_service import continue_worker, create_handoff
|
|
24
|
+
from .input_contract import (
|
|
25
|
+
load_input_payload,
|
|
26
|
+
read_blocks,
|
|
27
|
+
read_bool,
|
|
28
|
+
read_string,
|
|
29
|
+
read_string_list,
|
|
30
|
+
reject_duplicates,
|
|
31
|
+
)
|
|
32
|
+
from .launcher_service import probe_launcher
|
|
33
|
+
from .output import emit_error_and_exit, emit_json, ok_envelope
|
|
34
|
+
from .paths import resolve_workspace_path
|
|
35
|
+
from .prompt_service import render_prompt
|
|
36
|
+
from .state import StateStore
|
|
37
|
+
from .turn_service import approve_request, cancel_turn, send_message, wait_for_event, watch_events
|
|
38
|
+
from .worker_service import inspect_worker, list_workers, show_worker, start_worker, stop_worker
|
|
39
|
+
|
|
40
|
+
app = typer.Typer(
|
|
41
|
+
help=(
|
|
42
|
+
"subagent: protocol-agnostic worker orchestration CLI\n"
|
|
43
|
+
"If you are a manager agent, start with: `subagent prompt render --target manager`"
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
launcher_app = typer.Typer(help="Manage launcher registry from config")
|
|
47
|
+
profile_app = typer.Typer(help="Manage profile registry from config")
|
|
48
|
+
pack_app = typer.Typer(help="Manage pack registry from config")
|
|
49
|
+
prompt_app = typer.Typer(help="Render manager/worker prompts")
|
|
50
|
+
controller_app = typer.Typer(help="Manage controller ownership")
|
|
51
|
+
worker_app = typer.Typer(help="Manage worker lifecycle")
|
|
52
|
+
|
|
53
|
+
app.add_typer(launcher_app, name="launcher")
|
|
54
|
+
app.add_typer(profile_app, name="profile")
|
|
55
|
+
app.add_typer(pack_app, name="pack")
|
|
56
|
+
app.add_typer(prompt_app, name="prompt")
|
|
57
|
+
app.add_typer(controller_app, name="controller")
|
|
58
|
+
app.add_typer(worker_app, name="worker")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_config_or_exit(config_path: Path | None, *, json_output: bool) -> SubagentConfig:
|
|
62
|
+
try:
|
|
63
|
+
return load_config(config_path)
|
|
64
|
+
except SubagentError as error:
|
|
65
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _store() -> StateStore:
|
|
69
|
+
store = StateStore()
|
|
70
|
+
store.bootstrap()
|
|
71
|
+
return store
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_param_default(ctx: typer.Context, name: str) -> bool:
|
|
75
|
+
source = ctx.get_parameter_source(name)
|
|
76
|
+
return source == click.core.ParameterSource.DEFAULT
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _require_value(value: Any, *, name: str, json_output: bool) -> Any:
|
|
80
|
+
if value is None:
|
|
81
|
+
emit_error_and_exit(
|
|
82
|
+
SubagentError(
|
|
83
|
+
code="INVALID_INPUT",
|
|
84
|
+
message=f"`{name}` is required",
|
|
85
|
+
),
|
|
86
|
+
json_output=json_output,
|
|
87
|
+
)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _emit_simple_list(
|
|
92
|
+
*,
|
|
93
|
+
title: str,
|
|
94
|
+
items: list[dict[str, Any]],
|
|
95
|
+
json_output: bool,
|
|
96
|
+
event_type: str,
|
|
97
|
+
config: SubagentConfig,
|
|
98
|
+
) -> None:
|
|
99
|
+
if json_output:
|
|
100
|
+
emit_json(
|
|
101
|
+
ok_envelope(
|
|
102
|
+
event_type,
|
|
103
|
+
{
|
|
104
|
+
"items": items,
|
|
105
|
+
"count": len(items),
|
|
106
|
+
"configPath": str(config.path),
|
|
107
|
+
"configLoaded": config.loaded,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
if not items:
|
|
113
|
+
typer.echo(f"(no {title} configured)")
|
|
114
|
+
return
|
|
115
|
+
for item in items:
|
|
116
|
+
typer.echo(item["name"])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _emit_simple_show(
|
|
120
|
+
*,
|
|
121
|
+
item: dict[str, Any],
|
|
122
|
+
item_type: str,
|
|
123
|
+
json_output: bool,
|
|
124
|
+
) -> None:
|
|
125
|
+
if json_output:
|
|
126
|
+
emit_json(ok_envelope(f"{item_type}.shown", item))
|
|
127
|
+
return
|
|
128
|
+
for key, value in item.items():
|
|
129
|
+
typer.echo(f"{key}: {value}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_blocks_json_or_exit(
|
|
133
|
+
blocks_json: str | None,
|
|
134
|
+
*,
|
|
135
|
+
json_output: bool,
|
|
136
|
+
) -> list[dict[str, Any]] | None:
|
|
137
|
+
if blocks_json is None:
|
|
138
|
+
return None
|
|
139
|
+
try:
|
|
140
|
+
parsed = json.loads(blocks_json)
|
|
141
|
+
except json.JSONDecodeError as error:
|
|
142
|
+
emit_error_and_exit(
|
|
143
|
+
SubagentError(
|
|
144
|
+
code="INVALID_INPUT",
|
|
145
|
+
message="--blocks must be valid JSON",
|
|
146
|
+
details={"error": str(error)},
|
|
147
|
+
),
|
|
148
|
+
json_output=json_output,
|
|
149
|
+
)
|
|
150
|
+
if not isinstance(parsed, list):
|
|
151
|
+
emit_error_and_exit(
|
|
152
|
+
SubagentError(
|
|
153
|
+
code="INVALID_INPUT",
|
|
154
|
+
message="--blocks JSON must be a list",
|
|
155
|
+
),
|
|
156
|
+
json_output=json_output,
|
|
157
|
+
)
|
|
158
|
+
blocks: list[dict[str, Any]] = []
|
|
159
|
+
for idx, item in enumerate(parsed):
|
|
160
|
+
if not isinstance(item, dict):
|
|
161
|
+
emit_error_and_exit(
|
|
162
|
+
SubagentError(
|
|
163
|
+
code="INVALID_INPUT",
|
|
164
|
+
message=f"--blocks[{idx}] must be an object",
|
|
165
|
+
),
|
|
166
|
+
json_output=json_output,
|
|
167
|
+
)
|
|
168
|
+
blocks.append(item)
|
|
169
|
+
return blocks
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@launcher_app.command("list")
|
|
173
|
+
def launcher_list(
|
|
174
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
175
|
+
config_path: Path | None = typer.Option(
|
|
176
|
+
None,
|
|
177
|
+
"--config",
|
|
178
|
+
help="Override config path (default: ~/.config/subagent/config.yaml).",
|
|
179
|
+
),
|
|
180
|
+
) -> None:
|
|
181
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
182
|
+
items = [
|
|
183
|
+
{
|
|
184
|
+
"name": launcher.name,
|
|
185
|
+
"backendKind": launcher.backend_kind,
|
|
186
|
+
"command": launcher.command,
|
|
187
|
+
}
|
|
188
|
+
for launcher in sorted(config.launchers.values(), key=lambda x: x.name)
|
|
189
|
+
]
|
|
190
|
+
_emit_simple_list(
|
|
191
|
+
title="launchers",
|
|
192
|
+
items=items,
|
|
193
|
+
json_output=json_output,
|
|
194
|
+
event_type="launcher.listed",
|
|
195
|
+
config=config,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@launcher_app.command("show")
|
|
200
|
+
def launcher_show(
|
|
201
|
+
name: str = typer.Argument(..., help="Launcher name"),
|
|
202
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
203
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
204
|
+
) -> None:
|
|
205
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
206
|
+
launcher = config.launchers.get(name)
|
|
207
|
+
if launcher is None:
|
|
208
|
+
emit_error_and_exit(
|
|
209
|
+
SubagentError(
|
|
210
|
+
code="LAUNCHER_NOT_FOUND",
|
|
211
|
+
message=f"Launcher not found: {name}",
|
|
212
|
+
details={"name": name},
|
|
213
|
+
),
|
|
214
|
+
json_output=json_output,
|
|
215
|
+
)
|
|
216
|
+
_emit_simple_show(item=launcher.to_dict(), item_type="launcher", json_output=json_output)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@launcher_app.command("probe")
|
|
220
|
+
def launcher_probe(
|
|
221
|
+
name: str = typer.Argument(..., help="Launcher name"),
|
|
222
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
223
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
224
|
+
) -> None:
|
|
225
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
226
|
+
try:
|
|
227
|
+
payload = probe_launcher(config, name)
|
|
228
|
+
except SubagentError as error:
|
|
229
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
230
|
+
if json_output:
|
|
231
|
+
emit_json(ok_envelope("launcher.probed", payload))
|
|
232
|
+
else:
|
|
233
|
+
status = "available" if payload["available"] else "missing"
|
|
234
|
+
typer.echo(f"{payload['name']}: {status}")
|
|
235
|
+
typer.echo(f"command: {payload['command']}")
|
|
236
|
+
typer.echo(f"resolvedPath: {payload['resolvedCommandPath']}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@profile_app.command("list")
|
|
240
|
+
def profile_list(
|
|
241
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
242
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
243
|
+
) -> None:
|
|
244
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
245
|
+
items = [
|
|
246
|
+
{
|
|
247
|
+
"name": profile.name,
|
|
248
|
+
"promptLanguage": profile.prompt_language,
|
|
249
|
+
"responseLanguage": profile.response_language,
|
|
250
|
+
}
|
|
251
|
+
for profile in sorted(config.profiles.values(), key=lambda x: x.name)
|
|
252
|
+
]
|
|
253
|
+
_emit_simple_list(
|
|
254
|
+
title="profiles",
|
|
255
|
+
items=items,
|
|
256
|
+
json_output=json_output,
|
|
257
|
+
event_type="profile.listed",
|
|
258
|
+
config=config,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@profile_app.command("show")
|
|
263
|
+
def profile_show(
|
|
264
|
+
name: str = typer.Argument(..., help="Profile name"),
|
|
265
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
266
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
267
|
+
) -> None:
|
|
268
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
269
|
+
profile = config.profiles.get(name)
|
|
270
|
+
if profile is None:
|
|
271
|
+
emit_error_and_exit(
|
|
272
|
+
SubagentError(
|
|
273
|
+
code="PROFILE_NOT_FOUND",
|
|
274
|
+
message=f"Profile not found: {name}",
|
|
275
|
+
details={"name": name},
|
|
276
|
+
),
|
|
277
|
+
json_output=json_output,
|
|
278
|
+
)
|
|
279
|
+
_emit_simple_show(item=profile.to_dict(), item_type="profile", json_output=json_output)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@pack_app.command("list")
|
|
283
|
+
def pack_list(
|
|
284
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
285
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
286
|
+
) -> None:
|
|
287
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
288
|
+
items = [
|
|
289
|
+
{
|
|
290
|
+
"name": pack.name,
|
|
291
|
+
"description": pack.description,
|
|
292
|
+
}
|
|
293
|
+
for pack in sorted(config.packs.values(), key=lambda x: x.name)
|
|
294
|
+
]
|
|
295
|
+
_emit_simple_list(
|
|
296
|
+
title="packs",
|
|
297
|
+
items=items,
|
|
298
|
+
json_output=json_output,
|
|
299
|
+
event_type="pack.listed",
|
|
300
|
+
config=config,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pack_app.command("show")
|
|
305
|
+
def pack_show(
|
|
306
|
+
name: str = typer.Argument(..., help="Pack name"),
|
|
307
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
308
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
309
|
+
) -> None:
|
|
310
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
311
|
+
pack = config.packs.get(name)
|
|
312
|
+
if pack is None:
|
|
313
|
+
emit_error_and_exit(
|
|
314
|
+
SubagentError(
|
|
315
|
+
code="PACK_NOT_FOUND",
|
|
316
|
+
message=f"Pack not found: {name}",
|
|
317
|
+
details={"name": name},
|
|
318
|
+
),
|
|
319
|
+
json_output=json_output,
|
|
320
|
+
)
|
|
321
|
+
_emit_simple_show(item=pack.to_dict(), item_type="pack", json_output=json_output)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@prompt_app.command("render")
|
|
325
|
+
def prompt_render(
|
|
326
|
+
target: str = typer.Option(..., "--target", help="Prompt target: manager|worker"),
|
|
327
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile name for worker target"),
|
|
328
|
+
packs: list[str] = typer.Option([], "--pack", help="Pack names (repeatable)"),
|
|
329
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
330
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
331
|
+
) -> None:
|
|
332
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
333
|
+
try:
|
|
334
|
+
payload = render_prompt(
|
|
335
|
+
config,
|
|
336
|
+
target=target,
|
|
337
|
+
profile_name=profile,
|
|
338
|
+
pack_names=packs,
|
|
339
|
+
)
|
|
340
|
+
except SubagentError as error:
|
|
341
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
342
|
+
if json_output:
|
|
343
|
+
emit_json(ok_envelope("prompt.rendered", payload))
|
|
344
|
+
else:
|
|
345
|
+
typer.echo(payload["prompt"])
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _handle_print_env_flag(print_env: bool, json_output: bool) -> None:
|
|
349
|
+
if print_env and json_output:
|
|
350
|
+
emit_error_and_exit(
|
|
351
|
+
SubagentError(
|
|
352
|
+
code="INVALID_ARGUMENT",
|
|
353
|
+
message="--print-env cannot be combined with --json",
|
|
354
|
+
),
|
|
355
|
+
json_output=True,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@controller_app.command("init")
|
|
360
|
+
def controller_init(
|
|
361
|
+
cwd: Path = typer.Option(Path("."), "--cwd", help="Workspace root"),
|
|
362
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Controller ID override"),
|
|
363
|
+
label: str = typer.Option("default-manager", "--label", help="Controller label"),
|
|
364
|
+
print_env: bool = typer.Option(
|
|
365
|
+
False,
|
|
366
|
+
"--print-env",
|
|
367
|
+
help="Print shell exports for SUBAGENT_CTL_* variables.",
|
|
368
|
+
),
|
|
369
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
370
|
+
) -> None:
|
|
371
|
+
_handle_print_env_flag(print_env, json_output)
|
|
372
|
+
store = _store()
|
|
373
|
+
try:
|
|
374
|
+
initialized = init_controller(
|
|
375
|
+
store,
|
|
376
|
+
workspace=resolve_workspace_path(cwd),
|
|
377
|
+
controller_id=controller_id,
|
|
378
|
+
label=label,
|
|
379
|
+
)
|
|
380
|
+
except SubagentError as error:
|
|
381
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
382
|
+
|
|
383
|
+
if print_env:
|
|
384
|
+
for line in shell_env_exports(initialized.owner):
|
|
385
|
+
typer.echo(line)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
payload = initialized.to_dict()
|
|
389
|
+
if json_output:
|
|
390
|
+
emit_json(ok_envelope("controller.initialized", payload))
|
|
391
|
+
else:
|
|
392
|
+
typer.echo(f"controllerId: {payload['controllerId']}")
|
|
393
|
+
typer.echo(f"workspaceKey: {payload['workspaceKey']}")
|
|
394
|
+
typer.echo(f"epoch: {payload['owner']['epoch']}")
|
|
395
|
+
typer.echo(f"hintPath: {payload['hintPath']}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@controller_app.command("attach")
|
|
399
|
+
def controller_attach(
|
|
400
|
+
cwd: Path = typer.Option(Path("."), "--cwd", help="Workspace root"),
|
|
401
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Controller ID override"),
|
|
402
|
+
takeover: bool = typer.Option(False, "--takeover", help="Take ownership even if active owner exists"),
|
|
403
|
+
print_env: bool = typer.Option(
|
|
404
|
+
False,
|
|
405
|
+
"--print-env",
|
|
406
|
+
help="Print shell exports for SUBAGENT_CTL_* variables.",
|
|
407
|
+
),
|
|
408
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
409
|
+
) -> None:
|
|
410
|
+
_handle_print_env_flag(print_env, json_output)
|
|
411
|
+
store = _store()
|
|
412
|
+
try:
|
|
413
|
+
attached = attach_controller(
|
|
414
|
+
store,
|
|
415
|
+
workspace=resolve_workspace_path(cwd),
|
|
416
|
+
controller_id=controller_id,
|
|
417
|
+
takeover=takeover,
|
|
418
|
+
)
|
|
419
|
+
except SubagentError as error:
|
|
420
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
421
|
+
|
|
422
|
+
if print_env:
|
|
423
|
+
for line in shell_env_exports(attached.owner):
|
|
424
|
+
typer.echo(line)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
payload = attached.to_dict()
|
|
428
|
+
if json_output:
|
|
429
|
+
emit_json(ok_envelope("controller.attached", payload))
|
|
430
|
+
else:
|
|
431
|
+
typer.echo(f"controllerId: {payload['controllerId']}")
|
|
432
|
+
typer.echo(f"workspaceKey: {payload['workspaceKey']}")
|
|
433
|
+
typer.echo(f"epoch: {payload['owner']['epoch']}")
|
|
434
|
+
typer.echo(f"hintPath: {payload['hintPath']}")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@controller_app.command("status")
|
|
438
|
+
def controller_status(
|
|
439
|
+
cwd: Path = typer.Option(Path("."), "--cwd", help="Workspace root"),
|
|
440
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Controller ID override"),
|
|
441
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
442
|
+
) -> None:
|
|
443
|
+
store = _store()
|
|
444
|
+
workspace = resolve_workspace_path(cwd)
|
|
445
|
+
resolved_controller_id = resolve_controller_id(
|
|
446
|
+
store,
|
|
447
|
+
workspace,
|
|
448
|
+
explicit_controller_id=controller_id,
|
|
449
|
+
)
|
|
450
|
+
if resolved_controller_id is None:
|
|
451
|
+
payload = {
|
|
452
|
+
"workspaceKey": str(workspace),
|
|
453
|
+
"state": "dormant",
|
|
454
|
+
"controllerId": None,
|
|
455
|
+
"activeOwner": None,
|
|
456
|
+
"envHandle": read_env_handle(),
|
|
457
|
+
}
|
|
458
|
+
if json_output:
|
|
459
|
+
emit_json(ok_envelope("controller.status", payload))
|
|
460
|
+
else:
|
|
461
|
+
typer.echo("state: dormant")
|
|
462
|
+
typer.echo("controllerId: (none)")
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
payload = store.get_controller_status(resolved_controller_id)
|
|
467
|
+
except SubagentError as error:
|
|
468
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
469
|
+
|
|
470
|
+
env_handle = read_env_handle()
|
|
471
|
+
if env_handle is None:
|
|
472
|
+
payload["envHandle"] = {"present": False, "valid": False}
|
|
473
|
+
elif env_handle.get("valid") is False:
|
|
474
|
+
payload["envHandle"] = {
|
|
475
|
+
"present": True,
|
|
476
|
+
"valid": False,
|
|
477
|
+
"reason": env_handle.get("reason"),
|
|
478
|
+
}
|
|
479
|
+
else:
|
|
480
|
+
env_controller_id = str(env_handle["controllerId"])
|
|
481
|
+
env_epoch = int(env_handle["epoch"])
|
|
482
|
+
env_token = str(env_handle["token"])
|
|
483
|
+
valid = (
|
|
484
|
+
env_controller_id == resolved_controller_id
|
|
485
|
+
and store.validate_handle(resolved_controller_id, env_epoch, env_token)
|
|
486
|
+
)
|
|
487
|
+
payload["envHandle"] = {
|
|
488
|
+
"present": True,
|
|
489
|
+
"valid": valid,
|
|
490
|
+
"controllerId": env_controller_id,
|
|
491
|
+
"epoch": env_epoch,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if json_output:
|
|
495
|
+
emit_json(ok_envelope("controller.status", payload))
|
|
496
|
+
else:
|
|
497
|
+
typer.echo(f"state: {payload['state']}")
|
|
498
|
+
typer.echo(f"controllerId: {payload['controllerId']}")
|
|
499
|
+
owner = payload.get("activeOwner")
|
|
500
|
+
if owner is None:
|
|
501
|
+
typer.echo("owner: (none)")
|
|
502
|
+
else:
|
|
503
|
+
typer.echo(f"owner.epoch: {owner['epoch']}")
|
|
504
|
+
typer.echo(f"owner.pid: {owner['pid']}")
|
|
505
|
+
env_payload = payload["envHandle"]
|
|
506
|
+
typer.echo(f"env.valid: {env_payload.get('valid', False)}")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@controller_app.command("recover")
|
|
510
|
+
def controller_recover(
|
|
511
|
+
cwd: Path | None = typer.Option(None, "--cwd", help="Optional workspace filter"),
|
|
512
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
513
|
+
) -> None:
|
|
514
|
+
store = _store()
|
|
515
|
+
try:
|
|
516
|
+
payload = recover_controllers(store, workspace=cwd)
|
|
517
|
+
except SubagentError as error:
|
|
518
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
519
|
+
if json_output:
|
|
520
|
+
emit_json(ok_envelope("controller.recovered", payload))
|
|
521
|
+
else:
|
|
522
|
+
if payload["count"] == 0:
|
|
523
|
+
typer.echo("(no controllers)")
|
|
524
|
+
return
|
|
525
|
+
for item in payload["items"]:
|
|
526
|
+
typer.echo(f"{item['controllerId']}\t{item['state']}\t{item['workspaceKey']}")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@controller_app.command("release")
|
|
530
|
+
def controller_release(
|
|
531
|
+
cwd: Path = typer.Option(Path("."), "--cwd", help="Workspace root"),
|
|
532
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Controller ID override"),
|
|
533
|
+
force: bool = typer.Option(False, "--force", help="Release without validating env handle"),
|
|
534
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
535
|
+
) -> None:
|
|
536
|
+
store = _store()
|
|
537
|
+
try:
|
|
538
|
+
payload = release_controller(
|
|
539
|
+
store,
|
|
540
|
+
workspace=resolve_workspace_path(cwd),
|
|
541
|
+
controller_id=controller_id,
|
|
542
|
+
force=force,
|
|
543
|
+
)
|
|
544
|
+
except SubagentError as error:
|
|
545
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
546
|
+
if json_output:
|
|
547
|
+
emit_json(ok_envelope("controller.released", payload))
|
|
548
|
+
else:
|
|
549
|
+
typer.echo(f"controllerId: {payload['controllerId']}")
|
|
550
|
+
typer.echo(f"released: {payload['released']}")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@app.command("send")
|
|
554
|
+
def send(
|
|
555
|
+
ctx: typer.Context,
|
|
556
|
+
worker_id: str | None = typer.Option(None, "--worker", help="Worker ID"),
|
|
557
|
+
text: str | None = typer.Option(None, "--text", help="Instruction text"),
|
|
558
|
+
blocks_json: str | None = typer.Option(
|
|
559
|
+
None,
|
|
560
|
+
"--blocks",
|
|
561
|
+
help="Optional blocks payload as JSON list",
|
|
562
|
+
),
|
|
563
|
+
request_approval: bool = typer.Option(
|
|
564
|
+
False,
|
|
565
|
+
"--request-approval",
|
|
566
|
+
help="Simulate approval-required turn in local runtime",
|
|
567
|
+
),
|
|
568
|
+
input_path: str | None = typer.Option(None, "--input", help="Read command JSON from file path or '-'"),
|
|
569
|
+
debug_mode: bool = typer.Option(
|
|
570
|
+
False,
|
|
571
|
+
"--debug-mode/--no-debug-mode",
|
|
572
|
+
help="Enable local simulation mode for debug/testing.",
|
|
573
|
+
),
|
|
574
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
575
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
576
|
+
) -> None:
|
|
577
|
+
try:
|
|
578
|
+
input_payload = load_input_payload(input_path)
|
|
579
|
+
reject_duplicates(
|
|
580
|
+
input_payload,
|
|
581
|
+
flag_values={
|
|
582
|
+
"worker_id": worker_id,
|
|
583
|
+
"text": text,
|
|
584
|
+
"blocks_json": blocks_json,
|
|
585
|
+
"debug_mode": debug_mode,
|
|
586
|
+
},
|
|
587
|
+
value_is_default={
|
|
588
|
+
"worker_id": _is_param_default(ctx, "worker_id"),
|
|
589
|
+
"text": _is_param_default(ctx, "text"),
|
|
590
|
+
"blocks_json": _is_param_default(ctx, "blocks_json"),
|
|
591
|
+
"debug_mode": _is_param_default(ctx, "debug_mode"),
|
|
592
|
+
},
|
|
593
|
+
mapping={
|
|
594
|
+
"workerId": "worker_id",
|
|
595
|
+
"text": "text",
|
|
596
|
+
"blocks": "blocks_json",
|
|
597
|
+
"debugMode": "debug_mode",
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
if input_payload is not None:
|
|
601
|
+
worker_id = read_string(input_payload, "workerId") or worker_id
|
|
602
|
+
text = read_string(input_payload, "text") or text
|
|
603
|
+
blocks = read_blocks(input_payload, "blocks")
|
|
604
|
+
payload_debug_mode = read_bool(input_payload, "debugMode")
|
|
605
|
+
if payload_debug_mode is not None:
|
|
606
|
+
debug_mode = payload_debug_mode
|
|
607
|
+
else:
|
|
608
|
+
blocks = None
|
|
609
|
+
|
|
610
|
+
worker_id = _require_value(worker_id, name="worker", json_output=json_output)
|
|
611
|
+
text = _require_value(text, name="text", json_output=json_output)
|
|
612
|
+
except SubagentError as error:
|
|
613
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
614
|
+
|
|
615
|
+
store = _store()
|
|
616
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
617
|
+
if blocks is None:
|
|
618
|
+
blocks = _parse_blocks_json_or_exit(blocks_json, json_output=json_output)
|
|
619
|
+
execution_mode = "simulate" if debug_mode else "strict"
|
|
620
|
+
try:
|
|
621
|
+
payload = send_message(
|
|
622
|
+
store,
|
|
623
|
+
worker_id=worker_id,
|
|
624
|
+
text=text,
|
|
625
|
+
blocks=blocks,
|
|
626
|
+
request_approval=request_approval,
|
|
627
|
+
config=config,
|
|
628
|
+
execution_mode=execution_mode,
|
|
629
|
+
)
|
|
630
|
+
except SubagentError as error:
|
|
631
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
632
|
+
if json_output:
|
|
633
|
+
emit_json(ok_envelope("turn.accepted", payload))
|
|
634
|
+
else:
|
|
635
|
+
typer.echo(f"workerId: {payload['workerId']}")
|
|
636
|
+
typer.echo(f"turnId: {payload['turnId']}")
|
|
637
|
+
typer.echo(f"state: {payload['state']}")
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@app.command("watch")
|
|
641
|
+
def watch(
|
|
642
|
+
worker_id: str = typer.Option(..., "--worker", help="Worker ID"),
|
|
643
|
+
from_event_id: str | None = typer.Option(None, "--from-event-id", help="Cursor event ID"),
|
|
644
|
+
follow: bool = typer.Option(False, "--follow", help="Follow events for a short window"),
|
|
645
|
+
timeout_seconds: float = typer.Option(
|
|
646
|
+
1.0,
|
|
647
|
+
"--timeout-seconds",
|
|
648
|
+
min=0.1,
|
|
649
|
+
help="Polling window when --follow is enabled",
|
|
650
|
+
),
|
|
651
|
+
ndjson: bool = typer.Option(False, "--ndjson", help="Emit one normalized event per line"),
|
|
652
|
+
raw: bool = typer.Option(False, "--raw", help="Include raw payload when available"),
|
|
653
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
654
|
+
) -> None:
|
|
655
|
+
if ndjson and json_output:
|
|
656
|
+
emit_error_and_exit(
|
|
657
|
+
SubagentError(
|
|
658
|
+
code="INVALID_ARGUMENT",
|
|
659
|
+
message="--ndjson cannot be combined with --json",
|
|
660
|
+
),
|
|
661
|
+
json_output=True,
|
|
662
|
+
)
|
|
663
|
+
store = _store()
|
|
664
|
+
try:
|
|
665
|
+
events = watch_events(
|
|
666
|
+
store,
|
|
667
|
+
worker_id=worker_id,
|
|
668
|
+
from_event_id=from_event_id,
|
|
669
|
+
follow=follow,
|
|
670
|
+
timeout_seconds=timeout_seconds,
|
|
671
|
+
include_raw=raw,
|
|
672
|
+
)
|
|
673
|
+
except SubagentError as error:
|
|
674
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
675
|
+
|
|
676
|
+
ndjson_mode = ndjson or not json_output
|
|
677
|
+
if ndjson_mode:
|
|
678
|
+
for event in events:
|
|
679
|
+
emit_json(event)
|
|
680
|
+
return
|
|
681
|
+
|
|
682
|
+
if json_output:
|
|
683
|
+
emit_json(
|
|
684
|
+
ok_envelope(
|
|
685
|
+
"events.watched",
|
|
686
|
+
{
|
|
687
|
+
"workerId": worker_id,
|
|
688
|
+
"count": len(events),
|
|
689
|
+
"items": events,
|
|
690
|
+
},
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@app.command("wait")
|
|
697
|
+
def wait(
|
|
698
|
+
ctx: typer.Context,
|
|
699
|
+
worker_id: str | None = typer.Option(None, "--worker", help="Worker ID"),
|
|
700
|
+
until: str | None = typer.Option(None, "--until", help="Event type to wait for"),
|
|
701
|
+
from_event_id: str | None = typer.Option(None, "--from-event-id", help="Cursor event ID"),
|
|
702
|
+
timeout_seconds: float = typer.Option(10.0, "--timeout-seconds", min=0.1, help="Timeout in seconds"),
|
|
703
|
+
input_path: str | None = typer.Option(None, "--input", help="Read command JSON from file path or '-'"),
|
|
704
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
705
|
+
) -> None:
|
|
706
|
+
try:
|
|
707
|
+
input_payload = load_input_payload(input_path)
|
|
708
|
+
reject_duplicates(
|
|
709
|
+
input_payload,
|
|
710
|
+
flag_values={
|
|
711
|
+
"worker_id": worker_id,
|
|
712
|
+
"until": until,
|
|
713
|
+
"from_event_id": from_event_id,
|
|
714
|
+
"timeout_seconds": timeout_seconds,
|
|
715
|
+
},
|
|
716
|
+
value_is_default={
|
|
717
|
+
"worker_id": _is_param_default(ctx, "worker_id"),
|
|
718
|
+
"until": _is_param_default(ctx, "until"),
|
|
719
|
+
"from_event_id": _is_param_default(ctx, "from_event_id"),
|
|
720
|
+
"timeout_seconds": _is_param_default(ctx, "timeout_seconds"),
|
|
721
|
+
},
|
|
722
|
+
mapping={
|
|
723
|
+
"workerId": "worker_id",
|
|
724
|
+
"until": "until",
|
|
725
|
+
"fromEventId": "from_event_id",
|
|
726
|
+
"timeoutSeconds": "timeout_seconds",
|
|
727
|
+
},
|
|
728
|
+
)
|
|
729
|
+
if input_payload is not None:
|
|
730
|
+
worker_id = read_string(input_payload, "workerId") or worker_id
|
|
731
|
+
until = read_string(input_payload, "until") or until
|
|
732
|
+
from_event_id = read_string(input_payload, "fromEventId") or from_event_id
|
|
733
|
+
timeout_value = input_payload.get("timeoutSeconds")
|
|
734
|
+
if timeout_value is not None:
|
|
735
|
+
if not isinstance(timeout_value, (int, float)):
|
|
736
|
+
emit_error_and_exit(
|
|
737
|
+
SubagentError(code="INVALID_INPUT", message="`timeoutSeconds` must be a number"),
|
|
738
|
+
json_output=json_output,
|
|
739
|
+
)
|
|
740
|
+
timeout_seconds = float(timeout_value)
|
|
741
|
+
|
|
742
|
+
worker_id = _require_value(worker_id, name="worker", json_output=json_output)
|
|
743
|
+
until = _require_value(until, name="until", json_output=json_output)
|
|
744
|
+
except SubagentError as error:
|
|
745
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
746
|
+
|
|
747
|
+
store = _store()
|
|
748
|
+
try:
|
|
749
|
+
event = wait_for_event(
|
|
750
|
+
store,
|
|
751
|
+
worker_id=worker_id,
|
|
752
|
+
until=until,
|
|
753
|
+
from_event_id=from_event_id,
|
|
754
|
+
timeout_seconds=timeout_seconds,
|
|
755
|
+
)
|
|
756
|
+
except SubagentError as error:
|
|
757
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
758
|
+
if json_output:
|
|
759
|
+
emit_json(ok_envelope("event.matched", event))
|
|
760
|
+
else:
|
|
761
|
+
typer.echo(f"{event['eventId']}\t{event['type']}")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@app.command("approve")
|
|
765
|
+
def approve(
|
|
766
|
+
ctx: typer.Context,
|
|
767
|
+
worker_id: str | None = typer.Option(None, "--worker", help="Worker ID"),
|
|
768
|
+
request_id: str | None = typer.Option(None, "--request", help="Approval request ID"),
|
|
769
|
+
decision: str | None = typer.Option(None, "--decision", help="Decision alias"),
|
|
770
|
+
option_id: str | None = typer.Option(None, "--option-id", help="Approval option id"),
|
|
771
|
+
alias: str | None = typer.Option(None, "--alias", help="Approval option alias"),
|
|
772
|
+
note: str | None = typer.Option(None, "--note", help="Decision note"),
|
|
773
|
+
input_path: str | None = typer.Option(None, "--input", help="Read command JSON from file path or '-'"),
|
|
774
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
775
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
776
|
+
) -> None:
|
|
777
|
+
try:
|
|
778
|
+
input_payload = load_input_payload(input_path)
|
|
779
|
+
reject_duplicates(
|
|
780
|
+
input_payload,
|
|
781
|
+
flag_values={
|
|
782
|
+
"worker_id": worker_id,
|
|
783
|
+
"request_id": request_id,
|
|
784
|
+
"decision": decision,
|
|
785
|
+
"option_id": option_id,
|
|
786
|
+
"alias": alias,
|
|
787
|
+
"note": note,
|
|
788
|
+
},
|
|
789
|
+
value_is_default={
|
|
790
|
+
"worker_id": _is_param_default(ctx, "worker_id"),
|
|
791
|
+
"request_id": _is_param_default(ctx, "request_id"),
|
|
792
|
+
"decision": _is_param_default(ctx, "decision"),
|
|
793
|
+
"option_id": _is_param_default(ctx, "option_id"),
|
|
794
|
+
"alias": _is_param_default(ctx, "alias"),
|
|
795
|
+
"note": _is_param_default(ctx, "note"),
|
|
796
|
+
},
|
|
797
|
+
mapping={
|
|
798
|
+
"workerId": "worker_id",
|
|
799
|
+
"requestId": "request_id",
|
|
800
|
+
"decision": "decision",
|
|
801
|
+
"optionId": "option_id",
|
|
802
|
+
"alias": "alias",
|
|
803
|
+
"note": "note",
|
|
804
|
+
},
|
|
805
|
+
)
|
|
806
|
+
if input_payload is not None:
|
|
807
|
+
worker_id = read_string(input_payload, "workerId") or worker_id
|
|
808
|
+
request_id = read_string(input_payload, "requestId") or request_id
|
|
809
|
+
decision = read_string(input_payload, "decision") or decision
|
|
810
|
+
option_id = read_string(input_payload, "optionId") or option_id
|
|
811
|
+
alias = read_string(input_payload, "alias") or alias
|
|
812
|
+
note = read_string(input_payload, "note") or note
|
|
813
|
+
|
|
814
|
+
worker_id = _require_value(worker_id, name="worker", json_output=json_output)
|
|
815
|
+
request_id = _require_value(request_id, name="request", json_output=json_output)
|
|
816
|
+
except SubagentError as error:
|
|
817
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
818
|
+
|
|
819
|
+
store = _store()
|
|
820
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
821
|
+
try:
|
|
822
|
+
payload = approve_request(
|
|
823
|
+
store,
|
|
824
|
+
worker_id=worker_id,
|
|
825
|
+
request_id=request_id,
|
|
826
|
+
decision=decision,
|
|
827
|
+
option_id=option_id,
|
|
828
|
+
alias=alias,
|
|
829
|
+
note=note,
|
|
830
|
+
config=config,
|
|
831
|
+
)
|
|
832
|
+
except SubagentError as error:
|
|
833
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
834
|
+
if json_output:
|
|
835
|
+
emit_json(ok_envelope("approval.decided", payload))
|
|
836
|
+
else:
|
|
837
|
+
typer.echo(f"requestId: {payload['requestId']}")
|
|
838
|
+
typer.echo(f"decision: {payload['decision']}")
|
|
839
|
+
typer.echo(f"state: {payload['state']}")
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
@app.command("cancel")
|
|
843
|
+
def cancel(
|
|
844
|
+
worker_id: str = typer.Option(..., "--worker", help="Worker ID"),
|
|
845
|
+
reason: str | None = typer.Option(None, "--reason", help="Cancel reason"),
|
|
846
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
847
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
848
|
+
) -> None:
|
|
849
|
+
store = _store()
|
|
850
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
851
|
+
try:
|
|
852
|
+
payload = cancel_turn(
|
|
853
|
+
store,
|
|
854
|
+
worker_id=worker_id,
|
|
855
|
+
reason=reason,
|
|
856
|
+
config=config,
|
|
857
|
+
)
|
|
858
|
+
except SubagentError as error:
|
|
859
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
860
|
+
if json_output:
|
|
861
|
+
emit_json(ok_envelope("turn.canceled", payload))
|
|
862
|
+
else:
|
|
863
|
+
typer.echo(f"workerId: {payload['workerId']}")
|
|
864
|
+
typer.echo(f"state: {payload['state']}")
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
@worker_app.command("start")
|
|
868
|
+
def worker_start(
|
|
869
|
+
ctx: typer.Context,
|
|
870
|
+
launcher: str | None = typer.Option(None, "--launcher", help="Launcher name"),
|
|
871
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile name"),
|
|
872
|
+
packs: list[str] = typer.Option([], "--pack", help="Pack names (repeatable)"),
|
|
873
|
+
cwd: Path = typer.Option(Path("."), "--cwd", help="Worker working directory"),
|
|
874
|
+
label: str | None = typer.Option(None, "--label", help="Worker label"),
|
|
875
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Controller ID override"),
|
|
876
|
+
debug_mode: bool = typer.Option(
|
|
877
|
+
False,
|
|
878
|
+
"--debug-mode/--no-debug-mode",
|
|
879
|
+
help="Start worker without backend runtime (for debug/testing).",
|
|
880
|
+
),
|
|
881
|
+
input_path: str | None = typer.Option(None, "--input", help="Read command JSON from file path or '-'"),
|
|
882
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
883
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
884
|
+
) -> None:
|
|
885
|
+
try:
|
|
886
|
+
input_payload = load_input_payload(input_path)
|
|
887
|
+
reject_duplicates(
|
|
888
|
+
input_payload,
|
|
889
|
+
flag_values={
|
|
890
|
+
"launcher": launcher,
|
|
891
|
+
"profile": profile,
|
|
892
|
+
"packs": packs,
|
|
893
|
+
"cwd": str(cwd),
|
|
894
|
+
"label": label,
|
|
895
|
+
"controller_id": controller_id,
|
|
896
|
+
"debug_mode": debug_mode,
|
|
897
|
+
},
|
|
898
|
+
value_is_default={
|
|
899
|
+
"launcher": _is_param_default(ctx, "launcher"),
|
|
900
|
+
"profile": _is_param_default(ctx, "profile"),
|
|
901
|
+
"packs": _is_param_default(ctx, "packs"),
|
|
902
|
+
"cwd": _is_param_default(ctx, "cwd"),
|
|
903
|
+
"label": _is_param_default(ctx, "label"),
|
|
904
|
+
"controller_id": _is_param_default(ctx, "controller_id"),
|
|
905
|
+
"debug_mode": _is_param_default(ctx, "debug_mode"),
|
|
906
|
+
},
|
|
907
|
+
mapping={
|
|
908
|
+
"launcher": "launcher",
|
|
909
|
+
"profile": "profile",
|
|
910
|
+
"packs": "packs",
|
|
911
|
+
"cwd": "cwd",
|
|
912
|
+
"label": "label",
|
|
913
|
+
"controllerId": "controller_id",
|
|
914
|
+
"debugMode": "debug_mode",
|
|
915
|
+
},
|
|
916
|
+
)
|
|
917
|
+
if input_payload is not None:
|
|
918
|
+
launcher = read_string(input_payload, "launcher") or launcher
|
|
919
|
+
profile = read_string(input_payload, "profile") or profile
|
|
920
|
+
payload_packs = read_string_list(input_payload, "packs")
|
|
921
|
+
if payload_packs is not None:
|
|
922
|
+
packs = payload_packs
|
|
923
|
+
payload_cwd = read_string(input_payload, "cwd")
|
|
924
|
+
if payload_cwd is not None:
|
|
925
|
+
cwd = Path(payload_cwd)
|
|
926
|
+
label = read_string(input_payload, "label") or label
|
|
927
|
+
controller_id = read_string(input_payload, "controllerId") or controller_id
|
|
928
|
+
payload_debug_mode = read_bool(input_payload, "debugMode")
|
|
929
|
+
if payload_debug_mode is not None:
|
|
930
|
+
debug_mode = payload_debug_mode
|
|
931
|
+
except SubagentError as error:
|
|
932
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
933
|
+
|
|
934
|
+
store = _store()
|
|
935
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
936
|
+
try:
|
|
937
|
+
payload = start_worker(
|
|
938
|
+
store,
|
|
939
|
+
config,
|
|
940
|
+
workspace=resolve_workspace_path(cwd),
|
|
941
|
+
worker_cwd=resolve_workspace_path(cwd),
|
|
942
|
+
controller_id=controller_id,
|
|
943
|
+
launcher=launcher,
|
|
944
|
+
profile=profile,
|
|
945
|
+
packs=packs,
|
|
946
|
+
label=label,
|
|
947
|
+
debug_mode=debug_mode,
|
|
948
|
+
)
|
|
949
|
+
except SubagentError as error:
|
|
950
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
951
|
+
if json_output:
|
|
952
|
+
emit_json(ok_envelope("worker.started", payload))
|
|
953
|
+
else:
|
|
954
|
+
typer.echo(f"workerId: {payload['workerId']}")
|
|
955
|
+
typer.echo(f"controllerId: {payload['controllerId']}")
|
|
956
|
+
typer.echo(f"state: {payload['state']}")
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@worker_app.command("list")
|
|
960
|
+
def worker_list(
|
|
961
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Filter by controller ID"),
|
|
962
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
963
|
+
) -> None:
|
|
964
|
+
store = _store()
|
|
965
|
+
try:
|
|
966
|
+
items = list_workers(store, controller_id=controller_id)
|
|
967
|
+
except SubagentError as error:
|
|
968
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
969
|
+
payload = {
|
|
970
|
+
"items": items,
|
|
971
|
+
"count": len(items),
|
|
972
|
+
"controllerId": controller_id,
|
|
973
|
+
}
|
|
974
|
+
if json_output:
|
|
975
|
+
emit_json(ok_envelope("worker.listed", payload))
|
|
976
|
+
else:
|
|
977
|
+
if not items:
|
|
978
|
+
typer.echo("(no workers)")
|
|
979
|
+
return
|
|
980
|
+
for item in items:
|
|
981
|
+
typer.echo(f"{item['workerId']}\t{item['state']}\t{item['label']}")
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
@worker_app.command("show")
|
|
985
|
+
def worker_show(
|
|
986
|
+
worker_id: str = typer.Argument(..., help="Worker ID"),
|
|
987
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
988
|
+
) -> None:
|
|
989
|
+
store = _store()
|
|
990
|
+
try:
|
|
991
|
+
payload = show_worker(store, worker_id)
|
|
992
|
+
except SubagentError as error:
|
|
993
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
994
|
+
if json_output:
|
|
995
|
+
emit_json(ok_envelope("worker.shown", payload))
|
|
996
|
+
else:
|
|
997
|
+
for key, value in payload.items():
|
|
998
|
+
typer.echo(f"{key}: {value}")
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
@worker_app.command("inspect")
|
|
1002
|
+
def worker_inspect(
|
|
1003
|
+
worker_id: str = typer.Argument(..., help="Worker ID"),
|
|
1004
|
+
events_limit: int = typer.Option(20, "--events-limit", min=1, max=200, help="Number of recent events"),
|
|
1005
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
1006
|
+
) -> None:
|
|
1007
|
+
store = _store()
|
|
1008
|
+
try:
|
|
1009
|
+
payload = inspect_worker(store, worker_id, events_limit=events_limit)
|
|
1010
|
+
except SubagentError as error:
|
|
1011
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
1012
|
+
if json_output:
|
|
1013
|
+
emit_json(ok_envelope("worker.inspected", payload))
|
|
1014
|
+
else:
|
|
1015
|
+
worker = payload["worker"]
|
|
1016
|
+
typer.echo(f"workerId: {worker['workerId']}")
|
|
1017
|
+
typer.echo(f"state: {worker['state']}")
|
|
1018
|
+
typer.echo(f"recoveryState: {worker['recoveryState']}")
|
|
1019
|
+
typer.echo(f"pendingApprovals: {len(payload['pendingApprovals'])}")
|
|
1020
|
+
typer.echo(f"events: {len(payload['events'])}")
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
@worker_app.command("stop")
|
|
1024
|
+
def worker_stop(
|
|
1025
|
+
worker_id: str = typer.Argument(..., help="Worker ID"),
|
|
1026
|
+
force: bool = typer.Option(False, "--force", help="Force transition to stopped"),
|
|
1027
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
1028
|
+
) -> None:
|
|
1029
|
+
store = _store()
|
|
1030
|
+
try:
|
|
1031
|
+
payload = stop_worker(store, worker_id, force=force)
|
|
1032
|
+
except SubagentError as error:
|
|
1033
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
1034
|
+
if json_output:
|
|
1035
|
+
emit_json(ok_envelope("worker.stopped", payload))
|
|
1036
|
+
else:
|
|
1037
|
+
typer.echo(f"workerId: {payload['workerId']}")
|
|
1038
|
+
typer.echo(f"state: {payload['state']}")
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
@worker_app.command("handoff")
|
|
1042
|
+
def worker_handoff(
|
|
1043
|
+
worker_id: str = typer.Option(..., "--worker", help="Worker ID"),
|
|
1044
|
+
handoffs_dir: Path | None = typer.Option(
|
|
1045
|
+
None,
|
|
1046
|
+
"--handoffs-dir",
|
|
1047
|
+
help="Override handoff storage directory",
|
|
1048
|
+
),
|
|
1049
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
1050
|
+
) -> None:
|
|
1051
|
+
store = _store()
|
|
1052
|
+
try:
|
|
1053
|
+
payload = create_handoff(
|
|
1054
|
+
store,
|
|
1055
|
+
worker_id=worker_id,
|
|
1056
|
+
handoffs_dir=handoffs_dir,
|
|
1057
|
+
)
|
|
1058
|
+
except SubagentError as error:
|
|
1059
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
1060
|
+
if json_output:
|
|
1061
|
+
emit_json(ok_envelope("worker.handoff.ready", payload))
|
|
1062
|
+
else:
|
|
1063
|
+
typer.echo(f"workerId: {payload['workerId']}")
|
|
1064
|
+
typer.echo(f"handoffPath: {payload['handoffPath']}")
|
|
1065
|
+
typer.echo(f"checkpointPath: {payload['checkpointPath']}")
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
@worker_app.command("continue")
|
|
1069
|
+
def worker_continue(
|
|
1070
|
+
from_worker: str | None = typer.Option(None, "--from-worker", help="Source worker ID"),
|
|
1071
|
+
from_handoff: Path | None = typer.Option(None, "--from-handoff", help="Path to handoff.md"),
|
|
1072
|
+
launcher: str | None = typer.Option(None, "--launcher", help="Launcher override"),
|
|
1073
|
+
profile: str | None = typer.Option(None, "--profile", help="Profile override"),
|
|
1074
|
+
packs: list[str] = typer.Option([], "--pack", help="Pack override (repeatable)"),
|
|
1075
|
+
cwd: Path | None = typer.Option(None, "--cwd", help="Target working directory"),
|
|
1076
|
+
label: str | None = typer.Option(None, "--label", help="Target worker label"),
|
|
1077
|
+
controller_id: str | None = typer.Option(None, "--controller-id", help="Controller override"),
|
|
1078
|
+
handoffs_dir: Path | None = typer.Option(
|
|
1079
|
+
None,
|
|
1080
|
+
"--handoffs-dir",
|
|
1081
|
+
help="Override handoff storage directory",
|
|
1082
|
+
),
|
|
1083
|
+
debug_mode: bool = typer.Option(
|
|
1084
|
+
False,
|
|
1085
|
+
"--debug-mode/--no-debug-mode",
|
|
1086
|
+
help="Enable debug mode for worker startup and bootstrap turn.",
|
|
1087
|
+
),
|
|
1088
|
+
config_path: Path | None = typer.Option(None, "--config", help="Override config path."),
|
|
1089
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON envelope."),
|
|
1090
|
+
) -> None:
|
|
1091
|
+
store = _store()
|
|
1092
|
+
config = _load_config_or_exit(config_path, json_output=json_output)
|
|
1093
|
+
execution_mode = "simulate" if debug_mode else "strict"
|
|
1094
|
+
try:
|
|
1095
|
+
payload = continue_worker(
|
|
1096
|
+
store,
|
|
1097
|
+
config,
|
|
1098
|
+
from_worker=from_worker,
|
|
1099
|
+
from_handoff=from_handoff,
|
|
1100
|
+
launcher=launcher,
|
|
1101
|
+
profile=profile,
|
|
1102
|
+
packs=packs,
|
|
1103
|
+
cwd=cwd,
|
|
1104
|
+
label=label,
|
|
1105
|
+
controller_id=controller_id,
|
|
1106
|
+
handoffs_dir=handoffs_dir,
|
|
1107
|
+
debug_mode=debug_mode,
|
|
1108
|
+
execution_mode=execution_mode,
|
|
1109
|
+
)
|
|
1110
|
+
except SubagentError as error:
|
|
1111
|
+
emit_error_and_exit(error, json_output=json_output)
|
|
1112
|
+
if json_output:
|
|
1113
|
+
emit_json(ok_envelope("worker.continued", payload))
|
|
1114
|
+
else:
|
|
1115
|
+
typer.echo(f"sourceHandoffPath: {payload['sourceHandoffPath']}")
|
|
1116
|
+
typer.echo(f"workerId: {payload['worker']['workerId']}")
|
|
1117
|
+
typer.echo(f"state: {payload['worker']['state']}")
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def main() -> None:
|
|
1121
|
+
app()
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
if __name__ == "__main__":
|
|
1125
|
+
main()
|