bantamkit 0.27.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 (119) hide show
  1. bantamkit/__init__.py +32 -0
  2. bantamkit/agent.py +458 -0
  3. bantamkit/assets/contracts/default.yaml +90 -0
  4. bantamkit/assets/evals/devteam/manifest.yaml +351 -0
  5. bantamkit/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. bantamkit/assets/evals/devteam/repo/README.md +12 -0
  7. bantamkit/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. bantamkit/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. bantamkit/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. bantamkit/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. bantamkit/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. bantamkit/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. bantamkit/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. bantamkit/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. bantamkit/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. bantamkit/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. bantamkit/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. bantamkit/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. bantamkit/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. bantamkit/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. bantamkit/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. bantamkit/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. bantamkit/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. bantamkit/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. bantamkit/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. bantamkit/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. bantamkit/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. bantamkit/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. bantamkit/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. bantamkit/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. bantamkit/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. bantamkit/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. bantamkit/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. bantamkit/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. bantamkit/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. bantamkit/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. bantamkit/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. bantamkit/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. bantamkit/assets/evals/fixtures/.gitkeep +0 -0
  40. bantamkit/assets/evals/fixtures/catalog.json +6 -0
  41. bantamkit/assets/evals/perturbations/task-completion.yaml +576 -0
  42. bantamkit/assets/evals/tasks/.gitkeep +0 -0
  43. bantamkit/assets/evals/tasks/extract-contact.yaml +14 -0
  44. bantamkit/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. bantamkit/assets/evals/tasks/extract-order.yaml +15 -0
  46. bantamkit/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. bantamkit/assets/evals/tasks/extract-versions.yaml +17 -0
  48. bantamkit/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. bantamkit/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. bantamkit/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. bantamkit/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. bantamkit/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. bantamkit/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. bantamkit/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. bantamkit/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. bantamkit/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. bantamkit/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. bantamkit/assets/evals/tasks/recall-owner.yaml +13 -0
  59. bantamkit/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. bantamkit/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. bantamkit/assets/evals/tasks/shop-compare.yaml +9 -0
  62. bantamkit/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. bantamkit/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. bantamkit/assets/evals/tasks/shop-total.yaml +9 -0
  65. bantamkit/assets/profiles/default.yaml +31 -0
  66. bantamkit/assets/profiles/patient.yaml +31 -0
  67. bantamkit/assets/rubrics/.gitkeep +0 -0
  68. bantamkit/assets/rubrics/code-quality.yaml +20 -0
  69. bantamkit/assets/rubrics/grounded-completion.yaml +37 -0
  70. bantamkit/assets/rubrics/task-completion.yaml +28 -0
  71. bantamkit/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. bantamkit/assets/skills/.gitkeep +0 -0
  73. bantamkit/assets/skills/file-graph.md +7 -0
  74. bantamkit/assets/skills/memory.md +35 -0
  75. bantamkit/assets/tools/.gitkeep +0 -0
  76. bantamkit/assets/tools/bantamkit_read.json +48 -0
  77. bantamkit/assets/tools/bantamkit_status.json +25 -0
  78. bantamkit/assets/tools/build_identity.json +17 -0
  79. bantamkit/assets/tools/document_list.json +12 -0
  80. bantamkit/assets/tools/document_read.json +31 -0
  81. bantamkit/assets/tools/file_graph.json +12 -0
  82. bantamkit/assets/tools/memory_compact.json +31 -0
  83. bantamkit/assets/tools/memory_recall.json +38 -0
  84. bantamkit/assets/tools/memory_save.json +61 -0
  85. bantamkit/assets/tools/shiftwork_clock_in.json +25 -0
  86. bantamkit/assets/tools/shiftwork_clock_out.json +60 -0
  87. bantamkit/assets/tools/shiftwork_status.json +25 -0
  88. bantamkit/assets/tools/skill_audit.json +70 -0
  89. bantamkit/assets/tools/validate_json.json +31 -0
  90. bantamkit/assets.py +67 -0
  91. bantamkit/budget.py +114 -0
  92. bantamkit/client.py +329 -0
  93. bantamkit/contract.py +522 -0
  94. bantamkit/criticreplay.py +3241 -0
  95. bantamkit/critique.py +301 -0
  96. bantamkit/docread.py +1744 -0
  97. bantamkit/evalrun.py +2003 -0
  98. bantamkit/eventlog.py +282 -0
  99. bantamkit/filegraph.py +218 -0
  100. bantamkit/loopguard.py +101 -0
  101. bantamkit/mcpreport.py +763 -0
  102. bantamkit/mcpserver.py +1334 -0
  103. bantamkit/memory/__init__.py +28 -0
  104. bantamkit/memory/__main__.py +291 -0
  105. bantamkit/memory/component.py +569 -0
  106. bantamkit/memory/divergence.py +744 -0
  107. bantamkit/memory/layers.py +257 -0
  108. bantamkit/memory/store.py +940 -0
  109. bantamkit/pdfread.py +1402 -0
  110. bantamkit/profile.py +46 -0
  111. bantamkit/shiftwork.py +212 -0
  112. bantamkit/skillaudit.py +853 -0
  113. bantamkit/statusline.py +313 -0
  114. bantamkit/structured.py +125 -0
  115. bantamkit/textutil.py +30 -0
  116. bantamkit-0.27.0.dist-info/METADATA +207 -0
  117. bantamkit-0.27.0.dist-info/RECORD +119 -0
  118. bantamkit-0.27.0.dist-info/WHEEL +4 -0
  119. bantamkit-0.27.0.dist-info/entry_points.txt +2 -0
