Skip to main content

Core Principles

Purpose: This document defines the fundamental principles that guide Baleybots architecture decisions. These principles were formalized during the v2.0 refactoring and serve as a reference for all future development.

Status: Active - These principles guide current and future development decisions.


Introduction

Baleybots v2.0 represents a deliberate simplification of the codebase without sacrificing functionality. During the refactoring process, we identified core principles that led to removing ~700 lines of code while maintaining 100% of current capabilities.

These principles are not just about the v2.0 refactoring -- they are the foundation for all architectural decisions going forward. When in doubt, refer to these principles to guide your choices.

Key Insight: Migration pain is temporary. Code complexity is forever.


Principles

1. Simplicity Over Compatibility

Statement: Prefer clean breaks and simple code over maintaining backward compatibility when the complexity cost is too high.

Rationale:

  • Backward compatibility adds complexity (feature flags, dual code paths, deprecated field support)
  • Supporting old APIs defeats the purpose of simplification
  • Clean codebases are easier to maintain, debug, and extend
  • During alpha, breaking changes are expected and acceptable

Examples from Refactoring:

  • Removed transport layer: 11 files (~1,200 lines) deleted, replaced with simple fetch override
  • No backward compatibility: v2.0 is a clean break from v1.x
  • Removed deprecated fields: proxyUrl, transport, finalResponseSchema removed entirely
  • Simplified BaseProvider: 200 lines to 60 lines (70% reduction)

When to Apply:

  • When removing complexity significantly improves maintainability
  • During alpha/beta phases (before v1.0 stabilization)
  • When migration is simple enough (mechanical changes, automated codemod available)
  • When the codebase is small and community can migrate quickly
  • Avoid in production releases with large user base
  • Avoid when migration would be prohibitively difficult

Trade-off: Users must migrate, but codebase stays clean and maintainable.


2. Progressive Disclosure

Statement: Simple cases should be simple. Complex cases should be possible. Default to the simplest mental model.

Rationale:

  • 80% of use cases are simple -- don't make them pay for the 20% that need complexity
  • Developers should only need to understand one clear abstraction
  • Complexity should be opt-in, not required

Examples from Refactoring:

  • Simple model config: model: 'gpt-4.1' (uses env vars automatically)
  • Complex model config: model: { id: 'gpt-4.1', provider: { apiKey, baseUrl, fetch } }
  • Simple schema: outputSchema: mySchema
  • Complex schema: outputSchema: { schema: mySchema, strict: true, name: 'MySchema' }
  • Simple fetch: Uses globalThis.fetch by default
  • Complex fetch: Custom fetch wrapper for Tauri, React Native, retries, logging

When to Apply:

  • When designing APIs -- start with the simplest possible interface
  • When adding features -- provide sensible defaults
  • When documenting -- show simple examples first, then advanced
  • Don't hide important configuration behind "magic"
  • Don't make simple cases require understanding complex internals

Trade-off: Some advanced use cases might require more configuration, but the common path is much simpler.


3. Standard Web APIs

Statement: Prefer standard web APIs over custom abstractions. Use composable patterns over registries.

Rationale:

  • Standard APIs are familiar to developers
  • Platform capabilities (fetch, Response, ReadableStream) are well-tested
  • Custom abstractions add indirection and complexity
  • Composable patterns are more flexible than registries

Examples from Refactoring:

  • Replaced transport layer with fetch: 11 files deleted, replaced with config.fetch override
  • Standard Response: Use Response from fetch API instead of custom TransportResponse
  • Composable fetch wrappers: wrapWithRetries, wrapWithLogging, composeFetch instead of transport registry
  • Standard ReadableStream: Use Response.body for streaming instead of custom transport unwrapping

Before (Custom Abstraction):

const transport = new HttpClient({ fetch: tauriFetch });
const bot = Baleybot.create({ transport });

After (Standard API):

const bot = new Baleybot({
model: { id: 'gpt-4.1', provider: { fetch: tauriFetch } }
});

