nolo-cli 0.1.12 → 0.1.14

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 (323) hide show
  1. package/README.md +54 -2
  2. package/agent-runtime/hostAdapter.ts +53 -0
  3. package/agent-runtime/index.ts +28 -0
  4. package/agent-runtime/localLoop.ts +62 -0
  5. package/agent-runtime/runtimeDecision.ts +70 -0
  6. package/agent-runtime/types.ts +87 -0
  7. package/agentRuntimeCommands.ts +141 -22
  8. package/agentRuntimeLocal.ts +7 -0
  9. package/ai/agent/_executeModel.ts +118 -0
  10. package/ai/agent/agentSlice.ts +545 -0
  11. package/ai/agent/appWorkingMemory.ts +126 -0
  12. package/ai/agent/avatarUtils.ts +24 -0
  13. package/ai/agent/buildEditingContext.ts +373 -0
  14. package/ai/agent/buildSystemPrompt.ts +532 -0
  15. package/ai/agent/cleanAgentMessages.ts +140 -0
  16. package/ai/agent/cliChatClient.ts +119 -0
  17. package/ai/agent/contextCompiler.ts +107 -0
  18. package/ai/agent/contextLayerContract.ts +44 -0
  19. package/ai/agent/createAgentSchema.ts +234 -0
  20. package/ai/agent/executeToolCall.ts +58 -0
  21. package/ai/agent/fetchAgentContexts.ts +42 -0
  22. package/ai/agent/generatePrompt.ts +3 -0
  23. package/ai/agent/getFullChatContextKeys.ts +168 -0
  24. package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
  25. package/ai/agent/hooks/useAgentConfig.ts +61 -0
  26. package/ai/agent/hooks/useAgentDialog.ts +35 -0
  27. package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
  28. package/ai/agent/hooks/usePublicAgents.ts +473 -0
  29. package/ai/agent/persistMessageWithFixedId.ts +37 -0
  30. package/ai/agent/planSlice.ts +259 -0
  31. package/ai/agent/referenceUtils.ts +229 -0
  32. package/ai/agent/runAgentBackground.ts +238 -0
  33. package/ai/agent/runAgentClientLoop.ts +138 -0
  34. package/ai/agent/runtimeGuidance.ts +97 -0
  35. package/ai/agent/runtimeServerBase.ts +37 -0
  36. package/ai/agent/server/fetchPublicAgents.ts +128 -0
  37. package/ai/agent/startParallelAgentStreams.ts +424 -0
  38. package/ai/agent/startupProtocol.ts +53 -0
  39. package/ai/agent/streamAgentChatTurn.ts +1299 -0
  40. package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
  41. package/ai/agent/types.ts +71 -0
  42. package/ai/agent/utils/imageOutput.ts +39 -0
  43. package/ai/agent/utils/publicImageAgentMode.ts +26 -0
  44. package/ai/agent/utils/sortUtils.ts +250 -0
  45. package/ai/agent/web/referencePickerUtils.ts +146 -0
  46. package/ai/agent.ts +2 -0
  47. package/ai/ai.locale.ts +1083 -0
  48. package/ai/chat/accumulateToolCallChunks.ts +95 -0
  49. package/ai/chat/fetchUtils.native.ts +276 -0
  50. package/ai/chat/fetchUtils.ts +153 -0
  51. package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
  52. package/ai/chat/parseApiError.ts +64 -0
  53. package/ai/chat/parseMultilineSSE.ts +95 -0
  54. package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
  55. package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
  56. package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
  57. package/ai/chat/shouldUseServerProxy.ts +18 -0
  58. package/ai/chat/sseClient.native.ts +91 -0
  59. package/ai/chat/sseClient.ts +67 -0
  60. package/ai/chat/streamReader.native.ts +31 -0
  61. package/ai/chat/streamReader.ts +62 -0
  62. package/ai/chat/updateTotalUsage.ts +72 -0
  63. package/ai/context/buildReferenceContext.ts +437 -0
  64. package/ai/context/calculateContextUsage.ts +133 -0
  65. package/ai/context/retention.ts +165 -0
  66. package/ai/context/tokenUtils.ts +78 -0
  67. package/ai/index.ts +1 -0
  68. package/ai/llm/agentCapabilities.ts +74 -0
  69. package/ai/llm/calculateGeminiImageTokens.ts +57 -0
  70. package/ai/llm/deepinfra.ts +28 -0
  71. package/ai/llm/fireworks.ts +68 -0
  72. package/ai/llm/generateRequestBody.ts +165 -0
  73. package/ai/llm/getModelContextWindow.ts +84 -0
  74. package/ai/llm/getNoloKey.ts +37 -0
  75. package/ai/llm/getPricing.ts +232 -0
  76. package/ai/llm/hooks/useModelPricing.ts +75 -0
  77. package/ai/llm/imagePricing.ts +66 -0
  78. package/ai/llm/isResponseAPIModel.ts +13 -0
  79. package/ai/llm/kimi.ts +18 -0
  80. package/ai/llm/mimo.ts +71 -0
  81. package/ai/llm/mistral.ts +22 -0
  82. package/ai/llm/modelAvatar.ts +427 -0
  83. package/ai/llm/models.ts +45 -0
  84. package/ai/llm/openrouterModels.ts +141 -0
  85. package/ai/llm/providers.ts +307 -0
  86. package/ai/llm/reasoningModels.ts +28 -0
  87. package/ai/llm/types.ts +59 -0
  88. package/ai/llm/usageRequestOptions.ts +59 -0
  89. package/ai/memory/capture.ts +148 -0
  90. package/ai/memory/consolidate.ts +104 -0
  91. package/ai/memory/delete.ts +147 -0
  92. package/ai/memory/overlay.ts +84 -0
  93. package/ai/memory/query.ts +38 -0
  94. package/ai/memory/queryShared.ts +160 -0
  95. package/ai/memory/rank.ts +105 -0
  96. package/ai/memory/recentRelationshipRecap.ts +247 -0
  97. package/ai/memory/remember.ts +167 -0
  98. package/ai/memory/runtime.ts +76 -0
  99. package/ai/memory/store.ts +20 -0
  100. package/ai/memory/storeShared.ts +76 -0
  101. package/ai/memory/types.ts +46 -0
  102. package/ai/memory/understanding.ts +349 -0
  103. package/ai/memory/understandingGreeting.ts +264 -0
  104. package/ai/messages/type.ts +20 -0
  105. package/ai/policy/personalizationDialog.ts +333 -0
  106. package/ai/policy/runtimePolicy.ts +440 -0
  107. package/ai/policy/selfUpdateFields.ts +48 -0
  108. package/ai/policy/types.ts +64 -0
  109. package/ai/skills/referenceRuntime.ts +274 -0
  110. package/ai/skills/skillDiagnostics.ts +251 -0
  111. package/ai/skills/skillDocBuilder.ts +139 -0
  112. package/ai/skills/skillDocProtocol.ts +434 -0
  113. package/ai/skills/skillReferenceSummary.ts +63 -0
  114. package/ai/skills/skillSummaryMarker.ts +26 -0
  115. package/ai/token/calculatePrice.ts +546 -0
  116. package/ai/token/db.ts +98 -0
  117. package/ai/token/externalToolCost.ts +321 -0
  118. package/ai/token/hooks/useRecords.ts +65 -0
  119. package/ai/token/missingUsageEstimate.ts +42 -0
  120. package/ai/token/modelUsageQuery.ts +252 -0
  121. package/ai/token/normalizeUsage.ts +84 -0
  122. package/ai/token/openaiImageGenerationUsage.ts +56 -0
  123. package/ai/token/prepareTokenUsageData.ts +88 -0
  124. package/ai/token/query.ts +88 -0
  125. package/ai/token/queryUserTokens.ts +59 -0
  126. package/ai/token/resolveBillingTarget.ts +52 -0
  127. package/ai/token/saveTokenRecord.ts +53 -0
  128. package/ai/token/serverDialogProjection.ts +78 -0
  129. package/ai/token/serverTokenWriter.ts +143 -0
  130. package/ai/token/stats.ts +21 -0
  131. package/ai/token/tokenThunks.ts +24 -0
  132. package/ai/token/types.ts +93 -0
  133. package/ai/tools/agent/agentTools.ts +176 -0
  134. package/ai/tools/agent/agentUpdateShared.ts +311 -0
  135. package/ai/tools/agent/callAgentTool.ts +139 -0
  136. package/ai/tools/agent/createAgentTool.ts +512 -0
  137. package/ai/tools/agent/createDialogTool.ts +69 -0
  138. package/ai/tools/agent/createSkillAgentTool.ts +62 -0
  139. package/ai/tools/agent/parallelBudget.ts +221 -0
  140. package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
  141. package/ai/tools/agent/runLlmTool.ts +96 -0
  142. package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
  143. package/ai/tools/agent/skillAgentArgs.ts +106 -0
  144. package/ai/tools/agent/skillAgentPreset.ts +89 -0
  145. package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
  146. package/ai/tools/agent/updateAgentTool.ts +96 -0
  147. package/ai/tools/agent/updateSelfTool.ts +113 -0
  148. package/ai/tools/amazonProductScraperTool.ts +86 -0
  149. package/ai/tools/apifyActorClient.ts +45 -0
  150. package/ai/tools/appEditGuard.ts +372 -0
  151. package/ai/tools/appReadSnapshot.ts +153 -0
  152. package/ai/tools/appTools.ts +1549 -0
  153. package/ai/tools/applyEditTool.ts +256 -0
  154. package/ai/tools/applyLineEditsTool.ts +312 -0
  155. package/ai/tools/browserTools/click.ts +33 -0
  156. package/ai/tools/browserTools/closeSession.ts +29 -0
  157. package/ai/tools/browserTools/common.ts +27 -0
  158. package/ai/tools/browserTools/openSession.ts +48 -0
  159. package/ai/tools/browserTools/readContent.ts +38 -0
  160. package/ai/tools/browserTools/selectOption.ts +46 -0
  161. package/ai/tools/browserTools/typeText.ts +42 -0
  162. package/ai/tools/category/createCategoryTool.ts +66 -0
  163. package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
  164. package/ai/tools/category/updateContentCategoryTool.ts +75 -0
  165. package/ai/tools/cfBrowserTools.ts +319 -0
  166. package/ai/tools/cfSpeechToTextTool.ts +49 -0
  167. package/ai/tools/checkEnvTool.ts +65 -0
  168. package/ai/tools/cloudflareCrawlTool.ts +289 -0
  169. package/ai/tools/codeSearchTool.ts +111 -0
  170. package/ai/tools/codeTools.ts +101 -0
  171. package/ai/tools/createDocTool.ts +132 -0
  172. package/ai/tools/createPlanTool.ts +999 -0
  173. package/ai/tools/createSkillDocTool.ts +155 -0
  174. package/ai/tools/createWorkflowTool.ts +154 -0
  175. package/ai/tools/deepseekOcrTool.ts +34 -0
  176. package/ai/tools/delayTool.ts +31 -0
  177. package/ai/tools/deleteSpacesTool.ts +325 -0
  178. package/ai/tools/deleteSpacesToolModel.ts +159 -0
  179. package/ai/tools/devReloadUtils.ts +29 -0
  180. package/ai/tools/dialogMessageSearch.ts +137 -0
  181. package/ai/tools/doctorSkillTool.ts +72 -0
  182. package/ai/tools/ecommerceScraperTool.ts +86 -0
  183. package/ai/tools/emailTools.ts +549 -0
  184. package/ai/tools/evalSkillTool.ts +92 -0
  185. package/ai/tools/exaSearchTool.ts +64 -0
  186. package/ai/tools/execBashTool.ts +379 -0
  187. package/ai/tools/executeSqlTool.ts +192 -0
  188. package/ai/tools/fetchWebpageSupport.ts +309 -0
  189. package/ai/tools/fetchWebpageTool.ts +84 -0
  190. package/ai/tools/geminiImagePreviewTool.ts +361 -0
  191. package/ai/tools/generateDocxTool.ts +215 -0
  192. package/ai/tools/googleSearchScraperTool.ts +106 -0
  193. package/ai/tools/importDataTool.ts +133 -0
  194. package/ai/tools/importSkillTool.ts +162 -0
  195. package/ai/tools/index.ts +1927 -0
  196. package/ai/tools/listFilesTool.ts +82 -0
  197. package/ai/tools/listUserSpacesTool.ts +113 -0
  198. package/ai/tools/modelUsageTools.ts +199 -0
  199. package/ai/tools/olmOcrTool.ts +34 -0
  200. package/ai/tools/openaiImageTool.ts +267 -0
  201. package/ai/tools/prepareTools.ts +23 -0
  202. package/ai/tools/readDocTool.ts +84 -0
  203. package/ai/tools/readFileTool.ts +211 -0
  204. package/ai/tools/readTool.ts +163 -0
  205. package/ai/tools/readXPostTool.ts +233 -0
  206. package/ai/tools/rememberMemoryTool.ts +84 -0
  207. package/ai/tools/remotionVideoTool.ts +151 -0
  208. package/ai/tools/searchDialogMessagesTool.ts +222 -0
  209. package/ai/tools/searchRepoTool.ts +115 -0
  210. package/ai/tools/searchWorkspaceTool.ts +259 -0
  211. package/ai/tools/skillFollowup.ts +86 -0
  212. package/ai/tools/surfWeatherTool.ts +169 -0
  213. package/ai/tools/table/addTableRowTool.ts +217 -0
  214. package/ai/tools/table/createTableTool.ts +315 -0
  215. package/ai/tools/table/rowTools.ts +366 -0
  216. package/ai/tools/table/schemaTools.ts +244 -0
  217. package/ai/tools/table/shareTableTool.ts +148 -0
  218. package/ai/tools/table/toolShared.ts +129 -0
  219. package/ai/tools/toolApiClient.ts +198 -0
  220. package/ai/tools/toolNameAliases.ts +57 -0
  221. package/ai/tools/toolResultError.ts +42 -0
  222. package/ai/tools/toolRunSlice.ts +303 -0
  223. package/ai/tools/toolSchemaCompatibility.ts +53 -0
  224. package/ai/tools/toolVisibility.ts +4 -0
  225. package/ai/tools/types.ts +20 -0
  226. package/ai/tools/uiAskChoiceTool.ts +104 -0
  227. package/ai/tools/updateContentTitleTool.ts +84 -0
  228. package/ai/tools/updateDocTool.ts +105 -0
  229. package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
  230. package/ai/tools/whisperTool.ts +77 -0
  231. package/ai/tools/writeFileTool.ts +210 -0
  232. package/ai/tools/youtubeScraperTool.ts +116 -0
  233. package/ai/tools/ziweiChartTool.ts +678 -0
  234. package/ai/types.ts +55 -0
  235. package/ai/workflow/workflowExecutor.ts +323 -0
  236. package/ai/workflow/workflowSlice.ts +73 -0
  237. package/ai/workflow/workflowTypes.ts +106 -0
  238. package/authCommands.ts +185 -21
  239. package/client/agentRun.test.ts +240 -0
  240. package/client/agentRun.ts +182 -19
  241. package/client/compactDialog.test.ts +238 -0
  242. package/client/compactDialog.ts +5 -2
  243. package/client/localRuntimeAdapter.test.ts +135 -0
  244. package/client/localRuntimeAdapter.ts +244 -0
  245. package/client/profileConfig.test.ts +40 -0
  246. package/client/streamingOutput.test.ts +22 -0
  247. package/client/streamingOutput.ts +38 -0
  248. package/commandRegistry.ts +11 -2
  249. package/connector-experimental/index.ts +5 -0
  250. package/database/actions/cacheMergedUserData.ts +64 -0
  251. package/database/actions/common.ts +242 -0
  252. package/database/actions/deleteFile.ts +40 -0
  253. package/database/actions/fetchUserData.ts +16 -0
  254. package/database/actions/fileContent.ts +125 -0
  255. package/database/actions/patch.ts +155 -0
  256. package/database/actions/read.ts +337 -0
  257. package/database/actions/readAndWait.ts +224 -0
  258. package/database/actions/readRequestManager.ts +120 -0
  259. package/database/actions/remove.ts +94 -0
  260. package/database/actions/replication.ts +366 -0
  261. package/database/actions/upload.ts +174 -0
  262. package/database/actions/upsert.ts +56 -0
  263. package/database/actions/write.ts +126 -0
  264. package/database/client/db.native.ts +73 -0
  265. package/database/client/db.ts +51 -0
  266. package/database/client/fetchUserData.ts +61 -0
  267. package/database/client/handleError.ts +19 -0
  268. package/database/client/queryRequest.ts +21 -0
  269. package/database/config.ts +21 -0
  270. package/database/dbActionThunks.ts +1 -0
  271. package/database/dbSlice.ts +149 -0
  272. package/database/email.ts +42 -0
  273. package/database/fileRing.ts +51 -0
  274. package/database/fileSharding.ts +70 -0
  275. package/database/fileStorage.native.ts +92 -0
  276. package/database/fileStorage.ts +232 -0
  277. package/database/fileUrl.ts +34 -0
  278. package/database/hooks/useUserData.ts +489 -0
  279. package/database/index.ts +1 -0
  280. package/database/keys.ts +765 -0
  281. package/database/queryPrefixes.ts +14 -0
  282. package/database/requests.ts +443 -0
  283. package/database/runtimeServerContext.ts +35 -0
  284. package/database/server/MemoryDB.ts +76 -0
  285. package/database/server/actorAccess.ts +76 -0
  286. package/database/server/agentDelegation.ts +124 -0
  287. package/database/server/coreDataOwnership.ts +13 -0
  288. package/database/server/coreDataProxy.ts +76 -0
  289. package/database/server/cybotReadonly.ts +18 -0
  290. package/database/server/dataHandlers.ts +111 -0
  291. package/database/server/db.ts +118 -0
  292. package/database/server/dbPath.ts +20 -0
  293. package/database/server/delete.ts +499 -0
  294. package/database/server/emailRepository.ts +1480 -0
  295. package/database/server/ensureDbOpen.ts +12 -0
  296. package/database/server/fileRead.ts +337 -0
  297. package/database/server/fileService.ts +436 -0
  298. package/database/server/handleTransaction.ts +86 -0
  299. package/database/server/patch.ts +282 -0
  300. package/database/server/query.ts +138 -0
  301. package/database/server/read.ts +325 -0
  302. package/database/server/resourceAccess.ts +211 -0
  303. package/database/server/routes.ts +110 -0
  304. package/database/server/spaceMemberAuthority.ts +67 -0
  305. package/database/server/upload.ts +159 -0
  306. package/database/server/write.ts +494 -0
  307. package/database/server/writeAuthority.ts +133 -0
  308. package/database/sqliteDb.ts +46 -0
  309. package/database/table/deleteTable.ts +120 -0
  310. package/database/tenantPlacement.ts +57 -0
  311. package/database/tombstones.ts +52 -0
  312. package/database/userDataLoadDecision.ts +17 -0
  313. package/database/userDataMerge.ts +95 -0
  314. package/database/userPreferenceRegister.ts +108 -0
  315. package/database/utils/dbPath.ts +47 -0
  316. package/database/utils/ulid.native.ts +6 -0
  317. package/database/utils/ulid.ts +1 -0
  318. package/index.ts +25 -15
  319. package/localRuntimeDb.ts +28 -0
  320. package/package.json +16 -4
  321. package/runtimeModeArgs.ts +33 -0
  322. package/tui/readlineWorkspace.ts +1 -0
  323. package/tui/session.ts +22 -0
