The problem: who watches the agents?
You're running 4 agents in parallel. One of them — the refactoring agent — decides to drop 3 legacy database tables. It found them through static analysis: no foreign key references, no migrations that touch them, no recent queries. Looks safe.
It's not safe. One of those tables is used by a reporting service that nobody documented, and the schema doesn't show it. The table gets dropped. The BI dashboard goes dark. You spend three hours tracing back what happened.
This is the kind of failure that happens when agents act autonomously on incomplete information. The refactoring agent wasn't wrong — it was just missing context that a human had. There was no mechanism to ask.
The solution: an append-only coordination log
KAOS v0.6.0 adds a SharedLog — an append-only, position-ordered SQLite table implementing the LogAct protocol (Balakrishnan et al., Meta 2026). Every agent can read it. Every agent can write to it. Nothing executes without consensus.
The 4-stage loop:
Any agent can propose an action (intent). Any agent can vote on it (vote). Any agent — including a human supervisor — can reject. The decision is computed from the tally and recorded. The action only executes if it passes.
The table-drop scenario, replayed with a shared log
from kaos.shared_log import SharedLog
from kaos import Kaos
db = Kaos("project.db")
log = SharedLog(db.conn)
# Human supervisor sets a standing policy — recorded at position 0
log.policy(
"human-supervisor",
"Table drops require explicit human approval before execution.",
metadata={"severity": "critical"}
)
# Refactoring agent proposes the drop
intent_id = log.intent(
"refactor-agent",
"DROP TABLE legacy_sessions, temp_exports, old_audit",
metadata={"reason": "No FK refs found via static analysis"}
)
# Safety monitor auto-approves (it also checked FK refs — none found)
log.vote("safety-monitor", intent_id, approve=True,
reason="No FK dependencies found in schema.")
# Human supervisor REJECTS
log.vote("human-supervisor", intent_id, approve=False,
reason="legacy_sessions is used by the BI reporting service. "
"Not documented in schema. DO NOT DROP.")
# Tally: 1 approve, 1 reject — fails the 2-approval policy
summary = log.tally(intent_id)
# summary.approve == 1, summary.reject == 1, summary.passed == False
# Decision recorded — refactor-agent reads it and stops
decision = log.decide(intent_id, "refactor-agent")
# decision.payload == {"passed": False, "approve": 1, "reject": 1}
The action is blocked. The full trail — policy, intent, both votes, decision — is in the database. It's append-only: nothing can be modified retroactively.
Human supervisor rejects the table drop. Refactor agent reads the reason, saves it to memory, and proposes a safe rename instead.
What the audit trail looks like
After the above scenario, the log has 4 entries:
Every entry has a monotonically increasing position, a type, an agent_id, a ref_id (linking votes and decisions back to their intent), a payload (JSON), and a created_at timestamp. Query it all with SQL:
-- Who voted on intent #1 and what did they say?
SELECT type, agent_id, json_extract(payload, '$.approve') as approved,
json_extract(payload, '$.reason') as reason
FROM shared_log
WHERE ref_id = 1 AND type = 'vote'
ORDER BY position;
-- All failed decisions in the last hour
SELECT sl.position, sl.agent_id,
json_extract(sl.payload, '$.action') as action
FROM shared_log sl
JOIN shared_log intent ON intent.log_id = sl.ref_id
WHERE sl.type = 'decision'
AND json_extract(sl.payload, '$.passed') = 0
AND sl.created_at > datetime('now', '-1 hour');
The agent learns from rejection
When the refactor agent reads the decision, it sees the reason: "BI reporting service uses this table." It saves that to memory — so no future agent wastes a vote on the same proposal.
from kaos.memory import MemoryStore
mem = MemoryStore(db.conn)
# Save the lesson — any future agent searching for "legacy_sessions" will find this
mem.write(
"refactor-agent",
"legacy_sessions table has undocumented dependency in BI reporting service. "
"Never drop without explicit verification of BI service.",
type="error",
key="legacy-sessions-dep"
)
# Propose a safe alternative instead
intent_id_v2 = log.intent(
"refactor-agent",
"RENAME TABLE legacy_sessions -> _deprecated_legacy_sessions (data preserved)",
metadata={"reason": "Safe rename instead of drop — BI service unaffected"}
)
# Both agents approve the rename
log.vote("safety-monitor", intent_id_v2, approve=True)
log.vote("human-supervisor", intent_id_v2, approve=True,
reason="Rename preserves data. Safe.")
decision_v2 = log.decide(intent_id_v2, "refactor-agent")
# decision_v2.passed == True
# Execute and record
log.commit("refactor-agent", intent_id_v2,
summary="3 tables renamed, 0 rows deleted. BI service unaffected.")
The log now has 9 entries. The full story — rejection, learning, revised proposal, approval, execution — is in one append-only table, queryable forever.
Entry types
| Type | Who writes it | What it means |
|---|---|---|
policy | Supervisor | Standing rule that applies to all future intents |
intent | Any agent | Proposed action that requires votes before executing |
vote | Any agent | Approve or reject an intent, with optional reason |
decision | Intent proposer | Computed tally — passed or failed |
commit | Intent proposer | Action executed successfully (after passed decision) |
result | Any agent | Output or finding from an action (no prior intent required) |
abort | Intent proposer | Action cancelled after a failed decision |
mail | Any agent | Agent-to-agent typed message (to_agent in payload) |
Building a safety gate
The simplest pattern is a safety gate — a wrapper that refuses to execute dangerous operations without consensus:
from dataclasses import dataclass
from kaos.shared_log import SharedLog
@dataclass
class SafetyGate:
log: SharedLog
required_approvals: int = 2
def propose(self, agent_id: str, action: str, **metadata) -> int:
"""Return intent_id. Caller must collect votes before calling .execute()."""
return self.log.intent(agent_id, action, metadata=metadata)
def can_execute(self, intent_id: int) -> bool:
summary = self.log.tally(intent_id)
if summary.approve >= self.required_approvals:
return True
return False
def execute(self, agent_id: str, intent_id: int, fn, *args):
"""Run fn(*args) only if the intent has enough approvals."""
decision = self.log.decide(intent_id, agent_id)
if not decision.passed:
reason = self.log.thread(intent_id) # full vote context
raise PermissionError(f"Action blocked: {reason}")
result = fn(*args)
self.log.commit(agent_id, intent_id,
summary=f"Executed: {result}")
return result
The CLI
# Agent declares intent
kaos log intent refactor-agent "DROP TABLE legacy_sessions"
# Peers vote
kaos log vote safety-monitor --intent 2 --approve true
kaos log vote human-supervisor --intent 2 --approve false \
--reason "BI service dependency"
# Compute decision
kaos log decide refactor-agent --intent 2
# => passed=False (1 approve, 1 reject)
# View the full log
kaos log tail --n 10
kaos log ls # summary stats by type
# Raw SQL
kaos query "SELECT type, agent_id, payload FROM shared_log ORDER BY position"
MCP tools
All coordination operations are available as MCP tools:
shared_log_intent— declare an intent before a risky actionshared_log_vote— approve or reject an intent with a reasonshared_log_decide— compute the tally and record the decisionshared_log_read— read the log, filter by position, type, or agent
Ask your AI assistant: "with kaos, propose dropping these tables and wait for human approval" and it will call shared_log_intent, then pause.
The schema
CREATE TABLE shared_log (
log_id INTEGER PRIMARY KEY AUTOINCREMENT,
position INTEGER UNIQUE NOT NULL, -- monotonic, append-only
type TEXT NOT NULL CHECK (type IN (
'intent','vote','decision','commit',
'result','abort','policy','mail')),
agent_id TEXT NOT NULL,
ref_id INTEGER REFERENCES shared_log(log_id), -- vote/decision -> intent
payload TEXT NOT NULL DEFAULT '{}', -- JSON
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
);
The UNIQUE NOT NULL constraint on position enforces monotonic ordering at the database level — no two entries can share a position, and no concurrent transaction can reorder entries. Combined with SQLite WAL mode, the log is safe for concurrent multi-agent writes.
Where to go next
- Full shared log documentation — API reference, CLI, MCP, schema
- shared_log_coordination.py — full 4-stage walkthrough in Python
- safety_voting.py — SafetyGate wrapper, 3 scenarios
- Previous: Cross-Agent Memory ←
policy and mail entry types, and integrates agent_id as a first-class citizen.