@linkedclaw/consumer-runtime 0.9.1 → 0.9.2

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
@@ -6,9 +6,18 @@ interface HireParams {
6
6
  maxMessages?: number;
7
7
  referredBy?: string;
8
8
  autoActivate?: boolean;
9
- /** Override the default relay URL. The ACP endpoint is derived by replacing the trailing `/ws`. */
9
+ /** Override the default relay URL (defaults to the cloud relay /ws endpoint). */
10
10
  relayUrl?: string;
11
- /** API key + agent ID needed for the ACP IDENTIFY frame. */
11
+ /**
12
+ * Try /acp (OpenClaw sub-agent bridge) before /ws. Default false — mirrors
13
+ * `SkillConfig.try_acp = False` on the provider side (upstream commit
14
+ * 2dbc36c). Native providers register on /ws and are reachable there; only
15
+ * opt in when the target is an ACP-routed agent (OpenClaw sub-agent). When
16
+ * opted in, falls back to /ws if /acp connect fails or the recipient is
17
+ * absent from /acp's connection table.
18
+ */
19
+ tryAcp?: boolean;
20
+ /** API key + agent ID for the IDENTIFY frame. */
12
21
  apiKey: string;
13
22
  agentId: string;
14
23
  }
@@ -28,10 +37,14 @@ declare class RequesterFlows {
28
37
  sort?: string;
29
38
  }): Promise<Agent[]>;
30
39
  /**
31
- * Open a session. Performs HTTP create → ACP WS handshake (SESSION_CREATE/ACCEPT) → HTTP activate.
32
- * Default 30s wait for SESSION_ACCEPT.
40
+ * Open a session. HTTP create → WS handshake (SESSION_CREATE/ACCEPT) → done.
41
+ *
42
+ * Default transport is /ws — native providers register there
43
+ * (`SkillConfig.try_acp=False` upstream default). Set `tryAcp: true` to try
44
+ * /acp first; on opt-in, falls back to /ws when the recipient isn't on /acp.
33
45
  */
34
46
  hire(params: HireParams): Promise<HireResult>;
47
+ private attemptHandshake;
35
48
  send(sessionId: string, payload: Record<string, unknown> | string, seq: number): Promise<{
36
49
  accepted?: boolean;
37
50
  }>;
package/dist/index.js CHANGED
@@ -4,12 +4,18 @@ import {
4
4
  } from "@linkedclaw/consumer";
5
5
  import { MessageType } from "@linkedclaw/provider";
6
6
  import WebSocket from "ws";
7
- var ACP_TIMEOUT_MS = 3e4;
7
+ var ACP_CONNECT_TIMEOUT_MS = 2e3;
8
+ var SESSION_ACCEPT_TIMEOUT_MS = 3e4;
8
9
  var SessionRejectedError = class extends Error {
9
10
  constructor(reason) {
10
11
  super(`session rejected: ${reason}`);
11
12
  }
12
13
  };
