@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.
- 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 +454 -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/scripts/city-model-tool-loop.test.mjs +181 -0
- 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 +553 -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
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
package/src/session/Session.ts
CHANGED
|
@@ -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.
|
|
261
|
-
? { modelLabel: metadata.
|
|
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 =
|
|
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
|
}
|