fix(core): harden env loading, OpenAI compatibility, and runtime recovery
This commit is contained in:
@@ -260,6 +260,13 @@ export class NativeAgent {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toolCalls.length === 0) {
|
||||
const recovered = this.extractMalformedShellToolCall(response.content);
|
||||
if (recovered && toolRegistry.getByApiName(recovered.toolCall.name)) {
|
||||
toolCalls = [recovered.toolCall];
|
||||
assistantTextContent = recovered.remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const wantsToolUse = toolCalls.length > 0;
|
||||
if (!wantsToolUse) {
|
||||
@@ -705,6 +712,94 @@ export class NativeAgent {
|
||||
};
|
||||
}
|
||||
|
||||
private extractMalformedShellToolCall(content: string): { toolCall: ModelToolCall; remainingText: string } | null {
|
||||
if (!content || !/"type"\s*:\s*"tool_use"/.test(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameMatch = content.match(/"name"\s*:\s*"([^"]+)"/);
|
||||
if (!nameMatch?.[1]) {
|
||||
return null;
|
||||
}
|
||||
const name = nameMatch[1];
|
||||
const normalized = name.replace(/_/g, '.').toLowerCase();
|
||||
if (normalized !== 'shell.exec') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recover malformed shell command payloads where inner quotes are not escaped.
|
||||
const commandMatch = content.match(/"command"\s*:\s*"([\s\S]*)"\s*}\s*}/);
|
||||
if (!commandMatch?.[1]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const command = this.sanitizeRecoveredShellCommand(commandMatch[1]);
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const idMatch = content.match(/"id"\s*:\s*"([^"]+)"/);
|
||||
const id = idMatch?.[1]?.trim().length ? idMatch[1] : 'text_tool_call_recovered_shell';
|
||||
|
||||
return {
|
||||
toolCall: {
|
||||
id,
|
||||
name,
|
||||
args: { command },
|
||||
},
|
||||
remainingText: this.stripFirstToolUseObject(content),
|
||||
};
|
||||
}
|
||||
|
||||
private sanitizeRecoveredShellCommand(raw: string): string {
|
||||
let command = raw.trim();
|
||||
if (command.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Common malformed pattern: opening quote duplicated into value.
|
||||
if ((command.startsWith('"') && !command.endsWith('"')) || (command.startsWith('\'') && !command.endsWith('\''))) {
|
||||
command = command.slice(1).trimStart();
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private stripFirstToolUseObject(content: string): string {
|
||||
const typeMatch = /"type"\s*:\s*"tool_use"/.exec(content);
|
||||
if (!typeMatch || typeMatch.index < 0) {
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
const objectStart = content.lastIndexOf('{', typeMatch.index);
|
||||
if (objectStart < 0) {
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
const objectEnd = this.findObjectEndByBraceDepth(content, objectStart);
|
||||
if (objectEnd < 0) {
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
return `${content.slice(0, objectStart)}${content.slice(objectEnd + 1)}`.trim();
|
||||
}
|
||||
|
||||
private findObjectEndByBraceDepth(content: string, start: number): number {
|
||||
let depth = 0;
|
||||
for (let i = start; i < content.length; i++) {
|
||||
const ch = content[i];
|
||||
if (ch === '{') {
|
||||
depth++;
|
||||
} else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private findJsonObjectEnd(content: string, start: number): number {
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
|
||||
Reference in New Issue
Block a user