@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/index.js
CHANGED
|
@@ -3479,6 +3479,174 @@ var PROXY_PORT = (() => {
|
|
|
3479
3479
|
return DEFAULT_PORT;
|
|
3480
3480
|
})();
|
|
3481
3481
|
|
|
3482
|
+
// src/journal.ts
|
|
3483
|
+
var DEFAULT_CONFIG2 = {
|
|
3484
|
+
maxEntries: 100,
|
|
3485
|
+
maxAgeMs: 24 * 60 * 60 * 1e3,
|
|
3486
|
+
// 24 hours
|
|
3487
|
+
maxEventsPerResponse: 5
|
|
3488
|
+
};
|
|
3489
|
+
var SessionJournal = class {
|
|
3490
|
+
journals = /* @__PURE__ */ new Map();
|
|
3491
|
+
config;
|
|
3492
|
+
constructor(config) {
|
|
3493
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Extract key events from assistant response content.
|
|
3497
|
+
* Looks for patterns like "I created...", "I fixed...", "Successfully..."
|
|
3498
|
+
*/
|
|
3499
|
+
extractEvents(content) {
|
|
3500
|
+
if (!content || typeof content !== "string") {
|
|
3501
|
+
return [];
|
|
3502
|
+
}
|
|
3503
|
+
const events = [];
|
|
3504
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3505
|
+
const patterns = [
|
|
3506
|
+
// Creation patterns
|
|
3507
|
+
/I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
|
|
3508
|
+
// Fix patterns
|
|
3509
|
+
/I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
|
|
3510
|
+
// Completion patterns
|
|
3511
|
+
/I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
|
|
3512
|
+
// Update patterns
|
|
3513
|
+
/I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
|
|
3514
|
+
// Success patterns
|
|
3515
|
+
/Successfully ([^.!?\n]{10,150})/gi,
|
|
3516
|
+
// Tool usage patterns (when agent uses tools)
|
|
3517
|
+
/I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
|
|
3518
|
+
];
|
|
3519
|
+
for (const pattern of patterns) {
|
|
3520
|
+
pattern.lastIndex = 0;
|
|
3521
|
+
let match;
|
|
3522
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3523
|
+
const action = match[0].trim();
|
|
3524
|
+
const normalized = action.toLowerCase();
|
|
3525
|
+
if (seen.has(normalized)) {
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
if (action.length >= 15 && action.length <= 200) {
|
|
3529
|
+
events.push(action);
|
|
3530
|
+
seen.add(normalized);
|
|
3531
|
+
}
|
|
3532
|
+
if (events.length >= this.config.maxEventsPerResponse) {
|
|
3533
|
+
break;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
if (events.length >= this.config.maxEventsPerResponse) {
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
return events;
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Record events to the session journal.
|
|
3544
|
+
*/
|
|
3545
|
+
record(sessionId, events, model) {
|
|
3546
|
+
if (!sessionId || !events.length) {
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
const journal = this.journals.get(sessionId) || [];
|
|
3550
|
+
const now = Date.now();
|
|
3551
|
+
for (const action of events) {
|
|
3552
|
+
journal.push({
|
|
3553
|
+
timestamp: now,
|
|
3554
|
+
action,
|
|
3555
|
+
model
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
const cutoff = now - this.config.maxAgeMs;
|
|
3559
|
+
const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
|
|
3560
|
+
this.journals.set(sessionId, trimmed);
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Check if the user message indicates a need for historical context.
|
|
3564
|
+
*/
|
|
3565
|
+
needsContext(lastUserMessage) {
|
|
3566
|
+
if (!lastUserMessage || typeof lastUserMessage !== "string") {
|
|
3567
|
+
return false;
|
|
3568
|
+
}
|
|
3569
|
+
const lower = lastUserMessage.toLowerCase();
|
|
3570
|
+
const triggers = [
|
|
3571
|
+
// Direct questions about past work
|
|
3572
|
+
"what did you do",
|
|
3573
|
+
"what have you done",
|
|
3574
|
+
"what did we do",
|
|
3575
|
+
"what have we done",
|
|
3576
|
+
// Temporal references
|
|
3577
|
+
"earlier",
|
|
3578
|
+
"before",
|
|
3579
|
+
"previously",
|
|
3580
|
+
"this session",
|
|
3581
|
+
"today",
|
|
3582
|
+
"so far",
|
|
3583
|
+
// Summary requests
|
|
3584
|
+
"remind me",
|
|
3585
|
+
"summarize",
|
|
3586
|
+
"summary of",
|
|
3587
|
+
"recap",
|
|
3588
|
+
// Progress inquiries
|
|
3589
|
+
"your work",
|
|
3590
|
+
"your progress",
|
|
3591
|
+
"accomplished",
|
|
3592
|
+
"achievements",
|
|
3593
|
+
"completed tasks"
|
|
3594
|
+
];
|
|
3595
|
+
return triggers.some((t) => lower.includes(t));
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Format the journal for injection into system message.
|
|
3599
|
+
* Returns null if journal is empty.
|
|
3600
|
+
*/
|
|
3601
|
+
format(sessionId) {
|
|
3602
|
+
const journal = this.journals.get(sessionId);
|
|
3603
|
+
if (!journal?.length) {
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
const lines = journal.map((e) => {
|
|
3607
|
+
const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
|
|
3608
|
+
hour: "2-digit",
|
|
3609
|
+
minute: "2-digit",
|
|
3610
|
+
hour12: true
|
|
3611
|
+
});
|
|
3612
|
+
return `- ${time}: ${e.action}`;
|
|
3613
|
+
});
|
|
3614
|
+
return `[Session Memory - Key Actions]
|
|
3615
|
+
${lines.join("\n")}`;
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Get the raw journal entries for a session (for debugging/testing).
|
|
3619
|
+
*/
|
|
3620
|
+
getEntries(sessionId) {
|
|
3621
|
+
return this.journals.get(sessionId) || [];
|
|
3622
|
+
}
|
|
3623
|
+
/**
|
|
3624
|
+
* Clear journal for a specific session.
|
|
3625
|
+
*/
|
|
3626
|
+
clear(sessionId) {
|
|
3627
|
+
this.journals.delete(sessionId);
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Clear all journals.
|
|
3631
|
+
*/
|
|
3632
|
+
clearAll() {
|
|
3633
|
+
this.journals.clear();
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* Get stats about the journal.
|
|
3637
|
+
*/
|
|
3638
|
+
getStats() {
|
|
3639
|
+
let totalEntries = 0;
|
|
3640
|
+
for (const entries of this.journals.values()) {
|
|
3641
|
+
totalEntries += entries.length;
|
|
3642
|
+
}
|
|
3643
|
+
return {
|
|
3644
|
+
sessions: this.journals.size,
|
|
3645
|
+
totalEntries
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
|
|
3482
3650
|
// src/proxy.ts
|
|
3483
3651
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
3484
3652
|
var AUTO_MODEL = "blockrun/auto";
|
|
@@ -3888,6 +4056,7 @@ async function startProxy(options) {
|
|
|
3888
4056
|
const deduplicator = new RequestDeduplicator();
|
|
3889
4057
|
const responseCache = new ResponseCache(options.cacheConfig);
|
|
3890
4058
|
const sessionStore = new SessionStore(options.sessionConfig);
|
|
4059
|
+
const sessionJournal = new SessionJournal();
|
|
3891
4060
|
const connections = /* @__PURE__ */ new Set();
|
|
3892
4061
|
const server = createServer(async (req, res) => {
|
|
3893
4062
|
req.on("error", (err) => {
|
|
@@ -3983,7 +4152,8 @@ async function startProxy(options) {
|
|
|
3983
4152
|
deduplicator,
|
|
3984
4153
|
balanceMonitor,
|
|
3985
4154
|
sessionStore,
|
|
3986
|
-
responseCache
|
|
4155
|
+
responseCache,
|
|
4156
|
+
sessionJournal
|
|
3987
4157
|
);
|
|
3988
4158
|
} catch (err) {
|
|
3989
4159
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -4184,7 +4354,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
4184
4354
|
};
|
|
4185
4355
|
}
|
|
4186
4356
|
}
|
|
4187
|
-
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
|
|
4357
|
+
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
|
|
4188
4358
|
const startTime = Date.now();
|
|
4189
4359
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
4190
4360
|
const bodyChunks = [];
|
|
@@ -4197,7 +4367,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4197
4367
|
let modelId = "";
|
|
4198
4368
|
let maxTokens = 4096;
|
|
4199
4369
|
let routingProfile = null;
|
|
4370
|
+
let accumulatedContent = "";
|
|
4200
4371
|
const isChatCompletion = req.url?.includes("/chat/completions");
|
|
4372
|
+
const sessionId = getSessionId(req.headers);
|
|
4201
4373
|
if (isChatCompletion && body.length > 0) {
|
|
4202
4374
|
try {
|
|
4203
4375
|
const parsed = JSON.parse(body.toString());
|
|
@@ -4205,6 +4377,30 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4205
4377
|
modelId = parsed.model || "";
|
|
4206
4378
|
maxTokens = parsed.max_tokens || 4096;
|
|
4207
4379
|
let bodyModified = false;
|
|
4380
|
+
if (sessionId && Array.isArray(parsed.messages)) {
|
|
4381
|
+
const messages = parsed.messages;
|
|
4382
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
4383
|
+
const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
4384
|
+
if (sessionJournal.needsContext(lastContent)) {
|
|
4385
|
+
const journalText = sessionJournal.format(sessionId);
|
|
4386
|
+
if (journalText) {
|
|
4387
|
+
const sysIdx = messages.findIndex((m) => m.role === "system");
|
|
4388
|
+
if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
|
|
4389
|
+
messages[sysIdx] = {
|
|
4390
|
+
...messages[sysIdx],
|
|
4391
|
+
content: journalText + "\n\n" + messages[sysIdx].content
|
|
4392
|
+
};
|
|
4393
|
+
} else {
|
|
4394
|
+
messages.unshift({ role: "system", content: journalText });
|
|
4395
|
+
}
|
|
4396
|
+
parsed.messages = messages;
|
|
4397
|
+
bodyModified = true;
|
|
4398
|
+
console.log(
|
|
4399
|
+
`[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4208
4404
|
if (parsed.stream === true) {
|
|
4209
4405
|
parsed.stream = false;
|
|
4210
4406
|
bodyModified = true;
|
|
@@ -4243,18 +4439,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4243
4439
|
latencyMs: 0
|
|
4244
4440
|
});
|
|
4245
4441
|
} else {
|
|
4246
|
-
const
|
|
4442
|
+
const sessionId2 = getSessionId(
|
|
4247
4443
|
req.headers
|
|
4248
4444
|
);
|
|
4249
|
-
const existingSession =
|
|
4445
|
+
const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
|
|
4250
4446
|
if (existingSession) {
|
|
4251
4447
|
console.log(
|
|
4252
|
-
`[ClawRouter] Session ${
|
|
4448
|
+
`[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
|
|
4253
4449
|
);
|
|
4254
4450
|
parsed.model = existingSession.model;
|
|
4255
4451
|
modelId = existingSession.model;
|
|
4256
4452
|
bodyModified = true;
|
|
4257
|
-
sessionStore.touchSession(
|
|
4453
|
+
sessionStore.touchSession(sessionId2);
|
|
4258
4454
|
} else {
|
|
4259
4455
|
const messages = parsed.messages;
|
|
4260
4456
|
let lastUserMsg;
|
|
@@ -4283,10 +4479,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4283
4479
|
parsed.model = routingDecision.model;
|
|
4284
4480
|
modelId = routingDecision.model;
|
|
4285
4481
|
bodyModified = true;
|
|
4286
|
-
if (
|
|
4287
|
-
sessionStore.setSession(
|
|
4482
|
+
if (sessionId2) {
|
|
4483
|
+
sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
|
|
4288
4484
|
console.log(
|
|
4289
|
-
`[ClawRouter] Session ${
|
|
4485
|
+
`[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
|
|
4290
4486
|
);
|
|
4291
4487
|
}
|
|
4292
4488
|
options.onRouted?.(routingDecision);
|
|
@@ -4621,6 +4817,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4621
4817
|
const content = stripThinkingTokens(rawContent);
|
|
4622
4818
|
const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
|
|
4623
4819
|
const index = choice.index ?? 0;
|
|
4820
|
+
if (content) {
|
|
4821
|
+
accumulatedContent += content;
|
|
4822
|
+
}
|
|
4624
4823
|
const roleChunk = {
|
|
4625
4824
|
...baseChunk,
|
|
4626
4825
|
choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
|
|
@@ -4734,6 +4933,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4734
4933
|
});
|
|
4735
4934
|
console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
|
|
4736
4935
|
}
|
|
4936
|
+
try {
|
|
4937
|
+
const rspJson = JSON.parse(responseBody.toString());
|
|
4938
|
+
if (rspJson.choices?.[0]?.message?.content) {
|
|
4939
|
+
accumulatedContent = rspJson.choices[0].message.content;
|
|
4940
|
+
}
|
|
4941
|
+
} catch {
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
if (sessionId && accumulatedContent) {
|
|
4945
|
+
const events = sessionJournal.extractEvents(accumulatedContent);
|
|
4946
|
+
if (events.length > 0) {
|
|
4947
|
+
sessionJournal.record(sessionId, events, actualModelUsed);
|
|
4948
|
+
console.log(
|
|
4949
|
+
`[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
|
|
4950
|
+
);
|
|
4951
|
+
}
|
|
4737
4952
|
}
|
|
4738
4953
|
if (estimatedCostMicros !== void 0) {
|
|
4739
4954
|
balanceMonitor.deductEstimated(estimatedCostMicros);
|
|
@@ -5044,7 +5259,7 @@ function injectModelsConfig(logger) {
|
|
|
5044
5259
|
{ id: "opus", alias: "br-opus" },
|
|
5045
5260
|
{ id: "haiku", alias: "br-haiku" },
|
|
5046
5261
|
{ id: "gpt5", alias: "gpt5" },
|
|
5047
|
-
{ id: "
|
|
5262
|
+
{ id: "codex", alias: "codex" },
|
|
5048
5263
|
{ id: "grok-fast", alias: "grok-fast" },
|
|
5049
5264
|
{ id: "grok-code", alias: "grok-code" },
|
|
5050
5265
|
{ id: "deepseek", alias: "deepseek" },
|