mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: support exec async sub agent task
This commit is contained in:
@@ -60,6 +60,9 @@
|
||||
"builtins.lobe-gtd.apiName.createPlan": "Create plan",
|
||||
"builtins.lobe-gtd.apiName.createPlan.result": "Create plan: <goal>{{goal}}</goal>",
|
||||
"builtins.lobe-gtd.apiName.createTodos": "Create todos",
|
||||
"builtins.lobe-gtd.apiName.execTask": "Execute task",
|
||||
"builtins.lobe-gtd.apiName.execTask.result": "Execute: <desc>{{description}}</desc>",
|
||||
"builtins.lobe-gtd.apiName.execTasks": "Execute tasks",
|
||||
"builtins.lobe-gtd.apiName.removeTodos": "Delete todos",
|
||||
"builtins.lobe-gtd.apiName.updatePlan": "Update plan",
|
||||
"builtins.lobe-gtd.apiName.updatePlan.completed": "Completed",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"builtins.lobe-gtd.apiName.createPlan": "创建计划",
|
||||
"builtins.lobe-gtd.apiName.createPlan.result": "创建计划:<goal>{{goal}}</goal>",
|
||||
"builtins.lobe-gtd.apiName.createTodos": "创建待办",
|
||||
"builtins.lobe-gtd.apiName.execTask": "执行任务",
|
||||
"builtins.lobe-gtd.apiName.execTask.result": "执行:<desc>{{description}}</desc>",
|
||||
"builtins.lobe-gtd.apiName.execTasks": "执行多个任务",
|
||||
"builtins.lobe-gtd.apiName.removeTodos": "删除待办",
|
||||
"builtins.lobe-gtd.apiName.updatePlan": "更新计划",
|
||||
"builtins.lobe-gtd.apiName.updatePlan.completed": "已完成",
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
GeneralAgentCallingToolInstructionPayload,
|
||||
GeneralAgentConfig,
|
||||
HumanAbortPayload,
|
||||
TaskResultPayload,
|
||||
TasksBatchResultPayload,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
@@ -315,7 +317,37 @@ export class GeneralChatAgent implements Agent {
|
||||
}
|
||||
|
||||
case 'tool_result': {
|
||||
const { parentMessageId } = context.payload as GeneralAgentCallToolResultPayload;
|
||||
const { data, parentMessageId, stop } =
|
||||
context.payload as GeneralAgentCallToolResultPayload;
|
||||
|
||||
// Check if this is a GTD async task request (only execTask/execTasks are passed here with stop=true)
|
||||
if (stop && data?.state) {
|
||||
const stateType = data.state.type;
|
||||
|
||||
// GTD async task (single)
|
||||
if (stateType === 'execTask') {
|
||||
const { parentMessageId: execParentId, task } = data.state as {
|
||||
parentMessageId: string;
|
||||
task: any;
|
||||
};
|
||||
return {
|
||||
payload: { parentMessageId: execParentId, task },
|
||||
type: 'exec_task',
|
||||
};
|
||||
}
|
||||
|
||||
// GTD async tasks (multiple)
|
||||
if (stateType === 'execTasks') {
|
||||
const { parentMessageId: execParentId, tasks } = data.state as {
|
||||
parentMessageId: string;
|
||||
tasks: any[];
|
||||
};
|
||||
return {
|
||||
payload: { parentMessageId: execParentId, tasks },
|
||||
type: 'exec_tasks',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are still pending tool messages waiting for approval
|
||||
const pendingToolMessages = state.messages.filter(
|
||||
@@ -380,6 +412,40 @@ export class GeneralChatAgent implements Agent {
|
||||
};
|
||||
}
|
||||
|
||||
case 'task_result': {
|
||||
// Single async task completed, continue to call LLM with result
|
||||
const { parentMessageId } = context.payload as TaskResultPayload;
|
||||
|
||||
// Continue to call LLM with updated messages (task message is already in state)
|
||||
return {
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
model: this.config.modelRuntimeConfig?.model,
|
||||
parentMessageId,
|
||||
provider: this.config.modelRuntimeConfig?.provider,
|
||||
tools: state.tools,
|
||||
} as GeneralAgentCallLLMInstructionPayload,
|
||||
type: 'call_llm',
|
||||
};
|
||||
}
|
||||
|
||||
case 'tasks_batch_result': {
|
||||
// Async tasks batch completed, continue to call LLM with results
|
||||
const { parentMessageId } = context.payload as TasksBatchResultPayload;
|
||||
|
||||
// Continue to call LLM with updated messages (task messages are already in state)
|
||||
return {
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
model: this.config.modelRuntimeConfig?.model,
|
||||
parentMessageId,
|
||||
provider: this.config.modelRuntimeConfig?.provider,
|
||||
tools: state.tools,
|
||||
} as GeneralAgentCallLLMInstructionPayload,
|
||||
type: 'call_llm',
|
||||
};
|
||||
}
|
||||
|
||||
case 'human_abort': {
|
||||
// User aborted the operation
|
||||
const { hasToolsCalling, parentMessageId, toolsCalling, reason } =
|
||||
|
||||
@@ -893,6 +893,164 @@ describe('GeneralChatAgent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('task_result phase (single task)', () => {
|
||||
it('should return call_llm when task completed', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const state = createMockState({
|
||||
messages: [
|
||||
{ role: 'user', content: 'Execute task' },
|
||||
{ role: 'assistant', content: '' },
|
||||
{ role: 'task', content: 'Task result', metadata: { instruction: 'Do task' } },
|
||||
] as any,
|
||||
});
|
||||
|
||||
const context = createMockContext('task_result', {
|
||||
parentMessageId: 'task-parent-msg',
|
||||
result: { success: true, taskMessageId: 'task-1', threadId: 'thread-1', result: 'Task result' },
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'call_llm',
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
model: 'gpt-4o-mini',
|
||||
parentMessageId: 'task-parent-msg',
|
||||
provider: 'openai',
|
||||
tools: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return call_llm even when task failed', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const state = createMockState({
|
||||
messages: [
|
||||
{ role: 'user', content: 'Execute task' },
|
||||
{ role: 'assistant', content: '' },
|
||||
{ role: 'task', content: 'Task failed: timeout', metadata: { instruction: 'Do task' } },
|
||||
] as any,
|
||||
});
|
||||
|
||||
const context = createMockContext('task_result', {
|
||||
parentMessageId: 'task-parent-msg',
|
||||
result: {
|
||||
success: false,
|
||||
taskMessageId: 'task-1',
|
||||
threadId: 'thread-1',
|
||||
error: 'Task timeout after 1800000ms',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'call_llm',
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
model: 'gpt-4o-mini',
|
||||
parentMessageId: 'task-parent-msg',
|
||||
provider: 'openai',
|
||||
tools: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tasks_batch_result phase (multiple tasks)', () => {
|
||||
it('should return call_llm when tasks completed', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const state = createMockState({
|
||||
messages: [
|
||||
{ role: 'user', content: 'Execute tasks' },
|
||||
{ role: 'assistant', content: '' },
|
||||
{ role: 'task', content: 'Task 1 result', metadata: { instruction: 'Do task 1' } },
|
||||
{ role: 'task', content: 'Task 2 result', metadata: { instruction: 'Do task 2' } },
|
||||
] as any,
|
||||
});
|
||||
|
||||
const context = createMockContext('tasks_batch_result', {
|
||||
parentMessageId: 'task-parent-msg',
|
||||
results: [
|
||||
{ success: true, taskMessageId: 'task-1', threadId: 'thread-1', result: 'Task 1 result' },
|
||||
{ success: true, taskMessageId: 'task-2', threadId: 'thread-2', result: 'Task 2 result' },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'call_llm',
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
model: 'gpt-4o-mini',
|
||||
parentMessageId: 'task-parent-msg',
|
||||
provider: 'openai',
|
||||
tools: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return call_llm even when some tasks failed', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const state = createMockState({
|
||||
messages: [
|
||||
{ role: 'user', content: 'Execute tasks' },
|
||||
{ role: 'assistant', content: '' },
|
||||
{ role: 'task', content: 'Task 1 result', metadata: { instruction: 'Do task 1' } },
|
||||
{ role: 'task', content: 'Task failed: timeout', metadata: { instruction: 'Do task 2' } },
|
||||
] as any,
|
||||
});
|
||||
|
||||
const context = createMockContext('tasks_batch_result', {
|
||||
parentMessageId: 'task-parent-msg',
|
||||
results: [
|
||||
{ success: true, taskMessageId: 'task-1', threadId: 'thread-1', result: 'Task 1 result' },
|
||||
{
|
||||
success: false,
|
||||
taskMessageId: 'task-2',
|
||||
threadId: 'thread-2',
|
||||
error: 'Task timeout after 1800000ms',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'call_llm',
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
model: 'gpt-4o-mini',
|
||||
parentMessageId: 'task-parent-msg',
|
||||
provider: 'openai',
|
||||
tools: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown phase', () => {
|
||||
it('should return finish instruction for unknown phase', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface GeneralAgentCallToolResultPayload {
|
||||
executionTime: number;
|
||||
isSuccess: boolean;
|
||||
parentMessageId: string;
|
||||
/** Whether tool requested to stop execution (e.g., group management speak/delegate, GTD async tasks) */
|
||||
stop?: boolean;
|
||||
toolCall: ChatToolPayload;
|
||||
toolCallId: string;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface AgentRuntimeContext {
|
||||
| 'llm_result'
|
||||
| 'tool_result'
|
||||
| 'tools_batch_result'
|
||||
| 'task_result'
|
||||
| 'tasks_batch_result'
|
||||
| 'human_response'
|
||||
| 'human_approved_tool'
|
||||
| 'human_abort'
|
||||
@@ -224,6 +226,88 @@ export interface AgentInstructionCompressContext {
|
||||
type: 'compress_context';
|
||||
}
|
||||
|
||||
/**
|
||||
* Task definition for exec_tasks instruction
|
||||
*/
|
||||
export interface ExecTaskItem {
|
||||
/** Brief description of what this task does (shown in UI) */
|
||||
description: string;
|
||||
/** Whether to inherit context messages from parent conversation */
|
||||
inheritMessages?: boolean;
|
||||
/** Detailed instruction/prompt for the task execution */
|
||||
instruction: string;
|
||||
/** Timeout in milliseconds (optional, default 30 minutes) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruction to execute a single async task
|
||||
*/
|
||||
export interface AgentInstructionExecTask {
|
||||
payload: {
|
||||
/** Parent message ID (tool message that triggered the task) */
|
||||
parentMessageId: string;
|
||||
/** Task to execute */
|
||||
task: ExecTaskItem;
|
||||
};
|
||||
type: 'exec_task';
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruction to execute multiple async tasks in parallel
|
||||
*/
|
||||
export interface AgentInstructionExecTasks {
|
||||
payload: {
|
||||
/** Parent message ID (tool message that triggered the tasks) */
|
||||
parentMessageId: string;
|
||||
/** Array of tasks to execute */
|
||||
tasks: ExecTaskItem[];
|
||||
};
|
||||
type: 'exec_tasks';
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for task_result phase (single task)
|
||||
*/
|
||||
export interface TaskResultPayload {
|
||||
/** Parent message ID */
|
||||
parentMessageId: string;
|
||||
/** Result from executed task */
|
||||
result: {
|
||||
/** Error message if task failed */
|
||||
error?: string;
|
||||
/** Task result content */
|
||||
result?: string;
|
||||
/** Whether the task completed successfully */
|
||||
success: boolean;
|
||||
/** Task message ID */
|
||||
taskMessageId: string;
|
||||
/** Thread ID where the task was executed */
|
||||
threadId: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for tasks_batch_result phase (multiple tasks)
|
||||
*/
|
||||
export interface TasksBatchResultPayload {
|
||||
/** Parent message ID */
|
||||
parentMessageId: string;
|
||||
/** Results from executed tasks */
|
||||
results: Array<{
|
||||
/** Error message if task failed */
|
||||
error?: string;
|
||||
/** Task result content */
|
||||
result?: string;
|
||||
/** Whether the task completed successfully */
|
||||
success: boolean;
|
||||
/** Task message ID */
|
||||
taskMessageId: string;
|
||||
/** Thread ID where the task was executed */
|
||||
threadId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A serializable instruction object that the "Agent" (Brain) returns
|
||||
* to the "AgentRuntime" (Engine) to execute.
|
||||
@@ -232,6 +316,8 @@ export type AgentInstruction =
|
||||
| AgentInstructionCallLlm
|
||||
| AgentInstructionCallTool
|
||||
| AgentInstructionCallToolsBatch
|
||||
| AgentInstructionExecTask
|
||||
| AgentInstructionExecTasks
|
||||
| AgentInstructionRequestHumanPrompt
|
||||
| AgentInstructionRequestHumanSelect
|
||||
| AgentInstructionRequestHumanApprove
|
||||
|
||||
@@ -14,48 +14,4 @@ Your role is to:
|
||||
- Provide clear and concise explanations
|
||||
- Be friendly and professional in your responses
|
||||
|
||||
<builtin_tools_guidelines>
|
||||
You have access to two built-in tools: **Notebook** for content creation and **GTD** for task management.
|
||||
|
||||
## Notebook Tool (createDocument)
|
||||
Use Notebook to create documents when:
|
||||
- User requests relatively long content (articles, reports, analyses, tutorials, guides)
|
||||
- User explicitly asks to "write", "draft", "create", "generate" substantial content
|
||||
- Output would exceed ~500 words or benefit from structured formatting
|
||||
- Content should be preserved for future reference or editing
|
||||
- Creating deliverables: blog posts, documentation, summaries, research notes
|
||||
|
||||
**When to respond directly in chat instead**:
|
||||
- Short answers, explanations, or clarifications
|
||||
- Quick Q&A interactions
|
||||
- Code snippets or brief examples
|
||||
- Conversational exchanges
|
||||
|
||||
## GTD Tool (createPlan, createTodos)
|
||||
**ONLY use GTD when user explicitly requests task/project management**:
|
||||
- User explicitly asks to "create a plan", "make a todo list", "track tasks"
|
||||
- User says "help me plan [project]", "organize my tasks", "remind me to..."
|
||||
- User provides a list of things they need to do and wants to track them
|
||||
|
||||
**When NOT to use GTD** (respond in chat instead):
|
||||
- Answering questions (even if about "what to do" or "steps to take")
|
||||
- Providing advice, analysis, or opinions
|
||||
- Code review or technical consultations
|
||||
- Explaining concepts or procedures
|
||||
- Any question that starts with "Is...", "Can...", "Should...", "Would...", "What if..."
|
||||
- Security assessments or risk analysis
|
||||
|
||||
**Key principle**: GTD is for ACTION TRACKING, not for answering questions. If the user is asking a question (even about tasks or plans), just answer it directly.
|
||||
|
||||
## Choosing the Right Tool
|
||||
- "Write me an article about..." → Notebook
|
||||
- "Help me plan my project" → GTD (plan + todos)
|
||||
- "Create a to-do list for..." → GTD (todos)
|
||||
- "Draft a report on..." → Notebook
|
||||
- "What are the steps to..." → Chat (just explain)
|
||||
- "Is this code secure?" → Chat (just answer)
|
||||
- "Should I do X or Y?" → Chat (just advise)
|
||||
- "Remember to..." / "Add to my list..." → GTD (todos)
|
||||
</builtin_tools_guidelines>
|
||||
|
||||
Respond in the same language the user is using.`;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { ExecTaskParams, ExecTaskState } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
description: css`
|
||||
padding-block-end: 1px;
|
||||
color: ${cssVar.colorText};
|
||||
background: linear-gradient(to top, ${cssVar.colorInfoBg} 40%, transparent 40%);
|
||||
`,
|
||||
root: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const ExecTaskInspector = memo<BuiltinInspectorProps<ExecTaskParams, ExecTaskState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const description = args?.description || partialArgs?.description;
|
||||
|
||||
if (isArgumentsStreaming && !description) {
|
||||
return (
|
||||
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-gtd.apiName.execTask')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}>
|
||||
{description ? (
|
||||
<Trans
|
||||
components={{ desc: <span className={styles.description} /> }}
|
||||
i18nKey="builtins.lobe-gtd.apiName.execTask.result"
|
||||
ns="plugin"
|
||||
values={{ description }}
|
||||
/>
|
||||
) : (
|
||||
<span>{t('builtins.lobe-gtd.apiName.execTask')}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ExecTaskInspector.displayName = 'ExecTaskInspector';
|
||||
|
||||
export default ExecTaskInspector;
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { ListTodo } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { ExecTasksParams, ExecTasksState } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
count: css`
|
||||
font-family: ${cssVar.fontFamilyCode};
|
||||
color: ${cssVar.colorInfo};
|
||||
`,
|
||||
root: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
`,
|
||||
title: css`
|
||||
margin-inline-end: 8px;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const ExecTasksInspector = memo<BuiltinInspectorProps<ExecTasksParams, ExecTasksState>>(
|
||||
({ args, partialArgs, isArgumentsStreaming }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const tasks = args?.tasks || partialArgs?.tasks || [];
|
||||
const count = tasks.length;
|
||||
|
||||
if (isArgumentsStreaming && count === 0) {
|
||||
return (
|
||||
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-gtd.apiName.execTasks')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}>
|
||||
<span className={styles.title}>{t('builtins.lobe-gtd.apiName.execTasks')}</span>
|
||||
{count > 0 && (
|
||||
<span className={styles.count}>
|
||||
<Icon icon={ListTodo} size={12} />
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ExecTasksInspector.displayName = 'ExecTasksInspector';
|
||||
|
||||
export default ExecTasksInspector;
|
||||
@@ -5,6 +5,8 @@ import { ClearTodosInspector } from './ClearTodos';
|
||||
import { CompleteTodosInspector } from './CompleteTodos';
|
||||
import { CreatePlanInspector } from './CreatePlan';
|
||||
import { CreateTodosInspector } from './CreateTodos';
|
||||
import { ExecTaskInspector } from './ExecTask';
|
||||
import { ExecTasksInspector } from './ExecTasks';
|
||||
import { RemoveTodosInspector } from './RemoveTodos';
|
||||
import { UpdatePlanInspector } from './UpdatePlan';
|
||||
import { UpdateTodosInspector } from './UpdateTodos';
|
||||
@@ -20,6 +22,8 @@ export const GTDInspectors: Record<string, BuiltinInspector> = {
|
||||
[GTDApiName.completeTodos]: CompleteTodosInspector as BuiltinInspector,
|
||||
[GTDApiName.createPlan]: CreatePlanInspector as BuiltinInspector,
|
||||
[GTDApiName.createTodos]: CreateTodosInspector as BuiltinInspector,
|
||||
[GTDApiName.execTask]: ExecTaskInspector as BuiltinInspector,
|
||||
[GTDApiName.execTasks]: ExecTasksInspector as BuiltinInspector,
|
||||
[GTDApiName.removeTodos]: RemoveTodosInspector as BuiltinInspector,
|
||||
[GTDApiName.updatePlan]: UpdatePlanInspector as BuiltinInspector,
|
||||
[GTDApiName.updateTodos]: UpdateTodosInspector as BuiltinInspector,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Markdown } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { ExecTaskParams } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: ${cssVar.colorFillQuaternary};
|
||||
`,
|
||||
description: css`
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
instruction: css`
|
||||
font-size: 13px;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const ExecTaskStreaming = memo<BuiltinStreamingProps<ExecTaskParams>>(({ args }) => {
|
||||
const { instruction } = args || {};
|
||||
|
||||
if (!instruction) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{instruction && (
|
||||
<div className={styles.instruction}>
|
||||
<Markdown animated variant={'chat'}>
|
||||
{instruction}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ExecTaskStreaming.displayName = 'ExecTaskStreaming';
|
||||
|
||||
export default ExecTaskStreaming;
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Markdown } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { ExecTasksParams } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`,
|
||||
description: css`
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
instruction: css`
|
||||
font-size: 13px;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
taskItem: css`
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: ${cssVar.colorFillQuaternary};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const ExecTasksStreaming = memo<BuiltinStreamingProps<ExecTasksParams>>(({ args }) => {
|
||||
const { tasks } = args || {};
|
||||
|
||||
if (!tasks || tasks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{tasks.map((task, index) => (
|
||||
<div className={styles.taskItem} key={index}>
|
||||
{task.description && <div className={styles.description}>{task.description}</div>}
|
||||
{task.instruction && (
|
||||
<div className={styles.instruction}>
|
||||
<Markdown animated variant={'chat'}>
|
||||
{task.instruction}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ExecTasksStreaming.displayName = 'ExecTasksStreaming';
|
||||
|
||||
export default ExecTasksStreaming;
|
||||
18
packages/builtin-tool-gtd/src/client/Streaming/index.ts
Normal file
18
packages/builtin-tool-gtd/src/client/Streaming/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { BuiltinStreaming } from '@lobechat/types';
|
||||
|
||||
import { GTDApiName } from '../../types';
|
||||
import { ExecTaskStreaming } from './ExecTask';
|
||||
import { ExecTasksStreaming } from './ExecTasks';
|
||||
|
||||
/**
|
||||
* GTD Streaming Components Registry
|
||||
*
|
||||
* Streaming components render tool calls while they are
|
||||
* still executing, allowing real-time feedback to users.
|
||||
*/
|
||||
export const GTDStreamings: Record<string, BuiltinStreaming> = {
|
||||
[GTDApiName.execTask]: ExecTaskStreaming as BuiltinStreaming,
|
||||
[GTDApiName.execTasks]: ExecTasksStreaming as BuiltinStreaming,
|
||||
};
|
||||
|
||||
export { ExecTaskStreaming, ExecTasksStreaming };
|
||||
@@ -5,6 +5,9 @@ export { GTDInspectors } from './Inspector';
|
||||
export type { TodoListRenderState } from './Render';
|
||||
export { GTDRenders, TodoListRender, TodoListUI } from './Render';
|
||||
|
||||
// Streaming components (real-time tool execution feedback)
|
||||
export { ExecTaskStreaming, ExecTasksStreaming, GTDStreamings } from './Streaming';
|
||||
|
||||
// Intervention components (interactive editing)
|
||||
export { AddTodoIntervention, ClearTodosIntervention, GTDInterventions } from './Intervention';
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
type CompleteTodosParams,
|
||||
type CreatePlanParams,
|
||||
type CreateTodosParams,
|
||||
type ExecTaskParams,
|
||||
type ExecTasksParams,
|
||||
GTDApiName,
|
||||
type Plan,
|
||||
type RemoveTodosParams,
|
||||
@@ -19,12 +21,13 @@ import {
|
||||
} from '../types';
|
||||
import { getTodosFromContext } from './helper';
|
||||
|
||||
// API enum for MVP (Todo + Plan)
|
||||
const GTDApiNameMVP = {
|
||||
const GTDApiNameEnum = {
|
||||
clearTodos: GTDApiName.clearTodos,
|
||||
completeTodos: GTDApiName.completeTodos,
|
||||
createPlan: GTDApiName.createPlan,
|
||||
createTodos: GTDApiName.createTodos,
|
||||
execTask: GTDApiName.execTask,
|
||||
execTasks: GTDApiName.execTasks,
|
||||
removeTodos: GTDApiName.removeTodos,
|
||||
updatePlan: GTDApiName.updatePlan,
|
||||
updateTodos: GTDApiName.updateTodos,
|
||||
@@ -33,9 +36,9 @@ const GTDApiNameMVP = {
|
||||
/**
|
||||
* GTD Tool Executor
|
||||
*/
|
||||
class GTDExecutor extends BaseExecutor<typeof GTDApiNameMVP> {
|
||||
class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
||||
readonly identifier = GTDIdentifier;
|
||||
protected readonly apiEnum = GTDApiNameMVP;
|
||||
protected readonly apiEnum = GTDApiNameEnum;
|
||||
|
||||
// ==================== Todo APIs ====================
|
||||
|
||||
@@ -477,6 +480,92 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameMVP> {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Async Tasks API ====================
|
||||
|
||||
/**
|
||||
* Execute a single async task
|
||||
*
|
||||
* This method triggers async task execution by returning a special state.
|
||||
* The AgentRuntime's executor will recognize this state and trigger the exec_task instruction.
|
||||
*
|
||||
* Flow:
|
||||
* 1. GTD tool returns stop: true with state.type = 'execTask'
|
||||
* 2. AgentRuntime executor recognizes the state and triggers exec_task instruction
|
||||
* 3. exec_task executor creates task message and polls for completion
|
||||
*/
|
||||
execTask = async (
|
||||
params: ExecTaskParams,
|
||||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
const { description, instruction, inheritMessages, timeout } = params;
|
||||
|
||||
if (!description || !instruction) {
|
||||
return {
|
||||
content: 'Task description and instruction are required.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Return stop: true with special state that AgentRuntime will recognize
|
||||
// The exec_task executor will be triggered by the runtime when it sees this state
|
||||
return {
|
||||
content: `🚀 Triggered async task for execution:\n- ${description}`,
|
||||
state: {
|
||||
parentMessageId: ctx.messageId,
|
||||
task: {
|
||||
description,
|
||||
inheritMessages,
|
||||
instruction,
|
||||
timeout,
|
||||
},
|
||||
type: 'execTask',
|
||||
},
|
||||
stop: true,
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute one or more async tasks
|
||||
*
|
||||
* This method triggers async task execution by returning a special state.
|
||||
* The AgentRuntime's executor will recognize this state and trigger the exec_tasks instruction.
|
||||
*
|
||||
* Flow:
|
||||
* 1. GTD tool returns stop: true with state.type = 'execTasks'
|
||||
* 2. AgentRuntime executor recognizes the state and triggers exec_tasks instruction
|
||||
* 3. exec_tasks executor creates task messages and polls for completion
|
||||
*/
|
||||
execTasks = async (
|
||||
params: ExecTasksParams,
|
||||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
const { tasks } = params;
|
||||
|
||||
if (!tasks || tasks.length === 0) {
|
||||
return {
|
||||
content: 'No tasks provided to execute.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const taskCount = tasks.length;
|
||||
const taskList = tasks.map((t, i) => `${i + 1}. ${t.description}`).join('\n');
|
||||
|
||||
// Return stop: true with special state that AgentRuntime will recognize
|
||||
// The exec_tasks executor will be triggered by the runtime when it sees this state
|
||||
return {
|
||||
content: `🚀 Triggered ${taskCount} async task${taskCount > 1 ? 's' : ''} for execution:\n${taskList}`,
|
||||
state: {
|
||||
parentMessageId: ctx.messageId,
|
||||
tasks,
|
||||
type: 'execTasks',
|
||||
},
|
||||
stop: true,
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Export the executor instance for registration
|
||||
|
||||
@@ -37,12 +37,13 @@ export const GTDManifest: BuiltinToolManifest = {
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Update an existing plan document. Use this to modify the goal, description, context, or mark the plan as completed.',
|
||||
'Update an existing plan document. Only use this when the goal fundamentally changes. Plans should remain stable once created - do not update plans just because details change.',
|
||||
name: GTDApiName.updatePlan,
|
||||
parameters: {
|
||||
properties: {
|
||||
planId: {
|
||||
description: 'The ID of the plan to update.',
|
||||
description:
|
||||
'The document ID of the plan to update (e.g., "docs_xxx"). This ID is returned in the createPlan response. Do NOT use the goal text as planId.',
|
||||
type: 'string',
|
||||
},
|
||||
goal: {
|
||||
@@ -57,10 +58,6 @@ export const GTDManifest: BuiltinToolManifest = {
|
||||
description: 'Updated detailed context.',
|
||||
type: 'string',
|
||||
},
|
||||
completed: {
|
||||
description: 'Mark the plan as completed.',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
required: ['planId'],
|
||||
type: 'object',
|
||||
@@ -71,8 +68,7 @@ export const GTDManifest: BuiltinToolManifest = {
|
||||
{
|
||||
description: 'Create new todo items. Pass an array of text strings.',
|
||||
name: GTDApiName.createTodos,
|
||||
humanIntervention: 'always',
|
||||
renderDisplayControl: 'expand',
|
||||
humanIntervention: 'required',
|
||||
parameters: {
|
||||
properties: {
|
||||
adds: {
|
||||
@@ -179,6 +175,74 @@ export const GTDManifest: BuiltinToolManifest = {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Async Tasks ====================
|
||||
{
|
||||
description:
|
||||
'Execute a single long-running async task. The task runs in an isolated context and can take significant time to complete. Use this for a single complex operation that requires extended processing.',
|
||||
name: GTDApiName.execTask,
|
||||
parameters: {
|
||||
properties: {
|
||||
description: {
|
||||
description: 'Brief description of what this task does (shown in UI).',
|
||||
type: 'string',
|
||||
},
|
||||
instruction: {
|
||||
description: 'Detailed instruction/prompt for the task execution.',
|
||||
type: 'string',
|
||||
},
|
||||
inheritMessages: {
|
||||
description:
|
||||
'Whether to inherit context messages from the parent conversation. Default is false.',
|
||||
type: 'boolean',
|
||||
},
|
||||
timeout: {
|
||||
description: 'Optional timeout in milliseconds. Default is 30 minutes.',
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: ['description', 'instruction'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Execute one or more long-running async tasks. Each task runs in an isolated context and can take significant time to complete. Use this for complex operations that require extended processing.',
|
||||
name: GTDApiName.execTasks,
|
||||
parameters: {
|
||||
properties: {
|
||||
tasks: {
|
||||
description: 'Array of tasks to execute asynchronously.',
|
||||
items: {
|
||||
properties: {
|
||||
description: {
|
||||
description: 'Brief description of what this task does (shown in UI).',
|
||||
type: 'string',
|
||||
},
|
||||
instruction: {
|
||||
description: 'Detailed instruction/prompt for the task execution.',
|
||||
type: 'string',
|
||||
},
|
||||
inheritMessages: {
|
||||
description:
|
||||
'Whether to inherit context messages from the parent conversation. Default is false.',
|
||||
type: 'boolean',
|
||||
},
|
||||
timeout: {
|
||||
description: 'Optional timeout in milliseconds. Default is 30 minutes.',
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: ['description', 'instruction'],
|
||||
type: 'object',
|
||||
},
|
||||
type: 'array',
|
||||
},
|
||||
},
|
||||
required: ['tasks'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
],
|
||||
identifier: GTDIdentifier,
|
||||
meta: {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
export const systemPrompt = `You have GTD (Getting Things Done) tools to help manage plans and todos effectively. These tools support two levels of task management:
|
||||
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:
|
||||
|
||||
- **Plan**: A high-level strategic document describing goals, context, and overall direction. Plans do NOT contain actionable steps - they define the "what" and "why".
|
||||
- **Todo**: The concrete execution list with actionable items. Todos define the "how" - specific tasks to accomplish the plan.
|
||||
- **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.
|
||||
- **Todo**: The concrete execution list with actionable items. Todos define the "how" - specific tasks to accomplish the plan. **Todos are dynamic** - they can be added, updated, completed, and removed as work progresses.
|
||||
- **Task**: Long-running async operations that execute in isolated contexts. Tasks are for complex, multi-step operations that require extended processing time. **Tasks run independently** - they can inherit context but execute separately from the main conversation.
|
||||
|
||||
<tool_overview>
|
||||
**Planning Tools** - For high-level goal documentation:
|
||||
- \`createPlan\`: Create a strategic plan document with goal and context
|
||||
- \`updatePlan\`: Update plan details or mark as completed
|
||||
- \`updatePlan\`: Update plan details
|
||||
|
||||
**Todo Tools** - For actionable execution items:
|
||||
- \`createTodos\`: Create new todo items from text array
|
||||
@@ -14,11 +15,14 @@ export const systemPrompt = `You have GTD (Getting Things Done) tools to help ma
|
||||
- \`completeTodos\`: Mark items as done by indices
|
||||
- \`removeTodos\`: Remove items by indices
|
||||
- \`clearTodos\`: Clear completed or all items
|
||||
|
||||
**Async Task Tools** - For long-running background tasks:
|
||||
- \`execTask\`: Execute a single async task in isolated context
|
||||
- \`execTasks\`: Execute multiple async tasks in parallel
|
||||
</tool_overview>
|
||||
|
||||
<default_workflow>
|
||||
**IMPORTANT: Always create a Plan first, then Todos.**
|
||||
|
||||
When a user asks you to help with a task, goal, or project:
|
||||
1. **First**, use \`createPlan\` to document the goal and relevant context
|
||||
2. **Then**, use \`createTodos\` to break down the plan into actionable steps
|
||||
@@ -47,6 +51,26 @@ This "Plan-First" approach ensures:
|
||||
- User explicitly requests only action items
|
||||
- Capturing quick, simple tasks that don't need planning
|
||||
- Tracking progress on concrete deliverables
|
||||
|
||||
**Use Async Tasks when:**
|
||||
- **The request requires gathering external information**: User wants you to research, investigate, or find information that you don't already know. This requires web searches, reading multiple sources, and synthesizing information.
|
||||
- **The task involves multiple steps**: The request cannot be answered in one simple response - it requires searching, reading, analyzing, and summarizing.
|
||||
- **Quality depends on thorough investigation**: A superficial answer would be insufficient; the user expects comprehensive, well-researched results.
|
||||
- **Independent execution is beneficial**: The task can run separately while freeing up the main conversation.
|
||||
|
||||
**How to identify async task scenarios:**
|
||||
Ask yourself: "Can I answer this well from my existing knowledge, or does this require actively gathering new information?"
|
||||
- If you need to search the web, read articles, or investigate → Use async task
|
||||
- If you can answer directly from knowledge → Just respond
|
||||
|
||||
Use \`execTask\` for a single task, \`execTasks\` for multiple parallel tasks.
|
||||
|
||||
**Example scenarios:**
|
||||
- User asks about best restaurants in a city → execTask (needs current info from reviews, searches)
|
||||
- User wants research on a topic → execTask (multi-step: search, read, analyze, summarize)
|
||||
- User asks to compare products/services → execTask (needs to gather data from multiple sources)
|
||||
- User asks a factual question you know → Just answer directly
|
||||
- User wants multiple independent analyses → execTasks (parallel execution)
|
||||
</when_to_use>
|
||||
|
||||
<best_practices>
|
||||
@@ -58,6 +82,57 @@ This "Plan-First" approach ensures:
|
||||
- **Track progress**: Use todo completion to measure plan progress
|
||||
</best_practices>
|
||||
|
||||
<todo_granularity>
|
||||
**IMPORTANT: Keep todos focused on major stages, not detailed sub-tasks.**
|
||||
|
||||
- **Limit to 5-10 items**: A todo list should contain around 5-10 major milestones or stages, not 20+ detailed tasks.
|
||||
- **Think in phases**: Group related tasks into higher-level stages (e.g., "Plan itinerary" instead of listing every city separately).
|
||||
- **Use hierarchical numbering** when more detail is needed: Use "1.", "2.", "2.1", "2.2", "3." format to show parent-child relationships.
|
||||
|
||||
**Good example** (Japan trip - 7 items, stage-focused):
|
||||
- 1. Determine travel dates and duration
|
||||
- 2. Handle visa and documentation
|
||||
- 3. Book flights and accommodation
|
||||
- 4. Plan city itineraries
|
||||
- 5. Arrange local transportation
|
||||
- 6. Prepare for departure
|
||||
- 7. Final confirmation before trip
|
||||
|
||||
**Bad example** (20+ detailed items):
|
||||
- Book Tokyo hotel
|
||||
- Book Kyoto hotel
|
||||
- Book Osaka hotel
|
||||
- Buy Suica card
|
||||
- Download Google Maps
|
||||
- Download translation app
|
||||
- ... (too granular!)
|
||||
|
||||
**When user needs more detail**, use hierarchical numbering:
|
||||
- 1. Determine travel dates
|
||||
- 2. Plan itinerary
|
||||
- 2.1 Tokyo attractions (3 days)
|
||||
- 2.2 Kyoto attractions (2 days)
|
||||
- 2.3 Osaka attractions (2 days)
|
||||
- 3. Handle bookings
|
||||
- 3.1 Flights
|
||||
- 3.2 Hotels
|
||||
- 3.3 JR Pass
|
||||
- 4. Departure preparation
|
||||
</todo_granularity>
|
||||
|
||||
<plan_stability>
|
||||
**IMPORTANT: Plans should remain stable once created. Each conversation has only ONE plan.**
|
||||
|
||||
- **Do NOT update plans** when details change (dates, locations, preferences). Instead, update the todos to reflect new information.
|
||||
- **Only use updatePlan** when the user's goal fundamentally changes (e.g., destination changes from Japan to Korea).
|
||||
- When user provides more specific information (like exact dates or preferences), **update or add todos** - not the plan.
|
||||
|
||||
Example:
|
||||
- User: "Plan a trip to Japan" → Create plan with goal "Japan Trip"
|
||||
- User: "I want to go in February" → Update todos to include February-specific tasks, NOT update the plan
|
||||
- User: "Actually I want to go to Korea instead" → Use updatePlan to change the goal to "Korea Trip" (fundamental goal change)
|
||||
</plan_stability>
|
||||
|
||||
<response_format>
|
||||
When working with GTD tools:
|
||||
- Confirm actions: "Created plan: [goal]" or "Added [n] todo items"
|
||||
|
||||
@@ -24,6 +24,13 @@ export const GTDApiName = {
|
||||
/** Create new todo items */
|
||||
createTodos: 'createTodos',
|
||||
|
||||
// ==================== Async Tasks ====================
|
||||
/** Execute a single async task */
|
||||
execTask: 'execTask',
|
||||
|
||||
/** Execute one or more async tasks */
|
||||
execTasks: 'execTasks',
|
||||
|
||||
/** Remove todo items by indices */
|
||||
removeTodos: 'removeTodos',
|
||||
|
||||
@@ -229,3 +236,67 @@ export interface UpdatePlanState {
|
||||
/** The updated plan document */
|
||||
plan: Plan;
|
||||
}
|
||||
|
||||
// ==================== Async Tasks Types ====================
|
||||
|
||||
/**
|
||||
* Single task item for execution
|
||||
*/
|
||||
export interface ExecTaskItem {
|
||||
/** Brief description of what this task does (shown in UI) */
|
||||
description: string;
|
||||
/** Whether to inherit context messages from parent conversation */
|
||||
inheritMessages?: boolean;
|
||||
/** Detailed instruction/prompt for the task execution */
|
||||
instruction: string;
|
||||
/** Timeout in milliseconds (optional, default 30 minutes) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for execTask API
|
||||
* Execute a single async task
|
||||
*/
|
||||
export interface ExecTaskParams {
|
||||
/** Brief description of what this task does (shown in UI) */
|
||||
description: string;
|
||||
/** Whether to inherit context messages from parent conversation */
|
||||
inheritMessages?: boolean;
|
||||
/** Detailed instruction/prompt for the task execution */
|
||||
instruction: string;
|
||||
/** Timeout in milliseconds (optional, default 30 minutes) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for execTasks API
|
||||
* Execute one or more async tasks
|
||||
*/
|
||||
export interface ExecTasksParams {
|
||||
/** Array of tasks to execute */
|
||||
tasks: ExecTaskItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* State returned after triggering exec_task
|
||||
*/
|
||||
export interface ExecTaskState {
|
||||
/** Parent message ID (tool message) */
|
||||
parentMessageId: string;
|
||||
/** The task definition that was triggered */
|
||||
task: ExecTaskItem;
|
||||
/** Type identifier for render component */
|
||||
type: 'execTask';
|
||||
}
|
||||
|
||||
/**
|
||||
* State returned after triggering exec_tasks
|
||||
*/
|
||||
export interface ExecTasksState {
|
||||
/** Parent message ID (tool message) */
|
||||
parentMessageId: string;
|
||||
/** Array of task definitions that were triggered */
|
||||
tasks: ExecTaskItem[];
|
||||
/** Type identifier for render component */
|
||||
type: 'execTasks';
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { memo, useEffect, useState } from 'react';
|
||||
import Actions from '@/features/Conversation/Messages/AssistantGroup/Tool/Actions';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
import { getBuiltinRender } from '@/tools/renders';
|
||||
import { getBuiltinStreaming } from '@/tools/streamings';
|
||||
|
||||
import Inspectors from './Inspector';
|
||||
@@ -79,6 +80,10 @@ const Tool = memo<GroupToolProps>(
|
||||
const hasStreamingRenderer = !!getBuiltinStreaming(identifier, apiName);
|
||||
const forceShowStreamingRender = isArgumentsStreaming && hasStreamingRenderer;
|
||||
|
||||
// Check if tool has custom render - if not, disable expand
|
||||
// Custom render exists when: builtin render exists OR plugin type is not 'default'
|
||||
const hasCustomRender = !!getBuiltinRender(identifier, apiName) || (!!type && type !== 'default');
|
||||
|
||||
// Wrap handleExpand to prevent collapsing when alwaysExpand is set
|
||||
const wrappedHandleExpand = (expand?: boolean) => {
|
||||
// Block collapse action when alwaysExpand is set
|
||||
@@ -117,6 +122,7 @@ const Tool = memo<GroupToolProps>(
|
||||
showPluginRender={showPluginRender}
|
||||
/>
|
||||
}
|
||||
allowExpand={hasCustomRender}
|
||||
itemKey={id}
|
||||
paddingBlock={4}
|
||||
paddingInline={4}
|
||||
|
||||
@@ -60,6 +60,9 @@ export default {
|
||||
'builtins.lobe-gtd.apiName.createPlan': 'Create plan',
|
||||
'builtins.lobe-gtd.apiName.createPlan.result': 'Create plan: <goal>{{goal}}</goal>',
|
||||
'builtins.lobe-gtd.apiName.createTodos': 'Create todos',
|
||||
'builtins.lobe-gtd.apiName.execTask': 'Execute task',
|
||||
'builtins.lobe-gtd.apiName.execTask.result': 'Execute: <desc>{{description}}</desc>',
|
||||
'builtins.lobe-gtd.apiName.execTasks': 'Execute tasks',
|
||||
'builtins.lobe-gtd.apiName.removeTodos': 'Delete todos',
|
||||
'builtins.lobe-gtd.apiName.updatePlan': 'Update plan',
|
||||
'builtins.lobe-gtd.apiName.updatePlan.completed': 'Completed',
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
import type { AgentRuntimeContext, TaskResultPayload } from '@lobechat/agent-runtime';
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { aiAgentService } from '@/services/aiAgent';
|
||||
|
||||
import { createExecTaskInstruction } from './fixtures';
|
||||
import { createMockStore } from './fixtures/mockStore';
|
||||
import { createInitialState, createTestContext, executeWithMockContext } from './helpers';
|
||||
|
||||
// Mock aiAgentService
|
||||
vi.mock('@/services/aiAgent', () => ({
|
||||
aiAgentService: {
|
||||
execSubAgentTask: vi.fn(),
|
||||
getSubAgentTaskStatus: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper to get typed mocks
|
||||
const mockExecSubAgentTask = aiAgentService.execSubAgentTask as Mock;
|
||||
const mockGetSubAgentTaskStatus = aiAgentService.getSubAgentTaskStatus as Mock;
|
||||
|
||||
describe('exec_task executor', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Basic Behavior', () => {
|
||||
it('should execute single task successfully', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction(
|
||||
{ description: 'Test task', instruction: 'Do something' },
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Mock task message creation
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
// Mock task execution
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
// Mock task status polling - completed on first poll
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Task completed successfully',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
expect((result.nextContext as AgentRuntimeContext).phase).toBe('task_result');
|
||||
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result).toBeDefined();
|
||||
expect(payload.result.success).toBe(true);
|
||||
expect(payload.result.threadId).toBe('thread_1');
|
||||
expect(payload.result.taskMessageId).toBe('task_msg_1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should return error when no context available', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext({ agentId: undefined, topicId: null });
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Override operation context to have no agentId/topicId
|
||||
mockStore.operations[context.operationId] = {
|
||||
abortController: new AbortController(),
|
||||
childOperationIds: [],
|
||||
context: {
|
||||
agentId: undefined,
|
||||
topicId: undefined,
|
||||
},
|
||||
id: context.operationId,
|
||||
metadata: { startTime: Date.now() },
|
||||
status: 'running',
|
||||
type: 'execAgentRuntime',
|
||||
};
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(false);
|
||||
expect(payload.result.error).toBe('No valid context available');
|
||||
});
|
||||
|
||||
it('should handle task message creation failure', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Mock task message creation failure
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce(null);
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(false);
|
||||
expect(payload.result.error).toBe('Failed to create task message');
|
||||
});
|
||||
|
||||
it('should handle task creation API failure', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: '',
|
||||
error: 'API error',
|
||||
operationId: '',
|
||||
success: false,
|
||||
threadId: '',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(false);
|
||||
expect(payload.result.error).toBe('API error');
|
||||
expect(mockStore.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
||||
'task_msg_1',
|
||||
'Task creation failed: API error',
|
||||
undefined,
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle task execution failure', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
error: 'Execution error',
|
||||
status: 'failed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(false);
|
||||
expect(payload.result.error).toBe('Execution error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Status Polling', () => {
|
||||
it('should update task message with taskDetail when completed', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
// Return completed with taskDetail on first poll
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
taskDetail: { status: 'completed' },
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockStore.internal_dispatchMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'task_msg_1',
|
||||
type: 'updateMessage',
|
||||
value: { taskDetail: { status: 'completed' } },
|
||||
},
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle cancelled task status', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
// Use mockImplementationOnce to ensure fresh mock behavior
|
||||
mockGetSubAgentTaskStatus.mockImplementationOnce(async () => ({
|
||||
status: 'cancel',
|
||||
}));
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(false);
|
||||
expect(payload.result.error).toBe('Task was cancelled');
|
||||
expect(mockStore.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
||||
'task_msg_1',
|
||||
'Task was cancelled',
|
||||
undefined,
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Operation Cancellation', () => {
|
||||
it('should stop polling when operation is cancelled before poll', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
// Use same operationId for both context and state
|
||||
const operationId = 'test-op';
|
||||
const context = createTestContext({ operationId });
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ operationId });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
// Mock execSubAgentTask to mark operation as cancelled after it's called
|
||||
// This simulates cancellation happening right after task creation but before polling
|
||||
mockExecSubAgentTask.mockImplementation(async () => {
|
||||
// After task creation API is called, mark operation as cancelled
|
||||
// This simulates cancellation happening right after task creation
|
||||
mockStore.operations[operationId].status = 'cancelled';
|
||||
return {
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
};
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TaskResultPayload;
|
||||
expect(payload.result.success).toBe(false);
|
||||
expect(payload.result.error).toBe('Operation cancelled');
|
||||
// getSubAgentTaskStatus should not be called since operation was cancelled before poll
|
||||
expect(mockGetSubAgentTaskStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result Phase', () => {
|
||||
it('should return task_result phase with correct session info', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction(
|
||||
{ description: 'Test', instruction: 'Test instruction' },
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op', stepCount: 5 });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
const nextContext = result.nextContext as AgentRuntimeContext;
|
||||
expect(nextContext.phase).toBe('task_result');
|
||||
expect(nextContext.session?.stepCount).toBe(6);
|
||||
expect(nextContext.session?.status).toBe('running');
|
||||
|
||||
const payload = nextContext.payload as TaskResultPayload;
|
||||
expect(payload.parentMessageId).toBe('msg_parent');
|
||||
});
|
||||
|
||||
it('should update messages in newState from store', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTaskInstruction();
|
||||
const state = createInitialState({ messages: [], operationId: 'test-op' });
|
||||
|
||||
const updatedMessages = [{ content: 'test', id: 'msg_1', role: 'user' }];
|
||||
mockStore.dbMessagesMap[context.messageKey] = updatedMessages as any;
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.newState.messages).toEqual(updatedMessages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Message Creation', () => {
|
||||
it('should create task message with correct parameters', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext({ agentId: 'agent_1', topicId: 'topic_1' });
|
||||
const instruction = createExecTaskInstruction(
|
||||
{ description: 'Test task', instruction: 'Do something important' },
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_task',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
agentId: 'agent_1',
|
||||
content: '',
|
||||
metadata: { instruction: 'Do something important' },
|
||||
parentId: 'msg_parent',
|
||||
role: 'task',
|
||||
topicId: 'topic_1',
|
||||
},
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,598 @@
|
||||
import type { AgentRuntimeContext, TasksBatchResultPayload } from '@lobechat/agent-runtime';
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { aiAgentService } from '@/services/aiAgent';
|
||||
|
||||
import { createExecTasksInstruction } from './fixtures';
|
||||
import { createMockStore } from './fixtures/mockStore';
|
||||
import { createInitialState, createTestContext, executeWithMockContext } from './helpers';
|
||||
|
||||
// Mock aiAgentService
|
||||
vi.mock('@/services/aiAgent', () => ({
|
||||
aiAgentService: {
|
||||
execSubAgentTask: vi.fn(),
|
||||
getSubAgentTaskStatus: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper to get typed mocks
|
||||
const mockExecSubAgentTask = aiAgentService.execSubAgentTask as Mock;
|
||||
const mockGetSubAgentTaskStatus = aiAgentService.getSubAgentTaskStatus as Mock;
|
||||
|
||||
describe('exec_tasks executor', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Basic Behavior', () => {
|
||||
it('should execute single task successfully', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction(
|
||||
[{ description: 'Test task 1', instruction: 'Do something' }],
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Mock task message creation
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
// Mock task execution
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
// Mock task status polling - completed on first poll
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Task completed successfully',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
expect((result.nextContext as AgentRuntimeContext).phase).toBe('tasks_batch_result');
|
||||
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results).toHaveLength(1);
|
||||
expect(payload.results[0].success).toBe(true);
|
||||
expect(payload.results[0].threadId).toBe('thread_1');
|
||||
expect(payload.results[0].taskMessageId).toBe('task_msg_1');
|
||||
});
|
||||
|
||||
it('should execute multiple tasks in parallel', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction(
|
||||
[
|
||||
{ description: 'Task 1', instruction: 'Do task 1' },
|
||||
{ description: 'Task 2', instruction: 'Do task 2' },
|
||||
{ description: 'Task 3', instruction: 'Do task 3' },
|
||||
],
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Mock task message creation for each task
|
||||
(mockStore.optimisticCreateMessage as Mock)
|
||||
.mockResolvedValueOnce({ id: 'task_msg_1' })
|
||||
.mockResolvedValueOnce({ id: 'task_msg_2' })
|
||||
.mockResolvedValueOnce({ id: 'task_msg_3' });
|
||||
|
||||
// Mock task execution for each task
|
||||
mockExecSubAgentTask
|
||||
.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_2',
|
||||
operationId: 'op_2',
|
||||
success: true,
|
||||
threadId: 'thread_2',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_3',
|
||||
operationId: 'op_3',
|
||||
success: true,
|
||||
threadId: 'thread_3',
|
||||
});
|
||||
|
||||
// Mock task status polling - all completed
|
||||
mockGetSubAgentTaskStatus
|
||||
.mockResolvedValueOnce({ result: 'Result 1', status: 'completed' })
|
||||
.mockResolvedValueOnce({ result: 'Result 2', status: 'completed' })
|
||||
.mockResolvedValueOnce({ result: 'Result 3', status: 'completed' });
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results).toHaveLength(3);
|
||||
expect(payload.results.every((r) => r.success)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should return error when no context available', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext({ agentId: undefined, topicId: null });
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Override operation context to have no agentId/topicId
|
||||
mockStore.operations[context.operationId] = {
|
||||
abortController: new AbortController(),
|
||||
childOperationIds: [],
|
||||
context: {
|
||||
agentId: undefined,
|
||||
topicId: undefined,
|
||||
},
|
||||
id: context.operationId,
|
||||
metadata: { startTime: Date.now() },
|
||||
status: 'running',
|
||||
type: 'execAgentRuntime',
|
||||
};
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results).toHaveLength(1);
|
||||
expect(payload.results[0].success).toBe(false);
|
||||
expect(payload.results[0].error).toBe('No valid context available');
|
||||
});
|
||||
|
||||
it('should handle task message creation failure', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
// Mock task message creation failure
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce(null);
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results[0].success).toBe(false);
|
||||
expect(payload.results[0].error).toBe('Failed to create task message');
|
||||
});
|
||||
|
||||
it('should handle task creation API failure', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: '',
|
||||
error: 'API error',
|
||||
operationId: '',
|
||||
success: false,
|
||||
threadId: '',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results[0].success).toBe(false);
|
||||
expect(payload.results[0].error).toBe('API error');
|
||||
expect(mockStore.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
||||
'task_msg_1',
|
||||
'Task creation failed: API error',
|
||||
undefined,
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle task execution failure', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
error: 'Execution error',
|
||||
status: 'failed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results[0].success).toBe(false);
|
||||
expect(payload.results[0].error).toBe('Execution error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Status Polling', () => {
|
||||
it('should update task message with taskDetail when completed', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
// Return completed with taskDetail on first poll
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
taskDetail: { status: 'completed' },
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockStore.internal_dispatchMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'task_msg_1',
|
||||
type: 'updateMessage',
|
||||
value: { taskDetail: { status: 'completed' } },
|
||||
},
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results[0].success).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle cancelled task status', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
// Use mockImplementationOnce to ensure fresh mock behavior
|
||||
mockGetSubAgentTaskStatus.mockImplementationOnce(async () => ({
|
||||
status: 'cancel',
|
||||
}));
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results[0].success).toBe(false);
|
||||
expect(payload.results[0].error).toBe('Task was cancelled');
|
||||
expect(mockStore.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
||||
'task_msg_1',
|
||||
'Task was cancelled',
|
||||
undefined,
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Operation Cancellation', () => {
|
||||
it('should stop polling when operation is cancelled before poll', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
// Use same operationId for both context and state
|
||||
const operationId = 'test-op';
|
||||
const context = createTestContext({ operationId });
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ operationId });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
// Mock execSubAgentTask to mark operation as cancelled after it's called
|
||||
// This simulates cancellation happening right after task creation but before polling
|
||||
mockExecSubAgentTask.mockImplementation(async () => {
|
||||
// After task creation API is called, mark operation as cancelled
|
||||
// This simulates cancellation happening right after task creation
|
||||
// Note: state.operationId is used in the polling loop for cancellation check
|
||||
mockStore.operations[operationId].status = 'cancelled';
|
||||
return {
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
};
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results[0].success).toBe(false);
|
||||
expect(payload.results[0].error).toBe('Operation cancelled');
|
||||
// getSubAgentTaskStatus should not be called since operation was cancelled before poll
|
||||
expect(mockGetSubAgentTaskStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result Phase', () => {
|
||||
it('should return tasks_result phase with correct session info', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction(
|
||||
[{ description: 'Test', instruction: 'Test instruction' }],
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op', stepCount: 5 });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.nextContext).toBeDefined();
|
||||
const nextContext = result.nextContext as AgentRuntimeContext;
|
||||
expect(nextContext.phase).toBe('tasks_batch_result');
|
||||
expect(nextContext.session?.stepCount).toBe(6);
|
||||
expect(nextContext.session?.status).toBe('running');
|
||||
|
||||
const payload = nextContext.payload as TasksBatchResultPayload;
|
||||
expect(payload.parentMessageId).toBe('msg_parent');
|
||||
});
|
||||
|
||||
it('should update messages in newState from store', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction();
|
||||
const state = createInitialState({ messages: [], operationId: 'test-op' });
|
||||
|
||||
const updatedMessages = [{ content: 'test', id: 'msg_1', role: 'user' }];
|
||||
mockStore.dbMessagesMap[context.messageKey] = updatedMessages as any;
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.newState.messages).toEqual(updatedMessages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Message Creation', () => {
|
||||
it('should create task message with correct parameters', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext({ agentId: 'agent_1', topicId: 'topic_1' });
|
||||
const instruction = createExecTasksInstruction(
|
||||
[{ description: 'Test task', instruction: 'Do something important' }],
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock).mockResolvedValueOnce({ id: 'task_msg_1' });
|
||||
|
||||
mockExecSubAgentTask.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus.mockResolvedValueOnce({
|
||||
result: 'Done',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
// When
|
||||
await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
agentId: 'agent_1',
|
||||
content: '',
|
||||
metadata: { instruction: 'Do something important' },
|
||||
parentId: 'msg_parent',
|
||||
role: 'task',
|
||||
topicId: 'topic_1',
|
||||
},
|
||||
{ operationId: 'test-op' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed Results', () => {
|
||||
it('should handle mix of successful and failed tasks', async () => {
|
||||
// Given
|
||||
const mockStore = createMockStore();
|
||||
const context = createTestContext();
|
||||
const instruction = createExecTasksInstruction(
|
||||
[
|
||||
{ description: 'Task 1', instruction: 'Success task' },
|
||||
{ description: 'Task 2', instruction: 'Fail task' },
|
||||
],
|
||||
'msg_parent',
|
||||
);
|
||||
const state = createInitialState({ operationId: 'test-op' });
|
||||
|
||||
(mockStore.optimisticCreateMessage as Mock)
|
||||
.mockResolvedValueOnce({ id: 'task_msg_1' })
|
||||
.mockResolvedValueOnce({ id: 'task_msg_2' });
|
||||
|
||||
mockExecSubAgentTask
|
||||
.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_1',
|
||||
operationId: 'op_1',
|
||||
success: true,
|
||||
threadId: 'thread_1',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
assistantMessageId: 'asst_2',
|
||||
operationId: 'op_2',
|
||||
success: true,
|
||||
threadId: 'thread_2',
|
||||
});
|
||||
|
||||
mockGetSubAgentTaskStatus
|
||||
.mockResolvedValueOnce({ result: 'Success', status: 'completed' })
|
||||
.mockResolvedValueOnce({ error: 'Task failed', status: 'failed' });
|
||||
|
||||
// When
|
||||
const result = await executeWithMockContext({
|
||||
context,
|
||||
executor: 'exec_tasks',
|
||||
instruction,
|
||||
mockStore,
|
||||
state,
|
||||
});
|
||||
|
||||
// Then
|
||||
const payload = (result.nextContext as AgentRuntimeContext).payload as TasksBatchResultPayload;
|
||||
expect(payload.results).toHaveLength(2);
|
||||
expect(payload.results[0].success).toBe(true);
|
||||
expect(payload.results[1].success).toBe(false);
|
||||
expect(payload.results[1].error).toBe('Task failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,9 @@ import type {
|
||||
AgentInstruction,
|
||||
AgentInstructionCallLlm,
|
||||
AgentInstructionCallTool,
|
||||
AgentInstructionExecTask,
|
||||
AgentInstructionExecTasks,
|
||||
ExecTaskItem,
|
||||
GeneralAgentCallLLMInstructionPayload,
|
||||
GeneralAgentCallingToolInstructionPayload,
|
||||
} from '@lobechat/agent-runtime';
|
||||
@@ -124,3 +127,50 @@ export const createFinishInstruction = (
|
||||
type: 'finish',
|
||||
} as AgentInstruction;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a mock exec_task instruction (single task)
|
||||
*/
|
||||
export const createExecTaskInstruction = (
|
||||
task?: Partial<ExecTaskItem>,
|
||||
parentMessageId?: string,
|
||||
): AgentInstructionExecTask => {
|
||||
const defaultTask: ExecTaskItem = {
|
||||
description: 'Test task',
|
||||
instruction: 'Execute test task',
|
||||
...task,
|
||||
};
|
||||
|
||||
return {
|
||||
payload: {
|
||||
parentMessageId: parentMessageId || `msg_${nanoid()}`,
|
||||
task: defaultTask,
|
||||
},
|
||||
type: 'exec_task',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a mock exec_tasks instruction (multiple tasks)
|
||||
*/
|
||||
export const createExecTasksInstruction = (
|
||||
tasks: ExecTaskItem[] = [],
|
||||
parentMessageId?: string,
|
||||
): AgentInstructionExecTasks => {
|
||||
const defaultTasks: ExecTaskItem[] = tasks.length
|
||||
? tasks
|
||||
: [
|
||||
{
|
||||
description: 'Test task',
|
||||
instruction: 'Execute test task',
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
payload: {
|
||||
parentMessageId: parentMessageId || `msg_${nanoid()}`,
|
||||
tasks: defaultTasks,
|
||||
},
|
||||
type: 'exec_tasks',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,6 +56,8 @@ export const createMockStore = (overrides: Partial<ChatStore> = {}): ChatStore =
|
||||
}),
|
||||
|
||||
// AI chat methods
|
||||
internal_dispatchMessage: vi.fn(),
|
||||
|
||||
internal_fetchAIChatMessage: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
internal_invokeDifferentTypePlugin: vi.fn().mockResolvedValue({ error: null }),
|
||||
|
||||
@@ -3,12 +3,16 @@ import {
|
||||
type AgentInstruction,
|
||||
type AgentInstructionCallLlm,
|
||||
type AgentInstructionCallTool,
|
||||
type AgentInstructionExecTask,
|
||||
type AgentInstructionExecTasks,
|
||||
type AgentRuntimeContext,
|
||||
type GeneralAgentCallLLMInstructionPayload,
|
||||
type GeneralAgentCallLLMResultPayload,
|
||||
type GeneralAgentCallToolResultPayload,
|
||||
type GeneralAgentCallingToolInstructionPayload,
|
||||
type InstructionExecutor,
|
||||
type TaskResultPayload,
|
||||
type TasksBatchResultPayload,
|
||||
UsageCounter,
|
||||
} from '@lobechat/agent-runtime';
|
||||
import type { ChatToolPayload, CreateMessageParams } from '@lobechat/types';
|
||||
@@ -16,7 +20,9 @@ import debug from 'debug';
|
||||
import pMap from 'p-map';
|
||||
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
import { aiAgentService } from '@/services/aiAgent';
|
||||
import type { ChatStore } from '@/store/chat/store';
|
||||
import { sleep } from '@/utils/sleep';
|
||||
|
||||
const log = debug('lobe-store:agent-executors');
|
||||
|
||||
@@ -558,21 +564,58 @@ export const createAgentExecutors = (context: {
|
||||
toolCost.toFixed(4),
|
||||
);
|
||||
|
||||
// Check if tool wants to stop execution flow (e.g., group management tools)
|
||||
// Check if tool wants to stop execution flow
|
||||
if (result?.stop) {
|
||||
log(
|
||||
'[%s][call_tool] Tool returned stop=true, terminating execution. state: %O',
|
||||
sessionLogId,
|
||||
result.state,
|
||||
);
|
||||
log('[%s][call_tool] Tool returned stop=true, state: %O', sessionLogId, result.state);
|
||||
|
||||
// Mark state as done and return without nextContext to stop the runtime
|
||||
const stateType = result.state?.type;
|
||||
|
||||
// GTD async tasks need to be passed to Agent for exec_task/exec_tasks instruction
|
||||
if (stateType === 'execTask' || stateType === 'execTasks') {
|
||||
log(
|
||||
'[%s][call_tool] Detected %s state, passing to Agent for decision',
|
||||
sessionLogId,
|
||||
stateType,
|
||||
);
|
||||
|
||||
return {
|
||||
events,
|
||||
newState,
|
||||
nextContext: {
|
||||
payload: {
|
||||
data: result,
|
||||
executionTime,
|
||||
isSuccess,
|
||||
parentMessageId: toolMessageId,
|
||||
stop: true,
|
||||
toolCall: chatToolPayload,
|
||||
toolCallId: chatToolPayload.id,
|
||||
} as GeneralAgentCallToolResultPayload,
|
||||
phase: 'tool_result',
|
||||
session: {
|
||||
eventCount: events.length,
|
||||
messageCount: newState.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
stepUsage: {
|
||||
cost: toolCost,
|
||||
toolName,
|
||||
unitPrice: toolCost,
|
||||
usageCount: 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
// Other stop types (speak, delegate, broadcast, etc.) - stop execution immediately
|
||||
newState.status = 'done';
|
||||
|
||||
return {
|
||||
events,
|
||||
newState,
|
||||
nextContext: undefined, // No next context means execution stops
|
||||
nextContext: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -817,6 +860,625 @@ export const createAgentExecutors = (context: {
|
||||
|
||||
return { events, newState };
|
||||
},
|
||||
|
||||
/**
|
||||
* exec_task executor
|
||||
* Executes a single async task
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create a task message (role: 'task') as placeholder
|
||||
* 2. Call execSubAgentTask API (backend creates thread)
|
||||
* 3. Poll for task completion
|
||||
* 4. Update task message content with result on completion
|
||||
* 5. Return task_result phase with result
|
||||
*/
|
||||
exec_task: async (instruction, state) => {
|
||||
const { parentMessageId, task } = (instruction as AgentInstructionExecTask).payload;
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
const sessionLogId = `${state.operationId}:${state.stepCount}`;
|
||||
|
||||
log('[%s][exec_task] Starting execution of task: %s', sessionLogId, task.description);
|
||||
|
||||
// Get context from operation
|
||||
const opContext = getOperationContext();
|
||||
const { agentId, topicId } = opContext;
|
||||
|
||||
if (!agentId || !topicId) {
|
||||
log('[%s][exec_task] No valid context, cannot execute task', sessionLogId);
|
||||
return {
|
||||
events,
|
||||
newState: state,
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: 'No valid context available',
|
||||
success: false,
|
||||
taskMessageId: '',
|
||||
threadId: '',
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: state.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const taskLogId = `${sessionLogId}:task`;
|
||||
|
||||
try {
|
||||
// 1. Create task message as placeholder
|
||||
const taskMessageResult = await context.get().optimisticCreateMessage(
|
||||
{
|
||||
agentId,
|
||||
content: '',
|
||||
metadata: { instruction: task.instruction },
|
||||
parentId: parentMessageId,
|
||||
role: 'task',
|
||||
topicId,
|
||||
},
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
|
||||
if (!taskMessageResult) {
|
||||
log('[%s] Failed to create task message', taskLogId);
|
||||
return {
|
||||
events,
|
||||
newState: state,
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: 'Failed to create task message',
|
||||
success: false,
|
||||
taskMessageId: '',
|
||||
threadId: '',
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: state.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
const taskMessageId = taskMessageResult.id;
|
||||
log('[%s] Created task message: %s', taskLogId, taskMessageId);
|
||||
|
||||
// 2. Create task via backend API
|
||||
const createResult = await aiAgentService.execSubAgentTask({
|
||||
agentId,
|
||||
instruction: task.instruction,
|
||||
parentMessageId: taskMessageId,
|
||||
topicId,
|
||||
});
|
||||
|
||||
if (!createResult.success) {
|
||||
log('[%s] Failed to create task: %s', taskLogId, createResult.error);
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task creation failed: ${createResult.error}`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
return {
|
||||
events,
|
||||
newState: state,
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: createResult.error,
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: '',
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: state.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
log('[%s] Task created with threadId: %s', taskLogId, createResult.threadId);
|
||||
|
||||
// 3. Poll for task completion
|
||||
const pollInterval = 3000; // 3 seconds
|
||||
const maxWait = task.timeout || 1_800_000; // Default 30 minutes
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < maxWait) {
|
||||
// Check if operation has been cancelled
|
||||
const currentOperation = context.get().operations[state.operationId];
|
||||
if (currentOperation?.status === 'cancelled') {
|
||||
log('[%s] Operation cancelled, stopping polling', taskLogId);
|
||||
const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
||||
return {
|
||||
events,
|
||||
newState: { ...state, messages: updatedMessages },
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: 'Operation cancelled',
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: updatedMessages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
const status = await aiAgentService.getSubAgentTaskStatus({
|
||||
threadId: createResult.threadId,
|
||||
});
|
||||
|
||||
// Update taskDetail in message if available
|
||||
if (status.taskDetail) {
|
||||
context.get().internal_dispatchMessage(
|
||||
{
|
||||
id: taskMessageId,
|
||||
type: 'updateMessage',
|
||||
value: { taskDetail: status.taskDetail },
|
||||
},
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
log('[%s] Updated task message with taskDetail', taskLogId);
|
||||
}
|
||||
|
||||
if (status.status === 'completed') {
|
||||
log('[%s] Task completed successfully', taskLogId);
|
||||
if (status.result) {
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(taskMessageId, status.result, undefined, {
|
||||
operationId: state.operationId,
|
||||
});
|
||||
}
|
||||
const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
||||
return {
|
||||
events,
|
||||
newState: { ...state, messages: updatedMessages },
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
result: status.result,
|
||||
success: true,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: updatedMessages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
if (status.status === 'failed') {
|
||||
log('[%s] Task failed: %s', taskLogId, status.error);
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task failed: ${status.error}`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
||||
return {
|
||||
events,
|
||||
newState: { ...state, messages: updatedMessages },
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: status.error,
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: updatedMessages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
if (status.status === 'cancel') {
|
||||
log('[%s] Task was cancelled', taskLogId);
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(taskMessageId, 'Task was cancelled', undefined, {
|
||||
operationId: state.operationId,
|
||||
});
|
||||
const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
||||
return {
|
||||
events,
|
||||
newState: { ...state, messages: updatedMessages },
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: 'Task was cancelled',
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: updatedMessages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
// Still processing, wait and poll again
|
||||
await sleep(pollInterval);
|
||||
}
|
||||
|
||||
// Timeout reached
|
||||
log('[%s] Task timeout after %dms', taskLogId, maxWait);
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task timeout after ${maxWait}ms`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
|
||||
const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
||||
return {
|
||||
events,
|
||||
newState: { ...state, messages: updatedMessages },
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: `Task timeout after ${maxWait}ms`,
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: updatedMessages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
} catch (error) {
|
||||
log('[%s] Error executing task: %O', taskLogId, error);
|
||||
return {
|
||||
events,
|
||||
newState: state,
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
result: {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
success: false,
|
||||
taskMessageId: '',
|
||||
threadId: '',
|
||||
},
|
||||
} as TaskResultPayload,
|
||||
phase: 'task_result',
|
||||
session: {
|
||||
messageCount: state.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* exec_tasks executor
|
||||
* Executes one or more async tasks in parallel
|
||||
*
|
||||
* Flow:
|
||||
* 1. For each task, create a task message (role: 'task') as placeholder
|
||||
* 2. Call execSubAgentTask API (backend creates thread)
|
||||
* 3. Poll for task completion
|
||||
* 4. Update task message content with result on completion
|
||||
* 5. Return tasks_batch_result phase with all results
|
||||
*/
|
||||
exec_tasks: async (instruction, state) => {
|
||||
const { parentMessageId, tasks } = (instruction as AgentInstructionExecTasks).payload;
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
const sessionLogId = `${state.operationId}:${state.stepCount}`;
|
||||
|
||||
log('[%s][exec_tasks] Starting execution of %d tasks', sessionLogId, tasks.length);
|
||||
|
||||
// Get context from operation
|
||||
const opContext = getOperationContext();
|
||||
const { agentId, topicId } = opContext;
|
||||
|
||||
if (!agentId || !topicId) {
|
||||
log('[%s][exec_tasks] No valid context, cannot execute tasks', sessionLogId);
|
||||
return {
|
||||
events,
|
||||
newState: state,
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
results: tasks.map(() => ({
|
||||
error: 'No valid context available',
|
||||
success: false,
|
||||
taskMessageId: '',
|
||||
threadId: '',
|
||||
})),
|
||||
} as TasksBatchResultPayload,
|
||||
phase: 'tasks_batch_result',
|
||||
session: {
|
||||
messageCount: state.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
}
|
||||
|
||||
// Execute all tasks in parallel
|
||||
const results = await pMap(
|
||||
tasks,
|
||||
async (task, taskIndex) => {
|
||||
const taskLogId = `${sessionLogId}:task-${taskIndex}`;
|
||||
log('[%s] Starting task: %s', taskLogId, task.description);
|
||||
|
||||
try {
|
||||
// 1. Create task message as placeholder
|
||||
const taskMessageResult = await context.get().optimisticCreateMessage(
|
||||
{
|
||||
agentId,
|
||||
content: '',
|
||||
metadata: { instruction: task.instruction },
|
||||
parentId: parentMessageId,
|
||||
role: 'task',
|
||||
topicId,
|
||||
},
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
|
||||
if (!taskMessageResult) {
|
||||
log('[%s] Failed to create task message', taskLogId);
|
||||
return {
|
||||
error: 'Failed to create task message',
|
||||
success: false,
|
||||
taskMessageId: '',
|
||||
threadId: '',
|
||||
};
|
||||
}
|
||||
|
||||
const taskMessageId = taskMessageResult.id;
|
||||
log('[%s] Created task message: %s', taskLogId, taskMessageId);
|
||||
|
||||
// 2. Create task via backend API (no groupId for single agent mode)
|
||||
const createResult = await aiAgentService.execSubAgentTask({
|
||||
agentId,
|
||||
instruction: task.instruction,
|
||||
parentMessageId: taskMessageId,
|
||||
topicId,
|
||||
});
|
||||
|
||||
if (!createResult.success) {
|
||||
log('[%s] Failed to create task: %s', taskLogId, createResult.error);
|
||||
// Update task message with error
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task creation failed: ${createResult.error}`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
return {
|
||||
error: createResult.error,
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: '',
|
||||
};
|
||||
}
|
||||
|
||||
log('[%s] Task created with threadId: %s', taskLogId, createResult.threadId);
|
||||
|
||||
// 3. Poll for task completion
|
||||
const pollInterval = 3000; // 3 seconds
|
||||
const maxWait = task.timeout || 1_800_000; // Default 30 minutes
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < maxWait) {
|
||||
// Check if operation has been cancelled
|
||||
const currentOperation = context.get().operations[state.operationId];
|
||||
if (currentOperation?.status === 'cancelled') {
|
||||
log('[%s] Operation cancelled, stopping polling', taskLogId);
|
||||
return {
|
||||
error: 'Operation cancelled',
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
};
|
||||
}
|
||||
|
||||
const status = await aiAgentService.getSubAgentTaskStatus({
|
||||
threadId: createResult.threadId,
|
||||
});
|
||||
|
||||
// Update taskDetail in message if available
|
||||
if (status.taskDetail) {
|
||||
context.get().internal_dispatchMessage(
|
||||
{
|
||||
id: taskMessageId,
|
||||
type: 'updateMessage',
|
||||
value: { taskDetail: status.taskDetail },
|
||||
},
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
log('[%s] Updated task message with taskDetail', taskLogId);
|
||||
}
|
||||
|
||||
if (status.status === 'completed') {
|
||||
log('[%s] Task completed successfully', taskLogId);
|
||||
// 4. Update task message with result
|
||||
if (status.result) {
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(taskMessageId, status.result, undefined, {
|
||||
operationId: state.operationId,
|
||||
});
|
||||
}
|
||||
return {
|
||||
result: status.result,
|
||||
success: true,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
};
|
||||
}
|
||||
|
||||
if (status.status === 'failed') {
|
||||
log('[%s] Task failed: %s', taskLogId, status.error);
|
||||
// Update task message with error
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task failed: ${status.error}`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
return {
|
||||
error: status.error,
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
};
|
||||
}
|
||||
|
||||
if (status.status === 'cancel') {
|
||||
log('[%s] Task was cancelled', taskLogId);
|
||||
// Update task message with cancelled status
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(taskMessageId, 'Task was cancelled', undefined, {
|
||||
operationId: state.operationId,
|
||||
});
|
||||
return {
|
||||
error: 'Task was cancelled',
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
};
|
||||
}
|
||||
|
||||
// Still processing, wait and poll again
|
||||
await sleep(pollInterval);
|
||||
}
|
||||
|
||||
// Timeout reached
|
||||
log('[%s] Task timeout after %dms', taskLogId, maxWait);
|
||||
// Update task message with timeout error
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task timeout after ${maxWait}ms`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
|
||||
return {
|
||||
error: `Task timeout after ${maxWait}ms`,
|
||||
success: false,
|
||||
taskMessageId,
|
||||
threadId: createResult.threadId,
|
||||
};
|
||||
} catch (error) {
|
||||
log('[%s] Error executing task: %O', taskLogId, error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
success: false,
|
||||
taskMessageId: '',
|
||||
threadId: '',
|
||||
};
|
||||
}
|
||||
},
|
||||
{ concurrency: 5 }, // Limit concurrent tasks
|
||||
);
|
||||
|
||||
log('[%s][exec_tasks] All tasks completed, results: %O', sessionLogId, results);
|
||||
|
||||
// Get latest messages from store
|
||||
const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
||||
const newState = { ...state, messages: updatedMessages };
|
||||
|
||||
// Return tasks_batch_result phase
|
||||
return {
|
||||
events,
|
||||
newState,
|
||||
nextContext: {
|
||||
payload: {
|
||||
parentMessageId,
|
||||
results,
|
||||
} as TasksBatchResultPayload,
|
||||
phase: 'tasks_batch_result',
|
||||
session: {
|
||||
messageCount: newState.messages.length,
|
||||
sessionId: state.operationId,
|
||||
status: 'running',
|
||||
stepCount: state.stepCount + 1,
|
||||
},
|
||||
} as AgentRuntimeContext,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return executors;
|
||||
|
||||
@@ -78,12 +78,13 @@ export interface PluginTypesAction {
|
||||
* @param id - Tool message ID
|
||||
* @param payload - Tool call payload
|
||||
* @param stepContext - Optional step context with dynamic state like GTD todos
|
||||
* @returns The tool execution result (including stop flag for flow control)
|
||||
*/
|
||||
invokeBuiltinTool: (
|
||||
id: string,
|
||||
payload: ChatToolPayload,
|
||||
stepContext?: RuntimeStepContext,
|
||||
) => Promise<void>;
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Invoke Cloud Code Interpreter tool
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GTDManifest, GTDStreamings } from '@lobechat/builtin-tool-gtd/client';
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { type BuiltinStreaming } from '@lobechat/types';
|
||||
|
||||
@@ -15,6 +16,7 @@ import { LocalSystemStreamings } from './local-system/Streaming';
|
||||
*/
|
||||
const BuiltinToolStreamings: Record<string, Record<string, BuiltinStreaming>> = {
|
||||
[CodeInterpreterIdentifier]: CodeInterpreterStreamings as Record<string, BuiltinStreaming>,
|
||||
[GTDManifest.identifier]: GTDStreamings as Record<string, BuiltinStreaming>,
|
||||
[LocalSystemManifest.identifier]: LocalSystemStreamings as Record<string, BuiltinStreaming>,
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export default defineConfig({
|
||||
'@/utils/errorResponse': resolve(__dirname, './src/utils/errorResponse'),
|
||||
'@/utils/unzipFile': resolve(__dirname, './src/utils/unzipFile'),
|
||||
'@/utils/server': resolve(__dirname, './src/utils/server'),
|
||||
'@/utils/identifier': resolve(__dirname, './src/utils/identifier'),
|
||||
'@/utils/electron': resolve(__dirname, './src/utils/electron'),
|
||||
'@/utils/identifier': resolve(__dirname, './src/utils/identifier'),
|
||||
'@/utils': resolve(__dirname, './packages/utils/src'),
|
||||
|
||||
Reference in New Issue
Block a user