@mirasoth/soothe-client 0.4.0 → 0.4.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
@@ -1,12 +1,7 @@
1
1
  # @mirasoth/soothe-client
2
2
 
3
- TypeScript WebSocket client for the [Soothe](https://github.com/mirasoth/soothe) daemon.
4
-
5
- Provides a typed protocol-1 message stack, session bootstrap, reconnect/reattach,
6
- appkit (connection pool, turn runner, event classifier), and a dual-socket
7
- `DaemonSession` for streamed turns — matching the production Go and Python clients.
8
-
9
- ## Install
3
+ Talk to a running **soothe-daemon** over WebSocket send prompts, stream agent
4
+ turns, run jobs.
10
5
 
11
6
  ```bash
12
7
  npm install @mirasoth/soothe-client
@@ -14,7 +9,7 @@ npm install @mirasoth/soothe-client
14
9
  npm install sharp
15
10
  ```
16
11
 
17
- Requires Node.js `>=19`.
12
+ Requires Node.js `>=19` and a local daemon (default `ws://127.0.0.1:8765`).
18
13
 
19
14
  ## Quick start
20
15
 
@@ -23,15 +18,17 @@ import { DaemonSession } from '@mirasoth/soothe-client';
23
18
 
24
19
  const session = new DaemonSession('ws://127.0.0.1:8765');
25
20
  await session.connect();
26
- await session.sendTurn('summarize this repo');
21
+ await session.sendTurn('Summarize this in one sentence: agents need tools.');
27
22
 
28
- for await (const [namespace, mode, data] of session.iterTurnChunks()) {
23
+ for await (const [_namespace, mode, data] of session.iterTurnChunks()) {
29
24
  console.log(mode, data);
30
25
  }
31
26
 
32
27
  await session.close();
33
28
  ```
34
29
 
30
+ More patterns: [`examples/`](examples/) (hello → streaming → multi-turn → pool → jobs).
31
+
35
32
  ## What you get
36
33
 
37
34
  | Need | Use |
@@ -42,16 +39,23 @@ await session.close();
42
39
  | Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
43
40
 
44
41
  `iterTurnChunks` peels leftover prior-goal terminals at turn start, ignores
45
- premature `soothe.stream.end` until the turn has real progress, and drains a
46
- short post-idle window before returning. Terminal stream frames send
47
- `delivery_ack` (daemon drain gating).
42
+ premature `soothe.stream.end` until the turn has real progress, drains a short
43
+ post-idle window, and sends `delivery_ack` on terminal frames for daemon drain
44
+ gating.
45
+
46
+ ```ts
47
+ import { CommandClient } from '@mirasoth/soothe-client';
48
+
49
+ const cc = new CommandClient('ws://127.0.0.1:8765', { timeoutMs: 30_000 });
50
+ const created = await cc.jobCreate('Echo: smoke job', '/tmp/workspace');
51
+ await cc.jobStatus(String(created.job_id));
52
+ await cc.jobCancel(String(created.job_id));
53
+ ```
48
54
 
49
55
  ## Appkit TurnRunner
50
56
 
51
57
  Product backends that pool connections per chat session use `ConnectionPool` +
52
- `QueryGate` + `TurnRunner` + `EventClassifier` (RFC-629 Layer 1).
53
-
54
- Lifecycle knobs (all opt-in; defaults match historical fail-on-timeout behaviour):
58
+ `QueryGate` + `TurnRunner` + `EventClassifier`.
55
59
 
56
60
  | Knob | Default | Notes |
57
61
  |------|---------|--------|
@@ -76,15 +80,23 @@ Lifecycle knobs (all opt-in; defaults match historical fail-on-timeout behaviour
76
80
 
77
81
  ## API surface
78
82
 
79
- - **`Client`** — WebSocket session, RPC, reconnect/reattach, peel stale pending frames
80
- - **`DaemonSession`** — dual-socket loop session + `iterTurnChunks`
81
- - **`TurnRunner` / `ConnectionPool` / `QueryGate` / `EventClassifier` / `SSEBroadcaster`** — appkit
83
+ - **`DaemonSession`** — dual-socket loop session + `iterTurnChunks` (preferred for chat)
84
+ - **`CommandClient`** — ephemeral connect one RPC → close (jobs / cron)
85
+ - **`Client`** long-lived WebSocket, RPC, reconnect/reattach, peel-stale helpers
86
+ - **`ConnectionPool` / `TurnRunner` / `QueryGate` / `EventClassifier` / `SSEBroadcaster`** — multi-user appkit
82
87
  - **`connectedWebsocket` / `protocol1Rpc`** — oneshot CLI-style helpers
83
- - **`bootstrapLoopSession`**, **`connectWithRetries`** — session helpers
88
+ - **`bootstrapLoopSession` / `connectWithRetries`** — session helpers
84
89
 
85
90
  See `dist/index.d.ts` or `src/index.ts` for the full export list.
86
91
 
87
- ## Development
92
+ ## Limitations
93
+
94
+ Autopilot control is WebSocket-only (protocol-1 `autopilot_*` / `job_*`
95
+ request RPCs). Prefer `CommandClient` for job/cron/autopilot one-shots so they
96
+ do not share a streaming socket. Worker event streams use
97
+ `client.autopilotSubscribe()` on a long-lived `Client`.
98
+
99
+ ## Develop
88
100
 
89
101
  ```bash
90
102
  make help # list targets
@@ -92,8 +104,14 @@ make install # install dependencies
92
104
  make build # compile to dist/
93
105
  make test # unit tests
94
106
  make verify # full pre-publish verification
107
+ npm test -- examples/progressive # 01–06 ladder (offline)
95
108
  ```
96
109
 
110
+ ## Compatibility
111
+
112
+ Same protocol-1 WebSocket contract as `soothe-client-python` and
113
+ `soothe-client-go`.
114
+
97
115
  ## License
98
116
 
99
117
  MIT — see [LICENSE](./LICENSE).
@@ -63,7 +63,7 @@ var ConnectionError = class extends Error {
63
63
  }
64
64
  };
65
65
  var DaemonError = class extends Error {
66
- /** Numeric error code from the RFC-450 §7.3 registry. */
66
+ /** Numeric error code from the daemon error registry. */
67
67
  code;
68
68
  /** The daemon's error message text. */
69
69
  daemonMessage;
@@ -621,7 +621,7 @@ function messagesWireTerminal(data) {
621
621
  import { randomUUID } from "crypto";
622
622
  var PROTO_VERSION = "1";
623
623
  var DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
624
- var CLIENT_VERSION = "0.4.0";
624
+ var CLIENT_VERSION = "0.4.1";
625
625
  function encodeMessage(msg) {
626
626
  return JSON.stringify(msg) + "\n";
627
627
  }
@@ -793,7 +793,7 @@ var Client = class extends EventEmitter {
793
793
  inboundDroppedCount = 0;
794
794
  onStreamDegraded = null;
795
795
  resolvers = [];
796
- // Protocol-1 handshake state (RFC-450 §8.2)
796
+ // Protocol-1 handshake state
797
797
  handshakeComplete = false;
798
798
  negotiatedCapabilities = /* @__PURE__ */ new Set();
799
799
  protocolVersion = null;
@@ -801,12 +801,12 @@ var Client = class extends EventEmitter {
801
801
  heartbeatIntervalMs = 0;
802
802
  heartbeatTimer = null;
803
803
  lastPongMonotonic = 0;
804
- // Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
804
+ // Mid-session drop signal. The 'disconnected' event is
805
805
  // emitted exactly once when the connection drops, carrying a DisconnectCause
806
806
  // that distinguishes clean (peer `disconnect`) from unclean (read/write
807
807
  // error or missed pong). `disconnFired` guards the once-only delivery.
808
808
  disconnFired = false;
809
- // Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
809
+ // Pending-request/subscription multiplexer. Routes
810
810
  // inbound frames by (type, id) instead of discarding non-matching events.
811
811
  mux = new Multiplexer();
812
812
  deliveryRecvSeq = /* @__PURE__ */ new Map();
@@ -923,7 +923,7 @@ var Client = class extends EventEmitter {
923
923
  return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.handshakeComplete;
924
924
  }
925
925
  // ---------------------------------------------------------------------------
926
- // Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
926
+ // Mid-session drop signal + reconnect/reattach
927
927
  // ---------------------------------------------------------------------------
928
928
  /**
929
929
  * Returns whether the connection has dropped (the `'disconnected'` event has
@@ -958,8 +958,8 @@ var Client = class extends EventEmitter {
958
958
  }
959
959
  }
960
960
  /**
961
- * Re-dials the daemon and re-handshakes after a connection drop (RFC-450
962
- * §8.3). Does not re-establish loop subscriptions; follow with
961
+ * Re-dials the daemon and re-handshakes after a connection drop.
962
+ * Does not re-establish loop subscriptions; follow with
963
963
  * `reattachAndProbe()` to resume a loop session. The caller should invoke
964
964
  * this after the `'disconnected'` event fires. Reuses the same Client,
965
965
  * resetting the drop signal and multiplexer.
@@ -993,7 +993,7 @@ var Client = class extends EventEmitter {
993
993
  * Returns a `StaleLoopError` when the probe fails; callers should fall back
994
994
  * to a fresh `loop_new` bootstrap.
995
995
  *
996
- * Per RFC-629: connection-level readiness is the handshake's readiness_state
996
+ * Note: connection-level readiness is the handshake's readiness_state
997
997
  * (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
998
998
  * probe.
999
999
  */
@@ -1034,7 +1034,7 @@ var Client = class extends EventEmitter {
1034
1034
  }
1035
1035
  }
1036
1036
  // ---------------------------------------------------------------------------
1037
- // Protocol-1 handshake (RFC-450 §8.2)
1037
+ // Protocol-1 handshake
1038
1038
  // ---------------------------------------------------------------------------
1039
1039
  /** Send connection_init and wait for connection_ack with readiness "ready". */
1040
1040
  async _performHandshake() {
@@ -1083,7 +1083,7 @@ var Client = class extends EventEmitter {
1083
1083
  throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
1084
1084
  }
1085
1085
  // ---------------------------------------------------------------------------
1086
- // Heartbeat (RFC-450 §8.3)
1086
+ // Heartbeat
1087
1087
  // ---------------------------------------------------------------------------
1088
1088
  _startHeartbeat() {
1089
1089
  if (!this.negotiatedCapabilities.has("heartbeat")) return;
@@ -1278,7 +1278,7 @@ var Client = class extends EventEmitter {
1278
1278
  }
1279
1279
  }
1280
1280
  // ---------------------------------------------------------------------------
1281
- // Protocol-1 RPC primitives (RFC-450 §5/§9)
1281
+ // Protocol-1 RPC primitives
1282
1282
  // ---------------------------------------------------------------------------
1283
1283
  /**
1284
1284
  * Reads the next frame directly from the live socket (via a resolver),
@@ -1304,16 +1304,16 @@ var Client = class extends EventEmitter {
1304
1304
  });
1305
1305
  }
1306
1306
  /**
1307
- * Sends a `request` envelope and waits for the matching `response` (or
1308
- * `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
1309
- *
1310
- * Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
1311
- * keyed by the request id so that, even when a `receiveMessages()` reader
1312
- * is concurrently active, the matching `response`/`error` is routed to
1313
- * this caller instead of being discarded or buffered behind a stream.
1314
- * Non-matching frames are routed to their own waiters by the multiplexer
1315
- * or flow on to the resolver queue for stream readers.
1316
- */
1307
+ * Sends a `request` envelope and waits for the matching `response` (or
1308
+ * `error`) correlated by `id`. Returns the `result` object.
1309
+ *
1310
+ * Multiplexer-aware: registers a pending RPC wait
1311
+ * keyed by the request id so that, even when a `receiveMessages()` reader
1312
+ * is concurrently active, the matching `response`/`error` is routed to
1313
+ * this caller instead of being discarded or buffered behind a stream.
1314
+ * Non-matching frames are routed to their own waiters by the multiplexer
1315
+ * or flow on to the resolver queue for stream readers.
1316
+ */
1317
1317
  async requestResponse(method, params, responseType, timeout = 15e3) {
1318
1318
  const req = requestEnvelope(method, params);
1319
1319
  const rid = req.id;
@@ -1464,7 +1464,7 @@ var Client = class extends EventEmitter {
1464
1464
  return ev;
1465
1465
  }
1466
1466
  // ---------------------------------------------------------------------------
1467
- // High-level API methods (Loop-first, RFC-503)
1467
+ // High-level API methods
1468
1468
  // ---------------------------------------------------------------------------
1469
1469
  /** Sends user input to the daemon (loop_input notification; requires loopID). */
1470
1470
  sendInput(text, options) {
@@ -1503,7 +1503,7 @@ var Client = class extends EventEmitter {
1503
1503
  return this.notify("slash_command", { cmd });
1504
1504
  }
1505
1505
  // ---------------------------------------------------------------------------
1506
- // Loop lifecycle methods (RFC-503)
1506
+ // Loop lifecycle methods
1507
1507
  // ---------------------------------------------------------------------------
1508
1508
  /** Requests the daemon to create a new StrangeLoop and waits for the response. */
1509
1509
  sendLoopNew(opts) {
@@ -1598,7 +1598,7 @@ var Client = class extends EventEmitter {
1598
1598
  sendLoopCardsFetch(loopID) {
1599
1599
  return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
1600
1600
  }
1601
- /** Requests the full loop history (RFC-631). */
1601
+ /** Requests the full loop history. */
1602
1602
  sendLoopHistoryFetch(loopID) {
1603
1603
  return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
1604
1604
  }
@@ -1693,7 +1693,7 @@ var Client = class extends EventEmitter {
1693
1693
  );
1694
1694
  }
1695
1695
  // ---------------------------------------------------------------------------
1696
- // RFC-228 Job IPC methods
1696
+ // Job IPC methods
1697
1697
  // ---------------------------------------------------------------------------
1698
1698
  /** Creates an autopilot job and waits for the response. */
1699
1699
  createJob(goal, verificationRules, workspace, timeout) {
@@ -1728,6 +1728,98 @@ var Client = class extends EventEmitter {
1728
1728
  if (goalId) params.goal_id = goalId;
1729
1729
  return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
1730
1730
  }
1731
+ // ---------------------------------------------------------------------------
1732
+ // Autopilot goal RPCs (protocol-1 request methods)
1733
+ // ---------------------------------------------------------------------------
1734
+ /** Return autopilot scheduler status (running / dreaming / pool). */
1735
+ autopilotStatus(timeout) {
1736
+ return this.requestResponse("autopilot_status", {}, "autopilot_status", timeout ?? 15e3);
1737
+ }
1738
+ /** Submit a new autopilot goal (returns goal_id). */
1739
+ autopilotSubmit(description, opts) {
1740
+ const params = {
1741
+ description,
1742
+ priority: opts?.priority ?? 50
1743
+ };
1744
+ if (opts?.workspace) params.workspace = opts.workspace;
1745
+ return this.requestResponse(
1746
+ "autopilot_submit",
1747
+ params,
1748
+ "autopilot_submit",
1749
+ opts?.timeout ?? 15e3
1750
+ );
1751
+ }
1752
+ /** List all goals (including non-root children). */
1753
+ autopilotListGoals(timeout) {
1754
+ return this.requestResponse(
1755
+ "autopilot_list_goals",
1756
+ {},
1757
+ "autopilot_list_goals",
1758
+ timeout ?? 15e3
1759
+ );
1760
+ }
1761
+ /** Fetch one goal by id. */
1762
+ autopilotGetGoal(goalId, timeout) {
1763
+ return this.requestResponse(
1764
+ "autopilot_get_goal",
1765
+ { goal_id: goalId },
1766
+ "autopilot_get_goal",
1767
+ timeout ?? 15e3
1768
+ );
1769
+ }
1770
+ /** Cancel a goal and its non-terminal descendants. */
1771
+ autopilotCancelGoal(goalId, timeout) {
1772
+ return this.requestResponse(
1773
+ "autopilot_cancel_goal",
1774
+ { goal_id: goalId },
1775
+ "autopilot_cancel_goal",
1776
+ timeout ?? 15e3
1777
+ );
1778
+ }
1779
+ /** Cancel every open (non-terminal) goal. */
1780
+ autopilotCancelAll(timeout) {
1781
+ return this.requestResponse(
1782
+ "autopilot_cancel_all",
1783
+ {},
1784
+ "autopilot_cancel_all",
1785
+ timeout ?? 15e3
1786
+ );
1787
+ }
1788
+ /** Exit dreaming mode and resume scheduling. */
1789
+ autopilotWake(timeout) {
1790
+ return this.requestResponse("autopilot_wake", {}, "autopilot_wake", timeout ?? 15e3);
1791
+ }
1792
+ /** Force dreaming mode. */
1793
+ autopilotDream(timeout) {
1794
+ return this.requestResponse("autopilot_dream", {}, "autopilot_dream", timeout ?? 15e3);
1795
+ }
1796
+ /** Resume a suspended or blocked goal. */
1797
+ autopilotResume(goalId, timeout) {
1798
+ return this.requestResponse(
1799
+ "autopilot_resume",
1800
+ { goal_id: goalId },
1801
+ "autopilot_resume",
1802
+ timeout ?? 15e3
1803
+ );
1804
+ }
1805
+ /** List root goals only (jobs). Prefer createJob / getJobStatus for job control. */
1806
+ autopilotListJobs(timeout) {
1807
+ return this.requestResponse(
1808
+ "autopilot_list_jobs",
1809
+ {},
1810
+ "autopilot_list_jobs",
1811
+ timeout ?? 15e3
1812
+ );
1813
+ }
1814
+ /** Get a root job with DAG snapshot. Prefer getJobStatus / getJobDag. */
1815
+ autopilotGetJob(jobId, timeout) {
1816
+ return this.requestResponse(
1817
+ "autopilot_get_job",
1818
+ { job_id: jobId },
1819
+ "autopilot_get_job",
1820
+ timeout ?? 15e3
1821
+ );
1822
+ }
1731
1823
  /** Subscribes to autopilot worker events. */
1732
1824
  autopilotSubscribe(timeout) {
1733
1825
  return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
@@ -1738,7 +1830,7 @@ var Client = class extends EventEmitter {
1738
1830
  return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
1739
1831
  }
1740
1832
  // ---------------------------------------------------------------------------
1741
- // RFC-229 Cron IPC methods
1833
+ // Cron IPC methods
1742
1834
  // ---------------------------------------------------------------------------
1743
1835
  /** Creates a scheduled job from natural language. */
1744
1836
  cronAdd(text, priority, timeout) {
@@ -1880,4 +1972,4 @@ export {
1880
1972
  inboundNeedsDeliveryAck,
1881
1973
  Client
1882
1974
  };
1883
- //# sourceMappingURL=chunk-U6RMINYV.js.map
1975
+ //# sourceMappingURL=chunk-YYUVHZ3W.js.map