Skip to content

feat(S3): four-agent orchestration + FHIR Task creation - #4

Merged
manjula25 merged 14 commits into
mainfrom
feature/caresync-s3-four-agent-orchestration
Jul 4, 2026
Merged

feat(S3): four-agent orchestration + FHIR Task creation#4
manjula25 merged 14 commits into
mainfrom
feature/caresync-s3-four-agent-orchestration

Conversation

@manjula25

Copy link
Copy Markdown
Collaborator

Summary

  • Extends S2's single Risk agent to the full four-agent care team: an Orchestrator dispatches Risk, Care Gap, and SDOH agents in parallel over the patient's FHIR bundle, then an Action Planner synthesizes their outputs into prioritized FHIR Task resources written to HAPI — every citation validated against the bundle (GD11), none fabricated.
  • All four feeds stream live in the UI with per-agent color identity; created Tasks render as cards with citation chips.
  • This session closed out the two verification phases that were still outstanding (D2 Playwright E2E, D3 live OpenAI call against real HAPI), and code-review caught and fixed one real drift: the SDOH/Care Gap agent prompts still referenced resource types (QuestionnaireResponse/CarePlan) that don't exist in the seed data, re-verified live with 0 dropped citations after the fix.

Test plan

  • npm run test:api — 19 suites / 80 tests passing
  • npm run test:web — 5 files / 24 tests passing
  • npm run test:e2e — 4/4 Playwright specs passing (incl. new four-feed + Task-card spec)
  • npm run build + npm run lint clean for both apps/api and apps/web
  • Live evidence: one real orchestrated run against OpenAI gpt-5.5 + live HAPI — all four agents produced structured output, 10 Tasks landed in HAPI, fabrication-drop path proven against the real validator
  • verification-before-completion and code-review both passed — see docs/plans/caresync-ai/verification.md and review.md

🤖 Generated with Claude Code

