← All posts

How AI Agents Learn From Corrections in Vocion

When a human edits, approves, or rejects a draft, that decision can become a rule the agent reads next time — but only after a person approves it.

Vocion Teamvocion-v2.47.4

An AI agent that "learns from feedback" is not fine-tuning a model on your corrections — it is accumulating rules a human has approved. In Vocion, a learning step is a named bucket of rules (learnings/<name>.yaml) mounted into an agent's context on every turn. Two paths add rules to a bucket: a human directly edits the bucket in the dashboard, or a learning candidate is proposed — either automatically, when the review pipeline scores a CRM-update decision, or from classified feedback (a Slack reaction, a Drive comment, manual UI feedback) — and a person approves it before it lands. The system does not auto-commit a rule from a raw correction; every rule an agent reads has a human's decision behind it.

If you have heard "our agent learns from feedback" as a vendor line and want the actual mechanism, this is it: which event fires, who approves it, and where the resulting rule lives. That distinction matters in production because a correction loop that writes rules on its own is a drift risk — an agent that silently reinterprets one edited draft as a new standing rule can wander from what a person actually meant. Gating every rule behind an explicit approve or reject call means the worst case of a bad correction is one bad rule sitting in a queue, not one bad rule already changing behavior.

What is a learning step, concretely?

A learning step is authored at learnings/<name>.yaml in the workspace. The schema (LearningStepManifestSchema, packages/core/src/libs/workspace/schemas.ts) is five fields:

name: meeting_triage
title: Meeting Triage
description: >-
  Rules for deciding whether a calendar event is a real sales conversation
  worth a debrief.
preamble: |
  These rules came from misfires — internal syncs treated as discovery calls,
  and recurring 1:1s summarized as prospect meetings.
agents:
  - meeting-prep
  - followup-coordinator

At runtime the step is rendered to /learnings/<name>.md in the agent's virtual filesystem, and the individual rule rows come from the learning_step and learning tables, not the YAML file itself — the manifest just declares which buckets exist and which agents read them (docs/entities/learning-step.md).

Here is what a bucket looks like once reviewers have actually corrected it. The support-reply demo ships context/support-demo/learnings/draft_reply.yaml with this description:

Skill-specific rules learned over time from human reviewers approving / rejecting / editing past drafts. Mounted at /learnings/draft_reply.md during runs of the draft_reply skill.

and rules like mirror-customer-tone, never-restate-the-problem, and end-with-a-concrete-next-step — plain-language directives, not weights, not embeddings.

Path one: a reviewer's decision on a CRM-update proposal becomes a rule automatically

There is exactly one place in vocion-core where a review decision writes a learning rule without a separate approval step, and it is scoped narrowly. ReviewService.decide() handles the action kind — a proposed action sitting in the review queue — and after it approves or rejects the underlying action, it calls a private function:

// The decision is training signal: record what a good/bad proposal
// looks like in the `crm-updates` learning step so agents check their
// next proposals against real operator judgment. Never blocks the
// decision itself.
await recordActionDecisionLearning(item.id, orgId, action, opts?.reason ?? opts?.note).catch(() => {});

recordActionDecisionLearning reads the action run, refuses to act unless the proposal came from an agent (if (!run || !run.invokedBy?.startsWith('agent:')) return; — only agent proposals train agents, not human-initiated actions), builds a plain-English rule text describing the fields touched and the operator's stated reason, and hands it to addLearning with a fixed target:

await addLearning({
  orgId,
  stepName: 'crm-updates',
  ruleText,
  source: `action_run:${runId}`,
  createdBy: 'review-decision',
});

That stepName: 'crm-updates' is not a placeholder — it is the only step this automatic write ever targets. An approved proposal produces a rule like "this class of update matched operator judgment; similar evidence justifies similar proposals"; a rejected one produces "do not propose this class again without stronger evidence," including the reviewer's reason, truncated to 120 characters. This is CRM-update-specific. A workflow approval gate, a mission pause, or an approval on a non-CRM action does not go through this code path at all.

Path two: classified feedback becomes a learning candidate a human approves

Everything else — a Slack reaction, a Drive comment, feedback submitted through the dashboard — goes through FeedbackWorkerService, a poll loop that drains feedback_job rows and classifies each one with a Haiku-class classifier. Its file header states the boundary directly:

The worker DOES NOT auto-commit learnings. When a classification proposes rule text, it records a learning candidate — a suggestion sitting in a queue — and stops there. A person adopts it (in the dashboard, or through /api/v1/learning-candidates) or rejects it with a reason. The worker's role is to triage and queue, never to change how an agent behaves.

The row it writes is a candidate, managed by LearningCandidateServicecreateCandidate inserts with status: 'pending', and only decideCandidate can move it to approved or rejected. This worker is a separate process (npm run worker:serve, opt-in via ENABLE_FEEDBACK_WORKER=1), not part of the Next.js request path.

How does a human approve or reject a candidate?

Three real routes back the candidate lifecycle:

On the dashboard side, /dashboard/learnings/<step> shows each rule's source — manual, feedback:<id>, or self-improver:<run_id> — so you can trace where a rule came from. The product's own docs page states the guarantee plainly: "Humans approve before the rule lands in the bucket. This is the explicit 'agent learns from feedback but doesn't drift on its own' guarantee" (docs/features/learnings.md). That same page describes a self-improver subagent that watches runs for a moment worth promoting into a rule and proposes it through the same comment-feedback path described above — it is another producer of candidates, not a shortcut around the approval step.

What stops the rule bucket from filling with near-duplicate rules?

LearningsService runs a trigram-Jaccard similarity check against existing rules in the step before a new one is written:

const DEDUP_THRESHOLD = 0.72;

Above that threshold, the write fails with a 409 that carries the existing rule's id and the similarity score, rather than silently doubling up a paraphrase. The file's own comment notes this mirrors an internal pattern that used Python's difflib.SequenceMatcher at the same 0.72 ratio — the TS implementation is a different algorithm at the same cutoff. This applies uniformly, whether the rule is typed by hand or approved from a candidate.

What does an edited draft look like as a correction?

ReviewService.decide() treats an edit as evidence distinct from a clean approval. If the reviewer edited the proposed input before approving (opts.editedInput), the edited payload is persisted first so execution sends what the reviewer actually saw, and the recorded signal is different from a plain approve:

signal: action === 'approve' ? (opts?.editedInput ? 'edit' : 'approve') : 'reject',

An edit is weaker tone-match evidence than a clean approval — the operator changed something, which is itself information. For the support-reply demo specifically, a reviewer editing a draft_reply output at the approval gate produces this edit signal, but the automatic write to crm-updates covers CRM-update actions only, so a correction on a draft_reply output would need to travel the general feedback-candidate path — FeedbackWorkerService plus /api/v1/learning-candidates — to become a rule in draft_reply.yaml. The demo's YAML file shows the shape a rule bucket reaches after that kind of correction; it is not evidence of an automatic pipeline wired end-to-end for that skill.

Read the code, not the pitch: ReviewService.decide(), recordActionDecisionLearning, and FeedbackWorkerService are the whole mechanism.