@k2b/cloud 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -0,0 +1,202 @@
1
+ import { openAICompatibleTranscription, type TranscriptionProvider } from "@k2b/nessi/ai";
2
+ import { coreSettings } from "../services";
3
+ import { logger, type TraceContext, trace } from "../services/logging";
4
+ import { AI_AUDIO_MAX_BYTES, inspectAiAudio } from "./audio-format";
5
+ import { getAiCredential } from "./credentials";
6
+ import { beginAiCall, finishAiCall } from "./inference-calls";
7
+ import { readAiSettingsState, selectAiModelProfile } from "./settings";
8
+ import { type AiUsageAttribution, safelyRecordStructuredRun } from "./structured-runs";
9
+ import type { AiDataBoundary, AiModelProfile } from "./types";
10
+
11
+ export const AI_AUDIO_MODEL_SETTING_KEY = "ai.audio_model_id";
12
+ /** Matches the existing chat turn execution budget; callers may cancel sooner. */
13
+ export const AI_TRANSCRIPTION_TIMEOUT_MS = 10 * 60_000;
14
+
15
+ export type AiResolvedAudioModel = { profile: AiModelProfile; provider: TranscriptionProvider };
16
+
17
+ export const createAiTranscriptionProvider = (profile: AiModelProfile, apiKey?: string): TranscriptionProvider => {
18
+ if (
19
+ profile.capabilities.length !== 1 ||
20
+ !profile.capabilities.includes("transcription") ||
21
+ (profile.provider !== "openai" && profile.provider !== "openai-compatible")
22
+ ) {
23
+ throw new Error("Choose an audio transcription profile with an OpenAI-compatible endpoint.");
24
+ }
25
+ const baseURL = profile.baseURL ?? (profile.provider === "openai" ? "https://api.openai.com/v1" : undefined);
26
+ if (!baseURL) throw new Error("Audio transcription requires a base URL.");
27
+ return openAICompatibleTranscription(profile.model, { name: profile.provider, baseURL, apiKey });
28
+ };
29
+
30
+ export const resolveAiAudioModel = async (
31
+ input: { requestedModelId?: string; allowedDataBoundaries?: AiDataBoundary[] } = {},
32
+ ): Promise<AiResolvedAudioModel> => {
33
+ const state = await readAiSettingsState();
34
+ if (!state.ok) throw Object.assign(new Error(state.error.message), { aiError: state.error });
35
+ if (!state.enabled) throw new Error("AI is disabled.");
36
+ const modelId = input.requestedModelId ?? String((await coreSettings.get<string>(AI_AUDIO_MODEL_SETTING_KEY)) ?? "").trim();
37
+ if (!modelId) throw new Error("No audio transcription model is configured.");
38
+ const profile = selectAiModelProfile(state, {
39
+ kind: "locked",
40
+ modelId,
41
+ requiredCapabilities: ["transcription"],
42
+ allowedDataBoundaries: input.allowedDataBoundaries,
43
+ });
44
+ const credential = await getAiCredential(profile.id);
45
+ if (profile.provider === "openai" && !credential?.trim()) throw new Error("The audio model is missing provider credentials.");
46
+ return { profile, provider: createAiTranscriptionProvider(profile, credential?.trim() || undefined) };
47
+ };
48
+
49
+ export type RunAiTranscriptionInput = {
50
+ task: string;
51
+ file: Blob;
52
+ filename: string;
53
+ language?: string;
54
+ prompt?: string;
55
+ requestedModelId?: string;
56
+ allowedDataBoundaries?: AiDataBoundary[];
57
+ signal?: AbortSignal;
58
+ appId?: string;
59
+ traceParent?: TraceContext;
60
+ /** Attribution is metadata; the caller must authorize the source before calling. */
61
+ attribution?: AiUsageAttribution;
62
+ resolveModel?: () => Promise<AiResolvedAudioModel>;
63
+ };
64
+
65
+ /** One provider call. Durable execution and input authorization belong to the caller. */
66
+ export const runAiTranscription = async (input: RunAiTranscriptionInput): Promise<{ text: string; modelProfileId: string }> => {
67
+ return trace.withSpan(
68
+ {
69
+ name: `ai.transcription.${input.task}`,
70
+ source: `ai:transcription:${input.task}`,
71
+ category: "ai",
72
+ appId: input.appId,
73
+ parent: input.traceParent,
74
+ },
75
+ async (span) => {
76
+ const startedAt = Date.now();
77
+ let failure: AiTranscriptionError | undefined;
78
+ let stage: TranscriptionStage = "input";
79
+ let resolved: AiResolvedAudioModel | undefined;
80
+ let succeeded = false;
81
+ let callId: string | undefined;
82
+ const timeout = AbortSignal.timeout(AI_TRANSCRIPTION_TIMEOUT_MS);
83
+ const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout;
84
+ try {
85
+ signal.throwIfAborted();
86
+ if (input.file.size > AI_AUDIO_MAX_BYTES) throw new Error("Audio transcription accepts files up to 25 MB.");
87
+ const format = inspectAiAudio(new Uint8Array(await input.file.arrayBuffer()));
88
+ stage = "configuration";
89
+ resolved = await (input.resolveModel?.() ?? resolveAiAudioModel(input));
90
+ const filename =
91
+ input.filename
92
+ .split(/[\\/]/)
93
+ .at(-1)
94
+ ?.replace(/\.[^.]*$/, "") || "audio";
95
+ stage = "provider";
96
+ callId = (
97
+ await beginAiCall(
98
+ { ...resolved.profile, pricing: undefined },
99
+ { kind: "background", task: input.task, appId: input.appId, traceId: span.traceId, ...input.attribution },
100
+ 0,
101
+ )
102
+ ).id;
103
+ const result = await resolved.provider.transcribe({
104
+ file: new Blob([input.file], { type: format.mediaType }),
105
+ filename: `${filename}.${format.extension}`,
106
+ language: input.language,
107
+ prompt: input.prompt,
108
+ signal,
109
+ });
110
+ signal.throwIfAborted();
111
+ succeeded = true;
112
+ return { text: result.text, modelProfileId: resolved.profile.id };
113
+ } catch (error) {
114
+ failure = describeTranscriptionFailure(error, stage, signal.aborted);
115
+ logger("ai:transcription")[input.signal?.aborted ? "info" : "error"](failure.message, {
116
+ traceId: span.traceId,
117
+ task: input.task,
118
+ modelProfileId: resolved?.profile.id ?? input.requestedModelId,
119
+ providerModel: resolved?.profile.model,
120
+ stage,
121
+ errorCode: failure.code,
122
+ httpStatus: failure.httpStatus,
123
+ });
124
+ throw failure;
125
+ } finally {
126
+ if (callId) {
127
+ try {
128
+ await finishAiCall(callId, undefined, succeeded ? "ok" : "failed");
129
+ } catch {
130
+ logger("ai:transcription").error("AI cost booking failed", { callId });
131
+ }
132
+ }
133
+ await safelyRecordStructuredRun({
134
+ traceId: span.traceId,
135
+ task: input.task,
136
+ appId: input.appId,
137
+ attribution: input.attribution,
138
+ modelProfileId: resolved?.profile.id,
139
+ providerModel: resolved?.profile.model,
140
+ mode: "transcription",
141
+ attempts: resolved ? 1 : 0,
142
+ status: succeeded ? "ok" : "failed",
143
+ durationMs: Date.now() - startedAt,
144
+ // Provider error bodies may echo source data. Keep accounting metadata-only.
145
+ errorCode: failure?.code,
146
+ error: failure?.message,
147
+ });
148
+ }
149
+ },
150
+ );
151
+ };
152
+
153
+ export type TranscriptionStage = "input" | "configuration" | "provider";
154
+ /** Only classified metadata escapes the adapter: response bodies can contain private source data. */
155
+ export class AiTranscriptionError extends Error {
156
+ constructor(
157
+ readonly code: string,
158
+ message: string,
159
+ readonly retryable = false,
160
+ readonly httpStatus?: number,
161
+ ) {
162
+ super(message);
163
+ this.name = "AiTranscriptionError";
164
+ }
165
+ }
166
+ export const describeTranscriptionFailure = (error: unknown, stage: TranscriptionStage, aborted = false): AiTranscriptionError => {
167
+ if (error instanceof AiTranscriptionError) return error;
168
+ if (aborted) return new AiTranscriptionError("transcription_aborted", "Audio transcription was canceled or timed out.");
169
+ const message = error instanceof Error ? error.message : "";
170
+ const match = /^(?:openai|openai-compatible) (\d{3}): /.exec(message);
171
+ if (match) {
172
+ const status = Number(match[1]);
173
+ const hint =
174
+ status === 401 || status === 403
175
+ ? "Check provider credentials and model access."
176
+ : status === 404
177
+ ? "Check the provider base URL and model name."
178
+ : status === 429
179
+ ? "The provider rate limit was reached."
180
+ : "The provider rejected the audio transcription request.";
181
+ return new AiTranscriptionError(
182
+ `transcription_http_${status}`,
183
+ `Audio provider HTTP ${status}. ${hint}`,
184
+ [408, 409, 425, 429].includes(status) || status >= 500,
185
+ status,
186
+ );
187
+ }
188
+ if (/^(?:openai|openai-compatible) connection failed: /.test(message))
189
+ return new AiTranscriptionError(
190
+ "transcription_connection_failed",
191
+ "Could not connect to the audio provider. Check its endpoint and network availability.",
192
+ true,
193
+ );
194
+ return new AiTranscriptionError(
195
+ `transcription_${stage}_failed`,
196
+ stage === "configuration"
197
+ ? "Audio model configuration could not be resolved. Check the audio profile, base URL, credentials and access policy."
198
+ : stage === "input"
199
+ ? "Audio input is invalid or exceeds the supported size."
200
+ : "Audio provider returned an invalid response or an unclassified error.",
201
+ );
202
+ };
@@ -0,0 +1,114 @@
1
+ import type { LoopAggregate, OutboundEvent } from "@k2b/nessi";
2
+ import { sql } from "bun";
3
+
4
+ export type TimingInterval = { start: number; end: number };
5
+ const union = (intervals: TimingInterval[]): TimingInterval[] => {
6
+ const result: TimingInterval[] = [];
7
+ for (const interval of intervals.filter((x) => x.end > x.start).sort((a, b) => a.start - b.start)) {
8
+ const previous = result.at(-1);
9
+ if (previous && interval.start <= previous.end) previous.end = Math.max(previous.end, interval.end);
10
+ else result.push({ ...interval });
11
+ }
12
+ return result;
13
+ };
14
+ const duration = (intervals: TimingInterval[]) => union(intervals).reduce((sum, x) => sum + x.end - x.start, 0);
15
+ const excluding = (intervals: TimingInterval[], removed: TimingInterval[]) => {
16
+ let result = union(intervals);
17
+ for (const cut of union(removed))
18
+ result = result.flatMap((x) =>
19
+ cut.end <= x.start || cut.start >= x.end
20
+ ? [x]
21
+ : [
22
+ { start: x.start, end: Math.min(x.end, cut.start) },
23
+ { start: Math.max(x.start, cut.end), end: x.end },
24
+ ].filter((x) => x.end > x.start),
25
+ );
26
+ return result;
27
+ };
28
+
29
+ /** Exclusive phase durations; parallel tools and nested approvals never double-count wall time. */
30
+ export function summarizeTurnTiming(input: {
31
+ start: number;
32
+ end: number;
33
+ generation: TimingInterval[];
34
+ tools: TimingInterval[];
35
+ waits: TimingInterval[];
36
+ outputTokens?: number;
37
+ }): NonNullable<LoopAggregate["timing"]> {
38
+ const clip = (rows: TimingInterval[]) => rows.map((x) => ({ start: Math.max(input.start, x.start), end: Math.min(input.end, x.end) }));
39
+ const generation = clip(input.generation),
40
+ waits = excluding(clip(input.waits), generation);
41
+ const generationMs = duration(generation),
42
+ actionWaitMs = duration(waits);
43
+ const toolExecutionMs = duration(excluding(clip(input.tools), [...generation, ...waits]));
44
+ const wallMs = Math.max(0, input.end - input.start);
45
+ return {
46
+ wallMs,
47
+ totalElapsedMs: wallMs,
48
+ generationMs,
49
+ actionWaitMs,
50
+ toolExecutionMs,
51
+ ...(generationMs > 0 && input.outputTokens !== undefined ? { outputTokensPerSecond: input.outputTokens / (generationMs / 1000) } : {}),
52
+ };
53
+ }
54
+
55
+ /** One recorder per executor attempt; rows survive client actions and process resumption. */
56
+ export function createTurnTimingRecorder(turnId: string, db = sql) {
57
+ let requestId: string | null = null;
58
+ const finishGeneration = async () => {
59
+ if (!requestId) return;
60
+ const id = requestId;
61
+ requestId = null;
62
+ await db`UPDATE ai.turn_generation_timings SET ended_at=now() WHERE turn_id=${turnId}::uuid AND request_id=${id} AND ended_at IS NULL`;
63
+ };
64
+ return {
65
+ finishGeneration,
66
+ async event(event: OutboundEvent) {
67
+ if (event.type === "turn_start" && !event.resumed) {
68
+ await finishGeneration();
69
+ requestId = event.turnId;
70
+ await db`INSERT INTO ai.turn_generation_timings(turn_id,request_id) VALUES(${turnId}::uuid,${requestId}) ON CONFLICT DO NOTHING`;
71
+ } else if (
72
+ event.type === "usage" ||
73
+ event.type === "tool_execution_start" ||
74
+ event.type === "turn_end" ||
75
+ event.type === "loop_end"
76
+ ) {
77
+ await finishGeneration();
78
+ }
79
+ },
80
+ };
81
+ }
82
+
83
+ export async function withDurableTurnTiming(turnId: string, aggregate: LoopAggregate, db = sql): Promise<LoopAggregate> {
84
+ const [turn] = await db<
85
+ { start: Date; end: Date }[]
86
+ >`SELECT created_at AS start,COALESCE(completed_at,now()) AS end FROM ai.turns WHERE id=${turnId}::uuid`;
87
+ const generation = await db<
88
+ { start: Date; end: Date | null }[]
89
+ >`SELECT started_at AS start,ended_at AS end FROM ai.turn_generation_timings WHERE turn_id=${turnId}::uuid`;
90
+ // An older/incomplete trace cannot justify a speed computed from the whole loop's tokens.
91
+ if (!turn || generation.length < aggregate.assistantMessageCount || generation.some((x) => !x.end)) {
92
+ const { timing: _, ...rest } = aggregate;
93
+ return rest;
94
+ }
95
+ const tools = await db<
96
+ { start: Date; end: Date }[]
97
+ >`SELECT started_at AS start,COALESCE(completed_at,now()) AS end FROM ai.tool_calls WHERE turn_id=${turnId}::uuid AND started_at IS NOT NULL`;
98
+ const waits = await db<{ start: Date; end: Date }[]>`SELECT a.created_at AS start,
99
+ CASE WHEN a.kind='client_tool' THEN LEAST(COALESCE(t.started_at,a.resolved_at,now()),COALESCE(a.resolved_at,now())) ELSE COALESCE(a.resolved_at,now()) END AS end
100
+ FROM ai.pending_actions a LEFT JOIN ai.tool_calls t ON t.turn_id=a.turn_id AND t.call_id=a.call_id WHERE a.turn_id=${turnId}::uuid`;
101
+ const intervals = (rows: { start: Date; end: Date | null }[]) =>
102
+ rows.flatMap((x) => (x.end ? [{ start: new Date(x.start).getTime(), end: new Date(x.end).getTime() }] : []));
103
+ return {
104
+ ...aggregate,
105
+ timing: summarizeTurnTiming({
106
+ start: new Date(turn.start).getTime(),
107
+ end: new Date(turn.end).getTime(),
108
+ generation: intervals(generation),
109
+ tools: intervals(tools),
110
+ waits: intervals(waits),
111
+ outputTokens: aggregate.usage?.output,
112
+ }),
113
+ };
114
+ }
package/src/ai/types.ts CHANGED
@@ -16,7 +16,7 @@ import type { CapabilityActionReview, CloudResourceRef } from "../contracts/capa
16
16
  import type { RequestActor } from "../server";
