@downcity/agent 1.1.51 → 1.1.62

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 (125) 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 +338 -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/src/agent/Agent.ts +3 -2
  89. package/src/config/AgentInitializer.ts +1 -1
  90. package/src/config/DowncitySchema.ts +1 -1
  91. package/src/config/ExecutionBinding.ts +1 -1
  92. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +0 -4
  93. package/src/executor/composer/system/default/InitPrompts.ts +1 -1
  94. package/src/executor/composer/system/default/SystemDomain.ts +1 -1
  95. package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
  96. package/src/executor/composer/system/default/assets/core.prompt.ts.txt +1 -1
  97. package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
  98. package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +18 -18
  99. package/src/executor/composer/system/default/assets/task.prompt.ts +1 -1
  100. package/src/executor/composer/system/default/assets/task.prompt.ts.txt +1 -1
  101. package/src/executor/messages/ChatMessageMarkupTypes.ts +1 -1
  102. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +17 -57
  103. package/src/executor/tools/shell/ShellToolBridge.ts +1 -1
  104. package/src/executor/tools/shell/ShellToolFormatting.ts +7 -7
  105. package/src/executor/types/SessionHistoryMeta.ts +5 -25
  106. package/src/index.ts +5 -5
  107. package/src/model/CityModelAdapter.ts +384 -0
  108. package/src/runtime/host/daemon/Paths.ts +1 -1
  109. package/src/runtime/host/daemon/ProjectSetup.ts +2 -2
  110. package/src/session/Session.ts +40 -6
  111. package/src/session/SessionTitle.ts +192 -0
  112. package/src/session/browse/Browse.ts +22 -13
  113. package/src/session/index.ts +5 -0
  114. package/src/session/storage/Metadata.ts +14 -29
  115. package/src/session/storage/Persistence.ts +1 -4
  116. package/src/types/agent/AgentTypes.ts +10 -5
  117. package/src/types/config/AgentProject.ts +1 -1
  118. package/src/types/config/DowncityConfig.ts +3 -3
  119. package/src/types/config/ExecutionBinding.ts +4 -4
  120. package/src/types/runtime/auth/AuthPermission.ts +2 -2
  121. package/src/types/runtime/host/Store.ts +3 -182
  122. package/src/types/runtime/http/InlineInstant.ts +1 -1
  123. package/src/types/runtime/platform/Platform.ts +1 -1
  124. package/tsconfig.json +1 -0
  125. package/tsconfig.tsbuildinfo +1 -1
