@downcity/agent 1.1.202 → 1.1.204
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/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +2 -0
- package/bin/session/Session.js.map +1 -1
- package/bin/session/SessionTitle.d.ts +9 -0
- package/bin/session/SessionTitle.d.ts.map +1 -1
- package/bin/session/SessionTitle.js +68 -3
- package/bin/session/SessionTitle.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +6 -0
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +15 -3
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts +6 -0
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +3 -0
- package/bin/session/services/SessionViewService.js.map +1 -1
- package/package.json +1 -1
- package/scripts/session-action-message.test.mjs +46 -2
- package/scripts/session-title-event.test.mjs +66 -0
- package/src/session/Session.ts +2 -0
- package/src/session/SessionTitle.ts +135 -3
- package/src/session/services/SessionStateService.ts +23 -3
- package/src/session/services/SessionViewService.ts +9 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -45,6 +45,27 @@ function create_mock_title_model(title_text) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function create_failing_title_model() {
|
|
49
|
+
return new MockLanguageModelV3({
|
|
50
|
+
modelId: "mock-session-title-failing-model",
|
|
51
|
+
doGenerate: async () => {
|
|
52
|
+
throw new Error("mock title generation failed");
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function read_log_lines(agent_path) {
|
|
58
|
+
const logs_path = path.join(agent_path, ".downcity", "logs");
|
|
59
|
+
const entries = await fs.readdir(logs_path);
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
63
|
+
const content = await fs.readFile(path.join(logs_path, entry), "utf8");
|
|
64
|
+
lines.push(...content.split("\n").filter(Boolean));
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
test("Session keeps title empty when no model is available", async () => {
|
|
49
70
|
const agent_path = await fs.mkdtemp(
|
|
50
71
|
path.join(os.tmpdir(), "downcity-agent-session-title-"),
|
|
@@ -75,6 +96,51 @@ test("Session keeps title empty when no model is available", async () => {
|
|
|
75
96
|
}
|
|
76
97
|
});
|
|
77
98
|
|
|
99
|
+
test("Session logs title generation failure without blocking the session", async () => {
|
|
100
|
+
const agent_path = await fs.mkdtemp(
|
|
101
|
+
path.join(os.tmpdir(), "downcity-agent-session-title-log-"),
|
|
102
|
+
);
|
|
103
|
+
const agent = new Agent({
|
|
104
|
+
id: "title_log_agent",
|
|
105
|
+
path: agent_path,
|
|
106
|
+
model: create_failing_title_model(),
|
|
107
|
+
});
|
|
108
|
+
const session = await agent.sessions.create();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await session.append_user_message({
|
|
112
|
+
text: "Diagnose why session title generation is flaky",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const records = await session.records();
|
|
116
|
+
assert.equal(records.session.title, undefined);
|
|
117
|
+
|
|
118
|
+
await agent.getLogger().saveAllLogs();
|
|
119
|
+
const log_lines = await read_log_lines(agent_path);
|
|
120
|
+
const title_failure_log = log_lines
|
|
121
|
+
.map((line) => JSON.parse(line))
|
|
122
|
+
.find((entry) => entry.message.includes("session_title.generate_failed"));
|
|
123
|
+
|
|
124
|
+
assert.ok(title_failure_log);
|
|
125
|
+
assert.equal(title_failure_log.type, "warn");
|
|
126
|
+
assert.equal(title_failure_log.details.sessionId, session.id);
|
|
127
|
+
assert.equal(
|
|
128
|
+
title_failure_log.details.modelLabel,
|
|
129
|
+
"mock-session-title-failing-model",
|
|
130
|
+
);
|
|
131
|
+
assert.equal(
|
|
132
|
+
title_failure_log.details.message,
|
|
133
|
+
"mock title generation failed",
|
|
134
|
+
);
|
|
135
|
+
assert.equal(
|
|
136
|
+
title_failure_log.details.firstUserTextLength,
|
|
137
|
+
"Diagnose why session title generation is flaky".length,
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
await agent.dispose();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
78
144
|
test("Session retries title generation after model becomes available", async () => {
|
|
79
145
|
const agent_path = await fs.mkdtemp(
|
|
80
146
|
path.join(os.tmpdir(), "downcity-agent-session-title-retry-"),
|
package/src/session/Session.ts
CHANGED
|
@@ -120,6 +120,7 @@ export class Session implements AgentSession {
|
|
|
120
120
|
history_store: this.historyStore,
|
|
121
121
|
executor: this.executor,
|
|
122
122
|
state: this.localState,
|
|
123
|
+
logger: this.logger,
|
|
123
124
|
ensure_configured_hook: this.ensureConfiguredHook
|
|
124
125
|
? async () => {
|
|
125
126
|
await this.ensureConfiguredHook?.(this);
|
|
@@ -142,6 +143,7 @@ export class Session implements AgentSession {
|
|
|
142
143
|
session_id: this.id,
|
|
143
144
|
history_store: this.historyStore,
|
|
144
145
|
state_service: this.stateService,
|
|
146
|
+
logger: this.logger,
|
|
145
147
|
is_executing: () => this.isExecuting(),
|
|
146
148
|
get_instruction_system_blocks: this.getInstructionSystemBlocks,
|
|
147
149
|
get_managed_plugin_system_blocks: this.getManagedPluginSystemBlocks,
|
|
@@ -11,6 +11,7 @@ import { generateText, type LanguageModel } from "ai";
|
|
|
11
11
|
import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
|
|
12
12
|
import type { SessionRecordV1 } from "@/executor/types/SessionRecords.js";
|
|
13
13
|
import { is_session_message_record } from "@/executor/types/SessionRecords.js";
|
|
14
|
+
import type { Logger } from "@/utils/logger/Logger.js";
|
|
14
15
|
import {
|
|
15
16
|
normalizeSessionTitle,
|
|
16
17
|
readSessionMetadata,
|
|
@@ -48,6 +49,16 @@ export interface EnsureSessionTitleParams {
|
|
|
48
49
|
*/
|
|
49
50
|
model?: LanguageModel;
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 当前模型展示标签;仅用于排障日志,不参与生成逻辑。
|
|
54
|
+
*/
|
|
55
|
+
modelLabel?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 当前 session 运行日志器;标题生成失败时仅记录摘要,不影响主流程。
|
|
59
|
+
*/
|
|
60
|
+
logger?: Logger;
|
|
61
|
+
|
|
51
62
|
/**
|
|
52
63
|
* 是否允许调用模型生成标题。
|
|
53
64
|
*/
|
|
@@ -102,16 +113,95 @@ function normalizeGeneratedTitle(input: string): string | undefined {
|
|
|
102
113
|
: undefined;
|
|
103
114
|
}
|
|
104
115
|
|
|
116
|
+
function summarizeTitleError(error: unknown): {
|
|
117
|
+
/**
|
|
118
|
+
* 错误对象名称。
|
|
119
|
+
*/
|
|
120
|
+
name: string | null;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 错误消息摘要。
|
|
124
|
+
*/
|
|
125
|
+
message: string | null;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 字符串化后的错误摘要。
|
|
129
|
+
*/
|
|
130
|
+
error: string;
|
|
131
|
+
} {
|
|
132
|
+
const record =
|
|
133
|
+
error && typeof error === "object" && !Array.isArray(error)
|
|
134
|
+
? (error as Record<string, unknown>)
|
|
135
|
+
: {};
|
|
136
|
+
return {
|
|
137
|
+
name: typeof record.name === "string" ? record.name : null,
|
|
138
|
+
message: typeof record.message === "string" ? record.message : null,
|
|
139
|
+
error: String(error),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function logSessionTitleDiagnostic(input: {
|
|
144
|
+
/**
|
|
145
|
+
* 当前 session 标识。
|
|
146
|
+
*/
|
|
147
|
+
sessionId: string;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 日志级别。
|
|
151
|
+
*/
|
|
152
|
+
level: "debug" | "warn";
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 日志消息。
|
|
156
|
+
*/
|
|
157
|
+
message: string;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 结构化日志字段。
|
|
161
|
+
*/
|
|
162
|
+
details: Record<string, string | number | boolean | null | undefined>;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 当前 session 运行日志器。
|
|
166
|
+
*/
|
|
167
|
+
logger?: Logger;
|
|
168
|
+
}): Promise<void> {
|
|
169
|
+
if (!input.logger) return;
|
|
170
|
+
try {
|
|
171
|
+
await input.logger.log(input.level, input.message, {
|
|
172
|
+
sessionId: input.sessionId,
|
|
173
|
+
...input.details,
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
// 关键点(中文):标题诊断日志失败不能影响 session 主流程。
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
105
180
|
async function generateSessionTitle(input: {
|
|
106
181
|
/**
|
|
107
182
|
* 当前模型实例。
|
|
108
183
|
*/
|
|
109
184
|
model: LanguageModel;
|
|
110
185
|
|
|
186
|
+
/**
|
|
187
|
+
* 当前 session 标识。
|
|
188
|
+
*/
|
|
189
|
+
sessionId: string;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 当前模型展示标签;仅用于排障日志。
|
|
193
|
+
*/
|
|
194
|
+
modelLabel?: string;
|
|
195
|
+
|
|
111
196
|
/**
|
|
112
197
|
* 首条用户消息文本。
|
|
113
198
|
*/
|
|
114
199
|
firstUserText: string;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 当前 session 运行日志器。
|
|
203
|
+
*/
|
|
204
|
+
logger?: Logger;
|
|
115
205
|
}): Promise<string | undefined> {
|
|
116
206
|
try {
|
|
117
207
|
const result = await generateText({
|
|
@@ -125,8 +215,33 @@ async function generateSessionTitle(input: {
|
|
|
125
215
|
input.firstUserText,
|
|
126
216
|
].join("\n"),
|
|
127
217
|
});
|
|
128
|
-
|
|
129
|
-
|
|
218
|
+
const generatedTitle = normalizeGeneratedTitle(result.text);
|
|
219
|
+
if (!generatedTitle) {
|
|
220
|
+
await logSessionTitleDiagnostic({
|
|
221
|
+
logger: input.logger,
|
|
222
|
+
sessionId: input.sessionId,
|
|
223
|
+
level: "warn",
|
|
224
|
+
message: "[agent] session_title.empty",
|
|
225
|
+
details: {
|
|
226
|
+
modelLabel: input.modelLabel || null,
|
|
227
|
+
firstUserTextLength: input.firstUserText.length,
|
|
228
|
+
rawTitleLength: String(result.text || "").length,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return generatedTitle;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
await logSessionTitleDiagnostic({
|
|
235
|
+
logger: input.logger,
|
|
236
|
+
sessionId: input.sessionId,
|
|
237
|
+
level: "warn",
|
|
238
|
+
message: "[agent] session_title.generate_failed",
|
|
239
|
+
details: {
|
|
240
|
+
modelLabel: input.modelLabel || null,
|
|
241
|
+
firstUserTextLength: input.firstUserText.length,
|
|
242
|
+
...summarizeTitleError(error),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
130
245
|
// 关键点(中文):标题生成失败不能影响 session 主流程。
|
|
131
246
|
return undefined;
|
|
132
247
|
}
|
|
@@ -142,13 +257,30 @@ export async function ensureSessionTitle(
|
|
|
142
257
|
if (current.title) return current;
|
|
143
258
|
|
|
144
259
|
const firstUserText = resolveFirstUserText(input.messages);
|
|
145
|
-
if (input.generate !== true
|
|
260
|
+
if (input.generate !== true) {
|
|
261
|
+
return current;
|
|
262
|
+
}
|
|
263
|
+
if (!input.model || !firstUserText) {
|
|
264
|
+
await logSessionTitleDiagnostic({
|
|
265
|
+
logger: input.logger,
|
|
266
|
+
sessionId: input.sessionId,
|
|
267
|
+
level: "debug",
|
|
268
|
+
message: "[agent] session_title.skipped",
|
|
269
|
+
details: {
|
|
270
|
+
reason: !input.model ? "missing_model" : "missing_first_user_text",
|
|
271
|
+
modelLabel: input.modelLabel || null,
|
|
272
|
+
messageCount: input.messages.length,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
146
275
|
return current;
|
|
147
276
|
}
|
|
148
277
|
|
|
149
278
|
const generatedTitle = await generateSessionTitle({
|
|
150
279
|
model: input.model,
|
|
280
|
+
sessionId: input.sessionId,
|
|
281
|
+
modelLabel: input.modelLabel,
|
|
151
282
|
firstUserText,
|
|
283
|
+
logger: input.logger,
|
|
152
284
|
});
|
|
153
285
|
if (!generatedTitle) return current;
|
|
154
286
|
|
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
import { to_session_action_record } from "@/executor/types/SessionRecords.js";
|
|
42
42
|
import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
|
|
43
43
|
import { generateId } from "@/utils/Id.js";
|
|
44
|
+
import type { Logger } from "@/utils/logger/Logger.js";
|
|
44
45
|
|
|
45
46
|
type SessionStateServiceOptions = {
|
|
46
47
|
/**
|
|
@@ -73,6 +74,11 @@ type SessionStateServiceOptions = {
|
|
|
73
74
|
*/
|
|
74
75
|
state: SessionLocalState;
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 当前 session 运行日志器。
|
|
79
|
+
*/
|
|
80
|
+
logger: Logger;
|
|
81
|
+
|
|
76
82
|
/**
|
|
77
83
|
* 在执行前补齐宿主级配置。
|
|
78
84
|
*/
|
|
@@ -107,6 +113,7 @@ export class SessionStateService {
|
|
|
107
113
|
private readonly history_store: JsonlSessionHistoryStore;
|
|
108
114
|
private readonly executor: Executor;
|
|
109
115
|
private readonly state: SessionLocalState;
|
|
116
|
+
private readonly logger: Logger;
|
|
110
117
|
private readonly ensure_configured_hook?: SessionStateServiceOptions["ensure_configured_hook"];
|
|
111
118
|
private readonly publish_event: SessionStateServiceOptions["publish_event"];
|
|
112
119
|
|
|
@@ -117,6 +124,7 @@ export class SessionStateService {
|
|
|
117
124
|
this.history_store = options.history_store;
|
|
118
125
|
this.executor = options.executor;
|
|
119
126
|
this.state = options.state;
|
|
127
|
+
this.logger = options.logger;
|
|
120
128
|
this.ensure_configured_hook = options.ensure_configured_hook;
|
|
121
129
|
this.publish_event = options.publish_event;
|
|
122
130
|
}
|
|
@@ -226,9 +234,17 @@ export class SessionStateService {
|
|
|
226
234
|
const next_model_label = input.model
|
|
227
235
|
? inferAgentModelLabel(input.model)
|
|
228
236
|
: undefined;
|
|
237
|
+
const should_emit_model_switch_action = Boolean(
|
|
238
|
+
input.model &&
|
|
239
|
+
should_emit_action &&
|
|
240
|
+
this.state.sessionConfig.model &&
|
|
241
|
+
previous_model_label &&
|
|
242
|
+
next_model_label &&
|
|
243
|
+
previous_model_label !== next_model_label,
|
|
244
|
+
);
|
|
229
245
|
const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
|
|
230
246
|
|
|
231
|
-
if (
|
|
247
|
+
if (should_emit_model_switch_action) {
|
|
232
248
|
await this.emit_action_event({
|
|
233
249
|
id: action_id,
|
|
234
250
|
title: "Switching session model",
|
|
@@ -252,7 +268,7 @@ export class SessionStateService {
|
|
|
252
268
|
model: this.state.sessionConfig.model,
|
|
253
269
|
});
|
|
254
270
|
} catch (error) {
|
|
255
|
-
if (
|
|
271
|
+
if (should_emit_model_switch_action) {
|
|
256
272
|
await this.emit_action_event({
|
|
257
273
|
id: action_id,
|
|
258
274
|
title: "Session model switch failed",
|
|
@@ -263,7 +279,7 @@ export class SessionStateService {
|
|
|
263
279
|
throw error;
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
if (
|
|
282
|
+
if (should_emit_model_switch_action) {
|
|
267
283
|
await this.emit_action_event({
|
|
268
284
|
id: action_id,
|
|
269
285
|
title: "Session model switched",
|
|
@@ -332,6 +348,10 @@ export class SessionStateService {
|
|
|
332
348
|
sessionId: this.session_id,
|
|
333
349
|
messages,
|
|
334
350
|
...(input?.generate ? { model: this.state.sessionConfig.model } : {}),
|
|
351
|
+
...(this.state.sessionConfig.modelLabel
|
|
352
|
+
? { modelLabel: this.state.sessionConfig.modelLabel }
|
|
353
|
+
: {}),
|
|
354
|
+
logger: this.logger,
|
|
335
355
|
generate: input?.generate === true,
|
|
336
356
|
});
|
|
337
357
|
const next_title = String(next_metadata.title || "").trim();
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
import type { SessionRecordV1 } from "@/executor/types/SessionRecords.js";
|
|
32
32
|
import { SessionStateService } from "@/session/services/SessionStateService.js";
|
|
33
33
|
import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
|
|
34
|
+
import type { Logger } from "@/utils/logger/Logger.js";
|
|
34
35
|
|
|
35
36
|
type SessionViewServiceOptions<TSession extends Pick<AgentSession, "set">> = {
|
|
36
37
|
/**
|
|
@@ -58,6 +59,11 @@ type SessionViewServiceOptions<TSession extends Pick<AgentSession, "set">> = {
|
|
|
58
59
|
*/
|
|
59
60
|
state_service: SessionStateService;
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 当前 session 运行日志器。
|
|
64
|
+
*/
|
|
65
|
+
logger: Logger;
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
68
|
* 判断当前 session 是否正在执行。
|
|
63
69
|
*/
|
|
@@ -106,6 +112,7 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
|
|
|
106
112
|
private readonly session_id: string;
|
|
107
113
|
private readonly history_store: JsonlSessionHistoryStore;
|
|
108
114
|
private readonly state_service: SessionStateService;
|
|
115
|
+
private readonly logger: Logger;
|
|
109
116
|
private readonly is_executing: SessionViewServiceOptions<TSession>["is_executing"];
|
|
110
117
|
private readonly get_instruction_system_blocks: SessionViewServiceOptions<TSession>["get_instruction_system_blocks"];
|
|
111
118
|
private readonly get_managed_plugin_system_blocks: SessionViewServiceOptions<TSession>["get_managed_plugin_system_blocks"];
|
|
@@ -119,6 +126,7 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
|
|
|
119
126
|
this.session_id = options.session_id;
|
|
120
127
|
this.history_store = options.history_store;
|
|
121
128
|
this.state_service = options.state_service;
|
|
129
|
+
this.logger = options.logger;
|
|
122
130
|
this.is_executing = options.is_executing;
|
|
123
131
|
this.get_instruction_system_blocks = options.get_instruction_system_blocks;
|
|
124
132
|
this.get_managed_plugin_system_blocks =
|
|
@@ -164,6 +172,7 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
|
|
|
164
172
|
agentId: this.agent_id,
|
|
165
173
|
sessionId: this.session_id,
|
|
166
174
|
messages: input.messages,
|
|
175
|
+
logger: this.logger,
|
|
167
176
|
});
|
|
168
177
|
return buildSessionInfo({
|
|
169
178
|
projectRoot: this.project_root,
|