delegate-agent-cli 0.11.0__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.
- delegate_agent/__init__.py +5 -0
- delegate_agent/archived_logs.py +44 -0
- delegate_agent/argv_builders.py +423 -0
- delegate_agent/argv_utils.py +36 -0
- delegate_agent/capability_commands.py +99 -0
- delegate_agent/cli.py +987 -0
- delegate_agent/cli_parser.py +1627 -0
- delegate_agent/command_errors.py +29 -0
- delegate_agent/command_help.py +1264 -0
- delegate_agent/config.py +1117 -0
- delegate_agent/config_commands.py +143 -0
- delegate_agent/constants.py +50 -0
- delegate_agent/describe_payload.py +1018 -0
- delegate_agent/errors.py +32 -0
- delegate_agent/git_utils.py +180 -0
- delegate_agent/harness_events.py +719 -0
- delegate_agent/inspection_commands.py +104 -0
- delegate_agent/isolation.py +316 -0
- delegate_agent/json_types.py +18 -0
- delegate_agent/log_output.py +78 -0
- delegate_agent/private_io.py +109 -0
- delegate_agent/profile_commands.py +42 -0
- delegate_agent/profile_guard.py +116 -0
- delegate_agent/profiles.py +389 -0
- delegate_agent/prompt_instructions.py +39 -0
- delegate_agent/prompt_transport.py +12 -0
- delegate_agent/reasoning.py +916 -0
- delegate_agent/redaction.py +193 -0
- delegate_agent/rendering.py +573 -0
- delegate_agent/request_build.py +1681 -0
- delegate_agent/request_models.py +191 -0
- delegate_agent/retention.py +321 -0
- delegate_agent/run_metadata.py +78 -0
- delegate_agent/run_output_commands.py +645 -0
- delegate_agent/run_registry.py +747 -0
- delegate_agent/run_status.py +300 -0
- delegate_agent/runner.py +1830 -0
- delegate_agent/safe_workspace.py +821 -0
- delegate_agent/snapshot_view.py +229 -0
- delegate_agent/wait_cancel_commands.py +559 -0
- delegate_agent/worktree_commands.py +218 -0
- delegate_agent/worktree_execution.py +654 -0
- delegate_agent/worktree_gc.py +529 -0
- delegate_agent/worktree_mgmt.py +782 -0
- delegate_agent/worktree_records.py +232 -0
- delegate_agent/worktree_remove.py +547 -0
- delegate_agent/worktree_summary.py +242 -0
- delegate_agent/wsl.py +65 -0
- delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
- delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
- delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
- delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
- delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
- delegate_agent_cli-0.11.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from delegate_agent import archived_logs
|
|
8
|
+
from delegate_agent.json_types import JsonObject
|
|
9
|
+
|
|
10
|
+
LARGE_LOG_WARN_MIB = 50
|
|
11
|
+
# 1 << 20 == 1 MiB == run_registry.BYTES_PER_MIB. Inlined so this module needs no
|
|
12
|
+
# import-time run_registry reference, which would otherwise create an import-order
|
|
13
|
+
# cycle (run_registry re-exports this module after defining BYTES_PER_MIB).
|
|
14
|
+
LARGE_LOG_WARN_BYTES = LARGE_LOG_WARN_MIB * (1 << 20)
|
|
15
|
+
DEFAULT_RUNS_LIMIT = 20
|
|
16
|
+
STATUS_RUNNING = "running"
|
|
17
|
+
STATUS_SUCCEEDED = "succeeded"
|
|
18
|
+
STATUS_FAILED = "failed"
|
|
19
|
+
STATUS_CANCELLED = "cancelled"
|
|
20
|
+
STATUS_STALE = "stale"
|
|
21
|
+
STATUS_UNKNOWN = "unknown"
|
|
22
|
+
TERMINAL_STATUSES = frozenset({STATUS_SUCCEEDED, STATUS_FAILED, STATUS_CANCELLED})
|
|
23
|
+
STATUS_FILTER_ACTIVE = "active"
|
|
24
|
+
STATUS_FILTER_RECENT = "recent"
|
|
25
|
+
STATUS_FILTER_RUNNING = "running"
|
|
26
|
+
STATUS_FILTER_STALE = "stale"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_UNSET = object()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def large_log_warnings(stdout_bytes: int, stderr_bytes: int) -> list[str]:
|
|
33
|
+
warnings: list[str] = []
|
|
34
|
+
if stdout_bytes > LARGE_LOG_WARN_BYTES:
|
|
35
|
+
warnings.append(
|
|
36
|
+
f"{run_registry.STDOUT_LOG} > {LARGE_LOG_WARN_MIB} MiB ({stdout_bytes} bytes)"
|
|
37
|
+
)
|
|
38
|
+
if stderr_bytes > LARGE_LOG_WARN_BYTES:
|
|
39
|
+
warnings.append(
|
|
40
|
+
f"{run_registry.STDERR_LOG} > {LARGE_LOG_WARN_MIB} MiB ({stderr_bytes} bytes)"
|
|
41
|
+
)
|
|
42
|
+
return warnings
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def process_alive(pid: int | None) -> bool | None:
|
|
46
|
+
if pid is None:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
os.kill(pid, 0)
|
|
50
|
+
except ProcessLookupError:
|
|
51
|
+
return False
|
|
52
|
+
except PermissionError:
|
|
53
|
+
return True
|
|
54
|
+
except OSError:
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def effective_status(state: JsonObject | None) -> str:
|
|
60
|
+
return status_fields(state)["effectiveStatus"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def raw_status(state: JsonObject | None) -> str:
|
|
64
|
+
if not state:
|
|
65
|
+
return STATUS_UNKNOWN
|
|
66
|
+
status = state.get("status")
|
|
67
|
+
if not isinstance(status, str) or not status:
|
|
68
|
+
return STATUS_UNKNOWN
|
|
69
|
+
return status
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def status_fields(state: JsonObject | None) -> JsonObject:
|
|
73
|
+
# Probe pid liveness once so effectiveStatus and staleReason cannot disagree
|
|
74
|
+
# when the process exits between two separate probes.
|
|
75
|
+
raw = raw_status(state)
|
|
76
|
+
effective = raw
|
|
77
|
+
reason: str | None = None
|
|
78
|
+
if raw == STATUS_RUNNING:
|
|
79
|
+
pid = state.get("pid") if state else None
|
|
80
|
+
if not isinstance(pid, int) or isinstance(pid, bool) or pid < 1:
|
|
81
|
+
effective, reason = STATUS_STALE, "missing_pid"
|
|
82
|
+
elif process_alive(pid) is False:
|
|
83
|
+
effective, reason = STATUS_STALE, "dead_pid"
|
|
84
|
+
fields: JsonObject = {
|
|
85
|
+
"rawStatus": raw,
|
|
86
|
+
"effectiveStatus": effective,
|
|
87
|
+
"status": effective,
|
|
88
|
+
}
|
|
89
|
+
if reason is not None:
|
|
90
|
+
fields["staleReason"] = reason
|
|
91
|
+
return fields
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def stale_next_actions(alias_or_run_id: str, *, cwd: str | None = None) -> list[str]:
|
|
95
|
+
return [
|
|
96
|
+
run_registry.snapshot_command(alias_or_run_id, cwd=cwd),
|
|
97
|
+
run_registry.run_output_command(alias_or_run_id, completion_report=True, cwd=cwd),
|
|
98
|
+
f"{run_registry.run_output_command(alias_or_run_id, cwd=cwd)} --stderr --tail 100",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def log_byte_sizes(registry_root: Path, run_id: str) -> tuple[int, int]:
|
|
103
|
+
run_path = run_registry.run_directory(registry_root, run_id)
|
|
104
|
+
stdout_bytes = 0
|
|
105
|
+
stderr_bytes = 0
|
|
106
|
+
stdout_path = run_path / run_registry.STDOUT_LOG
|
|
107
|
+
stderr_path = run_path / run_registry.STDERR_LOG
|
|
108
|
+
if stdout_path.exists():
|
|
109
|
+
stdout_bytes = stdout_path.stat().st_size
|
|
110
|
+
if stderr_path.exists():
|
|
111
|
+
stderr_bytes = stderr_path.stat().st_size
|
|
112
|
+
return stdout_bytes, stderr_bytes
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def raw_logs_archived(registry_root: Path, run_id: str) -> bool:
|
|
116
|
+
return archived_logs.archive_path(registry_root, run_id).exists()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def effective_log_byte_sizes(
|
|
120
|
+
registry_root: Path,
|
|
121
|
+
run_id: str,
|
|
122
|
+
state: JsonObject | None = _UNSET, # type: ignore[assignment]
|
|
123
|
+
) -> tuple[int, int]:
|
|
124
|
+
run_path = run_registry.run_directory(registry_root, run_id)
|
|
125
|
+
stdout_path = run_path / run_registry.STDOUT_LOG
|
|
126
|
+
stderr_path = run_path / run_registry.STDERR_LOG
|
|
127
|
+
if stdout_path.exists() or stderr_path.exists():
|
|
128
|
+
return log_byte_sizes(registry_root, run_id)
|
|
129
|
+
if raw_logs_archived(registry_root, run_id):
|
|
130
|
+
if state is _UNSET:
|
|
131
|
+
state = run_registry.load_run_state(registry_root, run_id)
|
|
132
|
+
state_sizes = archived_logs.state_log_byte_sizes(state)
|
|
133
|
+
if state_sizes is not None:
|
|
134
|
+
return state_sizes
|
|
135
|
+
return archived_logs.archive_log_byte_sizes(
|
|
136
|
+
archived_logs.archive_path(registry_root, run_id),
|
|
137
|
+
stdout_log=run_registry.STDOUT_LOG,
|
|
138
|
+
stderr_log=run_registry.STDERR_LOG,
|
|
139
|
+
)
|
|
140
|
+
return 0, 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def activity_timestamp(
|
|
144
|
+
state: JsonObject | None,
|
|
145
|
+
manifest: JsonObject | None,
|
|
146
|
+
run_id: str | None = None,
|
|
147
|
+
) -> str:
|
|
148
|
+
if state:
|
|
149
|
+
for key in ("finishedAt", "lastActivityAt", "startedAt"):
|
|
150
|
+
value = state.get(key)
|
|
151
|
+
if isinstance(value, str) and value:
|
|
152
|
+
return value
|
|
153
|
+
if manifest:
|
|
154
|
+
started = manifest.get("startedAt")
|
|
155
|
+
if isinstance(started, str) and started:
|
|
156
|
+
return started
|
|
157
|
+
if run_id:
|
|
158
|
+
return run_registry.timestamp_from_run_id(run_id)
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def activity_datetime(
|
|
163
|
+
state: JsonObject | None,
|
|
164
|
+
manifest: JsonObject | None,
|
|
165
|
+
run_id: str | None = None,
|
|
166
|
+
) -> datetime | None:
|
|
167
|
+
return run_registry.parse_utc_timestamp(activity_timestamp(state, manifest, run_id))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_run_summary(
|
|
171
|
+
registry_root: Path,
|
|
172
|
+
run_id: str,
|
|
173
|
+
index_entry: JsonObject,
|
|
174
|
+
) -> JsonObject:
|
|
175
|
+
state = run_registry.load_run_state_or_none(registry_root, run_id)
|
|
176
|
+
manifest = run_registry.load_run_manifest_or_none(registry_root, run_id)
|
|
177
|
+
source_cwd = _source_workspace(registry_root, index_entry, state, manifest)
|
|
178
|
+
|
|
179
|
+
stdout_bytes, stderr_bytes = effective_log_byte_sizes(registry_root, run_id, state)
|
|
180
|
+
alias = index_entry.get("alias")
|
|
181
|
+
harness = index_entry.get("harness")
|
|
182
|
+
handle = alias if isinstance(alias, str) else run_id
|
|
183
|
+
# Merge state-persisted warnings into the runs-table warnings array the same
|
|
184
|
+
# way snapshot_view does, deduping to avoid repeating the same warning across
|
|
185
|
+
# channels.
|
|
186
|
+
warnings: list[str] = list(large_log_warnings(stdout_bytes, stderr_bytes))
|
|
187
|
+
if isinstance(state, dict):
|
|
188
|
+
state_warnings = state.get("warnings")
|
|
189
|
+
if isinstance(state_warnings, list):
|
|
190
|
+
for warning in state_warnings:
|
|
191
|
+
if isinstance(warning, str) and warning not in warnings:
|
|
192
|
+
warnings.append(warning)
|
|
193
|
+
summary: JsonObject = {
|
|
194
|
+
"runId": run_id,
|
|
195
|
+
"alias": alias if isinstance(alias, str) else None,
|
|
196
|
+
"harness": harness if isinstance(harness, str) else None,
|
|
197
|
+
"group": index_entry.get("group") if isinstance(index_entry.get("group"), str) else None,
|
|
198
|
+
"stdoutBytes": stdout_bytes,
|
|
199
|
+
"stderrBytes": stderr_bytes,
|
|
200
|
+
"warnings": warnings,
|
|
201
|
+
"activityAt": activity_timestamp(state, manifest),
|
|
202
|
+
}
|
|
203
|
+
if manifest:
|
|
204
|
+
for key in ("modelAlias", "modelResolved", "terminalEvent", "terminalStatus"):
|
|
205
|
+
value = manifest.get(key)
|
|
206
|
+
if value is not None:
|
|
207
|
+
summary[key] = value
|
|
208
|
+
if state:
|
|
209
|
+
for key in (
|
|
210
|
+
"terminalEvent",
|
|
211
|
+
"terminalStatus",
|
|
212
|
+
"failureReason",
|
|
213
|
+
"pgid",
|
|
214
|
+
"completionReportWritten",
|
|
215
|
+
"completionReportSource",
|
|
216
|
+
"resultQuality",
|
|
217
|
+
):
|
|
218
|
+
value = state.get(key)
|
|
219
|
+
if value is not None:
|
|
220
|
+
summary[key] = value
|
|
221
|
+
summary.update(status_fields(state))
|
|
222
|
+
if summary.get("effectiveStatus") == STATUS_STALE:
|
|
223
|
+
summary["nextActions"] = stale_next_actions(handle, cwd=source_cwd)
|
|
224
|
+
if state and isinstance(state.get("current"), str):
|
|
225
|
+
summary["current"] = state["current"]
|
|
226
|
+
if isinstance(alias, str):
|
|
227
|
+
summary["snapshotCommand"] = run_registry.snapshot_command(alias, cwd=source_cwd)
|
|
228
|
+
|
|
229
|
+
# Isolation metadata: detect persistent worktree runs.
|
|
230
|
+
worktree_status = None
|
|
231
|
+
if state and isinstance(state.get("worktreeStatus"), str):
|
|
232
|
+
worktree_status = state["worktreeStatus"]
|
|
233
|
+
summary["worktreeStatus"] = worktree_status
|
|
234
|
+
summary["isolationLifecycle"] = "persistent"
|
|
235
|
+
if manifest:
|
|
236
|
+
execution_cwd = manifest.get("executionCwd")
|
|
237
|
+
if isinstance(execution_cwd, str) and execution_cwd:
|
|
238
|
+
summary["executionCwd"] = execution_cwd
|
|
239
|
+
source_git_root = manifest.get("sourceGitRoot")
|
|
240
|
+
if isinstance(source_git_root, str) and source_git_root:
|
|
241
|
+
summary["sourceGitRoot"] = source_git_root
|
|
242
|
+
|
|
243
|
+
return summary
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _source_workspace(
|
|
247
|
+
registry_root: Path,
|
|
248
|
+
index_entry: JsonObject,
|
|
249
|
+
state: JsonObject | None,
|
|
250
|
+
manifest: JsonObject | None,
|
|
251
|
+
) -> str:
|
|
252
|
+
for source in (manifest, state, index_entry):
|
|
253
|
+
if not source:
|
|
254
|
+
continue
|
|
255
|
+
cwd = source.get("cwd")
|
|
256
|
+
if isinstance(cwd, str) and cwd:
|
|
257
|
+
return cwd
|
|
258
|
+
return str(registry_root.parent)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def list_run_summaries(
|
|
262
|
+
registry_root: Path,
|
|
263
|
+
index: JsonObject,
|
|
264
|
+
*,
|
|
265
|
+
active: bool = False,
|
|
266
|
+
status_filter: str | None = None,
|
|
267
|
+
harness: str | None = None,
|
|
268
|
+
group: str | None = None,
|
|
269
|
+
limit: int = DEFAULT_RUNS_LIMIT,
|
|
270
|
+
) -> list[JsonObject]:
|
|
271
|
+
if limit < 1:
|
|
272
|
+
raise ValueError("limit must be at least 1")
|
|
273
|
+
summaries: list[JsonObject] = []
|
|
274
|
+
for run_id, entry in index.get("runs", {}).items():
|
|
275
|
+
if not isinstance(entry, dict):
|
|
276
|
+
continue
|
|
277
|
+
entry_harness = entry.get("harness")
|
|
278
|
+
if harness is not None and entry_harness != harness:
|
|
279
|
+
continue
|
|
280
|
+
entry_group = entry.get("group")
|
|
281
|
+
if group is not None and entry_group != group:
|
|
282
|
+
continue
|
|
283
|
+
summary = build_run_summary(registry_root, run_id, entry)
|
|
284
|
+
status = summary.get("status")
|
|
285
|
+
if active and status not in (STATUS_RUNNING, STATUS_STALE):
|
|
286
|
+
continue
|
|
287
|
+
if status_filter == STATUS_FILTER_RUNNING and status != STATUS_RUNNING:
|
|
288
|
+
continue
|
|
289
|
+
if status_filter == STATUS_FILTER_STALE and status != STATUS_STALE:
|
|
290
|
+
continue
|
|
291
|
+
summaries.append(summary)
|
|
292
|
+
summaries.sort(key=lambda item: item.get("activityAt", ""), reverse=True)
|
|
293
|
+
return summaries[:limit]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Deferred to the bottom to break the run_registry<->run_status facade cycle:
|
|
297
|
+
# run_registry re-exports this module's surface (a top-level import here would
|
|
298
|
+
# fail when run_status is imported first). Every `run_registry.<name>` access
|
|
299
|
+
# above is call-time, so binding the module after our own definitions suffices.
|
|
300
|
+
from delegate_agent import run_registry # noqa: E402
|