@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
@@ -0,0 +1,328 @@
1
+ /**
2
+ * The tool definitions, written once and adapted per framework.
3
+ *
4
+ * A tool's name, its description and its argument docs are the entire interface
5
+ * a model has to M91 — they are the prompt. Four adapters each spelling their
6
+ * own copy is four descriptions that drift, and the drift is invisible until an
7
+ * agent on one framework behaves differently from the same agent on another.
8
+ *
9
+ * Every adapter therefore imports the schema and the `run` from here and does
10
+ * nothing but wrap it in the shape its framework expects.
11
+ */
12
+
13
+ import type { M91Client } from '../client.js';
14
+ import { M91Error } from '../exceptions.js';
15
+ import { redact } from '../http.js';
16
+ import type { AlertResponse, Severity } from '../models.js';
17
+
18
+ export const APPROVED = 'approved';
19
+ export const REJECTED = 'rejected';
20
+
21
+ /**
22
+ * `error: …` — never `approved`, whatever went wrong.
23
+ *
24
+ * The message is redacted: an M91 error can carry the URL it failed on, and
25
+ * that URL has the send token in its path. A tool result goes straight into a
26
+ * model's context and from there into logs, traces and evals.
27
+ */
28
+ export function errorResult(error: unknown): string {
29
+ if (error instanceof M91Error) return `error: [${error.code}] ${redact(error.message)}`;
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ return `error: ${redact(message) || 'unknown failure'}`;
32
+ }
33
+
34
+ /**
35
+ * `sent:delivered=N` — plus a word when nobody was actually reached.
36
+ *
37
+ * A model that reads "sent" and stops has no way to know the channel's phones
38
+ * were all stale. Saying so in the result is the only channel back to it.
39
+ */
40
+ export function alertResult(alert: AlertResponse): string {
41
+ if (alert.deduplicated) {
42
+ return (
43
+ 'sent:delivered=0 (an alert with this customId is already open and already ringing — ' +
44
+ 'nobody was disturbed a second time)'
45
+ );
46
+ }
47
+ if (alert.delivered === 0) {
48
+ return (
49
+ `sent:delivered=0 WARNING: the alert was filed but no phone could be reached ` +
50
+ `(unregistered=${alert.unregistered.length}, noDevice=${alert.noDevice}, ` +
51
+ `pending=${alert.pending}). Nobody has been woken.`
52
+ );
53
+ }
54
+ return `sent:delivered=${alert.delivered}`;
55
+ }
56
+
57
+ const SEVERITY_DOC =
58
+ 'LOW, MEDIUM, HIGH or CRITICAL. All of them ring through Do Not Disturb; HIGH and ' +
59
+ 'CRITICAL also place a voice call after 45 seconds of silence.';
60
+
61
+ const CUSTOM_ID_DOC =
62
+ 'A stable id for this exact request, derived from what is being decided ' +
63
+ "('refund-ORD-40122'). NEVER a timestamp, a UUID or a counter: the id is what " +
64
+ 'deduplicates a retry and what closes the alert afterwards.';
65
+
66
+ /** JSON Schema rather than zod, so nothing here needs a dependency to exist. */
67
+ export const APPROVAL_SCHEMA = {
68
+ type: 'object',
69
+ properties: {
70
+ title: {
71
+ type: 'string',
72
+ description:
73
+ 'The question, as the person reads it at 3am on a lock screen. Name the action ' +
74
+ "and its consequence: 'Refund 48,000 on ORD-40122?' — not 'approve action'. " +
75
+ '5-2000 characters.',
76
+ },
77
+ customId: { type: 'string', description: CUSTOM_ID_DOC },
78
+ description: {
79
+ type: 'string',
80
+ description:
81
+ 'The context the person needs to answer without asking you anything back: what ' +
82
+ 'you found, what you are about to do, and what happens if they say no.',
83
+ },
84
+ severity: {
85
+ type: 'string',
86
+ enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'],
87
+ description: `${SEVERITY_DOC} An approval that blocks you is normally HIGH.`,
88
+ },
89
+ approveLabel: { type: 'string', description: 'The yes button, 24 characters or fewer.' },
90
+ rejectLabel: { type: 'string', description: 'The no button, 24 characters or fewer.' },
91
+ timeoutSeconds: {
92
+ type: 'number',
93
+ description: 'How long to wait for an answer before giving up. Default 300.',
94
+ },
95
+ },
96
+ required: ['title', 'customId'],
97
+ additionalProperties: false,
98
+ } as const;
99
+
100
+ export const ALERT_SCHEMA = {
101
+ type: 'object',
102
+ properties: {
103
+ title: {
104
+ type: 'string',
105
+ description: 'What happened, in one line somebody can act on. 5-2000 characters.',
106
+ },
107
+ customId: {
108
+ type: 'string',
109
+ description:
110
+ "A stable id for the CONDITION, not this occurrence — 'disk-full-node7'. Repeats " +
111
+ 'while it is still open wake nobody a second time. Never a timestamp.',
112
+ },
113
+ description: { type: 'string', description: 'Any detail that helps them act.' },
114
+ severity: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'], description: SEVERITY_DOC },
115
+ },
116
+ required: ['title'],
117
+ additionalProperties: false,
118
+ } as const;
119
+
120
+ export const APPROVAL_TOOL_NAME = 'm91_request_approval';
121
+ export const ALERT_TOOL_NAME = 'm91_send_alert';
122
+
123
+ export const APPROVAL_DESCRIPTION =
124
+ 'Ask a human for permission and wait for their answer. Use this BEFORE any ' +
125
+ 'irreversible or consequential action: payments, refunds, deletions, deployments, ' +
126
+ "sending messages on someone's behalf, changing production data, or anything you " +
127
+ 'would want a colleague to sign off on. ' +
128
+ 'The person is woken on their phone immediately at full volume, through silent mode ' +
129
+ 'and Do Not Disturb, and called if they do not answer within 45 seconds. ' +
130
+ 'This call BLOCKS until they respond or the timeout passes. ' +
131
+ "Returns 'approved' or 'rejected'. Treat anything that is not exactly 'approved' as " +
132
+ 'a refusal and do not perform the action.';
133
+
134
+ export const ALERT_DESCRIPTION =
135
+ 'Wake a human immediately about something that has already happened, without waiting ' +
136
+ 'for a reply. Use for failures, anomalies, breaches, and long jobs finishing that ' +
137
+ 'somebody is waiting on. ' +
138
+ 'The alert rings every phone on the on-call channel at full volume through silent ' +
139
+ 'mode and Do Not Disturb, and keeps ringing until somebody answers. ' +
140
+ 'Do NOT use this to ask permission — use m91_request_approval, which waits. ' +
141
+ 'Do NOT use it for anything that could wait until morning; this wakes people up. ' +
142
+ "Returns 'sent:delivered=N', where N is how many phones were actually reached.";
143
+
144
+ export interface ApprovalArgs {
145
+ title: string;
146
+ customId: string;
147
+ description?: string;
148
+ severity?: Severity;
149
+ approveLabel?: string;
150
+ rejectLabel?: string;
151
+ timeoutSeconds?: number;
152
+ }
153
+
154
+ export interface AlertArgs {
155
+ title: string;
156
+ customId?: string;
157
+ description?: string;
158
+ severity?: Severity;
159
+ }
160
+
161
+ /**
162
+ * What a deployment wants when the model does not say.
163
+ *
164
+ * Set these where the tool is built, not in the prompt. How long a person has
165
+ * to wake up, and what the buttons say, are operations decisions — a model
166
+ * choosing its own timeout is a model deciding how long somebody has to answer.
167
+ * It can still override per call when it has a reason.
168
+ */
169
+ export interface ApprovalDefaults {
170
+ severity?: Severity;
171
+ approveLabel?: string;
172
+ rejectLabel?: string;
173
+ timeoutSeconds?: number;
174
+ }
175
+
176
+ export interface AlertDefaults {
177
+ severity?: Severity;
178
+ }
179
+
180
+ /** Ask, wait, and answer in a word a model cannot misread. */
181
+ export async function runApproval(
182
+ client: M91Client,
183
+ args: ApprovalArgs,
184
+ defaults: ApprovalDefaults = {},
185
+ ): Promise<string> {
186
+ try {
187
+ const approved = await client.requestApproval(args.title, {
188
+ customId: args.customId,
189
+ description: args.description,
190
+ severity: args.severity ?? defaults.severity ?? 'HIGH',
191
+ approveLabel: args.approveLabel ?? defaults.approveLabel ?? 'Approve',
192
+ rejectLabel: args.rejectLabel ?? defaults.rejectLabel ?? 'Reject',
193
+ // `??`, not `||`: an explicit 0 means "do not wait", and `||` would turn
194
+ // that into five minutes.
195
+ timeoutMs: (args.timeoutSeconds ?? defaults.timeoutSeconds ?? 300) * 1000,
196
+ });
197
+ return approved ? APPROVED : REJECTED;
198
+ } catch (error) {
199
+ // Returned, not thrown: a thrown tool error ends the agent run, and an
200
+ // agent that cannot ask permission should be able to say so and stop.
201
+ return errorResult(error);
202
+ }
203
+ }
204
+
205
+ export async function runAlert(
206
+ client: M91Client,
207
+ args: AlertArgs,
208
+ defaults: AlertDefaults = {},
209
+ ): Promise<string> {
210
+ try {
211
+ return alertResult(
212
+ await client.send(args.title, {
213
+ description: args.description,
214
+ severity: args.severity ?? defaults.severity ?? 'MEDIUM',
215
+ customId: args.customId,
216
+ }),
217
+ );
218
+ } catch (error) {
219
+ // Includes M91DeliveryError, whose message already says nobody's phone
220
+ // rang. A model reading "error:" and retrying is right there; a model
221
+ // reading "sent" and stopping is not.
222
+ return errorResult(error);
223
+ }
224
+ }
225
+
226
+ /* ------------------------------------------------------------------------ */
227
+ /* Gating a framework's own tool calls */
228
+ /* ------------------------------------------------------------------------ */
229
+
230
+ /**
231
+ * One tool call a framework is about to make, in the only three fields every
232
+ * framework agrees on.
233
+ */
234
+ export interface GatedCall {
235
+ toolName: string;
236
+ input: unknown;
237
+ /** The framework's id for this call. Becomes the alert's customId. */
238
+ callId: string;
239
+ }
240
+
241
+ /** How a gate decides who gets asked, and how the question reads. */
242
+ export interface GateOptions extends ApprovalDefaults {
243
+ /**
244
+ * Which tool calls need a human: a list of tool names, or a predicate.
245
+ *
246
+ * Required where a framework routes every call through the gate (Claude
247
+ * Agent SDK, Google ADK, Vercel `toolApproval`): ringing somebody's phone
248
+ * for every file read is how a channel gets muted.
249
+ */
250
+ tools?: readonly string[] | ((toolName: string, input: unknown) => boolean | Promise<boolean>);
251
+ /** Write the lock-screen question yourself. Default: "Allow <tool>?" plus the arguments. */
252
+ describe?: (call: GatedCall) => { title: string; description?: string };
253
+ }
254
+
255
+ export interface GateDecision {
256
+ approved: boolean;
257
+ /** Why, in words a model can repeat to the user. */
258
+ reason: string;
259
+ }
260
+
261
+ /** Does this call need a human? With no `tools`, every call that reaches the gate does. */
262
+ export async function needsHuman(
263
+ options: GateOptions,
264
+ toolName: string,
265
+ input: unknown,
266
+ ): Promise<boolean> {
267
+ const { tools } = options;
268
+ if (tools === undefined) return true;
269
+ if (typeof tools === 'function') return Boolean(await tools(toolName, input));
270
+ return tools.includes(toolName);
271
+ }
272
+
273
+ /** A customId M91 accepts, stable across a framework's retries of the same call. */
274
+ export function gateCustomId(call: GatedCall): string {
275
+ const raw = `tool-${call.toolName}-${call.callId}`;
276
+ return raw.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 100);
277
+ }
278
+
279
+ function defaultQuestion(call: GatedCall): { title: string; description: string } {
280
+ let args: string;
281
+ try {
282
+ args = JSON.stringify(call.input, null, 2) ?? '';
283
+ } catch {
284
+ args = String(call.input);
285
+ }
286
+ if (args.length > 1500) args = `${args.slice(0, 1500)}\n… (truncated)`;
287
+ return {
288
+ title: `Allow the agent to run ${call.toolName}?`,
289
+ description: `The agent wants to call ${call.toolName} with:\n${args}`,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Ring a phone about a tool call and wait for the answer.
295
+ *
296
+ * Fails closed: an M91 error, an unreachable channel or a timeout is a no,
297
+ * with the reason spelled out, never a silent yes.
298
+ */
299
+ export async function decideToolCall(
300
+ client: M91Client,
301
+ call: GatedCall,
302
+ options: GateOptions = {},
303
+ ): Promise<GateDecision> {
304
+ const question = options.describe ? options.describe(call) : defaultQuestion(call);
305
+ try {
306
+ const approved = await client.requestApproval(question.title, {
307
+ customId: gateCustomId(call),
308
+ description: question.description,
309
+ severity: options.severity ?? 'HIGH',
310
+ approveLabel: options.approveLabel ?? 'Approve',
311
+ rejectLabel: options.rejectLabel ?? 'Reject',
312
+ timeoutMs: (options.timeoutSeconds ?? 300) * 1000,
313
+ });
314
+ return approved
315
+ ? { approved: true, reason: 'Approved by a human via M91.' }
316
+ : {
317
+ approved: false,
318
+ reason:
319
+ 'A human rejected this action via M91 (or did not answer in time). ' +
320
+ 'Do not retry it; tell the user it was not approved.',
321
+ };
322
+ } catch (error) {
323
+ return {
324
+ approved: false,
325
+ reason: `Could not get human approval via M91, so the action was not run. ${errorResult(error)}`,
326
+ };
327
+ }
328
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Vercel AI SDK: two tools, and a gate for your own tools.
3
+ *
4
+ * **Tools** the model calls when it decides to ask (`createM91Tools`).
5
+ *
6
+ * **Gate** (`createM91ToolApproval`) plugs into AI SDK 6+'s `toolApproval`
7
+ * option: every call to the tools you name rings a phone first, and runs only
8
+ * if a person approves. The model cannot skip it, because it never decides to
9
+ * ask: the SDK asks for it.
10
+ *
11
+ * ```ts
12
+ * const { text } = await generateText({
13
+ * model: 'openai/gpt-5-mini',
14
+ * tools: { issueRefund, deleteUser },
15
+ * toolApproval: createM91ToolApproval(client, { tools: ['issueRefund', 'deleteUser'] }),
16
+ * stopWhen: isStepCount(5),
17
+ * prompt: 'Refund order ORD-40122.',
18
+ * });
19
+ * ```
20
+ *
21
+ * ```ts
22
+ * import { M91Client } from '@msg91comm/m91';
23
+ * import { createM91ApprovalTool, createM91AlertTool } from '@msg91comm/m91/integrations/vercel-ai';
24
+ *
25
+ * const client = new M91Client(process.env.M91_SEND_LINK!);
26
+ *
27
+ * const { text } = await generateText({
28
+ * model: openai('gpt-4o'),
29
+ * tools: {
30
+ * m91_request_approval: await createM91ApprovalTool(client),
31
+ * m91_send_alert: await createM91AlertTool(client),
32
+ * },
33
+ * stopWhen: isStepCount(10),
34
+ * prompt: 'Look into order ORD-40122 and refund it if it checks out.',
35
+ * });
36
+ * ```
37
+ *
38
+ * The factories are **async** because `ai` is an optional peer dependency,
39
+ * imported only when one of these is called — `npm install @msg91comm/m91` stays at zero
40
+ * dependencies.
41
+ */
42
+
43
+ import type { Tool, ToolApprovalStatus, ToolSet } from 'ai';
44
+
45
+ import type { M91Client } from '../client.js';
46
+ import {
47
+ ALERT_DESCRIPTION,
48
+ ALERT_SCHEMA,
49
+ APPROVAL_DESCRIPTION,
50
+ APPROVAL_SCHEMA,
51
+ type AlertArgs,
52
+ type AlertDefaults,
53
+ type ApprovalArgs,
54
+ type ApprovalDefaults,
55
+ type GateOptions,
56
+ decideToolCall,
57
+ needsHuman,
58
+ runAlert,
59
+ runApproval,
60
+ } from './specs.js';
61
+
62
+ interface AiModule {
63
+ tool: (definition: unknown) => unknown;
64
+ jsonSchema: (schema: unknown) => unknown;
65
+ }
66
+
67
+ async function aiModule(): Promise<AiModule> {
68
+ try {
69
+ // A variable specifier: an optional peer dependency must not be a build
70
+ // requirement for this package.
71
+ const specifier = 'ai';
72
+ return (await import(specifier)) as unknown as AiModule;
73
+ } catch (error) {
74
+ throw new Error(
75
+ 'The Vercel AI SDK integration needs the `ai` package. Install it with: npm install ai',
76
+ { cause: error },
77
+ );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The blocking approval gate. Returns `"approved"`, `"rejected"` or
83
+ * `"error: …"`, and nothing but a real human approval returns `"approved"`.
84
+ */
85
+ export async function createM91ApprovalTool(
86
+ client: M91Client,
87
+ defaults: ApprovalDefaults = {},
88
+ ): Promise<Tool<ApprovalArgs, string>> {
89
+ const { tool, jsonSchema } = await aiModule();
90
+ return tool({
91
+ description: APPROVAL_DESCRIPTION,
92
+ // JSON Schema rather than zod, so this adapter adds no dependency of its own
93
+ // and the argument docs stay in one file shared with every other framework.
94
+ inputSchema: jsonSchema(APPROVAL_SCHEMA),
95
+ execute: (args: ApprovalArgs) => runApproval(client, args, defaults),
96
+ }) as Tool<ApprovalArgs, string>;
97
+ }
98
+
99
+ /** The fire-and-forget alert. Returns `"sent:delivered=N"` or `"error: …"`. */
100
+ export async function createM91AlertTool(
101
+ client: M91Client,
102
+ defaults: AlertDefaults = {},
103
+ ): Promise<Tool<AlertArgs, string>> {
104
+ const { tool, jsonSchema } = await aiModule();
105
+ return tool({
106
+ description: ALERT_DESCRIPTION,
107
+ inputSchema: jsonSchema(ALERT_SCHEMA),
108
+ execute: (args: AlertArgs) => runAlert(client, args, defaults),
109
+ }) as Tool<AlertArgs, string>;
110
+ }
111
+
112
+ /** Both, keyed by tool name, ready to spread into `tools: {…}`. */
113
+ export async function createM91Tools(
114
+ client: M91Client,
115
+ defaults: ApprovalDefaults & AlertDefaults = {},
116
+ ): Promise<ToolSet> {
117
+ return {
118
+ m91_request_approval: await createM91ApprovalTool(client, defaults),
119
+ m91_send_alert: await createM91AlertTool(client, defaults),
120
+ };
121
+ }
122
+
123
+ /** The fields of an AI SDK tool call the gate reads. */
124
+ interface ApprovalRequestLike {
125
+ toolCall: { toolName: string; input: unknown; toolCallId: string };
126
+ }
127
+
128
+ /**
129
+ * A `toolApproval` function for `generateText` / `streamText` / `ToolLoopAgent`.
130
+ *
131
+ * Calls to the tools in `options.tools` block while M91 rings a phone, then run
132
+ * if a person approves and come back to the model as denied otherwise. Every
133
+ * other call is `not-applicable` and runs as normal. Not synchronous: it waits
134
+ * up to `timeoutSeconds` (default 300) for an answer.
135
+ */
136
+ export function createM91ToolApproval(
137
+ client: M91Client,
138
+ options: GateOptions & { tools: NonNullable<GateOptions['tools']> },
139
+ ): (request: ApprovalRequestLike) => Promise<ToolApprovalStatus> {
140
+ return async ({ toolCall }) => {
141
+ if (!(await needsHuman(options, toolCall.toolName, toolCall.input))) return 'not-applicable';
142
+ const decision = await decideToolCall(
143
+ client,
144
+ { toolName: toolCall.toolName, input: toolCall.input, callId: toolCall.toolCallId },
145
+ options,
146
+ );
147
+ return decision.approved
148
+ ? { type: 'approved', reason: decision.reason }
149
+ : { type: 'denied', reason: decision.reason };
150
+ };
151
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The approval and alert arguments as zod shapes, for frameworks that only
3
+ * take zod (Claude Agent SDK, Mastra, Google ADK).
4
+ *
5
+ * Built from the zod the caller already has, passed in, so this package never
6
+ * imports zod itself and the descriptions stay the ones in `specs.ts`.
7
+ */
8
+
9
+ import { ALERT_SCHEMA, APPROVAL_SCHEMA } from './specs.js';
10
+
11
+ /** The slice of zod this file uses; v3 and v4 both satisfy it. */
12
+ export interface ZodLike {
13
+ string(): any;
14
+ number(): any;
15
+ enum(values: readonly [string, ...string[]]): any;
16
+ object(shape: Record<string, any>): any;
17
+ }
18
+
19
+ const SEVERITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;
20
+
21
+ export function approvalShape(z: ZodLike): Record<string, any> {
22
+ const p = APPROVAL_SCHEMA.properties;
23
+ return {
24
+ title: z.string().describe(p.title.description),
25
+ customId: z.string().describe(p.customId.description),
26
+ description: z.string().optional().describe(p.description.description),
27
+ severity: z.enum(SEVERITIES).optional().describe(p.severity.description),
28
+ approveLabel: z.string().optional().describe(p.approveLabel.description),
29
+ rejectLabel: z.string().optional().describe(p.rejectLabel.description),
30
+ timeoutSeconds: z.number().optional().describe(p.timeoutSeconds.description),
31
+ };
32
+ }
33
+
34
+ export function alertShape(z: ZodLike): Record<string, any> {
35
+ const p = ALERT_SCHEMA.properties;
36
+ return {
37
+ title: z.string().describe(p.title.description),
38
+ customId: z.string().optional().describe(p.customId.description),
39
+ description: z.string().optional().describe(p.description.description),
40
+ severity: z.enum(SEVERITIES).optional().describe(p.severity.description),
41
+ };
42
+ }
43
+
44
+ /** zod is an optional peer dependency: import it only when asked, and say so if it is missing. */
45
+ export async function loadZod(framework: string): Promise<ZodLike> {
46
+ try {
47
+ const specifier = 'zod';
48
+ const mod = (await import(specifier)) as { z?: ZodLike } & ZodLike;
49
+ return mod.z ?? mod;
50
+ } catch (error) {
51
+ throw new Error(`The ${framework} integration needs zod. Install it with: npm install zod`, {
52
+ cause: error,
53
+ });
54
+ }
55
+ }