@blockrun/clawrouter 0.9.14 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +224 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.js +225 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3339,6 +3339,174 @@ var PROXY_PORT = (() => {
|
|
|
3339
3339
|
return DEFAULT_PORT;
|
|
3340
3340
|
})();
|
|
3341
3341
|
|
|
3342
|
+
// src/journal.ts
|
|
3343
|
+
var DEFAULT_CONFIG2 = {
|
|
3344
|
+
maxEntries: 100,
|
|
3345
|
+
maxAgeMs: 24 * 60 * 60 * 1e3,
|
|
3346
|
+
// 24 hours
|
|
3347
|
+
maxEventsPerResponse: 5
|
|
3348
|
+
};
|
|
3349
|
+
var SessionJournal = class {
|
|
3350
|
+
journals = /* @__PURE__ */ new Map();
|
|
3351
|
+
config;
|
|
3352
|
+
constructor(config) {
|
|
3353
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
3354
|
+
}
|
|
3355
|
+
/**
|
|
3356
|
+
* Extract key events from assistant response content.
|
|
3357
|
+
* Looks for patterns like "I created...", "I fixed...", "Successfully..."
|
|
3358
|
+
*/
|
|
3359
|
+
extractEvents(content) {
|
|
3360
|
+
if (!content || typeof content !== "string") {
|
|
3361
|
+
return [];
|
|
3362
|
+
}
|
|
3363
|
+
const events = [];
|
|
3364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3365
|
+
const patterns = [
|
|
3366
|
+
// Creation patterns
|
|
3367
|
+
/I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
|
|
3368
|
+
// Fix patterns
|
|
3369
|
+
/I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
|
|
3370
|
+
// Completion patterns
|
|
3371
|
+
/I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
|
|
3372
|
+
// Update patterns
|
|
3373
|
+
/I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
|
|
3374
|
+
// Success patterns
|
|
3375
|
+
/Successfully ([^.!?\n]{10,150})/gi,
|
|
3376
|
+
// Tool usage patterns (when agent uses tools)
|
|
3377
|
+
/I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
|
|
3378
|
+
];
|
|
3379
|
+
for (const pattern of patterns) {
|
|
3380
|
+
pattern.lastIndex = 0;
|
|
3381
|
+
let match;
|
|
3382
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3383
|
+
const action = match[0].trim();
|
|
3384
|
+
const normalized = action.toLowerCase();
|
|
3385
|
+
if (seen.has(normalized)) {
|
|
3386
|
+
continue;
|
|
3387
|
+
}
|
|
3388
|
+
if (action.length >= 15 && action.length <= 200) {
|
|
3389
|
+
events.push(action);
|
|
3390
|
+
seen.add(normalized);
|
|
3391
|
+
}
|
|
3392
|
+
if (events.length >= this.config.maxEventsPerResponse) {
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
if (events.length >= this.config.maxEventsPerResponse) {
|
|
3397
|
+
break;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
return events;
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Record events to the session journal.
|
|
3404
|
+
*/
|
|
3405
|
+
record(sessionId, events, model) {
|
|
3406
|
+
if (!sessionId || !events.length) {
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
const journal = this.journals.get(sessionId) || [];
|
|
3410
|
+
const now = Date.now();
|
|
3411
|
+
for (const action of events) {
|
|
3412
|
+
journal.push({
|
|
3413
|
+
timestamp: now,
|
|
3414
|
+
action,
|
|
3415
|
+
model
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
const cutoff = now - this.config.maxAgeMs;
|
|
3419
|
+
const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
|
|
3420
|
+
this.journals.set(sessionId, trimmed);
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* Check if the user message indicates a need for historical context.
|
|
3424
|
+
*/
|
|
3425
|
+
needsContext(lastUserMessage) {
|
|
3426
|
+
if (!lastUserMessage || typeof lastUserMessage !== "string") {
|
|
3427
|
+
return false;
|
|
3428
|
+
}
|
|
3429
|
+
const lower = lastUserMessage.toLowerCase();
|
|
3430
|
+
const triggers = [
|
|
3431
|
+
// Direct questions about past work
|
|
3432
|
+
"what did you do",
|
|
3433
|
+
"what have you done",
|
|
3434
|
+
"what did we do",
|
|
3435
|
+
"what have we done",
|
|
3436
|
+
// Temporal references
|
|
3437
|
+
"earlier",
|
|
3438
|
+
"before",
|
|
3439
|
+
"previously",
|
|
3440
|
+
"this session",
|
|
3441
|
+
"today",
|
|
3442
|
+
"so far",
|
|
3443
|
+
// Summary requests
|
|
3444
|
+
"remind me",
|
|
3445
|
+
"summarize",
|
|
3446
|
+
"summary of",
|
|
3447
|
+
"recap",
|
|
3448
|
+
// Progress inquiries
|
|
3449
|
+
"your work",
|
|
3450
|
+
"your progress",
|
|
3451
|
+
"accomplished",
|
|
3452
|
+
"achievements",
|
|
3453
|
+
"completed tasks"
|
|
3454
|
+
];
|
|
3455
|
+
return triggers.some((t) => lower.includes(t));
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Format the journal for injection into system message.
|
|
3459
|
+
* Returns null if journal is empty.
|
|
3460
|
+
*/
|
|
3461
|
+
format(sessionId) {
|
|
3462
|
+
const journal = this.journals.get(sessionId);
|
|
3463
|
+
if (!journal?.length) {
|
|
3464
|
+
return null;
|
|
3465
|
+
}
|
|
3466
|
+
const lines = journal.map((e) => {
|
|
3467
|
+
const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
|
|
3468
|
+
hour: "2-digit",
|
|
3469
|
+
minute: "2-digit",
|
|
3470
|
+
hour12: true
|
|
3471
|
+
});
|
|
3472
|
+
return `- ${time}: ${e.action}`;
|
|
3473
|
+
});
|
|
3474
|
+
return `[Session Memory - Key Actions]
|
|
3475
|
+
${lines.join("\n")}`;
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Get the raw journal entries for a session (for debugging/testing).
|
|
3479
|
+
*/
|
|
3480
|
+
getEntries(sessionId) {
|
|
3481
|
+
return this.journals.get(sessionId) || [];
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* Clear journal for a specific session.
|
|
3485
|
+
*/
|
|
3486
|
+
clear(sessionId) {
|
|
3487
|
+
this.journals.delete(sessionId);
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Clear all journals.
|
|
3491
|
+
*/
|
|
3492
|
+
clearAll() {
|
|
3493
|
+
this.journals.clear();
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Get stats about the journal.
|
|
3497
|
+
*/
|
|
3498
|
+
getStats() {
|
|
3499
|
+
let totalEntries = 0;
|
|
3500
|
+
for (const entries of this.journals.values()) {
|
|
3501
|
+
totalEntries += entries.length;
|
|
3502
|
+
}
|
|
3503
|
+
return {
|
|
3504
|
+
sessions: this.journals.size,
|
|
3505
|
+
totalEntries
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
|
|
3342
3510
|
// src/proxy.ts
|
|
3343
3511
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
3344
3512
|
var AUTO_MODEL = "blockrun/auto";
|
|
@@ -3748,6 +3916,7 @@ async function startProxy(options) {
|
|
|
3748
3916
|
const deduplicator = new RequestDeduplicator();
|
|
3749
3917
|
const responseCache = new ResponseCache(options.cacheConfig);
|
|
3750
3918
|
const sessionStore = new SessionStore(options.sessionConfig);
|
|
3919
|
+
const sessionJournal = new SessionJournal();
|
|
3751
3920
|
const connections = /* @__PURE__ */ new Set();
|
|
3752
3921
|
const server = createServer(async (req, res) => {
|
|
3753
3922
|
req.on("error", (err) => {
|
|
@@ -3843,7 +4012,8 @@ async function startProxy(options) {
|
|
|
3843
4012
|
deduplicator,
|
|
3844
4013
|
balanceMonitor,
|
|
3845
4014
|
sessionStore,
|
|
3846
|
-
responseCache
|
|
4015
|
+
responseCache,
|
|
4016
|
+
sessionJournal
|
|
3847
4017
|
);
|
|
3848
4018
|
} catch (err) {
|
|
3849
4019
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -4044,7 +4214,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
4044
4214
|
};
|
|
4045
4215
|
}
|
|
4046
4216
|
}
|
|
4047
|
-
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
|
|
4217
|
+
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
|
|
4048
4218
|
const startTime = Date.now();
|
|
4049
4219
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
4050
4220
|
const bodyChunks = [];
|
|
@@ -4057,7 +4227,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4057
4227
|
let modelId = "";
|
|
4058
4228
|
let maxTokens = 4096;
|
|
4059
4229
|
let routingProfile = null;
|
|
4230
|
+
let accumulatedContent = "";
|
|
4060
4231
|
const isChatCompletion = req.url?.includes("/chat/completions");
|
|
4232
|
+
const sessionId = getSessionId(req.headers);
|
|
4061
4233
|
if (isChatCompletion && body.length > 0) {
|
|
4062
4234
|
try {
|
|
4063
4235
|
const parsed = JSON.parse(body.toString());
|
|
@@ -4065,6 +4237,30 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4065
4237
|
modelId = parsed.model || "";
|
|
4066
4238
|
maxTokens = parsed.max_tokens || 4096;
|
|
4067
4239
|
let bodyModified = false;
|
|
4240
|
+
if (sessionId && Array.isArray(parsed.messages)) {
|
|
4241
|
+
const messages = parsed.messages;
|
|
4242
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
4243
|
+
const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
4244
|
+
if (sessionJournal.needsContext(lastContent)) {
|
|
4245
|
+
const journalText = sessionJournal.format(sessionId);
|
|
4246
|
+
if (journalText) {
|
|
4247
|
+
const sysIdx = messages.findIndex((m) => m.role === "system");
|
|
4248
|
+
if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
|
|
4249
|
+
messages[sysIdx] = {
|
|
4250
|
+
...messages[sysIdx],
|
|
4251
|
+
content: journalText + "\n\n" + messages[sysIdx].content
|
|
4252
|
+
};
|
|
4253
|
+
} else {
|
|
4254
|
+
messages.unshift({ role: "system", content: journalText });
|
|
4255
|
+
}
|
|
4256
|
+
parsed.messages = messages;
|
|
4257
|
+
bodyModified = true;
|
|
4258
|
+
console.log(
|
|
4259
|
+
`[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4068
4264
|
if (parsed.stream === true) {
|
|
4069
4265
|
parsed.stream = false;
|
|
4070
4266
|
bodyModified = true;
|
|
@@ -4103,18 +4299,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4103
4299
|
latencyMs: 0
|
|
4104
4300
|
});
|
|
4105
4301
|
} else {
|
|
4106
|
-
const
|
|
4302
|
+
const sessionId2 = getSessionId(
|
|
4107
4303
|
req.headers
|
|
4108
4304
|
);
|
|
4109
|
-
const existingSession =
|
|
4305
|
+
const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
|
|
4110
4306
|
if (existingSession) {
|
|
4111
4307
|
console.log(
|
|
4112
|
-
`[ClawRouter] Session ${
|
|
4308
|
+
`[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
|
|
4113
4309
|
);
|
|
4114
4310
|
parsed.model = existingSession.model;
|
|
4115
4311
|
modelId = existingSession.model;
|
|
4116
4312
|
bodyModified = true;
|
|
4117
|
-
sessionStore.touchSession(
|
|
4313
|
+
sessionStore.touchSession(sessionId2);
|
|
4118
4314
|
} else {
|
|
4119
4315
|
const messages = parsed.messages;
|
|
4120
4316
|
let lastUserMsg;
|
|
@@ -4143,10 +4339,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4143
4339
|
parsed.model = routingDecision.model;
|
|
4144
4340
|
modelId = routingDecision.model;
|
|
4145
4341
|
bodyModified = true;
|
|
4146
|
-
if (
|
|
4147
|
-
sessionStore.setSession(
|
|
4342
|
+
if (sessionId2) {
|
|
4343
|
+
sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
|
|
4148
4344
|
console.log(
|
|
4149
|
-
`[ClawRouter] Session ${
|
|
4345
|
+
`[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
|
|
4150
4346
|
);
|
|
4151
4347
|
}
|
|
4152
4348
|
options.onRouted?.(routingDecision);
|
|
@@ -4481,6 +4677,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4481
4677
|
const content = stripThinkingTokens(rawContent);
|
|
4482
4678
|
const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
|
|
4483
4679
|
const index = choice.index ?? 0;
|
|
4680
|
+
if (content) {
|
|
4681
|
+
accumulatedContent += content;
|
|
4682
|
+
}
|
|
4484
4683
|
const roleChunk = {
|
|
4485
4684
|
...baseChunk,
|
|
4486
4685
|
choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
|
|
@@ -4594,6 +4793,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4594
4793
|
});
|
|
4595
4794
|
console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
|
|
4596
4795
|
}
|
|
4796
|
+
try {
|
|
4797
|
+
const rspJson = JSON.parse(responseBody.toString());
|
|
4798
|
+
if (rspJson.choices?.[0]?.message?.content) {
|
|
4799
|
+
accumulatedContent = rspJson.choices[0].message.content;
|
|
4800
|
+
}
|
|
4801
|
+
} catch {
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
if (sessionId && accumulatedContent) {
|
|
4805
|
+
const events = sessionJournal.extractEvents(accumulatedContent);
|
|
4806
|
+
if (events.length > 0) {
|
|
4807
|
+
sessionJournal.record(sessionId, events, actualModelUsed);
|
|
4808
|
+
console.log(
|
|
4809
|
+
`[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
|
|
4810
|
+
);
|
|
4811
|
+
}
|
|
4597
4812
|
}
|
|
4598
4813
|
if (estimatedCostMicros !== void 0) {
|
|
4599
4814
|
balanceMonitor.deductEstimated(estimatedCostMicros);
|