From 5a0f0400b9f2d39c10dcef1452898380ead99140 Mon Sep 17 00:00:00 2001 From: foleydang Date: Sun, 20 Sep 2026 11:09:57 +0800 Subject: [PATCH] feat(agentstudio): session agent override + env_vars + mcp_configs Session create/update now support the agent override object form (type=agent_with_overrides with system/tools/mcp_servers/skills) in addition to the Agent ID string, plus the new top-level environment_variables and mcp_configs fields. - SessionCreateParam: agent accepts String (backward compatible), override Map, or SessionAgent; add environmentVariables/mcpConfigs. - SessionUpdateParam: add vaultIds/environmentVariables/agent(patch)/ mcpConfigs; shouldSerializeExplicitNulls preserves null sub-fields in the agent patch (delete-override semantics) on the wire. - Session model: add environment_variables/mcp_configs response fields. - SessionAgent: union helper (ofString/ofOverride + toJsonElement that preserves null sub-fields via serializeNulls gson). - Tests: session create/update body contract + SessionAgent union serialization + null-revoke coverage. Co-Authored-By: Claude Code --- .../dashscope/agentstudio/model/Session.java | 6 + .../agentstudio/param/SessionAgent.java | 103 ++++++++++++++ .../agentstudio/param/SessionCreateParam.java | 14 +- .../agentstudio/param/SessionUpdateParam.java | 25 ++++ .../alibaba/dashscope/TestAgentStudio.java | 132 +++++++++++++++++- 5 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionAgent.java diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java index 1d0223bd..a0fc5bf9 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java @@ -44,6 +44,12 @@ public class Session extends FlattenResultBase { @SerializedName("vault_ids") private List vaultIds; + @SerializedName("environment_variables") + private Map environmentVariables; + + @SerializedName("mcp_configs") + private List> mcpConfigs; + @SerializedName("stats") private Stats stats; diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionAgent.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionAgent.java new file mode 100644 index 00000000..c76fdc29 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionAgent.java @@ -0,0 +1,103 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import java.util.Map; +import lombok.EqualsAndHashCode; + +/** + * Session-level agent reference: an Agent ID string or an override object carrying + * system/tools/mcp_servers/skills. + */ +@EqualsAndHashCode +public final class SessionAgent { + private final String string; + private final Map override; + + private SessionAgent(String string, Map override) { + this.string = string; + this.override = override; + } + + /** + * The Agent ID string form — pins the latest version for the session. + * + * @throws IllegalArgumentException if {@code agentId} is {@code null}. + */ + public static SessionAgent ofString(String agentId) { + if (agentId == null) { + throw new IllegalArgumentException("agentId is required"); + } + return new SessionAgent(agentId, null); + } + + /** + * The Agent override object form ({@code type=agent_with_overrides} with Session-level {@code + * system}/{@code tools}/{@code mcp_servers}/{@code skills} overrides). The mapping is passed + * through verbatim. + * + * @throws IllegalArgumentException if {@code override} is {@code null}. + */ + public static SessionAgent ofOverride(Map override) { + if (override == null) { + throw new IllegalArgumentException("override is required"); + } + return new SessionAgent(null, override); + } + + /** @return {@code true} if this is the Agent ID string form. */ + public boolean isString() { + return string != null; + } + + /** @return the Agent ID string, or {@code null} if this is the override form. */ + public String asString() { + return string; + } + + /** @return the override mapping, or {@code null} if this is the string form. */ + public Map asOverride() { + return override; + } + + private static final Gson NULLS_GSON = + new GsonBuilder().serializeNulls().disableHtmlEscaping().create(); + + /** Serialize this reference to JSON: a string primitive or an override object. */ + public JsonElement toJsonElement() { + if (string != null) { + return new JsonPrimitive(string); + } + return NULLS_GSON.toJsonTree(override); + } + + /** Serialize a raw agent value (SessionAgent, String, or Map) to JSON. */ + public static JsonElement toJsonElement(Object agent) { + if (agent == null) { + throw new IllegalArgumentException("agent is required"); + } + if (agent instanceof SessionAgent) { + return ((SessionAgent) agent).toJsonElement(); + } + if (agent instanceof String) { + return new JsonPrimitive((String) agent); + } + if (agent instanceof Map) { + @SuppressWarnings("unchecked") + Map override = (Map) agent; + return NULLS_GSON.toJsonTree(override); + } + throw new IllegalArgumentException( + "agent must be a String (Agent ID), a Map (override), or a SessionAgent"); + } + + @Override + public String toString() { + return string != null + ? "SessionAgent{string=" + string + "}" + : "SessionAgent{override=" + override + "}"; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java index ac01e01c..a692136a 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java @@ -16,17 +16,21 @@ @SuperBuilder @EqualsAndHashCode(callSuper = true) public class SessionCreateParam extends FlattenHalfDuplexParamBase { - @NonNull private String agent; + /** Agent ID string, override Map, or SessionAgent. See {@link SessionAgent}. */ + @NonNull private Object agent; + @Default private String environmentId = null; @Default private String title = null; @Default private List> resources = null; @Default private List vaultIds = null; + @Default private Map environmentVariables = null; + @Default private List> mcpConfigs = null; @Default private Map metadata = null; @Override public JsonObject getHttpBody() { JsonObject body = new JsonObject(); - body.addProperty("agent", agent); + body.add("agent", SessionAgent.toJsonElement(agent)); if (environmentId != null) { body.addProperty("environment_id", environmentId); } @@ -39,6 +43,12 @@ public JsonObject getHttpBody() { if (vaultIds != null && !vaultIds.isEmpty()) { body.add("vault_ids", JsonUtils.toJsonElement(vaultIds)); } + if (environmentVariables != null && !environmentVariables.isEmpty()) { + body.add("environment_variables", JsonUtils.toJsonElement(environmentVariables)); + } + if (mcpConfigs != null && !mcpConfigs.isEmpty()) { + body.add("mcp_configs", JsonUtils.toJsonElement(mcpConfigs)); + } if (metadata != null && !metadata.isEmpty()) { body.add("metadata", JsonUtils.toJsonElement(metadata)); } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java index 73c9e5af..8f29972d 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java @@ -4,6 +4,7 @@ import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; import com.alibaba.dashscope.utils.JsonUtils; import com.google.gson.JsonObject; +import java.util.List; import java.util.Map; import lombok.Builder.Default; import lombok.Data; @@ -16,6 +17,18 @@ public class SessionUpdateParam extends FlattenHalfDuplexParamBase { @Default private String title = null; @Default private Map metadata = null; + @Default private List vaultIds = null; + @Default private Map environmentVariables = null; + /** Agent config patch (override Map or SessionAgent). See {@link SessionAgent}. */ + @Default private Object agent = null; + + @Default private List> mcpConfigs = null; + + @Override + public boolean shouldSerializeExplicitNulls() { + // Preserve null sub-fields inside the agent patch on the wire. + return agent != null; + } @Override public JsonObject getHttpBody() { @@ -26,6 +39,18 @@ public JsonObject getHttpBody() { if (metadata != null) { body.add("metadata", JsonUtils.toJsonElement(metadata)); } + if (vaultIds != null) { + body.add("vault_ids", JsonUtils.toJsonElement(vaultIds)); + } + if (environmentVariables != null) { + body.add("environment_variables", JsonUtils.toJsonElement(environmentVariables)); + } + if (agent != null) { + body.add("agent", SessionAgent.toJsonElement(agent)); + } + if (mcpConfigs != null) { + body.add("mcp_configs", JsonUtils.toJsonElement(mcpConfigs)); + } addExtraBody(body); return body; } diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java index 543f9d13..c3040375 100644 --- a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java @@ -33,6 +33,7 @@ import com.alibaba.dashscope.agentstudio.param.EnvironmentCreateParam; import com.alibaba.dashscope.agentstudio.param.EnvironmentListParam; import com.alibaba.dashscope.agentstudio.param.FileListParam; +import com.alibaba.dashscope.agentstudio.param.SessionAgent; import com.alibaba.dashscope.agentstudio.param.SessionCreateParam; import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; import com.alibaba.dashscope.agentstudio.param.SessionListParam; @@ -50,6 +51,7 @@ import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.utils.Constants; import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -59,6 +61,8 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -288,7 +292,11 @@ public void testSessionCreate() throws Exception { enqueue("session_response"); Session session = new Sessions(null, null, null) - .create(SessionCreateParam.builder().agent("agent_xyz").title("Test Session").build()); + .create( + SessionCreateParam.builder() + .agent(SessionAgent.ofString("agent_xyz")) + .title("Test Session") + .build()); RecordedRequest req = mockServer.takeRequest(); assertEquals("POST", req.getMethod()); assertTrue(req.getPath().endsWith("/sessions")); @@ -309,7 +317,7 @@ public void testSessionCreateWithVaultIds() throws Exception { new Sessions(null, null, null) .create( SessionCreateParam.builder() - .agent("agent_xyz") + .agent(SessionAgent.ofString("agent_xyz")) .vaultIds(Arrays.asList("vlt_x")) .metadata(Collections.singletonMap("biz_ticket_id", "1234")) .build()); @@ -323,11 +331,129 @@ public void testSessionCreateWithVaultIds() throws Exception { // Omitted vault_ids must not produce a null vault_ids in the body. enqueue("session_response"); - new Sessions(null, null, null).create(SessionCreateParam.builder().agent("agent_xyz").build()); + new Sessions(null, null, null) + .create(SessionCreateParam.builder().agent(SessionAgent.ofString("agent_xyz")).build()); JsonObject body2 = JsonUtils.parse(mockServer.takeRequest().getBody().readUtf8()); assertFalse(body2.has("vault_ids")); } + @Test + public void testSessionCreateWithAgentOverride() throws Exception { + // agent accepts an override object (type=agent_with_overrides) in + // addition to the Agent ID string; environment_variables / mcp_configs + // are new top-level fields. Omitted optional fields are not sent. + enqueue("session_response"); + Map agent = new HashMap<>(); + agent.put("type", "agent_with_overrides"); + agent.put("id", "agent_xxx"); + agent.put("version", 3); + agent.put("system", "你是此 Session 专用的助手。"); + agent.put("tools", Collections.emptyList()); + Map mcpServer = new HashMap<>(); + mcpServer.put("type", "official"); + mcpServer.put("name", "weather"); + agent.put("mcp_servers", Collections.singletonList(mcpServer)); + + Map headers = new HashMap<>(); + headers.put("X-Tenant-Id", "demo"); + Map mcpConfig = new HashMap<>(); + mcpConfig.put("mcp_server_name", "weather"); + mcpConfig.put("headers", headers); + + new Sessions(null, null, null) + .create( + SessionCreateParam.builder() + .agent(SessionAgent.ofOverride(agent)) + .title("项目分析") + .environmentVariables(Collections.singletonMap("LANG", "zh_CN.UTF-8")) + .mcpConfigs(Collections.singletonList(mcpConfig)) + .build()); + JsonObject body = JsonUtils.parse(mockServer.takeRequest().getBody().readUtf8()); + assertTrue(body.has("agent")); + assertEquals("agent_with_overrides", body.getAsJsonObject("agent").get("type").getAsString()); + assertEquals("agent_xxx", body.getAsJsonObject("agent").get("id").getAsString()); + assertTrue(body.getAsJsonObject("agent").get("tools").isJsonArray()); + assertEquals( + "zh_CN.UTF-8", body.getAsJsonObject("environment_variables").get("LANG").getAsString()); + assertEquals( + "demo", + body.getAsJsonArray("mcp_configs") + .get(0) + .getAsJsonObject() + .getAsJsonObject("headers") + .get("X-Tenant-Id") + .getAsString()); + // Omitted fields are absent — never sent as null. + assertFalse(body.has("resources")); + assertFalse(body.has("vault_ids")); + assertFalse(body.has("metadata")); + } + + @Test + public void testSessionUpdateAgentPatchAndClear() throws Exception { + // update: agent patch sub-fields — null revokes the override (must be + // preserved as JSON null, not dropped), "" / [] override to empty, an + // omitted sub-field stays absent. Top-level [] / {} explicitly clear. + enqueue("session_response"); + Map agentPatch = new HashMap<>(); + agentPatch.put("system", ""); + agentPatch.put("tools", null); + agentPatch.put("skills", Collections.emptyList()); + + new Sessions(null, null, null) + .update( + "sess_abc123", + SessionUpdateParam.builder() + .title("更新后") + .vaultIds(Collections.emptyList()) + .environmentVariables(new HashMap()) + .mcpConfigs(Collections.emptyList()) + .agent(SessionAgent.ofOverride(agentPatch)) + .build()); + JsonObject body = JsonUtils.parse(mockServer.takeRequest().getBody().readUtf8()); + // Top-level [] / {} clear (sent, not null, not omitted). + assertTrue(body.has("vault_ids")); + assertTrue(body.getAsJsonArray("vault_ids").isEmpty()); + assertTrue(body.has("environment_variables")); + assertTrue(body.getAsJsonObject("environment_variables").entrySet().isEmpty()); + assertTrue(body.has("mcp_configs")); + assertTrue(body.getAsJsonArray("mcp_configs").isEmpty()); + // Agent patch: null preserved (revoke), "" preserved (empty prompt), + // [] preserved (empty), omitted sub-field absent (keep). + JsonObject agent = body.getAsJsonObject("agent"); + assertTrue(agent.has("tools")); + assertTrue(agent.get("tools").isJsonNull()); + assertEquals("", agent.get("system").getAsString()); + assertTrue(agent.getAsJsonArray("skills").isEmpty()); + assertFalse(agent.has("mcp_servers")); + } + + @Test + public void testSessionAgentUnionSerialization() { + // string form -> JSON string primitive + JsonElement s = SessionAgent.ofString("agent_01").toJsonElement(); + assertTrue(s.isJsonPrimitive()); + assertEquals("agent_01", s.getAsString()); + + // override form preserves null sub-fields (revoke override) and [] / "" + Map patch = new HashMap<>(); + patch.put("system", ""); + patch.put("tools", null); + patch.put("skills", Collections.emptyList()); + JsonElement o = SessionAgent.ofOverride(patch).toJsonElement(); + assertTrue(o.isJsonObject()); + assertTrue(o.getAsJsonObject().get("tools").isJsonNull()); + assertEquals("", o.getAsJsonObject().get("system").getAsString()); + assertTrue(o.getAsJsonObject().getAsJsonArray("skills").isEmpty()); + + // static toJsonElement accepts String / Map / SessionAgent + assertEquals("agent_01", SessionAgent.toJsonElement("agent_01").getAsString()); + assertTrue(SessionAgent.toJsonElement(patch).isJsonObject()); + // unsupported type -> IllegalArgumentException + assertThrows(IllegalArgumentException.class, () -> SessionAgent.toJsonElement(42)); + assertThrows(IllegalArgumentException.class, () -> SessionAgent.toJsonElement(null)); + } + @Test public void testSessionRetrieve() throws Exception { enqueue("session_response");