codeforge-dev 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.devcontainer/.env +22 -0
  2. package/.devcontainer/CHANGELOG.md +197 -0
  3. package/.devcontainer/CLAUDE.md +117 -0
  4. package/.devcontainer/README.md +222 -0
  5. package/.devcontainer/config/main-system-prompt.md +502 -0
  6. package/.devcontainer/config/settings.json +47 -0
  7. package/.devcontainer/devcontainer.json +94 -0
  8. package/.devcontainer/features/README.md +113 -0
  9. package/.devcontainer/features/agent-browser/README.md +65 -0
  10. package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
  11. package/.devcontainer/features/agent-browser/install.sh +79 -0
  12. package/.devcontainer/features/ast-grep/README.md +24 -0
  13. package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
  14. package/.devcontainer/features/ast-grep/install.sh +51 -0
  15. package/.devcontainer/features/ccstatusline/README.md +296 -0
  16. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
  17. package/.devcontainer/features/ccstatusline/install.sh +290 -0
  18. package/.devcontainer/features/ccusage/README.md +205 -0
  19. package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
  20. package/.devcontainer/features/ccusage/install.sh +132 -0
  21. package/.devcontainer/features/claude-code/README.md +498 -0
  22. package/.devcontainer/features/claude-code/config/settings.json +36 -0
  23. package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
  24. package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
  25. package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
  26. package/.devcontainer/features/claude-code/install.sh +466 -0
  27. package/.devcontainer/features/claude-monitor/README.md +74 -0
  28. package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
  29. package/.devcontainer/features/claude-monitor/install.sh +99 -0
  30. package/.devcontainer/features/lsp-servers/README.md +85 -0
  31. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
  32. package/.devcontainer/features/lsp-servers/install.sh +116 -0
  33. package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
  34. package/.devcontainer/features/mcp-qdrant/README.md +474 -0
  35. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
  36. package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
  37. package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
  38. package/.devcontainer/features/mcp-reasoner/README.md +177 -0
  39. package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
  40. package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
  41. package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
  42. package/.devcontainer/features/notify-hook/README.md +86 -0
  43. package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
  44. package/.devcontainer/features/notify-hook/install.sh +38 -0
  45. package/.devcontainer/features/splitrail/README.md +140 -0
  46. package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
  47. package/.devcontainer/features/splitrail/install.sh +129 -0
  48. package/.devcontainer/features/tree-sitter/README.md +138 -0
  49. package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
  50. package/.devcontainer/features/tree-sitter/install.sh +173 -0
  51. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
  52. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
  53. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
  54. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
  55. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
  56. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
  57. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
  58. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
  60. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
  61. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
  62. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
  63. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
  64. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
  65. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
  66. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
  67. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
  68. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
  69. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
  70. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
  71. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
  72. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
  73. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
  74. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
  75. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
  76. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
  77. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
  78. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
  79. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
  80. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
  81. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
  82. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
  83. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
  84. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
  85. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
  86. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
  87. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
  88. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
  89. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
  90. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
  91. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
  92. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
  93. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
  94. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
  95. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
  96. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
  97. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
  98. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
  99. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
  100. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
  101. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
  102. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
  103. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
  104. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
  106. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
  107. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
  108. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
  109. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
  110. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
  111. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
  112. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
  113. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
  114. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
  115. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
  116. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
  117. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
  118. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
  119. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
  120. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
  121. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
  122. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
  123. package/.devcontainer/scripts/setup-aliases.sh +80 -0
  124. package/.devcontainer/scripts/setup-config.sh +28 -0
  125. package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
  126. package/.devcontainer/scripts/setup-plugins.sh +80 -0
  127. package/.devcontainer/scripts/setup.sh +58 -0
  128. package/LICENSE.txt +674 -0
  129. package/README.md +267 -0
  130. package/package.json +44 -0
  131. package/setup.js +83 -0
