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.
- bantamkit/__init__.py +32 -0
- bantamkit/agent.py +458 -0
- bantamkit/assets/contracts/default.yaml +90 -0
- bantamkit/assets/evals/devteam/manifest.yaml +351 -0
- bantamkit/assets/evals/devteam/repo/HISTORY.md +18 -0
- bantamkit/assets/evals/devteam/repo/README.md +12 -0
- bantamkit/assets/evals/devteam/repo/docs/architecture.md +17 -0
- bantamkit/assets/evals/devteam/repo/docs/runbook.md +10 -0
- bantamkit/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
- bantamkit/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/config.py +35 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/report.py +9 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
- bantamkit/assets/evals/devteam/repo/tests/test_posting.py +13 -0
- bantamkit/assets/evals/devteam/repo/tests/test_settle.py +9 -0
- bantamkit/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
- bantamkit/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
- bantamkit/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
- bantamkit/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
- bantamkit/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
- bantamkit/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
- bantamkit/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
- bantamkit/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-137.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-261.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-388.yaml +37 -0
- bantamkit/assets/evals/fixtures/.gitkeep +0 -0
- bantamkit/assets/evals/fixtures/catalog.json +6 -0
- bantamkit/assets/evals/perturbations/task-completion.yaml +576 -0
- bantamkit/assets/evals/tasks/.gitkeep +0 -0
- bantamkit/assets/evals/tasks/extract-contact.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-invoice.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-order.yaml +15 -0
- bantamkit/assets/evals/tasks/extract-schedule.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-versions.yaml +17 -0
- bantamkit/assets/evals/tasks/nav-prod-port.yaml +84 -0
- bantamkit/assets/evals/tasks/nav-release-bundle.yaml +87 -0
- bantamkit/assets/evals/tasks/recall-audit-retention.yaml +17 -0
- bantamkit/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-db-port.yaml +17 -0
- bantamkit/assets/evals/tasks/recall-deploy.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
- bantamkit/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
- bantamkit/assets/evals/tasks/recall-oncall.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-org-quota.yaml +18 -0
- bantamkit/assets/evals/tasks/recall-owner.yaml +13 -0
- bantamkit/assets/evals/tasks/shop-basket-total.yaml +10 -0
- bantamkit/assets/evals/tasks/shop-cheapest.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-compare.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-gadget-value.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-stock-total.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-total.yaml +9 -0
- bantamkit/assets/profiles/default.yaml +31 -0
- bantamkit/assets/profiles/patient.yaml +31 -0
- bantamkit/assets/rubrics/.gitkeep +0 -0
- bantamkit/assets/rubrics/code-quality.yaml +20 -0
- bantamkit/assets/rubrics/grounded-completion.yaml +37 -0
- bantamkit/assets/rubrics/task-completion.yaml +28 -0
- bantamkit/assets/schemas/shiftwork-checkpoint.json +188 -0
- bantamkit/assets/skills/.gitkeep +0 -0
- bantamkit/assets/skills/file-graph.md +7 -0
- bantamkit/assets/skills/memory.md +35 -0
- bantamkit/assets/tools/.gitkeep +0 -0
- bantamkit/assets/tools/bantamkit_read.json +48 -0
- bantamkit/assets/tools/bantamkit_status.json +25 -0
- bantamkit/assets/tools/build_identity.json +17 -0
- bantamkit/assets/tools/document_list.json +12 -0
- bantamkit/assets/tools/document_read.json +31 -0
- bantamkit/assets/tools/file_graph.json +12 -0
- bantamkit/assets/tools/memory_compact.json +31 -0
- bantamkit/assets/tools/memory_recall.json +38 -0
- bantamkit/assets/tools/memory_save.json +61 -0
- bantamkit/assets/tools/shiftwork_clock_in.json +25 -0
- bantamkit/assets/tools/shiftwork_clock_out.json +60 -0
- bantamkit/assets/tools/shiftwork_status.json +25 -0
- bantamkit/assets/tools/skill_audit.json +70 -0
- bantamkit/assets/tools/validate_json.json +31 -0
- bantamkit/assets.py +67 -0
- bantamkit/budget.py +114 -0
- bantamkit/client.py +329 -0
- bantamkit/contract.py +522 -0
- bantamkit/criticreplay.py +3241 -0
- bantamkit/critique.py +301 -0
- bantamkit/docread.py +1744 -0
- bantamkit/evalrun.py +2003 -0
- bantamkit/eventlog.py +282 -0
- bantamkit/filegraph.py +218 -0
- bantamkit/loopguard.py +101 -0
- bantamkit/mcpreport.py +763 -0
- bantamkit/mcpserver.py +1334 -0
- bantamkit/memory/__init__.py +28 -0
- bantamkit/memory/__main__.py +291 -0
- bantamkit/memory/component.py +569 -0
- bantamkit/memory/divergence.py +744 -0
- bantamkit/memory/layers.py +257 -0
- bantamkit/memory/store.py +940 -0
- bantamkit/pdfread.py +1402 -0
- bantamkit/profile.py +46 -0
- bantamkit/shiftwork.py +212 -0
- bantamkit/skillaudit.py +853 -0
- bantamkit/statusline.py +313 -0
- bantamkit/structured.py +125 -0
- bantamkit/textutil.py +30 -0
- bantamkit-0.27.0.dist-info/METADATA +207 -0
- bantamkit-0.27.0.dist-info/RECORD +119 -0
- bantamkit-0.27.0.dist-info/WHEEL +4 -0
- 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
|
+
}
|