@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.
- package/CHANGELOG.md +94 -0
- package/LICENSE +21 -0
- package/README.md +647 -2
- package/dist/src/client.d.ts +165 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +221 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/exceptions.d.ts +114 -0
- package/dist/src/exceptions.d.ts.map +1 -0
- package/dist/src/exceptions.js +199 -0
- package/dist/src/exceptions.js.map +1 -0
- package/dist/src/http.d.ts +71 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +150 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +49 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +45 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/claude-agent-sdk.d.ts +63 -0
- package/dist/src/integrations/claude-agent-sdk.d.ts.map +1 -0
- package/dist/src/integrations/claude-agent-sdk.js +109 -0
- package/dist/src/integrations/claude-agent-sdk.js.map +1 -0
- package/dist/src/integrations/google-adk.d.ts +47 -0
- package/dist/src/integrations/google-adk.d.ts.map +1 -0
- package/dist/src/integrations/google-adk.js +84 -0
- package/dist/src/integrations/google-adk.js.map +1 -0
- package/dist/src/integrations/index.d.ts +26 -0
- package/dist/src/integrations/index.d.ts.map +1 -0
- package/dist/src/integrations/index.js +25 -0
- package/dist/src/integrations/index.js.map +1 -0
- package/dist/src/integrations/langchain.d.ts +94 -0
- package/dist/src/integrations/langchain.d.ts.map +1 -0
- package/dist/src/integrations/langchain.js +129 -0
- package/dist/src/integrations/langchain.js.map +1 -0
- package/dist/src/integrations/langgraph.d.ts +79 -0
- package/dist/src/integrations/langgraph.d.ts.map +1 -0
- package/dist/src/integrations/langgraph.js +114 -0
- package/dist/src/integrations/langgraph.js.map +1 -0
- package/dist/src/integrations/mastra.d.ts +75 -0
- package/dist/src/integrations/mastra.d.ts.map +1 -0
- package/dist/src/integrations/mastra.js +109 -0
- package/dist/src/integrations/mastra.js.map +1 -0
- package/dist/src/integrations/openai-agents.d.ts +61 -0
- package/dist/src/integrations/openai-agents.d.ts.map +1 -0
- package/dist/src/integrations/openai-agents.js +127 -0
- package/dist/src/integrations/openai-agents.js.map +1 -0
- package/dist/src/integrations/specs.d.ts +173 -0
- package/dist/src/integrations/specs.d.ts.map +1 -0
- package/dist/src/integrations/specs.js +217 -0
- package/dist/src/integrations/specs.js.map +1 -0
- package/dist/src/integrations/vercel-ai.d.ts +74 -0
- package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
- package/dist/src/integrations/vercel-ai.js +102 -0
- package/dist/src/integrations/vercel-ai.js.map +1 -0
- package/dist/src/integrations/zod-shapes.d.ts +19 -0
- package/dist/src/integrations/zod-shapes.d.ts.map +1 -0
- package/dist/src/integrations/zod-shapes.js +44 -0
- package/dist/src/integrations/zod-shapes.js.map +1 -0
- package/dist/src/models.d.ts +274 -0
- package/dist/src/models.d.ts.map +1 -0
- package/dist/src/models.js +383 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/platform.d.ts +176 -0
- package/dist/src/platform.d.ts.map +1 -0
- package/dist/src/platform.js +286 -0
- package/dist/src/platform.js.map +1 -0
- package/dist/src/url-builder.d.ts +65 -0
- package/dist/src/url-builder.d.ts.map +1 -0
- package/dist/src/url-builder.js +167 -0
- package/dist/src/url-builder.js.map +1 -0
- package/examples/approval-gate.ts +35 -0
- package/examples/basic-alert.ts +53 -0
- package/examples/claude-agent-sdk.ts +23 -0
- package/examples/deep-agents.ts +31 -0
- package/examples/google-adk.ts +39 -0
- package/examples/langchain-agent.ts +65 -0
- package/examples/langgraph-hitl.ts +70 -0
- package/examples/mastra.ts +41 -0
- package/examples/openai-agents.ts +33 -0
- package/examples/vercel-ai-agent.ts +34 -0
- package/examples/webhook-urls.ts +44 -0
- package/package.json +135 -4
- package/src/client.ts +324 -0
- package/src/exceptions.ts +229 -0
- package/src/http.ts +174 -0
- package/src/index.ts +91 -0
- package/src/integrations/claude-agent-sdk.ts +158 -0
- package/src/integrations/google-adk.ts +122 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/langchain.ts +207 -0
- package/src/integrations/langgraph.ts +142 -0
- package/src/integrations/mastra.ts +178 -0
- package/src/integrations/openai-agents.ts +173 -0
- package/src/integrations/specs.ts +328 -0
- package/src/integrations/vercel-ai.ts +151 -0
- package/src/integrations/zod-shapes.ts +55 -0
- package/src/models.ts +660 -0
- package/src/platform.ts +343 -0
- package/src/url-builder.ts +225 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain tools.
|
|
3
|
+
*
|
|
4
|
+
* Two of them, and the split matters more than it looks:
|
|
5
|
+
*
|
|
6
|
+
* - the approval tool **blocks**. The agent stops until a human answers on
|
|
7
|
+
* their phone. Give it to an agent that can do something irreversible.
|
|
8
|
+
* - the alert tool does not. It wakes somebody and carries on.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { M91Client } from '@msg91comm/m91';
|
|
12
|
+
* import { createM91ApprovalTool, createM91AlertTool } from '@msg91comm/m91/integrations/langchain';
|
|
13
|
+
*
|
|
14
|
+
* const client = new M91Client(process.env.M91_SEND_LINK!);
|
|
15
|
+
* const tools = [
|
|
16
|
+
* await createM91ApprovalTool(client),
|
|
17
|
+
* await createM91AlertTool(client),
|
|
18
|
+
* ...myTools,
|
|
19
|
+
* ];
|
|
20
|
+
* const agent = createAgent({ model: 'openai:gpt-5-mini', tools }); // from 'langchain'
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The factories are **async** because `@langchain/core` is an optional peer
|
|
24
|
+
* dependency, imported only when one of these is called. That keeps `npm
|
|
25
|
+
* install @msg91comm/m91` at zero dependencies, which matters for a package whose job is
|
|
26
|
+
* to work when everything else is on fire.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
|
30
|
+
|
|
31
|
+
import type { M91Client } from '../client.js';
|
|
32
|
+
import {
|
|
33
|
+
ALERT_DESCRIPTION,
|
|
34
|
+
ALERT_SCHEMA,
|
|
35
|
+
ALERT_TOOL_NAME,
|
|
36
|
+
APPROVAL_DESCRIPTION,
|
|
37
|
+
APPROVAL_SCHEMA,
|
|
38
|
+
APPROVAL_TOOL_NAME,
|
|
39
|
+
type AlertArgs,
|
|
40
|
+
type AlertDefaults,
|
|
41
|
+
type ApprovalArgs,
|
|
42
|
+
type ApprovalDefaults,
|
|
43
|
+
type GateOptions,
|
|
44
|
+
decideToolCall,
|
|
45
|
+
runAlert,
|
|
46
|
+
runApproval,
|
|
47
|
+
} from './specs.js';
|
|
48
|
+
|
|
49
|
+
async function langchainTool(): Promise<(fn: unknown, config: unknown) => unknown> {
|
|
50
|
+
try {
|
|
51
|
+
// The specifier is a variable so TypeScript does not try to resolve an
|
|
52
|
+
// optional peer dependency at build time. Compiling this package must not
|
|
53
|
+
// require every framework it can adapt to.
|
|
54
|
+
const specifier = '@langchain/core/tools';
|
|
55
|
+
const mod = (await import(specifier)) as { tool: (fn: unknown, config: unknown) => unknown };
|
|
56
|
+
return mod.tool;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'The LangChain integration needs @langchain/core. Install it with: ' +
|
|
60
|
+
'npm install @langchain/core',
|
|
61
|
+
{ cause: error },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The blocking approval gate, as a LangChain tool named `m91_request_approval`.
|
|
68
|
+
* Returns `"approved"`, `"rejected"` or `"error: …"` — and nothing but a real
|
|
69
|
+
* human approval returns `"approved"`.
|
|
70
|
+
*/
|
|
71
|
+
export async function createM91ApprovalTool(
|
|
72
|
+
client: M91Client,
|
|
73
|
+
defaults: ApprovalDefaults = {},
|
|
74
|
+
): Promise<DynamicStructuredTool> {
|
|
75
|
+
const tool = await langchainTool();
|
|
76
|
+
return tool((args: ApprovalArgs) => runApproval(client, args, defaults), {
|
|
77
|
+
name: APPROVAL_TOOL_NAME,
|
|
78
|
+
description: APPROVAL_DESCRIPTION,
|
|
79
|
+
schema: APPROVAL_SCHEMA,
|
|
80
|
+
}) as DynamicStructuredTool;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The fire-and-forget alert, as a LangChain tool named `m91_send_alert`.
|
|
85
|
+
* Returns `"sent:delivered=N"` or `"error: …"`.
|
|
86
|
+
*/
|
|
87
|
+
export async function createM91AlertTool(
|
|
88
|
+
client: M91Client,
|
|
89
|
+
defaults: AlertDefaults = {},
|
|
90
|
+
): Promise<DynamicStructuredTool> {
|
|
91
|
+
const tool = await langchainTool();
|
|
92
|
+
return tool((args: AlertArgs) => runAlert(client, args, defaults), {
|
|
93
|
+
name: ALERT_TOOL_NAME,
|
|
94
|
+
description: ALERT_DESCRIPTION,
|
|
95
|
+
schema: ALERT_SCHEMA,
|
|
96
|
+
}) as DynamicStructuredTool;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Both, in the order an agent's `tools` array usually wants them. */
|
|
100
|
+
export async function createM91Tools(
|
|
101
|
+
client: M91Client,
|
|
102
|
+
defaults: ApprovalDefaults & AlertDefaults = {},
|
|
103
|
+
): Promise<DynamicStructuredTool[]> {
|
|
104
|
+
return [
|
|
105
|
+
await createM91ApprovalTool(client, defaults),
|
|
106
|
+
await createM91AlertTool(client, defaults),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ------------------------------------------------------------------------ */
|
|
111
|
+
/* humanInTheLoopMiddleware / Deep Agents: answering the interrupt */
|
|
112
|
+
/* ------------------------------------------------------------------------ */
|
|
113
|
+
|
|
114
|
+
/** One tool call the middleware paused on, as `HITLRequest.actionRequests` carries it. */
|
|
115
|
+
interface ActionRequestLike {
|
|
116
|
+
name: string;
|
|
117
|
+
args: Record<string, unknown>;
|
|
118
|
+
description?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The payload of the interrupt `humanInTheLoopMiddleware` (and Deep Agents' `interruptOn`) raises. */
|
|
122
|
+
export interface HITLRequestLike {
|
|
123
|
+
actionRequests: ActionRequestLike[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** What the middleware expects back: one decision per action request, in order. */
|
|
127
|
+
export interface M91HITLResponse {
|
|
128
|
+
decisions: Array<{ type: 'approve' } | { type: 'reject'; message: string }>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isHITLRequest(value: unknown): value is HITLRequestLike {
|
|
132
|
+
return Array.isArray((value as HITLRequestLike | undefined)?.actionRequests);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Ask a person about every tool call in a `HITLRequest`, one phone alert per
|
|
137
|
+
* call, and build the `{ decisions }` the middleware resumes with.
|
|
138
|
+
*
|
|
139
|
+
* `"approved"` / `"rejected"` strings (what `M91InterruptNotifier` returns) are
|
|
140
|
+
* **not** a valid resume value for the middleware; this is.
|
|
141
|
+
*
|
|
142
|
+
* `threadId` makes each alert's customId stable across a retry of the same
|
|
143
|
+
* pause, so a retried resume does not ring twice.
|
|
144
|
+
*/
|
|
145
|
+
export async function resolveHITLWithM91(
|
|
146
|
+
request: HITLRequestLike,
|
|
147
|
+
client: M91Client,
|
|
148
|
+
options: GateOptions & { threadId?: string } = {},
|
|
149
|
+
): Promise<M91HITLResponse> {
|
|
150
|
+
const decisions: M91HITLResponse['decisions'] = [];
|
|
151
|
+
for (const [index, action] of request.actionRequests.entries()) {
|
|
152
|
+
const callId = `${options.threadId ?? 'run'}-${index}`;
|
|
153
|
+
const decision = await decideToolCall(
|
|
154
|
+
client,
|
|
155
|
+
{ toolName: action.name, input: action.args, callId },
|
|
156
|
+
options,
|
|
157
|
+
);
|
|
158
|
+
decisions.push(decision.approved ? { type: 'approve' } : { type: 'reject', message: decision.reason });
|
|
159
|
+
}
|
|
160
|
+
return { decisions };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The parts of a LangChain agent / Deep Agent / compiled graph this file calls. */
|
|
164
|
+
export interface InvokableAgentLike {
|
|
165
|
+
invoke(input: any, config?: any): Promise<any>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `agent.invoke()`, with every `humanInTheLoopMiddleware` pause answered on a
|
|
170
|
+
* phone through M91 and resumed with `Command({ resume: { decisions } })`.
|
|
171
|
+
* Works for `createAgent` with `humanInTheLoopMiddleware` and for Deep Agents
|
|
172
|
+
* created with `interruptOn`. Needs a checkpointer and a `thread_id`.
|
|
173
|
+
*/
|
|
174
|
+
export async function invokeWithM91Approvals<TResult = any>(
|
|
175
|
+
agent: InvokableAgentLike,
|
|
176
|
+
input: unknown,
|
|
177
|
+
config: { configurable: { thread_id: string } } & Record<string, unknown>,
|
|
178
|
+
client: M91Client,
|
|
179
|
+
options: GateOptions & { maxRounds?: number } = {},
|
|
180
|
+
): Promise<TResult> {
|
|
181
|
+
const { Command } = await langgraphModule();
|
|
182
|
+
const { maxRounds = 20, ...gate } = options;
|
|
183
|
+
let result = await agent.invoke(input, config);
|
|
184
|
+
for (let round = 0; round < maxRounds; round += 1) {
|
|
185
|
+
const pending = (result?.__interrupt__ as Array<{ value: unknown }> | undefined)?.[0]?.value;
|
|
186
|
+
if (!isHITLRequest(pending)) break;
|
|
187
|
+
const response = await resolveHITLWithM91(pending, client, {
|
|
188
|
+
...gate,
|
|
189
|
+
threadId: `${config.configurable.thread_id}-${round}`,
|
|
190
|
+
});
|
|
191
|
+
result = await agent.invoke(new Command({ resume: response }), config);
|
|
192
|
+
}
|
|
193
|
+
return result as TResult;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function langgraphModule(): Promise<{ Command: new (options: { resume: unknown }) => unknown }> {
|
|
197
|
+
try {
|
|
198
|
+
const specifier = '@langchain/langgraph';
|
|
199
|
+
return (await import(specifier)) as { Command: new (options: { resume: unknown }) => unknown };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
'Resuming a LangChain / Deep Agents interrupt needs @langchain/langgraph. Install it with: ' +
|
|
203
|
+
'npm install @langchain/langgraph',
|
|
204
|
+
{ cause: error },
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangGraph helpers for the `interrupt()` pattern.
|
|
3
|
+
*
|
|
4
|
+
* LangGraph already knows how to stop a graph and resume it. What it has no
|
|
5
|
+
* opinion about is **how the human finds out it stopped** — and the default
|
|
6
|
+
* answer is that somebody notices a paused run in a web UI. At 2am, for a graph
|
|
7
|
+
* that paused because it wants to move money, nobody does. M91 fills exactly
|
|
8
|
+
* that gap: the graph pauses, a phone rings until somebody answers, and the
|
|
9
|
+
* answer resumes the graph.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here imports `@langchain/langgraph`. These are plain callables you use
|
|
12
|
+
* *around* your graph, so they work the same with `interrupt()`, with
|
|
13
|
+
* `new Command({ resume })`, or with a node you wrote by hand.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { M91Client } from '../client.js';
|
|
17
|
+
import type { Severity } from '../models.js';
|
|
18
|
+
import { APPROVED, REJECTED } from './specs.js';
|
|
19
|
+
|
|
20
|
+
export interface NotifyOptions {
|
|
21
|
+
title: string;
|
|
22
|
+
customId: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
severity?: Severity;
|
|
25
|
+
approveLabel?: string;
|
|
26
|
+
rejectLabel?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type NotifyDefaults = Omit<NotifyOptions, 'title' | 'customId'>;
|
|
32
|
+
|
|
33
|
+
export type Verdict = typeof APPROVED | typeof REJECTED;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wake a human about an interrupted graph and return what they said.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const notifier = new M91InterruptNotifier(client);
|
|
40
|
+
*
|
|
41
|
+
* const result = await graph.invoke(state, config);
|
|
42
|
+
* if (result.__interrupt__) {
|
|
43
|
+
* const data = result.__interrupt__[0].value;
|
|
44
|
+
* const decision = await notifier.notify({
|
|
45
|
+
* title: data.question,
|
|
46
|
+
* customId: data.customId,
|
|
47
|
+
* timeoutMs: 600_000,
|
|
48
|
+
* });
|
|
49
|
+
* await graph.invoke(new Command({ resume: decision }), config);
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Resolves `"approved"` or `"rejected"` and **never anything else** — a failure
|
|
54
|
+
* to reach anybody comes back as `"rejected"`, because a graph that resumes
|
|
55
|
+
* with an ambiguous value resumes with whatever the node's `if` happens to do
|
|
56
|
+
* with it.
|
|
57
|
+
*/
|
|
58
|
+
export class M91InterruptNotifier {
|
|
59
|
+
private readonly client: M91Client;
|
|
60
|
+
private readonly defaults: NotifyDefaults;
|
|
61
|
+
|
|
62
|
+
constructor(client: M91Client, defaults: NotifyDefaults = {}) {
|
|
63
|
+
this.client = client;
|
|
64
|
+
this.defaults = defaults;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async notify(options: NotifyOptions): Promise<Verdict> {
|
|
68
|
+
const { title, customId, ...rest } = options;
|
|
69
|
+
const merged = { ...this.defaults, ...strip(rest) };
|
|
70
|
+
try {
|
|
71
|
+
const approved = await this.client.requestApproval(title, {
|
|
72
|
+
customId,
|
|
73
|
+
severity: 'HIGH',
|
|
74
|
+
timeoutMs: 300_000,
|
|
75
|
+
...merged,
|
|
76
|
+
});
|
|
77
|
+
return approved ? APPROVED : REJECTED;
|
|
78
|
+
} catch {
|
|
79
|
+
// Nobody could be reached, so nobody approved. Resuming a graph with
|
|
80
|
+
// "approved" because the pager was down is the one outcome worth ruling
|
|
81
|
+
// out by construction.
|
|
82
|
+
return REJECTED;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build a graph node that asks a human and writes the answer into state.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* builder.addNode('humanApproval', createM91ApprovalNode(client, {
|
|
92
|
+
* approveLabel: 'Deploy Now',
|
|
93
|
+
* rejectLabel: 'Hold Release',
|
|
94
|
+
* timeoutMs: 600_000,
|
|
95
|
+
* }));
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* The node reads `title` (or `question`) and `customId` from state and returns
|
|
99
|
+
* `{ decision, approved }` for the next edge to branch on. Anything passed here
|
|
100
|
+
* is a default the state can override per run.
|
|
101
|
+
*/
|
|
102
|
+
export function createM91ApprovalNode(
|
|
103
|
+
client: M91Client,
|
|
104
|
+
defaults: NotifyDefaults = {},
|
|
105
|
+
): (state: unknown) => Promise<{ decision: Verdict; approved: boolean; error?: string }> {
|
|
106
|
+
const notifier = new M91InterruptNotifier(client, defaults);
|
|
107
|
+
|
|
108
|
+
return async (state: unknown) => {
|
|
109
|
+
const data = (state && typeof state === 'object' ? state : {}) as Record<string, unknown>;
|
|
110
|
+
const title = data.title ?? data.question;
|
|
111
|
+
const customId = data.customId ?? data.custom_id;
|
|
112
|
+
|
|
113
|
+
if (!title || !customId) {
|
|
114
|
+
// Said loudly rather than defaulted. A node that invents a customId is a
|
|
115
|
+
// node whose alerts never deduplicate and never close.
|
|
116
|
+
return {
|
|
117
|
+
decision: REJECTED,
|
|
118
|
+
approved: false,
|
|
119
|
+
error:
|
|
120
|
+
"error: the approval node needs 'title' (or 'question') and 'customId' in state",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const decision = await notifier.notify({
|
|
125
|
+
title: String(title),
|
|
126
|
+
customId: String(customId),
|
|
127
|
+
...strip({
|
|
128
|
+
description: data.description as string | undefined,
|
|
129
|
+
severity: data.severity as Severity | undefined,
|
|
130
|
+
approveLabel: data.approveLabel as string | undefined,
|
|
131
|
+
rejectLabel: data.rejectLabel as string | undefined,
|
|
132
|
+
timeoutMs: data.timeoutMs as number | undefined,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
return { decision, approved: decision === APPROVED };
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Drop undefined keys so a spread cannot blank out a default with `undefined`. */
|
|
140
|
+
function strip<T extends Record<string, unknown>>(value: T): Partial<T> {
|
|
141
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>;
|
|
142
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra (`@mastra/core`): M91 as the approver for `requireApproval` tools,
|
|
3
|
+
* and M91 as tools.
|
|
4
|
+
*
|
|
5
|
+
* **Approver.** Mark your risky tools `requireApproval: true`. Mastra suspends
|
|
6
|
+
* the run when the agent calls one; `generateWithM91Approvals` rings a phone
|
|
7
|
+
* for each suspended call and approves or declines it with the person's
|
|
8
|
+
* answer until the run finishes.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Agent } from '@mastra/core/agent';
|
|
12
|
+
* import { createTool } from '@mastra/core/tools';
|
|
13
|
+
* import { M91Client } from '@msg91comm/m91';
|
|
14
|
+
* import { generateWithM91Approvals } from '@msg91comm/m91/integrations/mastra';
|
|
15
|
+
*
|
|
16
|
+
* const issueRefund = createTool({
|
|
17
|
+
* id: 'issue-refund',
|
|
18
|
+
* description: 'Refund an order.',
|
|
19
|
+
* inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
|
|
20
|
+
* requireApproval: true,
|
|
21
|
+
* execute: async ({ orderId, amount }) => refunds.create(orderId, amount),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const agent = new Agent({ id: 'support', name: 'Support', instructions: '…',
|
|
25
|
+
* model: 'openai/gpt-5-mini', tools: { issueRefund } });
|
|
26
|
+
* const client = new M91Client(process.env.M91_SEND_LINK!);
|
|
27
|
+
*
|
|
28
|
+
* const output = await generateWithM91Approvals(agent, 'Refund ORD-40122.', client);
|
|
29
|
+
* console.log(output.text);
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Mastra keeps suspended runs in its storage, so configure a storage provider
|
|
33
|
+
* on your `Mastra` instance for approvals to resume.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { Tool } from '@mastra/core/tools';
|
|
37
|
+
|
|
38
|
+
import type { M91Client } from '../client.js';
|
|
39
|
+
import {
|
|
40
|
+
ALERT_DESCRIPTION,
|
|
41
|
+
ALERT_TOOL_NAME,
|
|
42
|
+
APPROVAL_DESCRIPTION,
|
|
43
|
+
APPROVAL_TOOL_NAME,
|
|
44
|
+
type AlertArgs,
|
|
45
|
+
type AlertDefaults,
|
|
46
|
+
type ApprovalArgs,
|
|
47
|
+
type ApprovalDefaults,
|
|
48
|
+
type GateOptions,
|
|
49
|
+
decideToolCall,
|
|
50
|
+
needsHuman,
|
|
51
|
+
runAlert,
|
|
52
|
+
runApproval,
|
|
53
|
+
} from './specs.js';
|
|
54
|
+
import { alertShape, approvalShape, loadZod } from './zod-shapes.js';
|
|
55
|
+
|
|
56
|
+
/** The parts of a Mastra `generate()` result this file reads. */
|
|
57
|
+
export interface MastraOutputLike {
|
|
58
|
+
text: string;
|
|
59
|
+
finishReason?: unknown;
|
|
60
|
+
runId?: string;
|
|
61
|
+
suspendPayload?: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The parts of a Mastra `Agent` this file calls. */
|
|
65
|
+
export interface MastraAgentLike<TOutput extends MastraOutputLike = MastraOutputLike> {
|
|
66
|
+
generate(prompt: any, options?: any): Promise<TOutput>;
|
|
67
|
+
approveToolCallGenerate(options: { runId: string; toolCallId?: string }): Promise<TOutput>;
|
|
68
|
+
declineToolCallGenerate(options: {
|
|
69
|
+
runId: string;
|
|
70
|
+
toolCallId?: string;
|
|
71
|
+
reason?: string;
|
|
72
|
+
}): Promise<TOutput>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PendingApproval {
|
|
76
|
+
toolCallId: string;
|
|
77
|
+
toolName: string;
|
|
78
|
+
args: unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pendingApproval(output: MastraOutputLike): PendingApproval | null {
|
|
82
|
+
if (output.finishReason !== 'suspended' || !output.runId) return null;
|
|
83
|
+
const payload = output.suspendPayload as Partial<PendingApproval> | undefined;
|
|
84
|
+
if (!payload?.toolCallId || !payload.toolName) return null;
|
|
85
|
+
return { toolCallId: payload.toolCallId, toolName: payload.toolName, args: payload.args };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `agent.generate()`, with every tool approval the run suspends for answered
|
|
90
|
+
* on a phone through M91. Approvals for tools outside `options.tools` are
|
|
91
|
+
* returned still suspended, for your own code.
|
|
92
|
+
*/
|
|
93
|
+
export async function generateWithM91Approvals<TOutput extends MastraOutputLike>(
|
|
94
|
+
agent: MastraAgentLike<TOutput>,
|
|
95
|
+
prompt: unknown,
|
|
96
|
+
client: M91Client,
|
|
97
|
+
options: GateOptions & { generateOptions?: Record<string, unknown>; maxRounds?: number } = {},
|
|
98
|
+
): Promise<TOutput> {
|
|
99
|
+
const { generateOptions, maxRounds = 20, ...gate } = options;
|
|
100
|
+
let output = await agent.generate(prompt, generateOptions);
|
|
101
|
+
for (let round = 0; round < maxRounds; round += 1) {
|
|
102
|
+
const pending = pendingApproval(output);
|
|
103
|
+
if (!pending || !(await needsHuman(gate, pending.toolName, pending.args))) break;
|
|
104
|
+
const runId = output.runId as string;
|
|
105
|
+
const decision = await decideToolCall(
|
|
106
|
+
client,
|
|
107
|
+
{ toolName: pending.toolName, input: pending.args, callId: pending.toolCallId },
|
|
108
|
+
gate,
|
|
109
|
+
);
|
|
110
|
+
output = decision.approved
|
|
111
|
+
? await agent.approveToolCallGenerate({ runId, toolCallId: pending.toolCallId })
|
|
112
|
+
: await agent.declineToolCallGenerate({
|
|
113
|
+
runId,
|
|
114
|
+
toolCallId: pending.toolCallId,
|
|
115
|
+
reason: decision.reason,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return output;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface MastraToolsModule {
|
|
122
|
+
createTool: (options: unknown) => unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function mastraTools(): Promise<MastraToolsModule> {
|
|
126
|
+
try {
|
|
127
|
+
const specifier = '@mastra/core/tools';
|
|
128
|
+
return (await import(specifier)) as unknown as MastraToolsModule;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error('The Mastra integration needs @mastra/core. Install it with: npm install @mastra/core', {
|
|
131
|
+
cause: error,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A Mastra tool, typed loosely enough to drop into any agent's `tools`. */
|
|
137
|
+
export type M91MastraTool = Tool<any, any, any, any, any, string, any>;
|
|
138
|
+
|
|
139
|
+
/** The blocking approval tool, `m91_request_approval`, as a Mastra tool. */
|
|
140
|
+
export async function createM91ApprovalTool(
|
|
141
|
+
client: M91Client,
|
|
142
|
+
defaults: ApprovalDefaults = {},
|
|
143
|
+
): Promise<M91MastraTool> {
|
|
144
|
+
const { createTool } = await mastraTools();
|
|
145
|
+
const z = await loadZod('Mastra');
|
|
146
|
+
return createTool({
|
|
147
|
+
id: APPROVAL_TOOL_NAME,
|
|
148
|
+
description: APPROVAL_DESCRIPTION,
|
|
149
|
+
inputSchema: z.object(approvalShape(z)),
|
|
150
|
+
execute: async (args: ApprovalArgs) => ({ result: await runApproval(client, args, defaults) }),
|
|
151
|
+
}) as M91MastraTool;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The fire-and-forget alert tool, `m91_send_alert`, as a Mastra tool. */
|
|
155
|
+
export async function createM91AlertTool(
|
|
156
|
+
client: M91Client,
|
|
157
|
+
defaults: AlertDefaults = {},
|
|
158
|
+
): Promise<M91MastraTool> {
|
|
159
|
+
const { createTool } = await mastraTools();
|
|
160
|
+
const z = await loadZod('Mastra');
|
|
161
|
+
return createTool({
|
|
162
|
+
id: ALERT_TOOL_NAME,
|
|
163
|
+
description: ALERT_DESCRIPTION,
|
|
164
|
+
inputSchema: z.object(alertShape(z)),
|
|
165
|
+
execute: async (args: AlertArgs) => ({ result: await runAlert(client, args, defaults) }),
|
|
166
|
+
}) as M91MastraTool;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Both tools, keyed for an `Agent`'s `tools` object. */
|
|
170
|
+
export async function createM91Tools(
|
|
171
|
+
client: M91Client,
|
|
172
|
+
defaults: ApprovalDefaults & AlertDefaults = {},
|
|
173
|
+
): Promise<Record<string, M91MastraTool>> {
|
|
174
|
+
return {
|
|
175
|
+
[APPROVAL_TOOL_NAME]: await createM91ApprovalTool(client, defaults),
|
|
176
|
+
[ALERT_TOOL_NAME]: await createM91AlertTool(client, defaults),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Agents SDK (`@openai/agents`): two tools, and M91 as the approver.
|
|
3
|
+
*
|
|
4
|
+
* **Approver.** Mark your risky tools `needsApproval: true`. The SDK pauses the
|
|
5
|
+
* run and returns `result.interruptions`; `runWithM91Approvals` rings a phone
|
|
6
|
+
* for each one, approves or rejects it on the run state with the person's
|
|
7
|
+
* answer, and resumes the same run until it finishes.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Agent, tool } from '@openai/agents';
|
|
11
|
+
* import { M91Client } from '@msg91comm/m91';
|
|
12
|
+
* import { runWithM91Approvals } from '@msg91comm/m91/integrations/openai-agents';
|
|
13
|
+
*
|
|
14
|
+
* const issueRefund = tool({
|
|
15
|
+
* name: 'issue_refund',
|
|
16
|
+
* description: 'Refund an order.',
|
|
17
|
+
* parameters: z.object({ orderId: z.string(), amount: z.number() }),
|
|
18
|
+
* needsApproval: true,
|
|
19
|
+
* execute: async ({ orderId, amount }) => refunds.create(orderId, amount),
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const agent = new Agent({ name: 'Support', instructions: '…', tools: [issueRefund] });
|
|
23
|
+
* const client = new M91Client(process.env.M91_SEND_LINK!);
|
|
24
|
+
*
|
|
25
|
+
* const result = await runWithM91Approvals(agent, 'Refund ORD-40122, it never arrived.', client);
|
|
26
|
+
* console.log(result.finalOutput);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* **Tools** (`createM91Tools`) are for when the model itself should decide to
|
|
30
|
+
* ask, or to wake somebody about something that happened.
|
|
31
|
+
*
|
|
32
|
+
* `@openai/agents` is an optional peer dependency, imported only when one of
|
|
33
|
+
* these is called.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { Agent, FunctionTool, RunResult, RunState, RunToolApprovalItem } from '@openai/agents';
|
|
37
|
+
|
|
38
|
+
import type { M91Client } from '../client.js';
|
|
39
|
+
import {
|
|
40
|
+
ALERT_DESCRIPTION,
|
|
41
|
+
ALERT_SCHEMA,
|
|
42
|
+
ALERT_TOOL_NAME,
|
|
43
|
+
APPROVAL_DESCRIPTION,
|
|
44
|
+
APPROVAL_SCHEMA,
|
|
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
|
+
|
|
57
|
+
interface AgentsModule {
|
|
58
|
+
tool: (options: unknown) => unknown;
|
|
59
|
+
run: (agent: unknown, input: unknown, options?: unknown) => Promise<unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function agentsModule(): Promise<AgentsModule> {
|
|
63
|
+
try {
|
|
64
|
+
const specifier = '@openai/agents';
|
|
65
|
+
return (await import(specifier)) as unknown as AgentsModule;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'The OpenAI Agents integration needs @openai/agents. Install it with: npm install @openai/agents',
|
|
69
|
+
{ cause: error },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The blocking approval tool, `m91_request_approval`. Returns `"approved"`, `"rejected"` or `"error: …"`. */
|
|
75
|
+
export async function createM91ApprovalTool(
|
|
76
|
+
client: M91Client,
|
|
77
|
+
defaults: ApprovalDefaults = {},
|
|
78
|
+
): Promise<FunctionTool> {
|
|
79
|
+
const { tool } = await agentsModule();
|
|
80
|
+
return tool({
|
|
81
|
+
name: APPROVAL_TOOL_NAME,
|
|
82
|
+
description: APPROVAL_DESCRIPTION,
|
|
83
|
+
// JSON Schema with optional fields: the non-strict mode accepts it as is.
|
|
84
|
+
parameters: APPROVAL_SCHEMA,
|
|
85
|
+
strict: false,
|
|
86
|
+
execute: (args: ApprovalArgs) => runApproval(client, args, defaults),
|
|
87
|
+
}) as FunctionTool;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The fire-and-forget alert tool, `m91_send_alert`. Returns `"sent:delivered=N"` or `"error: …"`. */
|
|
91
|
+
export async function createM91AlertTool(
|
|
92
|
+
client: M91Client,
|
|
93
|
+
defaults: AlertDefaults = {},
|
|
94
|
+
): Promise<FunctionTool> {
|
|
95
|
+
const { tool } = await agentsModule();
|
|
96
|
+
return tool({
|
|
97
|
+
name: ALERT_TOOL_NAME,
|
|
98
|
+
description: ALERT_DESCRIPTION,
|
|
99
|
+
parameters: ALERT_SCHEMA,
|
|
100
|
+
strict: false,
|
|
101
|
+
execute: (args: AlertArgs) => runAlert(client, args, defaults),
|
|
102
|
+
}) as FunctionTool;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Both tools, for an agent's `tools` array. */
|
|
106
|
+
export async function createM91Tools(
|
|
107
|
+
client: M91Client,
|
|
108
|
+
defaults: ApprovalDefaults & AlertDefaults = {},
|
|
109
|
+
): Promise<FunctionTool[]> {
|
|
110
|
+
return [await createM91ApprovalTool(client, defaults), await createM91AlertTool(client, defaults)];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseArguments(item: RunToolApprovalItem): unknown {
|
|
114
|
+
const raw = item.arguments;
|
|
115
|
+
if (raw === undefined) return undefined;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
} catch {
|
|
119
|
+
return raw;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function callIdOf(item: RunToolApprovalItem): string {
|
|
124
|
+
const raw = item.rawItem as { callId?: string; id?: string };
|
|
125
|
+
return raw.callId ?? raw.id ?? `${item.name ?? 'tool'}-${item.arguments ?? ''}`.slice(0, 60);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ask a person about every pending approval on a paused run, and record the
|
|
130
|
+
* answers on its state. Returns how many were decided; resume with
|
|
131
|
+
* `run(agent, result.state)`.
|
|
132
|
+
*
|
|
133
|
+
* Interruptions for tools outside `options.tools` are left pending for your
|
|
134
|
+
* own code to decide.
|
|
135
|
+
*/
|
|
136
|
+
export async function approveInterruptionsWithM91(
|
|
137
|
+
result: Pick<RunResult<any, any>, 'interruptions' | 'state'>,
|
|
138
|
+
client: M91Client,
|
|
139
|
+
options: GateOptions = {},
|
|
140
|
+
): Promise<number> {
|
|
141
|
+
const state = result.state as RunState<any, any>;
|
|
142
|
+
let decided = 0;
|
|
143
|
+
for (const item of result.interruptions ?? []) {
|
|
144
|
+
const toolName = item.name ?? 'tool';
|
|
145
|
+
const input = parseArguments(item);
|
|
146
|
+
if (!(await needsHuman(options, toolName, input))) continue;
|
|
147
|
+
const decision = await decideToolCall(client, { toolName, input, callId: callIdOf(item) }, options);
|
|
148
|
+
if (decision.approved) state.approve(item);
|
|
149
|
+
else state.reject(item, { message: decision.reason });
|
|
150
|
+
decided += 1;
|
|
151
|
+
}
|
|
152
|
+
return decided;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `run()`, with every approval the run pauses for answered on a phone through
|
|
157
|
+
* M91, until the run finishes or nothing left to decide is M91's.
|
|
158
|
+
*/
|
|
159
|
+
export async function runWithM91Approvals<TAgent extends Agent<any, any>>(
|
|
160
|
+
agent: TAgent,
|
|
161
|
+
input: string | Parameters<typeof import('@openai/agents').run>[1],
|
|
162
|
+
client: M91Client,
|
|
163
|
+
options: GateOptions & { runOptions?: Record<string, unknown>; maxRounds?: number } = {},
|
|
164
|
+
): Promise<RunResult<any, TAgent>> {
|
|
165
|
+
const { run } = await agentsModule();
|
|
166
|
+
const { runOptions, maxRounds = 20, ...gate } = options;
|
|
167
|
+
let result = (await run(agent, input, runOptions)) as RunResult<any, TAgent>;
|
|
168
|
+
for (let round = 0; round < maxRounds && (result.interruptions?.length ?? 0) > 0; round += 1) {
|
|
169
|
+
if ((await approveInterruptionsWithM91(result, client, gate)) === 0) break;
|
|
170
|
+
result = (await run(agent, result.state, runOptions)) as RunResult<any, TAgent>;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|