Skip to content

Commit 2fa55bd

Browse files
authored
Merge branch 'main' into 23740-chat-recording-jsonl-streaming
2 parents f606a3f + b238a45 commit 2fa55bd

49 files changed

Lines changed: 931 additions & 344 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/core/subagents.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,24 @@ field.
521521
}
522522
```
523523

524+
#### Safety policies (TOML)
525+
526+
You can restrict access to specific subagents using the CLI's **Policy Engine**.
527+
Subagents are treated as virtual tool names for policy matching purposes.
528+
529+
To govern access to a subagent, create a `.toml` file in your policy directory
530+
(e.g., `~/.gemini/policies/`):
531+
532+
```toml
533+
[[rule]]
534+
toolName = "codebase_investigator"
535+
decision = "deny"
536+
deny_message = "Deep codebase analysis is restricted for this session."
537+
```
538+
539+
For more information on setting up fine-grained safety guardrails, see the
540+
[Policy Engine reference](../reference/policy-engine.md#special-syntax-for-subagents).
541+
524542
### Optimizing your subagent
525543

526544
The main agent's system prompt encourages it to use an expert subagent when one

docs/reference/keyboard-shortcuts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ available combinations.
9191
| `input.submit` | Submit the current prompt. | `Enter` |
9292
| `input.queueMessage` | Queue the current prompt to be processed after the current task finishes. | `Tab` |
9393
| `input.newline` | Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
94-
| `input.openExternalEditor` | Open the current prompt or the plan in an external editor. | `Ctrl+G` |
94+
| `input.openExternalEditor` | Open the current prompt or the plan in an external editor. | `Ctrl+G`<br />`Ctrl+Shift+G` |
9595
| `input.deprecatedOpenExternalEditor` | Deprecated command to open external editor. | `Ctrl+X` |
9696
| `input.paste` | Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
9797

docs/reference/policy-engine.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,33 @@ decision = "ask_user"
438438
priority = 10
439439
```
440440
441+
### Special syntax for subagents
442+
443+
You can secure and govern subagents using standard policy rules by treating the
444+
subagent's name as the `toolName`.
445+
446+
When the main agent invokes a subagent (e.g., using the unified `invoke_agent`
447+
tool), the Policy Engine automatically treats the target `agent_name` as a
448+
virtual tool alias for rule matching.
449+
450+
**Example:**
451+
452+
This rule denies access to the `codebase_investigator` subagent.
453+
454+
```toml
455+
[[rule]]
456+
toolName = "codebase_investigator"
457+
decision = "deny"
458+
priority = 500
459+
deny_message = "Deep codebase analysis is restricted for this session."
460+
```
461+
462+
- **Backward Compatibility**: Any rules written targeting historical 1:1
463+
subagent tool names will continue to match transparently.
464+
- **Context differentiation**: To create rules based on **who** is calling a
465+
tool, use the `subagent` field instead. See
466+
[TOML rule schema](#toml-rule-schema).
467+
441468
## Default policies
442469
443470
The Gemini CLI ships with a set of default policies to provide a safe

evals/subagents.eval.ts

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,46 @@ import path from 'node:path';
99

1010
import { describe, expect } from 'vitest';
1111

12-
import { evalTest, TEST_AGENTS } from './test-helper.js';
12+
import { AGENT_TOOL_NAME } from '@google/gemini-cli-core';
13+
import { evalTest, TEST_AGENTS, TestRig } from './test-helper.js';
1314

1415
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;\n';
1516

17+
/**
18+
* Helper to verify that a specific subagent was successfully invoked via the unified tool.
19+
*/
20+
async function expectSubagentCall(rig: TestRig, agentName: string) {
21+
await rig.expectToolCallSuccess(
22+
[AGENT_TOOL_NAME],
23+
undefined,
24+
(args: string) => {
25+
try {
26+
const parsed = JSON.parse(args);
27+
return parsed.agent_name === agentName;
28+
} catch {
29+
return false;
30+
}
31+
},
32+
);
33+
}
34+
35+
/**
36+
* Helper to check if a subagent (either via unified tool or direct name) was called.
37+
*/
38+
function isSubagentCalled(toolLogs: any[], agentName: string): boolean {
39+
return toolLogs.some((l) => {
40+
if (l.toolRequest.name === AGENT_TOOL_NAME) {
41+
try {
42+
const args = JSON.parse(l.toolRequest.args);
43+
return args.agent_name === agentName;
44+
} catch {
45+
return false;
46+
}
47+
}
48+
return l.toolRequest.name === agentName;
49+
});
50+
}
51+
1652
// A minimal package.json is used to provide a realistic workspace anchor.
1753
// This prevents the agent from making incorrect assumptions about the environment
1854
// and helps it properly navigate or act as if it is in a standard Node.js project.
@@ -62,7 +98,7 @@ describe('subagent eval test cases', () => {
6298
'README.md': 'TODO: update the README.\n',
6399
},
64100
assert: async (rig, _result) => {
65-
await rig.expectToolCallSuccess([TEST_AGENTS.DOCS_AGENT.name]);
101+
await expectSubagentCall(rig, TEST_AGENTS.DOCS_AGENT.name);
66102
},
67103
});
68104

