@lobehub/lobehub 2.0.0-next.251 → 2.0.0-next.253

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/build/entitlements.mac.plist +9 -0
  3. package/apps/desktop/resources/locales/zh-CN/dialog.json +5 -1
  4. package/apps/desktop/resources/locales/zh-CN/menu.json +7 -0
  5. package/apps/desktop/src/main/controllers/SystemCtr.ts +186 -94
  6. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +200 -31
  7. package/apps/desktop/src/main/core/browser/Browser.ts +9 -0
  8. package/apps/desktop/src/main/locales/default/dialog.ts +7 -2
  9. package/apps/desktop/src/main/locales/default/menu.ts +7 -0
  10. package/apps/desktop/src/main/menus/impls/macOS.ts +44 -1
  11. package/apps/desktop/src/main/utils/fullDiskAccess.ts +121 -0
  12. package/changelog/v1.json +14 -0
  13. package/package.json +1 -1
  14. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +0 -2
  15. package/packages/database/migrations/meta/_journal.json +1 -1
  16. package/packages/database/src/models/__tests__/topics/topic.create.test.ts +37 -8
  17. package/packages/database/src/models/topic.ts +71 -4
  18. package/packages/database/src/schemas/agentCronJob.ts +1 -2
  19. package/packages/electron-client-ipc/src/events/system.ts +1 -0
  20. package/packages/memory-user-memory/src/extractors/context.ts +1 -4
  21. package/packages/memory-user-memory/src/extractors/experience.ts +2 -8
  22. package/packages/memory-user-memory/src/extractors/preference.ts +2 -8
  23. package/packages/memory-user-memory/src/prompts/gatekeeper.ts +123 -123
  24. package/packages/memory-user-memory/src/prompts/layers/context.ts +152 -152
  25. package/packages/memory-user-memory/src/prompts/layers/experience.ts +159 -159
  26. package/packages/memory-user-memory/src/prompts/layers/identity.ts +213 -213
  27. package/packages/memory-user-memory/src/prompts/layers/preference.ts +160 -160
  28. package/packages/memory-user-memory/src/services/extractExecutor.ts +33 -30
  29. package/packages/memory-user-memory/src/types.ts +10 -8
  30. package/packages/types/src/topic/topic.ts +9 -0
  31. package/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +16 -30
  32. package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +19 -9
  33. package/src/app/[variants]/(desktop)/desktop-onboarding/storage.ts +49 -0
  34. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Body.tsx +4 -1
  35. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +74 -0
  36. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicItem.tsx +40 -0
  37. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/index.tsx +140 -0
  38. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/index.tsx +1 -1
  39. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/index.tsx +1 -1
  40. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/index.tsx +1 -1
  41. package/src/app/[variants]/(main)/chat/cron/[cronId]/index.tsx +664 -0
  42. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobCards.tsx +160 -0
  43. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobForm.tsx +202 -0
  44. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobList.tsx +137 -0
  45. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/hooks/useAgentCronJobs.ts +138 -0
  46. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/index.tsx +130 -0
  47. package/src/app/[variants]/(main)/chat/profile/features/ProfileEditor/index.tsx +33 -3
  48. package/src/app/[variants]/router/desktopRouter.config.tsx +7 -0
  49. package/src/features/ChatInput/ActionBar/Params/Controls.tsx +7 -6
  50. package/src/hooks/useFetchCronTopics.ts +29 -0
  51. package/src/hooks/useFetchCronTopicsWithJobInfo.ts +56 -0
  52. package/src/hooks/useFetchTopics.ts +4 -1
  53. package/src/locales/default/setting.ts +44 -1
  54. package/src/server/routers/lambda/agentCronJob.ts +367 -0
  55. package/src/server/routers/lambda/image/index.test.ts +2 -2
  56. package/src/server/routers/lambda/index.ts +2 -0
  57. package/src/server/routers/lambda/topic.ts +15 -3
  58. package/src/server/services/aiAgent/index.ts +18 -1
  59. package/src/server/services/memory/userMemory/extract.ts +14 -6
  60. package/src/services/agentCronJob.ts +95 -0
  61. package/src/services/topic/index.ts +1 -0
  62. package/src/store/chat/slices/topic/action.ts +53 -2
  63. package/src/store/chat/slices/topic/initialState.ts +1 -0
  64. package/src/store/chat/slices/topic/selectors.ts +14 -6
  65. package/src/tools/placeholders.ts +1 -4
  66. package/apps/desktop/src/main/controllers/scripts/full-disk-access.applescript +0 -85
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
+ import { Flexbox } from '@lobehub/ui';
5
+ import { Modal, Typography } from 'antd';
6
+ import { Clock } from 'lucide-react';
7
+ import { memo, useRef, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import { useAgentStore } from '@/store/agent';
11
+
12
+ import CronJobCards from './CronJobCards';
13
+ import CronJobForm from './CronJobForm';
14
+ import { useAgentCronJobs } from './hooks/useAgentCronJobs';
15
+
16
+ const { Title } = Typography;
17
+
18
+ interface AgentCronJobsProps {
19
+ onFormModalChange?: (show: boolean) => void;
20
+ showFormModal?: boolean;
21
+ }
22
+
23
+ const AgentCronJobs = memo<AgentCronJobsProps>(({ showFormModal, onFormModalChange }) => {
24
+ const { t } = useTranslation('setting');
25
+ const agentId = useAgentStore((s) => s.activeAgentId);
26
+ const [internalShowForm, setInternalShowForm] = useState(false);
27
+ const [editingJob, setEditingJob] = useState<string | null>(null);
28
+ const [submitting, setSubmitting] = useState(false);
29
+ const formRef = useRef<any>(null);
30
+
31
+ // Use external control if provided, otherwise use internal state
32
+ const showForm = showFormModal ?? internalShowForm;
33
+ const setShowForm = onFormModalChange ?? setInternalShowForm;
34
+
35
+ const { cronJobs, loading, createCronJob, updateCronJob, deleteCronJob } =
36
+ useAgentCronJobs(agentId);
37
+
38
+ if (!ENABLE_BUSINESS_FEATURES) return null;
39
+
40
+ if (!agentId) {
41
+ return null;
42
+ }
43
+
44
+ const handleCreate = async (data: any) => {
45
+ setSubmitting(true);
46
+ try {
47
+ await createCronJob(data);
48
+ setShowForm(false);
49
+ } finally {
50
+ setSubmitting(false);
51
+ }
52
+ };
53
+
54
+ const handleEdit = (jobId: string) => {
55
+ setEditingJob(jobId);
56
+ setShowForm(true);
57
+ };
58
+
59
+ const handleUpdate = async (data: any) => {
60
+ if (editingJob) {
61
+ setSubmitting(true);
62
+ try {
63
+ await updateCronJob(editingJob, data);
64
+ setShowForm(false);
65
+ setEditingJob(null);
66
+ } finally {
67
+ setSubmitting(false);
68
+ }
69
+ }
70
+ };
71
+
72
+ const handleCancel = () => {
73
+ setShowForm(false);
74
+ setEditingJob(null);
75
+ formRef.current?.resetFields();
76
+ };
77
+
78
+ const handleModalOk = () => {
79
+ formRef.current?.submit();
80
+ };
81
+
82
+ const handleDelete = async (jobId: string) => {
83
+ await deleteCronJob(jobId);
84
+ };
85
+
86
+ const hasCronJobs = cronJobs && cronJobs.length > 0;
87
+
88
+ return (
89
+ <>
90
+ {/* Show cards section only if there are jobs */}
91
+ {hasCronJobs && (
92
+ <Flexbox gap={12} style={{ marginBottom: 16, marginTop: 16 }}>
93
+ <Title level={5} style={{ margin: 0 }}>
94
+ <Flexbox align="center" gap={8} horizontal>
95
+ <Clock size={16} />
96
+ {t('agentCronJobs.title')}
97
+ </Flexbox>
98
+ </Title>
99
+
100
+ <CronJobCards
101
+ cronJobs={cronJobs}
102
+ loading={loading}
103
+ onDelete={handleDelete}
104
+ onEdit={handleEdit}
105
+ />
106
+ </Flexbox>
107
+ )}
108
+
109
+ {/* Form Modal */}
110
+ <Modal
111
+ confirmLoading={submitting}
112
+ okText={editingJob ? t('agentCronJobs.save' as any) : t('agentCronJobs.create' as any)}
113
+ onCancel={handleCancel}
114
+ onOk={handleModalOk}
115
+ open={showForm}
116
+ title={editingJob ? t('agentCronJobs.editJob') : t('agentCronJobs.addJob')}
117
+ width={640}
118
+ >
119
+ <CronJobForm
120
+ editingJob={editingJob ? cronJobs?.find((job) => job.id === editingJob) : undefined}
121
+ formRef={formRef}
122
+ onCancel={handleCancel}
123
+ onSubmit={editingJob ? handleUpdate : handleCreate}
124
+ />
125
+ </Modal>
126
+ </>
127
+ );
128
+ });
129
+
130
+ export default AgentCronJobs;
@@ -1,19 +1,22 @@
1
1
  'use client';
2
2
 
3
+ import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
3
4
  import { Button, Flexbox } from '@lobehub/ui';
4
- import { Divider } from 'antd';
5
+ import { Divider, message } from 'antd';
5
6
  import isEqual from 'fast-deep-equal';
6
- import { PlayIcon } from 'lucide-react';
7
- import React, { memo } from 'react';
7
+ import { Clock, PlayIcon } from 'lucide-react';
8
+ import React, { memo, useCallback } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
  import urlJoin from 'url-join';
10
11
 
11
12
  import ModelSelect from '@/features/ModelSelect';
12
13
  import { useQueryRoute } from '@/hooks/useQueryRoute';
14
+ import { agentCronJobService } from '@/services/agentCronJob';
13
15
  import { useAgentStore } from '@/store/agent';
14
16
  import { agentSelectors } from '@/store/agent/selectors';
15
17
  import { useChatStore } from '@/store/chat';
16
18
 
19
+ import AgentCronJobs from '../AgentCronJobs';
17
20
  import EditorCanvas from '../EditorCanvas';
18
21
  import AgentHeader from './AgentHeader';
19
22
  import AgentTool from './AgentTool';
@@ -26,6 +29,26 @@ const ProfileEditor = memo(() => {
26
29
  const switchTopic = useChatStore((s) => s.switchTopic);
27
30
  const router = useQueryRoute();
28
31
 
32
+ const handleCreateCronJob = useCallback(async () => {
33
+ if (!agentId) return;
34
+ try {
35
+ const result = await agentCronJobService.create({
36
+ agentId,
37
+ content: t('agentCronJobs.form.content.placeholder') || 'This is a cron job',
38
+ cronPattern: '0 */30 * * *',
39
+ enabled: true,
40
+ name: t('agentCronJobs.addJob') || 'Cron Job Task',
41
+ });
42
+
43
+ if (result.success) {
44
+ router.push(urlJoin('/agent', agentId, 'cron', result.data.id));
45
+ }
46
+ } catch (error) {
47
+ console.error('Failed to create cron job:', error);
48
+ message.error('Failed to create scheduled task');
49
+ }
50
+ }, [agentId, router, t]);
51
+
29
52
  return (
30
53
  <>
31
54
  <Flexbox
@@ -72,11 +95,18 @@ const ProfileEditor = memo(() => {
72
95
  >
73
96
  {t('startConversation')}
74
97
  </Button>
98
+ {ENABLE_BUSINESS_FEATURES && (
99
+ <Button icon={Clock} onClick={handleCreateCronJob}>
100
+ {t('agentCronJobs.addJob')}
101
+ </Button>
102
+ )}
75
103
  </Flexbox>
76
104
  </Flexbox>
77
105
  <Divider />
78
106
  {/* Main Content: Prompt Editor */}
79
107
  <EditorCanvas />
108
+ {/* Agent Cron Jobs Display (only show if jobs exist) */}
109
+ {ENABLE_BUSINESS_FEATURES && <AgentCronJobs />}
80
110
  </>
81
111
  );
82
112
  });
@@ -39,6 +39,13 @@ export const desktopRoutes: RouteConfig[] = [
39
39
  ),
40
40
  path: 'profile',
41
41
  },
42
+ {
43
+ element: dynamicElement(
44
+ () => import('../(main)/chat/cron/[cronId]'),
45
+ 'Desktop > Chat > Cron Detail',
46
+ ),
47
+ path: 'cron/:cronId',
48
+ },
42
49
  ],
43
50
  element: <DesktopChatLayout />,
44
51
  errorElement: <ErrorBoundary resetPath="/agent" />,
@@ -80,10 +80,10 @@ const ParamControlWrapper = memo<ParamControlWrapperProps>(
80
80
  <Checkbox
81
81
  checked={checked}
82
82
  className={styles.checkbox}
83
- onChange={(v) => {
84
- onToggle(v);
83
+ onClick={(e) => {
84
+ e.stopPropagation();
85
+ onToggle(!checked);
85
86
  }}
86
- onClick={(e) => e.stopPropagation()}
87
87
  />
88
88
  <div style={{ flex: 1 }}>
89
89
  <Component disabled={disabled} onChange={onChange} value={value} />
@@ -275,6 +275,7 @@ const Controls = memo<ControlsProps>(({ setUpdating }) => {
275
275
  disabled={!enabled}
276
276
  onToggle={(checked) => handleToggle(key, checked)}
277
277
  styles={styles}
278
+ value={form.getFieldValue(PARAM_NAME_MAP[key])}
278
279
  />
279
280
  ),
280
281
  label: (
@@ -297,9 +298,9 @@ const Controls = memo<ControlsProps>(({ setUpdating }) => {
297
298
  mobile
298
299
  ? baseItems
299
300
  : baseItems.map(({ tag, ...item }) => ({
300
- ...item,
301
- desc: <Tag size={'small'}>{tag}</Tag>,
302
- }))
301
+ ...item,
302
+ desc: <Tag size={'small'}>{tag}</Tag>,
303
+ }))
303
304
  }
304
305
  itemsType={'flat'}
305
306
  onValuesChange={handleValuesChange}
@@ -0,0 +1,29 @@
1
+ import useSWR from 'swr';
2
+
3
+ import { lambdaClient } from '@/libs/trpc/client/lambda';
4
+ import { useAgentStore } from '@/store/agent';
5
+
6
+ /**
7
+ * Fetch cron topics grouped by cronJob for the current agent
8
+ */
9
+ export const useFetchCronTopics = () => {
10
+ const agentId = useAgentStore((s) => s.activeAgentId);
11
+
12
+ const { data, isLoading, error, mutate } = useSWR(
13
+ agentId ? ['cronTopics', agentId] : null,
14
+ async () => {
15
+ if (!agentId) return [];
16
+ return await lambdaClient.topic.getCronTopicsGroupedByCronJob.query({ agentId });
17
+ },
18
+ {
19
+ revalidateOnFocus: false,
20
+ },
21
+ );
22
+
23
+ return {
24
+ cronTopicsGroups: data || [],
25
+ error,
26
+ isLoading,
27
+ mutate,
28
+ };
29
+ };
@@ -0,0 +1,56 @@
1
+ import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
2
+ import useSWR from 'swr';
3
+
4
+ import { lambdaClient } from '@/libs/trpc/client/lambda';
5
+ import { useAgentStore } from '@/store/agent';
6
+
7
+ /**
8
+ * Fetch cron topics grouped by cronJob with job information
9
+ */
10
+ export const useFetchCronTopicsWithJobInfo = () => {
11
+ const agentId = useAgentStore((s) => s.activeAgentId);
12
+
13
+ const { data, isLoading, error, mutate } = useSWR(
14
+ ENABLE_BUSINESS_FEATURES && agentId ? ['cronTopicsWithJobInfo', agentId] : null,
15
+ async () => {
16
+ if (!agentId) return [];
17
+
18
+ const [cronJobsResult, cronTopicsGroups] = await Promise.all([
19
+ lambdaClient.agentCronJob.findByAgent.query({ agentId }),
20
+ lambdaClient.topic.getCronTopicsGroupedByCronJob.query({ agentId }),
21
+ ]);
22
+
23
+ const cronJobs = cronJobsResult.success ? cronJobsResult.data : [];
24
+ const topicsByCronId = new Map(
25
+ cronTopicsGroups.map((group) => [group.cronJobId, group.topics]),
26
+ );
27
+ const cronJobIds = new Set(cronJobs.map((job) => job.id));
28
+
29
+ const groupsWithJobs = cronJobs.map((job) => ({
30
+ cronJob: job,
31
+ cronJobId: job.id,
32
+ topics: topicsByCronId.get(job.id) || [],
33
+ }));
34
+
35
+ const orphanGroups = cronTopicsGroups
36
+ .filter((group) => !cronJobIds.has(group.cronJobId))
37
+ .map((group) => ({
38
+ cronJob: null,
39
+ cronJobId: group.cronJobId,
40
+ topics: group.topics,
41
+ }));
42
+
43
+ return [...groupsWithJobs, ...orphanGroups];
44
+ },
45
+ {
46
+ revalidateOnFocus: false,
47
+ },
48
+ );
49
+
50
+ return {
51
+ cronTopicsGroupsWithJobInfo: data || [],
52
+ error,
53
+ isLoading,
54
+ mutate,
55
+ };
56
+ };
@@ -7,7 +7,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
7
7
  /**
8
8
  * Fetch topics for the current session (agent or group)
9
9
  */
10
- export const useFetchTopics = () => {
10
+ export const useFetchTopics = (options?: { excludeTriggers?: string[] }) => {
11
11
  const isInbox = useAgentStore(builtinAgentSelectors.isInboxAgent);
12
12
  const [activeAgentId, activeGroupId, useFetchTopicsHook] = useChatStore((s) => [
13
13
  s.activeAgentId,
@@ -20,6 +20,9 @@ export const useFetchTopics = () => {
20
20
  // If in group session, use groupId; otherwise use agentId
21
21
  const { isValidating, data } = useFetchTopicsHook(true, {
22
22
  agentId: activeAgentId,
23
+ ...(options?.excludeTriggers && options.excludeTriggers.length > 0
24
+ ? { excludeTriggers: options.excludeTriggers }
25
+ : {}),
23
26
  groupId: activeGroupId,
24
27
  isInbox: activeGroupId ? false : isInbox,
25
28
  pageSize: topicPageSize,
@@ -2,11 +2,54 @@ export default {
2
2
  '_cloud.officialProvider': '{{name}} Official Model Service',
3
3
  'about.title': 'About',
4
4
  'advancedSettings': 'Advanced Settings',
5
+ 'agentCronJobs.addJob': 'Add Scheduled Task',
6
+ 'agentCronJobs.confirmDelete': 'Are you sure you want to delete this scheduled task?',
7
+ 'agentCronJobs.content': 'Task Content',
8
+ 'agentCronJobs.create': 'Create',
9
+ 'agentCronJobs.createSuccess': 'Scheduled task created successfully',
10
+ 'agentCronJobs.deleteJob': 'Delete Task',
11
+ 'agentCronJobs.deleteSuccess': 'Scheduled task deleted successfully',
12
+ 'agentCronJobs.description': 'Automate your agent with scheduled executions',
13
+ 'agentCronJobs.disable': 'Disable',
14
+ 'agentCronJobs.editJob': 'Edit Scheduled Task',
15
+ 'agentCronJobs.empty.description': 'Create your first scheduled task to automate your agent',
16
+ 'agentCronJobs.empty.title': 'No scheduled tasks yet',
17
+ 'agentCronJobs.enable': 'Enable',
18
+ 'agentCronJobs.form.content.placeholder': 'Enter the prompt or instruction for the agent',
19
+ 'agentCronJobs.form.maxExecutions.placeholder': 'Leave empty for unlimited',
20
+ 'agentCronJobs.form.name.placeholder': 'Enter task name',
21
+ 'agentCronJobs.form.timeRange.end': 'End Time',
22
+ 'agentCronJobs.form.timeRange.start': 'Start Time',
23
+ 'agentCronJobs.form.validation.contentRequired': 'Task content is required',
24
+ 'agentCronJobs.form.validation.invalidTimeRange': 'Start time must be before end time',
25
+ 'agentCronJobs.form.validation.nameRequired': 'Task name is required',
26
+ 'agentCronJobs.interval.12hours': 'Every 12 hours',
27
+ 'agentCronJobs.interval.1hour': 'Every hour',
28
+ 'agentCronJobs.interval.30min': 'Every 30 minutes',
29
+ 'agentCronJobs.interval.6hours': 'Every 6 hours',
30
+ 'agentCronJobs.interval.daily': 'Daily',
31
+ 'agentCronJobs.interval.weekly': 'Weekly',
32
+ 'agentCronJobs.lastExecuted': 'Last Executed',
33
+ 'agentCronJobs.maxExecutions': 'Max Executions',
34
+ 'agentCronJobs.name': 'Task Name',
35
+ 'agentCronJobs.never': 'Never',
36
+ 'agentCronJobs.remainingExecutions': 'Remaining: {{count}}',
37
+ 'agentCronJobs.save': 'Save',
38
+ 'agentCronJobs.schedule': 'Schedule',
39
+ 'agentCronJobs.status.depleted': 'Depleted',
40
+ 'agentCronJobs.status.disabled': 'Disabled',
41
+ 'agentCronJobs.status.enabled': 'Enabled',
42
+ 'agentCronJobs.timeRange': 'Time Range',
43
+ 'agentCronJobs.title': 'Scheduled Tasks',
44
+ 'agentCronJobs.unlimited': 'Unlimited',
45
+ 'agentCronJobs.updateSuccess': 'Scheduled task updated successfully',
46
+ 'agentCronJobs.weekdays': 'Weekdays',
5
47
  'agentInfoDescription.basic.avatar': 'Avatar',
6
48
  'agentInfoDescription.basic.description': 'Description',
7
49
  'agentInfoDescription.basic.name': 'Name',
8
50
  'agentInfoDescription.basic.tags': 'Tags',
9
51
  'agentInfoDescription.basic.title': 'Agent info',
52
+
10
53
  'agentInfoDescription.chat.enableHistoryCount': 'Enable Message History Count',
11
54
  'agentInfoDescription.chat.historyCount': 'Message History Count',
12
55
  'agentInfoDescription.chat.no': 'No',
@@ -47,7 +90,7 @@ export default {
47
90
  'danger.reset.action': 'Reset Now',
48
91
  'danger.reset.confirm': 'Reset all settings?',
49
92
  'danger.reset.currentVersion': 'Current Version',
50
- 'danger.reset.desc': 'Restore all settings to defaults. Your data won’t be deleted.',
93
+ 'danger.reset.desc': 'Restore all settings to defaults. Your data wont be deleted.',
51
94
  'danger.reset.success': 'All settings have been reset',
52
95
  'danger.reset.title': 'Reset All Settings',
53
96
  'defaultAgent.model.desc': 'Default model used when creating a new Agent',