Cherry-pick from develop to release/1.0.1#2142
Open
Andrey Bragin (EugeneTheDev) wants to merge 28 commits into
Open
Cherry-pick from develop to release/1.0.1#2142Andrey Bragin (EugeneTheDev) wants to merge 28 commits into
Andrey Bragin (EugeneTheDev) wants to merge 28 commits into
Conversation
<!-- Thank you for opening a pull request! Please add a brief description of the proposed change here. Also, please tick the appropriate points in the checklist below. --> feat(CliAIAgent): introduce `CliAIAgent` which calls another cli agent under the hood: e.g. claude, codex, etc. `CliAIAgent` is an instance of `AIAgent`, and can also be used as a node in a graph, e.g. `val claudeNode by ClaudeCodeAgent(...).asNode()` ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> --- #### Type of the changes - [x] New feature (non-breaking change which adds functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Tests improvement - [ ] Refactoring - [ ] CI/CD changes - [ ] Dependencies update #### Checklist - [x] The pull request has a description of the proposed change - [x] I read the [Contributing Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md) before opening the pull request - [x] The pull request uses **`develop`** as the base branch - [x] Tests for the changes have been added - [x] All new and existing tests passed ##### Additional steps for pull requests adding a new feature - [ ] An issue describing the proposed change exists - [ ] The pull request includes a link to the issue - [ ] The change was discussed and approved in the issue - [ ] Docs have been added / updated (cherry picked from commit 0a70557)
(cherry picked from commit 396f000)
<!-- PR title should follow Conventional Commits format: type(scope): description For breaking changes, append ! after type/scope: type(scope)!: description Examples: feat(agents): add streaming response node fix(prompt): handle null responses in PromptExecutor refactor(agents)!: remove deprecated methods from Tool See CONTRIBUTING.md for more details --> Add `Gemini3_1Pro_Preview`, `Gemini3_1FlashLite_Preview`, `Gemini3_1FlashLite`, and `Gemini3_5Flash` profiles. <!-- Include DEPRECATED section below only if this PR deprecates any public APIs. Otherwise, delete it. --> DEPRECATED: * `Gemini 3 Pro Preview` profile is no longer supported. `Gemini3_1Pro_Preview` is recommended instead. (cherry picked from commit 2689548)
…f nullable collections (#1943) ## Summary - Nullable `List<X>?` fields produced `{"type": ["array", "null"], "items": {...}}`, which OpenAI's GPT-5-family strict-mode validator rejects (surfaces as `'Invalid schema for response_format ... got "type": "None"'`). Switch `OpenAIStandardJsonSchemaGenerator.processList` to emit `{"anyOf": [{"type": "array", "items": ...}, {"type": "null"}]}` for nullable lists, mirroring the shape already used for nullable objects. - Non-nullable lists, nullable primitives, and the LLM-agnostic `StandardJsonSchemaGenerator` are unchanged. Only the OpenAI-specific generator is touched, so other providers keep their existing type-union encoding. - Adds a regression test (`testGenerateOpenAIStandardJsonSchemaForNullableLists`) covering both primitive (`List<String>?`) and `$ref` (`List<Tag>?`) item types. ## Out of scope (could follow up) - `OpenAIBasicJsonSchemaGenerator` still emits the OpenAPI-3.0-style `nullable: true` keyword, which OpenAI strict mode silently ignores. Same root cause family, different generator — happy to file a follow-up if useful. closes #1930 (cherry picked from commit 54b8ecb)
…2085) <!-- PR title should follow Conventional Commits format: type(scope): description For breaking changes, append ! after type/scope: type(scope)!: description Examples: feat(agents): add streaming response node fix(prompt): handle null responses in PromptExecutor refactor(agents)!: remove deprecated methods from Tool See CONTRIBUTING.md for more details --> Add round-trip tests for redesigned message parts. (cherry picked from commit 75f1351)
ci(agents): add public API compatibility checks via ABI validation Enable Kotlin's built-in ABI validation to catch breaking changes to the public API before they reach the main branch. The check runs as part of the CI compilation job and blocks merge if any public API has changed without updating the committed dumps. ### What's included - **Convention plugins**: Enabled `abiValidation` in both `ai.kotlin.multiplatform.gradle.kts` and `ai.kotlin.jvm.gradle.kts`. - **Beta module exclusion**: Modules with `isBeta = true` have all ABI tasks disabled automatically. - **CI integration**: Added `./gradlew checkLegacyAbi` step to the `compilation` job in `.github/workflows/checks.yml`. - **Contribution guide**: Documented the public API compatibility workflow in `CONTRIBUTING.md`, including how to update dumps when making intentional API changes. ### Developer workflow When changing a public API in a stable module: 1. Make code changes 2. Run `./gradlew updateLegacyAbi` 3. Review the diff in `api/` directories 4. Commit the updated `.api` files alongside code changes (cherry picked from commit 7cee8b8)
<!-- PR title should follow Conventional Commits format: type(scope): description For breaking changes, append ! after type/scope: type(scope)!: description Examples: feat(agents): add streaming response node fix(prompt): handle null responses in PromptExecutor refactor(agents)!: remove deprecated methods from Tool See CONTRIBUTING.md for more details --> Replace a deprecated Google model with an up-to-date version. Add DeepSeek integration tests, ACP message conversion tests, Metadata retrieval tests, and nullable tool schema support. Fix Ollama tests run. (cherry picked from commit e439121)
) Closes #2002. ### Summary `StreamFrameFlowBuilder.emitToolCallDelta()` treated every chunk with a non-null tool-call `id` as a new tool call. This fix treats a repeated `id` as a continuation of the pending tool call, so OpenAI-compatible providers that repeat the same `id` on each streaming chunk no longer fragment one logical tool call into multiple premature `ToolCallComplete` frames. ### Fix A new tool call now begins only when a present `id` or `index` differs from the pending call, instead of whenever `id` is non-null. Before: ```kotlin val new: PendingToolCall = if (sanitizedId != null || index != previous?.index) { ``` After: ```kotlin val isNewToolCall = (sanitizedId != null && sanitizedId != previous?.id) || (index != null && index != previous?.index) val new: PendingToolCall = if (isNewToolCall) { ``` This follows the same id-change principle as `emitReasoningDelta`; absent or blank ids still continue the pending call (the blank case preserves the #1915 handling). The fix lives in the shared `StreamFrameFlowBuilder` because all streaming clients (OpenAI, OpenRouter, Mistral, Anthropic, DeepSeek, Ollama, Bedrock, Dashscope, Google) emit tool-call deltas through it. ### Tests Two regression tests in `StreamFrameFlowBuilderTest`: `testEmitToolCallDeltaWithRepeatedIdAppendsToExisting` (three chunks with the same `id` combine into one `ToolCallComplete`) and `testEmitToolCallDeltaWithRepeatedIdSeparatesDistinctToolCalls` (switching to a different `id` still starts a separate tool call). `:prompt:prompt-model:jvmTest` (20 tests, 0 failures), `wasmJsNodeTest`, and `ktlintCheck` all pass locally; the full `./gradlew build` (Android and browser targets) is left to CI, since this contributor environment lacks the Android SDK and a headless browser. --------- Co-authored-by: Anastasiia.Zarechneva <Anastasiia.Zarechneva@jetbrains.com> (cherry picked from commit 581d6af)
…2099) <!-- PR title should follow Conventional Commits format: type(scope): description For breaking changes, append ! after type/scope: type(scope)!: description Examples: feat(agents): add streaming response node fix(prompt): handle null responses in PromptExecutor refactor(agents)!: remove deprecated methods from Tool See CONTRIBUTING.md for more details --> Drop .lowercase() call in AnthropicLLMClient enum serialization. <!-- Include references to related issues below, e.g., closes #1, closes KG-1. Otherwise, delete it. --> closes #2020 (cherry picked from commit c779c04)
) Introduces model resolution as a preliminary step before prompt execution—allowing decorators (such as `ContextualPromptExecutor`) to be certain which `LLModel` will be used for the given operation. Currently, dynamic model resolution happens internally (ie via fallback in `MultiLLMPromptExecutor`) which might result in misleading model information (ie `ContextualPromptExecutor` triggers `onLLMCallCompleted` with `requestModel` instead of `effectiveModel`). Adds `resolveModel` and execution paths accepting `ResolvedModel` to `PromptExecutorAPI`. All of these have default implementations that make existing executors work in the same way as before. Adds `DynamicPromptExecutor`: a `PromptExecutor` subclass that exposes model resolution as an explicit step (`resolveModel`) before LLM dispatch. Subclasses translate a requested LLModel into a ResolvedModel (e.g. fallback selection); the LLModel entry points are finalized to flow through resolveModel exactly once per call. `MultiLLMPromptExecutor` and `RoutingLLMPromptExecutor` move fallback logic into `resolveModel`. PromptExecutor stays open — existing direct subclasses are unaffected. (cherry picked from commit 6befd8b)
`FinishTool` (the internal tool that `subgraphWithTask<Input, Output>` uses to carry the structured result) generates its schema via `kotlinx-schema-generator-json`. For a sealed `Output` two things stacked: - `JsonSchema.toActualPropertyDefinition` only dispatched on `type` and dropped the `oneOf` that the generator emits for polymorphic top-level schemas - FinishTool requested `includePolymorphicDiscriminator = false`, so even the per-branch schemas omitted the `type` const that kotlinx-serialization needs to decode back into a sealed instance. The combined effect: `subgraphWithTask<String, MySealedInterface>` advertised an empty `result` Object to the LLM and could not decode whatever it returned. The converter now prefers `oneOf`/`anyOf` when present, and FinishTool generates its schema with the discriminator enabled. Other tools keep `defaultJsonSchemaConfig` unchanged. OpenAI and Google emit the resulting `anyOf` directly, and Anthropic's pre-existing "AnyOf type is not supported" limitation continues to apply there. New tests pin the converter at the schema layer, the FinishTool descriptor and round-trip, and the end-to-end behavior through `subgraphWithTask` with a raw `PromptExecutor` returning the polymorphic JSON. closes #1941 (cherry picked from commit 482a434)
…erator (#1875) `executeStructured` crashed with `IllegalArgumentException: Encountered unsupported type while generating JSON schema: CONTEXTUAL` when the response data class contained a `@Contextual` property (e.g. `java.util.UUID`, any non-`@Serializable` type registered via `SerializersModule`). `GenericJsonSchemaGenerator.process()` had no branch for `SerialKind.CONTEXTUAL`, so it fell through to the `else` clause and threw immediately — before any LLM request was made. Added an explicit `SerialKind.CONTEXTUAL` branch that: 1. Calls `serializersModule.getContextualDescriptor(descriptor)` (stable API in kotlinx.serialization 1.7+) to resolve the actual serializer registered in the `Json` instance's module. 2. Delegates to `process()` recursively with the resolved descriptor, so the type is generated correctly (e.g. a UUID serialized as a string → `"type": "string"`). 3. If no contextual serializer is found, throws a **descriptive** `IllegalArgumentException` explaining how to fix it (register via `SerializersModule { contextual(...) }` or use `@Serializable(with = ...)`). | File | Change | |---|---| | `GenericJsonSchemaGenerator.kt` | Add `SerialKind.CONTEXTUAL` branch; `@OptIn(ExperimentalSerializationApi::class)` on class | | `JsonSchemaGeneratorTest.kt` | 3 new test cases: basic generator, standard generator, and missing-serializer error path | ``` ./gradlew :prompt:prompt-structure:jvmTest --tests "ai.koog.prompt.structure.json.generator.JsonSchemaGeneratorTest" BUILD SUCCESSFUL ``` All 3 new tests pass alongside the existing suite. Use `@Serializable(with = MySerializer::class)` on the property instead of `@Contextual`, as noted in the issue. closes #1486 (cherry picked from commit 6ee3b63)
) Added support for DataStore with Okio on the web target. Consequently, MemoryAppSettings has been replaced with a DataStore-based implementation. (cherry picked from commit 45e3730)
…tible requests (#2096) ## What `MessagePart.Tool.Call.args` already holds the **JSON-encoded** arguments string (the parse side builds it via the `JsonObject` constructor, and `function.arguments` is read back with `Json.parseToJsonElement(...)`). When rebuilding request history it was passed through `Json.encodeToString()` **again**, producing a **double-encoded** (quoted) JSON string on the wire. OpenAI tolerates this, but strict OpenAI-compatible backends — notably **DashScope / Qwen** — reject the request: > `<400> InternalError.Algo.InvalidParameter: The "function.arguments" parameter of the code model must be in JSON format.` This breaks every tool-calling **agent** loop on DashScope at the follow-up `nodeLLMSendToolResults` call (the first tool call runs; replaying the result 400s). ## Fix Emit `args` verbatim in both serialization paths: - `AbstractOpenAILLMClient.createMessages` — Chat Completions - `OpenAILLMClient.convertPromptToResponsesMessages` — Responses API ### On the wire ```jsonc // before (double-encoded) — decodes to the STRING "{\"a\":3,\"b\":4}" "arguments":"\"{\\\"a\\\":3,\\\"b\\\":4}\"" // after — decodes to the OBJECT {"a":3,"b":4} "arguments":"{\"a\":3,\"b\":4}" ``` ## Tests Adds `testToolCallArgumentsAreNotDoubleEncodedInRequest` to `OpenAIChatCompletionLLMClientTest`: it replays an assistant tool call through the client, captures the outgoing request body, and asserts `function.arguments` decode to a `JsonObject` (not a double-encoded string). The pre-existing round-trip tests only asserted the tool-call **name**, never the **arguments**, which is why this regressed unnoticed. Also verified end-to-end against the live DashScope `qwen-plus` endpoint: the single-encoded form returns `200`, the double-encoded form returns the `400` above. > Reproduced against the **China-mainland** endpoint — base URL `https://dashscope.aliyuncs.com/` with the compatible-mode path `compatible-mode/v1/chat/completions` (note `DashscopeClientSettings` defaults to the international host `https://dashscope-intl.aliyuncs.com/`). The defect is in shared serialization, so it is endpoint-independent, but the captured 400 above came from the mainland path. closes #2095 (cherry picked from commit 485e294)
<!-- PR title should follow Conventional Commits format: type(scope): description For breaking changes, append ! after type/scope: type(scope)!: description Examples: feat(agents): add streaming response node fix(prompt): handle null responses in PromptExecutor refactor(agents)!: remove deprecated methods from Tool See CONTRIBUTING.md for more details --> Widens `Message.Tool.Result.parts` from `List<ContentPart.Text>` to `List<ContentPart>`, allowing tools to return images and files alongside text. Anthropic, OpenAI and Gemini clients handle the new content types; unsupported API paths throw a clear error. Closes #1491 (cherry picked from commit 6a35c2d)
…serve generics (#2116) `JacksonSerializer.decodeFromJSONElement` was passing `javaType.rawClass` to `objectMapper.treeToValue`, discarding generic type parameters. This causes parameterized types (e.g. `List<Person>`) to deserialize elements as `Map` instead of the expected type. The fix passes the full `JavaType` to `treeToValue`, consistent with how `decodeFromString` already works. This also removes the now-unnecessary `@Suppress("UNCHECKED_CAST")` and explicit `as T` cast. closes #2115 (cherry picked from commit 58a8168)
<!-- PR title should follow Conventional Commits format: type(scope): description For breaking changes, append ! after type/scope: type(scope)!: description Examples: feat(agents): add streaming response node fix(prompt): handle null responses in PromptExecutor refactor(agents)!: remove deprecated methods from Tool See CONTRIBUTING.md for more details --> Add support for the newly released Anthropic Claude Fable 5 model. (cherry picked from commit e4e9e40)
Rename MessageTokenizer storage key:
"agents-features-tracing"->"agents-features-message-tokenizer"
MessageTokenizer AIAgentStorageKey named "agents-features-tracing",
causing a runtime exception when Tracing feature is also installed (they
have the same name):
```
ai.koog.agents.core.agent.GraphAIAgent - Execution exception reported by server!
java.lang.IllegalArgumentException: Feature ai.koog.agents.core.agent.entity.AIAgentStorageKey@35e21111(name=agents-features-tracing) is found, but it is not of the expected type.
Expected type: MessageTokenizer
Actual type: Tracing
at ai.koog.agents.core.feature.pipeline.AIAgentPipelineImpl.feature(AIAgentPipelineImpl.kt:104)
```
Closes #2143
(cherry picked from commit d97a6e4)
Strategy input on `StrategyStartingContext`: - Added `input: Any?` field to `StrategyStartingContext` (mirrors the `result: Any?` field already on `StrategyCompletedContext`). - Use case: sometimes it is useful to hook into the agent's execution and perform an action that depends on the user's query at runtime, e.g. retrieve relevant memories from a dedicated collection and inject them into the prompt before the strategy starts. Without the input on the starting event there was no way for a feature to see what the agent had been invoked with. (cherry picked from commit a720371)
… support (#1889) Small public API addition to support the agent optimization infrastructure built on top of koog. **`freshHistory` parameter for subgraphs:** Added `freshHistory: Boolean = false` to `AIAgentSubgraph`, all `subgraph()` DSL overloads, all `subgraphWithTask()` overloads, and `setupSubgraphWithTask()` When `true`, the subgraph starts with an empty conversation history instead of inheriting the parent context's prompt. The subgraph's history is discarded upon completion. Use case: optimizable subgraphs where each module should reason independently, and where few-shot demonstrations should be the only conversation history. --------- Co-authored-by: Denis Lochmelis <denis.lochmelis@jetbrains.com> (cherry picked from commit a457663)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.