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/turn_service.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""Turn operations over worker runtime state and event journal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .approval_utils import resolve_option
|
|
10
|
+
from .config import SubagentConfig
|
|
11
|
+
from .errors import SubagentError
|
|
12
|
+
from .runtime_service import restart_worker_runtime, runtime_request
|
|
13
|
+
from .state import (
|
|
14
|
+
WORKER_STATE_IDLE,
|
|
15
|
+
WORKER_STATE_RUNNING,
|
|
16
|
+
WORKER_STATE_WAITING_APPROVAL,
|
|
17
|
+
StateStore,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_event(event: dict[str, Any], *, include_raw: bool = False) -> dict[str, Any]:
|
|
22
|
+
payload: dict[str, Any] = {
|
|
23
|
+
"schemaVersion": "v1",
|
|
24
|
+
"eventId": event["event_id"],
|
|
25
|
+
"ts": event["ts"],
|
|
26
|
+
"workerId": event["worker_id"],
|
|
27
|
+
"type": event["event_type"],
|
|
28
|
+
"data": event["data"],
|
|
29
|
+
}
|
|
30
|
+
turn_id = event.get("turn_id")
|
|
31
|
+
if turn_id:
|
|
32
|
+
payload["turnId"] = turn_id
|
|
33
|
+
if include_raw and event.get("raw") is not None:
|
|
34
|
+
payload["raw"] = event["raw"]
|
|
35
|
+
return payload
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ensure_worker_sendable(worker: dict[str, Any]) -> None:
|
|
39
|
+
state = str(worker["state"])
|
|
40
|
+
if state != WORKER_STATE_IDLE:
|
|
41
|
+
raise SubagentError(
|
|
42
|
+
code="WORKER_BUSY",
|
|
43
|
+
message="worker has an active turn",
|
|
44
|
+
retryable=False,
|
|
45
|
+
details={"state": state},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_until_set(until: str | None) -> set[str]:
|
|
50
|
+
if until is None:
|
|
51
|
+
return set()
|
|
52
|
+
trimmed = until.strip()
|
|
53
|
+
if not trimmed or trimmed in {"*", "any"}:
|
|
54
|
+
return set()
|
|
55
|
+
return {part.strip() for part in trimmed.split(",") if part.strip()}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _begin_turn(
|
|
59
|
+
store: StateStore,
|
|
60
|
+
*,
|
|
61
|
+
worker_id: str,
|
|
62
|
+
text: str,
|
|
63
|
+
blocks: list[dict[str, Any]] | None,
|
|
64
|
+
runtime_kind: str,
|
|
65
|
+
) -> tuple[str, dict[str, Any]]:
|
|
66
|
+
turn_id = f"turn_{uuid.uuid4().hex[:10]}"
|
|
67
|
+
store.update_worker_state(worker_id, next_state=WORKER_STATE_RUNNING)
|
|
68
|
+
store.set_worker_active_turn(worker_id, turn_id)
|
|
69
|
+
|
|
70
|
+
store.append_worker_event(
|
|
71
|
+
worker_id,
|
|
72
|
+
event_type="turn.started",
|
|
73
|
+
turn_id=turn_id,
|
|
74
|
+
data={
|
|
75
|
+
"turnId": turn_id,
|
|
76
|
+
"input": {
|
|
77
|
+
"text": text,
|
|
78
|
+
"blocksCount": len(blocks or []),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
raw={"runtime": runtime_kind, "phase": "turn.started"},
|
|
82
|
+
)
|
|
83
|
+
message_event = store.append_worker_event(
|
|
84
|
+
worker_id,
|
|
85
|
+
event_type="message.sent",
|
|
86
|
+
turn_id=turn_id,
|
|
87
|
+
data={
|
|
88
|
+
"text": text,
|
|
89
|
+
"blocks": blocks or [],
|
|
90
|
+
},
|
|
91
|
+
raw={"runtime": runtime_kind, "phase": "message.sent"},
|
|
92
|
+
)
|
|
93
|
+
return turn_id, message_event
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _complete_turn(
|
|
97
|
+
store: StateStore,
|
|
98
|
+
*,
|
|
99
|
+
worker_id: str,
|
|
100
|
+
turn_id: str,
|
|
101
|
+
runtime_kind: str,
|
|
102
|
+
outcome: str = "completed",
|
|
103
|
+
details: dict[str, Any] | None = None,
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
store.update_worker_state(worker_id, next_state=WORKER_STATE_IDLE)
|
|
106
|
+
payload = {
|
|
107
|
+
"turnId": turn_id,
|
|
108
|
+
"outcome": outcome,
|
|
109
|
+
"state": WORKER_STATE_IDLE,
|
|
110
|
+
}
|
|
111
|
+
if details:
|
|
112
|
+
payload.update(details)
|
|
113
|
+
return store.append_worker_event(
|
|
114
|
+
worker_id,
|
|
115
|
+
event_type="turn.completed",
|
|
116
|
+
turn_id=turn_id,
|
|
117
|
+
data=payload,
|
|
118
|
+
raw={"runtime": runtime_kind, "phase": "turn.completed"},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _simulate_send_message(
|
|
123
|
+
store: StateStore,
|
|
124
|
+
*,
|
|
125
|
+
worker_id: str,
|
|
126
|
+
text: str,
|
|
127
|
+
blocks: list[dict[str, Any]] | None,
|
|
128
|
+
request_approval: bool,
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
turn_id, message_event = _begin_turn(
|
|
131
|
+
store,
|
|
132
|
+
worker_id=worker_id,
|
|
133
|
+
text=text,
|
|
134
|
+
blocks=blocks,
|
|
135
|
+
runtime_kind="local",
|
|
136
|
+
)
|
|
137
|
+
if request_approval:
|
|
138
|
+
request = store.create_approval_request(
|
|
139
|
+
worker_id,
|
|
140
|
+
turn_id=turn_id,
|
|
141
|
+
message=f"Approval requested for turn {turn_id}",
|
|
142
|
+
)
|
|
143
|
+
store.update_worker_state(worker_id, next_state=WORKER_STATE_WAITING_APPROVAL)
|
|
144
|
+
approval_event = store.append_worker_event(
|
|
145
|
+
worker_id,
|
|
146
|
+
event_type="approval.requested",
|
|
147
|
+
turn_id=turn_id,
|
|
148
|
+
data={
|
|
149
|
+
"requestId": request["request_id"],
|
|
150
|
+
"kind": request["kind"],
|
|
151
|
+
"message": request["message"],
|
|
152
|
+
"options": request["options"],
|
|
153
|
+
},
|
|
154
|
+
raw={"runtime": "local", "phase": "approval.requested"},
|
|
155
|
+
)
|
|
156
|
+
return {
|
|
157
|
+
"workerId": worker_id,
|
|
158
|
+
"turnId": turn_id,
|
|
159
|
+
"state": WORKER_STATE_WAITING_APPROVAL,
|
|
160
|
+
"requestId": request["request_id"],
|
|
161
|
+
"eventId": approval_event["event_id"],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
store.append_worker_event(
|
|
165
|
+
worker_id,
|
|
166
|
+
event_type="progress.message",
|
|
167
|
+
turn_id=turn_id,
|
|
168
|
+
data={
|
|
169
|
+
"role": "assistant",
|
|
170
|
+
"text": "STATUS: turn accepted and completed in local runtime.",
|
|
171
|
+
},
|
|
172
|
+
raw={"runtime": "local", "phase": "progress.message"},
|
|
173
|
+
)
|
|
174
|
+
completed_event = _complete_turn(
|
|
175
|
+
store,
|
|
176
|
+
worker_id=worker_id,
|
|
177
|
+
turn_id=turn_id,
|
|
178
|
+
runtime_kind="local",
|
|
179
|
+
)
|
|
180
|
+
return {
|
|
181
|
+
"workerId": worker_id,
|
|
182
|
+
"turnId": turn_id,
|
|
183
|
+
"state": WORKER_STATE_IDLE,
|
|
184
|
+
"eventId": completed_event["event_id"],
|
|
185
|
+
"acceptedEventId": message_event["event_id"],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _send_via_runtime(
|
|
190
|
+
store: StateStore,
|
|
191
|
+
*,
|
|
192
|
+
worker_id: str,
|
|
193
|
+
text: str,
|
|
194
|
+
blocks: list[dict[str, Any]] | None,
|
|
195
|
+
config: SubagentConfig | None,
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
turn_id, message_event = _begin_turn(
|
|
198
|
+
store,
|
|
199
|
+
worker_id=worker_id,
|
|
200
|
+
text=text,
|
|
201
|
+
blocks=blocks,
|
|
202
|
+
runtime_kind="acp-stdio",
|
|
203
|
+
)
|
|
204
|
+
try:
|
|
205
|
+
runtime_result = _runtime_request_with_restart(
|
|
206
|
+
store,
|
|
207
|
+
config=config,
|
|
208
|
+
worker_id=worker_id,
|
|
209
|
+
method="start_turn",
|
|
210
|
+
params={
|
|
211
|
+
"turnId": turn_id,
|
|
212
|
+
"text": text,
|
|
213
|
+
"blocks": blocks or [],
|
|
214
|
+
},
|
|
215
|
+
timeout_seconds=3600.0,
|
|
216
|
+
)
|
|
217
|
+
except SubagentError as error:
|
|
218
|
+
store.append_worker_event(
|
|
219
|
+
worker_id,
|
|
220
|
+
event_type="turn.failed",
|
|
221
|
+
turn_id=turn_id,
|
|
222
|
+
data={"turnId": turn_id, "error": error.to_dict()},
|
|
223
|
+
raw={"runtime": "acp-stdio", "phase": "turn.failed"},
|
|
224
|
+
)
|
|
225
|
+
store.update_worker_state(
|
|
226
|
+
worker_id,
|
|
227
|
+
next_state=WORKER_STATE_IDLE,
|
|
228
|
+
last_error=error.message,
|
|
229
|
+
)
|
|
230
|
+
raise
|
|
231
|
+
|
|
232
|
+
state = runtime_result.get("state")
|
|
233
|
+
if state == WORKER_STATE_WAITING_APPROVAL:
|
|
234
|
+
request_id = runtime_result.get("requestId")
|
|
235
|
+
if not isinstance(request_id, str):
|
|
236
|
+
raise SubagentError(
|
|
237
|
+
code="BACKEND_PROTOCOL_ERROR",
|
|
238
|
+
message="Runtime response missing approval requestId.",
|
|
239
|
+
details={"response": runtime_result},
|
|
240
|
+
)
|
|
241
|
+
latest_event = store.get_latest_worker_event(worker_id)
|
|
242
|
+
return {
|
|
243
|
+
"workerId": worker_id,
|
|
244
|
+
"turnId": turn_id,
|
|
245
|
+
"state": WORKER_STATE_WAITING_APPROVAL,
|
|
246
|
+
"requestId": request_id,
|
|
247
|
+
"eventId": latest_event["event_id"] if latest_event else None,
|
|
248
|
+
"acceptedEventId": message_event["event_id"],
|
|
249
|
+
}
|
|
250
|
+
if state != WORKER_STATE_IDLE:
|
|
251
|
+
raise SubagentError(
|
|
252
|
+
code="BACKEND_PROTOCOL_ERROR",
|
|
253
|
+
message="Runtime returned an unknown state.",
|
|
254
|
+
details={"response": runtime_result},
|
|
255
|
+
)
|
|
256
|
+
event_id = runtime_result.get("eventId")
|
|
257
|
+
if not isinstance(event_id, str):
|
|
258
|
+
latest_event = store.get_latest_worker_event(worker_id)
|
|
259
|
+
event_id = str(latest_event["event_id"]) if latest_event is not None else ""
|
|
260
|
+
return {
|
|
261
|
+
"workerId": worker_id,
|
|
262
|
+
"turnId": turn_id,
|
|
263
|
+
"state": WORKER_STATE_IDLE,
|
|
264
|
+
"eventId": event_id,
|
|
265
|
+
"acceptedEventId": message_event["event_id"],
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _runtime_request_with_restart(
|
|
270
|
+
store: StateStore,
|
|
271
|
+
*,
|
|
272
|
+
config: SubagentConfig | None,
|
|
273
|
+
worker_id: str,
|
|
274
|
+
method: str,
|
|
275
|
+
params: dict[str, Any],
|
|
276
|
+
timeout_seconds: float,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
try:
|
|
279
|
+
return runtime_request(
|
|
280
|
+
store,
|
|
281
|
+
worker_id=worker_id,
|
|
282
|
+
method=method,
|
|
283
|
+
params=params,
|
|
284
|
+
timeout_seconds=timeout_seconds,
|
|
285
|
+
)
|
|
286
|
+
except SubagentError as error:
|
|
287
|
+
if error.code != "BACKEND_UNAVAILABLE" or config is None:
|
|
288
|
+
raise
|
|
289
|
+
restart_worker_runtime(store, config, worker_id=worker_id, timeout_seconds=10.0)
|
|
290
|
+
return runtime_request(
|
|
291
|
+
store,
|
|
292
|
+
worker_id=worker_id,
|
|
293
|
+
method=method,
|
|
294
|
+
params=params,
|
|
295
|
+
timeout_seconds=timeout_seconds,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def send_message(
|
|
300
|
+
store: StateStore,
|
|
301
|
+
*,
|
|
302
|
+
worker_id: str,
|
|
303
|
+
text: str,
|
|
304
|
+
blocks: list[dict[str, Any]] | None = None,
|
|
305
|
+
request_approval: bool = False,
|
|
306
|
+
config: SubagentConfig | None = None,
|
|
307
|
+
execution_mode: str = "strict",
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
worker = store.get_worker(worker_id)
|
|
310
|
+
if worker is None:
|
|
311
|
+
raise SubagentError(
|
|
312
|
+
code="WORKER_NOT_FOUND",
|
|
313
|
+
message=f"Worker not found: {worker_id}",
|
|
314
|
+
details={"workerId": worker_id},
|
|
315
|
+
)
|
|
316
|
+
_ensure_worker_sendable(worker)
|
|
317
|
+
if execution_mode not in {"strict", "simulate"}:
|
|
318
|
+
raise SubagentError(
|
|
319
|
+
code="INVALID_ARGUMENT",
|
|
320
|
+
message=f"Unknown execution mode: {execution_mode}",
|
|
321
|
+
details={"executionMode": execution_mode},
|
|
322
|
+
)
|
|
323
|
+
if request_approval:
|
|
324
|
+
return _simulate_send_message(
|
|
325
|
+
store,
|
|
326
|
+
worker_id=worker_id,
|
|
327
|
+
text=text,
|
|
328
|
+
blocks=blocks,
|
|
329
|
+
request_approval=True,
|
|
330
|
+
)
|
|
331
|
+
if execution_mode == "simulate":
|
|
332
|
+
return _simulate_send_message(
|
|
333
|
+
store,
|
|
334
|
+
worker_id=worker_id,
|
|
335
|
+
text=text,
|
|
336
|
+
blocks=blocks,
|
|
337
|
+
request_approval=False,
|
|
338
|
+
)
|
|
339
|
+
return _send_via_runtime(
|
|
340
|
+
store,
|
|
341
|
+
worker_id=worker_id,
|
|
342
|
+
text=text,
|
|
343
|
+
blocks=blocks,
|
|
344
|
+
config=config,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def watch_events(
|
|
349
|
+
store: StateStore,
|
|
350
|
+
*,
|
|
351
|
+
worker_id: str,
|
|
352
|
+
from_event_id: str | None = None,
|
|
353
|
+
follow: bool = False,
|
|
354
|
+
timeout_seconds: float = 1.0,
|
|
355
|
+
include_raw: bool = False,
|
|
356
|
+
) -> list[dict[str, Any]]:
|
|
357
|
+
collected: list[dict[str, Any]] = []
|
|
358
|
+
cursor = from_event_id
|
|
359
|
+
if not follow:
|
|
360
|
+
events = store.list_worker_events(worker_id, from_event_id=cursor)
|
|
361
|
+
return [_normalize_event(event, include_raw=include_raw) for event in events]
|
|
362
|
+
|
|
363
|
+
deadline = time.monotonic() + timeout_seconds
|
|
364
|
+
while True:
|
|
365
|
+
events = store.list_worker_events(worker_id, from_event_id=cursor)
|
|
366
|
+
if events:
|
|
367
|
+
cursor = str(events[-1]["event_id"])
|
|
368
|
+
collected.extend(_normalize_event(event, include_raw=include_raw) for event in events)
|
|
369
|
+
if time.monotonic() >= deadline:
|
|
370
|
+
break
|
|
371
|
+
time.sleep(0.05)
|
|
372
|
+
return collected
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def wait_for_event(
|
|
376
|
+
store: StateStore,
|
|
377
|
+
*,
|
|
378
|
+
worker_id: str,
|
|
379
|
+
until: str,
|
|
380
|
+
from_event_id: str | None = None,
|
|
381
|
+
timeout_seconds: float = 10.0,
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
until_set = _parse_until_set(until)
|
|
384
|
+
deadline = time.monotonic() + timeout_seconds
|
|
385
|
+
cursor = from_event_id
|
|
386
|
+
while True:
|
|
387
|
+
events = store.list_worker_events(worker_id, from_event_id=cursor)
|
|
388
|
+
if events:
|
|
389
|
+
for event in events:
|
|
390
|
+
event_type = str(event["event_type"])
|
|
391
|
+
if not until_set or event_type in until_set:
|
|
392
|
+
return _normalize_event(event)
|
|
393
|
+
cursor = str(events[-1]["event_id"])
|
|
394
|
+
if time.monotonic() >= deadline:
|
|
395
|
+
break
|
|
396
|
+
time.sleep(0.05)
|
|
397
|
+
raise SubagentError(
|
|
398
|
+
code="WAIT_TIMEOUT",
|
|
399
|
+
message=f"No event matched `{until}` before timeout",
|
|
400
|
+
retryable=True,
|
|
401
|
+
details={
|
|
402
|
+
"workerId": worker_id,
|
|
403
|
+
"until": until,
|
|
404
|
+
"timeoutSeconds": timeout_seconds,
|
|
405
|
+
},
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def cancel_turn(
|
|
410
|
+
store: StateStore,
|
|
411
|
+
*,
|
|
412
|
+
worker_id: str,
|
|
413
|
+
reason: str | None = None,
|
|
414
|
+
config: SubagentConfig | None = None,
|
|
415
|
+
) -> dict[str, Any]:
|
|
416
|
+
worker = store.get_worker(worker_id)
|
|
417
|
+
if worker is None:
|
|
418
|
+
raise SubagentError(
|
|
419
|
+
code="WORKER_NOT_FOUND",
|
|
420
|
+
message=f"Worker not found: {worker_id}",
|
|
421
|
+
details={"workerId": worker_id},
|
|
422
|
+
)
|
|
423
|
+
state = str(worker["state"])
|
|
424
|
+
if state not in {WORKER_STATE_RUNNING, WORKER_STATE_WAITING_APPROVAL}:
|
|
425
|
+
raise SubagentError(
|
|
426
|
+
code="WORKER_NOT_RUNNING",
|
|
427
|
+
message="worker has no active turn to cancel",
|
|
428
|
+
details={"workerId": worker_id, "state": state},
|
|
429
|
+
)
|
|
430
|
+
runtime_socket = worker.get("runtime_socket")
|
|
431
|
+
if isinstance(runtime_socket, str) and runtime_socket:
|
|
432
|
+
return _runtime_request_with_restart(
|
|
433
|
+
store,
|
|
434
|
+
config=config,
|
|
435
|
+
worker_id=worker_id,
|
|
436
|
+
method="cancel_turn",
|
|
437
|
+
params={"reason": reason or "canceled by manager"},
|
|
438
|
+
timeout_seconds=120.0,
|
|
439
|
+
)
|
|
440
|
+
turn_id = worker.get("active_turn_id")
|
|
441
|
+
store.update_worker_state(worker_id, next_state="canceling")
|
|
442
|
+
canceled_event = store.append_worker_event(
|
|
443
|
+
worker_id,
|
|
444
|
+
event_type="turn.canceled",
|
|
445
|
+
turn_id=str(turn_id) if turn_id else None,
|
|
446
|
+
data={
|
|
447
|
+
"turnId": turn_id,
|
|
448
|
+
"reason": reason or "canceled by manager",
|
|
449
|
+
},
|
|
450
|
+
raw={"runtime": "local", "phase": "turn.canceled"},
|
|
451
|
+
)
|
|
452
|
+
store.update_worker_state(worker_id, next_state=WORKER_STATE_IDLE)
|
|
453
|
+
return {
|
|
454
|
+
"workerId": worker_id,
|
|
455
|
+
"state": WORKER_STATE_IDLE,
|
|
456
|
+
"eventId": canceled_event["event_id"],
|
|
457
|
+
"turnId": turn_id,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def approve_request(
|
|
462
|
+
store: StateStore,
|
|
463
|
+
*,
|
|
464
|
+
worker_id: str,
|
|
465
|
+
request_id: str,
|
|
466
|
+
decision: str | None = None,
|
|
467
|
+
option_id: str | None = None,
|
|
468
|
+
alias: str | None = None,
|
|
469
|
+
note: str | None = None,
|
|
470
|
+
config: SubagentConfig | None = None,
|
|
471
|
+
) -> dict[str, Any]:
|
|
472
|
+
worker = store.get_worker(worker_id)
|
|
473
|
+
if worker is None:
|
|
474
|
+
raise SubagentError(
|
|
475
|
+
code="WORKER_NOT_FOUND",
|
|
476
|
+
message=f"Worker not found: {worker_id}",
|
|
477
|
+
details={"workerId": worker_id},
|
|
478
|
+
)
|
|
479
|
+
runtime_socket = worker.get("runtime_socket")
|
|
480
|
+
if isinstance(runtime_socket, str) and runtime_socket:
|
|
481
|
+
return _runtime_request_with_restart(
|
|
482
|
+
store,
|
|
483
|
+
config=config,
|
|
484
|
+
worker_id=worker_id,
|
|
485
|
+
method="approve",
|
|
486
|
+
params={
|
|
487
|
+
"requestId": request_id,
|
|
488
|
+
"decision": decision,
|
|
489
|
+
"optionId": option_id,
|
|
490
|
+
"alias": alias,
|
|
491
|
+
"note": note,
|
|
492
|
+
},
|
|
493
|
+
timeout_seconds=120.0,
|
|
494
|
+
)
|
|
495
|
+
if str(worker["state"]) != WORKER_STATE_WAITING_APPROVAL:
|
|
496
|
+
raise SubagentError(
|
|
497
|
+
code="WORKER_NOT_WAITING_APPROVAL",
|
|
498
|
+
message="worker is not waiting for approval",
|
|
499
|
+
details={"workerId": worker_id, "state": worker["state"]},
|
|
500
|
+
)
|
|
501
|
+
request = store.get_approval_request(worker_id, request_id)
|
|
502
|
+
if request is None:
|
|
503
|
+
raise SubagentError(
|
|
504
|
+
code="APPROVAL_NOT_FOUND",
|
|
505
|
+
message=f"Approval request not found: {request_id}",
|
|
506
|
+
details={"workerId": worker_id, "requestId": request_id},
|
|
507
|
+
)
|
|
508
|
+
selected_option_id, selected_alias, resolved_decision = resolve_option(
|
|
509
|
+
request,
|
|
510
|
+
decision=decision,
|
|
511
|
+
option_id=option_id,
|
|
512
|
+
alias=alias,
|
|
513
|
+
)
|
|
514
|
+
decided = store.decide_approval_request(
|
|
515
|
+
worker_id,
|
|
516
|
+
request_id,
|
|
517
|
+
decision=resolved_decision,
|
|
518
|
+
selected_option_id=selected_option_id,
|
|
519
|
+
selected_alias=selected_alias,
|
|
520
|
+
note=note,
|
|
521
|
+
)
|
|
522
|
+
turn_id = request.get("turn_id")
|
|
523
|
+
store.append_worker_event(
|
|
524
|
+
worker_id,
|
|
525
|
+
event_type="approval.decided",
|
|
526
|
+
turn_id=str(turn_id) if turn_id else None,
|
|
527
|
+
data={
|
|
528
|
+
"requestId": request_id,
|
|
529
|
+
"decision": resolved_decision,
|
|
530
|
+
"optionId": selected_option_id,
|
|
531
|
+
"alias": selected_alias,
|
|
532
|
+
"note": note,
|
|
533
|
+
},
|
|
534
|
+
raw={"runtime": "local", "phase": "approval.decided"},
|
|
535
|
+
)
|
|
536
|
+
store.update_worker_state(worker_id, next_state=WORKER_STATE_RUNNING)
|
|
537
|
+
outcome = "approved" if selected_option_id in {"allow", "approve", "yes"} else "rejected"
|
|
538
|
+
completed_event = store.append_worker_event(
|
|
539
|
+
worker_id,
|
|
540
|
+
event_type="turn.completed",
|
|
541
|
+
turn_id=str(turn_id) if turn_id else None,
|
|
542
|
+
data={
|
|
543
|
+
"turnId": turn_id,
|
|
544
|
+
"outcome": outcome,
|
|
545
|
+
"state": WORKER_STATE_IDLE,
|
|
546
|
+
},
|
|
547
|
+
raw={"runtime": "local", "phase": "turn.completed"},
|
|
548
|
+
)
|
|
549
|
+
store.update_worker_state(worker_id, next_state=WORKER_STATE_IDLE)
|
|
550
|
+
return {
|
|
551
|
+
"workerId": worker_id,
|
|
552
|
+
"requestId": request_id,
|
|
553
|
+
"decision": decided["decision"],
|
|
554
|
+
"optionId": decided["selected_option_id"],
|
|
555
|
+
"alias": decided["selected_alias"],
|
|
556
|
+
"state": WORKER_STATE_IDLE,
|
|
557
|
+
"eventId": completed_event["event_id"],
|
|
558
|
+
}
|