When to Apply:

  • When platform provides a standard API (fetch, Response, ReadableStream)
  • When custom abstraction doesn't add significant value
  • When composable patterns work better than registries
  • Avoid when standard API doesn't meet requirements
  • Avoid when abstraction significantly improves developer experience

Trade-off: Less control over internals, but more familiar and maintainable.


4. Clean Separation of Concerns

Statement: Agent configuration should be separate from provider configuration. Model configuration should be separate from agent behavior.

Rationale:

  • Mixed concerns create confusion about what belongs where
  • Clear boundaries make code easier to understand and maintain
  • Separation enables independent evolution of components
  • Reduces cognitive load -- developers know where to look

Examples from Refactoring:

  • Before: apiKey and baseUrl in BaleybotConfig (mixed agent + provider concerns)
  • After: Agent config (name, goal, tools) separate from model config (model: { id, provider: { apiKey, baseUrl } })
  • Before: finalResponseSchema mixed with provider config
  • After: outputSchema in agent config, provider config in model.provider

Before (Mixed Concerns):

const bot = Baleybot.create({
name: 'bot',
goal: 'Do thing',
finalResponseSchema: schema,
apiKey: 'sk-...', // Provider concern
baseUrl: 'https://...', // Provider concern
model: 'gpt-4.1'
});

After (Separated Concerns):

const bot = new Baleybot({
// Agent configuration
name: 'bot',
goal: 'Do thing',
outputSchema: schema,

// Model configuration (separated)
model: {
id: 'gpt-4.1',
provider: {
apiKey: 'sk-...',
baseUrl: 'https://...'
}
}
});

When to Apply:

  • When designing configuration interfaces
  • When refactoring mixed concerns
  • When adding new features -- ask "does this belong here?"
  • Don't over-separate -- some coupling is natural
  • Don't create artificial boundaries

Trade-off: Slightly more verbose configuration, but much clearer mental model.


5. Unified Naming

Statement: One name per concept. Use clear, descriptive names. Maintain consistent naming across the codebase.

Rationale:

  • Multiple names for the same concept create confusion
  • Consistent naming reduces cognitive load
  • Clear names are self-documenting
  • Easier to search and understand codebase

Examples from Refactoring:

  • Schema naming: Unified to outputSchema (was finalResponseSchema, responseSchema, schema, output)
  • Tool schema naming: Unified to inputSchema (was parameters, schema)
  • Event naming: StreamEvent to BaleybotStreamEvent (clearer, more descriptive)
  • Config naming: ProviderConfig (not ProviderOptions, ProviderSettings)

Before (Inconsistent):

// 4 different names for the same concept
finalResponseSchema?: Schema;
responseSchema?: Schema;
schema?: Schema;
output?: Schema;

// Tool definitions
parameters?: JsonSchema; // JSON Schema tools
schema?: ZodType; // Zod tools

After (Unified):

// One name everywhere
outputSchema?: Schema | StructuredOutputConfig;

// Tool definitions
inputSchema: JsonSchema | ZodType; // Consistent naming

When to Apply:

  • When renaming for clarity
  • When adding new concepts -- check existing naming patterns
  • When refactoring -- standardize naming as you go
  • Don't rename just for the sake of it
  • Don't break existing naming without good reason

Trade-off: Some migration pain, but much clearer codebase.


6. Streaming-First

Statement: Preserve streaming capabilities at every step. Maintain provider-agnostic streaming abstraction with unified event format.

Rationale:

  • Streaming is a core feature -- must work reliably
  • Provider-agnostic abstraction enables switching providers easily
  • Unified event format (BaleybotStreamEvent) is the contract
  • Simplification should not break streaming

Examples from Refactoring:

  • Unified event format: All providers transform native format to BaleybotStreamEvent
  • Preserved streaming: v2.0 maintains 100% streaming functionality
  • Simplified flow: 8 layers to 3 layers (62.5% reduction) with zero functionality loss
  • Standard Response.body: Use Response.body ReadableStream instead of custom transport unwrapping

