Platform Architecture

When the AI rewrites its own instructions

How Acompli's agents modify their own prompts in production — and the guardrails that keep it safe

Acompli's AI agents can generate the prompts that govern their own future behaviour. That sentence should make you nervous. It made us nervous too — and that productive discomfort is what led us to build the governance architecture described in this article.

Most discussions of AI in compliance start and end with "the AI drafts things faster." That framing is too shallow. The real power — and the real risk — comes when the AI system can modify its own operating instructions based on what it encounters in production. An agent that processes a healthcare privacy assessment and then generates a specialised healthcare overlay prompt that improves all future healthcare assessments is doing something fundamentally different from an agent that merely drafts text. It's rewriting the instructions that govern its own behaviour.

We built this capability into Acompli. And then we spent a long time building the guardrails around it.

This article describes the closed-loop self-modification architecture at the core of the Acompli platform: what it does, how the data flows, and what governance properties we enforce. If you work in AI engineering, compliance technology, or enterprise SaaS, the problems here will feel familiar even if the specific domain doesn't. The tension between adaptability and accountability shows up everywhere that AI systems operate in regulated environments.


Why prompt-level modification is different

Not all self-modification is created equal, and the distinctions matter for governance.

Session-level modification is what most agent frameworks do: the agent reflects on its own performance within a single conversation, adjusts its approach, and moves on. When the session ends, the modifications vanish. Nobody else is affected. The blast radius is one interaction, and no governance is required because nothing persists.

Model-level modification is fine-tuning or LoRA adapters: you alter the model's weights, changing behaviour across every prompt and every tenant. The modification is opaque — you can't read weight matrices to understand what changed — and reversible only if you maintain separate model checkpoints. Governance happens at the infrastructure layer: model registries, A/B deployment, canary rollouts.

Prompt-level modification — what we built — sits between these two. The prompt content in the database is altered, changing behaviour for all future requests that resolve to that prompt within the modifying tenant's scope. The modification is inspectable (it's natural language text), reversible (previous versions are retained), and scoped (tenant isolation prevents cross-organisational effects). But because it's persistent and cross-session, it needs governance mechanisms that session-level modification doesn't.

Concretely: every prompt in our system is a tuple — a name, a category, the content itself, an organisation ID (which may be null for system-wide defaults), and a version. When our AI generation pipeline needs a prompt, it resolves by name and category with tenant-scoped fallback: first check for an org-specific fork, then fall back to the system default. A prompt-level modification changes the content such that all subsequent generation requests resolving to that prompt use the new content instead of the original. Every user in that tenant, every session, every future request — all of them now operate under modified instructions.

That's the capability. The rest of this article is about making it safe.


The closed loop

The self-modification loop is worth walking through in detail, because the data flow is where both the power and the governance live.

Skills as executable specifications

Everything runs through the skill execution engine. A skill is a declarative specification — metadata, access control parameters, and an ordered list of typed steps. Each step has a declared type: TOOL (invoke a registered tool), LLM (call the language model), or CONDITIONAL_TOOL (a tool call that signals runtime-dependent execution).

SkillSpec:
  skill_id:        string          # unique identifier
  organization_id: string          # tenant scope
  name:            string          # human-readable
  version:         string          # semver
  status:          enum            # draft | testing | active | archived
  min_role:        string          # minimum RBAC role required
  input_schema:    JSON Schema     # validates input payload
  steps:           list[SkillStep] # ordered execution plan
  required_tools:  list[string]    # tool whitelist

The executor processes steps sequentially. For each step, it evaluates an optional when condition against the current execution state, dispatches based on step type, and stores the result in a state dictionary keyed by step_id. That state dictionary is what makes the closed loop possible — it's the connective tissue between generation and persistence.

A concrete example

Consider a skill that generates a domain-specific enhancement overlay — a prompt supplement providing specialised instructions for a particular assessment domain (healthcare privacy, financial compliance, environmental regulation, etc.).

Skill: generate-domain-overlay

