Skip to content

Commit 454e94b

Browse files
committed
2 parents 1d13858 + 4f20147 commit 454e94b

File tree

11 files changed

+272
-140
lines changed

11 files changed

+272
-140
lines changed

Dockerfile

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,14 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
112112
# You may override with DISABLE_AUTH=true in development.
113113
ENV DISABLE_AUTH=false
114114

115-
# Default to development mode (no separate worker needed).
116-
# For production, override the command to remove --no-worker and run a separate task-worker container.
115+
# Default to development mode using the API's default backend (Docket). For
116+
# single-process development without a worker, add `--task-backend=asyncio` to
117+
# the api command.
117118
# Examples:
118-
# Development: docker run -p 8000:8000 redislabs/agent-memory-server
119+
# Development: docker run -p 8000:8000 redislabs/agent-memory-server agent-memory api --host 0.0.0.0 --port 8000 --task-backend=asyncio
119120
# Production API: docker run -p 8000:8000 redislabs/agent-memory-server agent-memory api --host 0.0.0.0 --port 8000
120121
# Production Worker: docker run redislabs/agent-memory-server agent-memory task-worker --concurrency 10
121-
CMD ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000", "--no-worker"]
122+
CMD ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000"]
122123

123124
# ============================================
124125
# AWS VARIANT - Includes AWS Bedrock support
@@ -142,10 +143,11 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
142143
# You may override with DISABLE_AUTH=true in development.
143144
ENV DISABLE_AUTH=false
144145

145-
# Default to development mode (no separate worker needed).
146-
# For production, override the command to remove --no-worker and run a separate task-worker container.
146+
# Default to development mode using the API's default backend (Docket). For
147+
# single-process development without a worker, add `--task-backend=asyncio` to
148+
# the api command.
147149
# Examples:
148-
# Development: docker run -p 8000:8000 redislabs/agent-memory-server:aws
150+
# Development: docker run -p 8000:8000 redislabs/agent-memory-server:aws agent-memory api --host 0.0.0.0 --port 8000 --task-backend=asyncio
149151
# Production API: docker run -p 8000:8000 redislabs/agent-memory-server:aws agent-memory api --host 0.0.0.0 --port 8000
150152
# Production Worker: docker run redislabs/agent-memory-server:aws agent-memory task-worker --concurrency 10
151-
CMD ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000", "--no-worker"]
153+
CMD ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000"]

README.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ docker-compose up
3131
docker run -p 8000:8000 \
3232
-e REDIS_URL=redis://your-redis:6379 \
3333
-e OPENAI_API_KEY=your-key \
34-
redislabs/agent-memory-server:latest
34+
redislabs/agent-memory-server:latest \
35+
agent-memory api --host 0.0.0.0 --port 8000 --task-backend=asyncio
3536
```
3637

37-
The default image runs in development mode (`--no-worker`), which is perfect for testing and development.
38+
By default, the image runs the API with the **Docket** task backend, which
39+
expects a separate `agent-memory task-worker` process for non-blocking
40+
background tasks. The example above shows how to override this to use the
41+
asyncio backend for a single-container development setup.
3842

3943
**Production Deployment**:
4044

@@ -74,13 +78,8 @@ uv install --all-extras
7478
# Start Redis
7579
docker-compose up redis
7680

77-
<<<<<<< Updated upstream
78-
# Start the server (development mode)
79-
uv run agent-memory api --no-worker
80-
=======
8181
# Start the server (development mode, asyncio task backend)
8282
uv run agent-memory api --task-backend=asyncio
83-
>>>>>>> Stashed changes
8483
```
8584

8685
### 2. Python SDK
@@ -160,8 +159,8 @@ result = await executor.ainvoke({"input": "Remember that I love pizza"})
160159
# Start MCP server (stdio mode - recommended for Claude Desktop)
161160
uv run agent-memory mcp
162161

