akanjs 3.0.0-beta.1 → 3.0.0-beta.11

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 (143) hide show
  1. package/common/index.ts +38 -0
  2. package/common/pathGet.ts +12 -2
  3. package/common/pathSet.ts +2 -3
  4. package/common/toPathSegments.ts +10 -0
  5. package/common/tunnelWire.ts +355 -0
  6. package/common/types.ts +5 -1
  7. package/common/websocketHeartbeat.ts +27 -0
  8. package/dictionary/agentTurn.dictionary.ts +6 -2
  9. package/dictionary/base.dictionary.ts +5 -0
  10. package/fetch/agentTurn.ts +1 -1
  11. package/fetch/client/fetchClient.ts +20 -2
  12. package/fetch/client/httpClient.ts +4 -0
  13. package/fetch/client/wsClient.ts +46 -6
  14. package/index.ts +6 -0
  15. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  16. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  17. package/package.json +6 -1
  18. package/server/di/diLifecycle.ts +5 -1
  19. package/server/logging/logStreamRoute.ts +1 -1
  20. package/server/mcp/McpEventStream.ts +1 -1
  21. package/server/routing/apiRouter.ts +5 -1
  22. package/server/tunnel/TunnelAgent.ts +250 -0
  23. package/server/tunnel/TunnelDataSocket.ts +140 -0
  24. package/server/tunnel/TunnelHttpStream.ts +93 -0
  25. package/server/tunnel/TunnelTcpStream.ts +100 -0
  26. package/server/tunnel/TunnelWebsocketStream.ts +115 -0
  27. package/server/tunnel/index.ts +6 -0
  28. package/server/tunnel/tunnelStream.ts +44 -0
  29. package/server/webRouter.ts +0 -96
  30. package/service/agent.service.ts +99 -4
  31. package/service/predefinedAdaptor/anthropicLlm.ts +39 -6
  32. package/service/predefinedAdaptor/llm.adaptor.ts +24 -1
  33. package/service/predefinedAdaptor/openaiDialect.ts +40 -10
  34. package/signal/agentTurnStream.ts +32 -29
  35. package/signal/middleware.ts +82 -47
  36. package/signal/serializer/fetch.serializer.ts +1 -0
  37. package/signal/signalContext.ts +8 -1
  38. package/signal/types.ts +24 -0
  39. package/store/agent/AgentCursor.ts +97 -0
  40. package/store/agent/AgentVisual.ts +102 -0
  41. package/store/agent/ScreenFlash.ts +78 -0
  42. package/store/agent/ScreenTarget.ts +11 -2
  43. package/store/agent/StoreSurfaceSource.ts +2 -30
  44. package/store/agent/index.ts +3 -0
  45. package/store/agentic/index.ts +1 -0
  46. package/store/agentic/useAgentReference.ts +49 -0
  47. package/store/hooks.ts +1 -1
  48. package/types/common/index.d.ts +4 -0
  49. package/types/common/toPathSegments.d.ts +9 -0
  50. package/types/common/tunnelWire.d.ts +253 -0
  51. package/types/common/types.d.ts +5 -1
  52. package/types/common/websocketHeartbeat.d.ts +25 -0
  53. package/types/dictionary/base.dictionary.d.ts +1 -1
  54. package/types/dictionary/dictionary.d.ts +8 -8
  55. package/types/fetch/agentTurn.d.ts +3 -3
  56. package/types/fetch/client/fetchClient.d.ts +8 -1
  57. package/types/fetch/client/httpClient.d.ts +2 -0
  58. package/types/index.d.ts +6 -0
  59. package/types/server/mcp/McpEventStream.d.ts +1 -1
  60. package/types/server/tunnel/TunnelAgent.d.ts +33 -0
  61. package/types/server/tunnel/TunnelDataSocket.d.ts +25 -0
  62. package/types/server/tunnel/TunnelHttpStream.d.ts +10 -0
  63. package/types/server/tunnel/TunnelTcpStream.d.ts +14 -0
  64. package/types/server/tunnel/TunnelWebsocketStream.d.ts +10 -0
  65. package/types/server/tunnel/index.d.ts +6 -0
  66. package/types/server/tunnel/tunnelStream.d.ts +30 -0
  67. package/types/server/tunnel.d.ts +1 -0
  68. package/types/service/agent.service.d.ts +55 -1
  69. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +21 -0
  70. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +23 -1
  71. package/types/service/predefinedAdaptor/openaiDialect.d.ts +13 -1
  72. package/types/signal/agent.signal.d.ts +1 -1
  73. package/types/signal/agentTurn.d.ts +1 -1
  74. package/types/signal/agentTurnStream.d.ts +4 -2
  75. package/types/signal/middleware.d.ts +19 -10
  76. package/types/signal/signalContext.d.ts +6 -1
  77. package/types/signal/types.d.ts +24 -0
  78. package/types/store/agent/AgentCursor.d.ts +25 -0
  79. package/types/store/agent/AgentVisual.d.ts +36 -0
  80. package/types/store/agent/ScreenFlash.d.ts +45 -0
  81. package/types/store/agent/ScreenTarget.d.ts +5 -0
  82. package/types/store/agent/StoreSurfaceSource.d.ts +0 -4
  83. package/types/store/agent/index.d.ts +3 -0
  84. package/types/store/agentic/index.d.ts +1 -0
  85. package/types/store/agentic/useAgentReference.d.ts +32 -0
  86. package/types/store/hooks.d.ts +1 -1
  87. package/types/ui/Agent/Attach.d.ts +1 -1
  88. package/types/ui/Agent/Chat.d.ts +25 -1
  89. package/types/ui/Agent/Composer.d.ts +9 -2
  90. package/types/ui/Agent/Menu.d.ts +4 -2
  91. package/types/ui/Agent/Refer.d.ts +13 -0
  92. package/types/ui/Agent/Steps.d.ts +33 -0
  93. package/types/ui/Agent/Zone.d.ts +9 -1
  94. package/types/ui/Agent/agentSessionOf.d.ts +4 -1
  95. package/types/ui/Agent/index.d.ts +1 -1
  96. package/types/ui/Agent/useChatQueue.d.ts +3 -1
  97. package/types/ui/Agent/useChatReferences.d.ts +26 -0
  98. package/types/ui/Agent/useReferenceMenu.d.ts +43 -0
  99. package/types/ui/Field/lightRefCache.d.ts +19 -0
  100. package/types/ui/Field/useRelationOptions.d.ts +39 -0
  101. package/types/ui/Loading/Spin.d.ts +19 -3
  102. package/types/ui/Select.d.ts +2 -0
  103. package/types/ui/UiOverride/context.d.ts +2 -0
  104. package/types/ui/index.d.ts +6 -1
  105. package/types/ui/recipe/inputRecipe.d.ts +2 -2
  106. package/types/vendor/use-agentic/AgentSession.d.ts +60 -1
  107. package/types/vendor/use-agentic/Reference.d.ts +56 -0
  108. package/types/vendor/use-agentic/ToolRunner.d.ts +7 -1
  109. package/types/vendor/use-agentic/Transcript.d.ts +2 -1
  110. package/types/vendor/use-agentic/index.d.ts +1 -0
  111. package/types/vendor/use-agentic/types.d.ts +50 -1
  112. package/ui/Agent/Attach.tsx +4 -3
  113. package/ui/Agent/Bubble.tsx +2 -0
  114. package/ui/Agent/Chat.tsx +82 -14
  115. package/ui/Agent/Composer.tsx +18 -1
  116. package/ui/Agent/Menu.tsx +8 -3
  117. package/ui/Agent/Queued.tsx +2 -0
  118. package/ui/Agent/Refer.tsx +44 -0
  119. package/ui/Agent/Steps.tsx +49 -0
  120. package/ui/Agent/Zone.tsx +10 -0
  121. package/ui/Agent/agentSessionOf.ts +6 -1
  122. package/ui/Agent/sessionHistory.ts +38 -12
  123. package/ui/Agent/useChatQueue.ts +11 -1
  124. package/ui/Agent/useChatReferences.ts +67 -0
  125. package/ui/Agent/useReferenceMenu.ts +108 -0
  126. package/ui/Field/Relation.tsx +70 -150
  127. package/ui/Field/lightRefCache.ts +73 -0
  128. package/ui/Field/useRelationOptions.ts +106 -0
  129. package/ui/Loading/Spin.tsx +24 -4
  130. package/ui/Select.tsx +24 -14
  131. package/ui/UiOverride/context.ts +2 -0
  132. package/ui/index.ts +11 -0
  133. package/ui/styles.css +102 -1
  134. package/vendor/use-agentic/AgentSession.ts +150 -3
  135. package/vendor/use-agentic/Compaction.ts +9 -1
  136. package/vendor/use-agentic/Reference.ts +99 -0
  137. package/vendor/use-agentic/ToolRunner.ts +19 -2
  138. package/vendor/use-agentic/Transcript.ts +9 -3
  139. package/vendor/use-agentic/httpRunner.ts +1 -1
  140. package/vendor/use-agentic/index.ts +1 -0
  141. package/vendor/use-agentic/types.ts +53 -1
  142. /package/{server/routing → common}/eventStream.ts +0 -0
  143. /package/types/{server/routing → common}/eventStream.d.ts +0 -0
