remdb 0.3.171__py3-none-any.whl → 0.3.180__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.
@@ -161,6 +161,11 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
161
161
  param_desc = ", ".join(template_vars)
162
162
  description = f"{description}\n\nParameters: {param_desc}"
163
163
 
164
+ # Capture mcp_server reference at tool creation time (for closure)
165
+ # This ensures the correct server is used even if called later
166
+ _captured_mcp_server = mcp_server
167
+ _captured_uri = uri # Also capture URI for consistent logging
168
+
164
169
  if template_vars:
165
170
  # Template URI -> create parameterized tool
166
171
  async def wrapper(**kwargs: Any) -> str:
@@ -168,13 +173,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
168
173
  import asyncio
169
174
  import inspect
170
175
 
176
+ logger.debug(f"Resource tool invoked: uri={_captured_uri}, kwargs={kwargs}, mcp_server={'set' if _captured_mcp_server else 'None'}")
177
+
171
178
  # Try to resolve from MCP server's resource templates first
172
- if mcp_server is not None:
179
+ if _captured_mcp_server is not None:
173
180
  try:
174
181
  # Get resource templates from MCP server
175
- templates = await mcp_server.get_resource_templates()
176
- if uri in templates:
177
- template = templates[uri]
182
+ templates = await _captured_mcp_server.get_resource_templates()
183
+ logger.debug(f"MCP server templates: {list(templates.keys())}")
184
+ if _captured_uri in templates:
185
+ template = templates[_captured_uri]
186
+ logger.debug(f"Found template for {_captured_uri}, calling fn with kwargs={kwargs}")
178
187
  # Call the template's underlying function directly
179
188
  # The fn expects the template variables as kwargs
180
189
  fn_result = template.fn(**kwargs)
@@ -184,17 +193,22 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
184
193
  if isinstance(fn_result, str):
185
194
  return fn_result
186
195
  return json.dumps(fn_result, indent=2)
196
+ else:
197
+ logger.warning(f"Template {_captured_uri} not found in MCP server templates: {list(templates.keys())}")
187
198
  except Exception as e:
188
- logger.warning(f"Failed to resolve resource {uri} from MCP server: {e}")
199
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
200
+ else:
201
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
189
202
 
190
203
  # Fallback: substitute template variables and use load_resource
191
- resolved_uri = uri
204
+ resolved_uri = _captured_uri
192
205
  for var in template_vars:
193
206
  if var in kwargs:
194
207
  resolved_uri = resolved_uri.replace(f"{{{var}}}", str(kwargs[var]))
195
208
  else:
196
209
  return json.dumps({"error": f"Missing required parameter: {var}"})
197
210
 
211
+ logger.debug(f"Using fallback load_resource for resolved URI: {resolved_uri}")
198
212
  from rem.api.mcp_router.resources import load_resource
199
213
  result = await load_resource(resolved_uri)
200
214
  if isinstance(result, str):
@@ -208,7 +222,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
208
222
  wrapper.__annotations__ = {var: str for var in template_vars}
209
223
  wrapper.__annotations__['return'] = str
210
224
 
211
- logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars})")
225
+ logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars}, mcp_server={'provided' if mcp_server else 'None'})")
212
226
  else:
213
227
  # Concrete URI -> no-param tool
214
228
  async def wrapper(**kwargs: Any) -> str:
@@ -219,12 +233,16 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
219
233
  if kwargs:
220
234
  logger.warning(f"Resource tool {func_name} called with unexpected kwargs: {list(kwargs.keys())}")
221
235
 
236
+ logger.debug(f"Concrete resource tool invoked: uri={_captured_uri}, mcp_server={'set' if _captured_mcp_server else 'None'}")
237
+
222
238
  # Try to resolve from MCP server's resources first
223
- if mcp_server is not None:
239
+ if _captured_mcp_server is not None:
224
240
  try:
