@zmzai/agent-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/fs-workspace.d.ts +7 -0
- package/dist/adapters/fs-workspace.d.ts.map +1 -0
- package/dist/adapters/fs-workspace.js +91 -0
- package/dist/adapters/fs-workspace.js.map +1 -0
- package/dist/adapters/index.d.ts +46 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +14 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai-provider.d.ts +17 -0
- package/dist/adapters/openai-provider.d.ts.map +1 -0
- package/dist/adapters/openai-provider.js +116 -0
- package/dist/adapters/openai-provider.js.map +1 -0
- package/dist/adapters/subprocess-sandbox.d.ts +7 -0
- package/dist/adapters/subprocess-sandbox.d.ts.map +1 -0
- package/dist/adapters/subprocess-sandbox.js +91 -0
- package/dist/adapters/subprocess-sandbox.js.map +1 -0
- package/dist/adapters/workspace-edit.d.ts +19 -0
- package/dist/adapters/workspace-edit.d.ts.map +1 -0
- package/dist/adapters/workspace-edit.js +36 -0
- package/dist/adapters/workspace-edit.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +191 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent/loader.d.ts +17 -0
- package/dist/core/agent/loader.d.ts.map +1 -0
- package/dist/core/agent/loader.js +148 -0
- package/dist/core/agent/loader.js.map +1 -0
- package/dist/core/agent/plugin.d.ts +59 -0
- package/dist/core/agent/plugin.d.ts.map +1 -0
- package/dist/core/agent/plugin.js +125 -0
- package/dist/core/agent/plugin.js.map +1 -0
- package/dist/core/agent/registry.d.ts +45 -0
- package/dist/core/agent/registry.d.ts.map +1 -0
- package/dist/core/agent/registry.js +98 -0
- package/dist/core/agent/registry.js.map +1 -0
- package/dist/core/agent/resolver.d.ts +16 -0
- package/dist/core/agent/resolver.d.ts.map +1 -0
- package/dist/core/agent/resolver.js +2 -0
- package/dist/core/agent/resolver.js.map +1 -0
- package/dist/core/events/bus.d.ts +30 -0
- package/dist/core/events/bus.d.ts.map +1 -0
- package/dist/core/events/bus.js +112 -0
- package/dist/core/events/bus.js.map +1 -0
- package/dist/core/events/manifest.d.ts +117 -0
- package/dist/core/events/manifest.d.ts.map +1 -0
- package/dist/core/events/manifest.js +69 -0
- package/dist/core/events/manifest.js.map +1 -0
- package/dist/core/permission/engine.d.ts +84 -0
- package/dist/core/permission/engine.d.ts.map +1 -0
- package/dist/core/permission/engine.js +131 -0
- package/dist/core/permission/engine.js.map +1 -0
- package/dist/core/permission/ruleset.d.ts +30 -0
- package/dist/core/permission/ruleset.d.ts.map +1 -0
- package/dist/core/permission/ruleset.js +85 -0
- package/dist/core/permission/ruleset.js.map +1 -0
- package/dist/core/runtime/compaction.d.ts +59 -0
- package/dist/core/runtime/compaction.d.ts.map +1 -0
- package/dist/core/runtime/compaction.js +108 -0
- package/dist/core/runtime/compaction.js.map +1 -0
- package/dist/core/runtime/lease-recovery.d.ts +45 -0
- package/dist/core/runtime/lease-recovery.d.ts.map +1 -0
- package/dist/core/runtime/lease-recovery.js +112 -0
- package/dist/core/runtime/lease-recovery.js.map +1 -0
- package/dist/core/runtime/loop-guard.d.ts +51 -0
- package/dist/core/runtime/loop-guard.d.ts.map +1 -0
- package/dist/core/runtime/loop-guard.js +90 -0
- package/dist/core/runtime/loop-guard.js.map +1 -0
- package/dist/core/runtime/pi-bridge.d.ts +53 -0
- package/dist/core/runtime/pi-bridge.d.ts.map +1 -0
- package/dist/core/runtime/pi-bridge.js +259 -0
- package/dist/core/runtime/pi-bridge.js.map +1 -0
- package/dist/core/runtime/runner.d.ts +127 -0
- package/dist/core/runtime/runner.d.ts.map +1 -0
- package/dist/core/runtime/runner.js +614 -0
- package/dist/core/runtime/runner.js.map +1 -0
- package/dist/core/session/ids.d.ts +7 -0
- package/dist/core/session/ids.d.ts.map +1 -0
- package/dist/core/session/ids.js +18 -0
- package/dist/core/session/ids.js.map +1 -0
- package/dist/core/session/jsonl-store.d.ts +14 -0
- package/dist/core/session/jsonl-store.d.ts.map +1 -0
- package/dist/core/session/jsonl-store.js +142 -0
- package/dist/core/session/jsonl-store.js.map +1 -0
- package/dist/core/session/store.d.ts +25 -0
- package/dist/core/session/store.d.ts.map +1 -0
- package/dist/core/session/store.js +2 -0
- package/dist/core/session/store.js.map +1 -0
- package/dist/core/session/types.d.ts +142 -0
- package/dist/core/session/types.d.ts.map +1 -0
- package/dist/core/session/types.js +2 -0
- package/dist/core/session/types.js.map +1 -0
- package/dist/core/tools/adapter.d.ts +33 -0
- package/dist/core/tools/adapter.d.ts.map +1 -0
- package/dist/core/tools/adapter.js +87 -0
- package/dist/core/tools/adapter.js.map +1 -0
- package/dist/core/tools/builtins.d.ts +13 -0
- package/dist/core/tools/builtins.d.ts.map +1 -0
- package/dist/core/tools/builtins.js +219 -0
- package/dist/core/tools/builtins.js.map +1 -0
- package/dist/core/tools/context.d.ts +136 -0
- package/dist/core/tools/context.d.ts.map +1 -0
- package/dist/core/tools/context.js +2 -0
- package/dist/core/tools/context.js.map +1 -0
- package/dist/core/tools/def.d.ts +27 -0
- package/dist/core/tools/def.d.ts.map +1 -0
- package/dist/core/tools/def.js +2 -0
- package/dist/core/tools/def.js.map +1 -0
- package/dist/core/tools/qa-check.d.ts +30 -0
- package/dist/core/tools/qa-check.d.ts.map +1 -0
- package/dist/core/tools/qa-check.js +50 -0
- package/dist/core/tools/qa-check.js.map +1 -0
- package/dist/core/tools/task.d.ts +8 -0
- package/dist/core/tools/task.d.ts.map +1 -0
- package/dist/core/tools/task.js +33 -0
- package/dist/core/tools/task.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/server/create-server.d.ts +51 -0
- package/dist/server/create-server.d.ts.map +1 -0
- package/dist/server/create-server.js +29 -0
- package/dist/server/create-server.js.map +1 -0
- package/openapi.yaml +239 -0
- package/package.json +32 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { newPermissionRequestId } from "../session/ids.js";
|
|
2
|
+
import { evaluateRules } from "../permission/ruleset.js";
|
|
3
|
+
export class RejectedError extends Error {
|
|
4
|
+
feedback;
|
|
5
|
+
constructor(message, feedback) {
|
|
6
|
+
super(feedback ? `${message}(用户反馈:${feedback})` : message);
|
|
7
|
+
this.feedback = feedback;
|
|
8
|
+
this.name = "RejectedError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class PermissionEngine {
|
|
12
|
+
sessionId;
|
|
13
|
+
options;
|
|
14
|
+
/** Rulesets in ascending precedence: built-in defaults → agent preset → session. */
|
|
15
|
+
rulesets;
|
|
16
|
+
sessionRules = [];
|
|
17
|
+
pending = new Map();
|
|
18
|
+
disposed = false;
|
|
19
|
+
/** 临时允许缓存:once 批准后同一 run 内相同模式直接放行(F1,避免 Agent
|
|
20
|
+
* 二次调用相同命令时重复打断用户)。run 结束(dispose)时清空,下次 run
|
|
21
|
+
* 重新询问。键 = permission + pattern(bash 的 pattern 即命令原文)。 */
|
|
22
|
+
onceAllowed = new Set();
|
|
23
|
+
constructor(sessionId, baseRulesets, sessionRules = [], options = {}) {
|
|
24
|
+
this.sessionId = sessionId;
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.rulesets = [...baseRulesets, this.sessionRules];
|
|
27
|
+
this.sessionRules.push(...sessionRules);
|
|
28
|
+
}
|
|
29
|
+
onceKey(permission, pattern) {
|
|
30
|
+
return `${permission}\u0000${pattern}`;
|
|
31
|
+
}
|
|
32
|
+
evaluate(permission, pattern) {
|
|
33
|
+
return evaluateRules(this.rulesets, permission, pattern);
|
|
34
|
+
}
|
|
35
|
+
/** Snapshot of the effective session rules (for persistence on reply "always"). */
|
|
36
|
+
get sessionRuleset() {
|
|
37
|
+
return [...this.sessionRules];
|
|
38
|
+
}
|
|
39
|
+
revoke(permission, patterns) {
|
|
40
|
+
const before = this.sessionRules.length;
|
|
41
|
+
const patternSet = new Set(patterns);
|
|
42
|
+
const retained = this.sessionRules.filter((rule) => !(rule.permission === permission && patternSet.has(rule.pattern)));
|
|
43
|
+
this.sessionRules.splice(0, this.sessionRules.length, ...retained);
|
|
44
|
+
return retained.length !== before;
|
|
45
|
+
}
|
|
46
|
+
get pendingRequests() {
|
|
47
|
+
return [...this.pending.values()].map((entry) => entry.request);
|
|
48
|
+
}
|
|
49
|
+
async ask(input) {
|
|
50
|
+
if (this.disposed)
|
|
51
|
+
throw new RejectedError("权限引擎已关闭(服务重启或会话结束),请重试");
|
|
52
|
+
const patterns = input.patterns.length ? input.patterns : ["*"];
|
|
53
|
+
// 已 once 批准的相同模式直接放行,不重复询问。
|
|
54
|
+
const undecided = patterns.filter((pattern) => this.evaluate(input.permission, pattern) !== "allow" && !this.onceAllowed.has(this.onceKey(input.permission, pattern)));
|
|
55
|
+
if (undecided.length === 0)
|
|
56
|
+
return "once";
|
|
57
|
+
const request = {
|
|
58
|
+
id: newPermissionRequestId(),
|
|
59
|
+
sessionId: input.sessionId,
|
|
60
|
+
permission: input.permission,
|
|
61
|
+
patterns: undecided,
|
|
62
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
63
|
+
always: input.always ?? [...undecided],
|
|
64
|
+
...(input.tool ? { tool: input.tool } : {}),
|
|
65
|
+
};
|
|
66
|
+
const decision = new Promise((resolve) => {
|
|
67
|
+
this.pending.set(request.id, { request, resolve });
|
|
68
|
+
});
|
|
69
|
+
await this.options.onAsked?.(request);
|
|
70
|
+
const { reply, feedback } = await decision;
|
|
71
|
+
await this.options.onReplied?.(request, reply);
|
|
72
|
+
if (reply === "reject") {
|
|
73
|
+
throw new RejectedError(`权限被拒绝:${input.permission} ${undecided.join(", ")}`, feedback);
|
|
74
|
+
}
|
|
75
|
+
if (reply === "once") {
|
|
76
|
+
// F1:本 run 内相同命令/模式不再询问。
|
|
77
|
+
for (const pattern of request.patterns)
|
|
78
|
+
this.onceAllowed.add(this.onceKey(input.permission, pattern));
|
|
79
|
+
}
|
|
80
|
+
if (reply === "always") {
|
|
81
|
+
for (const pattern of request.always) {
|
|
82
|
+
const rule = { permission: input.permission, pattern, action: "allow" };
|
|
83
|
+
this.sessionRules.push(rule);
|
|
84
|
+
await this.options.onSessionRuleAdded?.(input.sessionId, rule);
|
|
85
|
+
}
|
|
86
|
+
// Auto-resolve other pending requests in this session now fully covered.
|
|
87
|
+
for (const [id, entry] of [...this.pending]) {
|
|
88
|
+
const covered = entry.request.patterns.every((pattern) => this.evaluate(entry.request.permission, pattern) === "allow");
|
|
89
|
+
if (covered) {
|
|
90
|
+
this.pending.delete(id);
|
|
91
|
+
entry.resolve({ reply: "always" });
|
|
92
|
+
await this.options.onReplied?.(entry.request, "always");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return reply;
|
|
97
|
+
}
|
|
98
|
+
/** Resolves a pending request. Returns false when the id is unknown (already
|
|
99
|
+
* resolved or session restarted — callers map that to a 404/conflict).
|
|
100
|
+
*
|
|
101
|
+
* Harness-course retrofit(tutorial-harness 08,OpenCode 拒绝级联):用户拒绝一个
|
|
102
|
+
* 请求时,同会话排队中的其他待批请求一并拒绝——用户说“不”的时候,
|
|
103
|
+
* 不该让后面的请求继续敲门。批准不连坐,拒绝连坐。 */
|
|
104
|
+
reply(requestId, reply, feedback) {
|
|
105
|
+
const entry = this.pending.get(requestId);
|
|
106
|
+
if (!entry)
|
|
107
|
+
return false;
|
|
108
|
+
this.pending.delete(requestId);
|
|
109
|
+
entry.resolve({ reply, feedback });
|
|
110
|
+
if (reply === "reject") {
|
|
111
|
+
for (const [id, other] of [...this.pending]) {
|
|
112
|
+
this.pending.delete(id);
|
|
113
|
+
// 级联拒绝不复用原反馈:这些请求没被单独审过,理由必须如实说是连坐
|
|
114
|
+
other.resolve({ reply: "reject", feedback: "用户已拒绝同会话的另一个请求,本次一并拒绝" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/** Rejects everything still pending — used on abort and process teardown so
|
|
120
|
+
* tool calls never hang forever (spec §5.4: restart semantics). Also clears
|
|
121
|
+
* the once-allowed cache so the next run asks again. */
|
|
122
|
+
dispose(reason = "会话已中止或服务重启,请重试") {
|
|
123
|
+
this.disposed = true;
|
|
124
|
+
for (const [, entry] of [...this.pending]) {
|
|
125
|
+
entry.resolve({ reply: "reject", feedback: reason });
|
|
126
|
+
}
|
|
127
|
+
this.pending.clear();
|
|
128
|
+
this.onceAllowed.clear();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/core/permission/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Permission ruleset DSL (spec §5.1).
|
|
2
|
+
*
|
|
3
|
+
* A ruleset is an ordered list of rules; the LAST matching rule wins and the
|
|
4
|
+
* default action when nothing matches is "ask". Config syntax mirrors
|
|
5
|
+
* opencode.json: a bare action string, or a map of permission -> action or
|
|
6
|
+
* pattern->action map. */
|
|
7
|
+
export type Action = "allow" | "deny" | "ask";
|
|
8
|
+
export type Rule = {
|
|
9
|
+
permission: string;
|
|
10
|
+
pattern: string;
|
|
11
|
+
action: Action;
|
|
12
|
+
expiresAt?: string;
|
|
13
|
+
};
|
|
14
|
+
export type Ruleset = Rule[];
|
|
15
|
+
export type PermissionConfig = Action | Record<string, Action | Record<string, Action>>;
|
|
16
|
+
/** Well-known permission keys (spec §5.1). Tools may introduce additional keys
|
|
17
|
+
* (e.g. future MCP `server_*`); the engine treats keys as opaque strings and
|
|
18
|
+
* matches them with wildcards. */
|
|
19
|
+
export declare const PERMISSIONS: readonly ["read", "edit", "bash", "glob", "grep", "list", "webfetch", "connector", "task", "todo", "external_directory"];
|
|
20
|
+
/** Converts config syntax into a flat ruleset. Key order in the config object
|
|
21
|
+
* is preserved, so later keys override earlier ones for overlapping matches. */
|
|
22
|
+
export declare function rulesetFromConfig(config: PermissionConfig): Ruleset;
|
|
23
|
+
/** Glob-style wildcard match: `*` matches any run of characters (including
|
|
24
|
+
* path separators), `?` matches one character. Same spirit as OpenCode's
|
|
25
|
+
* Wildcard.match — patterns are not filesystem paths, just strings. */
|
|
26
|
+
export declare function wildcardMatch(pattern: string, value: string): boolean;
|
|
27
|
+
/** Core evaluation: last matching rule across all rulesets (in order) wins.
|
|
28
|
+
* Later rulesets in the array have higher precedence. Default: "ask". */
|
|
29
|
+
export declare function evaluateRules(rulesets: Ruleset[], permission: string, pattern: string): Action;
|
|
30
|
+
//# sourceMappingURL=ruleset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruleset.d.ts","sourceRoot":"","sources":["../../../src/core/permission/ruleset.ts"],"names":[],"mappings":"AAAA;;;;;2BAK2B;AAE3B,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAE9C,MAAM,MAAM,IAAI,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC/F,MAAM,MAAM,OAAO,GAAG,IAAI,EAAE,CAAC;AAE7B,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAExF;;mCAEmC;AACnC,eAAO,MAAM,WAAW,0HAYd,CAAC;AAEX;iFACiF;AACjF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAanE;AAED;;wEAEwE;AACxE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAqBrE;AAED;0EAC0E;AAC1E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAW9F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Permission ruleset DSL (spec §5.1).
|
|
2
|
+
*
|
|
3
|
+
* A ruleset is an ordered list of rules; the LAST matching rule wins and the
|
|
4
|
+
* default action when nothing matches is "ask". Config syntax mirrors
|
|
5
|
+
* opencode.json: a bare action string, or a map of permission -> action or
|
|
6
|
+
* pattern->action map. */
|
|
7
|
+
/** Well-known permission keys (spec §5.1). Tools may introduce additional keys
|
|
8
|
+
* (e.g. future MCP `server_*`); the engine treats keys as opaque strings and
|
|
9
|
+
* matches them with wildcards. */
|
|
10
|
+
export const PERMISSIONS = [
|
|
11
|
+
"read",
|
|
12
|
+
"edit", // covers write/edit/apply_patch
|
|
13
|
+
"bash",
|
|
14
|
+
"glob",
|
|
15
|
+
"grep",
|
|
16
|
+
"list",
|
|
17
|
+
"webfetch",
|
|
18
|
+
"connector",
|
|
19
|
+
"task",
|
|
20
|
+
"todo",
|
|
21
|
+
"external_directory",
|
|
22
|
+
];
|
|
23
|
+
/** Converts config syntax into a flat ruleset. Key order in the config object
|
|
24
|
+
* is preserved, so later keys override earlier ones for overlapping matches. */
|
|
25
|
+
export function rulesetFromConfig(config) {
|
|
26
|
+
if (typeof config === "string")
|
|
27
|
+
return [{ permission: "*", pattern: "*", action: config }];
|
|
28
|
+
const rules = [];
|
|
29
|
+
for (const [permission, value] of Object.entries(config)) {
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
rules.push({ permission, pattern: "*", action: value });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const [pattern, action] of Object.entries(value)) {
|
|
35
|
+
rules.push({ permission, pattern, action });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return rules;
|
|
40
|
+
}
|
|
41
|
+
/** Glob-style wildcard match: `*` matches any run of characters (including
|
|
42
|
+
* path separators), `?` matches one character. Same spirit as OpenCode's
|
|
43
|
+
* Wildcard.match — patterns are not filesystem paths, just strings. */
|
|
44
|
+
export function wildcardMatch(pattern, value) {
|
|
45
|
+
let p = 0;
|
|
46
|
+
let v = 0;
|
|
47
|
+
let star = -1;
|
|
48
|
+
let starValue = 0;
|
|
49
|
+
while (v < value.length) {
|
|
50
|
+
if (p < pattern.length && (pattern[p] === "?" || pattern[p] === value[v])) {
|
|
51
|
+
p++;
|
|
52
|
+
v++;
|
|
53
|
+
}
|
|
54
|
+
else if (p < pattern.length && pattern[p] === "*") {
|
|
55
|
+
star = p++;
|
|
56
|
+
starValue = v;
|
|
57
|
+
}
|
|
58
|
+
else if (star !== -1) {
|
|
59
|
+
p = star + 1;
|
|
60
|
+
v = ++starValue;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
while (p < pattern.length && pattern[p] === "*")
|
|
67
|
+
p++;
|
|
68
|
+
return p === pattern.length;
|
|
69
|
+
}
|
|
70
|
+
/** Core evaluation: last matching rule across all rulesets (in order) wins.
|
|
71
|
+
* Later rulesets in the array have higher precedence. Default: "ask". */
|
|
72
|
+
export function evaluateRules(rulesets, permission, pattern) {
|
|
73
|
+
let result = "ask";
|
|
74
|
+
for (const ruleset of rulesets) {
|
|
75
|
+
for (const rule of ruleset) {
|
|
76
|
+
if (rule.expiresAt && Date.parse(rule.expiresAt) <= Date.now())
|
|
77
|
+
continue;
|
|
78
|
+
if (wildcardMatch(rule.permission, permission) && wildcardMatch(rule.pattern, pattern)) {
|
|
79
|
+
result = rule.action;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=ruleset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruleset.js","sourceRoot":"","sources":["../../../src/core/permission/ruleset.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
/** Framework compaction (spec §8.3): when the PI-visible context approaches the
|
|
4
|
+
* model's window, replace the older history with a generated summary and keep
|
|
5
|
+
* a recent tail. Wired through PI's transformContext — the loop calls it
|
|
6
|
+
* before every LLM request. The emitted `compaction` part marks the boundary
|
|
7
|
+
* in the transcript (the full pre-compaction messages stay persisted in the
|
|
8
|
+
* framework store; only the model's working context is condensed). */
|
|
9
|
+
export type CompactionOptions = {
|
|
10
|
+
/** Cheap model used to write the summary (the relay's small model). */
|
|
11
|
+
summaryModel: Model<Api>;
|
|
12
|
+
/** Main model's context window (tokens). */
|
|
13
|
+
contextWindow: number;
|
|
14
|
+
/** Reserve headroom for the summary prompt + next reply. */
|
|
15
|
+
reserveTokens?: number;
|
|
16
|
+
/** How many recent messages to keep verbatim. */
|
|
17
|
+
keepRecentMessages?: number;
|
|
18
|
+
/** Streams one completion from the summary model. */
|
|
19
|
+
streamSummary: (messages: AgentMessage[]) => Promise<string>;
|
|
20
|
+
/** Called when a compaction happens so the runner can emit the part. */
|
|
21
|
+
onCompacted?: (summary: string, tokensBefore: number) => void;
|
|
22
|
+
/** Called when a compaction attempt fails ("summary-empty" | "summary-inflated");
|
|
23
|
+
* after any failure the transform stops retrying for this run (失败记忆). */
|
|
24
|
+
onCompactionFailed?: (reason: "summary-empty" | "summary-inflated") => void | Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
/** Returns a transformContext that compacts when over threshold.
|
|
27
|
+
*
|
|
28
|
+
* Harness-course retrofits (tutorial-harness 05/07):
|
|
29
|
+
* - 膨胀拒绝(Gemini chatCompressionService):摘要比被压区还长时作废本次压缩,
|
|
30
|
+
* 宁可用全量上下文也不换一份负收益的摘要。
|
|
31
|
+
* - 失败记忆(Gemini hasFailedCompressionAttempt):摘要调用失败或膨胀后记住这次
|
|
32
|
+
* 失败,后续不再反复烧摘要 token;失败不静默,通过 onCompactionFailed 上报。
|
|
33
|
+
*/
|
|
34
|
+
export declare function createCompactionTransform(options: CompactionOptions): (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
35
|
+
/** One-shot completion via an AssistantMessageEventStream (relay streamFn
|
|
36
|
+
* shape): drives the stream to completion and returns its full text. Used for
|
|
37
|
+
* summary generation and async title generation (spec §13.2). */
|
|
38
|
+
export declare function streamOneText(streamFn: (model: Model<Api>, context: {
|
|
39
|
+
systemPrompt: string;
|
|
40
|
+
messages: AgentMessage[];
|
|
41
|
+
}) => Promise<{
|
|
42
|
+
result(): Promise<{
|
|
43
|
+
content?: unknown;
|
|
44
|
+
}>;
|
|
45
|
+
}> | {
|
|
46
|
+
result(): Promise<{
|
|
47
|
+
content?: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
}, model: Model<Api>, systemPrompt: string, messages: AgentMessage[]): Promise<string>;
|
|
50
|
+
/** Builds the compaction transform for a run from a relay stream fn + model
|
|
51
|
+
* refs. Returns undefined when compaction is disabled (no summaryModel). */
|
|
52
|
+
export declare function buildCompactionTransform(input: {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
contextWindow: number;
|
|
55
|
+
summaryModel: Model<Api> | null;
|
|
56
|
+
streamOne: (model: Model<Api>, messages: AgentMessage[]) => Promise<string>;
|
|
57
|
+
onCompacted?: (summary: string, tokensBefore: number) => void;
|
|
58
|
+
}): ((messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>) | undefined;
|
|
59
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../../src/core/runtime/compaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;uEAKuE;AAEvE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uEAAuE;IACvE,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACzB,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9D;8EAC0E;IAC1E,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,GAAG,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7F,CAAC;AAiCF;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAuCjJ;AAED;;kEAEkE;AAClE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,KAAK,OAAO,CAAC;IAAE,MAAM,IAAI,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,GAAG;IAAE,MAAM,IAAI,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAAE,EAClM,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,YAAY,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED;6EAC6E;AAC7E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/D,GAAG,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,GAAG,SAAS,CAQ5F"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Rough token estimate (chars/4) — good enough for a threshold trigger; the
|
|
2
|
+
* precise accounting happens provider-side. */
|
|
3
|
+
function estimateTokens(messages) {
|
|
4
|
+
let total = 0;
|
|
5
|
+
for (const message of messages) {
|
|
6
|
+
const content = message.content;
|
|
7
|
+
if (typeof content === "string")
|
|
8
|
+
total += Math.ceil(content.length / 4);
|
|
9
|
+
else if (Array.isArray(content)) {
|
|
10
|
+
for (const block of content) {
|
|
11
|
+
if (typeof block === "object" && block !== null && typeof block.text === "string") {
|
|
12
|
+
total += Math.ceil((block.text).length / 4);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (message.role === "toolResult") {
|
|
17
|
+
const blocks = message.content;
|
|
18
|
+
if (Array.isArray(blocks)) {
|
|
19
|
+
for (const block of blocks) {
|
|
20
|
+
if (typeof block === "object" && block !== null && typeof block.text === "string") {
|
|
21
|
+
total += Math.ceil((block.text).length / 4);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
const SUMMARY_INSTRUCTION = "把以下对话压缩成一份中文工作摘要,保留:任务目标、已完成的关键步骤与工具结果、当前进度、待办事项、重要的文件路径/命令/产物。不要续写对话,只输出结构化摘要。";
|
|
30
|
+
/** Returns a transformContext that compacts when over threshold.
|
|
31
|
+
*
|
|
32
|
+
* Harness-course retrofits (tutorial-harness 05/07):
|
|
33
|
+
* - 膨胀拒绝(Gemini chatCompressionService):摘要比被压区还长时作废本次压缩,
|
|
34
|
+
* 宁可用全量上下文也不换一份负收益的摘要。
|
|
35
|
+
* - 失败记忆(Gemini hasFailedCompressionAttempt):摘要调用失败或膨胀后记住这次
|
|
36
|
+
* 失败,后续不再反复烧摘要 token;失败不静默,通过 onCompactionFailed 上报。
|
|
37
|
+
*/
|
|
38
|
+
export function createCompactionTransform(options) {
|
|
39
|
+
const reserve = options.reserveTokens ?? 4096;
|
|
40
|
+
const keepRecent = options.keepRecentMessages ?? 8;
|
|
41
|
+
let hasFailed = false;
|
|
42
|
+
return async (messages, signal) => {
|
|
43
|
+
void signal;
|
|
44
|
+
const tokens = estimateTokens(messages);
|
|
45
|
+
if (tokens + reserve < options.contextWindow)
|
|
46
|
+
return messages;
|
|
47
|
+
if (hasFailed)
|
|
48
|
+
return messages; // 失败记忆:不再反复尝试摘要
|
|
49
|
+
if (messages.length <= keepRecent + 1)
|
|
50
|
+
return messages; // nothing worth compacting
|
|
51
|
+
const tail = messages.slice(-keepRecent);
|
|
52
|
+
const head = messages.slice(0, -keepRecent);
|
|
53
|
+
const summaryInput = [
|
|
54
|
+
...head,
|
|
55
|
+
{ role: "user", content: SUMMARY_INSTRUCTION, timestamp: Date.now() },
|
|
56
|
+
];
|
|
57
|
+
const summary = await options.streamSummary(summaryInput).catch(() => "");
|
|
58
|
+
if (!summary) {
|
|
59
|
+
hasFailed = true;
|
|
60
|
+
await options.onCompactionFailed?.("summary-empty");
|
|
61
|
+
return messages; // compaction failed — degrade to full context
|
|
62
|
+
}
|
|
63
|
+
// 膨胀拒绝:摘要比被压区还长,压了不如不压
|
|
64
|
+
const headTokens = estimateTokens(head);
|
|
65
|
+
if (estimateTokens([{ role: "user", content: summary, timestamp: Date.now() }]) >= headTokens) {
|
|
66
|
+
hasFailed = true;
|
|
67
|
+
await options.onCompactionFailed?.("summary-inflated");
|
|
68
|
+
return messages;
|
|
69
|
+
}
|
|
70
|
+
options.onCompacted?.(summary, tokens);
|
|
71
|
+
const compactionMessage = {
|
|
72
|
+
role: "user",
|
|
73
|
+
content: `【早期对话摘要】\n${summary}`,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
return [compactionMessage, ...tail];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** One-shot completion via an AssistantMessageEventStream (relay streamFn
|
|
80
|
+
* shape): drives the stream to completion and returns its full text. Used for
|
|
81
|
+
* summary generation and async title generation (spec §13.2). */
|
|
82
|
+
export async function streamOneText(streamFn, model, systemPrompt, messages) {
|
|
83
|
+
const stream = await streamFn(model, { systemPrompt, messages });
|
|
84
|
+
const final = await stream.result();
|
|
85
|
+
const content = final.content;
|
|
86
|
+
if (typeof content === "string")
|
|
87
|
+
return content;
|
|
88
|
+
if (Array.isArray(content)) {
|
|
89
|
+
return content
|
|
90
|
+
.map((block) => (typeof block === "object" && block !== null && block.type === "text" ? String(block.text ?? "") : ""))
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join("\n");
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
/** Builds the compaction transform for a run from a relay stream fn + model
|
|
97
|
+
* refs. Returns undefined when compaction is disabled (no summaryModel). */
|
|
98
|
+
export function buildCompactionTransform(input) {
|
|
99
|
+
if (!input.enabled || !input.summaryModel)
|
|
100
|
+
return undefined;
|
|
101
|
+
return createCompactionTransform({
|
|
102
|
+
summaryModel: input.summaryModel,
|
|
103
|
+
contextWindow: input.contextWindow,
|
|
104
|
+
streamSummary: (messages) => input.streamOne(input.summaryModel, messages),
|
|
105
|
+
...(input.onCompacted ? { onCompacted: input.onCompacted } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=compaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../../src/core/runtime/compaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EventLog } from "../events/bus.js";
|
|
2
|
+
import type { SessionStore } from "../session/store.js";
|
|
3
|
+
/** Lease recovery (spec §3.2): the runner stamps a lease on the session
|
|
4
|
+
* document while it owns a run. A periodic scan reclaims sessions whose lease
|
|
5
|
+
* lapsed without a live owner (process crash/restart), emitting a settle
|
|
6
|
+
* event so clients don't sit on a stale "running" status forever.
|
|
7
|
+
*
|
|
8
|
+
* Storage-agnostic (M5): the session store owns the lease fields; recovery
|
|
9
|
+
* only needs a list + clear. Implementations provide the store-specific
|
|
10
|
+
* `listExpiredLeases` / `clearLeaseIfExpired`. */
|
|
11
|
+
export declare const scanIntervalMs = 60000;
|
|
12
|
+
export declare const leaseDurationMs: number;
|
|
13
|
+
export type LeaseRecoveryStore = {
|
|
14
|
+
/** Sessions whose lease lapsed (leaseExpiresAt < now), capped. */
|
|
15
|
+
listExpiredLeases(): Promise<{
|
|
16
|
+
sessionId: string;
|
|
17
|
+
}[]>;
|
|
18
|
+
/** Clears the lease if it is still expired; false if another won the race. */
|
|
19
|
+
clearLeaseIfExpired(sessionId: string): Promise<boolean>;
|
|
20
|
+
};
|
|
21
|
+
/** Interrupted-run finalization (product P2): when a run dies with its lease
|
|
22
|
+
* (crash/restart), the run's in-flight projections would otherwise stay
|
|
23
|
+
* frozen mid-run forever — a pending permission card, tool parts stuck
|
|
24
|
+
* "running", todos stuck "in_progress". The event log is the projection's
|
|
25
|
+
* single source of truth, so recovery derives the leftovers from it and
|
|
26
|
+
* folds each into a terminal state (replied/reject, tool error, cancelled),
|
|
27
|
+
* both in the store and as appended events. Idempotent: a second pass finds
|
|
28
|
+
* no pending leftovers. */
|
|
29
|
+
export declare function finalizeInterruptedRun(input: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
log: EventLog;
|
|
32
|
+
store: SessionStore;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
export declare function reclaimExpiredLeases(input: {
|
|
35
|
+
store: LeaseRecoveryStore;
|
|
36
|
+
log: EventLog;
|
|
37
|
+
finalizeStore?: SessionStore;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
export declare function startLeaseRecovery(input: {
|
|
40
|
+
store: LeaseRecoveryStore;
|
|
41
|
+
log: EventLog;
|
|
42
|
+
finalizeStore?: SessionStore;
|
|
43
|
+
}): void;
|
|
44
|
+
export { leaseDurationMs as fwLeaseDurationMs };
|
|
45
|
+
//# sourceMappingURL=lease-recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease-recovery.d.ts","sourceRoot":"","sources":["../../../src/core/runtime/lease-recovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;mDAOmD;AAEnD,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,eAAO,MAAM,eAAe,QAAiB,CAAC;AAE9C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,kEAAkE;IAClE,iBAAiB,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACtD,8EAA8E;IAC9E,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D,CAAC;AAIF;;;;;;;4BAO4B;AAC5B,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsD5H;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,QAAQ,CAAC;IAAC,aAAa,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3I;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,QAAQ,CAAC;IAAC,aAAa,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,IAAI,CAS1H;AAED,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { notifyEventLogListeners } from "../events/bus.js";
|
|
2
|
+
/** Lease recovery (spec §3.2): the runner stamps a lease on the session
|
|
3
|
+
* document while it owns a run. A periodic scan reclaims sessions whose lease
|
|
4
|
+
* lapsed without a live owner (process crash/restart), emitting a settle
|
|
5
|
+
* event so clients don't sit on a stale "running" status forever.
|
|
6
|
+
*
|
|
7
|
+
* Storage-agnostic (M5): the session store owns the lease fields; recovery
|
|
8
|
+
* only needs a list + clear. Implementations provide the store-specific
|
|
9
|
+
* `listExpiredLeases` / `clearLeaseIfExpired`. */
|
|
10
|
+
export const scanIntervalMs = 60_000;
|
|
11
|
+
export const leaseDurationMs = 10 * 60 * 1000;
|
|
12
|
+
const globalRecovery = globalThis;
|
|
13
|
+
/** Interrupted-run finalization (product P2): when a run dies with its lease
|
|
14
|
+
* (crash/restart), the run's in-flight projections would otherwise stay
|
|
15
|
+
* frozen mid-run forever — a pending permission card, tool parts stuck
|
|
16
|
+
* "running", todos stuck "in_progress". The event log is the projection's
|
|
17
|
+
* single source of truth, so recovery derives the leftovers from it and
|
|
18
|
+
* folds each into a terminal state (replied/reject, tool error, cancelled),
|
|
19
|
+
* both in the store and as appended events. Idempotent: a second pass finds
|
|
20
|
+
* no pending leftovers. */
|
|
21
|
+
export async function finalizeInterruptedRun(input) {
|
|
22
|
+
const events = await input.log.read(input.sessionId, 0, 1_000);
|
|
23
|
+
const append = async (event) => {
|
|
24
|
+
const persisted = await input.log.append({ sessionId: input.sessionId, ...event }).catch(() => null);
|
|
25
|
+
if (persisted)
|
|
26
|
+
notifyEventLogListeners(persisted);
|
|
27
|
+
};
|
|
28
|
+
// 1. Pending permission: a `permission.asked` with no later `replied` for
|
|
29
|
+
// the same request id. Fold it to `reject` so the card clears.
|
|
30
|
+
for (const asked of events) {
|
|
31
|
+
if (asked.type !== "permission.asked")
|
|
32
|
+
continue;
|
|
33
|
+
const requestId = asked.data.request.id;
|
|
34
|
+
const repliedAfter = events.some((event) => event.type === "permission.replied" && event.data.id === requestId);
|
|
35
|
+
if (repliedAfter)
|
|
36
|
+
continue;
|
|
37
|
+
await append({ type: "permission.replied", data: { id: requestId, reply: "reject" } });
|
|
38
|
+
}
|
|
39
|
+
const runningParts = new Map();
|
|
40
|
+
for (const event of events) {
|
|
41
|
+
if (event.type !== "message.part.updated")
|
|
42
|
+
continue;
|
|
43
|
+
const part = event.data.part;
|
|
44
|
+
if (part.type !== "tool")
|
|
45
|
+
continue;
|
|
46
|
+
if (part.state.status === "running" || part.state.status === "pending")
|
|
47
|
+
runningParts.set(part.id, part);
|
|
48
|
+
else
|
|
49
|
+
runningParts.delete(part.id);
|
|
50
|
+
}
|
|
51
|
+
for (const part of runningParts.values()) {
|
|
52
|
+
const started = part.state.status === "pending" ? new Date().toISOString() : part.state.time.start;
|
|
53
|
+
const terminal = {
|
|
54
|
+
...part,
|
|
55
|
+
state: {
|
|
56
|
+
status: "error",
|
|
57
|
+
input: part.state.input,
|
|
58
|
+
error: "运行因服务重启中断,可在同一会话继续。",
|
|
59
|
+
time: { start: started, end: new Date().toISOString() },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
await input.store.updatePart(terminal).catch(() => undefined);
|
|
63
|
+
await append({ type: "message.part.updated", data: { part: terminal } });
|
|
64
|
+
}
|
|
65
|
+
// 3. Todos stuck in flight: mark pending/in_progress items cancelled. Todos
|
|
66
|
+
// are event-only (no store row), so an appended event suffices.
|
|
67
|
+
const lastTodo = [...events].reverse().find((event) => event.type === "todo.updated");
|
|
68
|
+
if (lastTodo) {
|
|
69
|
+
const todos = lastTodo.data.todos;
|
|
70
|
+
if (todos.some((item) => item.status === "pending" || item.status === "in_progress")) {
|
|
71
|
+
const settled = todos.map((item) => (item.status === "pending" || item.status === "in_progress" ? { ...item, status: "cancelled" } : item));
|
|
72
|
+
await append({ type: "todo.updated", data: { todos: settled } });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function reclaimExpiredLeases(input) {
|
|
77
|
+
const expired = await input.store.listExpiredLeases();
|
|
78
|
+
for (const session of expired) {
|
|
79
|
+
const reclaimed = await input.store.clearLeaseIfExpired(session.sessionId);
|
|
80
|
+
if (!reclaimed)
|
|
81
|
+
continue; // another scanner won the race
|
|
82
|
+
const events = [
|
|
83
|
+
{ type: "session.error", data: { name: "LeaseExpired", message: "运行因服务重启中断,可在同一会话继续。" } },
|
|
84
|
+
{ type: "session.status", data: { status: "idle" } },
|
|
85
|
+
];
|
|
86
|
+
for (const event of events) {
|
|
87
|
+
const persisted = await input.log.append({
|
|
88
|
+
sessionId: session.sessionId,
|
|
89
|
+
type: event.type,
|
|
90
|
+
data: event.data,
|
|
91
|
+
}).catch(() => null);
|
|
92
|
+
if (persisted)
|
|
93
|
+
notifyEventLogListeners(persisted);
|
|
94
|
+
}
|
|
95
|
+
if (input.finalizeStore) {
|
|
96
|
+
await finalizeInterruptedRun({ sessionId: session.sessionId, log: input.log, store: input.finalizeStore }).catch(() => undefined);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function startLeaseRecovery(input) {
|
|
101
|
+
if (globalRecovery.__zmzaiFrameworkLeaseTimer)
|
|
102
|
+
return;
|
|
103
|
+
// A process may restart immediately after a lease has expired. Do one scan
|
|
104
|
+
// on startup so those sessions do not remain stale until the first interval.
|
|
105
|
+
void reclaimExpiredLeases(input).catch(() => undefined);
|
|
106
|
+
globalRecovery.__zmzaiFrameworkLeaseTimer = setInterval(() => {
|
|
107
|
+
void reclaimExpiredLeases(input).catch(() => undefined);
|
|
108
|
+
}, scanIntervalMs);
|
|
109
|
+
globalRecovery.__zmzaiFrameworkLeaseTimer.unref?.();
|
|
110
|
+
}
|
|
111
|
+
export { leaseDurationMs as fwLeaseDurationMs };
|
|
112
|
+
//# sourceMappingURL=lease-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease-recovery.js","sourceRoot":"","sources":["../../../src/core/runtime/lease-recovery.ts"],"names":[],"mappings":"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"}
|