@downcity/agent 1.1.51 → 1.1.63

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 (126) hide show
  1. package/README.md +5 -4
  2. package/bin/agent/Agent.d.ts.map +1 -1
  3. package/bin/agent/Agent.js.map +1 -1
  4. package/bin/config/AgentInitializer.d.ts +1 -1
  5. package/bin/config/AgentInitializer.js +1 -1
  6. package/bin/config/DowncitySchema.js +1 -1
  7. package/bin/config/DowncitySchema.js.map +1 -1
  8. package/bin/config/ExecutionBinding.d.ts +1 -1
  9. package/bin/config/ExecutionBinding.js +1 -1
  10. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  11. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +0 -4
  12. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  13. package/bin/executor/composer/system/default/InitPrompts.d.ts +1 -1
  14. package/bin/executor/composer/system/default/InitPrompts.js +1 -1
  15. package/bin/executor/composer/system/default/SystemDomain.js +1 -1
  16. package/bin/executor/composer/system/default/assets/core.prompt.d.ts +1 -1
  17. package/bin/executor/composer/system/default/assets/core.prompt.js +1 -1
  18. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +1 -1
  19. package/bin/executor/composer/system/default/assets/plugin.prompt.js +1 -1
  20. package/bin/executor/composer/system/default/assets/task.prompt.d.ts +1 -1
  21. package/bin/executor/composer/system/default/assets/task.prompt.js +1 -1
  22. package/bin/executor/messages/ChatMessageMarkupTypes.d.ts +1 -1
  23. package/bin/executor/messages/ChatMessageMarkupTypes.js +1 -1
  24. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +1 -2
  25. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  26. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +17 -56
  27. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  28. package/bin/executor/tools/shell/ShellToolBridge.js +1 -1
  29. package/bin/executor/tools/shell/ShellToolBridge.js.map +1 -1
  30. package/bin/executor/tools/shell/ShellToolDefinition.d.ts +2 -2
  31. package/bin/executor/tools/shell/ShellToolFormatting.d.ts +1 -1
  32. package/bin/executor/tools/shell/ShellToolFormatting.js +7 -7
  33. package/bin/executor/tools/shell/ShellToolFormatting.js.map +1 -1
  34. package/bin/executor/tools/shell/ShellToolSchemas.d.ts +7 -75
  35. package/bin/executor/tools/shell/ShellToolSchemas.d.ts.map +1 -1
  36. package/bin/executor/types/SessionHistoryMeta.d.ts +5 -24
  37. package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
  38. package/bin/executor/types/SessionHistoryMeta.js +1 -1
  39. package/bin/index.d.ts +3 -2
  40. package/bin/index.d.ts.map +1 -1
  41. package/bin/index.js +1 -0
  42. package/bin/index.js.map +1 -1
  43. package/bin/model/CityModelAdapter.d.ts +23 -0
  44. package/bin/model/CityModelAdapter.d.ts.map +1 -0
  45. package/bin/model/CityModelAdapter.js +454 -0
  46. package/bin/model/CityModelAdapter.js.map +1 -0
  47. package/bin/runtime/host/daemon/Paths.d.ts +1 -1
  48. package/bin/runtime/host/daemon/Paths.js +1 -1
  49. package/bin/runtime/host/daemon/ProjectSetup.js +2 -2
  50. package/bin/session/Session.d.ts +1 -0
  51. package/bin/session/Session.d.ts.map +1 -1
  52. package/bin/session/Session.js +32 -6
  53. package/bin/session/Session.js.map +1 -1
  54. package/bin/session/SessionTitle.d.ts +49 -0
  55. package/bin/session/SessionTitle.d.ts.map +1 -0
  56. package/bin/session/SessionTitle.js +130 -0
  57. package/bin/session/SessionTitle.js.map +1 -0
  58. package/bin/session/browse/Browse.d.ts +2 -1
  59. package/bin/session/browse/Browse.d.ts.map +1 -1
  60. package/bin/session/browse/Browse.js +18 -15
  61. package/bin/session/browse/Browse.js.map +1 -1
  62. package/bin/session/index.d.ts +2 -1
  63. package/bin/session/index.d.ts.map +1 -1
  64. package/bin/session/index.js +2 -1
  65. package/bin/session/index.js.map +1 -1
  66. package/bin/session/storage/Metadata.d.ts +4 -0
  67. package/bin/session/storage/Metadata.d.ts.map +1 -1
  68. package/bin/session/storage/Metadata.js +12 -25
  69. package/bin/session/storage/Metadata.js.map +1 -1
  70. package/bin/session/storage/Persistence.d.ts.map +1 -1
  71. package/bin/session/storage/Persistence.js +1 -4
  72. package/bin/session/storage/Persistence.js.map +1 -1
  73. package/bin/types/agent/AgentTypes.d.ts +9 -5
  74. package/bin/types/agent/AgentTypes.d.ts.map +1 -1
  75. package/bin/types/config/AgentProject.d.ts +1 -1
  76. package/bin/types/config/DowncityConfig.d.ts +3 -3
  77. package/bin/types/config/ExecutionBinding.d.ts +4 -4
  78. package/bin/types/config/ExecutionBinding.js +1 -1
  79. package/bin/types/runtime/auth/AuthPermission.js +2 -2
  80. package/bin/types/runtime/auth/AuthPermission.js.map +1 -1
  81. package/bin/types/runtime/host/Store.d.ts +3 -177
  82. package/bin/types/runtime/host/Store.d.ts.map +1 -1
  83. package/bin/types/runtime/host/Store.js +7 -0
  84. package/bin/types/runtime/host/Store.js.map +1 -1
  85. package/bin/types/runtime/http/InlineInstant.d.ts +1 -1
  86. package/bin/types/runtime/platform/Platform.d.ts +1 -1
  87. package/package.json +19 -18
  88. package/scripts/city-model-tool-loop.test.mjs +181 -0
  89. package/src/agent/Agent.ts +3 -2
  90. package/src/config/AgentInitializer.ts +1 -1
  91. package/src/config/DowncitySchema.ts +1 -1
  92. package/src/config/ExecutionBinding.ts +1 -1
  93. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +0 -4
  94. package/src/executor/composer/system/default/InitPrompts.ts +1 -1
  95. package/src/executor/composer/system/default/SystemDomain.ts +1 -1
  96. package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
  97. package/src/executor/composer/system/default/assets/core.prompt.ts.txt +1 -1
  98. package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
  99. package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +18 -18
  100. package/src/executor/composer/system/default/assets/task.prompt.ts +1 -1
  101. package/src/executor/composer/system/default/assets/task.prompt.ts.txt +1 -1
  102. package/src/executor/messages/ChatMessageMarkupTypes.ts +1 -1
  103. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +17 -57
  104. package/src/executor/tools/shell/ShellToolBridge.ts +1 -1
  105. package/src/executor/tools/shell/ShellToolFormatting.ts +7 -7
  106. package/src/executor/types/SessionHistoryMeta.ts +5 -25
  107. package/src/index.ts +5 -5
  108. package/src/model/CityModelAdapter.ts +553 -0
  109. package/src/runtime/host/daemon/Paths.ts +1 -1
  110. package/src/runtime/host/daemon/ProjectSetup.ts +2 -2
  111. package/src/session/Session.ts +40 -6
  112. package/src/session/SessionTitle.ts +192 -0
  113. package/src/session/browse/Browse.ts +22 -13
  114. package/src/session/index.ts +5 -0
  115. package/src/session/storage/Metadata.ts +14 -29
  116. package/src/session/storage/Persistence.ts +1 -4
  117. package/src/types/agent/AgentTypes.ts +10 -5
  118. package/src/types/config/AgentProject.ts +1 -1
  119. package/src/types/config/DowncityConfig.ts +3 -3
  120. package/src/types/config/ExecutionBinding.ts +4 -4
  121. package/src/types/runtime/auth/AuthPermission.ts +2 -2
  122. package/src/types/runtime/host/Store.ts +3 -182
  123. package/src/types/runtime/http/InlineInstant.ts +1 -1
  124. package/src/types/runtime/platform/Platform.ts +1 -1
  125. package/tsconfig.json +1 -0
  126. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,553 @@
