deepagents-serve 0.1.0__tar.gz

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.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepagents_serve
3
+ Version: 0.1.0
4
+ Summary: Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/FengJi2021/deepagents-serve
7
+ Project-URL: Repository, https://github.com/FengJi2021/deepagents-serve
8
+ Project-URL: Issues, https://github.com/FengJi2021/deepagents-serve/issues
9
+ Keywords: agents,ai,llm,langgraph,langchain,agent-server,vercel-ai,streaming
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: <4.0,>=3.11
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: starlette>=0.46.0
22
+ Requires-Dist: uvicorn>=0.34.0
23
+ Requires-Dist: langgraph>=0.4.0
24
+ Requires-Dist: langchain-core>=0.3.0
25
+
26
+ # deepagents-serve
27
+
28
+ Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
29
+
30
+ ## Quick start
31
+
32
+ ```python
33
+ from deepagents_serve import DeepAgentsServeApp
34
+ from deepagents import create_deep_agent
35
+ import uvicorn
36
+
37
+ agent = create_deep_agent(
38
+ model="anthropic:claude-opus-4-8",
39
+ skills=["myskillsregistry/skills"],
40
+ system_prompt="You are a helpful assistant."
41
+ )
42
+
43
+ app = DeepAgentsServeApp(agent)
44
+
45
+ uvicorn.run(app.build(), host="0.0.0.0", port=8000)
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Endpoint design
51
+
52
+ ### `POST /chat/stream`
53
+
54
+ Stateless. One request, one SSE stream, done. No session state is kept between calls.
55
+
56
+ **Request**
57
+
58
+ ```json
59
+ {
60
+ "messages": [
61
+ {"role": "user", "content": "List the files in the working directory."}
62
+ ]
63
+ }
64
+ ```
65
+
66
+ **Response** — `Content-Type: text/event-stream`
67
+
68
+ Uses the [Vercel AI Data Protocol](https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol). Each line is `data: <json>`:
69
+
70
+ ```text
71
+ data: {"type": "text-start", "id": "msg_01"}
72
+ data: {"type": "text-delta", "id": "msg_01", "delta": "Here are the files:\n"}
73
+ data: {"type": "text-end", "id": "msg_01"}
74
+
75
+ data: {"type": "tool-input-start", "toolCallId": "tc_01", "toolName": "bash"}
76
+ data: {"type": "tool-input-delta", "toolCallId": "tc_01", "inputTextDelta": "{\"command\":\"ls\"}"}
77
+ data: {"type": "tool-output-available", "toolCallId": "tc_01", "output": "README.md\nsrc/"}
78
+
79
+ data: {"type": "reasoning-start", "id": "r_01"}
80
+ data: {"type": "reasoning-delta", "id": "r_01", "delta": "I should list the files..."}
81
+ data: {"type": "reasoning-end", "id": "r_01"}
82
+
83
+ data: {"type": "error", "errorText": "something went wrong"}
84
+ data: {"type": "abort", "reason": "user cancelled"}
85
+ data: {"type": "finish"}
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Signal diagram
91
+
92
+ ```text
93
+ Client (Vercel SDK / Claude Code / TUI)
94
+
95
+ │ POST /chat/stream
96
+ │ {"messages": [{"role": "user", "content": "..."}]}
97
+
98
+ ┌──────────────────┐
99
+ │ Input Parser │ extract last user message from messages array
100
+ └────────┬─────────┘
101
+ │ {"role": "user", "content": "..."}
102
+
103
+ ┌──────────────────┐
104
+ │ LangGraph Graph │ graph.astream({"messages": [...]}, stream_mode=["messages"])
105
+ └────────┬─────────┘
106
+ │ (stream_part, metadata) chunks
107
+
108
+ ┌──────────────────┐
109
+ │ Normaliser │ LangGraph messages → canonical AgentEvent
110
+ │ │
111
+ │ AIMessage │→ TextDelta | ReasoningDelta
112
+ │ ToolCall │→ ToolInputDelta
113
+ │ ToolMessage │→ ToolOutputAvailable
114
+ │ Error │→ Error | Abort
115
+ └────────┬─────────┘
116
+ │ AgentEvent
117
+
118
+ ┌──────────────────┐
119
+ │ Vercel Adapter │ AgentEvent → Vercel SSE stream parts
120
+ └────────┬─────────┘
121
+ │ text/event-stream
122
+
123
+ Client
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Roadmap
129
+
130
+ **Near-term**
131
+
132
+ - [ ] Input parsing — read request body, extract messages
133
+ - [ ] Complete normaliser — tool calls, tool results, reasoning deltas from LangGraph
134
+ - [ ] Request validation — reject malformed payloads at the boundary
135
+
136
+ **Protocol compatibility**
137
+
138
+ - [ ] Claude Managed Agents endpoints — `/v1/sessions`, `/v1/sessions/{id}/events`, `/v1/sessions/{id}/events/stream`
139
+ - [ ] Claude Managed Agents SSE encoding — `agent.message`, `agent.tool_use`, `session.status_idle` event types
140
+ - [ ] Agent metadata endpoint — `GET /v1/agents/default`
141
+
142
+ **Statefulness**
143
+
144
+ - [ ] Session layer — LangGraph `thread_id` checkpointing, `session_id` → `thread_id` mapping
145
+ - [ ] Multi-turn conversations — persist conversation history across requests
146
+ - [ ] Interrupt support — `user.interrupt` event stops graph mid-execution
147
+
148
+ **Deployment**
149
+
150
+ - [ ] kagent packaging — Kubernetes-compatible worker pattern
151
+
152
+ **Eval**
153
+
154
+ - [ ] Event history capture — persist full turn events with timestamps and token counts
155
+ - [ ] Session replay — re-run a captured session for regression testing
156
+
157
+ **Skills**
158
+
159
+ - [ ] Skill registry compatibility — discover and invoke skills from a registry
160
+ - [ ] Finetune on skills — export session history as structured training data
@@ -0,0 +1,135 @@
1
+ # deepagents-serve
2
+
3
+ Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
4
+
5
+ ## Quick start
6
+
7
+ ```python
8
+ from deepagents_serve import DeepAgentsServeApp
9
+ from deepagents import create_deep_agent
10
+ import uvicorn
11
+
12
+ agent = create_deep_agent(
13
+ model="anthropic:claude-opus-4-8",
14
+ skills=["myskillsregistry/skills"],
15
+ system_prompt="You are a helpful assistant."
16
+ )
17
+
18
+ app = DeepAgentsServeApp(agent)
19
+
20
+ uvicorn.run(app.build(), host="0.0.0.0", port=8000)
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Endpoint design
26
+
27
+ ### `POST /chat/stream`
28
+
29
+ Stateless. One request, one SSE stream, done. No session state is kept between calls.
30
+
31
+ **Request**
32
+
33
+ ```json
34
+ {
35
+ "messages": [
36
+ {"role": "user", "content": "List the files in the working directory."}
37
+ ]
38
+ }
39
+ ```
40
+
41
+ **Response** — `Content-Type: text/event-stream`
42
+
43
+ Uses the [Vercel AI Data Protocol](https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol). Each line is `data: <json>`:
44
+
45
+ ```text
46
+ data: {"type": "text-start", "id": "msg_01"}
47
+ data: {"type": "text-delta", "id": "msg_01", "delta": "Here are the files:\n"}
48
+ data: {"type": "text-end", "id": "msg_01"}
49
+
50
+ data: {"type": "tool-input-start", "toolCallId": "tc_01", "toolName": "bash"}
51
+ data: {"type": "tool-input-delta", "toolCallId": "tc_01", "inputTextDelta": "{\"command\":\"ls\"}"}
52
+ data: {"type": "tool-output-available", "toolCallId": "tc_01", "output": "README.md\nsrc/"}
53
+
54
+ data: {"type": "reasoning-start", "id": "r_01"}
55
+ data: {"type": "reasoning-delta", "id": "r_01", "delta": "I should list the files..."}
56
+ data: {"type": "reasoning-end", "id": "r_01"}
57
+
58
+ data: {"type": "error", "errorText": "something went wrong"}
59
+ data: {"type": "abort", "reason": "user cancelled"}
60
+ data: {"type": "finish"}
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Signal diagram
66
+
67
+ ```text
68
+ Client (Vercel SDK / Claude Code / TUI)
69
+
70
+ │ POST /chat/stream
71
+ │ {"messages": [{"role": "user", "content": "..."}]}
72
+
73
+ ┌──────────────────┐
74
+ │ Input Parser │ extract last user message from messages array
75
+ └────────┬─────────┘
76
+ │ {"role": "user", "content": "..."}
77
+
78
+ ┌──────────────────┐
79
+ │ LangGraph Graph │ graph.astream({"messages": [...]}, stream_mode=["messages"])
80
+ └────────┬─────────┘
81
+ │ (stream_part, metadata) chunks
82
+
83
+ ┌──────────────────┐
84
+ │ Normaliser │ LangGraph messages → canonical AgentEvent
85
+ │ │
86
+ │ AIMessage │→ TextDelta | ReasoningDelta
87
+ │ ToolCall │→ ToolInputDelta
88
+ │ ToolMessage │→ ToolOutputAvailable
89
+ │ Error │→ Error | Abort
90
+ └────────┬─────────┘
91
+ │ AgentEvent
92
+
93
+ ┌──────────────────┐
94
+ │ Vercel Adapter │ AgentEvent → Vercel SSE stream parts
95
+ └────────┬─────────┘
96
+ │ text/event-stream
97
+
98
+ Client
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Roadmap
104
+
105
+ **Near-term**
106
+
107
+ - [ ] Input parsing — read request body, extract messages
108
+ - [ ] Complete normaliser — tool calls, tool results, reasoning deltas from LangGraph
109
+ - [ ] Request validation — reject malformed payloads at the boundary
110
+
111
+ **Protocol compatibility**
112
+
113
+ - [ ] Claude Managed Agents endpoints — `/v1/sessions`, `/v1/sessions/{id}/events`, `/v1/sessions/{id}/events/stream`
114
+ - [ ] Claude Managed Agents SSE encoding — `agent.message`, `agent.tool_use`, `session.status_idle` event types
115
+ - [ ] Agent metadata endpoint — `GET /v1/agents/default`
116
+
117
+ **Statefulness**
118
+
119
+ - [ ] Session layer — LangGraph `thread_id` checkpointing, `session_id` → `thread_id` mapping
120
+ - [ ] Multi-turn conversations — persist conversation history across requests
121
+ - [ ] Interrupt support — `user.interrupt` event stops graph mid-execution
122
+
123
+ **Deployment**
124
+
125
+ - [ ] kagent packaging — Kubernetes-compatible worker pattern
126
+
127
+ **Eval**
128
+
129
+ - [ ] Event history capture — persist full turn events with timestamps and token counts
130
+ - [ ] Session replay — re-run a captured session for regression testing
131
+
132
+ **Skills**
133
+
134
+ - [ ] Skill registry compatibility — discover and invoke skills from a registry
135
+ - [ ] Finetune on skills — export session history as structured training data
@@ -0,0 +1,5 @@
1
+ """deepagents-serve: universal agent server for LangGraph graphs."""
2
+
3
+ from deepagents_serve.app import DeepAgentsServeApp
4
+
5
+ __all__ = ["DeepAgentsServeApp"]
@@ -0,0 +1 @@
1
+ """Adapters that encode :data:`~deepagents_serve.events.AgentEvent` values into protocol-specific SSE streams."""
@@ -0,0 +1,81 @@
1
+ """Encode :data:`~deepagents_serve.events.AgentEvent` values as Vercel AI Data Protocol SSE frames."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import json
7
+ from typing import TYPE_CHECKING
8
+
9
+ from deepagents_serve import events
10
+ from deepagents_serve.adapter import vercel_protocol as protocol
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Iterator
14
+
15
+ from deepagents_serve.events import AgentEvent
16
+
17
+
18
+ class VercelAIProtocolAdapter:
19
+ """Stateful adapter that converts :class:`~deepagents_serve.events.AgentEvent` values into Vercel AI Data Protocol SSE frames.
20
+
21
+ Tracks open text/reasoning/tool-input blocks so that ``*-start`` and ``*-end``
22
+ frames are emitted correctly around delta streams.
23
+ """
24
+
25
+ def __init__(self) -> None:
26
+ """Initialise with no active blocks."""
27
+ self._active_text_id: str | None = None
28
+ self._active_reasoning_id: str | None = None
29
+ self._active_tool_input_id: str | None = None
30
+
31
+ def encode(self, agent_event: AgentEvent) -> Iterator[str]:
32
+ """Encode a single :class:`~deepagents_serve.events.AgentEvent` as one or more SSE data lines."""
33
+ for part in self._to_parts(agent_event):
34
+ yield f"data: {json.dumps(dataclasses.asdict(part))}\n\n"
35
+
36
+ def flush(self) -> Iterator[str]:
37
+ """Close any open text/reasoning blocks after the stream ends."""
38
+ for part in self._flush_parts():
39
+ yield f"data: {json.dumps(dataclasses.asdict(part))}\n\n"
40
+
41
+ def _to_parts(self, agent_event: AgentEvent) -> Iterator[protocol.StreamPart]: # noqa: C901
42
+ match agent_event:
43
+ case events.TextDelta(id=id, content=content):
44
+ if id != self._active_text_id:
45
+ if self._active_text_id is not None:
46
+ yield protocol.TextEnd(id=self._active_text_id)
47
+ yield protocol.TextStart(id=id)
48
+ self._active_text_id = id
49
+ yield protocol.TextDelta(id=id, delta=content)
50
+
51
+ case events.ReasoningDelta(id=id, content=content):
52
+ if id != self._active_reasoning_id:
53
+ if self._active_reasoning_id is not None:
54
+ yield protocol.ReasoningEnd(id=self._active_reasoning_id)
55
+ yield protocol.ReasoningStart(id=id)
56
+ self._active_reasoning_id = id
57
+ yield protocol.ReasoningDelta(id=id, delta=content)
58
+
59
+ case events.ToolInputDelta(id=id, name=name, args_delta=args_delta):
60
+ if id != self._active_tool_input_id:
61
+ yield protocol.ToolInputStart(toolCallId=id, toolName=name)
62
+ self._active_tool_input_id = id
63
+ yield protocol.ToolInputDelta(toolCallId=id, inputTextDelta=args_delta)
64
+
65
+ case events.ToolOutputAvailable(id=id, output=output):
66
+ yield protocol.ToolOutputAvailable(toolCallId=id, output=output)
67
+
68
+ case events.Error(text=text):
69
+ yield protocol.ErrorPart(errorText=text)
70
+
71
+ case events.Abort(reason=reason):
72
+ yield protocol.AbortPart(reason=reason)
73
+
74
+ def _flush_parts(self) -> Iterator[protocol.StreamPart]:
75
+ if self._active_text_id is not None:
76
+ yield protocol.TextEnd(id=self._active_text_id)
77
+ self._active_text_id = None
78
+ if self._active_reasoning_id is not None:
79
+ yield protocol.ReasoningEnd(id=self._active_reasoning_id)
80
+ self._active_reasoning_id = None
81
+ self._active_tool_input_id = None
@@ -0,0 +1,193 @@
1
+ """Vercel AI Data Protocol stream-part dataclasses.
2
+
3
+ Field names are intentionally camelCase to match the JSON field names defined by the
4
+ `Vercel AI Data Protocol <https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol>`_.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class MessageStart:
15
+ """Signals the start of a new AI message."""
16
+
17
+ messageId: str
18
+ type: str = field(default="start", init=False)
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class TextStart:
23
+ """Signals the start of a text content block."""
24
+
25
+ id: str
26
+ type: str = field(default="text-start", init=False)
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class TextDelta:
31
+ """A text content fragment."""
32
+
33
+ id: str
34
+ delta: str
35
+ type: str = field(default="text-delta", init=False)
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class TextEnd:
40
+ """Signals the end of a text content block."""
41
+
42
+ id: str
43
+ type: str = field(default="text-end", init=False)
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class ReasoningStart:
48
+ """Signals the start of a reasoning/thinking block."""
49
+
50
+ id: str
51
+ type: str = field(default="reasoning-start", init=False)
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class ReasoningDelta:
56
+ """A reasoning/thinking content fragment."""
57
+
58
+ id: str
59
+ delta: str
60
+ type: str = field(default="reasoning-delta", init=False)
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class ReasoningEnd:
65
+ """Signals the end of a reasoning/thinking block."""
66
+
67
+ id: str
68
+ type: str = field(default="reasoning-end", init=False)
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class SourceUrl:
73
+ """A source URL citation."""
74
+
75
+ sourceId: str
76
+ url: str
77
+ type: str = field(default="source-url", init=False)
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class SourceDocument:
82
+ """A source document citation."""
83
+
84
+ sourceId: str
85
+ mediaType: str
86
+ title: str
87
+ type: str = field(default="source-document", init=False)
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class FilePart:
92
+ """A file attachment part."""
93
+
94
+ url: str
95
+ mediaType: str
96
+ type: str = field(default="file", init=False)
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class ErrorPart:
101
+ """A non-fatal error part."""
102
+
103
+ errorText: str
104
+ type: str = field(default="error", init=False)
105
+
106
+
107
+ @dataclass(frozen=True, slots=True)
108
+ class ToolInputStart:
109
+ """Signals the start of a tool-call input stream."""
110
+
111
+ toolCallId: str
112
+ toolName: str
113
+ type: str = field(default="tool-input-start", init=False)
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class ToolInputDelta:
118
+ """A streamed fragment of a tool-call input JSON string."""
119
+
120
+ toolCallId: str
121
+ inputTextDelta: str
122
+ type: str = field(default="tool-input-delta", init=False)
123
+
124
+
125
+ @dataclass(frozen=True, slots=True)
126
+ class ToolInputAvailable:
127
+ """The complete, parsed input for a tool call."""
128
+
129
+ toolCallId: str
130
+ toolName: str
131
+ input: Any
132
+ type: str = field(default="tool-input-available", init=False)
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class ToolOutputAvailable:
137
+ """The output of a completed tool call."""
138
+
139
+ toolCallId: str
140
+ output: Any
141
+ type: str = field(default="tool-output-available", init=False)
142
+
143
+
144
+ @dataclass(frozen=True, slots=True)
145
+ class StartStep:
146
+ """Signals the start of an agent reasoning step."""
147
+
148
+ type: str = field(default="start-step", init=False)
149
+
150
+
151
+ @dataclass(frozen=True, slots=True)
152
+ class FinishStep:
153
+ """Signals the end of an agent reasoning step."""
154
+
155
+ type: str = field(default="finish-step", init=False)
156
+
157
+
158
+ @dataclass(frozen=True, slots=True)
159
+ class FinishMessage:
160
+ """Signals that the full AI message is complete."""
161
+
162
+ type: str = field(default="finish", init=False)
163
+
164
+
165
+ @dataclass(frozen=True, slots=True)
166
+ class AbortPart:
167
+ """Signals that the stream was aborted."""
168
+
169
+ reason: str | None = None
170
+ type: str = field(default="abort", init=False)
171
+
172
+
173
+ StreamPart = (
174
+ MessageStart
175
+ | TextStart
176
+ | TextDelta
177
+ | TextEnd
178
+ | ReasoningStart
179
+ | ReasoningDelta
180
+ | ReasoningEnd
181
+ | SourceUrl
182
+ | SourceDocument
183
+ | FilePart
184
+ | ErrorPart
185
+ | ToolInputStart
186
+ | ToolInputDelta
187
+ | ToolInputAvailable
188
+ | ToolOutputAvailable
189
+ | StartStep
190
+ | FinishStep
191
+ | FinishMessage
192
+ | AbortPart
193
+ )
@@ -0,0 +1,33 @@
1
+ """DeepAgentsServeApp — wraps a LangGraph graph and exposes it via Starlette."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from starlette.applications import Starlette
8
+
9
+ from deepagents_serve.route.health import router as health_router
10
+ from deepagents_serve.route.vercel import router as vercel_router
11
+
12
+
13
+ class DeepAgentsServeApp:
14
+ """Builds a Starlette ASGI application from any compiled LangGraph graph.
15
+
16
+ Args:
17
+ graph: A compiled LangGraph graph (or any object with an ``astream`` method).
18
+ """
19
+
20
+ def __init__(self, graph: Any) -> None:
21
+ """Initialise with a compiled LangGraph graph."""
22
+ self._graph = graph
23
+
24
+ def build(self) -> Starlette:
25
+ """Assemble and return the Starlette ASGI application."""
26
+ app = Starlette(
27
+ routes=[
28
+ *health_router.routes,
29
+ *vercel_router.routes,
30
+ ]
31
+ )
32
+ app.state.graph = self._graph
33
+ return app
@@ -0,0 +1,53 @@
1
+ """Canonical agent event types emitted by the normaliser."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class TextDelta:
8
+ """A streamed text content fragment from an AI message."""
9
+
10
+ id: str
11
+ content: str
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ReasoningDelta:
16
+ """A streamed reasoning/thinking fragment from an AI message."""
17
+
18
+ id: str
19
+ content: str
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class ToolInputDelta:
24
+ """A streamed partial argument string for a tool call."""
25
+
26
+ id: str
27
+ name: str
28
+ args_delta: str
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ToolOutputAvailable:
33
+ """The complete output of a finished tool call."""
34
+
35
+ id: str
36
+ output: str
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class Error:
41
+ """A non-fatal error event from the agent."""
42
+
43
+ text: str
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Abort:
48
+ """Signals that the agent stream was aborted."""
49
+
50
+ reason: str | None = None
51
+
52
+
53
+ AgentEvent = TextDelta | ReasoningDelta | ToolInputDelta | ToolOutputAvailable | Error | Abort
@@ -0,0 +1,54 @@
1
+ """Normalise LangGraph ``stream_mode='messages'`` output into :data:`AgentEvent` values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from langchain_core.messages import AIMessage, AIMessageChunk, ToolMessage
8
+
9
+ from deepagents_serve.events import (
10
+ AgentEvent,
11
+ TextDelta,
12
+ ToolInputDelta,
13
+ ToolOutputAvailable,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import AsyncIterator, Iterator
18
+
19
+
20
+ async def normalize(stream: AsyncIterator[Any]) -> AsyncIterator[AgentEvent]:
21
+ """Yield :class:`AgentEvent` values from a LangGraph messages stream."""
22
+ async for message, _metadata in stream:
23
+ for agent_event in _parse_message(message):
24
+ yield agent_event
25
+
26
+
27
+ def _parse_message(message: Any) -> Iterator[AgentEvent]:
28
+ if isinstance(message, (AIMessage, AIMessageChunk)):
29
+ yield from _parse_ai_message(message)
30
+ elif isinstance(message, ToolMessage):
31
+ yield ToolOutputAvailable(id=message.tool_call_id, output=str(message.content))
32
+
33
+
34
+ def _parse_ai_message(message: AIMessage | AIMessageChunk) -> Iterator[AgentEvent]:
35
+ msg_id = message.id or "msg"
36
+ content = message.content
37
+
38
+ if isinstance(content, str):
39
+ if content:
40
+ yield TextDelta(id=msg_id, content=content)
41
+ elif isinstance(content, list):
42
+ for block in content:
43
+ if not isinstance(block, dict):
44
+ continue
45
+ if block.get("type") == "text" and block.get("text"):
46
+ yield TextDelta(id=msg_id, content=block["text"])
47
+
48
+ for tool_call in getattr(message, "tool_call_chunks", []):
49
+ if tool_call.get("args"):
50
+ yield ToolInputDelta(
51
+ id=tool_call["id"] or msg_id,
52
+ name=tool_call.get("name", ""),
53
+ args_delta=tool_call["args"],
54
+ )
File without changes
@@ -0,0 +1 @@
1
+ """Starlette route definitions for the deepagents-serve endpoints."""
@@ -0,0 +1,24 @@
1
+ """Health and readiness check endpoints."""
2
+
3
+ from starlette.requests import Request
4
+ from starlette.responses import JSONResponse
5
+ from starlette.routing import Route, Router
6
+
7
+
8
+ async def _health(_request: Request) -> JSONResponse:
9
+ return JSONResponse({"status": "ok"})
10
+
11
+
12
+ async def _ready(request: Request) -> JSONResponse:
13
+ graph = request.app.state.graph
14
+ if graph is None:
15
+ return JSONResponse({"status": "not ready"}, status_code=503)
16
+ return JSONResponse({"status": "ready"})
17
+
18
+
19
+ router = Router(
20
+ routes=[
21
+ Route("/health", _health, methods=["GET"]),
22
+ Route("/ready", _ready, methods=["GET"]),
23
+ ]
24
+ )
@@ -0,0 +1,49 @@
1
+ """Vercel AI Data Protocol streaming chat endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from starlette.responses import StreamingResponse
8
+ from starlette.routing import Route, Router
9
+
10
+ from deepagents_serve.adapter.vercel import VercelAIProtocolAdapter
11
+ from deepagents_serve.normaliser import normalize
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import AsyncIterator
15
+
16
+ from starlette.requests import Request
17
+
18
+ # TODO: formalise request schema with a dataclass/Pydantic model
19
+ # Expected shape: {"messages": [{"role": "user"|"assistant", "content": str}]}
20
+
21
+
22
+ async def _chat_stream(request: Request) -> StreamingResponse:
23
+ body = await request.json()
24
+ messages = body.get("messages", [])
25
+ graph = request.app.state.graph
26
+
27
+ async def event_generator() -> AsyncIterator[str]:
28
+ adapter = VercelAIProtocolAdapter()
29
+ stream = graph.astream({"messages": messages}, stream_mode="messages")
30
+
31
+ async for agent_event in normalize(stream):
32
+ for chunk in adapter.encode(agent_event):
33
+ yield chunk
34
+
35
+ for chunk in adapter.flush():
36
+ yield chunk
37
+
38
+ return StreamingResponse(
39
+ event_generator(),
40
+ media_type="text/event-stream",
41
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
42
+ )
43
+
44
+
45
+ router = Router(
46
+ routes=[
47
+ Route("/chat/stream", _chat_stream, methods=["POST"]),
48
+ ]
49
+ )
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepagents_serve
3
+ Version: 0.1.0
4
+ Summary: Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/FengJi2021/deepagents-serve
7
+ Project-URL: Repository, https://github.com/FengJi2021/deepagents-serve
8
+ Project-URL: Issues, https://github.com/FengJi2021/deepagents-serve/issues
9
+ Keywords: agents,ai,llm,langgraph,langchain,agent-server,vercel-ai,streaming
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: <4.0,>=3.11
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: starlette>=0.46.0
22
+ Requires-Dist: uvicorn>=0.34.0
23
+ Requires-Dist: langgraph>=0.4.0
24
+ Requires-Dist: langchain-core>=0.3.0
25
+
26
+ # deepagents-serve
27
+
28
+ Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
29
+
30
+ ## Quick start
31
+
32
+ ```python
33
+ from deepagents_serve import DeepAgentsServeApp
34
+ from deepagents import create_deep_agent
35
+ import uvicorn
36
+
37
+ agent = create_deep_agent(
38
+ model="anthropic:claude-opus-4-8",
39
+ skills=["myskillsregistry/skills"],
40
+ system_prompt="You are a helpful assistant."
41
+ )
42
+
43
+ app = DeepAgentsServeApp(agent)
44
+
45
+ uvicorn.run(app.build(), host="0.0.0.0", port=8000)
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Endpoint design
51
+
52
+ ### `POST /chat/stream`
53
+
54
+ Stateless. One request, one SSE stream, done. No session state is kept between calls.
55
+
56
+ **Request**
57
+
58
+ ```json
59
+ {
60
+ "messages": [
61
+ {"role": "user", "content": "List the files in the working directory."}
62
+ ]
63
+ }
64
+ ```
65
+
66
+ **Response** — `Content-Type: text/event-stream`
67
+
68
+ Uses the [Vercel AI Data Protocol](https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol). Each line is `data: <json>`:
69
+
70
+ ```text
71
+ data: {"type": "text-start", "id": "msg_01"}
72
+ data: {"type": "text-delta", "id": "msg_01", "delta": "Here are the files:\n"}
73
+ data: {"type": "text-end", "id": "msg_01"}
74
+
75
+ data: {"type": "tool-input-start", "toolCallId": "tc_01", "toolName": "bash"}
76
+ data: {"type": "tool-input-delta", "toolCallId": "tc_01", "inputTextDelta": "{\"command\":\"ls\"}"}
77
+ data: {"type": "tool-output-available", "toolCallId": "tc_01", "output": "README.md\nsrc/"}
78
+
79
+ data: {"type": "reasoning-start", "id": "r_01"}
80
+ data: {"type": "reasoning-delta", "id": "r_01", "delta": "I should list the files..."}
81
+ data: {"type": "reasoning-end", "id": "r_01"}
82
+
83
+ data: {"type": "error", "errorText": "something went wrong"}
84
+ data: {"type": "abort", "reason": "user cancelled"}
85
+ data: {"type": "finish"}
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Signal diagram
91
+
92
+ ```text
93
+ Client (Vercel SDK / Claude Code / TUI)
94
+
95
+ │ POST /chat/stream
96
+ │ {"messages": [{"role": "user", "content": "..."}]}
97
+
98
+ ┌──────────────────┐
99
+ │ Input Parser │ extract last user message from messages array
100
+ └────────┬─────────┘
101
+ │ {"role": "user", "content": "..."}
102
+
103
+ ┌──────────────────┐
104
+ │ LangGraph Graph │ graph.astream({"messages": [...]}, stream_mode=["messages"])
105
+ └────────┬─────────┘
106
+ │ (stream_part, metadata) chunks
107
+
108
+ ┌──────────────────┐
109
+ │ Normaliser │ LangGraph messages → canonical AgentEvent
110
+ │ │
111
+ │ AIMessage │→ TextDelta | ReasoningDelta
112
+ │ ToolCall │→ ToolInputDelta
113
+ │ ToolMessage │→ ToolOutputAvailable
114
+ │ Error │→ Error | Abort
115
+ └────────┬─────────┘
116
+ │ AgentEvent
117
+
118
+ ┌──────────────────┐
119
+ │ Vercel Adapter │ AgentEvent → Vercel SSE stream parts
120
+ └────────┬─────────┘
121
+ │ text/event-stream
122
+
123
+ Client
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Roadmap
129
+
130
+ **Near-term**
131
+
132
+ - [ ] Input parsing — read request body, extract messages
133
+ - [ ] Complete normaliser — tool calls, tool results, reasoning deltas from LangGraph
134
+ - [ ] Request validation — reject malformed payloads at the boundary
135
+
136
+ **Protocol compatibility**
137
+
138
+ - [ ] Claude Managed Agents endpoints — `/v1/sessions`, `/v1/sessions/{id}/events`, `/v1/sessions/{id}/events/stream`
139
+ - [ ] Claude Managed Agents SSE encoding — `agent.message`, `agent.tool_use`, `session.status_idle` event types
140
+ - [ ] Agent metadata endpoint — `GET /v1/agents/default`
141
+
142
+ **Statefulness**
143
+
144
+ - [ ] Session layer — LangGraph `thread_id` checkpointing, `session_id` → `thread_id` mapping
145
+ - [ ] Multi-turn conversations — persist conversation history across requests
146
+ - [ ] Interrupt support — `user.interrupt` event stops graph mid-execution
147
+
148
+ **Deployment**
149
+
150
+ - [ ] kagent packaging — Kubernetes-compatible worker pattern
151
+
152
+ **Eval**
153
+
154
+ - [ ] Event history capture — persist full turn events with timestamps and token counts
155
+ - [ ] Session replay — re-run a captured session for regression testing
156
+
157
+ **Skills**
158
+
159
+ - [ ] Skill registry compatibility — discover and invoke skills from a registry
160
+ - [ ] Finetune on skills — export session history as structured training data
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ deepagents_serve/__init__.py
4
+ deepagents_serve/app.py
5
+ deepagents_serve/events.py
6
+ deepagents_serve/normaliser.py
7
+ deepagents_serve/py.typed
8
+ deepagents_serve.egg-info/PKG-INFO
9
+ deepagents_serve.egg-info/SOURCES.txt
10
+ deepagents_serve.egg-info/dependency_links.txt
11
+ deepagents_serve.egg-info/requires.txt
12
+ deepagents_serve.egg-info/top_level.txt
13
+ deepagents_serve/adapter/__init__.py
14
+ deepagents_serve/adapter/vercel.py
15
+ deepagents_serve/adapter/vercel_protocol.py
16
+ deepagents_serve/route/__init__.py
17
+ deepagents_serve/route/health.py
18
+ deepagents_serve/route/vercel.py
@@ -0,0 +1,4 @@
1
+ starlette>=0.46.0
2
+ uvicorn>=0.34.0
3
+ langgraph>=0.4.0
4
+ langchain-core>=0.3.0
@@ -0,0 +1 @@
1
+ deepagents_serve
@@ -0,0 +1,107 @@
1
+ [project]
2
+ name = "deepagents_serve"
3
+ version = "0.1.0"
4
+ description = "Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11,<4.0"
8
+ keywords = ["agents", "ai", "llm", "langgraph", "langchain", "agent-server", "vercel-ai", "streaming"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ dependencies = [
21
+ "starlette>=0.46.0",
22
+ "uvicorn>=0.34.0",
23
+ "langgraph>=0.4.0",
24
+ "langchain-core>=0.3.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/FengJi2021/deepagents-serve"
29
+ Repository = "https://github.com/FengJi2021/deepagents-serve"
30
+ Issues = "https://github.com/FengJi2021/deepagents-serve/issues"
31
+
32
+ [dependency-groups]
33
+ test = [
34
+ "ruff>=0.12.2,<0.16.0",
35
+ "twine",
36
+ "build",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["setuptools>=73.0.0", "wheel"]
41
+ build-backend = "setuptools.build_meta"
42
+
43
+ [tool.setuptools.package-data]
44
+ "*" = ["py.typed", "*.md"]
45
+
46
+ [tool.ruff]
47
+ line-length = 150
48
+
49
+ [tool.ruff.format]
50
+ docstring-code-format = true
51
+
52
+ [tool.ruff.lint]
53
+ select = [
54
+ "ALL",
55
+ ]
56
+ ignore = [
57
+ "COM812", # Trailing comma missing — conflicts with the ruff formatter
58
+ "ISC001", # Implicit string concatenation on one line — conflicts with the ruff formatter
59
+ "PERF203", # `try`-`except` within a loop — incurs overhead only when exceptions are common
60
+ "SLF001", # Access to a private member of an external class
61
+ "PLR0913", # Too many arguments to function definition
62
+ "PLC0414", # Import alias does not rename original package — conflicts with type-checker re-export conventions
63
+ "ANN401", # Any is needed for LangGraph's dynamic message types
64
+ "FIX002", # TODO comment warnings are noisy during active development
65
+ "TD002", # Missing author in TODO — not enforced in this project
66
+ "TD003", # Missing issue link in TODO — not enforced in this project
67
+ ]
68
+ unfixable = ["B028"]
69
+ extend-safe-fixes = ["PLR6201"]
70
+
71
+ [tool.ruff.lint.pyupgrade]
72
+ keep-runtime-typing = true
73
+
74
+ [tool.ruff.lint.flake8-annotations]
75
+ allow-star-arg-any = true
76
+
77
+ [tool.ruff.lint.flake8-tidy-imports]
78
+ ban-relative-imports = "all"
79
+
80
+ [tool.ruff.lint.isort]
81
+ force-single-line = false
82
+ combine-as-imports = true
83
+ known-first-party = ["deepagents_serve"]
84
+
85
+ [tool.ruff.lint.pydocstyle]
86
+ convention = "google"
87
+ ignore-var-parameters = true
88
+
89
+ [tool.ruff.lint.per-file-ignores]
90
+ "deepagents_serve/adapter/vercel_protocol.py" = [
91
+ "N815", # camelCase fields are required to match Vercel AI SDK JSON field names
92
+ ]
93
+ "tests/**" = [
94
+ "ANN001",
95
+ "ANN201",
96
+ "ANN202",
97
+ "ARG002",
98
+ "D1",
99
+ "PLR2004",
100
+ "PT018",
101
+ "S101",
102
+ "S311",
103
+ ]
104
+ "scripts/**" = [
105
+ "BLE001",
106
+ "INP001",
107
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+