@lobehub/lobehub 2.0.0-next.327 → 2.0.0-next.329

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 (83) hide show
  1. package/.env.example +0 -3
  2. package/.env.example.development +0 -3
  3. package/CHANGELOG.md +58 -0
  4. package/Dockerfile +1 -2
  5. package/changelog/v1.json +18 -0
  6. package/docs/self-hosting/advanced/auth.mdx +5 -6
  7. package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
  8. package/docs/self-hosting/environment-variables/auth.mdx +0 -7
  9. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
  10. package/locales/en-US/chat.json +6 -1
  11. package/locales/en-US/discover.json +1 -0
  12. package/locales/zh-CN/chat.json +5 -0
  13. package/locales/zh-CN/discover.json +1 -0
  14. package/package.json +1 -1
  15. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  16. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  17. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  18. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  19. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  20. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  21. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  22. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  23. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  24. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  25. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  26. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  27. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  28. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  29. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  30. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  31. package/packages/database/src/models/message.ts +8 -1
  32. package/packages/database/src/models/thread.ts +1 -1
  33. package/packages/types/src/message/ui/chat.ts +2 -0
  34. package/packages/types/src/topic/thread.ts +20 -0
  35. package/scripts/prebuild.mts +2 -2
  36. package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
  37. package/src/components/StreamingMarkdown/index.tsx +10 -43
  38. package/src/envs/__tests__/app.test.ts +81 -0
  39. package/src/envs/app.ts +14 -2
  40. package/src/envs/auth.test.ts +0 -13
  41. package/src/envs/auth.ts +0 -41
  42. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  43. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  44. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  45. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  46. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  47. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  48. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  49. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  50. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  51. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  52. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  53. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  54. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  55. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  56. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  57. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  58. package/src/hooks/useAutoScroll.ts +117 -0
  59. package/src/libs/better-auth/auth-client.ts +0 -9
  60. package/src/libs/better-auth/define-config.ts +13 -12
  61. package/src/libs/better-auth/sso/index.ts +2 -1
  62. package/src/libs/better-auth/utils/config.ts +2 -2
  63. package/src/libs/next/proxy/define-config.ts +4 -6
  64. package/src/locales/default/chat.ts +6 -1
  65. package/src/locales/default/discover.ts +2 -0
  66. package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
  67. package/src/server/routers/lambda/aiAgent.ts +239 -1
  68. package/src/server/routers/lambda/thread.ts +2 -0
  69. package/src/server/routers/lambda/topic.ts +6 -0
  70. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
  71. package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
  72. package/src/server/services/message/__tests__/index.test.ts +37 -0
  73. package/src/server/services/message/index.ts +6 -1
  74. package/src/services/aiAgent.ts +51 -0
  75. package/src/services/topic/index.ts +4 -0
  76. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  77. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  78. package/src/store/chat/slices/message/actions/query.ts +33 -1
  79. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  80. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  81. package/src/store/chat/slices/operation/types.ts +4 -0
  82. package/src/store/chat/slices/topic/action.test.ts +2 -1
  83. package/src/store/chat/slices/topic/action.ts +1 -1
package/.env.example CHANGED
@@ -307,9 +307,6 @@ OPENAI_API_KEY=sk-xxxxxxxxx
307
307
  # Shared between Better-Auth and Next-Auth
308
308
  # AUTH_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
309
309
 
310
- # Auth URL (accessible from browser, optional if same domain)
311
- # NEXT_PUBLIC_AUTH_URL=http://localhost:3210
312
-
313
310
  # Require email verification before allowing users to sign in (default: false)
314
311
  # Set to '1' to force users to verify their email before signing in
315
312
  # NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION=0
@@ -43,9 +43,6 @@ NEXT_PUBLIC_ENABLE_BETTER_AUTH=1
43
43
  # Better Auth secret for JWT signing (generate with: openssl rand -base64 32)
44
44
  AUTH_SECRET=${UNSAFE_SECRET}
45
45
 
