@harness-gw/sdk 0.6.0 → 0.6.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 CHANGED
@@ -63,7 +63,7 @@ You need a running harness. Hermes is the primary one; see
63
63
  docker run --rm \
64
64
  -v "$PWD/config.yaml:/run/harness-gw/config.yaml:ro" \
65
65
  -v "$PWD/hermes-token:/run/secrets/hermes-token:ro" \
66
- ghcr.io/almogbaku/harness-gw:0.6.0 \
66
+ ghcr.io/almogbaku/harness-gw:0.6.1 \
67
67
  config check --config /run/harness-gw/config.yaml
68
68
  ```
69
69
 
@@ -73,7 +73,7 @@ You need a running harness. Hermes is the primary one; see
73
73
  docker run --rm -p 127.0.0.1:4100:4100 \
74
74
  -v "$PWD/config.yaml:/run/harness-gw/config.yaml:ro" \
75
75
  -v "$PWD/hermes-token:/run/secrets/hermes-token:ro" \
76
- ghcr.io/almogbaku/harness-gw:0.6.0 \
76
+ ghcr.io/almogbaku/harness-gw:0.6.1 \
77
77
  serve --config /run/harness-gw/config.yaml
78
78
  ```
79
79
 
@@ -278,6 +278,13 @@ class AgentUpdateError extends Error {
278
278
  }
279
279
  }
280
280
 
281
+ class AcpConnectionClosedError extends Error {
282
+ constructor(message = "The ACP connection closed", options) {
283
+ super(message, options);
284
+ this.name = "AcpConnectionClosedError";
285
+ }
286
+ }
287
+
281
288
  // client/limits.ts
282
289
  var HANDSHAKE_DEADLINE_MS = 1e4;
