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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
# Changelog

All notable changes to this project will be documented in this file.
2.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There appears to be a stray 2. on this line which seems to be a typo. It should be removed to maintain the clean formatting of the changelog.

## [0.4.1] - 2025-12-18

### 🤖 Next-Gen LLM Adapter Upgrades
- **Gemini 3 Family Support**:
- Full support for **Gemini 3 Pro**, **Flash**, and **Deep Think** models.
- Implemented **native `systemInstruction`** support for improved behavioral steering.
- Added **`thinkingLevel` control** (low, minimal, medium, high) to balance reasoning depth and latency.
- Updated default model to `gemini-3-flash`.
- **Claude 4.5 Family Support**:
- Support for **Claude 4.5 Opus**, **Sonnet**, and **Haiku**.
- Maintained and optimized support for **thinking tokens** and reasoning blocks.
- Updated default model to `claude-4.5-sonnet`.
- **GPT-5 Family Support**:
- Full support for **GPT-5**, **GPT-5.1**, and **GPT-5.2** (including Instant, Thinking, and Pro variants).
- Updated default model to `gpt-5.2-instant`.
- **SDK Dependencies**: Upgraded `@google/genai`, `@anthropic-ai/sdk`, and `openai` to their latest December 2025 versions.

### 🛠 Refactors & Maintenance
- Improved integration test resilience by ensuring tests skip correctly when API keys are missing.
- Updated documentation and README to reflect v0.4.1 status.


## [0.4.0] beta - 2025-12-18

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


# ✨ ART: Agentic Runtime Framework (beta) <img src="https://img.shields.io/badge/Version-v0.4.0-blue" alt="Version 0.4.0">
# ✨ ART: Agentic Runtime Framework (beta) <img src="https://img.shields.io/badge/Version-v0.4.1-blue" alt="Version 0.4.1">

<p align="center">
<img src="docs/art-logo.jpeg" alt="ART Framework Logo" width="200"/>
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "art-framework",
"version": "0.4.0",
"version": "0.4.1",
"description": "Agent Runtine (ART) Framework - A browser-first JavaScript/TypeScript framework for building LLM-powered Agentic AI applications that supports MCP and A2A protocols natively",
"type": "module",
"main": "./dist/index.js",
Expand Down Expand Up @@ -78,16 +78,16 @@
"ws": "^8.18.1"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.51.0",
"@google/genai": "^1.17.0",
"@anthropic-ai/sdk": "^0.71.1",
"@google/genai": "^1.34.0",
"@modelcontextprotocol/sdk": "^1.12.3",
"@supabase/supabase-js": "^2.56.0",
"@types/mathjs": "^9.4.1",
"@types/mustache": "^4.2.5",
"ajv": "^8.17.1",
"mathjs": "^14.4.0",
"mustache": "^4.2.0",
"openai": "^4.98.0",
"openai": "^4.120.0",
"react-syntax-highlighter": "^15.6.1",
"reconnecting-eventsource": "^1.6.4",
"uuid": "^9.0.0",
Expand Down
22 changes: 11 additions & 11 deletions src/integrations/reasoning/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Logger } from '@/utils/logger';
import { ARTError, ErrorCode } from '@/errors';

// Default model if not specified
const ANTHROPIC_DEFAULT_MODEL_ID = 'claude-3-7-sonnet-20250219';
const ANTHROPIC_DEFAULT_MODEL_ID = 'claude-4.5-sonnet';
const ANTHROPIC_DEFAULT_MAX_TOKENS = 4096;

/**
Expand All @@ -21,7 +21,7 @@ const ANTHROPIC_DEFAULT_MAX_TOKENS = 4096;
export interface AnthropicAdapterOptions {
/** Your Anthropic API key. Handle securely. */
apiKey: string;
/** The default Anthropic model ID to use (e.g., 'claude-3-opus-20240229', 'claude-3-5-sonnet-20240620'). */
/** The default Anthropic model ID to use (e.g., 'claude_4.5_sonnet', 'claude_4.5_opus'). */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The example model IDs in this comment use underscores (e.g., claude_4.5_sonnet), which is inconsistent with the default model ID defined on line 15 (claude-4.5-sonnet) and Anthropic's typical model naming convention, which uses hyphens. Using hyphens in the examples would improve consistency and reduce potential confusion for developers.

