Skip to content

Conversation

@LinZhihao-723
Copy link
Member

@LinZhihao-723 LinZhihao-723 commented Jan 27, 2026

Description

This PR adds serde support for TaskGraph, and further adds helpers for serializing/deserializing a task graph in JSON format.

Under the hood, the task graph is serialized into a list of TaskDescriptor. The descriptor contains the necessary information to insert a task into the graph. The API for adding new tasks to TaskGraph has also been updated to accept a TaskDescriptor.
When deserializing from a serializable task graph, it replays the task insertion by feeding the serialized TaskDescriptor into the task graph in order.

The serde is implemented using low-level APIs for two reasons:

  • It enables streaming serialization for task descriptors.
  • It allows us to prioritize schema version check before throwing any other errors. In particular, we deserialize everything into results and compare versions before dispatching any other deserialization errors. In this way, if a schema-breaking change is introduced in a future release, the older version of the serialized task graph will have a clear error message showing the version is not compatible.

In addition to the task descriptor, we also serialize a task graph schema version as a part of the serialized task graph. This schema version determines whether the current build of spider-core supports a serialized task graph. In the future, this schema version may be used to differentiate which serializable task graph format to choose at runtime.

The test cases for serde are added under the tests folder (not as in-place unit tests). This is to better simulate how the serde feature is used at the user level, ensuring that the top-level API works when the underlying serde helpers are private to the user.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  • Ensure all workflows pass.
  • Add basic test cases to verify the serde behavior of the task graph.

Summary by CodeRabbit

  • New Features

    • TaskGraph can be serialized/deserialized to JSON and MessagePack with schema-version compatibility checks.
    • Task creation simplified via a TaskDescriptor-based API; provides a get_num_tasks() accessor.
  • Chores

    • Added dependencies to support versioning and (de)serialization.
  • Tests

    • Added comprehensive serde round-trip and schema-validation tests (JSON/MessagePack and error cases).

✏️ Tip: You can customize this high-level summary in your review settings.

@LinZhihao-723 LinZhihao-723 marked this pull request as ready for review January 27, 2026 17:36
@LinZhihao-723 LinZhihao-723 requested review from a team and sitaowang1998 as code owners January 27, 2026 17:36
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Walkthrough

Adds JSON/MessagePack (de)serialization and schema-version compatibility checks to TaskGraph, introduces a public TaskDescriptor type and a TaskGraph insert_task API that accepts it, derives serde traits for related types, updates tests for round-trip I/O, and adds three new Cargo dependencies.

Changes

Cohort / File(s) Summary
Cargo dependencies
components/spider-core/Cargo.toml
Added dependencies: semver = "1.0.27", strum = "0.27.2", strum_macros = "0.27.2".
Task graph implementation
components/spider-core/src/task/task_graph.rs
Major addition of serde-based (de)serialization for TaskGraph (JSON & MessagePack) with schema_version and compatibility checks; added public TaskDescriptor type; changed insert_task to accept TaskDescriptor; derived/added Serialize/Deserialize on several types; added to_json/from_json and to_msgpack/from_msgpack; added schema constants and LazyLock-based VersionReq; added get_num_tasks() and private serialized payload types; visitor-based deserializer enforces insertion order.
Tests: serde round-trip
components/spider-core/tests/test_task_graph_serde.rs
New tests for JSON and MessagePack round-trip, schema version validity/incompatibility cases, invalid/missing fields, and insertion-order preservation during deserialization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding serialization/deserialization support for TaskGraph, which is the primary objective of this pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

Copy link
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

🤖 Fix all issues with AI agents
In `@components/spider-core/Cargo.toml`:
- Around line 12-14: Remove the unused once_cell dependency declaration from
Cargo.toml (delete the once_cell = "1.21.3" entry) because the codebase uses
std::sync::LazyLock instead; then run cargo check/build to ensure no remaining
references to once_cell exist and keep the semver dependency as-is for version
validation.

