mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3824f8a6cb | |||
| 5ed5db00a4 | |||
| d7ecbb072f |
@@ -1,4 +1,4 @@
|
||||
name: 'Automated Documentation Audit'
|
||||
name: 'Weekly Docs Audit'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -26,7 +26,6 @@ jobs:
|
||||
node-version: '20'
|
||||
|
||||
- name: 'Run Docs Audit with Gemini'
|
||||
id: 'run_gemini'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
@@ -34,28 +33,17 @@ jobs:
|
||||
Activate the 'docs-writer' skill.
|
||||
|
||||
**Task:** Execute the docs audit procedure, as defined in your 'docs-auditing.md' reference.
|
||||
Provide a detailed summary of the changes you make.
|
||||
|
||||
- name: 'Get current date'
|
||||
id: 'date'
|
||||
run: |
|
||||
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Create Pull Request with Audit Results'
|
||||
uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c'
|
||||
with:
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
commit-message: 'docs: weekly audit results for ${{ github.run_id }}'
|
||||
title: 'Docs audit: ${{ steps.date.outputs.date }}'
|
||||
title: 'Docs Audit for Week of ${{ github.event.schedule }}'
|
||||
body: |
|
||||
This PR contains the auto-generated documentation audit for the week. It includes a new `audit-results-*.md` file with findings and any direct fixes applied by the agent.
|
||||
|
||||
### Audit Summary:
|
||||
${{ steps.run_gemini.outputs.summary || 'No summary provided.' }}
|
||||
|
||||
Please review the suggestions and merge.
|
||||
|
||||
Related to #25152
|
||||
branch: 'docs-audit-${{ github.run_id }}'
|
||||
base: 'main'
|
||||
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
|
||||
|
||||
@@ -505,19 +505,15 @@ events. For more information, see the [telemetry documentation](./telemetry.md).
|
||||
## Authentication
|
||||
|
||||
You can enforce a specific authentication method for all users by setting the
|
||||
`security.auth.enforcedType` in the system-level `settings.json` file. This
|
||||
prevents users from choosing a different authentication method. See the
|
||||
`enforcedAuthType` in the system-level `settings.json` file. This prevents users
|
||||
from choosing a different authentication method. See the
|
||||
[Authentication docs](../get-started/authentication.md) for more details.
|
||||
|
||||
**Example:** Enforce the use of Google login for all users.
|
||||
|
||||
```json
|
||||
{
|
||||
"security": {
|
||||
"auth": {
|
||||
"enforcedType": "oauth-personal"
|
||||
}
|
||||
}
|
||||
"enforcedAuthType": "oauth-personal"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+1
-3
@@ -309,9 +309,7 @@ $env:SANDBOX_SET_UID_GID="false" # Disable UID/GID mapping
|
||||
|
||||
**Missing commands**
|
||||
|
||||
- Add to a custom Dockerfile. Automatic `BUILD_SANDBOX` builds are only
|
||||
available when running Gemini CLI from source; npm installs need a prebuilt
|
||||
image instead.
|
||||
- Add to custom Dockerfile.
|
||||
- Install via `sandbox.bashrc`.
|
||||
|
||||
**Network issues**
|
||||
|
||||
@@ -2509,10 +2509,6 @@ sandbox image:
|
||||
BUILD_SANDBOX=1 gemini -s
|
||||
```
|
||||
|
||||
Building a custom sandbox with `BUILD_SANDBOX` is only supported when running
|
||||
Gemini CLI from source. If you installed the CLI with npm, build the Docker
|
||||
image separately and reference that image in your sandbox configuration.
|
||||
|
||||
## Usage statistics
|
||||
|
||||
To help us improve Gemini CLI, we collect anonymized usage statistics. This data
|
||||
|
||||
@@ -99,7 +99,7 @@ available combinations.
|
||||
|
||||
| Command | Action | Keys |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| `app.showErrorDetails` | Toggle the debug console for detailed error information. | `F12` |
|
||||
| `app.showErrorDetails` | Toggle detailed error information. | `F12` |
|
||||
| `app.showFullTodos` | Toggle the full TODO list. | `Ctrl+T` |
|
||||
| `app.showIdeContextDetail` | Show IDE context details. | `F4` |
|
||||
| `app.toggleMarkdown` | Toggle Markdown rendering. | `Alt+M` |
|
||||
@@ -248,83 +248,6 @@ a `key` combination.
|
||||
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
|
||||
view full content inline. Double-click again to collapse.
|
||||
|
||||
## Vi mode shortcuts
|
||||
|
||||
When vim mode is enabled with `/vim` or `general.vimMode: true`, Gemini CLI
|
||||
supports NORMAL and INSERT modes.
|
||||
|
||||
### Mode switching
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------- | --------- |
|
||||
| Enter NORMAL mode from INSERT mode | `Esc` |
|
||||
| Enter INSERT mode at the cursor | `i` |
|
||||
| Enter INSERT mode after the cursor | `a` |
|
||||
| Enter INSERT mode at the start of the line | `I` |
|
||||
| Enter INSERT mode at the end of the line | `A` |
|
||||
| Insert a new line below and switch to INSERT | `o` |
|
||||
| Insert a new line above and switch to INSERT | `O` |
|
||||
| Clear input in NORMAL mode | `Esc Esc` |
|
||||
|
||||
### Navigation in NORMAL mode
|
||||
|
||||
| Action | Keys |
|
||||
| --------------------------------- | --------------- |
|
||||
| Move left | `h` |
|
||||
| Move down | `j` |
|
||||
| Move up | `k` |
|
||||
| Move right | `l` |
|
||||
| Move to start of line | `0` |
|
||||
| Move to first non-whitespace char | `^` |
|
||||
| Move to end of line | `$` |
|
||||
| Move forward by word | `w` |
|
||||
| Move backward by word | `b` |
|
||||
| Move to end of word | `e` |
|
||||
| Move forward by WORD | `W` |
|
||||
| Move backward by WORD | `B` |
|
||||
| Move to end of WORD | `E` |
|
||||
| Go to first line | `gg` |
|
||||
| Go to last line | `G` |
|
||||
| Go to line N | `N G` or `N gg` |
|
||||
|
||||
Counts are supported for navigation commands. For example, `5j` moves down five
|
||||
lines and `3w` moves forward three words.
|
||||
|
||||
### Editing in NORMAL mode
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------ | ----- |
|
||||
| Delete character under cursor | `x` |
|
||||
| Delete to end of line | `D` |
|
||||
| Delete line | `dd` |
|
||||
| Change to end of line | `C` |
|
||||
| Change line | `cc` |
|
||||
| Delete forward word | `dw` |
|
||||
| Delete backward word | `db` |
|
||||
| Delete to end of word | `de` |
|
||||
| Delete forward WORD | `dW` |
|
||||
| Delete backward WORD | `dB` |
|
||||
| Delete to end of WORD | `dE` |
|
||||
| Change forward word | `cw` |
|
||||
| Change backward word | `cb` |
|
||||
| Change to end of word | `ce` |
|
||||
| Change forward WORD | `cW` |
|
||||
| Change backward WORD | `cB` |
|
||||
| Change to end of WORD | `cE` |
|
||||
| Delete to start of line | `d0` |
|
||||
| Delete to first non-whitespace | `d^` |
|
||||
| Change to start of line | `c0` |
|
||||
| Change to first non-whitespace | `c^` |
|
||||
| Delete from first line to here | `dgg` |
|
||||
| Delete from here to last line | `dG` |
|
||||
| Change from first line to here | `cgg` |
|
||||
| Change from here to last line | `cG` |
|
||||
| Undo last change | `u` |
|
||||
| Repeat last command | `.` |
|
||||
|
||||
Counts are also supported for editing commands. For example, `3dd` deletes three
|
||||
lines and `2cw` changes two words.
|
||||
|
||||
## Limitations
|
||||
|
||||
- On [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal):
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ const external = [
|
||||
'@lydell/node-pty-linux-x64',
|
||||
'@lydell/node-pty-win32-arm64',
|
||||
'@lydell/node-pty-win32-x64',
|
||||
'@github/keytar',
|
||||
'keytar',
|
||||
'@google/gemini-cli-devtools',
|
||||
];
|
||||
|
||||
|
||||
@@ -76,9 +76,7 @@ describe('ripgrep-real-direct', () => {
|
||||
|
||||
it('should find matches using the real ripgrep binary', async () => {
|
||||
const invocation = tool.build({ pattern: 'hello' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 2 matches');
|
||||
expect(result.llmContent).toContain('file1.txt');
|
||||
@@ -92,9 +90,7 @@ describe('ripgrep-real-direct', () => {
|
||||
|
||||
it('should handle no matches correctly', async () => {
|
||||
const invocation = tool.build({ pattern: 'nonexistent_pattern_123' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('No matches found');
|
||||
});
|
||||
@@ -110,9 +106,7 @@ describe('ripgrep-real-direct', () => {
|
||||
pattern: 'hello',
|
||||
include_pattern: '*.js',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
expect(result.llmContent).toContain('script.js');
|
||||
@@ -130,9 +124,7 @@ describe('ripgrep-real-direct', () => {
|
||||
pattern: 'match',
|
||||
context: 1,
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
expect(result.llmContent).toContain('context.txt');
|
||||
|
||||
@@ -1,55 +1,30 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-04-10T15:36:04.547Z",
|
||||
"updatedAt": "2026-04-08T01:21:58.770Z",
|
||||
"scenarios": {
|
||||
"multi-turn-conversation": {
|
||||
"heapUsedBytes": 120082704,
|
||||
"heapTotalBytes": 177586176,
|
||||
"rssBytes": 269172736,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:17.603Z"
|
||||
"timestamp": "2026-04-08T01:21:57.127Z"
|
||||
},
|
||||
"multi-function-call-repo-search": {
|
||||
"heapUsedBytes": 104644984,
|
||||
"heapTotalBytes": 111575040,
|
||||
"rssBytes": 204079104,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:22.480Z"
|
||||
"timestamp": "2026-04-08T01:21:58.770Z"
|
||||
},
|
||||
"idle-session-startup": {
|
||||
"heapUsedBytes": 119813672,
|
||||
"heapTotalBytes": 177061888,
|
||||
"rssBytes": 267943936,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:08.035Z"
|
||||
"timestamp": "2026-04-08T01:21:53.855Z"
|
||||
},
|
||||
"simple-prompt-response": {
|
||||
"heapUsedBytes": 119722064,
|
||||
"heapTotalBytes": 177324032,
|
||||
"rssBytes": 268812288,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:12.770Z"
|
||||
},
|
||||
"resume-large-chat-with-messages": {
|
||||
"heapUsedBytes": 106545568,
|
||||
"heapTotalBytes": 111509504,
|
||||
"rssBytes": 202596352,
|
||||
"externalBytes": 4306101,
|
||||
"timestamp": "2026-04-10T15:36:04.547Z"
|
||||
},
|
||||
"resume-large-chat": {
|
||||
"heapUsedBytes": 106513760,
|
||||
"heapTotalBytes": 111509504,
|
||||
"rssBytes": 202596352,
|
||||
"externalBytes": 4306101,
|
||||
"timestamp": "2026-04-10T15:35:59.528Z"
|
||||
},
|
||||
"large-chat": {
|
||||
"heapUsedBytes": 106471568,
|
||||
"heapTotalBytes": 111509504,
|
||||
"rssBytes": 202596352,
|
||||
"externalBytes": 4306101,
|
||||
"timestamp": "2026-04-10T15:35:53.180Z"
|
||||
"timestamp": "2026-04-08T01:21:55.491Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,15 +8,6 @@ import { describe, it, beforeAll, afterAll, afterEach } from 'vitest';
|
||||
import { TestRig, MemoryTestHarness } from '@google/gemini-cli-test-utils';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
createWriteStream,
|
||||
copyFileSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
rmSync,
|
||||
} from 'node:fs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const BASELINES_PATH = join(__dirname, 'baselines.json');
|
||||
@@ -191,312 +182,4 @@ describe('Memory Usage Tests', () => {
|
||||
harness.assertWithinBaseline(result);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Large Chat Scenarios', () => {
|
||||
let sharedResumeResponsesPath: string;
|
||||
let sharedActiveResponsesPath: string;
|
||||
let sharedHistoryPath: string;
|
||||
let sharedPrompts: string;
|
||||
let tempDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDir = join(__dirname, `large-chat-tmp-${randomUUID()}`);
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const { resumeResponsesPath, activeResponsesPath, historyPath, prompts } =
|
||||
await generateSharedLargeChatData(tempDir);
|
||||
sharedActiveResponsesPath = activeResponsesPath;
|
||||
sharedResumeResponsesPath = resumeResponsesPath;
|
||||
sharedHistoryPath = historyPath;
|
||||
sharedPrompts = prompts;
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => {
|
||||
if (existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('large-chat: memory usage within baseline', async () => {
|
||||
rig = new TestRig();
|
||||
rig.setup('memory-large-chat', {
|
||||
fakeResponsesPath: sharedActiveResponsesPath,
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
'large-chat',
|
||||
async (recordSnapshot) => {
|
||||
await rig.run({
|
||||
stdin: sharedPrompts,
|
||||
timeout: 600000,
|
||||
env: TEST_ENV,
|
||||
});
|
||||
|
||||
await recordSnapshot('after-large-chat');
|
||||
},
|
||||
);
|
||||
|
||||
if (UPDATE_BASELINES) {
|
||||
harness.updateScenarioBaseline(result);
|
||||
console.log(
|
||||
`Updated baseline for large-chat: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`,
|
||||
);
|
||||
} else {
|
||||
harness.assertWithinBaseline(result);
|
||||
}
|
||||
});
|
||||
|
||||
it('resume-large-chat: memory usage within baseline', async () => {
|
||||
rig = new TestRig();
|
||||
rig.setup('memory-resume-large-chat', {
|
||||
fakeResponsesPath: sharedResumeResponsesPath,
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
'resume-large-chat',
|
||||
async (recordSnapshot) => {
|
||||
// Ensure the history file is linked
|
||||
const targetChatsDir = join(
|
||||
rig.testDir!,
|
||||
'tmp',
|
||||
'test-project-hash',
|
||||
'chats',
|
||||
);
|
||||
mkdirSync(targetChatsDir, { recursive: true });
|
||||
const targetHistoryPath = join(
|
||||
targetChatsDir,
|
||||
'large-chat-session.json',
|
||||
);
|
||||
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
|
||||
copyFileSync(sharedHistoryPath, targetHistoryPath);
|
||||
|
||||
await rig.run({
|
||||
// add a prompt to make sure it does not hang there and exits immediately
|
||||
args: ['--resume', 'latest', '--prompt', 'hello'],
|
||||
timeout: 600000,
|
||||
env: TEST_ENV,
|
||||
});
|
||||
|
||||
await recordSnapshot('after-resume-large-chat');
|
||||
},
|
||||
);
|
||||
|
||||
if (UPDATE_BASELINES) {
|
||||
harness.updateScenarioBaseline(result);
|
||||
console.log(
|
||||
`Updated baseline for resume-large-chat: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`,
|
||||
);
|
||||
} else {
|
||||
harness.assertWithinBaseline(result);
|
||||
}
|
||||
});
|
||||
|
||||
it('resume-large-chat-with-messages: memory usage within baseline', async () => {
|
||||
rig = new TestRig();
|
||||
rig.setup('memory-resume-large-chat-msgs', {
|
||||
fakeResponsesPath: sharedResumeResponsesPath,
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
'resume-large-chat-with-messages',
|
||||
async (recordSnapshot) => {
|
||||
// Ensure the history file is linked
|
||||
const targetChatsDir = join(
|
||||
rig.testDir!,
|
||||
'tmp',
|
||||
'test-project-hash',
|
||||
'chats',
|
||||
);
|
||||
mkdirSync(targetChatsDir, { recursive: true });
|
||||
const targetHistoryPath = join(
|
||||
targetChatsDir,
|
||||
'large-chat-session.json',
|
||||
);
|
||||
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
|
||||
copyFileSync(sharedHistoryPath, targetHistoryPath);
|
||||
|
||||
const stdinContent = 'new prompt 1\nnew prompt 2\n';
|
||||
|
||||
await rig.run({
|
||||
args: ['--resume', 'latest'],
|
||||
stdin: stdinContent,
|
||||
timeout: 600000,
|
||||
env: TEST_ENV,
|
||||
});
|
||||
|
||||
await recordSnapshot('after-resume-and-append');
|
||||
},
|
||||
);
|
||||
|
||||
if (UPDATE_BASELINES) {
|
||||
harness.updateScenarioBaseline(result);
|
||||
console.log(
|
||||
`Updated baseline for resume-large-chat-with-messages: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`,
|
||||
);
|
||||
} else {
|
||||
harness.assertWithinBaseline(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function generateSharedLargeChatData(tempDir: string) {
|
||||
const resumeResponsesPath = join(tempDir, 'large-chat-resume-chat.responses');
|
||||
const activeResponsesPath = join(tempDir, 'large-chat-active-chat.responses');
|
||||
const historyPath = join(tempDir, 'large-chat-history.json');
|
||||
const sourceSessionPath = join(__dirname, 'large-chat-session.json');
|
||||
|
||||
const session = JSON.parse(readFileSync(sourceSessionPath, 'utf8'));
|
||||
const messages = session.messages;
|
||||
|
||||
copyFileSync(sourceSessionPath, historyPath);
|
||||
|
||||
// Generate fake responses for active chat
|
||||
const promptsList: string[] = [];
|
||||
const activeResponsesStream = createWriteStream(activeResponsesPath);
|
||||
const complexityResponse = {
|
||||
method: 'generateContent',
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: '{"complexity_reasoning":"simple","complexity_score":1}',
|
||||
},
|
||||
],
|
||||
role: 'model',
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const summaryResponse = {
|
||||
method: 'generateContent',
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ text: '{"originalSummary":"large chat summary","events":[]}' },
|
||||
],
|
||||
role: 'model',
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (msg.type === 'user') {
|
||||
promptsList.push(msg.content[0].text);
|
||||
|
||||
// Start of a new turn
|
||||
activeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
|
||||
|
||||
// Find all subsequent gemini messages until the next user message
|
||||
let j = i + 1;
|
||||
while (j < messages.length && messages[j].type === 'gemini') {
|
||||
const geminiMsg = messages[j];
|
||||
const parts = [];
|
||||
if (geminiMsg.content) {
|
||||
parts.push({ text: geminiMsg.content });
|
||||
}
|
||||
if (geminiMsg.toolCalls) {
|
||||
for (const tc of geminiMsg.toolCalls) {
|
||||
parts.push({
|
||||
functionCall: {
|
||||
name: tc.name,
|
||||
args: tc.args,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activeResponsesStream.write(
|
||||
JSON.stringify({
|
||||
method: 'generateContentStream',
|
||||
response: [
|
||||
{
|
||||
candidates: [
|
||||
{
|
||||
content: { parts, role: 'model' },
|
||||
finishReason: 'STOP',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 100,
|
||||
totalTokenCount: 200,
|
||||
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 100 }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}) + '\n',
|
||||
);
|
||||
j++;
|
||||
}
|
||||
// End of turn
|
||||
activeResponsesStream.write(JSON.stringify(summaryResponse) + '\n');
|
||||
// Skip the gemini messages we just processed
|
||||
i = j - 1;
|
||||
}
|
||||
}
|
||||
activeResponsesStream.end();
|
||||
|
||||
// Generate responses for resumed chat
|
||||
const resumeResponsesStream = createWriteStream(resumeResponsesPath);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
|
||||
resumeResponsesStream.write(
|
||||
JSON.stringify({
|
||||
method: 'generateContentStream',
|
||||
response: [
|
||||
{
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [{ text: `Resume response ${i}` }],
|
||||
role: 'model',
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 10,
|
||||
candidatesTokenCount: 10,
|
||||
totalTokenCount: 20,
|
||||
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 10 }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}) + '\n',
|
||||
);
|
||||
resumeResponsesStream.write(JSON.stringify(summaryResponse) + '\n');
|
||||
}
|
||||
resumeResponsesStream.end();
|
||||
|
||||
// Wait for streams to finish
|
||||
await Promise.all([
|
||||
new Promise((res) => activeResponsesStream.on('finish', res)),
|
||||
new Promise((res) => resumeResponsesStream.on('finish', res)),
|
||||
]);
|
||||
|
||||
return {
|
||||
resumeResponsesPath,
|
||||
activeResponsesPath,
|
||||
historyPath,
|
||||
prompts: promptsList.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
Generated
+81
-537
@@ -74,13 +74,13 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/keytar": "^7.10.6",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@lydell/node-pty-darwin-arm64": "1.1.0",
|
||||
"@lydell/node-pty-darwin-x64": "1.1.0",
|
||||
"@lydell/node-pty-linux-x64": "1.1.0",
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.0.0"
|
||||
}
|
||||
},
|
||||
@@ -1099,27 +1099,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/keytar": {
|
||||
"version": "7.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@github/keytar/-/keytar-7.10.6.tgz",
|
||||
"integrity": "sha512-mRW6cUsSG+nj4jp5gp8e91zPySaT73r+2JM6VyMZfrEgksjPmjSMr+tPGNOK3HUHV+GUU9B1LAiiYy/wmAnIxA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/keytar/node_modules/node-addon-api": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
|
||||
"integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/common": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz",
|
||||
@@ -1498,9 +1477,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.13",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
|
||||
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
|
||||
"version": "1.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
|
||||
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
@@ -5841,9 +5820,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz",
|
||||
"integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
|
||||
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -5936,9 +5915,9 @@
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -6782,9 +6761,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
@@ -7868,7 +7847,6 @@
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -9800,9 +9778,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.12",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
|
||||
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
|
||||
"version": "4.12.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
|
||||
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -11219,6 +11197,26 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keytar": {
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
|
||||
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^4.3.0",
|
||||
"prebuild-install": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keytar/node_modules/prebuild-install": {
|
||||
"name": "nop",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nop/-/nop-1.0.0.tgz",
|
||||
"integrity": "sha512-XdkOuXGx0DTwlqb0DWTcDqelgU/F3YyZ+PTRaecpDVpkYskcnh3OeUYKfvjcRQ2D1diTIGxi/a3eHVjW5yPupQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -11496,9 +11494,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -12241,6 +12239,13 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -12548,9 +12553,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-all2/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -13233,16 +13238,6 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
|
||||
@@ -13293,9 +13288,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14376,6 +14371,15 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/router/node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
||||
@@ -15984,9 +15988,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -16601,12 +16605,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -16696,463 +16700,6 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -17171,9 +16718,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -17255,9 +16802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -17687,18 +17234,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
@@ -18224,13 +17768,13 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/keytar": "^7.10.6",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@lydell/node-pty-darwin-arm64": "1.1.0",
|
||||
"@lydell/node-pty-darwin-x64": "1.1.0",
|
||||
"@lydell/node-pty-linux-x64": "1.1.0",
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.0.0"
|
||||
}
|
||||
},
|
||||
@@ -18379,9 +17923,9 @@
|
||||
}
|
||||
},
|
||||
"packages/core/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
+1
-1
@@ -150,13 +150,13 @@
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/keytar": "^7.10.6",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@lydell/node-pty-darwin-arm64": "1.1.0",
|
||||
"@lydell/node-pty-darwin-x64": "1.1.0",
|
||||
"@lydell/node-pty-linux-x64": "1.1.0",
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -101,8 +101,8 @@ export class AddMemoryCommand implements Command {
|
||||
const tool = toolRegistry.getTool(result.toolName);
|
||||
if (tool) {
|
||||
const abortController = new AbortController();
|
||||
const abortSignal = abortController.signal;
|
||||
await tool.buildAndExecute(result.toolArgs, abortSignal, undefined, {
|
||||
const signal = abortController.signal;
|
||||
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
|
||||
shellExecutionConfig: {
|
||||
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
|
||||
sandboxManager: loopContext.sandboxManager,
|
||||
|
||||
@@ -1129,9 +1129,7 @@ export class Session {
|
||||
});
|
||||
}
|
||||
|
||||
const toolResult: ToolResult = await invocation.execute({
|
||||
abortSignal,
|
||||
});
|
||||
const toolResult: ToolResult = await invocation.execute(abortSignal);
|
||||
const content = toToolCallContent(toolResult);
|
||||
|
||||
const updateContent: acp.ToolCallContent[] = content ? [content] : [];
|
||||
@@ -1673,7 +1671,7 @@ export class Session {
|
||||
kind: toAcpToolKind(readManyFilesTool.kind),
|
||||
});
|
||||
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
const content = toToolCallContent(result) || {
|
||||
type: 'content',
|
||||
content: {
|
||||
|
||||
@@ -990,6 +990,7 @@ export async function loadCliConfig(
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalMemoryManager: settings.experimental?.memoryManager,
|
||||
experimentalDynamicTools: settings.experimental?.dynamicTools,
|
||||
contextManagement,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
|
||||
|
||||
@@ -2217,6 +2217,16 @@ const SETTINGS_SCHEMA = {
|
||||
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
|
||||
showInDialog: true,
|
||||
},
|
||||
dynamicTools: {
|
||||
type: 'boolean',
|
||||
label: 'Dynamic Tools Documentation',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable documentation-injected dynamic tools experiment. This hides native tools and exposes a single execute() function.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
|
||||
@@ -1428,13 +1428,12 @@ describe('startInteractiveUI', () => {
|
||||
vi.mock('./ui/utils/updateCheck.js', () => ({
|
||||
checkForUpdates: vi.fn(() => Promise.resolve(null)),
|
||||
}));
|
||||
|
||||
vi.mock('./utils/cleanup.js', () => ({
|
||||
cleanupCheckpoints: vi.fn(() => Promise.resolve()),
|
||||
registerCleanup: vi.fn(),
|
||||
removeCleanup: vi.fn(),
|
||||
runExitCleanup: vi.fn(),
|
||||
registerSyncCleanup: vi.fn(),
|
||||
removeSyncCleanup: vi.fn(),
|
||||
registerTelemetryConfig: vi.fn(),
|
||||
setupSignalHandlers: vi.fn(),
|
||||
setupTtyCheck: vi.fn(() => vi.fn()),
|
||||
|
||||
@@ -142,9 +142,7 @@ vi.mock('./utils/cleanup.js', async (importOriginal) => {
|
||||
...actual,
|
||||
cleanupCheckpoints: vi.fn().mockResolvedValue(undefined),
|
||||
registerCleanup: vi.fn(),
|
||||
removeCleanup: vi.fn(),
|
||||
registerSyncCleanup: vi.fn(),
|
||||
removeSyncCleanup: vi.fn(),
|
||||
registerTelemetryConfig: vi.fn(),
|
||||
runExitCleanup: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
@@ -9,11 +9,7 @@ import { render } from 'ink';
|
||||
import { basename } from 'node:path';
|
||||
import { AppContainer } from './ui/AppContainer.js';
|
||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||
import {
|
||||
registerCleanup,
|
||||
removeCleanup,
|
||||
setupTtyCheck,
|
||||
} from './utils/cleanup.js';
|
||||
import { registerCleanup, setupTtyCheck } from './utils/cleanup.js';
|
||||
import {
|
||||
type StartupWarning,
|
||||
type Config,
|
||||
@@ -93,6 +89,7 @@ export async function startInteractiveUI(
|
||||
debugMode: config.getDebugMode(),
|
||||
});
|
||||
consolePatcher.patch();
|
||||
registerCleanup(consolePatcher.cleanup);
|
||||
|
||||
const { stdout: inkStdout, stderr: inkStderr } = createWorkingStdio();
|
||||
|
||||
@@ -170,11 +167,11 @@ export async function startInteractiveUI(
|
||||
},
|
||||
);
|
||||
|
||||
let cleanupLineWrapping: (() => void) | undefined;
|
||||
if (useAlternateBuffer) {
|
||||
disableLineWrapping();
|
||||
cleanupLineWrapping = () => enableLineWrapping();
|
||||
registerCleanup(cleanupLineWrapping);
|
||||
registerCleanup(() => {
|
||||
enableLineWrapping();
|
||||
});
|
||||
}
|
||||
|
||||
checkForUpdates(settings)
|
||||
@@ -188,48 +185,9 @@ export async function startInteractiveUI(
|
||||
}
|
||||
});
|
||||
|
||||
const cleanupUnmount = () => instance.unmount();
|
||||
registerCleanup(cleanupUnmount);
|
||||
registerCleanup(() => instance.unmount());
|
||||
|
||||
const cleanupTtyCheck = setupTtyCheck();
|
||||
registerCleanup(cleanupTtyCheck);
|
||||
|
||||
const cleanupConsolePatcher = () => consolePatcher.cleanup();
|
||||
registerCleanup(cleanupConsolePatcher);
|
||||
|
||||
try {
|
||||
await instance.waitUntilExit();
|
||||
} finally {
|
||||
try {
|
||||
removeCleanup(cleanupConsolePatcher);
|
||||
cleanupConsolePatcher();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.error('Error cleaning up console patcher:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
removeCleanup(cleanupUnmount);
|
||||
instance.unmount();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.error('Error unmounting Ink instance:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
removeCleanup(cleanupTtyCheck);
|
||||
cleanupTtyCheck();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.error('Error in TTY cleanup:', e);
|
||||
}
|
||||
|
||||
if (cleanupLineWrapping) {
|
||||
try {
|
||||
removeCleanup(cleanupLineWrapping);
|
||||
cleanupLineWrapping();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.error('Error restoring line wrapping:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
registerCleanup(setupTtyCheck());
|
||||
}
|
||||
|
||||
function setWindowTitle(title: string, settings: LoadedSettings) {
|
||||
|
||||
@@ -3290,85 +3290,6 @@ describe('AppContainer State Management', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not reset the hint timer when overflowingIdsSize decreases', async () => {
|
||||
const { unmount } = await act(async () => renderAppContainer());
|
||||
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id-1');
|
||||
capturedOverflowActions.addOverflowingId('test-id-2');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.removeOverflowingId('test-id-2');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not auto-reset the hint timer for new overflow while expanded', async () => {
|
||||
const { stdin, unmount } = await act(async () => renderAppContainer());
|
||||
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id-1');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.write('\x0f'); // Ctrl+O
|
||||
});
|
||||
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 1000);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id-2');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
|
||||
const { stdin, unmount } = await act(async () => renderAppContainer());
|
||||
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
|
||||
|
||||
@@ -136,11 +136,7 @@ import { type IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
|
||||
import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
|
||||
import { type UpdateObject } from './utils/updateCheck.js';
|
||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||
import {
|
||||
registerCleanup,
|
||||
removeCleanup,
|
||||
runExitCleanup,
|
||||
} from '../utils/cleanup.js';
|
||||
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||
import { relaunchApp } from '../utils/processUtils.js';
|
||||
import type { SessionInfo } from '../utils/sessionUtils.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
@@ -187,7 +183,6 @@ import {
|
||||
isToolAwaitingConfirmation,
|
||||
getAllToolCalls,
|
||||
} from './utils/historyUtils.js';
|
||||
import { shouldAutoTriggerExpandHint } from './utils/expandHint.js';
|
||||
|
||||
interface AppContainerProps {
|
||||
config: Config;
|
||||
@@ -330,35 +325,24 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const showIsExpandableHint = Boolean(expandHintTrigger);
|
||||
const overflowState = useOverflowState();
|
||||
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
|
||||
const previousOverflowingIdsSizeRef = useRef(0);
|
||||
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
|
||||
|
||||
/**
|
||||
* Manages the visibility and x-second timer for the expansion hint.
|
||||
*
|
||||
* This effect triggers the timer countdown whenever an overflow is detected
|
||||
* while the response is actually constrained. The Ctrl+O handler still
|
||||
* refreshes the hint manually when the user toggles expansion.
|
||||
* or the user manually toggles the expansion state with Ctrl+O.
|
||||
* By depending on overflowingIdsSize, the timer resets when *new* views
|
||||
* overflow, but avoids infinitely resetting during single-view streaming.
|
||||
*
|
||||
* We only auto-refresh when the number of overflowing regions grows. That
|
||||
* keeps the "show more" hint responsive for newly truncated content without
|
||||
* retriggering on layout churn, overflow shrinkage, or while the content is
|
||||
* already expanded.
|
||||
* In alternate buffer mode, we don't trigger the hint automatically on overflow
|
||||
* to avoid noise, but the user can still trigger it manually with Ctrl+O.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const previousOverflowingIdsSize = previousOverflowingIdsSizeRef.current;
|
||||
|
||||
if (
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight,
|
||||
overflowingIdsSize,
|
||||
previousOverflowingIdsSize,
|
||||
})
|
||||
) {
|
||||
if (hasOverflowState) {
|
||||
triggerExpandHint(true);
|
||||
}
|
||||
|
||||
previousOverflowingIdsSizeRef.current = overflowingIdsSize;
|
||||
}, [constrainHeight, overflowingIdsSize, triggerExpandHint]);
|
||||
}, [hasOverflowState, overflowingIdsSize, triggerExpandHint]);
|
||||
|
||||
const [defaultBannerText, setDefaultBannerText] = useState('');
|
||||
const [warningBannerText, setWarningBannerText] = useState('');
|
||||
@@ -535,7 +519,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
debugLogger.warn('Background summary generation failed:', e);
|
||||
});
|
||||
})();
|
||||
const cleanupFn = async () => {
|
||||
registerCleanup(async () => {
|
||||
// Turn off mouse scroll.
|
||||
disableMouseEvents();
|
||||
|
||||
@@ -551,15 +535,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
// Fire SessionEnd hook on cleanup (only if hooks are enabled)
|
||||
await config?.getHookSystem()?.fireSessionEndEvent(SessionEndReason.Exit);
|
||||
};
|
||||
registerCleanup(cleanupFn);
|
||||
|
||||
return () => {
|
||||
removeCleanup(cleanupFn);
|
||||
cleanupFn().catch((e: unknown) =>
|
||||
debugLogger.error('Error during cleanup:', e),
|
||||
);
|
||||
};
|
||||
});
|
||||
// Disable the dependencies check here. historyManager gets flagged
|
||||
// but we don't want to react to changes to it because each new history
|
||||
// item, including the ones from the start session hook will cause a
|
||||
|
||||
@@ -15,6 +15,7 @@ export const settingsCommand: SlashCommand = {
|
||||
description: 'View and edit Gemini CLI settings',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
isSafeConcurrent: true,
|
||||
action: (_context, _args): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'settings',
|
||||
|
||||
@@ -22,7 +22,8 @@ import { theme } from '../../semantic-colors.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
import { isShellTool } from './ToolShared.js';
|
||||
import {
|
||||
isVisibleInToolGroup,
|
||||
shouldHideToolCall,
|
||||
CoreToolCallStatus,
|
||||
Kind,
|
||||
EDIT_DISPLAY_NAME,
|
||||
GLOB_DISPLAY_NAME,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
READ_MANY_FILES_DISPLAY_NAME,
|
||||
isFileDiff,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { buildToolVisibilityContextFromDisplay } from '../../utils/historyUtils.js';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
@@ -125,13 +125,40 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
// Filter out tool calls that should be hidden (e.g. in-progress Ask User, or Plan Mode operations).
|
||||
const visibleToolCalls = useMemo(
|
||||
() =>
|
||||
allToolCalls.filter((t) =>
|
||||
// Use the unified visibility utility
|
||||
isVisibleInToolGroup(
|
||||
buildToolVisibilityContextFromDisplay(t),
|
||||
isLowErrorVerbosity ? 'low' : 'full',
|
||||
),
|
||||
),
|
||||
allToolCalls.filter((t) => {
|
||||
// Hide internal errors unless full verbosity
|
||||
if (
|
||||
isLowErrorVerbosity &&
|
||||
t.status === CoreToolCallStatus.Error &&
|
||||
!t.isClientInitiated
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Standard hiding logic (e.g. Plan Mode internal edits)
|
||||
if (
|
||||
shouldHideToolCall({
|
||||
displayName: t.name,
|
||||
status: t.status,
|
||||
approvalMode: t.approvalMode,
|
||||
hasResultDisplay: !!t.resultDisplay,
|
||||
parentCallId: t.parentCallId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We HIDE tools that are still in pre-execution states (Confirming, Pending)
|
||||
// from the History log. They live in the Global Queue or wait for their turn.
|
||||
// Only show tools that are actually running or finished.
|
||||
const displayStatus = mapCoreStatusToDisplayStatus(t.status);
|
||||
|
||||
// We hide Confirming tools from the history log because they are
|
||||
// currently being rendered in the interactive ToolConfirmationQueue.
|
||||
// We show everything else, including Pending (waiting to run) and
|
||||
// Canceled (rejected by user), to ensure the history is complete
|
||||
// and to avoid tools "vanishing" after approval.
|
||||
return displayStatus !== ToolCallStatus.Confirming;
|
||||
}),
|
||||
[allToolCalls, isLowErrorVerbosity],
|
||||
);
|
||||
|
||||
|
||||
@@ -533,7 +533,7 @@ async function readLocalFiles(
|
||||
let invocation: AnyToolInvocation | undefined = undefined;
|
||||
try {
|
||||
invocation = readManyFilesTool.build(toolArgs);
|
||||
const result = await invocation.execute({ abortSignal: signal });
|
||||
const result = await invocation.execute(signal);
|
||||
const display: IndividualToolCallDisplay = {
|
||||
callId: `client-read-${userMessageTimestamp}`,
|
||||
name: readManyFilesTool.displayName,
|
||||
|
||||
@@ -133,7 +133,6 @@ describe('useExecutionLifecycle', () => {
|
||||
mockConfig = {
|
||||
getTargetDir: () => '/test/dir',
|
||||
getEnableInteractiveShell: () => false,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getShellExecutionConfig: () => ({
|
||||
terminalHeight: 20,
|
||||
terminalWidth: 80,
|
||||
@@ -247,32 +246,11 @@ describe('useExecutionLifecycle', () => {
|
||||
expect.any(Function),
|
||||
expect.any(Object),
|
||||
false,
|
||||
expect.objectContaining({
|
||||
sessionId: 'test-session-id',
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(onExecMock).toHaveBeenCalledWith(expect.any(Promise));
|
||||
});
|
||||
|
||||
it('should pass the config sessionId into shell execution config', async () => {
|
||||
const { result } = await renderProcessorHook();
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleShellCommand('top', new AbortController().signal);
|
||||
});
|
||||
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'/test/dir',
|
||||
expect.any(Function),
|
||||
expect.any(Object),
|
||||
false,
|
||||
expect.objectContaining({
|
||||
sessionId: 'test-session-id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle successful execution and update history correctly', async () => {
|
||||
const { result } = await renderProcessorHook();
|
||||
|
||||
|
||||
@@ -409,7 +409,6 @@ export const useExecutionLifecycle = (
|
||||
const activeTheme = themeManager.getActiveTheme();
|
||||
const shellExecutionConfig = {
|
||||
...config.getShellExecutionConfig(),
|
||||
sessionId: config.getSessionId(),
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
defaultFg: activeTheme.colors.Foreground,
|
||||
|
||||
@@ -39,8 +39,7 @@ import {
|
||||
isBackgroundExecutionData,
|
||||
Kind,
|
||||
ACTIVATE_SKILL_TOOL_NAME,
|
||||
isRenderedInHistory,
|
||||
buildToolVisibilityContext,
|
||||
shouldHideToolCall,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
UPDATE_TOPIC_DISPLAY_NAME,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -648,8 +647,29 @@ export const useGeminiStream = (
|
||||
toolCalls.every((tc) => pushedToolCallIds.has(tc.request.callId));
|
||||
|
||||
const isToolVisible = (tc: TrackedToolCall) => {
|
||||
const displayName = tc.tool?.displayName ?? tc.request.name;
|
||||
|
||||
let hasResultDisplay = false;
|
||||
if (
|
||||
tc.status === CoreToolCallStatus.Success ||
|
||||
tc.status === CoreToolCallStatus.Error ||
|
||||
tc.status === CoreToolCallStatus.Cancelled
|
||||
) {
|
||||
hasResultDisplay = !!tc.response?.resultDisplay;
|
||||
} else if (tc.status === CoreToolCallStatus.Executing) {
|
||||
hasResultDisplay = !!tc.liveOutput;
|
||||
}
|
||||
|
||||
// AskUser tools and Plan Mode write/edit are handled by this logic
|
||||
if (!isRenderedInHistory(buildToolVisibilityContext(tc))) {
|
||||
if (
|
||||
shouldHideToolCall({
|
||||
displayName,
|
||||
status: tc.status,
|
||||
approvalMode: tc.approvalMode,
|
||||
hasResultDisplay,
|
||||
parentCallId: tc.request.parentCallId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useTimedMessage } from './useTimedMessage.js';
|
||||
|
||||
describe('useTimedMessage', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('resets the timeout when the same message is retriggered', async () => {
|
||||
const { result, unmount } = await renderHook(() => useTimedMessage(1000));
|
||||
|
||||
act(() => {
|
||||
result.current[1]('hint');
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current[1]('hint');
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
expect(result.current[0]).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clears the message immediately when asked to hide it', async () => {
|
||||
const { result, unmount } = await renderHook(() => useTimedMessage(1000));
|
||||
|
||||
act(() => {
|
||||
result.current[1]('hint');
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
result.current[1](null);
|
||||
});
|
||||
expect(result.current[0]).toBeNull();
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(result.current[0]).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -12,29 +12,19 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
*/
|
||||
export function useTimedMessage<T>(durationMs: number) {
|
||||
const [message, setMessage] = useState<T | null>(null);
|
||||
const messageRef = useRef<T | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: T | null) => {
|
||||
setMessage(msg);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
if (msg !== null) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
messageRef.current = null;
|
||||
setMessage((prev) => (prev === null ? prev : null));
|
||||
setMessage(null);
|
||||
}, durationMs);
|
||||
}
|
||||
|
||||
if (Object.is(messageRef.current, msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageRef.current = msg;
|
||||
setMessage(msg);
|
||||
},
|
||||
[durationMs],
|
||||
);
|
||||
@@ -43,7 +33,6 @@ export function useTimedMessage<T>(durationMs: number) {
|
||||
() => () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -637,8 +637,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.PASTE_CLIPBOARD]: 'Paste from the clipboard.',
|
||||
|
||||
// App Controls
|
||||
[Command.SHOW_ERROR_DETAILS]:
|
||||
'Toggle the debug console for detailed error information.',
|
||||
[Command.SHOW_ERROR_DETAILS]: 'Toggle detailed error information.',
|
||||
[Command.SHOW_FULL_TODOS]: 'Toggle the full TODO list.',
|
||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.',
|
||||
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
|
||||
|
||||
@@ -4,18 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
CoreToolCallStatus,
|
||||
belongsInConfirmationQueue,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type HistoryItemWithoutId,
|
||||
type IndividualToolCallDisplay,
|
||||
} from '../types.js';
|
||||
import {
|
||||
getAllToolCalls,
|
||||
buildToolVisibilityContextFromDisplay,
|
||||
} from './historyUtils.js';
|
||||
import { getAllToolCalls } from './historyUtils.js';
|
||||
|
||||
export interface ConfirmingToolState {
|
||||
tool: IndividualToolCallDisplay;
|
||||
@@ -39,18 +33,14 @@ export function getConfirmingToolState(
|
||||
return null;
|
||||
}
|
||||
|
||||
const actionablePendingTools = allPendingTools.filter((tool) =>
|
||||
belongsInConfirmationQueue(buildToolVisibilityContextFromDisplay(tool)),
|
||||
);
|
||||
|
||||
const head = confirmingTools[0];
|
||||
const headIndexInFullList = actionablePendingTools.findIndex(
|
||||
const headIndexInFullList = allPendingTools.findIndex(
|
||||
(tool) => tool.callId === head.callId,
|
||||
);
|
||||
|
||||
return {
|
||||
tool: head,
|
||||
index: headIndexInFullList + 1,
|
||||
total: actionablePendingTools.length,
|
||||
total: allPendingTools.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { shouldAutoTriggerExpandHint } from './expandHint.js';
|
||||
|
||||
describe('shouldAutoTriggerExpandHint', () => {
|
||||
it('returns true when constrained content gains a new overflowing region', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: true,
|
||||
overflowingIdsSize: 2,
|
||||
previousOverflowingIdsSize: 1,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when overflowingIdsSize decreases', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: true,
|
||||
overflowingIdsSize: 1,
|
||||
previousOverflowingIdsSize: 2,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when overflowingIdsSize is unchanged', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: true,
|
||||
overflowingIdsSize: 1,
|
||||
previousOverflowingIdsSize: 1,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false while content is already expanded', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: false,
|
||||
overflowingIdsSize: 2,
|
||||
previousOverflowingIdsSize: 1,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
interface ExpandHintAutoTriggerParams {
|
||||
constrainHeight: boolean;
|
||||
overflowingIdsSize: number;
|
||||
previousOverflowingIdsSize: number;
|
||||
}
|
||||
|
||||
export function shouldAutoTriggerExpandHint({
|
||||
constrainHeight,
|
||||
overflowingIdsSize,
|
||||
previousOverflowingIdsSize,
|
||||
}: ExpandHintAutoTriggerParams): boolean {
|
||||
return constrainHeight && overflowingIdsSize > previousOverflowingIdsSize;
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type ToolVisibilityContext } from '@google/gemini-cli-core';
|
||||
import { CoreToolCallStatus } from '../types.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
@@ -13,23 +12,6 @@ import type {
|
||||
IndividualToolCallDisplay,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
* Maps an IndividualToolCallDisplay from the CLI to a ToolVisibilityContext for core logic.
|
||||
*/
|
||||
export function buildToolVisibilityContextFromDisplay(
|
||||
tool: IndividualToolCallDisplay,
|
||||
): ToolVisibilityContext {
|
||||
return {
|
||||
name: tool.originalRequestName ?? tool.name,
|
||||
displayName: tool.name, // In CLI, 'name' is usually the resolved display name
|
||||
status: tool.status,
|
||||
hasResult: !!tool.resultDisplay,
|
||||
approvalMode: tool.approvalMode,
|
||||
isClientInitiated: tool.isClientInitiated,
|
||||
parentCallId: tool.parentCallId,
|
||||
};
|
||||
}
|
||||
|
||||
export function getLastTurnToolCallIds(
|
||||
history: HistoryItem[],
|
||||
pendingHistoryItems: HistoryItemWithoutId[],
|
||||
|
||||
@@ -266,29 +266,6 @@ describe('textUtils', () => {
|
||||
// 0xA0 is non-breaking space, should be preserved
|
||||
expect(stripUnsafeCharacters('hello\xA0world')).toBe('hello\xA0world');
|
||||
});
|
||||
|
||||
it('should not lose text after DCS (0x90) — regression for data loss', () => {
|
||||
// 0x90 (DCS) starts a Device Control String that stripVTControlCharacters
|
||||
// treats as an unterminated sequence, swallowing all subsequent text.
|
||||
// Stripping C1 chars before VT processing prevents this data loss.
|
||||
expect(stripUnsafeCharacters('important\x90data after DCS')).toBe(
|
||||
'importantdata after DCS',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fully strip 8-bit CSI (0x9B) sequences', () => {
|
||||
// 0x9B (CSI) is equivalent to ESC[. stripAnsi should handle the
|
||||
// whole sequence including parameters.
|
||||
expect(stripUnsafeCharacters('keep\x9B42mthis text')).toBe(
|
||||
'keepthis text',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not lose text when multiple C1 chars precede valid content', () => {
|
||||
expect(stripUnsafeCharacters('start\x90\x9B\x85middle\x80end')).toBe(
|
||||
'startmiddleend',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANSI escape sequence stripping', () => {
|
||||
|
||||
@@ -98,16 +98,8 @@ export function cpSlice(str: string, start: number, end?: number): string {
|
||||
/**
|
||||
* Strip characters that can break terminal rendering.
|
||||
*
|
||||
* This is a strict sanitization function intended for general display
|
||||
* contexts. It strips all C1 control characters (0x80-0x9F) and VT
|
||||
* control sequences. For list display contexts where a more lenient
|
||||
* approach is needed (preserving C1 characters and only stripping ANSI
|
||||
* codes and newlines/tabs), use a separate function instead.
|
||||
*
|
||||
* Processing order:
|
||||
* 1. stripAnsi removes ANSI escape sequences (including 8-bit CSI 0x9B)
|
||||
* 2. Regex strips C0, C1, BiDi, and zero-width control characters
|
||||
* 3. stripVTControlCharacters removes any remaining VT sequences
|
||||
* Uses Node.js built-in stripVTControlCharacters to handle VT sequences,
|
||||
* then filters remaining control characters that can disrupt display.
|
||||
*
|
||||
* Characters stripped:
|
||||
* - ANSI escape sequences (via strip-ansi)
|
||||
@@ -127,20 +119,18 @@ export function cpSlice(str: string, start: number, end?: number): string {
|
||||
*/
|
||||
export function stripUnsafeCharacters(str: string): string {
|
||||
const strippedAnsi = stripAnsi(str);
|
||||
const strippedVT = stripVTControlCharacters(strippedAnsi);
|
||||
|
||||
// Strip C0, C1, and other unsafe characters via regex first.
|
||||
// This is more efficient than multiple replaces and crucially removes C1
|
||||
// characters (e.g., 0x90 DCS) before they can be misinterpreted by
|
||||
// stripVTControlCharacters, which could otherwise cause data loss.
|
||||
const strippedWithRegex = strippedAnsi.replace(
|
||||
// Use a regex to strip remaining unsafe control characters
|
||||
// C0: 0x00-0x1F except 0x09 (TAB), 0x0A (LF), 0x0D (CR)
|
||||
// C1: 0x80-0x9F
|
||||
// BiDi: U+200E (LRM), U+200F (RLM), U+202A-U+202E, U+2066-U+2069
|
||||
// Zero-width: U+200B (ZWSP), U+FEFF (BOM)
|
||||
return strippedVT.replace(
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F\u200E\u200F\u202A-\u202E\u2066-\u2069\u200B\uFEFF]/g,
|
||||
'',
|
||||
);
|
||||
|
||||
// Finally, use stripVTControlCharacters for any remaining VT sequences
|
||||
// that the regex might not cover.
|
||||
return stripVTControlCharacters(strippedWithRegex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,24 +24,10 @@ export function registerCleanup(fn: (() => void) | (() => Promise<void>)) {
|
||||
cleanupFunctions.push(fn);
|
||||
}
|
||||
|
||||
export function removeCleanup(fn: (() => void) | (() => Promise<void>)) {
|
||||
const index = cleanupFunctions.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
cleanupFunctions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSyncCleanup(fn: () => void) {
|
||||
syncCleanupFunctions.push(fn);
|
||||
}
|
||||
|
||||
export function removeSyncCleanup(fn: () => void) {
|
||||
const index = syncCleanupFunctions.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
syncCleanupFunctions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal cleanup state for testing purposes.
|
||||
* This allows tests to run in isolation without vi.resetModules().
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('succeeds if effectiveAuthType matches enforcedType', async () => {
|
||||
it('succeeds if effectiveAuthType matches enforcedAuthType', async () => {
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
process.env['GEMINI_API_KEY'] = 'fake-key';
|
||||
const nonInteractiveConfig = createLocalMockConfig({});
|
||||
@@ -308,7 +308,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exits if configuredAuthType does not match enforcedType', async () => {
|
||||
it('exits if configuredAuthType does not match enforcedAuthType', async () => {
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.LOGIN_WITH_GOOGLE;
|
||||
const nonInteractiveConfig = createLocalMockConfig({
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||
@@ -334,7 +334,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('exits if auth from env var does not match enforcedType', async () => {
|
||||
it('exits if auth from env var does not match enforcedAuthType', async () => {
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.LOGIN_WITH_GOOGLE;
|
||||
process.env['GEMINI_API_KEY'] = 'fake-key';
|
||||
const nonInteractiveConfig = createLocalMockConfig({
|
||||
|
||||
@@ -91,13 +91,13 @@
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/keytar": "^7.10.6",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@lydell/node-pty-darwin-arm64": "1.1.0",
|
||||
"@lydell/node-pty-darwin-x64": "1.1.0",
|
||||
"@lydell/node-pty-linux-x64": "1.1.0",
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type ToolResult,
|
||||
BaseToolInvocation,
|
||||
type ToolCallConfirmationDetails,
|
||||
type ExecuteOptions,
|
||||
type ToolLiveOutput,
|
||||
} from '../tools/tools.js';
|
||||
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
@@ -185,8 +185,10 @@ class DelegateInvocation extends BaseToolInvocation<
|
||||
return invocation.shouldConfirmExecute(abortSignal);
|
||||
}
|
||||
|
||||
async execute(options: ExecuteOptions): Promise<ToolResult> {
|
||||
const { abortSignal: signal, updateOutput } = options;
|
||||
async execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: ToolLiveOutput) => void,
|
||||
): Promise<ToolResult> {
|
||||
const hintedParams = this.withUserHints(this.mappedInputs);
|
||||
const invocation = this.buildChildInvocation(hintedParams);
|
||||
|
||||
@@ -202,10 +204,7 @@ class DelegateInvocation extends BaseToolInvocation<
|
||||
},
|
||||
async ({ metadata }) => {
|
||||
metadata.input = this.params;
|
||||
const result = await invocation.execute({
|
||||
abortSignal: signal,
|
||||
updateOutput,
|
||||
});
|
||||
const result = await invocation.execute(signal, updateOutput);
|
||||
metadata.output = result;
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -99,9 +99,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Find the blue submit button',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Verify screenshot was captured
|
||||
expect(browserManager.callTool).toHaveBeenCalledWith(
|
||||
@@ -167,7 +165,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Find the search bar',
|
||||
});
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
const contentGenerator = config.getContentGenerator();
|
||||
expect(contentGenerator.generateContent).toHaveBeenCalledWith(
|
||||
@@ -196,9 +194,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Find the button',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain('Failed to capture screenshot');
|
||||
@@ -221,9 +217,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Check the layout',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain('Visual model returned no analysis');
|
||||
@@ -244,9 +238,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Find the red error',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain(
|
||||
@@ -269,9 +261,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Identify the element',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain(
|
||||
@@ -291,9 +281,7 @@ describe('analyzeScreenshot', () => {
|
||||
const invocation = tool.build({
|
||||
instruction: 'Find something',
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain('Visual analysis failed');
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Kind,
|
||||
type ToolResult,
|
||||
type ToolInvocation,
|
||||
type ExecuteOptions,
|
||||
} from '../../tools/tools.js';
|
||||
import { Environment } from '@google/genai';
|
||||
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
|
||||
@@ -81,7 +80,7 @@ class AnalyzeScreenshotInvocation extends BaseToolInvocation<
|
||||
return `Visual analysis: "${instruction}"`;
|
||||
}
|
||||
|
||||
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
const instruction = String(this.params['instruction'] ?? '');
|
||||
|
||||
|
||||
@@ -223,10 +223,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
const controller = new AbortController();
|
||||
const updateOutput: (output: ToolLiveOutput) => void = vi.fn();
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: controller.signal,
|
||||
updateOutput,
|
||||
});
|
||||
const result = await invocation.execute(controller.signal, updateOutput);
|
||||
|
||||
expect(Array.isArray(result.llmContent)).toBe(true);
|
||||
expect((result.llmContent as Array<{ text: string }>)[0].text).toContain(
|
||||
@@ -245,7 +242,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
const controller = new AbortController();
|
||||
// Should not throw even with no updateOutput
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: controller.signal }),
|
||||
invocation.execute(controller.signal),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
@@ -259,9 +256,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
);
|
||||
|
||||
const controller = new AbortController();
|
||||
const result = await invocation.execute({
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
const result = await invocation.execute(controller.signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(removeInputBlocker).toHaveBeenCalled();
|
||||
@@ -303,10 +298,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, updateOutput);
|
||||
|
||||
const firstCall = updateOutput.mock.calls[0]?.[0] as SubagentProgress;
|
||||
expect(firstCall.isSubagentProgress).toBe(true);
|
||||
@@ -323,10 +315,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, updateOutput);
|
||||
|
||||
const lastCall = updateOutput.mock.calls[
|
||||
updateOutput.mock.calls.length - 1
|
||||
@@ -345,10 +334,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
// Allow createBrowserAgentDefinition to resolve and onActivity to be registered
|
||||
await Promise.resolve();
|
||||
@@ -388,10 +377,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
// Allow createBrowserAgentDefinition to resolve and onActivity to be registered
|
||||
await Promise.resolve();
|
||||
@@ -435,10 +424,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -486,10 +475,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -530,10 +519,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -575,10 +564,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -615,10 +604,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -658,10 +647,10 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const executePromise = invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -714,10 +703,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockParams,
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput: vi.fn(),
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, vi.fn());
|
||||
|
||||
expect(recordBrowserAgentTaskOutcome).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
@@ -745,10 +731,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, updateOutput);
|
||||
|
||||
expect(recordBrowserAgentTaskOutcome).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
@@ -768,10 +751,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockParams,
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput: vi.fn(),
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, vi.fn());
|
||||
|
||||
expect(cleanupBrowserAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -827,7 +807,7 @@ describe('BrowserAgentInvocation', () => {
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Verify list_pages was called
|
||||
expect(mockBrowserManager.callTool).toHaveBeenCalledWith(
|
||||
|
||||
@@ -22,7 +22,7 @@ import { LocalAgentExecutor } from '../local-executor.js';
|
||||
import {
|
||||
BaseToolInvocation,
|
||||
type ToolResult,
|
||||
type ExecuteOptions,
|
||||
type ToolLiveOutput,
|
||||
} from '../../tools/tools.js';
|
||||
import { ToolErrorType } from '../../tools/tool-error.js';
|
||||
import {
|
||||
@@ -107,8 +107,10 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
|
||||
* 3. Runs the agent via LocalAgentExecutor
|
||||
* 4. Cleans up browser resources
|
||||
*/
|
||||
async execute(options: ExecuteOptions): Promise<ToolResult> {
|
||||
const { abortSignal: signal, updateOutput } = options;
|
||||
async execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: ToolLiveOutput) => void,
|
||||
): Promise<ToolResult> {
|
||||
const invocationStartMs = Date.now();
|
||||
let browserManager;
|
||||
let recentActivity: SubagentActivityItem[] = [];
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('mcpToolWrapper', () => {
|
||||
);
|
||||
|
||||
const invocation = tools[1].build({ uid: 'elem-123' });
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockBrowserManager.callTool).toHaveBeenCalledWith(
|
||||
'click',
|
||||
@@ -158,9 +158,7 @@ describe('mcpToolWrapper', () => {
|
||||
);
|
||||
|
||||
const invocation = tools[0].build({ verbose: true });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toBe('Tool result');
|
||||
expect(result.error).toBeUndefined();
|
||||
@@ -179,9 +177,7 @@ describe('mcpToolWrapper', () => {
|
||||
);
|
||||
|
||||
const invocation = tools[1].build({ uid: 'invalid' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.message).toBe('Element not found');
|
||||
@@ -199,9 +195,7 @@ describe('mcpToolWrapper', () => {
|
||||
);
|
||||
|
||||
const invocation = tools[0].build({});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.message).toBe('Connection lost');
|
||||
@@ -218,7 +212,7 @@ describe('mcpToolWrapper', () => {
|
||||
|
||||
const clickTool = tools.find((t) => t.name === 'click')!;
|
||||
const invocation = clickTool.build({ uid: 'elem-42' });
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
// callTool: suspend blocker + click + resume blocker
|
||||
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(3);
|
||||
@@ -263,7 +257,7 @@ describe('mcpToolWrapper', () => {
|
||||
|
||||
const snapshotTool = tools.find((t) => t.name === 'take_snapshot')!;
|
||||
const invocation = snapshotTool.build({});
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
// callTool should only be called once for take_snapshot — no suspend/resume
|
||||
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(1);
|
||||
@@ -283,7 +277,7 @@ describe('mcpToolWrapper', () => {
|
||||
|
||||
const clickTool = tools.find((t) => t.name === 'click')!;
|
||||
const invocation = clickTool.build({ uid: 'elem-42' });
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
// callTool should only be called once for click — no suspend/resume
|
||||
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(1);
|
||||
@@ -303,9 +297,7 @@ describe('mcpToolWrapper', () => {
|
||||
|
||||
const clickTool = tools.find((t) => t.name === 'click')!;
|
||||
const invocation = clickTool.build({ uid: 'bad-elem' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Should return error, not throw
|
||||
expect(result.error).toBeDefined();
|
||||
@@ -336,9 +328,7 @@ describe('mcpToolWrapper', () => {
|
||||
|
||||
const uploadTool = tools.find((t) => t.name === 'upload_file')!;
|
||||
const invocation = uploadTool.build({ path: 'test.txt' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain('File uploads are blocked');
|
||||
@@ -355,9 +345,7 @@ describe('mcpToolWrapper', () => {
|
||||
|
||||
const uploadTool = tools.find((t) => t.name === 'upload_file')!;
|
||||
const invocation = uploadTool.build({ path: 'test.txt' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.llmContent).toBe('Tool result');
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
type ToolInvocation,
|
||||
type ToolCallConfirmationDetails,
|
||||
type PolicyUpdateOptions,
|
||||
type ExecuteOptions,
|
||||
} from '../../tools/tools.js';
|
||||
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
|
||||
import {
|
||||
@@ -118,7 +117,7 @@ class McpToolInvocation extends BaseToolInvocation<
|
||||
return this.shouldDisableInput && INTERACTIVE_TOOLS.has(this.toolName);
|
||||
}
|
||||
|
||||
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
// Hard block for file uploads if configured
|
||||
if (this.blockFileUploads && this.toolName === 'upload_file') {
|
||||
|
||||
@@ -110,8 +110,8 @@ export const CodebaseInvestigatorAgent = (
|
||||
},
|
||||
|
||||
runConfig: {
|
||||
maxTimeMinutes: 10,
|
||||
maxTurns: 50,
|
||||
maxTimeMinutes: 3,
|
||||
maxTurns: 10,
|
||||
},
|
||||
|
||||
toolConfig: {
|
||||
|
||||
@@ -83,6 +83,12 @@ import {
|
||||
COMPLETE_TASK_TOOL_NAME,
|
||||
ACTIVATE_SKILL_TOOL_NAME,
|
||||
} from '../tools/definitions/base-declarations.js';
|
||||
import {
|
||||
getDynamicToolsDocumentation,
|
||||
EXECUTE_FUNCTION_DECLARATION,
|
||||
unwrapExecuteArgs,
|
||||
wrapAsExecute,
|
||||
} from '../utils/dynamicToolsUtils.js';
|
||||
|
||||
/** A callback function to report on agent activity. */
|
||||
export type ActivityCallback = (activity: SubagentActivityEvent) => void;
|
||||
@@ -114,7 +120,7 @@ export function createUnauthorizedToolError(toolName: string): string {
|
||||
export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
readonly definition: LocalAgentDefinition<TOutput>;
|
||||
|
||||
readonly agentId: string;
|
||||
private readonly agentId: string;
|
||||
private readonly toolRegistry: ToolRegistry;
|
||||
private readonly promptRegistry: PromptRegistry;
|
||||
private readonly resourceRegistry: ResourceRegistry;
|
||||
@@ -1075,7 +1081,58 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
|
||||
for (const [index, functionCall] of functionCalls.entries()) {
|
||||
const callId = functionCall.id ?? `${promptId}-${index}`;
|
||||
const { args, error: parseError } = this.parseToolArguments(functionCall);
|
||||
let { args, error: parseError } = this.parseToolArguments(functionCall);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
let effectiveToolName = functionCall.name as string;
|
||||
let effectiveArgs = args;
|
||||
let originalRequestName: string | undefined;
|
||||
let originalRequestArgs: Record<string, unknown> | undefined;
|
||||
|
||||
// Handle experimental dynamic tools
|
||||
if (
|
||||
!parseError &&
|
||||
this.context.config.getExperimentalDynamicTools()
|
||||
) {
|
||||
if (functionCall.name === 'execute') {
|
||||
const unwrapped = unwrapExecuteArgs(functionCall.args);
|
||||
if (unwrapped) {
|
||||
debugLogger.log(
|
||||
`[LocalAgentExecutor] Unwrapping execute call for tool: ${unwrapped.name}`,
|
||||
);
|
||||
effectiveToolName = unwrapped.name;
|
||||
effectiveArgs = unwrapped.args;
|
||||
args = unwrapped.args;
|
||||
originalRequestName = 'execute';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
originalRequestArgs = functionCall.args as Record<string, unknown>;
|
||||
} else {
|
||||
parseError =
|
||||
'Invalid execute arguments. Expected {name: string, args: object}';
|
||||
}
|
||||
} else if (functionCall.name) {
|
||||
// Model called a tool directly despite documentation.
|
||||
// Recovery: Wrap it as an execute call internally.
|
||||
debugLogger.log(
|
||||
`[LocalAgentExecutor] Model called tool '${functionCall.name}' directly. Wrapping as 'execute' for experiment compatibility.`,
|
||||
);
|
||||
|
||||
const wrapped = wrapAsExecute(functionCall.name, args);
|
||||
const unwrapped = unwrapExecuteArgs(wrapped.args); // Still unwrap to get resilient parameter mapping
|
||||
|
||||
if (unwrapped) {
|
||||
// IMPORTANT: Rewrite the original functionCall so history recording sees it as "execute"
|
||||
functionCall.name = wrapped.name;
|
||||
functionCall.args = wrapped.args;
|
||||
|
||||
effectiveToolName = unwrapped.name;
|
||||
effectiveArgs = unwrapped.args;
|
||||
args = unwrapped.args;
|
||||
originalRequestName = 'execute';
|
||||
originalRequestArgs = wrapped.args as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parseError) {
|
||||
debugLogger.warn(`[LocalAgentExecutor] ${parseError}`);
|
||||
@@ -1098,8 +1155,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const toolName = functionCall.name as string;
|
||||
const toolName = effectiveToolName;
|
||||
|
||||
let displayName = toolName;
|
||||
let description: string | undefined = undefined;
|
||||
@@ -1130,7 +1186,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
|
||||
syncResults.set(callId, {
|
||||
functionResponse: {
|
||||
name: toolName,
|
||||
name: functionCall.name,
|
||||
id: callId,
|
||||
response: { error },
|
||||
},
|
||||
@@ -1150,7 +1206,9 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
toolRequests.push({
|
||||
callId,
|
||||
name: toolName,
|
||||
args,
|
||||
args: effectiveArgs,
|
||||
originalRequestName,
|
||||
originalRequestArgs,
|
||||
isClientInitiated: false, // These are coming from the subagent (the "model")
|
||||
prompt_id: promptId,
|
||||
});
|
||||
@@ -1286,6 +1344,9 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
* Prepares the list of tool function declarations to be sent to the model.
|
||||
*/
|
||||
private prepareToolsList(): FunctionDeclaration[] {
|
||||
if (this.context.config.getExperimentalDynamicTools()) {
|
||||
return [EXECUTE_FUNCTION_DECLARATION];
|
||||
}
|
||||
const toolsList: FunctionDeclaration[] = [];
|
||||
const { toolConfig } = this.definition;
|
||||
|
||||
@@ -1342,6 +1403,14 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
const dirContext = await getDirectoryContextString(this.context.config);
|
||||
finalPrompt += `\n\n# Environment Context\n${dirContext}`;
|
||||
|
||||
if (this.context.config.getExperimentalDynamicTools()) {
|
||||
const toolDeclarations = this.toolRegistry.getFunctionDeclarations(
|
||||
this.definition.modelConfig.model,
|
||||
);
|
||||
const toolDocumentation = getDynamicToolsDocumentation(toolDeclarations);
|
||||
finalPrompt += `\n\n${toolDocumentation}`;
|
||||
}
|
||||
|
||||
// Append standard rules for non-interactive execution.
|
||||
finalPrompt += `
|
||||
Important Rules:
|
||||
|
||||
@@ -79,7 +79,6 @@ describe('LocalSubagentInvocation', () => {
|
||||
mockExecutorInstance = {
|
||||
run: vi.fn(),
|
||||
definition: testDefinition,
|
||||
agentId: 'test-agent-id',
|
||||
} as unknown as Mocked<LocalAgentExecutor<z.ZodUnknown>>;
|
||||
|
||||
MockLocalAgentExecutor.create.mockResolvedValue(
|
||||
@@ -187,10 +186,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
};
|
||||
mockExecutorInstance.run.mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: signal,
|
||||
updateOutput,
|
||||
});
|
||||
const result = await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(MockLocalAgentExecutor.create).toHaveBeenCalledWith(
|
||||
testDefinition,
|
||||
@@ -227,10 +223,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
};
|
||||
mockExecutorInstance.run.mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: signal,
|
||||
updateOutput,
|
||||
});
|
||||
const result = await invocation.execute(signal, updateOutput);
|
||||
|
||||
const display = result.returnDisplay as SubagentProgress;
|
||||
expect(display.isSubagentProgress).toBe(true);
|
||||
@@ -260,7 +253,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: signal, updateOutput });
|
||||
await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalledTimes(4); // Initial + 2 updates + Final completion
|
||||
const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
|
||||
@@ -299,7 +292,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: signal, updateOutput });
|
||||
await invocation.execute(signal, updateOutput);
|
||||
|
||||
const calls = updateOutput.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as SubagentProgress;
|
||||
@@ -332,7 +325,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: signal, updateOutput });
|
||||
await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalledTimes(4); // Initial + 2 updates + Final completion
|
||||
const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
|
||||
@@ -366,7 +359,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: signal, updateOutput });
|
||||
await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalled();
|
||||
const lastCall = updateOutput.mock.calls[
|
||||
@@ -410,7 +403,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
};
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: signal, updateOutput });
|
||||
await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalledTimes(4);
|
||||
const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
|
||||
@@ -439,7 +432,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
});
|
||||
|
||||
// Execute without the optional callback
|
||||
const result = await invocation.execute({ abortSignal: signal });
|
||||
const result = await invocation.execute(signal);
|
||||
expect(result.error).toBeUndefined();
|
||||
const display = result.returnDisplay as SubagentProgress;
|
||||
expect(display.isSubagentProgress).toBe(true);
|
||||
@@ -451,10 +444,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
const error = new Error('Model failed during execution.');
|
||||
mockExecutorInstance.run.mockRejectedValue(error);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: signal,
|
||||
updateOutput,
|
||||
});
|
||||
const result = await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.llmContent).toBe(
|
||||
@@ -475,10 +465,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
const creationError = new Error('Failed to initialize tools.');
|
||||
MockLocalAgentExecutor.create.mockRejectedValue(creationError);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: signal,
|
||||
updateOutput,
|
||||
});
|
||||
const result = await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(mockExecutorInstance.run).not.toHaveBeenCalled();
|
||||
expect(result.error).toBeUndefined();
|
||||
@@ -499,10 +486,10 @@ describe('LocalSubagentInvocation', () => {
|
||||
mockExecutorInstance.run.mockRejectedValue(abortError);
|
||||
|
||||
const controller = new AbortController();
|
||||
const executePromise = invocation.execute({
|
||||
abortSignal: controller.signal,
|
||||
const executePromise = invocation.execute(
|
||||
controller.signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
controller.abort();
|
||||
await expect(executePromise).rejects.toThrow('Aborted');
|
||||
|
||||
@@ -519,9 +506,9 @@ describe('LocalSubagentInvocation', () => {
|
||||
};
|
||||
mockExecutorInstance.run.mockResolvedValue(mockOutput);
|
||||
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: signal, updateOutput }),
|
||||
).rejects.toThrow('Operation cancelled by user');
|
||||
await expect(invocation.execute(signal, updateOutput)).rejects.toThrow(
|
||||
'Operation cancelled by user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish SUBAGENT_ACTIVITY events to the MessageBus', async () => {
|
||||
@@ -541,7 +528,7 @@ describe('LocalSubagentInvocation', () => {
|
||||
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: signal, updateOutput });
|
||||
await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LocalAgentExecutor } from './local-executor.js';
|
||||
import {
|
||||
BaseToolInvocation,
|
||||
type ToolResult,
|
||||
type ExecuteOptions,
|
||||
type ToolLiveOutput,
|
||||
} from '../tools/tools.js';
|
||||
import {
|
||||
type LocalAgentDefinition,
|
||||
@@ -25,14 +25,12 @@ import {
|
||||
isToolActivityError,
|
||||
} from './types.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { z } from 'zod';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import {
|
||||
sanitizeThoughtContent,
|
||||
sanitizeToolArgs,
|
||||
sanitizeErrorMessage,
|
||||
} from '../utils/agent-sanitization-utils.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
const INPUT_PREVIEW_MAX_LENGTH = 50;
|
||||
const DESCRIPTION_MAX_LENGTH = 200;
|
||||
@@ -105,10 +103,11 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
* agent's thoughts, to the user interface.
|
||||
* @returns A `Promise` that resolves with the final `ToolResult`.
|
||||
*/
|
||||
async execute(options: ExecuteOptions): Promise<ToolResult> {
|
||||
const { abortSignal: signal, updateOutput } = options;
|
||||
async execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: ToolLiveOutput) => void,
|
||||
): Promise<ToolResult> {
|
||||
const recentActivity: SubagentActivityItem[] = [];
|
||||
let executor: LocalAgentExecutor<z.ZodUnknown> | undefined;
|
||||
|
||||
try {
|
||||
if (updateOutput) {
|
||||
@@ -274,7 +273,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
}
|
||||
};
|
||||
|
||||
executor = await LocalAgentExecutor.create(
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
this.definition,
|
||||
this.context,
|
||||
onActivity,
|
||||
@@ -320,14 +319,11 @@ ${output.result}`;
|
||||
return {
|
||||
llmContent: [{ text: resultContent }],
|
||||
returnDisplay: progress,
|
||||
data: { agentId: executor.agentId },
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
debugLogger.error(`Subagent '${this.definition.name}' failed:`, error);
|
||||
|
||||
const isAbort =
|
||||
(error instanceof Error && error.name === 'AbortError') ||
|
||||
errorMessage.includes('Aborted');
|
||||
@@ -373,7 +369,6 @@ ${output.result}`;
|
||||
return {
|
||||
llmContent: `Subagent '${this.definition.name}' failed. Error: ${errorMessage}`,
|
||||
returnDisplay: progress,
|
||||
data: executor ? { agentId: executor.agentId } : undefined,
|
||||
// We omit the 'error' property so that the UI renders our rich returnDisplay
|
||||
// instead of the raw error message. The llmContent still informs the agent of the failure.
|
||||
};
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{},
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockClientManager.sendMessageStream).toHaveBeenCalledWith(
|
||||
'test-agent',
|
||||
@@ -185,7 +185,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockClientManager.loadAgent).toHaveBeenCalledWith(
|
||||
'test-agent',
|
||||
@@ -230,7 +230,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(A2AAuthProviderFactory.create).toHaveBeenCalledWith({
|
||||
authConfig: mockAuth,
|
||||
@@ -264,9 +264,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
@@ -295,7 +293,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockClientManager.loadAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -327,9 +325,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
);
|
||||
|
||||
// Execute first time
|
||||
const result1 = await invocation1.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result1 = await invocation1.execute(new AbortController().signal);
|
||||
expect(result1.returnDisplay).toMatchObject({
|
||||
result: 'Response 1',
|
||||
});
|
||||
@@ -361,9 +357,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
const result2 = await invocation2.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result2 = await invocation2.execute(new AbortController().signal);
|
||||
expect((result2.returnDisplay as SubagentProgress).result).toBe(
|
||||
'Response 2',
|
||||
);
|
||||
@@ -396,7 +390,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation3.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation3.execute(new AbortController().signal);
|
||||
|
||||
// Fourth call: Should start new task (taskId undefined)
|
||||
mockClientManager.sendMessageStream.mockImplementationOnce(
|
||||
@@ -418,7 +412,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation4.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation4.execute(new AbortController().signal);
|
||||
|
||||
expect(mockClientManager.sendMessageStream).toHaveBeenLastCalledWith(
|
||||
'test-agent',
|
||||
@@ -453,10 +447,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, updateOutput);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -504,9 +495,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
const result = await invocation.execute(controller.signal);
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
});
|
||||
@@ -528,9 +517,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({
|
||||
state: 'error',
|
||||
@@ -563,9 +550,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
},
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Just check that text is present, exact formatting depends on helper
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
@@ -608,10 +593,10 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
const result = await invocation.execute(
|
||||
new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -685,10 +670,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
updateOutput,
|
||||
});
|
||||
await invocation.execute(new AbortController().signal, updateOutput);
|
||||
|
||||
expect(updateOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -756,9 +738,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
@@ -778,9 +758,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||
@@ -809,9 +787,7 @@ describe('RemoteAgentInvocation', () => {
|
||||
{ query: 'hi' },
|
||||
mockMessageBus,
|
||||
);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||
// Should contain both the partial output and the error message
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type ToolConfirmationOutcome,
|
||||
type ToolResult,
|
||||
type ToolCallConfirmationDetails,
|
||||
type ExecuteOptions,
|
||||
} from '../tools/tools.js';
|
||||
import {
|
||||
DEFAULT_QUERY_STRING,
|
||||
@@ -29,6 +28,7 @@ import type {
|
||||
import { extractIdsFromResponse, A2AResultReassembler } from './a2aUtils.js';
|
||||
import type { AuthenticationHandler } from '@a2a-js/sdk/client';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import type { AnsiOutput } from '../utils/terminalSerializer.js';
|
||||
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
|
||||
import { A2AAgentError } from './a2a-errors.js';
|
||||
|
||||
@@ -126,8 +126,10 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
||||
};
|
||||
}
|
||||
|
||||
async execute(options: ExecuteOptions): Promise<ToolResult> {
|
||||
const { abortSignal: _signal, updateOutput } = options;
|
||||
async execute(
|
||||
_signal: AbortSignal,
|
||||
updateOutput?: (output: string | AnsiOutput | SubagentProgress) => void,
|
||||
): Promise<ToolResult> {
|
||||
// 1. Ensure the agent is loaded (cached by manager)
|
||||
// We assume the user has provided an access token via some mechanism (TODO),
|
||||
// or we rely on ADC.
|
||||
|
||||
@@ -304,53 +304,6 @@ describe('Server Config (config.ts)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setShellExecutionConfig', () => {
|
||||
it('should preserve existing shell execution fields that are not being updated', () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
command: 'windows-native',
|
||||
networkAccess: false,
|
||||
},
|
||||
shellBackgroundCompletionBehavior: 'notify',
|
||||
});
|
||||
|
||||
expect(config.getShellExecutionConfig()).toEqual(
|
||||
expect.objectContaining({
|
||||
sandboxConfig: expect.objectContaining({
|
||||
enabled: true,
|
||||
command: 'windows-native',
|
||||
networkAccess: false,
|
||||
}),
|
||||
backgroundCompletionBehavior: 'notify',
|
||||
}),
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
terminalWidth: 123,
|
||||
terminalHeight: 45,
|
||||
showColor: true,
|
||||
pager: 'cat',
|
||||
sanitizationConfig: config.sanitizationConfig,
|
||||
sandboxManager: config.sandboxManager,
|
||||
});
|
||||
|
||||
expect(config.getShellExecutionConfig()).toEqual(
|
||||
expect.objectContaining({
|
||||
terminalWidth: 123,
|
||||
terminalHeight: 45,
|
||||
sandboxConfig: expect.objectContaining({
|
||||
enabled: true,
|
||||
command: 'windows-native',
|
||||
networkAccess: false,
|
||||
}),
|
||||
backgroundCompletionBehavior: 'notify',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks if necessary
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -699,6 +699,7 @@ export interface ConfigParameters {
|
||||
experimentalJitContext?: boolean;
|
||||
autoDistillation?: boolean;
|
||||
experimentalMemoryManager?: boolean;
|
||||
experimentalDynamicTools?: boolean;
|
||||
experimentalAgentHistoryTruncation?: boolean;
|
||||
experimentalAgentHistoryTruncationThreshold?: number;
|
||||
experimentalAgentHistoryRetainedMessages?: number;
|
||||
@@ -941,6 +942,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly experimentalMemoryManager: boolean;
|
||||
private readonly experimentalDynamicTools: boolean;
|
||||
private readonly memoryBoundaryMarkers: readonly string[];
|
||||
private readonly topicUpdateNarration: boolean;
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
@@ -1152,6 +1154,12 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
|
||||
this.experimentalDynamicTools =
|
||||
params.experimentalDynamicTools === true ||
|
||||
process.env['GEMINI_CLI_EXP_DYNAMIC_TOOLS'] === 'true';
|
||||
if (this.experimentalDynamicTools) {
|
||||
debugLogger.log('[Config] Experimental Dynamic Tools enabled.');
|
||||
}
|
||||
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
|
||||
this.contextManagement = {
|
||||
enabled: params.contextManagement?.enabled ?? false,
|
||||
@@ -2426,6 +2434,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.experimentalMemoryManager;
|
||||
}
|
||||
|
||||
getExperimentalDynamicTools(): boolean {
|
||||
return this.experimentalDynamicTools;
|
||||
}
|
||||
|
||||
getContextManagementConfig(): ContextManagementConfig {
|
||||
return this.contextManagement;
|
||||
}
|
||||
@@ -3334,7 +3346,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
setShellExecutionConfig(config: ShellExecutionConfig): void {
|
||||
this.shellExecutionConfig = {
|
||||
...this.shellExecutionConfig,
|
||||
terminalWidth:
|
||||
config.terminalWidth ?? this.shellExecutionConfig.terminalWidth,
|
||||
terminalHeight:
|
||||
|
||||
@@ -762,11 +762,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -913,11 +908,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -1046,11 +1036,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -1703,11 +1688,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -1867,11 +1847,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -2035,11 +2010,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -2203,11 +2173,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -2367,11 +2332,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -2533,11 +2493,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -2658,11 +2613,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -2821,11 +2771,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -3112,11 +3057,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -3533,11 +3473,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -3697,11 +3632,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -3975,11 +3905,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
@@ -4139,11 +4064,6 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import type { Config } from '../config/config.js';
|
||||
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import { getCoreSystemPrompt } from './prompts.js';
|
||||
import { EXECUTE_FUNCTION_DECLARATION } from '../utils/dynamicToolsUtils.js';
|
||||
import { checkNextSpeaker } from '../utils/nextSpeakerChecker.js';
|
||||
import { reportError } from '../utils/errorReporting.js';
|
||||
import { GeminiChat } from './geminiChat.js';
|
||||
@@ -301,12 +302,20 @@ export class GeminiClient {
|
||||
}
|
||||
this.lastUsedModelId = modelId;
|
||||
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const toolDeclarations = toolRegistry.getFunctionDeclarations(modelId);
|
||||
const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
|
||||
const tools = this.getChatTools(modelId);
|
||||
this.getChat().setTools(tools);
|
||||
}
|
||||
|
||||
private getChatTools(modelId?: string): Tool[] {
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
if (this.config.getExperimentalDynamicTools()) {
|
||||
debugLogger.log('[GeminiClient] Experimental Dynamic Tools enabled.');
|
||||
return [{ functionDeclarations: [EXECUTE_FUNCTION_DECLARATION] }];
|
||||
}
|
||||
const toolDeclarations = toolRegistry.getFunctionDeclarations(modelId);
|
||||
return [{ functionDeclarations: toolDeclarations }];
|
||||
}
|
||||
|
||||
async resetChat(): Promise<void> {
|
||||
this.chat = await this.startChat();
|
||||
this.updateTelemetryTokenCount();
|
||||
@@ -369,9 +378,7 @@ export class GeminiClient {
|
||||
this.hasFailedCompressionAttempt = false;
|
||||
this.lastUsedModelId = undefined;
|
||||
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const toolDeclarations = toolRegistry.getFunctionDeclarations();
|
||||
const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
|
||||
const tools = this.getChatTools();
|
||||
|
||||
const history = await getInitialChatHistory(this.config, extraHistory);
|
||||
|
||||
@@ -386,10 +393,7 @@ export class GeminiClient {
|
||||
resumedSessionData,
|
||||
async (modelId: string) => {
|
||||
this.lastUsedModelId = modelId;
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const toolDeclarations =
|
||||
toolRegistry.getFunctionDeclarations(modelId);
|
||||
return [{ functionDeclarations: toolDeclarations }];
|
||||
return this.getChatTools(modelId);
|
||||
},
|
||||
);
|
||||
await chat.initialize(resumedSessionData, 'main');
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
BaseToolInvocation,
|
||||
type ToolResult,
|
||||
type AnyDeclarativeTool,
|
||||
type ExecuteOptions,
|
||||
type ToolLiveOutput,
|
||||
} from '../tools/tools.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { HookSystem } from '../hooks/hookSystem.js';
|
||||
@@ -46,7 +46,11 @@ class MockBackgroundableInvocation extends BaseToolInvocation<
|
||||
getDescription() {
|
||||
return 'mock-pid';
|
||||
}
|
||||
async execute(options: ExecuteOptions) {
|
||||
async execute(
|
||||
_signal: AbortSignal,
|
||||
_updateOutput?: (output: ToolLiveOutput) => void,
|
||||
options?: { setExecutionIdCallback?: (executionId: number) => void },
|
||||
) {
|
||||
options?.setExecutionIdCallback?.(4242);
|
||||
return {
|
||||
llmContent: 'pid',
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function executeToolWithHooks(
|
||||
signal: AbortSignal,
|
||||
tool: AnyDeclarativeTool,
|
||||
liveOutputCallback?: (outputChunk: ToolLiveOutput) => void,
|
||||
options?: Omit<ExecuteOptions, 'abortSignal' | 'updateOutput'>,
|
||||
options?: ExecuteOptions,
|
||||
config?: Config,
|
||||
originalRequestName?: string,
|
||||
skipBeforeHook?: boolean,
|
||||
@@ -154,11 +154,11 @@ export async function executeToolWithHooks(
|
||||
|
||||
// Execute the actual tool. Tools that support backgrounding can optionally
|
||||
// surface an execution ID via the callback.
|
||||
const toolResult: ToolResult = await invocation.execute({
|
||||
...options,
|
||||
abortSignal: signal,
|
||||
updateOutput: liveOutputCallback,
|
||||
});
|
||||
const toolResult: ToolResult = await invocation.execute(
|
||||
signal,
|
||||
liveOutputCallback,
|
||||
options,
|
||||
);
|
||||
|
||||
// Append notification if parameters were modified
|
||||
if (inputWasModified) {
|
||||
|
||||
@@ -49,12 +49,14 @@ import { isFunctionResponse } from '../utils/messageInspectors.js';
|
||||
import { partListUnionToString } from './geminiRequest.js';
|
||||
import type { ModelConfigKey } from '../services/modelConfigService.js';
|
||||
import { estimateTokenCountSync } from '../utils/tokenCalculation.js';
|
||||
import { wrapAsExecute } from '../utils/dynamicToolsUtils.js';
|
||||
import {
|
||||
applyModelSelection,
|
||||
createAvailabilityContextProvider,
|
||||
} from '../availability/policyHelpers.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
export enum StreamEventType {
|
||||
/** A regular content chunk from the API. */
|
||||
@@ -897,11 +899,30 @@ export class GeminiChat {
|
||||
if (isValidResponse(chunk)) {
|
||||
const content = chunk.candidates?.[0]?.content;
|
||||
if (content?.parts) {
|
||||
if (content.parts.some((part) => part.thought)) {
|
||||
// Record thoughts
|
||||
hasThoughts = true;
|
||||
this.recordThoughtFromContent(content);
|
||||
for (const part of content.parts) {
|
||||
if (part.thought) {
|
||||
// Record thoughts
|
||||
hasThoughts = true;
|
||||
this.recordThoughtFromContent(content);
|
||||
}
|
||||
if (
|
||||
part.functionCall &&
|
||||
part.functionCall.name &&
|
||||
part.functionCall.name !== 'execute' &&
|
||||
this.context.config.getExperimentalDynamicTools()
|
||||
) {
|
||||
debugLogger.log(
|
||||
`[GeminiChat] Rewriting hallucinated tool call '${part.functionCall.name}' to 'execute' wrapper.`,
|
||||
);
|
||||
const wrapped = wrapAsExecute(
|
||||
part.functionCall.name,
|
||||
part.functionCall.args,
|
||||
);
|
||||
part.functionCall.name = wrapped.name;
|
||||
part.functionCall.args = wrapped.args;
|
||||
}
|
||||
}
|
||||
|
||||
if (content.parts.some((part) => part.functionCall)) {
|
||||
hasToolCall = true;
|
||||
}
|
||||
@@ -1050,13 +1071,11 @@ export class GeminiChat {
|
||||
result: call.response?.responseParts || null,
|
||||
status: call.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
agentId:
|
||||
typeof call.response?.data?.['agentId'] === 'string'
|
||||
? call.response.data['agentId']
|
||||
: undefined,
|
||||
resultDisplay,
|
||||
description:
|
||||
'invocation' in call ? call.invocation?.getDescription() : undefined,
|
||||
originalRequestName: call.request.originalRequestName,
|
||||
originalRequestArgs: call.request.originalRequestArgs,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ import { parseThought, type ThoughtSummary } from '../utils/thoughtUtils.js';
|
||||
import type { ModelConfigKey } from '../services/modelConfigService.js';
|
||||
import { getCitations } from '../utils/generateContentResponseUtilities.js';
|
||||
import { LlmRole } from '../telemetry/types.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import {
|
||||
unwrapExecuteArgs,
|
||||
wrapAsExecute,
|
||||
} from '../utils/dynamicToolsUtils.js';
|
||||
|
||||
import {
|
||||
type ToolCallRequestInfo,
|
||||
@@ -237,6 +242,7 @@ export type ServerGeminiStreamEvent =
|
||||
// A turn manages the agentic loop turn within the server context.
|
||||
export class Turn {
|
||||
private callCounter = 0;
|
||||
private handledCallIds = new Set<string>();
|
||||
|
||||
readonly pendingToolCalls: ToolCallRequestInfo[] = [];
|
||||
private debugResponses: GenerateContentResponse[] = [];
|
||||
@@ -409,7 +415,74 @@ export class Turn {
|
||||
): ServerGeminiStreamEvent | null {
|
||||
const name = fnCall.name || 'undefined_tool_name';
|
||||
const args = fnCall.args || {};
|
||||
const callId = fnCall.id ?? `${name}_${Date.now()}_${this.callCounter++}`;
|
||||
const callId = fnCall.id || `${name}_${Date.now()}_${this.callCounter++}`;
|
||||
|
||||
if (this.handledCallIds.has(callId)) {
|
||||
return null;
|
||||
}
|
||||
this.handledCallIds.add(callId);
|
||||
|
||||
// Handle experimental dynamic tools
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
|
||||
(this.chat as any).context.config.getExperimentalDynamicTools()
|
||||
) {
|
||||
if (name === 'execute') {
|
||||
const unwrapped = unwrapExecuteArgs(fnCall.args);
|
||||
if (unwrapped) {
|
||||
debugLogger.log(
|
||||
`[Turn] Unwrapping execute call for tool: ${unwrapped.name}`,
|
||||
);
|
||||
|
||||
const toolCallRequest: ToolCallRequestInfo = {
|
||||
callId,
|
||||
name: unwrapped.name,
|
||||
args: unwrapped.args,
|
||||
originalRequestName: 'execute',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
|
||||
originalRequestArgs: fnCall.args as any,
|
||||
isClientInitiated: false,
|
||||
prompt_id: this.prompt_id,
|
||||
traceId,
|
||||
};
|
||||
|
||||
this.pendingToolCalls.push(toolCallRequest);
|
||||
return {
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
value: toolCallRequest,
|
||||
};
|
||||
}
|
||||
} else if (name !== 'undefined_tool_name') {
|
||||
// Model called a tool directly despite documentation.
|
||||
// It should have been wrapped by GeminiChat.makeApiCallAndProcessStream,
|
||||
// but as a failsafe we wrap it here too.
|
||||
debugLogger.log(
|
||||
`[Turn] Model called tool '${name}' directly. Recovery wrapping as 'execute'.`,
|
||||
);
|
||||
|
||||
const unwrapped = unwrapExecuteArgs(wrapAsExecute(name, args).args);
|
||||
|
||||
if (unwrapped) {
|
||||
const toolCallRequest: ToolCallRequestInfo = {
|
||||
callId,
|
||||
name: unwrapped.name,
|
||||
args: unwrapped.args,
|
||||
originalRequestName: 'execute',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
|
||||
originalRequestArgs: wrapAsExecute(name, args).args as any,
|
||||
isClientInitiated: false,
|
||||
prompt_id: this.prompt_id,
|
||||
traceId,
|
||||
};
|
||||
|
||||
this.pendingToolCalls.push(toolCallRequest);
|
||||
return {
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
value: toolCallRequest,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolCallRequest: ToolCallRequestInfo = {
|
||||
callId,
|
||||
@@ -426,15 +499,6 @@ export class Turn {
|
||||
return { type: GeminiEventType.ToolCallRequest, value: toolCallRequest };
|
||||
}
|
||||
|
||||
getDebugResponses(): GenerateContentResponse[] {
|
||||
return this.debugResponses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the concatenated response text from all responses in this turn.
|
||||
* This extracts and joins all text content from the model's responses.
|
||||
* The result is cached since this is called multiple times per turn.
|
||||
*/
|
||||
getResponseText(): string {
|
||||
if (this.cachedResponseText === undefined) {
|
||||
this.cachedResponseText = this.debugResponses
|
||||
@@ -444,4 +508,8 @@ export class Turn {
|
||||
}
|
||||
return this.cachedResponseText;
|
||||
}
|
||||
|
||||
getDebugResponses(): GenerateContentResponse[] {
|
||||
return this.debugResponses;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ export {
|
||||
PRIORITY_YOLO_ALLOW_ALL,
|
||||
} from './policy/types.js';
|
||||
export * from './utils/tool-utils.js';
|
||||
export * from './utils/tool-visibility.js';
|
||||
export * from './utils/terminalSerializer.js';
|
||||
export * from './utils/systemEncoding.js';
|
||||
export * from './utils/textUtils.js';
|
||||
|
||||
@@ -32,6 +32,9 @@ import { resolveModel, supportsModernFeatures } from '../config/models.js';
|
||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||
import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import {
|
||||
getDynamicToolsDocumentation,
|
||||
} from '../utils/dynamicToolsUtils.js';
|
||||
|
||||
/**
|
||||
* Orchestrates prompt generation by gathering context and building options.
|
||||
@@ -96,6 +99,13 @@ export class PromptProvider {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
let dynamicToolsDocumentation: string | undefined;
|
||||
if (context.config.getExperimentalDynamicTools()) {
|
||||
dynamicToolsDocumentation = getDynamicToolsDocumentation(
|
||||
context.toolRegistry.getFunctionDeclarations(),
|
||||
);
|
||||
}
|
||||
|
||||
let basePrompt: string;
|
||||
|
||||
// --- Template File Override ---
|
||||
@@ -141,6 +151,7 @@ export class PromptProvider {
|
||||
contextFilenames,
|
||||
topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(),
|
||||
})),
|
||||
dynamicToolsDocumentation,
|
||||
subAgents: this.withSection(
|
||||
'agentContexts',
|
||||
() =>
|
||||
@@ -282,6 +293,7 @@ export class PromptProvider {
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
|
||||
return activeSnippets.getCompressionPrompt(
|
||||
context.config.getApprovedPlanPath(),
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js';
|
||||
export interface SystemPromptOptions {
|
||||
preamble?: PreambleOptions;
|
||||
coreMandates?: CoreMandatesOptions;
|
||||
dynamicToolsDocumentation?: string;
|
||||
subAgents?: SubAgentOptions[];
|
||||
agentSkills?: AgentSkillOptions[];
|
||||
hookContext?: boolean;
|
||||
@@ -127,6 +128,8 @@ ${renderPreamble(options.preamble)}
|
||||
|
||||
${renderCoreMandates(options.coreMandates)}
|
||||
|
||||
${options.dynamicToolsDocumentation ? `\n<tools>\n${options.dynamicToolsDocumentation}\n</tools>\n` : ''}
|
||||
|
||||
${renderSubAgents(options.subAgents)}
|
||||
|
||||
${renderAgentSkills(options.agentSkills)}
|
||||
@@ -347,11 +350,6 @@ ${workflowStepStrategy(options)}
|
||||
|
||||
**Validation is the only path to finality.** Never assume success or settle for unverified changes. Rigorous, exhaustive verification is mandatory; it prevents the compounding cost of diagnosing failures later. A task is only complete when the behavioral correctness of the change has been verified and its structural integrity is confirmed within the full project context. Prioritize comprehensive validation above all else, utilizing redirection and focused analysis to manage high-output tasks without sacrificing depth. Never sacrifice validation rigor for the sake of brevity or to minimize tool-call overhead; partial or isolated checks are insufficient when more comprehensive validation is possible.
|
||||
|
||||
**Strategic Re-evaluation:** If you have attempted to fix a failing implementation more than 3 times without success, you must:
|
||||
1. Stop and remind yourself of the original task description.
|
||||
2. List your current assumptions and identify which ones might be wrong.
|
||||
3. Propose a different architectural approach rather than continuing to patch the current one.
|
||||
|
||||
## New Applications
|
||||
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
|
||||
@@ -20,20 +20,12 @@ using System.Text;
|
||||
* It also supports internal commands for safe file I/O within the sandbox.
|
||||
*/
|
||||
public class GeminiSandbox {
|
||||
// --- P/Invoke Constants and Structures ---
|
||||
// P/Invoke constants and structures
|
||||
private const int JobObjectExtendedLimitInformation = 9;
|
||||
private const int JobObjectNetRateControlInformation = 32;
|
||||
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||
private const uint JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400;
|
||||
private const uint JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008;
|
||||
|
||||
private const int TokenIntegrityLevel = 25;
|
||||
private const uint SE_GROUP_INTEGRITY = 0x00000020;
|
||||
private const uint TOKEN_ALL_ACCESS = 0xF01FF;
|
||||
private const uint DISABLE_MAX_PRIVILEGE = 0x1;
|
||||
|
||||
private const int SE_FILE_OBJECT = 1;
|
||||
private const uint LABEL_SECURITY_INFORMATION = 0x00000010;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
|
||||
@@ -75,6 +67,39 @@ public class GeminiSandbox {
|
||||
public byte DscpTag;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool SetInformationJobObject(IntPtr hJob, int JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern uint ResumeThread(IntPtr hThread);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, uint ImpersonationLevel, uint TokenType, out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct STARTUPINFO {
|
||||
public uint cb;
|
||||
@@ -105,6 +130,21 @@ public class GeminiSandbox {
|
||||
public uint dwThreadId;
|
||||
}
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool ImpersonateLoggedOnUser(IntPtr hToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool RevertToSelf();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern uint GetLongPathName(string lpszShortPath, [Out] StringBuilder lpszLongPath, uint cchBuffer);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern bool ConvertStringSidToSid(string StringSid, out IntPtr ptrSid);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool SetTokenInformation(IntPtr TokenHandle, int TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct SID_AND_ATTRIBUTES {
|
||||
public IntPtr Sid;
|
||||
@@ -116,81 +156,14 @@ public class GeminiSandbox {
|
||||
public SID_AND_ATTRIBUTES Label;
|
||||
}
|
||||
|
||||
// --- Kernel32 P/Invokes ---
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
||||
private const int TokenIntegrityLevel = 25;
|
||||
private const uint SE_GROUP_INTEGRITY = 0x00000020;
|
||||
private const uint TOKEN_ALL_ACCESS = 0xF01FF;
|
||||
private const uint DISABLE_MAX_PRIVILEGE = 0x1;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool SetInformationJobObject(IntPtr hJob, int JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern uint ResumeThread(IntPtr hThread);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern uint GetLongPathName(string lpszShortPath, [Out] StringBuilder lpszLongPath, uint cchBuffer);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr LocalFree(IntPtr hMem);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
||||
|
||||
// --- Advapi32 P/Invokes ---
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, uint ImpersonationLevel, uint TokenType, out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool ImpersonateLoggedOnUser(IntPtr hToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool RevertToSelf();
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern bool ConvertStringSidToSid(string StringSid, out IntPtr ptrSid);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool SetTokenInformation(IntPtr TokenHandle, int TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(string StringSecurityDescriptor, uint StringSDRevision, out IntPtr SecurityDescriptor, out uint SecurityDescriptorSize);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern uint SetNamedSecurityInfo(string pObjectName, int ObjectType, uint SecurityInfo, IntPtr psidOwner, IntPtr psidGroup, IntPtr pDacl, IntPtr pSacl);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool GetSecurityDescriptorSacl(IntPtr pSecurityDescriptor, out bool lpbSaclPresent, out IntPtr pSacl, out bool lpbSaclDefaulted);
|
||||
|
||||
// --- Main Entry Point ---
|
||||
static int Main(string[] args) {
|
||||
if (args.Length < 3) {
|
||||
Console.Error.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> [--forbidden-manifest <path>] [--allowed-manifest <path>] <command> [args...]");
|
||||
Console.Error.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> [--forbidden-manifest <path>] <command> [args...]");
|
||||
Console.Error.WriteLine("Internal commands: __read <path>, __write <path>");
|
||||
return 1;
|
||||
}
|
||||
@@ -198,32 +171,21 @@ public class GeminiSandbox {
|
||||
bool networkAccess = args[0] == "1";
|
||||
string cwd = args[1];
|
||||
HashSet<string> forbiddenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
HashSet<string> allowedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
int argIndex = 2;
|
||||
|
||||
// 1. Parse Command Line Arguments & Manifests
|
||||
while (argIndex < args.Length) {
|
||||
if (args[argIndex] == "--forbidden-manifest") {
|
||||
if (argIndex + 1 < args.Length) {
|
||||
ParseManifest(args[argIndex + 1], forbiddenPaths);
|
||||
argIndex += 2;
|
||||
} else {
|
||||
break;
|
||||
if (argIndex < args.Length && args[argIndex] == "--forbidden-manifest") {
|
||||
if (argIndex + 1 < args.Length) {
|
||||
string manifestPath = args[argIndex + 1];
|
||||
if (File.Exists(manifestPath)) {
|
||||
foreach (string line in File.ReadAllLines(manifestPath)) {
|
||||
if (!string.IsNullOrWhiteSpace(line)) {
|
||||
forbiddenPaths.Add(GetNormalizedPath(line.Trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (args[argIndex] == "--allowed-manifest") {
|
||||
if (argIndex + 1 < args.Length) {
|
||||
ParseManifest(args[argIndex + 1], allowedPaths);
|
||||
argIndex += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
argIndex += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Apply Bulk ACLs
|
||||
ApplyBulkAcls(allowedPaths, forbiddenPaths);
|
||||
|
||||
if (argIndex >= args.Length) {
|
||||
Console.Error.WriteLine("Error: Missing command");
|
||||
@@ -238,18 +200,20 @@ public class GeminiSandbox {
|
||||
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
||||
|
||||
try {
|
||||
// 3. Duplicate Primary Token and Create Restricted Token
|
||||
// 1. Duplicate Primary Token
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out hToken)) {
|
||||
Console.Error.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create a restricted token to strip administrative privileges
|
||||
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, 0, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, out hRestrictedToken)) {
|
||||
Console.Error.WriteLine("Error: CreateRestrictedToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 4. Lower Integrity Level to "Low" (S-1-16-4096)
|
||||
// 2. Lower Integrity Level to Low
|
||||
// S-1-16-4096 is the SID for "Low Mandatory Level"
|
||||
IntPtr lowIntegritySid = IntPtr.Zero;
|
||||
if (ConvertStringSidToSid("S-1-16-4096", out lowIntegritySid)) {
|
||||
TOKEN_MANDATORY_LABEL tml = new TOKEN_MANDATORY_LABEL();
|
||||
@@ -268,7 +232,7 @@ public class GeminiSandbox {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Setup Job Object
|
||||
// 3. Setup Job Object for cleanup
|
||||
hJob = CreateJobObject(IntPtr.Zero, null);
|
||||
if (hJob == IntPtr.Zero) {
|
||||
Console.Error.WriteLine("Error: CreateJobObject failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
@@ -299,6 +263,7 @@ public class GeminiSandbox {
|
||||
try {
|
||||
Marshal.StructureToPtr(netLimits, lpNetLimits, false);
|
||||
if (!SetInformationJobObject(hJob, JobObjectNetRateControlInformation, lpNetLimits, (uint)Marshal.SizeOf(netLimits))) {
|
||||
// Some versions of Windows might not support network rate control, but we should know if it fails.
|
||||
Console.Error.WriteLine("Warning: SetInformationJobObject(NetRate) failed (" + Marshal.GetLastWin32Error() + "). Network might not be throttled.");
|
||||
}
|
||||
} finally {
|
||||
@@ -306,7 +271,7 @@ public class GeminiSandbox {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Handle Internal Commands or External Process
|
||||
// 4. Handle Internal Commands or External Process
|
||||
if (command == "__read") {
|
||||
if (argIndex + 1 >= args.Length) {
|
||||
Console.Error.WriteLine("Error: Missing path for __read");
|
||||
@@ -336,6 +301,7 @@ public class GeminiSandbox {
|
||||
|
||||
try {
|
||||
using (MemoryStream ms = new MemoryStream()) {
|
||||
// Buffer stdin before impersonation (as restricted token can't read the inherited pipe).
|
||||
using (Stream stdin = Console.OpenStandardInput()) {
|
||||
stdin.CopyTo(ms);
|
||||
}
|
||||
@@ -354,7 +320,7 @@ public class GeminiSandbox {
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Execute External Process
|
||||
// External Process
|
||||
STARTUPINFO si = new STARTUPINFO();
|
||||
si.cb = (uint)Marshal.SizeOf(si);
|
||||
si.dwFlags = 0x00000100; // STARTF_USESTDHANDLES
|
||||
@@ -408,89 +374,14 @@ public class GeminiSandbox {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Methods ---
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
||||
|
||||
private static void ParseManifest(string manifestPath, HashSet<string> paths) {
|
||||
if (!File.Exists(manifestPath)) return;
|
||||
foreach (string line in File.ReadAllLines(manifestPath, Encoding.UTF8)) {
|
||||
if (!string.IsNullOrWhiteSpace(line)) {
|
||||
paths.Add(GetNormalizedPath(line.Trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
private static void ApplyBulkAcls(HashSet<string> allowedPaths, HashSet<string> forbiddenPaths) {
|
||||
SecurityIdentifier lowSid = new SecurityIdentifier("S-1-16-4096");
|
||||
|
||||
// 1. Apply Deny Rules
|
||||
foreach (string path in forbiddenPaths) {
|
||||
try {
|
||||
if (File.Exists(path)) {
|
||||
FileSecurity fs = File.GetAccessControl(path);
|
||||
fs.AddAccessRule(new FileSystemAccessRule(lowSid, FileSystemRights.FullControl, AccessControlType.Deny));
|
||||
File.SetAccessControl(path, fs);
|
||||
} else if (Directory.Exists(path)) {
|
||||
DirectorySecurity ds = Directory.GetAccessControl(path);
|
||||
ds.AddAccessRule(new FileSystemAccessRule(lowSid, FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Deny));
|
||||
Directory.SetAccessControl(path, ds);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Console.Error.WriteLine("Warning: Failed to apply deny ACL to " + path + ": " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Pre-calculate Security Descriptors for Allow Rules
|
||||
IntPtr pSdDir = IntPtr.Zero;
|
||||
IntPtr pSdFile = IntPtr.Zero;
|
||||
IntPtr pSaclDir = IntPtr.Zero;
|
||||
IntPtr pSaclFile = IntPtr.Zero;
|
||||
uint sdSize = 0;
|
||||
bool saclPresent = false;
|
||||
bool saclDefaulted = false;
|
||||
|
||||
if (ConvertStringSecurityDescriptorToSecurityDescriptor("S:(ML;OICI;NW;;;LW)", 1, out pSdDir, out sdSize)) {
|
||||
GetSecurityDescriptorSacl(pSdDir, out saclPresent, out pSaclDir, out saclDefaulted);
|
||||
}
|
||||
if (ConvertStringSecurityDescriptorToSecurityDescriptor("S:(ML;;NW;;;LW)", 1, out pSdFile, out sdSize)) {
|
||||
GetSecurityDescriptorSacl(pSdFile, out saclPresent, out pSaclFile, out saclDefaulted);
|
||||
}
|
||||
|
||||
// 3. Apply Allow Rules
|
||||
foreach (string path in allowedPaths) {
|
||||
try {
|
||||
bool isDir = Directory.Exists(path);
|
||||
if (isDir) {
|
||||
DirectorySecurity ds = Directory.GetAccessControl(path);
|
||||
ds.AddAccessRule(new FileSystemAccessRule(lowSid, FileSystemRights.Modify, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
|
||||
Directory.SetAccessControl(path, ds);
|
||||
} else if (File.Exists(path)) {
|
||||
FileSecurity fs = File.GetAccessControl(path);
|
||||
fs.AddAccessRule(new FileSystemAccessRule(lowSid, FileSystemRights.Modify, AccessControlType.Allow));
|
||||
File.SetAccessControl(path, fs);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure we use the 8.3 long-name equivalent for robust security checks per guidelines
|
||||
StringBuilder sb = new StringBuilder(1024);
|
||||
GetLongPathName(path, sb, 1024);
|
||||
string longPath = sb.ToString();
|
||||
|
||||
IntPtr pSacl = isDir ? pSaclDir : pSaclFile;
|
||||
if (pSacl != IntPtr.Zero) {
|
||||
uint result = SetNamedSecurityInfo(longPath, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, pSacl);
|
||||
if (result != 0) {
|
||||
Console.Error.WriteLine("Warning: SetNamedSecurityInfo failed for " + longPath + " with error " + result);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Console.Error.WriteLine("Warning: Failed to apply allow ACL to " + path + ": " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
if (pSdDir != IntPtr.Zero) LocalFree(pSdDir);
|
||||
if (pSdFile != IntPtr.Zero) LocalFree(pSdFile);
|
||||
}
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
||||
|
||||
private static int RunInImpersonation(IntPtr hToken, Func<int> action) {
|
||||
if (!ImpersonateLoggedOnUser(hToken)) {
|
||||
@@ -565,4 +456,4 @@ public class GeminiSandbox {
|
||||
sb.Append('\"');
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { WindowsSandboxManager } from './WindowsSandboxManager.js';
|
||||
import * as sandboxManager from '../../services/sandboxManager.js';
|
||||
import * as paths from '../../utils/paths.js';
|
||||
import type { SandboxRequest } from '../../services/sandboxManager.js';
|
||||
import { spawnAsync } from '../../utils/shell-utils.js';
|
||||
import type { SandboxPolicyManager } from '../../policy/sandboxPolicyManager.js';
|
||||
|
||||
vi.mock('../../utils/shell-utils.js', async (importOriginal) => {
|
||||
@@ -42,26 +43,6 @@ describe('WindowsSandboxManager', () => {
|
||||
WindowsSandboxManager.HELPER_EXE,
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to read manifests from sandbox args
|
||||
*/
|
||||
function getManifestPaths(args: string[]): {
|
||||
forbidden: string[];
|
||||
allowed: string[];
|
||||
} {
|
||||
const forbiddenPath = args[3];
|
||||
const allowedPath = args[5];
|
||||
const forbidden = fs
|
||||
.readFileSync(forbiddenPath, 'utf8')
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
const allowed = fs
|
||||
.readFileSync(allowedPath, 'utf8')
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
return { forbidden, allowed };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
vi.spyOn(paths, 'resolveToRealPath').mockImplementation((p) => p);
|
||||
@@ -109,9 +90,7 @@ describe('WindowsSandboxManager', () => {
|
||||
'0',
|
||||
testCwd,
|
||||
'--forbidden-manifest',
|
||||
expect.stringMatching(/forbidden\.txt$/),
|
||||
'--allowed-manifest',
|
||||
expect.stringMatching(/allowed\.txt$/),
|
||||
expect.stringMatching(/manifest\.txt$/),
|
||||
'whoami',
|
||||
'/groups',
|
||||
]);
|
||||
@@ -146,12 +125,19 @@ describe('WindowsSandboxManager', () => {
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
const { allowed } = getManifestPaths(result.args);
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
// Should NOT have drive roots (C:\, D:\, etc.) in the allowed manifest
|
||||
const driveRoots = allowed.filter((p) => /^[A-Z]:\\$/.test(p));
|
||||
expect(driveRoots).toHaveLength(0);
|
||||
// Verify spawnAsync was called for icacls
|
||||
const icaclsCalls = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((call) => call[0] === 'icacls');
|
||||
|
||||
// Should NOT have called icacls for C:\, D:\, etc.
|
||||
const driveRootCalls = icaclsCalls.filter(
|
||||
(call) =>
|
||||
typeof call[1]?.[0] === 'string' && /^[A-Z]:\\$/.test(call[1][0]),
|
||||
);
|
||||
expect(driveRootCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle network access from additionalPermissions', async () => {
|
||||
@@ -219,8 +205,18 @@ describe('WindowsSandboxManager', () => {
|
||||
const result = await managerWithPolicy.prepareCommand(req);
|
||||
expect(result.args[0]).toBe('1'); // Network allowed by persistent policy
|
||||
|
||||
const { allowed } = getManifestPaths(result.args);
|
||||
expect(allowed).toContain(persistentPath);
|
||||
const icaclsArgs = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((c) => c[0] === 'icacls')
|
||||
.map((c) => c[1]);
|
||||
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
persistentPath,
|
||||
'/grant',
|
||||
'*S-1-16-4096:(OI)(CI)(M)',
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sanitize environment variables', async () => {
|
||||
@@ -262,7 +258,7 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(fs.lstatSync(path.join(testCwd, '.git')).isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it('should include the workspace and allowed paths in the allowed manifest', async () => {
|
||||
it('should grant Low Integrity access to the workspace and allowed paths', async () => {
|
||||
const allowedPath = createTempDir('allowed');
|
||||
try {
|
||||
const req: SandboxRequest = {
|
||||
@@ -275,17 +271,34 @@ describe('WindowsSandboxManager', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
const { allowed } = getManifestPaths(result.args);
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
expect(allowed).toContain(testCwd);
|
||||
expect(allowed).toContain(allowedPath);
|
||||
const icaclsArgs = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((c) => c[0] === 'icacls')
|
||||
.map((c) => c[1]);
|
||||
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
testCwd,
|
||||
'/grant',
|
||||
'*S-1-16-4096:(OI)(CI)(M)',
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
allowedPath,
|
||||
'/grant',
|
||||
'*S-1-16-4096:(OI)(CI)(M)',
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(allowedPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should exclude git worktree paths from the allowed manifest (enforce read-only)', async () => {
|
||||
it('should NOT grant Low Integrity access to git worktree paths (enforce read-only)', async () => {
|
||||
const worktreeGitDir = createTempDir('worktree-git');
|
||||
const mainGitDir = createTempDir('main-git');
|
||||
|
||||
@@ -310,19 +323,36 @@ describe('WindowsSandboxManager', () => {
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
const { allowed } = getManifestPaths(result.args);
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
// Verify that the git directories are NOT in the allowed manifest
|
||||
expect(allowed).not.toContain(worktreeGitDir);
|
||||
expect(allowed).not.toContain(mainGitDir);
|
||||
const icaclsArgs = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((c) => c[0] === 'icacls')
|
||||
.map((c) => c[1]);
|
||||
|
||||
// Verify that no icacls grants were issued for the git directories
|
||||
expect(icaclsArgs).not.toContainEqual([
|
||||
worktreeGitDir,
|
||||
'/grant',
|
||||
'*S-1-16-4096:(OI)(CI)(M)',
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
|
||||
expect(icaclsArgs).not.toContainEqual([
|
||||
mainGitDir,
|
||||
'/grant',
|
||||
'*S-1-16-4096:(OI)(CI)(M)',
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(worktreeGitDir, { recursive: true, force: true });
|
||||
fs.rmSync(mainGitDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should include additional write paths in the allowed manifest', async () => {
|
||||
it('should grant Low Integrity access to additional write paths', async () => {
|
||||
const extraWritePath = createTempDir('extra-write');
|
||||
try {
|
||||
const req: SandboxRequest = {
|
||||
@@ -339,17 +369,27 @@ describe('WindowsSandboxManager', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
const { allowed } = getManifestPaths(result.args);
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
expect(allowed).toContain(extraWritePath);
|
||||
const icaclsArgs = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((c) => c[0] === 'icacls')
|
||||
.map((c) => c[1]);
|
||||
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
extraWritePath,
|
||||
'/grant',
|
||||
'*S-1-16-4096:(OI)(CI)(M)',
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(extraWritePath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
'should reject UNC paths for allowed access',
|
||||
'should reject UNC paths in grantLowIntegrityAccess',
|
||||
async () => {
|
||||
const uncPath = '\\\\attacker\\share\\malicious.txt';
|
||||
const req: SandboxRequest = {
|
||||
@@ -368,11 +408,18 @@ describe('WindowsSandboxManager', () => {
|
||||
|
||||
// Rejected because it's an unreachable/invalid UNC path or it doesn't exist
|
||||
await expect(manager.prepareCommand(req)).rejects.toThrow();
|
||||
|
||||
const icaclsArgs = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((c) => c[0] === 'icacls')
|
||||
.map((c) => c[1]);
|
||||
|
||||
expect(icaclsArgs).not.toContainEqual(expect.arrayContaining([uncPath]));
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
'should include extended-length and local device paths in the allowed manifest',
|
||||
'should allow extended-length and local device paths',
|
||||
async () => {
|
||||
// Create actual files for inheritance/existence checks
|
||||
const longPath = path.join(testCwd, 'very_long_path.txt');
|
||||
@@ -394,15 +441,31 @@ describe('WindowsSandboxManager', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
const { allowed } = getManifestPaths(result.args);
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
expect(allowed).toContain(path.resolve(longPath));
|
||||
expect(allowed).toContain(path.resolve(devicePath));
|
||||
const icaclsArgs = vi
|
||||
.mocked(spawnAsync)
|
||||
.mock.calls.filter((c) => c[0] === 'icacls')
|
||||
.map((c) => c[1]);
|
||||
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
path.resolve(longPath),
|
||||
'/grant',
|
||||
'*S-1-16-4096:(M)',
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
]);
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
path.resolve(devicePath),
|
||||
'/grant',
|
||||
'*S-1-16-4096:(M)',
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it('includes non-existent forbidden paths in the forbidden manifest', async () => {
|
||||
it('skips denying access to non-existent forbidden paths to prevent icacls failure', async () => {
|
||||
const missingPath = path.join(
|
||||
os.tmpdir(),
|
||||
'gemini-cli-test-missing',
|
||||
@@ -426,13 +489,17 @@ describe('WindowsSandboxManager', () => {
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await managerWithForbidden.prepareCommand(req);
|
||||
const { forbidden } = getManifestPaths(result.args);
|
||||
await managerWithForbidden.prepareCommand(req);
|
||||
|
||||
expect(forbidden).toContain(path.resolve(missingPath));
|
||||
// Should NOT have called icacls to deny the missing path
|
||||
expect(spawnAsync).not.toHaveBeenCalledWith('icacls', [
|
||||
path.resolve(missingPath),
|
||||
'/deny',
|
||||
'*S-1-16-4096:(OI)(CI)(F)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should include forbidden paths in the forbidden manifest', async () => {
|
||||
it('should deny Low Integrity access to forbidden paths', async () => {
|
||||
const forbiddenPath = createTempDir('forbidden');
|
||||
try {
|
||||
const managerWithForbidden = new WindowsSandboxManager({
|
||||
@@ -447,16 +514,19 @@ describe('WindowsSandboxManager', () => {
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await managerWithForbidden.prepareCommand(req);
|
||||
const { forbidden } = getManifestPaths(result.args);
|
||||
await managerWithForbidden.prepareCommand(req);
|
||||
|
||||
expect(forbidden).toContain(forbiddenPath);
|
||||
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
|
||||
forbiddenPath,
|
||||
'/deny',
|
||||
'*S-1-16-4096:(OI)(CI)(F)',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(forbiddenPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should exclude forbidden paths from the allowed manifest if a conflict exists', async () => {
|
||||
it('should override allowed paths if a path is also in forbidden paths', async () => {
|
||||
const conflictPath = createTempDir('conflict');
|
||||
try {
|
||||
const managerWithForbidden = new WindowsSandboxManager({
|
||||
@@ -474,12 +544,27 @@ describe('WindowsSandboxManager', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await managerWithForbidden.prepareCommand(req);
|
||||
const { forbidden, allowed } = getManifestPaths(result.args);
|
||||
await managerWithForbidden.prepareCommand(req);
|
||||
|
||||
const spawnMock = vi.mocked(spawnAsync);
|
||||
const allowCallIndex = spawnMock.mock.calls.findIndex(
|
||||
(call) =>
|
||||
call[1] &&
|
||||
call[1].includes('/setintegritylevel') &&
|
||||
call[0] === 'icacls' &&
|
||||
call[1][0] === conflictPath,
|
||||
);
|
||||
const denyCallIndex = spawnMock.mock.calls.findIndex(
|
||||
(call) =>
|
||||
call[1] &&
|
||||
call[1].includes('/deny') &&
|
||||
call[0] === 'icacls' &&
|
||||
call[1][0] === conflictPath,
|
||||
);
|
||||
|
||||
// Conflict should have been filtered out of allow calls
|
||||
expect(allowed).not.toContain(conflictPath);
|
||||
expect(forbidden).toContain(conflictPath);
|
||||
expect(allowCallIndex).toBe(-1);
|
||||
expect(denyCallIndex).toBeGreaterThan(-1);
|
||||
} finally {
|
||||
fs.rmSync(conflictPath, { recursive: true, force: true });
|
||||
}
|
||||
@@ -497,12 +582,12 @@ describe('WindowsSandboxManager', () => {
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
// [network, cwd, --forbidden-manifest, fPath, --allowed-manifest, aPath, command, ...args]
|
||||
expect(result.args[6]).toBe('__write');
|
||||
expect(result.args[7]).toBe(filePath);
|
||||
// [network, cwd, --forbidden-manifest, manifestPath, command, ...args]
|
||||
expect(result.args[4]).toBe('__write');
|
||||
expect(result.args[5]).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should safely handle special characters in internal command paths', async () => {
|
||||
it('should safely handle special characters in __write path using environment variables', async () => {
|
||||
const maliciousPath = path.join(testCwd, 'foo & echo bar; ! .txt');
|
||||
fs.writeFileSync(maliciousPath, '');
|
||||
const req: SandboxRequest = {
|
||||
@@ -515,8 +600,8 @@ describe('WindowsSandboxManager', () => {
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
// Native commands pass arguments directly; the binary handles quoting via QuoteArgument
|
||||
expect(result.args[6]).toBe('__write');
|
||||
expect(result.args[7]).toBe(maliciousPath);
|
||||
expect(result.args[4]).toBe('__write');
|
||||
expect(result.args[5]).toBe(maliciousPath);
|
||||
});
|
||||
|
||||
it('should pass __read directly to native helper', async () => {
|
||||
@@ -531,11 +616,11 @@ describe('WindowsSandboxManager', () => {
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.args[6]).toBe('__read');
|
||||
expect(result.args[7]).toBe(filePath);
|
||||
expect(result.args[4]).toBe('__read');
|
||||
expect(result.args[5]).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should return a cleanup function that deletes the temporary manifest directory', async () => {
|
||||
it('should return a cleanup function that deletes the temporary manifest', async () => {
|
||||
const req: SandboxRequest = {
|
||||
command: 'test',
|
||||
args: [],
|
||||
@@ -544,16 +629,13 @@ describe('WindowsSandboxManager', () => {
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
const forbiddenManifestPath = result.args[3];
|
||||
const allowedManifestPath = result.args[5];
|
||||
const manifestPath = result.args[3];
|
||||
|
||||
expect(fs.existsSync(forbiddenManifestPath)).toBe(true);
|
||||
expect(fs.existsSync(allowedManifestPath)).toBe(true);
|
||||
expect(fs.existsSync(manifestPath)).toBe(true);
|
||||
expect(result.cleanup).toBeDefined();
|
||||
|
||||
result.cleanup?.();
|
||||
expect(fs.existsSync(forbiddenManifestPath)).toBe(false);
|
||||
expect(fs.existsSync(allowedManifestPath)).toBe(false);
|
||||
expect(fs.existsSync(path.dirname(forbiddenManifestPath))).toBe(false);
|
||||
expect(fs.existsSync(manifestPath)).toBe(false);
|
||||
expect(fs.existsSync(path.dirname(manifestPath))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '../../services/environmentSanitization.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { spawnAsync, getCommandName } from '../../utils/shell-utils.js';
|
||||
import { isNodeError } from '../../utils/errors.js';
|
||||
import {
|
||||
isKnownSafeCommand,
|
||||
isDangerousCommand,
|
||||
@@ -46,6 +47,13 @@ import {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// S-1-16-4096 is the SID for "Low Mandatory Level" (Low Integrity)
|
||||
const LOW_INTEGRITY_SID = '*S-1-16-4096';
|
||||
|
||||
// icacls flags: (OI) Object Inherit, (CI) Container Inherits.
|
||||
// Omit /T (recursive) for performance; (OI)(CI) ensures inheritance for new items.
|
||||
const DIRECTORY_FLAGS = '(OI)(CI)';
|
||||
|
||||
/**
|
||||
* A SandboxManager implementation for Windows that uses Restricted Tokens,
|
||||
* Job Objects, and Low Integrity levels for process isolation.
|
||||
@@ -55,6 +63,8 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
static readonly HELPER_EXE = 'GeminiSandbox.exe';
|
||||
private readonly helperPath: string;
|
||||
private initialized = false;
|
||||
private readonly allowedCache = new Set<string>();
|
||||
private readonly deniedCache = new Set<string>();
|
||||
private readonly denialCache: SandboxDenialCache = createSandboxDenialCache();
|
||||
|
||||
constructor(private readonly options: GlobalSandboxOptions) {
|
||||
@@ -276,73 +286,11 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
mergedAdditional,
|
||||
);
|
||||
|
||||
// 1. Collect all forbidden paths.
|
||||
// We start with explicitly forbidden paths from the options and request.
|
||||
const forbiddenManifest = new Set(
|
||||
resolvedPaths.forbidden.map((p) => resolveToRealPath(p)),
|
||||
);
|
||||
// Track all roots where Low Integrity write access has been granted.
|
||||
// New files created within these roots will inherit the Low label.
|
||||
const writableRoots: string[] = [];
|
||||
|
||||
// On Windows, we explicitly deny access to secret files for Low Integrity processes.
|
||||
// We scan common search directories (workspace, allowed paths) for secrets.
|
||||
const searchDirs = new Set([
|
||||
resolvedPaths.workspace.resolved,
|
||||
...resolvedPaths.policyAllowed,
|
||||
...resolvedPaths.globalIncludes,
|
||||
]);
|
||||
|
||||
const secretFilesPromises = Array.from(searchDirs).map(async (dir) => {
|
||||
try {
|
||||
// We use maxDepth 3 to catch common nested secrets while keeping performance high.
|
||||
const secretFiles = await findSecretFiles(dir, 3);
|
||||
for (const secretFile of secretFiles) {
|
||||
forbiddenManifest.add(resolveToRealPath(secretFile));
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.log(
|
||||
`WindowsSandboxManager: Failed to find secret files in ${dir}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(secretFilesPromises);
|
||||
|
||||
// 2. Track paths that will be granted write access.
|
||||
// 'allowedManifest' contains resolved paths for the C# helper to apply ACLs.
|
||||
// 'inheritanceRoots' contains both original and resolved paths for Node.js sub-path validation.
|
||||
const allowedManifest = new Set<string>();
|
||||
const inheritanceRoots = new Set<string>();
|
||||
|
||||
const addWritableRoot = (p: string) => {
|
||||
const resolved = resolveToRealPath(p);
|
||||
|
||||
// Track both versions for inheritance checks to be robust against symlinks.
|
||||
inheritanceRoots.add(p);
|
||||
inheritanceRoots.add(resolved);
|
||||
|
||||
// Never grant access to system directories or explicitly forbidden paths.
|
||||
if (this.isSystemDirectory(resolved)) return;
|
||||
if (forbiddenManifest.has(resolved)) return;
|
||||
|
||||
// Explicitly reject UNC paths to prevent credential theft/SSRF,
|
||||
// but allow local extended-length and device paths.
|
||||
if (
|
||||
resolved.startsWith('\\\\') &&
|
||||
!resolved.startsWith('\\\\?\\') &&
|
||||
!resolved.startsWith('\\\\.\\')
|
||||
) {
|
||||
debugLogger.log(
|
||||
'WindowsSandboxManager: Rejecting UNC path for allowed manifest:',
|
||||
resolved,
|
||||
);
|
||||
return;
|
||||
}
|
||||
allowedManifest.add(resolved);
|
||||
};
|
||||
|
||||
// 3. Populate writable roots from various sources.
|
||||
|
||||
// A. Workspace access
|
||||
// 1. Workspace access
|
||||
const isApproved = allowOverrides
|
||||
? await isStrictlyApproved(
|
||||
command,
|
||||
@@ -354,15 +302,17 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
const workspaceWrite = !isReadonlyMode || isApproved || isYolo;
|
||||
|
||||
if (workspaceWrite) {
|
||||
addWritableRoot(resolvedPaths.workspace.resolved);
|
||||
await this.grantLowIntegrityAccess(resolvedPaths.workspace.resolved);
|
||||
writableRoots.push(resolvedPaths.workspace.resolved);
|
||||
}
|
||||
|
||||
// B. Globally included directories
|
||||
// 2. Globally included directories
|
||||
for (const includeDir of resolvedPaths.globalIncludes) {
|
||||
addWritableRoot(includeDir);
|
||||
await this.grantLowIntegrityAccess(includeDir);
|
||||
writableRoots.push(includeDir);
|
||||
}
|
||||
|
||||
// C. Explicitly allowed paths from the request policy
|
||||
// 3. Explicitly allowed paths from the request policy
|
||||
for (const allowedPath of resolvedPaths.policyAllowed) {
|
||||
try {
|
||||
await fs.promises.access(allowedPath, fs.constants.F_OK);
|
||||
@@ -372,18 +322,19 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',
|
||||
);
|
||||
}
|
||||
addWritableRoot(allowedPath);
|
||||
await this.grantLowIntegrityAccess(allowedPath);
|
||||
writableRoots.push(allowedPath);
|
||||
}
|
||||
|
||||
// D. Additional write paths (e.g. from internal __write command)
|
||||
// 4. Additional write paths (e.g. from internal __write command)
|
||||
for (const writePath of resolvedPaths.policyWrite) {
|
||||
try {
|
||||
await fs.promises.access(writePath, fs.constants.F_OK);
|
||||
addWritableRoot(writePath);
|
||||
await this.grantLowIntegrityAccess(writePath);
|
||||
continue;
|
||||
} catch {
|
||||
// If the file doesn't exist, it's only allowed if it resides within a granted root.
|
||||
const isInherited = Array.from(inheritanceRoots).some((root) =>
|
||||
const isInherited = writableRoots.some((root) =>
|
||||
isSubpath(root, writePath),
|
||||
);
|
||||
|
||||
@@ -397,46 +348,88 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
}
|
||||
|
||||
// Support git worktrees/submodules; read-only to prevent malicious hook/config modification (RCE).
|
||||
// Read access is inherited; skip addWritableRoot to ensure write protection.
|
||||
// Read access is inherited; skip grantLowIntegrityAccess to ensure write protection.
|
||||
if (resolvedPaths.gitWorktree) {
|
||||
// No-op for read access on Windows.
|
||||
// No-op for read access.
|
||||
}
|
||||
|
||||
// 4. Protected governance files
|
||||
// 2. Collect secret files and apply protective ACLs
|
||||
// On Windows, we explicitly deny access to secret files for Low Integrity
|
||||
// processes to ensure they cannot be read or written.
|
||||
const secretsToBlock: string[] = [];
|
||||
const searchDirs = new Set([
|
||||
resolvedPaths.workspace.resolved,
|
||||
...resolvedPaths.policyAllowed,
|
||||
...resolvedPaths.globalIncludes,
|
||||
]);
|
||||
for (const dir of searchDirs) {
|
||||
try {
|
||||
// We use maxDepth 3 to catch common nested secrets while keeping performance high.
|
||||
const secretFiles = await findSecretFiles(dir, 3);
|
||||
for (const secretFile of secretFiles) {
|
||||
try {
|
||||
secretsToBlock.push(secretFile);
|
||||
await this.denyLowIntegrityAccess(secretFile);
|
||||
} catch (e) {
|
||||
debugLogger.log(
|
||||
`WindowsSandboxManager: Failed to secure secret file ${secretFile}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.log(
|
||||
`WindowsSandboxManager: Failed to find secret files in ${dir}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Denies access to forbiddenPaths for Low Integrity processes.
|
||||
// Note: Denying access to arbitrary paths (like system files) via icacls
|
||||
// is restricted to avoid host corruption. External commands rely on
|
||||
// Low Integrity read/write restrictions, while internal commands
|
||||
// use the manifest for enforcement.
|
||||
for (const forbiddenPath of resolvedPaths.forbidden) {
|
||||
try {
|
||||
await this.denyLowIntegrityAccess(forbiddenPath);
|
||||
} catch (e) {
|
||||
debugLogger.log(
|
||||
`WindowsSandboxManager: Failed to secure forbidden path ${forbiddenPath}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Protected governance files
|
||||
// These must exist on the host before running the sandbox to prevent
|
||||
// the sandboxed process from creating them with Low integrity.
|
||||
// By being created as Medium integrity, they are write-protected from Low processes.
|
||||
for (const file of GOVERNANCE_FILES) {
|
||||
const filePath = path.join(resolvedPaths.workspace.resolved, file.path);
|
||||
this.touch(filePath, file.isDirectory);
|
||||
}
|
||||
|
||||
// 5. Generate Manifests
|
||||
const tempDir = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'gemini-cli-sandbox-'),
|
||||
// 4. Forbidden paths manifest
|
||||
// We use a manifest file to avoid command-line length limits.
|
||||
const allForbidden = Array.from(
|
||||
new Set([...secretsToBlock, ...resolvedPaths.forbidden]),
|
||||
);
|
||||
|
||||
const forbiddenManifestPath = path.join(tempDir, 'forbidden.txt');
|
||||
await fs.promises.writeFile(
|
||||
forbiddenManifestPath,
|
||||
Array.from(forbiddenManifest).join('\n'),
|
||||
const tempDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-forbidden-'),
|
||||
);
|
||||
const manifestPath = path.join(tempDir, 'manifest.txt');
|
||||
fs.writeFileSync(manifestPath, allForbidden.join('\n'));
|
||||
|
||||
const allowedManifestPath = path.join(tempDir, 'allowed.txt');
|
||||
await fs.promises.writeFile(
|
||||
allowedManifestPath,
|
||||
Array.from(allowedManifest).join('\n'),
|
||||
);
|
||||
|
||||
// 6. Construct the helper command
|
||||
// 5. Construct the helper command
|
||||
// GeminiSandbox.exe <network:0|1> <cwd> --forbidden-manifest <path> <command> [args...]
|
||||
const program = this.helperPath;
|
||||
|
||||
const finalArgs = [
|
||||
networkAccess ? '1' : '0',
|
||||
req.cwd,
|
||||
'--forbidden-manifest',
|
||||
forbiddenManifestPath,
|
||||
'--allowed-manifest',
|
||||
allowedManifestPath,
|
||||
manifestPath,
|
||||
command,
|
||||
...args,
|
||||
];
|
||||
@@ -458,6 +451,111 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants "Low Mandatory Level" access to a path using icacls.
|
||||
*/
|
||||
private async grantLowIntegrityAccess(targetPath: string): Promise<void> {
|
||||
if (os.platform() !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = resolveToRealPath(targetPath);
|
||||
if (this.allowedCache.has(resolvedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Explicitly reject UNC paths to prevent credential theft/SSRF,
|
||||
// but allow local extended-length and device paths.
|
||||
if (
|
||||
resolvedPath.startsWith('\\\\') &&
|
||||
!resolvedPath.startsWith('\\\\?\\') &&
|
||||
!resolvedPath.startsWith('\\\\.\\')
|
||||
) {
|
||||
debugLogger.log(
|
||||
'WindowsSandboxManager: Rejecting UNC path for Low Integrity grant:',
|
||||
resolvedPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSystemDirectory(resolvedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.promises.stat(resolvedPath);
|
||||
const isDirectory = stats.isDirectory();
|
||||
|
||||
const flags = isDirectory ? DIRECTORY_FLAGS : '';
|
||||
|
||||
// 1. Grant explicit Modify access to the Low Integrity SID
|
||||
// 2. Set the Mandatory Label to Low to allow "Write Up" from Low processes
|
||||
await spawnAsync('icacls', [
|
||||
resolvedPath,
|
||||
'/grant',
|
||||
`${LOW_INTEGRITY_SID}:${flags}(M)`,
|
||||
'/setintegritylevel',
|
||||
`${flags}Low`,
|
||||
]);
|
||||
this.allowedCache.add(resolvedPath);
|
||||
} catch (e) {
|
||||
debugLogger.log(
|
||||
'WindowsSandboxManager: icacls failed for',
|
||||
resolvedPath,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly denies access to a path for Low Integrity processes using icacls.
|
||||
*/
|
||||
private async denyLowIntegrityAccess(targetPath: string): Promise<void> {
|
||||
if (os.platform() !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = resolveToRealPath(targetPath);
|
||||
if (this.deniedCache.has(resolvedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Never modify ACEs for system directories
|
||||
if (this.isSystemDirectory(resolvedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// icacls fails on non-existent paths, so we cannot explicitly deny
|
||||
// paths that do not yet exist (unlike macOS/Linux).
|
||||
// Skip to prevent sandbox initialization failure.
|
||||
let isDirectory = false;
|
||||
try {
|
||||
const stats = await fs.promises.stat(resolvedPath);
|
||||
isDirectory = stats.isDirectory();
|
||||
} catch (e: unknown) {
|
||||
if (isNodeError(e) && e.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const flags = isDirectory ? DIRECTORY_FLAGS : '';
|
||||
|
||||
try {
|
||||
await spawnAsync('icacls', [
|
||||
resolvedPath,
|
||||
'/deny',
|
||||
`${LOW_INTEGRITY_SID}:${flags}(F)`,
|
||||
]);
|
||||
this.deniedCache.add(resolvedPath);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to deny access to forbidden path: ${resolvedPath}. ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isSystemDirectory(resolvedPath: string): boolean {
|
||||
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
|
||||
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
|
||||
|
||||
@@ -536,34 +536,6 @@ describe('ChatRecordingService', () => {
|
||||
.toolCalls,
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should record agentId when provided', async () => {
|
||||
chatRecordingService.recordMessage({
|
||||
type: 'gemini',
|
||||
content: '',
|
||||
model: 'gemini-pro',
|
||||
});
|
||||
|
||||
const toolCall: ToolCallRecord = {
|
||||
id: 'tool-1',
|
||||
name: 'testTool',
|
||||
args: {},
|
||||
status: CoreToolCallStatus.Success,
|
||||
timestamp: new Date().toISOString(),
|
||||
agentId: 'test-agent-id',
|
||||
};
|
||||
chatRecordingService.recordToolCalls('gemini-pro', [toolCall]);
|
||||
|
||||
const sessionFile = chatRecordingService.getConversationFilePath()!;
|
||||
const conversation = (await loadConversationRecord(
|
||||
sessionFile,
|
||||
)) as ConversationRecord;
|
||||
const geminiMsg = conversation.messages[0] as MessageRecord & {
|
||||
type: 'gemini';
|
||||
};
|
||||
expect(geminiMsg.toolCalls).toHaveLength(1);
|
||||
expect(geminiMsg.toolCalls![0].agentId).toBe('test-agent-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSession', () => {
|
||||
|
||||
@@ -229,6 +229,7 @@ export async function loadConversationRecord(
|
||||
summary: metadata.summary,
|
||||
directories: metadata.directories,
|
||||
kind: metadata.kind,
|
||||
experimentalDynamicTools: metadata.experimentalDynamicTools,
|
||||
messages: Array.from(messagesMap.values()),
|
||||
messageCount: options?.metadataOnly
|
||||
? messageIds.length
|
||||
@@ -292,6 +293,7 @@ export class ChatRecordingService {
|
||||
kind: this.cachedConversation.kind,
|
||||
directories: this.cachedConversation.directories,
|
||||
summary: this.cachedConversation.summary,
|
||||
experimentalDynamicTools: this.cachedConversation.experimentalDynamicTools,
|
||||
};
|
||||
this.appendRecord(initialMetadata);
|
||||
for (const msg of this.cachedConversation.messages) {
|
||||
@@ -358,18 +360,21 @@ export class ChatRecordingService {
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const initialMetadata = {
|
||||
const initialMetadata: PartialMetadataRecord = {
|
||||
sessionId: this.sessionId,
|
||||
projectHash: this.projectHash,
|
||||
startTime: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
kind: this.kind,
|
||||
directories,
|
||||
experimentalDynamicTools: this.context.config.getExperimentalDynamicTools(),
|
||||
};
|
||||
|
||||
this.appendRecord(initialMetadata);
|
||||
this.cachedConversation = {
|
||||
...initialMetadata,
|
||||
startTime: initialMetadata.startTime!,
|
||||
lastUpdated: initialMetadata.lastUpdated!,
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,12 +45,15 @@ export interface ToolCallRecord {
|
||||
result?: PartListUnion | null;
|
||||
status: Status;
|
||||
timestamp: string;
|
||||
agentId?: string;
|
||||
// UI-specific fields for display purposes
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
resultDisplay?: ToolResultDisplay;
|
||||
renderOutputAsMarkdown?: boolean;
|
||||
/** Experimental: original name of the tool requested by the model before unwrapping */
|
||||
originalRequestName?: string;
|
||||
/** Experimental: original arguments of the tool requested by the model before unwrapping */
|
||||
originalRequestArgs?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +90,8 @@ export interface ConversationRecord {
|
||||
directories?: string[];
|
||||
/** The kind of conversation (main agent or subagent) */
|
||||
kind?: 'main' | 'subagent';
|
||||
/** Experimental: Whether dynamic tools documentation-injection was enabled */
|
||||
experimentalDynamicTools?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,4 +127,6 @@ export interface PartialMetadataRecord {
|
||||
summary?: string;
|
||||
directories?: string[];
|
||||
kind?: 'main' | 'subagent';
|
||||
/** Experimental: Whether dynamic tools documentation-injection was enabled */
|
||||
experimentalDynamicTools?: boolean;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const mockFileKeychain: MockKeychain = {
|
||||
findCredentials: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('@github/keytar', () => ({ default: mockKeytar }));
|
||||
vi.mock('keytar', () => ({ default: mockKeytar }));
|
||||
|
||||
vi.mock('./fileKeychain.js', () => ({
|
||||
FileKeychain: vi.fn(() => mockFileKeychain),
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FileKeychain } from './fileKeychain.js';
|
||||
export const FORCE_FILE_STORAGE_ENV_VAR = 'GEMINI_FORCE_FILE_STORAGE';
|
||||
|
||||
/**
|
||||
* Service for interacting with OS-level secure storage (e.g. @github/keytar).
|
||||
* Service for interacting with OS-level secure storage (e.g. keytar).
|
||||
*/
|
||||
export class KeychainService {
|
||||
// Track an ongoing initialization attempt to avoid race conditions.
|
||||
@@ -119,7 +119,7 @@ export class KeychainService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load and verify the native keychain module (@github/keytar).
|
||||
* Attempts to load and verify the native keychain module (keytar).
|
||||
*/
|
||||
private async getNativeKeychain(): Promise<Keychain | null> {
|
||||
try {
|
||||
@@ -152,7 +152,7 @@ export class KeychainService {
|
||||
|
||||
// Low-level dynamic loading and structural validation.
|
||||
private async loadKeychainModule(): Promise<Keychain | null> {
|
||||
const moduleName = '@github/keytar';
|
||||
const moduleName = 'keytar';
|
||||
const module: unknown = await import(moduleName);
|
||||
const potential = (isRecord(module) && module['default']) || module;
|
||||
|
||||
@@ -189,7 +189,7 @@ export class KeychainService {
|
||||
*/
|
||||
private isMacOSKeychainAvailable(): boolean {
|
||||
// Probing via the `security` CLI avoids a blocking OS-level popup that
|
||||
// occurs when calling @github/keytar without a configured keychain.
|
||||
// occurs when calling keytar without a configured keychain.
|
||||
const result = spawnSync('security', ['default-keychain'], {
|
||||
encoding: 'utf8',
|
||||
// We pipe stdout to read the path, but ignore stderr to suppress
|
||||
|
||||
@@ -8,7 +8,7 @@ import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Interface for OS-level secure storage operations.
|
||||
* Note: Method names must match the underlying library (e.g. @github/keytar)
|
||||
* Note: Method names must match the underlying library (e.g. keytar)
|
||||
* to support correct dynamic loading and schema validation.
|
||||
*/
|
||||
export interface Keychain {
|
||||
|
||||
@@ -75,7 +75,6 @@ vi.mock('../utils/shell-utils.js', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
resolveExecutable: mockResolveExecutable,
|
||||
spawnAsync: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
|
||||
};
|
||||
});
|
||||
vi.mock('node:child_process', async (importOriginal) => {
|
||||
@@ -209,7 +208,6 @@ describe('ShellExecutionService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ExecutionLifecycleService.resetForTest();
|
||||
ShellExecutionService.resetForTest();
|
||||
mockSerializeTerminalToObject.mockReturnValue([]);
|
||||
mockIsBinary.mockReturnValue(false);
|
||||
mockPlatform.mockReturnValue('linux');
|
||||
@@ -696,7 +694,7 @@ describe('ShellExecutionService', () => {
|
||||
);
|
||||
|
||||
expect(sigtermCallIndex).toBe(0);
|
||||
expect(sigkillCallIndex).toBeGreaterThan(0);
|
||||
expect(sigkillCallIndex).toBe(1);
|
||||
expect(sigtermCallIndex).toBeLessThan(sigkillCallIndex);
|
||||
|
||||
expect(result.signal).toBe(9);
|
||||
@@ -1249,8 +1247,6 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ExecutionLifecycleService.resetForTest();
|
||||
ShellExecutionService.resetForTest();
|
||||
|
||||
mockIsBinary.mockReturnValue(false);
|
||||
mockPlatform.mockReturnValue('linux');
|
||||
@@ -1477,11 +1473,8 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
|
||||
const { result } = await simulateExecution(
|
||||
'sleep 10',
|
||||
async (cp, abortController) => {
|
||||
(cp, abortController) => {
|
||||
abortController.abort();
|
||||
await new Promise(process.nextTick);
|
||||
await new Promise(process.nextTick);
|
||||
await new Promise(process.nextTick);
|
||||
if (expectedExit.signal) {
|
||||
cp.emit('exit', null, expectedExit.signal);
|
||||
cp.emit('close', null, expectedExit.signal);
|
||||
@@ -1501,14 +1494,11 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
expectedSignal,
|
||||
);
|
||||
} else {
|
||||
// Taskkill is spawned via spawnAsync which is mocked
|
||||
const { spawnAsync } = await import('../utils/shell-utils.js');
|
||||
expect(spawnAsync).toHaveBeenCalledWith(expectedCommand, [
|
||||
'/pid',
|
||||
String(mockChildProcess.pid),
|
||||
'/f',
|
||||
'/t',
|
||||
]);
|
||||
expect(mockCpSpawn).toHaveBeenCalledWith(
|
||||
expectedCommand,
|
||||
['/pid', String(mockChildProcess.pid), '/f', '/t'],
|
||||
expect.anything(),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1538,7 +1528,6 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
);
|
||||
|
||||
abortController.abort();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
// Check the first kill signal
|
||||
expect(mockProcessKill).toHaveBeenCalledWith(
|
||||
@@ -1618,22 +1607,6 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
'exit',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should correctly measure sniffedBytes with >20 small chunks to prevent OOM (regression #22170)', async () => {
|
||||
mockIsBinary.mockReturnValue(false);
|
||||
|
||||
await simulateExecution('cat lots_of_chunks', (cp) => {
|
||||
for (let i = 0; i < 25; i++) {
|
||||
cp.stdout?.emit('data', Buffer.alloc(10, 'a'));
|
||||
}
|
||||
cp.emit('exit', 0, null);
|
||||
cp.emit('close', 0, null);
|
||||
});
|
||||
|
||||
const lastCallBuffer =
|
||||
mockIsBinary.mock.calls[mockIsBinary.mock.calls.length - 1][0];
|
||||
expect(lastCallBuffer.length).toBe(250);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Platform-Specific Behavior', () => {
|
||||
@@ -1689,8 +1662,6 @@ describe('ShellExecutionService execution method selection', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ExecutionLifecycleService.resetForTest();
|
||||
ShellExecutionService.resetForTest();
|
||||
onOutputEventMock = vi.fn();
|
||||
|
||||
// Mock for pty
|
||||
@@ -1741,12 +1712,10 @@ describe('ShellExecutionService execution method selection', () => {
|
||||
);
|
||||
|
||||
// Simulate exit to allow promise to resolve
|
||||
if (!mockPtyProcess.onExit.mock.calls[0]) {
|
||||
const res = await handle.result;
|
||||
throw new Error(`Failed early in executeWithPty: ${res.error}`);
|
||||
}
|
||||
mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
||||
const result = await handle.result;
|
||||
|
||||
expect(mockGetPty).toHaveBeenCalled();
|
||||
expect(mockPtySpawn).toHaveBeenCalled();
|
||||
expect(mockCpSpawn).not.toHaveBeenCalled();
|
||||
expect(result.executionMethod).toBe('mock-pty');
|
||||
@@ -1817,8 +1786,6 @@ describe('ShellExecutionService environment variables', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ExecutionLifecycleService.resetForTest();
|
||||
ShellExecutionService.resetForTest();
|
||||
vi.resetModules(); // Reset modules to ensure process.env changes are fresh
|
||||
|
||||
// Mock for pty
|
||||
|
||||
@@ -112,10 +112,8 @@ export interface ShellExecutionConfig {
|
||||
*/
|
||||
export type ShellOutputEvent = ExecutionOutputEvent;
|
||||
|
||||
export type DestroyablePty = IPty & { destroy?: () => void };
|
||||
|
||||
interface ActivePty {
|
||||
ptyProcess: DestroyablePty;
|
||||
ptyProcess: IPty;
|
||||
headlessTerminal: pkg.Terminal;
|
||||
maxSerializedLines?: number;
|
||||
command: string;
|
||||
@@ -632,7 +630,7 @@ export class ShellExecutionService {
|
||||
}
|
||||
|
||||
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
||||
const sniffBuffer = Buffer.concat(state.sniffChunks);
|
||||
const sniffBuffer = Buffer.concat(state.sniffChunks.slice(0, 20));
|
||||
sniffedBytes = sniffBuffer.length;
|
||||
|
||||
if (isBinary(sniffBuffer)) {
|
||||
@@ -706,10 +704,7 @@ export class ShellExecutionService {
|
||||
|
||||
const finalStrippedOutput = stripAnsi(combinedOutput).trim();
|
||||
const exitCode = code;
|
||||
const exitSignal =
|
||||
signal && os.constants.signals
|
||||
? (os.constants.signals[signal] ?? null)
|
||||
: null;
|
||||
const exitSignal = signal ? os.constants.signals[signal] : null;
|
||||
|
||||
const resultPayload: ShellExecutionResult = {
|
||||
rawOutput: Buffer.from(''),
|
||||
@@ -835,42 +830,6 @@ export class ShellExecutionService {
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Destroys a PTY process to release its file descriptors.
|
||||
* This is critical to prevent system-wide PTY exhaustion (see #15945).
|
||||
*/
|
||||
private static destroyPtyProcess(ptyProcess: DestroyablePty): void {
|
||||
try {
|
||||
if (typeof ptyProcess?.destroy === 'function') {
|
||||
ptyProcess.destroy();
|
||||
} else if (typeof ptyProcess?.kill === 'function') {
|
||||
// Fallback: if destroy() is unavailable, kill() may still close FDs
|
||||
ptyProcess.kill();
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during PTY cleanup — process may already be dead
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all resources associated with a PTY entry:
|
||||
* the PTY process (file descriptors) and the headless terminal (memory buffers).
|
||||
*/
|
||||
private static cleanupPtyEntry(pid: number): void {
|
||||
const entry = this.activePtys.get(pid);
|
||||
if (!entry) return;
|
||||
|
||||
this.destroyPtyProcess(entry.ptyProcess);
|
||||
|
||||
try {
|
||||
entry.headlessTerminal.dispose();
|
||||
} catch {
|
||||
// Ignore errors during terminal cleanup
|
||||
}
|
||||
|
||||
this.activePtys.delete(pid);
|
||||
}
|
||||
|
||||
private static async executeWithPty(
|
||||
commandToExecute: string,
|
||||
cwd: string,
|
||||
@@ -883,7 +842,7 @@ export class ShellExecutionService {
|
||||
// This should not happen, but as a safeguard...
|
||||
throw new Error('PTY implementation not found');
|
||||
}
|
||||
let spawnedPty: DestroyablePty | undefined;
|
||||
let spawnedPty: IPty | undefined;
|
||||
let cmdCleanup: (() => void) | undefined;
|
||||
|
||||
try {
|
||||
@@ -916,7 +875,7 @@ export class ShellExecutionService {
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
spawnedPty = ptyProcess as DestroyablePty;
|
||||
spawnedPty = ptyProcess as IPty;
|
||||
const ptyPid = Number(ptyProcess.pid);
|
||||
|
||||
const headlessTerminal = new Terminal({
|
||||
@@ -950,6 +909,13 @@ export class ShellExecutionService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
pty: ptyProcess,
|
||||
}).catch(() => {});
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(ptyProcess as IPty & { destroy?: () => void }).destroy?.();
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
this.activePtys.delete(ptyPid);
|
||||
},
|
||||
isActive: () => {
|
||||
try {
|
||||
@@ -1125,7 +1091,7 @@ export class ShellExecutionService {
|
||||
}
|
||||
|
||||
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
||||
const sniffBuffer = Buffer.concat(sniffChunks);
|
||||
const sniffBuffer = Buffer.concat(sniffChunks.slice(0, 20));
|
||||
sniffedBytes = sniffBuffer.length;
|
||||
|
||||
if (isBinary(sniffBuffer)) {
|
||||
@@ -1177,11 +1143,13 @@ export class ShellExecutionService {
|
||||
({ exitCode, signal }: { exitCode: number; signal?: number }) => {
|
||||
exited = true;
|
||||
abortSignal.removeEventListener('abort', abortHandler);
|
||||
|
||||
// Immediately destroy the PTY to release its master FD.
|
||||
// The headless terminal is kept alive until finalize() extracts
|
||||
// its buffer contents, then disposed to free memory.
|
||||
ShellExecutionService.destroyPtyProcess(ptyProcess);
|
||||
// Attempt to destroy the PTY to ensure FD is closed
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(ptyProcess as IPty & { destroy?: () => void }).destroy?.();
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
const finalize = () => {
|
||||
render(true);
|
||||
@@ -1205,6 +1173,11 @@ export class ShellExecutionService {
|
||||
}
|
||||
onOutputEvent(event);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ShellExecutionService.cleanupLogStream(ptyPid).then(() => {
|
||||
ShellExecutionService.activePtys.delete(ptyPid);
|
||||
});
|
||||
|
||||
const endLine = headlessTerminal.buffer.active.length;
|
||||
const startLine = Math.max(
|
||||
0,
|
||||
@@ -1215,24 +1188,10 @@ export class ShellExecutionService {
|
||||
startLine,
|
||||
endLine,
|
||||
);
|
||||
const finalOutput = getFullBufferText(headlessTerminal);
|
||||
|
||||
// Dispose the headless terminal to free scrollback buffers.
|
||||
// This must happen after getFullBufferText() extracts the output.
|
||||
try {
|
||||
headlessTerminal.dispose();
|
||||
} catch {
|
||||
// Ignore errors during terminal cleanup
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ShellExecutionService.cleanupLogStream(ptyPid).then(() => {
|
||||
ShellExecutionService.activePtys.delete(ptyPid);
|
||||
});
|
||||
|
||||
ExecutionLifecycleService.completeWithResult(ptyPid, {
|
||||
rawOutput: Buffer.from(''),
|
||||
output: finalOutput,
|
||||
output: getFullBufferText(headlessTerminal),
|
||||
ansiOutput: ansiOutputSnapshot,
|
||||
exitCode,
|
||||
signal: signal ?? null,
|
||||
@@ -1287,10 +1246,14 @@ export class ShellExecutionService {
|
||||
cmdCleanup?.();
|
||||
|
||||
if (spawnedPty) {
|
||||
ShellExecutionService.destroyPtyProcess(spawnedPty);
|
||||
try {
|
||||
(spawnedPty as IPty & { destroy?: () => void }).destroy?.();
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
|
||||
if (error?.message?.includes('posix_spawnp failed')) {
|
||||
if (error.message.includes('posix_spawnp failed')) {
|
||||
onOutputEvent({
|
||||
type: 'data',
|
||||
chunk:
|
||||
@@ -1350,9 +1313,9 @@ export class ShellExecutionService {
|
||||
*/
|
||||
static async kill(pid: number): Promise<void> {
|
||||
await this.cleanupLogStream(pid);
|
||||
this.activePtys.delete(pid);
|
||||
this.activeChildProcesses.delete(pid);
|
||||
ExecutionLifecycleService.kill(pid);
|
||||
this.cleanupPtyEntry(pid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1540,16 +1503,4 @@ export class ShellExecutionService {
|
||||
signal: info.signal,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal state of the ShellExecutionService.
|
||||
* This is intended for use in tests to ensure isolation.
|
||||
*/
|
||||
static resetForTest(): void {
|
||||
this.activePtys.clear();
|
||||
this.activeChildProcesses.clear();
|
||||
this.backgroundLogPids.clear();
|
||||
this.backgroundLogStreams.clear();
|
||||
this.backgroundProcessHistory.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,32 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { diag, SpanStatusCode, trace } from '@opentelemetry/api';
|
||||
import type { Tracer } from '@opentelemetry/api';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { trace, SpanStatusCode, diag, type Tracer } from '@opentelemetry/api';
|
||||
import { runInDevTraceSpan, truncateForTelemetry } from './trace.js';
|
||||
import {
|
||||
GeminiCliOperation,
|
||||
GEN_AI_CONVERSATION_ID,
|
||||
GEN_AI_AGENT_DESCRIPTION,
|
||||
GEN_AI_AGENT_NAME,
|
||||
GEN_AI_CONVERSATION_ID,
|
||||
GEN_AI_INPUT_MESSAGES,
|
||||
GEN_AI_OPERATION_NAME,
|
||||
GEN_AI_OUTPUT_MESSAGES,
|
||||
GeminiCliOperation,
|
||||
SERVICE_DESCRIPTION,
|
||||
SERVICE_NAME,
|
||||
} from './constants.js';
|
||||
import {
|
||||
runInDevTraceSpan,
|
||||
spanRegistry,
|
||||
truncateForTelemetry,
|
||||
} from './trace.js';
|
||||
|
||||
vi.mock('@opentelemetry/api', async (importOriginal) => {
|
||||
const original = await importOriginal();
|
||||
return Object.assign({}, original, {
|
||||
const original = await importOriginal<typeof import('@opentelemetry/api')>();
|
||||
return {
|
||||
...original,
|
||||
trace: {
|
||||
getTracer: vi.fn(),
|
||||
},
|
||||
diag: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../utils/session.js', () => ({
|
||||
@@ -212,45 +207,6 @@ describe('runInDevTraceSpan', () => {
|
||||
expect(mockSpan.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register async generators with spanRegistry', async () => {
|
||||
const spy = vi.spyOn(spanRegistry, 'register');
|
||||
async function* testStream() {
|
||||
yield 1;
|
||||
}
|
||||
|
||||
const resultStream = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
async () => testStream(),
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(resultStream, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should be idempotent and call span.end only once', async () => {
|
||||
vi.spyOn(spanRegistry, 'register');
|
||||
async function* testStream() {
|
||||
yield 1;
|
||||
}
|
||||
|
||||
const resultStream = await runInDevTraceSpan(
|
||||
{ operation: GeminiCliOperation.LLMCall, sessionId: 'test-session-id' },
|
||||
async () => testStream(),
|
||||
);
|
||||
|
||||
// Simulate completion
|
||||
for await (const _ of resultStream) {
|
||||
// iterate
|
||||
}
|
||||
expect(mockSpan.end).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Try to end again (simulating registry or double call)
|
||||
const endSpanFn = vi.mocked(spanRegistry.register).mock
|
||||
.calls[0][1] as () => void;
|
||||
endSpanFn();
|
||||
|
||||
expect(mockSpan.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should end span automatically on error in async iterators', async () => {
|
||||
const error = new Error('streaming error');
|
||||
async function* errorStream() {
|
||||
|
||||
@@ -11,11 +11,9 @@ import {
|
||||
type AttributeValue,
|
||||
type SpanOptions,
|
||||
} from '@opentelemetry/api';
|
||||
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import { truncateString } from '../utils/textUtils.js';
|
||||
import {
|
||||
type GeminiCliOperation,
|
||||
GEN_AI_AGENT_DESCRIPTION,
|
||||
GEN_AI_AGENT_NAME,
|
||||
GEN_AI_CONVERSATION_ID,
|
||||
@@ -24,44 +22,23 @@ import {
|
||||
GEN_AI_OUTPUT_MESSAGES,
|
||||
SERVICE_DESCRIPTION,
|
||||
SERVICE_NAME,
|
||||
type GeminiCliOperation,
|
||||
} from './constants.js';
|
||||
|
||||
import { truncateString } from '../utils/textUtils.js';
|
||||
|
||||
const TRACER_NAME = 'gemini-cli';
|
||||
const TRACER_VERSION = 'v1';
|
||||
|
||||
/**
|
||||
* Registry used to ensure that spans are properly ended when their associated
|
||||
* async objects are garbage collected.
|
||||
*/
|
||||
export const spanRegistry = new FinalizationRegistry((endSpan: () => void) => {
|
||||
try {
|
||||
endSpan();
|
||||
} catch (e) {
|
||||
debugLogger.warn(
|
||||
'Error in FinalizationRegistry callback for span cleanup',
|
||||
e,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Truncates a value for inclusion in telemetry attributes.
|
||||
*
|
||||
* @param value The value to truncate.
|
||||
* @param maxLength The maximum length of the stringified value.
|
||||
* @returns The truncated value, or undefined if the value type is not supported.
|
||||
*/
|
||||
export function truncateForTelemetry(
|
||||
value: unknown,
|
||||
maxLength = 10000,
|
||||
maxLength: number = 10000,
|
||||
): AttributeValue | undefined {
|
||||
if (typeof value === 'string') {
|
||||
return truncateString(
|
||||
value,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${value.length}]`,
|
||||
) as AttributeValue;
|
||||
);
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const stringified = safeJsonStringify(value);
|
||||
@@ -69,10 +46,10 @@ export function truncateForTelemetry(
|
||||
stringified,
|
||||
maxLength,
|
||||
`...[TRUNCATED: original length ${stringified.length}]`,
|
||||
) as AttributeValue;
|
||||
);
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value as AttributeValue;
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -105,15 +82,12 @@ export interface SpanMetadata {
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await runInDevTraceSpan(
|
||||
* { operation: GeminiCliOperation.LLMCall, sessionId: 'my-session' },
|
||||
* async ({ metadata }) => {
|
||||
* metadata.input = { foo: 'bar' };
|
||||
* // ... do work ...
|
||||
* metadata.output = { result: 'baz' };
|
||||
* metadata.attributes['my.custom.attribute'] = 'some-value';
|
||||
* }
|
||||
* );
|
||||
* runInDevTraceSpan({ name: 'my-operation' }, ({ metadata }) => {
|
||||
* metadata.input = { foo: 'bar' };
|
||||
* // ... do work ...
|
||||
* metadata.output = { result: 'baz' };
|
||||
* metadata.attributes['my.custom.attribute'] = 'some-value';
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param opts The options for the span.
|
||||
@@ -141,12 +115,7 @@ export async function runInDevTraceSpan<R>(
|
||||
[GEN_AI_CONVERSATION_ID]: sessionId,
|
||||
},
|
||||
};
|
||||
let spanEnded = false;
|
||||
const endSpan = () => {
|
||||
if (spanEnded) {
|
||||
return;
|
||||
}
|
||||
spanEnded = true;
|
||||
try {
|
||||
if (logPrompts !== false) {
|
||||
if (meta.input !== undefined) {
|
||||
@@ -200,7 +169,7 @@ export async function runInDevTraceSpan<R>(
|
||||
const streamWrapper = (async function* () {
|
||||
try {
|
||||
yield* result;
|
||||
} catch (e: unknown) {
|
||||
} catch (e) {
|
||||
meta.error = e;
|
||||
throw e;
|
||||
} finally {
|
||||
@@ -208,12 +177,10 @@ export async function runInDevTraceSpan<R>(
|
||||
}
|
||||
})();
|
||||
|
||||
const finalResult = Object.assign(streamWrapper, result);
|
||||
spanRegistry.register(finalResult, endSpan);
|
||||
return finalResult;
|
||||
return Object.assign(streamWrapper, result);
|
||||
}
|
||||
return result;
|
||||
} catch (e: unknown) {
|
||||
} catch (e) {
|
||||
meta.error = e;
|
||||
throw e;
|
||||
} finally {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Kind,
|
||||
type ToolCallConfirmationDetails,
|
||||
type ToolInvocation,
|
||||
type ToolLiveOutput,
|
||||
type ToolResult,
|
||||
type ExecuteOptions,
|
||||
} from '../tools/tools.js';
|
||||
@@ -53,8 +54,11 @@ class MockToolInvocation extends BaseToolInvocation<
|
||||
super(params, messageBus, tool.name, tool.displayName);
|
||||
}
|
||||
|
||||
execute(options: ExecuteOptions): Promise<ToolResult> {
|
||||
const { abortSignal: signal, updateOutput } = options;
|
||||
execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: ToolLiveOutput) => void,
|
||||
options?: ExecuteOptions,
|
||||
): Promise<ToolResult> {
|
||||
return this.tool.execute(
|
||||
this.params,
|
||||
signal,
|
||||
@@ -154,10 +158,11 @@ export class MockModifiableToolInvocation extends BaseToolInvocation<
|
||||
super(params, messageBus, tool.name, tool.displayName);
|
||||
}
|
||||
|
||||
async execute({
|
||||
abortSignal: _signal,
|
||||
updateOutput: _updateOutput,
|
||||
}: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(
|
||||
_signal: AbortSignal,
|
||||
_updateOutput?: (output: ToolLiveOutput) => void,
|
||||
_options?: ExecuteOptions,
|
||||
): Promise<ToolResult> {
|
||||
const result = this.tool.executeFn(this.params);
|
||||
return (
|
||||
result ?? {
|
||||
|
||||
@@ -107,9 +107,7 @@ describe('ActivateSkillTool', () => {
|
||||
it('should activate a valid skill and return its content in XML tags', async () => {
|
||||
const params = { name: 'test-skill' };
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockConfig.getSkillManager().activateSkill).toHaveBeenCalledWith(
|
||||
'test-skill',
|
||||
@@ -138,9 +136,7 @@ describe('ActivateSkillTool', () => {
|
||||
vi.mocked(mockConfig.getSkillManager().getSkill).mockReturnValue(null);
|
||||
const params = { name: 'test-skill' };
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Error: Skill "test-skill" not found.');
|
||||
expect(mockConfig.getSkillManager().activateSkill).not.toHaveBeenCalled();
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type ToolCallConfirmationDetails,
|
||||
type ToolInvocation,
|
||||
type ToolConfirmationOutcome,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { ACTIVATE_SKILL_TOOL_NAME } from './tool-names.js';
|
||||
@@ -108,7 +107,7 @@ ${folderStructure}`,
|
||||
return confirmationDetails;
|
||||
}
|
||||
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
const skillName = this.params.name;
|
||||
const skillManager = this.config.getSkillManager();
|
||||
const skill = skillManager.getSkill(skillName);
|
||||
|
||||
@@ -410,9 +410,7 @@ describe('AskUserTool', () => {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.returnDisplay).toContain('User answered:');
|
||||
expect(result.returnDisplay).toContain(
|
||||
' Approach → Quick fix (Recommended)',
|
||||
@@ -455,9 +453,7 @@ describe('AskUserTool', () => {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.returnDisplay).toBe(
|
||||
'User submitted without answering questions.',
|
||||
);
|
||||
@@ -503,9 +499,7 @@ describe('AskUserTool', () => {
|
||||
await details.onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.returnDisplay).toBe('User dismissed dialog');
|
||||
expect(result.llmContent).toBe(
|
||||
'User dismissed ask_user dialog without answering.',
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type ToolAskUserConfirmationDetails,
|
||||
type ToolConfirmationPayload,
|
||||
ToolConfirmationOutcome,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
@@ -153,7 +152,7 @@ export class AskUserInvocation extends BaseToolInvocation<
|
||||
return `Asking user: ${this.params.questions.map((q) => q.question).join(', ')}`;
|
||||
}
|
||||
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
const questionTypes = this.params.questions.map((q) => q.type);
|
||||
|
||||
if (this.confirmationOutcome === ToolConfirmationOutcome.Cancel) {
|
||||
|
||||
@@ -63,9 +63,7 @@ describe('CompleteTaskTool', () => {
|
||||
|
||||
it('should execute and return correct data', async () => {
|
||||
const invocation = tool.build({ result: 'Success message' });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.data).toEqual({
|
||||
taskCompleted: true,
|
||||
@@ -135,9 +133,7 @@ describe('CompleteTaskTool', () => {
|
||||
it('should execute and return structured data', async () => {
|
||||
const outputValue = { report: 'Final findings', score: 42 };
|
||||
const invocation = tool.build({ my_output: outputValue });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.data?.['taskCompleted']).toBe(true);
|
||||
expect(result.data?.['submittedOutput']).toBe(
|
||||
@@ -156,9 +152,7 @@ describe('CompleteTaskTool', () => {
|
||||
|
||||
const outputValue = { report: 'Final findings', score: 42 };
|
||||
const invocation = toolWithProcess.build({ my_output: outputValue });
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.data?.['submittedOutput']).toBe('Score was 42');
|
||||
});
|
||||
|
||||
@@ -9,9 +9,7 @@ import {
|
||||
BaseToolInvocation,
|
||||
type ToolResult,
|
||||
Kind,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
|
||||
import {
|
||||
COMPLETE_TASK_TOOL_NAME,
|
||||
COMPLETE_TASK_DISPLAY_NAME,
|
||||
@@ -142,7 +140,7 @@ export class CompleteTaskInvocation<
|
||||
return 'Completing task and submitting results.';
|
||||
}
|
||||
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
let submittedOutput: string | null = null;
|
||||
let outputValue: unknown;
|
||||
|
||||
|
||||
@@ -695,9 +695,9 @@ function doIt() {
|
||||
throw abortError;
|
||||
});
|
||||
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: abortController.signal }),
|
||||
).rejects.toBe(abortError);
|
||||
await expect(invocation.execute(abortController.signal)).rejects.toBe(
|
||||
abortError,
|
||||
);
|
||||
|
||||
calculateSpy.mockRestore();
|
||||
});
|
||||
@@ -714,9 +714,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toMatch(/Successfully modified file/);
|
||||
expect(fs.readFileSync(filePath, 'utf8')).toBe(newContent);
|
||||
@@ -739,9 +737,7 @@ function doIt() {
|
||||
new_string: 'replacement',
|
||||
};
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.llmContent).toMatch(/0 occurrences found for old_string/);
|
||||
expect(result.returnDisplay).toMatch(
|
||||
/Failed to edit, could not find the string to replace./,
|
||||
@@ -772,9 +768,7 @@ function doIt() {
|
||||
});
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.llmContent).toMatch(/Successfully modified file/);
|
||||
@@ -794,7 +788,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
const finalContent = fs.readFileSync(filePath, 'utf8');
|
||||
expect(finalContent).toBe(newContent);
|
||||
@@ -810,7 +804,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
const finalContent = fs.readFileSync(filePath, 'utf8');
|
||||
expect(finalContent).toBe(newContentWithCRLF);
|
||||
@@ -838,9 +832,7 @@ function doIt() {
|
||||
});
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error?.type).toBe(
|
||||
ToolErrorType.EDIT_NO_CHANGE_LLM_JUDGEMENT,
|
||||
@@ -884,7 +876,7 @@ function doIt() {
|
||||
.mockResolvedValueOnce(externallyModifiedContent); // Second call in `attemptSelfCorrection`
|
||||
|
||||
const invocation = tool.build(params);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
// Assert that the file was read twice (initial read, then re-read for hash comparison).
|
||||
expect(readTextFileSpy).toHaveBeenCalledTimes(2);
|
||||
@@ -946,9 +938,7 @@ function doIt() {
|
||||
instruction: 'test',
|
||||
...params,
|
||||
});
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.error?.type).toBe(expectedError);
|
||||
},
|
||||
);
|
||||
@@ -1030,9 +1020,7 @@ function doIt() {
|
||||
...(allow_multiple !== undefined && { allow_multiple }),
|
||||
};
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
if (shouldSucceed) {
|
||||
expect(result.error).toBeUndefined();
|
||||
@@ -1174,9 +1162,7 @@ function doIt() {
|
||||
ai_proposed_content: '',
|
||||
};
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
if (
|
||||
result.returnDisplay &&
|
||||
@@ -1229,9 +1215,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error?.type).toBe(ToolErrorType.EDIT_NO_OCCURRENCE_FOUND);
|
||||
expect(mockFixLLMEditWithInstruction).not.toHaveBeenCalled();
|
||||
@@ -1252,7 +1236,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockFixLLMEditWithInstruction).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1281,9 +1265,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(discoverJitContext).toHaveBeenCalled();
|
||||
expect(result.llmContent).toContain('Newly Discovered Project Context');
|
||||
@@ -1312,9 +1294,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).not.toContain(
|
||||
'Newly Discovered Project Context',
|
||||
@@ -1349,9 +1329,7 @@ function doIt() {
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toMatch(/Successfully modified file/);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
type ToolResult,
|
||||
type ToolResultDisplay,
|
||||
type PolicyUpdateOptions,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import { buildFilePathArgsPattern } from '../policy/utils.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
@@ -833,7 +832,7 @@ class EditToolInvocation
|
||||
* @param params Parameters for the edit operation
|
||||
* @returns Result of the edit operation
|
||||
*/
|
||||
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
const validationError = this.config.validatePathAccess(this.resolvedPath);
|
||||
if (validationError) {
|
||||
return {
|
||||
|
||||
@@ -121,9 +121,7 @@ describe('EnterPlanModeTool', () => {
|
||||
const invocation = tool.build({});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
@@ -136,7 +134,7 @@ describe('EnterPlanModeTool', () => {
|
||||
const invocation = tool.build({});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/plans/dir', {
|
||||
recursive: true,
|
||||
@@ -148,9 +146,7 @@ describe('EnterPlanModeTool', () => {
|
||||
const invocation = tool.build({ reason });
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
@@ -181,9 +177,7 @@ describe('EnterPlanModeTool', () => {
|
||||
await details.onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(mockConfig.setApprovalMode).not.toHaveBeenCalled();
|
||||
expect(result.returnDisplay).toBe('Cancelled');
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Kind,
|
||||
type ToolInfoConfirmationDetails,
|
||||
ToolConfirmationOutcome,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
@@ -115,7 +114,7 @@ export class EnterPlanModeInvocation extends BaseToolInvocation<
|
||||
};
|
||||
}
|
||||
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
if (this.confirmationOutcome === ToolConfirmationOutcome.Cancel) {
|
||||
return {
|
||||
llmContent: 'User cancelled entering Plan Mode.',
|
||||
|
||||
@@ -135,9 +135,9 @@ describe('ExitPlanModeTool', () => {
|
||||
|
||||
expect(result).toBe(false);
|
||||
// Verify it auto-approved internally
|
||||
const executeResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const executeResult = await invocation.execute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(executeResult.llmContent).toContain('Plan approved');
|
||||
});
|
||||
|
||||
@@ -164,9 +164,7 @@ describe('ExitPlanModeTool', () => {
|
||||
const invocation = tool.build({ plan_filename: planRelativePath });
|
||||
|
||||
await invocation.shouldConfirmExecute(new AbortController().signal);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Plan file is empty');
|
||||
expect(result.llmContent).toContain('write content to the plan');
|
||||
@@ -177,9 +175,7 @@ describe('ExitPlanModeTool', () => {
|
||||
const invocation = tool.build({ plan_filename: planRelativePath });
|
||||
|
||||
await invocation.shouldConfirmExecute(new AbortController().signal);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Plan file does not exist');
|
||||
});
|
||||
@@ -201,9 +197,7 @@ describe('ExitPlanModeTool', () => {
|
||||
approvalMode: ApprovalMode.DEFAULT,
|
||||
});
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
const expectedPath = path.join(mockPlansDir, 'test.md');
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -231,9 +225,7 @@ Read and follow the plan strictly during implementation.`,
|
||||
approvalMode: ApprovalMode.AUTO_EDIT,
|
||||
});
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
const expectedPath = path.join(mockPlansDir, 'test.md');
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -264,9 +256,7 @@ Read and follow the plan strictly during implementation.`,
|
||||
feedback: 'Please add more details.',
|
||||
});
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
const expectedPath = path.join(mockPlansDir, 'test.md');
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -292,9 +282,7 @@ Revise the plan based on the feedback.`,
|
||||
approved: false,
|
||||
});
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
const expectedPath = path.join(mockPlansDir, 'test.md');
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -320,7 +308,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
approvalMode: ApprovalMode.AUTO_EDIT,
|
||||
});
|
||||
|
||||
await invocation.execute({ abortSignal: new AbortController().signal });
|
||||
await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(loggers.logPlanExecution).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
@@ -342,9 +330,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
|
||||
await confirmDetails.onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result).toEqual({
|
||||
llmContent:
|
||||
@@ -362,9 +348,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
// Simulate the scheduler's policy ALLOW path: execute() is called
|
||||
// directly without ever calling shouldConfirmExecute(), leaving
|
||||
// approvalPayload null.
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
const expectedPath = path.join(mockPlansDir, 'test.md');
|
||||
|
||||
expect(result.llmContent).toContain('Plan approved');
|
||||
@@ -383,9 +367,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
const invocation = tool.build({ plan_filename: planRelativePath });
|
||||
|
||||
// Directly call execute to trigger the internal getAllowApprovalMode
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('YOLO mode');
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
@@ -399,9 +381,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
const invocation = tool.build({ plan_filename: planRelativePath });
|
||||
|
||||
// Directly call execute to trigger the internal getAllowApprovalMode
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Default mode');
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
@@ -426,9 +406,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
approvalMode: mode,
|
||||
});
|
||||
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.llmContent).toContain(expected);
|
||||
};
|
||||
|
||||
@@ -462,7 +440,7 @@ Ask the user for specific feedback on how to improve the plan.`,
|
||||
});
|
||||
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: new AbortController().signal }),
|
||||
invocation.execute(new AbortController().signal),
|
||||
).rejects.toThrow(/Unexpected approval mode/);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
type ToolExitPlanModeConfirmationDetails,
|
||||
type ToolExitPlanModeConfirmationPayload,
|
||||
type ToolResult,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import path from 'node:path';
|
||||
@@ -183,7 +182,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
|
||||
return path.join(this.config.storage.getPlansDir(), safeFilename);
|
||||
}
|
||||
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
const resolvedPlanPath = this.getResolvedPlanPath();
|
||||
|
||||
if (this.planValidationError) {
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('GetInternalDocsTool (Integration)', () => {
|
||||
|
||||
it('should find the documentation root and list files', async () => {
|
||||
const invocation = tool.build({});
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
// Verify we found some files
|
||||
@@ -45,7 +45,7 @@ describe('GetInternalDocsTool (Integration)', () => {
|
||||
const expectedContent = await fs.readFile(expectedDocsPath, 'utf8');
|
||||
|
||||
const invocation = tool.build({ path: 'index.md' });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.llmContent).toBe(expectedContent);
|
||||
@@ -55,7 +55,7 @@ describe('GetInternalDocsTool (Integration)', () => {
|
||||
it('should prevent access to files outside the docs directory (Path Traversal)', async () => {
|
||||
// Attempt to read package.json from the root
|
||||
const invocation = tool.build({ path: '../package.json' });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.type).toBe(ToolErrorType.EXECUTION_FAILED);
|
||||
@@ -64,7 +64,7 @@ describe('GetInternalDocsTool (Integration)', () => {
|
||||
|
||||
it('should handle non-existent files', async () => {
|
||||
const invocation = tool.build({ path: 'this-file-does-not-exist.md' });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.type).toBe(ToolErrorType.EXECUTION_FAILED);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type ToolInvocation,
|
||||
type ToolResult,
|
||||
type ToolCallConfirmationDetails,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import { GET_INTERNAL_DOCS_TOOL_NAME } from './tool-names.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
@@ -97,7 +96,7 @@ class GetInternalDocsInvocation extends BaseToolInvocation<
|
||||
return 'Listing all available internal documentation.';
|
||||
}
|
||||
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
const docsRoot = await getDocsRoot();
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('GlobTool', () => {
|
||||
it('should find files matching a simple pattern in the root', async () => {
|
||||
const params: GlobToolParams = { pattern: '*.txt' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 2 file(s)');
|
||||
expect(result.llmContent).toContain(path.join(tempRootDir, 'fileA.txt'));
|
||||
expect(result.llmContent).toContain(path.join(tempRootDir, 'FileB.TXT'));
|
||||
@@ -121,7 +121,7 @@ describe('GlobTool', () => {
|
||||
it('should find files case-sensitively when case_sensitive is true', async () => {
|
||||
const params: GlobToolParams = { pattern: '*.txt', case_sensitive: true };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain(path.join(tempRootDir, 'fileA.txt'));
|
||||
expect(result.llmContent).not.toContain(
|
||||
@@ -133,7 +133,7 @@ describe('GlobTool', () => {
|
||||
const params: GlobToolParams = { pattern: '*.TXT' };
|
||||
const invocation = globTool.build(params);
|
||||
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('fileA.txt');
|
||||
expect(result.llmContent).toContain('FileB.TXT');
|
||||
@@ -145,7 +145,7 @@ describe('GlobTool', () => {
|
||||
case_sensitive: false,
|
||||
};
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 2 file(s)');
|
||||
expect(result.llmContent).toContain(path.join(tempRootDir, 'fileA.txt'));
|
||||
expect(result.llmContent).toContain(path.join(tempRootDir, 'FileB.TXT'));
|
||||
@@ -154,7 +154,7 @@ describe('GlobTool', () => {
|
||||
it('should find files using a pattern that includes a subdirectory', async () => {
|
||||
const params: GlobToolParams = { pattern: 'sub/*.md' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 2 file(s)');
|
||||
expect(result.llmContent).toContain(
|
||||
path.join(tempRootDir, 'sub', 'fileC.md'),
|
||||
@@ -167,7 +167,7 @@ describe('GlobTool', () => {
|
||||
it('should find files in a specified relative path (relative to rootDir)', async () => {
|
||||
const params: GlobToolParams = { pattern: '*.md', dir_path: 'sub' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 2 file(s)');
|
||||
expect(result.llmContent).toContain(
|
||||
path.join(tempRootDir, 'sub', 'fileC.md'),
|
||||
@@ -180,7 +180,7 @@ describe('GlobTool', () => {
|
||||
it('should find files using a deep globstar pattern (e.g., **/*.log)', async () => {
|
||||
const params: GlobToolParams = { pattern: '**/*.log' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain(
|
||||
path.join(tempRootDir, 'sub', 'deep', 'fileE.log'),
|
||||
@@ -190,7 +190,7 @@ describe('GlobTool', () => {
|
||||
it('should return "No files found" message when pattern matches nothing', async () => {
|
||||
const params: GlobToolParams = { pattern: '*.nonexistent' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'No files found matching pattern "*.nonexistent"',
|
||||
);
|
||||
@@ -201,7 +201,7 @@ describe('GlobTool', () => {
|
||||
await fs.writeFile(path.join(tempRootDir, 'file[1].txt'), 'content');
|
||||
const params: GlobToolParams = { pattern: 'file[1].txt' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain(
|
||||
path.join(tempRootDir, 'file[1].txt'),
|
||||
@@ -220,7 +220,7 @@ describe('GlobTool', () => {
|
||||
pattern: 'src/app/[test]/(dashboard)/testing/components/code.tsx',
|
||||
};
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain(filePath);
|
||||
}, 30000);
|
||||
@@ -228,7 +228,7 @@ describe('GlobTool', () => {
|
||||
it('should correctly sort files by modification time (newest first)', async () => {
|
||||
const params: GlobToolParams = { pattern: '*.sortme' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
const llmContent = partListUnionToString(result.llmContent);
|
||||
const newerIndex = llmContent.indexOf('newer.sortme');
|
||||
const olderIndex = llmContent.indexOf('older.sortme');
|
||||
@@ -244,7 +244,7 @@ describe('GlobTool', () => {
|
||||
vi.mocked(glob.glob).mockRejectedValue(new Error('Glob failed'));
|
||||
const params: GlobToolParams = { pattern: '*' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.error?.type).toBe(ToolErrorType.GLOB_EXECUTION_ERROR);
|
||||
}, 30000);
|
||||
});
|
||||
@@ -383,7 +383,7 @@ describe('GlobTool', () => {
|
||||
|
||||
const params: GlobToolParams = { pattern: '*_test.txt' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain('visible_test.txt');
|
||||
@@ -403,7 +403,7 @@ describe('GlobTool', () => {
|
||||
|
||||
const params: GlobToolParams = { pattern: 'visible_test.txt' };
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain('visible_test.txt');
|
||||
@@ -422,7 +422,7 @@ describe('GlobTool', () => {
|
||||
respect_git_ignore: false,
|
||||
};
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain('ignored_test.txt');
|
||||
@@ -443,7 +443,7 @@ describe('GlobTool', () => {
|
||||
respect_gemini_ignore: false,
|
||||
};
|
||||
const invocation = globTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 file(s)');
|
||||
expect(result.llmContent).toContain('gemini-ignored_test.txt');
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
type ToolResult,
|
||||
type PolicyUpdateOptions,
|
||||
type ToolConfirmationOutcome,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import { shortenPath, makeRelative } from '../utils/paths.js';
|
||||
import { type Config } from '../config/config.js';
|
||||
@@ -130,7 +129,7 @@ class GlobToolInvocation extends BaseToolInvocation<
|
||||
};
|
||||
}
|
||||
|
||||
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
const workspaceContext = this.config.getWorkspaceContext();
|
||||
const workspaceDirectories = workspaceContext.getDirectories();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { GrepTool, type GrepToolParams } from './grep.js';
|
||||
import type { ToolResult, GrepResult, ExecuteOptions } from './tools.js';
|
||||
import type { ToolResult, GrepResult } from './tools.js';
|
||||
import path from 'node:path';
|
||||
import { isSubpath } from '../utils/paths.js';
|
||||
import fs from 'node:fs/promises';
|
||||
@@ -176,7 +176,7 @@ describe('GrepTool', () => {
|
||||
it('should find matches for a simple pattern in all files', async () => {
|
||||
const params: GrepToolParams = { pattern: 'world' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 3 matches for pattern "world" in the workspace directory',
|
||||
);
|
||||
@@ -196,7 +196,7 @@ describe('GrepTool', () => {
|
||||
await fs.writeFile(path.join(tempRootDir, '..env'), 'world in ..env');
|
||||
const params: GrepToolParams = { pattern: 'world' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('File: ..env');
|
||||
expect(result.llmContent).toContain('L1: world in ..env');
|
||||
});
|
||||
@@ -209,13 +209,13 @@ describe('GrepTool', () => {
|
||||
const params: GrepToolParams = { pattern: 'hello' };
|
||||
const invocation = grepTool.build(params) as unknown as {
|
||||
isCommandAvailable: (command: string) => Promise<boolean>;
|
||||
execute: (options: ExecuteOptions) => Promise<ToolResult>;
|
||||
execute: (signal: AbortSignal) => Promise<ToolResult>;
|
||||
};
|
||||
invocation.isCommandAvailable = vi.fn(
|
||||
async (command: string) => command === 'grep',
|
||||
);
|
||||
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('File: ..env');
|
||||
expect(result.llmContent).toContain('L1: hello');
|
||||
expect(result.llmContent).not.toContain('secret.txt');
|
||||
@@ -224,7 +224,7 @@ describe('GrepTool', () => {
|
||||
it('should find matches in a specific path', async () => {
|
||||
const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 1 match for pattern "world" in path "sub"',
|
||||
);
|
||||
@@ -241,7 +241,7 @@ describe('GrepTool', () => {
|
||||
include_pattern: '*.js',
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 1 match for pattern "hello" in the workspace directory (filter: "*.js"):',
|
||||
);
|
||||
@@ -265,7 +265,7 @@ describe('GrepTool', () => {
|
||||
include_pattern: '*.js',
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 1 match for pattern "hello" in path "sub" (filter: "*.js")',
|
||||
);
|
||||
@@ -279,7 +279,7 @@ describe('GrepTool', () => {
|
||||
it('should return "No matches found" when pattern does not exist', async () => {
|
||||
const params: GrepToolParams = { pattern: 'nonexistentpattern' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'No matches found for pattern "nonexistentpattern" in the workspace directory.',
|
||||
);
|
||||
@@ -291,7 +291,7 @@ describe('GrepTool', () => {
|
||||
it('should handle regex special characters correctly', async () => {
|
||||
const params: GrepToolParams = { pattern: 'foo.*bar' }; // Matches 'const foo = "bar";'
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 1 match for pattern "foo.*bar" in the workspace directory:',
|
||||
);
|
||||
@@ -302,7 +302,7 @@ describe('GrepTool', () => {
|
||||
it('should be case-insensitive by default (JS fallback)', async () => {
|
||||
const params: GrepToolParams = { pattern: 'HELLO' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 2 matches for pattern "HELLO" in the workspace directory:',
|
||||
);
|
||||
@@ -325,7 +325,7 @@ describe('GrepTool', () => {
|
||||
vi.mocked(glob.globStream).mockRejectedValue(new Error('Glob failed'));
|
||||
const params: GrepToolParams = { pattern: 'hello' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.error?.type).toBe(ToolErrorType.GREP_EXECUTION_ERROR);
|
||||
vi.mocked(glob.globStream).mockReset();
|
||||
}, 30000);
|
||||
@@ -390,7 +390,7 @@ describe('GrepTool', () => {
|
||||
);
|
||||
const params: GrepToolParams = { pattern: 'world' };
|
||||
const invocation = multiDirGrepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
// Should find matches in both directories
|
||||
expect(result.llmContent).toContain(
|
||||
@@ -476,7 +476,7 @@ describe('GrepTool', () => {
|
||||
// Search only in the 'sub' directory of the first workspace
|
||||
const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' };
|
||||
const invocation = multiDirGrepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
// Should only find matches in the specified sub directory
|
||||
expect(result.llmContent).toContain(
|
||||
@@ -499,7 +499,7 @@ describe('GrepTool', () => {
|
||||
total_max_matches: 2,
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 2 matches');
|
||||
expect(result.llmContent).toContain(
|
||||
@@ -522,7 +522,7 @@ describe('GrepTool', () => {
|
||||
max_matches_per_file: 1,
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
// fileA.txt has 2 worlds, but should only return 1.
|
||||
// sub/fileC.txt has 1 world, so total matches = 2.
|
||||
@@ -544,7 +544,7 @@ describe('GrepTool', () => {
|
||||
names_only: true,
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 2 files with matches');
|
||||
expect(result.llmContent).toContain('fileA.txt');
|
||||
@@ -565,7 +565,7 @@ describe('GrepTool', () => {
|
||||
dir_path: '.',
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
expect(result.llmContent).toContain('copyright.txt');
|
||||
@@ -585,7 +585,7 @@ describe('GrepTool', () => {
|
||||
|
||||
const params: GrepToolParams = { pattern: 'Target match' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 1 match for pattern "Target match"',
|
||||
@@ -607,7 +607,7 @@ describe('GrepTool', () => {
|
||||
|
||||
const params: GrepToolParams = { pattern: 'Target match' };
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
// MAX_LINE_LENGTH_TEXT_FILE is 2000. It should be truncated.
|
||||
expect(result.llmContent).toContain('... [truncated]');
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
type ToolResult,
|
||||
type PolicyUpdateOptions,
|
||||
type ToolConfirmationOutcome,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||
import { getErrorMessage, isNodeError } from '../utils/errors.js';
|
||||
@@ -139,7 +138,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
return null;
|
||||
}
|
||||
|
||||
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
const workspaceContext = this.config.getWorkspaceContext();
|
||||
const pathParam = this.params.dir_path;
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('Line Ending Preservation', () => {
|
||||
await confirmDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
||||
}
|
||||
|
||||
await invocation.execute({ abortSignal });
|
||||
await invocation.execute(abortSignal);
|
||||
|
||||
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
||||
// Expect all newlines to be CRLF
|
||||
@@ -217,7 +217,7 @@ describe('Line Ending Preservation', () => {
|
||||
await confirmDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
||||
}
|
||||
|
||||
await invocation.execute({ abortSignal });
|
||||
await invocation.execute(abortSignal);
|
||||
|
||||
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
@@ -265,7 +265,7 @@ describe('Line Ending Preservation', () => {
|
||||
await confirmDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
||||
}
|
||||
|
||||
await invocation.execute({ abortSignal });
|
||||
await invocation.execute(abortSignal);
|
||||
|
||||
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('LSTool', () => {
|
||||
);
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('[DIR] subdir');
|
||||
expect(result.llmContent).toContain('file1.txt');
|
||||
@@ -146,7 +146,7 @@ describe('LSTool', () => {
|
||||
);
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempSecondaryDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('secondary-file.txt');
|
||||
expect(result.returnDisplay).toEqual({
|
||||
@@ -159,7 +159,7 @@ describe('LSTool', () => {
|
||||
const emptyDir = path.join(tempRootDir, 'empty');
|
||||
await fs.mkdir(emptyDir);
|
||||
const invocation = lsTool.build({ dir_path: emptyDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toBe(`Directory ${emptyDir} is empty.`);
|
||||
expect(result.returnDisplay).toBe('Directory is empty.');
|
||||
@@ -173,7 +173,7 @@ describe('LSTool', () => {
|
||||
dir_path: tempRootDir,
|
||||
ignore: ['*.log'],
|
||||
});
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('file1.txt');
|
||||
expect(result.llmContent).not.toContain('file2.log');
|
||||
@@ -189,7 +189,7 @@ describe('LSTool', () => {
|
||||
await fs.writeFile(path.join(tempRootDir, '.git'), '');
|
||||
await fs.writeFile(path.join(tempRootDir, '.gitignore'), '*.log');
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('file1.txt');
|
||||
expect(result.llmContent).not.toContain('file2.log');
|
||||
@@ -207,7 +207,7 @@ describe('LSTool', () => {
|
||||
'*.log',
|
||||
);
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('file1.txt');
|
||||
expect(result.llmContent).not.toContain('file2.log');
|
||||
@@ -221,7 +221,7 @@ describe('LSTool', () => {
|
||||
await fs.writeFile(testPath, 'content1');
|
||||
|
||||
const invocation = lsTool.build({ dir_path: testPath });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Path is not a directory');
|
||||
expect(result.returnDisplay).toBe('Error: Path is not a directory.');
|
||||
@@ -231,7 +231,7 @@ describe('LSTool', () => {
|
||||
it('should handle non-existent paths', async () => {
|
||||
const testPath = path.join(tempRootDir, 'does-not-exist');
|
||||
const invocation = lsTool.build({ dir_path: testPath });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Error listing directory');
|
||||
expect(result.returnDisplay).toBe('Error: Failed to list directory.');
|
||||
@@ -245,7 +245,7 @@ describe('LSTool', () => {
|
||||
await fs.mkdir(path.join(tempRootDir, 'y-dir'));
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
const lines = (
|
||||
typeof result.llmContent === 'string' ? result.llmContent : ''
|
||||
@@ -270,7 +270,7 @@ describe('LSTool', () => {
|
||||
vi.spyOn(fs, 'readdir').mockRejectedValueOnce(error);
|
||||
|
||||
const invocation = lsTool.build({ dir_path: restrictedDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Error listing directory');
|
||||
expect(result.llmContent).toContain('permission denied');
|
||||
@@ -295,7 +295,7 @@ describe('LSTool', () => {
|
||||
});
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
// Should still list the other files
|
||||
expect(result.llmContent).toContain('file1.txt');
|
||||
@@ -360,7 +360,7 @@ describe('LSTool', () => {
|
||||
);
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempSecondaryDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('secondary-file.txt');
|
||||
expect(result.returnDisplay).toEqual({
|
||||
@@ -378,7 +378,7 @@ describe('LSTool', () => {
|
||||
await fs.writeFile(path.join(tempRootDir, 'jit-file.txt'), 'content');
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(discoverJitContext).toHaveBeenCalled();
|
||||
expect(result.llmContent).toContain('Newly Discovered Project Context');
|
||||
@@ -395,7 +395,7 @@ describe('LSTool', () => {
|
||||
);
|
||||
|
||||
const invocation = lsTool.build({ dir_path: tempRootDir });
|
||||
const result = await invocation.execute({ abortSignal });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).not.toContain(
|
||||
'Newly Discovered Project Context',
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type ToolResult,
|
||||
type PolicyUpdateOptions,
|
||||
type ToolConfirmationOutcome,
|
||||
type ExecuteOptions,
|
||||
} from './tools.js';
|
||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
@@ -156,7 +155,7 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
|
||||
* Executes the LS operation with the given parameters
|
||||
* @returns Result of the LS operation
|
||||
*/
|
||||
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
const resolvedDirPath = path.resolve(
|
||||
this.config.getTargetDir(),
|
||||
this.params.dir_path,
|
||||
|
||||
@@ -812,7 +812,7 @@ type StatusChangeListener = (
|
||||
serverName: string,
|
||||
status: MCPServerStatus,
|
||||
) => void;
|
||||
const statusChangeListeners: Set<StatusChangeListener> = new Set();
|
||||
const statusChangeListeners: StatusChangeListener[] = [];
|
||||
|
||||
/**
|
||||
* Add a listener for MCP server status changes
|
||||
@@ -820,7 +820,7 @@ const statusChangeListeners: Set<StatusChangeListener> = new Set();
|
||||
export function addMCPStatusChangeListener(
|
||||
listener: StatusChangeListener,
|
||||
): void {
|
||||
statusChangeListeners.add(listener);
|
||||
statusChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -829,7 +829,10 @@ export function addMCPStatusChangeListener(
|
||||
export function removeMCPStatusChangeListener(
|
||||
listener: StatusChangeListener,
|
||||
): void {
|
||||
statusChangeListeners.delete(listener);
|
||||
const index = statusChangeListeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
statusChangeListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -240,9 +240,9 @@ describe('DiscoveredMCPTool', () => {
|
||||
mockCallTool.mockResolvedValue(mockMcpToolResponseParts);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult: ToolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult: ToolResult = await invocation.execute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
expect(mockCallTool).toHaveBeenCalledWith([
|
||||
{ name: serverToolName, args: params },
|
||||
@@ -262,9 +262,9 @@ describe('DiscoveredMCPTool', () => {
|
||||
const mockMcpToolResponsePartsEmpty: Part[] = [];
|
||||
mockCallTool.mockResolvedValue(mockMcpToolResponsePartsEmpty);
|
||||
const invocation = tool.build(params);
|
||||
const toolResult: ToolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult: ToolResult = await invocation.execute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(toolResult.returnDisplay).toBe('```json\n[]\n```');
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{ text: '[Error: Could not parse tool response]' },
|
||||
@@ -278,7 +278,7 @@ describe('DiscoveredMCPTool', () => {
|
||||
|
||||
const invocation = tool.build(params);
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: new AbortController().signal }),
|
||||
invocation.execute(new AbortController().signal),
|
||||
).rejects.toThrow(expectedError);
|
||||
});
|
||||
|
||||
@@ -324,9 +324,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
functionCall,
|
||||
)} with response: ${safeJsonStringify(mockMcpToolResponseParts)}`;
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error?.type).toBe(ToolErrorType.MCP_TOOL_ERROR);
|
||||
expect(result.llmContent).toBe(expectedErrorMessage);
|
||||
expect(result.returnDisplay).toContain(
|
||||
@@ -371,9 +370,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
functionCall,
|
||||
)} with response: ${safeJsonStringify(mockMcpToolResponseParts)}`;
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.error?.type).toBe(ToolErrorType.MCP_TOOL_ERROR);
|
||||
expect(result.llmContent).toBe(expectedErrorMessage);
|
||||
expect(result.returnDisplay).toContain(
|
||||
@@ -428,9 +426,10 @@ describe('DiscoveredMCPTool', () => {
|
||||
mockCallTool.mockResolvedValue(mockMcpToolResponseParts);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
const stringifiedResponseContent = JSON.stringify(
|
||||
mockToolSuccessResultObject,
|
||||
);
|
||||
@@ -452,9 +451,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
// 1. Assert that the llmContent sent to the scheduler is a clean Part array.
|
||||
expect(toolResult.llmContent).toEqual([{ text: successMessage }]);
|
||||
|
||||
@@ -482,9 +480,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{
|
||||
text: `[Tool '${serverToolName}' provided the following audio data with mime-type: audio/mp3]`,
|
||||
@@ -515,9 +512,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{
|
||||
text: 'Resource Link: My Resource at file:///path/to/thing',
|
||||
@@ -546,9 +542,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{ text: 'This is the text content.' },
|
||||
]);
|
||||
@@ -573,9 +568,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{
|
||||
text: `[Tool '${serverToolName}' provided the following embedded resource with mime-type: application/octet-stream]`,
|
||||
@@ -609,9 +603,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{ text: 'First part.' },
|
||||
{
|
||||
@@ -642,9 +635,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([{ text: 'Valid part.' }]);
|
||||
expect(toolResult.returnDisplay).toBe(
|
||||
'Valid part.\n[Unknown content type: future_block]',
|
||||
@@ -681,9 +673,8 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const toolResult = await invocation.execute({
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
const toolResult = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(toolResult.llmContent).toEqual([
|
||||
{ text: 'Here is a resource.' },
|
||||
{
|
||||
@@ -716,9 +707,9 @@ describe('DiscoveredMCPTool', () => {
|
||||
|
||||
const invocation = tool.build(params);
|
||||
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: controller.signal }),
|
||||
).rejects.toThrow('Tool call aborted');
|
||||
await expect(invocation.execute(controller.signal)).rejects.toThrow(
|
||||
'Tool call aborted',
|
||||
);
|
||||
|
||||
// Tool should not be called if signal is already aborted
|
||||
expect(mockCallTool).not.toHaveBeenCalled();
|
||||
@@ -748,7 +739,7 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const promise = invocation.execute({ abortSignal: controller.signal });
|
||||
const promise = invocation.execute(controller.signal);
|
||||
|
||||
// Abort after a short delay to simulate cancellation during execution
|
||||
setTimeout(() => controller.abort(), ABORT_DELAY);
|
||||
@@ -767,9 +758,7 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
const result = await invocation.execute(controller.signal);
|
||||
|
||||
expect(result.llmContent).toEqual([{ text: 'Success' }]);
|
||||
expect(result.returnDisplay).toBe('Success');
|
||||
@@ -787,9 +776,7 @@ describe('DiscoveredMCPTool', () => {
|
||||
);
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute({
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
const result = await invocation.execute(controller.signal);
|
||||
|
||||
expect(result.error?.type).toBe(ToolErrorType.MCP_TOOL_ERROR);
|
||||
expect(result.returnDisplay).toContain(
|
||||
@@ -806,9 +793,9 @@ describe('DiscoveredMCPTool', () => {
|
||||
|
||||
const invocation = tool.build(params);
|
||||
|
||||
await expect(
|
||||
invocation.execute({ abortSignal: controller.signal }),
|
||||
).rejects.toThrow(expectedError);
|
||||
await expect(invocation.execute(controller.signal)).rejects.toThrow(
|
||||
expectedError,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -842,12 +829,12 @@ describe('DiscoveredMCPTool', () => {
|
||||
|
||||
if (expectError) {
|
||||
try {
|
||||
await invocation.execute({ abortSignal: controller.signal });
|
||||
await invocation.execute(controller.signal);
|
||||
} catch {
|
||||
// Expected error
|
||||
}
|
||||
} else {
|
||||
await invocation.execute({ abortSignal: controller.signal });
|
||||
await invocation.execute(controller.signal);
|
||||
}
|
||||
|
||||
// Verify cleanup by aborting after execution
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user