bantamkit/profile.py ADDED
@@ -0,0 +1,46 @@
1
+ """Layer 4 — named tunable profiles: policy numbers live in the asset pack, not in code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import yaml
6
+
7
+ from bantamkit.assets import AssetNotFound, assets_root
8
+ from bantamkit.client import BantamError
9
+
10
+ REQUIRED = {
11
+ "agent": ("max_turns", "observation_budget"),
12
+ "structured": ("max_retries",),
13
+ "schema_gate": ("max_attempts",),
14
+ "json_answer": ("max_attempts",),
15
+ "critique": ("max_rounds", "evidence_budget"),
16
+ "token_budget": ("ceiling", "optional_cutoff"),
17
+ "loop_guard": ("inject_at", "warn_at"),
18
+ }
19
+
20
+
21
+ def load_profile(name: str = "default") -> dict:
22
+ path = assets_root() / "profiles" / f"{name}.yaml"
23
+ if not path.exists():
24
+ raise AssetNotFound(f"profile asset not found: {path}")
25
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
26
+ for section, keys in REQUIRED.items():
27
+ missing = [k for k in keys if k not in data.get(section, {})]
28
+ if missing:
29
+ raise BantamError(
30
+ f"profile '{name}' section '{section}' missing key(s): {', '.join(missing)}"
31
+ )
32
+ return data
33
+
34
+
35
+ def default(section: str, key: str) -> int:
36
+ return int(load_profile()[section][key])
37
+
38
+
39
+ def default_float(section: str, key: str) -> float:
40
+ """Same lookup for the numbers that are fractions, not counts.
41
+
42
+ Separate from `default` rather than loosening its return type: every existing
43
+ caller wants an int and an accidental 0.75 -> 0 truncation is exactly the kind
44
+ of silent policy change this layer exists to prevent.
45
+ """
46
+ return float(load_profile()[section][key])
bantamkit/shiftwork.py ADDED
@@ -0,0 +1,212 @@
1
+ """Shift-work checkpoint operations for the MCP flavor: mirror the driver, never diverge.
2
+
3
+ The driver (tools/shiftwork/driver.py) spawns sessions from outside; these
4
+ functions serve the inverse topology — an already-running orchestrator session
5
+ clocking subagents in and out of one checkpoint. Same contract, same schema
6
+ asset, same discipline:
7
+
8
+ * every read full-schema-validates first (`load_schema("shiftwork-checkpoint")`
9
+ + the `schema_error` engine sessions use under the driver);
10
+ * ESCALATE (non-empty `handoff.open_questions`) and SUCCESS (every unit
11
+ done/dropped) come back as structured refusals, never exceptions;
12
+ * clock-out only accepts the cursor unit — the contract is
13
+ execute-the-cursor-unit (driver parity), never pick-a-unit;
14
+ * clock-out validates the ENTIRE mutated document before writing, then writes
15
+ atomically (temp file + rename, the driver's pattern) — a validation failure
16
+ writes nothing, and every write-path OSError comes back as a structured
17
+ `{"result": "error"}` refusal, mirroring the read side;
18
+ * every clock-out appends one line to `<checkpoint>.log.jsonl` beside the
19
+ checkpoint (unit, role, status, ts, plus orchestrator-reported accounting)
20
+ — the driver-log shape, so the history ring's 5-entry cap never loses
21
+ measurement data. The log is append-only and never read here.
22
+
23
+ Log-then-commit ordering: the accounting line is appended BEFORE the atomic
24
+ checkpoint rename, so a partial failure can lose the commit but never the
25
+ accounting. The recovery semantic is one line: a log line whose commit failed
26
+ is detectable by re-reading the checkpoint (its unit is still the non-terminal
27
+ cursor unit), so a retried clock-out may leave one duplicate log line — never
28
+ a missing one.
29
+
30
+ Cursor advance is v1-linear: it moves to the first non-terminal unit in plan
31
+ order and ignores `depends_on` — non-linear plans need a planner unit to
32
+ reorder `plan.units` first.
33
+
34
+ No lock: the MCP topology has one orchestrator by construction. The driver's
35
+ O_EXCL lock guards cross-process races this shape does not have, and clock-out
36
+ re-validates before writing so a concurrent driver run fails validation-visibly.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import contextlib
42
+ import json
43
+ import time
44
+ from pathlib import Path
45
+ from typing import Any
46
+
47
+ from bantamkit.assets import load_schema
48
+ from bantamkit.contract import schema_error
49
+
50
+ SCHEMA_NAME = "shiftwork-checkpoint"
51
+ TERMINAL_UNIT_STATUS = frozenset({"done", "dropped"}) # the driver's SUCCESS test
52
+ HISTORY_RING_SIZE = 5 # the schema's maxItems — older entries fall off the ring
53
+
54
+
55
+ def _error(reason: str) -> dict[str, Any]:
56
+ return {"result": "error", "reason": reason}
57
+
58
+
59
+ def _read_valid(path: Path) -> tuple[dict | None, dict[str, Any] | None]:
60
+ """Read and full-schema-validate. Returns (document, None) or (None, refusal)."""
61
+ try:
62
+ text = path.read_text(encoding="utf-8")
63
+ except OSError as e:
64
+ return None, _error(f"checkpoint unreadable: {e}")
65
+ try:
66
+ document = json.loads(text)
67
+ except json.JSONDecodeError as e:
68
+ return None, _error(f"checkpoint is not parseable as JSON: {e}")
69
+ problem = schema_error(text, load_schema(SCHEMA_NAME))
70
+ if problem is not None:
71
+ return None, _error(f"checkpoint invalid: {problem}")
72
+ return document, None
73
+
74
+
75
+ def _find_unit(document: dict, unit_id: str) -> dict | None:
76
+ for unit in document["plan"]["units"]:
77
+ if unit.get("id") == unit_id:
78
+ return unit
79
+ return None
80
+
81
+
82
+ def _timestamp(now: float) -> str:
83
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now))
84
+
85
+
86
+ def clock_in(checkpoint: str) -> dict[str, Any]:
87
+ """Validate the checkpoint and return the cursor unit's brief, or a refusal.
88
+
89
+ Refusals mirror the driver's terminal exits: ``escalate`` when
90
+ `handoff.open_questions` is non-empty or the cursor names no unit,
91
+ ``success`` when every unit is done/dropped. The ``brief`` payload is what
92
+ the orchestrator hands to the spawned agent verbatim.
93
+ """
94
+ document, refusal = _read_valid(Path(checkpoint))
95
+ if refusal is not None:
96
+ return refusal
97
+ open_questions = document["handoff"]["open_questions"]
98
+ if open_questions:
99
+ return {
100
+ "result": "escalate",
101
+ "reason": f"open question: {open_questions[0]}",
102
+ "open_questions": open_questions,
103
+ }
104
+ units = document["plan"]["units"]
105
+ if all(u["status"] in TERMINAL_UNIT_STATUS for u in units):
106
+ return {"result": "success", "reason": "all units done or dropped"}
107
+ cursor = document["plan"]["cursor"]
108
+ unit = _find_unit(document, cursor)
109
+ if unit is None:
110
+ return {"result": "escalate", "reason": f"cursor {cursor} names no unit"}
111
+ return {
112
+ "result": "brief",
113
+ "unit": unit,
114
+ "role": unit["role"],
115
+ "invariants": document["job"]["constraints"],
116
+ "handoff": document["handoff"],
117
+ "do_not": document["handoff"]["do_not"],
118
+ "files": document["state"]["artifacts"],
119
+ }
120
+
121
+
122
+ def clock_out(
123
+ checkpoint: str,
124
+ unit_id: str,
125
+ status: str,
126
+ handoff_patch: dict[str, Any],
127
+ history_entry: dict[str, Any],
128
+ accounting: dict[str, Any] | None = None,
129
+ ) -> dict[str, Any]:
130
+ """Apply the cursor unit's result to the checkpoint, validate whole, write atomically.
131
+
132
+ `unit_id` must name the cursor unit — the contract is execute-the-cursor
133
+ (driver parity); anything else is a structured error. Mutations: set the
134
+ unit's status, advance `plan.cursor` to the first non-terminal unit
135
+ (v1-linear, `depends_on` is ignored), shallow-merge `handoff_patch` into
136
+ `handoff`, push `history_entry` onto the 5-entry ring. The mutated
137
+ document is validated against the full schema BEFORE any write — a
138
+ validation failure writes nothing. Then log-then-commit: the accounting
139
+ line is appended to `<checkpoint>.log.jsonl` first, the checkpoint is
140
+ renamed into place second, and either step's OSError is a structured
141
+ error that leaves the prior checkpoint bytes intact (a log line whose
142
+ commit failed is the detectable, tolerable leftover).
143
+ """
144
+ path = Path(checkpoint)
145
+ document, refusal = _read_valid(path)
146
+ if refusal is not None:
147
+ return refusal
148
+ unit = _find_unit(document, unit_id)
149
+ if unit is None:
150
+ return _error(f"unit {unit_id} is not in the plan")
151
+ cursor = document["plan"]["cursor"]
152
+ if unit_id != cursor:
153
+ return _error(f"unit {unit_id} is not the cursor unit {cursor}")
154
+
155
+ unit["status"] = status
156
+ remaining = [u for u in document["plan"]["units"] if u["status"] not in TERMINAL_UNIT_STATUS]
157
+ document["plan"]["cursor"] = remaining[0]["id"] if remaining else unit_id
158
+ document["handoff"].update(handoff_patch or {})
159
+ document["history"] = (document["history"] + [history_entry])[-HISTORY_RING_SIZE:]
160
+
161
+ problem = schema_error(json.dumps(document), load_schema(SCHEMA_NAME))
162
+ if problem is not None:
163
+ return _error(f"refused to write: {problem}")
164
+
165
+ record: dict[str, Any] = {
166
+ "ts": _timestamp(time.time()),
167
+ "unit": unit_id,
168
+ "role": unit["role"],
169
+ "status": status,
170
+ }
171
+ record.update(accounting or {})
172
+ log_path = Path(str(path) + ".log.jsonl")
173
+ try:
174
+ with log_path.open("a", encoding="utf-8") as fh:
175
+ fh.write(json.dumps(record, sort_keys=True) + "\n")
176
+ except OSError as e:
177
+ return _error(f"accounting log unwritable, checkpoint untouched: {e}")
178
+
179
+ tmp = path.with_suffix(path.suffix + ".tmp")
180
+ try:
181
+ tmp.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
182
+ tmp.replace(path)
183
+ except OSError as e:
184
+ with contextlib.suppress(OSError):
185
+ tmp.unlink()
186
+ return _error(f"checkpoint unwritable, last log line uncommitted: {e}")
187
+
188
+ return {
189
+ "result": "ok",
190
+ "unit": unit_id,
191
+ "status": status,
192
+ "cursor": document["plan"]["cursor"],
193
+ "log": str(log_path),
194
+ }
195
+
196
+
197
+ def status(checkpoint: str) -> dict[str, Any]:
198
+ """Read-only progress summary. Never mutates."""
199
+ document, refusal = _read_valid(Path(checkpoint))
200
+ if refusal is not None:
201
+ return refusal
202
+ counts: dict[str, int] = {}
203
+ for unit in document["plan"]["units"]:
204
+ counts[unit["status"]] = counts.get(unit["status"], 0) + 1
205
+ history = document["history"]
206
+ return {
207
+ "result": "status",
208
+ "cursor": document["plan"]["cursor"],
209
+ "units": counts,
210
+ "open_questions": len(document["handoff"]["open_questions"]),
211
+ "last_history": history[-1] if history else None,
212
+ }