@@ -0,0 +1,318 @@
1
+ # Server-Sent Events and Streaming -- Deep Dive
2
+
3
+ ## 1. SSE Protocol Fundamentals
4
+
5
+ Server-Sent Events use a persistent HTTP connection with `Content-Type: text/event-stream`. The server writes UTF-8 text frames separated by double newlines. Each frame contains one or more fields:
6
+
7
+ ```
8
+ event: message
9
+ id: 42
10
+ retry: 5000
11
+ data: {"text": "hello"}
12
+
13
+ ```
14
+
15
+ | Field | Purpose | Default |
16
+ |-------|---------|---------|
17
+ | `data` | Event payload (required) | -- |
18
+ | `event` | Event type name | `"message"` |
19
+ | `id` | Last-event ID for reconnection | none |
20
+ | `retry` | Client reconnection interval (ms) | browser default (~3s) |
21
+
22
+ Multiple `data` lines in one frame are concatenated with newlines. A line starting with `:` is a comment -- browsers ignore it, but it serves as a keep-alive heartbeat.
23
+
24
+ ---
25
+
26
+ ## 2. sse-starlette API
27
+
28
+ `sse-starlette` wraps an async iterable into a compliant `EventSourceResponse`. Install with `pip install sse-starlette`.
29
+
30
+ ### Basic Configuration
31
+
32
+ ```python
33
+ from sse_starlette.sse import EventSourceResponse
34
+
35
+ @app.get("/events")
36
+ async def stream():
37
+ return EventSourceResponse(
38
+ content=event_generator(),
39
+ media_type="text/event-stream",
40
+ ping=30, # heartbeat interval in seconds
41
+ ping_message_factory=lambda: {"comment": "keep-alive"},
42
+ )
43
+ ```
44
+
45
+ ### Yielding Events
46
+
47
+ The generator can yield several formats:
48
+
49
+ ```python
50
+ async def event_generator():
51
+ # Dict with SSE fields
52
+ yield {"event": "status", "data": "connected", "id": "1"}
53
+
54
+ # Plain string -- becomes a data-only event
55
+ yield "simple message"
56
+
57
+ # Dict with just data
58
+ yield {"data": '{"count": 42}'}
59
+
60
+ # ServerSentEvent object for full control
61
+ from sse_starlette.sse import ServerSentEvent
62
+ yield ServerSentEvent(data="precise", event="custom", id="2", retry=10000)
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 3. Reconnection with Last-Event-ID
68
+
69
+ Browsers automatically reconnect when an SSE connection drops. They send the last received `id` in the `Last-Event-ID` header. Use this to resume the stream without replaying events the client already received:
70
+
71
+ ```python
72
+ from fastapi import Request
73
+
74
+ async def resumable_stream(request: Request):
75
+ last_id = request.headers.get("last-event-id")
76
+ cursor = int(last_id) + 1 if last_id else 0
77
+
78
+ async for event in fetch_events_from(cursor):
79
+ yield {
80
+ "event": "update",
81
+ "data": event.to_json(),
82
+ "id": str(event.sequence_id),
83
+ }
84
+ ```
85
+
86
+ ### Designing for Resumability
87
+
88
+ - Assign monotonically increasing IDs (database sequence, timestamp, or counter).
89
+ - Store events in a bounded buffer or persistent log so the server can replay from any recent ID.
90
+ - Set `retry` to control how quickly clients reconnect (default is ~3 seconds).
91
+ - Send an initial "snapshot" event on reconnection if the gap is too large to replay individual events.
92
+
93
+ ---
94
+
95
+ ## 4. Backpressure with Bounded Queues
96
+
97
+ When an event source produces faster than clients consume, unbounded buffering causes memory growth. Use `asyncio.Queue` with a maximum size to apply backpressure:
98
+
99
+ ```python
100
+ import asyncio
101
+
102
+ class EventBus:
103
+ def __init__(self):
104
+ self.subscribers: list[asyncio.Queue] = []
105
+
106
+ def subscribe(self, maxsize: int = 100) -> asyncio.Queue:
107
+ queue = asyncio.Queue(maxsize=maxsize)
108
+ self.subscribers.append(queue)
109
+ return queue
110
+
111
+ def unsubscribe(self, queue: asyncio.Queue):
112
+ self.subscribers.remove(queue)
113
+
114
+ async def publish(self, event: dict):
115
+ for queue in self.subscribers:
116
+ try:
117
+ queue.put_nowait(event)
118
+ except asyncio.QueueFull:
119
+ # Drop oldest event to make room
120
+ try:
121
+ queue.get_nowait()
122
+ except asyncio.QueueEmpty:
123
+ pass
124
+ queue.put_nowait(event)
125
+
126
+ bus = EventBus()
127
+ ```
128
+
129
+ ```python
130
+ async def subscriber_generator(request: Request):
131
+ queue = bus.subscribe(maxsize=100)
132
+ try:
133
+ while True:
134
+ if await request.is_disconnected():
135
+ break
136
+ try:
137
+ event = await asyncio.wait_for(queue.get(), timeout=30)
138
+ yield {"data": json.dumps(event)}
139
+ except asyncio.TimeoutError:
140
+ yield {"comment": "keep-alive"}
141
+ finally:
142
+ bus.unsubscribe(queue)
143
+ ```
144
+
145
+ The drop-oldest strategy keeps the client close to real-time. Alternative strategies: block the producer (`await queue.put()`), drop the newest event, or disconnect slow clients.
146
+
147
+ ---
148
+
149
+ ## 5. Heartbeats and Keep-Alive
150
+
151
+ Proxies, load balancers, and CDNs may close idle connections. Send periodic heartbeat comments to keep the connection alive:
152
+
153
+ ```python
154
+ async def heartbeat_generator():
155
+ event_iter = aiter(fetch_events())
156
+ while True:
157
+ try:
158
+ event = await asyncio.wait_for(anext(event_iter), timeout=15)
159
+ yield {"data": event.json(), "id": str(event.id)}
160
+ except asyncio.TimeoutError:
161
+ yield {"comment": "heartbeat"}
162
+ except StopAsyncIteration:
163
+ break
164
+ ```
165
+
166
+ `sse-starlette` provides built-in ping support via the `ping` parameter (interval in seconds). Use the built-in ping for simple cases; implement custom heartbeats when the heartbeat needs to carry data (e.g., server timestamp, queue depth).
167
+
168
+ ---
169
+
170
+ ## 6. Disconnect Detection
171
+
172
+ Detect client disconnection to stop generating events and release resources:
173
+
174
+ ```python
175
+ from fastapi import Request
176
+
177
+ async def safe_generator(request: Request):
178
+ try:
179
+ async for chunk in data_source():
180
+ if await request.is_disconnected():
181
+ break
182
+ yield {"data": chunk}
183
+ finally:
184
+ await cleanup_resources()
185
+ ```
186
+
187
+ `request.is_disconnected()` is an async check -- call it between yields, not inside tight loops. For generators driven by a queue, the `finally` block handles cleanup when the client disconnects and `EventSourceResponse` cancels the generator.
188
+
189
+ ---
190
+
191
+ ## 7. LLM Streaming with Tool Calls
192
+
193
+ Stream LLM responses that may include interleaved text and tool-use events. Structure events by type so the client can render text incrementally while handling tool calls separately:
194
+
195
+ ```python
196
+ import json
197
+
198
+ async def stream_with_tools(messages: list[dict]):
199
+ response = await client.messages.create(
200
+ model="claude-sonnet-4-20250514",
201
+ messages=messages,
202
+ tools=tool_definitions,
203
+ stream=True,
204
+ )
205
+
206
+ async for event in response:
207
+ if event.type == "content_block_start":
208
+ if event.content_block.type == "text":
209
+ yield {"event": "text_start", "data": ""}
210
+ elif event.content_block.type == "tool_use":
211
+ yield {
212
+ "event": "tool_start",
213
+ "data": json.dumps({
214
+ "id": event.content_block.id,
215
+ "name": event.content_block.name,
216
+ }),
217
+ }
218
+ elif event.type == "content_block_delta":
219
+ if event.delta.type == "text_delta":
220
+ yield {"event": "text_delta", "data": event.delta.text}
221
+ elif event.delta.type == "input_json_delta":
222
+ yield {"event": "tool_delta", "data": event.delta.partial_json}
223
+ elif event.type == "message_stop":
224
+ yield {"event": "done", "data": ""}
225
+ ```
226
+
227
+ ### Client-Side Handling (JavaScript)
228
+
229
+ ```javascript
230
+ const source = new EventSource("/chat");
231
+
232
+ source.addEventListener("text_delta", (e) => {
233
+ appendToOutput(e.data);
234
+ });
235
+
236
+ source.addEventListener("tool_start", (e) => {
237
+ const { id, name } = JSON.parse(e.data);
238
+ startToolIndicator(id, name);
239
+ });
240
+
241
+ source.addEventListener("done", () => {
242
+ source.close();
243
+ });
244
+ ```
245
+
246
+ ---
247
+
248
+ ## 8. Testing SSE Endpoints
249
+
250
+ ### Unit Testing with httpx
251
+
252
+ ```python
253
+ import pytest
254
+ from httpx import AsyncClient, ASGITransport
255
+
256
+ @pytest.mark.anyio
257
+ async def test_sse_stream():
258
+ transport = ASGITransport(app=app)
259
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
260
+ async with client.stream("GET", "/events") as response:
261
+ assert response.headers["content-type"] == "text/event-stream"
262
+ events = []
263
+ async for line in response.aiter_lines():
264
+ if line.startswith("data:"):
265
+ events.append(line[5:].strip())
266
+ if len(events) >= 3:
267
+ break
268
+ assert len(events) == 3
269
+ ```
270
+
271
+ ### Parsing SSE Lines
272
+
273
+ ```python
274
+ def parse_sse_events(raw_lines: list[str]) -> list[dict]:
275
+ events = []
276
+ current = {}
277
+ for line in raw_lines:
278
+ if line == "":
279
+ if current:
280
+ events.append(current)
281
+ current = {}
282
+ elif line.startswith("event:"):
283
+ current["event"] = line[6:].strip()
284
+ elif line.startswith("data:"):
285
+ data = line[5:].strip()
286
+ current["data"] = current.get("data", "") + data
287
+ elif line.startswith("id:"):
288
+ current["id"] = line[3:].strip()
289
+ if current:
290
+ events.append(current)
291
+ return events
292
+ ```
293
+
294
+ ### Testing Reconnection
295
+
296
+ ```python
297
+ @pytest.mark.anyio
298
+ async def test_sse_reconnection():
299
+ transport = ASGITransport(app=app)
300
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
301
+ # First connection -- collect some events
302
+ headers = {}
303
+ async with client.stream("GET", "/events", headers=headers) as resp:
304
+ last_id = None
305
+ async for line in resp.aiter_lines():
306
+ if line.startswith("id:"):
307
+ last_id = line[3:].strip()
308
+ break
309
+
310
+ # Reconnect with Last-Event-ID
311
+ headers = {"Last-Event-ID": last_id}
312
+ async with client.stream("GET", "/events", headers=headers) as resp:
313
+ async for line in resp.aiter_lines():
314
+ if line.startswith("data:"):
315
+ event = json.loads(line[5:].strip())
316
+ assert event["sequence"] > int(last_id)
317
+ break
318
+ ```
@@ -0,0 +1,345 @@
1
+ ---
2
+ name: pydantic-ai
3
+ description: >-
4
+ This skill should be used when the user asks to "build a PydanticAI agent",
5
+ "create an AI agent with PydanticAI", "add tools to a PydanticAI agent",
6
+ "stream responses with PydanticAI", "test a PydanticAI agent",
7
+ "connect PydanticAI to a Svelte frontend", "use RunContext for dependency injection",
8
+ "configure model fallbacks in PydanticAI", or discusses PydanticAI agents,
9
+ tool decorators, structured output, agent testing, or VercelAIAdapter.
10
+ version: 0.1.0
11
+ ---
12
+
13
+ # PydanticAI Agent Development
14
+
15
+ ## Mental Model
16
+
17
+ PydanticAI is a Python agent framework where **the `Agent` class is the single orchestration primitive**. An agent binds a model, a set of tools, and a typed output contract into one object. Tools are plain Python functions decorated onto the agent; the framework generates JSON schemas from their type annotations and docstrings, sends them to the model, and validates the model's tool-call arguments with Pydantic before invoking the function. This means type annotations are the contract -- there is no separate schema definition layer.
18
+
19
+ Dependency injection flows through `RunContext[DepsT]`. The caller passes a `deps` object at run time; every tool and validator receives it via `RunContext.deps`. This keeps tools pure (no global state, no module-level singletons) and makes testing straightforward -- swap the deps object, swap the behavior.
20
+
21
+ Streaming and structured output are first-class. An agent can stream raw text deltas, partially-validated Pydantic models, or both. The `VercelAIAdapter` bridges PydanticAI's streaming protocol to the Vercel AI Data Stream Protocol, enabling direct consumption by `@ai-sdk/svelte` on the frontend without a custom SSE layer.
22
+
23
+ Assume `pydantic-ai>=0.1` with Pydantic v2 for all new code.
24
+
25
+ ---
26
+
27
+ ## Agent Definition
28
+
29
+ The `Agent` class is generic over dependencies and output type: `Agent[DepsT, OutputT]`. The constructor wires together model, tools, output contract, and defaults:
30
+
31
+ ```python
32
+ from pydantic_ai import Agent
33
+ from pydantic import BaseModel
34
+
35
+ class CityInfo(BaseModel):
36
+ name: str
37
+ country: str
38
+ population: int
39
+
40
+ agent = Agent(
41
+ "anthropic:claude-sonnet-4-20250514",
42
+ output_type=CityInfo,
43
+ instructions="Extract structured city information from user queries.",
44
+ deps_type=type(None),
45
+ retries=2,
46
+ end_strategy="early",
47
+ )
48
+ ```
49
+
50
+ Key constructor parameters:
51
+
52
+ - **`model`**: String identifier like `"openai:gpt-5"` or `"anthropic:claude-sonnet-4-20250514"`, or a `Model` instance. Overridable per-run.
53
+ - **`output_type`**: Defaults to `str`. Accepts a Pydantic `BaseModel`, dataclass, `TypedDict`, list of types (union), `ToolOutput`, `NativeOutput`, or `PromptedOutput`.
54
+ - **`instructions`**: Static string or dynamic callable returning a string. Re-evaluated each run. Not included when `message_history` is provided.
55
+ - **`system_prompt`**: Static string(s) included in every request, preserved in conversation history.
56
+ - **`deps_type`**: The type of the dependency object passed at run time.
57
+ - **`retries`**: Default retry count for tools when the model sends invalid arguments (default 1).
58
+ - **`end_strategy`**: `"early"` stops on first valid output; `"exhaustive"` runs all pending tool calls first.
59
+
60
+ ### Running an Agent
61
+
62
+ ```python
63
+ # Async (preferred)
64
+ result = await agent.run("Tell me about Paris", deps=my_deps)
65
+ print(result.output) # CityInfo(name='Paris', country='France', population=2161000)
66
+ print(result.usage()) # RunUsage(requests=1, input_tokens=..., output_tokens=...)
67
+
68
+ # Sync convenience
69
+ result = agent.run_sync("Tell me about Paris")
70
+
71
+ # With overrides
72
+ result = await agent.run(
73
+ "Tell me about Paris",
74
+ model="openai:gpt-5",
75
+ model_settings={"temperature": 0.0},
76
+ usage_limits=UsageLimits(request_limit=5),
77
+ )
78
+ ```
79
+
80
+ The `AgentRunResult` exposes `.output` (typed), `.usage()`, `.all_messages()`, and `.new_messages()` for conversation continuation.
81
+
82
+ ---
83
+
84
+ ## Tools
85
+
86
+ Tools give the agent the ability to take actions and retrieve information. There are two decorator forms based on whether the tool needs access to dependencies:
87
+
88
+ ### Context-Aware Tools
89
+
90
+ ```python
91
+ from pydantic_ai import Agent, RunContext
92
+
93
+ agent = Agent("openai:gpt-5", deps_type=DatabasePool)
94
+
95
+ @agent.tool(retries=3)
96
+ async def lookup_user(ctx: RunContext[DatabasePool], user_id: int) -> str:
97
+ """Find a user by their ID. Returns the user's name and email."""
98
+ row = await ctx.deps.fetchrow("SELECT name, email FROM users WHERE id = $1", user_id)
99
+ if not row:
100
+ raise ModelRetry("No user found with that ID, try a different one")
101
+ return f"{row['name']} ({row['email']})"
102
+ ```
103
+
104
+ The first parameter **must** be `RunContext[DepsT]`. All subsequent parameters become the tool's JSON schema. The docstring becomes the tool description sent to the model.
105
+
106
+ ### Plain Tools
107
+
108
+ ```python
109
+ @agent.tool_plain
110
+ def roll_dice(sides: int = 6) -> str:
111
+ """Roll a die with the specified number of sides."""
112
+ return str(random.randint(1, sides))
113
+ ```
114
+
115
+ No `RunContext` parameter. Use for stateless utilities that need no dependencies.
116
+
117
+ ### Tool Decorator Options
118
+
119
+ Both `@agent.tool` and `@agent.tool_plain` accept:
120
+
121
+ - **`retries`**: Override the agent-level retry count for this tool.
122
+ - **`prepare`**: A `ToolsPrepareFunc` to dynamically include/exclude the tool or modify its schema per run.
123
+ - **`name`** / **`description`**: Override the function name or docstring.
124
+
125
+ ### Error Handling with ModelRetry
126
+
127
+ ```python
128
+ from pydantic_ai import ModelRetry
129
+
130
+ @agent.tool
131
+ async def search(ctx: RunContext[SearchClient], query: str) -> str:
132
+ """Search the knowledge base."""
133
+ results = await ctx.deps.search(query)
134
+ if not results:
135
+ raise ModelRetry("No results found. Try broader or different search terms.")
136
+ return "\n".join(r.summary for r in results[:5])
137
+ ```
138
+
139
+ Raising `ModelRetry(message)` sends the error back to the model as a tool-call error, letting it self-correct its arguments.
140
+
141
+ > **Deep dive:** See `references/agents-and-tools.md` for tool registration via constructor, `prepare` functions for dynamic tool filtering, result types and validators, and advanced output type patterns (unions, `ToolOutput`, `NativeOutput`).
142
+
143
+ ---
144
+
145
+ ## Dependency Injection with RunContext
146
+
147
+ The dependency system is explicit: define a deps type, pass it at run time, access it in tools and validators through `RunContext.deps`:
148
+
149
+ ```python
150
+ from dataclasses import dataclass
151
+ from pydantic_ai import Agent, RunContext
152
+
153
+ @dataclass
154
+ class AppDeps:
155
+ db: DatabasePool
156
+ cache: RedisClient
157
+ api_key: str
158
+
159
+ agent = Agent("anthropic:claude-sonnet-4-20250514", deps_type=AppDeps)
160
+
161
+ @agent.tool
162
+ async def fetch_order(ctx: RunContext[AppDeps], order_id: str) -> str:
163
+ """Retrieve order details by ID."""
164
+ cached = await ctx.deps.cache.get(f"order:{order_id}")
165
+ if cached:
166
+ return cached
167
+ row = await ctx.deps.db.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)
168
+ return json.dumps(dict(row))
169
+
170
+ # At call site
171
+ deps = AppDeps(db=pool, cache=redis, api_key=os.environ["API_KEY"])
172
+ result = await agent.run("What's the status of order ORD-123?", deps=deps)
173
+ ```
174
+
175
+ Dependencies can be any object -- a dataclass, a named tuple, a plain class, or even a single value. The `deps_type` annotation enables type checking throughout the tool chain.
176
+
177
+ ---
178
+
179
+ ## Model Configuration
180
+
181
+ ### Model Selection
182
+
183
+ ```python
184
+ # By string identifier (most common)
185
+ agent = Agent("anthropic:claude-sonnet-4-20250514")
186
+ agent = Agent("openai:gpt-5")
187
+ agent = Agent("google-gla:gemini-2.0-flash")
188
+
189
+ # Override at run time
190
+ result = await agent.run("prompt", model="openai:gpt-5")
191
+ ```
192
+
193
+ ### FallbackModel
194
+
195
+ Sequence multiple models; the framework switches to the next on API errors:
196
+
197
+ ```python
198
+ from pydantic_ai.models.fallback import FallbackModel
199
+
200
+ fallback = FallbackModel("anthropic:claude-sonnet-4-20250514", "openai:gpt-5")
201
+ agent = Agent(fallback)
202
+ ```
203
+
204
+ On failure of all models, raises `FallbackExceptionGroup`. By default only `ModelHTTPError` triggers fallback; validation errors use the retry mechanism instead.
205
+
206
+ ### ModelSettings
207
+
208
+ Control generation parameters per-agent or per-run:
209
+
210
+ ```python
211
+ from pydantic_ai import ModelSettings
212
+
213
+ agent = Agent(
214
+ "openai:gpt-5",
215
+ model_settings=ModelSettings(temperature=0.0, max_tokens=1000),
216
+ )
217
+
218
+ # Override per-run
219
+ result = await agent.run("prompt", model_settings={"temperature": 0.7})
220
+ ```
221
+
222
+ Available settings: `temperature`, `max_tokens`, `top_p`, `timeout`, `seed`, `parallel_tool_calls`, `presence_penalty`, `frequency_penalty`, `stop_sequences`.
223
+
224
+ > **Deep dive:** See `references/models-and-streaming.md` for streaming patterns (text, structured, deltas), `VercelAIAdapter` for Svelte integration, `TestModel` and `FunctionModel` for testing, and usage tracking with `UsageLimits`.
225
+
226
+ ---
227
+
228
+ ## Streaming
229
+
230
+ ### Text Streaming
231
+
232
+ ```python
233
+ async with agent.run_stream("Tell me a story") as result:
234
+ async for delta in result.stream_text(delta=True):
235
+ print(delta, end="", flush=True)
236
+ ```
237
+
238
+ `stream_text()` yields cumulative text by default. Pass `delta=True` for incremental chunks. The `debounce_by` parameter (default `0.1s`) batches rapid chunks to reduce overhead.
239
+
240
+ ### Structured Output Streaming
241
+
242
+ ```python
243
+ agent = Agent("openai:gpt-5", output_type=Story)
244
+
245
+ async with agent.run_stream("Write a story") as result:
246
+ async for partial in result.stream_output(debounce_by=0.1):
247
+ print(partial) # Partially-validated Story objects
248
+ ```
249
+
250
+ Each yielded object is a `Story` instance with whatever fields have been parsed so far. Validators with side effects should check `ctx.partial_output` to avoid running during intermediate validations.
251
+
252
+ ---
253
+
254
+ ## Testing
255
+
256
+ PydanticAI provides two test models that avoid real API calls:
257
+
258
+ ### TestModel
259
+
260
+ Calls all registered tools and generates data matching output schemas procedurally:
261
+
262
+ ```python
263
+ from pydantic_ai.models.test import TestModel
264
+
265
+ with agent.override(model=TestModel()):
266
+ result = agent.run_sync("test prompt")
267
+ assert isinstance(result.output, CityInfo)
268
+ ```
269
+
270
+ ### FunctionModel
271
+
272
+ Full control over model responses via a callback:
273
+
274
+ ```python
275
+ from pydantic_ai.models.function import FunctionModel, AgentInfo
276
+ from pydantic_ai.messages import ModelResponse, TextPart
277
+
278
+ def mock_model(messages, info: AgentInfo) -> ModelResponse:
279
+ return ModelResponse(parts=[TextPart("mocked response")])
280
+
281
+ with agent.override(model=FunctionModel(mock_model)):
282
+ result = agent.run_sync("test")
283
+ assert result.output == "mocked response"
284
+ ```
285
+
286
+ ### Safety Guard
287
+
288
+ Prevent accidental real API calls in test suites:
289
+
290
+ ```python
291
+ from pydantic_ai import models
292
+ models.ALLOW_MODEL_REQUESTS = False # Raises error if a non-test model is used
293
+ ```
294
+
295
+ ---
296
+
297
+ ## VercelAIAdapter (Svelte Integration)
298
+
299
+ Bridge a PydanticAI agent to a Vercel AI SDK frontend with a single endpoint:
300
+
301
+ ```python
302
+ from fastapi import FastAPI, Request, Response
303
+ from pydantic_ai import Agent
304
+ from pydantic_ai.ui.vercel_ai import VercelAIAdapter
305
+
306
+ app = FastAPI()
307
+ agent = Agent("anthropic:claude-sonnet-4-20250514", instructions="Be helpful.")
308
+
309
+ @app.post("/chat")
310
+ async def chat(request: Request) -> Response:
311
+ return await VercelAIAdapter.dispatch_request(request, agent=agent)
312
+ ```
313
+
314
+ On the Svelte side, consume with `@ai-sdk/svelte`:
315
+
316
+ ```svelte
317
+ <script>
318
+ import { useChat } from '@ai-sdk/svelte';
319
+ const { messages, input, handleSubmit } = useChat({ api: '/chat' });
320
+ </script>
321
+ ```
322
+
323
+ `VercelAIAdapter` translates PydanticAI's event format to the Vercel AI Data Stream Protocol automatically -- no custom SSE parsing needed.
324
+
325
+ ---
326
+
327
+ ## Ambiguity Policy
328
+
329
+ These defaults apply when the user does not specify a preference. State the assumption when making a choice so the user can override:
330
+
331
+ - **Model selection:** Default to `"anthropic:claude-sonnet-4-20250514"` for agents requiring tool use. Use `"openai:gpt-5"` only when the user specifies OpenAI.
332
+ - **Output type:** Default to `str` for conversational agents. Use a Pydantic model when the caller needs structured data.
333
+ - **Dependency injection:** Default to a dataclass for `deps_type` when multiple resources are needed. Use a single value when only one resource is injected.
334
+ - **Streaming:** Default to `run_stream()` when output is displayed to a user. Use `run()` for background/batch processing.
335
+ - **Testing:** Default to `TestModel` for unit tests. Use `FunctionModel` when specific response sequences are needed.
336
+ - **Frontend bridge:** Default to `VercelAIAdapter` when the frontend uses `@ai-sdk/svelte` or any Vercel AI SDK client.
337
+
338
+ ---
339
+
340
+ ## Reference Files
341
+
342
+ | File | Contents |
343
+ |------|----------|
344
+ | `references/agents-and-tools.md` | Agent constructor details, tool registration via constructor, `prepare` functions for dynamic tool sets, result types and validators, output type patterns (unions, ToolOutput, NativeOutput, PromptedOutput), ModelRetry mechanics |
345
+ | `references/models-and-streaming.md` | Model configuration, FallbackModel, streaming (text, structured, deltas), StreamedRunResult API, VercelAIAdapter integration, TestModel and FunctionModel for testing, usage tracking with UsageLimits, capture_run_messages |