Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@

- [ai-core] discovered skills from `.agents/skills` directories alongside `.prompts/skills` (workspace and home directory) [#17553](https://github.com/eclipse-theia/theia/pull/17553)
- [terminal] fixed Cmd+V / Ctrl+V paste in the integrated terminal and restored the effect of the `terminal.enablePaste` and `terminal.enableCopy` preferences [#17603](https://github.com/eclipse-theia/theia/pull/17603)
- [ai-anthropic, ai-google, ai-ollama, ai-openai, ai-copilot] added rebindable `<provider>LanguageModelFactory` bindings for instantiating provider language models [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-core] executed a model turn's tool calls concurrently, including parallel agent delegations, instead of sequentially, via a new injectable `ToolCallExecutor` [#17623](https://github.com/eclipse-theia/theia/pull/17623)

<a name="breaking_changes_1.73.0">[Breaking Changes:](#breaking_changes_1.73.0)</a>

- [ai-core] `DefaultSkillService.getDefaultSkillsDirectoryPath()` has been renamed to `getDefaultSkillsDirectoryPaths()` and now returns `string[]` instead of `string` to include both the product configuration `skills` directory and the user's `~/.agents/skills` directory [#17553](https://github.com/eclipse-theia/theia/pull/17553)
- [ai-core] `combineSkillDirectories` signature changed: `workspaceSkillsDir` and `defaultSkillsDir` parameters are now `string[]` (previously `string | undefined`), and the return type is now `SkillDirectoryEntry[]` (an array of `{ path, tier }` entries) instead of `string[]` [#17553](https://github.com/eclipse-theia/theia/pull/17553)
- [terminal] `TerminalWidget` gained a new abstract method `paste(text: string)`; downstream subclasses must implement it (consistent with `getSelection()` / `hasSelection()` added in [#17290](https://github.com/eclipse-theia/theia/pull/17290)) [#17603](https://github.com/eclipse-theia/theia/pull/17603)
- [ai-openai] `OpenAiResponseApiUtils` is now bound in the connection-scoped backend container instead of the root container, so it can no longer be injected into root-scoped services [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-openai] `OpenAiLanguageModelsManagerImpl` no longer injects `OpenAiModelUtils` or `OpenAiResponseApiUtils` (the `openAiModelUtils` and `responseApiUtils` protected fields were removed); provider models are now constructed via the injected `OpenAiLanguageModelFactory` [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-openai, ai-copilot] `OpenAiModel.createTools()` and `CopilotLanguageModel.createTools()` now return `ChatCompletionTool[]` instead of `RunnableToolFunctionWithoutParse[]`, because the OpenAI SDK `runTools` runner is no longer used [#17623](https://github.com/eclipse-theia/theia/pull/17623)

## 1.72.0 - 5/28/2026

Expand Down
21 changes: 21 additions & 0 deletions packages/ai-anthropic/src/node/anthropic-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,37 @@
// *****************************************************************************

import { ContainerModule } from '@theia/core/shared/inversify';
import { ToolCallExecutor } from '@theia/ai-core';
import { ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, AnthropicLanguageModelsManager } from '../common/anthropic-language-models-manager';
import { ConnectionHandler, PreferenceContribution, RpcConnectionHandler } from '@theia/core';
import { AnthropicLanguageModelsManagerImpl } from './anthropic-language-models-manager-impl';
import { AnthropicLanguageModelFactory, AnthropicModel, AnthropicModelParams } from './anthropic-language-model';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { AnthropicPreferencesSchema } from '../common/anthropic-preferences';

// We use a connection module to handle AI services separately for each frontend.
const anthropicConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService, bindFrontendService }) => {
bind(AnthropicLanguageModelsManagerImpl).toSelf().inSingletonScope();
bind(AnthropicLanguageModelsManager).toService(AnthropicLanguageModelsManagerImpl);
bind(AnthropicLanguageModelFactory).toFactory<AnthropicModel, [AnthropicModelParams]>(
Comment thread
cdamus marked this conversation as resolved.
({ container }) => params => new AnthropicModel(
params.id,
params.model,
params.status,
params.enableStreaming,
params.useCaching,
params.apiKey,
params.url,
params.maxTokens,
params.maxRetries,
params.proxy,
params.reasoningSupport,
params.reasoningApi,
params.supportsXHighEffort,
params.maxInputTokens,
container.get(ToolCallExecutor)
)
);
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(AnthropicLanguageModelsManager))
).inSingletonScope();
Expand Down
43 changes: 30 additions & 13 deletions packages/ai-anthropic/src/node/anthropic-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
// *****************************************************************************

import {
createToolCallError,
ImageContent,
ImageMimeType,
LanguageModel,
Expand All @@ -29,7 +28,7 @@ import {
ReasoningApi,
ReasoningSupport,
ToolCallResult,
ToolInvocationContext,
ToolCallExecutor,
UserRequest
} from '@theia/ai-core';
import { CancellationToken, isArray } from '@theia/core';
Expand Down Expand Up @@ -218,6 +217,27 @@ function formatToolCallResult(result: ToolCallResult): ToolResultBlockParam['con
return result;
}

/** Parameters for constructing an {@link AnthropicModel}. */
Comment thread
cdamus marked this conversation as resolved.
Outdated
export interface AnthropicModelParams {
id: string;
model: string;
status: LanguageModelStatus;
enableStreaming: boolean;
useCaching: boolean;
apiKey: () => string | undefined;
url: string | undefined;
maxTokens?: number;
maxRetries?: number;
proxy?: string;
reasoningSupport?: ReasoningSupport;
reasoningApi?: ReasoningApi;
supportsXHighEffort?: boolean;
maxInputTokens?: number;
}

export const AnthropicLanguageModelFactory = Symbol('AnthropicLanguageModelFactory');
export type AnthropicLanguageModelFactory = (params: AnthropicModelParams) => AnthropicModel;

/**
* Implements the Anthropic language model integration for Theia. Reasoning-level
* translation lives in {@link anthropicReasoningFor}.
Expand All @@ -238,7 +258,8 @@ export class AnthropicModel implements LanguageModel {
public reasoningSupport?: ReasoningSupport,
public reasoningApi?: ReasoningApi,
public supportsXHighEffort?: boolean,
public maxInputTokens?: number
public maxInputTokens?: number,
protected readonly toolCallExecutor: ToolCallExecutor = new ToolCallExecutor()
) { }

protected getSettings(request: LanguageModelRequest): Readonly<Record<string, unknown>> {
Expand Down Expand Up @@ -379,16 +400,12 @@ export class AnthropicModel implements LanguageModel {
}
}
if (toolCalls.length > 0) {
const toolResult = await Promise.all(toolCalls.map(async tc => {
const tool = request.tools?.find(t => t.name === tc.name);
const argsObject = tc.args.length === 0 ? '{}' : tc.args;
const handlerResult = tool
? await tool.handler(argsObject, ToolInvocationContext.create(tc.id))
: createToolCallError(`Tool '${tc.name}' not found in the available tools for this request.`, 'tool-not-available');

return { name: tc.name, result: handlerResult, id: tc.id, arguments: argsObject };

}));
// Tool calls of a single turn are executed concurrently; see ToolCallExecutor.
Comment thread
cdamus marked this conversation as resolved.
Outdated
const toolResult = await that.toolCallExecutor.executeToolCalls(
toolCalls.map(tc => ({ id: tc.id, name: tc.name, arguments: tc.args.length === 0 ? '{}' : tc.args })),
request.tools,
{ cancellationToken }
);

const calls = toolResult.map(tr => ({ finished: true, id: tr.id, result: tr.result, function: { name: tr.name, arguments: tr.arguments } }));
yield { tool_calls: calls };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { createProxyFetch, getProxyUrl } from '@theia/ai-core/lib/node';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Anthropic } from '@anthropic-ai/sdk';
import type { ModelInfo } from '@anthropic-ai/sdk/resources/models';
import { AnthropicModel, DEFAULT_MAX_TOKENS } from './anthropic-language-model';
import { AnthropicLanguageModelFactory, AnthropicModel, DEFAULT_MAX_TOKENS } from './anthropic-language-model';
import { AnthropicLanguageModelsManager, AnthropicModelDescription } from '../common';

const ANTHROPIC_REASONING_SUPPORT: ReasoningSupport = {
Expand Down Expand Up @@ -47,6 +47,9 @@ export class AnthropicLanguageModelsManagerImpl implements AnthropicLanguageMode
@inject(LanguageModelRegistry)
protected readonly languageModelRegistry: LanguageModelRegistry;

@inject(AnthropicLanguageModelFactory)
protected readonly anthropicLanguageModelFactory: AnthropicLanguageModelFactory;

get apiKey(): string | undefined {
return this._apiKey ?? process.env.ANTHROPIC_API_KEY;
}
Expand Down Expand Up @@ -94,22 +97,22 @@ export class AnthropicLanguageModelsManagerImpl implements AnthropicLanguageMode
});
} else {
this.languageModelRegistry.addLanguageModels([
new AnthropicModel(
modelDescription.id,
modelDescription.model,
this.anthropicLanguageModelFactory({
id: modelDescription.id,
model: modelDescription.model,
status,
modelDescription.enableStreaming,
modelDescription.useCaching,
apiKeyProvider,
modelDescription.url,
metadata.maxTokens,
modelDescription.maxRetries,
proxyUrl,
metadata.reasoningSupport,
metadata.reasoningApi,
metadata.supportsXHighEffort,
metadata.maxInputTokens
)
enableStreaming: modelDescription.enableStreaming,
useCaching: modelDescription.useCaching,
apiKey: apiKeyProvider,
url: modelDescription.url,
maxTokens: metadata.maxTokens,
maxRetries: modelDescription.maxRetries,
proxy: proxyUrl,
reasoningSupport: metadata.reasoningSupport,
reasoningApi: metadata.reasoningApi,
supportsXHighEffort: metadata.supportsXHighEffort,
maxInputTokens: metadata.maxInputTokens
})
]);
}
}
Expand Down
9 changes: 7 additions & 2 deletions packages/ai-chat/src/common/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,8 @@ class ChatResponseImpl implements ChatResponse {
protected _content: ChatResponseContent[];
protected _responseRepresentation: string;
protected _responseRepresentationForDisplay: string;
/** Forwarding listeners on per-content change events, disposed and rebuilt whenever the content is reset. */
protected readonly toDisposeOnClearContent = new DisposableCollection();

constructor() {
this._content = [];
Expand All @@ -2799,6 +2801,7 @@ class ChatResponseImpl implements ChatResponse {
}

clearContent(): void {
this.toDisposeOnClearContent.dispose();
this._content = [];
this._updateResponseRepresentation();
this._onDidChangeEmitter.fire();
Expand Down Expand Up @@ -2833,8 +2836,10 @@ class ChatResponseImpl implements ChatResponse {
this._content.push(nextContent);
// Forward content-level change events (e.g. partial-result updates from a
// renderer) so auto-save can persist them. Without this, mutations that
// don't go through addContent/merge are invisible to listeners.
nextContent.onDidChange(() => this._onDidChangeEmitter.fire());
// don't go through addContent/merge are invisible to listeners. Registered against
// toDisposeOnClearContent so the subscription is disposed when the content is cleared,
// otherwise re-adding the same content (as the stream parser does on every text token) leaks listeners.
nextContent.onDidChange(() => this._onDidChangeEmitter.fire(), undefined, this.toDisposeOnClearContent);
}
} else {
const lastElement = this._content.length > 0
Expand Down
22 changes: 22 additions & 0 deletions packages/ai-chat/src/common/chat-response-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ describe('MutableChatResponseModel', () => {
// lost on reload.
expect(fireCount).to.equal(1);
});

it('does not leak content-change listeners when content is repeatedly cleared and re-added', () => {
const response = new MutableChatResponseModel('req-1');
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'tool', '{}', false);
response.response.addContent(toolCall);

// The stream parser clears and re-adds prior content (e.g. a preceding tool call) on
// every streamed text token. Each re-add must not accumulate another forwarding listener.
for (let i = 0; i < 50; i++) {
response.response.clearContent();
response.response.addContents([toolCall]);
}

let fireCount = 0;
response.onDidChange(() => { fireCount++; });

toolCall.updateResult('partial');

// A single content change must propagate exactly once regardless of how many times the
// content was re-added; otherwise each re-add leaks another listener on the content.
expect(fireCount).to.equal(1);
});
});

describe('setTokenUsage', () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/ai-copilot/src/node/copilot-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { ContainerModule } from '@theia/core/shared/inversify';
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core';
import { ToolCallExecutor } from '@theia/ai-core';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import {
CopilotLanguageModelsManager,
Expand All @@ -26,6 +27,7 @@ import {
} from '../common';
import { CopilotOAuthConfig, DEFAULT_COPILOT_OAUTH_CONFIG } from '../common/copilot-oauth-config';
import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
import { CopilotLanguageModel, CopilotLanguageModelFactory, CopilotLanguageModelParams } from './copilot-language-model';
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';

const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
Expand All @@ -35,6 +37,21 @@ const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bind(CopilotLanguageModelsManagerImpl).toSelf().inSingletonScope();
bind(CopilotLanguageModelsManager).toService(CopilotLanguageModelsManagerImpl);

bind(CopilotLanguageModelFactory).toFactory<CopilotLanguageModel, [CopilotLanguageModelParams]>(
({ container }) => params => new CopilotLanguageModel(
params.id,
params.model,
params.status,
params.enableStreaming,
params.supportsStructuredOutput,
params.maxRetries,
params.accessTokenProvider,
params.enterpriseUrlProvider,
params.userAgentProvider,
container.get(ToolCallExecutor)
)
);

bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler<CopilotAuthServiceClient>(
COPILOT_AUTH_SERVICE_PATH,
Expand Down
22 changes: 21 additions & 1 deletion packages/ai-copilot/src/node/copilot-language-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { expect } from 'chai';
import { LanguageModelMessage } from '@theia/ai-core';
import { LanguageModelMessage, LanguageModelRequest } from '@theia/ai-core';
import { ChatCompletionMessageParam } from 'openai/resources';
import { CopilotLanguageModel } from './copilot-language-model';

Expand All @@ -37,6 +37,10 @@ class TestableCopilotLanguageModel extends CopilotLanguageModel {
public callProcessMessages(messages: LanguageModelMessage[]): ChatCompletionMessageParam[] {
return this.processMessages(messages);
}

public callCreateTools(request: LanguageModelRequest): unknown {
return this.createTools(request);
}
}

describe('CopilotLanguageModel - processMessages', () => {
Expand Down Expand Up @@ -104,3 +108,19 @@ describe('CopilotLanguageModel - processMessages', () => {
expect(result[2]).to.deep.equal({ role: 'assistant', content: 'final answer' });
});
});

describe('CopilotLanguageModel - createTools', () => {
it('produces plain function tool definitions without an embedded handler function', () => {
const model = new TestableCopilotLanguageModel();
const tools = model.callCreateTools({
messages: [],
tools: [{ id: 't', name: 't', parameters: { type: 'object', properties: {} }, handler: async () => 'x' }]
}) as Array<{ type: string; function: Record<string, unknown> }>;

expect(tools).to.have.lengthOf(1);
expect(tools[0].type).to.equal('function');
expect(tools[0].function.name).to.equal('t');
// The SDK runTools() runner is no longer used, so no executable function is embedded.
expect('function' in tools[0].function).to.equal(false);
});
});
Loading
Loading