@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.
Files changed (122) hide show
  1. package/bin/agent/Agent.d.ts +29 -2
  2. package/bin/agent/Agent.d.ts.map +1 -1
  3. package/bin/agent/Agent.js +56 -10
  4. package/bin/agent/Agent.js.map +1 -1
  5. package/bin/agent/AgentSessionIdentity.d.ts +19 -0
  6. package/bin/agent/AgentSessionIdentity.d.ts.map +1 -0
  7. package/bin/agent/AgentSessionIdentity.js +26 -0
  8. package/bin/agent/AgentSessionIdentity.js.map +1 -0
  9. package/bin/agent/AgentSessions.d.ts +35 -9
  10. package/bin/agent/AgentSessions.d.ts.map +1 -1
  11. package/bin/agent/AgentSessions.js +110 -63
  12. package/bin/agent/AgentSessions.js.map +1 -1
  13. package/bin/agent/SessionWork.d.ts +85 -0
  14. package/bin/agent/SessionWork.d.ts.map +1 -0
  15. package/bin/agent/SessionWork.js +165 -0
  16. package/bin/agent/SessionWork.js.map +1 -0
  17. package/bin/index.d.ts +5 -0
  18. package/bin/index.d.ts.map +1 -1
  19. package/bin/index.js.map +1 -1
  20. package/bin/session/Session.d.ts +34 -8
  21. package/bin/session/Session.d.ts.map +1 -1
  22. package/bin/session/Session.js +102 -27
  23. package/bin/session/Session.js.map +1 -1
  24. package/bin/session/input/StepInput.d.ts +15 -0
  25. package/bin/session/input/StepInput.d.ts.map +1 -1
  26. package/bin/session/input/StepInput.js +39 -5
  27. package/bin/session/input/StepInput.js.map +1 -1
  28. package/bin/session/loop/SessionLoop.d.ts +2 -1
  29. package/bin/session/loop/SessionLoop.d.ts.map +1 -1
  30. package/bin/session/loop/SessionLoop.js +7 -9
  31. package/bin/session/loop/SessionLoop.js.map +1 -1
  32. package/bin/session/loop/SessionState.d.ts +21 -1
  33. package/bin/session/loop/SessionState.d.ts.map +1 -1
  34. package/bin/session/loop/SessionState.js +54 -1
  35. package/bin/session/loop/SessionState.js.map +1 -1
  36. package/bin/session/storage/Browse.d.ts.map +1 -1
  37. package/bin/session/storage/Browse.js +2 -2
  38. package/bin/session/storage/Browse.js.map +1 -1
  39. package/bin/session/storage/LocalSessionStore.d.ts.map +1 -1
  40. package/bin/session/storage/LocalSessionStore.js +7 -7
  41. package/bin/session/storage/LocalSessionStore.js.map +1 -1
  42. package/bin/tools/session/SessionToolSchemas.d.ts +34 -0
  43. package/bin/tools/session/SessionToolSchemas.d.ts.map +1 -0
  44. package/bin/tools/session/SessionToolSchemas.js +39 -0
  45. package/bin/tools/session/SessionToolSchemas.js.map +1 -0
  46. package/bin/tools/session/SessionTools.d.ts +57 -0
  47. package/bin/tools/session/SessionTools.d.ts.map +1 -0
  48. package/bin/tools/session/SessionTools.js +111 -0
  49. package/bin/tools/session/SessionTools.js.map +1 -0
  50. package/bin/types/agent/AgentOptions.d.ts +10 -1
  51. package/bin/types/agent/AgentOptions.d.ts.map +1 -1
  52. package/bin/types/agent/AgentSessionCollection.d.ts +17 -6
  53. package/bin/types/agent/AgentSessionCollection.d.ts.map +1 -1
  54. package/bin/types/agent/AgentSessionCollection.js +1 -1
  55. package/bin/types/agent/AgentSessionInput.d.ts +14 -0
  56. package/bin/types/agent/AgentSessionInput.d.ts.map +1 -0
  57. package/bin/types/agent/AgentSessionInput.js +9 -0
  58. package/bin/types/agent/AgentSessionInput.js.map +1 -0
  59. package/bin/types/agent/SessionActor.d.ts +16 -5
  60. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  61. package/bin/types/agent/SessionContext.d.ts +26 -0
  62. package/bin/types/agent/SessionContext.d.ts.map +1 -0
  63. package/bin/types/agent/SessionContext.js +12 -0
  64. package/bin/types/agent/SessionContext.js.map +1 -0
  65. package/bin/types/agent/SessionState.d.ts +55 -0
  66. package/bin/types/agent/SessionState.d.ts.map +1 -0
  67. package/bin/types/agent/SessionState.js +15 -0
  68. package/bin/types/agent/SessionState.js.map +1 -0
  69. package/bin/types/agent/SessionTypes.d.ts +12 -1
  70. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  71. package/bin/types/agent/SessionWork.d.ts +53 -0
  72. package/bin/types/agent/SessionWork.d.ts.map +1 -0
  73. package/bin/types/agent/SessionWork.js +13 -0
  74. package/bin/types/agent/SessionWork.js.map +1 -0
  75. package/bin/types/session/SessionLocalState.d.ts +20 -1
  76. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  77. package/bin/types/session/SessionLoop.d.ts +6 -2
  78. package/bin/types/session/SessionLoop.d.ts.map +1 -1
  79. package/bin/types/session/SessionOptions.d.ts +9 -11
  80. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  81. package/bin/types/session/StepInput.d.ts +5 -5
  82. package/bin/types/session/StepInput.d.ts.map +1 -1
  83. package/bin/types/tools/session/SessionTools.d.ts +92 -0
  84. package/bin/types/tools/session/SessionTools.d.ts.map +1 -0
  85. package/bin/types/tools/session/SessionTools.js +12 -0
  86. package/bin/types/tools/session/SessionTools.js.map +1 -0
  87. package/package.json +2 -2
  88. package/scripts/session-executor-context-pressure.test.mjs +1 -1
  89. package/scripts/session-executor-failure-result.test.mjs +1 -1
  90. package/scripts/session-executor-max-steps.test.mjs +1 -1
  91. package/scripts/session-initialization.test.mjs +4 -2
  92. package/scripts/session-interaction-migration.test.mjs +2 -2
  93. package/scripts/session-prompt-idempotency.test.mjs +2 -2
  94. package/scripts/session-storage-sqlite.test.mjs +3 -3
  95. package/scripts/session-turn-context.test.mjs +4 -4
  96. package/src/agent/Agent.ts +64 -10
  97. package/src/agent/AgentSessionIdentity.ts +30 -0
  98. package/src/agent/AgentSessions.ts +119 -77
  99. package/src/agent/SessionWork.ts +235 -0
  100. package/src/index.ts +19 -0
  101. package/src/session/Session.ts +112 -29
  102. package/src/session/input/StepInput.ts +42 -7
  103. package/src/session/loop/SessionLoop.ts +7 -9
  104. package/src/session/loop/SessionState.ts +61 -1
  105. package/src/session/storage/Browse.ts +2 -1
  106. package/src/session/storage/LocalSessionStore.ts +7 -7
  107. package/src/tools/session/SessionToolSchemas.ts +61 -0
  108. package/src/tools/session/SessionTools.ts +170 -0
  109. package/src/types/agent/AgentOptions.ts +12 -1
  110. package/src/types/agent/AgentSessionCollection.ts +18 -7
  111. package/src/types/agent/AgentSessionInput.ts +15 -0
  112. package/src/types/agent/SessionActor.ts +23 -5
  113. package/src/types/agent/SessionContext.ts +29 -0
  114. package/src/types/agent/SessionState.ts +74 -0
  115. package/src/types/agent/SessionTypes.ts +13 -1
  116. package/src/types/agent/SessionWork.ts +57 -0
  117. package/src/types/session/SessionLocalState.ts +24 -1
  118. package/src/types/session/SessionLoop.ts +6 -2
  119. package/src/types/session/SessionOptions.ts +10 -14
  120. package/src/types/session/StepInput.ts +5 -5
  121. package/src/types/tools/session/SessionTools.ts +104 -0
  122. package/tsconfig.tsbuildinfo +1 -1
