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

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 (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/en-US/chat.json +6 -1
  4. package/locales/zh-CN/chat.json +5 -0
  5. package/package.json +1 -1
  6. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  7. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  8. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  9. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  10. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  11. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  12. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  13. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  14. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  15. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  16. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  17. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  18. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  19. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  20. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  21. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  22. package/packages/database/src/models/message.ts +8 -1
  23. package/packages/database/src/models/thread.ts +1 -1
  24. package/packages/types/src/message/ui/chat.ts +2 -0
  25. package/packages/types/src/topic/thread.ts +20 -0
  26. package/src/components/StreamingMarkdown/index.tsx +10 -43
  27. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  28. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  29. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  30. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  31. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  32. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  33. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  34. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  35. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  36. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  37. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  38. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  39. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  40. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  41. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  42. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  43. package/src/hooks/useAutoScroll.ts +117 -0
  44. package/src/locales/default/chat.ts +6 -1
  45. package/src/server/routers/lambda/aiAgent.ts +239 -1
  46. package/src/server/routers/lambda/thread.ts +2 -0
  47. package/src/server/services/message/__tests__/index.test.ts +37 -0
  48. package/src/server/services/message/index.ts +6 -1
  49. package/src/services/aiAgent.ts +51 -0
  50. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  51. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  52. package/src/store/chat/slices/message/actions/query.ts +33 -1
  53. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  54. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  55. package/src/store/chat/slices/operation/types.ts +4 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.328](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.327...v2.0.0-next.328)
