@actagent/google-meet 2026.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +26 -0
  2. package/actagent.plugin.json +532 -0
  3. package/doctor-contract-api.ts +2 -0
  4. package/google-meet.live.test.ts +86 -0
  5. package/index.create.test.ts +672 -0
  6. package/index.test.ts +5130 -0
  7. package/index.ts +1225 -0
  8. package/node-host.test.ts +242 -0
  9. package/npm-shrinkwrap.json +39 -0
  10. package/package.json +46 -0
  11. package/src/agent-consult.ts +159 -0
  12. package/src/calendar.ts +253 -0
  13. package/src/cli.test.ts +1307 -0
  14. package/src/cli.ts +2382 -0
  15. package/src/config-compat.test.ts +99 -0
  16. package/src/config-compat.ts +79 -0
  17. package/src/config.test.ts +57 -0
  18. package/src/config.ts +598 -0
  19. package/src/create.ts +158 -0
  20. package/src/drive.ts +73 -0
  21. package/src/google-api-errors.ts +21 -0
  22. package/src/meet.ts +1027 -0
  23. package/src/node-host.ts +524 -0
  24. package/src/oauth.test.ts +164 -0
  25. package/src/oauth.ts +247 -0
  26. package/src/realtime-node.ts +771 -0
  27. package/src/realtime.ts +1355 -0
  28. package/src/runtime.ts +1009 -0
  29. package/src/setup.ts +277 -0
  30. package/src/test-support/plugin-harness.ts +233 -0
  31. package/src/transports/chrome-audio-device.ts +6 -0
  32. package/src/transports/chrome-browser-proxy.test.ts +67 -0
  33. package/src/transports/chrome-browser-proxy.ts +206 -0
  34. package/src/transports/chrome-create.ts +365 -0
  35. package/src/transports/chrome.test.ts +21 -0
  36. package/src/transports/chrome.ts +1073 -0
  37. package/src/transports/twilio.ts +58 -0
  38. package/src/transports/types.ts +148 -0
  39. package/src/voice-call-gateway.test.ts +153 -0
  40. package/src/voice-call-gateway.ts +242 -0
  41. package/tsconfig.json +16 -0
