Skip to content

fix: handle non-serializable objects in agent canvas SSE and state se…#14210

Open
RazmikGevorgyan wants to merge 1 commit intoinfiniflow:mainfrom
RazmikGevorgyan:fix/agent-canvas-serialization
Open

fix: handle non-serializable objects in agent canvas SSE and state se…#14210
RazmikGevorgyan wants to merge 1 commit intoinfiniflow:mainfrom
RazmikGevorgyan:fix/agent-canvas-serialization

Conversation

@RazmikGevorgyan
Copy link
Copy Markdown
Contributor

@RazmikGevorgyan RazmikGevorgyan commented Apr 18, 2026

…rialization

Agent components (llm.py, agent_with_tools.py, message.py) store functools.partial objects as deferred streaming handles in their output slots. When the canvas state gets serialized for SSE events, Redis commits, or logging, these partials — plus non-copyable objects like Langfuse clients — crash json.dumps and deepcopy.

Changes:

  • canvas_app.py: add default=str to json.dumps for SSE event serialization (lines 238, 296)
  • canvas.py: wrap deepcopy calls in try/except to handle non-copyable objects (Langfuse clients, etc.), add default=str to final json.dumps
  • base.py: add default=str to ComponentParamBase.str to handle non-serializable objects in component parameters

Closes #14229

What problem does this PR solve?

Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR.

Type of change

  • Bug Fix (non-breaking change which fixes an issue)
  • New Feature (non-breaking change which adds functionality)
  • Documentation Update
  • Refactoring
  • Performance Improvement
  • Other (please describe):

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Apr 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c4d5004-47ff-4286-a41d-ad1e37b1a548

📥 Commits

Reviewing files that changed from the base of the PR and between 8faee2d and 734efd5.

📒 Files selected for processing (3)
  • agent/canvas.py
  • agent/component/base.py
  • api/apps/canvas_app.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • agent/component/base.py
  • api/apps/canvas_app.py

📝 Walkthrough

Walkthrough

This PR makes JSON serialization more resilient by adding custom default handlers to json.dumps() in three modules and by guarding deepcopy of DSL/component entries with try/except, falling back to original references on failure.

Changes

Cohort / File(s) Summary
Canvas graph serialization
agent/canvas.py
Guards deepcopy for DSL entries and component fields (excluding obj) with try/except; logs warnings and uses the original reference on failure. Uses json.dumps(..., default=_serialize_default) and handles obj fields via json.loads(str(...)) for final serialization.
Component param stringification
agent/component/base.py
ComponentParamBase.__str__ now calls json.dumps(..., ensure_ascii=False, default=_serialize_default), converting callables to None and falling back to str(obj) for other unsupported types while logging warnings.
SSE payload serialization
api/apps/canvas_app.py
Adds internal _canvas_json_default(obj) helper and passes it as default to json.dumps(...) in SSE (sse()/generate()) code paths so non-serializable objects (e.g., callables) serialize safely instead of raising.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇
I nibble at tangled deepcopy thread,
turn odd callables into blossoms instead.
Safe JSON hops across the night,
no crashes now — just gentle light. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: handling non-serializable objects in agent canvas SSE and state serialization across multiple files.
Description check ✅ Passed The PR description includes required sections (problem statement, type of change) with sufficient detail about what the PR solves and its implementation approach.

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


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
agent/canvas.py (1)

138-138: Prefer deterministic fallback values over raw str(obj) in persisted canvas snapshots.

At Line 138, default=str may store unstable repr strings (e.g., memory-address-bearing object representations) into committed DSL state. Consider a controlled serializer (e.g., None or typed placeholder) for unsupported runtime-only objects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent/canvas.py` at line 138, Replace the unstable default=str fallback used
in the json.dumps call (json.dumps(dsl, ensure_ascii=False, default=str)) with a
deterministic serializer function that returns controlled placeholders for
unsupported runtime-only objects (for example None or a dict like {"__type__":
type(obj).__name__} or {"__placeholder__": "<type>"}); implement a small helper
(e.g., fallback_serializer(obj)) and pass it as default=fallback_serializer so
persisted canvas snapshots never contain unpredictable reprs from objects in
dsl.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent/canvas.py`:
- Around line 122-125: Add logging inside the deepcopy fallback branches so
failures are not silently swallowed: before assigning the original reference in
the except blocks, call the module/class logger (e.g., logger.exception or
logger.warning) to record the key k, a representation and type of self.dsl[k]
(e.g., repr(self.dsl[k]) and type(self.dsl[k]).__name__) and the caught
exception; apply the same change to both occurrences around the deepcopy of
self.dsl[k] (the try/except that assigns dsl[k]) so the component/key/type and
stacktrace are logged prior to falling back to the original reference.

In `@api/apps/canvas_app.py`:
- Line 238: The SSE payload currently uses json.dumps(..., default=str) which
coerces arbitrary objects to opaque strings and breaks client expectations;
replace that by ensuring the emitted ans is JSON-serializable without
default=str: implement a sanitizer/encoder (e.g., sanitize_for_json or
_sse_json_default) and use it when producing the SSE payload where yield "data:"
+ json.dumps(ans, ensure_ascii=False, ...) is called (the generator that yields
SSE lines in canvas_app.py); the sanitizer should recursively convert known
non-serializable types to safe representations (e.g., datetimes -> ISO strings,
UUIDs -> str, bytes -> base64) and raise or omit unsupported types so callers
can notice errors, then call json.dumps(ans_sanitized, ensure_ascii=False) (or
json.dumps(ans, default=_sse_json_default) if your default only handles specific
types and raises TypeError for others) instead of using default=str.

