medOS ultra

EMR Cowork Substrate

Hospital-wide AI coworkers on the assistant + acknowledgement + collab stack: three data planes, agent identity/delegation, recommender-first.

15 min read diagramsUpdated 2026-06-09docs/architecture/emr-cowork-substrate.md

Read before any “AI coworker”, agent-identity, agent-delegation, or marketing-AI work. Companion docs: unified-clinical-assistant.md (the skill runtime this builds on), acknowledgement-system.md (the delegation mesh), patient-360.md (the collab surface), navigation-next-best-action.md + ai-training-corpus.md (consent + telemetry + de-id stance), revenue-platform-master-plan/00-MASTER.md (the “agentic substrate, recommender-first” stance this realizes).

Thesis

“Claude Cowork” is AI agents as coworkers — not a chat box, but named, role-scoped agents that sit in the workflow, pick up tasks from a shared queue, do background work, hand a decision back to a human at a gate, and can be handed work by humans. medOS-ultra has already shipped ~70–80% of that substrate under different names (“clinical assistant”, “acknowledgements”, “body-collab”). Building “hospital EMR cowork” here is therefore a reframe + four targeted bridges, applied across the whole hospital (clinical and non-clinical teams), under a three-data-plane safety model.

Verdict: feasible, mostly assembled, gated by safety not by missing tech. The hard, irreversible parts (clinical liability, PDPA/PHI-in-marketing) are handled by the plane model below, not deferred.


1. What already exists (do not rebuild)

Grounded against the codebase 2026-06-05. Status = shipped / partial / design-only.