163-
# Or with SSE mode (development mode)
164-
uv run agent-memory mcp --mode sse --port 9000 --no-worker
162+
# Or with SSE mode (development mode, default asyncio backend)
163+
uv run agent-memory mcp --mode sse --port 9000
165164
```
166165

167166
### MCP config via uvx (recommended)

agent_memory_server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Redis Agent Memory Server - A memory system for conversational AI."""
22

3-
__version__ = "0.12.5"
3+
__version__ = "0.12.6"

agent_memory_server/cli.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ async def run_migration():
257257
)
258258
else:
259259
click.echo(
260-
"\nMigration completed with errors. " "Run again to retry failed keys."
260+
"\nMigration completed with errors. Run again to retry failed keys."
261261
)
262262

263263
asyncio.run(run_migration())
@@ -268,16 +268,41 @@ async def run_migration():
268268
@click.option("--host", default="0.0.0.0", help="Host to run the server on")
269269
@click.option("--reload", is_flag=True, help="Enable auto-reload")
270270
@click.option(
271-
"--no-worker", is_flag=True, help="Use FastAPI background tasks instead of Docket"
271+
"--no-worker",
272+
is_flag=True,
273+
help=(
274+
"(DEPRECATED) Use --task-backend=asyncio instead. "
275+
"If present, force FastAPI/asyncio background tasks instead of Docket."
276+
),
277+
deprecated=True,
278+
)
279+
@click.option(
280+
"--task-backend",
281+
default="docket",
282+
type=click.Choice(["asyncio", "docket"]),
283+
help=(
284+
"Background task backend (asyncio, docket). "
285+
"Default is 'docket' to preserve existing behavior using Docket-based "
286+
"workers (requires a running `agent-memory task-worker` for "
287+
"non-blocking background tasks). Use 'asyncio' (or deprecated "
288+
"--no-worker) for single-process development without a worker."
289+
),
272290
)
273-
def api(port: int, host: str, reload: bool, no_worker: bool):
291+
def api(port: int, host: str, reload: bool, no_worker: bool, task_backend: str):
274292
"""Run the REST API server."""
275293
from agent_memory_server.main import on_start_logger
276294

277295
configure_logging()
278296

279-
# Set use_docket based on the --no-worker flag
280-
if no_worker:
297+
# Determine effective backend.
298+
# - Default is 'docket' to preserve prior behavior (Docket workers).
299+
# - --task-backend=asyncio opts into single-process asyncio background tasks.
300+
# - Deprecated --no-worker flag forces asyncio for backward compatibility.
301+
effective_backend = "asyncio" if no_worker else task_backend
302+
303+
if effective_backend == "docket":
304+
settings.use_docket = True
305+
else: # "asyncio"
281306
settings.use_docket = False
282307

283308
on_start_logger(port)
@@ -298,9 +323,17 @@ def api(port: int, host: str, reload: bool, no_worker: bool):
298323
type=click.Choice(["stdio", "sse"]),
299324
)
300325
@click.option(
301-
"--no-worker", is_flag=True, help="Use FastAPI background tasks instead of Docket"
326+
"--task-backend",
327+
default="asyncio",
328+
type=click.Choice(["asyncio", "docket"]),
329+
help=(
330+
"Background task backend (asyncio, docket). "
331+
"Default is 'asyncio' (no separate worker needed). "
332+
"Use 'docket' for production setups with a running task worker "
333+
"(see `agent-memory task-worker`)."
334+
),
302335
)
303-
def mcp(port: int, mode: str, no_worker: bool):
336+
def mcp(port: int, mode: str, task_backend: str):
304337
"""Run the MCP server."""
305338
import asyncio
306339

@@ -317,14 +350,12 @@ def mcp(port: int, mode: str, no_worker: bool):
317350
from agent_memory_server.mcp import mcp_app
318351

319352
async def setup_and_run():
320-
# Redis setup is handled by the MCP app before it starts
321-
322-
# Set use_docket based on mode and --no-worker flag
323-
if mode == "stdio":
324-
# Don't run a task worker in stdio mode by default
325-
settings.use_docket = False
326-
elif no_worker:
327-
# Use --no-worker flag for SSE mode
353+
# Configure background task backend for MCP.
354+
# Default is asyncio (no separate worker required). Use 'docket' to
355+
# send tasks to a separate worker process.
356+
if task_backend == "docket":
357+
settings.use_docket = True
358+
else: # "asyncio"
328359
settings.use_docket = False
329360

330361
# Run the MCP server

agent_memory_server/dependencies.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import asyncio
12
import concurrent.futures
23
from collections.abc import Callable
34
from typing import Any
45

56
from fastapi import BackgroundTasks
7+
from starlette.concurrency import run_in_threadpool
68

79
from agent_memory_server.config import settings
810
from agent_memory_server.logging import get_logger
@@ -12,11 +14,26 @@
1214

1315

1416
class HybridBackgroundTasks(BackgroundTasks):
15-
"""A BackgroundTasks implementation that can use either Docket or FastAPI background tasks."""
17+
"""A BackgroundTasks implementation that can use either Docket or asyncio tasks.
18+
19+
When use_docket=True, tasks are scheduled through Docket's Redis-based queue
20+
for processing by a separate worker process.
21+
22+
When use_docket=False, tasks are scheduled using asyncio.create_task() to run
23+
in the current event loop. This works in both FastAPI and MCP contexts, unlike
24+
the parent class's approach which relies on Starlette's response lifecycle
25+
(which doesn't exist in MCP's stdio/SSE modes).
26+
"""
1627

1728
def add_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
18-
"""Run tasks either directly, through Docket, or through FastAPI background tasks"""
19-
logger.info("Adding task to background tasks...")
29+
"""Schedule a background task for execution.
30+
31+
Args:
32+
func: The function to run (can be sync or async)
33+
*args: Positional arguments to pass to the function
34+
**kwargs: Keyword arguments to pass to the function
35+
"""
36+
logger.info(f"Adding background task: {func.__name__}")
2037

2138
if settings.use_docket:
2239
logger.info("Scheduling task through Docket")
@@ -28,7 +45,6 @@ def add_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
2845
# This runs in a thread to avoid event loop conflicts
2946
def run_in_thread():
3047
"""Run the async Docket operations in a separate thread"""
31-
import asyncio
3248

3349
async def schedule_task():
3450
async with Docket(
@@ -48,9 +64,34 @@ async def schedule_task():
4864

4965
# When using Docket, we don't add anything to FastAPI background tasks
5066
else:
51-
logger.info("Using FastAPI background tasks")
52-
# Use FastAPI's background tasks directly
53-
super().add_task(func, *args, **kwargs)
67+
logger.info("Scheduling task with asyncio.create_task")
68+
# Use asyncio.create_task to schedule the task in the event loop.
69+
# This works universally in both FastAPI and MCP contexts.
70+
#
71+
# Note: We don't use super().add_task() because Starlette's BackgroundTasks
72+
# relies on being attached to a response object and run after the response
73+
# is sent. In MCP mode (stdio/SSE), there's no Starlette response lifecycle,
74+
# so tasks added via super().add_task() would never execute.
75+
asyncio.create_task(self._run_task(func, *args, **kwargs))
76+
77+
async def _run_task(
78+
self, func: Callable[..., Any], *args: Any, **kwargs: Any
79+
) -> None:
80+
"""Execute a background task, handling both sync and async functions.
81+
82+
Args:
83+
func: The function to run (can be sync or async)
84+
*args: Positional arguments to pass to the function
85+
**kwargs: Keyword arguments to pass to the function
86+
"""
87+
try:
88+
if asyncio.iscoroutinefunction(func):
89+
await func(*args, **kwargs)
90+
else:
91+
# Run sync functions in a thread pool to avoid blocking the event loop
92+
await run_in_threadpool(func, *args, **kwargs)
93+
except Exception as e:
94+
logger.error(f"Background task {func.__name__} failed: {e}", exc_info=True)
5495

5596

5697
# Backwards compatibility alias

docker-compose-task-workers.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ networks:
1010

1111
services:
1212
# For testing a production-like setup, you can run this API and the
13-
# task-worker container. This API container does NOT use --no-worker, so when
14-
# it starts background work, the task-worker will process those tasks.
13+
# task-worker container. This API container uses --task-backend=docket, so
14+
# when it starts background work, the task-worker will process those tasks.
1515

1616
# =============================================================================
1717
# STANDARD (OpenAI/Anthropic only)
@@ -44,7 +44,7 @@ services:
4444
interval: 30s
4545
timeout: 10s
4646
retries: 3
47-
command: ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000"]
47+
command: ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000", "--task-backend", "docket"]
4848

4949
mcp:
5050
profiles: ["standard", ""]
@@ -64,7 +64,7 @@ services:
6464
- "9050:9000"
6565
depends_on:
6666
- redis
67-
command: ["agent-memory", "mcp", "--mode", "sse"]
67+
command: ["agent-memory", "mcp", "--mode", "sse", "--task-backend", "docket"]
6868
networks:
6969
- server-network
7070

@@ -126,7 +126,7 @@ services:
126126
interval: 30s
127127
timeout: 10s
128128
retries: 3
129-
command: ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000"]
129+
command: ["agent-memory", "api", "--host", "0.0.0.0", "--port", "8000", "--task-backend", "docket"]
130130

131131
mcp-aws:
132132
profiles: ["aws"]
@@ -151,7 +151,7 @@ services:
151151
- "9050:9000"
152152
depends_on:
153153
- redis
154-
command: ["agent-memory", "mcp", "--mode", "sse"]
154+
command: ["agent-memory", "mcp", "--mode", "sse", "--task-backend", "docket"]
155155
networks:
156156
- server-network
157157

docs/cli.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,16 @@ agent-memory api [OPTIONS]
2727
- `--port INTEGER`: Port to run the server on. (Default: value from `settings.port`, usually 8000)
2828
- `--host TEXT`: Host to run the server on. (Default: "0.0.0.0")
2929
- `--reload`: Enable auto-reload for development.
30-
- `--no-worker`: Use FastAPI background tasks instead of Docket workers. Ideal for development and testing.
30+
- `--task-backend [asyncio|docket]`: Background task backend. `docket` (default) uses Docket-based background workers (requires a running `agent-memory task-worker` for non-blocking tasks). `asyncio` runs tasks inline in the API process and does **not** require a separate worker.
31+
- `--no-worker` (**deprecated**): Backwards-compatible alias for `--task-backend=asyncio`. Maintained for older scripts; prefer `--task-backend`.
3132

3233
**Examples:**
3334

3435
```bash
35-
# Development mode (no separate worker needed)
36-
agent-memory api --port 8080 --reload --no-worker
36+
# Development mode (no separate worker needed, asyncio backend)
37+
agent-memory api --port 8080 --reload --task-backend asyncio
3738

38-
# Production mode (requires separate worker process)
39+
# Production mode (default Docket backend; requires separate worker process)
3940
agent-memory api --port 8080
4041
```
4142

@@ -51,22 +52,22 @@ agent-memory mcp [OPTIONS]
5152

5253
- `--port INTEGER`: Port to run the MCP server on. (Default: value from `settings.mcp_port`, usually 9000)
5354
- `--mode [stdio|sse]`: Run the MCP server in stdio or SSE mode. (Default: stdio)
54-
- `--no-worker`: Use FastAPI background tasks instead of Docket workers. Ideal for development and testing.
55+
- `--task-backend [asyncio|docket]`: Background task backend. `asyncio` (default) runs tasks inline in the MCP process with no separate worker. `docket` sends tasks to a Docket queue, which requires running `agent-memory task-worker`.
5556

5657
**Examples:**
5758

5859
```bash
59-
# Stdio mode (recommended for Claude Desktop) - automatically uses --no-worker
60+
# Stdio mode (recommended for Claude Desktop) - default asyncio backend
6061
agent-memory mcp
6162

6263
# SSE mode for development (no separate worker needed)
63-
agent-memory mcp --mode sse --port 9001 --no-worker
64+
agent-memory mcp --mode sse --port 9001
6465

6566
# SSE mode for production (requires separate worker process)
66-
agent-memory mcp --mode sse --port 9001
67+
agent-memory mcp --mode sse --port 9001 --task-backend docket
6768
```
6869

69-
**Note:** Stdio mode automatically disables Docket workers as they're not needed when Claude Desktop manages the process lifecycle.
70+
**Note:** Stdio mode is designed for tools like Claude Desktop and, by default, uses the asyncio backend (no worker). Use `--task-backend docket` if you want MCP to enqueue background work into a shared Docket worker.
7071

7172
### `schedule-task`
7273

docs/getting-started.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ But you can also run these components via the CLI commands. Here's how you
2828
run the REST API server:
2929

3030
```bash
31-
# Development mode (no separate worker needed)
32-
uv run agent-memory api --no-worker
31+
# Development mode (no separate worker needed, asyncio backend)
32+
uv run agent-memory api --task-backend asyncio
3333