---

Nitpick comments:
In `@agent/canvas.py`:
- Line 138: Replace the unstable default=str fallback used in the json.dumps
call (json.dumps(dsl, ensure_ascii=False, default=str)) with a deterministic
serializer function that returns controlled placeholders for unsupported
runtime-only objects (for example None or a dict like {"__type__":
type(obj).__name__} or {"__placeholder__": "<type>"}); implement a small helper
(e.g., fallback_serializer(obj)) and pass it as default=fallback_serializer so
persisted canvas snapshots never contain unpredictable reprs from objects in
dsl.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 569bf9e2-debd-4366-bfb0-a50f06de42db

📥 Commits

Reviewing files that changed from the base of the PR and between 6712b50 and 907efe0.

📒 Files selected for processing (3)
  • agent/canvas.py
  • agent/component/base.py
  • api/apps/canvas_app.py

Comment thread agent/canvas.py
Comment thread api/apps/canvas_app.py Outdated
@RazmikGevorgyan RazmikGevorgyan force-pushed the fix/agent-canvas-serialization branch from 907efe0 to 8faee2d Compare April 18, 2026 10:40
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Apr 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent/canvas.py`:
- Around line 140-144: The current _serialize_default used in Graph.__str__
raises TypeError for non-callable non-JSON-serializable objects, which still
lets json.dumps crash; change _serialize_default (the function used as the
default= handler in the json.dumps(...) call) to return a safe string
representation (e.g., str(obj) or repr(obj)) instead of raising, while still
returning None for callables if you want them as null; this ensures json.dumps
will not raise and Graph.__str__ will always produce a serializable output.

In `@api/apps/canvas_app.py`:
- Around line 24-35: Update _canvas_json_default to log a warning before
returning None for callables: import or obtain a module logger (e.g.
logging.getLogger(__name__)) and call logger.warning with context including the
object's type and a short repr (e.g. f"Coercing callable to None in canvas SSE
payload: type=%s repr=%s", type(obj).__name__, repr(obj)) right before the
"return None" line in _canvas_json_default so that callable-to-None coercions
are recorded without changing behavior for non-callables.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94bcec32-4ce8-4f14-90da-23ff8bd92d8e

📥 Commits

Reviewing files that changed from the base of the PR and between 907efe0 and 8faee2d.

📒 Files selected for processing (3)
  • agent/canvas.py
  • agent/component/base.py
  • api/apps/canvas_app.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • agent/component/base.py

Comment thread agent/canvas.py
Comment thread api/apps/canvas_app.py
Comment on lines +24 to +35
def _canvas_json_default(obj):
"""Fallback serializer for canvas SSE events.

Agent components store functools.partial objects as deferred streaming
handles (see llm.py, agent_with_tools.py, message.py). These leak into
SSE event dicts via component input/output propagation and are not
JSON-serializable. This handler converts them to None so that downstream
consumers never receive opaque ``str(partial(...))`` representations.
"""
if callable(obj):
return None
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
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 | 🟡 Minor

Log callable-to-None coercions in _canvas_json_default.

This new fallback path currently drops data silently. Add a warning before coercion so production traceability is preserved when payload fields disappear.

🔧 Proposed fix
 def _canvas_json_default(obj):
@@
     if callable(obj):
+        logging.warning(
+            "canvas_app: SSE JSON fallback coerced callable type=%s to None",
+            type(obj).__name__,
+        )
         return None
     raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

As per coding guidelines, **/*.py: Add logging for new flows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/apps/canvas_app.py` around lines 24 - 35, Update _canvas_json_default to
log a warning before returning None for callables: import or obtain a module
logger (e.g. logging.getLogger(__name__)) and call logger.warning with context
including the object's type and a short repr (e.g. f"Coercing callable to None
in canvas SSE payload: type=%s repr=%s", type(obj).__name__, repr(obj)) right
before the "return None" line in _canvas_json_default so that callable-to-None
coercions are recorded without changing behavior for non-callables.

…rialization

Agent components (llm.py, agent_with_tools.py, message.py) store
functools.partial objects as deferred streaming handles in their
output slots. When the canvas state gets serialized for SSE events,
Redis commits, or logging, these partials — plus non-copyable objects
like Langfuse clients — crash json.dumps and deepcopy.

Reproduced with Azure OpenAI (gpt-5.4-nano via LiteLLM) and Langfuse
tracing enabled on agent workflows.

Changes:
- canvas_app.py: add _canvas_json_default() that converts callables
  to None (not str) to preserve client JSON contracts, re-raises for
  all other non-serializable types
- canvas.py: wrap deepcopy calls in try/except with logging.warning
  so failures are diagnosable, add callable→None serializer for
  final json.dumps
- base.py: add callable→None serializer to ComponentParamBase.__str__

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@RazmikGevorgyan RazmikGevorgyan force-pushed the fix/agent-canvas-serialization branch from 8faee2d to 734efd5 Compare April 18, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant