microloop 0.3.0__cp310-abi3-win_amd64.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.
- microloop/__init__.py +236 -0
- microloop/cli.py +317 -0
- microloop/microloop_core.pyd +0 -0
- microloop/py.typed +0 -0
- microloop-0.3.0.dist-info/METADATA +80 -0
- microloop-0.3.0.dist-info/RECORD +11 -0
- microloop-0.3.0.dist-info/WHEEL +4 -0
- microloop-0.3.0.dist-info/entry_points.txt +2 -0
- microloop-0.3.0.dist-info/licenses/LICENSE-APACHE +201 -0
- microloop-0.3.0.dist-info/licenses/LICENSE-MIT +21 -0
- microloop-0.3.0.dist-info/sboms/microloop-python.cyclonedx.json +1570 -0
microloop/__init__.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microloop — Keep agents making progress.
|
|
3
|
+
|
|
4
|
+
A local reliability runtime for autonomous agents. It watches an agent's
|
|
5
|
+
execution trajectory, detects when the agent is looping, stalled, regressing,
|
|
6
|
+
or operating on stale state, and reports a configured intervention.
|
|
7
|
+
|
|
8
|
+
The public API is intentionally small: ``Monitor``, ``Policy``, ``Event``,
|
|
9
|
+
``Decision`` and ``InterventionAction``. The host owns the agent loop; Microloop
|
|
10
|
+
only observes it and returns instructions.
|
|
11
|
+
|
|
12
|
+
``SCHEMA_VERSION`` is the trajectory schema the ``microloop`` CLI reads. It is
|
|
13
|
+
not enforced here: the runtime ``Event`` has no version field and ignores unknown
|
|
14
|
+
keys, so ``Monitor.observe`` accepts any payload. Version checking happens at the
|
|
15
|
+
file boundary in :mod:`microloop.cli`.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .microloop_core import Monitor as _CoreMonitor
|
|
25
|
+
from .microloop_core import Policy as _CorePolicy
|
|
26
|
+
from .microloop_core import version as _version
|
|
27
|
+
|
|
28
|
+
__version__: str = _version()
|
|
29
|
+
SCHEMA_VERSION = "0.3.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Decision",
|
|
33
|
+
"Event",
|
|
34
|
+
"InterventionAction",
|
|
35
|
+
"Monitor",
|
|
36
|
+
"Policy",
|
|
37
|
+
"ProgressState",
|
|
38
|
+
"SCHEMA_VERSION",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InterventionAction:
|
|
44
|
+
"""What the host is advised to do. Compare with plain strings."""
|
|
45
|
+
|
|
46
|
+
Observe = "observe"
|
|
47
|
+
Replan = "replan"
|
|
48
|
+
Stop = "stop"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProgressState:
|
|
52
|
+
"""Trajectory progress classification."""
|
|
53
|
+
|
|
54
|
+
Healthy = "healthy"
|
|
55
|
+
Warning = "warning"
|
|
56
|
+
Stalled = "stalled"
|
|
57
|
+
Regressing = "regressing"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Event:
|
|
62
|
+
"""One agent step."""
|
|
63
|
+
|
|
64
|
+
step: int
|
|
65
|
+
action: str
|
|
66
|
+
observation: str
|
|
67
|
+
state: Mapping[str, str] | None = None
|
|
68
|
+
metrics: Mapping[str, float] | None = None
|
|
69
|
+
metadata: Mapping[str, str] | None = None
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
payload: dict[str, Any] = {
|
|
73
|
+
"step": self.step,
|
|
74
|
+
"action": self.action,
|
|
75
|
+
"observation": self.observation,
|
|
76
|
+
}
|
|
77
|
+
if self.state is not None:
|
|
78
|
+
payload["state"] = dict(self.state)
|
|
79
|
+
if self.metrics is not None:
|
|
80
|
+
payload["metrics"] = dict(self.metrics)
|
|
81
|
+
if self.metadata is not None:
|
|
82
|
+
payload["metadata"] = dict(self.metadata)
|
|
83
|
+
return payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Decision:
|
|
88
|
+
"""The runtime's classification of one step."""
|
|
89
|
+
|
|
90
|
+
step: int
|
|
91
|
+
status: str
|
|
92
|
+
reasons: list[str] = field(default_factory=list)
|
|
93
|
+
evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
94
|
+
intervention: str = InterventionAction.Observe
|
|
95
|
+
severity: float = 0.0
|
|
96
|
+
verified_progress: bool = False
|
|
97
|
+
feedback: str | None = None
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(cls, payload: Mapping[str, Any]) -> Decision:
|
|
101
|
+
return cls(
|
|
102
|
+
step=int(payload["step"]),
|
|
103
|
+
status=str(payload["status"]),
|
|
104
|
+
reasons=[str(reason) for reason in payload.get("reasons", [])],
|
|
105
|
+
evidence=list(payload.get("evidence", [])),
|
|
106
|
+
intervention=str(payload.get("intervention", InterventionAction.Observe)),
|
|
107
|
+
severity=float(payload.get("severity", 0.0)),
|
|
108
|
+
verified_progress=bool(payload.get("verified_progress", False)),
|
|
109
|
+
feedback=payload.get("feedback"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def should_intervene(self) -> bool:
|
|
114
|
+
"""True when the policy advises an action other than ``observe``."""
|
|
115
|
+
return self.intervention != InterventionAction.Observe
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def recovery_context(self) -> str:
|
|
119
|
+
"""Recovery prompt to inject when intervening, or an empty string."""
|
|
120
|
+
return self.feedback or ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Policy:
|
|
124
|
+
"""
|
|
125
|
+
Recovery policy: maps a progress state to an intervention.
|
|
126
|
+
|
|
127
|
+
Defaults to observation-only. Automatic ``replan`` or ``stop`` requires an
|
|
128
|
+
explicit opt-in.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
healthy: str = InterventionAction.Observe,
|
|
135
|
+
warning: str = InterventionAction.Observe,
|
|
136
|
+
stalled: str = InterventionAction.Observe,
|
|
137
|
+
regressing: str = InterventionAction.Observe,
|
|
138
|
+
cooldown_steps: int = 5,
|
|
139
|
+
max_interventions: int = 2,
|
|
140
|
+
stop_at_step: int | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
config: dict[str, Any] = {
|
|
143
|
+
"healthy": healthy,
|
|
144
|
+
"warning": warning,
|
|
145
|
+
"stalled": stalled,
|
|
146
|
+
"regressing": regressing,
|
|
147
|
+
"cooldown_steps": cooldown_steps,
|
|
148
|
+
"max_interventions": max_interventions,
|
|
149
|
+
}
|
|
150
|
+
if stop_at_step is not None:
|
|
151
|
+
config["stop_at_step"] = stop_at_step
|
|
152
|
+
self._config = config
|
|
153
|
+
self._inner = _CorePolicy(json.dumps(config))
|
|
154
|
+
|
|
155
|
+
def evaluate(self, decision: Decision) -> str:
|
|
156
|
+
if not isinstance(decision, Decision):
|
|
157
|
+
raise TypeError("Policy.evaluate expects a Decision")
|
|
158
|
+
payload = {
|
|
159
|
+
"step": decision.step,
|
|
160
|
+
"status": decision.status,
|
|
161
|
+
"reasons": decision.reasons,
|
|
162
|
+
"evidence": decision.evidence,
|
|
163
|
+
"intervention": decision.intervention,
|
|
164
|
+
"severity": decision.severity,
|
|
165
|
+
"verified_progress": decision.verified_progress,
|
|
166
|
+
}
|
|
167
|
+
return json.loads(self._inner.evaluate_json(json.dumps(payload)))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Monitor:
|
|
171
|
+
"""
|
|
172
|
+
Trajectory progress monitor.
|
|
173
|
+
|
|
174
|
+
Defaults to observation-only. Automatic ``replan`` or ``stop`` requires an
|
|
175
|
+
explicit :class:`Policy`.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
window: int = 32,
|
|
182
|
+
repetitions: int = 3,
|
|
183
|
+
stagnation_steps: int = 8,
|
|
184
|
+
verification_samples: int = 3,
|
|
185
|
+
normalization: bool = True,
|
|
186
|
+
policy: Policy | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
config = {
|
|
189
|
+
"window": window,
|
|
190
|
+
"repetitions": repetitions,
|
|
191
|
+
"stagnation_steps": stagnation_steps,
|
|
192
|
+
"verification_samples": verification_samples,
|
|
193
|
+
"normalization": normalization,
|
|
194
|
+
}
|
|
195
|
+
policy_json = json.dumps(policy._config) if policy is not None else None
|
|
196
|
+
self._inner = _CoreMonitor(json.dumps(config), policy_json)
|
|
197
|
+
self._step = 0
|
|
198
|
+
|
|
199
|
+
def observe(
|
|
200
|
+
self,
|
|
201
|
+
action: str,
|
|
202
|
+
observation: str,
|
|
203
|
+
*,
|
|
204
|
+
state: Mapping[str, str] | None = None,
|
|
205
|
+
metrics: Mapping[str, float] | None = None,
|
|
206
|
+
metadata: Mapping[str, str] | None = None,
|
|
207
|
+
step: int | None = None,
|
|
208
|
+
) -> Decision:
|
|
209
|
+
"""Observe one step and return its :class:`Decision`."""
|
|
210
|
+
if step is None:
|
|
211
|
+
self._step += 1
|
|
212
|
+
step = self._step
|
|
213
|
+
else:
|
|
214
|
+
self._step = max(self._step, step)
|
|
215
|
+
event = Event(
|
|
216
|
+
step=step,
|
|
217
|
+
action=action,
|
|
218
|
+
observation=observation,
|
|
219
|
+
state=state,
|
|
220
|
+
metrics=metrics,
|
|
221
|
+
metadata=metadata,
|
|
222
|
+
)
|
|
223
|
+
return Decision.from_dict(
|
|
224
|
+
json.loads(self._inner.observe_json(json.dumps(event.to_dict())))
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def observe_event(self, event: Event) -> Decision:
|
|
228
|
+
"""Observe a pre-built :class:`Event` (used by the CLI)."""
|
|
229
|
+
return self.observe(
|
|
230
|
+
action=event.action,
|
|
231
|
+
observation=event.observation,
|
|
232
|
+
state=event.state,
|
|
233
|
+
metrics=event.metrics,
|
|
234
|
+
metadata=event.metadata,
|
|
235
|
+
step=event.step,
|
|
236
|
+
)
|
microloop/cli.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Microloop CLI: inspect, replay, monitor and doctor.
|
|
2
|
+
|
|
3
|
+
Reads schema 0.3.0 trajectory JSONL and streams it through the native runtime.
|
|
4
|
+
``monitor`` can follow a live file as an agent appends steps.
|
|
5
|
+
|
|
6
|
+
``replay`` re-runs *recorded events* through the current Microloop engine. It
|
|
7
|
+
does not reproduce the original agent execution: no model is called and no tools
|
|
8
|
+
run. A trajectory recorded by an older engine may therefore classify differently
|
|
9
|
+
today.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import platform
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from . import (
|
|
21
|
+
SCHEMA_VERSION,
|
|
22
|
+
Event,
|
|
23
|
+
InterventionAction,
|
|
24
|
+
Monitor,
|
|
25
|
+
Policy,
|
|
26
|
+
ProgressState,
|
|
27
|
+
__version__,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Trajectory schema major version this runtime can read. A different major
|
|
31
|
+
# version changes the meaning of fields, so it is rejected rather than guessed.
|
|
32
|
+
_SCHEMA_MAJOR = int(SCHEMA_VERSION.split(".", 1)[0])
|
|
33
|
+
|
|
34
|
+
# Recommendation policy for `replay` and `monitor`: surfaces what a host *could*
|
|
35
|
+
# do without ever taking it. `inspect` deliberately does not use this; see below.
|
|
36
|
+
_RECOMMENDATION_POLICY = Policy(
|
|
37
|
+
warning=InterventionAction.Observe,
|
|
38
|
+
stalled=InterventionAction.Replan,
|
|
39
|
+
regressing=InterventionAction.Replan,
|
|
40
|
+
cooldown_steps=1,
|
|
41
|
+
max_interventions=10_000,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_COLORS = {
|
|
45
|
+
"healthy": "\033[32m",
|
|
46
|
+
"warning": "\033[33m",
|
|
47
|
+
"stalled": "\033[31m",
|
|
48
|
+
"regressing": "\033[35m",
|
|
49
|
+
}
|
|
50
|
+
_RESET = "\033[0m"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _paint(text: str, status: str, enabled: bool) -> str:
|
|
54
|
+
if not enabled:
|
|
55
|
+
return text
|
|
56
|
+
return f"{_COLORS.get(status, '')}{text}{_RESET}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _check_schema(record: dict[str, Any], path: str, lineno: int) -> None:
|
|
60
|
+
"""Reject a trajectory this runtime cannot read."""
|
|
61
|
+
raw = record.get("schema_version")
|
|
62
|
+
if raw is None:
|
|
63
|
+
return
|
|
64
|
+
version = str(raw)
|
|
65
|
+
major = version.partition(".")[0]
|
|
66
|
+
if not major.isdigit():
|
|
67
|
+
raise SystemExit(
|
|
68
|
+
f"{path}:{lineno}: unreadable schema_version {version!r}; "
|
|
69
|
+
f"expected '<major>.<minor>' (this runtime reads {SCHEMA_VERSION})"
|
|
70
|
+
)
|
|
71
|
+
if int(major) != _SCHEMA_MAJOR:
|
|
72
|
+
raise SystemExit(
|
|
73
|
+
f"{path}:{lineno}: incompatible trajectory schema {version}; "
|
|
74
|
+
f"this runtime reads schema {SCHEMA_VERSION}. Trajectory major versions "
|
|
75
|
+
"must match — upgrade Microloop, or re-record the trajectory with "
|
|
76
|
+
"this schema version."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _load_record(path: str, lineno: int, line: str) -> dict[str, Any]:
|
|
81
|
+
try:
|
|
82
|
+
record = json.loads(line)
|
|
83
|
+
except json.JSONDecodeError as error:
|
|
84
|
+
raise SystemExit(f"{path}:{lineno}: invalid JSON: {error}") from error
|
|
85
|
+
if not isinstance(record, dict):
|
|
86
|
+
raise SystemExit(f"{path}:{lineno}: expected a JSON object")
|
|
87
|
+
_check_schema(record, path, lineno)
|
|
88
|
+
return record
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _iter_records(path: str, follow: bool = False, interval: float = 0.25):
|
|
92
|
+
"""Yield trajectory records, optionally following a file as it grows."""
|
|
93
|
+
with open(path, encoding="utf-8") as handle:
|
|
94
|
+
lineno = 0
|
|
95
|
+
while True:
|
|
96
|
+
where = handle.tell()
|
|
97
|
+
raw = handle.readline()
|
|
98
|
+
if raw == "":
|
|
99
|
+
if not follow:
|
|
100
|
+
return
|
|
101
|
+
handle.seek(where)
|
|
102
|
+
time.sleep(interval)
|
|
103
|
+
continue
|
|
104
|
+
line = raw.strip()
|
|
105
|
+
if not line:
|
|
106
|
+
continue
|
|
107
|
+
lineno += 1
|
|
108
|
+
yield _load_record(path, lineno, line)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _read_trajectory(path: str) -> tuple[str, list[dict[str, Any]]]:
|
|
112
|
+
schema = SCHEMA_VERSION
|
|
113
|
+
records: list[dict[str, Any]] = []
|
|
114
|
+
for record in _iter_records(path):
|
|
115
|
+
if "schema_version" in record:
|
|
116
|
+
schema = str(record["schema_version"])
|
|
117
|
+
records.append(record)
|
|
118
|
+
if not records:
|
|
119
|
+
raise SystemExit(f"{path}: no trajectory events found")
|
|
120
|
+
return schema, records
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _event_from_record(record: dict[str, Any], fallback_step: int) -> Event:
|
|
124
|
+
return Event(
|
|
125
|
+
step=int(record.get("step", fallback_step)),
|
|
126
|
+
action=str(record.get("action", "")),
|
|
127
|
+
observation=str(record.get("observation", "")),
|
|
128
|
+
state=record.get("state"),
|
|
129
|
+
metrics=record.get("metrics"),
|
|
130
|
+
metadata=record.get("metadata"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _recommending() -> Monitor:
|
|
135
|
+
return Monitor(policy=_RECOMMENDATION_POLICY)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _observing() -> Monitor:
|
|
139
|
+
"""Default runtime posture: detection without any intervention."""
|
|
140
|
+
return Monitor()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def commands() -> dict[str, Any]:
|
|
144
|
+
return {
|
|
145
|
+
"inspect": _inspect,
|
|
146
|
+
"replay": _replay,
|
|
147
|
+
"monitor": _monitor_live,
|
|
148
|
+
"doctor": _doctor,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _inspect(args: argparse.Namespace) -> int:
|
|
153
|
+
schema, records = _read_trajectory(args.trajectory)
|
|
154
|
+
# Detection only: `inspect` reports what the detectors found, so it runs the
|
|
155
|
+
# default observation-only policy and never shows a host's replan/stop choice.
|
|
156
|
+
monitor = _observing()
|
|
157
|
+
first_detected: int | None = None
|
|
158
|
+
last = None
|
|
159
|
+
for index, record in enumerate(records, start=1):
|
|
160
|
+
decision = monitor.observe_event(_event_from_record(record, index))
|
|
161
|
+
if decision.status != ProgressState.Healthy and first_detected is None:
|
|
162
|
+
first_detected = decision.step
|
|
163
|
+
last = decision
|
|
164
|
+
assert last is not None
|
|
165
|
+
|
|
166
|
+
reasons = ", ".join(last.reasons) if last.reasons else "none"
|
|
167
|
+
evidence = last.evidence[0]["detail"] if last.evidence else "none"
|
|
168
|
+
print(f"Microloop trajectory analysis (schema {schema})")
|
|
169
|
+
print(f"{'Steps':<15}{len(records)}")
|
|
170
|
+
print(f"{'Status':<15}{last.status}")
|
|
171
|
+
print(f"{'Detected at':<15}{'step ' + str(first_detected) if first_detected else 'none'}")
|
|
172
|
+
print(f"{'Reasons':<15}{reasons}")
|
|
173
|
+
print(f"{'Evidence':<15}{evidence}")
|
|
174
|
+
print(f"{'Action':<15}{last.intervention} (default policy: observe only)")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _replay(args: argparse.Namespace) -> int:
|
|
179
|
+
schema, records = _read_trajectory(args.trajectory)
|
|
180
|
+
monitor = _recommending()
|
|
181
|
+
for index, record in enumerate(records, start=1):
|
|
182
|
+
decision = monitor.observe_event(_event_from_record(record, index))
|
|
183
|
+
payload = {
|
|
184
|
+
"schema_version": schema,
|
|
185
|
+
"step": decision.step,
|
|
186
|
+
"status": decision.status,
|
|
187
|
+
"reasons": decision.reasons,
|
|
188
|
+
"intervention": decision.intervention,
|
|
189
|
+
"severity": decision.severity,
|
|
190
|
+
"verified_progress": decision.verified_progress,
|
|
191
|
+
}
|
|
192
|
+
if args.json:
|
|
193
|
+
print(json.dumps(payload))
|
|
194
|
+
else:
|
|
195
|
+
reasons = ",".join(decision.reasons) or "-"
|
|
196
|
+
print(
|
|
197
|
+
f"{decision.step:>4} {decision.status:<10} "
|
|
198
|
+
f"{decision.intervention:<8} {reasons}"
|
|
199
|
+
)
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _monitor_live(args: argparse.Namespace) -> int:
|
|
204
|
+
color = sys.stdout.isatty() and not args.no_color
|
|
205
|
+
monitor = _recommending()
|
|
206
|
+
steps = 0
|
|
207
|
+
stalls = 0
|
|
208
|
+
warnings = 0
|
|
209
|
+
regressions = 0
|
|
210
|
+
interventions = 0
|
|
211
|
+
recovered = False
|
|
212
|
+
seen_issue = False
|
|
213
|
+
|
|
214
|
+
print("Microloop")
|
|
215
|
+
print(f"trajectory {args.trajectory}" + (" (following)" if args.follow else ""))
|
|
216
|
+
print()
|
|
217
|
+
try:
|
|
218
|
+
for record in _iter_records(args.trajectory, follow=args.follow, interval=args.interval):
|
|
219
|
+
steps += 1
|
|
220
|
+
decision = monitor.observe_event(_event_from_record(record, steps))
|
|
221
|
+
if decision.status == "stalled":
|
|
222
|
+
stalls += 1
|
|
223
|
+
elif decision.status == "warning":
|
|
224
|
+
warnings += 1
|
|
225
|
+
elif decision.status == "regressing":
|
|
226
|
+
regressions += 1
|
|
227
|
+
if decision.status != "healthy":
|
|
228
|
+
seen_issue = True
|
|
229
|
+
elif seen_issue:
|
|
230
|
+
recovered = True
|
|
231
|
+
|
|
232
|
+
header = _paint(f"{decision.status.upper():<10}", decision.status, color)
|
|
233
|
+
reasons = " ".join(decision.reasons)
|
|
234
|
+
print(f"{decision.step:>4} {header} {reasons}".rstrip())
|
|
235
|
+
for item in decision.evidence:
|
|
236
|
+
print(f" {item['detail']}")
|
|
237
|
+
if decision.intervention != InterventionAction.Observe:
|
|
238
|
+
interventions += 1
|
|
239
|
+
print(f" -> {decision.intervention.upper()} (recommended)")
|
|
240
|
+
print()
|
|
241
|
+
except KeyboardInterrupt:
|
|
242
|
+
print()
|
|
243
|
+
|
|
244
|
+
print("completed" if not seen_issue or recovered else "stopped")
|
|
245
|
+
print(f"{'Steps':<18}{steps}")
|
|
246
|
+
print(f"{'Stalls':<18}{stalls}")
|
|
247
|
+
print(f"{'Warnings':<18}{warnings}")
|
|
248
|
+
print(f"{'Regressions':<18}{regressions}")
|
|
249
|
+
print(f"{'Recommended':<18}{interventions}")
|
|
250
|
+
print(f"{'Recovered':<18}{'yes' if recovered else 'no'}")
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _doctor(_args: argparse.Namespace) -> int:
|
|
255
|
+
checks = [
|
|
256
|
+
("microloop", __version__),
|
|
257
|
+
("schema", SCHEMA_VERSION),
|
|
258
|
+
("runtime", "native (PyO3)"),
|
|
259
|
+
("python", platform.python_version()),
|
|
260
|
+
("platform", f"{platform.system()} {platform.machine()}"),
|
|
261
|
+
]
|
|
262
|
+
ok = True
|
|
263
|
+
for name, value in checks:
|
|
264
|
+
print(f"[ok] {name:<10} {value}")
|
|
265
|
+
try:
|
|
266
|
+
from .microloop_core import Monitor as _Native # noqa: F401
|
|
267
|
+
|
|
268
|
+
print("[ok] native extension importable")
|
|
269
|
+
except Exception as error: # pragma: no cover - environment dependent
|
|
270
|
+
ok = False
|
|
271
|
+
print(f"[fail] native extension: {error}")
|
|
272
|
+
return 0 if ok else 1
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
276
|
+
parser = argparse.ArgumentParser(prog="microloop", description="Microloop runtime tools")
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--version",
|
|
279
|
+
action="version",
|
|
280
|
+
version=f"microloop {__version__} (schema {SCHEMA_VERSION}, runtime: native)",
|
|
281
|
+
)
|
|
282
|
+
sub = parser.add_subparsers(dest="command")
|
|
283
|
+
inspect_parser = sub.add_parser(
|
|
284
|
+
"inspect",
|
|
285
|
+
help="summarize a trajectory JSONL (detection only, no intervention policy)",
|
|
286
|
+
)
|
|
287
|
+
inspect_parser.add_argument("trajectory")
|
|
288
|
+
replay_parser = sub.add_parser(
|
|
289
|
+
"replay",
|
|
290
|
+
help="re-run recorded events through the current engine",
|
|
291
|
+
)
|
|
292
|
+
replay_parser.add_argument("trajectory")
|
|
293
|
+
replay_parser.add_argument("--json", action="store_true", help="emit decisions as JSONL")
|
|
294
|
+
monitor_parser = sub.add_parser("monitor", help="print a live progress view")
|
|
295
|
+
monitor_parser.add_argument("trajectory")
|
|
296
|
+
monitor_parser.add_argument(
|
|
297
|
+
"-f", "--follow", action="store_true", help="keep reading as the file grows"
|
|
298
|
+
)
|
|
299
|
+
monitor_parser.add_argument(
|
|
300
|
+
"--interval", type=float, default=0.25, help="poll interval in seconds when following"
|
|
301
|
+
)
|
|
302
|
+
monitor_parser.add_argument("--no-color", action="store_true", help="disable ANSI color")
|
|
303
|
+
sub.add_parser("doctor", help="check the native runtime")
|
|
304
|
+
return parser
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def main(argv: list[str] | None = None) -> int:
|
|
308
|
+
parser = build_parser()
|
|
309
|
+
args = parser.parse_args(argv)
|
|
310
|
+
if not args.command:
|
|
311
|
+
parser.print_help()
|
|
312
|
+
return 0
|
|
313
|
+
return commands()[args.command](args)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__": # pragma: no cover
|
|
317
|
+
sys.exit(main())
|
|
Binary file
|
microloop/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microloop
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
License-File: LICENSE-MIT
|
|
16
|
+
License-File: LICENSE-APACHE
|
|
17
|
+
Summary: Local reliability runtime for autonomous agents
|
|
18
|
+
Keywords: agent,llm,trajectory,progress,loop-detection
|
|
19
|
+
Author: Microloop Team
|
|
20
|
+
License-Expression: MIT OR Apache-2.0
|
|
21
|
+
Requires-Python: >=3.10, <3.14
|
|
22
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
23
|
+
Project-URL: Homepage, https://github.com/Devaretanmay/microloop
|
|
24
|
+
Project-URL: Repository, https://github.com/Devaretanmay/microloop
|
|
25
|
+
|
|
26
|
+
# Microloop Python SDK
|
|
27
|
+
|
|
28
|
+
Python bindings for the Microloop local reliability runtime.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from microloop import InterventionAction, Monitor, Policy
|
|
32
|
+
|
|
33
|
+
policy = Policy(
|
|
34
|
+
stalled=InterventionAction.Replan,
|
|
35
|
+
regressing=InterventionAction.Stop,
|
|
36
|
+
cooldown_steps=5,
|
|
37
|
+
)
|
|
38
|
+
monitor = Monitor(policy=policy)
|
|
39
|
+
|
|
40
|
+
for step in agent.steps():
|
|
41
|
+
decision = monitor.observe(
|
|
42
|
+
action=step.action,
|
|
43
|
+
observation=step.result,
|
|
44
|
+
state=step.state,
|
|
45
|
+
metrics={"exit_code": step.exit_code},
|
|
46
|
+
)
|
|
47
|
+
if decision.should_intervene:
|
|
48
|
+
agent.inject(decision.recovery_context)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`Monitor.observe(...)` returns a `Decision` with `status`, `reasons`,
|
|
52
|
+
`intervention`, `severity`, `verified_progress` and `feedback`. The default
|
|
53
|
+
policy only observes; automatic recovery requires explicit opt-in.
|
|
54
|
+
|
|
55
|
+
You own the agent loop. `Monitor` classifies and advises; it never runs tools,
|
|
56
|
+
calls a model, or stops a process.
|
|
57
|
+
|
|
58
|
+
## CLI
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
microloop inspect trajectory.jsonl # detection only, default observe-only policy
|
|
62
|
+
microloop replay trajectory.jsonl --json # re-run recorded events through the engine
|
|
63
|
+
microloop monitor trajectory.jsonl --follow
|
|
64
|
+
microloop doctor
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`replay` re-runs *recorded events* through the current engine. It does not
|
|
68
|
+
reproduce the original agent execution. A trajectory with an incompatible
|
|
69
|
+
`schema_version` major version is rejected rather than analyzed.
|
|
70
|
+
|
|
71
|
+
## Building
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install maturin
|
|
75
|
+
maturin develop --manifest-path python/microloop/Cargo.toml
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
No external credentials or network connections are required. All trajectory
|
|
79
|
+
evaluation runs locally in Rust via PyO3.
|
|
80
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
microloop/__init__.py,sha256=TyEdfEbnggSZH7fIP9AFWLkSjHZbEWv866OJb0fdwe0,7563
|
|
2
|
+
microloop/cli.py,sha256=AkjqrI7_msaYJLLoq-__XXXWKQp7x1MTQ9o6A1N5i7o,11285
|
|
3
|
+
microloop/microloop_core.pyd,sha256=6ELkzbzuXtfetELCBfR1qR8y0WfSz4m-fyQArJugkec,1214976
|
|
4
|
+
microloop/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
microloop-0.3.0.dist-info/METADATA,sha256=xyGPUmORrizxF1b0VLTJWfm2AZc9m3tCFGP3VmR1f5I,2775
|
|
6
|
+
microloop-0.3.0.dist-info/WHEEL,sha256=7HL-TGvq44Ofp0yhZL-uRIIFHcJTIqqkRZVQ2wZidzM,96
|
|
7
|
+
microloop-0.3.0.dist-info/entry_points.txt,sha256=IH9dbn7NkvU8eRSKXpvNhYBsPVuIqll-DqFEelqfLTs,47
|
|
8
|
+
microloop-0.3.0.dist-info/licenses/LICENSE-APACHE,sha256=rbE9VbG9z2yR4NDeFiwhzbli2eDjiXYeUIhQIvcUdM8,11548
|
|
9
|
+
microloop-0.3.0.dist-info/licenses/LICENSE-MIT,sha256=NRAxjDGOWZmFNvH50bZgkWzQLXQKLyJrPmrR_K5KJsA,1087
|
|
10
|
+
microloop-0.3.0.dist-info/sboms/microloop-python.cyclonedx.json,sha256=cAVkmwmBV3p1i8JkwBpFVHtK33fWMs1sKZ7YNkMQl_4,48880
|
|
11
|
+
microloop-0.3.0.dist-info/RECORD,,
|