agent-run-supervisor 0.1.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 (35) hide show
  1. agent_run_supervisor/__init__.py +5 -0
  2. agent_run_supervisor/__main__.py +3 -0
  3. agent_run_supervisor/caller.py +301 -0
  4. agent_run_supervisor/cli.py +192 -0
  5. agent_run_supervisor/commands.py +381 -0
  6. agent_run_supervisor/event_store.py +159 -0
  7. agent_run_supervisor/exit_classifier.py +137 -0
  8. agent_run_supervisor/fixtures/acpx-0.12.0/success-codex-sentinel/stdout.ndjson +15 -0
  9. agent_run_supervisor/hermes_caller/__init__.py +74 -0
  10. agent_run_supervisor/hermes_caller/events.py +245 -0
  11. agent_run_supervisor/hermes_caller/feishu_adapter.py +61 -0
  12. agent_run_supervisor/hermes_caller/hermes.py +135 -0
  13. agent_run_supervisor/hermes_caller/intake.py +136 -0
  14. agent_run_supervisor/hermes_caller/task.py +21 -0
  15. agent_run_supervisor/hermes_caller/verdict.py +93 -0
  16. agent_run_supervisor/hermes_caller/view_model.py +144 -0
  17. agent_run_supervisor/live_stream.py +131 -0
  18. agent_run_supervisor/parser.py +487 -0
  19. agent_run_supervisor/policy.py +291 -0
  20. agent_run_supervisor/preflight.py +493 -0
  21. agent_run_supervisor/process_liveness.py +240 -0
  22. agent_run_supervisor/redaction.py +134 -0
  23. agent_run_supervisor/result.py +77 -0
  24. agent_run_supervisor/retention.py +455 -0
  25. agent_run_supervisor/role.py +327 -0
  26. agent_run_supervisor/runner.py +800 -0
  27. agent_run_supervisor/session.py +583 -0
  28. agent_run_supervisor/session_runtime.py +780 -0
  29. agent_run_supervisor/workspace.py +105 -0
  30. agent_run_supervisor-0.1.0.dist-info/METADATA +311 -0
  31. agent_run_supervisor-0.1.0.dist-info/RECORD +35 -0
  32. agent_run_supervisor-0.1.0.dist-info/WHEEL +5 -0
  33. agent_run_supervisor-0.1.0.dist-info/entry_points.txt +2 -0
  34. agent_run_supervisor-0.1.0.dist-info/licenses/LICENSE +21 -0
  35. agent_run_supervisor-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """agent-run-supervisor local ACP/acpx run and session supervision toolkit."""
