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.
- package/README.md +54 -2
- package/agent-runtime/hostAdapter.ts +53 -0
- package/agent-runtime/index.ts +28 -0
- package/agent-runtime/localLoop.ts +62 -0
- package/agent-runtime/runtimeDecision.ts +70 -0
- package/agent-runtime/types.ts +87 -0
- package/agentRuntimeCommands.ts +141 -22
- package/agentRuntimeLocal.ts +7 -0
- package/ai/agent/_executeModel.ts +118 -0
- package/ai/agent/agentSlice.ts +545 -0
- package/ai/agent/appWorkingMemory.ts +126 -0
- package/ai/agent/avatarUtils.ts +24 -0
- package/ai/agent/buildEditingContext.ts +373 -0
- package/ai/agent/buildSystemPrompt.ts +532 -0
- package/ai/agent/cleanAgentMessages.ts +140 -0
- package/ai/agent/cliChatClient.ts +119 -0
- package/ai/agent/contextCompiler.ts +107 -0
- package/ai/agent/contextLayerContract.ts +44 -0
- package/ai/agent/createAgentSchema.ts +234 -0
- package/ai/agent/executeToolCall.ts +58 -0
- package/ai/agent/fetchAgentContexts.ts +42 -0
- package/ai/agent/generatePrompt.ts +3 -0
- package/ai/agent/getFullChatContextKeys.ts +168 -0
- package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
- package/ai/agent/hooks/useAgentConfig.ts +61 -0
- package/ai/agent/hooks/useAgentDialog.ts +35 -0
- package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
- package/ai/agent/hooks/usePublicAgents.ts +473 -0
- package/ai/agent/persistMessageWithFixedId.ts +37 -0
- package/ai/agent/planSlice.ts +259 -0
- package/ai/agent/referenceUtils.ts +229 -0
- package/ai/agent/runAgentBackground.ts +238 -0
- package/ai/agent/runAgentClientLoop.ts +138 -0
- package/ai/agent/runtimeGuidance.ts +97 -0
- package/ai/agent/runtimeServerBase.ts +37 -0
- package/ai/agent/server/fetchPublicAgents.ts +128 -0
- package/ai/agent/startParallelAgentStreams.ts +424 -0
- package/ai/agent/startupProtocol.ts +53 -0
- package/ai/agent/streamAgentChatTurn.ts +1299 -0
- package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
- package/ai/agent/types.ts +71 -0
- package/ai/agent/utils/imageOutput.ts +39 -0
- package/ai/agent/utils/publicImageAgentMode.ts +26 -0
- package/ai/agent/utils/sortUtils.ts +250 -0
- package/ai/agent/web/referencePickerUtils.ts +146 -0
- package/ai/agent.ts +2 -0
- package/ai/ai.locale.ts +1083 -0
- package/ai/chat/accumulateToolCallChunks.ts +95 -0
- package/ai/chat/fetchUtils.native.ts +276 -0
- package/ai/chat/fetchUtils.ts +153 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
- package/ai/chat/parseApiError.ts +64 -0
- package/ai/chat/parseMultilineSSE.ts +95 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
- package/ai/chat/shouldUseServerProxy.ts +18 -0
- package/ai/chat/sseClient.native.ts +91 -0
- package/ai/chat/sseClient.ts +67 -0
- package/ai/chat/streamReader.native.ts +31 -0
- package/ai/chat/streamReader.ts +62 -0
- package/ai/chat/updateTotalUsage.ts +72 -0
- package/ai/context/buildReferenceContext.ts +437 -0
- package/ai/context/calculateContextUsage.ts +133 -0
- package/ai/context/retention.ts +165 -0
- package/ai/context/tokenUtils.ts +78 -0
- package/ai/index.ts +1 -0
- package/ai/llm/agentCapabilities.ts +74 -0
- package/ai/llm/calculateGeminiImageTokens.ts +57 -0
- package/ai/llm/deepinfra.ts +28 -0
- package/ai/llm/fireworks.ts +68 -0
- package/ai/llm/generateRequestBody.ts +165 -0
- package/ai/llm/getModelContextWindow.ts +84 -0
- package/ai/llm/getNoloKey.ts +37 -0
- package/ai/llm/getPricing.ts +232 -0
- package/ai/llm/hooks/useModelPricing.ts +75 -0
- package/ai/llm/imagePricing.ts +66 -0
- package/ai/llm/isResponseAPIModel.ts +13 -0
- package/ai/llm/kimi.ts +18 -0
- package/ai/llm/mimo.ts +71 -0
- package/ai/llm/mistral.ts +22 -0
- package/ai/llm/modelAvatar.ts +427 -0
- package/ai/llm/models.ts +45 -0
- package/ai/llm/openrouterModels.ts +141 -0
- package/ai/llm/providers.ts +307 -0
- package/ai/llm/reasoningModels.ts +28 -0
- package/ai/llm/types.ts +59 -0
- package/ai/llm/usageRequestOptions.ts +59 -0
- package/ai/memory/capture.ts +148 -0
- package/ai/memory/consolidate.ts +104 -0
- package/ai/memory/delete.ts +147 -0
- package/ai/memory/overlay.ts +84 -0
- package/ai/memory/query.ts +38 -0
- package/ai/memory/queryShared.ts +160 -0
- package/ai/memory/rank.ts +105 -0
- package/ai/memory/recentRelationshipRecap.ts +247 -0
- package/ai/memory/remember.ts +167 -0
- package/ai/memory/runtime.ts +76 -0
- package/ai/memory/store.ts +20 -0
- package/ai/memory/storeShared.ts +76 -0
- package/ai/memory/types.ts +46 -0
- package/ai/memory/understanding.ts +349 -0
- package/ai/memory/understandingGreeting.ts +264 -0
- package/ai/messages/type.ts +20 -0
- package/ai/policy/personalizationDialog.ts +333 -0
- package/ai/policy/runtimePolicy.ts +440 -0
- package/ai/policy/selfUpdateFields.ts +48 -0
- package/ai/policy/types.ts +64 -0
- package/ai/skills/referenceRuntime.ts +274 -0
- package/ai/skills/skillDiagnostics.ts +251 -0
- package/ai/skills/skillDocBuilder.ts +139 -0
- package/ai/skills/skillDocProtocol.ts +434 -0
- package/ai/skills/skillReferenceSummary.ts +63 -0
- package/ai/skills/skillSummaryMarker.ts +26 -0
- package/ai/token/calculatePrice.ts +546 -0
- package/ai/token/db.ts +98 -0
- package/ai/token/externalToolCost.ts +321 -0
- package/ai/token/hooks/useRecords.ts +65 -0
- package/ai/token/missingUsageEstimate.ts +42 -0
- package/ai/token/modelUsageQuery.ts +252 -0
- package/ai/token/normalizeUsage.ts +84 -0
- package/ai/token/openaiImageGenerationUsage.ts +56 -0
- package/ai/token/prepareTokenUsageData.ts +88 -0
- package/ai/token/query.ts +88 -0
- package/ai/token/queryUserTokens.ts +59 -0
- package/ai/token/resolveBillingTarget.ts +52 -0
- package/ai/token/saveTokenRecord.ts +53 -0
- package/ai/token/serverDialogProjection.ts +78 -0
- package/ai/token/serverTokenWriter.ts +143 -0
- package/ai/token/stats.ts +21 -0
- package/ai/token/tokenThunks.ts +24 -0
- package/ai/token/types.ts +93 -0
- package/ai/tools/agent/agentTools.ts +176 -0
- package/ai/tools/agent/agentUpdateShared.ts +311 -0
- package/ai/tools/agent/callAgentTool.ts +139 -0
- package/ai/tools/agent/createAgentTool.ts +512 -0
- package/ai/tools/agent/createDialogTool.ts +69 -0
- package/ai/tools/agent/createSkillAgentTool.ts +62 -0
- package/ai/tools/agent/parallelBudget.ts +221 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
- package/ai/tools/agent/runLlmTool.ts +96 -0
- package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
- package/ai/tools/agent/skillAgentArgs.ts +106 -0
- package/ai/tools/agent/skillAgentPreset.ts +89 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
- package/ai/tools/agent/updateAgentTool.ts +96 -0
- package/ai/tools/agent/updateSelfTool.ts +113 -0
- package/ai/tools/amazonProductScraperTool.ts +86 -0
- package/ai/tools/apifyActorClient.ts +45 -0
- package/ai/tools/appEditGuard.ts +372 -0
- package/ai/tools/appReadSnapshot.ts +153 -0
- package/ai/tools/appTools.ts +1549 -0
- package/ai/tools/applyEditTool.ts +256 -0
- package/ai/tools/applyLineEditsTool.ts +312 -0
- package/ai/tools/browserTools/click.ts +33 -0
- package/ai/tools/browserTools/closeSession.ts +29 -0
- package/ai/tools/browserTools/common.ts +27 -0
- package/ai/tools/browserTools/openSession.ts +48 -0
- package/ai/tools/browserTools/readContent.ts +38 -0
- package/ai/tools/browserTools/selectOption.ts +46 -0
- package/ai/tools/browserTools/typeText.ts +42 -0
- package/ai/tools/category/createCategoryTool.ts +66 -0
- package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
- package/ai/tools/category/updateContentCategoryTool.ts +75 -0
- package/ai/tools/cfBrowserTools.ts +319 -0
- package/ai/tools/cfSpeechToTextTool.ts +49 -0
- package/ai/tools/checkEnvTool.ts +65 -0
- package/ai/tools/cloudflareCrawlTool.ts +289 -0
- package/ai/tools/codeSearchTool.ts +111 -0
- package/ai/tools/codeTools.ts +101 -0
- package/ai/tools/createDocTool.ts +132 -0
- package/ai/tools/createPlanTool.ts +999 -0
- package/ai/tools/createSkillDocTool.ts +155 -0
- package/ai/tools/createWorkflowTool.ts +154 -0
- package/ai/tools/deepseekOcrTool.ts +34 -0
- package/ai/tools/delayTool.ts +31 -0
- package/ai/tools/deleteSpacesTool.ts +325 -0
- package/ai/tools/deleteSpacesToolModel.ts +159 -0
- package/ai/tools/devReloadUtils.ts +29 -0
- package/ai/tools/dialogMessageSearch.ts +137 -0
- package/ai/tools/doctorSkillTool.ts +72 -0
- package/ai/tools/ecommerceScraperTool.ts +86 -0
- package/ai/tools/emailTools.ts +549 -0
- package/ai/tools/evalSkillTool.ts +92 -0
- package/ai/tools/exaSearchTool.ts +64 -0
- package/ai/tools/execBashTool.ts +379 -0
- package/ai/tools/executeSqlTool.ts +192 -0
- package/ai/tools/fetchWebpageSupport.ts +309 -0
- package/ai/tools/fetchWebpageTool.ts +84 -0
- package/ai/tools/geminiImagePreviewTool.ts +361 -0
- package/ai/tools/generateDocxTool.ts +215 -0
- package/ai/tools/googleSearchScraperTool.ts +106 -0
- package/ai/tools/importDataTool.ts +133 -0
- package/ai/tools/importSkillTool.ts +162 -0
- package/ai/tools/index.ts +1927 -0
- package/ai/tools/listFilesTool.ts +82 -0
- package/ai/tools/listUserSpacesTool.ts +113 -0
- package/ai/tools/modelUsageTools.ts +199 -0
- package/ai/tools/olmOcrTool.ts +34 -0
- package/ai/tools/openaiImageTool.ts +267 -0
- package/ai/tools/prepareTools.ts +23 -0
- package/ai/tools/readDocTool.ts +84 -0
- package/ai/tools/readFileTool.ts +211 -0
- package/ai/tools/readTool.ts +163 -0
- package/ai/tools/readXPostTool.ts +233 -0
- package/ai/tools/rememberMemoryTool.ts +84 -0
- package/ai/tools/remotionVideoTool.ts +151 -0
- package/ai/tools/searchDialogMessagesTool.ts +222 -0
- package/ai/tools/searchRepoTool.ts +115 -0
- package/ai/tools/searchWorkspaceTool.ts +259 -0
- package/ai/tools/skillFollowup.ts +86 -0
- package/ai/tools/surfWeatherTool.ts +169 -0
- package/ai/tools/table/addTableRowTool.ts +217 -0
- package/ai/tools/table/createTableTool.ts +315 -0
- package/ai/tools/table/rowTools.ts +366 -0
- package/ai/tools/table/schemaTools.ts +244 -0
- package/ai/tools/table/shareTableTool.ts +148 -0
- package/ai/tools/table/toolShared.ts +129 -0
- package/ai/tools/toolApiClient.ts +198 -0
- package/ai/tools/toolNameAliases.ts +57 -0
- package/ai/tools/toolResultError.ts +42 -0
- package/ai/tools/toolRunSlice.ts +303 -0
- package/ai/tools/toolSchemaCompatibility.ts +53 -0
- package/ai/tools/toolVisibility.ts +4 -0
- package/ai/tools/types.ts +20 -0
- package/ai/tools/uiAskChoiceTool.ts +104 -0
- package/ai/tools/updateContentTitleTool.ts +84 -0
- package/ai/tools/updateDocTool.ts +105 -0
- package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
- package/ai/tools/whisperTool.ts +77 -0
- package/ai/tools/writeFileTool.ts +210 -0
- package/ai/tools/youtubeScraperTool.ts +116 -0
- package/ai/tools/ziweiChartTool.ts +678 -0
- package/ai/types.ts +55 -0
- package/ai/workflow/workflowExecutor.ts +323 -0
- package/ai/workflow/workflowSlice.ts +73 -0
- package/ai/workflow/workflowTypes.ts +106 -0
- package/authCommands.ts +185 -21
- package/client/agentRun.test.ts +240 -0
- package/client/agentRun.ts +182 -19
- package/client/compactDialog.test.ts +238 -0
- package/client/compactDialog.ts +5 -2
- package/client/localRuntimeAdapter.test.ts +135 -0
- package/client/localRuntimeAdapter.ts +244 -0
- package/client/profileConfig.test.ts +40 -0
- package/client/streamingOutput.test.ts +22 -0
- package/client/streamingOutput.ts +38 -0
- package/commandRegistry.ts +11 -2
- package/connector-experimental/index.ts +5 -0
- package/database/actions/cacheMergedUserData.ts +64 -0
- package/database/actions/common.ts +242 -0
- package/database/actions/deleteFile.ts +40 -0
- package/database/actions/fetchUserData.ts +16 -0
- package/database/actions/fileContent.ts +125 -0
- package/database/actions/patch.ts +155 -0
- package/database/actions/read.ts +337 -0
- package/database/actions/readAndWait.ts +224 -0
- package/database/actions/readRequestManager.ts +120 -0
- package/database/actions/remove.ts +94 -0
- package/database/actions/replication.ts +366 -0
- package/database/actions/upload.ts +174 -0
- package/database/actions/upsert.ts +56 -0
- package/database/actions/write.ts +126 -0
- package/database/client/db.native.ts +73 -0
- package/database/client/db.ts +51 -0
- package/database/client/fetchUserData.ts +61 -0
- package/database/client/handleError.ts +19 -0
- package/database/client/queryRequest.ts +21 -0
- package/database/config.ts +21 -0
- package/database/dbActionThunks.ts +1 -0
- package/database/dbSlice.ts +149 -0
- package/database/email.ts +42 -0
- package/database/fileRing.ts +51 -0
- package/database/fileSharding.ts +70 -0
- package/database/fileStorage.native.ts +92 -0
- package/database/fileStorage.ts +232 -0
- package/database/fileUrl.ts +34 -0
- package/database/hooks/useUserData.ts +489 -0
- package/database/index.ts +1 -0
- package/database/keys.ts +765 -0
- package/database/queryPrefixes.ts +14 -0
- package/database/requests.ts +443 -0
- package/database/runtimeServerContext.ts +35 -0
- package/database/server/MemoryDB.ts +76 -0
- package/database/server/actorAccess.ts +76 -0
- package/database/server/agentDelegation.ts +124 -0
- package/database/server/coreDataOwnership.ts +13 -0
- package/database/server/coreDataProxy.ts +76 -0
- package/database/server/cybotReadonly.ts +18 -0
- package/database/server/dataHandlers.ts +111 -0
- package/database/server/db.ts +118 -0
- package/database/server/dbPath.ts +20 -0
- package/database/server/delete.ts +499 -0
- package/database/server/emailRepository.ts +1480 -0
- package/database/server/ensureDbOpen.ts +12 -0
- package/database/server/fileRead.ts +337 -0
- package/database/server/fileService.ts +436 -0
- package/database/server/handleTransaction.ts +86 -0
- package/database/server/patch.ts +282 -0
- package/database/server/query.ts +138 -0
- package/database/server/read.ts +325 -0
- package/database/server/resourceAccess.ts +211 -0
- package/database/server/routes.ts +110 -0
- package/database/server/spaceMemberAuthority.ts +67 -0
- package/database/server/upload.ts +159 -0
- package/database/server/write.ts +494 -0
- package/database/server/writeAuthority.ts +133 -0
- package/database/sqliteDb.ts +46 -0
- package/database/table/deleteTable.ts +120 -0
- package/database/tenantPlacement.ts +57 -0
- package/database/tombstones.ts +52 -0
- package/database/userDataLoadDecision.ts +17 -0
- package/database/userDataMerge.ts +95 -0
- package/database/userPreferenceRegister.ts +108 -0
- package/database/utils/dbPath.ts +47 -0
- package/database/utils/ulid.native.ts +6 -0
- package/database/utils/ulid.ts +1 -0
- package/index.ts +25 -15
- package/localRuntimeDb.ts +28 -0
- package/package.json +16 -4
- package/runtimeModeArgs.ts +33 -0
- package/tui/readlineWorkspace.ts +1 -0
- package/tui/session.ts +22 -0
package/client/agentRun.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
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:
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
+
});
|
package/client/compactDialog.ts
CHANGED
|
@@ -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
|
|
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
|
+
});
|