@skein-js/server-kit 0.21.0 → 0.23.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/README.md +6 -1
- package/dist/channels-runtime-5TESQBKO.js +213 -0
- package/dist/{chunk-UJZBVPYI.js → chunk-3IKRBGJU.js} +315 -5
- package/dist/{in-memory-runtime-IOQZUNLV.js → in-memory-runtime-D4PEZVMU.js} +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.js +20 -15
- package/package.json +12 -8
- package/dist/channels-runtime-JHTMXARF.js +0 -156
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Part of **[skein-js](https://github.com/skein-js/skein-js)** — the open-source
|
|
|
10
10
|
|
|
11
11
|
This package is the common ground the framework adapters ([`@skein-js/express`](../server-express),
|
|
12
12
|
[`@skein-js/fastify`](../server-fastify), [`@skein-js/nestjs`](../server-nestjs),
|
|
13
|
-
[`@skein-js/nextjs`](../server-nextjs)) stand on — so no adapter has to depend on another (or on
|
|
13
|
+
[`@skein-js/nextjs`](../server-nextjs), and [`@skein-js/fetch`](../server-fetch)) stand on — so no adapter has to depend on another (or on
|
|
14
14
|
Express) just to reuse it. It holds the framework-agnostic pieces (runtime assembly, dev-state import,
|
|
15
15
|
CORS, and the Node-`http` transport the Node-based adapters share); each adapter still writes only the
|
|
16
16
|
thin request/response shim its framework needs over the [`@skein-js/agent-protocol`](../agent-protocol)
|
|
@@ -31,6 +31,11 @@ handler table.
|
|
|
31
31
|
`describeSnapshot`, from the **`@skein-js/server-kit/dev` subpath**: read an existing
|
|
32
32
|
`.langgraph_api/` directory and reconstruct skein's own `DevStateSnapshot`, so adopting skein carries
|
|
33
33
|
all local state over losslessly.
|
|
34
|
+
- **Native HTTP and graph UI** — `loadConfigHttpHandler` loads exported Fetch/Hono applications
|
|
35
|
+
from `http.app` and graph UI from `ui`/`ui_config`. A 404 falls through with the original request
|
|
36
|
+
body preserved. `withNativePath`, `readNativeNodeRequest`, and `sendNativeResponse` share mounting
|
|
37
|
+
and streaming behavior across adapters; production assets are compiled by `skein build`. See
|
|
38
|
+
[CLI compatibility](https://skein-js.github.io/skein-js/langgraph-cli-compat#graph-ui-and-custom-http-applications).
|
|
34
39
|
- **CORS** — `corsFromHttpConfig` / `toCorsOptions` map a `langgraph.json` `http.cors` block to
|
|
35
40
|
`cors`-style `CorsOptions`; `allowedOrigin` / `corsResponseHeaders` / `applyNodeCors` /
|
|
36
41
|
`sendNodePreflight` derive CORS headers for the adapters without a CORS middleware of their own
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// src/channels-runtime.ts
|
|
2
|
+
import { createAuthScopedStore, createProtocolServiceFromContext } from "@skein-js/agent-protocol";
|
|
3
|
+
import { SkeinConfigError } from "@skein-js/config";
|
|
4
|
+
import {
|
|
5
|
+
SkeinHttpError
|
|
6
|
+
} from "@skein-js/core";
|
|
7
|
+
async function resolveChannels(input) {
|
|
8
|
+
const names = Object.keys(input.channels);
|
|
9
|
+
if (names.length === 0) return void 0;
|
|
10
|
+
const channels = await importChannelsPackage(names);
|
|
11
|
+
const registry = channels.buildChannelRegistry({
|
|
12
|
+
channels: Object.fromEntries(
|
|
13
|
+
Object.entries(input.channels).map(([name, loaded]) => [
|
|
14
|
+
name,
|
|
15
|
+
{
|
|
16
|
+
module: loaded.module,
|
|
17
|
+
config: {
|
|
18
|
+
assistant: loaded.assistant,
|
|
19
|
+
...loaded.allowedAssistants ? { allowedAssistants: loaded.allowedAssistants } : {},
|
|
20
|
+
...loaded.publicUrl ? { publicUrl: loaded.publicUrl } : {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
])
|
|
24
|
+
),
|
|
25
|
+
graphIds: input.graphIds
|
|
26
|
+
});
|
|
27
|
+
const channelInventory = registry.names.map((routeName) => {
|
|
28
|
+
const registered = registry.get(routeName);
|
|
29
|
+
if (!registered) throw new Error(`Channel registry lost configured route "${routeName}".`);
|
|
30
|
+
return {
|
|
31
|
+
route_name: routeName,
|
|
32
|
+
assistant: registered.config.assistant,
|
|
33
|
+
allowed_assistants: [...registered.config.allowedAssistants ?? []],
|
|
34
|
+
channel_name: registered.channel.name,
|
|
35
|
+
delivery_supported: typeof registered.channel.deliver === "function"
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
routes: [
|
|
40
|
+
{ method: "get", path: "/channels", handler: "listChannels" },
|
|
41
|
+
...channels.channelRouteBindings(registry.names)
|
|
42
|
+
],
|
|
43
|
+
wrapDispatcher: (inner) => channels.wrapChannelDispatcher({
|
|
44
|
+
registry,
|
|
45
|
+
inner,
|
|
46
|
+
logger: input.deps.logger ?? console
|
|
47
|
+
}),
|
|
48
|
+
handlers: (context) => ({
|
|
49
|
+
listChannels: async () => ({
|
|
50
|
+
kind: "json",
|
|
51
|
+
status: 200,
|
|
52
|
+
body: { channels: channelInventory }
|
|
53
|
+
}),
|
|
54
|
+
handleInboundEvent: async (req) => {
|
|
55
|
+
const name = req.url.split("?")[0].split("/channels/")[1] ?? "";
|
|
56
|
+
const registered = registry.get(name);
|
|
57
|
+
if (!registered) throw SkeinHttpError.notFound(`No channel named "${name}".`);
|
|
58
|
+
return channels.handleInboundEvent(
|
|
59
|
+
registered,
|
|
60
|
+
{
|
|
61
|
+
method: req.method,
|
|
62
|
+
url: req.url,
|
|
63
|
+
headers: req.headers,
|
|
64
|
+
...typeof req.body === "string" ? { text: req.body } : {}
|
|
65
|
+
},
|
|
66
|
+
buildPipelineDeps(context, [
|
|
67
|
+
registered.config.assistant,
|
|
68
|
+
...registered.config.allowedAssistants ?? []
|
|
69
|
+
])
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildPipelineDeps(context, allowedAssistants) {
|
|
76
|
+
const deps = context.deps;
|
|
77
|
+
const service = () => createProtocolServiceFromContext(context);
|
|
78
|
+
let requestFilters;
|
|
79
|
+
let verifiedContext;
|
|
80
|
+
const scopedService = (authContext, admission) => {
|
|
81
|
+
if (!authContext && !requestFilters && !admission) return service();
|
|
82
|
+
const base = context;
|
|
83
|
+
return createProtocolServiceFromContext({
|
|
84
|
+
...base,
|
|
85
|
+
...admission && deps.store.admission ? { admission } : {},
|
|
86
|
+
...authContext?.user ? { authUser: authContext.user } : {},
|
|
87
|
+
...authContext?.scopes ? { authScopes: authContext.scopes } : {},
|
|
88
|
+
deps: requestFilters && deps.auth ? {
|
|
89
|
+
...base.deps,
|
|
90
|
+
store: createAuthScopedStore(base.deps.store, deps.auth, requestFilters, "threads")
|
|
91
|
+
} : base.deps
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
...deps.store.idempotency ? {
|
|
96
|
+
idempotency: {
|
|
97
|
+
...deps.store.idempotency,
|
|
98
|
+
claim: async (input) => {
|
|
99
|
+
const authorizeRecorded = async (record) => {
|
|
100
|
+
const run = record.run_id ? await deps.store.runs.get(record.run_id) : null;
|
|
101
|
+
if (run) {
|
|
102
|
+
const assistant = await deps.store.assistants.get(run.assistant_id);
|
|
103
|
+
if (!allowedAssistants.includes(run.assistant_id) && !(assistant && allowedAssistants.includes(assistant.graph_id)))
|
|
104
|
+
throw SkeinHttpError.forbidden(
|
|
105
|
+
"Recorded assistant is outside the channel allowlist.",
|
|
106
|
+
{ code: "assistant_not_allowed" }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!deps.auth) return;
|
|
110
|
+
const threadId = run?.thread_id ?? record.thread_id;
|
|
111
|
+
const decision = await deps.auth.authorize({
|
|
112
|
+
resource: "threads",
|
|
113
|
+
action: "create_run",
|
|
114
|
+
context: verifiedContext,
|
|
115
|
+
value: {
|
|
116
|
+
...threadId ? { thread_id: threadId } : {},
|
|
117
|
+
...run ? { run_id: run.run_id, assistant_id: run.assistant_id } : {}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const store = decision.filters ? createAuthScopedStore(deps.store, deps.auth, decision.filters, "threads") : deps.store;
|
|
121
|
+
if (threadId && !await store.threads.get(threadId))
|
|
122
|
+
throw SkeinHttpError.notFound("Thread not found.");
|
|
123
|
+
if (record.run_id && !await store.runs.get(record.run_id))
|
|
124
|
+
throw SkeinHttpError.notFound("Run not found.");
|
|
125
|
+
};
|
|
126
|
+
const accepted = deps.store.admission ? await deps.store.idempotency.get(`admission ${input.scope}`, input.key) : null;
|
|
127
|
+
if (accepted && accepted.expires_at > input.now && accepted.fingerprint === input.fingerprint && accepted.run_id) {
|
|
128
|
+
await authorizeRecorded(accepted);
|
|
129
|
+
return {
|
|
130
|
+
claimed: false,
|
|
131
|
+
record: {
|
|
132
|
+
...accepted,
|
|
133
|
+
scope: input.scope,
|
|
134
|
+
response: { status: 202, body: { run_id: accepted.run_id } }
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const result = await deps.store.idempotency.claim(input);
|
|
139
|
+
if (!result.claimed && result.record.status === "done")
|
|
140
|
+
await authorizeRecorded(result.record);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} : {},
|
|
145
|
+
clock: () => deps.clock ? deps.clock() : /* @__PURE__ */ new Date(),
|
|
146
|
+
logger: deps.logger ?? console,
|
|
147
|
+
// Straight off the run event bus — the same stream `runs.joinStream` serves, without the HTTP
|
|
148
|
+
// connection per conversation that reaching it from outside would cost.
|
|
149
|
+
runFrames: { subscribe: (runId) => deps.bus.subscribe(runId) },
|
|
150
|
+
authorize: async (principal, authz, value) => {
|
|
151
|
+
if (!deps.auth) return void 0;
|
|
152
|
+
const context2 = {
|
|
153
|
+
user: {
|
|
154
|
+
...principal.metadata ?? {},
|
|
155
|
+
identity: principal.identity,
|
|
156
|
+
display_name: principal.identity,
|
|
157
|
+
is_authenticated: true,
|
|
158
|
+
permissions: principal.permissions ?? []
|
|
159
|
+
},
|
|
160
|
+
scopes: principal.permissions ?? []
|
|
161
|
+
};
|
|
162
|
+
verifiedContext = context2;
|
|
163
|
+
const decision = await deps.auth.authorize({
|
|
164
|
+
resource: authz.resource,
|
|
165
|
+
action: authz.action,
|
|
166
|
+
value: value ?? {},
|
|
167
|
+
context: context2
|
|
168
|
+
});
|
|
169
|
+
requestFilters = decision.filters;
|
|
170
|
+
return context2;
|
|
171
|
+
},
|
|
172
|
+
ensureThread: async (threadId, metadata, authContext) => {
|
|
173
|
+
await scopedService(authContext).threads.create({
|
|
174
|
+
thread_id: threadId,
|
|
175
|
+
metadata,
|
|
176
|
+
ifExists: "do_nothing"
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
threadStatus: async (threadId) => {
|
|
180
|
+
const thread = await scopedService(void 0).threads.get(threadId).catch(() => null);
|
|
181
|
+
return thread?.status ?? null;
|
|
182
|
+
},
|
|
183
|
+
createRun: async (input) => {
|
|
184
|
+
const run = await scopedService(input.authContext, input.admission).runs.createBackground(
|
|
185
|
+
input.threadId,
|
|
186
|
+
{
|
|
187
|
+
assistant_id: input.assistantId,
|
|
188
|
+
...input.input !== void 0 ? { input: input.input } : {},
|
|
189
|
+
...input.command ? { command: input.command } : {},
|
|
190
|
+
...input.ifThreadStatus ? { if_thread_status: [...input.ifThreadStatus] } : {},
|
|
191
|
+
...input.multitaskStrategy ? { multitask_strategy: input.multitaskStrategy } : {},
|
|
192
|
+
...input.metadata ? { metadata: input.metadata } : {},
|
|
193
|
+
...input.webhook ? { webhook: input.webhook } : {},
|
|
194
|
+
stream_mode: [...input.streamMode]
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
return { runId: run.run_id };
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function importChannelsPackage(names) {
|
|
202
|
+
try {
|
|
203
|
+
return await import("@skein-js/channels");
|
|
204
|
+
} catch (cause) {
|
|
205
|
+
throw new SkeinConfigError(
|
|
206
|
+
`skein.channels configures ${names.join(", ")}, but @skein-js/channels is not installed. Add it as a dependency.`,
|
|
207
|
+
{ cause }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
resolveChannels
|
|
213
|
+
};
|
|
@@ -12,6 +12,286 @@ import {
|
|
|
12
12
|
} from "@skein-js/config";
|
|
13
13
|
import { MemorySkeinStore as MemorySkeinStore2 } from "@skein-js/storage-memory";
|
|
14
14
|
|
|
15
|
+
// src/config-http-handler.ts
|
|
16
|
+
import { pathToFileURL } from "url";
|
|
17
|
+
import {
|
|
18
|
+
parseGraphSpec,
|
|
19
|
+
SkeinConfigError
|
|
20
|
+
} from "@skein-js/config";
|
|
21
|
+
|
|
22
|
+
// src/graph-ui.ts
|
|
23
|
+
import { readFile } from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { SkeinHttpError as SkeinHttpError2 } from "@skein-js/core";
|
|
26
|
+
|
|
27
|
+
// src/native-http.ts
|
|
28
|
+
import { serializeWireJson, SkeinHttpError } from "@skein-js/core";
|
|
29
|
+
var sourceUrls = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
function withNativePath(request, pathname) {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
url.pathname = pathname;
|
|
33
|
+
const native = new Request(url, request.clone());
|
|
34
|
+
sourceUrls.set(native, sourceUrls.get(request) ?? request.url);
|
|
35
|
+
return native;
|
|
36
|
+
}
|
|
37
|
+
function originalNativeUrl(request) {
|
|
38
|
+
return sourceUrls.get(request) ?? request.url;
|
|
39
|
+
}
|
|
40
|
+
function toNativeRequest(request, rawBody) {
|
|
41
|
+
const headers = new Headers();
|
|
42
|
+
for (const [name, value] of Object.entries(request.headers))
|
|
43
|
+
if (value !== void 0) headers.set(name, value);
|
|
44
|
+
const hasBody = !["GET", "HEAD"].includes(request.method.toUpperCase());
|
|
45
|
+
const body = !hasBody ? void 0 : rawBody ?? (request.body === void 0 ? void 0 : typeof request.body === "string" || request.body instanceof Uint8Array ? request.body : serializeWireJson(request.body));
|
|
46
|
+
if (!rawBody) headers.delete("content-length");
|
|
47
|
+
return new Request(request.url, {
|
|
48
|
+
method: request.method,
|
|
49
|
+
headers,
|
|
50
|
+
body: body instanceof Uint8Array ? new Uint8Array(body) : body,
|
|
51
|
+
signal: request.signal
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function readNativeNodeRequest(request, url, signal, handler) {
|
|
55
|
+
const matcher = handler?.[/* @__PURE__ */ Symbol.for("@skein-js/native-request-matcher")];
|
|
56
|
+
if (matcher?.(request.method ?? "GET", url) === false) return void 0;
|
|
57
|
+
const headers = {};
|
|
58
|
+
for (const [name, value] of Object.entries(request.headers))
|
|
59
|
+
headers[name] = Array.isArray(value) ? value.join(", ") : value;
|
|
60
|
+
let rawBody = request.rawBody;
|
|
61
|
+
if (!rawBody && request.body === void 0 && !["GET", "HEAD"].includes(request.method ?? "GET") && !request.readableEnded) {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let size = 0;
|
|
64
|
+
for await (const chunk of request) {
|
|
65
|
+
const bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(String(chunk));
|
|
66
|
+
size += bytes.byteLength;
|
|
67
|
+
if (size > 100 * 1024)
|
|
68
|
+
throw SkeinHttpError.payloadTooLarge("Request body exceeds 102400 bytes.");
|
|
69
|
+
chunks.push(bytes);
|
|
70
|
+
}
|
|
71
|
+
rawBody = Buffer.concat(chunks);
|
|
72
|
+
request.rawBody = rawBody;
|
|
73
|
+
const text = new TextDecoder().decode(rawBody);
|
|
74
|
+
try {
|
|
75
|
+
request.body = headers["content-type"]?.includes("application/json") ? text ? JSON.parse(text) : {} : text;
|
|
76
|
+
} catch {
|
|
77
|
+
throw SkeinHttpError.badRequest("Request body is not valid JSON.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const native = toNativeRequest(
|
|
81
|
+
{ method: request.method ?? "GET", url, headers, body: request.body, signal },
|
|
82
|
+
rawBody
|
|
83
|
+
);
|
|
84
|
+
sourceUrls.set(native, new URL(request.originalUrl ?? request.url ?? url, url).href);
|
|
85
|
+
return native;
|
|
86
|
+
}
|
|
87
|
+
async function sendNativeResponse(response, target) {
|
|
88
|
+
target.statusCode = response.status;
|
|
89
|
+
for (const [name, value] of response.headers)
|
|
90
|
+
if (name !== "set-cookie") target.setHeader(name, value);
|
|
91
|
+
const cookies = response.headers.getSetCookie();
|
|
92
|
+
if (cookies.length) target.setHeader("set-cookie", cookies);
|
|
93
|
+
if (!response.body) {
|
|
94
|
+
target.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const reader = response.body.getReader();
|
|
98
|
+
const close = () => {
|
|
99
|
+
void reader.cancel().catch(() => void 0);
|
|
100
|
+
};
|
|
101
|
+
target.once("close", close);
|
|
102
|
+
try {
|
|
103
|
+
for (; ; ) {
|
|
104
|
+
const chunk = await reader.read();
|
|
105
|
+
if (chunk.done || target.destroyed) break;
|
|
106
|
+
if (!target.write(chunk.value))
|
|
107
|
+
await new Promise((resolve) => {
|
|
108
|
+
const done = () => {
|
|
109
|
+
target.removeListener("drain", done);
|
|
110
|
+
target.removeListener("close", done);
|
|
111
|
+
resolve();
|
|
112
|
+
};
|
|
113
|
+
target.once("drain", done);
|
|
114
|
+
target.once("close", done);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
target.removeListener("close", close);
|
|
119
|
+
reader.releaseLock();
|
|
120
|
+
if (!target.writableEnded) target.end();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/graph-ui.ts
|
|
125
|
+
var watches = /* @__PURE__ */ new Map();
|
|
126
|
+
function escapeAttribute(value) {
|
|
127
|
+
return value.replace(
|
|
128
|
+
/[&<>"']/g,
|
|
129
|
+
(character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
async function readCompiledUi(config, configDir) {
|
|
133
|
+
const root = path.join(configDir, "ui");
|
|
134
|
+
let manifest;
|
|
135
|
+
try {
|
|
136
|
+
manifest = JSON.parse(
|
|
137
|
+
await readFile(path.join(root, "schemas.json"), "utf8")
|
|
138
|
+
);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.code === "ENOENT") return void 0;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
const files = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const graphId of Object.keys(config.ui ?? {})) {
|
|
145
|
+
const assets = manifest[graphId]?.assets;
|
|
146
|
+
if (!Array.isArray(assets) || !assets.length)
|
|
147
|
+
throw new Error(`Built UI manifest has no assets for "${graphId}".`);
|
|
148
|
+
const loaded = [];
|
|
149
|
+
for (const basename of assets) {
|
|
150
|
+
if (typeof basename !== "string" || basename !== path.basename(basename) || basename === "." || basename === "..")
|
|
151
|
+
throw new Error("Built UI manifest contains an invalid asset name.");
|
|
152
|
+
const filename = path.resolve(root, "public", graphId, basename);
|
|
153
|
+
if (!filename.startsWith(`${path.resolve(root, "public")}${path.sep}`))
|
|
154
|
+
throw new Error("Built UI asset escapes its artifact directory.");
|
|
155
|
+
loaded.push({ basename, contents: new Uint8Array(await readFile(filename)) });
|
|
156
|
+
}
|
|
157
|
+
files.set(graphId, loaded);
|
|
158
|
+
}
|
|
159
|
+
return files;
|
|
160
|
+
}
|
|
161
|
+
async function loadGraphUiHandler(config, configDir) {
|
|
162
|
+
if (!config.ui || !Object.keys(config.ui).length) return void 0;
|
|
163
|
+
let files = await readCompiledUi(config, configDir);
|
|
164
|
+
if (!files) {
|
|
165
|
+
const key = JSON.stringify([configDir, config.ui, config.ui_config]);
|
|
166
|
+
let pending = watches.get(key);
|
|
167
|
+
if (!pending) {
|
|
168
|
+
pending = (async () => {
|
|
169
|
+
const { build, watch } = await import(
|
|
170
|
+
/* webpackIgnore: true */
|
|
171
|
+
"@langchain/langgraph-ui"
|
|
172
|
+
);
|
|
173
|
+
await build({
|
|
174
|
+
cwd: configDir,
|
|
175
|
+
defs: config.ui,
|
|
176
|
+
config: config.ui_config,
|
|
177
|
+
output: ".skein/ui"
|
|
178
|
+
});
|
|
179
|
+
const results = await readCompiledUi(config, path.join(configDir, ".skein"));
|
|
180
|
+
await watch({
|
|
181
|
+
cwd: configDir,
|
|
182
|
+
defs: config.ui,
|
|
183
|
+
config: config.ui_config,
|
|
184
|
+
onOutput: (graphId, output) => {
|
|
185
|
+
results.set(graphId, output);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return results;
|
|
189
|
+
})();
|
|
190
|
+
watches.set(key, pending);
|
|
191
|
+
void pending.catch(() => watches.delete(key));
|
|
192
|
+
}
|
|
193
|
+
files = await pending;
|
|
194
|
+
}
|
|
195
|
+
const registry = files;
|
|
196
|
+
return async (request) => {
|
|
197
|
+
const url = new URL(request.url);
|
|
198
|
+
const match = /^\/ui\/([^/]+)(?:\/([^/]+))?$/.exec(url.pathname);
|
|
199
|
+
if (!match) return void 0;
|
|
200
|
+
let graphId;
|
|
201
|
+
let basename;
|
|
202
|
+
try {
|
|
203
|
+
graphId = decodeURIComponent(match[1]);
|
|
204
|
+
basename = match[2] ? decodeURIComponent(match[2]) : void 0;
|
|
205
|
+
} catch {
|
|
206
|
+
throw SkeinHttpError2.badRequest("Invalid UI path encoding.");
|
|
207
|
+
}
|
|
208
|
+
const assets = registry.get(graphId);
|
|
209
|
+
if (!assets?.length) return new Response("UI not found", { status: 404 });
|
|
210
|
+
if (request.method === "GET" && basename) {
|
|
211
|
+
const asset = assets.find((file) => file.basename === basename);
|
|
212
|
+
if (!asset) return new Response("Asset not found", { status: 404 });
|
|
213
|
+
const mime = {
|
|
214
|
+
".js": "text/javascript",
|
|
215
|
+
".css": "text/css",
|
|
216
|
+
".json": "application/json",
|
|
217
|
+
".svg": "image/svg+xml",
|
|
218
|
+
".png": "image/png",
|
|
219
|
+
".woff2": "font/woff2"
|
|
220
|
+
};
|
|
221
|
+
return new Response(new Uint8Array(asset.contents), {
|
|
222
|
+
headers: {
|
|
223
|
+
"content-type": mime[path.extname(basename)] ?? "application/octet-stream",
|
|
224
|
+
"cache-control": "no-cache"
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (request.method !== "POST" || basename) return void 0;
|
|
229
|
+
let body;
|
|
230
|
+
try {
|
|
231
|
+
body = await request.json();
|
|
232
|
+
} catch {
|
|
233
|
+
throw SkeinHttpError2.badRequest("UI bootstrap requires JSON.");
|
|
234
|
+
}
|
|
235
|
+
if (typeof body !== "object" || body === null || !("name" in body) || typeof body.name !== "string")
|
|
236
|
+
throw SkeinHttpError2.badRequest("UI bootstrap requires a component name.");
|
|
237
|
+
const assetUrl = (basename2) => escapeAttribute(
|
|
238
|
+
new URL(
|
|
239
|
+
`${encodeURIComponent(graphId)}/${encodeURIComponent(basename2)}`,
|
|
240
|
+
originalNativeUrl(request)
|
|
241
|
+
).href
|
|
242
|
+
);
|
|
243
|
+
const html = assets.filter((file) => file.basename.endsWith(".css")).map((file) => `<link rel="stylesheet" href="${assetUrl(file.basename)}" />`);
|
|
244
|
+
const script = assets.find((file) => file.basename.endsWith(".js"));
|
|
245
|
+
if (script) {
|
|
246
|
+
const stableName = graphId.replace(/[^a-zA-Z0-9]/g, "_");
|
|
247
|
+
const render = `__LGUI_${stableName}.render(${JSON.stringify(body.name)}, "{{shadowRootId}}")`;
|
|
248
|
+
html.push(
|
|
249
|
+
`<script src="${assetUrl(script.basename)}" onload="${escapeAttribute(render)}"></script>`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return new Response(html.join("\n"), {
|
|
253
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/config-http-handler.ts
|
|
259
|
+
async function loadConfigHttpHandler(config, options) {
|
|
260
|
+
const app = config.http?.["app"];
|
|
261
|
+
const ui = await loadGraphUiHandler(config, options.configDir);
|
|
262
|
+
if (app === void 0) return ui;
|
|
263
|
+
if (typeof app !== "string") throw new SkeinConfigError("http.app must be a path:export string.");
|
|
264
|
+
const spec = parseGraphSpec(app, options.configDir);
|
|
265
|
+
const importer = options.importModule ?? ((file) => import(pathToFileURL(file).href));
|
|
266
|
+
const module = await importer(spec.sourceFile);
|
|
267
|
+
const exported = module[spec.exportSymbol];
|
|
268
|
+
let fetch;
|
|
269
|
+
if (typeof exported === "function") fetch = exported;
|
|
270
|
+
else if (typeof exported === "object" && exported !== null && "fetch" in exported && typeof exported.fetch === "function")
|
|
271
|
+
fetch = exported.fetch.bind(exported);
|
|
272
|
+
else
|
|
273
|
+
throw new SkeinConfigError(
|
|
274
|
+
`http.app "${app}" must export a Fetch function or an object with fetch().`
|
|
275
|
+
);
|
|
276
|
+
return async (request) => {
|
|
277
|
+
const applicationRequest = request.clone();
|
|
278
|
+
let response;
|
|
279
|
+
try {
|
|
280
|
+
response = await fetch(applicationRequest);
|
|
281
|
+
if (!(response instanceof Response)) throw new TypeError("http.app must return a Response.");
|
|
282
|
+
if (response.status === 404) {
|
|
283
|
+
void response.body?.cancel().catch(() => void 0);
|
|
284
|
+
return ui?.(request);
|
|
285
|
+
}
|
|
286
|
+
void request.body?.cancel().catch(() => void 0);
|
|
287
|
+
return response;
|
|
288
|
+
} finally {
|
|
289
|
+
if (!response || response.status === 404 || response.body !== applicationRequest.body)
|
|
290
|
+
void applicationRequest.body?.cancel().catch(() => void 0);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
15
295
|
// src/cors-config.ts
|
|
16
296
|
import {
|
|
17
297
|
filterSkeinRoutes,
|
|
@@ -53,6 +333,8 @@ function corsFromHttpConfig(http) {
|
|
|
53
333
|
return toCorsOptions(cors);
|
|
54
334
|
}
|
|
55
335
|
var DISABLE_FLAGS = {
|
|
336
|
+
disable_mcp: "mcp",
|
|
337
|
+
disable_a2a: "a2a",
|
|
56
338
|
disable_assistants: "assistants",
|
|
57
339
|
disable_threads: "threads",
|
|
58
340
|
disable_runs: "runs",
|
|
@@ -102,20 +384,26 @@ function resolveIdempotency(raw) {
|
|
|
102
384
|
import { MemorySaver } from "@langchain/langgraph";
|
|
103
385
|
import {
|
|
104
386
|
cloneLangGraphCheckpoint,
|
|
387
|
+
inspectMemoryThreadStorage,
|
|
105
388
|
normalizeEmbeddableGraphs,
|
|
106
389
|
SkeinBaseStore
|
|
107
390
|
} from "@skein-js/langgraph";
|
|
108
|
-
import {
|
|
391
|
+
import {
|
|
392
|
+
MemoryRunEventBus,
|
|
393
|
+
MemoryThreadEventBus,
|
|
394
|
+
MemoryRunQueue,
|
|
395
|
+
MemorySkeinStore
|
|
396
|
+
} from "@skein-js/storage-memory";
|
|
109
397
|
|
|
110
398
|
// src/max-page-size.ts
|
|
111
399
|
import { DEFAULT_MAX_PAGE_SIZE } from "@skein-js/core";
|
|
112
400
|
|
|
113
401
|
// src/env-numbers.ts
|
|
114
|
-
import { SkeinConfigError } from "@skein-js/config/errors";
|
|
402
|
+
import { SkeinConfigError as SkeinConfigError2 } from "@skein-js/config/errors";
|
|
115
403
|
function requirePositiveInteger(source, raw) {
|
|
116
404
|
const parsed = Number(raw);
|
|
117
405
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
118
|
-
throw new
|
|
406
|
+
throw new SkeinConfigError2(`${source} must be a positive integer (got "${String(raw)}").`);
|
|
119
407
|
}
|
|
120
408
|
return parsed;
|
|
121
409
|
}
|
|
@@ -196,6 +484,7 @@ import {
|
|
|
196
484
|
} from "@skein-js/langgraph";
|
|
197
485
|
function embedInMemoryGraphs(graphs, overrides = {}) {
|
|
198
486
|
const webhooks = resolveWebhooks(void 0);
|
|
487
|
+
const checkpointer = overrides.checkpointer ?? new MemorySaver();
|
|
199
488
|
return {
|
|
200
489
|
// `maxPageSize` resolved for the same reason as the bus limits below: an embedded host's
|
|
201
490
|
// `SKEIN_MAX_PAGE_SIZE` has to reach the driver that applies it.
|
|
@@ -205,7 +494,11 @@ function embedInMemoryGraphs(graphs, overrides = {}) {
|
|
|
205
494
|
// Resolved rather than defaulted, so `SKEIN_MEMORY_BUS_*` reaches an embedded host too — this is
|
|
206
495
|
// the driver set a long-lived single-process deployment actually runs on.
|
|
207
496
|
bus: new MemoryRunEventBus(resolveMemoryBusLimits()),
|
|
208
|
-
|
|
497
|
+
threadBus: new MemoryThreadEventBus(),
|
|
498
|
+
checkpointer,
|
|
499
|
+
...checkpointer instanceof MemorySaver ? {
|
|
500
|
+
inspectThreadStorage: (threadId) => inspectMemoryThreadStorage(checkpointer, threadId)
|
|
501
|
+
} : {},
|
|
209
502
|
// Bridges long-term memory into a LangGraph `BaseStore` so nodes reach it via `getStore()`. A
|
|
210
503
|
// function on deps rather than an import inside the engine — see `ProtocolDeps.storeBridge`.
|
|
211
504
|
storeBridge: (store) => new SkeinBaseStore(store),
|
|
@@ -229,6 +522,7 @@ var createInMemoryDeps = embedInMemoryGraphs;
|
|
|
229
522
|
function resolveThreadTtl(raw) {
|
|
230
523
|
if (!raw) return void 0;
|
|
231
524
|
const ttl = {};
|
|
525
|
+
if (raw.strategy === "keep_latest") ttl.strategy = raw.strategy;
|
|
232
526
|
if (typeof raw.default_ttl === "number") ttl.defaultTtl = raw.default_ttl;
|
|
233
527
|
if (typeof raw.sweep_interval_minutes === "number") {
|
|
234
528
|
ttl.sweepIntervalMinutes = raw.sweep_interval_minutes;
|
|
@@ -262,6 +556,7 @@ async function loadInMemoryRuntime(configPath, importModule, staticSchemas) {
|
|
|
262
556
|
});
|
|
263
557
|
const deps = embedInMemoryGraphs(toGraphResolver(graphs));
|
|
264
558
|
deps.auth = await loadAuthEngine(config.auth, { configDir, importModule });
|
|
559
|
+
deps.httpHandler = await loadConfigHttpHandler(config, { configDir, importModule });
|
|
265
560
|
return {
|
|
266
561
|
deps,
|
|
267
562
|
channels: await loadChannels(config.skein?.channels, {
|
|
@@ -296,6 +591,10 @@ async function loadReloadableInMemoryRuntime(configPath, importModule, staticSch
|
|
|
296
591
|
store,
|
|
297
592
|
checkpointer,
|
|
298
593
|
auth: await loadAuthEngine(first.config.auth, { configDir: first.configDir, importModule }),
|
|
594
|
+
httpHandler: await loadConfigHttpHandler(first.config, {
|
|
595
|
+
configDir: first.configDir,
|
|
596
|
+
importModule
|
|
597
|
+
}),
|
|
299
598
|
// Present only when configured — its presence is what starts the thread TTL sweeper.
|
|
300
599
|
...threadTtl ? { threadTtl } : {},
|
|
301
600
|
// Unlike `threadTtl`, absence here means *defaults*, not *off*: the header is honoured either way.
|
|
@@ -319,7 +618,13 @@ async function loadReloadableInMemoryRuntime(configPath, importModule, staticSch
|
|
|
319
618
|
config: first.config,
|
|
320
619
|
configDir: first.configDir,
|
|
321
620
|
reloadGraphs: async () => {
|
|
322
|
-
|
|
621
|
+
const next = await loadConfig({ configPath, importModule, staticSchemas });
|
|
622
|
+
const httpHandler = await loadConfigHttpHandler(next.config, {
|
|
623
|
+
configDir: next.configDir,
|
|
624
|
+
importModule
|
|
625
|
+
});
|
|
626
|
+
current = next.graphs;
|
|
627
|
+
deps.httpHandler = httpHandler;
|
|
323
628
|
},
|
|
324
629
|
snapshotState: () => ({
|
|
325
630
|
version: 1,
|
|
@@ -336,6 +641,11 @@ async function loadReloadableInMemoryRuntime(configPath, importModule, staticSch
|
|
|
336
641
|
export {
|
|
337
642
|
requirePositiveInteger,
|
|
338
643
|
positiveIntegerFromEnv,
|
|
644
|
+
withNativePath,
|
|
645
|
+
toNativeRequest,
|
|
646
|
+
readNativeNodeRequest,
|
|
647
|
+
sendNativeResponse,
|
|
648
|
+
loadConfigHttpHandler,
|
|
339
649
|
toCorsOptions,
|
|
340
650
|
corsFromHttpConfig,
|
|
341
651
|
disabledRoutesFromHttpConfig,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProtocolRuntime, RouteBinding, Logger, ProtocolDeps, RunWorkerOptions, CronSchedulerOptions, RunFailureReport, IdempotencyConfig, WebhookDeliveryConfig, GraphResolver, RouteMatcher, DisabledRouteGroups, ProtocolResponse } from '@skein-js/agent-protocol';
|
|
1
|
+
import { ProtocolRuntime, RouteBinding, Logger, ProtocolDeps, RunWorkerOptions, CronSchedulerOptions, RunFailureReport, IdempotencyConfig, WebhookDeliveryConfig, GraphResolver, RouteMatcher, DisabledRouteGroups, ProtocolResponse, ProtocolRequest } from '@skein-js/agent-protocol';
|
|
2
2
|
export { CompiledGraphFactory, DEFAULT_RUN_CONCURRENCY, DEFAULT_SHUTDOWN_GRACE_MS, GraphResolver, Logger, ProtocolDeps, ResolvedGraph, RunWorkerOptions } from '@skein-js/agent-protocol';
|
|
3
3
|
import { LoadedChannel, ModuleImporter, LanggraphJson, GraphSchemas } from '@skein-js/config';
|
|
4
4
|
import { CorsOptions } from 'cors';
|
|
@@ -9,9 +9,11 @@ export { a as DevStateCounts } from './dev-Zz4VRv7T.js';
|
|
|
9
9
|
import { StoreTtlConfig, ThreadTtlConfig } from '@skein-js/core';
|
|
10
10
|
import { EmbeddableGraph } from '@skein-js/langgraph';
|
|
11
11
|
export { EmbeddableGraph, graphMapToResolver, normalizeEmbeddableGraphs } from '@skein-js/langgraph';
|
|
12
|
-
import { ServerResponse } from 'node:http';
|
|
12
|
+
import { ServerResponse, IncomingMessage } from 'node:http';
|
|
13
13
|
|
|
14
14
|
interface SkeinRuntimeCommonOptions {
|
|
15
|
+
/** Override the mounted protocol route inventory, including discovery. */
|
|
16
|
+
routes?: readonly RouteBinding[];
|
|
15
17
|
/**
|
|
16
18
|
* Where skein reports itself: failed runs, webhook delivery failures, background-run summaries, and
|
|
17
19
|
* the adapter's own transport faults. It reaches **the run engine**, not just the transport — this
|
|
@@ -469,15 +471,14 @@ extraDeps?: Partial<Omit<ProtocolDeps, "graphs">>): Promise<ReloadableInMemoryRu
|
|
|
469
471
|
/** The raw `checkpointer.ttl` block as `langgraph.json` spells it. */
|
|
470
472
|
interface RawThreadTtl {
|
|
471
473
|
default_ttl?: number;
|
|
472
|
-
strategy?: "delete";
|
|
474
|
+
strategy?: "delete" | "keep_latest";
|
|
473
475
|
sweep_interval_minutes?: number;
|
|
474
476
|
}
|
|
475
477
|
/**
|
|
476
478
|
* Map the raw block to {@link ThreadTtlConfig}, or `undefined` when nothing is set — so threads never
|
|
477
479
|
* expire unless asked to, and no sweeper is started.
|
|
478
480
|
*
|
|
479
|
-
*
|
|
480
|
-
* expired thread can become, so there is nothing for the drivers to branch on.
|
|
481
|
+
* Deletion stays the default; keep_latest is explicit so existing deployments retain their policy.
|
|
481
482
|
*/
|
|
482
483
|
declare function resolveThreadTtl(raw: RawThreadTtl | undefined): ThreadTtlConfig | undefined;
|
|
483
484
|
/** The raw `store.ttl` block as `langgraph.json` spells it. */
|
|
@@ -716,4 +717,25 @@ type RuntimeGlobal = typeof globalThis & {
|
|
|
716
717
|
/** Detect Node, Bun, or Deno without importing a runtime-specific module. */
|
|
717
718
|
declare function detectRuntimeCapabilities(scope?: RuntimeGlobal): RuntimeCapabilities;
|
|
718
719
|
|
|
719
|
-
|
|
720
|
+
/** Load the same path:export notation as graphs; preserve Hono's receiver for its fetch method. */
|
|
721
|
+
declare function loadConfigHttpHandler(config: LanggraphJson, options: {
|
|
722
|
+
configDir: string;
|
|
723
|
+
importModule?: ModuleImporter;
|
|
724
|
+
}): Promise<((request: Request) => Promise<Response | undefined>) | undefined>;
|
|
725
|
+
|
|
726
|
+
/** Strip a mount path for app dispatch while retaining the public URL for asset references. */
|
|
727
|
+
declare function withNativePath(request: Request, pathname: string): Request;
|
|
728
|
+
/** Build a Fetch request from a framework's normalized body without changing protocol responses. */
|
|
729
|
+
declare function toNativeRequest(request: Pick<ProtocolRequest, "method" | "url" | "headers" | "body" | "signal">, rawBody?: Uint8Array): Request;
|
|
730
|
+
/** Native Node bridge also handles binary/form bodies when the host has not parsed them. */
|
|
731
|
+
type NativeNodeRequest = IncomingMessage & {
|
|
732
|
+
body?: unknown;
|
|
733
|
+
rawBody?: Uint8Array;
|
|
734
|
+
originalUrl?: string;
|
|
735
|
+
};
|
|
736
|
+
declare function readNativeNodeRequest(request: NativeNodeRequest, url: string, signal?: AbortSignal): Promise<Request>;
|
|
737
|
+
declare function readNativeNodeRequest(request: NativeNodeRequest, url: string, signal: AbortSignal | undefined, handler: (request: Request) => Promise<Response | undefined>): Promise<Request | undefined>;
|
|
738
|
+
/** Stream binary, HTML, JSON or SSE from a native app with backpressure and cancellation. */
|
|
739
|
+
declare function sendNativeResponse(response: Response, target: ServerResponse): Promise<void>;
|
|
740
|
+
|
|
741
|
+
export { type ConsoleLogLevel, type ConsoleLoggerOptions, type CorsSetting, DEFAULT_HEAP_SAMPLE_MS, DEFAULT_HEAP_WARN_PERCENT, DevStateSnapshot, type FileReader, HEAP_REARM_MARGIN_PERCENT, HEAP_REARM_PERCENT, type HeapHeadroom, type HeapPressureContext, type HeapPressureMonitor, type HeapPressureMonitorOptions, type InMemoryRuntimeConfig, type IntervalHandle, type LanggraphCorsConfig, type RawIdempotency, type RawStoreTtl, type RawThreadTtl, type RawWebhooks, type ReloadableInMemoryRuntime, type ResolvedMemoryBusLimits, type ResolvedProtocolRuntime, type ResolvedRuntimeDeps, type RuntimeCapabilities, type RuntimeMemorySnapshot, type RuntimeSignalSubscription, type SkeinRuntimeCommonOptions, type SkeinRuntimeOptions, allowedOrigin, applyNodeCors, checkHeapHeadroom, consoleMountFromHttpConfig, corsFromHttpConfig, corsPreflightHeaders, corsResponseHeaders, createConsoleLogger, createInMemoryDeps, describeError, describeHeapHeadroom, describePoolPressure, detectRuntimeCapabilities, disabledRoutesFromHttpConfig, embedInMemoryGraphs, formatLogMeta, joinList, loadConfigHttpHandler, loadInMemoryRuntime, loadReloadableInMemoryRuntime, readContainerMemoryLimit, readNativeNodeRequest, resolveCronTickMs, resolveHeapPressureOptions, resolveIdempotency, resolveMaxPageSize, resolveMemoryBusLimits, resolveProtocolRuntime, resolveRunConcurrency, resolveRunTimeoutMs, resolveRuntimeDeps, resolveShutdownGraceMs, resolveStoreTtl, resolveThreadTtl, resolveWebhooks, routeMatcherFor, routesFromHttpConfig, runFailureIdentity, sendNativeResponse, sendNodeError, sendNodePreflight, sendNodeResponse, startHeapPressureMonitor, stripBasePath, toCorsOptions, toNativeRequest, withNativePath };
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
disabledRoutesFromHttpConfig,
|
|
6
6
|
embedInMemoryGraphs,
|
|
7
7
|
graphMapToResolver,
|
|
8
|
+
loadConfigHttpHandler,
|
|
8
9
|
loadInMemoryRuntime,
|
|
9
10
|
loadReloadableInMemoryRuntime,
|
|
10
11
|
normalizeEmbeddableGraphs,
|
|
11
12
|
positiveIntegerFromEnv,
|
|
13
|
+
readNativeNodeRequest,
|
|
12
14
|
requirePositiveInteger,
|
|
13
15
|
resolveIdempotency,
|
|
14
16
|
resolveMaxPageSize,
|
|
@@ -17,8 +19,11 @@ import {
|
|
|
17
19
|
resolveThreadTtl,
|
|
18
20
|
resolveWebhooks,
|
|
19
21
|
routesFromHttpConfig,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
sendNativeResponse,
|
|
23
|
+
toCorsOptions,
|
|
24
|
+
toNativeRequest,
|
|
25
|
+
withNativePath
|
|
26
|
+
} from "./chunk-3IKRBGJU.js";
|
|
22
27
|
import "./chunk-JUBBCC7R.js";
|
|
23
28
|
|
|
24
29
|
// src/resolve-runtime.ts
|
|
@@ -168,13 +173,13 @@ function resolveLogger(option, injected, frameworkLogger) {
|
|
|
168
173
|
return option ?? frameworkLogger;
|
|
169
174
|
}
|
|
170
175
|
async function resolveRuntimeDeps(options, frameworkLogger) {
|
|
171
|
-
const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes, channels: options.channels } : await (await import("./in-memory-runtime-
|
|
176
|
+
const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes, channels: options.channels } : await (await import("./in-memory-runtime-D4PEZVMU.js")).loadInMemoryRuntime(options.config, options.importModule);
|
|
172
177
|
const logger = resolveLogger(options.logger, loaded.deps.logger, frameworkLogger);
|
|
173
178
|
if (logger && !loaded.deps.logger) loaded.deps.logger = logger;
|
|
174
179
|
return {
|
|
175
180
|
deps: loaded.deps,
|
|
176
181
|
cors: loaded.cors,
|
|
177
|
-
routes: loaded.routes,
|
|
182
|
+
routes: options.routes ?? loaded.routes,
|
|
178
183
|
...loaded.channels ? { channels: loaded.channels } : {},
|
|
179
184
|
logger
|
|
180
185
|
};
|
|
@@ -190,22 +195,18 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
|
|
|
190
195
|
const heapPressureOptions = resolveHeapPressureOptions();
|
|
191
196
|
const runTimeoutMs = resolveRunTimeoutMs(deps.runTimeoutMs);
|
|
192
197
|
if (runTimeoutMs !== void 0) deps.runTimeoutMs = runTimeoutMs;
|
|
193
|
-
const
|
|
194
|
-
const channelSurface = channels ? await (await import("./channels-runtime-JHTMXARF.js")).resolveChannels({
|
|
198
|
+
const channelSurface = channels ? await (await import("./channels-runtime-5TESQBKO.js")).resolveChannels({
|
|
195
199
|
channels,
|
|
196
200
|
graphIds: deps.graphs.ids,
|
|
197
|
-
deps
|
|
198
|
-
service: () => {
|
|
199
|
-
const current = runtimeRef.current;
|
|
200
|
-
if (!current) throw new Error("channel pipeline used before the runtime was assembled");
|
|
201
|
-
return current.service;
|
|
202
|
-
}
|
|
201
|
+
deps
|
|
203
202
|
}) : void 0;
|
|
204
203
|
if (channelSurface) {
|
|
205
204
|
const inner = deps.webhookDispatcher ?? fetchWebhookDispatcher;
|
|
206
205
|
deps.webhookDispatcher = channelSurface.wrapDispatcher(inner);
|
|
207
206
|
}
|
|
207
|
+
const mountedRoutes = channelSurface ? [...routes, ...channelSurface.routes] : routes;
|
|
208
208
|
const runtime = createProtocolRuntime(deps, {
|
|
209
|
+
routes: mountedRoutes,
|
|
209
210
|
...channelSurface ? { handlers: channelSurface.handlers } : {},
|
|
210
211
|
worker: {
|
|
211
212
|
...options.worker,
|
|
@@ -220,7 +221,6 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
|
|
|
220
221
|
enabled: options.scheduler?.enabled ?? routes.some((route) => route.group === "crons")
|
|
221
222
|
}
|
|
222
223
|
});
|
|
223
|
-
runtimeRef.current = runtime;
|
|
224
224
|
await runtime.service.assistants.registerGraphAssistants();
|
|
225
225
|
if (options.warm) {
|
|
226
226
|
await Promise.all(
|
|
@@ -262,7 +262,7 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
|
|
|
262
262
|
// Channel routes are *appended*, never declared in `skeinRoutes`: they exist only when a channel
|
|
263
263
|
// is configured, which is what makes the feature undetectable to a deployment that never wanted
|
|
264
264
|
// it — strictly stronger than an `http.disable_*` flag, and it needs no `RouteGroup` to express.
|
|
265
|
-
routes:
|
|
265
|
+
routes: mountedRoutes,
|
|
266
266
|
logger
|
|
267
267
|
};
|
|
268
268
|
}
|
|
@@ -707,10 +707,12 @@ export {
|
|
|
707
707
|
formatLogMeta,
|
|
708
708
|
graphMapToResolver,
|
|
709
709
|
joinList,
|
|
710
|
+
loadConfigHttpHandler,
|
|
710
711
|
loadInMemoryRuntime,
|
|
711
712
|
loadReloadableInMemoryRuntime,
|
|
712
713
|
normalizeEmbeddableGraphs,
|
|
713
714
|
readContainerMemoryLimit,
|
|
715
|
+
readNativeNodeRequest,
|
|
714
716
|
resolveCronTickMs,
|
|
715
717
|
resolveHeapPressureOptions,
|
|
716
718
|
resolveIdempotency,
|
|
@@ -727,10 +729,13 @@ export {
|
|
|
727
729
|
routeMatcherFor,
|
|
728
730
|
routesFromHttpConfig,
|
|
729
731
|
runFailureIdentity,
|
|
732
|
+
sendNativeResponse,
|
|
730
733
|
sendNodeError,
|
|
731
734
|
sendNodePreflight,
|
|
732
735
|
sendNodeResponse,
|
|
733
736
|
startHeapPressureMonitor,
|
|
734
737
|
stripBasePath,
|
|
735
|
-
toCorsOptions
|
|
738
|
+
toCorsOptions,
|
|
739
|
+
toNativeRequest,
|
|
740
|
+
withNativePath
|
|
736
741
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skein-js/server-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Shared, framework-agnostic building blocks for skein-js HTTP adapters — in-memory dev runtime, LangGraph dev-state import, and langgraph.json CORS mapping.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Maina Wycliffe <wmmaina7@gmail.com>",
|
|
@@ -49,22 +49,23 @@
|
|
|
49
49
|
"cors": "^2.8.5",
|
|
50
50
|
"superjson": "^2.2.6",
|
|
51
51
|
"zod": "^3.25.76",
|
|
52
|
-
"@skein-js/agent-protocol": "0.
|
|
53
|
-
"@skein-js/
|
|
54
|
-
"@skein-js/
|
|
55
|
-
"@skein-js/langgraph": "0.
|
|
56
|
-
"@skein-js/storage-memory": "0.
|
|
52
|
+
"@skein-js/agent-protocol": "0.23.0",
|
|
53
|
+
"@skein-js/core": "0.23.0",
|
|
54
|
+
"@skein-js/config": "0.23.0",
|
|
55
|
+
"@skein-js/langgraph": "0.23.0",
|
|
56
|
+
"@skein-js/storage-memory": "0.23.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@langchain/langgraph": "^1.4.0",
|
|
60
|
-
"@skein-js/channels": "0.
|
|
60
|
+
"@skein-js/channels": "0.23.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@langchain/core": "^1.2.9",
|
|
64
64
|
"@langchain/langgraph": "^1.4.0",
|
|
65
65
|
"@langchain/langgraph-sdk": "^1.9.0",
|
|
66
66
|
"@types/cors": "^2.8.17",
|
|
67
|
-
"
|
|
67
|
+
"hono": "4.13.13",
|
|
68
|
+
"@skein-js/channels": "0.23.0"
|
|
68
69
|
},
|
|
69
70
|
"publishConfig": {
|
|
70
71
|
"access": "public"
|
|
@@ -73,5 +74,8 @@
|
|
|
73
74
|
"@skein-js/channels": {
|
|
74
75
|
"optional": true
|
|
75
76
|
}
|
|
77
|
+
},
|
|
78
|
+
"optionalDependencies": {
|
|
79
|
+
"@langchain/langgraph-ui": "1.4.2"
|
|
76
80
|
}
|
|
77
81
|
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// src/channels-runtime.ts
|
|
2
|
-
import {
|
|
3
|
-
createAuthScopedStore,
|
|
4
|
-
createContext,
|
|
5
|
-
createProtocolServiceFromContext
|
|
6
|
-
} from "@skein-js/agent-protocol";
|
|
7
|
-
import { SkeinConfigError } from "@skein-js/config";
|
|
8
|
-
import { SkeinHttpError } from "@skein-js/core";
|
|
9
|
-
async function resolveChannels(input) {
|
|
10
|
-
const names = Object.keys(input.channels);
|
|
11
|
-
if (names.length === 0) return void 0;
|
|
12
|
-
const channels = await importChannelsPackage(names);
|
|
13
|
-
const registry = channels.buildChannelRegistry({
|
|
14
|
-
channels: Object.fromEntries(
|
|
15
|
-
Object.entries(input.channels).map(([name, loaded]) => [
|
|
16
|
-
name,
|
|
17
|
-
{
|
|
18
|
-
module: loaded.module,
|
|
19
|
-
config: {
|
|
20
|
-
assistant: loaded.assistant,
|
|
21
|
-
...loaded.allowedAssistants ? { allowedAssistants: loaded.allowedAssistants } : {},
|
|
22
|
-
...loaded.publicUrl ? { publicUrl: loaded.publicUrl } : {}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
])
|
|
26
|
-
),
|
|
27
|
-
graphIds: input.graphIds
|
|
28
|
-
});
|
|
29
|
-
const pipelineDeps = buildPipelineDeps(input.deps, input.service);
|
|
30
|
-
const channelInventory = registry.names.map((routeName) => {
|
|
31
|
-
const registered = registry.get(routeName);
|
|
32
|
-
if (!registered) throw new Error(`Channel registry lost configured route "${routeName}".`);
|
|
33
|
-
return {
|
|
34
|
-
route_name: routeName,
|
|
35
|
-
assistant: registered.config.assistant,
|
|
36
|
-
allowed_assistants: [...registered.config.allowedAssistants ?? []],
|
|
37
|
-
channel_name: registered.channel.name,
|
|
38
|
-
delivery_supported: typeof registered.channel.deliver === "function"
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
return {
|
|
42
|
-
routes: [
|
|
43
|
-
{ method: "get", path: "/channels", handler: "listChannels" },
|
|
44
|
-
...channels.channelRouteBindings(registry.names)
|
|
45
|
-
],
|
|
46
|
-
wrapDispatcher: (inner) => channels.wrapChannelDispatcher({
|
|
47
|
-
registry,
|
|
48
|
-
inner,
|
|
49
|
-
logger: input.deps.logger ?? console
|
|
50
|
-
}),
|
|
51
|
-
handlers: {
|
|
52
|
-
listChannels: async () => ({
|
|
53
|
-
kind: "json",
|
|
54
|
-
status: 200,
|
|
55
|
-
body: { channels: channelInventory }
|
|
56
|
-
}),
|
|
57
|
-
handleInboundEvent: async (req) => {
|
|
58
|
-
const name = req.url.split("?")[0].split("/channels/")[1] ?? "";
|
|
59
|
-
const registered = registry.get(name);
|
|
60
|
-
if (!registered) throw SkeinHttpError.notFound(`No channel named "${name}".`);
|
|
61
|
-
return channels.handleInboundEvent(
|
|
62
|
-
registered,
|
|
63
|
-
{
|
|
64
|
-
method: req.method,
|
|
65
|
-
url: req.url,
|
|
66
|
-
headers: req.headers,
|
|
67
|
-
...typeof req.body === "string" ? { text: req.body } : {}
|
|
68
|
-
},
|
|
69
|
-
pipelineDeps
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function buildPipelineDeps(deps, service) {
|
|
76
|
-
let requestFilters;
|
|
77
|
-
const scopedService = (authContext) => {
|
|
78
|
-
if (!authContext && !requestFilters) return service();
|
|
79
|
-
const base = createContext(deps);
|
|
80
|
-
return createProtocolServiceFromContext({
|
|
81
|
-
...base,
|
|
82
|
-
...authContext?.user ? { authUser: authContext.user } : {},
|
|
83
|
-
...authContext?.scopes ? { authScopes: authContext.scopes } : {},
|
|
84
|
-
deps: requestFilters && deps.auth ? {
|
|
85
|
-
...base.deps,
|
|
86
|
-
store: createAuthScopedStore(base.deps.store, deps.auth, requestFilters, "threads")
|
|
87
|
-
} : base.deps
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
return {
|
|
91
|
-
...deps.store.idempotency ? { idempotency: deps.store.idempotency } : {},
|
|
92
|
-
clock: () => deps.clock ? deps.clock() : /* @__PURE__ */ new Date(),
|
|
93
|
-
logger: deps.logger ?? console,
|
|
94
|
-
// Straight off the run event bus — the same stream `runs.joinStream` serves, without the HTTP
|
|
95
|
-
// connection per conversation that reaching it from outside would cost.
|
|
96
|
-
runFrames: { subscribe: (runId) => deps.bus.subscribe(runId) },
|
|
97
|
-
authorize: async (principal, authz) => {
|
|
98
|
-
if (!deps.auth) return void 0;
|
|
99
|
-
const context = {
|
|
100
|
-
user: {
|
|
101
|
-
identity: principal.identity,
|
|
102
|
-
display_name: principal.identity,
|
|
103
|
-
is_authenticated: true,
|
|
104
|
-
permissions: principal.permissions ?? [],
|
|
105
|
-
...principal.metadata ?? {}
|
|
106
|
-
},
|
|
107
|
-
scopes: principal.permissions ?? []
|
|
108
|
-
};
|
|
109
|
-
const decision = await deps.auth.authorize({
|
|
110
|
-
resource: authz.resource,
|
|
111
|
-
action: authz.action,
|
|
112
|
-
value: {},
|
|
113
|
-
context
|
|
114
|
-
});
|
|
115
|
-
requestFilters = decision.filters;
|
|
116
|
-
return context;
|
|
117
|
-
},
|
|
118
|
-
ensureThread: async (threadId, metadata, authContext) => {
|
|
119
|
-
await scopedService(authContext).threads.create({
|
|
120
|
-
thread_id: threadId,
|
|
121
|
-
metadata,
|
|
122
|
-
ifExists: "do_nothing"
|
|
123
|
-
});
|
|
124
|
-
},
|
|
125
|
-
threadStatus: async (threadId) => {
|
|
126
|
-
const thread = await service().threads.get(threadId).catch(() => null);
|
|
127
|
-
return thread?.status ?? null;
|
|
128
|
-
},
|
|
129
|
-
createRun: async (input) => {
|
|
130
|
-
const run = await scopedService(input.authContext).runs.createBackground(input.threadId, {
|
|
131
|
-
assistant_id: input.assistantId,
|
|
132
|
-
...input.input !== void 0 ? { input: input.input } : {},
|
|
133
|
-
...input.command ? { command: input.command } : {},
|
|
134
|
-
...input.ifThreadStatus ? { if_thread_status: [...input.ifThreadStatus] } : {},
|
|
135
|
-
...input.multitaskStrategy ? { multitask_strategy: input.multitaskStrategy } : {},
|
|
136
|
-
...input.metadata ? { metadata: input.metadata } : {},
|
|
137
|
-
...input.webhook ? { webhook: input.webhook } : {},
|
|
138
|
-
stream_mode: [...input.streamMode]
|
|
139
|
-
});
|
|
140
|
-
return { runId: run.run_id };
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
async function importChannelsPackage(names) {
|
|
145
|
-
try {
|
|
146
|
-
return await import("@skein-js/channels");
|
|
147
|
-
} catch (cause) {
|
|
148
|
-
throw new SkeinConfigError(
|
|
149
|
-
`skein.channels configures ${names.join(", ")}, but @skein-js/channels is not installed. Add it as a dependency.`,
|
|
150
|
-
{ cause }
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
export {
|
|
155
|
-
resolveChannels
|
|
156
|
-
};
|