nolo-cli 0.1.18 → 0.1.20

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 (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +14 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +78 -6
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +2 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +289 -54
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +823 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +361 -7
  79. package/client/agentRun.ts +449 -63
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +813 -20
  89. package/client/localRuntimeAdapter.ts +279 -232
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +237 -0
@@ -0,0 +1,70 @@
1
+ import type {
2
+ AgentRuntimeChatMessage,
3
+ AgentRuntimeResult,
4
+ } from "./types";
5
+
6
+ export type OpenAiCompatibleProviderConfig = {
7
+ model: string;
8
+ endpoint: string;
9
+ apiKey: string;
10
+ provider: string;
11
+ requestOptions: Record<string, number | string>;
12
+ };
13
+
14
+ type OpenAiCompatibleTool = Record<string, unknown>;
15
+
16
+ function toOpenAiCompatibleMessages(messages: AgentRuntimeChatMessage[]) {
17
+ return messages.map((message) => ({
18
+ role: message.role,
19
+ content: message.content ?? "",
20
+ ...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}),
21
+ ...(Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {}),
22
+ ...(message.reasoning_content ? { reasoning_content: message.reasoning_content } : {}),
23
+ }));
24
+ }
25
+
26
+ export function buildOpenAiCompatibleChatCompletionRequest(args: {
27
+ providerConfig: OpenAiCompatibleProviderConfig;
28
+ messages: AgentRuntimeChatMessage[];
29
+ tools?: OpenAiCompatibleTool[];
30
+ }) {
31
+ const body = {
32
+ model: args.providerConfig.model,
33
+ messages: toOpenAiCompatibleMessages(args.messages),
34
+ stream: false,
35
+ ...args.providerConfig.requestOptions,
36
+ ...(args.tools && args.tools.length > 0 ? { tools: args.tools } : {}),
37
+ };
38
+ return {
39
+ url: args.providerConfig.endpoint,
40
+ init: {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ ...(args.providerConfig.apiKey
45
+ ? { Authorization: `Bearer ${args.providerConfig.apiKey}` }
46
+ : {}),
47
+ },
48
+ body: JSON.stringify(body),
49
+ },
50
+ };
51
+ }
52
+
53
+ export function parseOpenAiCompatibleChatCompletionResponse(args: {
54
+ providerConfig: OpenAiCompatibleProviderConfig;
55
+ data: any;
56
+ trace: AgentRuntimeChatMessage[];
57
+ }): AgentRuntimeResult {
58
+ const choiceMessage = args.data?.choices?.[0]?.message ?? {};
59
+ return {
60
+ content: String(choiceMessage?.content ?? ""),
61
+ model: args.providerConfig.model,
62
+ provider: args.providerConfig.provider,
63
+ ...(Array.isArray(choiceMessage?.tool_calls) ? { tool_calls: choiceMessage.tool_calls } : {}),
64
+ ...(typeof choiceMessage?.reasoning_content === "string" && choiceMessage.reasoning_content
65
+ ? { reasoning_content: choiceMessage.reasoning_content }
66
+ : {}),
67
+ usage: args.data?.usage,
68
+ trace: args.trace,
69
+ };
70
+ }
@@ -0,0 +1,38 @@
1
+ import type { AgentRuntimeAgentConfig } from "./hostAdapter";
2
+ import type { OpenAiCompatibleProviderConfig } from "./openAiCompatibleProvider";
3
+ import { pickAgentRuntimeInferenceOptions } from "./agentConfigOptions";
4
+
5
+ type EnvLike = Record<string, string | undefined>;
6
+
7
+ function trimTrailingSlash(value: string) {
8
+ return value.replace(/\/+$/, "");
9
+ }
10
+
11
+ function resolveChatCompletionsEndpoint(value: string) {
12
+ const trimmed = trimTrailingSlash(value.trim());
13
+ return trimmed.endsWith("/chat/completions")
14
+ ? trimmed
15
+ : `${trimmed}/chat/completions`;
16
+ }
17
+
18
+ function resolveOpenAiCompatibleBaseUrl(env: EnvLike) {
19
+ return env.NOLO_LOCAL_OPENAI_BASE_URL || env.OPENAI_BASE_URL || "https://api.openai.com/v1";
20
+ }
21
+
22
+ function resolveOpenAiCompatibleApiKey(env: EnvLike) {
23
+ return env.OPENAI_API_KEY || env.NOLO_LOCAL_OPENAI_API_KEY || "";
24
+ }
25
+
26
+ export function resolveOpenAiCompatibleProviderConfig(args: {
27
+ agentConfig: AgentRuntimeAgentConfig;
28
+ env: EnvLike;
29
+ }): OpenAiCompatibleProviderConfig {
30
+ const customProviderUrl = args.agentConfig.customProviderUrl?.trim();
31
+ return {
32
+ model: args.agentConfig.model || "gpt-4.1-mini",
33
+ endpoint: resolveChatCompletionsEndpoint(customProviderUrl || resolveOpenAiCompatibleBaseUrl(args.env)),
34
+ apiKey: resolveOpenAiCompatibleApiKey(args.env),
35
+ provider: args.agentConfig.provider || "openai-compatible",
36
+ requestOptions: pickAgentRuntimeInferenceOptions(args.agentConfig),
37
+ };
38
+ }
@@ -0,0 +1,241 @@
1
+ import type { AgentRuntimeAgentConfig } from "./hostAdapter";
2
+ import type {
3
+ AgentRuntimeChatMessage,
4
+ AgentRuntimeResult,
5
+ } from "./types";
6
+ import { pickAgentRuntimeInferenceOptions } from "./agentConfigOptions";
7
+
8
+ type EnvLike = Record<string, string | undefined>;
9
+
10
+ const CHAT_PROXY_PATH = "/api/v1/chat";
11
+
12
+ const PROVIDER_ENDPOINTS: Record<string, string> = {
13
+ deepinfra: "https://api.deepinfra.com/v1/openai/chat/completions",
14
+ deepseek: "https://api.deepseek.com/chat/completions",
15
+ fireworks: "https://api.fireworks.ai/inference/v1/chat/completions",
16
+ google: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
17
+ mistral: "https://api.mistral.ai/v1/chat/completions",
18
+ mimo: "https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
19
+ openai: "https://api.openai.com/v1/chat/completions",
20
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
21
+ };
22
+
23
+ export type PlatformChatProviderConfig = {
24
+ serverUrl: string;
25
+ authToken: string;
26
+ model: string;
27
+ provider: string;
28
+ endpoint: string;
29
+ requestOptions: Record<string, number | string>;
30
+ apiKey?: string;
31
+ apiSource?: string;
32
+ };
33
+
34
+ type PlatformChatTool = Record<string, unknown>;
35
+
36
+ function trimTrailingSlash(value: string) {
37
+ return value.trim().replace(/\/+$/, "");
38
+ }
39
+
40
+ function resolveChatCompletionsEndpoint(value: string) {
41
+ const trimmed = trimTrailingSlash(value);
42
+ return trimmed.endsWith("/chat/completions")
43
+ ? trimmed
44
+ : `${trimmed}/chat/completions`;
45
+ }
46
+
47
+ function resolvePlatformServerUrl(env: EnvLike) {
48
+ return trimTrailingSlash(env.NOLO_SERVER || env.BASE_URL || "https://nolo.chat");
49
+ }
50
+
51
+ function resolvePlatformAuthToken(env: EnvLike) {
52
+ return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
53
+ }
54
+
55
+ function resolveProviderEndpoint(agentConfig: AgentRuntimeAgentConfig) {
56
+ const customProviderUrl = agentConfig.customProviderUrl?.trim();
57
+ if (customProviderUrl) return resolveChatCompletionsEndpoint(customProviderUrl);
58
+
59
+ const provider = agentConfig.provider?.trim().toLowerCase();
60
+ if (!provider) {
61
+ throw new Error("Platform chat provider requires agentConfig.provider.");
62
+ }
63
+
64
+ const endpoint = PROVIDER_ENDPOINTS[provider];
65
+ if (!endpoint) {
66
+ throw new Error(`Platform chat provider does not support provider "${provider}".`);
67
+ }
68
+ return endpoint;
69
+ }
70
+
71
+ function shouldDisableThinking(providerConfig: PlatformChatProviderConfig) {
72
+ return (
73
+ providerConfig.provider.toLowerCase() === "mimo" ||
74
+ /xiaomimimo\.com/i.test(providerConfig.endpoint)
75
+ );
76
+ }
77
+
78
+ function toOpenAiCompatibleMessages(messages: AgentRuntimeChatMessage[]) {
79
+ return messages.map((message) => ({
80
+ role: message.role,
81
+ content: message.content ?? "",
82
+ ...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}),
83
+ ...(Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {}),
84
+ ...(message.reasoning_content ? { reasoning_content: message.reasoning_content } : {}),
85
+ }));
86
+ }
87
+
88
+ export function resolvePlatformChatProviderConfig(args: {
89
+ agentConfig: AgentRuntimeAgentConfig;
90
+ env: EnvLike;
91
+ }): PlatformChatProviderConfig {
92
+ const provider = args.agentConfig.provider || args.agentConfig.apiSource || "openai";
93
+ return {
94
+ serverUrl: resolvePlatformServerUrl(args.env),
95
+ authToken: resolvePlatformAuthToken(args.env),
96
+ model: args.agentConfig.model || "gpt-4.1-mini",
97
+ provider,
98
+ endpoint: resolveProviderEndpoint(args.agentConfig),
99
+ requestOptions: pickAgentRuntimeInferenceOptions(args.agentConfig),
100
+ ...(typeof args.agentConfig.rawRecord?.apiKey === "string"
101
+ ? { apiKey: args.agentConfig.rawRecord.apiKey.trim() }
102
+ : {}),
103
+ ...(args.agentConfig.apiSource ? { apiSource: args.agentConfig.apiSource } : {}),
104
+ };
105
+ }
106
+
107
+ export function buildPlatformChatCompletionRequest(args: {
108
+ providerConfig: PlatformChatProviderConfig;
109
+ messages: AgentRuntimeChatMessage[];
110
+ tools?: PlatformChatTool[];
111
+ }) {
112
+ const body = {
113
+ model: args.providerConfig.model,
114
+ messages: toOpenAiCompatibleMessages(args.messages),
115
+ stream: false,
116
+ ...args.providerConfig.requestOptions,
117
+ ...(args.tools && args.tools.length > 0 ? { tools: args.tools, tool_choice: "auto" } : {}),
118
+ ...(shouldDisableThinking(args.providerConfig) ? { thinking: { type: "disabled" } } : {}),
119
+ url: args.providerConfig.endpoint,
120
+ provider: args.providerConfig.provider,
121
+ ...(args.providerConfig.apiSource ? { apiSource: args.providerConfig.apiSource } : {}),
122
+ ...(args.providerConfig.apiKey ? { KEY: args.providerConfig.apiKey } : {}),
123
+ };
124
+
125
+ return {
126
+ url: `${args.providerConfig.serverUrl}${CHAT_PROXY_PATH}`,
127
+ init: {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ Authorization: `Bearer ${args.providerConfig.authToken}`,
132
+ },
133
+ body: JSON.stringify(body),
134
+ },
135
+ };
136
+ }
137
+
138
+ function tryParseJson(raw: string) {
139
+ try {
140
+ return JSON.parse(raw);
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function extractJsonObjects(raw: string) {
147
+ const objects: any[] = [];
148
+ let depth = 0;
149
+ let start = -1;
150
+ let inString = false;
151
+ let escaped = false;
152
+
153
+ for (let index = 0; index < raw.length; index += 1) {
154
+ const char = raw[index];
155
+ if (inString) {
156
+ if (escaped) {
157
+ escaped = false;
158
+ } else if (char === "\\") {
159
+ escaped = true;
160
+ } else if (char === "\"") {
161
+ inString = false;
162
+ }
163
+ continue;
164
+ }
165
+
166
+ if (char === "\"") {
167
+ inString = true;
168
+ continue;
169
+ }
170
+ if (char === "{") {
171
+ if (depth === 0) start = index;
172
+ depth += 1;
173
+ continue;
174
+ }
175
+ if (char !== "}" || depth === 0) continue;
176
+
177
+ depth -= 1;
178
+ if (depth === 0 && start >= 0) {
179
+ const parsed = tryParseJson(raw.slice(start, index + 1));
180
+ if (parsed) objects.push(parsed);
181
+ start = -1;
182
+ }
183
+ }
184
+
185
+ return objects;
186
+ }
187
+
188
+ export function parsePlatformChatCompletionData(raw: string) {
189
+ const direct = tryParseJson(raw.trim());
190
+ if (direct) return direct;
191
+
192
+ const objects = extractJsonObjects(raw);
193
+ return objects.find((object) => Array.isArray(object?.choices)) ?? objects[0] ?? {};
194
+ }
195
+
196
+ export function parsePlatformChatCompletionResponse(args: {
197
+ providerConfig: PlatformChatProviderConfig;
198
+ data: any;
199
+ trace: AgentRuntimeChatMessage[];
200
+ }): AgentRuntimeResult {
201
+ const choiceMessage = args.data?.choices?.[0]?.message ?? {};
202
+ return {
203
+ content: String(choiceMessage?.content ?? ""),
204
+ model: args.providerConfig.model,
205
+ provider: args.providerConfig.provider,
206
+ ...(Array.isArray(choiceMessage?.tool_calls) ? { tool_calls: choiceMessage.tool_calls } : {}),
207
+ ...(typeof choiceMessage?.reasoning_content === "string" && choiceMessage.reasoning_content
208
+ ? { reasoning_content: choiceMessage.reasoning_content }
209
+ : {}),
210
+ usage: args.data?.usage,
211
+ trace: args.trace,
212
+ };
213
+ }
214
+
215
+ export function canUsePlatformChatProvider(env: EnvLike) {
216
+ return Boolean(resolvePlatformAuthToken(env));
217
+ }
218
+
219
+ export function hasDirectOpenAiCompatibleProvider(env: EnvLike) {
220
+ return Boolean(
221
+ env.NOLO_LOCAL_OPENAI_API_KEY ||
222
+ env.OPENAI_API_KEY ||
223
+ env.NOLO_LOCAL_OPENAI_BASE_URL ||
224
+ env.OPENAI_BASE_URL ||
225
+ env.OLLAMA_BASE_URL
226
+ );
227
+ }
228
+
229
+ function agentRequiresPlatformChatProvider(agentConfig?: AgentRuntimeAgentConfig) {
230
+ return agentConfig?.apiSource === "platform" || agentConfig?.rawRecord?.useServerProxy === true;
231
+ }
232
+
233
+ export function shouldUsePlatformChatProvider(
234
+ env: EnvLike,
235
+ agentConfig?: AgentRuntimeAgentConfig
236
+ ) {
237
+ if (agentRequiresPlatformChatProvider(agentConfig)) return true;
238
+ if (env.NOLO_LOCAL_LLM === "platform") return true;
239
+ if (env.NOLO_LOCAL_LLM === "direct") return false;
240
+ return !hasDirectOpenAiCompatibleProvider(env) && canUsePlatformChatProvider(env);
241
+ }
@@ -0,0 +1,193 @@
1
+ import {
2
+ lstat,
3
+ mkdir,
4
+ readdir,
5
+ readFile,
6
+ readlink,
7
+ rm,
8
+ symlink,
9
+ } from "node:fs/promises";
10
+ import { dirname, join, resolve, sep } from "node:path";
11
+
12
+ type EnvLike = Record<string, string | undefined>;
13
+
14
+ export type PreparedGitTaskWorkspace = {
15
+ path: string;
16
+ branchName?: string;
17
+ };
18
+
19
+ export type WorkspacePackageLink = {
20
+ name: string;
21
+ target: string;
22
+ linkPath: string;
23
+ };
24
+
25
+ function safeTaskWorkspacePathSegment(input: string) {
26
+ return input.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "agent";
27
+ }
28
+
29
+ function createTaskWorkspaceId() {
30
+ return Date.now().toString(36).toUpperCase();
31
+ }
32
+
33
+ async function runGitForTaskWorkspace(args: string[], cwd: string) {
34
+ const proc = Bun.spawn(["git", ...args], {
35
+ cwd,
36
+ stdout: "pipe",
37
+ stderr: "pipe",
38
+ stdin: "ignore",
39
+ });
40
+ const [stdout, stderr, exitCode] = await Promise.all([
41
+ new Response(proc.stdout).text(),
42
+ new Response(proc.stderr).text(),
43
+ proc.exited,
44
+ ]);
45
+ if (exitCode !== 0) {
46
+ throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`);
47
+ }
48
+ return stdout.trim();
49
+ }
50
+
51
+ function packageLinkPath(nodeModulesPath: string, packageName: string) {
52
+ if (!packageName.startsWith("@")) {
53
+ return join(nodeModulesPath, packageName);
54
+ }
55
+ const [scope, name] = packageName.split("/");
56
+ if (!scope || !name) {
57
+ throw new Error(`Invalid scoped package name: ${packageName}`);
58
+ }
59
+ return join(nodeModulesPath, scope, name);
60
+ }
61
+
62
+ async function readPackageName(packageJsonPath: string) {
63
+ const text = await readFile(packageJsonPath, "utf8");
64
+ const parsed = JSON.parse(text) as { name?: unknown };
65
+ return typeof parsed.name === "string" && parsed.name.trim()
66
+ ? parsed.name.trim()
67
+ : null;
68
+ }
69
+
70
+ export async function collectWorkspacePackageLinks(workspacePath: string) {
71
+ const packagesRoot = join(workspacePath, "packages");
72
+ const packageDirs = await readdir(packagesRoot, { withFileTypes: true }).catch(
73
+ () => []
74
+ );
75
+ const nodeModulesPath = join(workspacePath, "node_modules");
76
+ const links: WorkspacePackageLink[] = [];
77
+
78
+ for (const entry of packageDirs) {
79
+ if (!entry.isDirectory()) continue;
80
+ const target = join(packagesRoot, entry.name);
81
+ const name = await readPackageName(join(target, "package.json")).catch(
82
+ () => null
83
+ );
84
+ if (!name) continue;
85
+ links.push({
86
+ name,
87
+ target,
88
+ linkPath: packageLinkPath(nodeModulesPath, name),
89
+ });
90
+ }
91
+
92
+ return links;
93
+ }
94
+
95
+ async function ensureWorkspacePackageLink(link: WorkspacePackageLink) {
96
+ await mkdir(dirname(link.linkPath), { recursive: true });
97
+ const existing = await Bun.file(link.linkPath).stat().catch(() => null);
98
+ if (existing) {
99
+ const existingTarget = await readlinkIfSymlink(link.linkPath);
100
+ if (
101
+ existingTarget &&
102
+ resolve(dirname(link.linkPath), existingTarget) === resolve(link.target)
103
+ ) {
104
+ return false;
105
+ }
106
+ if (!existingTarget) {
107
+ throw new Error(`Cannot replace non-symlink workspace package path: ${link.linkPath}`);
108
+ }
109
+ await rm(link.linkPath, { force: true });
110
+ }
111
+ await symlink(link.target, link.linkPath, "dir");
112
+ return true;
113
+ }
114
+
115
+ async function readlinkIfSymlink(path: string) {
116
+ const stat = await lstat(path).catch(() => null);
117
+ if (!stat?.isSymbolicLink()) return null;
118
+ return readlink(path);
119
+ }
120
+
121
+ function isInsideWorkspace(workspacePath: string, path: string) {
122
+ const workspaceRoot = resolve(workspacePath);
123
+ const resolvedPath = resolve(path);
124
+ return resolvedPath === workspaceRoot || resolvedPath.startsWith(`${workspaceRoot}${sep}`);
125
+ }
126
+
127
+ function isFormalTaskWorkspacePath(path: string) {
128
+ return resolve(path).split(/[\\/]+/).includes(".worktrees");
129
+ }
130
+
131
+ async function ensureWorkspaceNodeModulesDirectory(workspacePath: string) {
132
+ const nodeModulesPath = join(workspacePath, "node_modules");
133
+ const symlinkTarget = await readlinkIfSymlink(nodeModulesPath);
134
+ if (symlinkTarget) {
135
+ const resolvedTarget = resolve(dirname(nodeModulesPath), symlinkTarget);
136
+ if (!isInsideWorkspace(workspacePath, resolvedTarget)) {
137
+ await rm(nodeModulesPath, { force: true });
138
+ }
139
+ }
140
+ await mkdir(nodeModulesPath, { recursive: true });
141
+ }
142
+
143
+ export async function ensureWorkspacePackageLinks(workspacePath: string) {
144
+ const links = await collectWorkspacePackageLinks(workspacePath);
145
+ if (links.length === 0) {
146
+ return {
147
+ workspacePath,
148
+ links,
149
+ createdOrUpdated: 0,
150
+ };
151
+ }
152
+ await ensureWorkspaceNodeModulesDirectory(workspacePath);
153
+ let createdOrUpdated = 0;
154
+ for (const link of links) {
155
+ if (await ensureWorkspacePackageLink(link)) {
156
+ createdOrUpdated += 1;
157
+ }
158
+ }
159
+ return {
160
+ workspacePath,
161
+ links,
162
+ createdOrUpdated,
163
+ };
164
+ }
165
+
166
+ export async function prepareGitTaskWorkspace(args: {
167
+ agentKey: string;
168
+ cwd?: string;
169
+ env?: EnvLike;
170
+ }): Promise<PreparedGitTaskWorkspace> {
171
+ if (args.env?.NOLO_LOCAL_WORKTREE) {
172
+ await ensureWorkspacePackageLinks(args.env.NOLO_LOCAL_WORKTREE);
173
+ return { path: args.env.NOLO_LOCAL_WORKTREE };
174
+ }
175
+ const cwd = args.cwd ?? process.cwd();
176
+ const root = await runGitForTaskWorkspace(["rev-parse", "--show-toplevel"], cwd);
177
+ if (isFormalTaskWorkspacePath(root)) {
178
+ await ensureWorkspacePackageLinks(root);
179
+ return { path: root };
180
+ }
181
+ const parent = join(root, ".worktrees");
182
+ const safeAgent = safeTaskWorkspacePathSegment(args.agentKey);
183
+ const taskId = createTaskWorkspaceId();
184
+ const workspacePath = join(parent, `nolo-agent-${safeAgent}-${taskId}`);
185
+ const branchName = `nolo-agent-${safeAgent}-${taskId}`;
186
+ await runGitForTaskWorkspace(["worktree", "add", workspacePath, "-b", branchName, "HEAD"], root);
187
+ await ensureWorkspacePackageLinks(workspacePath);
188
+ return { path: workspacePath, branchName };
189
+ }
190
+
191
+ export function formatPreparedGitTaskWorkspace(workspace: PreparedGitTaskWorkspace) {
192
+ return `[nolo] task worktree: ${workspace.path}${workspace.branchName ? ` (branch ${workspace.branchName})` : ""}\n`;
193
+ }
@@ -33,6 +33,7 @@ export interface AgentRuntimeChatMessage {
33
33
  content: AgentRuntimeMessageContent;
34
34
  tool_call_id?: string;
35
35
  tool_calls?: AgentRuntimeToolCall[];
36
+ tool_result_metadata?: Record<string, unknown>;
36
37
  reasoning_content?: string;
37
38
  cybotKey?: string;
38
39
  agentKey?: string;
@@ -52,6 +53,7 @@ export interface AgentRuntimeResult {
52
53
  usage?: Record<string, any>;
53
54
  trace?: AgentRuntimeChatMessage[];
54
55
  tool_calls?: AgentRuntimeToolCall[];
56
+ reasoning_content?: string;
55
57
  runtimeToolNames?: string[];
56
58
  toolCallCount?: number;
57
59
  policyState?: unknown;
@@ -0,0 +1,76 @@
1
+ // Runtime workspace sessions describe where code tools execute.
2
+ // They are intentionally separate from product Spaces (`spaceId`, shared content, membership).
3
+ export type WorkspaceSessionMode = "current" | "task-worktree";
4
+
5
+ export type PreparedTaskWorkspace = {
6
+ path: string;
7
+ branchName?: string;
8
+ };
9
+
10
+ export type PrepareTaskWorkspace = (args: {
11
+ agentKey: string;
12
+ env?: Record<string, string | undefined>;
13
+ }) => Promise<PreparedTaskWorkspace>;
14
+
15
+ export type WorkspaceSession =
16
+ | {
17
+ kind: "current";
18
+ workspaceRoot: string;
19
+ explicitCwd: boolean;
20
+ }
21
+ | {
22
+ kind: "task-worktree";
23
+ workspaceRoot: string;
24
+ explicitCwd: false;
25
+ branchName?: string;
26
+ };
27
+
28
+ export function createWorkspaceSession(args: {
29
+ cwd?: string;
30
+ defaultCwd?: string;
31
+ }): WorkspaceSession {
32
+ return {
33
+ kind: "current",
34
+ workspaceRoot: args.cwd ?? args.defaultCwd ?? process.cwd(),
35
+ explicitCwd: Boolean(args.cwd),
36
+ };
37
+ }
38
+
39
+ export async function activateWorkspaceSession(
40
+ session: WorkspaceSession,
41
+ args: {
42
+ agentKey: string;
43
+ mode: WorkspaceSessionMode;
44
+ env?: Record<string, string | undefined>;
45
+ prepareTaskWorkspace?: PrepareTaskWorkspace;
46
+ }
47
+ ): Promise<WorkspaceSession> {
48
+ if (args.mode !== "task-worktree") return session;
49
+ if (session.kind === "task-worktree" || session.explicitCwd) return session;
50
+ if (!args.prepareTaskWorkspace) {
51
+ throw new Error("task-worktree workspace sessions require a prepareTaskWorkspace implementation.");
52
+ }
53
+ const prepared = await args.prepareTaskWorkspace({
54
+ agentKey: args.agentKey,
55
+ env: args.env,
56
+ });
57
+ return workspaceSessionFromTaskWorkspace(prepared);
58
+ }
59
+
60
+ function workspaceSessionFromTaskWorkspace(
61
+ workspace: PreparedTaskWorkspace
62
+ ): WorkspaceSession {
63
+ return {
64
+ kind: "task-worktree",
65
+ workspaceRoot: workspace.path,
66
+ explicitCwd: false,
67
+ ...(workspace.branchName ? { branchName: workspace.branchName } : {}),
68
+ };
69
+ }
70
+
71
+ export function formatWorkspaceSessionActivation(session: WorkspaceSession) {
72
+ if (session.kind !== "task-worktree") return "";
73
+ return `[nolo] workspace session: task-worktree ${session.workspaceRoot}${
74
+ session.branchName ? ` (branch ${session.branchName})` : ""
75
+ }\n`;
76
+ }
package/agentAliases.ts CHANGED
@@ -2,14 +2,51 @@ export const PLATFORM_DEMO_USER_ID = "b2e06f801f";
2
2
  export const FRONTEND_IMPLEMENTER_AGENT_ID = "01FRONTENDAG0000000115N4E1";
3
3
  export const FRONTEND_IMPLEMENTER_AGENT_KEY =
4
4
  `agent-${PLATFORM_DEMO_USER_ID}-${FRONTEND_IMPLEMENTER_AGENT_ID}`;
5
+ export const NOLO_FRONTEND_AGENT_ID = FRONTEND_IMPLEMENTER_AGENT_ID;
6
+ export const NOLO_FRONTEND_AGENT_KEY = FRONTEND_IMPLEMENTER_AGENT_KEY;
7
+ export const NOLO_PROJECT_MANAGER_AGENT_ID = "01NOLOPROJMGR00000000MSVGG";
8
+ export const NOLO_PROJECT_MANAGER_AGENT_KEY =
9
+ `agent-${PLATFORM_DEMO_USER_ID}-${NOLO_PROJECT_MANAGER_AGENT_ID}`;
10
+ export const NOLO_FULLSTACK_AGENT_ID = "01NOLOFULLSTK00000001J740Q";
11
+ export const NOLO_FULLSTACK_AGENT_KEY =
12
+ `agent-${PLATFORM_DEMO_USER_ID}-${NOLO_FULLSTACK_AGENT_ID}`;
13
+ export const NOLO_SENIOR_FULLSTACK_AGENT_ID = "01NOLOFULLSTK00000000T3XR4";
14
+ export const NOLO_SENIOR_FULLSTACK_AGENT_KEY =
15
+ `agent-${PLATFORM_DEMO_USER_ID}-${NOLO_SENIOR_FULLSTACK_AGENT_ID}`;
16
+ export const NOLO_REVIEW_AGENT_ID = "01NOLOREVIEW0000000000R8V1";
17
+ export const NOLO_REVIEW_AGENT_KEY =
18
+ `agent-${PLATFORM_DEMO_USER_ID}-${NOLO_REVIEW_AGENT_ID}`;
5
19
 
6
20
  const AGENT_ALIAS_TO_KEY: Record<string, string> = {
21
+ "nolo-frontend": FRONTEND_IMPLEMENTER_AGENT_KEY,
7
22
  "frontend-implementer": FRONTEND_IMPLEMENTER_AGENT_KEY,
8
23
  "frontend-agent": FRONTEND_IMPLEMENTER_AGENT_KEY,
9
24
  frontend: FRONTEND_IMPLEMENTER_AGENT_KEY,
10
25
  "front-end": FRONTEND_IMPLEMENTER_AGENT_KEY,
11
26
  "前端": FRONTEND_IMPLEMENTER_AGENT_KEY,
12
27
  "前端实现员": FRONTEND_IMPLEMENTER_AGENT_KEY,
28
+ "nolo-project-manager": NOLO_PROJECT_MANAGER_AGENT_KEY,
29
+ "project-manager": NOLO_PROJECT_MANAGER_AGENT_KEY,
30
+ "nolo-pm": NOLO_PROJECT_MANAGER_AGENT_KEY,
31
+ pm: NOLO_PROJECT_MANAGER_AGENT_KEY,
32
+ "项目经理": NOLO_PROJECT_MANAGER_AGENT_KEY,
33
+ "nolo 项目经理": NOLO_PROJECT_MANAGER_AGENT_KEY,
34
+ "nolo-fullstack": NOLO_FULLSTACK_AGENT_KEY,
35
+ fullstack: NOLO_FULLSTACK_AGENT_KEY,
36
+ "full-stack": NOLO_FULLSTACK_AGENT_KEY,
37
+ "全栈": NOLO_FULLSTACK_AGENT_KEY,
38
+ "nolo 全栈工程师": NOLO_FULLSTACK_AGENT_KEY,
39
+ "senior-fullstack": NOLO_SENIOR_FULLSTACK_AGENT_KEY,
40
+ "nolo-senior-fullstack": NOLO_SENIOR_FULLSTACK_AGENT_KEY,
41
+ "高级全栈": NOLO_SENIOR_FULLSTACK_AGENT_KEY,
42
+ "nolo 高级全栈工程师": NOLO_SENIOR_FULLSTACK_AGENT_KEY,
43
+ "nolo-reviewer": NOLO_REVIEW_AGENT_KEY,
44
+ reviewer: NOLO_REVIEW_AGENT_KEY,
45
+ "nolo-code-review": NOLO_REVIEW_AGENT_KEY,
46
+ "code-review": NOLO_REVIEW_AGENT_KEY,
47
+ review: NOLO_REVIEW_AGENT_KEY,
48
+ "代码审查": NOLO_REVIEW_AGENT_KEY,
49
+ "nolo 代码审查": NOLO_REVIEW_AGENT_KEY,
13
50
  };
14
51
 
15
52
  function parseAgentKeyFromInput(raw: string): string {
@@ -56,7 +56,7 @@ function extractToolNames(record: any) {
56
56
  if (Array.isArray(record?.toolNames)) return record.toolNames.filter((tool: unknown) => typeof tool === "string");
57
57
  if (!Array.isArray(record?.tools)) return [];
58
58
  return record.tools
59
- .map((tool: any) => typeof tool === "string" ? tool : tool?.name)
59
+ .map((tool: any) => typeof tool === "string" ? tool : tool?.name || tool?.function?.name)
60
60
  .filter((tool: unknown): tool is string => typeof tool === "string" && tool.length > 0);
61
61
  }
62
62