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.
Files changed (54) hide show
  1. delegate_agent/__init__.py +5 -0
  2. delegate_agent/archived_logs.py +44 -0
  3. delegate_agent/argv_builders.py +423 -0
  4. delegate_agent/argv_utils.py +36 -0
  5. delegate_agent/capability_commands.py +99 -0
  6. delegate_agent/cli.py +987 -0
  7. delegate_agent/cli_parser.py +1627 -0
  8. delegate_agent/command_errors.py +29 -0
  9. delegate_agent/command_help.py +1264 -0
  10. delegate_agent/config.py +1117 -0
  11. delegate_agent/config_commands.py +143 -0
  12. delegate_agent/constants.py +50 -0
  13. delegate_agent/describe_payload.py +1018 -0
  14. delegate_agent/errors.py +32 -0
  15. delegate_agent/git_utils.py +180 -0
  16. delegate_agent/harness_events.py +719 -0
  17. delegate_agent/inspection_commands.py +104 -0
  18. delegate_agent/isolation.py +316 -0
  19. delegate_agent/json_types.py +18 -0
  20. delegate_agent/log_output.py +78 -0
  21. delegate_agent/private_io.py +109 -0
  22. delegate_agent/profile_commands.py +42 -0
  23. delegate_agent/profile_guard.py +116 -0
  24. delegate_agent/profiles.py +389 -0
  25. delegate_agent/prompt_instructions.py +39 -0
  26. delegate_agent/prompt_transport.py +12 -0
  27. delegate_agent/reasoning.py +916 -0
  28. delegate_agent/redaction.py +193 -0
  29. delegate_agent/rendering.py +573 -0
  30. delegate_agent/request_build.py +1681 -0
  31. delegate_agent/request_models.py +191 -0
  32. delegate_agent/retention.py +321 -0
  33. delegate_agent/run_metadata.py +78 -0
  34. delegate_agent/run_output_commands.py +645 -0
  35. delegate_agent/run_registry.py +747 -0
  36. delegate_agent/run_status.py +300 -0
  37. delegate_agent/runner.py +1830 -0
  38. delegate_agent/safe_workspace.py +821 -0
  39. delegate_agent/snapshot_view.py +229 -0
  40. delegate_agent/wait_cancel_commands.py +559 -0
  41. delegate_agent/worktree_commands.py +218 -0
  42. delegate_agent/worktree_execution.py +654 -0
  43. delegate_agent/worktree_gc.py +529 -0
  44. delegate_agent/worktree_mgmt.py +782 -0
  45. delegate_agent/worktree_records.py +232 -0
  46. delegate_agent/worktree_remove.py +547 -0
  47. delegate_agent/worktree_summary.py +242 -0
  48. delegate_agent/wsl.py +65 -0
  49. delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
  50. delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
  51. delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
  52. delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
  53. delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
  54. 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