@rivus/agent 0.14.2 → 0.14.4

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 (37) hide show
  1. package/README.md +1 -1
  2. package/dist/acp.js +1 -2
  3. package/dist/bootstrap/pi-feishu.d.ts +1 -1
  4. package/dist/bootstrap/pi-feishu.js +4 -4
  5. package/dist/chunks/agent-loop.d.ts +55 -300
  6. package/dist/chunks/agent-loop.js +3 -1123
  7. package/dist/chunks/background-session-authority.js +230 -0
  8. package/dist/chunks/background-session-control-input.js +51 -0
  9. package/dist/chunks/background-session-service.d.ts +382 -0
  10. package/dist/chunks/index.d.ts +1201 -538
  11. package/dist/chunks/pi-tool-proxy.d.ts +22 -90
  12. package/dist/chunks/pi.js +5 -2
  13. package/dist/chunks/rivus-agent-definition-resolver.js +508 -0
  14. package/dist/chunks/rivus-daemon-cli.js +2776 -3244
  15. package/dist/chunks/rivus-plugin-testkit.d.ts +175 -2
  16. package/dist/chunks/rivus-plugin-testkit.js +11 -4
  17. package/dist/chunks/rivus-skill.d.ts +95 -0
  18. package/dist/chunks/sha256-digest.js +2 -7
  19. package/dist/chunks/src.js +11941 -7001
  20. package/dist/chunks/tool-input-digest.js +158 -0
  21. package/dist/cli.js +764 -712
  22. package/dist/index.d.ts +6 -7
  23. package/dist/index.js +7 -8
  24. package/dist/mcp.d.ts +48 -9
  25. package/dist/mcp.js +146 -20
  26. package/dist/pi.d.ts +3 -4
  27. package/dist/pi.js +1 -1
  28. package/package.json +5 -5
  29. package/dist/chunks/api.d.ts +0 -70
  30. package/dist/chunks/api.js +0 -471
  31. package/dist/chunks/api2.d.ts +0 -387
  32. package/dist/chunks/api2.js +0 -1331
  33. package/dist/chunks/api3.d.ts +0 -402
  34. package/dist/chunks/module.js +0 -267
  35. package/dist/chunks/pi-skill-tool.js +0 -460
  36. package/dist/chunks/spi.d.ts +0 -1
  37. package/dist/chunks/spi.js +0 -2