@@ -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
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * SDK Session 标题生成与持久化辅助。
3
+ *
4
+ * 关键点(中文)
5
+ * - session title 是 `meta.json` 顶层字段,列表与详情都以它为准。
6
+ * - 首次用户消息出现后优先使用模型生成标题,失败时回退到首条用户消息截断。
7
+ * - 老 session 缺失 title 时可在读取列表/详情时补写 fallback title。
8
+ */
9
+
10
+ import { generateText, type LanguageModel } from "ai";
11
+ import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
12
+ import type { SessionMessageV1 } from "@/executor/types/SessionMessages.js";
13
+ import {
14
+ normalizeSessionTitle,
15
+ readSessionMetadata,
16
+ writeSessionMetadata,
17
+ } from "@/session/storage/Metadata.js";
18
+
19
+ const FALLBACK_SESSION_TITLE_MAX_CHARS = 60;
20
+ const GENERATED_SESSION_TITLE_MAX_CHARS = 24;
21
+
22
+ /**
23
+ * 标题持久化参数。
24
+ */
25
+ export interface EnsureSessionTitleParams {
26
+ /**
27
+ * 当前项目根目录。
28
+ */
29
+ projectRoot: string;
30
+
31
+ /**
32
+ * 当前 agent 稳定标识。
33
+ */
34
+ agentId: string;
35
+
36
+ /**
37
+ * 当前 sessionId。
38
+ */
39
+ sessionId: string;
40
+
41
+ /**
42
+ * 当前 session 已落盘消息。
43
+ */
44
+ messages: SessionMessageV1[];
45
+
46
+ /**
47
+ * 可选模型实例;传入时会尝试生成更短标题。
48
+ */
49
+ model?: LanguageModel;
50
+
51
+ /**
52
+ * 是否允许调用模型生成标题。
53
+ */
54
+ generate?: boolean;
55
+ }
56
+
57
+ function truncateTitle(input: string, maxChars: number): string {
58
+ const title = String(input || "").replace(/\s+/g, " ").trim();
59
+ if (!title) return "";
60
+ if (title.length <= maxChars) return title;
61
+ return title.slice(0, maxChars).trimEnd();
62
+ }
63
+
64
+ function extractTextFromMessage(message: SessionMessageV1): string {
65
+ if (!Array.isArray(message.parts)) return "";
66
+ const texts: string[] = [];
67
+ for (const part of message.parts) {
68
+ if (!part || typeof part !== "object") continue;
69
+ const textPart = part as { type?: unknown; text?: unknown };
70
+ if (textPart.type !== "text" || typeof textPart.text !== "string") continue;
71
+ const text = textPart.text.trim();
72
+ if (!text) continue;
73
+ texts.push(text);
74
+ }
75
+ return texts.join("\n").trim();
76
+ }
77
+
78
+ function resolveFirstUserText(messages: SessionMessageV1[]): string {
79
+ for (const message of messages) {
80
+ if (message.role !== "user") continue;
81
+ const text = extractTextFromMessage(message);
82
+ if (text) return text;
83
+ }
84
+ return "";
85
+ }
86
+
87
+ function normalizeGeneratedTitle(input: string): string | undefined {
88
+ const firstLine = String(input || "")
89
+ .split("\n")
90
+ .map((line) => line.trim())
91
+ .find(Boolean);
92
+ const title = normalizeSessionTitle(
93
+ String(firstLine || "")
94
+ .replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, "")
95
+ .replace(/^标题[::]\s*/i, "")
96
+ .trim(),
97
+ );
98
+ return title
99
+ ? truncateTitle(title, GENERATED_SESSION_TITLE_MAX_CHARS)
100
+ : undefined;
101
+ }
102
+
103
+ /**
104
+ * 从首条用户消息推导 fallback 标题。
105
+ */
106
+ export function resolveFallbackSessionTitle(
107
+ messages: SessionMessageV1[],
108
+ ): string | undefined {
109
+ const firstUserText = resolveFirstUserText(messages);
110
+ const title = truncateTitle(
111
+ firstUserText,
112
+ FALLBACK_SESSION_TITLE_MAX_CHARS,
113
+ );
114
+ return normalizeSessionTitle(title);
115
+ }
116
+
117
+ async function generateSessionTitle(input: {
118
+ /**
119
+ * 当前模型实例。
120
+ */
121
+ model: LanguageModel;
122
+
123
+ /**
124
+ * 首条用户消息文本。
125
+ */
126
+ firstUserText: string;
127
+ }): Promise<string | undefined> {
128
+ try {
129
+ const result = await generateText({
130
+ model: input.model,
131
+ system:
132
+ "你负责为一段会话生成极简标题。只输出标题本身,不要解释,不要使用引号。",
133
+ prompt: [
134
+ "根据下面这条用户首条消息,生成一个简短的会话标题。",
135
+ "要求:3 到 12 个汉字或 2 到 6 个英文词;不要句号;不要前缀。",
136
+ "",
137
+ input.firstUserText,
138
+ ].join("\n"),
139
+ });
140
+ return normalizeGeneratedTitle(result.text);
141
+ } catch {
142
+ // 关键点(中文):标题生成失败不能影响 session 主流程。
143
+ return undefined;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 确保当前 session meta 中持久化 title。
149
+ */
150
+ export async function ensureSessionTitle(
151
+ input: EnsureSessionTitleParams,
152
+ ): Promise<SessionHistoryMetaV1> {
153
+ const current = await readSessionMetadata(input);
154
+ if (current.title) return current;
155
+
156
+ const firstUserText = resolveFirstUserText(input.messages);
157
+ const fallbackTitle = resolveFallbackSessionTitle(input.messages);
158
+ if (!fallbackTitle) return current;
159
+
160
+ const fallbackMeta: SessionHistoryMetaV1 = {
161
+ ...current,
162
+ title: fallbackTitle,
163
+ };
164
+ await writeSessionMetadata({
165
+ projectRoot: input.projectRoot,
166
+ agentId: input.agentId,
167
+ sessionId: input.sessionId,
168
+ meta: fallbackMeta,
169
+ });
170
+
171
+ if (input.generate !== true || !input.model || !firstUserText) {
172
+ return fallbackMeta;
173
+ }
174
+
175
+ const generatedTitle = await generateSessionTitle({
176
+ model: input.model,
177
+ firstUserText,
178
+ });
179
+ if (!generatedTitle) return fallbackMeta;
180
+
181
+ const generatedMeta: SessionHistoryMetaV1 = {
182
+ ...fallbackMeta,
183
+ title: generatedTitle,
184
+ };
185
+ await writeSessionMetadata({
186
+ projectRoot: input.projectRoot,
187
+ agentId: input.agentId,
188
+ sessionId: input.sessionId,
189
+ meta: generatedMeta,
190
+ });
191
+ return generatedMeta;
192
+ }
@@ -2,7 +2,8 @@
2
2
  * SDK Session 浏览辅助。
3
3
  *
4
4
  * 关键点(中文)
5
- * - 统一负责 session 列表摘要、session 详情与 history 分页的只读投影逻辑。
5
+ * - 统一负责 session 列表摘要、session 详情与 history 分页的投影逻辑。
6
+ * - 老 session 缺失持久化 title 时,会在列表读取阶段补写 fallback title。
6
7
  * - 面向 SDK / RemoteAgent / HTTP route 复用,避免在多个入口重复拼列表与分页语义。
7
8
  * - 这里不持有运行态状态;执行状态等动态信息通过调用参数显式注入。
8
9
  */
@@ -33,6 +34,10 @@ import { pickLastSuccessfulChatSendText } from "@/executor/messages/UserVisibleT
33
34
  import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
34
35
  import { getSdkAgentSessionsRootDirPath } from "@/session/storage/Paths.js";
35
36
  import { readSessionMetadata } from "@/session/storage/Metadata.js";
37
+ import {
38
+ ensureSessionTitle,
39
+ resolveFallbackSessionTitle,
40
+ } from "@/session/SessionTitle.js";
36
41
 
37
42
  type AnyUiPart = UIMessagePart<Record<string, never>, Record<string, never>>;
38
43
 
@@ -184,14 +189,7 @@ export function resolveSessionMessagePreview(message: SessionMessageV1): string
184
189
  * 推导当前 session 的可读标题。
185
190
  */
186
191
  export function resolveSessionTitle(messages: SessionMessageV1[]): string | undefined {
187
- for (const message of messages) {
188
- if (message.role !== "user") continue;
189
- const preview = resolveSessionMessagePreview(message);
190
- if (!preview) continue;
191
- return truncateText(preview, 80);
192
- }
193
- const fallback = messages[0] ? resolveSessionMessagePreview(messages[0]) : "";
194
- return fallback ? truncateText(fallback, 80) : undefined;
192
+ return resolveFallbackSessionTitle(messages);
195
193
  }
196
194
 
197
195
  function resolveToolName(part: ToolPartCompatShape, aiToolName?: string): string {
@@ -361,7 +359,10 @@ export function buildSessionInfo(
361
359
  180,
362
360
  )
363
361
  : undefined;
364
- const title = resolveSessionTitle(input.messages);
362
+ const title =
363
+ typeof input.metadata.title === "string" && input.metadata.title.trim()
364
+ ? input.metadata.title.trim()
365
+ : undefined;
365
366
  return {
366
367
  agentId: input.agentId,
367
368
  sessionId: input.sessionId,
@@ -374,8 +375,8 @@ export function buildSessionInfo(
374
375
  ...(typeof input.metadata.updatedAt === "number"
375
376
  ? { updatedAt: input.metadata.updatedAt }
376
377
  : {}),
377
- ...(input.metadata.sdkConfig?.modelLabel
378
- ? { modelLabel: input.metadata.sdkConfig.modelLabel }
378
+ ...(input.metadata.modelLabel
379
+ ? { modelLabel: input.metadata.modelLabel }
379
380
  : {}),
380
381
  ...(typeof input.metadata.timezone === "string" && input.metadata.timezone.trim()
381
382
  ? { timezone: input.metadata.timezone.trim() }
@@ -470,11 +471,19 @@ export async function listAgentSessionSummaryPage(params: {
470
471
  const messages = await loadSessionMessagesFromPath(
471
472
  getSdkAgentSessionMessagesPath(params.projectRoot, params.agentId, sessionId),
472
473
  );
474
+ const metadataWithTitle = metadata.title
475
+ ? metadata
476
+ : await ensureSessionTitle({
477
+ projectRoot: params.projectRoot,
478
+ agentId: params.agentId,
479
+ sessionId,
480
+ messages,
481
+ });
473
482
  const info = buildSessionInfo({
474
483
  projectRoot: params.projectRoot,
475
484
  agentId: params.agentId,
476
485
  sessionId,
477
- metadata,
486
+ metadata: metadataWithTitle,
478
487
  messages,
479
488
  executing: params.executingSessionIds?.has(sessionId),
480
489
  });
@@ -21,11 +21,16 @@ export {
21
21
  } from "./storage/Paths.js";
22
22
  export {
23
23
  inferModelLabel,
24
+ normalizeSessionTitle,
24
25
  patchSessionModelLabel,
25
26
  readSessionMetadata,
26
27
  resolveSystemTimezone,
27
28
  writeSessionMetadata,
28
29
  } from "./storage/Metadata.js";
30
+ export {
31
+ ensureSessionTitle,
32
+ resolveFallbackSessionTitle,
33
+ } from "./SessionTitle.js";
29
34
  export {
30
35
  persistSdkAssistantResult,
31
36
  touchSessionMetadata,
@@ -8,10 +8,7 @@
8
8
 
9
9
  import fs from "fs-extra";
10
10
  import type { LanguageModel } from "ai";
11
- import type {
12
- SessionHistoryMetaV1,
13
- SessionHistorySdkConfigV1,
14
- } from "@/executor/types/SessionHistoryMeta.js";
11
+ import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
15
12
  import { getSdkAgentSessionMetaPath } from "@/session/storage/Paths.js";
16
13
 
17
14
  type ReadSessionMetadataInput = {
@@ -36,6 +33,14 @@ function normalizeModelLabel(input: unknown): string | undefined {
36
33
  return label || undefined;
37
34
  }
38
35
 
36
+ /**
37
+ * 归一化 session 标题。
38
+ */
39
+ export function normalizeSessionTitle(input: unknown): string | undefined {
40
+ const title = typeof input === "string" ? input.trim() : "";
41
+ return title || undefined;
42
+ }
43
+
39
44
  /**
40
45
  * 读取当前系统时区。
41
46
  */
@@ -103,27 +108,11 @@ export async function readSessionMetadata(
103
108
  typeof raw.updatedAt === "number" && Number.isFinite(raw.updatedAt)
104
109
  ? raw.updatedAt
105
110
  : 0,
106
- pinnedSkillIds: Array.isArray(raw.pinnedSkillIds)
107
- ? raw.pinnedSkillIds
108
- .map((item) => (typeof item === "string" ? item.trim() : ""))
109
- .filter(Boolean)
110
- : [],
111
- ...(typeof raw.lastArchiveId === "string" && raw.lastArchiveId.trim()
112
- ? { lastArchiveId: raw.lastArchiveId.trim() }
113
- : {}),
114
- ...(typeof raw.keepLastMessages === "number" &&
115
- Number.isFinite(raw.keepLastMessages)
116
- ? { keepLastMessages: raw.keepLastMessages }
117
- : {}),
118
- ...(typeof raw.maxInputTokensApprox === "number" &&
119
- Number.isFinite(raw.maxInputTokensApprox)
120
- ? { maxInputTokensApprox: raw.maxInputTokensApprox }
121
- : {}),
122
- ...(typeof raw.compactRatio === "number" && Number.isFinite(raw.compactRatio)
123
- ? { compactRatio: raw.compactRatio }
111
+ ...(normalizeSessionTitle(raw.title)
112
+ ? { title: normalizeSessionTitle(raw.title) }
124
113
  : {}),
125
- ...(raw.sdkConfig && typeof raw.sdkConfig === "object"
126
- ? { sdkConfig: raw.sdkConfig as SessionHistorySdkConfigV1 }
114
+ ...(normalizeModelLabel(raw.modelLabel)
115
+ ? { modelLabel: normalizeModelLabel(raw.modelLabel) }
127
116
  : {}),
128
117
  };
129
118
  } catch {
@@ -134,7 +123,6 @@ export async function readSessionMetadata(
134
123
  createdAt: Date.now(),
135
124
  timezone: resolveSystemTimezone(),
136
125
  updatedAt: 0,
137
- pinnedSkillIds: [],
138
126
  };
139
127
  }
140
128
  }
@@ -181,10 +169,7 @@ export async function patchSessionModelLabel(
181
169
  updatedAt: Date.now(),
182
170
  ...(modelLabel
183
171
  ? {
184
- sdkConfig: {
185
- ...(current.sdkConfig || {}),
186
- modelLabel,
187
- },
172
+ modelLabel,
188
173
  }
189
174
  : {}),
190
175
  };
@@ -83,10 +83,7 @@ export async function touchSessionMetadata(
83
83
  updatedAt: Date.now(),
84
84
  ...(params.sessionConfig.modelLabel
85
85
  ? {
86
- sdkConfig: {
87
- ...(current.sdkConfig || {}),
88
- modelLabel: params.sessionConfig.modelLabel,
89
- },
86
+ modelLabel: params.sessionConfig.modelLabel,
90
87
  }
91
88
  : {}),
92
89
  };
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { LanguageModel, Tool } from "ai";
11
11
  import type { BasePlugin } from "@/plugin/core/BasePlugin.js";
12
+ import type { AgentModel } from "@/model/CityModelAdapter.js";
12
13
  import type { RpcServerInstance } from "@/rpc/Server.js";
13
14
  import type { SessionMessageV1 } from "@/executor/types/SessionMessages.js";
14
15
  import type {
@@ -18,6 +19,8 @@ import type {
18
19
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
19
20
  import type { AgentSessionTurnHandle } from "@/types/sdk/AgentSessionTurn.js";
20
21
 
22
+ export type { AgentModel } from "@/model/CityModelAdapter.js";
23
+
21
24
  /**
22
25
  * 本地 Agent 构造参数。
23
26
  */
@@ -59,10 +62,11 @@ export interface AgentOptions {
59
62
  * 当前 agent 为新建 session 提供的默认模型实例。
60
63
  *
61
64
  * 关键点(中文)
62
- * - SDK 仍不负责“选择哪个模型”,这里只接收宿主已经创建好的 `LanguageModel`。
65
+ * - SDK 仍不负责“选择哪个模型”,这里只接收宿主已经创建好的模型实例。
66
+ * - 支持 AI SDK `LanguageModel`,也支持 City City 返回的 `CityModel`。
63
67
  * - 该模型会作为 session 首次执行前的默认注入值。
64
68
  */
65
- model?: LanguageModel;
69
+ model?: AgentModel;
66
70
 
67
71
  /**
68
72
  * 当前 agent 显式持有的插件实例集合。
@@ -252,7 +256,7 @@ export interface AgentSessionSetInput {
252
256
  * - 这里接受运行中的模型实例,而不是模型 ID。
253
257
  * - 由于模型实例通常不可序列化,落盘只保留轻量可读标签用于展示。
254
258
  */
255
- model?: LanguageModel;
259
+ model?: AgentModel;
256
260
  }
257
261
 
258
262
  /**
@@ -441,8 +445,9 @@ export interface AgentSessionSummary {
441
445
  * 当前 session 可读标题。
442
446
  *
443
447
  * 说明(中文)
444
- * - 当前 SDK 不要求标题一定存在。
445
- * - 若调用方需要展示列表标题,可优先使用这里;为空时再回退到 `sessionId`。
448
+ * - 标题持久化在 session `meta.json` 顶层。
449
+ * - 首条用户消息出现后,SDK 会优先生成标题,失败时回退到首条用户消息截断。
450
+ * - 空 session 可能暂时没有标题,调用方可回退到 `sessionId`。
446
451
  */
447
452
  title?: string;
448
453
 
@@ -36,7 +36,7 @@ export interface AgentProjectInitializationInput {
36
36
  * 项目执行绑定配置。
37
37
  *
38
38
  * 说明(中文)
39
- * - 绑定平台全局模型池中的模型 ID。
39
+ * - 绑定 City AIService 暴露的模型 ID。
40
40
  */
41
41
  execution: ExecutionBindingConfig;
42
42
 
@@ -159,7 +159,7 @@ export interface DowncityConfig {
159
159
  id: string;
160
160
  version: string;
161
161
  /**
162
- * Runtime startup configuration used by `downcity agent start`.
162
+ * Runtime startup configuration used by `town agent start`.
163
163
  * CLI flags (if provided) take precedence over this config.
164
164
  */
165
165
  start?: {
@@ -181,7 +181,7 @@ export interface DowncityConfig {
181
181
  *
182
182
  * 关键点(中文)
183
183
  * - 项目只有一种执行模式:`api`。
184
- * - 绑定平台全局模型池中的模型 ID。
184
+ * - 绑定 City AIService 暴露的模型 ID。
185
185
  */
186
186
  execution?: ExecutionBindingConfig;
187
187
  /**
@@ -197,7 +197,7 @@ export interface DowncityConfig {
197
197
  *
198
198
  * 关键点(中文)
199
199
  * - `@downcity/agent` 本地 SDK 不直接消费该字段。
200
- * - 宿主侧(例如 `@downcity/city`)可读取该字段控制模型工厂行为,例如 `llm.logMessages`。
200
+ * - 宿主侧(例如 `downcity`)可读取该字段控制模型工厂行为,例如 `llm.logMessages`。
201
201
  * - 对于项目内 `downcity.json`,通常不需要显式写 provider/model 明细。
202
202
  */
203
203
  llm?: LlmConfig;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 项目运行入口只有一种执行模式:`api`。
6
- * - 绑定平台全局模型池中的模型 ID。
6
+ * - 绑定 City AIService 暴露的模型 ID。
7
7
  * - 该类型是项目 `downcity.json` 中唯一的执行配置。
8
8
  */
9
9
 
@@ -17,11 +17,11 @@ export interface ExecutionBindingConfig {
17
17
  type: "api";
18
18
 
19
19
  /**
20
- * 平台全局模型池中的模型 ID。
20
+ * City AIService 中的模型 ID。
21
21
  *
22
22
  * 说明(中文)
23
- * - 必须能在 `~/.downcity/downcity.db` 的模型池中解析到。
24
- * - 例如:`default`、`fast`、`quality`。
23
+ * - 必须能通过 City 的 `/v1/ai/models` 目录查询到。
24
+ * - 例如:`deepseek-v4-flash`、`fast`、`quality`。
25
25
  */
26
26
  modelId: string;
27
27
  }
@@ -44,8 +44,8 @@ export const AUTH_PERMISSION_DESCRIPTIONS: Record<AuthPermissionKey, string> = {
44
44
  "agent.execute": "触发 agent 执行与会话运行。",
45
45
  "task.read": "查看 task 定义与运行结果。",
46
46
  "task.run": "创建或手动执行 task。",
47
- "model.read": "查看模型池与模型绑定。",
48
- "model.write": "修改模型池与模型绑定。",
47
+ "model.read": "查看 City AIService 模型与模型绑定。",
48
+ "model.write": "修改模型绑定。",
49
49
  "env.read": "查看环境变量配置。",
50
50
  "env.write": "修改环境变量配置。",
51
51
  "channel.read": "查看渠道账号与渠道状态。",