In `@components/spider-core/src/task/task_graph.rs`:
- Around line 221-225: The doc comment for the field describing positional
inputs refers to `positional_inputs` but the actual field is named `inputs`;
update the documentation to use the correct field name `inputs` (e.g., change
"The vector length must match the length of `positional_inputs`." to "The vector
length must match the length of `inputs`.") so the comment around the field (in
task_graph.rs) accurately references the `inputs` field and retains the existing
explanation about Some(sources)/None semantics and length requirement.
🧹 Nitpick comments (2)
components/spider-core/src/task/task_graph.rs (2)

501-503: Preallocate serialized_tasks.

This avoids reallocation churn for larger graphs.

♻️ Suggested tweak
-        let mut serialized_tasks = Vec::new();
+        let mut serialized_tasks = Vec::with_capacity(self.tasks.len());

946-948: Drop the debug print to keep tests quiet.

This looks like leftover debugging noise.

🧹 Suggested cleanup
-        let json_serialized = serde_json::to_string_pretty(&graph).expect("shouldn't fail");
-        println!("{json_serialized}");

Copy link
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

🤖 Fix all issues with AI agents
In `@components/spider-core/Cargo.toml`:
- Line 12: Remove the unused once_cell dependency from the crate by deleting the
once_cell = "1.21.3" entry in Cargo.toml; ensure no code references once_cell
(the code uses std::sync::LazyLock) and run cargo check to verify compilation
still succeeds after removing the once_cell dependency declaration.

In `@components/spider-core/src/task/task_graph.rs`:
- Around line 200-228: The documentation for TaskDescriptor's input_sources
incorrectly references `positional_inputs`; update the comment to reference the
actual field name `inputs` instead (or use `inputs` consistently) so the
sentence reads that the vector length must match the length of `inputs`; ensure
this change is applied to the doc comment near the `input_sources` field in the
`TaskDescriptor` struct.
🧹 Nitpick comments (1)
components/spider-core/src/task/task_graph.rs (1)

945-946: Remove debug println! statement from test.

This appears to be a debugging artifact. Consider removing it or, if the output is valuable, using eprintln! within a conditional or a separate debug test.

🧹 Suggested fix
-        let json_serialized = serde_json::to_string_pretty(&graph).expect("shouldn't fail");
-        println!("{json_serialized}");
-

Comment on lines 573 to 586
let serializable = SerializableTaskGraph::deserialize(deserializer)?;
let schema_version = Version::parse(&serializable.schema_version).map_err(|error| {
serde::de::Error::custom(format!(
"invalid schema version string '{}': {}",
serializable.schema_version, error
))
})?;

if !TASK_GRAPH_SCHEMA_COMPATIBLE_VERSION_REQUIREMENT.matches(&schema_version) {
return Err(serde::de::Error::custom(format!(
"incompatible task graph schema version: found {}, compatible requirements: {}",
serializable.schema_version, TASK_GRAPH_SCHEMA_COMPATIBLE_VERSION
)));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we have mismatched schema version and also a change in the format, most likely the SerializableTaskGraph::deserialize will fail and user will get an error that is not incompatible schema version.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. But as the motivation explained in the PR description, I think this version validation is still necessary. It is possible that our version "0.1.0" is a subset of "0.2.0", and if so, this check becomes necessary because:

  • We don't reject unknown fields (but technically we could do it. But without enforcing unknown fields, users can add their own metadata to a task descriptor and it won't affect our serde).
  • If a graph of "0.2.0" only uses features in "0.1.0" (for example, we add channel support in 0.2.0 but it's not used, the task graph will still be serializable in 0.1.0), it should still be invalidated by the deserializer.

To formally support multiple versions when we have different variants of schemas, we can make the actual task graph representations an enum. In that case, all possible schemas will be serializable, and this version validation becomes much more reliable. But I'm not sure if that's what we will do eventually... maintaining all supported schemas in a large enum sounds insane. We can have a selective set of it)

Copy link
Collaborator

Choose a reason for hiding this comment

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

