Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.9.870] - 2026-02-24
### Added
- Agent templates: full-stack feature for creating, editing, and managing
reusable agent templates. Includes database schema, repository, service layer,
Riverpod providers, and UI (list page, detail page with model selector).
- Template evolution workflow: LLM-assisted 1-on-1 page for reviewing agent
performance metrics, providing structured feedback, and having the LLM rewrite
template directives for approval.
- Template performance metrics: track token usage, execution counts, and success
rates per template.
- Inference provider resolver for selecting AI models per template.
- Settings navigation for agent templates (list, create, edit).

## [0.9.869] - 2026-02-23
### Added
- Provider filter for category prompt selection: when multiple AI providers
Expand All @@ -31,6 +44,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
many images are available. The image grid now scrolls independently while
the button stays pinned at the bottom.

## [0.9.868] - 2026-02-22
### Added
- Agent cross-device sync via Matrix: agent entities and links synchronize
across devices with wake subscription restoration.
- Sync maintenance UI supports agent re-sync steps.
- Zone-based transaction isolation: outbox messages are buffered until commit,
with nested transaction and rollback support.

## [0.9.867] - 2026-02-22
### Added
- Agent running-state feedback: reactive spinner indicators on the task page
Expand Down
5 changes: 5 additions & 0 deletions flatpak/com.matthiasn.lotti.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
<launchable type="desktop-id">com.matthiasn.lotti.desktop</launchable>
<icon type="stock">com.matthiasn.lotti</icon>
<releases>
<release version="0.9.870" date="2026-02-24">
<description>
<p>Agent templates: full-stack feature for reusable agent templates with database, service, state, and UI layers. Template evolution workflow with LLM-assisted 1-on-1 review of performance metrics and directive rewriting. Inference provider resolver and settings navigation for templates.</p>
</description>
</release>
<release version="0.9.869" date="2026-02-23">
<description>
<p>Provider filter for category prompt selection. Collapsible text entries in linked-entry context. Agent cross-device sync via Matrix with zone-based transaction isolation. Desktop crash-on-exit fix via graceful service disposal. Reference image grid overflow fix.</p>
Expand Down
26 changes: 26 additions & 0 deletions lib/beamer/locations/settings_location.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:beamer/beamer.dart';
import 'package:flutter/material.dart';
import 'package:lotti/features/agents/ui/agent_template_detail_page.dart';
import 'package:lotti/features/agents/ui/agent_template_list_page.dart';
import 'package:lotti/features/ai/ui/settings/ai_settings_page.dart';
import 'package:lotti/features/categories/ui/pages/categories_list_page.dart'
as new_categories;
Expand Down Expand Up @@ -66,6 +68,9 @@ class SettingsLocation extends BeamLocation<BeamState> {
'/settings/habits/by_id/:habitId',
'/settings/habits/create',
'/settings/habits/search/:searchTerm',
'/settings/templates',
'/settings/templates/create',
'/settings/templates/:templateId',
'/settings/flags',
'/settings/theming',
'/settings/advanced',
Expand Down Expand Up @@ -296,6 +301,27 @@ class SettingsLocation extends BeamLocation<BeamState> {
child: CreateHabitPage(),
),

// Agent Templates
if (pathContains('templates/create'))
const BeamPage(
key: ValueKey('settings-templates-create'),
child: AgentTemplateDetailPage(),
)
else if (pathContains('templates') && pathContainsKey('templateId'))
BeamPage(
key: ValueKey(
'settings-templates-${state.pathParameters['templateId']}',
),
child: AgentTemplateDetailPage(
templateId: state.pathParameters['templateId'],
),
)
else if (pathContains('templates'))
const BeamPage(
key: ValueKey('settings-templates'),
child: AgentTemplateListPage(),
),

// Flags
if (pathContains('flags'))
const BeamPage(
Expand Down
22 changes: 15 additions & 7 deletions lib/features/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Freezed sealed unions following the `JournalEntity` / `EntryLink` patterns:
- **`agent_link.dart`** — `AgentLink` with 6 variants: `basic` (fallback), `agentState`, `messagePrev`, `messagePayload`, `toolEffect`, `agentTask`. Uses `@Freezed(fallbackUnion: 'basic')`.
- **`agent_config.dart`** — `AgentConfig`, `AgentSlots`, `AgentMessageMetadata` as Freezed data classes.
- **`agent_enums.dart`** — `AgentLifecycle`, `AgentInteractionMode`, `AgentRunStatus`, `AgentMessageKind`.
- **`template_performance_metrics.dart`** — `TemplatePerformanceMetrics` Freezed data class aggregating wake-run log data for a template (total wakes, success/failure counts, avg duration, active instances).

### Wake Infrastructure (`wake/`)

Expand All @@ -58,13 +59,15 @@ The full wake cycle implementation:

- **`task_agent_strategy.dart`** — `ConversationStrategy` implementation that dispatches LLM tool calls to `AgentToolExecutor`. The `record_observations` tool is intercepted locally (no executor needed since it doesn't modify journal entities) — observations accumulate in memory and are retrieved via `extractObservations()`. The final text response becomes the report via `extractReportContent()`. Each message turn is persisted to `agent.sqlite` for durability.
- **`task_agent_workflow.dart`** — Assembles context (agent state, current report, agentJournal observations, task details from `AiInputRepository`, trigger delta), resolves a Gemini inference provider, runs the conversation via `ConversationRepository`, persists the updated report and observations, and updates agent state. Persists the user message as an `agentMessage` (kind=user) for inspectability. Includes `conversationRepository.deleteConversation(conversationId)` cleanup in a `finally` block.
- **`template_evolution_workflow.dart`** — LLM-assisted template evolution. Creates a single-turn conversation with a meta-prompt instructing the LLM to rewrite directives based on performance metrics and user feedback. Returns an `EvolutionProposal` with proposed vs original directives for user approval. Strips markdown fences from LLM output.

### Service Layer (`service/`)

High-level agent lifecycle management:

- **`agent_service.dart`** — `AgentService` provides `createAgent`, `getAgent`, `listAgents`, `getAgentReport`, `pauseAgent`, `resumeAgent`, `destroyAgent`. Each mutation writes via `AgentSyncService` (which enqueues changes for cross-device sync) and reads via `AgentRepository`. Lifecycle transitions update the identity entity and manage wake subscriptions.
- **`task_agent_service.dart`** — `TaskAgentService` provides task-specific operations: `createTaskAgent` (creates agent + state + link + subscription), `getTaskAgentForTask` (lookup via `agent_task` link), `triggerReanalysis` (manual re-wake), `restoreSubscriptions` (queries active agents, filters for task_agent kind, registers subscriptions on app startup), `restoreSubscriptionsForAgent` (re-registers subscriptions for a single agent after resume).
- **`agent_template_service.dart`** — `AgentTemplateService` provides template CRUD, versioning, category filtering, rollback, and `computeMetrics` (aggregates wake-run log data into `TemplatePerformanceMetrics`).

### Sync (`sync/`)

Expand All @@ -89,6 +92,7 @@ Agent inspection interface:
- **`agent_conversation_log.dart`** — Thread-grouped conversation view: messages grouped by `threadId` (wake cycle), sorted most-recent-first, each rendered as an `ExpansionTile` with timestamp, message count, and tool call count.
- **`agent_controls.dart`** — Pause/resume (with subscription restore), re-analyze, destroy, and hard-delete actions. Uses busy-state guards and error snackbars.
- **`agent_date_format.dart`** — Shared date formatting utilities using `intl.DateFormat`.
- **`agent_one_on_one_page.dart`** — 1-on-1 template evolution page: performance metrics dashboard, structured feedback form (what worked well, what didn't, specific changes), evolve button that invokes `TemplateEvolutionWorkflow`, and a diff-style preview of current vs proposed directives with approve/reject controls.

## Memory Model

Expand Down Expand Up @@ -128,7 +132,8 @@ lib/features/agents/
│ ├── agent_domain_entity.dart # Freezed sealed union (7 variants)
│ ├── agent_link.dart # Freezed sealed union (6 variants)
│ ├── agent_config.dart # AgentConfig, AgentSlots, AgentMessageMetadata
│ └── agent_enums.dart # All agent-domain enums
│ ├── agent_enums.dart # All agent-domain enums
│ └── template_performance_metrics.dart # Freezed metrics aggregation
├── database/
│ ├── agent_database.dart # Drift database class
│ ├── agent_database.drift # SQL schema
Expand All @@ -145,10 +150,12 @@ lib/features/agents/
│ └── task_title_handler.dart # New set_task_title handler
├── workflow/
│ ├── task_agent_strategy.dart # ConversationStrategy for task agent
│ └── task_agent_workflow.dart # Full wake cycle orchestration
│ ├── task_agent_workflow.dart # Full wake cycle orchestration
│ └── template_evolution_workflow.dart # LLM-assisted directive evolution
├── service/
│ ├── agent_service.dart # Lifecycle management
│ └── task_agent_service.dart # Task-agent-specific operations
│ ├── task_agent_service.dart # Task-agent-specific operations
│ └── agent_template_service.dart # Template CRUD, versioning, metrics
├── sync/
│ └── agent_sync_service.dart # Sync-aware writes + transaction isolation
├── state/
Expand All @@ -160,7 +167,8 @@ lib/features/agents/
├── agent_activity_log.dart # Message log with expandable payloads
├── agent_conversation_log.dart # Thread-grouped conversation view
├── agent_controls.dart # Action buttons
└── agent_date_format.dart # Shared date formatting utilities
├── agent_date_format.dart # Shared date formatting utilities
└── agent_one_on_one_page.dart # 1-on-1 template evolution page
```

## Testing
Expand All @@ -172,10 +180,10 @@ Tests mirror the source structure under `test/features/agents/`:
- **Wake tests** — Run key determinism (incl. canonical key ordering), queue dedup, single-flight, orchestrator matching + dispatch (76 tests)
- **Tool tests** — Category enforcement, audit logging, vector clock capture, handler dispatch, registry validation incl. `record_observations` (54 tests)
- **Sync tests** — Transaction isolation (buffer, flush, nested commit/rollback, zone isolation), entity/link upsert with outbox enqueue
- **Service tests** — Lifecycle management, task agent creation, link lookups, `restoreSubscriptions`, `restoreSubscriptionsForAgent`
- **Workflow tests** — Context assembly, conversation execution, report persistence, tool-based observation capture
- **Service tests** — Lifecycle management, task agent creation, link lookups, `restoreSubscriptions`, `restoreSubscriptionsForAgent`, `computeMetrics` aggregation
- **Workflow tests** — Context assembly, conversation execution, report persistence, tool-based observation capture, template evolution proposal generation
- **State tests** — Riverpod provider unit tests for agent report, state, identity, messages, payload text, initialization, and task agent lookup
- **UI tests** — Widget tests for agent detail page, Markdown report rendering, activity log, controls, date formatting
- **UI tests** — Widget tests for agent detail page, Markdown report rendering, activity log, controls, date formatting, 1-on-1 evolution page

Run `make test` to verify current test count and status.

Expand Down
28 changes: 21 additions & 7 deletions lib/features/agents/service/agent_template_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import 'package:lotti/features/agents/model/template_performance_metrics.dart';
import 'package:lotti/features/agents/sync/agent_sync_service.dart';
import 'package:uuid/uuid.dart';

/// Thrown when a template cannot be deleted because active agents reference it.
class TemplateInUseException implements Exception {
const TemplateInUseException({
required this.templateId,
required this.activeCount,
});

final String templateId;
final int activeCount;

@override
String toString() =>
'TemplateInUseException: cannot delete template $templateId — '
'$activeCount active instance(s)';
}

/// Well-known template IDs for seeded defaults.
const lauraTemplateId = 'template-laura-001';
const tomTemplateId = 'template-tom-001';
Expand Down Expand Up @@ -224,9 +240,9 @@ class AgentTemplateService {
agents.where((a) => a.lifecycle != AgentLifecycle.destroyed).toList();

if (activeAgents.isNotEmpty) {
throw StateError(
'Cannot delete template $templateId: '
'${activeAgents.length} active instance(s)',
throw TemplateInUseException(
templateId: templateId,
activeCount: activeAgents.length,
);
}

Expand Down Expand Up @@ -375,8 +391,7 @@ class AgentTemplateService {
if (r.status == WakeRunStatus.completed.name) successCount++;
if (r.status == WakeRunStatus.failed.name) failureCount++;
if (r.startedAt != null && r.completedAt != null) {
final diffMs =
r.completedAt!.difference(r.startedAt!).inMilliseconds;
final diffMs = r.completedAt!.difference(r.startedAt!).inMilliseconds;
if (diffMs > 0) {
durationSumMs += diffMs;
durationCount++;
Expand All @@ -385,8 +400,7 @@ class AgentTemplateService {
}

final terminalCount = successCount + failureCount;
final successRate =
terminalCount > 0 ? successCount / terminalCount : 0.0;
final successRate = terminalCount > 0 ? successCount / terminalCount : 0.0;
final averageDuration = durationCount > 0
? Duration(milliseconds: durationSumMs ~/ durationCount)
: null;
Expand Down
84 changes: 84 additions & 0 deletions lib/features/agents/ui/agent_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:lotti/features/agents/ui/agent_activity_log.dart';
import 'package:lotti/features/agents/ui/agent_controls.dart';
import 'package:lotti/features/agents/ui/agent_conversation_log.dart';
import 'package:lotti/features/agents/ui/agent_date_format.dart';
import 'package:lotti/features/agents/ui/agent_template_detail_page.dart';
import 'package:lotti/l10n/app_localizations_context.dart';
import 'package:lotti/themes/theme.dart';

Expand Down Expand Up @@ -124,6 +125,11 @@ class AgentDetailPage extends ConsumerWidget {

const Divider(indent: 16, endIndent: 16),

// Template assignment
_TemplateSection(agentId: agentId),

const Divider(indent: 16, endIndent: 16),

// Controls
AgentControls(
agentId: agentId,
Expand Down Expand Up @@ -283,6 +289,84 @@ class _AgentMessagesSectionState extends State<_AgentMessagesSection>
}
}

/// Shows the template assigned to this agent, if any.
class _TemplateSection extends ConsumerWidget {
const _TemplateSection({required this.agentId});

final String agentId;

@override
Widget build(BuildContext context, WidgetRef ref) {
final templateAsync = ref.watch(templateForAgentProvider(agentId));
final template = templateAsync.value?.mapOrNull(agentTemplate: (e) => e);

return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppTheme.cardPadding,
vertical: AppTheme.spacingSmall,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.messages.agentTemplateAssignedLabel,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppTheme.spacingSmall),
if (templateAsync.isLoading && template == null)
const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
else if (templateAsync.hasError && template == null)
Text(
context.messages.commonError,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.error,
),
)
else if (template != null)
ActionChip(
avatar: Icon(
Icons.smart_toy_outlined,
size: 16,
color: context.colorScheme.primary,
),
label: Text(template.displayName),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AgentTemplateDetailPage(
templateId: template.id,
),
),
);
},
)
else
Text(
context.messages.agentTemplateNoneAssigned,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppTheme.spacingXSmall),
Text(
context.messages.agentTemplateSwitchHint,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
],
),
);
}
}

class _LifecycleBadge extends StatelessWidget {
const _LifecycleBadge({required this.lifecycle});

Expand Down
Loading
Loading