feat: sync budget controls + agent activity level picker (#3117) (#3174)

This commit is contained in:
Steven Enamakel's Droid
2026-06-01 20:44:33 -07:00
committed by GitHub
parent e9f3e3a7da
commit 5890dc56c7
47 changed files with 1954 additions and 2 deletions
Generated
+12
View File
@@ -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"
+1
View File
@@ -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
+1
View File
@@ -5394,6 +5394,7 @@ dependencies = [
"sentry",
"serde",
"serde_json",
"serde_repr",
"serde_yaml",
"sha1",
"sha2 0.10.9",
@@ -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<string>(source.sync_depth_days?.toString() ?? '');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(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<typeof updateMemorySource>[1]);
onSaved();
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setSaving(false);
}
}, [source.id, maxTokens, maxCost, depthDays, onSaved, onClose]);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={onClose}>
<div
className="bg-white dark:bg-neutral-900 rounded-xl shadow-xl border border-stone-200 dark:border-neutral-800 w-full max-w-md mx-4 p-5"
onClick={e => e.stopPropagation()}>
<h3 className="text-base font-semibold text-stone-900 dark:text-neutral-100 mb-1">
{t('syncBudget.title')}
</h3>
<p className="text-xs text-stone-500 dark:text-neutral-400 mb-4">{source.label}</p>
<div className="flex flex-col gap-4">
<div>
<label
htmlFor="budget-tokens"
className="block text-sm font-medium text-stone-700 dark:text-neutral-300">
{t('syncBudget.maxTokens')}
</label>
<p className="text-xs text-stone-500 dark:text-neutral-400 mt-0.5 mb-1">
{t('syncBudget.maxTokensHelp')}
</p>
<input
id="budget-tokens"
type="number"
min={0}
step={10000}
value={maxTokens}
onChange={e => 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"
/>
</div>
<div>
<label
htmlFor="budget-cost"
className="block text-sm font-medium text-stone-700 dark:text-neutral-300">
{t('syncBudget.maxCost')}
</label>
<p className="text-xs text-stone-500 dark:text-neutral-400 mt-0.5 mb-1">
{t('syncBudget.maxCostHelp')}
</p>
<input
id="budget-cost"
type="number"
min={0}
step={0.01}
value={maxCost}
onChange={e => 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"
/>
</div>
<div>
<label
htmlFor="budget-depth"
className="block text-sm font-medium text-stone-700 dark:text-neutral-300">
{t('syncBudget.syncDepth')}
</label>
<p className="text-xs text-stone-500 dark:text-neutral-400 mt-0.5 mb-1">
{t('syncBudget.syncDepthHelp')}
</p>
<select
id="budget-depth"
value={depthDays}
onChange={e => setDepthDays(e.target.value)}
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">
<option value="">{t('syncBudget.allTime')}</option>
<option value="7">{t('syncBudget.days7')}</option>
<option value="30">{t('syncBudget.days30')}</option>
<option value="90">{t('syncBudget.days90')}</option>
</select>
</div>
</div>
{error && <p className="mt-3 text-xs text-coral-600">{error}</p>}
<div className="flex justify-end gap-2 mt-5">
<button
onClick={onClose}
className="px-3 py-1.5 rounded-md text-xs font-medium text-stone-600 dark:text-neutral-400 hover:bg-stone-100 dark:hover:bg-neutral-800">
{t('syncConfirm.cancel')}
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 rounded-md bg-primary-600 hover:bg-primary-500 disabled:opacity-50 text-white text-xs font-medium">
{saving ? t('autonomy.statusSaving') : t('common.save')}
</button>
</div>
</div>
</div>
);
}
@@ -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<SyncEstimate | null>(null);
const [error, setError] = useState<string | null>(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 (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={onCancel}>
<div
className="bg-white dark:bg-neutral-900 rounded-xl shadow-xl border border-stone-200 dark:border-neutral-800 w-full max-w-sm mx-4 p-5"
onClick={e => e.stopPropagation()}>
<h3 className="text-base font-semibold text-stone-900 dark:text-neutral-100 mb-3">
{t('syncConfirm.title')}
</h3>
{!estimate && !error && (
<p className="text-sm text-stone-500 dark:text-neutral-400">
{t('syncConfirm.estimating')}
</p>
)}
{error && <p className="text-sm text-coral-600">{error}</p>}
{estimate && (
<div className="flex flex-col gap-2">
<p className="text-sm text-stone-700 dark:text-neutral-300">
{t('syncConfirm.message')
.replace('{items}', String(estimate.item_count))
.replace('{tokens}', tokenStr)
.replace('{cost}', estimate.estimated_cost_usd.toFixed(4))}
</p>
{estimate.budget_max_cost_usd != null && (
<p className="text-xs text-stone-500 dark:text-neutral-400">
{t('syncConfirm.budgetNote').replace(
'{max}',
estimate.budget_max_cost_usd.toFixed(2)
)}
</p>
)}
</div>
)}
<div className="flex justify-end gap-2 mt-5">
<button
onClick={onCancel}
className="px-3 py-1.5 rounded-md text-xs font-medium text-stone-600 dark:text-neutral-400 hover:bg-stone-100 dark:hover:bg-neutral-800">
{t('syncConfirm.cancel')}
</button>
<button
onClick={onConfirm}
disabled={!estimate}
className="px-3 py-1.5 rounded-md bg-primary-600 hover:bg-primary-500 disabled:opacity-50 text-white text-xs font-medium">
{t('syncConfirm.proceed')}
</button>
</div>
</div>
</div>
);
}
@@ -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<ActivityLevelSettings | null>(null);
const [monthlyCost, setMonthlyCost] = useState<MonthlyCostSummary | null>(null);
const [status, setStatus] = useState<Status>('loading');
const [error, setError] = useState<string | null>(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 (
<div className="p-4 text-sm text-stone-500 dark:text-neutral-400">{t('common.loading')}</div>
);
}
return (
<div className="flex flex-col gap-4 p-4">
<div>
<h2 className="text-lg font-semibold text-stone-900 dark:text-neutral-100">
{t('activityLevel.title')}
</h2>
<p className="text-xs text-stone-600 dark:text-neutral-400 mt-1">
{t('activityLevel.description')}
</p>
</div>
{monthlyCost && monthlyCost.total_cost_usd > 0 && (
<div className="px-3 py-2 rounded-md bg-stone-100 dark:bg-neutral-800 text-sm">
<span className="font-medium text-stone-700 dark:text-neutral-300">
{t('activityLevel.currentMonth').replace(
'{amount}',
monthlyCost.total_cost_usd.toFixed(2)
)}
</span>
</div>
)}
<div className="flex flex-col gap-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 (
<button
key={key}
onClick={() => handleLevelChange(apiKey)}
disabled={status === 'saving'}
className={`w-full text-left px-4 py-3 rounded-lg border transition-colors ${
isSelected
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 hover:border-stone-300 dark:hover:border-neutral-700'
} ${status === 'saving' ? 'opacity-50' : ''}`}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
{t(`activityLevel.${key as LevelKey}`)}
</span>
{value === 2 && (
<span className="text-xs px-1.5 py-0.5 rounded bg-stone-200 dark:bg-neutral-700 text-stone-600 dark:text-neutral-400">
{t('activityLevel.default')}
</span>
)}
</div>
<p className="text-xs text-stone-500 dark:text-neutral-400 mt-0.5">
{t(`activityLevel.${key as LevelKey}Desc`)}
</p>
</div>
<div className="text-xs font-mono text-stone-500 dark:text-neutral-400 shrink-0 ml-4">
{costMin === 0 && costMax === 0
? t('activityLevel.costFree')
: t('activityLevel.costRange')
.replace('{min}', String(costMin))
.replace('{max}', String(costMax))}
</div>
</div>
</button>
);
})}
</div>
<div role="status" aria-live="polite" aria-atomic="true" className="text-xs min-h-[1rem]">
{status === 'saving' && (
<span className="text-stone-500">{t('autonomy.statusSaving')}</span>
)}
{status === 'saved' && (
<span className="text-sage-700 dark:text-sage-400">{t('activityLevel.saved')}</span>
)}
{error && <span className="text-coral-600">{error}</span>}
</div>
</div>
);
}
+55
View File
@@ -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;
+60
View File
@@ -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;
+63
View File
@@ -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;
+58 -1
View File
@@ -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;
+61
View File
@@ -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;
+62
View File
@@ -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;
+57
View File
@@ -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;
+61
View File
@@ -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;
+63
View File
@@ -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;
+56
View File
@@ -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;
+60
View File
@@ -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;
+60
View File
@@ -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;
+61
View File
@@ -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;
+54
View File
@@ -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;
+9
View File
@@ -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 = () => {
<Route path="persona" element={wrapSettingsPage(<PersonaPanel />)} />
<Route path="appearance" element={wrapSettingsPage(<AppearancePanel />)} />
<Route path="agent-access" element={wrapSettingsPage(<AgentAccessPanel />)} />
<Route path="activity-level" element={wrapSettingsPage(<AgentActivityPanel />)} />
<Route path="approval-history" element={wrapSettingsPage(<ApprovalHistoryPanel />)} />
<Route path="agents" element={wrapSettingsPage(<AgentsPanel />)} />
<Route path="agents/new" element={wrapSettingsPage(<AgentEditorPage />)} />
+2
View File
@@ -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 = () => {
<Route path="custom/oauth" element={<CustomOAuthPage />} />
<Route path="custom/search" element={<CustomSearchPage />} />
<Route path="custom/embeddings" element={<CustomEmbeddingsPage />} />
<Route path="custom/activity" element={<CustomActivityPage />} />
{/* <Route path="custom/memory" element={<CustomMemoryPage />} /> */}
<Route path="*" element={<Navigate to="welcome" replace />} />
</Route>
@@ -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 {
@@ -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<CustomStepKey, string> = {
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<CustomStepKey, string> = {
oauth: '/settings/composio-routing',
search: '/settings/tools',
embeddings: '/settings/embeddings',
activity: '/settings/activity-level',
memory: '/settings/memory-data',
};
@@ -0,0 +1,8 @@
import AgentActivityPanel from '../../../components/settings/panels/AgentActivityPanel';
import CustomWizardConfigPage from './CustomWizardConfigPage';
const CustomActivityPage = () => (
<CustomWizardConfigPage stepKey="activity" configureContent={<AgentActivityPanel />} />
);
export default CustomActivityPage;
@@ -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);
+99
View File
@@ -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 (04).
#[derive(Debug, Clone, Default)]
pub struct ActivityLevelSettingsPatch {
/// "off" | "minimal" | "moderate" | "active" | "always_on" (or "0"-"4").
pub level: Option<String>,
}
/// Returns the current activity level and its derived settings.
pub async fn get_activity_level_settings() -> Result<RpcOutcome<serde_json::Value>, 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<RpcOutcome<serde_json::Value>, 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<RpcOutcome<serde_json::Value>, 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
@@ -0,0 +1,157 @@
//! Agent activity level — controls how proactive background AI is.
//!
//! Maps a single 04 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<Self> {
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<u64> {
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<u64> {
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());
}
}
+2
View File
@@ -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,
+7
View File
@@ -123,6 +123,12 @@ pub struct Config {
#[serde(default)]
pub scheduler_gate: SchedulerGateConfig,
/// User-facing activity-level knob (04) 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(),
+44
View File
@@ -228,6 +228,12 @@ struct AgentSettingsUpdate {
agent_timeout_secs: Option<u64>,
}
#[derive(Debug, Deserialize)]
struct ActivityLevelSettingsUpdate {
/// "off" | "minimal" | "moderate" | "active" | "always_on" (or "0"-"4").
level: Option<String>,
}
pub fn all_controller_schemas() -> Vec<ControllerSchema> {
vec![
schemas("get_config"),
@@ -265,6 +271,8 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
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<RegisteredController> {
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 (04) 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 04).")],
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<String, Value>) -> ControllerFuture {
})
}
fn handle_get_activity_level_settings(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move { to_json(config_rpc::get_activity_level_settings().await?) })
}
fn handle_update_activity_level_settings(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let update = deserialize_params::<ActivityLevelSettingsUpdate>(params)?;
let patch = config_rpc::ActivityLevelSettingsPatch {
level: update.level,
};
to_json(config_rpc::load_and_apply_activity_level_settings(patch).await?)
})
}
fn deserialize_params<T: DeserializeOwned>(params: Map<String, Value>) -> Result<T, String> {
serde_json::from_value(Value::Object(params)).map_err(|e| format!("invalid params: {e}"))
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
+18
View File
@@ -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<u32>,
#[serde(default)]
pub selector: Option<String>,
#[serde(default)]
pub max_tokens_per_sync: Option<u64>,
#[serde(default)]
pub max_cost_per_sync_usd: Option<f64>,
#[serde(default)]
pub sync_depth_days: Option<u32>,
}
#[cfg(test)]
+112
View File
@@ -81,6 +81,12 @@ pub struct AddRequest {
pub max_items: Option<u32>,
#[serde(default)]
pub selector: Option<String>,
#[serde(default)]
pub max_tokens_per_sync: Option<u64>,
#[serde(default)]
pub max_cost_per_sync_usd: Option<f64>,
#[serde(default)]
pub sync_depth_days: Option<u32>,
}
fn default_true() -> bool {
@@ -121,6 +127,9 @@ pub async fn add_rpc(req: AddRequest) -> Result<RpcOutcome<AddResponse>, 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<RpcOutcome<SyncAuditLogResponse>, 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<f64>,
pub budget_max_tokens: Option<u64>,
}
pub async fn estimate_sync_cost_rpc(
req: EstimateSyncCostRequest,
) -> Result<RpcOutcome<EstimateSyncCostResponse>, 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<RpcOutcome<MonthlyCostSummaryResponse>, 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![],
))
}
+134
View File
@@ -79,6 +79,24 @@ fn kind_specific_fields() -> Vec<FieldSchema> {
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<ControllerSchema> {
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<RegisteredController> {
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<String, Value>) -> ControllerFuture {
Box::pin(async move { to_json(rpc::sync_audit_log_rpc().await?) })
}
fn handle_estimate_sync_cost(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let req = parse_value::<rpc::EstimateSyncCostRequest>(Value::Object(params))?;
to_json(rpc::estimate_sync_cost_rpc(req).await?)
})
}
fn handle_monthly_cost_summary(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move { to_json(rpc::monthly_cost_summary_rpc().await?) })
}
fn parse_value<T: DeserializeOwned>(v: Value) -> Result<T, String> {
serde_json::from_value(v).map_err(|e| format!("invalid params: {e}"))
}
+3
View File
@@ -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:%");
+14
View File
@@ -88,6 +88,17 @@ pub struct MemorySourceEntry {
// ── WebPage ──
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
// ── 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<u64>,
/// 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<f64>,
/// 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<u32>,
}
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,
}
}
}
@@ -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,
}
}
@@ -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),
+3
View File
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
+12
View File
@@ -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());
+3
View File
@@ -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,
}
}