225
- resources = await mcp_server.get_resources()
226
- if uri in resources:
227
- resource = resources[uri]
241
+ resources = await _captured_mcp_server.get_resources()
242
+ logger.debug(f"MCP server resources: {list(resources.keys())}")
243
+ if _captured_uri in resources:
244
+ resource = resources[_captured_uri]
245
+ logger.debug(f"Found resource for {_captured_uri}")
228
246
  # Call the resource's underlying function
229
247
  fn_result = resource.fn()
230
248
  if inspect.iscoroutine(fn_result):
@@ -232,12 +250,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
232
250
  if isinstance(fn_result, str):
233
251
  return fn_result
234
252
  return json.dumps(fn_result, indent=2)
253
+ else:
254
+ logger.warning(f"Resource {_captured_uri} not found in MCP server resources: {list(resources.keys())}")
235
255
  except Exception as e:
236
- logger.warning(f"Failed to resolve resource {uri} from MCP server: {e}")
256
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
257
+ else:
258
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
237
259
 
238
260
  # Fallback to load_resource
261
+ logger.debug(f"Using fallback load_resource for URI: {_captured_uri}")
239
262
  from rem.api.mcp_router.resources import load_resource
240
- result = await load_resource(uri)
263
+ result = await load_resource(_captured_uri)
241
264
  if isinstance(result, str):
242
265
  return result
243
266
  return json.dumps(result, indent=2)
@@ -245,6 +268,6 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
245
268
  wrapper.__name__ = func_name
246
269
  wrapper.__doc__ = description
247
270
 
248
- logger.info(f"Built resource tool: {func_name} (uri: {uri})")
271
+ logger.info(f"Built resource tool: {func_name} (uri: {uri}, mcp_server={'provided' if mcp_server else 'None'})")
249
272
 
250
273
  return Tool(wrapper)
@@ -96,6 +96,35 @@ TODO:
96
96
 
97
97
  Priority: HIGH (blocks production scaling beyond 50 req/sec)
98
98
 
99
+ 4. Response Format Control (structured_output enhancement):
100
+ - Current: structured_output is bool (True=strict schema, False=free-form text)
101
+ - Missing: OpenAI JSON mode (valid JSON without strict schema enforcement)
102
+ - Missing: Completions API support (some models only support completions, not chat)
103
+
104
+ Proposed schema field values for `structured_output`:
105
+ - True (default): Strict structured output using provider's native schema support
106
+ - False: Free-form text response (properties converted to prompt guidance)
107
+ - "json": JSON mode - ensures valid JSON but no schema enforcement
108
+ (OpenAI: response_format={"type": "json_object"})
109
+ - "text": Explicit free-form text (alias for False)
110
+
111
+ Implementation:
112
+ a) Update AgentSchemaMetadata.structured_output type:
113
+ structured_output: bool | Literal["json", "text"] = True
114
+ b) In create_agent(), handle each mode:
115
+ - True: Use output_type with Pydantic model (current behavior)
116
+ - False/"text": Convert properties to prompt guidance (current behavior)
117
+ - "json": Use provider's JSON mode without strict schema
118
+ c) Provider-specific JSON mode:
119
+ - OpenAI: model_settings={"response_format": {"type": "json_object"}}
120
+ - Anthropic: Not supported natively, use prompt guidance
121
+ - Others: Fallback to prompt guidance with JSON instruction
122
+
123
+ Related: Some providers (Cerebras) have completions-only models where
124
+ structured output isn't available. Consider model capability detection.
125
+
126
+ Priority: MEDIUM (enables more flexible output control)
127
+
99
128
  Example Agent Schema:
