Skip to content

Commit 2a1c563

Browse files
bloveclaude
andauthored
fix(chat+langgraph): streaming complex-content + GFM markdown + model picker (chat 0.0.18, langgraph 0.0.10) (#191)
* fix(langgraph): stream complex-content correctly + dedupe assistant bubbles OpenAI's gpt-5/o-series Responses API streams `BaseMessage.content` as arrays of typed blocks (`[{type:'text',text:'…',index:n}, …]`) rather than accumulated strings, and emits two parallel views of the same assistant message: per-event `AIMessageChunk`s on `messages-tuple` and the canonical `ai` on `values`-sync. The bridge wasn't equipped for either — chunks JSON-dumped into the bubble and the two paths each spawned their own bubble that never collapsed. Bridge fixes (`stream-manager.bridge.ts`): - `extractText` walks complex-content arrays and pulls visible text blocks (`text` / `output_text`), skipping reasoning / tool_use / image blocks. - `accumulateContent` merges incoming-chunk content into the prior slot's accumulated text. Handles the three cases: incoming is a strict superset (final-id swap), existing is a strict superset (chunk arrives after canonical), or pure delta append. Always returns string so downstream `findContentMatch` can prefix-compare cleanly without `JSON.stringify`-ing the array. - `normalizeMessageType` collapses `AIMessage` / `AIMessageChunk` / `ai` / `assistant` to `ai` so `findContentMatch` and `sameRoleAndContent` correctly match across the values-sync and messages-tuple paths. - `mergeMessages` gains an AIMessageChunk fallback: when an AIMessageChunk arrives without an id-match or content-prefix match, accumulate into the trailing AI message. The OpenAI Responses API emits per-chunk *event* ids, not message ids, so consecutive chunks would otherwise each create a fresh bubble. - Empty-content AI placeholders are dropped from `state.messages` before the values-sync merge — keeping them creates a phantom slot that competes with the chunk-streamed AIMessageChunk. - `collapseAdjacentAi` post-pass collapses adjacent AI messages where one's text is a prefix/equal of the other, keeping the older slot's id for stable track-by-id. Also dropped an obsolete hand-rolled rawMessages throttle in `agent.fn.ts` — `messages$` already emits at the bridge boundary and extra signal-side throttling collapsed visible token streaming. Bumps @ngaf/langgraph 0.0.9 → 0.0.10. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): render markdown with full GFM coverage + visible CommonMark styles Two related rendering bugs converged into a chat that showed raw JSON arrays and then, after fixing that, plain unstyled paragraphs with no heading sizes, bullets, or table borders. `messageContent` shared util: - Was JSON-stringifying complex-content arrays, dumping `[{"type":"text",…}]` into the chat bubble for OpenAI gpt-5 / o-series output. Now extracts visible `text` / `output_text` blocks the same way the langgraph adapter does and joins them; everything else (reasoning, tool_use, images) is skipped. `chat-streaming-md` component: - Switched to `ViewEncapsulation.None`. The component renders markdown by assigning sanitized HTML to `innerHTML`, so the resulting `<ul>`, `<p>`, `<table>`, etc. nodes never carry the `_ngcontent-…` attribute that emulated encapsulation requires. Without this the exported `CHAT_MARKDOWN_STYLES` were silently skipped for every selector below `:host`. - Wired `CHAT_MARKDOWN_STYLES` into the component (it was exported but never applied anywhere). `CHAT_MARKDOWN_STYLES`: - Re-scoped from `:host` to `chat-streaming-md` element selectors so the rules stay locally meaningful under `ViewEncapsulation.None`. - Expanded coverage to the full CommonMark + GFM surface: heading hierarchy (h1–h6 with size scale), `strong` / `b`, `em` / `i`, `del` / `s`, `mark`, `sub` / `sup`, `a` (with hover), bullet / ordered / nested lists with visible `disc` / `circle` / `decimal` markers, GFM task-list checkboxes, inline `code` / fenced `pre`, `blockquote`, `hr`, GFM `table` (bordered, header background), and `img` (max-width). `marked` options: - Enabled `gfm: true, breaks: true` so single newlines render as `<br>` (matching common chat UX) and tables / strikethrough / task lists / autolinks are honored. Bumps @ngaf/chat 0.0.17 → 0.0.18. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chat): first-class model picker on <chat> + restyle welcome suggestions as pills Two UX improvements driven by smoke-testing the chat-select primitive in real apps. `<chat>` composition: - New `[modelOptions]`, `[(selectedModel)]`, `[modelPickerPlaceholder]` inputs that, when populated, render a `<chat-select chatInputModelSelect>` inside the chat-input pill on both the welcome screen and the conversation footer. Consumers no longer have to project the slot themselves for the common "model picker" use case — they just pass options + a model signal. - Slot projection still works in conversation mode for any consumer that needs custom chat-input children (an inner `ngProjectAs` bridges the outer `[chatInputModelSelect]` content through). Welcome suggestions: - Restyled from full-width stacked rows separated by dividers to centered floating pills (`border-radius: 9999px`, surface background, equal gap). Matches the chat-input pill aesthetic. - Container becomes `flex-wrap` + `justify-content: center` + `gap: 8px` so suggestions reflow naturally on narrow viewports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump cockpit demos + superpowers docs from gpt-4o-mini to gpt-5-mini Default model in every demo graph and prose example is now gpt-5-mini. Reasoning models (gpt-5/o-series) stream visibly out of the box at `reasoning.effort='minimal'`, which the langgraph adapter sets by default — no developer-facing config needed. Older non-reasoning gpt-4o-mini references in graphs and docs were stale. No code path changed; pure model-name swap across cockpit examples and docs/superpowers plan / spec files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(spec): chat reasoning + tool-call templates (B2 design) Design spec for the next chat phase: surface model reasoning content as a first-class collapsible "Thinking… / Thought for Ns" pill above the assistant response, and turn tool-call rendering into a first- class extension surface via a chatToolCallTemplate directive while keeping a polished default that auto-collapses completed cards and groups sequential same-name calls. Lands one new primitive (<chat-reasoning>), one new directive (chatToolCallTemplate, including a "*" wildcard catch-all), augments two existing primitives (<chat-tool-calls>, <chat-tool-call-card>), and adds two new optional fields (Message.reasoning, Message.reasoningDurationMs) populated by both adapters from provider-agnostic sources: LangGraph complex-content reasoning blocks and AG-UI REASONING_MESSAGE_* events. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(chat): messageContent now extracts text from complex-content arrays The prior assertion checked that JSON-stringified output contained the literal string 'text' (the JSON key from {type:'text',...}). After the 0.0.18 fix to extract visible text instead of dumping JSON, the assertion is wrong by construction. Replaced with two assertions: single-block text extraction, and multi-block concatenation that skips reasoning blocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): chat reasoning + tool-call templates implementation plan Eleven phases, ~70 tasks. TDD throughout for new primitives + the reasoning conformance fixture. Each task is self-contained with full context, exact paths, complete code, and explicit verification commands. Subagent-friendly per superpowers:subagent-driven-development. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ed2825d commit 2a1c563

42 files changed

Lines changed: 4484 additions & 163 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.

cockpit/chat/generative-ui/python/src/graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
_PROMPT = (Path(__file__).parent.parent / "prompts" / "dashboard.md").read_text()
2121

22-
_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, streaming=True)
22+
_llm = ChatOpenAI(model="gpt-5-mini", temperature=0, streaming=True)
2323
_llm_with_tools = _llm.bind_tools(ALL_TOOLS)
2424

