mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
feat: handling platform event mcp notification for acp (#10038)
This commit is contained in:
parent
98a13fff4b
commit
e2a2a64d3c
4 changed files with 169 additions and 17 deletions
|
|
@ -13,6 +13,9 @@ enum ToolNotification {
|
|||
Progress {
|
||||
params: ProgressNotificationParam,
|
||||
},
|
||||
PlatformEvent {
|
||||
params: serde_json::Value,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) fn tool_notification_update(
|
||||
|
|
@ -26,6 +29,13 @@ pub(super) fn tool_notification_update(
|
|||
ServerNotification::ProgressNotification(notification) => ToolNotification::Progress {
|
||||
params: notification.params,
|
||||
},
|
||||
ServerNotification::CustomNotification(notification)
|
||||
if notification.method == "platform_event" =>
|
||||
{
|
||||
ToolNotification::PlatformEvent {
|
||||
params: notification.params.unwrap_or(serde_json::Value::Null),
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
|
@ -48,8 +58,9 @@ pub(super) fn tool_notification_update(
|
|||
mod tests {
|
||||
use super::tool_notification_update;
|
||||
use rmcp::model::{
|
||||
CancelledNotificationParam, LoggingLevel, LoggingMessageNotificationParam, Notification,
|
||||
NumberOrString, ProgressNotificationParam, ProgressToken, ServerNotification,
|
||||
CancelledNotificationParam, CustomNotification, LoggingLevel,
|
||||
LoggingMessageNotificationParam, Notification, NumberOrString, ProgressNotificationParam,
|
||||
ProgressToken, ServerNotification,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -135,4 +146,46 @@ mod tests {
|
|||
|
||||
assert!(tool_notification_update("tool_1", notification).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_platform_event_custom_notification_to_tool_update_meta() {
|
||||
let notification = ServerNotification::CustomNotification(CustomNotification::new(
|
||||
"platform_event",
|
||||
Some(json!({
|
||||
"extension": "apps",
|
||||
"event_type": "app_created",
|
||||
"app_name": "platform-event-repro"
|
||||
})),
|
||||
));
|
||||
|
||||
let update = tool_notification_update("tool_1", notification).expect("expected update");
|
||||
let value = serde_json::to_value(update).expect("update should serialize");
|
||||
|
||||
assert_eq!(value["sessionUpdate"], "tool_call_update");
|
||||
assert_eq!(value["toolCallId"], "tool_1");
|
||||
assert_eq!(value["status"], "in_progress");
|
||||
assert_eq!(value["_meta"]["toolNotification"]["type"], "platform_event");
|
||||
assert_eq!(
|
||||
value["_meta"]["toolNotification"]["params"]["extension"],
|
||||
"apps"
|
||||
);
|
||||
assert_eq!(
|
||||
value["_meta"]["toolNotification"]["params"]["event_type"],
|
||||
"app_created"
|
||||
);
|
||||
assert_eq!(
|
||||
value["_meta"]["toolNotification"]["params"]["app_name"],
|
||||
"platform-event-repro"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_platform_event_custom_notifications() {
|
||||
let notification = ServerNotification::CustomNotification(CustomNotification::new(
|
||||
"notifications/custom",
|
||||
Some(json!({ "extension": "apps" })),
|
||||
));
|
||||
|
||||
assert!(tool_notification_update("tool_1", notification).is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
import type { Session } from '../../api';
|
||||
import { AppEvents } from '../../constants/events';
|
||||
import { ChatState } from '../../types/chatState';
|
||||
import { maybeHandlePlatformEvent } from '../../utils/platform_events';
|
||||
import { handleAcpSessionNotification } from '../chatNotifications';
|
||||
import type { AcpChatSessionSnapshot } from '../chatSessionStore';
|
||||
import { acpChatSessionActions, acpChatSessionStore } from '../chatSessionStore';
|
||||
|
|
@ -21,6 +22,10 @@ vi.mock('../chatSessionStore', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/platform_events', () => ({
|
||||
maybeHandlePlatformEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
const SESSION_ID = 'session-1';
|
||||
|
||||
function sessionInfoUpdate(title: string): SessionNotification {
|
||||
|
|
@ -33,6 +38,27 @@ function sessionInfoUpdate(title: string): SessionNotification {
|
|||
};
|
||||
}
|
||||
|
||||
function platformEventToolUpdate(status: 'in_progress' | 'completed'): SessionNotification {
|
||||
return {
|
||||
sessionId: SESSION_ID,
|
||||
update: {
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId: 'tool-1',
|
||||
status,
|
||||
_meta: {
|
||||
toolNotification: {
|
||||
type: 'platform_event',
|
||||
params: {
|
||||
extension: 'apps',
|
||||
event_type: 'app_created',
|
||||
app_name: 'platform-event-repro',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sessionWithName(name: string): Session {
|
||||
return {
|
||||
id: SESSION_ID,
|
||||
|
|
@ -138,4 +164,26 @@ describe('handleAcpSessionNotification', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards live ACP platform events to the desktop platform event handler', async () => {
|
||||
await handleAcpSessionNotification(platformEventToolUpdate('in_progress'));
|
||||
|
||||
expect(maybeHandlePlatformEvent).toHaveBeenCalledWith(
|
||||
{
|
||||
method: 'platform_event',
|
||||
params: {
|
||||
extension: 'apps',
|
||||
event_type: 'app_created',
|
||||
app_name: 'platform-event-repro',
|
||||
},
|
||||
},
|
||||
SESSION_ID
|
||||
);
|
||||
});
|
||||
|
||||
it('does not forward completed platform event metadata as a live desktop event', async () => {
|
||||
await handleAcpSessionNotification(platformEventToolUpdate('completed'));
|
||||
|
||||
expect(maybeHandlePlatformEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ import type { AcpChatStateChange } from './shared';
|
|||
import { isRecord } from './shared';
|
||||
|
||||
type ToolNotification =
|
||||
| {
|
||||
type: 'message';
|
||||
params: LoggingMessageNotificationParams;
|
||||
}
|
||||
| {
|
||||
type: 'progress';
|
||||
params: ProgressNotificationParams;
|
||||
};
|
||||
| {
|
||||
type: 'message';
|
||||
params: LoggingMessageNotificationParams;
|
||||
}
|
||||
| {
|
||||
type: 'progress';
|
||||
params: ProgressNotificationParams;
|
||||
}
|
||||
| {
|
||||
type: 'platform_event';
|
||||
params: PlatformEventParams;
|
||||
};
|
||||
|
||||
type LoggingMessageNotificationParams = {
|
||||
level: string;
|
||||
|
|
@ -26,20 +30,31 @@ type ProgressNotificationParams = {
|
|||
message?: string;
|
||||
};
|
||||
|
||||
type PlatformEventParams = Record<string, unknown>;
|
||||
|
||||
export function toolNotificationChange(
|
||||
update: ToolCallUpdate
|
||||
): Extract<AcpChatStateChange, { type: 'notification' }> | undefined {
|
||||
const toolNotification = parseToolNotification(update._meta);
|
||||
if (!toolNotification) {
|
||||
const notification = toolNotificationEvent(update);
|
||||
if (!notification) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'notification',
|
||||
notification: toNotificationEvent(update.toolCallId, toolNotification),
|
||||
notification,
|
||||
};
|
||||
}
|
||||
|
||||
export function toolNotificationEvent(update: ToolCallUpdate): NotificationEvent | undefined {
|
||||
const toolNotification = parseToolNotification(update._meta);
|
||||
if (!toolNotification) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return toNotificationEvent(update.toolCallId, toolNotification);
|
||||
}
|
||||
|
||||
function parseToolNotification(meta: unknown): ToolNotification | undefined {
|
||||
if (!isRecord(meta)) {
|
||||
return undefined;
|
||||
|
|
@ -60,6 +75,11 @@ function parseToolNotification(meta: unknown): ToolNotification | undefined {
|
|||
return params ? { type: 'progress', params } : undefined;
|
||||
}
|
||||
|
||||
if (toolNotification.type === 'platform_event') {
|
||||
const params = parsePlatformEventParams(toolNotification.params);
|
||||
return params ? { type: 'platform_event', params } : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +112,10 @@ function parseProgressParams(value: unknown): ProgressNotificationParams | undef
|
|||
};
|
||||
}
|
||||
|
||||
function parsePlatformEventParams(value: unknown): PlatformEventParams | undefined {
|
||||
return isRecord(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function toNotificationEvent(
|
||||
toolCallId: string,
|
||||
toolNotification: ToolNotification
|
||||
|
|
@ -100,11 +124,19 @@ function toNotificationEvent(
|
|||
type: 'Notification',
|
||||
request_id: toolCallId,
|
||||
message: {
|
||||
method:
|
||||
toolNotification.type === 'message'
|
||||
? 'notifications/message'
|
||||
: 'notifications/progress',
|
||||
method: notificationMethod(toolNotification),
|
||||
params: toolNotification.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function notificationMethod(toolNotification: ToolNotification): string {
|
||||
switch (toolNotification.type) {
|
||||
case 'message':
|
||||
return 'notifications/message';
|
||||
case 'progress':
|
||||
return 'notifications/progress';
|
||||
case 'platform_event':
|
||||
return 'platform_event';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type { GooseSessionNotification_unstable } from '@aaif/goose-sdk';
|
|||
import type { SessionNotification } from '@agentclientprotocol/sdk';
|
||||
import { USE_ACP_CHAT } from '../acpChatFeatureFlag';
|
||||
import { AppEvents } from '../constants/events';
|
||||
import { maybeHandlePlatformEvent } from '../utils/platform_events';
|
||||
import { toolNotificationEvent } from './adapter/toolNotifications';
|
||||
import { acpChatSessionActions, acpChatSessionStore } from './chatSessionStore';
|
||||
|
||||
export function handleAcpSessionNotification(notification: SessionNotification): Promise<void> {
|
||||
|
|
@ -14,6 +16,7 @@ export function handleAcpSessionNotification(notification: SessionNotification):
|
|||
? notification.update.title
|
||||
: undefined;
|
||||
acpChatSessionActions.applyAcpSessionNotification(notification);
|
||||
maybeHandleLivePlatformEvent(notification);
|
||||
|
||||
if (updatedName && updatedName !== sessionNameBeforeNotification) {
|
||||
window.dispatchEvent(
|
||||
|
|
@ -26,6 +29,22 @@ export function handleAcpSessionNotification(notification: SessionNotification):
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function maybeHandleLivePlatformEvent(notification: SessionNotification): void {
|
||||
const update = notification.update;
|
||||
if (
|
||||
update.sessionUpdate !== 'tool_call_update' ||
|
||||
update.status === 'completed' ||
|
||||
update.status === 'failed'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = toolNotificationEvent(update);
|
||||
if (event?.message.method === 'platform_event') {
|
||||
maybeHandlePlatformEvent(event.message, notification.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAcpGooseSessionNotification(
|
||||
notification: GooseSessionNotification_unstable
|
||||
): Promise<void> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue