bounded-agent 0.1.2__tar.gz → 0.1.3__tar.gz

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 (21) hide show
  1. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/PKG-INFO +48 -9
  2. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/README.md +47 -8
  3. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/pyproject.toml +1 -1
  4. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/actions.py +9 -0
  5. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_actions.py +15 -0
  6. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/uv.lock +1 -1
  7. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/.github/workflows/ci.yml +0 -0
  8. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/.gitignore +0 -0
  9. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/LICENSE +0 -0
  10. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/examples/sentinel/contracts.yaml +0 -0
  11. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/examples/sentinel/sentinel.py +0 -0
  12. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/__init__.py +0 -0
  13. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/contracts.py +0 -0
  14. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/envelope.py +0 -0
  15. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/leaf.py +0 -0
  16. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/src/bounded_agent/ledgers.py +0 -0
  17. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/__init__.py +0 -0
  18. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_contracts.py +0 -0
  19. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_envelope.py +0 -0
  20. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_leaf.py +0 -0
  21. {bounded_agent-0.1.2 → bounded_agent-0.1.3}/tests/test_ledgers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bounded-agent
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Contract-bounded autonomy for LLM agents: five small primitives that make an agent safe to leave alone.
5
5
  Project-URL: Repository, https://github.com/scholih/bounded-agent
6
6
  Project-URL: Manifesto, https://medium.com/@scholih/the-edge-is-the-harness-not-the-model-a543abade391
@@ -55,6 +55,16 @@ contracts:
55
55
  scope: checkout-stack
56
56
  tier: act # observe | propose | act
57
57
  allows: [restart_worker]
58
+ - name: cache
59
+ status: active
60
+ scope: checkout-stack
61
+ tier: propose # may draft fixes; a human executes them
62
+ allows: [clear_cache]
63
+ - name: payments-db
64
+ status: proposed # not signed off — grants NOTHING until active
65
+ scope: checkout-stack
66
+ tier: act
67
+ allows: [failover_replica]
58
68
  ```
59
69
 
60
70
  ```python
@@ -63,8 +73,15 @@ from pathlib import Path
63
73
 
64
74
  from bounded_agent import ActionSet, ContractBook, Envelope, EvidenceLedger, Gate
65
75
 
76
+ print("1. the book — who may do what, declared by a human:")
66
77
  book = ContractBook.load("contracts.yaml") # raises loudly if missing/malformed
67
- granted = book.granted(Gate(required_scope="checkout-stack", required_tier="act"))
78
+ for c in book.contracts:
79
+ print(f" {c.name}: status={c.status} scope={c.scope} tier={c.tier} allows={list(c.allows)}")
80
+
81
+ gate = Gate(required_scope="checkout-stack", required_tier="act")
82
+ granted = book.granted(gate)
83
+ print(f"\n2. the gate — scope={gate.required_scope!r} at tier '{gate.required_tier}' grants:")
84
+ print(f" {granted or 'nothing — no active, in-scope contract at this tier'}")
68
85
 
69
86
  actions = ActionSet() # the implemented closed set:
70
87
  actions.register("restart_worker", lambda: (True, "worker restarted"))
@@ -72,20 +89,42 @@ actions.register("restart_worker", lambda: (True, "worker restarted"))
72
89
  env = Envelope(armed=True, # ships disarmed; arming is explicit
73
90
  kill_switch_path=Path("STOP"), # `touch STOP` halts everything
74
91
  max_actions_per_run=2,
75
- notify=print) # act-then-report (stand in a pager here)
92
+ notify=lambda msg: print(f" [report] {msg}"))
93
+ ks = "PRESENT" if env.kill_switch_path.exists() else "absent"
94
+ print(f"\n3. the envelope — armed={env.armed}, kill-switch {ks}, breaker {env.max_actions_per_run}/run")
76
95
 
96
+ print("\n4. executing (double-closed: contracted AND registered; every outcome reported):")
77
97
  outcome = actions.execute(list(granted.items()), env,
78
98
  ledger=EvidenceLedger(Path("ledger")),
79
99
  period=dt.date.today())
80
- print(outcome.executed, "action(s) —", outcome.records[0].status)
100
+
101
+ print(f"\n5. outcome — {outcome.summary()}")
102
+ print(f" ledger: ledger/{dt.date.today().isoformat()}.jsonl (append-only; every run adds lines)")
103
+ ```
104
+
105
+ Run it and the output narrates the authorization chain — note the gate silently denying
106
+ `cache` (propose tier) and `payments-db` (not signed off):
107
+
81
108
  ```
