@msg91comm/m91 0.0.0-stage → 1.1.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 (70) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +425 -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/index.d.ts +22 -0
  21. package/dist/src/integrations/index.d.ts.map +1 -0
  22. package/dist/src/integrations/index.js +21 -0
  23. package/dist/src/integrations/index.js.map +1 -0
  24. package/dist/src/integrations/langchain.d.ts +43 -0
  25. package/dist/src/integrations/langchain.d.ts.map +1 -0
  26. package/dist/src/integrations/langchain.js +75 -0
  27. package/dist/src/integrations/langchain.js.map +1 -0
  28. package/dist/src/integrations/langgraph.d.ts +79 -0
  29. package/dist/src/integrations/langgraph.d.ts.map +1 -0
  30. package/dist/src/integrations/langgraph.js +114 -0
  31. package/dist/src/integrations/langgraph.js.map +1 -0
  32. package/dist/src/integrations/specs.d.ts +131 -0
  33. package/dist/src/integrations/specs.d.ts.map +1 -0
  34. package/dist/src/integrations/specs.js +156 -0
  35. package/dist/src/integrations/specs.js.map +1 -0
  36. package/dist/src/integrations/vercel-ai.d.ts +36 -0
  37. package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
  38. package/dist/src/integrations/vercel-ai.js +67 -0
  39. package/dist/src/integrations/vercel-ai.js.map +1 -0
  40. package/dist/src/models.d.ts +274 -0
  41. package/dist/src/models.d.ts.map +1 -0
  42. package/dist/src/models.js +383 -0
  43. package/dist/src/models.js.map +1 -0
  44. package/dist/src/platform.d.ts +176 -0
  45. package/dist/src/platform.d.ts.map +1 -0
  46. package/dist/src/platform.js +286 -0
  47. package/dist/src/platform.js.map +1 -0
  48. package/dist/src/url-builder.d.ts +65 -0
  49. package/dist/src/url-builder.d.ts.map +1 -0
  50. package/dist/src/url-builder.js +167 -0
  51. package/dist/src/url-builder.js.map +1 -0
  52. package/examples/approval-gate.ts +35 -0
  53. package/examples/basic-alert.ts +53 -0
  54. package/examples/langchain-agent.ts +65 -0
  55. package/examples/langgraph-hitl.ts +70 -0
  56. package/examples/vercel-ai-agent.ts +34 -0
  57. package/examples/webhook-urls.ts +44 -0
  58. package/package.json +83 -4
  59. package/src/client.ts +324 -0
  60. package/src/exceptions.ts +229 -0
  61. package/src/http.ts +174 -0
  62. package/src/index.ts +91 -0
  63. package/src/integrations/index.ts +23 -0
  64. package/src/integrations/langchain.ts +104 -0
  65. package/src/integrations/langgraph.ts +142 -0
  66. package/src/integrations/specs.ts +224 -0
  67. package/src/integrations/vercel-ai.ts +99 -0
  68. package/src/models.ts +660 -0
  69. package/src/platform.ts +343 -0
  70. package/src/url-builder.ts +225 -0
@@ -0,0 +1,224 @@
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
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Vercel AI SDK tools.
3
+ *
4
+ * ```ts
5
+ * import { M91Client } from '@msg91comm/m91';
6
+ * import { createM91ApprovalTool, createM91AlertTool } from '@msg91comm/m91/integrations/vercel-ai';
7
+ *
8
+ * const client = new M91Client(process.env.M91_SEND_LINK!);
9
+ *
10
+ * const { text } = await generateText({
11
+ * model: openai('gpt-4o'),
12
+ * tools: {
13
+ * m91_request_approval: await createM91ApprovalTool(client),
14
+ * m91_send_alert: await createM91AlertTool(client),
15
+ * },
16
+ * maxSteps: 10,
17
+ * prompt: 'Look into order ORD-40122 and refund it if it checks out.',
18
+ * });
19
+ * ```
20
+ *
21
+ * The factories are **async** because `ai` is an optional peer dependency,
22
+ * imported only when one of these is called — `npm install @msg91comm/m91` stays at zero
23
+ * dependencies.
24
+ */
25
+
26
+ import type { M91Client } from '../client.js';
27
+ import {
28
+ ALERT_DESCRIPTION,
29
+ ALERT_SCHEMA,
30
+ APPROVAL_DESCRIPTION,
31
+ APPROVAL_SCHEMA,
32
+ type AlertArgs,
33
+ type AlertDefaults,
34
+ type ApprovalArgs,
35
+ type ApprovalDefaults,
36
+ runAlert,
37
+ runApproval,
38
+ } from './specs.js';
39
+
40
+ interface AiModule {
41
+ tool: (definition: unknown) => unknown;
42
+ jsonSchema: (schema: unknown) => unknown;
43
+ }
44
+
45
+ async function aiModule(): Promise<AiModule> {
46
+ try {
47
+ // A variable specifier: an optional peer dependency must not be a build
48
+ // requirement for this package.
49
+ const specifier = 'ai';
50
+ return (await import(specifier)) as unknown as AiModule;
51
+ } catch (error) {
52
+ throw new Error(
53
+ 'The Vercel AI SDK integration needs the `ai` package. Install it with: npm install ai',
54
+ { cause: error },
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The blocking approval gate. Returns `"approved"`, `"rejected"` or
61
+ * `"error: …"`, and nothing but a real human approval returns `"approved"`.
62
+ */
63
+ export async function createM91ApprovalTool(
64
+ client: M91Client,
65
+ defaults: ApprovalDefaults = {},
66
+ ): Promise<unknown> {
67
+ const { tool, jsonSchema } = await aiModule();
68
+ return tool({
69
+ description: APPROVAL_DESCRIPTION,
70
+ // JSON Schema rather than zod, so this adapter adds no dependency of its own
71
+ // and the argument docs stay in one file shared with every other framework.
72
+ inputSchema: jsonSchema(APPROVAL_SCHEMA),
73
+ execute: (args: ApprovalArgs) => runApproval(client, args, defaults),
74
+ });
75
+ }
76
+
77
+ /** The fire-and-forget alert. Returns `"sent:delivered=N"` or `"error: …"`. */
78
+ export async function createM91AlertTool(
79
+ client: M91Client,
80
+ defaults: AlertDefaults = {},
81
+ ): Promise<unknown> {
82
+ const { tool, jsonSchema } = await aiModule();
83
+ return tool({
84
+ description: ALERT_DESCRIPTION,
85
+ inputSchema: jsonSchema(ALERT_SCHEMA),
86
+ execute: (args: AlertArgs) => runAlert(client, args, defaults),
87
+ });
88
+ }
89
+
90
+ /** Both, keyed by tool name, ready to spread into `tools: {…}`. */
91
+ export async function createM91Tools(
92
+ client: M91Client,
93
+ defaults: ApprovalDefaults & AlertDefaults = {},
94
+ ): Promise<Record<string, unknown>> {
95
+ return {
96
+ m91_request_approval: await createM91ApprovalTool(client, defaults),
97
+ m91_send_alert: await createM91AlertTool(client, defaults),
98
+ };
99
+ }