@zmzai/agent-framework 0.1.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 (126) hide show
  1. package/dist/adapters/fs-workspace.d.ts +7 -0
  2. package/dist/adapters/fs-workspace.d.ts.map +1 -0
  3. package/dist/adapters/fs-workspace.js +91 -0
  4. package/dist/adapters/fs-workspace.js.map +1 -0
  5. package/dist/adapters/index.d.ts +46 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +14 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/openai-provider.d.ts +17 -0
  10. package/dist/adapters/openai-provider.d.ts.map +1 -0
  11. package/dist/adapters/openai-provider.js +116 -0
  12. package/dist/adapters/openai-provider.js.map +1 -0
  13. package/dist/adapters/subprocess-sandbox.d.ts +7 -0
  14. package/dist/adapters/subprocess-sandbox.d.ts.map +1 -0
  15. package/dist/adapters/subprocess-sandbox.js +91 -0
  16. package/dist/adapters/subprocess-sandbox.js.map +1 -0
  17. package/dist/adapters/workspace-edit.d.ts +19 -0
  18. package/dist/adapters/workspace-edit.d.ts.map +1 -0
  19. package/dist/adapters/workspace-edit.js +36 -0
  20. package/dist/adapters/workspace-edit.js.map +1 -0
  21. package/dist/cli.d.ts +3 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +191 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/core/agent/loader.d.ts +17 -0
  26. package/dist/core/agent/loader.d.ts.map +1 -0
  27. package/dist/core/agent/loader.js +148 -0
  28. package/dist/core/agent/loader.js.map +1 -0
  29. package/dist/core/agent/plugin.d.ts +59 -0
  30. package/dist/core/agent/plugin.d.ts.map +1 -0
  31. package/dist/core/agent/plugin.js +125 -0
  32. package/dist/core/agent/plugin.js.map +1 -0
  33. package/dist/core/agent/registry.d.ts +45 -0
  34. package/dist/core/agent/registry.d.ts.map +1 -0
  35. package/dist/core/agent/registry.js +98 -0
  36. package/dist/core/agent/registry.js.map +1 -0
  37. package/dist/core/agent/resolver.d.ts +16 -0
  38. package/dist/core/agent/resolver.d.ts.map +1 -0
  39. package/dist/core/agent/resolver.js +2 -0
  40. package/dist/core/agent/resolver.js.map +1 -0
  41. package/dist/core/events/bus.d.ts +30 -0
  42. package/dist/core/events/bus.d.ts.map +1 -0
  43. package/dist/core/events/bus.js +112 -0
  44. package/dist/core/events/bus.js.map +1 -0
  45. package/dist/core/events/manifest.d.ts +117 -0
  46. package/dist/core/events/manifest.d.ts.map +1 -0
  47. package/dist/core/events/manifest.js +69 -0
  48. package/dist/core/events/manifest.js.map +1 -0
  49. package/dist/core/permission/engine.d.ts +84 -0
  50. package/dist/core/permission/engine.d.ts.map +1 -0
  51. package/dist/core/permission/engine.js +131 -0
  52. package/dist/core/permission/engine.js.map +1 -0
  53. package/dist/core/permission/ruleset.d.ts +30 -0
  54. package/dist/core/permission/ruleset.d.ts.map +1 -0
  55. package/dist/core/permission/ruleset.js +85 -0
  56. package/dist/core/permission/ruleset.js.map +1 -0
  57. package/dist/core/runtime/compaction.d.ts +59 -0
  58. package/dist/core/runtime/compaction.d.ts.map +1 -0
  59. package/dist/core/runtime/compaction.js +108 -0
  60. package/dist/core/runtime/compaction.js.map +1 -0
  61. package/dist/core/runtime/lease-recovery.d.ts +45 -0
  62. package/dist/core/runtime/lease-recovery.d.ts.map +1 -0
  63. package/dist/core/runtime/lease-recovery.js +112 -0
  64. package/dist/core/runtime/lease-recovery.js.map +1 -0
  65. package/dist/core/runtime/loop-guard.d.ts +51 -0
  66. package/dist/core/runtime/loop-guard.d.ts.map +1 -0
  67. package/dist/core/runtime/loop-guard.js +90 -0
  68. package/dist/core/runtime/loop-guard.js.map +1 -0
  69. package/dist/core/runtime/pi-bridge.d.ts +53 -0
  70. package/dist/core/runtime/pi-bridge.d.ts.map +1 -0
  71. package/dist/core/runtime/pi-bridge.js +259 -0
  72. package/dist/core/runtime/pi-bridge.js.map +1 -0
  73. package/dist/core/runtime/runner.d.ts +127 -0
  74. package/dist/core/runtime/runner.d.ts.map +1 -0
  75. package/dist/core/runtime/runner.js +614 -0
  76. package/dist/core/runtime/runner.js.map +1 -0
  77. package/dist/core/session/ids.d.ts +7 -0
  78. package/dist/core/session/ids.d.ts.map +1 -0
  79. package/dist/core/session/ids.js +18 -0
  80. package/dist/core/session/ids.js.map +1 -0
  81. package/dist/core/session/jsonl-store.d.ts +14 -0
  82. package/dist/core/session/jsonl-store.d.ts.map +1 -0
  83. package/dist/core/session/jsonl-store.js +142 -0
  84. package/dist/core/session/jsonl-store.js.map +1 -0
  85. package/dist/core/session/store.d.ts +25 -0
  86. package/dist/core/session/store.d.ts.map +1 -0
  87. package/dist/core/session/store.js +2 -0
  88. package/dist/core/session/store.js.map +1 -0
  89. package/dist/core/session/types.d.ts +142 -0
  90. package/dist/core/session/types.d.ts.map +1 -0
  91. package/dist/core/session/types.js +2 -0
  92. package/dist/core/session/types.js.map +1 -0
  93. package/dist/core/tools/adapter.d.ts +33 -0
  94. package/dist/core/tools/adapter.d.ts.map +1 -0
  95. package/dist/core/tools/adapter.js +87 -0
  96. package/dist/core/tools/adapter.js.map +1 -0
  97. package/dist/core/tools/builtins.d.ts +13 -0
  98. package/dist/core/tools/builtins.d.ts.map +1 -0
  99. package/dist/core/tools/builtins.js +219 -0
  100. package/dist/core/tools/builtins.js.map +1 -0
  101. package/dist/core/tools/context.d.ts +136 -0
  102. package/dist/core/tools/context.d.ts.map +1 -0
  103. package/dist/core/tools/context.js +2 -0
  104. package/dist/core/tools/context.js.map +1 -0
  105. package/dist/core/tools/def.d.ts +27 -0
  106. package/dist/core/tools/def.d.ts.map +1 -0
  107. package/dist/core/tools/def.js +2 -0
  108. package/dist/core/tools/def.js.map +1 -0
  109. package/dist/core/tools/qa-check.d.ts +30 -0
  110. package/dist/core/tools/qa-check.d.ts.map +1 -0
  111. package/dist/core/tools/qa-check.js +50 -0
  112. package/dist/core/tools/qa-check.js.map +1 -0
  113. package/dist/core/tools/task.d.ts +8 -0
  114. package/dist/core/tools/task.d.ts.map +1 -0
  115. package/dist/core/tools/task.js +33 -0
  116. package/dist/core/tools/task.js.map +1 -0
  117. package/dist/index.d.ts +38 -0
  118. package/dist/index.d.ts.map +1 -0
  119. package/dist/index.js +28 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/server/create-server.d.ts +51 -0
  122. package/dist/server/create-server.d.ts.map +1 -0
  123. package/dist/server/create-server.js +29 -0
  124. package/dist/server/create-server.js.map +1 -0
  125. package/openapi.yaml +239 -0
  126. package/package.json +32 -0
