manychat-ai-agent 0.17.0 → 0.18.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 +28 -0
- package/dist/agent/prompt.d.ts +5 -1
- package/dist/agent/prompt.js +13 -2
- package/dist/agent/runner.d.ts +3 -0
- package/dist/agent/runner.js +4 -2
- package/dist/agent/tools.d.ts +3 -0
- package/dist/agent/tools.js +3 -0
- package/dist/channels/manychat/client.js +3 -0
- package/dist/cli/run.js +20 -2
- package/dist/contracts/agent.d.ts +10 -1
- package/dist/contracts/agent.js +16 -2
- package/dist/conversation/actions.js +7 -1
- package/dist/conversation/store.d.ts +2 -2
- package/dist/db/schema.d.ts +2 -2
- package/dist/evals/runner.js +5 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/main.js +7 -1
- package/dist/outbox/worker.d.ts +4 -0
- package/dist/outbox/worker.js +4 -2
- package/dist/plugins/api.d.ts +103 -0
- package/dist/plugins/api.js +21 -0
- package/dist/plugins/loader.d.ts +20 -0
- package/dist/plugins/loader.js +267 -0
- package/dist/plugins/plugins.d.ts +49 -0
- package/dist/plugins/plugins.js +157 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +5 -1
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.0](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.17.1...v0.18.0) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* finish plugin tools, and move plugin channels to spec 038 (spec 036) ([712c860](https://github.com/pedronastasi/manychat-ai-agent/commit/712c8603a291da1b0af3a980d30d2b0958e84d7d))
|
|
9
|
+
* finish plugin tools, and move plugin channels to spec 038 (spec 036) ([1908609](https://github.com/pedronastasi/manychat-ai-agent/commit/190860916fffaea797571226647672a2a2b5751d))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Specs and Docs
|
|
13
|
+
|
|
14
|
+
* record PR 182 in spec 036 ([2ab0918](https://github.com/pedronastasi/manychat-ai-agent/commit/2ab0918a8bd5beadcf2498ac1065e8b042a9a819))
|
|
15
|
+
|
|
16
|
+
## [0.17.1](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.17.0...v0.17.1) (2026-10-06)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **deps:** update dependency @ai-sdk/provider to v4.0.22 ([#140](https://github.com/pedronastasi/manychat-ai-agent/issues/140)) ([1177517](https://github.com/pedronastasi/manychat-ai-agent/commit/1177517f781c981497de4d41e279e01640e8c55d))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Dependencies
|
|
25
|
+
|
|
26
|
+
* **deps:** lock file maintenance ([0ae62c4](https://github.com/pedronastasi/manychat-ai-agent/commit/0ae62c4defdfdae998ca99abe4b6364c65d01868))
|
|
27
|
+
* **deps:** lock file maintenance ([d178897](https://github.com/pedronastasi/manychat-ai-agent/commit/d1788977ffd898eef4c81735350b2638b8f10de2))
|
|
28
|
+
* **deps:** update github actions ([0241f86](https://github.com/pedronastasi/manychat-ai-agent/commit/0241f8608927e81e9339ad45cd2101a6ed6cac5e))
|
|
29
|
+
* **deps:** update github actions (major) ([e26d157](https://github.com/pedronastasi/manychat-ai-agent/commit/e26d1576abb97747686e244504c1a3fd1c0cc38d))
|
|
30
|
+
|
|
3
31
|
## [0.17.0](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.16.0...v0.17.0) (2026-10-06)
|
|
4
32
|
|
|
5
33
|
|
package/dist/agent/prompt.d.ts
CHANGED
|
@@ -24,7 +24,11 @@ export interface SystemPromptParts {
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function buildSystemPrompt(persona: string, catalog: Catalog, rules: Rules,
|
|
26
26
|
/** This tenant's `tools.json` (specs/012); its funnel adds the SALES rules (specs/023). */
|
|
27
|
-
tools?: Tools
|
|
27
|
+
tools?: Tools,
|
|
28
|
+
/** Whether the deployment's plugins add tools of their own (specs/036). */
|
|
29
|
+
options?: {
|
|
30
|
+
pluginTools?: boolean;
|
|
31
|
+
}): SystemPromptParts;
|
|
28
32
|
declare const NUDGE_NOTE_OPEN = "[no reply from the contact since";
|
|
29
33
|
/**
|
|
30
34
|
* The trigger of a nudge turn, in place of a contact message (specs/025 § A
|
package/dist/agent/prompt.js
CHANGED
|
@@ -58,8 +58,10 @@ function renderCatalog(catalog) {
|
|
|
58
58
|
*/
|
|
59
59
|
export function buildSystemPrompt(persona, catalog, rules,
|
|
60
60
|
/** This tenant's `tools.json` (specs/012); its funnel adds the SALES rules (specs/023). */
|
|
61
|
-
tools = NO_TOOLS
|
|
62
|
-
|
|
61
|
+
tools = NO_TOOLS,
|
|
62
|
+
/** Whether the deployment's plugins add tools of their own (specs/036). */
|
|
63
|
+
options = {}) {
|
|
64
|
+
const withTools = offersTools(tools) || options.pluginTools === true;
|
|
63
65
|
const staticPrefix = [
|
|
64
66
|
persona.trim(),
|
|
65
67
|
'',
|
|
@@ -112,6 +114,7 @@ tools = NO_TOOLS) {
|
|
|
112
114
|
'someone who has declined twice, or a turn whose opening flow asks it.',
|
|
113
115
|
'Null is a decision, not a way to skip the field.',
|
|
114
116
|
...(withTools ? ACTIONS_SECTION : []),
|
|
117
|
+
...(options.pluginTools ? PLUGIN_TOOLS_LINES : []),
|
|
115
118
|
...intentSection(tools),
|
|
116
119
|
...salesSection(tools),
|
|
117
120
|
...coursesSection(tools),
|
|
@@ -141,6 +144,14 @@ const ACTIONS_SECTION = [
|
|
|
141
144
|
'writes that line, not you: it lists what reached ManyChat on that turn. Do not',
|
|
142
145
|
'repeat those actions unless the contact asks, and never write such a line.',
|
|
143
146
|
];
|
|
147
|
+
/**
|
|
148
|
+
* Only when a plugin adds a tool, so a deployment without one gets the prompt
|
|
149
|
+
* it had before (specs/036).
|
|
150
|
+
*/
|
|
151
|
+
const PLUGIN_TOOLS_LINES = [
|
|
152
|
+
'This deployment adds tools of its own. They are staged like the others: performed',
|
|
153
|
+
'after your reply, and not at all if you escalate. Their description says when to use them.',
|
|
154
|
+
];
|
|
144
155
|
const NUDGE_NOTE_OPEN = '[no reply from the contact since';
|
|
145
156
|
/**
|
|
146
157
|
* How the model schedules and writes a follow-up (specs/025). Only present
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ContactReads } from './contact.ts';
|
|
|
7
7
|
import type { FlowSends } from './flows.ts';
|
|
8
8
|
import { ActionStage } from './tools.ts';
|
|
9
9
|
import type { ContactActions } from './tools.ts';
|
|
10
|
+
import type { Plugins } from '../plugins/plugins.ts';
|
|
10
11
|
export interface AgentUsage {
|
|
11
12
|
inputTokens: number | undefined;
|
|
12
13
|
outputTokens: number | undefined;
|
|
@@ -125,6 +126,8 @@ export interface RunnerOptions {
|
|
|
125
126
|
* what Constitution C5 forbids. Enable only against a trusted collector.
|
|
126
127
|
*/
|
|
127
128
|
recordPromptsInTraces?: boolean;
|
|
129
|
+
/** The tenant's plugin tools, loaded once at boot (specs/036). */
|
|
130
|
+
plugins?: Plugins | undefined;
|
|
128
131
|
}
|
|
129
132
|
export declare class GenerateTextRunner implements AgentRunner {
|
|
130
133
|
private readonly opts;
|
package/dist/agent/runner.js
CHANGED
|
@@ -91,11 +91,12 @@ export class GenerateTextRunner {
|
|
|
91
91
|
const config = this.opts.config();
|
|
92
92
|
if (this.cached?.config !== config) {
|
|
93
93
|
const tools = config.tools ?? NO_TOOLS;
|
|
94
|
-
const
|
|
94
|
+
const pluginTools = this.opts.plugins?.hasTools ?? false;
|
|
95
|
+
const withTools = offersTools(tools) || pluginTools;
|
|
95
96
|
this.cached = {
|
|
96
97
|
config,
|
|
97
98
|
withTools,
|
|
98
|
-
...buildSystemPrompt(config.persona, config.catalog, config.rules, tools),
|
|
99
|
+
...buildSystemPrompt(config.persona, config.catalog, config.rules, tools, { pluginTools }),
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
102
|
return this.cached;
|
|
@@ -110,6 +111,7 @@ export class GenerateTextRunner {
|
|
|
110
111
|
nudgeTurn: nudge !== undefined,
|
|
111
112
|
flows,
|
|
112
113
|
beforeFlow,
|
|
114
|
+
plugins: this.opts.plugins,
|
|
113
115
|
})
|
|
114
116
|
: undefined;
|
|
115
117
|
// The opening is no longer known before the loop runs: the prospect write
|
package/dist/agent/tools.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Tools, INTENT_VALUES } from '../contracts/config.ts';
|
|
|
3
3
|
import type { ActionRecord, ActionStatus, PerformableAction, StagedAction } from '../contracts/agent.ts';
|
|
4
4
|
import type { ContactReads } from './contact.ts';
|
|
5
5
|
import type { FlowSends, SendFlowAction } from './flows.ts';
|
|
6
|
+
import type { Plugins } from '../plugins/plugins.ts';
|
|
6
7
|
/**
|
|
7
8
|
* Steps one to three may call tools; step four offers none, so it must
|
|
8
9
|
* produce the reply. Four fit read, act, read again and reply (specs/024
|
|
@@ -235,4 +236,6 @@ options?: {
|
|
|
235
236
|
* (specs/032 § The opening flow is the server's, not the model's).
|
|
236
237
|
*/
|
|
237
238
|
beforeFlow?: (() => Promise<void>) | undefined;
|
|
239
|
+
/** The tenant's plugin tools, staged as the built-in ones are (specs/036). */
|
|
240
|
+
plugins?: Plugins | undefined;
|
|
238
241
|
}): ToolSet | undefined;
|
package/dist/agent/tools.js
CHANGED
|
@@ -685,5 +685,8 @@ options = {}) {
|
|
|
685
685
|
},
|
|
686
686
|
});
|
|
687
687
|
}
|
|
688
|
+
// Added last, and a built-in name was refused when the plugin loaded, so a
|
|
689
|
+
// plugin can neither replace nor shadow one of the tools above.
|
|
690
|
+
options.plugins?.addTools(tools, stage, closed, description => `${description}\n${STAGED}`);
|
|
688
691
|
return Object.keys(tools).length > 0 ? tools : undefined;
|
|
689
692
|
}
|
|
@@ -118,6 +118,9 @@ export class ManyChatHttpClient {
|
|
|
118
118
|
// A row in `nudges`, written by NudgingPerformer (specs/025). Reaching
|
|
119
119
|
// ManyChat with one means the wrapper was left out.
|
|
120
120
|
throw new Error('schedule_nudge is not a ManyChat action');
|
|
121
|
+
case 'plugin':
|
|
122
|
+
// Performed by its plugin (specs/036); the same mistake as above.
|
|
123
|
+
throw new Error('a plugin tool is not a ManyChat action');
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
/**
|
package/dist/cli/run.js
CHANGED
|
@@ -90,7 +90,22 @@ export async function run(argv) {
|
|
|
90
90
|
return undefined;
|
|
91
91
|
}
|
|
92
92
|
case 'config check': {
|
|
93
|
-
|
|
93
|
+
// Plugins are checked as `agent serve` loads them, so a check that
|
|
94
|
+
// passes is a boot that does not stop on one (specs/036).
|
|
95
|
+
const { loadPlugins } = await import("../plugins/loader.js");
|
|
96
|
+
let names;
|
|
97
|
+
try {
|
|
98
|
+
names = (await loadPlugins(configDir)).names;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof ConfigError) {
|
|
102
|
+
console.error(`invalid plugins (${configDir}): ${error.message}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const loaded = names.length > 0 ? `; plugins: ${names.join(', ')}` : '';
|
|
108
|
+
console.log(`config check passed (${configDir}${loaded})`);
|
|
94
109
|
return 0;
|
|
95
110
|
}
|
|
96
111
|
case 'upgrade': {
|
|
@@ -119,9 +134,11 @@ async function startWorkers(configDir) {
|
|
|
119
134
|
const { resolveModel } = await import("../agent/registry.js");
|
|
120
135
|
const { ConfigStore } = await import("../config/loader.js");
|
|
121
136
|
const { createLogger } = await import("../observability/logger.js");
|
|
137
|
+
const { loadPlugins } = await import("../plugins/loader.js");
|
|
122
138
|
const env = loadEnv();
|
|
123
139
|
const logger = createLogger(env.LOG_LEVEL);
|
|
124
140
|
const configStore = new ConfigStore(configDir, reservedNames(env));
|
|
141
|
+
const plugins = await loadPlugins(configDir);
|
|
125
142
|
let db;
|
|
126
143
|
if (dbMod.isEmbedded(env.DATABASE_URL)) {
|
|
127
144
|
const embedded = await dbMod.createEmbeddedDatabase(env.DATABASE_URL);
|
|
@@ -141,8 +158,9 @@ async function startWorkers(configDir) {
|
|
|
141
158
|
maxOutputTokens: env.AGENT_MAX_OUTPUT_TOKENS,
|
|
142
159
|
temperature: env.AGENT_TEMPERATURE,
|
|
143
160
|
reasoningEffort: env.AGENT_REASONING_EFFORT,
|
|
161
|
+
plugins,
|
|
144
162
|
});
|
|
145
|
-
const stopOutbox = new OutboxWorker({ db, client: manychat, logger }).start();
|
|
163
|
+
const stopOutbox = new OutboxWorker({ db, client: manychat, logger, plugins }).start();
|
|
146
164
|
const stopNudges = new NudgeWorker({
|
|
147
165
|
db,
|
|
148
166
|
runner,
|
|
@@ -146,7 +146,8 @@ export type ToolName = z.infer<typeof ToolName>;
|
|
|
146
146
|
* What a turn's `actions` entry may record: a tool the model called, or a
|
|
147
147
|
* `send_event`, which no model is ever offered. The server performs it as a
|
|
148
148
|
* follow-on of a funnel write (specs/027 § Every event is recorded beside the
|
|
149
|
-
* write that caused it).
|
|
149
|
+
* write that caused it). A plugin tool is recorded as `plugin`, its name as
|
|
150
|
+
* the id (specs/036).
|
|
150
151
|
*/
|
|
151
152
|
export declare const ActionKind: z.ZodEnum<{
|
|
152
153
|
send_flow: "send_flow";
|
|
@@ -156,6 +157,7 @@ export declare const ActionKind: z.ZodEnum<{
|
|
|
156
157
|
write_note: "write_note";
|
|
157
158
|
schedule_nudge: "schedule_nudge";
|
|
158
159
|
send_event: "send_event";
|
|
160
|
+
plugin: "plugin";
|
|
159
161
|
}>;
|
|
160
162
|
export type ActionKind = z.infer<typeof ActionKind>;
|
|
161
163
|
/**
|
|
@@ -234,6 +236,12 @@ export declare const StagedAction: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
234
236
|
tool: z.ZodLiteral<"schedule_nudge">;
|
|
235
237
|
id: z.ZodString;
|
|
236
238
|
minutes: z.ZodNumber;
|
|
239
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
240
|
+
tool: z.ZodLiteral<"plugin">;
|
|
241
|
+
id: z.ZodString;
|
|
242
|
+
plugin: z.ZodString;
|
|
243
|
+
params: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
244
|
+
notes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
237
245
|
}, z.core.$strip>], "tool">;
|
|
238
246
|
export type StagedAction = z.infer<typeof StagedAction>;
|
|
239
247
|
/** Everything that may reach ManyChat as an action: a staged one, or a follow-on event (specs/027). */
|
|
@@ -263,6 +271,7 @@ export declare const ActionRecord: z.ZodObject<{
|
|
|
263
271
|
write_note: "write_note";
|
|
264
272
|
schedule_nudge: "schedule_nudge";
|
|
265
273
|
send_event: "send_event";
|
|
274
|
+
plugin: "plugin";
|
|
266
275
|
}>;
|
|
267
276
|
id: z.ZodString;
|
|
268
277
|
value: z.ZodOptional<z.ZodString>;
|
package/dist/contracts/agent.js
CHANGED
|
@@ -146,9 +146,10 @@ export const ToolName = z.enum([
|
|
|
146
146
|
* What a turn's `actions` entry may record: a tool the model called, or a
|
|
147
147
|
* `send_event`, which no model is ever offered. The server performs it as a
|
|
148
148
|
* follow-on of a funnel write (specs/027 § Every event is recorded beside the
|
|
149
|
-
* write that caused it).
|
|
149
|
+
* write that caused it). A plugin tool is recorded as `plugin`, its name as
|
|
150
|
+
* the id (specs/036).
|
|
150
151
|
*/
|
|
151
|
-
export const ActionKind = z.enum([...ToolName.options, 'send_event']);
|
|
152
|
+
export const ActionKind = z.enum([...ToolName.options, 'send_event', 'plugin']);
|
|
152
153
|
/**
|
|
153
154
|
* An action the model staged, resolved against `tools.json` at the moment it
|
|
154
155
|
* was staged. Carries the ManyChat names it will be performed with, so it
|
|
@@ -224,6 +225,19 @@ export const StagedAction = z.discriminatedUnion('tool', [
|
|
|
224
225
|
* `nudges`, never as a ManyChat request (specs/025).
|
|
225
226
|
*/
|
|
226
227
|
z.object({ tool: z.literal('schedule_nudge'), id: z.string(), minutes: z.number() }),
|
|
228
|
+
/**
|
|
229
|
+
* A tool a tenant's plugin declares, `id` being its name. Its parameters
|
|
230
|
+
* were validated against the declaration when it was staged, and its notes
|
|
231
|
+
* cleaned and kept apart so they can be kept out of the logs as a
|
|
232
|
+
* `write_note`'s text is. The plugin performs it, never ManyChat (specs/036).
|
|
233
|
+
*/
|
|
234
|
+
z.object({
|
|
235
|
+
tool: z.literal('plugin'),
|
|
236
|
+
id: z.string(),
|
|
237
|
+
plugin: z.string(),
|
|
238
|
+
params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
239
|
+
notes: z.record(z.string(), z.string()).optional(),
|
|
240
|
+
}),
|
|
227
241
|
]);
|
|
228
242
|
/** What became of a staged action (specs/012 § Every staged action is recorded on its turn). */
|
|
229
243
|
export const ActionStatus = z.enum([
|
|
@@ -10,7 +10,13 @@ function reasonFor(error, subscriberId, action) {
|
|
|
10
10
|
// A ManyChat error's message already names the endpoint, the status and
|
|
11
11
|
// ManyChat's own message.
|
|
12
12
|
const raw = error instanceof Error ? error.message : String(error);
|
|
13
|
-
|
|
13
|
+
// A plugin's own error can quote anything it was given (specs/036).
|
|
14
|
+
const quoted = action.tool === 'write_note'
|
|
15
|
+
? [action.text]
|
|
16
|
+
: action.tool === 'plugin'
|
|
17
|
+
? Object.values(action.notes ?? {})
|
|
18
|
+
: [];
|
|
19
|
+
const unquoted = quoted.reduce((text, value) => (value.length > 0 ? text.split(value).join('[note]') : text), raw);
|
|
14
20
|
return redactText(unquoted.split(subscriberId).join('[subscriber]')).slice(0, 200);
|
|
15
21
|
}
|
|
16
22
|
/**
|
|
@@ -83,7 +83,7 @@ export declare class ConversationStore {
|
|
|
83
83
|
actionHistory(conversationId: string): Promise<{
|
|
84
84
|
createdAt: Date;
|
|
85
85
|
actions: {
|
|
86
|
-
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event";
|
|
86
|
+
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event" | "plugin";
|
|
87
87
|
id: string;
|
|
88
88
|
status: "staged" | "performed" | "failed" | "discarded" | "dropped_over_cap" | "dead_lettered";
|
|
89
89
|
value?: string | undefined;
|
|
@@ -109,7 +109,7 @@ export declare class ConversationStore {
|
|
|
109
109
|
role: "user" | "agent";
|
|
110
110
|
text: string;
|
|
111
111
|
actions: {
|
|
112
|
-
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event";
|
|
112
|
+
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event" | "plugin";
|
|
113
113
|
id: string;
|
|
114
114
|
status: "staged" | "performed" | "failed" | "discarded" | "dropped_over_cap" | "dead_lettered";
|
|
115
115
|
value?: string | undefined;
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -427,7 +427,7 @@ export declare const turns: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
427
427
|
dataType: "json";
|
|
428
428
|
columnType: "PgJsonb";
|
|
429
429
|
data: {
|
|
430
|
-
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event";
|
|
430
|
+
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event" | "plugin";
|
|
431
431
|
id: string;
|
|
432
432
|
status: "staged" | "performed" | "failed" | "discarded" | "dropped_over_cap" | "dead_lettered";
|
|
433
433
|
value?: string | undefined;
|
|
@@ -448,7 +448,7 @@ export declare const turns: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
448
448
|
generated: undefined;
|
|
449
449
|
}, {}, {
|
|
450
450
|
$type: {
|
|
451
|
-
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event";
|
|
451
|
+
tool: "send_flow" | "add_tag" | "remove_tag" | "set_field" | "write_note" | "schedule_nudge" | "send_event" | "plugin";
|
|
452
452
|
id: string;
|
|
453
453
|
status: "staged" | "performed" | "failed" | "discarded" | "dropped_over_cap" | "dead_lettered";
|
|
454
454
|
value?: string | undefined;
|
package/dist/evals/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadEnv, loadTenantConfig } from "../config/loader.js";
|
|
2
2
|
import { resolveModel } from "../agent/registry.js";
|
|
3
3
|
import { GenerateTextRunner } from "../agent/runner.js";
|
|
4
|
+
import { loadPlugins } from "../plugins/loader.js";
|
|
4
5
|
import { ActionStage, describeAction } from "../agent/tools.js";
|
|
5
6
|
import { checkCase, classify, evalDir, loadCases } from "./cases.js";
|
|
6
7
|
/** When a nudge case's contact went quiet (specs/025). Invented, and fixed. */
|
|
@@ -20,6 +21,9 @@ export async function runEval() {
|
|
|
20
21
|
const configDir = process.env.CONFIG_DIR ?? 'config';
|
|
21
22
|
const suiteDir = evalDir();
|
|
22
23
|
const tenant = loadTenantConfig(configDir);
|
|
24
|
+
// Offered as in production, so a suite can assert a plugin tool is chosen.
|
|
25
|
+
// Nothing staged here is performed (specs/036).
|
|
26
|
+
const plugins = await loadPlugins(configDir);
|
|
23
27
|
const cases = loadCases(suiteDir);
|
|
24
28
|
// Defaults to the race deadline, so a suite that sets nothing behaves as
|
|
25
29
|
// before. A tenant evaluating a reasoning model raises this rather than
|
|
@@ -32,6 +36,7 @@ export async function runEval() {
|
|
|
32
36
|
maxOutputTokens: env.AGENT_MAX_OUTPUT_TOKENS,
|
|
33
37
|
temperature: env.AGENT_TEMPERATURE,
|
|
34
38
|
reasoningEffort: env.AGENT_REASONING_EFFORT,
|
|
39
|
+
plugins,
|
|
35
40
|
});
|
|
36
41
|
console.log(`\n model: ${env.AGENT_MODEL} suite: ${suiteDir} cases: ${cases.length}\n`);
|
|
37
42
|
const outcomes = [];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bare `manychat-ai-agent` entry point: the plugin API (specs/036). The
|
|
3
|
+
* agent itself is run through its CLI or image, never imported.
|
|
4
|
+
*/
|
|
5
|
+
export { definePlugin, defineTool, PLUGIN_API_VERSION } from './plugins/api.ts';
|
|
6
|
+
export type { ParamsOf, Plugin, PluginCall, PluginLogger, PluginParameter, PluginParameters, PluginTool, } from './plugins/api.ts';
|
package/dist/index.js
ADDED
package/dist/main.js
CHANGED
|
@@ -14,11 +14,15 @@ import { manychatClientFor } from "./channels/manychat/client.js";
|
|
|
14
14
|
import { OutboxWorker } from "./outbox/worker.js";
|
|
15
15
|
import { NudgeWorker } from "./nudge/worker.js";
|
|
16
16
|
import { buildServer } from "./server.js";
|
|
17
|
+
import { loadPlugins } from "./plugins/loader.js";
|
|
17
18
|
export async function main() {
|
|
18
19
|
const env = loadEnv();
|
|
19
20
|
// A tool aimed at the reply flow or field fails the boot, and a reload that
|
|
20
21
|
// introduces one is refused (specs/012).
|
|
21
|
-
const
|
|
22
|
+
const configDir = process.env.CONFIG_DIR ?? 'config';
|
|
23
|
+
const configStore = new ConfigStore(configDir, reservedNames(env));
|
|
24
|
+
// Code, loaded once: a plugin that does not load stops the boot (specs/036).
|
|
25
|
+
const plugins = await loadPlugins(configDir);
|
|
22
26
|
let db;
|
|
23
27
|
if (isEmbedded(env.DATABASE_URL)) {
|
|
24
28
|
const embedded = await createEmbeddedDatabase(env.DATABASE_URL);
|
|
@@ -36,6 +40,7 @@ export async function main() {
|
|
|
36
40
|
configStore,
|
|
37
41
|
manychat,
|
|
38
42
|
contacts: manychat,
|
|
43
|
+
plugins,
|
|
39
44
|
});
|
|
40
45
|
const migrated = await runMigrations(db);
|
|
41
46
|
if (migrated.length > 0)
|
|
@@ -44,6 +49,7 @@ export async function main() {
|
|
|
44
49
|
db,
|
|
45
50
|
client: manychat,
|
|
46
51
|
logger: app.log,
|
|
52
|
+
plugins,
|
|
47
53
|
}).start();
|
|
48
54
|
// Follow-ups the agent scheduled (specs/025). Idle for a tenant without a
|
|
49
55
|
// `nudge` section: nothing schedules one.
|
package/dist/outbox/worker.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from '../db/client.ts';
|
|
2
2
|
import type { ManyChatClient } from '../channels/manychat/client.ts';
|
|
3
|
+
import type { Plugins } from '../plugins/plugins.ts';
|
|
3
4
|
export interface WorkerLogger {
|
|
4
5
|
info: (obj: object, msg: string) => void;
|
|
5
6
|
warn: (obj: object, msg: string) => void;
|
|
@@ -9,6 +10,8 @@ export interface WorkerOptions {
|
|
|
9
10
|
db: Database;
|
|
10
11
|
client: ManyChatClient;
|
|
11
12
|
logger: WorkerLogger;
|
|
13
|
+
/** Performs the plugin actions deferred with a reply (specs/036). */
|
|
14
|
+
plugins?: Plugins | undefined;
|
|
12
15
|
batchSize?: number;
|
|
13
16
|
pollIntervalMs?: number;
|
|
14
17
|
}
|
|
@@ -26,6 +29,7 @@ export declare class OutboxWorker {
|
|
|
26
29
|
private readonly tokens;
|
|
27
30
|
private readonly store;
|
|
28
31
|
private readonly nudges;
|
|
32
|
+
private readonly actions;
|
|
29
33
|
private running;
|
|
30
34
|
/**
|
|
31
35
|
* Set by the stop function. A contact's rows after the one being sent go
|
package/dist/outbox/worker.js
CHANGED
|
@@ -12,6 +12,7 @@ export class OutboxWorker {
|
|
|
12
12
|
tokens;
|
|
13
13
|
store;
|
|
14
14
|
nudges;
|
|
15
|
+
actions;
|
|
15
16
|
running = false;
|
|
16
17
|
/**
|
|
17
18
|
* Set by the stop function. A contact's rows after the one being sent go
|
|
@@ -26,6 +27,7 @@ export class OutboxWorker {
|
|
|
26
27
|
this.tokens = new ContactTokens(opts.db, opts.client);
|
|
27
28
|
this.store = new ConversationStore(opts.db);
|
|
28
29
|
this.nudges = new NudgeStore(opts.db);
|
|
30
|
+
this.actions = opts.plugins ? opts.plugins.performer(opts.client, opts.logger) : opts.client;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* The actions deferred with a reply, performed once, after its first
|
|
@@ -39,8 +41,8 @@ export class OutboxWorker {
|
|
|
39
41
|
return;
|
|
40
42
|
try {
|
|
41
43
|
const performer = row.conversationId
|
|
42
|
-
? new NudgingPerformer(this.
|
|
43
|
-
: this.
|
|
44
|
+
? new NudgingPerformer(this.actions, this.nudges, row.conversationId)
|
|
45
|
+
: this.actions;
|
|
44
46
|
const outcomes = await performActions(performer, row.subscriberId, actions, this.opts.logger);
|
|
45
47
|
if (turnId)
|
|
46
48
|
await this.store.resolveStaged(turnId, outcomes);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin API: what a tenant project's plugin package exports, imported
|
|
3
|
+
* from the bare `manychat-ai-agent` entry point (specs/036).
|
|
4
|
+
*
|
|
5
|
+
* A plugin adds tools. Each is staged when the model calls it and performed
|
|
6
|
+
* after the reply, exactly as the built-in tools of specs/012 are, so a plugin
|
|
7
|
+
* gets no more power than one of them has.
|
|
8
|
+
*/
|
|
9
|
+
/** The plugin API this agent implements. A plugin naming another is refused at startup. */
|
|
10
|
+
export declare const PLUGIN_API_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* One parameter of a plugin tool. There is no free-text string: text is a
|
|
13
|
+
* `note`, bounded and cleaned as a `tools.json` note is (specs/012 § Free-text
|
|
14
|
+
* field values are refused, ADR-0017).
|
|
15
|
+
*/
|
|
16
|
+
export type PluginParameter = {
|
|
17
|
+
type: 'enum';
|
|
18
|
+
values: readonly [string, ...string[]];
|
|
19
|
+
description?: string;
|
|
20
|
+
optional?: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'number';
|
|
23
|
+
integer?: boolean;
|
|
24
|
+
min?: number;
|
|
25
|
+
max?: number;
|
|
26
|
+
description?: string;
|
|
27
|
+
optional?: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'boolean';
|
|
30
|
+
description?: string;
|
|
31
|
+
optional?: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'note';
|
|
34
|
+
/** At most `MAX_NOTE_LENGTH` (500), as a `tools.json` note. */
|
|
35
|
+
maxLength: number;
|
|
36
|
+
description?: string;
|
|
37
|
+
optional?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export type PluginParameters = Readonly<Record<string, PluginParameter>>;
|
|
40
|
+
type ValueOf<P extends PluginParameter> = P extends {
|
|
41
|
+
type: 'enum';
|
|
42
|
+
values: readonly (infer V)[];
|
|
43
|
+
} ? V : P extends {
|
|
44
|
+
type: 'number';
|
|
45
|
+
} ? number : P extends {
|
|
46
|
+
type: 'boolean';
|
|
47
|
+
} ? boolean : string;
|
|
48
|
+
/** The validated values `perform` receives, typed from the declared parameters. */
|
|
49
|
+
export type ParamsOf<S extends PluginParameters> = {
|
|
50
|
+
[K in keyof S as S[K] extends {
|
|
51
|
+
optional: true;
|
|
52
|
+
} ? never : K]: ValueOf<S[K]>;
|
|
53
|
+
} & {
|
|
54
|
+
[K in keyof S as S[K] extends {
|
|
55
|
+
optional: true;
|
|
56
|
+
} ? K : never]?: ValueOf<S[K]>;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The agent's own redacting logger, scoped to the plugin. Message and string
|
|
60
|
+
* fields are redacted before they are written (C5).
|
|
61
|
+
*/
|
|
62
|
+
export interface PluginLogger {
|
|
63
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
64
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
65
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Everything `perform` is given: the turn's subscriber, never one the model
|
|
69
|
+
* named; the validated parameters; the logger; and a signal that aborts when
|
|
70
|
+
* the agent stops waiting. No model, prompt, catalog or database.
|
|
71
|
+
*/
|
|
72
|
+
export interface PluginCall<P> {
|
|
73
|
+
subscriberId: string;
|
|
74
|
+
params: P;
|
|
75
|
+
logger: PluginLogger;
|
|
76
|
+
signal: AbortSignal;
|
|
77
|
+
}
|
|
78
|
+
export interface PluginTool<S extends PluginParameters = PluginParameters> {
|
|
79
|
+
/** Lowercase snake case, unique, and not a built-in tool's name. */
|
|
80
|
+
name: string;
|
|
81
|
+
/** What the model reads to decide when to call it. */
|
|
82
|
+
description: string;
|
|
83
|
+
parameters: S;
|
|
84
|
+
/**
|
|
85
|
+
* Runs after the reply is sent, once. A failure is logged and recorded on
|
|
86
|
+
* the turn, never retried (specs/012 § A failed action is logged, never retried).
|
|
87
|
+
*/
|
|
88
|
+
perform(call: PluginCall<ParamsOf<S>>): Promise<void> | void;
|
|
89
|
+
}
|
|
90
|
+
export interface Plugin {
|
|
91
|
+
name: string;
|
|
92
|
+
/** The plugin API it was written against: `PLUGIN_API_VERSION`. */
|
|
93
|
+
apiVersion: number;
|
|
94
|
+
tools?: readonly PluginTool<any>[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A plugin package's default export. Returns the plugin unchanged: the agent
|
|
98
|
+
* checks it when it loads, so a plain object passes or fails the same way.
|
|
99
|
+
*/
|
|
100
|
+
export declare function definePlugin<const P extends Plugin>(plugin: P): P;
|
|
101
|
+
/** Types a tool's `perform` from its parameters. Returns the tool unchanged. */
|
|
102
|
+
export declare function defineTool<const S extends PluginParameters>(tool: PluginTool<S>): PluginTool<S>;
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin API: what a tenant project's plugin package exports, imported
|
|
3
|
+
* from the bare `manychat-ai-agent` entry point (specs/036).
|
|
4
|
+
*
|
|
5
|
+
* A plugin adds tools. Each is staged when the model calls it and performed
|
|
6
|
+
* after the reply, exactly as the built-in tools of specs/012 are, so a plugin
|
|
7
|
+
* gets no more power than one of them has.
|
|
8
|
+
*/
|
|
9
|
+
/** The plugin API this agent implements. A plugin naming another is refused at startup. */
|
|
10
|
+
export const PLUGIN_API_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* A plugin package's default export. Returns the plugin unchanged: the agent
|
|
13
|
+
* checks it when it loads, so a plain object passes or fails the same way.
|
|
14
|
+
*/
|
|
15
|
+
export function definePlugin(plugin) {
|
|
16
|
+
return plugin;
|
|
17
|
+
}
|
|
18
|
+
/** Types a tool's `perform` from its parameters. Returns the tool unchanged. */
|
|
19
|
+
export function defineTool(tool) {
|
|
20
|
+
return tool;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConfigError } from '../config/loader.ts';
|
|
2
|
+
import { Plugins } from './plugins.ts';
|
|
3
|
+
/** The optional tenant file that lists the plugin packages to load (specs/036). */
|
|
4
|
+
export declare const PLUGINS_FILE = "plugins.json";
|
|
5
|
+
/** A plugin that cannot be loaded. A `ConfigError`, so every command reports it as one. */
|
|
6
|
+
export declare class PluginError extends ConfigError {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** The packages `plugins.json` lists, or none when the file is absent. */
|
|
10
|
+
export declare function listedPlugins(configDir: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Loads the plugins `config/plugins.json` lists, once, at boot (specs/036).
|
|
13
|
+
* Each is resolved from the tenant project's `node_modules`: by default the
|
|
14
|
+
* directory that holds `configDir`. Any that does not load is a startup error.
|
|
15
|
+
*
|
|
16
|
+
* `SIGHUP` does not come here: it reloads configuration, not code.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadPlugins(configDir: string, options?: {
|
|
19
|
+
from?: string;
|
|
20
|
+
}): Promise<Plugins>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { ConfigError } from "../config/loader.js";
|
|
14
|
+
import { MAX_NOTE_LENGTH } from "../contracts/config.js";
|
|
15
|
+
import { ToolName } from "../contracts/agent.js";
|
|
16
|
+
import { PLUGIN_API_VERSION } from "./api.js";
|
|
17
|
+
import { Plugins } from "./plugins.js";
|
|
18
|
+
/** The optional tenant file that lists the plugin packages to load (specs/036). */
|
|
19
|
+
export const PLUGINS_FILE = 'plugins.json';
|
|
20
|
+
const PluginsFile = z.object({ plugins: z.array(z.string().min(1)) }).strict();
|
|
21
|
+
/** Names the model already knows: the six of specs/012 and the read of specs/024. */
|
|
22
|
+
const BUILT_IN = new Set([...ToolName.options, 'get_contact']);
|
|
23
|
+
const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
24
|
+
const PLUGIN_KEYS = new Set(['name', 'apiVersion', 'tools']);
|
|
25
|
+
const TOOL_KEYS = new Set(['name', 'description', 'parameters', 'perform']);
|
|
26
|
+
const PARAMETER_KEYS = {
|
|
27
|
+
enum: new Set(['type', 'values', 'description', 'optional']),
|
|
28
|
+
number: new Set(['type', 'integer', 'min', 'max', 'description', 'optional']),
|
|
29
|
+
boolean: new Set(['type', 'description', 'optional']),
|
|
30
|
+
note: new Set(['type', 'maxLength', 'description', 'optional']),
|
|
31
|
+
};
|
|
32
|
+
/** A plugin that cannot be loaded. A `ConfigError`, so every command reports it as one. */
|
|
33
|
+
export class PluginError extends ConfigError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'PluginError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
|
+
function unknownKeys(value, allowed) {
|
|
41
|
+
return Object.keys(value).filter(key => !allowed.has(key));
|
|
42
|
+
}
|
|
43
|
+
/** The directory of `name` in the first `node_modules` above `from`, as Node finds a package. */
|
|
44
|
+
function packageDir(name, from) {
|
|
45
|
+
let dir = resolve(from);
|
|
46
|
+
for (;;) {
|
|
47
|
+
const candidate = join(dir, 'node_modules', name);
|
|
48
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
49
|
+
return candidate;
|
|
50
|
+
const parent = dirname(dir);
|
|
51
|
+
if (parent === dir)
|
|
52
|
+
return undefined;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The ESM entry an `exports` value points the bare import at. */
|
|
57
|
+
function exported(target) {
|
|
58
|
+
if (typeof target === 'string')
|
|
59
|
+
return target;
|
|
60
|
+
if (Array.isArray(target)) {
|
|
61
|
+
for (const option of target) {
|
|
62
|
+
const found = exported(option);
|
|
63
|
+
if (found)
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if (!isObject(target))
|
|
69
|
+
return undefined;
|
|
70
|
+
if ('.' in target)
|
|
71
|
+
return exported(target['.']);
|
|
72
|
+
for (const condition of ['import', 'node', 'default']) {
|
|
73
|
+
const found = exported(target[condition]);
|
|
74
|
+
if (found)
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function entryOf(dir) {
|
|
80
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
81
|
+
const pkg = isObject(manifest) ? manifest : {};
|
|
82
|
+
const target = exported(pkg.exports) ?? (typeof pkg.main === 'string' ? pkg.main : undefined) ?? 'index.js';
|
|
83
|
+
return join(dir, target);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Checks one parameter against what a built-in tool may take. A string outside
|
|
87
|
+
* a note is refused, as `tools.json` refuses one (specs/012 § Free-text field
|
|
88
|
+
* values are refused).
|
|
89
|
+
*/
|
|
90
|
+
function checkParameter(where, key, parameter) {
|
|
91
|
+
const at = `${where}, parameter "${key}"`;
|
|
92
|
+
if (!isObject(parameter))
|
|
93
|
+
throw new PluginError(`${at}: is not a parameter declaration`);
|
|
94
|
+
const type = parameter.type;
|
|
95
|
+
if (type === 'string' || type === 'text') {
|
|
96
|
+
throw new PluginError(`${at}: free text is refused; declare it as a "note" with a maxLength (specs/012)`);
|
|
97
|
+
}
|
|
98
|
+
if (typeof type !== 'string' || !(type in PARAMETER_KEYS)) {
|
|
99
|
+
throw new PluginError(`${at}: type must be one of enum, number, boolean, note`);
|
|
100
|
+
}
|
|
101
|
+
const extra = unknownKeys(parameter, PARAMETER_KEYS[type]);
|
|
102
|
+
if (extra.length > 0)
|
|
103
|
+
throw new PluginError(`${at}: unknown keys ${extra.join(', ')}`);
|
|
104
|
+
if (parameter.description !== undefined && typeof parameter.description !== 'string') {
|
|
105
|
+
throw new PluginError(`${at}: description must be a string`);
|
|
106
|
+
}
|
|
107
|
+
if (parameter.optional !== undefined && typeof parameter.optional !== 'boolean') {
|
|
108
|
+
throw new PluginError(`${at}: optional must be a boolean`);
|
|
109
|
+
}
|
|
110
|
+
if (type === 'enum') {
|
|
111
|
+
const values = parameter.values;
|
|
112
|
+
if (!Array.isArray(values) ||
|
|
113
|
+
values.length === 0 ||
|
|
114
|
+
values.some(value => typeof value !== 'string' || value.length === 0) ||
|
|
115
|
+
new Set(values).size !== values.length) {
|
|
116
|
+
throw new PluginError(`${at}: values must be distinct, non-empty strings`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (type === 'number') {
|
|
120
|
+
const { min, max, integer } = parameter;
|
|
121
|
+
for (const [name, bound] of [
|
|
122
|
+
['min', min],
|
|
123
|
+
['max', max],
|
|
124
|
+
]) {
|
|
125
|
+
if (bound !== undefined && (typeof bound !== 'number' || !Number.isFinite(bound))) {
|
|
126
|
+
throw new PluginError(`${at}: ${name} must be a finite number`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (typeof min === 'number' && typeof max === 'number' && min > max) {
|
|
130
|
+
throw new PluginError(`${at}: min is greater than max`);
|
|
131
|
+
}
|
|
132
|
+
if (integer !== undefined && typeof integer !== 'boolean') {
|
|
133
|
+
throw new PluginError(`${at}: integer must be a boolean`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (type === 'note') {
|
|
137
|
+
const { maxLength } = parameter;
|
|
138
|
+
if (typeof maxLength !== 'number' ||
|
|
139
|
+
!Number.isInteger(maxLength) ||
|
|
140
|
+
maxLength < 1 ||
|
|
141
|
+
maxLength > MAX_NOTE_LENGTH) {
|
|
142
|
+
throw new PluginError(`${at}: maxLength must be a whole number from 1 to ${MAX_NOTE_LENGTH} (ADR-0017)`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function checkTool(plugin, value, taken) {
|
|
147
|
+
if (!isObject(value))
|
|
148
|
+
throw new PluginError(`plugin ${plugin}: a tool is not an object`);
|
|
149
|
+
const name = value.name;
|
|
150
|
+
if (typeof name !== 'string' || !TOOL_NAME.test(name)) {
|
|
151
|
+
throw new PluginError(`plugin ${plugin}: tool name ${JSON.stringify(name)} must be lowercase snake case`);
|
|
152
|
+
}
|
|
153
|
+
const where = `plugin ${plugin}, tool ${name}`;
|
|
154
|
+
if (BUILT_IN.has(name))
|
|
155
|
+
throw new PluginError(`${where}: takes the name of a built-in tool`);
|
|
156
|
+
const owner = taken.get(name);
|
|
157
|
+
if (owner !== undefined)
|
|
158
|
+
throw new PluginError(`${where}: plugin ${owner} already defines it`);
|
|
159
|
+
const extra = unknownKeys(value, TOOL_KEYS);
|
|
160
|
+
if (extra.length > 0)
|
|
161
|
+
throw new PluginError(`${where}: unknown keys ${extra.join(', ')}`);
|
|
162
|
+
if (typeof value.description !== 'string' || value.description.trim().length === 0) {
|
|
163
|
+
throw new PluginError(`${where}: description must be a non-empty string`);
|
|
164
|
+
}
|
|
165
|
+
if (!isObject(value.parameters))
|
|
166
|
+
throw new PluginError(`${where}: parameters must be an object`);
|
|
167
|
+
for (const [key, parameter] of Object.entries(value.parameters)) {
|
|
168
|
+
checkParameter(where, key, parameter);
|
|
169
|
+
}
|
|
170
|
+
if (typeof value.perform !== 'function') {
|
|
171
|
+
throw new PluginError(`${where}: perform must be a function`);
|
|
172
|
+
}
|
|
173
|
+
taken.set(name, plugin);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Checks a plugin's default export. Everything that would leave the prompt
|
|
178
|
+
* promising an action nothing performs fails here, at startup (C6).
|
|
179
|
+
*/
|
|
180
|
+
function checkPlugin(spec, value, taken) {
|
|
181
|
+
if (!isObject(value)) {
|
|
182
|
+
throw new PluginError(`plugin ${spec}: its default export is not a plugin (use definePlugin)`);
|
|
183
|
+
}
|
|
184
|
+
if (typeof value.name !== 'string' || value.name.length === 0) {
|
|
185
|
+
throw new PluginError(`plugin ${spec}: name must be a non-empty string`);
|
|
186
|
+
}
|
|
187
|
+
const name = value.name;
|
|
188
|
+
if (value.apiVersion !== PLUGIN_API_VERSION) {
|
|
189
|
+
throw new PluginError(`plugin ${name}: apiVersion ${JSON.stringify(value.apiVersion)} is not supported; ` +
|
|
190
|
+
`this agent supports ${PLUGIN_API_VERSION}`);
|
|
191
|
+
}
|
|
192
|
+
// Refused rather than ignored: a channel that silently never mounts is a
|
|
193
|
+
// contact nobody answers (specs/038).
|
|
194
|
+
if ('channels' in value) {
|
|
195
|
+
throw new PluginError(`plugin ${name}: channels are not supported yet (specs/038)`);
|
|
196
|
+
}
|
|
197
|
+
const extra = unknownKeys(value, PLUGIN_KEYS);
|
|
198
|
+
if (extra.length > 0)
|
|
199
|
+
throw new PluginError(`plugin ${name}: unknown keys ${extra.join(', ')}`);
|
|
200
|
+
if (value.tools !== undefined && !Array.isArray(value.tools)) {
|
|
201
|
+
throw new PluginError(`plugin ${name}: tools must be an array`);
|
|
202
|
+
}
|
|
203
|
+
const tools = (value.tools ?? []).map(tool => checkTool(name, tool, taken));
|
|
204
|
+
return { name, tools };
|
|
205
|
+
}
|
|
206
|
+
/** The packages `plugins.json` lists, or none when the file is absent. */
|
|
207
|
+
export function listedPlugins(configDir) {
|
|
208
|
+
const path = join(configDir, PLUGINS_FILE);
|
|
209
|
+
if (!existsSync(path))
|
|
210
|
+
return [];
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw new PluginError(`${PLUGINS_FILE}: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
const parsed = PluginsFile.safeParse(raw);
|
|
219
|
+
if (!parsed.success) {
|
|
220
|
+
throw new PluginError(`${PLUGINS_FILE}: ${z.prettifyError(parsed.error)}`);
|
|
221
|
+
}
|
|
222
|
+
const packages = parsed.data.plugins;
|
|
223
|
+
const repeated = packages.find((name, index) => packages.indexOf(name) !== index);
|
|
224
|
+
if (repeated !== undefined) {
|
|
225
|
+
throw new PluginError(`${PLUGINS_FILE}: ${repeated} is listed twice`);
|
|
226
|
+
}
|
|
227
|
+
return packages;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Loads the plugins `config/plugins.json` lists, once, at boot (specs/036).
|
|
231
|
+
* Each is resolved from the tenant project's `node_modules`: by default the
|
|
232
|
+
* directory that holds `configDir`. Any that does not load is a startup error.
|
|
233
|
+
*
|
|
234
|
+
* `SIGHUP` does not come here: it reloads configuration, not code.
|
|
235
|
+
*/
|
|
236
|
+
export async function loadPlugins(configDir, options = {}) {
|
|
237
|
+
const packages = listedPlugins(configDir);
|
|
238
|
+
if (packages.length === 0)
|
|
239
|
+
return Plugins.NONE;
|
|
240
|
+
const from = options.from ?? dirname(resolve(configDir));
|
|
241
|
+
const taken = new Map();
|
|
242
|
+
const names = new Set();
|
|
243
|
+
const loaded = [];
|
|
244
|
+
for (const spec of packages) {
|
|
245
|
+
if (spec.startsWith('.') || spec.startsWith('/') || spec.includes('\\')) {
|
|
246
|
+
throw new PluginError(`plugin ${spec}: list a package name, not a path`);
|
|
247
|
+
}
|
|
248
|
+
const dir = packageDir(spec, from);
|
|
249
|
+
if (!dir) {
|
|
250
|
+
throw new PluginError(`plugin ${spec} is not installed (no node_modules/${spec} from ${from})`);
|
|
251
|
+
}
|
|
252
|
+
let module;
|
|
253
|
+
try {
|
|
254
|
+
module = (await import(__rewriteRelativeImportExtension(pathToFileURL(entryOf(dir)).href)));
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
throw new PluginError(`plugin ${spec} failed to load: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
const plugin = checkPlugin(spec, module.default, taken);
|
|
260
|
+
if (names.has(plugin.name)) {
|
|
261
|
+
throw new PluginError(`plugin ${spec}: another plugin is already named ${plugin.name}`);
|
|
262
|
+
}
|
|
263
|
+
names.add(plugin.name);
|
|
264
|
+
loaded.push(...plugin.tools.map(tool => ({ plugin: plugin.name, tool })));
|
|
265
|
+
}
|
|
266
|
+
return new Plugins(loaded, [...names]);
|
|
267
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ToolSet } from 'ai';
|
|
2
|
+
import type { ActionPerformer } from '../channels/manychat/client.ts';
|
|
3
|
+
import type { ActionStage } from '../agent/tools.ts';
|
|
4
|
+
import type { PluginTool } from './api.ts';
|
|
5
|
+
/**
|
|
6
|
+
* How long the agent waits for a plugin's `perform`. Past it the signal
|
|
7
|
+
* aborts and the action is recorded as failed, so a hung plugin cannot hold
|
|
8
|
+
* up the rest of the turn's actions or the outbox (specs/036). Chosen, not
|
|
9
|
+
* measured: the ManyChat client's own request timeout is shorter.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PLUGIN_PERFORM_TIMEOUT_MS = 10000;
|
|
12
|
+
/** The parameter values a staged plugin action carries, already validated and cleaned. */
|
|
13
|
+
export type PluginParams = Record<string, string | number | boolean>;
|
|
14
|
+
/** The logger the agent writes through; the process's redacting pino logger in production. */
|
|
15
|
+
export interface HostLogger {
|
|
16
|
+
info: (obj: object, msg: string) => void;
|
|
17
|
+
warn: (obj: object, msg: string) => void;
|
|
18
|
+
error: (obj: object, msg: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface LoadedTool {
|
|
21
|
+
/** The plugin that declared it. */
|
|
22
|
+
plugin: string;
|
|
23
|
+
tool: PluginTool;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The plugins a process loaded at boot (specs/036). Each tool is offered to
|
|
27
|
+
* the model beside the built-in ones and performed after the reply, as a
|
|
28
|
+
* built-in tool is.
|
|
29
|
+
*/
|
|
30
|
+
export declare class Plugins {
|
|
31
|
+
static readonly NONE: Plugins;
|
|
32
|
+
private readonly byName;
|
|
33
|
+
/** The loaded plugins' names, in the order `plugins.json` lists them. */
|
|
34
|
+
readonly names: readonly string[];
|
|
35
|
+
constructor(tools: readonly LoadedTool[], names?: readonly string[]);
|
|
36
|
+
get hasTools(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Adds each plugin tool to the turn's tools. A call stages the action and
|
|
39
|
+
* nothing else; `closed` refuses it as the built-in writes are refused
|
|
40
|
+
* before a contact is a prospect (specs/034).
|
|
41
|
+
*/
|
|
42
|
+
addTools(tools: ToolSet, stage: ActionStage, closed: () => boolean, describe: (description: string) => string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Wraps the process's performer: a plugin action goes to its plugin, every
|
|
45
|
+
* other action where it went before.
|
|
46
|
+
*/
|
|
47
|
+
performer(inner: ActionPerformer, logger: HostLogger): ActionPerformer;
|
|
48
|
+
private perform;
|
|
49
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { cleanNote } from "../agent/contact.js";
|
|
4
|
+
import { redactText } from "../observability/redact.js";
|
|
5
|
+
/**
|
|
6
|
+
* How long the agent waits for a plugin's `perform`. Past it the signal
|
|
7
|
+
* aborts and the action is recorded as failed, so a hung plugin cannot hold
|
|
8
|
+
* up the rest of the turn's actions or the outbox (specs/036). Chosen, not
|
|
9
|
+
* measured: the ManyChat client's own request timeout is shorter.
|
|
10
|
+
*/
|
|
11
|
+
export const PLUGIN_PERFORM_TIMEOUT_MS = 10_000;
|
|
12
|
+
/** The model-facing schema, built here from the declaration, never taken from the plugin. */
|
|
13
|
+
function schemaOf(parameter) {
|
|
14
|
+
let schema;
|
|
15
|
+
switch (parameter.type) {
|
|
16
|
+
case 'enum':
|
|
17
|
+
schema = z.enum(parameter.values);
|
|
18
|
+
break;
|
|
19
|
+
case 'number': {
|
|
20
|
+
let number = parameter.integer ? z.number().int() : z.number();
|
|
21
|
+
if (parameter.min !== undefined)
|
|
22
|
+
number = number.min(parameter.min);
|
|
23
|
+
if (parameter.max !== undefined)
|
|
24
|
+
number = number.max(parameter.max);
|
|
25
|
+
schema = number;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case 'boolean':
|
|
29
|
+
schema = z.boolean();
|
|
30
|
+
break;
|
|
31
|
+
case 'note':
|
|
32
|
+
schema = z.string();
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (parameter.description !== undefined)
|
|
36
|
+
schema = schema.describe(parameter.description);
|
|
37
|
+
return parameter.optional ? schema.optional() : schema;
|
|
38
|
+
}
|
|
39
|
+
const NOTE_RULE = 'A note parameter is for the people who follow up: never put a name, phone number, email or link in one; they are removed.';
|
|
40
|
+
/**
|
|
41
|
+
* A logger that redacts what a plugin writes and names the plugin on every
|
|
42
|
+
* line: the agent's logger, never one the plugin brings (C5).
|
|
43
|
+
*/
|
|
44
|
+
function pluginLogger(host, plugin, subscriberId) {
|
|
45
|
+
const clean = (text) => redactText(text.split(subscriberId).join('[subscriber]'));
|
|
46
|
+
const write = (level) => (message, fields = {}) => {
|
|
47
|
+
const redacted = Object.fromEntries(Object.entries(fields).map(([key, value]) => [
|
|
48
|
+
key,
|
|
49
|
+
typeof value === 'string' ? clean(value) : value,
|
|
50
|
+
]));
|
|
51
|
+
host[level]({ ...redacted, plugin }, clean(message));
|
|
52
|
+
};
|
|
53
|
+
return { info: write('info'), warn: write('warn'), error: write('error') };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The plugins a process loaded at boot (specs/036). Each tool is offered to
|
|
57
|
+
* the model beside the built-in ones and performed after the reply, as a
|
|
58
|
+
* built-in tool is.
|
|
59
|
+
*/
|
|
60
|
+
export class Plugins {
|
|
61
|
+
static NONE = new Plugins([]);
|
|
62
|
+
byName;
|
|
63
|
+
/** The loaded plugins' names, in the order `plugins.json` lists them. */
|
|
64
|
+
names;
|
|
65
|
+
constructor(tools, names = []) {
|
|
66
|
+
this.byName = new Map(tools.map(entry => [entry.tool.name, entry]));
|
|
67
|
+
this.names = names;
|
|
68
|
+
}
|
|
69
|
+
get hasTools() {
|
|
70
|
+
return this.byName.size > 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Adds each plugin tool to the turn's tools. A call stages the action and
|
|
74
|
+
* nothing else; `closed` refuses it as the built-in writes are refused
|
|
75
|
+
* before a contact is a prospect (specs/034).
|
|
76
|
+
*/
|
|
77
|
+
addTools(tools, stage, closed, describe) {
|
|
78
|
+
for (const { plugin, tool: declared } of this.byName.values()) {
|
|
79
|
+
const parameters = Object.entries(declared.parameters);
|
|
80
|
+
const notes = parameters.filter(([, parameter]) => parameter.type === 'note');
|
|
81
|
+
tools[declared.name] = tool({
|
|
82
|
+
description: describe(notes.length > 0 ? `${declared.description}\n${NOTE_RULE}` : declared.description),
|
|
83
|
+
inputSchema: z.object(Object.fromEntries(parameters.map(([key, parameter]) => [key, schemaOf(parameter)]))),
|
|
84
|
+
execute: (input) => {
|
|
85
|
+
if (closed())
|
|
86
|
+
return { staged: false, reason: 'not_prospect' };
|
|
87
|
+
const params = {};
|
|
88
|
+
const notes = {};
|
|
89
|
+
for (const [key, parameter] of parameters) {
|
|
90
|
+
const value = input[key];
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
if (parameter.type !== 'note') {
|
|
94
|
+
params[key] = value;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Cleaned when staged, as a `tools.json` note is, so the outbox
|
|
98
|
+
// never holds what the plugin will not be given (specs/024).
|
|
99
|
+
const cleaned = cleanNote(value, parameter.maxLength);
|
|
100
|
+
if (cleaned.length > 0)
|
|
101
|
+
notes[key] = cleaned;
|
|
102
|
+
else if (!parameter.optional)
|
|
103
|
+
return { staged: false };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
staged: stage.stage({
|
|
107
|
+
tool: 'plugin',
|
|
108
|
+
id: declared.name,
|
|
109
|
+
plugin,
|
|
110
|
+
params,
|
|
111
|
+
...(Object.keys(notes).length > 0 ? { notes } : {}),
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Wraps the process's performer: a plugin action goes to its plugin, every
|
|
120
|
+
* other action where it went before.
|
|
121
|
+
*/
|
|
122
|
+
performer(inner, logger) {
|
|
123
|
+
return {
|
|
124
|
+
performAction: (subscriberId, action) => action.tool === 'plugin'
|
|
125
|
+
? this.perform(subscriberId, action.id, { ...action.params, ...action.notes }, logger)
|
|
126
|
+
: inner.performAction(subscriberId, action),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async perform(subscriberId, name, params, logger) {
|
|
130
|
+
// A row queued before a restart that dropped the plugin.
|
|
131
|
+
const entry = this.byName.get(name);
|
|
132
|
+
if (!entry)
|
|
133
|
+
throw new Error(`plugin tool ${name} is not loaded`);
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
let timer;
|
|
136
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
controller.abort();
|
|
139
|
+
reject(new Error(`plugin tool ${name} timed out after ${PLUGIN_PERFORM_TIMEOUT_MS} ms`));
|
|
140
|
+
}, PLUGIN_PERFORM_TIMEOUT_MS);
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
await Promise.race([
|
|
144
|
+
Promise.resolve().then(() => entry.tool.perform({
|
|
145
|
+
subscriberId,
|
|
146
|
+
params,
|
|
147
|
+
logger: pluginLogger(logger, entry.plugin, subscriberId),
|
|
148
|
+
signal: controller.signal,
|
|
149
|
+
})),
|
|
150
|
+
timedOut,
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AgentRunner } from './agent/runner.ts';
|
|
|
7
7
|
import type { ContactReader, ManyChatClient } from './channels/manychat/client.ts';
|
|
8
8
|
import type { ConfigStore } from './config/loader.ts';
|
|
9
9
|
import { type FfmpegPaths } from './media/ffmpeg.ts';
|
|
10
|
+
import { Plugins } from './plugins/plugins.ts';
|
|
10
11
|
export interface BuildOptions {
|
|
11
12
|
env: Env;
|
|
12
13
|
db: Database;
|
|
@@ -34,6 +35,8 @@ export interface BuildOptions {
|
|
|
34
35
|
transcriptionModel?: TranscriptionModel;
|
|
35
36
|
/** Injected by tests to stand in for a server without ffmpeg. */
|
|
36
37
|
ffmpegPaths?: FfmpegPaths;
|
|
38
|
+
/** The tenant's plugins, loaded once at boot (specs/036). None when absent. */
|
|
39
|
+
plugins?: Plugins;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* The whole production composition. Plugins are registered before any route is
|
package/dist/server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { bearerToken, createSharedSecretGuard, isAuthenticated } from "./routes/
|
|
|
20
20
|
import { escalationReply } from "./agent/guardrails.js";
|
|
21
21
|
import { mediaScrubbingStream, redactText } from "./observability/redact.js";
|
|
22
22
|
import { loggerOptions } from "./observability/logger.js";
|
|
23
|
+
import { Plugins } from "./plugins/plugins.js";
|
|
23
24
|
const MESSAGE_ROUTE = '/v1/channels/manychat/message';
|
|
24
25
|
/**
|
|
25
26
|
* The whole production composition. Plugins are registered before any route is
|
|
@@ -52,6 +53,7 @@ export async function buildServer(opts) {
|
|
|
52
53
|
const manychatClient = opts.manychat ?? built;
|
|
53
54
|
const contacts = opts.contacts ?? built;
|
|
54
55
|
const adapter = new ManyChatAdapter(manychatClient);
|
|
56
|
+
const plugins = opts.plugins ?? Plugins.NONE;
|
|
55
57
|
const runner = opts.runner ??
|
|
56
58
|
new GenerateTextRunner({
|
|
57
59
|
model: resolveModel(env.AGENT_MODEL),
|
|
@@ -62,6 +64,7 @@ export async function buildServer(opts) {
|
|
|
62
64
|
maxOutputTokens: env.AGENT_MAX_OUTPUT_TOKENS,
|
|
63
65
|
temperature: env.AGENT_TEMPERATURE,
|
|
64
66
|
reasoningEffort: env.AGENT_REASONING_EFFORT,
|
|
67
|
+
plugins,
|
|
65
68
|
});
|
|
66
69
|
// Resolved once, like the answering model: a TRANSCRIPTION_MODEL typo fails
|
|
67
70
|
// the deploy, and a missing ffmpeg is found at boot rather than per video.
|
|
@@ -210,7 +213,8 @@ export async function buildServer(opts) {
|
|
|
210
213
|
logger: request.log,
|
|
211
214
|
tokenWriter: manychatClient,
|
|
212
215
|
tokensEnforced: env.CONTACT_TOKENS_ENFORCED,
|
|
213
|
-
|
|
216
|
+
// A plugin action goes to its plugin, the rest to ManyChat (specs/036).
|
|
217
|
+
actions: plugins.performer(manychatClient, request.log),
|
|
214
218
|
contacts,
|
|
215
219
|
media,
|
|
216
220
|
lanes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "manychat-ai-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "A provider-agnostic conversational AI agent gateway that bridges no-code chat platforms to any LLM.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"agent": "dist/cli.js"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
15
19
|
"./config": {
|
|
16
20
|
"types": "./dist/config/index.d.ts",
|
|
17
21
|
"default": "./dist/config/index.js"
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
"@ai-sdk/anthropic": "^4.0.53",
|
|
63
67
|
"@ai-sdk/google": "^4.0.69",
|
|
64
68
|
"@ai-sdk/openai": "^4.0.66",
|
|
65
|
-
"@ai-sdk/provider": "4.0.
|
|
69
|
+
"@ai-sdk/provider": "4.0.22",
|
|
66
70
|
"@fastify/helmet": "^13.1.1",
|
|
67
71
|
"@fastify/rate-limit": "^11.2.0",
|
|
68
72
|
"@fastify/swagger": "^9.8.1",
|