@downcity/agent 1.1.445 → 1.5.0
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 +29 -2
- package/bin/agent/Agent.d.ts.map +1 -1
- package/bin/agent/Agent.js +56 -10
- package/bin/agent/Agent.js.map +1 -1
- package/bin/agent/AgentSessionIdentity.d.ts +19 -0
- package/bin/agent/AgentSessionIdentity.d.ts.map +1 -0
- package/bin/agent/AgentSessionIdentity.js +26 -0
- package/bin/agent/AgentSessionIdentity.js.map +1 -0
- package/bin/agent/AgentSessions.d.ts +35 -9
- package/bin/agent/AgentSessions.d.ts.map +1 -1
- package/bin/agent/AgentSessions.js +110 -63
- package/bin/agent/AgentSessions.js.map +1 -1
- package/bin/agent/SessionWork.d.ts +85 -0
- package/bin/agent/SessionWork.d.ts.map +1 -0
- package/bin/agent/SessionWork.js +165 -0
- package/bin/agent/SessionWork.js.map +1 -0
- package/bin/index.d.ts +5 -0
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/session/Session.d.ts +34 -8
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +102 -27
- package/bin/session/Session.js.map +1 -1
- package/bin/session/input/StepInput.d.ts +15 -0
- package/bin/session/input/StepInput.d.ts.map +1 -1
- package/bin/session/input/StepInput.js +39 -5
- package/bin/session/input/StepInput.js.map +1 -1
- package/bin/session/loop/SessionLoop.d.ts +2 -1
- package/bin/session/loop/SessionLoop.d.ts.map +1 -1
- package/bin/session/loop/SessionLoop.js +7 -9
- package/bin/session/loop/SessionLoop.js.map +1 -1
- package/bin/session/loop/SessionState.d.ts +21 -1
- package/bin/session/loop/SessionState.d.ts.map +1 -1
- package/bin/session/loop/SessionState.js +54 -1
- package/bin/session/loop/SessionState.js.map +1 -1
- package/bin/session/storage/Browse.d.ts.map +1 -1
- package/bin/session/storage/Browse.js +2 -2
- package/bin/session/storage/Browse.js.map +1 -1
- package/bin/session/storage/LocalSessionStore.d.ts.map +1 -1
- package/bin/session/storage/LocalSessionStore.js +7 -7
- package/bin/session/storage/LocalSessionStore.js.map +1 -1
- package/bin/tools/session/SessionToolSchemas.d.ts +34 -0
- package/bin/tools/session/SessionToolSchemas.d.ts.map +1 -0
- package/bin/tools/session/SessionToolSchemas.js +39 -0
- package/bin/tools/session/SessionToolSchemas.js.map +1 -0
- package/bin/tools/session/SessionTools.d.ts +57 -0
- package/bin/tools/session/SessionTools.d.ts.map +1 -0
- package/bin/tools/session/SessionTools.js +111 -0
- package/bin/tools/session/SessionTools.js.map +1 -0
- package/bin/types/agent/AgentOptions.d.ts +10 -1
- package/bin/types/agent/AgentOptions.d.ts.map +1 -1
- package/bin/types/agent/AgentSessionCollection.d.ts +17 -6
- package/bin/types/agent/AgentSessionCollection.d.ts.map +1 -1
- package/bin/types/agent/AgentSessionCollection.js +1 -1
- package/bin/types/agent/AgentSessionInput.d.ts +14 -0
- package/bin/types/agent/AgentSessionInput.d.ts.map +1 -0
- package/bin/types/agent/AgentSessionInput.js +9 -0
- package/bin/types/agent/AgentSessionInput.js.map +1 -0
- package/bin/types/agent/SessionActor.d.ts +16 -5
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/agent/SessionContext.d.ts +26 -0
- package/bin/types/agent/SessionContext.d.ts.map +1 -0
- package/bin/types/agent/SessionContext.js +12 -0
- package/bin/types/agent/SessionContext.js.map +1 -0
- package/bin/types/agent/SessionState.d.ts +55 -0
- package/bin/types/agent/SessionState.d.ts.map +1 -0
- package/bin/types/agent/SessionState.js +15 -0
- package/bin/types/agent/SessionState.js.map +1 -0
- package/bin/types/agent/SessionTypes.d.ts +12 -1
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/agent/SessionWork.d.ts +53 -0
- package/bin/types/agent/SessionWork.d.ts.map +1 -0
- package/bin/types/agent/SessionWork.js +13 -0
- package/bin/types/agent/SessionWork.js.map +1 -0
- package/bin/types/session/SessionLocalState.d.ts +20 -1
- package/bin/types/session/SessionLocalState.d.ts.map +1 -1
- package/bin/types/session/SessionLoop.d.ts +6 -2
- package/bin/types/session/SessionLoop.d.ts.map +1 -1
- package/bin/types/session/SessionOptions.d.ts +9 -11
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/session/StepInput.d.ts +5 -5
- package/bin/types/session/StepInput.d.ts.map +1 -1
- package/bin/types/tools/session/SessionTools.d.ts +92 -0
- package/bin/types/tools/session/SessionTools.d.ts.map +1 -0
- package/bin/types/tools/session/SessionTools.js +12 -0
- package/bin/types/tools/session/SessionTools.js.map +1 -0
- package/package.json +2 -2
- package/scripts/session-executor-context-pressure.test.mjs +1 -1
- package/scripts/session-executor-failure-result.test.mjs +1 -1
- package/scripts/session-executor-max-steps.test.mjs +1 -1
- package/scripts/session-initialization.test.mjs +4 -2
- package/scripts/session-interaction-migration.test.mjs +2 -2
- package/scripts/session-prompt-idempotency.test.mjs +2 -2
- package/scripts/session-storage-sqlite.test.mjs +3 -3
- package/scripts/session-turn-context.test.mjs +4 -4
- package/src/agent/Agent.ts +64 -10
- package/src/agent/AgentSessionIdentity.ts +30 -0
- package/src/agent/AgentSessions.ts +119 -77
- package/src/agent/SessionWork.ts +235 -0
- package/src/index.ts +19 -0
- package/src/session/Session.ts +112 -29
- package/src/session/input/StepInput.ts +42 -7
- package/src/session/loop/SessionLoop.ts +7 -9
- package/src/session/loop/SessionState.ts +61 -1
- package/src/session/storage/Browse.ts +2 -1
- package/src/session/storage/LocalSessionStore.ts +7 -7
- package/src/tools/session/SessionToolSchemas.ts +61 -0
- package/src/tools/session/SessionTools.ts +170 -0
- package/src/types/agent/AgentOptions.ts +12 -1
- package/src/types/agent/AgentSessionCollection.ts +18 -7
- package/src/types/agent/AgentSessionInput.ts +15 -0
- package/src/types/agent/SessionActor.ts +23 -5
- package/src/types/agent/SessionContext.ts +29 -0
- package/src/types/agent/SessionState.ts +74 -0
- package/src/types/agent/SessionTypes.ts +13 -1
- package/src/types/agent/SessionWork.ts +57 -0
- package/src/types/session/SessionLocalState.ts +24 -1
- package/src/types/session/SessionLoop.ts +6 -2
- package/src/types/session/SessionOptions.ts +10 -14
- package/src/types/session/StepInput.ts +5 -5
- package/src/types/tools/session/SessionTools.ts +104 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionWork:Agent 会话安排与观察其他会话的实现。
|
|
3
|
+
*
|
|
4
|
+
* 职责说明(中文)
|
|
5
|
+
* - 把 AgentSessions 的能力投影成「一件活」的完整动作:开、看、答、停。
|
|
6
|
+
* - 只读取公开的摘要与消息投影,不缓存会话,也不持有存储。
|
|
7
|
+
*
|
|
8
|
+
* 关键点(中文)
|
|
9
|
+
* - 清单排除 Agent 自己的会话,否则清单里会出现「自己」。
|
|
10
|
+
* - `spawn` 不等待 Turn 结束:派活的意义就是 Agent 会话立刻能继续。
|
|
11
|
+
* - 详情按最近消息分页读取,让对话能回答「它做了什么」,而不把整段历史塞进上下文。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { SessionInteractionResponse, WorkspaceRuntime } from "@downcity/type";
|
|
15
|
+
import { is_agent_session_origin } from "@downcity/type";
|
|
16
|
+
import type { SessionWorkContract } from "@/types/agent/SessionWork.js";
|
|
17
|
+
import type {
|
|
18
|
+
SessionCheckDetailToolOutput,
|
|
19
|
+
SessionToolState,
|
|
20
|
+
SessionToolSummary,
|
|
21
|
+
} from "@/types/tools/session/SessionTools.js";
|
|
22
|
+
import type { AgentSession } from "@/types/agent/SessionActor.js";
|
|
23
|
+
|
|
24
|
+
/** 清单默认返回的会话数量上限。 */
|
|
25
|
+
const DEFAULT_SESSION_LIMIT = 20;
|
|
26
|
+
|
|
27
|
+
/** 详情默认返回的最近消息条数。 */
|
|
28
|
+
const DEFAULT_MESSAGE_LIMIT = 20;
|
|
29
|
+
|
|
30
|
+
/** 清单中的一条会话列表项;由 AgentSessions 摘要投影而来。 */
|
|
31
|
+
export interface SessionWorkListEntry {
|
|
32
|
+
/** 会话稳定标识。 */
|
|
33
|
+
session_id: string;
|
|
34
|
+
/** 用户可见标题;未生成时省略。 */
|
|
35
|
+
title?: string;
|
|
36
|
+
/** 最近更新的预览文本。 */
|
|
37
|
+
preview_text?: string;
|
|
38
|
+
/** 最近更新时间戳(ms)。 */
|
|
39
|
+
updated_at?: number;
|
|
40
|
+
/** 会话来源。 */
|
|
41
|
+
origin: { type: string };
|
|
42
|
+
/** 当前是否正在执行。 */
|
|
43
|
+
executing?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** SessionWork 构造参数。 */
|
|
47
|
+
export interface SessionWorkOptions {
|
|
48
|
+
/** 列出活动会话摘要,`limit` 由本模块归一化后传入。 */
|
|
49
|
+
list_sessions: (input: { limit: number }) => Promise<{
|
|
50
|
+
items: SessionWorkListEntry[];
|
|
51
|
+
}>;
|
|
52
|
+
/** 恢复单条会话的运行时实例。 */
|
|
53
|
+
get_session: (session_id: string, origin_type: string) => Promise<AgentSession>;
|
|
54
|
+
/** 新建一条属于当前 Agent 的会话。 */
|
|
55
|
+
create_session: (input?: { workspace?: WorkspaceRuntime }) => Promise<AgentSession>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Agent 会话的会话工作运行时。 */
|
|
59
|
+
export class SessionWork implements SessionWorkContract {
|
|
60
|
+
private readonly options: SessionWorkOptions;
|
|
61
|
+
|
|
62
|
+
constructor(options: SessionWorkOptions) {
|
|
63
|
+
this.options = options;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 开一条工作会话并立刻把任务交给它。 */
|
|
67
|
+
async spawn(input: {
|
|
68
|
+
query: string;
|
|
69
|
+
title?: string;
|
|
70
|
+
workspace?: WorkspaceRuntime;
|
|
71
|
+
}): Promise<{ session_id: string }> {
|
|
72
|
+
const query = String(input.query || "").trim();
|
|
73
|
+
if (!query) throw new Error("session_start requires a non-empty query");
|
|
74
|
+
const session = await this.options.create_session(
|
|
75
|
+
input.workspace ? { workspace: input.workspace } : undefined,
|
|
76
|
+
);
|
|
77
|
+
const title = String(input.title || "").trim();
|
|
78
|
+
if (title) await session.rename(title);
|
|
79
|
+
// 不等待 Turn 结束:派活的意义就是 Agent 会话立刻能继续。
|
|
80
|
+
void session.prompt({ query }).catch(() => undefined);
|
|
81
|
+
return { session_id: session.id };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 列出当前 Agent 除自己那条之外的全部会话。 */
|
|
85
|
+
async list(input?: { limit?: number }): Promise<SessionToolSummary[]> {
|
|
86
|
+
const page = await this.options.list_sessions({
|
|
87
|
+
limit: normalize_limit(input?.limit, DEFAULT_SESSION_LIMIT),
|
|
88
|
+
});
|
|
89
|
+
const summaries: SessionToolSummary[] = [];
|
|
90
|
+
for (const item of page.items) {
|
|
91
|
+
if (is_agent_session_origin(item.origin)) continue;
|
|
92
|
+
const pending_interaction_count = await this.count_pending(item);
|
|
93
|
+
summaries.push({
|
|
94
|
+
session_id: item.session_id,
|
|
95
|
+
...(item.title ? { title: item.title } : {}),
|
|
96
|
+
state: resolve_session_state({
|
|
97
|
+
executing: item.executing === true,
|
|
98
|
+
pending_interaction_count,
|
|
99
|
+
}),
|
|
100
|
+
...(item.preview_text ? { preview_text: item.preview_text } : {}),
|
|
101
|
+
...(typeof item.updated_at === "number" ? { updated_at: item.updated_at } : {}),
|
|
102
|
+
pending_interaction_count,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return summaries;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 读取单条会话的摘要、最近消息与待响应 Interaction。 */
|
|
109
|
+
async read(input: {
|
|
110
|
+
session_id: string;
|
|
111
|
+
limit?: number;
|
|
112
|
+
}): Promise<SessionCheckDetailToolOutput> {
|
|
113
|
+
const item = await this.require_list_entry(input.session_id);
|
|
114
|
+
const session = await this.options.get_session(item.session_id, item.origin.type);
|
|
115
|
+
const [messages, pending, status] = await Promise.all([
|
|
116
|
+
session.messages({ limit: normalize_limit(input.limit, DEFAULT_MESSAGE_LIMIT) }),
|
|
117
|
+
session.interactions(),
|
|
118
|
+
session.status(),
|
|
119
|
+
]);
|
|
120
|
+
const failure = resolve_failure_text(messages.items);
|
|
121
|
+
return {
|
|
122
|
+
mode: "detail",
|
|
123
|
+
session: {
|
|
124
|
+
session_id: item.session_id,
|
|
125
|
+
...(item.title ? { title: item.title } : {}),
|
|
126
|
+
state: resolve_session_state({
|
|
127
|
+
executing: status.state === "running",
|
|
128
|
+
pending_interaction_count: pending.length,
|
|
129
|
+
}),
|
|
130
|
+
...(item.preview_text ? { preview_text: item.preview_text } : {}),
|
|
131
|
+
...(typeof item.updated_at === "number" ? { updated_at: item.updated_at } : {}),
|
|
132
|
+
pending_interaction_count: pending.length,
|
|
133
|
+
},
|
|
134
|
+
messages: messages.items
|
|
135
|
+
.map((message) => project_message_text(message))
|
|
136
|
+
.filter((entry): entry is { role: "user" | "agent"; text: string } => Boolean(entry)),
|
|
137
|
+
pending_interactions: pending.map((interaction) => ({
|
|
138
|
+
interaction_id: interaction.interaction_id,
|
|
139
|
+
type: interaction.type,
|
|
140
|
+
...(interaction.title ? { title: interaction.title } : {}),
|
|
141
|
+
...(interaction.description ? { description: interaction.description } : {}),
|
|
142
|
+
})),
|
|
143
|
+
...(failure ? { failure } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 代答某条会话正在等待的用户响应。 */
|
|
148
|
+
async reply(input: {
|
|
149
|
+
session_id: string;
|
|
150
|
+
interaction_id: string;
|
|
151
|
+
response: SessionInteractionResponse;
|
|
152
|
+
}): Promise<void> {
|
|
153
|
+
const item = await this.require_list_entry(input.session_id);
|
|
154
|
+
const session = await this.options.get_session(item.session_id, item.origin.type);
|
|
155
|
+
const pending = await session.interactions();
|
|
156
|
+
if (!pending.some((entry) => entry.interaction_id === input.interaction_id)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Interaction is not pending on session "${input.session_id}": ${input.interaction_id}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
await session.respond({
|
|
162
|
+
interaction_id: input.interaction_id,
|
|
163
|
+
response: input.response,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 停止某条会话当前正在执行的 Turn。 */
|
|
168
|
+
async stop(input: { session_id: string }): Promise<void> {
|
|
169
|
+
const item = await this.require_list_entry(input.session_id);
|
|
170
|
+
const session = await this.options.get_session(item.session_id, item.origin.type);
|
|
171
|
+
await session.stop();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 按标识取出属于当前 Agent 的一条会话;不存在时明确失败。 */
|
|
175
|
+
private async require_list_entry(session_id_input: string): Promise<SessionWorkListEntry> {
|
|
176
|
+
const session_id = String(session_id_input || "").trim();
|
|
177
|
+
if (!session_id) throw new Error("Session id is required");
|
|
178
|
+
const page = await this.options.list_sessions({ limit: Number.MAX_SAFE_INTEGER });
|
|
179
|
+
const item = page.items.find((candidate) => candidate.session_id === session_id);
|
|
180
|
+
if (!item) throw new Error(`Session is not owned by this agent: ${session_id}`);
|
|
181
|
+
return item;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 读取一条会话当前待响应的 Interaction 数量;读取失败时按零处理。 */
|
|
185
|
+
private async count_pending(item: SessionWorkListEntry): Promise<number> {
|
|
186
|
+
try {
|
|
187
|
+
const session = await this.options.get_session(item.session_id, item.origin.type);
|
|
188
|
+
return (await session.interactions()).length;
|
|
189
|
+
} catch {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 归一化可选数量上限;非法值回退到默认值。 */
|
|
196
|
+
function normalize_limit(input: number | undefined, fallback: number): number {
|
|
197
|
+
return Number.isInteger(input) && (input as number) > 0 ? (input as number) : fallback;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 由执行态与待响应数量推导清单状态。 */
|
|
201
|
+
function resolve_session_state(input: {
|
|
202
|
+
/** 该会话当前是否在执行 Turn。 */
|
|
203
|
+
executing: boolean;
|
|
204
|
+
/** 当前待用户响应的 Interaction 数量。 */
|
|
205
|
+
pending_interaction_count: number;
|
|
206
|
+
}): SessionToolState {
|
|
207
|
+
if (input.pending_interaction_count > 0) return "waiting_for_user";
|
|
208
|
+
if (input.executing) return "running";
|
|
209
|
+
return "idle";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 把 canonical Message 投影为纯文本;没有可见文本时返回空。 */
|
|
213
|
+
function project_message_text(message: {
|
|
214
|
+
role: string;
|
|
215
|
+
parts: readonly { type: string; text?: string }[];
|
|
216
|
+
}): { role: "user" | "agent"; text: string } | null {
|
|
217
|
+
if (message.role !== "user" && message.role !== "agent") return null;
|
|
218
|
+
const text = message.parts
|
|
219
|
+
.flatMap((part) => (typeof part.text === "string" ? [part.text] : []))
|
|
220
|
+
.join("\n")
|
|
221
|
+
.trim();
|
|
222
|
+
return text ? { role: message.role, text } : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 读取最近一条 Agent 消息上的失败文本;没有失败时返回空。 */
|
|
226
|
+
function resolve_failure_text(
|
|
227
|
+
messages: readonly {
|
|
228
|
+
role: string;
|
|
229
|
+
parts: readonly { type: string; message?: string }[];
|
|
230
|
+
}[],
|
|
231
|
+
): string | undefined {
|
|
232
|
+
const latest_agent = [...messages].reverse().find((message) => message.role === "agent");
|
|
233
|
+
const error = latest_agent?.parts.find((part) => part.type === "error");
|
|
234
|
+
return error?.message ? error.message : undefined;
|
|
235
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,25 @@
|
|
|
9
9
|
|
|
10
10
|
// Agent 入口
|
|
11
11
|
export { Agent } from "./agent/Agent.js";
|
|
12
|
+
export type { AgentSessionInput } from "./types/agent/AgentSessionInput.js";
|
|
13
|
+
export type { SessionWorkContract } from "./types/agent/SessionWork.js";
|
|
14
|
+
export type { SessionContextResolution } from "./types/agent/SessionContext.js";
|
|
15
|
+
export type {
|
|
16
|
+
AgentSessionStateKey,
|
|
17
|
+
AgentSessionStateValues,
|
|
18
|
+
RemoteSessionStateKey,
|
|
19
|
+
RemoteSessionStateValues,
|
|
20
|
+
} from "./types/agent/SessionState.js";
|
|
21
|
+
export type {
|
|
22
|
+
SessionCheckDetailToolOutput,
|
|
23
|
+
SessionCheckListToolOutput,
|
|
24
|
+
SessionCheckToolOutput,
|
|
25
|
+
SessionReplyToolOutput,
|
|
26
|
+
SessionStartToolOutput,
|
|
27
|
+
SessionStopToolOutput,
|
|
28
|
+
SessionToolState,
|
|
29
|
+
SessionToolSummary,
|
|
30
|
+
} from "./types/tools/session/SessionTools.js";
|
|
12
31
|
export { Group } from "./group/Group.js";
|
|
13
32
|
export type {
|
|
14
33
|
DispatchAssignment,
|
package/src/session/Session.ts
CHANGED
|
@@ -45,8 +45,12 @@ import {
|
|
|
45
45
|
import { SessionLoop } from "@/session/loop/SessionLoop.js";
|
|
46
46
|
import { SessionQueue } from "@/session/loop/SessionQueue.js";
|
|
47
47
|
import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
|
|
48
|
+
import type {
|
|
49
|
+
AgentSessionStateKey,
|
|
50
|
+
AgentSessionStateValues,
|
|
51
|
+
} from "@/types/agent/SessionState.js";
|
|
48
52
|
import type { SessionOptions } from "@/types/session/SessionOptions.js";
|
|
49
|
-
import type { ToolCallContext, ToolHookSet } from "@downcity/type";
|
|
53
|
+
import type { ToolCallContext, ToolHookSet, WorkspaceRuntime } from "@downcity/type";
|
|
50
54
|
import { SessionInteractions } from "@/session/messages/SessionInteractions.js";
|
|
51
55
|
import { DefaultSessionComposer } from "@/session/input/composer/DefaultSessionComposer.js";
|
|
52
56
|
import type { SessionComposer } from "@/types/session/SessionComposer.js";
|
|
@@ -76,12 +80,15 @@ export class Session implements AgentSession {
|
|
|
76
80
|
readonly agent_name: string;
|
|
77
81
|
/** 当前 Agent 一句话能力描述;进入工具与扩展的调用环境。 */
|
|
78
82
|
readonly agent_description: string;
|
|
79
|
-
/**
|
|
80
|
-
|
|
83
|
+
/**
|
|
84
|
+
* 当前 Session 正在使用的 Workspace ID。
|
|
85
|
+
*
|
|
86
|
+
* 关键点(中文):这是可变的会话状态,不是身份;切换目录后随之改变。
|
|
87
|
+
*/
|
|
88
|
+
workspace_id?: string;
|
|
81
89
|
/** 当前 Session 的创建来源。 */
|
|
82
90
|
readonly origin: SessionOptions["origin"];
|
|
83
91
|
|
|
84
|
-
private readonly workspace_path: string;
|
|
85
92
|
private readonly store: SessionStorage;
|
|
86
93
|
private readonly create_session_store: SessionOptions["create_session_store"];
|
|
87
94
|
private readonly register_forked_session: SessionOptions["register_forked_session"];
|
|
@@ -94,9 +101,6 @@ export class Session implements AgentSession {
|
|
|
94
101
|
/** 当前执行面生效的审批模式;在 Step 检查点从 configured 同步。 */
|
|
95
102
|
private effective_approval_mode: SessionApprovalMode = "ask";
|
|
96
103
|
private readonly local_state: SessionLocalState;
|
|
97
|
-
private readonly get_workspace_env: SessionOptions["get_workspace_env"];
|
|
98
|
-
/** 读取当前 Session 绑定的 Workspace 实例。 */
|
|
99
|
-
private readonly get_workspace: SessionOptions["get_workspace"];
|
|
100
104
|
private readonly get_agent_model: SessionOptions["get_agent_model"];
|
|
101
105
|
private readonly get_hooks: SessionOptions["get_hooks"];
|
|
102
106
|
private readonly get_instruction_system_blocks:
|
|
@@ -116,16 +120,13 @@ export class Session implements AgentSession {
|
|
|
116
120
|
this.agent_id = String(options.agent_id || "").trim();
|
|
117
121
|
this.agent_name = String(options.agent_name || "").trim() || this.agent_id;
|
|
118
122
|
this.agent_description = String(options.agent_description || "").trim();
|
|
119
|
-
this.workspace_id = String(options.
|
|
123
|
+
this.workspace_id = String(options.workspace?.id || "").trim() || undefined;
|
|
120
124
|
this.origin = options.origin;
|
|
121
|
-
this.workspace_path = String(options.workspace_path || "").trim();
|
|
122
125
|
this.store = options.store;
|
|
123
126
|
this.create_session_store = options.create_session_store;
|
|
124
127
|
this.register_forked_session = options.register_forked_session;
|
|
125
128
|
this.get_tools = options.get_tools;
|
|
126
129
|
this.logger = options.logger;
|
|
127
|
-
this.get_workspace_env = options.get_workspace_env;
|
|
128
|
-
this.get_workspace = options.get_workspace;
|
|
129
130
|
this.get_agent_model = options.get_agent_model;
|
|
130
131
|
this.get_hooks = options.get_hooks;
|
|
131
132
|
this.get_instruction_system_blocks = options.get_instruction_system_blocks;
|
|
@@ -136,7 +137,7 @@ export class Session implements AgentSession {
|
|
|
136
137
|
if (!this.agent_id) {
|
|
137
138
|
throw new Error("Session requires a non-empty agent_id");
|
|
138
139
|
}
|
|
139
|
-
if (!
|
|
140
|
+
if (!String(options.workspace_path || "").trim()) {
|
|
140
141
|
throw new Error("Session requires a non-empty workspace_path");
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -155,22 +156,24 @@ export class Session implements AgentSession {
|
|
|
155
156
|
// 审批入口读执行面当前模式;configured 与 effective 的区别保留。
|
|
156
157
|
read_approval_mode: () => this.effective_approval_mode,
|
|
157
158
|
});
|
|
158
|
-
this.local_state = create_session_local_state(
|
|
159
|
+
this.local_state = create_session_local_state({
|
|
160
|
+
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
161
|
+
fallback_workspace_path: String(options.workspace_path || "").trim(),
|
|
162
|
+
});
|
|
159
163
|
this.step_input = new StepInput({
|
|
160
164
|
agent_id: this.agent_id,
|
|
161
165
|
agent_name: this.agent_name,
|
|
162
166
|
agent_description: this.agent_description,
|
|
163
167
|
session_id: this.id,
|
|
164
168
|
session_origin: this.origin,
|
|
165
|
-
|
|
169
|
+
get_workspace_path: () => this.get_workspace_path(),
|
|
166
170
|
store: this.store,
|
|
167
171
|
composer: this.composer,
|
|
168
172
|
get_tools: this.get_tools,
|
|
169
173
|
instruction_system_blocks: options.instruction_system_blocks,
|
|
170
174
|
get_instruction_system_blocks: this.get_instruction_system_blocks,
|
|
171
175
|
get_hooks: this.get_hooks,
|
|
172
|
-
|
|
173
|
-
get_workspace: this.get_workspace,
|
|
176
|
+
get_workspace: () => this.local_state.current_workspace,
|
|
174
177
|
get_model: () => this.get_model(),
|
|
175
178
|
get_model_context_window: () => this.get_model_context_window(),
|
|
176
179
|
get_created_at: () => this.local_state.created_at,
|
|
@@ -192,7 +195,7 @@ export class Session implements AgentSession {
|
|
|
192
195
|
this.session_loop = new SessionLoop({
|
|
193
196
|
session_id: this.id,
|
|
194
197
|
session_origin: this.origin,
|
|
195
|
-
|
|
198
|
+
get_workspace_path: () => this.get_workspace_path(),
|
|
196
199
|
step_input: this.step_input,
|
|
197
200
|
advance_context: async (trigger) => await this.advance_context(trigger),
|
|
198
201
|
maintain_context: async () => {
|
|
@@ -260,9 +263,45 @@ export class Session implements AgentSession {
|
|
|
260
263
|
|
|
261
264
|
/**
|
|
262
265
|
* 读取当前 session 配置快照。
|
|
266
|
+
*
|
|
267
|
+
* 删除说明:它与 `get("model")` / `get("model_label")` 重复;
|
|
268
|
+
* 读取统一走 `get(key)`,不再保留属性形式的入口。
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 读取当前 Session 状态。
|
|
273
|
+
*
|
|
274
|
+
* 关键点(中文)
|
|
275
|
+
* - 持久化字段与运行时实例共用这一个入口;键名即语义。
|
|
276
|
+
* - 只读,不产生副作用:不解析、不落盘、不发布 mutation。
|
|
263
277
|
*/
|
|
264
|
-
get
|
|
265
|
-
|
|
278
|
+
get<K extends AgentSessionStateKey>(key: K): AgentSessionStateValues[K] {
|
|
279
|
+
switch (key) {
|
|
280
|
+
case "workspace":
|
|
281
|
+
return this.local_state.current_workspace as AgentSessionStateValues[K];
|
|
282
|
+
case "workspace_id":
|
|
283
|
+
return this.workspace_id as AgentSessionStateValues[K];
|
|
284
|
+
case "model":
|
|
285
|
+
return this.get_selected_model() as AgentSessionStateValues[K];
|
|
286
|
+
case "model_label":
|
|
287
|
+
return this.state.get_config().model_label as AgentSessionStateValues[K];
|
|
288
|
+
case "model_context_window":
|
|
289
|
+
return this.state.get_config().model_context_window as AgentSessionStateValues[K];
|
|
290
|
+
case "approval_mode":
|
|
291
|
+
return this.state.get_approval_mode() as AgentSessionStateValues[K];
|
|
292
|
+
case "title":
|
|
293
|
+
return this.local_state.title as AgentSessionStateValues[K];
|
|
294
|
+
case "timezone":
|
|
295
|
+
return this.local_state.timezone as AgentSessionStateValues[K];
|
|
296
|
+
case "created_at":
|
|
297
|
+
return this.local_state.created_at as AgentSessionStateValues[K];
|
|
298
|
+
case "updated_at":
|
|
299
|
+
return this.local_state.updated_at as AgentSessionStateValues[K];
|
|
300
|
+
default: {
|
|
301
|
+
const unreachable: never = key;
|
|
302
|
+
throw new Error(`Unsupported session state key: ${String(unreachable)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
266
305
|
}
|
|
267
306
|
|
|
268
307
|
/**
|
|
@@ -272,8 +311,8 @@ export class Session implements AgentSession {
|
|
|
272
311
|
input: AgentSessionSetInput,
|
|
273
312
|
options?: AgentSessionSetOptions,
|
|
274
313
|
): Promise<void> {
|
|
275
|
-
if (!input.model && !input.security) {
|
|
276
|
-
throw new Error("session.set requires model or
|
|
314
|
+
if (!input.model && !input.security && input.workspace === undefined) {
|
|
315
|
+
throw new Error("session.set requires model, security, or workspace");
|
|
277
316
|
}
|
|
278
317
|
const requested_approval_mode = input.security?.approval_mode;
|
|
279
318
|
if (
|
|
@@ -296,11 +335,28 @@ export class Session implements AgentSession {
|
|
|
296
335
|
const model_result = input.model
|
|
297
336
|
? await this.state.set_model(input.model)
|
|
298
337
|
: undefined;
|
|
338
|
+
const workspace_changed = input.workspace !== undefined
|
|
339
|
+
? await this.state.apply_workspace(input.workspace ?? undefined)
|
|
340
|
+
: false;
|
|
341
|
+
const next_workspace = this.local_state.current_workspace;
|
|
299
342
|
const next_approval_mode = requested_approval_mode;
|
|
300
343
|
const security_changed = next_approval_mode !== undefined
|
|
301
344
|
? await this.state.set_approval_mode(next_approval_mode)
|
|
302
345
|
: false;
|
|
303
|
-
if (!model_result && !security_changed) return;
|
|
346
|
+
if (!model_result && !security_changed && !workspace_changed) return;
|
|
347
|
+
if (workspace_changed) {
|
|
348
|
+
this.workspace_id = next_workspace?.id;
|
|
349
|
+
}
|
|
350
|
+
if (workspace_changed && publish_mutation) {
|
|
351
|
+
this.events.publish({
|
|
352
|
+
mutation_id: generate_id(),
|
|
353
|
+
variant: "session",
|
|
354
|
+
type: "workspace",
|
|
355
|
+
session_id: this.id,
|
|
356
|
+
created_at: Date.now(),
|
|
357
|
+
...(next_workspace ? { workspace_id: next_workspace.id } : {}),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
304
360
|
if (model_result?.changed && publish_mutation) {
|
|
305
361
|
this.events.publish({
|
|
306
362
|
mutation_id: generate_id(),
|
|
@@ -320,6 +376,9 @@ export class Session implements AgentSession {
|
|
|
320
376
|
...(model_result?.changed
|
|
321
377
|
? [`model: ${String(model_result.config.model_label || "configured")}`]
|
|
322
378
|
: []),
|
|
379
|
+
...(workspace_changed
|
|
380
|
+
? [`workspace: ${String(next_workspace?.id || "none")}`]
|
|
381
|
+
: []),
|
|
323
382
|
...(security_changed
|
|
324
383
|
? [`security.approval_mode: ${String(next_approval_mode)}`]
|
|
325
384
|
: []),
|
|
@@ -431,7 +490,7 @@ export class Session implements AgentSession {
|
|
|
431
490
|
"",
|
|
432
491
|
).trim();
|
|
433
492
|
return build_session_info({
|
|
434
|
-
project_root: this.
|
|
493
|
+
project_root: this.get_workspace_path(),
|
|
435
494
|
agent_id: this.agent_id,
|
|
436
495
|
session_id: this.id,
|
|
437
496
|
metadata: {
|
|
@@ -449,6 +508,20 @@ export class Session implements AgentSession {
|
|
|
449
508
|
return await this.session_messages.list_messages(input);
|
|
450
509
|
}
|
|
451
510
|
|
|
511
|
+
/**
|
|
512
|
+
* 返回当前目录;没有项目上下文时为空。
|
|
513
|
+
*
|
|
514
|
+
* 关键点(中文):只读读取。修改走 `set({ workspace })`,与 `model` 同一手柄。
|
|
515
|
+
*/
|
|
516
|
+
get_workspace(): WorkspaceRuntime | undefined {
|
|
517
|
+
return this.local_state.current_workspace;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** 返回当前目录路径;没有 Workspace 时使用回退值。 */
|
|
521
|
+
private get_workspace_path(): string {
|
|
522
|
+
return this.state.get_workspace_path();
|
|
523
|
+
}
|
|
524
|
+
|
|
452
525
|
/**
|
|
453
526
|
* 读取当前 session 生效的 system 快照。
|
|
454
527
|
*/
|
|
@@ -552,7 +625,7 @@ export class Session implements AgentSession {
|
|
|
552
625
|
/** 当前 Turn 的取消信号。 */
|
|
553
626
|
readonly abort_signal: AbortSignal;
|
|
554
627
|
}): ToolCallContext {
|
|
555
|
-
const workspace = this.
|
|
628
|
+
const workspace = this.local_state.current_workspace;
|
|
556
629
|
return Object.freeze({
|
|
557
630
|
agent_id: this.agent_id,
|
|
558
631
|
agent_name: this.agent_name,
|
|
@@ -566,17 +639,28 @@ export class Session implements AgentSession {
|
|
|
566
639
|
turn_id: input.turn_id,
|
|
567
640
|
abort_signal: input.abort_signal,
|
|
568
641
|
interactions: this.session_interactions,
|
|
569
|
-
workspace_env: Object.freeze({ ...this.
|
|
642
|
+
workspace_env: Object.freeze({ ...this.read_workspace_env() }),
|
|
570
643
|
});
|
|
571
644
|
}
|
|
572
645
|
|
|
646
|
+
/**
|
|
647
|
+
* 读取当前目录的环境变量快照。
|
|
648
|
+
*
|
|
649
|
+
* 关键点(中文):环境属于目录(Workspace 实例),不是 Session 自身配置;没有目录时为空。
|
|
650
|
+
*/
|
|
651
|
+
private read_workspace_env(): Record<string, string> {
|
|
652
|
+
return { ...(this.local_state.current_workspace?.get_env() ?? {}) };
|
|
653
|
+
}
|
|
654
|
+
|
|
573
655
|
private create_fork_session(session_id: string): this {
|
|
574
656
|
return this.create_child_session({
|
|
575
657
|
agent_id: this.agent_id,
|
|
576
658
|
agent_name: this.agent_name,
|
|
577
659
|
agent_description: this.agent_description,
|
|
578
|
-
workspace_path: this.
|
|
579
|
-
...(this.
|
|
660
|
+
workspace_path: this.local_state.fallback_workspace_path,
|
|
661
|
+
...(this.local_state.current_workspace
|
|
662
|
+
? { workspace: this.local_state.current_workspace }
|
|
663
|
+
: {}),
|
|
580
664
|
origin: this.origin,
|
|
581
665
|
store: this.create_session_store(session_id),
|
|
582
666
|
create_session_store: this.create_session_store,
|
|
@@ -586,8 +670,7 @@ export class Session implements AgentSession {
|
|
|
586
670
|
logger: this.logger,
|
|
587
671
|
instruction_system_blocks: this.step_input.instruction_blocks(),
|
|
588
672
|
get_instruction_system_blocks: this.get_instruction_system_blocks,
|
|
589
|
-
|
|
590
|
-
get_workspace: this.get_workspace,
|
|
673
|
+
get_workspace: () => this.local_state.current_workspace,
|
|
591
674
|
get_hooks: this.get_hooks,
|
|
592
675
|
get_agent_model: this.get_agent_model,
|
|
593
676
|
composer: this.composer,
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
SessionHookContextBlock,
|
|
16
16
|
SessionTurnContextHookValue,
|
|
17
17
|
ToolCallContext,
|
|
18
|
+
WorkspaceRuntime,
|
|
18
19
|
} from "@downcity/type";
|
|
19
20
|
import type { BoundAgentTool, ToolCallSite } from "@/types/tool/BoundAgentTool.js";
|
|
20
21
|
import type {
|
|
@@ -39,6 +40,10 @@ import { resolve_session_power_system_blocks } from "@/session/input/SessionSyst
|
|
|
39
40
|
export class StepInput {
|
|
40
41
|
private readonly options: StepInputOptions;
|
|
41
42
|
private effective_instruction_blocks: AgentSessionSystemBlock[];
|
|
43
|
+
/** 当前目录对应的环境变量快照;由目录实例提供。 */
|
|
44
|
+
private readonly workspace_env_by_id = new Map<string, Record<string, string>>();
|
|
45
|
+
/** 当前目录变化时取消的旧订阅。 */
|
|
46
|
+
private workspace_env_unsubscribe?: () => void;
|
|
42
47
|
/** 当前 Session 首次组装后冻结的完整 system;null 表示尚未冻结。 */
|
|
43
48
|
private frozen_blocks: AgentSessionSystemBlock[] | null = null;
|
|
44
49
|
/** 当前 Session 的一次性初始化任务。 */
|
|
@@ -128,8 +133,7 @@ export class StepInput {
|
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
/** 把当前完整 system 显式固化到 instruction.md。 */
|
|
131
|
-
async snapshot(): Promise<void> {
|
|
132
|
-
await this.run_mutation(async () => {
|
|
136
|
+
async snapshot(): Promise<void> { await this.run_mutation(async () => {
|
|
133
137
|
const system_snapshot = await this.read_system();
|
|
134
138
|
await this.write_snapshot(system_snapshot.blocks);
|
|
135
139
|
});
|
|
@@ -163,7 +167,7 @@ export class StepInput {
|
|
|
163
167
|
session: {
|
|
164
168
|
agent_id: this.options.agent_id,
|
|
165
169
|
session_id: this.options.session_id,
|
|
166
|
-
project_root: this.options.
|
|
170
|
+
project_root: this.options.get_workspace_path(),
|
|
167
171
|
created_at: new Date(this.options.get_created_at()).toISOString(),
|
|
168
172
|
timezone: this.options.get_timezone(),
|
|
169
173
|
},
|
|
@@ -176,12 +180,38 @@ export class StepInput {
|
|
|
176
180
|
return this.effective_instruction_blocks.map((block) => ({ ...block }));
|
|
177
181
|
}
|
|
178
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 读取当前目录的环境变量。
|
|
185
|
+
*
|
|
186
|
+
* 关键点(中文)
|
|
187
|
+
* - 目录是可变会话配置,环境订阅也随之切换:首次遇到某个目录时建立订阅,
|
|
188
|
+
* 后续读取直接取其快照,不再重建。
|
|
189
|
+
* - 没有目录时返回空对象。
|
|
190
|
+
*/
|
|
191
|
+
private read_workspace_env(): Record<string, string> {
|
|
192
|
+
const workspace = this.options.get_workspace?.();
|
|
193
|
+
if (!workspace) {
|
|
194
|
+
this.workspace_env_unsubscribe?.();
|
|
195
|
+
this.workspace_env_unsubscribe = undefined;
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
const cached = this.workspace_env_by_id.get(workspace.id);
|
|
199
|
+
if (cached) return cached;
|
|
200
|
+
// 同一 Session 只保留当前目录的订阅,避免旧目录继续推送。
|
|
201
|
+
this.workspace_env_unsubscribe?.();
|
|
202
|
+
this.workspace_env_by_id.set(workspace.id, { ...workspace.get_env() });
|
|
203
|
+
this.workspace_env_unsubscribe = workspace.subscribe_env(() => {
|
|
204
|
+
this.workspace_env_by_id.set(workspace.id, { ...workspace.get_env() });
|
|
205
|
+
});
|
|
206
|
+
return this.workspace_env_by_id.get(workspace.id) as Record<string, string>;
|
|
207
|
+
}
|
|
208
|
+
|
|
179
209
|
/** 创建 Composer 共用的稳定 Session 身份快照。 */
|
|
180
210
|
identity(): SessionComposeIdentity {
|
|
181
211
|
return {
|
|
182
212
|
agent_id: this.options.agent_id,
|
|
183
213
|
session_id: this.options.session_id,
|
|
184
|
-
project_root: this.options.
|
|
214
|
+
project_root: this.options.get_workspace_path(),
|
|
185
215
|
created_at: this.options.get_created_at(),
|
|
186
216
|
timezone: this.options.get_timezone(),
|
|
187
217
|
};
|
|
@@ -196,7 +226,7 @@ export class StepInput {
|
|
|
196
226
|
const instruction_system_blocks = refresh_system
|
|
197
227
|
? this.options.get_instruction_system_blocks().map((block) => ({ ...block }))
|
|
198
228
|
: this.instruction_blocks();
|
|
199
|
-
const workspace_env = Object.freeze({ ...this.
|
|
229
|
+
const workspace_env = Object.freeze({ ...this.read_workspace_env() });
|
|
200
230
|
// Power system 参与 Compose,必须先看到当前检查点已确定的 env 与 instruction。
|
|
201
231
|
turn_context?.step.commit({
|
|
202
232
|
workspace_env,
|
|
@@ -401,7 +431,7 @@ export class StepInput {
|
|
|
401
431
|
private create_session_call_context(
|
|
402
432
|
workspace_env: Readonly<Record<string, string>>,
|
|
403
433
|
): ToolCallContext {
|
|
404
|
-
const workspace = this.
|
|
434
|
+
const workspace = this.resolve_active_workspace();
|
|
405
435
|
return Object.freeze({
|
|
406
436
|
agent_id: this.options.agent_id,
|
|
407
437
|
agent_name: this.options.agent_name,
|
|
@@ -424,7 +454,7 @@ export class StepInput {
|
|
|
424
454
|
): ToolCallContext {
|
|
425
455
|
const workspace_env = turn_context.step.workspace_env;
|
|
426
456
|
const agent_systems = turn_context.step.agent_systems;
|
|
427
|
-
const workspace = this.
|
|
457
|
+
const workspace = this.resolve_active_workspace();
|
|
428
458
|
return Object.freeze({
|
|
429
459
|
agent_id: this.options.agent_id,
|
|
430
460
|
agent_name: this.options.agent_name,
|
|
@@ -441,6 +471,11 @@ export class StepInput {
|
|
|
441
471
|
...(workspace_env ? { workspace_env } : {}),
|
|
442
472
|
});
|
|
443
473
|
}
|
|
474
|
+
|
|
475
|
+
/** 解析当前生效的 Workspace 实例。 */
|
|
476
|
+
private resolve_active_workspace(): WorkspaceRuntime | undefined {
|
|
477
|
+
return this.options.get_workspace?.();
|
|
478
|
+
}
|
|
444
479
|
}
|
|
445
480
|
|
|
446
481
|
/** 把 Turn pipeline 输出限制为低权限动态参考内容块。 */
|