34-
# Production mode (requires separate worker process)
34+
# Production mode (default Docket backend; requires separate worker process)
3535
uv run agent-memory api
3636
```
3737

@@ -42,10 +42,10 @@ Or the MCP server:
4242
uv run agent-memory mcp
4343

4444
# SSE mode for development
45-
uv run agent-memory mcp --mode sse --no-worker
46-
47-
# SSE mode for production
4845
uv run agent-memory mcp --mode sse
46+
47+
# SSE mode for production (use Docket backend)
48+
uv run agent-memory mcp --mode sse --task-backend docket
4949
```
5050

5151
### Using uvx in MCP clients
@@ -80,7 +80,7 @@ Notes:
8080
uv run agent-memory task-worker
8181
```
8282

83-
**For development**, use the `--no-worker` flag to run tasks inline without needing a separate worker process.
83+
**For development**, the default `--task-backend=asyncio` on the `mcp` command runs tasks inline without needing a separate worker process. For the `api` command, use `--task-backend=asyncio` explicitly when you want single-process behavior.
8484

8585
**NOTE:** With uv, prefix the command with `uv`, e.g.: `uv run agent-memory --mode sse`. If you installed from source, you'll probably need to add `--directory` to tell uv where to find the code: `uv run --directory <path/to/checkout> run agent-memory --mode stdio`.
8686

0 commit comments

Comments
 (0)