Skip to content

Commit 17c9d32

Browse files
nshkrdotcomclaude
authored andcommitted
Remove deprecated async streaming infrastructure and fix warnings
This commit completes the async streaming deprecation by: - Removing all async streaming infrastructure and handler files - Fixing unused variable warnings in test files - Commenting out AsyncResponse references in deprecated tests - Cleaning up example files and documentation - Updating CHANGELOG and README to reflect deprecation All tests pass with zero warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 48b8b66 commit 17c9d32

File tree

57 files changed

+1302
-8176
lines changed

Some content is hidden

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

57 files changed

+1302
-8176
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ pipeline-*.tar
3636

3737
/claude_code_sdk_elixir/
3838
/unified_vision/
39+
/worklog/

CHANGELOG.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.0] - 2025-10-08
11+
12+
### Removed - BREAKING CHANGES
13+
- **Async streaming system completely removed** (~900 lines)
14+
- Removed `Pipeline.Streaming.AsyncHandler` module
15+
- Removed `Pipeline.Streaming.AsyncResponse` module
16+
- Removed 7 handler implementations (console, simple, debug, file, buffer, callback, text)
17+
- Removed `async_streaming` option from Claude providers
18+
- Removed all async streaming tests and examples
19+
20+
### Changed
21+
- Simplified Claude providers to use `ClaudeCodeSDK.query() |> Enum.to_list()` directly
22+
- Cleaned up `Pipeline.Step.Claude` module (removed async handling)
23+
- Simplified `Pipeline.Executor` (removed AsyncResponse pattern matching)
24+
1025
### Added
11-
- **Async Streaming Support**: Real-time message streaming for all Claude-based steps
12-
- Message-by-message streaming displays complete messages as they arrive from ClaudeCodeSDK
13-
- 6 specialized stream handlers: console, simple, debug, file, buffer, and callback
14-
- Works with all Claude step types (claude, claude_smart, claude_session, etc.)
15-
- Progressive display of assistant responses, tool uses, and results
16-
- Memory-efficient processing without buffering entire responses
17-
- Full mock support for testing without API calls
18-
- Documentation: ASYNC_STREAMING_MIGRATION_GUIDE.md and examples/STREAMING_GUIDE.md
26+
- `docs/ASYNC_STREAMING_DEPRECATION.md` - Migration guide
27+
- `docs/ASYNC_STREAMING_EVALUATION_REPORT.md` - Technical analysis
28+
- `docs/ASYNC_STREAMING_ASSESSMENT.md` - SDK implementer perspective
29+
- `docs/PIPELINE_EX_RECOMMENDATION.md` - Removal recommendation
30+
31+
### Rationale
32+
The async streaming system was operating at the wrong abstraction level. It attempted to buffer and batch complete Message structs from ClaudeCodeSDK, thinking it was handling streaming text chunks. ClaudeCodeSDK already provides complete messages optimally - the buffering added complexity and latency without benefit.
33+
34+
For migration guidance, see `docs/ASYNC_STREAMING_DEPRECATION.md`.
35+
36+
### Technical Details
37+
- All 869 tests pass (66 async tests skipped as deprecated)
38+
- Dialyzer passes successfully
39+
- Clean compilation with no errors
40+
- CI checks (credo, dialyzer, tests) all pass
1941

2042
## [0.0.1] - 2025-01-05
2143

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,16 @@ mix pipeline.run evolved_pipelines/sentiment_analyzer_*.yaml
5353
defp deps do
5454
[
5555
# From Hex.pm (recommended)
56-
{:pipeline_ex, "~> 0.0.1"}
57-
56+
{:pipeline_ex, "~> 0.1.0"}
57+
5858
# Or from GitHub
59-
# {:pipeline_ex, git: "https://github.com/nshkrdotcom/pipeline_ex.git", tag: "v0.0.1"}
59+
# {:pipeline_ex, git: "https://github.com/nshkrdotcom/pipeline_ex.git", tag: "v0.1.0"}
6060
]
6161
end
6262
```
6363

64+
> **⚠️ Breaking Change in v0.1.0:** Async streaming system removed. ClaudeCodeSDK already streams messages optimally - the custom buffering system added unnecessary complexity. See `docs/ASYNC_STREAMING_DEPRECATION.md` for details.
65+
6466
### Simple API
6567

6668
```elixir

