Skip to content

Commit 80fd72d

Browse files
committed
Add extMethod and extNotification to typescript lib
1 parent 45fdf04 commit 80fd72d

2 files changed

Lines changed: 368 additions & 0 deletions

File tree

typescript/acp.test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,4 +536,226 @@ describe("Connection", () => {
536536
expect(response.authMethods).toHaveLength(1);
537537
expect(response.authMethods?.[0].id).toBe("oauth");
538538
});
539+
540+
it("handles extension methods and notifications", async () => {
541+
const extensionLog: string[] = [];
542+
543+
// Create client with extension method support
544+
class TestClient implements Client {
545+
async writeTextFile(
546+
_: WriteTextFileRequest,
547+
): Promise<WriteTextFileResponse> {
548+
return null;
549+
}
550+
async readTextFile(
551+
_: ReadTextFileRequest,
552+
): Promise<ReadTextFileResponse> {
553+
return { content: "test" };
554+
}
555+
async requestPermission(
556+
_: RequestPermissionRequest,
557+
): Promise<RequestPermissionResponse> {
558+
return {
559+
outcome: {
560+
outcome: "selected",
561+
optionId: "allow",
562+
},
563+
};
564+
}
565+
async sessionUpdate(_: SessionNotification): Promise<void> {
566+
// no-op
567+
}
568+
async extMethod(
569+
method: string,
570+
params: Record<string, unknown>,
571+
): Promise<Record<string, unknown>> {
572+
if (method === "example.com/ping") {
573+
return { response: "pong", params };
574+
}
575+
throw new Error(`Unknown method: ${method}`);
576+
}
577+
async extNotification(
578+
method: string,
579+
params: Record<string, unknown>,
580+
): Promise<void> {
581+
extensionLog.push(`client extNotification: ${method}`);
582+
}
583+
}
584+
585+
// Create agent with extension method support
586+
class TestAgent implements Agent {
587+
async initialize(_: InitializeRequest): Promise<InitializeResponse> {
588+
return {
589+
protocolVersion: PROTOCOL_VERSION,
590+
agentCapabilities: { loadSession: false },
591+
};
592+
}
593+
async newSession(_: NewSessionRequest): Promise<NewSessionResponse> {
594+
return { sessionId: "test-session" };
595+
}
596+
async authenticate(_: AuthenticateRequest): Promise<void> {
597+
// no-op
598+
}
599+
async prompt(_: PromptRequest): Promise<PromptResponse> {
600+
return { stopReason: "end_turn" };
601+
}
602+
async cancel(_: CancelNotification): Promise<void> {
603+
// no-op
604+
}
605+
async extMethod(
606+
method: string,
607+
params: Record<string, unknown>,
608+
): Promise<Record<string, unknown>> {
609+
if (method === "example.com/echo") {
610+
return { echo: params };
611+
}
612+
throw new Error(`Unknown method: ${method}`);
613+
}
614+
async extNotification(
615+
method: string,
616+
params: Record<string, unknown>,
617+
): Promise<void> {
618+
extensionLog.push(`agent extNotification: ${method}`);
619+
}
620+
}
621+
622+
const agentConnection = new ClientSideConnection(
623+
() => new TestClient(),
624+
clientToAgent.writable,
625+
agentToClient.readable,
626+
);
627+
628+
const clientConnection = new AgentSideConnection(
629+
() => new TestAgent(),
630+
agentToClient.writable,
631+
clientToAgent.readable,
632+
);
633+
634+
// Test agent calling client extension method
635+
const clientResponse = await clientConnection.extMethod(
636+
"example.com/ping",
637+
{
638+
data: "test",
639+
},
640+
);
641+
expect(clientResponse).toEqual({
642+
response: "pong",
643+
params: { data: "test" },
644+
});
645+
646+
// Test client calling agent extension method
647+
const agentResponse = await agentConnection.extMethod("example.com/echo", {
648+
message: "hello",
649+
});
650+
expect(agentResponse).toEqual({ echo: { message: "hello" } });
651+
652+
// Test extension notifications
653+
await clientConnection.extNotification("example.com/client/notify", {
654+
info: "client notification",
655+
});
656+
await agentConnection.extNotification("example.com/agent/notify", {
657+
info: "agent notification",
658+
});
659+
660+
// Wait a bit for async handlers
661+
await new Promise((resolve) => setTimeout(resolve, 50));
662+
663+
// Verify notifications were logged
664+
expect(extensionLog).toContain(
665+
"client extNotification: example.com/client/notify",
666+
);
667+
expect(extensionLog).toContain(
668+
"agent extNotification: example.com/agent/notify",
669+
);
670+
});
671+
672+
it("handles optional extension methods correctly", async () => {
673+
// Create client WITHOUT extension methods
674+
class TestClientWithoutExtensions implements Client {
675+
async writeTextFile(
676+
_: WriteTextFileRequest,
677+
): Promise<WriteTextFileResponse> {
678+
return null;
679+
}
680+
async readTextFile(
681+
_: ReadTextFileRequest,
682+
): Promise<ReadTextFileResponse> {
683+
return { content: "test" };
684+
}
685+
async requestPermission(
686+
_: RequestPermissionRequest,
687+
): Promise<RequestPermissionResponse> {
688+
return {
689+
outcome: {
690+
outcome: "selected",
691+
optionId: "allow",
692+
},
693+
};
694+
}
695+
async sessionUpdate(_: SessionNotification): Promise<void> {
696+
// no-op
697+
}
698+
// Note: No extMethod or extNotification implemented
699+
}
700+
701+
// Create agent WITHOUT extension methods
702+
class TestAgentWithoutExtensions implements Agent {
703+
async initialize(_: InitializeRequest): Promise<InitializeResponse> {
704+
return {
705+
protocolVersion: PROTOCOL_VERSION,
706+
agentCapabilities: { loadSession: false },
707+
};
708+
}
709+
async newSession(_: NewSessionRequest): Promise<NewSessionResponse> {
710+
return { sessionId: "test-session" };
711+
}
712+
async authenticate(_: AuthenticateRequest): Promise<void> {
713+
// no-op
714+
}
715+
async prompt(_: PromptRequest): Promise<PromptResponse> {
716+
return { stopReason: "end_turn" };
717+
}
718+
async cancel(_: CancelNotification): Promise<void> {
719+
// no-op
720+
}
721+
// Note: No extMethod or extNotification implemented
722+
}
723+
724+
const agentConnection = new ClientSideConnection(
725+
() => new TestClientWithoutExtensions(),
726+
clientToAgent.writable,
727+
agentToClient.readable,
728+
);
729+
730+
const clientConnection = new AgentSideConnection(
731+
() => new TestAgentWithoutExtensions(),
732+
agentToClient.writable,
733+
clientToAgent.readable,
734+
);
735+
736+
// Test that calling extension methods on connections without them throws method not found
737+
try {
738+
await clientConnection.extMethod("example.com/ping", { data: "test" });
739+
expect.fail("Should have thrown method not found error");
740+
} catch (error: any) {
741+
expect(error.code).toBe(-32601); // Method not found
742+
expect(error.data.method).toBe("example.com/ping"); // Should show inner method name
743+
}
744+
745+
try {
746+
await agentConnection.extMethod("example.com/echo", { message: "hello" });
747+
expect.fail("Should have thrown method not found error");
748+
} catch (error: any) {
749+
expect(error.code).toBe(-32601); // Method not found
750+
expect(error.data.method).toBe("example.com/echo"); // Should show inner method name
751+
}
752+
753+
// Notifications should be ignored when not implemented (no error thrown)
754+
await clientConnection.extNotification("example.com/notify", {
755+
info: "test",
756+
});
757+
await agentConnection.extNotification("example.com/notify", {
758+
info: "test",
759+
});
760+
});
539761
});

0 commit comments

Comments
 (0)