diff --git a/Cargo.lock b/Cargo.lock index 7feebbbd8..403354743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5141,6 +5141,7 @@ dependencies = [ "serde", "serde-big-array", "serde_json", + "serde_repr", "serde_yaml", "sha1", "sha2 0.10.9", @@ -7176,6 +7177,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_spanned" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index d27b6d8a1..43b054015 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ crate-type = ["rlib"] [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_repr = "0.1" serde_yaml = "0.9" # (Removed `html2md` dep. dhat-rs profiling on real Gmail inboxes # showed `html2md::walk` and `html2md::tables::handle` allocating diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 2ad79bb22..09e38275c 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -5394,6 +5394,7 @@ dependencies = [ "sentry", "serde", "serde_json", + "serde_repr", "serde_yaml", "sha1", "sha2 0.10.9", diff --git a/app/src/components/intelligence/SyncBudgetDialog.tsx b/app/src/components/intelligence/SyncBudgetDialog.tsx new file mode 100644 index 000000000..4f8de3f91 --- /dev/null +++ b/app/src/components/intelligence/SyncBudgetDialog.tsx @@ -0,0 +1,139 @@ +import { useCallback, useState } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import { updateMemorySource } from '../../services/memorySourcesService'; + +interface SyncBudgetDialogProps { + source: { + id: string; + label: string; + max_tokens_per_sync?: number | null; + max_cost_per_sync_usd?: number | null; + sync_depth_days?: number | null; + }; + onClose: () => void; + onSaved: () => void; +} + +export default function SyncBudgetDialog({ source, onClose, onSaved }: SyncBudgetDialogProps) { + const { t } = useT(); + const [maxTokens, setMaxTokens] = useState(source.max_tokens_per_sync?.toString() ?? ''); + const [maxCost, setMaxCost] = useState(source.max_cost_per_sync_usd?.toString() ?? ''); + const [depthDays, setDepthDays] = useState(source.sync_depth_days?.toString() ?? ''); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const handleSave = useCallback(async () => { + setSaving(true); + setError(null); + try { + await updateMemorySource(source.id, { + max_tokens_per_sync: maxTokens ? Number(maxTokens) : undefined, + max_cost_per_sync_usd: maxCost ? Number(maxCost) : undefined, + sync_depth_days: depthDays ? Number(depthDays) : undefined, + } as Parameters[1]); + onSaved(); + onClose(); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setSaving(false); + } + }, [source.id, maxTokens, maxCost, depthDays, onSaved, onClose]); + + return ( +
+
e.stopPropagation()}> +

+ {t('syncBudget.title')} +

+

{source.label}

+ +
+
+ +

+ {t('syncBudget.maxTokensHelp')} +

+ setMaxTokens(e.target.value)} + placeholder={t('syncBudget.unlimited')} + className="w-full px-3 py-1.5 rounded-md border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-sm font-mono" + /> +
+ +
+ +

+ {t('syncBudget.maxCostHelp')} +

+ setMaxCost(e.target.value)} + placeholder={t('syncBudget.unlimited')} + className="w-full px-3 py-1.5 rounded-md border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-sm font-mono" + /> +
+ +
+ +

+ {t('syncBudget.syncDepthHelp')} +

+ +
+
+ + {error &&

{error}

} + +
+ + +
+
+
+ ); +} diff --git a/app/src/components/intelligence/SyncConfirmDialog.tsx b/app/src/components/intelligence/SyncConfirmDialog.tsx new file mode 100644 index 000000000..aa0650a2e --- /dev/null +++ b/app/src/components/intelligence/SyncConfirmDialog.tsx @@ -0,0 +1,109 @@ +import { useEffect, useState } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import { callCoreRpc } from '../../services/coreRpcClient'; + +interface SyncEstimate { + item_count: number; + estimated_tokens: number; + estimated_cost_usd: number; + budget_max_cost_usd: number | null; + budget_max_tokens: number | null; +} + +interface SyncConfirmDialogProps { + sourceId: string; + onConfirm: () => void; + onCancel: () => void; +} + +export default function SyncConfirmDialog({ + sourceId, + onConfirm, + onCancel, +}: SyncConfirmDialogProps) { + const { t } = useT(); + const [estimate, setEstimate] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setEstimate(null); + setError(null); + (async () => { + try { + const resp = await callCoreRpc<{ result: SyncEstimate }>({ + method: 'openhuman.memory_sources_estimate_sync_cost', + params: { source_id: sourceId }, + }); + if (!cancelled) setEstimate(resp.result); + } catch (e) { + if (!cancelled) setError(e instanceof Error ? e.message : String(e)); + } + })(); + return () => { + cancelled = true; + }; + }, [sourceId]); + + const tokenStr = estimate + ? estimate.estimated_tokens > 1000 + ? `${Math.round(estimate.estimated_tokens / 1000)}k` + : String(estimate.estimated_tokens) + : ''; + + return ( +
+
e.stopPropagation()}> +

+ {t('syncConfirm.title')} +

