manychat-ai-agent 0.25.0 → 0.26.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 +27 -0
- package/dist/contracts/config.d.ts +3 -0
- package/dist/contracts/config.js +9 -0
- package/dist/conversation/turns.d.ts +5 -0
- package/dist/conversation/turns.js +21 -4
- package/dist/routes/turn.js +57 -48
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.0](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.25.0...v0.26.0) (2026-10-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* wait for a burst to end before calling the model ([729bfca](https://github.com/pedronastasi/manychat-ai-agent/commit/729bfca32c20ea1d44d6eef2438ab852ec1b9bfb))
|
|
9
|
+
* wait for a burst to end before calling the model ([2cd1d91](https://github.com/pedronastasi/manychat-ai-agent/commit/2cd1d915b046b0972b0ca303e34bc3286ac08ebd))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **deps:** update patch dependencies ([#239](https://github.com/pedronastasi/manychat-ai-agent/issues/239)) ([371bfaf](https://github.com/pedronastasi/manychat-ai-agent/commit/371bfaf4dbb525f23f73f4dfbc3691afd393ccf8))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Specs and Docs
|
|
18
|
+
|
|
19
|
+
* record PR 244 in spec 047 ([52fd1bd](https://github.com/pedronastasi/manychat-ai-agent/commit/52fd1bda1af595c7094a05aaa97c82213e84c585))
|
|
20
|
+
* specify a burst window before the model (spec 047) ([3351037](https://github.com/pedronastasi/manychat-ai-agent/commit/3351037174ab519bb7a9387c9a878db72e7747d9))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Dependencies
|
|
24
|
+
|
|
25
|
+
* **deps:** lock file maintenance ([5c22bf0](https://github.com/pedronastasi/manychat-ai-agent/commit/5c22bf0622de025e5ade1155b9f9dd1ebcbac90c))
|
|
26
|
+
* **deps:** lock file maintenance ([ca4b49b](https://github.com/pedronastasi/manychat-ai-agent/commit/ca4b49b3deee81c3951b4aa9ae80d24d73676ad2))
|
|
27
|
+
* **deps:** update pnpm to v12.14.0 ([6599f97](https://github.com/pedronastasi/manychat-ai-agent/commit/6599f97b7e27f4b41d1dced8e90c8a188db66c42))
|
|
28
|
+
* **deps:** update pnpm to v12.14.0 ([c68b87e](https://github.com/pedronastasi/manychat-ai-agent/commit/c68b87ee8e72786f201b372f037304cb387fa42b))
|
|
29
|
+
|
|
3
30
|
## [0.25.0](https://github.com/pedronastasi/manychat-ai-agent/compare/v0.24.1...v0.25.0) (2026-10-16)
|
|
4
31
|
|
|
5
32
|
|
|
@@ -85,6 +85,8 @@ export declare const LearningSchema: z.ZodObject<{
|
|
|
85
85
|
maxRunCostUsd: z.ZodNumber;
|
|
86
86
|
}, z.core.$strict>;
|
|
87
87
|
export type Learning = z.infer<typeof LearningSchema>;
|
|
88
|
+
/** The longest `burstWindowMs`: it leaves the model most of the race (specs/047). */
|
|
89
|
+
export declare const BURST_WINDOW_MAX_MS = 4000;
|
|
88
90
|
export declare const RulesSchema: z.ZodObject<{
|
|
89
91
|
messages: z.ZodObject<{
|
|
90
92
|
acknowledgement: z.ZodString;
|
|
@@ -109,6 +111,7 @@ export declare const RulesSchema: z.ZodObject<{
|
|
|
109
111
|
turnsPerSubscriberPerHour: z.ZodDefault<z.ZodNumber>;
|
|
110
112
|
}, z.core.$strip>;
|
|
111
113
|
foldBursts: z.ZodDefault<z.ZodBoolean>;
|
|
114
|
+
burstWindowMs: z.ZodDefault<z.ZodNumber>;
|
|
112
115
|
learning: z.ZodOptional<z.ZodObject<{
|
|
113
116
|
language: z.ZodString;
|
|
114
117
|
convertedTag: z.ZodString;
|
package/dist/contracts/config.js
CHANGED
|
@@ -133,6 +133,8 @@ export const LearningSchema = z
|
|
|
133
133
|
maxRunCostUsd: z.number().positive(),
|
|
134
134
|
})
|
|
135
135
|
.strict();
|
|
136
|
+
/** The longest `burstWindowMs`: it leaves the model most of the race (specs/047). */
|
|
137
|
+
export const BURST_WINDOW_MAX_MS = 4_000;
|
|
136
138
|
export const RulesSchema = z.object({
|
|
137
139
|
messages: MessagesSchema,
|
|
138
140
|
confidenceThreshold: z.number().min(0).max(1).default(0.6),
|
|
@@ -172,6 +174,13 @@ export const RulesSchema = z.object({
|
|
|
172
174
|
* Bubbles that wait behind a running turn are folded whatever this says.
|
|
173
175
|
*/
|
|
174
176
|
foldBursts: z.boolean().default(false),
|
|
177
|
+
/**
|
|
178
|
+
* How long a turn waits for the contact to write again before it calls the
|
|
179
|
+
* model (specs/047). A bubble that arrives in that time folds this turn's
|
|
180
|
+
* message into its own. The wait counts against the race deadline (C7), so
|
|
181
|
+
* it is capped well inside it. 0, the default, waits for nothing.
|
|
182
|
+
*/
|
|
183
|
+
burstWindowMs: z.number().int().min(0).max(BURST_WINDOW_MAX_MS).default(0),
|
|
175
184
|
/**
|
|
176
185
|
* Learning from outcomes (specs/031). Absent, no learning job runs, no
|
|
177
186
|
* playbook is loaded, and the prompt is what it was before that spec.
|
|
@@ -18,6 +18,11 @@ export interface TurnSlot {
|
|
|
18
18
|
* yet: the contact kept writing, and that turn answers them (specs/046).
|
|
19
19
|
*/
|
|
20
20
|
followed: () => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Settles with `true` as soon as this turn is followed, or with `false` once
|
|
23
|
+
* `ms` have passed without it: the contact has gone quiet (specs/047).
|
|
24
|
+
*/
|
|
25
|
+
quiet: (ms: number) => Promise<boolean>;
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
23
28
|
* One turn at a time per contact, in the order their requests arrived
|
|
@@ -28,6 +28,11 @@ export class TurnLanes {
|
|
|
28
28
|
const places = this.places.get(contact) ?? [];
|
|
29
29
|
places.push(place);
|
|
30
30
|
this.places.set(contact, places);
|
|
31
|
+
const followed = () => {
|
|
32
|
+
const current = this.places.get(contact) ?? [];
|
|
33
|
+
const index = current.indexOf(place);
|
|
34
|
+
return index >= 0 && current.slice(index + 1).some(later => later.claimed);
|
|
35
|
+
};
|
|
31
36
|
const gone = () => {
|
|
32
37
|
const current = this.places.get(contact);
|
|
33
38
|
if (!current)
|
|
@@ -47,12 +52,24 @@ export class TurnLanes {
|
|
|
47
52
|
},
|
|
48
53
|
claim: () => {
|
|
49
54
|
place.claimed = true;
|
|
50
|
-
},
|
|
51
|
-
followed: () => {
|
|
52
55
|
const current = this.places.get(contact) ?? [];
|
|
53
|
-
const
|
|
54
|
-
|
|
56
|
+
for (const earlier of current.slice(0, current.indexOf(place)))
|
|
57
|
+
earlier.wake?.();
|
|
55
58
|
},
|
|
59
|
+
followed,
|
|
60
|
+
quiet: ms => new Promise(resolve => {
|
|
61
|
+
if (followed())
|
|
62
|
+
return resolve(true);
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
place.wake = undefined;
|
|
65
|
+
resolve(false);
|
|
66
|
+
}, ms);
|
|
67
|
+
place.wake = () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
place.wake = undefined;
|
|
70
|
+
resolve(true);
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
56
73
|
};
|
|
57
74
|
}
|
|
58
75
|
bounded(before) {
|
package/dist/routes/turn.js
CHANGED
|
@@ -150,58 +150,67 @@ export class TurnHandler {
|
|
|
150
150
|
};
|
|
151
151
|
const leave = () => void background.then(slot.leave, slot.leave);
|
|
152
152
|
const expired = () => logger.error({ conversation: known?.id }, 'turn wait expired');
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
logger.error({ conversation: known?.id, err: String(error) }, 'waiting turn failed');
|
|
173
|
-
await this.handOffSilent(inbound);
|
|
174
|
-
})
|
|
175
|
-
.finally(leave);
|
|
176
|
-
// The previous turn has started the conversation by now; its reply,
|
|
177
|
-
// or its holding line, is the contact's answer for the moment.
|
|
178
|
-
const current = known ?? (await this.store.find(inbound.tenantId, inbound.subscriberId));
|
|
179
|
-
logger.info({ conversation: current?.id }, 'turn waits for the previous one');
|
|
180
|
-
return {
|
|
181
|
-
reply: {
|
|
182
|
-
messages: [],
|
|
183
|
-
escalate: false,
|
|
184
|
-
escalation_reason: null,
|
|
185
|
-
confidence: 1,
|
|
186
|
-
closing_question: null,
|
|
187
|
-
},
|
|
188
|
-
outcome: 'deferred',
|
|
189
|
-
conversationId: current?.id ?? '',
|
|
190
|
-
binding,
|
|
191
|
-
silent: true,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (waited === 'expired')
|
|
153
|
+
/**
|
|
154
|
+
* Whether this turn only records its message, which a later turn of the
|
|
155
|
+
* contact answers (specs/046): the contact wrote again while it waited for
|
|
156
|
+
* their previous turn, or, with a burst window, before that window passed
|
|
157
|
+
* in silence (specs/047). Only a turn that reads history folds: any other
|
|
158
|
+
* runs, and is bound by the token state it finds then (specs/019).
|
|
159
|
+
*/
|
|
160
|
+
const folds = async () => {
|
|
161
|
+
if (!readsHistory || !this.foldable(inbound))
|
|
162
|
+
return false;
|
|
163
|
+
if (slot.waited && slot.followed())
|
|
164
|
+
return true;
|
|
165
|
+
const windowMs = this.deps.rules.burstWindowMs;
|
|
166
|
+
return windowMs > 0 && (await slot.quiet(windowMs));
|
|
167
|
+
};
|
|
168
|
+
// The wait for the previous turn and the burst window both count against
|
|
169
|
+
// the deadline (C7): a turn still deciding at it is answered silently now.
|
|
170
|
+
const decided = (async () => {
|
|
171
|
+
if (slot.waited && (await slot.ready) === 'expired')
|
|
195
172
|
expired();
|
|
173
|
+
return (await folds()) ? 'fold' : 'run';
|
|
174
|
+
})();
|
|
175
|
+
let timer;
|
|
176
|
+
const late = new Promise(resolve => {
|
|
177
|
+
timer = setTimeout(() => resolve('late'), Math.max(0, deadlineAt - Date.now()));
|
|
178
|
+
});
|
|
179
|
+
const first = await Promise.race([decided, late]).finally(() => clearTimeout(timer));
|
|
180
|
+
if (first === 'late') {
|
|
181
|
+
void decided
|
|
182
|
+
.then(async (decision) => {
|
|
183
|
+
if (decision === 'fold')
|
|
184
|
+
await this.fold(inbound, binding);
|
|
185
|
+
else
|
|
186
|
+
await this.run(inbound, { ...opts, late: true });
|
|
187
|
+
})
|
|
188
|
+
.catch(async (error) => {
|
|
189
|
+
logger.error({ conversation: known?.id, err: String(error) }, 'waiting turn failed');
|
|
190
|
+
await this.handOffSilent(inbound);
|
|
191
|
+
})
|
|
192
|
+
.finally(leave);
|
|
193
|
+
// The previous turn has started the conversation by now; its reply,
|
|
194
|
+
// or its holding line, is the contact's answer for the moment.
|
|
195
|
+
const current = known ?? (await this.store.find(inbound.tenantId, inbound.subscriberId));
|
|
196
|
+
logger.info({ conversation: current?.id }, 'turn answered silently while it waits');
|
|
197
|
+
return {
|
|
198
|
+
reply: {
|
|
199
|
+
messages: [],
|
|
200
|
+
escalate: false,
|
|
201
|
+
escalation_reason: null,
|
|
202
|
+
confidence: 1,
|
|
203
|
+
closing_question: null,
|
|
204
|
+
},
|
|
205
|
+
outcome: 'deferred',
|
|
206
|
+
conversationId: current?.id ?? '',
|
|
207
|
+
binding,
|
|
208
|
+
silent: true,
|
|
209
|
+
};
|
|
196
210
|
}
|
|
197
211
|
try {
|
|
198
|
-
|
|
199
|
-
// answers both, so this one only records it (specs/046). Only a turn
|
|
200
|
-
// that reads history folds: any other runs, and is bound by the token
|
|
201
|
-
// state it finds then (specs/019).
|
|
202
|
-
if (readsHistory && slot.waited && slot.followed() && this.foldable(inbound)) {
|
|
212
|
+
if (first === 'fold')
|
|
203
213
|
return await this.fold(inbound, binding);
|
|
204
|
-
}
|
|
205
214
|
return await this.run(inbound, opts);
|
|
206
215
|
}
|
|
207
216
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "manychat-ai-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=22"
|
|
38
38
|
},
|
|
39
|
-
"packageManager": "pnpm@12.
|
|
39
|
+
"packageManager": "pnpm@12.14.0",
|
|
40
40
|
"scripts": {
|
|
41
41
|
"dev": "node --experimental-strip-types --disable-warning=ExperimentalWarning --env-file=.env --watch src/main.ts",
|
|
42
42
|
"bootstrap": "node scripts/setup.mjs",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@ai-sdk/anthropic": "^4.0.53",
|
|
72
72
|
"@ai-sdk/google": "^4.0.69",
|
|
73
73
|
"@ai-sdk/openai": "^4.0.66",
|
|
74
|
-
"@ai-sdk/provider": "4.0.
|
|
74
|
+
"@ai-sdk/provider": "4.0.29",
|
|
75
75
|
"@fastify/helmet": "^13.1.1",
|
|
76
76
|
"@fastify/rate-limit": "^11.2.0",
|
|
77
77
|
"@fastify/swagger": "^9.8.1",
|