The Golden Rule: Every provider transforms their native streaming format to unified BaleybotStreamEvent

// Provider-agnostic streaming events
export type BaleybotStreamEvent =
| { type: 'text_delta'; content: string }
| { type: 'tool_call_stream_start'; id: string; toolName: string }
| { type: 'tool_execution_output'; toolName: string; result: unknown }
| { type: 'error'; error: Error };
// ... more events

// Works the same regardless of provider
await bot.process('Hello', {
onToken: {
onTextDelta(botName, event) {
// event is ALWAYS: { type: 'text_delta', content: string }
// Works with OpenAI, Anthropic, Google, etc.
console.log(event.content);
}
}
});

When to Apply:

  • When refactoring streaming code -- preserve functionality
  • When adding new providers -- must output BaleybotStreamEvent
  • When simplifying -- streaming must still work
  • Never break streaming for simplicity
  • Never expose provider-specific streaming APIs

Trade-off: Some complexity in provider transform layer, but enables provider-agnostic code.


7. Maintain Functionality

Statement: Maintain 100% of current power and flexibility. Zero functionality loss. Enhance, don't remove capabilities.

Rationale:

  • Users depend on current functionality
  • Simplification should not reduce capabilities
  • Can simplify implementation while maintaining interface
  • New features should add, not replace

Examples from Refactoring:

  • Removed 700+ lines: But maintained 100% functionality
  • Simplified transport: But all use cases still work (Tauri, React Native, testing, retries, logging)
  • Unified naming: But all schema types still supported (JSON Schema, Zod, StructuredOutputConfig)
  • Cleaner config: But all configuration options still available

Validation:

  • All integration tests pass
  • All examples work
  • All use cases supported (Tauri, React Native, testing, etc.)
  • Performance maintained or improved

When to Apply:

  • When refactoring -- maintain all functionality
  • When simplifying -- preserve capabilities
  • When adding features -- don't break existing
  • Don't remove features "because they're complex"
  • Don't simplify at the cost of functionality

Trade-off: More careful refactoring required, but users don't lose capabilities.


8. Developer Experience

Statement: Prioritize clearer APIs, easier testing, better documentation, and fewer concepts to learn.

Rationale:

  • Developer experience directly impacts adoption
  • Clear APIs reduce bugs and support burden
  • Easier testing improves code quality
  • Better documentation reduces onboarding time

Examples from Refactoring:

  • Clearer APIs: Both Baleybot.create() and new Baleybot() work — use whichever you prefer
  • Easier testing: Standard fetch mocks instead of custom transport mocks
  • Better docs: Fewer concepts (7 to 4), clearer examples
  • Fewer concepts: Bot, Provider, Model, Fetch (was: Bot, Provider, Transport, Registry, ClientTransport, TransportHandler, etc.)

Metrics:

  • Config fields: 10 to 6 (Baleybot), 5 to 4 (Provider)
  • Concepts to learn: 7 to 4
  • Tauri setup: 8-10 lines to 3-4 lines
  • Testing mock: 10-12 lines to 4-5 lines

Before (Complex):

// Custom transport mock
const mockTransport = {
send: vi.fn().mockResolvedValue({
status: 200,
body: JSON.stringify({ ... })
})
};
const bot = Baleybot.create({ transport: mockTransport });

After (Simple):

// Standard fetch mock
const mockFetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ ... }))
);
const bot = new Baleybot({
model: { id: 'gpt-4.1', provider: { fetch: mockFetch } }
});

When to Apply:

  • When designing APIs -- prioritize clarity
  • When writing docs -- show real examples
  • When adding features -- consider developer experience
  • When testing -- use standard patterns
  • Don't optimize for edge cases at the expense of common cases
  • Don't add complexity "just in case"

Trade-off: Some advanced use cases might require more setup, but common cases are much easier.


Applying These Principles

Decision-Making Framework

