@msg91comm/m91 0.0.0-stage → 1.2.0

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 (100) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +647 -2
  4. package/dist/src/client.d.ts +165 -0
  5. package/dist/src/client.d.ts.map +1 -0
  6. package/dist/src/client.js +221 -0
  7. package/dist/src/client.js.map +1 -0
  8. package/dist/src/exceptions.d.ts +114 -0
  9. package/dist/src/exceptions.d.ts.map +1 -0
  10. package/dist/src/exceptions.js +199 -0
  11. package/dist/src/exceptions.js.map +1 -0
  12. package/dist/src/http.d.ts +71 -0
  13. package/dist/src/http.d.ts.map +1 -0
  14. package/dist/src/http.js +150 -0
  15. package/dist/src/http.js.map +1 -0
  16. package/dist/src/index.d.ts +49 -0
  17. package/dist/src/index.d.ts.map +1 -0
  18. package/dist/src/index.js +45 -0
  19. package/dist/src/index.js.map +1 -0
  20. package/dist/src/integrations/claude-agent-sdk.d.ts +63 -0
  21. package/dist/src/integrations/claude-agent-sdk.d.ts.map +1 -0
  22. package/dist/src/integrations/claude-agent-sdk.js +109 -0
  23. package/dist/src/integrations/claude-agent-sdk.js.map +1 -0
  24. package/dist/src/integrations/google-adk.d.ts +47 -0
  25. package/dist/src/integrations/google-adk.d.ts.map +1 -0
  26. package/dist/src/integrations/google-adk.js +84 -0
  27. package/dist/src/integrations/google-adk.js.map +1 -0
  28. package/dist/src/integrations/index.d.ts +26 -0
  29. package/dist/src/integrations/index.d.ts.map +1 -0
  30. package/dist/src/integrations/index.js +25 -0
  31. package/dist/src/integrations/index.js.map +1 -0
  32. package/dist/src/integrations/langchain.d.ts +94 -0
  33. package/dist/src/integrations/langchain.d.ts.map +1 -0
  34. package/dist/src/integrations/langchain.js +129 -0
  35. package/dist/src/integrations/langchain.js.map +1 -0
  36. package/dist/src/integrations/langgraph.d.ts +79 -0
  37. package/dist/src/integrations/langgraph.d.ts.map +1 -0
  38. package/dist/src/integrations/langgraph.js +114 -0
  39. package/dist/src/integrations/langgraph.js.map +1 -0
  40. package/dist/src/integrations/mastra.d.ts +75 -0
  41. package/dist/src/integrations/mastra.d.ts.map +1 -0
  42. package/dist/src/integrations/mastra.js +109 -0
  43. package/dist/src/integrations/mastra.js.map +1 -0
  44. package/dist/src/integrations/openai-agents.d.ts +61 -0
  45. package/dist/src/integrations/openai-agents.d.ts.map +1 -0
  46. package/dist/src/integrations/openai-agents.js +127 -0
  47. package/dist/src/integrations/openai-agents.js.map +1 -0
  48. package/dist/src/integrations/specs.d.ts +173 -0
  49. package/dist/src/integrations/specs.d.ts.map +1 -0
  50. package/dist/src/integrations/specs.js +217 -0
  51. package/dist/src/integrations/specs.js.map +1 -0
  52. package/dist/src/integrations/vercel-ai.d.ts +74 -0
  53. package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
  54. package/dist/src/integrations/vercel-ai.js +102 -0
  55. package/dist/src/integrations/vercel-ai.js.map +1 -0
  56. package/dist/src/integrations/zod-shapes.d.ts +19 -0
  57. package/dist/src/integrations/zod-shapes.d.ts.map +1 -0
  58. package/dist/src/integrations/zod-shapes.js +44 -0
  59. package/dist/src/integrations/zod-shapes.js.map +1 -0
  60. package/dist/src/models.d.ts +274 -0
  61. package/dist/src/models.d.ts.map +1 -0
  62. package/dist/src/models.js +383 -0
  63. package/dist/src/models.js.map +1 -0
  64. package/dist/src/platform.d.ts +176 -0
  65. package/dist/src/platform.d.ts.map +1 -0
  66. package/dist/src/platform.js +286 -0
  67. package/dist/src/platform.js.map +1 -0
  68. package/dist/src/url-builder.d.ts +65 -0
  69. package/dist/src/url-builder.d.ts.map +1 -0
  70. package/dist/src/url-builder.js +167 -0
  71. package/dist/src/url-builder.js.map +1 -0
  72. package/examples/approval-gate.ts +35 -0
  73. package/examples/basic-alert.ts +53 -0
  74. package/examples/claude-agent-sdk.ts +23 -0
  75. package/examples/deep-agents.ts +31 -0
  76. package/examples/google-adk.ts +39 -0
  77. package/examples/langchain-agent.ts +65 -0
  78. package/examples/langgraph-hitl.ts +70 -0
  79. package/examples/mastra.ts +41 -0
  80. package/examples/openai-agents.ts +33 -0
  81. package/examples/vercel-ai-agent.ts +34 -0
  82. package/examples/webhook-urls.ts +44 -0
  83. package/package.json +135 -4
  84. package/src/client.ts +324 -0
  85. package/src/exceptions.ts +229 -0
  86. package/src/http.ts +174 -0
  87. package/src/index.ts +91 -0
  88. package/src/integrations/claude-agent-sdk.ts +158 -0
  89. package/src/integrations/google-adk.ts +122 -0
  90. package/src/integrations/index.ts +27 -0
  91. package/src/integrations/langchain.ts +207 -0
  92. package/src/integrations/langgraph.ts +142 -0
  93. package/src/integrations/mastra.ts +178 -0
  94. package/src/integrations/openai-agents.ts +173 -0
  95. package/src/integrations/specs.ts +328 -0
  96. package/src/integrations/vercel-ai.ts +151 -0
  97. package/src/integrations/zod-shapes.ts +55 -0
  98. package/src/models.ts +660 -0
  99. package/src/platform.ts +343 -0
  100. package/src/url-builder.ts +225 -0