100
129
  {
101
130
  "type": "object",
@@ -550,70 +579,73 @@ async def create_agent(
550
579
  # Extract schema fields using typed helpers
551
580
  from ..schema import get_system_prompt, get_metadata
552
581
 
553
- # Track whether mcp_servers was explicitly configured (even if empty)
554
- mcp_servers_explicitly_set = False
555
-
556
582
  if agent_schema:
557
583
  system_prompt = get_system_prompt(agent_schema)
558
584
  metadata = get_metadata(agent_schema)
559
- # Check if mcp_servers was explicitly set (could be empty list to disable)
560
- if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers is not None:
561
- mcp_server_configs = [s.model_dump() for s in metadata.mcp_servers]
562
- mcp_servers_explicitly_set = True
563
- else:
564
- mcp_server_configs = []
565
585
  resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
566
586
 
587
+ # DEPRECATED: mcp_servers in agent schemas is ignored
588
+ # MCP servers are now always auto-detected at the application level
589
+ if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers:
590
+ logger.warning(
591
+ "DEPRECATED: mcp_servers in agent schema is ignored. "
592
+ "MCP servers are auto-detected from tools.mcp_server module. "
593
+ "Remove mcp_servers from your agent schema."
594
+ )
595
+
567
596
  if metadata.system_prompt:
568
597
  logger.debug("Using custom system_prompt from json_schema_extra")
569
598
  else:
570
599
  system_prompt = ""
571
600
  metadata = None
572
- mcp_server_configs = []
573
601
  resource_configs = []
574
602
 
575
- # Auto-detect local MCP server if not explicitly configured
576
- # This makes mcp_servers config optional - agents get tools automatically
577
- # But if mcp_servers: [] is explicitly set, respect that (no auto-detection)
578
- if not mcp_server_configs and not mcp_servers_explicitly_set:
579
- import importlib
580
- import os
581
- import sys
582
-
583
- # Ensure current working directory is in sys.path for local imports
584
- cwd = os.getcwd()
585
- if cwd not in sys.path:
586
- sys.path.insert(0, cwd)
587
-
588
- # Try common local MCP server module paths first
589
- auto_detect_modules = [
590
- "tools.mcp_server", # Convention: tools/mcp_server.py
591
- "mcp_server", # Alternative: mcp_server.py in root
592
- ]
593
- for module_path in auto_detect_modules:
594
- try:
595
- mcp_module = importlib.import_module(module_path)
596
- if hasattr(mcp_module, "mcp"):
597
- logger.info(f"Auto-detected local MCP server: {module_path}")
598
- mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
599
- break
600
- except ImportError:
601
- continue
602
-
603
- # Fall back to REM's default MCP server if no local server found
604
- if not mcp_server_configs:
605
- logger.debug("No local MCP server found, using REM default")
606
- mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
603
+ # Auto-detect MCP server at application level
604
+ # Convention: tools/mcp_server.py exports `mcp` FastMCP instance
605
+ # Falls back to REM's built-in MCP server if no local server found
606
+ import importlib
607
+ import os
608
+ import sys
609
+
610
+ # Ensure current working directory is in sys.path for local imports
611
+ cwd = os.getcwd()
612
+ if cwd not in sys.path:
613
+ sys.path.insert(0, cwd)
614
+
615
+ mcp_server_configs = []
616
+ auto_detect_modules = [
617
+ "tools.mcp_server", # Convention: tools/mcp_server.py
618
+ "mcp_server", # Alternative: mcp_server.py in root
619
+ ]
620
+ for module_path in auto_detect_modules:
621
+ try:
622
+ mcp_module = importlib.import_module(module_path)
623
+ if hasattr(mcp_module, "mcp"):
624
+ logger.info(f"Auto-detected local MCP server: {module_path}")
625
+ mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
626
+ break
627
+ except ImportError as e:
628
+ logger.debug(f"MCP server auto-detect: {module_path} not found ({e})")
629
+ continue
630
+ except Exception as e:
631
+ logger.warning(f"MCP server auto-detect: {module_path} failed to load: {e}")
632
+ continue
633
+
634
+ # Fall back to REM's default MCP server if no local server found
635
+ if not mcp_server_configs:
636
+ logger.info("No local MCP server found, using REM default (rem.mcp_server)")
637
+ mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
607
638
 
608
639
  # Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
609
640
  if metadata:
610
641
  temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
611
642
  max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
612
- use_structured_output = metadata.structured_output
643
+ # Use schema-level structured_output if set, otherwise fall back to global setting
644
+ use_structured_output = metadata.structured_output if metadata.structured_output is not None else settings.llm.default_structured_output
613
645
  else:
614
646
  temperature = settings.llm.default_temperature
615
647
  max_iterations = settings.llm.default_max_iterations
616
- use_structured_output = True
648
+ use_structured_output = settings.llm.default_structured_output
617
649
 
618
650
  # Build list of tools - start with built-in tools
619
651
  tools = _get_builtin_tools()
@@ -736,6 +768,7 @@ async def create_agent(
736
768
 
737
769
  # Create tools from collected resource URIs
738
770
  # Pass the loaded MCP server so resources can be resolved from it
771
+ logger.info(f"Creating {len(resource_uris)} resource tools with mcp_server={'set' if loaded_mcp_server else 'None'}")
739
772
  for uri, usage in resource_uris:
740
773
  resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
741
774
  tools.append(resource_tool)
rem/agentic/schema.py CHANGED
@@ -215,12 +215,13 @@ class AgentSchemaMetadata(BaseModel):
215
215
  )
216
216
 
217
217
  # Structured output toggle
218
- structured_output: bool = Field(
219
- default=True,
218
+ structured_output: bool | None = Field(
219
+ default=None,
220
220
  description=(
221
221
  "Whether to enforce structured JSON output. "
222
222
  "When False, the agent produces free-form text and schema properties "
223
- "are converted to prompt guidance instead. Default: True (JSON output)."
223
+ "are converted to prompt guidance instead. "
224
+ "Default: None (uses LLM__DEFAULT_STRUCTURED_OUTPUT setting, which defaults to False)."
224
225
  ),
225
226
  )
226
227
 
@@ -3,6 +3,17 @@ REM tools for agent execution (CLI and API compatible).
3
3
 
4
4
  These tools work in both CLI and API contexts by initializing services on-demand.
5
5
  They wrap the service layer directly, not MCP tools.
6
+
7
+ Core tables (always available):
8
+ - resources: Documents, content chunks, artifacts
9
+ - moments: Temporal narratives extracted from resources (usually user-specific)
10
+ - ontologies: Domain entities with semantic links for further lookups (like a wiki)
11
+
12
+ Other tables (may vary by deployment):
13
+ - users, sessions, messages, files, schemas, feedbacks
14
+
15
+ Note: Not all tables are populated in all systems. Use FUZZY or SEARCH
16
+ to discover what data exists before assuming specific tables have content.
6
17
  """
7
18
 
8
19
  from typing import Any, Literal, cast
@@ -349,6 +349,53 @@ def register_agent_resources(mcp: FastMCP):
349
349
  except Exception as e:
350
350
  return f"# Available Agents\n\nError listing agents: {e}"
351
351
 
352
+ @mcp.resource("rem://agents/{agent_name}")
353
+ def get_agent_schema(agent_name: str) -> str:
354
+ """
355
+ Get a specific agent schema by name.
356
+
357
+ Args:
358
+ agent_name: Name of the agent (e.g., "ask_rem", "agent-builder")
359
+
360
+ Returns:
361
+ Full agent schema as YAML string, or error message if not found.
362
+ """
363
+ import importlib.resources
364
+ import yaml
365
+ from pathlib import Path
366
+
367
+ try:
368
+ # Find packaged agent schemas
369
+ agents_ref = importlib.resources.files("rem") / "schemas" / "agents"
370
+ agents_dir = Path(str(agents_ref))
371
+
372
+ if not agents_dir.exists():
373
+ return f"# Agent Not Found\n\nNo agent schemas directory found."
374
+
375
+ # Search for agent file (try multiple extensions)
376
+ for ext in [".yaml", ".yml", ".json"]:
377
+ # Try exact match first
378
+ agent_file = agents_dir / f"{agent_name}{ext}"
379
+ if agent_file.exists():
380
+ with open(agent_file, "r") as f:
381
+ content = f.read()
382
+ return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
383
+
384
+ # Try recursive search
385
+ matches = list(agents_dir.rglob(f"{agent_name}{ext}"))
386
+ if matches:
387
+ with open(matches[0], "r") as f:
388
+ content = f.read()
389
+ return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
390
+
391
+ # Not found - list available agents
392
+ available = [f.stem for f in agents_dir.rglob("*.yaml")] + \
393
+ [f.stem for f in agents_dir.rglob("*.yml")]
394
+ return f"# Agent Not Found\n\nAgent '{agent_name}' not found.\n\nAvailable agents: {', '.join(sorted(set(available)))}"
395
+
396
+ except Exception as e:
397
+ return f"# Error\n\nError loading agent '{agent_name}': {e}"
398
+
352
399
 
353
400
  def register_file_resources(mcp: FastMCP):
354
401
  """
@@ -501,10 +548,11 @@ async def load_resource(uri: str) -> dict | str:
501
548
  Load an MCP resource by URI.
502
549
 
503
550
  This function is called by the read_resource tool to dispatch to
504
- registered resource handlers.
551
+ registered resource handlers. Supports both regular resources and
552
+ parameterized resource templates (e.g., rem://agents/{agent_name}).
505
553
 
506
554
  Args:
507
- uri: Resource URI (e.g., "rem://schemas", "rem://status")
555
+ uri: Resource URI (e.g., "rem://agents", "rem://agents/ask_rem", "rem://status")
508
556
 
509
557
  Returns:
510
558
  Resource data (dict or string)
@@ -512,9 +560,10 @@ async def load_resource(uri: str) -> dict | str:
512
560
  Raises:
513
561
  ValueError: If URI is invalid or resource not found
514
562
  """
515
- # Create temporary MCP instance with resources
563
+ import inspect
516
564
  from fastmcp import FastMCP
517
565
 
566
+ # Create temporary MCP instance with resources
518
567
  mcp = FastMCP(name="temp")
519
568
 
520
569
  # Register all resources
@@ -523,14 +572,26 @@ async def load_resource(uri: str) -> dict | str:
523
572
  register_file_resources(mcp)
524
573
  register_status_resources(mcp)
525
574
 
526
- # Get resource handlers from MCP internal registry
527
- # FastMCP stores resources in a dict by URI
528
- if hasattr(mcp, "_resources"):
529
- if uri in mcp._resources:
530
- handler = mcp._resources[uri]
531
- if callable(handler):
532
- result = handler()
533
- return result if result else {"error": "Resource returned None"}
534
-
535
- # If not found, raise error
536
- raise ValueError(f"Resource not found: {uri}. Available resources: {list(mcp._resources.keys()) if hasattr(mcp, '_resources') else 'unknown'}")
575
+ # 1. Try exact match in regular resources
576
+ resources = await mcp.get_resources()
577
+ if uri in resources:
578
+ resource = resources[uri]
579
+ result = resource.fn()
580
+ if inspect.iscoroutine(result):
581
+ result = await result
582
+ return result if result else {"error": "Resource returned None"}
583
+
584
+ # 2. Try matching against parameterized resource templates
585
+ templates = await mcp.get_resource_templates()
586
+ for template_uri, template in templates.items():
587
+ params = template.matches(uri)
588
+ if params is not None:
589
+ # Template matched - call function with extracted parameters
590
+ result = template.fn(**params)
591
+ if inspect.iscoroutine(result):
592
+ result = await result
593
+ return result if result else {"error": "Resource returned None"}
594
+
595
+ # 3. Not found - include both resources and templates in error
596
+ available = list(resources.keys()) + list(templates.keys())
597
+ raise ValueError(f"Resource not found: {uri}. Available resources: {available}")
@@ -1,7 +1,7 @@
1
1
  """
2
2
  MCP server creation and configuration for REM.
3
3
 
4
- Design Pattern
4
+ Design Pattern
5
5
  1. Create FastMCP server with tools and resources
6
6
  2. Register tools using @mcp.tool() decorator
7
7
  3. Register resources using resource registration functions
@@ -20,10 +20,30 @@ FastMCP Features:
20
20
  """
21
21
 
22
22
  import importlib.metadata
23
+ from functools import wraps
23
24
 
24
25
  from fastmcp import FastMCP
26
+ from loguru import logger
25
27
 
26
28
  from ...settings import settings
29
+ from .prompts import register_prompts
30
+ from .resources import (
31
+ register_agent_resources,
32
+ register_file_resources,
33
+ register_schema_resources,
34
+ register_status_resources,
35
+ )
36
+ from .tools import (
37
+ ask_rem_agent,
38
+ get_schema,
39
+ ingest_into_rem,
40
+ list_schema,
41
+ read_resource,
42
+ register_metadata,
43
+ save_agent,
44
+ search_rem,
45
+ test_error_handling,
46
+ )
27
47
 
28
48
  # Get package version
29
49
  try:
@@ -174,18 +194,7 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
174
194
  ),
175
195
  )
176
196
 
177
- # Register REM tools
178
- from .tools import (
179
- ask_rem_agent,
180
- get_schema,
181
- ingest_into_rem,
182
- list_schema,
183
- read_resource,
184
- register_metadata,
185
- save_agent,
186
- search_rem,
187
- )
188
-
197
+ # Register core REM tools
189
198
  mcp.tool()(search_rem)
190
199
  mcp.tool()(ask_rem_agent)
191
200
  mcp.tool()(read_resource)
@@ -194,10 +203,13 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
194
203
  mcp.tool()(get_schema)
195
204
  mcp.tool()(save_agent)
196
205
 
206
+ # Register test tool only in development environment (not staging/production)
207
+ if settings.environment not in ("staging", "production"):
208
+ mcp.tool()(test_error_handling)
209
+ logger.debug("Registered test_error_handling tool (dev environment only)")
210
+
197
211
  # File ingestion tool (with local path support for local servers)
198
212
  # Wrap to inject is_local parameter
199
- from functools import wraps
200
-
201
213
  @wraps(ingest_into_rem)
202
214
  async def ingest_into_rem_wrapper(
203
215
  file_uri: str,
@@ -216,18 +228,9 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
216
228
  mcp.tool()(ingest_into_rem_wrapper)
217
229
 
218
230
  # Register prompts
219
- from .prompts import register_prompts
220
-
221
231
  register_prompts(mcp)
222
232
 
223
233
  # Register schema resources
224
- from .resources import (
225
- register_agent_resources,
226
- register_file_resources,
227
- register_schema_resources,
228
- register_status_resources,
229
- )
230
-
231
234
  register_schema_resources(mcp)
232
235
  register_agent_resources(mcp)
233
236
  register_file_resources(mcp)
@@ -1132,3 +1132,82 @@ async def save_agent(
1132
1132
  result["message"] = f"Agent '{name}' saved. Use `/custom-agent {name}` to chat with it."
1133
1133
 
1134
1134
  return result
1135
+
1136
+
1137
+ # =============================================================================
1138
+ # Test/Debug Tools (for development only)
1139
+ # =============================================================================
1140
+
1141
+ @mcp_tool_error_handler
1142
+ async def test_error_handling(
1143
+ error_type: Literal["exception", "error_response", "timeout", "success"] = "success",
1144
+ delay_seconds: float = 0,
1145
+ error_message: str = "Test error occurred",
1146
+ ) -> dict[str, Any]:
1147
+ """
1148
+ Test tool for simulating different error scenarios.
1149
+
1150
+ **FOR DEVELOPMENT/TESTING ONLY** - This tool helps verify that error
1151
+ handling works correctly through the streaming layer.
1152
+
1153
+ Args:
1154
+ error_type: Type of error to simulate:
1155
+ - "success": Returns successful response (default)
1156
+ - "exception": Raises an exception (tests @mcp_tool_error_handler)
1157
+ - "error_response": Returns {"status": "error", ...} dict
1158
+ - "timeout": Delays for 60 seconds (simulates timeout)
1159
+ delay_seconds: Optional delay before responding (0-10 seconds)
1160
+ error_message: Custom error message for error scenarios
1161
+
1162
+ Returns:
1163
+ Dict with test results or error information
1164
+
1165
+ Examples:
1166
+ # Test successful response
1167
+ test_error_handling(error_type="success")
1168
+
1169
+ # Test exception handling
1170
+ test_error_handling(error_type="exception", error_message="Database connection failed")
1171
+
1172
+ # Test error response format
1173
+ test_error_handling(error_type="error_response", error_message="Resource not found")
1174
+
1175
+ # Test with delay
1176
+ test_error_handling(error_type="success", delay_seconds=2)
1177
+ """
1178
+ import asyncio
1179
+
1180
+ logger.info(f"test_error_handling called: type={error_type}, delay={delay_seconds}")
1181
+
1182
+ # Apply delay (capped at 10 seconds for safety)
1183
+ if delay_seconds > 0:
1184
+ await asyncio.sleep(min(delay_seconds, 10))
1185
+
1186
+ if error_type == "exception":
1187
+ # This tests the @mcp_tool_error_handler decorator
1188
+ raise RuntimeError(f"TEST EXCEPTION: {error_message}")
1189
+
1190
+ elif error_type == "error_response":
1191
+ # This tests how the streaming layer handles error status responses
1192
+ return {
1193
+ "status": "error",
1194
+ "error": error_message,
1195
+ "error_code": "TEST_ERROR",
1196
+ "recoverable": True,
1197
+ }
1198
+
1199
+ elif error_type == "timeout":
1200
+ # Simulate a very long operation (for testing client-side timeouts)
1201
+ await asyncio.sleep(60)
1202
+ return {"status": "success", "message": "Timeout test completed (should not reach here)"}
1203
+
1204
+ else: # success
1205
+ return {
1206
+ "status": "success",
1207
+ "message": "Test completed successfully",
1208
+ "test_data": {
1209
+ "error_type": error_type,
1210
+ "delay_applied": delay_seconds,
1211
+ "timestamp": str(asyncio.get_event_loop().time()),
1212
+ },
1213
+ }
rem/api/routers/auth.py CHANGED
@@ -30,14 +30,17 @@ Access Control Flow (send-code):
30
30
  │ ├── Yes → Check user.tier
31
31
  │ │ ├── tier == BLOCKED → Reject "Account is blocked"
32
32
  │ │ └── tier != BLOCKED → Allow (send code, existing users grandfathered)
33
- │ └── No (new user) → Check EMAIL__TRUSTED_EMAIL_DOMAINS
34
- │ ├── Setting configureddomain in trusted list?
35
- │ ├── Yes Create user & send code
36
- │ └── NoReject "Email domain not allowed for signup"
37
- └── Not configured (empty) → Create user & send code (no restrictions)
33
+ │ └── No (new user) → Check subscriber list first
34
+ │ ├── Email in subscribers table? Allow (create user & send code)
35
+ └── Not a subscriber Check EMAIL__TRUSTED_EMAIL_DOMAINS
36
+ ├── Setting configured → domain in trusted list?
37
+ │ ├── Yes → Create user & send code
38
+ │ │ └── No → Reject "Email domain not allowed for signup"
39
+ │ └── Not configured (empty) → Create user & send code (no restrictions)
38
40
 
39
41
  Key Behaviors:
40
42
  - Existing users: Always allowed to login (unless tier=BLOCKED)
43
+ - Subscribers: Always allowed to login (regardless of email domain)
41
44
  - New users: Must have email from trusted domain (if EMAIL__TRUSTED_EMAIL_DOMAINS is set)
42
45
  - No restrictions: Leave EMAIL__TRUSTED_EMAIL_DOMAINS empty to allow all domains
43
46
 
rem/cli/commands/ask.py CHANGED
@@ -75,7 +75,7 @@ async def run_agent_streaming(
75
75
  """
76
76
  Run agent in streaming mode using agent.iter() with usage limits.
77
77
 
78
- Design Pattern (from carrier):
78
+ Design Pattern:
79
79
  - Use agent.iter() for complete execution with tool call visibility
80
80
  - run_stream() stops after first output, missing tool calls
81
81
  - Stream tool call markers: [Calling: tool_name]