Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
45b482a
feat: filter image artifacts for non-vision models
JumpLink Jan 24, 2026
c3a70dd
feat: add agent-level vision toggle and filter base64 image artifacts
JumpLink Jan 24, 2026
fcdba86
refactor: streamline image URL handling and simplify artifact payload…
JumpLink Jan 24, 2026
ca955a0
feat: enhance vision capability handling and artifact processing
JumpLink Jan 26, 2026
2d57d4a
refactor: improve artifact processing and streamline message handling
JumpLink Jan 26, 2026
714af6c
feat: enhance artifact handling and message processing across components
JumpLink Jan 26, 2026
25947e2
fix: enhance artifact processing and vision capability validation in …
JumpLink Jan 27, 2026
9512fd7
feat: integrate vision capability across OpenAI components and improv…
JumpLink Jan 27, 2026
47ee8b3
fix: ensure max_tokens is set correctly in CustomAnthropic class
JumpLink Jan 27, 2026
ca88dc9
Merge upstream: resolve conflict (vision + toolDefinitions in AgentIn…
JumpLink Feb 11, 2026
4cd82be
merge upstream into feat/vision
JumpLink Feb 12, 2026
d19b4e3
Merge remote-tracking branch 'upstream' into feat/vision
JumpLink Feb 16, 2026
69b20e9
fix: restore upstream #54 handoff fix (instructions via system prompt)
JumpLink Feb 16, 2026
268129c
Merge upstream into feat/vision, resolve Graph.ts conflict (handoff +…
JumpLink Feb 17, 2026
bfea2a9
fix: align handoff instructions handling with upstream (#59)
JumpLink Feb 18, 2026
3de0f42
Merge remote-tracking branch 'upstream' into feat/vision
JumpLink Mar 6, 2026
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
10 changes: 10 additions & 0 deletions src/agents/AgentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class AgentContext {
maxContextTokens,
reasoningKey,
useLegacyContent,
vision,
discoveredTools,
} = agentConfig;

Expand All @@ -63,6 +64,7 @@ export class AgentContext {
instructionTokens: 0,
tokenCounter,
useLegacyContent,
vision,
discoveredTools,
});

Expand Down Expand Up @@ -167,6 +169,11 @@ export class AgentContext {
tokenCalculationPromise?: Promise<void>;
/** Format content blocks as strings (for legacy compatibility) */
useLegacyContent: boolean = false;
/**
* Whether this agent supports vision capabilities (image processing).
* Defaults to false if not explicitly set.
*/
vision?: boolean;
/**
* Handoff context when this agent receives control via handoff.
* Contains source and parallel execution info for system message context.
Expand Down Expand Up @@ -196,6 +203,7 @@ export class AgentContext {
toolEnd,
instructionTokens,
useLegacyContent,
vision,
discoveredTools,
}: {
agentId: string;
Expand All @@ -215,6 +223,7 @@ export class AgentContext {
toolEnd?: boolean;
instructionTokens?: number;
useLegacyContent?: boolean;
vision?: boolean;
discoveredTools?: string[];
}) {
this.agentId = agentId;
Expand All @@ -230,6 +239,7 @@ export class AgentContext {
this.toolDefinitions = toolDefinitions;
this.instructions = instructions;
this.additionalInstructions = additionalInstructions;
this.vision = vision;
if (reasoningKey) {
this.reasoningKey = reasoningKey;
}
Expand Down
36 changes: 26 additions & 10 deletions src/graphs/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@ import { nanoid } from 'nanoid';
import { concat } from '@langchain/core/utils/stream';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { ChatVertexAI } from '@langchain/google-vertexai';
import {
START,
END,
StateGraph,
Annotation,
messagesStateReducer,
} from '@langchain/langgraph';
import { START, END, StateGraph, Annotation } from '@langchain/langgraph';
import { messagesStateReducer } from '@/messages/reducer';
import {
Runnable,
RunnableConfig,
Expand Down Expand Up @@ -173,7 +168,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
agentContexts: Map<string, AgentContext> = new Map();
/** Default agent ID to use */
defaultAgentId: string;

constructor({
// parent-level graph inputs
runId,
Expand Down Expand Up @@ -658,6 +652,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
throw new Error('No model found');
}

if ((_tools?.length ?? 0) > 0 && manualToolStreamProviders.has(provider)) {
if (!model.stream) {
throw new Error('Model does not support stream');
}
}

if (model.stream) {
/**
* Process all model output through a local ChatModelStreamHandler in the
Expand Down Expand Up @@ -758,12 +758,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
}

const toolsForBinding = agentContext.getToolsForBinding();
const clientOptionsWithVision = {
...agentContext.clientOptions,
vision: agentContext.vision,
} as unknown as t.ClientOptions;
let model =
this.overrideModel ??
this.initializeModel({
tools: toolsForBinding,
provider: agentContext.provider,
clientOptions: agentContext.clientOptions,
clientOptions: clientOptionsWithVision,
});

if (agentContext.systemRunnable) {
Expand All @@ -779,6 +783,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
this.config = config;

let messagesToUse = messages;

if (
!agentContext.pruneMessages &&
agentContext.tokenCounter &&
Expand Down Expand Up @@ -842,13 +847,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {

const isLatestToolMessage = lastMessageY instanceof ToolMessage;

// Check if any ToolMessage in current turn has artifacts
// This is more robust than only checking if the last message is a ToolMessage
const hasToolMessagesWithArtifacts = finalMessages.some((msg) => {
if (msg._getType() !== 'tool') return false;
const toolMsg = msg as ToolMessage & { artifact?: t.MCPArtifact };
return (
toolMsg.artifact != null || toolMsg.additional_kwargs.artifact != null
);
});

if (
isLatestToolMessage &&
agentContext.provider === Providers.ANTHROPIC
) {
formatAnthropicArtifactContent(finalMessages);
} else if (
isLatestToolMessage &&
hasToolMessagesWithArtifacts &&
((isOpenAILike(agentContext.provider) &&
agentContext.provider !== Providers.DEEPSEEK) ||
isGoogleLike(agentContext.provider))
Expand Down Expand Up @@ -1092,6 +1107,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
}

agentContext.currentUsage = this.getUsageMetadata(result.messages?.[0]);

this.cleanupSignalListener();
return result;
};
Expand Down
6 changes: 4 additions & 2 deletions src/graphs/MultiAgentGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
StateGraph,
Annotation,
getCurrentTaskInput,
messagesStateReducer,
} from '@langchain/langgraph';
import { messagesStateReducer } from '@/messages/reducer';
import type { LangGraphRunnableConfig } from '@langchain/langgraph';
import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
import type { ToolRunnableConfig } from '@langchain/core/tools';
Expand Down Expand Up @@ -821,7 +821,9 @@ export class MultiAgentGraph extends StandardGraph {
}
}

/** Update token map if we have a token counter */
/**
* Update token map if we have a token counter
*/
if (agentContext?.tokenCounter && hasInstructions) {
const freshTokenMap: Record<string, number> = {};
for (
Expand Down
6 changes: 4 additions & 2 deletions src/llm/anthropic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,24 @@ export class CustomAnthropic extends ChatAnthropicMessages {
);
}

const maxTokens = Math.max(1, this.maxTokens ?? 4096);
return {
model: this.model,
stop_sequences: options?.stop ?? this.stopSequences,
stream: this.streaming,
max_tokens: this.maxTokens,
max_tokens: maxTokens,
...sharedParams,
};
}
const maxTokens = Math.max(1, this.maxTokens ?? 4096);
return {
model: this.model,
temperature: this.temperature,
top_k: this.top_k,
top_p: this.topP,
stop_sequences: options?.stop ?? this.stopSequences,
stream: this.streaming,
max_tokens: this.maxTokens,
max_tokens: maxTokens,
...sharedParams,
};
}
Expand Down
55 changes: 46 additions & 9 deletions src/llm/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,19 @@ export class CustomAzureOpenAIClient extends AzureOpenAIClient {
export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
_lc_stream_delay?: number;

/** When false, image_url parts are stripped before sending (avoids non-multimodal API errors). */
protected visionCapable: boolean;

constructor(
fields?: t.ChatOpenAICallOptions & {
_lc_stream_delay?: number;
vision?: boolean;
} & t.OpenAIChatInput['modelKwargs']
) {
super(fields);
this._lc_stream_delay = fields?._lc_stream_delay;
const { vision, _lc_stream_delay, ...rest } = fields ?? {};
super(rest as typeof fields);
this._lc_stream_delay = _lc_stream_delay;
this.visionCapable = vision ?? true;
}

public get exposedClient(): CustomOpenAIClient {
Expand Down Expand Up @@ -287,7 +293,8 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
input: _convertMessagesToOpenAIResponsesParams(
messages,
this.model,
this.zdrEnabled
this.zdrEnabled,
this.visionCapable
),
stream: true,
},
Expand Down Expand Up @@ -322,7 +329,9 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
runManager?: CallbackManagerForLLMRun
): AsyncGenerator<ChatGenerationChunk> {
const messagesMapped: OpenAICompletionParam[] =
_convertMessagesToOpenAIParams(messages, this.model);
_convertMessagesToOpenAIParams(messages, this.model, {
visionCapable: this.visionCapable,
});

const params = {
...this.invocationParams(options, {
Expand Down Expand Up @@ -459,9 +468,18 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
_lc_stream_delay?: number;

constructor(fields?: t.AzureOpenAIInput & { _lc_stream_delay?: number }) {
super(fields);
this._lc_stream_delay = fields?._lc_stream_delay;
protected visionCapable: boolean;

constructor(
fields?: t.AzureOpenAIInput & {
_lc_stream_delay?: number;
vision?: boolean;
}
) {
const { vision, _lc_stream_delay, ...rest } = fields ?? {};
super(rest as typeof fields);
this._lc_stream_delay = _lc_stream_delay;
this.visionCapable = vision ?? true;
}

public get exposedClient(): CustomOpenAIClient {
Expand Down Expand Up @@ -581,7 +599,8 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
input: _convertMessagesToOpenAIResponsesParams(
messages,
this.model,
this.zdrEnabled
this.zdrEnabled,
this.visionCapable
),
stream: true,
},
Expand Down Expand Up @@ -611,6 +630,17 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
}
}
export class ChatDeepSeek extends OriginalChatDeepSeek {
protected visionCapable: boolean;

constructor(
fields?: ConstructorParameters<typeof OriginalChatDeepSeek>[0] & {
vision?: boolean;
}
) {
const { vision, ...rest } = fields ?? {};
super(rest as ConstructorParameters<typeof OriginalChatDeepSeek>[0]);
this.visionCapable = vision ?? true;
}
public get exposedClient(): CustomOpenAIClient {
return this.client;
}
Expand Down Expand Up @@ -758,6 +788,7 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
const messagesMapped: OpenAICompletionParam[] =
_convertMessagesToOpenAIParams(messages, this.model, {
includeReasoningContent: true,
visionCapable: this.visionCapable,
});

const params = {
Expand Down Expand Up @@ -1138,15 +1169,19 @@ export class ChatMoonshot extends ChatOpenAI {
export class ChatXAI extends OriginalChatXAI {
_lc_stream_delay?: number;

protected visionCapable: boolean;

constructor(
fields?: Partial<ChatXAIInput> & {
configuration?: { baseURL?: string };
clientConfig?: { baseURL?: string };
_lc_stream_delay?: number;
vision?: boolean;
}
) {
super(fields);
this._lc_stream_delay = fields?._lc_stream_delay;
this.visionCapable = fields?.vision ?? true;
const customBaseURL =
fields?.configuration?.baseURL ?? fields?.clientConfig?.baseURL;
if (customBaseURL != null && customBaseURL) {
Expand Down Expand Up @@ -1202,7 +1237,9 @@ export class ChatXAI extends OriginalChatXAI {
runManager?: CallbackManagerForLLMRun
): AsyncGenerator<ChatGenerationChunk> {
const messagesMapped: OpenAICompletionParam[] =
_convertMessagesToOpenAIParams(messages, this.model);
_convertMessagesToOpenAIParams(messages, this.model, {
visionCapable: this.visionCapable,
});

const params = {
...this.invocationParams(options, {
Expand Down
Loading