@wrongstack/core 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/LICENSE +17 -0
- package/dist/defaults/index.d.ts +1082 -0
- package/dist/defaults/index.js +4549 -0
- package/dist/defaults/index.js.map +1 -0
- package/dist/index.d.ts +180 -0
- package/dist/index.js +6410 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/index.d.ts +71 -0
- package/dist/kernel/index.js +361 -0
- package/dist/kernel/index.js.map +1 -0
- package/dist/secret-scrubber-Dax_Ou_o.d.ts +1055 -0
- package/dist/system-prompt-BG3nks8P.d.ts +16 -0
- package/dist/tool-executor-DjnMELMV.d.ts +689 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +296 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +88 -0
- package/dist/utils/index.js +461 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/wstack-paths-D24ynAz1.d.ts +56 -0
- package/package.json +46 -0
- package/skills/git-flow/SKILL.md +32 -0
- package/skills/multi-agent/SKILL.md +62 -0
- package/skills/node-modern/SKILL.md +41 -0
- package/skills/prompt-engineering/SKILL.md +28 -0
- package/skills/react-modern/SKILL.md +34 -0
- package/skills/sdd/SKILL.md +78 -0
- package/skills/typescript-strict/SKILL.md +50 -0
|
@@ -0,0 +1,4549 @@
|
|
|
1
|
+
import * as fs4 from 'fs';
|
|
2
|
+
import * as path2 from 'path';
|
|
3
|
+
import * as fsp from 'fs/promises';
|
|
4
|
+
import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'crypto';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
|
|
8
|
+
// src/defaults/logger.ts
|
|
9
|
+
|
|
10
|
+
// src/utils/color.ts
|
|
11
|
+
var isColorTty = () => {
|
|
12
|
+
if (process.env.NO_COLOR) return false;
|
|
13
|
+
if (process.env.FORCE_COLOR) return true;
|
|
14
|
+
return Boolean(process.stdout?.isTTY);
|
|
15
|
+
};
|
|
16
|
+
var COLOR = isColorTty();
|
|
17
|
+
var wrap = (open3, close) => (s) => COLOR ? `\x1B[${open3}m${s}\x1B[${close}m` : s;
|
|
18
|
+
var color = {
|
|
19
|
+
reset: wrap("0", "0"),
|
|
20
|
+
bold: wrap("1", "22"),
|
|
21
|
+
dim: wrap("2", "22"),
|
|
22
|
+
italic: wrap("3", "23"),
|
|
23
|
+
underline: wrap("4", "24"),
|
|
24
|
+
red: wrap("31", "39"),
|
|
25
|
+
green: wrap("32", "39"),
|
|
26
|
+
yellow: wrap("33", "39"),
|
|
27
|
+
blue: wrap("34", "39"),
|
|
28
|
+
magenta: wrap("35", "39"),
|
|
29
|
+
cyan: wrap("36", "39"),
|
|
30
|
+
gray: wrap("90", "39"),
|
|
31
|
+
amber: wrap("38;5;214", "39"),
|
|
32
|
+
pink: wrap("38;5;205", "39"),
|
|
33
|
+
bgRed: wrap("41", "49"),
|
|
34
|
+
bgGreen: wrap("42", "49")
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/defaults/logger.ts
|
|
38
|
+
var LEVEL_RANK = {
|
|
39
|
+
error: 0,
|
|
40
|
+
warn: 1,
|
|
41
|
+
info: 2,
|
|
42
|
+
debug: 3,
|
|
43
|
+
trace: 4
|
|
44
|
+
};
|
|
45
|
+
var COLORS = {
|
|
46
|
+
error: color.red,
|
|
47
|
+
warn: color.yellow,
|
|
48
|
+
info: color.cyan,
|
|
49
|
+
debug: color.gray,
|
|
50
|
+
trace: color.dim
|
|
51
|
+
};
|
|
52
|
+
var DefaultLogger = class _DefaultLogger {
|
|
53
|
+
level;
|
|
54
|
+
file;
|
|
55
|
+
bindings;
|
|
56
|
+
pretty;
|
|
57
|
+
constructor(opts = {}) {
|
|
58
|
+
this.level = opts.level ?? process.env.WRONGSTACK_LOG_LEVEL ?? "info";
|
|
59
|
+
this.file = opts.file;
|
|
60
|
+
this.bindings = opts.bindings ?? {};
|
|
61
|
+
this.pretty = opts.pretty ?? true;
|
|
62
|
+
if (this.file) {
|
|
63
|
+
try {
|
|
64
|
+
fs4.mkdirSync(path2.dirname(this.file), { recursive: true });
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
error(msg, ctx) {
|
|
70
|
+
this.log("error", msg, ctx);
|
|
71
|
+
}
|
|
72
|
+
warn(msg, ctx) {
|
|
73
|
+
this.log("warn", msg, ctx);
|
|
74
|
+
}
|
|
75
|
+
info(msg, ctx) {
|
|
76
|
+
this.log("info", msg, ctx);
|
|
77
|
+
}
|
|
78
|
+
debug(msg, ctx) {
|
|
79
|
+
this.log("debug", msg, ctx);
|
|
80
|
+
}
|
|
81
|
+
trace(msg, ctx) {
|
|
82
|
+
this.log("trace", msg, ctx);
|
|
83
|
+
}
|
|
84
|
+
child(bindings) {
|
|
85
|
+
return new _DefaultLogger({
|
|
86
|
+
level: this.level,
|
|
87
|
+
file: this.file,
|
|
88
|
+
pretty: this.pretty,
|
|
89
|
+
bindings: { ...this.bindings, ...bindings }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
log(level, msg, ctx) {
|
|
93
|
+
const r = LEVEL_RANK[level];
|
|
94
|
+
const allowed = LEVEL_RANK[this.level];
|
|
95
|
+
if (r > allowed) return;
|
|
96
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
97
|
+
const entry = { ts, level, msg, ...this.bindings };
|
|
98
|
+
if (ctx !== void 0) {
|
|
99
|
+
entry.ctx = ctx instanceof Error ? { message: ctx.message, stack: ctx.stack } : ctx;
|
|
100
|
+
}
|
|
101
|
+
if (this.file) {
|
|
102
|
+
try {
|
|
103
|
+
fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
|
|
104
|
+
`);
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (r <= LEVEL_RANK.warn || this.level === "debug" || this.level === "trace") {
|
|
109
|
+
const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
|
|
110
|
+
if (ctx !== void 0) {
|
|
111
|
+
process.stderr.write(`${head} ${formatCtx(ctx)}
|
|
112
|
+
`);
|
|
113
|
+
} else {
|
|
114
|
+
process.stderr.write(`${head}
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function formatCtx(ctx) {
|
|
121
|
+
if (ctx instanceof Error) return color.dim(ctx.message);
|
|
122
|
+
if (typeof ctx === "string") return color.dim(ctx);
|
|
123
|
+
try {
|
|
124
|
+
return color.dim(JSON.stringify(ctx));
|
|
125
|
+
} catch {
|
|
126
|
+
return color.dim(String(ctx));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
var PROJECT_MARKERS = [
|
|
130
|
+
".git",
|
|
131
|
+
"package.json",
|
|
132
|
+
"pnpm-workspace.yaml",
|
|
133
|
+
"go.mod",
|
|
134
|
+
"Cargo.toml",
|
|
135
|
+
"pyproject.toml",
|
|
136
|
+
".wrongstack"
|
|
137
|
+
];
|
|
138
|
+
var DefaultPathResolver = class {
|
|
139
|
+
projectRoot;
|
|
140
|
+
cwd;
|
|
141
|
+
constructor(cwd = process.cwd()) {
|
|
142
|
+
this.cwd = path2.resolve(cwd);
|
|
143
|
+
this.projectRoot = this.detectProjectRoot(this.cwd);
|
|
144
|
+
}
|
|
145
|
+
detectProjectRoot(start) {
|
|
146
|
+
let dir = path2.resolve(start);
|
|
147
|
+
const root = path2.parse(dir).root;
|
|
148
|
+
while (dir !== root) {
|
|
149
|
+
for (const marker of PROJECT_MARKERS) {
|
|
150
|
+
try {
|
|
151
|
+
fs4.accessSync(path2.join(dir, marker));
|
|
152
|
+
return dir;
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const parent = path2.dirname(dir);
|
|
157
|
+
if (parent === dir) break;
|
|
158
|
+
dir = parent;
|
|
159
|
+
}
|
|
160
|
+
return path2.resolve(start);
|
|
161
|
+
}
|
|
162
|
+
resolve(input) {
|
|
163
|
+
const abs = path2.isAbsolute(input) ? input : path2.resolve(this.cwd, input);
|
|
164
|
+
let real;
|
|
165
|
+
try {
|
|
166
|
+
real = fs4.realpathSync(abs);
|
|
167
|
+
} catch {
|
|
168
|
+
real = path2.normalize(abs);
|
|
169
|
+
}
|
|
170
|
+
return real;
|
|
171
|
+
}
|
|
172
|
+
isInsideRoot(absPath) {
|
|
173
|
+
const normalized = path2.normalize(absPath);
|
|
174
|
+
const root = path2.normalize(this.projectRoot);
|
|
175
|
+
if (normalized === root) return true;
|
|
176
|
+
const rel = path2.relative(root, normalized);
|
|
177
|
+
return !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
178
|
+
}
|
|
179
|
+
ensureInsideRoot(absPath) {
|
|
180
|
+
const resolved = this.resolve(absPath);
|
|
181
|
+
if (!this.isInsideRoot(resolved)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Path "${absPath}" resolves outside the project root (${this.projectRoot})`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/defaults/secret-scrubber.ts
|
|
191
|
+
var PATTERNS = [
|
|
192
|
+
// Anchored at the start where possible so partial matches inside larger
|
|
193
|
+
// strings don't trigger false positives.
|
|
194
|
+
{ type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
|
|
195
|
+
{ type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
|
|
196
|
+
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
|
|
197
|
+
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
|
|
198
|
+
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
|
|
199
|
+
{ type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
|
|
200
|
+
{ type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
|
|
201
|
+
{ type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
|
|
202
|
+
{ type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
|
|
203
|
+
{
|
|
204
|
+
type: "jwt",
|
|
205
|
+
// Anchored: look for literal "eyJ" which is unambiguous for JWT header
|
|
206
|
+
regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
type: "private_key",
|
|
210
|
+
// Anchored: start must be BEGIN, end must be END with no extra dashes after END
|
|
211
|
+
regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
|
|
212
|
+
},
|
|
213
|
+
{ type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
|
|
214
|
+
{ type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
|
|
215
|
+
{ type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
|
|
216
|
+
{ type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
|
|
217
|
+
{ type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
|
|
218
|
+
{
|
|
219
|
+
type: "high_entropy_env",
|
|
220
|
+
// Value-side word boundary + length gate to avoid matching short random strings
|
|
221
|
+
regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
var DefaultSecretScrubber = class {
|
|
225
|
+
scrub(text) {
|
|
226
|
+
if (!text) return text;
|
|
227
|
+
let out = text;
|
|
228
|
+
for (const p of PATTERNS) {
|
|
229
|
+
out = out.replace(p.regex, (_match, group1, group2) => {
|
|
230
|
+
if (p.type === "high_entropy_env" && group1 && group2) {
|
|
231
|
+
return `${group1}=[REDACTED:${p.type}]`;
|
|
232
|
+
}
|
|
233
|
+
return `[REDACTED:${p.type}]`;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
scrubObject(obj) {
|
|
239
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
240
|
+
const visit = (v) => {
|
|
241
|
+
if (typeof v === "string") return this.scrub(v);
|
|
242
|
+
if (v === null || typeof v !== "object") return v;
|
|
243
|
+
if (seen.has(v)) return v;
|
|
244
|
+
seen.add(v);
|
|
245
|
+
if (Array.isArray(v)) return v.map(visit);
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const [k, val] of Object.entries(v)) {
|
|
248
|
+
out[k] = visit(val);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
};
|
|
252
|
+
return visit(obj);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/types/provider.ts
|
|
257
|
+
var ProviderError = class extends Error {
|
|
258
|
+
status;
|
|
259
|
+
retryable;
|
|
260
|
+
providerId;
|
|
261
|
+
body;
|
|
262
|
+
cause;
|
|
263
|
+
constructor(message, status, retryable, providerId, opts = {}) {
|
|
264
|
+
super(message);
|
|
265
|
+
this.name = "ProviderError";
|
|
266
|
+
this.status = status;
|
|
267
|
+
this.retryable = retryable;
|
|
268
|
+
this.providerId = providerId;
|
|
269
|
+
this.body = opts.body;
|
|
270
|
+
this.cause = opts.cause;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Render a one-line, user-facing description. Designed for the CLI/TUI
|
|
274
|
+
* status line and the agent's retry warning. Avoids dumping raw JSON
|
|
275
|
+
* (which is what users see today when a 529 lands and the log message
|
|
276
|
+
* includes the full `{"type":"error",...}` body).
|
|
277
|
+
*
|
|
278
|
+
* Examples:
|
|
279
|
+
* "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
|
|
280
|
+
* "openai rate limited (429): Retry after 12s"
|
|
281
|
+
* "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
|
|
282
|
+
* "groq HTTP 500 (server error)"
|
|
283
|
+
*/
|
|
284
|
+
describe() {
|
|
285
|
+
const kind = describeStatus(this.status, this.body?.type);
|
|
286
|
+
const head = `${this.providerId} ${kind}`;
|
|
287
|
+
const detail = this.body?.message?.trim();
|
|
288
|
+
const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
|
|
289
|
+
if (detail && detail.length > 0) {
|
|
290
|
+
return `${head}: ${truncate(detail, 240)}${reqId}`;
|
|
291
|
+
}
|
|
292
|
+
return `${head}${reqId}`;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
function describeStatus(status, type) {
|
|
296
|
+
if (status === 0) return "network error";
|
|
297
|
+
if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
|
|
298
|
+
if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
|
|
299
|
+
if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
|
|
300
|
+
if (type === "permission_error" || status === 403) return `forbidden (${status})`;
|
|
301
|
+
if (type === "not_found_error" || status === 404) return `not found (${status})`;
|
|
302
|
+
if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
|
|
303
|
+
if (status === 408) return `timeout (${status})`;
|
|
304
|
+
if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
|
|
305
|
+
if (type) return `${type} (${status})`;
|
|
306
|
+
return `HTTP ${status}`;
|
|
307
|
+
}
|
|
308
|
+
function truncate(s, n) {
|
|
309
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/defaults/retry-policy.ts
|
|
313
|
+
var DefaultRetryPolicy = class {
|
|
314
|
+
shouldRetry(err, attempt) {
|
|
315
|
+
if (err instanceof ProviderError) {
|
|
316
|
+
if (!err.retryable) return false;
|
|
317
|
+
return attempt < this.maxAttempts(err);
|
|
318
|
+
}
|
|
319
|
+
const msg = err.message ?? "";
|
|
320
|
+
const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
|
|
321
|
+
if (isNetwork) return attempt < 2;
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
maxAttempts(err) {
|
|
325
|
+
if (err instanceof ProviderError) {
|
|
326
|
+
if (err.status === 429) return 5;
|
|
327
|
+
if (err.status === 529) return 3;
|
|
328
|
+
if (err.status >= 500) return 3;
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
return 2;
|
|
332
|
+
}
|
|
333
|
+
delayMs(attempt) {
|
|
334
|
+
const base = 1e3;
|
|
335
|
+
const exp = base * 2 ** attempt;
|
|
336
|
+
const jitter = Math.random() * base;
|
|
337
|
+
return Math.min(3e4, exp + jitter);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// src/defaults/error-handler.ts
|
|
342
|
+
function buildRecoveryStrategies(opts) {
|
|
343
|
+
return [
|
|
344
|
+
{
|
|
345
|
+
label: "context_overflow_reduce",
|
|
346
|
+
compactor: opts?.compactor,
|
|
347
|
+
async attempt(err, ctx) {
|
|
348
|
+
if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
|
|
349
|
+
if (this.compactor) {
|
|
350
|
+
try {
|
|
351
|
+
const report = await this.compactor.compact(ctx, { aggressive: true });
|
|
352
|
+
if (report.after < report.before) {
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
|
|
355
|
+
stopReason: "end_turn",
|
|
356
|
+
usage: { input: 0, output: 0 },
|
|
357
|
+
model: ctx.model
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
label: "rate_limit_backoff",
|
|
370
|
+
async attempt(err, ctx) {
|
|
371
|
+
if (err instanceof ProviderError && err.status === 429) {
|
|
372
|
+
const delayMs = err.body?.retryAfterMs ?? 5e3;
|
|
373
|
+
const delay = Math.max(1e3, Math.min(delayMs, 6e4));
|
|
374
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
|
|
377
|
+
stopReason: "end_turn",
|
|
378
|
+
usage: { input: 0, output: 0 },
|
|
379
|
+
model: ctx.model
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
label: "downgrade_model",
|
|
387
|
+
async attempt(err, ctx) {
|
|
388
|
+
if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
|
|
397
|
+
var DefaultErrorHandler = class {
|
|
398
|
+
strategies;
|
|
399
|
+
constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
|
|
400
|
+
this.strategies = strategies;
|
|
401
|
+
}
|
|
402
|
+
classify(err) {
|
|
403
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
404
|
+
return { kind: "abort", retryable: false };
|
|
405
|
+
}
|
|
406
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
407
|
+
return { kind: "abort", retryable: false };
|
|
408
|
+
}
|
|
409
|
+
if (err instanceof ProviderError) {
|
|
410
|
+
if (err.status === 429) return { kind: "rate_limit", retryable: true };
|
|
411
|
+
if (err.status === 529) return { kind: "overloaded", retryable: true };
|
|
412
|
+
if (err.status >= 500) return { kind: "server", retryable: true };
|
|
413
|
+
if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
|
|
414
|
+
return { kind: "context_overflow", retryable: false };
|
|
415
|
+
}
|
|
416
|
+
if (err.status >= 400) return { kind: "client", retryable: false };
|
|
417
|
+
}
|
|
418
|
+
if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
|
|
419
|
+
return { kind: "network", retryable: true };
|
|
420
|
+
}
|
|
421
|
+
return { kind: "unknown", retryable: false };
|
|
422
|
+
}
|
|
423
|
+
async recover(err, ctx) {
|
|
424
|
+
for (const strategy of this.strategies) {
|
|
425
|
+
const result = await strategy.attempt(err, ctx);
|
|
426
|
+
if (result !== null) return result;
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/defaults/token-counter.ts
|
|
433
|
+
var DefaultTokenCounter = class {
|
|
434
|
+
input = 0;
|
|
435
|
+
output = 0;
|
|
436
|
+
cacheRead = 0;
|
|
437
|
+
cacheWrite = 0;
|
|
438
|
+
costInput = 0;
|
|
439
|
+
costOutput = 0;
|
|
440
|
+
registry;
|
|
441
|
+
providerId;
|
|
442
|
+
events;
|
|
443
|
+
priceCache = /* @__PURE__ */ new Map();
|
|
444
|
+
constructor(opts = {}) {
|
|
445
|
+
this.registry = opts.registry;
|
|
446
|
+
this.providerId = opts.providerId;
|
|
447
|
+
this.events = opts.events;
|
|
448
|
+
}
|
|
449
|
+
account(usage, model) {
|
|
450
|
+
this.input += usage.input;
|
|
451
|
+
this.output += usage.output;
|
|
452
|
+
this.cacheRead += usage.cacheRead ?? 0;
|
|
453
|
+
this.cacheWrite += usage.cacheWrite ?? 0;
|
|
454
|
+
const price = model ? this.priceCache.get(model) : void 0;
|
|
455
|
+
if (price) {
|
|
456
|
+
this.applyPrice(usage, price);
|
|
457
|
+
} else if (this.registry && this.providerId && model) {
|
|
458
|
+
void this.registry.getModel(this.providerId, model).then((m) => {
|
|
459
|
+
if (m) {
|
|
460
|
+
const p = priceFromModel(m);
|
|
461
|
+
this.priceCache.set(model, p);
|
|
462
|
+
this.applyPrice(usage, p);
|
|
463
|
+
}
|
|
464
|
+
}).catch(() => {
|
|
465
|
+
this.events?.emit("token.cost_estimate_unavailable", { model: model ?? "<unknown>" });
|
|
466
|
+
return void 0;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** Synchronous variant for code paths that have already resolved the model. */
|
|
471
|
+
accountWithModel(usage, resolved) {
|
|
472
|
+
this.input += usage.input;
|
|
473
|
+
this.output += usage.output;
|
|
474
|
+
this.cacheRead += usage.cacheRead ?? 0;
|
|
475
|
+
this.cacheWrite += usage.cacheWrite ?? 0;
|
|
476
|
+
const price = priceFromModel(resolved);
|
|
477
|
+
this.priceCache.set(resolved.modelId, price);
|
|
478
|
+
this.applyPrice(usage, price);
|
|
479
|
+
}
|
|
480
|
+
total() {
|
|
481
|
+
return {
|
|
482
|
+
input: this.input,
|
|
483
|
+
output: this.output,
|
|
484
|
+
cacheRead: this.cacheRead,
|
|
485
|
+
cacheWrite: this.cacheWrite
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
estimateCost() {
|
|
489
|
+
return {
|
|
490
|
+
input: round4(this.costInput),
|
|
491
|
+
output: round4(this.costOutput),
|
|
492
|
+
total: round4(this.costInput + this.costOutput),
|
|
493
|
+
currency: "USD"
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
cacheStats() {
|
|
497
|
+
const denom = this.cacheRead + this.input;
|
|
498
|
+
return {
|
|
499
|
+
readTokens: this.cacheRead,
|
|
500
|
+
writeTokens: this.cacheWrite,
|
|
501
|
+
hitRatio: denom === 0 ? 0 : this.cacheRead / denom
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/** Invalidate cached prices so the next account() call fetches fresh data. */
|
|
505
|
+
invalidateCache() {
|
|
506
|
+
this.priceCache.clear();
|
|
507
|
+
}
|
|
508
|
+
reset() {
|
|
509
|
+
this.input = 0;
|
|
510
|
+
this.output = 0;
|
|
511
|
+
this.cacheRead = 0;
|
|
512
|
+
this.cacheWrite = 0;
|
|
513
|
+
this.costInput = 0;
|
|
514
|
+
this.costOutput = 0;
|
|
515
|
+
}
|
|
516
|
+
applyPrice(usage, price) {
|
|
517
|
+
if (price.input) this.costInput += usage.input / 1e6 * price.input;
|
|
518
|
+
if (price.output) this.costOutput += usage.output / 1e6 * price.output;
|
|
519
|
+
if (usage.cacheRead && price.cacheRead) {
|
|
520
|
+
this.costInput += usage.cacheRead / 1e6 * price.cacheRead;
|
|
521
|
+
}
|
|
522
|
+
if (usage.cacheWrite && price.cacheWrite) {
|
|
523
|
+
this.costInput += usage.cacheWrite / 1e6 * price.cacheWrite;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
function priceFromModel(m) {
|
|
528
|
+
return {
|
|
529
|
+
input: m.cost?.input,
|
|
530
|
+
output: m.cost?.output,
|
|
531
|
+
cacheRead: m.cost?.cache_read,
|
|
532
|
+
cacheWrite: m.cost?.cache_write
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function round4(n) {
|
|
536
|
+
return Math.round(n * 1e4) / 1e4;
|
|
537
|
+
}
|
|
538
|
+
async function atomicWrite(targetPath, content, opts = {}) {
|
|
539
|
+
const dir = path2.dirname(targetPath);
|
|
540
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
541
|
+
const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
542
|
+
try {
|
|
543
|
+
if (typeof content === "string") {
|
|
544
|
+
await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
545
|
+
} else {
|
|
546
|
+
await fsp.writeFile(tmp, content, { flag: "wx" });
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const fh = await fsp.open(tmp, "r+");
|
|
550
|
+
await fh.sync();
|
|
551
|
+
await fh.close();
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
let mode;
|
|
555
|
+
try {
|
|
556
|
+
const stat4 = await fsp.stat(targetPath);
|
|
557
|
+
mode = stat4.mode & 511;
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
if (mode !== void 0) {
|
|
561
|
+
await fsp.chmod(tmp, mode);
|
|
562
|
+
}
|
|
563
|
+
await fsp.rename(tmp, targetPath);
|
|
564
|
+
} catch (err) {
|
|
565
|
+
try {
|
|
566
|
+
await fsp.unlink(tmp);
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function ensureDir(dir) {
|
|
573
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/defaults/session-store.ts
|
|
577
|
+
var DefaultSessionStore = class {
|
|
578
|
+
dir;
|
|
579
|
+
events;
|
|
580
|
+
constructor(opts) {
|
|
581
|
+
this.dir = opts.dir;
|
|
582
|
+
this.events = opts.events;
|
|
583
|
+
}
|
|
584
|
+
async create(meta) {
|
|
585
|
+
await ensureDir(this.dir);
|
|
586
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
587
|
+
const id = meta.id ?? `${startedAt.replace(/[:.]/g, "-")}-${randomBytes(2).toString("hex")}`;
|
|
588
|
+
const file = path2.join(this.dir, `${id}.jsonl`);
|
|
589
|
+
let handle;
|
|
590
|
+
try {
|
|
591
|
+
handle = await fsp.open(file, "a", 384);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
|
|
597
|
+
} catch (err) {
|
|
598
|
+
await handle.close().catch(() => {
|
|
599
|
+
});
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async resume(id) {
|
|
604
|
+
const data = await this.load(id);
|
|
605
|
+
const file = path2.join(this.dir, `${id}.jsonl`);
|
|
606
|
+
let handle;
|
|
607
|
+
try {
|
|
608
|
+
handle = await fsp.open(file, "a", 384);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const writer = new FileSessionWriter(
|
|
615
|
+
id,
|
|
616
|
+
handle,
|
|
617
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
618
|
+
{
|
|
619
|
+
id,
|
|
620
|
+
model: data.metadata.model,
|
|
621
|
+
provider: data.metadata.provider
|
|
622
|
+
},
|
|
623
|
+
{ resumed: true, dir: this.dir, filePath: file }
|
|
624
|
+
);
|
|
625
|
+
return { writer, data };
|
|
626
|
+
}
|
|
627
|
+
async load(id) {
|
|
628
|
+
const file = path2.join(this.dir, `${id}.jsonl`);
|
|
629
|
+
const raw = await fsp.readFile(file, "utf8");
|
|
630
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
631
|
+
const events = [];
|
|
632
|
+
for (const line of lines) {
|
|
633
|
+
try {
|
|
634
|
+
events.push(JSON.parse(line));
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const meta = this.metaFromEvents(id, events);
|
|
639
|
+
const { messages, usage } = this.replay(events, id);
|
|
640
|
+
return { metadata: meta, events, messages, usage };
|
|
641
|
+
}
|
|
642
|
+
async list(limit = 20) {
|
|
643
|
+
try {
|
|
644
|
+
await ensureDir(this.dir);
|
|
645
|
+
const files = await fsp.readdir(this.dir);
|
|
646
|
+
const ids = files.filter((f) => f.endsWith(".jsonl")).map((f) => f.replace(/\.jsonl$/, ""));
|
|
647
|
+
const sessions = await Promise.all(
|
|
648
|
+
ids.map((id) => this.summaryFor(id).catch(() => null))
|
|
649
|
+
);
|
|
650
|
+
const out = sessions.filter((s) => s !== null);
|
|
651
|
+
out.sort((a, b) => a.startedAt < b.startedAt ? 1 : -1);
|
|
652
|
+
return out.slice(0, limit);
|
|
653
|
+
} catch {
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async summaryFor(id) {
|
|
658
|
+
const manifest = path2.join(this.dir, `${id}.summary.json`);
|
|
659
|
+
try {
|
|
660
|
+
const raw = await fsp.readFile(manifest, "utf8");
|
|
661
|
+
return JSON.parse(raw);
|
|
662
|
+
} catch {
|
|
663
|
+
const full = path2.join(this.dir, `${id}.jsonl`);
|
|
664
|
+
const stat4 = await fsp.stat(full);
|
|
665
|
+
const summary = await this.summarize(id, stat4.mtime.toISOString());
|
|
666
|
+
await fsp.writeFile(manifest, JSON.stringify(summary), { mode: 384 }).catch(() => void 0);
|
|
667
|
+
return summary;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async delete(id) {
|
|
671
|
+
await fsp.unlink(path2.join(this.dir, `${id}.jsonl`));
|
|
672
|
+
await fsp.unlink(path2.join(this.dir, `${id}.summary.json`)).catch(() => void 0);
|
|
673
|
+
}
|
|
674
|
+
async summarize(id, mtime) {
|
|
675
|
+
try {
|
|
676
|
+
const data = await this.load(id);
|
|
677
|
+
const firstUser = data.events.find((e) => e.type === "user_input");
|
|
678
|
+
const title = firstUser && firstUser.type === "user_input" ? userInputTitle(firstUser.content) : "(empty session)";
|
|
679
|
+
return {
|
|
680
|
+
id,
|
|
681
|
+
title,
|
|
682
|
+
startedAt: data.metadata.startedAt,
|
|
683
|
+
model: data.metadata.model ?? "unknown",
|
|
684
|
+
provider: data.metadata.provider ?? "unknown",
|
|
685
|
+
tokenTotal: data.usage.input + data.usage.output
|
|
686
|
+
};
|
|
687
|
+
} catch {
|
|
688
|
+
return {
|
|
689
|
+
id,
|
|
690
|
+
title: "(damaged)",
|
|
691
|
+
startedAt: mtime,
|
|
692
|
+
model: "unknown",
|
|
693
|
+
provider: "unknown",
|
|
694
|
+
tokenTotal: 0
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
metaFromEvents(id, events) {
|
|
699
|
+
const start = events.find((e) => e.type === "session_start");
|
|
700
|
+
const end = events.find((e) => e.type === "session_end");
|
|
701
|
+
return {
|
|
702
|
+
id,
|
|
703
|
+
startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
704
|
+
endedAt: end?.ts,
|
|
705
|
+
model: start?.model,
|
|
706
|
+
provider: start?.provider
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
replay(events, sessionId = "unknown") {
|
|
710
|
+
const messages = [];
|
|
711
|
+
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
712
|
+
const openToolUses = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const e of events) {
|
|
714
|
+
if (e.type === "user_input") {
|
|
715
|
+
openToolUses.clear();
|
|
716
|
+
messages.push({ role: "user", content: e.content });
|
|
717
|
+
} else if (e.type === "llm_response") {
|
|
718
|
+
messages.push({ role: "assistant", content: e.content });
|
|
719
|
+
for (const b of e.content) {
|
|
720
|
+
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
721
|
+
}
|
|
722
|
+
usage = {
|
|
723
|
+
input: usage.input + (e.usage.input ?? 0),
|
|
724
|
+
output: usage.output + (e.usage.output ?? 0),
|
|
725
|
+
cacheRead: (usage.cacheRead ?? 0) + (e.usage.cacheRead ?? 0),
|
|
726
|
+
cacheWrite: (usage.cacheWrite ?? 0) + (e.usage.cacheWrite ?? 0)
|
|
727
|
+
};
|
|
728
|
+
} else if (e.type === "tool_result") {
|
|
729
|
+
if (!openToolUses.has(e.id)) {
|
|
730
|
+
this.events?.emit("session.damaged", {
|
|
731
|
+
sessionId,
|
|
732
|
+
detail: `Orphan tool_result "${e.id}" has no matching tool_use`
|
|
733
|
+
});
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
openToolUses.delete(e.id);
|
|
737
|
+
const content = [
|
|
738
|
+
{
|
|
739
|
+
type: "tool_result",
|
|
740
|
+
tool_use_id: e.id,
|
|
741
|
+
content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
|
|
742
|
+
is_error: e.isError
|
|
743
|
+
}
|
|
744
|
+
];
|
|
745
|
+
const last = messages[messages.length - 1];
|
|
746
|
+
if (last && last.role === "user") {
|
|
747
|
+
if (Array.isArray(last.content)) {
|
|
748
|
+
last.content.push(...content);
|
|
749
|
+
} else if (typeof last.content === "string") {
|
|
750
|
+
last.content = [{ type: "text", text: last.content }, ...content];
|
|
751
|
+
} else {
|
|
752
|
+
messages.push({ role: "user", content });
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
messages.push({ role: "user", content });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (openToolUses.size > 0) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Session damaged: ${openToolUses.size} tool_use blocks without matching results`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return { messages, usage };
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
var FileSessionWriter = class {
|
|
768
|
+
constructor(id, handle, startedAt, meta, opts = {}) {
|
|
769
|
+
this.id = id;
|
|
770
|
+
this.handle = handle;
|
|
771
|
+
this.startedAt = startedAt;
|
|
772
|
+
this.meta = meta;
|
|
773
|
+
this.resumed = opts.resumed ?? false;
|
|
774
|
+
this.manifestFile = opts.dir ? path2.join(opts.dir, `${id}.summary.json`) : "";
|
|
775
|
+
this.filePath = opts.filePath ?? "";
|
|
776
|
+
this.summary = {
|
|
777
|
+
id,
|
|
778
|
+
title: "(empty session)",
|
|
779
|
+
startedAt,
|
|
780
|
+
model: meta.model ?? "unknown",
|
|
781
|
+
provider: meta.provider ?? "unknown",
|
|
782
|
+
tokenTotal: 0
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
id;
|
|
786
|
+
handle;
|
|
787
|
+
startedAt;
|
|
788
|
+
meta;
|
|
789
|
+
closed = false;
|
|
790
|
+
manifestFile;
|
|
791
|
+
summary;
|
|
792
|
+
tokenIn = 0;
|
|
793
|
+
tokenOut = 0;
|
|
794
|
+
filePath;
|
|
795
|
+
initDone = false;
|
|
796
|
+
resumed;
|
|
797
|
+
async writeSessionStart() {
|
|
798
|
+
if (this.initDone || this.closed) return;
|
|
799
|
+
this.initDone = true;
|
|
800
|
+
const record = `${JSON.stringify({
|
|
801
|
+
type: this.resumed ? "session_resumed" : "session_start",
|
|
802
|
+
ts: this.startedAt,
|
|
803
|
+
id: this.id,
|
|
804
|
+
model: this.meta.model ?? "unknown",
|
|
805
|
+
provider: this.meta.provider ?? "unknown"
|
|
806
|
+
})}
|
|
807
|
+
`;
|
|
808
|
+
try {
|
|
809
|
+
if (this.filePath) {
|
|
810
|
+
await fsp.writeFile(this.filePath, record, { flag: "a", mode: 384 });
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async append(event) {
|
|
816
|
+
if (this.closed) return;
|
|
817
|
+
if (!this.initDone) {
|
|
818
|
+
await this.writeSessionStart();
|
|
819
|
+
}
|
|
820
|
+
this.observeForSummary(event);
|
|
821
|
+
try {
|
|
822
|
+
await this.handle.appendFile(`${JSON.stringify(event)}
|
|
823
|
+
`, "utf8");
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.warn("[session] append failed:", err instanceof Error ? err.message : String(err));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Watch events as they're appended and keep the summary state hot, so
|
|
830
|
+
* `close()` can flush a `<id>.summary.json` manifest without re-reading
|
|
831
|
+
* the JSONL. `list()` reads only manifests, turning a per-session full
|
|
832
|
+
* parse into a single stat+read.
|
|
833
|
+
*/
|
|
834
|
+
observeForSummary(event) {
|
|
835
|
+
if (event.type === "user_input" && this.summary.title === "(empty session)") {
|
|
836
|
+
this.summary = { ...this.summary, title: userInputTitle(event.content) };
|
|
837
|
+
} else if (event.type === "llm_response") {
|
|
838
|
+
this.tokenIn += event.usage.input;
|
|
839
|
+
this.tokenOut += event.usage.output;
|
|
840
|
+
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
841
|
+
} else if (event.type === "session_end") {
|
|
842
|
+
const total = event.usage.input + event.usage.output;
|
|
843
|
+
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async close() {
|
|
847
|
+
if (this.closed) return;
|
|
848
|
+
this.closed = true;
|
|
849
|
+
if (this.manifestFile) {
|
|
850
|
+
try {
|
|
851
|
+
await fsp.writeFile(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
await this.handle.close();
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
function userInputTitle(content) {
|
|
862
|
+
if (typeof content === "string") return content.slice(0, 60);
|
|
863
|
+
const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
864
|
+
return (text || "(non-text input)").slice(0, 60);
|
|
865
|
+
}
|
|
866
|
+
var LOCK_FILE = "active.json";
|
|
867
|
+
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
868
|
+
var RecoveryLock = class {
|
|
869
|
+
file;
|
|
870
|
+
pid;
|
|
871
|
+
hostname;
|
|
872
|
+
maxAgeMs;
|
|
873
|
+
sessionStore;
|
|
874
|
+
probe;
|
|
875
|
+
constructor(opts) {
|
|
876
|
+
this.file = path2.join(opts.dir, LOCK_FILE);
|
|
877
|
+
this.pid = opts.pid ?? process.pid;
|
|
878
|
+
this.hostname = opts.hostname ?? os.hostname();
|
|
879
|
+
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
880
|
+
this.sessionStore = opts.sessionStore;
|
|
881
|
+
this.probe = opts.isPidAlive ?? defaultIsPidAlive;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Examine the lockfile and decide whether it represents an abandoned
|
|
885
|
+
* session. Returns `null` if the file is missing, points to a live
|
|
886
|
+
* instance, references a clean-closed session, is too old, or is
|
|
887
|
+
* malformed. Otherwise returns enough detail to prompt the user.
|
|
888
|
+
*
|
|
889
|
+
* Important: this is a read-only check. We never delete an active
|
|
890
|
+
* lock from here — if another wstack instance is alive, the caller
|
|
891
|
+
* should bail or run with a fresh session instead.
|
|
892
|
+
*/
|
|
893
|
+
async checkAbandoned() {
|
|
894
|
+
const lock = await this.readLock();
|
|
895
|
+
if (!lock) return null;
|
|
896
|
+
const ageMs = Date.now() - new Date(lock.startedAt).getTime();
|
|
897
|
+
if (Number.isNaN(ageMs) || ageMs < 0) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
if (ageMs > this.maxAgeMs) return null;
|
|
901
|
+
if (lock.hostname === this.hostname && this.probe(lock.pid)) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
let messageCount = 0;
|
|
905
|
+
if (this.sessionStore) {
|
|
906
|
+
try {
|
|
907
|
+
const data = await this.sessionStore.load(lock.sessionId);
|
|
908
|
+
const closed = data.events.some((e) => e.type === "session_end");
|
|
909
|
+
if (closed) return null;
|
|
910
|
+
messageCount = data.messages.length;
|
|
911
|
+
} catch {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
sessionId: lock.sessionId,
|
|
917
|
+
pid: lock.pid,
|
|
918
|
+
startedAt: lock.startedAt,
|
|
919
|
+
ageMs,
|
|
920
|
+
messageCount
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Claim the lock for the given session. Overwrites any existing lock
|
|
925
|
+
* — the caller should have already handled abandonment (via
|
|
926
|
+
* `checkAbandoned`) before calling this.
|
|
927
|
+
*/
|
|
928
|
+
async write(sessionId) {
|
|
929
|
+
await ensureDir(path2.dirname(this.file));
|
|
930
|
+
const lock = {
|
|
931
|
+
v: 1,
|
|
932
|
+
sessionId,
|
|
933
|
+
pid: this.pid,
|
|
934
|
+
hostname: this.hostname,
|
|
935
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
936
|
+
};
|
|
937
|
+
const tmp = `${this.file}.tmp`;
|
|
938
|
+
await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
|
|
939
|
+
await fsp.rename(tmp, this.file);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Release the lock. Idempotent — silently succeeds if the file is
|
|
943
|
+
* already gone (e.g. someone else cleared it, or the directory was
|
|
944
|
+
* wiped).
|
|
945
|
+
*/
|
|
946
|
+
async clear() {
|
|
947
|
+
try {
|
|
948
|
+
await fsp.unlink(this.file);
|
|
949
|
+
} catch (err) {
|
|
950
|
+
const code = err.code;
|
|
951
|
+
if (code === "ENOENT") return;
|
|
952
|
+
throw err;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async readLock() {
|
|
956
|
+
let raw;
|
|
957
|
+
try {
|
|
958
|
+
raw = await fsp.readFile(this.file, "utf8");
|
|
959
|
+
} catch (err) {
|
|
960
|
+
const code = err.code;
|
|
961
|
+
if (code === "ENOENT") return null;
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const parsed = JSON.parse(raw);
|
|
966
|
+
if (!isLockFile(parsed)) return null;
|
|
967
|
+
return parsed;
|
|
968
|
+
} catch {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
function isLockFile(v) {
|
|
974
|
+
if (typeof v !== "object" || v === null) return false;
|
|
975
|
+
const o = v;
|
|
976
|
+
return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
|
|
977
|
+
}
|
|
978
|
+
function defaultIsPidAlive(pid) {
|
|
979
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
980
|
+
try {
|
|
981
|
+
process.kill(pid, 0);
|
|
982
|
+
return true;
|
|
983
|
+
} catch (err) {
|
|
984
|
+
const code = err.code;
|
|
985
|
+
if (code === "EPERM") return true;
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
var QueueStore = class {
|
|
990
|
+
file;
|
|
991
|
+
constructor(opts) {
|
|
992
|
+
this.file = path2.join(opts.dir, "queue.json");
|
|
993
|
+
}
|
|
994
|
+
async write(items) {
|
|
995
|
+
if (items.length === 0) {
|
|
996
|
+
await this.clear();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
await atomicWrite(this.file, JSON.stringify(items), { mode: 384 });
|
|
1000
|
+
}
|
|
1001
|
+
async read() {
|
|
1002
|
+
let raw;
|
|
1003
|
+
try {
|
|
1004
|
+
raw = await fsp.readFile(this.file, "utf8");
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
const code = err.code;
|
|
1007
|
+
if (code === "ENOENT") return [];
|
|
1008
|
+
return [];
|
|
1009
|
+
}
|
|
1010
|
+
let parsed;
|
|
1011
|
+
try {
|
|
1012
|
+
parsed = JSON.parse(raw);
|
|
1013
|
+
} catch {
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
if (!Array.isArray(parsed)) return [];
|
|
1017
|
+
const out = [];
|
|
1018
|
+
for (const v of parsed) {
|
|
1019
|
+
if (isPersistedQueueItem(v)) out.push(v);
|
|
1020
|
+
}
|
|
1021
|
+
return out;
|
|
1022
|
+
}
|
|
1023
|
+
async clear() {
|
|
1024
|
+
try {
|
|
1025
|
+
await fsp.unlink(this.file);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
const code = err.code;
|
|
1028
|
+
if (code === "ENOENT") return;
|
|
1029
|
+
console.warn(`QueueStore.clear() failed for ${this.file}: ${err.message}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
function isPersistedQueueItem(v) {
|
|
1034
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1035
|
+
const o = v;
|
|
1036
|
+
return typeof o["displayText"] === "string" && Array.isArray(o["blocks"]);
|
|
1037
|
+
}
|
|
1038
|
+
var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
|
|
1039
|
+
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)\]/g;
|
|
1040
|
+
var DefaultAttachmentStore = class {
|
|
1041
|
+
items = /* @__PURE__ */ new Map();
|
|
1042
|
+
refs = [];
|
|
1043
|
+
nextSeq = { text: 0, image: 0, file: 0 };
|
|
1044
|
+
spoolDir;
|
|
1045
|
+
spoolThreshold;
|
|
1046
|
+
constructor(opts = {}) {
|
|
1047
|
+
this.spoolDir = opts.spoolDir;
|
|
1048
|
+
this.spoolThreshold = opts.spoolThresholdBytes ?? DEFAULT_SPOOL_THRESHOLD;
|
|
1049
|
+
}
|
|
1050
|
+
async add(input) {
|
|
1051
|
+
const seq = ++this.nextSeq[input.kind];
|
|
1052
|
+
const id = `${kindPrefix(input.kind)}-${seq}-${randomBytes(3).toString("hex")}`;
|
|
1053
|
+
const bytes = Buffer.byteLength(input.data, input.kind === "image" ? "base64" : "utf8");
|
|
1054
|
+
let spooledPath;
|
|
1055
|
+
let data = input.data;
|
|
1056
|
+
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
1057
|
+
await fsp.mkdir(this.spoolDir, { recursive: true });
|
|
1058
|
+
spooledPath = path2.join(this.spoolDir, `${id}.bin`);
|
|
1059
|
+
await fsp.writeFile(spooledPath, input.data, input.kind === "image" ? "base64" : "utf8");
|
|
1060
|
+
data = void 0;
|
|
1061
|
+
}
|
|
1062
|
+
const att = {
|
|
1063
|
+
id,
|
|
1064
|
+
kind: input.kind,
|
|
1065
|
+
meta: input.meta ?? {},
|
|
1066
|
+
data,
|
|
1067
|
+
path: spooledPath,
|
|
1068
|
+
bytes,
|
|
1069
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1070
|
+
};
|
|
1071
|
+
this.items.set(id, att);
|
|
1072
|
+
const ref = { id, kind: input.kind, seq, meta: att.meta };
|
|
1073
|
+
this.refs.push(ref);
|
|
1074
|
+
return ref;
|
|
1075
|
+
}
|
|
1076
|
+
async get(id) {
|
|
1077
|
+
return this.items.get(id);
|
|
1078
|
+
}
|
|
1079
|
+
list() {
|
|
1080
|
+
return [...this.refs];
|
|
1081
|
+
}
|
|
1082
|
+
async expand(text) {
|
|
1083
|
+
const matches = [...text.matchAll(PLACEHOLDER_RE)];
|
|
1084
|
+
if (matches.length === 0) return text ? [{ type: "text", text }] : [];
|
|
1085
|
+
const blocks = [];
|
|
1086
|
+
let lastIndex = 0;
|
|
1087
|
+
for (const m of matches) {
|
|
1088
|
+
const idx = m.index ?? 0;
|
|
1089
|
+
const before = text.slice(lastIndex, idx);
|
|
1090
|
+
if (before) blocks.push({ type: "text", text: before });
|
|
1091
|
+
const kind = prefixToKind(m[1]);
|
|
1092
|
+
const seq = Number(m[2]);
|
|
1093
|
+
const ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
|
|
1094
|
+
const att = ref ? this.items.get(ref.id) : void 0;
|
|
1095
|
+
if (!att) {
|
|
1096
|
+
blocks.push({ type: "text", text: m[0] });
|
|
1097
|
+
} else {
|
|
1098
|
+
blocks.push(await this.toBlock(att));
|
|
1099
|
+
}
|
|
1100
|
+
lastIndex = idx + m[0].length;
|
|
1101
|
+
}
|
|
1102
|
+
const tail = text.slice(lastIndex);
|
|
1103
|
+
if (tail) blocks.push({ type: "text", text: tail });
|
|
1104
|
+
return mergeAdjacentText(blocks);
|
|
1105
|
+
}
|
|
1106
|
+
async clear() {
|
|
1107
|
+
this.items.clear();
|
|
1108
|
+
this.refs.length = 0;
|
|
1109
|
+
this.nextSeq = { text: 0, image: 0, file: 0 };
|
|
1110
|
+
}
|
|
1111
|
+
async toBlock(att) {
|
|
1112
|
+
if (att.kind === "image") {
|
|
1113
|
+
const data = att.data ?? (att.path ? await fsp.readFile(att.path, { encoding: "base64" }) : "");
|
|
1114
|
+
return {
|
|
1115
|
+
type: "image",
|
|
1116
|
+
source: {
|
|
1117
|
+
type: "base64",
|
|
1118
|
+
media_type: att.meta.mediaType ?? "image/png",
|
|
1119
|
+
data
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
const raw = att.data ?? (att.path ? await fsp.readFile(att.path, "utf8") : "");
|
|
1124
|
+
const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
|
|
1125
|
+
const close = att.meta.filename ? "</file>" : "</pasted>";
|
|
1126
|
+
return { type: "text", text: `${label}
|
|
1127
|
+
${raw}
|
|
1128
|
+
${close}` };
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
function kindPrefix(kind) {
|
|
1132
|
+
return kind === "text" ? "pasted" : kind;
|
|
1133
|
+
}
|
|
1134
|
+
function prefixToKind(prefix) {
|
|
1135
|
+
if (prefix === "pasted") return "text";
|
|
1136
|
+
if (prefix === "image") return "image";
|
|
1137
|
+
return "file";
|
|
1138
|
+
}
|
|
1139
|
+
function mergeAdjacentText(blocks) {
|
|
1140
|
+
const out = [];
|
|
1141
|
+
for (const b of blocks) {
|
|
1142
|
+
const prev = out[out.length - 1];
|
|
1143
|
+
if (b.type === "text" && prev && prev.type === "text") {
|
|
1144
|
+
prev.text += b.text;
|
|
1145
|
+
} else {
|
|
1146
|
+
out.push(b);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return out;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/types/secret-vault.ts
|
|
1153
|
+
var ENCRYPTED_PREFIX = "enc:v1:";
|
|
1154
|
+
|
|
1155
|
+
// src/defaults/secret-vault.ts
|
|
1156
|
+
var KEY_BYTES = 32;
|
|
1157
|
+
var IV_BYTES = 12;
|
|
1158
|
+
var TAG_BYTES = 16;
|
|
1159
|
+
var ALGO = "aes-256-gcm";
|
|
1160
|
+
var DefaultSecretVault = class {
|
|
1161
|
+
keyFile;
|
|
1162
|
+
key;
|
|
1163
|
+
constructor(opts) {
|
|
1164
|
+
this.keyFile = opts.keyFile;
|
|
1165
|
+
}
|
|
1166
|
+
isEncrypted(value) {
|
|
1167
|
+
return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
|
|
1168
|
+
}
|
|
1169
|
+
encrypt(plaintext) {
|
|
1170
|
+
if (this.isEncrypted(plaintext)) return plaintext;
|
|
1171
|
+
const key = this.loadOrCreateKey();
|
|
1172
|
+
const iv = randomBytes(IV_BYTES);
|
|
1173
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
1174
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1175
|
+
const tag = cipher.getAuthTag();
|
|
1176
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
1177
|
+
}
|
|
1178
|
+
decrypt(value) {
|
|
1179
|
+
if (!this.isEncrypted(value)) return value;
|
|
1180
|
+
const rest = value.slice(ENCRYPTED_PREFIX.length);
|
|
1181
|
+
const parts = rest.split(":");
|
|
1182
|
+
if (parts.length !== 3) {
|
|
1183
|
+
throw new Error("SecretVault: malformed encrypted value");
|
|
1184
|
+
}
|
|
1185
|
+
const [ivB64, tagB64, ctB64] = parts;
|
|
1186
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
1187
|
+
const tag = Buffer.from(tagB64, "base64");
|
|
1188
|
+
const ct = Buffer.from(ctB64, "base64");
|
|
1189
|
+
if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
|
|
1190
|
+
if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
|
|
1191
|
+
const key = this.loadOrCreateKey();
|
|
1192
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
1193
|
+
decipher.setAuthTag(tag);
|
|
1194
|
+
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
1195
|
+
return pt.toString("utf8");
|
|
1196
|
+
}
|
|
1197
|
+
loadOrCreateKey() {
|
|
1198
|
+
if (this.key) return this.key;
|
|
1199
|
+
try {
|
|
1200
|
+
const buf = fs4.readFileSync(this.keyFile);
|
|
1201
|
+
if (buf.length !== KEY_BYTES) {
|
|
1202
|
+
throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
|
|
1203
|
+
}
|
|
1204
|
+
this.key = buf;
|
|
1205
|
+
return this.key;
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
if (err.code !== "ENOENT") throw err;
|
|
1208
|
+
}
|
|
1209
|
+
fs4.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
|
|
1210
|
+
const key = randomBytes(KEY_BYTES);
|
|
1211
|
+
try {
|
|
1212
|
+
fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
if (err.code !== "EEXIST") throw err;
|
|
1215
|
+
const buf = fs4.readFileSync(this.keyFile);
|
|
1216
|
+
if (buf.length !== KEY_BYTES) {
|
|
1217
|
+
throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
|
|
1218
|
+
}
|
|
1219
|
+
this.key = buf;
|
|
1220
|
+
return this.key;
|
|
1221
|
+
}
|
|
1222
|
+
this.key = key;
|
|
1223
|
+
return key;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
function decryptConfigSecrets(cfg, vault) {
|
|
1227
|
+
return walk(cfg, vault, (v) => vault.decrypt(v));
|
|
1228
|
+
}
|
|
1229
|
+
function encryptConfigSecrets(cfg, vault) {
|
|
1230
|
+
return walk(cfg, vault, (v) => vault.encrypt(v));
|
|
1231
|
+
}
|
|
1232
|
+
function walk(node, vault, transform) {
|
|
1233
|
+
if (node === null || node === void 0) return node;
|
|
1234
|
+
if (typeof node !== "object") return node;
|
|
1235
|
+
if (Array.isArray(node)) {
|
|
1236
|
+
return node.map((item) => walk(item, vault, transform));
|
|
1237
|
+
}
|
|
1238
|
+
const out = {};
|
|
1239
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1240
|
+
if (typeof v === "string" && isSecretField(k)) {
|
|
1241
|
+
out[k] = transform(v);
|
|
1242
|
+
} else if (typeof v === "object" && v !== null) {
|
|
1243
|
+
out[k] = walk(v, vault, transform);
|
|
1244
|
+
} else {
|
|
1245
|
+
out[k] = v;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return out;
|
|
1249
|
+
}
|
|
1250
|
+
var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key)/i;
|
|
1251
|
+
var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
|
|
1252
|
+
function isSecretField(name) {
|
|
1253
|
+
const lc = name.toLowerCase();
|
|
1254
|
+
if (NON_SECRET_OVERRIDES.has(lc)) return false;
|
|
1255
|
+
return SECRET_KEY_PATTERN.test(lc);
|
|
1256
|
+
}
|
|
1257
|
+
async function rewriteConfigEncrypted(configPath, vault, patch) {
|
|
1258
|
+
let current = {};
|
|
1259
|
+
try {
|
|
1260
|
+
const raw = await fsp.readFile(configPath, "utf8");
|
|
1261
|
+
current = JSON.parse(raw);
|
|
1262
|
+
} catch {
|
|
1263
|
+
}
|
|
1264
|
+
const merged = deepMerge(current, patch ?? {});
|
|
1265
|
+
const encrypted = encryptConfigSecrets(merged, vault);
|
|
1266
|
+
await fsp.mkdir(path2.dirname(configPath), { recursive: true });
|
|
1267
|
+
await fsp.writeFile(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
1268
|
+
try {
|
|
1269
|
+
await fsp.chmod(configPath, 384);
|
|
1270
|
+
} catch {
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
async function migratePlaintextSecrets(configPath, vault) {
|
|
1274
|
+
let raw;
|
|
1275
|
+
try {
|
|
1276
|
+
raw = await fsp.readFile(configPath, "utf8");
|
|
1277
|
+
} catch {
|
|
1278
|
+
return { migrated: 0, file: configPath };
|
|
1279
|
+
}
|
|
1280
|
+
let parsed;
|
|
1281
|
+
try {
|
|
1282
|
+
parsed = JSON.parse(raw);
|
|
1283
|
+
} catch {
|
|
1284
|
+
return { migrated: 0, file: configPath };
|
|
1285
|
+
}
|
|
1286
|
+
const counter = { n: 0 };
|
|
1287
|
+
const migrated = walkCount(parsed, vault, counter);
|
|
1288
|
+
if (counter.n === 0) return { migrated: 0, file: configPath };
|
|
1289
|
+
await fsp.writeFile(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
|
|
1290
|
+
try {
|
|
1291
|
+
await fsp.chmod(configPath, 384);
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
return { migrated: counter.n, file: configPath };
|
|
1295
|
+
}
|
|
1296
|
+
function walkCount(node, vault, counter) {
|
|
1297
|
+
if (node === null || node === void 0) return node;
|
|
1298
|
+
if (typeof node !== "object") return node;
|
|
1299
|
+
if (Array.isArray(node)) {
|
|
1300
|
+
return node.map((item) => walkCount(item, vault, counter));
|
|
1301
|
+
}
|
|
1302
|
+
const out = {};
|
|
1303
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1304
|
+
if (typeof v === "string" && isSecretField(k) && !vault.isEncrypted(v) && v.length > 0) {
|
|
1305
|
+
out[k] = vault.encrypt(v);
|
|
1306
|
+
counter.n++;
|
|
1307
|
+
} else if (typeof v === "object" && v !== null) {
|
|
1308
|
+
out[k] = walkCount(v, vault, counter);
|
|
1309
|
+
} else {
|
|
1310
|
+
out[k] = v;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return out;
|
|
1314
|
+
}
|
|
1315
|
+
function deepMerge(a, b) {
|
|
1316
|
+
const out = { ...a };
|
|
1317
|
+
for (const [k, v] of Object.entries(b)) {
|
|
1318
|
+
const existing = out[k];
|
|
1319
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
1320
|
+
out[k] = deepMerge(existing, v);
|
|
1321
|
+
} else {
|
|
1322
|
+
out[k] = v;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return out;
|
|
1326
|
+
}
|
|
1327
|
+
var MAX_BYTES_TOTAL = 32e3;
|
|
1328
|
+
var DefaultMemoryStore = class {
|
|
1329
|
+
files;
|
|
1330
|
+
constructor(opts) {
|
|
1331
|
+
this.files = {
|
|
1332
|
+
"project-agents": opts.paths.inProjectAgentsFile,
|
|
1333
|
+
"project-memory": opts.paths.projectMemory,
|
|
1334
|
+
"user-memory": opts.paths.globalMemory
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
async readAll() {
|
|
1338
|
+
const parts = [];
|
|
1339
|
+
for (const scope of ["project-agents", "project-memory", "user-memory"]) {
|
|
1340
|
+
const body = await this.read(scope);
|
|
1341
|
+
if (body.trim()) parts.push(`## ${labelOf(scope)}
|
|
1342
|
+
|
|
1343
|
+
${body.trim()}`);
|
|
1344
|
+
}
|
|
1345
|
+
return parts.join("\n\n");
|
|
1346
|
+
}
|
|
1347
|
+
async read(scope) {
|
|
1348
|
+
try {
|
|
1349
|
+
return await fsp.readFile(this.files[scope], "utf8");
|
|
1350
|
+
} catch {
|
|
1351
|
+
return "";
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async remember(text, scope = "project-memory") {
|
|
1355
|
+
const file = this.files[scope];
|
|
1356
|
+
await ensureDir(path2.dirname(file));
|
|
1357
|
+
let existing = "";
|
|
1358
|
+
try {
|
|
1359
|
+
existing = await fsp.readFile(file, "utf8");
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1363
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1364
|
+
const entry = `
|
|
1365
|
+
- [${ts}] ${id} ${text.replace(/\n/g, " ")}
|
|
1366
|
+
`;
|
|
1367
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
1368
|
+
${entry}`;
|
|
1369
|
+
await atomicWrite(file, next);
|
|
1370
|
+
const buf = Buffer.byteLength(next, "utf8");
|
|
1371
|
+
if (buf > MAX_BYTES_TOTAL) {
|
|
1372
|
+
await this.consolidate(scope);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async forget(query, scope = "project-memory") {
|
|
1376
|
+
const file = this.files[scope];
|
|
1377
|
+
let existing;
|
|
1378
|
+
try {
|
|
1379
|
+
existing = await fsp.readFile(file, "utf8");
|
|
1380
|
+
} catch {
|
|
1381
|
+
return 0;
|
|
1382
|
+
}
|
|
1383
|
+
const needle = query.toLowerCase();
|
|
1384
|
+
const idMatcher = /mem_\d+_\w+/;
|
|
1385
|
+
let removed = 0;
|
|
1386
|
+
const lines = existing.split("\n").filter((line) => {
|
|
1387
|
+
const trimmed = line.trim();
|
|
1388
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
1389
|
+
if (idMatcher.test(query)) {
|
|
1390
|
+
const afterBracket = trimmed.indexOf("] ");
|
|
1391
|
+
if (afterBracket !== -1) {
|
|
1392
|
+
const afterTs = trimmed.slice(afterBracket + 2);
|
|
1393
|
+
const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
|
|
1394
|
+
if (entryIdMatch && entryIdMatch[0] === query) {
|
|
1395
|
+
removed++;
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (trimmed.toLowerCase().includes(needle)) {
|
|
1401
|
+
removed++;
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
return true;
|
|
1405
|
+
});
|
|
1406
|
+
if (removed > 0) {
|
|
1407
|
+
await atomicWrite(file, lines.join("\n"));
|
|
1408
|
+
}
|
|
1409
|
+
return removed;
|
|
1410
|
+
}
|
|
1411
|
+
async consolidate(scope) {
|
|
1412
|
+
const file = this.files[scope];
|
|
1413
|
+
let existing;
|
|
1414
|
+
try {
|
|
1415
|
+
existing = await fsp.readFile(file, "utf8");
|
|
1416
|
+
} catch {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1420
|
+
const lines = existing.split("\n").filter((line) => {
|
|
1421
|
+
const trimmed = line.trim();
|
|
1422
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
1423
|
+
const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
|
|
1424
|
+
if (seen.has(norm)) return false;
|
|
1425
|
+
seen.add(norm);
|
|
1426
|
+
return true;
|
|
1427
|
+
});
|
|
1428
|
+
const next = lines.join("\n");
|
|
1429
|
+
try {
|
|
1430
|
+
await atomicWrite(file, next);
|
|
1431
|
+
} catch {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const backup = `${file}.bak.${Date.now()}`;
|
|
1435
|
+
try {
|
|
1436
|
+
await fsp.copyFile(file, backup);
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
async clear(scope) {
|
|
1441
|
+
if (scope) {
|
|
1442
|
+
await atomicWrite(this.files[scope], "");
|
|
1443
|
+
} else {
|
|
1444
|
+
for (const s of ["project-agents", "project-memory", "user-memory"]) {
|
|
1445
|
+
await atomicWrite(this.files[s], "");
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
function labelOf(scope) {
|
|
1451
|
+
switch (scope) {
|
|
1452
|
+
case "project-agents":
|
|
1453
|
+
return "Project AGENTS.md";
|
|
1454
|
+
case "project-memory":
|
|
1455
|
+
return "Project memory";
|
|
1456
|
+
case "user-memory":
|
|
1457
|
+
return "User memory";
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/utils/glob-match.ts
|
|
1462
|
+
function escapeRegex(s) {
|
|
1463
|
+
return s.replace(/[.+^${}()|\\/]/g, "\\$&");
|
|
1464
|
+
}
|
|
1465
|
+
function compileGlob(pattern) {
|
|
1466
|
+
let i = 0;
|
|
1467
|
+
let re = "^";
|
|
1468
|
+
while (i < pattern.length) {
|
|
1469
|
+
const c = pattern[i];
|
|
1470
|
+
if (c === "*") {
|
|
1471
|
+
if (pattern[i + 1] === "*") {
|
|
1472
|
+
re += ".*";
|
|
1473
|
+
i += 2;
|
|
1474
|
+
if (pattern[i] === "/") i++;
|
|
1475
|
+
} else {
|
|
1476
|
+
re += "[^/]*";
|
|
1477
|
+
i++;
|
|
1478
|
+
}
|
|
1479
|
+
} else if (c === "?") {
|
|
1480
|
+
re += "[^/]";
|
|
1481
|
+
i++;
|
|
1482
|
+
} else if (c === "[") {
|
|
1483
|
+
let cls = "[";
|
|
1484
|
+
i++;
|
|
1485
|
+
if (pattern[i] === "!" || pattern[i] === "^") {
|
|
1486
|
+
cls += "^";
|
|
1487
|
+
i++;
|
|
1488
|
+
}
|
|
1489
|
+
while (i < pattern.length && pattern[i] !== "]") {
|
|
1490
|
+
const ch = pattern[i] ?? "";
|
|
1491
|
+
if (ch === "\\") {
|
|
1492
|
+
cls += "\\\\";
|
|
1493
|
+
} else if (ch === "]" || ch === "^") {
|
|
1494
|
+
cls += `\\${ch}`;
|
|
1495
|
+
} else {
|
|
1496
|
+
cls += ch;
|
|
1497
|
+
}
|
|
1498
|
+
i++;
|
|
1499
|
+
}
|
|
1500
|
+
cls += "]";
|
|
1501
|
+
re += cls;
|
|
1502
|
+
i++;
|
|
1503
|
+
} else {
|
|
1504
|
+
re += escapeRegex(c ?? "");
|
|
1505
|
+
i++;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
re += "$";
|
|
1509
|
+
return new RegExp(re);
|
|
1510
|
+
}
|
|
1511
|
+
function matchGlob(pattern, input) {
|
|
1512
|
+
return compileGlob(pattern).test(input);
|
|
1513
|
+
}
|
|
1514
|
+
function matchAny(patterns, input) {
|
|
1515
|
+
return patterns.some((p) => matchGlob(p, input));
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/utils/safe-json.ts
|
|
1519
|
+
function safeParse(input, maxBytes = 5e6) {
|
|
1520
|
+
if (input.length > maxBytes) {
|
|
1521
|
+
return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
return { ok: true, value: JSON.parse(input) };
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
return {
|
|
1527
|
+
ok: false,
|
|
1528
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/defaults/permission-policy.ts
|
|
1534
|
+
var DefaultPermissionPolicy = class {
|
|
1535
|
+
policy = {};
|
|
1536
|
+
loaded = false;
|
|
1537
|
+
trustFile;
|
|
1538
|
+
yolo;
|
|
1539
|
+
promptDelegate;
|
|
1540
|
+
constructor(opts) {
|
|
1541
|
+
this.trustFile = opts.trustFile;
|
|
1542
|
+
this.yolo = opts.yolo ?? false;
|
|
1543
|
+
this.promptDelegate = opts.promptDelegate;
|
|
1544
|
+
}
|
|
1545
|
+
async reload() {
|
|
1546
|
+
try {
|
|
1547
|
+
const raw = await fsp.readFile(this.trustFile, "utf8");
|
|
1548
|
+
const parsed = safeParse(raw);
|
|
1549
|
+
if (parsed.ok && parsed.value) this.policy = parsed.value;
|
|
1550
|
+
} catch {
|
|
1551
|
+
this.policy = {};
|
|
1552
|
+
}
|
|
1553
|
+
this.loaded = true;
|
|
1554
|
+
}
|
|
1555
|
+
async evaluate(tool, input, _ctx) {
|
|
1556
|
+
if (!this.loaded) await this.reload();
|
|
1557
|
+
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
1558
|
+
const entry = this.policy[tool.name] ?? namespaceEntry;
|
|
1559
|
+
const subject = this.subjectFor(tool.name, input);
|
|
1560
|
+
if (entry?.deny && subject && matchAny(entry.deny, subject)) {
|
|
1561
|
+
return { permission: "deny", source: "deny", reason: "matched deny pattern" };
|
|
1562
|
+
}
|
|
1563
|
+
if (tool.permission === "deny") {
|
|
1564
|
+
return { permission: "deny", source: "default", reason: "tool default deny" };
|
|
1565
|
+
}
|
|
1566
|
+
if (entry?.allow && subject && matchAny(entry.allow, subject)) {
|
|
1567
|
+
return { permission: "auto", source: "trust", reason: "matched allow pattern" };
|
|
1568
|
+
}
|
|
1569
|
+
if (entry?.auto) {
|
|
1570
|
+
return { permission: "auto", source: "trust" };
|
|
1571
|
+
}
|
|
1572
|
+
if (this.yolo) {
|
|
1573
|
+
return { permission: "auto", source: "yolo" };
|
|
1574
|
+
}
|
|
1575
|
+
if (tool.permission === "auto") {
|
|
1576
|
+
return { permission: "auto", source: "default" };
|
|
1577
|
+
}
|
|
1578
|
+
if (this.promptDelegate) {
|
|
1579
|
+
const decision = await this.promptDelegate(tool, input, subject ?? tool.name);
|
|
1580
|
+
if (decision === "always") {
|
|
1581
|
+
await this.trust({ tool: tool.name, pattern: subject ?? tool.name });
|
|
1582
|
+
return { permission: "auto", source: "user", reason: "user always-allowed" };
|
|
1583
|
+
}
|
|
1584
|
+
if (decision === "deny") {
|
|
1585
|
+
return { permission: "deny", source: "user", reason: "user denied" };
|
|
1586
|
+
}
|
|
1587
|
+
return { permission: decision === "yes" ? "auto" : "deny", source: "user" };
|
|
1588
|
+
}
|
|
1589
|
+
return { permission: "confirm", source: "default" };
|
|
1590
|
+
}
|
|
1591
|
+
async trust(rule) {
|
|
1592
|
+
if (!this.loaded) await this.reload();
|
|
1593
|
+
const entry = this.policy[rule.tool] ?? {};
|
|
1594
|
+
entry.allow = Array.from(/* @__PURE__ */ new Set([...entry.allow ?? [], rule.pattern]));
|
|
1595
|
+
this.policy[rule.tool] = entry;
|
|
1596
|
+
try {
|
|
1597
|
+
await atomicWrite(this.trustFile, JSON.stringify(this.policy, null, 2));
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
const existing = this.policy[rule.tool];
|
|
1600
|
+
if (existing?.allow) {
|
|
1601
|
+
const idx = existing.allow.indexOf(rule.pattern);
|
|
1602
|
+
if (idx !== -1) existing.allow.splice(idx, 1);
|
|
1603
|
+
}
|
|
1604
|
+
throw err;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
subjectFor(toolName, input) {
|
|
1608
|
+
if (!input || typeof input !== "object") return void 0;
|
|
1609
|
+
const obj = input;
|
|
1610
|
+
const globChars = /[*?\[\]]/g;
|
|
1611
|
+
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
1612
|
+
if (toolName === "bash" && typeof obj.command === "string") {
|
|
1613
|
+
return escapeGlob(obj.command);
|
|
1614
|
+
}
|
|
1615
|
+
if (typeof obj.path === "string") {
|
|
1616
|
+
return escapeGlob(obj.path.replace(/\\/g, "/"));
|
|
1617
|
+
}
|
|
1618
|
+
if (typeof obj.url === "string") {
|
|
1619
|
+
return escapeGlob(obj.url);
|
|
1620
|
+
}
|
|
1621
|
+
if (typeof obj.name === "string") {
|
|
1622
|
+
return escapeGlob(obj.name);
|
|
1623
|
+
}
|
|
1624
|
+
return void 0;
|
|
1625
|
+
}
|
|
1626
|
+
findNamespaceEntry(toolName) {
|
|
1627
|
+
for (const key of Object.keys(this.policy)) {
|
|
1628
|
+
if (key.includes("*") && matchGlob(key, toolName)) {
|
|
1629
|
+
return this.policy[key];
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return void 0;
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
var DefaultSkillLoader = class {
|
|
1636
|
+
dirs;
|
|
1637
|
+
cache;
|
|
1638
|
+
constructor(opts) {
|
|
1639
|
+
this.dirs = [
|
|
1640
|
+
{ dir: opts.paths.inProjectSkills, source: "project" },
|
|
1641
|
+
{ dir: opts.paths.globalSkills, source: "user" }
|
|
1642
|
+
];
|
|
1643
|
+
if (opts.bundledDir) {
|
|
1644
|
+
this.dirs.push({ dir: opts.bundledDir, source: "bundled" });
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
async list() {
|
|
1648
|
+
if (this.cache) return this.cache;
|
|
1649
|
+
const found = [];
|
|
1650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1651
|
+
for (const { dir, source } of this.dirs) {
|
|
1652
|
+
try {
|
|
1653
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1654
|
+
for (const e of entries) {
|
|
1655
|
+
if (!e.isDirectory()) continue;
|
|
1656
|
+
const skillFile = path2.join(dir, e.name, "SKILL.md");
|
|
1657
|
+
try {
|
|
1658
|
+
const raw = await fsp.readFile(skillFile, "utf8");
|
|
1659
|
+
const meta = parseFrontmatter(raw);
|
|
1660
|
+
if (!meta.name || !meta.description) continue;
|
|
1661
|
+
if (seen.has(meta.name)) continue;
|
|
1662
|
+
seen.add(meta.name);
|
|
1663
|
+
found.push({
|
|
1664
|
+
name: meta.name,
|
|
1665
|
+
description: meta.description,
|
|
1666
|
+
version: meta.version,
|
|
1667
|
+
path: skillFile,
|
|
1668
|
+
source
|
|
1669
|
+
});
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
} catch {
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
this.cache = found;
|
|
1677
|
+
return found;
|
|
1678
|
+
}
|
|
1679
|
+
async find(name) {
|
|
1680
|
+
const all = await this.list();
|
|
1681
|
+
return all.find((s) => s.name === name);
|
|
1682
|
+
}
|
|
1683
|
+
async manifestText() {
|
|
1684
|
+
const skills = await this.list();
|
|
1685
|
+
if (skills.length === 0) return "";
|
|
1686
|
+
const lines = ["## Available skills"];
|
|
1687
|
+
for (const s of skills) {
|
|
1688
|
+
lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
|
|
1689
|
+
lines.push(` Path: ${s.path}`);
|
|
1690
|
+
}
|
|
1691
|
+
return lines.join("\n");
|
|
1692
|
+
}
|
|
1693
|
+
async readBody(name) {
|
|
1694
|
+
const m = await this.find(name);
|
|
1695
|
+
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
1696
|
+
return fsp.readFile(m.path, "utf8");
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
function parseFrontmatter(raw) {
|
|
1700
|
+
if (!raw.startsWith("---")) return {};
|
|
1701
|
+
const end = raw.indexOf("\n---", 4);
|
|
1702
|
+
if (end === -1) return {};
|
|
1703
|
+
const block = raw.slice(4, end);
|
|
1704
|
+
const out = {};
|
|
1705
|
+
let key = null;
|
|
1706
|
+
let value = [];
|
|
1707
|
+
const flush = () => {
|
|
1708
|
+
if (key) {
|
|
1709
|
+
out[key] = value.join("\n").trim();
|
|
1710
|
+
}
|
|
1711
|
+
key = null;
|
|
1712
|
+
value = [];
|
|
1713
|
+
};
|
|
1714
|
+
for (const line of block.split("\n")) {
|
|
1715
|
+
const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
|
|
1716
|
+
if (m) {
|
|
1717
|
+
flush();
|
|
1718
|
+
key = m[1] ?? "";
|
|
1719
|
+
const pipe = m[2];
|
|
1720
|
+
const rest = m[3] ?? "";
|
|
1721
|
+
if (pipe === "|") {
|
|
1722
|
+
value = [];
|
|
1723
|
+
} else if (rest) {
|
|
1724
|
+
value = [rest];
|
|
1725
|
+
} else {
|
|
1726
|
+
value = [];
|
|
1727
|
+
}
|
|
1728
|
+
} else if (key) {
|
|
1729
|
+
value.push(line.replace(/^\s+/, ""));
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
flush();
|
|
1733
|
+
return out;
|
|
1734
|
+
}
|
|
1735
|
+
var BEHAVIOR_DEFAULTS = {
|
|
1736
|
+
version: 1,
|
|
1737
|
+
context: {
|
|
1738
|
+
warnThreshold: 0.6,
|
|
1739
|
+
softThreshold: 0.75,
|
|
1740
|
+
hardThreshold: 0.9,
|
|
1741
|
+
autoCompact: true,
|
|
1742
|
+
preserveK: 10,
|
|
1743
|
+
eliseThreshold: 2e3
|
|
1744
|
+
},
|
|
1745
|
+
tools: {
|
|
1746
|
+
defaultExecutionStrategy: "smart",
|
|
1747
|
+
maxIterations: 100,
|
|
1748
|
+
iterationTimeoutMs: 3e5,
|
|
1749
|
+
sessionTimeoutMs: 18e5,
|
|
1750
|
+
perIterationOutputCapBytes: 1e5,
|
|
1751
|
+
autoExtendLimit: true
|
|
1752
|
+
},
|
|
1753
|
+
log: { level: "info" },
|
|
1754
|
+
features: {
|
|
1755
|
+
mcp: true,
|
|
1756
|
+
plugins: true,
|
|
1757
|
+
memory: true,
|
|
1758
|
+
modelsRegistry: true,
|
|
1759
|
+
skills: true
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
var ENV_MAP = {
|
|
1763
|
+
WRONGSTACK_PROVIDER: (c, v) => {
|
|
1764
|
+
c.provider = v;
|
|
1765
|
+
},
|
|
1766
|
+
WRONGSTACK_MODEL: (c, v) => {
|
|
1767
|
+
c.model = v;
|
|
1768
|
+
},
|
|
1769
|
+
WRONGSTACK_API_KEY: (c, v) => {
|
|
1770
|
+
c.apiKey = v;
|
|
1771
|
+
},
|
|
1772
|
+
WRONGSTACK_BASE_URL: (c, v) => {
|
|
1773
|
+
c.baseUrl = v;
|
|
1774
|
+
},
|
|
1775
|
+
WRONGSTACK_LOG_LEVEL: (c, v) => {
|
|
1776
|
+
if (!c.log) c.log = { level: "info" };
|
|
1777
|
+
c.log.level = v;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
function isPrimitiveArray(a) {
|
|
1781
|
+
return a.every((v) => v === null || typeof v !== "object");
|
|
1782
|
+
}
|
|
1783
|
+
function deepMerge2(base, patch) {
|
|
1784
|
+
if (typeof base !== "object" || base === null) return patch ?? base;
|
|
1785
|
+
if (typeof patch !== "object" || patch === null) return base;
|
|
1786
|
+
const out = { ...base };
|
|
1787
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1788
|
+
const existing = out[k];
|
|
1789
|
+
if (Array.isArray(v)) {
|
|
1790
|
+
if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
|
|
1791
|
+
out[k] = [.../* @__PURE__ */ new Set([...existing, ...v])];
|
|
1792
|
+
} else {
|
|
1793
|
+
out[k] = v;
|
|
1794
|
+
if (process.env.WRONGSTACK_DEBUG_CONFIG) {
|
|
1795
|
+
console.warn(
|
|
1796
|
+
`[config] Non-primitive array for "${k}" replaced (global + local config merge). Global entries: ${existing?.length ?? 0}, local entries: ${v.length}.`
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} else if (typeof v === "object" && v !== null && typeof existing === "object" && existing !== null) {
|
|
1801
|
+
out[k] = deepMerge2(existing, v);
|
|
1802
|
+
} else if (v !== void 0) {
|
|
1803
|
+
out[k] = v;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return out;
|
|
1807
|
+
}
|
|
1808
|
+
var DefaultConfigLoader = class {
|
|
1809
|
+
paths;
|
|
1810
|
+
strict;
|
|
1811
|
+
vault;
|
|
1812
|
+
extraSources;
|
|
1813
|
+
constructor(opts) {
|
|
1814
|
+
this.paths = opts.paths;
|
|
1815
|
+
this.strict = opts.strict ?? false;
|
|
1816
|
+
this.vault = opts.vault;
|
|
1817
|
+
this.extraSources = opts.sources ?? [];
|
|
1818
|
+
}
|
|
1819
|
+
async load(opts = {}) {
|
|
1820
|
+
let cfg = { ...BEHAVIOR_DEFAULTS };
|
|
1821
|
+
const [global, local] = await Promise.all([
|
|
1822
|
+
this.readJson(this.paths.globalConfig),
|
|
1823
|
+
this.readJson(this.paths.projectLocalConfig)
|
|
1824
|
+
]);
|
|
1825
|
+
cfg = deepMerge2(cfg, global);
|
|
1826
|
+
cfg = deepMerge2(cfg, local);
|
|
1827
|
+
for (const [key, fn] of Object.entries(ENV_MAP)) {
|
|
1828
|
+
const v = process.env[key];
|
|
1829
|
+
if (v) fn(cfg, v);
|
|
1830
|
+
}
|
|
1831
|
+
const sorted = [...this.extraSources].sort((a, b) => {
|
|
1832
|
+
const pd = (a.priority ?? 50) - (b.priority ?? 50);
|
|
1833
|
+
if (pd !== 0) return pd;
|
|
1834
|
+
return a.name.localeCompare(b.name);
|
|
1835
|
+
});
|
|
1836
|
+
for (const src of sorted) {
|
|
1837
|
+
try {
|
|
1838
|
+
const patch = await src.read();
|
|
1839
|
+
if (patch && Object.keys(patch).length > 0) {
|
|
1840
|
+
cfg = deepMerge2(cfg, patch);
|
|
1841
|
+
}
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
console.warn(`Config source "${src.name}" failed`, err);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (opts.cliFlags) {
|
|
1847
|
+
cfg = deepMerge2(cfg, opts.cliFlags);
|
|
1848
|
+
}
|
|
1849
|
+
if (this.vault) {
|
|
1850
|
+
cfg = decryptConfigSecrets(cfg, this.vault);
|
|
1851
|
+
}
|
|
1852
|
+
this.validateBehavior(cfg);
|
|
1853
|
+
if (this.strict) this.validateIdentity(cfg);
|
|
1854
|
+
return Object.freeze(cfg);
|
|
1855
|
+
}
|
|
1856
|
+
async readJson(file) {
|
|
1857
|
+
try {
|
|
1858
|
+
const raw = await fsp.readFile(file, "utf8");
|
|
1859
|
+
const parsed = safeParse(raw);
|
|
1860
|
+
if (parsed.ok && parsed.value) return parsed.value;
|
|
1861
|
+
} catch {
|
|
1862
|
+
}
|
|
1863
|
+
return {};
|
|
1864
|
+
}
|
|
1865
|
+
validateBehavior(cfg) {
|
|
1866
|
+
if (cfg.version === void 0) throw new Error("Config: missing version field");
|
|
1867
|
+
if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
|
|
1868
|
+
const c = cfg.context;
|
|
1869
|
+
if (!c) throw new Error("Config: missing context section");
|
|
1870
|
+
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
1871
|
+
throw new Error("Config: context thresholds must satisfy warn < soft < hard");
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
validateIdentity(cfg) {
|
|
1875
|
+
if (!cfg.provider) {
|
|
1876
|
+
throw new Error(
|
|
1877
|
+
"Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (!cfg.model) {
|
|
1881
|
+
throw new Error(
|
|
1882
|
+
"Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL."
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// src/defaults/compactor.ts
|
|
1889
|
+
var HybridCompactor = class {
|
|
1890
|
+
preserveK;
|
|
1891
|
+
eliseThreshold;
|
|
1892
|
+
estimator;
|
|
1893
|
+
constructor(opts = {}) {
|
|
1894
|
+
this.preserveK = opts.preserveK ?? 10;
|
|
1895
|
+
this.eliseThreshold = opts.eliseThreshold ?? 2e3;
|
|
1896
|
+
this.estimator = opts.estimator ?? roughTokenEstimate;
|
|
1897
|
+
}
|
|
1898
|
+
async compact(ctx, opts = {}) {
|
|
1899
|
+
const beforeTokens = this.estimateMessages(ctx.messages);
|
|
1900
|
+
const reductions = [];
|
|
1901
|
+
const phase1Saved = this.eliseOldToolResults(ctx);
|
|
1902
|
+
if (phase1Saved > 0) reductions.push({ phase: "elision", saved: phase1Saved });
|
|
1903
|
+
if (opts.aggressive) {
|
|
1904
|
+
const phase2Saved = this.collapseAncientTurns(ctx);
|
|
1905
|
+
if (phase2Saved > 0) reductions.push({ phase: "summary", saved: phase2Saved });
|
|
1906
|
+
}
|
|
1907
|
+
const afterTokens = this.estimateMessages(ctx.messages);
|
|
1908
|
+
return { before: beforeTokens, after: afterTokens, reductions };
|
|
1909
|
+
}
|
|
1910
|
+
eliseOldToolResults(ctx) {
|
|
1911
|
+
const messages = ctx.messages;
|
|
1912
|
+
let pairCount = 0;
|
|
1913
|
+
let preserveStart = messages.length;
|
|
1914
|
+
for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
|
|
1915
|
+
const m = messages[i];
|
|
1916
|
+
if (!m) continue;
|
|
1917
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
1918
|
+
pairCount++;
|
|
1919
|
+
preserveStart = i;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
let saved = 0;
|
|
1923
|
+
for (let i = 0; i < preserveStart; i++) {
|
|
1924
|
+
const msg = messages[i];
|
|
1925
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
1926
|
+
const newContent = msg.content.map((b) => {
|
|
1927
|
+
if (b.type !== "tool_result") return b;
|
|
1928
|
+
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
1929
|
+
const tokens = this.estimator(text);
|
|
1930
|
+
if (tokens < this.eliseThreshold) return b;
|
|
1931
|
+
saved += tokens;
|
|
1932
|
+
const elided = {
|
|
1933
|
+
type: "tool_result",
|
|
1934
|
+
tool_use_id: b.tool_use_id,
|
|
1935
|
+
content: `[elided: ~${tokens} tokens removed. Call the tool again if needed.]`,
|
|
1936
|
+
is_error: b.is_error
|
|
1937
|
+
};
|
|
1938
|
+
return elided;
|
|
1939
|
+
});
|
|
1940
|
+
messages[i] = { ...msg, content: newContent };
|
|
1941
|
+
}
|
|
1942
|
+
return saved;
|
|
1943
|
+
}
|
|
1944
|
+
collapseAncientTurns(ctx) {
|
|
1945
|
+
const messages = ctx.messages;
|
|
1946
|
+
const cutTarget = Math.max(0, messages.length - this.preserveK * 2);
|
|
1947
|
+
if (cutTarget <= 0) return 0;
|
|
1948
|
+
let boundary = -1;
|
|
1949
|
+
for (let i = cutTarget; i < messages.length; i++) {
|
|
1950
|
+
const m = messages[i];
|
|
1951
|
+
if (!m) continue;
|
|
1952
|
+
if (m.role === "user" && hasTextContent(m)) {
|
|
1953
|
+
boundary = i;
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (boundary <= 0) return 0;
|
|
1958
|
+
const removed = messages.slice(0, boundary);
|
|
1959
|
+
const removedTokens = this.estimateMessages(removed);
|
|
1960
|
+
const summary = [
|
|
1961
|
+
{
|
|
1962
|
+
role: "user",
|
|
1963
|
+
content: `[previous_session_summary: ${removed.length} earlier turns compacted. Todo state preserved in context.]`
|
|
1964
|
+
},
|
|
1965
|
+
{ role: "assistant", content: "Continuing from compacted context." }
|
|
1966
|
+
];
|
|
1967
|
+
ctx.messages.splice(0, boundary, ...summary);
|
|
1968
|
+
return Math.max(0, removedTokens - this.estimateMessages(summary));
|
|
1969
|
+
}
|
|
1970
|
+
estimateMessages(messages) {
|
|
1971
|
+
let total = 0;
|
|
1972
|
+
for (const m of messages) {
|
|
1973
|
+
if (typeof m.content === "string") {
|
|
1974
|
+
total += this.estimator(m.content);
|
|
1975
|
+
} else {
|
|
1976
|
+
for (const b of m.content) {
|
|
1977
|
+
if (b.type === "text") total += this.estimator(b.text);
|
|
1978
|
+
else if (b.type === "tool_use") total += this.estimator(JSON.stringify(b.input));
|
|
1979
|
+
else if (b.type === "tool_result") {
|
|
1980
|
+
total += this.estimator(
|
|
1981
|
+
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return total;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
function hasTextContent(m) {
|
|
1991
|
+
if (typeof m.content === "string") return m.content.trim().length > 0;
|
|
1992
|
+
return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
|
|
1993
|
+
}
|
|
1994
|
+
function roughTokenEstimate(text) {
|
|
1995
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/types/blocks.ts
|
|
1999
|
+
function isTextBlock(b) {
|
|
2000
|
+
return b.type === "text";
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/defaults/intelligent-compactor.ts
|
|
2004
|
+
var IntelligentCompactor = class {
|
|
2005
|
+
provider;
|
|
2006
|
+
warnThreshold;
|
|
2007
|
+
softThreshold;
|
|
2008
|
+
hardThreshold;
|
|
2009
|
+
maxContext;
|
|
2010
|
+
preserveK;
|
|
2011
|
+
eliseThreshold;
|
|
2012
|
+
summarizerPrompt;
|
|
2013
|
+
summarizerModel;
|
|
2014
|
+
constructor(opts) {
|
|
2015
|
+
this.provider = opts.provider;
|
|
2016
|
+
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
2017
|
+
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
2018
|
+
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
2019
|
+
this.maxContext = opts.maxContext ?? 128e3;
|
|
2020
|
+
this.preserveK = opts.preserveK ?? 4;
|
|
2021
|
+
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
2022
|
+
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of conversation messages, produce a concise but complete summary that preserves all factual information, decisions made, and any state changes (e.g. file edits, todo updates). Do not add commentary. Output only the summary.";
|
|
2023
|
+
this.summarizerModel = opts.summarizerModel;
|
|
2024
|
+
}
|
|
2025
|
+
async compact(ctx, opts = {}) {
|
|
2026
|
+
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
2027
|
+
const reductions = [];
|
|
2028
|
+
const load = beforeTokens / this.maxContext;
|
|
2029
|
+
const aggressive = opts.aggressive ?? load >= this.softThreshold;
|
|
2030
|
+
const saved1 = this.eliseOldToolResults(ctx);
|
|
2031
|
+
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
2032
|
+
if (aggressive) {
|
|
2033
|
+
const saved2 = await this.summarizeAncientTurns(ctx);
|
|
2034
|
+
if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
|
|
2035
|
+
} else if (load >= this.warnThreshold) {
|
|
2036
|
+
const saved2 = this.lightweightCompact(ctx);
|
|
2037
|
+
if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
|
|
2038
|
+
}
|
|
2039
|
+
const afterTokens = this.estimateTokens(ctx.messages);
|
|
2040
|
+
return { before: beforeTokens, after: afterTokens, reductions };
|
|
2041
|
+
}
|
|
2042
|
+
async summarizeAncientTurns(ctx) {
|
|
2043
|
+
const messages = ctx.messages;
|
|
2044
|
+
const cutoff = Math.max(0, messages.length - this.preserveK * 2);
|
|
2045
|
+
if (cutoff <= 2) return 0;
|
|
2046
|
+
const boundary = this.findSafeBoundary(messages, 0, cutoff);
|
|
2047
|
+
if (boundary <= 1) return 0;
|
|
2048
|
+
const toSummarize = messages.slice(0, boundary);
|
|
2049
|
+
const removedTokens = this.estimateTokens(toSummarize);
|
|
2050
|
+
let summaryText;
|
|
2051
|
+
try {
|
|
2052
|
+
summaryText = await this.callSummarizer(toSummarize, ctx);
|
|
2053
|
+
} catch {
|
|
2054
|
+
summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
|
|
2055
|
+
}
|
|
2056
|
+
const summaryMsg = {
|
|
2057
|
+
role: "system",
|
|
2058
|
+
content: `[prior_turns_summary: ${summaryText}]`
|
|
2059
|
+
};
|
|
2060
|
+
const summaryTokens = this.estimateTokens([summaryMsg]);
|
|
2061
|
+
ctx.messages.splice(0, boundary, summaryMsg);
|
|
2062
|
+
return Math.max(0, removedTokens - summaryTokens);
|
|
2063
|
+
}
|
|
2064
|
+
findSafeBoundary(messages, from, to) {
|
|
2065
|
+
for (let i = to; i >= from; i--) {
|
|
2066
|
+
const m = messages[i];
|
|
2067
|
+
if (!m) continue;
|
|
2068
|
+
if (m.role === "user" && this.hasTextContent(m)) {
|
|
2069
|
+
return this.findExchangeStart(messages, i);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return -1;
|
|
2073
|
+
}
|
|
2074
|
+
findExchangeStart(messages, userIndex) {
|
|
2075
|
+
for (let i = userIndex - 1; i >= 0; i--) {
|
|
2076
|
+
const m = messages[i];
|
|
2077
|
+
if (!m) continue;
|
|
2078
|
+
if (m.role === "assistant") {
|
|
2079
|
+
const hasToolUse = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
|
|
2080
|
+
if (!hasToolUse) {
|
|
2081
|
+
return i + 1;
|
|
2082
|
+
}
|
|
2083
|
+
} else if (m.role !== "user") ; else {
|
|
2084
|
+
return i;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return 0;
|
|
2088
|
+
}
|
|
2089
|
+
async callSummarizer(messages, ctx) {
|
|
2090
|
+
const prompt = [
|
|
2091
|
+
{ type: "text", text: this.summarizerPrompt },
|
|
2092
|
+
{ type: "text", text: "\n\nConversation to summarize:\n" },
|
|
2093
|
+
...this.messagesToText(messages)
|
|
2094
|
+
];
|
|
2095
|
+
const req = {
|
|
2096
|
+
model: this.summarizerModel ?? ctx.model,
|
|
2097
|
+
system: prompt,
|
|
2098
|
+
messages: [],
|
|
2099
|
+
maxTokens: 1024
|
|
2100
|
+
};
|
|
2101
|
+
const signal = ctx.signal ?? new AbortController().signal;
|
|
2102
|
+
const res = await this.provider.complete(req, { signal });
|
|
2103
|
+
const textBlocks = res.content.filter(isTextBlock);
|
|
2104
|
+
return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
|
|
2105
|
+
}
|
|
2106
|
+
messagesToText(messages) {
|
|
2107
|
+
const lines = [];
|
|
2108
|
+
for (const m of messages) {
|
|
2109
|
+
const role = m.role.padEnd(10, " ");
|
|
2110
|
+
if (typeof m.content === "string") {
|
|
2111
|
+
lines.push(`[${role}]: ${m.content.slice(0, 500)}`);
|
|
2112
|
+
} else if (Array.isArray(m.content)) {
|
|
2113
|
+
const textParts = m.content.filter(isTextBlock).map((b) => b.text);
|
|
2114
|
+
if (textParts.length > 0) {
|
|
2115
|
+
lines.push(`[${role}]: ${textParts.join(" ").slice(0, 500)}`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
2120
|
+
}
|
|
2121
|
+
lightweightCompact(ctx) {
|
|
2122
|
+
return this.eliseOldToolResults(ctx);
|
|
2123
|
+
}
|
|
2124
|
+
eliseOldToolResults(ctx) {
|
|
2125
|
+
const messages = ctx.messages;
|
|
2126
|
+
let pairCount = 0;
|
|
2127
|
+
let preserveStart = messages.length;
|
|
2128
|
+
for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
|
|
2129
|
+
const m = messages[i];
|
|
2130
|
+
if (!m) continue;
|
|
2131
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
2132
|
+
pairCount++;
|
|
2133
|
+
preserveStart = i;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
let saved = 0;
|
|
2137
|
+
for (let i = 0; i < preserveStart; i++) {
|
|
2138
|
+
const msg = messages[i];
|
|
2139
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
2140
|
+
const newContent = msg.content.map((b) => {
|
|
2141
|
+
if (b.type !== "tool_result") return b;
|
|
2142
|
+
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
2143
|
+
const tokens = this.roughTokenEstimate(text);
|
|
2144
|
+
if (tokens < this.eliseThreshold) return b;
|
|
2145
|
+
saved += tokens;
|
|
2146
|
+
return {
|
|
2147
|
+
type: "tool_result",
|
|
2148
|
+
tool_use_id: b.tool_use_id,
|
|
2149
|
+
content: `[elided: ~${tokens} tokens]`,
|
|
2150
|
+
is_error: b.is_error
|
|
2151
|
+
};
|
|
2152
|
+
});
|
|
2153
|
+
messages[i] = { ...msg, content: newContent };
|
|
2154
|
+
}
|
|
2155
|
+
return saved;
|
|
2156
|
+
}
|
|
2157
|
+
hasTextContent(m) {
|
|
2158
|
+
if (typeof m.content === "string") return m.content.trim().length > 0;
|
|
2159
|
+
return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
|
|
2160
|
+
}
|
|
2161
|
+
estimateTokens(messages) {
|
|
2162
|
+
let total = 0;
|
|
2163
|
+
for (const m of messages) {
|
|
2164
|
+
if (typeof m.content === "string") {
|
|
2165
|
+
total += this.roughTokenEstimate(m.content);
|
|
2166
|
+
} else {
|
|
2167
|
+
for (const b of m.content) {
|
|
2168
|
+
if (b.type === "text") total += this.roughTokenEstimate(b.text);
|
|
2169
|
+
else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
|
|
2170
|
+
else if (b.type === "tool_result") {
|
|
2171
|
+
total += this.roughTokenEstimate(
|
|
2172
|
+
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return total;
|
|
2179
|
+
}
|
|
2180
|
+
roughTokenEstimate(text) {
|
|
2181
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
// src/defaults/llm-selector.ts
|
|
2186
|
+
var DEFAULT_SYSTEM_PROMPT = `You are a context pruning assistant. Given a conversation history and a token budget, decide which message ranges are worth keeping verbatim and which should be collapsed into summaries.
|
|
2187
|
+
|
|
2188
|
+
Output a JSON object with this structure:
|
|
2189
|
+
{
|
|
2190
|
+
"kept": [{"from": 0, "to": 5, "importance": "critical"}],
|
|
2191
|
+
"collapsed": [{"from": 6, "to": 20, "summary": "optional summary"}],
|
|
2192
|
+
"reasoning": "brief explanation of decisions"
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
Importance tiers:
|
|
2196
|
+
- "critical": decisions, file edits, tool results that affect state, final answers
|
|
2197
|
+
- "high": substantive tool use, complex reasoning, non-obvious observations
|
|
2198
|
+
- "medium": routine exchanges, confirmations, straightforward Q&A
|
|
2199
|
+
|
|
2200
|
+
Rules:
|
|
2201
|
+
- Always keep the most recent K pairs (preserve recency)
|
|
2202
|
+
- Never collapse the final 2 user/assistant pairs (working memory)
|
|
2203
|
+
- Preserve tool results that modified files or had external effects
|
|
2204
|
+
- Collapse old, low-information exchanges (greetings, acknowledgements, etc.)
|
|
2205
|
+
- If unsure, keep rather than collapse (errors are more costly than waste)
|
|
2206
|
+
|
|
2207
|
+
Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
|
|
2208
|
+
function estimateTokens(messages) {
|
|
2209
|
+
let total = 0;
|
|
2210
|
+
for (const m of messages) {
|
|
2211
|
+
if (typeof m.content === "string") {
|
|
2212
|
+
total += Math.ceil(m.content.length / 4);
|
|
2213
|
+
} else if (Array.isArray(m.content)) {
|
|
2214
|
+
for (const b of m.content) {
|
|
2215
|
+
if (b.type === "text") total += Math.ceil(b.text.length / 4);
|
|
2216
|
+
else total += Math.ceil(JSON.stringify(b).length / 4);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return total;
|
|
2221
|
+
}
|
|
2222
|
+
function formatMessages(messages, maxChars = 8e3) {
|
|
2223
|
+
const lines = [];
|
|
2224
|
+
let used = 0;
|
|
2225
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2226
|
+
const m = messages[i];
|
|
2227
|
+
const role = m.role.padEnd(10, " ");
|
|
2228
|
+
let text;
|
|
2229
|
+
if (typeof m.content === "string") {
|
|
2230
|
+
text = m.content.slice(0, 500);
|
|
2231
|
+
} else {
|
|
2232
|
+
const content = m.content;
|
|
2233
|
+
text = content.filter(isTextBlock).map((b) => b.text).join(" ");
|
|
2234
|
+
const toolUses = content.filter((b) => b.type === "tool_use");
|
|
2235
|
+
if (toolUses.length > 0) {
|
|
2236
|
+
text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const line = `[${i}][${role}]: ${text}`;
|
|
2240
|
+
if (used + line.length > maxChars) break;
|
|
2241
|
+
lines.push(line);
|
|
2242
|
+
used += line.length;
|
|
2243
|
+
}
|
|
2244
|
+
return lines.join("\n");
|
|
2245
|
+
}
|
|
2246
|
+
var LLMSelector = class {
|
|
2247
|
+
provider;
|
|
2248
|
+
model;
|
|
2249
|
+
maxContextTokens;
|
|
2250
|
+
systemPrompt;
|
|
2251
|
+
constructor(opts) {
|
|
2252
|
+
this.provider = opts.provider;
|
|
2253
|
+
this.model = opts.model ?? "unknown";
|
|
2254
|
+
this.maxContextTokens = opts.maxContextTokens ?? 4e4;
|
|
2255
|
+
this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2256
|
+
}
|
|
2257
|
+
async select(messages, maxToKeep) {
|
|
2258
|
+
const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
|
|
2259
|
+
const historyText = formatMessages(messages);
|
|
2260
|
+
const totalTokens = estimateTokens(messages);
|
|
2261
|
+
const systemText = `${this.systemPrompt}
|
|
2262
|
+
|
|
2263
|
+
Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
|
|
2264
|
+
`;
|
|
2265
|
+
const budgetInstruction = totalTokens > effectiveBudget ? `
|
|
2266
|
+
|
|
2267
|
+
IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
|
|
2268
|
+
const req = {
|
|
2269
|
+
model: this.model,
|
|
2270
|
+
system: [{ type: "text", text: systemText + budgetInstruction }],
|
|
2271
|
+
messages: [{ role: "user", content: historyText }],
|
|
2272
|
+
maxTokens: 1024
|
|
2273
|
+
};
|
|
2274
|
+
let raw;
|
|
2275
|
+
try {
|
|
2276
|
+
const res = await this.provider.complete(req, { signal: new AbortController().signal });
|
|
2277
|
+
const textBlocks = res.content.filter(isTextBlock);
|
|
2278
|
+
raw = textBlocks.map((b) => b.text).join("\n").trim();
|
|
2279
|
+
} catch (err) {
|
|
2280
|
+
return this.fallbackSelect(messages, effectiveBudget);
|
|
2281
|
+
}
|
|
2282
|
+
return this.parseSelectorOutput(raw, messages.length);
|
|
2283
|
+
}
|
|
2284
|
+
fallbackSelect(messages, budget) {
|
|
2285
|
+
const toKeep = [];
|
|
2286
|
+
const toCollapse = [];
|
|
2287
|
+
let tokenCount = 0;
|
|
2288
|
+
let startIdx = 0;
|
|
2289
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2290
|
+
const m = messages[i];
|
|
2291
|
+
const cost = typeof m.content === "string" ? Math.ceil(m.content.length / 4) : m.content.reduce((acc, b) => acc + (b.type === "text" ? Math.ceil(b.text.length / 4) : Math.ceil(JSON.stringify(b).length / 4)), 0);
|
|
2292
|
+
if (tokenCount + cost <= budget) {
|
|
2293
|
+
tokenCount += cost;
|
|
2294
|
+
} else {
|
|
2295
|
+
startIdx = i + 1;
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (startIdx > 0) {
|
|
2300
|
+
toCollapse.push({ from: 0, to: startIdx - 1 });
|
|
2301
|
+
}
|
|
2302
|
+
toKeep.push({ from: startIdx, to: messages.length - 1, importance: "high" });
|
|
2303
|
+
return {
|
|
2304
|
+
kept: toKeep,
|
|
2305
|
+
collapsed: toCollapse,
|
|
2306
|
+
reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
parseSelectorOutput(raw, messageCount) {
|
|
2310
|
+
const jsonStart = raw.indexOf("{");
|
|
2311
|
+
const jsonEnd = raw.lastIndexOf("}");
|
|
2312
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
2313
|
+
return this.fallbackSelect(
|
|
2314
|
+
Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
|
|
2315
|
+
this.maxContextTokens
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
let parsed;
|
|
2319
|
+
try {
|
|
2320
|
+
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
2321
|
+
} catch {
|
|
2322
|
+
return this.fallbackSelect(
|
|
2323
|
+
Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
|
|
2324
|
+
this.maxContextTokens
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
const obj = parsed;
|
|
2328
|
+
const kept = obj.kept ?? [];
|
|
2329
|
+
const collapsed = obj.collapsed ?? [];
|
|
2330
|
+
return {
|
|
2331
|
+
kept: kept.map((k) => ({ from: k.from, to: k.to, importance: k.importance ?? "medium" })),
|
|
2332
|
+
collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
|
|
2333
|
+
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
// src/defaults/selective-compactor.ts
|
|
2339
|
+
var SelectiveCompactor = class {
|
|
2340
|
+
provider;
|
|
2341
|
+
selector;
|
|
2342
|
+
warnThreshold;
|
|
2343
|
+
softThreshold;
|
|
2344
|
+
hardThreshold;
|
|
2345
|
+
maxContext;
|
|
2346
|
+
preserveK;
|
|
2347
|
+
eliseThreshold;
|
|
2348
|
+
summarizerModel;
|
|
2349
|
+
summarizerPrompt;
|
|
2350
|
+
constructor(opts) {
|
|
2351
|
+
this.provider = opts.provider;
|
|
2352
|
+
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
|
|
2353
|
+
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
2354
|
+
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
2355
|
+
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
2356
|
+
this.maxContext = opts.maxContext ?? 128e3;
|
|
2357
|
+
this.preserveK = opts.preserveK ?? 4;
|
|
2358
|
+
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
2359
|
+
this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
|
|
2360
|
+
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
|
|
2361
|
+
}
|
|
2362
|
+
async compact(ctx, opts = {}) {
|
|
2363
|
+
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
2364
|
+
const reductions = [];
|
|
2365
|
+
const load = beforeTokens / this.maxContext;
|
|
2366
|
+
const shouldCompact = load >= this.warnThreshold || opts.aggressive;
|
|
2367
|
+
if (!shouldCompact) {
|
|
2368
|
+
const saved = this.eliseOldToolResults(ctx);
|
|
2369
|
+
if (saved > 0) reductions.push({ phase: "elision", saved });
|
|
2370
|
+
const afterTokens2 = this.estimateTokens(ctx.messages);
|
|
2371
|
+
return { before: beforeTokens, after: afterTokens2, reductions };
|
|
2372
|
+
}
|
|
2373
|
+
const savedElision = this.eliseOldToolResults(ctx);
|
|
2374
|
+
if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
|
|
2375
|
+
const afterPhase1 = this.estimateTokens(ctx.messages);
|
|
2376
|
+
const targetBudget = this.computeTargetBudget(load, opts.aggressive ?? false);
|
|
2377
|
+
if (afterPhase1 > targetBudget) {
|
|
2378
|
+
const savedSelective = await this.runSelector(ctx, targetBudget);
|
|
2379
|
+
if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
|
|
2380
|
+
}
|
|
2381
|
+
const afterTokens = this.estimateTokens(ctx.messages);
|
|
2382
|
+
return { before: beforeTokens, after: afterTokens, reductions };
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Run the LLM selector to decide what to keep vs collapse.
|
|
2386
|
+
* Returns the token savings achieved.
|
|
2387
|
+
*/
|
|
2388
|
+
async runSelector(ctx, targetBudget) {
|
|
2389
|
+
const before = this.estimateTokens(ctx.messages);
|
|
2390
|
+
let result;
|
|
2391
|
+
try {
|
|
2392
|
+
result = await this.selector.select(ctx.messages, targetBudget);
|
|
2393
|
+
} catch {
|
|
2394
|
+
return this.aggressiveRecencyTrim(ctx, targetBudget);
|
|
2395
|
+
}
|
|
2396
|
+
await this.executePlan(ctx, result);
|
|
2397
|
+
const after = this.estimateTokens(ctx.messages);
|
|
2398
|
+
return Math.max(0, before - after);
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* Execute a SelectorResult plan: collapse/remove ranges and
|
|
2402
|
+
* insert summaries where the selector provided them.
|
|
2403
|
+
*/
|
|
2404
|
+
async executePlan(ctx, plan) {
|
|
2405
|
+
const messages = ctx.messages;
|
|
2406
|
+
if (messages.length === 0) return;
|
|
2407
|
+
const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
|
|
2408
|
+
for (const range of sortedCollapsed) {
|
|
2409
|
+
if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
|
|
2410
|
+
let summary = range.summary;
|
|
2411
|
+
if (!summary) {
|
|
2412
|
+
const toSummarize = messages.slice(range.from, range.to + 1);
|
|
2413
|
+
summary = await this.summarizeRange(toSummarize, ctx);
|
|
2414
|
+
}
|
|
2415
|
+
const summaryMsg = {
|
|
2416
|
+
role: "system",
|
|
2417
|
+
content: `[prior_turns_${range.from}-${range.to}: ${summary}]`
|
|
2418
|
+
};
|
|
2419
|
+
messages.splice(range.from, range.to - range.from + 1, summaryMsg);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
async summarizeRange(messages, ctx) {
|
|
2423
|
+
const systemText = `${this.summarizerPrompt}
|
|
2424
|
+
|
|
2425
|
+
Summarize the following message range:`;
|
|
2426
|
+
const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
|
|
2427
|
+
const req = {
|
|
2428
|
+
model: this.summarizerModel,
|
|
2429
|
+
system: [{ type: "text", text: systemText }],
|
|
2430
|
+
messages: [{ role: "user", content: body }],
|
|
2431
|
+
maxTokens: 512
|
|
2432
|
+
};
|
|
2433
|
+
try {
|
|
2434
|
+
const res = await this.provider.complete(req, { signal: ctx.signal ?? new AbortController().signal });
|
|
2435
|
+
return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
|
|
2436
|
+
} catch {
|
|
2437
|
+
return `[${messages.length} earlier turns omitted]`;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
messagePreview(m) {
|
|
2441
|
+
if (typeof m.content === "string") return m.content.slice(0, 300);
|
|
2442
|
+
return m.content.filter(isTextBlock).map((b) => b.text).join(" ").slice(0, 300);
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Fallback when selector fails: aggressively trim from the oldest end
|
|
2446
|
+
* until we hit targetBudget.
|
|
2447
|
+
*/
|
|
2448
|
+
aggressiveRecencyTrim(ctx, targetBudget) {
|
|
2449
|
+
const messages = ctx.messages;
|
|
2450
|
+
this.estimateTokens(messages);
|
|
2451
|
+
const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
|
|
2452
|
+
if (preserveIdx <= 0) return 0;
|
|
2453
|
+
let boundary = preserveIdx;
|
|
2454
|
+
for (let i = preserveIdx; i < messages.length && i < preserveIdx + 6; i++) {
|
|
2455
|
+
const m = messages[i];
|
|
2456
|
+
if (m.role === "user" && this.hasTextContent(m)) {
|
|
2457
|
+
boundary = i;
|
|
2458
|
+
break;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
const removed = messages.slice(0, boundary);
|
|
2462
|
+
const removedTokens = this.estimateTokens(removed);
|
|
2463
|
+
const summaryMsg = {
|
|
2464
|
+
role: "system",
|
|
2465
|
+
content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
|
|
2466
|
+
};
|
|
2467
|
+
messages.splice(0, boundary, summaryMsg);
|
|
2468
|
+
return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
|
|
2469
|
+
}
|
|
2470
|
+
computeTargetBudget(load, aggressive) {
|
|
2471
|
+
if (load >= this.hardThreshold) {
|
|
2472
|
+
return Math.floor(this.maxContext * 0.5);
|
|
2473
|
+
}
|
|
2474
|
+
if (load >= this.softThreshold) {
|
|
2475
|
+
return Math.floor(this.maxContext * 0.65);
|
|
2476
|
+
}
|
|
2477
|
+
return Math.floor(this.maxContext * 0.75);
|
|
2478
|
+
}
|
|
2479
|
+
eliseOldToolResults(ctx) {
|
|
2480
|
+
const messages = ctx.messages;
|
|
2481
|
+
let pairCount = 0;
|
|
2482
|
+
let preserveStart = messages.length;
|
|
2483
|
+
for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
|
|
2484
|
+
const m = messages[i];
|
|
2485
|
+
if (!m) continue;
|
|
2486
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
2487
|
+
pairCount++;
|
|
2488
|
+
preserveStart = i;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
let saved = 0;
|
|
2492
|
+
for (let i = 0; i < preserveStart; i++) {
|
|
2493
|
+
const msg = messages[i];
|
|
2494
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
2495
|
+
const newContent = msg.content.map((b) => {
|
|
2496
|
+
if (b.type !== "tool_result") return b;
|
|
2497
|
+
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
2498
|
+
const tokens = this.roughTokenEstimate(text);
|
|
2499
|
+
if (tokens < this.eliseThreshold) return b;
|
|
2500
|
+
saved += tokens;
|
|
2501
|
+
return {
|
|
2502
|
+
type: "tool_result",
|
|
2503
|
+
tool_use_id: b.tool_use_id,
|
|
2504
|
+
content: `[elided: ~${tokens} tokens]`,
|
|
2505
|
+
is_error: b.is_error
|
|
2506
|
+
};
|
|
2507
|
+
});
|
|
2508
|
+
messages[i] = { ...msg, content: newContent };
|
|
2509
|
+
}
|
|
2510
|
+
return saved;
|
|
2511
|
+
}
|
|
2512
|
+
hasTextContent(m) {
|
|
2513
|
+
if (typeof m.content === "string") return m.content.trim().length > 0;
|
|
2514
|
+
return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
|
|
2515
|
+
}
|
|
2516
|
+
estimateTokens(messages) {
|
|
2517
|
+
let total = 0;
|
|
2518
|
+
for (const m of messages) {
|
|
2519
|
+
if (typeof m.content === "string") {
|
|
2520
|
+
total += this.roughTokenEstimate(m.content);
|
|
2521
|
+
} else {
|
|
2522
|
+
for (const b of m.content) {
|
|
2523
|
+
if (b.type === "text") total += this.roughTokenEstimate(b.text);
|
|
2524
|
+
else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
|
|
2525
|
+
else if (b.type === "tool_result") {
|
|
2526
|
+
total += this.roughTokenEstimate(
|
|
2527
|
+
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return total;
|
|
2534
|
+
}
|
|
2535
|
+
roughTokenEstimate(text) {
|
|
2536
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
|
|
2540
|
+
// src/defaults/auto-compaction-middleware.ts
|
|
2541
|
+
var AutoCompactionMiddleware = class {
|
|
2542
|
+
name = "AutoCompaction";
|
|
2543
|
+
compactor;
|
|
2544
|
+
warnThreshold;
|
|
2545
|
+
// fraction of maxContext (0-1)
|
|
2546
|
+
softThreshold;
|
|
2547
|
+
hardThreshold;
|
|
2548
|
+
maxContext;
|
|
2549
|
+
estimator;
|
|
2550
|
+
aggressiveOn;
|
|
2551
|
+
/**
|
|
2552
|
+
* @param compactor Compactor to use for compaction
|
|
2553
|
+
* @param maxContext Provider's max context window in tokens
|
|
2554
|
+
* @param estimator Token estimation function (ctx → token count)
|
|
2555
|
+
* @param thresholds Threshold fractions (0-1) of maxContext
|
|
2556
|
+
* @param aggressiveOn Which threshold triggers aggressive (full LLM summarization)
|
|
2557
|
+
*/
|
|
2558
|
+
constructor(compactor, maxContext, estimator, thresholds, aggressiveOn = "soft") {
|
|
2559
|
+
this.compactor = compactor;
|
|
2560
|
+
this.maxContext = maxContext;
|
|
2561
|
+
this.estimator = estimator;
|
|
2562
|
+
this.warnThreshold = thresholds.warn;
|
|
2563
|
+
this.softThreshold = thresholds.soft;
|
|
2564
|
+
this.hardThreshold = thresholds.hard;
|
|
2565
|
+
this.aggressiveOn = aggressiveOn;
|
|
2566
|
+
}
|
|
2567
|
+
handler() {
|
|
2568
|
+
return async (ctx, next) => {
|
|
2569
|
+
const tokens = this.estimator(ctx);
|
|
2570
|
+
const load = tokens / this.maxContext;
|
|
2571
|
+
if (load >= this.hardThreshold) {
|
|
2572
|
+
await this.compact(ctx, true);
|
|
2573
|
+
} else if (load >= this.softThreshold) {
|
|
2574
|
+
await this.compact(ctx, this.aggressiveOn !== "hard");
|
|
2575
|
+
} else if (load >= this.warnThreshold) {
|
|
2576
|
+
await this.compact(ctx, false);
|
|
2577
|
+
}
|
|
2578
|
+
return next(ctx);
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
async compact(ctx, aggressive) {
|
|
2582
|
+
try {
|
|
2583
|
+
await this.compactor.compact(ctx, { aggressive });
|
|
2584
|
+
} catch {
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
var DEFAULT_URL = "https://models.dev/api.json";
|
|
2589
|
+
var DEFAULT_TTL_SECONDS = 24 * 3600;
|
|
2590
|
+
var FAMILY_BY_NPM = {
|
|
2591
|
+
"@ai-sdk/anthropic": "anthropic",
|
|
2592
|
+
"@ai-sdk/google-vertex/anthropic": "anthropic",
|
|
2593
|
+
"@ai-sdk/openai": "openai",
|
|
2594
|
+
"@ai-sdk/openai-compatible": "openai-compatible",
|
|
2595
|
+
"@ai-sdk/groq": "openai-compatible",
|
|
2596
|
+
"@ai-sdk/xai": "openai-compatible",
|
|
2597
|
+
"@ai-sdk/cerebras": "openai-compatible",
|
|
2598
|
+
"@ai-sdk/togetherai": "openai-compatible",
|
|
2599
|
+
"@ai-sdk/perplexity": "openai-compatible",
|
|
2600
|
+
"@ai-sdk/deepinfra": "openai-compatible",
|
|
2601
|
+
"@openrouter/ai-sdk-provider": "openai-compatible",
|
|
2602
|
+
"ai-gateway-provider": "openai-compatible",
|
|
2603
|
+
"@ai-sdk/vercel": "openai-compatible",
|
|
2604
|
+
"@ai-sdk/gateway": "openai-compatible",
|
|
2605
|
+
"@aihubmix/ai-sdk-provider": "openai-compatible",
|
|
2606
|
+
"venice-ai-sdk-provider": "openai-compatible",
|
|
2607
|
+
"@ai-sdk/google": "google"
|
|
2608
|
+
};
|
|
2609
|
+
function classifyFamily(npm) {
|
|
2610
|
+
if (!npm) return "unsupported";
|
|
2611
|
+
return FAMILY_BY_NPM[npm] ?? "unsupported";
|
|
2612
|
+
}
|
|
2613
|
+
var DefaultModelsRegistry = class {
|
|
2614
|
+
payload;
|
|
2615
|
+
fetchedAt;
|
|
2616
|
+
cacheFile;
|
|
2617
|
+
url;
|
|
2618
|
+
ttlMs;
|
|
2619
|
+
fetchImpl;
|
|
2620
|
+
seed;
|
|
2621
|
+
maxStaleAgeMs;
|
|
2622
|
+
constructor(opts) {
|
|
2623
|
+
this.cacheFile = opts.cacheFile;
|
|
2624
|
+
this.url = opts.url ?? DEFAULT_URL;
|
|
2625
|
+
this.ttlMs = (opts.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1e3;
|
|
2626
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
2627
|
+
this.seed = opts.seed;
|
|
2628
|
+
const maxStaleSeconds = opts.maxStaleAgeSeconds ?? 7 * 24 * 3600;
|
|
2629
|
+
this.maxStaleAgeMs = maxStaleSeconds * 1e3;
|
|
2630
|
+
}
|
|
2631
|
+
async load(opts = {}) {
|
|
2632
|
+
if (this.payload && !opts.force) return this.payload;
|
|
2633
|
+
if (this.seed) {
|
|
2634
|
+
this.payload = this.seed;
|
|
2635
|
+
this.fetchedAt = /* @__PURE__ */ new Date();
|
|
2636
|
+
return this.payload;
|
|
2637
|
+
}
|
|
2638
|
+
if (!opts.force) {
|
|
2639
|
+
const cached = await this.readCache();
|
|
2640
|
+
if (cached && this.isFresh(cached.fetchedAt)) {
|
|
2641
|
+
this.payload = cached.payload;
|
|
2642
|
+
this.fetchedAt = new Date(cached.fetchedAt);
|
|
2643
|
+
return cached.payload;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
try {
|
|
2647
|
+
return await this.refresh();
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
const cached = await this.readCache();
|
|
2650
|
+
if (cached && this.isWithinMaxStaleAge(cached.fetchedAt)) {
|
|
2651
|
+
this.payload = cached.payload;
|
|
2652
|
+
this.fetchedAt = new Date(cached.fetchedAt);
|
|
2653
|
+
return cached.payload;
|
|
2654
|
+
}
|
|
2655
|
+
throw err;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
async refresh() {
|
|
2659
|
+
const res = await this.fetchImpl(this.url, {
|
|
2660
|
+
method: "GET",
|
|
2661
|
+
headers: { accept: "application/json" }
|
|
2662
|
+
});
|
|
2663
|
+
if (!res.ok) {
|
|
2664
|
+
throw new Error(`ModelsRegistry: HTTP ${res.status} fetching ${this.url}`);
|
|
2665
|
+
}
|
|
2666
|
+
const json = await res.json();
|
|
2667
|
+
this.payload = json;
|
|
2668
|
+
this.fetchedAt = /* @__PURE__ */ new Date();
|
|
2669
|
+
const envelope = {
|
|
2670
|
+
fetchedAt: this.fetchedAt.toISOString(),
|
|
2671
|
+
url: this.url,
|
|
2672
|
+
payload: json
|
|
2673
|
+
};
|
|
2674
|
+
await atomicWrite(this.cacheFile, JSON.stringify(envelope));
|
|
2675
|
+
return json;
|
|
2676
|
+
}
|
|
2677
|
+
async listProviders() {
|
|
2678
|
+
const payload = await this.load();
|
|
2679
|
+
return Object.values(payload).map((p) => this.resolveProvider(p));
|
|
2680
|
+
}
|
|
2681
|
+
async getProvider(id) {
|
|
2682
|
+
const payload = await this.load();
|
|
2683
|
+
const p = payload[id];
|
|
2684
|
+
return p ? this.resolveProvider(p) : void 0;
|
|
2685
|
+
}
|
|
2686
|
+
async getModel(providerId, modelId) {
|
|
2687
|
+
const provider = await this.getProvider(providerId);
|
|
2688
|
+
if (!provider) return void 0;
|
|
2689
|
+
const model = provider.models.find((m) => m.id === modelId);
|
|
2690
|
+
if (!model) return void 0;
|
|
2691
|
+
return {
|
|
2692
|
+
providerId,
|
|
2693
|
+
modelId,
|
|
2694
|
+
capabilities: {
|
|
2695
|
+
tools: model.tool_call ?? false,
|
|
2696
|
+
vision: Boolean(model.modalities?.input?.includes("image")),
|
|
2697
|
+
reasoning: model.reasoning ?? false,
|
|
2698
|
+
maxContext: model.limit?.context ?? 0,
|
|
2699
|
+
maxOutput: model.limit?.output,
|
|
2700
|
+
knowledge: model.knowledge
|
|
2701
|
+
},
|
|
2702
|
+
cost: model.cost
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
async suggestModel(providerId) {
|
|
2706
|
+
const provider = await this.getProvider(providerId);
|
|
2707
|
+
if (!provider || provider.models.length === 0) return void 0;
|
|
2708
|
+
const ranked = [...provider.models].sort((a, b) => {
|
|
2709
|
+
const at = a.release_date ?? a.last_updated ?? "";
|
|
2710
|
+
const bt = b.release_date ?? b.last_updated ?? "";
|
|
2711
|
+
return bt.localeCompare(at);
|
|
2712
|
+
});
|
|
2713
|
+
return ranked[0]?.id;
|
|
2714
|
+
}
|
|
2715
|
+
async ageSeconds() {
|
|
2716
|
+
if (!this.fetchedAt) {
|
|
2717
|
+
const cached = await this.readCache();
|
|
2718
|
+
if (!cached) return Number.POSITIVE_INFINITY;
|
|
2719
|
+
return (Date.now() - new Date(cached.fetchedAt).getTime()) / 1e3;
|
|
2720
|
+
}
|
|
2721
|
+
return (Date.now() - this.fetchedAt.getTime()) / 1e3;
|
|
2722
|
+
}
|
|
2723
|
+
resolveProvider(p) {
|
|
2724
|
+
return {
|
|
2725
|
+
id: p.id,
|
|
2726
|
+
name: p.name,
|
|
2727
|
+
family: classifyFamily(p.npm),
|
|
2728
|
+
apiBase: p.api,
|
|
2729
|
+
envVars: p.env ?? [],
|
|
2730
|
+
doc: p.doc,
|
|
2731
|
+
models: Object.values(p.models ?? {}),
|
|
2732
|
+
npm: p.npm
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
isFresh(fetchedAtIso) {
|
|
2736
|
+
return Date.now() - new Date(fetchedAtIso).getTime() < this.ttlMs;
|
|
2737
|
+
}
|
|
2738
|
+
isWithinMaxStaleAge(fetchedAtIso) {
|
|
2739
|
+
return Date.now() - new Date(fetchedAtIso).getTime() < this.maxStaleAgeMs;
|
|
2740
|
+
}
|
|
2741
|
+
async readCache() {
|
|
2742
|
+
try {
|
|
2743
|
+
const raw = await fsp.readFile(this.cacheFile, "utf8");
|
|
2744
|
+
return JSON.parse(raw);
|
|
2745
|
+
} catch {
|
|
2746
|
+
return void 0;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
/** Used by `wstack models refresh` to expose where the cache lives. */
|
|
2750
|
+
cacheLocation() {
|
|
2751
|
+
return path2.resolve(this.cacheFile);
|
|
2752
|
+
}
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
// src/types/mode.ts
|
|
2756
|
+
var DEFAULT_MODES = [
|
|
2757
|
+
{
|
|
2758
|
+
id: "default",
|
|
2759
|
+
name: "Default",
|
|
2760
|
+
description: "General-purpose coding assistant",
|
|
2761
|
+
prompt: "",
|
|
2762
|
+
tags: ["general"]
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
id: "code-reviewer",
|
|
2766
|
+
name: "Code Reviewer",
|
|
2767
|
+
description: "Focus on code quality, best practices, and potential bugs",
|
|
2768
|
+
prompt: `## Code Reviewer Mode
|
|
2769
|
+
|
|
2770
|
+
When reviewing code:
|
|
2771
|
+
- Look for potential bugs, race conditions, and edge cases
|
|
2772
|
+
- Check for security vulnerabilities (SQL injection, XSS, CSRF, etc.)
|
|
2773
|
+
- Evaluate error handling completeness
|
|
2774
|
+
- Assess code readability and maintainability
|
|
2775
|
+
- Check for performance anti-patterns
|
|
2776
|
+
- Verify test coverage for critical paths
|
|
2777
|
+
- Ensure naming conventions are followed`,
|
|
2778
|
+
tags: ["review", "quality", "security"],
|
|
2779
|
+
toolPreferences: ["read", "grep", "git", "diff", "test"]
|
|
2780
|
+
},
|
|
2781
|
+
{
|
|
2782
|
+
id: "code-auditor",
|
|
2783
|
+
name: "Code Auditor",
|
|
2784
|
+
description: "Security-focused code analysis",
|
|
2785
|
+
prompt: `## Code Auditor Mode
|
|
2786
|
+
|
|
2787
|
+
When auditing code for security:
|
|
2788
|
+
- Identify injection vulnerabilities (SQL, Command, XSS, LDAP)
|
|
2789
|
+
- Check authentication and authorization patterns
|
|
2790
|
+
- Look for sensitive data exposure (secrets, PII in logs)
|
|
2791
|
+
- Verify cryptographic implementations
|
|
2792
|
+
- Check for insecure dependencies or configurations
|
|
2793
|
+
- Assess input validation and output encoding
|
|
2794
|
+
- Look for timing attacks and information leakage`,
|
|
2795
|
+
tags: ["security", "audit", "compliance"],
|
|
2796
|
+
toolPreferences: ["grep", "read", "audit", "bash"]
|
|
2797
|
+
},
|
|
2798
|
+
{
|
|
2799
|
+
id: "architect",
|
|
2800
|
+
name: "Software Architect",
|
|
2801
|
+
description: "Design patterns, scalability, and system design",
|
|
2802
|
+
prompt: `## Architect Mode
|
|
2803
|
+
|
|
2804
|
+
When designing or reviewing architecture:
|
|
2805
|
+
- Evaluate scalability and future growth
|
|
2806
|
+
- Check for appropriate design patterns
|
|
2807
|
+
- Assess coupling and cohesion
|
|
2808
|
+
- Look forSOLID principle violations
|
|
2809
|
+
- Evaluate data modeling decisions
|
|
2810
|
+
- Check for eventual consistency issues
|
|
2811
|
+
- Assess API design and contract stability
|
|
2812
|
+
- Consider operational aspects (monitoring, logging, deployment)`,
|
|
2813
|
+
tags: ["architecture", "design", "scalability"],
|
|
2814
|
+
toolPreferences: ["read", "glob", "tree", "diff"]
|
|
2815
|
+
},
|
|
2816
|
+
{
|
|
2817
|
+
id: "debugger",
|
|
2818
|
+
name: "Debugger",
|
|
2819
|
+
description: "Root cause analysis and error investigation",
|
|
2820
|
+
prompt: `## Debugger Mode
|
|
2821
|
+
|
|
2822
|
+
When investigating bugs:
|
|
2823
|
+
- Reproduce the issue with minimal steps
|
|
2824
|
+
- Check error messages and stack traces thoroughly
|
|
2825
|
+
- Look for related logs and historical context
|
|
2826
|
+
- Verify assumptions about data flow
|
|
2827
|
+
- Check for race conditions in async code
|
|
2828
|
+
- Validate environment and configuration
|
|
2829
|
+
- Use binary search to isolate the root cause
|
|
2830
|
+
- Verify fixes with tests before considering done`,
|
|
2831
|
+
tags: ["debug", "investigation", "error-resolution"],
|
|
2832
|
+
toolPreferences: ["read", "grep", "bash", "logs", "test"]
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
id: "tester",
|
|
2836
|
+
name: "QA Engineer",
|
|
2837
|
+
description: "Test coverage, edge cases, and quality assurance",
|
|
2838
|
+
prompt: `## Tester Mode
|
|
2839
|
+
|
|
2840
|
+
When testing or writing tests:
|
|
2841
|
+
- Cover happy path and error paths equally
|
|
2842
|
+
- Think about edge cases and boundary conditions
|
|
2843
|
+
- Check for missing null/undefined handling tests
|
|
2844
|
+
- Verify error messages are tested
|
|
2845
|
+
- Look for race condition tests in async code
|
|
2846
|
+
- Assess mutation testing opportunities
|
|
2847
|
+
- Check for integration test gaps
|
|
2848
|
+
- Verify test isolation and cleanup`,
|
|
2849
|
+
tags: ["testing", "qa", "quality"],
|
|
2850
|
+
toolPreferences: ["read", "grep", "test", "bash"]
|
|
2851
|
+
},
|
|
2852
|
+
{
|
|
2853
|
+
id: "devops",
|
|
2854
|
+
name: "DevOps Engineer",
|
|
2855
|
+
description: "Infrastructure, deployment, and operations",
|
|
2856
|
+
prompt: `## DevOps Mode
|
|
2857
|
+
|
|
2858
|
+
When working on infrastructure:
|
|
2859
|
+
- Check for containerization and deployment readiness
|
|
2860
|
+
- Verify CI/CD pipeline configurations
|
|
2861
|
+
- Assess monitoring and alerting setup
|
|
2862
|
+
- Look for health check endpoints
|
|
2863
|
+
- Check for graceful shutdown handling
|
|
2864
|
+
- Verify backup and disaster recovery plans
|
|
2865
|
+
- Assess secrets management
|
|
2866
|
+
- Check for resource limits and quotas`,
|
|
2867
|
+
tags: ["devops", "infrastructure", "operations"],
|
|
2868
|
+
toolPreferences: ["read", "bash", "grep", "logs", "git"]
|
|
2869
|
+
},
|
|
2870
|
+
{
|
|
2871
|
+
id: "refactorer",
|
|
2872
|
+
name: "Refactorer",
|
|
2873
|
+
description: "Code improvement and modernization",
|
|
2874
|
+
prompt: `## Refactorer Mode
|
|
2875
|
+
|
|
2876
|
+
When refactoring code:
|
|
2877
|
+
- Maintain existing behavior \u2014 tests must pass before and after
|
|
2878
|
+
- Make one change at a time, verify after each
|
|
2879
|
+
- Prefer small, focused commits
|
|
2880
|
+
- Preserve API contracts unless explicitly changing
|
|
2881
|
+
- Remove dead code and comments
|
|
2882
|
+
- Improve naming as you go
|
|
2883
|
+
- Don't mix formatting changes with logic changes
|
|
2884
|
+
- Keep performance in mind \u2014 don't regress`,
|
|
2885
|
+
tags: ["refactor", "modernization", "improvement"],
|
|
2886
|
+
toolPreferences: ["read", "edit", "test", "git", "grep"]
|
|
2887
|
+
}
|
|
2888
|
+
];
|
|
2889
|
+
|
|
2890
|
+
// src/defaults/mode-store.ts
|
|
2891
|
+
var DefaultModeStore = class {
|
|
2892
|
+
activeModeId = null;
|
|
2893
|
+
modes;
|
|
2894
|
+
configDir;
|
|
2895
|
+
constructor(config) {
|
|
2896
|
+
this.configDir = config.directory;
|
|
2897
|
+
this.modes = [...DEFAULT_MODES];
|
|
2898
|
+
}
|
|
2899
|
+
async getActiveMode() {
|
|
2900
|
+
if (!this.activeModeId) {
|
|
2901
|
+
await this.loadActiveMode();
|
|
2902
|
+
}
|
|
2903
|
+
if (!this.activeModeId) return null;
|
|
2904
|
+
return this.modes.find((m) => m.id === this.activeModeId) ?? null;
|
|
2905
|
+
}
|
|
2906
|
+
async setActiveMode(modeId) {
|
|
2907
|
+
this.activeModeId = modeId;
|
|
2908
|
+
await this.saveActiveMode();
|
|
2909
|
+
}
|
|
2910
|
+
async listModes() {
|
|
2911
|
+
return [...this.modes];
|
|
2912
|
+
}
|
|
2913
|
+
async getMode(modeId) {
|
|
2914
|
+
return this.modes.find((m) => m.id === modeId) ?? null;
|
|
2915
|
+
}
|
|
2916
|
+
async addMode(mode) {
|
|
2917
|
+
const idx = this.modes.findIndex((m) => m.id === mode.id);
|
|
2918
|
+
if (idx >= 0) {
|
|
2919
|
+
this.modes[idx] = mode;
|
|
2920
|
+
} else {
|
|
2921
|
+
this.modes.push(mode);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
async removeMode(modeId) {
|
|
2925
|
+
const builtIn = DEFAULT_MODES.find((m) => m.id === modeId);
|
|
2926
|
+
if (builtIn) {
|
|
2927
|
+
throw new Error(`Cannot remove built-in mode "${modeId}"`);
|
|
2928
|
+
}
|
|
2929
|
+
this.modes = this.modes.filter((m) => m.id !== modeId);
|
|
2930
|
+
if (this.activeModeId === modeId) {
|
|
2931
|
+
this.activeModeId = null;
|
|
2932
|
+
await this.saveActiveMode();
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
async loadActiveMode() {
|
|
2936
|
+
try {
|
|
2937
|
+
const configPath = path2.join(this.configDir, "mode.json");
|
|
2938
|
+
const content = await fsp.readFile(configPath, "utf8");
|
|
2939
|
+
const data = JSON.parse(content);
|
|
2940
|
+
this.activeModeId = data.activeMode ?? null;
|
|
2941
|
+
} catch {
|
|
2942
|
+
this.activeModeId = "default";
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
async saveActiveMode() {
|
|
2946
|
+
try {
|
|
2947
|
+
await fsp.mkdir(this.configDir, { recursive: true });
|
|
2948
|
+
const configPath = path2.join(this.configDir, "mode.json");
|
|
2949
|
+
await fsp.writeFile(
|
|
2950
|
+
configPath,
|
|
2951
|
+
JSON.stringify({ activeMode: this.activeModeId }, null, 2),
|
|
2952
|
+
"utf8"
|
|
2953
|
+
);
|
|
2954
|
+
} catch {
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
async function loadProjectModes(modesDir) {
|
|
2959
|
+
const modes = [];
|
|
2960
|
+
try {
|
|
2961
|
+
const entries = await fsp.readdir(modesDir);
|
|
2962
|
+
for (const entry of entries) {
|
|
2963
|
+
if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
|
|
2964
|
+
const filePath = path2.join(modesDir, entry);
|
|
2965
|
+
const stat4 = await fsp.stat(filePath);
|
|
2966
|
+
if (!stat4.isFile()) continue;
|
|
2967
|
+
const content = await fsp.readFile(filePath, "utf8");
|
|
2968
|
+
const id = path2.basename(entry, path2.extname(entry));
|
|
2969
|
+
modes.push({
|
|
2970
|
+
id,
|
|
2971
|
+
name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
2972
|
+
description: content.split("\n")[0] ?? id,
|
|
2973
|
+
prompt: content,
|
|
2974
|
+
tags: ["project"]
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
} catch {
|
|
2978
|
+
}
|
|
2979
|
+
return modes;
|
|
2980
|
+
}
|
|
2981
|
+
async function loadUserModes(modesDir) {
|
|
2982
|
+
const modes = [];
|
|
2983
|
+
try {
|
|
2984
|
+
const manifestPath = path2.join(modesDir, "modes.json");
|
|
2985
|
+
const content = await fsp.readFile(manifestPath, "utf8");
|
|
2986
|
+
const manifest = JSON.parse(content);
|
|
2987
|
+
for (const mode of manifest.modes) {
|
|
2988
|
+
modes.push(mode);
|
|
2989
|
+
}
|
|
2990
|
+
} catch {
|
|
2991
|
+
}
|
|
2992
|
+
return modes;
|
|
2993
|
+
}
|
|
2994
|
+
var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
2995
|
+
coordinatorId;
|
|
2996
|
+
config;
|
|
2997
|
+
subagents = /* @__PURE__ */ new Map();
|
|
2998
|
+
pendingTasks = [];
|
|
2999
|
+
completedResults = [];
|
|
3000
|
+
totalIterations = 0;
|
|
3001
|
+
constructor(config) {
|
|
3002
|
+
super();
|
|
3003
|
+
this.coordinatorId = config.coordinatorId;
|
|
3004
|
+
this.config = config;
|
|
3005
|
+
}
|
|
3006
|
+
async spawn(subagent) {
|
|
3007
|
+
const id = subagent.id || randomUUID();
|
|
3008
|
+
const context = {
|
|
3009
|
+
subagentId: id,
|
|
3010
|
+
tasks: [],
|
|
3011
|
+
parentBridge: null,
|
|
3012
|
+
doneCondition: this.config.doneCondition,
|
|
3013
|
+
maxConcurrent: this.config.maxConcurrent ?? 4
|
|
3014
|
+
};
|
|
3015
|
+
this.subagents.set(id, {
|
|
3016
|
+
config: subagent,
|
|
3017
|
+
context,
|
|
3018
|
+
status: "idle"
|
|
3019
|
+
});
|
|
3020
|
+
this.emit("subagent.started", { subagent: { ...subagent, id } });
|
|
3021
|
+
return {
|
|
3022
|
+
subagentId: id,
|
|
3023
|
+
agentId: id
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
async assign(task) {
|
|
3027
|
+
this.pendingTasks.push(task);
|
|
3028
|
+
const available = this.getAvailableSubagent();
|
|
3029
|
+
if (available) {
|
|
3030
|
+
await this.dispatch(available, task);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
async delegate(to, msg) {
|
|
3034
|
+
const subagent = this.subagents.get(to);
|
|
3035
|
+
if (!subagent) throw new Error(`Subagent "${to}" not found`);
|
|
3036
|
+
if (!subagent.context.parentBridge) {
|
|
3037
|
+
throw new Error(`Subagent "${to}" has no parentBridge \u2014 call setSubagentBridge() first`);
|
|
3038
|
+
}
|
|
3039
|
+
await subagent.context.parentBridge.send(msg);
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Wire up the communication bridge for a subagent. Call this after `spawn()`
|
|
3043
|
+
* once the caller has created the bidirectional bridge connection.
|
|
3044
|
+
*/
|
|
3045
|
+
setSubagentBridge(subagentId, bridge) {
|
|
3046
|
+
const subagent = this.subagents.get(subagentId);
|
|
3047
|
+
if (!subagent) throw new Error(`Subagent "${subagentId}" not found`);
|
|
3048
|
+
subagent.context.parentBridge = bridge;
|
|
3049
|
+
}
|
|
3050
|
+
async stop(subagentId) {
|
|
3051
|
+
const subagent = this.subagents.get(subagentId);
|
|
3052
|
+
if (!subagent) return;
|
|
3053
|
+
subagent.status = "stopped";
|
|
3054
|
+
subagent.currentTask = void 0;
|
|
3055
|
+
subagent.context.parentBridge = null;
|
|
3056
|
+
this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
|
|
3057
|
+
}
|
|
3058
|
+
async stopAll() {
|
|
3059
|
+
for (const id of this.subagents.keys()) {
|
|
3060
|
+
await this.stop(id);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
getStatus() {
|
|
3064
|
+
return {
|
|
3065
|
+
coordinatorId: this.coordinatorId,
|
|
3066
|
+
subagents: Array.from(this.subagents.entries()).map(([id, s]) => ({
|
|
3067
|
+
id,
|
|
3068
|
+
name: s.config.name,
|
|
3069
|
+
status: s.status,
|
|
3070
|
+
currentTask: s.currentTask
|
|
3071
|
+
})),
|
|
3072
|
+
pendingTasks: this.pendingTasks.length,
|
|
3073
|
+
completedTasks: this.completedResults.length,
|
|
3074
|
+
totalIterations: this.totalIterations,
|
|
3075
|
+
done: this.isDone()
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
getAvailableSubagent() {
|
|
3079
|
+
for (const [id, s] of this.subagents) {
|
|
3080
|
+
if (s.status === "idle") return id;
|
|
3081
|
+
}
|
|
3082
|
+
return null;
|
|
3083
|
+
}
|
|
3084
|
+
async dispatch(subagentId, task) {
|
|
3085
|
+
const subagent = this.subagents.get(subagentId);
|
|
3086
|
+
if (!subagent) return;
|
|
3087
|
+
subagent.status = "running";
|
|
3088
|
+
subagent.currentTask = task.id;
|
|
3089
|
+
task.subagentId = subagentId;
|
|
3090
|
+
subagent.context.tasks.push(task);
|
|
3091
|
+
if (!subagent.context.parentBridge) {
|
|
3092
|
+
this.emit("task.assigned", { task, subagentId });
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
await subagent.context.parentBridge.send({
|
|
3096
|
+
id: randomUUID(),
|
|
3097
|
+
type: "task",
|
|
3098
|
+
from: this.coordinatorId,
|
|
3099
|
+
to: subagentId,
|
|
3100
|
+
payload: task,
|
|
3101
|
+
timestamp: Date.now()
|
|
3102
|
+
});
|
|
3103
|
+
this.emit("task.assigned", { task, subagentId });
|
|
3104
|
+
}
|
|
3105
|
+
isDone() {
|
|
3106
|
+
if (this.config.doneCondition.type === "all_tasks_done") {
|
|
3107
|
+
return this.pendingTasks.length === 0 && this.completedResults.every((r) => r.status === "success");
|
|
3108
|
+
}
|
|
3109
|
+
if (this.config.doneCondition.maxIterations && this.totalIterations >= this.config.doneCondition.maxIterations) {
|
|
3110
|
+
return true;
|
|
3111
|
+
}
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3114
|
+
completeTask(result) {
|
|
3115
|
+
this.completedResults.push(result);
|
|
3116
|
+
this.totalIterations += result.iterations;
|
|
3117
|
+
const subagent = this.subagents.get(result.subagentId);
|
|
3118
|
+
if (subagent) {
|
|
3119
|
+
subagent.status = "idle";
|
|
3120
|
+
subagent.currentTask = void 0;
|
|
3121
|
+
}
|
|
3122
|
+
this.emit("task.completed", {
|
|
3123
|
+
task: this.pendingTasks.shift(),
|
|
3124
|
+
result
|
|
3125
|
+
});
|
|
3126
|
+
if (this.pendingTasks.length > 0) {
|
|
3127
|
+
const available = this.getAvailableSubagent();
|
|
3128
|
+
if (available) {
|
|
3129
|
+
const nextTask = this.pendingTasks.shift();
|
|
3130
|
+
this.dispatch(available, nextTask);
|
|
3131
|
+
}
|
|
3132
|
+
} else if (this.isDone()) {
|
|
3133
|
+
this.emit("done", {
|
|
3134
|
+
results: this.completedResults,
|
|
3135
|
+
totalIterations: this.totalIterations
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
};
|
|
3140
|
+
var InMemoryBridgeTransport = class {
|
|
3141
|
+
subs = /* @__PURE__ */ new Map();
|
|
3142
|
+
send(msg, to) {
|
|
3143
|
+
const handlers = this.subs.get(to);
|
|
3144
|
+
if (handlers) {
|
|
3145
|
+
for (const h of handlers) {
|
|
3146
|
+
try {
|
|
3147
|
+
h(msg);
|
|
3148
|
+
} catch {
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return Promise.resolve();
|
|
3153
|
+
}
|
|
3154
|
+
subscribe(agentId, handler) {
|
|
3155
|
+
if (!this.subs.has(agentId)) this.subs.set(agentId, /* @__PURE__ */ new Set());
|
|
3156
|
+
this.subs.get(agentId).add(handler);
|
|
3157
|
+
return () => this.subs.get(agentId)?.delete(handler);
|
|
3158
|
+
}
|
|
3159
|
+
close(agentId) {
|
|
3160
|
+
this.subs.delete(agentId);
|
|
3161
|
+
return Promise.resolve();
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
var InMemoryAgentBridge = class {
|
|
3165
|
+
agentId;
|
|
3166
|
+
coordinatorId;
|
|
3167
|
+
transport;
|
|
3168
|
+
subscriptions = /* @__PURE__ */ new Set();
|
|
3169
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
3170
|
+
stopped = false;
|
|
3171
|
+
timeoutMs;
|
|
3172
|
+
constructor(config, transport) {
|
|
3173
|
+
this.agentId = config.agentId;
|
|
3174
|
+
this.coordinatorId = config.coordinatorId;
|
|
3175
|
+
this.transport = transport;
|
|
3176
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
3177
|
+
this.transport.subscribe(this.agentId, (msg) => {
|
|
3178
|
+
if (msg.type === "heartbeat") return;
|
|
3179
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
3180
|
+
if (pending) {
|
|
3181
|
+
clearTimeout(pending.timer);
|
|
3182
|
+
this.pendingRequests.delete(msg.id);
|
|
3183
|
+
pending.resolve(msg);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
for (const h of this.subscriptions) {
|
|
3187
|
+
try {
|
|
3188
|
+
h(msg);
|
|
3189
|
+
} catch {
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
async send(msg) {
|
|
3195
|
+
msg.timestamp = Date.now();
|
|
3196
|
+
await this.transport.send(msg, msg.to ?? this.coordinatorId);
|
|
3197
|
+
}
|
|
3198
|
+
async broadcast(msg) {
|
|
3199
|
+
msg.timestamp = Date.now();
|
|
3200
|
+
msg.to = "*";
|
|
3201
|
+
await this.transport.send(msg, "*");
|
|
3202
|
+
}
|
|
3203
|
+
subscribe(handler) {
|
|
3204
|
+
this.subscriptions.add(handler);
|
|
3205
|
+
return () => this.subscriptions.delete(handler);
|
|
3206
|
+
}
|
|
3207
|
+
async request(msg, timeoutMs) {
|
|
3208
|
+
if (this.stopped) throw new Error("Bridge is stopped");
|
|
3209
|
+
const timeout = timeoutMs ?? this.timeoutMs;
|
|
3210
|
+
const correlationId = msg.id;
|
|
3211
|
+
return new Promise((resolve3, reject) => {
|
|
3212
|
+
const timer = setTimeout(() => {
|
|
3213
|
+
this.pendingRequests.delete(correlationId);
|
|
3214
|
+
reject(new Error(`Request ${correlationId} timed out after ${timeout}ms`));
|
|
3215
|
+
}, timeout);
|
|
3216
|
+
this.pendingRequests.set(correlationId, { resolve: resolve3, reject, timer });
|
|
3217
|
+
msg.timestamp = Date.now();
|
|
3218
|
+
this.transport.send(msg, msg.to ?? this.coordinatorId).catch((e) => {
|
|
3219
|
+
clearTimeout(timer);
|
|
3220
|
+
this.pendingRequests.delete(correlationId);
|
|
3221
|
+
reject(e);
|
|
3222
|
+
});
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
async stop() {
|
|
3226
|
+
this.stopped = true;
|
|
3227
|
+
for (const [, p] of this.pendingRequests) {
|
|
3228
|
+
clearTimeout(p.timer);
|
|
3229
|
+
}
|
|
3230
|
+
this.pendingRequests.clear();
|
|
3231
|
+
this.subscriptions.clear();
|
|
3232
|
+
await this.transport.close(this.agentId);
|
|
3233
|
+
}
|
|
3234
|
+
};
|
|
3235
|
+
function createMessage(type, from, payload, to) {
|
|
3236
|
+
return {
|
|
3237
|
+
id: randomUUID(),
|
|
3238
|
+
type,
|
|
3239
|
+
from,
|
|
3240
|
+
to,
|
|
3241
|
+
payload,
|
|
3242
|
+
timestamp: Date.now(),
|
|
3243
|
+
priority: "normal"
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
// src/defaults/autonomous-runner.ts
|
|
3248
|
+
var DoneConditionChecker = class {
|
|
3249
|
+
constructor(condition) {
|
|
3250
|
+
this.condition = condition;
|
|
3251
|
+
}
|
|
3252
|
+
condition;
|
|
3253
|
+
check(state) {
|
|
3254
|
+
switch (this.condition.type) {
|
|
3255
|
+
case "iterations":
|
|
3256
|
+
if (this.condition.maxIterations && state.iterations >= this.condition.maxIterations) {
|
|
3257
|
+
return { done: true, reason: `max iterations (${this.condition.maxIterations}) reached`, ...state };
|
|
3258
|
+
}
|
|
3259
|
+
break;
|
|
3260
|
+
case "tool_calls":
|
|
3261
|
+
if (this.condition.maxToolCalls && state.toolCalls >= this.condition.maxToolCalls) {
|
|
3262
|
+
return { done: true, reason: `max tool calls (${this.condition.maxToolCalls}) reached`, ...state };
|
|
3263
|
+
}
|
|
3264
|
+
break;
|
|
3265
|
+
case "output_match":
|
|
3266
|
+
if (this.condition.pattern && state.lastOutput) {
|
|
3267
|
+
const regex = new RegExp(this.condition.pattern);
|
|
3268
|
+
if (regex.test(state.lastOutput)) {
|
|
3269
|
+
return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
break;
|
|
3273
|
+
}
|
|
3274
|
+
return { done: false, iterations: state.iterations, toolCalls: state.toolCalls };
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
var AutonomousRunner = class {
|
|
3278
|
+
constructor(opts) {
|
|
3279
|
+
this.opts = opts;
|
|
3280
|
+
this.doneChecker = new DoneConditionChecker(opts.doneCondition);
|
|
3281
|
+
}
|
|
3282
|
+
opts;
|
|
3283
|
+
iterations = 0;
|
|
3284
|
+
toolCalls = 0;
|
|
3285
|
+
lastOutput;
|
|
3286
|
+
stopped = false;
|
|
3287
|
+
doneChecker;
|
|
3288
|
+
async run() {
|
|
3289
|
+
while (!this.stopped) {
|
|
3290
|
+
const check = this.doneChecker.check({
|
|
3291
|
+
iterations: this.iterations,
|
|
3292
|
+
toolCalls: this.toolCalls,
|
|
3293
|
+
lastOutput: this.lastOutput
|
|
3294
|
+
});
|
|
3295
|
+
if (check.done) {
|
|
3296
|
+
const result = {
|
|
3297
|
+
status: "done",
|
|
3298
|
+
iterations: this.iterations,
|
|
3299
|
+
toolCalls: this.toolCalls,
|
|
3300
|
+
reason: check.reason
|
|
3301
|
+
};
|
|
3302
|
+
this.opts.onDone?.(result);
|
|
3303
|
+
return result;
|
|
3304
|
+
}
|
|
3305
|
+
this.opts.onIteration?.({ iteration: this.iterations, toolCalls: this.toolCalls });
|
|
3306
|
+
const ctrl = new AbortController();
|
|
3307
|
+
const timeout = setTimeout(() => ctrl.abort(), this.opts.iterationTimeoutMs ?? 3e4);
|
|
3308
|
+
try {
|
|
3309
|
+
const result = await this.opts.agent.run(
|
|
3310
|
+
"",
|
|
3311
|
+
{ signal: ctrl.signal, maxIterations: 1, executionStrategy: "sequential" }
|
|
3312
|
+
);
|
|
3313
|
+
this.iterations++;
|
|
3314
|
+
this.lastOutput = result.finalText;
|
|
3315
|
+
this.toolCalls++;
|
|
3316
|
+
if (result.status === "failed" || result.status === "aborted") {
|
|
3317
|
+
const failedResult = {
|
|
3318
|
+
status: result.status,
|
|
3319
|
+
error: result.error,
|
|
3320
|
+
iterations: this.iterations,
|
|
3321
|
+
toolCalls: this.toolCalls
|
|
3322
|
+
};
|
|
3323
|
+
this.opts.onDone?.(failedResult);
|
|
3324
|
+
return failedResult;
|
|
3325
|
+
}
|
|
3326
|
+
} catch (e) {
|
|
3327
|
+
if (e.message.includes("timeout")) {
|
|
3328
|
+
const timeoutResult = {
|
|
3329
|
+
status: "failed",
|
|
3330
|
+
error: e,
|
|
3331
|
+
iterations: this.iterations,
|
|
3332
|
+
toolCalls: this.toolCalls,
|
|
3333
|
+
reason: "iteration timeout"
|
|
3334
|
+
};
|
|
3335
|
+
this.opts.onDone?.(timeoutResult);
|
|
3336
|
+
return timeoutResult;
|
|
3337
|
+
}
|
|
3338
|
+
} finally {
|
|
3339
|
+
clearTimeout(timeout);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return {
|
|
3343
|
+
status: "aborted",
|
|
3344
|
+
iterations: this.iterations,
|
|
3345
|
+
toolCalls: this.toolCalls,
|
|
3346
|
+
reason: "stopped externally"
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
stop() {
|
|
3350
|
+
this.stopped = true;
|
|
3351
|
+
}
|
|
3352
|
+
};
|
|
3353
|
+
|
|
3354
|
+
// src/defaults/spec-parser.ts
|
|
3355
|
+
var SpecParser = class {
|
|
3356
|
+
constructor(opts = {}) {
|
|
3357
|
+
this.opts = opts;
|
|
3358
|
+
}
|
|
3359
|
+
opts;
|
|
3360
|
+
parse(content) {
|
|
3361
|
+
const lines = content.split("\n");
|
|
3362
|
+
const sections = this.extractSections(lines);
|
|
3363
|
+
const requirements = this.extractRequirements(lines);
|
|
3364
|
+
return {
|
|
3365
|
+
id: crypto.randomUUID(),
|
|
3366
|
+
title: this.extractTitle(lines),
|
|
3367
|
+
version: this.extractVersion(lines),
|
|
3368
|
+
status: "draft",
|
|
3369
|
+
overview: this.extractOverview(lines),
|
|
3370
|
+
sections,
|
|
3371
|
+
requirements,
|
|
3372
|
+
createdAt: Date.now(),
|
|
3373
|
+
updatedAt: Date.now()
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
extractTitle(lines) {
|
|
3377
|
+
for (const line of lines) {
|
|
3378
|
+
const m = /^#\s+(.+)/.exec(line.trim());
|
|
3379
|
+
if (m?.[1]) return m[1];
|
|
3380
|
+
}
|
|
3381
|
+
return "Untitled Specification";
|
|
3382
|
+
}
|
|
3383
|
+
extractVersion(lines) {
|
|
3384
|
+
for (const line of lines) {
|
|
3385
|
+
const m = /version[:\s]+(\d+\.\d+\.\d+)/i.exec(line.trim());
|
|
3386
|
+
if (m?.[1]) return m[1];
|
|
3387
|
+
}
|
|
3388
|
+
return "0.0.1";
|
|
3389
|
+
}
|
|
3390
|
+
extractOverview(lines) {
|
|
3391
|
+
const overviewLines = [];
|
|
3392
|
+
let inOverview = false;
|
|
3393
|
+
let foundHeading = false;
|
|
3394
|
+
for (const line of lines) {
|
|
3395
|
+
if (/^##\s+Overview/i.test(line.trim())) {
|
|
3396
|
+
inOverview = true;
|
|
3397
|
+
foundHeading = true;
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
if (foundHeading && /^##\s+/.test(line.trim())) break;
|
|
3401
|
+
if (inOverview) overviewLines.push(line);
|
|
3402
|
+
}
|
|
3403
|
+
return overviewLines.join("\n").trim() || "No overview provided";
|
|
3404
|
+
}
|
|
3405
|
+
extractSections(lines) {
|
|
3406
|
+
const sections = [];
|
|
3407
|
+
let currentSection = null;
|
|
3408
|
+
let currentLines = [];
|
|
3409
|
+
let depth = 1;
|
|
3410
|
+
for (const line of lines) {
|
|
3411
|
+
const h2 = /^##\s+(.+)/.exec(line.trim());
|
|
3412
|
+
const h3 = /^###\s+(.+)/.exec(line.trim());
|
|
3413
|
+
if (h2) {
|
|
3414
|
+
if (currentSection && currentLines.length > 0) {
|
|
3415
|
+
sections.push({
|
|
3416
|
+
type: this.mapSectionType(currentSection.title ?? "unknown"),
|
|
3417
|
+
title: currentSection.title ?? "Unknown",
|
|
3418
|
+
level: depth,
|
|
3419
|
+
content: currentLines.join("\n").trim()
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
currentSection = { title: h2[1] ?? "Unknown" };
|
|
3423
|
+
currentLines = [];
|
|
3424
|
+
depth = 2;
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
if (h3) {
|
|
3428
|
+
currentLines.push(line);
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
if (currentSection) {
|
|
3432
|
+
currentLines.push(line);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
if (currentSection && currentLines.length > 0) {
|
|
3436
|
+
sections.push({
|
|
3437
|
+
type: this.mapSectionType(currentSection.title ?? "unknown"),
|
|
3438
|
+
title: currentSection.title ?? "Unknown",
|
|
3439
|
+
level: depth,
|
|
3440
|
+
content: currentLines.join("\n").trim()
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
return sections;
|
|
3444
|
+
}
|
|
3445
|
+
extractRequirements(lines) {
|
|
3446
|
+
const requirements = [];
|
|
3447
|
+
let inRequirements = false;
|
|
3448
|
+
let idCounter = 0;
|
|
3449
|
+
for (const line of lines) {
|
|
3450
|
+
if (/^##\s+Requirements/i.test(line.trim())) {
|
|
3451
|
+
inRequirements = true;
|
|
3452
|
+
continue;
|
|
3453
|
+
}
|
|
3454
|
+
if (inRequirements && /^##\s+/.test(line.trim())) break;
|
|
3455
|
+
if (inRequirements) {
|
|
3456
|
+
const req = this.parseRequirementLine(line, `REQ-${++idCounter}`);
|
|
3457
|
+
if (req) requirements.push(req);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
return requirements;
|
|
3461
|
+
}
|
|
3462
|
+
parseRequirementLine(line, id) {
|
|
3463
|
+
const trimmed = line.trim();
|
|
3464
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
3465
|
+
const typeMap = {
|
|
3466
|
+
"functional": "functional",
|
|
3467
|
+
"non-functional": "non-functional",
|
|
3468
|
+
"security": "security",
|
|
3469
|
+
"performance": "performance",
|
|
3470
|
+
"ux": "ux"
|
|
3471
|
+
};
|
|
3472
|
+
let type = "functional";
|
|
3473
|
+
let priority = "medium";
|
|
3474
|
+
for (const [key, val] of Object.entries(typeMap)) {
|
|
3475
|
+
if (trimmed.toLowerCase().includes(`[${key}]`)) {
|
|
3476
|
+
type = val;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
|
|
3480
|
+
priority = "critical";
|
|
3481
|
+
} else if (trimmed.includes("[high]")) {
|
|
3482
|
+
priority = "high";
|
|
3483
|
+
} else if (trimmed.includes("[low]")) {
|
|
3484
|
+
priority = "low";
|
|
3485
|
+
}
|
|
3486
|
+
return {
|
|
3487
|
+
id,
|
|
3488
|
+
type,
|
|
3489
|
+
priority,
|
|
3490
|
+
description: trimmed.replace(/\[[^\]]+\]/g, "").trim(),
|
|
3491
|
+
acceptanceCriteria: []
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
mapSectionType(title) {
|
|
3495
|
+
const t = title.toLowerCase();
|
|
3496
|
+
if (t.includes("overview")) return "overview";
|
|
3497
|
+
if (t.includes("requirement")) return "requirements";
|
|
3498
|
+
if (t.includes("architect")) return "architecture";
|
|
3499
|
+
if (t.includes("api")) return "api";
|
|
3500
|
+
if (t.includes("data")) return "data";
|
|
3501
|
+
if (t.includes("security")) return "security";
|
|
3502
|
+
if (t.includes("acceptance")) return "acceptance";
|
|
3503
|
+
return "overview";
|
|
3504
|
+
}
|
|
3505
|
+
analyze(spec) {
|
|
3506
|
+
const gaps = [];
|
|
3507
|
+
const suggestions = [];
|
|
3508
|
+
const risks = [];
|
|
3509
|
+
const hasOverview = spec.sections.some((s) => s.type === "overview");
|
|
3510
|
+
const hasRequirements = spec.sections.some((s) => s.type === "requirements");
|
|
3511
|
+
const hasAcceptance = spec.sections.some((s) => s.type === "acceptance");
|
|
3512
|
+
if (!hasOverview) gaps.push("Missing Overview section");
|
|
3513
|
+
if (!hasRequirements) gaps.push("Missing Requirements section");
|
|
3514
|
+
if (!hasAcceptance) gaps.push("Missing Acceptance Criteria section");
|
|
3515
|
+
if (spec.requirements.length === 0) {
|
|
3516
|
+
gaps.push("No requirements defined");
|
|
3517
|
+
suggestions.push("Add specific functional and non-functional requirements");
|
|
3518
|
+
}
|
|
3519
|
+
const unverifiedReqs = spec.requirements.filter((r) => r.acceptanceCriteria.length === 0);
|
|
3520
|
+
if (unverifiedReqs.length > 0) {
|
|
3521
|
+
gaps.push(`${unverifiedReqs.length} requirements without acceptance criteria`);
|
|
3522
|
+
suggestions.push("Define clear acceptance criteria for each requirement");
|
|
3523
|
+
}
|
|
3524
|
+
const criticalUnresolved = spec.requirements.filter(
|
|
3525
|
+
(r) => r.priority === "critical" && r.blockedBy && r.blockedBy.length > 0
|
|
3526
|
+
);
|
|
3527
|
+
for (const req of criticalUnresolved) {
|
|
3528
|
+
risks.push({
|
|
3529
|
+
requirement: req.id,
|
|
3530
|
+
risk: `Critical requirement blocked by ${req.blockedBy?.length} other requirements`,
|
|
3531
|
+
severity: "high"
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
const completeness = Math.round(
|
|
3535
|
+
((hasOverview ? 1 : 0) + (hasRequirements ? 1 : 0) + (hasAcceptance ? 1 : 0) + (spec.requirements.length > 0 ? 1 : 0) + (spec.sections.length > 3 ? 1 : 0)) / 5 * 100
|
|
3536
|
+
);
|
|
3537
|
+
return {
|
|
3538
|
+
specId: spec.id,
|
|
3539
|
+
completeness,
|
|
3540
|
+
coverage: {
|
|
3541
|
+
requirements: spec.requirements.length,
|
|
3542
|
+
apiEndpoints: spec.apiEndpoints?.length ?? 0,
|
|
3543
|
+
edgeCases: 0,
|
|
3544
|
+
errorHandling: 0
|
|
3545
|
+
},
|
|
3546
|
+
gaps,
|
|
3547
|
+
risks,
|
|
3548
|
+
suggestions
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
validate(spec) {
|
|
3552
|
+
const errors = [];
|
|
3553
|
+
const warnings = [];
|
|
3554
|
+
if (!spec.title.trim()) {
|
|
3555
|
+
errors.push({ path: "title", message: "Title is required" });
|
|
3556
|
+
}
|
|
3557
|
+
if (!spec.version.trim()) {
|
|
3558
|
+
errors.push({ path: "version", message: "Version is required" });
|
|
3559
|
+
}
|
|
3560
|
+
for (const req of spec.requirements) {
|
|
3561
|
+
if (!req.description.trim()) {
|
|
3562
|
+
errors.push({ path: `requirement.${req.id}`, message: "Requirement description is empty" });
|
|
3563
|
+
}
|
|
3564
|
+
if (req.acceptanceCriteria.length === 0) {
|
|
3565
|
+
warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
|
|
3569
|
+
for (const id of blockedByIds) {
|
|
3570
|
+
if (!spec.requirements.find((r) => r.id === id)) {
|
|
3571
|
+
errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
return {
|
|
3575
|
+
valid: errors.length === 0,
|
|
3576
|
+
errors,
|
|
3577
|
+
warnings
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
|
|
3582
|
+
// src/defaults/task-generator.ts
|
|
3583
|
+
var TaskGenerator = class {
|
|
3584
|
+
constructor(opts) {
|
|
3585
|
+
this.opts = opts;
|
|
3586
|
+
}
|
|
3587
|
+
opts;
|
|
3588
|
+
async generateFromSpec(spec) {
|
|
3589
|
+
const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
|
|
3590
|
+
const overview = spec.sections.find((s) => s.type === "overview");
|
|
3591
|
+
if (overview) {
|
|
3592
|
+
this.opts.taskTracker.addNode({
|
|
3593
|
+
title: `Implement ${spec.title}`,
|
|
3594
|
+
description: overview.content,
|
|
3595
|
+
type: "feature",
|
|
3596
|
+
priority: "high",
|
|
3597
|
+
status: "pending"
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
const criticalReqs = spec.requirements.filter((r) => r.priority === "critical");
|
|
3601
|
+
const highReqs = spec.requirements.filter((r) => r.priority === "high");
|
|
3602
|
+
const mediumReqs = spec.requirements.filter((r) => r.priority === "medium");
|
|
3603
|
+
const lowReqs = spec.requirements.filter((r) => r.priority === "low");
|
|
3604
|
+
for (const req of criticalReqs) {
|
|
3605
|
+
const task = this.createTaskFromRequirement(req, spec.title);
|
|
3606
|
+
this.opts.taskTracker.addNode(task);
|
|
3607
|
+
}
|
|
3608
|
+
for (const req of highReqs) {
|
|
3609
|
+
const task = this.createTaskFromRequirement(req, spec.title);
|
|
3610
|
+
this.opts.taskTracker.addNode(task);
|
|
3611
|
+
}
|
|
3612
|
+
for (const req of mediumReqs) {
|
|
3613
|
+
const task = this.createTaskFromRequirement(req, spec.title);
|
|
3614
|
+
this.opts.taskTracker.addNode(task);
|
|
3615
|
+
}
|
|
3616
|
+
for (const req of lowReqs) {
|
|
3617
|
+
const task = this.createTaskFromRequirement(req, spec.title);
|
|
3618
|
+
this.opts.taskTracker.addNode(task);
|
|
3619
|
+
}
|
|
3620
|
+
if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
|
|
3621
|
+
const apiParent = this.opts.taskTracker.addNode({
|
|
3622
|
+
title: "API Implementation",
|
|
3623
|
+
description: `Implement ${spec.apiEndpoints.length} API endpoints`,
|
|
3624
|
+
type: "feature",
|
|
3625
|
+
priority: "high",
|
|
3626
|
+
status: "pending"
|
|
3627
|
+
});
|
|
3628
|
+
for (const endpoint of spec.apiEndpoints) {
|
|
3629
|
+
const task = this.createTaskFromEndpoint(endpoint);
|
|
3630
|
+
this.opts.taskTracker.addNode({
|
|
3631
|
+
...task,
|
|
3632
|
+
parentId: apiParent.id
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
this.opts.taskTracker.addNode({
|
|
3637
|
+
title: "Write Tests",
|
|
3638
|
+
description: "Comprehensive test coverage for all features",
|
|
3639
|
+
type: "test",
|
|
3640
|
+
priority: "high",
|
|
3641
|
+
status: "pending"
|
|
3642
|
+
});
|
|
3643
|
+
this.opts.taskTracker.addNode({
|
|
3644
|
+
title: "Update Documentation",
|
|
3645
|
+
description: "Update docs for new features",
|
|
3646
|
+
type: "docs",
|
|
3647
|
+
priority: "medium",
|
|
3648
|
+
status: "pending"
|
|
3649
|
+
});
|
|
3650
|
+
return graph;
|
|
3651
|
+
}
|
|
3652
|
+
createTaskFromRequirement(req, specTitle) {
|
|
3653
|
+
const type = this.mapRequirementType(req.type);
|
|
3654
|
+
const tags = [req.type, req.priority];
|
|
3655
|
+
return {
|
|
3656
|
+
title: req.description,
|
|
3657
|
+
description: this.buildDescription(req, specTitle),
|
|
3658
|
+
type,
|
|
3659
|
+
priority: this.mapPriority(req.priority),
|
|
3660
|
+
status: "pending",
|
|
3661
|
+
specRequirementId: req.id,
|
|
3662
|
+
tags,
|
|
3663
|
+
estimateHours: this.estimateHours(req)
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
createTaskFromEndpoint(endpoint) {
|
|
3667
|
+
return {
|
|
3668
|
+
title: `${endpoint.method} ${endpoint.path}`,
|
|
3669
|
+
description: endpoint.description,
|
|
3670
|
+
type: "feature",
|
|
3671
|
+
priority: "high",
|
|
3672
|
+
status: "pending",
|
|
3673
|
+
tags: [endpoint.method],
|
|
3674
|
+
estimateHours: this.estimateForEndpoint(endpoint)
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
buildDescription(req, specTitle) {
|
|
3678
|
+
const lines = [
|
|
3679
|
+
req.description,
|
|
3680
|
+
"",
|
|
3681
|
+
"**Type:** " + req.type,
|
|
3682
|
+
"**Priority:** " + req.priority
|
|
3683
|
+
];
|
|
3684
|
+
if (req.acceptanceCriteria.length > 0) {
|
|
3685
|
+
lines.push("", "**Acceptance Criteria:**");
|
|
3686
|
+
for (const criterion of req.acceptanceCriteria) {
|
|
3687
|
+
lines.push(`- ${criterion}`);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
if (req.blockedBy && req.blockedBy.length > 0) {
|
|
3691
|
+
lines.push("", `**Blocked by:** ${req.blockedBy.join(", ")}`);
|
|
3692
|
+
}
|
|
3693
|
+
return lines.join("\n");
|
|
3694
|
+
}
|
|
3695
|
+
mapRequirementType(type) {
|
|
3696
|
+
switch (type) {
|
|
3697
|
+
case "functional":
|
|
3698
|
+
return "feature";
|
|
3699
|
+
case "non-functional":
|
|
3700
|
+
return "feature";
|
|
3701
|
+
case "security":
|
|
3702
|
+
return "feature";
|
|
3703
|
+
case "performance":
|
|
3704
|
+
return "feature";
|
|
3705
|
+
case "ux":
|
|
3706
|
+
return "feature";
|
|
3707
|
+
default:
|
|
3708
|
+
return "feature";
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
mapPriority(priority) {
|
|
3712
|
+
switch (priority) {
|
|
3713
|
+
case "critical":
|
|
3714
|
+
return "critical";
|
|
3715
|
+
case "high":
|
|
3716
|
+
return "high";
|
|
3717
|
+
case "medium":
|
|
3718
|
+
return "medium";
|
|
3719
|
+
case "low":
|
|
3720
|
+
return "low";
|
|
3721
|
+
default:
|
|
3722
|
+
return "medium";
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
estimateHours(req) {
|
|
3726
|
+
switch (req.priority) {
|
|
3727
|
+
case "critical":
|
|
3728
|
+
return 8;
|
|
3729
|
+
case "high":
|
|
3730
|
+
return 4;
|
|
3731
|
+
case "medium":
|
|
3732
|
+
return 2;
|
|
3733
|
+
case "low":
|
|
3734
|
+
return 1;
|
|
3735
|
+
default:
|
|
3736
|
+
return 2;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
estimateForEndpoint(endpoint) {
|
|
3740
|
+
let hours = 2;
|
|
3741
|
+
if (endpoint.auth) hours += 1;
|
|
3742
|
+
if (endpoint.request) hours += 1;
|
|
3743
|
+
return hours;
|
|
3744
|
+
}
|
|
3745
|
+
async generateSubtasks(parentTaskId, spec) {
|
|
3746
|
+
const reqId = this.opts.taskTracker.getNode(parentTaskId)?.specRequirementId;
|
|
3747
|
+
if (!reqId) return;
|
|
3748
|
+
const req = spec.requirements.find((r) => r.id === reqId);
|
|
3749
|
+
if (!req) return;
|
|
3750
|
+
if (req.acceptanceCriteria.length > 0) {
|
|
3751
|
+
for (const criterion of req.acceptanceCriteria) {
|
|
3752
|
+
this.opts.taskTracker.addNode({
|
|
3753
|
+
title: criterion,
|
|
3754
|
+
description: `Verify: ${criterion}`,
|
|
3755
|
+
type: "test",
|
|
3756
|
+
priority: "medium",
|
|
3757
|
+
status: "pending",
|
|
3758
|
+
parentId: parentTaskId
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
};
|
|
3764
|
+
var DefaultTaskStore = class {
|
|
3765
|
+
graphs = /* @__PURE__ */ new Map();
|
|
3766
|
+
async saveGraph(graph) {
|
|
3767
|
+
this.graphs.set(graph.id, this.cloneGraph(graph));
|
|
3768
|
+
}
|
|
3769
|
+
async loadGraph(id) {
|
|
3770
|
+
const g = this.graphs.get(id);
|
|
3771
|
+
return g ? this.cloneGraph(g) : null;
|
|
3772
|
+
}
|
|
3773
|
+
async listGraphs() {
|
|
3774
|
+
return Array.from(this.graphs.values()).map((g) => ({
|
|
3775
|
+
id: g.id,
|
|
3776
|
+
title: g.title,
|
|
3777
|
+
updatedAt: g.updatedAt
|
|
3778
|
+
}));
|
|
3779
|
+
}
|
|
3780
|
+
async deleteGraph(id) {
|
|
3781
|
+
this.graphs.delete(id);
|
|
3782
|
+
}
|
|
3783
|
+
cloneGraph(g) {
|
|
3784
|
+
return {
|
|
3785
|
+
...g,
|
|
3786
|
+
nodes: new Map(g.nodes),
|
|
3787
|
+
edges: [...g.edges],
|
|
3788
|
+
rootNodes: [...g.rootNodes]
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
|
|
3793
|
+
// src/types/task-graph.ts
|
|
3794
|
+
function computeTaskProgress(graph) {
|
|
3795
|
+
const nodes = Array.from(graph.nodes.values());
|
|
3796
|
+
const total = nodes.length;
|
|
3797
|
+
const completed = nodes.filter((n) => n.status === "completed").length;
|
|
3798
|
+
const pending = nodes.filter((n) => n.status === "pending").length;
|
|
3799
|
+
const inProgress = nodes.filter((n) => n.status === "in_progress").length;
|
|
3800
|
+
const blocked = nodes.filter((n) => n.status === "blocked").length;
|
|
3801
|
+
const failed = nodes.filter((n) => n.status === "failed").length;
|
|
3802
|
+
const review = nodes.filter((n) => n.status === "review").length;
|
|
3803
|
+
const estimatedHours = nodes.reduce((sum, n) => sum + (n.estimateHours ?? 0), 0);
|
|
3804
|
+
const actualHours = nodes.reduce((sum, n) => sum + (n.actualHours ?? 0), 0);
|
|
3805
|
+
return {
|
|
3806
|
+
total,
|
|
3807
|
+
pending,
|
|
3808
|
+
inProgress,
|
|
3809
|
+
blocked,
|
|
3810
|
+
failed,
|
|
3811
|
+
review,
|
|
3812
|
+
completed,
|
|
3813
|
+
percentComplete: total > 0 ? Math.round(completed / total * 100) : 0,
|
|
3814
|
+
estimatedHours,
|
|
3815
|
+
actualHours
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// src/defaults/task-tracker.ts
|
|
3820
|
+
var TaskTracker = class {
|
|
3821
|
+
constructor(opts) {
|
|
3822
|
+
this.opts = opts;
|
|
3823
|
+
}
|
|
3824
|
+
opts;
|
|
3825
|
+
graph = null;
|
|
3826
|
+
transitions = [];
|
|
3827
|
+
async createGraph(specId, title) {
|
|
3828
|
+
this.graph = {
|
|
3829
|
+
id: crypto.randomUUID(),
|
|
3830
|
+
specId,
|
|
3831
|
+
title,
|
|
3832
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
3833
|
+
edges: [],
|
|
3834
|
+
rootNodes: [],
|
|
3835
|
+
createdAt: Date.now(),
|
|
3836
|
+
updatedAt: Date.now()
|
|
3837
|
+
};
|
|
3838
|
+
await this.opts.store.saveGraph(this.graph);
|
|
3839
|
+
return this.graph;
|
|
3840
|
+
}
|
|
3841
|
+
async loadGraph(id) {
|
|
3842
|
+
this.graph = await this.opts.store.loadGraph(id);
|
|
3843
|
+
return this.graph;
|
|
3844
|
+
}
|
|
3845
|
+
addNode(node) {
|
|
3846
|
+
if (!this.graph) throw new Error("No graph loaded");
|
|
3847
|
+
const now = Date.now();
|
|
3848
|
+
const newNode = {
|
|
3849
|
+
...node,
|
|
3850
|
+
id: crypto.randomUUID(),
|
|
3851
|
+
status: node.status ?? "pending",
|
|
3852
|
+
createdAt: now,
|
|
3853
|
+
updatedAt: now
|
|
3854
|
+
};
|
|
3855
|
+
this.graph.nodes.set(newNode.id, newNode);
|
|
3856
|
+
if (!node.parentId) {
|
|
3857
|
+
this.graph.rootNodes.push(newNode.id);
|
|
3858
|
+
}
|
|
3859
|
+
this.graph.updatedAt = now;
|
|
3860
|
+
this.opts.store.saveGraph(this.graph);
|
|
3861
|
+
return newNode;
|
|
3862
|
+
}
|
|
3863
|
+
addEdge(from, to, type = "depends_on") {
|
|
3864
|
+
if (!this.graph) throw new Error("No graph loaded");
|
|
3865
|
+
const edge = {
|
|
3866
|
+
id: crypto.randomUUID(),
|
|
3867
|
+
from,
|
|
3868
|
+
to,
|
|
3869
|
+
type
|
|
3870
|
+
};
|
|
3871
|
+
this.graph.edges.push(edge);
|
|
3872
|
+
this.graph.updatedAt = Date.now();
|
|
3873
|
+
this.opts.store.saveGraph(this.graph);
|
|
3874
|
+
}
|
|
3875
|
+
updateNodeStatus(id, status, reason) {
|
|
3876
|
+
if (!this.graph) throw new Error("No graph loaded");
|
|
3877
|
+
const node = this.graph.nodes.get(id);
|
|
3878
|
+
if (!node) throw new Error(`Node ${id} not found`);
|
|
3879
|
+
const from = node.status;
|
|
3880
|
+
node.status = status;
|
|
3881
|
+
node.updatedAt = Date.now();
|
|
3882
|
+
if (status === "completed") {
|
|
3883
|
+
node.completedAt = Date.now();
|
|
3884
|
+
}
|
|
3885
|
+
this.transitions.push({ from, to: status, timestamp: Date.now(), reason });
|
|
3886
|
+
if (status === "completed") {
|
|
3887
|
+
this.unblockDependents(id);
|
|
3888
|
+
}
|
|
3889
|
+
if (status === "in_progress") {
|
|
3890
|
+
this.checkAndBlockIfNeeded(id);
|
|
3891
|
+
}
|
|
3892
|
+
this.graph.updatedAt = Date.now();
|
|
3893
|
+
this.opts.store.saveGraph(this.graph);
|
|
3894
|
+
}
|
|
3895
|
+
getNode(id) {
|
|
3896
|
+
return this.graph?.nodes.get(id);
|
|
3897
|
+
}
|
|
3898
|
+
getAllNodes(filter, sort) {
|
|
3899
|
+
if (!this.graph) return [];
|
|
3900
|
+
let nodes = Array.from(this.graph.nodes.values());
|
|
3901
|
+
if (filter) {
|
|
3902
|
+
nodes = nodes.filter((n) => {
|
|
3903
|
+
if (filter.status?.length && !filter.status.includes(n.status)) return false;
|
|
3904
|
+
if (filter.priority?.length && !filter.priority.includes(n.priority)) return false;
|
|
3905
|
+
if (filter.type?.length && !filter.type.includes(n.type)) return false;
|
|
3906
|
+
if (filter.assignee?.length && n.assignee && !filter.assignee.includes(n.assignee)) return false;
|
|
3907
|
+
if (filter.tags?.length && n.tags && !n.tags.some((t) => filter.tags.includes(t))) return false;
|
|
3908
|
+
if (filter.specRequirementId && n.specRequirementId !== filter.specRequirementId) return false;
|
|
3909
|
+
return true;
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
if (sort) {
|
|
3913
|
+
nodes.sort((a, b) => {
|
|
3914
|
+
const aVal = a[sort.field] ?? "";
|
|
3915
|
+
const bVal = b[sort.field] ?? "";
|
|
3916
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
3917
|
+
return sort.direction === "asc" ? cmp : -cmp;
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
return nodes;
|
|
3921
|
+
}
|
|
3922
|
+
getChildren(parentId) {
|
|
3923
|
+
if (!this.graph) return [];
|
|
3924
|
+
return Array.from(this.graph.nodes.values()).filter((n) => n.parentId === parentId);
|
|
3925
|
+
}
|
|
3926
|
+
getDependents(taskId) {
|
|
3927
|
+
if (!this.graph) return [];
|
|
3928
|
+
return this.graph.edges.filter((e) => e.from === taskId && e.type === "depends_on").map((e) => e.to);
|
|
3929
|
+
}
|
|
3930
|
+
getBlockers(taskId) {
|
|
3931
|
+
if (!this.graph) return [];
|
|
3932
|
+
return this.graph.edges.filter((e) => e.to === taskId && e.type === "depends_on").map((e) => e.from);
|
|
3933
|
+
}
|
|
3934
|
+
canStart(taskId) {
|
|
3935
|
+
const blockers = this.getBlockers(taskId);
|
|
3936
|
+
return blockers.every((id) => {
|
|
3937
|
+
const node = this.graph?.nodes.get(id);
|
|
3938
|
+
return node?.status === "completed";
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
getProgress() {
|
|
3942
|
+
if (!this.graph) {
|
|
3943
|
+
return {
|
|
3944
|
+
total: 0,
|
|
3945
|
+
pending: 0,
|
|
3946
|
+
inProgress: 0,
|
|
3947
|
+
blocked: 0,
|
|
3948
|
+
failed: 0,
|
|
3949
|
+
review: 0,
|
|
3950
|
+
completed: 0,
|
|
3951
|
+
percentComplete: 0,
|
|
3952
|
+
estimatedHours: 0,
|
|
3953
|
+
actualHours: 0
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
return computeTaskProgress(this.graph);
|
|
3957
|
+
}
|
|
3958
|
+
getTransitions(taskId) {
|
|
3959
|
+
if (!taskId) return [...this.transitions];
|
|
3960
|
+
return [...this.transitions];
|
|
3961
|
+
}
|
|
3962
|
+
unblockDependents(completedId) {
|
|
3963
|
+
if (!this.graph) return;
|
|
3964
|
+
const dependents = this.getDependents(completedId);
|
|
3965
|
+
for (const depId of dependents) {
|
|
3966
|
+
const dep = this.graph.nodes.get(depId);
|
|
3967
|
+
if (dep?.status === "blocked") {
|
|
3968
|
+
const remainingBlockers = this.getBlockers(depId);
|
|
3969
|
+
const allUnblocked = remainingBlockers.every((id) => {
|
|
3970
|
+
const blocker = this.graph?.nodes.get(id);
|
|
3971
|
+
return blocker?.status === "completed";
|
|
3972
|
+
});
|
|
3973
|
+
if (allUnblocked) {
|
|
3974
|
+
dep.status = "pending";
|
|
3975
|
+
dep.updatedAt = Date.now();
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
checkAndBlockIfNeeded(taskId) {
|
|
3981
|
+
if (!this.graph) return;
|
|
3982
|
+
const blockers = this.getBlockers(taskId);
|
|
3983
|
+
const someBlocked = blockers.some((id) => {
|
|
3984
|
+
const blocker = this.graph?.nodes.get(id);
|
|
3985
|
+
return blocker?.status !== "completed";
|
|
3986
|
+
});
|
|
3987
|
+
if (someBlocked) {
|
|
3988
|
+
const node = this.graph.nodes.get(taskId);
|
|
3989
|
+
if (node) {
|
|
3990
|
+
node.status = "blocked";
|
|
3991
|
+
node.updatedAt = Date.now();
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
|
|
3997
|
+
// src/defaults/task-flow.ts
|
|
3998
|
+
var TaskFlow = class {
|
|
3999
|
+
constructor(opts) {
|
|
4000
|
+
this.opts = opts;
|
|
4001
|
+
this.setPhase("idle");
|
|
4002
|
+
}
|
|
4003
|
+
opts;
|
|
4004
|
+
phase = "idle";
|
|
4005
|
+
spec = null;
|
|
4006
|
+
graph = null;
|
|
4007
|
+
stopped = false;
|
|
4008
|
+
emit(event, payload) {
|
|
4009
|
+
this.opts.events.emit(event, payload);
|
|
4010
|
+
}
|
|
4011
|
+
async fromSpec(specContent) {
|
|
4012
|
+
this.setPhase("parsing");
|
|
4013
|
+
const parser = new SpecParser();
|
|
4014
|
+
this.spec = parser.parse(specContent);
|
|
4015
|
+
this.setPhase("analyzing");
|
|
4016
|
+
const analysis = parser.analyze(this.spec);
|
|
4017
|
+
this.emit("spec.analyzed", { analysis });
|
|
4018
|
+
if (analysis.completeness < 50) {
|
|
4019
|
+
this.emit("error", {
|
|
4020
|
+
phase: "analyzing",
|
|
4021
|
+
error: new Error(`Spec completeness too low: ${analysis.completeness}%`)
|
|
4022
|
+
});
|
|
4023
|
+
this.setPhase("failed");
|
|
4024
|
+
throw new Error("Spec too incomplete");
|
|
4025
|
+
}
|
|
4026
|
+
this.setPhase("generating");
|
|
4027
|
+
const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
|
|
4028
|
+
this.graph = await generator.generateFromSpec(this.spec);
|
|
4029
|
+
return this.graph;
|
|
4030
|
+
}
|
|
4031
|
+
async execute(ctx) {
|
|
4032
|
+
if (!this.graph) throw new Error("No graph loaded. Call fromSpec first.");
|
|
4033
|
+
this.setPhase("executing");
|
|
4034
|
+
this.stopped = false;
|
|
4035
|
+
const pendingTasks = this.getExecutableTasks();
|
|
4036
|
+
const maxConcurrent = this.opts.maxConcurrent ?? 2;
|
|
4037
|
+
while (pendingTasks.length > 0 && !this.stopped) {
|
|
4038
|
+
const batch = pendingTasks.splice(0, maxConcurrent);
|
|
4039
|
+
const results = await Promise.allSettled(
|
|
4040
|
+
batch.map((task) => this.executeSingleTask(task, ctx))
|
|
4041
|
+
);
|
|
4042
|
+
for (let i = 0; i < results.length; i++) {
|
|
4043
|
+
const result = results[i];
|
|
4044
|
+
const task = batch[i];
|
|
4045
|
+
if (!result || !task) continue;
|
|
4046
|
+
if (result.status === "rejected") {
|
|
4047
|
+
this.opts.tracker.updateNodeStatus(task.id, "failed", result.reason?.message);
|
|
4048
|
+
this.emit("task.failed", { taskId: task.id, error: result.reason?.message ?? "unknown" });
|
|
4049
|
+
ctx.onTaskFail?.(task, result.reason);
|
|
4050
|
+
} else {
|
|
4051
|
+
this.opts.tracker.updateNodeStatus(task.id, "completed");
|
|
4052
|
+
this.emit("task.completed", { taskId: task.id, result: result.value });
|
|
4053
|
+
ctx.onTaskComplete?.(task, result.value);
|
|
4054
|
+
}
|
|
4055
|
+
this.emitProgress();
|
|
4056
|
+
}
|
|
4057
|
+
const stillPending = this.getExecutableTasks();
|
|
4058
|
+
pendingTasks.length = 0;
|
|
4059
|
+
pendingTasks.push(...stillPending);
|
|
4060
|
+
if (this.checkDoneCondition()) {
|
|
4061
|
+
break;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
this.setPhase("completing");
|
|
4065
|
+
this.emit("done", { graph: this.graph });
|
|
4066
|
+
this.setPhase("done");
|
|
4067
|
+
return this.graph;
|
|
4068
|
+
}
|
|
4069
|
+
async reviewTask(taskId, approved, comment) {
|
|
4070
|
+
const task = this.opts.tracker.getNode(taskId);
|
|
4071
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
4072
|
+
if (approved) {
|
|
4073
|
+
this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
|
|
4074
|
+
this.emit("task.completed", { taskId });
|
|
4075
|
+
} else {
|
|
4076
|
+
this.opts.tracker.updateNodeStatus(taskId, "in_progress", comment ?? "Needs revision");
|
|
4077
|
+
this.emit("task.review", { taskId });
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
stop() {
|
|
4081
|
+
this.stopped = true;
|
|
4082
|
+
}
|
|
4083
|
+
getPhase() {
|
|
4084
|
+
return this.phase;
|
|
4085
|
+
}
|
|
4086
|
+
getGraph() {
|
|
4087
|
+
return this.graph;
|
|
4088
|
+
}
|
|
4089
|
+
getSpec() {
|
|
4090
|
+
return this.spec;
|
|
4091
|
+
}
|
|
4092
|
+
setPhase(phase) {
|
|
4093
|
+
const from = this.phase;
|
|
4094
|
+
this.phase = phase;
|
|
4095
|
+
this.emit("phase.change", { from, to: phase });
|
|
4096
|
+
}
|
|
4097
|
+
getExecutableTasks() {
|
|
4098
|
+
return this.opts.tracker.getAllNodes({ status: ["pending", "blocked"] }).filter((n) => n.status === "pending" && this.opts.tracker.canStart(n.id)).sort((a, b) => {
|
|
4099
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4100
|
+
return (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4);
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
async executeSingleTask(task, ctx) {
|
|
4104
|
+
this.opts.tracker.updateNodeStatus(task.id, "in_progress");
|
|
4105
|
+
this.emit("task.started", { taskId: task.id });
|
|
4106
|
+
return ctx.executeTask(task);
|
|
4107
|
+
}
|
|
4108
|
+
checkDoneCondition() {
|
|
4109
|
+
const condition = this.opts.doneCondition;
|
|
4110
|
+
if (!condition) {
|
|
4111
|
+
const progress = this.opts.tracker.getProgress();
|
|
4112
|
+
return progress.percentComplete === 100;
|
|
4113
|
+
}
|
|
4114
|
+
switch (condition.type) {
|
|
4115
|
+
case "all_tasks_done": {
|
|
4116
|
+
const progress = this.opts.tracker.getProgress();
|
|
4117
|
+
return progress.pending === 0 && progress.inProgress === 0;
|
|
4118
|
+
}
|
|
4119
|
+
case "iterations":
|
|
4120
|
+
return false;
|
|
4121
|
+
// Not tracked here
|
|
4122
|
+
case "tool_calls":
|
|
4123
|
+
return false;
|
|
4124
|
+
default:
|
|
4125
|
+
return false;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
emitProgress() {
|
|
4129
|
+
const progress = this.opts.tracker.getProgress();
|
|
4130
|
+
this.emit("progress", {
|
|
4131
|
+
percent: progress.percentComplete,
|
|
4132
|
+
message: `${progress.completed}/${progress.total} tasks completed`
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
};
|
|
4136
|
+
var SpecDrivenDev = class {
|
|
4137
|
+
store;
|
|
4138
|
+
tracker;
|
|
4139
|
+
events;
|
|
4140
|
+
flows = /* @__PURE__ */ new Map();
|
|
4141
|
+
constructor(opts) {
|
|
4142
|
+
this.store = new DefaultTaskStore();
|
|
4143
|
+
this.tracker = new TaskTracker({ store: this.store });
|
|
4144
|
+
this.events = opts.events;
|
|
4145
|
+
}
|
|
4146
|
+
async createFlow(specContent, options) {
|
|
4147
|
+
const flow = new TaskFlow({
|
|
4148
|
+
tracker: this.tracker,
|
|
4149
|
+
events: this.events,
|
|
4150
|
+
...options
|
|
4151
|
+
});
|
|
4152
|
+
const graph = await flow.fromSpec(specContent);
|
|
4153
|
+
this.flows.set(graph.id, flow);
|
|
4154
|
+
return flow;
|
|
4155
|
+
}
|
|
4156
|
+
getTracker() {
|
|
4157
|
+
return this.tracker;
|
|
4158
|
+
}
|
|
4159
|
+
getFlow(graphId) {
|
|
4160
|
+
return this.flows.get(graphId);
|
|
4161
|
+
}
|
|
4162
|
+
listFlows() {
|
|
4163
|
+
return Array.from(this.flows.entries()).map(([id, flow]) => ({
|
|
4164
|
+
id,
|
|
4165
|
+
title: flow.getGraph()?.title ?? "Untitled",
|
|
4166
|
+
phase: flow.getPhase()
|
|
4167
|
+
}));
|
|
4168
|
+
}
|
|
4169
|
+
};
|
|
4170
|
+
|
|
4171
|
+
// src/utils/tool-output-serializer.ts
|
|
4172
|
+
function createToolOutputSerializer(opts = {}) {
|
|
4173
|
+
const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
|
|
4174
|
+
function serialize(value) {
|
|
4175
|
+
if (typeof value === "string") return value;
|
|
4176
|
+
if (value === null || value === void 0) return "";
|
|
4177
|
+
if (typeof value === "object") {
|
|
4178
|
+
if (Array.isArray(value)) return value.map(serialize).join("\n");
|
|
4179
|
+
if ("text" in value) {
|
|
4180
|
+
const t = value.text;
|
|
4181
|
+
return typeof t === "string" ? t : JSON.stringify(value, null, 2);
|
|
4182
|
+
}
|
|
4183
|
+
try {
|
|
4184
|
+
return JSON.stringify(value, null, 2);
|
|
4185
|
+
} catch {
|
|
4186
|
+
return String(value);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
return String(value);
|
|
4190
|
+
}
|
|
4191
|
+
function enforceCap(text, remainingBudget) {
|
|
4192
|
+
if (remainingBudget <= 0) {
|
|
4193
|
+
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
4194
|
+
}
|
|
4195
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
4196
|
+
if (textBytes <= remainingBudget) {
|
|
4197
|
+
return { text, newBudget: remainingBudget - textBytes };
|
|
4198
|
+
}
|
|
4199
|
+
const marker = `
|
|
4200
|
+
\u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
|
|
4201
|
+
`;
|
|
4202
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
4203
|
+
const available = remainingBudget - markerBytes;
|
|
4204
|
+
if (available <= 0) {
|
|
4205
|
+
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
4206
|
+
}
|
|
4207
|
+
const half = Math.floor(available / 2);
|
|
4208
|
+
const first = text.slice(0, half);
|
|
4209
|
+
Buffer.byteLength(first, "utf8");
|
|
4210
|
+
const second = text.slice(text.length - half);
|
|
4211
|
+
return { text: `${first}${marker}${second}`, newBudget: 0 };
|
|
4212
|
+
}
|
|
4213
|
+
return { serialize, enforceCap, capBytes };
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// src/defaults/tool-executor.ts
|
|
4217
|
+
var ToolExecutor = class {
|
|
4218
|
+
constructor(registry, opts) {
|
|
4219
|
+
this.registry = registry;
|
|
4220
|
+
this.opts = opts;
|
|
4221
|
+
this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
|
|
4222
|
+
this.serializer = createToolOutputSerializer({
|
|
4223
|
+
perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
registry;
|
|
4227
|
+
opts;
|
|
4228
|
+
serializer;
|
|
4229
|
+
iterationTimeoutMs;
|
|
4230
|
+
/**
|
|
4231
|
+
* Execute a batch of tool uses using the configured strategy.
|
|
4232
|
+
* Returns the execution results and the remaining output budget.
|
|
4233
|
+
*/
|
|
4234
|
+
async executeBatch(toolUses, ctx, strategy) {
|
|
4235
|
+
let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
|
|
4236
|
+
const runOne = async (use, index) => {
|
|
4237
|
+
const start = Date.now();
|
|
4238
|
+
const tool = this.registry.get(use.name);
|
|
4239
|
+
let result;
|
|
4240
|
+
if (!tool) {
|
|
4241
|
+
result = this.unknownToolResult(use, () => this.registry.list().map((t) => t.name));
|
|
4242
|
+
} else {
|
|
4243
|
+
const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
|
|
4244
|
+
if (decision.permission === "deny") {
|
|
4245
|
+
result = this.deniedResult(use, decision.reason);
|
|
4246
|
+
} else if (decision.permission === "confirm") {
|
|
4247
|
+
result = this.confirmResult(use);
|
|
4248
|
+
} else {
|
|
4249
|
+
try {
|
|
4250
|
+
result = await this.executeTool(tool, use, ctx, budget);
|
|
4251
|
+
} catch (err) {
|
|
4252
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4253
|
+
const scrubbed = this.opts.secretScrubber.scrub(msg);
|
|
4254
|
+
this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
|
|
4255
|
+
result = {
|
|
4256
|
+
type: "tool_result",
|
|
4257
|
+
tool_use_id: use.id,
|
|
4258
|
+
content: `Tool "${use.name}" threw: ${scrubbed}`,
|
|
4259
|
+
is_error: true
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
|
|
4265
|
+
budget = Math.max(0, budget - contentBytes);
|
|
4266
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
4267
|
+
};
|
|
4268
|
+
if (strategy === "sequential") {
|
|
4269
|
+
const outputs = [];
|
|
4270
|
+
for (let i = 0; i < toolUses.length; i++) {
|
|
4271
|
+
const use = toolUses[i];
|
|
4272
|
+
if (use) outputs.push(await runOne(use));
|
|
4273
|
+
}
|
|
4274
|
+
return { outputs, remainingBudget: budget };
|
|
4275
|
+
}
|
|
4276
|
+
if (strategy === "parallel") {
|
|
4277
|
+
const outputs = await Promise.all(toolUses.map((use, i) => runOne(use)));
|
|
4278
|
+
return { outputs, remainingBudget: budget };
|
|
4279
|
+
}
|
|
4280
|
+
const nonMutating = [];
|
|
4281
|
+
const mutating = [];
|
|
4282
|
+
for (let i = 0; i < toolUses.length; i++) {
|
|
4283
|
+
const use = toolUses[i];
|
|
4284
|
+
if (!use) continue;
|
|
4285
|
+
const tool = this.registry.get(use.name);
|
|
4286
|
+
if (tool?.mutating) mutating.push({ use, index: i });
|
|
4287
|
+
else nonMutating.push({ use, index: i });
|
|
4288
|
+
}
|
|
4289
|
+
const firstPass = await Promise.all(nonMutating.map(({ use, index }) => runOne(use)));
|
|
4290
|
+
const secondPass = [];
|
|
4291
|
+
for (const { use, index } of mutating) {
|
|
4292
|
+
secondPass.push(await runOne(use));
|
|
4293
|
+
}
|
|
4294
|
+
return {
|
|
4295
|
+
outputs: [...firstPass, ...secondPass],
|
|
4296
|
+
remainingBudget: budget
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
/**
|
|
4300
|
+
* Execute a single tool with timeout, permission check, and output capping.
|
|
4301
|
+
* Emits `tool.started` via the injected EventBus (if any) right before
|
|
4302
|
+
* invoking the tool — closes the observability gap between "model decided
|
|
4303
|
+
* to call a tool" and "tool.executed".
|
|
4304
|
+
*/
|
|
4305
|
+
async executeTool(tool, use, ctx, budget) {
|
|
4306
|
+
this.opts.events?.emit("tool.started", {
|
|
4307
|
+
name: tool.name,
|
|
4308
|
+
id: use.id,
|
|
4309
|
+
input: use.input
|
|
4310
|
+
});
|
|
4311
|
+
this.opts.renderer?.writeToolCall(tool.name, use.input);
|
|
4312
|
+
const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx);
|
|
4313
|
+
const text = this.serializer.serialize(output);
|
|
4314
|
+
const scrubbed = this.opts.secretScrubber.scrub(text);
|
|
4315
|
+
const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
|
|
4316
|
+
this.opts.renderer?.writeToolResult(tool.name, capped, false);
|
|
4317
|
+
return {
|
|
4318
|
+
type: "tool_result",
|
|
4319
|
+
tool_use_id: use.id,
|
|
4320
|
+
name: tool.name,
|
|
4321
|
+
content: capped,
|
|
4322
|
+
is_error: false
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
async runWithTimeout(tool, input, parentSignal, ctx) {
|
|
4326
|
+
if (parentSignal.aborted) {
|
|
4327
|
+
throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
|
|
4328
|
+
}
|
|
4329
|
+
const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
|
|
4330
|
+
const ctrl = new AbortController();
|
|
4331
|
+
const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
|
|
4332
|
+
const combined = anySignal([parentSignal, ctrl.signal]);
|
|
4333
|
+
try {
|
|
4334
|
+
return await tool.execute(input, ctx, { signal: combined });
|
|
4335
|
+
} finally {
|
|
4336
|
+
clearTimeout(timer);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
unknownToolResult(use, listFns) {
|
|
4340
|
+
return {
|
|
4341
|
+
type: "tool_result",
|
|
4342
|
+
tool_use_id: use.id,
|
|
4343
|
+
content: `Tool "${use.name}" is not registered. Available tools: ${listFns().join(", ")}`,
|
|
4344
|
+
is_error: true
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
deniedResult(use, reason) {
|
|
4348
|
+
return {
|
|
4349
|
+
type: "tool_result",
|
|
4350
|
+
tool_use_id: use.id,
|
|
4351
|
+
content: `Tool "${use.name}" denied: ${reason ?? "policy"}`,
|
|
4352
|
+
is_error: true
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
confirmResult(use) {
|
|
4356
|
+
return {
|
|
4357
|
+
type: "tool_result",
|
|
4358
|
+
tool_use_id: use.id,
|
|
4359
|
+
content: `Tool "${use.name}" requires user confirmation but no prompt handler was available.`,
|
|
4360
|
+
is_error: true
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
};
|
|
4364
|
+
function anySignal(signals) {
|
|
4365
|
+
if (typeof AbortSignal.any === "function") {
|
|
4366
|
+
return AbortSignal.any(signals);
|
|
4367
|
+
}
|
|
4368
|
+
const ctrl = new AbortController();
|
|
4369
|
+
const abortSources = [];
|
|
4370
|
+
for (const s of signals) {
|
|
4371
|
+
if (s.aborted) {
|
|
4372
|
+
ctrl.abort(s.reason);
|
|
4373
|
+
return ctrl.signal;
|
|
4374
|
+
}
|
|
4375
|
+
abortSources.push(s);
|
|
4376
|
+
}
|
|
4377
|
+
for (const s of abortSources) {
|
|
4378
|
+
s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
|
|
4379
|
+
}
|
|
4380
|
+
return ctrl.signal;
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// src/defaults/context-manager.ts
|
|
4384
|
+
var CONTEXT_MANAGER_TOOL_NAME = "context_manager";
|
|
4385
|
+
function roughEstimate(messages) {
|
|
4386
|
+
let total = 0;
|
|
4387
|
+
for (const m of messages) {
|
|
4388
|
+
if (typeof m.content === "string") {
|
|
4389
|
+
total += Math.ceil(m.content.length / 4);
|
|
4390
|
+
} else if (Array.isArray(m.content)) {
|
|
4391
|
+
for (const b of m.content) {
|
|
4392
|
+
if (b.type === "text") total += Math.ceil(b.text.length / 4);
|
|
4393
|
+
else if (b.type === "tool_use" || b.type === "tool_result") {
|
|
4394
|
+
total += Math.ceil(JSON.stringify(b).length / 4);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
return total;
|
|
4400
|
+
}
|
|
4401
|
+
function createContextManagerTool(opts = {}) {
|
|
4402
|
+
return {
|
|
4403
|
+
name: CONTEXT_MANAGER_TOOL_NAME,
|
|
4404
|
+
description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction.',
|
|
4405
|
+
inputSchema: {
|
|
4406
|
+
type: "object",
|
|
4407
|
+
properties: {
|
|
4408
|
+
action: {
|
|
4409
|
+
type: "string",
|
|
4410
|
+
enum: ["check", "summary", "prune", "add_note", "compact"],
|
|
4411
|
+
description: "The context operation to perform."
|
|
4412
|
+
},
|
|
4413
|
+
from: {
|
|
4414
|
+
type: "number",
|
|
4415
|
+
description: "Start index (inclusive) for summary/prune operations."
|
|
4416
|
+
},
|
|
4417
|
+
to: {
|
|
4418
|
+
type: "number",
|
|
4419
|
+
description: "End index (inclusive) for summary/prune operations."
|
|
4420
|
+
},
|
|
4421
|
+
text: {
|
|
4422
|
+
type: "string",
|
|
4423
|
+
description: 'Summary or note text. For "summary": the model-provided summary of the removed range. For "add_note": the note to inject.'
|
|
4424
|
+
},
|
|
4425
|
+
afterIndex: {
|
|
4426
|
+
type: "number",
|
|
4427
|
+
description: "Insert after this index (for add_note). Defaults to prepend (0)."
|
|
4428
|
+
}
|
|
4429
|
+
},
|
|
4430
|
+
required: ["action"]
|
|
4431
|
+
},
|
|
4432
|
+
permission: "auto",
|
|
4433
|
+
mutating: true,
|
|
4434
|
+
async execute(input, ctx) {
|
|
4435
|
+
const messages = ctx.messages;
|
|
4436
|
+
const beforeTokens = roughEstimate(messages);
|
|
4437
|
+
switch (input.action) {
|
|
4438
|
+
case "check": {
|
|
4439
|
+
return {
|
|
4440
|
+
action: "check",
|
|
4441
|
+
beforeTokens,
|
|
4442
|
+
messageCount: messages.length,
|
|
4443
|
+
notes: JSON.stringify({
|
|
4444
|
+
messages: messages.length,
|
|
4445
|
+
tokens: beforeTokens,
|
|
4446
|
+
readFiles: ctx.readFiles.size,
|
|
4447
|
+
todos: ctx.todos.length,
|
|
4448
|
+
inProgress: ctx.todos.filter((t) => t.status === "in_progress").length
|
|
4449
|
+
})
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
case "compact": {
|
|
4453
|
+
if (!opts.compactor) {
|
|
4454
|
+
return {
|
|
4455
|
+
action: "compact",
|
|
4456
|
+
beforeTokens,
|
|
4457
|
+
messageCount: messages.length,
|
|
4458
|
+
notes: "No compactor registered. Use /compact aggressive via slash command instead."
|
|
4459
|
+
};
|
|
4460
|
+
}
|
|
4461
|
+
const report = await opts.compactor.compact(ctx);
|
|
4462
|
+
return {
|
|
4463
|
+
action: "compact",
|
|
4464
|
+
beforeTokens,
|
|
4465
|
+
afterTokens: report.after,
|
|
4466
|
+
messageCount: messages.length
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4469
|
+
case "prune": {
|
|
4470
|
+
const from = input.from ?? 0;
|
|
4471
|
+
const to = input.to ?? messages.length - 1;
|
|
4472
|
+
if (from < 0 || to >= messages.length || from > to) {
|
|
4473
|
+
return {
|
|
4474
|
+
action: "prune",
|
|
4475
|
+
beforeTokens,
|
|
4476
|
+
messageCount: messages.length,
|
|
4477
|
+
notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
const removed = messages.splice(from, to - from + 1);
|
|
4481
|
+
const afterTokens = roughEstimate(messages);
|
|
4482
|
+
return {
|
|
4483
|
+
action: "prune",
|
|
4484
|
+
beforeTokens,
|
|
4485
|
+
afterTokens,
|
|
4486
|
+
messageCount: messages.length,
|
|
4487
|
+
removedCount: removed.length
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
case "add_note": {
|
|
4491
|
+
const noteText = input.text ?? "(no summary)";
|
|
4492
|
+
const afterIdx = Math.min(input.afterIndex ?? 0, messages.length);
|
|
4493
|
+
const noteMsg = {
|
|
4494
|
+
role: "system",
|
|
4495
|
+
content: `[note: ${noteText}]`
|
|
4496
|
+
};
|
|
4497
|
+
messages.splice(afterIdx, 0, noteMsg);
|
|
4498
|
+
const afterTokens = roughEstimate(messages);
|
|
4499
|
+
return {
|
|
4500
|
+
action: "add_note",
|
|
4501
|
+
beforeTokens,
|
|
4502
|
+
afterTokens,
|
|
4503
|
+
messageCount: messages.length,
|
|
4504
|
+
summary: noteText
|
|
4505
|
+
};
|
|
4506
|
+
}
|
|
4507
|
+
case "summary": {
|
|
4508
|
+
const from = input.from ?? 0;
|
|
4509
|
+
const to = input.to ?? messages.length - 1;
|
|
4510
|
+
if (from < 0 || to >= messages.length || from > to) {
|
|
4511
|
+
return {
|
|
4512
|
+
action: "summary",
|
|
4513
|
+
beforeTokens,
|
|
4514
|
+
messageCount: messages.length,
|
|
4515
|
+
notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
messages.slice(from, to + 1);
|
|
4519
|
+
const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
|
|
4520
|
+
const summaryMsg = {
|
|
4521
|
+
role: "system",
|
|
4522
|
+
content: `[summary of messages ${from}\u2013${to}]: ${summaryText}`
|
|
4523
|
+
};
|
|
4524
|
+
messages.splice(from, to - from + 1, summaryMsg);
|
|
4525
|
+
const afterTokens = roughEstimate(messages);
|
|
4526
|
+
return {
|
|
4527
|
+
action: "summary",
|
|
4528
|
+
beforeTokens,
|
|
4529
|
+
afterTokens,
|
|
4530
|
+
messageCount: messages.length,
|
|
4531
|
+
summary: summaryText
|
|
4532
|
+
};
|
|
4533
|
+
}
|
|
4534
|
+
default:
|
|
4535
|
+
return {
|
|
4536
|
+
action: input.action,
|
|
4537
|
+
beforeTokens,
|
|
4538
|
+
messageCount: messages.length,
|
|
4539
|
+
notes: `Unknown action: ${input.action}`
|
|
4540
|
+
};
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
var contextManagerTool = createContextManagerTool();
|
|
4546
|
+
|
|
4547
|
+
export { AutoCompactionMiddleware, AutonomousRunner, DefaultAttachmentStore, DefaultConfigLoader, DefaultErrorHandler, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, IntelligentCompactor, LLMSelector, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, classifyFamily, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, loadProjectModes, loadUserModes, migratePlaintextSecrets, rewriteConfigEncrypted };
|
|
4548
|
+
//# sourceMappingURL=index.js.map
|
|
4549
|
+
//# sourceMappingURL=index.js.map
|