Skip to content

Commit 7337dbd

Browse files
joshualittruomengz
authored andcommitted
feat(core): Thread AgentLoopContext through core. (#21944)
1 parent 8f96a38 commit 7337dbd

30 files changed

+272
-125
lines changed

packages/a2a-server/src/agent/task.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ export class Task {
542542
const messageBus = this.config.getMessageBus();
543543
const scheduler = new Scheduler({
544544
schedulerId: this.id,
545-
config: this.config,
545+
context: this.config,
546546
messageBus,
547547
getPreferredEditor: () => DEFAULT_GUI_EDITOR,
548548
});

packages/cli/src/nonInteractiveCli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export async function runNonInteractive({
211211

212212
const geminiClient = config.getGeminiClient();
213213
const scheduler = new Scheduler({
214-
config,
214+
context: config,
215215
messageBus: config.getMessageBus(),
216216
getPreferredEditor: () => undefined,
217217
schedulerId: ROOT_SCHEDULER_ID,

packages/cli/src/ui/hooks/useToolScheduler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export function useToolScheduler(
9797
const scheduler = useMemo(
9898
() =>
9999
new Scheduler({
100-
config,
100+
context: config,
101101
messageBus,
102102
getPreferredEditor: () => getPreferredEditorRef.current(),
103103
schedulerId: ROOT_SCHEDULER_ID,

packages/core/src/agents/agent-scheduler.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ vi.mock('../scheduler/scheduler.js', () => ({
2020

2121
describe('agent-scheduler', () => {
2222
let mockToolRegistry: Mocked<ToolRegistry>;
23+
let mockConfig: Mocked<Config>;
2324
let mockMessageBus: Mocked<MessageBus>;
2425

2526
beforeEach(() => {
@@ -29,6 +30,14 @@ describe('agent-scheduler', () => {
2930
getTool: vi.fn(),
3031
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
3132
} as unknown as Mocked<ToolRegistry>;
33+
mockConfig = {
34+
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
35+
toolRegistry: mockToolRegistry,
36+
} as unknown as Mocked<Config>;
37+
(mockConfig as unknown as { messageBus: MessageBus }).messageBus =
38+
mockMessageBus;
39+
(mockConfig as unknown as { toolRegistry: ToolRegistry }).toolRegistry =
40+
mockToolRegistry;
3241
});
3342

3443
it('should create a scheduler with agent-specific config', async () => {
@@ -69,7 +78,8 @@ describe('agent-scheduler', () => {
6978
}),
7079
);
7180

72-
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config;
81+
// Verify that the scheduler's context has the overridden tool registry
82+
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].context;
7383
expect(schedulerConfig.toolRegistry).toBe(mockToolRegistry);
7484
});
7585

@@ -106,9 +116,8 @@ describe('agent-scheduler', () => {
106116
},
107117
);
108118

109-
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config;
119+
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].context;
110120
expect(schedulerConfig.toolRegistry).toBe(agentRegistry);
111121
expect(schedulerConfig.toolRegistry).not.toBe(mainRegistry);
112-
expect(schedulerConfig.getToolRegistry()).toBe(agentRegistry);
113122
});
114123
});

packages/core/src/agents/agent-scheduler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function scheduleAgentTools(
6565
});
6666

6767
const scheduler = new Scheduler({
68-
config: agentConfig,
68+
context: agentConfig,
6969
messageBus: toolRegistry.getMessageBus(),
7070
getPreferredEditor: getPreferredEditor ?? (() => undefined),
7171
schedulerId,

packages/core/src/agents/browser/browserAgentInvocation.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { randomUUID } from 'node:crypto';
1818
import type { Config } from '../../config/config.js';
19+
import { type AgentLoopContext } from '../../config/agent-loop-context.js';
1920
import { LocalAgentExecutor } from '../local-executor.js';
2021
import { safeJsonToMarkdown } from '../../utils/markdownUtils.js';
2122
import {
@@ -179,7 +180,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
179180
ToolResult
180181
> {
181182
constructor(
182-
private readonly config: Config,
183+
private readonly context: AgentLoopContext,
183184
params: AgentInputs,
184185
messageBus: MessageBus,
185186
_toolName?: string,
@@ -194,6 +195,10 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
194195
);
195196
}
196197

198+
private get config(): Config {
199+
return this.context.config;
200+
}
201+
197202
/**
198203
* Returns a concise, human-readable description of the invocation.
199204
*/
@@ -409,7 +414,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
409414
// Create and run executor with the configured definition
410415
const executor = await LocalAgentExecutor.create(
411416
definition,
412-
this.config,
417+
this.context,
413418
onActivity,
414419
);
415420

packages/core/src/agents/local-executor.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ describe('LocalAgentExecutor', () => {
307307
vi.useFakeTimers();
308308

309309
mockConfig = makeFakeConfig();
310+
// .config is already set correctly by the getter on the instance.
311+
Object.defineProperty(mockConfig, 'promptId', {
312+
get: () => 'test-prompt-id',
313+
configurable: true,
314+
});
310315
parentToolRegistry = new ToolRegistry(
311316
mockConfig,
312317
mockConfig.getMessageBus(),
@@ -319,7 +324,9 @@ describe('LocalAgentExecutor', () => {
319324
);
320325
parentToolRegistry.registerTool(MOCK_TOOL_NOT_ALLOWED);
321326

322-
vi.spyOn(mockConfig, 'getToolRegistry').mockReturnValue(parentToolRegistry);
327+
vi.spyOn(mockConfig, 'toolRegistry', 'get').mockReturnValue(
328+
parentToolRegistry,
329+
);
323330
vi.spyOn(mockConfig, 'getAgentRegistry').mockReturnValue({
324331
getAllAgentNames: () => [],
325332
} as unknown as AgentRegistry);
@@ -382,7 +389,10 @@ describe('LocalAgentExecutor', () => {
382389

383390
it('should use parentPromptId from context to create agentId', async () => {
384391
const parentId = 'parent-id';
385-
mockedPromptIdContext.getStore.mockReturnValue(parentId);
392+
Object.defineProperty(mockConfig, 'promptId', {
393+
get: () => parentId,
394+
configurable: true,
395+
});
386396

387397
const definition = createTestDefinition();
388398
const executor = await LocalAgentExecutor.create(
@@ -2052,7 +2062,7 @@ describe('LocalAgentExecutor', () => {
20522062
vi.spyOn(configWithHints, 'getAgentRegistry').mockReturnValue({
20532063
getAllAgentNames: () => [],
20542064
} as unknown as AgentRegistry);
2055-
vi.spyOn(configWithHints, 'getToolRegistry').mockReturnValue(
2065+
vi.spyOn(configWithHints, 'toolRegistry', 'get').mockReturnValue(
20562066
parentToolRegistry,
20572067
);
20582068
});

packages/core/src/agents/local-executor.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import type { Config } from '../config/config.js';
8+
import { type AgentLoopContext } from '../config/agent-loop-context.js';
89
import { reportError } from '../utils/errorReporting.js';
910
import { GeminiChat, StreamEventType } from '../core/geminiChat.js';
1011
import {
@@ -92,29 +93,33 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
9293

9394
private readonly agentId: string;
9495
private readonly toolRegistry: ToolRegistry;
95-
private readonly runtimeContext: Config;
96+
private readonly context: AgentLoopContext;
9697
private readonly onActivity?: ActivityCallback;
9798
private readonly compressionService: ChatCompressionService;
9899
private readonly parentCallId?: string;
99100
private hasFailedCompressionAttempt = false;
100101

102+
private get config(): Config {
103+
return this.context.config;
104+
}
105+
101106
/**
102107
* Creates and validates a new `AgentExecutor` instance.
103108
*
104109
* This method ensures that all tools specified in the agent's definition are
105110
* safe for non-interactive use before creating the executor.
106111
*
107112
* @param definition The definition object for the agent.
108-
* @param runtimeContext The global runtime configuration.
113+
* @param context The execution context.
109114
* @param onActivity An optional callback to receive activity events.
110115
* @returns A promise that resolves to a new `LocalAgentExecutor` instance.
111116
*/
112117
static async create<TOutput extends z.ZodTypeAny>(
113118
definition: LocalAgentDefinition<TOutput>,
114-
runtimeContext: Config,
119+
context: AgentLoopContext,
115120
onActivity?: ActivityCallback,
116121
): Promise<LocalAgentExecutor<TOutput>> {
117-
const parentMessageBus = runtimeContext.getMessageBus();
122+
const parentMessageBus = context.messageBus;
118123

119124
// Create an override object to inject the subagent name into tool confirmation requests
120125
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
@@ -133,12 +138,12 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
133138

134139
// Create an isolated tool registry for this agent instance.
135140
const agentToolRegistry = new ToolRegistry(
136-
runtimeContext,
141+
context.config,
137142
subagentMessageBus,
138143
);
139-
const parentToolRegistry = runtimeContext.getToolRegistry();
144+
const parentToolRegistry = context.toolRegistry;
140145
const allAgentNames = new Set(
141-
runtimeContext.getAgentRegistry().getAllAgentNames(),
146+
context.config.getAgentRegistry().getAllAgentNames(),
142147
);
143148

144149
const registerToolByName = (toolName: string) => {
@@ -190,15 +195,15 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
190195
agentToolRegistry.sortTools();
191196

192197
// Get the parent prompt ID from context
193-
const parentPromptId = promptIdContext.getStore();
198+
const parentPromptId = context.promptId;
194199

195200
// Get the parent tool call ID from context
196201
const toolContext = getToolCallContext();
197202
const parentCallId = toolContext?.callId;
198203

199204
return new LocalAgentExecutor(
200205
definition,
201-
runtimeContext,
206+
context,
202207
agentToolRegistry,
203208
parentPromptId,
204209
parentCallId,
@@ -214,14 +219,14 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
214219
*/
215220
private constructor(
216221
definition: LocalAgentDefinition<TOutput>,
217-
runtimeContext: Config,
222+
context: AgentLoopContext,
218223
toolRegistry: ToolRegistry,
219224
parentPromptId: string | undefined,
220225
parentCallId: string | undefined,
221226
onActivity?: ActivityCallback,
222227
) {
223228
this.definition = definition;
224-
this.runtimeContext = runtimeContext;
229+
this.context = context;
225230
this.toolRegistry = toolRegistry;
226231
this.onActivity = onActivity;
227232
this.compressionService = new ChatCompressionService();
@@ -418,7 +423,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
418423
} finally {
419424
clearTimeout(graceTimeoutId);
420425
logRecoveryAttempt(
421-
this.runtimeContext,
426+
this.config,
422427
new RecoveryAttemptEvent(
423428
this.agentId,
424429
this.definition.name,
@@ -466,7 +471,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
466471
const combinedSignal = AbortSignal.any([signal, deadlineTimer.signal]);
467472

468473
logAgentStart(
469-
this.runtimeContext,
474+
this.config,
470475
new AgentStartEvent(this.agentId, this.definition.name),
471476
);
472477

@@ -477,7 +482,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
477482
const augmentedInputs = {
478483
...inputs,
479484
cliVersion: await getVersion(),
480-
activeModel: this.runtimeContext.getActiveModel(),
485+
activeModel: this.config.getActiveModel(),
481486
today: new Date().toLocaleDateString(),
482487
};
483488

@@ -494,13 +499,12 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
494499
// Capture the index of the last hint before starting to avoid re-injecting old hints.
495500
// NOTE: Hints added AFTER this point will be broadcast to all currently running
496501
// local agents via the listener below.
497-
const startIndex =
498-
this.runtimeContext.userHintService.getLatestHintIndex();
499-
this.runtimeContext.userHintService.onUserHint(hintListener);
502+
const startIndex = this.config.userHintService.getLatestHintIndex();
503+
this.config.userHintService.onUserHint(hintListener);
500504

501505
try {
502506
const initialHints =
503-
this.runtimeContext.userHintService.getUserHintsAfter(startIndex);
507+
this.config.userHintService.getUserHintsAfter(startIndex);
504508
const formattedInitialHints = formatUserHintsForModel(initialHints);
505509

506510
let currentMessage: Content = formattedInitialHints
@@ -561,7 +565,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
561565
}
562566
}
563567
} finally {
564-
this.runtimeContext.userHintService.offUserHint(hintListener);
568+
this.config.userHintService.offUserHint(hintListener);
565569
}
566570

567571
// === UNIFIED RECOVERY BLOCK ===
@@ -674,7 +678,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
674678
} finally {
675679
deadlineTimer.abort();
676680
logAgentFinish(
677-
this.runtimeContext,
681+
this.config,
678682
new AgentFinishEvent(
679683
this.agentId,
680684
this.definition.name,
@@ -697,7 +701,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
697701
prompt_id,
698702
false,
699703
model,
700-
this.runtimeContext,
704+
this.config,
701705
this.hasFailedCompressionAttempt,
702706
);
703707

@@ -735,11 +739,10 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
735739
const modelConfigAlias = getModelConfigAlias(this.definition);
736740

737741
// Resolve the model config early to get the concrete model string (which may be `auto`).
738-
const resolvedConfig =
739-
this.runtimeContext.modelConfigService.getResolvedConfig({
740-
model: modelConfigAlias,
741-
overrideScope: this.definition.name,
742-
});
742+
const resolvedConfig = this.config.modelConfigService.getResolvedConfig({
743+
model: modelConfigAlias,
744+
overrideScope: this.definition.name,
745+
});
743746
const requestedModel = resolvedConfig.model;
744747

745748
let modelToUse: string;
@@ -756,7 +759,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
756759
signal,
757760
requestedModel,
758761
};
759-
const router = this.runtimeContext.getModelRouterService();
762+
const router = this.config.getModelRouterService();
760763
const decision = await router.route(routingContext);
761764
modelToUse = decision.model;
762765
} catch (error) {
@@ -844,7 +847,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
844847

845848
try {
846849
return new GeminiChat(
847-
this.runtimeContext,
850+
this.config,
848851
systemInstruction,
849852
[{ functionDeclarations: tools }],
850853
startHistory,
@@ -1092,7 +1095,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
10921095
// Execute standard tool calls using the new scheduler
10931096
if (toolRequests.length > 0) {
10941097
const completedCalls = await scheduleAgentTools(
1095-
this.runtimeContext,
1098+
this.config,
10961099
toolRequests,
10971100
{
10981101
schedulerId: this.agentId,
@@ -1240,7 +1243,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
12401243
let finalPrompt = templateString(promptConfig.systemPrompt, inputs);
12411244

12421245
// Append environment context (CWD and folder structure).
1243-
const dirContext = await getDirectoryContextString(this.runtimeContext);
1246+
const dirContext = await getDirectoryContextString(this.config);
12441247
finalPrompt += `\n\n# Environment Context\n${dirContext}`;
12451248

12461249
// Append standard rules for non-interactive execution.

packages/core/src/agents/local-invocation.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ describe('LocalSubagentInvocation', () => {
6767
beforeEach(() => {
6868
vi.clearAllMocks();
6969
mockConfig = makeFakeConfig();
70+
// .config is already set correctly by the getter on the instance.
71+
Object.defineProperty(mockConfig, 'promptId', {
72+
get: () => 'test-prompt-id',
73+
configurable: true,
74+
});
7075
mockMessageBus = createMockMessageBus();
7176

7277
mockExecutorInstance = {

0 commit comments

Comments
 (0)