manjula25and others added 14 commits July 4, 2026 19:46
…der refs
- S4 plan: derive state machine from finding/complete/task+agentId (no dispatch
event); cache Task payloads not ids; phased-order replay; cold-cache fallback
test; reduced-motion E2E note.
- issues.md: align S4/S12 wording with GD13 (OpenAI gpt-5.5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dator for id-lists
Extract AgentId, per-agent output interfaces (Risk/CareGap/SDOH/ActionPlanner),
and a discriminated AgentEvent union keyed by agentId into agents/agent.ts.
Refactor runRiskAgent to self-tag events with agentId:'risk'; re-export
AgentEvent/RiskOutput from riskAgent for existing importers. Add
validateCitationList (all-or-nothing per item, narrows to valid subset) and
reimplement validateCitations as a thin wrapper. No change to emitted SSE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Care Gap agent implementing the shared Agent contract, mirroring
riskAgent's structure: lazy OpenAI client, gpt-5.5, a flat Responses-API
`report_care_gaps` function tool shaping CareGapOutput, streamed token
events self-tagged agentId:'careGap', and a single parsed result event.
Prompt enforces GD11 — every gap must cite an existing ResourceType/id.
Test-first: mocked OpenAI client, no live call. Full apps/api suite green
(67 tests), clean tsc build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the established agent pattern (risk/careGap): lazy OpenAI client,
gpt-5.5, a flat Responses-API report_sdoh function tool, streamed token
events self-tagged agentId:'sdoh', one parsed result event, throw-if-tool-
missing. Prompt directs the agent to read the AHC-HRSN QuestionnaireResponse
plus demographics/observations and cite the exact ResourceType/id per barrier
(GD11); fabricated citations are dropped downstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…indings
Fourth care-team agent — unlike the other three, it reads no PatientBundle
and instead builds its prompt entirely from the Risk/CareGap/SDOH agents'
already-parsed outputs, instructing the model that every task's
fhirResources must be drawn from the union of ids those three already cite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds orchestrate(bundle) which runs the risk, careGap, and sdoh agents
concurrently via a minimal race-based async-iterable merge, forwards their
events as they arrive (true interleaving, not sequential await-then-await),
then runs the action planner on their collected outputs once all three are
exhausted. Test-first with four injected stub agents using deferred gates to
deterministically prove concurrency, interleaving order, and the planner's
exact input.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds createTask and replacePatientTasks to FhirReadService so Action
Planner output can be persisted as real HAPI Task resources, tagged
ai-generated-task so replace-on-rerun never touches seed/Synthea Tasks.
replacePatientTasks reads existing owned Tasks via $everything rather
than Task?search — verified against the real local HAPI that its
search index lags multiple seconds behind writes under a create/delete
burst, while $everything reflects them immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swaps the analysis route from the single Risk agent to orchestrate(),
gating every citation (flags/gaps/barriers/tasks) through validation
before it reaches the client or is persisted to HAPI as a Task. Adds
per-agent narration buffering (a Map<AgentId, NarrationBuffer>, flushed
on each agent's own result) so four interleaved token streams can't
scramble each other's trailing text, and designs the final SSE
vocabulary: token/finding/complete tagged per agentId, one complete per
agent, a task event per Task actually created in HAPI, and a single
terminal done event (absent on any mid-stream error).
Generalizes validateCitations to <T extends {fhirResourceId}> so the
same gate covers RiskOutput.flags, CareGapOutput.gaps, and
SdohOutput.barriers; the Action Planner's tasks go through
validateCitationList (all-or-nothing per task, narrowed to survivors)
before replacePatientTasks is ever called.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend streamAnalysis's dispatch and exported types (AgentId,
AnalysisFinding, AnalysisSummary, AnalysisTask, AnalysisHandlers) to carry
the new agentId tag from the four-agent backend and route the two new
event types (task -> onTask, done -> onDone). Transport/parsing only;
PatientDetail.tsx consumption is a later task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted Tasks
Wire PatientDetail's remaining three feed boxes to the SSE stream using a
Record<AgentId,...> state slice (mirroring the Risk feed's existing pattern),
render newly-created Tasks with FHIR citation chips as they arrive via
onTask, and fix the two test fixtures missing the now-required `agentId`
field that broke `npm run build --workspace apps/web`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… ref
client.ts's dispatch dropped payload.agentId for token SSE events, and
PatientDetail's activeAgentRef heuristic only updated on finding/complete —
so a new agent's first token (before it emits any finding/complete) bled
into whichever agent's box the ref last pointed at. onToken now carries
agentId straight from the wire, matching finding/complete, and the ref is
gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mplete
Real backend narrates via token events throughout an agent's run and only
emits finding/complete at the very end, but withText never set started:true —
so Care Gap/SDOH/Action Planner sat on "Awaiting analysis run…" through their
entire live-narration phase and only dumped full text once finding/complete
finally arrived, defeating the streaming UX. Risk agent was unaffected since
it's force-started at run-begin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
issues.md/prd.md claimed SDOH reads a QuestionnaireResponse and Care Gap
reads CarePlan — neither resource type is ever seeded (S1's AHC-HRSN
screening is modeled as an Observation; no CarePlan exists). Verified live
against HAPI. Citation safety was never affected — both agents already cite
real, in-bundle resource ids of the types that do exist. Reworded to match
reality per user decision (fix wording over adding seed data).
Also checks off implementation-plan.md's S3 Phase A-C tasks (all implemented,
reviewed, tested against real HAPI) and Phase D1 (unit/API suites green),
and records two post-review bug fixes (token/agentId misattribution across
concurrent feeds; three of four feeds not visibly streaming during narration)
found only once the whole implementation was reviewed together. D2 (Playwright
E2E) and D3 (live OpenAI call) remain outstanding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…prompts
D2 (Playwright E2E) and D3 (live OpenAI call) were the two outstanding S3
verification phases. Extends patient-analysis.spec.ts with an agentId-tagged
four-feed + Task-card E2E test, and fixes the existing S2 test's frames
(they predated agentId tagging and had silently stopped routing to any feed).
Adds a data-testid to task cards so citation-chip assertions can disambiguate
from the same ids elsewhere on the page.
D3: one live orchestrated run against OpenAI gpt-5.5 and the live HAPI
container proved all four agents produce structured output with created
Tasks landing in HAPI, and the fabrication-drop path was proven against the
real validateCitations function.
code-review (this session) then caught a real drift: sdohAgent.ts and
careGapAgent.ts still prompted the model to cite QuestionnaireResponse/
CarePlan resources that don't exist in the seed data (AC #4 was corrected in
a prior commit, but only in docs, not the prompts) - a live risk that the
SDOH housing/food barrier could be validator-dropped. Fixed both prompts to
match the real Observation-based seed model and re-verified live: still 0
dropped citations, SDOH correctly citing the real Observation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@manjula25
manjula25 merged commit 94d4f82 into mainJul 4, 2026
manjula25 added a commit that referenced this pull request Jul 8, 2026
Caught by live smoke test (this session, after the PR was open):
the smoke test of gap #4 hit a regression that 281/281 unit tests
couldn't catch — every test file builds its own Express app without
mounting smartAuth, so the mount-order bug in index.ts was invisible
to the suite.
The regression: smartAuth was mounted via
app.use('/api/patients', smartAuth, createPatientsRouter(fhirService))
which meant smartAuth ran BEFORE the route's inner `requireAuth`. A
login JWT (signed with auth/jwt.ts's JWT_SECRET) would hit smartAuth
first, fail signature verification (smartAuth expects SMART tokens
signed with serverSecret), and 401 with `invalid_signature` before
requireAuth ever saw it. Net effect: every legitimate API caller
with a login JWT — the UI, scripts, the test-via-server path — was
locked out of every HAPI-touching route.
Two-part fix:
1. smartAuth no-double-auth pass-through. If `req.auth` is already
set by an upstream middleware (requireAuth), smartAuth calls
next() without validating the SMART shape. This is a 4-line
additive check at the top of the middleware function. Preserves
all 5 existing unit tests (none of them pre-set req.auth) and
adds a 6th test "passes through when req.auth is already set by
an upstream middleware (no double-auth)" that pins the new
behavior with a fake upstream.
2. Mount order: smartAuth is now wrapped INSIDE each router, AFTER
requireAuth. New helper `wrapRouterWithSmartAuth(router, smartAuth)`
in smartAuth.ts does `router.use(smartAuth)` post-construction.
index.ts mounts become
app.use('/api/patients', wrapRouterWithSmartAuth(createPatientsRouter(fhirService), smartAuth))
— same shape for all 10 HAPI-touching prefixes. The error
handler remains mounted globally at the bottom of index.ts.
Net result: login JWT → requireAuth passes → smartAuth no-double-auth
passes → route handler runs. SMART-shape tokens still hit smartAuth's
full validation (when requireAuth is taught to accept them too —
tracked as follow-up #7 in verification-s14.md).
Live smoke test (npm run dev):
- Login JWT → GET /api/patients/maria-chen → 200 ✓ (was 401)
- No token → 401 {"error":"Missing bearer token"} (caught by requireAuth)
- Garbage SMART-shape token → 401 {"error":"Invalid or expired token"} (caught by requireAuth; smartAuth never runs)
Test suite: 282/282 (was 281; the new pass-through test is the +1).
tsc clean.
Note: this is a real regression introduced by Commit 4 (5e73c68).
The Commit 4 self-review claim that "tests are unchanged because each
test file builds its own Express app without mounting smartAuth" was
true at face value but missed the production mount order. The unit
tests for the middleware itself were always green; the regression
lived in the integration glue (index.ts). This is a useful lesson
for S15: mount-order bugs don't surface from per-route test apps.
Follow-up #7 (verification-s14.md): requireAuth should also learn to
accept SMART-shape tokens so the two tiers fully interoperate. Out
of scope for this fix — the immediate regression is closed.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manjula25