# 0042 — VitruAgent M2 (Voice Agent — Code Complete)

תאריך: 2026-06-03  
משך: ~3 שעות  
אפליקציה: VitruAgent (ויטרו)  
המשך מסשן 0040 (M1 ליבה).

## תקציר
M2 (שיחה קולית רציפה דרך OpenAI Realtime) — **קוד מלא ✓**, deploy חסום ע"י (a) vitpmis ב-Spark
(b) מפתח OpenAI לא הוגדר. אומת ש-vitpmis ב-Spark דרך `gcloud billing projects describe`.
כל הקוד מוכן וממתין ל-`firebase deploy --only functions` ברגע ששני החוסמים סגורים.

## מה נבנה

### Tools layer (M1 הורחב)
- 3 כלי-עריכה חדשים ב-`AgentTools` — `update_task_due_date` (dueInDays / dueDateIso / clear=true),
  `update_task_priority`, `update_task_title`. סך-הכל **12 כלים**.
- מתועד: schema ב-Dart ב-`lib/services/agent_tools.dart`, schema mirror ב-JS ב-
  `functions/agent_tools_schema.js`. סנכרון ידני (אין codegen, file size לא מצדיק).

### Backend — Cloud Function
- `functions/package.json` (Node 22) + `index.js`:
  - `mintRealtimeToken` callable v2, europe-west1, secret `OPENAI_API_KEY`
  - מודל: primary `gpt-realtime-mini` + fallback `gpt-4o-realtime-mini` אם 400/404 model-not-found
  - server-VAD (threshold 0.5, prefix 300ms, silence 500ms)
  - Whisper STT עם hint `language: 'he'`
  - tools + system-prompt מוזרקים לסשן (איו round-trip חוזר)
  - voice param מהקליינט (allowlist alloy/shimmer/verse/ash, fallback alloy)
  - metadata: `{firebase_uid}` ל-OpenAI usage tracking
  - שגיאות לא מודלפות לקליינט (משעי `console.error` בלבד)
- `firebase.json` קיבל בלוק `functions`

### Voice agent screen — `lib/screens/agent/agent_screen.dart` (~750 שורות)
- WebRTC: `RTCPeerConnection` עם Google STUN, DataChannel `oai-events` (חובה), SDP exchange
  ידני (`POST api.openai.com/v1/realtime?model=X` עם `Content-Type: application/sdp`).
- אירועים שמטופלים מה-DataChannel:
  - `response.audio_transcript.delta` → תמלול חי של הסוכן
  - `conversation.item.input_audio_transcription.completed` → Whisper של המשתמש
  - `response.function_call_arguments.done` → לולאת tools:
    1. `AgentTools.dispatch(name, args)` (client-side)
    2. `conversation.item.create` { type: function_call_output, call_id, output }
    3. `response.create` → הסוכן ממשיך לדבר עם התוצאה
  - `response.created/done` → state transitions
  - `error` → system bubble
- Voice Orb עם 7 מצבים (idle/connecting/listening/thinking/speaking/error/permissionDenied),
  pulse animation על listening+speaking, color+icon לפי state.
- בקרת עלות:
  - 5 דק׳ session cap (configurable 3/5/10)
  - 30s idle disconnect (configurable 15/30/60)
  - 20 דק׳/יום soft cap: warning ב-16 דק׳ (אמבר), hard-block ב-20 דק׳ (אדום)
  - daily counter ב-SharedPreferences, מוצג ב-AppBar עם צבע לפי tier
- בקרות נוספות:
  - **Mute toggle** — `track.enabled = false` בלי לסגור את הסשן
  - **Auto-scroll** — `ScrollController.animateTo(maxScrollExtent)` ב-postFrameCallback בכל turn
  - **Retry** — ב-state error כפתור שמפעיל את `_start` מחדש
  - **AppLifecycle observer** — `paused/inactive/detached` במהלך סשן → `_stop()` אוטומטי
    (מונע bleed של עלות+פרטיות כשהאפליקציה ברקע)
  - **permission_handler**:
    - `request()` לפני getUserMedia
    - אם `isPermanentlyDenied` → state נפרד `permissionDenied` עם כפתור "פתח הגדרות"
      (`openAppSettings()`) — לא לזרוק לגנרי error
  - **Settings bottom sheet** — voice picker (alloy/shimmer/verse/ash), session cap
    (3/5/10 דק׳), idle (15/30/60s). ChoiceChip picker עם `AgentPrefs.save`.
  - **Onboarding bottom sheet** — 8 דוגמאות phrases ב-3 קבוצות (אפשר לשאול / ליצור / לערוך)
    ב-first launch + Help icon ב-AppBar לפתיחה חוזרת.