2
+
3
+ __version__ = "0.0.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ from agent_run_supervisor.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,301 @@
1
+ """Generic local caller boundary for library integrations.
2
+
3
+ The caller boundary is intentionally thin: it validates a local invocation
4
+ specification, loads the role when supplied by file, combines caller-owned
5
+ context with the prompt, and delegates execution to the existing supervisor
6
+ surfaces. It does not parse acpx streams, interpret business success, deliver
7
+ messages, or carry platform identifiers.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from agent_run_supervisor.role import AgentRoleSpec, load_role
17
+ from agent_run_supervisor.runner import DryRunResult, RunOutcome, SupervisorRunner
18
+ from agent_run_supervisor.session_runtime import (
19
+ SessionCloseOutcome,
20
+ SessionCreateOutcome,
21
+ SessionRuntime,
22
+ SessionStatusOutcome,
23
+ SessionTurnOutcome,
24
+ )
25
+
26
+ DEFAULT_SESSIONS_DIR_NAME = Path(".agent-run-supervisor") / "sessions"
27
+
28
+ EXEC_MODE = "exec"
29
+ EXEC_DRY_RUN_MODE = "exec_dry_run"
30
+ SESSION_CREATE_MODE = "session_create"
31
+ SESSION_SEND_MODE = "session_send"
32
+ SESSION_STATUS_MODE = "session_status"
33
+ SESSION_CLOSE_MODE = "session_close"
34
+
35
+ SUPPORTED_MODES = {
36
+ EXEC_MODE,
37
+ EXEC_DRY_RUN_MODE,
38
+ SESSION_CREATE_MODE,
39
+ SESSION_SEND_MODE,
40
+ SESSION_STATUS_MODE,
41
+ SESSION_CLOSE_MODE,
42
+ }
43
+ PROMPT_REQUIRED_MODES = {EXEC_MODE, EXEC_DRY_RUN_MODE, SESSION_SEND_MODE}
44
+ SESSION_MODES = {
45
+ SESSION_CREATE_MODE,
46
+ SESSION_SEND_MODE,
47
+ SESSION_STATUS_MODE,
48
+ SESSION_CLOSE_MODE,
49
+ }
50
+
51
+
52
+ class CallerInvocationError(ValueError):
53
+ """Raised when a local caller invocation is invalid before delegation."""
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class CallerInvocationSpec:
58
+ mode: str
59
+ role: AgentRoleSpec | None = None
60
+ role_file: str | Path | None = None
61
+ prompt: str | None = None
62
+ context: str | None = None
63
+ cwd: str | Path | None = None
64
+ runs_dir: str | Path | None = None
65
+ sessions_dir: str | Path | None = None
66
+ session_id: str | None = None
67
+ session_name: str | None = None
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class CallerResult:
72
+ mode: str
73
+ supervisor_status: str | None
74
+ result: dict[str, Any]
75
+ artifact_dir: str | None
76
+ run_dir: str | None
77
+ session_dir: str | None
78
+ business_verdict: None = None
79
+
80
+ def to_dict(self) -> dict[str, Any]:
81
+ return {
82
+ "mode": self.mode,
83
+ "supervisor_status": self.supervisor_status,
84
+ "result": self.result,
85
+ "artifact_dir": self.artifact_dir,
86
+ "run_dir": self.run_dir,
87
+ "session_dir": self.session_dir,
88
+ "business_verdict": self.business_verdict,
89
+ }
90
+
91
+
92
+ def invoke_caller(
93
+ spec: CallerInvocationSpec,
94
+ *,
95
+ runner: SupervisorRunner | None = None,
96
+ session_runtime: SessionRuntime | None = None,
97
+ ) -> CallerResult:
98
+ """Invoke the local supervisor through a generic caller-owned spec.
99
+
100
+ ``business_verdict`` stays ``None`` both in the wrapped supervisor result
101
+ and in the caller wrapper. Callers remain responsible for business verdicts,
102
+ rendering, delivery, and any platform integration outside this library.
103
+ """
104
+ _validate_spec(spec)
105
+ role = _resolve_role(spec)
106
+ cwd = _optional_str(spec.cwd)
107
+
108
+ if spec.mode in {EXEC_MODE, EXEC_DRY_RUN_MODE}:
109
+ active_runner = runner or SupervisorRunner(
110
+ runs_dir=Path(spec.runs_dir) if spec.runs_dir is not None else None
111
+ )
112
+ prompt = _combined_prompt(spec)
113
+ if spec.mode == EXEC_DRY_RUN_MODE:
114
+ dry = active_runner.dry_run(role=role, prompt=prompt, cwd=cwd)
115
+ return _from_run_like(mode=spec.mode, outcome=dry)
116
+ outcome = active_runner.run(role=role, prompt=prompt, cwd=cwd)
117
+ return _from_run_like(mode=spec.mode, outcome=outcome)
118
+
119
+ active_runtime = session_runtime or SessionRuntime(sessions_dir=_sessions_dir(spec))
120
+ session_id = _required_session_id(spec)
121
+
122
+ if spec.mode == SESSION_CREATE_MODE:
123
+ created = active_runtime.create_session(
124
+ role=role,
125
+ session_id=session_id,
126
+ session_name=spec.session_name,
127
+ cwd=cwd,
128
+ )
129
+ return _from_session_create(spec.mode, created)
130
+ if spec.mode == SESSION_SEND_MODE:
131
+ sent = active_runtime.send(
132
+ role=role,
133
+ session_id=session_id,
134
+ prompt=_combined_prompt(spec),
135
+ cwd=cwd,
136
+ )
137
+ return _from_session_turn(
138
+ spec.mode,
139
+ sent,
140
+ session_dir=_session_dir(active_runtime, spec, session_id),
141
+ )
142
+ if spec.mode == SESSION_STATUS_MODE:
143
+ status = active_runtime.status(role=role, session_id=session_id, cwd=cwd)
144
+ return _from_session_management(
145
+ spec.mode,
146
+ status.result,
147
+ session_dir=_session_dir(active_runtime, spec, session_id),
148
+ )
149
+ if spec.mode == SESSION_CLOSE_MODE:
150
+ closed = active_runtime.close(role=role, session_id=session_id, cwd=cwd)
151
+ return _from_session_close(spec.mode, closed, _session_dir(active_runtime, spec, session_id))
152
+
153
+ raise CallerInvocationError(f"unsupported mode {spec.mode!r}")
154
+
155
+
156
+ def _validate_spec(spec: CallerInvocationSpec) -> None:
157
+ if spec.mode not in SUPPORTED_MODES:
158
+ raise CallerInvocationError(f"unsupported mode {spec.mode!r}")
159
+ role_source_count = int(spec.role is not None) + int(spec.role_file is not None)
160
+ if role_source_count != 1:
161
+ raise CallerInvocationError("exactly one role source is required: role or role_file")
162
+ if spec.mode in PROMPT_REQUIRED_MODES and not _has_text(spec.prompt):
163
+ raise CallerInvocationError(f"mode {spec.mode!r} requires a non-empty prompt")
164
+ if spec.mode in SESSION_MODES and not _has_text(spec.session_id):
165
+ raise CallerInvocationError(f"mode {spec.mode!r} requires a non-empty session_id")
166
+ if spec.session_name is not None:
167
+ if spec.mode != SESSION_CREATE_MODE:
168
+ raise CallerInvocationError("session_name is only accepted for session_create")
169
+ if not _has_text(spec.session_name):
170
+ raise CallerInvocationError("session_name must be a non-empty string")
171
+
172
+
173
+ def _resolve_role(spec: CallerInvocationSpec) -> AgentRoleSpec:
174
+ if spec.role is not None:
175
+ return spec.role
176
+ if spec.role_file is None:
177
+ # Unreachable after validation; keeps type checkers honest.
178
+ raise CallerInvocationError("missing role source")
179
+ raw = json.loads(Path(spec.role_file).read_text(encoding="utf-8"))
180
+ return load_role(raw)
181
+
182
+
183
+ def _combined_prompt(spec: CallerInvocationSpec) -> str:
184
+ prompt = spec.prompt or ""
185
+ context = spec.context or ""
186
+ if context:
187
+ return f"{context}\n\n{prompt}"
188
+ return prompt
189
+
190
+
191
+ def _from_run_like(
192
+ *,
193
+ mode: str,
194
+ outcome: DryRunResult | RunOutcome,
195
+ ) -> CallerResult:
196
+ result = dict(outcome.result)
197
+ run_dir = str(outcome.run_dir)
198
+ return CallerResult(
199
+ mode=mode,
200
+ supervisor_status=_status_from(result),
201
+ result=result,
202
+ artifact_dir=run_dir,
203
+ run_dir=run_dir,
204
+ session_dir=None,
205
+ business_verdict=None,
206
+ )
207
+
208
+
209
+ def _from_session_create(mode: str, outcome: SessionCreateOutcome) -> CallerResult:
210
+ result = dict(outcome.result)
211
+ session_dir = str(outcome.session_dir)
212
+ return CallerResult(
213
+ mode=mode,
214
+ supervisor_status=_status_from(result),
215
+ result=result,
216
+ artifact_dir=session_dir,
217
+ run_dir=None,
218
+ session_dir=session_dir,
219
+ business_verdict=None,
220
+ )
221
+
222
+
223
+ def _from_session_turn(
224
+ mode: str,
225
+ outcome: SessionTurnOutcome,
226
+ *,
227
+ session_dir: Path,
228
+ ) -> CallerResult:
229
+ result = dict(outcome.result)
230
+ run_dir = result.get("run_dir")
231
+ run_dir_str = run_dir if isinstance(run_dir, str) else str(outcome.turn_dir)
232
+ return CallerResult(
233
+ mode=mode,
234
+ supervisor_status=_status_from(result),
235
+ result=result,
236
+ artifact_dir=run_dir_str,
237
+ run_dir=run_dir_str,
238
+ session_dir=str(session_dir),
239
+ business_verdict=None,
240
+ )
241
+
242
+
243
+ def _from_session_management(
244
+ mode: str,
245
+ result: dict[str, Any],
246
+ *,
247
+ session_dir: Path,
248
+ ) -> CallerResult:
249
+ result_copy = dict(result)
250
+ session_dir_str = str(session_dir)
251
+ return CallerResult(
252
+ mode=mode,
253
+ supervisor_status=_status_from(result_copy),
254
+ result=result_copy,
255
+ artifact_dir=session_dir_str,
256
+ run_dir=None,
257
+ session_dir=session_dir_str,
258
+ business_verdict=None,
259
+ )
260
+
261
+
262
+ def _from_session_close(
263
+ mode: str,
264
+ outcome: SessionCloseOutcome,
265
+ session_dir: Path,
266
+ ) -> CallerResult:
267
+ return _from_session_management(mode, outcome.result, session_dir=session_dir)
268
+
269
+
270
+ def _status_from(result: dict[str, Any]) -> str | None:
271
+ status = result.get("status")
272
+ return status if isinstance(status, str) else None
273
+
274
+
275
+ def _sessions_dir(spec: CallerInvocationSpec) -> Path:
276
+ if spec.sessions_dir is not None:
277
+ return Path(spec.sessions_dir)
278
+ return Path.cwd() / DEFAULT_SESSIONS_DIR_NAME
279
+
280
+
281
+ def _session_dir(
282
+ runtime: SessionRuntime,
283
+ spec: CallerInvocationSpec,
284
+ session_id: str,
285
+ ) -> Path:
286
+ base = Path(spec.sessions_dir) if spec.sessions_dir is not None else runtime.sessions_dir
287
+ return base / session_id
288
+
289
+
290
+ def _required_session_id(spec: CallerInvocationSpec) -> str:
291
+ if spec.session_id is None:
292
+ raise CallerInvocationError("session_id is required")
293
+ return spec.session_id
294
+
295
+
296
+ def _optional_str(value: str | Path | None) -> str | None:
297
+ return str(value) if value is not None else None
298
+
299
+
300
+ def _has_text(value: str | None) -> bool:
301
+ return isinstance(value, str) and bool(value)
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from typing import Sequence
6
+
7
+
8
+ def _build_parser() -> argparse.ArgumentParser:
9
+ parser = argparse.ArgumentParser(
10
+ prog="agent-run-supervisor",
11
+ description="Supervise ACP/acpx external AGENT runs and sessions with redacted local evidence.",
12
+ )
13
+ subparsers = parser.add_subparsers(dest="command", metavar="command")
14
+
15
+ validate_role = subparsers.add_parser(
16
+ "validate-role",
17
+ help="Validate an AgentRoleSpec file.",
18
+ )
19
+ validate_role.add_argument("role_file", help="Path to a JSON AgentRoleSpec file.")
20
+
21
+ replay = subparsers.add_parser(
22
+ "replay",
23
+ help="Replay an observed acpx stdout NDJSON stream through the parser.",
24
+ )
25
+ replay.add_argument("events_file", help="Path to acpx stdout NDJSON file.")
26
+
27
+ doctor = subparsers.add_parser(
28
+ "doctor",
29
+ help="Run environment/fixture probes without launching a real AGENT.",
30
+ )
31
+ doctor.add_argument("--role", default=None, help="Optional role file to validate.")
32
+ doctor.add_argument(
33
+ "--fixtures",
34
+ default=None,
35
+ help="Optional fixture directory to replay.",
36
+ )
37
+
38
+ run = subparsers.add_parser(
39
+ "run",
40
+ help="Supervise local acpx exec or persist safe dry-run artifacts.",
41
+ description="Supervise local acpx exec under AgentRoleSpec policy, or persist safe dry-run artifacts with --no-real-run.",
42
+ )
43
+ run.add_argument("--role", required=True, help="Path to AgentRoleSpec JSON file.")
44
+ run.add_argument("--prompt-file", required=True, help="Path to prompt text file.")
45
+ run.add_argument("--cwd", default=None, help="Override effective cwd for exec or dry-run compilation.")
46
+ run.add_argument(
47
+ "--no-real-run",
48
+ action="store_true",
49
+ help="Persist safe dry-run artifacts without launching local acpx exec.",
50
+ )
51
+ run.add_argument(
52
+ "--runs-dir",
53
+ default=None,
54
+ help="Directory that holds run artifacts (defaults to .agent-run-supervisor/runs).",
55
+ )
56
+
57
+ _add_session_parser(subparsers)
58
+ _add_cleanup_parser(subparsers)
59
+ return parser
60
+
61
+
62
+ def _add_cleanup_parser(subparsers: argparse._SubParsersAction) -> None:
63
+ """Confined, dry-run-first run/session artifact retention/cleanup."""
64
+ cleanup = subparsers.add_parser(
65
+ "cleanup",
66
+ help="Plan (dry-run) or apply confined retention cleanup of local run/session artifacts.",
67
+ description=(
68
+ "List run/session artifacts under a .agent-run-supervisor root that a "
69
+ "retention policy would remove. Dry-run by default; --apply deletes only "
70
+ "the planned, confined, non-live entries."
71
+ ),
72
+ )
73
+ cleanup.add_argument(
74
+ "--runs-dir",
75
+ default=None,
76
+ help="Directory that holds run artifacts (defaults to .agent-run-supervisor/runs).",
77
+ )
78
+ cleanup.add_argument(
79
+ "--sessions-dir",
80
+ default=None,
81
+ help="Directory that holds session records (defaults to .agent-run-supervisor/sessions).",
82
+ )
83
+ cleanup.add_argument(
84
+ "--max-age-days",
85
+ type=int,
86
+ default=None,
87
+ help="Delete artifacts strictly older than this many days.",
88
+ )
89
+ cleanup.add_argument(
90
+ "--max-count",
91
+ type=int,
92
+ default=None,
93
+ help="Keep only the newest N eligible artifacts; delete the rest.",
94
+ )
95
+ cleanup.add_argument(
96
+ "--apply",
97
+ action="store_true",
98
+ help="Actually delete the planned entries (default is a read-only dry-run).",
99
+ )
100
+
101
+
102
+ def _add_session_parser(subparsers: argparse._SubParsersAction) -> None:
103
+ """Persistent-session lifecycle: ``session create|send|status|close|abort|list``."""
104
+ session = subparsers.add_parser(
105
+ "session",
106
+ help="Persistent session lifecycle (create / send / status / close / abort / list).",
107
+ description="Create/open a role-bound persistent session, send a prompt turn, query status, close, abort/cancel, or list local session records.",
108
+ )
109
+ session_sub = session.add_subparsers(dest="session_command", metavar="session_command")
110
+
111
+ def _add_common(sub: argparse.ArgumentParser) -> None:
112
+ sub.add_argument("--role", required=True, help="Path to AgentRoleSpec JSON file.")
113
+ sub.add_argument("--session-id", required=True, help="Local session id (safe path component).")
114
+ sub.add_argument("--cwd", default=None, help="Override effective cwd for session work.")
115
+ sub.add_argument(
116
+ "--sessions-dir",
117
+ default=None,
118
+ help="Directory that holds session records (defaults to .agent-run-supervisor/sessions).",
119
+ )
120
+
121
+ create = session_sub.add_parser("create", help="Create/open a local persistent session.")
122
+ _add_common(create)
123
+ create.add_argument(
124
+ "--session-name",
125
+ default=None,
126
+ help="acpx session name (defaults to --session-id).",
127
+ )
128
+
129
+ send = session_sub.add_parser("send", help="Send one prompt turn to a session.")
130
+ _add_common(send)
131
+ send.add_argument("--prompt-file", required=True, help="Path to prompt text file.")
132
+
133
+ status = session_sub.add_parser("status", help="Query a session's status/show record.")
134
+ _add_common(status)
135
+
136
+ close = session_sub.add_parser("close", help="Close an open local persistent session.")
137
+ _add_common(close)
138
+
139
+ abort = session_sub.add_parser("abort", help="Cooperatively cancel/abort active session work.")
140
+ _add_common(abort)
141
+
142
+ list_sessions = session_sub.add_parser(
143
+ "list",
144
+ help="List local session records (read-only; does not launch acpx/AGENT work).",
145
+ )
146
+ list_sessions.add_argument(
147
+ "--sessions-dir",
148
+ default=None,
149
+ help="Directory that holds session records (defaults to .agent-run-supervisor/sessions).",
150
+ )
151
+ list_sessions.add_argument(
152
+ "--role",
153
+ default=None,
154
+ help="Optional role file; if given, list only records owned by that role.",
155
+ )
156
+
157
+
158
+ def main(argv: Sequence[str] | None = None) -> int:
159
+ parser = _build_parser()
160
+ args = parser.parse_args(argv)
161
+ if args.command is None:
162
+ parser.print_usage(sys.stderr)
163
+ print("error: a subcommand is required", file=sys.stderr)
164
+ return 2
165
+
166
+ if args.command == "validate-role":
167
+ from agent_run_supervisor.commands import cmd_validate_role
168
+
169
+ return cmd_validate_role(args)
170
+ if args.command == "replay":
171
+ from agent_run_supervisor.commands import cmd_replay
172
+
173
+ return cmd_replay(args)
174
+ if args.command == "doctor":
175
+ from agent_run_supervisor.commands import cmd_doctor
176
+
177
+ return cmd_doctor(args)
178
+ if args.command == "run":
179
+ from agent_run_supervisor.commands import cmd_run
180
+
181
+ return cmd_run(args)
182
+ if args.command == "session":
183
+ from agent_run_supervisor.commands import cmd_session
184
+
185
+ return cmd_session(args)
186
+ if args.command == "cleanup":
187
+ from agent_run_supervisor.commands import cmd_cleanup
188
+
189
+ return cmd_cleanup(args)
190
+ parser.print_usage(sys.stderr)
191
+ print(f"error: unknown command {args.command}", file=sys.stderr)
192
+ return 2