package/src/http.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * HTTP on native `fetch`, with the retry policy applied. Zero dependencies.
3
+ *
4
+ * The rules, which are the interesting part:
5
+ *
6
+ * - **429 is retried exactly once**, after exactly the delay the server asked
7
+ * for. The send limit is 60/minute per link and every 429 carries
8
+ * `Retry-After: 60`. Retrying a second time would be arguing with it.
9
+ * - **5xx is retried with backoff.** Not the caller's fault, and the alert has
10
+ * not been raised, so repeating it cannot double-page anybody.
11
+ * - **4xx is never retried.** Not transient; the next identical request fails
12
+ * identically.
13
+ * - **A network failure is never retried.** A send that timed out may or may not
14
+ * have raised the alert — the response is what was lost, not necessarily the
15
+ * request. Blind retries are how one condition wakes a team four times.
16
+ */
17
+
18
+ import { M91NetworkError, retryAfter, unwrap } from './exceptions.js';
19
+
20
+ export const USER_AGENT = 'm91-node/1.0.0';
21
+
22
+ /** Delays before the 2nd, 3rd and 4th attempt at a 5xx. Four requests total. */
23
+ export const SERVER_BACKOFF = [1000, 2000, 4000] as const;
24
+
25
+ export const RETRYABLE_STATUSES = new Set([500, 502, 503, 504]);
26
+
27
+ /**
28
+ * A 429 whose `Retry-After` is longer than this is thrown rather than slept
29
+ * through. The server sends 60; anything far past that is a caller sitting
30
+ * blocked in an alerting path, which is worse than an error it can see.
31
+ */
32
+ export const MAX_RETRY_AFTER_MS = 120_000;
33
+
34
+ /**
35
+ * Milliseconds to wait before trying again, or `null` to stop and throw.
36
+ * `attempt` is 1-based and counts requests already made.
37
+ */
38
+ export function retryDelay(status: number, attempt: number, retryAfterSeconds?: number): number | null {
39
+ if (status === 429) {
40
+ // Once, and only on the first 429. A second means the window has not
41
+ // actually cleared and the caller needs to hear about it.
42
+ if (attempt >= 2) return null;
43
+ const wait = (retryAfterSeconds ?? 60) * 1000;
44
+ return wait > MAX_RETRY_AFTER_MS ? null : Math.max(wait, 0);
45
+ }
46
+ if (RETRYABLE_STATUSES.has(status)) {
47
+ return attempt > SERVER_BACKOFF.length ? null : SERVER_BACKOFF[attempt - 1];
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Matches a send link anywhere — in a bare URL, or buried in a sentence — and
54
+ * takes any query string attached to it. Bounded on whitespace so it stops at
55
+ * the end of the URL rather than eating the rest of the message.
56
+ */
57
+ const SEND_URL_RE = /(\/s\/)[^\s/?#]+(\?[^\s]*)?/g;
58
+
59
+ /**
60
+ * Replace a send link's token with `***`, wherever it appears.
61
+ *
62
+ * **The token is the credential and it lives in the path**, so a URL is as
63
+ * sensitive as a password and must never reach a log, an error message or a
64
+ * stack trace intact. The server holds itself to the same rule — its error
65
+ * handler reports the route *pattern*, never `req.path`.
66
+ *
67
+ * This takes free text, not only a URL, and that is the point. Redacting only
68
+ * the URL the SDK interpolates is not enough: a fetch failure can carry the URL
69
+ * in its own message, so a carefully-redacted sentence would arrive with the
70
+ * raw token attached to it by the runtime.
71
+ *
72
+ * Query strings go too: a GET-form send carries the alert title, and titles
73
+ * routinely name customers and hosts.
74
+ */
75
+ export function redact(text: string): string {
76
+ if (typeof text !== 'string') return text;
77
+ return text.replace(SEND_URL_RE, '$1***');
78
+ }
79
+
80
+ export interface RequestOptions {
81
+ method: string;
82
+ url: string;
83
+ body?: unknown;
84
+ query?: Record<string, string>;
85
+ }
86
+
87
+ export interface TransportOptions {
88
+ /** Whole-request budget in milliseconds. Default 15s (5s connect + 10s read). */
89
+ timeoutMs?: number;
90
+ headers?: Record<string, string>;
91
+ /** Swappable so tests need no network and no mocking library. */
92
+ fetchImpl?: typeof fetch;
93
+ sleepImpl?: (ms: number) => Promise<void>;
94
+ }
95
+
96
+ const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
97
+
98
+ export class Transport {
99
+ private readonly timeoutMs: number;
100
+ private readonly headers: Record<string, string>;
101
+ private readonly fetchImpl: typeof fetch;
102
+ private readonly sleepImpl: (ms: number) => Promise<void>;
103
+
104
+ constructor(options: TransportOptions = {}) {
105
+ this.timeoutMs = options.timeoutMs ?? 15_000;
106
+ this.headers = {
107
+ // A GET-form send answers with a rendered HTML page when the caller looks
108
+ // like a browser. That page exists so a person tapping a bookmark sees
109
+ // something other than a wall of JSON — but an SDK is not a person, and
110
+ // has to opt out of it explicitly.
111
+ accept: 'application/json',
112
+ 'user-agent': USER_AGENT,
113
+ ...options.headers,
114
+ };
115
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
116
+ this.sleepImpl = options.sleepImpl ?? defaultSleep;
117
+ }
118
+
119
+ async request({ method, url, body, query }: RequestOptions): Promise<unknown> {
120
+ const target = query ? `${url}?${new URLSearchParams(query).toString()}` : url;
121
+ let attempt = 0;
122
+
123
+ for (;;) {
124
+ attempt += 1;
125
+ const controller = new AbortController();
126
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
127
+
128
+ let status: number;
129
+ let headers: Headers;
130
+ let parsed: unknown;
131
+ try {
132
+ const response = await this.fetchImpl(target, {
133
+ method,
134
+ headers: body === undefined
135
+ ? this.headers
136
+ : { ...this.headers, 'content-type': 'application/json' },
137
+ body: body === undefined ? undefined : JSON.stringify(body),
138
+ signal: controller.signal,
139
+ });
140
+ status = response.status;
141
+ headers = response.headers;
142
+ parsed = await readBody(response);
143
+ } catch (error) {
144
+ // Redacted AFTER composing, not before: a fetch failure can name the
145
+ // URL in its own message, so masking only our half still ships the token.
146
+ const reason = error instanceof Error ? error.message : String(error);
147
+ throw new M91NetworkError(redact(`Could not reach M91 at ${target}: ${reason}`));
148
+ } finally {
149
+ clearTimeout(timer);
150
+ }
151
+
152
+ const delay = retryDelay(status, attempt, retryAfter(headers));
153
+ if (delay === null) return unwrap(status, parsed, headers);
154
+ await this.sleepImpl(delay);
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * The parsed envelope, or something `unwrap` can still turn into an error.
161
+ *
162
+ * A non-JSON body here means a proxy, a load balancer or an HTML error page
163
+ * answered instead of M91. Wrapping the text in an envelope-shaped object keeps
164
+ * all the error construction in one place rather than adding a second path.
165
+ */
166
+ async function readBody(response: Response): Promise<unknown> {
167
+ const text = await response.text();
168
+ if (!text) return { error: { message: `HTTP ${response.status} with an empty body` } };
169
+ try {
170
+ return JSON.parse(text);
171
+ } catch {
172
+ return { error: { message: text.slice(0, 300) } };
173
+ }
174
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * M91 — wake a human when software cannot carry on alone.
3
+ *
4
+ * An alert rings every phone in a channel at full volume, through silent mode
5
+ * and Do Not Disturb, and keeps ringing until somebody answers. HIGH and
6
+ * CRITICAL place a voice call if 45 seconds pass with no answer. Whoever takes
7
+ * it on is recorded, and it clears for everyone else.
8
+ *
9
+ * ```ts
10
+ * import { M91Client } from '@msg91comm/m91';
11
+ *
12
+ * const m91 = new M91Client(process.env.M91_SEND_LINK!);
13
+ * await m91.verify(); // raises nothing; names the channel
14
+ * await m91.send('Payments API is down', { severity: 'CRITICAL', customId: 'payments-down' });
15
+ * await m91.close({ customId: 'payments-down' });
16
+ * ```
17
+ *
18
+ * And the reason an agent wants it — a question that blocks until a person
19
+ * answers on their phone:
20
+ *
21
+ * ```ts
22
+ * if (await m91.requestApproval('Refund 48,000 on ORD-40122?', { customId: 'refund-ORD-40122' })) {
23
+ * await processRefund();
24
+ * }
25
+ * ```
26
+ *
27
+ * Everything here is a convenience over one URL. The send link works from a
28
+ * browser bar, a cron `curl` or an `<img>` tag with no client at all, and
29
+ * `M91URLBuilder` is for exactly those.
30
+ */
31
+
32
+ export { M91Client } from './client.js';
33
+ export type { ApprovalOptions, ClientOptions, SendOptions, WaitOptions } from './client.js';
34
+
35
+ export { M91Platform, DEFAULT_BASE_URL } from './platform.js';
36
+ export type { PlatformOptions } from './platform.js';
37
+
38
+ export { M91URLBuilder, normalizeSendLink } from './url-builder.js';
39
+ export type { BuildManySpec, BuildUrlOptions } from './url-builder.js';
40
+
41
+ export { redact } from './http.js';
42
+
43
+ export {
44
+ M91Error,
45
+ M91ValidationError,
46
+ M91AuthError,
47
+ M91ForbiddenError,
48
+ M91NotFoundError,
49
+ M91ChannelEmptyError,
50
+ M91RateLimitError,
51
+ M91ServerError,
52
+ M91NetworkError,
53
+ M91DeliveryError,
54
+ M91TimeoutError,
55
+ } from './exceptions.js';
56
+
57
+ export {
58
+ SEVERITIES,
59
+ EFFECTS,
60
+ ESCALATING_SEVERITIES,
61
+ isApproved,
62
+ validatePhone,
63
+ buildAlertPayload,
64
+ } from './models.js';
65
+
66
+ export type {
67
+ AlertDetail,
68
+ AlertResponse,
69
+ AlertStatus,
70
+ ApiKeyInfo,
71
+ ChannelInfo,
72
+ Attachment,
73
+ Decision,
74
+ Response,
75
+ Effect,
76
+ InviteResult,
77
+ ProvisionedChannel,
78
+ Recipient,
79
+ RecipientState,
80
+ ResponseOption,
81
+ Severity,
82
+ } from './models.js';
83
+
84
+ /**
85
+ * Kept in step with package.json by `tests/version.test.ts`, not by hand.
86
+ *
87
+ * ESM has no portable way to read package.json at runtime, so this number is
88
+ * written twice — and it drifted, reporting 1.0.0 while the package shipped as
89
+ * 1.1.0. A test is what makes two copies safe.
90
+ */
91
+ export const VERSION = '1.2.0';
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`): M91 as the permission
3
+ * prompt, and M91 as tools.
4
+ *
5
+ * **Permission prompt.** `createM91CanUseTool` is a `canUseTool` callback: the
6
+ * tools you name ring a phone and run only if a person approves. Everything
7
+ * else is allowed through untouched.
8
+ *
9
+ * ```ts
10
+ * import { query } from '@anthropic-ai/claude-agent-sdk';
11
+ * import { M91Client } from '@msg91comm/m91';
12
+ * import { createM91CanUseTool } from '@msg91comm/m91/integrations/claude-agent-sdk';
13
+ *
14
+ * const client = new M91Client(process.env.M91_SEND_LINK!);
15
+ *
16
+ * for await (const message of query({
17
+ * prompt: 'Clean up the stale feature branches and deploy main.',
18
+ * options: { canUseTool: createM91CanUseTool(client, { tools: ['Bash', 'Write'] }) },
19
+ * })) {
20
+ * if (message.type === 'result') console.log(message);
21
+ * }
22
+ * ```
23
+ *
24
+ * `canUseTool` only runs when the SDK would otherwise ask. To gate a tool
25
+ * whatever the permission mode (including `bypassPermissions`), use the
26
+ * `PreToolUse` hook from `createM91PreToolUseHook` instead.
27
+ *
28
+ * **Tools.** `createM91McpServer` exposes `m91_request_approval` and
29
+ * `m91_send_alert` as an in-process MCP server, for when the agent itself
30
+ * should decide to ask or to wake somebody.
31
+ */
32
+
33
+ import type {
34
+ CanUseTool,
35
+ HookCallbackMatcher,
36
+ McpSdkServerConfigWithInstance,
37
+ PermissionResult,
38
+ } from '@anthropic-ai/claude-agent-sdk';
39
+
40
+ import type { M91Client } from '../client.js';
41
+ import {
42
+ ALERT_DESCRIPTION,
43
+ ALERT_TOOL_NAME,
44
+ APPROVAL_DESCRIPTION,
45
+ APPROVAL_TOOL_NAME,
46
+ type AlertArgs,
47
+ type AlertDefaults,
48
+ type ApprovalArgs,
49
+ type ApprovalDefaults,
50
+ type GateOptions,
51
+ decideToolCall,
52
+ needsHuman,
53
+ runAlert,
54
+ runApproval,
55
+ } from './specs.js';
56
+ import { alertShape, approvalShape, loadZod } from './zod-shapes.js';
57
+
58
+ /** Tool names, or a predicate. Required: an agent that rings for every `Read` gets muted. */
59
+ type ClaudeGateOptions = GateOptions & { tools: NonNullable<GateOptions['tools']> };
60
+
61
+ /**
62
+ * A `canUseTool` callback that asks a person on their phone before the tools
63
+ * in `options.tools` run.
64
+ */
65
+ export function createM91CanUseTool(client: M91Client, options: ClaudeGateOptions): CanUseTool {
66
+ return async (toolName, input, { toolUseID }): Promise<PermissionResult> => {
67
+ if (!(await needsHuman(options, toolName, input))) {
68
+ return { behavior: 'allow', updatedInput: input };
69
+ }
70
+ const decision = await decideToolCall(client, { toolName, input, callId: toolUseID }, options);
71
+ return decision.approved
72
+ ? { behavior: 'allow', updatedInput: input }
73
+ : { behavior: 'deny', message: decision.reason };
74
+ };
75
+ }
76
+
77
+ /**
78
+ * A `PreToolUse` hook matcher that asks a person before the tools in
79
+ * `options.tools` run, in every permission mode. Pass it as
80
+ * `options.hooks.PreToolUse`.
81
+ *
82
+ * ```ts
83
+ * query({ prompt, options: { hooks: { PreToolUse: createM91PreToolUseHook(client, { tools: ['Bash'] }) } } })
84
+ * ```
85
+ */
86
+ export function createM91PreToolUseHook(
87
+ client: M91Client,
88
+ options: ClaudeGateOptions & { timeoutSeconds?: number },
89
+ ): HookCallbackMatcher[] {
90
+ return [
91
+ {
92
+ // The hook must outlive the wait for a human, or the SDK gives up first.
93
+ timeout: (options.timeoutSeconds ?? 300) + 60,
94
+ hooks: [
95
+ async (input) => {
96
+ if (input.hook_event_name !== 'PreToolUse') return {};
97
+ if (!(await needsHuman(options, input.tool_name, input.tool_input))) return {};
98
+ const decision = await decideToolCall(
99
+ client,
100
+ { toolName: input.tool_name, input: input.tool_input, callId: input.tool_use_id },
101
+ options,
102
+ );
103
+ return {
104
+ hookSpecificOutput: {
105
+ hookEventName: 'PreToolUse',
106
+ permissionDecision: decision.approved ? 'allow' : 'deny',
107
+ permissionDecisionReason: decision.reason,
108
+ },
109
+ };
110
+ },
111
+ ],
112
+ },
113
+ ];
114
+ }
115
+
116
+ interface ClaudeSdkModule {
117
+ tool: (name: string, description: string, shape: unknown, handler: unknown) => unknown;
118
+ createSdkMcpServer: (options: { name: string; version?: string; tools: unknown[] }) => unknown;
119
+ }
120
+
121
+ async function claudeSdk(): Promise<ClaudeSdkModule> {
122
+ try {
123
+ const specifier = '@anthropic-ai/claude-agent-sdk';
124
+ return (await import(specifier)) as unknown as ClaudeSdkModule;
125
+ } catch (error) {
126
+ throw new Error(
127
+ 'The Claude Agent SDK integration needs @anthropic-ai/claude-agent-sdk. Install it with: ' +
128
+ 'npm install @anthropic-ai/claude-agent-sdk',
129
+ { cause: error },
130
+ );
131
+ }
132
+ }
133
+
134
+ const text = (value: string) => ({ content: [{ type: 'text' as const, text: value }] });
135
+
136
+ /**
137
+ * `m91_request_approval` and `m91_send_alert` as an in-process MCP server.
138
+ * Register it under `mcpServers.m91`; the tools appear to Claude as
139
+ * `mcp__m91__m91_request_approval` and `mcp__m91__m91_send_alert`.
140
+ */
141
+ export async function createM91McpServer(
142
+ client: M91Client,
143
+ defaults: ApprovalDefaults & AlertDefaults = {},
144
+ ): Promise<McpSdkServerConfigWithInstance> {
145
+ const { tool, createSdkMcpServer } = await claudeSdk();
146
+ const z = await loadZod('Claude Agent SDK');
147
+ return createSdkMcpServer({
148
+ name: 'm91',
149
+ tools: [
150
+ tool(APPROVAL_TOOL_NAME, APPROVAL_DESCRIPTION, approvalShape(z), async (args: ApprovalArgs) =>
151
+ text(await runApproval(client, args, defaults)),
152
+ ),
153
+ tool(ALERT_TOOL_NAME, ALERT_DESCRIPTION, alertShape(z), async (args: AlertArgs) =>
154
+ text(await runAlert(client, args, defaults)),
155
+ ),
156
+ ],
157
+ }) as McpSdkServerConfigWithInstance;
158
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Google Agent Development Kit for TypeScript (`@google/adk`): M91 as a
3
+ * before-tool gate, and M91 as tools.
4
+ *
5
+ * **Gate.** `createM91BeforeToolCallback` goes in an `LlmAgent`'s
6
+ * `beforeToolCallback`. Calls to the tools you name ring a phone first; if the
7
+ * person rejects (or does not answer), the tool does not run and the agent
8
+ * gets the reason back as the tool's result.
9
+ *
10
+ * ```ts
11
+ * import { LlmAgent } from '@google/adk';
12
+ * import { M91Client } from '@msg91comm/m91';
13
+ * import { createM91BeforeToolCallback, createM91Tools } from '@msg91comm/m91/integrations/google-adk';
14
+ *
15
+ * const client = new M91Client(process.env.M91_SEND_LINK!);
16
+ *
17
+ * const agent = new LlmAgent({
18
+ * name: 'support_agent',
19
+ * model: 'gemini-2.5-flash',
20
+ * instruction: 'Resolve refund requests.',
21
+ * tools: [issueRefund, ...(await createM91Tools(client))],
22
+ * beforeToolCallback: createM91BeforeToolCallback(client, { tools: ['issue_refund'] }),
23
+ * });
24
+ * ```
25
+ *
26
+ * The gate waits for the answer in-process. For approvals that take hours,
27
+ * use ADK's own Tool Confirmation and resume the session when M91's answer
28
+ * arrives.
29
+ */
30
+
31
+ import type { BaseTool, FunctionTool, SingleBeforeToolCallback } from '@google/adk';
32
+
33
+ import type { M91Client } from '../client.js';
34
+ import {
35
+ ALERT_DESCRIPTION,
36
+ ALERT_TOOL_NAME,
37
+ APPROVAL_DESCRIPTION,
38
+ APPROVAL_TOOL_NAME,
39
+ type AlertArgs,
40
+ type AlertDefaults,
41
+ type ApprovalArgs,
42
+ type ApprovalDefaults,
43
+ type GateOptions,
44
+ decideToolCall,
45
+ needsHuman,
46
+ runAlert,
47
+ runApproval,
48
+ } from './specs.js';
49
+ import { alertShape, approvalShape, loadZod } from './zod-shapes.js';
50
+
51
+ /**
52
+ * A `beforeToolCallback` that asks a person on their phone before the tools
53
+ * in `options.tools` run. Returning a result object skips the tool, which is
54
+ * how a rejection reaches the model.
55
+ */
56
+ export function createM91BeforeToolCallback(
57
+ client: M91Client,
58
+ options: GateOptions & { tools: NonNullable<GateOptions['tools']> },
59
+ ): SingleBeforeToolCallback {
60
+ return async ({ tool, args, context }) => {
61
+ const toolName = tool.name;
62
+ if (!(await needsHuman(options, toolName, args))) return undefined;
63
+ const callId = context.functionCallId ?? `${context.invocationId}-${toolName}`;
64
+ const decision = await decideToolCall(client, { toolName, input: args, callId }, options);
65
+ if (decision.approved) return undefined;
66
+ return { status: 'rejected', error: decision.reason };
67
+ };
68
+ }
69
+
70
+ interface AdkModule {
71
+ FunctionTool: new (options: unknown) => FunctionTool<any>;
72
+ }
73
+
74
+ async function adkModule(): Promise<AdkModule> {
75
+ try {
76
+ const specifier = '@google/adk';
77
+ return (await import(specifier)) as unknown as AdkModule;
78
+ } catch (error) {
79
+ throw new Error(
80
+ 'The Google ADK integration needs @google/adk. Install it with: npm install @google/adk',
81
+ { cause: error },
82
+ );
83
+ }
84
+ }
85
+
86
+ /** The blocking approval tool, `m91_request_approval`, as an ADK `FunctionTool`. */
87
+ export async function createM91ApprovalTool(
88
+ client: M91Client,
89
+ defaults: ApprovalDefaults = {},
90
+ ): Promise<BaseTool> {
91
+ const { FunctionTool } = await adkModule();
92
+ const z = await loadZod('Google ADK');
93
+ return new FunctionTool({
94
+ name: APPROVAL_TOOL_NAME,
95
+ description: APPROVAL_DESCRIPTION,
96
+ parameters: z.object(approvalShape(z)),
97
+ execute: async (args: ApprovalArgs) => ({ result: await runApproval(client, args, defaults) }),
98
+ });
99
+ }
100
+
101
+ /** The fire-and-forget alert tool, `m91_send_alert`, as an ADK `FunctionTool`. */
102
+ export async function createM91AlertTool(
103
+ client: M91Client,
104
+ defaults: AlertDefaults = {},
105
+ ): Promise<BaseTool> {
106
+ const { FunctionTool } = await adkModule();
107
+ const z = await loadZod('Google ADK');
108
+ return new FunctionTool({
109
+ name: ALERT_TOOL_NAME,
110
+ description: ALERT_DESCRIPTION,
111
+ parameters: z.object(alertShape(z)),
112
+ execute: async (args: AlertArgs) => ({ result: await runAlert(client, args, defaults) }),
113
+ });
114
+ }
115
+
116
+ /** Both tools, for an agent's `tools` array. */
117
+ export async function createM91Tools(
118
+ client: M91Client,
119
+ defaults: ApprovalDefaults & AlertDefaults = {},
120
+ ): Promise<BaseTool[]> {
121
+ return [await createM91ApprovalTool(client, defaults), await createM91AlertTool(client, defaults)];
122
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Framework adapters, re-exported.
3
+ *
4
+ * Each one imports its framework lazily, so `npm install @msg91comm/m91` pulls in none of
5
+ * them and using one you have not installed says which package to install
6
+ * rather than failing with a bare module-not-found from three frames down.
7
+ *
8
+ * Importing a specific adapter directly is usually better —
9
+ * `@msg91comm/m91/integrations/langchain` — because it pulls in only what you use.
10
+ *
11
+ * Several adapters export `createM91ApprovalTool`, so
12
+ * they are re-exported under namespaces rather than flattened: two functions of
13
+ * the same name doing the same job for different frameworks is exactly the pair
14
+ * that would silently resolve to the wrong one.
15
+ */
16
+
17
+ export * as langchain from './langchain.js';
18
+ export * as langgraph from './langgraph.js';
19
+ export * as vercelAi from './vercel-ai.js';
20
+ export * as openaiAgents from './openai-agents.js';
21
+ export * as claudeAgentSdk from './claude-agent-sdk.js';
22
+ export * as googleAdk from './google-adk.js';
23
+ export * as mastra from './mastra.js';
24
+
25
+ export { M91InterruptNotifier, createM91ApprovalNode } from './langgraph.js';
26
+ export { APPROVED, REJECTED, alertResult, decideToolCall, errorResult } from './specs.js';
27
+ export type { AlertArgs, ApprovalArgs, GateDecision, GateOptions, GatedCall } from './specs.js';