@msg91comm/m91 1.1.0 → 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 +35 -0
- package/README.md +241 -19
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- 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 +7 -3
- package/dist/src/integrations/index.d.ts.map +1 -1
- package/dist/src/integrations/index.js +6 -2
- package/dist/src/integrations/index.js.map +1 -1
- package/dist/src/integrations/langchain.d.ts +56 -5
- package/dist/src/integrations/langchain.d.ts.map +1 -1
- package/dist/src/integrations/langchain.js +56 -2
- package/dist/src/integrations/langchain.js.map +1 -1
- 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 +42 -0
- package/dist/src/integrations/specs.d.ts.map +1 -1
- package/dist/src/integrations/specs.js +61 -0
- package/dist/src/integrations/specs.js.map +1 -1
- package/dist/src/integrations/vercel-ai.d.ts +44 -6
- package/dist/src/integrations/vercel-ai.d.ts.map +1 -1
- package/dist/src/integrations/vercel-ai.js +38 -3
- package/dist/src/integrations/vercel-ai.js.map +1 -1
- 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/platform.d.ts +1 -1
- package/dist/src/platform.d.ts.map +1 -1
- package/dist/src/platform.js +1 -1
- package/dist/src/platform.js.map +1 -1
- package/dist/src/url-builder.js +1 -1
- package/dist/src/url-builder.js.map +1 -1
- package/examples/basic-alert.ts +1 -1
- package/examples/claude-agent-sdk.ts +23 -0
- package/examples/deep-agents.ts +31 -0
- package/examples/google-adk.ts +39 -0
- package/examples/mastra.ts +41 -0
- package/examples/openai-agents.ts +33 -0
- package/examples/webhook-urls.ts +1 -1
- package/package.json +63 -11
- package/src/index.ts +1 -1
- package/src/integrations/claude-agent-sdk.ts +158 -0
- package/src/integrations/google-adk.ts +122 -0
- package/src/integrations/index.ts +7 -3
- package/src/integrations/langchain.ts +109 -6
- package/src/integrations/mastra.ts +178 -0
- package/src/integrations/openai-agents.ts +173 -0
- package/src/integrations/specs.ts +104 -0
- package/src/integrations/vercel-ai.ts +59 -7
- package/src/integrations/zod-shapes.ts +55 -0
- package/src/platform.ts +1 -1
- package/src/url-builder.ts +1 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* await createM91AlertTool(client),
|
|
18
18
|
* ...myTools,
|
|
19
19
|
* ];
|
|
20
|
-
* const agent =
|
|
20
|
+
* const agent = createAgent({ model: 'openai:gpt-5-mini', tools }); // from 'langchain'
|
|
21
21
|
* ```
|
|
22
22
|
*
|
|
23
23
|
* The factories are **async** because `@langchain/core` is an optional peer
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* to work when everything else is on fire.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
|
30
|
+
|
|
29
31
|
import type { M91Client } from '../client.js';
|
|
30
32
|
import {
|
|
31
33
|
ALERT_DESCRIPTION,
|
|
@@ -38,6 +40,8 @@ import {
|
|
|
38
40
|
type AlertDefaults,
|
|
39
41
|
type ApprovalArgs,
|
|
40
42
|
type ApprovalDefaults,
|
|
43
|
+
type GateOptions,
|
|
44
|
+
decideToolCall,
|
|
41
45
|
runAlert,
|
|
42
46
|
runApproval,
|
|
43
47
|
} from './specs.js';
|
|
@@ -67,13 +71,13 @@ async function langchainTool(): Promise<(fn: unknown, config: unknown) => unknow
|
|
|
67
71
|
export async function createM91ApprovalTool(
|
|
68
72
|
client: M91Client,
|
|
69
73
|
defaults: ApprovalDefaults = {},
|
|
70
|
-
): Promise<
|
|
74
|
+
): Promise<DynamicStructuredTool> {
|
|
71
75
|
const tool = await langchainTool();
|
|
72
76
|
return tool((args: ApprovalArgs) => runApproval(client, args, defaults), {
|
|
73
77
|
name: APPROVAL_TOOL_NAME,
|
|
74
78
|
description: APPROVAL_DESCRIPTION,
|
|
75
79
|
schema: APPROVAL_SCHEMA,
|
|
76
|
-
});
|
|
80
|
+
}) as DynamicStructuredTool;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
/**
|
|
@@ -83,22 +87,121 @@ export async function createM91ApprovalTool(
|
|
|
83
87
|
export async function createM91AlertTool(
|
|
84
88
|
client: M91Client,
|
|
85
89
|
defaults: AlertDefaults = {},
|
|
86
|
-
): Promise<
|
|
90
|
+
): Promise<DynamicStructuredTool> {
|
|
87
91
|
const tool = await langchainTool();
|
|
88
92
|
return tool((args: AlertArgs) => runAlert(client, args, defaults), {
|
|
89
93
|
name: ALERT_TOOL_NAME,
|
|
90
94
|
description: ALERT_DESCRIPTION,
|
|
91
95
|
schema: ALERT_SCHEMA,
|
|
92
|
-
});
|
|
96
|
+
}) as DynamicStructuredTool;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
/** Both, in the order an agent's `tools` array usually wants them. */
|
|
96
100
|
export async function createM91Tools(
|
|
97
101
|
client: M91Client,
|
|
98
102
|
defaults: ApprovalDefaults & AlertDefaults = {},
|
|
99
|
-
): Promise<
|
|
103
|
+
): Promise<DynamicStructuredTool[]> {
|
|
100
104
|
return [
|
|
101
105
|
await createM91ApprovalTool(client, defaults),
|
|
102
106
|
await createM91AlertTool(client, defaults),
|
|
103
107
|
];
|
|
104
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,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
|
+
}
|
|
@@ -222,3 +222,107 @@ export async function runAlert(
|
|
|
222
222
|
return errorResult(error);
|
|
223
223
|
}
|
|
224
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
|
+
}
|