@@ -0,0 +1,242 @@
1
+ // Google Meet tests cover node host plugin behavior.
2
+ import { spawnSync } from "node:child_process";
3
+ import { EventEmitter } from "node:events";
4
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
5
+
6
+ type MockChild = EventEmitter & {
7
+ exitCode: number | null;
8
+ signalCode: NodeJS.Signals | null;
9
+ kill: ReturnType<typeof vi.fn>;
10
+ stdout?: EventEmitter;
11
+ stderr?: EventEmitter;
12
+ stdin?: { write: ReturnType<typeof vi.fn> };
13
+ };
14
+
15
+ const children: MockChild[] = [];
16
+ let handleGoogleMeetNodeHostCommand: typeof import("./src/node-host.js").handleGoogleMeetNodeHostCommand;
17
+
18
+ vi.mock("node:child_process", async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import("node:child_process")>();
20
+ return {
21
+ ...actual,
22
+ spawnSync: vi.fn(() => ({
23
+ status: 0,
24
+ stdout: "BlackHole 2ch",
25
+ stderr: "",
26
+ })),
27
+ spawn: vi.fn(() => {
28
+ const child = Object.assign(new EventEmitter(), {
29
+ exitCode: null,
30
+ signalCode: null,
31
+ kill: vi.fn((signal?: NodeJS.Signals) => {
32
+ child.signalCode = signal ?? "SIGTERM";
33
+ return true;
34
+ }),
35
+ stdout: new EventEmitter(),
36
+ stderr: new EventEmitter(),
37
+ stdin: { write: vi.fn() },
38
+ }) as MockChild;
39
+ children.push(child);
40
+ return child;
41
+ }),
42
+ };
43
+ });
44
+
45
+ describe("google-meet node host bridge sessions", () => {
46
+ beforeAll(async () => {
47
+ ({ handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js"));
48
+ });
49
+
50
+ afterEach(() => {
51
+ vi.useRealTimers();
52
+ children.length = 0;
53
+ });
54
+
55
+ afterAll(() => {
56
+ vi.doUnmock("node:child_process");
57
+ vi.resetModules();
58
+ });
59
+
60
+ it("reports malformed params JSON with an owned error", async () => {
61
+ await expect(handleGoogleMeetNodeHostCommand("{not json")).rejects.toThrow(
62
+ "Google Meet node host received malformed params JSON.",
63
+ );
64
+ });
65
+
66
+ it("starts observe-only Chrome without BlackHole or bridge processes", async () => {
67
+ const originalPlatform = process.platform;
68
+ children.length = 0;
69
+ vi.mocked(spawnSync).mockClear();
70
+
71
+ Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
72
+ try {
73
+ const start = JSON.parse(
74
+ await handleGoogleMeetNodeHostCommand(
75
+ JSON.stringify({
76
+ action: "start",
77
+ url: "https://meet.google.com/xyz-abcd-uvw",
78
+ mode: "transcribe",
79
+ launch: false,
80
+ audioInputCommand: ["mock-rec"],
81
+ audioOutputCommand: ["mock-play"],
82
+ }),
83
+ ),
84
+ );
85
+
86
+ expect(start).toEqual({ launched: false });
87
+ expect(spawnSync).not.toHaveBeenCalled();
88
+ expect(children).toHaveLength(0);
89
+ } finally {
90
+ Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
91
+ }
92
+ });
93
+
94
+ it("clears output playback without closing the active bridge when the old output exits", async () => {
95
+ const originalPlatform = process.platform;
96
+ children.length = 0;
97
+
98
+ Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
99
+ try {
100
+ const start = JSON.parse(
101
+ await handleGoogleMeetNodeHostCommand(
102
+ JSON.stringify({
103
+ action: "start",
104
+ url: "https://meet.google.com/xyz-abcd-uvw",
105
+ mode: "realtime",
106
+ launch: false,
107
+ audioInputCommand: ["mock-rec"],
108
+ audioOutputCommand: ["mock-play"],
109
+ }),
110
+ ),
111
+ );
112
+
113
+ expect(children).toHaveLength(2);
114
+ const firstOutput = children[0];
115
+
116
+ const cleared = JSON.parse(
117
+ await handleGoogleMeetNodeHostCommand(
118
+ JSON.stringify({
119
+ action: "clearAudio",
120
+ bridgeId: start.bridgeId,
121
+ }),
122
+ ),
123
+ );
124
+
125
+ expect(cleared).toEqual({ bridgeId: start.bridgeId, ok: true, clearCount: 1 });
126
+ expect(children).toHaveLength(3);
127
+ expect(firstOutput?.kill).toHaveBeenCalledWith("SIGTERM");
128
+
129
+ firstOutput?.emit("error", new Error("stale output failed after clear"));
130
+ firstOutput?.emit("exit", 0, "SIGTERM");
131
+
132
+ const status = JSON.parse(
133
+ await handleGoogleMeetNodeHostCommand(
134
+ JSON.stringify({
135
+ action: "status",
136
+ bridgeId: start.bridgeId,
137
+ }),
138
+ ),
139
+ );
140
+
141
+ expect(status.bridge.bridgeId).toBe(start.bridgeId);
142
+ expect(status.bridge.closed).toBe(false);
143
+ expect(status.bridge.clearCount).toBe(1);
144
+ expect(typeof status.bridge.createdAt).toBe("string");
145
+
146
+ const audio = Buffer.from([1, 2, 3]);
147
+ await handleGoogleMeetNodeHostCommand(
148
+ JSON.stringify({
149
+ action: "pushAudio",
150
+ bridgeId: start.bridgeId,
151
+ base64: audio.toString("base64"),
152
+ }),
153
+ );
154
+
155
+ expect(children[2]?.stdin?.write).toHaveBeenCalledWith(audio);
156
+ expect(firstOutput?.stdin?.write).not.toHaveBeenCalled();
157
+
158
+ await handleGoogleMeetNodeHostCommand(
159
+ JSON.stringify({
160
+ action: "stop",
161
+ bridgeId: start.bridgeId,
162
+ }),
163
+ );
164
+ } finally {
165
+ Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
166
+ }
167
+ });
168
+
169
+ it("lists active bridge sessions and hides closed sessions", async () => {
170
+ const originalPlatform = process.platform;
171
+ children.length = 0;
172
+
173
+ Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
174
+ try {
175
+ const start = JSON.parse(
176
+ await handleGoogleMeetNodeHostCommand(
177
+ JSON.stringify({
178
+ action: "start",
179
+ url: "https://meet.google.com/abc-defg-hij?authuser=1",
180
+ mode: "realtime",
181
+ launch: false,
182
+ audioInputCommand: ["mock-rec"],
183
+ audioOutputCommand: ["mock-play"],
184
+ }),
185
+ ),
186
+ );
187
+
188
+ expect(typeof start.bridgeId).toBe("string");
189
+ expect(start.bridgeId.length).toBeGreaterThan(0);
190
+ expect(start).toEqual({
191
+ audioBridge: { type: "node-command-pair" },
192
+ bridgeId: start.bridgeId,
193
+ launched: false,
194
+ });
195
+
196
+ const activeList = JSON.parse(
197
+ await handleGoogleMeetNodeHostCommand(
198
+ JSON.stringify({
199
+ action: "list",
200
+ url: "https://meet.google.com/abc-defg-hij",
201
+ mode: "realtime",
202
+ }),
203
+ ),
204
+ );
205
+
206
+ expect(activeList.bridges).toHaveLength(1);
207
+ expect(activeList.bridges[0]?.bridgeId).toBe(start.bridgeId);
208
+ expect(activeList.bridges[0]?.closed).toBe(false);
209
+ expect(activeList.bridges[0]?.mode).toBe("realtime");
210
+ expect(activeList.bridges[0]?.url).toBe("https://meet.google.com/abc-defg-hij?authuser=1");
211
+ expect(typeof activeList.bridges[0]?.createdAt).toBe("string");
212
+
213
+ children[1]?.emit("exit", 0, null);
214
+
215
+ const afterExitList = JSON.parse(
216
+ await handleGoogleMeetNodeHostCommand(
217
+ JSON.stringify({
218
+ action: "list",
219
+ url: "https://meet.google.com/abc-defg-hij",
220
+ mode: "realtime",
221
+ }),
222
+ ),
223
+ );
224
+
225
+ expect(afterExitList).toEqual({ bridges: [] });
226
+
227
+ const stopped = JSON.parse(
228
+ await handleGoogleMeetNodeHostCommand(
229
+ JSON.stringify({
230
+ action: "stopByUrl",
231
+ url: "https://meet.google.com/abc-defg-hij",
232
+ mode: "realtime",
233
+ }),
234
+ ),
235
+ );
236
+
237
+ expect(stopped).toEqual({ ok: true, stopped: 0 });
238
+ } finally {
239
+ Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
240
+ }
241
+ });
242
+ });
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@actagent/google-meet",
3
+ "version": "2026.6.2",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@actagent/google-meet",
9
+ "version": "2026.6.2",
10
+ "dependencies": {
11
+ "commander": "14.0.3",
12
+ "typebox": "1.1.39"
13
+ },
14
+ "peerDependencies": {
15
+ "actagent": ">=2026.6.2"
16
+ },
17
+ "peerDependenciesMeta": {
18
+ "actagent": {
19
+ "optional": true
20
+ }
21
+ }
22
+ },
23
+ "node_modules/commander": {
24
+ "version": "14.0.3",
25
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
26
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=20"
30
+ }
31
+ },
32
+ "node_modules/typebox": {
33
+ "version": "1.1.39",
34
+ "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz",
35
+ "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==",
36
+ "license": "MIT"
37
+ }
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@actagent/google-meet",
3
+ "version": "2026.6.2",
4
+ "description": "ACTAgent Google Meet participant plugin for joining calls through Chrome or Twilio transports.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/actagent/actagent"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "commander": "14.0.3",
12
+ "typebox": "1.1.39"
13
+ },
14
+ "devDependencies": {
15
+ "@actagent/plugin-sdk": "workspace:*",
16
+ "actagent": "workspace:*"
17
+ },
18
+ "peerDependencies": {
19
+ "actagent": "workspace:*"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "actagent": {
23
+ "optional": true
24
+ }
25
+ },
26
+ "actagent": {
27
+ "extensions": [
28
+ "./index.ts"
29
+ ],
30
+ "install": {
31
+ "npmSpec": "@actagent/google-meet",
32
+ "defaultChoice": "npm",
33
+ "minHostVersion": ">=2026.4.20"
34
+ },
35
+ "compat": {
36
+ "pluginApi": ">=2026.6.2"
37
+ },
38
+ "build": {
39
+ "actagentVersion": "2026.6.2"
40
+ },
41
+ "release": {
42
+ "publishToACTAgentHub": true,
43
+ "publishToNpm": true
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,159 @@
1
+ // Google Meet plugin module implements agent consult behavior.
2
+ import type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
3
+ import { formatErrorMessage } from "actagent/plugin-sdk/error-runtime";
4
+ import type { PluginRuntime, RuntimeLogger } from "actagent/plugin-sdk/plugin-runtime";
5
+ import {
6
+ buildRealtimeVoiceAgentConsultWorkingResponse,
7
+ consultRealtimeVoiceAgent,
8
+ REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME,
9
+ resolveRealtimeVoiceAgentConsultTools,
10
+ resolveRealtimeVoiceAgentConsultToolsAllow,
11
+ type RealtimeVoiceBridgeSession,
12
+ type RealtimeVoiceToolCallEvent,
13
+ type RealtimeVoiceTool,
14
+ type TalkEventInput,
15
+ } from "actagent/plugin-sdk/realtime-voice";
16
+ import { normalizeAgentId } from "actagent/plugin-sdk/routing";
17
+ import { normalizeOptionalString } from "actagent/plugin-sdk/string-coerce-runtime";
18
+ import type { GoogleMeetConfig, GoogleMeetToolPolicy } from "./config.js";
19
+
20
+ export const GOOGLE_MEET_AGENT_CONSULT_TOOL_NAME = REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME;
21
+
22
+ const GOOGLE_MEET_CONSULT_SYSTEM_PROMPT = [
23
+ "You are a behind-the-scenes consultant for a live meeting voice agent.",
24
+ "Prioritize a fast, speakable answer over exhaustive investigation.",
25
+ "For tool-backed status checks, prefer one or two bounded read-only queries before answering.",
26
+ "Do not print secret values or dump environment variables; only check whether required configuration is present.",
27
+ "Be accurate, brief, and speakable.",
28
+ ].join(" ");
29
+
30
+ export function resolveGoogleMeetRealtimeTools(policy: GoogleMeetToolPolicy): RealtimeVoiceTool[] {
31
+ return resolveRealtimeVoiceAgentConsultTools(policy);
32
+ }
33
+
34
+ export function submitGoogleMeetConsultWorkingResponse(
35
+ session: RealtimeVoiceBridgeSession,
36
+ callId: string,
37
+ ): void {
38
+ if (!session.bridge.supportsToolResultContinuation) {
39
+ return;
40
+ }
41
+ session.submitToolResult(callId, buildRealtimeVoiceAgentConsultWorkingResponse("participant"), {
42
+ willContinue: true,
43
+ });
44
+ }
45
+
46
+ export async function consultACTAgentAgentForGoogleMeet(params: {
47
+ config: GoogleMeetConfig;
48
+ fullConfig: ACTAgentConfig;
49
+ runtime: PluginRuntime;
50
+ logger: RuntimeLogger;
51
+ meetingSessionId: string;
52
+ requesterSessionKey?: string;
53
+ args: unknown;
54
+ transcript: Array<{ role: "user" | "assistant"; text: string }>;
55
+ }): Promise<{ text: string }> {
56
+ const agentId = normalizeAgentId(params.config.realtime.agentId);
57
+ const requesterSessionKey =
58
+ normalizeOptionalString(params.requesterSessionKey) ?? `agent:${agentId}:main`;
59
+ const sessionKey = `agent:${agentId}:subagent:google-meet:${params.meetingSessionId}`;
60
+ return await consultRealtimeVoiceAgent({
61
+ cfg: params.fullConfig,
62
+ agentRuntime: params.runtime.agent,
63
+ logger: params.logger,
64
+ agentId,
65
+ sessionKey,
66
+ messageProvider: "google-meet",
67
+ lane: "google-meet",
68
+ runIdPrefix: `google-meet:${params.meetingSessionId}`,
69
+ spawnedBy: requesterSessionKey,
70
+ contextMode: "fork",
71
+ args: params.args,
72
+ transcript: params.transcript,
73
+ surface: "a private Google Meet",
74
+ userLabel: "Participant",
75
+ assistantLabel: "Agent",
76
+ questionSourceLabel: "participant",
77
+ toolsAllow: resolveRealtimeVoiceAgentConsultToolsAllow(params.config.realtime.toolPolicy),
78
+ extraSystemPrompt: GOOGLE_MEET_CONSULT_SYSTEM_PROMPT,
79
+ });
80
+ }
81
+
82
+ export function handleGoogleMeetRealtimeConsultToolCall(params: {
83
+ strategy: string;
84
+ session: RealtimeVoiceBridgeSession;
85
+ event: RealtimeVoiceToolCallEvent;
86
+ config: GoogleMeetConfig;
87
+ fullConfig: ACTAgentConfig;
88
+ runtime: PluginRuntime;
89
+ logger: RuntimeLogger;
90
+ meetingSessionId: string;
91
+ requesterSessionKey?: string;
92
+ transcript: Array<{ role: "user" | "assistant"; text: string }>;
93
+ onTalkEvent?: (event: TalkEventInput) => void;
94
+ }): void {
95
+ const callId = params.event.callId || params.event.itemId;
96
+ if (params.strategy !== "bidi") {
97
+ params.onTalkEvent?.({
98
+ type: "tool.error",
99
+ callId,
100
+ payload: {
101
+ name: params.event.name,
102
+ error: `Tool "${params.event.name}" is only available in bidi realtime strategy`,
103
+ },
104
+ final: true,
105
+ });
106
+ params.session.submitToolResult(callId, {
107
+ error: `Tool "${params.event.name}" is only available in bidi realtime strategy`,
108
+ });
109
+ return;
110
+ }
111
+ if (params.event.name !== GOOGLE_MEET_AGENT_CONSULT_TOOL_NAME) {
112
+ params.onTalkEvent?.({
113
+ type: "tool.error",
114
+ callId,
115
+ payload: { name: params.event.name, error: `Tool "${params.event.name}" not available` },
116
+ final: true,
117
+ });
118
+ params.session.submitToolResult(callId, {
119
+ error: `Tool "${params.event.name}" not available`,
120
+ });
121
+ return;
122
+ }
123
+ params.onTalkEvent?.({
124
+ type: "tool.progress",
125
+ callId,
126
+ payload: { name: params.event.name, status: "working" },
127
+ });
128
+ submitGoogleMeetConsultWorkingResponse(params.session, callId);
129
+ void consultACTAgentAgentForGoogleMeet({
130
+ config: params.config,
131
+ fullConfig: params.fullConfig,
132
+ runtime: params.runtime,
133
+ logger: params.logger,
134
+ meetingSessionId: params.meetingSessionId,
135
+ requesterSessionKey: params.requesterSessionKey,
136
+ args: params.event.args,
137
+ transcript: params.transcript,
138
+ })
139
+ .then((result) => {
140
+ params.onTalkEvent?.({
141
+ type: "tool.result",
142
+ callId,
143
+ payload: { name: params.event.name, result },
144
+ final: true,
145
+ });
146
+ params.session.submitToolResult(callId, result);
147
+ })
148
+ .catch((error: unknown) => {
149
+ params.onTalkEvent?.({
150
+ type: "tool.error",
151
+ callId,
152
+ payload: { name: params.event.name, error: formatErrorMessage(error) },
153
+ final: true,
154
+ });
155
+ params.session.submitToolResult(callId, {
156
+ error: formatErrorMessage(error),
157
+ });
158
+ });
159
+ }