@@ -99,14 +135,10 @@ describe('subagent eval test cases', () => {
99135
}>;
100136

101137
expect(updatedIndex).toContain('export const sum =');
102-
expect(
103-
toolLogs.some(
104-
(l) => l.toolRequest.name === TEST_AGENTS.DOCS_AGENT.name,
105-
),
106-
).toBe(false);
107-
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
138+
expect(isSubagentCalled(toolLogs, TEST_AGENTS.DOCS_AGENT.name)).toBe(
108139
false,
109140
);
141+
expect(isSubagentCalled(toolLogs, 'generalist')).toBe(false);
110142
},
111143
});
112144

@@ -140,13 +172,11 @@ describe('subagent eval test cases', () => {
140172
},
141173
assert: async (rig, _result) => {
142174
const toolLogs = rig.readToolLogs() as Array<{
143-
toolRequest: { name: string };
175+
toolRequest: { name: string; args: string };
144176
}>;
145177

146-
await rig.expectToolCallSuccess([TEST_AGENTS.TESTING_AGENT.name]);
147-
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
148-
false,
149-
);
178+
await expectSubagentCall(rig, TEST_AGENTS.TESTING_AGENT.name);
179+
expect(isSubagentCalled(toolLogs, 'generalist')).toBe(false);
150180
},
151181
});
152182

@@ -181,18 +211,15 @@ describe('subagent eval test cases', () => {
181211
},
182212
assert: async (rig, _result) => {
183213
const toolLogs = rig.readToolLogs() as Array<{
184-
toolRequest: { name: string };
214+
toolRequest: { name: string; args: string };
185215
}>;
186216
const readme = readProjectFile(rig, 'README.md');
187217

188-
await rig.expectToolCallSuccess([
189-
TEST_AGENTS.DOCS_AGENT.name,
190-
TEST_AGENTS.TESTING_AGENT.name,
191-
]);
218+
await expectSubagentCall(rig, TEST_AGENTS.DOCS_AGENT.name);
219+
await expectSubagentCall(rig, TEST_AGENTS.TESTING_AGENT.name);
220+
192221
expect(readme).not.toContain('TODO: update the README.');
193-
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
194-
false,
195-
);
222+
expect(isSubagentCalled(toolLogs, 'generalist')).toBe(false);
196223
},
197224
});
198225

@@ -219,14 +246,11 @@ describe('subagent eval test cases', () => {
219246
'package.json': MOCK_PACKAGE_JSON,
220247
},
221248
assert: async (rig, _result) => {
222-
const toolLogs = rig.readToolLogs() as Array<{
223-
toolRequest: { name: string };
224-
}>;
225-
await rig.expectToolCallSuccess(['database-agent']);
249+
const toolLogs = rig.readToolLogs();
250+
await expectSubagentCall(rig, TEST_AGENTS.DATABASE_AGENT.name);
226251

227252
// Ensure the generalist and other irrelevant specialists were not invoked
228253
const uncalledAgents = [
229-
'generalist',
230254
TEST_AGENTS.DOCS_AGENT.name,
231255
TEST_AGENTS.TESTING_AGENT.name,
232256
TEST_AGENTS.CSS_AGENT.name,
@@ -239,10 +263,9 @@ describe('subagent eval test cases', () => {
239263
];
240264

241265
for (const agentName of uncalledAgents) {
242-
expect(toolLogs.some((l) => l.toolRequest.name === agentName)).toBe(
243-
false,
244-
);
266+
expect(isSubagentCalled(toolLogs, agentName)).toBe(false);
245267
}
268+
expect(isSubagentCalled(toolLogs, 'generalist')).toBe(false);
246269
},
247270
});
248271

@@ -274,14 +297,11 @@ describe('subagent eval test cases', () => {
274297
'package.json': MOCK_PACKAGE_JSON,
275298
},
276299
assert: async (rig, _result) => {
277-
const toolLogs = rig.readToolLogs() as Array<{
278-
toolRequest: { name: string };
279-
}>;
280-
await rig.expectToolCallSuccess(['database-agent']);
300+
const toolLogs = rig.readToolLogs();
301+
await expectSubagentCall(rig, TEST_AGENTS.DATABASE_AGENT.name);
281302

282303
// Ensure the generalist and other irrelevant specialists were not invoked
283304
const uncalledAgents = [
284-
'generalist',
285305
TEST_AGENTS.DOCS_AGENT.name,
286306
TEST_AGENTS.TESTING_AGENT.name,
287307
TEST_AGENTS.CSS_AGENT.name,
@@ -294,10 +314,9 @@ describe('subagent eval test cases', () => {
294314
];
295315

296316
for (const agentName of uncalledAgents) {
297-
expect(toolLogs.some((l) => l.toolRequest.name === agentName)).toBe(
298-
false,
299-
);
317+
expect(isSubagentCalled(toolLogs, agentName)).toBe(false);
300318
}
319+
expect(isSubagentCalled(toolLogs, 'generalist')).toBe(false);
301320
},
302321
});
303322
});

