AGENTS.md - Nova Assistant
Nova is an on-device AI assistant powered by Gemma, built with Flutter. All AI inference runs locally on the device - no data is sent to external servers.
Human docs: docs/ (GitHub Pages) · Agent skill: .cursor/skills/nova-dev
When a developer says use the nova_dev skill to setup and build the app or configure my own model, follow that skill first, then this file for coding standards.
Table of Contents
- Project Overview
- Technologies
- Useful Commands
- Project Structure
- Architecture
- Coding Standards and Best Practices
- Testing Guidelines
- Git Conventions
- Platform Notes
- Security Considerations
- Key Models Reference
- Available Tools
- Assistant Roles
Project Overview
Nova is a Flutter-based mobile AI assistant that:
- Runs AI models entirely on-device using
flutter_gemma - Supports multiple inference engines (LiteRtLm, MediaPipe)
- Provides voice input, screen capture, and tool execution capabilities
- Uses RAG (Retrieval-Augmented Generation) with local memory
- Supports multiple AI models with automatic selection
Technologies
Core Framework
| Technology | Version | Purpose |
|---|---|---|
| Flutter | >=3.44.0 | Cross-platform UI framework |
| Dart | >=3.12.0 <4.0.0 | Programming language |
| Kotlin | JVM 17 | Android native code |
AI and Inference
| Package | Version | Purpose |
|---|---|---|
flutter_gemma |
^1.2.0 | Core Gemma inference engine |
flutter_gemma_litertlm |
1.0.2 | LiteRT LM inference backend |
flutter_gemma_mediapipe |
1.0.3 | MediaPipe inference backend |
Storage and Persistence
| Package | Version | Purpose |
|---|---|---|
shared_preferences |
^2.3.4 | Key-value local storage |
sqflite |
^2.4.1 | SQLite database |
path_provider |
^2.1.5 | File system paths |
uuid |
^4.5.1 | Unique ID generation |
Audio and Speech
| Package | Version | Purpose |
|---|---|---|
speech_to_text |
^7.0.0 | Speech-to-text transcription |
record |
^7.1.1 | Audio recording |
just_audio |
^0.10.6 | Audio playback |
UI and Media
| Package | Version | Purpose |
|---|---|---|
flutter_markdown |
^0.7.6 | Markdown rendering |
image_picker |
^1.1.2 | Image selection from gallery |
file_picker |
^11.0.2 | File selection |
permission_handler |
^12.0.3 | Runtime permissions |
Dev Dependencies
| Package | Version | Purpose |
|---|---|---|
flutter_test |
SDK | Unit and widget testing |
flutter_lints |
^6.0.0 | Lint rules |
mockito |
^5.4.4 | Mocking for tests |
build_runner |
^2.4.13 | Code generation |
dart_code_linter |
^4.1.6 | Code metrics and analysis |
Useful Commands
Setup and Installation
# Install dependencies
flutter pub get
# Regenerate code (after model changes)
dart run build_runner build --delete-conflicting-outputs
# Check for outdated packages
flutter pub outdated
Run and Debug
# Run in debug mode on connected device
flutter run
# Run on specific platform
flutter run -d android
flutter run -d windows
flutter run -d chrome
# Run with specific flavor
flutter run --flavor production
# Hot reload (while running)
r
# Hot restart (while running)
R
Build and Release
# Build Android APK (debug)
flutter build apk --debug
# Build Android APK (release)
flutter build apk --release
# Build Android App Bundle
flutter build appbundle --release
# Build web
flutter build web --release
# Build Windows
flutter build windows --release
Versioned release (Windows)
# Default: patch bump, changelog, analyze, test, arm64 release APK, commit, tag, push
./scripts/release.ps1
# Preview without changing anything
./scripts/release.ps1 -DryRun
# Minor/major, custom changelog bullet, local-only
./scripts/release.ps1 -Bump minor -Message "Settings hubs" -NoPush
./scripts/release.ps1 -SkipTests -AppBundle
Requires a clean git working tree and CHANGELOG.md with an ## [Unreleased] section.
pubspec.yaml may use MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH+BUILD; releases always write +BUILD.
Testing
# Run all tests
flutter test
# Run specific test file
flutter test test/models/chat_message_test.dart
# Run tests with coverage
flutter test --coverage
Analysis and Linting
# Run static analysis
flutter analyze
# Fix auto-fixable issues
dart fix --apply
# Check formatting
dart format --set-exit-if-changed .
# Format all files
dart format .
Platform-Specific (Android)
# Clean Android build
cd android && .\gradlew.bat clean && cd ..
# Build Android debug APK
flutter build apk --debug --target-platform android-arm64
# Install on connected device
flutter install
Project Structure
nova_assistant/
lib/
main.dart # App entry point and initialization
models/ # Data models
agent_identity.dart # Agent identity configuration
assistant_role.dart # Role-based system prompts
chat_message.dart # Chat message data class
model_info.dart # Model definitions and selection
screens/ # UI screens
assistant_screen.dart # Main chat interface
identity_config_screen.dart # Identity configuration
memory_management_screen.dart # Memory management UI
settings_screen.dart # App settings
services/ # Business logic services
chat_history_service.dart # Chat history persistence
download_progress_service.dart# Download tracking
memory_service.dart # RAG memory system
model_manager.dart # Model installation and management
model_orchestrator.dart # Inference orchestration
model_update_service.dart # Model update checking
parallel_session_manager.dart # Multi-session support
platform_adaptation_service.dart # Platform-specific adaptations
platform/ # Platform channel integrations
assistant_role_service.dart # Assistant role via platform channel
screenshot_service.dart # Screen capture
tool_executor_service.dart # Tool execution via native code
tools/ # Tool definitions for AI
tool_definitions.dart # Available tools for AI model
widgets/ # Reusable UI components
chat_bubble.dart # Chat message bubble
voice_input.dart # Voice input button
test/ # Test files (mirrors lib/ structure)
models/
services/
tools/
widgets/
android/ # Android-specific code
doc/ # Documentation
pubspec.yaml # Dependencies
analysis_options.yaml # Lint rules
Architecture
Core Patterns
- Singleton Services: Most services use the singleton pattern
class MyService {
static MyService? _instance;
static MyService get instance => _instance ??= MyService._();
MyService._();
}
- Stream-Based Communication: Services communicate via broadcast streams
final _statusController = StreamController<String>.broadcast();
Stream<String> get statusStream => _statusController.stream;
- Platform Channels: Native functionality via MethodChannel
static const _channel = MethodChannel('dev.nova.assistant/tools');
Model Selection Flow
User Query
|
ModelSelector.selectForQuery()
|
+-- Short query (<=8 words) --> SmolLM (fast)
+-- Image attached ----------> FastVLM or Gemma 4 (vision)
+-- Thinking mode -----------> Gemma 4 (thinking)
+-- Default -----------------> Gemma 4 E2B (heavy)
Inference Pipeline
- User sends message
ModelOrchestrator.processMessage()receives query- RAG context retrieved from
MemoryService - Model selected based on query characteristics
- Model loaded via
FlutterGemma.getActiveModel() - Chat created with system prompt and tools
- Response streamed via
generateChatResponseAsync() - Tool calls intercepted and executed via
ToolExecutorService - Tool results fed back to model for final response
- Conversation stored in memory
Tool System
Tools are defined as Tool objects with JSON schemas:
static final Tool getTime = Tool(
name: 'get_time',
description: 'Get the current time, date, and day of the week',
parameters: <String, Object>{
'type': 'object',
'properties': <String, Object>{},
},
);
Tool execution flows through platform channels to native code.
Coding Standards and Best Practices
Dart Style Guide
This project enforces strict Dart style via analysis_options.yaml.
Enforced Rules
prefer_final_locals: Usefinalfor local variables when possibleprefer_final_fields: Usefinalfor private fields when possiblerequire_trailing_commas: Always add trailing commasalways_declare_return_types: Explicit return types on functionsannotate_overrides: Use@overrideannotationavoid_empty_else: No empty else blocksavoid_init_to_null: Do not initialize variables to nullprefer_single_quotes: Use single quotes for stringsprefer_const_constructors: Useconstwhen possiblesized_box_for_whitespace: UseSizedBoxnotContainerfor spacingsort_child_properties_last: Putchildproperty last in widgets
Code Metrics (dart_code_linter)
metrics:
cyclomatic-complexity: 20
number-of-parameters: 4
maximum-nesting-level: 5
Enforced Lint Rules
avoid-dynamic: Prefer specific types overdynamicavoid-passing-async-when-sync-expectedavoid-redundant-async: Do not await futures in sync contextsavoid-unnecessary-type-assertionsavoid-unnecessary-type-castsavoid-unrelated-type-assertionsavoid-unused-parametersavoid-nested-conditional-expressionsnewline-before-return: Blank line before returnno-boolean-literal-compareno-empty-blockprefer-trailing-commaprefer-conditional-expressionsno-equal-then-elseprefer-moving-to-variableprefer-match-file-name
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Classes | PascalCase | ModelOrchestrator, ChatMessage |
| Files | snake_case | model_orchestrator.dart |
| Variables | camelCase | activeModel, isGenerating |
| Constants | camelCase | _maxToolRounds, _prefsKey |
| Private members | _ prefix |
_instance, _statusController |
| Enums | PascalCase | NovaModel, DownloadStatus |
| Enum values | camelCase | NovaModel.smollm, DownloadStatus.downloading |
File Organization
// 1. Dart core imports
import 'dart:async';
import 'dart:convert';
import 'dart:io';
// 2. Flutter imports
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
// 3. Package imports
import 'package:flutter_gemma/flutter_gemma.dart';
import 'package:shared_preferences/shared_preferences.dart';
// 4. Project imports
import 'package:nova_assistant/models/model_info.dart';
import 'package:nova_assistant/services/model_manager.dart';
Widget Guidelines
- Use
constconstructors when possible - Prefer
SizedBoxoverContainerfor whitespace - Use
withValues()instead ofwithOpacity()for color - Always include
keyparameter in widget constructors - Use
copyWith()for immutable data updates
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
width: 100,
height: 100,
);
}
}
Error Handling
// Use try-catch for async operations
try {
final result = await someAsyncOperation();
return result;
} on PlatformException catch (e) {
debugPrint('Platform error: ${e.message}');
return null;
} catch (e) {
debugPrint('Unexpected error: $e');
rethrow;
}
// Always check mounted before setState in async callbacks
await for (final event in stream) {
if (!mounted) break;
setState(() => _value = event);
}
Stream Management
// Always close streams in dispose()
@override
void dispose() {
_statusController.close();
_subscriptions.cancel();
super.dispose();
}
// Use broadcast streams for multiple listeners
final _controller = StreamController<String>.broadcast();
Stream<String> get stream => _controller.stream;
Singleton Pattern
class MyService {
static MyService? _instance;
static MyService get instance => _instance ??= MyService._();
MyService._();
// Private constructor prevents external instantiation
}
Testing Guidelines
Test Structure
Mirror the lib/ directory structure in test/:
test/
models/
chat_message_test.dart
services/
model_orchestrator_test.dart
memory_service_test.dart
tools/
tool_definitions_test.dart
widgets/
chat_bubble_test.dart
Writing Tests
import 'package:flutter_test/flutter_test.dart';
import 'package:nova_assistant/models/chat_message.dart';
void main() {
group('ChatMessage', () {
test('should serialize to JSON correctly', () {
final message = ChatMessage(
id: '1',
text: 'Hello',
isUser: true,
timestamp: DateTime(2024, 1, 1),
);
final json = message.toJson();
expect(json['id'], '1');
expect(json['text'], 'Hello');
expect(json['isUser'], true);
});
test('should deserialize from JSON', () {
final json = {
'id': '1',
'text': 'Hello',
'isUser': true,
'timestamp': '2024-01-01T00:00:00.000',
};
final message = ChatMessage.fromJson(json);
expect(message.id, '1');
expect(message.text, 'Hello');
expect(message.isUser, true);
});
test('copyWith should preserve unchanged fields', () {
final original = ChatMessage(
id: '1',
text: 'Hello',
isUser: true,
timestamp: DateTime(2024, 1, 1),
);
final copy = original.copyWith(text: 'World');
expect(copy.id, '1');
expect(copy.text, 'World');
expect(copy.isUser, true);
});
});
}
Mocking
Use mockito for mocking dependencies:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateMocks([SharedPreferences])
import 'my_test.mocks.dart';
void main() {
late MockSharedPreferences mockPrefs;
setUp(() {
mockPrefs = MockSharedPreferences();
when(mockPrefs.getString(any)).thenReturn(null);
});
}
Running Tests
# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Run specific test
flutter test test/models/chat_message_test.dart
# Run tests matching pattern
flutter test --name "should serialize"
Git Conventions
Branch Naming
main- Production-ready codedevelop- Integration branchfeature/description- New featuresfix/description- Bug fixesdocs/description- Documentation changes
Commit Messages
Follow Conventional Commits:
feat: add parallel session management
fix: resolve model loading timeout
docs: update AGENTS.md
refactor: extract model selection logic
test: add unit tests for ChatMessage
chore: update dependencies
Pre-Commit Checklist
- Run
flutter analyze- no errors - Run
dart format .- consistent formatting - Run
flutter test- all tests pass - No debug prints in production code
- No hardcoded strings (use constants)
- Trailing commas on multi-line parameters
Platform Notes
Android
- Min SDK: 26 (Android 8.0)
- Target SDK: Latest Flutter default
- NDK Version: 28.2.13676358
- ABI Filter:
arm64-v8aonly (64-bit devices) - Java/Kotlin: JVM 17
Web Limitations
The web platform has several limitations:
- Vision/image input not supported on web litertlm
- Thinking mode not supported on web litertlm
- Function calling not supported on web litertlm
- Chrome has approximately 2GB ArrayBuffer limit for model storage
- Use MediaPipe
.taskmodels for web vision/thinking/function calling support
Security Considerations
Data Privacy
- All AI inference runs entirely on-device - no data is sent to external servers
- Chat history is stored locally via SharedPreferences
- Model files are stored in the app’s documents directory
- No analytics or telemetry by default
Permissions
The app requests the following permissions:
- Microphone: For voice input (speech-to-text)
- Photos: For image picking from gallery
- Screen Capture: For screenshot-based context (requires user consent)
Model Security
- Models are downloaded from trusted HuggingFace repositories
- Model files are verified after download
- Download retry logic with exponential backoff
- Models can be uninstalled to free storage
Best Practices
- Never log sensitive user data in debug output
- Clear
debugPrintstatements before release builds - Use
flutter build apk --obfuscatefor release builds - Do not commit API keys or secrets to version control
- Use environment variables for sensitive configuration
Key Models Reference
| Model | Size | Vision | Thinking | Function Calling | Format |
|---|---|---|---|---|---|
| SmolLM-135M | 135MB | No | No | Yes | .task |
| FastVLM-0.5B | 500MB | Yes | No | Yes | .litertlm |
| Gemma 3 1B | 500MB | No | No | Yes | .litertlm |
| Gemma 4 E2B | 2400MB | Yes | Yes | Yes | .litertlm |
Model Selection Logic
- Short queries (<=8 words): Use SmolLM for fast response
- Image input: Use FastVLM or Gemma 4 E2B (vision capable)
- Thinking mode: Use Gemma 4 E2B (thinking capable)
- Complex queries: Use Gemma 4 E2B (most capable)
Available Tools
| Tool | Description | Parameters |
|---|---|---|
get_time |
Get current time, date, and day | None |
set_alarm |
Set a device alarm | hour, minute, message |
cancel_alarm |
Cancel an existing alarm | hour, minute |
open_app |
Open an app by package name | package |
search_web |
Open browser with search query | query |
get_weather |
Get weather for a location | location |
send_sms |
Send an SMS message | phone, message |
open_settings |
Open device Settings | None |
take_screenshot |
Capture current screen | None |
Assistant Roles
| Role | Description |
|---|---|
helpful |
General helpful assistant (default) |
coder |
Expert programming assistant |
creative |
Creative writing assistant |
student |
Study companion |
analyst |
Data analysis assistant |
Each role has a custom system prompt that shapes the AI’s behavior and personality.