109
+ 1. the book — who may do what, declared by a human:
110
+ queue-worker: status=active scope=checkout-stack tier=act allows=['restart_worker']
111
+ cache: status=active scope=checkout-stack tier=propose allows=['clear_cache']
112
+ payments-db: status=proposed scope=checkout-stack tier=act allows=['failover_replica']
113
+
114
+ 2. the gate — scope='checkout-stack' at tier 'act' grants:
115
+ {'queue-worker': ('restart_worker',)}
82
116
 
83
- Both files as-is, `python` it, and you'll see the act-then-report line fire and one
84
- `executed-ok` record land in `ledger/` — the whole authorization chain in 20 lines.
117
+ 3. the envelope armed=True, kill-switch absent, breaker 2/run
118
+
119
+ 4. executing (double-closed: contracted AND registered; every outcome reported):
120
+ [report] executed-ok: restart_worker on queue-worker (worker restarted)
121
+
122
+ 5. outcome — 1 executed | queue-worker:restart_worker=executed-ok
123
+ ledger: ledger/2026-08-01.jsonl (append-only; every run adds lines)
124
+ ```
85
125
 
86
- An action fires only if it appears in **both** the contract's `allows` list **and** the
87
- registered set and only inside an armed envelope with no kill-switch present and breaker
88
- budget remaining. Everything that happens (and everything refused) is one JSONL line.
126
+ Then `touch STOP` and run again to watch the kill-switch refuse everything and land in
127
+ the ledger anyway (the halt is on the record too; silence is impossible).
89
128
 
90
129
  For the LLM side, see `examples/sentinel/` — a complete service-health watchdog: deterministic
91
130
  probes → an escalate-only LLM verdict → a typed diagnosis → contract-bounded remediation.
@@ -41,6 +41,16 @@ contracts:
41
41
  scope: checkout-stack
42
42
  tier: act # observe | propose | act
43
43
  allows: [restart_worker]
44
+ - name: cache
45
+ status: active
46
+ scope: checkout-stack
47
+ tier: propose # may draft fixes; a human executes them
48
+ allows: [clear_cache]
49
+ - name: payments-db
50
+ status: proposed # not signed off — grants NOTHING until active
51
+ scope: checkout-stack
52
+ tier: act
53
+ allows: [failover_replica]
44
54
  ```
45
55
 
46
56
  ```python
@@ -49,8 +59,15 @@ from pathlib import Path
49
59
 
50
60
  from bounded_agent import ActionSet, ContractBook, Envelope, EvidenceLedger, Gate
51
61
 
62
+ print("1. the book — who may do what, declared by a human:")
52
63
  book = ContractBook.load("contracts.yaml") # raises loudly if missing/malformed
53
- granted = book.granted(Gate(required_scope="checkout-stack", required_tier="act"))
64
+ for c in book.contracts:
65
+ print(f" {c.name}: status={c.status} scope={c.scope} tier={c.tier} allows={list(c.allows)}")
66
+
67
+ gate = Gate(required_scope="checkout-stack", required_tier="act")
68
+ granted = book.granted(gate)
69
+ print(f"\n2. the gate — scope={gate.required_scope!r} at tier '{gate.required_tier}' grants:")
70
+ print(f" {granted or 'nothing — no active, in-scope contract at this tier'}")
54
71
 
55
72
  actions = ActionSet() # the implemented closed set:
56
73
  actions.register("restart_worker", lambda: (True, "worker restarted"))
@@ -58,20 +75,42 @@ actions.register("restart_worker", lambda: (True, "worker restarted"))
58
75
  env = Envelope(armed=True, # ships disarmed; arming is explicit
59
76
  kill_switch_path=Path("STOP"), # `touch STOP` halts everything
60
77
  max_actions_per_run=2,
61
- notify=print) # act-then-report (stand in a pager here)
78
+ notify=lambda msg: print(f" [report] {msg}"))
79
+ ks = "PRESENT" if env.kill_switch_path.exists() else "absent"
80
+ print(f"\n3. the envelope — armed={env.armed}, kill-switch {ks}, breaker {env.max_actions_per_run}/run")
62
81
 
82
+ print("\n4. executing (double-closed: contracted AND registered; every outcome reported):")
63
83
  outcome = actions.execute(list(granted.items()), env,
64
84
  ledger=EvidenceLedger(Path("ledger")),
65
85
  period=dt.date.today())
66
- print(outcome.executed, "action(s) —", outcome.records[0].status)
86
+
87
+ print(f"\n5. outcome — {outcome.summary()}")
88
+ print(f" ledger: ledger/{dt.date.today().isoformat()}.jsonl (append-only; every run adds lines)")
89
+ ```
90
+
91
+ Run it and the output narrates the authorization chain — note the gate silently denying
92
+ `cache` (propose tier) and `payments-db` (not signed off):
93
+
67
94
  ```
95
+ 1. the book — who may do what, declared by a human:
96
+ queue-worker: status=active scope=checkout-stack tier=act allows=['restart_worker']
97
+ cache: status=active scope=checkout-stack tier=propose allows=['clear_cache']
98
+ payments-db: status=proposed scope=checkout-stack tier=act allows=['failover_replica']
99
+
100
+ 2. the gate — scope='checkout-stack' at tier 'act' grants:
101
+ {'queue-worker': ('restart_worker',)}
68
102
 
69
- Both files as-is, `python` it, and you'll see the act-then-report line fire and one
70
- `executed-ok` record land in `ledger/` — the whole authorization chain in 20 lines.
103
+ 3. the envelope armed=True, kill-switch absent, breaker 2/run
104
+
105
+ 4. executing (double-closed: contracted AND registered; every outcome reported):
106
+ [report] executed-ok: restart_worker on queue-worker (worker restarted)
107
+
108
+ 5. outcome — 1 executed | queue-worker:restart_worker=executed-ok
109
+ ledger: ledger/2026-08-01.jsonl (append-only; every run adds lines)
110
+ ```
71
111
 
