From bcfa7a684b65810ba118f8664a0be17e9f058259 Mon Sep 17 00:00:00 2001 From: Weirui Kuang <39145382+rayrayraykk@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:37:20 +0800 Subject: [PATCH] fix(message): preserve event timestamps and validate rules --- .../renderer/src/hooks/agent-event-handler.ts | 6 +++- .../src/message/append-event.test.ts | 3 -- .../agentscope/src/message/message.test.ts | 28 ++++++++++++++++++- packages/agentscope/src/message/message.ts | 28 ++++++++++++++----- packages/agentscope/src/message/schema.ts | 12 ++++++-- 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/renderer/src/hooks/agent-event-handler.ts b/apps/desktop/src/renderer/src/hooks/agent-event-handler.ts index a0cf3ce..8d58abf 100644 --- a/apps/desktop/src/renderer/src/hooks/agent-event-handler.ts +++ b/apps/desktop/src/renderer/src/hooks/agent-event-handler.ts @@ -55,7 +55,11 @@ export function applyAgentEvent( setMessages(prev => prev.map(m => { if (m.id !== event.reply_id) return m; - const cloned: Msg = { ...m, content: m.content.map(b => ({ ...b })) }; + const cloned: Msg = { + ...m, + content: m.content.map(b => ({ ...b })), + usage: m.usage ? { ...m.usage } : null, + }; appendEvent(cloned, event); return cloned; }) diff --git a/packages/agentscope/src/message/append-event.test.ts b/packages/agentscope/src/message/append-event.test.ts index 78ec5f1..d052bbc 100644 --- a/packages/agentscope/src/message/append-event.test.ts +++ b/packages/agentscope/src/message/append-event.test.ts @@ -885,7 +885,6 @@ describe('appendEvent', () => { // Apply all events and check ground truths expect(events.length).toBe(groundTruths.length); for (let i = 0; i < events.length; i++) { - jest.setSystemTime(new Date(events[i].created_at)); appendEvent(msg, events[i]); expect(msgDump(msg)).toEqual(groundTruths[i]); } @@ -946,7 +945,6 @@ describe('appendEvent', () => { // String hint with source — created_at and finished_at both come // from the one-shot event's timestamp. - jest.setSystemTime(new Date(ts(1))); appendEvent(msg, { id: 'e1', created_at: ts(1), @@ -977,7 +975,6 @@ describe('appendEvent', () => { created_at: ts(2), }, ]; - jest.setSystemTime(new Date(ts(2))); appendEvent(msg, { id: 'e2', created_at: ts(2), diff --git a/packages/agentscope/src/message/message.test.ts b/packages/agentscope/src/message/message.test.ts index 00ca889..d86587f 100644 --- a/packages/agentscope/src/message/message.test.ts +++ b/packages/agentscope/src/message/message.test.ts @@ -1,6 +1,6 @@ import { ErrorType } from '../type'; import { createMsg, getContentBlocks, getTextContent, hasContentBlocks } from './message'; -import { parseMsg } from './schema'; +import { parseContentBlock, parseMsg } from './schema'; const TS = '2024-01-01T00:00:00.000Z'; @@ -136,4 +136,30 @@ describe('Message', () => { error: { type: ErrorType.UNKNOWN, message: 'failed' }, }); }); + + test('validate permission rules in serialized tool calls', () => { + const toolCall = { + type: 'tool_call', + id: 'call-1', + name: 'Bash', + input: '{}', + suggested_rules: [ + { + tool_name: 'Bash', + rule_content: null, + behavior: 'ask', + source: 'userSettings', + }, + ], + }; + + expect(parseContentBlock(toolCall)).toMatchObject(toolCall); + expect(() => + parseContentBlock({ + ...toolCall, + suggested_rules: [{ ...toolCall.suggested_rules[0], behavior: 'invalid' }], + }) + ).toThrow(); + expect(() => parseContentBlock({ ...toolCall, suggested_rules: [42] })).toThrow(); + }); }); diff --git a/packages/agentscope/src/message/message.ts b/packages/agentscope/src/message/message.ts index 059c395..3228172 100644 --- a/packages/agentscope/src/message/message.ts +++ b/packages/agentscope/src/message/message.ts @@ -396,7 +396,9 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { break; case EventType.TEXT_BLOCK_START: - msg.content.push(TextBlock({ id: event.block_id, text: '' })); + msg.content.push( + TextBlock({ id: event.block_id, text: '', created_at: event.created_at }) + ); break; case EventType.TEXT_BLOCK_DELTA: { @@ -420,7 +422,9 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { } case EventType.THINKING_BLOCK_START: - msg.content.push(ThinkingBlock({ id: event.block_id, thinking: '' })); + msg.content.push( + ThinkingBlock({ id: event.block_id, thinking: '', created_at: event.created_at }) + ); break; case EventType.THINKING_BLOCK_DELTA: { @@ -450,8 +454,9 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { id: event.block_id, hint: event.hint, source: event.source ?? null, + created_at: event.created_at, + finished_at: event.created_at, }); - hintBlock.finished_at = hintBlock.created_at; msg.content.push(hintBlock); break; } @@ -461,6 +466,7 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { DataBlock({ id: event.block_id, source: Base64Source({ data: '', media_type: event.media_type }), + created_at: event.created_at, }) ); break; @@ -501,6 +507,7 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { id: event.tool_call_id, name: event.tool_call_name, input: '', + created_at: event.created_at, }) ); break; @@ -531,6 +538,7 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { id: event.tool_call_id, name: event.tool_call_name, output: [], + created_at: event.created_at, }) ); break; @@ -542,11 +550,11 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { } else { const trb = block as ToolResultBlock; if (typeof trb.output === 'string') { - trb.output = [TextBlock({ text: trb.output })]; + trb.output = [TextBlock({ text: trb.output, created_at: event.created_at })]; } const last = trb.output[trb.output.length - 1]; if (!last || last.type !== 'text') { - trb.output.push(TextBlock({ text: event.delta })); + trb.output.push(TextBlock({ text: event.delta, created_at: event.created_at })); } else { (last as TextBlock).text += event.delta; } @@ -561,13 +569,19 @@ export function appendEvent(msg: Msg, event: AgentEvent): Msg { } else { const trb = block as ToolResultBlock; if (typeof trb.output === 'string') { - trb.output = [TextBlock({ text: trb.output })]; + trb.output = [TextBlock({ text: trb.output, created_at: event.created_at })]; } const source: Base64Source | URLSource = event.data != null ? Base64Source({ data: event.data, media_type: event.media_type }) : URLSource({ url: event.url!, media_type: event.media_type }); - trb.output.push(DataBlock({ id: event.block_id ?? _generateId(), source })); + trb.output.push( + DataBlock({ + id: event.block_id ?? _generateId(), + source, + created_at: event.created_at, + }) + ); } break; } diff --git a/packages/agentscope/src/message/schema.ts b/packages/agentscope/src/message/schema.ts index 32a4d90..2afe399 100644 --- a/packages/agentscope/src/message/schema.ts +++ b/packages/agentscope/src/message/schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; +import { PermissionBehavior } from '../permission'; import { ErrorType, ReplyFinishedReason } from '../type'; import { Base64Source, @@ -23,6 +24,13 @@ const entityFields = { const jsonRecordSchema = z.record(z.string(), z.json()); +const permissionRuleSchema = z.object({ + tool_name: z.string(), + rule_content: z.string().nullable(), + behavior: z.nativeEnum(PermissionBehavior), + source: z.string(), +}); + /** Runtime schema for Python-compatible text blocks. */ export const TextBlockSchema = z .object({ @@ -95,11 +103,11 @@ export const ToolCallBlockSchema = z name: z.string(), input: z.string(), state: z.enum(['pending', 'asking', 'allowed', 'submitted', 'finished']).optional(), - suggested_rules: z.array(z.unknown()).optional(), + suggested_rules: z.array(permissionRuleSchema).optional(), created_at: z.string().optional(), finished_at: z.string().nullable().optional(), }) - .transform(value => ToolCallBlock(value as Parameters[0])); + .transform(value => ToolCallBlock(value)); /** Runtime schema for tool-result blocks. */ export const ToolResultBlockSchema = z