1
+ /**
2
+ * CityModel 到 AI SDK LanguageModel 的适配模块。
3
+ *
4
+ * 关键点(中文)
5
+ * - Agent 对外可以接收 CityModel,但 executor 内部仍只处理 AI SDK LanguageModel。
6
+ * - 适配逻辑集中在这里,避免 City 协议散落到 session/executor 各处。
7
+ * - 这里不依赖 @downcity/city,只依赖 @downcity/type 的共享协议。
8
+ */
9
+
10
+ import {
11
+ CITY_MODEL_INVOKER,
12
+ isCityModel,
13
+ type CityModel,
14
+ type CityModelInvokeInput,
15
+ } from "@downcity/type";
16
+ import type { LanguageModel, UIMessage, UIMessageChunk } from "ai";
17
+
18
+ /**
19
+ * Agent SDK 可接受的模型输入。
20
+ */
21
+ export type AgentModel = LanguageModel | CityModel;
22
+
23
+ type ProviderPromptMessage = {
24
+ /**
25
+ * 模型消息角色。
26
+ */
27
+ role?: string;
28
+
29
+ /**
30
+ * 模型消息内容。
31
+ */
32
+ content?: unknown;
33
+ };
34
+
35
+ type ProviderStreamController = ReadableStreamDefaultController<Record<string, unknown>>;
36
+ type ProviderContentPart = Record<string, unknown>;
37
+ type ProviderPromptRole = "system" | "user" | "assistant" | "tool";
38
+
39
+ type ProviderToolResultOutput = {
40
+ /**
41
+ * tool result 输出类型。
42
+ */
43
+ type?: unknown;
44
+
45
+ /**
46
+ * tool result 输出值。
47
+ */
48
+ value?: unknown;
49
+
50
+ /**
51
+ * tool 拒绝原因。
52
+ */
53
+ reason?: unknown;
54
+ };
55
+
56
+ function normalizeFinishReason(input: unknown): {
57
+ unified: "stop" | "length" | "content-filter" | "tool-calls" | "error" | "other";
58
+ raw: string | undefined;
59
+ } {
60
+ const text = typeof input === "string" ? input : "";
61
+ if (text === "stop" || text === "length" || text === "content-filter" || text === "tool-calls" || text === "error") {
62
+ return { unified: text, raw: text };
63
+ }
64
+ return { unified: "stop", raw: text || "stop" };
65
+ }
66
+
67
+ function stringifyToolInput(input: unknown): string {
68
+ if (typeof input === "string") return input;
69
+ try {
70
+ return JSON.stringify(input ?? {});
71
+ } catch {
72
+ return "{}";
73
+ }
74
+ }
75
+
76
+ function textFromProviderContent(content: unknown): string {
77
+ if (typeof content === "string") return content;
78
+ if (!Array.isArray(content)) return "";
79
+ return content
80
+ .filter((part) => part && typeof part === "object" && (part as { type?: unknown }).type === "text")
81
+ .map((part) => String((part as { text?: unknown }).text ?? ""))
82
+ .join("\n")
83
+ .trim();
84
+ }
85
+
86
+ function fileUrlFromProviderPart(part: Record<string, unknown>): string {
87
+ const data = part.data;
88
+ if (data instanceof URL) return data.toString();
89
+ if (typeof data === "string") return data;
90
+ return "";
91
+ }
92
+
93
+ function resolveProviderPromptRole(input: unknown): ProviderPromptRole {
94
+ return input === "system" || input === "user" || input === "assistant" || input === "tool"
95
+ ? input
96
+ : "user";
97
+ }
98
+
99
+ function normalizeProviderToolResultOutput(
100
+ output: ProviderToolResultOutput,
101
+ ): {
102
+ /**
103
+ * 归一后的 tool part 状态。
104
+ */
105
+ state: "output-available" | "output-error";
106
+
107
+ /**
108
+ * tool 成功输出。
109
+ */
110
+ output?: unknown;
111
+
112
+ /**
113
+ * tool 错误文本。
114
+ */
115
+ errorText?: string;
116
+ } {
117
+ const outputType = typeof output.type === "string" ? output.type : "";
118
+ if (outputType === "json" || outputType === "text" || outputType === "content") {
119
+ return {
120
+ state: "output-available",
121
+ output: "value" in output ? output.value : null,
122
+ };
123
+ }
124
+ if (outputType === "error-json") {
125
+ return {
126
+ state: "output-error",
127
+ errorText: stringifyToolInput("value" in output ? output.value : null),
128
+ };
129
+ }
130
+ if (outputType === "error-text") {
131
+ return {
132
+ state: "output-error",
133
+ errorText: String(output.value ?? ""),
134
+ };
135
+ }
136
+ if (outputType === "execution-denied") {
137
+ return {
138
+ state: "output-error",
139
+ errorText: String(output.reason ?? "tool execution denied"),
140
+ };
141
+ }
142
+ return {
143
+ state: "output-available",
144
+ output,
145
+ };
146
+ }
147
+
148
+ function providerContentToUiParts(
149
+ content: unknown,
150
+ existingParts?: UIMessage["parts"],
151
+ ): UIMessage["parts"] {
152
+ if (!Array.isArray(content)) {
153
+ return [{ type: "text", text: textFromProviderContent(content) }];
154
+ }
155
+
156
+ const parts: UIMessage["parts"] = Array.isArray(existingParts)
157
+ ? [...existingParts]
158
+ : [];
159
+ for (const part of content) {
160
+ if (!part || typeof part !== "object") continue;
161
+ const record = part as Record<string, unknown>;
162
+ if (record.type === "text") {
163
+ parts.push({ type: "text", text: String(record.text ?? "") });
164
+ continue;
165
+ }
166
+ if (record.type === "reasoning") {
167
+ parts.push({ type: "reasoning", text: String(record.text ?? "") });
168
+ continue;
169
+ }
170
+ if (record.type === "file") {
171
+ const url = fileUrlFromProviderPart(record);
172
+ if (!url) continue;
173
+ parts.push({
174
+ type: "file",
175
+ mediaType: String(record.mediaType ?? "application/octet-stream"),
176
+ filename: typeof record.filename === "string" ? record.filename : undefined,
177
+ url,
178
+ });
179
+ continue;
180
+ }
181
+ if (record.type === "tool-call") {
182
+ parts.push({
183
+ type: "dynamic-tool",
184
+ toolName: String(record.toolName ?? ""),
185
+ toolCallId: String(record.toolCallId ?? ""),
186
+ state: "input-available",
187
+ input: record.input,
188
+ providerExecuted: Boolean(record.providerExecuted),
189
+ });
190
+ continue;
191
+ }
192
+ if (record.type === "tool-result") {
193
+ const toolCallId = String(record.toolCallId ?? "");
194
+ const toolName = String(record.toolName ?? "");
195
+ const normalizedOutput = normalizeProviderToolResultOutput(
196
+ (record.output as ProviderToolResultOutput | undefined) ?? {},
197
+ );
198
+ const existingPart = parts.find((item) => {
199
+ if (!item || typeof item !== "object") return false;
200
+ const toolPart = item as { toolCallId?: unknown };
201
+ return String(toolPart.toolCallId ?? "") === toolCallId;
202
+ }) as
203
+ | ({
204
+ toolCallId?: unknown;
205
+ toolName?: unknown;
206
+ input?: unknown;
207
+ providerExecuted?: unknown;
208
+ } & Record<string, unknown>)
209
+ | undefined;
210
+ const baseInput = existingPart?.input ?? null;
211
+ const baseToolName = String(existingPart?.toolName ?? toolName);
212
+ const nextPart = normalizedOutput.state === "output-available"
213
+ ? {
214
+ type: "dynamic-tool" as const,
215
+ toolName: baseToolName,
216
+ toolCallId,
217
+ state: "output-available" as const,
218
+ input: baseInput,
219
+ output: normalizedOutput.output,
220
+ providerExecuted: false,
221
+ }
222
+ : {
223
+ type: "dynamic-tool" as const,
224
+ toolName: baseToolName,
225
+ toolCallId,
226
+ state: "output-error" as const,
227
+ input: baseInput,
228
+ errorText: normalizedOutput.errorText ?? "tool_error",
229
+ providerExecuted: false,
230
+ };
231
+ if (existingPart) {
232
+ const index = parts.indexOf(existingPart as never);
233
+ if (index >= 0) {
234
+ parts[index] = nextPart;
235
+ continue;
236
+ }
237
+ }
238
+ parts.push(nextPart);
239
+ }
240
+ }
241
+ return parts;
242
+ }
243
+
244
+ function providerPromptToMessages(prompt: unknown): UIMessage[] {
245
+ if (!Array.isArray(prompt)) return [];
246
+ const messages: UIMessage[] = [];
247
+ for (const [index, message] of prompt.entries()) {
248
+ if (!message || typeof message !== "object") continue;
249
+ const item = message as ProviderPromptMessage;
250
+ const role = resolveProviderPromptRole(item.role);
251
+ if (role === "tool") {
252
+ const lastAssistantMessage = [...messages]
253
+ .reverse()
254
+ .find((candidate) => candidate.role === "assistant");
255
+ if (lastAssistantMessage) {
256
+ lastAssistantMessage.parts = providerContentToUiParts(
257
+ item.content,
258
+ lastAssistantMessage.parts,
259
+ );
260
+ continue;
261
+ }
262
+ messages.push({
263
+ id: `city-model-message-${String(index)}`,
264
+ role: "assistant",
265
+ parts: providerContentToUiParts(item.content),
266
+ });
267
+ continue;
268
+ }
269
+ messages.push({
270
+ id: `city-model-message-${String(index)}`,
271
+ role,
272
+ parts: providerContentToUiParts(item.content),
273
+ });
274
+ }
275
+ return messages;
276
+ }
277
+
278
+ function providerOptionsToInput(options: Record<string, unknown>): CityModelInvokeInput {
279
+ return {
280
+ messages: providerPromptToMessages(options.prompt),
281
+ tools: options.tools,
282
+ toolChoice: options.toolChoice,
283
+ providerOptions: options.providerOptions,
284
+ };
285
+ }
286
+
287
+ function textFromUiMessage(message: UIMessage): string {
288
+ return message.parts
289
+ .filter((part) => part.type === "text")
290
+ .map((part) => String((part as { text?: unknown }).text ?? ""))
291
+ .join("\n")
292
+ .trim();
293
+ }
294
+
295
+ function uiMessageToProviderContent(message: UIMessage): ProviderContentPart[] {
296
+ return message.parts.flatMap((part): ProviderContentPart[] => {
297
+ if (part.type === "text") {
298
+ return [{ type: "text", text: String((part as { text?: unknown }).text ?? "") }];
299
+ }
300
+ if (part.type === "reasoning") {
301
+ return [{ type: "reasoning", text: String((part as { text?: unknown }).text ?? "") }];
302
+ }
303
+ if (part.type === "dynamic-tool") {
304
+ const toolPart = part as {
305
+ toolCallId?: unknown;
306
+ toolName?: unknown;
307
+ input?: unknown;
308
+ output?: unknown;
309
+ errorText?: unknown;
310
+ state?: unknown;
311
+ providerExecuted?: unknown;
312
+ };
313
+ const content: ProviderContentPart[] = [{
314
+ type: "tool-call",
315
+ toolCallId: String(toolPart.toolCallId ?? ""),
316
+ toolName: String(toolPart.toolName ?? ""),
317
+ input: stringifyToolInput(toolPart.input),
318
+ providerExecuted: Boolean(toolPart.providerExecuted),
319
+ }];
320
+ if (toolPart.state === "output-available") {
321
+ content.push({
322
+ type: "tool-result",
323
+ toolCallId: String(toolPart.toolCallId ?? ""),
324
+ toolName: String(toolPart.toolName ?? ""),
325
+ output: {
326
+ type: "json",
327
+ value: toolPart.output ?? null,
328
+ },
329
+ });
330
+ } else if (toolPart.state === "output-error") {
331
+ content.push({
332
+ type: "tool-result",
333
+ toolCallId: String(toolPart.toolCallId ?? ""),
334
+ toolName: String(toolPart.toolName ?? ""),
335
+ output: {
336
+ type: "error-text",
337
+ value: String(toolPart.errorText ?? "tool_error"),
338
+ },
339
+ });
340
+ }
341
+ return content;
342
+ }
343
+ return [];
344
+ });
345
+ }
346
+
347
+ function enqueueFinish(
348
+ controller: ProviderStreamController,
349
+ finishReason: unknown,
350
+ ): void {
351
+ controller.enqueue({
352
+ type: "finish",
353
+ finishReason: normalizeFinishReason(finishReason),
354
+ usage: {
355
+ inputTokens: {
356
+ total: undefined,
357
+ noCache: undefined,
358
+ cacheRead: undefined,
359
+ cacheWrite: undefined,
360
+ },
361
+ outputTokens: {
362
+ total: undefined,
363
+ text: undefined,
364
+ reasoning: undefined,
365
+ },
366
+ },
367
+ });
368
+ }
369
+
370
+ function enqueueProviderParts(
371
+ controller: ProviderStreamController,
372
+ parts: Record<string, unknown>[],
373
+ state: {
374
+ /**
375
+ * 当前流是否已经发出 stream-start。
376
+ */
377
+ sawStart: boolean;
378
+
379
+ /**
380
+ * 当前流是否已经发出 finish。
381
+ */
382
+ sawFinish: boolean;
383
+ },
384
+ ): void {
385
+ for (const part of parts) {
386
+ if (part.type !== "stream-start" && !state.sawStart) {
387
+ controller.enqueue({ type: "stream-start", warnings: [] });
388
+ state.sawStart = true;
389
+ }
390
+ if (part.type === "stream-start") state.sawStart = true;
391
+ if (part.type === "finish") state.sawFinish = true;
392
+ controller.enqueue(part);
393
+ }
394
+ }
395
+
396
+ function mapUiChunkToProviderParts(chunk: UIMessageChunk): ProviderContentPart[] {
397
+ switch (chunk.type) {
398
+ case "start":
399
+ return [{ type: "stream-start", warnings: [] }];
400
+ case "text-start":
401
+ return [{ type: "text-start", id: chunk.id }];
402
+ case "text-delta":
403
+ return [{ type: "text-delta", id: chunk.id, delta: chunk.delta }];
404
+ case "text-end":
405
+ return [{ type: "text-end", id: chunk.id }];
406
+ case "reasoning-start":
407
+ return [{ type: "reasoning-start", id: chunk.id }];
408
+ case "reasoning-delta":
409
+ return [{ type: "reasoning-delta", id: chunk.id, delta: chunk.delta }];
410
+ case "reasoning-end":
411
+ return [{ type: "reasoning-end", id: chunk.id }];
412
+ case "tool-input-start":
413
+ return [{
414
+ type: "tool-input-start",
415
+ id: chunk.toolCallId,
416
+ toolName: chunk.toolName,
417
+ providerExecuted: chunk.providerExecuted,
418
+ dynamic: chunk.dynamic,
419
+ }];
420
+ case "tool-input-delta":
421
+ return [{
422
+ type: "tool-input-delta",
423
+ id: chunk.toolCallId,
424
+ delta: chunk.inputTextDelta,
425
+ }];
426
+ case "tool-input-available":
427
+ return [{
428
+ type: "tool-call",
429
+ toolCallId: chunk.toolCallId,
430
+ toolName: chunk.toolName,
431
+ input: stringifyToolInput(chunk.input),
432
+ providerExecuted: chunk.providerExecuted,
433
+ dynamic: chunk.dynamic,
434
+ }];
435
+ case "tool-output-available":
436
+ return [{
437
+ type: "tool-result",
438
+ toolCallId: chunk.toolCallId,
439
+ toolName: "",
440
+ output: { type: "json", value: chunk.output },
441
+ providerExecuted: chunk.providerExecuted,
442
+ dynamic: chunk.dynamic,
443
+ }];
444
+ case "error":
445
+ return [{ type: "error", error: new Error(chunk.errorText) }];
446
+ default:
447
+ return [];
448
+ }
449
+ }
450
+
451
+ function cityModelToLanguageModel(model: CityModel): LanguageModel {
452
+ const invoker = model[CITY_MODEL_INVOKER];
453
+ const languageModel = {
454
+ specificationVersion: "v3",
455
+ provider: "downcity",
456
+ modelId: model.id,
457
+ supportedUrls: {},
458
+ async doGenerate(options) {
459
+ const message = await invoker.text(providerOptionsToInput(options as Record<string, unknown>));
460
+ return {
461
+ content: uiMessageToProviderContent(message),
462
+ finishReason: normalizeFinishReason("stop"),
463
+ usage: {
464
+ inputTokens: {
465
+ total: undefined,
466
+ noCache: undefined,
467
+ cacheRead: undefined,
468
+ cacheWrite: undefined,
469
+ },
470
+ outputTokens: {
471
+ total: undefined,
472
+ text: undefined,
473
+ reasoning: undefined,
474
+ },
475
+ },
476
+ response: {
477
+ modelId: model.id,
478
+ },
479
+ warnings: [],
480
+ };
481
+ },
482
+ async doStream(options) {
483
+ const cityStream = await invoker.stream(providerOptionsToInput(options as Record<string, unknown>));
484
+ return {
485
+ stream: new ReadableStream({
486
+ async start(controller: ProviderStreamController) {
487
+ const reader = cityStream.getReader();
488
+ const state = {
489
+ sawStart: false,
490
+ sawFinish: false,
491
+ };
492
+ try {
493
+ while (true) {
494
+ const { done, value } = await reader.read();
495
+ if (done) break;
496
+ const parts = mapUiChunkToProviderParts(value);
497
+ enqueueProviderParts(controller, parts, state);
498
+ }
499
+ if (!state.sawStart) controller.enqueue({ type: "stream-start", warnings: [] });
500
+ if (!state.sawFinish) enqueueFinish(controller, "stop");
501
+ controller.close();
502
+ } catch (error) {
503
+ controller.enqueue({ type: "error", error });
504
+ if (!state.sawFinish) enqueueFinish(controller, "error");
505
+ controller.close();
506
+ } finally {
507
+ reader.releaseLock();
508
+ }
509
+ },
510
+ }),
511
+ response: {
512
+ modelId: model.id,
513
+ },
514
+ };
515
+ },
516
+ };
517
+
518
+ return languageModel as unknown as LanguageModel;
519
+ }
520
+
521
+ /**
522
+ * 将 Agent 可接受的模型输入归一为 AI SDK LanguageModel。
523
+ */
524
+ export function normalizeAgentModel(model: AgentModel): LanguageModel {
525
+ if (isCityModel(model)) return cityModelToLanguageModel(model);
526
+ return model;
527
+ }
528
+
529
+ /**
530
+ * 从 Agent 模型输入推导展示标签。
531
+ */
532
+ export function inferAgentModelLabel(model: AgentModel | undefined): string | undefined {
533
+ if (!model) return undefined;
534
+ if (isCityModel(model)) return model.name || model.id;
535
+ if (typeof model !== "object") return undefined;
536
+ const record = model as Record<string, unknown>;
537
+ const candidates = [
538
+ record.modelId,
539
+ record.model,
540
+ record.id,
541
+ record.name,
542
+ record.label,
543
+ ];
544
+ for (const candidate of candidates) {
545
+ const label = typeof candidate === "string" ? candidate.trim() : "";
546
+ if (label) return label;
547
+ }
548
+ const constructorName =
549
+ model.constructor && typeof model.constructor.name === "string"
550
+ ? model.constructor.name.trim()
551
+ : "";
552
+ return constructorName || "configured-model";
553
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 这里只保留 agent 侧访问 daemon meta 所需的项目级路径。
6
- * - daemon 进程启停、pid 清理、registry 同步属于 `@downcity/city`,不放在 agent 包内。
6
+ * - daemon 进程启停、pid 清理、registry 同步属于 `downcity`,不放在 agent 包内。
7
7
  */
8
8
 
9
9
  import path from "node:path";
@@ -30,14 +30,14 @@ function ensureContextFiles(projectRoot: string): void {
30
30
  // Check if initialized(启动入口一次性确认工程根目录与关键文件)
31
31
  if (!fs.existsSync(getProfileMdPath(projectRoot))) {
32
32
  console.error(
33
- '❌ Project not initialized. Please run "city agent create" first',
33
+ '❌ Project not initialized. Please run "town agent create" first',
34
34
  );
35
35
  process.exit(1);
36
36
  }
37
37
 
38
38
  if (!fs.existsSync(getDowncityJsonPath(projectRoot))) {
39
39
  console.error(
40
- '❌ downcity.json does not exist. Please run "city agent create" first',
40
+ '❌ downcity.json does not exist. Please run "town agent create" first',
41
41
  );
42
42
  process.exit(1);
43
43
  }
@@ -30,7 +30,6 @@ import {
30
30
  SessionSystemBuilder,
31
31
  } from "@/session/SessionSystemBuilder.js";
32
32
  import {
33
- inferModelLabel,
34
33
  buildSessionHistoryPage,
35
34
  buildSessionInfo,
36
35
  patchSessionModelLabel,
@@ -62,6 +61,11 @@ import type { AgentSessionTurnHandle } from "@/types/sdk/AgentSessionTurn.js";
62
61
  import type { SessionUserMessageV1 } from "@/executor/types/SessionMessages.js";
63
62
  import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
64
63
  import { SessionPromptRuntime } from "@/session/runtime/SessionPromptRuntime.js";
64
+ import {
65
+ inferAgentModelLabel,
66
+ normalizeAgentModel,
67
+ } from "@/model/CityModelAdapter.js";
68
+ import { ensureSessionTitle } from "@/session/SessionTitle.js";
65
69
 
66
70
  type SessionOptions = {
67
71
  /**
@@ -257,8 +261,8 @@ export class Session implements AgentSession {
257
261
  this.createdAt = createdAt;
258
262
  this.timezone = timezone;
259
263
  this.sessionConfig = {
260
- ...(metadata.sdkConfig?.modelLabel
261
- ? { modelLabel: metadata.sdkConfig.modelLabel }
264
+ ...(metadata.modelLabel
265
+ ? { modelLabel: metadata.modelLabel }
262
266
  : {}),
263
267
  };
264
268
  return this;
@@ -280,8 +284,8 @@ export class Session implements AgentSession {
280
284
  */
281
285
  async set(input: AgentSessionSetInput): Promise<void> {
282
286
  if (input.model) {
283
- this.sessionConfig.model = input.model;
284
- this.sessionConfig.modelLabel = inferModelLabel(input.model);
287
+ this.sessionConfig.model = normalizeAgentModel(input.model);
288
+ this.sessionConfig.modelLabel = inferAgentModelLabel(input.model);
285
289
  this.executor.clearExecutor();
286
290
  }
287
291
  await patchSessionModelLabel({
@@ -333,6 +337,7 @@ export class Session implements AgentSession {
333
337
  await this.executor.appendUserMessage({
334
338
  text: String(input.text || "").trim(),
335
339
  });
340
+ await this.ensureTitleFromHistory({ generate: true });
336
341
  await this.touchMetadata();
337
342
  }
338
343
 
@@ -363,11 +368,19 @@ export class Session implements AgentSession {
363
368
  }),
364
369
  this.historyStore.list(),
365
370
  ]);
371
+ const metadataWithTitle = metadata.title
372
+ ? metadata
373
+ : await ensureSessionTitle({
374
+ projectRoot: this.projectRoot,
375
+ agentId: this.agentId,
376
+ sessionId: this.id,
377
+ messages,
378
+ });
366
379
  return buildSessionInfo({
367
380
  projectRoot: this.projectRoot,
368
381
  agentId: this.agentId,
369
382
  sessionId: this.id,
370
- metadata,
383
+ metadata: metadataWithTitle,
371
384
  messages,
372
385
  executing: this.isExecuting(),
373
386
  });
@@ -477,6 +490,7 @@ export class Session implements AgentSession {
477
490
  for (const message of forkMessages) {
478
491
  await forked.historyStore.append(message);
479
492
  }
493
+ await forked.ensureTitleFromHistory({ generate: true });
480
494
  await forked.touchMetadata();
481
495
  return forked;
482
496
  }
@@ -503,6 +517,8 @@ export class Session implements AgentSession {
503
517
  },
504
518
  appendUserMessage: async (messageParams) => {
505
519
  await this.executor.appendUserMessage(messageParams);
520
+ await this.ensureTitleFromHistory({ generate: true });
521
+ await this.touchMetadata();
506
522
  },
507
523
  appendAssistantMessage: async (messageParams) => {
508
524
  await this.executor.appendAssistantMessage(messageParams);
@@ -558,6 +574,23 @@ export class Session implements AgentSession {
558
574
  });
559
575
  }
560
576
 
577
+ private async ensureTitleFromHistory(input?: {
578
+ /**
579
+ * 是否允许调用模型生成标题。
580
+ */
581
+ generate?: boolean;
582
+ }): Promise<void> {
583
+ const messages = await this.historyStore.list();
584
+ await ensureSessionTitle({
585
+ projectRoot: this.projectRoot,
586
+ agentId: this.agentId,
587
+ sessionId: this.id,
588
+ messages,
589
+ ...(input?.generate ? { model: this.sessionConfig.model } : {}),
590
+ generate: input?.generate === true,
591
+ });
592
+ }
593
+
561
594
  private async persistAssistantResult(
562
595
  assistantMessage: SessionMessageV1,
563
596
  ): Promise<void> {
@@ -583,6 +616,7 @@ export class Session implements AgentSession {
583
616
  await this.executor.appendUserMessage({
584
617
  message,
585
618
  });
619
+ await this.ensureTitleFromHistory({ generate: true });
586
620
  await this.touchMetadata();
587
621
  return message;
588
622
  }