Agent loop defensive (#3554)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -2332,17 +2332,16 @@
|
||||
}
|
||||
},
|
||||
"ResourceContents": {
|
||||
"oneOf": [
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"uri",
|
||||
"text"
|
||||
"text",
|
||||
"uri"
|
||||
],
|
||||
"properties": {
|
||||
"mime_type": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
@@ -2355,16 +2354,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"uri",
|
||||
"blob"
|
||||
"blob",
|
||||
"uri"
|
||||
],
|
||||
"properties": {
|
||||
"blob": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
|
||||
@@ -1174,7 +1174,7 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
const config = window.electron.getConfig();
|
||||
const STRICT_ALLOWLIST = config.GOOSE_ALLOWLIST_WARNING === true ? false : true;
|
||||
const STRICT_ALLOWLIST = config.GOOSE_ALLOWLIST_WARNING !== true;
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Setting up extension handler');
|
||||
|
||||
@@ -463,12 +463,12 @@ export type RedactedThinkingContent = {
|
||||
};
|
||||
|
||||
export type ResourceContents = {
|
||||
mime_type?: string | null;
|
||||
mime_type?: string;
|
||||
text: string;
|
||||
uri: string;
|
||||
} | {
|
||||
blob: string;
|
||||
mime_type?: string | null;
|
||||
mime_type?: string;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -537,7 +537,7 @@ function BaseChatContent({
|
||||
recipeDetails={{
|
||||
title: recipeConfig?.title,
|
||||
description: recipeConfig?.description,
|
||||
instructions: recipeConfig?.instructions,
|
||||
instructions: recipeConfig?.instructions || undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -126,10 +126,10 @@ const notificationToProgress = (notification: NotificationEvent): Progress =>
|
||||
const getExtensionTooltip = (toolCallName: string): string | null => {
|
||||
const lastIndex = toolCallName.lastIndexOf('__');
|
||||
if (lastIndex === -1) return null;
|
||||
|
||||
|
||||
const extensionName = toolCallName.substring(0, lastIndex);
|
||||
if (!extensionName) return null;
|
||||
|
||||
|
||||
return `${extensionName} extension`;
|
||||
};
|
||||
|
||||
@@ -377,7 +377,7 @@ function ToolCallView({
|
||||
// This ensures any MCP tool works without explicit handling
|
||||
const toolDisplayName = snakeToTitleCase(toolName);
|
||||
const entries = Object.entries(args);
|
||||
|
||||
|
||||
if (entries.length === 0) {
|
||||
return `${toolDisplayName}`;
|
||||
}
|
||||
@@ -413,7 +413,7 @@ function ToolCallView({
|
||||
};
|
||||
|
||||
const toolLabel = (
|
||||
<span className={cn("ml-2", extensionTooltip && "cursor-pointer hover:opacity-80")}>
|
||||
<span className={cn('ml-2', extensionTooltip && 'cursor-pointer hover:opacity-80')}>
|
||||
{getToolLabelContent()}
|
||||
</span>
|
||||
);
|
||||
|
||||
+30
-61
@@ -1,20 +1,20 @@
|
||||
import type { OpenDialogReturnValue } from 'electron';
|
||||
import {
|
||||
app,
|
||||
session,
|
||||
App,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
Event,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Notification,
|
||||
powerSaveBlocker,
|
||||
Tray,
|
||||
App,
|
||||
globalShortcut,
|
||||
session,
|
||||
shell,
|
||||
Event,
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import type { OpenDialogReturnValue } from 'electron';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import fs from 'node:fs/promises';
|
||||
import fsSync from 'node:fs';
|
||||
@@ -33,20 +33,20 @@ import {
|
||||
EnvToggles,
|
||||
loadSettings,
|
||||
saveSettings,
|
||||
SchedulingEngine,
|
||||
updateEnvironmentVariables,
|
||||
updateSchedulingEngineEnvironment,
|
||||
SchedulingEngine,
|
||||
} from './utils/settings';
|
||||
import * as crypto from 'crypto';
|
||||
// import electron from "electron";
|
||||
import * as yaml from 'yaml';
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
import {
|
||||
setupAutoUpdater,
|
||||
getUpdateAvailable,
|
||||
registerUpdateIpcHandlers,
|
||||
setTrayRef,
|
||||
setupAutoUpdater,
|
||||
updateTrayMenu,
|
||||
getUpdateAvailable,
|
||||
} from './utils/autoUpdater';
|
||||
import { UPDATES_ENABLED } from './updates';
|
||||
import { Recipe } from './recipe';
|
||||
@@ -589,7 +589,7 @@ const createChat = async (
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
|
||||
trafficLightPosition: process.platform === 'darwin' ? { x: 20, y: 16 } : undefined,
|
||||
vibrancy: process.platform === 'darwin' ? 'window' : undefined,
|
||||
frame: process.platform === 'darwin' ? false : true,
|
||||
frame: process.platform !== 'darwin',
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
@@ -1542,15 +1542,8 @@ ipcMain.handle('show-message-box', async (_event, options) => {
|
||||
return result;
|
||||
});
|
||||
|
||||
// Handle allowed extensions list fetching
|
||||
ipcMain.handle('get-allowed-extensions', async () => {
|
||||
try {
|
||||
const allowList = await getAllowList();
|
||||
return allowList;
|
||||
} catch (error) {
|
||||
console.error('Error fetching allowed extensions:', error);
|
||||
throw error;
|
||||
}
|
||||
return await getAllowList();
|
||||
});
|
||||
|
||||
const createNewWindow = async (app: App, dir?: string | null) => {
|
||||
@@ -2068,57 +2061,33 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches the allowed extensions list from the remote YAML file if GOOSE_ALLOWLIST is set.
|
||||
* If the ALLOWLIST is not set, any are allowed. If one is set, it will warn if the deeplink
|
||||
* doesn't match a command from the list.
|
||||
* If it fails to load, then it will return an empty list.
|
||||
* If the format is incorrect, it will return an empty list.
|
||||
* Format of yaml is:
|
||||
*
|
||||
```yaml:
|
||||
extensions:
|
||||
- id: slack
|
||||
command: uvx mcp_slack
|
||||
- id: knowledge_graph_memory
|
||||
command: npx -y @modelcontextprotocol/server-memory
|
||||
```
|
||||
*
|
||||
* @returns A promise that resolves to an array of extension commands that are allowed.
|
||||
*/
|
||||
async function getAllowList(): Promise<string[]> {
|
||||
if (!process.env.GOOSE_ALLOWLIST) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the YAML file
|
||||
const response = await fetch(process.env.GOOSE_ALLOWLIST);
|
||||
const response = await fetch(process.env.GOOSE_ALLOWLIST);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch allowed extensions: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch allowed extensions: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the YAML content
|
||||
const yamlContent = await response.text();
|
||||
const parsedYaml = yaml.parse(yamlContent);
|
||||
// Parse the YAML content
|
||||
const yamlContent = await response.text();
|
||||
const parsedYaml = yaml.parse(yamlContent);
|
||||
|
||||
// Extract the commands from the extensions array
|
||||
if (parsedYaml && parsedYaml.extensions && Array.isArray(parsedYaml.extensions)) {
|
||||
const commands = parsedYaml.extensions.map(
|
||||
(ext: { id: string; command: string }) => ext.command
|
||||
);
|
||||
console.log(`Fetched ${commands.length} allowed extension commands`);
|
||||
return commands;
|
||||
} else {
|
||||
console.error('Invalid YAML structure:', parsedYaml);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in getAllowList:', error);
|
||||
throw error;
|
||||
// Extract the commands from the extensions array
|
||||
if (parsedYaml && parsedYaml.extensions && Array.isArray(parsedYaml.extensions)) {
|
||||
const commands = parsedYaml.extensions.map(
|
||||
(ext: { id: string; command: string }) => ext.command
|
||||
);
|
||||
console.log(`Fetched ${commands.length} allowed extension commands`);
|
||||
return commands;
|
||||
} else {
|
||||
console.error('Invalid YAML structure:', parsedYaml);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import { getApiUrl, getSecretKey } from '../config';
|
||||
import { safeJsonParse } from './jsonUtils';
|
||||
|
||||
const getQuestionClassifierPrompt = (messageContent: string): string => `
|
||||
You are a simple classifier that takes content and decides if it is asking for input
|
||||
from a person before continuing if there is more to do, or not. These are questions
|
||||
on if a course of action should proceeed or not, or approval is needed. If it is CLEARLY a
|
||||
question asking if it ok to proceed or make a choice or some input is required to proceed, then, and ONLY THEN, return QUESTION, otherwise READY if not 97% sure.
|
||||
|
||||
### Examples message content that is classified as READY:
|
||||
anything else I can do?
|
||||
Could you please run the application and verify that the headlines are now visible in dark mode? You can use npm start.
|
||||
Would you like me to make any adjustments to the formatting of these multiline strings?
|
||||
Would you like me to show you how to ... (do something)?
|
||||
Listing window titles... Is there anything specific you'd like help with using these tools?
|
||||
Would you like me to demonstrate any specific capability or help you with a particular task?
|
||||
Would you like me to run any tests?
|
||||
Would you like me to make any adjustments or would you like to test?
|
||||
Would you like me to dive deeper into any aspect?
|
||||
Would you like me to make any other adjustments to this implementation?
|
||||
Would you like any further information or assistance?
|
||||
Would you like to me to make any changes?
|
||||
Would you like me to make any adjustments to this implementation?
|
||||
Would you like me to show you how to…
|
||||
What would you like to do next?
|
||||
|
||||
### Examples that are QUESTIONS:
|
||||
Should I go ahead and make the changes?
|
||||
Should I Go ahead with this plan?
|
||||
Should I focus on X or Y?
|
||||
Provide me with the name of the package and version you would like to install.
|
||||
|
||||
|
||||
### Message Content:
|
||||
${messageContent}
|
||||
|
||||
You must provide a response strictly limited to one of the following two words:
|
||||
QUESTION, READY. No other words, phrases, or explanations are allowed.
|
||||
|
||||
Response:`;
|
||||
|
||||
const getOptionsClassifierPrompt = (messageContent: string): string => `
|
||||
You are a simple classifier that takes content and decides if it a list of options
|
||||
or plans to choose from, or not a list of options to choose from. It is IMPORTANT
|
||||
that you really know this is a choice, just not numbered steps. If it is a list
|
||||
of options and you are 95% sure, return OPTIONS, otherwise return NO.
|
||||
|
||||
### Example (text -> response):
|
||||
Would you like me to proceed with creating this file? Please let me know if you want any changes before I write it. -> NO
|
||||
Here are some options for you to choose from: -> OPTIONS
|
||||
which one do you want to choose? -> OPTIONS
|
||||
Would you like me to dive deeper into any aspects of these components? -> NO
|
||||
Should I focus on X or Y? -> OPTIONS
|
||||
|
||||
### Message Content:
|
||||
${messageContent}
|
||||
|
||||
You must provide a response strictly limited to one of the following two words:
|
||||
OPTIONS, NO. No other words, phrases, or explanations are allowed.
|
||||
|
||||
Response:`;
|
||||
|
||||
const getOptionsFormatterPrompt = (messageContent: string): string => `
|
||||
If the content is list of distinct options or plans of action to choose from, and
|
||||
not just a list of things, but clearly a list of things to choose one from, taking
|
||||
into account the Message Content alone, try to format it in a json array, like this
|
||||
JSON array of objects of the form optionTitle:string, optionDescription:string (markdown).
|
||||
|
||||
If is not a list of options or plans to choose from, then return empty list.
|
||||
|
||||
### Message Content:
|
||||
${messageContent}
|
||||
|
||||
You must provide a response strictly as json in the format descriribed. No other
|
||||
words, phrases, or explanations are allowed.
|
||||
|
||||
Response:`;
|
||||
|
||||
const getFormPrompt = (messageContent: string): string => `
|
||||
When you see a request for several pieces of information, then provide a well formed JSON object like will be shown below.
|
||||
The response will have:
|
||||
* a title, description,
|
||||
* a list of fields, each field will have a label, type, name, placeholder, and required (boolean).
|
||||
(type is either text or textarea only).
|
||||
If it is not requesting clearly several pieces of information, just return an empty object.
|
||||
If the task could be confirmed without more information, return an empty object.
|
||||
|
||||
### Example Message:
|
||||
I'll help you scaffold out a Python package. To create a well-structured Python package, I'll need to know a few key pieces of information:
|
||||
|
||||
Package name - What would you like to call your package? (This should be a valid Python package name - lowercase, no spaces, typically using underscores for separators if needed)
|
||||
|
||||
Brief description - What is the main purpose of the package? This helps in setting up appropriate documentation and structure.
|
||||
|
||||
Initial modules - Do you have specific functionality in mind that should be split into different modules?
|
||||
|
||||
Python version - Which Python version(s) do you want to support?
|
||||
|
||||
Dependencies - Are there any known external packages you'll need?
|
||||
|
||||
### Example JSON Response:
|
||||
{
|
||||
"title": "Python Package Scaffolding Form",
|
||||
"description": "Provide the details below to scaffold a well-structured Python package.",
|
||||
"fields": [
|
||||
{
|
||||
"label": "Package Name",
|
||||
"type": "text",
|
||||
"name": "package_name",
|
||||
"placeholder": "Enter the package name (lowercase, no spaces, use underscores if needed)",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "Brief Description",
|
||||
"type": "textarea",
|
||||
"name": "brief_description",
|
||||
"placeholder": "Enter a brief description of the package's purpose",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "Initial Modules",
|
||||
"type": "textarea",
|
||||
"name": "initial_modules",
|
||||
"placeholder": "List the specific functionalities or modules (optional)",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"label": "Python Version(s)",
|
||||
"type": "text",
|
||||
"name": "python_versions",
|
||||
"placeholder": "Enter the Python version(s) to support (e.g., 3.8, 3.9, 3.10)",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "Dependencies",
|
||||
"type": "textarea",
|
||||
"name": "dependencies",
|
||||
"placeholder": "List any known external packages you'll need (optional)",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
### Message Content:
|
||||
${messageContent}
|
||||
|
||||
You must provide a response strictly as json in the format described. No other
|
||||
words, phrases, or explanations are allowed.
|
||||
|
||||
Response:`;
|
||||
|
||||
/**
|
||||
* Core function to ask the AI a single question and get a response
|
||||
* @param prompt The prompt to send to the AI
|
||||
* @returns Promise<string> The AI's response
|
||||
*/
|
||||
export async function ask(prompt: string): Promise<string> {
|
||||
const response = await fetch(getApiUrl('/ask'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({ prompt }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get response');
|
||||
}
|
||||
|
||||
const data = await safeJsonParse<{ response: string }>(response, 'Failed to get AI response');
|
||||
return data.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to ask the LLM multiple questions to clarify without wider context.
|
||||
* @param messageContent The content to analyze
|
||||
* @returns Promise<string[]> Array of responses from the AI for each classifier
|
||||
*/
|
||||
export async function askAi(messageContent: string): Promise<string[]> {
|
||||
// First, check the question classifier
|
||||
const questionClassification = await ask(getQuestionClassifierPrompt(messageContent));
|
||||
|
||||
// If READY, return early with empty responses for options
|
||||
if (questionClassification === 'READY') {
|
||||
return [questionClassification, 'NO', '[]', '{}'];
|
||||
}
|
||||
|
||||
// Otherwise, proceed with all classifiers in parallel
|
||||
const prompts = [
|
||||
Promise.resolve(questionClassification), // Reuse the result we already have
|
||||
ask(getOptionsClassifierPrompt(messageContent)),
|
||||
ask(getOptionsFormatterPrompt(messageContent)),
|
||||
ask(getFormPrompt(messageContent)),
|
||||
];
|
||||
|
||||
return Promise.all(prompts);
|
||||
}
|
||||
Reference in New Issue
Block a user