refactor: clean CLI taxonomy — canonical subcommands, merged memory.update, no aliases

- calendar: split write → create/read/update/delete/share
- contacts: rename lookup → read
- memory: merge write+forget → update (unified action field in operations)
- Remove all alias/normalization logic from adapter and handlers
- Update tool_postprocessor ui_hints builders to canonical keys
- Remove frontend legacy TOOL_CALL_START/ARGS/END events and ToolCallItem
- Update SKILL.md files and protocol docs
- Update tests and settings screens
This commit is contained in:
qzl
2026-04-23 12:12:41 +08:00
parent 91077a933d
commit 19e273a9e6
48 changed files with 1578 additions and 811 deletions
@@ -10,11 +10,7 @@ String agUiEventLabel(AgUiEventType type) {
AgUiEventType.stepStarted => l10n.agUiEventStepStarted,
AgUiEventType.stepFinished => l10n.agUiEventStepFinished,
AgUiEventType.textMessageEnd => l10n.agUiEventTextMessageEnd,
AgUiEventType.toolCallStart => l10n.agUiEventToolCallStart,
AgUiEventType.toolCallArgs => l10n.agUiEventToolCallArgs,
AgUiEventType.toolCallEnd => l10n.agUiEventToolCallEnd,
AgUiEventType.toolCallResult => l10n.agUiEventToolCallResult,
AgUiEventType.toolCallError => l10n.agUiEventToolCallError,
AgUiEventType.unknown => l10n.agUiEventUnknown,
};
}
@@ -257,7 +257,17 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
bool _shouldRefreshCalendarForTool(ToolCallResultEvent event) {
final name = event.toolName.trim().toLowerCase();
final status = event.status.trim().toLowerCase();
if (name != 'calendar_write') {
if (name != 'project_cli') {
return false;
}
final args = event.toolCallArgs;
if (args == null) {
return false;
}
final command = (args['command'] as String?)?.trim().toLowerCase();
final subcommand = (args['subcommand'] as String?)?.trim().toLowerCase();
const mutationSubcommands = {'create', 'update', 'delete'};
if (command != 'calendar' || !mutationSubcommands.contains(subcommand)) {
return false;
}
return status == 'success' || status == 'partial';
@@ -24,24 +24,13 @@ extension _ChatBlocEvents on ChatBloc {
case AgUiEventType.runFinished:
_trackChatCompleted();
_clearRunMetrics();
emit(
_resetRunState().copyWith(items: _removeToolCallItems(state.items)),
);
emit(_resetRunState());
case AgUiEventType.runError:
final errorEvent = event as RunErrorEvent;
_clearRunMetrics();
final isCanceledByUser = errorEvent.code == 'RUN_CANCELED';
emit(
_resetRunState(
error: isCanceledByUser ? null : errorEvent.message,
).copyWith(
items: _markActiveToolCallsFailed(
state.items,
reason: isCanceledByUser
? L10n.current.chatRunCanceled
: L10n.current.chatRunFailed,
),
),
_resetRunState(error: isCanceledByUser ? null : errorEvent.message),
);
case AgUiEventType.stepStarted:
_handleStepStarted(event as StepStartedEvent);
@@ -49,12 +38,6 @@ extension _ChatBlocEvents on ChatBloc {
_handleStepFinished(event as StepFinishedEvent);
case AgUiEventType.textMessageEnd:
_handleTextMessageEnd(event as TextMessageEndEvent);
case AgUiEventType.toolCallStart:
_handleToolCallStart(event as ToolCallStartEvent);
case AgUiEventType.toolCallArgs:
_handleToolCallArgs(event as ToolCallArgsEvent);
case AgUiEventType.toolCallEnd:
_handleToolCallEnd(event as ToolCallEndEvent);
case AgUiEventType.toolCallResult:
_handleToolCallResult(event as ToolCallResultEvent);
case AgUiEventType.unknown:
@@ -84,12 +67,13 @@ extension _ChatBlocEvents on ChatBloc {
state.items,
event.messageId,
event.answer,
event.suggestedActions,
timestamp,
);
emit(
state.copyWith(
items: _removeToolCallItems(items),
items: items,
currentMessageId: null,
isWaitingFirstToken: false,
isStreaming: false,
@@ -101,6 +85,7 @@ extension _ChatBlocEvents on ChatBloc {
List<ChatListItem> items,
String messageId,
String content,
List<String> suggestedActions,
DateTime timestamp,
) {
final result = List<ChatListItem>.from(items);
@@ -110,7 +95,11 @@ extension _ChatBlocEvents on ChatBloc {
if (index >= 0) {
final existing = result[index] as TextMessageItem;
result[index] = existing.copyWith(content: content, isStreaming: false);
result[index] = existing.copyWith(
content: content,
isStreaming: false,
suggestedActions: suggestedActions,
);
return result;
}
@@ -121,73 +110,18 @@ extension _ChatBlocEvents on ChatBloc {
timestamp: timestamp,
sender: MessageSender.ai,
isStreaming: false,
suggestedActions: suggestedActions,
),
);
return result;
}
void _handleToolCallStart(ToolCallStartEvent event) {
final exists = state.items.any(
(item) => item is ToolCallItem && item.id == event.toolCallId,
);
if (exists) {
return;
}
emit(
state.copyWith(
items: [
...state.items,
ToolCallItem(
id: event.toolCallId,
callId: event.toolCallId,
toolName: event.toolCallName,
args: const {},
status: ToolCallStatus.pending,
timestamp: DateTime.now(),
sender: MessageSender.ai,
),
],
),
);
}
void _handleToolCallArgs(ToolCallArgsEvent event) {
emit(
state.copyWith(
items: state.items.map((item) {
if (item is ToolCallItem && item.id == event.toolCallId) {
return item.copyWith(args: event.args);
}
return item;
}).toList(),
),
);
}
void _handleToolCallEnd(ToolCallEndEvent event) {
emit(
state.copyWith(
items: state.items.map((item) {
if (item is ToolCallItem && item.id == event.toolCallId) {
return item.copyWith(status: ToolCallStatus.executing);
}
return item;
}).toList(),
),
);
}
void _handleToolCallResult(ToolCallResultEvent event) {
if (_shouldRefreshCalendarForTool(event)) {
unawaited(_refreshCalendarAfterToolMutation());
}
final timestamp = DateTime.now();
final items = state.items.map((item) {
if (item is ToolCallItem && item.id == event.toolCallId) {
return item.copyWith(status: ToolCallStatus.completed);
}
return item;
}).toList();
final items = List<ChatListItem>.from(state.items);
final uiSchema = event.uiSchema;
if (uiSchema != null) {
@@ -219,30 +153,12 @@ extension _ChatBlocEvents on ChatBloc {
items.add(uiItem);
}
List<ChatListItem> _removeToolCallItems(List<ChatListItem> items) {
return items.where((item) => item is! ToolCallItem).toList();
}
List<ChatListItem> _markActiveToolCallsFailed(
List<ChatListItem> items, {
required String reason,
}) {
return items.map((item) {
if (item is! ToolCallItem ||
item.status == ToolCallStatus.error ||
item.status == ToolCallStatus.completed) {
return item;
}
return item.copyWith(status: ToolCallStatus.error, errorMessage: reason);
}).toList();
}
List<ChatListItem> _convertHistoryMessages(List<HistoryMessage> messages) {
final converted = <ChatListItem>[];
for (final msg in messages) {
final normalizedRole = msg.role.toLowerCase();
final isUser = normalizedRole == 'user';
final isTool = normalizedRole == 'tool' || normalizedRole == 'tools';
final isTool = normalizedRole == 'tool';
final sender = isUser ? MessageSender.user : MessageSender.ai;
final attachments = msg.attachments
.map(
@@ -262,11 +178,12 @@ extension _ChatBlocEvents on ChatBloc {
sender: sender,
isLocalEcho: false,
attachments: attachments,
suggestedActions: msg.suggestedActions,
),
);
}
if (!isTool && msg.uiSchema != null) {
if (isTool && msg.uiSchema != null) {
converted.add(
ToolResultItem(
id: '${msg.id}-ui',