package/common/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  } from "./clientAddress";
18
18
  export { deepObjectify } from "./deepObjectify";
19
19
  export type { DynamicRecord } from "./dynamicRecord";
20
+ export { EventStream, type EventStreamOptions } from "./eventStream";
20
21
  export {
21
22
  type FileUploadCapability,
22
23
  fileUploadContract,
@@ -111,6 +112,38 @@ export { sleep } from "./sleep";
111
112
  export { splitVersion } from "./splitVersion";
112
113
  export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
113
114
  export { TrustedProxy } from "./TrustedProxy";
115
+ export { toPathSegments } from "./toPathSegments";
116
+ export {
117
+ type TunnelAgentIdentity,
118
+ type TunnelAttachedFrame,
119
+ type TunnelAttachFrame,
120
+ type TunnelByeFrame,
121
+ type TunnelCloseCode,
122
+ type TunnelControlFromAgent,
123
+ type TunnelControlFromGateway,
124
+ type TunnelDataFromAgent,
125
+ type TunnelDataFromGateway,
126
+ type TunnelDemandFrame,
127
+ type TunnelEndFrame,
128
+ type TunnelFrame,
129
+ type TunnelHeaderList,
130
+ type TunnelHeadFrame,
131
+ type TunnelHelloFrame,
132
+ type TunnelOpenFrame,
133
+ type TunnelPingFrame,
134
+ type TunnelPongFrame,
135
+ type TunnelReadyFrame,
136
+ type TunnelReleaseFrame,
137
+ type TunnelResetCode,
138
+ type TunnelResetFrame,
139
+ type TunnelStreamKind,
140
+ type TunnelWsPayloadKind,
141
+ tunnelCloseCode,
142
+ tunnelForwardedHeaders,
143
+ tunnelHopByHopHeaders,
144
+ tunnelWireContract,
145
+ tunnelWsPayload,
146
+ } from "./tunnelWire";
114
147
  export type * from "./types";
115
148
  export {
116
149
  type WebsocketAuthAckData,
@@ -118,3 +151,8 @@ export {
118
151
  websocketAuthContract,
119
152
  } from "./websocketAuth";
120
153
  export { type WebsocketBinaryFrame, websocketBinaryFrameContract } from "./websocketBinaryFrame";
154
+ export {
155
+ type WebsocketHeartbeatAckData,
156
+ type WebsocketHeartbeatRequest,
157
+ websocketHeartbeatContract,
158
+ } from "./websocketHeartbeat";
package/common/pathGet.ts CHANGED
@@ -1,13 +1,23 @@
1
+ import { toPathSegments } from "./toPathSegments";
2
+
1
3
  type Indexable = Record<string | number, unknown>;
4
+ type PathSegment = string | number;
2
5
 
3
6
  const isIndexable = (value: unknown): value is Indexable => Object(value) === value;
4
7
 
8
+ const readChild = (container: Indexable, key: PathSegment) =>
9
+ container instanceof Map ? (container as Map<PathSegment, unknown>).get(key) : container[key];
10
+
5
11
  export const pathGet = (
6
12
  path: string | (string | number)[],
7
13
  obj: unknown,
8
14
  separator = ".",
9
15
  fallback: unknown = null,
10
16
  ): unknown => {
11
- const properties = Array.isArray(path) ? path : path.split(separator);
12
- return properties.reduce((prev, curr) => (isIndexable(prev) ? (prev[curr] ?? fallback) : fallback), obj);
17
+
18
+ const properties = separator === "." ? toPathSegments(path) : Array.isArray(path) ? [...path] : path.split(separator);
19
+ return properties.reduce<unknown>(
20
+ (prev, curr) => (isIndexable(prev) ? (readChild(prev, curr) ?? fallback) : fallback),
21
+ obj,
22
+ );
13
23
  };
package/common/pathSet.ts CHANGED
@@ -1,10 +1,9 @@
1
+ import { toPathSegments } from "./toPathSegments";
2
+
1
3
  type MutableIndexable = Record<string | number, unknown>;
2
4
  type PathSegment = string | number;
3
5
  type Container = MutableIndexable | Map<PathSegment, unknown>;
4
6
 
5
- const toPathSegments = (path: string | readonly PathSegment[]) =>
6
- Array.isArray(path) ? [...path] : path.toString().match(/[^.[\]]+/g) || [];
7
-
8
7
  const readChild = (container: Container, key: PathSegment) =>
9
8
  container instanceof Map ? container.get(key) : container[key];
10
9
 
@@ -0,0 +1,10 @@
1
+ type PathSegment = string | number;
2
+
3
+ /**
4
+ * The one definition of what a dotted path's segments are, so a path that writes and a path that reads cannot
5
+ * disagree about it. `a.0.b` and `a[0].b` are the same three segments — the bracket form is what a form field
6
+ * hands `writeOn<Model>`, and a read of the same path has to accept the same spelling or the agent can write
7
+ * somewhere it cannot read back.
8
+ */
9
+ export const toPathSegments = (path: string | readonly PathSegment[]) =>
10
+ Array.isArray(path) ? [...path] : path.toString().match(/[^.[\]]+/g) || [];
@@ -0,0 +1,355 @@
1
+ import { forwardedHeaders } from "./clientAddress";
2
+
3
+ /**
4
+ * The wire between a tunnel agent (inside the `akan` CLI, on a private machine) and the tunnel gateway (public,
5
+ * in the cluster). It is the only thing the two sides share, so it lives here rather than in either of them.
6
+ *
7
+ * **Shape: one control socket plus a pool of data sockets, one stream per data socket.** No multiplexing and no
8
+ * hand-written flow control — the runtime already does per-socket flow control, and a single multiplexed socket
9
+ * would have to re-implement windowing to keep one large download from starving every other stream. The cost is
10
+ * a socket per concurrent stream, which pooling amortizes.
11
+ *
12
+ * **Framing is the WebSocket frame type.** A text frame is always one JSON control message of this module; a
13
+ * binary frame is always payload. Nothing else needs a header, a length prefix, or a stream id on the hot path,
14
+ * because a data socket carries exactly one stream at a time.
15
+ *
16
+ * Every socket is dialled by the agent — the gateway can never reach in. That is the whole reason a tunnel needs
17
+ * no public IP, no inbound port, and no NAT traversal.
18
+ */
19
+
20
+ /**
21
+ * `http` and `tcp` bodies are raw binary frames; a `websocket` stream prefixes each one — see `tunnelWsPayload`.
22
+ *
23
+ * `tcp` describes the **provider** side only: an agent dialling a local port. What opens a `tcp` stream in the
24
+ * first place — a local listener somewhere else that a person points `ssh` at — is undefined in version 1, and
25
+ * naming that role is a version 1 addition rather than a breaking change.
26
+ */
27
+ export type TunnelStreamKind = "http" | "websocket" | "tcp";
28
+
29
+ export type TunnelResetCode =
30
+ | "originUnreachable"
31
+ | "originTimeout"
32
+ | "originRefused"
33
+ | "protocol"
34
+ | "canceled"
35
+ | "tooLarge"
36
+ | "internal";
37
+
38
+ /**
39
+ * Pairs rather than a record: `set-cookie` legitimately repeats, and a record keyed by name keeps only the last
40
+ * one — which silently drops every session cookie but one on a sign-in response.
41
+ */
42
+ export type TunnelHeaderList = [string, string][];
43
+
44
+ export interface TunnelAgentIdentity {
45
+ /** What the operator called this share, for the gateway's log and the control plane's UI. */
46
+ name: string;
47
+ version: string;
48
+ platform: string;
49
+ }
50
+
51
+ export interface TunnelHelloFrame {
52
+ type: "hello";
53
+ version: number;
54
+ /** What the agent intends to serve. The gateway answers with what the token actually grants. */
55
+ hostnames: string[];
56
+ agent: TunnelAgentIdentity;
57
+ }
58
+
59
+ export interface TunnelReadyFrame {
60
+ type: "ready";
61
+ version: number;
62
+ sessionId: string;
63
+ /** Authoritative — the agent serves these and nothing else, whatever it asked for. */
64
+ hostnames: string[];
65
+ /** The public URLs, already assembled, so the CLI prints what the gateway believes rather than guessing. */
66
+ urls: string[];
67
+ /** How many idle data sockets to hold, and the ceiling past which the agent refuses to grow the pool. */
68
+ idle: number;
69
+ maxSockets: number;
70
+ heartbeatMs: number;
71
+ /** Epoch ms, when the control plane put a TTL on this share. */
72
+ expiresAt?: number;
73
+ }
74
+
75
+ export interface TunnelPingFrame {
76
+ type: "ping";
77
+ at: number;
78
+ }
79
+
80
+ export interface TunnelPongFrame {
81
+ type: "pong";
82
+ at: number;
83
+ }
84
+
85
+ /** The gateway noticed the idle pool running short; the agent grows it to `idle`, capped by `maxSockets`. */
86
+ export interface TunnelDemandFrame {
87
+ type: "demand";
88
+ idle: number;
89
+ }
90
+
91
+ /** A graceful goodbye from either side, so the peer stops reconnecting instead of treating it as a drop. */
92
+ export interface TunnelByeFrame {
93
+ type: "bye";
94
+ reason?: string;
95
+ }
96
+
97
+ export type TunnelControlFromAgent = TunnelHelloFrame | TunnelPingFrame | TunnelPongFrame | TunnelByeFrame;
98
+ export type TunnelControlFromGateway =
99
+ | TunnelReadyFrame
100
+ | TunnelPingFrame
101
+ | TunnelPongFrame
102
+ | TunnelDemandFrame
103
+ | TunnelByeFrame;
104
+
105
+ /** First frame on a data socket. The session id names which agent connection this socket belongs to. */
106
+ export interface TunnelAttachFrame {
107
+ type: "attach";
108
+ version: number;
109
+ sessionId: string;
110
+ }
111
+
112
+ /** The socket joined the idle pool. Until this arrives the agent must not count it toward `idle`. */
113
+ export interface TunnelAttachedFrame {
114
+ type: "attached";
115
+ }
116
+
117
+ export interface TunnelOpenFrame {
118
+ type: "open";
119
+ streamId: string;
120
+ kind: TunnelStreamKind;
121
+ /** Which tunnel host the public caller addressed, for an agent serving more than one. */
122
+ hostname: string;
123
+ /** `http` and `websocket`. */
124
+ method?: string;
125
+ /** `http` and `websocket`: path plus query, already percent-encoded. */
126
+ path?: string;
127
+ /**
128
+ * `http` and `websocket`. The agent replays this list verbatim, so the gateway owes it two things.
129
+ *
130
+ * **Strip every name in `forwardedHeaderNames` from what the public caller sent, then write its own.** Not
131
+ * append — `hostFromRequest` reads `x-forwarded-host?.split(",")[0]`, so a caller-supplied value placed ahead
132
+ * of the gateway's wins, and the app computes its own origin from a host the caller chose. That is Host header
133
+ * injection: every absolute URL the app builds — a redirect, a password-reset link, an OAuth callback — points
134
+ * at the attacker's host.
135
+ *
136
+ * **Write `x-forwarded-host` as the tunnel hostname.** It is what `hostFromRequest` reads, so the app behind
137
+ * the tunnel resolves its own origin as the public URL and `CrossSiteGuard` compares the host the browser
138
+ * actually addressed. Leave it off and every mutation from a tunnelled page is refused.
139
+ *
140
+ * Hop-by-hop names are stripped by both ends independently (`isHopByHop`); these cannot be, because only the
141
+ * gateway knows who the public caller was.
142
+ */
143
+ headers?: TunnelHeaderList;
144
+ /** `tcp`: the port on the local origin to dial. */
145
+ port?: number;
146
+ /** Whether payload frames follow this one. Absent or false means the request is complete as sent. */
147
+ body?: boolean;
148
+ }
149
+
150
+ export interface TunnelHeadFrame {
151
+ type: "head";
152
+ streamId: string;
153
+ /**
154
+ * `101` accepts a `websocket` open and `200` accepts a `tcp` one; any other status is the agent declining, and
155
+ * the gateway answers the public caller with it rather than upgrading.
156
+ */
157
+ status: number;
158
+ statusText?: string;
159
+ headers: TunnelHeaderList;
160
+ body?: boolean;
161
+ }
162
+
163
+ /** No more payload in the direction this was sent. Each direction ends independently. */
164
+ export interface TunnelEndFrame {
165
+ type: "end";
166
+ streamId: string;
167
+ }
168
+
169
+ /** The stream failed. The socket is not reusable after one and is closed rather than returned to the pool. */
170
+ export interface TunnelResetFrame {
171
+ type: "reset";
172
+ streamId: string;
173
+ code: TunnelResetCode;
174
+ message?: string;
175
+ }
176
+
177
+ /**
178
+ * Gateway → agent, after both directions have ended: the socket is idle again.
179
+ *
180
+ * Only `http` streams are released. A `websocket` or `tcp` stream owns its socket for its whole life and the
181
+ * socket closes with it — resynchronizing a raw byte stream back to an idle pool buys nothing and is a place
182
+ * for a desync to hide.
183
+ */
184
+ export interface TunnelReleaseFrame {
185
+ type: "release";
186
+ streamId: string;
187
+ }
188
+
189
+ /**
190
+ * A payload frame carries no stream id — a data socket holds one stream at a time, which is what keeps the hot
191
+ * path free of a header. Three invariants are what make that safe, and an optimization that breaks any one of
192
+ * them desynchronizes the socket silently:
193
+ *
194
+ * 1. Neither side sends a payload frame after its own `end` for that stream.
195
+ * 2. `release` goes out only once *both* directions have ended.
196
+ * 3. A JSON frame naming a `streamId` other than the current one is dropped — that is what the id is for.
197
+ */
198
+ export type TunnelDataFromAgent = TunnelAttachFrame | TunnelHeadFrame | TunnelEndFrame | TunnelResetFrame;
199
+ export type TunnelDataFromGateway =
200
+ | TunnelAttachedFrame
201
+ | TunnelOpenFrame
202
+ | TunnelEndFrame
203
+ | TunnelResetFrame
204
+ | TunnelReleaseFrame;
205
+
206
+ export type TunnelFrame =
207
+ | TunnelControlFromAgent
208
+ | TunnelControlFromGateway
209
+ | TunnelDataFromAgent
210
+ | TunnelDataFromGateway;
211
+
212
+ /**
213
+ * A tunnelled WebSocket's own text/binary distinction cannot ride the tunnel socket's, which is already spoken
214
+ * for by the control/payload split — so a `websocket` stream prefixes every payload frame with one byte. `http`
215
+ * and `tcp` payload frames carry no prefix; their bytes are a stream with nothing to distinguish.
216
+ */
217
+ export const tunnelWsPayload = {
218
+ text: 0,
219
+ binary: 1,
220
+ /** Remainder is UTF-8 JSON `{ code, reason }` — the inner close, which is not the tunnel socket closing. */
221
+ close: 2,
222
+ } as const;
223
+
224
+ export type TunnelWsPayloadKind = (typeof tunnelWsPayload)[keyof typeof tunnelWsPayload];
225
+
226
+ /**
227
+ * Hop-by-hop headers, which describe one connection and are meaningless on the next. The gateway strips them
228
+ * from what it forwards and the agent strips them from what it sends back, so neither end has to trust the other
229
+ * to have done it.
230
+ */
231
+ export const tunnelHopByHopHeaders = [
232
+ "connection",
233
+ "keep-alive",
234
+ "proxy-authenticate",
235
+ "proxy-authorization",
236
+ "proxy-connection",
237
+ "te",
238
+ "trailer",
239
+ "transfer-encoding",
240
+ "upgrade",
241
+ ] as const;
242
+
243
+ /**
244
+ * Close codes, and the **only** way a refusal reaches the agent. The 4000 range is the application range every
245
+ * WebSocket implementation passes through verbatim; an HTTP status does not survive the upgrade, because a
246
+ * refused upgrade reaches the client as a bare `1006` with no code and no reason. The agent treats these codes
247
+ * as final and every other close as a dropped link to retry — so a gateway that answers `401` at the upgrade
248
+ * instead of accepting it and closing with `unauthorized` makes an agent retry a revoked token forever.
249
+ *
250
+ * **Accept the upgrade, then close on a later tick.** Closing inside the socket's own `open` handler races the
251
+ * handshake the runtime is still finishing, and the client sees `1006` again with the code discarded.
252
+ */
253
+ export const tunnelCloseCode = {
254
+ unauthorized: 4001,
255
+ unsupportedVersion: 4002,
256
+ hostnameNotGranted: 4003,
257
+ unknownSession: 4004,
258
+ /** A newer session for the same tunnel took over; this one must not reconnect. */
259
+ superseded: 4005,
260
+ streamLimit: 4008,
261
+ goingAway: 4009,
262
+ } as const;
263
+
264
+ export type TunnelCloseCode = (typeof tunnelCloseCode)[keyof typeof tunnelCloseCode];
265
+
266
+ /**
267
+ * Headers naming the original caller. The gateway deletes these from what arrived before writing its own — see
268
+ * `TunnelOpenFrame.headers`. `forwarded` is RFC 7239's single-header form of the same claim, and omitting it
269
+ * would leave one spelling of the injection open.
270
+ */
271
+ export const tunnelForwardedHeaders = [...forwardedHeaders, "forwarded"] as const;
272
+
273
+ const hopByHop = new Set<string>(tunnelHopByHopHeaders);
274
+
275
+ export const tunnelWireContract = {
276
+ version: 1,
277
+ controlPath: "/_tunnel/control",
278
+ dataPath: "/_tunnel/data",
279
+ /**
280
+ * Both upgrades carry `Authorization: Bearer <connectorToken>` and nothing else — no code in the path, no
281
+ * query, no second header. Keeping the URL bare is what keeps the tunnel's identity out of every access log
282
+ * between here and the gateway, so the token is also the *only* thing the gateway can resolve a tunnel from:
283
+ * it owns a `token -> { code, hostnames }` lookup, and the agent never names which tunnel it is.
284
+ */
285
+ authScheme: "Bearer",
286
+ defaultIdleSockets: 4,
287
+ defaultMaxSockets: 128,
288
+ defaultHeartbeatMs: 30_000,
289
+
290
+ /** What a sender splits an `http` or `tcp` payload to. A boundary means nothing in a byte stream. */
291
+ chunkBytes: 64 * 1024,
292
+ /**
293
+ * What both ends must accept — `maxPayloadLength` on every tunnel socket is set to at least this.
294
+ *
295
+ * It clears 16 MB because an inner websocket message is a **message**: splitting it loses the boundary the
296
+ * receiver needs, so it rides as one frame, and `akanApp` lets an app's own sockets carry 16 MB. The default
297
+ * `maxPayloadLength` is 16 MB, which the largest legal inner message plus its one-byte prefix exceeds by
298
+ * exactly the amount that makes a 100 MB upload fail on the frame that carries its last kilobyte.
299
+ */
300
+ maxFrameBytes: 16 * 1024 * 1024 + 4096,
301
+
302
+ /** Deletes every `forwardedHeaderNames` entry, so the gateway writes its own onto a clean slate. */
303
+ stripForwarded: (headers: Headers): Headers => {
304
+ for (const name of tunnelForwardedHeaders) headers.delete(name);
305
+ return headers;
306
+ },
307
+
308
+ forwardedHeaderNames: tunnelForwardedHeaders,
309
+
310
+ isHopByHop: (name: string): boolean => hopByHop.has(name.toLowerCase()),
311
+
312
+ /** `Headers.forEach` combines repeats into one comma-joined value — except `set-cookie`, which it yields one
313
+ * at a time. That exception is what makes this lossless. */
314
+ headerList: (headers: Headers): TunnelHeaderList => {
315
+ const list: TunnelHeaderList = [];
316
+ headers.forEach((value, name) => {
317
+ if (!hopByHop.has(name.toLowerCase())) list.push([name, value]);
318
+ });
319
+ return list;
320
+ },
321
+
322
+ headersOf: (list: TunnelHeaderList): Headers => {
323
+ const headers = new Headers();
324
+ for (const [name, value] of list) {
325
+ if (!hopByHop.has(name.toLowerCase())) headers.append(name, value);
326
+ }
327
+ return headers;
328
+ },
329
+
330
+ encodeWsPayload: (kind: TunnelWsPayloadKind, data: Uint8Array): Uint8Array => {
331
+ const framed = new Uint8Array(data.byteLength + 1);
332
+ framed[0] = kind;
333
+ framed.set(data, 1);
334
+ return framed;
335
+ },
336
+
337
+ decodeWsPayload: (frame: Uint8Array): { kind: TunnelWsPayloadKind; data: Uint8Array } | null => {
338
+ if (!frame.byteLength) return null;
339
+ const kind = frame[0] as TunnelWsPayloadKind;
340
+ if (kind !== tunnelWsPayload.text && kind !== tunnelWsPayload.binary && kind !== tunnelWsPayload.close) return null;
341
+ return { kind, data: frame.subarray(1) };
342
+ },
343
+
344
+ /** A frame that does not parse is a protocol error, never something to guess at — hence `null`, not a throw. */
345
+ parseFrame: (raw: string): TunnelFrame | null => {
346
+ try {
347
+ const frame = JSON.parse(raw) as TunnelFrame;
348
+ return frame && typeof frame === "object" && typeof (frame as { type?: unknown }).type === "string"
349
+ ? frame
350
+ : null;
351
+ } catch {
352
+ return null;
353
+ }
354
+ },
355
+ } as const;
package/common/types.ts CHANGED
@@ -5,7 +5,11 @@ export interface FetchPolicy<Returns = unknown> {
5
5
  onError?: (error: string) => void;
6
6
  token?: string;
7
7
  partial?: string[];
8
- timeout?: number;
8
+ /**
9
+ * Milliseconds before this call is abandoned, `false` to wait as long as the runtime will. Overrides the
10
+ * endpoint's declared `timeout`, which overrides the client's own default.
11
+ */
12
+ timeout?: number | false;
9
13
  /**
10
14
  * A `pubsub` subscription only: called after the room has been resubscribed following a dropped connection.
11
15
  *
@@ -0,0 +1,27 @@
1
+ export interface WebsocketHeartbeatRequest {
2
+ key: string;
3
+ data: [];
4
+ }
5
+
6
+ export interface WebsocketHeartbeatAckData {
7
+ type: "pong";
8
+ }
9
+
10
+ /**
11
+ * Framework-owned websocket keep-alive. A browser cannot send a protocol-level ping frame — `WebSocket` exposes
12
+ * no API for one — so a socket that nobody publishes to carries no bytes at all, and every intermediary with an
13
+ * idle timeout (nginx `proxy_read_timeout` 60s, most CDNs, a tunnel gateway) reaps it. The server's own
14
+ * `idleTimeout` is 0, so this exists for what sits between.
15
+ *
16
+ * The ack matters as much as the ping: a half-open socket still accepts `send()` without error, so inbound
17
+ * traffic is the only evidence the peer is still there.
18
+ */
19
+ export const websocketHeartbeatContract = {
20
+ key: "__ping",
21
+ /** Under the 60s idle timeout nginx and most CDNs ship with. */
22
+ intervalMs: 45_000,
23
+ /** How long without any inbound frame before the socket is assumed half-open and reconnected. */
24
+ silenceMs: 45_000 * 3,
25
+ makeRequest: (): WebsocketHeartbeatRequest => ({ key: "__ping", data: [] }),
26
+ makeAck: (): WebsocketHeartbeatAckData => ({ type: "pong" }),
27
+ } as const;
@@ -19,11 +19,15 @@ export const agentTurnDictionary = scalarDictionary(["en", "ko"])
19
19
  "클라이언트가 실행할 툴 호출 목록 ({ id, name, args })",
20
20
  ]),
21
21
  stop: t(["Stop", "종료 사유"]).desc([
22
- "Why the turn ended — end, or toolUse when tool results are awaited",
23
- "턴이 끝난 이유 — end 또는 툴 결과를 기다리는 toolUse",
22
+ "Why the turn ended — end, toolUse when tool results are awaited, or length when the provider cut it off",
23
+ "턴이 끝난 이유 — end, 툴 결과를 기다리는 toolUse, 프로바이더가 잘라낸 length",
24
24
  ]),
25
25
  }))