integration-tests/browser-agent-localhost.dynamic.responses

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll check the dynamic content page on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/dynamic.html, wait for the dynamic content to load, then capture the accessibility tree and report what content appeared"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
1+
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll check the dynamic content page on the localhost server."},{"functionCall":{"name":"invoke_agent","args":{"agent_name":"browser_agent","prompt":"Navigate to http://127.0.0.1:18923/dynamic.html, wait for the dynamic content to load, then capture the accessibility tree and report what content appeared"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
22
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/dynamic.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
33
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"wait_for","args":{"selector":"#dynamic-content","state":"visible","timeout":5000}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]}
44
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":15,"totalTokenCount":195}}]}

integration-tests/browser-agent-localhost.form.responses

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll fill out the contact form on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/form.html, fill in the name field with 'Test User', the email field with 'test@example.com', the message field with 'Hello World', and submit the form"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
1+
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll fill out the contact form on the localhost server."},{"functionCall":{"name":"invoke_agent","args":{"agent_name":"browser_agent","prompt":"Navigate to http://127.0.0.1:18923/form.html, fill in the name field with 'Test User', the email field with 'test@example.com', the message field with 'Hello World', and submit the form"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
22
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/form.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
33
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#name","value":"Test User"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]}
44
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#email","value":"test@example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":25,"totalTokenCount":205}}]}

integration-tests/browser-agent-localhost.multistep.responses

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll go through the multi-step flow on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/multi-step/step1.html, fill in 'testuser' as the username, click Next, then on step 2 select 'Option B' and click Finish. Report the final result page content."}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
1+
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll go through the multi-step flow on the localhost server."},{"functionCall":{"name":"invoke_agent","args":{"agent_name":"browser_agent","prompt":"Navigate to http://127.0.0.1:18923/multi-step/step1.html, fill in 'testuser' as the username, click Next, then on step 2 select 'Option B' and click Finish. Report the final result page content."}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
22
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/multi-step/step1.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
33
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#username","value":"testuser"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]}
44
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#next-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":20,"totalTokenCount":200}}]}

integration-tests/browser-agent-localhost.navigate.responses

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to the localhost page and read its content using the browser agent."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/index.html and tell me the page title and list all links on the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
1+
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to the localhost page and read its content using the browser agent."},{"functionCall":{"name":"invoke_agent","args":{"agent_name":"browser_agent","prompt":"Navigate to http://127.0.0.1:18923/index.html and tell me the page title and list all links on the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
22
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/index.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
33
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
44
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Page title is 'Test Fixture - Home'. Found 3 links: Contact Form (/form.html), Multi-Step Flow (/multi-step/step1.html), Dynamic Content (/dynamic.html)."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}

integration-tests/browser-agent-localhost.screenshot.responses

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll take a screenshot of the localhost test page."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/index.html and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
1+
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll take a screenshot of the localhost test page."},{"functionCall":{"name":"invoke_agent","args":{"agent_name":"browser_agent","prompt":"Navigate to http://127.0.0.1:18923/index.html and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
22
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/index.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
33
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":15,"totalTokenCount":165}}]}
44
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Screenshot captured of the localhost test fixture home page showing the heading, navigation links, and footer.","data":{"screenshotTaken":true}}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}

0 commit comments

Comments
 (0)