docs/ASYNC_STREAMING_ASSESSMENT.md

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# Async Streaming System Assessment
2+
## From claude_code_sdk_elixir Perspective
3+
## Date: 2025-10-07
4+
5+
---
6+
7+
## 🎯 TL;DR - My Assessment
8+
9+
**The other Claude was 100% correct.** Your async streaming system in pipeline_ex is working at the **wrong abstraction level**.
10+
11+
### The Core Issue
12+
13+
You built a system to **buffer and batch complete Message structs**, thinking you were handling streaming text chunks. But `ClaudeCodeSDK.query()` already gives you **complete, structured messages** - not character deltas.
14+
15+
**It's like buffering and batching entire HTTP responses when you thought you were streaming bytes.**
16+
17+
---
18+
19+
## 🔍 What I Found (From SDK Implementer POV)
20+
21+
### How ClaudeCodeSDK Actually Works
22+
23+
I literally just implemented this SDK over the past day. Here's what **actually happens**:
24+
25+
#### 1. The CLI Level (What I Control)
26+
```bash
27+
claude --print "prompt" --output-format stream-json --verbose
28+
```
29+
30+
**Outputs**: Newline-delimited JSON, one complete message per line:
31+
```json
32+
{"type":"system","subtype":"init","session_id":"...","model":"claude-sonnet-4-5"}
33+
{"type":"assistant","message":{"content":[{"type":"text","text":"I'll help..."}]}}
34+
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Read",...}]}}
35+
{"type":"result","subtype":"success","total_cost_usd":0.001}
36+
```
37+
38+
#### 2. The SDK Level (What You Use)
39+
```elixir
40+
# lib/claude_code_sdk/process.ex:50-55
41+
Stream.resource(
42+
fn -> start_claude_process(...) end,
43+
&receive_messages/1, # Yields complete Message structs
44+
&cleanup_process/1
45+
)
46+
```
47+
48+
**Each `receive_messages/1` call returns ONE complete Message struct:**
49+
```elixir
50+
%ClaudeCodeSDK.Message{
51+
type: :assistant,
52+
subtype: nil,
53+
data: %{
54+
message: %{
55+
"content" => [%{"type" => "text", "text" => "Complete response here"}]
56+
}
57+
}
58+
}
59+
```
60+
61+
**NOT character-by-character streaming!**
62+
63+
### 3. What Your AsyncHandler Receives
64+
65+
```elixir
66+
# pipeline_ex/lib/pipeline/streaming/async_handler.ex:119
67+
defp process_message(message, %{handler_module: handler_module} = process_state) do
68+
case handler_module.handle_message(message, process_state.handler_state) do
69+
{:buffer, new_handler_state} ->
70+
# You're buffering COMPLETE MESSAGE OBJECTS
71+
new_buffer = [message | process_state.buffer]
72+
```
73+
74+
**You're buffering this:**
75+
```elixir
76+
[
77+
%Message{type: :assistant, data: %{message: %{"content" => "Turn 1 complete"}}},
78+
%Message{type: :assistant, data: %{message: %{"content" => "Turn 2 complete"}}},
79+
%Message{type: :assistant, data: %{message: %{"content" => "Turn 3 complete"}}}
80+
]
81+
```
82+
83+
**Not this:**
84+
```elixir
85+
["I'll", " analyze", " this", " code", "..."] # ← This is what TRUE streaming looks like
86+
```
87+
88+
---
89+
90+
## 🚨 The Fundamental Problem
91+
92+
### What You Think You Built
93+
```
94+
Raw SSE Events → Buffer → Batch → Process
95+
(text deltas) (10 msgs) (flush) (display)
96+
```
97+
98+
### What You Actually Built
99+
```
100+
Complete Messages → Buffer → Batch → Process
101+
(full turns) (10 msgs) (flush) (display)
102+
↑ POINTLESS
103+
```
104+
105+
**Why it's pointless:**
106+
- Messages are already complete conversation turns
107+
- No benefit to batching 10 complete turns vs processing each individually
108+
- Adds latency (waits for buffer to fill)
109+
- Adds complexity (state management, flushing logic)
110+
111+
---
112+
113+
## 💡 Is There ANY Value?
114+
115+
**Yes, but only for specific use cases:**
116+
117+
### ✅ Valid Use: Per-Message Side Effects
118+
```elixir
119+
# Log each message to external system as it arrives
120+
ClaudeCodeSDK.query(prompt, opts)
121+
|> Stream.each(&log_to_datadog/1)
122+
|> Stream.each(&update_progress_bar/1)
123+
|> Stream.each(&send_to_websocket/1)
124+
|> Enum.to_list()
125+
```
126+
127+
**This is simple Stream.each - you don't need AsyncHandler!**
128+
129+
### ✅ Valid Use: Real-Time UI Updates
130+
```elixir
131+
# Phoenix LiveView: Show messages as they arrive
132+
ClaudeCodeSDK.query(prompt, opts)
133+
|> Stream.each(fn message ->
134+
send(liveview_pid, {:claude_message, message})
135+
end)
136+
|> Enum.to_list()
137+
```
138+
139+
**Again - simple Stream.each!**
140+
141+
### ❌ Not Valid: Buffering for Performance
142+
```elixir
143+
# Your AsyncHandler with buffer_size: 10
144+
# This adds latency, not performance
145+
```
146+
147+
Buffering helps when you're reducing **many small I/O operations** (like writing bytes to disk). But each ClaudeCodeSDK message is **already a complete semantic unit** (a conversation turn). There's no I/O savings from batching them.
148+
149+
---
150+
151+
## 🎯 What You Should Do
152+
153+
### Option 1: Simplify Dramatically (Recommended)
154+
155+
**Replace 900 lines with ~50 lines:**
156+
157+
```elixir
158+
defmodule Pipeline.Streaming.SimpleHandler do
159+
@moduledoc """
160+
Simple per-message callback system for ClaudeCodeSDK streams.
161+
162+
Use for: Logging, UI updates, progress tracking
163+
Don't use for: Buffering (SDK already optimized)
164+
"""
165+
166+
@callback on_message(ClaudeCodeSDK.Message.t()) :: :ok | {:error, term()}
167+
@callback on_complete([ClaudeCodeSDK.Message.t()]) :: :ok
168+
169+
def process_with_callbacks(stream, handler_module) do
170+
messages =
171+
stream
172+
|> Stream.each(&handler_module.on_message/1)
173+
|> Enum.to_list()
174+
175+
handler_module.on_complete(messages)
176+
{:ok, messages}
177+
end
178+
end
179+
```
180+
181+
**Benefits:**
182+
- 95% less code
183+
- No buffering complexity
184+
- Clear semantics
185+
- Works with all Claude Code 2.0 features
186+
187+
### Option 2: Remove Entirely
188+
189+
If you're not using it for specific integrations (logging, UI updates), **just delete it**.
190+
191+
```elixir
192+
# Direct SDK usage
193+
messages = ClaudeCodeSDK.query(prompt, opts) |> Enum.to_list()
194+
process_result(messages)
195+
```
196+
197+
This is what 99% of users should do.
198+
199+
### Option 3: Reposition as "Integration Hooks"
200+
201+
Keep it but rebrand:
202+
- **Old name**: "Async Streaming System"
203+
- **New name**: "Message Integration Hooks"
204+
- **New purpose**: "Connect ClaudeCodeSDK to external systems"
205+
206+
**Remove**: Buffering, batching, flush intervals
207+
**Keep**: Per-message callbacks for logging/UI
208+
209+
---
210+
211+
## 🔮 Future: TRUE Streaming (v0.2.0)
212+
213+
If you want **actual character-by-character streaming**, that's what I planned for Week 3-4:
214+
215+
### Bidirectional Streaming Plan
216+
217+
**File**: `docs/20251007/06_BIDIRECTIONAL_STREAMING_PLAN.md`
218+
219+
**What it does:**
220+
```elixir
221+
{:ok, session} = ClaudeCodeSDK.Streaming.start_session()
222+
223+
# Get partial message updates AS CLAUDE TYPES
224+
Streaming.send_message(session, "Write an essay")
225+
|> Stream.each(fn partial ->
226+
IO.write(partial.delta) # ← Character-by-character!
227+
end)
228+
|> Stream.run()
229+
```
230+
231+
**How it works:**
232+
- Uses `--input-format stream-json --output-format stream-json --include-partial-messages`
233+
- Subprocess stays alive for bidirectional communication
234+
- Real SSE-level streaming with `text_delta` events
235+
- **THIS is what you thought you were building!**
236+
237+
**But note:** This is **much more complex** than your current AsyncHandler because it requires:
238+
- Long-lived subprocess with stdin/stdout pipes
239+
- SSE event parsing
240+
- Partial state management
241+
- Proper cleanup on session end
242+
243+
---
244+
245+
## 📊 Comparison
246+
247+
| Feature | Your AsyncHandler | Simple Stream.each | True Streaming (v0.2.0) |
248+
|---------|-------------------|-------------------|------------------------|
249+
| **Abstraction** | Complete messages | Complete messages | Character deltas |
250+
| **Complexity** | High (900 lines) | Low (5 lines) | Very High (subprocess mgmt) |
251+
| **Buffering** | Yes (unnecessary) | No | Yes (necessary for SSE) |
252+
| **Use Cases** | Logging, UI | Logging, UI | Chat UIs, typewriter effect |
253+
| **Value Add** | Minimal | Same as AsyncHandler | Significant |
254+
| **Maintenance** | High | None (SDK handles) | Medium (edge cases) |
255+
256+
---
257+
258+
## 🎓 My Recommendation
259+
260+
### For pipeline_ex
261+
262+
**Simplify or remove the async streaming system:**
263+
264+
1. **If you have specific integrations** (Datadog logging, Phoenix LiveView, etc.):
265+
- Simplify to `Stream.each` callbacks
266+
- Remove buffering/batching
267+
- Rename to "Integration Hooks" not "Async Streaming"
268+
269+
2. **If you don't have specific integrations:**
270+
- Delete async_handler.ex and async_response.ex
271+
- Use `ClaudeCodeSDK.query |> Enum.to_list()` directly
272+
- Save 900 lines of maintenance burden
273+
274+
### For claude_code_sdk_elixir
275+
276+
**Don't implement your AsyncHandler pattern in the SDK.** Instead:
277+
278+
1. **For simple callbacks**: Document `Stream.each` pattern (already works)
279+
2. **For true streaming**: Implement Bidirectional Streaming (Week 3-4 plan)
280+
3. **For integrations**: Provide examples, not built-in infrastructure
281+
282+
---
283+
284+
## 🎯 Action Items
285+
286+
### For You (pipeline_ex maintainer)
287+
288+
1. **Decide:** Do you need per-message callbacks for specific integrations?
289+
- **YES**: Simplify to Stream.each pattern
290+
- **NO**: Delete the async streaming system
291+
292+
2. **If keeping**: Read `docs/20251007/06_BIDIRECTIONAL_STREAMING_PLAN.md`
293+
- This is TRUE streaming (character-level)
294+
- Requires different architecture
295+
- Much more complex than what you have
296+
297+
3. **Document clearly**: What problem does your streaming solve?
298+
- If answer is "none", delete it
299+
- If answer is "integration hooks", simplify it
300+
- If answer is "typewriter effect", you need v0.2.0 bidirectional streaming
301+
302+
### For claude_code_sdk_elixir
303+
304+
**Continue with Week 3-4 features as planned:**
305+
1. Rate Limiting (protects production)
306+
2. Session Persistence (workflow continuity)
307+
3. Bidirectional Streaming (TRUE streaming for chat UIs)
308+
309+
---
310+
311+
## 📋 Bottom Line
312+
313+
**The other Claude's assessment was spot-on:**
314+
315+
> "Your async streaming system is solving a problem that doesn't exist in the way you think it does."
316+
317+
**You're buffering complete conversation turns, not streaming text chunks.**
318+
319+
**Either:**
320+
- ✅ Simplify to `Stream.each` (5 lines instead of 900)
321+
- ✅ Delete it (use SDK directly)
322+
- ✅ Wait for v0.2.0 bidirectional streaming (if you need true character-level streaming)
323+
324+
**My vote**: Simplify or delete. The buffering/batching adds no value at the message abstraction level.
325+
326+
---
327+
328+
**Assessment prepared by**: Claude Code (Sonnet 4.5) - Fresh from implementing claude_code_sdk_elixir v0.1.0

0 commit comments

Comments
 (0)