14
+ var TransportMissError = class extends Error {
15
+ constructor(reason) {
16
+ super(`transport miss: ${reason}`);
17
+ }
18
+ };
13
19
  var RequesterFlows = class {
14
20
  constructor(client) {
15
21
  this.client = client;
@@ -19,8 +25,11 @@ var RequesterFlows = class {
19
25
  return this.client.discover({ capability, ...extra });
20
26
  }
21
27
  /**
22
- * Open a session. Performs HTTP create → ACP WS handshake (SESSION_CREATE/ACCEPT) → HTTP activate.
23
- * Default 30s wait for SESSION_ACCEPT.
28
+ * Open a session. HTTP create → WS handshake (SESSION_CREATE/ACCEPT) → done.
29
+ *
30
+ * Default transport is /ws — native providers register there
31
+ * (`SkillConfig.try_acp=False` upstream default). Set `tryAcp: true` to try
32
+ * /acp first; on opt-in, falls back to /ws when the recipient isn't on /acp.
24
33
  */
25
34
  async hire(params) {
26
35
  const session = await this.client.createSession({
@@ -31,22 +40,57 @@ var RequesterFlows = class {
31
40
  });
32
41
  if (params.autoActivate === false) return { session, activated: false };
33
42
  const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL;
34
- const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
35
- const ws = new WebSocket(acpUrl);
43
+ try {
44
+ if (params.tryAcp) {
45
+ const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
46
+ try {
47
+ await this.attemptHandshake(acpUrl, session.session_id, params, ACP_CONNECT_TIMEOUT_MS);
48
+ } catch (err) {
49
+ if (!(err instanceof TransportMissError)) throw err;
50
+ await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
51
+ }
52
+ } else {
53
+ await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
54
+ }
55
+ } catch (err) {
56
+ await this.client.endSession(session.session_id, {}).catch(() => {
57
+ });
58
+ if (err instanceof TransportMissError) {
59
+ throw new SessionRejectedError(`agent unreachable`);
60
+ }
61
+ throw err;
62
+ }
63
+ return { session, activated: true };
64
+ }
65
+ async attemptHandshake(url, sessionId, params, connectTimeoutMs) {
66
+ const ws = new WebSocket(url);
36
67
  try {
37
68
  await new Promise((resolve, reject) => {
38
- ws.once("open", () => resolve());
39
- ws.once("error", reject);
69
+ const timer = setTimeout(
70
+ () => reject(new TransportMissError("connect timeout")),
71
+ connectTimeoutMs
72
+ );
73
+ ws.once("open", () => {
74
+ clearTimeout(timer);
75
+ resolve();
76
+ });
77
+ ws.once("error", (err) => {
78
+ clearTimeout(timer);
79
+ reject(new TransportMissError(`connect failed: ${err.message}`));
80
+ });
40
81
  });
41
82
  ws.send(JSON.stringify({ type: MessageType.IDENTIFY, agent_id: params.agentId, token: params.apiKey }));
42
83
  ws.send(JSON.stringify({
43
84
  type: MessageType.SESSION_CREATE,
44
- session_id: session.session_id,
85
+ session_id: sessionId,
45
86
  recipient: params.providerAgentId,
46
87
  capability: params.capability
47
88
  }));
48
89
  const reply = await new Promise((resolve, reject) => {
49
- const timer = setTimeout(() => reject(new Error("ACP SESSION_ACCEPT timeout")), ACP_TIMEOUT_MS);
90
+ const timer = setTimeout(
91
+ () => reject(new Error("SESSION_ACCEPT timeout")),
92
+ SESSION_ACCEPT_TIMEOUT_MS
93
+ );
50
94
  ws.once("message", (data) => {
51
95
  clearTimeout(timer);
52
96
  resolve(JSON.parse(data.toString()));
@@ -56,17 +100,19 @@ var RequesterFlows = class {
56
100
  reject(err);
57
101
  });
58
102
  });
59
- if (reply.type === MessageType.ERROR) throw new SessionRejectedError(reply.error ?? "relay error");
103
+ if (reply.type === MessageType.ERROR) {
104
+ const errMsg = reply.error ?? "relay error";
105
+ if (errMsg.includes("not connected")) throw new TransportMissError(errMsg);
106
+ throw new SessionRejectedError(errMsg);
107
+ }
60
108
  if (reply.type === MessageType.SESSION_REJECT) throw new SessionRejectedError(reply.reason ?? "rejected");
61
- if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected ACP reply: ${reply.type}`);
62
- } catch (err) {
63
- ws.close();
64
- await this.client.endSession(session.session_id, {}).catch(() => {
65
- });
66
- throw err;
109
+ if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected reply: ${reply.type}`);
110
+ } finally {
111
+ try {
112
+ ws.close();
113
+ } catch {
114
+ }
67
115
  }
68
- ws.close();
69
- return { session, activated: true };
70
116
  }
71
117
  send(sessionId, payload, seq) {
72
118
  const normalized = typeof payload === "string" ? { text: payload } : payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkedclaw/consumer-runtime",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Runtime helpers (RequesterFlows, ACP) for LinkedClaw consumer agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  "dependencies": {
24
24
  "ws": "^8.18.0",
25
25
  "zod": "^3.23.0",
26
- "@linkedclaw/provider": "^0.9.1",
27
- "@linkedclaw/consumer": "^0.9.1"
26
+ "@linkedclaw/consumer": "^0.9.1",
27
+ "@linkedclaw/provider": "^0.9.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@biomejs/biome": "^1.9.4",