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
fetchoverride - No backward compatibility: v2.0 is a clean break from v1.x
- Removed deprecated fields:
proxyUrl,transport,finalResponseSchemaremoved 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.fetchby 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.fetchoverride - Standard Response: Use
Responsefrom fetch API instead of customTransportResponse - Composable fetch wrappers:
wrapWithRetries,wrapWithLogging,composeFetchinstead of transport registry - Standard ReadableStream: Use
Response.bodyfor 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:
apiKeyandbaseUrlinBaleybotConfig(mixed agent + provider concerns) - After: Agent config (
name,goal,tools) separate from model config (model: { id, provider: { apiKey, baseUrl } }) - Before:
finalResponseSchemamixed with provider config - After:
outputSchemain agent config, provider config inmodel.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(wasfinalResponseSchema,responseSchema,schema,output) - Tool schema naming: Unified to
inputSchema(wasparameters,schema) - Event naming:
StreamEventtoBaleybotStreamEvent(clearer, more descriptive) - Config naming:
ProviderConfig(notProviderOptions,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.bodyReadableStream 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()andnew Baleybot()work — use whichever you prefer - Easier testing: Standard
fetchmocks 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:
- Does this simplify the codebase? (Principle 1: Simplicity)
- Is the simple case simple? (Principle 2: Progressive Disclosure)
- Can we use a standard API? (Principle 3: Standard Web APIs)
- Are concerns properly separated? (Principle 4: Separation of Concerns)
- Is naming consistent? (Principle 5: Unified Naming)
- Does this preserve streaming? (Principle 6: Streaming-First)
- Does this maintain functionality? (Principle 7: Maintain Functionality)
- Does this improve developer experience? (Principle 8: Developer Experience)
Example: Adding a New Feature
Scenario: Adding support for a new LLM provider.
Apply Principles:
- Simplicity: Use same
BaseProviderpattern, don't create new abstractions - Progressive Disclosure: Simple case:
model: 'new-model-id', complex case: full config - Standard Web APIs: Use
fetchandResponse, no custom transport - Separation: Provider config in
model.provider, not in agent config - Unified Naming: Use
ProviderConfig,ModelConfig(consistent with existing) - Streaming-First: Must output
BaleybotStreamEvent, transform native format - Maintain Functionality: All existing features work (tools, schemas, streaming)
- Developer Experience: Clear examples, easy to test with mock fetch
Example: Refactoring Existing Code
Scenario: Simplifying a complex function.
Apply Principles:
- Simplicity: Remove unnecessary complexity, even if breaking
- Progressive Disclosure: Keep simple API, make advanced features opt-in
- Standard Web APIs: Replace custom code with standard APIs where possible
- Separation: Split mixed concerns into separate functions/modules
- Unified Naming: Use consistent naming throughout
- Streaming-First: Preserve streaming functionality
- Maintain Functionality: All tests pass, no regressions
- 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:
- Streaming-First (never break streaming)
- Maintain Functionality (preserve capabilities)
- Developer Experience (prioritize common cases)
- Simplicity (remove complexity)
- Progressive Disclosure (simple by default)
- Standard Web APIs (use platform capabilities)
- Separation of Concerns (clear boundaries)
- 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.