mirror of
https://github.com/SHOGGOTH-SECTOR/sica-fondt.git
synced 2026-08-01 08:30:20 +00:00
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):
- brain/ LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
dispatch, A51 channels, and the OSINT cluster
- knowledge/ LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
MITRE ATT&CK data
- reference/ defensive threat-reference (C3, shhbruh doc) + AdaYaml parser
License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.
Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.
https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
303 lines
11 KiB
TypeScript
303 lines
11 KiB
TypeScript
/**
|
|
* Copyright 2026 Reto Meier
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { DEFAULT_GEMINI_FLASH_MODEL } from "../config/models.js";
|
|
import { GeminiClient } from "../services/geminiClient.js";
|
|
import { getAssetString, replaceRuntimePlaceholders } from "../services/promptManager.js";
|
|
import { TranscriptManager } from "../services/transcriptManager.js";
|
|
import { removeBacktickFences, repairTruncatedJsonArray, replaceContentBetweenMarkers } from "./markdownUtils.js";
|
|
import { InfrastructureContext, MultiAgentToolContext } from "../momoa_core/types.js";
|
|
import { parseToolRequest } from "../tools/multiAgentToolParser.js";
|
|
import { executeTool, getTool } from "../tools/multiAgentToolRegistry.js";
|
|
|
|
/**
|
|
* Defines the output structure for a file's relevance to a specific task.
|
|
*/
|
|
export interface TaskRelevantFile {
|
|
/** The path of the file. */
|
|
filename: string;
|
|
/** A brief explanation of *why* this file is relevant to the *specific* task. */
|
|
description: string;
|
|
}
|
|
|
|
/**
|
|
* Trims all content from the response after the first valid tool call.
|
|
* Handles nested braces and strings within the tool parameters to ensure
|
|
* the cut is made exactly at the end of the JSON object.
|
|
* * @param response The raw string response from the LLM.
|
|
* @returns The response string trimmed immediately after the tool call's closing brace.
|
|
*/
|
|
function trimResponseAfterToolCall(response: string): string {
|
|
// Find the start of the tool call: @TOOLNAME{
|
|
const toolStartRegex = /(@[a-zA-Z0-9_]+)(\{)/;
|
|
const match = response.match(toolStartRegex);
|
|
|
|
// If no tool call pattern is found, return the original response
|
|
if (!match || match.index === undefined) {
|
|
return response;
|
|
}
|
|
|
|
const startIndex = match.index + match[1].length;
|
|
|
|
let braceDepth = 0;
|
|
let inString = false;
|
|
let isEscaped = false;
|
|
|
|
// Iterate through the string starting from the opening brace '{'
|
|
for (let i = startIndex; i < response.length; i++) {
|
|
const char = response[i];
|
|
|
|
// Handle escaped characters (e.g., \" inside a string)
|
|
if (isEscaped) {
|
|
isEscaped = false;
|
|
continue;
|
|
}
|
|
if (char === '\\') {
|
|
isEscaped = true;
|
|
continue;
|
|
}
|
|
|
|
// Toggle string state when encountering unescaped quotes
|
|
if (char === '"') {
|
|
inString = !inString;
|
|
continue;
|
|
}
|
|
|
|
// Only process braces if we are NOT inside a string
|
|
if (!inString) {
|
|
if (char === '{') {
|
|
braceDepth++;
|
|
} else if (char === '}') {
|
|
braceDepth--;
|
|
|
|
// If depth returns to 0, we found the closing brace of the tool parameters
|
|
if (braceDepth === 0) {
|
|
// Return the substring up to and including the closing brace
|
|
return response.substring(0, i + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the loop finishes without finding a balanced closing brace (malformed JSON),
|
|
// return the original response to allow downstream parsers to handle the error.
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Analyzes a user's task to find relevant files and generate task-specific
|
|
* descriptions for them using a dedicated ReAct loop.
|
|
*
|
|
* @param taskDescription The user's task (e.g., "Implement a new authentication endpoint").
|
|
* @param fileMap A Map of all file paths to their string contents.
|
|
* @param binaryFileMap A Map of all binary file paths.
|
|
* @param multiAgentGeminiClient The Gemini client for making LLM calls.
|
|
* @returns A promise that resolves to an array of TaskRelevantFile objects.
|
|
*/
|
|
export async function analyzeRelevantFilesForTask(
|
|
taskDescription: string,
|
|
assumptions: string,
|
|
fileMap: Map<string, string>,
|
|
binaryFileMap: Map<string, string>,
|
|
infrastructureContext: InfrastructureContext,
|
|
multiAgentGeminiClient: GeminiClient,
|
|
sendMessage: (message: string) => void,
|
|
image?: string,
|
|
imageMimeType?: string
|
|
): Promise<TaskRelevantFile[]> {
|
|
|
|
const updateLog = async (message: string, updateProgress: boolean = false) => {
|
|
sendMessage(JSON.stringify({
|
|
status: 'WORK_LOG',
|
|
message: message,
|
|
}));
|
|
|
|
if (updateProgress) {
|
|
sendMessage(JSON.stringify({
|
|
status: 'PROGRESS_UPDATES',
|
|
completed_status_message: message,
|
|
}));
|
|
}
|
|
};
|
|
|
|
const transcript = new TranscriptManager({
|
|
context: infrastructureContext
|
|
});
|
|
const basePrompt = await getAssetString('task-file-analyzer');
|
|
const fileListSummary = [...fileMap.keys(), ...binaryFileMap.keys()].join('\n');
|
|
const initialPrompt = await replaceRuntimePlaceholders(basePrompt, {
|
|
TaskDescription: taskDescription,
|
|
Assumptions: assumptions,
|
|
FileSummary: fileListSummary.length > 0 ? fileListSummary : "--No files available--",
|
|
});
|
|
|
|
if (image && imageMimeType)
|
|
transcript.addImage(initialPrompt, image, imageMimeType);
|
|
else
|
|
transcript.addEntry('user', initialPrompt);
|
|
await updateLog("Analyzing files for relevancy to the task.", true);
|
|
|
|
const toolContext: MultiAgentToolContext = {
|
|
fileMap: fileMap,
|
|
binaryFileMap: binaryFileMap,
|
|
editedFilesSet: new Set<string>(), // Not relevant for read-only analysis
|
|
originalFilesSet: new Set<string>([...fileMap.keys(),...binaryFileMap.keys()]),
|
|
originalFileMap: new Map(fileMap), // Use a copy
|
|
originalBinaryFileMap: new Map(binaryFileMap),
|
|
sendMessage: sendMessage,
|
|
experts: [],
|
|
overseer: undefined,
|
|
transcriptsToUpdate: [transcript],
|
|
transcriptForContext: transcript,
|
|
multiAgentGeminiClient: multiAgentGeminiClient,
|
|
saveFileResolver: null,
|
|
infrastructureContext: infrastructureContext,
|
|
sessionTitle: 'File Analysis Session',
|
|
initialPrompt: taskDescription,
|
|
julesBranchName: null,
|
|
saveFiles: false,
|
|
secrets: {} as any,
|
|
};
|
|
|
|
const toolPrefix = await getAssetString('tool-prefix');
|
|
const FINISH_TOOL_PREFIX = `${toolPrefix}TOOL_CALL:`; // Kept for the custom FINISH command
|
|
const FINISH_REGEX = new RegExp(`${FINISH_TOOL_PREFIX}FINISH[\\{\\[](.*)[\\}\\]]`, 'sm');
|
|
|
|
const fileContentPrefix = await getAssetString('file-content-prefix');
|
|
const fileContentSuffix = await getAssetString('file-content-suffix');
|
|
const urlContentPrefix = await getAssetString('url-content-prefix');
|
|
const urlContentSuffix = await getAssetString('url-content-suffix');
|
|
const logReplacementString = '---CONTENT INTENTIONALLY REMOVED---';
|
|
|
|
const maxTurns = 30;
|
|
let turns = 0;
|
|
let isDone = false;
|
|
|
|
await updateLog(`## File Analyzer\n`);
|
|
|
|
while (!isDone) {
|
|
turns++;
|
|
|
|
if (turns > maxTurns) {
|
|
transcript.addEntry('user', `You have run out of turns and MUST use the ${toolPrefix}TOOL_CALL:FINISH tool to return your well formatted and valid JSON array of relevant files and descriptions, even if the list isn't complete.`);
|
|
} else if (turns > maxTurns + 2) {
|
|
isDone = true;
|
|
continue;
|
|
}
|
|
|
|
const llmMessage = await multiAgentGeminiClient.sendTranscriptMessage(
|
|
transcript,
|
|
{ model: DEFAULT_GEMINI_FLASH_MODEL }
|
|
);
|
|
let rawResponse = llmMessage.text || '';
|
|
|
|
rawResponse = await transcript.cleanLLMResponse(rawResponse);
|
|
rawResponse = trimResponseAfterToolCall(rawResponse);
|
|
|
|
await updateLog(`${rawResponse}\n`);
|
|
|
|
const finishMatch = rawResponse.match(FINISH_REGEX);
|
|
if (finishMatch) {
|
|
try {
|
|
let jsonResult = removeBacktickFences(finishMatch[1].trim());
|
|
jsonResult = repairTruncatedJsonArray(jsonResult) || "";
|
|
|
|
let relevantFiles = JSON.parse(jsonResult) as TaskRelevantFile[];
|
|
|
|
// Ensure all returned files actually exist in the project fileMap
|
|
const originalCount = relevantFiles.length;
|
|
relevantFiles = relevantFiles.filter(file => fileMap.has(file.filename) || binaryFileMap.has(file.filename));
|
|
|
|
if (relevantFiles.length < originalCount) {
|
|
await updateLog(`(Filtered out ${originalCount - relevantFiles.length} non-existent files from analysis results)`);
|
|
}
|
|
|
|
await updateLog(`#### Potentially relevant files identified`, true);
|
|
|
|
const formattedOutput = relevantFiles
|
|
.map(item => `\`${item.filename}\`\n\n${item.description}`)
|
|
.join('\n\n');
|
|
|
|
await updateLog(`${formattedOutput}`, true);
|
|
|
|
return relevantFiles; // Success!
|
|
} catch (e: any) {
|
|
const errorMsg = `TOOL_RESPONSE: Error parsing final JSON. ${e.message}. Please provide the full, correct JSON array again.`;
|
|
transcript.addEntry('user', errorMsg);
|
|
await updateLog(`File Analysis Error: ${errorMsg}`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const toolRequest = await parseToolRequest(rawResponse, toolPrefix, toolContext);
|
|
if (typeof toolRequest === 'string') {
|
|
// Tool parsing error
|
|
const errorMsg = `Tool Parsing Error: ${toolRequest}`;
|
|
transcript.addEntry('user', errorMsg);
|
|
await updateLog(errorMsg);
|
|
continue;
|
|
}
|
|
|
|
if (toolRequest?.toolName) {
|
|
// A standard tool was found
|
|
const tool = getTool(toolRequest.toolName);
|
|
await updateLog(`'${tool?.displayName || toolRequest.toolName}' Invoked`);
|
|
|
|
try {
|
|
// Execute the tool using the standard function
|
|
const toolResult = await executeTool(toolRequest.toolName, toolRequest.params, toolContext);
|
|
|
|
// Add to transcript with replacement string (for hiding content)
|
|
transcript.addEntry('user', toolResult.result, {
|
|
documentId: toolResult.transcriptReplacementID,
|
|
replacementIfSuperseded: toolResult.transcriptReplacementString
|
|
});
|
|
|
|
// Log the result, but hide file content
|
|
let toolResponseLogString = toolResult.result;
|
|
toolResponseLogString = replaceContentBetweenMarkers(
|
|
toolResponseLogString,
|
|
fileContentPrefix,
|
|
fileContentSuffix,
|
|
logReplacementString
|
|
);
|
|
|
|
// Log the result, but hide URL content
|
|
toolResponseLogString = replaceContentBetweenMarkers(
|
|
toolResponseLogString,
|
|
urlContentPrefix,
|
|
urlContentSuffix,
|
|
logReplacementString
|
|
);
|
|
|
|
await updateLog(`Tool Result:\n${toolResponseLogString}`);
|
|
|
|
} catch (error: any) {
|
|
const errorMessage = `Tool execution failed: ${error.message}`;
|
|
transcript.addEntry('user', errorMessage);
|
|
await updateLog(`Tool Error:\n${errorMessage}`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If no FINISH command and no standard tool was parsed, just continue.
|
|
continue;
|
|
}
|
|
|
|
console.warn("Task File Analyzer reached max turns without finishing.");
|
|
await updateLog('Reached max turns without finishing.');
|
|
return [];
|
|
} |