72
- An action fires only if it appears in **both** the contract's `allows` list **and** the
73
- registered set and only inside an armed envelope with no kill-switch present and breaker
74
- budget remaining. Everything that happens (and everything refused) is one JSONL line.
112
+ Then `touch STOP` and run again to watch the kill-switch refuse everything and land in
113
+ the ledger anyway (the halt is on the record too; silence is impossible).
75
114
 
76
115
  For the LLM side, see `examples/sentinel/` — a complete service-health watchdog: deterministic
77
116
  probes → an escalate-only LLM verdict → a typed diagnosis → contract-bounded remediation.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bounded-agent"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Contract-bounded autonomy for LLM agents: five small primitives that make an agent safe to leave alone."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -57,6 +57,15 @@ class ExecOutcome(BaseModel):
57
57
  records: tuple[ActionRecord, ...] = ()
58
58
  executed: int = 0 # attempts, successful or not
59
59
 
60
+ def summary(self) -> str:
61
+ """One human line for logs/pagers: what ran, what was refused, what halted."""
62
+ if self.blocked_reason is not None:
63
+ return f"blocked: {self.blocked_reason}"
64
+ if not self.records:
65
+ return "0 executed (nothing actionable)"
66
+ parts = [f"{r.contract}:{r.action}={r.status}" for r in self.records]
67
+ return f"{self.executed} executed | " + "; ".join(parts)
68
+
60
69
 
61
70
  class ActionSet:
62
71
  """The registered half of the double-closed check, plus the execution loop."""
@@ -107,3 +107,18 @@ def test_every_record_is_notified_and_ledgered(tmp_path):
107
107
  ledger=ledger, period=DAY)
108
108
  lines = (tmp_path / "ev" / f"{DAY.isoformat()}.jsonl").read_text().splitlines()
109
109
  assert len(lines) == 2 and len(seen) == 2 # the refusal is recorded too
110
+
111
+
112
+ def test_outcome_summary_reads_like_a_report():
113
+ calls: list[str] = []
114
+ out = _actions(calls).execute([("svc", ("restart_worker", "drop_database"))],
115
+ Envelope(armed=True, max_actions_per_run=5))
116
+ s = out.summary()
117
+ assert "1 executed" in s
118
+ assert "svc:restart_worker=executed-ok" in s
119
+ assert "svc:drop_database=refused" in s
120
+
121
+
122
+ def test_outcome_summary_when_blocked():
123
+ out = _actions([]).execute([("svc", ("restart_worker",))], Envelope())
124
+ assert out.summary() == "blocked: disarmed"
@@ -45,7 +45,7 @@ wheels = [
45
45
 
46
46
  [[package]]
47
47
  name = "bounded-agent"
48
- version = "0.1.0"
48
+ version = "0.1.3"
49
49
  source = { editable = "." }
50
50
  dependencies = [
51
51
  { name = "pydantic" },
File without changes
File without changes