46
- # Authentication URL
47
- NEXT_PUBLIC_AUTH_URL=${APP_URL}
48
-
49
46
  # SSO providers configuration - using Casdoor for development
50
47
  AUTH_SSO_PROVIDERS=casdoor
51
48
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.329](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.328...v2.0.0-next.329)
6
+
7
+ <sup>Released on **2026-01-21**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **auth**: Remove NEXT_PUBLIC_AUTH_URL env variable.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: Sloved the old removeSessionTopics not work.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### Code refactoring
23
+
24
+ - **auth**: Remove NEXT_PUBLIC_AUTH_URL env variable, closes [#11658](https://github.com/lobehub/lobe-chat/issues/11658) ([c0f9875](https://github.com/lobehub/lobe-chat/commit/c0f9875))
25
+
26
+ #### What's fixed
27
+
28
+ - **misc**: Sloved the old removeSessionTopics not work, closes [#11671](https://github.com/lobehub/lobe-chat/issues/11671) ([06d41e5](https://github.com/lobehub/lobe-chat/commit/06d41e5))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ## [Version 2.0.0-next.328](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.327...v2.0.0-next.328)
39
+
40
+ <sup>Released on **2026-01-20**</sup>
41
+
42
+ #### ✨ Features
43
+
44
+ - **misc**: Support client tasks mode.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### What's improved
52
+
53
+ - **misc**: Support client tasks mode, closes [#11666](https://github.com/lobehub/lobe-chat/issues/11666) ([98cf57b](https://github.com/lobehub/lobe-chat/commit/98cf57b))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ## [Version 2.0.0-next.327](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.326...v2.0.0-next.327)
6
64
 
7
65
  <sup>Released on **2026-01-20**</sup>
package/Dockerfile CHANGED
@@ -189,8 +189,7 @@ ENV KEY_VAULTS_SECRET="" \
189
189
 
190
190
  # Better Auth
191
191
  ENV AUTH_SECRET="" \
192
- AUTH_SSO_PROVIDERS="" \
193
- NEXT_PUBLIC_AUTH_URL=""
192
+ AUTH_SSO_PROVIDERS=""
194
193
 
195
194
  # Clerk
196
195
  ENV CLERK_SECRET_KEY="" \
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Sloved the old removeSessionTopics not work."
6
+ ]
7
+ },
8
+ "date": "2026-01-21",
9
+ "version": "2.0.0-next.329"
10
+ },
11
+ {
12
+ "children": {
13
+ "features": [
14
+ "Support client tasks mode."
15
+ ]
16
+ },
17
+ "date": "2026-01-20",
18
+ "version": "2.0.0-next.328"
19
+ },
2
20
  {
3
21
  "children": {},
4
22
  "date": "2026-01-20",
@@ -39,12 +39,11 @@ By setting the environment variables `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CL
39
39
 
40
40
  To enable Better Auth in LobeChat, set the following environment variables:
41
41
 
42
- | Environment Variable | Type | Description |
43
- | -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | Required | Set to `1` to enable Better Auth service |
45
- | `AUTH_SECRET` | Required | Key used to encrypt session tokens. Generate using: `openssl rand -base64 32` |
46
- | `NEXT_PUBLIC_AUTH_URL` | Required | The browser-accessible base URL for Better Auth (e.g., `http://localhost:3010`, `https://lobechat.com`). Optional for Vercel deployments (auto-detected from `VERCEL_URL`) |
47
- | `AUTH_SSO_PROVIDERS` | Optional | Comma-separated list of enabled SSO providers, e.g., `google,github,microsoft` |
42
+ | Environment Variable | Type | Description |
43
+ | -------------------------------- | -------- | ----------------------------------------------------------------------------- |
44
+ | `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | Required | Set to `1` to enable Better Auth service |
45
+ | `AUTH_SECRET` | Required | Key used to encrypt session tokens. Generate using: `openssl rand -base64 32` |
46
+ | `AUTH_SSO_PROVIDERS` | Optional | Comma-separated list of enabled SSO providers, e.g., `google,github,microsoft`|
48
47
 
49
48
  <Callout type={'error'}>
50
49
  **Important**: Better Auth is currently only suitable for **fresh deployments**. If you are already using NextAuth or Clerk and have existing user data in your database, **do not switch to Better Auth yet**, otherwise existing users will not be able to log in.
@@ -37,12 +37,11 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全
37
37
 
38
38
  要在 LobeChat 中启用 Better Auth,请设置以下环境变量:
39
39
 
40
- | 环境变量 | 类型 | 描述 |
41
- | -------------------------------- | -- | ---------------------------------------------------------------------------------------------------------------- |
42
- | `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | 必选 | 设置为 `1` 以启用 Better Auth 服务 |
43
- | `AUTH_SECRET` | 必选 | 用于加密会话令牌的密钥。使用以下命令生成:`openssl rand -base64 32` |
44
- | `NEXT_PUBLIC_AUTH_URL` | 必选 | 浏览器可访问的 Better Auth 基础 URL(例如 `http://localhost:3010`、`https://lobechat.com`)。Vercel 部署时可选(会自动从 `VERCEL_URL` 获取) |
45
- | `AUTH_SSO_PROVIDERS` | 可选 | 启用的 SSO 提供商列表,以逗号分隔,例如 `google,github,microsoft` |
40
+ | 环境变量 | 类型 | 描述 |
41
+ | -------------------------------- | -- | ----------------------------------------------------------- |
42
+ | `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | 必选 | 设置为 `1` 以启用 Better Auth 服务 |
43
+ | `AUTH_SECRET` | 必选 | 用于加密会话令牌的密钥。使用以下命令生成:`openssl rand -base64 32` |
44
+ | `AUTH_SSO_PROVIDERS` | 可选 | 启用的 SSO 提供商列表,以逗号分隔,例如 `google,github,microsoft` |
46
45
 
47
46
  <Callout type={'error'}>
48
47
  **重要提示**:Better Auth 目前仅适用于**全新部署**的场景。如果你已经使用 NextAuth 或 Clerk 并且数据库中存在用户数据,**请暂时不要切换到 Better Auth**,否则现有用户将无法登录。
@@ -34,13 +34,6 @@ LobeChat provides a complete authentication service capability when deployed. Th
34
34
  - Default: `-`
35
35
  - Example: `Tfhi2t2pelSMEA8eaV61KaqPNEndFFdMIxDaJnS1CUI=`
36
36
 
37
- #### `NEXT_PUBLIC_AUTH_URL`
38
-
39
- - Type: Optional
40
- - Description: The URL accessible from the browser for Better Auth callbacks. Only set this if the default generated URL is incorrect.
41
- - Default: `-`
42
- - Example: `https://example.com`
43
-
44
37
  #### `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION`
45
38
 
46
39
  - Type: Optional
@@ -32,13 +32,6 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相
32
32
  - 默认值:`-`
33
33
  - 示例:`Tfhi2t2pelSMEA8eaV61KaqPNEndFFdMIxDaJnS1CUI=`
34
34
 
35
- #### `NEXT_PUBLIC_AUTH_URL`
36
-
37
- - 类型:可选
38
- - 描述:浏览器可访问的 Better Auth 回调 URL。仅在默认生成的 URL 不正确时设置。
39
- - 默认值:`-`
40
- - 示例:`https://example.com`
41
-
42
35
  #### `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION`
43
36
 
44
37
  - 类型:可选
@@ -204,6 +204,7 @@
204
204
  "noSelectedAgents": "No members selected yet",
205
205
  "openInNewWindow": "Open in New Window",
206
206
  "operation.execAgentRuntime": "Preparing response",
207
+ "operation.execClientTask": "Executing task",
207
208
  "operation.sendMessage": "Sending message",
208
209
  "owner": "Group owner",
209
210
  "pageCopilot.title": "Page Agent",
@@ -322,13 +323,17 @@
322
323
  "tab.profile": "Agent Profile",
323
324
  "tab.search": "Search",
324
325
  "task.activity.calling": "Calling Skill...",
326
+ "task.activity.clientExecuting": "Executing locally...",
325
327
  "task.activity.generating": "Generating response...",
326
328
  "task.activity.gotResult": "Tool result received",
327
329
  "task.activity.toolCalling": "Calling {{toolName}}...",
328
330
  "task.activity.toolResult": "{{toolName}} result received",
329
331
  "task.batchTasks": "{{count}} Batch Subtasks",
332
+ "task.instruction": "Task Instruction",
333
+ "task.intermediateSteps": "{{count}} intermediate steps",
334
+ "task.metrics.duration": "(took {{duration}})",
330
335
  "task.metrics.stepsShort": "steps",
331
- "task.metrics.toolCallsShort": "tool uses",
336
+ "task.metrics.toolCallsShort": "skill uses",
332
337
  "task.status.cancelled": "Task Cancelled",
333
338
  "task.status.failed": "Task Failed",
334
339
  "task.status.initializing": "Initializing task...",
@@ -141,6 +141,7 @@
141
141
  "filterBy.timePeriod.year": "Last Year",
142
142
  "footer.desc": "Evolve with AI users worldwide. Become a creator to submit your agents and skills to the LobeHub Community.",
143
143
  "footer.title": "Share your creation on LobeHub Community today",
144
+ "groupAgents.tag": "Group",
144
145
  "home.communityAgents": "Community Agents",
145
146
  "home.featuredAssistants": "Featured Agents",
146
147
  "home.featuredModels": "Featured Models",
@@ -204,6 +204,7 @@
204
204
  "noSelectedAgents": "还未选择成员",
205
205
  "openInNewWindow": "在新窗口打开",
206
206
  "operation.execAgentRuntime": "准备响应中",
207
+ "operation.execClientTask": "执行任务中",
207
208
  "operation.sendMessage": "消息发送中",
208
209
  "owner": "群主",
209
210
  "pageCopilot.title": "文稿助理",
@@ -322,11 +323,15 @@
322
323
  "tab.profile": "助理档案",
323
324
  "tab.search": "搜索",
324
325
  "task.activity.calling": "正在调用技能…",
326
+ "task.activity.clientExecuting": "本地执行中…",
325
327
  "task.activity.generating": "正在生成回复…",
326
328
  "task.activity.gotResult": "已获取技能结果",
327
329
  "task.activity.toolCalling": "正在调用 {{toolName}}…",
328
330
  "task.activity.toolResult": "已获取 {{toolName}} 结果",
329
331
  "task.batchTasks": "{{count}} 个批量子任务",
332
+ "task.instruction": "任务说明",
333
+ "task.intermediateSteps": "{{count}} 个中间步骤",
334
+ "task.metrics.duration": "(用时 {{duration}})",
330
335
  "task.metrics.stepsShort": "步",
331
336
  "task.metrics.toolCallsShort": "次技能调用",
332
337
  "task.status.cancelled": "任务已取消",
@@ -141,6 +141,7 @@
141
141
  "filterBy.timePeriod.year": "近一年",
142
142
  "footer.desc": "与全球 AI 用户共同进化。成为创作者,向 LobeHub 社区提交你的助手和技能。",
143
143
  "footer.title": "立即在 LobeHub 社区分享你的创作",
144
+ "groupAgents.tag": "群组",
144
145
  "home.communityAgents": "社区助理",
145
146
  "home.featuredAssistants": "推荐助理",
146
147
  "home.featuredModels": "推荐模型",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.327",
3
+ "version": "2.0.0-next.329",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -359,6 +359,30 @@ export class GeneralChatAgent implements Agent {
359
359
  type: 'exec_tasks',
360
360
  };
361
361
  }
