Welcome to AgentForge! This guide will help you understand how to contribute to the project effectively.
AgentForge is built on a signal-driven architecture with these core components:
graph TB
Signal[Signal] --> Handler[Handler]
Handler --> Store[Store]
Handler --> Flow[Flow]
Flow --> Runtime[Runtime]
- Functional Approach: Use immutable data structures and pure functions where possible
- Pattern Matching: Prefer pattern matching over conditional logic
- Documentation: All public functions must have
@docand@moduledoccomments - Testing: Maintain test coverage for all new code
- Error Handling: Use tagged tuples (
{:ok, result}or{:error, reason})
Example of good code style:
@doc """
Creates a new primitive that processes signals.
## Examples
iex> my_primitive = create_primitive(opts)
iex> signal = Signal.new(:test, "data")
iex> {{:emit, result}, state} = my_primitive.(signal, %{})
"""
def create_primitive(opts) do
fn signal, state ->
case process(signal, state) do
{:ok, result} -> {{:emit, result}, state}
{:error, reason} -> {{:emit, Signal.new(:error, reason)}, state}
end
end
endWhen adding new primitives:
-
Interface Consistency: Follow the standard primitive interface:
fn signal, state -> {result, new_state} end
-
Result Types: Use these standard result formats:
{:emit, signal}: Single signal emission{:emit_many, signals}: Multiple signal emission{:halt, result}: Stop processing{:wait, reason}: Pause processing{:error, reason}: Error condition
-
State Management: Preserve state immutability and handle updates properly
-
Error Handling: Gracefully handle all error cases
-
Documentation: Include clear examples in documentation
Example of a new primitive:
@doc """
Creates a filter primitive that only passes signals meeting a condition.
## Examples
iex> filter = AgentForge.Primitives.filter(&(&1.data > 10))
iex> signal = Signal.new(:value, 15)
iex> {{:emit, result}, _} = filter.(signal, %{})
"""
def filter(condition) when is_function(condition, 1) do
fn signal, state ->
if condition.(signal) do
{{:emit, signal}, state}
else
{:skip, state}
end
end
end-
Test Coverage: All primitives must have tests for:
- Happy path behavior
- Error conditions
- Edge cases
- State management
-
Test Organization: Use descriptive
describeandtestblocks -
Assertions: Make specific assertions about results
Example test structure:
describe "primitive_name/2" do
test "handles successful case" do
# Setup
primitive = Primitives.create_primitive(opts)
signal = Signal.new(:test, "data")
# Execute
{{:emit, result}, new_state} = primitive.(signal, initial_state)
# Assert
assert result.type == :expected_type
assert result.data == "expected_data"
assert new_state.key == "expected_value"
end
test "handles error cases" do
# Similar structure for error conditions
end
end-
Branch Naming:
feature/for new featuresfix/for bug fixesdocs/for documentation updates
-
Commit Messages: Follow conventional commits format:
type(scope): description [optional body] [optional footer] -
PR Description:
- Clear description of changes
- Reference any related issues
- Include test coverage report
- List breaking changes if any
-
Review Process:
- All PRs require at least one review
- All tests must pass
- Documentation must be updated
- No linter warnings
# Run all tests
mix test
# Run specific test file
mix test test/agent_forge/primitives_test.exs
# Run with coverage
mix test --cover- Open an issue for bugs or feature requests
- Start a discussion for architectural questions
- Tag maintainers for urgent issues
By contributing, you agree to license your work under the project's MIT license.