### Supporting files
- `agent_prefs.dart` — `AgentSettings` class, defaults, allowed values, load/save דרך SharedPreferences
- `agent_onboarding.dart` — `isFirstTime()` / `markSeen()` / `show()` + `_OnboardingSheet` UI

### Wiring
- `main.dart`: AgentScreen = tab 0 (default). 6 tabs: ויטרו / עץ / משימות / פרויקטים / ציר / הגדרות
- `projects_screen.dart`: FAB מכוון ל-AgentScreen (היה ל-LiveModeScreen)
- `live_mode/` נמחק לחלוטין
- `voice_service.dart` קיבל כותרת שמתעדת שלא בשימוש ע"י זרימת הסוכן (Whisper בשרת)

### AndroidManifest
- `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_MICROPHONE` (Android 14+)
- `BLUETOOTH` + `BLUETOOTH_CONNECT` + `MODIFY_AUDIO_SETTINGS` (Bluetooth audio routing)
- `ACCESS_NETWORK_STATE`
- label שונה ל-`VitruAgent`

### pubspec
- **נוספו:** `flutter_webrtc 0.12.5`, `cloud_functions 5.1.3`, `shared_preferences 2.3.2`,
  `permission_handler 11.3.1`
- **הוסר:** `audioplayers`
- **ירד ל-transitive:** `web_socket_channel` (היה direct)

### Tests
- `test/services/agent_tools_test.dart` — 23/23 ירוק:
  - `openAiToolDefs` shape (12 entries, all `type: function`)
  - 12 שמות הכלים בדיוק
  - `rawTools` unmodifiable
  - routing — `dispatch('unknown_tool', {})` → error
  - input-validation עבור כל 12 הכלים (taskId/projectId/title חסר; enum invalid; date invalid)
  - persona prompt mentions "ויטרו", "סיימת", "גמרת"
- דרש 1-line fix ב-`FirestoreService`: `_db` הופך ל-lazy getter כדי שהsingleton לא יזרוק
  ב-load כש-Firebase לא מאותחל

### Lint
- `flutter analyze lib/screens/agent lib/services/agent_tools.dart` — `No issues found!`
- 82 הערות info ירושה מ-VitPMIS בקבצים אחרים — לא נגעתי (scope creep)

## גילויים / החלטות

- **vitpmis ב-Spark** — אומת `billingEnabled=false`. כל ה-Functions code ready, deploy חסום
- **`gpt-realtime-mini` שם המודל ב-2025/2026** — primary; אם OpenAI ישנה ל-`gpt-4o-realtime-mini`,
  fallback אוטומטי דרך error-detection ב-Function (model-name error → retry עם השני)
- **Schema mirroring (Dart ↔ JS)** — סנכרון ידני מוצדק כי File size קטן + Drift safe
  (DataChannel `session.update` שולח את ה-Dart side לאחר WebRTC connect, מסונכרן עם המודל בכל מקרה)
- **AppLifecycle pause** — קריטי לעלות+פרטיות; בלי זה Mic נשאר חי ברקע
- **permission_handler permanently-denied** — UX קריטי, אחרת brick wall
- **Onboarding** — 8 דוגמאות ב-3 קבוצות (לא overlay של feature list)
- **Cost cap layers** — session (5 דק׳) → idle (30s) → daily soft (20 דק׳ warn 16/block 20)

## Memory entries
- `project-vitpmis-spark-blocker.md` — תזכורת לכל סשן הבא
- `reference-agent-tools-schema-mirror.md` — חוק עריכה: Dart+JS schemas יחד

## הצעד הבא (0043)
לאחר שני החוסמים סגורים — `npm install` → `secrets:set` → `deploy` → `flutter run` →
אימות E2E (3 תרחישים: list_projects, add_task, complete_task).
