From 0451c06cc9741d5ddc18612ff19a60ff0e4cc839 Mon Sep 17 00:00:00 2001 From: foleydang Date: Tue, 8 Sep 2026 22:52:17 +0800 Subject: [PATCH 1/3] feat(agentstudio): tool approval, event deltas, session resources/threads, security, message accessors - P0: tool_approval_request/response (replaces tool_confirmation), StopReason pending_batch_id/pending_call_ids, PermissionPolicy, SessionStatus rescheduled/deleted, StopReasonType/PermissionPolicyType, userToolApprovalResponse, event_ids deleted, types[] query fix - P1: event_deltas streaming (stream(eventDeltas) + TextDeltaStream), SessionResources (add/retrieve/list/delete), SessionThreads (list/retrieve/archive + events.list), Security (overview/listAgentLogs) - P2: Message accessors (toolApprovalRequest/error/pendingToolApprovals/ modelRequestEnd/outcomeEvaluation/threadStatus/threadCreated/ sessionUpdated/threadMessageRouting/eventStart/eventDelta/deltaText), RosterEntry name/description, PermissionPolicy model Co-Authored-By: Claude Code --- .../agentstudio/AgentStudioClient.java | 7 + .../agentstudio/AgentStudioConstants.java | 23 ++- .../agentstudio/message/ClientEvents.java | 31 ++- .../agentstudio/message/Message.java | 121 ++++++++++- .../dashscope/agentstudio/model/Configs.java | 12 ++ .../dashscope/agentstudio/model/Session.java | 60 +++++- .../param/SecurityListAgentLogsParam.java | 55 +++++ .../param/SessionEventListParam.java | 4 +- .../param/SessionResourceAddParam.java | 31 +++ .../param/SessionResourceListParam.java | 30 +++ .../param/SessionThreadListParam.java | 30 +++ .../resource/AgentStudioEventStream.java | 96 ++++++++- .../agentstudio/resource/Security.java | 172 ++++++++++++++++ .../agentstudio/resource/SessionEvents.java | 26 ++- .../resource/SessionResources.java | 138 +++++++++++++ .../agentstudio/resource/SessionThreads.java | 188 ++++++++++++++++++ .../agentstudio/resource/Sessions.java | 12 ++ .../dashscope/TestAgentStudioProtocol.java | 143 +++++++++++++ src/test/resources/agentstudio.json | 3 +- 19 files changed, 1153 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SecurityListAgentLogsParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceAddParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionThreadListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Security.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionResources.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionThreads.java create mode 100644 src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java index f3ad1ad2..89e72ab5 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java @@ -6,6 +6,7 @@ import com.alibaba.dashscope.agentstudio.resource.Deployments; import com.alibaba.dashscope.agentstudio.resource.Environments; import com.alibaba.dashscope.agentstudio.resource.Files; +import com.alibaba.dashscope.agentstudio.resource.Security; import com.alibaba.dashscope.agentstudio.resource.Sessions; import com.alibaba.dashscope.agentstudio.resource.Skills; import com.alibaba.dashscope.agentstudio.resource.Vaults; @@ -25,6 +26,7 @@ public class AgentStudioClient implements Closeable { private final Vaults vaults; private final Files files; private final WebhookEndpoints webhookEndpoints; + private final Security security; private final String baseUrl; public AgentStudioClient() { @@ -55,6 +57,7 @@ public AgentStudioClient( this.skills = new Skills(this.baseUrl, connectionOptions, apiKey, this.files); this.vaults = new Vaults(this.baseUrl, connectionOptions, apiKey); this.webhookEndpoints = new WebhookEndpoints(this.baseUrl, connectionOptions, apiKey); + this.security = new Security(this.baseUrl, connectionOptions, apiKey); } public static Builder builder() { @@ -134,6 +137,10 @@ public WebhookEndpoints webhookEndpoints() { return webhookEndpoints; } + public Security security() { + return security; + } + public String getBaseUrl() { return baseUrl; } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java index ba574be1..5049633a 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java @@ -25,7 +25,7 @@ private AgentStudioConstants() {} public static final class SSEEventType { public static final String MESSAGE = "message"; public static final String INTERRUPT = "interrupt"; - public static final String TOOL_CONFIRMATION = "tool_confirmation"; + public static final String TOOL_APPROVAL_RESPONSE = "tool_approval_response"; public static final String FUNCTION_CALL_OUTPUT = "function_call_output"; public static final String TOOL_CALL_OUTPUT = "tool_call_output"; public static final String DEFINE_OUTCOME = "define_outcome"; @@ -34,6 +34,7 @@ public static final class SSEEventType { public static final String REASONING = "reasoning"; public static final String MCP_CALL = "mcp_call"; public static final String MCP_CALL_OUTPUT = "mcp_call_output"; + public static final String TOOL_APPROVAL_REQUEST = "tool_approval_request"; public static final String SESSION_STATUS = "session_status"; public static final String ERROR = "error"; public static final String SESSION_UPDATED = "session_updated"; @@ -72,12 +73,30 @@ private BlockType() {} public static final class SessionStatusValue { public static final String IDLE = "idle"; public static final String RUNNING = "running"; - public static final String RESCHEDULING = "rescheduling"; + public static final String RESCHEDULED = "rescheduled"; public static final String TERMINATED = "terminated"; + public static final String DELETED = "deleted"; private SessionStatusValue() {} } + /** {@code stop_reason.type} values carried by {@code session_status} idle events. */ + public static final class StopReasonType { + public static final String END_TURN = "end_turn"; + public static final String REQUIRES_ACTION = "requires_action"; + public static final String RETRIES_EXHAUSTED = "retries_exhausted"; + + private StopReasonType() {} + } + + /** Tool approval policy ({@code permission_policy.type}). */ + public static final class PermissionPolicyType { + public static final String ALWAYS_ALLOW = "always_allow"; + public static final String ALWAYS_ASK = "always_ask"; + + private PermissionPolicyType() {} + } + public static String resolveBaseUrl(String workspace, String region) { String ws = workspace; if (ws == null || ws.isEmpty()) { diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java b/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java index 249edbbd..a17fcc40 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java @@ -74,22 +74,22 @@ public static JsonObject userInterrupt(String sessionThreadId) { return event; } - public static JsonObject userToolConfirmation(String toolUseId, String result) { - return userToolConfirmation(toolUseId, result, null, null); - } - - public static JsonObject userToolConfirmation( - String toolUseId, String result, String denyMessage) { - return userToolConfirmation(toolUseId, result, denyMessage, null); - } - - public static JsonObject userToolConfirmation( - String toolUseId, String result, String denyMessage, String sessionThreadId) { + /** + * Submit a tool approval ruling for an {@code always_ask} tool call. + * + *

{@code result} must be {@code "allow"} or {@code "deny"}; {@code denyMessage} is optional + * and only meaningful when denying. The approval identity is the {@code (batchId, callId)} + * composite key — {@code callId} may be reused across turns, so never match on it alone. Approval + * responses target the primary thread only (no {@code sessionThreadId}). + */ + public static JsonObject userToolApprovalResponse( + String batchId, String callId, String result, String denyMessage) { if (!"allow".equals(result) && !"deny".equals(result)) { - throw new IllegalArgumentException("tool_confirmation result must be 'allow' or 'deny'"); + throw new IllegalArgumentException("result must be 'allow' or 'deny'"); } JsonObject data = new JsonObject(); - data.addProperty("call_id", toolUseId); + data.addProperty("batch_id", batchId); + data.addProperty("call_id", callId); data.addProperty("result", result); if (denyMessage != null && "deny".equals(result)) { data.addProperty("deny_message", denyMessage); @@ -100,12 +100,9 @@ public static JsonObject userToolConfirmation( JsonArray content = new JsonArray(); content.add(dataBlock); JsonObject event = new JsonObject(); - event.addProperty("type", "tool_confirmation"); + event.addProperty("type", "tool_approval_response"); event.addProperty("role", "user"); event.add("content", content); - if (sessionThreadId != null) { - event.addProperty("session_thread_id", sessionThreadId); - } return event; } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java b/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java index a23b98b8..02644583 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java @@ -5,6 +5,7 @@ import com.alibaba.dashscope.utils.JsonUtils; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; +import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.Data; @@ -54,6 +55,19 @@ public class Message { @SerializedName("message") private String message; + // ── delta-protocol frame fields (opt-in via event_deltas) ── + @SerializedName("error") + private JsonObject errorPayload; + + @SerializedName("event") + private JsonObject eventPayload; + + @SerializedName("event_id") + private String deltaEventId; + + @SerializedName("delta") + private JsonObject deltaPayload; + /** * Extract stop_reason from a session_status event's data block. * @@ -61,7 +75,8 @@ public class Message { * {"stop_reason":{"type":"end_turn"},"session_status":"idle"}}. This helper parses it out so * callers don't need to manually dig through DataContent blocks. * - * @return stop_reason (has type, eventIds), or null if not a session_status event or absent + * @return stop_reason (has type, pendingBatchId, pendingCallIds), or null if not a session_status + * event or absent */ public Session.StopReason getStopReason() { if (!"session_status".equals(type) || content == null) return null; @@ -75,4 +90,108 @@ public Session.StopReason getStopReason() { } return null; } + + // typed data accessors + + /** The first content block's {@code data} payload, for events that carry one. */ + public JsonObject getData() { + if (content == null) return null; + for (ContentBlock block : content) { + if (block instanceof ContentBlock.DataContent) { + return ((ContentBlock.DataContent) block).getData(); + } + } + return null; + } + + /** {@code tool_approval_request} payload: batch_id/call_id/name/arguments/tool_type. */ + public JsonObject getToolApprovalRequest() { + if (!"tool_approval_request".equals(type)) return null; + return getData(); + } + + /** {@code {code, message}} from {@code type=error} events. */ + public JsonObject getError() { + if (!"error".equals(type)) return null; + return errorPayload; + } + + /** Suspend signal {@code {batch_id, call_ids}} from metadata while the approval barrier is up. */ + @SuppressWarnings("unchecked") + public Map getPendingToolApprovals() { + if (metadata == null) return null; + Object pending = metadata.get("pending_tool_approvals"); + return pending instanceof Map ? (Map) pending : null; + } + + /** {@code model_request_end} usage payload. */ + public JsonObject getModelRequestEnd() { + if (!"model_request_end".equals(type)) return null; + return getData(); + } + + /** {@code outcome_evaluation} payload (outcome_id/iteration/phase/result). */ + public JsonObject getOutcomeEvaluation() { + if (!"outcome_evaluation".equals(type)) return null; + return getData(); + } + + /** {@code thread_status} payload (session_thread_id/agent_name/thread_status/stop_reason). */ + public JsonObject getThreadStatus() { + if (!"thread_status".equals(type)) return null; + return getData(); + } + + /** {@code thread_created} payload (session_thread_id/agent_name). */ + public JsonObject getThreadCreated() { + if (!"thread_created".equals(type)) return null; + return getData(); + } + + /** {@code session_updated} payload (title/session_metadata/agent). */ + public JsonObject getSessionUpdated() { + if (!"session_updated".equals(type)) return null; + return getData(); + } + + /** Sub-agent routing from {@code thread_message_sent}/{@code received} metadata. */ + public Map getThreadMessageRouting() { + if (!"thread_message_sent".equals(type) && !"thread_message_received".equals(type)) { + return null; + } + if (metadata == null) return null; + Map routing = new HashMap<>(); + for (String key : + new String[] { + "to_session_thread_id", "to_agent_name", + "from_session_thread_id", "from_agent_name" + }) { + if (metadata.containsKey(key)) routing.put(key, metadata.get(key)); + } + return routing.isEmpty() ? null : routing; + } + + // ── delta-protocol frames (opt-in via event_deltas) ── + + /** {@code {id, type}} from {@code event_start} frames (preview, no content). */ + public JsonObject getEventStart() { + if (!"event_start".equals(type)) return null; + return eventPayload; + } + + /** Delta payload from {@code event_delta} frames. */ + public JsonObject getEventDelta() { + if (!"event_delta".equals(type)) return null; + return deltaPayload; + } + + /** Incremental text from an {@code event_delta}'s {@code content_delta}. */ + public String getDeltaText() { + if (!"event_delta".equals(type) || deltaPayload == null) return null; + if (deltaPayload.has("content") && deltaPayload.get("content").isJsonObject()) { + JsonObject c = deltaPayload.getAsJsonObject("content"); + if (c.has("text")) return c.get("text").getAsString(); + } + return null; + } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java index 4b25ad0d..9e038672 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java @@ -97,6 +97,18 @@ public static class RosterEntry { @SerializedName("version") private Integer version; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; } } + + @Data + public static class PermissionPolicy { + @SerializedName("type") + private String type; + } } 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 b645cff9..1d0223bd 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java @@ -101,8 +101,64 @@ public static class StopReason { @SerializedName("type") private String type; - @SerializedName("event_ids") - private List eventIds; + @SerializedName("pending_batch_id") + private String pendingBatchId; + + @SerializedName("pending_call_ids") + private List pendingCallIds; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SessionResource extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("file_id") + private String fileId; + + @SerializedName("mount_path") + private String mountPath; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SessionThread extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("session_id") + private String sessionId; + + @SerializedName("parent_thread_id") + private String parentThreadId; + + @SerializedName("agent") + private Map agent; + + @SerializedName("status") + private String status; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + @SerializedName("archived_at") + private String archivedAt; } @Data diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SecurityListAgentLogsParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SecurityListAgentLogsParam.java new file mode 100644 index 00000000..194590c3 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SecurityListAgentLogsParam.java @@ -0,0 +1,55 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import java.util.List; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SecurityListAgentLogsParam extends FlattenHalfDuplexParamBase { + @Default private Integer currentPage = null; + @Default private Integer pageSize = null; + @Default private String riskLevel = null; + @Default private String status = null; + @Default private String riskName = null; + @Default private String appName = null; + @Default private String assetType = null; + @Default private String vendor = null; + @Default private String orderBy = null; + @Default private String order = null; + @Default private String lang = null; + @Default private List statusList = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "current_page", currentPage); + AgentStudioConstants.appendParam(sb, "page_size", pageSize); + AgentStudioConstants.appendParam(sb, "risk_level", riskLevel); + AgentStudioConstants.appendParam(sb, "status", status); + AgentStudioConstants.appendParam(sb, "risk_name", riskName); + AgentStudioConstants.appendParam(sb, "app_name", appName); + AgentStudioConstants.appendParam(sb, "asset_type", assetType); + AgentStudioConstants.appendParam(sb, "vendor", vendor); + AgentStudioConstants.appendParam(sb, "order_by", orderBy); + AgentStudioConstants.appendParam(sb, "order", order); + AgentStudioConstants.appendParam(sb, "lang", lang); + if (statusList != null) { + for (String s : statusList) { + AgentStudioConstants.appendParam(sb, "status_list", s); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java index 218e3285..77b2d933 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java @@ -31,7 +31,9 @@ public JsonObject getHttpBody() { public String toQueryString() { StringBuilder sb = new StringBuilder(); if (types != null && !types.isEmpty()) { - AgentStudioConstants.appendParam(sb, "types", String.join(",", types)); + for (String t : types) { + AgentStudioConstants.appendParam(sb, "types[]", t); + } } AgentStudioConstants.appendParam(sb, "created_at[gt]", createdAtGt); AgentStudioConstants.appendParam(sb, "created_at[gte]", createdAtGte); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceAddParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceAddParam.java new file mode 100644 index 00000000..838eb1c8 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceAddParam.java @@ -0,0 +1,31 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionResourceAddParam extends FlattenHalfDuplexParamBase { + @NonNull private String type; + @NonNull private String fileId; + @Default private String mountPath = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + body.addProperty("type", type); + body.addProperty("file_id", fileId); + if (mountPath != null) { + body.addProperty("mount_path", mountPath); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceListParam.java new file mode 100644 index 00000000..c29099af --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionResourceListParam.java @@ -0,0 +1,30 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionResourceListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionThreadListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionThreadListParam.java new file mode 100644 index 00000000..7ae8fc87 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionThreadListParam.java @@ -0,0 +1,30 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionThreadListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java index 8b75d6f7..a32ce75e 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java @@ -7,6 +7,7 @@ import com.alibaba.dashscope.exception.ApiException; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; import java.io.Closeable; import java.io.IOException; import java.util.Iterator; @@ -88,6 +89,8 @@ private static ApiException wrapFailure(Throwable t, Response response) { } int code = response.code(); String body = ""; + String errorCode = null; + String errorMsg = null; try (ResponseBody rb = response.body()) { if (rb != null) { body = rb.string(); @@ -95,11 +98,28 @@ private static ApiException wrapFailure(Throwable t, Response response) { } catch (IOException e) { log.debug("Failed to read SSE failure response body", e); } + // Parse the agentstudio error envelope {type:error, error:{code,message}} + // to surface the server's actual error code instead of a generic one. + if (body != null && !body.isEmpty()) { + try { + JsonObject parsed = GSON.fromJson(body, JsonObject.class); + if (parsed != null && parsed.has("error") && parsed.get("error").isJsonObject()) { + JsonObject errObj = parsed.getAsJsonObject("error"); + if (errObj.has("code")) errorCode = errObj.get("code").getAsString(); + if (errObj.has("message")) errorMsg = errObj.get("message").getAsString(); + } + } catch (Exception e) { + log.debug("Failed to parse SSE error body as JSON", e); + } + } Status status = Status.builder() .statusCode(code) - .code(code >= 500 ? "server_error" : code == 401 ? "auth_error" : "http_error") - .message(body.isEmpty() ? "HTTP " + code : body) + .code( + errorCode != null + ? errorCode + : code >= 500 ? "server_error" : code == 401 ? "auth_error" : "http_error") + .message(errorMsg != null ? errorMsg : (body.isEmpty() ? "HTTP " + code : body)) .build(); return new ApiException(status, t); } @@ -152,6 +172,10 @@ public TextStream textStream() { return new TextStream(this); } + public TextDeltaStream textDeltas() { + return new TextDeltaStream(this); + } + @Override public void close() { if (closed.compareAndSet(false, true)) { @@ -188,7 +212,8 @@ public boolean hasNext() { String status = extractSessionStatus(msg); if ("idle".equals(status) || "terminated".equals(status) - || "rescheduling".equals(status)) { + || "rescheduled".equals(status) + || "deleted".equals(status)) { done = true; return false; } @@ -242,4 +267,69 @@ public void close() { source.close(); } } + + /** + * Incremental text chunks from {@code event_delta} frames. Requires the stream to be opened with + * {@code event_deltas}; otherwise yields nothing (use {@link TextStream} for terminal full text). + * Stops on terminal {@code session_status}. + */ + public static class TextDeltaStream implements Iterable, Closeable { + private final AgentStudioEventStream source; + + TextDeltaStream(AgentStudioEventStream source) { + this.source = source; + } + + @Override + public Iterator iterator() { + final Iterator msgIter = source.iterator(); + return new Iterator() { + private String next; + private boolean done; + + @Override + public boolean hasNext() { + if (next != null) return true; + if (done) return false; + while (msgIter.hasNext()) { + Message msg = msgIter.next(); + String type = msg.getType(); + if ("session_status".equals(type)) { + String status = TextStream.extractSessionStatus(msg); + if (status != null + && ("idle".equals(status) + || "terminated".equals(status) + || "rescheduled".equals(status) + || "deleted".equals(status))) { + done = true; + return false; + } + } + if ("event_delta".equals(type)) { + String text = msg.getDeltaText(); + if (text != null && !text.isEmpty()) { + next = text; + return true; + } + } + } + done = true; + return false; + } + + @Override + public String next() { + if (!hasNext()) throw new NoSuchElementException(); + String result = next; + next = null; + return result; + } + }; + } + + @Override + public void close() { + source.close(); + } + } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Security.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Security.java new file mode 100644 index 00000000..06503ade --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Security.java @@ -0,0 +1,172 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.param.SecurityListAgentLogsParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.google.gson.annotations.SerializedName; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.Data; +import lombok.EqualsAndHashCode; + +public final class Security { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + + public Security(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + } + + // ── sync ── + + public SecurityOverview overview() { + return AsyncHelper.joinAndUnwrap(overviewAsync()); + } + + public SecurityAlertList listAgentLogs(SecurityListAgentLogsParam param) { + return AsyncHelper.joinAndUnwrap(listAgentLogsAsync(param)); + } + + // ── async ── + + public CompletableFuture overviewAsync() { + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.GET, "security/overview", baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, SecurityOverview.class)); + } + + public CompletableFuture listAgentLogsAsync(SecurityListAgentLogsParam param) { + if (param == null) { + param = SecurityListAgentLogsParam.builder().build(); + } + String query = param.toQueryString(); + String path = query.isEmpty() ? "security/agent_logs" : "security/agent_logs?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, SecurityAlertList.class)); + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SecurityOverview extends FlattenResultBase { + @SerializedName("capabilities") + private List capabilities; + + @SerializedName("protection") + private List protection; + + @SerializedName("content_safety") + private SecurityOverview.SecurityScanStat contentSafety; + + @SerializedName("file_scan") + private SecurityOverview.SecurityScanStat fileScan; + + @SerializedName("skill_scan") + private SecurityOverview.SecurityScanStat skillScan; + + @Data + public static class SecurityCapability { + @SerializedName("key") + private String key; + + @SerializedName("enabled") + private Boolean enabled; + } + + @Data + public static class SecurityScanStat { + @SerializedName("hit") + private Long hit; + + @SerializedName("scanned") + private Long scanned; + } + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SecurityAlertList extends FlattenResultBase { + @SerializedName("stats") + private SecurityAlertList.SecurityAlertStats stats; + + @SerializedName("data") + private List data; + + @SerializedName("next_page") + private String nextPage; + + @Data + public static class SecurityAlertStats { + @SerializedName("total") + private Long total; + + @SerializedName("high") + private Long high; + + @SerializedName("medium") + private Long medium; + + @SerializedName("low") + private Long low; + } + + @Data + public static class SecurityAlert { + @SerializedName("alert_id") + private String alertId; + + @SerializedName("risk_level") + private String riskLevel; + + @SerializedName("risk_name") + private String riskName; + + @SerializedName("risk_desc") + private String riskDesc; + + @SerializedName("asset_type") + private String assetType; + + @SerializedName("asset_name") + private String assetName; + + @SerializedName("app_id") + private String appId; + + @SerializedName("app_name") + private String appName; + + @SerializedName("agent_name") + private String agentName; + + @SerializedName("status") + private String status; + + @SerializedName("source") + private String source; + + @SerializedName("check_time") + private String checkTime; + + @SerializedName("handle_time") + private String handleTime; + + @SerializedName("vendor") + private String vendor; + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java index cb3efeff..670a3b5d 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java @@ -131,15 +131,39 @@ public CompletableFuture> listAsync( } public AgentStudioEventStream stream(String sessionId) { - return stream(sessionId, AgentStudioConstants.DEFAULT_TIMEOUT_MS); + return stream(sessionId, null, AgentStudioConstants.DEFAULT_TIMEOUT_MS); } public AgentStudioEventStream stream(String sessionId, long timeoutMs) { + return stream(sessionId, null, timeoutMs); + } + + /** + * Open the SSE stream, optionally opting into incremental text deltas. + * + *

{@code eventDeltas} accepts {@code "message"} and/or {@code "reasoning"} (aliases {@code + * "agent.message"}/{@code "agent.thinking"}). When set, the stream emits {@code + * event_start}/{@code event_delta} frames carrying partial text — consume via {@link + * AgentStudioEventStream#textDeltas()}. + */ + public AgentStudioEventStream stream(String sessionId, List eventDeltas) { + return stream(sessionId, eventDeltas, AgentStudioConstants.DEFAULT_TIMEOUT_MS); + } + + public AgentStudioEventStream stream(String sessionId, List eventDeltas, long timeoutMs) { String url = resolveStreamBaseUrl(); if (!url.endsWith("/")) { url += "/"; } url += StringUtils.format("sessions/%s/events/stream", sessionId); + if (eventDeltas != null && !eventDeltas.isEmpty()) { + StringBuilder qs = new StringBuilder("?"); + for (int i = 0; i < eventDeltas.size(); i++) { + if (i > 0) qs.append("&"); + qs.append("event_deltas[]=").append(eventDeltas.get(i)); + } + url += qs; + } String resolvedKey = resolveApiKey(); OkHttpClient client = diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionResources.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionResources.java new file mode 100644 index 00000000..f0cbfd57 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionResources.java @@ -0,0 +1,138 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.SessionResourceAddParam; +import com.alibaba.dashscope.agentstudio.param.SessionResourceListParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +public final class SessionResources { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + + public SessionResources(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + } + + public Session.SessionResource add(String sessionId, SessionResourceAddParam param) { + return AsyncHelper.joinAndUnwrap(addAsync(sessionId, param)); + } + + public Session.SessionResource retrieve(String sessionId, String resourceId) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(sessionId, resourceId)); + } + + public CursorPage list( + String sessionId, SessionResourceListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(sessionId, param)); + } + + public AgentStudioDeletionStatus delete(String sessionId, String resourceId) { + return AsyncHelper.joinAndUnwrap(deleteAsync(sessionId, resourceId)); + } + + // ── async ── + + public CompletableFuture addAsync( + String sessionId, SessionResourceAddParam param) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("sessions/%s/resources", sessionId), baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.SessionResource.class)); + } + + public CompletableFuture retrieveAsync( + String sessionId, String resourceId) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (resourceId == null || resourceId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("resourceId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, + StringUtils.format("sessions/%s/resources/%s", sessionId, resourceId), + baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.SessionResource.class)); + } + + public CompletableFuture> listAsync( + String sessionId, SessionResourceListParam param) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (param == null) { + param = SessionResourceListParam.builder().build(); + } + final SessionResourceListParam finalParam = param; + String query = param.toQueryString(); + String path = + query.isEmpty() + ? StringUtils.format("sessions/%s/resources", sessionId) + : StringUtils.format("sessions/%s/resources?%s", sessionId, query); + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = + FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + sessionId, + SessionResourceListParam.builder() + .limit(finalParam.getLimit()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture deleteAsync( + String sessionId, String resourceId) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (resourceId == null || resourceId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("resourceId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, + StringUtils.format("sessions/%s/resources/%s", sessionId, resourceId), + baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionThreads.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionThreads.java new file mode 100644 index 00000000..8a1feee7 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionThreads.java @@ -0,0 +1,188 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; +import com.alibaba.dashscope.agentstudio.param.SessionThreadListParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +public final class SessionThreads { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + private final SessionThreadEvents events; + + public SessionThreads(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + this.events = new SessionThreadEvents(baseUrl, connectionOptions, apiKey); + } + + public SessionThreadEvents events() { + return events; + } + + // ── sync ── + + public CursorPage list(String sessionId, SessionThreadListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(sessionId, param)); + } + + public Session.SessionThread retrieve(String sessionId, String threadId) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(sessionId, threadId)); + } + + public Session.SessionThread archive(String sessionId, String threadId) { + return AsyncHelper.joinAndUnwrap(archiveAsync(sessionId, threadId)); + } + + // ── async ── + + public CompletableFuture> listAsync( + String sessionId, SessionThreadListParam param) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (param == null) { + param = SessionThreadListParam.builder().build(); + } + final SessionThreadListParam finalParam = param; + String query = param.toQueryString(); + String path = + query.isEmpty() + ? StringUtils.format("sessions/%s/threads", sessionId) + : StringUtils.format("sessions/%s/threads?%s", sessionId, query); + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = + FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + sessionId, + SessionThreadListParam.builder() + .limit(finalParam.getLimit()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture retrieveAsync(String sessionId, String threadId) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (threadId == null || threadId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("threadId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, + StringUtils.format("sessions/%s/threads/%s", sessionId, threadId), + baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.SessionThread.class)); + } + + public CompletableFuture archiveAsync(String sessionId, String threadId) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (threadId == null || threadId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("threadId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, + StringUtils.format("sessions/%s/threads/%s/archive", sessionId, threadId), + baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.SessionThread.class)); + } + + // ── nested: thread-scoped events ── + + public final class SessionThreadEvents { + private final GeneralApi threadApi; + private final String threadBaseUrl; + private final String threadApiKey; + + SessionThreadEvents(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.threadBaseUrl = baseUrl; + this.threadApiKey = apiKey; + this.threadApi = + connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + } + + public CursorPage list( + String sessionId, String threadId, SessionEventListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(sessionId, threadId, param)); + } + + public CompletableFuture> listAsync( + String sessionId, String threadId, SessionEventListParam param) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (threadId == null || threadId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("threadId is required!")); + } + if (param == null) { + param = SessionEventListParam.builder().build(); + } + final SessionEventListParam finalParam = param; + String query = param.toQueryString(); + String basePath = StringUtils.format("sessions/%s/threads/%s/events", sessionId, threadId); + String path = query.isEmpty() ? basePath : basePath + "?" + query; + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.GET, path, threadBaseUrl); + return AsyncHelper.callAsync( + threadApi, + GeneralGetParam.builder().apiKey(threadApiKey).headers(new HashMap<>()).build(), + opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + sessionId, + threadId, + SessionEventListParam.builder() + .types(finalParam.getTypes()) + .createdAtGt(finalParam.getCreatedAtGt()) + .createdAtGte(finalParam.getCreatedAtGte()) + .createdAtLt(finalParam.getCreatedAtLt()) + .createdAtLte(finalParam.getCreatedAtLte()) + .limit(finalParam.getLimit()) + .order(finalParam.getOrder()) + .page(cursor) + .build())); + return page; + }); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java index 9434459f..88baaa7b 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java @@ -28,18 +28,30 @@ public final class Sessions { private final String baseUrl; private final String apiKey; private final SessionEvents events; + private final SessionResources resources; + private final SessionThreads threads; public Sessions(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); this.events = new SessionEvents(baseUrl, connectionOptions, apiKey); + this.resources = new SessionResources(baseUrl, connectionOptions, apiKey); + this.threads = new SessionThreads(baseUrl, connectionOptions, apiKey); } public SessionEvents events() { return events; } + public SessionResources resources() { + return resources; + } + + public SessionThreads threads() { + return threads; + } + public Session create(SessionCreateParam param) { return AsyncHelper.joinAndUnwrap(createAsync(param)); } diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java new file mode 100644 index 00000000..6121bb80 --- /dev/null +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java @@ -0,0 +1,143 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.alibaba.dashscope.agentstudio.message.ClientEvents; +import com.alibaba.dashscope.agentstudio.message.ContentBlock; +import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +/** + * Focused unit tests for the AgentStudio protocol surface (tool approval, stop_reason, event + * deltas, types[] query, message accessors). No network — pure model/helper assertions. + */ +class TestAgentStudioProtocol { + + private static final Gson GSON = + new GsonBuilder() + .registerTypeAdapter(ContentBlock.class, new ContentBlock.Deserializer()) + .create(); + + @Test + void testEventListTypesRepeatedQuery() { + // types must be repeated types[]= keys, not a single comma-joined value. + String query = + SessionEventListParam.builder() + .types(Arrays.asList("message", "tool_call", "tool_approval_request")) + .limit(50) + .build() + .toQueryString(); + assertTrue(query.contains("types[]=message"), query); + assertTrue(query.contains("types[]=tool_call"), query); + assertTrue(query.contains("types[]=tool_approval_request"), query); + assertFalse(query.contains(","), "types should not be comma-joined: " + query); + } + + @Test + void testUserToolApprovalResponseWire() { + JsonObject allow = ClientEvents.userToolApprovalResponse("b1", "c1", "allow", null); + assertEquals("tool_approval_response", allow.get("type").getAsString()); + assertEquals("user", allow.get("role").getAsString()); + JsonObject data = + allow.getAsJsonArray("content").get(0).getAsJsonObject().getAsJsonObject("data"); + assertEquals("b1", data.get("batch_id").getAsString()); + assertEquals("c1", data.get("call_id").getAsString()); + assertEquals("allow", data.get("result").getAsString()); + assertFalse(data.has("deny_message")); + + JsonObject deny = ClientEvents.userToolApprovalResponse("b1", "c2", "deny", "nope"); + JsonObject denyData = + deny.getAsJsonArray("content").get(0).getAsJsonObject().getAsJsonObject("data"); + assertEquals("deny", denyData.get("result").getAsString()); + assertEquals("nope", denyData.get("deny_message").getAsString()); + } + + @Test + void testUserToolApprovalResponseInvalidResult() { + assertThrows( + IllegalArgumentException.class, + () -> ClientEvents.userToolApprovalResponse("b1", "c1", "maybe", null)); + } + + @Test + void testStopReasonPendingFields() { + Session.StopReason sr = new Session.StopReason(); + sr.setType("requires_action"); + sr.setPendingBatchId("response_xxx:9f2c"); + sr.setPendingCallIds(Arrays.asList("call_1", "call_2")); + assertEquals("requires_action", sr.getType()); + assertEquals("response_xxx:9f2c", sr.getPendingBatchId()); + assertEquals(Arrays.asList("call_1", "call_2"), sr.getPendingCallIds()); + } + + @Test + void testMessageDeltaText() { + String json = + "{\"type\":\"event_delta\",\"event_id\":\"sevt_a\"," + + "\"delta\":{\"type\":\"content_delta\",\"index\":0," + + "\"content\":{\"type\":\"text\",\"text\":\"hello\"}}}"; + Message msg = GSON.fromJson(json, Message.class); + assertEquals("event_delta", msg.getType()); + assertEquals("sevt_a", msg.getDeltaEventId()); + assertEquals("hello", msg.getDeltaText()); + } + + @Test + void testMessageEventStart() { + String json = "{\"type\":\"event_start\",\"event\":{\"id\":\"sevt_a\",\"type\":\"message\"}}"; + Message msg = GSON.fromJson(json, Message.class); + assertEquals("event_start", msg.getType()); + JsonObject event = msg.getEventStart(); + assertEquals("sevt_a", event.get("id").getAsString()); + assertEquals("message", event.get("type").getAsString()); + } + + @Test + void testMessageToolApprovalRequestAccessor() { + String json = + "{\"object\":\"message\",\"status\":\"completed\",\"id\":\"m1\"," + + "\"role\":\"assistant\",\"type\":\"tool_approval_request\"," + + "\"content\":[{\"type\":\"data\",\"data\":{\"batch_id\":\"b1\"," + + "\"call_id\":\"c1\",\"name\":\"bash\",\"tool_type\":\"builtin\"}}]}"; + Message msg = GSON.fromJson(json, Message.class); + JsonObject ap = msg.getToolApprovalRequest(); + assertEquals("b1", ap.get("batch_id").getAsString()); + assertEquals("c1", ap.get("call_id").getAsString()); + assertEquals("bash", ap.get("name").getAsString()); + } + + @Test + void testMessageErrorAccessor() { + String json = + "{\"type\":\"error\",\"error\":{\"code\":\"tool_approval_service_unavailable\"," + + "\"message\":\"down\"},\"metadata\":{\"pending_tool_approvals\":" + + "{\"batch_id\":\"b1\",\"call_ids\":[\"c1\"]}}}"; + Message msg = GSON.fromJson(json, Message.class); + assertEquals("error", msg.getType()); + assertEquals("tool_approval_service_unavailable", msg.getError().get("code").getAsString()); + assertEquals("down", msg.getError().get("message").getAsString()); + assertEquals("b1", msg.getPendingToolApprovals().get("batch_id")); + } + + @Test + void testMessageAccessorsReturnNullForNonMatchingType() { + Message msg = new Message(); + msg.setType("message"); + assertNull(msg.getToolApprovalRequest()); + assertNull(msg.getError()); + assertNull(msg.getPendingToolApprovals()); + assertNull(msg.getEventStart()); + assertNull(msg.getDeltaText()); + } +} diff --git a/src/test/resources/agentstudio.json b/src/test/resources/agentstudio.json index 271c9fcd..35338d98 100644 --- a/src/test/resources/agentstudio.json +++ b/src/test/resources/agentstudio.json @@ -72,8 +72,7 @@ "environment_id": "env_001", "status": "idle", "stop_reason": { - "type": "end_turn", - "event_ids": ["evt_1", "evt_2"] + "type": "end_turn" }, "usage": { "input_tokens": 100, From ec15af97c9ce13e3579113180206b91795f29e6d Mon Sep 17 00:00:00 2001 From: foleydang Date: Thu, 10 Sep 2026 14:16:44 +0800 Subject: [PATCH 2/3] fix: wire PermissionPolicy into DefaultConfig/PerToolConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PermissionPolicy was defined but never referenced from ToolConfig's inner classes — Gson serialized tools without permission_policy, making always_ask unreachable for Java users. Co-Authored-By: Claude Code --- .../com/alibaba/dashscope/agentstudio/model/Configs.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java index 9e038672..b9218f33 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java @@ -35,6 +35,9 @@ public static class ToolConfig { public static class DefaultConfig { @SerializedName("enabled") private Boolean enabled; + + @SerializedName("permission_policy") + private PermissionPolicy permissionPolicy; } @Data @@ -44,6 +47,9 @@ public static class PerToolConfig { @SerializedName("enabled") private Boolean enabled; + + @SerializedName("permission_policy") + private PermissionPolicy permissionPolicy; } } From ab9e0b3cda23e56b3663b0848f43fc944c150020 Mon Sep 17 00:00:00 2001 From: foleydang Date: Thu, 10 Sep 2026 14:38:17 +0800 Subject: [PATCH 3/3] test: verify permission_policy serialized in ToolConfig Co-Authored-By: Claude Code --- .../dashscope/TestAgentStudioProtocol.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java index 6121bb80..21c24181 100644 --- a/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java @@ -10,7 +10,9 @@ import com.alibaba.dashscope.agentstudio.message.ClientEvents; import com.alibaba.dashscope.agentstudio.message.ContentBlock; import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.agentstudio.model.Configs; import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.param.AgentCreateParam; import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -140,4 +142,31 @@ void testMessageAccessorsReturnNullForNonMatchingType() { assertNull(msg.getEventStart()); assertNull(msg.getDeltaText()); } + + @Test + void testPermissionPolicySerializedInToolConfig() { + Configs.ToolConfig.DefaultConfig dc = new Configs.ToolConfig.DefaultConfig(); + dc.setEnabled(false); + Configs.PermissionPolicy pp = new Configs.PermissionPolicy(); + pp.setType("always_ask"); + dc.setPermissionPolicy(pp); + + Configs.ToolConfig tool = new Configs.ToolConfig(); + tool.setType("builtin_toolkit"); + tool.setDefaultConfig(dc); + + JsonObject body = + AgentCreateParam.builder() + .name("test") + .model("qwen-max") + .tools(Arrays.asList(tool)) + .build() + .getHttpBody(); + + JsonObject dcJson = + body.getAsJsonArray("tools").get(0).getAsJsonObject().getAsJsonObject("default_config"); + assertTrue(dcJson.has("permission_policy")); + assertEquals( + "always_ask", dcJson.getAsJsonObject("permission_policy").get("type").getAsString()); + } }