283
290
  var REQUEST_DEADLINE_MS = {
@@ -436,9 +443,6 @@ var SESSION_STATES = new Map([
436
443
  ["refused", "unavailable"],
437
444
  ["gone", "gone"]
438
445
  ]);
439
- function isTimeout(error) {
440
- return error instanceof DOMException && error.name === "TimeoutError";
441
- }
442
446
  function agentUpdateError(error) {
443
447
  if (codeOf(error) === HGW_JSONRPC_ERRORS2.unsupported)
444
448
  return new AgentUpdateError("unsupported", "This runtime cannot store that Agent field");
@@ -482,6 +486,9 @@ function trackedInbound(stream, onReceived) {
482
486
  writable: stream.writable
483
487
  };
484
488
  }
489
+ function closedUnder({ connection }, error) {
490
+ return connection.signal.aborted && error === connection.signal.reason;
491
+ }
485
492
  function observedSocket(Socket, onOpen, onClose) {
486
493
  return class extends Socket {
487
494
  constructor(...args) {
@@ -705,6 +712,7 @@ function createAcpConnection(options) {
705
712
  let live;
706
713
  let recovering = false;
707
714
  let joinable = false;
715
+ let recovery;
708
716
  let invitation;
709
717
  let lastFocus;
710
718
  let silenceTimer = undefined;
@@ -817,18 +825,48 @@ function createAcpConnection(options) {
817
825
  return;
818
826
  sessions.get(notice.sessionId)?.owner?.actor.send({ type: "gone" });
819
827
  }
820
- async function readyTransport() {
828
+ async function liveTransport() {
821
829
  start();
822
830
  if (inspecting)
823
831
  await inspecting;
824
- const current = live;
825
- if (ended || !current)
826
- throw new Error("The ACP connection is not open");
832
+ if (ended || !live)
833
+ throw new AcpConnectionClosedError("The ACP connection is not open");
834
+ return live;
835
+ }
836
+ async function readyTransport() {
837
+ const current = await liveTransport();
827
838
  await current.ready;
828
839
  return current;
829
840
  }
830
- async function request(tier, send) {
831
- return requestOn(await readyTransport(), tier, send);
841
+ function recovered() {
842
+ start();
843
+ if (ended)
844
+ return Promise.reject(new AcpConnectionClosedError);
845
+ if (joinable)
846
+ return Promise.resolve();
847
+ recovery ??= Promise.withResolvers();
848
+ return recovery.promise;
849
+ }
850
+ async function request(tier, send, resend) {
851
+ const sessionId = resend === "read" ? undefined : resend?.sessionId;
852
+ for (;; ) {
853
+ if (sessionId !== undefined && sessions.has(sessionId))
854
+ await joined(sessionId);
855
+ else if (resend)
856
+ await recovered();
857
+ const transport = await liveTransport();
858
+ try {
859
+ await transport.ready;
860
+ return await requestOn(transport, tier, send);
861
+ } catch (error) {
862
+ if (!closedUnder(transport, error))
863
+ throw error;
864
+ const again = resend === "read" || sessionId !== undefined && sessions.has(sessionId);
865
+ if (!again)
866
+ throw new AcpConnectionClosedError(undefined, { cause: error });
867
+ await transport.connection.closed.catch((err) => logger.warn({ err }, "acp.connection.closed_with_error"));
868
+ }
869
+ }
832
870
  }
833
871
  async function requestOn({ connection }, tier, send, parent) {
834
872
  const deadline = new Deadline(REQUEST_DEADLINE_MS[tier], clock, parent);
@@ -845,21 +883,6 @@ function createAcpConnection(options) {
845
883
  throw error;
846
884
  }
847
885
  }
848
- async function write(sessionId, tier, send) {
849
- for (;; ) {
850
- if (sessions.has(sessionId))
851
- await joined(sessionId);
852
- const transport = await readyTransport();
853
- try {
854
- return await requestOn(transport, tier, send);
855
- } catch (error) {
856
- const closedUnder = transport.connection.signal.aborted && !isTimeout(error) && sessions.has(sessionId);
857
- if (!closedUnder)
858
- throw error;
859
- await transport.connection.closed.catch((err) => logger.warn({ err }, "acp.connection.closed_with_error"));
860
- }
861
- }
862
- }
863
886
  function notifyAgent(send, sessionId) {
864
887
  (async () => {
865
888
  if (sessionId !== undefined && sessions.has(sessionId))
@@ -958,7 +981,7 @@ function createAcpConnection(options) {
958
981
  }
959
982
  }
960
983
  async function listAgents() {
961
- const response = AgentsListEntriesSchema.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.agents.list, undefined, options)));
984
+ const response = AgentsListEntriesSchema.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.agents.list, undefined, options), "read"));
962
985
  return {
963
986
  ...response,
964
987
  agents: readEach(response.agents, HgwAgentsListResponseSchema2.shape.agents.element, (entry) => logger.warn({ id: loggedId(entry, "summary", "id") }, "agents.entry_skipped"))
@@ -1075,18 +1098,20 @@ function createAcpConnection(options) {
1075
1098
  });
1076
1099
  }
1077
1100
  async function resumePage(sessionId, cursor) {
1078
- await joined(sessionId);
1079
1101
  const open = sessions.get(sessionId);
1080
1102
  if (!open)
1081
1103
  throw notOpen();
1082
- const updates = [];
1083
- open.page = updates;
1104
+ let updates = [];
1084
1105
  try {
1085
- const response = await request("medium", (agent, options) => agent.request(methods2.agent.session.resume, {
1086
- sessionId,
1087
- cwd: AGENT_CWD,
1088
- replayFrom: { type: HGW_REPLAY_BEFORE2, cursor }
1089
- }, options));
1106
+ const response = await request("medium", (agent, options) => {
1107
+ updates = [];
1108
+ open.page = updates;
1109
+ return agent.request(methods2.agent.session.resume, {
1110
+ sessionId,
1111
+ cwd: AGENT_CWD,
1112
+ replayFrom: { type: HGW_REPLAY_BEFORE2, cursor }
1113
+ }, options);
1114
+ }, { sessionId });
1090
1115
  const { history } = HgwHistoryPageResponseMetaSchema2.parse(hgwMetaOf(response._meta));
1091
1116
  return { updates, history };
1092
1117
  } finally {
@@ -1103,6 +1128,8 @@ function createAcpConnection(options) {
1103
1128
  }
1104
1129
  joinable = true;
1105
1130
  setOutage(undefined);
1131
+ recovery?.resolve();
1132
+ recovery = undefined;
1106
1133
  const opened = [...sessions.keys()];
1107
1134
  for (const open of sessions.values())
1108
1135
  open.owner?.actor.send({ type: "ready" });
@@ -1204,13 +1231,15 @@ function createAcpConnection(options) {
1204
1231
  ended = true;
1205
1232
  setStatus("closed");
1206
1233
  setOutage(undefined);
1207
- failInitialized?.(new Error("The ACP connection closed"));
1234
+ failInitialized?.(new AcpConnectionClosedError);
1208
1235
  failInitialized = undefined;
1209
1236
  settleInitialized = undefined;
1237
+ recovery?.reject(new AcpConnectionClosedError);
1238
+ recovery = undefined;
1210
1239
  for (const open of sessions.values()) {
1211
1240
  clock.clearTimeout(open.grace);
1212
1241
  open.owner?.dispose();
1213
- open.waiters?.reject(new Error("The ACP connection closed"));
1242
+ open.waiters?.reject(new AcpConnectionClosedError);
1214
1243
  }
1215
1244
  sessions.clear();
1216
1245
  live?.connection.close();
@@ -1247,7 +1276,7 @@ function createAcpConnection(options) {
1247
1276
  const response = await request("short", (agent, options) => agent.request(methods2.agent.session.list, {
1248
1277
  ...cursor === undefined ? {} : { cursor },
1249
1278
  _meta: { [HGW_META_KEY2]: meta }
1250
- }, options));
1279
+ }, options), "read");
1251
1280
  return {
1252
1281
  sessions: response.sessions,
1253
1282
  ...response.nextCursor ? { nextCursor: response.nextCursor } : {}
@@ -1255,8 +1284,10 @@ function createAcpConnection(options) {
1255
1284
  },
1256
1285
  async searchSessions(meta) {
1257
1286
  const response = SessionSearchEntriesSchema.parse(await request("short", (agent, options) => agent.request(HGW_METHODS2.sessions.search, {
1258
- _meta: { [HGW_META_KEY2]: HgwSessionSearchMetaSchema2.parse(meta) }
1259
- }, options)));
1287
+ _meta: {
1288
+ [HGW_META_KEY2]: HgwSessionSearchMetaSchema2.parse(meta)
1289
+ }
1290
+ }, options), "read"));
1260
1291
  return {
1261
1292
  ...response,
1262
1293
  sessions: readEach(response.sessions, HgwSessionSearchResponseSchema2.shape.sessions.element, (entry) => logger.warn({ id: loggedId(entry, "sessionId") }, "sessions.result_skipped"))
@@ -1275,14 +1306,14 @@ function createAcpConnection(options) {
1275
1306
  _meta: { [HGW_META_KEY2]: meta }
1276
1307
  };
1277
1308
  assertFrameFits(params);
1278
- const response = await write(sessionId, "long", (agent, options) => agent.request(methods2.agent.session.prompt, params, options));
1309
+ const response = await request("long", (agent, options) => agent.request(methods2.agent.session.prompt, params, options), { sessionId });
1279
1310
  return { messageId: response.messageId };
1280
1311
  },
1281
1312
  cancel(sessionId) {
1282
1313
  notifyAgent((agent) => agent.notify(methods2.agent.session.cancel, { sessionId }), sessionId);
1283
1314
  },
1284
1315
  async setConfigOption(sessionId, configId, value) {
1285
- const response = await write(sessionId, "medium", (agent, options) => agent.request(methods2.agent.session.setConfigOption, { sessionId, configId, type: "id", value }, options));
1316
+ const response = await request("medium", (agent, options) => agent.request(methods2.agent.session.setConfigOption, { sessionId, configId, type: "id", value }, options), { sessionId });
1286
1317
  return response.configOptions;
1287
1318
  },
1288
1319
  async deleteSession(sessionId) {
@@ -1293,7 +1324,7 @@ function createAcpConnection(options) {
1293
1324
  },
1294
1325
  async steer(steer) {
1295
1326
  assertFrameFits(steer);
1296
- return HgwSteerResponseSchema2.parse(await write(steer.sessionId, "medium", (agent, options) => agent.request(HGW_METHODS2.session.steer, steer, options)));
1327
+ return HgwSteerResponseSchema2.parse(await request("medium", (agent, options) => agent.request(HGW_METHODS2.session.steer, steer, options), { sessionId: steer.sessionId }));
1297
1328
  },
1298
1329
  focus(sessionId, presence) {
1299
1330
  const report = {
@@ -2682,6 +2713,7 @@ class HgwRemoteClient {
2682
2713
  }
2683
2714
  }
2684
2715
  export {
2716
+ AcpConnectionClosedError,
2685
2717
  AgentUpdateError,
2686
2718
  HgwClientError,
2687
2719
  HgwRemoteClient,
@@ -63,6 +63,13 @@ export type AcpSessionListener = {
63
63
  replay?: AcpSessionReplayListener;
64
64
  state?: (state: AcpSessionState) => void;
65
65
  };
66
+ /**
67
+ * A read (`listAgents`, `listSessions`, `searchSessions`, `resumePage`) waits
68
+ * out a reconnect and goes out again on the transport that recovers, and a
69
+ * write for an opened Session once it has rejoined. Any other call a
70
+ * transport closes under, and every call once the connection has ended,
71
+ * rejects as `AcpConnectionClosedError`.
72
+ */
66
73
  export interface AcpConnection {
67
74
  readonly status: AcpConnectionStatus;
68
75
  /**
@@ -17,6 +17,15 @@ export declare class AgentUpdateError extends Error {
17
17
  readonly code: "unsupported" | "conflict";
18
18
  constructor(code: "unsupported" | "conflict", message: string);
19
19
  }
20
+ /**
21
+ * The ACP connection is gone for a request: it has ended, no transport is open
22
+ * to send on, or a transport closed under a request it does not send again.
23
+ * `cause` holds what that transport closed with, which a failed socket leaves
24
+ * a DOM `Event`. Gateway refusals and timeouts reject as they come.
25
+ */
26
+ export declare class AcpConnectionClosedError extends Error {
27
+ constructor(message?: string, options?: ErrorOptions);
28
+ }
20
29
  export type SessionStatus = "idle" | "running" | "waiting-for-input" | "failed" | "unknown";
21
30
  export type ActivityBase = {
22
31
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-gw/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A gateway that puts Hermes, OpenClaw, and OpenCode behind one ACP WebSocket, and the browser client and protocol SDK for it.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",