+ + {!estimate && !error && ( +

+ {t('syncConfirm.estimating')} +

+ )} + + {error &&

{error}

} + + {estimate && ( +
+

+ {t('syncConfirm.message') + .replace('{items}', String(estimate.item_count)) + .replace('{tokens}', tokenStr) + .replace('{cost}', estimate.estimated_cost_usd.toFixed(4))} +

+ {estimate.budget_max_cost_usd != null && ( +

+ {t('syncConfirm.budgetNote').replace( + '{max}', + estimate.budget_max_cost_usd.toFixed(2) + )} +

+ )} +
+ )} + +
+ + +
+
+
+ ); +} diff --git a/app/src/components/settings/panels/AgentActivityPanel.tsx b/app/src/components/settings/panels/AgentActivityPanel.tsx new file mode 100644 index 000000000..dc8058d9c --- /dev/null +++ b/app/src/components/settings/panels/AgentActivityPanel.tsx @@ -0,0 +1,182 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { useT } from '../../../lib/i18n/I18nContext'; +import { callCoreRpc } from '../../../services/coreRpcClient'; + +interface ActivityLevelSettings { + level: number; + level_label: string; + sync_interval_secs: number | null; + heartbeat_enabled: boolean; + subconscious_enabled: boolean; + token_budget_per_cycle: number | null; + estimated_monthly_cost_min_usd: number; + estimated_monthly_cost_max_usd: number; +} + +interface MonthlyCostSummary { + month: string; + total_cost_usd: number; + total_syncs: number; +} + +const LEVELS = [ + { key: 'off', value: 0 }, + { key: 'minimal', value: 1 }, + { key: 'moderate', value: 2 }, + { key: 'active', value: 3 }, + { key: 'alwaysOn', value: 4 }, +] as const; + +type LevelKey = (typeof LEVELS)[number]['key']; + +type Status = 'idle' | 'loading' | 'saving' | 'saved' | 'error'; + +// These tables intentionally duplicate the backend constants in +// AgentActivityLevel::estimated_monthly_cost_range (config/schema/activity_level.rs). +// The backend only returns cost ranges for the *current* level, so we need a +// static lookup to render cost estimates for all levels simultaneously. +// A future RPC that returns ranges for all levels would allow removing these. +function getCostMin(level: number): number { + return [0, 0.1, 1, 5, 20][level] ?? 0; +} + +function getCostMax(level: number): number { + return [0, 0.5, 5, 20, 100][level] ?? 0; +} + +export default function AgentActivityPanel() { + const { t } = useT(); + const [settings, setSettings] = useState(null); + const [monthlyCost, setMonthlyCost] = useState(null); + const [status, setStatus] = useState('loading'); + const [error, setError] = useState(null); + + const loadSettings = useCallback(async () => { + try { + setStatus('loading'); + const [settingsResp, costResp] = await Promise.all([ + callCoreRpc<{ result: ActivityLevelSettings }>({ + method: 'openhuman.config_get_activity_level_settings', + params: {}, + }), + callCoreRpc<{ result: MonthlyCostSummary }>({ + method: 'openhuman.memory_sources_monthly_cost_summary', + params: {}, + }), + ]); + setSettings(settingsResp.result); + setMonthlyCost(costResp.result); + setStatus('idle'); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + setStatus('error'); + } + }, []); + + useEffect(() => { + loadSettings(); + }, [loadSettings]); + + const handleLevelChange = useCallback(async (levelKey: string) => { + try { + setStatus('saving'); + setError(null); + const resp = await callCoreRpc<{ result: ActivityLevelSettings }>({ + method: 'openhuman.config_update_activity_level_settings', + params: { level: levelKey }, + }); + setSettings(resp.result); + setStatus('saved'); + setTimeout(() => setStatus('idle'), 2000); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + setStatus('error'); + } + }, []); + + if (status === 'loading' && !settings) { + return ( +
{t('common.loading')}
+ ); + } + + return ( +
+
+

+ {t('activityLevel.title')} +

+

+ {t('activityLevel.description')} +

+
+ + {monthlyCost && monthlyCost.total_cost_usd > 0 && ( +
+ + {t('activityLevel.currentMonth').replace( + '{amount}', + monthlyCost.total_cost_usd.toFixed(2) + )} + +
+ )} + +
+ {LEVELS.map(({ key, value }) => { + const isSelected = settings?.level === value; + const apiKey = key === 'alwaysOn' ? 'always_on' : (key as string); + const costMin = getCostMin(value); + const costMax = getCostMax(value); + return ( + + ); + })} +
+ +
+ {status === 'saving' && ( + {t('autonomy.statusSaving')} + )} + {status === 'saved' && ( + {t('activityLevel.saved')} + )} + {error && {error}} +
+
+ ); +} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 32bdad73e..91985d2b2 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4372,6 +4372,61 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'رفض التخزين المحلي', 'pages.settings.account.security': 'الأمان', 'pages.settings.account.securityDesc': 'وضع تخزين الأسرار وحالة سلسلة المفاتيح', + + // Agent activity level + 'activityLevel.title': 'مستوى نشاط الوكيل', + 'activityLevel.description': + 'تحكم في مدى استباقية وكيلك. المستويات الأعلى تستخدم المزيد من الرموز.', + 'activityLevel.off': 'إيقاف', + 'activityLevel.offDesc': 'لا معالجة في الخلفية. يزامن فقط عند الضغط على الزر.', + 'activityLevel.minimal': 'أدنى حد', + 'activityLevel.minimalDesc': 'مزامنة المصادر مرة يوميًا. لا رسائل استباقية.', + 'activityLevel.moderate': 'متوسط', + 'activityLevel.moderateDesc': 'مزامنة كل ساعة. ملخص يومي. يقترح إجراءات.', + 'activityLevel.active': 'نشط', + 'activityLevel.activeDesc': 'مزامنة كل 10 دقائق. يراقب القنوات ويصنف ويصيغ الردود.', + 'activityLevel.alwaysOn': 'دائم التشغيل', + 'activityLevel.alwaysOnDesc': 'مزامنة فورية. استقلالية كاملة ضمن الحدود المحددة.', + 'activityLevel.currentMonth': 'هذا الشهر: ${amount}', + 'activityLevel.saved': 'تم تحديث مستوى النشاط.', + 'activityLevel.default': 'افتراضي', + 'activityLevel.costFree': '0$', + 'activityLevel.costRange': '~${min}–${max}/شهر', + + // Sync budget dialog + 'syncBudget.title': 'ميزانية المزامنة', + 'syncBudget.maxTokens': 'الحد الأقصى للرموز لكل مزامنة', + 'syncBudget.maxTokensHelp': 'توقف المزامنة عند استهلاك هذا العدد من الرموز.', + 'syncBudget.maxCost': 'الحد الأقصى للتكلفة لكل مزامنة (USD)', + 'syncBudget.maxCostHelp': 'حد التكلفة المطلق بالدولار لكل تشغيل مزامنة.', + 'syncBudget.syncDepth': 'عمق المزامنة', + 'syncBudget.syncDepthHelp': 'جلب العناصر من هذه الفترة الزمنية فقط.', + 'syncBudget.days7': 'آخر 7 أيام', + 'syncBudget.days30': 'آخر 30 يومًا', + 'syncBudget.days90': 'آخر 90 يومًا', + 'syncBudget.allTime': 'كل الأوقات', + 'syncBudget.unlimited': 'غير محدود', + 'syncBudget.saved': 'تم حفظ الميزانية.', + + // Sync confirm dialog + 'syncConfirm.title': 'تأكيد المزامنة', + 'syncConfirm.message': 'ستعالج هذه المزامنة ~{items} عناصر (~{tokens} رمز، تقديري ${cost}).', + 'syncConfirm.budgetNote': 'حد الميزانية: ${max}', + 'syncConfirm.proceed': 'المتابعة', + 'syncConfirm.cancel': 'إلغاء', + 'syncConfirm.estimating': 'جارٍ تقدير التكلفة...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} هذا الشهر', + 'monthlyCost.noData': 'لا مزامنات هذا الشهر', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'النشاط', + 'onboarding.custom.activity.title': 'نشاط الوكيل', + 'onboarding.custom.activity.subtitle': 'مدى استباقية وكيلك في المراقبة والتصرف في الخلفية.', + 'onboarding.custom.activity.defaultDesc': 'نشاط متوسط — مزامنة كل ساعة، ملخص يومي.', + 'onboarding.custom.activity.configureDesc': + 'اختر مستوى نشاطك الخاص. الإعداد في الإعدادات › مستوى نشاط الوكيل.', }; export default messages; diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 380616ccb..8e6bb1d3e 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4450,6 +4450,66 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'স্থানীয় সঞ্চয়স্থান প্রত্যাখ্যান করুন', 'pages.settings.account.security': 'নিরাপত্তা', 'pages.settings.account.securityDesc': 'গোপনীয়তা সঞ্চয়স্থান মোড এবং কিচেন অবস্থা', + + // Agent activity level + 'activityLevel.title': 'এজেন্ট কার্যকলাপের স্তর', + 'activityLevel.description': + 'আপনার এজেন্ট কতটা সক্রিয় তা নিয়ন্ত্রণ করুন। উচ্চ স্তরে বেশি টোকেন ব্যবহার হয়।', + 'activityLevel.off': 'বন্ধ', + 'activityLevel.offDesc': 'পটভূমিতে কোনো প্রক্রিয়াকরণ নেই। শুধুমাত্র বোতাম চাপলে সিঙ্ক করে।', + 'activityLevel.minimal': 'ন্যূনতম', + 'activityLevel.minimalDesc': 'প্রতিদিন একবার উৎস সিঙ্ক করে। কোনো সক্রিয় বার্তা নেই।', + 'activityLevel.moderate': 'মাঝারি', + 'activityLevel.moderateDesc': + 'প্রতি ঘণ্টায় সিঙ্ক করে। দৈনিক সারসংক্ষেপ। কর্মপন্থা প্রস্তাব করে।', + 'activityLevel.active': 'সক্রিয়', + 'activityLevel.activeDesc': + 'প্রতি ১০ মিনিটে সিঙ্ক করে। চ্যানেল পর্যবেক্ষণ, বাছাই ও উত্তর খসড়া করে।', + 'activityLevel.alwaysOn': 'সর্বদা চালু', + 'activityLevel.alwaysOnDesc': 'রিয়েল-টাইম সিঙ্ক। নির্ধারিত সীমার মধ্যে পূর্ণ স্বায়ত্তশাসন।', + 'activityLevel.currentMonth': 'এই মাস: ${amount}', + 'activityLevel.saved': 'কার্যকলাপের স্তর আপডেট হয়েছে।', + 'activityLevel.default': 'ডিফল্ট', + 'activityLevel.costFree': '$০', + 'activityLevel.costRange': '~${min}–${max}/মাস', + + // Sync budget dialog + 'syncBudget.title': 'সিঙ্ক বাজেট', + 'syncBudget.maxTokens': 'প্রতি সিঙ্কে সর্বোচ্চ টোকেন', + 'syncBudget.maxTokensHelp': 'এতটা টোকেন ব্যবহার হলে সিঙ্ক বন্ধ করুন।', + 'syncBudget.maxCost': 'প্রতি সিঙ্কে সর্বোচ্চ খরচ (USD)', + 'syncBudget.maxCostHelp': 'প্রতিটি সিঙ্ক রানের জন্য নির্দিষ্ট ডলার সীমা।', + 'syncBudget.syncDepth': 'সিঙ্ক গভীরতা', + 'syncBudget.syncDepthHelp': 'কেবল এই সময়সীমার আইটেম আনুন।', + 'syncBudget.days7': 'গত ৭ দিন', + 'syncBudget.days30': 'গত ৩০ দিন', + 'syncBudget.days90': 'গত ৯০ দিন', + 'syncBudget.allTime': 'সব সময়', + 'syncBudget.unlimited': 'সীমাহীন', + 'syncBudget.saved': 'বাজেট সংরক্ষিত হয়েছে।', + + // Sync confirm dialog + 'syncConfirm.title': 'সিঙ্ক নিশ্চিত করুন', + 'syncConfirm.message': + 'এই সিঙ্ক ~{items}টি আইটেম (~{tokens} টোকেন, আনুমানিক ${cost}) প্রক্রিয়া করবে।', + 'syncConfirm.budgetNote': 'বাজেট সীমা: ${max}', + 'syncConfirm.proceed': 'চালিয়ে যান', + 'syncConfirm.cancel': 'বাতিল', + 'syncConfirm.estimating': 'খরচ অনুমান করা হচ্ছে...', + + // Monthly cost badge + 'monthlyCost.badge': 'এই মাসে ${amount}', + 'monthlyCost.noData': 'এই মাসে কোনো সিঙ্ক নেই', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'কার্যকলাপ', + 'onboarding.custom.activity.title': 'এজেন্টের কার্যকলাপ', + 'onboarding.custom.activity.subtitle': + 'আপনার এজেন্ট পটভূমিতে কতটা সক্রিয়ভাবে পর্যবেক্ষণ ও কাজ করে।', + 'onboarding.custom.activity.defaultDesc': + 'মাঝারি কার্যকলাপ — প্রতি ঘণ্টায় সিঙ্ক, দৈনিক সারসংক্ষেপ।', + 'onboarding.custom.activity.configureDesc': + 'নিজের কার্যকলাপের স্তর বেছে নিন। সেটিংস › এজেন্ট কার্যকলাপ স্তরে কনফিগার করুন।', }; export default messages; diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index c515e38cb..44d4918ec 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -4567,6 +4567,69 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Lokalen Speicher ablehnen', 'pages.settings.account.security': 'Sicherheit', 'pages.settings.account.securityDesc': 'Geheimnisspeicher-Modus und Schlüsselbund-Status', + + // Agent activity level + 'activityLevel.title': 'Agent-Aktivitätsstufe', + 'activityLevel.description': + 'Steuern Sie, wie proaktiv Ihr Agent ist. Höhere Stufen verbrauchen mehr Tokens.', + 'activityLevel.off': 'Aus', + 'activityLevel.offDesc': 'Keine Hintergrundverarbeitung. Synchronisiert nur auf Knopfdruck.', + 'activityLevel.minimal': 'Minimal', + 'activityLevel.minimalDesc': + 'Quellen einmal täglich synchronisieren. Keine proaktiven Nachrichten.', + 'activityLevel.moderate': 'Moderat', + 'activityLevel.moderateDesc': + 'Stündliche Synchronisierung. Tägliche Zusammenfassung. Schlägt Aktionen vor.', + 'activityLevel.active': 'Aktiv', + 'activityLevel.activeDesc': + 'Alle 10 Minuten synchronisieren. Überwacht Kanäle, priorisiert und entwirft Antworten.', + 'activityLevel.alwaysOn': 'Immer aktiv', + 'activityLevel.alwaysOnDesc': + 'Echtzeit-Synchronisierung. Volle Autonomie innerhalb von Leitplanken.', + 'activityLevel.currentMonth': 'Dieser Monat: ${amount}', + 'activityLevel.saved': 'Aktivitätsstufe aktualisiert.', + 'activityLevel.default': 'Standard', + 'activityLevel.costFree': '0 $', + 'activityLevel.costRange': '~${min}–${max}/Monat', + + // Sync budget dialog + 'syncBudget.title': 'Synchronisierungsbudget', + 'syncBudget.maxTokens': 'Max. Tokens pro Synchronisierung', + 'syncBudget.maxTokensHelp': + 'Synchronisierung stoppen, sobald diese Anzahl von Tokens verbraucht wurde.', + 'syncBudget.maxCost': 'Max. Kosten pro Synchronisierung (USD)', + 'syncBudget.maxCostHelp': 'Absolutes Kostenlimit pro Synchronisierungslauf.', + 'syncBudget.syncDepth': 'Synchronisierungstiefe', + 'syncBudget.syncDepthHelp': 'Nur Elemente aus diesem Zeitfenster abrufen.', + 'syncBudget.days7': 'Letzte 7 Tage', + 'syncBudget.days30': 'Letzte 30 Tage', + 'syncBudget.days90': 'Letzte 90 Tage', + 'syncBudget.allTime': 'Gesamte Zeit', + 'syncBudget.unlimited': 'Unbegrenzt', + 'syncBudget.saved': 'Budget gespeichert.', + + // Sync confirm dialog + 'syncConfirm.title': 'Synchronisierung bestätigen', + 'syncConfirm.message': + 'Diese Synchronisierung verarbeitet ~{items} Elemente (~{tokens} Tokens, ca. ${cost}).', + 'syncConfirm.budgetNote': 'Budgetlimit: ${max}', + 'syncConfirm.proceed': 'Fortfahren', + 'syncConfirm.cancel': 'Abbrechen', + 'syncConfirm.estimating': 'Kosten werden geschätzt...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} diesen Monat', + 'monthlyCost.noData': 'Keine Synchronisierungen diesen Monat', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Aktivität', + 'onboarding.custom.activity.title': 'Agent-Aktivität', + 'onboarding.custom.activity.subtitle': + 'Wie proaktiv Ihr Agent im Hintergrund überwacht und handelt.', + 'onboarding.custom.activity.defaultDesc': + 'Moderate Aktivität – stündliche Synchronisierung, tägliche Zusammenfassung.', + 'onboarding.custom.activity.configureDesc': + 'Eigene Aktivitätsstufe wählen. Konfigurieren in Einstellungen › Agent-Aktivitätsstufe.', }; export default messages; diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index ab85b6715..016360ff0 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -608,12 +608,13 @@ const en: TranslationMap = { 'onboarding.apiKeys.continue': 'Save and continue', 'onboarding.apiKeys.saving': 'Saving…', - // Onboarding: Custom wizard (Inference / Voice / OAuth / Search / Memory) + // Onboarding: Custom wizard (Inference / Voice / OAuth / Search / Activity / Memory) 'onboarding.custom.stepperInference': 'Inference', 'onboarding.custom.stepperVoice': 'Voice', 'onboarding.custom.stepperOAuth': 'OAuth', 'onboarding.custom.stepperSearch': 'Search', 'onboarding.custom.stepperEmbeddings': 'Embeddings', + 'onboarding.custom.stepperActivity': 'Activity', 'onboarding.custom.stepperMemory': 'Memory', 'onboarding.custom.stepCounter': 'Step {n} of {total}', 'onboarding.custom.defaultTitle': 'Default', @@ -671,6 +672,15 @@ const en: TranslationMap = { 'onboarding.custom.embeddings.configureDesc': 'Bring your own embedding provider (OpenAI, Voyage, Ollama, etc.).', + // Onboarding: Custom > Activity + 'onboarding.custom.activity.title': 'Agent Activity', + 'onboarding.custom.activity.subtitle': + 'How proactively your agent monitors and acts in the background.', + 'onboarding.custom.activity.defaultDesc': + 'Moderate activity — syncs every hour, sends a daily digest. Balanced cost and responsiveness.', + 'onboarding.custom.activity.configureDesc': + 'Pick your own activity level. Configure in Settings › Agent activity level.', + // Onboarding: Custom > Memory 'onboarding.custom.memory.title': 'Memory', 'onboarding.custom.memory.subtitle': @@ -680,6 +690,26 @@ const en: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Inspect, export, or wipe memory yourself. Configure in Settings › Memory.', + // Agent activity level + 'activityLevel.title': 'Agent activity level', + 'activityLevel.description': + 'Control how proactive your agent is. Higher levels use more tokens.', + 'activityLevel.off': 'Off', + 'activityLevel.offDesc': 'No background processing. Syncs only when you press the button.', + 'activityLevel.minimal': 'Minimal', + 'activityLevel.minimalDesc': 'Sync sources once per day. No proactive messages.', + 'activityLevel.moderate': 'Moderate', + 'activityLevel.moderateDesc': 'Sync every hour. Daily digest. Suggests actions.', + 'activityLevel.active': 'Active', + 'activityLevel.activeDesc': 'Sync every 10 min. Monitors channels, triages, drafts replies.', + 'activityLevel.alwaysOn': 'Always-on', + 'activityLevel.alwaysOnDesc': 'Real-time sync. Full autonomy within guardrails.', + 'activityLevel.currentMonth': 'This month: ${amount}', + 'activityLevel.saved': 'Activity level updated.', + 'activityLevel.default': 'default', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/mo', + // Accounts 'accounts.addAccount': 'Add Account', 'accounts.manageAccounts': 'Manage Accounts', @@ -4679,6 +4709,33 @@ const en: TranslationMap = { 'keyring.settings.revokeConsent': 'Decline local storage', 'pages.settings.account.security': 'Security', 'pages.settings.account.securityDesc': 'Secret storage mode and keychain status', + + // Sync budget dialog + 'syncBudget.title': 'Sync budget', + 'syncBudget.maxTokens': 'Max tokens per sync', + 'syncBudget.maxTokensHelp': 'Stop syncing once this many tokens are consumed.', + 'syncBudget.maxCost': 'Max cost per sync (USD)', + 'syncBudget.maxCostHelp': 'Hard dollar cap per sync run.', + 'syncBudget.syncDepth': 'Sync depth', + 'syncBudget.syncDepthHelp': 'Only fetch items from this time window.', + 'syncBudget.days7': 'Last 7 days', + 'syncBudget.days30': 'Last 30 days', + 'syncBudget.days90': 'Last 90 days', + 'syncBudget.allTime': 'All time', + 'syncBudget.unlimited': 'Unlimited', + 'syncBudget.saved': 'Budget saved.', + + // Sync confirm dialog + 'syncConfirm.title': 'Confirm sync', + 'syncConfirm.message': 'This sync will process ~{items} items (~{tokens} tokens, est. ${cost}).', + 'syncConfirm.budgetNote': 'Budget cap: ${max}', + 'syncConfirm.proceed': 'Proceed', + 'syncConfirm.cancel': 'Cancel', + 'syncConfirm.estimating': 'Estimating cost...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} this month', + 'monthlyCost.noData': 'No syncs this month', }; export default en; diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index fbf428496..7cb1f0e91 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -4533,6 +4533,67 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Rechazar almacenamiento local', 'pages.settings.account.security': 'Seguridad', 'pages.settings.account.securityDesc': 'Modo de almacenamiento de secretos y estado del llavero', + + // Agent activity level + 'activityLevel.title': 'Nivel de actividad del agente', + 'activityLevel.description': + 'Controla qué tan proactivo es tu agente. Los niveles más altos usan más tokens.', + 'activityLevel.off': 'Desactivado', + 'activityLevel.offDesc': + 'Sin procesamiento en segundo plano. Sincroniza solo al presionar el botón.', + 'activityLevel.minimal': 'Mínimo', + 'activityLevel.minimalDesc': 'Sincroniza fuentes una vez al día. Sin mensajes proactivos.', + 'activityLevel.moderate': 'Moderado', + 'activityLevel.moderateDesc': 'Sincroniza cada hora. Resumen diario. Sugiere acciones.', + 'activityLevel.active': 'Activo', + 'activityLevel.activeDesc': + 'Sincroniza cada 10 min. Monitorea canales, prioriza y redacta respuestas.', + 'activityLevel.alwaysOn': 'Siempre activo', + 'activityLevel.alwaysOnDesc': + 'Sincronización en tiempo real. Autonomía total dentro de los límites.', + 'activityLevel.currentMonth': 'Este mes: ${amount}', + 'activityLevel.saved': 'Nivel de actividad actualizado.', + 'activityLevel.default': 'predeterminado', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/mes', + + // Sync budget dialog + 'syncBudget.title': 'Presupuesto de sincronización', + 'syncBudget.maxTokens': 'Tokens máximos por sincronización', + 'syncBudget.maxTokensHelp': 'Detener la sincronización al consumir este número de tokens.', + 'syncBudget.maxCost': 'Costo máximo por sincronización (USD)', + 'syncBudget.maxCostHelp': 'Límite absoluto en dólares por ejecución de sincronización.', + 'syncBudget.syncDepth': 'Profundidad de sincronización', + 'syncBudget.syncDepthHelp': 'Solo obtener elementos de esta ventana de tiempo.', + 'syncBudget.days7': 'Últimos 7 días', + 'syncBudget.days30': 'Últimos 30 días', + 'syncBudget.days90': 'Últimos 90 días', + 'syncBudget.allTime': 'Todo el tiempo', + 'syncBudget.unlimited': 'Ilimitado', + 'syncBudget.saved': 'Presupuesto guardado.', + + // Sync confirm dialog + 'syncConfirm.title': 'Confirmar sincronización', + 'syncConfirm.message': + 'Esta sincronización procesará ~{items} elementos (~{tokens} tokens, est. ${cost}).', + 'syncConfirm.budgetNote': 'Límite de presupuesto: ${max}', + 'syncConfirm.proceed': 'Proceder', + 'syncConfirm.cancel': 'Cancelar', + 'syncConfirm.estimating': 'Calculando costo...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} este mes', + 'monthlyCost.noData': 'Sin sincronizaciones este mes', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Actividad', + 'onboarding.custom.activity.title': 'Actividad del agente', + 'onboarding.custom.activity.subtitle': + 'Qué tan proactivamente monitorea y actúa tu agente en segundo plano.', + 'onboarding.custom.activity.defaultDesc': + 'Actividad moderada — sincronización por hora, resumen diario.', + 'onboarding.custom.activity.configureDesc': + 'Elige tu propio nivel de actividad. Configurar en Ajustes › Nivel de actividad del agente.', }; export default messages; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 24ff48ae7..93abd8fd2 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -4548,6 +4548,68 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Refuser le stockage local', 'pages.settings.account.security': 'Sécurité', 'pages.settings.account.securityDesc': 'Mode de stockage des secrets et état du trousseau', + + // Agent activity level + 'activityLevel.title': "Niveau d'activité de l'agent", + 'activityLevel.description': + 'Contrôlez le niveau de proactivité de votre agent. Les niveaux élevés consomment plus de tokens.', + 'activityLevel.off': 'Désactivé', + 'activityLevel.offDesc': + 'Aucun traitement en arrière-plan. Synchronise uniquement sur pression du bouton.', + 'activityLevel.minimal': 'Minimal', + 'activityLevel.minimalDesc': 'Synchronise les sources une fois par jour. Aucun message proactif.', + 'activityLevel.moderate': 'Modéré', + 'activityLevel.moderateDesc': + 'Synchronise toutes les heures. Résumé quotidien. Suggère des actions.', + 'activityLevel.active': 'Actif', + 'activityLevel.activeDesc': + 'Synchronise toutes les 10 min. Surveille les canaux, trie et rédige des réponses.', + 'activityLevel.alwaysOn': 'Toujours actif', + 'activityLevel.alwaysOnDesc': + 'Synchronisation en temps réel. Pleine autonomie dans les limites définies.', + 'activityLevel.currentMonth': 'Ce mois-ci : ${amount}', + 'activityLevel.saved': "Niveau d'activité mis à jour.", + 'activityLevel.default': 'par défaut', + 'activityLevel.costFree': '0 $', + 'activityLevel.costRange': '~${min}–${max}/mois', + + // Sync budget dialog + 'syncBudget.title': 'Budget de synchronisation', + 'syncBudget.maxTokens': 'Tokens max par synchronisation', + 'syncBudget.maxTokensHelp': 'Arrêter la synchronisation une fois ce nombre de tokens consommé.', + 'syncBudget.maxCost': 'Coût max par synchronisation (USD)', + 'syncBudget.maxCostHelp': 'Plafond absolu en dollars par exécution de synchronisation.', + 'syncBudget.syncDepth': 'Profondeur de synchronisation', + 'syncBudget.syncDepthHelp': 'Récupérer uniquement les éléments de cette fenêtre temporelle.', + 'syncBudget.days7': '7 derniers jours', + 'syncBudget.days30': '30 derniers jours', + 'syncBudget.days90': '90 derniers jours', + 'syncBudget.allTime': 'Tout le temps', + 'syncBudget.unlimited': 'Illimité', + 'syncBudget.saved': 'Budget enregistré.', + + // Sync confirm dialog + 'syncConfirm.title': 'Confirmer la synchronisation', + 'syncConfirm.message': + 'Cette synchronisation traitera ~{items} éléments (~{tokens} tokens, est. ${cost}).', + 'syncConfirm.budgetNote': 'Plafond budgétaire : ${max}', + 'syncConfirm.proceed': 'Procéder', + 'syncConfirm.cancel': 'Annuler', + 'syncConfirm.estimating': 'Estimation du coût...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} ce mois-ci', + 'monthlyCost.noData': 'Aucune synchronisation ce mois-ci', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Activité', + 'onboarding.custom.activity.title': "Activité de l'agent", + 'onboarding.custom.activity.subtitle': + 'À quel point votre agent surveille et agit en arrière-plan.', + 'onboarding.custom.activity.defaultDesc': + 'Activité modérée — synchronisation horaire, résumé quotidien.', + 'onboarding.custom.activity.configureDesc': + "Choisissez votre propre niveau d'activité. Configurer dans Paramètres › Niveau d'activité de l'agent.", }; export default messages; diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 245983a51..4d32f43c4 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4457,6 +4457,63 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'स्थानीय भंडारण अस्वीकार करें', 'pages.settings.account.security': 'सुरक्षा', 'pages.settings.account.securityDesc': 'रहस्य भंडारण मोड और कीचेन स्थिति', + + // Agent activity level + 'activityLevel.title': 'एजेंट गतिविधि स्तर', + 'activityLevel.description': + 'नियंत्रित करें कि आपका एजेंट कितना सक्रिय है। उच्च स्तर पर अधिक टोकन का उपयोग होता है।', + 'activityLevel.off': 'बंद', + 'activityLevel.offDesc': 'पृष्ठभूमि में कोई प्रसंस्करण नहीं। केवल बटन दबाने पर सिंक होता है।', + 'activityLevel.minimal': 'न्यूनतम', + 'activityLevel.minimalDesc': 'दिन में एक बार स्रोत सिंक करता है। कोई सक्रिय संदेश नहीं।', + 'activityLevel.moderate': 'मध्यम', + 'activityLevel.moderateDesc': 'हर घंटे सिंक करता है। दैनिक सारांश। क्रियाएं सुझाता है।', + 'activityLevel.active': 'सक्रिय', + 'activityLevel.activeDesc': + 'हर 10 मिनट में सिंक करता है। चैनलों की निगरानी करता है, प्राथमिकता देता है और उत्तर तैयार करता है।', + 'activityLevel.alwaysOn': 'हमेशा चालू', + 'activityLevel.alwaysOnDesc': 'रीयल-टाइम सिंक। निर्धारित सीमाओं के भीतर पूर्ण स्वायत्तता।', + 'activityLevel.currentMonth': 'इस महीने: ${amount}', + 'activityLevel.saved': 'गतिविधि स्तर अपडेट किया गया।', + 'activityLevel.default': 'डिफ़ॉल्ट', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/माह', + + // Sync budget dialog + 'syncBudget.title': 'सिंक बजट', + 'syncBudget.maxTokens': 'प्रति सिंक अधिकतम टोकन', + 'syncBudget.maxTokensHelp': 'इतने टोकन उपयोग होने पर सिंक बंद करें।', + 'syncBudget.maxCost': 'प्रति सिंक अधिकतम लागत (USD)', + 'syncBudget.maxCostHelp': 'प्रति सिंक रन के लिए डॉलर की सीमा।', + 'syncBudget.syncDepth': 'सिंक गहराई', + 'syncBudget.syncDepthHelp': 'केवल इस समय सीमा के आइटम लाएं।', + 'syncBudget.days7': 'पिछले 7 दिन', + 'syncBudget.days30': 'पिछले 30 दिन', + 'syncBudget.days90': 'पिछले 90 दिन', + 'syncBudget.allTime': 'सब समय', + 'syncBudget.unlimited': 'असीमित', + 'syncBudget.saved': 'बजट सहेजा गया।', + + // Sync confirm dialog + 'syncConfirm.title': 'सिंक की पुष्टि करें', + 'syncConfirm.message': 'यह सिंक ~{items} आइटम (~{tokens} टोकन, अनुमानित ${cost}) प्रोसेस करेगा।', + 'syncConfirm.budgetNote': 'बजट सीमा: ${max}', + 'syncConfirm.proceed': 'आगे बढ़ें', + 'syncConfirm.cancel': 'रद्द करें', + 'syncConfirm.estimating': 'लागत का अनुमान लगाया जा रहा है...', + + // Monthly cost badge + 'monthlyCost.badge': 'इस महीने ${amount}', + 'monthlyCost.noData': 'इस महीने कोई सिंक नहीं', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'गतिविधि', + 'onboarding.custom.activity.title': 'एजेंट गतिविधि', + 'onboarding.custom.activity.subtitle': + 'आपका एजेंट पृष्ठभूमि में कितनी सक्रियता से निगरानी और कार्य करता है।', + 'onboarding.custom.activity.defaultDesc': 'मध्यम गतिविधि — प्रति घंटे सिंक, दैनिक सारांश।', + 'onboarding.custom.activity.configureDesc': + 'अपना गतिविधि स्तर चुनें। सेटिंग्स › एजेंट गतिविधि स्तर में कॉन्फ़िगर करें।', }; export default messages; diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 225e91304..96ae2414b 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4467,6 +4467,67 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Tolak penyimpanan lokal', 'pages.settings.account.security': 'Keamanan', 'pages.settings.account.securityDesc': 'Mode penyimpanan rahasia dan status keychain', + + // Agent activity level + 'activityLevel.title': 'Tingkat aktivitas agen', + 'activityLevel.description': + 'Kendalikan seberapa proaktif agen Anda. Tingkat lebih tinggi menggunakan lebih banyak token.', + 'activityLevel.off': 'Mati', + 'activityLevel.offDesc': + 'Tidak ada pemrosesan latar belakang. Hanya sinkronisasi saat tombol ditekan.', + 'activityLevel.minimal': 'Minimal', + 'activityLevel.minimalDesc': 'Sinkronisasi sumber sekali sehari. Tidak ada pesan proaktif.', + 'activityLevel.moderate': 'Sedang', + 'activityLevel.moderateDesc': 'Sinkronisasi setiap jam. Ringkasan harian. Menyarankan tindakan.', + 'activityLevel.active': 'Aktif', + 'activityLevel.activeDesc': + 'Sinkronisasi setiap 10 menit. Memantau saluran, mengurutkan, dan menyusun balasan.', + 'activityLevel.alwaysOn': 'Selalu aktif', + 'activityLevel.alwaysOnDesc': + 'Sinkronisasi real-time. Otonomi penuh dalam batas yang ditentukan.', + 'activityLevel.currentMonth': 'Bulan ini: ${amount}', + 'activityLevel.saved': 'Tingkat aktivitas diperbarui.', + 'activityLevel.default': 'bawaan', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/bln', + + // Sync budget dialog + 'syncBudget.title': 'Anggaran sinkronisasi', + 'syncBudget.maxTokens': 'Token maksimum per sinkronisasi', + 'syncBudget.maxTokensHelp': 'Hentikan sinkronisasi setelah sejumlah token ini dikonsumsi.', + 'syncBudget.maxCost': 'Biaya maksimum per sinkronisasi (USD)', + 'syncBudget.maxCostHelp': 'Batas dolar mutlak per jalannya sinkronisasi.', + 'syncBudget.syncDepth': 'Kedalaman sinkronisasi', + 'syncBudget.syncDepthHelp': 'Hanya ambil item dari jendela waktu ini.', + 'syncBudget.days7': '7 hari terakhir', + 'syncBudget.days30': '30 hari terakhir', + 'syncBudget.days90': '90 hari terakhir', + 'syncBudget.allTime': 'Sepanjang waktu', + 'syncBudget.unlimited': 'Tidak terbatas', + 'syncBudget.saved': 'Anggaran disimpan.', + + // Sync confirm dialog + 'syncConfirm.title': 'Konfirmasi sinkronisasi', + 'syncConfirm.message': + 'Sinkronisasi ini akan memproses ~{items} item (~{tokens} token, est. ${cost}).', + 'syncConfirm.budgetNote': 'Batas anggaran: ${max}', + 'syncConfirm.proceed': 'Lanjutkan', + 'syncConfirm.cancel': 'Batal', + 'syncConfirm.estimating': 'Memperkirakan biaya...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} bulan ini', + 'monthlyCost.noData': 'Tidak ada sinkronisasi bulan ini', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Aktivitas', + 'onboarding.custom.activity.title': 'Aktivitas agen', + 'onboarding.custom.activity.subtitle': + 'Seberapa proaktif agen Anda memantau dan bertindak di latar belakang.', + 'onboarding.custom.activity.defaultDesc': + 'Aktivitas sedang — sinkronisasi per jam, ringkasan harian.', + 'onboarding.custom.activity.configureDesc': + 'Pilih tingkat aktivitas Anda sendiri. Konfigurasi di Pengaturan › Tingkat aktivitas agen.', }; export default messages; diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index b22df856b..c91b1fa54 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -4525,6 +4525,69 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Rifiuta archiviazione locale', 'pages.settings.account.security': 'Sicurezza', 'pages.settings.account.securityDesc': 'Modalità archiviazione segreti e stato del portachiavi', + + // Agent activity level + 'activityLevel.title': "Livello di attività dell'agente", + 'activityLevel.description': + 'Controlla quanto è proattivo il tuo agente. I livelli più alti usano più token.', + 'activityLevel.off': 'Disattivato', + 'activityLevel.offDesc': + 'Nessuna elaborazione in background. Sincronizza solo alla pressione del pulsante.', + 'activityLevel.minimal': 'Minimo', + 'activityLevel.minimalDesc': + 'Sincronizza le sorgenti una volta al giorno. Nessun messaggio proattivo.', + 'activityLevel.moderate': 'Moderato', + 'activityLevel.moderateDesc': 'Sincronizza ogni ora. Riepilogo giornaliero. Suggerisce azioni.', + 'activityLevel.active': 'Attivo', + 'activityLevel.activeDesc': + 'Sincronizza ogni 10 min. Monitora i canali, ordina e redige risposte.', + 'activityLevel.alwaysOn': 'Sempre attivo', + 'activityLevel.alwaysOnDesc': + 'Sincronizzazione in tempo reale. Piena autonomia entro i limiti definiti.', + 'activityLevel.currentMonth': 'Questo mese: ${amount}', + 'activityLevel.saved': 'Livello di attività aggiornato.', + 'activityLevel.default': 'predefinito', + 'activityLevel.costFree': '0 $', + 'activityLevel.costRange': '~${min}–${max}/mese', + + // Sync budget dialog + 'syncBudget.title': 'Budget di sincronizzazione', + 'syncBudget.maxTokens': 'Token massimi per sincronizzazione', + 'syncBudget.maxTokensHelp': + 'Ferma la sincronizzazione dopo aver consumato questo numero di token.', + 'syncBudget.maxCost': 'Costo massimo per sincronizzazione (USD)', + 'syncBudget.maxCostHelp': 'Limite assoluto in dollari per ogni esecuzione di sincronizzazione.', + 'syncBudget.syncDepth': 'Profondità di sincronizzazione', + 'syncBudget.syncDepthHelp': 'Recupera solo gli elementi di questa finestra temporale.', + 'syncBudget.days7': 'Ultimi 7 giorni', + 'syncBudget.days30': 'Ultimi 30 giorni', + 'syncBudget.days90': 'Ultimi 90 giorni', + 'syncBudget.allTime': 'Sempre', + 'syncBudget.unlimited': 'Illimitato', + 'syncBudget.saved': 'Budget salvato.', + + // Sync confirm dialog + 'syncConfirm.title': 'Conferma sincronizzazione', + 'syncConfirm.message': + 'Questa sincronizzazione elaborerà ~{items} elementi (~{tokens} token, stima ${cost}).', + 'syncConfirm.budgetNote': 'Limite budget: ${max}', + 'syncConfirm.proceed': 'Procedi', + 'syncConfirm.cancel': 'Annulla', + 'syncConfirm.estimating': 'Stima del costo in corso...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} questo mese', + 'monthlyCost.noData': 'Nessuna sincronizzazione questo mese', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Attività', + 'onboarding.custom.activity.title': "Attività dell'agente", + 'onboarding.custom.activity.subtitle': + 'Quanto proattivamente il tuo agente monitora e agisce in background.', + 'onboarding.custom.activity.defaultDesc': + 'Attività moderata — sincronizzazione oraria, riepilogo giornaliero.', + 'onboarding.custom.activity.configureDesc': + "Scegli il tuo livello di attività. Configura in Impostazioni › Livello di attività dell'agente.", }; export default messages; diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 2a222045a..9413695c3 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4415,6 +4415,62 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': '로컬 저장소 거부', 'pages.settings.account.security': '보안', 'pages.settings.account.securityDesc': '비밀 저장 모드 및 키체인 상태', + + // Agent activity level + 'activityLevel.title': '에이전트 활동 수준', + 'activityLevel.description': + '에이전트의 능동성을 조절하세요. 높은 수준은 더 많은 토큰을 사용합니다.', + 'activityLevel.off': '끄기', + 'activityLevel.offDesc': '백그라운드 처리 없음. 버튼을 눌러야만 동기화됩니다.', + 'activityLevel.minimal': '최소', + 'activityLevel.minimalDesc': '소스를 하루에 한 번 동기화합니다. 능동적 메시지 없음.', + 'activityLevel.moderate': '보통', + 'activityLevel.moderateDesc': '매 시간 동기화. 일일 요약. 작업을 제안합니다.', + 'activityLevel.active': '활성', + 'activityLevel.activeDesc': '10분마다 동기화. 채널 모니터링, 분류, 답변 초안 작성.', + 'activityLevel.alwaysOn': '항상 켜짐', + 'activityLevel.alwaysOnDesc': '실시간 동기화. 가이드라인 내 완전한 자율성.', + 'activityLevel.currentMonth': '이번 달: ${amount}', + 'activityLevel.saved': '활동 수준이 업데이트되었습니다.', + 'activityLevel.default': '기본값', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/월', + + // Sync budget dialog + 'syncBudget.title': '동기화 예산', + 'syncBudget.maxTokens': '동기화당 최대 토큰', + 'syncBudget.maxTokensHelp': '이 토큰 수를 소비하면 동기화를 중지합니다.', + 'syncBudget.maxCost': '동기화당 최대 비용 (USD)', + 'syncBudget.maxCostHelp': '각 동기화 실행당 달러 한도.', + 'syncBudget.syncDepth': '동기화 깊이', + 'syncBudget.syncDepthHelp': '이 기간의 항목만 가져옵니다.', + 'syncBudget.days7': '최근 7일', + 'syncBudget.days30': '최근 30일', + 'syncBudget.days90': '최근 90일', + 'syncBudget.allTime': '전체 기간', + 'syncBudget.unlimited': '무제한', + 'syncBudget.saved': '예산이 저장되었습니다.', + + // Sync confirm dialog + 'syncConfirm.title': '동기화 확인', + 'syncConfirm.message': '이 동기화는 ~{items}개 항목 (~{tokens} 토큰, 예상 ${cost})을 처리합니다.', + 'syncConfirm.budgetNote': '예산 한도: ${max}', + 'syncConfirm.proceed': '진행', + 'syncConfirm.cancel': '취소', + 'syncConfirm.estimating': '비용 계산 중...', + + // Monthly cost badge + 'monthlyCost.badge': '이번 달 ${amount}', + 'monthlyCost.noData': '이번 달 동기화 없음', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': '활동', + 'onboarding.custom.activity.title': '에이전트 활동', + 'onboarding.custom.activity.subtitle': + '에이전트가 백그라운드에서 얼마나 능동적으로 모니터링하고 행동하는지.', + 'onboarding.custom.activity.defaultDesc': '보통 활동 — 매시간 동기화, 일일 요약.', + 'onboarding.custom.activity.configureDesc': + '자신만의 활동 수준을 선택하세요. 설정 › 에이전트 활동 수준에서 구성하세요.', }; export default messages; diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 3f58cb516..2ca862c31 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -4524,6 +4524,66 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Odmów lokalnego przechowywania', 'pages.settings.account.security': 'Bezpieczeństwo', 'pages.settings.account.securityDesc': 'Tryb przechowywania sekretów i stan pęku kluczy', + + // Agent activity level + 'activityLevel.title': 'Poziom aktywności agenta', + 'activityLevel.description': + 'Kontroluj, jak proaktywny jest Twój agent. Wyższe poziomy zużywają więcej tokenów.', + 'activityLevel.off': 'Wyłączony', + 'activityLevel.offDesc': + 'Brak przetwarzania w tle. Synchronizuje tylko po naciśnięciu przycisku.', + 'activityLevel.minimal': 'Minimalny', + 'activityLevel.minimalDesc': 'Synchronizuje źródła raz dziennie. Brak proaktywnych wiadomości.', + 'activityLevel.moderate': 'Umiarkowany', + 'activityLevel.moderateDesc': 'Synchronizuje co godzinę. Codzienny skrót. Sugeruje działania.', + 'activityLevel.active': 'Aktywny', + 'activityLevel.activeDesc': + 'Synchronizuje co 10 min. Monitoruje kanały, porządkuje i redaguje odpowiedzi.', + 'activityLevel.alwaysOn': 'Zawsze włączony', + 'activityLevel.alwaysOnDesc': + 'Synchronizacja w czasie rzeczywistym. Pełna autonomia w granicach.', + 'activityLevel.currentMonth': 'W tym miesiącu: ${amount}', + 'activityLevel.saved': 'Poziom aktywności zaktualizowany.', + 'activityLevel.default': 'domyślny', + 'activityLevel.costFree': '0 $', + 'activityLevel.costRange': '~${min}–${max}/mies.', + + // Sync budget dialog + 'syncBudget.title': 'Budżet synchronizacji', + 'syncBudget.maxTokens': 'Maks. tokenów na synchronizację', + 'syncBudget.maxTokensHelp': 'Zatrzymaj synchronizację po zużyciu tej liczby tokenów.', + 'syncBudget.maxCost': 'Maks. koszt na synchronizację (USD)', + 'syncBudget.maxCostHelp': 'Bezwzględny limit w dolarach na jedno uruchomienie synchronizacji.', + 'syncBudget.syncDepth': 'Głębokość synchronizacji', + 'syncBudget.syncDepthHelp': 'Pobieraj tylko elementy z tego okna czasowego.', + 'syncBudget.days7': 'Ostatnie 7 dni', + 'syncBudget.days30': 'Ostatnie 30 dni', + 'syncBudget.days90': 'Ostatnie 90 dni', + 'syncBudget.allTime': 'Cały czas', + 'syncBudget.unlimited': 'Bez limitu', + 'syncBudget.saved': 'Budżet zapisany.', + + // Sync confirm dialog + 'syncConfirm.title': 'Potwierdź synchronizację', + 'syncConfirm.message': + 'Ta synchronizacja przetworzy ~{items} elementów (~{tokens} tokenów, szacunkowo ${cost}).', + 'syncConfirm.budgetNote': 'Limit budżetu: ${max}', + 'syncConfirm.proceed': 'Kontynuuj', + 'syncConfirm.cancel': 'Anuluj', + 'syncConfirm.estimating': 'Szacowanie kosztów...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} w tym miesiącu', + 'monthlyCost.noData': 'Brak synchronizacji w tym miesiącu', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Aktywność', + 'onboarding.custom.activity.title': 'Aktywność agenta', + 'onboarding.custom.activity.subtitle': 'Jak proaktywnie Twój agent monitoruje i działa w tle.', + 'onboarding.custom.activity.defaultDesc': + 'Umiarkowana aktywność — synchronizacja co godzinę, codzienny skrót.', + 'onboarding.custom.activity.configureDesc': + 'Wybierz własny poziom aktywności. Skonfiguruj w Ustawieniach › Poziom aktywności agenta.', }; export default messages; diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 2037e20de..afbc75798 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -4522,6 +4522,66 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Recusar armazenamento local', 'pages.settings.account.security': 'Segurança', 'pages.settings.account.securityDesc': 'Modo de armazenamento de segredos e status do chaveiro', + + // Agent activity level + 'activityLevel.title': 'Nível de atividade do agente', + 'activityLevel.description': + 'Controle o quão proativo é o seu agente. Níveis mais altos usam mais tokens.', + 'activityLevel.off': 'Desligado', + 'activityLevel.offDesc': + 'Nenhum processamento em segundo plano. Sincroniza apenas ao pressionar o botão.', + 'activityLevel.minimal': 'Mínimo', + 'activityLevel.minimalDesc': 'Sincroniza fontes uma vez por dia. Sem mensagens proativas.', + 'activityLevel.moderate': 'Moderado', + 'activityLevel.moderateDesc': 'Sincroniza a cada hora. Resumo diário. Sugere ações.', + 'activityLevel.active': 'Ativo', + 'activityLevel.activeDesc': + 'Sincroniza a cada 10 min. Monitora canais, prioriza e rascunha respostas.', + 'activityLevel.alwaysOn': 'Sempre ativo', + 'activityLevel.alwaysOnDesc': 'Sincronização em tempo real. Autonomia total dentro dos limites.', + 'activityLevel.currentMonth': 'Este mês: ${amount}', + 'activityLevel.saved': 'Nível de atividade atualizado.', + 'activityLevel.default': 'padrão', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/mês', + + // Sync budget dialog + 'syncBudget.title': 'Orçamento de sincronização', + 'syncBudget.maxTokens': 'Tokens máximos por sincronização', + 'syncBudget.maxTokensHelp': 'Parar a sincronização ao consumir este número de tokens.', + 'syncBudget.maxCost': 'Custo máximo por sincronização (USD)', + 'syncBudget.maxCostHelp': 'Limite absoluto em dólares por execução de sincronização.', + 'syncBudget.syncDepth': 'Profundidade de sincronização', + 'syncBudget.syncDepthHelp': 'Buscar apenas itens desta janela de tempo.', + 'syncBudget.days7': 'Últimos 7 dias', + 'syncBudget.days30': 'Últimos 30 dias', + 'syncBudget.days90': 'Últimos 90 dias', + 'syncBudget.allTime': 'Todo o tempo', + 'syncBudget.unlimited': 'Ilimitado', + 'syncBudget.saved': 'Orçamento salvo.', + + // Sync confirm dialog + 'syncConfirm.title': 'Confirmar sincronização', + 'syncConfirm.message': + 'Esta sincronização processará ~{items} itens (~{tokens} tokens, est. ${cost}).', + 'syncConfirm.budgetNote': 'Limite de orçamento: ${max}', + 'syncConfirm.proceed': 'Prosseguir', + 'syncConfirm.cancel': 'Cancelar', + 'syncConfirm.estimating': 'Estimando custo...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} este mês', + 'monthlyCost.noData': 'Sem sincronizações este mês', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Atividade', + 'onboarding.custom.activity.title': 'Atividade do agente', + 'onboarding.custom.activity.subtitle': + 'O quão proativamente seu agente monitora e age em segundo plano.', + 'onboarding.custom.activity.defaultDesc': + 'Atividade moderada — sincronização por hora, resumo diário.', + 'onboarding.custom.activity.configureDesc': + 'Escolha seu próprio nível de atividade. Configurar em Configurações › Nível de atividade do agente.', }; export default messages; diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 18301d55c..6a1753780 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -4493,6 +4493,67 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': 'Отклонить локальное хранилище', 'pages.settings.account.security': 'Безопасность', 'pages.settings.account.securityDesc': 'Режим хранения секретов и статус связки ключей', + + // Agent activity level + 'activityLevel.title': 'Уровень активности агента', + 'activityLevel.description': + 'Управляйте проактивностью вашего агента. Более высокие уровни потребляют больше токенов.', + 'activityLevel.off': 'Выкл.', + 'activityLevel.offDesc': 'Нет фоновой обработки. Синхронизация только по нажатию кнопки.', + 'activityLevel.minimal': 'Минимальный', + 'activityLevel.minimalDesc': 'Синхронизация источников раз в день. Нет проактивных сообщений.', + 'activityLevel.moderate': 'Умеренный', + 'activityLevel.moderateDesc': 'Синхронизация каждый час. Ежедневная сводка. Предлагает действия.', + 'activityLevel.active': 'Активный', + 'activityLevel.activeDesc': + 'Синхронизация каждые 10 мин. Мониторинг каналов, сортировка и составление ответов.', + 'activityLevel.alwaysOn': 'Всегда включён', + 'activityLevel.alwaysOnDesc': + 'Синхронизация в реальном времени. Полная автономия в рамках ограничений.', + 'activityLevel.currentMonth': 'В этом месяце: ${amount}', + 'activityLevel.saved': 'Уровень активности обновлён.', + 'activityLevel.default': 'по умолчанию', + 'activityLevel.costFree': '0 $', + 'activityLevel.costRange': '~${min}–${max}/мес.', + + // Sync budget dialog + 'syncBudget.title': 'Бюджет синхронизации', + 'syncBudget.maxTokens': 'Макс. токенов на синхронизацию', + 'syncBudget.maxTokensHelp': + 'Остановить синхронизацию после потребления указанного количества токенов.', + 'syncBudget.maxCost': 'Макс. стоимость на синхронизацию (USD)', + 'syncBudget.maxCostHelp': 'Жёсткий лимит в долларах за один запуск синхронизации.', + 'syncBudget.syncDepth': 'Глубина синхронизации', + 'syncBudget.syncDepthHelp': 'Загружать только элементы из этого временного окна.', + 'syncBudget.days7': 'Последние 7 дней', + 'syncBudget.days30': 'Последние 30 дней', + 'syncBudget.days90': 'Последние 90 дней', + 'syncBudget.allTime': 'За всё время', + 'syncBudget.unlimited': 'Без ограничений', + 'syncBudget.saved': 'Бюджет сохранён.', + + // Sync confirm dialog + 'syncConfirm.title': 'Подтвердить синхронизацию', + 'syncConfirm.message': + 'Эта синхронизация обработает ~{items} элементов (~{tokens} токенов, ест. ${cost}).', + 'syncConfirm.budgetNote': 'Лимит бюджета: ${max}', + 'syncConfirm.proceed': 'Продолжить', + 'syncConfirm.cancel': 'Отмена', + 'syncConfirm.estimating': 'Оценка стоимости...', + + // Monthly cost badge + 'monthlyCost.badge': '${amount} в этом месяце', + 'monthlyCost.noData': 'Синхронизаций в этом месяце нет', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': 'Активность', + 'onboarding.custom.activity.title': 'Активность агента', + 'onboarding.custom.activity.subtitle': + 'Насколько проактивно агент отслеживает события и действует в фоне.', + 'onboarding.custom.activity.defaultDesc': + 'Умеренная активность — синхронизация каждый час, ежедневная сводка.', + 'onboarding.custom.activity.configureDesc': + 'Выберите свой уровень активности. Настройка в Параметры › Уровень активности агента.', }; export default messages; diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 1094d9919..fd6147eae 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4236,6 +4236,60 @@ const messages: TranslationMap = { 'keyring.settings.revokeConsent': '拒绝本地存储', 'pages.settings.account.security': '安全', 'pages.settings.account.securityDesc': '密钥存储模式和密钥链状态', + + // Agent activity level + 'activityLevel.title': '智能体活动级别', + 'activityLevel.description': '控制您的智能体的主动程度。级别越高,消耗的令牌越多。', + 'activityLevel.off': '关闭', + 'activityLevel.offDesc': '无后台处理。仅在按下按钮时同步。', + 'activityLevel.minimal': '最低', + 'activityLevel.minimalDesc': '每天同步一次来源。无主动消息。', + 'activityLevel.moderate': '适中', + 'activityLevel.moderateDesc': '每小时同步。每日摘要。建议操作。', + 'activityLevel.active': '活跃', + 'activityLevel.activeDesc': '每10分钟同步。监控频道、分类并起草回复。', + 'activityLevel.alwaysOn': '始终开启', + 'activityLevel.alwaysOnDesc': '实时同步。在规定范围内完全自主。', + 'activityLevel.currentMonth': '本月:${amount}', + 'activityLevel.saved': '活动级别已更新。', + 'activityLevel.default': '默认', + 'activityLevel.costFree': '$0', + 'activityLevel.costRange': '~${min}–${max}/月', + + // Sync budget dialog + 'syncBudget.title': '同步预算', + 'syncBudget.maxTokens': '每次同步最大令牌数', + 'syncBudget.maxTokensHelp': '消耗此数量的令牌后停止同步。', + 'syncBudget.maxCost': '每次同步最大费用(USD)', + 'syncBudget.maxCostHelp': '每次同步运行的绝对美元上限。', + 'syncBudget.syncDepth': '同步深度', + 'syncBudget.syncDepthHelp': '仅获取此时间窗口内的项目。', + 'syncBudget.days7': '最近7天', + 'syncBudget.days30': '最近30天', + 'syncBudget.days90': '最近90天', + 'syncBudget.allTime': '所有时间', + 'syncBudget.unlimited': '无限制', + 'syncBudget.saved': '预算已保存。', + + // Sync confirm dialog + 'syncConfirm.title': '确认同步', + 'syncConfirm.message': '此同步将处理约{items}个项目(约{tokens}个令牌,预计${cost})。', + 'syncConfirm.budgetNote': '预算上限:${max}', + 'syncConfirm.proceed': '继续', + 'syncConfirm.cancel': '取消', + 'syncConfirm.estimating': '正在估算费用...', + + // Monthly cost badge + 'monthlyCost.badge': '本月${amount}', + 'monthlyCost.noData': '本月无同步', + + // Onboarding: Custom > Activity + 'onboarding.custom.stepperActivity': '活动', + 'onboarding.custom.activity.title': '智能体活动', + 'onboarding.custom.activity.subtitle': '您的智能体在后台监控和行动的主动程度。', + 'onboarding.custom.activity.defaultDesc': '适中活动——每小时同步,每日摘要。', + 'onboarding.custom.activity.configureDesc': + '选择您自己的活动级别。在设置 › 智能体活动级别中配置。', }; export default messages; diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx index d1c451e4f..1fb485267 100644 --- a/app/src/pages/Settings.tsx +++ b/app/src/pages/Settings.tsx @@ -5,6 +5,7 @@ import CostDashboardPanel from '../components/dashboard/CostDashboardPanel'; import LogoutAndClearActions from '../components/settings/LogoutAndClearActions'; import AboutPanel from '../components/settings/panels/AboutPanel'; import AgentAccessPanel from '../components/settings/panels/AgentAccessPanel'; +import AgentActivityPanel from '../components/settings/panels/AgentActivityPanel'; import AgentChatPanel from '../components/settings/panels/AgentChatPanel'; import AgentEditorPage from '../components/settings/panels/AgentEditorPage'; import AgentsPanel from '../components/settings/panels/AgentsPanel'; @@ -457,6 +458,13 @@ const Settings = () => { route: 'agent-access', icon: AgentAccessIcon, }, + { + id: 'activity-level', + title: t('activityLevel.title'), + description: t('activityLevel.description'), + route: 'activity-level', + icon: LlmIcon, + }, ]; const composioSettingsItems = [ @@ -587,6 +595,7 @@ const Settings = () => { )} /> )} /> )} /> + )} /> )} /> )} /> )} /> diff --git a/app/src/pages/onboarding/Onboarding.tsx b/app/src/pages/onboarding/Onboarding.tsx index 963e20c7c..4de345ad9 100644 --- a/app/src/pages/onboarding/Onboarding.tsx +++ b/app/src/pages/onboarding/Onboarding.tsx @@ -1,6 +1,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import OnboardingLayout from './OnboardingLayout'; +import CustomActivityPage from './pages/CustomActivityPage'; import CustomEmbeddingsPage from './pages/CustomEmbeddingsPage'; import CustomInferencePage from './pages/CustomInferencePage'; // Memory step is hidden from the flow for now (file kept on disk; @@ -39,6 +40,7 @@ const Onboarding = () => { } /> } /> } /> + } /> {/* } /> */} } /> diff --git a/app/src/pages/onboarding/OnboardingContext.tsx b/app/src/pages/onboarding/OnboardingContext.tsx index a962482d6..6f4eb7453 100644 --- a/app/src/pages/onboarding/OnboardingContext.tsx +++ b/app/src/pages/onboarding/OnboardingContext.tsx @@ -2,7 +2,14 @@ import { createContext, useContext } from 'react'; export type AiMode = 'cloud' | 'custom'; -export type CustomStepKey = 'inference' | 'voice' | 'oauth' | 'search' | 'embeddings' | 'memory'; +export type CustomStepKey = + | 'inference' + | 'voice' + | 'oauth' + | 'search' + | 'embeddings' + | 'memory' + | 'activity'; export type CustomStepChoice = 'default' | 'configure'; export interface OnboardingDraft { diff --git a/app/src/pages/onboarding/customWizardSteps.ts b/app/src/pages/onboarding/customWizardSteps.ts index e9b09264f..d32b6a8cc 100644 --- a/app/src/pages/onboarding/customWizardSteps.ts +++ b/app/src/pages/onboarding/customWizardSteps.ts @@ -9,6 +9,7 @@ export const CUSTOM_WIZARD_STEPS: CustomStepKey[] = [ 'oauth', 'search', 'embeddings', + 'activity', // 'memory', ]; @@ -18,6 +19,7 @@ export const CUSTOM_WIZARD_ROUTES: Record = { oauth: '/onboarding/custom/oauth', search: '/onboarding/custom/search', embeddings: '/onboarding/custom/embeddings', + activity: '/onboarding/custom/activity', memory: '/onboarding/custom/memory', }; @@ -29,5 +31,6 @@ export const CUSTOM_WIZARD_SETTINGS_ROUTES: Record = { oauth: '/settings/composio-routing', search: '/settings/tools', embeddings: '/settings/embeddings', + activity: '/settings/activity-level', memory: '/settings/memory-data', }; diff --git a/app/src/pages/onboarding/pages/CustomActivityPage.tsx b/app/src/pages/onboarding/pages/CustomActivityPage.tsx new file mode 100644 index 000000000..39ae99cb6 --- /dev/null +++ b/app/src/pages/onboarding/pages/CustomActivityPage.tsx @@ -0,0 +1,8 @@ +import AgentActivityPanel from '../../../components/settings/panels/AgentActivityPanel'; +import CustomWizardConfigPage from './CustomWizardConfigPage'; + +const CustomActivityPage = () => ( + } /> +); + +export default CustomActivityPage; diff --git a/app/src/pages/onboarding/steps/CustomWizardStep.tsx b/app/src/pages/onboarding/steps/CustomWizardStep.tsx index 1ab95d70c..b08682eae 100644 --- a/app/src/pages/onboarding/steps/CustomWizardStep.tsx +++ b/app/src/pages/onboarding/steps/CustomWizardStep.tsx @@ -114,6 +114,7 @@ const CustomWizardStep = ({ t('onboarding.custom.stepperOAuth'), t('onboarding.custom.stepperSearch'), t('onboarding.custom.stepperEmbeddings'), + t('onboarding.custom.stepperActivity'), t('onboarding.custom.stepperMemory'), ].slice(0, stepCount); diff --git a/src/openhuman/config/ops.rs b/src/openhuman/config/ops.rs index 9e078ba99..60b218849 100644 --- a/src/openhuman/config/ops.rs +++ b/src/openhuman/config/ops.rs @@ -913,6 +913,105 @@ pub async fn load_and_apply_autonomy_settings( apply_autonomy_settings(&mut config, update).await } +// ── Agent Activity Level ─────────────────────────────────────────────── + +/// Partial update for the agent activity level (0–4). +#[derive(Debug, Clone, Default)] +pub struct ActivityLevelSettingsPatch { + /// "off" | "minimal" | "moderate" | "active" | "always_on" (or "0"-"4"). + pub level: Option, +} + +/// Returns the current activity level and its derived settings. +pub async fn get_activity_level_settings() -> Result, String> { + let config = load_config_with_timeout().await?; + let level = config.agent_activity_level; + let (cost_min, cost_max) = level.estimated_monthly_cost_range(); + let value = serde_json::json!({ + "level": level as u8, + "level_label": level.as_str(), + "sync_interval_secs": level.sync_interval_secs(), + "heartbeat_enabled": level.heartbeat_enabled(), + "subconscious_enabled": level.subconscious_enabled(), + "token_budget_per_cycle": level.token_budget_per_cycle(), + "estimated_monthly_cost_min_usd": cost_min, + "estimated_monthly_cost_max_usd": cost_max, + }); + Ok(RpcOutcome::single_log( + value, + "activity level settings read", + )) +} + +/// Updates the agent activity level and pushes it into the scheduler gate. +pub async fn apply_activity_level_settings( + config: &mut Config, + update: ActivityLevelSettingsPatch, +) -> Result, String> { + use crate::openhuman::config::schema::activity_level::AgentActivityLevel; + use crate::openhuman::config::SchedulerGateMode; + + if let Some(level_str) = update.level { + let level = AgentActivityLevel::from_str_opt(&level_str).ok_or_else(|| { + format!( + "invalid activity level '{}' \ + (expected off|minimal|moderate|active|always_on or 0-4)", + level_str + ) + })?; + config.agent_activity_level = level; + } + + // Derive the gate mode from the (possibly updated) activity level and + // persist it alongside the level so the saved config is self-consistent. + let level = config.agent_activity_level; + let gate_mode = match level { + AgentActivityLevel::Off => SchedulerGateMode::Off, + AgentActivityLevel::Minimal | AgentActivityLevel::Moderate => SchedulerGateMode::Auto, + AgentActivityLevel::Active | AgentActivityLevel::AlwaysOn => SchedulerGateMode::AlwaysOn, + }; + config.scheduler_gate.mode = gate_mode; + + config.save().await.map_err(|e| e.to_string())?; + + let gate_cfg = config.scheduler_gate.clone(); + crate::openhuman::scheduler_gate::gate::update_config(gate_cfg); + + tracing::info!( + level = %level.as_str(), + gate_mode = %gate_mode.as_str(), + "[config:activity_level] activity level updated" + ); + + let (cost_min, cost_max) = level.estimated_monthly_cost_range(); + let value = serde_json::json!({ + "level": level as u8, + "level_label": level.as_str(), + "sync_interval_secs": level.sync_interval_secs(), + "heartbeat_enabled": level.heartbeat_enabled(), + "subconscious_enabled": level.subconscious_enabled(), + "token_budget_per_cycle": level.token_budget_per_cycle(), + "estimated_monthly_cost_min_usd": cost_min, + "estimated_monthly_cost_max_usd": cost_max, + }); + Ok(RpcOutcome::new( + value, + vec![format!( + "activity level set to '{}' — saved to {}", + level.as_str(), + config.config_path.display() + )], + )) +} + +/// Loads the configuration, applies activity level settings, and saves it. +pub async fn load_and_apply_activity_level_settings( + update: ActivityLevelSettingsPatch, +) -> Result, String> { + let mut config = load_config_with_timeout().await?; + apply_activity_level_settings(&mut config, update).await +} + /// Serializes the load-modify-save in [`add_auto_approve_tool`] so two /// concurrent "Always allow" appends (different tools) can't read the same /// `auto_approve`, each push their own, and clobber the other on save diff --git a/src/openhuman/config/schema/activity_level.rs b/src/openhuman/config/schema/activity_level.rs new file mode 100644 index 000000000..d8d5e3465 --- /dev/null +++ b/src/openhuman/config/schema/activity_level.rs @@ -0,0 +1,157 @@ +//! Agent activity level — controls how proactive background AI is. +//! +//! Maps a single 0–4 knob into scheduler-gate mode, periodic sync +//! cadence, heartbeat/subconscious toggles, and token budgets. + +use schemars::JsonSchema; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// User-facing activity level for background AI work. +/// +/// Each level is an opinionated preset that controls multiple subsystems: +/// - Scheduler-gate mode (off / auto / always_on) +/// - Periodic sync cadence (never / daily / hourly / 10min / realtime) +/// - Heartbeat & subconscious inference (disabled / enabled) +/// - Token budget per background cycle +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr, JsonSchema)] +#[repr(u8)] +pub enum AgentActivityLevel { + /// No background processing. Syncs only on manual press. + Off = 0, + /// Sync sources once per day. No proactive messages. + Minimal = 1, + /// Sync every hour. Daily digest. Suggests actions. (default) + Moderate = 2, + /// Sync every 10 min. Monitors channels, triages, drafts replies. + Active = 3, + /// Real-time sync. Full autonomy within guardrails. + AlwaysOn = 4, +} + +impl AgentActivityLevel { + pub fn as_str(self) -> &'static str { + match self { + Self::Off => "off", + Self::Minimal => "minimal", + Self::Moderate => "moderate", + Self::Active => "active", + Self::AlwaysOn => "always_on", + } + } + + pub fn from_str_opt(s: &str) -> Option { + match s.trim().to_ascii_lowercase().as_str() { + "off" | "0" => Some(Self::Off), + "minimal" | "1" => Some(Self::Minimal), + "moderate" | "2" => Some(Self::Moderate), + "active" | "3" => Some(Self::Active), + "always_on" | "alwayson" | "4" => Some(Self::AlwaysOn), + _ => None, + } + } + + /// Periodic sync interval in seconds for this level. + /// Returns None for Off (manual-only). + pub fn sync_interval_secs(self) -> Option { + match self { + Self::Off => None, + Self::Minimal => Some(86_400), // 24h + Self::Moderate => Some(3_600), // 1h + Self::Active => Some(600), // 10min + Self::AlwaysOn => Some(60), // 1min + } + } + + /// Whether heartbeat inference should run at this level. + pub fn heartbeat_enabled(self) -> bool { + matches!(self, Self::Moderate | Self::Active | Self::AlwaysOn) + } + + /// Whether subconscious background reasoning should run. + pub fn subconscious_enabled(self) -> bool { + matches!(self, Self::Active | Self::AlwaysOn) + } + + /// Per-background-cycle token budget. None = unlimited. + pub fn token_budget_per_cycle(self) -> Option { + match self { + Self::Off => Some(0), + Self::Minimal => Some(100_000), + Self::Moderate => Some(500_000), + Self::Active => Some(2_000_000), + Self::AlwaysOn => None, + } + } + + /// Estimated monthly cost range (min, max) in USD for display. + pub fn estimated_monthly_cost_range(self) -> (f64, f64) { + match self { + Self::Off => (0.0, 0.0), + Self::Minimal => (0.10, 0.50), + Self::Moderate => (1.0, 5.0), + Self::Active => (5.0, 20.0), + Self::AlwaysOn => (20.0, 100.0), + } + } +} + +impl Default for AgentActivityLevel { + fn default() -> Self { + Self::Moderate + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_is_moderate() { + assert_eq!(AgentActivityLevel::default(), AgentActivityLevel::Moderate); + } + + #[test] + fn from_str_round_trips() { + for level in [ + AgentActivityLevel::Off, + AgentActivityLevel::Minimal, + AgentActivityLevel::Moderate, + AgentActivityLevel::Active, + AgentActivityLevel::AlwaysOn, + ] { + let parsed = AgentActivityLevel::from_str_opt(level.as_str()).unwrap(); + assert_eq!(parsed, level); + } + } + + #[test] + fn serde_repr_round_trips() { + for level in [ + AgentActivityLevel::Off, + AgentActivityLevel::Minimal, + AgentActivityLevel::Moderate, + AgentActivityLevel::Active, + AgentActivityLevel::AlwaysOn, + ] { + let json = serde_json::to_string(&level).unwrap(); + let parsed: AgentActivityLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, level); + } + } + + #[test] + fn sync_interval_none_for_off() { + assert_eq!(AgentActivityLevel::Off.sync_interval_secs(), None); + assert_eq!( + AgentActivityLevel::Minimal.sync_interval_secs(), + Some(86_400) + ); + } + + #[test] + fn heartbeat_disabled_for_low_levels() { + assert!(!AgentActivityLevel::Off.heartbeat_enabled()); + assert!(!AgentActivityLevel::Minimal.heartbeat_enabled()); + assert!(AgentActivityLevel::Moderate.heartbeat_enabled()); + } +} diff --git a/src/openhuman/config/schema/mod.rs b/src/openhuman/config/schema/mod.rs index 5a8980c3f..edcda1ebf 100644 --- a/src/openhuman/config/schema/mod.rs +++ b/src/openhuman/config/schema/mod.rs @@ -2,6 +2,8 @@ //! //! Split into submodules; this module re-exports the main `Config` and all public types. +pub mod activity_level; +pub use activity_level::AgentActivityLevel; pub mod cloud_providers; pub use cloud_providers::{ generate_provider_id, is_slug_reserved, migrate_legacy_fields, AuthStyle, CloudProviderCreds, diff --git a/src/openhuman/config/schema/types.rs b/src/openhuman/config/schema/types.rs index e468275ab..988ffc9e1 100644 --- a/src/openhuman/config/schema/types.rs +++ b/src/openhuman/config/schema/types.rs @@ -123,6 +123,12 @@ pub struct Config { #[serde(default)] pub scheduler_gate: SchedulerGateConfig, + /// User-facing activity-level knob (0–4) controlling how proactive + /// background AI work is. Maps into scheduler_gate mode, periodic sync + /// cadence, heartbeat/subconscious toggles. See issue #3117. + #[serde(default)] + pub agent_activity_level: AgentActivityLevel, + #[serde(default)] pub agent: AgentConfig, @@ -653,6 +659,7 @@ impl Default for Config { reliability: ReliabilityConfig::default(), scheduler: SchedulerConfig::default(), scheduler_gate: SchedulerGateConfig::default(), + agent_activity_level: AgentActivityLevel::default(), agent: AgentConfig::default(), orchestrator: OrchestratorModelConfig::default(), teams: HashMap::new(), diff --git a/src/openhuman/config/schemas.rs b/src/openhuman/config/schemas.rs index a127b5e9c..f2cf3aeaa 100644 --- a/src/openhuman/config/schemas.rs +++ b/src/openhuman/config/schemas.rs @@ -228,6 +228,12 @@ struct AgentSettingsUpdate { agent_timeout_secs: Option, } +#[derive(Debug, Deserialize)] +struct ActivityLevelSettingsUpdate { + /// "off" | "minimal" | "moderate" | "active" | "always_on" (or "0"-"4"). + level: Option, +} + pub fn all_controller_schemas() -> Vec { vec![ schemas("get_config"), @@ -265,6 +271,8 @@ pub fn all_controller_schemas() -> Vec { schemas("update_agent_settings"), schemas("update_search_settings"), schemas("get_search_settings"), + schemas("get_activity_level_settings"), + schemas("update_activity_level_settings"), ] } @@ -410,6 +418,14 @@ pub fn all_registered_controllers() -> Vec { schema: schemas("get_search_settings"), handler: handle_get_search_settings, }, + RegisteredController { + schema: schemas("get_activity_level_settings"), + handler: handle_get_activity_level_settings, + }, + RegisteredController { + schema: schemas("update_activity_level_settings"), + handler: handle_update_activity_level_settings, + }, ] } @@ -912,6 +928,20 @@ pub fn schemas(function: &str) -> ControllerSchema { "Engine, effective engine, limits, and per-provider configuration flags.", )], }, + "get_activity_level_settings" => ControllerSchema { + namespace: "config", + function: "get_activity_level_settings", + description: "Get the agent activity level (0–4) and its derived settings: sync cadence, heartbeat/subconscious toggles, token budget, estimated monthly cost.", + inputs: vec![], + outputs: vec![json_output("settings", "Activity level settings with cost estimates.")], + }, + "update_activity_level_settings" => ControllerSchema { + namespace: "config", + function: "update_activity_level_settings", + description: "Set the agent activity level. Immediately updates the scheduler gate mode and persists the change.", + inputs: vec![optional_string("level", "Activity level: off | minimal | moderate | active | always_on (or 0–4).")], + outputs: vec![json_output("settings", "Updated activity level settings with cost estimates.")], + }, "agent_server_status" => ControllerSchema { namespace: "config", function: "agent_server_status", @@ -1657,6 +1687,20 @@ fn handle_get_search_settings(_params: Map) -> ControllerFuture { }) } +fn handle_get_activity_level_settings(_params: Map) -> ControllerFuture { + Box::pin(async move { to_json(config_rpc::get_activity_level_settings().await?) }) +} + +fn handle_update_activity_level_settings(params: Map) -> ControllerFuture { + Box::pin(async move { + let update = deserialize_params::(params)?; + let patch = config_rpc::ActivityLevelSettingsPatch { + level: update.level, + }; + to_json(config_rpc::load_and_apply_activity_level_settings(patch).await?) + }) +} + fn deserialize_params(params: Map) -> Result { serde_json::from_value(Value::Object(params)).map_err(|e| format!("invalid params: {e}")) } diff --git a/src/openhuman/memory_sources/readers/composio.rs b/src/openhuman/memory_sources/readers/composio.rs index d39c0a6c5..a5847fb1b 100644 --- a/src/openhuman/memory_sources/readers/composio.rs +++ b/src/openhuman/memory_sources/readers/composio.rs @@ -90,6 +90,9 @@ mod tests { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/src/openhuman/memory_sources/readers/folder.rs b/src/openhuman/memory_sources/readers/folder.rs index 2e29e45a4..17935037e 100644 --- a/src/openhuman/memory_sources/readers/folder.rs +++ b/src/openhuman/memory_sources/readers/folder.rs @@ -166,6 +166,9 @@ mod tests { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/src/openhuman/memory_sources/readers/twitter.rs b/src/openhuman/memory_sources/readers/twitter.rs index 469ab8170..b4eefabeb 100644 --- a/src/openhuman/memory_sources/readers/twitter.rs +++ b/src/openhuman/memory_sources/readers/twitter.rs @@ -91,6 +91,9 @@ mod tests { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/src/openhuman/memory_sources/registry.rs b/src/openhuman/memory_sources/registry.rs index 98d0afd48..bbfdedf28 100644 --- a/src/openhuman/memory_sources/registry.rs +++ b/src/openhuman/memory_sources/registry.rs @@ -102,6 +102,15 @@ pub async fn update_source( if let Some(selector) = patch.selector { entry.selector = Some(selector); } + if let Some(v) = patch.max_tokens_per_sync { + entry.max_tokens_per_sync = Some(v); + } + if let Some(v) = patch.max_cost_per_sync_usd { + entry.max_cost_per_sync_usd = Some(v); + } + if let Some(v) = patch.sync_depth_days { + entry.sync_depth_days = Some(v); + } entry.validate()?; let updated = entry.clone(); @@ -212,6 +221,9 @@ pub async fn upsert_composio_source( since_days: None, max_items: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, }; config.memory_sources.push(entry.clone()); config @@ -257,6 +269,12 @@ pub struct MemorySourcePatch { pub max_items: Option, #[serde(default)] pub selector: Option, + #[serde(default)] + pub max_tokens_per_sync: Option, + #[serde(default)] + pub max_cost_per_sync_usd: Option, + #[serde(default)] + pub sync_depth_days: Option, } #[cfg(test)] diff --git a/src/openhuman/memory_sources/rpc.rs b/src/openhuman/memory_sources/rpc.rs index 1a9042ec4..367b87de0 100644 --- a/src/openhuman/memory_sources/rpc.rs +++ b/src/openhuman/memory_sources/rpc.rs @@ -81,6 +81,12 @@ pub struct AddRequest { pub max_items: Option, #[serde(default)] pub selector: Option, + #[serde(default)] + pub max_tokens_per_sync: Option, + #[serde(default)] + pub max_cost_per_sync_usd: Option, + #[serde(default)] + pub sync_depth_days: Option, } fn default_true() -> bool { @@ -121,6 +127,9 @@ pub async fn add_rpc(req: AddRequest) -> Result, String> since_days: req.since_days, max_items: req.max_items, selector: req.selector, + max_tokens_per_sync: req.max_tokens_per_sync, + max_cost_per_sync_usd: req.max_cost_per_sync_usd, + sync_depth_days: req.sync_depth_days, }; let source = registry::add_source(entry).await?; @@ -282,3 +291,106 @@ pub async fn sync_audit_log_rpc() -> Result, St let entries = crate::openhuman::memory_sync::sources::audit::read_audit_log(&config); Ok(RpcOutcome::new(SyncAuditLogResponse { entries }, vec![])) } + +// ── Estimate Sync Cost ── + +#[derive(Debug, serde::Deserialize)] +pub struct EstimateSyncCostRequest { + pub source_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct EstimateSyncCostResponse { + pub source_id: String, + pub item_count: u32, + pub estimated_tokens: u64, + pub estimated_cost_usd: f64, + pub budget_max_cost_usd: Option, + pub budget_max_tokens: Option, +} + +pub async fn estimate_sync_cost_rpc( + req: EstimateSyncCostRequest, +) -> Result, String> { + tracing::debug!(source_id = %req.source_id, "[memory_sources] estimate_sync_cost_rpc: entry"); + + let source = registry::get_source(&req.source_id) + .await? + .ok_or_else(|| format!("source '{}' not found", req.source_id))?; + + let config = config_rpc::load_config_with_timeout().await?; + let reader = readers::reader_for(&source.kind); + let items = reader.list_items(&source, &config).await?; + + let item_count = items.len() as u32; + // estimated_tokens includes both input (500/item) and output (100/item) + // to be consistent with the cost calculation below. + let estimated_input_tokens = item_count as u64 * 500; + let estimated_output_tokens = item_count as u64 * 100; + let estimated_tokens = estimated_input_tokens + estimated_output_tokens; + let estimated_cost_usd = crate::openhuman::memory_sync::sources::audit::estimate_cost_usd( + estimated_input_tokens, + estimated_output_tokens, + ); + + Ok(RpcOutcome::new( + EstimateSyncCostResponse { + source_id: req.source_id, + item_count, + estimated_tokens, + estimated_cost_usd, + budget_max_cost_usd: source.max_cost_per_sync_usd, + budget_max_tokens: source.max_tokens_per_sync, + }, + vec![], + )) +} + +// ── Monthly Cost Summary ── + +#[derive(Debug, serde::Serialize)] +pub struct MonthlyCostSummaryResponse { + pub month: String, + pub total_cost_usd: f64, + pub total_syncs: u32, + pub total_items: u32, + pub total_input_tokens: u64, + pub total_output_tokens: u64, +} + +pub async fn monthly_cost_summary_rpc() -> Result, String> { + tracing::debug!("[memory_sources] monthly_cost_summary_rpc: entry"); + let config = config_rpc::load_config_with_timeout().await?; + let entries = crate::openhuman::memory_sync::sources::audit::read_audit_log(&config); + + let now = chrono::Utc::now(); + let month_str = now.format("%Y-%m").to_string(); + + let mut total_cost_usd = 0.0f64; + let mut total_syncs = 0u32; + let mut total_items = 0u32; + let mut total_input_tokens = 0u64; + let mut total_output_tokens = 0u64; + + for entry in &entries { + if entry.timestamp.format("%Y-%m").to_string() == month_str { + total_cost_usd += entry.effective_cost_usd(); + total_syncs += 1; + total_items += entry.items_fetched; + total_input_tokens += entry.input_tokens; + total_output_tokens += entry.output_tokens; + } + } + + Ok(RpcOutcome::new( + MonthlyCostSummaryResponse { + month: month_str, + total_cost_usd, + total_syncs, + total_items, + total_input_tokens, + total_output_tokens, + }, + vec![], + )) +} diff --git a/src/openhuman/memory_sources/schemas.rs b/src/openhuman/memory_sources/schemas.rs index 341c10be8..7184e2c45 100644 --- a/src/openhuman/memory_sources/schemas.rs +++ b/src/openhuman/memory_sources/schemas.rs @@ -79,6 +79,24 @@ fn kind_specific_fields() -> Vec { comment: "CSS selector for web_page sources.", required: false, }, + FieldSchema { + name: "max_tokens_per_sync", + ty: TypeSchema::Option(Box::new(TypeSchema::U64)), + comment: "Max tokens per sync run.", + required: false, + }, + FieldSchema { + name: "max_cost_per_sync_usd", + ty: TypeSchema::Option(Box::new(TypeSchema::F64)), + comment: "Max cost per sync run in USD.", + required: false, + }, + FieldSchema { + name: "sync_depth_days", + ty: TypeSchema::Option(Box::new(TypeSchema::U64)), + comment: "Only sync items from the last N days.", + required: false, + }, ] } @@ -94,6 +112,8 @@ pub fn all_controller_schemas() -> Vec { schemas("sync"), schemas("status_list"), schemas("sync_audit_log"), + schemas("estimate_sync_cost"), + schemas("monthly_cost_summary"), ] } @@ -139,6 +159,14 @@ pub fn all_registered_controllers() -> Vec { schema: schemas("sync_audit_log"), handler: handle_sync_audit_log, }, + RegisteredController { + schema: schemas("estimate_sync_cost"), + handler: handle_estimate_sync_cost, + }, + RegisteredController { + schema: schemas("monthly_cost_summary"), + handler: handle_monthly_cost_summary, + }, ] } @@ -364,6 +392,101 @@ pub fn schemas(function: &str) -> ControllerSchema { required: true, }], }, + "estimate_sync_cost" => ControllerSchema { + namespace: NAMESPACE, + function: "estimate_sync_cost", + description: + "Estimate the cost of syncing a source before starting. Returns item count, \ + estimated tokens, and estimated cost in USD.", + inputs: vec![FieldSchema { + name: "source_id", + ty: TypeSchema::String, + comment: "Source id to estimate.", + required: true, + }], + outputs: vec![ + FieldSchema { + name: "source_id", + ty: TypeSchema::String, + comment: "Echo of source id.", + required: true, + }, + FieldSchema { + name: "item_count", + ty: TypeSchema::U64, + comment: "Number of items to sync.", + required: true, + }, + FieldSchema { + name: "estimated_tokens", + ty: TypeSchema::U64, + comment: "Estimated input tokens.", + required: true, + }, + FieldSchema { + name: "estimated_cost_usd", + ty: TypeSchema::F64, + comment: "Estimated cost in USD.", + required: true, + }, + FieldSchema { + name: "budget_max_cost_usd", + ty: TypeSchema::Option(Box::new(TypeSchema::F64)), + comment: "Configured cost cap if set.", + required: false, + }, + FieldSchema { + name: "budget_max_tokens", + ty: TypeSchema::Option(Box::new(TypeSchema::U64)), + comment: "Configured token cap if set.", + required: false, + }, + ], + }, + "monthly_cost_summary" => ControllerSchema { + namespace: NAMESPACE, + function: "monthly_cost_summary", + description: "Aggregate sync costs for the current calendar month.", + inputs: vec![], + outputs: vec![ + FieldSchema { + name: "month", + ty: TypeSchema::String, + comment: "YYYY-MM.", + required: true, + }, + FieldSchema { + name: "total_cost_usd", + ty: TypeSchema::F64, + comment: "Total spend in USD.", + required: true, + }, + FieldSchema { + name: "total_syncs", + ty: TypeSchema::U64, + comment: "Number of sync runs.", + required: true, + }, + FieldSchema { + name: "total_items", + ty: TypeSchema::U64, + comment: "Total items fetched.", + required: true, + }, + FieldSchema { + name: "total_input_tokens", + ty: TypeSchema::U64, + comment: "Total input tokens.", + required: true, + }, + FieldSchema { + name: "total_output_tokens", + ty: TypeSchema::U64, + comment: "Total output tokens.", + required: true, + }, + ], + }, other => panic!("unknown memory_sources schema function: {other}"), } } @@ -429,6 +552,17 @@ fn handle_sync_audit_log(_params: Map) -> ControllerFuture { Box::pin(async move { to_json(rpc::sync_audit_log_rpc().await?) }) } +fn handle_estimate_sync_cost(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::estimate_sync_cost_rpc(req).await?) + }) +} + +fn handle_monthly_cost_summary(_params: Map) -> ControllerFuture { + Box::pin(async move { to_json(rpc::monthly_cost_summary_rpc().await?) }) +} + fn parse_value(v: Value) -> Result { serde_json::from_value(v).map_err(|e| format!("invalid params: {e}")) } diff --git a/src/openhuman/memory_sources/status.rs b/src/openhuman/memory_sources/status.rs index 1f1f8cda5..f9f538247 100644 --- a/src/openhuman/memory_sources/status.rs +++ b/src/openhuman/memory_sources/status.rs @@ -179,6 +179,9 @@ mod tests { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, }; assert_eq!(source_id_prefix(&entry), "mem_src:src_abc:%"); diff --git a/src/openhuman/memory_sources/types.rs b/src/openhuman/memory_sources/types.rs index b34056c15..f0bce598a 100644 --- a/src/openhuman/memory_sources/types.rs +++ b/src/openhuman/memory_sources/types.rs @@ -88,6 +88,17 @@ pub struct MemorySourceEntry { // ── WebPage ── #[serde(default, skip_serializing_if = "Option::is_none")] pub selector: Option, + + // ── Sync Budget (all source kinds) ── + /// Maximum tokens to consume per sync run. Sync stops once this budget is hit. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_tokens_per_sync: Option, + /// Maximum cost in USD per sync run. Refuses LLM calls once reached. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_cost_per_sync_usd: Option, + /// Sync depth in days — only fetch items from the last N days. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sync_depth_days: Option, } impl MemorySourceEntry { @@ -262,6 +273,9 @@ mod tests { since_days: None, max_items: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } } diff --git a/tests/app_credentials_threads_memory_sources_raw_coverage_e2e.rs b/tests/app_credentials_threads_memory_sources_raw_coverage_e2e.rs index f72c1aa96..9021999e1 100644 --- a/tests/app_credentials_threads_memory_sources_raw_coverage_e2e.rs +++ b/tests/app_credentials_threads_memory_sources_raw_coverage_e2e.rs @@ -190,6 +190,9 @@ fn source_entry(id: &str, kind: SourceKind, label: &str) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/tests/config_auth_app_state_connectivity_e2e.rs b/tests/config_auth_app_state_connectivity_e2e.rs index 93ea2c1ea..0198d2159 100644 --- a/tests/config_auth_app_state_connectivity_e2e.rs +++ b/tests/config_auth_app_state_connectivity_e2e.rs @@ -2783,6 +2783,7 @@ async fn worker_a_controller_schemas_are_fully_exposed() { vec![ "openhuman.config_agent_server_status", "openhuman.config_get", + "openhuman.config_get_activity_level_settings", "openhuman.config_get_agent_settings", "openhuman.config_get_analytics_settings", "openhuman.config_get_autonomy_settings", @@ -2800,6 +2801,7 @@ async fn worker_a_controller_schemas_are_fully_exposed() { "openhuman.config_resolve_api_url", "openhuman.config_set_browser_allow_all", "openhuman.config_set_onboarding_completed", + "openhuman.config_update_activity_level_settings", "openhuman.config_update_agent_settings", "openhuman.config_update_analytics_settings", "openhuman.config_update_autonomy_settings", @@ -2846,6 +2848,23 @@ async fn worker_a_controller_schemas_are_fully_exposed() { ], ), ("connectivity", vec!["openhuman.connectivity_diag"]), + ( + "memory_sources", + vec![ + "openhuman.memory_sources_add", + "openhuman.memory_sources_estimate_sync_cost", + "openhuman.memory_sources_get", + "openhuman.memory_sources_list", + "openhuman.memory_sources_list_items", + "openhuman.memory_sources_monthly_cost_summary", + "openhuman.memory_sources_read_item", + "openhuman.memory_sources_remove", + "openhuman.memory_sources_status_list", + "openhuman.memory_sources_sync", + "openhuman.memory_sources_sync_audit_log", + "openhuman.memory_sources_update", + ], + ), ] { assert_eq!( schema_method_names(&schema, namespace), diff --git a/tests/memory_raw_coverage_e2e.rs b/tests/memory_raw_coverage_e2e.rs index b810775f8..146daa5be 100644 --- a/tests/memory_raw_coverage_e2e.rs +++ b/tests/memory_raw_coverage_e2e.rs @@ -71,6 +71,9 @@ fn source_entry(kind: SourceKind, id: &str) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/tests/memory_sources_closure_round23_raw_coverage_e2e.rs b/tests/memory_sources_closure_round23_raw_coverage_e2e.rs index 19c8e50ff..0fa56baa5 100644 --- a/tests/memory_sources_closure_round23_raw_coverage_e2e.rs +++ b/tests/memory_sources_closure_round23_raw_coverage_e2e.rs @@ -135,6 +135,9 @@ fn source_entry(id: &str, kind: SourceKind) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/tests/memory_sources_readers_round21_raw_coverage_e2e.rs b/tests/memory_sources_readers_round21_raw_coverage_e2e.rs index 054576273..48eb47027 100644 --- a/tests/memory_sources_readers_round21_raw_coverage_e2e.rs +++ b/tests/memory_sources_readers_round21_raw_coverage_e2e.rs @@ -74,6 +74,9 @@ fn source_entry(id: &str, kind: SourceKind) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/tests/memory_sync_sources_raw_coverage_e2e.rs b/tests/memory_sync_sources_raw_coverage_e2e.rs index 7b803035f..5a336ef9d 100644 --- a/tests/memory_sync_sources_raw_coverage_e2e.rs +++ b/tests/memory_sync_sources_raw_coverage_e2e.rs @@ -118,6 +118,9 @@ fn source(kind: SourceKind, id: &str) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } diff --git a/tests/memory_threads_raw_coverage_e2e.rs b/tests/memory_threads_raw_coverage_e2e.rs index d22c7da6a..40dcc654b 100644 --- a/tests/memory_threads_raw_coverage_e2e.rs +++ b/tests/memory_threads_raw_coverage_e2e.rs @@ -229,6 +229,9 @@ fn source(kind: SourceKind, id: &str) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } } @@ -3818,6 +3821,9 @@ async fn memory_sources_registry_rpc_and_schema_handlers_cover_crud_edges() { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, }) .await .unwrap_err(); @@ -3841,6 +3847,9 @@ async fn memory_sources_registry_rpc_and_schema_handlers_cover_crud_edges() { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, }) .await .expect("add folder") @@ -3865,6 +3874,9 @@ async fn memory_sources_registry_rpc_and_schema_handlers_cover_crud_edges() { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, }) .await .is_ok()); diff --git a/tests/near90_closure_raw_coverage_e2e.rs b/tests/near90_closure_raw_coverage_e2e.rs index 02e3748b1..aab76370e 100644 --- a/tests/near90_closure_raw_coverage_e2e.rs +++ b/tests/near90_closure_raw_coverage_e2e.rs @@ -197,6 +197,9 @@ fn source_entry(id: &str, kind: SourceKind) -> MemorySourceEntry { max_issues: None, max_prs: None, selector: None, + max_tokens_per_sync: None, + max_cost_per_sync_usd: None, + sync_depth_days: None, } }