2525

cockpit/deep-agents/filesystem/python/docs/guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def write_file(path: str, content: str) -> str:
139139
return f"Successfully wrote {len(content)} bytes to {path}"
140140

141141
# Bind tools to the LLM
142-
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([read_file, write_file])
142+
llm = ChatOpenAI(model="gpt-5-mini").bind_tools([read_file, write_file])
143143
```
144144

145145
The agent node invokes the LLM, which may emit tool calls. A conditional edge routes to the `ToolNode` when tool calls are present, then loops back to the agent. The frontend sees each tool call in `stream.messages()`.

cockpit/deep-agents/filesystem/python/src/graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class FilesystemState(TypedDict):
3535

3636
def build_filesystem_graph():
3737
tools = [read_file, write_file]
38-
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True).bind_tools(tools)
38+
llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(tools)
3939

4040
async def agent(state: FilesystemState) -> dict:
4141
"""Run the agent — may emit tool calls."""

cockpit/deep-agents/memory/python/src/graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class MemoryState(TypedDict):
2424

2525

2626
def build_memory_graph():
27-
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
27+
llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
2828

2929
async def generate(state: MemoryState) -> dict:
3030
"""Generate a response using remembered facts in the system prompt."""

cockpit/deep-agents/planning/python/src/graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class PlanningState(TypedDict):
2727

2828

2929
def build_planning_graph():
30-
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
30+
llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
3131

3232
async def create_plan(state: PlanningState) -> dict:
3333
"""Decompose the task into ordered steps."""

cockpit/deep-agents/sandboxes/python/docs/guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def run_code(code: str) -> str:
153153
"exit_status": 0,
154154
})
155155

156-
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([run_code])
156+
llm = ChatOpenAI(model="gpt-5-mini").bind_tools([run_code])
157157
tool_node = ToolNode([run_code])
158158
```
159159

