diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java
index f3ad1ad..89e72ab 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 ba574be..5049633 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 249edbb..a17fcc4 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 a23b98b..0264458 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 4b25ad0..b9218f3 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;
}
}
@@ -97,6 +103,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 b645cff..1d0223b 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 0000000..194590c
--- /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 218e328..77b2d93 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 0000000..838eb1c
--- /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 0000000..c29099a
--- /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 0000000..7ae8fc8
--- /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 8b75d6f..a32ce75 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 0000000..06503ad
--- /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 cb3efef..670a3b5 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 0000000..f0cbfd5
--- /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 0000000..8a1feee
--- /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 9434459..88baaa7 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 0000000..21c2418
--- /dev/null
+++ b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java
@@ -0,0 +1,172 @@
+// 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.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;
+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());
+ }
+
+ @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());
+ }
+}
diff --git a/src/test/resources/agentstudio.json b/src/test/resources/agentstudio.json
index 271c9fc..35338d9 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,