@@ -0,0 +1,614 @@
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { leaseDurationMs } from "../../adapters/index.js";
3
+ import { notifyEventLogListeners } from "../events/bus.js";
4
+ import { PermissionEngine, RejectedError } from "../permission/engine.js";
5
+ import { PartProjector, serializeEmit } from "./pi-bridge.js";
6
+ import { LoopGuard, REPEAT_EDIT_FAILURE_THRESHOLD } from "./loop-guard.js";
7
+ import { newPartId, newSessionId } from "../session/ids.js";
8
+ import { adaptTool, permissionForCall } from "../tools/adapter.js";
9
+ import { builtinTools } from "../tools/builtins.js";
10
+ import { noopSandboxExecutor } from "../../adapters/index.js";
11
+ /** 事件的 sessionId 提取:能自带的自带(message/part/session),其余
12
+ * (message.part.delta 等)用发起 run 的会话 id 兜底。 */
13
+ function sessionIdOf(event, fallbackSessionId) {
14
+ if (event.type === "message.updated")
15
+ return event.data.message.sessionId;
16
+ if (event.type === "message.part.updated")
17
+ return event.data.part.sessionId;
18
+ if (event.type === "session.updated")
19
+ return event.data.session.id;
20
+ return fallbackSessionId;
21
+ }
22
+ const globalRunners = globalThis;
23
+ const activeRuns = globalRunners.__zmzaiFrameworkRuns ?? new Map();
24
+ globalRunners.__zmzaiFrameworkRuns = activeRuns;
25
+ /** 上游中断类错误(F6):模型流偶发终止/连接断开时自动重试一次,避免
26
+ * 偶发中断直接结束任务(实测 relay 透传 "terminated"、上游断流
27
+ * "upstream_http2_stream_error" 等)。余额/鉴权等确定性错误不重试。 */
28
+ export function isRetryableError(message) {
29
+ const normalized = message.toLowerCase();
30
+ return (normalized.includes("terminated") ||
31
+ normalized.includes("econnreset") ||
32
+ normalized.includes("socket hang up") ||
33
+ normalized.includes("etimedout") ||
34
+ normalized.includes("api connection error") ||
35
+ normalized.includes("overloaded") ||
36
+ normalized.includes("timeout") ||
37
+ normalized.includes("upstream") ||
38
+ normalized.includes("stream failed"));
39
+ }
40
+ /** Default ToolContext built from injected workspace + sandbox. emitX events
41
+ * are routed through the runner's eventLog at call time. */
42
+ function defaultToolContext(input) {
43
+ const { session, engine, workspace, sandbox, emit } = input;
44
+ return {
45
+ sessionId: session.id,
46
+ userId: session.userId,
47
+ workspaceId: session.workspaceId,
48
+ agent: session.agent,
49
+ abort: new AbortController().signal,
50
+ ask: engine.ask.bind(engine),
51
+ workspace,
52
+ buildSnapshot: async () => sandbox.buildSnapshot({ userId: session.userId, workspaceId: session.workspaceId, runId: session.id }),
53
+ runSandbox: async (execInput) => {
54
+ const result = await sandbox.run({
55
+ ...execInput,
56
+ userId: session.userId,
57
+ workspaceId: session.workspaceId,
58
+ runId: session.id,
59
+ });
60
+ return result;
61
+ },
62
+ setTodos: async (todos) => {
63
+ await emit({ type: "todo.updated", data: { todos } });
64
+ },
65
+ emitFileEdited: async (payload) => {
66
+ await emit({ type: "file.edited", data: payload });
67
+ },
68
+ emitArtifact: async (payload) => {
69
+ await emit({ type: "artifact.created", data: payload });
70
+ },
71
+ };
72
+ }
73
+ export class SessionRunner {
74
+ deps;
75
+ constructor(deps) {
76
+ this.deps = deps;
77
+ }
78
+ /** fallbackSessionId 由 runLoop 闭包传入(而非实例字段):runner 是进程级
79
+ * 单例,两个会话并发时实例字段会互相覆盖,导致 message.part.delta 等
80
+ * 无自带 sessionId 的事件落到错误的会话事件流里(串台)。 */
81
+ async persist(event, fallbackSessionId) {
82
+ if (event.type === "message.updated") {
83
+ const message = event.data.message;
84
+ const exists = await this.deps.store.getMessages(message.sessionId).then((entries) => entries.some((entry) => entry.info.id === message.id));
85
+ if (exists)
86
+ await this.deps.store.updateMessage(message.id, message);
87
+ else
88
+ await this.deps.store.appendMessage(message);
89
+ }
90
+ else if (event.type === "message.part.updated") {
91
+ await this.deps.store.appendPart(event.data.part).catch(async () => this.deps.store.updatePart(event.data.part));
92
+ }
93
+ else if (event.type === "session.updated") {
94
+ await this.deps.store.updateSession(event.data.session.id, event.data.session);
95
+ }
96
+ const persisted = await this.deps.eventLog.append({ sessionId: sessionIdOf(event, fallbackSessionId), ...event });
97
+ notifyEventLogListeners(persisted);
98
+ }
99
+ async publish(event, sessionId) {
100
+ const persisted = await this.deps.eventLog.append({ sessionId, ...event });
101
+ notifyEventLogListeners(persisted);
102
+ }
103
+ async stampLease(sessionId) {
104
+ if (!this.deps.leaseStore)
105
+ return; // demo/JSONL mode: no lease
106
+ await this.deps.leaseStore.stamp(sessionId, `node:${process.pid}`, new Date(Date.now() + leaseDurationMs)).catch(() => undefined);
107
+ }
108
+ async clearLease(sessionId) {
109
+ if (!this.deps.leaseStore)
110
+ return;
111
+ await this.deps.leaseStore.clear(sessionId).catch(() => undefined);
112
+ }
113
+ async prompt(sessionId, input) {
114
+ const session = await this.deps.store.getSession(sessionId);
115
+ if (!session)
116
+ throw new Error("SESSION_NOT_FOUND");
117
+ if (activeRuns.has(sessionId)) {
118
+ await this.deps.store.enqueuePrompt(sessionId, { text: input.text, ...(input.agent ? { agent: input.agent } : {}), enqueuedAt: new Date().toISOString() });
119
+ return { queued: true };
120
+ }
121
+ void this.runLoop(session, input);
122
+ return { queued: false };
123
+ }
124
+ async replyPermission(sessionId, requestId, reply, feedback) {
125
+ const active = activeRuns.get(sessionId);
126
+ if (!active)
127
+ return false;
128
+ return active.engine.reply(requestId, reply, feedback);
129
+ }
130
+ /** Revokes matching session-scoped rules immediately for a live run and
131
+ * removes their persisted continuation access. */
132
+ async revokePermission(sessionId, permission, patterns) {
133
+ const active = activeRuns.get(sessionId);
134
+ const removedFromEngine = active?.engine.revoke(permission, patterns) ?? false;
135
+ const session = await this.deps.store.getSession(sessionId);
136
+ if (!session)
137
+ return removedFromEngine;
138
+ const next = session.permission.filter((rule) => !(rule.permission === permission && patterns.includes(rule.pattern)));
139
+ const removedFromStore = next.length !== session.permission.length;
140
+ if (removedFromStore)
141
+ await this.deps.store.updateSession(sessionId, { permission: next });
142
+ return removedFromEngine || removedFromStore;
143
+ }
144
+ async abort(sessionId) {
145
+ const active = activeRuns.get(sessionId);
146
+ await this.deps.store.clearQueuedPrompts(sessionId);
147
+ if (!active)
148
+ return;
149
+ // PI's abort signal does not cancel a PermissionEngine.ask() promise.
150
+ // Rejecting pending approvals first releases beforeToolCall so the run can
151
+ // publish its terminal state and a continuation cannot overlap it.
152
+ active.engine.dispose("任务已停止,未处理的授权请求已取消");
153
+ active.abort();
154
+ await active.done;
155
+ }
156
+ /** Builds the compaction transformContext (spec §8.3) when the runner has a
157
+ * summary model configured. Emits a `compaction` part on the latest
158
+ * assistant message so the boundary shows in the transcript. */
159
+ async buildCompaction(session, emit) {
160
+ if (!this.deps.compaction?.enabled || !this.deps.compaction.summaryModel)
161
+ return undefined;
162
+ const { buildCompactionTransform, streamOneText } = await import("./compaction.js");
163
+ return buildCompactionTransform({
164
+ enabled: true,
165
+ contextWindow: this.deps.compaction.contextWindow,
166
+ summaryModel: this.deps.compaction.summaryModel,
167
+ streamOne: async (model, messages) => {
168
+ const streamFn = this.deps.streamFnFor(session);
169
+ return streamOneText(async (m, ctx) => {
170
+ const stream = await streamFn(m, ctx);
171
+ return stream;
172
+ }, model, "你是上下文压缩助手。只输出结构化摘要,不续写对话。", messages);
173
+ },
174
+ onCompacted: (summary) => {
175
+ void (async () => {
176
+ const entries = await this.deps.store.getMessages(session.id);
177
+ const lastAssistant = [...entries].reverse().find((entry) => entry.info.role === "assistant");
178
+ if (!lastAssistant)
179
+ return;
180
+ const part = { id: newPartId(), sessionId: session.id, messageId: lastAssistant.info.id, type: "compaction", summary };
181
+ await this.deps.store.appendPart(part).catch(() => undefined);
182
+ emit({ type: "message.part.updated", data: { part } });
183
+ })();
184
+ },
185
+ });
186
+ }
187
+ /** Layers the session's workspace custom agents (`.zmzai/agents/*.md`) on top
188
+ * of the shared registry without mutating it (spec §6.3). Load failures
189
+ * degrade to the base registry — a malformed md never blocks a run. */
190
+ async registryFor(session) {
191
+ const base = this.deps.registry;
192
+ if (!this.deps.loadWorkspaceAgents)
193
+ return base;
194
+ try {
195
+ const custom = await this.deps.loadWorkspaceAgents(session);
196
+ return base.derive(custom);
197
+ }
198
+ catch {
199
+ return base;
200
+ }
201
+ }
202
+ /** Versioned agents are resolved from the product control plane. A missing
203
+ * version intentionally falls back to the M1-M5 registry so old sessions
204
+ * and standalone consumers remain valid. */
205
+ async resolvedAgentFor(session) {
206
+ if (!this.deps.agentResolver)
207
+ return null;
208
+ try {
209
+ return await this.deps.agentResolver.resolve(session);
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ async runLoop(session, input) {
216
+ const registry = await this.registryFor(session);
217
+ const resolved = await this.resolvedAgentFor(session);
218
+ const agentName = resolved ? resolved.agent.name : input.agent ?? session.agent;
219
+ const agentInfo = resolved?.agent ?? registry.get(agentName) ?? registry.get("default");
220
+ const model = input.model ?? agentInfo?.model ?? session.model;
221
+ const agentRulesets = resolved ? [registry.rulesetsFor("default")[0], resolved.agent.permission] : registry.rulesetsFor(agentInfo?.name ?? "default");
222
+ const engine = new PermissionEngine(session.id, agentRulesets, session.permission, {
223
+ onAsked: async (request) => {
224
+ await this.publish({ type: "session.status", data: { status: "waiting_permission" } }, session.id);
225
+ await this.publish({ type: "permission.asked", data: { request } }, session.id);
226
+ },
227
+ onReplied: async (request, reply) => {
228
+ await this.publish({ type: "permission.replied", data: { id: request.id, reply } }, session.id);
229
+ await this.publish({ type: "session.status", data: { status: "running" } }, session.id);
230
+ },
231
+ onSessionRuleAdded: async (sessionId, rule) => {
232
+ const latest = await this.deps.store.getSession(sessionId);
233
+ if (!latest)
234
+ return;
235
+ const expiresAt = this.deps.sessionRuleTtlMs && this.deps.sessionRuleTtlMs > 0
236
+ ? new Date(Date.now() + this.deps.sessionRuleTtlMs).toISOString()
237
+ : undefined;
238
+ await this.deps.store.updateSession(sessionId, { permission: [...latest.permission, { ...rule, ...(expiresAt ? { expiresAt } : {}) }] });
239
+ },
240
+ });
241
+ const { emit, settled } = serializeEmit(async (event) => {
242
+ await this.persist(event, session.id);
243
+ });
244
+ const projector = new PartProjector({ sessionId: session.id, agent: agentInfo?.name ?? "default", model });
245
+ // Exclude task from contexts that can't nest; include for primary runs.
246
+ const baseTools = [...(this.deps.tools ?? builtinTools), ...(resolved?.tools ?? [])];
247
+ const toolList = session.parentId ? baseTools.filter((def) => def.id !== "task") : baseTools;
248
+ const toolDefs = new Map(toolList.map((def) => [def.id, def]));
249
+ const sandbox = this.deps.sandbox ?? noopSandboxExecutor();
250
+ const workspace = this.deps.workspaceFor(session);
251
+ const emitAsync = async (event) => {
252
+ const persisted = await this.deps.eventLog.append({ sessionId: session.id, ...event });
253
+ notifyEventLogListeners(persisted);
254
+ };
255
+ const toolContext = (this.deps.buildToolContext ?? defaultToolContext)({ session, engine, workspace, sandbox, emit: emitAsync });
256
+ // Subagent spawning is only available to primary (non-child) sessions, and
257
+ // only when the runner can host a nested run (spec §6.4).
258
+ if (!session.parentId) {
259
+ toolContext.spawnSubagent = (spawnInput) => this.spawnSubagent(session, spawnInput, registry, engine);
260
+ }
261
+ const piTools = [...toolDefs.values()].map((def) => adaptTool(def, toolContext));
262
+ let unknownSideEffect = false;
263
+ const compactionTransform = await this.buildCompaction(session, emit);
264
+ const agent = new Agent({
265
+ initialState: {
266
+ systemPrompt: agentInfo?.prompt ?? "",
267
+ model: this.deps.modelFor(model),
268
+ // NOTE: deliberately NOT setting thinkingLevel here. The relay backend
269
+ // rejects `reasoning_effort` in its request schema (400 INVALID_BODY),
270
+ // so any non-"off" thinking level breaks every model call. If reasoning
271
+ // output is desired later, the relay must accept the field first.
272
+ tools: piTools,
273
+ messages: await this.rebuildMessages(session.id),
274
+ },
275
+ streamFn: this.deps.streamFnFor(session),
276
+ toolExecution: "sequential",
277
+ ...(compactionTransform ? { transformContext: compactionTransform } : {}),
278
+ shouldStopAfterTurn: ({ newMessages }) => newMessages.filter((message) => message.role === "assistant").length >= (agentInfo?.steps ?? 12),
279
+ });
280
+ const abortController = new AbortController();
281
+ const abort = () => {
282
+ abortController.abort();
283
+ agent.abort();
284
+ };
285
+ let resolveDone;
286
+ const done = new Promise((resolve) => { resolveDone = resolve; });
287
+ activeRuns.set(session.id, { agent, engine, settled, abort, done });
288
+ await this.stampLease(session.id);
289
+ const loopGuard = new LoopGuard();
290
+ agent.beforeToolCall = async ({ toolCall, args }) => {
291
+ if (unknownSideEffect) {
292
+ return { block: true, reason: "上一个可能产生副作用的动作结果不确定,已暂停执行。请先确认外部系统状态,再决定继续或重试。", terminate: true };
293
+ }
294
+ // 重复失败守卫(edit):同一 (path, oldText) 已连续失败多次时,
295
+ // 重试前先复查文件状态——oldText 已唯一存在则放行清记录,
296
+ // 否则直接拦截(避免无意义空转烧步数)。
297
+ const editDef = toolCall.name === "edit" ? toolDefs.get("edit") : undefined;
298
+ const editArgs = editDef ? editDef.parameters.safeParse(args) : undefined;
299
+ if (editDef && editArgs?.success && loopGuard.needsEditRecheck(editArgs.data.path, editArgs.data.oldText)) {
300
+ const file = await workspace.read(editArgs.data.path);
301
+ const occurrences = file ? file.content.split(editArgs.data.oldText).length - 1 : 0;
302
+ if (occurrences === 1)
303
+ loopGuard.clearEditFailure(editArgs.data.path, editArgs.data.oldText);
304
+ else {
305
+ return {
306
+ block: true,
307
+ reason: `[循环防护] 同一 edit(${editArgs.data.path})已连续失败 ${REPEAT_EDIT_FAILURE_THRESHOLD} 次以上,且文件内容没有变化。` +
308
+ `不要继续重试:先用 read 读取 ${editArgs.data.path} 的最新内容,按实际内容重新选择 oldText。`,
309
+ terminate: false,
310
+ };
311
+ }
312
+ }
313
+ const mapped = permissionForCall(toolDefs, toolCall.name, args);
314
+ if (!mapped)
315
+ return undefined;
316
+ try {
317
+ await engine.ask({
318
+ sessionId: session.id,
319
+ permission: mapped.permission,
320
+ patterns: mapped.patterns,
321
+ always: mapped.always,
322
+ metadata: mapped.metadata,
323
+ tool: { messageId: projector.currentAssistantMessageId ?? "", callId: toolCall.id },
324
+ });
325
+ return undefined;
326
+ }
327
+ catch (error) {
328
+ if (error instanceof RejectedError) {
329
+ // 连续被拒 streak:拼入改变策略指令,阻止模型硬闯不允许的操作
330
+ const advisory = loopGuard.onBlocked(toolCall.name);
331
+ return { block: true, reason: advisory ? `${error.message}\n\n${advisory}` : error.message, terminate: false };
332
+ }
333
+ throw error;
334
+ }
335
+ };
336
+ // storm 断路器:同一工具连续以相同响应失败 3 次(签名不含 args,
337
+ // 防"化妆参数"重试),在第 3 次的结果里注入改变策略指令。
338
+ // 只碰工具结果,不碰 F6 模型级重试路径。
339
+ agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
340
+ const details = typeof result.details === "object" && result.details !== null ? result.details : null;
341
+ if (details?.outcome === "unknown")
342
+ unknownSideEffect = true;
343
+ const resultText = (result.content ?? [])
344
+ .filter((block) => block.type === "text")
345
+ .map((block) => block.text)
346
+ .join("\n");
347
+ let advisory = loopGuard.onToolResult({ toolName: toolCall.name, isError, errorText: resultText });
348
+ if (toolCall.name === "edit") {
349
+ const parsed = toolDefs.get("edit")?.parameters.safeParse(args);
350
+ if (parsed?.success) {
351
+ if (isError) {
352
+ loopGuard.noteEditFailure(parsed.data.path, parsed.data.oldText);
353
+ if (!advisory && loopGuard.needsEditRecheck(parsed.data.path, parsed.data.oldText)) {
354
+ advisory = `[循环防护] 同一 edit 已连续失败 ${REPEAT_EDIT_FAILURE_THRESHOLD} 次(oldText 不存在或不唯一)。停止重试:先 read 该文件获取最新内容,再按实际内容重新选择 oldText。`;
355
+ }
356
+ }
357
+ else {
358
+ loopGuard.clearEditFailure(parsed.data.path, parsed.data.oldText);
359
+ }
360
+ }
361
+ }
362
+ if (!advisory)
363
+ return undefined;
364
+ return { content: [{ type: "text", text: `${advisory}\n\n--- 原始工具结果 ---\n${resultText}` }] };
365
+ };
366
+ agent.subscribe((event) => {
367
+ switch (event.type) {
368
+ case "message_start":
369
+ if (event.message.role === "assistant")
370
+ projector.onAssistantStart(emit);
371
+ break;
372
+ case "message_update": {
373
+ const streamEvent = event.assistantMessageEvent;
374
+ if (streamEvent.type === "text_delta")
375
+ projector.onTextDelta(emit, streamEvent.contentIndex, streamEvent.delta);
376
+ if (streamEvent.type === "thinking_delta")
377
+ projector.onThinkingDelta(emit, streamEvent.contentIndex, streamEvent.delta);
378
+ break;
379
+ }
380
+ case "message_end":
381
+ if (event.message.role === "assistant")
382
+ projector.onAssistantEnd(emit, event.message);
383
+ break;
384
+ case "tool_execution_start":
385
+ projector.onToolExecutionStart(emit, event.toolCallId, event.toolName, event.args, toolDefs.get(event.toolName)?.label);
386
+ break;
387
+ case "tool_execution_update":
388
+ projector.onToolExecutionUpdate(emit, event.toolCallId, event.partialResult);
389
+ break;
390
+ case "tool_execution_end":
391
+ projector.onToolExecutionEnd(emit, event.toolCallId, event.result, event.isError);
392
+ break;
393
+ }
394
+ });
395
+ await this.publish({ type: "session.status", data: { status: "running" } }, session.id);
396
+ try {
397
+ projector.onUserPrompt(emit, input.text);
398
+ await agent.prompt(input.text);
399
+ await settled();
400
+ let failed = agent.state.errorMessage;
401
+ if (unknownSideEffect) {
402
+ await this.publish({ type: "session.status", data: { status: "waiting_input" } }, session.id);
403
+ }
404
+ else {
405
+ // 自动重试(F6):上游中断类错误(terminated/econnreset/timeout 等)
406
+ // 带退避重试,最多 3 次。PI 失败时会注入一条 assistant failure 占位
407
+ // 消息(last 是 assistant → continue() 被拒),换成合成 user 消息驱动
408
+ // continue 重新生成;store 里同步的 failure 消息保留展示(UI 显示
409
+ // "出错了"有诊断价值)。退避 500ms→1s→2s 避免对上游施压。
410
+ const MAX_RETRIES = 3;
411
+ for (let attempt = 0; attempt < MAX_RETRIES && failed && isRetryableError(failed); attempt += 1) {
412
+ if (attempt > 0)
413
+ await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
414
+ const messages = agent.state.messages;
415
+ const last = messages[messages.length - 1];
416
+ if (last?.role === "assistant" && "errorMessage" in last && last.errorMessage) {
417
+ agent.state.messages = [
418
+ ...messages.slice(0, -1),
419
+ { role: "user", content: [{ type: "text", text: "(上轮回复生成中断,请继续完成回复。)" }], timestamp: Date.now() },
420
+ ];
421
+ }
422
+ try {
423
+ await agent.continue();
424
+ await settled();
425
+ failed = agent.state.errorMessage;
426
+ }
427
+ catch (retryError) {
428
+ failed = retryError instanceof Error ? retryError.message : "Agent 重试失败";
429
+ }
430
+ }
431
+ if (failed) {
432
+ await this.publish({ type: "session.error", data: { name: "APIError", message: failed } }, session.id);
433
+ }
434
+ await this.publish({ type: "session.status", data: { status: "idle" } }, session.id);
435
+ }
436
+ }
437
+ catch (error) {
438
+ await settled();
439
+ const aborted = abortController.signal.aborted;
440
+ await this.publish(unknownSideEffect
441
+ ? { type: "session.status", data: { status: "waiting_input" } }
442
+ : aborted
443
+ ? { type: "session.status", data: { status: "idle" } }
444
+ : { type: "session.error", data: { name: "AgentRuntimeError", message: error instanceof Error ? error.message : "Agent 运行失败" } }, session.id);
445
+ }
446
+ finally {
447
+ activeRuns.delete(session.id);
448
+ engine.dispose();
449
+ await this.clearLease(session.id);
450
+ resolveDone();
451
+ }
452
+ if (unknownSideEffect)
453
+ return;
454
+ // FIFO queued prompts (spec §13.3): settle fully, then take the next one.
455
+ const next = await this.deps.store.dequeuePrompt(session.id);
456
+ if (next) {
457
+ const latest = await this.deps.store.getSession(session.id);
458
+ if (latest)
459
+ await this.runLoop(latest, { text: next.text, ...(next.agent ? { agent: next.agent } : {}) });
460
+ }
461
+ }
462
+ /** Spawns a subagent child session (spec §6.4): depth-capped, permission
463
+ * stamped from parent session + subagent preset, runs a nested PI loop to
464
+ * completion, and returns the child's final assistant text as the parent
465
+ * tool's result. Awaits the nested runLoop directly. */
466
+ async spawnSubagent(parent, input, registry, parentEngine) {
467
+ const depth = await this.sessionDepth(parent);
468
+ if (depth >= this.deps.subagentDepth) {
469
+ throw new Error(`子代理嵌套深度超过限制(${this.deps.subagentDepth})`);
470
+ }
471
+ const subagent = registry.get(input.subagentType);
472
+ if (!subagent || (subagent.mode !== "subagent" && subagent.mode !== "all")) {
473
+ throw new Error(`未知或非子代理类型:${input.subagentType}`);
474
+ }
475
+ await parentEngine.ask({
476
+ sessionId: parent.id,
477
+ permission: "task",
478
+ patterns: [input.subagentType],
479
+ always: ["*"],
480
+ metadata: { subagent: input.subagentType, description: input.description },
481
+ });
482
+ const childSession = await createFrameworkSession({
483
+ store: this.deps.store,
484
+ userId: parent.userId,
485
+ workspaceId: parent.workspaceId,
486
+ agent: input.subagentType,
487
+ model: subagent.model ?? parent.model,
488
+ prompt: input.prompt,
489
+ parentId: parent.id,
490
+ title: input.description,
491
+ permission: [...parent.permission],
492
+ });
493
+ try {
494
+ // Run the child with a FRESH runner (not this instance's nested runLoop):
495
+ // reusing runLoop here would deadlock on the shared in-process state and
496
+ // the parent's event chain. A dedicated runner owns the child's loop.
497
+ const childRunner = new SessionRunner(this.deps);
498
+ await childRunner.runLoop(childSession, { text: input.prompt, agent: input.subagentType });
499
+ const summary = await this.lastAssistantText(childSession.id);
500
+ await this.recordSubtask(parent, { prompt: input.prompt, description: input.description, agent: input.subagentType, childSessionId: childSession.id });
501
+ return { childSessionId: childSession.id, summary: summary || "(子代理无文本输出)", state: "completed" };
502
+ }
503
+ catch (error) {
504
+ await this.recordSubtask(parent, { prompt: input.prompt, description: input.description, agent: input.subagentType, childSessionId: childSession.id });
505
+ return { childSessionId: childSession.id, summary: `子代理失败:${error instanceof Error ? error.message : "未知错误"}`, state: "error" };
506
+ }
507
+ }
508
+ /** Persists a subtask part on the parent's latest assistant message so the
509
+ * transcript links to the child session (spec §6.4 step 5). */
510
+ async recordSubtask(parent, input) {
511
+ const entries = await this.deps.store.getMessages(parent.id);
512
+ const lastAssistant = [...entries].reverse().find((entry) => entry.info.role === "assistant");
513
+ if (!lastAssistant)
514
+ return;
515
+ const part = {
516
+ id: newPartId(),
517
+ sessionId: parent.id,
518
+ messageId: lastAssistant.info.id,
519
+ type: "subtask",
520
+ prompt: input.prompt,
521
+ description: input.description,
522
+ agent: input.agent,
523
+ childSessionId: input.childSessionId,
524
+ };
525
+ await this.deps.store.appendPart(part).catch(() => undefined);
526
+ await this.publish({ type: "message.part.updated", data: { part } }, parent.id);
527
+ }
528
+ async lastAssistantText(sessionId) {
529
+ const entries = await this.deps.store.getMessages(sessionId);
530
+ for (let index = entries.length - 1; index >= 0; index--) {
531
+ const entry = entries[index];
532
+ if (entry.info.role !== "assistant")
533
+ continue;
534
+ const text = entry.parts
535
+ .filter((part) => part.type === "text")
536
+ .map((part) => part.text)
537
+ .join("\n")
538
+ .trim();
539
+ if (text)
540
+ return text;
541
+ }
542
+ return "";
543
+ }
544
+ async sessionDepth(session) {
545
+ let depth = 0;
546
+ let current = session;
547
+ while (current.parentId) {
548
+ depth += 1;
549
+ const next = await this.deps.store.getSession(current.parentId);
550
+ if (!next)
551
+ break;
552
+ current = next;
553
+ }
554
+ return depth;
555
+ }
556
+ async rebuildMessages(sessionId) {
557
+ const entries = await this.deps.store.getMessages(sessionId);
558
+ const messages = [];
559
+ for (const { info, parts } of entries) {
560
+ if (info.role === "user") {
561
+ const text = parts
562
+ .filter((part) => part.type === "text")
563
+ .map((part) => part.text)
564
+ .join("\n");
565
+ messages.push({ role: "user", content: text || "(空消息)", timestamp: Date.parse(info.time.created) || Date.now() });
566
+ }
567
+ else {
568
+ const text = parts
569
+ .filter((part) => part.type === "text")
570
+ .map((part) => part.text)
571
+ .join("\n");
572
+ if (!text)
573
+ continue;
574
+ messages.push({
575
+ role: "assistant",
576
+ content: [{ type: "text", text }],
577
+ api: "openai-completions",
578
+ provider: info.model.providerId,
579
+ model: info.model.modelId,
580
+ usage: { input: info.tokens?.input ?? 0, output: info.tokens?.output ?? 0, cacheRead: info.tokens?.cacheRead ?? 0, cacheWrite: info.tokens?.cacheWrite ?? 0, totalTokens: (info.tokens?.input ?? 0) + (info.tokens?.output ?? 0) + (info.tokens?.cacheRead ?? 0) + (info.tokens?.cacheWrite ?? 0) },
581
+ stopReason: info.error ? "error" : "stop",
582
+ ...(info.error ? { errorMessage: info.error.message } : {}),
583
+ timestamp: Date.parse(info.time.created) || Date.now(),
584
+ });
585
+ }
586
+ }
587
+ return messages;
588
+ }
589
+ }
590
+ export async function createFrameworkSession(input) {
591
+ const session = {
592
+ id: input.id ?? newSessionId(),
593
+ workspaceId: input.workspaceId,
594
+ userId: input.userId,
595
+ ...(input.parentId ? { parentId: input.parentId } : {}),
596
+ title: (input.title ?? input.prompt ?? "新会话").slice(0, 40),
597
+ agent: input.agent ?? "default",
598
+ ...(input.agentId ? { agentId: input.agentId } : {}),
599
+ ...(input.agentVersionId ? { agentVersionId: input.agentVersionId } : {}),
600
+ model: input.model,
601
+ permission: input.permission ?? [],
602
+ queuedPrompts: [],
603
+ time: { created: new Date().toISOString(), updated: new Date().toISOString() },
604
+ };
605
+ await input.store.createSession(session);
606
+ return session;
607
+ }
608
+ export function isSessionActive(sessionId) {
609
+ return activeRuns.has(sessionId);
610
+ }
611
+ // The package runner is storage-agnostic: stores (Mongo/JSONL), event logs,
612
+ // workspace backends and sandbox executors are all injected via RunnerDeps.
613
+ // Products assemble them in createServer(); the CLI uses JSONL + subprocess.
614
+ //# sourceMappingURL=runner.js.map