remdb 0.3.180__py3-none-any.whl → 0.3.258__py3-none-any.whl

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 (70) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/__init__.py +10 -1
  3. rem/agentic/context.py +185 -1
  4. rem/agentic/context_builder.py +56 -35
  5. rem/agentic/mcp/tool_wrapper.py +2 -2
  6. rem/agentic/providers/pydantic_ai.py +303 -111
  7. rem/agentic/schema.py +2 -2
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +223 -0
  10. rem/api/mcp_router/server.py +4 -0
  11. rem/api/mcp_router/tools.py +608 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +219 -20
  14. rem/api/routers/chat/child_streaming.py +393 -0
  15. rem/api/routers/chat/completions.py +77 -40
  16. rem/api/routers/chat/sse_events.py +7 -3
  17. rem/api/routers/chat/streaming.py +381 -291
  18. rem/api/routers/chat/streaming_utils.py +325 -0
  19. rem/api/routers/common.py +18 -0
  20. rem/api/routers/dev.py +7 -1
  21. rem/api/routers/feedback.py +11 -3
  22. rem/api/routers/messages.py +176 -38
  23. rem/api/routers/models.py +9 -1
  24. rem/api/routers/query.py +17 -15
  25. rem/api/routers/shared_sessions.py +16 -0
  26. rem/auth/jwt.py +19 -4
  27. rem/auth/middleware.py +42 -28
  28. rem/cli/README.md +62 -0
  29. rem/cli/commands/ask.py +205 -114
  30. rem/cli/commands/db.py +55 -31
  31. rem/cli/commands/experiments.py +1 -1
  32. rem/cli/commands/process.py +179 -43
  33. rem/cli/commands/query.py +109 -0
  34. rem/cli/commands/session.py +117 -0
  35. rem/cli/main.py +2 -0
  36. rem/models/core/experiment.py +1 -1
  37. rem/models/entities/ontology.py +18 -20
  38. rem/models/entities/session.py +1 -0
  39. rem/schemas/agents/core/agent-builder.yaml +1 -1
  40. rem/schemas/agents/rem.yaml +1 -1
  41. rem/schemas/agents/test_orchestrator.yaml +42 -0
  42. rem/schemas/agents/test_structured_output.yaml +52 -0
  43. rem/services/content/providers.py +151 -49
  44. rem/services/content/service.py +18 -5
  45. rem/services/embeddings/worker.py +26 -12
  46. rem/services/postgres/__init__.py +28 -3
  47. rem/services/postgres/diff_service.py +57 -5
  48. rem/services/postgres/programmable_diff_service.py +635 -0
  49. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  50. rem/services/postgres/register_type.py +11 -10
  51. rem/services/postgres/repository.py +39 -28
  52. rem/services/postgres/schema_generator.py +5 -5
  53. rem/services/postgres/sql_builder.py +6 -5
  54. rem/services/rem/README.md +4 -3
  55. rem/services/rem/parser.py +7 -10
  56. rem/services/rem/service.py +47 -0
  57. rem/services/session/__init__.py +8 -1
  58. rem/services/session/compression.py +47 -5
  59. rem/services/session/pydantic_messages.py +310 -0
  60. rem/services/session/reload.py +2 -1
  61. rem/settings.py +92 -7
  62. rem/sql/migrations/001_install.sql +125 -7
  63. rem/sql/migrations/002_install_models.sql +159 -149
  64. rem/sql/migrations/004_cache_system.sql +10 -276
  65. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  66. rem/utils/schema_loader.py +180 -120
  67. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/METADATA +7 -6
  68. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/RECORD +70 -61
  69. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/WHEEL +0 -0
  70. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/entry_points.txt +0 -0
rem/agentic/README.md CHANGED
@@ -716,11 +716,45 @@ curl -X POST http://localhost:8000/api/v1/chat/completions \
716
716
 
717
717
  See `rem/api/README.md` for full SSE event protocol documentation.
718
718
 
