evrex-mcp 0.8.0 → 0.8.2
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/account.js +1 -1
- package/dist/capture.js +243 -27
- package/dist/continuity.js +1 -1
- package/dist/hook.js +5 -2
- package/dist/import.js +280 -27
- package/dist/index.js +398 -76
- package/dist/pretool.js +1 -1
- package/dist/tickets.js +1 -1
- package/package.json +16 -15
package/dist/index.js
CHANGED
|
@@ -63,7 +63,7 @@ var init_client = __esm({
|
|
|
63
63
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
64
64
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
65
65
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
66
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
66
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
67
67
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
68
68
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
69
69
|
// interleaved, each with the handle evrex_expand takes.
|
|
@@ -525,6 +525,29 @@ async function synthesizeAnswer(question, evidence, client2, options = {}) {
|
|
|
525
525
|
};
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
+
// ../../packages/llm-core/src/retrieval-boost.ts
|
|
529
|
+
var CORPUS = `The corpus is an engineering-memory index built from a team's coding-agent conversations and git history, retrieved with Postgres full-text OR-matching plus BM25 (exact word stems match; meanings do not). Searchable documents:
|
|
530
|
+
- Conversation turns: prose by the person and the agent, plus tool-call lines like "[tool_call: Edit] apps/backend/src/query/query.service.ts" \u2014 so file paths, function names, error strings and identifiers appear verbatim.
|
|
531
|
+
- Extracted insights: short titled records of decisions made, approaches REJECTED (with the reason), constraints, and procedures.
|
|
532
|
+
- Commit messages: conventional-commit subjects and explanatory bodies.`;
|
|
533
|
+
var EXPAND_SYSTEM = `You expand a search query into terms that will match an engineering record.
|
|
534
|
+
|
|
535
|
+
${CORPUS}
|
|
536
|
+
|
|
537
|
+
Given a person's question, produce the search terms most likely to appear VERBATIM in that corpus but absent from the question itself: the identifiers an engineer would have typed (function, constant, env var, file names), the error text the failure would have printed, and the words the same idea takes in commit-message register ("fix the flaky test" \u2192 "flaky", "deflake", "retry", "timeout"). Prefer specific over general \u2014 "RLS" and "row level security" over "database permissions". Never invent identifiers you are not confident exist in this kind of codebase; a wrong specific term matches nothing and costs nothing, but fill the list with your best evidence-seeking guesses.
|
|
538
|
+
|
|
539
|
+
Respond with JSON only: {"terms": ["...", ...]} \u2014 3 to 8 terms, each 1 to 4 words, no duplicates of the question's own words.`;
|
|
540
|
+
var RERANK_SYSTEM = `You order search results for an engineer's question about their own project's history.
|
|
541
|
+
|
|
542
|
+
${CORPUS}
|
|
543
|
+
|
|
544
|
+
You are given the question and a numbered list of candidate results that lexical retrieval fetched. Order the candidate ids best-first by how much each would actually help ANSWER the question:
|
|
545
|
+
- Recorded reasoning outranks mechanical record: a decision, a rejected approach with its reason, a constraint, or an explanation beats a log line or a file listing that merely shares words with the question.
|
|
546
|
+
- A candidate about the question's subject beats one that only quotes its vocabulary; watch for results that repeat the question's words while being about something else entirely.
|
|
547
|
+
- Prefer the origin of an idea over a later mention of it; among near-duplicates prefer the more complete one; use the date only to break ties.
|
|
548
|
+
|
|
549
|
+
Respond with JSON only: {"ranked": ["id", ...]} \u2014 every listed id at most once, ids from the list only, best first. Include every id you consider relevant; omit only what would waste the reader's attention.`;
|
|
550
|
+
|
|
528
551
|
// ../../packages/llm-core/src/cli-client.ts
|
|
529
552
|
import { execFile } from "node:child_process";
|
|
530
553
|
var TIMEOUT_MS = 12e4;
|
|
@@ -646,6 +669,29 @@ ${request2.user}`;
|
|
|
646
669
|
// ../../packages/llm-core/src/provider-clients.ts
|
|
647
670
|
import Anthropic3 from "@anthropic-ai/sdk";
|
|
648
671
|
var NOOP = { warn: () => void 0, error: () => void 0 };
|
|
672
|
+
var BATCH_POLL_MS = 5e3;
|
|
673
|
+
var BATCH_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
674
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
675
|
+
function totalize(items, results) {
|
|
676
|
+
for (const item of items) if (!results.has(item.id)) results.set(item.id, null);
|
|
677
|
+
return results;
|
|
678
|
+
}
|
|
679
|
+
function totalizeIds(ids, results) {
|
|
680
|
+
for (const id of ids) if (!results.has(id)) results.set(id, null);
|
|
681
|
+
return results;
|
|
682
|
+
}
|
|
683
|
+
async function pollToCompletion(handle, resolve, sleepFn, onTimeout) {
|
|
684
|
+
const startedAt = Date.now();
|
|
685
|
+
for (; ; ) {
|
|
686
|
+
const resolved = await resolve(handle);
|
|
687
|
+
if (resolved) return resolved;
|
|
688
|
+
if (Date.now() - startedAt > BATCH_TIMEOUT_MS) {
|
|
689
|
+
onTimeout(handle.id);
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
await sleepFn(BATCH_POLL_MS);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
649
695
|
var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
650
696
|
var MAX_RETRIES = 4;
|
|
651
697
|
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
@@ -653,12 +699,12 @@ function isExhaustedStatus(status, retriesSpent) {
|
|
|
653
699
|
if (status === 401 || status === 403) return true;
|
|
654
700
|
return status === 429 && retriesSpent;
|
|
655
701
|
}
|
|
656
|
-
async function waitBeforeRetry(response, attempt,
|
|
702
|
+
async function waitBeforeRetry(response, attempt, sleep2) {
|
|
657
703
|
const header = response.headers.get("retry-after") ?? response.headers.get("x-ratelimit-reset-tokens");
|
|
658
704
|
const seconds = header ? parseDuration(header) : null;
|
|
659
705
|
const backoff = Math.min(8 * 2 ** attempt, 60);
|
|
660
706
|
const wait = Math.min(seconds ?? backoff, MAX_RETRY_WAIT_SECONDS);
|
|
661
|
-
await
|
|
707
|
+
await sleep2(wait * 1e3);
|
|
662
708
|
}
|
|
663
709
|
var MAX_RETRY_WAIT_SECONDS = 90;
|
|
664
710
|
function parseDuration(value) {
|
|
@@ -678,7 +724,7 @@ function parseJsonBody(text) {
|
|
|
678
724
|
return null;
|
|
679
725
|
}
|
|
680
726
|
}
|
|
681
|
-
var AnthropicClient = class {
|
|
727
|
+
var AnthropicClient = class _AnthropicClient {
|
|
682
728
|
constructor(key, model, logger) {
|
|
683
729
|
this.model = model;
|
|
684
730
|
this.logger = logger;
|
|
@@ -687,37 +733,47 @@ var AnthropicClient = class {
|
|
|
687
733
|
provider = "anthropic";
|
|
688
734
|
exhausted = false;
|
|
689
735
|
sdk;
|
|
736
|
+
// The one place a ModelRequest becomes Anthropic's message params, so the
|
|
737
|
+
// single and batch paths cannot drift on the cache breakpoint or the
|
|
738
|
+
// json_schema wiring.
|
|
739
|
+
messageParams(request2) {
|
|
740
|
+
return {
|
|
741
|
+
model: request2.model ?? this.model,
|
|
742
|
+
max_tokens: request2.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
743
|
+
// The breakpoint sits at the end of the system prompt, which is the
|
|
744
|
+
// only part of these requests that repeats. Rendering order is
|
|
745
|
+
// tools -> system -> messages, so a marker here caches everything
|
|
746
|
+
// ahead of the transcript.
|
|
747
|
+
//
|
|
748
|
+
// Deliberately NOT on the user block. That block is a different
|
|
749
|
+
// transcript chunk on every call, so a breakpoint there would write a
|
|
750
|
+
// fresh cache entry per request and read none of them back — paying
|
|
751
|
+
// the write premium for nothing, which is worse than not caching.
|
|
752
|
+
system: [
|
|
753
|
+
{
|
|
754
|
+
type: "text",
|
|
755
|
+
text: request2.system,
|
|
756
|
+
cache_control: { type: "ephemeral" }
|
|
757
|
+
}
|
|
758
|
+
],
|
|
759
|
+
messages: [{ role: "user", content: request2.user }],
|
|
760
|
+
...request2.schema ? {
|
|
761
|
+
output_config: {
|
|
762
|
+
format: { type: "json_schema", schema: request2.schema }
|
|
763
|
+
}
|
|
764
|
+
} : {}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
static messageText(message) {
|
|
768
|
+
return message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
769
|
+
}
|
|
690
770
|
async completeJson(request2) {
|
|
691
771
|
try {
|
|
692
|
-
const message = await this.sdk.messages.create(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
// The breakpoint sits at the end of the system prompt, which is the
|
|
696
|
-
// only part of these requests that repeats. Rendering order is
|
|
697
|
-
// tools -> system -> messages, so a marker here caches everything
|
|
698
|
-
// ahead of the transcript.
|
|
699
|
-
//
|
|
700
|
-
// Deliberately NOT on the user block. That block is a different
|
|
701
|
-
// transcript chunk on every call, so a breakpoint there would write a
|
|
702
|
-
// fresh cache entry per request and read none of them back — paying
|
|
703
|
-
// the write premium for nothing, which is worse than not caching.
|
|
704
|
-
system: [
|
|
705
|
-
{
|
|
706
|
-
type: "text",
|
|
707
|
-
text: request2.system,
|
|
708
|
-
cache_control: { type: "ephemeral" }
|
|
709
|
-
}
|
|
710
|
-
],
|
|
711
|
-
messages: [{ role: "user", content: request2.user }],
|
|
712
|
-
...request2.schema ? {
|
|
713
|
-
output_config: {
|
|
714
|
-
format: { type: "json_schema", schema: request2.schema }
|
|
715
|
-
}
|
|
716
|
-
} : {}
|
|
717
|
-
});
|
|
772
|
+
const message = await this.sdk.messages.create(
|
|
773
|
+
this.messageParams(request2)
|
|
774
|
+
);
|
|
718
775
|
this.reportCacheUsage(message.usage);
|
|
719
|
-
|
|
720
|
-
return parseJsonBody(text);
|
|
776
|
+
return parseJsonBody(_AnthropicClient.messageText(message));
|
|
721
777
|
} catch (err) {
|
|
722
778
|
const status = err?.status;
|
|
723
779
|
if (typeof status === "number" && isExhaustedStatus(status, true)) {
|
|
@@ -727,6 +783,58 @@ var AnthropicClient = class {
|
|
|
727
783
|
return null;
|
|
728
784
|
}
|
|
729
785
|
}
|
|
786
|
+
async submitBatch(items) {
|
|
787
|
+
if (items.length === 0) return null;
|
|
788
|
+
try {
|
|
789
|
+
const created = await this.sdk.messages.batches.create({
|
|
790
|
+
requests: items.map((item) => ({
|
|
791
|
+
custom_id: item.id,
|
|
792
|
+
params: this.messageParams(item.request)
|
|
793
|
+
}))
|
|
794
|
+
});
|
|
795
|
+
return { provider: "anthropic", id: created.id, itemIds: items.map((i) => i.id) };
|
|
796
|
+
} catch (err) {
|
|
797
|
+
const status = err?.status;
|
|
798
|
+
if (typeof status === "number" && isExhaustedStatus(status, true)) {
|
|
799
|
+
this.exhausted = true;
|
|
800
|
+
}
|
|
801
|
+
this.logger.warn(`anthropic batch submit failed: ${describeError(err)}`);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async resolveBatch(handle) {
|
|
806
|
+
try {
|
|
807
|
+
const batch = await this.sdk.messages.batches.retrieve(handle.id);
|
|
808
|
+
if (batch.processing_status !== "ended") return null;
|
|
809
|
+
const results = /* @__PURE__ */ new Map();
|
|
810
|
+
for await (const entry of await this.sdk.messages.batches.results(
|
|
811
|
+
handle.id
|
|
812
|
+
)) {
|
|
813
|
+
results.set(
|
|
814
|
+
entry.custom_id,
|
|
815
|
+
entry.result.type === "succeeded" ? parseJsonBody(_AnthropicClient.messageText(entry.result.message)) : null
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return totalizeIds(handle.itemIds, results);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
this.logger.warn(`anthropic batch resolve failed: ${describeError(err)}`);
|
|
821
|
+
return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async completeJsonBatch(items) {
|
|
825
|
+
if (items.length === 0) return /* @__PURE__ */ new Map();
|
|
826
|
+
const handle = await this.submitBatch(items);
|
|
827
|
+
if (!handle) return totalize(items, /* @__PURE__ */ new Map());
|
|
828
|
+
const resolved = await pollToCompletion(
|
|
829
|
+
handle,
|
|
830
|
+
(h) => this.resolveBatch(h),
|
|
831
|
+
sleep,
|
|
832
|
+
(id) => this.logger.warn(
|
|
833
|
+
`anthropic batch ${id} did not finish within the cap; the sessions it covers stay unextracted until the next pass`
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
return resolved ?? totalize(items, /* @__PURE__ */ new Map());
|
|
837
|
+
}
|
|
730
838
|
/**
|
|
731
839
|
* Says out loud whether the cache was actually used.
|
|
732
840
|
*
|
|
@@ -753,12 +861,12 @@ var AnthropicClient = class {
|
|
|
753
861
|
);
|
|
754
862
|
}
|
|
755
863
|
};
|
|
756
|
-
var GoogleClient = class {
|
|
757
|
-
constructor(key, model, logger,
|
|
864
|
+
var GoogleClient = class _GoogleClient {
|
|
865
|
+
constructor(key, model, logger, sleep2 = realSleep) {
|
|
758
866
|
this.key = key;
|
|
759
867
|
this.model = model;
|
|
760
868
|
this.logger = logger;
|
|
761
|
-
this.sleep =
|
|
869
|
+
this.sleep = sleep2;
|
|
762
870
|
}
|
|
763
871
|
provider = "google";
|
|
764
872
|
exhausted = false;
|
|
@@ -801,56 +909,129 @@ ${JSON.stringify(request2.schema)}` : "";
|
|
|
801
909
|
return null;
|
|
802
910
|
}
|
|
803
911
|
}
|
|
912
|
+
static GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
913
|
+
async submitBatch(items) {
|
|
914
|
+
if (items.length === 0) return null;
|
|
915
|
+
const model = items[0].request.model ?? this.model;
|
|
916
|
+
try {
|
|
917
|
+
const create = await fetch(
|
|
918
|
+
`${_GoogleClient.GEMINI_BASE}/models/${encodeURIComponent(model)}:batchGenerateContent`,
|
|
919
|
+
{
|
|
920
|
+
method: "POST",
|
|
921
|
+
headers: {
|
|
922
|
+
"Content-Type": "application/json",
|
|
923
|
+
"x-goog-api-key": this.key
|
|
924
|
+
},
|
|
925
|
+
body: JSON.stringify({
|
|
926
|
+
batch: {
|
|
927
|
+
display_name: "evrex-extraction",
|
|
928
|
+
input_config: {
|
|
929
|
+
requests: {
|
|
930
|
+
requests: items.map((item) => {
|
|
931
|
+
const shape = item.request.schema ? `
|
|
932
|
+
|
|
933
|
+
Respond with JSON matching exactly this schema:
|
|
934
|
+
${JSON.stringify(item.request.schema)}` : "";
|
|
935
|
+
return {
|
|
936
|
+
request: {
|
|
937
|
+
systemInstruction: {
|
|
938
|
+
parts: [{ text: item.request.system + shape }]
|
|
939
|
+
},
|
|
940
|
+
contents: [
|
|
941
|
+
{ role: "user", parts: [{ text: item.request.user }] }
|
|
942
|
+
],
|
|
943
|
+
generationConfig: { responseMimeType: "application/json" }
|
|
944
|
+
},
|
|
945
|
+
metadata: { key: item.id }
|
|
946
|
+
};
|
|
947
|
+
})
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
if (!create.ok) {
|
|
955
|
+
if (isExhaustedStatus(create.status, true)) this.exhausted = true;
|
|
956
|
+
this.logger.warn(
|
|
957
|
+
`google batch submit failed: ${create.status} ${(await create.text()).slice(0, 200)}`
|
|
958
|
+
);
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const name = (await create.json()).name;
|
|
962
|
+
if (!name) {
|
|
963
|
+
this.logger.warn("google batch: no job name returned");
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
return { provider: "google", id: name, itemIds: items.map((i) => i.id) };
|
|
967
|
+
} catch (err) {
|
|
968
|
+
this.logger.warn(`google batch submit failed: ${describeError(err)}`);
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async resolveBatch(handle) {
|
|
973
|
+
try {
|
|
974
|
+
const poll = await fetch(`${_GoogleClient.GEMINI_BASE}/${handle.id}`, {
|
|
975
|
+
headers: { "x-goog-api-key": this.key }
|
|
976
|
+
});
|
|
977
|
+
if (!poll.ok) {
|
|
978
|
+
this.logger.warn(`google batch poll failed: ${poll.status}`);
|
|
979
|
+
return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
|
|
980
|
+
}
|
|
981
|
+
const job = await poll.json();
|
|
982
|
+
const state = job.state ?? "";
|
|
983
|
+
if (state === "JOB_STATE_FAILED" || state === "JOB_STATE_CANCELLED" || state === "JOB_STATE_EXPIRED") {
|
|
984
|
+
this.logger.warn(`google batch ${handle.id} ended ${state}`);
|
|
985
|
+
return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
|
|
986
|
+
}
|
|
987
|
+
if (state !== "JOB_STATE_SUCCEEDED") return null;
|
|
988
|
+
const inlined = job.response?.inlinedResponses ?? [];
|
|
989
|
+
const results = /* @__PURE__ */ new Map();
|
|
990
|
+
handle.itemIds.forEach((id, i) => {
|
|
991
|
+
const text = inlined[i]?.response?.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
|
|
992
|
+
results.set(id, text ? parseJsonBody(text) : null);
|
|
993
|
+
});
|
|
994
|
+
return totalizeIds(handle.itemIds, results);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
this.logger.warn(`google batch resolve failed: ${describeError(err)}`);
|
|
997
|
+
return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async completeJsonBatch(items) {
|
|
1001
|
+
if (items.length === 0) return /* @__PURE__ */ new Map();
|
|
1002
|
+
const handle = await this.submitBatch(items);
|
|
1003
|
+
if (!handle) return totalize(items, /* @__PURE__ */ new Map());
|
|
1004
|
+
const resolved = await pollToCompletion(
|
|
1005
|
+
handle,
|
|
1006
|
+
(h) => this.resolveBatch(h),
|
|
1007
|
+
this.sleep,
|
|
1008
|
+
(id) => this.logger.warn(
|
|
1009
|
+
`google batch ${id} did not finish within the cap; leaving its sessions for a later pass`
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
return resolved ?? totalize(items, /* @__PURE__ */ new Map());
|
|
1013
|
+
}
|
|
804
1014
|
};
|
|
805
1015
|
var OpenAiCompatibleClient = class {
|
|
806
|
-
constructor(provider, baseUrl, key, model, logger,
|
|
1016
|
+
constructor(provider, baseUrl, key, model, logger, sleep2 = realSleep) {
|
|
807
1017
|
this.provider = provider;
|
|
808
1018
|
this.baseUrl = baseUrl;
|
|
809
1019
|
this.key = key;
|
|
810
1020
|
this.model = model;
|
|
811
1021
|
this.logger = logger;
|
|
812
|
-
this.sleep =
|
|
1022
|
+
this.sleep = sleep2;
|
|
1023
|
+
if (this.provider === "openai") {
|
|
1024
|
+
this.completeJsonBatch = (items) => this.runBatch(items);
|
|
1025
|
+
}
|
|
813
1026
|
}
|
|
814
1027
|
exhausted = false;
|
|
1028
|
+
completeJsonBatch;
|
|
815
1029
|
async completeJson(request2, attempt = 0) {
|
|
816
|
-
const strict = this.provider === "openai";
|
|
817
|
-
const shape = request2.schema && !strict ? `
|
|
818
|
-
|
|
819
|
-
Respond with JSON matching exactly this schema:
|
|
820
|
-
${JSON.stringify(request2.schema)}` : "";
|
|
821
1030
|
try {
|
|
822
1031
|
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
823
1032
|
method: "POST",
|
|
824
|
-
headers:
|
|
825
|
-
|
|
826
|
-
// A local server does not want one, and some reject an empty bearer
|
|
827
|
-
// token outright.
|
|
828
|
-
...this.key ? { Authorization: `Bearer ${this.key}` } : {}
|
|
829
|
-
},
|
|
830
|
-
body: JSON.stringify({
|
|
831
|
-
model: request2.model ?? this.model,
|
|
832
|
-
messages: [
|
|
833
|
-
{ role: "system", content: request2.system + shape },
|
|
834
|
-
{ role: "user", content: request2.user }
|
|
835
|
-
],
|
|
836
|
-
...request2.schema && strict ? {
|
|
837
|
-
response_format: {
|
|
838
|
-
type: "json_schema",
|
|
839
|
-
json_schema: {
|
|
840
|
-
name: "evrex_result",
|
|
841
|
-
strict: true,
|
|
842
|
-
schema: request2.schema
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
} : { response_format: { type: "json_object" } },
|
|
846
|
-
// Extraction is a reading task with a right answer, not a writing
|
|
847
|
-
// task, and a default sampling temperature makes it a different
|
|
848
|
-
// answer each run: one model scored 53% and then 20% on the same ten
|
|
849
|
-
// sessions before this was pinned. Indexing twice must not produce
|
|
850
|
-
// two different histories.
|
|
851
|
-
temperature: 0,
|
|
852
|
-
stream: false
|
|
853
|
-
})
|
|
1033
|
+
headers: this.authHeaders(),
|
|
1034
|
+
body: JSON.stringify(this.chatBody(request2))
|
|
854
1035
|
});
|
|
855
1036
|
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
|
|
856
1037
|
await waitBeforeRetry(res, attempt, this.sleep);
|
|
@@ -871,6 +1052,147 @@ ${JSON.stringify(request2.schema)}` : "";
|
|
|
871
1052
|
return null;
|
|
872
1053
|
}
|
|
873
1054
|
}
|
|
1055
|
+
authHeaders() {
|
|
1056
|
+
return {
|
|
1057
|
+
"Content-Type": "application/json",
|
|
1058
|
+
// A local server does not want one, and some reject an empty bearer token.
|
|
1059
|
+
...this.key ? { Authorization: `Bearer ${this.key}` } : {}
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
// One place a ModelRequest becomes the chat body, so completeJson and the
|
|
1063
|
+
// batch path cannot drift on response_format or the pinned temperature.
|
|
1064
|
+
chatBody(request2) {
|
|
1065
|
+
const strict = this.provider === "openai";
|
|
1066
|
+
const shape = request2.schema && !strict ? `
|
|
1067
|
+
|
|
1068
|
+
Respond with JSON matching exactly this schema:
|
|
1069
|
+
${JSON.stringify(request2.schema)}` : "";
|
|
1070
|
+
return {
|
|
1071
|
+
model: request2.model ?? this.model,
|
|
1072
|
+
messages: [
|
|
1073
|
+
{ role: "system", content: request2.system + shape },
|
|
1074
|
+
{ role: "user", content: request2.user }
|
|
1075
|
+
],
|
|
1076
|
+
...request2.schema && strict ? {
|
|
1077
|
+
response_format: {
|
|
1078
|
+
type: "json_schema",
|
|
1079
|
+
json_schema: {
|
|
1080
|
+
name: "evrex_result",
|
|
1081
|
+
strict: true,
|
|
1082
|
+
schema: request2.schema
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} : { response_format: { type: "json_object" } },
|
|
1086
|
+
// A reading task with a right answer: temperature pinned so indexing
|
|
1087
|
+
// twice is the same history.
|
|
1088
|
+
temperature: 0,
|
|
1089
|
+
stream: false
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
async runBatch(items) {
|
|
1093
|
+
const results = /* @__PURE__ */ new Map();
|
|
1094
|
+
if (items.length === 0) return results;
|
|
1095
|
+
const auth = this.key ? { Authorization: `Bearer ${this.key}` } : {};
|
|
1096
|
+
try {
|
|
1097
|
+
const jsonl = items.map(
|
|
1098
|
+
(item) => JSON.stringify({
|
|
1099
|
+
custom_id: item.id,
|
|
1100
|
+
method: "POST",
|
|
1101
|
+
url: "/v1/chat/completions",
|
|
1102
|
+
body: this.chatBody(item.request)
|
|
1103
|
+
})
|
|
1104
|
+
).join("\n");
|
|
1105
|
+
const form = new FormData();
|
|
1106
|
+
form.append("purpose", "batch");
|
|
1107
|
+
form.append(
|
|
1108
|
+
"file",
|
|
1109
|
+
new Blob([jsonl], { type: "application/jsonl" }),
|
|
1110
|
+
"evrex-batch.jsonl"
|
|
1111
|
+
);
|
|
1112
|
+
const upload = await fetch(`${this.baseUrl}/files`, {
|
|
1113
|
+
method: "POST",
|
|
1114
|
+
headers: auth,
|
|
1115
|
+
body: form
|
|
1116
|
+
});
|
|
1117
|
+
if (!upload.ok) {
|
|
1118
|
+
if (isExhaustedStatus(upload.status, true)) this.exhausted = true;
|
|
1119
|
+
this.logger.warn(
|
|
1120
|
+
`openai batch upload failed: ${upload.status} ${(await upload.text()).slice(0, 200)}`
|
|
1121
|
+
);
|
|
1122
|
+
return totalize(items, results);
|
|
1123
|
+
}
|
|
1124
|
+
const fileId = (await upload.json()).id;
|
|
1125
|
+
if (!fileId) return totalize(items, results);
|
|
1126
|
+
const created = await fetch(`${this.baseUrl}/batches`, {
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
headers: this.authHeaders(),
|
|
1129
|
+
body: JSON.stringify({
|
|
1130
|
+
input_file_id: fileId,
|
|
1131
|
+
endpoint: "/v1/chat/completions",
|
|
1132
|
+
completion_window: "24h"
|
|
1133
|
+
})
|
|
1134
|
+
});
|
|
1135
|
+
if (!created.ok) {
|
|
1136
|
+
this.logger.warn(
|
|
1137
|
+
`openai batch create failed: ${created.status} ${(await created.text()).slice(0, 200)}`
|
|
1138
|
+
);
|
|
1139
|
+
return totalize(items, results);
|
|
1140
|
+
}
|
|
1141
|
+
const batchId = (await created.json()).id;
|
|
1142
|
+
if (!batchId) return totalize(items, results);
|
|
1143
|
+
let status = "";
|
|
1144
|
+
let outputFileId;
|
|
1145
|
+
const startedAt = Date.now();
|
|
1146
|
+
while (status !== "completed") {
|
|
1147
|
+
if (Date.now() - startedAt > BATCH_TIMEOUT_MS) {
|
|
1148
|
+
this.logger.warn(
|
|
1149
|
+
`openai batch ${batchId} did not finish within the cap; leaving its sessions for a later pass`
|
|
1150
|
+
);
|
|
1151
|
+
return totalize(items, results);
|
|
1152
|
+
}
|
|
1153
|
+
await this.sleep(BATCH_POLL_MS);
|
|
1154
|
+
const poll = await fetch(`${this.baseUrl}/batches/${batchId}`, {
|
|
1155
|
+
headers: auth
|
|
1156
|
+
});
|
|
1157
|
+
if (!poll.ok) {
|
|
1158
|
+
this.logger.warn(`openai batch poll failed: ${poll.status}`);
|
|
1159
|
+
return totalize(items, results);
|
|
1160
|
+
}
|
|
1161
|
+
const job = await poll.json();
|
|
1162
|
+
status = job.status ?? "";
|
|
1163
|
+
outputFileId = job.output_file_id;
|
|
1164
|
+
if (status === "failed" || status === "expired" || status === "cancelled") {
|
|
1165
|
+
this.logger.warn(`openai batch ${batchId} ended ${status}`);
|
|
1166
|
+
return totalize(items, results);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (!outputFileId) return totalize(items, results);
|
|
1170
|
+
const out = await fetch(`${this.baseUrl}/files/${outputFileId}/content`, {
|
|
1171
|
+
headers: auth
|
|
1172
|
+
});
|
|
1173
|
+
if (!out.ok) {
|
|
1174
|
+
this.logger.warn(`openai batch results fetch failed: ${out.status}`);
|
|
1175
|
+
return totalize(items, results);
|
|
1176
|
+
}
|
|
1177
|
+
for (const line of (await out.text()).split("\n")) {
|
|
1178
|
+
if (!line.trim()) continue;
|
|
1179
|
+
try {
|
|
1180
|
+
const row = JSON.parse(line);
|
|
1181
|
+
const content = row.response?.body?.choices?.[0]?.message?.content;
|
|
1182
|
+
if (row.custom_id) {
|
|
1183
|
+
results.set(
|
|
1184
|
+
row.custom_id,
|
|
1185
|
+
typeof content === "string" ? parseJsonBody(content) : null
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
} catch {
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
this.logger.warn(`openai batch failed: ${describeError(err)}`);
|
|
1193
|
+
}
|
|
1194
|
+
return totalize(items, results);
|
|
1195
|
+
}
|
|
874
1196
|
};
|
|
875
1197
|
function describeError(err) {
|
|
876
1198
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -882,9 +1204,9 @@ function createModelClient(key, options = {}) {
|
|
|
882
1204
|
const model = options.model ?? DEFAULT_MODELS[provider];
|
|
883
1205
|
const trimmed = key.trim();
|
|
884
1206
|
if (provider === "anthropic") return new AnthropicClient(trimmed, model, logger);
|
|
885
|
-
const
|
|
1207
|
+
const sleep2 = options.sleep ?? realSleep;
|
|
886
1208
|
if (provider === "google")
|
|
887
|
-
return new GoogleClient(trimmed, model, logger,
|
|
1209
|
+
return new GoogleClient(trimmed, model, logger, sleep2);
|
|
888
1210
|
const cli = CLI_SPECS[provider];
|
|
889
1211
|
if (cli) return new CliClient(cli, model, logger);
|
|
890
1212
|
if (provider === "local") {
|
|
@@ -894,7 +1216,7 @@ function createModelClient(key, options = {}) {
|
|
|
894
1216
|
null,
|
|
895
1217
|
model,
|
|
896
1218
|
logger,
|
|
897
|
-
|
|
1219
|
+
sleep2
|
|
898
1220
|
);
|
|
899
1221
|
}
|
|
900
1222
|
const baseUrl = options.baseUrl ?? OPENAI_COMPATIBLE_BASE_URLS[provider];
|
|
@@ -905,7 +1227,7 @@ function createModelClient(key, options = {}) {
|
|
|
905
1227
|
trimmed,
|
|
906
1228
|
model,
|
|
907
1229
|
logger,
|
|
908
|
-
|
|
1230
|
+
sleep2
|
|
909
1231
|
);
|
|
910
1232
|
}
|
|
911
1233
|
|
package/dist/pretool.js
CHANGED
|
@@ -49,7 +49,7 @@ var evrexApi = {
|
|
|
49
49
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
50
50
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
51
51
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
52
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
52
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
53
53
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
54
54
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
55
55
|
// interleaved, each with the handle evrex_expand takes.
|
package/dist/tickets.js
CHANGED
|
@@ -63,7 +63,7 @@ var init_client = __esm({
|
|
|
63
63
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
64
64
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
65
65
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
66
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
66
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
67
67
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
68
68
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
69
69
|
// interleaved, each with the handle evrex_expand takes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evrex-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "MCP server that gives coding agents the recorded reasoning behind a repo: prior decisions, hard constraints, and approaches already rejected.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -36,27 +36,28 @@
|
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=20"
|
|
38
38
|
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"bundle": "node scripts/bundle.mjs",
|
|
42
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
43
|
+
"lint": "eslint .",
|
|
44
|
+
"check-types": "tsc --noEmit",
|
|
45
|
+
"test": "tsc -p tsconfig.json && node --test dist/*.test.js",
|
|
46
|
+
"prepack": "pnpm run bundle"
|
|
47
|
+
},
|
|
39
48
|
"dependencies": {
|
|
40
49
|
"@anthropic-ai/sdk": "^0.72.0",
|
|
41
50
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
42
51
|
"zod": "^3.24.1"
|
|
43
52
|
},
|
|
44
53
|
"devDependencies": {
|
|
54
|
+
"@repo/eslint-config": "workspace:*",
|
|
55
|
+
"@repo/ingest-core": "workspace:*",
|
|
56
|
+
"@repo/llm-core": "workspace:*",
|
|
57
|
+
"@repo/typescript-config": "workspace:*",
|
|
45
58
|
"@types/node": "^24.0.0",
|
|
46
59
|
"esbuild": "^0.25.12",
|
|
47
60
|
"eslint": "^9.39.1",
|
|
48
|
-
"typescript": "^5.9.2"
|
|
49
|
-
"@repo/eslint-config": "0.0.0",
|
|
50
|
-
"@repo/ingest-core": "0.0.0",
|
|
51
|
-
"@repo/llm-core": "0.0.0",
|
|
52
|
-
"@repo/typescript-config": "0.0.0"
|
|
53
|
-
},
|
|
54
|
-
"scripts": {
|
|
55
|
-
"build": "tsc -p tsconfig.json",
|
|
56
|
-
"bundle": "node scripts/bundle.mjs",
|
|
57
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
58
|
-
"lint": "eslint .",
|
|
59
|
-
"check-types": "tsc --noEmit",
|
|
60
|
-
"test": "tsc -p tsconfig.json && node --test dist/*.test.js"
|
|
61
|
+
"typescript": "^5.9.2"
|
|
61
62
|
}
|
|
62
|
-
}
|
|
63
|
+
}
|