Skip to content

Propagate multi-user context (user_id, namespace_id, session_id) to Evolve MCP server#194

Open
gjt-prog wants to merge 11 commits into
mainfrom
feat/evolve-multi-user-integration
Open

Propagate multi-user context (user_id, namespace_id, session_id) to Evolve MCP server#194
gjt-prog wants to merge 11 commits into
mainfrom
feat/evolve-multi-user-integration

Conversation

@gjt-prog

@gjt-prog gjt-prog commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

ALTK-Evolve's MCP server already supports multi-user parameters (user_id, namespace_id, session_id) across get_guidelines, save_trajectory, and get_entities. However, the CUGA agent never sends these values — all Evolve interactions appear as "default" user with no tenant or session context.
Root causes:

  1. event_stream() in server/main.py receives user_id from the authenticated request but never assigns it to local_state.user_id
  2. CugaLiteState lacks a user_id field, so the subgraph cannot propagate user identity
  3. EvolveIntegration.get_guidelines() and save_trajectory() don't accept or forward any context parameters

Changes

File Change
server/main.py Populate local_state.user_id = user_id after service_scope is set
cuga_lite_graph.py Add user_id: Optional[str] to CugaLiteState shared keys
evolve/integration.py Extend get_guidelines() and save_trajectory() with optional user_id, namespace_id, session_id
cuga_lite_graph.py Pass context from CugaLiteState to get_guidelines() call
cuga_lite_node.py Pass context from AgentState to save_trajectory() calls
tests/test_integration.py 4 new tests for parameter propagation and backward compatibility

Behavior

  • Retrieval stays broadget_guidelines does not hard-filter by user/session; context is logged for attribution only
  • Writes are scopedsave_trajectory stamps user_id, namespace_id, session_id into entity metadata
  • Fully backward-compatible — all new parameters default to None and are omitted from the MCP payload when not set

Testing

29/29 unit tests pass (25 existing + 4 new).

Summary by CodeRabbit

  • New Features

    • Caller identity (user, namespace, session) is now captured and forwarded to the Evolve integration for richer attribution; identifiers are normalized and omitted when unset. Trajectory saves remain conditional and may run asynchronously per configuration.
    • Per-request user context is applied so downstream processing receives caller identifiers.
  • Tests

    • Added unit and integration tests verifying multi-user context propagation, defaults, and correct inclusion/omission of identifiers.

Review Change Stack

visahak and others added 2 commits April 29, 2026 15:53
…) to Evolve MCP

- Fix event_stream to populate local_state.user_id from auth context
- Add user_id field to CugaLiteState for subgraph propagation
- Extend EvolveIntegration.get_guidelines() and save_trajectory() signatures
- Wire call sites in cuga_lite_graph.py and cuga_lite_node.py
- Add 4 new tests for parameter propagation and backward compat (29/29 pass)
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d00264e-6381-470e-a6ca-d817f2265ad1

📥 Commits

Reviewing files that changed from the base of the PR and between a358bd6 and 848e4cb.

📒 Files selected for processing (3)
  • src/cuga/backend/evolve/integration.py
  • src/cuga/backend/server/main.py
  • tests/unit/test_server_user_id_propagation.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/cuga/backend/server/main.py
  • src/cuga/backend/evolve/integration.py
  • tests/unit/test_server_user_id_propagation.py

📝 Walkthrough

Walkthrough

Adds optional per-request user identifiers to state docs and threads normalized user_id, namespace_id, and session_id through memory, Evolve integration (get_guidelines/save_trajectory), graph/node saving (async or awaited), server propagation helper, and tests.

Changes

Multi-User Context in Evolve Integration