26
26
  .enum<AgentStop>("agentStop", (t) => ({
27
27
  end: t(["End", "종료"]).desc(["The final answer", "최종 응답"]),
28
28
  toolUse: t(["Tool Use", "툴 사용"]).desc(["The model awaits tool results", "모델이 툴 결과를 기다린다"]),
29
+ length: t(["Length", "길이 초과"]).desc([
30
+ "The provider's answer ceiling cut the turn off, so it is incomplete",
31
+ "프로바이더의 응답 상한에 걸려 턴이 잘렸다. 미완성이다",
32
+ ]),
29
33
  }));
@@ -81,6 +81,11 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
81
81
  agentAttach: ["Attach a file", "파일 첨부"],
82
82
  agentAttachRemove: ["Remove attachment", "첨부 제거"],
83
83
  agentAttachReading: ["Reading…", "읽는 중…"],
84
+ agentReferenceRemove: ["Remove reference", "참조 제거"],
85
+ agentReferenceFailed: [
86
+ "{label} could not be read, so the agent has only its name.",
87
+ "{label}을(를) 읽지 못해 이름만 전달됩니다.",
88
+ ],
84
89
  agentAttachTooLarge: ["{name} is too large to attach.", "{name}은(는) 용량이 너무 커서 첨부할 수 없습니다."],
