@skein-js/agent-protocol 0.8.0 → 0.9.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/README.md +4 -1
- package/dist/index.d.ts +67 -13
- package/dist/index.js +216 -75
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -102,7 +102,10 @@ Graph **state, history, and interrupt/resume are 100% LangGraph-native** via the
|
|
|
102
102
|
|
|
103
103
|
- **Entry points:** `createProtocolRuntime(deps, options?)` → `{ service, handlers, worker }`;
|
|
104
104
|
`createProtocolService` / `createProtocolServiceFromContext`; `createProtocolHandlers`; `createContext`;
|
|
105
|
-
`createRunWorker(ctx, options?)` (`RunWorkerOptions`: `maxConcurrency
|
|
105
|
+
`createRunWorker(ctx, options?)` (`RunWorkerOptions`: `maxConcurrency` — queued runs at once,
|
|
106
|
+
default `DEFAULT_RUN_CONCURRENCY` (10, matching the LangGraph CLI) — and `shutdownGraceMs`). The
|
|
107
|
+
adapters surface this as `worker.maxConcurrency` and also read `SKEIN_RUN_CONCURRENCY` /
|
|
108
|
+
`N_JOBS_PER_WORKER`; see [runs-and-redis.md](../../docs/runs-and-redis.md#run-concurrency).
|
|
106
109
|
- **Service surface** (`runtime.service`): `assistants` (`registerGraphAssistants`, `get`, `list`,
|
|
107
110
|
`search`, `schemas`), `threads` (`create`/`get`/`list`/`patch`/`delete`/`history`/`getState`),
|
|
108
111
|
`threadStream` (`stream` / `joinStream` / `command` — HIL resume, requires status `interrupted`),
|
package/dist/index.d.ts
CHANGED
|
@@ -374,6 +374,12 @@ interface ProtocolRequest {
|
|
|
374
374
|
query: Record<string, string | string[] | undefined>;
|
|
375
375
|
body: unknown;
|
|
376
376
|
headers: Record<string, string | undefined>;
|
|
377
|
+
/**
|
|
378
|
+
* Aborts when the client goes away, so a handler can stop work the caller will never read.
|
|
379
|
+
* Optional: an adapter that can't observe disconnects simply omits it. Currently honored by the
|
|
380
|
+
* single-graph invoke surface; protocol runs carry their own cancellation through the run row.
|
|
381
|
+
*/
|
|
382
|
+
signal?: AbortSignal;
|
|
377
383
|
}
|
|
378
384
|
/** A normalized response an adapter serializes back onto its framework response. */
|
|
379
385
|
type ProtocolResponse = {
|
|
@@ -430,8 +436,19 @@ interface ProtocolHandlers {
|
|
|
430
436
|
}
|
|
431
437
|
declare function createProtocolHandlers(service: ProtocolService): ProtocolHandlers;
|
|
432
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Queued runs one worker executes at once when nothing is configured. Matches the LangGraph CLI's
|
|
441
|
+
* `--n-jobs-per-worker` default, so a project moving off `langgraph dev` keeps its throughput.
|
|
442
|
+
*/
|
|
443
|
+
declare const DEFAULT_RUN_CONCURRENCY = 10;
|
|
433
444
|
interface RunWorkerOptions {
|
|
434
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* Max queued runs this worker executes at once. Default {@link DEFAULT_RUN_CONCURRENCY}.
|
|
447
|
+
* Per-thread ordering does not depend on this: `startRunExecution` serializes by `thread_id` at
|
|
448
|
+
* every value. The trade-off is head-of-line blocking — a run waiting on a busy thread's lock
|
|
449
|
+
* still holds a slot, so a burst of `multitask_strategy: "enqueue"` runs on one thread can occupy
|
|
450
|
+
* the worker. See docs/runs-and-redis.md.
|
|
451
|
+
*/
|
|
435
452
|
maxConcurrency?: number;
|
|
436
453
|
/** How long `stop()` waits for in-flight runs before aborting them (ms). Default 5000. */
|
|
437
454
|
shutdownGraceMs?: number;
|
|
@@ -446,7 +463,7 @@ declare function createRunWorker(ctx: ProtocolContext, options?: RunWorkerOption
|
|
|
446
463
|
|
|
447
464
|
/** Options for {@link createProtocolRuntime}. */
|
|
448
465
|
interface ProtocolRuntimeOptions {
|
|
449
|
-
/** Tuning for the background run worker (concurrency,
|
|
466
|
+
/** Tuning for the background run worker (max concurrency, shutdown grace period). */
|
|
450
467
|
worker?: RunWorkerOptions;
|
|
451
468
|
}
|
|
452
469
|
/** The wired engine: the service, the transport-neutral handler table, and the background worker. */
|
|
@@ -466,11 +483,16 @@ interface ProtocolRuntime {
|
|
|
466
483
|
declare function createProtocolRuntime(deps: ProtocolDeps, options?: ProtocolRuntimeOptions): ProtocolRuntime;
|
|
467
484
|
|
|
468
485
|
type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
469
|
-
|
|
486
|
+
/**
|
|
487
|
+
* One route. `HandlerName` defaults to a member of the protocol handler table; a surface mounted
|
|
488
|
+
* outside that table (the single-graph invoke endpoint) parameterizes it with its own handler names,
|
|
489
|
+
* so `handler` never has to lie about being a `keyof ProtocolHandlers` an adapter could look up.
|
|
490
|
+
*/
|
|
491
|
+
interface RouteBinding<HandlerName = keyof ProtocolHandlers> {
|
|
470
492
|
method: HttpMethod;
|
|
471
493
|
/** Path with `:param` placeholders, e.g. `/threads/:thread_id/runs/stream`. */
|
|
472
494
|
path: string;
|
|
473
|
-
handler:
|
|
495
|
+
handler: HandlerName;
|
|
474
496
|
/**
|
|
475
497
|
* Fold the path `thread_id` into the request body before dispatch. The SDK addresses a
|
|
476
498
|
* thread-scoped run by its path (`POST /threads/{id}/runs/stream`) while carrying only
|
|
@@ -493,18 +515,50 @@ declare function copyThreadIdIntoBody(request: ProtocolRequest): ProtocolRequest
|
|
|
493
515
|
*/
|
|
494
516
|
declare const foldThreadId: typeof copyThreadIdIntoBody;
|
|
495
517
|
/** A resolved route: the matched binding plus the path params extracted from the URL. */
|
|
496
|
-
interface RouteMatch {
|
|
497
|
-
binding: RouteBinding
|
|
518
|
+
interface RouteMatch<HandlerName = keyof ProtocolHandlers> {
|
|
519
|
+
binding: RouteBinding<HandlerName>;
|
|
498
520
|
params: Record<string, string>;
|
|
499
521
|
}
|
|
522
|
+
/** Matches a `method` + `pathname` against a fixed route table. Built by {@link createRouteMatcher}. */
|
|
523
|
+
type RouteMatcher<HandlerName = keyof ProtocolHandlers> = (method: string, pathname: string) => RouteMatch<HandlerName> | undefined;
|
|
524
|
+
/**
|
|
525
|
+
* Build a matcher over any route table — the protocol's own, or the simplified invoke surface's.
|
|
526
|
+
* Each `:param` path is compiled to a named-group regex once, anchored (`^…$`) so a literal segment
|
|
527
|
+
* can't be matched as a param value. The returned matcher iterates in table order, which is
|
|
528
|
+
* most-specific-first, so a literal segment wins over a `:param`.
|
|
529
|
+
*/
|
|
530
|
+
declare function createRouteMatcher<HandlerName>(bindings: readonly RouteBinding<HandlerName>[]): RouteMatcher<HandlerName>;
|
|
531
|
+
/**
|
|
532
|
+
* Match a `method` + `pathname` (no query string) against the Agent Protocol route table. This is
|
|
533
|
+
* what adapters that dispatch from a catch-all route (NestJS middleware, Next.js route handlers) use
|
|
534
|
+
* in place of a framework router; Express/Fastify bind `skeinRoutes` to their native router instead.
|
|
535
|
+
*/
|
|
536
|
+
declare const matchSkeinRoute: RouteMatcher;
|
|
537
|
+
|
|
538
|
+
/** The default path prefix the invoke endpoint mounts under. */
|
|
539
|
+
declare const DEFAULT_INVOKE_PREFIX = "/invoke";
|
|
540
|
+
/** Options for {@link createGraphInvokeHandler}. */
|
|
541
|
+
interface GraphInvokeOptions {
|
|
542
|
+
/**
|
|
543
|
+
* Stream modes used when the caller opts into SSE, overridable per request via `?stream_mode=`.
|
|
544
|
+
* Defaults to `"values"` — each frame is the full state after a step, ending at the same value the
|
|
545
|
+
* JSON response would have returned.
|
|
546
|
+
*/
|
|
547
|
+
streamMode?: StreamMode | StreamMode[];
|
|
548
|
+
}
|
|
549
|
+
/** The single handler name this surface binds — deliberately not a `keyof ProtocolHandlers`. */
|
|
550
|
+
type GraphInvokeHandlerName = "invokeGraph";
|
|
551
|
+
/**
|
|
552
|
+
* The route table for the invoke surface — one POST, shaped like {@link skeinRoutes} so catch-all
|
|
553
|
+
* adapters (NestJS, Next.js) can match it the same way and native routers can bind it directly.
|
|
554
|
+
* Parameterized with its own handler name, so nothing can mistake it for a protocol-table lookup key.
|
|
555
|
+
*/
|
|
556
|
+
declare function graphInvokeRoutes(prefix?: string): RouteBinding<GraphInvokeHandlerName>[];
|
|
500
557
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* most-specific-first, so a literal segment wins over a `:param`. This is what adapters that dispatch
|
|
504
|
-
* from a catch-all route (NestJS middleware, Next.js route handlers) use in place of a framework
|
|
505
|
-
* router; Express/Fastify bind `skeinRoutes` to their native router instead.
|
|
558
|
+
* Build the handler for `POST <prefix>/:graph_id`. Returns a {@link ProtocolHandler}, so every
|
|
559
|
+
* adapter serializes it through the same JSON/SSE path the protocol routes already use.
|
|
506
560
|
*/
|
|
507
|
-
declare function
|
|
561
|
+
declare function createGraphInvokeHandler(deps: ProtocolDeps, options?: GraphInvokeOptions): ProtocolHandler;
|
|
508
562
|
|
|
509
563
|
type LangGraphSearchItem = Item$1 & {
|
|
510
564
|
score?: number;
|
|
@@ -559,4 +613,4 @@ declare function toSseEvents(frames: AsyncIterable<RunFrame>, finalStatus: () =>
|
|
|
559
613
|
*/
|
|
560
614
|
declare function parseAfterSeq(lastEventId: string | undefined): number;
|
|
561
615
|
|
|
562
|
-
export { type AssistantService, type Clock, type CommandInput, type CompiledGraphFactory, type CreateAssistantInput, type CreateRunInput, type CreateThreadInput, type DeleteAssistantOptions, type DrawGraphOptions, type GraphResolver, type GraphSchemas, type HistoryOptions, type HttpMethod, type Logger, type PatchThreadInput, type ProtocolContext, type ProtocolDeps, type ProtocolHandler, type ProtocolHandlers, type ProtocolRequest, type ProtocolResponse, type ProtocolRuntime, type ProtocolRuntimeOptions, type ProtocolService, type ResolvedGraph, type RouteBinding, type RouteMatch, type RunService, type RunWorker, type RunWorkerOptions, SSE_HEADERS, SkeinBaseStore, type StartedStream, type StoreService, type SubgraphsOptions, type ThreadService, type ThreadStreamInput, type ThreadStreamService, buildProtocolService, copyThreadIdIntoBody, createContext, createProtocolHandlers, createProtocolRuntime, createProtocolService, createProtocolServiceFromContext, createRunWorker, encodeFrame, encodeTerminal, foldThreadId, matchSkeinRoute, parseAfterSeq, skeinRoutes, toSseEvents };
|
|
616
|
+
export { type AssistantService, type Clock, type CommandInput, type CompiledGraphFactory, type CreateAssistantInput, type CreateRunInput, type CreateThreadInput, DEFAULT_INVOKE_PREFIX, DEFAULT_RUN_CONCURRENCY, type DeleteAssistantOptions, type DrawGraphOptions, type GraphInvokeHandlerName, type GraphInvokeOptions, type GraphResolver, type GraphSchemas, type HistoryOptions, type HttpMethod, type Logger, type PatchThreadInput, type ProtocolContext, type ProtocolDeps, type ProtocolHandler, type ProtocolHandlers, type ProtocolRequest, type ProtocolResponse, type ProtocolRuntime, type ProtocolRuntimeOptions, type ProtocolService, type ResolvedGraph, type RouteBinding, type RouteMatch, type RouteMatcher, type RunService, type RunWorker, type RunWorkerOptions, SSE_HEADERS, SkeinBaseStore, type StartedStream, type StoreService, type SubgraphsOptions, type ThreadService, type ThreadStreamInput, type ThreadStreamService, buildProtocolService, copyThreadIdIntoBody, createContext, createGraphInvokeHandler, createProtocolHandlers, createProtocolRuntime, createProtocolService, createProtocolServiceFromContext, createRouteMatcher, createRunWorker, encodeFrame, encodeTerminal, foldThreadId, graphInvokeRoutes, matchSkeinRoute, parseAfterSeq, skeinRoutes, toSseEvents };
|
package/dist/index.js
CHANGED
|
@@ -758,50 +758,6 @@ import {
|
|
|
758
758
|
SkeinHttpError as SkeinHttpError4
|
|
759
759
|
} from "@skein-js/core";
|
|
760
760
|
|
|
761
|
-
// src/normalize-error.ts
|
|
762
|
-
import { isSkeinHttpError as isSkeinHttpError2, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
|
|
763
|
-
function serializeError(error) {
|
|
764
|
-
if (error instanceof Error) {
|
|
765
|
-
return { name: error.name, message: error.message };
|
|
766
|
-
}
|
|
767
|
-
return { name: "Error", message: String(error) };
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// src/sse/run-frame-stream.ts
|
|
771
|
-
var STREAM_MODES = [
|
|
772
|
-
"values",
|
|
773
|
-
"updates",
|
|
774
|
-
"messages",
|
|
775
|
-
"messages-tuple",
|
|
776
|
-
"custom",
|
|
777
|
-
"events",
|
|
778
|
-
"debug"
|
|
779
|
-
];
|
|
780
|
-
function isStreamMode(value) {
|
|
781
|
-
return typeof value === "string" && STREAM_MODES.includes(value);
|
|
782
|
-
}
|
|
783
|
-
function chunkToFrameBody(chunk) {
|
|
784
|
-
if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
|
|
785
|
-
return { event: chunk[0], data: chunk[1] };
|
|
786
|
-
}
|
|
787
|
-
return { event: "updates", data: chunk };
|
|
788
|
-
}
|
|
789
|
-
function toRunFrame(seq, body) {
|
|
790
|
-
return { seq, event: body.event, data: body.data };
|
|
791
|
-
}
|
|
792
|
-
function streamEventToFrameBody(event, runId, graphModes) {
|
|
793
|
-
if (event.tags?.includes("langsmith:hidden")) return null;
|
|
794
|
-
if (event.event === "on_chain_stream" && event.run_id === runId) {
|
|
795
|
-
const chunk = event.data?.chunk;
|
|
796
|
-
if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
|
|
797
|
-
const mode = chunk[0];
|
|
798
|
-
return graphModes.includes(mode) ? { event: mode, data: chunk[1] } : null;
|
|
799
|
-
}
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
|
-
return { event: "events", data: event };
|
|
803
|
-
}
|
|
804
|
-
|
|
805
761
|
// src/store/skein-base-store.ts
|
|
806
762
|
import {
|
|
807
763
|
BaseStore
|
|
@@ -878,6 +834,60 @@ var SkeinBaseStore = class extends BaseStore {
|
|
|
878
834
|
}
|
|
879
835
|
};
|
|
880
836
|
|
|
837
|
+
// src/graphs/resolve-compiled-graph.ts
|
|
838
|
+
async function resolveCompiledGraph(graphs, graphId, attachments) {
|
|
839
|
+
const resolved = await graphs.load(graphId);
|
|
840
|
+
const shared = typeof resolved === "function" ? await resolved({ configurable: attachments.configurable }) : resolved;
|
|
841
|
+
const graph = Object.create(shared);
|
|
842
|
+
graph.checkpointer = attachments.checkpointer;
|
|
843
|
+
graph.store = new SkeinBaseStore(attachments.store);
|
|
844
|
+
return graph;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/normalize-error.ts
|
|
848
|
+
import { isSkeinHttpError as isSkeinHttpError2, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
|
|
849
|
+
function serializeError(error) {
|
|
850
|
+
if (error instanceof Error) {
|
|
851
|
+
return { name: error.name, message: error.message };
|
|
852
|
+
}
|
|
853
|
+
return { name: "Error", message: String(error) };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/sse/run-frame-stream.ts
|
|
857
|
+
var STREAM_MODES = [
|
|
858
|
+
"values",
|
|
859
|
+
"updates",
|
|
860
|
+
"messages",
|
|
861
|
+
"messages-tuple",
|
|
862
|
+
"custom",
|
|
863
|
+
"events",
|
|
864
|
+
"debug"
|
|
865
|
+
];
|
|
866
|
+
function isStreamMode(value) {
|
|
867
|
+
return typeof value === "string" && STREAM_MODES.includes(value);
|
|
868
|
+
}
|
|
869
|
+
function chunkToFrameBody(chunk) {
|
|
870
|
+
if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
|
|
871
|
+
return { event: chunk[0], data: chunk[1] };
|
|
872
|
+
}
|
|
873
|
+
return { event: "updates", data: chunk };
|
|
874
|
+
}
|
|
875
|
+
function toRunFrame(seq, body) {
|
|
876
|
+
return { seq, event: body.event, data: body.data };
|
|
877
|
+
}
|
|
878
|
+
function streamEventToFrameBody(event, runId, graphModes) {
|
|
879
|
+
if (event.tags?.includes("langsmith:hidden")) return null;
|
|
880
|
+
if (event.event === "on_chain_stream" && event.run_id === runId) {
|
|
881
|
+
const chunk = event.data?.chunk;
|
|
882
|
+
if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
|
|
883
|
+
const mode = chunk[0];
|
|
884
|
+
return graphModes.includes(mode) ? { event: mode, data: chunk[1] } : null;
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
return { event: "events", data: event };
|
|
889
|
+
}
|
|
890
|
+
|
|
881
891
|
// src/runs/run-status.ts
|
|
882
892
|
import { isTerminalRunStatus } from "@skein-js/core";
|
|
883
893
|
function threadStatusForRun(status) {
|
|
@@ -1125,16 +1135,11 @@ function abortedStatus(reason) {
|
|
|
1125
1135
|
return "cancelled";
|
|
1126
1136
|
}
|
|
1127
1137
|
async function resolveGraph(deps, graphId, kwargs) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
await resolved({ configurable: toFactoryConfigurable(kwargs) })
|
|
1134
|
-
) : resolved;
|
|
1135
|
-
graph.checkpointer = deps.checkpointer;
|
|
1136
|
-
graph.store = new SkeinBaseStore(deps.store.store);
|
|
1137
|
-
return graph;
|
|
1138
|
+
return resolveCompiledGraph(deps.graphs, graphId, {
|
|
1139
|
+
configurable: toFactoryConfigurable(kwargs),
|
|
1140
|
+
checkpointer: deps.checkpointer,
|
|
1141
|
+
store: deps.store.store
|
|
1142
|
+
});
|
|
1138
1143
|
}
|
|
1139
1144
|
async function finalizeRun(deps, runId, status) {
|
|
1140
1145
|
const fresh = await deps.store.runs.get(runId);
|
|
@@ -1960,27 +1965,29 @@ function synthesizeRequest(req) {
|
|
|
1960
1965
|
});
|
|
1961
1966
|
}
|
|
1962
1967
|
|
|
1963
|
-
// src/auth/
|
|
1968
|
+
// src/auth/authenticate-request.ts
|
|
1964
1969
|
var STUDIO_USER = {
|
|
1965
1970
|
identity: "langgraph-studio-user",
|
|
1966
1971
|
display_name: "langgraph-studio-user",
|
|
1967
1972
|
is_authenticated: true,
|
|
1968
1973
|
permissions: []
|
|
1969
1974
|
};
|
|
1975
|
+
async function resolveAuthContext(engine, req) {
|
|
1976
|
+
if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
|
|
1977
|
+
return { user: STUDIO_USER, scopes: [] };
|
|
1978
|
+
}
|
|
1979
|
+
return engine.authenticate(synthesizeRequest(req));
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/auth/authorizing-handlers.ts
|
|
1970
1983
|
function createAuthorizingHandlers(context, engine) {
|
|
1971
1984
|
const baseHandlers = createProtocolHandlers(createProtocolServiceFromContext(context));
|
|
1972
1985
|
const names = Object.keys(ROUTE_AUTHZ);
|
|
1973
|
-
const resolveAuthContext = async (req) => {
|
|
1974
|
-
if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
|
|
1975
|
-
return { user: STUDIO_USER, scopes: [] };
|
|
1976
|
-
}
|
|
1977
|
-
return engine.authenticate(synthesizeRequest(req));
|
|
1978
|
-
};
|
|
1979
1986
|
const wrapped = {};
|
|
1980
1987
|
for (const name of names) {
|
|
1981
1988
|
const route = ROUTE_AUTHZ[name];
|
|
1982
1989
|
wrapped[name] = async (req) => {
|
|
1983
|
-
const authContext = await resolveAuthContext(req);
|
|
1990
|
+
const authContext = await resolveAuthContext(engine, req);
|
|
1984
1991
|
const { filters } = await engine.authorize({
|
|
1985
1992
|
resource: route.resource,
|
|
1986
1993
|
action: route.action,
|
|
@@ -2007,6 +2014,7 @@ function createAuthorizingHandlers(context, engine) {
|
|
|
2007
2014
|
import {
|
|
2008
2015
|
isTerminalRunStatus as isTerminalRunStatus6
|
|
2009
2016
|
} from "@skein-js/core";
|
|
2017
|
+
var DEFAULT_RUN_CONCURRENCY = 10;
|
|
2010
2018
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2011
2019
|
function logRunLifecycle(logger, run, status, startedAt, endedAt) {
|
|
2012
2020
|
logger.info(status === "success" ? "Background run succeeded" : `Background run ${status}`, {
|
|
@@ -2021,7 +2029,7 @@ function logRunLifecycle(logger, run, status, startedAt, endedAt) {
|
|
|
2021
2029
|
}
|
|
2022
2030
|
function createRunWorker(ctx, options = {}) {
|
|
2023
2031
|
const { deps, control } = ctx;
|
|
2024
|
-
const maxConcurrency = options.maxConcurrency ??
|
|
2032
|
+
const maxConcurrency = options.maxConcurrency ?? DEFAULT_RUN_CONCURRENCY;
|
|
2025
2033
|
const shutdownGraceMs = options.shutdownGraceMs ?? 5e3;
|
|
2026
2034
|
const inFlight = /* @__PURE__ */ new Set();
|
|
2027
2035
|
const process = async (queued) => {
|
|
@@ -2154,19 +2162,22 @@ function copyThreadIdIntoBody(request) {
|
|
|
2154
2162
|
return { ...request, body: { ...base, thread_id: threadId } };
|
|
2155
2163
|
}
|
|
2156
2164
|
var foldThreadId = copyThreadIdIntoBody;
|
|
2157
|
-
|
|
2158
|
-
binding
|
|
2159
|
-
|
|
2160
|
-
})
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2165
|
+
function createRouteMatcher(bindings) {
|
|
2166
|
+
const compiled = bindings.map((binding) => ({
|
|
2167
|
+
binding,
|
|
2168
|
+
regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
|
|
2169
|
+
}));
|
|
2170
|
+
return (method, pathname) => {
|
|
2171
|
+
const wanted = method.toLowerCase();
|
|
2172
|
+
for (const { binding, regex } of compiled) {
|
|
2173
|
+
if (binding.method !== wanted) continue;
|
|
2174
|
+
const match = regex.exec(pathname);
|
|
2175
|
+
if (match) return { binding, params: decodeParams(match.groups) };
|
|
2176
|
+
}
|
|
2177
|
+
return void 0;
|
|
2178
|
+
};
|
|
2169
2179
|
}
|
|
2180
|
+
var matchSkeinRoute = createRouteMatcher(skeinRoutes);
|
|
2170
2181
|
function decodeParams(groups) {
|
|
2171
2182
|
const params = {};
|
|
2172
2183
|
for (const [key, value] of Object.entries(groups ?? {})) {
|
|
@@ -2178,20 +2189,150 @@ function decodeParams(groups) {
|
|
|
2178
2189
|
}
|
|
2179
2190
|
return params;
|
|
2180
2191
|
}
|
|
2192
|
+
|
|
2193
|
+
// src/invoke/graph-invoke.ts
|
|
2194
|
+
import { randomUUID } from "crypto";
|
|
2195
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
2196
|
+
import {
|
|
2197
|
+
SkeinHttpError as SkeinHttpError10
|
|
2198
|
+
} from "@skein-js/core";
|
|
2199
|
+
import { z as z2 } from "zod";
|
|
2200
|
+
var DEFAULT_INVOKE_PREFIX = "/invoke";
|
|
2201
|
+
function graphInvokeRoutes(prefix = DEFAULT_INVOKE_PREFIX) {
|
|
2202
|
+
return [{ method: "post", path: `${normalizePrefix(prefix)}/:graph_id`, handler: "invokeGraph" }];
|
|
2203
|
+
}
|
|
2204
|
+
function normalizePrefix(prefix) {
|
|
2205
|
+
const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2206
|
+
return trimmed === "/" ? "" : trimmed;
|
|
2207
|
+
}
|
|
2208
|
+
function wantsEventStream(req) {
|
|
2209
|
+
return req.headers["accept"]?.includes("text/event-stream") ?? false;
|
|
2210
|
+
}
|
|
2211
|
+
function createRunSignal(timeoutMs, callerSignal) {
|
|
2212
|
+
if (timeoutMs === void 0 && !callerSignal) return { signal: void 0, dispose: () => {
|
|
2213
|
+
} };
|
|
2214
|
+
const controller = new AbortController();
|
|
2215
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new Error(`Run exceeded ${timeoutMs}ms`)), timeoutMs);
|
|
2216
|
+
const onCallerAbort = () => controller.abort(callerSignal?.reason);
|
|
2217
|
+
if (callerSignal?.aborted) onCallerAbort();
|
|
2218
|
+
else callerSignal?.addEventListener("abort", onCallerAbort);
|
|
2219
|
+
return {
|
|
2220
|
+
signal: controller.signal,
|
|
2221
|
+
dispose: () => {
|
|
2222
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2223
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
var INVOKE_STREAM_MODES = [
|
|
2228
|
+
"values",
|
|
2229
|
+
"updates",
|
|
2230
|
+
"messages",
|
|
2231
|
+
"messages-tuple",
|
|
2232
|
+
"custom",
|
|
2233
|
+
"debug"
|
|
2234
|
+
];
|
|
2235
|
+
var streamModeQuerySchema = z2.array(z2.enum(INVOKE_STREAM_MODES)).min(1, "expected at least one stream mode");
|
|
2236
|
+
function streamModeFromQuery(value) {
|
|
2237
|
+
const raw = Array.isArray(value) ? value : value === void 0 ? [] : value.split(",");
|
|
2238
|
+
const modes = raw.map((mode) => mode.trim()).filter((mode) => mode.length > 0);
|
|
2239
|
+
if (modes.length === 0) return void 0;
|
|
2240
|
+
if (modes.includes("events")) {
|
|
2241
|
+
throw SkeinHttpError10.badRequest(
|
|
2242
|
+
'Stream mode "events" is not supported on the invoke surface; use the Agent Protocol run endpoints for token-level events.'
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
return parse(streamModeQuerySchema, modes, "stream_mode query parameter");
|
|
2246
|
+
}
|
|
2247
|
+
function createGraphInvokeHandler(deps, options = {}) {
|
|
2248
|
+
return async (req) => {
|
|
2249
|
+
const graphId = requireParam(req.params, "graph_id");
|
|
2250
|
+
let authContext;
|
|
2251
|
+
if (deps.auth) {
|
|
2252
|
+
authContext = await resolveAuthContext(deps.auth, req);
|
|
2253
|
+
await deps.auth.authorize({
|
|
2254
|
+
resource: "threads",
|
|
2255
|
+
action: "create_run",
|
|
2256
|
+
// `authValue` spreads the body last, and on this surface the body is arbitrary caller-supplied
|
|
2257
|
+
// graph input — so a body key named `graph_id` would shadow the path param and let a policy
|
|
2258
|
+
// authorize a different graph than the one that actually runs. Re-stamp the server-derived id
|
|
2259
|
+
// last so the value a policy judges is always the graph we execute.
|
|
2260
|
+
value: { ...authValue(req), graph_id: graphId },
|
|
2261
|
+
context: authContext
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
if (!deps.graphs.ids.includes(graphId)) {
|
|
2265
|
+
throw SkeinHttpError10.notFound(`Graph "${graphId}" is not registered.`, {
|
|
2266
|
+
code: "graph_not_found"
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
const factoryConfigurable2 = withAuthUser({}, authContext?.user, authContext?.scopes);
|
|
2270
|
+
const graph = await resolveCompiledGraph(deps.graphs, graphId, {
|
|
2271
|
+
configurable: Object.keys(factoryConfigurable2).length > 0 ? factoryConfigurable2 : void 0,
|
|
2272
|
+
checkpointer: new MemorySaver(),
|
|
2273
|
+
store: deps.store.store
|
|
2274
|
+
});
|
|
2275
|
+
const configurable = withAuthUser(
|
|
2276
|
+
{ thread_id: randomUUID() },
|
|
2277
|
+
authContext?.user,
|
|
2278
|
+
authContext?.scopes
|
|
2279
|
+
);
|
|
2280
|
+
const input = req.body ?? {};
|
|
2281
|
+
const { signal, dispose } = createRunSignal(deps.runTimeoutMs, req.signal);
|
|
2282
|
+
if (!wantsEventStream(req)) {
|
|
2283
|
+
try {
|
|
2284
|
+
const output = await graph.invoke(input, { configurable, signal });
|
|
2285
|
+
return { kind: "json", status: 200, body: output };
|
|
2286
|
+
} finally {
|
|
2287
|
+
dispose();
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const streamMode = toGraphStreamModes(
|
|
2291
|
+
streamModeFromQuery(req.query["stream_mode"]) ?? options.streamMode ?? "values"
|
|
2292
|
+
);
|
|
2293
|
+
let status = "success";
|
|
2294
|
+
const frames = async function* () {
|
|
2295
|
+
let seq = 0;
|
|
2296
|
+
try {
|
|
2297
|
+
const stream = await graph.stream(input, {
|
|
2298
|
+
configurable,
|
|
2299
|
+
streamMode,
|
|
2300
|
+
signal
|
|
2301
|
+
});
|
|
2302
|
+
for await (const chunk of stream) {
|
|
2303
|
+
seq += 1;
|
|
2304
|
+
yield toRunFrame(seq, chunkToFrameBody(chunk));
|
|
2305
|
+
}
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
status = "error";
|
|
2308
|
+
seq += 1;
|
|
2309
|
+
yield { seq, event: "error", data: serializeError(error) };
|
|
2310
|
+
} finally {
|
|
2311
|
+
dispose();
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
return { kind: "sse", status: 200, events: toSseEvents(frames(), async () => status) };
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2181
2317
|
export {
|
|
2318
|
+
DEFAULT_INVOKE_PREFIX,
|
|
2319
|
+
DEFAULT_RUN_CONCURRENCY,
|
|
2182
2320
|
SSE_HEADERS,
|
|
2183
2321
|
SkeinBaseStore,
|
|
2184
2322
|
buildProtocolService,
|
|
2185
2323
|
copyThreadIdIntoBody,
|
|
2186
2324
|
createContext,
|
|
2325
|
+
createGraphInvokeHandler,
|
|
2187
2326
|
createProtocolHandlers,
|
|
2188
2327
|
createProtocolRuntime,
|
|
2189
2328
|
createProtocolService,
|
|
2190
2329
|
createProtocolServiceFromContext,
|
|
2330
|
+
createRouteMatcher,
|
|
2191
2331
|
createRunWorker,
|
|
2192
2332
|
encodeFrame,
|
|
2193
2333
|
encodeTerminal,
|
|
2194
2334
|
foldThreadId,
|
|
2335
|
+
graphInvokeRoutes,
|
|
2195
2336
|
matchSkeinRoute,
|
|
2196
2337
|
parseAfterSeq,
|
|
2197
2338
|
skeinRoutes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skein-js/agent-protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Framework-agnostic Agent Protocol engine for LangGraph.js — run engine, handlers, and SSE, driven entirely by injected dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Maina Wycliffe <wmmaina7@gmail.com>",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"zod": "^3.25.76",
|
|
47
|
-
"@skein-js/core": "0.
|
|
47
|
+
"@skein-js/core": "0.9.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@langchain/langgraph": "^1.4.0",
|
|
51
51
|
"@langchain/langgraph-sdk": "^1.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@skein-js/storage-memory": "0.
|
|
54
|
+
"@skein-js/storage-memory": "0.9.1"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|