719
+ ## Multi-Agent Orchestration
720
+
721
+ Agents can delegate work to other agents via the `ask_agent` tool. This enables orchestrator patterns where a parent agent routes to specialists.
722
+
723
+ ### How It Works
724
+
725
+ 1. **Parent agent** calls `ask_agent(agent_name, input_text)`
726
+ 2. **Child agent** executes and streams its response
727
+ 3. **Child events** bubble up to parent via an event sink (asyncio.Queue in ContextVar)
728
+ 4. **All tool calls** are saved to the database for the session
729
+
730
+ ### Key Components
731
+
732
+ | Component | Location | Purpose |
733
+ |-----------|----------|---------|
734
+ | `ask_agent` tool | `mcp_router/tools.py` | Loads child agent, runs with streaming, pushes events to sink |
735
+ | Event sink | `context.py` | ContextVar holding asyncio.Queue for child→parent event flow |
736
+ | Streaming controller | `streaming.py` | Drains event sink, emits SSE events, saves to DB |
737
+
738
+ ### Event Types
739
+
740
+ Child agents emit events that the parent streams to the client:
741
+
742
+ - **`child_tool_start`**: Child is calling a tool (logged, streamed, saved to DB)
743
+ - **`child_content`**: Child's text response (streamed as SSE content delta)
744
+ - **`child_tool_result`**: Tool completed with result (metadata extraction)
745
+
746
+ ### Testing
747
+
748
+ Integration tests in `tests/integration/test_ask_agent_streaming.py` verify:
749
+ - Child content streams correctly to client
750
+ - Tool calls are persisted to database
751
+ - Multi-turn conversations save all messages
752
+
719
753
  ## Future Work
720
754
 
721
755
  - [ ] Phoenix evaluator integration
722
756
  - [ ] Agent schema registry (load schemas by URI)
723
757
  - [ ] Schema validation and versioning
724
- - [ ] Multi-turn conversation management
725
- - [ ] Agent composition (agents calling agents)
758
+ - [x] Multi-turn conversation management
759
+ - [x] Agent composition (agents calling agents)
726
760
  - [ ] Alternative provider implementations (if needed)
rem/agentic/__init__.py CHANGED
@@ -15,7 +15,13 @@ from .schema import (
15
15
  validate_agent_schema,
16
16
  create_agent_schema,
17
17
  )
18
- from .providers.pydantic_ai import create_agent_from_schema_file, create_agent, AgentRuntime
18
+ from .providers.pydantic_ai import (
19
+ create_agent_from_schema_file,
20
+ create_agent,
21
+ AgentRuntime,
22
+ clear_agent_cache,
23
+ get_agent_cache_stats,
24
+ )
19
25
  from .query_helper import ask_rem, REMQueryOutput
