@martinloop/mcp 0.3.5 → 0.3.7
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/README.md +137 -137
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +2 -0
- package/dist/prompts.js +19 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +125 -17
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +12 -0
- package/dist/server.js +18 -7
- package/dist/tools/doctor.js +6 -0
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +5 -1
- package/dist/tools/tool-support.d.ts +1 -1
- package/dist/tools/tool-support.js +2 -1
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
- package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
- package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/index.js +8 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
- package/dist/vendor/adapters/claude-cli.js +21 -20
- package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
- package/dist/vendor/adapters/cli-bridge.js +94 -8
- package/dist/vendor/adapters/codex-launcher.js +18 -7
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
- package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
- package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
- package/dist/vendor/adapters/contracts/src/governance.js +11 -0
- package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
- package/dist/vendor/adapters/contracts/src/identity.js +30 -0
- package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
- package/dist/vendor/adapters/contracts/src/index.js +225 -0
- package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
- package/dist/vendor/adapters/contracts/src/operator.js +11 -0
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
- package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
- package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
- package/dist/vendor/adapters/core/src/compiler.js +52 -0
- package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
- package/dist/vendor/adapters/core/src/context-graph.js +371 -0
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
- package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
- package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
- package/dist/vendor/adapters/core/src/grounding.js +273 -0
- package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
- package/dist/vendor/adapters/core/src/identity.js +86 -0
- package/dist/vendor/adapters/core/src/index.d.ts +186 -0
- package/dist/vendor/adapters/core/src/index.js +1495 -0
- package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
- package/dist/vendor/adapters/core/src/leash.js +497 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
- package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
- package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
- package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
- package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
- package/dist/vendor/adapters/core/src/policy.js +704 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
- package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
- package/dist/vendor/adapters/core/src/rollback.js +253 -0
- package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
- package/dist/vendor/adapters/core/src/routing.js +336 -0
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
- package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
- package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
- package/dist/vendor/adapters/core/src/trajectory.js +295 -0
- package/dist/vendor/adapters/openai-compatible.js +84 -51
- package/dist/vendor/adapters/verifier-only.js +2 -2
- package/dist/vendor/contracts/index.d.ts +2 -2
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/compiler.js +4 -9
- package/dist/vendor/core/index.d.ts +10 -2
- package/dist/vendor/core/index.js +32 -91
- package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/core/persistence/memory-store.js +115 -0
- package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/core/persistence/trace-store.js +125 -0
- package/dist/vendor/core/repo-analyzer.d.ts +53 -0
- package/dist/vendor/core/repo-analyzer.js +263 -0
- package/dist/vendor/core/rollback.js +2 -3
- package/dist/vendor/core/routing.d.ts +42 -0
- package/dist/vendor/core/routing.js +156 -5
- package/dist/vendor/core/test-integrity.d.ts +64 -0
- package/dist/vendor/core/test-integrity.js +197 -0
- package/dist/workflow-state.d.ts +1 -1
- package/dist/workflow-state.js +12 -4
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { cloneCircuitBreakDecision, cloneTrajectoryAssessment } from "../../../contracts/index.js";
|
|
2
|
+
const STOP_WORDS = new Set([
|
|
3
|
+
"about",
|
|
4
|
+
"after",
|
|
5
|
+
"again",
|
|
6
|
+
"along",
|
|
7
|
+
"also",
|
|
8
|
+
"been",
|
|
9
|
+
"before",
|
|
10
|
+
"being",
|
|
11
|
+
"between",
|
|
12
|
+
"could",
|
|
13
|
+
"failed",
|
|
14
|
+
"failure",
|
|
15
|
+
"from",
|
|
16
|
+
"have",
|
|
17
|
+
"into",
|
|
18
|
+
"just",
|
|
19
|
+
"keep",
|
|
20
|
+
"made",
|
|
21
|
+
"make",
|
|
22
|
+
"need",
|
|
23
|
+
"only",
|
|
24
|
+
"over",
|
|
25
|
+
"same",
|
|
26
|
+
"should",
|
|
27
|
+
"still",
|
|
28
|
+
"than",
|
|
29
|
+
"that",
|
|
30
|
+
"their",
|
|
31
|
+
"them",
|
|
32
|
+
"then",
|
|
33
|
+
"there",
|
|
34
|
+
"these",
|
|
35
|
+
"they",
|
|
36
|
+
"this",
|
|
37
|
+
"those",
|
|
38
|
+
"touching",
|
|
39
|
+
"without",
|
|
40
|
+
"would"
|
|
41
|
+
]);
|
|
42
|
+
const STALL_FAILURE_CLASSES = new Set([
|
|
43
|
+
"logic_error",
|
|
44
|
+
"verification_failure",
|
|
45
|
+
"repo_grounding_failure",
|
|
46
|
+
"scope_creep",
|
|
47
|
+
"no_progress"
|
|
48
|
+
]);
|
|
49
|
+
export function assessTrajectory(input) {
|
|
50
|
+
const attempts = input.attempts.filter((attempt) => attempt.index > 0);
|
|
51
|
+
if (attempts.length === 0) {
|
|
52
|
+
return cloneTrajectoryAssessment({
|
|
53
|
+
status: "healthy",
|
|
54
|
+
confidence: "low",
|
|
55
|
+
summary: "No prior attempts exist, so Martin has no drift evidence yet.",
|
|
56
|
+
signals: []
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const signals = [];
|
|
60
|
+
const recentAttempts = attempts.slice(-4);
|
|
61
|
+
const lastThree = recentAttempts.slice(-3);
|
|
62
|
+
const lastTwo = recentAttempts.slice(-2);
|
|
63
|
+
const oscillation = detectFailureOscillation(lastThree);
|
|
64
|
+
if (oscillation) {
|
|
65
|
+
signals.push(oscillation);
|
|
66
|
+
}
|
|
67
|
+
const repetition = detectAttemptRepetition(lastThree);
|
|
68
|
+
if (repetition) {
|
|
69
|
+
signals.push(repetition);
|
|
70
|
+
}
|
|
71
|
+
const verificationStall = detectVerificationStall(lastTwo);
|
|
72
|
+
if (verificationStall) {
|
|
73
|
+
signals.push(verificationStall);
|
|
74
|
+
}
|
|
75
|
+
const objectiveDrift = detectObjectiveDrift({
|
|
76
|
+
objective: input.objective,
|
|
77
|
+
verificationPlan: input.verificationPlan,
|
|
78
|
+
attempts: lastTwo
|
|
79
|
+
});
|
|
80
|
+
if (objectiveDrift) {
|
|
81
|
+
signals.push(objectiveDrift);
|
|
82
|
+
}
|
|
83
|
+
const status = deriveTrajectoryStatus(signals);
|
|
84
|
+
const confidence = deriveTrajectoryConfidence(signals);
|
|
85
|
+
const recommendedIntervention = deriveRecommendedIntervention(signals);
|
|
86
|
+
if (signals.length === 0) {
|
|
87
|
+
return cloneTrajectoryAssessment({
|
|
88
|
+
status: "healthy",
|
|
89
|
+
confidence: "medium",
|
|
90
|
+
summary: "Recent attempts stay aligned enough that Martin does not need a circuit break yet.",
|
|
91
|
+
signals: []
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const summary = summarizeTrajectory(status, signals);
|
|
95
|
+
return cloneTrajectoryAssessment({
|
|
96
|
+
status,
|
|
97
|
+
confidence,
|
|
98
|
+
summary,
|
|
99
|
+
signals,
|
|
100
|
+
...(recommendedIntervention ? { recommendedIntervention } : {})
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function decideCircuitBreak(input) {
|
|
104
|
+
const assessment = assessTrajectory(input);
|
|
105
|
+
const remainingIterations = input.remainingIterations ?? Number.POSITIVE_INFINITY;
|
|
106
|
+
const stopForHighSignal = assessment.signals.some((signal) => signal.severity === "high");
|
|
107
|
+
const stopForLateDrift = assessment.status === "drifting" &&
|
|
108
|
+
remainingIterations <= 1 &&
|
|
109
|
+
assessment.signals.some((signal) => signal.code === "objective_drift");
|
|
110
|
+
const stopForLateStall = assessment.status === "watch" &&
|
|
111
|
+
remainingIterations <= 1 &&
|
|
112
|
+
assessment.signals.some((signal) => signal.code === "verification_stall");
|
|
113
|
+
const shouldStop = stopForHighSignal || stopForLateDrift || stopForLateStall;
|
|
114
|
+
return cloneCircuitBreakDecision({
|
|
115
|
+
shouldStop,
|
|
116
|
+
reason: shouldStop
|
|
117
|
+
? assessment.summary
|
|
118
|
+
: "Trajectory signals do not justify a hard circuit break yet.",
|
|
119
|
+
assessment,
|
|
120
|
+
...(assessment.recommendedIntervention
|
|
121
|
+
? { recommendedIntervention: assessment.recommendedIntervention }
|
|
122
|
+
: {})
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function detectFailureOscillation(attempts) {
|
|
126
|
+
if (attempts.length < 3) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const [first, second, third] = attempts;
|
|
130
|
+
if (!first?.failureClass ||
|
|
131
|
+
!second?.failureClass ||
|
|
132
|
+
!third?.failureClass ||
|
|
133
|
+
first.failureClass === second.failureClass ||
|
|
134
|
+
first.failureClass !== third.failureClass) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
code: "failure_oscillation",
|
|
139
|
+
severity: "high",
|
|
140
|
+
summary: `Attempts ${first.index}, ${second.index}, and ${third.index} oscillate between failure classes instead of converging.`,
|
|
141
|
+
attemptIndexes: [first.index, second.index, third.index]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function detectAttemptRepetition(attempts) {
|
|
145
|
+
if (attempts.length < 3) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const tokenSets = attempts.map((attempt) => toTokenSet(attempt.summary));
|
|
149
|
+
if (tokenSets.some((tokens) => tokens.size < 4)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const pairScores = [
|
|
153
|
+
jaccardSimilarity(tokenSets[0], tokenSets[1]),
|
|
154
|
+
jaccardSimilarity(tokenSets[1], tokenSets[2]),
|
|
155
|
+
jaccardSimilarity(tokenSets[0], tokenSets[2])
|
|
156
|
+
];
|
|
157
|
+
if (pairScores.every((score) => score >= 0.55)) {
|
|
158
|
+
const matchedTerms = intersectSets(tokenSets[0], new Set(intersectSets(tokenSets[1], tokenSets[2])));
|
|
159
|
+
return {
|
|
160
|
+
code: "attempt_repetition",
|
|
161
|
+
severity: "high",
|
|
162
|
+
summary: `Attempts ${attempts.map((attempt) => attempt.index).join(", ")} are materially repetitive and are not exploring a new path.`,
|
|
163
|
+
attemptIndexes: attempts.map((attempt) => attempt.index),
|
|
164
|
+
...(matchedTerms.length > 0 ? { matchedTerms: matchedTerms.slice(0, 6) } : {})
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function detectVerificationStall(attempts) {
|
|
170
|
+
if (attempts.length < 2) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const relevant = attempts.filter((attempt) => {
|
|
174
|
+
const failureClass = attempt.failureClass;
|
|
175
|
+
return failureClass !== undefined && STALL_FAILURE_CLASSES.has(failureClass);
|
|
176
|
+
});
|
|
177
|
+
if (relevant.length < 2) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const tokenSets = relevant.map((attempt) => toTokenSet(attempt.summary));
|
|
181
|
+
const overlap = tokenSets
|
|
182
|
+
.slice(1)
|
|
183
|
+
.map((tokens, index) => jaccardSimilarity(tokenSets[index], tokens))
|
|
184
|
+
.reduce((highest, score) => Math.max(highest, score), 0);
|
|
185
|
+
if (overlap < 0.35) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const latest = relevant.at(-1);
|
|
189
|
+
return {
|
|
190
|
+
code: "verification_stall",
|
|
191
|
+
severity: "medium",
|
|
192
|
+
summary: `Recent attempts keep landing on the same verifier-facing failure surface (${latest.failureClass}).`,
|
|
193
|
+
attemptIndexes: relevant.map((attempt) => attempt.index)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function detectObjectiveDrift(input) {
|
|
197
|
+
if (input.attempts.length < 2) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const objectiveTerms = toTokenSet(`${input.objective} ${(input.verificationPlan ?? []).join(" ")}`);
|
|
201
|
+
if (objectiveTerms.size < 4) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const summaryTerms = input.attempts.map((attempt) => toTokenSet(attempt.summary));
|
|
205
|
+
const overlaps = summaryTerms.map((terms) => jaccardSimilarity(terms, objectiveTerms));
|
|
206
|
+
if (overlaps.some((score) => score >= 0.18)) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const sharedDriftTerms = intersectSets(summaryTerms[0], summaryTerms[1]);
|
|
210
|
+
if (sharedDriftTerms.length < 3) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
code: "objective_drift",
|
|
215
|
+
severity: "medium",
|
|
216
|
+
summary: "Recent attempt summaries are converging on work that does not overlap with the original objective or verifier plan.",
|
|
217
|
+
attemptIndexes: input.attempts.map((attempt) => attempt.index),
|
|
218
|
+
matchedTerms: sharedDriftTerms.slice(0, 6)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function deriveTrajectoryStatus(signals) {
|
|
222
|
+
if (signals.some((signal) => signal.severity === "high")) {
|
|
223
|
+
return "stalled";
|
|
224
|
+
}
|
|
225
|
+
if (signals.some((signal) => signal.code === "objective_drift")) {
|
|
226
|
+
return "drifting";
|
|
227
|
+
}
|
|
228
|
+
if (signals.some((signal) => signal.severity === "medium")) {
|
|
229
|
+
return "watch";
|
|
230
|
+
}
|
|
231
|
+
return "healthy";
|
|
232
|
+
}
|
|
233
|
+
function deriveTrajectoryConfidence(signals) {
|
|
234
|
+
if (signals.some((signal) => signal.severity === "high")) {
|
|
235
|
+
return "high";
|
|
236
|
+
}
|
|
237
|
+
if (signals.some((signal) => signal.severity === "medium")) {
|
|
238
|
+
return "medium";
|
|
239
|
+
}
|
|
240
|
+
return "low";
|
|
241
|
+
}
|
|
242
|
+
function deriveRecommendedIntervention(signals) {
|
|
243
|
+
if (signals.some((signal) => signal.code === "failure_oscillation" || signal.code === "attempt_repetition")) {
|
|
244
|
+
return "escalate_human";
|
|
245
|
+
}
|
|
246
|
+
if (signals.some((signal) => signal.code === "objective_drift")) {
|
|
247
|
+
return "tighten_task";
|
|
248
|
+
}
|
|
249
|
+
if (signals.some((signal) => signal.code === "verification_stall")) {
|
|
250
|
+
return "run_verifier";
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
function summarizeTrajectory(status, signals) {
|
|
255
|
+
const lead = signals[0];
|
|
256
|
+
if (!lead) {
|
|
257
|
+
return "Trajectory is healthy.";
|
|
258
|
+
}
|
|
259
|
+
switch (status) {
|
|
260
|
+
case "stalled":
|
|
261
|
+
return `Trajectory stalled: ${lead.summary}`;
|
|
262
|
+
case "drifting":
|
|
263
|
+
return `Trajectory drift detected: ${lead.summary}`;
|
|
264
|
+
case "watch":
|
|
265
|
+
return `Trajectory warning: ${lead.summary}`;
|
|
266
|
+
default:
|
|
267
|
+
return lead.summary;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function toTokenSet(text) {
|
|
271
|
+
return new Set((text ?? "")
|
|
272
|
+
.toLowerCase()
|
|
273
|
+
.match(/[a-z][a-z0-9_/-]{2,}/g)
|
|
274
|
+
?.flatMap((token) => splitCompositeToken(token))
|
|
275
|
+
.map((token) => token.trim())
|
|
276
|
+
.filter((token) => token.length >= 3 && !STOP_WORDS.has(token)) ?? []);
|
|
277
|
+
}
|
|
278
|
+
function splitCompositeToken(token) {
|
|
279
|
+
return token
|
|
280
|
+
.split(/[-_/]/g)
|
|
281
|
+
.flatMap((part) => part.split(/(?=[A-Z])/g))
|
|
282
|
+
.map((part) => part.toLowerCase())
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
}
|
|
285
|
+
function jaccardSimilarity(left, right) {
|
|
286
|
+
if (left.size === 0 || right.size === 0) {
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
const intersection = intersectSets(left, right).length;
|
|
290
|
+
const union = new Set([...left, ...right]).size;
|
|
291
|
+
return union === 0 ? 0 : intersection / union;
|
|
292
|
+
}
|
|
293
|
+
function intersectSets(left, right) {
|
|
294
|
+
return [...left].filter((term) => right.has(term));
|
|
295
|
+
}
|
|
@@ -166,70 +166,103 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
166
166
|
failure: { message: "budget_preflight_exceeded", classHint: "budget_pressure" }
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
-
// Call the OpenAI-compatible endpoint
|
|
169
|
+
// Call the OpenAI-compatible endpoint with exponential-backoff retry on
|
|
170
|
+
// transient failures (429 rate-limit, 503/5xx server errors, network errors).
|
|
171
|
+
// Auth errors (401/403) and bad-request errors (400) are not retried — they
|
|
172
|
+
// indicate a permanent configuration problem.
|
|
173
|
+
const MAX_RETRIES = 3;
|
|
174
|
+
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
170
175
|
const endpoint = `${baseUrl}/v1/chat/completions`;
|
|
171
176
|
let responseText = "";
|
|
172
177
|
let tokensIn = estimated.tokensIn;
|
|
173
178
|
let tokensOut = 0;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
179
|
+
const headers = { "Content-Type": "application/json" };
|
|
180
|
+
if (apiKey)
|
|
181
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
182
|
+
if (baseUrl.includes("openrouter")) {
|
|
183
|
+
headers["HTTP-Referer"] = "https://martinloop.com";
|
|
184
|
+
headers["X-Title"] = "MartinLoop";
|
|
185
|
+
}
|
|
186
|
+
const requestBody = JSON.stringify({
|
|
187
|
+
model,
|
|
188
|
+
messages: [
|
|
189
|
+
{ role: "system", content: systemPrompt },
|
|
190
|
+
{ role: "user", content: prompt }
|
|
191
|
+
],
|
|
192
|
+
temperature: 0.2,
|
|
193
|
+
max_tokens: 8192
|
|
194
|
+
});
|
|
195
|
+
let lastError = "";
|
|
196
|
+
let succeeded = false;
|
|
197
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetchFn(endpoint, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers,
|
|
204
|
+
body: requestBody,
|
|
205
|
+
signal: controller.signal
|
|
206
|
+
});
|
|
207
|
+
const body = (await res.json());
|
|
208
|
+
if (!res.ok || body.error) {
|
|
209
|
+
const errMsg = body.error?.message ?? `HTTP ${res.status}`;
|
|
210
|
+
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES - 1) {
|
|
211
|
+
lastError = errMsg;
|
|
212
|
+
// Exponential backoff: 1s, 2s, 4s
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
status: "failed",
|
|
218
|
+
summary: `${model} API error: ${errMsg}`,
|
|
219
|
+
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
220
|
+
verification: { passed: false, summary: "API call failed before verifier." },
|
|
221
|
+
failure: { message: errMsg, classHint: "infrastructure_error" }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
responseText = body.choices?.[0]?.message?.content ?? "";
|
|
225
|
+
if (body.usage) {
|
|
226
|
+
tokensIn = body.usage.prompt_tokens ?? tokensIn;
|
|
227
|
+
tokensOut = body.usage.completion_tokens ?? 0;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
tokensOut = Math.ceil(responseText.length / CHARS_PER_TOKEN);
|
|
231
|
+
}
|
|
232
|
+
succeeded = true;
|
|
233
|
+
break;
|
|
209
234
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
catch (error) {
|
|
236
|
+
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
237
|
+
if (isAbort || attempt === MAX_RETRIES - 1) {
|
|
238
|
+
const message = isAbort
|
|
239
|
+
? `${model} request timed out after ${timeoutMs}ms`
|
|
240
|
+
: String(error);
|
|
241
|
+
return {
|
|
242
|
+
status: "failed",
|
|
243
|
+
summary: message,
|
|
244
|
+
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
245
|
+
verification: { passed: false, summary: isAbort ? "Request timed out." : "Network error." },
|
|
246
|
+
failure: { message, classHint: "infrastructure_error" }
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// Transient network error — retry with backoff
|
|
250
|
+
lastError = String(error);
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
|
214
252
|
}
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
finally {
|
|
254
|
+
clearTimeout(timer);
|
|
217
255
|
}
|
|
218
256
|
}
|
|
219
|
-
|
|
220
|
-
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
221
|
-
const message = isAbort ? `${model} request timed out after ${timeoutMs}ms` : String(error);
|
|
257
|
+
if (!succeeded) {
|
|
222
258
|
return {
|
|
223
259
|
status: "failed",
|
|
224
|
-
summary:
|
|
260
|
+
summary: `${model} API error after ${MAX_RETRIES} attempts: ${lastError}`,
|
|
225
261
|
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
226
|
-
verification: { passed: false, summary:
|
|
227
|
-
failure: { message, classHint: "infrastructure_error" }
|
|
262
|
+
verification: { passed: false, summary: "API call failed after retries." },
|
|
263
|
+
failure: { message: lastError, classHint: "infrastructure_error" }
|
|
228
264
|
};
|
|
229
265
|
}
|
|
230
|
-
finally {
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
}
|
|
233
266
|
if (!responseText.trim()) {
|
|
234
267
|
return {
|
|
235
268
|
status: "failed",
|
|
@@ -20,11 +20,11 @@ export function createVerifierOnlyAdapter(options = {}) {
|
|
|
20
20
|
const shouldTrackVerifierWrites = request.context.verificationPlan.length > 0 ||
|
|
21
21
|
(request.context.verificationStack?.length ?? 0) > 0;
|
|
22
22
|
const baselineChangedFiles = shouldTrackVerifierWrites
|
|
23
|
-
? new Set(await readGitChangedFiles(workingDirectory, 5_000))
|
|
23
|
+
? new Set(await readGitChangedFiles(workingDirectory, 5_000, options.spawnImpl))
|
|
24
24
|
: new Set();
|
|
25
25
|
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl);
|
|
26
26
|
const changedFiles = shouldTrackVerifierWrites
|
|
27
|
-
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
27
|
+
? (await readGitChangedFiles(workingDirectory, 5_000, options.spawnImpl)).filter((file) => !baselineChangedFiles.has(file))
|
|
28
28
|
: [];
|
|
29
29
|
const execution = { changedFiles };
|
|
30
30
|
if (verification.passed) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type LoopStatus = "queued" | "running" | "verifying" | "completed" | "failed" | "exited";
|
|
2
2
|
export type LoopLifecycleState = "created" | "running" | "verifying" | "completed" | "budget_exit" | "diminishing_returns" | "stuck_exit" | "human_escalation";
|
|
3
|
-
export declare const FAILURE_CLASSES: readonly ["logic_error", "hallucination", "syntax_error", "type_error", "test_regression", "scope_creep", "no_progress", "repo_grounding_failure", "verification_failure", "environment_mismatch", "budget_pressure", "safety_leash_blocked"];
|
|
3
|
+
export declare const FAILURE_CLASSES: readonly ["logic_error", "hallucination", "syntax_error", "type_error", "test_regression", "scope_creep", "no_progress", "repo_grounding_failure", "verification_failure", "environment_mismatch", "budget_pressure", "safety_leash_blocked", "sandbox_write_blocked"];
|
|
4
4
|
export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
5
5
|
export type InterventionType = "compress_context" | "change_model" | "tighten_task" | "switch_adapter" | "run_verifier" | "escalate_human" | "stop_loop";
|
|
6
6
|
export type LoopEventType = "run.started" | "attempt.started" | "attempt.completed" | "failure.classified" | "intervention.selected" | "verification.completed" | "budget.updated" | "run.completed";
|
|
@@ -23,7 +23,7 @@ export interface LoopTask {
|
|
|
23
23
|
acceptanceCriteria?: string[];
|
|
24
24
|
}
|
|
25
25
|
export type ExecutionProfile = "strict_local" | "ci_safe" | "staging_controlled" | "research_untrusted";
|
|
26
|
-
export type MutationMode = "edit"
|
|
26
|
+
export type MutationMode = "edit";
|
|
27
27
|
export interface ApprovalPolicy {
|
|
28
28
|
dependencyAdds?: boolean;
|
|
29
29
|
migrations?: boolean;
|
|
@@ -8,15 +8,10 @@ export function compilePromptPacket(request) {
|
|
|
8
8
|
const priorFailurePatterns = request.previousAttempts
|
|
9
9
|
.filter((a) => a.failureClass && a.intervention)
|
|
10
10
|
.map((a) => `${a.failureClass}:${a.intervention}`);
|
|
11
|
-
const guidanceParts =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
]
|
|
16
|
-
: [
|
|
17
|
-
"Only modify files directly required to satisfy the contract.",
|
|
18
|
-
"Do not touch files outside the allowed paths."
|
|
19
|
-
];
|
|
11
|
+
const guidanceParts = [
|
|
12
|
+
"Only modify files directly required to satisfy the contract.",
|
|
13
|
+
"Do not touch files outside the allowed paths."
|
|
14
|
+
];
|
|
20
15
|
if (request.context.allowedPaths && request.context.allowedPaths.length > 0) {
|
|
21
16
|
guidanceParts.push(`Allowed paths: ${request.context.allowedPaths.join(", ")}.`);
|
|
22
17
|
}
|
|
@@ -14,8 +14,12 @@ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, ver
|
|
|
14
14
|
export type { LocalIdentityAuthority } from "./identity.js";
|
|
15
15
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
16
16
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
17
|
-
export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
|
|
18
|
-
export type { RouteDecision, RouteClassificationInput } from "./routing.js";
|
|
17
|
+
export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
|
|
18
|
+
export type { RouteDecision, RouteClassificationInput, AvailableEngine } from "./routing.js";
|
|
19
|
+
export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
|
|
20
|
+
export type { TestIntegrityReport, TestIntegrityFinding } from "./test-integrity.js";
|
|
21
|
+
export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
|
|
22
|
+
export type { RepoStyleProfile } from "./repo-analyzer.js";
|
|
19
23
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
20
24
|
export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
|
|
21
25
|
export { compilePromptPacket } from "./compiler.js";
|
|
@@ -24,6 +28,10 @@ export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoop
|
|
|
24
28
|
export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
25
29
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
26
30
|
export type { CompileResult } from "./persistence/index.js";
|
|
31
|
+
export { appendMemory, readMemoryEntries, getPreference, buildMemorySummary, recordPreference, recordConsent } from "./persistence/memory-store.js";
|
|
32
|
+
export type { MemoryEntry, MemoryKind, MemorySummary } from "./persistence/memory-store.js";
|
|
33
|
+
export { appendTraceEntry, readTraceEntries, aggregateTraces, getHistoricalDirectSuccessRate } from "./persistence/trace-store.js";
|
|
34
|
+
export type { TraceEntry, TraceAggregation } from "./persistence/trace-store.js";
|
|
27
35
|
export interface MartinAdapterRequest {
|
|
28
36
|
loopId: string;
|
|
29
37
|
attemptId: string;
|