@@ -17,7 +17,7 @@ test("SessionTurnContext 在检查点消费输入并封装输出缓冲", async (
17
17
  const internal_messages = [];
18
18
  const context = create_session_turn_context({
19
19
  session_id: "session-context-test",
20
- session_origin: { type: "chat" },
20
+ session_origin: { type: "work" },
21
21
  turn_id: "turn-context-test",
22
22
  project_root: "/workspace",
23
23
  commit_step_input: async () => {
@@ -53,7 +53,7 @@ test("SessionTurnContext 在检查点消费输入并封装输出缓冲", async (
53
53
  test("SessionTurnContext 只按顺序收集当前 Turn 的 Tool effects", () => {
54
54
  const context = create_session_turn_context({
55
55
  session_id: "session-context-test",
56
- session_origin: { type: "chat" },
56
+ session_origin: { type: "work" },
57
57
  turn_id: "turn-context-test",
58
58
  });
59
59
  context.effects.append([{
@@ -109,7 +109,7 @@ test("SessionTurnContext 把 Workspace env 与 Agent systems 固化为只读快
109
109
  test("SessionTurnContext 不再承担 Power hook 装配", () => {
110
110
  const context = create_session_turn_context({
111
111
  session_id: "session-context-test",
112
- session_origin: { type: "chat" },
112
+ session_origin: { type: "work" },
113
113
  turn_id: "turn-context-test",
114
114
  });
115
115
  for (const removed of ["replace_hooks", "hook_context", "release_extensions", "hooks"]) {
@@ -120,7 +120,7 @@ test("SessionTurnContext 不再承担 Power hook 装配", () => {
120
120
  test("SessionTurnContext 在整个 Turn 中只解析一次 Power Context", async () => {
121
121
  const context = create_session_turn_context({
122
122
  session_id: "session-context-test",
123
- session_origin: { type: "chat" },
123
+ session_origin: { type: "work" },
124
124
  turn_id: "turn-context-test",
125
125
  });
126
126
  let resolve_count = 0;
@@ -2,9 +2,10 @@
2
2
  * Agent:身份、模型、指令与 Session 的主体对象。
3
3
  *
4
4
  * 职责说明(中文)
5
- * - Agent 不绑定 Workspace;调用方通过 `agent.sessions.create({ workspace })` 选择本次执行环境。
5
+ * - Agent 不绑定 Workspace;`workspace` 只是创建 Session 时的默认目录,可在会话中切换。
6
6
  * - 能力与 Transport 由容器持有,Agent 只收下容器在绑定时推送的能力,不持有容器引用。
7
- * - Session 由 AgentSessions 统一持有;Workspace 只在单个 Session 创建时提供执行资源。
7
+ * - Session 由 AgentSessions 统一持有;Workspace 只在单个 Session 创建或恢复时提供。
8
+ * - Agent 自己那条会话由 `session()` 暴露;它是一条普通 Session。
8
9
  */
9
10
 
10
11
  import type {
@@ -16,6 +17,7 @@ import type {
16
17
  ToolHookSet,
17
18
  WorkspaceRuntime,
18
19
  } from "@downcity/type";
20
+
19
21
  import {
20
22
  create_instruction_system_blocks,
21
23
  normalize_instruction_input,
@@ -41,6 +43,11 @@ import {
41
43
  } from "@/session/input/SessionSystem.js";
42
44
  import type { AgentSessionSystemBlock } from "@/types/agent/SessionTypes.js";
43
45
  import { resolve_system_timezone } from "@/session/storage/Metadata.js";
46
+ import { SessionWork } from "@/agent/SessionWork.js";
47
+ import { is_agent_session_origin, SESSION_ORIGIN_TYPE_WORK } from "@downcity/type";
48
+ import { create_session_tools } from "@/tools/session/SessionTools.js";
49
+ import type { AgentSessionInput } from "@/types/agent/AgentSessionInput.js";
50
+ import type { AgentSession } from "@/types/agent/SessionActor.js";
44
51
 
45
52
  /** SDK Agent 主体。 */
46
53
  export class Agent {
@@ -56,6 +63,15 @@ export class Agent {
56
63
  /** Agent 默认模型;Session 可以显式覆盖。 */
57
64
  readonly model?: ModelClient;
58
65
 
66
+ /**
67
+ * 新建 Session 时注入的默认目录。
68
+ *
69
+ * 关键点(中文)
70
+ * - 它是宿主在构造 Agent 时声明的默认值,不是绑定;单个 Session 可用 `set({ workspace })` 换掉。
71
+ * - 省略时新建的 Session 没有项目上下文:没有文件工具与 Shell,纯模型对话仍可用。
72
+ */
73
+ readonly workspace?: WorkspaceRuntime;
74
+
59
75
  /** Agent 面向用户的 Session 创建入口。 */
60
76
  readonly sessions: AgentSessionCollection;
61
77
 
@@ -77,6 +93,14 @@ export class Agent {
77
93
  /** Agent 自有检查点处理器;与容器提供的 Power hooks 合并使用。 */
78
94
  private readonly custom_hooks: ToolHookSet;
79
95
 
96
+ /**
97
+ * Agent 会话可用的会话工具;延迟创建并复用。
98
+ *
99
+ * 关键点(中文):工具闭包住 AgentSessions,因此不需要从 ToolCallContext 回查主体;
100
+ * 它们只在 `origin.type === "agent"` 的会话上下文里注册。
101
+ */
102
+ private session_tools?: Record<string, Tool>;
103
+
80
104
  /** Agent 释放状态。 */
81
105
  private dispose_promise?: Promise<void>;
82
106
 
@@ -98,6 +122,7 @@ export class Agent {
98
122
  this.name = String(options.name || "").trim() || this.id;
99
123
  this.description = String(options.description || "").trim();
100
124
  this.model = options.model;
125
+ this.workspace = options.workspace;
101
126
  this.instruction = normalize_instruction_input(options.instruction);
102
127
  this.session_class = options.session_class;
103
128
  this.session_composer = options.session_composer;
@@ -114,26 +139,29 @@ export class Agent {
114
139
  get_agent_model: () => this.model,
115
140
  session_class: this.session_class,
116
141
  session_composer: this.session_composer,
117
- resolve_session_context: (workspace) => {
142
+ resolve_session_context: (input) => {
118
143
  const storage = this.resolve_storage();
144
+ // 默认目录只用于新建会话;恢复时目录由宿主在 get() 时注入,
145
+ // 用默认值顶替会变成一次静默换目录。
146
+ const workspace = input?.workspace
147
+ ?? (input?.creating ? this.workspace : undefined);
148
+ const is_agent_session = is_agent_session_origin(input?.origin);
119
149
  if (!workspace) return {
120
150
  workspace_path: ".",
121
151
  logger: this.logger,
122
- get_tools: () => this.resolve_tools(),
123
- get_workspace_env: () => ({}),
152
+ get_tools: () => this.resolve_tools(undefined, is_agent_session),
124
153
  get_workspace: () => undefined,
125
154
  get_hooks: () => this.resolve_hooks(),
126
155
  store: storage.sessions,
127
156
  };
128
157
  // 静态 Workspace Tool 与 Agent Tool 的命名冲突属于 Session 创建不变量,
129
158
  // 在装配时立即失败;执行时仍通过 getter 读取最新宿主扩展。
130
- this.resolve_tools(workspace);
159
+ this.resolve_tools(workspace, is_agent_session);
131
160
  return {
132
161
  workspace_path: workspace.path,
133
162
  workspace_id: workspace.id,
134
163
  logger: this.logger,
135
- get_tools: () => this.resolve_tools(workspace),
136
- get_workspace_env: () => workspace.get_env(),
164
+ get_tools: () => this.resolve_tools(workspace, is_agent_session),
137
165
  get_workspace: () => workspace,
138
166
  get_hooks: () => this.resolve_hooks(),
139
167
  store: storage.sessions,
@@ -154,6 +182,18 @@ export class Agent {
154
182
  return [...this.instruction];
155
183
  }
156
184
 
185
+ /**
186
+ * 取用 Agent 自己那条会话,不存在时先建立。
187
+ *
188
+ * 关键点(中文)
189
+ * - 它就是一条普通 Session:工作区能力、set/get、消息、审批与其他会话完全一致。
190
+ * - 不接收 Workspace:目录在创建时注入,之后用 `session.set({ workspace })` 切换。
191
+ */ async session(input?: AgentSessionInput): Promise<AgentSession> {
192
+ return await this.session_manager.self(
193
+ input?.workspace ? { workspace: input.workspace } : undefined,
194
+ );
195
+ }
196
+
157
197
  /** 返回 Agent 级日志器。 */
158
198
  get_logger(): Logger {
159
199
  return this.logger;
@@ -256,7 +296,7 @@ export class Agent {
256
296
  input.instruction_system_blocks.map((block) => block.content),
257
297
  ),
258
298
  session_id: input.session_id,
259
- session_origin: { type: "chat" },
299
+ session_origin: { type: SESSION_ORIGIN_TYPE_WORK },
260
300
  workspace: input.workspace,
261
301
  messages: Object.freeze([]),
262
302
  workspace_env: Object.freeze({ ...input.workspace.get_env() }),
@@ -318,14 +358,28 @@ export class Agent {
318
358
  * - 容器 Power 工具从环境句柄实时读取,不缓存;容器增删 Power 后下一次求值即生效。
319
359
  * - 静态 Workspace Tool 与 Agent Tool 的命名冲突属于 Session 创建不变量,装配时立即失败。
320
360
  */
321
- private resolve_tools(workspace?: WorkspaceRuntime): Record<string, Tool> {
361
+ private resolve_tools(workspace?: WorkspaceRuntime, is_agent_session = false): Record<string, Tool> {
322
362
  const tools: Record<string, Tool> = {};
323
363
  if (workspace) register_tools(tools, workspace.tools, "WorkspaceTools");
324
364
  register_tools(tools, this.host?.power_tools ?? {}, "Power");
325
365
  register_tools(tools, this.custom_tools, "AgentOptions.tools");
366
+ if (is_agent_session) register_tools(tools, this.resolve_session_tools(), "AgentSessionTools");
326
367
  return tools;
327
368
  }
328
369
 
370
+ /** 返回 Agent 会话的会话工具集合;首次调用时创建。 */
371
+ private resolve_session_tools(): Record<string, Tool> {
372
+ if (this.session_tools) return this.session_tools;
373
+ const work = new SessionWork({
374
+ list_sessions: async (input) => await this.session_manager.list(input),
375
+ get_session: async (session_id, origin_type) =>
376
+ await this.session_manager.get(session_id, origin_type),
377
+ create_session: async (input) => await this.session_manager.create(input),
378
+ });
379
+ this.session_tools = create_session_tools(work);
380
+ return this.session_tools;
381
+ }
382
+
329
383
  /** 合并容器 Power hooks 与 Agent 自有 hooks。 */
330
384
  private resolve_hooks(): ToolHookSet {
331
385
  const power_hooks = this.host?.power_hooks;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Agent 自己那条会话的存取规则。
3
+ *
4
+ * 职责说明(中文)
5
+ * - Agent 自己就是一条 Session,与 work / task / group 会话是同一种东西。
6
+ * - 它没有专属字段、专属索引或专属存储:角色由 `origin.type = "agent"` 表达,
7
+ * 分区沿用 `sessions/<origin_type>/<session_id>/`,能力与其他会话完全一致。
8
+ * - 因此这里只有存取所需的两个小函数,不引入新概念。
9
+ *
10
+ * 关键点(中文)
11
+ * - 它的 `session_id` 等于 Agent 的 `id`:会话由它属于谁来标识,不需要人造常量。
12
+ * - 一个 Agent 至多一条,惰性创建;被归档或删除后下一次取用自然重建。
13
+ */
14
+
15
+ import {
16
+ SESSION_ORIGIN_TYPE_AGENT,
17
+ type SessionOrigin,
18
+ } from "@downcity/type";
19
+
20
+ /** Agent 自己那条会话的来源;不携带额外业务字段。 */
21
+ export function create_agent_session_origin(): SessionOrigin {
22
+ return Object.freeze({ type: SESSION_ORIGIN_TYPE_AGENT });
23
+ }
24
+
25
+ /** Agent 自己那条会话的稳定标识:就是 Agent 的 id。 */
26
+ export function resolve_agent_session_id(agent_id: string): string {
27
+ const resolved = String(agent_id || "").trim();
28
+ if (!resolved) throw new Error("Agent session requires a non-empty agent id");
29
+ return resolved;
30
+ }
@@ -37,7 +37,9 @@ import type { SessionStore } from "@/types/store/SessionStore.js";
37
37
  import type { SessionStorage } from "@/types/store/SessionStorage.js";
38
38
  import type { WorkspaceRuntime } from "@downcity/type";
39
39
  import type { SessionOrigin } from "@downcity/type";
40
- import { normalize_session_origin, normalize_session_origin_type } from "@downcity/type";
40
+ import { normalize_session_origin, normalize_session_origin_type, SESSION_ORIGIN_TYPE_WORK } from "@downcity/type";
41
+ import type { SessionContextResolution } from "@/types/agent/SessionContext.js";
42
+ import { create_agent_session_origin, resolve_agent_session_id } from "@/agent/AgentSessionIdentity.js";
41
43
 
42
44
  type AgentSessionsOptions = {
43
45
  /**
@@ -58,15 +60,14 @@ type AgentSessionsOptions = {
58
60
  /**
59
61
  * 按 Session 解析执行上下文。
60
62
  *
61
- * AgentSessions 是 Agent 唯一的 Session 集合;Workspace 相关能力不能
62
- * 固定在集合实例上,而应在创建或恢复单个 Session 时解析。
63
+ * AgentSessions 是 Agent 唯一的 Session 集合;Workspace 与工具集合不能
64
+ * 固定在集合实例上,而应在创建或恢复单个 Session 时按会话身份解析。
63
65
  */
64
- resolve_session_context: (workspace?: WorkspaceRuntime) => {
66
+ resolve_session_context: (input?: SessionContextResolution) => {
65
67
  workspace_path: string;
66
68
  workspace_id?: string;
67
69
  logger: Logger;
68
70
  get_tools: () => Record<string, Tool>;
69
- get_workspace_env: () => Record<string, string>;
70
71
  get_workspace: () => WorkspaceRuntime | undefined;
71
72
  get_hooks: () => ToolHookSet;
72
73
  store: SessionStore;
@@ -108,6 +109,8 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
108
109
  private readonly session_composer: SessionComposer;
109
110
  private readonly get_agent_model: AgentSessionsOptions["get_agent_model"];
110
111
  private readonly sessions_by_id = new Map<string, AgentManagedSession>();
112
+ /** Agent 会话创建链;并发调用共享同一创建过程,避免产生第二条。 */
113
+ private agent_session_creation_chain: Promise<void> = Promise.resolve();
111
114
 
112
115
  constructor(options: AgentSessionsOptions) {
113
116
  this.agent_id = options.agent_id;
@@ -170,7 +173,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
170
173
  * runtime 是执行层的内部投影,不负责创建 Session。需要创建时必须
171
174
  * 通过 `create()` 获取内部生成的 ID;需要恢复时必须先调用 `get()`。
172
175
  */
173
- runtime(session_id: string, origin_type = "chat"): SessionPort {
176
+ runtime(session_id: string, origin_type = SESSION_ORIGIN_TYPE_WORK): SessionPort {
174
177
  const resolved_session_id = String(session_id || "").trim();
175
178
  const resolved_origin_type = normalize_session_origin_type(origin_type);
176
179
  const session = this.sessions_by_id.get(
@@ -184,6 +187,72 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
184
187
  return session.get_runtime_port();
185
188
  }
186
189
 
190
+ /**
191
+ * 取用 Agent 自己那条会话;不存在时创建。
192
+ *
193
+ * 关键点(中文)
194
+ * - 身份就是 `origin.type = "agent"`,`session_id` 就是 Agent 的 `id`:
195
+ * 因此不新增索引,也不维护额外状态。
196
+ * - 同一进程内并发调用会收敛到同一个创建过程,不会产生第二条。
197
+ * - 被归档或删除后,下一次调用自然重建一条。
198
+ */
199
+ async self(input?: { workspace?: WorkspaceRuntime }): Promise<AgentSession> {
200
+ const existing = await this.find_agent_session();
201
+ if (existing) return existing;
202
+ return await this.run_agent_session_creation(async () => {
203
+ const cached = await this.find_agent_session();
204
+ if (cached) return cached;
205
+ const origin = create_agent_session_origin();
206
+ const context = this.resolve_session_context({
207
+ origin,
208
+ creating: true,
209
+ ...(input?.workspace ? { workspace: input.workspace } : {}),
210
+ });
211
+ const session_id = resolve_agent_session_id(this.agent_id);
212
+ const session = this.get_or_create_session({
213
+ session_id,
214
+ origin,
215
+ ...(input?.workspace ? { workspace: input.workspace } : {}),
216
+ storage: context.store.create_session(session_id, origin, context.workspace_id),
217
+ });
218
+ await session.initialize();
219
+ return session;
220
+ });
221
+ }
222
+
223
+ /**
224
+ * 尝试恢复已存在的 Agent 会话;不存在或已失效时返回空。
225
+ *
226
+ * 关键点(中文):查找不传 Workspace;传 Workspace 只在创建时生效,
227
+ * 否则「已存在时忽略该参数」会退化成一次静默切目录。
228
+ */
229
+ private async find_agent_session(): Promise<AgentSession | undefined> {
230
+ const origin = create_agent_session_origin();
231
+ const context = this.resolve_session_context({ origin });
232
+ const session_id = resolve_agent_session_id(this.agent_id);
233
+ try {
234
+ if (!(await context.store.has_session(session_id, origin.type))) {
235
+ return undefined;
236
+ }
237
+ } catch {
238
+ return undefined;
239
+ }
240
+ return await this.get(session_id, origin.type);
241
+ }
242
+
243
+ /** 串行化 Agent 会话创建;失败时释放链,使下一次调用可以重试。 */
244
+ private async run_agent_session_creation(
245
+ create: () => Promise<AgentSession>,
246
+ ): Promise<AgentSession> {
247
+ const previous = this.agent_session_creation_chain;
248
+ const operation = previous.then(create, create);
249
+ this.agent_session_creation_chain = operation.then(
250
+ () => undefined,
251
+ () => undefined,
252
+ );
253
+ return await operation;
254
+ }
255
+
187
256
  /**
188
257
  * 新建一个 session。
189
258
  */
@@ -192,7 +261,11 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
192
261
  ): Promise<AgentSession> {
193
262
  const origin = normalize_session_origin(input?.origin);
194
263
  const session_id = `session-${Date.now()}-${nanoid(8)}`;
195
- const context = this.resolve_session_context(input?.workspace);
264
+ const context = this.resolve_session_context({
265
+ origin,
266
+ creating: true,
267
+ ...(input?.workspace ? { workspace: input.workspace } : {}),
268
+ });
196
269
  const session = this.get_or_create_session({
197
270
  session_id,
198
271
  workspace: input?.workspace,
@@ -209,10 +282,15 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
209
282
 
210
283
  /**
211
284
  * 获取一个已存在的 session。
285
+ *
286
+ * 关键点(中文)
287
+ * - 当前目录不是 Session 身份:传入的 Workspace 与持久化值不一致时不再报错,
288
+ * 而是以传入值为准成为该 Session 的新当前目录。
289
+ * - 省略 Workspace 时沿用持久化值;两者都为空时该 Session 没有项目上下文。
212
290
  */
213
291
  async get(
214
292
  session_id: string,
215
- origin_type = "chat",
293
+ origin_type = SESSION_ORIGIN_TYPE_WORK,
216
294
  input?: { workspace?: WorkspaceRuntime },
217
295
  ): Promise<AgentSession> {
218
296
  const resolved_session_id = String(session_id || "").trim();
@@ -221,7 +299,10 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
221
299
  }
222
300
  const resolved_origin_type = normalize_session_origin_type(origin_type);
223
301
  const cache_key = this.session_cache_key(resolved_session_id, resolved_origin_type);
224
- const context = this.resolve_session_context(input?.workspace);
302
+ const context = this.resolve_session_context({
303
+ origin: { type: resolved_origin_type },
304
+ ...(input?.workspace ? { workspace: input.workspace } : {}),
305
+ });
225
306
  const store = context.store;
226
307
  if (
227
308
  !this.sessions_by_id.has(cache_key) &&
@@ -234,28 +315,21 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
234
315
  const storage = await store.open_session(resolved_session_id, resolved_origin_type);
235
316
  const persisted_metadata = await storage.read_metadata();
236
317
  const persisted_workspace_id = String(persisted_metadata.workspace_id || "").trim() || undefined;
237
- const persisted_origin = persisted_metadata.origin;
238
- const cached = this.sessions_by_id.get(cache_key);
239
318
  const requested_workspace_id = String(input?.workspace?.id || "").trim() || undefined;
240
- if (persisted_workspace_id !== requested_workspace_id) {
241
- throw new Error(
242
- persisted_workspace_id
243
- ? `Session "${resolved_session_id}" requires Workspace "${persisted_workspace_id}"`
244
- : `Session "${resolved_session_id}" is not bound to a Workspace`,
245
- );
246
- }
247
- if (cached && cached.workspace_id !== requested_workspace_id) {
248
- throw new Error(
249
- `Session "${resolved_session_id}" is already bound to Workspace "${cached.workspace_id || ""}"`,
250
- );
251
- }
319
+ // 目录不是身份,也不在 Session 侧还原:
320
+ // - 宿主传了实例 → 用它;
321
+ // - 本进程有缓存 → 沿用内存实例;
322
+ // - 都没有 → 该会话没有项目上下文,但仍可正常读历史与对话。
252
323
  const session = this.get_or_create_session({
253
324
  session_id: resolved_session_id,
254
- workspace: input?.workspace,
255
- origin: persisted_origin,
325
+ ...(input?.workspace ? { workspace: input.workspace } : {}),
326
+ origin: persisted_metadata.origin,
256
327
  storage,
257
328
  });
258
329
  await session.initialize();
330
+ if (requested_workspace_id && requested_workspace_id !== persisted_workspace_id) {
331
+ await session.set({ workspace: input?.workspace ?? null });
332
+ }
259
333
  return session;
260
334
  }
261
335
 
@@ -266,7 +340,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
266
340
  * - 正在执行的 Session 会先停止,避免删除后继续写入。
267
341
  * - 该方法不处理任何 Power 自有数据。
268
342
  */
269
- async remove(session_id: string, origin_type = "chat"): Promise<boolean> {
343
+ async remove(session_id: string, origin_type = SESSION_ORIGIN_TYPE_WORK): Promise<boolean> {
270
344
  const resolved_session_id = String(session_id || "").trim();
271
345
  if (!resolved_session_id) {
272
346
  throw new Error("sessions.remove requires a non-empty session_id");
@@ -289,7 +363,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
289
363
  /**
290
364
  * 清空一个 Session 的消息目录。
291
365
  */
292
- async clear_messages(session_id: string, origin_type = "chat"): Promise<boolean> {
366
+ async clear_messages(session_id: string, origin_type = SESSION_ORIGIN_TYPE_WORK): Promise<boolean> {
293
367
  const resolved_session_id = String(session_id || "").trim();
294
368
  if (!resolved_session_id) {
295
369
  throw new Error("sessions.clear_messages requires a non-empty session_id");
@@ -315,8 +389,8 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
315
389
  async list(
316
390
  input?: AgentListSessionsInput,
317
391
  ): Promise<AgentSessionSummaryPage> {
318
- const origin_type = normalize_session_origin_type(input?.origin_type ?? "chat");
319
- return await this.resolve_session_context().store.list_sessions(
392
+ const origin_type = normalize_session_origin_type(input?.origin_type ?? SESSION_ORIGIN_TYPE_WORK);
393
+ return await this.resolve_session_context({ origin: { type: origin_type } }).store.list_sessions(
320
394
  { ...(input || {}), origin_type },
321
395
  new Set(this.list_executing_session_ids(undefined, origin_type)),
322
396
  );
@@ -332,7 +406,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
332
406
  if (!session_id) {
333
407
  throw new Error("sessions.archive requires a non-empty id");
334
408
  }
335
- const origin_type = normalize_session_origin_type(input.origin_type ?? "chat");
409
+ const origin_type = normalize_session_origin_type(input.origin_type ?? SESSION_ORIGIN_TYPE_WORK);
336
410
  const cache_key = this.session_cache_key(session_id, origin_type);
337
411
 
338
412
  const cached = this.sessions_by_id.get(cache_key);
@@ -363,46 +437,11 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
363
437
  }
364
438
 
365
439
  /**
366
- * 把 Session 重新绑定到另一个 Workspace。
440
+ * 删除:把 Session 重新绑定到另一个 Workspace。
367
441
  *
368
- * 关键点(中文)
369
- * - 只改写 session_state 中的 workspace_id 并清掉运行时缓存;
370
- * - 之后用新 Workspace 恢复时不再触发严格的 workspace 归属校验。
442
+ * 删除理由:它表达的是「Workspace 是 Session 绑定」这个旧模型。目录切换改由
443
+ * `session.set({ workspace })` 承担,恢复时不再校验归属,因此不需要第二条路径。
371
444
  */
372
- async workspace(
373
- session_id: string,
374
- workspace: WorkspaceRuntime,
375
- ): Promise<AgentSession> {
376
- const resolved_session_id = String(session_id || "").trim();
377
- if (!resolved_session_id) {
378
- throw new Error("sessions.workspace requires a non-empty session_id");
379
- }
380
- const resolved_origin_type = normalize_session_origin_type("chat");
381
- const cache_key = this.session_cache_key(resolved_session_id, resolved_origin_type);
382
- const cached = this.sessions_by_id.get(cache_key);
383
- if (cached?.is_executing()) {
384
- throw new Error(`Session "${resolved_session_id}" is currently executing`);
385
- }
386
- const context = this.resolve_session_context(workspace);
387
- if (!(await context.store.has_session(resolved_session_id, resolved_origin_type))) {
388
- throw new Error(`Session "${resolved_session_id}" not found`);
389
- }
390
- const store = await context.store.open_session(
391
- resolved_session_id,
392
- resolved_origin_type,
393
- );
394
- const metadata = await store.read_metadata();
395
- if (metadata.agent_id !== this.agent_id) {
396
- throw new Error(`Session "${resolved_session_id}" belongs to another Agent`);
397
- }
398
- await store.write_metadata({
399
- ...metadata,
400
- workspace_id: context.workspace_id,
401
- });
402
- this.sessions_by_id.delete(cache_key);
403
- cached?.dispose_title_generation?.();
404
- return await this.get(resolved_session_id, resolved_origin_type, { workspace });
405
- }
406
445
 
407
446
  private get_or_create_session(input?: {
408
447
  /**
@@ -419,28 +458,31 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
419
458
  const resolved_session_id = String(
420
459
  input?.session_id || input?.storage?.session_id || "",
421
460
  ).trim() || `session-${Date.now()}-${nanoid(8)}`;
422
- const origin = normalize_session_origin(input?.origin);
423
- const cache_key = this.session_cache_key(resolved_session_id, origin.type);
461
+ const resolved_origin = normalize_session_origin(input?.origin);
462
+ const cache_key = this.session_cache_key(resolved_session_id, resolved_origin.type);
424
463
  const cached = this.sessions_by_id.get(cache_key);
425
464
  if (cached) return cached;
426
465
 
427
- const context = this.resolve_session_context(input?.workspace);
466
+ const context = this.resolve_session_context({
467
+ ...(input?.origin ? { origin: input.origin } : {}),
468
+ creating: true,
469
+ ...(input?.workspace ? { workspace: input.workspace } : {}),
470
+ });
428
471
  const created = new this.session_class({
429
472
  agent_id: this.agent_id,
430
473
  agent_name: this.agent_name,
431
474
  agent_description: this.agent_description,
432
475
  workspace_path: context.workspace_path,
433
- ...(context.workspace_id ? { workspace_id: context.workspace_id } : {}),
434
- origin,
435
- store: input?.storage || context.store.create_session(resolved_session_id, origin, context.workspace_id),
436
- create_session_store: (session_id) => context.store.create_session(session_id, origin, context.workspace_id),
476
+ ...(context.get_workspace() ? { workspace: context.get_workspace() } : {}),
477
+ origin: resolved_origin,
478
+ store: input?.storage || context.store.create_session(resolved_session_id, resolved_origin, context.workspace_id),
479
+ create_session_store: (session_id) => context.store.create_session(session_id, resolved_origin, context.workspace_id),
437
480
  register_forked_session: (session) => this.register_forked_session(session),
438
481
  session_id: resolved_session_id,
439
482
  get_tools: () => context.get_tools(),
440
483
  logger: context.logger,
441
484
  instruction_system_blocks: this.load_instruction_system_blocks(),
442
485
  get_instruction_system_blocks: () => this.load_instruction_system_blocks(),
443
- get_workspace_env: () => context.get_workspace_env(),
444
486
  get_workspace: () => context.get_workspace(),
445
487
  get_agent_model: () => this.get_agent_model(),
446
488
  get_hooks: () => context.get_hooks(),
@@ -459,7 +501,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
459
501
  * - 返回是否真的释放。调用方必须先自行解除对该实例的订阅,否则重新 `get()` 会拿到新实例
460
502
  * 而旧订阅仍然挂在被丢弃的实例上。
461
503
  */
462
- release(session_id: string, origin_type = "chat"): boolean {
504
+ release(session_id: string, origin_type = SESSION_ORIGIN_TYPE_WORK): boolean {
463
505
  const resolved_session_id = String(session_id || "").trim();
464
506
  if (!resolved_session_id) return false;
465
507
  const cache_key = this.session_cache_key(