@aparte/runner-n8n 0.0.0-stage → 0.27.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/LICENSE +21 -0
- package/README.md +32 -2
- package/dist/history.d.ts +35 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/http.d.ts +18 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +396 -0
- package/dist/index.js.map +1 -0
- package/dist/runner.d.ts +49 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/wire.d.ts +72 -0
- package/dist/wire.d.ts.map +1 -0
- package/package.json +60 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aparté
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aparte/runner-n8n
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Put aparté on an **[n8n](https://n8n.io)** chat workflow. The workflow's agent runs in n8n, with
|
|
4
|
+
its model, its tools and its memory; aparté draws the chat on your page, streamed when the Chat
|
|
5
|
+
Trigger streams — in place of n8n's own chat widget.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @aparte/runner-n8n @aparte/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { AparteClient } from '@aparte/core';
|
|
13
|
+
import { createN8nRunner } from '@aparte/runner-n8n';
|
|
14
|
+
|
|
15
|
+
new AparteClient({
|
|
16
|
+
streamRunner: createN8nRunner({ webhookUrl: 'https://your-n8n.example.com/webhook/<id>/chat' }),
|
|
17
|
+
}).start();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **The workflow**: a Chat Trigger made publicly available, with your site in its *Allowed Origins
|
|
21
|
+
(CORS)*. Set its Response Mode to *Streaming* (and leave streaming on in the AI Agent) to see the
|
|
22
|
+
answer arrive as it is written, or to *When Last Node Finishes* for a whole answer.
|
|
23
|
+
- **Memory** is n8n's, per session: each turn sends only the new message. Give `sessionId` the
|
|
24
|
+
conversation's id when your app has several, or keeps one across reloads.
|
|
25
|
+
- **Images** the person attaches go to the workflow as files, as the Chat Trigger takes uploads.
|
|
26
|
+
- **Errors** say what to change: an inactive workflow, a trigger that streams while its agent does
|
|
27
|
+
not, a workflow that answers through response nodes, a proxy that timed out.
|
|
28
|
+
|
|
29
|
+
No n8n SDK is pulled in: the answer is plain JSON, parsed here, with `@aparte/core` as the only peer
|
|
30
|
+
dependency.
|
|
31
|
+
|
|
32
|
+
> Part of the [aparté](https://github.com/apartejs/aparte) monorepo. ESM-only.
|
|
33
|
+
> See **Providers → n8n** in the docs.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AparteMessage } from '@aparte/core';
|
|
2
|
+
export interface N8nHistoryOptions {
|
|
3
|
+
/** The Chat Trigger's production URL, the one the runner sends to. */
|
|
4
|
+
webhookUrl: string;
|
|
5
|
+
/** The session to read: the one the runner sends the conversation's turns in. */
|
|
6
|
+
sessionId: string;
|
|
7
|
+
/** Sent as `metadata`, as the widget sends it with this request too. */
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
/** Sent with the request; a function is called for it. */
|
|
10
|
+
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
11
|
+
/** @default the global `fetch` */
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
/** For a workflow on another origin that authenticates by cookie. */
|
|
14
|
+
credentials?: RequestCredentials;
|
|
15
|
+
/** @default 'sessionId', the Chat Trigger's */
|
|
16
|
+
chatSessionKey?: string;
|
|
17
|
+
/** Cancels the request. */
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The messages a chat shows, from what the memory stored: what the person wrote and what
|
|
22
|
+
* the agent answered. A tool's call and its result are left out, as the live stream
|
|
23
|
+
* leaves them out — where n8n's widget drew them as bubbles of raw JSON after a reload.
|
|
24
|
+
*/
|
|
25
|
+
export declare function n8nHistoryToMessages(stored: unknown[], { now }?: {
|
|
26
|
+
now?: number | undefined;
|
|
27
|
+
}): AparteMessage[];
|
|
28
|
+
/**
|
|
29
|
+
* Read a session's earlier messages from the workflow's memory — for a page that keeps
|
|
30
|
+
* the session id across reloads and shows the conversation again. Needs the Chat
|
|
31
|
+
* Trigger's *Load Previous Session* set to *From Memory*; when it is off, n8n answers
|
|
32
|
+
* with no messages.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadN8nHistory(options: N8nHistoryOptions): Promise<AparteMessage[]>;
|
|
35
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,MAAM,WAAW,iBAAiB;IAC9B,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACpG,kCAAkC;IAClC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAyBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,GAAgB,EAAE;;CAAK,GAAG,aAAa,EAAE,CAclG;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAyBzF"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An HTTP error from n8n, said so the developer knows what to change in the workflow.
|
|
3
|
+
*
|
|
4
|
+
* n8n answers an error as JSON `{ message, hint }` (a 404 for a workflow that is not
|
|
5
|
+
* active carries the hint to activate it), and a proxy in front of it answers as an HTML
|
|
6
|
+
* page — a 502 or a 504 when the workflow ran longer than the proxy waits.
|
|
7
|
+
*/
|
|
8
|
+
import { AparteError } from '@aparte/core';
|
|
9
|
+
/** What the request was for, said in the messages that depend on it. */
|
|
10
|
+
export type N8nRequestKind = 'answer' | 'history';
|
|
11
|
+
export declare function httpError(response: Response, kind: N8nRequestKind): Promise<AparteError>;
|
|
12
|
+
/**
|
|
13
|
+
* The request never reached an answer: n8n is down, the URL is wrong, or the browser
|
|
14
|
+
* refused the answer because the Chat Trigger does not allow this page's origin — which
|
|
15
|
+
* the browser reports exactly like a network failure, so all three are named.
|
|
16
|
+
*/
|
|
17
|
+
export declare function unreachable(error: unknown, url: string): unknown;
|
|
18
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAmB,MAAM,cAAc,CAAC;AAI5D,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAUlD,wBAAsB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,CAqC9F;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAShE"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aparte/runner-n8n — put aparté on an n8n chat workflow.
|
|
3
|
+
*
|
|
4
|
+
* The workflow's agent runs in n8n (its model, its tools, its memory per session);
|
|
5
|
+
* `AparteClient` draws its answer through the `streamRunner` seam, streamed when the
|
|
6
|
+
* Chat Trigger streams. `loadN8nHistory` reads a session's messages back after a reload.
|
|
7
|
+
*/
|
|
8
|
+
export { createN8nRunner } from './runner.js';
|
|
9
|
+
export type { N8nRunnerOptions } from './runner.js';
|
|
10
|
+
export { loadN8nHistory, n8nHistoryToMessages } from './history.js';
|
|
11
|
+
export type { N8nHistoryOptions } from './history.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { AparteError, AparteErrorCode, uuid } from "@aparte/core";
|
|
2
|
+
function opensStream(value) {
|
|
3
|
+
if (!isFrame(value)) return false;
|
|
4
|
+
const metadata = value.metadata;
|
|
5
|
+
const fromNode = typeof metadata === "object" && metadata !== null && typeof metadata["nodeId"] === "string";
|
|
6
|
+
if (value.type === "keepalive") return true;
|
|
7
|
+
if (value.type === "error") return fromNode || typeof value.content === "string";
|
|
8
|
+
return fromNode;
|
|
9
|
+
}
|
|
10
|
+
function isHtml(text) {
|
|
11
|
+
return /^\s*<(?:!doctype|html|head|body)[\s>]/i.test(text);
|
|
12
|
+
}
|
|
13
|
+
function parse(line) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(line);
|
|
16
|
+
} catch {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isFrame(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
|
|
22
|
+
}
|
|
23
|
+
function streamOf(frame) {
|
|
24
|
+
const m = frame.metadata ?? {};
|
|
25
|
+
return `${String(m["nodeId"] ?? "")}:${String(m["runIndex"] ?? 0)}:${String(m["itemIndex"] ?? 0)}`;
|
|
26
|
+
}
|
|
27
|
+
function frameToPart(frame) {
|
|
28
|
+
switch (frame.type) {
|
|
29
|
+
case "begin":
|
|
30
|
+
return { kind: "begin", stream: streamOf(frame) };
|
|
31
|
+
case "item":
|
|
32
|
+
return { kind: "item", stream: streamOf(frame), text: typeof frame.content === "string" ? frame.content : "" };
|
|
33
|
+
case "end":
|
|
34
|
+
return { kind: "end", stream: streamOf(frame) };
|
|
35
|
+
case "error":
|
|
36
|
+
return { kind: "error", message: typeof frame.content === "string" ? frame.content : "" };
|
|
37
|
+
// n8n's heartbeat: it says the workflow is still running, nothing to draw.
|
|
38
|
+
case "keepalive":
|
|
39
|
+
return void 0;
|
|
40
|
+
default:
|
|
41
|
+
return { kind: "unknown", type: frame.type };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function replyText(value) {
|
|
45
|
+
if (typeof value === "string") return value;
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
const texts = value.map(replyText);
|
|
48
|
+
return texts.every((t) => t === void 0) && value.length > 0 ? void 0 : texts.filter((t) => t !== void 0).join("\n\n");
|
|
49
|
+
}
|
|
50
|
+
if (typeof value !== "object" || value === null) return value === null || value === void 0 ? "" : String(value);
|
|
51
|
+
const record = value;
|
|
52
|
+
const key = ["output", "text", "message"].find((k) => record[k] !== void 0 && record[k] !== null);
|
|
53
|
+
if (key === void 0) return Object.keys(record).length > 0 ? void 0 : "";
|
|
54
|
+
const text = record[key];
|
|
55
|
+
if (typeof text === "string") return text;
|
|
56
|
+
if (typeof text === "object" && text !== null && text.type === "text" && typeof text.text === "string") {
|
|
57
|
+
return text.text;
|
|
58
|
+
}
|
|
59
|
+
if (typeof text === "number" || typeof text === "boolean") return String(text);
|
|
60
|
+
return ["```json", JSON.stringify(text, null, 2), "```"].join("\n");
|
|
61
|
+
}
|
|
62
|
+
function fieldsOf(value) {
|
|
63
|
+
let record = value;
|
|
64
|
+
while (Array.isArray(record)) record = record[0];
|
|
65
|
+
return Object.keys(record).slice(0, 6);
|
|
66
|
+
}
|
|
67
|
+
function wholePart(raw) {
|
|
68
|
+
const text = raw.trim();
|
|
69
|
+
if (!text) return void 0;
|
|
70
|
+
const value = parse(text);
|
|
71
|
+
if (value === void 0) return isHtml(text) ? { kind: "html" } : { kind: "reply", text: raw };
|
|
72
|
+
if (typeof value === "object" && value !== null && value.executionStarted === true) return { kind: "deferred" };
|
|
73
|
+
const reply = replyText(value);
|
|
74
|
+
return reply === void 0 ? { kind: "unreadable", fields: fieldsOf(value) } : { kind: "reply", text: reply };
|
|
75
|
+
}
|
|
76
|
+
async function* readN8nReply(body) {
|
|
77
|
+
const reader = body.getReader();
|
|
78
|
+
const decoder = new TextDecoder();
|
|
79
|
+
let mode = "unknown";
|
|
80
|
+
let raw = "";
|
|
81
|
+
let pending = "";
|
|
82
|
+
function* lines(text, last) {
|
|
83
|
+
const all = text.split("\n");
|
|
84
|
+
for (const [i, line] of all.entries()) {
|
|
85
|
+
if (!line.trim()) continue;
|
|
86
|
+
const value = parse(line);
|
|
87
|
+
if (mode === "unknown") {
|
|
88
|
+
if (!opensStream(value)) {
|
|
89
|
+
mode = "whole";
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
mode = "frames";
|
|
93
|
+
}
|
|
94
|
+
if (isFrame(value)) {
|
|
95
|
+
const part = frameToPart(value);
|
|
96
|
+
if (part) yield part;
|
|
97
|
+
} else if (last && i === all.length - 1 && line.trimStart().startsWith("{")) {
|
|
98
|
+
yield { kind: "cut" };
|
|
99
|
+
} else if (isHtml(line)) {
|
|
100
|
+
yield { kind: "html" };
|
|
101
|
+
} else {
|
|
102
|
+
yield { kind: "item", text: line };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
for (; ; ) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
const chunk = done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
110
|
+
raw += chunk;
|
|
111
|
+
if (mode !== "whole") {
|
|
112
|
+
pending += chunk;
|
|
113
|
+
const cut = done ? pending.length : pending.lastIndexOf("\n") + 1;
|
|
114
|
+
const complete = pending.slice(0, cut);
|
|
115
|
+
pending = pending.slice(cut);
|
|
116
|
+
yield* lines(complete, done);
|
|
117
|
+
}
|
|
118
|
+
if (done) break;
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
}
|
|
123
|
+
if (mode !== "frames") {
|
|
124
|
+
const part = wholePart(raw);
|
|
125
|
+
if (part) yield part;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const PROXY_STATUS = /* @__PURE__ */ new Set([502, 504, 524]);
|
|
129
|
+
function credentialNames(body) {
|
|
130
|
+
const list = Array.isArray(body.credentials) ? body.credentials : [];
|
|
131
|
+
const names = list.map((c) => typeof c === "object" && c !== null ? c.credentialName : void 0).filter((name) => typeof name === "string" && name.length > 0);
|
|
132
|
+
return names.length > 0 ? ` (${names.join(", ")})` : "";
|
|
133
|
+
}
|
|
134
|
+
async function httpError(response, kind) {
|
|
135
|
+
const status = response.status;
|
|
136
|
+
const text = (await response.text().catch(() => "")).trim();
|
|
137
|
+
const code = AparteError.codeForStatus(status) ?? (status === 404 ? AparteErrorCode.PROVIDER_MISCONFIGURED : AparteErrorCode.PROVIDER_ERROR);
|
|
138
|
+
const make = (message2, errorCode = code) => new AparteError(message2, errorCode, void 0, void 0, status);
|
|
139
|
+
const misconfigured = AparteErrorCode.PROVIDER_MISCONFIGURED;
|
|
140
|
+
if (text.startsWith("<") || (response.headers.get("content-type") ?? "").includes("text/html")) {
|
|
141
|
+
return PROXY_STATUS.has(status) ? make(`A proxy in front of n8n gave up waiting (HTTP ${status}). The workflow may still be running — its execution list shows the result.`, AparteErrorCode.NET_TIMEOUT) : make(`n8n answered HTTP ${status} with a web page instead of a chat answer. Check that the URL is the Chat Trigger's production URL, the one that ends in /chat.`, AparteError.codeForStatus(status) ?? misconfigured);
|
|
142
|
+
}
|
|
143
|
+
let body = {};
|
|
144
|
+
try {
|
|
145
|
+
body = JSON.parse(text);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
if (status === 428 || body.status === "credential_connections_required") {
|
|
149
|
+
return make(`The n8n workflow cannot run until its credentials are connected${credentialNames(body)}. Connect them in n8n, then try again.`, misconfigured);
|
|
150
|
+
}
|
|
151
|
+
if (body.status === "credential_readiness_check_failed") {
|
|
152
|
+
return make("n8n could not check the workflow's credentials. Open the workflow in n8n to see which one fails.");
|
|
153
|
+
}
|
|
154
|
+
if (kind === "history" && status === 500) {
|
|
155
|
+
return make(`n8n could not load the conversation's history. When the Chat Trigger's "Load Previous Session" is "From Memory", connect the memory node to the Chat Trigger as well as to the agent.`, misconfigured);
|
|
156
|
+
}
|
|
157
|
+
let message = typeof body.message === "string" ? body.message : text;
|
|
158
|
+
if (typeof body.message === "string" && typeof body.hint === "string") message = `${body.message} ${body.hint}`;
|
|
159
|
+
return make(message.slice(0, 400) || `The n8n workflow answered HTTP ${status}.`);
|
|
160
|
+
}
|
|
161
|
+
function unreachable(error, url) {
|
|
162
|
+
if (!(error instanceof TypeError)) return error;
|
|
163
|
+
const origin = typeof location === "undefined" ? "this page's origin" : location.origin;
|
|
164
|
+
const offline = typeof navigator !== "undefined" && navigator.onLine === false;
|
|
165
|
+
return new AparteError(
|
|
166
|
+
`Could not reach n8n at ${url}. Check that it is the Chat Trigger's production URL, that the workflow is published, and that ${origin} is in the trigger's Allowed Origins (CORS).`,
|
|
167
|
+
offline ? AparteErrorCode.NET_OFFLINE : AparteErrorCode.NET_ERROR
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const EMPTY_STREAM = `The n8n workflow streamed nothing. When its Chat Trigger streams, the node that answers has to stream too: turn on streaming in the AI Agent's options, or set the trigger's Response Mode to "When Last Node Finishes".`;
|
|
171
|
+
const EMPTY_WHOLE = "The n8n workflow finished without an answer: its last node returned no text. End the workflow on the node that answers — an AI Agent, or a node that returns an `output` field.";
|
|
172
|
+
const PROXY_TIMEOUT = "A proxy timed out waiting for n8n's answer. The workflow may still be running — its execution list shows the result.";
|
|
173
|
+
const DEFERRED = `This n8n workflow answers through response nodes, over n8n's chat WebSocket, which this runner does not read. Set the Chat Trigger's Response Mode to "Streaming" or "When Last Node Finishes".`;
|
|
174
|
+
const unreadable = (fields) => `The n8n workflow answered with the fields ${fields.map((f) => `"${f}"`).join(", ")} but no "output", "text" or "message" field, which is where a chat reads the answer. End the workflow on an AI Agent, or add a Set node that puts the reply in "output".`;
|
|
175
|
+
function lastUserIndex(messages) {
|
|
176
|
+
for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "user") return i;
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
function textOf$1(content) {
|
|
180
|
+
if (content === void 0) return "";
|
|
181
|
+
if (typeof content === "string") return content;
|
|
182
|
+
return content.flatMap((part) => part.type === "text" && part.text ? [part.text] : []).join("\n\n");
|
|
183
|
+
}
|
|
184
|
+
const EXTENSIONS = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
185
|
+
function imageFile(image, mimeType, index) {
|
|
186
|
+
const match = /^data:([^;,]+)?((?:;[^;,]*)*?);base64,(.*)$/s.exec(image);
|
|
187
|
+
if (!match) return void 0;
|
|
188
|
+
const type = mimeType ?? match[1] ?? "application/octet-stream";
|
|
189
|
+
const binary = atob(match[3] ?? "");
|
|
190
|
+
const bytes = new Uint8Array(binary.length);
|
|
191
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
192
|
+
return { blob: new Blob([bytes], { type }), name: `image-${index + 1}.${EXTENSIONS[type] ?? "bin"}` };
|
|
193
|
+
}
|
|
194
|
+
function filesOf(opts, message) {
|
|
195
|
+
if (opts.files && opts.files.length > 0) return opts.files.map((file) => ({ blob: file, name: file.name }));
|
|
196
|
+
return (Array.isArray(message?.content) ? message.content : []).flatMap((part) => part.type === "image" ? [part] : []).map((part, i) => imageFile(part.image, part.mimeType, i)).filter((file) => file !== void 0);
|
|
197
|
+
}
|
|
198
|
+
function createN8nRunner(options) {
|
|
199
|
+
const warn = options.onWarning ?? ((message) => console.warn(`[aparte] n8n: ${message}`));
|
|
200
|
+
const sessionKey = options.chatSessionKey ?? "sessionId";
|
|
201
|
+
const inputKey = options.chatInputKey ?? "chatInput";
|
|
202
|
+
let ownSession = `aparte-${uuid()}`;
|
|
203
|
+
return async (opts) => {
|
|
204
|
+
const emit = opts.emitter;
|
|
205
|
+
const doFetch = options.fetch ?? fetch;
|
|
206
|
+
try {
|
|
207
|
+
const messages = opts.baseRequest.messages;
|
|
208
|
+
const last = lastUserIndex(messages);
|
|
209
|
+
const message = last >= 0 ? messages[last] : void 0;
|
|
210
|
+
if (options.sessionId === void 0 && !messages.slice(0, Math.max(last, 0)).some((m) => m.role === "assistant")) {
|
|
211
|
+
ownSession = `aparte-${uuid()}`;
|
|
212
|
+
}
|
|
213
|
+
const headers = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
|
|
214
|
+
const sessionId = (typeof options.sessionId === "function" ? options.sessionId(opts) : options.sessionId) ?? ownSession;
|
|
215
|
+
const metadata = typeof options.metadata === "function" ? options.metadata(opts) : options.metadata;
|
|
216
|
+
const files = filesOf(opts, message);
|
|
217
|
+
const fields = {
|
|
218
|
+
action: "sendMessage",
|
|
219
|
+
[sessionKey]: sessionId,
|
|
220
|
+
[inputKey]: textOf$1(message?.content),
|
|
221
|
+
...metadata ? { metadata } : {}
|
|
222
|
+
};
|
|
223
|
+
let body;
|
|
224
|
+
const requestHeaders = { Accept: "text/plain" };
|
|
225
|
+
for (const [name, value] of Object.entries(headers)) if (name.toLowerCase() !== "content-type") requestHeaders[name] = value;
|
|
226
|
+
if (files.length > 0) {
|
|
227
|
+
const form = new FormData();
|
|
228
|
+
for (const [key, value] of Object.entries(fields)) form.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
229
|
+
for (const file of files) form.append("files", file.blob, file.name);
|
|
230
|
+
body = form;
|
|
231
|
+
} else {
|
|
232
|
+
body = JSON.stringify(fields);
|
|
233
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
234
|
+
}
|
|
235
|
+
const response = await doFetch(options.webhookUrl, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: requestHeaders,
|
|
238
|
+
body,
|
|
239
|
+
signal: opts.signal,
|
|
240
|
+
...options.credentials ? { credentials: options.credentials } : {}
|
|
241
|
+
}).catch((error) => {
|
|
242
|
+
throw unreachable(error, options.webhookUrl);
|
|
243
|
+
});
|
|
244
|
+
if (!response.ok) throw await httpError(response, "answer");
|
|
245
|
+
emit({ type: "run-start" });
|
|
246
|
+
emit({ type: "turn-start" });
|
|
247
|
+
let drawn = false;
|
|
248
|
+
let whole = false;
|
|
249
|
+
let textOpen = false;
|
|
250
|
+
let active;
|
|
251
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
252
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
253
|
+
const flush = () => {
|
|
254
|
+
if (textOpen) emit({ type: "text-flush" });
|
|
255
|
+
textOpen = false;
|
|
256
|
+
};
|
|
257
|
+
const draw = (text) => {
|
|
258
|
+
if (!text) return;
|
|
259
|
+
drawn = true;
|
|
260
|
+
textOpen = true;
|
|
261
|
+
emit({ type: "text-delta", delta: text });
|
|
262
|
+
};
|
|
263
|
+
const open = (stream) => {
|
|
264
|
+
active = stream;
|
|
265
|
+
flush();
|
|
266
|
+
};
|
|
267
|
+
const drain = () => {
|
|
268
|
+
active = void 0;
|
|
269
|
+
for (const [stream, queued] of waiting) {
|
|
270
|
+
waiting.delete(stream);
|
|
271
|
+
open(stream);
|
|
272
|
+
for (const text of queued.text) draw(text);
|
|
273
|
+
if (!queued.ended) return;
|
|
274
|
+
active = void 0;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const waitFor = (stream) => {
|
|
278
|
+
let queued = waiting.get(stream);
|
|
279
|
+
if (!queued) waiting.set(stream, queued = { text: [], ended: false });
|
|
280
|
+
return queued;
|
|
281
|
+
};
|
|
282
|
+
if (!response.body) throw new AparteError(EMPTY_STREAM, AparteErrorCode.PROVIDER_MISCONFIGURED);
|
|
283
|
+
for await (const part of readN8nReply(response.body)) {
|
|
284
|
+
switch (part.kind) {
|
|
285
|
+
case "begin":
|
|
286
|
+
if (active === void 0) open(part.stream);
|
|
287
|
+
else if (part.stream !== active) waitFor(part.stream);
|
|
288
|
+
break;
|
|
289
|
+
case "item":
|
|
290
|
+
if (part.stream === void 0 || part.stream === active) draw(part.text);
|
|
291
|
+
else if (active === void 0) {
|
|
292
|
+
open(part.stream);
|
|
293
|
+
draw(part.text);
|
|
294
|
+
} else waitFor(part.stream).text.push(part.text);
|
|
295
|
+
break;
|
|
296
|
+
case "end":
|
|
297
|
+
if (part.stream === active) drain();
|
|
298
|
+
else waitFor(part.stream).ended = true;
|
|
299
|
+
break;
|
|
300
|
+
case "error":
|
|
301
|
+
flush();
|
|
302
|
+
throw new AparteError(part.message || "The n8n workflow failed.", AparteErrorCode.PROVIDER_ERROR);
|
|
303
|
+
case "reply":
|
|
304
|
+
whole = true;
|
|
305
|
+
draw(part.text);
|
|
306
|
+
break;
|
|
307
|
+
case "unreadable":
|
|
308
|
+
throw new AparteError(unreadable(part.fields), AparteErrorCode.PROVIDER_MISCONFIGURED);
|
|
309
|
+
case "deferred":
|
|
310
|
+
throw new AparteError(DEFERRED, AparteErrorCode.PROVIDER_MISCONFIGURED);
|
|
311
|
+
case "html":
|
|
312
|
+
flush();
|
|
313
|
+
throw new AparteError(PROXY_TIMEOUT, AparteErrorCode.NET_TIMEOUT);
|
|
314
|
+
case "cut":
|
|
315
|
+
warn("n8n's stream stopped in the middle of a frame; the text before it is kept.");
|
|
316
|
+
break;
|
|
317
|
+
case "unknown":
|
|
318
|
+
if (!unknown.has(part.type)) warn(`n8n sent a "${part.type}" frame, which this runner does not read; it was skipped.`);
|
|
319
|
+
unknown.add(part.type);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
drain();
|
|
324
|
+
for (const [stream, queued] of waiting) {
|
|
325
|
+
waiting.delete(stream);
|
|
326
|
+
open(stream);
|
|
327
|
+
for (const text of queued.text) draw(text);
|
|
328
|
+
}
|
|
329
|
+
if (!drawn) throw new AparteError(whole ? EMPTY_WHOLE : EMPTY_STREAM, AparteErrorCode.PROVIDER_MISCONFIGURED);
|
|
330
|
+
flush();
|
|
331
|
+
emit({ type: "run-done" });
|
|
332
|
+
return void 0;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (opts.signal.aborted) return void 0;
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function classOf(message) {
|
|
340
|
+
const path = Array.isArray(message.id) ? message.id : [];
|
|
341
|
+
const last = path[path.length - 1];
|
|
342
|
+
return typeof last === "string" ? last : typeof message.type === "string" ? message.type : "";
|
|
343
|
+
}
|
|
344
|
+
function textOf(content) {
|
|
345
|
+
if (typeof content === "string") return content;
|
|
346
|
+
if (!Array.isArray(content)) return "";
|
|
347
|
+
return content.map((part) => typeof part === "string" ? part : typeof part === "object" && part !== null && part.type === "text" ? String(part.text ?? "") : "").filter(Boolean).join("\n\n");
|
|
348
|
+
}
|
|
349
|
+
function n8nHistoryToMessages(stored, { now = Date.now() } = {}) {
|
|
350
|
+
const messages = [];
|
|
351
|
+
stored.forEach((entry, index) => {
|
|
352
|
+
if (typeof entry !== "object" || entry === null) return;
|
|
353
|
+
const message = entry;
|
|
354
|
+
const kind = classOf(message);
|
|
355
|
+
const role = /^Human/.test(kind) || kind === "human" ? "user" : /^AI/.test(kind) || kind === "ai" ? "assistant" : void 0;
|
|
356
|
+
if (!role) return;
|
|
357
|
+
const content = textOf(message.kwargs?.content);
|
|
358
|
+
if (!content.trim()) return;
|
|
359
|
+
messages.push({ id: `n8n-${index}`, role, content, timestamp: now - (stored.length - index), status: "completed" });
|
|
360
|
+
});
|
|
361
|
+
return messages;
|
|
362
|
+
}
|
|
363
|
+
async function loadN8nHistory(options) {
|
|
364
|
+
const doFetch = options.fetch ?? fetch;
|
|
365
|
+
const headers = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
|
|
366
|
+
const sent = { "Content-Type": "application/json" };
|
|
367
|
+
for (const [name, value] of Object.entries(headers)) if (name.toLowerCase() !== "content-type") sent[name] = value;
|
|
368
|
+
const response = await doFetch(options.webhookUrl, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
headers: sent,
|
|
371
|
+
body: JSON.stringify({
|
|
372
|
+
action: "loadPreviousSession",
|
|
373
|
+
[options.chatSessionKey ?? "sessionId"]: options.sessionId,
|
|
374
|
+
...options.metadata ? { metadata: options.metadata } : {}
|
|
375
|
+
}),
|
|
376
|
+
...options.signal ? { signal: options.signal } : {},
|
|
377
|
+
...options.credentials ? { credentials: options.credentials } : {}
|
|
378
|
+
}).catch((error) => {
|
|
379
|
+
throw unreachable(error, options.webhookUrl);
|
|
380
|
+
});
|
|
381
|
+
if (!response.ok) throw await httpError(response, "history");
|
|
382
|
+
let body;
|
|
383
|
+
try {
|
|
384
|
+
body = await response.json();
|
|
385
|
+
} catch {
|
|
386
|
+
throw new AparteError("n8n answered the history request with something that is not JSON. Check that the URL is the Chat Trigger's production URL.", AparteErrorCode.PROVIDER_MISCONFIGURED);
|
|
387
|
+
}
|
|
388
|
+
const data = typeof body === "object" && body !== null ? body.data : void 0;
|
|
389
|
+
return n8nHistoryToMessages(Array.isArray(data) ? data : []);
|
|
390
|
+
}
|
|
391
|
+
export {
|
|
392
|
+
createN8nRunner,
|
|
393
|
+
loadN8nHistory,
|
|
394
|
+
n8nHistoryToMessages
|
|
395
|
+
};
|
|
396
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/wire.ts","../src/http.ts","../src/runner.ts","../src/history.ts"],"sourcesContent":["/**\n * The body of n8n's answer to a chat message, read as it arrives.\n *\n * Two shapes come back, both labelled `application/json` by n8n:\n * - **streamed** — a Chat Trigger (or Webhook node) whose Response Mode is *Streaming*\n * sends newline-delimited JSON frames, `begin` / `item` / `end` per node that streams,\n * `error` when the workflow fails, and `keepalive` every 30 seconds while nothing else\n * comes — so before the first `begin` when a slow node runs first;\n * - **whole** — any other Response Mode sends one value once the workflow ends: the last\n * node's item (`{ output }` for an AI Agent), or what a *Respond to Webhook* node sent.\n *\n * Which one it is shows on the first line: a frame has a known `type`. Read off\n * `@n8n/chat` 1.43.3, the widget n8n embeds, and recorded against n8n 2.35.7 and 2.44.6.\n */\n\n/** One thing an answer carries, in order. */\nexport type N8nReplyPart =\n /** A node starts streaming. `stream` names it (node, run, item). */\n | { kind: 'begin'; stream: string }\n /** A piece of text. `stream` is absent for a line that was not a frame. */\n | { kind: 'item'; stream?: string; text: string }\n | { kind: 'end'; stream: string }\n /** The workflow failed; its message. */\n | { kind: 'error'; message: string }\n /** A frame type this reader does not know (a newer n8n). */\n | { kind: 'unknown'; type: string }\n /** The whole answer of a workflow that does not stream. */\n | { kind: 'reply'; text: string }\n /** A whole answer that is JSON with none of the fields a chat reads; its field names. */\n | { kind: 'unreadable'; fields: string[] }\n /** The workflow answers later, through response nodes over n8n's chat WebSocket. */\n | { kind: 'deferred' }\n /** An HTML page instead of the answer: a proxy gave up waiting. */\n | { kind: 'html' }\n /** The stream stopped in the middle of a frame. */\n | { kind: 'cut' };\n\n/**\n * Whether the first line opens a stream. A whole answer can be any JSON — a Respond to\n * Webhook node may send `{ type: 'error', message }` or `{ type: 'item', output }` — so a\n * frame has to show it is one: n8n stamps every node frame with `metadata.nodeId`, its\n * `error` frame carries the text in `content`, and `keepalive` is n8n's alone.\n */\nfunction opensStream(value: unknown): boolean {\n if (!isFrame(value)) return false;\n const metadata = value.metadata;\n const fromNode = typeof metadata === 'object' && metadata !== null && typeof metadata['nodeId'] === 'string';\n if (value.type === 'keepalive') return true;\n if (value.type === 'error') return fromNode || typeof value.content === 'string';\n return fromNode;\n}\n\n/**\n * A proxy's error page: text that STARTS as HTML. An answer that merely contains markup —\n * an agent asked for a snippet — is not one, which is why this is a prefix test.\n */\nfunction isHtml(text: string): boolean {\n return /^\\s*<(?:!doctype|html|head|body)[\\s>]/i.test(text);\n}\n\nfunction parse(line: string): unknown {\n try { return JSON.parse(line); } catch { return undefined; }\n}\n\nfunction isFrame(value: unknown): value is { type: string; content?: unknown; metadata?: Record<string, unknown> } {\n return typeof value === 'object' && value !== null && !Array.isArray(value) && typeof (value as { type?: unknown }).type === 'string';\n}\n\n/** Which node's stream a frame belongs to: n8n streams one per node, run and item. */\nfunction streamOf(frame: { metadata?: Record<string, unknown> }): string {\n const m = frame.metadata ?? {};\n return `${String(m['nodeId'] ?? '')}:${String(m['runIndex'] ?? 0)}:${String(m['itemIndex'] ?? 0)}`;\n}\n\nfunction frameToPart(frame: { type: string; content?: unknown; metadata?: Record<string, unknown> }): N8nReplyPart | undefined {\n switch (frame.type) {\n case 'begin': return { kind: 'begin', stream: streamOf(frame) };\n case 'item': return { kind: 'item', stream: streamOf(frame), text: typeof frame.content === 'string' ? frame.content : '' };\n case 'end': return { kind: 'end', stream: streamOf(frame) };\n case 'error': return { kind: 'error', message: typeof frame.content === 'string' ? frame.content : '' };\n // n8n's heartbeat: it says the workflow is still running, nothing to draw.\n case 'keepalive': return undefined;\n default: return { kind: 'unknown', type: frame.type };\n }\n}\n\n/**\n * The text of a whole answer, the way n8n's widget reads it: `output`, else `text`, else\n * `message`; a `{ type: 'text', text }` part as its text. `undefined` when the value has\n * none of them — where the widget printed the JSON into the bubble, the most asked-about\n * failure on n8n's forum, this lets the runner say which field is missing instead.\n */\nexport function replyText(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) {\n const texts = value.map(replyText);\n return texts.every((t) => t === undefined) && value.length > 0 ? undefined : texts.filter((t) => t !== undefined).join('\\n\\n');\n }\n if (typeof value !== 'object' || value === null) return value === null || value === undefined ? '' : String(value);\n const record = value as Record<string, unknown>;\n const key = ['output', 'text', 'message'].find((k) => record[k] !== undefined && record[k] !== null);\n if (key === undefined) return Object.keys(record).length > 0 ? undefined : '';\n const text = record[key];\n if (typeof text === 'string') return text;\n if (typeof text === 'object' && text !== null && (text as { type?: unknown }).type === 'text' && typeof (text as { text?: unknown }).text === 'string') {\n return (text as { text: string }).text;\n }\n if (typeof text === 'number' || typeof text === 'boolean') return String(text);\n // An agent with an output parser answers with a structure: shown as such, readable.\n return ['```json', JSON.stringify(text, null, 2), '```'].join('\\n');\n}\n\n/**\n * The field names a whole answer carried, to name them in the error: the first item's, for\n * a list. Called only for a value `replyText` could not read, which always holds a record.\n */\nfunction fieldsOf(value: unknown): string[] {\n let record = value;\n while (Array.isArray(record)) record = record[0];\n return Object.keys(record as object).slice(0, 6);\n}\n\nfunction wholePart(raw: string): N8nReplyPart | undefined {\n const text = raw.trim();\n if (!text) return undefined;\n // Parsed first: a JSON answer is an answer, whatever markup its text holds.\n const value = parse(text);\n if (value === undefined) return isHtml(text) ? { kind: 'html' } : { kind: 'reply', text: raw };\n if (typeof value === 'object' && value !== null && (value as { executionStarted?: unknown }).executionStarted === true) return { kind: 'deferred' };\n const reply = replyText(value);\n return reply === undefined ? { kind: 'unreadable', fields: fieldsOf(value) } : { kind: 'reply', text: reply };\n}\n\n/** Yields what n8n's answer carries, frame by frame as it streams, or once for a whole answer. */\nexport async function* readN8nReply(body: ReadableStream<Uint8Array>): AsyncGenerator<N8nReplyPart> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n // Widened on purpose: `lines` below decides it, a closure TypeScript's narrowing does not follow.\n let mode = 'unknown' as 'unknown' | 'frames' | 'whole';\n let raw = '';\n let pending = '';\n\n function* lines(text: string, last: boolean): Generator<N8nReplyPart> {\n const all = text.split('\\n');\n for (const [i, line] of all.entries()) {\n if (!line.trim()) continue;\n const value = parse(line);\n if (mode === 'unknown') {\n if (!opensStream(value)) { mode = 'whole'; return; }\n mode = 'frames';\n }\n if (isFrame(value)) {\n const part = frameToPart(value);\n if (part) yield part;\n } else if (last && i === all.length - 1 && line.trimStart().startsWith('{')) {\n // The body ended inside a frame: what came before it stands, the fragment does not.\n yield { kind: 'cut' };\n } else if (isHtml(line)) {\n yield { kind: 'html' };\n } else {\n // A line that is not a frame is text, as the widget reads it.\n yield { kind: 'item', text: line };\n }\n }\n }\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n const chunk = done ? decoder.decode() : decoder.decode(value, { stream: true });\n raw += chunk;\n if (mode !== 'whole') {\n pending += chunk;\n const cut = done ? pending.length : pending.lastIndexOf('\\n') + 1;\n const complete = pending.slice(0, cut);\n pending = pending.slice(cut);\n yield* lines(complete, done);\n }\n if (done) break;\n }\n } finally {\n reader.releaseLock();\n }\n if (mode !== 'frames') {\n const part = wholePart(raw);\n if (part) yield part;\n }\n}\n","/**\n * An HTTP error from n8n, said so the developer knows what to change in the workflow.\n *\n * n8n answers an error as JSON `{ message, hint }` (a 404 for a workflow that is not\n * active carries the hint to activate it), and a proxy in front of it answers as an HTML\n * page — a 502 or a 504 when the workflow ran longer than the proxy waits.\n */\nimport { AparteError, AparteErrorCode } from '@aparte/core';\n\nconst PROXY_STATUS = new Set([502, 504, 524]);\n\n/** What the request was for, said in the messages that depend on it. */\nexport type N8nRequestKind = 'answer' | 'history';\n\nfunction credentialNames(body: { credentials?: unknown }): string {\n const list = Array.isArray(body.credentials) ? body.credentials : [];\n const names = list\n .map((c) => (typeof c === 'object' && c !== null ? (c as { credentialName?: unknown }).credentialName : undefined))\n .filter((name): name is string => typeof name === 'string' && name.length > 0);\n return names.length > 0 ? ` (${names.join(', ')})` : '';\n}\n\nexport async function httpError(response: Response, kind: N8nRequestKind): Promise<AparteError> {\n const status = response.status;\n const text = (await response.text().catch(() => '')).trim();\n // A 404 from a Chat Trigger is a workflow that is not published or a wrong URL: n8n is\n // set up so it cannot answer, which no retry mends.\n const code = AparteError.codeForStatus(status) ?? (status === 404 ? AparteErrorCode.PROVIDER_MISCONFIGURED : AparteErrorCode.PROVIDER_ERROR);\n const make = (message: string, errorCode: AparteErrorCode = code) => new AparteError(message, errorCode, undefined, undefined, status);\n const misconfigured = AparteErrorCode.PROVIDER_MISCONFIGURED;\n\n // A page, not an answer: never show its markup to the person chatting.\n if (text.startsWith('<') || (response.headers.get('content-type') ?? '').includes('text/html')) {\n return PROXY_STATUS.has(status)\n ? make(`A proxy in front of n8n gave up waiting (HTTP ${status}). The workflow may still be running — its execution list shows the result.`, AparteErrorCode.NET_TIMEOUT)\n // A status that says something keeps it (a 429, a 503, a refused key); an unmapped\n // one, a 404 above all, is a wrong URL: set up so it cannot answer.\n : make(`n8n answered HTTP ${status} with a web page instead of a chat answer. Check that the URL is the Chat Trigger's production URL, the one that ends in /chat.`, AparteError.codeForStatus(status) ?? misconfigured);\n }\n\n let body: { message?: unknown; hint?: unknown; status?: unknown; credentials?: unknown } = {};\n try { body = JSON.parse(text) as typeof body; } catch { /* not JSON: its text below */ }\n\n // The Chat Trigger checks the workflow's credentials before it runs it.\n if (status === 428 || body.status === 'credential_connections_required') {\n return make(`The n8n workflow cannot run until its credentials are connected${credentialNames(body)}. Connect them in n8n, then try again.`, misconfigured);\n }\n if (body.status === 'credential_readiness_check_failed') {\n return make('n8n could not check the workflow\\'s credentials. Open the workflow in n8n to see which one fails.');\n }\n // Loading a session's history from memory needs the memory on the trigger too.\n if (kind === 'history' && status === 500) {\n return make('n8n could not load the conversation\\'s history. When the Chat Trigger\\'s \"Load Previous Session\" '\n + 'is \"From Memory\", connect the memory node to the Chat Trigger as well as to the agent.', misconfigured);\n }\n\n let message = typeof body.message === 'string' ? body.message : text;\n if (typeof body.message === 'string' && typeof body.hint === 'string') message = `${body.message} ${body.hint}`;\n return make(message.slice(0, 400) || `The n8n workflow answered HTTP ${status}.`);\n}\n\n/**\n * The request never reached an answer: n8n is down, the URL is wrong, or the browser\n * refused the answer because the Chat Trigger does not allow this page's origin — which\n * the browser reports exactly like a network failure, so all three are named.\n */\nexport function unreachable(error: unknown, url: string): unknown {\n if (!(error instanceof TypeError)) return error;\n const origin = typeof location === 'undefined' ? \"this page's origin\" : location.origin;\n const offline = typeof navigator !== 'undefined' && navigator.onLine === false;\n return new AparteError(\n `Could not reach n8n at ${url}. Check that it is the Chat Trigger's production URL, that the workflow is published, `\n + `and that ${origin} is in the trigger's Allowed Origins (CORS).`,\n offline ? AparteErrorCode.NET_OFFLINE : AparteErrorCode.NET_ERROR,\n );\n}\n","/**\n * createN8nRunner — an n8n chat workflow, drawn by aparté.\n *\n * The workflow's agent runs in n8n, with its model, its tools and its memory; n8n keeps\n * the conversation per session. So each turn sends what n8n's own widget sends — the\n * session, the message, the files — and the runner turns n8n's answer into the events\n * `AparteClient`'s adapter draws, the same ones a loop run in the page emits.\n */\nimport { AparteError, AparteErrorCode, uuid } from '@aparte/core';\nimport type { AparteChatMessage, AparteStreamRunner, AparteStreamRunOptions } from '@aparte/core';\nimport { readN8nReply } from './wire.js';\nimport { httpError, unreachable } from './http.js';\n\nexport interface N8nRunnerOptions {\n /**\n * The workflow's chat URL: the Chat Trigger's production URL, the one that ends in\n * `/chat` — or a Webhook node's URL, for a workflow that answers through it.\n */\n webhookUrl: string;\n /**\n * The n8n session a turn runs in. n8n keeps the agent's memory per session and the\n * page sends only the new message, so this is what ties a conversation to its memory.\n * Give the id your app keeps for the conversation — the one you loaded its history\n * with ({@link loadN8nHistory}), or a conversation manager's `activeId`. A function\n * is read every turn.\n * @default a new session for each new conversation: a turn with no reply before it\n * starts one, and the turns after it continue it\n */\n sessionId?: string | ((opts: AparteStreamRunOptions) => string);\n /** Sent with every message as `metadata`, which the workflow reads from the trigger's output. */\n metadata?: Record<string, unknown> | ((opts: AparteStreamRunOptions) => Record<string, unknown>);\n /** Sent with every request; a function is called per request (a fresh token each time). */\n headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);\n /** @default the global `fetch` */\n fetch?: typeof fetch;\n /** For a workflow on another origin that authenticates by cookie. */\n credentials?: RequestCredentials;\n /**\n * The body field that carries the session, for a workflow that reads another one.\n * @default 'sessionId', the Chat Trigger's\n */\n chatSessionKey?: string;\n /**\n * The body field that carries the message, for a workflow that reads another one.\n * @default 'chatInput', the Chat Trigger's\n */\n chatInputKey?: string;\n /**\n * Told what the runner could not read in n8n's answer — a frame type it does not\n * know, a stream cut mid-frame. For the developer, not the person chatting.\n * @default `console.warn`\n */\n onWarning?: (message: string) => void;\n}\n\n/** The workflow streamed nothing: the widget's own diagnosis, said to the developer. */\nconst EMPTY_STREAM = 'The n8n workflow streamed nothing. When its Chat Trigger streams, the node that answers '\n + 'has to stream too: turn on streaming in the AI Agent\\'s options, or set the trigger\\'s Response Mode to '\n + '\"When Last Node Finishes\".';\n\nconst EMPTY_WHOLE = 'The n8n workflow finished without an answer: its last node returned no text. End the '\n + 'workflow on the node that answers — an AI Agent, or a node that returns an `output` field.';\n\nconst PROXY_TIMEOUT = 'A proxy timed out waiting for n8n\\'s answer. The workflow may still be running — '\n + 'its execution list shows the result.';\n\nconst DEFERRED = 'This n8n workflow answers through response nodes, over n8n\\'s chat WebSocket, which this runner '\n + 'does not read. Set the Chat Trigger\\'s Response Mode to \"Streaming\" or \"When Last Node Finishes\".';\n\nconst unreadable = (fields: string[]) => `The n8n workflow answered with the fields ${fields.map((f) => `\"${f}\"`).join(', ')} `\n + 'but no \"output\", \"text\" or \"message\" field, which is where a chat reads the answer. End the workflow on an AI Agent, '\n + 'or add a Set node that puts the reply in \"output\".';\n\n/** The message the turn answers: the last one the person wrote. */\nfunction lastUserIndex(messages: AparteChatMessage[]): number {\n for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === 'user') return i;\n return -1;\n}\n\n/** The text of a message, its parts kept apart: a file inlined after the words starts on a line of its own. */\nfunction textOf(content: AparteChatMessage['content'] | undefined): string {\n if (content === undefined) return '';\n if (typeof content === 'string') return content;\n return content.flatMap((part) => (part.type === 'text' && part.text ? [part.text] : [])).join('\\n\\n');\n}\n\nconst EXTENSIONS: Record<string, string> = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg' };\n\n/**\n * An image part — a data URL — as the file the Chat Trigger receives. A `Blob` and a name\n * rather than a `File`, which Node only has from 20 and this package runs from 18.\n */\nfunction imageFile(image: string, mimeType: string | undefined, index: number): { blob: Blob; name: string } | undefined {\n const match = /^data:([^;,]+)?((?:;[^;,]*)*?);base64,(.*)$/s.exec(image);\n if (!match) return undefined;\n const type = mimeType ?? match[1] ?? 'application/octet-stream';\n const binary = atob(match[3] ?? '');\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return { blob: new Blob([bytes], { type }), name: `image-${index + 1}.${EXTENSIONS[type] ?? 'bin'}` };\n}\n\n/**\n * What the Chat Trigger receives as `files`: the files the person picked, whatever their\n * type, when the client hands them over (`files` on a send) — else the images inlined in\n * the message, for a caller that only has those.\n */\nfunction filesOf(opts: AparteStreamRunOptions, message: AparteChatMessage | undefined): { blob: Blob; name: string }[] {\n if (opts.files && opts.files.length > 0) return opts.files.map((file) => ({ blob: file, name: file.name }));\n return (Array.isArray(message?.content) ? message.content : [])\n .flatMap((part) => (part.type === 'image' ? [part] : []))\n .map((part, i) => imageFile(part.image, part.mimeType, i))\n .filter((file) => file !== undefined);\n}\n\n/**\n * Put aparté on an n8n chat workflow: pass the result as `AparteClient`'s `streamRunner`.\n * n8n runs the agent, its tools and its memory; the page sends the person's message and\n * draws the answer, streamed when the Chat Trigger streams.\n */\nexport function createN8nRunner(options: N8nRunnerOptions): AparteStreamRunner {\n const warn = options.onWarning ?? ((message: string) => console.warn(`[aparte] n8n: ${message}`));\n const sessionKey = options.chatSessionKey ?? 'sessionId';\n const inputKey = options.chatInputKey ?? 'chatInput';\n let ownSession = `aparte-${uuid()}`;\n\n return async (opts) => {\n const emit = opts.emitter;\n const doFetch = options.fetch ?? fetch;\n try {\n const messages = (opts.baseRequest as { messages: AparteChatMessage[] }).messages;\n const last = lastUserIndex(messages);\n const message = last >= 0 ? messages[last] : undefined;\n // No reply before the message: a new conversation, or one that was cleared. n8n\n // must not answer it from the memory of the one before.\n if (options.sessionId === undefined && !messages.slice(0, Math.max(last, 0)).some((m) => m.role === 'assistant')) {\n ownSession = `aparte-${uuid()}`;\n }\n const headers = typeof options.headers === 'function' ? await options.headers() : options.headers ?? {};\n const sessionId = (typeof options.sessionId === 'function' ? options.sessionId(opts) : options.sessionId) ?? ownSession;\n const metadata = typeof options.metadata === 'function' ? options.metadata(opts) : options.metadata;\n const files = filesOf(opts, message);\n\n // As n8n's widget sends it: JSON, or a form when the message carries files.\n const fields: Record<string, string | Record<string, unknown>> = {\n action: 'sendMessage',\n [sessionKey]: sessionId,\n [inputKey]: textOf(message?.content),\n ...(metadata ? { metadata } : {}),\n };\n let body: string | FormData;\n // The page's headers, without a Content-Type in any case: a form's boundary is\n // the browser's to write, and JSON gets exactly one, ours.\n const requestHeaders: Record<string, string> = { Accept: 'text/plain' };\n for (const [name, value] of Object.entries(headers)) if (name.toLowerCase() !== 'content-type') requestHeaders[name] = value;\n if (files.length > 0) {\n const form = new FormData();\n for (const [key, value] of Object.entries(fields)) form.append(key, typeof value === 'string' ? value : JSON.stringify(value));\n for (const file of files) form.append('files', file.blob, file.name);\n body = form;\n } else {\n body = JSON.stringify(fields);\n requestHeaders['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(options.webhookUrl, {\n method: 'POST',\n headers: requestHeaders,\n body,\n signal: opts.signal,\n ...(options.credentials ? { credentials: options.credentials } : {}),\n }).catch((error: unknown) => { throw unreachable(error, options.webhookUrl); });\n if (!response.ok) throw await httpError(response, 'answer');\n\n emit({ type: 'run-start' });\n emit({ type: 'turn-start' });\n\n // n8n streams once per node that answers. One is drawn at a time, in the order\n // they began: another node's text waits for the drawn one to end, and starts a\n // block of its own below it.\n let drawn = false;\n let whole = false;\n let textOpen = false;\n let active: string | undefined;\n const waiting = new Map<string, { text: string[]; ended: boolean }>();\n const unknown = new Set<string>();\n const flush = () => {\n if (textOpen) emit({ type: 'text-flush' });\n textOpen = false;\n };\n const draw = (text: string) => {\n if (!text) return;\n drawn = true;\n textOpen = true;\n emit({ type: 'text-delta', delta: text });\n };\n const open = (stream: string) => {\n active = stream;\n flush();\n };\n const drain = () => {\n active = undefined;\n for (const [stream, queued] of waiting) {\n waiting.delete(stream);\n open(stream);\n for (const text of queued.text) draw(text);\n if (!queued.ended) return;\n active = undefined;\n }\n };\n const waitFor = (stream: string) => {\n let queued = waiting.get(stream);\n if (!queued) waiting.set(stream, queued = { text: [], ended: false });\n return queued;\n };\n\n if (!response.body) throw new AparteError(EMPTY_STREAM, AparteErrorCode.PROVIDER_MISCONFIGURED);\n for await (const part of readN8nReply(response.body)) {\n switch (part.kind) {\n case 'begin':\n if (active === undefined) open(part.stream);\n else if (part.stream !== active) waitFor(part.stream);\n break;\n case 'item':\n if (part.stream === undefined || part.stream === active) draw(part.text);\n else if (active === undefined) { open(part.stream); draw(part.text); }\n else waitFor(part.stream).text.push(part.text);\n break;\n case 'end':\n if (part.stream === active) drain();\n else waitFor(part.stream).ended = true;\n break;\n case 'error':\n flush();\n throw new AparteError(part.message || 'The n8n workflow failed.', AparteErrorCode.PROVIDER_ERROR);\n case 'reply':\n whole = true;\n draw(part.text);\n break;\n case 'unreadable':\n throw new AparteError(unreadable(part.fields), AparteErrorCode.PROVIDER_MISCONFIGURED);\n case 'deferred':\n throw new AparteError(DEFERRED, AparteErrorCode.PROVIDER_MISCONFIGURED);\n case 'html':\n flush();\n throw new AparteError(PROXY_TIMEOUT, AparteErrorCode.NET_TIMEOUT);\n case 'cut':\n warn('n8n\\'s stream stopped in the middle of a frame; the text before it is kept.');\n break;\n case 'unknown':\n if (!unknown.has(part.type)) warn(`n8n sent a \"${part.type}\" frame, which this runner does not read; it was skipped.`);\n unknown.add(part.type);\n break;\n }\n }\n // A stream that was cut before its `end`: what it sent is still the answer — all\n // of it, the streams still waiting included, ended or not.\n drain();\n for (const [stream, queued] of waiting) {\n waiting.delete(stream);\n open(stream);\n for (const text of queued.text) draw(text);\n }\n if (!drawn) throw new AparteError(whole ? EMPTY_WHOLE : EMPTY_STREAM, AparteErrorCode.PROVIDER_MISCONFIGURED);\n flush();\n emit({ type: 'run-done' });\n return undefined;\n } catch (error) {\n // Stopped by the person: not a failure, nothing to report.\n if (opts.signal.aborted) return undefined;\n throw error;\n }\n };\n}\n","/**\n * loadN8nHistory — a session's earlier messages, read back from the workflow's memory.\n *\n * n8n keeps the conversation, the page keeps nothing: after a reload, the Chat Trigger\n * answers `loadPreviousSession` with the session's messages as its memory node stored\n * them (LangChain's serialised messages). This turns them into messages a chat shows.\n */\nimport { AparteError, AparteErrorCode } from '@aparte/core';\nimport type { AparteMessage } from '@aparte/core';\nimport { httpError, unreachable } from './http.js';\n\nexport interface N8nHistoryOptions {\n /** The Chat Trigger's production URL, the one the runner sends to. */\n webhookUrl: string;\n /** The session to read: the one the runner sends the conversation's turns in. */\n sessionId: string;\n /** Sent as `metadata`, as the widget sends it with this request too. */\n metadata?: Record<string, unknown>;\n /** Sent with the request; a function is called for it. */\n headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);\n /** @default the global `fetch` */\n fetch?: typeof fetch;\n /** For a workflow on another origin that authenticates by cookie. */\n credentials?: RequestCredentials;\n /** @default 'sessionId', the Chat Trigger's */\n chatSessionKey?: string;\n /** Cancels the request. */\n signal?: AbortSignal;\n}\n\ninterface StoredMessage {\n id?: unknown;\n type?: unknown;\n kwargs?: { content?: unknown };\n}\n\n/** LangChain's message class, last segment of its `id` path (`['langchain_core','messages','AIMessage']`). */\nfunction classOf(message: StoredMessage): string {\n const path = Array.isArray(message.id) ? message.id : [];\n const last = path[path.length - 1];\n return typeof last === 'string' ? last : typeof message.type === 'string' ? message.type : '';\n}\n\n/** Text content, or the text parts of a content list; a turn that only called a tool has none. */\nfunction textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => (typeof part === 'string' ? part : typeof part === 'object' && part !== null && (part as { type?: unknown }).type === 'text' ? String((part as { text?: unknown }).text ?? '') : ''))\n .filter(Boolean)\n .join('\\n\\n');\n}\n\n/**\n * The messages a chat shows, from what the memory stored: what the person wrote and what\n * the agent answered. A tool's call and its result are left out, as the live stream\n * leaves them out — where n8n's widget drew them as bubbles of raw JSON after a reload.\n */\nexport function n8nHistoryToMessages(stored: unknown[], { now = Date.now() } = {}): AparteMessage[] {\n const messages: AparteMessage[] = [];\n stored.forEach((entry, index) => {\n if (typeof entry !== 'object' || entry === null) return;\n const message = entry as StoredMessage;\n const kind = classOf(message);\n const role = /^Human/.test(kind) || kind === 'human' ? 'user' : /^AI/.test(kind) || kind === 'ai' ? 'assistant' : undefined;\n if (!role) return;\n const content = textOf(message.kwargs?.content);\n if (!content.trim()) return;\n // Ordered, in the past: the memory keeps no time, and a chat sorts nothing by it.\n messages.push({ id: `n8n-${index}`, role, content, timestamp: now - (stored.length - index), status: 'completed' });\n });\n return messages;\n}\n\n/**\n * Read a session's earlier messages from the workflow's memory — for a page that keeps\n * the session id across reloads and shows the conversation again. Needs the Chat\n * Trigger's *Load Previous Session* set to *From Memory*; when it is off, n8n answers\n * with no messages.\n */\nexport async function loadN8nHistory(options: N8nHistoryOptions): Promise<AparteMessage[]> {\n const doFetch = options.fetch ?? fetch;\n const headers = typeof options.headers === 'function' ? await options.headers() : options.headers ?? {};\n const sent: Record<string, string> = { 'Content-Type': 'application/json' };\n for (const [name, value] of Object.entries(headers)) if (name.toLowerCase() !== 'content-type') sent[name] = value;\n const response = await doFetch(options.webhookUrl, {\n method: 'POST',\n headers: sent,\n body: JSON.stringify({\n action: 'loadPreviousSession',\n [options.chatSessionKey ?? 'sessionId']: options.sessionId,\n ...(options.metadata ? { metadata: options.metadata } : {}),\n }),\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.credentials ? { credentials: options.credentials } : {}),\n }).catch((error: unknown) => { throw unreachable(error, options.webhookUrl); });\n if (!response.ok) throw await httpError(response, 'history');\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n throw new AparteError('n8n answered the history request with something that is not JSON. Check that the URL is the Chat Trigger\\'s production URL.', AparteErrorCode.PROVIDER_MISCONFIGURED);\n }\n const data = typeof body === 'object' && body !== null ? (body as { data?: unknown }).data : undefined;\n return n8nHistoryToMessages(Array.isArray(data) ? data : []);\n}\n"],"names":["message","textOf"],"mappings":"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"}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AparteStreamRunner, AparteStreamRunOptions } from '@aparte/core';
|
|
2
|
+
export interface N8nRunnerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The workflow's chat URL: the Chat Trigger's production URL, the one that ends in
|
|
5
|
+
* `/chat` — or a Webhook node's URL, for a workflow that answers through it.
|
|
6
|
+
*/
|
|
7
|
+
webhookUrl: string;
|
|
8
|
+
/**
|
|
9
|
+
* The n8n session a turn runs in. n8n keeps the agent's memory per session and the
|
|
10
|
+
* page sends only the new message, so this is what ties a conversation to its memory.
|
|
11
|
+
* Give the id your app keeps for the conversation — the one you loaded its history
|
|
12
|
+
* with ({@link loadN8nHistory}), or a conversation manager's `activeId`. A function
|
|
13
|
+
* is read every turn.
|
|
14
|
+
* @default a new session for each new conversation: a turn with no reply before it
|
|
15
|
+
* starts one, and the turns after it continue it
|
|
16
|
+
*/
|
|
17
|
+
sessionId?: string | ((opts: AparteStreamRunOptions) => string);
|
|
18
|
+
/** Sent with every message as `metadata`, which the workflow reads from the trigger's output. */
|
|
19
|
+
metadata?: Record<string, unknown> | ((opts: AparteStreamRunOptions) => Record<string, unknown>);
|
|
20
|
+
/** Sent with every request; a function is called per request (a fresh token each time). */
|
|
21
|
+
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
22
|
+
/** @default the global `fetch` */
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
/** For a workflow on another origin that authenticates by cookie. */
|
|
25
|
+
credentials?: RequestCredentials;
|
|
26
|
+
/**
|
|
27
|
+
* The body field that carries the session, for a workflow that reads another one.
|
|
28
|
+
* @default 'sessionId', the Chat Trigger's
|
|
29
|
+
*/
|
|
30
|
+
chatSessionKey?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The body field that carries the message, for a workflow that reads another one.
|
|
33
|
+
* @default 'chatInput', the Chat Trigger's
|
|
34
|
+
*/
|
|
35
|
+
chatInputKey?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Told what the runner could not read in n8n's answer — a frame type it does not
|
|
38
|
+
* know, a stream cut mid-frame. For the developer, not the person chatting.
|
|
39
|
+
* @default `console.warn`
|
|
40
|
+
*/
|
|
41
|
+
onWarning?: (message: string) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Put aparté on an n8n chat workflow: pass the result as `AparteClient`'s `streamRunner`.
|
|
45
|
+
* n8n runs the agent, its tools and its memory; the page sends the person's message and
|
|
46
|
+
* draws the answer, streamed when the Chat Trigger streams.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createN8nRunner(options: N8nRunnerOptions): AparteStreamRunner;
|
|
49
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAqB,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAIlG,MAAM,WAAW,gBAAgB;IAC7B;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,sBAAsB,KAAK,MAAM,CAAC,CAAC;IAChE,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,sBAAsB,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjG,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACpG,kCAAkC;IAClC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AA8DD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,kBAAkB,CAyJ7E"}
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body of n8n's answer to a chat message, read as it arrives.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes come back, both labelled `application/json` by n8n:
|
|
5
|
+
* - **streamed** — a Chat Trigger (or Webhook node) whose Response Mode is *Streaming*
|
|
6
|
+
* sends newline-delimited JSON frames, `begin` / `item` / `end` per node that streams,
|
|
7
|
+
* `error` when the workflow fails, and `keepalive` every 30 seconds while nothing else
|
|
8
|
+
* comes — so before the first `begin` when a slow node runs first;
|
|
9
|
+
* - **whole** — any other Response Mode sends one value once the workflow ends: the last
|
|
10
|
+
* node's item (`{ output }` for an AI Agent), or what a *Respond to Webhook* node sent.
|
|
11
|
+
*
|
|
12
|
+
* Which one it is shows on the first line: a frame has a known `type`. Read off
|
|
13
|
+
* `@n8n/chat` 1.43.3, the widget n8n embeds, and recorded against n8n 2.35.7 and 2.44.6.
|
|
14
|
+
*/
|
|
15
|
+
/** One thing an answer carries, in order. */
|
|
16
|
+
export type N8nReplyPart =
|
|
17
|
+
/** A node starts streaming. `stream` names it (node, run, item). */
|
|
18
|
+
{
|
|
19
|
+
kind: 'begin';
|
|
20
|
+
stream: string;
|
|
21
|
+
}
|
|
22
|
+
/** A piece of text. `stream` is absent for a line that was not a frame. */
|
|
23
|
+
| {
|
|
24
|
+
kind: 'item';
|
|
25
|
+
stream?: string;
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'end';
|
|
29
|
+
stream: string;
|
|
30
|
+
}
|
|
31
|
+
/** The workflow failed; its message. */
|
|
32
|
+
| {
|
|
33
|
+
kind: 'error';
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
/** A frame type this reader does not know (a newer n8n). */
|
|
37
|
+
| {
|
|
38
|
+
kind: 'unknown';
|
|
39
|
+
type: string;
|
|
40
|
+
}
|
|
41
|
+
/** The whole answer of a workflow that does not stream. */
|
|
42
|
+
| {
|
|
43
|
+
kind: 'reply';
|
|
44
|
+
text: string;
|
|
45
|
+
}
|
|
46
|
+
/** A whole answer that is JSON with none of the fields a chat reads; its field names. */
|
|
47
|
+
| {
|
|
48
|
+
kind: 'unreadable';
|
|
49
|
+
fields: string[];
|
|
50
|
+
}
|
|
51
|
+
/** The workflow answers later, through response nodes over n8n's chat WebSocket. */
|
|
52
|
+
| {
|
|
53
|
+
kind: 'deferred';
|
|
54
|
+
}
|
|
55
|
+
/** An HTML page instead of the answer: a proxy gave up waiting. */
|
|
56
|
+
| {
|
|
57
|
+
kind: 'html';
|
|
58
|
+
}
|
|
59
|
+
/** The stream stopped in the middle of a frame. */
|
|
60
|
+
| {
|
|
61
|
+
kind: 'cut';
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The text of a whole answer, the way n8n's widget reads it: `output`, else `text`, else
|
|
65
|
+
* `message`; a `{ type: 'text', text }` part as its text. `undefined` when the value has
|
|
66
|
+
* none of them — where the widget printed the JSON into the bubble, the most asked-about
|
|
67
|
+
* failure on n8n's forum, this lets the runner say which field is missing instead.
|
|
68
|
+
*/
|
|
69
|
+
export declare function replyText(value: unknown): string | undefined;
|
|
70
|
+
/** Yields what n8n's answer carries, frame by frame as it streams, or once for a whole answer. */
|
|
71
|
+
export declare function readN8nReply(body: ReadableStream<Uint8Array>): AsyncGenerator<N8nReplyPart>;
|
|
72
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../src/wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,6CAA6C;AAC7C,MAAM,MAAM,YAAY;AACpB,oEAAoE;AAClE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACnC,2EAA2E;GACzE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACjC,wCAAwC;GACtC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACpC,4DAA4D;GAC1D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACnC,2DAA2D;GACzD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACjC,yFAAyF;GACvF;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE;AAC1C,oFAAoF;GAClF;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE;AACtB,mEAAmE;GACjE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,mDAAmD;GACjD;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAmDtB;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAkB5D;AAuBD,kGAAkG;AAClG,wBAAuB,YAAY,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,YAAY,CAAC,CAqDlG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/runner-n8n",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.27.1",
|
|
4
|
+
"homepage": "https://apartejs.dev/providers/agent/n8n/",
|
|
5
|
+
"description": "Put aparté on an n8n chat workflow — the Chat Trigger's agent runs in n8n, with its memory and its tools; aparté draws the chat, streamed or not.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"@aparte-workspace/source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@aparte/core": ">=0.27.1 <1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"typescript": "~5.7.0",
|
|
33
|
+
"vite": "^6.0.0",
|
|
34
|
+
"@aparte/core": "0.27.1"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"ai",
|
|
38
|
+
"agent",
|
|
39
|
+
"n8n",
|
|
40
|
+
"chat-trigger",
|
|
41
|
+
"chatbot",
|
|
42
|
+
"workflow",
|
|
43
|
+
"chat",
|
|
44
|
+
"runner"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/apartejs/aparte.git",
|
|
50
|
+
"directory": "packages/providers/agent/n8n"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/apartejs/aparte/issues"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"dev": "vite",
|
|
57
|
+
"build": "vite build && tsc -b --emitDeclarationOnly --force && node ../../../../scripts/stamp-dist.mjs",
|
|
58
|
+
"preview": "vite preview",
|
|
59
|
+
"test": "vitest",
|
|
60
|
+
"test:run": "vitest run",
|
|
61
|
+
"test:coverage": "vitest run --coverage"
|
|
62
|
+
}
|
|
6
63
|
}
|