From 5a50ff8f9b42375c7a6905c662a04076bdc5728a Mon Sep 17 00:00:00 2001 From: DavdGao Date: Tue, 2 Jun 2026 21:28:25 +0800 Subject: [PATCH] feat(event): add custom event --- packages/agentscope/package.json | 5 + packages/agentscope/src/event/index.ts | 16 ++- packages/agentscope/src/state/index.ts | 23 ++++ packages/agentscope/src/tool/task.test.ts | 46 ++++---- packages/agentscope/src/tool/task.ts | 128 ++++++++-------------- packages/agentscope/tsup.config.ts | 1 + 6 files changed, 112 insertions(+), 107 deletions(-) create mode 100644 packages/agentscope/src/state/index.ts diff --git a/packages/agentscope/package.json b/packages/agentscope/package.json index 4ec957a..6b5618f 100644 --- a/packages/agentscope/package.json +++ b/packages/agentscope/package.json @@ -43,6 +43,11 @@ "types": "./dist/storage/index.d.ts", "import": "./dist/storage/index.mjs", "require": "./dist/storage/index.js" + }, + "./state": { + "types": "./dist/state/index.d.ts", + "import": "./dist/state/index.mjs", + "require": "./dist/state/index.js" } }, "scripts": { diff --git a/packages/agentscope/src/event/index.ts b/packages/agentscope/src/event/index.ts index 8538aee..bf7feab 100644 --- a/packages/agentscope/src/event/index.ts +++ b/packages/agentscope/src/event/index.ts @@ -36,6 +36,8 @@ export enum EventType { USER_CONFIRM_RESULT = 'USER_CONFIRM_RESULT', EXTERNAL_EXECUTION_RESULT = 'EXTERNAL_EXECUTION_RESULT', + + CUSTOM = 'CUSTOM', } export interface EventBase { @@ -217,6 +219,16 @@ export interface ExternalExecutionResultEvent extends EventBase { execution_results: ToolResultBlock[]; } +/** + * A custom event carrying an arbitrary name and payload. + * Mirrors the Python `agentscope.event.CustomEvent` model. + */ +export interface CustomEvent extends EventBase { + type: EventType.CUSTOM; + name: string; + value: Record; +} + export type AgentEvent = // The control events for the whole run | ReplyStartEvent @@ -245,4 +257,6 @@ export type AgentEvent = | ToolResultEndEvent // The events from the external execution or user confirmation | UserConfirmResultEvent - | ExternalExecutionResultEvent; + | ExternalExecutionResultEvent + // Custom events + | CustomEvent; diff --git a/packages/agentscope/src/state/index.ts b/packages/agentscope/src/state/index.ts new file mode 100644 index 0000000..0cb2fc8 --- /dev/null +++ b/packages/agentscope/src/state/index.ts @@ -0,0 +1,23 @@ +/** + * Task represents a unit of work tracked in an agent's task context. + * Mirrors the Python `agentscope.state.Task` model. + */ +export interface Task { + id: string; + subject: string; + description: string; + metadata: Record; + created_at: string; + state: 'pending' | 'in_progress' | 'completed'; + owner: string | null; + blocks: string[]; + blocked_by: string[]; +} + +/** + * TaskContext holds the list of tasks associated with an agent session. + * Mirrors the Python `agentscope.state.TaskContext` model. + */ +export interface TaskContext { + tasks: Task[]; +} diff --git a/packages/agentscope/src/tool/task.test.ts b/packages/agentscope/src/tool/task.test.ts index dd82bae..e70de62 100644 --- a/packages/agentscope/src/tool/task.test.ts +++ b/packages/agentscope/src/tool/task.test.ts @@ -22,13 +22,13 @@ describe('Task Tools', () => { }; describe('TaskCreate', () => { - it('creates a task with pending status', () => { + it('creates a task with pending state', () => { const response = taskCreate.call!({ subject: 'Fix bug', description: 'Fix the authentication bug', }) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('Task #1 created successfully'); + expect(result).toContain('Task 1 created successfully'); expect(result).toContain('Fix bug'); }); @@ -39,18 +39,17 @@ describe('Task Tools', () => { description: 'Second task', }) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('Task #2 created successfully'); + expect(result).toContain('Task 2 created successfully'); }); - it('supports optional activeForm and metadata', () => { + it('supports optional metadata', () => { const response = taskCreate.call!({ subject: 'Run tests', description: 'Execute test suite', - activeForm: 'Running tests', metadata: { priority: 'high' }, }) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('Task #1 created successfully'); + expect(result).toContain('Task 1 created successfully'); }); }); @@ -59,18 +58,18 @@ describe('Task Tools', () => { taskCreate.call!({ subject: 'Task 1', description: 'First task' }); }); - it('updates task status', () => { + it('updates task state', () => { const response = taskUpdate.call!({ taskId: '1', status: 'in_progress', }) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('Task #1 updated successfully'); + expect(result).toContain('Task 1 updated successfully'); // Verify the update by getting the task const getResponse = taskGet.call!({ taskId: '1' }) as ToolResponse; const getResult = getTextFromResponse(getResponse); - expect(getResult).toContain('Status: in_progress'); + expect(getResult).toContain('State: in_progress'); }); it('updates task subject and description', () => { @@ -182,7 +181,7 @@ describe('Task Tools', () => { status: 'deleted', }) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('Task #1 deleted successfully'); + expect(result).toContain('Task 1 deleted successfully'); // Verify task is gone expect(() => taskGet.call!({ taskId: '1' })).toThrow('Task not found: 1'); @@ -194,21 +193,18 @@ describe('Task Tools', () => { taskCreate.call!({ subject: 'Test task', description: 'Test description', - activeForm: 'Testing', metadata: { priority: 'high' }, }); const response = taskGet.call!({ taskId: '1' }) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('Task #1: Test task'); - expect(result).toContain('Status: pending'); + expect(result).toContain('Task 1: Test task'); + expect(result).toContain('State: pending'); expect(result).toContain('Description: Test description'); - expect(result).toContain('Active Form: Testing'); expect(result).toContain('priority'); expect(result).toContain('high'); expect(result).toContain('Created:'); - expect(result).toContain('Updated:'); }); it('throws on non-existent task', () => { @@ -220,7 +216,7 @@ describe('Task Tools', () => { it('returns empty message when no tasks exist', () => { const response = taskList.call!({}) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toBe('No active tasks found'); + expect(result).toBe('No tasks available.'); }); it('lists pending and in_progress tasks', () => { @@ -231,8 +227,8 @@ describe('Task Tools', () => { const response = taskList.call!({}) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('#1 [pending] Task 1'); - expect(result).toContain('#2 [in_progress] Task 2'); + expect(result).toContain('1 [pending] Task 1'); + expect(result).toContain('2 [in_progress] Task 2'); }); it('filters out completed tasks', () => { @@ -244,7 +240,7 @@ describe('Task Tools', () => { const result = getTextFromResponse(response); expect(result).not.toContain('Task 1'); - expect(result).toContain('#2 [pending] Task 2'); + expect(result).toContain('2 [pending] Task 2'); }); it('filters out deleted tasks', () => { @@ -256,7 +252,7 @@ describe('Task Tools', () => { const result = getTextFromResponse(response); expect(result).not.toContain('Task 1'); - expect(result).toContain('#2 [pending] Task 2'); + expect(result).toContain('2 [pending] Task 2'); }); it('shows blocked tasks with dependencies', () => { @@ -267,7 +263,7 @@ describe('Task Tools', () => { const response = taskList.call!({}) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toContain('#2 [pending] Task 2 (blocked by: #1)'); + expect(result).toContain('2 [pending] Task 2[blocked by 1]'); }); it('sorts tasks by ID', () => { @@ -279,9 +275,9 @@ describe('Task Tools', () => { const result = getTextFromResponse(response); const lines = result.split('\n'); - expect(lines[0]).toContain('#1'); - expect(lines[1]).toContain('#2'); - expect(lines[2]).toContain('#3'); + expect(lines[0]).toContain('1'); + expect(lines[1]).toContain('2'); + expect(lines[2]).toContain('3'); }); it('returns empty when all tasks are completed', () => { @@ -293,7 +289,7 @@ describe('Task Tools', () => { const response = taskList.call!({}) as ToolResponse; const result = getTextFromResponse(response); - expect(result).toBe('No active tasks found'); + expect(result).toBe('No tasks available.'); }); }); }); diff --git a/packages/agentscope/src/tool/task.ts b/packages/agentscope/src/tool/task.ts index efe738f..0117b9d 100644 --- a/packages/agentscope/src/tool/task.ts +++ b/packages/agentscope/src/tool/task.ts @@ -1,48 +1,40 @@ import { z } from 'zod'; import { createToolResponse, ToolResponse } from './response'; +import type { Task } from '../state'; -// Task type definitions -export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'deleted'; - -export interface Task { - id: string; - subject: string; - description: string; - status: TaskStatus; - activeForm?: string; - owner?: string; - metadata?: Record; - blocks: string[]; - blockedBy: string[]; - createdAt: string; - updatedAt: string; -} +export type { Task, TaskContext } from '../state'; + +/** Task state — mirrors Python's ``Literal["pending", "in_progress", "completed"]``. */ +export type TaskState = Task['state']; // Module-level storage const taskStore = new Map(); -let nextId = 1; /** - * Generate a unique task ID - * @returns A unique task ID as a string + * Generate the next sequential task ID based on existing tasks. + * @returns A unique task ID as a string (e.g. "1", "2", "3"). */ function generateId(): string { - return String(nextId++); + let maxNumeric = 0; + for (const [id] of taskStore) { + const n = Number(id); + if (!isNaN(n) && n > maxNumeric) maxNumeric = n; + } + return String(maxNumeric + 1); } /** - * Reset task store for testing purposes + * Reset task store for testing purposes. * @internal */ export function _resetTaskStore(): void { taskStore.clear(); - nextId = 1; } /** - * Tool for creating tasks - * @returns A Tool object for creating tasks + * Tool for creating tasks. + * @returns A Tool object for creating tasks. */ export function TaskCreate() { return { @@ -59,7 +51,7 @@ Use this tool proactively in these scenarios: - User explicitly requests todo list - When the user directly asks you to use the todo list - User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) -All tasks are created with status 'pending'.`, +All tasks are created with state 'pending'.`, inputSchema: z.object({ subject: z .string() @@ -71,12 +63,6 @@ All tasks are created with status 'pending'.`, .describe( 'Detailed description of what needs to be done, including context and acceptance criteria' ), - activeForm: z - .string() - .optional() - .describe( - 'Present continuous form shown in the spinner when the task is in_progress (e.g., "Fixing authentication bug"). If omitted, the spinner shows the subject instead.' - ), metadata: z .record(z.string(), z.unknown()) .optional() @@ -87,28 +73,24 @@ All tasks are created with status 'pending'.`, call({ subject, description, - activeForm, metadata, }: { subject: string; description: string; - activeForm?: string; metadata?: Record; }): ToolResponse { const id = generateId(); - const now = new Date().toISOString(); const task: Task = { id, subject, description, - status: 'pending', - activeForm, - metadata, + state: 'pending', + metadata: metadata ?? {}, + owner: null, blocks: [], - blockedBy: [], - createdAt: now, - updatedAt: now, + blocked_by: [], + created_at: new Date().toISOString(), }; taskStore.set(id, task); @@ -118,7 +100,7 @@ All tasks are created with status 'pending'.`, { id: crypto.randomUUID(), type: 'text', - text: `Task #${id} created successfully: ${subject}`, + text: `Task ${id} created successfully: ${subject}`, }, ], state: 'success', @@ -128,8 +110,8 @@ All tasks are created with status 'pending'.`, } /** - * Tool for updating tasks - * @returns A Tool object for updating tasks + * Tool for updating tasks. + * @returns A Tool object for updating tasks. */ export function TaskUpdate() { return { @@ -158,12 +140,6 @@ export function TaskUpdate() { .describe('New status for the task'), subject: z.string().optional().describe('New subject for the task'), description: z.string().optional().describe('New description for the task'), - activeForm: z - .string() - .optional() - .describe( - 'Present continuous form shown in spinner when in_progress (e.g., "Running tests")' - ), owner: z.string().optional().describe('New owner for the task'), metadata: z .record(z.string(), z.unknown()) @@ -179,17 +155,15 @@ export function TaskUpdate() { status, subject, description, - activeForm, owner, metadata, addBlocks, addBlockedBy, }: { taskId: string; - status?: TaskStatus; + status?: 'pending' | 'in_progress' | 'completed' | 'deleted'; subject?: string; description?: string; - activeForm?: string; owner?: string; metadata?: Record; addBlocks?: string[]; @@ -219,14 +193,10 @@ export function TaskUpdate() { // Update fields if (subject !== undefined) task.subject = subject; if (description !== undefined) task.description = description; - if (activeForm !== undefined) task.activeForm = activeForm; if (owner !== undefined) task.owner = owner; // Merge metadata if (metadata !== undefined) { - if (!task.metadata) { - task.metadata = {}; - } for (const [key, value] of Object.entries(metadata)) { if (value === null) { delete task.metadata[key]; @@ -241,11 +211,9 @@ export function TaskUpdate() { task.blocks = [...new Set([...task.blocks, ...addBlocks])]; } if (addBlockedBy) { - task.blockedBy = [...new Set([...task.blockedBy, ...addBlockedBy])]; + task.blocked_by = [...new Set([...task.blocked_by, ...addBlockedBy])]; } - task.updatedAt = new Date().toISOString(); - // Handle status change if (status !== undefined) { if (status === 'deleted') { @@ -255,13 +223,13 @@ export function TaskUpdate() { { id: crypto.randomUUID(), type: 'text', - text: `Task #${taskId} deleted successfully`, + text: `Task ${taskId} deleted successfully`, }, ], state: 'success', }); } - task.status = status; + task.state = status; } return createToolResponse({ @@ -269,7 +237,7 @@ export function TaskUpdate() { { id: crypto.randomUUID(), type: 'text', - text: `Task #${taskId} updated successfully`, + text: `Task ${taskId} updated successfully`, }, ], state: 'success', @@ -279,8 +247,8 @@ export function TaskUpdate() { } /** - * Tool for retrieving a single task - * @returns A Tool object for retrieving a task by ID + * Tool for retrieving a single task. + * @returns A Tool object for retrieving a task by ID. */ export function TaskGet() { return { @@ -303,27 +271,23 @@ export function TaskGet() { throw new Error(`Task not found: ${taskId}`); } - let text = `Task #${task.id}: ${task.subject}\n`; - text += `Status: ${task.status}\n`; + let text = `Task ${task.id}: ${task.subject}\n`; + text += `State: ${task.state}\n`; text += `Description: ${task.description}\n`; - if (task.activeForm) { - text += `Active Form: ${task.activeForm}\n`; - } if (task.owner) { text += `Owner: ${task.owner}\n`; } if (task.blocks.length > 0) { text += `Blocks: ${task.blocks.join(', ')}\n`; } - if (task.blockedBy.length > 0) { - text += `Blocked By: ${task.blockedBy.join(', ')}\n`; + if (task.blocked_by.length > 0) { + text += `Blocked By: ${task.blocked_by.join(', ')}\n`; } if (task.metadata && Object.keys(task.metadata).length > 0) { text += `Metadata: ${JSON.stringify(task.metadata, null, 2)}\n`; } - text += `Created: ${task.createdAt}\n`; - text += `Updated: ${task.updatedAt}`; + text += `Created: ${task.created_at}`; return createToolResponse({ content: [ @@ -340,8 +304,8 @@ export function TaskGet() { } /** - * Tool for listing all active tasks - * @returns A Tool object for listing all active tasks + * Tool for listing all active tasks. + * @returns A Tool object for listing all active tasks. */ export function TaskList() { return { @@ -358,9 +322,8 @@ export function TaskList() { requireUserConfirm: false, call(): ToolResponse { - // Filter to only pending and in_progress tasks const activeTasks = Array.from(taskStore.values()) - .filter(task => task.status === 'pending' || task.status === 'in_progress') + .filter(task => task.state === 'pending' || task.state === 'in_progress') .sort((a, b) => Number(a.id) - Number(b.id)); if (activeTasks.length === 0) { @@ -369,7 +332,7 @@ export function TaskList() { { id: crypto.randomUUID(), type: 'text', - text: 'No active tasks found', + text: 'No tasks available.', }, ], state: 'success', @@ -377,9 +340,12 @@ export function TaskList() { } const lines = activeTasks.map(task => { - let line = `#${task.id} [${task.status}] ${task.subject}`; - if (task.blockedBy.length > 0) { - line += ` (blocked by: #${task.blockedBy.join(', #')})`; + let line = `${task.id} [${task.state}] ${task.subject}`; + if (task.owner) { + line += `(${task.owner})`; + } + if (task.blocked_by.length > 0) { + line += `[blocked by ${task.blocked_by.join(', ')}]`; } return line; }); diff --git a/packages/agentscope/tsup.config.ts b/packages/agentscope/tsup.config.ts index f14ed70..79150fd 100644 --- a/packages/agentscope/tsup.config.ts +++ b/packages/agentscope/tsup.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ 'event/index': 'src/event/index.ts', 'mcp/index': 'src/mcp/index.ts', 'storage/index': 'src/storage/index.ts', + 'state/index': 'src/state/index.ts', }, format: ['cjs', 'esm'], dts: true,