17
17
  import type { AiTurnBlock } from "./protocol";
18
18
 
19
- export const AI_MODEL_CAPABILITIES = ["streaming", "tools", "vision"] as const;
19
+ export const AI_MODEL_CAPABILITIES = ["streaming", "tools", "vision", "transcription"] as const;
20
20
  export type AiModelCapability = (typeof AI_MODEL_CAPABILITIES)[number];
21
21
 
22
22
  export const AI_IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"] as const;
@@ -49,8 +49,7 @@ export type AiModelProfile = {
49
49
  maxLoadedTools?: number;
50
50
  /** Tool-using model rounds per chat turn. Missing or <= 0 is unlimited. */
51
51
  maxToolRounds?: number;
52
- creditsPerInputToken?: number;
53
- creditsPerOutputToken?: number;
52
+ pricing?: import("../shared/ai-costs").AiModelPricing;
54
53
  };
55
54
 
56
55
  export type AiPublicModelProfile = Pick<
@@ -118,8 +117,9 @@ export type AiResolvedModel = {
118
117
 
119
118
  export type AiDraftContentPart =
120
119
  | { type: "text"; text: string }
121
- | { type: "resource"; ref: CloudResourceRef; title?: string; icon?: string; href?: string }
122
- | { type: "file"; path: string; mediaType: string; size: number; version: number };
120
+ | { type: "resource"; ref: CloudResourceRef; title?: string; icon?: string; href?: string; inline?: boolean }
121
+ | { type: "file"; path: string; mediaType: string; size: number; version: number; inline?: boolean }
122
+ | { type: "project-file"; path: string; inline?: boolean };
123
123
 
124
124
  export type AiConversationDraft = {
125
125
  content: AiDraftContentPart[];
@@ -129,10 +129,12 @@ export type AiConversationDraft = {
129
129
 
130
130
  export type AiConversationFieldSource = "default" | "auto" | "user";
131
131
  export type AiConversationTitleSource = AiConversationFieldSource;
132
- export type AiConversationRunStatus = "idle" | "queued" | "running" | "needs_attention" | "failed";
133
- export type AiConversationStatusFilter = Exclude<AiConversationRunStatus, "idle" | "queued"> | "unread";
132
+ export type AiConversationRunStatus = "idle" | "queued" | "running" | "needs_attention" | "waiting_for_browser" | "failed";
133
+ export type AiConversationStatusFilter = Exclude<AiConversationRunStatus, "idle" | "queued" | "waiting_for_browser"> | "unread";
134
134
 
135
135
  export type AiConversation = {
136
+ activity?: { completed: number; total: number; step: string | null; tool: string | null };
137
+ todoPlan?: { seq: number; todos: import("./todo-contracts").AiTodoPlan["todos"] };
136
138
  id: string;
137
139
  shortId: string;
138
140
  title: string;
@@ -146,6 +148,11 @@ export type AiConversation = {
146
148
  keywords: string[];
147
149
  pinnedAt: string | null;
148
150
  archivedAt: string | null;
151
+ /** null: automatic after seven days without use; false: explicitly active; true: finished. */
152
+ done: boolean | null;
153
+ isDone: boolean;
154
+ hasActiveSchedule?: boolean;
155
+ lastUsedAt: string;
149
156
  runStatus: AiConversationRunStatus;
150
157
  /** Error from the latest turn when `runStatus` is `failed`. */
151
158
  runError: string | null;
@@ -247,7 +254,7 @@ export type AiConversationPage = {
247
254
 
248
255
  export type AiConversationProjectUpdateResult =
249
256
  | { ok: true; conversation: AiConversation }
250
- | { ok: false; reason: "not_found" | "active_turn" };
257
+ | { ok: false; reason: "not_found" | "active_turn" | "already_assigned" };
251
258
 
252
259
  export type AiStoredMessage = {
253
260
  id: string;
@@ -270,7 +277,10 @@ export type AiStoredMessage = {
270
277
  compactedAt: string | null;
271
278
  /** UI metadata (e.g. how many messages a compaction summary replaced). */
272
279
  meta: {
280
+ /** The atomically consumed composer revision, used to confirm a local send. */
281
+ submittedDraftRevision?: number;
273
282
  compactedCount?: number;
283
+ todoPlan?: import("./todo-contracts").AiTodoPlan;
274
284
  steerId?: string;
275
285
  agentMessage?: {
276
286
  id: string;
@@ -336,6 +346,9 @@ export type AiTurnFinalizedEvent = {
336
346
  };
337
347
 
338
348
  export type AiTurn = {
349
+ deadline?: string | null;
350
+ runBudgetMs?: number | null;
351
+ cancelRequestedAt?: string | null;
339
352
  id: string;
340
353
  shortId: string;
341
354
  conversationId: string;
@@ -419,7 +432,17 @@ export type AiPendingTurnActionRecord = {
419
432
 
420
433
  export type AiTurnToolSource = { kind: "none" } | { kind: "default"; appTools?: boolean };
421
434
 
422
- export type AiClientToolId = "local_bash";
435
+ export type AiClientToolId =
436
+ | "local_bash"
437
+ | "code_run"
438
+ | "code_action"
439
+ | "code_inspect"
440
+ | "code_interact"
441
+ | "code_stop"
442
+ | "code_open"
443
+ | "code_present"
444
+ | "code_export"
445
+ | "code_secret";
423
446
 
424
447
  /** Immutable project instructions and context manifest captured for one turn. */
425
448
  export type AiProjectPromptSnapshot = {
@@ -517,6 +540,7 @@ export type AiConversationSourceObservation = {
517
540
  };
518
541
 
519
542
  export type AiConversationFileSnapshotEntry = {
543
+ dictationRecordedAt?: string;
520
544
  path: string;
521
545
  size: number;
522
546
  mediaType: string;
@@ -531,7 +555,15 @@ export type AiConversationFileSnapshot = {
531
555
  total: number;
532
556
  };
533
557
 
558
+ export type AiBackgroundRunContext = {
559
+ taskId: string;
560
+ occurrenceId: string;
561
+ context: Array<{ seq: number; kind: "message" | "summary"; message: Message }>;
562
+ };
563
+
534
564
  export type AiChatTurnRunConfig = {
565
+ background?: AiBackgroundRunContext;
566
+ selectedSkillIds?: string[];
535
567
  /** Server-owned marker: model grants apply only to interactive Assistant chat. */
536
568
  assistantChat?: true;
537
569
  kind?: "chat";
@@ -614,16 +646,18 @@ export type AiConversationService = {
614
646
  search?: string;
615
647
  refs?: CloudResourceRef[];
616
648
  archived?: boolean;
649
+ done?: boolean;
617
650
  status?: AiConversationStatusFilter;
618
651
  projectId?: string;
619
652
  unassigned?: boolean;
620
653
  limit?: number;
621
654
  }): Promise<AiConversation[]>;
622
- listSidebarConversations(input: { ownerUserId: string; unassignedLimit?: number; perProjectLimit?: number }): Promise<AiConversation[]>;
655
+ listSidebarConversations(input: { ownerUserId: string }): Promise<AiConversation[]>;
623
656
  listConversationsPage(input: {
624
657
  ownerUserId: string;
625
658
  search?: string;
626
659
  archived?: boolean;
660
+ done?: boolean;
627
661
  status?: AiConversationStatusFilter;
628
662
  projectId?: string;
629
663
  unassigned?: boolean;
@@ -712,8 +746,14 @@ export type AiConversationService = {
712
746
  conversationId: string;
713
747
  ownerUserId?: string;
714
748
  projectId: string | null;
749
+ onlyUnassigned?: boolean;
715
750
  }): Promise<AiConversationProjectUpdateResult>;
716
751
  setConversationPinned(input: { conversationId: string; ownerUserId?: string; pinned: boolean }): Promise<AiConversation | null>;
752
+ setConversationDone(input: {
753
+ conversationId: string;
754
+ ownerUserId: string;
755
+ done: boolean | null;
756
+ }): Promise<{ ok: true; conversation: AiConversation } | { ok: false; reason: "not_found" | "active_turn" }>;
717
757
  archiveConversation(input: { conversationId: string; ownerUserId?: string }): Promise<boolean>;
718
758
  restoreConversation(input: { conversationId: string; ownerUserId?: string }): Promise<AiConversation | null>;
719
759
  markConversationViewed(input: { conversationId: string; ownerUserId?: string }): Promise<boolean>;
@@ -788,6 +828,7 @@ export type AiConversationService = {
788
828
  }): Promise<void>;
789
829
  compactMessages(input: {
790
830
  conversationId: string;
831
+ turnId?: string;
791
832
  checkpointSeq: number;
792
833
  summary: Message;
793
834
  modelProfileId?: string | null;
@@ -803,6 +844,7 @@ export type AiConversationService = {
803
844
  createCompactionTurn(input: { conversationId: string; modelProfileId: string; runConfig: AiCompactionTurnRunConfig }): Promise<AiTurn>;
804
845
  /** Persist the user message and create its turn in one transaction. */
805
846
  submitChatTurn(input: {
847
+ queuedMessageId?: string;
806
848
  conversationId: string;
807
849
  modelProfileId: string;
808
850
  runConfig: AiChatTurnRunConfig;
@@ -819,6 +861,7 @@ export type AiConversationService = {
819
861
  retrySourceTurnId?: string;
820
862
  }): Promise<{ turn: AiTurn; message: AiStoredMessage }>;
821
863
  getTurnRunConfig(input: { conversationId: string; turnId: string }): Promise<AiTurnRunConfig | null>;
864
+ getLatestTurn(input: { conversationId: string }): Promise<AiTurn | null>;
822
865
  getTurn(input: { conversationId: string; turnId: string }): Promise<AiTurn | null>;
823
866
  getTurnByShortId(input: { conversationId: string; shortId: string }): Promise<AiTurn | null>;
824
867
  getActiveTurn(input: { conversationId: string }): Promise<{ turn: AiTurn; liveBlocks: AiTurnBlock[]; liveSeq: number } | null>;
@@ -888,6 +931,9 @@ export type AiConversationService = {
888
931
  /** Atomically make pending steers visible to the model and return them in order. */
889
932
  takePendingTurnSteers(input: { conversationId: string; turnId: string; leaseOwner: string }): Promise<AiTurnSteer[]>;
890
933
  createSessionStore(input: {
934
+ background?: AiBackgroundRunContext;
935
+ /** Called after a durable append, for incremental usage/history projection. */
936
+ onMessage?: (message: AiStoredMessage) => Promise<void>;
891
937
  conversationId: string;
892
938
  modelProfileId?: string | null;
893
939
  turnId?: string | null;
@@ -956,6 +1002,8 @@ export type AiToolRuntime<TInput extends z.ZodType = z.ZodType, TOutput extends
956
1002
  selectedModel?: AiResolvedModel;
957
1003
  locale?: string;
958
1004
  timeZone?: string;
1005
+ /** Localized short status; never include payloads or secrets. */
1006
+ reportProgress?: (message: string) => Promise<void>;
959
1007
  },
960
1008
  ): Promise<z.infer<TOutput>>;
961
1009
  }
package/src/ai/ui.tsx CHANGED
@@ -1,3 +1,5 @@
1
+ export { AiTurnBlockView } from "./chat/blocks";
2
+
1
3
  import { latestLoopUsage, latestUsage, latestUsageSnapshot, textFromMessage } from "./chat/message-utils";
2
4
 
3
5
  export {
@@ -9,6 +11,7 @@ export {
9
11
  aiChatAttachments,
10
12
  aiChatModelOptions,
11
13
  aiComposerAttachmentRecords,
14
+ aiComposerDraft,
12
15
  aiComposerFileAccept,
13
16
  aiComposerSendInput,
14
17
  createAiPastedTextFile,