Approval memory¶
Reviewers decide the same kinds of calls again and again. Approval memory shows them what was decided before on similar requests, and can propose a confirm rule from that history. It is advice: nothing is ever approved from memory. A request still waits for a person, and a proposed rule changes nothing until a person accepts it and commits it.
flowchart LR
S[("approval store<br/>decided records")] --> C["approval_context(request, records)"]
C --> K["ApprovalContext<br/>counts · who · outcomes"]
K --> V["chat approval cards · Runs desk ·<br/>GET /approvals/{id} (approvers)"]
S --> P["propose_rules(records)"]
P --> R["ProposedRule (data)"]
R -->|a person calls accept| A["AcceptedRule<br/>code · config · confirm="]
A --> T["ToolSpec.confirm"]
| File | Does |
|---|---|
approval/memory.py |
approval_context, argument_similarity, ApprovalContext, SimilarDecision |
approval/rules.py |
propose_rules, ProposedRule, accept, AcceptedRule, confirm_from_config |
viewer/chat/approval_history.py |
chat decisions (stored turns) as records, for the cards |
Similar decisions¶
waiting = await ask(executor, 30) # still asks: memory never approves
record = await store.get(waiting)
context = approval_context(record.request, await store.list())
print(record.status, "-", context.summary()) # pending - 5 similar: 5 approved, ...
A past record counts when a person decided it (approved or denied), it is the same tool
and kind, and its arguments are similar enough (min_similarity=0.5):
- Arguments are compared field by field, nested up to three levels. A nested key can be
data (an address), so a match there is named only as
field.*. - An equal value scores 1, the same field with another value 0.5, a field only one call has 0. The score is the mean over all fields.
- Secret-named fields (
api_key,password,token, ...) are never compared and never named. The context holds no argument values at all: only counts, deciders, times, scores and the names of equal fields. - Timed-out and stopped chat approvals are not decisions and do not count.
ApprovalContext.summary() gives the one line the cards show, such as
"3 similar: 2 approved, 1 denied (by ann)".
Where it shows¶
- Chat: each approval card shows "Seen before: ..." under the arguments. History comes from this user's stored chats and the decisions of the running server.
- Runs view: each pending record of
--approvals PATH.dbshows its context. - HTTP server:
GET /approvals/{id}adds acontextobject, for holders of the approver scope only. Invokers see the record as before.
Proposed rules¶
(rule,) = propose_rules(await store.list(), min_count=5, min_deciders=2)
print("proposed:", rule.describe()) # data only: nothing changed yet
accepted = accept(rule, accepted_by="ann") # a person's explicit choice
print(accepted.code) # commit this; it builds the same confirm= rule
propose_rules(records, min_count=5, min_deciders=2, max_denied=0, review_days=90)
proposes one rule per tool and argument shape. A rule is a policy over all arguments,
bounded to what reviewers actually approved:
- Every non-secret top-level argument gets a condition:
==for a value that never changed,infor up to 10 seen values,lo <= field <= hifor numbers (both bounds are the observed minimum and maximum, so negative amounts are not covered). - A shape with a nested value, or a text field with more than 10 values, gets no rule.
- It needs
min_countapprovals decided as-is, by at leastmin_deciderspeople. - Denials and edited approvals are counter-evidence: if more than
max_deniedof them fall inside the bounds, there is no rule.evidence.denied_outsidecounts the rest.
A ProposedRule is data. accept(rule, accepted_by="ann") is the explicit step: it
returns the config and the code to commit, plus the confirm= callable they build.
reviewed = Registry()
reviewed.tool(confirm=accepted.confirm)(refund) # the rule a person accepted
checked = Executor(reviewed, approval=DeferredApproval(store))
print(await checked.run("refund", {"amount": 15, "reason": "damaged"})) # inside the rule
for amount, reason in ((90, "damaged"), (-5, "damaged"), (15, "changed my mind")):
print("asks again:", bool(await ask(checked, amount, reason))) # outside: a person
The callable skips asking only when the call is that tool, has exactly the covered
non-secret fields, each inside its bounds with the same type, and the review date
(accepted_at + review_days) has not passed. Anything else asks: a new field, a missing
one, a value never seen, a bool for a number, another tool, or any call after review.
confirm_from_config refuses unknown keys, secret-named fields and bad bounds.
Security notes¶
- Memory never decides and never removes a request from the queue; tests check that a long, unanimous history still asks.
- An accepted rule skips asking for calls inside it. Read every condition before you accept: it is code, and a wide range you accept is a range you will not see again. Accept again (with fresh evidence) after the review date.
- Run
python examples/approval_memory.pyto see the whole loop offline.
Related: Approval, Approval stores.