@mirasoth/soothe-client 0.4.0 → 0.5.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/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;
@@ -320,13 +320,16 @@ function isValidVerbosityLevel(s) {
320
320
 
321
321
  // src/events.ts
322
322
  var EventPlanCreated = "soothe.cognition.plan.created";
323
- var EventExploreStarted = "soothe.subagent.explore.started";
324
- var EventExploreMilestone = "soothe.subagent.explore.milestone";
325
- var EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
326
- var EventExploreCompleted = "soothe.subagent.explore.completed";
327
- var EventTacitusStarted = "soothe.subagent.tacitus.started";
328
- var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
329
- var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
323
+ var EventExplorerStarted = "soothe.subagent.explorer.started";
324
+ var EventExplorerMilestone = "soothe.subagent.explorer.milestone";
325
+ var EventExplorerStepCompleted = "soothe.subagent.explorer.step.completed";
326
+ var EventExplorerCompleted = "soothe.subagent.explorer.completed";
327
+ var EventDeepResearchStarted = "soothe.subagent.deep_research.started";
328
+ var EventDeepResearchProgress = "soothe.subagent.deep_research.progress";
329
+ var EventDeepResearchStepCompleted = "soothe.subagent.deep_research.step.completed";
330
+ var EventDeepResearchGatherSummary = "soothe.subagent.deep_research.gather.summary";
331
+ var EventDeepResearchCrawlSummary = "soothe.subagent.deep_research.crawl.summary";
332
+ var EventDeepResearchCompleted = "soothe.subagent.deep_research.completed";
330
333
  var EventReplayComplete = "replay_complete";
331
334
  var EventLoopReattachedWire = "loop_reattached";
332
335
  var EventCardReplayBegin = "card.replay_begin";
@@ -621,7 +624,7 @@ function messagesWireTerminal(data) {
621
624
  import { randomUUID } from "crypto";
622
625
  var PROTO_VERSION = "1";
623
626
  var DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
624
- var CLIENT_VERSION = "0.4.0";
627
+ var CLIENT_VERSION = "0.5.0";
625
628
  function encodeMessage(msg) {
626
629
  return JSON.stringify(msg) + "\n";
627
630
  }
@@ -793,7 +796,7 @@ var Client = class extends EventEmitter {
793
796
  inboundDroppedCount = 0;
794
797
  onStreamDegraded = null;
795
798
  resolvers = [];
796
- // Protocol-1 handshake state (RFC-450 §8.2)
799
+ // Protocol-1 handshake state
797
800
  handshakeComplete = false;
798
801
  negotiatedCapabilities = /* @__PURE__ */ new Set();
799
802
  protocolVersion = null;
@@ -801,12 +804,12 @@ var Client = class extends EventEmitter {
801
804
  heartbeatIntervalMs = 0;
802
805
  heartbeatTimer = null;
803
806
  lastPongMonotonic = 0;
804
- // Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
807
+ // Mid-session drop signal. The 'disconnected' event is
805
808
  // emitted exactly once when the connection drops, carrying a DisconnectCause
806
809
  // that distinguishes clean (peer `disconnect`) from unclean (read/write
807
810
  // error or missed pong). `disconnFired` guards the once-only delivery.
808
811
  disconnFired = false;
809
- // Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
812
+ // Pending-request/subscription multiplexer. Routes
810
813
  // inbound frames by (type, id) instead of discarding non-matching events.
811
814
  mux = new Multiplexer();
812
815
  deliveryRecvSeq = /* @__PURE__ */ new Map();
@@ -923,7 +926,7 @@ var Client = class extends EventEmitter {
923
926
  return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.handshakeComplete;
924
927
  }
925
928
  // ---------------------------------------------------------------------------
926
- // Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
929
+ // Mid-session drop signal + reconnect/reattach
927
930
  // ---------------------------------------------------------------------------
928
931
  /**
929
932
  * Returns whether the connection has dropped (the `'disconnected'` event has
@@ -958,8 +961,8 @@ var Client = class extends EventEmitter {
958
961
  }
959
962
  }
960
963
  /**
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
964
+ * Re-dials the daemon and re-handshakes after a connection drop.
965
+ * Does not re-establish loop subscriptions; follow with
963
966
  * `reattachAndProbe()` to resume a loop session. The caller should invoke
964
967
  * this after the `'disconnected'` event fires. Reuses the same Client,
965
968
  * resetting the drop signal and multiplexer.
@@ -993,7 +996,7 @@ var Client = class extends EventEmitter {
993
996
  * Returns a `StaleLoopError` when the probe fails; callers should fall back
994
997
  * to a fresh `loop_new` bootstrap.
995
998
  *
996
- * Per RFC-629: connection-level readiness is the handshake's readiness_state
999
+ * Note: connection-level readiness is the handshake's readiness_state
997
1000
  * (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
998
1001
  * probe.
999
1002
  */
@@ -1034,7 +1037,7 @@ var Client = class extends EventEmitter {
1034
1037
  }
1035
1038
  }
1036
1039
  // ---------------------------------------------------------------------------
1037
- // Protocol-1 handshake (RFC-450 §8.2)
1040
+ // Protocol-1 handshake
1038
1041
  // ---------------------------------------------------------------------------
1039
1042
  /** Send connection_init and wait for connection_ack with readiness "ready". */
1040
1043
  async _performHandshake() {
@@ -1083,7 +1086,7 @@ var Client = class extends EventEmitter {
1083
1086
  throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
1084
1087
  }
1085
1088
  // ---------------------------------------------------------------------------
1086
- // Heartbeat (RFC-450 §8.3)
1089
+ // Heartbeat
1087
1090
  // ---------------------------------------------------------------------------
1088
1091
  _startHeartbeat() {
1089
1092
  if (!this.negotiatedCapabilities.has("heartbeat")) return;
@@ -1278,7 +1281,7 @@ var Client = class extends EventEmitter {
1278
1281
  }
1279
1282
  }
1280
1283
  // ---------------------------------------------------------------------------
1281
- // Protocol-1 RPC primitives (RFC-450 §5/§9)
1284
+ // Protocol-1 RPC primitives
1282
1285
  // ---------------------------------------------------------------------------
1283
1286
  /**
1284
1287
  * Reads the next frame directly from the live socket (via a resolver),
@@ -1304,16 +1307,16 @@ var Client = class extends EventEmitter {
1304
1307
  });
1305
1308
  }
1306
1309
  /**
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
- */
1310
+ * Sends a `request` envelope and waits for the matching `response` (or
1311
+ * `error`) correlated by `id`. Returns the `result` object.
1312
+ *
1313
+ * Multiplexer-aware: registers a pending RPC wait
1314
+ * keyed by the request id so that, even when a `receiveMessages()` reader
1315
+ * is concurrently active, the matching `response`/`error` is routed to
1316
+ * this caller instead of being discarded or buffered behind a stream.
1317
+ * Non-matching frames are routed to their own waiters by the multiplexer
1318
+ * or flow on to the resolver queue for stream readers.
1319
+ */
1317
1320
  async requestResponse(method, params, responseType, timeout = 15e3) {
1318
1321
  const req = requestEnvelope(method, params);
1319
1322
  const rid = req.id;
@@ -1464,7 +1467,7 @@ var Client = class extends EventEmitter {
1464
1467
  return ev;
1465
1468
  }
1466
1469
  // ---------------------------------------------------------------------------
1467
- // High-level API methods (Loop-first, RFC-503)
1470
+ // High-level API methods
1468
1471
  // ---------------------------------------------------------------------------
1469
1472
  /** Sends user input to the daemon (loop_input notification; requires loopID). */
1470
1473
  sendInput(text, options) {
@@ -1503,7 +1506,7 @@ var Client = class extends EventEmitter {
1503
1506
  return this.notify("slash_command", { cmd });
1504
1507
  }
1505
1508
  // ---------------------------------------------------------------------------
1506
- // Loop lifecycle methods (RFC-503)
1509
+ // Loop lifecycle methods
1507
1510
  // ---------------------------------------------------------------------------
1508
1511
  /** Requests the daemon to create a new StrangeLoop and waits for the response. */
1509
1512
  sendLoopNew(opts) {
@@ -1598,7 +1601,7 @@ var Client = class extends EventEmitter {
1598
1601
  sendLoopCardsFetch(loopID) {
1599
1602
  return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
1600
1603
  }
1601
- /** Requests the full loop history (RFC-631). */
1604
+ /** Requests the full loop history. */
1602
1605
  sendLoopHistoryFetch(loopID) {
1603
1606
  return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
1604
1607
  }
@@ -1693,7 +1696,7 @@ var Client = class extends EventEmitter {
1693
1696
  );
1694
1697
  }
1695
1698
  // ---------------------------------------------------------------------------
1696
- // RFC-228 Job IPC methods
1699
+ // Job IPC methods
1697
1700
  // ---------------------------------------------------------------------------
1698
1701
  /** Creates an autopilot job and waits for the response. */
1699
1702
  createJob(goal, verificationRules, workspace, timeout) {
@@ -1728,6 +1731,98 @@ var Client = class extends EventEmitter {
1728
1731
  if (goalId) params.goal_id = goalId;
1729
1732
  return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
1730
1733
  }
1734
+ // ---------------------------------------------------------------------------
1735
+ // Autopilot goal RPCs (protocol-1 request methods)
1736
+ // ---------------------------------------------------------------------------
1737
+ /** Return autopilot scheduler status (running / dreaming / pool). */
1738
+ autopilotStatus(timeout) {
1739
+ return this.requestResponse("autopilot_status", {}, "autopilot_status", timeout ?? 15e3);
1740
+ }
1741
+ /** Submit a new autopilot goal (returns goal_id). */
1742
+ autopilotSubmit(description, opts) {
1743
+ const params = {
1744
+ description,
1745
+ priority: opts?.priority ?? 50
1746
+ };
1747
+ if (opts?.workspace) params.workspace = opts.workspace;
1748
+ return this.requestResponse(
1749
+ "autopilot_submit",
1750
+ params,
1751
+ "autopilot_submit",
1752
+ opts?.timeout ?? 15e3
1753
+ );
1754
+ }
1755
+ /** List all goals (including non-root children). */
1756
+ autopilotListGoals(timeout) {
1757
+ return this.requestResponse(
1758
+ "autopilot_list_goals",
1759
+ {},
1760
+ "autopilot_list_goals",
1761
+ timeout ?? 15e3
1762
+ );
1763
+ }
1764
+ /** Fetch one goal by id. */
1765
+ autopilotGetGoal(goalId, timeout) {
1766
+ return this.requestResponse(
1767
+ "autopilot_get_goal",
1768
+ { goal_id: goalId },
1769
+ "autopilot_get_goal",
1770
+ timeout ?? 15e3
1771
+ );
1772
+ }
1773
+ /** Cancel a goal and its non-terminal descendants. */
1774
+ autopilotCancelGoal(goalId, timeout) {
1775
+ return this.requestResponse(
1776
+ "autopilot_cancel_goal",
1777
+ { goal_id: goalId },
1778
+ "autopilot_cancel_goal",
1779
+ timeout ?? 15e3
1780
+ );
1781
+ }
1782
+ /** Cancel every open (non-terminal) goal. */
1783
+ autopilotCancelAll(timeout) {
1784
+ return this.requestResponse(
1785
+ "autopilot_cancel_all",
1786
+ {},
1787
+ "autopilot_cancel_all",
1788
+ timeout ?? 15e3
1789
+ );
1790
+ }
1791
+ /** Exit dreaming mode and resume scheduling. */
1792
+ autopilotWake(timeout) {
1793
+ return this.requestResponse("autopilot_wake", {}, "autopilot_wake", timeout ?? 15e3);
1794
+ }
1795
+ /** Force dreaming mode. */
1796
+ autopilotDream(timeout) {
1797
+ return this.requestResponse("autopilot_dream", {}, "autopilot_dream", timeout ?? 15e3);
1798
+ }
1799
+ /** Resume a suspended or blocked goal. */
1800
+ autopilotResume(goalId, timeout) {
1801
+ return this.requestResponse(
1802
+ "autopilot_resume",
1803
+ { goal_id: goalId },
1804
+ "autopilot_resume",
1805
+ timeout ?? 15e3
1806
+ );
1807
+ }
1808
+ /** List root goals only (jobs). Prefer createJob / getJobStatus for job control. */
1809
+ autopilotListJobs(timeout) {
1810
+ return this.requestResponse(
1811
+ "autopilot_list_jobs",
1812
+ {},
1813
+ "autopilot_list_jobs",
1814
+ timeout ?? 15e3
1815
+ );
1816
+ }
1817
+ /** Get a root job with DAG snapshot. Prefer getJobStatus / getJobDag. */
1818
+ autopilotGetJob(jobId, timeout) {
1819
+ return this.requestResponse(
1820
+ "autopilot_get_job",
1821
+ { job_id: jobId },
1822
+ "autopilot_get_job",
1823
+ timeout ?? 15e3
1824
+ );
1825
+ }
1731
1826
  /** Subscribes to autopilot worker events. */
1732
1827
  autopilotSubscribe(timeout) {
1733
1828
  return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
@@ -1738,7 +1833,7 @@ var Client = class extends EventEmitter {
1738
1833
  return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
1739
1834
  }
1740
1835
  // ---------------------------------------------------------------------------
1741
- // RFC-229 Cron IPC methods
1836
+ // Cron IPC methods
1742
1837
  // ---------------------------------------------------------------------------
1743
1838
  /** Creates a scheduled job from natural language. */
1744
1839
  cronAdd(text, priority, timeout) {
@@ -1835,13 +1930,16 @@ export {
1835
1930
  LOOP_ASSISTANT_OUTPUT_PHASES,
1836
1931
  DEFAULT_DELIVERABLE_PHASES,
1837
1932
  EventPlanCreated,
1838
- EventExploreStarted,
1839
- EventExploreMilestone,
1840
- EventExploreStepCompleted,
1841
- EventExploreCompleted,
1842
- EventTacitusStarted,
1843
- EventTacitusGatherSummary,
1844
- EventTacitusCompleted,
1933
+ EventExplorerStarted,
1934
+ EventExplorerMilestone,
1935
+ EventExplorerStepCompleted,
1936
+ EventExplorerCompleted,
1937
+ EventDeepResearchStarted,
1938
+ EventDeepResearchProgress,
1939
+ EventDeepResearchStepCompleted,
1940
+ EventDeepResearchGatherSummary,
1941
+ EventDeepResearchCrawlSummary,
1942
+ EventDeepResearchCompleted,
1845
1943
  EventReplayComplete,
1846
1944
  EventLoopReattachedWire,
1847
1945
  EventCardReplayBegin,
@@ -1880,4 +1978,4 @@ export {
1880
1978
  inboundNeedsDeliveryAck,
1881
1979
  Client
1882
1980
  };
1883
- //# sourceMappingURL=chunk-U6RMINYV.js.map
1981
+ //# sourceMappingURL=chunk-A6WGUBQE.js.map