akanjs 3.0.0-alpha.16 → 3.0.0-alpha.18

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 (181) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/constant/index.ts +1 -0
  4. package/constant/labelOf.ts +17 -0
  5. package/dictionary/agent.dictionary.ts +31 -0
  6. package/dictionary/agentTurn.dictionary.ts +29 -0
  7. package/dictionary/base.dictionary.ts +8 -0
  8. package/dictionary/dictionary.ts +9 -3
  9. package/fetch/agentTurn.ts +19 -0
  10. package/fetch/client/fetchClient.ts +34 -12
  11. package/fetch/index.ts +1 -0
  12. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  13. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  14. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  15. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
  16. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  17. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
  18. package/package.json +10 -8
  19. package/server/akanApp.ts +13 -34
  20. package/server/akanOption.ts +56 -2
  21. package/server/akanServer.ts +11 -6
  22. package/server/animatedImage.ts +83 -0
  23. package/server/di/diLifecycle.ts +30 -2
  24. package/server/di/predefinedAdaptor.ts +6 -0
  25. package/server/imageOptimizer.ts +33 -46
  26. package/server/rscWorker.tsx +6 -2
  27. package/server/vendor/akanjs-fetch.ts +1 -0
  28. package/service/agent.service.ts +14 -0
  29. package/service/base.service.ts +2 -1
  30. package/service/index.ts +1 -0
  31. package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
  32. package/service/predefinedAdaptor/index.ts +2 -0
  33. package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
  34. package/service/predefinedAdaptor/role.adaptor.ts +2 -0
  35. package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
  36. package/signal/agent.signal.ts +42 -0
  37. package/signal/agentTurn.ts +8 -0
  38. package/signal/agentTurnStream.ts +48 -0
  39. package/signal/base.signal.ts +2 -1
  40. package/signal/guards.ts +49 -0
  41. package/signal/index.ts +3 -0
  42. package/store/agent/AgentBridge.ts +46 -4
  43. package/store/agent/AgentContext.ts +74 -0
  44. package/store/agent/AgentPrompts.ts +82 -0
  45. package/store/agent/AgentVisibility.ts +68 -0
  46. package/store/agent/ScreenReader.ts +208 -0
  47. package/store/agent/StoreCatalogue.ts +58 -5
  48. package/store/agent/StoreSurfaceSource.ts +122 -0
  49. package/store/agent/index.ts +6 -0
  50. package/store/agent/storeSurface.ts +28 -0
  51. package/store/agentic/StToolBuilder.ts +199 -0
  52. package/store/agentic/attachAgentic.ts +22 -0
  53. package/store/agentic/index.ts +5 -0
  54. package/store/agentic/readableValue.ts +17 -0
  55. package/store/agentic/useStExpose.ts +18 -0
  56. package/store/agentic/useStState.ts +35 -0
  57. package/store/baseSt.ts +7 -0
  58. package/store/index.ts +1 -0
  59. package/store/store.ts +10 -1
  60. package/store/storeInstance.ts +107 -2
  61. package/store/storeRegistry.ts +6 -1
  62. package/store/types.ts +11 -0
  63. package/store/withSelector.ts +5 -4
  64. package/test/registerDom.ts +4 -0
  65. package/types/constant/index.d.ts +1 -0
  66. package/types/constant/labelOf.d.ts +6 -0
  67. package/types/dictionary/agent.dictionary.d.ts +1 -0
  68. package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
  69. package/types/dictionary/base.dictionary.d.ts +1 -1
  70. package/types/dictionary/dictionary.d.ts +11 -9
  71. package/types/fetch/agentTurn.d.ts +593 -0
  72. package/types/fetch/index.d.ts +1 -0
  73. package/types/server/akanApp.d.ts +0 -6
  74. package/types/server/akanOption.d.ts +30 -2
  75. package/types/server/animatedImage.d.ts +7 -0
  76. package/types/server/di/predefinedAdaptor.d.ts +4 -1
  77. package/types/server/vendor/akanjs-fetch.d.ts +1 -0
  78. package/types/service/agent.service.d.ts +12 -0
  79. package/types/service/base.service.d.ts +4 -0
  80. package/types/service/index.d.ts +1 -0
  81. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
  82. package/types/service/predefinedAdaptor/index.d.ts +2 -0
  83. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
  84. package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
  85. package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
  86. package/types/signal/agent.signal.d.ts +29 -0
  87. package/types/signal/agentTurn.d.ts +9 -0
  88. package/types/signal/agentTurnStream.d.ts +16 -0
  89. package/types/signal/base.signal.d.ts +1 -1
  90. package/types/signal/guards.d.ts +19 -0
  91. package/types/signal/index.d.ts +3 -0
  92. package/types/store/agent/AgentBridge.d.ts +12 -3
  93. package/types/store/agent/AgentContext.d.ts +16 -0
  94. package/types/store/agent/AgentPrompts.d.ts +31 -0
  95. package/types/store/agent/AgentVisibility.d.ts +21 -0
  96. package/types/store/agent/ScreenReader.d.ts +11 -0
  97. package/types/store/agent/StoreCatalogue.d.ts +5 -0
  98. package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
  99. package/types/store/agent/index.d.ts +6 -0
  100. package/types/store/agent/storeSurface.d.ts +14 -0
  101. package/types/store/agentic/StToolBuilder.d.ts +41 -0
  102. package/types/store/agentic/attachAgentic.d.ts +14 -0
  103. package/types/store/agentic/index.d.ts +5 -0
  104. package/types/store/agentic/readableValue.d.ts +3 -0
  105. package/types/store/agentic/useStExpose.d.ts +8 -0
  106. package/types/store/agentic/useStState.d.ts +12 -0
  107. package/types/store/agentic.d.ts +1 -0
  108. package/types/store/baseSt.d.ts +28 -21
  109. package/types/store/index.d.ts +1 -0
  110. package/types/store/store.d.ts +2 -1
  111. package/types/store/storeInstance.d.ts +16 -1
  112. package/types/store/storeRegistry.d.ts +2 -0
  113. package/types/store/types.d.ts +11 -0
  114. package/types/store/withSelector.d.ts +5 -4
  115. package/types/test/registerDom.d.ts +1 -0
  116. package/types/ui/Agent/Approval.d.ts +7 -0
  117. package/types/ui/Agent/Bubble.d.ts +13 -0
  118. package/types/ui/Agent/Chat.d.ts +24 -0
  119. package/types/ui/Agent/Context.d.ts +9 -0
  120. package/types/ui/Agent/Dock.d.ts +1 -1
  121. package/types/ui/Agent/Guide.d.ts +9 -0
  122. package/types/ui/Agent/StateKey.d.ts +2 -1
  123. package/types/ui/Agent/Zone.d.ts +23 -0
  124. package/types/ui/Agent/fetchRunner.d.ts +12 -0
  125. package/types/ui/Agent/index.d.ts +6 -0
  126. package/types/ui/Agent/index_.d.ts +1 -0
  127. package/types/ui/Agent/sessionHistory.d.ts +12 -0
  128. package/types/ui/UiOverride/context.d.ts +2 -0
  129. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  130. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  131. package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
  132. package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
  133. package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
  134. package/types/vendor/use-agentic/Agentic.d.ts +21 -0
  135. package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
  136. package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
  137. package/types/vendor/use-agentic/index.d.ts +13 -0
  138. package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
  139. package/types/vendor/use-agentic/types.d.ts +133 -0
  140. package/types/vendor/use-agentic/useAgent.d.ts +4 -0
  141. package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
  142. package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
  143. package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
  144. package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
  145. package/types/vendor/use-agentic.d.ts +1 -0
  146. package/types/webkit/index.d.ts +2 -0
  147. package/types/webkit/useScreenScope.d.ts +18 -0
  148. package/ui/Agent/Approval.tsx +29 -0
  149. package/ui/Agent/Bubble.tsx +74 -0
  150. package/ui/Agent/Chat.tsx +225 -0
  151. package/ui/Agent/Context.tsx +38 -0
  152. package/ui/Agent/Dock.tsx +15 -4
  153. package/ui/Agent/Guide.tsx +16 -0
  154. package/ui/Agent/StateKey.tsx +3 -1
  155. package/ui/Agent/Zone.tsx +48 -0
  156. package/ui/Agent/fetchRunner.ts +30 -0
  157. package/ui/Agent/index.ts +6 -1
  158. package/ui/Agent/index_.tsx +4 -0
  159. package/ui/Agent/sessionHistory.ts +33 -0
  160. package/ui/Load/Units.tsx +11 -2
  161. package/ui/Load/View.tsx +7 -2
  162. package/ui/System/ThemeToggle.tsx +55 -33
  163. package/ui/UiOverride/context.ts +2 -0
  164. package/vendor/use-agentic/AgentProvider.tsx +39 -0
  165. package/vendor/use-agentic/AgentScope.tsx +20 -0
  166. package/vendor/use-agentic/AgentSession.ts +292 -0
  167. package/vendor/use-agentic/Agentic.tsx +44 -0
  168. package/vendor/use-agentic/AgenticSurface.ts +294 -0
  169. package/vendor/use-agentic/WIRE.md +57 -0
  170. package/vendor/use-agentic/httpRunner.ts +115 -0
  171. package/vendor/use-agentic/index.ts +14 -0
  172. package/vendor/use-agentic/surfaceContext.ts +9 -0
  173. package/vendor/use-agentic/types.ts +144 -0
  174. package/vendor/use-agentic/useAgent.ts +17 -0
  175. package/vendor/use-agentic/useAgentGuide.ts +16 -0
  176. package/vendor/use-agentic/useAgentResource.ts +30 -0
  177. package/vendor/use-agentic/useAgentState.ts +61 -0
  178. package/vendor/use-agentic/useAgentTool.ts +55 -0
  179. package/webkit/index.ts +2 -0
  180. package/webkit/lazy.tsx +5 -1
  181. package/webkit/useScreenScope.tsx +49 -0