@@ -1,5 +1,9 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
+ import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
5
+ import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
6
+ import { createStreamingTextWriter } from "./streamingOutput";
3
7
 
4
8
  type EnvLike = Record<string, string | undefined>;
5
9
 
@@ -15,10 +19,13 @@ type RunAgentTurnOptions = {
15
19
  continueDialogId?: string;
16
20
  scriptDir: string;
17
21
  env: EnvLike;
18
- output: OutputLike;
19
- scriptPathExists?: (path: string) => boolean;
20
- fetchImpl?: typeof fetch;
21
- };
22
+ output: OutputLike;
23
+ runtimeMode?: AgentRuntimeRequestedMode;
24
+ localRuntimeAdapter?: AgentRuntimeHostAdapter;
25
+ localRuntimeAdapterFactory?: (env: EnvLike) => AgentRuntimeHostAdapter;
26
+ scriptPathExists?: (path: string) => boolean;
27
+ fetchImpl?: typeof fetch;
28
+ };
22
29
 
23
30
  export type RunAgentTurnResult = {
24
31
  exitCode: number;
@@ -38,9 +45,23 @@ function resolveAuthToken(env: EnvLike) {
38
45
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
39
46
  }
40
47
 
41
- function shouldShowUsage(env: EnvLike) {
42
- return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
43
- }
48
+ function shouldShowUsage(env: EnvLike) {
49
+ return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
50
+ }
51
+
52
+ function resolveRequestedRuntimeMode(options: RunAgentTurnOptions) {
53
+ const envMode = options.env.NOLO_RUNTIME_MODE;
54
+ if (options.runtimeMode) return options.runtimeMode;
55
+ if (envMode === "local" || envMode === "server" || envMode === "auto") return envMode;
56
+ return "auto";
57
+ }
58
+
59
+ function buildDefaultLocalRuntimeAdapter(options: RunAgentTurnOptions) {
60
+ return createCliLocalRuntimeAdapter({
61
+ env: options.env,
62
+ fetchImpl: options.fetchImpl,
63
+ });
64
+ }
44
65
 
45
66
  function formatUsage(usage: any, dialogId: unknown) {
46
67
  const parts: string[] = [];
@@ -112,7 +133,7 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
112
133
  return { exitCode };
113
134
  }
114
135
 
115
- async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
136
+ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
116
137
  options.output.write(`\n${options.agentName} -> working...\n`);
117
138
 
118
139
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -132,12 +153,12 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
132
153
  host: "terminal",
133
154
  runtime: "bun",
134
155
  entrypoint: "nolo-cli",
135
- capabilities: ["text-io", "slash-commands"],
156
+ capabilities: ["text-io", "streaming", "slash-commands"],
136
157
  },