@@ -1,1331 +0,0 @@
1
- import { n as createRandomId, t as createSha256Digest } from "./sha256-digest.js";
2
- import { Cause, Deferred, Effect, Fiber } from "effect";
3
- //#region src/platform/concurrency/periodic-effect-loop.ts
4
- function createEffectLoopDriver(loop) {
5
- let running = false;
6
- let fiber;
7
- return {
8
- running: () => running,
9
- start: () => {
10
- if (running) return;
11
- running = true;
12
- fiber = Effect.runFork(loop);
13
- },
14
- stop: async () => {
15
- running = false;
16
- const activeFiber = fiber;
17
- fiber = void 0;
18
- if (activeFiber) await Effect.runPromise(Fiber.interrupt(activeFiber));
19
- }
20
- };
21
- }
22
- function createPeriodicEffectLoop(options) {
23
- const reportError = (error) => {
24
- const reported = options.onError?.(error);
25
- return reported instanceof Promise ? Effect.promise(() => reported) : Effect.void;
26
- };
27
- const continueAfterReporting = (effect) => effect.pipe(Effect.asVoid, Effect.catchAll((error) => reportError(error)));
28
- const cycle = Effect.suspend(() => continueAfterReporting(options.run()).pipe(Effect.flatMap(() => continueAfterReporting(options.sleep(options.intervalMs))), Effect.flatMap(() => cycle)));
29
- return createEffectLoopDriver(cycle);
30
- }
31
- //#endregion
32
- //#region src/modules/background-session/application/authority/background-session-identity.ts
33
- const BACKGROUND_SESSION_SESSION_KEY_PREFIX = "background";
34
- function createBackgroundSessionKey(sessionId) {
35
- return `${BACKGROUND_SESSION_SESSION_KEY_PREFIX}:${sessionId}`;
36
- }
37
- function createBackgroundSessionStepSourceMessageId(sessionId, stepCount) {
38
- return `bg:${sessionId}:step:${stepCount}`;
39
- }
40
- //#endregion
41
- //#region src/modules/background-session/application/authority/background-session-authority.ts
42
- const BACKGROUND_SESSION_TOOL_IDS = [
43
- "background.start",
44
- "background.wait",
45
- "background.list",
46
- "background.status",
47
- "background.send",
48
- "background.stop"
49
- ];
50
- const BACKGROUND_SESSION_START_TOOL_ID = "background.start";
51
- const BACKGROUND_SESSION_TOOL_PLUGIN_ID = "rivus-core";
52
- const BACKGROUND_SESSION_TOOL_VERSION = "1.0.0";
53
- function createBackgroundSessionToolContracts() {
54
- return [
55
- Object.freeze({
56
- description: "Start a background agent session. Use when the request must wait for external changes, observe over time, or continue working after the foreground run ends. Returns a stable session id immediately; the foreground response can finish here. The detached session continues with the granted Skills, CLI, Tools, Project Space, and Memory of this agent.",
57
- digest: contractDigest("background.start"),
58
- id: "background.start",
59
- idempotency: "supported",
60
- inputSchema: Object.freeze({
61
- additionalProperties: false,
62
- properties: Object.freeze({
63
- displayName: {
64
- type: "string",
65
- maxLength: 200
66
- },
67
- prompt: {
68
- type: "string",
69
- minLength: 1,
70
- maxLength: 2e4
71
- }
72
- }),
73
- required: ["prompt"],
74
- type: "object"
75
- }),
76
- pluginId: BACKGROUND_SESSION_TOOL_PLUGIN_ID,
77
- risk: "mutate",
78
- version: BACKGROUND_SESSION_TOOL_VERSION
79
- }),
80
- Object.freeze({
81
- description: "Pause the current background session durably and end the current step. Call with delayMs to resume after a delay, with until to resume at an absolute ISO time, or with neither to wait for user input. After this call no further tool calls are accepted in this step.",
82
- digest: contractDigest("background.wait"),
83
- id: "background.wait",
84
- idempotency: "supported",
85
- inputSchema: Object.freeze({
86
- additionalProperties: false,
87
- properties: Object.freeze({
88
- delayMs: {
89
- type: "integer",
90
- minimum: 1e3,
91
- maximum: 864e5
92
- },
93
- reason: {
94
- type: "string",
95
- maxLength: 500
96
- },
97
- until: {
98
- type: "string",
99
- maxLength: 64
100
- }
101
- }),
102
- type: "object"
103
- }),
104
- pluginId: BACKGROUND_SESSION_TOOL_PLUGIN_ID,
105
- risk: "mutate",
106
- version: BACKGROUND_SESSION_TOOL_VERSION
107
- }),
108
- Object.freeze({
109
- description: "List background sessions owned by this conversation, newest first. Optionally filter by phase and limit the number of results.",
110
- digest: contractDigest("background.list"),
111
- id: "background.list",
112
- idempotency: "supported",
113
- inputSchema: Object.freeze({
114
- additionalProperties: false,
115
- properties: Object.freeze({
116
- limit: {
117
- type: "integer",
118
- minimum: 1,
119
- maximum: 50
120
- },
121
- phase: {
122
- enum: [
123
- "queued",
124
- "running",
125
- "waiting",
126
- "input-required",
127
- "stopping",
128
- "stopped",
129
- "completed",
130
- "failed",
131
- "reconciliation-required"
132
- ],
133
- type: "string"
134
- }
135
- }),
136
- type: "object"
137
- }),
138
- pluginId: BACKGROUND_SESSION_TOOL_PLUGIN_ID,
139
- risk: "observe",
140
- version: BACKGROUND_SESSION_TOOL_VERSION
141
- }),
142
- Object.freeze({
143
- description: "Return the current phase, step counts, wake time, and result of one background session owned by this conversation.",
144
- digest: contractDigest("background.status"),
145
- id: "background.status",
146
- idempotency: "supported",
147
- inputSchema: Object.freeze({
148
- additionalProperties: false,
149
- properties: Object.freeze({ sessionId: {
150
- type: "string",
151
- minLength: 1,
152
- maxLength: 200
153
- } }),
154
- required: ["sessionId"],
155
- type: "object"
156
- }),
157
- pluginId: BACKGROUND_SESSION_TOOL_PLUGIN_ID,
158
- risk: "observe",
159
- version: BACKGROUND_SESSION_TOOL_VERSION
160
- }),
161
- Object.freeze({
162
- description: "Send new user instruction text to a background session owned by this conversation and wake it. The input is delivered exactly once in the next step.",
163
- digest: contractDigest("background.send"),
164
- id: "background.send",
165
- idempotency: "supported",
166
- inputSchema: Object.freeze({
167
- additionalProperties: false,
168
- properties: Object.freeze({
169
- message: {
170
- type: "string",
171
- minLength: 1,
172
- maxLength: 2e4
173
- },
174
- sessionId: {
175
- type: "string",
176
- minLength: 1,
177
- maxLength: 200
178
- }
179
- }),
180
- required: ["message", "sessionId"],
181
- type: "object"
182
- }),
183
- pluginId: BACKGROUND_SESSION_TOOL_PLUGIN_ID,
184
- risk: "mutate",
185
- version: BACKGROUND_SESSION_TOOL_VERSION
186
- }),
187
- Object.freeze({
188
- description: "Stop a background session owned by this conversation. Persists the cancellation, aborts the active step and its owned process, and delivers a terminal notice.",
189
- digest: contractDigest("background.stop"),
190
- id: "background.stop",
191
- idempotency: "supported",
192
- inputSchema: Object.freeze({
193
- additionalProperties: false,
194
- properties: Object.freeze({
195
- reason: {
196
- type: "string",
197
- maxLength: 500
198
- },
199
- sessionId: {
200
- type: "string",
201
- minLength: 1,
202
- maxLength: 200
203
- }
204
- }),
205
- required: ["sessionId"],
206
- type: "object"
207
- }),
208
- pluginId: BACKGROUND_SESSION_TOOL_PLUGIN_ID,
209
- risk: "mutate",
210
- version: BACKGROUND_SESSION_TOOL_VERSION
211
- })
212
- ];
213
- }
214
- function backgroundSessionToolIds() {
215
- return [...BACKGROUND_SESSION_TOOL_IDS];
216
- }
217
- function isBackgroundSessionToolId(toolId) {
218
- return BACKGROUND_SESSION_TOOL_IDS.includes(toolId);
219
- }
220
- function extendBackgroundSessionDefinition(definition) {
221
- const contracts = createBackgroundSessionToolContracts();
222
- const existingIds = new Set(definition.tools.map(({ id }) => id));
223
- const additions = contracts.filter((contract) => !existingIds.has(contract.id));
224
- const toolGrantSet = Object.freeze({
225
- revision: grantRevision(definition.toolGrantSet.revision, additions.map(({ id }) => id)),
226
- toolIds: Object.freeze([...definition.toolGrantSet.toolIds, ...additions.map(({ id }) => id)].sort())
227
- });
228
- return Object.freeze({
229
- ...definition,
230
- tools: Object.freeze([...definition.tools, ...additions]),
231
- toolGrantSet
232
- });
233
- }
234
- function narrowBackgroundSessionDefinition(definition) {
235
- const childToolIds = definition.toolGrantSet.toolIds.filter((id) => id !== BACKGROUND_SESSION_START_TOOL_ID);
236
- const toolGrantSet = Object.freeze({
237
- revision: grantRevision(definition.toolGrantSet.revision, childToolIds),
238
- toolIds: Object.freeze(childToolIds)
239
- });
240
- return Object.freeze({
241
- ...definition,
242
- tools: Object.freeze(definition.tools.filter(({ id }) => id !== BACKGROUND_SESSION_START_TOOL_ID)),
243
- toolGrantSet
244
- });
245
- }
246
- function grantRevision(parentRevision, toolIds) {
247
- return createSha256Digest(JSON.stringify({
248
- parentRevision,
249
- toolIds: [...toolIds].sort()
250
- }));
251
- }
252
- function contractDigest(toolId) {
253
- return createSha256Digest(`background-tool:${toolId}:${BACKGROUND_SESSION_TOOL_VERSION}`);
254
- }
255
- //#endregion
256
- //#region src/modules/background-session/application/control/background-session-control-input.ts
257
- function readBackgroundSessionObject(input, allowed, error) {
258
- if (input === null || typeof input !== "object" || Array.isArray(input)) throw error("background tool input must be an object");
259
- const record = input;
260
- const unknown = Object.keys(record).find((key) => !allowed.includes(key));
261
- if (unknown) throw error(`field is not allowed: ${unknown}`);
262
- return record;
263
- }
264
- function readBackgroundSessionString(value, name, error) {
265
- if (typeof value !== "string" || value.trim() === "") throw error(`${name} must be a non-empty string`);
266
- return value;
267
- }
268
- function readBackgroundSessionInteger(value, name, error) {
269
- if (!Number.isSafeInteger(value) || value < 1) throw error(`${name} must be a positive integer`);
270
- return value;
271
- }
272
- function readBackgroundSessionPhase(value, error) {
273
- const phases = [
274
- "queued",
275
- "running",
276
- "waiting",
277
- "input-required",
278
- "stopping",
279
- "stopped",
280
- "completed",
281
- "failed",
282
- "reconciliation-required"
283
- ];
284
- if (typeof value !== "string" || !phases.includes(value)) throw error(`phase must be one of ${phases.join(", ")}`);
285
- return value;
286
- }
287
- function readBackgroundSessionWaitInput(input, error) {
288
- const { delayMs, reason, until } = readBackgroundSessionObject(input, [
289
- "delayMs",
290
- "reason",
291
- "until"
292
- ], error);
293
- const args = {};
294
- if (delayMs !== void 0) args.delayMs = readBackgroundSessionInteger(delayMs, "delayMs", error);
295
- if (reason !== void 0) args.reason = readBackgroundSessionString(reason, "reason", error);
296
- if (until !== void 0) args.until = readBackgroundSessionString(until, "until", error);
297
- return args;
298
- }
299
- //#endregion
300
- //#region src/modules/background-session/application/control/background-session-control.ts
301
- function createBackgroundSessionControl(options) {
302
- return { handle: async (command, context, input) => {
303
- const executionContext = {
304
- agentId: context.agentId,
305
- callId: `mcp-call:${createRandomId()}`,
306
- instanceId: `mcp:${context.agentId}`,
307
- policyEpoch: context.policyEpoch,
308
- runId: context.runId,
309
- sessionKey: context.sessionKey,
310
- sourceMessageId: context.sourceMessageId,
311
- toolId: `background.${command}`,
312
- toolVersion: "1.0.0",
313
- origin: toToolOrigin(context.origin)
314
- };
315
- switch (command) {
316
- case "start": {
317
- const { displayName, prompt } = readObject(input, ["displayName", "prompt"]);
318
- if (typeof prompt !== "string" || prompt.trim() === "") throw new BackgroundSessionControlError("background.start requires a non-empty prompt");
319
- const sessionId = `bg-${createRandomId()}`;
320
- return options.service.start({
321
- authority: {
322
- ...context.authority,
323
- sessionKey: createBackgroundSessionKey(sessionId)
324
- },
325
- context: executionContext,
326
- ...displayName === void 0 ? {} : { displayName: readString(displayName, "displayName") },
327
- prompt,
328
- sessionId
329
- });
330
- }
331
- case "wait": return options.service.wait({
332
- context: executionContext,
333
- ...readBackgroundSessionWaitInput(input, (message) => new BackgroundSessionControlError(message))
334
- });
335
- case "list": {
336
- const { limit, phase } = readObject(input, ["limit", "phase"]);
337
- return options.service.list({
338
- context: executionContext,
339
- ...limit === void 0 ? {} : { limit: readInteger(limit, "limit") },
340
- ...phase === void 0 ? {} : { phase: readPhase(phase) }
341
- });
342
- }
343
- case "status": {
344
- const { sessionId } = readObject(input, ["sessionId"]);
345
- return options.service.status({
346
- context: executionContext,
347
- sessionId: readString(sessionId, "sessionId")
348
- });
349
- }
350
- case "send": {
351
- const { message, sessionId } = readObject(input, ["message", "sessionId"]);
352
- return options.service.send({
353
- context: executionContext,
354
- message: readString(message, "message"),
355
- sessionId: readString(sessionId, "sessionId")
356
- });
357
- }
358
- case "stop": {
359
- const { reason, sessionId } = readObject(input, ["reason", "sessionId"]);
360
- return options.service.stop({
361
- context: executionContext,
362
- ...reason === void 0 ? {} : { reason: readString(reason, "reason") },
363
- sessionId: readString(sessionId, "sessionId")
364
- });
365
- }
366
- }
367
- } };
368
- }
369
- var BackgroundSessionControlError = class extends Error {
370
- name = "BackgroundSessionControlError";
371
- };
372
- function toControlContext(input) {
373
- return {
374
- agentId: input.agentId,
375
- authority: input.authority,
376
- origin: {
377
- allowedActorOpenIds: input.origin.allowedActorOpenIds,
378
- ...input.origin.conversationId === void 0 ? {} : { conversationId: input.origin.conversationId },
379
- endpointId: input.origin.endpointId,
380
- tenantKey: input.origin.tenantKey
381
- },
382
- policyEpoch: input.policyEpoch,
383
- runId: `mcp:${createRandomId()}`,
384
- sessionKey: input.sessionKey,
385
- sourceMessageId: input.sourceMessageId ?? `mcp:${createRandomId()}`
386
- };
387
- }
388
- function toToolOrigin(origin) {
389
- return {
390
- allowedActorOpenIds: origin.allowedActorOpenIds,
391
- endpointId: origin.endpointId,
392
- tenantKey: origin.tenantKey,
393
- ...origin.conversationId === void 0 ? {} : { conversationId: origin.conversationId }
394
- };
395
- }
396
- function readObject(input, allowed) {
397
- return readBackgroundSessionObject(input, allowed, (message) => new BackgroundSessionControlError(message));
398
- }
399
- function readString(value, name) {
400
- return readBackgroundSessionString(value, name, (message) => new BackgroundSessionControlError(message));
401
- }
402
- function readInteger(value, name) {
403
- return readBackgroundSessionInteger(value, name, (message) => new BackgroundSessionControlError(message));
404
- }
405
- function readPhase(value) {
406
- return readBackgroundSessionPhase(value, (message) => new BackgroundSessionControlError(message));
407
- }
408
- //#endregion
409
- //#region src/modules/background-session/domain/session/background-session-model.ts
410
- const BACKGROUND_SESSION_TERMINAL_PHASES = /* @__PURE__ */ new Set([
411
- "stopped",
412
- "completed",
413
- "failed",
414
- "reconciliation-required"
415
- ]);
416
- function isBackgroundSessionTerminalPhase(phase) {
417
- return BACKGROUND_SESSION_TERMINAL_PHASES.has(phase);
418
- }
419
- var BackgroundSessionTransitionDenied = class extends Error {
420
- sessionId;
421
- name = "BackgroundSessionTransitionDenied";
422
- constructor(sessionId, message) {
423
- super(message);
424
- this.sessionId = sessionId;
425
- }
426
- };
427
- //#endregion
428
- //#region src/modules/background-session/domain/session/background-session-transitions.ts
429
- function createBackgroundSession(input) {
430
- if (!input.sessionId.trim()) throw new BackgroundSessionTransitionDenied(input.sessionId, "background session id must not be empty");
431
- if (!input.authority.sessionKey.trim()) throw new BackgroundSessionTransitionDenied(input.sessionId, "background session key must not be empty");
432
- if (!input.authority.agentId.trim()) throw new BackgroundSessionTransitionDenied(input.sessionId, "background session authority requires an agent id");
433
- if (!input.sourceMessageId.trim()) throw new BackgroundSessionTransitionDenied(input.sessionId, "background session requires a trusted source message id");
434
- return Object.freeze({
435
- authority: Object.freeze({
436
- ...input.authority,
437
- memoryScopes: Object.freeze([...input.authority.memoryScopes])
438
- }),
439
- consecutiveFailures: 0,
440
- createdAt: input.now,
441
- displayName: input.displayName,
442
- origin: Object.freeze({
443
- ...input.origin,
444
- allowedActorOpenIds: Object.freeze([...input.origin.allowedActorOpenIds])
445
- }),
446
- parentRunId: input.parentRunId,
447
- pendingInput: Object.freeze([]),
448
- phase: "queued",
449
- prompt: input.prompt,
450
- revision: 1,
451
- sessionId: input.sessionId,
452
- sourceMessageId: input.sourceMessageId,
453
- stepCount: 0,
454
- updatedAt: input.now,
455
- wakeCount: 0
456
- });
457
- }
458
- function claimBackgroundSession(state, input) {
459
- if (isBackgroundSessionTerminalPhase(state.phase)) throw new BackgroundSessionTransitionDenied(state.sessionId, `terminal ${state.phase} session cannot run again`);
460
- if (state.phase !== "queued" && state.phase !== "waiting") throw new BackgroundSessionTransitionDenied(state.sessionId, `background session cannot start a step while ${state.phase}`);
461
- if (state.wakeAt !== void 0 && state.wakeAt > input.now) throw new BackgroundSessionTransitionDenied(state.sessionId, "background session is not due");
462
- return next(state, {
463
- currentStepRunId: input.stepRunId,
464
- lastWakeText: input.wakeText,
465
- lease: input.lease,
466
- pendingInput: Object.freeze([]),
467
- phase: "running",
468
- stepCount: state.stepCount + 1,
469
- updatedAt: input.now,
470
- wakeAt: void 0,
471
- wakeCount: state.wakeCount + 1
472
- });
473
- }
474
- function renewBackgroundSessionLease(state, lease) {
475
- if (state.lease === void 0) throw new BackgroundSessionTransitionDenied(state.sessionId, "cannot renew a lease that is not held");
476
- if (state.lease.owner !== lease.owner || state.lease.epoch !== lease.epoch) throw new BackgroundSessionTransitionDenied(state.sessionId, "cannot renew a lease from another worker");
477
- return next(state, { lease });
478
- }
479
- function releaseBackgroundSessionLease(state) {
480
- if (state.lease === void 0) throw new BackgroundSessionTransitionDenied(state.sessionId, "no lease to release");
481
- return next(state, { lease: void 0 });
482
- }
483
- function suspendBackgroundSession(state, input) {
484
- if (state.phase !== "running") throw new BackgroundSessionTransitionDenied(state.sessionId, `background session cannot wait while ${state.phase}`);
485
- if (input.wakeAt !== void 0 && input.wakeAt <= input.now) throw new BackgroundSessionTransitionDenied(state.sessionId, "background session wake time must be in the future");
486
- return next(state, {
487
- currentStepRunId: void 0,
488
- phase: input.wakeAt === void 0 ? "input-required" : "waiting",
489
- updatedAt: input.now,
490
- ...input.wakeAt === void 0 ? {} : { wakeAt: input.wakeAt }
491
- });
492
- }
493
- function completeBackgroundSessionStep(state, input) {
494
- if (state.phase !== "running") throw new BackgroundSessionTransitionDenied(state.sessionId, `background session cannot complete while ${state.phase}`);
495
- return next(state, {
496
- consecutiveFailures: 0,
497
- currentStepRunId: void 0,
498
- lease: void 0,
499
- phase: "completed",
500
- result: {
501
- completedAt: input.now,
502
- stepRunId: input.stepRunId,
503
- text: input.text
504
- },
505
- updatedAt: input.now
506
- });
507
- }
508
- function failBackgroundSessionStep(state, input) {
509
- if (state.phase !== "running") throw new BackgroundSessionTransitionDenied(state.sessionId, `background session cannot fail while ${state.phase}`);
510
- if (input.retryable) {
511
- if (input.wakeAt === void 0 || input.wakeAt <= input.now) throw new BackgroundSessionTransitionDenied(state.sessionId, "retryable failure requires a future wake time");
512
- return next(state, {
513
- consecutiveFailures: state.consecutiveFailures + 1,
514
- currentStepRunId: void 0,
515
- errorMessage: input.errorMessage,
516
- lease: void 0,
517
- phase: "queued",
518
- updatedAt: input.now,
519
- wakeAt: input.wakeAt
520
- });
521
- }
522
- return next(state, {
523
- consecutiveFailures: state.consecutiveFailures + 1,
524
- currentStepRunId: void 0,
525
- errorMessage: input.errorMessage,
526
- lease: void 0,
527
- phase: "failed",
528
- updatedAt: input.now
529
- });
530
- }
531
- function requestBackgroundSessionStop(state, input) {
532
- if (isBackgroundSessionTerminalPhase(state.phase)) throw new BackgroundSessionTransitionDenied(state.sessionId, `terminal ${state.phase} session cannot stop`);
533
- if (state.phase === "stopping") throw new BackgroundSessionTransitionDenied(state.sessionId, "background session is already stopping");
534
- if (state.phase === "running") return next(state, {
535
- cancellation: {
536
- reason: input.reason,
537
- requestedAt: input.now
538
- },
539
- phase: "stopping",
540
- updatedAt: input.now
541
- });
542
- return next(state, {
543
- cancellation: {
544
- reason: input.reason,
545
- requestedAt: input.now
546
- },
547
- currentStepRunId: void 0,
548
- lease: void 0,
549
- phase: "stopped",
550
- updatedAt: input.now,
551
- wakeAt: void 0
552
- });
553
- }
554
- function completeBackgroundSessionStop(state, input) {
555
- if (state.phase !== "stopping") throw new BackgroundSessionTransitionDenied(state.sessionId, `background session cannot finish stopping while ${state.phase}`);
556
- return next(state, {
557
- currentStepRunId: void 0,
558
- lease: void 0,
559
- phase: "stopped",
560
- updatedAt: input.now,
561
- wakeAt: void 0
562
- });
563
- }
564
- function parkBackgroundSessionForReconciliation(state, input) {
565
- if (isBackgroundSessionTerminalPhase(state.phase) && state.phase !== "reconciliation-required") throw new BackgroundSessionTransitionDenied(state.sessionId, `terminal ${state.phase} session cannot be reconciled`);
566
- return next(state, {
567
- currentStepRunId: void 0,
568
- errorMessage: input.reason,
569
- lease: void 0,
570
- phase: "reconciliation-required",
571
- reconciliationNote: input.reason,
572
- updatedAt: input.now,
573
- wakeAt: void 0
574
- });
575
- }
576
- function resolveBackgroundSessionReconciliation(state, input) {
577
- if (state.phase !== "reconciliation-required") throw new BackgroundSessionTransitionDenied(state.sessionId, `background session cannot resolve while ${state.phase}`);
578
- if (input.outcome === "continue") return next(state, {
579
- errorMessage: void 0,
580
- phase: "queued",
581
- reconciliationNote: void 0,
582
- updatedAt: input.now,
583
- wakeAt: input.now
584
- });
585
- return next(state, {
586
- errorMessage: void 0,
587
- phase: "stopped",
588
- reconciliationNote: void 0,
589
- updatedAt: input.now,
590
- wakeAt: void 0
591
- });
592
- }
593
- function appendBackgroundSessionInput(state, input) {
594
- if (isBackgroundSessionTerminalPhase(state.phase)) throw new BackgroundSessionTransitionDenied(state.sessionId, `terminal ${state.phase} session cannot accept input`);
595
- return next(state, {
596
- pendingInput: Object.freeze([...state.pendingInput, input.message]),
597
- updatedAt: input.now,
598
- ...state.phase === "waiting" || state.phase === "input-required" ? {
599
- phase: "queued",
600
- wakeAt: input.now
601
- } : {}
602
- });
603
- }
604
- function requeueInterruptedBackgroundSessionStep(state, input) {
605
- if (state.phase !== "running") throw new BackgroundSessionTransitionDenied(state.sessionId, "interrupted step recovery requires a running session");
606
- return next(state, {
607
- currentStepRunId: void 0,
608
- lastWakeText: input.wakeText,
609
- lease: void 0,
610
- phase: "queued",
611
- updatedAt: input.now,
612
- wakeAt: input.now
613
- });
614
- }
615
- function isBackgroundSessionLeaseExpired(state, now) {
616
- return state.lease !== void 0 && state.lease.expiresAt <= now;
617
- }
618
- function isBackgroundSessionDue(state, now) {
619
- if (state.phase !== "queued" && state.phase !== "waiting") return false;
620
- return state.wakeAt === void 0 || state.wakeAt <= now;
621
- }
622
- function next(state, changes) {
623
- const merged = {
624
- ...state,
625
- ...changes,
626
- revision: state.revision + 1
627
- };
628
- for (const key of Object.keys(merged)) if (merged[key] === void 0) delete merged[key];
629
- return Object.freeze(merged);
630
- }
631
- //#endregion
632
- //#region src/platform/concurrency/serial-executor.ts
633
- function createSerialExecutor() {
634
- let tail = Promise.resolve();
635
- return { run: (operation) => {
636
- const result = tail.then(operation, operation);
637
- tail = result.then(() => void 0, () => void 0);
638
- return result;
639
- } };
640
- }
641
- //#endregion
642
- //#region src/modules/background-session/application/session/background-session-service.ts
643
- var BackgroundSessionCallerDenied = class extends Error {
644
- name = "BackgroundSessionCallerDenied";
645
- constructor(message) {
646
- super(message);
647
- }
648
- };
649
- function createBackgroundSessionService(options) {
650
- const serial = createSerialExecutor();
651
- const start = async (input) => {
652
- const now = options.clock.now();
653
- const origin = requireOrigin(input.context);
654
- const state = createBackgroundSession({
655
- authority: input.authority,
656
- displayName: input.displayName?.trim() || input.sessionId,
657
- now,
658
- origin: {
659
- allowedActorOpenIds: origin.allowedActorOpenIds,
660
- ...origin.conversationId === void 0 ? {} : { conversationId: origin.conversationId },
661
- endpointId: origin.endpointId,
662
- ...input.context.memory ? { memory: {
663
- audience: input.context.memory.audience,
664
- ...input.context.memory.conversationId ? { conversationId: input.context.memory.conversationId } : {},
665
- ...input.context.memory.projectId ? { projectId: input.context.memory.projectId } : {},
666
- subjectId: input.context.memory.subjectId,
667
- tenantId: input.context.memory.tenantId
668
- } } : {},
669
- tenantKey: origin.tenantKey
670
- },
671
- parentRunId: input.context.runId,
672
- prompt: input.prompt,
673
- sessionId: input.sessionId,
674
- sourceMessageId: requireSourceMessageId(input.context)
675
- });
676
- return toSummary(await options.repository.create(state));
677
- };
678
- const wait = async (input) => {
679
- const sessionId = sessionIdFromSessionKey(input.context.sessionKey);
680
- if (!sessionId) throw new BackgroundSessionCallerDenied("background.wait is only available inside a background session step");
681
- if (input.delayMs !== void 0 && input.until !== void 0) throw new BackgroundSessionCallerDenied("background.wait cannot combine delayMs and until");
682
- const now = options.clock.now();
683
- let wakeAt;
684
- if (input.delayMs !== void 0) {
685
- if (!Number.isSafeInteger(input.delayMs) || input.delayMs < 1) throw new BackgroundSessionCallerDenied("background.wait delayMs must be a positive integer");
686
- wakeAt = new Date(Date.parse(now) + input.delayMs).toISOString();
687
- } else if (input.until !== void 0) {
688
- const parsed = Date.parse(input.until);
689
- if (!Number.isFinite(parsed)) throw new BackgroundSessionCallerDenied("background.wait until must be an ISO 8601 timestamp");
690
- wakeAt = new Date(parsed).toISOString();
691
- }
692
- return toSummary(requireState(await serial.run(async () => {
693
- const state = await options.repository.get(sessionId);
694
- if (!state) throw new BackgroundSessionCallerDenied(`background session not found: ${sessionId}`);
695
- requireMatchingOrigin(state, input.context);
696
- const nextState = suspendBackgroundSession(state, {
697
- now,
698
- ...input.reason === void 0 ? {} : { reason: input.reason },
699
- ...wakeAt ? { wakeAt } : {}
700
- });
701
- return options.repository.update({
702
- build: () => nextState,
703
- expectedRevision: state.revision,
704
- sessionId
705
- });
706
- }), sessionId));
707
- };
708
- const list = async (input) => {
709
- return (await options.repository.list(input.phase === void 0 ? void 0 : { phase: input.phase })).filter((state) => matchesOrigin(state, input.context)).sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, input.limit).map(toSummary);
710
- };
711
- const status = async (input) => {
712
- return toDetail(await requireOwnedState(input.sessionId, input.context));
713
- };
714
- const send = async (input) => {
715
- return toSummary(requireState(await serial.run(async () => {
716
- const state = await requireOwnedState(input.sessionId, input.context);
717
- const nextState = appendBackgroundSessionInput(state, {
718
- message: input.message,
719
- now: options.clock.now()
720
- });
721
- return options.repository.update({
722
- build: () => nextState,
723
- expectedRevision: state.revision,
724
- sessionId: input.sessionId
725
- });
726
- }), input.sessionId));
727
- };
728
- const stop = async (input) => {
729
- return toSummary(requireState(await serial.run(async () => {
730
- const state = await requireOwnedState(input.sessionId, input.context);
731
- const nextState = requestBackgroundSessionStop(state, {
732
- now: options.clock.now(),
733
- reason: input.reason ?? "stopped by conversation"
734
- });
735
- return updateAndEnqueueStopped(input.sessionId, state.revision, nextState);
736
- }), input.sessionId));
737
- };
738
- const requireOwnedState = async (sessionId, context) => {
739
- const state = await options.repository.get(sessionId);
740
- if (!state) throw new BackgroundSessionCallerDenied(`background session not found: ${sessionId}`);
741
- requireMatchingOrigin(state, context);
742
- return state;
743
- };
744
- const updateAndEnqueueStopped = async (sessionId, expectedRevision, nextState) => {
745
- const stored = await options.repository.update({
746
- build: () => nextState,
747
- expectedRevision,
748
- sessionId
749
- });
750
- if (stored?.phase === "stopped") await options.deliveries.enqueue({
751
- createdAt: options.clock.now(),
752
- deliveryId: terminalDeliveryId(sessionId, "stopped"),
753
- kind: "stopped",
754
- sessionId,
755
- text: stopNotice$1(stored)
756
- });
757
- return stored;
758
- };
759
- const resolveReconciliation = async (input) => {
760
- return toSummary(requireState(await serial.run(async () => {
761
- const state = await options.repository.get(input.sessionId);
762
- if (!state) throw new BackgroundSessionCallerDenied(`background session not found: ${input.sessionId}`);
763
- const nextState = resolveBackgroundSessionReconciliation(state, {
764
- now: options.clock.now(),
765
- outcome: input.outcome
766
- });
767
- return updateAndEnqueueStopped(input.sessionId, state.revision, nextState);
768
- }), input.sessionId));
769
- };
770
- return {
771
- list,
772
- resolveReconciliation,
773
- send,
774
- start,
775
- status,
776
- stop,
777
- wait
778
- };
779
- }
780
- function sessionIdFromSessionKey(sessionKey) {
781
- const prefix = `${createBackgroundSessionKey("")}`;
782
- return sessionKey.startsWith(prefix) ? sessionKey.slice(prefix.length) : void 0;
783
- }
784
- function terminalDeliveryId(sessionId, kind) {
785
- return `bg-final:${sessionId}:${kind}`;
786
- }
787
- function progressDeliveryId(sessionId, stepCount) {
788
- return `bg-progress:${sessionId}:${stepCount}`;
789
- }
790
- function requireOrigin(context) {
791
- if (!context.origin || !context.origin.endpointId || !context.origin.tenantKey) throw new BackgroundSessionCallerDenied("background tools require a trusted invocation origin; this run has none");
792
- return context.origin;
793
- }
794
- function requireSourceMessageId(context) {
795
- if (!context.sourceMessageId?.trim()) throw new BackgroundSessionCallerDenied("background.start requires a trusted source message id");
796
- return context.sourceMessageId;
797
- }
798
- function requireMatchingOrigin(state, context) {
799
- if (!matchesOrigin(state, context)) throw new BackgroundSessionCallerDenied("background session is not owned by this conversation");
800
- }
801
- function matchesOrigin(state, context) {
802
- const origin = context.origin;
803
- if (!origin) return false;
804
- if (context.agentId !== state.authority.agentId) return false;
805
- if (state.origin.endpointId !== origin.endpointId) return false;
806
- if (state.origin.tenantKey !== origin.tenantKey) return false;
807
- if ((state.origin.conversationId ?? void 0) !== (origin.conversationId ?? void 0)) return false;
808
- return true;
809
- }
810
- function requireState(state, sessionId) {
811
- if (!state) throw new BackgroundSessionCallerDenied(`background session not found: ${sessionId}`);
812
- return state;
813
- }
814
- function toSummary(state) {
815
- return {
816
- createdAt: state.createdAt,
817
- displayName: state.displayName,
818
- phase: state.phase,
819
- sessionId: state.sessionId,
820
- stepCount: state.stepCount,
821
- updatedAt: state.updatedAt,
822
- ...state.wakeAt ? { wakeAt: state.wakeAt } : {}
823
- };
824
- }
825
- function toDetail(state) {
826
- return {
827
- ...toSummary(state),
828
- ...state.cancellation ? { cancellationReason: state.cancellation.reason } : {},
829
- ...state.errorMessage ? { errorMessage: state.errorMessage } : {},
830
- parentRunId: state.parentRunId,
831
- pendingInput: state.pendingInput,
832
- ...state.reconciliationNote ? { reconciliationNote: state.reconciliationNote } : {},
833
- ...state.result ? { result: state.result } : {}
834
- };
835
- }
836
- function stopNotice$1(state) {
837
- return `后台会话「${state.displayName}」已停止。${state.cancellation ? `原因:${state.cancellation.reason}` : ""}`.trim();
838
- }
839
- //#endregion
840
- //#region src/modules/background-session/application/supervision/background-session-config.ts
841
- const DEFAULT_BACKGROUND_SESSION_STEP_TIMEOUT_MS = 300 * 1e3;
842
- const DEFAULT_BACKGROUND_SESSION_MAX_CONCURRENT_SESSIONS = 4;
843
- const DEFAULT_BACKGROUND_SESSION_LEASE_MS = 3e4;
844
- const DEFAULT_BACKGROUND_SESSION_LEASE_RENEWAL_INTERVAL_MS = 1e4;
845
- const DEFAULT_BACKGROUND_SESSION_MAX_CONSECUTIVE_FAILURES = 3;
846
- const DEFAULT_BACKGROUND_SESSION_RETRY_BACKOFF_MS = 3e4;
847
- const DEFAULT_BACKGROUND_SESSION_LIFETIME_MS = 1440 * 60 * 1e3;
848
- const DEFAULT_BACKGROUND_SESSION_SUPERVISOR_INTERVAL_MS = 1e3;
849
- function resolveBackgroundSessionSupervisorIntervalMs(leaseMs) {
850
- return Math.min(5e3, Math.max(200, Math.floor(leaseMs / 10)));
851
- }
852
- //#endregion
853
- //#region src/modules/background-session/application/supervision/background-session-delivery-queue.ts
854
- function createBackgroundSessionDeliveryQueue(options) {
855
- return {
856
- drain: () => Effect.gen(function* () {
857
- const records = yield* fromPromise$1(() => options.deliveries.pending());
858
- yield* Effect.forEach(records, (record) => fromPromise$1(() => options.deliver(record)).pipe(Effect.flatMap(({ providerMessageId }) => fromPromise$1(() => options.deliveries.markDelivered(record.deliveryId, providerMessageId))), Effect.tap((delivered) => delivered ? Effect.sync(options.onDelivered) : Effect.void), Effect.catchAll((error) => options.onError === void 0 ? Effect.void : Effect.sync(() => options.onError?.(error))), Effect.asVoid), { discard: true });
859
- }),
860
- enqueue: (sessionId, kind, text, session) => {
861
- if (text.trim() === "") return Effect.void;
862
- const deliveryId = kind === "progress" ? progressDeliveryId(sessionId, session.stepCount) : terminalDeliveryId(sessionId, kind);
863
- return fromPromise$1(() => options.deliveries.enqueue({
864
- createdAt: options.clock.now(),
865
- deliveryId,
866
- kind,
867
- sessionId,
868
- text
869
- })).pipe(Effect.asVoid);
870
- }
871
- };
872
- }
873
- function fromPromise$1(evaluate) {
874
- return Effect.tryPromise({
875
- catch: (error) => error,
876
- try: evaluate
877
- });
878
- }
879
- //#endregion
880
- //#region src/modules/background-session/application/supervision/background-session-error-policy.ts
881
- function isFencedBackgroundSessionOperationError(error) {
882
- const message = backgroundSessionErrorMessage(error).toLowerCase();
883
- return message.includes("fenced") || message.includes("reconciliation-required");
884
- }
885
- function isRetryableBackgroundSessionStepError(error) {
886
- if (!(error instanceof Error)) return false;
887
- if (error.name === "AgentRunCancelled") return true;
888
- return error.name === "AgentHarnessBusy" || error.name === "AgentInstanceBusy";
889
- }
890
- function backgroundSessionErrorMessage(error) {
891
- return error instanceof Error ? error.message : String(error);
892
- }
893
- //#endregion
894
- //#region src/modules/background-session/application/supervision/background-session-supervisor-snapshot.ts
895
- async function readBackgroundSessionSupervisorSnapshot(repository, now) {
896
- const counts = { ...emptyBackgroundSessionPhaseCounts() };
897
- let oldestDueAt;
898
- for (const session of await repository.list()) {
899
- counts[session.phase] += 1;
900
- if (!isBackgroundSessionDue(session, now)) continue;
901
- const dueAt = session.wakeAt ?? session.createdAt;
902
- if (oldestDueAt === void 0 || dueAt < oldestDueAt) oldestDueAt = dueAt;
903
- }
904
- return Object.freeze({
905
- ...oldestDueAt === void 0 ? {} : { oldestDueAt },
906
- phaseCounts: Object.freeze(counts)
907
- });
908
- }
909
- function emptyBackgroundSessionPhaseCounts() {
910
- return Object.freeze({
911
- completed: 0,
912
- failed: 0,
913
- "input-required": 0,
914
- queued: 0,
915
- "reconciliation-required": 0,
916
- running: 0,
917
- stopped: 0,
918
- stopping: 0,
919
- waiting: 0
920
- });
921
- }
922
- //#endregion
923
- //#region src/modules/background-session/application/supervision/background-session-wake-prompt.ts
924
- function buildBackgroundSessionWakeText(session) {
925
- if (session.wakeCount === 0) return `你在一个后台会话中开始工作。任务:\n${session.prompt}\n\n继续工作直到给出最终结果;如果必须等待外部变化或用户输入,调用 background.wait。`;
926
- const parts = [`后台会话第 ${session.stepCount + 1} 步。任务:\n${session.prompt}`, "继续先前的工作(历史在会话记录中)。"];
927
- if (session.pendingInput.length > 0) parts.push(`用户新的输入:\n${session.pendingInput.join("\n---\n")}`);
928
- parts.push("完成后给出最终结果;需要等待时调用 background.wait。");
929
- return parts.join("\n\n");
930
- }
931
- function buildInterruptedBackgroundSessionWakeText(session) {
932
- return `你的上一步在后台会话中被守护进程重启打断,未产生确定的工具副作用(副作用由工具账本保护)。继续先前的工作(历史在会话记录中)。` + (session.pendingInput.length > 0 ? `\n\n用户新的输入:\n${session.pendingInput.join("\n---\n")}` : "");
933
- }
934
- //#endregion
935
- //#region src/modules/background-session/application/supervision/background-session-supervisor.ts
936
- function createBackgroundSessionSupervisor(options) {
937
- const owner = `supervisor:${createRandomId()}`;
938
- const inFlight = /* @__PURE__ */ new Map();
939
- const stepControllers = /* @__PURE__ */ new Map();
940
- const supervision = Effect.unsafeMakeSemaphore(1);
941
- const counters = {
942
- completed: 0,
943
- deliveriesDelivered: 0,
944
- failed: 0,
945
- started: 0,
946
- staleLeaseEventsDropped: 0,
947
- stopped: 0
948
- };
949
- let repositoryHealthy = true;
950
- let snapshot = emptyBackgroundSessionPhaseCounts();
951
- let oldestDueAt;
952
- const requeuedInPass = /* @__PURE__ */ new Set();
953
- const deliveryQueue = createBackgroundSessionDeliveryQueue({
954
- clock: options.clock,
955
- deliver: options.deliver,
956
- deliveries: options.deliveries,
957
- onDelivered: () => {
958
- counters.deliveriesDelivered += 1;
959
- },
960
- ...options.onError === void 0 ? {} : { onError: options.onError }
961
- });
962
- const reportError = (error) => options.onError === void 0 ? Effect.void : Effect.sync(() => options.onError?.(error)).pipe(Effect.catchAllCause(() => Effect.void));
963
- const repositoryGet = (sessionId) => fromPromise(() => options.repository.get(sessionId));
964
- const repositoryList = () => fromPromise(() => options.repository.list());
965
- const repositoryUpdate = (input) => Effect.uninterruptible(fromPromise(() => options.repository.update(input)));
966
- const refreshSnapshot = () => fromPromise(() => readBackgroundSessionSupervisorSnapshot(options.repository, options.clock.now())).pipe(Effect.tap((latest) => Effect.sync(() => {
967
- snapshot = latest.phaseCounts;
968
- oldestDueAt = latest.oldestDueAt;
969
- })), Effect.asVoid);
970
- const completeStop = (state, expectedLease) => repositoryUpdate({
971
- build: (current) => completeBackgroundSessionStop(current, { now: options.clock.now() }),
972
- expectedLease,
973
- expectedRevision: state.revision,
974
- sessionId: state.sessionId
975
- }).pipe(Effect.flatMap((stopped) => {
976
- if (!stopped) return Effect.void;
977
- counters.stopped += 1;
978
- return deliveryQueue.enqueue(stopped.sessionId, "stopped", stopNotice(stopped), stopped);
979
- }));
980
- const resolveStaleLeases = () => Effect.gen(function* () {
981
- const now = options.clock.now();
982
- for (const session of yield* repositoryList()) {
983
- if (session.lease === void 0 || inFlight.has(session.sessionId)) continue;
984
- if (!(session.lease.owner !== owner || isBackgroundSessionLeaseExpired(session, now))) continue;
985
- counters.staleLeaseEventsDropped += 1;
986
- const expectedLease = {
987
- kind: "held",
988
- owner: session.lease.owner,
989
- epoch: session.lease.epoch
990
- };
991
- if (session.phase === "running") {
992
- yield* repositoryUpdate({
993
- build: (state) => requeueInterruptedBackgroundSessionStep(state, {
994
- now,
995
- wakeText: buildInterruptedBackgroundSessionWakeText(state)
996
- }),
997
- expectedLease,
998
- expectedRevision: session.revision,
999
- sessionId: session.sessionId
1000
- });
1001
- requeuedInPass.add(session.sessionId);
1002
- continue;
1003
- }
1004
- if (session.phase === "stopping") {
1005
- yield* completeStop(session, expectedLease);
1006
- continue;
1007
- }
1008
- yield* repositoryUpdate({
1009
- build: (state) => releaseBackgroundSessionLease(state),
1010
- expectedLease,
1011
- expectedRevision: session.revision,
1012
- sessionId: session.sessionId
1013
- });
1014
- }
1015
- });
1016
- const expireLifetime = () => Effect.gen(function* () {
1017
- const now = options.clock.now();
1018
- const cutoff = Date.parse(now) - options.config.sessionLifetimeMs;
1019
- for (const session of yield* repositoryList()) {
1020
- if (isBackgroundSessionTerminalPhase(session.phase)) continue;
1021
- if (Date.parse(session.createdAt) > cutoff) continue;
1022
- const nextState = requestBackgroundSessionStop(session, {
1023
- now,
1024
- reason: "background session lifetime exceeded"
1025
- });
1026
- const stored = yield* repositoryUpdate({
1027
- build: () => nextState,
1028
- expectedRevision: session.revision,
1029
- sessionId: session.sessionId
1030
- });
1031
- if (stored?.phase === "stopped") {
1032
- counters.stopped += 1;
1033
- yield* deliveryQueue.enqueue(stored.sessionId, "stopped", stopNotice(stored), stored);
1034
- } else stepControllers.get(session.sessionId)?.abort(/* @__PURE__ */ new Error("background session lifetime exceeded"));
1035
- }
1036
- });
1037
- const stopStoppingSessions = () => Effect.gen(function* () {
1038
- for (const session of yield* repositoryList()) {
1039
- if (session.phase !== "stopping") continue;
1040
- if (inFlight.has(session.sessionId)) {
1041
- stepControllers.get(session.sessionId)?.abort(/* @__PURE__ */ new Error("background session stop requested"));
1042
- continue;
1043
- }
1044
- const expectedLease = session.lease ? {
1045
- kind: "held",
1046
- owner: session.lease.owner,
1047
- epoch: session.lease.epoch
1048
- } : { kind: "absent" };
1049
- yield* completeStop(session, expectedLease);
1050
- }
1051
- });
1052
- const requeueAfterInterruptedFiber = (sessionId, lease) => Effect.gen(function* () {
1053
- const current = yield* repositoryGet(sessionId);
1054
- if (!current) return;
1055
- const expectedLease = {
1056
- kind: "held",
1057
- owner: lease.owner,
1058
- epoch: lease.epoch
1059
- };
1060
- if (current.phase === "stopping") {
1061
- yield* completeStop(current, expectedLease);
1062
- return;
1063
- }
1064
- if (current.phase !== "running") return;
1065
- yield* repositoryUpdate({
1066
- build: (state) => requeueInterruptedBackgroundSessionStep(state, {
1067
- now: options.clock.now(),
1068
- wakeText: buildInterruptedBackgroundSessionWakeText(state)
1069
- }),
1070
- expectedLease,
1071
- expectedRevision: current.revision,
1072
- sessionId
1073
- });
1074
- }).pipe(Effect.catchAll((error) => reportError(error)), Effect.asVoid);
1075
- const renewLeaseOnce = (sessionId, lease, controller) => Effect.gen(function* () {
1076
- const current = yield* repositoryGet(sessionId);
1077
- if (!current || current.lease === void 0 || current.lease.owner !== owner || current.lease.epoch !== lease.epoch) {
1078
- controller.abort(/* @__PURE__ */ new Error("background session lease was lost"));
1079
- return false;
1080
- }
1081
- if (!(yield* repositoryUpdate({
1082
- build: (state) => renewBackgroundSessionLease(state, {
1083
- expiresAt: toIso(Date.parse(options.clock.now()) + options.config.leaseMs),
1084
- epoch: lease.epoch,
1085
- owner
1086
- }),
1087
- expectedLease: {
1088
- kind: "held",
1089
- owner,
1090
- epoch: lease.epoch
1091
- },
1092
- expectedRevision: current.revision,
1093
- sessionId
1094
- }))) {
1095
- controller.abort(/* @__PURE__ */ new Error("background session lease renewal was denied"));
1096
- return false;
1097
- }
1098
- return true;
1099
- });
1100
- const renewLease = (sessionId, lease, controller) => {
1101
- const cycle = Effect.suspend(() => options.sleep(options.config.leaseRenewalIntervalMs).pipe(Effect.flatMap(() => renewLeaseOnce(sessionId, lease, controller)), Effect.flatMap((renewed) => renewed ? cycle : Effect.void), Effect.catchAllCause((cause) => {
1102
- if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
1103
- controller.abort(/* @__PURE__ */ new Error(`background session lease renewal failed: ${backgroundSessionErrorMessage(Cause.squash(cause))}`));
1104
- return Effect.void;
1105
- })));
1106
- return cycle;
1107
- };
1108
- const requeueAfterFailedCommit = (sessionId, lease, error) => Effect.gen(function* () {
1109
- const current = yield* repositoryGet(sessionId);
1110
- if (!current || current.phase !== "running") return;
1111
- yield* repositoryUpdate({
1112
- build: (state) => requeueInterruptedBackgroundSessionStep(state, {
1113
- now: options.clock.now(),
1114
- wakeText: `步骤结果提交失败(${backgroundSessionErrorMessage(error)}),将重试`
1115
- }),
1116
- expectedLease: {
1117
- kind: "held",
1118
- owner: lease.owner,
1119
- epoch: lease.epoch
1120
- },
1121
- expectedRevision: current.revision,
1122
- sessionId
1123
- });
1124
- }).pipe(Effect.catchAll((commitError) => reportError(commitError)), Effect.asVoid);
1125
- const commitStep = (claimed, outcome, lease) => Effect.gen(function* () {
1126
- const now = options.clock.now();
1127
- const current = yield* repositoryGet(claimed.sessionId);
1128
- if (!current) return;
1129
- const expectedLease = {
1130
- kind: "held",
1131
- owner: lease.owner,
1132
- epoch: lease.epoch
1133
- };
1134
- if (current.phase === "waiting" || current.phase === "input-required") {
1135
- const text = outcome.ok ? outcome.result.finalText : "";
1136
- yield* repositoryUpdate({
1137
- build: (state) => releaseBackgroundSessionLease(state),
1138
- expectedLease,
1139
- expectedRevision: current.revision,
1140
- sessionId: current.sessionId
1141
- });
1142
- if (text.trim() !== "") yield* deliveryQueue.enqueue(current.sessionId, "progress", text, current);
1143
- return;
1144
- }
1145
- if (current.phase === "stopping") {
1146
- yield* completeStop(current, expectedLease);
1147
- return;
1148
- }
1149
- if (current.phase !== "running") return;
1150
- if (outcome.ok) {
1151
- const completed = yield* repositoryUpdate({
1152
- build: (state) => completeBackgroundSessionStep(state, {
1153
- now,
1154
- stepRunId: outcome.result.runId,
1155
- text: outcome.result.finalText
1156
- }),
1157
- expectedLease,
1158
- expectedRevision: current.revision,
1159
- sessionId: current.sessionId
1160
- });
1161
- if (completed) {
1162
- counters.completed += 1;
1163
- yield* deliveryQueue.enqueue(completed.sessionId, "final", completed.result?.text ?? "", completed);
1164
- }
1165
- return;
1166
- }
1167
- const errorMessage = backgroundSessionErrorMessage(outcome.error);
1168
- if (isFencedBackgroundSessionOperationError(outcome.error)) {
1169
- const parked = yield* repositoryUpdate({
1170
- build: (state) => parkBackgroundSessionForReconciliation(state, {
1171
- now,
1172
- reason: `step failed on a fenced operation: ${errorMessage}`
1173
- }),
1174
- expectedLease,
1175
- expectedRevision: current.revision,
1176
- sessionId: current.sessionId
1177
- });
1178
- if (parked) yield* deliveryQueue.enqueue(parked.sessionId, "reconciliation", `后台会话「${parked.displayName}」需要人工对账:${parked.reconciliationNote ?? errorMessage}`, parked);
1179
- return;
1180
- }
1181
- if (isRetryableBackgroundSessionStepError(outcome.error) && current.consecutiveFailures + 1 < options.config.maxConsecutiveFailures) {
1182
- const backoff = options.config.retryBackoffMs * 2 ** current.consecutiveFailures;
1183
- yield* repositoryUpdate({
1184
- build: (state) => failBackgroundSessionStep(state, {
1185
- errorMessage,
1186
- now,
1187
- retryable: true,
1188
- wakeAt: toIso(Date.parse(now) + backoff)
1189
- }),
1190
- expectedLease,
1191
- expectedRevision: current.revision,
1192
- sessionId: current.sessionId
1193
- });
1194
- return;
1195
- }
1196
- const failed = yield* repositoryUpdate({
1197
- build: (state) => failBackgroundSessionStep(state, {
1198
- errorMessage,
1199
- now,
1200
- retryable: false
1201
- }),
1202
- expectedLease,
1203
- expectedRevision: current.revision,
1204
- sessionId: current.sessionId
1205
- });
1206
- if (failed) {
1207
- counters.failed += 1;
1208
- yield* deliveryQueue.enqueue(failed.sessionId, "failed", `后台会话「${failed.displayName}」失败:${failed.errorMessage ?? errorMessage}`, failed);
1209
- }
1210
- });
1211
- const runStep = (session) => Effect.gen(function* () {
1212
- const now = options.clock.now();
1213
- const stepCount = session.stepCount + 1;
1214
- const wakeText = requeuedInPass.has(session.sessionId) ? buildInterruptedBackgroundSessionWakeText(session) : buildBackgroundSessionWakeText(session);
1215
- requeuedInPass.delete(session.sessionId);
1216
- const lease = {
1217
- epoch: (session.lease?.epoch ?? 0) + 1,
1218
- expiresAt: toIso(Date.parse(now) + options.config.leaseMs),
1219
- owner
1220
- };
1221
- const claimed = yield* repositoryUpdate({
1222
- build: (state) => claimBackgroundSession(state, {
1223
- lease,
1224
- now,
1225
- stepRunId: `bg-step:${session.sessionId}:${stepCount}`,
1226
- wakeText
1227
- }),
1228
- expectedLease: { kind: "absent-or-stale" },
1229
- expectedRevision: session.revision,
1230
- now,
1231
- sessionId: session.sessionId
1232
- });
1233
- if (!claimed) return;
1234
- counters.started += 1;
1235
- const controller = new AbortController();
1236
- stepControllers.set(session.sessionId, controller);
1237
- yield* Effect.gen(function* () {
1238
- const outcome = yield* Effect.acquireUseRelease(Effect.fork(renewLease(session.sessionId, lease, controller).pipe(Effect.interruptible)), () => fromPromise(() => options.runStep({
1239
- session: claimed,
1240
- signal: controller.signal,
1241
- wakeText
1242
- })).pipe(Effect.match({
1243
- onFailure: (error) => ({
1244
- error,
1245
- ok: false
1246
- }),
1247
- onSuccess: (result) => ({
1248
- ok: true,
1249
- result
1250
- })
1251
- })), (fiber) => Fiber.interrupt(fiber).pipe(Effect.asVoid));
1252
- yield* Effect.uninterruptible(commitStep(claimed, outcome, lease).pipe(Effect.catchAll((error) => reportError(error).pipe(Effect.flatMap(() => requeueAfterFailedCommit(session.sessionId, lease, error))))));
1253
- }).pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort(/* @__PURE__ */ new Error("background session supervisor is stopping"))).pipe(Effect.flatMap(() => requeueAfterInterruptedFiber(session.sessionId, lease)))), Effect.ensuring(Effect.sync(() => {
1254
- stepControllers.delete(session.sessionId);
1255
- })));
1256
- });
1257
- const startStep = (session) => Effect.gen(function* () {
1258
- const gate = yield* Deferred.make();
1259
- const fiber = yield* Effect.forkDaemon(Deferred.await(gate).pipe(Effect.flatMap(() => runStep(session)), Effect.catchAllCause((cause) => Cause.isInterruptedOnly(cause) ? Effect.void : reportError(Cause.squash(cause))), Effect.ensuring(Effect.sync(() => {
1260
- inFlight.delete(session.sessionId);
1261
- }))));
1262
- inFlight.set(session.sessionId, fiber);
1263
- yield* Deferred.succeed(gate, void 0);
1264
- }).pipe(Effect.uninterruptible);
1265
- const startDueSessions = () => Effect.gen(function* () {
1266
- const now = options.clock.now();
1267
- for (const session of yield* repositoryList()) {
1268
- if (inFlight.size >= options.config.maxConcurrentSessions) break;
1269
- if (inFlight.has(session.sessionId)) continue;
1270
- if (!isBackgroundSessionDue(session, now)) continue;
1271
- if (session.lease !== void 0 && !isBackgroundSessionLeaseExpired(session, now)) continue;
1272
- yield* startStep(session);
1273
- }
1274
- });
1275
- const supervisionPass = () => Effect.gen(function* () {
1276
- yield* resolveStaleLeases();
1277
- yield* expireLifetime();
1278
- yield* stopStoppingSessions();
1279
- yield* deliveryQueue.drain();
1280
- yield* startDueSessions();
1281
- yield* refreshSnapshot();
1282
- repositoryHealthy = true;
1283
- }).pipe(Effect.tapError(() => Effect.sync(() => {
1284
- repositoryHealthy = false;
1285
- })), Effect.orDie);
1286
- const tick = () => supervision.withPermits(1)(supervisionPass());
1287
- const loop = createPeriodicEffectLoop({
1288
- intervalMs: options.config.intervalMs,
1289
- onError: (error) => {
1290
- options.onError?.(error);
1291
- },
1292
- run: tick,
1293
- sleep: options.sleep
1294
- });
1295
- const statusOf = () => Object.freeze({
1296
- capacity: options.config.maxConcurrentSessions,
1297
- counters: Object.freeze({ ...counters }),
1298
- inFlightSteps: inFlight.size,
1299
- ...oldestDueAt === void 0 ? {} : { oldestDueAt },
1300
- owner,
1301
- phaseCounts: snapshot,
1302
- reconciliationCount: snapshot["reconciliation-required"],
1303
- repositoryHealthy,
1304
- running: loop.running()
1305
- });
1306
- return {
1307
- recover: tick,
1308
- start: () => Effect.sync(() => loop.start()),
1309
- status: statusOf,
1310
- stop: () => Effect.gen(function* () {
1311
- yield* fromPromise(() => loop.stop());
1312
- for (const controller of stepControllers.values()) controller.abort(/* @__PURE__ */ new Error("background session supervisor is stopping"));
1313
- yield* Effect.forEach(inFlight.values(), (fiber) => Fiber.interrupt(fiber), { discard: true });
1314
- }).pipe(Effect.orDie),
1315
- tick
1316
- };
1317
- }
1318
- function fromPromise(evaluate) {
1319
- return Effect.tryPromise({
1320
- catch: (error) => error,
1321
- try: evaluate
1322
- });
1323
- }
1324
- function toIso(timeMs) {
1325
- return new Date(timeMs).toISOString();
1326
- }
1327
- function stopNotice(session) {
1328
- return `后台会话「${session.displayName}」已停止。${session.cancellation ? `原因:${session.cancellation.reason}` : ""}`.trim();
1329
- }
1330
- //#endregion
1331
- export { createBackgroundSessionStepSourceMessageId as $, resolveBackgroundSessionReconciliation as A, readBackgroundSessionString as B, isBackgroundSessionDue as C, renewBackgroundSessionLease as D, releaseBackgroundSessionLease as E, createBackgroundSessionControl as F, BACKGROUND_SESSION_TOOL_VERSION as G, BACKGROUND_SESSION_START_TOOL_ID as H, toControlContext as I, extendBackgroundSessionDefinition as J, backgroundSessionToolIds as K, readBackgroundSessionInteger as L, BackgroundSessionTransitionDenied as M, isBackgroundSessionTerminalPhase as N, requestBackgroundSessionStop as O, BackgroundSessionControlError as P, createBackgroundSessionKey as Q, readBackgroundSessionObject as R, failBackgroundSessionStep as S, parkBackgroundSessionForReconciliation as T, BACKGROUND_SESSION_TOOL_IDS as U, readBackgroundSessionWaitInput as V, BACKGROUND_SESSION_TOOL_PLUGIN_ID as W, narrowBackgroundSessionDefinition as X, isBackgroundSessionToolId as Y, BACKGROUND_SESSION_SESSION_KEY_PREFIX as Z, appendBackgroundSessionInput as _, DEFAULT_BACKGROUND_SESSION_MAX_CONCURRENT_SESSIONS as a, completeBackgroundSessionStop as b, DEFAULT_BACKGROUND_SESSION_STEP_TIMEOUT_MS as c, BackgroundSessionCallerDenied as d, createEffectLoopDriver as et, createBackgroundSessionService as f, createSerialExecutor as g, terminalDeliveryId as h, DEFAULT_BACKGROUND_SESSION_LIFETIME_MS as i, suspendBackgroundSession as j, requeueInterruptedBackgroundSessionStep as k, DEFAULT_BACKGROUND_SESSION_SUPERVISOR_INTERVAL_MS as l, sessionIdFromSessionKey as m, DEFAULT_BACKGROUND_SESSION_LEASE_MS as n, DEFAULT_BACKGROUND_SESSION_MAX_CONSECUTIVE_FAILURES as o, progressDeliveryId as p, createBackgroundSessionToolContracts as q, DEFAULT_BACKGROUND_SESSION_LEASE_RENEWAL_INTERVAL_MS as r, DEFAULT_BACKGROUND_SESSION_RETRY_BACKOFF_MS as s, createBackgroundSessionSupervisor as t, createPeriodicEffectLoop as tt, resolveBackgroundSessionSupervisorIntervalMs as u, claimBackgroundSession as v, isBackgroundSessionLeaseExpired as w, createBackgroundSession as x, completeBackgroundSessionStep as y, readBackgroundSessionPhase as z };