Skip to content

Commit efd7f02

Browse files
foleydangclaude
andauthored
feat(agentstudio): multiagent roster, file download, thread_id, event types (#259)
- Configs.MultiAgentConfig with a nested RosterEntry, wired into AgentCreateParam / AgentUpdateParam / Agent. Placed in the Configs container to match the existing tool/skill/mcp config classes, which are all @DaTa with setters (no @builder). - Files.download() / downloadAsync() returning FileContent with writeToFile(); mirrors the raw OkHttp path used by upload. - SessionCreateParam.vaultIds. - Message.threadId for the top-level thread_id on thread_* events. - SSEEventType: the 7 missing types (thread_created, thread_status, thread_message_sent/received, thread_context_compacted, model_request_start/end), bringing Java to the same 22 as Python. Verified: google-java-format clean, mvn clean test 205 passed, and the AgentStudio e2e suite exercised the roster create/update against a live backend. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 88741a5 commit efd7f02

10 files changed

Lines changed: 266 additions & 0 deletions

File tree

src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public static final class SSEEventType {
3737
public static final String SESSION_STATUS = "session_status";
3838
public static final String ERROR = "error";
3939
public static final String SESSION_UPDATED = "session_updated";
40+
public static final String THREAD_CREATED = "thread_created";
41+
public static final String THREAD_STATUS = "thread_status";
42+
public static final String THREAD_MESSAGE_SENT = "thread_message_sent";
43+
public static final String THREAD_MESSAGE_RECEIVED = "thread_message_received";
44+
public static final String THREAD_CONTEXT_COMPACTED = "thread_context_compacted";
45+
public static final String MODEL_REQUEST_START = "model_request_start";
46+
public static final String MODEL_REQUEST_END = "model_request_end";
4047
public static final String OUTCOME_EVALUATION = "outcome_evaluation";
4148

4249
private SSEEventType() {}

src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public class Message {
4444
@SerializedName("session_thread_id")
4545
private String sessionThreadId;
4646

47+
/** Thread this event belongs to; server sends it on all {@code thread_*} events. */
48+
@SerializedName("thread_id")
49+
private String threadId;
50+
4751
@SerializedName("code")
4852
private String code;
4953

src/main/java/com/alibaba/dashscope/agentstudio/model/Agent.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig;
55
import com.alibaba.dashscope.agentstudio.model.Configs.ModelConfig;
6+
import com.alibaba.dashscope.agentstudio.model.Configs.MultiAgentConfig;
67
import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig;
78
import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig;
89
import com.alibaba.dashscope.common.FlattenResultBase;
@@ -42,6 +43,9 @@ public class Agent extends FlattenResultBase {
4243
@SerializedName("mcp_servers")
4344
private List<McpServerConfig> mcpServers;
4445

46+
@SerializedName("multiagent")
47+
private MultiAgentConfig multiagent;
48+
4549
@SerializedName("version")
4650
private Integer version;
4751

src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,33 @@ public static class McpServerConfig {
7070
@SerializedName("url")
7171
private String url;
7272
}
73+
74+
/**
75+
* The {@code multiagent} field on an agent: {@code type} is currently always {@code
76+
* "coordinator"}, {@code agents} is the roster of 1-20 entries (an empty list clears it).
77+
*/
78+
@Data
79+
public static class MultiAgentConfig {
80+
@SerializedName("type")
81+
private String type;
82+
83+
@SerializedName("agents")
84+
private List<RosterEntry> agents;
85+
86+
/**
87+
* One roster entry: {@code type} is {@code "agent"} (reference another agent by {@code id} plus
88+
* optional {@code version}) or {@code "self"} (a copy of the coordinator; at most one).
89+
*/
90+
@Data
91+
public static class RosterEntry {
92+
@SerializedName("type")
93+
private String type;
94+
95+
@SerializedName("id")
96+
private String id;
97+
98+
@SerializedName("version")
99+
private Integer version;
100+
}
101+
}
73102
}

src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package com.alibaba.dashscope.agentstudio.param;
33

44
import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig;
5+
import com.alibaba.dashscope.agentstudio.model.Configs.MultiAgentConfig;
56
import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig;
67
import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig;
78
import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase;
@@ -26,6 +27,7 @@ public class AgentCreateParam extends FlattenHalfDuplexParamBase {
2627
@Default private List<ToolConfig> tools = null;
2728
@Default private List<McpServerConfig> mcpServers = null;
2829
@Default private List<SkillConfig> skills = null;
30+
@Default private MultiAgentConfig multiagent = null;
2931
@Default private Map<String, String> metadata = null;
3032

3133
@Override
@@ -50,6 +52,9 @@ public JsonObject getHttpBody() {
5052
if (skills != null) {
5153
body.add("skills", JsonUtils.toJsonElement(skills));
5254
}
55+
if (multiagent != null) {
56+
body.add("multiagent", JsonUtils.toJsonElement(multiagent));
57+
}
5358
if (metadata != null && !metadata.isEmpty()) {
5459
body.add("metadata", JsonUtils.toJsonElement(metadata));
5560
}

src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package com.alibaba.dashscope.agentstudio.param;
33

44
import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig;
5+
import com.alibaba.dashscope.agentstudio.model.Configs.MultiAgentConfig;
56
import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig;
67
import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig;
78
import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase;
@@ -29,6 +30,7 @@ public class AgentUpdateParam extends FlattenHalfDuplexParamBase {
2930
@Default private List<ToolConfig> tools = null;
3031
@Default private List<McpServerConfig> mcpServers = null;
3132
@Default private List<SkillConfig> skills = null;
33+
@Default private MultiAgentConfig multiagent = null;
3234
@Default private Map<String, String> metadata = null;
3335

3436
@Override
@@ -60,6 +62,9 @@ public JsonObject getHttpBody() {
6062
if (skills != null) {
6163
body.add("skills", JsonUtils.toJsonElement(skills));
6264
}
65+
if (multiagent != null) {
66+
body.add("multiagent", JsonUtils.toJsonElement(multiagent));
67+
}
6368
if (metadata != null && !metadata.isEmpty()) {
6469
body.add("metadata", JsonUtils.toJsonElement(metadata));
6570
}

src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class SessionCreateParam extends FlattenHalfDuplexParamBase {
2020
@Default private String environmentId = null;
2121
@Default private String title = null;
2222
@Default private List<Map<String, Object>> resources = null;
23+
@Default private List<String> vaultIds = null;
2324
@Default private Map<String, String> metadata = null;
2425

2526
@Override
@@ -35,6 +36,9 @@ public JsonObject getHttpBody() {
3536
if (resources != null && !resources.isEmpty()) {
3637
body.add("resources", JsonUtils.toJsonElement(resources));
3738
}
39+
if (vaultIds != null && !vaultIds.isEmpty()) {
40+
body.add("vault_ids", JsonUtils.toJsonElement(vaultIds));
41+
}
3842
if (metadata != null && !metadata.isEmpty()) {
3943
body.add("metadata", JsonUtils.toJsonElement(metadata));
4044
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Alibaba, Inc. and its affiliates.
2+
package com.alibaba.dashscope.agentstudio.resource;
3+
4+
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.Paths;
8+
import java.util.Objects;
9+
10+
/**
11+
* Binary content of a downloaded file.
12+
*
13+
* <p>Mirrors the Python SDK's {@code FileContent}: a thin wrapper over the raw bytes with a {@link
14+
* #writeToFile(Path)} helper, so callers can do {@code
15+
* client.files().download(fileId).writeToFile("output.txt")}.
16+
*/
17+
public final class FileContent {
18+
private final byte[] data;
19+
20+
public FileContent(byte[] data) {
21+
this.data = data != null ? data : new byte[0];
22+
}
23+
24+
/** The raw file bytes. */
25+
public byte[] getBytes() {
26+
return data;
27+
}
28+
29+
/** Number of bytes. */
30+
public int length() {
31+
return data.length;
32+
}
33+
34+
/** Write the content to {@code path} (creating parent directories) and return it. */
35+
public Path writeToFile(String path) throws IOException {
36+
return writeToFile(Paths.get(path));
37+
}
38+
39+
/** Write the content to {@code path} (creating parent directories) and return it. */
40+
public Path writeToFile(Path path) throws IOException {
41+
Objects.requireNonNull(path, "path");
42+
if (path.getParent() != null) {
43+
Files.createDirectories(path.getParent());
44+
}
45+
Files.write(path, data);
46+
return path;
47+
}
48+
}

src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,61 @@ public AgentStudioDeletionStatus delete(
8585
return AsyncHelper.joinAndUnwrap(deleteAsync(fileId, apiKey, headers));
8686
}
8787

88+
/**
89+
* Download a file's binary content.
90+
*
91+
* <p>Only files whose {@code downloadable} flag is true can be fetched; the service answers 403
92+
* otherwise. Use {@link FileContent#writeToFile(String)} to persist the bytes to disk.
93+
*/
94+
public FileContent download(String fileId) {
95+
return AsyncHelper.joinAndUnwrap(downloadAsync(fileId));
96+
}
97+
98+
public CompletableFuture<FileContent> downloadAsync(String fileId) {
99+
if (fileId == null || fileId.isEmpty()) {
100+
return AsyncHelper.failedFuture(new InputRequiredException("fileId is required!"));
101+
}
102+
String key;
103+
try {
104+
key = ApiKey.getApiKey(this.apiKey);
105+
} catch (Exception e) {
106+
return AsyncHelper.failedFuture(e);
107+
}
108+
String resolvedBase = resolveUploadBaseUrl();
109+
String url = resolvedBase + StringUtils.format("/files/%s/content", fileId);
110+
Request request =
111+
new Request.Builder().url(url).header("Authorization", "Bearer " + key).get().build();
112+
113+
CompletableFuture<FileContent> future = new CompletableFuture<>();
114+
uploadClient
115+
.newCall(request)
116+
.enqueue(
117+
new Callback() {
118+
@Override
119+
public void onFailure(Call call, IOException e) {
120+
future.completeExceptionally(new ApiException(e));
121+
}
122+
123+
@Override
124+
public void onResponse(Call call, Response response) {
125+
try (Response r = response) {
126+
if (!r.isSuccessful()) {
127+
String body = r.body() != null ? r.body().string() : "";
128+
future.completeExceptionally(
129+
new ApiException(
130+
Status.builder().statusCode(r.code()).message(body).build()));
131+
return;
132+
}
133+
byte[] bytes = r.body() != null ? r.body().bytes() : new byte[0];
134+
future.complete(new FileContent(bytes));
135+
} catch (Exception e) {
136+
future.completeExceptionally(new ApiException(e));
137+
}
138+
}
139+
});
140+
return future;
141+
}
142+
88143
public CompletableFuture<AgentStudioFile> uploadAsync(String filePath, String mimeType) {
89144
if (filePath == null || filePath.isEmpty()) {
90145
return AsyncHelper.failedFuture(new InputRequiredException("filePath is required!"));

0 commit comments

Comments
 (0)