cockpit/deep-agents/sandboxes/python/src/graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def run_code(code: str) -> str:
7171

7272
def build_sandboxes_graph():
7373
tools = [run_code]
74-
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True).bind_tools(tools)
74+
llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(tools)
7575
tool_node = ToolNode(tools)
7676

7777
async def agent(state: SandboxesState) -> dict:

cockpit/deep-agents/skills/python/docs/guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def summarize(text: str) -> str:
150150
return sentences[0] + "." if sentences else "No content."
151151

152152
# Bind all tools to the LLM
153-
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([calculator, word_count, summarize])
153+
llm = ChatOpenAI(model="gpt-5-mini").bind_tools([calculator, word_count, summarize])
154154
```
155155

156156
The agent selects which skill to call based on the user's request. `ToolNode` dispatches the call and returns the result as a `ToolMessage`.

cockpit/deep-agents/skills/python/src/graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def summarize(text: str) -> str:
6565

6666
def build_skills_graph():
6767
tools = [calculator, word_count, summarize]
68-
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True).bind_tools(tools)
68+
llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(tools)
6969
tool_node = ToolNode(tools)
7070

7171
async def agent(state: SkillsState) -> dict:

cockpit/deep-agents/subagents/python/src/graph.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ class SubagentsState(TypedDict):
2323

2424

2525
def build_subagents_graph():
26-
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
26+
llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
2727

2828
@tool
2929
async def research_agent(topic: str) -> str:
3030
"""Spawn a research subagent to gather information on a topic."""
31-
research_llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
31+
research_llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
3232
response = await research_llm.ainvoke([
3333
SystemMessage(content="You are a research specialist. Provide concise, factual information."),
3434
{"role": "human", "content": f"Research this topic and provide key facts: {topic}"},
@@ -38,7 +38,7 @@ async def research_agent(topic: str) -> str:
3838
@tool
3939
async def analysis_agent(content: str) -> str:
4040
"""Spawn an analysis subagent to analyze and synthesize information."""
41-
analysis_llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
41+
analysis_llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
4242
response = await analysis_llm.ainvoke([
4343
SystemMessage(content="You are an analysis specialist. Identify patterns, draw insights, and synthesize information clearly."),
4444
{"role": "human", "content": f"Analyze this content and provide key insights: {content}"},
@@ -48,7 +48,7 @@ async def analysis_agent(content: str) -> str:
4848
@tool
4949
async def summary_agent(findings: str) -> str:
5050
"""Spawn a summary subagent to produce a final coherent response."""
51-
summary_llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
51+
summary_llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
5252
response = await summary_llm.ainvoke([
5353
SystemMessage(content="You are a summarization specialist. Produce clear, well-structured summaries."),
5454
{"role": "human", "content": f"Summarize these findings into a concise final answer: {findings}"},

0 commit comments

Comments
 (0)