feat: Only send custom notifications when ACP client specifies this capability in the initialization request (#9596)
This commit is contained in:
@@ -194,6 +194,13 @@ jobs:
|
||||
source ./bin/activate-hermit
|
||||
just check-acp-schema
|
||||
|
||||
- name: Test ACP Client SDK
|
||||
run: |
|
||||
source ./bin/activate-hermit
|
||||
cd ui/sdk
|
||||
pnpm test
|
||||
pnpm run typecheck:test
|
||||
|
||||
desktop-lint:
|
||||
name: Test and Lint Electron Desktop App
|
||||
runs-on: macos-latest
|
||||
|
||||
@@ -215,10 +215,13 @@ fn available_commands_update(working_dir: &std::path::Path) -> AvailableCommands
|
||||
pub(super) fn send_session_setup_notifications(
|
||||
cx: &ConnectionTo<Client>,
|
||||
session: &Session,
|
||||
supports_goose_custom_notifications: bool,
|
||||
) -> Result<(), agent_client_protocol::Error> {
|
||||
let session_id = SessionId::new(session.id.clone());
|
||||
if let Some(updates) = build_usage_updates(session) {
|
||||
cx.send_notification(updates.custom)?;
|
||||
if supports_goose_custom_notifications {
|
||||
cx.send_notification(updates.custom)?;
|
||||
}
|
||||
cx.send_notification(SessionNotification::new(
|
||||
session_id.clone(),
|
||||
SessionUpdate::UsageUpdate(updates.standard),
|
||||
|
||||
+126
-53
@@ -208,6 +208,7 @@ pub struct GooseAcpAgent {
|
||||
client_fs_capabilities: OnceCell<FileSystemCapabilities>,
|
||||
client_terminal: OnceCell<bool>,
|
||||
client_mcp_host_info: OnceCell<GooseMcpHostInfo>,
|
||||
client_supports_goose_custom_notifications: OnceCell<bool>,
|
||||
use_login_shell_path: OnceCell<bool>,
|
||||
client_cx: OnceCell<ConnectionTo<Client>>,
|
||||
config_dir: std::path::PathBuf,
|
||||
@@ -421,15 +422,17 @@ fn extract_timeout_from_meta(meta: &Option<Meta>) -> Option<u64> {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct GooseClientMetaEnvelope {
|
||||
struct ClientCapabilitiesMeta {
|
||||
#[serde(default)]
|
||||
goose: Option<GooseClientMeta>,
|
||||
goose: Option<GooseClientCapabilities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct GooseClientMeta {
|
||||
struct GooseClientCapabilities {
|
||||
#[serde(rename = "mcpHostCapabilities", default)]
|
||||
mcp_host_capabilities: Option<GooseMcpHostCapabilities>,
|
||||
#[serde(rename = "customNotifications", default)]
|
||||
custom_notifications: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -438,24 +441,25 @@ struct GooseMcpHostCapabilities {
|
||||
extensions: Option<rmcp::model::ExtensionCapabilities>,
|
||||
}
|
||||
|
||||
fn extract_goose_client_meta(meta: &Meta) -> Option<GooseClientMetaEnvelope> {
|
||||
serde_json::from_value(serde_json::Value::Object(meta.clone())).ok()
|
||||
}
|
||||
|
||||
fn extract_client_mcp_host_info(args: &InitializeRequest) -> GooseMcpHostInfo {
|
||||
let host_capabilities = args
|
||||
.client_capabilities
|
||||
fn extract_client_capabilities_meta(args: &InitializeRequest) -> Option<ClientCapabilitiesMeta> {
|
||||
args.client_capabilities
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(extract_goose_client_meta)
|
||||
.and_then(|meta| meta.goose)
|
||||
.and_then(|goose| goose.mcp_host_capabilities);
|
||||
.and_then(|meta| serde_json::from_value(serde_json::Value::Object(meta.clone())).ok())
|
||||
}
|
||||
|
||||
fn extract_client_mcp_host_info(
|
||||
args: &InitializeRequest,
|
||||
goose_client_capabilities: Option<&GooseClientCapabilities>,
|
||||
) -> GooseMcpHostInfo {
|
||||
let host_capabilities =
|
||||
goose_client_capabilities.and_then(|goose| goose.mcp_host_capabilities.as_ref());
|
||||
let explicit_extensions = host_capabilities
|
||||
.as_ref()
|
||||
.and_then(|capabilities| capabilities.extensions.as_ref())
|
||||
.is_some();
|
||||
let extensions = host_capabilities
|
||||
.and_then(|capabilities| capabilities.extensions)
|
||||
.and_then(|capabilities| capabilities.extensions.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
GooseMcpHostInfo {
|
||||
@@ -1002,6 +1006,13 @@ impl GooseAcpAgent {
|
||||
Arc::clone(&self.permission_manager)
|
||||
}
|
||||
|
||||
pub(super) fn supports_goose_custom_notifications(&self) -> bool {
|
||||
self.client_supports_goose_custom_notifications
|
||||
.get()
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// TODO: goose reads Paths::in_state_dir globally (e.g. RequestLog), ignoring this data_dir.
|
||||
pub async fn new(options: GooseAcpAgentOptions) -> Result<Self> {
|
||||
let session_manager = Arc::new(SessionManager::new(options.data_dir));
|
||||
@@ -1032,6 +1043,7 @@ impl GooseAcpAgent {
|
||||
client_fs_capabilities: OnceCell::new(),
|
||||
client_terminal: OnceCell::new(),
|
||||
client_mcp_host_info: OnceCell::new(),
|
||||
client_supports_goose_custom_notifications: OnceCell::new(),
|
||||
use_login_shell_path: OnceCell::new(),
|
||||
client_cx: OnceCell::new(),
|
||||
config_dir: options.config_dir,
|
||||
@@ -1468,18 +1480,28 @@ impl GooseAcpAgent {
|
||||
} => {
|
||||
send_elicitation_interaction_update(
|
||||
cx,
|
||||
self.supports_goose_custom_notifications(),
|
||||
session_id.0.as_ref(),
|
||||
id.clone(),
|
||||
InteractionState::Pending,
|
||||
Some(message.clone()),
|
||||
Some(requested_schema.clone()),
|
||||
Some(interaction_update_meta(message_id, message_created)),
|
||||
InteractionUpdate {
|
||||
interaction: Interaction::Elicitation {
|
||||
id: id.clone(),
|
||||
state: InteractionState::Pending,
|
||||
message: Some(message.clone()),
|
||||
requested_schema: Some(requested_schema.clone()),
|
||||
},
|
||||
meta: Some(interaction_update_meta(message_id, message_created)),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
ActionRequiredData::ElicitationResponse { .. } => {}
|
||||
},
|
||||
MessageContent::SystemNotification(notification) => {
|
||||
send_status_message_update(cx, session_id.0.as_ref(), notification)?;
|
||||
send_status_message_update(
|
||||
cx,
|
||||
self.supports_goose_custom_notifications(),
|
||||
session_id.0.as_ref(),
|
||||
notification,
|
||||
)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1996,6 +2018,14 @@ impl GooseAcpAgent {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_client_supports_goose_custom_notifications(
|
||||
goose_client_capabilities: Option<&GooseClientCapabilities>,
|
||||
) -> bool {
|
||||
goose_client_capabilities
|
||||
.and_then(|goose| goose.custom_notifications)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn outcome_to_confirmation(outcome: &RequestPermissionOutcome) -> PermissionConfirmation {
|
||||
PermissionConfirmation {
|
||||
principal_type: PrincipalType::Tool,
|
||||
@@ -2043,14 +2073,17 @@ fn credits_exhausted_prompt_error(
|
||||
|
||||
fn send_status_message_update(
|
||||
cx: &ConnectionTo<Client>,
|
||||
supports_goose_custom_notifications: bool,
|
||||
session_id: &str,
|
||||
notification: &SystemNotificationContent,
|
||||
) -> Result<(), agent_client_protocol::Error> {
|
||||
if let Some(status) = status_message_from_system_notification(notification) {
|
||||
cx.send_notification(GooseSessionNotification {
|
||||
session_id: session_id.to_string(),
|
||||
update: GooseSessionUpdate::StatusMessage(StatusMessageUpdate { status }),
|
||||
})?;
|
||||
if supports_goose_custom_notifications {
|
||||
cx.send_notification(GooseSessionNotification {
|
||||
session_id: session_id.to_string(),
|
||||
update: GooseSessionUpdate::StatusMessage(StatusMessageUpdate { status }),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2071,25 +2104,17 @@ fn status_message_from_system_notification(
|
||||
|
||||
fn send_elicitation_interaction_update(
|
||||
cx: &ConnectionTo<Client>,
|
||||
supports_goose_custom_notifications: bool,
|
||||
session_id: &str,
|
||||
id: String,
|
||||
state: InteractionState,
|
||||
message: Option<String>,
|
||||
requested_schema: Option<serde_json::Value>,
|
||||
meta: Option<serde_json::Value>,
|
||||
update: InteractionUpdate,
|
||||
) -> Result<(), agent_client_protocol::Error> {
|
||||
cx.send_notification(GooseSessionNotification {
|
||||
session_id: session_id.to_string(),
|
||||
update: GooseSessionUpdate::InteractionUpdate(InteractionUpdate {
|
||||
interaction: Interaction::Elicitation {
|
||||
id,
|
||||
state,
|
||||
message,
|
||||
requested_schema,
|
||||
},
|
||||
meta,
|
||||
}),
|
||||
})
|
||||
if supports_goose_custom_notifications {
|
||||
cx.send_notification(GooseSessionNotification {
|
||||
session_id: session_id.to_string(),
|
||||
update: GooseSessionUpdate::InteractionUpdate(update),
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn interaction_update_meta(message_id: Option<&str>, created: i64) -> serde_json::Value {
|
||||
@@ -2221,9 +2246,15 @@ impl GooseAcpAgent {
|
||||
.client_fs_capabilities
|
||||
.set(args.client_capabilities.fs.clone());
|
||||
let _ = self.client_terminal.set(args.client_capabilities.terminal);
|
||||
let _ = self
|
||||
.client_mcp_host_info
|
||||
.set(extract_client_mcp_host_info(&args));
|
||||
let goose_client_capabilities =
|
||||
extract_client_capabilities_meta(&args).and_then(|meta| meta.goose);
|
||||
let _ = self.client_mcp_host_info.set(extract_client_mcp_host_info(
|
||||
&args,
|
||||
goose_client_capabilities.as_ref(),
|
||||
));
|
||||
let _ = self.client_supports_goose_custom_notifications.set(
|
||||
extract_client_supports_goose_custom_notifications(goose_client_capabilities.as_ref()),
|
||||
);
|
||||
let _ = self
|
||||
.use_login_shell_path
|
||||
.set(extract_use_login_shell_path(&args));
|
||||
@@ -2513,7 +2544,9 @@ impl GooseAcpAgent {
|
||||
.await
|
||||
.internal_err_ctx("Failed to load session")?;
|
||||
if let Some(updates) = build_usage_updates(&session) {
|
||||
cx.send_notification(updates.custom)?;
|
||||
if self.supports_goose_custom_notifications() {
|
||||
cx.send_notification(updates.custom)?;
|
||||
}
|
||||
// Standard ACP notification — emitted alongside the custom one for
|
||||
// backwards compatibility. Remove once all known clients have
|
||||
// migrated to `_goose/unstable/session/update`.
|
||||
@@ -2590,15 +2623,20 @@ impl GooseAcpAgent {
|
||||
|
||||
send_elicitation_interaction_update(
|
||||
cx,
|
||||
self.supports_goose_custom_notifications(),
|
||||
&req.session_id,
|
||||
req.elicitation_id,
|
||||
InteractionState::Submitted,
|
||||
None,
|
||||
None,
|
||||
Some(interaction_update_meta(
|
||||
response_message.id.as_deref(),
|
||||
response_message.created,
|
||||
)),
|
||||
InteractionUpdate {
|
||||
interaction: Interaction::Elicitation {
|
||||
id: req.elicitation_id,
|
||||
state: InteractionState::Submitted,
|
||||
message: None,
|
||||
requested_schema: None,
|
||||
},
|
||||
meta: Some(interaction_update_meta(
|
||||
response_message.id.as_deref(),
|
||||
response_message.created,
|
||||
)),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(EmptyResponse {})
|
||||
@@ -3722,4 +3760,39 @@ print(\"hello, world\")
|
||||
let session = make_session_with_usage(Some(120), Some(80), Some(40), None, None, None);
|
||||
assert!(build_usage_updates(&session).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_goose_custom_notifications_capability_defaults_to_false() {
|
||||
let request =
|
||||
InitializeRequest::new(agent_client_protocol::schema::ProtocolVersion::LATEST);
|
||||
let goose_client_capabilities =
|
||||
extract_client_capabilities_meta(&request).and_then(|meta| meta.goose);
|
||||
|
||||
assert!(!extract_client_supports_goose_custom_notifications(
|
||||
goose_client_capabilities.as_ref()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_goose_custom_notifications_capability_reads_client_meta() {
|
||||
let mut goose_meta = serde_json::Map::new();
|
||||
goose_meta.insert(
|
||||
"customNotifications".to_string(),
|
||||
serde_json::Value::Bool(true),
|
||||
);
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert("goose".to_string(), serde_json::Value::Object(goose_meta));
|
||||
|
||||
let request =
|
||||
InitializeRequest::new(agent_client_protocol::schema::ProtocolVersion::LATEST)
|
||||
.client_capabilities(
|
||||
agent_client_protocol::schema::ClientCapabilities::new().meta(meta),
|
||||
);
|
||||
let goose_client_capabilities =
|
||||
extract_client_capabilities_meta(&request).and_then(|meta| meta.goose);
|
||||
|
||||
assert!(extract_client_supports_goose_custom_notifications(
|
||||
goose_client_capabilities.as_ref()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,11 @@ impl GooseAcpAgent {
|
||||
if let Some(co) = config_options {
|
||||
response = response.config_options(co);
|
||||
}
|
||||
send_session_setup_notifications(cx, &goose_session)?;
|
||||
send_session_setup_notifications(
|
||||
cx,
|
||||
&goose_session,
|
||||
self.supports_goose_custom_notifications(),
|
||||
)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ fn send_replay_content_chunk(
|
||||
fn replay_conversation_to_client(
|
||||
cx: &ConnectionTo<Client>,
|
||||
session: &Session,
|
||||
supports_goose_custom_notifications: bool,
|
||||
) -> Result<HashMap<String, crate::conversation::message::ToolRequest>, agent_client_protocol::Error>
|
||||
{
|
||||
let session_id = SessionId::new(session.id.clone());
|
||||
@@ -165,12 +166,19 @@ fn replay_conversation_to_client(
|
||||
if !submitted_elicitation_ids.contains(id) {
|
||||
send_elicitation_interaction_update(
|
||||
cx,
|
||||
supports_goose_custom_notifications,
|
||||
session_id.0.as_ref(),
|
||||
id.clone(),
|
||||
InteractionState::Pending,
|
||||
Some(elicitation_message.clone()),
|
||||
Some(requested_schema.clone()),
|
||||
Some(serde_json::Value::Object(replay_message_meta(message))),
|
||||
InteractionUpdate {
|
||||
interaction: Interaction::Elicitation {
|
||||
id: id.clone(),
|
||||
state: InteractionState::Pending,
|
||||
message: Some(elicitation_message.clone()),
|
||||
requested_schema: Some(requested_schema.clone()),
|
||||
},
|
||||
meta: Some(serde_json::Value::Object(replay_message_meta(
|
||||
message,
|
||||
))),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -226,7 +234,11 @@ impl GooseAcpAgent {
|
||||
.prepare_session_for_activation(session, args.cwd.clone(), args.mcp_servers, true)
|
||||
.await?;
|
||||
|
||||
let replay_tool_requests = replay_conversation_to_client(cx, &session)?;
|
||||
let replay_tool_requests = replay_conversation_to_client(
|
||||
cx,
|
||||
&session,
|
||||
self.supports_goose_custom_notifications(),
|
||||
)?;
|
||||
let (agent, extension_results) = self.prepare_acp_session_agent(cx, &session).await?;
|
||||
self.register_acp_session(session_id_str.clone(), agent.clone(), replay_tool_requests)
|
||||
.await;
|
||||
@@ -245,7 +257,7 @@ impl GooseAcpAgent {
|
||||
let (mode_state, model_state, config_options) =
|
||||
build_session_setup_config(&self.provider_inventory, &session).await?;
|
||||
|
||||
send_session_setup_notifications(cx, &session)?;
|
||||
send_session_setup_notifications(cx, &session, self.supports_goose_custom_notifications())?;
|
||||
|
||||
let mut response = LoadSessionResponse::new().modes(mode_state);
|
||||
if let Some(ms) = model_state {
|
||||
|
||||
@@ -96,7 +96,11 @@ impl GooseAcpAgent {
|
||||
meta.insert("extensionResults".to_string(), extension_results);
|
||||
response = response.meta(meta);
|
||||
}
|
||||
super::send_session_setup_notifications(cx, &goose_session)?;
|
||||
super::send_session_setup_notifications(
|
||||
cx,
|
||||
&goose_session,
|
||||
self.supports_goose_custom_notifications(),
|
||||
)?;
|
||||
debug!(
|
||||
target: "perf",
|
||||
sid = %sid,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,
|
||||
GooseClient,
|
||||
type Client,
|
||||
type GooseInitializeRequest,
|
||||
} from '@aaif/goose-sdk';
|
||||
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
||||
import packageJson from '../../package.json';
|
||||
@@ -58,7 +57,7 @@ async function initializeConnection(): Promise<GooseClient> {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
},
|
||||
} satisfies GooseInitializeRequest);
|
||||
});
|
||||
|
||||
monitorConnection(client);
|
||||
return client;
|
||||
|
||||
+18
-14
@@ -239,18 +239,16 @@ async function generateClient(meta: {
|
||||
|
||||
const handlerFields: string[] = [];
|
||||
const dispatchCases: string[] = [];
|
||||
const handlerKeys: string[] = [];
|
||||
|
||||
for (const n of meta.notifications ?? []) {
|
||||
const handlerName = methodToHandlerName(n.method);
|
||||
handlerKeys.push(handlerName);
|
||||
if (!n.paramsType) {
|
||||
handlerFields.push(
|
||||
` ${handlerName}?: (params: Record<string, unknown>) => Promise<void>;`,
|
||||
);
|
||||
dispatchCases.push(
|
||||
` case "${n.method}": {
|
||||
await ${handlerName}?.(params);
|
||||
await callbacks.${handlerName}?.(params);
|
||||
return;
|
||||
}`,
|
||||
);
|
||||
@@ -265,16 +263,12 @@ async function generateClient(meta: {
|
||||
dispatchCases.push(
|
||||
` case "${n.method}": {
|
||||
const parsed = ${zodName}.parse(params) as ${n.paramsType};
|
||||
await ${handlerName}?.(parsed);
|
||||
await callbacks.${handlerName}?.(parsed);
|
||||
return;
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
const handlerDestructure =
|
||||
handlerKeys.length > 0
|
||||
? `const { ${handlerKeys.join(", ")}, ...rest } = callbacks;`
|
||||
: `const rest = callbacks;`;
|
||||
const handlersInterface = `export interface GooseExtNotifications {
|
||||
${handlerFields.join("\n")}
|
||||
}`;
|
||||
@@ -282,19 +276,26 @@ ${handlerFields.join("\n")}
|
||||
const dispatcherFn = `export function installGooseExtNotificationDispatcher(
|
||||
callbacks: GooseClientCallbacks,
|
||||
): Client {
|
||||
${handlerDestructure}
|
||||
const userExtNotification = rest.extNotification;
|
||||
return {
|
||||
...rest,
|
||||
const dispatcher: Pick<Client, "extNotification"> = {
|
||||
extNotification: async (method, params) => {
|
||||
switch (method) {
|
||||
${dispatchCases.join("\n")}
|
||||
default:
|
||||
await userExtNotification?.(method, params);
|
||||
await callbacks.extNotification?.(method, params);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
return new Proxy(callbacks, {
|
||||
get(target, property) {
|
||||
if (property === "extNotification") {
|
||||
return dispatcher.extNotification;
|
||||
}
|
||||
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
}) as Client;
|
||||
}`;
|
||||
|
||||
const upstreamImportLine = `import type { ${[...upstreamTypeImports].sort().join(", ")} } from "@agentclientprotocol/sdk";`;
|
||||
@@ -322,7 +323,10 @@ ${methodDefs.join("\n")}
|
||||
|
||||
${handlersInterface}
|
||||
|
||||
export type GooseClientCallbacks = Client & GooseExtNotifications;
|
||||
export type GooseClientCallbacks =
|
||||
Omit<Client, "extNotification"> &
|
||||
Partial<Pick<Client, "extNotification">> &
|
||||
GooseExtNotifications;
|
||||
|
||||
${dispatcherFn}
|
||||
`;
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"build:native:all": "tsx scripts/build-native.ts --all",
|
||||
"generate": "tsx generate-schema.ts",
|
||||
"lint": "tsc --noEmit",
|
||||
"test": "node --import tsx --test tests/*.test.ts",
|
||||
"typecheck:test": "tsc -p tsconfig.test.json --noEmit",
|
||||
"format": "prettier --write src/",
|
||||
"check:compat": "node scripts/check-binary-compat.mjs"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { GooseMcpHostCapabilities } from "./mcp-apps.js";
|
||||
|
||||
export interface GooseClientCapabilitiesMeta {
|
||||
goose?: {
|
||||
mcpHostCapabilities?: GooseMcpHostCapabilities;
|
||||
customNotifications?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -733,28 +733,37 @@ export interface GooseExtNotifications {
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export type GooseClientCallbacks = Client & GooseExtNotifications;
|
||||
export type GooseClientCallbacks = Omit<Client, "extNotification"> &
|
||||
Partial<Pick<Client, "extNotification">> &
|
||||
GooseExtNotifications;
|
||||
|
||||
export function installGooseExtNotificationDispatcher(
|
||||
callbacks: GooseClientCallbacks,
|
||||
): Client {
|
||||
const { unstable_sessionUpdate, ...rest } = callbacks;
|
||||
const userExtNotification = rest.extNotification;
|
||||
return {
|
||||
...rest,
|
||||
const dispatcher: Pick<Client, "extNotification"> = {
|
||||
extNotification: async (method, params) => {
|
||||
switch (method) {
|
||||
case "_goose/unstable/session/update": {
|
||||
const parsed = zGooseSessionNotification_unstable.parse(
|
||||
params,
|
||||
) as GooseSessionNotification_unstable;
|
||||
await unstable_sessionUpdate?.(parsed);
|
||||
await callbacks.unstable_sessionUpdate?.(parsed);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
await userExtNotification?.(method, params);
|
||||
await callbacks.extNotification?.(method, params);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
return new Proxy(callbacks, {
|
||||
get(target, property) {
|
||||
if (property === "extNotification") {
|
||||
return dispatcher.extNotification;
|
||||
}
|
||||
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
}) as Client;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
} from "./generated/client.gen.js";
|
||||
export { GooseClient } from "./goose-client.js";
|
||||
export { createHttpStream } from "./http-stream.js";
|
||||
export * from "./client-capabilities.js";
|
||||
export * from "./mcp-apps.js";
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type {
|
||||
Implementation,
|
||||
InitializeRequest,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||
import type {
|
||||
McpUiAppResourceConfig,
|
||||
@@ -68,19 +64,6 @@ export interface GooseToolCallUpdateMeta {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GooseClientMeta {
|
||||
goose: {
|
||||
mcpHostCapabilities: GooseMcpHostCapabilities;
|
||||
};
|
||||
}
|
||||
|
||||
export type GooseInitializeRequest = InitializeRequest & {
|
||||
clientCapabilities: NonNullable<InitializeRequest["clientCapabilities"]> & {
|
||||
_meta: GooseClientMeta;
|
||||
};
|
||||
clientInfo: Implementation;
|
||||
};
|
||||
|
||||
export const DEFAULT_GOOSE_MCP_HOST_CAPABILITIES: GooseMcpHostCapabilities = {
|
||||
extensions: {
|
||||
[GOOSE_MCP_UI_EXTENSION_ID]: {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { installGooseExtNotificationDispatcher } from "../src/generated/client.gen.ts";
|
||||
import type { GooseSessionNotification_unstable } from "../src/generated/types.gen.ts";
|
||||
import type {
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionNotification,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
|
||||
class ClassBackedCallbacks {
|
||||
#events: string[] = [];
|
||||
|
||||
get events(): string[] {
|
||||
return this.#events;
|
||||
}
|
||||
|
||||
async requestPermission(
|
||||
_params: RequestPermissionRequest,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
this.#events.push("requestPermission");
|
||||
return { outcome: { outcome: "cancelled" } };
|
||||
}
|
||||
|
||||
async sessionUpdate(_params: SessionNotification): Promise<void> {
|
||||
this.#events.push("sessionUpdate");
|
||||
}
|
||||
|
||||
async extNotification(
|
||||
method: string,
|
||||
_params: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
this.#events.push(`extNotification:${method}`);
|
||||
}
|
||||
|
||||
async unstable_sessionUpdate(
|
||||
notification: GooseSessionNotification_unstable,
|
||||
): Promise<void> {
|
||||
this.#events.push(
|
||||
`unstable_sessionUpdate:${notification.update.sessionUpdate}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MinimalCallbacks {
|
||||
async requestPermission(
|
||||
_params: RequestPermissionRequest,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return { outcome: { outcome: "cancelled" } };
|
||||
}
|
||||
|
||||
async sessionUpdate(_params: SessionNotification): Promise<void> {}
|
||||
}
|
||||
|
||||
test("dispatcher preserves class-backed callback receivers", async () => {
|
||||
const callbacks = new ClassBackedCallbacks();
|
||||
const client = installGooseExtNotificationDispatcher(callbacks);
|
||||
|
||||
await client.requestPermission({} as RequestPermissionRequest);
|
||||
await client.sessionUpdate({} as SessionNotification);
|
||||
await client.extNotification!("_goose/unstable/session/update", {
|
||||
sessionId: "session-1",
|
||||
update: {
|
||||
sessionUpdate: "status_message",
|
||||
status: {
|
||||
type: "notice",
|
||||
message: "ready",
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.extNotification!("example/unknown", {});
|
||||
|
||||
assert.deepEqual(callbacks.events, [
|
||||
"requestPermission",
|
||||
"sessionUpdate",
|
||||
"unstable_sessionUpdate:status_message",
|
||||
"extNotification:example/unknown",
|
||||
]);
|
||||
});
|
||||
|
||||
test("raw extNotification is optional", async () => {
|
||||
const client = installGooseExtNotificationDispatcher(new MinimalCallbacks());
|
||||
|
||||
await client.extNotification!("example/unknown", {});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
Reference in New Issue
Block a user