Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/agents",
"version": "3.6.10",
"version": "3.6.11",
"reova": {
"enabled": true,
"endpoint": "https://telemetry.reo.dev/data"
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/stream.eagerEventExecution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ function createGraph(overrides: Partial<StandardGraph> = {}): StandardGraph {
toolDefinitions: [{ name: 'weather' }],
graphTools: [],
agentId: 'agent_1',
getCallerCapabilityProjectionSnapshot: jest.fn(() => ({
version: 1 as const,
directToolNames: ['weather'],
codeExecutionToolNames: [],
directOnlyToolNames: ['weather'],
codeExecutionOnlyToolNames: [],
})),
})
),
getStepKey: jest.fn(() => 'step-key'),
Expand Down Expand Up @@ -215,6 +222,13 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
stepId: expect.stringMatching(/^step_/),
turn: 0,
});
expect(toolExecuteCalls[0].callerCapabilityProjection).toEqual({
version: 1,
directToolNames: ['weather'],
codeExecutionToolNames: [],
directOnlyToolNames: ['weather'],
codeExecutionOnlyToolNames: [],
});
expect(graph.eagerEventToolExecutions.get('call_weather')).toMatchObject({
toolCallId: 'call_weather',
toolName: 'weather',
Expand Down
172 changes: 157 additions & 15 deletions src/agents/AgentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ import {
} from '@/common';
import {
isProgrammaticRunnerAutoBound,
isProgrammaticRunnerResolvedDirectly,
resolveLocalImplementationNames,
resolveLocalToolRegistry,
} from '@/tools/local/resolveLocalExecutionTools';
import {
type CallerCapabilityProjection,
allowsToolCaller,
applyCallerCapabilityDefinitionOverrides,
createCallerCapabilityProjectionSnapshot,
isToolDefinitionActive,
isProgrammaticControlTool,
mergeCallerCapabilityDefinitions,
resolveCallerCapabilityProjection,
} from '@/tools/CallerCapabilities';
import { createSchemaOnlyTools } from '@/tools/schema';
Expand All @@ -59,6 +65,12 @@ type AgentSystemContentBlock =

type PromptCacheProvider = Providers.ANTHROPIC | Providers.OPENROUTER;

type ProgrammaticToolInstructionTarget = {
name: string;
codeGuidance: string;
executesDirectly: boolean;
};

/**
* Encapsulates agent-specific state that can vary between agents in a multi-agent system
*/
Expand Down Expand Up @@ -483,12 +495,53 @@ export class AgentContext {

/** Builds the caller boundary and schemas for programmatic-only tools. */
private buildProgrammaticOnlyToolsInstructions(): string {
if (!this.toolRegistry) return '';

const capabilities = resolveCallerCapabilityProjection(
this.toolRegistry.values(),
(toolDef) => isToolDefinitionActive(toolDef, this.discoveredToolNames)
const programmaticTools = this.getProgrammaticToolInstructionTargets();
if (programmaticTools.length === 0) return '';
const directProgrammaticTools = programmaticTools.filter(
(tool) => tool.executesDirectly
);
const eventProgrammaticTools = programmaticTools.filter(
(tool) => !tool.executesDirectly
);
const groups: Array<{
tools: ProgrammaticToolInstructionTarget[];
capabilities: CallerCapabilityProjection;
label: string;
}> = [];
if (directProgrammaticTools.length > 0) {
groups.push({
tools: directProgrammaticTools,
capabilities: this.getDirectProgrammaticCapabilityProjection(),
label: 'Direct programmatic runners',
});
}
if (eventProgrammaticTools.length > 0) {
groups.push({
tools: eventProgrammaticTools,
capabilities: this.getCallerCapabilityProjection(),
label: 'Event-dispatched programmatic runners',
});
}
if (groups.length === 0) {
return '';
}
const showGroupLabels = groups.length > 1;
return (
'\n\n## Programmatic Tool Calling' +
groups
.map(
({ tools, capabilities, label }) =>
(showGroupLabels ? `\n\n### ${label}` : '') +
this.buildProgrammaticToolGroupInstructions(tools, capabilities)
)
.join('')
);
}

private buildProgrammaticToolGroupInstructions(
programmaticTools: ProgrammaticToolInstructionTarget[],
capabilities: CallerCapabilityProjection
): string {
const programmaticOnlyTools = capabilities.codeExecutionOnlyTools;
const programmaticToolNames = capabilities.codeExecutionTools.map(
(toolDef) => toolDef.name
Expand All @@ -497,8 +550,6 @@ export class AgentContext {
.map((toolDef) => toolDef.name)
.filter((name) => !isProgrammaticControlTool(name));

const programmaticTools = this.getProgrammaticToolInstructionTargets();
if (programmaticTools.length === 0) return '';
const programmaticRunnerNames = programmaticTools
.map((tool) => `\`${tool.name}\``)
.join(' or ');
Expand All @@ -513,7 +564,7 @@ export class AgentContext {
.join(', ')}. Every ${programmaticRunnerNames} call must include a \`tool_manifest\` containing the exact registered names used by its code; the manifest is validated before execution starts.`
: '';
const boundary =
'\n\n## Programmatic Tool Calling\n\n' +
'\n\n' +
`Only these tools may be invoked inside ${programmaticRunnerNames}: ${quotedProgrammaticNames}.` +
directOnlyBoundary;

Expand Down Expand Up @@ -548,11 +599,8 @@ export class AgentContext {
);
}

private getProgrammaticToolInstructionTargets(): Array<{
name: string;
codeGuidance: string;
}> {
const targets: Array<{ name: string; codeGuidance: string }> = [];
private getProgrammaticToolInstructionTargets(): ProgrammaticToolInstructionTarget[] {
const targets: ProgrammaticToolInstructionTarget[] = [];
if (
this.hasBoundTool(Constants.BASH_PROGRAMMATIC_TOOL_CALLING) ||
isProgrammaticRunnerAutoBound(
Expand All @@ -563,6 +611,9 @@ export class AgentContext {
targets.push({
name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
codeGuidance: 'Bash code',
executesDirectly: this.isProgrammaticRunnerDirectlyBound(
Constants.BASH_PROGRAMMATIC_TOOL_CALLING
),
});
}

Expand All @@ -581,12 +632,73 @@ export class AgentContext {
codeGuidance: localDefault
? 'Bash code by default, or set `lang: "py"` to use Python code'
: 'Python code',
executesDirectly: this.isProgrammaticRunnerDirectlyBound(
Constants.PROGRAMMATIC_TOOL_CALLING
),
});
}

return targets;
}

/** Whether ToolNode executes this runner in-process instead of via an event. */
private isProgrammaticRunnerDirectlyBound(name: string): boolean {
return (
isProgrammaticRunnerResolvedDirectly(
name,
this.toolExecution,
this.toolDefinitions?.some((toolDef) => toolDef.name === name) === true
) ||
this.graphTools?.some(
(tool) => 'name' in tool && tool.name === name
) === true
);
}

/** Mirrors ToolNode's executable implementation gate for direct runners. */
private getDirectProgrammaticCapabilityProjection(): CallerCapabilityProjection {
const implementationNames = new Set<string>();
const isEventDriven = (this.toolDefinitions?.length ?? 0) > 0;
const resolverInputNames = new Set<string>();
if (isEventDriven) {
for (const toolDef of this.toolDefinitions ?? []) {
resolverInputNames.add(toolDef.name);
}
} else {
for (const tool of (this.tools as t.GenericTool[] | undefined) ?? []) {
if ('name' in tool && typeof tool.name === 'string') {
implementationNames.add(tool.name);
resolverInputNames.add(tool.name);
}
}
}
for (const tool of (this.graphTools as t.GenericTool[] | undefined) ?? []) {
if ('name' in tool && typeof tool.name === 'string') {
implementationNames.add(tool.name);
resolverInputNames.add(tool.name);
}
}
for (const name of resolveLocalImplementationNames(
resolverInputNames,
this.toolExecution
)) {
implementationNames.add(name);
}
const activeCapabilities = this.getCallerCapabilityProjection();
const executableCapabilities = resolveCallerCapabilityProjection(
this.toolRegistry?.values() ?? [],
(toolDef) =>
implementationNames.has(toolDef.name) &&
isToolDefinitionActive(toolDef, this.discoveredToolNames)
);
return {
directTools: activeCapabilities.directTools,
directOnlyTools: activeCapabilities.directOnlyTools,
codeExecutionTools: executableCapabilities.codeExecutionTools,
codeExecutionOnlyTools: executableCapabilities.codeExecutionOnlyTools,
};
}

private hasBoundTool(name: string): boolean {
return (
this.getToolsForBinding()?.some(
Expand Down Expand Up @@ -1123,9 +1235,21 @@ export class AgentContext {
this.indexTokenCountMap = { ...baseTokenMap };
}

/** Event definitions with matching runtime caller/defer metadata applied. */
getEffectiveToolDefinitions(): t.LCTool[] | undefined {
if (!this.toolDefinitions) {
return undefined;
}
return applyCallerCapabilityDefinitionOverrides(
this.toolDefinitions,
this.toolRegistry?.values()
);
}

/** Active tool definitions for token accounting (excludes deferred-and-undiscovered entries). */
private getActiveToolDefinitions(): t.LCTool[] {
if (!this.toolDefinitions) {
const effectiveToolDefinitions = this.getEffectiveToolDefinitions();
if (!effectiveToolDefinitions) {
return [];
}
/**
Expand All @@ -1136,7 +1260,7 @@ export class AgentContext {
* `toolSchemaTokens` even though they were never bound.
*/
return resolveCallerCapabilityProjection(
this.toolDefinitions,
effectiveToolDefinitions,
(toolDef) => isToolDefinitionActive(toolDef, this.discoveredToolNames)
).directTools;
}
Expand Down Expand Up @@ -1767,6 +1891,24 @@ export class AgentContext {
return Array.from(this.discoveredToolNames);
}

/** Returns the live projection shared by prompt and event execution. */
private getCallerCapabilityProjection(): CallerCapabilityProjection {
return resolveCallerCapabilityProjection(
mergeCallerCapabilityDefinitions(
this.toolDefinitions,
this.toolRegistry?.values()
),
(toolDef) => isToolDefinitionActive(toolDef, this.discoveredToolNames)
);
}

/** Returns the SDK-owned active caller projection for event-driven hosts. */
getCallerCapabilityProjectionSnapshot(): t.CallerCapabilityProjectionSnapshot {
return createCallerCapabilityProjectionSnapshot(
this.getCallerCapabilityProjection()
);
}

/**
* Marks tools as discovered via tool search.
* Discovered tools will be included in the next model binding.
Expand Down
Loading
Loading