agent-collab-kit 0.1.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ALTA VISTA360, S.L.U.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-collab-kit
3
+ Version: 0.1.1
4
+ Summary: Local SQLite mailbox, acknowledgements and cooperative leases for AI agents on one trusted machine. Python standard library only.
5
+ Author: ALTA VISTA360, S.L.U.
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kanibal-studio/agent-collab-kit
8
+ Project-URL: Issues, https://github.com/kanibal-studio/agent-collab-kit/issues
9
+ Project-URL: PyPI, https://pypi.org/project/agent-collab-kit/
10
+ Keywords: ai-agents,multi-agent,coordination,sqlite,mailbox,leases,claude-code
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # Agent Collab Kit
28
+
29
+ A small Python 3.10+ standard-library tool for cooperative work on one trusted
30
+ machine: durable messages, explicit acknowledgements, and leases for exact
31
+ resource names. Inspired by operational patterns, this is an original standalone
32
+ implementation, not an export of a production system. It runs without a provider,
33
+ agent framework, cloud service, Git execution, or automatic orchestration.
34
+ It does not claim a new coordination model.
35
+
36
+ ## Try it with an isolated database
37
+
38
+ Run from this directory on a local disk. The example uses a temporary directory
39
+ and fictitious participants; never point a demonstration at a live project DB.
40
+ Shell commands below use a POSIX shell:
41
+
42
+ ```sh
43
+ DEMO_DIR=$(mktemp -d)
44
+ DB="$DEMO_DIR/demo.db"
45
+ python3 agent_collab.py --db "$DB" init
46
+ python3 agent_collab.py --db "$DB" send --sender worker-a --recipient integrator --key demo-result-1 --body 'Addition is ready for review'
47
+ python3 agent_collab.py --db "$DB" inbox --recipient integrator
48
+ # Copy the returned message id into MESSAGE_ID.
49
+ python3 agent_collab.py --db "$DB" ack --recipient integrator --id MESSAGE_ID
50
+ python3 agent_collab.py --db "$DB" claim --resource src/add.py --owner worker-a --ttl 300
51
+ # Copy the returned generation UUID into GENERATION.
52
+ python3 agent_collab.py --db "$DB" renew --resource src/add.py --owner worker-a --generation GENERATION --ttl 300
53
+ python3 agent_collab.py --db "$DB" leases
54
+ python3 agent_collab.py --db "$DB" release --resource src/add.py --owner worker-a --generation GENERATION
55
+ python3 -m unittest discover -s tests -v
56
+ ```
57
+
58
+ All commands require `--db`. `init` is repeatable. Commands return JSON on success,
59
+ exit 0, or an error on stderr, exit 1 (invalid CLI syntax exits 2). An idempotency
60
+ key is unique across the database: repeating the same sender, recipient and body
61
+ returns the existing message, including its acknowledgement state. Reusing a key
62
+ for different content fails. `inbox` shows only pending mail for the named recipient.
63
+ `ack` requires that recipient name and is repeatable. Acknowledgement means the
64
+ caller marked the message received; it does not prove work was delivered or tested.
65
+
66
+ ## Cooperative leases and recovery
67
+
68
+ Use the fictitious [collaboration board](examples/COLLAB.md) to agree on distinct
69
+ responsibilities and one integrator. Claim every exact resource before editing;
70
+ use a shared resource such as `__commit__` to serialize integration by agreement.
71
+ Resource names are opaque and case-sensitive. `src`, `src/a.py`, and `src/A.py`
72
+ are separate resources. Paths are not normalized, folders have no recursive
73
+ coverage, and aliases such as `./src/a.py` require a naming convention.
74
+
75
+ A claim creates a fresh generation UUID. An active claim conflicts even for the
76
+ same owner; renew using the current generation. TTL must be finite, positive and
77
+ at most 86400 seconds. Renew and release require the matching owner and generation
78
+ and an unexpired lease. At the exact expiration time, a new claim may take over.
79
+ An expired token cannot renew or release its successor, even with the same owner.
80
+ `leases` lists active leases only; expired rows remain until replaced.
81
+
82
+ After a crash, reopen the same database, inspect pending messages and leases,
83
+ check the actual work and running processes, then agree on a takeover. There is
84
+ no forced release. Lease expiry does not stop the previous worker or undo edits:
85
+ a paused worker may resume after takeover. Workers must check ownership and stop
86
+ on loss; this tool provides no filesystem fencing. The test suite demonstrates
87
+ expiry, stale tokens, persistence and two real processes racing for one claim.
88
+
89
+ Every mutation uses SQLite `BEGIN IMMEDIATE` and a 10-second busy timeout.
90
+ A timeout is an error, not a successful claim; inspect state before retrying.
91
+ The API accepts an injected clock for tests; normal operation trusts the wall
92
+ clock. Clock corrections can shorten or prolong leases. Close connections before
93
+ making a consistent backup, and retain SQLite sidecars if present. No background
94
+ service, watcher, automatic renewal, pruning or deletion is supplied.
95
+
96
+ ## Trust and limits
97
+
98
+ Use a single trusted machine and a local disk. Network filesystems and separate
99
+ hosts are unsupported. Names and UUIDs are cooperative tokens, not authenticated
100
+ identities or security boundaries: anyone with database access can impersonate a
101
+ participant, read messages, or alter state. Use operating-system permissions and
102
+ keep secrets out of message bodies. Coordination does not guarantee exclusive
103
+ writes, safe Git operations, exactly-once external effects, or code quality.
104
+ The tool reads no production configuration and invokes no external commands.
105
+
106
+ Database files and common SQLite sidecars are ignored by Git; keep any custom
107
+ DB filename outside the repository or extend the ignore rules yourself.
108
+
109
+ ## Help project agents improve
110
+
111
+ The [supervised improvement method](examples/AGENT_IMPROVEMENT.md) shows how
112
+ business agents can contribute evidence and proposals to development, with an
113
+ isolated reproduction, regression and counterfactual checks, cost/quality
114
+ measurement, an integrator's adoption or rejection decision, and learning saved
115
+ in artifacts the agent actually consumes. Its triage example is fictional.
116
+ The kit coordinates this loop through mail and exact-resource claims; it does
117
+ not train models or authorize operational actions automatically.
118
+
119
+ ## Use with Claude Code
120
+
121
+ Run one Claude Code session per worker on the same machine and give every session
122
+ the same database, kept outside any repository:
123
+
124
+ ```sh
125
+ mkdir -p ~/.agent-collab
126
+ python3 /path/to/agent_collab.py --db ~/.agent-collab/team.db init
127
+ ```
128
+
129
+ A short block in each project's `CLAUDE.md` is enough for the sessions to use it:
130
+
131
+ ```markdown
132
+ ## Coordination
133
+ - At the start of a task, read your mail:
134
+ `python3 /path/to/agent_collab.py --db ~/.agent-collab/team.db inbox --recipient <your-name>`,
135
+ then acknowledge each message you have handled with `ack`.
136
+ - Before editing a file, claim its exact path with `claim --resource <path> --owner <your-name> --ttl 900`,
137
+ renew with the returned generation while you work, and `release` it when done.
138
+ - If a claim is refused, do not edit that file: `send` a message to the current owner instead.
139
+ - Only the integrator commits, after claiming `__commit__`.
140
+ ```
141
+
142
+ Each session runs these commands through its usual shell tool, so your permission
143
+ settings still apply. The leases stay cooperative: they guide sessions that follow
144
+ the instruction and do not stop one that ignores it.
145
+
146
+ ## Project context and license
147
+
148
+ Prepared by [Kanibal Studio](https://github.com/kanibal-studio), associated with
149
+ ALTA VISTA360, S.L.U. Our public sites are [K-Pulse](https://infos.kanibal-studio.com/),
150
+ [Altavista360](https://altavista360.com), and
151
+ [Altavista Conseil](https://altavista-conseil.com). These links provide context;
152
+ this local tool has no integration with those sites.
153
+
154
+ This package is released under the [MIT license](LICENSE).
155
+
156
+ **En français :** cet outil local facilite la coordination volontaire de plusieurs
157
+ agents : messages persistants, accusés de réception explicites et baux sur noms
158
+ exacts. Il ne contrôle ni les identités ni les fichiers. Un seul intégrateur
159
+ vérifie et intègre le travail. Les exemples et tests utilisent des données fictives.
@@ -0,0 +1,133 @@
1
+ # Agent Collab Kit
2
+
3
+ A small Python 3.10+ standard-library tool for cooperative work on one trusted
4
+ machine: durable messages, explicit acknowledgements, and leases for exact
5
+ resource names. Inspired by operational patterns, this is an original standalone
6
+ implementation, not an export of a production system. It runs without a provider,
7
+ agent framework, cloud service, Git execution, or automatic orchestration.
8
+ It does not claim a new coordination model.
9
+
10
+ ## Try it with an isolated database
11
+
12
+ Run from this directory on a local disk. The example uses a temporary directory
13
+ and fictitious participants; never point a demonstration at a live project DB.
14
+ Shell commands below use a POSIX shell:
15
+
16
+ ```sh
17
+ DEMO_DIR=$(mktemp -d)
18
+ DB="$DEMO_DIR/demo.db"
19
+ python3 agent_collab.py --db "$DB" init
20
+ python3 agent_collab.py --db "$DB" send --sender worker-a --recipient integrator --key demo-result-1 --body 'Addition is ready for review'
21
+ python3 agent_collab.py --db "$DB" inbox --recipient integrator
22
+ # Copy the returned message id into MESSAGE_ID.
23
+ python3 agent_collab.py --db "$DB" ack --recipient integrator --id MESSAGE_ID
24
+ python3 agent_collab.py --db "$DB" claim --resource src/add.py --owner worker-a --ttl 300
25
+ # Copy the returned generation UUID into GENERATION.
26
+ python3 agent_collab.py --db "$DB" renew --resource src/add.py --owner worker-a --generation GENERATION --ttl 300
27
+ python3 agent_collab.py --db "$DB" leases
28
+ python3 agent_collab.py --db "$DB" release --resource src/add.py --owner worker-a --generation GENERATION
29
+ python3 -m unittest discover -s tests -v
30
+ ```
31
+
32
+ All commands require `--db`. `init` is repeatable. Commands return JSON on success,
33
+ exit 0, or an error on stderr, exit 1 (invalid CLI syntax exits 2). An idempotency
34
+ key is unique across the database: repeating the same sender, recipient and body
35
+ returns the existing message, including its acknowledgement state. Reusing a key
36
+ for different content fails. `inbox` shows only pending mail for the named recipient.
37
+ `ack` requires that recipient name and is repeatable. Acknowledgement means the
38
+ caller marked the message received; it does not prove work was delivered or tested.
39
+
40
+ ## Cooperative leases and recovery
41
+
42
+ Use the fictitious [collaboration board](examples/COLLAB.md) to agree on distinct
43
+ responsibilities and one integrator. Claim every exact resource before editing;
44
+ use a shared resource such as `__commit__` to serialize integration by agreement.
45
+ Resource names are opaque and case-sensitive. `src`, `src/a.py`, and `src/A.py`
46
+ are separate resources. Paths are not normalized, folders have no recursive
47
+ coverage, and aliases such as `./src/a.py` require a naming convention.
48
+
49
+ A claim creates a fresh generation UUID. An active claim conflicts even for the
50
+ same owner; renew using the current generation. TTL must be finite, positive and
51
+ at most 86400 seconds. Renew and release require the matching owner and generation
52
+ and an unexpired lease. At the exact expiration time, a new claim may take over.
53
+ An expired token cannot renew or release its successor, even with the same owner.
54
+ `leases` lists active leases only; expired rows remain until replaced.
55
+
56
+ After a crash, reopen the same database, inspect pending messages and leases,
57
+ check the actual work and running processes, then agree on a takeover. There is
58
+ no forced release. Lease expiry does not stop the previous worker or undo edits:
59
+ a paused worker may resume after takeover. Workers must check ownership and stop
60
+ on loss; this tool provides no filesystem fencing. The test suite demonstrates
61
+ expiry, stale tokens, persistence and two real processes racing for one claim.
62
+
63
+ Every mutation uses SQLite `BEGIN IMMEDIATE` and a 10-second busy timeout.
64
+ A timeout is an error, not a successful claim; inspect state before retrying.
65
+ The API accepts an injected clock for tests; normal operation trusts the wall
66
+ clock. Clock corrections can shorten or prolong leases. Close connections before
67
+ making a consistent backup, and retain SQLite sidecars if present. No background
68
+ service, watcher, automatic renewal, pruning or deletion is supplied.
69
+
70
+ ## Trust and limits
71
+
72
+ Use a single trusted machine and a local disk. Network filesystems and separate
73
+ hosts are unsupported. Names and UUIDs are cooperative tokens, not authenticated
74
+ identities or security boundaries: anyone with database access can impersonate a
75
+ participant, read messages, or alter state. Use operating-system permissions and
76
+ keep secrets out of message bodies. Coordination does not guarantee exclusive
77
+ writes, safe Git operations, exactly-once external effects, or code quality.
78
+ The tool reads no production configuration and invokes no external commands.
79
+
80
+ Database files and common SQLite sidecars are ignored by Git; keep any custom
81
+ DB filename outside the repository or extend the ignore rules yourself.
82
+
83
+ ## Help project agents improve
84
+
85
+ The [supervised improvement method](examples/AGENT_IMPROVEMENT.md) shows how
86
+ business agents can contribute evidence and proposals to development, with an
87
+ isolated reproduction, regression and counterfactual checks, cost/quality
88
+ measurement, an integrator's adoption or rejection decision, and learning saved
89
+ in artifacts the agent actually consumes. Its triage example is fictional.
90
+ The kit coordinates this loop through mail and exact-resource claims; it does
91
+ not train models or authorize operational actions automatically.
92
+
93
+ ## Use with Claude Code
94
+
95
+ Run one Claude Code session per worker on the same machine and give every session
96
+ the same database, kept outside any repository:
97
+
98
+ ```sh
99
+ mkdir -p ~/.agent-collab
100
+ python3 /path/to/agent_collab.py --db ~/.agent-collab/team.db init
101
+ ```
102
+
103
+ A short block in each project's `CLAUDE.md` is enough for the sessions to use it:
104
+
105
+ ```markdown
106
+ ## Coordination
107
+ - At the start of a task, read your mail:
108
+ `python3 /path/to/agent_collab.py --db ~/.agent-collab/team.db inbox --recipient <your-name>`,
109
+ then acknowledge each message you have handled with `ack`.
110
+ - Before editing a file, claim its exact path with `claim --resource <path> --owner <your-name> --ttl 900`,
111
+ renew with the returned generation while you work, and `release` it when done.
112
+ - If a claim is refused, do not edit that file: `send` a message to the current owner instead.
113
+ - Only the integrator commits, after claiming `__commit__`.
114
+ ```
115
+
116
+ Each session runs these commands through its usual shell tool, so your permission
117
+ settings still apply. The leases stay cooperative: they guide sessions that follow
118
+ the instruction and do not stop one that ignores it.
119
+
120
+ ## Project context and license
121
+
122
+ Prepared by [Kanibal Studio](https://github.com/kanibal-studio), associated with
123
+ ALTA VISTA360, S.L.U. Our public sites are [K-Pulse](https://infos.kanibal-studio.com/),
124
+ [Altavista360](https://altavista360.com), and
125
+ [Altavista Conseil](https://altavista-conseil.com). These links provide context;
126
+ this local tool has no integration with those sites.
127
+
128
+ This package is released under the [MIT license](LICENSE).
129
+
130
+ **En français :** cet outil local facilite la coordination volontaire de plusieurs
131
+ agents : messages persistants, accusés de réception explicites et baux sur noms
132
+ exacts. Il ne contrôle ni les identités ni les fichiers. Un seul intégrateur
133
+ vérifie et intègre le travail. Les exemples et tests utilisent des données fictives.
@@ -0,0 +1,234 @@
1
+ """Cooperative mail and exact-resource leases for a trusted local machine."""
2
+
3
+ __version__ = "0.1.1"
4
+
5
+ import argparse
6
+ from contextlib import contextmanager
7
+ import json
8
+ import math
9
+ import sqlite3
10
+ import sys
11
+ import time
12
+ import uuid
13
+
14
+
15
+ class Conflict(ValueError):
16
+ """An existing message or lease conflicts with this request."""
17
+
18
+
19
+ def text_value(value, name):
20
+ if not isinstance(value, str) or not value.strip() or "\x00" in value:
21
+ raise ValueError(name + " must be a nonempty string without NUL")
22
+ return value
23
+
24
+
25
+ def ttl_value(value):
26
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
27
+ raise ValueError("ttl must be numeric")
28
+ if not math.isfinite(value) or not 0 < value <= 86400:
29
+ raise ValueError("ttl must be finite, positive and <= 86400 seconds")
30
+ return value
31
+
32
+
33
+ class Store:
34
+ """Every mutating call uses one BEGIN IMMEDIATE transaction.
35
+
36
+ clock is injectable for tests; production assumes a trusted wall clock.
37
+ Opening requires an explicitly initialized database.
38
+ """
39
+
40
+ def __init__(self, db, clock=time.time):
41
+ text_value(str(db), "db")
42
+ self.clock = clock
43
+ self.conn = sqlite3.connect(str(db), timeout=10, isolation_level=None)
44
+ self.conn.row_factory = sqlite3.Row
45
+ self.conn.execute("PRAGMA busy_timeout=10000")
46
+ version = self.conn.execute("PRAGMA user_version").fetchone()[0]
47
+ if version != 1:
48
+ self.conn.close()
49
+ raise ValueError("database is not initialized; run init")
50
+
51
+ @staticmethod
52
+ def initialize(db):
53
+ text_value(str(db), "db")
54
+ conn = sqlite3.connect(str(db), timeout=10, isolation_level=None)
55
+ try:
56
+ conn.execute("PRAGMA busy_timeout=10000")
57
+ conn.execute("BEGIN IMMEDIATE")
58
+ version = conn.execute("PRAGMA user_version").fetchone()[0]
59
+ if version not in (0, 1):
60
+ raise ValueError("unsupported database version")
61
+ conn.execute("""CREATE TABLE IF NOT EXISTS messages (
62
+ id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE,
63
+ sender TEXT NOT NULL, recipient TEXT NOT NULL, body TEXT NOT NULL,
64
+ created_at REAL NOT NULL, acknowledged_at REAL)""")
65
+ conn.execute("""CREATE TABLE IF NOT EXISTS leases (
66
+ resource TEXT PRIMARY KEY COLLATE BINARY, owner TEXT NOT NULL,
67
+ generation TEXT NOT NULL, expires_at REAL NOT NULL)""")
68
+ conn.execute("PRAGMA user_version=1")
69
+ conn.execute("COMMIT")
70
+ except Exception:
71
+ if conn.in_transaction:
72
+ conn.execute("ROLLBACK")
73
+ raise
74
+ finally:
75
+ conn.close()
76
+
77
+ def close(self):
78
+ self.conn.close()
79
+
80
+ def now(self):
81
+ value = self.clock()
82
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
83
+ raise ValueError("clock must return a finite number")
84
+ if not math.isfinite(value):
85
+ raise ValueError("clock must return a finite number")
86
+ return value
87
+
88
+ @contextmanager
89
+ def transaction(self):
90
+ self.conn.execute("BEGIN IMMEDIATE")
91
+ try:
92
+ yield
93
+ self.conn.execute("COMMIT")
94
+ except Exception:
95
+ self.conn.execute("ROLLBACK")
96
+ raise
97
+
98
+ def send(self, sender, recipient, body, key):
99
+ for name, value in (("sender", sender), ("recipient", recipient),
100
+ ("body", body), ("key", key)):
101
+ text_value(value, name)
102
+ with self.transaction():
103
+ old = self.conn.execute(
104
+ "SELECT * FROM messages WHERE idempotency_key=?", (key,)
105
+ ).fetchone()
106
+ if old is not None:
107
+ if (old["sender"], old["recipient"], old["body"]) != (
108
+ sender, recipient, body):
109
+ raise Conflict("idempotency key has a different payload")
110
+ return dict(old)
111
+ identifier = str(uuid.uuid4())
112
+ self.conn.execute(
113
+ "INSERT INTO messages VALUES (?,?,?,?,?,?,NULL)",
114
+ (identifier, key, sender, recipient, body, self.now()))
115
+ return dict(self.conn.execute(
116
+ "SELECT * FROM messages WHERE id=?", (identifier,)).fetchone())
117
+
118
+ def inbox(self, recipient):
119
+ text_value(recipient, "recipient")
120
+ return [dict(row) for row in self.conn.execute(
121
+ "SELECT * FROM messages WHERE recipient=? AND acknowledged_at IS NULL "
122
+ "ORDER BY created_at, id", (recipient,))]
123
+
124
+ def ack(self, identifier, recipient):
125
+ text_value(identifier, "id")
126
+ text_value(recipient, "recipient")
127
+ with self.transaction():
128
+ row = self.conn.execute(
129
+ "SELECT * FROM messages WHERE id=? AND recipient=?",
130
+ (identifier, recipient)).fetchone()
131
+ if row is None:
132
+ raise Conflict("message not found for recipient")
133
+ if row["acknowledged_at"] is None:
134
+ self.conn.execute(
135
+ "UPDATE messages SET acknowledged_at=? WHERE id=?",
136
+ (self.now(), identifier))
137
+ return dict(self.conn.execute(
138
+ "SELECT * FROM messages WHERE id=?", (identifier,)).fetchone())
139
+
140
+ def claim(self, resource, owner, ttl):
141
+ text_value(resource, "resource")
142
+ text_value(owner, "owner")
143
+ ttl_value(ttl)
144
+ with self.transaction():
145
+ now = self.now()
146
+ row = self.conn.execute(
147
+ "SELECT * FROM leases WHERE resource=?", (resource,)).fetchone()
148
+ if row is not None and row["expires_at"] > now:
149
+ raise Conflict("resource has an active lease")
150
+ generation = str(uuid.uuid4())
151
+ self.conn.execute("INSERT OR REPLACE INTO leases VALUES (?,?,?,?)",
152
+ (resource, owner, generation, now + ttl))
153
+ return {"resource": resource, "owner": owner,
154
+ "generation": generation, "expires_at": now + ttl}
155
+
156
+ def _owned(self, resource, owner, generation, now):
157
+ for name, value in (("resource", resource), ("owner", owner),
158
+ ("generation", generation)):
159
+ text_value(value, name)
160
+ row = self.conn.execute(
161
+ "SELECT * FROM leases WHERE resource=? AND owner=? AND generation=?",
162
+ (resource, owner, generation)).fetchone()
163
+ if row is None or row["expires_at"] <= now:
164
+ raise Conflict("lease token does not identify an active lease")
165
+ return row
166
+
167
+ def renew(self, resource, owner, generation, ttl):
168
+ ttl_value(ttl)
169
+ with self.transaction():
170
+ now = self.now()
171
+ self._owned(resource, owner, generation, now)
172
+ self.conn.execute("UPDATE leases SET expires_at=? WHERE resource=?",
173
+ (now + ttl, resource))
174
+ return dict(self.conn.execute(
175
+ "SELECT * FROM leases WHERE resource=?", (resource,)).fetchone())
176
+
177
+ def release(self, resource, owner, generation):
178
+ with self.transaction():
179
+ self._owned(resource, owner, generation, self.now())
180
+ self.conn.execute("DELETE FROM leases WHERE resource=?", (resource,))
181
+ return {"released": resource}
182
+
183
+ def leases(self):
184
+ now = self.now()
185
+ return [dict(row) for row in self.conn.execute(
186
+ "SELECT * FROM leases WHERE expires_at>? ORDER BY resource", (now,))]
187
+
188
+
189
+ def main(argv=None):
190
+ parser = argparse.ArgumentParser(description=__doc__)
191
+ parser.add_argument("--db", required=True, help="explicit local SQLite file")
192
+ commands = parser.add_subparsers(dest="command", required=True)
193
+ commands.add_parser("init")
194
+ send = commands.add_parser("send")
195
+ for name in ("sender", "recipient", "body", "key"):
196
+ send.add_argument("--" + name, required=True)
197
+ inbox = commands.add_parser("inbox")
198
+ inbox.add_argument("--recipient", required=True)
199
+ ack = commands.add_parser("ack")
200
+ ack.add_argument("--id", required=True)
201
+ ack.add_argument("--recipient", required=True)
202
+ for command in ("claim", "renew", "release"):
203
+ sub = commands.add_parser(command)
204
+ sub.add_argument("--resource", required=True)
205
+ sub.add_argument("--owner", required=True)
206
+ if command != "claim":
207
+ sub.add_argument("--generation", required=True)
208
+ if command != "release":
209
+ sub.add_argument("--ttl", required=True, type=float)
210
+ commands.add_parser("leases")
211
+ args = vars(parser.parse_args(argv))
212
+ db, command = args.pop("db"), args.pop("command")
213
+ store = None
214
+ try:
215
+ if command == "init":
216
+ Store.initialize(db)
217
+ result = {"initialized": db}
218
+ else:
219
+ store = Store(db)
220
+ if command == "ack":
221
+ args["identifier"] = args.pop("id")
222
+ result = getattr(store, command)(**args)
223
+ print(json.dumps(result, ensure_ascii=False))
224
+ return 0
225
+ except (ValueError, sqlite3.Error, OSError) as exc:
226
+ print(str(exc), file=sys.stderr)
227
+ return 1
228
+ finally:
229
+ if store is not None:
230
+ store.close()
231
+
232
+
233
+ if __name__ == "__main__":
234
+ sys.exit(main())
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-collab-kit
3
+ Version: 0.1.1
4
+ Summary: Local SQLite mailbox, acknowledgements and cooperative leases for AI agents on one trusted machine. Python standard library only.
5
+ Author: ALTA VISTA360, S.L.U.
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kanibal-studio/agent-collab-kit
8
+ Project-URL: Issues, https://github.com/kanibal-studio/agent-collab-kit/issues
9
+ Project-URL: PyPI, https://pypi.org/project/agent-collab-kit/
10
+ Keywords: ai-agents,multi-agent,coordination,sqlite,mailbox,leases,claude-code
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # Agent Collab Kit
28
+
29
+ A small Python 3.10+ standard-library tool for cooperative work on one trusted
30
+ machine: durable messages, explicit acknowledgements, and leases for exact
31
+ resource names. Inspired by operational patterns, this is an original standalone
32
+ implementation, not an export of a production system. It runs without a provider,
33
+ agent framework, cloud service, Git execution, or automatic orchestration.
34
+ It does not claim a new coordination model.
35
+
36
+ ## Try it with an isolated database
37
+
38
+ Run from this directory on a local disk. The example uses a temporary directory
39
+ and fictitious participants; never point a demonstration at a live project DB.
40
+ Shell commands below use a POSIX shell:
41
+
42
+ ```sh
43
+ DEMO_DIR=$(mktemp -d)
44
+ DB="$DEMO_DIR/demo.db"
45
+ python3 agent_collab.py --db "$DB" init
46
+ python3 agent_collab.py --db "$DB" send --sender worker-a --recipient integrator --key demo-result-1 --body 'Addition is ready for review'
47
+ python3 agent_collab.py --db "$DB" inbox --recipient integrator
48
+ # Copy the returned message id into MESSAGE_ID.
49
+ python3 agent_collab.py --db "$DB" ack --recipient integrator --id MESSAGE_ID
50
+ python3 agent_collab.py --db "$DB" claim --resource src/add.py --owner worker-a --ttl 300
51
+ # Copy the returned generation UUID into GENERATION.
52
+ python3 agent_collab.py --db "$DB" renew --resource src/add.py --owner worker-a --generation GENERATION --ttl 300
53
+ python3 agent_collab.py --db "$DB" leases
54
+ python3 agent_collab.py --db "$DB" release --resource src/add.py --owner worker-a --generation GENERATION
55
+ python3 -m unittest discover -s tests -v
56
+ ```
57
+
58
+ All commands require `--db`. `init` is repeatable. Commands return JSON on success,
59
+ exit 0, or an error on stderr, exit 1 (invalid CLI syntax exits 2). An idempotency
60
+ key is unique across the database: repeating the same sender, recipient and body
61
+ returns the existing message, including its acknowledgement state. Reusing a key
62
+ for different content fails. `inbox` shows only pending mail for the named recipient.
63
+ `ack` requires that recipient name and is repeatable. Acknowledgement means the
64
+ caller marked the message received; it does not prove work was delivered or tested.
65
+
66
+ ## Cooperative leases and recovery
67
+
68
+ Use the fictitious [collaboration board](examples/COLLAB.md) to agree on distinct
69
+ responsibilities and one integrator. Claim every exact resource before editing;
70
+ use a shared resource such as `__commit__` to serialize integration by agreement.
71
+ Resource names are opaque and case-sensitive. `src`, `src/a.py`, and `src/A.py`
72
+ are separate resources. Paths are not normalized, folders have no recursive
73
+ coverage, and aliases such as `./src/a.py` require a naming convention.
74
+
75
+ A claim creates a fresh generation UUID. An active claim conflicts even for the
76
+ same owner; renew using the current generation. TTL must be finite, positive and
77
+ at most 86400 seconds. Renew and release require the matching owner and generation
78
+ and an unexpired lease. At the exact expiration time, a new claim may take over.
79
+ An expired token cannot renew or release its successor, even with the same owner.
80
+ `leases` lists active leases only; expired rows remain until replaced.
81
+
82
+ After a crash, reopen the same database, inspect pending messages and leases,
83
+ check the actual work and running processes, then agree on a takeover. There is
84
+ no forced release. Lease expiry does not stop the previous worker or undo edits:
85
+ a paused worker may resume after takeover. Workers must check ownership and stop
86
+ on loss; this tool provides no filesystem fencing. The test suite demonstrates
87
+ expiry, stale tokens, persistence and two real processes racing for one claim.
88
+
89
+ Every mutation uses SQLite `BEGIN IMMEDIATE` and a 10-second busy timeout.
90
+ A timeout is an error, not a successful claim; inspect state before retrying.
91
+ The API accepts an injected clock for tests; normal operation trusts the wall
92
+ clock. Clock corrections can shorten or prolong leases. Close connections before
93
+ making a consistent backup, and retain SQLite sidecars if present. No background
94
+ service, watcher, automatic renewal, pruning or deletion is supplied.
95
+
96
+ ## Trust and limits
97
+
98
+ Use a single trusted machine and a local disk. Network filesystems and separate
99
+ hosts are unsupported. Names and UUIDs are cooperative tokens, not authenticated
100
+ identities or security boundaries: anyone with database access can impersonate a
101
+ participant, read messages, or alter state. Use operating-system permissions and
102
+ keep secrets out of message bodies. Coordination does not guarantee exclusive
103
+ writes, safe Git operations, exactly-once external effects, or code quality.
104
+ The tool reads no production configuration and invokes no external commands.
105
+
106
+ Database files and common SQLite sidecars are ignored by Git; keep any custom
107
+ DB filename outside the repository or extend the ignore rules yourself.
108
+
109
+ ## Help project agents improve
110
+
111
+ The [supervised improvement method](examples/AGENT_IMPROVEMENT.md) shows how
112
+ business agents can contribute evidence and proposals to development, with an
113
+ isolated reproduction, regression and counterfactual checks, cost/quality
114
+ measurement, an integrator's adoption or rejection decision, and learning saved
115
+ in artifacts the agent actually consumes. Its triage example is fictional.
116
+ The kit coordinates this loop through mail and exact-resource claims; it does
117
+ not train models or authorize operational actions automatically.
118
+
119
+ ## Use with Claude Code
120
+
121
+ Run one Claude Code session per worker on the same machine and give every session
122
+ the same database, kept outside any repository:
123
+
124
+ ```sh
125
+ mkdir -p ~/.agent-collab
126
+ python3 /path/to/agent_collab.py --db ~/.agent-collab/team.db init
127
+ ```
128
+
129
+ A short block in each project's `CLAUDE.md` is enough for the sessions to use it:
130
+
131
+ ```markdown
132
+ ## Coordination
133
+ - At the start of a task, read your mail:
134
+ `python3 /path/to/agent_collab.py --db ~/.agent-collab/team.db inbox --recipient <your-name>`,
135
+ then acknowledge each message you have handled with `ack`.
136
+ - Before editing a file, claim its exact path with `claim --resource <path> --owner <your-name> --ttl 900`,
137
+ renew with the returned generation while you work, and `release` it when done.
138
+ - If a claim is refused, do not edit that file: `send` a message to the current owner instead.
139
+ - Only the integrator commits, after claiming `__commit__`.
140
+ ```
141
+
142
+ Each session runs these commands through its usual shell tool, so your permission
143
+ settings still apply. The leases stay cooperative: they guide sessions that follow
144
+ the instruction and do not stop one that ignores it.
145
+
146
+ ## Project context and license
147
+
148
+ Prepared by [Kanibal Studio](https://github.com/kanibal-studio), associated with
149
+ ALTA VISTA360, S.L.U. Our public sites are [K-Pulse](https://infos.kanibal-studio.com/),
150
+ [Altavista360](https://altavista360.com), and
151
+ [Altavista Conseil](https://altavista-conseil.com). These links provide context;
152
+ this local tool has no integration with those sites.
153
+
154
+ This package is released under the [MIT license](LICENSE).
155
+
156
+ **En français :** cet outil local facilite la coordination volontaire de plusieurs
157
+ agents : messages persistants, accusés de réception explicites et baux sur noms
158
+ exacts. Il ne contrôle ni les identités ni les fichiers. Un seul intégrateur
159
+ vérifie et intègre le travail. Les exemples et tests utilisent des données fictives.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ agent_collab.py
4
+ pyproject.toml
5
+ agent_collab_kit.egg-info/PKG-INFO
6
+ agent_collab_kit.egg-info/SOURCES.txt
7
+ agent_collab_kit.egg-info/dependency_links.txt
8
+ agent_collab_kit.egg-info/entry_points.txt
9
+ agent_collab_kit.egg-info/top_level.txt
10
+ tests/test_agent_collab.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-collab = agent_collab:main
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agent-collab-kit"
7
+ dynamic = ["version"]
8
+ description = "Local SQLite mailbox, acknowledgements and cooperative leases for AI agents on one trusted machine. Python standard library only."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "ALTA VISTA360, S.L.U." }]
14
+ keywords = [
15
+ "ai-agents",
16
+ "multi-agent",
17
+ "coordination",
18
+ "sqlite",
19
+ "mailbox",
20
+ "leases",
21
+ "claude-code",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ "Topic :: Software Development",
35
+ ]
36
+
37
+ [project.scripts]
38
+ agent-collab = "agent_collab:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/kanibal-studio/agent-collab-kit"
42
+ Issues = "https://github.com/kanibal-studio/agent-collab-kit/issues"
43
+ PyPI = "https://pypi.org/project/agent-collab-kit/"
44
+
45
+ [tool.setuptools]
46
+ py-modules = ["agent_collab"]
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = { attr = "agent_collab.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,128 @@
1
+ import multiprocessing
2
+ from pathlib import Path
3
+ import tempfile
4
+ import unittest
5
+
6
+ from agent_collab import Conflict, Store
7
+
8
+
9
+ def race_claim(db, ready, start, result):
10
+ store = Store(db)
11
+ ready.put(True)
12
+ start.wait(10)
13
+ try:
14
+ store.claim("src/shared.py", "worker", 60)
15
+ result.put("claimed")
16
+ except Conflict:
17
+ result.put("conflict")
18
+ finally:
19
+ store.close()
20
+
21
+
22
+ class StoreTests(unittest.TestCase):
23
+ def setUp(self):
24
+ self.temp = tempfile.TemporaryDirectory()
25
+ self.db = str(Path(self.temp.name) / "test.db")
26
+ self.time = 100.0
27
+ Store.initialize(self.db)
28
+ self.store = Store(self.db, clock=lambda: self.time)
29
+
30
+ def tearDown(self):
31
+ self.store.close()
32
+ self.temp.cleanup()
33
+
34
+ def test_idempotency_and_mismatch(self):
35
+ first = self.store.send("a", "b", "hello", "one")
36
+ self.assertEqual(first, self.store.send("a", "b", "hello", "one"))
37
+ for payload in (("x", "b", "hello"), ("a", "x", "hello"),
38
+ ("a", "b", "changed")):
39
+ with self.assertRaises(Conflict):
40
+ self.store.send(*payload, "one")
41
+ self.assertEqual(1, len(self.store.inbox("b")))
42
+
43
+ def test_inbox_ack_recipient_and_persistence(self):
44
+ message = self.store.send("a", "b", "hello", "one")
45
+ self.assertEqual([], self.store.inbox("a"))
46
+ with self.assertRaises(Conflict):
47
+ self.store.ack(message["id"], "a")
48
+ self.assertEqual(1, len(self.store.inbox("b")))
49
+ acknowledged = self.store.ack(message["id"], "b")
50
+ self.assertEqual(acknowledged, self.store.ack(message["id"], "b"))
51
+ self.store.close()
52
+ self.store = Store(self.db, clock=lambda: self.time)
53
+ self.assertEqual([], self.store.inbox("b"))
54
+ self.assertEqual(acknowledged,
55
+ self.store.send("a", "b", "hello", "one"))
56
+
57
+ def test_expiry_stale_tokens_and_exact_resource(self):
58
+ old = self.store.claim("src/a.py", "a", 10)
59
+ with self.assertRaises(Conflict):
60
+ self.store.claim("src/a.py", "a", 10)
61
+ self.store.claim("src/A.py", "b", 10)
62
+ self.store.claim("src", "c", 10)
63
+ self.assertEqual(3, len(self.store.leases()))
64
+ self.time = 110
65
+ current = self.store.claim("src/a.py", "a", 20)
66
+ self.assertNotEqual(old["generation"], current["generation"])
67
+ with self.assertRaises(Conflict):
68
+ self.store.release("src/a.py", "a", old["generation"])
69
+ with self.assertRaises(Conflict):
70
+ self.store.renew("src/a.py", "a", old["generation"], 20)
71
+ with self.assertRaises(Conflict):
72
+ self.store.release("src/a.py", "b", current["generation"])
73
+ renewed = self.store.renew("src/a.py", "a", current["generation"], 30)
74
+ self.assertEqual(140, renewed["expires_at"])
75
+ self.store.release("src/a.py", "a", current["generation"])
76
+ self.assertEqual([], self.store.leases())
77
+
78
+ def test_expired_lease_cannot_renew_or_release(self):
79
+ old = self.store.claim("r", "a", 1)
80
+ self.time += 1
81
+ for operation in (self.store.renew, self.store.release):
82
+ args = ["r", "a", old["generation"]]
83
+ if operation == self.store.renew:
84
+ args.append(10)
85
+ with self.assertRaises(Conflict):
86
+ operation(*args)
87
+
88
+ def test_invalid_inputs_and_ttl(self):
89
+ for ttl in (float("nan"), float("inf"), -1, 0, 86401, True, "3"):
90
+ with self.assertRaises(ValueError):
91
+ self.store.claim("r", "a", ttl)
92
+ for invalid in ("", " ", "bad\x00value", None):
93
+ with self.assertRaises(ValueError):
94
+ self.store.claim(invalid, "a", 1)
95
+ with self.assertRaises(ValueError):
96
+ self.store.send("a", "b", "body", invalid)
97
+ with self.assertRaises(ValueError):
98
+ self.store.inbox(invalid)
99
+ self.assertEqual([], self.store.leases())
100
+
101
+ def test_two_process_race(self):
102
+ ctx = multiprocessing.get_context("spawn")
103
+ ready, result, start = ctx.Queue(), ctx.Queue(), ctx.Event()
104
+ processes = [ctx.Process(target=race_claim,
105
+ args=(self.db, ready, start, result))
106
+ for _ in range(2)]
107
+ try:
108
+ for process in processes:
109
+ process.start()
110
+ for _ in processes:
111
+ self.assertTrue(ready.get(timeout=15))
112
+ start.set()
113
+ outcomes = [result.get(timeout=15) for _ in processes]
114
+ self.assertEqual(["claimed", "conflict"], sorted(outcomes))
115
+ for process in processes:
116
+ process.join(15)
117
+ self.assertEqual(0, process.exitcode)
118
+ finally:
119
+ for process in processes:
120
+ if process.is_alive():
121
+ process.terminate()
122
+ process.join(5)
123
+ ready.close()
124
+ result.close()
125
+
126
+
127
+ if __name__ == "__main__":
128
+ unittest.main()