@downcity/agent 1.1.317 → 1.1.323
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/agent/Agent.d.ts.map +1 -1
- package/bin/agent/Agent.js +1 -0
- package/bin/agent/Agent.js.map +1 -1
- package/bin/agent/AgentSessions.d.ts +2 -0
- package/bin/agent/AgentSessions.d.ts.map +1 -1
- package/bin/agent/AgentSessions.js +9 -0
- package/bin/agent/AgentSessions.js.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.d.ts +15 -7
- package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.js +22 -32
- package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
- package/bin/executor/types/SessionHistoryMeta.d.ts +2 -0
- package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
- package/bin/index.d.ts +2 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/remote/RemoteSession.d.ts +2 -2
- package/bin/remote/RemoteSession.d.ts.map +1 -1
- package/bin/remote/RemoteSession.js +2 -2
- package/bin/remote/RemoteSession.js.map +1 -1
- package/bin/remote/RemoteTransport.d.ts +2 -2
- package/bin/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/remote/transports/HttpRemoteAgentTransport.d.ts +2 -2
- package/bin/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/remote/transports/HttpRemoteAgentTransport.js +2 -2
- package/bin/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/remote/transports/RpcClient.d.ts +2 -2
- package/bin/remote/transports/RpcClient.d.ts.map +1 -1
- package/bin/remote/transports/RpcClient.js +2 -2
- package/bin/remote/transports/RpcClient.js.map +1 -1
- package/bin/remote/transports/RpcRemoteAgentTransport.d.ts +2 -2
- package/bin/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/remote/transports/RpcRemoteAgentTransport.js +2 -2
- package/bin/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/session/Session.d.ts +6 -4
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +56 -31
- package/bin/session/Session.js.map +1 -1
- package/bin/session/SessionLoop.d.ts.map +1 -1
- package/bin/session/SessionLoop.js +6 -4
- package/bin/session/SessionLoop.js.map +1 -1
- package/bin/session/SessionMessages.d.ts +9 -3
- package/bin/session/SessionMessages.d.ts.map +1 -1
- package/bin/session/SessionMessages.js +35 -21
- package/bin/session/SessionMessages.js.map +1 -1
- package/bin/session/SessionState.d.ts +23 -10
- package/bin/session/SessionState.d.ts.map +1 -1
- package/bin/session/SessionState.js +129 -51
- package/bin/session/SessionState.js.map +1 -1
- package/bin/session/SessionTitle.d.ts +4 -0
- package/bin/session/SessionTitle.d.ts.map +1 -1
- package/bin/session/SessionTitle.js +5 -1
- package/bin/session/SessionTitle.js.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.js +4 -4
- package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
- package/bin/session/runtime/SessionTitleTask.d.ts +35 -0
- package/bin/session/runtime/SessionTitleTask.d.ts.map +1 -0
- package/bin/session/runtime/SessionTitleTask.js +72 -0
- package/bin/session/runtime/SessionTitleTask.js.map +1 -0
- package/bin/session/storage/Metadata.d.ts.map +1 -1
- package/bin/session/storage/Metadata.js +7 -0
- package/bin/session/storage/Metadata.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +3 -3
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +20 -0
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +2 -1
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/sdk/AgentSessionPrompt.d.ts +22 -0
- package/bin/types/sdk/AgentSessionPrompt.d.ts.map +1 -1
- package/bin/types/sdk/AgentSessionPrompt.js.map +1 -1
- package/bin/types/session/SessionCommand.d.ts +2 -0
- package/bin/types/session/SessionCommand.d.ts.map +1 -1
- package/bin/types/session/SessionLocalState.d.ts +3 -0
- package/bin/types/session/SessionLocalState.d.ts.map +1 -1
- package/bin/types/session/SessionMessages.d.ts +5 -0
- package/bin/types/session/SessionMessages.d.ts.map +1 -1
- package/bin/types/session/SessionOptions.d.ts +2 -0
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/store/SessionAttachmentStore.d.ts +29 -0
- package/bin/types/store/SessionAttachmentStore.d.ts.map +1 -0
- package/bin/types/store/SessionAttachmentStore.js +15 -0
- package/bin/types/store/SessionAttachmentStore.js.map +1 -0
- package/bin/types/store/SessionStore.d.ts +3 -0
- package/bin/types/store/SessionStore.d.ts.map +1 -1
- package/bin/workspace/store/LocalSessionAttachmentStore.d.ts +25 -0
- package/bin/workspace/store/LocalSessionAttachmentStore.d.ts.map +1 -0
- package/bin/workspace/store/LocalSessionAttachmentStore.js +72 -0
- package/bin/workspace/store/LocalSessionAttachmentStore.js.map +1 -0
- package/bin/workspace/store/LocalSessionStore.d.ts +3 -0
- package/bin/workspace/store/LocalSessionStore.d.ts.map +1 -1
- package/bin/workspace/store/LocalSessionStore.js +8 -1
- package/bin/workspace/store/LocalSessionStore.js.map +1 -1
- package/bin/workspace/store/LocalStorePaths.d.ts +6 -0
- package/bin/workspace/store/LocalStorePaths.d.ts.map +1 -1
- package/bin/workspace/store/LocalStorePaths.js +8 -0
- package/bin/workspace/store/LocalStorePaths.js.map +1 -1
- package/package.json +6 -6
- package/scripts/remote-session-reconnect.test.mjs +25 -0
- package/scripts/session-attachments.test.mjs +101 -0
- package/scripts/session-config-turn-boundary.test.mjs +185 -0
- package/scripts/session-list-title.test.mjs +6 -0
- package/scripts/session-title-event.test.mjs +67 -3
- package/scripts/session-turn-failure.test.mjs +34 -1
- package/src/agent/Agent.ts +1 -0
- package/src/agent/AgentSessions.ts +10 -0
- package/src/executor/messages/SessionAttachmentMapper.ts +24 -37
- package/src/executor/types/SessionHistoryMeta.ts +2 -0
- package/src/index.ts +2 -0
- package/src/remote/RemoteSession.ts +6 -2
- package/src/remote/RemoteTransport.ts +6 -1
- package/src/remote/transports/HttpRemoteAgentTransport.ts +3 -1
- package/src/remote/transports/RpcClient.ts +3 -1
- package/src/remote/transports/RpcRemoteAgentTransport.ts +3 -1
- package/src/session/Session.ts +65 -32
- package/src/session/SessionLoop.ts +6 -3
- package/src/session/SessionMessages.ts +61 -20
- package/src/session/SessionState.ts +150 -59
- package/src/session/SessionTitle.ts +13 -1
- package/src/session/messages/SessionAssistantMessageWriter.ts +7 -3
- package/src/session/runtime/SessionTitleTask.ts +86 -0
- package/src/session/storage/Metadata.ts +10 -0
- package/src/types/agent/SessionActor.ts +3 -2
- package/src/types/agent/SessionTypes.ts +22 -0
- package/src/types/rpc/RpcProtocol.ts +6 -1
- package/src/types/sdk/AgentSessionPrompt.ts +22 -0
- package/src/types/session/SessionCommand.ts +2 -0
- package/src/types/session/SessionLocalState.ts +4 -0
- package/src/types/session/SessionMessages.ts +5 -0
- package/src/types/session/SessionOptions.ts +3 -0
- package/src/types/store/SessionAttachmentStore.ts +29 -0
- package/src/types/store/SessionStore.ts +4 -0
- package/src/workspace/store/LocalSessionAttachmentStore.ts +96 -0
- package/src/workspace/store/LocalSessionStore.ts +13 -0
- package/src/workspace/store/LocalStorePaths.ts +16 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -21,10 +21,21 @@ import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
|
|
|
21
21
|
import { generate_id } from "@/utils/Id.js";
|
|
22
22
|
import type { Logger } from "@/utils/logger/Logger.js";
|
|
23
23
|
import { SessionMessages } from "@/session/SessionMessages.js";
|
|
24
|
+
import { SessionTitleTask } from "@/session/runtime/SessionTitleTask.js";
|
|
24
25
|
import { to_executor_history } from "@/session/messages/SessionMessageCodec.js";
|
|
25
26
|
import type { SessionMessage } from "@/types/session/SessionMessage.js";
|
|
27
|
+
import { is_session_message_record } from "@/executor/types/SessionRecords.js";
|
|
26
28
|
import type { SessionStateOptions } from "@/types/session/SessionState.js";
|
|
27
29
|
import type { SessionStore } from "@/types/store/SessionStore.js";
|
|
30
|
+
import type { SessionApprovalMode } from "@/types/session/SessionInteraction.js";
|
|
31
|
+
|
|
32
|
+
/** Session 模型配置写入结果。 */
|
|
33
|
+
export interface SessionModelSetResult {
|
|
34
|
+
/** 已包含最新运行时模型实例的 configured 快照。 */
|
|
35
|
+
config: AgentSessionConfigSnapshot;
|
|
36
|
+
/** 持久化模型身份是否发生真实变化。 */
|
|
37
|
+
changed: boolean;
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
/**
|
|
30
41
|
* 本地 Session 配置与 Metadata 状态管理器。
|
|
@@ -39,6 +50,8 @@ export class SessionState {
|
|
|
39
50
|
private readonly ensure_configured_hook?: SessionStateOptions["ensure_configured_hook"];
|
|
40
51
|
private readonly get_model: SessionStateOptions["get_model"];
|
|
41
52
|
private readonly publish_event: SessionStateOptions["publish_event"];
|
|
53
|
+
private readonly title_task: SessionTitleTask;
|
|
54
|
+
private metadata_mutation_chain: Promise<void> = Promise.resolve();
|
|
42
55
|
|
|
43
56
|
constructor(options: SessionStateOptions) {
|
|
44
57
|
this.agent_id = options.agent_id;
|
|
@@ -50,6 +63,10 @@ export class SessionState {
|
|
|
50
63
|
this.ensure_configured_hook = options.ensure_configured_hook;
|
|
51
64
|
this.get_model = options.get_model;
|
|
52
65
|
this.publish_event = options.publish_event;
|
|
66
|
+
this.title_task = new SessionTitleTask({
|
|
67
|
+
session_id: this.session_id,
|
|
68
|
+
logger: this.logger,
|
|
69
|
+
});
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
/**
|
|
@@ -61,6 +78,11 @@ export class SessionState {
|
|
|
61
78
|
};
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
/** 读取当前 Session 已接受的 Shell 审批模式。 */
|
|
82
|
+
get_approval_mode(): SessionApprovalMode {
|
|
83
|
+
return this.state.configured_approval_mode;
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
/**
|
|
65
87
|
* 读取当前 session 创建时间。
|
|
66
88
|
*/
|
|
@@ -99,10 +121,13 @@ export class SessionState {
|
|
|
99
121
|
});
|
|
100
122
|
this.state.created_at = created_at;
|
|
101
123
|
this.state.timezone = timezone;
|
|
102
|
-
this.state.session_config = {
|
|
124
|
+
this.state.session_config = {
|
|
125
|
+
...(metadata.model_label ? { model_label: metadata.model_label } : {}),
|
|
126
|
+
};
|
|
103
127
|
this.state.effective_session_config = {
|
|
104
128
|
...this.state.session_config,
|
|
105
129
|
};
|
|
130
|
+
this.state.configured_approval_mode = metadata.approval_mode || "ask";
|
|
106
131
|
})();
|
|
107
132
|
await this.state.initialize_promise;
|
|
108
133
|
}
|
|
@@ -141,23 +166,44 @@ export class SessionState {
|
|
|
141
166
|
/**
|
|
142
167
|
* 写入当前 session 配置。
|
|
143
168
|
*/
|
|
144
|
-
async set_model(model: AgentModel): Promise<
|
|
169
|
+
async set_model(model: AgentModel): Promise<SessionModelSetResult> {
|
|
145
170
|
const next_model_label = infer_agent_model_label(model);
|
|
171
|
+
const changed = next_model_label !== this.state.session_config.model_label;
|
|
146
172
|
const next_config: AgentSessionConfigSnapshot = {
|
|
147
173
|
...this.state.session_config,
|
|
148
174
|
model,
|
|
149
175
|
model_label: next_model_label,
|
|
150
176
|
model_context_window: read_agent_model_context_window(model),
|
|
151
177
|
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
if (changed) {
|
|
179
|
+
await this.run_metadata_mutation(async () => {
|
|
180
|
+
const metadata = await this.store.read_metadata();
|
|
181
|
+
await this.store.write_metadata({
|
|
182
|
+
...metadata,
|
|
183
|
+
agent_id: this.agent_id,
|
|
184
|
+
updated_at: Date.now(),
|
|
185
|
+
...(next_model_label ? { model_label: next_model_label } : {}),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
159
189
|
this.state.session_config = next_config;
|
|
160
|
-
return next_config;
|
|
190
|
+
return { config: next_config, changed };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 接受并持久化当前 Session 的 Shell 审批模式。 */
|
|
194
|
+
async set_approval_mode(mode: SessionApprovalMode): Promise<boolean> {
|
|
195
|
+
if (mode === this.state.configured_approval_mode) return false;
|
|
196
|
+
await this.run_metadata_mutation(async () => {
|
|
197
|
+
const metadata = await this.store.read_metadata();
|
|
198
|
+
await this.store.write_metadata({
|
|
199
|
+
...metadata,
|
|
200
|
+
agent_id: this.agent_id,
|
|
201
|
+
updated_at: Date.now(),
|
|
202
|
+
approval_mode: mode,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
this.state.configured_approval_mode = mode;
|
|
206
|
+
return true;
|
|
161
207
|
}
|
|
162
208
|
|
|
163
209
|
/** 在 Session Step 检查点提交模型配置。 */
|
|
@@ -175,62 +221,107 @@ export class SessionState {
|
|
|
175
221
|
const preview_text = resolve_message_preview(
|
|
176
222
|
stats.latest_message || undefined,
|
|
177
223
|
).slice(0, 180);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
224
|
+
await this.run_metadata_mutation(async () => {
|
|
225
|
+
const metadata = await this.store.read_metadata();
|
|
226
|
+
await this.store.write_metadata({
|
|
227
|
+
...metadata,
|
|
228
|
+
agent_id: this.agent_id,
|
|
229
|
+
updated_at: Date.now(),
|
|
230
|
+
...(this.state.session_config.model_label
|
|
231
|
+
? { model_label: this.state.session_config.model_label }
|
|
232
|
+
: {}),
|
|
233
|
+
message_count: stats.message_count,
|
|
234
|
+
historyBytes: stats.history_bytes,
|
|
235
|
+
...(preview_text ? { preview_text: preview_text } : {}),
|
|
236
|
+
});
|
|
189
237
|
});
|
|
190
238
|
}
|
|
191
239
|
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
await this.messages.context_snapshot(),
|
|
204
|
-
);
|
|
205
|
-
const before_metadata = await this.store.read_metadata();
|
|
206
|
-
const before_title = String(before_metadata.title || "").trim();
|
|
207
|
-
const next_metadata = await ensure_session_title({
|
|
208
|
-
session_id: this.session_id,
|
|
209
|
-
store: this.store,
|
|
210
|
-
messages,
|
|
211
|
-
...(input?.generate
|
|
212
|
-
? {
|
|
213
|
-
model: this.get_model(),
|
|
214
|
-
}
|
|
215
|
-
: {}),
|
|
216
|
-
...(this.state.session_config.model_label
|
|
217
|
-
? { model_label: this.state.session_config.model_label }
|
|
218
|
-
: {}),
|
|
219
|
-
logger: this.logger,
|
|
220
|
-
generate: input?.generate === true,
|
|
240
|
+
/** 在后台刷新 Session metadata,不阻塞当前 Turn。 */
|
|
241
|
+
touch_metadata_in_background(): void {
|
|
242
|
+
void this.touch_metadata().catch(async (error) => {
|
|
243
|
+
try {
|
|
244
|
+
await this.logger.log("warn", "[agent] session_metadata.background_update_failed", {
|
|
245
|
+
session_id: this.session_id,
|
|
246
|
+
error: error instanceof Error ? error.message : String(error),
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
// metadata 诊断日志失败不能影响当前 Turn。
|
|
250
|
+
}
|
|
221
251
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 异步调度首条用户消息的 Session 标题生成。 */
|
|
255
|
+
schedule_title_generation(): void {
|
|
256
|
+
this.title_task.schedule(async (signal) => {
|
|
257
|
+
const before_metadata = await this.store.read_metadata();
|
|
258
|
+
if (String(before_metadata.title || "").trim()) return;
|
|
259
|
+
const records = to_executor_history(
|
|
260
|
+
this.session_id,
|
|
261
|
+
await this.messages.context_snapshot(),
|
|
262
|
+
);
|
|
263
|
+
const first_user_message = records.find(
|
|
264
|
+
(record) => is_session_message_record(record) && record.role === "user",
|
|
265
|
+
);
|
|
266
|
+
if (!first_user_message) return;
|
|
267
|
+
const first_user_message_id = first_user_message.id;
|
|
268
|
+
const before_title = String(before_metadata.title || "").trim();
|
|
269
|
+
const next_metadata = await ensure_session_title({
|
|
270
|
+
session_id: this.session_id,
|
|
271
|
+
store: this.store,
|
|
272
|
+
messages: records,
|
|
273
|
+
model: this.get_model(),
|
|
274
|
+
model_label: this.state.session_config.model_label,
|
|
275
|
+
logger: this.logger,
|
|
276
|
+
generate: true,
|
|
277
|
+
signal,
|
|
278
|
+
commit_title: async (title) => await this.run_metadata_mutation(async () => {
|
|
279
|
+
const latest_metadata = await this.store.read_metadata();
|
|
280
|
+
if (signal.aborted) return latest_metadata;
|
|
281
|
+
if (String(latest_metadata.title || "").trim()) return latest_metadata;
|
|
282
|
+
const latest_records = to_executor_history(
|
|
283
|
+
this.session_id,
|
|
284
|
+
await this.messages.context_snapshot(),
|
|
285
|
+
);
|
|
286
|
+
const source_exists = latest_records.some(
|
|
287
|
+
(record) =>
|
|
288
|
+
is_session_message_record(record) &&
|
|
289
|
+
record.id === first_user_message_id,
|
|
290
|
+
);
|
|
291
|
+
if (!source_exists || signal.aborted) return latest_metadata;
|
|
292
|
+
const next_metadata = { ...latest_metadata, title };
|
|
293
|
+
await this.store.write_metadata(next_metadata);
|
|
294
|
+
return next_metadata;
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
const next_title = String(next_metadata.title || "").trim();
|
|
298
|
+
if (!next_title || next_title === before_title || signal.aborted) return;
|
|
299
|
+
this.publish_event({
|
|
300
|
+
mutation_id: generate_id(),
|
|
301
|
+
variant: "session",
|
|
302
|
+
type: "title",
|
|
303
|
+
session_id: this.session_id,
|
|
304
|
+
created_at: Date.now(),
|
|
305
|
+
title: next_title,
|
|
306
|
+
});
|
|
231
307
|
});
|
|
232
308
|
}
|
|
233
309
|
|
|
310
|
+
/** 取消并释放当前 Session 的标题后台任务。 */
|
|
311
|
+
dispose_title_generation(): void {
|
|
312
|
+
this.title_task.dispose();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 串行提交 Session metadata,避免后台标题覆盖其他字段。 */
|
|
316
|
+
private async run_metadata_mutation<T>(mutation: () => Promise<T>): Promise<T> {
|
|
317
|
+
const task = this.metadata_mutation_chain.then(mutation, mutation);
|
|
318
|
+
this.metadata_mutation_chain = task.then(
|
|
319
|
+
() => undefined,
|
|
320
|
+
() => undefined,
|
|
321
|
+
);
|
|
322
|
+
return await task;
|
|
323
|
+
}
|
|
324
|
+
|
|
234
325
|
}
|
|
235
326
|
|
|
236
327
|
function resolve_message_preview(message: SessionMessage | undefined): string {
|
|
@@ -53,6 +53,12 @@ export interface EnsureSessionTitleParams {
|
|
|
53
53
|
* 是否允许调用模型生成标题。
|
|
54
54
|
*/
|
|
55
55
|
generate?: boolean;
|
|
56
|
+
|
|
57
|
+
/** 可选的标题请求取消信号。 */
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
|
|
60
|
+
/** 可选的标题提交入口,用于与其他 metadata mutation 串行化。 */
|
|
61
|
+
commit_title?: (title: string) => Promise<SessionHistoryMetaV1>;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function truncateTitle(input: string, maxChars: number): string {
|
|
@@ -192,6 +198,9 @@ async function generateSessionTitle(input: {
|
|
|
192
198
|
* 当前 session 运行日志器。
|
|
193
199
|
*/
|
|
194
200
|
logger?: Logger;
|
|
201
|
+
|
|
202
|
+
/** 标题请求取消信号。 */
|
|
203
|
+
signal?: AbortSignal;
|
|
195
204
|
}): Promise<string | undefined> {
|
|
196
205
|
let observedStreamError: unknown;
|
|
197
206
|
try {
|
|
@@ -208,6 +217,7 @@ async function generateSessionTitle(input: {
|
|
|
208
217
|
onError: ({ error }) => {
|
|
209
218
|
observedStreamError = error;
|
|
210
219
|
},
|
|
220
|
+
abortSignal: input.signal,
|
|
211
221
|
});
|
|
212
222
|
const text = await result.text;
|
|
213
223
|
const generatedTitle = normalizeGeneratedTitle(text);
|
|
@@ -277,11 +287,13 @@ export async function ensure_session_title(
|
|
|
277
287
|
model_label: input.model_label,
|
|
278
288
|
firstUserText,
|
|
279
289
|
logger: input.logger,
|
|
290
|
+
signal: input.signal,
|
|
280
291
|
});
|
|
281
292
|
if (!generatedTitle) return current;
|
|
282
293
|
|
|
294
|
+
if (input.commit_title) return await input.commit_title(generatedTitle);
|
|
283
295
|
const generatedMeta: SessionHistoryMetaV1 = {
|
|
284
|
-
...
|
|
296
|
+
...(await input.store.read_metadata()),
|
|
285
297
|
title: generatedTitle,
|
|
286
298
|
};
|
|
287
299
|
await input.store.write_metadata(generatedMeta);
|
|
@@ -559,9 +559,12 @@ export class SessionAssistantMessageWriter {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
/** 以失败状态关闭当前 Assistant Message。 */
|
|
562
|
-
async fail(
|
|
562
|
+
async fail(error: unknown): Promise<void> {
|
|
563
563
|
await this.enqueue_write(async () => {
|
|
564
|
-
await this.close_serialized(
|
|
564
|
+
await this.close_serialized(
|
|
565
|
+
"failed",
|
|
566
|
+
error instanceof Error ? error.message : String(error || ""),
|
|
567
|
+
);
|
|
565
568
|
});
|
|
566
569
|
}
|
|
567
570
|
|
|
@@ -786,13 +789,14 @@ export class SessionAssistantMessageWriter {
|
|
|
786
789
|
/** 在队列内关闭当前 Assistant Message。 */
|
|
787
790
|
private async close_serialized(
|
|
788
791
|
status: "completed" | "stopped" | "failed",
|
|
792
|
+
error?: string,
|
|
789
793
|
): Promise<void> {
|
|
790
794
|
if (this.closed) return;
|
|
791
795
|
this.tool_part_gate.close(
|
|
792
796
|
`Assistant Message writer closed with status ${status}`,
|
|
793
797
|
);
|
|
794
798
|
this.reset_step_state();
|
|
795
|
-
await this.recorder.complete_assistant_message(this.message_id, status);
|
|
799
|
+
await this.recorder.complete_assistant_message(this.message_id, status, error);
|
|
796
800
|
this.closed = true;
|
|
797
801
|
}
|
|
798
802
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 标题后台任务协调器。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 标题生成属于 metadata 增强任务,不得阻塞 Turn 主链路。
|
|
6
|
+
* - 同一个 Session 同时只允许一个标题任务运行。
|
|
7
|
+
* - 任务失败只记录日志,不能反向影响 Prompt 或 Executor。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Logger } from "@/utils/logger/Logger.js";
|
|
11
|
+
|
|
12
|
+
/** 标题后台任务执行函数。 */
|
|
13
|
+
export type SessionTitleTaskRunner = (signal: AbortSignal) => Promise<void>;
|
|
14
|
+
|
|
15
|
+
/** Session 标题后台任务协调器配置。 */
|
|
16
|
+
export interface SessionTitleTaskOptions {
|
|
17
|
+
/** 当前 Session 的稳定标识。 */
|
|
18
|
+
session_id: string;
|
|
19
|
+
|
|
20
|
+
/** 用于记录后台任务失败的统一日志器。 */
|
|
21
|
+
logger: Logger;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Session 标题后台任务协调器。 */
|
|
25
|
+
export class SessionTitleTask {
|
|
26
|
+
private readonly session_id: string;
|
|
27
|
+
private readonly logger: Logger;
|
|
28
|
+
private active_task: Promise<void> | null = null;
|
|
29
|
+
private abort_controller: AbortController | null = null;
|
|
30
|
+
private pending_runner: SessionTitleTaskRunner | null = null;
|
|
31
|
+
private disposed = false;
|
|
32
|
+
|
|
33
|
+
constructor(options: SessionTitleTaskOptions) {
|
|
34
|
+
this.session_id = String(options.session_id || "").trim();
|
|
35
|
+
this.logger = options.logger;
|
|
36
|
+
if (!this.session_id) {
|
|
37
|
+
throw new Error("SessionTitleTask requires a non-empty session_id");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 调度标题任务;已有任务或已释放时直接忽略。 */
|
|
42
|
+
schedule(runner: SessionTitleTaskRunner): void {
|
|
43
|
+
if (this.disposed) return;
|
|
44
|
+
if (this.active_task) {
|
|
45
|
+
this.pending_runner = runner;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const abort_controller = new AbortController();
|
|
49
|
+
this.abort_controller = abort_controller;
|
|
50
|
+
const task = Promise.resolve()
|
|
51
|
+
.then(async () => await runner(abort_controller.signal))
|
|
52
|
+
.catch(async (error) => {
|
|
53
|
+
if (abort_controller.signal.aborted) return;
|
|
54
|
+
try {
|
|
55
|
+
await this.logger.log("warn", "[agent] session_title.task_failed", {
|
|
56
|
+
session_id: this.session_id,
|
|
57
|
+
error: error instanceof Error ? error.message : String(error),
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
// 标题诊断日志失败不能影响 Session 主流程。
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.finally(() => {
|
|
64
|
+
if (this.active_task === task) {
|
|
65
|
+
this.active_task = null;
|
|
66
|
+
this.abort_controller = null;
|
|
67
|
+
const pending_runner = this.pending_runner;
|
|
68
|
+
this.pending_runner = null;
|
|
69
|
+
if (pending_runner && !this.disposed) this.schedule(pending_runner);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
this.active_task = task;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 取消当前标题任务并阻止后续结果提交。 */
|
|
76
|
+
cancel(): void {
|
|
77
|
+
this.abort_controller?.abort();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 释放协调器;释放后不再接受新的标题任务。 */
|
|
81
|
+
dispose(): void {
|
|
82
|
+
this.disposed = true;
|
|
83
|
+
this.pending_runner = null;
|
|
84
|
+
this.cancel();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -14,6 +14,13 @@ function normalizeModelLabel(input: unknown): string | undefined {
|
|
|
14
14
|
return label || undefined;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** 归一化 Session Shell 审批模式。 */
|
|
18
|
+
function normalize_approval_mode(
|
|
19
|
+
input: unknown,
|
|
20
|
+
): "ask" | "always-allow" | undefined {
|
|
21
|
+
return input === "ask" || input === "always-allow" ? input : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* 归一化 session 标题。
|
|
19
26
|
*/
|
|
@@ -106,6 +113,9 @@ export function normalize_session_metadata(
|
|
|
106
113
|
...(normalizeModelLabel(raw.model_label)
|
|
107
114
|
? { model_label: normalizeModelLabel(raw.model_label) }
|
|
108
115
|
: {}),
|
|
116
|
+
...(normalize_approval_mode(raw.approval_mode)
|
|
117
|
+
? { approval_mode: normalize_approval_mode(raw.approval_mode) }
|
|
118
|
+
: {}),
|
|
109
119
|
...(normalize_message_count(raw.message_count) !== undefined
|
|
110
120
|
? { message_count: normalize_message_count(raw.message_count) }
|
|
111
121
|
: {}),
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
AgentSessionForkInput,
|
|
19
19
|
AgentSessionInfo,
|
|
20
20
|
AgentSessionSetInput,
|
|
21
|
+
AgentSessionSetOptions,
|
|
21
22
|
AgentSessionStatus,
|
|
22
23
|
AgentSessionSummaryPage,
|
|
23
24
|
AgentSessionSystemSnapshot,
|
|
@@ -121,7 +122,7 @@ export interface AgentSession extends AgentSessionActor {
|
|
|
121
122
|
readonly config: AgentSessionConfigSnapshot;
|
|
122
123
|
|
|
123
124
|
/** 写入当前 session 默认配置。 */
|
|
124
|
-
set(input: AgentSessionSetInput): Promise<void>;
|
|
125
|
+
set(input: AgentSessionSetInput, options?: AgentSessionSetOptions): Promise<void>;
|
|
125
126
|
|
|
126
127
|
/** 把当前 Session 首次生成后固定的完整 system 显式固化到 instruction.md。 */
|
|
127
128
|
snapshot(): Promise<void>;
|
|
@@ -138,7 +139,7 @@ export interface AgentSession extends AgentSessionActor {
|
|
|
138
139
|
*/
|
|
139
140
|
export interface RemoteAgentSession extends AgentSessionActor {
|
|
140
141
|
/** 写入当前远程 Session 的可序列化动态配置。 */
|
|
141
|
-
set(input: RemoteSessionSetInput): Promise<void>;
|
|
142
|
+
set(input: RemoteSessionSetInput, options?: AgentSessionSetOptions): Promise<void>;
|
|
142
143
|
|
|
143
144
|
/** 从当前远程 session 创建一个分叉会话。 */
|
|
144
145
|
fork(input?: AgentSessionForkInput | string): Promise<RemoteAgentSession>;
|
|
@@ -80,6 +80,28 @@ export interface AgentSessionSetInput {
|
|
|
80
80
|
security?: AgentSessionSecurityConfig;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Session 配置提交后的 Action 可观测性选项。 */
|
|
84
|
+
export interface AgentSessionSetOptions {
|
|
85
|
+
/**
|
|
86
|
+
* 是否把真实配置变化持久化为 completed Action Message。
|
|
87
|
+
*
|
|
88
|
+
* 关键点(中文)
|
|
89
|
+
* - 默认值为 true。
|
|
90
|
+
* - 设为 false 只关闭 Action Message,不影响配置写入与检查点提交。
|
|
91
|
+
* - 适用于 Session 初始化、恢复与其他不应进入时间线的宿主装配流程。
|
|
92
|
+
*/
|
|
93
|
+
persist_action?: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 是否在 Action Message 持久化成功后发布对应 Session Mutation。
|
|
97
|
+
*
|
|
98
|
+
* 关键点(中文)
|
|
99
|
+
* - 默认跟随 `persist_action`。
|
|
100
|
+
* - 只有 `persist_action=true` 时才允许设为 true,避免发布不存在于 canonical Message 历史中的事件。
|
|
101
|
+
*/
|
|
102
|
+
publish_mutation?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
/** 可通过本地或远程 Session 动态更新的安全策略。 */
|
|
84
106
|
export interface AgentSessionSecurityConfig {
|
|
85
107
|
/** 高风险操作是否需要用户逐次审批。 */
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
AgentArchiveSessionResult,
|
|
16
16
|
AgentArchiveSessionsResult,
|
|
17
17
|
AgentCleanArchiveResult,
|
|
18
|
+
AgentSessionSetOptions,
|
|
18
19
|
RemoteSessionSetInput,
|
|
19
20
|
AgentSessionSystemSnapshot,
|
|
20
21
|
} from "@/types/agent/SessionTypes.js";
|
|
@@ -155,7 +156,11 @@ export type RpcRequest =
|
|
|
155
156
|
/** 更新指定 Session 的可序列化动态配置。 */
|
|
156
157
|
method: "sdk.sessions.set";
|
|
157
158
|
/** Session 与动态配置参数。 */
|
|
158
|
-
params: {
|
|
159
|
+
params: {
|
|
160
|
+
session_id: string;
|
|
161
|
+
input: RemoteSessionSetInput;
|
|
162
|
+
options?: AgentSessionSetOptions;
|
|
163
|
+
};
|
|
159
164
|
}
|
|
160
165
|
| {
|
|
161
166
|
/** 请求 id,用于匹配响应。 */
|
|
@@ -19,6 +19,28 @@ export type SessionUserMessagePart = UIMessagePart<UIDataTypes, UITools>;
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Session prompt 输入。
|
|
22
|
+
*
|
|
23
|
+
* 输入示例(中文):
|
|
24
|
+
* ```ts
|
|
25
|
+
* session.prompt({
|
|
26
|
+
* query: [
|
|
27
|
+
* { type: "text", text: "请分析这个附件" },
|
|
28
|
+
* {
|
|
29
|
+
* type: "file",
|
|
30
|
+
* mediaType: "image/png",
|
|
31
|
+
* url: "data:image/png;base64,...",
|
|
32
|
+
* filename: "image.png",
|
|
33
|
+
* },
|
|
34
|
+
* ],
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* 生命周期说明(中文):
|
|
39
|
+
* - `string` 会直接成为新的文本 User Message。
|
|
40
|
+
* - `file` part 如果携带 Data URL,会在 Message 入库前解码到 Session 的 attachments 目录。
|
|
41
|
+
* - 持久化 Message 里的 `url` 变成相对 Workspace 根目录的文件路径,而不是 Base64 内容。
|
|
42
|
+
* - 模型执行前,Executor 再按该路径读取文件,并转换成 AI SDK / Provider 可消费的 ModelMessage。
|
|
43
|
+
* - 远程 Session 中,Data URL 在服务端落盘;调用方本地路径必须对服务端可访问。
|
|
22
44
|
*/
|
|
23
45
|
export interface AgentSessionPromptInput {
|
|
24
46
|
/**
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { AgentSessionConfigSnapshot } from "@/types/agent/SessionTypes.js";
|
|
11
|
+
import type { SessionApprovalMode } from "@/types/session/SessionInteraction.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 本地 Session 内存状态。
|
|
@@ -27,6 +28,9 @@ export interface SessionLocalState {
|
|
|
27
28
|
*/
|
|
28
29
|
effective_session_config: AgentSessionConfigSnapshot;
|
|
29
30
|
|
|
31
|
+
/** 当前 Session 已接受并持久化的 Shell 审批模式。 */
|
|
32
|
+
configured_approval_mode: SessionApprovalMode;
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* 当前 session 创建时间(毫秒时间戳)。
|
|
32
36
|
*/
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { JsonObject } from "@/types/common/Json.js";
|
|
8
8
|
import type { SessionRecordV1 } from "@/executor/types/SessionRecords.js";
|
|
9
9
|
import type { SessionMessageStore } from "@/types/store/SessionStore.js";
|
|
10
|
+
import type { SessionAttachmentStore } from "@/types/store/SessionAttachmentStore.js";
|
|
10
11
|
import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
|
|
11
12
|
import type {
|
|
12
13
|
SessionAssistantMessagePart,
|
|
@@ -20,6 +21,8 @@ export interface SessionMessagesOptions {
|
|
|
20
21
|
session_id: string;
|
|
21
22
|
/** Message 快照持久化 Store。 */
|
|
22
23
|
store: SessionMessageStore;
|
|
24
|
+
/** 当前 Session 的附件持久化能力。 */
|
|
25
|
+
attachment_store: SessionAttachmentStore;
|
|
23
26
|
/** 持久化成功后的实时 Mutation 发布函数。 */
|
|
24
27
|
publish: (mutation: SessionMutation) => void;
|
|
25
28
|
}
|
|
@@ -80,6 +83,8 @@ export interface OpenSessionActionMessageInput {
|
|
|
80
83
|
description?: string;
|
|
81
84
|
/** 当前 Action 附加数据。 */
|
|
82
85
|
data?: JsonObject;
|
|
86
|
+
/** Action 创建或更新后是否发布对应 Session Mutation。 */
|
|
87
|
+
publish_mutation?: boolean;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/** Error Message 创建参数。 */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 附件持久化能力。
|
|
3
|
+
*
|
|
4
|
+
* Session Message 只保存附件引用,附件内容由该 Store 在 Session 生命周期内持有。
|
|
5
|
+
*
|
|
6
|
+
* 数据边界(中文):
|
|
7
|
+
* - 输入边界:`session.prompt()` 接收 `type: "file"`、`mediaType` 和 Data URL。
|
|
8
|
+
* - 存储边界:Store 解码 Data URL,写入 `.downcity/.../attachments/att_<id>.<ext>`。
|
|
9
|
+
* - Message 边界:Session Message 只记录相对 Workspace 根目录的附件路径。
|
|
10
|
+
* - 模型边界:模型转换器读取该路径,在内存中恢复成模型需要的文件内容。
|
|
11
|
+
*
|
|
12
|
+
* 因此,Data URL 不会直接进入 JSONL Message;Session Message 是附件路径的唯一引用。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 可持久化的 Data URL 附件。 */
|
|
16
|
+
export interface SessionAttachmentStore {
|
|
17
|
+
/**
|
|
18
|
+
* 保存 Data URL,并返回相对 Workspace 根目录的稳定文件路径。
|
|
19
|
+
* 调用成功后,返回路径对应的附件文件必须已经完整落盘。
|
|
20
|
+
*/
|
|
21
|
+
persist_data_url(input: {
|
|
22
|
+
/** 完整的 Data URL。 */
|
|
23
|
+
data_url: string;
|
|
24
|
+
/** 调用侧声明的 MIME 类型。 */
|
|
25
|
+
media_type: string;
|
|
26
|
+
/** 可选的原始文件名,仅用于推导扩展名。 */
|
|
27
|
+
filename?: string;
|
|
28
|
+
}): Promise<string>;
|
|
29
|
+
}
|