package/README.ko.md CHANGED
@@ -26,7 +26,7 @@ generated reference가 같은 의도에서 이어집니다.
26
26
 
27
27
  ## 요구사항
28
28
 
29
- - [Bun](https://bun.sh) `>=1.3.13`
29
+ - [Bun](https://bun.sh) `>=1.4.0`
30
30
  - TypeScript 중심의 애플리케이션 코드
31
31
  - React 기반 UI surface
32
32
 
package/README.md CHANGED
@@ -25,7 +25,7 @@ server, app packaging, deployment artifacts, and generated references.
25
25
 
26
26
  ## Requirements
27
27
 
28
- - [Bun](https://bun.sh) `>=1.3.13`
28
+ - [Bun](https://bun.sh) `>=1.4.0`
29
29
  - TypeScript-first application code
30
30
  - React-based UI surfaces
31
31
 
package/constant/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./deserialize";
5
5
  export * from "./fieldInfo";
6
6
  export * from "./getDefault";
7
7
  export * from "./immerify";
8
+ export * from "./labelOf";
8
9
  export * from "./mask";
9
10
  export * from "./purify";
10
11
  export * from "./serialize";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The one line a human scans for, derived from what the model already declared: the `text: "title"` search role
3
+ * names exactly that field, so an agent-facing label costs no new declaration. Falls back to the conventional
4
+ * `title`/`name` keys; the id is the caller's floor.
5
+ */
6
+ export const labelOf = (model: unknown, value: unknown): string | undefined => {
7
+ if (!value || typeof value !== "object") return undefined;
8
+ const source = value as Record<string, unknown>;
9
+ const paths = (model as { text?: { title?: Iterable<string> } } | null)?.text?.title;
10
+ const titlePath = [...(paths ?? [])].find((path) => !path.includes(".") && !path.includes("["));
11
+ for (const key of [titlePath, "title", "name"]) {
12
+ if (!key) continue;
13
+ const candidate = source[key];
14
+ if (typeof candidate === "string" && candidate) return candidate;
15
+ }
16
+ return undefined;
17
+ };
@@ -0,0 +1,31 @@
1
+ import type { AgentEndpoint } from "akanjs/signal";
2
+
3
+ import { serviceDictionary } from "./dictInfo";
4
+
5
+ export const agentDictionary = serviceDictionary(["en", "ko"])
6
+ .endpoint<AgentEndpoint>((fn) => ({
7
+ runAgentTurn: fn(["Run Agent Turn", "에이전트 턴 실행"])
8
+ .desc([
9
+ "Relays one in-page agent turn to the model and returns its answer; tools execute in the caller's browser",
10
+ "인페이지 에이전트 턴 하나를 모델에 릴레이하고 응답을 돌려준다. 툴 실행은 호출자의 브라우저에서 한다",
11
+ ])
12
+ .arg((t) => ({
13
+ messages: t(["Messages", "메시지"]).desc(["The whole transcript in wire shape", "와이어 형태의 전체 대화"]),
14
+ tools: t(["Tools", "툴"]).desc(["The published tool catalogue, schemas only", "게시된 툴 카탈로그 (스키마만)"]),
15
+ context: t(["Context", "컨텍스트"]).desc([
16
+ "Screen context blocks, forwarded as data",
17
+ "화면 컨텍스트 블록 (데이터로 전달)",
18
+ ]),
19
+ instructions: t(["Instructions", "지시문"]).desc(["App-level system instructions", "앱 수준 시스템 지시문"]),
20
+ })),
21
+ }))
22
+ .error({
23
+ llmUnavailable: [
24
+ "The agent model is unavailable — no LLM adaptor answered this turn",
25
+ "에이전트 모델을 사용할 수 없습니다. 이번 턴에 응답한 LLM 어댑터가 없습니다",
26
+ ],
27
+ deepseekRequestFailed: [
28
+ "DeepSeek request failed with status {status}",
29
+ "DeepSeek 요청이 실패했습니다 (status {status})",
30
+ ],
31
+ });
@@ -0,0 +1,29 @@
1
+ import type { AgentStop, AgentTurn } from "akanjs/signal";
2
+
3
+ import { scalarDictionary } from "./dictInfo";
4
+
5
+ export const agentTurnDictionary = scalarDictionary(["en", "ko"])
6
+ .of((t) =>
7
+ t(["Agent Turn", "에이전트 턴"]).desc([
8
+ "One assistant answer of an in-page agent turn",
9
+ "인페이지 에이전트 턴의 어시스턴트 응답 하나",
10
+ ]),
11
+ )
12
+ .model<AgentTurn>((t) => ({
13
+ text: t(["Text", "텍스트"]).desc([
14
+ "What the assistant said; empty when the turn is only tool calls",
15
+ "어시스턴트가 말한 내용. 툴 호출뿐인 턴에서는 비어 있다",
16
+ ]),
17
+ toolCalls: t(["Tool Calls", "툴 호출"]).desc([
18
+ "Tool calls the client should execute, as { id, name, args }",
19
+ "클라이언트가 실행할 툴 호출 목록 ({ id, name, args })",
20
+ ]),
21
+ stop: t(["Stop", "종료 사유"]).desc([
22
+ "Why the turn ended — end, or toolUse when tool results are awaited",
23
+ "턴이 끝난 이유 — end 또는 툴 결과를 기다리는 toolUse",
24
+ ]),
25
+ }))
26
+ .enum<AgentStop>("agentStop", (t) => ({
27
+ end: t(["End", "종료"]).desc(["The final answer", "최종 응답"]),
28
+ toolUse: t(["Tool Use", "툴 사용"]).desc(["The model awaits tool results", "모델이 툴 결과를 기다린다"]),
29
+ }));
@@ -48,6 +48,14 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
48
48
  ok: ["OK", "확인"],
49
49
  cancel: ["Cancel", "취소"],
50
50
  unauthorized: ["Unauthorized", "권한 없음"],
51
+ agent: ["Agent", "에이전트"],
52
+ agentIntro: ["Ask about this page or tell the agent what to do.", "이 화면에 대해 묻거나 할 일을 지시해 보세요."],
53
+ agentPlaceholder: ["Message the agent...", "에이전트에게 메시지..."],
54
+ agentClear: ["Clear conversation", "대화 비우기"],
55
+ send: ["Send", "보내기"],
56
+ stop: ["Stop", "중지"],
57
+ approve: ["Approve", "승인"],
58
+ decline: ["Decline", "거절"],
51
59
  confirmClose: ["Are you sure you want to close the modal?", "모달을 닫으시겠습니까?"],
52
60
  textTooShortError: ["More than {minlength} characters needed.", "{minlength}자 이상 필요합니다."],
53
61
  textTooLongError: ["Less than {maxlength} characters needed.", "{maxlength}자 이하로 필요합니다."],
@@ -1,10 +1,16 @@
1
- import type { BaseEndpoint } from "akanjs/signal";
1
+ import type { AgentEndpoint, AgentTurn, BaseEndpoint } from "akanjs/signal";
2
2
 
3
+ import { agentDictionary } from "./agent.dictionary";
4
+ import { agentTurnDictionary } from "./agentTurn.dictionary";
3
5
  import { baseDictionary } from "./base.dictionary";
4
- import { registerServiceTrans } from "./locale";
6
+ import { registerScalarTrans, registerServiceTrans } from "./locale";
5
7
  import { makeTrans } from "./trans";
6
8
 
7
- export const dictionary = { base: registerServiceTrans<"base", BaseEndpoint, typeof baseDictionary>(baseDictionary) };
9
+ export const dictionary = {
10
+ base: registerServiceTrans<"base", BaseEndpoint, typeof baseDictionary>(baseDictionary),
11
+ agentTurn: registerScalarTrans<"agentTurn", AgentTurn, typeof agentTurnDictionary>(agentTurnDictionary),
12
+ agent: registerServiceTrans<"agent", AgentEndpoint, typeof agentDictionary>(agentDictionary),
13
+ };
8
14
 
9
15
  export const { Err, translate, msg, getDictionary, getAllDictionary, __Dict_Key__, __Error_Key__ } =
10
16
  makeTrans(dictionary);
@@ -0,0 +1,19 @@
1
+ import { Any, enumOf } from "akanjs/base";
2
+ import { ConstantRegistry, via } from "akanjs/constant";
3
+
4
+ export class AgentStop extends enumOf("agentStop", ["end", "toolUse"] as const) {}
5
+
6
+ export class AgentTurn extends via((field) => ({
7
+ text: field(String, { default: "" }), // the assistant's words; empty when the turn is only tool calls
8
+ toolCalls: field([Any]), // { id, name, args } per call — args carries the tool's own schema, not a model's
9
+ stop: field(AgentStop, { default: "end" }),
10
+ })) {
11
+ isToolUse() {
12
+ return this.stop === "toolUse";
13
+ }
14
+ }
15
+
16
+ export const agentTurnConstant = ConstantRegistry.buildScalar("agentTurn" as const, AgentTurn, {
17
+ AgentTurn,
18
+ AgentStop,
19
+ });
@@ -10,6 +10,7 @@ import type {
10
10
  SerializedSlice,
11
11
  ServiceSignal,
12
12
  } from "akanjs/signal";
13
+ import { agentTurnConstant } from "../agentTurn";
13
14
  import type { ClientSignal, FetchClientType, FetchSignalInput, MergeAllFetchTypes, SliceMeta } from "../fetchType";
14
15
  import { memoizeRequestQuery, cookies as requestCookies, headers as requestHeaders } from "../requestStorage";
15
16
  import type { GetSliceMetaObjFromDatabaseSignals } from "../types";
@@ -47,6 +48,19 @@ const globalWithSharedClient = globalThis as typeof globalThis & { [SHARED_CLIEN
47
48
  const sharedClientState: SharedClientState = globalWithSharedClient[SHARED_CLIENT_KEY] ?? { proxy: null, origin: null };
48
49
  globalWithSharedClient[SHARED_CLIENT_KEY] = sharedClientState;
49
50
 
51
+ interface SharedSignalRegistry {
52
+ signal: { [key: string]: SerializedSignal };
53
+ version: number;
54
+ }
55
+
56
+ const SHARED_SIGNAL_KEY = Symbol.for("akanjs.fetch.sharedSignalRegistry");
57
+ const globalWithSharedSignal = globalThis as typeof globalThis & { [SHARED_SIGNAL_KEY]?: SharedSignalRegistry };
58
+ const sharedSignalRegistry: SharedSignalRegistry = globalWithSharedSignal[SHARED_SIGNAL_KEY] ?? {
59
+ signal: {},
60
+ version: 0,
61
+ };
62
+ globalWithSharedSignal[SHARED_SIGNAL_KEY] = sharedSignalRegistry;
63
+
50
64
  type ClientSignalMap<SigType extends { fetch: any }> = {
51
65
  [K in keyof SigType as SigType[K] extends DatabaseSignal<any, any, any, any>
52
66
  ? K
@@ -57,8 +71,9 @@ type ClientSignalMap<SigType extends { fetch: any }> = {
57
71
 
58
72
  /** Runtime fetch client that registers serialized Akan signals as HTTP/WebSocket methods. */
59
73
  export class FetchClient {
60
- static #sharedSerializedSignal: { [key: string]: SerializedSignal } = {};
61
- static #sharedRegistryVersion = 0;
74
+ static {
75
+ ConstantRegistry.setScalar(agentTurnConstant.refName, agentTurnConstant);
76
+ }
62
77
  readonly logger = new Logger("FetchClient");
63
78
  readonly origin: string;
64
79
  readonly http: HttpClient;
@@ -93,11 +108,11 @@ export class FetchClient {
93
108
  * would change what the next client applies. Read by the agent catalogue, which needs the argument schemas.
94
109
  */
95
110
  static get sharedSerializedSignal(): { [key: string]: SerializedSignal } {
96
- return { ...FetchClient.#sharedSerializedSignal };
111
+ return { ...sharedSignalRegistry.signal };
97
112
  }
98
113
  static resetSharedRegistry() {
99
- FetchClient.#sharedSerializedSignal = {};
100
- FetchClient.#sharedRegistryVersion++;
114
+ sharedSignalRegistry.signal = {};
115
+ sharedSignalRegistry.version++;
101
116
  }
102
117
  static resetSharedClient() {
103
118
  sharedClientState.proxy = null;
@@ -147,9 +162,9 @@ export class FetchClient {
147
162
  applySignal(serializedSignal: { [key: string]: SerializedSignal }, { share = true }: { share?: boolean } = {}) {
148
163
  if (share && Object.keys(serializedSignal).length > 0) {
149
164
  for (const [refName, signal] of Object.entries(serializedSignal))
150
- FetchClient.#mergeSerializedSignalInto(FetchClient.#sharedSerializedSignal, refName, signal);
151
- FetchClient.#sharedRegistryVersion++;
152
- this.#sharedRegistryAppliedVersion = FetchClient.#sharedRegistryVersion;
165
+ FetchClient.#mergeSerializedSignalInto(sharedSignalRegistry.signal, refName, signal);
166
+ sharedSignalRegistry.version++;
167
+ this.#sharedRegistryAppliedVersion = sharedSignalRegistry.version;
153
168
  }
154
169
  for (const [refName, signal] of Object.entries(serializedSignal))
155
170
  FetchClient.#mergeSerializedSignalInto(this.serializedSignal, refName, signal);
@@ -183,9 +198,9 @@ export class FetchClient {
183
198
  });
184
199
  }
185
200
  #syncSharedRegistry() {
186
- if (this.#sharedRegistryAppliedVersion === FetchClient.#sharedRegistryVersion) return;
187
- this.applySignal(FetchClient.#sharedSerializedSignal, { share: false });
188
- this.#sharedRegistryAppliedVersion = FetchClient.#sharedRegistryVersion;
201
+ if (this.#sharedRegistryAppliedVersion === sharedSignalRegistry.version) return;
202
+ this.applySignal(sharedSignalRegistry.signal, { share: false });
203
+ this.#sharedRegistryAppliedVersion = sharedSignalRegistry.version;
189
204
  }
190
205
  #getOrCreateHandler(key: string): FetchHandler | undefined {
191
206
  const current = this.#handlerStore[key];
@@ -719,9 +734,16 @@ export class FetchClient {
719
734
  });
720
735
  }
721
736
  });
722
- const instance = new FetchClient(getEnv().serverHttpUri, handler, serializedSignal);
737
+ const instance = new FetchClient(FetchClient.#originFromEnv(), handler, serializedSignal);
723
738
  return FetchClient.#makeProxy<MergeAllFetchTypes<Signals>, GetSliceMetaObjFromDatabaseSignals<Signals>>(instance);
724
739
  }
740
+ static #originFromEnv() {
741
+ try {
742
+ return getEnv().serverHttpUri;
743
+ } catch {
744
+ return "";
745
+ }
746
+ }
725
747
  static build<SigType extends { fetch: any }>(
726
748
  constant: object,
727
749
  serializedSignal: { [key: string]: SerializedSignal },
package/fetch/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./agentTurn";
1
2
  export * from "./client";
2
3
  export * from "./fetchType";
3
4
  export * from "./requestStorage";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.16",
3
+ "version": "3.0.0-alpha.18",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -180,7 +180,6 @@
180
180
  "dependencies": {
181
181
  "dayjs": "^1.11.20",
182
182
  "immer": "^11.1.8",
183
- "sharp": "^0.34.5",
184
183
  "tailwind-merge": "^3.3.1",
185
184
  "tailwind-variants": "^3.3.0"
186
185
  },
@@ -202,6 +201,7 @@
202
201
  "@capacitor/preferences": "^8.0.1",
203
202
  "@capacitor/push-notifications": "^8.1.1",
204
203
  "@capgo/capacitor-updater": "^8.46.1",
204
+ "@happy-dom/global-registrator": "^20.11.2",
205
205
  "@libsql/client": "^0.17.3",
206
206
  "@playwright/test": "^1.60.0",
207
207
  "@react-spring/web": "^10.1.0",
@@ -210,8 +210,8 @@
210
210
  "capacitor-plugin-safe-area": "^5.0.0",
211
211
  "chance": "^1.1.13",
212
212
  "cordova-plugin-purchase": "^13.16.0",
213
- "croner": "^10.0.1",
214
213
  "firebase": "^12.13.0",
214
+ "happy-dom": "^20.11.2",
215
215
  "ioredis": "^5.10.1",
216
216
  "mermaid": "^11.15.0",
217
217
  "postgres": "^3.4.9",
@@ -225,7 +225,6 @@
225
225
  "react-simple-pull-to-refresh": "^1.3.4",
226
226
  "react-spring": "^10.0.4",
227
227
  "scheduler": "^0.27.0",
228
- "sharp": "^0.34.5",
229
228
  "tailwind-scrollbar": "^4.0.2",
230
229
  "typescript": "^6.0.3"
231
230
  },
@@ -281,6 +280,9 @@
281
280
  "@capgo/capacitor-updater": {
282
281
  "optional": true
283
282
  },
283
+ "@happy-dom/global-registrator": {
284
+ "optional": true
285
+ },
284
286
  "@libsql/client": {
285
287
  "optional": true
286
288
  },
@@ -305,10 +307,10 @@
305
307
  "cordova-plugin-purchase": {
306
308
  "optional": true
307
309
  },
308
- "croner": {
310
+ "firebase": {
309
311
  "optional": true
310
312
  },
311
- "firebase": {
313
+ "happy-dom": {
312
314
  "optional": true
313
315
  },
314
316
  "ioredis": {
@@ -359,13 +361,13 @@
359
361
  },
360
362
  "devDependencies": {
361
363
  "@biomejs/biome": "2.5.8",
362
- "@types/bun": "^1.3.14"
364
+ "@types/bun": "^1.4.0"
363
365
  },
364
366
  "bun": {
365
367
  "platform": "bun"
366
368
  },
367
369
  "engines": {
368
- "bun": ">=1.3.13"
370
+ "bun": ">=1.4.0"
369
371
  },
370
372
  "main": "./index.ts"
371
373
  }
package/server/akanApp.ts CHANGED
@@ -6,7 +6,6 @@ import { Logger } from "akanjs/common";
6
6
  import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport, AkanUpstream } from "akanjs/service";
7
7
  import { isTraceEnabled } from "akanjs/signal";
8
8
  import { makeAkanChildProxyHeaders } from "./akanAppHeaders";
9
- import type { McpServerOption } from "./akanServer";
10
9
  import type { BuilderCsrReq, BuilderCsrRes, BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
11
10
  import { resolveEncodedSidecar } from "./assetEncoding";
12
11
  import { isPortInUseError } from "./lifecycle/portInUse";
@@ -55,11 +54,6 @@ export interface AkanAppOptions {
55
54
  port?: number;
56
55
  wsBasePort?: number;
57
56
  openapi?: boolean;
58
- /**
59
- * MCP server config for the children, which are the processes that actually mount `/mcp`. Declared here
60
- * because `server.ts` is generated and takes no options, so `main.ts` is the only app-authored place left.
61
- */
62
- mcp?: boolean | McpServerOption;
63
57
  }
64
58
 
65
59
  interface AkanReplicaConfig {
@@ -92,7 +86,6 @@ export class AkanApp {
92
86
  readonly #port: number;
93
87
  readonly #wsBasePort: number;
94
88
  readonly #openapi?: boolean;
95
- readonly #mcpEnv: Record<string, string>;
96
89
  readonly #children = new Map<number, ChildState>();
97
90
  readonly #roomChildren = new Map<string, Set<number>>();
98
91
  readonly #childRooms = new Map<number, Set<string>>();
@@ -134,30 +127,6 @@ export class AkanApp {
134
127
  this.#port = Number(resolvedOptions.port ?? process.env.PORT ?? 8282);
135
128
  this.#wsBasePort = Number(resolvedOptions.wsBasePort ?? process.env.AKAN_WS_BASE_PORT ?? this.#port + 10_000);
136
129
  this.#openapi = resolvedOptions.openapi;
137
- this.#mcpEnv = AkanApp.#toMcpEnv(resolvedOptions.mcp);
138
- }
139
-
140
- /**
141
- * `McpServerOption`, spelled as the environment a child is spawned with — the only channel the gateway has to
142
- * a process it starts, and the same one a deployment configures MCP through when it sets nothing in code.
143
- */
144
- static #toMcpEnv(mcp: boolean | McpServerOption | undefined): Record<string, string> {
145
- if (mcp === undefined) return {};
146
- if (typeof mcp === "boolean") return { AKAN_MCP: String(mcp) };
147
- const { enabled, readOnly, path: mcpPath, version, instructions, allowedOrigins, pageSize, language, auth } = mcp;
148
- return {
149
- AKAN_MCP: String(enabled ?? true),
150
- ...(readOnly === undefined ? {} : { AKAN_MCP_READONLY: String(readOnly) }),
151
- ...(mcpPath ? { AKAN_MCP_PATH: mcpPath } : {}),
152
- ...(version ? { AKAN_MCP_VERSION: version } : {}),
153
- ...(instructions ? { AKAN_MCP_INSTRUCTIONS: instructions } : {}),
154
- ...(allowedOrigins?.length ? { AKAN_MCP_ALLOWED_ORIGINS: allowedOrigins.join(",") } : {}),
155
- ...(pageSize === undefined ? {} : { AKAN_MCP_PAGE_SIZE: String(pageSize) }),
156
- ...(language ? { AKAN_MCP_LANGUAGE: language } : {}),
157
- ...(auth?.authorizationServers?.length ? { AKAN_MCP_AUTH_SERVERS: auth.authorizationServers.join(",") } : {}),
158
- ...(auth?.scopes?.length ? { AKAN_MCP_SCOPES: auth.scopes.join(",") } : {}),
159
- ...(auth?.resource ? { AKAN_MCP_RESOURCE: auth.resource } : {}),
160
- };
161
130
  }
162
131
 
163
132
  static #resolveServerPath(serverPath: string) {
@@ -328,7 +297,6 @@ export class AkanApp {
328
297
  AKAN_CHILD_SOCKET: upstream.http.socketPath,
329
298
  AKAN_CHILD_WS_PORT: upstream.ws ? String(upstream.ws.port) : "",
330
299
  ...(this.#openapi === undefined ? {} : { AKAN_OPENAPI: this.#openapi ? "true" : "false" }),
331
- ...this.#mcpEnv,
332
300
  },
333
301
  ipc: (message) => this.#handleMessage(idx, message as AkanIpcMessage, proc),
334
302
  stdout: "pipe",
@@ -626,7 +594,11 @@ export class AkanApp {
626
594
  const result = ws.send(event.data as string | ArrayBuffer);
627
595
  if (result === 0) upstream.close();
628
596
  });
629
- upstream.addEventListener("close", (event) => ws.close(event.code, event.reason));
597
+ upstream.addEventListener("close", (event) => {
598
+ const code = AkanApp.#sendableCloseCode(event.code);
599
+ if (code === undefined) ws.close();
600
+ else ws.close(code, event.reason);
601
+ });
630
602
  upstream.addEventListener("error", () => ws.close(1011, "upstream websocket error"));
631
603
  Object.assign(ws.data, { pending });
632
604
  }
@@ -646,8 +618,15 @@ export class AkanApp {
646
618
  else ws.data.pending?.push(payload as string | ArrayBuffer);
647
619
  }
648
620
 
621
+ static #sendableCloseCode(code: number): number | undefined {
622
+ const sendable = (code >= 1000 && code <= 1014 && (code < 1004 || code > 1006)) || (code >= 3000 && code <= 4999);
623
+ return sendable ? code : undefined;
624
+ }
625
+
649
626
  #handleWsClose(ws: Bun.ServerWebSocket<GatewayWsData>, code: number, reason: string) {
650
- ws.data.upstream.close(code, reason);
627
+ const upstreamCode = AkanApp.#sendableCloseCode(code);
628
+ if (upstreamCode === undefined) ws.data.upstream.close();
629
+ else ws.data.upstream.close(upstreamCode, reason);
651
630
  const child = this.#children.get(ws.data.childIdx);
652
631
  if (child) child.metrics.activeWebSockets = Math.max(0, (child.metrics.activeWebSockets ?? 1) - 1);
653
632
  }
@@ -1,13 +1,27 @@
1
1
  import type { BaseEnv, PromiseOrObject } from "akanjs/base";
2
- import type { MiddlewareCls } from "akanjs/signal";
2
+ import type { Adaptor, AdaptorCls, LlmOption } from "akanjs/service";
3
+ import type { AgentRelayPolicy, MiddlewareCls } from "akanjs/signal";
4
+ import type { McpServerOption } from "./akanServer";
3
5
  import type { WebProxyRegistration } from "./proxy";
4
6
  import { HostBasePathWebProxy, LocaleWebProxy } from "./proxy";
5
7
 
6
- /** App/library server option builder for use objects, signal middleware, and web proxies. */
8
+ export interface AdaptorOverride {
9
+ role: AdaptorCls;
10
+ adaptor: AdaptorCls;
11
+ }
12
+
13
+ /**
14
+ * App/library server option builder: use objects, signal middleware, adaptor overrides, web proxies, and the
15
+ * server settings an app owns — MCP, the agent relay's access policy, and the LLM the relay speaks to.
16
+ */
7
17
  export class AkanOption<Env extends BaseEnv = BaseEnv> {
8
18
  readonly #getUses: ((env: Env) => Record<string, PromiseOrObject<unknown>>)[];
9
19
  readonly #middlewares: MiddlewareCls[] = [];
20
+ readonly #adaptorOverrides: AdaptorOverride[] = [];
10
21
  readonly #webProxies: WebProxyRegistration[] = [];
22
+ readonly #getLlms: ((env: Env) => LlmOption)[] = [];
23
+ #mcp: boolean | McpServerOption | undefined;
24
+ #agentAccess: AgentRelayPolicy | null | undefined;
11
25
  constructor() {
12
26
  this.#getUses = [];
13
27
  }
@@ -21,10 +35,38 @@ export class AkanOption<Env extends BaseEnv = BaseEnv> {
21
35
  this.#middlewares.push(...middlewares);
22
36
  return this;
23
37
  }
38
+ /** Rebinds a predefined adaptor role (e.g. `LlmAdaptorRole`) to the app's own implementation. Last writer wins. */
39
+ applyAdaptor<T extends Adaptor>(role: AdaptorCls<T>, adaptor: AdaptorCls<T>) {
40
+ this.#adaptorOverrides.push({ role: role as AdaptorCls, adaptor: adaptor as AdaptorCls });
41
+ return this;
42
+ }
24
43
  applyWebProxy(...proxies: WebProxyRegistration[]) {
25
44
  this.#webProxies.push(...proxies);
26
45
  return this;
27
46
  }
47
+ /**
48
+ * MCP server settings for the app mounting this option, merged over the `AKAN_MCP_*` environment and under an
49
+ * option the server is constructed with. The app's own `option.ts` is the last lib the server reads, so it wins
50
+ * over every library it depends on.
51
+ */
52
+ setMcp(mcp: boolean | McpServerOption = true) {
53
+ this.#mcp = mcp;
54
+ return this;
55
+ }
56
+ /**
57
+ * Who may spend the LLM key through the `runAgentTurn` relay — the `AgentRelayAccess` policy, which defaults to
58
+ * allowing everyone because the framework has no account model to gate on. `null` reopens it.
59
+ */
60
+ setAgentAccess(policy: AgentRelayPolicy | null) {
61
+ this.#agentAccess = policy;
62
+ return this;
63
+ }
64
+ /** Settings for whichever adaptor fills `LlmAdaptorRole`, injected into it as the `llmOption` use. */
65
+ setLlm(llmOrFn: LlmOption | ((env: Env) => LlmOption)) {
66
+ if (typeof llmOrFn === "function") this.#getLlms.push(llmOrFn);
67
+ else this.#getLlms.push(() => llmOrFn);
68
+ return this;
69
+ }
28
70
  getUses(env: Env): Record<string, PromiseOrObject<unknown>> {
29
71
  const uses = this.#getUses.map((fn) => fn(env));
30
72
  return Object.assign({}, ...uses);
@@ -32,9 +74,21 @@ export class AkanOption<Env extends BaseEnv = BaseEnv> {
32
74
  getMiddlewares(): MiddlewareCls[] {
33
75
  return this.#middlewares;
34
76
  }
77
+ getAdaptorOverrides(): AdaptorOverride[] {
78
+ return this.#adaptorOverrides;
79
+ }
35
80
  getWebProxies(): WebProxyRegistration[] {
36
81
  return this.#webProxies;
37
82
  }
83
+ getMcp(): boolean | McpServerOption | undefined {
84
+ return this.#mcp;
85
+ }
86
+ getAgentAccess(): AgentRelayPolicy | null | undefined {
87
+ return this.#agentAccess;
88
+ }
89
+ getLlm(env: Env): LlmOption {
90
+ return Object.assign({}, ...this.#getLlms.map((fn) => fn(env)));
91
+ }
38
92
  }
39
93
 
40
94
  export function createDefaultAkanOption() {
@@ -11,6 +11,7 @@ import type {
11
11
  SolidConfig,
12
12
  } from "akanjs/service";
13
13
  import type { ServerSignal, ServerSignalCls, WebsocketPublishData } from "akanjs/signal";
14
+ import { AgentRelayAccess } from "../signal/guards";
14
15
  import { createOpenApiDocument } from "../signal/openapi";
15
16
  import { FetchSerializer } from "../signal/serializer";
16
17
  import type { AkanLib, AkanLibProps } from "./akanLib";
@@ -142,6 +143,13 @@ export class AkanServer {
142
143
  this.libs = libs;
143
144
  this.env = { ...env };
144
145
  this.openapi = options?.openapi ?? this.openapi;
146
+
147
+ libs.forEach((lib) => {
148
+ const mcp = lib.option.getMcp();
149
+ if (mcp !== undefined) this.setMcp(mcp);
150
+ const agentAccess = lib.option.getAgentAccess();
151
+ if (agentAccess !== undefined) AgentRelayAccess.use(agentAccess);
152
+ });
145
153
  this.setMcp(options?.mcp ?? this.mcp);
146
154
  this.serverMode = serverMode;
147
155
  this.#di = new DiLifecycle(this.env, serverMode, ...libs);
@@ -616,12 +624,9 @@ export class AkanServer {
616
624
  }
617
625
 
618
626
  /**
619
- * The rest of `McpServerOption`, in the one channel that reaches this process.
620
- *
621
- * `server.ts` is generated and constructs `AkanServer` with no options, and a child of the gateway is handed
622
- * nothing but its environment — so without an env spelling per field, the only settable parts were the two
623
- * booleans and `instructions` / `allowedOrigins` / `path` / `version` / `pageSize` / `language` had no path
624
- * from an app at all. An option passed in code still wins: `setMcp` merges over this.
627
+ * The rest of `McpServerOption`, spelled as environment — the channel a deployment configures what the source
628
+ * does not through, and the only one a child of the gateway is handed. An app writes these in its `option.ts`,
629
+ * which merges over this: a value written in code wins over the env of the same name.
625
630
  */
626
631
  /**
627
632
  * Keys whose value is `undefined` are dropped before the merge. "An option written in code wins over the env of