@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.
- package/README.md +1 -1
- package/dist/acp.js +1 -2
- package/dist/bootstrap/pi-feishu.d.ts +1 -1
- package/dist/bootstrap/pi-feishu.js +4 -4
- package/dist/chunks/agent-loop.d.ts +55 -300
- package/dist/chunks/agent-loop.js +3 -1123
- package/dist/chunks/background-session-authority.js +230 -0
- package/dist/chunks/background-session-control-input.js +51 -0
- package/dist/chunks/background-session-service.d.ts +382 -0
- package/dist/chunks/index.d.ts +1201 -538
- package/dist/chunks/pi-tool-proxy.d.ts +22 -90
- package/dist/chunks/pi.js +5 -2
- package/dist/chunks/rivus-agent-definition-resolver.js +508 -0
- package/dist/chunks/rivus-daemon-cli.js +2776 -3244
- package/dist/chunks/rivus-plugin-testkit.d.ts +175 -2
- package/dist/chunks/rivus-plugin-testkit.js +11 -4
- package/dist/chunks/rivus-skill.d.ts +95 -0
- package/dist/chunks/sha256-digest.js +2 -7
- package/dist/chunks/src.js +11941 -7001
- package/dist/chunks/tool-input-digest.js +158 -0
- package/dist/cli.js +764 -712
- package/dist/index.d.ts +6 -7
- package/dist/index.js +7 -8
- package/dist/mcp.d.ts +48 -9
- package/dist/mcp.js +146 -20
- package/dist/pi.d.ts +3 -4
- package/dist/pi.js +1 -1
- package/package.json +5 -5
- package/dist/chunks/api.d.ts +0 -70
- package/dist/chunks/api.js +0 -471
- package/dist/chunks/api2.d.ts +0 -387
- package/dist/chunks/api2.js +0 -1331
- package/dist/chunks/api3.d.ts +0 -402
- package/dist/chunks/module.js +0 -267
- package/dist/chunks/pi-skill-tool.js +0 -460
- package/dist/chunks/spi.d.ts +0 -1
- package/dist/chunks/spi.js +0 -2
package/dist/chunks/api2.js
DELETED
|
@@ -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 };
|