Contributing
Code Standards
Conventions, patterns, and processes for contributing to acton-ai.
Rust coding conventions
General style
- Edition 2021. All code targets Rust edition 2021.
- No external error crates. The project deliberately avoids
thiserror,anyhow, andeyre. All error types are hand-written to keep the public API explicit and dependency-light. - No external derive macros for errors. Error types manually implement
Display,Debug,Clone,PartialEq,Eq, andstd::error::Error. #[must_use]on constructors and query methods. Any function that returns a value the caller should not silently discard is annotated with#[must_use].- Builder pattern for configuration. Public-facing configuration uses the builder pattern (
ActonAIBuilder,ConversationBuilder,PromptBuilder). Builders consumeselfand returnSelffor chaining. - Derive
Clonegenerously. Configuration types, error types, and message types all deriveClone. This aligns with the actor model where messages are sent by value.
Actor conventions
- Actor state structs use the
#[acton_actor]derive macro. - Actor messages use the
#[acton_message]derive macro, which requiresAny + Send + Sync + Debug + Clone + 'static. In practice,#[derive(Clone, Debug)]on your message struct is sufficient since the blanket impl covers the rest. - The public field for user state in actor handlers is
actor.model. Reply::pending(async move { ... })blocks the mailbox -- use it intentionally to serialize message processing. Usetokio::spawninsideReply::pendingif you need to release the mailbox while awaiting.- Keep handler closures focused. Extract complex logic into standalone
async fnhelpers (as seen inprocess_streaming_requestin the LLM provider).
Naming conventions
- Actor modules follow a consistent structure:
mod.rs(public exports),actor.rs(actor implementation),config.rs(configuration types). - Error types are named
{Module}Errorwith a{Module}ErrorKindenum (e.g.,LLMError/LLMErrorKind). - Message types describe the action or event:
SpawnAgent,LLMStreamToken,InitKernel. - ID types use the
{Entity}Idpattern:AgentId,CorrelationId,TaskId.
Function parameter limits
Group function parameters into structs when a function would take more than 7 arguments. This avoids Clippy's too_many_arguments lint and improves readability.
Error handling patterns
Error type hierarchy
The project uses a consistent pattern across all modules. Each error type is a struct with a kind field containing an enum of specific error variants:
ActonAIError -- High-level facade API errors
ActonAIErrorKind
Configuration
LaunchFailed
PromptFailed
StreamError
ProviderError
RuntimeShutdown
KernelError -- Kernel actor errors
KernelErrorKind
AgentNotFound
SpawnFailed
AgentAlreadyExists
ShuttingDown
InvalidConfig
AgentError -- Agent actor errors
AgentErrorKind
InvalidState
ProcessingFailed
LLMRequestFailed
ToolExecutionFailed
Stopping
InvalidConfig
LLMError -- LLM provider errors
LLMErrorKind
Network
RateLimited
ApiError
AuthenticationFailed
InvalidRequest
StreamError
ParseError
ShuttingDown
InvalidConfig
ModelOverloaded
Timeout
ToolError -- Tool system errors
ToolErrorKind
NotFound
AlreadyRegistered
ExecutionFailed
Timeout
ValidationFailed
SandboxError
ShuttingDown
Internal
MultiAgentError -- Multi-agent coordination errors
MultiAgentErrorKind
AgentNotFound
TaskNotFound
TaskAlreadyAccepted
NoCapableAgent
DelegationFailed
RoutingFailed
PersistenceError -- Database/storage errors
PersistenceErrorKind
DatabaseOpen
SchemaInit
QueryFailed
NotFound
SerializationFailed
EmbeddingError -- Embedding generation errors
Conventions for error types
Every error type follows these rules:
Struct + Kind enum. The struct holds optional context (like an
AgentIdorCorrelationId) plus akindfield. The enum lists all possible error variants with their data.Named constructors. Each variant has a corresponding constructor method on the struct (e.g.,
LLMError::network("connection refused")). Constructors acceptimpl Into<String>for convenience.Query methods. Boolean helpers like
is_retriable(),is_not_found(), andis_shutting_down()make it easy to branch on error types without matching on the kind enum directly.Actionable Display messages. Error messages describe both what happened and what the user can do about it. For example:
"agent 'abc123' not found; verify the agent ID is correct and the agent is running".All standard traits. Every error type implements
Debug,Clone,PartialEq,Eq,Display, andstd::error::Error.Size optimization. Large error kinds (like
ToolErrorKindandPersistenceErrorKind) are boxed inside the error struct to keep theResultsize small.
Using errors in handlers
Actor message handlers use try_mutate_on / try_act_on with typed errors:
builder.try_mutate_on::<LLMRequest, (), LLMError>(|actor, envelope| {
if actor.model.shutting_down {
return Reply::try_err(LLMError::shutting_down());
}
// ... process request ...
Reply::try_ok(())
});
Testing requirements
Test co-location
All unit tests live alongside the code they test, inside #[cfg(test)] mod tests blocks at the bottom of each source file. This is the standard Rust pattern and makes it easy to find tests for any given module.
What to test
- Constructors and defaults. Verify that
Defaultimplementations produce expected values and that builder methods set the right fields. - Error formatting. Every error variant should have a test verifying its
Displayoutput contains the relevant information. - Query methods. Test boolean helpers like
is_retriable()for both positive and negative cases. - Clone and Eq. Verify that error types and message types correctly implement
CloneandPartialEq. - Actor behavior. For actor-specific logic, use
tokio::testwith the actual actor runtime to verify message handling.
Test utilities
tokio-testis available as a dev dependency for async test utilities.tempfileis available for tests that need temporary directories or files.StubEmbeddingProviderandStubSandbox/StubSandboxFactoryare available in test builds for mocking external dependencies.
Running tests
# Full test suite
cargo test
# Tests for a single module
cargo test --lib llm::error
# Single test
cargo test rate_limiter_allows_initial_request
# With output
cargo test -- --nocapture
Documentation standards
Rustdoc
- Every public type, trait, method, and function must have a doc comment.
- Use
///for item-level documentation and//!for module-level documentation. - Include a
# Examplesection withrust,ignorecode blocks for complex APIs. - Use
# Errors,# Panics, and# Argumentssections where applicable. - Link to related types using
[backtick]syntax.
Module-level docs
Each module's mod.rs (or top-level file) should include:
- A one-line summary of the module's purpose
- An
## Architecturesection for modules with internal structure - A
## Usagesection with code examples - Re-exports of the module's public API
Clippy configuration
The project does not use a clippy.toml or .clippy.toml file. All Clippy configuration is implicit via the defaults.
Run Clippy with:
cargo clippy --all-targets --all-features
Clippy policy
The tree is expected to build clean under cargo clippy --all-targets --all-features -- -D warnings. Any existing or new lint should be fixed rather than suppressed.
Do not suppress Clippy warnings
Do not add #[allow(clippy::...)] attributes without discussion. If Clippy flags something in new code, fix it. If you believe a lint is a false positive, raise it in the PR.
Semver policy
Acton-ai follows Semantic Versioning with the pre-1.0 convention:
- Patch (0.25.x): Bug fixes and non-breaking changes
- Minor (0.x.0): Breaking changes (since we are pre-1.0)
- Major: Reserved for 1.0 release
Pre-1.0 breaking changes
While the version is below 1.0, breaking changes bump the minor version. This is standard semver practice for pre-release software. After 1.0, breaking changes will require a major version bump.
Release process
Releases are managed with cargo release:
# Dry run
cargo release patch --dry-run
# Actual release (bumps version, tags, publishes)
cargo release patch
Commits are signed with SSH (-S flag), and tags are signed (tag -s). GitHub verifies signatures automatically.
Commit message conventions
Write commit messages that explain why the change was made, not just what changed. Use the imperative mood in the subject line.
Format
type: short description
Optional longer explanation of why this change was made,
what problem it solves, and any relevant context.
Types
| Type | When to use |
|---|---|
feat | New feature or capability |
fix | Bug fix |
refactor | Code restructuring without behavior change |
docs | Documentation changes |
test | Test additions or modifications |
chore | Build, CI, release, or tooling changes |
perf | Performance improvements |
Breaking changes
Prefix breaking changes with refactor! or feat! (note the !):
refactor!: make Conversation an actor for concurrency safety
PR process
Before submitting
- Run the full check suite:
cargo fmt -- --check
cargo clippy --all-targets --all-features
cargo test
- Verify documentation builds:
cargo doc --no-deps
Keep PRs focused. One logical change per PR. If you find unrelated issues while working, file them separately.
Add tests. New functionality should include tests. Bug fixes should include a regression test.
PR structure
- Title: Short, descriptive, in imperative mood (e.g., "Add retry logic to LLM provider")
- Description: Explain the motivation, approach, and any trade-offs. Reference related issues.
- Test plan: Describe how to verify the change works.
Review expectations
- All Clippy warnings must be resolved (except the two known
derivable_implswarnings) - All tests must pass
- Public API additions should include rustdoc with examples
- Error types should follow the struct + kind enum pattern described above
Next steps
- Development Setup -- get the project building and running
- Architecture Overview -- understand the system design
- Error Handling -- detailed guide to error types and patterns
- Testing -- comprehensive testing guide