Skip to content

Commit c21c4d9

Browse files
bloveclaude
andauthored
feat(e2e-harness): restore aimock — replay+record wrapper, drift infra, scrub repairs (#947)
* chore: reinstate the aimock devDependency for e2e replay and record Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(e2e-harness): thin aimock wrapper with replay and record modes replaces the vendored mock Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(e2e-harness): AIMOCK_MODE env wiring for the setup factories and examples suite Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(e2e-harness): restore the fixture drift differ as part of the shared harness Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: restore the weekly fixture-drift run against the live provider Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(scripts): restore the per-cap fixture recorders Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: repair the scrub artifacts left in historical plans and specs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: declare aimock through an npm alias so code never names the upstream org Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: gitignore staged recordings; only true drift failures open issues Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 11b7417 commit c21c4d9

87 files changed

Lines changed: 1110 additions & 634 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/aimock-drift.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: aimock fixture drift
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
# Weekly, Monday 09:00 UTC. Advisory only — never a merge gate. The @drift
7+
# e2e subset runs against the LIVE provider through aimock's record proxy;
8+
# a red run means today's model no longer satisfies our contract
9+
# assertions. Recordings are uploaded as an artifact either way.
10+
- cron: '0 9 * * 1'
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: false
15+
16+
permissions:
17+
contents: read
18+
issues: write
19+
20+
env:
21+
DO_NOT_TRACK: '1'
22+
23+
jobs:
24+
drift:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
28+
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
29+
with:
30+
node-version: 22
31+
cache: npm
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
34+
- run: npm ci
35+
- name: Cache examples-chat python venv
36+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
37+
with:
38+
path: examples/chat/python/.venv
39+
key: uv-examples-chat-${{ runner.os }}-${{ hashFiles('examples/chat/python/uv.lock') }}
40+
- name: Sync examples-chat python
41+
working-directory: examples/chat/python
42+
run: uv sync
43+
- run: npx playwright install --with-deps chromium
44+
- name: Run @drift subset against the live provider
45+
id: drift-run
46+
env:
47+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
48+
AIMOCK_MODE: record
49+
AIMOCK_RECORD_DIR: ${{ runner.temp }}/recordings
50+
working-directory: examples/chat/angular
51+
run: npx playwright test --config e2e/playwright.config.ts --grep @drift
52+
- name: Structural diff vs committed fixtures
53+
if: always()
54+
working-directory: examples/chat/angular
55+
run: |
56+
mkdir -p "${{ runner.temp }}/recordings"
57+
npx tsx ../../../libs/e2e-harness/src/drift.ts "${{ runner.temp }}/recordings" e2e/fixtures | tee "${{ runner.temp }}/drift-report.json"
58+
- name: Upload recordings artifact
59+
if: always()
60+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
61+
with:
62+
name: aimock-recordings
63+
path: ${{ runner.temp }}/recordings
64+
if-no-files-found: warn
65+
- name: Open issue on drift
66+
if: failure() && steps.drift-run.conclusion == 'failure'
67+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
68+
with:
69+
script: |
70+
const fs = require('fs');
71+
const { owner, repo } = context.repo;
72+
let report = '(drift report unavailable)';
73+
try { report = fs.readFileSync(process.env.RUNNER_TEMP + '/drift-report.json', 'utf8'); } catch {}
74+
const trigger = context.eventName === 'schedule' ? 'scheduled' : 'manually dispatched';
75+
await github.rest.issues.create({
76+
owner, repo,
77+
title: 'aimock drift: @drift subset failed against the live provider',
78+
body: [
79+
`The ${trigger} fixture drift check failed.`,
80+
'',
81+
`Run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
82+
'',
83+
'Structural diff of recordings vs committed fixtures:',
84+
'```json',
85+
report.slice(0, 6000),
86+
'```',
87+
].join('\n'),
88+
labels: ['drift'],
89+
});

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,9 @@ keys/
7474

7575
# Playwright demo-recording output (large binaries; see apps/website/scripts/upload-demo-media.md)
7676
**/.record-output/
77+
78+
# aimock record-mode scratch dirs (AIMOCK_MODE=record captures; see libs/e2e-harness/src/aimock-mode.ts)
79+
.aimock-recordings/
80+
81+
# Aborted fixture-record runs strand raw recordings under a committed path
82+
**/e2e/fixtures/.staging/
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MIT
3+
#
4+
# Capture aimock fixtures for the c-interrupts graph by running the standalone
5+
# langgraph dev server against aimock in --record mode. Drives TWO booking
6+
# flows in sequence so the recorded fixture covers both confirm and cancel
7+
# resume paths.
8+
#
9+
# WHY THIS IS A SPECIAL-CASE SCRIPT (not using the generic
10+
# scripts/record-aimock-cap.sh):
11+
#
12+
# Most caps' flows are normal LLM-call → tool_call → continuation cycles
13+
# that complete in a single run; the generic recorder handles those by
14+
# polling for terminal status (success/error/timeout/interrupted) and then
15+
# merging the captured fixture files. c-interrupts is different: the graph
16+
# calls langgraph's interrupt() inside a ToolNode, which pauses the run
17+
# (status=interrupted) and requires the client to POST a `command.resume`
18+
# value back to continue. The recorded fixture has to capture BOTH the
19+
# pre-interrupt LLM call AND the post-resume continuation, which means
20+
# driving the resume API call from the recorder script. The drive_flow
21+
# helper below handles that two-phase dance.
22+
#
23+
# Run from repo root:
24+
# OPENAI_API_KEY=sk-... bash cockpit/chat/interrupts/angular/e2e/scripts/record-c-interrupts.sh
25+
set -euo pipefail
26+
27+
REPO_ROOT="$(cd "$(dirname "$0")/../../../../../.." && pwd)"
28+
cd "$REPO_ROOT"
29+
30+
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
31+
for env_path in examples/chat/python/.env cockpit/chat/interrupts/python/.env; do
32+
if [[ -f "$env_path" ]]; then
33+
set -a; source "$env_path"; set +a
34+
break
35+
fi
36+
done
37+
fi
38+
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
39+
echo "OPENAI_API_KEY not set (in env or examples/chat/python/.env)" >&2
40+
exit 1
41+
fi
42+
43+
AIMOCK_PORT=19999
44+
LANGGRAPH_PORT=5503
45+
FIXTURE_OUT="cockpit/chat/interrupts/angular/e2e/fixtures/c-interrupts.json"
46+
RECORD_DIR="$(pwd)/cockpit/chat/interrupts/angular/e2e/fixtures/.staging"
47+
rm -rf "$RECORD_DIR"
48+
mkdir -p "$RECORD_DIR"
49+
TMP_DIR="$(mktemp -d)"
50+
trap 'rm -rf "$TMP_DIR"' EXIT
51+
52+
if [[ -f "examples/chat/python/.env" ]]; then
53+
cp examples/chat/python/.env cockpit/chat/interrupts/python/.env
54+
fi
55+
56+
echo "[record] starting aimock --record on :$AIMOCK_PORT"
57+
mkdir -p "$(dirname "$FIXTURE_OUT")"
58+
npx llmock \
59+
--port "$AIMOCK_PORT" \
60+
--record \
61+
--provider-openai https://api.openai.com \
62+
--fixtures "$RECORD_DIR" \
63+
--chunk-size 4096 \
64+
> "$TMP_DIR/aimock.log" 2>&1 &
65+
AIMOCK_PID=$!
66+
67+
cleanup() {
68+
if [[ -n "${LG_PID:-}" ]]; then
69+
pkill -P "$LG_PID" 2>/dev/null || true
70+
kill "$LG_PID" 2>/dev/null || true
71+
fi
72+
kill "$AIMOCK_PID" 2>/dev/null || true
73+
wait 2>/dev/null || true
74+
rm -rf "$TMP_DIR"
75+
}
76+
trap cleanup EXIT
77+
78+
for _ in {1..30}; do
79+
if curl -sf "http://127.0.0.1:$AIMOCK_PORT/health" > /dev/null 2>&1; then break; fi
80+
if curl -sf "http://127.0.0.1:$AIMOCK_PORT/" > /dev/null 2>&1; then break; fi
81+
sleep 1
82+
done
83+
echo "[record] aimock ready"
84+
85+
echo "[record] starting langgraph dev on :$LANGGRAPH_PORT (OPENAI_BASE_URL=http://127.0.0.1:$AIMOCK_PORT/v1)"
86+
if command -v setsid >/dev/null 2>&1; then
87+
RUN_PREFIX="setsid"
88+
else
89+
RUN_PREFIX=""
90+
fi
91+
(
92+
cd cockpit/chat/interrupts/python
93+
# aimock --record forwards requests to real OpenAI but doesn't substitute
94+
# the API key, so we must pass the real key through to langgraph. The
95+
# recorded fixture matches on request body content (userMessage, tool
96+
# results, etc.) — not on the Authorization header — so no auth leak.
97+
OPENAI_BASE_URL="http://127.0.0.1:$AIMOCK_PORT/v1" OPENAI_API_KEY="$OPENAI_API_KEY" \
98+
exec $RUN_PREFIX uv run langgraph dev --port "$LANGGRAPH_PORT" --no-browser
99+
) > "$TMP_DIR/langgraph.log" 2>&1 &
100+
LG_PID=$!
101+
102+
for _ in {1..60}; do
103+
if curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/ok" > /dev/null; then break; fi
104+
sleep 1
105+
done
106+
if ! curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/ok" > /dev/null; then
107+
echo "[record] langgraph failed to start; tail of log:" >&2
108+
tail -30 "$TMP_DIR/langgraph.log" >&2
109+
exit 2
110+
fi
111+
echo "[record] langgraph ready"
112+
113+
# Helper: drive one full booking flow (prompt → interrupt → resume → final).
114+
drive_flow() {
115+
local prompt="$1"
116+
local resume_value="$2"
117+
local label="$3"
118+
119+
echo "[record][$label] thread + run with prompt: $prompt"
120+
local thread
121+
thread=$(curl -sf -X POST "http://127.0.0.1:$LANGGRAPH_PORT/threads" \
122+
-H 'content-type: application/json' -d '{}' \
123+
| python3 -c 'import sys,json; print(json.load(sys.stdin)["thread_id"])')
124+
local run
125+
run=$(curl -sf -X POST "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/runs" \
126+
-H 'content-type: application/json' \
127+
-d "{\"assistant_id\": \"c-interrupts\", \"input\": {\"messages\": [{\"role\": \"user\", \"content\": \"$prompt\"}]}}" \
128+
| python3 -c 'import sys,json; print(json.load(sys.stdin)["run_id"])')
129+
echo "[record][$label] thread=$thread run=$run; polling for interrupt"
130+
131+
# LangGraph quirk: when interrupt() fires inside a ToolNode, runs.get()
132+
# reports status=success. The authoritative interrupt signal is the
133+
# presence of an unresolved interrupt in thread state. Gate on that.
134+
local status=""
135+
local has_interrupt="False"
136+
for _ in {1..120}; do
137+
status=$(curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/runs/$run" \
138+
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("status",""))')
139+
case "$status" in
140+
interrupted|success|error|timeout) break ;;
141+
esac
142+
sleep 2
143+
done
144+
if [[ "$status" == "error" || "$status" == "timeout" ]]; then
145+
echo "[record][$label] run terminal status=$status (no normal stop)" >&2
146+
tail -40 "$TMP_DIR/langgraph.log" >&2
147+
exit 3
148+
fi
149+
has_interrupt=$(curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/state" \
150+
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(any(it.get("value") is not None for t in d.get("tasks",[]) for it in t.get("interrupts",[])))')
151+
if [[ "$has_interrupt" != "True" ]]; then
152+
echo "[record][$label] expected pending interrupt in thread state, found none (run status=$status)" >&2
153+
tail -40 "$TMP_DIR/langgraph.log" >&2
154+
exit 3
155+
fi
156+
echo "[record][$label] interrupt fired; posting resume=$resume_value"
157+
158+
local resume_run
159+
resume_run=$(curl -sf -X POST "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/runs" \
160+
-H 'content-type: application/json' \
161+
-d "{\"assistant_id\": \"c-interrupts\", \"command\": {\"resume\": \"$resume_value\"}}" \
162+
| python3 -c 'import sys,json; print(json.load(sys.stdin)["run_id"])')
163+
164+
# Resume run completion signal: terminal status reached AND no pending
165+
# interrupt remains in thread state.
166+
status=""
167+
for _ in {1..120}; do
168+
status=$(curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/runs/$resume_run" \
169+
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("status",""))')
170+
case "$status" in
171+
success|error|timeout|interrupted) break ;;
172+
esac
173+
sleep 2
174+
done
175+
if [[ "$status" == "error" || "$status" == "timeout" ]]; then
176+
echo "[record][$label] resume run did not reach a normal stop (status=$status)" >&2
177+
tail -40 "$TMP_DIR/langgraph.log" >&2
178+
exit 4
179+
fi
180+
local leftover
181+
leftover=$(curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/state" \
182+
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(any(it.get("value") is not None for t in d.get("tasks",[]) for it in t.get("interrupts",[])))')
183+
if [[ "$leftover" == "True" ]]; then
184+
echo "[record][$label] resume left a pending interrupt in thread state" >&2
185+
tail -40 "$TMP_DIR/langgraph.log" >&2
186+
exit 4
187+
fi
188+
echo "[record][$label] resume run succeeded"
189+
}
190+
191+
drive_flow "Book me on UA123." "confirm" "confirm"
192+
drive_flow "Book me on AA404." "cancel" "cancel"
193+
194+
# Give aimock a moment to flush per-request fixture files.
195+
sleep 2
196+
197+
RECORDED_DIR="$RECORD_DIR/recorded"
198+
if [[ ! -d "$RECORDED_DIR" ]]; then
199+
echo "[record] no recorded fixtures dir at $RECORDED_DIR" >&2
200+
tail -40 "$TMP_DIR/aimock.log" >&2
201+
exit 5
202+
fi
203+
RECORDED_FILES=$(find "$RECORDED_DIR" -name "*.json" | wc -l | tr -d ' ')
204+
echo "[record] $RECORDED_FILES recorded fixture files in $RECORDED_DIR"
205+
206+
python3 - <<PYEOF
207+
import json, os, glob
208+
recorded = sorted(glob.glob(os.path.join(r"$RECORDED_DIR", "*.json")))
209+
merged = {"fixtures": []}
210+
for f in recorded:
211+
with open(f) as fh:
212+
data = json.load(fh)
213+
merged["fixtures"].extend(data.get("fixtures", []))
214+
with open(r"$FIXTURE_OUT", "w") as fh:
215+
json.dump(merged, fh, indent=2)
216+
print(f"[record] merged {len(merged['fixtures'])} entries into $FIXTURE_OUT")
217+
PYEOF
218+
219+
rm -rf "$RECORD_DIR"
220+
221+
if [[ ! -s "$FIXTURE_OUT" ]]; then
222+
echo "[record] fixture file is missing or empty: $FIXTURE_OUT" >&2
223+
exit 6
224+
fi
225+
echo "[record] fixture written: $FIXTURE_OUT ($(wc -c < "$FIXTURE_OUT") bytes)"
226+
ENTRY_COUNT=$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(len(d.get("fixtures",[])))' "$FIXTURE_OUT")
227+
echo "[record] $ENTRY_COUNT fixture entries"

docs/superpowers/context/2026-07-07-client-tools-continuation-handoff.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ M0 concretely (see spec §11 for the full checklist):
120120
Prefer keeping M0 test scaffolding internal to spec/test files.
121121
- Patch-only releases: never bump @threadplane/* to 0.1.0; increment patch.
122122
- Deterministic, local tests only (no network, no live LLM in unit tests).
123-
- NEVER reference external frameworks (hashbrown / a React agent UI framework / chatgpt / claude)
123+
- NEVER reference external frameworks (hashbrown / a competing React framework / chatgpt / claude)
124124
in code, comments, or commit/PR text. Spec/plan markdown is the only sanctioned
125125
place. The architecture is independently arrived at.
126126
- Do not relitigate the resolved decisions above.

docs/superpowers/plans/2026-04-21-chat-runtime-decoupling-phase-1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ Insert after the existing introduction paragraph:
429429
Chat primitives consume a runtime-neutral `ChatAgent` contract. Two adapters ship today:
430430

431431
- **`@cacheplane/langgraph`** — for LangGraph / LangGraph Platform backends.
432-
- **`@cacheplane/ag-ui`** — for any AG-UI-compatible backend (LangGraph, CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, a React agent UI framework runtime).
432+
- **`@cacheplane/ag-ui`** — for any AG-UI-compatible backend (LangGraph, CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, a competing React framework runtime).
433433

434434
Custom backends can implement `ChatAgent` directly with no library dependency.
435435

docs/superpowers/plans/2026-04-30-ag-ui-fake-agent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ gh pr create --title "feat(ag-ui): FakeAgent for offline cockpit demo" --body "$
383383
- Wires the AG-UI cockpit demo to use the fake — \`nx serve cockpit-ag-ui-streaming-angular\` now shows a working streaming chat with no backend.
384384
385385
## Motivation
386-
The dojo at \`dojo.ag-ui.com\` speaks a React agent UI framework's runtime protocol, not raw AG-UI HTTP — so an HttpAgent can't directly connect. Until a public AG-UI-native endpoint exists (or we ship a local backend), the in-process fake unblocks the demo.
386+
The dojo at \`dojo.ag-ui.com\` speaks the competing React framework's runtime protocol, not raw AG-UI HTTP — so an HttpAgent can't directly connect. Until a public AG-UI-native endpoint exists (or we ship a local backend), the in-process fake unblocks the demo.
387387
388388
## Real-backend swap
389389
One line in \`app.config.ts\`:

docs/superpowers/plans/2026-04-30-license-migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ gh pr create --title "feat: relicense to MIT (selective; minting-service stays p
362362
- \`apps/minting-service/\` stays on its existing proprietary terms.
363363
364364
## Motivation
365-
Aligns with industry SDK norms (a React agent UI framework, LangChain, Vercel AI SDK all permissive). Removes adoption friction; commercial revenue path shifts to enterprise add-ons + potential managed service.
365+
Aligns with industry SDK norms (a competing React framework, LangChain, Vercel AI SDK all permissive). Removes adoption friction; commercial revenue path shifts to enterprise add-ons + potential managed service.
366366
367367
## Test Plan
368368
- [x] All 16 libs + 1 package + cockpit demos lint/test/build pass

docs/superpowers/plans/2026-05-02-chat-pipeline-redesign.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2321,7 +2321,7 @@ git push origin chat-v0.0.15
23212321
## Self-review notes
23222322

23232323
- **Spec coverage**: All three active phases mapped to task sets. Phase 4 explicitly deferred per spec.
2324-
- **Constraint enforcement**: No a React agent UI framework / inspirational-library references in any task body, code, or commit message.
2324+
- **Constraint enforcement**: No a competing React framework / inspirational-library references in any task body, code, or commit message.
23252325
- **Type consistency**: `ChatWelcomeComponent`, `ChatWelcomeSuggestionComponent`, `classifiers: Map<string, ContentClassifier>`, `showWelcome: Signal<boolean>`, `welcomeDisabled: InputSignal<boolean>` consistent across tasks.
23262326
- **Test before code**: Every new module follows write-test → run-fail → implement → run-pass.
23272327
- **Exact commands**: Every step that runs a tool gives the exact command + expected outcome.

docs/superpowers/plans/2026-05-03-chat-reasoning-and-tool-call-templates.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
44
5-
**Goal:** Surface model reasoning content as a first-class collapsible pill above the assistant response, and turn tool-call rendering into a a React agent UI framework-style extension surface via a `chatToolCallTemplate` directive while keeping a polished default that auto-collapses completed cards and groups sequential same-name calls.
5+
**Goal:** Surface model reasoning content as a first-class collapsible pill above the assistant response, and turn tool-call rendering into a competing-React-framework-style extension surface via a `chatToolCallTemplate` directive while keeping a polished default that auto-collapses completed cards and groups sequential same-name calls.
66

77
**Architecture:** One new primitive (`<chat-reasoning>`), one new directive (`chatToolCallTemplate`), augmentations to two existing primitives (`<chat-tool-calls>`, `<chat-tool-call-card>`), two new optional `Message` fields (`reasoning`, `reasoningDurationMs`) populated by both adapters from provider-agnostic sources (LangGraph complex-content reasoning blocks and AG-UI `REASONING_MESSAGE_*` events). Single-PR shipment across `@ngaf/chat`, `@ngaf/langgraph`, and `@ngaf/ag-ui`.
88

99
**Tech Stack:** Angular 21 standalone + signals + OnPush; vitest for library tests; nx monorepo build (`npx nx build <project>`, `npx nx test <project>`); LangGraph SDK + AG-UI client for adapter event streams; marked + sanitized innerHTML for markdown rendering; @ngaf/chat as the shared contract surface between adapters.
1010

1111
**Reference spec:** `docs/superpowers/specs/2026-05-03-chat-reasoning-and-tool-call-templates-design.md`
1212

13-
**Hard constraint:** Never reference any chat-UI library this work was inspired by — no `a React agent UI framework` / `chatgpt` / `chatbot-kit` / similar references in code, comments, commits, PR bodies, or docs. Aesthetic and extensibility patterns are independently arrived at.
13+
**Hard constraint:** Never reference any chat-UI library this work was inspired by — no `a competing React framework` / `chatgpt` / `chatbot-kit` / similar references in code, comments, commits, PR bodies, or docs. Aesthetic and extensibility patterns are independently arrived at.
1414

1515
---
1616

@@ -3626,4 +3626,4 @@ git push origin chat-v0.0.19 langgraph-v0.0.11 ag-ui-v0.0.3
36263626

36273627
- **Type consistency:** `Message.reasoning?: string`, `Message.reasoningDurationMs?: number` defined in Task 1.1 and consumed everywhere. `ToolCallStatus` re-used (not redefined). `ChatToolCallTemplateContext` defined in Task 3.2 and referenced in Task 10.2. `summarizeGroup` exported from `group-summary.ts` (Task 4.2) and consumed in `chat-tool-calls.component.ts` (Task 4.3) and tested in Task 4.1.
36283628

3629-
- **Hard constraint adherence:** plan body, code samples, commit messages, and PR body contain no references to `a React agent UI framework`, `chatgpt`, `chatbot-kit`, or similar.
3629+
- **Hard constraint adherence:** plan body, code samples, commit messages, and PR body contain no references to `a competing React framework`, `chatgpt`, `chatbot-kit`, or similar.

0 commit comments

Comments
 (0)