6
+
7
+ <sup>Released on **2026-01-20**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Support client tasks mode.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Support client tasks mode, closes [#11666](https://github.com/lobehub/lobe-chat/issues/11666) ([98cf57b](https://github.com/lobehub/lobe-chat/commit/98cf57b))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.327](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.326...v2.0.0-next.327)
6
31
 
7
32
  <sup>Released on **2026-01-20**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Support client tasks mode."
6
+ ]
7
+ },
8
+ "date": "2026-01-20",
9
+ "version": "2.0.0-next.328"
10
+ },
2
11
  {
3
12
  "children": {},
4
13
  "date": "2026-01-20",
@@ -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...",
@@ -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": "任务已取消",
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.328",
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';
@@ -407,18 +407,20 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
407
407
  * Execute a single async task
408
408
  *
409
409
  * This method triggers async task execution by returning a special state.
410
- * The AgentRuntime's executor will recognize this state and trigger the exec_task instruction.
410
+ * The AgentRuntime's executor will recognize this state and trigger the appropriate instruction.
411
411
  *
412
412
  * Flow:
413
- * 1. GTD tool returns stop: true with state.type = 'execTask'
414
- * 2. AgentRuntime executor recognizes the state and triggers exec_task instruction
415
- * 3. exec_task executor creates task message and polls for completion
413
+ * 1. GTD tool returns stop: true with state.type = 'execTask' or 'execClientTask'
414
+ * 2. AgentRuntime executor recognizes the state and triggers exec_task or exec_client_task instruction
415
+ * 3. The executor creates task message and handles execution
416
+ *
417
+ * @param params.runInClient - If true, returns 'execClientTask' state for client-side execution
416
418
  */
417
419
  execTask = async (
418
420
  params: ExecTaskParams,
419
421
  ctx: BuiltinToolContext,
420
422
  ): Promise<BuiltinToolResult> => {
421
- const { description, instruction, inheritMessages, timeout } = params;
423
+ const { description, instruction, inheritMessages, timeout, runInClient } = params;
422
424
 
423
425
  if (!description || !instruction) {
424
426
  return {
@@ -427,19 +429,25 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
427
429
  };
428
430
  }
429
431
 
432
+ const task = {
433
+ description,
434
+ inheritMessages,
435
+ instruction,
436
+ runInClient,
437
+ timeout,
438
+ };
439
+
440
+ // Determine state type based on runInClient
441
+ // If runInClient is true, return 'execClientTask' to trigger client-side executor
442
+ const stateType = runInClient ? 'execClientTask' : 'execTask';
443
+
430
444
  // Return stop: true with special state that AgentRuntime will recognize
431
- // The exec_task executor will be triggered by the runtime when it sees this state
432
445
  return {
433
- content: `🚀 Triggered async task for execution:\n- ${description}`,
446
+ content: `🚀 Triggered async task for ${runInClient ? 'client-side' : ''} execution:\n- ${description}`,
434
447
  state: {
435
448
  parentMessageId: ctx.messageId,
436
- task: {
437
- description,
438
- inheritMessages,
439
- instruction,
440
- timeout,
441
- },
442
- type: 'execTask',
449
+ task,
450
+ type: stateType,
443
451
  },
444
452
  stop: true,
445
453
  success: true,
@@ -450,12 +458,15 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
450
458
  * Execute one or more async tasks
451
459
  *
452
460
  * This method triggers async task execution by returning a special state.
453
- * The AgentRuntime's executor will recognize this state and trigger the exec_tasks instruction.
461
+ * The AgentRuntime's executor will recognize this state and trigger the appropriate instruction.
454
462
  *
455
463
  * Flow:
456
- * 1. GTD tool returns stop: true with state.type = 'execTasks'
457
- * 2. AgentRuntime executor recognizes the state and triggers exec_tasks instruction
458
- * 3. exec_tasks executor creates task messages and polls for completion
464
+ * 1. GTD tool returns stop: true with state.type = 'execTasks' or 'execClientTasks'
465
+ * 2. AgentRuntime executor recognizes the state and triggers exec_tasks or exec_client_tasks instruction
466
+ * 3. The executor creates task messages and handles execution
467
+ *
468
+ * Note: If any task has runInClient=true, all tasks will be routed to 'execClientTasks'.
469
+ * This is because client-side execution is the "special" case requiring local tool access.
459
470
  */
460
471
  execTasks = async (
461
472
  params: ExecTasksParams,
@@ -473,14 +484,20 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
473
484
  const taskCount = tasks.length;
474
485
  const taskList = tasks.map((t, i) => `${i + 1}. ${t.description}`).join('\n');
475
486
 
487
+ // Check if any task requires client-side execution
488
+ const hasClientTasks = tasks.some((t) => t.runInClient);
489
+
490
+ // Determine state type: if any task needs client-side, route all to client executor
491
+ const stateType = hasClientTasks ? 'execClientTasks' : 'execTasks';
492
+ const executionMode = hasClientTasks ? 'client-side' : '';
493
+
476
494
  // Return stop: true with special state that AgentRuntime will recognize
477
- // The exec_tasks executor will be triggered by the runtime when it sees this state
478
495
  return {
479
- content: `🚀 Triggered ${taskCount} async task${taskCount > 1 ? 's' : ''} for execution:\n${taskList}`,
496
+ content: `🚀 Triggered ${taskCount} async task${taskCount > 1 ? 's' : ''} for ${executionMode} execution:\n${taskList}`,
480
497
  state: {
481
498
  parentMessageId: ctx.messageId,
482
499
  tasks,
483
- type: 'execTasks',
500
+ type: stateType,
484
501
  },
485
502
  stop: true,
486
503
  success: true,
@@ -1,5 +1,6 @@
1
1
  import type { BuiltinToolManifest } from '@lobechat/types';
2
2
 
3
+ import { isDesktop } from './const';
3
4
  import { systemPrompt } from './systemRole';
4
5
  import { GTDApiName } from './types';
5
6
 
@@ -171,6 +172,13 @@ export const GTDManifest: BuiltinToolManifest = {
171
172
  'Whether to inherit context messages from the parent conversation. Default is false.',
172
173
  type: 'boolean',
173
174
  },
175
+ ...(isDesktop && {
176
+ runInClient: {
177
+ description:
178
+ 'Whether to run on the desktop client (for local file/shell access). MUST be true when task requires local-system tools. Default is false (server execution).',
179
+ type: 'boolean',
180
+ },
181
+ }),
174
182
  timeout: {
175
183
  description: 'Optional timeout in milliseconds. Default is 30 minutes.',
176
184
  type: 'number',
@@ -203,6 +211,13 @@ export const GTDManifest: BuiltinToolManifest = {
203
211
  'Whether to inherit context messages from the parent conversation. Default is false.',
204
212
  type: 'boolean',
205
213
  },
214
+ ...(isDesktop && {
215
+ runInClient: {
216
+ description:
217
+ 'Whether to run on the desktop client (for local file/shell access). MUST be true when task requires local-system tools. Default is false (server execution).',
218
+ type: 'boolean',
219
+ },
220
+ }),
206
221
  timeout: {
207
222
  description: 'Optional timeout in milliseconds. Default is 30 minutes.',
208
223
  type: 'number',
@@ -1,3 +1,35 @@
1
+ import { isDesktop } from './const';
2
+
3
+ const runInClientSection = `
4
+ <run_in_client>
5
+ **IMPORTANT: When to use \`runInClient: true\` for async tasks**
6
+
7
+ The \`runInClient\` parameter controls WHERE the async task executes:
8
+ - \`runInClient: false\` (default): Task runs on the **server** - suitable for web searches, API calls, general research
9
+ - \`runInClient: true\`: Task runs on the **desktop client** - required for local system access
10
+
11
+ **MUST set \`runInClient: true\` when the task involves:**
12
+ - Reading or writing local files (via \`local-system\` tool)
13
+ - Executing shell commands on the user's machine
14
+ - Accessing local directories or file system
15
+ - Any operation that requires desktop-only local tools
16
+
17
+ **Keep \`runInClient: false\` (or omit) when:**
18
+ - Task only needs web searches or API calls
19
+ - Task processes data that doesn't require local file access
20
+ - Task can be fully completed with server-side capabilities
21
+
22
+ **Note:** \`runInClient\` only has effect on the **desktop app**. On web platform, tasks always run on the server regardless of this setting.
23
+
24
+ **Examples:**
25
+ - "Research Python best practices" → \`runInClient: false\` (web search only)
26
+ - "Organize files in my Downloads folder" → \`runInClient: true\` (local file access required)
27
+ - "Read the project README and summarize it" → \`runInClient: true\` (local file read required)
28
+ - "Find trending tech news" → \`runInClient: false\` (web search only)
29
+ - "Create a new directory structure for my project" → \`runInClient: true\` (local shell/file required)
30
+ </run_in_client>
31
+ `;
32
+
1
33
  export const systemPrompt = `You have GTD (Getting Things Done) tools to help manage plans, todos and tasks effectively. These tools support three levels of task management:
2
34
 
3
35
  - **Plan**: A high-level strategic document describing goals, context, and overall direction. Plans do NOT contain actionable steps - they define the "what" and "why". **Plans should be stable once created** - they represent the overarching objective that rarely changes.
@@ -85,7 +117,7 @@ Use \`execTask\` for a single task, \`execTasks\` for multiple parallel tasks.
85
117
  - User asks a factual question you know → Just answer directly
86
118
  - User wants multiple independent analyses → execTasks (parallel execution)
87
119
  </when_to_use>
88
-
120
+ ${isDesktop ? runInClientSection : ''}
89
121
  <best_practices>
90
122
  - **Plan first, then todos**: Always start with a plan unless explicitly told otherwise
91
123
  - **Separate concerns**: Plans describe goals; Todos list actions