@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.
- package/dist/adapters/subprocess-sandbox.d.ts.map +1 -1
- package/dist/adapters/subprocess-sandbox.js +77 -6
- package/dist/adapters/subprocess-sandbox.js.map +1 -1
- package/dist/adapters/terminal-backend.js +30 -6
- package/dist/adapters/terminal-backend.js.map +1 -1
- package/dist/core/agent/registry.d.ts.map +1 -1
- package/dist/core/agent/registry.js +2 -1
- package/dist/core/agent/registry.js.map +1 -1
- package/dist/core/events/bus.d.ts.map +1 -1
- package/dist/core/events/bus.js +17 -8
- package/dist/core/events/bus.js.map +1 -1
- package/dist/core/events/sqlite-event-log.d.ts.map +1 -1
- package/dist/core/events/sqlite-event-log.js +24 -11
- package/dist/core/events/sqlite-event-log.js.map +1 -1
- package/dist/core/mcp/client.js +13 -3
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/servers.js +4 -1
- package/dist/core/mcp/servers.js.map +1 -1
- package/dist/core/runtime/attachments.d.ts +14 -0
- package/dist/core/runtime/attachments.d.ts.map +1 -0
- package/dist/core/runtime/attachments.js +29 -0
- package/dist/core/runtime/attachments.js.map +1 -0
- package/dist/core/runtime/lease-recovery.d.ts.map +1 -1
- package/dist/core/runtime/lease-recovery.js +18 -3
- package/dist/core/runtime/lease-recovery.js.map +1 -1
- package/dist/core/runtime/pi-bridge.d.ts +3 -2
- package/dist/core/runtime/pi-bridge.d.ts.map +1 -1
- package/dist/core/runtime/pi-bridge.js +9 -1
- package/dist/core/runtime/pi-bridge.js.map +1 -1
- package/dist/core/runtime/runner.d.ts +21 -2
- package/dist/core/runtime/runner.d.ts.map +1 -1
- package/dist/core/runtime/runner.js +207 -49
- package/dist/core/runtime/runner.js.map +1 -1
- package/dist/core/session/message-search.d.ts +21 -0
- package/dist/core/session/message-search.d.ts.map +1 -0
- package/dist/core/session/message-search.js +33 -0
- package/dist/core/session/message-search.js.map +1 -0
- package/dist/core/session/sqlite-store.d.ts.map +1 -1
- package/dist/core/session/sqlite-store.js +359 -54
- package/dist/core/session/sqlite-store.js.map +1 -1
- package/dist/core/session/store.d.ts +57 -0
- package/dist/core/session/store.d.ts.map +1 -1
- package/dist/core/session/types.d.ts +22 -0
- package/dist/core/session/types.d.ts.map +1 -1
- package/dist/core/session/workflow.d.ts +35 -0
- package/dist/core/session/workflow.d.ts.map +1 -0
- package/dist/core/session/workflow.js +12 -0
- package/dist/core/session/workflow.js.map +1 -0
- package/dist/core/tools/builtins.d.ts.map +1 -1
- package/dist/core/tools/builtins.js +5 -1
- package/dist/core/tools/builtins.js.map +1 -1
- package/dist/core/tools/terminal.d.ts.map +1 -1
- package/dist/core/tools/terminal.js +12 -6
- package/dist/core/tools/terminal.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/server/create-server.d.ts +4 -0
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +1 -0
- package/dist/server/create-server.js.map +1 -1
- 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
|
|
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 =
|
|
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, {
|
|
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
|
-
|
|
245
|
+
this.stopRequested.add(sessionId);
|
|
167
246
|
await this.deps.store.clearQueuedPrompts(sessionId);
|
|
168
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
623
|
-
|
|
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, {
|
|
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
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
streamOneText
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|