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.
- package/common/index.ts +38 -0
- package/common/pathGet.ts +12 -2
- package/common/pathSet.ts +2 -3
- package/common/toPathSegments.ts +10 -0
- package/common/tunnelWire.ts +355 -0
- package/common/types.ts +5 -1
- package/common/websocketHeartbeat.ts +27 -0
- package/dictionary/agentTurn.dictionary.ts +6 -2
- package/dictionary/base.dictionary.ts +5 -0
- package/fetch/agentTurn.ts +1 -1
- package/fetch/client/fetchClient.ts +20 -2
- package/fetch/client/httpClient.ts +4 -0
- package/fetch/client/wsClient.ts +46 -6
- package/index.ts +6 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +6 -1
- package/server/di/diLifecycle.ts +5 -1
- package/server/logging/logStreamRoute.ts +1 -1
- package/server/mcp/McpEventStream.ts +1 -1
- package/server/routing/apiRouter.ts +5 -1
- package/server/tunnel/TunnelAgent.ts +250 -0
- package/server/tunnel/TunnelDataSocket.ts +140 -0
- package/server/tunnel/TunnelHttpStream.ts +93 -0
- package/server/tunnel/TunnelTcpStream.ts +100 -0
- package/server/tunnel/TunnelWebsocketStream.ts +115 -0
- package/server/tunnel/index.ts +6 -0
- package/server/tunnel/tunnelStream.ts +44 -0
- package/server/webRouter.ts +0 -96
- package/service/agent.service.ts +99 -4
- package/service/predefinedAdaptor/anthropicLlm.ts +39 -6
- package/service/predefinedAdaptor/llm.adaptor.ts +24 -1
- package/service/predefinedAdaptor/openaiDialect.ts +40 -10
- package/signal/agentTurnStream.ts +32 -29
- package/signal/middleware.ts +82 -47
- package/signal/serializer/fetch.serializer.ts +1 -0
- package/signal/signalContext.ts +8 -1
- package/signal/types.ts +24 -0
- package/store/agent/AgentCursor.ts +97 -0
- package/store/agent/AgentVisual.ts +102 -0
- package/store/agent/ScreenFlash.ts +78 -0
- package/store/agent/ScreenTarget.ts +11 -2
- package/store/agent/StoreSurfaceSource.ts +2 -30
- package/store/agent/index.ts +3 -0
- package/store/agentic/index.ts +1 -0
- package/store/agentic/useAgentReference.ts +49 -0
- package/store/hooks.ts +1 -1
- package/types/common/index.d.ts +4 -0
- package/types/common/toPathSegments.d.ts +9 -0
- package/types/common/tunnelWire.d.ts +253 -0
- package/types/common/types.d.ts +5 -1
- package/types/common/websocketHeartbeat.d.ts +25 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/fetch/agentTurn.d.ts +3 -3
- package/types/fetch/client/fetchClient.d.ts +8 -1
- package/types/fetch/client/httpClient.d.ts +2 -0
- package/types/index.d.ts +6 -0
- package/types/server/mcp/McpEventStream.d.ts +1 -1
- package/types/server/tunnel/TunnelAgent.d.ts +33 -0
- package/types/server/tunnel/TunnelDataSocket.d.ts +25 -0
- package/types/server/tunnel/TunnelHttpStream.d.ts +10 -0
- package/types/server/tunnel/TunnelTcpStream.d.ts +14 -0
- package/types/server/tunnel/TunnelWebsocketStream.d.ts +10 -0
- package/types/server/tunnel/index.d.ts +6 -0
- package/types/server/tunnel/tunnelStream.d.ts +30 -0
- package/types/server/tunnel.d.ts +1 -0
- package/types/service/agent.service.d.ts +55 -1
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +21 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +23 -1
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +13 -1
- package/types/signal/agent.signal.d.ts +1 -1
- package/types/signal/agentTurn.d.ts +1 -1
- package/types/signal/agentTurnStream.d.ts +4 -2
- package/types/signal/middleware.d.ts +19 -10
- package/types/signal/signalContext.d.ts +6 -1
- package/types/signal/types.d.ts +24 -0
- package/types/store/agent/AgentCursor.d.ts +25 -0
- package/types/store/agent/AgentVisual.d.ts +36 -0
- package/types/store/agent/ScreenFlash.d.ts +45 -0
- package/types/store/agent/ScreenTarget.d.ts +5 -0
- package/types/store/agent/StoreSurfaceSource.d.ts +0 -4
- package/types/store/agent/index.d.ts +3 -0
- package/types/store/agentic/index.d.ts +1 -0
- package/types/store/agentic/useAgentReference.d.ts +32 -0
- package/types/store/hooks.d.ts +1 -1
- package/types/ui/Agent/Attach.d.ts +1 -1
- package/types/ui/Agent/Chat.d.ts +25 -1
- package/types/ui/Agent/Composer.d.ts +9 -2
- package/types/ui/Agent/Menu.d.ts +4 -2
- package/types/ui/Agent/Refer.d.ts +13 -0
- package/types/ui/Agent/Steps.d.ts +33 -0
- package/types/ui/Agent/Zone.d.ts +9 -1
- package/types/ui/Agent/agentSessionOf.d.ts +4 -1
- package/types/ui/Agent/index.d.ts +1 -1
- package/types/ui/Agent/useChatQueue.d.ts +3 -1
- package/types/ui/Agent/useChatReferences.d.ts +26 -0
- package/types/ui/Agent/useReferenceMenu.d.ts +43 -0
- package/types/ui/Field/lightRefCache.d.ts +19 -0
- package/types/ui/Field/useRelationOptions.d.ts +39 -0
- package/types/ui/Loading/Spin.d.ts +19 -3
- package/types/ui/Select.d.ts +2 -0
- package/types/ui/UiOverride/context.d.ts +2 -0
- package/types/ui/index.d.ts +6 -1
- package/types/ui/recipe/inputRecipe.d.ts +2 -2
- package/types/vendor/use-agentic/AgentSession.d.ts +60 -1
- package/types/vendor/use-agentic/Reference.d.ts +56 -0
- package/types/vendor/use-agentic/ToolRunner.d.ts +7 -1
- package/types/vendor/use-agentic/Transcript.d.ts +2 -1
- package/types/vendor/use-agentic/index.d.ts +1 -0
- package/types/vendor/use-agentic/types.d.ts +50 -1
- package/ui/Agent/Attach.tsx +4 -3
- package/ui/Agent/Bubble.tsx +2 -0
- package/ui/Agent/Chat.tsx +82 -14
- package/ui/Agent/Composer.tsx +18 -1
- package/ui/Agent/Menu.tsx +8 -3
- package/ui/Agent/Queued.tsx +2 -0
- package/ui/Agent/Refer.tsx +44 -0
- package/ui/Agent/Steps.tsx +49 -0
- package/ui/Agent/Zone.tsx +10 -0
- package/ui/Agent/agentSessionOf.ts +6 -1
- package/ui/Agent/sessionHistory.ts +38 -12
- package/ui/Agent/useChatQueue.ts +11 -1
- package/ui/Agent/useChatReferences.ts +67 -0
- package/ui/Agent/useReferenceMenu.ts +108 -0
- package/ui/Field/Relation.tsx +70 -150
- package/ui/Field/lightRefCache.ts +73 -0
- package/ui/Field/useRelationOptions.ts +106 -0
- package/ui/Loading/Spin.tsx +24 -4
- package/ui/Select.tsx +24 -14
- package/ui/UiOverride/context.ts +2 -0
- package/ui/index.ts +11 -0
- package/ui/styles.css +102 -1
- package/vendor/use-agentic/AgentSession.ts +150 -3
- package/vendor/use-agentic/Compaction.ts +9 -1
- package/vendor/use-agentic/Reference.ts +99 -0
- package/vendor/use-agentic/ToolRunner.ts +19 -2
- package/vendor/use-agentic/Transcript.ts +9 -3
- package/vendor/use-agentic/httpRunner.ts +1 -1
- package/vendor/use-agentic/index.ts +1 -0
- package/vendor/use-agentic/types.ts +53 -1
- /package/{server/routing → common}/eventStream.ts +0 -0
- /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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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,
|
|
23
|
-
"턴이 끝난 이유 — end
|
|
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}은(는) 이미 첨부되어 있습니다."],
|
package/fetch/agentTurn.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|