137
158
  ...(options.continueDialogId
138
159
  ? { continueDialogId: options.continueDialogId }
139
160
  : {}),
140
- stream: false,
161
+ stream: true,
141
162
  }),
142
163
  });
143
164
  } catch (error) {
@@ -145,6 +166,11 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
145
166
  return { exitCode: 1 };
146
167
  }
147
168
 
169
+ const contentType = res.headers.get("content-type") || "";
170
+ if (contentType.includes("text/event-stream") && res.body) {
171
+ return readStreamingAgentRun(options, res);
172
+ }
173
+
148
174
  let data: any = {};
149
175
  try {
150
176
  data = await res.json();
@@ -175,17 +201,154 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
175
201
  ? { dialogId: data.dialogId }
176
202
  : {}),
177
203
  };
178
- }
204
+ }
205
+
206
+ async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
207
+ const adapter =
208
+ options.localRuntimeAdapter ||
209
+ options.localRuntimeAdapterFactory?.(options.env) ||
210
+ buildDefaultLocalRuntimeAdapter(options);
211
+ if (!adapter) {
212
+ options.output.write("[nolo] Local runtime was requested but no local runtime adapter is available.\n");
213
+ return { exitCode: 1 };
214
+ }
215
+
216
+ options.output.write(`\n${options.agentName} -> working locally...\n`);
217
+ try {
218
+ const result = await runLocalAgentTurn({
219
+ adapter,
220
+ agentRef: options.agentKey,
221
+ input: options.message,
222
+ continueDialogId: options.continueDialogId,
223
+ });
224
+ const content = result.content.trim();
225
+ if (content) {
226
+ options.output.write(`\n${options.agentName} > ${content}\n`);
227
+ } else {
228
+ options.output.write(`\n${options.agentName} > (no text response)\n`);
229
+ }
230
+ return { exitCode: 0, dialogId: result.dialogId };
231
+ } catch (error) {
232
+ options.output.write(
233
+ `[nolo] Local agent run failed: ${
234
+ error instanceof Error ? error.message : String(error)
235
+ }\n`
236
+ );
237
+ return { exitCode: 1 };
238
+ }
239
+ }
240
+
241
+ async function readStreamingAgentRun(
242
+ options: RunAgentTurnOptions,
243
+ res: Response,
244
+ ): Promise<RunAgentTurnResult> {
245
+ const reader = res.body?.getReader();
246
+ if (!reader) {
247
+ options.output.write("[nolo] Agent stream response did not include a readable body.\n");
248
+ return { exitCode: 1 };
249
+ }
250
+
251
+ const decoder = new TextDecoder();
252
+ const writer = createStreamingTextWriter({
253
+ write: (chunk) => options.output.write(chunk),
254
+ });
255
+ let buffer = "";
256
+ let content = "";
257
+ let dialogId: string | undefined;
258
+ let usage: any;
259
+ let hasPrintedLabel = false;
260
+
261
+ const printLabel = () => {
262
+ if (hasPrintedLabel) return;
263
+ options.output.write(`\n${options.agentName} > `);
264
+ hasPrintedLabel = true;
265
+ };
266
+
267
+ const handlePayload = (payload: any) => {
268
+ if (payload?.error || payload?.type === "error") {
269
+ throw new Error(String(payload.error || payload.message || "Agent stream failed"));
270
+ }
271
+ if (payload?.type === "done") {
272
+ if (typeof payload.dialogId === "string") dialogId = payload.dialogId;
273
+ usage = payload.usage;
274
+ return;
275
+ }
276
+
277
+ const chunk =
278
+ payload?.type === "text"
279
+ ? payload.content
280
+ : typeof payload?.chunk === "string"
281
+ ? payload.chunk
282
+ : "";
283
+ if (!chunk) return;
284
+
285
+ printLabel();
286
+ content += chunk;
287
+ writer.push(chunk);
288
+ };
289
+
290
+ try {
291
+ while (true) {
292
+ const { done, value } = await reader.read();
293
+ if (done) break;
179
294
 
180
- export async function runAgentTurn(options: RunAgentTurnOptions) {
181
- const authToken = resolveAuthToken(options.env);
182
- const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
183
- const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
295
+ buffer += decoder.decode(value, { stream: true });
296
+ const events = buffer.split("\n\n");
297
+ buffer = events.pop() ?? "";
184
298
 
185
- if (shouldUseScriptBridge({ hasAuthToken: Boolean(authToken), scriptPathExists })) {
186
- return runScriptBridge(options, scriptPath);
299
+ for (const event of events) {
300
+ const dataLines = event
301
+ .split("\n")
302
+ .filter((line) => line.startsWith("data:"))
303
+ .map((line) => line.slice(5).trim())
304
+ .filter(Boolean);
305
+ for (const raw of dataLines) {
306
+ handlePayload(JSON.parse(raw));
307
+ }
308
+ }
309
+ }
310
+ if (buffer.trim()) {
311
+ const raw = buffer
312
+ .split("\n")
313
+ .find((line) => line.startsWith("data:"))
314
+ ?.slice(5)
315
+ .trim();
316
+ if (raw) handlePayload(JSON.parse(raw));
317
+ }
318
+ } catch (error) {
319
+ options.output.write(`\n[nolo] Agent stream failed: ${error instanceof Error ? error.message : String(error)}\n`);
320
+ return { exitCode: 1 };
321
+ } finally {
322
+ writer.flushAll();
187
323
  }
188
324
 
325
+ if (!content) {
326
+ options.output.write(`\n${options.agentName} > (no text response)\n`);
327
+ } else {
328
+ options.output.write("\n");
329
+ }
330
+
331
+ const usageText = formatUsage(usage, dialogId);
332
+ if (usageText && shouldShowUsage(options.env)) options.output.write(`${usageText}\n`);
333
+ return {
334
+ exitCode: 0,
335
+ ...(dialogId ? { dialogId } : {}),
336
+ };
337
+ }
338
+
339
+ export async function runAgentTurn(options: RunAgentTurnOptions) {
340
+ const authToken = resolveAuthToken(options.env);
341
+ const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
342
+ const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
343
+
344
+ if (resolveRequestedRuntimeMode(options) === "local") {
345
+ return runInjectedLocalAgentTurn(options);
346
+ }
347
+
348
+ if (shouldUseScriptBridge({ hasAuthToken: Boolean(authToken), scriptPathExists })) {
349
+ return runScriptBridge(options, scriptPath);
350
+ }
351
+
189
352
  if (!authToken) {
190
353
  options.output.write(
191
354
  "[nolo] This install needs an auth token before it can talk to agents.\n" +
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { compactDialog, parseTokenUserId } from "./compactDialog";
3
+
4
+ // A minimal valid JWT-style token with userId encoded in base64 payload.
5
+ // JWT format: header.payload.signature
6
+ // Payload: { "userId": "user01" }
7
+ const USER_ID = "user01";
8
+ const TOKEN_HEADER = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64");
9
+ const TOKEN_PAYLOAD = Buffer.from(JSON.stringify({ userId: USER_ID })).toString("base64");
10
+ const FAKE_TOKEN = `${TOKEN_HEADER}.${TOKEN_PAYLOAD}.fakesig`;
11
+
12
+ const OLD_DIALOG_ID = "01OLD0000000000000000000AB";
13
+ const OLD_DIALOG_KEY = `dialog-${USER_ID}-${OLD_DIALOG_ID}`;
14
+
15
+ const OLD_DIALOG_RECORD = {
16
+ id: OLD_DIALOG_ID,
17
+ dbKey: OLD_DIALOG_KEY,
18
+ type: "dialog",
19
+ title: "My dialog",
20
+ cybots: ["agent-pub-01NOLOAPPBLD000000019KCKT0"],
21
+ spaceId: "myspace",
22
+ createdAt: "2024-01-01T00:00:00.000Z",
23
+ updatedAt: "2024-01-01T00:00:00.000Z",
24
+ inputTokens: 100,
25
+ outputTokens: 200,
26
+ totalCost: 0.001,
27
+ // conversation summary/compression state that must NOT be carried to the fork
28
+ summary: "This is a long summary of the old conversation.",
29
+ summarizedBeforeId: "msg-old-summary-anchor",
30
+ proactiveSummary: "Short recap of prior topics.",
31
+ proactiveSummaryBeforeId: "msg-old-proactive-anchor",
32
+ compressionCount: 3,
33
+ summaryPending: true,
34
+ };
35
+
36
+ function makeFetchMock(options: {
37
+ dialogRecord?: Record<string, unknown>;
38
+ writeOk?: boolean;
39
+ patchOk?: boolean;
40
+ }) {
41
+ const calls: { url: string; method: string; body?: unknown }[] = [];
42
+
43
+ const fetchMock: typeof fetch = async (input, init) => {
44
+ const url = String(input);
45
+ const method = init?.method ?? "GET";
46
+ const bodyStr = typeof init?.body === "string" ? init.body : undefined;
47
+ const body = bodyStr ? JSON.parse(bodyStr) : undefined;
48
+ calls.push({ url, method, body });
49
+
50
+ if (method === "GET" || !method) {
51
+ return new Response(JSON.stringify(options.dialogRecord ?? OLD_DIALOG_RECORD), {
52
+ status: 200,
53
+ headers: { "Content-Type": "application/json" },
54
+ });
55
+ }
56
+ if (method === "POST") {
57
+ return new Response("{}", { status: options.writeOk !== false ? 200 : 500 });
58
+ }
59
+ if (method === "PATCH") {
60
+ return new Response("{}", { status: options.patchOk !== false ? 200 : 500 });
61
+ }
62
+ return new Response("{}", { status: 404 });
63
+ };
64
+
65
+ return { fetchMock, calls };
66
+ }
67
+
68
+ describe("compactDialog", () => {
69
+ test("reads current dialog, writes a forked copy, and returns the new dialog id", async () => {
70
+ const { fetchMock, calls } = makeFetchMock({});
71
+
72
+ const result = await compactDialog({
73
+ serverUrl: "http://localhost:8080",
74
+ authToken: FAKE_TOKEN,
75
+ dialogId: OLD_DIALOG_ID,
76
+ fetchImpl: fetchMock,
77
+ });
78
+
79
+ // Should have made 3 HTTP calls: read, write, patch (space)
80
+ expect(calls).toHaveLength(3);
81
+
82
+ // 1. Read old dialog
83
+ expect(calls[0]?.url).toContain(`/api/v1/db/read/${OLD_DIALOG_KEY}`);
84
+ expect(calls[0]?.method).toBe("GET");
85
+
86
+ // 2. Write new dialog
87
+ expect(calls[1]?.url).toContain("/api/v1/db/write/");
88
+ expect(calls[1]?.method).toBe("POST");
89
+ const writeBody = calls[1]?.body as any;
90
+ expect(writeBody?.data?.inheritedFromDialogKey).toBe(OLD_DIALOG_KEY);
91
+ expect(writeBody?.data?.cybots).toEqual(OLD_DIALOG_RECORD.cybots);
92
+ expect(writeBody?.data?.spaceId).toBe("myspace");
93
+ // Token stats should be reset
94
+ expect(writeBody?.data?.inputTokens).toBe(0);
95
+ expect(writeBody?.data?.outputTokens).toBe(0);
96
+ expect(writeBody?.data?.totalCost).toBe(0);
97
+ // Key should differ from old
98
+ expect(writeBody?.customKey).not.toBe(OLD_DIALOG_KEY);
99
+ expect(writeBody?.customKey).toMatch(/^dialog-user01-/);
100
+
101
+ // 3. Patch space
102
+ expect(calls[2]?.url).toContain("/api/v1/db/patch/space-myspace");
103
+ expect(calls[2]?.method).toBe("PATCH");
104
+
105
+ // Result
106
+ expect(result.dialogId).toBeDefined();
107
+ expect(result.dialogId).not.toBe(OLD_DIALOG_ID);
108
+ expect(result.dialogKey).toMatch(/^dialog-user01-/);
109
+ expect(result.spaceId).toBe("myspace");
110
+ });
111
+
112
+ test("does not patch space when the dialog has no spaceId", async () => {
113
+ const dialogWithoutSpace = { ...OLD_DIALOG_RECORD, spaceId: undefined };
114
+ const { fetchMock, calls } = makeFetchMock({ dialogRecord: dialogWithoutSpace });
115
+
116
+ await compactDialog({
117
+ serverUrl: "http://localhost:8080",
118
+ authToken: FAKE_TOKEN,
119
+ dialogId: OLD_DIALOG_ID,
120
+ fetchImpl: fetchMock,
121
+ });
122
+
123
+ // Only read + write, no patch
124
+ expect(calls).toHaveLength(2);
125
+ expect(calls.every((c) => c.method !== "PATCH")).toBe(true);
126
+ });
127
+
128
+ test("throws when the auth token is missing or invalid", async () => {
129
+ const { fetchMock } = makeFetchMock({});
130
+
131
+ await expect(
132
+ compactDialog({
133
+ serverUrl: "http://localhost:8080",
134
+ authToken: "not-a-valid-token",
135
+ dialogId: OLD_DIALOG_ID,
136
+ fetchImpl: fetchMock,
137
+ })
138
+ ).rejects.toThrow(/invalid or missing auth token/);
139
+ });
140
+
141
+ test("throws when the server read fails", async () => {
142
+ const fetchFailing: typeof fetch = async () =>
143
+ new Response("{}", { status: 404 });
144
+
145
+ await expect(
146
+ compactDialog({
147
+ serverUrl: "http://localhost:8080",
148
+ authToken: FAKE_TOKEN,
149
+ dialogId: OLD_DIALOG_ID,
150
+ fetchImpl: fetchFailing,
151
+ })
152
+ ).rejects.toThrow(/Failed to read dialog/);
153
+ });
154
+
155
+ test("space patch failure does not throw (best-effort)", async () => {
156
+ const { fetchMock } = makeFetchMock({ patchOk: false });
157
+
158
+ // Should resolve without throwing even though PATCH fails
159
+ const result = await compactDialog({
160
+ serverUrl: "http://localhost:8080",
161
+ authToken: FAKE_TOKEN,
162
+ dialogId: OLD_DIALOG_ID,
163
+ fetchImpl: fetchMock,
164
+ });
165
+
166
+ expect(result.dialogId).toBeDefined();
167
+ });
168
+
169
+ test("forked dialog does NOT inherit conversation summary/compression state", async () => {
170
+ const { fetchMock, calls } = makeFetchMock({});
171
+
172
+ await compactDialog({
173
+ serverUrl: "http://localhost:8080",
174
+ authToken: FAKE_TOKEN,
175
+ dialogId: OLD_DIALOG_ID,
176
+ fetchImpl: fetchMock,
177
+ });
178
+
179
+ const writeBody = calls[1]?.body as any;
180
+ const forked = writeBody?.data ?? {};
181
+
182
+ // Conversation-state fields must be absent (undefined / not present)
183
+ expect(forked.summary).toBeUndefined();
184
+ expect(forked.summarizedBeforeId).toBeUndefined();
185
+ expect(forked.proactiveSummary).toBeUndefined();
186
+ expect(forked.proactiveSummaryBeforeId).toBeUndefined();
187
+ expect(forked.compressionCount).toBeUndefined();
188
+ expect(forked.summaryPending).toBeUndefined();
189
+
190
+ // Config/identity fields must still be carried forward
191
+ expect(forked.cybots).toEqual(OLD_DIALOG_RECORD.cybots);
192
+ expect(forked.type).toBe("dialog");
193
+ expect(forked.spaceId).toBe("myspace");
194
+ expect(forked.inheritedFromDialogKey).toBe(OLD_DIALOG_KEY);
195
+ expect(forked.inheritedFromDialogTitle).toBe(OLD_DIALOG_RECORD.title);
196
+
197
+ // Stats must be reset
198
+ expect(forked.inputTokens).toBe(0);
199
+ expect(forked.outputTokens).toBe(0);
200
+ expect(forked.totalCost).toBe(0);
201
+ });
202
+ });
203
+
204
+ describe("parseTokenUserId", () => {
205
+ test("extracts userId from a valid JWT token (header.payload.signature)", () => {
206
+ const userId = "user-12345";
207
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64");
208
+ const payload = Buffer.from(JSON.stringify({ userId })).toString("base64");
209
+ const token = `${header}.${payload}.signature`;
210
+
211
+ expect(parseTokenUserId(token)).toBe(userId);
212
+ });
213
+
214
+ test("returns null when token has fewer than 2 segments", () => {
215
+ expect(parseTokenUserId("onlyonepart")).toBeNull();
216
+ });
217
+
218
+ test("returns null when payload is not valid JSON", () => {
219
+ const token = "header.not-valid-base64-json.signature";
220
+ expect(parseTokenUserId(token)).toBeNull();
221
+ });
222
+
223
+ test("returns null when payload does not contain userId", () => {
224
+ const header = Buffer.from(JSON.stringify({ alg: "HS256" })).toString("base64");
225
+ const payload = Buffer.from(JSON.stringify({ sub: "someone" })).toString("base64");
226
+ const token = `${header}.${payload}.signature`;
227
+
228
+ expect(parseTokenUserId(token)).toBeNull();
229
+ });
230
+
231
+ test("returns null when userId is not a string", () => {
232
+ const header = Buffer.from(JSON.stringify({ alg: "HS256" })).toString("base64");
233
+ const payload = Buffer.from(JSON.stringify({ userId: 12345 })).toString("base64");
234
+ const token = `${header}.${payload}.signature`;
235
+
236
+ expect(parseTokenUserId(token)).toBeNull();
237
+ });
238
+ });
@@ -8,10 +8,13 @@ const DB_PATH = "/api/v1/db";
8
8
  /**
9
9
  * Extract userId from a JWT-style auth token without verifying the signature.
10
10
  * Mirrors the logic of `parseToken` in `auth/token.ts` without the crypto imports.
11
+ * @internal - exported for testing only
11
12
  */
12
- function parseTokenUserId(token: string): string | null {
13
+ export function parseTokenUserId(token: string): string | null {
13
14
  try {
14
- const [payloadBase64] = token.split(".");
15
+ const parts = token.split(".");
16
+ if (parts.length < 2) return null;
17
+ const payloadBase64 = parts[1];
15
18
  const payload = JSON.parse(
16
19
  Buffer.from(payloadBase64, "base64").toString("utf8")
17
20
  );
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { runLocalAgentTurn } from "../../agent-runtime/localLoop";
4
+ import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
5
+
6
+ describe("CLI local runtime adapter", () => {
7
+ test("loads agent/history from LevelDB and saves dialog/message records back to LevelDB", async () => {
8
+ const requests: Array<{ url: string; body: any; auth: string | null }> = [];
9
+ const store = new Map<string, any>([
10
+ ["agent-user-1-frontend", {
11
+ dbKey: "agent-user-1-frontend",
12
+ id: "frontend",
13
+ name: "Frontend",
14
+ prompt: "Fix UI",
15
+ model: "gpt-4.1-mini",
16
+ provider: "openai-compatible",
17
+ }],
18
+ ["dialog-user-1-dialog-existing", {
19
+ dbKey: "dialog-user-1-dialog-existing",
20
+ id: "dialog-existing",
21
+ type: "dialog",
22
+ userId: "user-1",
23
+ }],
24
+ ["dialog-dialog-existing-msg-001", {
25
+ dbKey: "dialog-dialog-existing-msg-001",
26
+ id: "msg-001",
27
+ dialogId: "dialog-existing",
28
+ role: "assistant",
29
+ content: "previous answer",
30
+ }],
31
+ ]);
32
+ const batchOps: any[] = [];
33
+ const adapter = createCliLocalRuntimeAdapter({
34
+ env: {
35
+ NOLO_LOCAL_USER_ID: "user-1",
36
+ OPENAI_API_KEY: "sk-local",
37
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
38
+ },
39
+ db: {
40
+ get: async (key) => {
41
+ if (!store.has(key)) throw new Error(`not found: ${key}`);
42
+ return store.get(key);
43
+ },
44
+ put: async (key, value) => {
45
+ store.set(key, value);
46
+ },
47
+ batch: async (ops) => {
48
+ batchOps.push(...ops);
49
+ for (const op of ops) {
50
+ if (op.type === "put") store.set(op.key, op.value);
51
+ }
52
+ },
53
+ iterator: ({ gte, lte }) => (async function* () {
54
+ for (const entry of [...store.entries()].sort(([a], [b]) => a.localeCompare(b))) {
55
+ if (entry[0] >= gte && entry[0] <= lte) yield entry;
56
+ }
57
+ })(),
58
+ },
59
+ now: () => 1710000000000,
60
+ createId: () => "01LOCAL",
61
+ fetchImpl: async (url, init) => {
62
+ requests.push({
63
+ url: String(url),
64
+ body: JSON.parse(String(init?.body)),
65
+ auth: new Headers(init?.headers).get("Authorization"),
66
+ });
67
+ return Response.json({
68
+ choices: [{ message: { content: "local adapter ok" } }],
69
+ usage: { prompt_tokens: 4, completion_tokens: 3 },
70
+ });
71
+ },
72
+ });
73
+
74
+ const result = await runLocalAgentTurn({
75
+ adapter,
76
+ agentRef: "frontend",
77
+ input: "make it cleaner",
78
+ continueDialogId: "dialog-existing",
79
+ });
80
+
81
+ expect(result).toMatchObject({
82
+ content: "local adapter ok",
83
+ model: "gpt-4.1-mini",
84
+ dialogId: "01LOCAL",
85
+ });
86
+ expect(requests).toEqual([{
87
+ url: "http://127.0.0.1:11434/v1/chat/completions",
88
+ auth: "Bearer sk-local",
89
+ body: {
90
+ model: "gpt-4.1-mini",
91
+ messages: [
92
+ { role: "system", content: "Fix UI" },
93
+ { role: "assistant", content: "previous answer" },
94
+ { role: "user", content: "make it cleaner" },
95
+ ],
96
+ stream: false,
97
+ },
98
+ }]);
99
+ expect(batchOps.map((op) => op.key)).toEqual([
100
+ "dialog-user-1-01LOCAL",
101
+ "dialog-01LOCAL-msg-user",
102
+ "dialog-01LOCAL-msg-assistant",
103
+ ]);
104
+ expect(store.get("dialog-user-1-01LOCAL")).toMatchObject({
105
+ id: "01LOCAL",
106
+ dbKey: "dialog-user-1-01LOCAL",
107
+ type: "dialog",
108
+ primaryAgentKey: "agent-user-1-frontend",
109
+ status: "done",
110
+ });
111
+ expect(store.get("dialog-01LOCAL-msg-user")).toMatchObject({
112
+ dialogId: "01LOCAL",
113
+ role: "user",
114
+ content: "make it cleaner",
115
+ });
116
+ expect(store.get("dialog-01LOCAL-msg-assistant")).toMatchObject({
117
+ dialogId: "01LOCAL",
118
+ role: "assistant",
119
+ content: "local adapter ok",
120
+ });
121
+ });
122
+
123
+ test("rejects tools until local tool policy is implemented", async () => {
124
+ const adapter = createCliLocalRuntimeAdapter({
125
+ env: {},
126
+ fetchImpl: async () => Response.json({}),
127
+ });
128
+
129
+ await expect(adapter.executeTool({
130
+ id: "call-1",
131
+ name: "execShell",
132
+ arguments: "{}",
133
+ })).rejects.toThrow("Local runtime tools are not enabled");
134
+ });
135
+ });