@zmzai/agent-framework 0.4.0 → 0.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 (63) hide show
  1. package/dist/adapters/subprocess-sandbox.d.ts.map +1 -1
  2. package/dist/adapters/subprocess-sandbox.js +77 -6
  3. package/dist/adapters/subprocess-sandbox.js.map +1 -1
  4. package/dist/adapters/terminal-backend.js +30 -6
  5. package/dist/adapters/terminal-backend.js.map +1 -1
  6. package/dist/core/agent/registry.d.ts.map +1 -1
  7. package/dist/core/agent/registry.js +2 -1
  8. package/dist/core/agent/registry.js.map +1 -1
  9. package/dist/core/events/bus.d.ts.map +1 -1
  10. package/dist/core/events/bus.js +17 -8
  11. package/dist/core/events/bus.js.map +1 -1
  12. package/dist/core/events/sqlite-event-log.d.ts.map +1 -1
  13. package/dist/core/events/sqlite-event-log.js +24 -11
  14. package/dist/core/events/sqlite-event-log.js.map +1 -1
  15. package/dist/core/mcp/client.js +13 -3
  16. package/dist/core/mcp/client.js.map +1 -1
  17. package/dist/core/mcp/servers.js +4 -1
  18. package/dist/core/mcp/servers.js.map +1 -1
  19. package/dist/core/runtime/attachments.d.ts +14 -0
  20. package/dist/core/runtime/attachments.d.ts.map +1 -0
  21. package/dist/core/runtime/attachments.js +29 -0
  22. package/dist/core/runtime/attachments.js.map +1 -0
  23. package/dist/core/runtime/lease-recovery.d.ts.map +1 -1
  24. package/dist/core/runtime/lease-recovery.js +18 -3
  25. package/dist/core/runtime/lease-recovery.js.map +1 -1
  26. package/dist/core/runtime/pi-bridge.d.ts +3 -2
  27. package/dist/core/runtime/pi-bridge.d.ts.map +1 -1
  28. package/dist/core/runtime/pi-bridge.js +9 -1
  29. package/dist/core/runtime/pi-bridge.js.map +1 -1
  30. package/dist/core/runtime/runner.d.ts +21 -2
  31. package/dist/core/runtime/runner.d.ts.map +1 -1
  32. package/dist/core/runtime/runner.js +207 -49
  33. package/dist/core/runtime/runner.js.map +1 -1
  34. package/dist/core/session/message-search.d.ts +21 -0
  35. package/dist/core/session/message-search.d.ts.map +1 -0
  36. package/dist/core/session/message-search.js +33 -0
  37. package/dist/core/session/message-search.js.map +1 -0
  38. package/dist/core/session/sqlite-store.d.ts.map +1 -1
  39. package/dist/core/session/sqlite-store.js +359 -54
  40. package/dist/core/session/sqlite-store.js.map +1 -1
  41. package/dist/core/session/store.d.ts +57 -0
  42. package/dist/core/session/store.d.ts.map +1 -1
  43. package/dist/core/session/types.d.ts +22 -0
  44. package/dist/core/session/types.d.ts.map +1 -1
  45. package/dist/core/session/workflow.d.ts +35 -0
  46. package/dist/core/session/workflow.d.ts.map +1 -0
  47. package/dist/core/session/workflow.js +12 -0
  48. package/dist/core/session/workflow.js.map +1 -0
  49. package/dist/core/tools/builtins.d.ts.map +1 -1
  50. package/dist/core/tools/builtins.js +5 -1
  51. package/dist/core/tools/builtins.js.map +1 -1
  52. package/dist/core/tools/terminal.d.ts.map +1 -1
  53. package/dist/core/tools/terminal.js +12 -6
  54. package/dist/core/tools/terminal.js.map +1 -1
  55. package/dist/index.d.ts +3 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +2 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/server/create-server.d.ts +4 -0
  60. package/dist/server/create-server.d.ts.map +1 -1
  61. package/dist/server/create-server.js +1 -0
  62. package/dist/server/create-server.js.map +1 -1
  63. package/package.json +12 -11
