manychat-ai-agent 0.22.0 → 0.23.1
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.js +10 -4
- package/dist/agent/runner.d.ts +9 -2
- package/dist/agent/runner.js +39 -39
- package/dist/channels/plugin/adapter.d.ts +47 -0
- package/dist/channels/plugin/adapter.js +108 -0
- package/dist/cli/run.js +4 -2
- package/dist/contracts/agent.d.ts +11 -0
- package/dist/contracts/agent.js +12 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/outbox/queue.d.ts +6 -0
- package/dist/outbox/queue.js +5 -0
- package/dist/outbox/worker.d.ts +9 -0
- package/dist/outbox/worker.js +29 -2
- package/dist/plugins/api.d.ts +81 -2
- package/dist/plugins/api.js +13 -1
- package/dist/plugins/loader.d.ts +7 -2
- package/dist/plugins/loader.js +55 -11
- package/dist/plugins/plugins.d.ts +11 -3
- package/dist/plugins/plugins.js +8 -2
- package/dist/routes/turn.d.ts +14 -0
- package/dist/routes/turn.js +20 -7
- package/dist/server.js +109 -34
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.23.1](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.23.0...v0.23.1) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* continue an offering already selected in conversation ([e9d8353](https://github.com/pedronastasi/manychat-ai-agent/commit/e9d8353ce2c1dc134810f9632c09447a46970faf))
|
|
9
|
+
* continue an offering already selected in conversation ([d9b6aa4](https://github.com/pedronastasi/manychat-ai-agent/commit/d9b6aa4e5f9812538efcdda44e624bede5ef180f))
|
|
10
|
+
* **deps:** update patch dependencies ([#208](https://github.com/pedronastasi/manychat-ai-agent/issues/208)) ([d9ba610](https://github.com/pedronastasi/manychat-ai-agent/commit/d9ba6101326ba47f231a58de295e488c4189fc9a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Specs and Docs
|
|
14
|
+
|
|
15
|
+
* specify complete conversation evals ([d8a0a48](https://github.com/pedronastasi/manychat-ai-agent/commit/d8a0a48fe3a8971424dc53ed83ce7de29204bc09))
|
|
16
|
+
* specify complete conversation evals ([81c4f83](https://github.com/pedronastasi/manychat-ai-agent/commit/81c4f83d6ea728ee85d41d19b8e07e3ac3a105f7))
|
|
17
|
+
|
|
18
|
+
## [0.23.0](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.22.0...v0.23.0) (2026-10-08)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* mount plugin channels beside ManyChat's route (spec 038) ([366c646](https://github.com/pedronastasi/manychat-ai-agent/commit/366c646a7223d959603b0f6f5159725d368bf33c))
|
|
24
|
+
* mount plugin channels beside ManyChat's route (spec 038) ([e53d5ed](https://github.com/pedronastasi/manychat-ai-agent/commit/e53d5ed9ea667246d4788faa087226049350ab96))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Specs and Docs
|
|
28
|
+
|
|
29
|
+
* record PR 206 in spec 038 ([f61a13e](https://github.com/pedronastasi/manychat-ai-agent/commit/f61a13ea2448920db388d842da52bfb7a655a698))
|
|
30
|
+
|
|
3
31
|
## [0.22.0](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.21.0...v0.22.0) (2026-10-08)
|
|
4
32
|
|
|
5
33
|
|
package/dist/agent/prompt.js
CHANGED
|
@@ -307,8 +307,13 @@ function offeringsSection(tools) {
|
|
|
307
307
|
`The field ${offering.id} records the offering this contact is buying, one of: ${offering.values.join(', ')}.`,
|
|
308
308
|
'Record it with set_field as soon as the contact chooses an offering or the fit is clear.',
|
|
309
309
|
'A flow listed with an offering is accepted only once that is the contact’s offering;',
|
|
310
|
-
'setting the offering earlier in the same turn is enough.
|
|
311
|
-
'
|
|
310
|
+
'setting the offering earlier in the same turn is enough. An absent recorded offering',
|
|
311
|
+
'does not mean the contact has not chosen. Check the current message and conversation',
|
|
312
|
+
'history, including an offering name or catalog page they supplied and your earlier',
|
|
313
|
+
'acknowledgment of that choice. If one offering is already clear, record it and continue',
|
|
314
|
+
'without asking them to choose or confirm it again. A short answer to a qualification',
|
|
315
|
+
'question does not reset that choice. Ask which offering only when it is still unknown,',
|
|
316
|
+
'before sending offering-specific content: one question, not a list of every offering.',
|
|
312
317
|
...(funnelField(tools)
|
|
313
318
|
? [
|
|
314
319
|
'The offering may change until the stage is offered. From offered on it is locked: a',
|
|
@@ -348,8 +353,9 @@ export function intentNotice(intent, advertOffering) {
|
|
|
348
353
|
* travels with the turn's message like the funnel note, outside the fence.
|
|
349
354
|
*/
|
|
350
355
|
export function offeringNotice(offering, changedFrom) {
|
|
351
|
-
if (offering === undefined)
|
|
352
|
-
return 'OFFERING: No offering is recorded for this contact yet.';
|
|
356
|
+
if (offering === undefined) {
|
|
357
|
+
return 'OFFERING: No offering is recorded for this contact yet. This is missing saved state, not proof of an unknown choice. If the message or history already identifies an offering, record it and continue without asking again.';
|
|
358
|
+
}
|
|
353
359
|
if (changedFrom !== undefined) {
|
|
354
360
|
return `OFFERING: This contact's offering changed from ${changedFrom} to ${offering} since you last saw it. Confirm which offering they want before continuing.`;
|
|
355
361
|
}
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -109,6 +109,12 @@ export interface AgentTurnInput {
|
|
|
109
109
|
nudge?: {
|
|
110
110
|
since: Date;
|
|
111
111
|
} | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* False on a plugin channel's turn: `tools.json`'s tools write to ManyChat,
|
|
114
|
+
* so neither they nor their prompt lines are offered there. Plugin tools
|
|
115
|
+
* still are (specs/038).
|
|
116
|
+
*/
|
|
117
|
+
builtInTools?: boolean | undefined;
|
|
112
118
|
}
|
|
113
119
|
/**
|
|
114
120
|
* The port every caller depends on. Implemented with `generateText`: a single
|
|
@@ -150,7 +156,8 @@ export interface RunnerOptions {
|
|
|
150
156
|
}
|
|
151
157
|
export declare class GenerateTextRunner implements AgentRunner {
|
|
152
158
|
private readonly opts;
|
|
153
|
-
|
|
159
|
+
/** One per kind of turn: with `tools.json`'s tools, and without them (specs/038). */
|
|
160
|
+
private readonly cached;
|
|
154
161
|
constructor(opts: RunnerOptions);
|
|
155
162
|
/**
|
|
156
163
|
* Rebuilds only when ConfigStore swaps in a new object, so the prefix stays
|
|
@@ -159,5 +166,5 @@ export declare class GenerateTextRunner implements AgentRunner {
|
|
|
159
166
|
* An activated playbook version rebuilds it once, not on every turn.
|
|
160
167
|
*/
|
|
161
168
|
private current;
|
|
162
|
-
run({ text, history, signal, media, stage, contact, reads, pluginReads, flows, beforeFlow, nudge, }: AgentTurnInput): Promise<AgentResult>;
|
|
169
|
+
run({ text, history, signal, media, stage, contact, reads, pluginReads, flows, beforeFlow, nudge, builtInTools, }: AgentTurnInput): Promise<AgentResult>;
|
|
163
170
|
}
|
package/dist/agent/runner.js
CHANGED
|
@@ -79,7 +79,8 @@ function nudgeMessage(since, contactNotices) {
|
|
|
79
79
|
}
|
|
80
80
|
export class GenerateTextRunner {
|
|
81
81
|
opts;
|
|
82
|
-
|
|
82
|
+
/** One per kind of turn: with `tools.json`'s tools, and without them (specs/038). */
|
|
83
|
+
cached = new Map();
|
|
83
84
|
constructor(opts) {
|
|
84
85
|
this.opts = opts;
|
|
85
86
|
}
|
|
@@ -89,43 +90,46 @@ export class GenerateTextRunner {
|
|
|
89
90
|
* failed reload keeps the previous object, so it correctly rebuilds nothing.
|
|
90
91
|
* An activated playbook version rebuilds it once, not on every turn.
|
|
91
92
|
*/
|
|
92
|
-
current() {
|
|
93
|
+
current(builtInTools) {
|
|
93
94
|
const config = this.opts.config();
|
|
94
95
|
const playbook = this.opts.playbook?.current();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
96
|
+
const cached = this.cached.get(builtInTools);
|
|
97
|
+
if (cached?.config === config && cached.loadedPlaybook === playbook?.id)
|
|
98
|
+
return cached;
|
|
99
|
+
const tools = builtInTools ? (config.tools ?? NO_TOOLS) : NO_TOOLS;
|
|
100
|
+
// The staged-tool lines follow write tools only: a read is not staged
|
|
101
|
+
// (specs/039 § The prompt changes only when a plugin adds a read tool).
|
|
102
|
+
const pluginTools = this.opts.plugins?.hasWriteTools ?? false;
|
|
103
|
+
const pluginReadTools = this.opts.plugins?.hasReadTools ?? false;
|
|
104
|
+
const withTools = offersTools(tools) || pluginTools || pluginReadTools;
|
|
105
|
+
const prompt = buildSystemPrompt(config.persona, config.catalog, config.rules, tools, {
|
|
106
|
+
pluginTools,
|
|
107
|
+
pluginReadTools,
|
|
108
|
+
});
|
|
109
|
+
// After the catalog and before the cache breakpoint, so a version is
|
|
110
|
+
// cached with the rest of the prefix (specs/031). Without `learning`
|
|
111
|
+
// the prefix is what it was before that spec.
|
|
112
|
+
const rendered = config.rules.learning ? renderPlaybook(playbook) : undefined;
|
|
113
|
+
const built = {
|
|
114
|
+
config,
|
|
115
|
+
loadedPlaybook: playbook?.id,
|
|
116
|
+
playbook: rendered ? playbook : undefined,
|
|
117
|
+
withTools,
|
|
118
|
+
tools,
|
|
119
|
+
staticPrefix: prompt.staticPrefix,
|
|
120
|
+
catalogBlock: rendered ? `${prompt.catalogBlock}\n\n${rendered}` : prompt.catalogBlock,
|
|
121
|
+
};
|
|
122
|
+
this.cached.set(builtInTools, built);
|
|
123
|
+
return built;
|
|
120
124
|
}
|
|
121
|
-
async run({ text, history, signal, media, stage = new ActionStage(), contact, reads, pluginReads, flows, beforeFlow, nudge, }) {
|
|
125
|
+
async run({ text, history, signal, media, stage = new ActionStage(), contact, reads, pluginReads, flows, beforeFlow, nudge, builtInTools = true, }) {
|
|
122
126
|
const started = Date.now();
|
|
123
127
|
// Resolved once per turn: a reload landing mid-turn must not produce a
|
|
124
128
|
// reply built from one config and guarded by another.
|
|
125
|
-
const { config, playbook, staticPrefix, catalogBlock, withTools } = this.current();
|
|
129
|
+
const { config, playbook, staticPrefix, catalogBlock, withTools, tools: tenantTools, } = this.current(builtInTools);
|
|
126
130
|
const playbookVersion = playbook?.id ?? null;
|
|
127
131
|
const tools = withTools
|
|
128
|
-
? buildTools(
|
|
132
|
+
? buildTools(tenantTools, stage, contact, reads, {
|
|
129
133
|
nudgeTurn: nudge !== undefined,
|
|
130
134
|
flows,
|
|
131
135
|
beforeFlow,
|
|
@@ -135,13 +139,11 @@ export class GenerateTextRunner {
|
|
|
135
139
|
: undefined;
|
|
136
140
|
// The opening is no longer known before the loop runs: the prospect write
|
|
137
141
|
// that queues it says so, and the reply step is told below (specs/034).
|
|
138
|
-
const opening = contact?.openingDue ? openingFlow(
|
|
142
|
+
const opening = contact?.openingDue ? openingFlow(tenantTools) : undefined;
|
|
139
143
|
const contactNotices = [
|
|
140
|
-
...(intentField(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
...(funnelField(config.tools ?? NO_TOOLS) ? [funnelNotice(contact?.funnelStage)] : []),
|
|
144
|
-
...(offeringField(config.tools ?? NO_TOOLS)
|
|
144
|
+
...(intentField(tenantTools) ? [intentNotice(contact?.intent, contact?.advertOffering)] : []),
|
|
145
|
+
...(funnelField(tenantTools) ? [funnelNotice(contact?.funnelStage)] : []),
|
|
146
|
+
...(offeringField(tenantTools)
|
|
145
147
|
? [offeringNotice(contact?.offering, contact?.offeringChangedFrom)]
|
|
146
148
|
: []),
|
|
147
149
|
];
|
|
@@ -167,9 +169,7 @@ export class GenerateTextRunner {
|
|
|
167
169
|
...messages,
|
|
168
170
|
{
|
|
169
171
|
role: 'user',
|
|
170
|
-
content: stagedNotice(stage, reads?.latest, opening && stagesProspect(stage,
|
|
171
|
-
? opening
|
|
172
|
-
: undefined, pluginReads?.latest),
|
|
172
|
+
content: stagedNotice(stage, reads?.latest, opening && stagesProspect(stage, tenantTools) ? opening : undefined, pluginReads?.latest),
|
|
173
173
|
},
|
|
174
174
|
],
|
|
175
175
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type AgentReply, type InboundMessage } from '../../contracts/agent.ts';
|
|
2
|
+
import type { ChannelReply, PluginChannel } from '../../plugins/api.ts';
|
|
3
|
+
import { type HostLogger, type Plugins } from '../../plugins/plugins.ts';
|
|
4
|
+
import type { ChannelAdapter, ParseContext } from '../port.ts';
|
|
5
|
+
/**
|
|
6
|
+
* How long the outbox waits for a channel's `push`. Past it the signal aborts
|
|
7
|
+
* and the delivery is retried, as a ManyChat send that timed out is. The same
|
|
8
|
+
* bound as a plugin's `perform` (specs/036); chosen, not measured.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CHANNEL_PUSH_TIMEOUT_MS = 10000;
|
|
11
|
+
/** A request that fails the channel's own schema: refused with a 400 before any turn (specs/038). */
|
|
12
|
+
export declare class ChannelInputError extends Error {
|
|
13
|
+
constructor(channel: string);
|
|
14
|
+
}
|
|
15
|
+
/** The route a channel is mounted at, beside ManyChat's (specs/038). */
|
|
16
|
+
export declare const channelRoute: (name: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* A plugin's channel behind the `ChannelAdapter` port (specs/038, ADR-0005).
|
|
19
|
+
* The plugin translates; everything it does not translate is decided here,
|
|
20
|
+
* the same way for every plugin channel: the tenant, the channel, and that
|
|
21
|
+
* the request carries no contact token and no offering.
|
|
22
|
+
*
|
|
23
|
+
* The contact's ID is prefixed with the channel's name, so a contact on one
|
|
24
|
+
* channel never shares a conversation, an order or an outbox lane with one on
|
|
25
|
+
* another whose platform happens to use the same ID.
|
|
26
|
+
*/
|
|
27
|
+
export declare class PluginChannelAdapter implements ChannelAdapter<unknown, unknown> {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly plugin: string;
|
|
30
|
+
readonly route: string;
|
|
31
|
+
private readonly channel;
|
|
32
|
+
private readonly logger;
|
|
33
|
+
private readonly prefix;
|
|
34
|
+
constructor(plugin: string, channel: PluginChannel, logger: HostLogger);
|
|
35
|
+
/** Whether the request passes the channel's schema; checked before anything reads it (C3). */
|
|
36
|
+
accepts(raw: unknown): boolean;
|
|
37
|
+
parse(raw: unknown, ctx: ParseContext): InboundMessage;
|
|
38
|
+
render(reply: AgentReply): unknown;
|
|
39
|
+
/** Delivers a lost race's reply (specs/038 § Outbound delivery is the adapter's `push`). */
|
|
40
|
+
push(to: {
|
|
41
|
+
subscriberId: string;
|
|
42
|
+
}, reply: AgentReply): Promise<void>;
|
|
43
|
+
/** What `push` sends, from what the outbox keeps of a reply. */
|
|
44
|
+
deliver(to: string, reply: ChannelReply): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** An adapter for each channel the plugins add, by name, logging through `logger`. */
|
|
47
|
+
export declare function channelAdapters(plugins: Plugins, logger: HostLogger): ReadonlyMap<string, PluginChannelAdapter>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ChannelMessage } from "../../contracts/agent.js";
|
|
2
|
+
import { pluginLogger } from "../../plugins/plugins.js";
|
|
3
|
+
/**
|
|
4
|
+
* How long the outbox waits for a channel's `push`. Past it the signal aborts
|
|
5
|
+
* and the delivery is retried, as a ManyChat send that timed out is. The same
|
|
6
|
+
* bound as a plugin's `perform` (specs/036); chosen, not measured.
|
|
7
|
+
*/
|
|
8
|
+
export const CHANNEL_PUSH_TIMEOUT_MS = 10_000;
|
|
9
|
+
/** A request that fails the channel's own schema: refused with a 400 before any turn (specs/038). */
|
|
10
|
+
export class ChannelInputError extends Error {
|
|
11
|
+
constructor(channel) {
|
|
12
|
+
super(`request does not match channel ${channel}'s schema`);
|
|
13
|
+
this.name = 'ChannelInputError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The route a channel is mounted at, beside ManyChat's (specs/038). */
|
|
17
|
+
export const channelRoute = (name) => `/v1/channels/${name}/message`;
|
|
18
|
+
/**
|
|
19
|
+
* A plugin's channel behind the `ChannelAdapter` port (specs/038, ADR-0005).
|
|
20
|
+
* The plugin translates; everything it does not translate is decided here,
|
|
21
|
+
* the same way for every plugin channel: the tenant, the channel, and that
|
|
22
|
+
* the request carries no contact token and no offering.
|
|
23
|
+
*
|
|
24
|
+
* The contact's ID is prefixed with the channel's name, so a contact on one
|
|
25
|
+
* channel never shares a conversation, an order or an outbox lane with one on
|
|
26
|
+
* another whose platform happens to use the same ID.
|
|
27
|
+
*/
|
|
28
|
+
export class PluginChannelAdapter {
|
|
29
|
+
name;
|
|
30
|
+
plugin;
|
|
31
|
+
route;
|
|
32
|
+
channel;
|
|
33
|
+
logger;
|
|
34
|
+
prefix;
|
|
35
|
+
constructor(plugin, channel, logger) {
|
|
36
|
+
this.plugin = plugin;
|
|
37
|
+
this.channel = channel;
|
|
38
|
+
this.logger = logger;
|
|
39
|
+
this.name = channel.name;
|
|
40
|
+
this.route = channelRoute(channel.name);
|
|
41
|
+
this.prefix = `${channel.name}:`;
|
|
42
|
+
}
|
|
43
|
+
/** Whether the request passes the channel's schema; checked before anything reads it (C3). */
|
|
44
|
+
accepts(raw) {
|
|
45
|
+
return this.channel.inbound.safeParse(raw).success;
|
|
46
|
+
}
|
|
47
|
+
parse(raw, ctx) {
|
|
48
|
+
const checked = this.channel.inbound.safeParse(raw);
|
|
49
|
+
if (!checked.success)
|
|
50
|
+
throw new ChannelInputError(this.name);
|
|
51
|
+
// What the plugin returns is an external boundary too (C3).
|
|
52
|
+
const message = ChannelMessage.parse(this.channel.parse(checked.data));
|
|
53
|
+
return {
|
|
54
|
+
tenantId: ctx.tenantId,
|
|
55
|
+
subscriberId: `${this.prefix}${message.subscriberId}`,
|
|
56
|
+
text: message.text,
|
|
57
|
+
channel: this.name,
|
|
58
|
+
contactName: message.contactName ?? null,
|
|
59
|
+
locale: message.locale ?? null,
|
|
60
|
+
// The channel's own auth binds its contacts: there is no token to
|
|
61
|
+
// present, and none is issued (specs/038 § A plugin channel carries no
|
|
62
|
+
// contact token).
|
|
63
|
+
contactToken: null,
|
|
64
|
+
offering: null,
|
|
65
|
+
receivedAt: new Date(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
render(reply) {
|
|
69
|
+
return this.channel.render({ messages: reply.messages, escalate: reply.escalate });
|
|
70
|
+
}
|
|
71
|
+
/** Delivers a lost race's reply (specs/038 § Outbound delivery is the adapter's `push`). */
|
|
72
|
+
async push(to, reply) {
|
|
73
|
+
await this.deliver(to.subscriberId, { messages: reply.messages, escalate: reply.escalate });
|
|
74
|
+
}
|
|
75
|
+
/** What `push` sends, from what the outbox keeps of a reply. */
|
|
76
|
+
async deliver(to, reply) {
|
|
77
|
+
const subscriberId = to.startsWith(this.prefix) ? to.slice(this.prefix.length) : to;
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
let timer;
|
|
80
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
81
|
+
timer = setTimeout(() => {
|
|
82
|
+
controller.abort();
|
|
83
|
+
reject(new Error(`channel ${this.name} push timed out after ${CHANNEL_PUSH_TIMEOUT_MS} ms`));
|
|
84
|
+
}, CHANNEL_PUSH_TIMEOUT_MS);
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
await Promise.race([
|
|
88
|
+
Promise.resolve().then(() => this.channel.push({
|
|
89
|
+
subscriberId,
|
|
90
|
+
reply,
|
|
91
|
+
logger: pluginLogger(this.logger, this.plugin, subscriberId),
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
})),
|
|
94
|
+
timedOut,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** An adapter for each channel the plugins add, by name, logging through `logger`. */
|
|
103
|
+
export function channelAdapters(plugins, logger) {
|
|
104
|
+
return new Map(plugins.channels.map(({ plugin, channel }) => [
|
|
105
|
+
channel.name,
|
|
106
|
+
new PluginChannelAdapter(plugin, channel, logger),
|
|
107
|
+
]));
|
|
108
|
+
}
|
package/dist/cli/run.js
CHANGED
|
@@ -112,11 +112,13 @@ export async function run(argv) {
|
|
|
112
112
|
}
|
|
113
113
|
throw error;
|
|
114
114
|
}
|
|
115
|
-
// Each plugin's read tools beside its write tools (specs/039).
|
|
115
|
+
// Each plugin's read tools beside its write tools (specs/039), and its channels.
|
|
116
116
|
const tools = (names) => (names.length > 0 ? names.join(', ') : 'none');
|
|
117
117
|
const loaded = summary.length > 0
|
|
118
118
|
? `; plugins: ${summary
|
|
119
|
-
.map(entry => `${entry.plugin} (writes: ${tools(entry.writes)}; reads: ${tools(entry.reads)}
|
|
119
|
+
.map(entry => `${entry.plugin} (writes: ${tools(entry.writes)}; reads: ${tools(entry.reads)}` +
|
|
120
|
+
// Only a plugin that adds one says so (specs/038).
|
|
121
|
+
(entry.channels.length > 0 ? `; channels: ${entry.channels.join(', ')})` : ')'))
|
|
120
122
|
.join(', ')}`
|
|
121
123
|
: '';
|
|
122
124
|
console.log(`config check passed (${configDir}${loaded})`);
|
|
@@ -114,6 +114,17 @@ export declare const InboundMessage: z.ZodObject<{
|
|
|
114
114
|
receivedAt: z.ZodDate;
|
|
115
115
|
}, z.core.$strip>;
|
|
116
116
|
export type InboundMessage = z.infer<typeof InboundMessage>;
|
|
117
|
+
/**
|
|
118
|
+
* What a plugin channel's `parse` returns, checked before the turn reads it
|
|
119
|
+
* (specs/038, C3). The bounds are ManyChat's inbound ones.
|
|
120
|
+
*/
|
|
121
|
+
export declare const ChannelMessage: z.ZodObject<{
|
|
122
|
+
subscriberId: z.ZodString;
|
|
123
|
+
text: z.ZodString;
|
|
124
|
+
contactName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
125
|
+
locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
126
|
+
}, z.core.$strict>;
|
|
127
|
+
export type ChannelMessage = z.infer<typeof ChannelMessage>;
|
|
117
128
|
/** Why a turn ended the way it did — recorded per turn for observability. */
|
|
118
129
|
export declare const TurnOutcome: z.ZodEnum<{
|
|
119
130
|
error: "error";
|
package/dist/contracts/agent.js
CHANGED
|
@@ -116,6 +116,18 @@ export const InboundMessage = z.object({
|
|
|
116
116
|
offering: z.string().nullish(),
|
|
117
117
|
receivedAt: z.date(),
|
|
118
118
|
});
|
|
119
|
+
/**
|
|
120
|
+
* What a plugin channel's `parse` returns, checked before the turn reads it
|
|
121
|
+
* (specs/038, C3). The bounds are ManyChat's inbound ones.
|
|
122
|
+
*/
|
|
123
|
+
export const ChannelMessage = z
|
|
124
|
+
.object({
|
|
125
|
+
subscriberId: z.string().min(1).max(256),
|
|
126
|
+
text: z.string().max(4096),
|
|
127
|
+
contactName: z.string().max(256).nullish(),
|
|
128
|
+
locale: z.string().max(64).nullish(),
|
|
129
|
+
})
|
|
130
|
+
.strict();
|
|
119
131
|
/** Why a turn ended the way it did — recorded per turn for observability. */
|
|
120
132
|
export const TurnOutcome = z.enum([
|
|
121
133
|
'answered_inline', // model won the race
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The bare `manychat-ai-agent` entry point: the plugin API (specs/036, with
|
|
3
|
-
* read tools from specs/039). The agent itself is run through its CLI or
|
|
3
|
+
* read tools from specs/039 and provisional channels from specs/038). The agent itself is run through its CLI or
|
|
4
4
|
* image, never imported.
|
|
5
5
|
*/
|
|
6
|
-
export { definePlugin, defineReadTool, defineTool, PLUGIN_API_VERSION, SUPPORTED_PLUGIN_API_VERSIONS, } from './plugins/api.ts';
|
|
7
|
-
export type { ParamsOf, Plugin, PluginCall, PluginLogger, PluginParameter, PluginParameters, PluginReadTool, PluginTool, ReadParameter, ReadParameters, ResultField, ResultFields, ResultOf, } from './plugins/api.ts';
|
|
6
|
+
export { CHANNEL_API_VERSION, defineChannel, definePlugin, defineReadTool, defineTool, PLUGIN_API_VERSION, SUPPORTED_CHANNEL_API_VERSIONS, SUPPORTED_PLUGIN_API_VERSIONS, } from './plugins/api.ts';
|
|
7
|
+
export type { ChannelMessage, ChannelPush, ChannelReply, InboundSchema, ParamsOf, Plugin, PluginCall, PluginChannel, PluginLogger, PluginParameter, PluginParameters, PluginReadTool, PluginTool, ReadParameter, ReadParameters, ResultField, ResultFields, ResultOf, } from './plugins/api.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The bare `manychat-ai-agent` entry point: the plugin API (specs/036, with
|
|
3
|
-
* read tools from specs/039). The agent itself is run through its CLI or
|
|
3
|
+
* read tools from specs/039 and provisional channels from specs/038). The agent itself is run through its CLI or
|
|
4
4
|
* image, never imported.
|
|
5
5
|
*/
|
|
6
|
-
export { definePlugin, defineReadTool, defineTool, PLUGIN_API_VERSION, SUPPORTED_PLUGIN_API_VERSIONS, } from "./plugins/api.js";
|
|
6
|
+
export { CHANNEL_API_VERSION, defineChannel, definePlugin, defineReadTool, defineTool, PLUGIN_API_VERSION, SUPPORTED_CHANNEL_API_VERSIONS, SUPPORTED_PLUGIN_API_VERSIONS, } from "./plugins/api.js";
|
package/dist/outbox/queue.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface ReplyPayload {
|
|
|
15
15
|
messages: string[];
|
|
16
16
|
actions?: StagedAction[];
|
|
17
17
|
turnId?: string;
|
|
18
|
+
/** The plugin channel that delivers it, through its adapter's `push`; ManyChat when absent (specs/038). */
|
|
19
|
+
channel?: string;
|
|
20
|
+
/** Set on a handoff queued for a plugin channel, which is told so (specs/038). */
|
|
21
|
+
escalate?: true;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* A deferred reply, or the retry of a contact token's write (specs/019). The
|
|
@@ -45,6 +49,8 @@ export declare class OutboxQueue {
|
|
|
45
49
|
} | undefined;
|
|
46
50
|
/** Held until then: a flow sent this turn is still playing (specs/030). */
|
|
47
51
|
notBefore?: Date | undefined;
|
|
52
|
+
/** A plugin channel's name; ManyChat when absent (specs/038). */
|
|
53
|
+
channel?: string | undefined;
|
|
48
54
|
}): Promise<string>;
|
|
49
55
|
/**
|
|
50
56
|
* Whether a reply to the contact is still queued: pending, or being
|
package/dist/outbox/queue.js
CHANGED
|
@@ -8,6 +8,11 @@ export class OutboxQueue {
|
|
|
8
8
|
}
|
|
9
9
|
async enqueue(input) {
|
|
10
10
|
const payload = { messages: input.reply.messages };
|
|
11
|
+
if (input.channel !== undefined) {
|
|
12
|
+
payload.channel = input.channel;
|
|
13
|
+
if (input.reply.escalate)
|
|
14
|
+
payload.escalate = true;
|
|
15
|
+
}
|
|
11
16
|
if (input.actions && input.actions.staged.length > 0) {
|
|
12
17
|
payload.actions = [...input.actions.staged];
|
|
13
18
|
payload.turnId = input.actions.turnId;
|
package/dist/outbox/worker.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Database } from '../db/client.ts';
|
|
2
2
|
import type { ManyChatClient } from '../channels/manychat/client.ts';
|
|
3
3
|
import type { Plugins } from '../plugins/plugins.ts';
|
|
4
|
+
/**
|
|
5
|
+
* A reply queued for a channel no plugin adds any more, because the process
|
|
6
|
+
* restarted without it. Never retried and never sent to ManyChat (specs/038).
|
|
7
|
+
*/
|
|
8
|
+
export declare class ChannelNotLoadedError extends Error {
|
|
9
|
+
constructor(channel: string);
|
|
10
|
+
}
|
|
4
11
|
export interface WorkerLogger {
|
|
5
12
|
info: (obj: object, msg: string) => void;
|
|
6
13
|
warn: (obj: object, msg: string) => void;
|
|
@@ -30,6 +37,8 @@ export declare class OutboxWorker {
|
|
|
30
37
|
private readonly store;
|
|
31
38
|
private readonly nudges;
|
|
32
39
|
private readonly actions;
|
|
40
|
+
/** The plugins' channels, which deliver their own replies (specs/038). */
|
|
41
|
+
private readonly channels;
|
|
33
42
|
private running;
|
|
34
43
|
/**
|
|
35
44
|
* Set by the stop function. A contact's rows after the one being sent go
|
package/dist/outbox/worker.js
CHANGED
|
@@ -6,6 +6,17 @@ import { performActions, performedOffering } from "../conversation/actions.js";
|
|
|
6
6
|
import { recordOf } from "../agent/tools.js";
|
|
7
7
|
import { NudgeStore } from "../nudge/store.js";
|
|
8
8
|
import { NudgingPerformer } from "../nudge/performer.js";
|
|
9
|
+
import { channelAdapters } from "../channels/plugin/adapter.js";
|
|
10
|
+
/**
|
|
11
|
+
* A reply queued for a channel no plugin adds any more, because the process
|
|
12
|
+
* restarted without it. Never retried and never sent to ManyChat (specs/038).
|
|
13
|
+
*/
|
|
14
|
+
export class ChannelNotLoadedError extends Error {
|
|
15
|
+
constructor(channel) {
|
|
16
|
+
super(`channel ${channel} is not loaded`);
|
|
17
|
+
this.name = 'ChannelNotLoadedError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
9
20
|
export class OutboxWorker {
|
|
10
21
|
opts;
|
|
11
22
|
queue;
|
|
@@ -13,6 +24,8 @@ export class OutboxWorker {
|
|
|
13
24
|
store;
|
|
14
25
|
nudges;
|
|
15
26
|
actions;
|
|
27
|
+
/** The plugins' channels, which deliver their own replies (specs/038). */
|
|
28
|
+
channels;
|
|
16
29
|
running = false;
|
|
17
30
|
/**
|
|
18
31
|
* Set by the stop function. A contact's rows after the one being sent go
|
|
@@ -28,6 +41,7 @@ export class OutboxWorker {
|
|
|
28
41
|
this.store = new ConversationStore(opts.db);
|
|
29
42
|
this.nudges = new NudgeStore(opts.db);
|
|
30
43
|
this.actions = opts.plugins ? opts.plugins.performer(opts.client, opts.logger) : opts.client;
|
|
44
|
+
this.channels = opts.plugins ? channelAdapters(opts.plugins, opts.logger) : new Map();
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* The actions deferred with a reply, performed once, after its first
|
|
@@ -69,7 +83,17 @@ export class OutboxWorker {
|
|
|
69
83
|
}
|
|
70
84
|
async deliver(row) {
|
|
71
85
|
if (row.kind === 'reply') {
|
|
72
|
-
|
|
86
|
+
const { channel, messages, escalate } = row.payload;
|
|
87
|
+
if (channel === undefined) {
|
|
88
|
+
await this.opts.client.sendText(row.subscriberId, messages);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// A plugin channel's reply goes out through its own adapter, never
|
|
92
|
+
// through ManyChat (specs/038).
|
|
93
|
+
const adapter = this.channels.get(channel);
|
|
94
|
+
if (!adapter)
|
|
95
|
+
throw new ChannelNotLoadedError(channel);
|
|
96
|
+
await adapter.deliver(row.subscriberId, { messages, escalate: escalate === true });
|
|
73
97
|
return;
|
|
74
98
|
}
|
|
75
99
|
// The token that failed to land was never stored, so a fresh one replaces
|
|
@@ -140,7 +164,10 @@ export class OutboxWorker {
|
|
|
140
164
|
// ManyChat's own verdict when it is ManyChat's error, and a retry
|
|
141
165
|
// otherwise: an unknown failure, such as the database, must not
|
|
142
166
|
// discard a reply (specs/022 § Retries follow the SDK's retryable).
|
|
143
|
-
const retryable = error instanceof ManyChatError
|
|
167
|
+
const retryable = error instanceof ManyChatError
|
|
168
|
+
? error.retryable
|
|
169
|
+
: // A channel that is gone will not come back by retrying (specs/038).
|
|
170
|
+
!(error instanceof ChannelNotLoadedError);
|
|
144
171
|
// A token write's error is kept to its status: ManyChat's answer to it
|
|
145
172
|
// could quote the value it was sent (specs/019).
|
|
146
173
|
const message = row.kind === 'contact_token'
|