20
26
  from .llm_provider_models import (
21
27
  ModelInfo,
@@ -41,6 +47,9 @@ __all__ = [
41
47
  "create_agent_from_schema_file",
42
48
  "create_agent",
43
49
  "AgentRuntime",
50
+ # Agent Cache Management
51
+ "clear_agent_cache",
52
+ "get_agent_cache_stats",
44
53
  # REM Query Helpers
45
54
  "ask_rem",
46
55
  "REMQueryOutput",
rem/agentic/context.py CHANGED
@@ -16,20 +16,160 @@ Headers Mapping:
16
16
  X-Agent-Schema → context.agent_schema_uri (default: "rem")
17
17
  X-Model-Name → context.default_model
18
18
  X-Is-Eval → context.is_eval (marks session as evaluation)
19
+ X-Client-Id → context.client_id (e.g., "web", "mobile", "cli")
19
20
 
20
21
  Key Design Pattern:
21
22
  - AgentContext is passed to agent factory, not stored in agents
22
23
  - Enables session tracking across API, CLI, and test execution
23
24
  - Supports header-based configuration override (model, schema URI)
24
25
  - Clean separation: context (who/what) vs agent (how)
26
+
27
+ Multi-Agent Context Propagation:
28
+ - ContextVar (_current_agent_context) threads context through nested agent calls
29
+ - Parent context is automatically available to child agents via get_current_context()
30
+ - Use agent_context_scope() context manager for scoped context setting
31
+ - Child agents inherit user_id, tenant_id, session_id, is_eval from parent
25
32
  """
26
33
 
34
+ import asyncio
35
+ from contextlib import contextmanager
36
+ from contextvars import ContextVar
37
+ from typing import Any, Generator
38
+
27
39
  from loguru import logger
28
40
  from pydantic import BaseModel, Field
29
41
 
30
42
  from ..settings import settings
31
43
 
32
44
 
45
+ # Thread-local context for current agent execution
46
+ # This enables context propagation through nested agent calls (multi-agent)
47
+ _current_agent_context: ContextVar["AgentContext | None"] = ContextVar(
48
+ "current_agent_context", default=None
49
+ )
50
+
51
+ # Event sink for streaming child agent events to parent
52
+ # When set, child agents (via ask_agent) should push their events here
53
+ # for the parent's streaming loop to proxy to the client
54
+ _parent_event_sink: ContextVar["asyncio.Queue | None"] = ContextVar(
55
+ "parent_event_sink", default=None
56
+ )
57
+
58
+
59
+ def get_current_context() -> "AgentContext | None":
60
+ """
61
+ Get the current agent context from context var.
62
+
63
+ Used by MCP tools (like ask_agent) to inherit context from parent agent.
64
+ Returns None if no context is set (e.g., direct CLI invocation without context).
65
+
66
+ Example:
67
+ # In an MCP tool
68
+ parent_context = get_current_context()
69
+ if parent_context:
70
+ # Inherit user_id, session_id, etc. from parent
71
+ child_context = parent_context.child_context(agent_schema_uri="child-agent")
72
+ """
73
+ return _current_agent_context.get()
74
+
75
+
76
+ def set_current_context(ctx: "AgentContext | None") -> None:
77
+ """
78
+ Set the current agent context.
79
+
80
+ Called by streaming layer before agent execution.
81
+ Should be cleared (set to None) after execution completes.
82
+ """
83
+ _current_agent_context.set(ctx)
84
+
85
+
86
+ @contextmanager
87
+ def agent_context_scope(ctx: "AgentContext") -> Generator["AgentContext", None, None]:
88
+ """
89
+ Context manager for scoped context setting.
90
+
91
+ Automatically restores previous context when exiting scope.
92
+ Safe for nested agent calls - each level preserves its parent's context.
93
+
94
+ Example:
95
+ context = AgentContext(user_id="user-123")
96
+ with agent_context_scope(context):
97
+ # Context is available via get_current_context()
98
+ result = await agent.run(...)
99
+ # Previous context (or None) is restored
100
+ """
101
+ previous = _current_agent_context.get()
102
+ _current_agent_context.set(ctx)
103
+ try:
104
+ yield ctx
105
+ finally:
106
+ _current_agent_context.set(previous)
107
+
108
+
109
+ # =============================================================================
110
+ # Event Sink for Streaming Multi-Agent Delegation
111
+ # =============================================================================
112
+
113
+
114
+ def get_event_sink() -> "asyncio.Queue | None":
115
+ """
116
+ Get the parent's event sink for streaming child events.
117
+
118
+ Used by ask_agent to push child agent events to the parent's stream.
119
+ Returns None if not in a streaming context.
120
+ """
121
+ return _parent_event_sink.get()
122
+
123
+
124
+ def set_event_sink(sink: "asyncio.Queue | None") -> None:
125
+ """Set the event sink for child agents to push events to."""
126
+ _parent_event_sink.set(sink)
127
+
128
+
129
+ @contextmanager
130
+ def event_sink_scope(sink: "asyncio.Queue") -> Generator["asyncio.Queue", None, None]:
131
+ """
132
+ Context manager for scoped event sink setting.
133
+
134
+ Used by streaming layer to set up event proxying before tool execution.
135
+ Child agents (via ask_agent) will push their events to this sink.
136
+
137
+ Example:
138
+ event_queue = asyncio.Queue()
139
+ with event_sink_scope(event_queue):
140
+ # ask_agent will push child events to event_queue
141
+ async for event in tools_stream:
142
+ ...
143
+ # Also consume from event_queue
144
+ """
145
+ previous = _parent_event_sink.get()
146
+ _parent_event_sink.set(sink)
147
+ try:
148
+ yield sink
149
+ finally:
150
+ _parent_event_sink.set(previous)
151
+
152
+
153
+ async def push_event(event: Any) -> bool:
154
+ """
155
+ Push an event to the parent's event sink (if available).
156
+
157
+ Used by ask_agent to proxy child agent events to the parent's stream.
158
+ Returns True if event was pushed, False if no sink available.
159
+
160
+ Args:
161
+ event: Any streaming event (ToolCallEvent, content chunk, etc.)
162
+
163
+ Returns:
164
+ True if event was pushed to sink, False otherwise
165
+ """
166
+ sink = _parent_event_sink.get()
167
+ if sink is not None:
168
+ await sink.put(event)
169
+ return True
170
+ return False
171
+
172
+
33
173
  class AgentContext(BaseModel):
34
174
  """
35
175
  Session and configuration context for agent execution.
@@ -83,8 +223,48 @@ class AgentContext(BaseModel):
83
223
  description="Whether this is an evaluation session (set via X-Is-Eval header)",
84
224
  )
85
225
 
226
+ client_id: str | None = Field(
227
+ default=None,
228
+ description="Client identifier (e.g., 'web', 'mobile', 'cli') set via X-Client-Id header",
229
+ )
230
+
86
231
  model_config = {"populate_by_name": True}
87
232
 
233
+ def child_context(
234
+ self,
235
+ agent_schema_uri: str | None = None,
236
+ model_override: str | None = None,
237
+ ) -> "AgentContext":
238
+ """
239
+ Create a child context for nested agent calls.
240
+
241
+ Inherits user_id, tenant_id, session_id, is_eval, client_id from parent.
242
+ Allows overriding agent_schema_uri and default_model for the child.
243
+
244
+ Args:
245
+ agent_schema_uri: Agent schema for the child agent (required for lineage)
246
+ model_override: Optional model override for child agent
247
+
248
+ Returns:
249
+ New AgentContext for the child agent
250
+
251
+ Example:
252
+ parent_context = get_current_context()
253
+ child_context = parent_context.child_context(
254
+ agent_schema_uri="sentiment-analyzer"
255
+ )
256
+ agent = await create_agent(context=child_context)
257
+ """
258
+ return AgentContext(
259
+ user_id=self.user_id,
260
+ tenant_id=self.tenant_id,
261
+ session_id=self.session_id,
262
+ default_model=model_override or self.default_model,
263
+ agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
264
+ is_eval=self.is_eval,
265
+ client_id=self.client_id,
266
+ )
267
+
88
268
  @staticmethod
89
269
  def get_user_id_or_default(
90
270
  user_id: str | None,
@@ -201,6 +381,7 @@ class AgentContext(BaseModel):
201
381
  default_model=normalized.get("x-model-name") or settings.llm.default_model,
202
382
  agent_schema_uri=normalized.get("x-agent-schema"),
203
383
  is_eval=is_eval,
384
+ client_id=normalized.get("x-client-id"),
204
385
  )
205
386
 
206
387
  @classmethod
@@ -218,6 +399,7 @@ class AgentContext(BaseModel):
218
399
  - X-Model-Name: Model override
219
400
  - X-Agent-Schema: Agent schema URI
220
401
  - X-Is-Eval: Whether this is an evaluation session (true/false)
402
+ - X-Client-Id: Client identifier (e.g., "web", "mobile", "cli")
221
403
 
222
404
  Args:
223
405
  headers: Dictionary of HTTP headers (case-insensitive)
@@ -231,7 +413,8 @@ class AgentContext(BaseModel):
231
413
  "X-Tenant-Id": "acme-corp",
232
414
  "X-Session-Id": "sess-456",
233
415
  "X-Model-Name": "anthropic:claude-opus-4-20250514",
234
- "X-Is-Eval": "true"
416
+ "X-Is-Eval": "true",
417
+ "X-Client-Id": "web"
235
418
  }
