@byokit/herdr 0.1.0-next.dbe7142 → 0.1.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/device.d.ts CHANGED
@@ -1,15 +1,44 @@
1
1
  import type { DeviceLink } from '@byokit/link';
2
- import type { AgentRef, BlockedAgent, HerdrSnapshot, HerdrState, PromptReceipt, StartAgent } from './types.ts';
2
+ import type { AgentRef, AgentStatus, BlockedAgent, HerdrEventName, HerdrEventOf, HerdrMethod, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, PromptReceipt, StartAgent } from './types.ts';
3
+ export type { HerdrEventName, HerdrEventOf, HerdrEvents, HerdrMethod, HerdrMethods, HerdrParams, HerdrResult, HerdrSubscription, } from './types.ts';
4
+ /** One `hd.events` frame (7.2); `raw` carries a line that was not JSON. */
5
+ export type HerdrLinkEvent = {
6
+ type: 'snapshot';
7
+ snapshot: HerdrSnapshot;
8
+ } | {
9
+ type: 'blocked';
10
+ change: 'added' | 'resolved';
11
+ blocked: BlockedAgent;
12
+ } | {
13
+ type: 'raw';
14
+ line: string;
15
+ };
16
+ /** A terminal over the link (7.2): Herdr's NDJSON frames both ways. */
17
+ export type DeviceTerminal = {
18
+ ready: Promise<void>;
19
+ exited: Promise<{
20
+ reason: string | null;
21
+ }>;
22
+ onFrame(fn: (line: string) => void): () => void;
23
+ send(line: string): void;
24
+ close(): void;
25
+ };
3
26
  export { openNotice } from './notices.ts';
27
+ export { agentWords, stateWords, words, WORDS, type WordKey } from './words.ts';
4
28
  export declare function herdrDevice(link: DeviceLink): {
5
29
  state(): Promise<{
6
30
  state: HerdrState;
7
31
  words: string;
8
32
  }>;
9
33
  tree(): Promise<HerdrSnapshot>;
34
+ agentKinds(): Promise<string[]>;
10
35
  startAgent(o: StartAgent): Promise<AgentRef>;
11
36
  prompt(paneId: string, text: string): Promise<PromptReceipt>;
12
37
  keys(paneId: string, keys: string[]): Promise<void>;
38
+ wait(paneId: string, o: {
39
+ until?: AgentStatus[];
40
+ timeoutMs: number;
41
+ }): Promise<AgentStatus>;
13
42
  read(paneId: string, o?: {
14
43
  source?: 'visible' | 'recent' | 'recent_unwrapped' | 'detection';
15
44
  lines?: number;
@@ -24,17 +53,14 @@ export declare function herdrDevice(link: DeviceLink): {
24
53
  tab?: string;
25
54
  workspace?: string;
26
55
  }): Promise<void>;
27
- events(): AsyncIterable<unknown>;
56
+ events(): AsyncIterable<HerdrLinkEvent>;
57
+ subscribe<E extends HerdrEventName>(subs: HerdrSubscription<E>[], on: (e: HerdrEventOf<E>) => void, onError?: (message: string) => void): () => void;
28
58
  registerNotices(seed: Uint8Array): Promise<void>;
29
59
  openNotice(data: Record<string, unknown>, seed: Uint8Array): BlockedAgent | null;
30
- call(method: string, params?: unknown): Promise<unknown>;
60
+ call<M extends HerdrMethod>(method: M, params: HerdrParams<M>): Promise<HerdrResult<M>>;
31
61
  terminal(paneId: string, o: {
32
62
  mode: 'control' | 'observe';
33
63
  cols: number;
34
64
  rows: number;
35
- }): {
36
- onFrame(fn: (line: string) => void): () => void;
37
- send(line: string): void;
38
- close(): void;
39
- };
65
+ }): DeviceTerminal;
40
66
  };
package/dist/device.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { openNotice } from "./notices.js";
2
+ export { agentWords, stateWords, words, WORDS } from "./words.js";
2
3
  import { boxPublicKeyB64, openNotice } from "./notices.js";
3
4
  const decodeLines = () => {
4
5
  const text = new TextDecoder();
@@ -18,42 +19,73 @@ const decodeLines = () => {
18
19
  },
19
20
  };
20
21
  };
21
- /** A link stream as newline-delimited JSON frames; unparsed lines surface as `{ type: 'raw', line }`. */
22
- async function* streamFrames(link, op, args) {
23
- const s = await link.stream(op, args);
24
- const ready = [];
25
- let wake;
26
- let ended = false;
27
- const lines = decodeLines();
28
- const emit = (frame) => {
29
- ready.push(frame);
30
- wake?.();
31
- wake = undefined;
32
- };
33
- s.onData = (chunk) => {
34
- for (const line of lines.push(chunk)) {
35
- try {
36
- emit(JSON.parse(line));
37
- }
38
- catch {
39
- emit({ type: 'raw', line });
40
- }
41
- }
42
- };
43
- s.onEnd = () => {
44
- for (const line of lines.flush())
45
- emit({ type: 'raw', line });
46
- ended = true;
47
- wake?.();
48
- wake = undefined;
22
+ /**
23
+ * A link stream as newline-delimited JSON frames; unparsed lines surface as `{ type: 'raw', line }`. A plain
24
+ * iterator rather than an async generator: `return()` (a `break`, or a view that stops watching) ends the link
25
+ * stream even while a `next()` waits for the next frame, which a generator parked at an await cannot do.
26
+ */
27
+ function streamFrames(link, op, args) {
28
+ return {
29
+ [Symbol.asyncIterator]() {
30
+ const ready = [];
31
+ const lines = decodeLines();
32
+ let s;
33
+ let opening;
34
+ let failed;
35
+ let ended = false;
36
+ let closed = false;
37
+ let wake;
38
+ const rouse = () => { wake?.(); wake = undefined; };
39
+ const emit = (frame) => { ready.push(frame); rouse(); };
40
+ const open = () => opening ??= link.stream(op, args).then((opened) => {
41
+ if (closed) {
42
+ opened.end();
43
+ return;
44
+ }
45
+ s = opened;
46
+ opened.onData = (chunk) => {
47
+ for (const line of lines.push(chunk)) {
48
+ try {
49
+ emit(JSON.parse(line));
50
+ }
51
+ catch {
52
+ emit({ type: 'raw', line });
53
+ }
54
+ }
55
+ };
56
+ opened.onEnd = () => {
57
+ for (const line of lines.flush())
58
+ emit({ type: 'raw', line });
59
+ ended = true;
60
+ rouse();
61
+ };
62
+ }, (error) => { failed = { error }; rouse(); });
63
+ return {
64
+ async next() {
65
+ void open();
66
+ for (;;) {
67
+ if (closed)
68
+ return { value: undefined, done: true };
69
+ if (ready.length > 0)
70
+ return { value: ready.shift(), done: false };
71
+ if (failed)
72
+ throw failed.error;
73
+ if (ended)
74
+ return { value: undefined, done: true };
75
+ await new Promise((resolve) => { wake = resolve; });
76
+ }
77
+ },
78
+ async return() {
79
+ if (!closed) {
80
+ closed = true;
81
+ s?.end();
82
+ rouse();
83
+ }
84
+ return { value: undefined, done: true };
85
+ },
86
+ };
87
+ },
49
88
  };
50
- for (;;) {
51
- while (ready.length > 0)
52
- yield ready.shift();
53
- if (ended)
54
- return;
55
- await new Promise((resolve) => { wake = resolve; });
56
- }
57
89
  }
58
90
  const terminalHandle = (opening) => {
59
91
  const listeners = new Set();
@@ -61,8 +93,29 @@ const terminalHandle = (opening) => {
61
93
  let s;
62
94
  const queued = [];
63
95
  let dead = false;
64
- // The frozen H7 signature returns the handle synchronously; the stream opens underneath and
65
- // sends made before it opens queue until then (frames cannot arrive before the open anyway).
96
+ let framed = false;
97
+ let markReady;
98
+ let failReady;
99
+ let markExited;
100
+ const ready = new Promise((resolve, reject) => { markReady = resolve; failReady = reject; });
101
+ const exited = new Promise((resolve) => { markExited = resolve; });
102
+ ready.catch(() => { }); // an app that never awaits ready must not see an unhandled rejection
103
+ const deliver = (line) => {
104
+ if (!framed) {
105
+ framed = true;
106
+ markReady();
107
+ }
108
+ for (const fn of listeners)
109
+ fn(line);
110
+ };
111
+ const finish = (reason) => {
112
+ dead = true;
113
+ if (!framed)
114
+ failReady(new Error(reason ?? 'herdr: the terminal ended before its first frame'));
115
+ markExited({ reason });
116
+ };
117
+ // The handle returns synchronously; the stream opens underneath and sends made before it opens
118
+ // queue until then (frames cannot arrive before the open anyway).
66
119
  opening.then((opened) => {
67
120
  if (dead) {
68
121
  opened.end();
@@ -70,32 +123,79 @@ const terminalHandle = (opening) => {
70
123
  }
71
124
  s = opened;
72
125
  opened.onData = (chunk) => {
73
- for (const line of lines.push(chunk)) {
74
- for (const fn of listeners)
75
- fn(line);
76
- }
126
+ for (const line of lines.push(chunk))
127
+ deliver(line);
77
128
  };
78
- opened.onEnd = () => {
79
- for (const line of lines.flush()) {
80
- for (const fn of listeners)
81
- fn(line);
82
- }
129
+ opened.onEnd = (error) => {
130
+ for (const line of lines.flush())
131
+ deliver(line);
132
+ finish(error ?? null);
83
133
  };
84
134
  for (const line of queued.splice(0))
85
135
  void opened.write(`${line}\n`).catch(() => { });
86
- }, () => { dead = true; });
136
+ }, (e) => { finish(e instanceof Error ? e.message : 'failed'); });
87
137
  return {
138
+ ready,
139
+ exited,
88
140
  onFrame: (fn) => {
89
141
  listeners.add(fn);
90
142
  return () => { listeners.delete(fn); };
91
143
  },
92
144
  send: (line) => {
93
- if (s !== undefined)
145
+ if (s !== undefined && !dead)
94
146
  void s.write(`${line}\n`).catch(() => { });
95
147
  else if (!dead)
96
148
  queued.push(line);
97
149
  },
98
- close: () => { dead = true; s?.end(); },
150
+ close: () => {
151
+ if (dead)
152
+ return;
153
+ s?.end();
154
+ finish(null);
155
+ },
156
+ };
157
+ };
158
+ /** `hd.subscribe`: one parsed frame per Herdr event; a host end with words reaches `onError`. */
159
+ const subscription = (opening, on, onError) => {
160
+ const lines = decodeLines();
161
+ let s;
162
+ let stopped = false;
163
+ const deliver = (line) => {
164
+ let frame;
165
+ try {
166
+ frame = JSON.parse(line);
167
+ }
168
+ catch {
169
+ return;
170
+ }
171
+ if (!stopped)
172
+ on(frame);
173
+ };
174
+ opening.then((opened) => {
175
+ if (stopped) {
176
+ opened.end();
177
+ return;
178
+ }
179
+ s = opened;
180
+ opened.onData = (chunk) => { for (const line of lines.push(chunk))
181
+ deliver(line); };
182
+ opened.onEnd = (error) => {
183
+ for (const line of lines.flush())
184
+ deliver(line);
185
+ if (!stopped && error !== undefined)
186
+ onError?.(error);
187
+ stopped = true;
188
+ };
189
+ }, (e) => {
190
+ if (!stopped)
191
+ onError?.(e instanceof Error ? e.message : 'failed');
192
+ stopped = true;
193
+ });
194
+ return () => {
195
+ if (stopped)
196
+ return;
197
+ stopped = true;
198
+ s?.end();
99
199
  };
100
200
  };
101
201
  export function herdrDevice(link) {
@@ -104,11 +204,13 @@ export function herdrDevice(link) {
104
204
  return {
105
205
  state: () => link.request('hd.state', {}),
106
206
  tree: () => link.request('hd.tree', {}),
207
+ agentKinds: () => link.request('hd.kinds', {}),
107
208
  startAgent: (o) => link.request('hd.agent.start', o),
108
209
  prompt: (paneId, text) => link.request('hd.prompt', { paneId, text }),
109
210
  keys: async (paneId, keys) => {
110
211
  await link.request('hd.keys', { paneId, keys });
111
212
  },
213
+ wait: (paneId, o) => link.request('hd.wait', { paneId, ...o }),
112
214
  read: (paneId, o) => link.request('hd.read', { paneId, ...o }),
113
215
  blocked: () => link.request('hd.blocked', {}),
114
216
  answer: async (paneId, keys, revision) => {
@@ -118,6 +220,7 @@ export function herdrDevice(link) {
118
220
  await link.request('hd.close', o);
119
221
  },
120
222
  events: () => streamFrames(link, 'hd.events', {}),
223
+ subscribe: (subs, on, onError) => subscription(link.stream('hd.subscribe', { subs }), on, onError),
121
224
  registerNotices: async (seed) => {
122
225
  await link.request('hd.notices.register', { boxPublicKey: boxPublicKeyB64(seed) });
123
226
  },
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { HERDR_VERSION, HERDR_PROTOCOL } from './constants.ts';
2
2
  export { HerdrKit } from './kit.ts';
3
- export type { AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrMethods, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrTransport, PromptReceipt, StartAgent, TerminalSession, } from './types.ts';
3
+ export { agentWords, stateWords, words, WORDS, type WordKey } from './words.ts';
4
+ export type { AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrMethods, HerdrParams, HerdrProtocolRange, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, HerdrTransport, PromptReceipt, StartAgent, TerminalSession, } from './types.ts';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@
2
2
  // host-side link adapter in `./link`; the fake Herdr and contract suite arrive with H6 in `./testing`.
3
3
  export { HERDR_VERSION, HERDR_PROTOCOL } from "./constants.js";
4
4
  export { HerdrKit } from "./kit.js";
5
+ export { agentWords, stateWords, words, WORDS } from "./words.js";
package/dist/kit.d.ts CHANGED
@@ -1,13 +1,17 @@
1
- import type { AgentRef, AgentStatus, BlockedAgent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, PromptReceipt, StartAgent, TerminalSession } from './types.ts';
1
+ import type { AgentRef, AgentStatus, BlockedAgent, HerdrEvent, HerdrEventName, HerdrEventOf, HerdrKitOptions, HerdrMethod, HerdrParams, HerdrResult, HerdrSnapshot, HerdrState, HerdrSubscription, PromptReceipt, StartAgent, TerminalSession } from './types.ts';
2
2
  export declare class HerdrKit {
3
3
  private readonly supervisor;
4
4
  private transport?;
5
5
  private current;
6
6
  private tree;
7
7
  private listeners;
8
+ private rawListeners;
9
+ private statusReadyPromise;
10
+ private statusReadyResolve?;
8
11
  private stops;
9
12
  private statusStops;
10
13
  private generation;
14
+ private statusEpoch;
11
15
  private readonly o;
12
16
  private readonly callAny;
13
17
  private readonly agents;
@@ -15,14 +19,20 @@ export declare class HerdrKit {
15
19
  constructor(o: HerdrKitOptions);
16
20
  get state(): HerdrState;
17
21
  private publish;
22
+ private emitRaw;
18
23
  private update;
19
24
  private bootstrap;
25
+ /**
26
+ * Connect and bootstrap. Non-fatal: a rejected `start()` (e.g. `failed/socket` while Herdr is
27
+ * down) leaves the kit stopped, and `start()` may be called again afterwards — the host comes up
28
+ * while Herdr is down by retrying `start()` in a backoff loop until it reaches `ready`.
29
+ */
20
30
  start(): Promise<void>;
21
31
  stop(): Promise<void>;
22
32
  call<M extends HerdrMethod>(method: M, params: HerdrParams<M>, o?: {
23
33
  timeoutMs?: number;
24
34
  }): Promise<HerdrResult<M>>;
25
- subscribe<E extends HerdrEventName>(subs: HerdrSubscription<E>[], on: (e: HerdrEventOf<E>) => void): () => void;
35
+ subscribe<E extends HerdrEventName>(subs: HerdrSubscription<E>[], on: (e: HerdrEventOf<E>) => void, onError?: (code: string, message: string) => void): () => void;
26
36
  cli(args: string[], o?: {
27
37
  timeoutMs?: number;
28
38
  }): Promise<{
@@ -38,6 +48,21 @@ export declare class HerdrKit {
38
48
  }): TerminalSession;
39
49
  snapshot(): HerdrSnapshot;
40
50
  onChange(fn: (s: HerdrSnapshot) => void): () => void;
51
+ /**
52
+ * Raw event tap (K3): delivers every event arriving on the kit's own batch and per-pane
53
+ * status sockets, with wire payloads intact (`pane.moved.previous_pane_id`, `workspace.*`).
54
+ * Fires on arrival — before the snapshot update/refresh for that event — so a listener sees
55
+ * the frame even when it triggers a re-bootstrap. Buffered replays never re-fire. Sockets
56
+ * opened via `subscribe()` are NOT tapped; `onEvent` itself opens no socket.
57
+ */
58
+ onEvent(fn: (e: HerdrEvent) => void): () => void;
59
+ /**
60
+ * Ready signal for the status-watch set (K3): resolves when the latest bootstrap's per-pane
61
+ * `pane.agent_status_changed` subscriptions have acked (immediately when no pane needs one).
62
+ * Await after `start()` — and after any reconnect-driven re-bootstrap — before relying on
63
+ * status events instead of opening a second watch.
64
+ */
65
+ statusWatchReady(): Promise<void>;
41
66
  startAgent(o: StartAgent): Promise<AgentRef>;
42
67
  prompt(target: AgentRef, text: string, o?: {
43
68
  wait?: {
@@ -69,5 +94,6 @@ export declare class HerdrKit {
69
94
  agentKinds(): Promise<string[]>;
70
95
  installedAgentKinds(kinds: readonly string[], o: {
71
96
  path: string[];
97
+ aliases?: Record<string, string[]>;
72
98
  }): string[];
73
99
  }