@skein-js/agent-protocol 0.2.1 → 0.4.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GraphSchema, SkeinStore, RunQueue, RunEventBus, AuthEngine, Assistant, Config, StreamMode, Metadata, MultitaskStrategy, RunFrame, DefaultValues, Run, RunStatus, Item, StoreSearchQuery, SearchItem, Thread, ThreadState, StoreRepo } from '@skein-js/core';
1
+ import { GraphSchema, SkeinStore, RunQueue, RunEventBus, AuthEngine, Assistant, Config, StreamMode, Metadata, MultitaskStrategy, RunFrame, DefaultValues, Run, RunStatus, StorePutOptions, Item, StoreSearchQuery, SearchItem, Thread, ThreadSearchQuery, ThreadState, StoreRepo } from '@skein-js/core';
2
2
  import { CompiledGraph, BaseCheckpointSaver, BaseStore, Item as Item$1, Operation, OperationResults } from '@langchain/langgraph';
3
3
 
4
4
  /** A factory export: called (optionally with per-run config) to produce a compiled graph. */
@@ -159,7 +159,7 @@ interface RunService {
159
159
  }
160
160
 
161
161
  interface StoreService {
162
- put(namespace: string[], key: string, value: Record<string, unknown>): Promise<Item>;
162
+ put(namespace: string[], key: string, value: Record<string, unknown>, options?: StorePutOptions): Promise<Item>;
163
163
  get(namespace: string[], key: string): Promise<Item>;
164
164
  delete(namespace: string[], key: string): Promise<void>;
165
165
  search(query: StoreSearchQuery): Promise<SearchItem[]>;
@@ -180,7 +180,11 @@ interface ThreadService {
180
180
  create(input?: CreateThreadInput): Promise<Thread>;
181
181
  get(threadId: string): Promise<Thread>;
182
182
  list(): Promise<Thread[]>;
183
+ /** Filtered + paginated listing — `POST /threads/search`. */
184
+ search(query: ThreadSearchQuery): Promise<Thread[]>;
183
185
  patch(threadId: string, patch: PatchThreadInput): Promise<Thread>;
186
+ /** Duplicate a thread (new id) together with its full checkpoint history — `POST /threads/{id}/copy`. */
187
+ copy(threadId: string): Promise<Thread>;
184
188
  delete(threadId: string): Promise<void>;
185
189
  history(threadId: string, options?: HistoryOptions): Promise<ThreadState[]>;
186
190
  /** The thread's current state snapshot — `GET /threads/{id}/state`, what `useStream` hydrates from. */
@@ -262,6 +266,7 @@ interface ProtocolHandlers {
262
266
  createThread: ProtocolHandler;
263
267
  getThread: ProtocolHandler;
264
268
  listThreads: ProtocolHandler;
269
+ copyThread: ProtocolHandler;
265
270
  patchThread: ProtocolHandler;
266
271
  deleteThread: ProtocolHandler;
267
272
  getThreadHistory: ProtocolHandler;
package/dist/index.js CHANGED
@@ -101,6 +101,16 @@ var threadCreateSchema = z.object({
101
101
  var threadPatchSchema = z.object({
102
102
  metadata: z.record(z.unknown()).optional()
103
103
  }).passthrough();
104
+ var threadSearchSchema = z.object({
105
+ metadata: z.record(z.unknown()).optional(),
106
+ values: z.record(z.unknown()).optional(),
107
+ status: z.enum(["idle", "busy", "interrupted", "error"]).optional(),
108
+ ids: z.array(z.string()).optional(),
109
+ limit: z.number().int().positive().optional(),
110
+ offset: z.number().int().nonnegative().optional(),
111
+ sort_by: z.enum(["thread_id", "status", "created_at", "updated_at"]).optional(),
112
+ sort_order: z.enum(["asc", "desc"]).optional()
113
+ }).passthrough();
104
114
  var assistantSearchSchema = z.object({
105
115
  graph_id: z.string().optional(),
106
116
  limit: z.number().int().positive().optional(),
@@ -109,7 +119,9 @@ var assistantSearchSchema = z.object({
109
119
  var storePutSchema = z.object({
110
120
  namespace: z.array(z.string()).min(1),
111
121
  key: z.string().min(1),
112
- value: z.record(z.unknown())
122
+ value: z.record(z.unknown()),
123
+ /** Optional item lifetime in minutes; overrides the configured `store.ttl.default_ttl`. */
124
+ ttl: z.number().positive().optional()
113
125
  }).passthrough();
114
126
  var storeSearchSchema = z.object({
115
127
  namespace_prefix: z.array(z.string()).optional(),
@@ -153,7 +165,22 @@ function createProtocolHandlers(service) {
153
165
  // --- threads ------------------------------------------------------------------------------
154
166
  createThread: async (req) => json(await service.threads.create(parse(threadCreateSchema, req.body ?? {}))),
155
167
  getThread: async (req) => json(await service.threads.get(requireParam(req.params, "thread_id"))),
156
- listThreads: async () => json(await service.threads.list()),
168
+ listThreads: async (req) => {
169
+ const body = parse(threadSearchSchema, req.body ?? {});
170
+ return json(
171
+ await service.threads.search({
172
+ metadata: body.metadata,
173
+ values: body.values,
174
+ status: body.status,
175
+ ids: body.ids,
176
+ limit: body.limit,
177
+ offset: body.offset,
178
+ sortBy: body.sort_by,
179
+ sortOrder: body.sort_order
180
+ })
181
+ );
182
+ },
183
+ copyThread: async (req) => json(await service.threads.copy(requireParam(req.params, "thread_id"))),
157
184
  patchThread: async (req) => json(
158
185
  await service.threads.patch(
159
186
  requireParam(req.params, "thread_id"),
@@ -221,7 +248,7 @@ function createProtocolHandlers(service) {
221
248
  // --- store --------------------------------------------------------------------------------
222
249
  putStoreItem: async (req) => {
223
250
  const body = parse(storePutSchema, req.body);
224
- return json(await service.store.put(body.namespace, body.key, body.value));
251
+ return json(await service.store.put(body.namespace, body.key, body.value, { ttl: body.ttl }));
225
252
  },
226
253
  getStoreItem: async (req) => {
227
254
  const namespace = namespaceFromQuery(req.query["namespace"]);
@@ -496,9 +523,11 @@ function threadStatusForRun(status) {
496
523
  case "error":
497
524
  case "timeout":
498
525
  return "error";
499
- // pending (not yet started) and success both leave the thread idle.
526
+ // pending (not yet started), success, and cancelled all leave the thread idle. A cancellation
527
+ // is a clean stop, not a failure, so the thread returns to idle rather than error.
500
528
  case "pending":
501
529
  case "success":
530
+ case "cancelled":
502
531
  return "idle";
503
532
  default:
504
533
  return "idle";
@@ -775,9 +804,9 @@ async function executeRun(deps, exec) {
775
804
  }
776
805
  if (control.signal.aborted) {
777
806
  const timedOut = control.reason.current === "timeout";
778
- const finalStatus2 = await finalizeRun(deps, runId, timedOut ? "timeout" : "error");
807
+ const finalStatus2 = await finalizeRun(deps, runId, timedOut ? "timeout" : "cancelled");
779
808
  if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
780
- else if (finalStatus2 === "error") await mirrorThreadStatus(deps, threadId, "idle");
809
+ else if (finalStatus2 === "cancelled") await mirrorThreadStatus(deps, threadId, "idle");
781
810
  logFinished(finalStatus2);
782
811
  return { status: finalStatus2, values: {} };
783
812
  }
@@ -806,8 +835,8 @@ async function executeRun(deps, exec) {
806
835
  return { status: finalStatus2, values: {} };
807
836
  }
808
837
  if (reason === "cancel" || control.signal.aborted) {
809
- const finalStatus2 = await finalizeRun(deps, runId, "error");
810
- if (finalStatus2 === "error") await mirrorThreadStatus(deps, threadId, "idle");
838
+ const finalStatus2 = await finalizeRun(deps, runId, "cancelled");
839
+ if (finalStatus2 === "cancelled") await mirrorThreadStatus(deps, threadId, "idle");
811
840
  logFinished(finalStatus2);
812
841
  return { status: finalStatus2, values: {} };
813
842
  }
@@ -840,19 +869,35 @@ function toKwargs(input) {
840
869
  function createRunService(ctx) {
841
870
  const { deps, control, locks } = ctx;
842
871
  const requireThread = async (threadId) => {
843
- if (!await deps.store.threads.get(threadId)) {
844
- throw SkeinHttpError5.notFound(`Thread "${threadId}" not found.`);
845
- }
872
+ const thread = await deps.store.threads.get(threadId);
873
+ if (!thread) throw SkeinHttpError5.notFound(`Thread "${threadId}" not found.`);
874
+ return thread;
846
875
  };
847
876
  const requireAssistant = async (assistantId) => {
848
877
  const assistant = await deps.store.assistants.get(assistantId);
849
878
  if (!assistant) throw SkeinHttpError5.notFound(`Assistant "${assistantId}" not found.`);
850
- return assistant.assistant_id;
879
+ return assistant;
880
+ };
881
+ const stampGraphOnThread = async (thread, assistant) => {
882
+ if (thread.metadata?.["graph_id"] === assistant.graph_id && thread.metadata?.["assistant_id"] === assistant.assistant_id) {
883
+ return;
884
+ }
885
+ try {
886
+ await deps.store.threads.update(thread.thread_id, {
887
+ metadata: {
888
+ ...thread.metadata,
889
+ graph_id: assistant.graph_id,
890
+ assistant_id: assistant.assistant_id
891
+ }
892
+ });
893
+ } catch (error) {
894
+ deps.logger.warn("failed to stamp graph_id/assistant_id onto thread", error);
895
+ }
851
896
  };
852
897
  const ensureThread = async (threadId) => {
853
- if (threadId === void 0) return (await deps.store.threads.create()).thread_id;
898
+ if (threadId === void 0) return deps.store.threads.create();
854
899
  const existing = await deps.store.threads.get(threadId);
855
- return (existing ?? await deps.store.threads.create({ thread_id: threadId })).thread_id;
900
+ return existing ?? await deps.store.threads.create({ thread_id: threadId });
856
901
  };
857
902
  const createPendingRun = async (input, threadId, assistantId, kwargs) => {
858
903
  return locks.run(threadId, async () => {
@@ -884,33 +929,36 @@ function createRunService(ctx) {
884
929
  };
885
930
  return {
886
931
  async createWait(input) {
887
- const assistantId = await requireAssistant(input.assistant_id);
888
- const threadId = await ensureThread(input.thread_id);
932
+ const assistant = await requireAssistant(input.assistant_id);
933
+ const thread = await ensureThread(input.thread_id);
889
934
  const kwargs = toKwargs(input);
890
- const run = await createPendingRun(input, threadId, assistantId, kwargs);
935
+ const run = await createPendingRun(input, thread.thread_id, assistant.assistant_id, kwargs);
936
+ await stampGraphOnThread(thread, assistant);
891
937
  const outcome = await runInline(run, kwargs);
892
938
  return outcome.values;
893
939
  },
894
940
  async createStream(input) {
895
- const assistantId = await requireAssistant(input.assistant_id);
896
- const threadId = await ensureThread(input.thread_id);
941
+ const assistant = await requireAssistant(input.assistant_id);
942
+ const thread = await ensureThread(input.thread_id);
897
943
  const kwargs = toKwargs(input);
898
- const run = await createPendingRun(input, threadId, assistantId, kwargs);
944
+ const run = await createPendingRun(input, thread.thread_id, assistant.assistant_id, kwargs);
945
+ await stampGraphOnThread(thread, assistant);
899
946
  void runInline(run, kwargs).catch(
900
947
  (error) => deps.logger.error("stream run failed", error)
901
948
  );
902
949
  return { runId: run.run_id, frames: deps.bus.subscribe(run.run_id, 0) };
903
950
  },
904
951
  async createBackground(threadId, input) {
905
- const assistantId = await requireAssistant(input.assistant_id);
906
- await requireThread(threadId);
952
+ const assistant = await requireAssistant(input.assistant_id);
953
+ const thread = await requireThread(threadId);
907
954
  const kwargs = toKwargs(input);
908
955
  const run = await createPendingRun(
909
956
  { ...input, thread_id: threadId },
910
957
  threadId,
911
- assistantId,
958
+ assistant.assistant_id,
912
959
  kwargs
913
960
  );
961
+ await stampGraphOnThread(thread, assistant);
914
962
  await deps.queue.enqueue({ run_id: run.run_id, thread_id: threadId });
915
963
  return run;
916
964
  },
@@ -928,12 +976,12 @@ function createRunService(ctx) {
928
976
  if (!run) throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
929
977
  if (isTerminalRunStatus3(run.status)) return run;
930
978
  if (run.status === "pending") {
931
- await deps.store.runs.setStatus(runId, "error");
979
+ await deps.store.runs.setStatus(runId, "cancelled");
932
980
  await deps.store.threads.update(run.thread_id, { status: "idle" });
933
981
  await deps.bus.close(runId);
934
982
  control.abort(runId, "cancel");
935
983
  } else {
936
- await deps.store.runs.setStatus(runId, "error");
984
+ await deps.store.runs.setStatus(runId, "cancelled");
937
985
  await deps.store.threads.update(run.thread_id, { status: "idle" });
938
986
  control.abort(runId, "cancel");
939
987
  }
@@ -960,10 +1008,12 @@ function createRunService(ctx) {
960
1008
  }
961
1009
 
962
1010
  // src/store/store-service.ts
963
- import { SkeinHttpError as SkeinHttpError6 } from "@skein-js/core";
1011
+ import {
1012
+ SkeinHttpError as SkeinHttpError6
1013
+ } from "@skein-js/core";
964
1014
  function createStoreService(deps) {
965
1015
  return {
966
- put: (namespace, key, value) => deps.store.store.put(namespace, key, value),
1016
+ put: (namespace, key, value, options) => deps.store.store.put(namespace, key, value, options),
967
1017
  async get(namespace, key) {
968
1018
  const item = await deps.store.store.get(namespace, key);
969
1019
  if (!item) {
@@ -980,10 +1030,50 @@ function createStoreService(deps) {
980
1030
  }
981
1031
 
982
1032
  // src/threads/thread-service.ts
1033
+ import {
1034
+ copyCheckpoint
1035
+ } from "@langchain/langgraph";
983
1036
  import {
984
1037
  isTerminalRunStatus as isTerminalRunStatus4,
985
1038
  SkeinHttpError as SkeinHttpError7
986
1039
  } from "@skein-js/core";
1040
+ async function copyCheckpointHistory(checkpointer, sourceId, targetId) {
1041
+ const tuples = [];
1042
+ for await (const tuple of checkpointer.list({ configurable: { thread_id: sourceId } })) {
1043
+ tuples.push(tuple);
1044
+ }
1045
+ for (const tuple of tuples.reverse()) {
1046
+ const ns = tuple.config.configurable?.checkpoint_ns ?? "";
1047
+ const parentId = tuple.parentConfig?.configurable?.checkpoint_id;
1048
+ const putConfig = {
1049
+ configurable: { thread_id: targetId, checkpoint_ns: ns, checkpoint_id: parentId }
1050
+ };
1051
+ await checkpointer.put(
1052
+ putConfig,
1053
+ copyCheckpoint(tuple.checkpoint),
1054
+ tuple.metadata ?? {},
1055
+ tuple.checkpoint.channel_versions
1056
+ );
1057
+ if (tuple.pendingWrites && tuple.pendingWrites.length > 0) {
1058
+ const writeConfig = {
1059
+ configurable: {
1060
+ thread_id: targetId,
1061
+ checkpoint_ns: ns,
1062
+ checkpoint_id: tuple.checkpoint.id
1063
+ }
1064
+ };
1065
+ const byTask = /* @__PURE__ */ new Map();
1066
+ for (const [taskId, channel, value] of tuple.pendingWrites) {
1067
+ const writes = byTask.get(taskId) ?? [];
1068
+ writes.push([channel, value]);
1069
+ byTask.set(taskId, writes);
1070
+ }
1071
+ for (const [taskId, writes] of byTask) {
1072
+ await checkpointer.putWrites(writeConfig, writes, taskId);
1073
+ }
1074
+ }
1075
+ }
1076
+ }
987
1077
  function emptyThreadState(threadId) {
988
1078
  return {
989
1079
  values: {},
@@ -1037,10 +1127,33 @@ function createThreadService(ctx) {
1037
1127
  create: (input) => deps.store.threads.create(input),
1038
1128
  get: requireThread,
1039
1129
  list: () => deps.store.threads.list(),
1130
+ search: (query) => deps.store.threads.search(query),
1040
1131
  async patch(threadId, patch) {
1041
1132
  await requireThread(threadId);
1042
1133
  return deps.store.threads.update(threadId, { metadata: patch.metadata });
1043
1134
  },
1135
+ async copy(threadId) {
1136
+ await requireThread(threadId);
1137
+ const copy = await deps.store.threads.copy(threadId);
1138
+ await copyCheckpointHistory(deps.checkpointer, threadId, copy.thread_id);
1139
+ const sourceRuns = await deps.store.runs.listByThread(threadId);
1140
+ for (const run of [...sourceRuns].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
1141
+ if (!isTerminalRunStatus4(run.status)) continue;
1142
+ const kwargs = await deps.store.runs.getKwargs(run.run_id);
1143
+ await deps.store.runs.create({
1144
+ thread_id: copy.thread_id,
1145
+ assistant_id: run.assistant_id,
1146
+ status: run.status,
1147
+ metadata: run.metadata,
1148
+ multitask_strategy: run.multitask_strategy,
1149
+ ...kwargs ? { kwargs } : {}
1150
+ });
1151
+ }
1152
+ if (copy.status === "busy") {
1153
+ return deps.store.threads.update(copy.thread_id, { status: "idle" });
1154
+ }
1155
+ return copy;
1156
+ },
1044
1157
  async delete(threadId) {
1045
1158
  await requireThread(threadId);
1046
1159
  const runs = await deps.store.runs.listByThread(threadId);
@@ -1162,6 +1275,14 @@ function createAuthScopedStore(inner, engine, filters, resource) {
1162
1275
  if (resource === "threads") {
1163
1276
  const threads = {
1164
1277
  list: async () => (await inner.threads.list()).filter((thread) => matches(thread.metadata)),
1278
+ search: async (query) => {
1279
+ const { limit, offset, ...filters2 } = query;
1280
+ const owned = (await inner.threads.search(filters2)).filter(
1281
+ (thread) => matches(thread.metadata)
1282
+ );
1283
+ const start = offset ?? 0;
1284
+ return owned.slice(start, limit === void 0 ? void 0 : start + limit);
1285
+ },
1165
1286
  get: async (threadId) => {
1166
1287
  const thread = await inner.threads.get(threadId);
1167
1288
  return thread && matches(thread.metadata) ? thread : null;
@@ -1183,6 +1304,13 @@ function createAuthScopedStore(inner, engine, filters, resource) {
1183
1304
  const next = patch.metadata !== void 0 ? { ...patch, metadata: stamp(patch.metadata) } : patch;
1184
1305
  return inner.threads.update(threadId, next);
1185
1306
  },
1307
+ copy: async (threadId) => {
1308
+ const thread = await inner.threads.get(threadId);
1309
+ if (!thread || !matches(thread.metadata)) {
1310
+ throw SkeinHttpError9.notFound(`Thread "${threadId}" not found.`);
1311
+ }
1312
+ return inner.threads.copy(threadId);
1313
+ },
1186
1314
  delete: async (threadId) => {
1187
1315
  const thread = await inner.threads.get(threadId);
1188
1316
  if (!thread || !matches(thread.metadata)) {
@@ -1227,6 +1355,7 @@ var ROUTE_AUTHZ = {
1227
1355
  searchAssistants: { resource: "assistants", action: "search" },
1228
1356
  // threads
1229
1357
  createThread: { resource: "threads", action: "create" },
1358
+ copyThread: { resource: "threads", action: "create" },
1230
1359
  getThread: { resource: "threads", action: "read" },
1231
1360
  getThreadState: { resource: "threads", action: "read" },
1232
1361
  getThreadHistory: { resource: "threads", action: "read" },
@@ -1312,8 +1441,21 @@ function createAuthorizingHandlers(context, engine) {
1312
1441
  }
1313
1442
 
1314
1443
  // src/runs/run-worker.ts
1315
- import { isTerminalRunStatus as isTerminalRunStatus6 } from "@skein-js/core";
1444
+ import {
1445
+ isTerminalRunStatus as isTerminalRunStatus6
1446
+ } from "@skein-js/core";
1316
1447
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1448
+ function logRunLifecycle(logger, run, status, startedAt, endedAt) {
1449
+ logger.info(status === "success" ? "Background run succeeded" : `Background run ${status}`, {
1450
+ run_id: run.run_id,
1451
+ run_attempt: 1,
1452
+ run_created_at: run.created_at,
1453
+ run_started_at: new Date(startedAt).toISOString(),
1454
+ run_ended_at: new Date(endedAt).toISOString(),
1455
+ run_exec_ms: endedAt - startedAt,
1456
+ run_queue_ms: startedAt - Date.parse(run.created_at)
1457
+ });
1458
+ }
1317
1459
  function createRunWorker(ctx, options = {}) {
1318
1460
  const { deps, control } = ctx;
1319
1461
  const maxConcurrency = options.maxConcurrency ?? 1;
@@ -1325,9 +1467,12 @@ function createRunWorker(ctx, options = {}) {
1325
1467
  const kwargs = await deps.store.runs.getKwargs(queued.run_id) ?? {};
1326
1468
  const runControl = control.register(queued.run_id);
1327
1469
  inFlight.add(queued.run_id);
1470
+ const startedAt = Date.now();
1471
+ let status = "error";
1328
1472
  try {
1329
- await executeRun(deps, { run, kwargs, control: runControl });
1473
+ status = (await executeRun(deps, { run, kwargs, control: runControl })).status;
1330
1474
  } finally {
1475
+ logRunLifecycle(deps.logger, run, status, startedAt, Date.now());
1331
1476
  control.clear(queued.run_id);
1332
1477
  inFlight.delete(queued.run_id);
1333
1478
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/agent-protocol",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
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.2.1"
47
+ "@skein-js/core": "0.4.0"
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.2.1"
54
+ "@skein-js/storage-memory": "0.4.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"