@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.
- package/README.md +5 -4
- package/bin/agent/Agent.d.ts.map +1 -1
- package/bin/agent/Agent.js.map +1 -1
- package/bin/config/AgentInitializer.d.ts +1 -1
- package/bin/config/AgentInitializer.js +1 -1
- package/bin/config/DowncitySchema.js +1 -1
- package/bin/config/DowncitySchema.js.map +1 -1
- package/bin/config/ExecutionBinding.d.ts +1 -1
- package/bin/config/ExecutionBinding.js +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +0 -4
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
- package/bin/executor/composer/system/default/InitPrompts.d.ts +1 -1
- package/bin/executor/composer/system/default/InitPrompts.js +1 -1
- package/bin/executor/composer/system/default/SystemDomain.js +1 -1
- package/bin/executor/composer/system/default/assets/core.prompt.d.ts +1 -1
- package/bin/executor/composer/system/default/assets/core.prompt.js +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.js +1 -1
- package/bin/executor/composer/system/default/assets/task.prompt.d.ts +1 -1
- package/bin/executor/composer/system/default/assets/task.prompt.js +1 -1
- package/bin/executor/messages/ChatMessageMarkupTypes.d.ts +1 -1
- package/bin/executor/messages/ChatMessageMarkupTypes.js +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +1 -2
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +17 -56
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/tools/shell/ShellToolBridge.js +1 -1
- package/bin/executor/tools/shell/ShellToolBridge.js.map +1 -1
- package/bin/executor/tools/shell/ShellToolDefinition.d.ts +2 -2
- package/bin/executor/tools/shell/ShellToolFormatting.d.ts +1 -1
- package/bin/executor/tools/shell/ShellToolFormatting.js +7 -7
- package/bin/executor/tools/shell/ShellToolFormatting.js.map +1 -1
- package/bin/executor/tools/shell/ShellToolSchemas.d.ts +7 -75
- package/bin/executor/tools/shell/ShellToolSchemas.d.ts.map +1 -1
- package/bin/executor/types/SessionHistoryMeta.d.ts +5 -24
- package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
- package/bin/executor/types/SessionHistoryMeta.js +1 -1
- package/bin/index.d.ts +3 -2
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -0
- package/bin/index.js.map +1 -1
- package/bin/model/CityModelAdapter.d.ts +23 -0
- package/bin/model/CityModelAdapter.d.ts.map +1 -0
- package/bin/model/CityModelAdapter.js +338 -0
- package/bin/model/CityModelAdapter.js.map +1 -0
- package/bin/runtime/host/daemon/Paths.d.ts +1 -1
- package/bin/runtime/host/daemon/Paths.js +1 -1
- package/bin/runtime/host/daemon/ProjectSetup.js +2 -2
- package/bin/session/Session.d.ts +1 -0
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +32 -6
- package/bin/session/Session.js.map +1 -1
- package/bin/session/SessionTitle.d.ts +49 -0
- package/bin/session/SessionTitle.d.ts.map +1 -0
- package/bin/session/SessionTitle.js +130 -0
- package/bin/session/SessionTitle.js.map +1 -0
- package/bin/session/browse/Browse.d.ts +2 -1
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +18 -15
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/index.d.ts +2 -1
- package/bin/session/index.d.ts.map +1 -1
- package/bin/session/index.js +2 -1
- package/bin/session/index.js.map +1 -1
- package/bin/session/storage/Metadata.d.ts +4 -0
- package/bin/session/storage/Metadata.d.ts.map +1 -1
- package/bin/session/storage/Metadata.js +12 -25
- package/bin/session/storage/Metadata.js.map +1 -1
- package/bin/session/storage/Persistence.d.ts.map +1 -1
- package/bin/session/storage/Persistence.js +1 -4
- package/bin/session/storage/Persistence.js.map +1 -1
- package/bin/types/agent/AgentTypes.d.ts +9 -5
- package/bin/types/agent/AgentTypes.d.ts.map +1 -1
- package/bin/types/config/AgentProject.d.ts +1 -1
- package/bin/types/config/DowncityConfig.d.ts +3 -3
- package/bin/types/config/ExecutionBinding.d.ts +4 -4
- package/bin/types/config/ExecutionBinding.js +1 -1
- package/bin/types/runtime/auth/AuthPermission.js +2 -2
- package/bin/types/runtime/auth/AuthPermission.js.map +1 -1
- package/bin/types/runtime/host/Store.d.ts +3 -177
- package/bin/types/runtime/host/Store.d.ts.map +1 -1
- package/bin/types/runtime/host/Store.js +7 -0
- package/bin/types/runtime/host/Store.js.map +1 -1
- package/bin/types/runtime/http/InlineInstant.d.ts +1 -1
- package/bin/types/runtime/platform/Platform.d.ts +1 -1
- package/package.json +19 -18
- package/src/agent/Agent.ts +3 -2
- package/src/config/AgentInitializer.ts +1 -1
- package/src/config/DowncitySchema.ts +1 -1
- package/src/config/ExecutionBinding.ts +1 -1
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +0 -4
- package/src/executor/composer/system/default/InitPrompts.ts +1 -1
- package/src/executor/composer/system/default/SystemDomain.ts +1 -1
- package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
- package/src/executor/composer/system/default/assets/core.prompt.ts.txt +1 -1
- package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
- package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +18 -18
- package/src/executor/composer/system/default/assets/task.prompt.ts +1 -1
- package/src/executor/composer/system/default/assets/task.prompt.ts.txt +1 -1
- package/src/executor/messages/ChatMessageMarkupTypes.ts +1 -1
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +17 -57
- package/src/executor/tools/shell/ShellToolBridge.ts +1 -1
- package/src/executor/tools/shell/ShellToolFormatting.ts +7 -7
- package/src/executor/types/SessionHistoryMeta.ts +5 -25
- package/src/index.ts +5 -5
- package/src/model/CityModelAdapter.ts +384 -0
- package/src/runtime/host/daemon/Paths.ts +1 -1
- package/src/runtime/host/daemon/ProjectSetup.ts +2 -2
- package/src/session/Session.ts +40 -6
- package/src/session/SessionTitle.ts +192 -0
- package/src/session/browse/Browse.ts +22 -13
- package/src/session/index.ts +5 -0
- package/src/session/storage/Metadata.ts +14 -29
- package/src/session/storage/Persistence.ts +1 -4
- package/src/types/agent/AgentTypes.ts +10 -5
- package/src/types/config/AgentProject.ts +1 -1
- package/src/types/config/DowncityConfig.ts +3 -3
- package/src/types/config/ExecutionBinding.ts +4 -4
- package/src/types/runtime/auth/AuthPermission.ts +2 -2
- package/src/types/runtime/host/Store.ts +3 -182
- package/src/types/runtime/http/InlineInstant.ts +1 -1
- package/src/types/runtime/platform/Platform.ts +1 -1
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -194,29 +194,9 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
|
|
|
194
194
|
await fs.ensureFile(this.getMessagesFilePath());
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
private
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const raw of input) {
|
|
201
|
-
const id = typeof raw === "string" ? raw.trim() : "";
|
|
202
|
-
if (!id) continue;
|
|
203
|
-
out.push(id);
|
|
204
|
-
}
|
|
205
|
-
return Array.from(new Set(out)).slice(0, 2000);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private normalizeSdkConfig(
|
|
209
|
-
input: SessionHistoryMetaV1["sdkConfig"],
|
|
210
|
-
): SessionHistoryMetaV1["sdkConfig"] {
|
|
211
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
const modelLabel =
|
|
215
|
-
typeof input.modelLabel === "string" ? input.modelLabel.trim() : "";
|
|
216
|
-
if (!modelLabel) return undefined;
|
|
217
|
-
return {
|
|
218
|
-
modelLabel,
|
|
219
|
-
};
|
|
197
|
+
private normalizeText(input: unknown): string | undefined {
|
|
198
|
+
const value = typeof input === "string" ? input.trim() : "";
|
|
199
|
+
return value || undefined;
|
|
220
200
|
}
|
|
221
201
|
|
|
222
202
|
private async readMetaUnsafe(): Promise<SessionHistoryMetaV1> {
|
|
@@ -235,24 +215,15 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
|
|
|
235
215
|
...(typeof raw.createdAt === "number" && Number.isFinite(raw.createdAt)
|
|
236
216
|
? { createdAt: raw.createdAt }
|
|
237
217
|
: {}),
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
...(typeof raw.lastArchiveId === "string" && raw.lastArchiveId.trim()
|
|
241
|
-
? { lastArchiveId: raw.lastArchiveId.trim() }
|
|
242
|
-
: {}),
|
|
243
|
-
...(typeof raw.keepLastMessages === "number" &&
|
|
244
|
-
Number.isFinite(raw.keepLastMessages)
|
|
245
|
-
? { keepLastMessages: raw.keepLastMessages }
|
|
246
|
-
: {}),
|
|
247
|
-
...(typeof raw.maxInputTokensApprox === "number" &&
|
|
248
|
-
Number.isFinite(raw.maxInputTokensApprox)
|
|
249
|
-
? { maxInputTokensApprox: raw.maxInputTokensApprox }
|
|
218
|
+
...(this.normalizeText(raw.timezone)
|
|
219
|
+
? { timezone: this.normalizeText(raw.timezone) }
|
|
250
220
|
: {}),
|
|
251
|
-
|
|
252
|
-
|
|
221
|
+
updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : 0,
|
|
222
|
+
...(this.normalizeText(raw.title)
|
|
223
|
+
? { title: this.normalizeText(raw.title) }
|
|
253
224
|
: {}),
|
|
254
|
-
...(this.
|
|
255
|
-
? {
|
|
225
|
+
...(this.normalizeText(raw.modelLabel)
|
|
226
|
+
? { modelLabel: this.normalizeText(raw.modelLabel) }
|
|
256
227
|
: {}),
|
|
257
228
|
};
|
|
258
229
|
} catch {
|
|
@@ -261,7 +232,6 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
|
|
|
261
232
|
sessionId: this.sessionId,
|
|
262
233
|
createdAt: Date.now(),
|
|
263
234
|
updatedAt: 0,
|
|
264
|
-
pinnedSkillIds: [],
|
|
265
235
|
};
|
|
266
236
|
}
|
|
267
237
|
}
|
|
@@ -276,26 +246,16 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
|
|
|
276
246
|
...(typeof next.createdAt === "number" && Number.isFinite(next.createdAt)
|
|
277
247
|
? { createdAt: next.createdAt }
|
|
278
248
|
: {}),
|
|
249
|
+
...(this.normalizeText(next.timezone)
|
|
250
|
+
? { timezone: this.normalizeText(next.timezone) }
|
|
251
|
+
: {}),
|
|
279
252
|
updatedAt:
|
|
280
253
|
typeof next.updatedAt === "number" ? next.updatedAt : Date.now(),
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
? { lastArchiveId: next.lastArchiveId.trim() }
|
|
284
|
-
: {}),
|
|
285
|
-
...(typeof next.keepLastMessages === "number" &&
|
|
286
|
-
Number.isFinite(next.keepLastMessages)
|
|
287
|
-
? { keepLastMessages: next.keepLastMessages }
|
|
288
|
-
: {}),
|
|
289
|
-
...(typeof next.maxInputTokensApprox === "number" &&
|
|
290
|
-
Number.isFinite(next.maxInputTokensApprox)
|
|
291
|
-
? { maxInputTokensApprox: next.maxInputTokensApprox }
|
|
292
|
-
: {}),
|
|
293
|
-
...(typeof next.compactRatio === "number" &&
|
|
294
|
-
Number.isFinite(next.compactRatio)
|
|
295
|
-
? { compactRatio: next.compactRatio }
|
|
254
|
+
...(this.normalizeText(next.title)
|
|
255
|
+
? { title: this.normalizeText(next.title) }
|
|
296
256
|
: {}),
|
|
297
|
-
...(this.
|
|
298
|
-
? {
|
|
257
|
+
...(this.normalizeText(next.modelLabel)
|
|
258
|
+
? { modelLabel: this.normalizeText(next.modelLabel) }
|
|
299
259
|
: {}),
|
|
300
260
|
};
|
|
301
261
|
await fs.writeJson(this.getMetaFilePath(), normalized, { spaces: 2 });
|
|
@@ -75,7 +75,7 @@ function tryParseJsonObject(raw: string): JsonObject | null {
|
|
|
75
75
|
function shouldEnableCommandBridge(command: string): boolean {
|
|
76
76
|
const raw = String(command || "").trim();
|
|
77
77
|
if (!raw) return false;
|
|
78
|
-
return /(?:^|\s)(?:city|
|
|
78
|
+
return /(?:^|\s)(?:city|town)(?:\s|$)/i.test(raw);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
async function injectUserTextMessage(params: {
|
|
@@ -33,7 +33,7 @@ function applyEnvMap(
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* 对 `
|
|
36
|
+
* 对 `town chat send` 命令做前置安全校验。
|
|
37
37
|
*
|
|
38
38
|
* 关键点(中文)
|
|
39
39
|
* - 历史上模型会把长文本直接拼进多行 shell 命令,导致后续行被 zsh 当作独立命令解析。
|
|
@@ -42,20 +42,20 @@ function applyEnvMap(
|
|
|
42
42
|
*/
|
|
43
43
|
export function validateChatSendCommand(cmd: string): string | null {
|
|
44
44
|
const source = String(cmd ?? "");
|
|
45
|
-
if (!/\
|
|
45
|
+
if (!/\bbay\s+chat\s+send\b/.test(source)) return null;
|
|
46
46
|
if (!/[\r\n]/.test(source)) return null;
|
|
47
|
-
if (/\
|
|
47
|
+
if (/\bbay\s+chat\s+send\b[\s\S]*\s--stdin(?:\s|$)/.test(source)) {
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
|
-
if (/\
|
|
50
|
+
if (/\bbay\s+chat\s+send\b[\s\S]*\s--text(?:\s|$)/.test(source)) {
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
|
-
if (/\
|
|
53
|
+
if (/\bbay\s+chat\s+send\b[\s\S]*\s--text-file(?:\s|$)/.test(source)) {
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
return [
|
|
57
|
-
"Unsafe `
|
|
58
|
-
"If your message is multi-line, use `
|
|
57
|
+
"Unsafe `town chat send` command: real newlines are not allowed.",
|
|
58
|
+
"If your message is multi-line, use `town chat send --stdin` (with heredoc/pipe), `--text-file`, or explicit `--text`.",
|
|
59
59
|
].join(" ");
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -3,21 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 存储位置:`.downcity/agents/<encodedAgentId>/sessions/<encodedSessionId>/messages/meta.json`
|
|
6
|
-
* - 用于保存
|
|
6
|
+
* - 用于保存 session 列表、详情和索引所需的轻量元信息
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* SDK Session 配置摘要。
|
|
11
|
-
*
|
|
12
|
-
* 关键点(中文)
|
|
13
|
-
* - 这里只存可稳定序列化、仅用于回显/索引的轻量配置摘要。
|
|
14
|
-
* - 例如本地注入的 `LanguageModel` 实例本身不可序列化,因此仅记录其可读标签。
|
|
15
|
-
*/
|
|
16
|
-
export type SessionHistorySdkConfigV1 = {
|
|
17
|
-
/** 当前 session 绑定模型的可读标签。 */
|
|
18
|
-
modelLabel?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
9
|
export type SessionHistoryMetaV1 = {
|
|
22
10
|
/** schema 版本。 */
|
|
23
11
|
v: 1;
|
|
@@ -31,16 +19,8 @@ export type SessionHistoryMetaV1 = {
|
|
|
31
19
|
timezone?: string;
|
|
32
20
|
/** 最近一次更新元信息的时间戳(ms)。 */
|
|
33
21
|
updatedAt: number;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
/** 最近一次 compact 生效的保留消息数。 */
|
|
39
|
-
keepLastMessages?: number;
|
|
40
|
-
/** 最近一次 compact 生效的输入 token 近似上限。 */
|
|
41
|
-
maxInputTokensApprox?: number;
|
|
42
|
-
/** 最近一次 compact 生效的前段压缩比例。 */
|
|
43
|
-
compactRatio?: number;
|
|
44
|
-
/** SDK 侧的轻量配置摘要。 */
|
|
45
|
-
sdkConfig?: SessionHistorySdkConfigV1;
|
|
22
|
+
/** 当前 session 持久化标题。 */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** 当前 session 绑定模型的可读标签。 */
|
|
25
|
+
modelLabel?: string;
|
|
46
26
|
};
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
// Agent 入口
|
|
11
11
|
export { Agent } from "./agent/Agent.js";
|
|
12
12
|
export { RemoteAgent } from "./agent/RemoteAgent.js";
|
|
13
|
+
export {
|
|
14
|
+
inferAgentModelLabel,
|
|
15
|
+
normalizeAgentModel,
|
|
16
|
+
} from "./model/CityModelAdapter.js";
|
|
13
17
|
export type {
|
|
14
18
|
AgentSessionCollection,
|
|
19
|
+
AgentModel,
|
|
15
20
|
AgentSessionActor,
|
|
16
21
|
AgentSession,
|
|
17
22
|
AgentCreateSessionInput,
|
|
@@ -281,14 +286,9 @@ export type {
|
|
|
281
286
|
StoredChannelAccountChannel,
|
|
282
287
|
StoredEnvEntry,
|
|
283
288
|
StoredGlobalEnvEntry,
|
|
284
|
-
StoredModel,
|
|
285
|
-
StoredModelProvider,
|
|
286
|
-
StoredProviderMeta,
|
|
287
289
|
UpsertChannelAccountInput,
|
|
288
290
|
UpsertEnvEntryInput,
|
|
289
291
|
UpsertGlobalEnvEntryInput,
|
|
290
|
-
UpsertModelInput,
|
|
291
|
-
UpsertModelProviderInput,
|
|
292
292
|
} from "./types/runtime/host/Store.js";
|
|
293
293
|
|
|
294
294
|
// HTTP auth 协议类型
|
|
@@ -0,0 +1,384 @@
|
|
|
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
|
+
|
|
38
|
+
function normalizeFinishReason(input: unknown): {
|
|
39
|
+
unified: "stop" | "length" | "content-filter" | "tool-calls" | "error" | "other";
|
|
40
|
+
raw: string | undefined;
|
|
41
|
+
} {
|
|
42
|
+
const text = typeof input === "string" ? input : "";
|
|
43
|
+
if (text === "stop" || text === "length" || text === "content-filter" || text === "tool-calls" || text === "error") {
|
|
44
|
+
return { unified: text, raw: text };
|
|
45
|
+
}
|
|
46
|
+
return { unified: "stop", raw: text || "stop" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stringifyToolInput(input: unknown): string {
|
|
50
|
+
if (typeof input === "string") return input;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.stringify(input ?? {});
|
|
53
|
+
} catch {
|
|
54
|
+
return "{}";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function textFromProviderContent(content: unknown): string {
|
|
59
|
+
if (typeof content === "string") return content;
|
|
60
|
+
if (!Array.isArray(content)) return "";
|
|
61
|
+
return content
|
|
62
|
+
.filter((part) => part && typeof part === "object" && (part as { type?: unknown }).type === "text")
|
|
63
|
+
.map((part) => String((part as { text?: unknown }).text ?? ""))
|
|
64
|
+
.join("\n")
|
|
65
|
+
.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fileUrlFromProviderPart(part: Record<string, unknown>): string {
|
|
69
|
+
const data = part.data;
|
|
70
|
+
if (data instanceof URL) return data.toString();
|
|
71
|
+
if (typeof data === "string") return data;
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function providerContentToUiParts(content: unknown): UIMessage["parts"] {
|
|
76
|
+
if (!Array.isArray(content)) {
|
|
77
|
+
return [{ type: "text", text: textFromProviderContent(content) }];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parts: UIMessage["parts"] = [];
|
|
81
|
+
for (const part of content) {
|
|
82
|
+
if (!part || typeof part !== "object") continue;
|
|
83
|
+
const record = part as Record<string, unknown>;
|
|
84
|
+
if (record.type === "text") {
|
|
85
|
+
parts.push({ type: "text", text: String(record.text ?? "") });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (record.type === "reasoning") {
|
|
89
|
+
parts.push({ type: "reasoning", text: String(record.text ?? "") });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (record.type === "file") {
|
|
93
|
+
const url = fileUrlFromProviderPart(record);
|
|
94
|
+
if (!url) continue;
|
|
95
|
+
parts.push({
|
|
96
|
+
type: "file",
|
|
97
|
+
mediaType: String(record.mediaType ?? "application/octet-stream"),
|
|
98
|
+
filename: typeof record.filename === "string" ? record.filename : undefined,
|
|
99
|
+
url,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (record.type === "tool-call") {
|
|
104
|
+
parts.push({
|
|
105
|
+
type: "dynamic-tool",
|
|
106
|
+
toolName: String(record.toolName ?? ""),
|
|
107
|
+
toolCallId: String(record.toolCallId ?? ""),
|
|
108
|
+
state: "input-available",
|
|
109
|
+
input: record.input,
|
|
110
|
+
providerExecuted: Boolean(record.providerExecuted),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function providerPromptToMessages(prompt: unknown): UIMessage[] {
|
|
118
|
+
if (!Array.isArray(prompt)) return [];
|
|
119
|
+
return prompt
|
|
120
|
+
.map((message, index): UIMessage | null => {
|
|
121
|
+
if (!message || typeof message !== "object") return null;
|
|
122
|
+
const item = message as ProviderPromptMessage;
|
|
123
|
+
const role = item.role === "system" || item.role === "assistant" ? item.role : "user";
|
|
124
|
+
const parts = providerContentToUiParts(item.content);
|
|
125
|
+
return {
|
|
126
|
+
id: `city-model-message-${String(index)}`,
|
|
127
|
+
role,
|
|
128
|
+
parts,
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
.filter((message): message is UIMessage => Boolean(message));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function providerOptionsToInput(options: Record<string, unknown>): CityModelInvokeInput {
|
|
135
|
+
return {
|
|
136
|
+
messages: providerPromptToMessages(options.prompt),
|
|
137
|
+
tools: options.tools,
|
|
138
|
+
toolChoice: options.toolChoice,
|
|
139
|
+
providerOptions: options.providerOptions,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function textFromUiMessage(message: UIMessage): string {
|
|
144
|
+
return message.parts
|
|
145
|
+
.filter((part) => part.type === "text")
|
|
146
|
+
.map((part) => String((part as { text?: unknown }).text ?? ""))
|
|
147
|
+
.join("\n")
|
|
148
|
+
.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function uiMessageToProviderContent(message: UIMessage): ProviderContentPart[] {
|
|
152
|
+
return message.parts.flatMap((part): ProviderContentPart[] => {
|
|
153
|
+
if (part.type === "text") {
|
|
154
|
+
return [{ type: "text", text: String((part as { text?: unknown }).text ?? "") }];
|
|
155
|
+
}
|
|
156
|
+
if (part.type === "reasoning") {
|
|
157
|
+
return [{ type: "reasoning", text: String((part as { text?: unknown }).text ?? "") }];
|
|
158
|
+
}
|
|
159
|
+
if (part.type === "dynamic-tool") {
|
|
160
|
+
const toolPart = part as {
|
|
161
|
+
toolCallId?: unknown;
|
|
162
|
+
toolName?: unknown;
|
|
163
|
+
input?: unknown;
|
|
164
|
+
providerExecuted?: unknown;
|
|
165
|
+
};
|
|
166
|
+
return [{
|
|
167
|
+
type: "tool-call",
|
|
168
|
+
toolCallId: String(toolPart.toolCallId ?? ""),
|
|
169
|
+
toolName: String(toolPart.toolName ?? ""),
|
|
170
|
+
input: stringifyToolInput(toolPart.input),
|
|
171
|
+
providerExecuted: Boolean(toolPart.providerExecuted),
|
|
172
|
+
}];
|
|
173
|
+
}
|
|
174
|
+
return [];
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function enqueueFinish(
|
|
179
|
+
controller: ProviderStreamController,
|
|
180
|
+
finishReason: unknown,
|
|
181
|
+
): void {
|
|
182
|
+
controller.enqueue({
|
|
183
|
+
type: "finish",
|
|
184
|
+
finishReason: normalizeFinishReason(finishReason),
|
|
185
|
+
usage: {
|
|
186
|
+
inputTokens: {
|
|
187
|
+
total: undefined,
|
|
188
|
+
noCache: undefined,
|
|
189
|
+
cacheRead: undefined,
|
|
190
|
+
cacheWrite: undefined,
|
|
191
|
+
},
|
|
192
|
+
outputTokens: {
|
|
193
|
+
total: undefined,
|
|
194
|
+
text: undefined,
|
|
195
|
+
reasoning: undefined,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function enqueueProviderParts(
|
|
202
|
+
controller: ProviderStreamController,
|
|
203
|
+
parts: Record<string, unknown>[],
|
|
204
|
+
state: {
|
|
205
|
+
/**
|
|
206
|
+
* 当前流是否已经发出 stream-start。
|
|
207
|
+
*/
|
|
208
|
+
sawStart: boolean;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 当前流是否已经发出 finish。
|
|
212
|
+
*/
|
|
213
|
+
sawFinish: boolean;
|
|
214
|
+
},
|
|
215
|
+
): void {
|
|
216
|
+
for (const part of parts) {
|
|
217
|
+
if (part.type !== "stream-start" && !state.sawStart) {
|
|
218
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
219
|
+
state.sawStart = true;
|
|
220
|
+
}
|
|
221
|
+
if (part.type === "stream-start") state.sawStart = true;
|
|
222
|
+
if (part.type === "finish") state.sawFinish = true;
|
|
223
|
+
controller.enqueue(part);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function mapUiChunkToProviderParts(chunk: UIMessageChunk): ProviderContentPart[] {
|
|
228
|
+
switch (chunk.type) {
|
|
229
|
+
case "start":
|
|
230
|
+
return [{ type: "stream-start", warnings: [] }];
|
|
231
|
+
case "text-start":
|
|
232
|
+
return [{ type: "text-start", id: chunk.id }];
|
|
233
|
+
case "text-delta":
|
|
234
|
+
return [{ type: "text-delta", id: chunk.id, delta: chunk.delta }];
|
|
235
|
+
case "text-end":
|
|
236
|
+
return [{ type: "text-end", id: chunk.id }];
|
|
237
|
+
case "reasoning-start":
|
|
238
|
+
return [{ type: "reasoning-start", id: chunk.id }];
|
|
239
|
+
case "reasoning-delta":
|
|
240
|
+
return [{ type: "reasoning-delta", id: chunk.id, delta: chunk.delta }];
|
|
241
|
+
case "reasoning-end":
|
|
242
|
+
return [{ type: "reasoning-end", id: chunk.id }];
|
|
243
|
+
case "tool-input-start":
|
|
244
|
+
return [{
|
|
245
|
+
type: "tool-input-start",
|
|
246
|
+
id: chunk.toolCallId,
|
|
247
|
+
toolName: chunk.toolName,
|
|
248
|
+
providerExecuted: chunk.providerExecuted,
|
|
249
|
+
dynamic: chunk.dynamic,
|
|
250
|
+
}];
|
|
251
|
+
case "tool-input-delta":
|
|
252
|
+
return [{
|
|
253
|
+
type: "tool-input-delta",
|
|
254
|
+
id: chunk.toolCallId,
|
|
255
|
+
delta: chunk.inputTextDelta,
|
|
256
|
+
}];
|
|
257
|
+
case "tool-input-available":
|
|
258
|
+
return [{
|
|
259
|
+
type: "tool-call",
|
|
260
|
+
toolCallId: chunk.toolCallId,
|
|
261
|
+
toolName: chunk.toolName,
|
|
262
|
+
input: stringifyToolInput(chunk.input),
|
|
263
|
+
providerExecuted: chunk.providerExecuted,
|
|
264
|
+
dynamic: chunk.dynamic,
|
|
265
|
+
}];
|
|
266
|
+
case "tool-output-available":
|
|
267
|
+
return [{
|
|
268
|
+
type: "tool-result",
|
|
269
|
+
toolCallId: chunk.toolCallId,
|
|
270
|
+
toolName: "",
|
|
271
|
+
output: { type: "json", value: chunk.output },
|
|
272
|
+
providerExecuted: chunk.providerExecuted,
|
|
273
|
+
dynamic: chunk.dynamic,
|
|
274
|
+
}];
|
|
275
|
+
case "error":
|
|
276
|
+
return [{ type: "error", error: new Error(chunk.errorText) }];
|
|
277
|
+
default:
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function cityModelToLanguageModel(model: CityModel): LanguageModel {
|
|
283
|
+
const invoker = model[CITY_MODEL_INVOKER];
|
|
284
|
+
const languageModel = {
|
|
285
|
+
specificationVersion: "v3",
|
|
286
|
+
provider: "downcity",
|
|
287
|
+
modelId: model.id,
|
|
288
|
+
supportedUrls: {},
|
|
289
|
+
async doGenerate(options) {
|
|
290
|
+
const message = await invoker.text(providerOptionsToInput(options as Record<string, unknown>));
|
|
291
|
+
return {
|
|
292
|
+
content: uiMessageToProviderContent(message),
|
|
293
|
+
finishReason: normalizeFinishReason("stop"),
|
|
294
|
+
usage: {
|
|
295
|
+
inputTokens: {
|
|
296
|
+
total: undefined,
|
|
297
|
+
noCache: undefined,
|
|
298
|
+
cacheRead: undefined,
|
|
299
|
+
cacheWrite: undefined,
|
|
300
|
+
},
|
|
301
|
+
outputTokens: {
|
|
302
|
+
total: undefined,
|
|
303
|
+
text: undefined,
|
|
304
|
+
reasoning: undefined,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
response: {
|
|
308
|
+
modelId: model.id,
|
|
309
|
+
},
|
|
310
|
+
warnings: [],
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
async doStream(options) {
|
|
314
|
+
const cityStream = await invoker.stream(providerOptionsToInput(options as Record<string, unknown>));
|
|
315
|
+
return {
|
|
316
|
+
stream: new ReadableStream({
|
|
317
|
+
async start(controller: ProviderStreamController) {
|
|
318
|
+
const reader = cityStream.getReader();
|
|
319
|
+
const state = {
|
|
320
|
+
sawStart: false,
|
|
321
|
+
sawFinish: false,
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
while (true) {
|
|
325
|
+
const { done, value } = await reader.read();
|
|
326
|
+
if (done) break;
|
|
327
|
+
const parts = mapUiChunkToProviderParts(value);
|
|
328
|
+
enqueueProviderParts(controller, parts, state);
|
|
329
|
+
}
|
|
330
|
+
if (!state.sawStart) controller.enqueue({ type: "stream-start", warnings: [] });
|
|
331
|
+
if (!state.sawFinish) enqueueFinish(controller, "stop");
|
|
332
|
+
controller.close();
|
|
333
|
+
} catch (error) {
|
|
334
|
+
controller.enqueue({ type: "error", error });
|
|
335
|
+
if (!state.sawFinish) enqueueFinish(controller, "error");
|
|
336
|
+
controller.close();
|
|
337
|
+
} finally {
|
|
338
|
+
reader.releaseLock();
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
response: {
|
|
343
|
+
modelId: model.id,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return languageModel as unknown as LanguageModel;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 将 Agent 可接受的模型输入归一为 AI SDK LanguageModel。
|
|
354
|
+
*/
|
|
355
|
+
export function normalizeAgentModel(model: AgentModel): LanguageModel {
|
|
356
|
+
if (isCityModel(model)) return cityModelToLanguageModel(model);
|
|
357
|
+
return model;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 从 Agent 模型输入推导展示标签。
|
|
362
|
+
*/
|
|
363
|
+
export function inferAgentModelLabel(model: AgentModel | undefined): string | undefined {
|
|
364
|
+
if (!model) return undefined;
|
|
365
|
+
if (isCityModel(model)) return model.name || model.id;
|
|
366
|
+
if (typeof model !== "object") return undefined;
|
|
367
|
+
const record = model as Record<string, unknown>;
|
|
368
|
+
const candidates = [
|
|
369
|
+
record.modelId,
|
|
370
|
+
record.model,
|
|
371
|
+
record.id,
|
|
372
|
+
record.name,
|
|
373
|
+
record.label,
|
|
374
|
+
];
|
|
375
|
+
for (const candidate of candidates) {
|
|
376
|
+
const label = typeof candidate === "string" ? candidate.trim() : "";
|
|
377
|
+
if (label) return label;
|
|
378
|
+
}
|
|
379
|
+
const constructorName =
|
|
380
|
+
model.constructor && typeof model.constructor.name === "string"
|
|
381
|
+
? model.constructor.name.trim()
|
|
382
|
+
: "";
|
|
383
|
+
return constructorName || "configured-model";
|
|
384
|
+
}
|
|
@@ -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 "
|
|
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 "
|
|
40
|
+
'❌ downcity.json does not exist. Please run "town agent create" first',
|
|
41
41
|
);
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|