What I meant is not about ditching version check. The problem is if we have a breaking change from version 0.1.0 to0.2.0, and user sends a 0.1.0 serialized task graph to 0.2.0 backend. The API server should report a version mismatch, but instead if fails to deserialize the task graph and report a deserialize error.

To make the versioning work, usually the first step is to not deserialize the whole message, but just check the version. This is a bit more verbose in implementation, especially with json, but it is more reliable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I understand. I do think it's worth fixing.
I've rewritten the serde in a more low-level way to ensure this behavior. It's worth doing since this also allows streaming serialization for task descriptors.

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@components/spider-core/tests/test_task_graph_serde.rs`:
- Around line 70-83: Update the test_missing_fields test to stop asserting on
the error string and instead pattern-match the returned error from
TaskGraph::from_json; specifically, in the Err arm match
Error::SerdeJsonError(e) and call e.is_missing_field()
(serde_json::Error::is_missing_field) to verify the missing-field case, and for
any other Err variant panic with "unexpected error variant"; leave the other
test (lines ~97–104) unchanged or consider adding a custom error variant later
since de::Error::custom() cannot be checked via is_missing_field().
🧹 Nitpick comments (1)
components/spider-core/tests/test_task_graph_serde.rs (1)

108-476: Consider moving the large JSON fixture to a dedicated test fixture file.

It will be easier to edit and review without scrolling past hundreds of lines of inline JSON.

Comment on lines +70 to +83
#[test]
fn test_missing_fields() {
let expected_fields = vec!["schema_version", "tasks"];
for field in expected_fields {
let mut task_graph: serde_json::Value = serde_json::from_str(TASK_GRAPH_IN_JSON).unwrap();
task_graph
.as_object_mut()
.expect("task graph should be an object")
.remove(field);
match TaskGraph::from_json(&task_graph.to_string()) {
Ok(_) => panic!("deserialization should fail"),
Err(err) => assert!(err.to_string().contains("missing field")),
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the TaskGraph implementation and from_json method
cd components/spider-core || exit
find . -name "*.rs" -type f | head -20

Repository: y-scope/spider

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Search for TaskGraph definition and from_json implementation
cd components/spider-core || exit
rg -n "impl.*TaskGraph|fn from_json" --type=rust -A 5

Repository: y-scope/spider

Length of output: 2881


🏁 Script executed:

#!/bin/bash
# Look for error type definitions
cd components/spider-core || exit
rg -n "enum.*Error|struct.*Error|type.*Error" --type=rust | head -30

Repository: y-scope/spider

Length of output: 198


🏁 Script executed:

#!/bin/bash
# Check the test file and the specific lines mentioned
cd components/spider-core || exit
cat -n tests/test_task_graph_serde.rs | sed -n '65,110p'

Repository: y-scope/spider

Length of output: 1823


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
cat -n src/task.rs | head -50

Repository: y-scope/spider

Length of output: 1152


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
rg -n "pub enum Error" -A 20 --type=rust

Repository: y-scope/spider

Length of output: 930


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if serde_json::Error is used elsewhere with structured matching
rg -n "serde_json::Error|\.is_io\(\)|\.is_syntax\(\)|\.is_eof\(\)|\.is_missing_field\(\)" --type=rust

Repository: y-scope/spider

Length of output: 175


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if there's a custom error handling or validation layer
rg -n "custom error|Error::|impl.*From" --type=rust -A 3 | head -60

Repository: y-scope/spider

Length of output: 3949


🏁 Script executed:

#!/bin/bash
# Check if serde_json::Error has methods for structured checking
python3 << 'EOF'
import subprocess
result = subprocess.run(['rg', '-n', 'is_io|is_syntax|is_eof|is_missing_field|is_duplicate_field|classification', '--type=rust'], 
                       capture_output=True, text=True, cwd='components/spider-core')
print(result.stdout)
print("---STDERR---")
print(result.stderr)
EOF

Repository: y-scope/spider

Length of output: 144


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check what serde_json version is being used
rg -n "serde_json" Cargo.toml

Repository: y-scope/spider

Length of output: 83


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check the actual error message that gets generated for missing fields
# by looking at the visitor implementation more carefully
cat -n src/task/task_graph.rs | sed -n '708,750p'

Repository: y-scope/spider

Length of output: 2003


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if there are any other error handling patterns in the codebase
rg -n "Error::SerdeJsonError|\.classification\(\)" --type=rust -A 2 -B 2

Repository: y-scope/spider

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for alternative error matching approaches in Rust projects
# Check if serde_json provides classification or is_* methods
python3 << 'EOF'
# Looking up serde_json 1.0.149 documentation
# Let's check if the error type is accessible and if it has classification methods
import subprocess
result = subprocess.run(['rg', '-n', 'classification|is_io|is_syntax|is_eof', '--type=rust'], 
                       capture_output=True, text=True, cwd='components/spider-core')
print("Results:", result.stdout)
if result.stdout == "":
    print("No usage of error classification methods found in codebase")
EOF

Repository: y-scope/spider

Length of output: 126


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if there's a custom deserializer or error handler that could be used
rg -n "struct TaskGraphVisitor|impl.*Visitor" -A 30 --type=rust | head -80

Repository: y-scope/spider

Length of output: 2404


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if there are imports from serde_json in the test file
rg -n "use serde_json|use std::error|use thiserror" tests/test_task_graph_serde.rs

Repository: y-scope/spider

Length of output: 40


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Look at the complete test file to understand context better
wc -l tests/test_task_graph_serde.rs

Repository: y-scope/spider

Length of output: 92


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if any other tests use structured error matching or serde_json error methods
rg -n "Error::|is_io|is_syntax|matches!" tests/test_task_graph_serde.rs

Repository: y-scope/spider

Length of output: 40


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check serde_json error classification and available methods
cat -n src/task/task_graph.rs | sed -n '240,260p'

Repository: y-scope/spider

Length of output: 795


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check how serde_json::Error is handled in practice - check if classification exists
rg -n "serde_json::error|Error::SerdeJsonError" --type=rust -B 2 -A 5

Repository: y-scope/spider

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Let's check the serde_json documentation for available error methods
# by searching for any Rust documentation or checking what's available in 1.0.149
python3 << 'EOF'
# According to serde_json 1.0.x documentation, serde_json::Error should have:
# - is_io() 
# - is_syntax()
# - is_eof()
# - is_missing_field() (added in 1.0.75)
# - is_duplicate_field() (added in 1.0.104)
# - classification() (returns a Classification enum)

# Let's verify if the code can access these methods
print("serde_json 1.0.149 provides the following error classification methods:")
print("- is_io(): checks if error is an I/O error")
print("- is_syntax(): checks if error is a syntax error")
print("- is_eof(): checks if error is EOF")
print("- is_missing_field(): checks if error is a missing field (serde deserialization)")
print("- is_duplicate_field(): checks if error is a duplicate field")
print("- classification(): returns Classification enum with Io, Syntax, or Eof variants")
print("\nThese allow structured error matching without string matching!")
EOF

Repository: y-scope/spider

Length of output: 543


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Verify these methods would work with the Error type wrapping serde_json::Error
# Create a simple test to see if we can match the error variant
cat > /tmp/test_error_matching.rs << 'RUST'
// This demonstrates that we can pattern match on Error::SerdeJsonError
// and then call is_missing_field() on the inner serde_json::Error

match some_result {
    Ok(_) => panic!("should fail"),
    Err(Error::SerdeJsonError(e)) => {
        assert!(e.is_missing_field());  // This is what the review suggests
    },
    Err(_) => panic!("unexpected error"),
}
RUST
cat /tmp/test_error_matching.rs

Repository: y-scope/spider

Length of output: 419


🏁 Script executed:

#!/bin/bash
cd components/spider-core || exit
# Check if there's a way to distinguish custom errors vs missing field errors
cat -n src/task/task_graph.rs | sed -n '718,730p'

Repository: y-scope/spider

Length of output: 713


🏁 Script executed:

#!/bin/bash
# Verify the serde_json Error API available in 1.0.149
python3 << 'EOF'
# serde_json::Error in 1.0.149 has the classification() method
# which returns a Classification enum with variants: Io, Syntax, or Eof
# However, serde-json doesn't expose a variant for custom deserialization errors

# The two test cases are:
# 1. missing field - uses de::Error::missing_field() -> creates serde_json error with is_missing_field() = true
# 2. incompatible version - uses de::Error::custom() -> creates serde_json error (no specific method to check)

# For case 2, we'd still need string matching or a custom error type wrapper

print("Analysis:")
print("Test 1 (lines 70-83): Missing field errors")
print("  Current: err.to_string().contains('missing field')")
print("  Better: if let Error::SerdeJsonError(e) = err { assert!(e.is_missing_field()); }")
print("")
print("Test 2 (lines 97-104): Custom error message")
print("  Current: err.to_string().contains('incompatible task graph schema version')")
print("  No structured variant for custom errors in serde_json")
print("  Would need custom error type or continue with string matching")
EOF

Repository: y-scope/spider

Length of output: 489


Use serde_json::Error::is_missing_field() instead of string matching for missing field errors (lines 70–83).

For the test at lines 70–83, replace err.to_string().contains("missing field") with a structured check after pattern matching on Error::SerdeJsonError:

match TaskGraph::from_json(&task_graph.to_string()) {
    Ok(_) => panic!("deserialization should fail"),
    Err(Error::SerdeJsonError(e)) => assert!(e.is_missing_field()),
    Err(_) => panic!("unexpected error variant"),
}

For the test at lines 97–104, the error is created via de::Error::custom(), which doesn't expose a structured variant in serde_json. Consider introducing a custom error type with variants (e.g., SchemaMismatch) or accept continued string matching for custom validation errors.

🤖 Prompt for AI Agents
In `@components/spider-core/tests/test_task_graph_serde.rs` around lines 70 - 83,
Update the test_missing_fields test to stop asserting on the error string and
instead pattern-match the returned error from TaskGraph::from_json;
specifically, in the Err arm match Error::SerdeJsonError(e) and call
e.is_missing_field() (serde_json::Error::is_missing_field) to verify the
missing-field case, and for any other Err variant panic with "unexpected error
variant"; leave the other test (lines ~97–104) unchanged or consider adding a
custom error variant later since de::Error::custom() cannot be checked via
is_missing_field().

Copy link
Member Author

Choose a reason for hiding this comment

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

We converted the error type to our own error, so we are missing the details of what type of json error it is.

Copy link
Contributor

Choose a reason for hiding this comment

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

@LinZhihao-723, thank you for the clarification! You're right—if your Error type wraps the serde_json::Error but you don't expose methods to inspect the underlying error category, string matching on the error message is the practical approach.

My suggestion to use is_missing_field() assumed direct access to the serde_json::Error methods, which doesn't align with your error handling design. String matching is a reasonable solution in this case. 👍

Comment on lines +101 to +102
err.to_string()
.contains("incompatible task graph schema version")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Using error message to determine the error type is a code smell. Can we use a custom error enum?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a bit tricky. The type of the source error is a trait, and we had to throw the error as de::Error::custom. And this error is flattened into spider_core::task::Error::RmpSerdeDecodeError or spider_core::task::Error::SerdeJsonError without preserving the original error type info. There's indeed a reason to do so: at the user level, I don't think it matters what exact type of error it is when the given task graph fails to deserialize; in most cases, this is out of scope for the program to handle at runtime. Printing the error in the log is sufficient, so all we need is to preserve enough information in the error message. That said, I wouldn't make the error enum to differentiate all deserialization failure cases since it creates unnecessary maintenance effort where the benefit isn't obvious.

@LinZhihao-723 LinZhihao-723 merged commit 4d01e85 into y-scope:main Jan 27, 2026
7 checks passed
@LinZhihao-723 LinZhihao-723 deleted the task-graph-serde branch January 27, 2026 23:54
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