236
419
  context = AgentContext.from_headers(headers)
237
420
  """
@@ -249,4 +432,5 @@ class AgentContext(BaseModel):
249
432
  default_model=normalized.get("x-model-name") or settings.llm.default_model,
250
433
  agent_schema_uri=normalized.get("x-agent-schema"),
251
434
  is_eval=is_eval,
435
+ client_id=normalized.get("x-client-id"),
252
436
  )
@@ -4,15 +4,12 @@ Centralized context builder for agent execution.
4
4
  Session History (ALWAYS loaded with compression):
5
5
  - Each chat request is a single message, so session history MUST be recovered
6
6
  - Uses SessionMessageStore with compression to keep context efficient
7
- - Long assistant responses include REM LOOKUP hints: "... [REM LOOKUP session-{id}-msg-{index}] ..."
8
- - Agent can retrieve full content on-demand using REM LOOKUP
9
7
  - Prevents context window bloat while maintaining conversation continuity
10
8
 
11
9
  User Context (on-demand by default):
12
- - System message includes REM LOOKUP hint for user profile
13
- - Agent decides whether to load profile based on query
14
- - More efficient for queries that don't need personalization
15
- - Example: "User: sarah@example.com. To load user profile: Use REM LOOKUP \"sarah@example.com\""
10
+ - System message includes user email for context awareness
11
+ - Fails silently if user not found - agent proceeds without user context
12
+ - Example: "User: sarah@example.com"
16
13
 
17
14
  User Context (auto-inject when enabled):
18
15
  - Set CHAT__AUTO_INJECT_USER_CONTEXT=true
@@ -22,8 +19,8 @@ User Context (auto-inject when enabled):
22
19
  Design Pattern:
23
20
  1. Extract AgentContext from headers (user_id, tenant_id, session_id)
24
21
  2. If auto-inject enabled: Load User/Session from database
25
- 3. If auto-inject disabled: Provide REM LOOKUP hints in system message
26
- 4. Construct system message with date + context (injected or hints)
22
+ 3. If auto-inject disabled: Show user email for context (fail silently if not found)
23
+ 4. Construct system message with date + context
27
24
  5. Return complete context ready for agent execution
28
25
 
29
26
  Integration Points:
@@ -40,11 +37,10 @@ Usage (on-demand, default):
40
37
 
41
38
  # Messages list structure (on-demand):
42
39
  # [
43
- # {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com\nTo load user profile: Use REM LOOKUP \"sarah@example.com\"\nSession ID: sess-123\nTo load session history: Use REM LOOKUP messages?session_id=sess-123"},
40
+ # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser: sarah@example.com"},
44
41
  # {"role": "user", "content": "What's next for the API migration?"}
45
42
  # ]
46
43
 
47
- # Agent receives hints and can decide to load context if needed
48
44
  agent = await create_agent(context=context, ...)
49
45
  prompt = "\n".join(msg.content for msg in messages)
50
46
  result = await agent.run(prompt)
@@ -52,7 +48,7 @@ Usage (on-demand, default):
52
48
  Usage (auto-inject, CHAT__AUTO_INJECT_USER_CONTEXT=true):
53
49
  # Messages list structure (auto-inject):
54
50
  # [
55
- # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser Context (auto-injected):\nSummary: ...\nInterests: ...\n\nSession History (auto-injected, 5 messages):"},
51
+ # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser Context (auto-injected):\nSummary: ...\nInterests: ..."},
56
52
  # {"role": "user", "content": "Previous message"},
57
53
  # {"role": "assistant", "content": "Previous response"},
58
54
  # {"role": "user", "content": "What's next for the API migration?"}
@@ -110,13 +106,11 @@ class ContextBuilder:
110
106
 
111
107
  Session History (ALWAYS loaded with compression):
112
108
  - If session_id provided, session history is ALWAYS loaded using SessionMessageStore
113
- - Compression keeps it efficient with REM LOOKUP hints for long messages
114
- - Example: "... [Message truncated - REM LOOKUP session-{id}-msg-{index}] ..."
115
- - Agent can retrieve full content on-demand using REM LOOKUP
109
+ - Compression keeps context efficient
116
110
 
117
111
  User Context (on-demand by default):
118
- - System message includes REM LOOKUP hint: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
119
- - Agent decides whether to load profile based on query
112
+ - System message includes user email: "User: {email}"
113
+ - Fails silently if user not found - agent proceeds without user context
120
114
 
121
115
  User Context (auto-inject when enabled):
122
116
  - Set CHAT__AUTO_INJECT_USER_CONTEXT=true
@@ -137,9 +131,9 @@ class ContextBuilder:
137
131
 
138
132
  # messages structure:
139
133
  # [
140
- # {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com\nTo load user profile: Use REM LOOKUP \"sarah@example.com\""},
134
+ # {"role": "system", "content": "Today's date: 2025-11-22\n\nUser: sarah@example.com"},
141
135
  # {"role": "user", "content": "Previous message"},
142
- # {"role": "assistant", "content": "Start of long response... [REM LOOKUP session-123-msg-1] ...end"},
136
+ # {"role": "assistant", "content": "Previous response"},
143
137
  # {"role": "user", "content": "New message"}
144
138
  # ]
145
139
  """
@@ -158,6 +152,7 @@ class ContextBuilder:
158
152
  default_model=context.default_model,
159
153
  agent_schema_uri=context.agent_schema_uri,
160
154
  is_eval=context.is_eval,
155
+ client_id=context.client_id,
161
156
  )
162
157
 
163
158
  # Initialize DB if not provided and needed (for user context or session history)
@@ -177,6 +172,10 @@ class ContextBuilder:
177
172
  today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
178
173
  context_hint = f"Today's date: {today}."
179
174
 
175
+ # Add client identifier if present
176
+ if context.client_id:
177
+ context_hint += f"\nClient: {context.client_id}"
178
+
180
179
  # Add user context (auto-inject or on-demand hint)
181
180
  if settings.chat.auto_inject_user_context and context.user_id and db:
182
181
  # Auto-inject: Load and include user profile
@@ -189,18 +188,18 @@ class ContextBuilder:
189
188
  context_hint += f"\n\nUser Context (auto-injected):\n{user_context_content}"
190
189
  else:
191
190
  context_hint += "\n\nNo user context available (anonymous or new user)."
192
- elif context.user_id:
193
- # On-demand: Provide hint to use REM LOOKUP
194
- # user_id is UUID5 hash of email - load user to get email for display and LOOKUP
195
- user_repo = Repository(User, "users", db=db)
196
- user = await user_repo.get_by_id(context.user_id, context.tenant_id)
197
- if user and user.email:
198
- # Show email (more useful than UUID) and LOOKUP hint
199
- context_hint += f"\n\nUser: {user.email}"
200
- context_hint += f"\nTo load user profile: Use REM LOOKUP \"{user.email}\""
201
- else:
202
- context_hint += f"\n\nUser ID: {context.user_id}"
203
- context_hint += "\nUser profile not available."
191
+ elif context.user_id and db:
192
+ # On-demand: Show user email for context (no REM LOOKUP - it requires exact user_id match)
193
+ # Fail silently if user lookup fails - just proceed without user context
194
+ try:
195
+ user_repo = Repository(User, "users", db=db)
196
+ user = await user_repo.get_by_id(context.user_id, context.tenant_id)
197
+ if user and user.email:
198
+ context_hint += f"\n\nUser: {user.email}"
199
+ # If user not found, just proceed without adding user context
200
+ except Exception as e:
201
+ # Fail silently - don't block agent execution if user lookup fails
202
+ logger.debug(f"Could not load user context: {e}")
204
203
 
205
204
  # Add system context hint
206
205
  messages.append(ContextMessage(role="system", content=context_hint))
@@ -217,11 +216,26 @@ class ContextBuilder:
217
216
  )
218
217
 
219
218
  # Convert to ContextMessage format
219
+ # For tool messages, wrap content with clear markers so the agent
220
+ # can see previous tool results when the prompt is concatenated
220
221
  for msg_dict in session_history:
222
+ role = msg_dict["role"]
223
+ content = msg_dict.get("content")
224
+
225
+ # Skip messages with null/empty content (common in tool messages)
226
+ if content is None or content == "":
227
+ logger.debug(f"Skipping {role} message with null/empty content")
228
+ continue
229
+
230
+ if role == "tool":
231
+ # Wrap tool results with clear markers for visibility
232
+ tool_name = msg_dict.get("tool_name", "unknown")
233
+ content = f"[TOOL RESULT: {tool_name}]\n{content}\n[/TOOL RESULT]"
234
+
221
235
  messages.append(
222
236
  ContextMessage(
223
- role=msg_dict["role"],
224
- content=msg_dict["content"],
237
+ role=role,
238
+ content=content,
225
239
  )
226
240
  )
227
241
 
@@ -308,6 +322,7 @@ class ContextBuilder:
308
322
  session_id: str | None = None,
309
323
  message: str = "Hello",
310
324
  model: str | None = None,
325
+ client_id: str | None = None,
311
326
  ) -> tuple[AgentContext, list[ContextMessage]]:
312
327
  """
313
328
  Build context for testing (no database lookup).
@@ -315,7 +330,7 @@ class ContextBuilder:
315
330
  Creates minimal context with:
316
331
  - Test user (test@rem.ai)
317
332
  - Test tenant
318
- - Context hint with date
333
+ - Context hint with date and client
319
334
  - Single user message
320
335
 
321
336
  Args:
@@ -324,6 +339,7 @@ class ContextBuilder:
324
339
  session_id: Optional session ID
325
340
  message: User message content
326
341
  model: Optional model override
342
+ client_id: Optional client identifier (e.g., "cli", "test")
327
343
 
328
344
  Returns:
329
345
  Tuple of (AgentContext, messages list)
@@ -331,7 +347,8 @@ class ContextBuilder:
331
347
  Example:
332
348
  context, messages = await ContextBuilder.build_from_test(
333
349
  user_id="test@rem.ai",
334
- message="What's the weather like?"
350
+ message="What's the weather like?",
351
+ client_id="cli"
335
352
  )
336
353
  """
337
354
  from ..settings import settings
@@ -342,11 +359,15 @@ class ContextBuilder:
342
359
  tenant_id=tenant_id,
343
360
  session_id=session_id,
344
361
  default_model=model or settings.llm.default_model,
362
+ client_id=client_id,
345
363
  )
346
364
 
347
365
  # Build minimal messages
348
366
  today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
349
- context_hint = f"Today's date: {today}.\n\nTest user context: {user_id} (test mode, no profile loaded)."
367
+ context_hint = f"Today's date: {today}."
368
+ if client_id:
369
+ context_hint += f"\nClient: {client_id}"
370
+ context_hint += f"\n\nTest user context: {user_id} (test mode, no profile loaded)."
350
371
 
351
372
  messages = [
352
373
  ContextMessage(role="system", content=context_hint),
@@ -116,7 +116,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
116
116
  the artificial MCP distinction between tools and resources.
117
117
 
118
118
  Supports both:
119
- - Concrete URIs: "rem://schemas" -> tool with no parameters
119
+ - Concrete URIs: "rem://agents" -> tool with no parameters
120
120
  - Template URIs: "patient-profile://field/{field_key}" -> tool with field_key parameter
121
121
 
122
122
  Args:
@@ -131,7 +131,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
131
131
 
132
132
  Example:
133
133
  # Concrete URI -> no-param tool
134
- tool = create_resource_tool("rem://schemas", "List all agent schemas")
134
+ tool = create_resource_tool("rem://agents", "List all agent schemas")
135
135
 
136
136
  # Template URI -> parameterized tool
137
137
  tool = create_resource_tool("patient-profile://field/{field_key}", "Get field definition", mcp_server=mcp)