When making architectural decisions, ask:

  1. Does this simplify the codebase? (Principle 1: Simplicity)
  2. Is the simple case simple? (Principle 2: Progressive Disclosure)
  3. Can we use a standard API? (Principle 3: Standard Web APIs)
  4. Are concerns properly separated? (Principle 4: Separation of Concerns)
  5. Is naming consistent? (Principle 5: Unified Naming)
  6. Does this preserve streaming? (Principle 6: Streaming-First)
  7. Does this maintain functionality? (Principle 7: Maintain Functionality)
  8. Does this improve developer experience? (Principle 8: Developer Experience)

Example: Adding a New Feature

Scenario: Adding support for a new LLM provider.

Apply Principles:

  1. Simplicity: Use same BaseProvider pattern, don't create new abstractions
  2. Progressive Disclosure: Simple case: model: 'new-model-id', complex case: full config
  3. Standard Web APIs: Use fetch and Response, no custom transport
  4. Separation: Provider config in model.provider, not in agent config
  5. Unified Naming: Use ProviderConfig, ModelConfig (consistent with existing)
  6. Streaming-First: Must output BaleybotStreamEvent, transform native format
  7. Maintain Functionality: All existing features work (tools, schemas, streaming)
  8. Developer Experience: Clear examples, easy to test with mock fetch

Example: Refactoring Existing Code

Scenario: Simplifying a complex function.

Apply Principles:

  1. Simplicity: Remove unnecessary complexity, even if breaking
  2. Progressive Disclosure: Keep simple API, make advanced features opt-in
  3. Standard Web APIs: Replace custom code with standard APIs where possible
  4. Separation: Split mixed concerns into separate functions/modules
  5. Unified Naming: Use consistent naming throughout
  6. Streaming-First: Preserve streaming functionality
  7. Maintain Functionality: All tests pass, no regressions
  8. Developer Experience: Clearer code, better docs, easier to understand

Trade-offs

Principle Conflicts

Principles sometimes conflict. Here's how to resolve common conflicts:

Simplicity vs. Maintain Functionality

Conflict: Simplifying might remove some functionality.

Resolution:

  • Simplify the implementation, not the interface
  • Maintain functionality through simpler code paths
  • If functionality must be removed, ensure migration path exists

Example: Removed transport layer (simplification) but maintained all use cases through fetch override (functionality preserved).

Progressive Disclosure vs. Unified Naming

Conflict: Simple API might use different names than complex API.

Resolution:

  • Use same names at both levels
  • Simple API is a subset of complex API
  • Names should be consistent across simple and complex cases

Example: outputSchema works for both simple (outputSchema: schema) and complex (outputSchema: { schema, strict, name }) cases.

Standard Web APIs vs. Developer Experience

Conflict: Standard API might be less ergonomic than custom abstraction.

Resolution:

  • Prefer standard API when it's "good enough"
  • Only create custom abstraction if it significantly improves DX
  • Use composable patterns to improve ergonomics

Example: Standard fetch is less ergonomic than custom transport, but composable wrappers (wrapWithRetries, wrapWithLogging) improve DX while staying standard.

Priority Order

When principles conflict, use this priority order:

  1. Streaming-First (never break streaming)
  2. Maintain Functionality (preserve capabilities)
  3. Developer Experience (prioritize common cases)
  4. Simplicity (remove complexity)
  5. Progressive Disclosure (simple by default)
  6. Standard Web APIs (use platform capabilities)
  7. Separation of Concerns (clear boundaries)
  8. Unified Naming (consistent terminology)

Rationale: Functionality and developer experience are non-negotiable. Simplicity and other principles support these goals.


Conclusion

These principles guide Baleybots development. They emerged from the v2.0 refactoring and will continue to guide future decisions.

Remember:

  • Migration pain is temporary. Code complexity is forever.
  • Simple cases should be simple. Complex cases should be possible.
  • Preserve functionality while simplifying implementation.
  • Prioritize developer experience in all decisions.

When in doubt, refer to these principles. They are the foundation of Baleybots architecture.