Cowork primitive What it is here File(s) Status
Agent runtime (tool loop) runAgentLoop + SeenMatches hallucination guard web/src/services/ai/shared/runner-engine.ts shipped (frontend-only)
Skill registry ASSISTANT_REGISTRY — domains w/ toolClass/render/systemPrompt/buildDispatcher/featureFlag/requiredRoles web/src/services/ai/registry.ts shipped (2 live: clinical-query, cohort-query)
Concrete skills voice-order, smart-diagnosis, symptom-mapper, clinical-query, cohort-query web/src/services/ai/*/runner.ts shipped (voice-order + smart-diagnosis not yet folded into registry)
LLM platform (Ollama/RAG) llm.chat/complete/embeddings.search, models/use-cases/corpora registries services/llm/src/api/llm/llmService.ts (+6 mixins) shipped
LLM/agent audit llm_audit_log (user_id, use_case, action, tokens, status) infrastructure/medbase/migrations/20260514c_llm_platform.sql shipped
Delegation mesh AcknowledgementRequest → user/role/department/group + escalation + multi-channel web/src/services/ever-foundation/acknowledgement-request/acknowledgementRequest.service.ts; backend services/foundation/.../acknowledgementRequest; read model acknowledgement_requests (mig 035) shipped
Delegation inbox (FAB) AcknowledgementInbox mounted in App.tsx, realtime on acknowledgement_requests web/src/common/components/medical/builder/acknowledgements/AcknowledgementInbox.tsx shipped
Task/gig board user_tasks + auto-assign + PorterGigBoard @miniapps/porter-gig-board; infrastructure/.../20260515_auto_assign_system.sql shipped
Multiplayer surface body-collab presence/cursors/comments/room-chat (BroadcastChannel ⇄ Supabase realtime) web/src/services/body-collab/* shipped
Confirm-gate / permissions toolClass:'action'render:'confirm-gate'; policy_gates web/src/services/ai/registry.ts; docs/architecture/policy-gates.md shipped
Background routines cron_jobs registry + edge functions + orchestrator docs/architecture/cron-jobs-registry.md shipped
Per-tenant flag + role gate featureFlag + requiredRoles per skill; VITE_TENANT_ID; app-mode web/src/services/ai/registry.ts; web/src/config/app-mode.config.ts shipped
Consent + de-id primitives ml_export_consent (opt-in default FALSE), ml_access_log, k-anon stance docs/architecture/ai-training-corpus.md design (pattern reusable)

Two findings that shrink the build:

  1. An agent can be the requester on AcknowledgementRequest today with zero schema changerequester is a plain string. So “agent proposes → human disposes” (the recommender-first path) rides existing rails. Only delegating to an agent (human→agent, agent→agent) needs a new AckRecipientType: 'agent'.
  2. The skill registry’s toolClass already encodes the safety contract per capability (read = no commit, action = confirm-gate, form-fill = open prefilled). Coworkers inherit it.

2. The four gaps to close

# Gap Why it’s needed Size
G1 Agent identity (agent-as-actor) An agent must be a valid assignee, audit author, ack requester/recipient, collab presence. Today agents are anonymous stateless loops. new table + audit column
G2 Agent ⇄ delegation bridge Let an agent post a proposal into the existing inbox/task board, and (later) receive work. small (requester is free; recipientType:'agent' + proposal envelope)
G3 Backend / background runner runAgentLoop runs only when a human has the tab open. Coworkers must run when nobody’s watching (overnight coding backlog, prior-auth drafts). biggest piece — move/duplicate the loop server-side, trigger from cron_jobs/events
G4 The cowork “team” surface See your coworkers, their queues, hand work over / take it back. extension of PorterGigBoard + AcknowledgementInbox, not a new app

Everything else in §1 is reused as-is.


3. Core abstraction — the Coworker

A coworker = a skill + an identity + a data plane + a role binding + a mode. FHIR-aligned: the coworker is a Provenance.agent where agent.who = Device (the AI software) and agent.onBehalfOf = Practitioner/role (its human supervisor). This matches the repo’s existing “mover-as-Practitioner+Device” stance (hospital-movement-architecture.md).

Entity: cowork_agents (canonical in Mongo foundation; Supabase read model)

cowork_agents
  id              uuid pk
  slug            text unique      -- 'coder-coworker'
  display_name    text             -- "Coder Coworker (AI)"
  avatar_url      text
  skill_id        text             -- → ASSISTANT_REGISTRY domain id
  data_plane      text             -- 'clinical' | 'operational' | 'growth'   (§4)
  consent_scope   text null        -- null | 'marketing'
  serves_role     text             -- role whose queue it feeds, e.g. 'coder'
  supervisor_role text             -- human owner + kill authority
  mode            text             -- 'recommender' | 'actor'   (default 'recommender')
  model_use_case  text             -- → llm_use_cases.code
  trigger_kind    text             -- 'cron' | 'event' | 'manual'
  trigger_config  jsonb            -- cron expr or hospital_events filter
  enabled         boolean default false   -- OFF by default
  kill_switch     boolean default false
  tenant_id       uuid null        -- multi-tenant (see Open Q4)
  created_by, created_at, updated_at

Audit attribution (extend, don’t replace)

Add agent_id text null to llm_audit_log. Every coworker action lands there with its agent identity + the RunnerStep[] trace. Human actions keep user_id; coworker actions set both agent_id (who acted) and user_display of the supervising role (on-behalf-of).

Skill contract (tiny extension to AssistantDomain)

registry.ts’s AssistantDomain gains one optional field so a skill can declare the maximum plane it may run in (defense-in-depth alongside the agent’s data_plane):

// additive — existing fields unchanged
allowedPlane?: 'clinical' | 'operational' | 'growth';

No other registry change. Voice-order/smart-diagnosis fold in as planned (their existing P0).


4. The three data planes (the safety spine)

Same substrate, three data-access contracts. The plane is enforced at the database grant level, not just app logic — a growth-plane agent’s DB role has no grant on clinical tables.

Plane Touches May read Write contract
Clinical PHI, dx, orders, results encounter_journey_cache (structured), clinical read models recommender-first, draft-until-signed, every write via confirm-gate/policy_gates, structured-only LLM payloads, full PHI audit
Operational scheduling, beds, queues, RCM/claims, supply, staffing, consented inbound inquiries department_queues, admission/bed read models, B2B referral data lower clinical risk; still audited; pseudonymize where possible; rides existing gates (policy_gates, auto-assign)
Growth acquisition, campaigns, CRM/loyalty, referral, intl-patient marketing only marketing_audience (consent-gated, de-identified/aggregate) hard-walled from PHI; per-read audit (ml_access_log pattern); bilingual outputs; nothing individually targeted without marketing consent

The marketing wall (PDPA — the sharp edge)

Under Thailand’s PDPA (and BDMS’s own consent posture) you may not use identifiable clinical data for marketing without explicit marketing-purpose consent. Therefore growth-plane coworkers:

  • read only a marketing_audience view exposing consented contacts + de-identified aggregates (age band, region, service-line interest in aggregate) — never diagnoses, results, or encounter detail;
  • run under a dedicated Postgres role with zero grants on encounter_journey_cache and all clinical tables (the wall is in the DB, not the prompt);
  • log every read to an ml_access_log-style audit with a named clinical/data owner per tenant.

This reuses the consent/de-id machinery already designed in ai-training-corpus.md.


5. The delegation bridge (G2)

The proposal envelope

A coworker never mutates the chart. It produces a proposal and asks a human to dispose of it:

cowork_proposals (new, small)
  id            uuid pk
  agent_id      → cowork_agents.id
  subject_ref   text            -- encounter / referral / campaign id
  draft_payload jsonb           -- the proposed codes / itinerary / campaign brief
  write_action  jsonb           -- the existing write path to call on accept
  confidence    numeric null
  runner_trace  jsonb           -- RunnerStep[] for audit + "why"
  status        text            -- 'pending' | 'accepted' | 'edited' | 'rejected'
  decided_by    text null
  decided_at    timestamptz null

The coworker then creates an AcknowledgementRequest:

createAcknowledgementRequest({
  subject:   { orderType: 'agent_proposal', orderId: proposal.id, display: '…' },
  requester: '<cowork_agents.id>',            // ← already allowed (string)
  recipient: { recipientType: 'role', roleId: 'coder' },
  priority:  'routine',
});

One new AckOrderType: 'agent_proposal' so proposals are first-class + filterable. The AcknowledgementInbox gets one renderer for that type: shows the draft + Accept / Edit / Reject.

Accept/Edit/Reject = the label

On disposition:

  • Accept → call write_action (the existing human write path — coding write, DialogOrder prefill, task dispatch). Same path a human uses; same gates.
  • Edit → human modifies, then the edited payload goes through the same write path.
  • Reject → no write.

The accept/edit/reject delta is captured to llm_audit_log as the training label (per feedback_clinical_ai_label_in_workflow — “useful?” not “certify?”). Never written to the chart.


6. The backend runner (G3)

Move/duplicate runAgentLoop server-side (extend services/llm, or a new services/ai backend microservice — Open Q1). Triggered by cron_jobs (sweep) or hospital_events (reactive). It runs the agent’s skill, produces a cowork_proposals row + an ack, and stops. It has no write grant on clinical tables; the human’s Accept is what writes.

cron_jobs / hospital_events
        │
        ▼
 backend runner ──reads (plane-scoped views)──▶ runAgentLoop(skill, ctx)  [SeenMatches guard]
        │                                              │
        │                              draft + RunnerStep[] + confidence
        ▼                                              ▼
 cowork_proposals  ───────────────▶  AcknowledgementRequest{requester=agent, recipient=role}
                                                       │
                                              AcknowledgementInbox (human)
                                                       │
                                   Accept / Edit / Reject ──▶ existing write path + label→audit

Invariant: the runner is decoupled from the alert/critical path and never writes a read model directly (no agent projection writes — same rule as the frontend).


7. BDMS-grounded coworker taxonomy

Grounded in Bangkok Hospital / BDMS (49+ hospitals, 60+ clinical departments, 100k+ international patients/yr; C-suite = President·CFO·COO·CMO·Chief Administrative Officer; strong digital marketing, CRM, physician-liaison referral, and international patient services). Sources at end.

Hospital team Cowork agent Rides on (already in repo) Plane
Coding / Health Info Coder (draft ICD-10/9 + DRG) smart-diagnosis + medical-coder + ack inbox Clinical
Nursing SBAR / alerts / care-plan draft kardex + CDS + acknowledgement Clinical
Pharmacy Compounding worksheet + interaction 2nd-opinion compounding-room + CDS Clinical
Physicians Ambient scribe + orders + differentials scribe (design) + voice-order + smart-diagnosis Clinical
Revenue Cycle (CFO) Claim draft, revenue-at-risk, prior-auth NHSO engine + revenue platform Operational
Patient Access / Admissions Admission coordinator admission-routing + queues + auto-assign Operational
International Patient Services Navigation, interpreter prep, records-transfer (consented), itinerary acknowledgement + tasks + FHIR bundle export Operational + consent
Physician Liaison / Referral Referral analytics, referring-doc outreach (B2B) tasks + LLM + analytics Growth (B2B, low PHI)
Marketing & Comms Campaign content/analytics (EN+TH), channel ops LLM platform + aggregate views Growth (consent/aggregate)
CRM / Loyalty / Patient Experience Segmentation, NPS analysis, re-engagement LLM + aggregate views Growth (consent-gated)
HR / Human Capital Rostering, credential-expiry, onboarding cron_jobs + tasks Operational (staff data)

8. First three vertical slices (one per plane)

Slice A — Coder coworker (Clinical)

  • Agent: coder-coworker, skill coding-draft (new form-fill domain on smart-diagnosis tools searchIcd10/searchSnomedCt/mapSnomedToIcd10 + a proposeCoding terminal), data_plane:'clinical', serves_role:'coder', mode:'recommender', trigger_kind:'event' (encounter discharged) or cron sweep of newly-discharged uncoded encounters.
  • Flow: runner reads the discharged encounter’s structured summary → drafts codes → writes cowork_proposals + ack to role coder → coder opens it in AcknowledgementInbox → Accept/Edit/Reject → on Accept the existing coding write path persists; gesture → label.
  • Reuses: smart-diagnosis runner/tools, runner-engine, AcknowledgementRequest, AcknowledgementInbox, llm + audit.
  • New: cowork_agents/cowork_proposals, agent_proposal ack type + renderer, the coding-draft skill, backend trigger.

Slice B — International-Patient / Referral coworker (Operational)

  • Agent: ips-coordinator, skill ips-itinerary (+ referral-analytics for the liaison persona), data_plane:'operational', consent_scope:null, serves_role:'international-patient-services'.
  • Flow: trigger on inbound international inquiry / new referral → runner drafts the patient journey itinerary (appointments to book, interpreter language, records-transfer checklist via web/src/utils/fhir-bundle-export.ts, consented) or a referral analytics summary for the physician liaison → proposal as a user_task assigned to the IPS team (auto-assign) → coordinator reviews/dispatches.
  • Data plane: patient/referrer initiated contact (consented purpose); no clinical decision-making; records-transfer only with consent; B2B referral data is non-PHI.
  • Reuses: user_tasks/PorterGigBoard, acknowledgement, FHIR bundle export, auto-assign.
  • New: the IPS skill + trigger.
  • Prerequisite (build first): marketing_audience view — consented contacts (marketing_consent = true) + de-identified aggregates only; no clinical columns. RLS so the growth-plane DB role can SELECT only this view and has no grant on clinical tables. Per-read audit (ml_access_log pattern).
  • Agent: campaign-coworker, skill campaign-draft (segment summary + bilingual EN/TH content), data_plane:'growth', consent_scope:'marketing', serves_role:'marketing', trigger_kind:'manual'|'cron'.
  • Flow: runner reads marketing_audience (aggregate) + LLM → drafts campaign concept / segment summary / content → proposal as user_task to the marketing team → marketer reviews/approves. The agent cannot read who has which diagnosis — the wall is in the DB.
  • Reuses: LLM platform, tasks, audit, consent/de-id pattern from ai-training-corpus.md.
  • New: marketing_audience view + RLS + growth DB role + marketing-consent source, the campaign-draft skill. PDPA review of the consent source (Open Q5).

9. Invariants (hard rules)

  1. No autonomous clinical writes. Coworkers PROPOSE; humans DISPOSE via the existing confirm-gate/policy_gates.
  2. Plane confinement is enforced at the DB grant level, not just app logic.
  3. Growth-plane coworkers never read PHI — only marketing_audience; their DB role has zero clinical grants.
  4. Every coworker action is audited in llm_audit_log under agent_id with the RunnerStep trace.
  5. Off by default — per-tenant enabled + kill_switch + role gating; ship disabled.
  6. Accept/Edit/Reject is the label — captured to the audit log, never written to the chart.
  7. Recommender → actor promotion is per-tenant, evidence-based, reversible (kill switch); default mode = recommender.
  8. No hallucinated entities — keep SeenMatches; terminal proposals may only reference catalog IDs surfaced during the run.
  9. Bilingual outputs (local + English) per repo i18n/seed rule.
  10. A coworker never writes a read model directly (no agent projection writes) — it rides backend write paths + the orchestrator, same as a human.

10. Phased rollout

Phase Deliverable Demo-ready?
P0 Agent identity: cowork_agents, llm_audit_log.agent_id, AssistantDomain.allowedPlane. No coworker runs yet. Laid down 2026-06-05infrastructure/medbase/migrations/20260605a_cowork_agents.sql + web/src/services/ai/registry.ts. ✅ done
P1 Delegation bridge: cowork_proposals, AckOrderType:'agent_proposal', Inbox renderer w/ Accept/Edit/Reject → label. Manual trigger. Built 2026-06-05 — mig 20260605b, cowork-proposal-decide edge fn, CoworkProposalPanel(+Connected) + Inbox branch, seed-cowork-proposal-demo.mjs. ✅ built
P2 Backend runner: llm.coworkRun mixin in services/llm (modules/coworkRunner/) — reads enabled cowork_agents, drives the skill via llm.complete (Ollama), writes cowork_proposals + agent_proposal ack, audits under agent_id. Built 2026-06-05 (verifies on backend deploy; needs a seeded cowork.default use_case + an enabled agent to run). ✅ built
P3 Slice A — Coder (clinical). End-to-end clinical vertical.
P4 Slice B — IPS/Referral (operational).
P5 Growth foundation: marketing_audience consent-gated view + RLS + growth DB role + per-read audit. infra
P6 Slice C — Campaign (growth).
P7 Cowork “team” surface (coworker roster + queues; recipientType:'agent' for human→agent / agent→agent); promotion governance.

P0→P3 is the shortest path to a real, safe clinical demo. P5→P6 adds the marketing plane behind the consent wall.


11. Open questions

  1. Backend runner home — ✅ RESOLVED 2026-06-05: host in services/llm as a separable cowork.* module; extract to services/cowork at P7. The runner’s first job (P3 Coder) is “LLM + tools + RAG → propose” — exactly what chatOrchestrator already does; reuses the Ollama connection, llm_audit_log, and llm_use_cases (a skill ≈ a use-case row + tools), and avoids a greenfield deploy target (the deploy list is finicky). A hard module boundary (cowork.* namespace, depends on llm.* via the broker only — no internal imports) keeps later extraction mechanical when it grows scheduling-ownership / multi-agent / agent-to-agent. Triggers ride the existing edge-fn → NestJS bridge (cf. close-encounter-billing): discharge event → hospital_events → encounter-orchestrator → cowork.run.
  2. recipientType:'agent' (human→agent / agent→agent delegation, P7) — confirm Mongo entity + acknowledgement_requests migration.
  3. Collab presence for agents — surface a coworker as a body-collab presence on Patient 360 (read-only, @-mentionable)? Phase 7.
  4. Multi-tenancycowork_agents.tenant_id vs the on-prem single-tenant reality (cloud tenancy is a separate cross-cutting migration per menu-price-ledger.md).
  5. Marketing consent source of truth — ✅ RESOLVED 2026-06-05: dedicated marketing_consents table; do NOT reuse ml_export_consent. PDPA consent is purpose-bound — ML-training-export consent ≠ marketing consent; reuse = purpose-creep (a violation). The record must be opt-in (default false), per-channel (email/SMS/push), withdrawable (withdrawn_at), and versioned (consent_version + source) — a boolean column is insufficient. P5 builds marketing_consents + the marketing_audience view (join granted = true AND withdrawn_at IS NULL for the purpose/channel). Consent-capture UI + notice text = separate workstream, DPO/legal sign-off required before go-live. (Data shape only; not legal advice.)
  6. Promotion criteria recommender→actor — acceptance-rate threshold, per-skill, per-tenant evidence bar?

Sources (Bangkok Hospital / BDMS grounding)


Created 2026-06-05. Status: design. Realizes the “agentic substrate, recommender-first, opt-in” stance from revenue-platform-master-plan/00-MASTER.md on top of the shipped assistant + acknowledgement + collab substrate.

Ask Anything