85
90
  agentAttachUnsupported: ["{name} cannot be attached here.", "{name}은(는) 여기에 첨부할 수 없습니다."],
86
91
  agentAttachDuplicate: ["{name} is already attached.", "{name}은(는) 이미 첨부되어 있습니다."],
@@ -1,7 +1,7 @@
1
1
  import { Any, enumOf } from "akanjs/base";
2
2
  import { ConstantRegistry, via } from "akanjs/constant";
3
3
 
4
- export class AgentStop extends enumOf("agentStop", ["end", "toolUse"] as const) {}
4
+ export class AgentStop extends enumOf("agentStop", ["end", "toolUse", "length"] as const) {}
5
5
 
6
6
  export class AgentTurn extends via((field) => ({
7
7
  text: field(String, { default: "" }), // the assistant's words; empty when the turn is only tool calls
@@ -173,6 +173,13 @@ export class FetchClient {
173
173
  }
174
174
  : signal;
175
175
  }
176
+ /**
177
+ * The budget for every call that neither names one nor is served by an endpoint declaring one. `false` waits
178
+ * as long as the runtime will, which is the browser's own limit — minutes.
179
+ */
180
+ setTimeout(timeout?: number | false) {
181
+ this.http.setTimeout(timeout);
182
+ }
176
183
  setErrorConstructor(ErrorCls?: ErrorConstructor) {
177
184
  this.ErrorCls = ErrorCls;
178
185
  this.http.setErrorConstructor(ErrorCls);
@@ -316,8 +323,9 @@ export class FetchClient {
316
323
  const url = FetchClient.makeHttpUrl(key, endpoint, prefix, argMap);
317
324
  const headers = this.#makeAuthHeaders(option);
318
325
  const baseUrl = option?.origin;
326
+ const timeout = option?.timeout ?? endpoint.timeout;
319
327
 
320
- const requestQuery = () => this.http.get(url, { headers, baseUrl });
328
+ const requestQuery = () => this.http.get(url, { headers, baseUrl, timeout });
321
329
 
322
330
  const claim = baseUrl
323
331
  ? { value: requestQuery(), owned: true }
@@ -339,6 +347,7 @@ export class FetchClient {
339
347
  const response = await this.http.send(endpoint.method ?? "POST", url, body, {
340
348
  headers: this.#makeAuthHeaders(option),
341
349
  baseUrl: option?.origin,
350
+ timeout: option?.timeout ?? endpoint.timeout,
342
351
  });
343
352
  const parsedReturn = parseReturn(response, { crystalize: option?.crystalize ?? true });
344
353
  return parsedReturn;
@@ -827,7 +836,15 @@ export class FetchClient {
827
836
  connect = false,
828
837
  base,
829
838
  Err,
830
- }: { origin?: string; connect?: boolean; base?: FetchProxy; Err?: ErrorConstructor } = {},
839
+ timeout,
840
+ }: {
841
+ origin?: string;
842
+ connect?: boolean;
843
+ base?: FetchProxy;
844
+ Err?: ErrorConstructor;
845
+ /** This app's own default request budget, for calls no endpoint and no caller gave one. */
846
+ timeout?: number | false;
847
+ } = {},
831
848
  ): {
832
849
  sig: ClientSignalMap<SigType>;
833
850
  fetch: SigType["fetch"];
@@ -838,6 +855,7 @@ export class FetchClient {
838
855
  const proxy =
839
856
  shared ??
840
857
  FetchClient.#makeProxy<unknown, Record<string, SliceMeta>>(new FetchClient(origin, {}, serializedSignal, Err));
858
+ if (timeout !== undefined) proxy.instance.setTimeout(timeout);
841
859
  if (connect) proxy.instance.connect();
842
860
  const sig = {} as any;
843
861
  Object.entries(serializedSignal).forEach(([refName, serializedSignal]) => {
@@ -49,6 +49,10 @@ export class HttpClient {
49
49
  setErrorConstructor(ErrorCls?: ErrorConstructor) {
50
50
  this.ErrorCls = ErrorCls;
51
51
  }
52
+ /** The budget every call that names none takes. `false` waits as long as the runtime will. */
53
+ setTimeout(timeout?: number | false) {
54
+ this.#timeout = timeout;
55
+ }
52
56
  #resolveBaseUrl(baseUrl?: string) {
53
57
  return (baseUrl ?? this.baseUrl).replace(/\/$/, "");
54
58
  }