@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
323 lines • 10.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyDiff = applyDiff;
/**
* Applies a headerless V4A diff to the provided file content.
* - mode "default": patch an existing file using V4A sections ("@@" + +/-/space lines).
* - mode "create": create-file syntax that requires every line to start with "+".
*
* The function preserves trailing newlines from the original file and throws when
* the diff cannot be applied cleanly.
*/
function applyDiff(input, diff, mode = 'default') {
const diffLines = normalizeDiffLines(diff);
if (mode === 'create') {
return parseCreateDiff(diffLines);
}
const lineEnding = updateLineEnding(input);
const normalizedInput = lineEnding === '\r\n' ? input.replace(/\r\n/g, '\n') : input;
const { chunks } = parseUpdateDiff(diffLines, normalizedInput);
return applyChunks(normalizedInput, chunks, lineEnding);
}
function updateLineEnding(input) {
let hasNewline = false;
for (let index = 0; index < input.length; index += 1) {
if (input[index] !== '\n')
continue;
hasNewline = true;
if (index === 0 || input[index - 1] !== '\r') {
return '\n';
}
}
return hasNewline ? '\r\n' : '\n';
}
const END_PATCH = '*** End Patch';
const END_FILE = '*** End of File';
const END_SECTION_MARKERS = [
END_PATCH,
'*** Update File:',
'*** Delete File:',
'*** Add File:',
END_FILE,
];
const SECTION_TERMINATORS = [
END_PATCH,
'*** Update File:',
'*** Delete File:',
'*** Add File:',
];
function normalizeDiffLines(diff) {
return diff
.split(/\r?\n/)
.map((line) => line.replace(/\r$/, ''))
.filter((line, idx, arr) => !(idx === arr.length - 1 && line === ''));
}
function isDone(state, prefixes) {
if (state.index >= state.lines.length)
return true;
if (prefixes.some((p) => state.lines[state.index]?.startsWith(p)))
return true;
return false;
}
function readStr(state, prefix) {
const current = state.lines[state.index];
if (typeof current === 'string' && current.startsWith(prefix)) {
state.index += 1;
return current.slice(prefix.length);
}
return '';
}
function parseCreateDiff(lines) {
const parser = {
lines: [...lines, END_PATCH],
index: 0,
fuzz: 0,
};
const output = [];
while (!isDone(parser, SECTION_TERMINATORS)) {
const line = parser.lines[parser.index];
parser.index += 1;
if (!line.startsWith('+')) {
throw new Error(`Invalid Add File Line: ${line}`);
}
output.push(line.slice(1));
}
return output.join('\n');
}
function parseUpdateDiff(lines, input) {
const parser = {
lines: [...lines, END_PATCH],
index: 0,
fuzz: 0,
};
const inputLines = input.split('\n');
const chunks = [];
let cursor = 0;
while (!isDone(parser, END_SECTION_MARKERS)) {
const { anchors, anchorCount } = readAnchors(parser);
if (!(anchorCount > 0 || cursor === 0)) {
throw new Error(`Invalid Line:\n${parser.lines[parser.index]}`);
}
const requireAnchorMatch = anchorCount > 1;
for (const [index, anchor] of anchors.entries()) {
cursor = advanceCursorToAnchor(anchor, inputLines, cursor, parser, requireAnchorMatch, index > 0);
}
const { nextContext, sectionChunks, endIndex, eof } = readSection(parser.lines, parser.index);
const nextContextText = nextContext.join('\n');
const { newIndex, fuzz } = findContext(inputLines, nextContext, cursor, eof);
if (newIndex === -1) {
if (eof) {
throw new Error(`Invalid EOF Context ${cursor}:\n${nextContextText}`);
}
throw new Error(`Invalid Context ${cursor}:\n${nextContextText}`);
}
parser.fuzz += fuzz;
for (const ch of sectionChunks) {
chunks.push({ ...ch, origIndex: ch.origIndex + newIndex });
}
cursor = newIndex + nextContext.length;
parser.index = endIndex;
}
return { chunks, fuzz: parser.fuzz };
}
function readAnchors(parser) {
const anchors = [];
let anchorCount = 0;
while (true) {
const startIndex = parser.index;
const anchor = readStr(parser, '@@ ');
let consumed = parser.index !== startIndex;
if (!consumed && parser.lines[parser.index] === '@@') {
parser.index += 1;
consumed = true;
}
if (!consumed)
break;
anchorCount += 1;
if (anchor.trim())
anchors.push(anchor);
}
return { anchors, anchorCount };
}
function advanceCursorToAnchor(anchor, inputLines, cursor, parser, requireMatch = false, forceForwardSearch = false) {
let found = false;
const hasExactMatchBeforeCursor = !forceForwardSearch &&
inputLines.slice(0, cursor).some((line) => line === anchor);
if (hasExactMatchBeforeCursor) {
found = true;
}
else {
for (let i = cursor; i < inputLines.length; i += 1) {
if (inputLines[i] === anchor) {
cursor = i + 1;
found = true;
break;
}
}
}
if (!found) {
const hasTrimmedMatchBeforeCursor = !forceForwardSearch &&
inputLines.slice(0, cursor).some((line) => line.trim() === anchor.trim());
if (hasTrimmedMatchBeforeCursor) {
found = true;
}
else {
for (let i = cursor; i < inputLines.length; i += 1) {
if (inputLines[i].trim() === anchor.trim()) {
cursor = i + 1;
parser.fuzz += 1;
found = true;
break;
}
}
}
}
if (requireMatch && !found) {
throw new Error(`Invalid Anchor ${cursor}:\n${anchor}`);
}
return cursor;
}
function readSection(lines, startIndex) {
const context = [];
let delLines = [];
let insLines = [];
const sectionChunks = [];
let mode = 'keep';
let index = startIndex;
const origIndex = index;
while (index < lines.length) {
const raw = lines[index];
if (raw.startsWith('@@') ||
raw.startsWith(END_PATCH) ||
raw.startsWith('*** Update File:') ||
raw.startsWith('*** Delete File:') ||
raw.startsWith('*** Add File:') ||
raw.startsWith(END_FILE)) {
break;
}
if (raw === '***')
break;
if (raw.startsWith('***')) {
throw new Error(`Invalid Line: ${raw}`);
}
index += 1;
const lastMode = mode;
let line = raw;
if (line === '')
line = ' ';
if (line[0] === '+') {
mode = 'add';
}
else if (line[0] === '-') {
mode = 'delete';
}
else if (line[0] === ' ') {
mode = 'keep';
}
else {
throw new Error(`Invalid Line: ${line}`);
}
line = line.slice(1);
const switchingToContext = mode === 'keep' && lastMode !== mode;
if (switchingToContext && (insLines.length || delLines.length)) {
sectionChunks.push({
origIndex: context.length - delLines.length,
delLines,
insLines,
});
delLines = [];
insLines = [];
}
if (mode === 'delete') {
delLines.push(line);
context.push(line);
}
else if (mode === 'add') {
insLines.push(line);
}
else {
context.push(line);
}
}
if (insLines.length || delLines.length) {
sectionChunks.push({
origIndex: context.length - delLines.length,
delLines,
insLines,
});
delLines = [];
insLines = [];
}
if (index < lines.length && lines[index] === END_FILE) {
index += 1;
return { nextContext: context, sectionChunks, endIndex: index, eof: true };
}
if (index === origIndex) {
throw new Error(`Nothing in this section - index=${index} ${lines[index]}`);
}
return { nextContext: context, sectionChunks, endIndex: index, eof: false };
}
function findContext(lines, context, start, eof) {
if (eof) {
const searchLines = lines.length > 1 && lines[lines.length - 1] === ''
? lines.slice(0, -1)
: lines;
const endStart = Math.max(0, searchLines.length - context.length);
const endMatch = findContextCore(searchLines, context, endStart);
if (endMatch.newIndex !== -1)
return endMatch;
const fallback = findContextCore(searchLines, context, Math.min(start, searchLines.length));
return { newIndex: fallback.newIndex, fuzz: fallback.fuzz + 10000 };
}
return findContextCore(lines, context, start);
}
function findContextCore(lines, context, start) {
if (!context.length) {
return { newIndex: start, fuzz: 0 };
}
for (let i = start; i < lines.length; i += 1) {
if (equalsSlice(lines, context, i, (s) => s))
return { newIndex: i, fuzz: 0 };
}
for (let i = start; i < lines.length; i += 1) {
if (equalsSlice(lines, context, i, (s) => s.trimEnd()))
return { newIndex: i, fuzz: 1 };
}
for (let i = start; i < lines.length; i += 1) {
if (equalsSlice(lines, context, i, (s) => s.trim()))
return { newIndex: i, fuzz: 100 };
}
return { newIndex: -1, fuzz: 0 };
}
function equalsSlice(source, target, start, mapFn) {
if (start + target.length > source.length)
return false;
for (let i = 0; i < target.length; i += 1) {
if (mapFn(source[start + i]) !== mapFn(target[i]))
return false;
}
return true;
}
function applyChunks(input, chunks, lineEnding) {
const origLines = input.split('\n');
const destLines = [];
let origIndex = 0;
for (const chunk of chunks) {
if (chunk.origIndex > origLines.length) {
throw new Error(`applyDiff: chunk.origIndex ${chunk.origIndex} > input length ${origLines.length}`);
}
if (origIndex > chunk.origIndex) {
throw new Error(`applyDiff: overlapping chunk at ${chunk.origIndex} (cursor ${origIndex})`);
}
destLines.push(...origLines.slice(origIndex, chunk.origIndex));
origIndex = chunk.origIndex;
if (chunk.insLines.length) {
destLines.push(...chunk.insLines);
}
origIndex += chunk.delLines.length;
}
destLines.push(...origLines.slice(origIndex));
const result = destLines.join(lineEnding);
return result;
}
//# sourceMappingURL=applyDiff.js.map