Warning, /swf-monitor/docs/AI_PROPOSALS.md is written in an unsupported language. File is not indexed.
0001 # epicprod AI Proposals
0002
0003 AI proposals are how the LLM acts as a change agent in epicprod with a human
0004 in the loop: a proposer emits concrete actions, a human reviews and decides,
0005 and approval executes the change. Proposals make full use of the LLM for
0006 automation — not only assessment — while keeping every mutation under
0007 deterministic human control.
0008
0009 ## The invariant: deterministic after authoring
0010
0011 A proposal is an **executable payload**, not prose: the action identifier and
0012 the exact validated arguments of the service call it wants (for the pilot,
0013 `dataset_propagation_set` — state, `replaced_by`, comment). The LLM is
0014 consulted exactly once, at authoring time. From the moment the proposal
0015 exists it is frozen data plus existing code, passing three deterministic
0016 checkpoints:
0017
0018 1. **Propose** — the payload is validated by the same validators the service
0019 uses. An unexecutable proposal is refused at birth; nothing reaches review
0020 that cannot run.
0021 2. **Decide** — preconditions are re-checked against current state
0022 (revalidation on touch, the `precondition` anchor). A record that changed
0023 since the proposal saw it is marked stale and withdrawn, never silently
0024 re-interpreted.
0025 3. **Execute** — the identical service call an operator makes by hand, with
0026 the approving human as `changed_by`. No LLM variance anywhere past
0027 checkpoint 1.
0028
0029 AI surfaces get only the *propose* verb; the decide path demands an
0030 authenticated human. This is the credential-boundary pattern applied to
0031 authority: the AI surface holds no mutation right, structurally.
0032
0033 ## The AI proposal list: the Proposal table is canonical
0034
0035 The AI subsystem is its own Django app, `src/ai/` — a sibling of `pcs`,
0036 owning the proposal model, services, pages, and API, plus the AI
0037 assessment helpers and the corun-ai client (`ai/assessments.py`,
0038 `ai/corun_client.py`; the legacy `AIContent` model remains in
0039 `monitor_app` until its planned post-backfill retirement). Executors stay
0040 in their domain apps; `ai` dispatches to them.
0041
0042 Every proposal is a `Proposal` row (`ai_proposal`): action, subject
0043 (type + key, `counterpart_key` for relation subjects such as a
0044 questionnaire–task match), frozen `payload`, required `comment`,
0045 `confidence`, proposer identity + scan version, `batch_id`, **executor**
0046 (`service` now; `agent_message` reserved for prod-ops-agent actions),
0047 `precondition` (the deterministic decide-time guard, capturing every prior
0048 value an undo must restore), input hash, status,
0049 and the decision record (`decided_by`, `decided_at`, optional `quality`,
0050 executed action-stream log id).
0051
0052 Status vocabulary: `proposed → executed | denied | withdrawn | stale`, with
0053 `approved_pending_execution` reserved for asynchronous executors. Undoing
0054 an executed proposal returns it to `proposed` — pending again, decision
0055 fields cleared, render projection restored — while the origin-stamped
0056 execution and undo events carry the record. **Terminal rows are retained** — the AI proposal list is where AI-proposed activity
0057 and its decisions stay visible: the queue (pending), the decision history,
0058 per-proposer track records, and the standing metric of what fraction of
0059 production mutations originate with AI. Operators can delete list rows
0060 (test or noise entries), logged with counts — a cleanup verb, never a
0061 decision verb.
0062
0063 Every proposal has a **ref** — its category prefix plus row id (`cp-123`
0064 for campaign propagation) — the one identifier humans use across surfaces:
0065 the web list's Ref column, the bot listing, and decide instructions. The
0066 prefix doubles as corroboration: a decide call whose prefix mismatches the
0067 row's category is refused, never reinterpreted, so a garbled or mis-relayed
0068 reference cannot decide the wrong proposal. Prefixes are declared per
0069 category in `ACTION_REF_PREFIXES` (`ai/models.py`).
0070
0071 Records a proposal targets carry a **render projection** in their metadata
0072 (`Dataset.metadata['proposal']`), written and cleared in the same
0073 transactions by the same services — so the catalog badges, filters, and
0074 compose detail render without joins while the table holds truth. Denial
0075 memory is a proposal-list query (denied row with the same subject and input
0076 hash), not record state.
0077
0078 ## Adding a proposal category
0079
0080 Every proposal category completes the same checklist, in order; the
0081 propagation pilot is the worked example of each item.
0082
0083 1. **Executor first.** A validated service in the domain app: one call is
0084 one operator action, it accepts `origin`, logs one origin-stamped
0085 action-stream event (action id registered with sublevel and live
0086 defaults), and returns the event's log id. Humans and approvals make
0087 the identical call — without the executor there is nothing to propose.
0088 2. **Precondition.** The deterministic decide-time anchor: the fields
0089 that prove the record has not moved, including every prior value an
0090 undo must restore (for propagation: `prev_state`, `prev_replaced_by`).
0091 3. **Propose service.** Validates the payload with the executor's own
0092 validators (an unexecutable proposal is refused at birth), captures
0093 the precondition, computes the input hash, honors denial memory, and
0094 writes the Proposal rows and the render projection.
0095 4. **Decide dispatch.** The action's revalidation rule and executor call
0096 registered in the decide path, and the category's ref prefix declared
0097 in `ACTION_REF_PREFIXES`. (The pilot hard-wires `proposal_decide` to
0098 the propagation action; the second category generalizes this into a
0099 per-action registry.)
0100 5. **Comment template.** Defined per action: code fills the facts, the
0101 model fills only the judgment fragment — or nothing.
0102 6. **Review surfaces.** The render projection badge and an AI-proposed
0103 filter on the domain list page in the `.ai-attr` treatment, with
0104 approve/deny in its bulk bar. The proposals page picks the rows up
0105 without further work.
0106 7. **Reversibility class.** Declared undoable, mitigable, or
0107 irreversible-by-design; review ceremony follows the class, and the
0108 approve control states its undo story.
0109 8. **Scan lifecycle.** Proposer identity, scan version, batch id;
0110 heartbeat refresh semantics if the proposer recurs; the per-action
0111 SysConfig off-switch, present in the SysConfig document at its
0112 default.
0113 9. **Weakest consumer.** If bots or small models read or relay the
0114 category, the retrieval surface is sized for the smallest model.
0115 10. **Documentation.** The category added to this document: its
0116 precondition, comment template, and reversibility class.
0117
0118 ## Review surfaces
0119
0120 Proposals are **visible but inert** on the open face: anyone can see a
0121 recommendation; only an authenticated approver can act. Everything
0122 AI-attributable renders in the universal AI presence scheme — purple ink
0123 on lavender ground, defined once (`--ai-fg`, `--ai-bg`, `--ai-border`,
0124 `.ai-attr` / `.ai-attr-text` / `.ai-fill`) in the base template. Used with
0125 restraint: it marks AI material embedded in non-AI surfaces and the titles
0126 of AI pages, so the color registers as "this is AI" without saturating.
0127 The marker follows artifact kind, not authorship: proposals and
0128 assessments are inherently AI products and are marked; artifacts of a
0129 kind humans equally produce — narratives, documents, code — dress plain,
0130 whoever drafted them.
0131
0132 - **The catalog list** — the origin surface, a first-class decision site:
0133 proposals sit purple-marked in the working material. AI filters isolate
0134 proposed and executed rows; pending rows badge the proposed transition
0135 and executed rows carry an executed mark; Approve AI / Deny AI / Undo AI
0136 in the bulk bar act on the selection, with the quality selector recorded
0137 on the decision.
0138 - **The compose detail**: the same proposal block with the same controls —
0139 the record is server-side, so list and detail cannot fork.
0140 - **The proposals page** (`/ai/proposals/`, on the production home under
0141 AI assessments): the pending queue and the decision history, filterable by
0142 status, action, proposer, and batch, with bulk decide and per-proposer
0143 track records.
0144 - **The bot surface** (MCP: `ai_list_proposals`, `ai_decide_proposal`): one
0145 flat list over every category — refs plus server-rendered description
0146 lines the bot shows verbatim — and a decide relay carrying the human's
0147 ref, verdict, and identity. The relaying LLM contributes no
0148 interpretation; deciders must be on the SysConfig approver list
0149 (`ai_proposal_mcp_approvers`, default empty), and the executed event's
0150 filter field records the relaying surface (`mcp:<ref>`).
0151
0152 A decision optionally carries a **quality tag** from the shared review
0153 vocabulary (`wrong | poor | ok | good` — the same scale as AI assessments):
0154 set the tag in the quality selector, then approve or deny, and it is
0155 recorded with the decision. A deny tagged `wrong` records a miscalibrated
0156 proposal and weighs against the proposer's track record; a plain deny
0157 records a sound proposal decided against.
0158
0159 Execution events and history entries are **origin-stamped**
0160 (`origin: ai_proposal`, proposer, scan version, batch, proposed-at)
0161 alongside the approving `username`, so every mutation is classifiable as
0162 human-originated or AI-proposed/human-approved.
0163
0164 ## Lifecycle: the scan heartbeat
0165
0166 Recurring proposers refresh their pending proposals: each scan withdraws
0167 its unacted proposals and re-derives from current inputs — what still holds
0168 returns freshly validated, what no longer holds does not return, new
0169 findings appear. Pending staleness is bounded by the scan cadence, an
0170 unreviewed proposal returns at each scan until decided, and denial is the
0171 only stop (a denied input hash is never re-proposed until the proposer's
0172 inputs change).
0173 Withdrawals are logged and counted, never silent. One-shot proposal sets
0174 persist until decided; revalidation on touch covers their staleness.
0175 Per-action-type proposing is switchable off in SysConfig.
0176
0177 ## Proposers
0178
0179 The subsystem is **proposer-agnostic**: rule-based detectors, LLMs, and
0180 hybrids are all legitimate proposers — a deterministic rule ("this family's
0181 EVGEN inputs vanished from Rucio → propose hold") emits through the same
0182 surface as an LLM scan.
0183
0184 **Comments are templated, not freeform.** Each proposer's harness defines a
0185 per-action comment template: code fills the facts (what changes, counts,
0186 replacement target, source and date), the model fills only the judgment
0187 fragment — some proposers fill nothing. Facts in comments therefore cannot
0188 be hallucinated, and the human reviews a uniform comment shape per action
0189 type. The questionnaire automatch is the worked hybrid example: structured
0190 frame plus the model's one-line reason.
0191
0192 **Weakest-consumer design.** The propose surface is one flat call with
0193 deterministic validation, so a small-model proposer's worst case is a
0194 denied proposal. The proposal list's
0195 per-proposer approval and wrong-rates make proposal rights earnable and
0196 revocable by data: a proposer whose wrong-rate climbs gets its proposing
0197 switched off. LLM *readers* of proposal state (bots, assessors) get a
0198 retrieval tool sized for the smallest model: summarize-first, prescriptive
0199 docstring.
0200
0201 ## Ceremony proportional to stakes
0202
0203 Review friction matches irreversibility, in both directions:
0204
0205 - **Undoable** actions (catalog state: propagation, tags, matches) approve
0206 in one tap, and the system offers **Undo** — a computed compensating
0207 action with full provenance (`origin: undo` carrying the proposal id),
0208 never erasure; history stays append-only. The undo offer expires when
0209 the record moves on, by the same precondition machinery. Propagation is
0210 the worked example: Undo on an executed proposal restores the
0211 precondition state — including the prior `replaced_by` — through the
0212 same executor, with a templated comment naming the proposal, and
0213 returns the proposal to the pending queue.
0214 - **Mitigable** actions (external effects: a submitted PanDA task can be
0215 killed or superseded by `.tryN`, not unsubmitted) offer the named
0216 mitigation, labeled honestly: it does not restore the prior world.
0217 - **Irreversible-by-design** actions (locking a tag, publishing a
0218 narrative revision) are labeled as such — their immutability is a
0219 guarantee other records depend on — and get ceremony: dry-run preview
0220 before approve.
0221
0222 Every approve control states its undo story. Reversibility class is
0223 declared per action type in the same registry family as sublevel and live
0224 defaults.
0225
0226 **Dry run** is the trust instrument for consequential and composite
0227 actions: execute the frozen payload with writes off and show the concrete
0228 before→after; approve then executes what was just shown. Dry runs write
0229 nothing, so they are open to all readers, not only approvers. Composite
0230 executors (campaign creation) must be
0231 dry-runnable; a composite proposal's review surface is its dry run, diffed
0232 against the preview frozen at propose time — staleness made visible at a
0233 scale a single `prev_state` anchor cannot guard.
0234
0235 ## Growth path
0236
0237 Designed-for extensions, reserved now, built when their consumer arrives:
0238
0239 - **Asynchronous executors** (`executor: agent_message`): approval queues
0240 the payload to the prod-ops agent; the proposal holds
0241 `approved_pending_execution` until the completion event closes it.
0242 - **Recordless subjects**: system-scoped proposals ("re-run catalog_sync")
0243 carry `subject_type: system` and no render projection — the queue page is
0244 their only surface.
0245 - **Creation subjects**: proposals that create records ("compose a draft
0246 task from request #202") anchor on idempotency keys rather than
0247 `prev_state`; the intake surface already provides them.
0248 - **Second consumer, first adaptation**: questionnaire–task matching — the
0249 automatch's `suggested` matches land as proposals with confidence
0250 (relation subjects, `counterpart_key`), high/medium confidence keeps
0251 auto-accepting with origin stamps: the graduation ladder
0252 (`manual → auto+notify`) already in production.
0253 - **Beyond epicprod**: the subsystem is system-wide — testbed and
0254 streaming actions are expected consumers. Proposal events currently log
0255 to the epicprod action stream; where non-epicprod proposals log is
0256 decided when the first such consumer arrives.
0257
0258 ## Action-stream records
0259
0260 | action | when |
0261 |---|---|
0262 | `proposal_created` | one per propose call, with count and batch |
0263 | `proposal_denied` | one per deny decision, with count and quality |
0264 | `proposal_expired` | one per heartbeat withdrawal, with count |
0265 | (execution) | the executed action's own event, origin-stamped |
0266 | (undo) | the compensating action's own event, `origin: undo` with the proposal id |
0267
0268 ## Related
0269
0270 - [PCS.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/PCS.md) — the propagation disposition model the pilot proposes on.
0271 - [ACTION_STREAM.md](ACTION_STREAM.md) — the event record.
0272 - [EPICPROD_NARRATIVES.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/EPICPROD_NARRATIVES.md) — the context proposers
0273 reason against.
0274 - [EPICPROD_TASK_CATALOG.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/EPICPROD_TASK_CATALOG.md) — the review surface.
0275 - [EPICPROD_OPS.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/EPICPROD_OPS.md) — the shared review-quality vocabulary.