@@ -1,4 +1,5 @@
1
1
  import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { validateAttachments, attachmentContent } from "./attachments.js";
2
3
  import { leaseDurationMs } from "../../adapters/index.js";
3
4
  import { notifyEventLogListeners } from "../events/bus.js";
4
5
  import { PermissionEngine, RejectedError } from "../permission/engine.js";
@@ -12,6 +13,7 @@ import { isExternalToolDef } from "../tools/def.js";
12
13
  import { fireRunStart, firstToolBlock, fireAfterToolCall, fireRunEnd } from "./lifecycle.js";
13
14
  import { extractRunTranscript, RETRY_PLACEHOLDER_TEXT } from "./run-transcript.js";
14
15
  import { noopSandboxExecutor } from "../../adapters/index.js";
16
+ import { randomUUID } from "node:crypto";
15
17
  /** 事件的 sessionId 提取:能自带的自带(message/part/session),其余
16
18
  * (message.part.delta 等)用发起 run 的会话 id 兜底。 */
17
19
  function sessionIdOf(event, fallbackSessionId) {
@@ -89,6 +91,49 @@ export class SessionRunner {
89
91
  constructor(deps) {
90
92
  this.deps = deps;
91
93
  }
94
+ draining = new Map();
95
+ stopRequested = new Set();
96
+ drain(sessionId) {
97
+ if (this.draining.has(sessionId))
98
+ return;
99
+ const work = (async () => {
100
+ while (true) {
101
+ const job = await this.deps.store.workflow.claimPrompt(sessionId, `node:${process.pid}`);
102
+ if (!job)
103
+ return;
104
+ let outcome = "recovery_required";
105
+ try {
106
+ if (this.stopRequested.has(sessionId)) {
107
+ outcome = "cancelled";
108
+ }
109
+ else {
110
+ const session = await this.deps.store.getSession(sessionId);
111
+ if (!session) {
112
+ outcome = "failed";
113
+ }
114
+ else {
115
+ outcome = await this.runLoop(session, job.input, job.receipt.userMessageId);
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Setup or settlement may have failed after a tool ran. Do not replay.
121
+ }
122
+ await this.deps.store.workflow.finishPrompt(sessionId, job.receipt.runId, job.revision, outcome);
123
+ if (outcome !== "completed")
124
+ return;
125
+ }
126
+ })();
127
+ this.draining.set(sessionId, work);
128
+ void work.catch(() => undefined).finally(async () => {
129
+ this.draining.delete(sessionId);
130
+ if (this.stopRequested.has(sessionId))
131
+ return;
132
+ const queued = await this.deps.store.workflow.workflowRuns(sessionId).catch(() => []);
133
+ if (queued.some(run => run.status === "queued") && !queued.some(run => run.status === "running" || run.status === "recovery_required"))
134
+ this.drain(sessionId);
135
+ });
136
+ }
92
137
  #hooks = [];
93
138
  /** 供 runLoop 取归一化钩子数组(lazy:constructor 后仍可由 deps 引用共享)。 */
94
139
  get hooks() {
@@ -100,6 +145,12 @@ export class SessionRunner {
100
145
  * 单例,两个会话并发时实例字段会互相覆盖,导致 message.part.delta 等
101
146
  * 无自带 sessionId 的事件落到错误的会话事件流里(串台)。 */
102
147
  async persist(event, fallbackSessionId) {
148
+ const sessionId = sessionIdOf(event, fallbackSessionId);
149
+ if (this.deps.store.persistEvent) {
150
+ const persisted = await this.deps.store.persistEvent({ sessionId, ...event });
151
+ notifyEventLogListeners(persisted);
152
+ return;
153
+ }
103
154
  if (event.type === "message.updated") {
104
155
  const message = event.data.message;
105
156
  const exists = await this.deps.store.getMessages(message.sessionId).then((entries) => entries.some((entry) => entry.info.id === message.id));
@@ -114,11 +165,13 @@ export class SessionRunner {
114
165
  else if (event.type === "session.updated") {
115
166
  await this.deps.store.updateSession(event.data.session.id, event.data.session);
116
167
  }
117
- const persisted = await this.deps.eventLog.append({ sessionId: sessionIdOf(event, fallbackSessionId), ...event });
168
+ const persisted = await this.deps.eventLog.append({ sessionId, ...event });
118
169
  notifyEventLogListeners(persisted);
119
170
  }
120
171
  async publish(event, sessionId) {
121
- const persisted = await this.deps.eventLog.append({ sessionId, ...event });
172
+ const persisted = this.deps.store.persistEvent
173
+ ? await this.deps.store.persistEvent({ sessionId, ...event })
174
+ : await this.deps.eventLog.append({ sessionId, ...event });
122
175
  notifyEventLogListeners(persisted);
123
176
  }
124
177
  async stampLease(sessionId) {
@@ -132,11 +185,37 @@ export class SessionRunner {
132
185
  await this.deps.leaseStore.clear(sessionId).catch(() => undefined);
133
186
  }
134
187
  async prompt(sessionId, input) {
188
+ input = { ...input, attachments: validateAttachments(input.attachments) };
135
189
  const session = await this.deps.store.getSession(sessionId);
136
190
  if (!session)
137
191
  throw new Error("SESSION_NOT_FOUND");
192
+ if (this.deps.store.workflow) {
193
+ input = { ...input, requestId: input.requestId ?? randomUUID(), model: input.model ?? session.model };
194
+ const projector = new PartProjector({ sessionId, agent: input.agent ?? session.agent, model: input.model });
195
+ const parts = [];
196
+ const message = projector.onUserPrompt(event => {
197
+ if (event.type === "message.part.updated")
198
+ parts.push(event.data.part);
199
+ }, input.text, input.images, input.skill, input.references, input.attachments);
200
+ const accepted = await this.deps.store.workflow.acceptPrompt(sessionId, input, { message, parts });
201
+ for (const event of accepted.events)
202
+ notifyEventLogListeners(event);
203
+ if (accepted.events.length)
204
+ this.drain(sessionId);
205
+ return accepted.receipt;
206
+ }
138
207
  if (activeRuns.has(sessionId)) {
139
- await this.deps.store.enqueuePrompt(sessionId, { text: input.text, ...(input.agent ? { agent: input.agent } : {}), ...(input.effort ? { effort: input.effort } : {}), enqueuedAt: new Date().toISOString() });
208
+ await this.deps.store.enqueuePrompt(sessionId, {
209
+ text: input.text,
210
+ attachments: input.attachments,
211
+ images: input.images,
212
+ model: input.model,
213
+ ...(input.agent ? { agent: input.agent } : {}),
214
+ ...(input.effort ? { effort: input.effort } : {}),
215
+ ...(input.skill ? { skill: input.skill } : {}),
216
+ ...(input.references?.length ? { references: [...input.references] } : {}),
217
+ enqueuedAt: new Date().toISOString(),
218
+ });
140
219
  return { queued: true };
141
220
  }
142
221
  void this.runLoop(session, input);
@@ -163,16 +242,19 @@ export class SessionRunner {
163
242
  return removedFromEngine || removedFromStore;
164
243
  }
165
244
  async abort(sessionId) {
166
- const active = activeRuns.get(sessionId);
245
+ this.stopRequested.add(sessionId);
167
246
  await this.deps.store.clearQueuedPrompts(sessionId);
168
- if (!active)
169
- return;
247
+ const active = activeRuns.get(sessionId);
170
248
  // PI's abort signal does not cancel a PermissionEngine.ask() promise.
171
249
  // Rejecting pending approvals first releases beforeToolCall so the run can
172
250
  // publish its terminal state and a continuation cannot overlap it.
173
- active.engine.dispose("任务已停止,未处理的授权请求已取消");
174
- active.abort();
175
- await active.done;
251
+ if (active) {
252
+ active.engine.dispose("任务已停止,未处理的授权请求已取消");
253
+ active.abort();
254
+ await active.done;
255
+ }
256
+ await this.draining.get(sessionId);
257
+ this.stopRequested.delete(sessionId);
176
258
  }
177
259
  /** 手动触发一次上下文压缩(UI「压缩当前会话」):无条件对当前历史跑一次
178
260
  * 摘要折叠,摘要通过 buildCompaction 的 onCompacted 落为 compaction part
@@ -187,7 +269,11 @@ export class SessionRunner {
187
269
  const transform = await this.buildCompaction(session, (event) => this.publish(event, session.id), true);
188
270
  if (!transform)
189
271
  return { ok: false, reason: "compaction-disabled" };
190
- const messages = await this.rebuildMessages(sessionId);
272
+ const workflowRuns = this.deps.store.workflow ? await this.deps.store.workflow.workflowRuns(sessionId) : [];
273
+ const excludedUserIds = this.deps.store.workflow
274
+ ? new Set(workflowRuns.filter((run) => run.status !== "completed" && run.status !== "failed").map((run) => run.receipt.userMessageId))
275
+ : undefined;
276
+ const messages = await this.rebuildMessages(sessionId, excludedUserIds);
191
277
  if (messages.length <= 1)
192
278
  return { ok: false, reason: "nothing-to-compact" };
193
279
  await transform(messages);
@@ -269,12 +355,23 @@ export class SessionRunner {
269
355
  return null;
270
356
  }
271
357
  }
272
- async runLoop(session, input) {
358
+ async runLoop(session, input, acceptedUserId) {
273
359
  const registry = await this.registryFor(session);
274
360
  const resolved = await this.resolvedAgentFor(session);
275
361
  const agentName = resolved ? resolved.agent.name : input.agent ?? session.agent;
276
362
  const agentInfo = resolved?.agent ?? registry.get(agentName) ?? registry.get("default");
277
363
  const model = input.model ?? agentInfo?.model ?? session.model;
364
+ // 回写当轮实际模型:session.model 是「会话当前模型」的持久来源,但
365
+ // prompt 传入的 model / agent 声明的 model 此前只用于当轮、从不落库,
366
+ // 于是所有读 session.model 的旁路(压缩阈值 contextWindowFor、总结
367
+ // 陈词 summarizeRun、子代理继承、宿主侧标题生成)拿到的都是建会话时
368
+ // 的旧模型甚至 env 兜底值。回写后这些旁路自动跟随当轮模型,无需各自
369
+ // 传参。必须在 buildCompaction 之前完成,否则压缩阈值仍按旧模型算。
370
+ if (model.providerId !== session.model?.providerId || model.modelId !== session.model?.modelId) {
371
+ await this.deps.store.updateSession(session.id, { model }).catch(() => undefined);
372
+ // 同步内存引用:本轮后续(buildCompaction/闭包捕获)都用新模型
373
+ session = { ...session, model };
374
+ }
278
375
  const agentRulesets = resolved ? [registry.rulesetsFor("default")[0], resolved.agent.permission] : registry.rulesetsFor(agentInfo?.name ?? "default");
279
376
  const engine = new PermissionEngine(session.id, agentRulesets, session.permission, {
280
377
  onAsked: async (request) => {
@@ -299,6 +396,15 @@ export class SessionRunner {
299
396
  await this.persist(event, session.id);
300
397
  });
301
398
  const projector = new PartProjector({ sessionId: session.id, agent: agentInfo?.name ?? "default", model });
399
+ if (acceptedUserId)
400
+ projector.restoreUserMessage(acceptedUserId);
401
+ let mandatorySkillContext = "";
402
+ if (input.skill) {
403
+ if (!this.deps.resolveMandatorySkill)
404
+ throw new Error("该运行环境不支持 Skill 加载");
405
+ const loaded = await this.deps.resolveMandatorySkill(session, input.skill);
406
+ mandatorySkillContext = loaded.context;
407
+ }
302
408
  // Exclude task from contexts that can't nest; include for primary runs.
303
409
  const baseTools = [...(this.deps.tools ?? builtinTools), ...(this.deps.localTools ?? []), ...(resolved?.tools ?? [])];
304
410
  const toolList = session.parentId ? baseTools.filter((def) => def.id !== "task") : baseTools;
@@ -309,8 +415,7 @@ export class SessionRunner {
309
415
  const rawWorkspace = this.deps.workspaceFor(session);
310
416
  const workspace = session.writePaths?.length ? confineWorkspaceFiles(rawWorkspace, session.writePaths) : rawWorkspace;
311
417
  const emitAsync = async (event) => {
312
- const persisted = await this.deps.eventLog.append({ sessionId: session.id, ...event });
313
- notifyEventLogListeners(persisted);
418
+ await this.publish(event, session.id);
314
419
  };
315
420
  const toolContext = (this.deps.buildToolContext ?? defaultToolContext)({ session, engine, workspace, sandbox, emit: emitAsync });
316
421
  // Subagent spawning is only available to primary (non-child) sessions, and
@@ -323,7 +428,11 @@ export class SessionRunner {
323
428
  const compactionTransform = await this.buildCompaction(session, emit);
324
429
  // 记忆召回(spec §记忆):单点注入 runLoop,天然覆盖正常 prompt/排队出
325
430
  // 队/automation/子代理触发四条路径。只进内存不落 store;抛错静默降级。
326
- const history = await this.rebuildMessages(session.id);
431
+ const workflowRuns = this.deps.store.workflow ? await this.deps.store.workflow.workflowRuns(session.id) : [];
432
+ const excludedUserIds = this.deps.store.workflow
433
+ ? new Set(workflowRuns.filter((run) => run.status !== "completed" && run.status !== "failed").map((run) => run.receipt.userMessageId))
434
+ : undefined;
435
+ const history = await this.rebuildMessages(session.id, excludedUserIds);
327
436
  if (this.deps.memoryContextFor) {
328
437
  try {
329
438
  const section = await this.deps.memoryContextFor(session, input.text);
@@ -339,7 +448,7 @@ export class SessionRunner {
339
448
  const baseline = history.length;
340
449
  const agent = new Agent({
341
450
  initialState: {
342
- systemPrompt: agentInfo?.prompt ?? "",
451
+ systemPrompt: [agentInfo?.prompt ?? "", mandatorySkillContext, input.references?.length ? `<attached-resources>\nThe user attached these workspace paths. Read the relevant ones before acting:\n${input.references.join("\n")}\n</attached-resources>` : ""].filter(Boolean).join("\n\n"),
343
452
  model: this.deps.modelFor(model),
344
453
  // 推理力度(P1-8 复活):relay 现已按模型白名单接受 reasoning_effort;
345
454
  // 仅当调用方显式选择且非 off 时下发(默认不设 = 完全不带该字段)。
@@ -360,6 +469,8 @@ export class SessionRunner {
360
469
  let resolveDone;
361
470
  const done = new Promise((resolve) => { resolveDone = resolve; });
362
471
  activeRuns.set(session.id, { agent, engine, settled, abort, done });
472
+ if (this.stopRequested.has(session.id))
473
+ abort();
363
474
  await this.stampLease(session.id);
364
475
  const loopGuard = new LoopGuard();
365
476
  agent.beforeToolCall = async ({ toolCall, args }) => {
@@ -538,15 +649,18 @@ export class SessionRunner {
538
649
  data: { toolCalls: summaryToolCalls, lastTool: summaryLastTool, elapsedMs },
539
650
  }, session.id).catch(() => undefined);
540
651
  }, CHECKPOINT_INTERVAL_MS);
541
- await Promise.all(fireRunStart(this.hooks, { sessionId: session.id, agent: session.agent, text: input.text }));
542
- await this.publish({ type: "session.status", data: { status: "running" } }, session.id);
543
652
  try {
544
- projector.onUserPrompt(emit, input.text, input.images);
653
+ await Promise.all(fireRunStart(this.hooks, { sessionId: session.id, agent: session.agent, text: input.text }));
654
+ await this.publish({ type: "session.status", data: { status: "running" } }, session.id);
655
+ if (abortController.signal.aborted)
656
+ throw new Error("Run cancelled during setup");
657
+ if (!acceptedUserId)
658
+ projector.onUserPrompt(emit, input.text, input.images, input.skill, input.references, input.attachments);
545
659
  const piImages = input.images?.map((img) => {
546
660
  const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
547
661
  return match ? { type: "image", data: match[2], mimeType: match[1] } : null;
548
662
  }).filter((img) => img !== null);
549
- await agent.prompt(input.text, piImages?.length ? piImages : undefined);
663
+ await agent.prompt({ role: "user", content: [{ type: "text", text: input.text }, ...attachmentContent(input.attachments ?? []), ...(piImages ?? [])], timestamp: Date.now() });
550
664
  await settled();
551
665
  let failed = agent.state.errorMessage;
552
666
  if (unknownSideEffect) {
@@ -597,13 +711,17 @@ export class SessionRunner {
597
711
  : aborted
598
712
  ? { type: "session.status", data: { status: "idle" } }
599
713
  : { type: "session.error", data: { name: "AgentRuntimeError", message: error instanceof Error ? error.message : "Agent 运行失败" } }, session.id);
714
+ if (!aborted && !unknownSideEffect)
715
+ await this.publish({ type: "session.status", data: { status: "idle" } }, session.id);
600
716
  }
601
717
  finally {
602
718
  clearInterval(streamWatchdog);
603
719
  clearInterval(checkpointTimer);
604
720
  activeRuns.delete(session.id);
605
721
  engine.dispose();
606
- await this.clearLease(session.id);
722
+ // Workflow settlement clears its lease atomically with the terminal run state.
723
+ if (!acceptedUserId)
724
+ await this.clearLease(session.id);
607
725
  const newMessages = extractRunTranscript(agent.state.messages, baseline);
608
726
  // 任务终态小结(N5):终态 status 已发布后补一条 session.summary,
609
727
  // 让「任务完成」有明确收尾(AI 一句总结 + 结构化统计)。await 保证
@@ -619,15 +737,26 @@ export class SessionRunner {
619
737
  void Promise.all(fireRunEnd(this.hooks, { sessionId: session.id, agent: session.agent, ok: !runErrored, aborted: abortController.signal.aborted, workspaceId: session.workspaceId, newMessages }));
620
738
  resolveDone();
621
739
  }
622
- if (unknownSideEffect)
623
- return;
740
+ const outcome = unknownSideEffect ? "recovery_required" : abortController.signal.aborted ? "cancelled" : runErrored ? "failed" : "completed";
741
+ if (acceptedUserId || unknownSideEffect)
742
+ return outcome;
624
743
  // FIFO queued prompts (spec §13.3): settle fully, then take the next one.
625
744
  const next = await this.deps.store.dequeuePrompt(session.id);
626
745
  if (next) {
627
746
  const latest = await this.deps.store.getSession(session.id);
628
747
  if (latest)
629
- await this.runLoop(latest, { text: next.text, ...(next.agent ? { agent: next.agent } : {}), ...(next.effort ? { effort: next.effort } : {}) });
748
+ await this.runLoop(latest, {
749
+ text: next.text,
750
+ attachments: next.attachments,
751
+ images: next.images,
752
+ model: next.model,
753
+ ...(next.agent ? { agent: next.agent } : {}),
754
+ ...(next.effort ? { effort: next.effort } : {}),
755
+ ...(next.skill ? { skill: next.skill } : {}),
756
+ ...(next.references?.length ? { references: [...next.references] } : {}),
757
+ });
630
758
  }
759
+ return outcome;
631
760
  }
632
761
  /** Spawns a subagent child session (spec §6.4): depth-capped, permission
633
762
  * stamped from parent session + subagent preset, runs a nested PI loop to
@@ -724,36 +853,52 @@ export class SessionRunner {
724
853
  // 终态先落 session 记录(列表三态用,N5)——即使 summaryModel 缺失或生成
725
854
  // 失败,lastOutcome 也要写回,保证列表能区分完成/中断/失败。
726
855
  await this.deps.store.updateSession(session.id, { lastOutcome: input.kind }).catch(() => undefined);
727
- const summaryModel = this.deps.compaction?.summaryModel ?? null;
728
- if (!summaryModel)
729
- return;
856
+ const durationMs = Date.now() - input.startedAt;
857
+ const meta = { filesEdited: input.stats.editedFiles.length, toolCalls: input.stats.toolCalls, durationMs };
858
+ // 兜底文案:summary 生成失败 / 无模型 / 超时 / 空文本时,仍然发一条带
859
+ // meta 的 session.summary,保证前端「任务完成卡」三态必现,而非悄无声息结束。
860
+ const fallbackText = input.kind === "completed"
861
+ ? `本轮任务已完成,共 ${meta.toolCalls} 次工具调用${meta.filesEdited > 0 ? `,改动 ${meta.filesEdited} 个文件` : ""}。`
862
+ : input.kind === "aborted"
863
+ ? `本轮任务已中断,已完成 ${meta.toolCalls} 次工具调用${meta.filesEdited > 0 ? `,改动 ${meta.filesEdited} 个文件` : ""}。`
864
+ : `本轮任务执行出错,已完成 ${meta.toolCalls} 次工具调用${meta.filesEdited > 0 ? `,改动 ${meta.filesEdited} 个文件` : ""}。`;
865
+ // 总结模型沿用「会话实际模型」而非 compaction 专用 summaryModel,保证
866
+ // 总结卡文案由当前会话正在用的模型生成。仅当 compaction 启用时才做 AI
867
+ // 总结(否则落到兜底文案),但模型来源与会话模型解耦。modelFor 抛错
868
+ // (宿主 provider 不认识该 ref)时回落 compaction.summaryModel,再不行
869
+ // 落到下方兜底文案。
870
+ let summaryModel = null;
871
+ if (this.deps.compaction?.enabled) {
872
+ try {
873
+ summaryModel = this.deps.modelFor(session.model);
874
+ }
875
+ catch {
876
+ summaryModel = this.deps.compaction.summaryModel ?? null;
877
+ }
878
+ }
730
879
  // 本轮新增消息(baseline 之后),只取 user/assistant 文本作总结素材
731
880
  const messages = input.agent.state.messages.slice(input.baseline);
732
881
  const textMessages = messages.filter((m) => m.role === "user" || m.role === "assistant");
733
- if (textMessages.length === 0)
734
- return;
735
- const durationMs = Date.now() - input.startedAt;
736
- const meta = { filesEdited: input.stats.editedFiles.length, toolCalls: input.stats.toolCalls, durationMs };
737
- // 总结提示词:只描述本轮做了什么,一句/几句自然语言,不续写
738
- const systemPrompt = "你是任务收尾助手。根据用户本轮的任务与助手已完成的工作,写一句简短、克制的中文总结(1-2 句)," +
739
- "说明「做了什么、结果如何、下一步建议」。不要客套、不要问句、不要续写任务,只输出总结正文。";
740
882
  let text = "";
741
- try {
742
- const { streamOneText } = await import("./compaction.js");
743
- const streamFn = this.deps.streamFnFor(session);
744
- // 15s 超时兜底:summary 是收尾的锦上添花,不能因为上游慢而卡住 run 收尾。
745
- text = await Promise.race([
746
- streamOneText(async (m, ctx) => streamFn(m, ctx), summaryModel, systemPrompt, textMessages),
747
- new Promise((resolve) => setTimeout(() => resolve(""), 15_000)),
748
- ]);
749
- }
750
- catch {
751
- /* 总结生成失败静默跳过 */
883
+ if (summaryModel && textMessages.length > 0) {
884
+ // 总结提示词:只描述本轮做了什么,一句/几句自然语言,不续写
885
+ const systemPrompt = "你是任务收尾助手。根据用户本轮的任务与助手已完成的工作,写一句简短、克制的中文总结(1-2 句)," +
886
+ "说明「做了什么、结果如何、下一步建议」。不要客套、不要问句、不要续写任务,只输出总结正文。";
887
+ try {
888
+ const { streamOneText } = await import("./compaction.js");
889
+ const streamFn = this.deps.streamFnFor(session);
890
+ // 15s 超时兜底:summary 是收尾的锦上添花,不能因为上游慢而卡住 run 收尾。
891
+ text = await Promise.race([
892
+ streamOneText(async (m, ctx) => streamFn(m, ctx), summaryModel, systemPrompt, textMessages),
893
+ new Promise((resolve) => setTimeout(() => resolve(""), 15_000)),
894
+ ]);
895
+ }
896
+ catch {
897
+ /* 总结生成失败 → 落到下方兜底文案 */
898
+ }
752
899
  }
753
900
  const trimmed = text.trim();
754
- if (!trimmed)
755
- return;
756
- await this.publish({ type: "session.summary", data: { text: trimmed, kind: input.kind, meta } }, session.id).catch(() => undefined);
901
+ await this.publish({ type: "session.summary", data: { text: trimmed || fallbackText, kind: input.kind, meta } }, session.id).catch(() => undefined);
757
902
  }
758
903
  async lastAssistantText(sessionId) {
759
904
  const entries = await this.deps.store.getMessages(sessionId);
@@ -783,16 +928,20 @@ export class SessionRunner {
783
928
  }
784
929
  return depth;
785
930
  }
786
- async rebuildMessages(sessionId) {
931
+ async rebuildMessages(sessionId, excludedUserIds) {
787
932
  const entries = await this.deps.store.getMessages(sessionId);
788
933
  const messages = [];
789
934
  for (const { info, parts } of entries) {
790
935
  if (info.role === "user") {
936
+ if (excludedUserIds?.has(info.id))
937
+ continue;
791
938
  const text = parts
792
939
  .filter((part) => part.type === "text")
793
940
  .map((part) => part.text)
794
941
  .join("\n");
795
- messages.push({ role: "user", content: text || "(空消息)", timestamp: Date.parse(info.time.created) || Date.now() });
942
+ const files = parts.filter((p) => p.type === "file" && p.url.startsWith("data:"));
943
+ const attachments = files.map((p) => ({ name: p.filename, mediaType: p.mime, data: p.url, size: Buffer.from(p.url.slice(p.url.indexOf(",") + 1), "base64").length }));
944
+ messages.push({ role: "user", content: [{ type: "text", text }, ...attachmentContent(attachments)], timestamp: Date.parse(info.time.created) || Date.now() });
796
945
  }
797
946
  else {
798
947
  const text = parts
@@ -831,6 +980,8 @@ export async function createFrameworkSession(input) {
831
980
  permission: input.permission ?? [],
832
981
  ...(input.writePaths?.length ? { writePaths: input.writePaths } : {}),
833
982
  queuedPrompts: [],
983
+ ...(input.creationRequestId ? { creationRequestId: input.creationRequestId } : {}),
984
+ ...(input.creationPayloadHash ? { creationPayloadHash: input.creationPayloadHash } : {}),
834
985
  time: { created: new Date().toISOString(), updated: new Date().toISOString() },
835
986
  };
836
987
  await input.store.createSession(session);
@@ -839,6 +990,13 @@ export async function createFrameworkSession(input) {
839
990
  export function isSessionActive(sessionId) {
840
991
  return activeRuns.has(sessionId);
841
992
  }
993
+ /** 会话当前 run 是否挂起等待人工授权(HITL 待确认)。宿主会话列表用它把
994
+ * 「待确认」从笼统的「运行中」里区分出来,否则后台会话被权限卡住时
995
+ * 侧边栏毫无信号。无 run 或 run 无 pending 请求均为 false。 */
996
+ export function isSessionAwaitingPermission(sessionId) {
997
+ const active = activeRuns.get(sessionId);
998
+ return !!active && active.engine.pendingRequests.length > 0;
999
+ }
842
1000
  /** 当前所有 running 会话 id(Electron 优雅退出等收尾场景枚举用,P2)。
843
1001
  * activeRuns 是模块级 globalThis 单例,跨项目/跨 runtime 共享。 */
844
1002
  export function listActiveSessions() {