Step 1: fetch_context     [TOOL: get_assessment_context]
  Retrieves assessment metadata and domain classification.

Step 2: check_existing    [TOOL: get_org_prompt]
  Checks whether an overlay already exists for this domain.
  Args: {prompt_name: "{{input.domain_name}} Enhancement
          Domain Overlay",
         category: "answer_enhancement"}

Step 3: generate_overlay  [LLM]
  When: check_existing.found == false
  System prompt: "You are a domain expert in
    {{input.domain_name}}. Generate a prompt overlay that
    provides specialised guidance for enhancing answers in
    this domain. Include: domain-specific terminology,
    regulatory context, quality criteria, and common
    pitfalls to avoid. Preserve all {{template_variables}}
    from the base prompt."

Step 4: persist_overlay   [TOOL: update_prompt]
  When: check_existing.found == false
  Args: {prompt_name: "{{input.domain_name}} Enhancement
          Domain Overlay",
         category: "answer_enhancement",
         new_content: "{{generate_overlay.summary}}",
         change_notes: "Auto-generated overlay for
                        {{input.domain_name}} domain"}

Step 5: debrief           [LLM]
  System prompt: "Summarise what was created or skipped,
    including any structural warnings returned by the
    update operation."

The critical transition is between Steps 3 and 4. Step 3's LLM output gets stored in the state dictionary under the key generate_overlay. Step 4's new_content argument references this output through the template expression {{generate_overlay.summary}}. At runtime, the template resolver extracts the generated text and passes it as the argument to the update_prompt tool, which writes it to the prompt database.

That's the closed loop: LLM generates content → template resolver bridges to the next step → tool writes to the prompt database → all future AI behaviour within the tenant scope is modified.

 ┌──────────────────────────────────────────────────────┐
 │                 SKILL EXECUTION                       │
 │                                                       │
 │  Step 1: TOOL [fetch_context]                         │
 │    └─► state["fetch_context"] = {domain: "healthcare" │
 │                                   ...}                │
 │                                                       │
 │  Step 2: TOOL [get_org_prompt]                        │
 │    └─► state["check_existing"] = {found: false}       │
 │                                                       │
 │  Step 3: LLM [generate_overlay]                       │
 │    ├── system_prompt: "You are a domain expert..."    │
 │    ├── user_message: serialised(state)                │
 │    └─► state["generate_overlay"] =                    │
 │          {summary: "<LLM-generated overlay text>"}    │
 │                         │                             │
 │                    ┌────┴────┐                        │
 │                    │TEMPLATE │                        │
 │                    │RESOLVE  │                        │
 │                    └────┬────┘                        │
 │                         │                             │
 │                         ▼                             │
 │  Step 4: TOOL [update_prompt]                         │
 │    ├── new_content: resolved from                     │
 │    │     {{generate_overlay.summary}}                 │
 │    ├── Structural fingerprint check                   │
 │    ├── Version history snapshot                       │
 │    └─► PROMPT DATABASE WRITE                          │
 │              │                                        │
 └──────────────┼────────────────────────────────────────┘
                │
                ▼
 ┌──────────────────────────┐
 │    PROMPT DATABASE        │
 │                           │
 │  name: "Healthcare        │
 │    Enhancement Domain     │
 │    Overlay"               │
 │  content: <LLM-generated> │
 │  org_id: tenant_xyz       │
 │  version: "1.1"           │
 │  version_history: [...]   │
 │  updated_by: user@org.com │
 └──────────┬───────────────┘
            │
            │  (future requests)
            ▼
 ┌──────────────────────────┐
 │   AI GENERATION PIPELINE  │
 │                           │
 │  Prompt resolution:       │
 │    1. Check org fork      │
 │    2. Fall back to system │
 │                           │
 │  The new overlay is now   │
 │  active for ALL future    │
 │  requests in tenant_xyz   │
 └──────────────────────────┘

The modified prompt becomes active immediately (modulo a TTL cache, default 300 seconds) for all subsequent AI generation requests within that tenant — no code deployment, no service restart.


The template resolver: bridging generation to persistence

The template substitution mechanism is deceptively simple and architecturally critical. It's the data flow layer that connects step outputs to step inputs across the entire skill execution.

Arguments in any skill step can contain template references of the form {{path.to.value}}. The resolver operates in two modes:

Exact match — when the entire argument value is a single template expression, the resolved value retains its original type. A dictionary remains a dictionary. A list remains a list. This matters because it means structured LLM outputs can flow directly into tool arguments without serialisation loss.

Inline match — when the template is embedded within a larger string, the resolved value is coerced to a string representation, with dictionaries and lists serialised as JSON.

function resolve_templates(value, state, item):
    if value is dict:
        return {k: resolve_templates(v, state, item)
                for k, v in value.items()}
    if value is list:
        return [resolve_templates(v, state, item)
                for v in value]
    if value is string:
        if exact_match(value):      # entire string is {{...}}
            return resolve_path(match.group, state, item)
        else:                        # inline: "prefix {{...}} suffix"
            return replace_all(value, lambda m:
                str(resolve_path(m.group, state, item)))
    return value

Path resolution supports several root namespaces: input.* for the skill's input payload, steps.step_id.* for a specific step's output, or directly step_id.* as shorthand. When a step uses foreach_from for iteration, an additional item.* namespace provides access to the current element.

This is what makes the closed loop possible without custom code. The output of an LLM step, stored at state["steps"]["generate_overlay"], can be referenced by a subsequent tool step's argument as {{generate_overlay.summary}}. No glue code, no intermediate processing. The skill specification is the data flow definition.


Tool-mediated writes: the governance gateway

All database mutations in the self-modification loop pass through registered tools, never through direct database access from the skill executor. This concentrates governance enforcement in a single layer.

The prompt management tools in the registry:

ToolOperationGovernance Effect
fork_promptCopy system default into org-scoped forkCreates initial fork with attribution
update_promptModify content of an org forkVersion history, fingerprint check, attribution
get_org_promptRead prompt with org-fork resolutionNo mutation; used for pre-checks
restore_promptDelete org fork, revert to system defaultHard-delete of fork, fallback to default
list_org_promptsList all prompts with fork statusNo mutation; inventory for agents

Each tool enforces three constraints that matter:

Tenant context propagation. The organisation_id is extracted from the AgentContext that the skill executor constructs from the invoking user's identity. Tools never accept organisation_id as a caller-supplied argument. This prevents a skill from operating on a different tenant's prompts — the identity is always derived from the authenticated context, not from the request.

Fork-before-write. The update_prompt tool verifies that the target prompt is an org-scoped fork before permitting modification. No fork? The tool raises an error. This means agents cannot modify system defaults directly. They can only modify tenant-scoped copies. The system-wide base prompts are the known-good baseline, and they're protected.

User resolution. Each tool resolves the human user associated with the current agent context through a governance service query. This resolved identity is propagated to the prompt service for change attribution. If the governance service is unavailable, a fallback identity is constructed from the context's user fields. Attribution is never silently dropped.

Here's what the update_prompt implementation looks like:

function update_prompt(call_args, context):
    org_id = context.organization_id
    prompt_name = call_args["prompt_name"]
    category = call_args["category"]
    new_content = call_args["new_content"]
    change_notes = call_args["change_notes"]
    user = resolve_tool_user(context)

    existing = get_prompt_by_name(prompt_name, category, org_id)
    if not existing or not existing.is_org_fork:
        raise Error("No org fork found. Call fork_prompt first.")

    # Structural fingerprint comparison (advisory)
    old_fp = extract_fingerprint(existing.content)
    new_fp = extract_fingerprint(new_content)
    warnings = compare_fingerprints(old_fp, new_fp)

    # Versioned update with attribution
    updated = prompt_service.update_prompt(
        existing.id, category,
        UpdateRequest(content=new_content,
                      change_notes=change_notes),
        user
    )

    return {success: true, new_version: updated.version,
            structural_warnings: warnings}

Notice the flow: tenant isolation is enforced by context extraction, the fork constraint is checked, the structural fingerprint comparison runs, and only then does the write proceed — with full version history and user attribution attached.


Governance properties

Six properties make prompt-level modification workable in regulated environments.

1. Version control with full content snapshots

Every content modification creates a version entry appended to the prompt's version_history array:

PromptVersion:
  version:      string    # auto-incremented ("1.0" → "1.1")
  content:      string    # full content snapshot
  created_at:   datetime  # UTC timestamp
  created_by:   string    # email of attributable user
  change_notes: string    # human/agent-provided description

We deliberately don't do delta compression. Each version stores the complete prompt content. This trades storage efficiency for auditability — any version can be inspected or restored without requiring reconstruction from a chain of diffs. When a regulator asks "what was the AI's instruction set on 14 February?", you can answer immediately by looking up the version that was active at that timestamp.

2. Change attribution

The skill executor constructs an AgentContext from the human user who initiated the skill run:

context = AgentContext(
    user_id    = actor.id,
    user_email = actor.email,
    user_name  = actor.display_name,
    organization_id = actor.org_id,
    roles      = actor.roles,
    session_id = "skill:<run_id>"
)

This context passes to every tool invocation. The resulting audit trail shows: User X invoked Skill Y, which at Step Z generated content via LLM and wrote it to Prompt W. The SkillRun record links the user, the skill, the step-by-step execution trace, and the resulting prompt modification.

The prompt record itself also stores updated_by, updated_by_user_id, updated_by_email, and updated_by_name — direct attribution on the modified object, independent of the skill run record.

Why does this matter? Because the EU AI Act, ISO 42001, SOC 2, and GDPR Article 22 all require that organisations maintain accountability for AI behaviour. When an AI system's behaviour changes, someone must be able to answer: what changed, who authorised it, when did it happen, and how can it be reversed? The attribution chain answers the "who" question, even when the content itself was generated by an LLM. The human who invoked the skill is the accountable actor.

3. Structural integrity verification

This is one half of a two-layer integrity checking system, and it's worth being precise about what each layer does and doesn't catch.

The structural fingerprint guard extracts structural elements from prompt text and compares before-and-after states. The fingerprint captures five element categories:

function extract_fingerprint(content):
    return {
        numbered_rules:
            # "1. VOICE & PERSONA:", etc.
            match(r"^\s*\d+\.\s+(.+)", lines),

        template_vars:
            # {{variable_name}}
            match(r"\{\{(\w+)\}\}", content),

        imperative_constraints:
            # lines containing must/always/never/required
            match(r"\b(must|always|never|required|
                      do not|cannot)\b", lines),

        section_headers:
            # ## Markdown or ALL-CAPS:
            match(r"^(#{1,3}\s+.+|
                     [A-Z][A-Z &/()-]{3,}:)\s*$", lines),

        output_format_markers:
            # "Return ONLY", "JSON", etc.
            match(r"\b(Return ONLY|JSON|markdown|
                      bullet|...)\b", lines)
    }

The comparison identifies elements present in the original but absent in the modification:

function compare_fingerprints(before, after):
    warnings = []
    for rule in (before.numbered_rules - after.numbered_rules):
        warnings.append("Removed numbered rule: '{rule}'")
    for var in (before.template_vars - after.template_vars):
        warnings.append("Dropped template variable: {{var}}")
    for constraint in (before.imperative_constraints
                       - after.imperative_constraints):
        keyword = detect_imperative_keyword(constraint)
        warnings.append(
            "Removed imperative constraint containing
             '{keyword}': '{constraint[:80]}...'")
    # ... section headers, output format markers ...
    return warnings

These warnings are returned to the agent and recorded in the skill run, but they do not block the modification. This is a deliberate design choice: blocking would create a denial-of-service vector where malformed fingerprint extraction prevents legitimate modifications. The advisory nature means warnings serve as a signal for human review rather than an automated gate. The semantic drift comparator (next section) adds a second layer that catches what structural fingerprinting cannot.

4. Semantic drift detection

The structural fingerprint catches syntactic changes — removed rules, dropped template variables, missing imperative keywords. But a prompt can retain all its structural elements while fundamentally changing its semantic meaning. A constraint reading "You must always cite sources" could be replaced with "You must always prioritise brevity" — both contain the imperative "must always," both would pass the fingerprint check, but they express opposite priorities.

We solved this by applying the same architectural pattern we built for a different problem: real-time persona drift detection in our voice pipeline.

Acompli's voice interface runs a lightweight topic classifier in parallel with the main conversation — a nano-class LLM (~150 tokens per call, completing in 20–50ms) that continuously monitors whether the conversation topic still matches the active persona's domain expertise. When it detects drift, the system switches the governing instructions mid-session. The classifier uses structured prompts with the current context, recent turns, and a catalogue of alternatives, returning a structured JSON verdict with an action, confidence score, and brief reasoning. Adaptive confidence thresholds, cooldown timers, and anti-oscillation guards prevent the system from being trigger-happy.

The insight was that this is the same problem. Detecting whether a modified prompt has drifted semantically from its original intent is structurally identical to detecting whether a conversation has drifted away from the active persona's domain. Both require comparing current content against an expected semantic baseline and returning a confidence-scored verdict.

We apply the pattern to prompt modification as follows. When the update_prompt tool runs, the semantic comparator receives the original prompt content and the proposed new content. A lightweight LLM call — the same nano-class model used in the voice pipeline — evaluates whether the new content preserves the semantic intent of the original. The classifier prompt is structured around specific dimensions:

Behavioural alignment. Do the original and modified prompts instruct the same fundamental behaviours? A prompt that originally emphasised thoroughness shouldn't silently shift to emphasising brevity.

Constraint preservation. Beyond structural presence (which the fingerprint catches), are the meanings of constraints preserved? "You must always verify claims against source documents" and "You must always generate responses quickly" both contain imperatives, but they encode different priorities.

Domain fidelity. Does the modified prompt remain within the same domain scope? A healthcare overlay that drifts into generic advice has lost its domain-specific value even if it retains the right section headers.

Safety intent. Are safety-critical instructions — disclosure restrictions, accuracy requirements, scope boundaries — semantically preserved even if their wording has changed?

The comparator returns a structured result:

{
  "action": "approve" | "warn" | "flag",
  "confidence": 0.0-1.0,
  "drift_dimensions": ["behavioural", "constraint",
                        "domain", "safety"],
  "reasoning": "brief explanation"
}

Like the structural fingerprint, the semantic comparator is advisory — it doesn't block the write. But unlike the structural fingerprint, which surfaces warnings that require a human to interpret ("Removed imperative constraint containing 'must'"), the semantic comparator surfaces warnings that are immediately actionable ("Proposed modification shifts the prompt's priority from thoroughness to speed, which contradicts the original behavioural intent"). The SSE stream delivers these semantic warnings alongside the structural ones, giving operators a layered view of what changed: structurally and semantically.

The adaptive confidence mechanism from the voice pipeline carries over. For prompts that have been stable through many versions, the confidence threshold for flagging semantic drift increases — the system requires stronger evidence before raising an alarm on a well-established prompt. For newly created or recently modified prompts, the threshold is lower, providing tighter oversight during the period when the prompt's semantic identity is still being established.

The cost is minimal. The nano-class model processes the comparison in ~20–50ms at roughly 200 tokens per call. In the context of a skill execution that includes a full LLM generation step (typically 2–5 seconds), this adds negligible latency. It runs as part of the update_prompt tool's governance checks, alongside the structural fingerprint comparison, before the database write proceeds.

5. Tenant isolation

Enforced at three layers:

Skill scoping. Skills are stored with an organisation_id and queried with an org filter. One tenant's skill definitions are invisible to other tenants.

Tool context. The AgentContext binds the organisation_id at skill invocation time. Tools extract it from this context, not from caller-supplied arguments. A skill executing in Tenant A's context cannot pass Tenant B's organisation ID to a prompt tool.

Prompt resolution. The get_prompt_by_name function implements two-level lookup: org-scoped fork first, system default second. An org fork created by Tenant A is only returned when the query includes Tenant A's organisation_id. Tenant B's queries never see Tenant A's forks.

The combination ensures that the self-modification loop is hermetically scoped: Tenant A's skill generates content, persists it to Tenant A's fork, and the modification only affects prompt resolution for Tenant A's future requests.

6. Real-time observability

The skill executor provides a streaming variant that yields server-sent events (SSE) for each phase of execution:

Event sequence for a 5-step skill:

  thinking     {skill_id, skill_name, total_steps, run_id}
  tool_call    {tool: "get_assessment_context", step_id}
  tool_result  {tool: "get_assessment_context",
                status: "succeeded", latency_ms}
  tool_call    {tool: "get_org_prompt", step_id}
  tool_result  {tool: "get_org_prompt",
                status: "succeeded"}
  tool_call    {tool: "llm",
                step_id: "generate_overlay"}
  tool_result  {tool: "llm",
                status: "succeeded", latency_ms}
  tool_call    {tool: "update_prompt",
                step_id: "persist_overlay"}
  tool_result  {tool: "update_prompt",
                status: "succeeded",
                result_preview: "version 1.1,
                                 2 structural warnings"}
  tool_call    {tool: "llm", step_id: "debrief"}
  tool_result  {tool: "llm", status: "succeeded"}
  message      {text: "5/5 steps succeeded."}
  done         {run_id, status: "succeeded"}

Delivered via HTTP text/event-stream with Cache-Control: no-cache and X-Accel-Buffering: no headers, ensuring real-time delivery through proxies. An operator watching a skill modify the prompt configuration can observe each step — including the structural warnings returned by the update operation — before the modification takes full effect.


Two-level customisation and why it compounds

The architecture supports customisation at two independent levels, and their combination creates a compounding effect that's worth understanding.

Level 1: Fork the generation skill. Because skills are stored per-tenant, an organisation can create its own version of the overlay generation skill — with modified steps, different LLM prompts, additional validation steps, or alternative tool sequences. This customises how overlays are generated.

Level 2: Fork the generated overlay. The output of the generation skill — the overlay prompt itself — is stored in the prompt database per-tenant. An organisation can further modify the generated overlay, either manually or through another skill. This customises what the overlay says.

 ┌──────────────────────────────────────────────────┐
 │                SYSTEM DEFAULTS                    │
 │                                                   │
 │  System Skill                System Overlay       │
 │  ┌──────────────┐           ┌──────────────┐     │
 │  │ generate-    │──creates──│ Healthcare   │     │
 │  │ domain-      │           │ Enhancement  │     │
 │  │ overlay v1.0 │           │ Overlay v1.0 │     │
 │  └──────────────┘           └──────────────┘     │
 │         │                          │              │
 │         │ fork                     │ fork         │
 │         ▼                          ▼              │
 │  ┌──────────────────────────────────────────┐    │
 │  │           TENANT A SCOPE                  │    │
 │  │                                           │    │
 │  │  Forked Skill              Forked Overlay │    │
 │  │  ┌──────────────┐        ┌──────────────┐│    │
 │  │  │ generate-    │─creates│ Healthcare   ││    │
 │  │  │ domain-      │  ─────►│ Enhancement  ││    │
 │  │  │ overlay v1.0 │        │ Overlay v1.1 ││    │
 │  │  │ (+ extra     │        │ (customised) ││    │
 │  │  │  validation) │        └──────────────┘│    │
 │  │  └──────────────┘                        │    │
 │  └──────────────────────────────────────────┘    │
 │                                                   │
 │  ┌──────────────────────────────────────────┐    │
 │  │           TENANT B SCOPE                  │    │
 │  │                                           │    │
 │  │  (uses system skill + system overlay)     │    │
 │  └──────────────────────────────────────────┘    │
 └──────────────────────────────────────────────────┘

Tenant A can modify the generation process to include regulatory requirements unique to their jurisdiction, and the output of that customised process is itself a customised prompt that governs AI behaviour specifically for Tenant A. Tenant B, operating in a different regulatory context, maintains their own independent customisation chain — or uses the system defaults entirely. The two tenants' paths are completely isolated.


Self-extension: the system builds its own capabilities

The closed-loop architecture enables something particularly powerful: the system can extend its own domain coverage without code deployment.

The static baseline

Acompli's AI generation pipeline supports domain-specific behaviour through prompt overlays — supplementary instructions that specialise the base prompt for a particular assessment domain. In the initial architecture, these overlays were defined statically: a fixed registry mapped known domain categories to pre-authored overlay prompts. Known domain? Load the overlay. Unknown domain? The base prompt handles it without specialisation.

Dynamic extension

The self-modification loop transforms this from static to dynamic. When the system encounters an assessment domain with no existing overlay, a skill can be invoked to classify the domain, check for existing overlays, generate a domain-specific overlay via LLM, persist it through the governed update path, and activate it immediately for all future requests in the tenant scope.

After this skill executes, the domain that was previously "unknown" is now "natively supported." The prompt database contains a dedicated overlay for it, indistinguishable from the statically defined overlays except for its provenance metadata indicating agent generation.

The pipeline from "unknown domain" to "natively supported domain" requires no code deployment, no configuration change, and no human authoring. An operator invokes the skill, the closed loop executes, and the system's domain coverage has permanently expanded for that tenant.

Compounding coverage

Each invocation permanently expands the system's coverage. Over time, a tenant processing diverse assessment types accumulates a growing library of domain-specific overlays — each generated by the LLM, each versioned and attributed, each reversible by deleting the org fork.

This is constructive self-modification: the system builds its own capabilities by creating new prompt artefacts that extend its behavioural repertoire. Unlike destructive self-modification (where existing behaviour is altered), constructive self-modification adds capabilities without changing what already works. The fork-from-system pattern ensures that statically defined overlays continue to function as before — new overlays simply fill gaps. For high-stakes regulatory domains, generated overlays go through the same version history and structural/semantic verification as any other prompt modification, and the full content is available for human review at any point.


Putting it together

The governance framework gives six properties that together make prompt-level modification workable in regulated environments: version control with full content snapshots, change attribution traceable to the invoking human, structural integrity verification via fingerprint comparison, semantic drift detection adapted from our voice pipeline's persona classifier, tenant-scoped isolation enforced at the tool registry layer, and real-time observability through SSE streaming.

Each property addresses a specific regulatory requirement. Version control and attribution answer "what changed and who authorised it." Structural and semantic verification catch degradation — syntactic and semantic — before it reaches production users. Tenant isolation bounds the blast radius of any modification to a single organisation. Real-time streaming gives operators visibility into the modification as it happens, including any warnings from the integrity checks.

The combination means that when Acompli's AI generates a new domain overlay, every step of that process — from the LLM generation to the template resolution to the database write — is typed, attributed, versioned, integrity-checked, and observable. The resulting overlay is immediately active for all future requests in that tenant's scope, but it's also immediately reversible: delete the org fork and the tenant reverts to the system default in a single operation.

This is the architecture that powers the domain overlay system behind Acompli's assessment engine. It's how the platform extends its own domain coverage without code deployment, adapts to each organisation's regulatory context through two-level customisation, and maintains the audit trail that the EU AI Act, GDPR Article 22, and ISO 42001 require.


This article describes the architecture as of March 2026. The semantic drift detection mechanism described here shares its architectural lineage with our dynamic persona selection system in the voice pipeline. For related work on AI governance in compliance workflows, see our research on governance-first design, the self-reinforcing data lifecycle, and automating administrative burden.