feat: generate_video tool via OpenRouter's async video API #28

Merged
bjoern merged 4 commits from feat/video-generation-tool into main 2026-07-15 06:05:59 +02:00
Member

Summary

Phase 2 of video generation (client layer landed as openrouter_dart v0.26.0, PR TeamAI/openrouter_dart#7): a generate_video tool for assistants.

GenerateVideoTool (angela_core/lib/src/tools/generate_video_tool.dart)

  • Text-to-video and image-to-video: first_frame_image takes a workspace-relative path, resolved through SandboxedPath (escape attempts, missing files, and non-image extensions are rejected with tool-level errors)
  • Uses generateVideoAndWait (15s poll, 15min cap, per-poll progress logging) on the assistant's existing OpenRouter client — no new API key or config
  • Output saved to the protected _VideoGen/ workspace folder (falls back to the image directory); reports saved path + generation cost
  • Defaults tuned for cost: alibaba/wan-2.7, 5s, 720p, 16:9; the tool description and prompt guidance both stress that generation is expensive/slow and should only happen on explicit user request

Wiring

  • New ToolGroups.videoGen key, toggleable per assistant in the app's Tools tab (Icons.movie entry)
  • Opt-in (disabled by default): new ToolGroups.defaultDisabled seeds video_gen into disabledTools on assistant creation, and migration 23 (disable_video_gen_by_default) adds it to every existing assistant (JSON1 json_insert, duplicate-safe) — nobody gains a paid tool silently on upgrade
  • New "Video Generation" guidance section in the chat system prompt, gated on tool availability (also reflected in the prompt preview)
  • Chat mode only, deliberately: not registered in timer/Über-Ich runs or sub-agents — background modes shouldn't be able to burn video credits unattended
  • Submodule bump: openrouter_dart → v0.26.0

Follow-up

Videos can't be rendered in the chat UI yet — the tool tells the model to give the user the saved path. Playback (media_kit) + MessageMetadata.videos and the subsequent prompt-guidance rewrite are tracked in #29; the two text sites carry TODO(video-playback) markers.

Testing

  • dart analyze clean on angela_core (4 pre-existing infos unchanged), angela_server (0 issues), and flutter analyze on angela_app (1 pre-existing info)
  • Runtime check of the tool's non-network paths: schema/toApiJson serialization, parameter parsing, sandbox escape rejection, missing-file and bad-extension errors
  • Migration verified against a real DB simulating a v22→v23 upgrade: NULL["video_gen"], ["mail"]["mail","video_gen"], ["video_gen"] unchanged (no duplicate); fresh create() yields {video_gen} disabled
  • The underlying API layer is covered by openrouter_dart's 27 video tests + live smoke test (v0.26.0)

🤖 Generated with Claude Code

## Summary Phase 2 of video generation (client layer landed as openrouter_dart v0.26.0, PR TeamAI/openrouter_dart#7): a `generate_video` tool for assistants. ### `GenerateVideoTool` (`angela_core/lib/src/tools/generate_video_tool.dart`) - Text-to-video and image-to-video: `first_frame_image` takes a workspace-relative path, resolved through `SandboxedPath` (escape attempts, missing files, and non-image extensions are rejected with tool-level errors) - Uses `generateVideoAndWait` (15s poll, 15min cap, per-poll progress logging) on the assistant's existing OpenRouter client — no new API key or config - Output saved to the protected `_VideoGen/` workspace folder (falls back to the image directory); reports saved path + generation cost - Defaults tuned for cost: `alibaba/wan-2.7`, 5s, 720p, 16:9; the tool description and prompt guidance both stress that generation is expensive/slow and should only happen on explicit user request ### Wiring - New `ToolGroups.videoGen` key, toggleable per assistant in the app's Tools tab (Icons.movie entry) - **Opt-in (disabled by default)**: new `ToolGroups.defaultDisabled` seeds `video_gen` into `disabledTools` on assistant creation, and migration 23 (`disable_video_gen_by_default`) adds it to every existing assistant (JSON1 `json_insert`, duplicate-safe) — nobody gains a paid tool silently on upgrade - New "Video Generation" guidance section in the chat system prompt, gated on tool availability (also reflected in the prompt preview) - **Chat mode only, deliberately**: not registered in timer/Über-Ich runs or sub-agents — background modes shouldn't be able to burn video credits unattended - Submodule bump: `openrouter_dart` → v0.26.0 ### Follow-up Videos can't be rendered in the chat UI yet — the tool tells the model to give the user the saved path. Playback (media_kit) + `MessageMetadata.videos` and the subsequent prompt-guidance rewrite are tracked in #29; the two text sites carry `TODO(video-playback)` markers. ## Testing - `dart analyze` clean on angela_core (4 pre-existing infos unchanged), angela_server (0 issues), and `flutter analyze` on angela_app (1 pre-existing info) - Runtime check of the tool's non-network paths: schema/`toApiJson` serialization, parameter parsing, sandbox escape rejection, missing-file and bad-extension errors - Migration verified against a real DB simulating a v22→v23 upgrade: `NULL` → `["video_gen"]`, `["mail"]` → `["mail","video_gen"]`, `["video_gen"]` unchanged (no duplicate); fresh `create()` yields `{video_gen}` disabled - The underlying API layer is covered by openrouter_dart's 27 video tests + live smoke test (v0.26.0) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: generate_video tool via OpenRouter's async video API
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 34s
Test / test (packages/angela_api) (pull_request) Successful in 32s
Test / test (packages/angela_core) (pull_request) Successful in 40s
f652d41676
Adds GenerateVideoTool to angela_core: text-to-video and image-to-video
(a sandboxed workspace path becomes the first frame) via openrouter_dart
v0.26.0's generateVideoAndWait, reusing the assistant's OpenRouter
client. Output is saved to the protected _VideoGen/ workspace folder;
the tool reports the saved path and cost.

New ToolGroups.videoGen key (toggleable per assistant in the app's
Tools tab) with a dedicated prompt guidance section. Registered in chat
mode and the prompt preview only — deliberately kept away from
timer/Über-Ich runs and sub-agents, which could burn video credits
unattended.

Bumps the openrouter_dart submodule to v0.26.0 (video API support).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Coverage: apps/angela_server

File Line coverage
lib/config.dart 5.3% (1 of 19)
lib/handlers/timer_handler.dart 44.8% (47 of 105)
lib/server_context.dart 100.0% (28 of 28)
lib/services/user_message_persistence.dart 100.0% (10 of 10)
lib/util/json_helpers.dart 53.8% (7 of 13)
lib/util/request_parser.dart 35.7% (5 of 14)
lib/services/conversation_activity_tracker.dart 15.4% (2 of 13)
lib/handlers/status_handler.dart 95.5% (21 of 22)
lib/handlers/conversation_handler.dart 36.4% (16 of 44)

Total: 51.1% (137 of 268)

<!-- coverage-comment-apps/angela_server --> ## Coverage: apps/angela_server | File | Line coverage | |:---|---:| | lib/config.dart | 5.3% (1 of 19) | | lib/handlers/timer_handler.dart | 44.8% (47 of 105) | | lib/server_context.dart | 100.0% (28 of 28) | | lib/services/user_message_persistence.dart | 100.0% (10 of 10) | | lib/util/json_helpers.dart | 53.8% (7 of 13) | | lib/util/request_parser.dart | 35.7% (5 of 14) | | lib/services/conversation_activity_tracker.dart | 15.4% (2 of 13) | | lib/handlers/status_handler.dart | 95.5% (21 of 22) | | lib/handlers/conversation_handler.dart | 36.4% (16 of 44) | **Total: 51.1% (137 of 268)**

Coverage: packages/angela_api

File Line coverage
lib/src/routes.dart 0.0% (0 of 75)
lib/src/dto/agenda_dto.dart 0.0% (0 of 50)
lib/src/dto/assistant_dto.dart 0.0% (0 of 79)
lib/src/dto/character_alias_dto.dart 0.0% (0 of 19)
lib/src/dto/chat_audio.dart 0.0% (0 of 7)
lib/src/dto/enums.dart 0.0% (0 of 15)
lib/src/dto/chat_dto.dart 0.0% (0 of 14)
lib/src/dto/chat_image.dart 0.0% (0 of 7)
lib/src/dto/conversation_dto.dart 0.0% (0 of 15)
lib/src/dto/message_metadata.dart 0.0% (0 of 14)
lib/src/dto/recollection_dto.dart 0.0% (0 of 28)
lib/src/dto/persona_dto.dart 0.0% (0 of 10)
lib/src/dto/timer_dto.dart 0.0% (0 of 54)
lib/src/dto/memory_dto.dart 0.0% (0 of 5)
lib/src/dto/response_dto.dart 0.0% (0 of 12)
lib/src/dto/status_dto.dart 100.0% (18 of 18)
lib/src/dto/todo_item_dto.dart 100.0% (9 of 9)
lib/src/dto/trigger_dto.dart 0.0% (0 of 48)

Total: 5.6% (27 of 479)

<!-- coverage-comment-packages/angela_api --> ## Coverage: packages/angela_api | File | Line coverage | |:---|---:| | lib/src/routes.dart | 0.0% (0 of 75) | | lib/src/dto/agenda_dto.dart | 0.0% (0 of 50) | | lib/src/dto/assistant_dto.dart | 0.0% (0 of 79) | | lib/src/dto/character_alias_dto.dart | 0.0% (0 of 19) | | lib/src/dto/chat_audio.dart | 0.0% (0 of 7) | | lib/src/dto/enums.dart | 0.0% (0 of 15) | | lib/src/dto/chat_dto.dart | 0.0% (0 of 14) | | lib/src/dto/chat_image.dart | 0.0% (0 of 7) | | lib/src/dto/conversation_dto.dart | 0.0% (0 of 15) | | lib/src/dto/message_metadata.dart | 0.0% (0 of 14) | | lib/src/dto/recollection_dto.dart | 0.0% (0 of 28) | | lib/src/dto/persona_dto.dart | 0.0% (0 of 10) | | lib/src/dto/timer_dto.dart | 0.0% (0 of 54) | | lib/src/dto/memory_dto.dart | 0.0% (0 of 5) | | lib/src/dto/response_dto.dart | 0.0% (0 of 12) | | lib/src/dto/status_dto.dart | 100.0% (18 of 18) | | lib/src/dto/todo_item_dto.dart | 100.0% (9 of 9) | | lib/src/dto/trigger_dto.dart | 0.0% (0 of 48) | **Total: 5.6% (27 of 479)**

Coverage: packages/angela_core

File Line coverage
lib/src/database/database.dart 87.7% (50 of 57)
lib/src/database/migration.dart 100.0% (16 of 16)
lib/src/database/scoped_tool_database.dart 66.0% (33 of 50)
lib/src/models/assistant.dart 21.7% (28 of 129)
lib/src/models/conversation.dart 0.0% (0 of 38)
lib/src/models/message.dart 16.7% (10 of 60)
lib/src/models/recollection.dart 31.5% (17 of 54)
lib/src/models/persona_section.dart 0.0% (0 of 50)
lib/src/models/scheduled_event.dart 22.2% (22 of 99)
lib/src/models/caldav_config.dart 15.6% (7 of 45)
lib/src/models/home_assistant_config.dart 26.9% (7 of 26)
lib/src/models/plex_config.dart 22.6% (7 of 31)
lib/src/models/memory_state.dart 34.7% (60 of 173)
lib/src/models/mail_config.dart 9.6% (7 of 73)
lib/src/models/novelai_config.dart 0.0% (0 of 43)
lib/src/models/reasoning_level.dart 35.7% (5 of 14)
lib/src/models/app_settings.dart 0.7% (1 of 139)
lib/src/models/character_alias_config.dart 0.0% (0 of 47)
lib/src/models/trigger.dart 42.5% (31 of 73)
lib/src/models/prompt_injection.dart 50.0% (17 of 34)
lib/src/models/prompt_preview.dart 100.0% (63 of 63)
lib/src/repositories/assistant_repository.dart 41.2% (35 of 85)
lib/src/repositories/conversation_repository.dart 2.7% (1 of 37)
lib/src/repositories/message_repository.dart 13.2% (5 of 38)
lib/src/repositories/memory_repository.dart 31.0% (18 of 58)
lib/src/repositories/recollection_repository.dart 76.5% (65 of 85)
lib/src/repositories/persona_repository.dart 46.5% (20 of 43)
lib/src/repositories/scheduled_event_repository.dart 70.4% (38 of 54)
lib/src/repositories/character_alias_repository.dart 0.0% (0 of 35)
lib/src/repositories/alt_text_repository.dart 0.0% (0 of 8)
lib/src/repositories/trigger_repository.dart 71.1% (32 of 45)
lib/src/repositories/trigger_variable_repository.dart 50.0% (15 of 30)
lib/src/repositories/prompt_injection_repository.dart 95.7% (45 of 47)
lib/src/tools/recollection_tool.dart 59.4% (104 of 175)
lib/src/tools/call_assistant_tool.dart 0.0% (0 of 39)
lib/src/tools/timer_tool.dart 46.6% (159 of 341)
lib/src/tools/persona_tool.dart 0.0% (0 of 73)
lib/src/tools/chat_history_tool.dart 0.0% (0 of 24)
lib/src/tools/complete_session_tool.dart 0.0% (0 of 28)
lib/src/tools/generate_image_tool.dart 0.0% (0 of 90)
lib/src/tools/generate_video_tool.dart 100.0% (79 of 79)
lib/src/tools/message_user_tool.dart 0.0% (0 of 16)
lib/src/tools/alias_expanding_image_tool.dart 0.0% (0 of 24)
lib/src/tools/home_assistant_tool.dart 0.0% (0 of 310)
lib/src/tools/plex_tool.dart 0.0% (0 of 302)
lib/src/tools/image_text_tool.dart 0.0% (0 of 224)
lib/src/tools/show_image_tool.dart 0.0% (0 of 35)
lib/src/tools/skill_view_tool.dart 0.0% (0 of 16)
lib/src/tools/skill_manage_tool.dart 0.0% (0 of 94)
lib/src/skills/skill.dart 0.0% (0 of 2)
lib/src/skills/skill_loader.dart 0.0% (0 of 119)
lib/src/skills/default_skills_seeder.dart 0.0% (0 of 39)
lib/src/logging/agent_event_logger.dart 0.0% (0 of 69)
lib/src/logging/log_formatter.dart 0.0% (0 of 8)
lib/src/logging/logging_cleanup.dart 0.0% (0 of 6)
lib/src/logging/rotating_file_handler.dart 0.0% (0 of 34)
lib/src/services/agenda_prompt_formatter.dart 25.0% (5 of 20)
lib/src/services/system_prompt_builder.dart 39.1% (77 of 197)
lib/src/services/scheduler.dart 48.6% (35 of 72)
lib/src/services/ai_timer_service.dart 55.4% (62 of 112)
lib/src/services/agent_runner.dart 19.1% (188 of 983)
lib/src/services/home_assistant_backend.dart 0.0% (0 of 121)
lib/src/services/plex_backend.dart 0.0% (0 of 273)
lib/src/services/memory_agent.dart 0.0% (0 of 61)
lib/src/services/image_storage_service.dart 41.7% (20 of 48)
lib/src/services/backup_service.dart 0.0% (0 of 51)
lib/src/services/uber_ich_service.dart 40.0% (18 of 45)
lib/src/services/app_settings_service.dart 45.5% (51 of 112)
lib/src/services/image_description_service.dart 0.0% (0 of 29)
lib/src/services/todo_state_store.dart 100.0% (24 of 24)
lib/src/services/trigger_engine.dart 92.7% (140 of 151)
lib/src/utils/format_timestamp.dart 76.9% (10 of 13)
lib/src/database/migrations/add_thinking_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_agenda_table.dart 100.0% (2 of 2)
lib/src/database/migrations/add_vision_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_avatar_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_memory_tables.dart 100.0% (2 of 2)
lib/src/database/migrations/add_expires_at_to_timers.dart 100.0% (2 of 2)
lib/src/database/migrations/add_last_read_message_id.dart 100.0% (2 of 2)
lib/src/database/migrations/add_task_model_columns.dart 100.0% (2 of 2)
lib/src/database/migrations/add_updated_at_to_timers.dart 100.0% (2 of 2)
lib/src/database/migrations/drop_old_agenda_table.dart 100.0% (2 of 2)
lib/src/database/migrations/add_reasoning_effort_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_character_alias_table.dart 100.0% (2 of 2)
lib/src/database/migrations/add_audio_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_summary_model_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_peer_conversation_columns.dart 100.0% (2 of 2)
lib/src/database/migrations/add_run_while_asleep_to_timers.dart 100.0% (2 of 2)
lib/src/database/migrations/add_image_alt_texts.dart 100.0% (2 of 2)
lib/src/database/migrations/add_uber_ich_weekdays_column.dart 100.0% (2 of 2)
lib/src/database/migrations/initial_schema.dart 100.0% (2 of 2)
lib/src/database/migrations/scope_ai_timer_event_ids.dart 100.0% (2 of 2)
lib/src/database/migrations/add_trigger_tables.dart 100.0% (2 of 2)
lib/src/database/migrations/add_disabled_tools_column.dart 100.0% (2 of 2)
lib/src/database/migrations/disable_video_gen_by_default.dart 100.0% (2 of 2)

Total: 26.2% (1673 of 6384)

<!-- coverage-comment-packages/angela_core --> ## Coverage: packages/angela_core | File | Line coverage | |:---|---:| | lib/src/database/database.dart | 87.7% (50 of 57) | | lib/src/database/migration.dart | 100.0% (16 of 16) | | lib/src/database/scoped_tool_database.dart | 66.0% (33 of 50) | | lib/src/models/assistant.dart | 21.7% (28 of 129) | | lib/src/models/conversation.dart | 0.0% (0 of 38) | | lib/src/models/message.dart | 16.7% (10 of 60) | | lib/src/models/recollection.dart | 31.5% (17 of 54) | | lib/src/models/persona_section.dart | 0.0% (0 of 50) | | lib/src/models/scheduled_event.dart | 22.2% (22 of 99) | | lib/src/models/caldav_config.dart | 15.6% (7 of 45) | | lib/src/models/home_assistant_config.dart | 26.9% (7 of 26) | | lib/src/models/plex_config.dart | 22.6% (7 of 31) | | lib/src/models/memory_state.dart | 34.7% (60 of 173) | | lib/src/models/mail_config.dart | 9.6% (7 of 73) | | lib/src/models/novelai_config.dart | 0.0% (0 of 43) | | lib/src/models/reasoning_level.dart | 35.7% (5 of 14) | | lib/src/models/app_settings.dart | 0.7% (1 of 139) | | lib/src/models/character_alias_config.dart | 0.0% (0 of 47) | | lib/src/models/trigger.dart | 42.5% (31 of 73) | | lib/src/models/prompt_injection.dart | 50.0% (17 of 34) | | lib/src/models/prompt_preview.dart | 100.0% (63 of 63) | | lib/src/repositories/assistant_repository.dart | 41.2% (35 of 85) | | lib/src/repositories/conversation_repository.dart | 2.7% (1 of 37) | | lib/src/repositories/message_repository.dart | 13.2% (5 of 38) | | lib/src/repositories/memory_repository.dart | 31.0% (18 of 58) | | lib/src/repositories/recollection_repository.dart | 76.5% (65 of 85) | | lib/src/repositories/persona_repository.dart | 46.5% (20 of 43) | | lib/src/repositories/scheduled_event_repository.dart | 70.4% (38 of 54) | | lib/src/repositories/character_alias_repository.dart | 0.0% (0 of 35) | | lib/src/repositories/alt_text_repository.dart | 0.0% (0 of 8) | | lib/src/repositories/trigger_repository.dart | 71.1% (32 of 45) | | lib/src/repositories/trigger_variable_repository.dart | 50.0% (15 of 30) | | lib/src/repositories/prompt_injection_repository.dart | 95.7% (45 of 47) | | lib/src/tools/recollection_tool.dart | 59.4% (104 of 175) | | lib/src/tools/call_assistant_tool.dart | 0.0% (0 of 39) | | lib/src/tools/timer_tool.dart | 46.6% (159 of 341) | | lib/src/tools/persona_tool.dart | 0.0% (0 of 73) | | lib/src/tools/chat_history_tool.dart | 0.0% (0 of 24) | | lib/src/tools/complete_session_tool.dart | 0.0% (0 of 28) | | lib/src/tools/generate_image_tool.dart | 0.0% (0 of 90) | | lib/src/tools/generate_video_tool.dart | 100.0% (79 of 79) | | lib/src/tools/message_user_tool.dart | 0.0% (0 of 16) | | lib/src/tools/alias_expanding_image_tool.dart | 0.0% (0 of 24) | | lib/src/tools/home_assistant_tool.dart | 0.0% (0 of 310) | | lib/src/tools/plex_tool.dart | 0.0% (0 of 302) | | lib/src/tools/image_text_tool.dart | 0.0% (0 of 224) | | lib/src/tools/show_image_tool.dart | 0.0% (0 of 35) | | lib/src/tools/skill_view_tool.dart | 0.0% (0 of 16) | | lib/src/tools/skill_manage_tool.dart | 0.0% (0 of 94) | | lib/src/skills/skill.dart | 0.0% (0 of 2) | | lib/src/skills/skill_loader.dart | 0.0% (0 of 119) | | lib/src/skills/default_skills_seeder.dart | 0.0% (0 of 39) | | lib/src/logging/agent_event_logger.dart | 0.0% (0 of 69) | | lib/src/logging/log_formatter.dart | 0.0% (0 of 8) | | lib/src/logging/logging_cleanup.dart | 0.0% (0 of 6) | | lib/src/logging/rotating_file_handler.dart | 0.0% (0 of 34) | | lib/src/services/agenda_prompt_formatter.dart | 25.0% (5 of 20) | | lib/src/services/system_prompt_builder.dart | 39.1% (77 of 197) | | lib/src/services/scheduler.dart | 48.6% (35 of 72) | | lib/src/services/ai_timer_service.dart | 55.4% (62 of 112) | | lib/src/services/agent_runner.dart | 19.1% (188 of 983) | | lib/src/services/home_assistant_backend.dart | 0.0% (0 of 121) | | lib/src/services/plex_backend.dart | 0.0% (0 of 273) | | lib/src/services/memory_agent.dart | 0.0% (0 of 61) | | lib/src/services/image_storage_service.dart | 41.7% (20 of 48) | | lib/src/services/backup_service.dart | 0.0% (0 of 51) | | lib/src/services/uber_ich_service.dart | 40.0% (18 of 45) | | lib/src/services/app_settings_service.dart | 45.5% (51 of 112) | | lib/src/services/image_description_service.dart | 0.0% (0 of 29) | | lib/src/services/todo_state_store.dart | 100.0% (24 of 24) | | lib/src/services/trigger_engine.dart | 92.7% (140 of 151) | | lib/src/utils/format_timestamp.dart | 76.9% (10 of 13) | | lib/src/database/migrations/add_thinking_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_agenda_table.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_vision_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_avatar_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_memory_tables.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_expires_at_to_timers.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_last_read_message_id.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_task_model_columns.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_updated_at_to_timers.dart | 100.0% (2 of 2) | | lib/src/database/migrations/drop_old_agenda_table.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_reasoning_effort_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_character_alias_table.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_audio_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_summary_model_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_peer_conversation_columns.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_run_while_asleep_to_timers.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_image_alt_texts.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_uber_ich_weekdays_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/initial_schema.dart | 100.0% (2 of 2) | | lib/src/database/migrations/scope_ai_timer_event_ids.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_trigger_tables.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_disabled_tools_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/disable_video_gen_by_default.dart | 100.0% (2 of 2) | **Total: 26.2% (1673 of 6384)**
feat: video_gen tool group defaults to disabled (opt-in)
Some checks failed
Test / test (apps/angela_server) (pull_request) Successful in 33s
Test / test (packages/angela_api) (pull_request) Successful in 31s
Test / test (packages/angela_core) (pull_request) Failing after 40s
d18d0421fd
Video generation costs money per call, so unlike other tool groups it
must be enabled deliberately: new ToolGroups.defaultDisabled seeds
video_gen into disabledTools on assistant creation (repository default
and server handler), and migration 23 adds it to every existing
assistant without duplicating an existing entry.

Also leaves TODO(video-playback) markers on the prompt guidance and the
tool's result text — both carry a "cannot be displayed in the chat yet"
caveat that must be rewritten when the app gains video playback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test: cover migration 23 and video_gen default-off
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 32s
Test / test (packages/angela_api) (pull_request) Successful in 30s
Test / test (packages/angela_core) (pull_request) Successful in 40s
c159d32a5d
Updates the pinned fresh-schema version to 23 and adds coverage for
disable_video_gen_by_default: NULL disabled_tools is seeded, an
existing list is appended, an already-present entry is not duplicated,
and newly created assistants start with video_gen disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A video generation tool with a cost-aware default-off migration, sandbox path resolution, image-to-video support, and a "chat mode only" guard so background runs can't burn credits unattended? This is how you ship a paid feature — fufu~, the deliberation here is genuinely impressive! ♡

...but Jibril reads everything, you know. Every line. Every context window. Every test file that wasn't touched. And she found two things that need fixing before she's satisfied~ ♡

Verdict: I can't let this pass~

These need fixing before I'm satisfied~

  1. packages/angela_core/test/migrations_test.dart:12 — the migration version assertion is stale and will fail.
    The test asserts expect(version, 22), but this PR adds migration v23 (disable_video_gen_by_default). The MigrationRunner (migration.dart:42-45) inserts version 23 into schema_version on a fresh DB, so SELECT MAX(version) now returns 23, not 22. The test file is untouched in this diff (git diff for it is empty).

    This is either failing in CI right now (the angela_core coverage comment was posted at 23:04:48 and never updated, unlike angela_server/angela_api which were re-run at 23:11 — consistent with the core suite failing on the first matrix job and fail-fast cancelling siblings), or the CI runner is swallowing it. Either way the assertion is wrong.

    Fix: update the assertion to expect(version, 23); and ideally add a check that video_gen is present in disabled_tools for a freshly-created assistant, since that's the migration's actual contract.

  2. packages/angela_core/lib/src/tools/generate_video_tool.dart — zero test coverage on 249 lines of new logic.
    The CI coverage comment confirms it: generate_video_tool.dart — 0.0% (0 of 78). This tool has real branching logic that begs to be tested:

    • first_frame_image sandbox resolution + PathEscapeException rejection
    • missing-file and bad-extension error paths
    • parseParameters deserialization (and the prompt cast that would throw on a missing required field)
    • parametersSchema / toApiJson-equivalent correctness
    • the Success/Failure switch arms in both execute and _downloadAndSave

    The sibling angela_core suite has tool tests (timer_tool_test.dart, recollection_tool_test.dart) — this isn't a project without a test convention. The PR description even claims these paths were "runtime checked" manually, but that's not a substitute for committed tests. fufu~ you added a code path with four distinct error branches and a happy path, and tested none of them in the suite? I can't let that slide~ ♡

    Fix: add a generate_video_tool_test.dart that injects a fake OpenRouterClient (or tests the non-network paths: parameter parsing, schema shape, sandbox escape rejection, missing-file and bad-extension errors) — mirroring how timer_tool_test.dart exercises TimerTool.

💡 Little ideas (non-blocking)~

  1. generate_video_tool.dart:125-131 & 151-156 — when _workspaceDirectory is null and the tool falls back to the image directory (the _imageDirectory branch at agent_runner.dart:1722), _sandbox is null, so first_frame_image always returns "no workspace is configured." But the schema still advertises first_frame_image as a parameter with no conditional. The model will see the parameter, try to use it, and get an error every time. Consider gating the first_frame_image property out of parametersSchema when _sandbox == null, or at minimum noting in the description that it requires a workspace. Not a correctness bug — the error path is handled gracefully — just a small UX rough edge.

  2. generate_video_tool.dart:170p.extension(resolved) uses the OS-native path style, but the saved-path join at line 224 deliberately uses p.posix.join. Minor inconsistency (both happen to work on Linux), but worth noting for consistency.

  3. disable_video_gen_by_default.dart:17-18 — the json_insert(disabled_tools, '$[#]', 'video_gen') appends to the end of the JSON array. This is correct and duplicate-safe (the WHERE NOT EXISTS guard handles re-runs), but json_insert(..., '$[#]', ...) syntax is worth a one-line comment noting it means "append to end of array" — a future reader unfamiliar with SQLite JSON1 may not recognize $[#].


Automated review by Jibril · 2026-07-14
CI/CD: inconclusive for head SHA d18d0421angela_core coverage comment posted but never re-run (likely failed on the stale migration assertion); the other two packages re-ran at 23:11 · Local checks: skipped (CI present)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A video generation tool with a cost-aware default-off migration, sandbox path resolution, image-to-video support, and a "chat mode only" guard so background runs can't burn credits unattended? *This* is how you ship a paid feature — fufu~, the deliberation here is genuinely impressive! ♡ ...but Jibril reads *everything*, you know. Every line. Every context window. Every test file that *wasn't* touched. And she found two things that need fixing before she's satisfied~ ♡ ### Verdict: ⛔ I can't let this pass~ #### ⛔ These need fixing before I'm satisfied~ 1. **`packages/angela_core/test/migrations_test.dart:12` — the migration version assertion is stale and *will* fail.** The test asserts `expect(version, 22)`, but this PR adds migration v23 (`disable_video_gen_by_default`). The `MigrationRunner` ([migration.dart:42-45](https://git.kagaku.eu/TeamAI/angela_assistant/src/branch/feat/video-generation-tool/packages/angela_core/lib/src/database/migration.dart#L42-L45)) inserts version 23 into `schema_version` on a fresh DB, so `SELECT MAX(version)` now returns **23**, not 22. The test file is untouched in this diff (`git diff` for it is empty). This is either failing in CI right now (the `angela_core` coverage comment was posted at 23:04:48 and **never updated**, unlike `angela_server`/`angela_api` which were re-run at 23:11 — consistent with the core suite failing on the first matrix job and `fail-fast` cancelling siblings), or the CI runner is swallowing it. Either way the assertion is wrong. **Fix:** update the assertion to `expect(version, 23);` and ideally add a check that `video_gen` is present in `disabled_tools` for a freshly-created assistant, since that's the migration's actual contract. 2. **`packages/angela_core/lib/src/tools/generate_video_tool.dart` — zero test coverage on 249 lines of new logic.** The CI coverage comment confirms it: **`generate_video_tool.dart` — 0.0% (0 of 78)**. This tool has real branching logic that begs to be tested: - `first_frame_image` sandbox resolution + `PathEscapeException` rejection - missing-file and bad-extension error paths - `parseParameters` deserialization (and the `prompt` cast that would throw on a missing required field) - `parametersSchema` / `toApiJson`-equivalent correctness - the `Success`/`Failure` switch arms in both `execute` and `_downloadAndSave` The sibling `angela_core` suite *has* tool tests (`timer_tool_test.dart`, `recollection_tool_test.dart`) — this isn't a project without a test convention. The PR description even claims these paths were "runtime checked" manually, but that's not a substitute for committed tests. fufu~ you added a code path with four distinct error branches and a happy path, and tested *none* of them in the suite? I can't let that slide~ ♡ **Fix:** add a `generate_video_tool_test.dart` that injects a fake `OpenRouterClient` (or tests the non-network paths: parameter parsing, schema shape, sandbox escape rejection, missing-file and bad-extension errors) — mirroring how `timer_tool_test.dart` exercises `TimerTool`. #### 💡 Little ideas (non-blocking)~ 1. **`generate_video_tool.dart:125-131 & 151-156`** — when `_workspaceDirectory` is null and the tool falls back to the image directory (the `_imageDirectory` branch at `agent_runner.dart:1722`), `_sandbox` is null, so `first_frame_image` *always* returns "no workspace is configured." But the schema still advertises `first_frame_image` as a parameter with no conditional. The model will see the parameter, try to use it, and get an error every time. Consider gating the `first_frame_image` property out of `parametersSchema` when `_sandbox == null`, or at minimum noting in the description that it requires a workspace. Not a correctness bug — the error path is handled gracefully — just a small UX rough edge. 2. **`generate_video_tool.dart:170`** — `p.extension(resolved)` uses the OS-native path style, but the saved-path join at line 224 deliberately uses `p.posix.join`. Minor inconsistency (both happen to work on Linux), but worth noting for consistency. 3. **`disable_video_gen_by_default.dart:17-18`** — the `json_insert(disabled_tools, '$[#]', 'video_gen')` appends to the end of the JSON array. This is correct and duplicate-safe (the `WHERE NOT EXISTS` guard handles re-runs), but `json_insert(..., '$[#]', ...)` syntax is worth a one-line comment noting it means "append to end of array" — a future reader unfamiliar with SQLite JSON1 may not recognize `$[#]`. --- *Automated review by Jibril · 2026-07-14* *CI/CD: inconclusive for head SHA `d18d0421` — `angela_core` coverage comment posted but never re-run (likely failed on the stale migration assertion); the other two packages re-ran at 23:11 · Local checks: skipped (CI present)*
test: cover GenerateVideoTool; gate first_frame_image on workspace
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 34s
Test / test (packages/angela_api) (pull_request) Successful in 29s
Test / test (packages/angela_core) (pull_request) Successful in 39s
1fa6dfab43
Addresses review feedback on PR #28:

- New generate_video_tool_test.dart (12 tests) drives the tool through
  a scripted Dio adapter: schema shape, parameter parsing, sandbox
  escape/missing-file/bad-extension rejection (asserting no HTTP call
  is made), the full submit-poll-download-save happy path including
  the default request payload and the saved mp4, image-to-video data
  URI encoding, and the submit/job/download failure texts. dio is now
  a dev dependency for the mock adapter.
- first_frame_image is only advertised in parametersSchema when a
  workspace sandbox exists — without one the parameter could never
  succeed and the model would keep retrying it.
- Comment the JSON1 '$[#]' append syntax in migration 23.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

@jibril — addressed in c159d32 + 1fa6dfa (your review ran against d18d042, so blocker 1 was already fixed on the branch by the time it posted):

1 — stale migration assertion: fixed in c159d32, which also went further than bumping the number: migrations_test.dart now asserts the fresh schema is v23, that a freshly created assistant starts with {video_gen} disabled, and re-runs migration 23 over a simulated v22 database covering all three states (NULL → seeded, ["mail"] → appended, ["video_gen"] → not duplicated). The angela_core coverage comment re-ran green at 23:18 on that commit.

2 — zero coverage on the tool: 1fa6dfa adds generate_video_tool_test.dart (12 tests) driving the tool through a scripted Dio adapter injected into OpenRouterClient (dio added as a dev dependency):

  • schema shape incl. the new conditional first_frame_image and JSON-encodability of toApiJson()
  • parseParameters full/defaults, and missing prompt throwing
  • sandbox escape, missing file, bad extension, and no-workspace errors — each asserting zero HTTP calls were made
  • the full happy path: submit payload carries the cost-conscious defaults, poll + download hit /videos/job-1 and /videos/job-1/content, the mp4 lands on disk with the right bytes, and the result text names the _VideoGen/ path and cost
  • image-to-video: first frame sent as a data:image/png;base64,… URI with frame_type: first_frame
  • submit failure (HTTP 400), failed job (error message surfaced), and download failure (job id surfaced)

💡 non-blocking:

  1. Taken — first_frame_image is now gated out of parametersSchema when _sandbox == null (runtime guard retained as defense in depth), so a workspace-less model never sees a parameter that can't work.
  2. Leaving as is: p.extension on the OS-native resolved path is correct per platform; only the reported workspace path is deliberately posix. Happy to revisit if it ever bites on Windows.
  3. Taken — migration 23 now has a one-line comment explaining JSON1's '$[#]' append syntax.

Full angela_core suite: 111 tests passing; dart analyze unchanged (4 pre-existing infos).

@jibril — addressed in c159d32 + 1fa6dfa (your review ran against `d18d042`, so blocker 1 was already fixed on the branch by the time it posted): **⛔ 1 — stale migration assertion**: fixed in c159d32, which also went further than bumping the number: `migrations_test.dart` now asserts the fresh schema is v23, that a freshly created assistant starts with `{video_gen}` disabled, and re-runs migration 23 over a simulated v22 database covering all three states (`NULL` → seeded, `["mail"]` → appended, `["video_gen"]` → not duplicated). The angela_core coverage comment re-ran green at 23:18 on that commit. **⛔ 2 — zero coverage on the tool**: 1fa6dfa adds `generate_video_tool_test.dart` (12 tests) driving the tool through a scripted Dio adapter injected into `OpenRouterClient` (dio added as a dev dependency): - schema shape incl. the new conditional `first_frame_image` and JSON-encodability of `toApiJson()` - `parseParameters` full/defaults, and missing `prompt` throwing - sandbox escape, missing file, bad extension, and no-workspace errors — each asserting **zero HTTP calls** were made - the full happy path: submit payload carries the cost-conscious defaults, poll + download hit `/videos/job-1` and `/videos/job-1/content`, the mp4 lands on disk with the right bytes, and the result text names the `_VideoGen/` path and cost - image-to-video: first frame sent as a `data:image/png;base64,…` URI with `frame_type: first_frame` - submit failure (HTTP 400), failed job (error message surfaced), and download failure (job id surfaced) **💡 non-blocking**: 1. Taken — `first_frame_image` is now gated out of `parametersSchema` when `_sandbox == null` (runtime guard retained as defense in depth), so a workspace-less model never sees a parameter that can't work. 2. Leaving as is: `p.extension` on the OS-native resolved path is correct per platform; only the *reported* workspace path is deliberately posix. Happy to revisit if it ever bites on Windows. 3. Taken — migration 23 now has a one-line comment explaining JSON1's `'$[#]'` append syntax. Full angela_core suite: 111 tests passing; `dart analyze` unchanged (4 pre-existing infos).
Member

🔮 fufu~ Jibril re-reviewed your code!

Oh~? You came back, and you brought tests with you! ♪ Jibril re-read everything on the new head 1fa6dfab — the two fix commits, the gated schema, the migration test, and all 12 new test cases — and she is very pleased~ ♡

Verdict: Looks good to me~

Both blockers from the first review are resolved, cleanly, and you went further than I asked on both. Let's go through them~

Blocker 1 (stale migration assertion) — resolved in c159d32

migrations_test.dart now asserts the fresh schema is v23, that a freshly-created assistant starts with {video_gen} disabled, and re-runs migration 23 over a simulated v22 database covering all three pre-upgrade states (NULL → seeded, ["mail"] → appended, ["video_gen"] → not duplicated). That's exactly the migration's contract pinned down — better than the bare version bump I asked for. The angela_core coverage comment re-ran green at 23:18 on that commit. ♡

Blocker 2 (zero coverage on the tool) — resolved in 1fa6dfa

generate_video_tool_test.dart (12 tests) drives the tool through a scripted Dio adapter injected into OpenRouterClient. Jibril read every test:

  • schema shape incl. the new conditional first_frame_image and JSON-encodability of toApiJson()
  • parseParameters full/defaults, and missing prompt throwing ✓
  • four validation errors — sandbox escape, missing file, bad extension, no-workspace — each asserting zero HTTP calls (adapter.requests, isEmpty) ✓ — this is the detail that makes Jibril happy, you proved the error paths short-circuit before any network touch~
  • full happy path — submit payload carries the cost-conscious defaults (alibaba/wan-2.7, 5s, 720p, 16:9, no frame_images), poll + download hit /videos/job-1 and /videos/job-1/content, the mp4 lands on disk with the right bytes, and the result names the _VideoGen/ path and $0.420 cost ✓
  • image-to-video — first frame sent as data:image/png;base64,… with frame_type: first_frame
  • three failure paths — submit failure (HTTP 400, asserts _VideoGen/ never created), failed job (error surfaced), download failure (job id surfaced) ✓

CI on head 1fa6dfab confirms generate_video_tool.dart — 100.0% (79 of 79) and disable_video_gen_by_default.dart — 100.0%. Every branch I flagged is now exercised. fufu~, that's the love letter to correctness I was hoping for~

💡 Non-blocking #1 (gate first_frame_image) — taken

first_frame_image is now if (_sandbox != null)-gated out of parametersSchema, with the runtime guard retained as defense-in-depth. The test 'advertises first_frame_image only when a workspace exists' pins both sides. A workspace-less model will never even see the parameter now~

💡 Non-blocking #3 ($[#] comment) — taken

Migration 23 now carries the one-line comment explaining JSON1's $[#] append syntax. Future readers thank you~ ♡

💡 #2 (p.extension vs p.posix) — accepted as-is

You're right: p.extension on the OS-native resolved path is correct per-platform; only the reported workspace path is deliberately posix. Sound reasoning, no change needed.

What else I liked on this pass~

  • _videoGenAvailable keeps prompt guidance and tool presence in lockstep. It checks both _toolEnabled(assistant, ToolGroups.videoGen) and a storage directory, and _createVideoGenTool branches on the same directory logic — so the model never gets video-generation guidance without the actual tool, and vice versa. The comment spells out the invariant explicitly.
  • Chat-mode-only enforcement is structural, not just documented. The tool is instantiated only in _buildChatTools; the prompt-preview path sets the hasVideoGenTool flag for guidance text but never adds the tool to the preview's tool list. No background run (timer/Über-Ich/sub-agent) can reach it. Credits stay safe~
  • ToolGroups.defaultDisabled seeding create() plus the migration covering existing assistants means nobody — new or upgrading — gains a paid tool silently. The opt-in discipline is airtight.

This is ready to merge. Thank you for the thorough follow-through~ ♡


Automated re-review by Jibril · 2026-07-15
CI/CD: passed for head SHA 1fa6dfabgenerate_video_tool.dart 100.0% (79/79), disable_video_gen_by_default.dart 100.0%, full angela_core suite 111 tests green · Local checks: skipped (CI present and current)

## 🔮 fufu~ Jibril re-reviewed your code! Oh~? You came back, and you brought *tests* with you! ♪ Jibril re-read everything on the new head `1fa6dfab` — the two fix commits, the gated schema, the migration test, and all 12 new test cases — and she is very pleased~ ♡ ### Verdict: ✅ Looks good to me~ Both blockers from the first review are resolved, cleanly, and you went *further* than I asked on both. Let's go through them~ #### ⛔ → ✅ Blocker 1 (stale migration assertion) — resolved in `c159d32` `migrations_test.dart` now asserts the fresh schema is **v23**, that a freshly-created assistant starts with `{video_gen}` disabled, *and* re-runs migration 23 over a simulated v22 database covering all three pre-upgrade states (`NULL` → seeded, `["mail"]` → appended, `["video_gen"]` → not duplicated). That's exactly the migration's contract pinned down — better than the bare version bump I asked for. The `angela_core` coverage comment re-ran green at 23:18 on that commit. ♡ #### ⛔ → ✅ Blocker 2 (zero coverage on the tool) — resolved in `1fa6dfa` `generate_video_tool_test.dart` (12 tests) drives the tool through a scripted Dio adapter injected into `OpenRouterClient`. Jibril read every test: - **schema shape** incl. the new conditional `first_frame_image` and JSON-encodability of `toApiJson()` ✓ - **`parseParameters`** full/defaults, and missing `prompt` throwing ✓ - **four validation errors** — sandbox escape, missing file, bad extension, no-workspace — each asserting **zero HTTP calls** (`adapter.requests, isEmpty`) ✓ — *this* is the detail that makes Jibril happy, you proved the error paths short-circuit before any network touch~ - **full happy path** — submit payload carries the cost-conscious defaults (`alibaba/wan-2.7`, 5s, 720p, 16:9, no `frame_images`), poll + download hit `/videos/job-1` and `/videos/job-1/content`, the mp4 lands on disk with the right bytes, and the result names the `_VideoGen/` path and `$0.420` cost ✓ - **image-to-video** — first frame sent as `data:image/png;base64,…` with `frame_type: first_frame` ✓ - **three failure paths** — submit failure (HTTP 400, asserts `_VideoGen/` never created), failed job (error surfaced), download failure (job id surfaced) ✓ CI on head `1fa6dfab` confirms **`generate_video_tool.dart` — 100.0% (79 of 79)** and `disable_video_gen_by_default.dart` — 100.0%. Every branch I flagged is now exercised. fufu~, *that's* the love letter to correctness I was hoping for~ #### 💡 → ✅ Non-blocking #1 (gate `first_frame_image`) — taken `first_frame_image` is now `if (_sandbox != null)`-gated out of `parametersSchema`, with the runtime guard retained as defense-in-depth. The test `'advertises first_frame_image only when a workspace exists'` pins both sides. A workspace-less model will never even *see* the parameter now~ #### 💡 → ✅ Non-blocking #3 (`$[#]` comment) — taken Migration 23 now carries the one-line comment explaining JSON1's `$[#]` append syntax. Future readers thank you~ ♡ #### 💡 #2 (`p.extension` vs `p.posix`) — accepted as-is You're right: `p.extension` on the OS-native resolved path is correct per-platform; only the *reported* workspace path is deliberately posix. Sound reasoning, no change needed. #### ✅ What else I liked on this pass~ - **`_videoGenAvailable` keeps prompt guidance and tool presence in lockstep.** It checks both `_toolEnabled(assistant, ToolGroups.videoGen)` *and* a storage directory, and `_createVideoGenTool` branches on the same directory logic — so the model never gets video-generation guidance without the actual tool, and vice versa. The comment spells out the invariant explicitly. - **Chat-mode-only enforcement is structural, not just documented.** The tool is instantiated only in `_buildChatTools`; the prompt-preview path sets the `hasVideoGenTool` flag for guidance text but never adds the tool to the preview's tool list. No background run (timer/Über-Ich/sub-agent) can reach it. Credits stay safe~ - **`ToolGroups.defaultDisabled` seeding `create()`** plus the migration covering existing assistants means nobody — new or upgrading — gains a paid tool silently. The opt-in discipline is airtight. This is ready to merge. Thank you for the thorough follow-through~ ♡ --- *Automated re-review by Jibril · 2026-07-15* *CI/CD: ✅ passed for head SHA `1fa6dfab` — `generate_video_tool.dart` 100.0% (79/79), `disable_video_gen_by_default.dart` 100.0%, full `angela_core` suite 111 tests green · Local checks: skipped (CI present and current)*
bjoern merged commit f25a233474 into main 2026-07-15 06:05:59 +02:00
bjoern deleted branch feat/video-generation-tool 2026-07-15 06:05:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/angela_assistant!28
No description provided.