Layer / File(s) Summary
CugaLiteState and tests
src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py, src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/*
Class docstring and inline comment document CugaLiteState.user_id; tests validate construction, defaulting, updates, and service_scope/thread interactions.
Evolve integration & memory
src/cuga/backend/evolve/integration.py, src/cuga/backend/evolve/memory.py, src/cuga/backend/evolve/tests/test_integration.py
Added normalize_evolve_identifier; get_guidelines() and save_trajectory() accept optional user_id, namespace_id, session_id and conditionally include them in tool payloads; memory wiring passes normalized identifiers; integration tests assert payload contents.
Graph/node Evolve calls & tests
src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py, src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_node.py, src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/*
Graph retrieval injects guidelines with multi-user args; callback_node extracts identifiers from state and calls save_trajectory() (async background task or awaited) with keyword identifiers; added end-to-end and callback tests for presence/absence of identifiers.
Server propagation & unit tests
src/cuga/backend/server/main.py, tests/unit/test_server_user_id_propagation.py
Introduced apply_request_user_context used by event_stream() to assign authenticated user_id into local_state and populate service_scope; unit tests validate propagation, None handling, overwrites, and preservation of thread_id.

Sequence Diagram

sequenceDiagram
    actor User
    participant Server as Server (event_stream)
    participant Graph as CugaLiteGraph (build / inject)
    participant Node as CugaLiteNode (callback_node)
    participant Evolve as EvolveIntegration

    User->>Server: authenticated request
    Server->>Graph: initialize local_state with user_id/thread_id/service_scope
    Graph->>Evolve: get_guidelines(task, user_id, namespace_id, session_id)
    Evolve-->>Graph: guidelines response
    Node->>Evolve: save_trajectory(trajectory_data, task_id, user_id=..., namespace_id=..., session_id=...)
    Evolve-->>Node: confirm saved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • sami-marreed

Poem

🐰 I stitched a user thread through code,
From request to graph the pathway flowed,
Guidelines recall the caller's name,
Trajectories saved with context to claim,
Hop, patch, and trace — attribution glowed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: propagating multi-user context parameters to the Evolve MCP server, which is the primary focus across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/evolve-multi-user-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cuga/backend/server/main.py (1)

1177-1181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist user_id into the checkpoint on resume flows.

When resume is truthy, run_stream(..., state=None, resume=resume) ignores the patched local_state, so this assignment never reaches the graph state for resumed/HITL conversations. Older threads will still hit Evolve without user_id.

Suggested fix
     if local_state:
         from cuga.config import get_service_instance_id, get_tenant_id

         local_state.service_scope = {"tenant_id": get_tenant_id(), "instance_id": get_service_instance_id()}
         local_state.user_id = user_id  # Propagate authenticated user into graph state
+        if resume and thread_id:
+            run_agent.graph.update_state(
+                {"configurable": {"thread_id": thread_id}},
+                {
+                    "service_scope": local_state.service_scope,
+                    "user_id": local_state.user_id,
+                },
+            )
         if os.getenv("CUGA_DEMO_MODE") == "health" and not local_state.pi:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/server/main.py` around lines 1177 - 1181, The patched
local_state.user_id isn't propagated when resume flows are used because
run_stream is invoked with state=None; fix by ensuring the resume checkpoint
carries the same fields you set on local_state: when resume is truthy, copy
service_scope and user_id into the resume/checkpoint object (e.g., set
resume.checkpoint.service_scope = {"tenant_id": get_tenant_id(), "instance_id":
get_service_instance_id()} and resume.checkpoint.user_id = user_id) or
alternatively call run_stream with state=local_state instead of None; update the
logic around local_state, run_stream(..., state=...), and the resume/checkpoint
handling so resumed/HITL conversations include user_id.
🧹 Nitpick comments (1)
src/cuga/backend/evolve/tests/test_integration.py (1)

153-177: ⚡ Quick win

Add one resume-path regression test for context propagation.

These tests only prove EvolveIntegration shapes its args correctly. They would not catch event_stream(..., resume=...) dropping user_id before Evolve is reached, which is the easiest place for this feature to regress.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/evolve/tests/test_integration.py` around lines 153 - 177,
Add a regression test that exercises the resume-path context propagation by
calling event_stream(..., resume=...) with user_id/namespace_id/session_id set
and asserting EvolveIntegration._call_tool receives those fields; specifically,
in src/cuga/backend/evolve/tests/test_integration.py add an async test (patching
EvolveIntegration._call_tool as AsyncMock and settings similar to existing
tests) that calls event_stream(...,
resume={"user_id":"user-1","namespace_id":"tenant-a","session_id":"thread-99"})
or otherwise simulates the resume flow and then verifies
mock_call_tool.assert_called_once_with("get_guidelines", {"task": "...",
"user_id":"user-1","namespace_id":"tenant-a","session_id":"thread-99"}) so any
regression that drops user context in event_stream's resume handling is caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/cuga/backend/server/main.py`:
- Around line 1177-1181: The patched local_state.user_id isn't propagated when
resume flows are used because run_stream is invoked with state=None; fix by
ensuring the resume checkpoint carries the same fields you set on local_state:
when resume is truthy, copy service_scope and user_id into the resume/checkpoint
object (e.g., set resume.checkpoint.service_scope = {"tenant_id":
get_tenant_id(), "instance_id": get_service_instance_id()} and
resume.checkpoint.user_id = user_id) or alternatively call run_stream with
state=local_state instead of None; update the logic around local_state,
run_stream(..., state=...), and the resume/checkpoint handling so resumed/HITL
conversations include user_id.

---

Nitpick comments:
In `@src/cuga/backend/evolve/tests/test_integration.py`:
- Around line 153-177: Add a regression test that exercises the resume-path
context propagation by calling event_stream(..., resume=...) with
user_id/namespace_id/session_id set and asserting EvolveIntegration._call_tool
receives those fields; specifically, in
src/cuga/backend/evolve/tests/test_integration.py add an async test (patching
EvolveIntegration._call_tool as AsyncMock and settings similar to existing
tests) that calls event_stream(...,
resume={"user_id":"user-1","namespace_id":"tenant-a","session_id":"thread-99"})
or otherwise simulates the resume flow and then verifies
mock_call_tool.assert_called_once_with("get_guidelines", {"task": "...",
"user_id":"user-1","namespace_id":"tenant-a","session_id":"thread-99"}) so any
regression that drops user context in event_stream's resume handling is caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8df50321-736b-47c9-9b55-6498a79845ea

📥 Commits

Reviewing files that changed from the base of the PR and between ce02fb4 and 3f8d0dd.

📒 Files selected for processing (5)
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_node.py
  • src/cuga/backend/evolve/integration.py
  • src/cuga/backend/evolve/tests/test_integration.py
  • src/cuga/backend/server/main.py

@sami-marreed sami-marreed marked this pull request as draft May 12, 2026 12:51
gjt-prog added 3 commits May 15, 2026 13:44
Resolved conflict in cuga_lite_graph.py by:
- Keeping filesystem/sandbox tools setup from main
- Using refactored Evolve integration (build_evolve_special_instructions_extension)
- Removing old EvolveIntegration.get_guidelines() implementation
@gjt-prog gjt-prog marked this pull request as ready for review May 28, 2026 16:50
@gjt-prog

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit please review again

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@gjt-prog Sure, I'll re-review the changes in PR #194 now!

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py (1)

74-76: ⚡ Quick win

Remove the duplicate user_id field declaration.

user_id is declared twice in CugaLiteState (Line 74 and Line 76). Keeping both invites drift in a shared state contract.

Proposed cleanup
-    user_id: Optional[str] = None
-    thread_id: Optional[str] = None
-    user_id: Optional[str] = None  # Shared with AgentState; None means unset (no user context sent to Evolve)
+    user_id: Optional[str] = None  # Shared with AgentState; None means unset (no user context sent to Evolve)
+    thread_id: Optional[str] = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py` around lines
74 - 76, CugaLiteState currently declares user_id twice; remove the redundant
declaration so there is a single Optional[str] user_id field and keep thread_id
as-is; update the class definition by deleting the duplicated "user_id:
Optional[str] = None" entry (reference symbol: CugaLiteState and the field name
user_id) to avoid drift in the shared state contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_node.py`:
- Around line 397-421: state.user_id is being forwarded directly into
_evolve_user_id and persisted via EvolveIntegration.save_trajectory, causing
sentinel placeholders like "default" to be saved; change the assignment logic
that sets _evolve_user_id so that if state.user_id equals known sentinel values
(e.g., the string "default" or other configured placeholders) it becomes None
instead of being forwarded, then pass that sanitized _evolve_user_id into
EvolveIntegration.save_trajectory (both async and await branches); also update
the related test test_callback_node_handles_missing_multi_user_params to assert
user_id is None rather than "default".

In `@src/cuga/backend/evolve/memory.py`:
- Around line 47-49: The current extraction of state IDs blindly forwards
sentinel values (e.g., "default") to Evolve; normalize placeholders by
converting known sentinels and empty values to None before calling Evolve
GUIDELINES. Update the variables _evolve_user_id, _evolve_namespace_id and
_evolve_session_id (the values derived from state.user_id, (state.service_scope
or {}).get('tenant_id'), and state.thread_id) to check for common sentinels like
"default", "anonymous", "unknown", empty string, or other project-specific
placeholders and set them to None when matched so Evolve doesn’t treat
placeholders as real identifiers.

In `@tests/unit/test_server_user_id_propagation.py`:
- Around line 8-135: Add a new unit test that calls event_stream (the function
that performs the real assignments) instead of directly mutating AgentState;
mock the agent/graph dependencies and patch cuga.config.get_tenant_id and
get_service_instance_id to known values, provide a simulated authenticated
user_id (e.g. via the same variable name user_id or request context used in
production), run event_stream to produce/emitted state, and assert the
returned/emitted AgentState (or the event payload) has user_id ==
"authenticated-user-123" and service_scope["tenant_id"] == "tenant-456" and
service_scope["instance_id"] == "instance-789". Ensure the test references
event_stream and AgentState so regressions in the real assignment are caught.

---

Nitpick comments:
In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py`:
- Around line 74-76: CugaLiteState currently declares user_id twice; remove the
redundant declaration so there is a single Optional[str] user_id field and keep
thread_id as-is; update the class definition by deleting the duplicated
"user_id: Optional[str] = None" entry (reference symbol: CugaLiteState and the
field name user_id) to avoid drift in the shared state contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9db9e906-f712-4078-a219-5ed50a11806f

📥 Commits

Reviewing files that changed from the base of the PR and between dd5165e and 0f247f2.

📒 Files selected for processing (10)
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_node.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_cuga_lite_graph_evolve_guidelines.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_cuga_lite_node.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_cuga_lite_state.py
  • src/cuga/backend/evolve/integration.py
  • src/cuga/backend/evolve/memory.py
  • src/cuga/backend/evolve/tests/test_integration.py
  • src/cuga/backend/server/main.py
  • tests/unit/test_server_user_id_propagation.py

Comment thread src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_node.py Outdated
Comment thread src/cuga/backend/evolve/memory.py Outdated
Comment on lines +47 to +49
_evolve_user_id = getattr(state, 'user_id', None)
_evolve_namespace_id = (getattr(state, 'service_scope', {}) or {}).get('tenant_id') or None
_evolve_session_id = getattr(state, 'thread_id', None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize placeholder IDs before calling Evolve guidelines.

If state.user_id carries a sentinel like "default", this code forwards it as a real user identifier. That can misattribute guidelines context across unrelated callers.

Proposed fix
-            _evolve_user_id = getattr(state, 'user_id', None)
-            _evolve_namespace_id = (getattr(state, 'service_scope', {}) or {}).get('tenant_id') or None
-            _evolve_session_id = getattr(state, 'thread_id', None)
+            raw_user_id = str(getattr(state, "user_id", "") or "").strip()
+            _evolve_user_id = None if raw_user_id in {"", "default"} else raw_user_id
+            _evolve_namespace_id = (
+                str((getattr(state, "service_scope", {}) or {}).get("tenant_id") or "").strip() or None
+            )
+            _evolve_session_id = str(getattr(state, "thread_id", "") or "").strip() or None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_evolve_user_id = getattr(state, 'user_id', None)
_evolve_namespace_id = (getattr(state, 'service_scope', {}) or {}).get('tenant_id') or None
_evolve_session_id = getattr(state, 'thread_id', None)
raw_user_id = str(getattr(state, "user_id", "") or "").strip()
_evolve_user_id = None if raw_user_id in {"", "default"} else raw_user_id
_evolve_namespace_id = (
str((getattr(state, "service_scope", {}) or {}).get("tenant_id") or "").strip() or None
)
_evolve_session_id = str(getattr(state, "thread_id", "") or "").strip() or None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/evolve/memory.py` around lines 47 - 49, The current
extraction of state IDs blindly forwards sentinel values (e.g., "default") to
Evolve; normalize placeholders by converting known sentinels and empty values to
None before calling Evolve GUIDELINES. Update the variables _evolve_user_id,
_evolve_namespace_id and _evolve_session_id (the values derived from
state.user_id, (state.service_scope or {}).get('tenant_id'), and
state.thread_id) to check for common sentinels like "default", "anonymous",
"unknown", empty string, or other project-specific placeholders and set them to
None when matched so Evolve doesn’t treat placeholders as real identifiers.

Comment on lines +8 to +135
def test_local_state_user_id_is_set_from_authenticated_user():
"""Verify that local_state.user_id is set from the authenticated user_id."""
# Create a mock state
local_state = AgentState(
input="test query",
url="https://example.com",
)

# Simulate the user_id assignment from main.py
user_id = "authenticated-user-123"
local_state.user_id = user_id

assert local_state.user_id == "authenticated-user-123"


def test_local_state_service_scope_is_set_with_tenant_and_instance():
"""Verify that local_state.service_scope is set with tenant_id and instance_id."""
local_state = AgentState(
input="test query",
url="https://example.com",
)

# Simulate the service_scope assignment from main.py
with patch("cuga.config.get_tenant_id", return_value="tenant-456"):
with patch("cuga.config.get_service_instance_id", return_value="instance-789"):
from cuga.config import get_service_instance_id, get_tenant_id

local_state.service_scope = {
"tenant_id": get_tenant_id(),
"instance_id": get_service_instance_id(),
}

assert local_state.service_scope["tenant_id"] == "tenant-456"
assert local_state.service_scope["instance_id"] == "instance-789"


def test_local_state_user_id_and_service_scope_set_together():
"""Verify that both user_id and service_scope are set correctly together."""
local_state = AgentState(
input="test query",
url="https://example.com",
)

user_id = "authenticated-user-123"

with patch("cuga.config.get_tenant_id", return_value="tenant-456"):
with patch("cuga.config.get_service_instance_id", return_value="instance-789"):
from cuga.config import get_service_instance_id, get_tenant_id

# Simulate the assignments from main.py (lines 1245-1247)
local_state.user_id = user_id
local_state.service_scope = {
"tenant_id": get_tenant_id(),
"instance_id": get_service_instance_id(),
}
local_state.user_id = user_id # Duplicate assignment as in main.py

# Verify both are set correctly
assert local_state.user_id == "authenticated-user-123"
assert local_state.service_scope["tenant_id"] == "tenant-456"
assert local_state.service_scope["instance_id"] == "instance-789"


def test_local_state_user_id_duplicate_assignment_does_not_cause_issues():
"""Verify that the duplicate user_id assignment (lines 1245 and 1247) doesn't cause issues."""
local_state = AgentState(
input="test query",
url="https://example.com",
)

user_id = "authenticated-user-123"

# First assignment (line 1245)
local_state.user_id = user_id
assert local_state.user_id == "authenticated-user-123"

# Second assignment (line 1247) - should not cause any issues
local_state.user_id = user_id
assert local_state.user_id == "authenticated-user-123"


def test_local_state_handles_none_user_id():
"""Verify that local_state handles None user_id gracefully."""
local_state = AgentState(
input="test query",
url="https://example.com",
)

user_id = None
local_state.user_id = user_id

assert local_state.user_id is None


def test_local_state_multi_user_context_complete():
"""Verify complete multi-user context is set correctly for Evolve integration."""
local_state = AgentState(
input="test query",
url="https://example.com",
thread_id="thread-999",
)

user_id = "authenticated-user-123"

with patch("cuga.config.get_tenant_id", return_value="tenant-456"):
with patch("cuga.config.get_service_instance_id", return_value="instance-789"):
from cuga.config import get_service_instance_id, get_tenant_id

local_state.user_id = user_id
local_state.service_scope = {
"tenant_id": get_tenant_id(),
"instance_id": get_service_instance_id(),
}

# Verify all multi-user context fields are set for Evolve
assert local_state.user_id == "authenticated-user-123"
assert local_state.thread_id == "thread-999"
assert local_state.service_scope["tenant_id"] == "tenant-456"

# These values would be passed to EvolveIntegration
evolve_user_id = local_state.user_id or None
evolve_namespace_id = (local_state.service_scope or {}).get("tenant_id") or None
evolve_session_id = local_state.thread_id or None

assert evolve_user_id == "authenticated-user-123"
assert evolve_namespace_id == "tenant-456"
assert evolve_session_id == "thread-999"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Tests don’t exercise event_stream, so they won’t catch propagation regressions.

These tests only set AgentState fields directly; they never call event_stream where the real assignment occurs. If local_state.user_id = user_id is removed from production code, this suite would still pass.

Please add at least one test that invokes event_stream (with mocked agent/graph and dependencies) and asserts the emitted/updated state contains expected user_id and service_scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_server_user_id_propagation.py` around lines 8 - 135, Add a
new unit test that calls event_stream (the function that performs the real
assignments) instead of directly mutating AgentState; mock the agent/graph
dependencies and patch cuga.config.get_tenant_id and get_service_instance_id to
known values, provide a simulated authenticated user_id (e.g. via the same
variable name user_id or request context used in production), run event_stream
to produce/emitted state, and assert the returned/emitted AgentState (or the
event payload) has user_id == "authenticated-user-123" and
service_scope["tenant_id"] == "tenant-456" and service_scope["instance_id"] ==
"instance-789". Ensure the test references event_stream and AgentState so
regressions in the real assignment are caught.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cuga/backend/evolve/integration.py (1)

57-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize identifiers inside EvolveIntegration before building the payload.

These public methods still forward raw truthy values. A caller that passes "default_user" or whitespace will still attribute data to a fake identity, which defeats the new omission contract. Normalize user_id, namespace_id, and session_id here so every call path gets the same behavior.

Proposed fix
     async def get_guidelines(
         cls,
         task: str,
         user_id: Optional[str] = None,
         namespace_id: Optional[str] = None,
         session_id: Optional[str] = None,
     ) -> Optional[str]:
         """Fetch guidelines from Evolve for the given task description."""
         if not cls.is_enabled():
             return None
         try:
+            user_id = normalize_evolve_identifier(user_id)
+            namespace_id = normalize_evolve_identifier(namespace_id)
+            session_id = normalize_evolve_identifier(session_id)
             args: dict = {"task": task}
             if user_id:
                 args["user_id"] = user_id
             if namespace_id:
                 args["namespace_id"] = namespace_id
@@
     async def save_trajectory(
         cls,
         chat_messages: List[BaseMessage],
         task_id: str,
         success: bool,
         user_id: Optional[str] = None,
         namespace_id: Optional[str] = None,
         session_id: Optional[str] = None,
     ) -> None:
         """Save the agent trajectory to Evolve for tip generation."""
         if not cls.is_enabled():
             return
@@
         try:
+            user_id = normalize_evolve_identifier(user_id)
+            namespace_id = normalize_evolve_identifier(namespace_id)
+            session_id = normalize_evolve_identifier(session_id)
             logger.debug(
                 f"Evolve: Converting {len(chat_messages)} chat_messages. "
                 f"Types: {[type(m).__name__ for m in chat_messages[:10]]}"
             )
             openai_messages = cls._convert_messages(chat_messages)

Also applies to: 143-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/evolve/integration.py` around lines 57 - 75, In
EvolveIntegration.get_guidelines normalize the identifier inputs before building
args: for user_id, namespace_id, and session_id call .strip() and if the result
is empty or equals "default_user" (or other sentinel strings used by your app)
set the variable to None so you don't forward fake/whitespace identities; then
only add keys to args when the normalized value is not None. Apply the same
normalization logic to the other public EvolveIntegration payload-building
method(s) referenced around lines 143-187 (where cls._call_tool is used) so all
call paths behave identically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/test_server_user_id_propagation.py`:
- Line 35: The function apply_request_user_context currently types its user_id
parameter as str but is called with None in tests; update its signature to
accept Optional[str] (import Optional from typing) and adjust any internal logic
and return type annotations to handle None safely (e.g., preserve
AgentState.user_id typing and guards where apply_request_user_context sets or
reads user_id); also update any callers or type hints that assume a non-None
string so they handle Optional[str] consistently.
- Around line 28-39: The test
test_apply_request_user_context_handles_none_user_id is missing an assertion for
service_scope["instance_id"]; update that test to assert
local_state.service_scope["instance_id"] == "instance-789" after calling
apply_request_user_context on the AgentState (local_state) with None user_id
while patching get_tenant_id and get_service_instance_id so the test checks both
tenant_id and instance_id are populated consistently with the other tests.

---

Outside diff comments:
In `@src/cuga/backend/evolve/integration.py`:
- Around line 57-75: In EvolveIntegration.get_guidelines normalize the
identifier inputs before building args: for user_id, namespace_id, and
session_id call .strip() and if the result is empty or equals "default_user" (or
other sentinel strings used by your app) set the variable to None so you don't
forward fake/whitespace identities; then only add keys to args when the
normalized value is not None. Apply the same normalization logic to the other
public EvolveIntegration payload-building method(s) referenced around lines
143-187 (where cls._call_tool is used) so all call paths behave identically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59efc7af-8745-4f5f-844e-0e2c216555b0

📥 Commits

Reviewing files that changed from the base of the PR and between 0f247f2 and a358bd6.

📒 Files selected for processing (7)
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_node.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_cuga_lite_node.py
  • src/cuga/backend/evolve/integration.py
  • src/cuga/backend/evolve/memory.py
  • src/cuga/backend/server/main.py
  • tests/unit/test_server_user_id_propagation.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/cuga/backend/server/main.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_cuga_lite_node.py
  • src/cuga/backend/evolve/memory.py

Comment thread tests/unit/test_server_user_id_propagation.py
Comment thread tests/unit/test_server_user_id_propagation.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants