agentfootprint 7.24.0 → 7.25.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.
Files changed (71) hide show
  1. package/dist/adapters/hosting/agentcore.js +112 -1
  2. package/dist/adapters/hosting/agentcore.js.map +1 -1
  3. package/dist/esm/adapters/hosting/agentcore.d.ts +50 -1
  4. package/dist/esm/adapters/hosting/agentcore.js +110 -0
  5. package/dist/esm/adapters/hosting/agentcore.js.map +1 -1
  6. package/dist/esm/hosting/errors.d.ts +44 -2
  7. package/dist/esm/hosting/errors.js +63 -0
  8. package/dist/esm/hosting/errors.js.map +1 -1
  9. package/dist/esm/hosting/headers.d.ts +16 -0
  10. package/dist/esm/hosting/headers.js +24 -0
  11. package/dist/esm/hosting/headers.js.map +1 -0
  12. package/dist/esm/hosting/httpHost.d.ts +70 -4
  13. package/dist/esm/hosting/httpHost.js +211 -47
  14. package/dist/esm/hosting/httpHost.js.map +1 -1
  15. package/dist/esm/hosting/index.d.ts +23 -8
  16. package/dist/esm/hosting/index.js +21 -6
  17. package/dist/esm/hosting/index.js.map +1 -1
  18. package/dist/esm/hosting/nodeHost.d.ts +23 -0
  19. package/dist/esm/hosting/nodeHost.js +21 -1
  20. package/dist/esm/hosting/nodeHost.js.map +1 -1
  21. package/dist/esm/hosting/types.d.ts +212 -6
  22. package/dist/esm/hosting/types.js +7 -5
  23. package/dist/esm/hosting/types.js.map +1 -1
  24. package/dist/esm/hosting/webSocketConversation.d.ts +101 -0
  25. package/dist/esm/hosting/webSocketConversation.js +341 -0
  26. package/dist/esm/hosting/webSocketConversation.js.map +1 -0
  27. package/dist/esm/hosting/webSocketFrames.d.ts +164 -0
  28. package/dist/esm/hosting/webSocketFrames.js +284 -0
  29. package/dist/esm/hosting/webSocketFrames.js.map +1 -0
  30. package/dist/esm/hosting-providers.d.ts +7 -2
  31. package/dist/esm/hosting-providers.js +7 -2
  32. package/dist/esm/hosting-providers.js.map +1 -1
  33. package/dist/hosting/errors.js +66 -1
  34. package/dist/hosting/errors.js.map +1 -1
  35. package/dist/hosting/headers.js +28 -0
  36. package/dist/hosting/headers.js.map +1 -0
  37. package/dist/hosting/httpHost.js +211 -47
  38. package/dist/hosting/httpHost.js.map +1 -1
  39. package/dist/hosting/index.js +23 -6
  40. package/dist/hosting/index.js.map +1 -1
  41. package/dist/hosting/nodeHost.js +20 -0
  42. package/dist/hosting/nodeHost.js.map +1 -1
  43. package/dist/hosting/types.js +7 -5
  44. package/dist/hosting/types.js.map +1 -1
  45. package/dist/hosting/webSocketConversation.js +345 -0
  46. package/dist/hosting/webSocketConversation.js.map +1 -0
  47. package/dist/hosting/webSocketFrames.js +297 -0
  48. package/dist/hosting/webSocketFrames.js.map +1 -0
  49. package/dist/hosting-providers.js +8 -2
  50. package/dist/hosting-providers.js.map +1 -1
  51. package/dist/types/adapters/hosting/agentcore.d.ts +50 -1
  52. package/dist/types/adapters/hosting/agentcore.d.ts.map +1 -1
  53. package/dist/types/hosting/errors.d.ts +44 -2
  54. package/dist/types/hosting/errors.d.ts.map +1 -1
  55. package/dist/types/hosting/headers.d.ts +17 -0
  56. package/dist/types/hosting/headers.d.ts.map +1 -0
  57. package/dist/types/hosting/httpHost.d.ts +70 -4
  58. package/dist/types/hosting/httpHost.d.ts.map +1 -1
  59. package/dist/types/hosting/index.d.ts +23 -8
  60. package/dist/types/hosting/index.d.ts.map +1 -1
  61. package/dist/types/hosting/nodeHost.d.ts +23 -0
  62. package/dist/types/hosting/nodeHost.d.ts.map +1 -1
  63. package/dist/types/hosting/types.d.ts +212 -6
  64. package/dist/types/hosting/types.d.ts.map +1 -1
  65. package/dist/types/hosting/webSocketConversation.d.ts +102 -0
  66. package/dist/types/hosting/webSocketConversation.d.ts.map +1 -0
  67. package/dist/types/hosting/webSocketFrames.d.ts +165 -0
  68. package/dist/types/hosting/webSocketFrames.d.ts.map +1 -0
  69. package/dist/types/hosting-providers.d.ts +7 -2
  70. package/dist/types/hosting-providers.d.ts.map +1 -1
  71. package/package.json +1 -1
@@ -1,13 +1,17 @@
1
1
  /**
2
- * agentfootprint/hosting — the two ports between an agent and the place it runs.
2
+ * agentfootprint/hosting — the ports between an agent and the place it runs.
3
3
  *
4
4
  * An agent that answers one call in a script and an agent that has been up for
5
- * a month differ in two things, and only two: something has to carry requests
6
- * to it, and the conversation has to outlive the request. This subpath is those
7
- * two things as **ports**, plus local adapters that prove the ports work, plus
8
- * the composer that wires them together.
5
+ * a month differ in a few things: something has to carry requests to it,
6
+ * something sometimes has to hold a channel OPEN to it, and the conversation
7
+ * has to outlive the request. This subpath is those things as **ports**, plus
8
+ * local adapters that prove the ports work, plus the composer that wires the
9
+ * request half together.
9
10
  *
10
11
  * `AgentHost` — something can call me.
12
+ * `ConversationHost` — something can talk to me, both ways, until one of us
13
+ * ends it. `HostRequest → HostReply` is one exchange;
14
+ * some doors are not.
11
15
  * `SessionLifecycle` — the conversation outlives the request.
12
16
  * `standingAgent` — hydrate → resume-or-fresh → persist → reply.
13
17
  *
@@ -34,6 +38,17 @@
34
38
  * re-decides. Pass `server` and it attaches to a `node:http` server YOU
35
39
  * own instead of binding one — for the container that gets a single port
36
40
  * and must serve a WebSocket upgrade (or anything else) beside the agent.
41
+ * • `host.serveConversations(handler)` — the conversation door, on every
42
+ * adapter built on `httpHost` that was given a `conversationPath`.
43
+ * `nodeHost` serves it on `/conversation` with a real WebSocket
44
+ * implementation and **no dependency to install**, sharing the socket with
45
+ * `/invoke`. Frames are STRINGS at the port; what they mean is your
46
+ * protocol's business, not this port's.
47
+ * • `ConversationLimits` — the ceilings a door DECLARES
48
+ * (`maxFrameBytes`, `idleMs`, `maxPendingBytes`) so the layer above can
49
+ * chunk or heartbeat on its own protocol. The port does neither, on
50
+ * purpose: hiding a cap inside auto-chunking decides a protocol question
51
+ * for every consumer at once.
37
52
  * • `memorySessions()` — conversations in a Map, for tests and local dev.
38
53
  * • `standingAgent({ agent, sessions, host, durability? })` — the composer.
39
54
  * • `toEnvelope` / `readEnvelope` — pack a conversation, and refuse by name to
@@ -63,5 +78,5 @@ export { httpHost, headerValue } from './httpHost.js';
63
78
  export { memorySessions } from './memorySessions.js';
64
79
  export { toEnvelope, toPausedEnvelope, readEnvelope, readPausedRun, checkEnvelope, } from './envelope.js';
65
80
  export { standingAgent } from './standingAgent.js';
66
- export { requireCapability, HostClosedError, ConcurrentRunError, PauseNotCarriedError, AwaitingDecisionError, NoPendingAskError, UnreadableEnvelopeError, } from './errors.js';
81
+ export { requireCapability, HostClosedError, ConcurrentRunError, PauseNotCarriedError, AwaitingDecisionError, NoPendingAskError, UnreadableEnvelopeError, ConversationClosedError, FrameTooLargeError, } from './errors.js';
67
82
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hosting/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGnD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAStD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hosting/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGnD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAWtD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,aAAa,CAAC"}
@@ -21,6 +21,13 @@
21
21
  * instead of binding one, so a WebSocket upgrade or your own routes can share
22
22
  * the port. `close()` then detaches and drains, and your socket stays up.
23
23
  *
24
+ * **There is a third door.** `serveConversations(handler)` takes WebSocket
25
+ * upgrades on `/conversation` and hands each one to your handler as a
26
+ * `HostConversation` — a channel that stays open, for the callers that cannot
27
+ * host an inbound endpoint and dial out instead. It shares this host's socket
28
+ * with `/invoke` and `/health`, needs nothing installed, and declares what it
29
+ * caps.
30
+ *
24
31
  * **Streaming is the caller's choice, not the server's.** Send
25
32
  * `Accept: text/event-stream` and the reply is Server-Sent Events, one `chunk`
26
33
  * event per `reply.emit(...)` then a final `complete`. Send anything else and
@@ -36,6 +43,7 @@
36
43
  */
37
44
  /// <reference types="node" />
38
45
  import { type HttpHost, type HttpHostHandle, type HttpWire } from './httpHost.js';
46
+ import type { ConversationLimits } from './types.js';
39
47
  /** Options for {@link nodeHost}. */
40
48
  export interface NodeHostOptions {
41
49
  /** Port to bind. Default `8080`. Pass `0` for an ephemeral port. Refused alongside `server`. */
@@ -46,6 +54,21 @@ export interface NodeHostOptions {
46
54
  readonly invokePath?: string;
47
55
  /** Path that answers a health probe. Default `'/health'`. */
48
56
  readonly healthPath?: string;
57
+ /**
58
+ * Path that takes a conversation upgrade. Default `'/conversation'` — this
59
+ * adapter's own word, chosen the way its other two paths were, and named for
60
+ * what it carries rather than for what any runtime calls it.
61
+ */
62
+ readonly conversationPath?: string;
63
+ /**
64
+ * What the conversation door caps. Defaults to one mebibyte per frame and one
65
+ * mebibyte held before a handler subscribes — declared, so
66
+ * `host.conversationLimits` always reports what is really enforced. This
67
+ * adapter declares no `idleMs`: it does not idle a conversation out, and
68
+ * reporting a ceiling it neither imposes nor sits behind would be inventing
69
+ * a fact.
70
+ */
71
+ readonly conversationLimits?: ConversationLimits;
49
72
  /**
50
73
  * A `node:http` server **you** own, already listening. Given one, this
51
74
  * adapter attaches its two routes to it instead of binding a socket of its
@@ -21,6 +21,13 @@
21
21
  * instead of binding one, so a WebSocket upgrade or your own routes can share
22
22
  * the port. `close()` then detaches and drains, and your socket stays up.
23
23
  *
24
+ * **There is a third door.** `serveConversations(handler)` takes WebSocket
25
+ * upgrades on `/conversation` and hands each one to your handler as a
26
+ * `HostConversation` — a channel that stays open, for the callers that cannot
27
+ * host an inbound endpoint and dial out instead. It shares this host's socket
28
+ * with `/invoke` and `/health`, needs nothing installed, and declares what it
29
+ * caps.
30
+ *
24
31
  * **Streaming is the caller's choice, not the server's.** Send
25
32
  * `Accept: text/event-stream` and the reply is Server-Sent Events, one `chunk`
26
33
  * event per `reply.emit(...)` then a final `complete`. Send anything else and
@@ -34,7 +41,7 @@
34
41
  * never quietly drift apart on what `close()` drains or what a handler that
35
42
  * throws does. `types.ts` knows none of it either way.
36
43
  */
37
- import { httpHost } from './httpHost.js';
44
+ import { headerValue, httpHost, } from './httpHost.js';
38
45
  const HOST_NAME = 'nodeHost';
39
46
  /**
40
47
  * This adapter's own JSON dialect: `{ input, sessionId? }` in, `{ output }`
@@ -66,6 +73,15 @@ export const jsonWire = {
66
73
  failure: (error, code) => ({ error, ...(code !== undefined && { code }) }),
67
74
  chunk: (text) => ({ text }),
68
75
  awaiting: (pending) => ({ awaiting: pending }),
76
+ readConversation(facts) {
77
+ // A handshake has no body, so this dialect names the two places a session
78
+ // id can be: the header a server-side caller sets, and the query a BROWSER
79
+ // has to use because the WebSocket API gives it no way to set a header.
80
+ // Header wins, so a caller that sets both is not surprised by which one the
81
+ // server preferred — the same rule the request dialect uses for body-vs-header.
82
+ const sessionId = headerValue(facts, 'x-session-id') ?? facts.query.get('sessionId') ?? undefined;
83
+ return { ...(sessionId !== undefined && sessionId.length > 0 && { sessionId }) };
84
+ },
69
85
  };
70
86
  /**
71
87
  * An HTTP host for one handler.
@@ -87,6 +103,10 @@ export function nodeHost(options = {}) {
87
103
  // particular runtime's container-contract path literal.
88
104
  invokePath: options.invokePath ?? '/invoke',
89
105
  healthPath: options.healthPath ?? '/health',
106
+ conversationPath: options.conversationPath ?? '/conversation',
107
+ ...(options.conversationLimits !== undefined && {
108
+ conversationLimits: options.conversationLimits,
109
+ }),
90
110
  // Passed through as given, never defaulted: with a caller-owned server
91
111
  // there is no port to default, and a port passed WITH one is a caller who
92
112
  // believes something untrue about where their agent answers — `httpHost`
@@ -1 +1 @@
1
- {"version":3,"file":"nodeHost.js","sourceRoot":"","sources":["../../../src/hosting/nodeHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,QAAQ,EAAqD,MAAM,eAAe,CAAC;AAqC5F,MAAM,SAAS,GAAG,UAAU,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAa;IAChC,WAAW,CAAC,KAAK;QACf,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,wEAAwE;QACxE,4EAA4E;QAC5E,MAAM,QAAQ,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7F,MAAM,SAAS,GAAG,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC5D,4EAA4E;QAC5E,wEAAwE;QACxE,0CAA0C;QAC1C,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;QACrC,OAAO;YACL,KAAK;YACL,GAAG,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC;YAC7C,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,CAAC;SAC5C,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAClD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC1E,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;CAC/C,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,QAAQ,CAAC,UAA2B,EAAE;IACpD,OAAO,QAAQ,CAAC;QACd,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,QAAQ;QACd,yEAAyE;QACzE,wDAAwD;QACxD,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,SAAS;QAC3C,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,SAAS;QAC3C,uEAAuE;QACvE,0EAA0E;QAC1E,yEAAyE;QACzE,0DAA0D;QAC1D,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;QACzD,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrE,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"nodeHost.js","sourceRoot":"","sources":["../../../src/hosting/nodeHost.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
1
  /**
2
- * hosting/types — the two ports an agent needs to stand up and stay up.
2
+ * hosting/types — the ports an agent needs to stand up and stay up.
3
3
  *
4
- * `AgentHost` is "something can call me". `SessionLifecycle` is "the
5
- * conversation outlives the request". Both are deliberately written in the
6
- * vocabulary every transport and every store already has — an input, a reply,
7
- * a session id, a stored blob — and in nothing else.
4
+ * `AgentHost` is "something can call me". `ConversationHost` is "something can
5
+ * TALK to me" — a door that stays open, because `HostRequest → HostReply` is
6
+ * one exchange and some doors are not. `SessionLifecycle` is "the conversation
7
+ * outlives the request". All three are deliberately written in the vocabulary
8
+ * every transport and every store already has — an input, a reply, a frame, a
9
+ * session id, a stored blob — and in nothing else.
8
10
  *
9
11
  * **The rule these types are written under:** no runtime, product or protocol
10
12
  * gets a field, a name or an assumption here. A port shaped around one
@@ -22,6 +24,8 @@ import type { Agent } from '../core/Agent.js';
22
24
  import type { CheckInRequest } from '../core/checkin.js';
23
25
  import type { MiddlewareAsk } from '../core/pause.js';
24
26
  import type { AgentRunCheckpoint } from '../core/runCheckpoint.js';
27
+ import type { Unsubscribe } from '../events/dispatcher.js';
28
+ export type { Unsubscribe };
25
29
  /**
26
30
  * Something a host can do BEYOND the baseline of "accept a request, deliver one
27
31
  * reply". Read it from {@link AgentHost.capabilities} and branch on it — never
@@ -32,8 +36,18 @@ import type { AgentRunCheckpoint } from '../core/runCheckpoint.js';
32
36
  * of a transport that does not exist yet: a capability nobody implements is a
33
37
  * promise the library cannot keep, and pre-minting one for an imagined future
34
38
  * transport would bake that transport's assumptions in before it arrives.
39
+ *
40
+ * - `'streaming'` — the caller SEES `reply.emit(...)` pieces as they arrive.
41
+ * - `'conversation'` — this host can also carry a two-way channel that stays
42
+ * open ({@link ConversationHost.serveConversations}). It joined the union
43
+ * when two shipped adapters honoured it, not when it was imagined.
44
+ *
45
+ * Declared at CONSTRUCTION and static thereafter, which is a constraint worth
46
+ * knowing about: a capability whose truth depended on what happened to be
47
+ * installed at call time could not be declared here honestly, so an adapter
48
+ * that can only sometimes keep a promise does not make it.
35
49
  */
36
- export type HostCapability = 'streaming';
50
+ export type HostCapability = 'streaming' | 'conversation';
37
51
  /**
38
52
  * One inbound request, as the transport described it.
39
53
  */
@@ -164,6 +178,198 @@ export interface AgentHost {
164
178
  /** Start serving. Resolves once the host is actually live. */
165
179
  serve(handler: HostHandler): Promise<HostHandle>;
166
180
  }
181
+ /**
182
+ * A door that stays open: one session-scoped, two-way channel.
183
+ *
184
+ * The distinction this type exists for, in the words of the field report that
185
+ * bought it: **`HostRequest → HostReply` is one exchange, and this door is a
186
+ * conversation.** A request has one reply and then it is over. A conversation
187
+ * has neither side taking turns by rule, no reply count, and an end that either
188
+ * side can call.
189
+ *
190
+ * ── Frames are STRINGS here, deliberately ───────────────────────────────────
191
+ * What the frames MEAN is the consumer's contract, not this port's: one
192
+ * consumer pushes tool calls down the channel, another speaks a standardized
193
+ * agent↔UI protocol, a third exchanges long-running task updates. JSON is what
194
+ * all three happen to use and none of them agree on beyond that, so the port
195
+ * carries text and stays out of it. Binary is a capability question, deferred
196
+ * until a consumer needs it rather than guessed at now.
197
+ *
198
+ * ── What is NOT here ────────────────────────────────────────────────────────
199
+ * No authentication, no chunking, no heartbeat, no protocol framing. See
200
+ * {@link ConversationLimits} for why the last two are absences with a reason
201
+ * rather than gaps.
202
+ */
203
+ export interface HostConversation {
204
+ /**
205
+ * The conversation this channel CLAIMS to belong to — caller data, exactly as
206
+ * the transport declared it, and **not identity**. The whole of
207
+ * {@link HostRequest.sessionId}'s warning applies here word for word: anyone
208
+ * who can reach the host can put any string here, including someone else's.
209
+ *
210
+ * A conversation and a request carrying the same string are the same
211
+ * session's, as far as this port is concerned. What that entitles either of
212
+ * them to is yours to decide, above the port.
213
+ */
214
+ readonly sessionId?: string;
215
+ /**
216
+ * Transport headers with lower-cased names, as delivered — so a handler can
217
+ * map its own conventions without the port guessing which ones matter.
218
+ *
219
+ * This is also where an adapter puts credentials that its transport spells
220
+ * some other way: a bearer token a browser could only send as a subprotocol
221
+ * arrives here as an ordinary `authorization` header, because a port field
222
+ * spelled the way one vendor spells it is how a port stops being one.
223
+ */
224
+ readonly headers?: Readonly<Record<string, string>>;
225
+ /**
226
+ * Host → far side. One frame, delivered whole.
227
+ *
228
+ * Refuses BY NAME rather than dropping quietly in two cases: a conversation
229
+ * that has ended (`ConversationClosedError`), and a frame past the ceiling the
230
+ * adapter declared (`FrameTooLargeError`). A dropped frame on a channel that
231
+ * looks open is the failure mode this port exists to make impossible.
232
+ */
233
+ send(frame: string): void;
234
+ /**
235
+ * Far side → host. Returns an unsubscribe.
236
+ *
237
+ * Frames that arrive BEFORE the first subscriber are held and delivered to
238
+ * it, up to the bound the adapter declares
239
+ * ({@link ConversationLimits.maxPendingBytes}) — an `async` handler that
240
+ * awaits anything before subscribing would otherwise silently lose the far
241
+ * side's opening frame, which on a channel whose first frame is a greeting is
242
+ * every conversation.
243
+ */
244
+ onFrame(cb: (frame: string) => void): Unsubscribe;
245
+ /**
246
+ * The end, delivered exactly once per subscriber — including to a subscriber
247
+ * that arrives after it already happened, which is answered immediately
248
+ * rather than never.
249
+ */
250
+ onClose(cb: (reason: ConversationClose) => void): Unsubscribe;
251
+ /**
252
+ * End it politely: flush what is queued, tell the far side, then stop.
253
+ * Idempotent — the first call owns the ending and later ones do nothing.
254
+ */
255
+ close(reason?: string): void;
256
+ }
257
+ /**
258
+ * How a conversation ended, in terms every transport can answer.
259
+ *
260
+ * `by` is the fact a consumer branches on and the reason there is no numeric
261
+ * code here: what all three of "a browser-parked channel", "a UI protocol" and
262
+ * "a long-running task exchange" need to know is whether the far side said
263
+ * goodbye, whether we did, or whether it broke — and a transport's own numbers
264
+ * answer that only if you already know that transport. Adapters render their
265
+ * own vocabulary (a close code, a timeout, a ceiling) into {@link reason}.
266
+ */
267
+ export interface ConversationClose {
268
+ /**
269
+ * - `'far-side'` — they ended it.
270
+ * - `'host'` — we did, through {@link HostConversation.close}.
271
+ * - `'transport'` — nobody ended it; it broke or timed out.
272
+ */
273
+ readonly by: 'far-side' | 'host' | 'transport';
274
+ /** What was said about it, when anything was. */
275
+ readonly reason?: string;
276
+ }
277
+ /**
278
+ * What you hand {@link ConversationHost.serveConversations} — called once per
279
+ * conversation, with that conversation.
280
+ *
281
+ * Throwing ends THAT conversation with a stated reason and never the host: one
282
+ * caller's bad frame is not an outage.
283
+ */
284
+ export type ConversationHandler = (conversation: HostConversation) => void | Promise<void>;
285
+ /**
286
+ * The ceilings a door imposes, **declared rather than discovered**.
287
+ *
288
+ * ── Why the port does not just handle them ──────────────────────────────────
289
+ * A transport that caps frame size or idles out must SAY so, and then get out
290
+ * of the way. Hiding a 32KB cap inside auto-chunking would be the adapter
291
+ * deciding a protocol question for every consumer at once — how a message is
292
+ * split, how the pieces are numbered, how the far side knows the last one has
293
+ * landed — and those answers differ per consumer. Same for liveness: a
294
+ * heartbeat is frames on somebody's protocol, and inventing them puts bytes on
295
+ * the wire that the consumer's parser never agreed to.
296
+ *
297
+ * So the port's job is to make the ceiling VISIBLE and let the layer above act:
298
+ * chunk above the port, heartbeat above the port.
299
+ *
300
+ * ── Enforced vs reported ────────────────────────────────────────────────────
301
+ * A door enforces what it IS and reports what it SITS BEHIND, and the doc on
302
+ * each field says which. Absent means "no ceiling this adapter knows of", never
303
+ * "no ceiling" — the runtime in front of you may have one it never told us
304
+ * about.
305
+ */
306
+ export interface ConversationLimits {
307
+ /**
308
+ * Largest single frame this door carries, in bytes of UTF-8.
309
+ *
310
+ * **Enforced, both directions**, by the door that declares it: an inbound
311
+ * frame past it ends the conversation with a stated reason, and
312
+ * {@link HostConversation.send} past it refuses by name instead of
313
+ * truncating or silently splitting.
314
+ *
315
+ * A frame the transport delivered in pieces counts in TOTAL — the port's
316
+ * frame is the whole message, not the transport's packet, so fragmentation
317
+ * cannot be used to walk around the ceiling.
318
+ */
319
+ readonly maxFrameBytes?: number;
320
+ /**
321
+ * How long the transport tolerates silence before it closes the channel.
322
+ *
323
+ * **Reported, not imposed.** The door declaring it usually is not the thing
324
+ * enforcing it — a runtime's front door idles a socket out long before the
325
+ * process inside notices — and a consumer that needs the channel to stay up
326
+ * sends its own heartbeat frames on its own protocol. Making that possible is
327
+ * the whole reason this number is written down.
328
+ */
329
+ readonly idleMs?: number;
330
+ /**
331
+ * How much the door holds for you before the first
332
+ * {@link HostConversation.onFrame} subscriber exists, in bytes.
333
+ *
334
+ * **Enforced**, and a ceiling on the DOOR rather than on the transport: the
335
+ * pre-subscribe buffer that stops an `async` handler from losing the opening
336
+ * frame is a queue somebody else fills and this process pays for, so it gets
337
+ * a number and a stated overflow instead of growing until the host dies. Past
338
+ * it, the conversation ends with a reason naming this bound.
339
+ *
340
+ * Bounded in BYTES rather than in frames on purpose: a frame count would
341
+ * still admit `count × maxFrameBytes` of memory, which is the same unbounded
342
+ * queue with an extra step.
343
+ */
344
+ readonly maxPendingBytes?: number;
345
+ }
346
+ /**
347
+ * The port: something that can carry conversations to one handler.
348
+ *
349
+ * It sits BESIDE {@link AgentHost} rather than inside it, because a transport
350
+ * that can carry a request cannot necessarily carry a conversation, and one
351
+ * that carries conversations need not answer requests at all. A host that does
352
+ * both implements both and declares `'conversation'` in
353
+ * {@link AgentHost.capabilities}.
354
+ */
355
+ export interface ConversationHost {
356
+ /** Which adapter this is. Every refusal names it. */
357
+ readonly name: string;
358
+ /** What this adapter can do beyond the baseline. Feature-detect; never assume. */
359
+ readonly capabilities: readonly HostCapability[];
360
+ /**
361
+ * What this door caps, as declared facts. Absent means this adapter knows of
362
+ * no ceiling — never that there is none.
363
+ */
364
+ readonly conversationLimits?: ConversationLimits;
365
+ /**
366
+ * Start taking conversations. Resolves once the door is actually open.
367
+ *
368
+ * The handle's `close()` ends every live conversation politely and then
369
+ * releases the door.
370
+ */
371
+ serveConversations(handler: ConversationHandler): Promise<HostHandle>;
372
+ }
167
373
  /**
168
374
  * A session packed for storage.
169
375
  *
@@ -1,10 +1,12 @@
1
1
  /**
2
- * hosting/types — the two ports an agent needs to stand up and stay up.
2
+ * hosting/types — the ports an agent needs to stand up and stay up.
3
3
  *
4
- * `AgentHost` is "something can call me". `SessionLifecycle` is "the
5
- * conversation outlives the request". Both are deliberately written in the
6
- * vocabulary every transport and every store already has — an input, a reply,
7
- * a session id, a stored blob — and in nothing else.
4
+ * `AgentHost` is "something can call me". `ConversationHost` is "something can
5
+ * TALK to me" — a door that stays open, because `HostRequest → HostReply` is
6
+ * one exchange and some doors are not. `SessionLifecycle` is "the conversation
7
+ * outlives the request". All three are deliberately written in the vocabulary
8
+ * every transport and every store already has — an input, a reply, a frame, a
9
+ * session id, a stored blob — and in nothing else.
8
10
  *
9
11
  * **The rule these types are written under:** no runtime, product or protocol
10
12
  * gets a field, a name or an assumption here. A port shaped around one
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/hosting/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/hosting/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * hosting/webSocketConversation — one upgraded socket, presented as a
3
+ * {@link HostConversation}.
4
+ *
5
+ * This is the whole adapter side of the conversation port: it takes the
6
+ * `'upgrade'` event a `node:http` server hands it, answers the handshake, and
7
+ * turns the frames flowing over the socket into the six-member port a handler
8
+ * sees. Everything protocol-shaped lives in `webSocketFrames.ts`; everything
9
+ * port-shaped lives in `types.ts`; this file is the join.
10
+ *
11
+ * ── The laws it keeps, all of them pinned by tests ───────────────────────────
12
+ * - **A path this door does not own is not touched.** `node:http` calls EVERY
13
+ * `'upgrade'` listener for every upgrade, exactly as it calls every
14
+ * `'request'` listener — so a caller's own protocol lives beside this one on
15
+ * the same socket. Whether an unclaimed path gets an answer is the caller's
16
+ * business on a server they own, and this door's only on a server it owns.
17
+ * - **Frames that arrive before the handler subscribes are held, up to a
18
+ * declared bound.** An `async` handler that awaits before calling `onFrame`
19
+ * would otherwise lose the far side's opening frame. The bound is in BYTES
20
+ * and overflow ends the conversation with a stated reason, because an
21
+ * unbounded queue somebody else fills is a way to kill this process, and an
22
+ * undeclared ceiling is the exact thing the declared-ceilings rule exists to
23
+ * forbid.
24
+ * - **`close()` ends every live conversation before the socket is released.**
25
+ * Measured, not assumed: an upgraded socket keeps `server.close()` waiting
26
+ * forever, so a door that let go of its conversations would hang the whole
27
+ * shutdown.
28
+ * - **The port's frame is the whole message.** A message the transport
29
+ * delivered in fragments counts against `maxFrameBytes` in total, so
30
+ * fragmentation cannot be used to walk around the ceiling.
31
+ *
32
+ * Pattern: Adapter. Role: outer ring, one transport.
33
+ */
34
+ /// <reference types="node" />
35
+ /// <reference types="node" />
36
+ import type { IncomingMessage } from 'node:http';
37
+ import type { Duplex } from 'node:stream';
38
+ import type { ConversationHandler, ConversationLimits } from './types.js';
39
+ /**
40
+ * What a wire read out of one handshake — the conversation half of a
41
+ * deployment's dialect.
42
+ *
43
+ * There is no body to read here, which is the whole difference from a request:
44
+ * a handshake is a URL and some headers, so a dialect that wants a session id
45
+ * or a credential has to find it in those.
46
+ */
47
+ export interface ConversationHandshake {
48
+ /** The session this conversation claims. Caller data; never identity. */
49
+ readonly sessionId?: string;
50
+ /**
51
+ * Headers to merge OVER the raw lower-cased ones the transport delivered.
52
+ *
53
+ * This is how an adapter maps its own spelling into the port's vocabulary —
54
+ * a credential a browser could only send as a subprotocol becomes an ordinary
55
+ * `authorization` header here. The raw headers are never removed, so nothing
56
+ * a mapping did not understand is lost.
57
+ */
58
+ readonly headers?: Readonly<Record<string, string>>;
59
+ /**
60
+ * The subprotocol to echo in the 101, when this dialect selects one.
61
+ *
62
+ * Selecting is the wire's job because only the wire read the offer. Echoing
63
+ * something the client did not offer makes the client fail the connection.
64
+ */
65
+ readonly protocol?: string;
66
+ }
67
+ /** Everything the door needs to answer one upgrade. */
68
+ export interface ConversationDoorOptions {
69
+ /** The adapter's name — every refusal carries it. */
70
+ readonly hostName: string;
71
+ /** The path this door owns. Anything else is not ours. */
72
+ readonly path: string;
73
+ /** The ceilings this door declares, already defaulted. */
74
+ readonly limits: ConversationLimits;
75
+ /** This deployment's handshake dialect. Absent ⇒ raw headers and no session. */
76
+ readonly readConversation?: (facts: HandshakeFacts) => ConversationHandshake;
77
+ /** Where a new conversation goes. */
78
+ readonly handler: ConversationHandler;
79
+ /** Whether the host is still taking conversations — false after `close()`. */
80
+ readonly accepting: () => boolean;
81
+ }
82
+ /** What a handshake dialect may read. Deliberately the same shape a request wire gets, minus the body. */
83
+ export interface HandshakeFacts {
84
+ /** Header names lower-cased. */
85
+ readonly headers: Readonly<Record<string, string>>;
86
+ /** The query string, already parsed — where a browser has to put things it cannot header. */
87
+ readonly query: URLSearchParams;
88
+ }
89
+ /** A door: hand it upgrades, close it when you are done. */
90
+ export interface ConversationDoor {
91
+ /**
92
+ * Take one upgrade, or decline it. Returns whether this door claimed the
93
+ * path — the caller decides what an unclaimed upgrade deserves.
94
+ */
95
+ handleUpgrade(request: IncomingMessage, socket: Duplex): boolean;
96
+ /** End every live conversation politely, then resolve. */
97
+ closeAll(reason: string): Promise<void>;
98
+ /** How many conversations are open right now. */
99
+ readonly liveCount: number;
100
+ }
101
+ export declare function conversationDoor(options: ConversationDoorOptions): ConversationDoor;