From 6549c6994a435af8482661edb80bf3134d59e3d1 Mon Sep 17 00:00:00 2001 From: Steven Huang <134844631+remote-controlled-man@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:19:18 +0800 Subject: [PATCH 1/4] fix(python): encode memory event subscription query parameters --- sdk/python/agentfield/memory_events.py | 11 ++++--- sdk/python/tests/test_memory_events.py | 42 ++++++++++++++++++++------ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/sdk/python/agentfield/memory_events.py b/sdk/python/agentfield/memory_events.py index 7aae36dd8..1e0ee8635 100644 --- a/sdk/python/agentfield/memory_events.py +++ b/sdk/python/agentfield/memory_events.py @@ -4,6 +4,7 @@ from datetime import datetime from functools import wraps from typing import Any, Callable, Dict, List, Optional, Union +from urllib.parse import urlencode import websockets @@ -152,16 +153,16 @@ async def connect( ws_url = f"{self.base_url}/api/v1/memory/events/ws" # Add query parameters for server-side filtering - query_params = [] + query_params = {} if patterns: - query_params.append(f"patterns={','.join(patterns)}") + query_params["patterns"] = ",".join(patterns) if scope: - query_params.append(f"scope={scope}") + query_params["scope"] = scope if scope_id: - query_params.append(f"scope_id={scope_id}") + query_params["scope_id"] = scope_id if query_params: - ws_url += "?" + "&".join(query_params) + ws_url += "?" + urlencode(query_params) self.websocket = await asyncio.wait_for( websockets.connect(ws_url, **{_HEADERS_KWARG: headers}), diff --git a/sdk/python/tests/test_memory_events.py b/sdk/python/tests/test_memory_events.py index 02420ccb5..b934cef9c 100644 --- a/sdk/python/tests/test_memory_events.py +++ b/sdk/python/tests/test_memory_events.py @@ -1,6 +1,7 @@ import asyncio import json from types import SimpleNamespace +from urllib.parse import parse_qs, urlsplit import websockets @@ -99,7 +100,18 @@ async def get(self, url, params=None, headers=None, timeout=None): @pytest.mark.asyncio -async def test_memory_event_client_connect_builds_ws_url(monkeypatch): +@pytest.mark.parametrize( + ("patterns", "scope", "scope_id"), + [ + (["cart.*", "order.*"], "session", "abc"), + (["user+plan.*"], "actor", "alice+trial@example.com"), + (["cart&total.*", "issue#*"], "session", "team&env=prod#1"), + (["user preferences.*", "用户.*"], "workflow", "会话 + 1"), + ], +) +async def test_memory_event_client_connect_builds_ws_url( + monkeypatch, patterns, scope, scope_id +): ctx = SimpleNamespace(to_headers=lambda: {"Authorization": "token"}) client = MemoryEventClient("http://agentfield", ctx) @@ -112,7 +124,9 @@ def __init__(self): async def fake_connect(url, **kwargs): record["url"] = url - record["headers"] = kwargs.get("additional_headers") or kwargs.get("extra_headers") + record["headers"] = kwargs.get("additional_headers") or kwargs.get( + "extra_headers" + ) return DummyWebSocket() async def fake_listen(self): @@ -121,15 +135,19 @@ async def fake_listen(self): monkeypatch.setattr("agentfield.memory_events.websockets.connect", fake_connect) monkeypatch.setattr(MemoryEventClient, "_listen", fake_listen, raising=False) - await client.connect( - patterns=["cart.*", "order.*"], scope="session", scope_id="abc" - ) + await client.connect(patterns=patterns, scope=scope, scope_id=scope_id) await asyncio.sleep(0) - assert record["url"].startswith("ws://agentfield") - assert "patterns=cart.*,order.*" in record["url"] - assert "scope=session" in record["url"] - assert "scope_id=abc" in record["url"] + parsed_url = urlsplit(record["url"]) + assert parsed_url.scheme == "ws" + assert parsed_url.netloc == "agentfield" + assert parsed_url.path == "/api/v1/memory/events/ws" + assert parsed_url.fragment == "" + assert parse_qs(parsed_url.query) == { + "patterns": [",".join(patterns)], + "scope": [scope], + "scope_id": [scope_id], + } assert record["headers"] == {"Authorization": "token"} assert listener_called.get("run") is True @@ -163,6 +181,7 @@ def __init__(self): async def fake_connect(url, **kwargs): called_with.update(kwargs) + called_with["url"] = url return DummyWebSocket() monkeypatch.setattr("agentfield.memory_events.websockets.connect", fake_connect) @@ -173,6 +192,7 @@ async def fake_connect(url, **kwargs): assert _HEADERS_KWARG in called_with assert called_with[_HEADERS_KWARG] == {"Authorization": "token"} + assert called_with["url"] == "ws://agentfield/api/v1/memory/events/ws" @pytest.mark.asyncio @@ -198,7 +218,9 @@ async def fake_reconnect(self): # Give the background task a chance to start await asyncio.sleep(0.05) - assert reconnect_started.is_set(), "reconnect should have been started in background" + assert reconnect_started.is_set(), ( + "reconnect should have been started in background" + ) assert not client.is_listening From 9288614e8e43cef26b7e48b095b3a0c39f738b02 Mon Sep 17 00:00:00 2001 From: Steven Huang <134844631+remote-controlled-man@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:49:54 +0800 Subject: [PATCH 2/4] test(control-plane): drain MCP jobs before replacing shared state --- control-plane/internal/handlers/mcp_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/control-plane/internal/handlers/mcp_test.go b/control-plane/internal/handlers/mcp_test.go index 8e1080cde..d90ac0df2 100644 --- a/control-plane/internal/handlers/mcp_test.go +++ b/control-plane/internal/handlers/mcp_test.go @@ -345,6 +345,8 @@ func TestMCP_GetReasonerSchema(t *testing.T) { func TestMCP_ExecuteReasoner(t *testing.T) { store := newMCPTestStore(mcpActiveAgent()) router := newMCPTestRouter(t, store) + // Finish accepted jobs before later tests replace the global limiter. + t.Cleanup(ResetAsyncWorkerPoolForTesting) t.Run("bad target errors mentioning discover_agents", func(t *testing.T) { payload, isErr := mcpCallTool(t, router, "execute_reasoner", map[string]interface{}{ From c292208edb4ac397a094b75a81d40cb8164d17c7 Mon Sep 17 00:00:00 2001 From: Steven Huang <134844631+remote-controlled-man@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:50:36 +0800 Subject: [PATCH 3/4] fix(build): format Go packages without recursive overlap --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 68bf67804..7b17bdda1 100644 --- a/Makefile +++ b/Makefile @@ -42,8 +42,8 @@ lint: ( cd sdk/python && ruff check || true ) fmt: - ( cd control-plane && gofmt -w $$(go list -f '{{.Dir}}' ./...) ) - ( cd sdk/go && gofmt -w $$(go list -f '{{.Dir}}' ./...) ) + ( cd control-plane && go fmt ./... ) + ( cd sdk/go && go fmt ./... ) ( cd sdk/python && ruff format . ) tidy: From c2c674b91d902c071b71a03b65744f053e4dc8ea Mon Sep 17 00:00:00 2001 From: Steven Huang <134844631+remote-controlled-man@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:10:58 +0800 Subject: [PATCH 4/4] chore: move validation fixes to focused pull requests --- Makefile | 4 ++-- control-plane/internal/handlers/mcp_test.go | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 7b17bdda1..68bf67804 100644 --- a/Makefile +++ b/Makefile @@ -42,8 +42,8 @@ lint: ( cd sdk/python && ruff check || true ) fmt: - ( cd control-plane && go fmt ./... ) - ( cd sdk/go && go fmt ./... ) + ( cd control-plane && gofmt -w $$(go list -f '{{.Dir}}' ./...) ) + ( cd sdk/go && gofmt -w $$(go list -f '{{.Dir}}' ./...) ) ( cd sdk/python && ruff format . ) tidy: diff --git a/control-plane/internal/handlers/mcp_test.go b/control-plane/internal/handlers/mcp_test.go index d90ac0df2..8e1080cde 100644 --- a/control-plane/internal/handlers/mcp_test.go +++ b/control-plane/internal/handlers/mcp_test.go @@ -345,8 +345,6 @@ func TestMCP_GetReasonerSchema(t *testing.T) { func TestMCP_ExecuteReasoner(t *testing.T) { store := newMCPTestStore(mcpActiveAgent()) router := newMCPTestRouter(t, store) - // Finish accepted jobs before later tests replace the global limiter. - t.Cleanup(ResetAsyncWorkerPoolForTesting) t.Run("bad target errors mentioning discover_agents", func(t *testing.T) { payload, isErr := mcpCallTool(t, router, "execute_reasoner", map[string]interface{}{