Suggested change
/** The default Anthropic model ID to use (e.g., 'claude_4.5_sonnet', 'claude_4.5_opus'). */
/** The default Anthropic model ID to use (e.g., 'claude-4.5-sonnet', 'claude-4.5-opus'). */

model?: string;
/** Optional: Override the base URL for the Anthropic API. */
apiBaseUrl?: string;
Expand Down Expand Up @@ -106,7 +106,7 @@ export class AnthropicAdapter implements ProviderAdapter {
const topP = anthropicApiParams.top_p || anthropicApiParams.topP || options.top_p || options.topP;
const topK = anthropicApiParams.top_k || anthropicApiParams.topK || options.top_k || options.topK;
const stopSequences = anthropicApiParams.stop_sequences || anthropicApiParams.stopSequences || options.stop || options.stop_sequences || options.stopSequences;
// Anthropic thinking config for Claude 3.7 Sonnet (reasoning): { type: 'enabled', budget_tokens?: number }
// Anthropic thinking config for Claude models (reasoning): { type: 'enabled', budget_tokens?: number }
const thinking = anthropicApiParams.thinking || options.thinking;

if (!maxTokens) {
Expand Down Expand Up @@ -379,7 +379,7 @@ export class AnthropicAdapter implements ProviderAdapter {
input: tu.input,
}));
if (responseText) {
yield { type: 'TOKEN', data: [{type: 'text', text: responseText}, ...toolData], threadId, traceId, sessionId, tokenType };
yield { type: 'TOKEN', data: [{ type: 'text', text: responseText }, ...toolData], threadId, traceId, sessionId, tokenType };
} else {
yield { type: 'TOKEN', data: toolData, threadId, traceId, sessionId, tokenType };
}
Expand Down Expand Up @@ -476,7 +476,7 @@ export class AnthropicAdapter implements ProviderAdapter {

if (currentRoleInternal === messageRoleToPush && messages.length > 0) {
const lastMessage = messages[messages.length - 1];

let currentLastMessageContentArray: Anthropic.Messages.ContentBlockParam[];
if (typeof lastMessage.content === 'string') {
currentLastMessageContentArray = [{ type: 'text', text: lastMessage.content } as Anthropic.Messages.TextBlockParam];
Expand All @@ -501,11 +501,11 @@ export class AnthropicAdapter implements ProviderAdapter {
// Anthropic requires the first message to be 'user' if messages exist and no system prompt.
if (!systemPrompt && messages.length > 0 && messages[0].role !== 'user') {
Logger.warn("AnthropicAdapter: Prompt does not start with user message and has no system prompt. Prepending an empty user message for compatibility.");
messages.unshift({ role: 'user', content: '(Previous turn context)'});
messages.unshift({ role: 'user', content: '(Previous turn context)' });
}

// Ensure conversation doesn't end on an assistant message if expecting tool results
const lastArtMsg = artPrompt[artPrompt.length -1];
const lastArtMsg = artPrompt[artPrompt.length - 1];
if (lastArtMsg?.role === 'assistant' && lastArtMsg.tool_calls && lastArtMsg.tool_calls.length > 0) {
Logger.debug("AnthropicAdapter: Prompt ends with assistant requesting tool calls.");
}
Expand All @@ -529,7 +529,7 @@ export class AnthropicAdapter implements ProviderAdapter {
// Handle text content
if (artMsg.content && typeof artMsg.content === 'string' && artMsg.content.trim() !== '') {
blocks.push({ type: 'text', text: artMsg.content });
} else if (artMsg.content && typeof artMsg.content !== 'string' && artMsg.role !== 'tool_result' && (!artMsg.tool_calls || artMsg.tool_calls.length === 0) ) {
} else if (artMsg.content && typeof artMsg.content !== 'string' && artMsg.role !== 'tool_result' && (!artMsg.tool_calls || artMsg.tool_calls.length === 0)) {
Logger.warn(`AnthropicAdapter: Non-string, non-tool_result, non-tool_call-only content for role ${artMsg.role}, stringifying.`, { content: artMsg.content });
blocks.push({ type: 'text', text: JSON.stringify(artMsg.content) });
}
Expand Down Expand Up @@ -570,7 +570,7 @@ export class AnthropicAdapter implements ProviderAdapter {
if (typeof artMsg.content === 'string') {
toolResultBlock.content = artMsg.content;
} else if (Array.isArray(artMsg.content) && artMsg.content.every(c => typeof c === 'object' && c.type === 'text' && typeof c.text === 'string')) {
toolResultBlock.content = artMsg.content.map(c => ({type: 'text', text: (c as any).text}));
toolResultBlock.content = artMsg.content.map(c => ({ type: 'text', text: (c as any).text }));
} else if (artMsg.content !== null && artMsg.content !== undefined) {
toolResultBlock.content = JSON.stringify(artMsg.content);
}
Expand All @@ -582,7 +582,7 @@ export class AnthropicAdapter implements ProviderAdapter {
if (blocks.length === 1 && blocks[0].type === 'text') {
return (blocks[0] as Anthropic.TextBlockParam).text;
}

// If blocks is empty, return empty string
if (blocks.length === 0) {
return "";
Expand Down
108 changes: 50 additions & 58 deletions src/integrations/reasoning/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ARTError, ErrorCode } from '@/errors'; // Import ARTError and ErrorCode
export interface GeminiAdapterOptions {
/** Your Google AI API key (e.g., from Google AI Studio). Handle securely. */
apiKey: string;
/** The default Gemini model ID to use (e.g., 'gemini-2.5-flash', 'gemini-pro'). Defaults to 'gemini-2.5-flash' if not provided. */
/** The default Gemini model ID to use (e.g., 'gemini-3-flash', 'gemini-pro'). Defaults to 'gemini-3-flash' if not provided. */
model?: string;
/** Optional: Override the base URL for the Google Generative AI API. */
apiBaseUrl?: string; // Note: Not directly used by SDK basic setup
Expand All @@ -48,10 +48,10 @@ function isRetryableError(error: any): boolean {
// Check for common retryable error messages
const message = String(error?.message || error?.error?.message || '').toLowerCase();
return message.includes('overloaded') ||
message.includes('rate limit') ||
message.includes('temporarily unavailable') ||
message.includes('503') ||
message.includes('429');
message.includes('rate limit') ||
message.includes('temporarily unavailable') ||
message.includes('503') ||
message.includes('429');
}

/**
Expand Down Expand Up @@ -120,7 +120,7 @@ export class GeminiAdapter implements ProviderAdapter {
throw new Error('GeminiAdapter requires an apiKey in options.');
}
this.apiKey = options.apiKey;
this.defaultModel = options.model || 'gemini-2.5-flash'; // Default to latest stable flash model
this.defaultModel = options.model || 'gemini-3-flash'; // Default to latest stable flash model
// Initialize the SDK
// Use correct constructor based on documentation
this.genAI = new GoogleGenAI({ apiKey: this.apiKey });
Expand All @@ -138,10 +138,11 @@ export class GeminiAdapter implements ProviderAdapter {
* Handles both streaming and non-streaming requests based on `options.stream`.
*
* Thinking tokens (Gemini):
* - On supported Gemini models (e.g., `gemini-2.5-*`), you can enable thought output via `config.thinkingConfig`.
* - On supported Gemini models (e.g., `gemini-3-*`), you can enable thought output via `config.thinkingConfig`.
* - This adapter reads provider-specific flags from the call options:
* - `options.gemini.thinking.includeThoughts: boolean` — when `true`, requests thought (reasoning) output.
* - `options.gemini.thinking.thinkingBudget?: number` — optional token budget for thinking.
* - `options.gemini.thinking.thinkingLevel?: 'low' | 'minimal' | 'medium' | 'high'` — optional thinking level (Gemini 3+).
* - When enabled and supported, the adapter will attempt to differentiate thought vs response parts and set
* `StreamEvent.tokenType` accordingly:
* - For planning calls (`callContext === 'AGENT_THOUGHT'`): `AGENT_THOUGHT_LLM_THINKING` or `AGENT_THOUGHT_LLM_RESPONSE`.
Expand Down Expand Up @@ -187,8 +188,11 @@ export class GeminiAdapter implements ProviderAdapter {

// --- Format Payload for SDK ---
let contents: Content[];
let systemInstruction: string | undefined;
try {
contents = this.translateToGemini(prompt); // Use the new translation function
const translation = this.translateToGemini(prompt);
contents = translation.contents;
systemInstruction = translation.systemInstruction;
} catch (error: any) {
Logger.error(`Error translating ArtStandardPrompt to Gemini format: ${error.message}`, { error, threadId, traceId });
// Immediately yield error and end if translation fails
Expand All @@ -213,19 +217,33 @@ export class GeminiAdapter implements ProviderAdapter {
delete generationConfig[key as keyof GenerationConfig]
);
// Build optional thinking configuration from CallOptions (feature flag)
// Expecting shape: options.gemini?.thinking?.{ includeThoughts?: boolean; thinkingBudget?: number }
const includeThoughts: boolean = !!(options as any)?.gemini?.thinking?.includeThoughts;
const thinkingBudget: number | undefined = (options as any)?.gemini?.thinking?.thinkingBudget;
// Expecting shape: options.gemini?.thinking?.{ includeThoughts?: boolean; thinkingBudget?: number; thinkingLevel?: string }
const geminiThinking = (options as any)?.gemini?.thinking;
const includeThoughts: boolean = !!geminiThinking?.includeThoughts;
const thinkingBudget: number | undefined = geminiThinking?.thinkingBudget;
const thinkingLevel: string | undefined = geminiThinking?.thinkingLevel;

// Merge into a requestConfig that is leniently typed to allow SDK preview fields
const requestConfig: any = includeThoughts
? {
...generationConfig,
thinkingConfig: {
includeThoughts: true,
...(thinkingBudget !== undefined ? { thinkingBudget } : {}),
...(thinkingLevel !== undefined ? { thinkingLevel } : {}),
},
}
: { ...generationConfig };

// Support systemInstruction in the newer SDK pattern
const callConfig: any = {
model: modelToUse,
contents,
config: requestConfig,
};
if (systemInstruction) {
callConfig.systemInstruction = systemInstruction;
}
// --- End Format Payload ---

Logger.debug(`Calling Gemini SDK with model ${modelToUse}, stream: ${!!stream}`, { threadId, traceId });
Expand All @@ -247,11 +265,7 @@ export class GeminiAdapter implements ProviderAdapter {
// Let TypeScript infer the type of streamResult
// Use the new SDK pattern: genAI.models.generateContentStream with retry logic
const streamResult = await withRetry(
() => genAIInstance.models.generateContentStream({
model: modelToUse,
contents,
config: requestConfig,
}),
() => genAIInstance.models.generateContentStream(callConfig),
{
onRetry: (error, attempt, delayMs) => {
Logger.info(`Retrying Gemini stream call (attempt ${attempt})`, { threadId, traceId, delayMs });
Expand Down Expand Up @@ -336,11 +350,7 @@ export class GeminiAdapter implements ProviderAdapter {
} else {
// Use the new SDK pattern: genAIInstance.models.generateContent with retry logic
const result: GenerateContentResponse = await withRetry(
() => genAIInstance.models.generateContent({
model: modelToUse,
contents,
config: requestConfig,
}),
() => genAIInstance.models.generateContent(callConfig),
{
onRetry: (error, attempt, delayMs) => {
Logger.info(`Retrying Gemini call (attempt ${attempt})`, { threadId, traceId, delayMs });
Expand Down Expand Up @@ -414,62 +424,50 @@ export class GeminiAdapter implements ProviderAdapter {
}

/**
* Translates the provider-agnostic `ArtStandardPrompt` into the Gemini API's `Content[]` format.
* Translates the provider-agnostic `ArtStandardPrompt` into the Gemini API's `Content[]` format
* and extracts any system instructions.
*
* Key translations:
* - `system` role: Merged into the first `user` message.
* - `system` role: Extracted to `systemInstruction`.
* - `user` role: Maps to Gemini's `user` role.
* - `assistant` role: Maps to Gemini's `model` role. Handles text content and `tool_calls` (mapped to `functionCall`).
* - `tool_result` role: Maps to Gemini's `user` role with a `functionResponse` part.
* - `tool_request` role: Skipped (implicitly handled by `assistant`'s `tool_calls`).
*
* Adds validation to ensure the conversation doesn't start with a 'model' role.
*
* @private
* @param {ArtStandardPrompt} artPrompt - The input `ArtStandardPrompt` array.
* @returns {Content[]} The `Content[]` array formatted for the Gemini API.
* @throws {ARTError} If translation encounters an issue, such as a `tool_result` missing required fields (ErrorCode.PROMPT_TRANSLATION_FAILED).
* @see https://ai.google.dev/api/rest/v1beta/Content
* @returns {{ contents: Content[], systemInstruction?: string }} The Gemini formatted payload.
* @throws {ARTError} If translation encounters an issue.
*/
private translateToGemini(artPrompt: ArtStandardPrompt): Content[] {
const geminiContents: Content[] = [];

// System prompt handling: Gemini prefers system instructions via specific parameters or
// potentially as the first part of the first 'user' message. For simplicity,
// we'll merge the system prompt content into the first user message if present.
let systemPromptContent: string | null = null;
private translateToGemini(artPrompt: ArtStandardPrompt): { contents: Content[], systemInstruction?: string } {
const contents: Content[] = [];
let systemInstruction: string | undefined;

for (const message of artPrompt) {
let role: 'user' | 'model';
const parts: Part[] = [];

switch (message.role) {
case 'system':
// Store system prompt content to potentially merge later.
if (typeof message.content === 'string') {
systemPromptContent = message.content;
systemInstruction = (systemInstruction ? systemInstruction + "\n\n" : "") + message.content;
} else {
Logger.warn(`GeminiAdapter: Ignoring non-string system prompt content.`, { content: message.content });
}
continue; // Don't add a separate 'system' role message
continue;

case 'user': { // Added braces to fix ESLint error
case 'user': {
role = 'user';
let userContent = '';
// Prepend system prompt if this is the first user message
if (systemPromptContent) {
userContent += systemPromptContent + "\n\n";
systemPromptContent = null; // Clear after merging
}
if (typeof message.content === 'string') {
userContent += message.content;
userContent = message.content;
} else {
Logger.warn(`GeminiAdapter: Stringifying non-string user content.`, { content: message.content });
userContent += JSON.stringify(message.content);
userContent = JSON.stringify(message.content);
}
parts.push({ text: userContent });
break;
} // Added braces
}

case 'assistant':
role = 'model';
Expand Down Expand Up @@ -528,21 +526,15 @@ export class GeminiAdapter implements ProviderAdapter {
continue;
}

geminiContents.push({ role, parts });
}

// Handle case where system prompt was provided but no user message followed
if (systemPromptContent) {
Logger.warn("GeminiAdapter: System prompt provided but no user message found to merge it into. Adding as a separate initial user message.");
geminiContents.unshift({ role: 'user', parts: [{ text: systemPromptContent }] });
contents.push({ role, parts });
}

// Gemini specific validation: Ensure conversation doesn't start with 'model'
if (geminiContents.length > 0 && geminiContents[0].role === 'model') {
Logger.warn("Gemini conversation history starts with 'model' role. Prepending a dummy 'user' turn.", { firstRole: geminiContents[0].role });
geminiContents.unshift({ role: 'user', parts: [{ text: "(Initial context)" }] }); // Prepend a generic user turn
if (contents.length > 0 && contents[0].role === 'model') {
Logger.warn("Gemini conversation history starts with 'model' role. Prepending a dummy 'user' turn.", { firstRole: contents[0].role });
contents.unshift({ role: 'user', parts: [{ text: "(Initial context)" }] }); // Prepend a generic user turn
}

return geminiContents;
return { contents, systemInstruction };
}
}
Loading