Uh oh!
There was an error while loading. Please reload this page.
feat(S3): four-agent orchestration + FHIR Task creation - #4
Merged
Conversation
…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 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
code-reviewcaught 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 passingnpm run test:web— 5 files / 24 tests passingnpm run test:e2e— 4/4 Playwright specs passing (incl. new four-feed + Task-card spec)npm run build+npm run lintclean for bothapps/apiandapps/webgpt-5.5+ live HAPI — all four agents produced structured output, 10 Tasks landed in HAPI, fabrication-drop path proven against the real validatorverification-before-completionandcode-reviewboth passed — seedocs/plans/caresync-ai/verification.mdandreview.md🤖 Generated with Claude Code