362
+
363
+ // GTD client-side async task (single, desktop only)
364
+ if (stateType === 'execClientTask') {
365
+ const { parentMessageId: execParentId, task } = data.state as {
366
+ parentMessageId: string;
367
+ task: any;
368
+ };
369
+ return {
370
+ payload: { parentMessageId: execParentId, task },
371
+ type: 'exec_client_task',
372
+ };
373
+ }
374
+
375
+ // GTD client-side async tasks (multiple, desktop only)
376
+ if (stateType === 'execClientTasks') {
377
+ const { parentMessageId: execParentId, tasks } = data.state as {
378
+ parentMessageId: string;
379
+ tasks: any[];
380
+ };
381
+ return {
382
+ payload: { parentMessageId: execParentId, tasks },
383
+ type: 'exec_client_tasks',
384
+ };
385
+ }
362
386
  }
363
387
 
364
388
  // Check if there are still pending tool messages waiting for approval
@@ -368,6 +368,216 @@ describe('GeneralChatAgent', () => {
368
368
  });
369
369
 
370
370
  describe('tool_result phase', () => {
371
+ describe('GTD async tasks', () => {
372
+ it('should return exec_task for single async task (execTask)', async () => {
373
+ const agent = new GeneralChatAgent({
374
+ agentConfig: { maxSteps: 100 },
375
+ operationId: 'test-session',
376
+ modelRuntimeConfig: mockModelRuntimeConfig,
377
+ });
378
+
379
+ const state = createMockState();
380
+ const context = createMockContext('tool_result', {
381
+ parentMessageId: 'tool-msg-1',
382
+ stop: true,
383
+ data: {
384
+ state: {
385
+ type: 'execTask',
386
+ parentMessageId: 'exec-parent-msg',
387
+ task: { instruction: 'Do something async', timeout: 30000 },
388
+ },
389
+ },
390
+ });
391
+
392
+ const result = await agent.runner(context, state);
393
+
394
+ expect(result).toEqual({
395
+ type: 'exec_task',
396
+ payload: {
397
+ parentMessageId: 'exec-parent-msg',
398
+ task: { instruction: 'Do something async', timeout: 30000 },
399
+ },
400
+ });
401
+ });
402
+
403
+ it('should return exec_tasks for multiple async tasks (execTasks)', async () => {
404
+ const agent = new GeneralChatAgent({
405
+ agentConfig: { maxSteps: 100 },
406
+ operationId: 'test-session',
407
+ modelRuntimeConfig: mockModelRuntimeConfig,
408
+ });
409
+
410
+ const state = createMockState();
411
+ const tasks = [
412
+ { instruction: 'Task 1', timeout: 30000 },
413
+ { instruction: 'Task 2', timeout: 30000 },
414
+ ];
415
+ const context = createMockContext('tool_result', {
416
+ parentMessageId: 'tool-msg-1',
417
+ stop: true,
418
+ data: {
419
+ state: {
420
+ type: 'execTasks',
421
+ parentMessageId: 'exec-parent-msg',
422
+ tasks,
423
+ },
424
+ },
425
+ });
426
+
427
+ const result = await agent.runner(context, state);
428
+
429
+ expect(result).toEqual({
430
+ type: 'exec_tasks',
431
+ payload: {
432
+ parentMessageId: 'exec-parent-msg',
433
+ tasks,
434
+ },
435
+ });
436
+ });
437
+
438
+ it('should return exec_client_task for single client-side async task (execClientTask)', async () => {
439
+ const agent = new GeneralChatAgent({
440
+ agentConfig: { maxSteps: 100 },
441
+ operationId: 'test-session',
442
+ modelRuntimeConfig: mockModelRuntimeConfig,
443
+ });
444
+
445
+ const state = createMockState();
446
+ const context = createMockContext('tool_result', {
447
+ parentMessageId: 'tool-msg-1',
448
+ stop: true,
449
+ data: {
450
+ state: {
451
+ type: 'execClientTask',
452
+ parentMessageId: 'exec-parent-msg',
453
+ task: { type: 'localFile', path: '/path/to/file' },
454
+ },
455
+ },
456
+ });
457
+
458
+ const result = await agent.runner(context, state);
459
+
460
+ expect(result).toEqual({
461
+ type: 'exec_client_task',
462
+ payload: {
463
+ parentMessageId: 'exec-parent-msg',
464
+ task: { type: 'localFile', path: '/path/to/file' },
465
+ },
466
+ });
467
+ });
468
+
469
+ it('should return exec_client_tasks for multiple client-side async tasks (execClientTasks)', async () => {
470
+ const agent = new GeneralChatAgent({
471
+ agentConfig: { maxSteps: 100 },
472
+ operationId: 'test-session',
473
+ modelRuntimeConfig: mockModelRuntimeConfig,
474
+ });
475
+
476
+ const state = createMockState();
477
+ const tasks = [
478
+ { type: 'localFile', path: '/path/to/file1' },
479
+ { type: 'localFile', path: '/path/to/file2' },
480
+ ];
481
+ const context = createMockContext('tool_result', {
482
+ parentMessageId: 'tool-msg-1',
483
+ stop: true,
484
+ data: {
485
+ state: {
486
+ type: 'execClientTasks',
487
+ parentMessageId: 'exec-parent-msg',
488
+ tasks,
489
+ },
490
+ },
491
+ });
492
+
493
+ const result = await agent.runner(context, state);
494
+
495
+ expect(result).toEqual({
496
+ type: 'exec_client_tasks',
497
+ payload: {
498
+ parentMessageId: 'exec-parent-msg',
499
+ tasks,
500
+ },
501
+ });
502
+ });
503
+
504
+ it('should not trigger exec_task when stop is false', async () => {
505
+ const agent = new GeneralChatAgent({
506
+ agentConfig: { maxSteps: 100 },
507
+ operationId: 'test-session',
508
+ modelRuntimeConfig: mockModelRuntimeConfig,
509
+ });
510
+
511
+ const state = createMockState({
512
+ messages: [
513
+ { role: 'user', content: 'Hello' },
514
+ { role: 'assistant', content: '' },
515
+ { role: 'tool', content: 'Result', tool_call_id: 'call-1' },
516
+ ] as any,
517
+ });
518
+ const context = createMockContext('tool_result', {
519
+ parentMessageId: 'tool-msg-1',
520
+ stop: false, // stop is false, should not trigger exec_task
521
+ data: {
522
+ state: {
523
+ type: 'execTask',
524
+ parentMessageId: 'exec-parent-msg',
525
+ task: { instruction: 'Do something async' },
526
+ },
527
+ },
528
+ });
529
+
530
+ const result = await agent.runner(context, state);
531
+
532
+ // Should return call_llm instead of exec_task
533
+ expect(result).toEqual({
534
+ type: 'call_llm',
535
+ payload: {
536
+ messages: state.messages,
537
+ model: 'gpt-4o-mini',
538
+ parentMessageId: 'tool-msg-1',
539
+ provider: 'openai',
540
+ tools: undefined,
541
+ },
542
+ });
543
+ });
544
+
545
+ it('should not trigger exec_task when data.state is undefined', async () => {
546
+ const agent = new GeneralChatAgent({
547
+ agentConfig: { maxSteps: 100 },
548
+ operationId: 'test-session',
549
+ modelRuntimeConfig: mockModelRuntimeConfig,
550
+ });
551
+
552
+ const state = createMockState({
553
+ messages: [
554
+ { role: 'user', content: 'Hello' },
555
+ { role: 'assistant', content: '' },
556
+ { role: 'tool', content: 'Result', tool_call_id: 'call-1' },
557
+ ] as any,
558
+ });
559
+ const context = createMockContext('tool_result', {
560
+ parentMessageId: 'tool-msg-1',
561
+ stop: true,
562
+ data: {}, // No state property
563
+ });
564
+
565
+ const result = await agent.runner(context, state);
566
+
567
+ // Should return call_llm instead of exec_task
568
+ expect(result).toEqual({
569
+ type: 'call_llm',
570
+ payload: {
571
+ messages: state.messages,
572
+ model: 'gpt-4o-mini',
573
+ parentMessageId: 'tool-msg-1',
574
+ provider: 'openai',
575
+ tools: undefined,
576
+ },
577
+ });
578
+ });
579
+ });
580
+
371
581
  it('should return call_llm when no pending tools', async () => {
372
582
  const agent = new GeneralChatAgent({
373
583
  agentConfig: { maxSteps: 100 },
@@ -236,12 +236,26 @@ export interface ExecTaskItem {
236
236
  inheritMessages?: boolean;
237
237
  /** Detailed instruction/prompt for the task execution */
238
238
  instruction: string;
239
+ /**
240
+ * Whether to execute the task on the client side (desktop only).
241
+ * When true and running on desktop, the task will be executed locally
242
+ * with access to local tools (file system, shell commands, etc.).
243
+ *
244
+ * IMPORTANT: This MUST be set to true when the task requires:
245
+ * - Reading/writing local files via `local-system` tool
246
+ * - Executing shell commands
247
+ * - Any other desktop-only local tool operations
248
+ *
249
+ * If not specified or false, the task runs on the server (default behavior).
250
+ * On non-desktop platforms (web), this flag is ignored and tasks always run on server.
251
+ */
252
+ runInClient?: boolean;
239
253
  /** Timeout in milliseconds (optional, default 30 minutes) */
240
254
  timeout?: number;
241
255
  }
242
256
 
243
257
  /**
244
- * Instruction to execute a single async task
258
+ * Instruction to execute a single async task (server-side)
245
259
  */
246
260
  export interface AgentInstructionExecTask {
247
261
  payload: {
@@ -254,7 +268,7 @@ export interface AgentInstructionExecTask {
254
268
  }
255
269
 
256
270
  /**
257
- * Instruction to execute multiple async tasks in parallel
271
+ * Instruction to execute multiple async tasks in parallel (server-side)
258
272
  */
259
273
  export interface AgentInstructionExecTasks {
260
274
  payload: {
@@ -266,6 +280,34 @@ export interface AgentInstructionExecTasks {
266
280
  type: 'exec_tasks';
267
281
  }
268
282
 
283
+ /**
284
+ * Instruction to execute a single async task on the client (desktop only)
285
+ * Used when task requires local tools like file system or shell commands
286
+ */
287
+ export interface AgentInstructionExecClientTask {
288
+ payload: {
289
+ /** Parent message ID (tool message that triggered the task) */
290
+ parentMessageId: string;
291
+ /** Task to execute */
292
+ task: ExecTaskItem;
293
+ };
294
+ type: 'exec_client_task';
295
+ }
296
+
297
+ /**
298
+ * Instruction to execute multiple async tasks on the client in parallel (desktop only)
299
+ * Used when tasks require local tools like file system or shell commands
300
+ */
301
+ export interface AgentInstructionExecClientTasks {
302
+ payload: {
303
+ /** Parent message ID (tool message that triggered the tasks) */
304
+ parentMessageId: string;
305
+ /** Array of tasks to execute */
306
+ tasks: ExecTaskItem[];
307
+ };
308
+ type: 'exec_client_tasks';
309
+ }
310
+
269
311
  /**
270
312
  * Payload for task_result phase (single task)
271
313
  */
@@ -318,6 +360,8 @@ export type AgentInstruction =
318
360
  | AgentInstructionCallToolsBatch
319
361
  | AgentInstructionExecTask
320
362
  | AgentInstructionExecTasks
363
+ | AgentInstructionExecClientTask
364
+ | AgentInstructionExecClientTasks
321
365
  | AgentInstructionRequestHumanPrompt
322
366
  | AgentInstructionRequestHumanSelect
323
367
  | AgentInstructionRequestHumanApprove
@@ -0,0 +1 @@
1
+ export const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';