Skip to content

Commit aa7fd21

Browse files
foleydangclaude
andcommitted
feat: sync-first API, typed models, CursorPage auto-pagination, and client.async()
Refactor AgentStudio SDK to follow sync-first convention: default methods return T directly, *Async() variants return CompletableFuture<T>. Add typed model configs (Configs.ModelConfig, ToolConfig), CursorPage Iterable support for auto-pagination, client.async() helper, and comprehensive unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 895263e commit aa7fd21

37 files changed

Lines changed: 1977 additions & 1077 deletions

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

Lines changed: 10 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88
import com.alibaba.dashscope.agentstudio.resource.Skills;
99
import com.alibaba.dashscope.agentstudio.resource.Vaults;
1010
import com.alibaba.dashscope.protocol.ConnectionOptions;
11-
import com.alibaba.dashscope.utils.Constants;
1211
import java.io.Closeable;
1312
import java.util.concurrent.CompletableFuture;
14-
import java.util.concurrent.ExecutorService;
15-
import java.util.concurrent.ForkJoinPool;
1613
import java.util.function.Supplier;
1714

1815
public class AgentStudioClient implements Closeable {
@@ -23,61 +20,34 @@ public class AgentStudioClient implements Closeable {
2320
private final Vaults vaults;
2421
private final Files files;
2522
private final String baseUrl;
26-
private final ExecutorService asyncExecutor;
2723

28-
/** All from env vars: DASHSCOPE_API_KEY, DASHSCOPE_WORKSPACE, DASHSCOPE_API_REGION. */
2924
public AgentStudioClient() {
3025
this(null, null, null, null, null);
3126
}
3227

33-
/**
34-
* Workspace only, apiKey from DASHSCOPE_API_KEY env var.
35-
*
36-
* @param workspace workspace ID (required)
37-
*/
3828
public AgentStudioClient(String workspace) {
3929
this(null, null, workspace, null, null);
4030
}
4131

42-
/**
43-
* Most common: apiKey + workspace for production.
44-
*
45-
* @param apiKey API Key (or set DASHSCOPE_API_KEY env var)
46-
* @param workspace workspace ID (required)
47-
*/
4832
public AgentStudioClient(String apiKey, String workspace) {
4933
this(apiKey, null, workspace, null, null);
5034
}
5135

52-
/**
53-
* Full constructor — prefer {@link #builder()} for readability.
54-
*
55-
* @param apiKey API Key (nullable → read from DASHSCOPE_API_KEY)
56-
* @param baseUrl full base URL (nullable → use workspace + region)
57-
* @param workspace workspace ID (required if baseUrl is null)
58-
* @param region region (nullable → default cn-beijing)
59-
* @param connectionOptions timeout / retry config (nullable)
60-
*/
6136
public AgentStudioClient(
6237
String apiKey,
6338
String baseUrl,
6439
String workspace,
6540
String region,
6641
ConnectionOptions connectionOptions) {
67-
if (apiKey != null && !apiKey.isEmpty()) {
68-
Constants.apiKey = apiKey;
69-
}
7042
this.baseUrl = resolveBaseUrl(baseUrl, workspace, region);
71-
this.agents = new Agents(this.baseUrl, connectionOptions);
72-
this.sessions = new Sessions(this.baseUrl, connectionOptions);
73-
this.environments = new Environments(this.baseUrl, connectionOptions);
74-
this.skills = new Skills(this.baseUrl, connectionOptions);
75-
this.vaults = new Vaults(this.baseUrl, connectionOptions);
76-
this.files = new Files(this.baseUrl, connectionOptions);
77-
this.asyncExecutor = ForkJoinPool.commonPool();
43+
this.agents = new Agents(this.baseUrl, connectionOptions, apiKey);
44+
this.sessions = new Sessions(this.baseUrl, connectionOptions, apiKey);
45+
this.environments = new Environments(this.baseUrl, connectionOptions, apiKey);
46+
this.files = new Files(this.baseUrl, connectionOptions, apiKey);
47+
this.skills = new Skills(this.baseUrl, connectionOptions, apiKey, this.files);
48+
this.vaults = new Vaults(this.baseUrl, connectionOptions, apiKey);
7849
}
7950

80-
/** Builder for advanced use cases (pre-env baseUrl, custom region, etc.). */
8151
public static Builder builder() {
8252
return new Builder();
8353
}
@@ -147,26 +117,15 @@ public String getBaseUrl() {
147117
return baseUrl;
148118
}
149119

150-
ExecutorService getAsyncExecutor() {
151-
return asyncExecutor;
152-
}
153-
154-
/**
155-
* Run any sync SDK call asynchronously.
156-
*
157-
* <pre>{@code
158-
* client.async(() -> client.agents().create(param))
159-
* .thenAccept(agent -> System.out.println(agent.getId()));
160-
* }</pre>
161-
*/
162120
public <T> CompletableFuture<T> async(Supplier<T> supplier) {
163-
return CompletableFuture.supplyAsync(supplier, asyncExecutor);
121+
return CompletableFuture.supplyAsync(supplier);
164122
}
165123

166124
@Override
167125
public void close() {
168126
sessions.events().close();
169-
asyncExecutor.shutdownNow();
127+
skills.close();
128+
files.close();
170129
}
171130

172131
private static String resolveBaseUrl(String explicitUrl, String workspace, String region) {
@@ -180,22 +139,6 @@ private static String resolveBaseUrl(String explicitUrl, String workspace, Strin
180139
if (envUrl != null && !envUrl.isEmpty()) {
181140
return envUrl;
182141
}
183-
String ws = workspace;
184-
if (ws == null || ws.isEmpty()) {
185-
ws = System.getenv(AgentStudioConstants.ENV_WORKSPACE);
186-
}
187-
if (ws == null || ws.isEmpty()) {
188-
throw new IllegalArgumentException(
189-
"workspace is required. Pass it to the constructor, "
190-
+ "set DASHSCOPE_WORKSPACE env var, or set DASHSCOPE_AGENTSTUDIO_URL.");
191-
}
192-
String r = region;
193-
if (r == null || r.isEmpty()) {
194-
r = System.getenv(AgentStudioConstants.ENV_REGION);
195-
}
196-
if (r == null || r.isEmpty()) {
197-
r = AgentStudioConstants.DEFAULT_REGION;
198-
}
199-
return String.format(AgentStudioConstants.BASE_URL_TEMPLATE, ws, r);
142+
return AgentStudioConstants.resolveBaseUrl(workspace, region);
200143
}
201144
}

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

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
// Copyright (c) Alibaba, Inc. and its affiliates.
22
package com.alibaba.dashscope.agentstudio;
33

4+
import com.alibaba.dashscope.base.HalfDuplexParamBase;
5+
import com.alibaba.dashscope.protocol.GeneralServiceOption;
6+
import com.alibaba.dashscope.protocol.HttpMethod;
7+
import com.alibaba.dashscope.protocol.Protocol;
8+
import com.alibaba.dashscope.protocol.StreamingMode;
9+
import java.io.UnsupportedEncodingException;
10+
import java.net.URLEncoder;
11+
412
public final class AgentStudioConstants {
513
public static final String BASE_URL_TEMPLATE =
614
"https://%s.%s.maas.aliyuncs.com/api/v1/agentstudio";
@@ -13,4 +21,120 @@ public final class AgentStudioConstants {
1321
public static final String ENV_REGION = "DASHSCOPE_API_REGION";
1422

1523
private AgentStudioConstants() {}
24+
25+
public static final class SSEEventType {
26+
public static final String MESSAGE = "message";
27+
public static final String INTERRUPT = "interrupt";
28+
public static final String TOOL_CONFIRMATION = "tool_confirmation";
29+
public static final String FUNCTION_CALL_OUTPUT = "function_call_output";
30+
public static final String TOOL_CALL_OUTPUT = "tool_call_output";
31+
public static final String DEFINE_OUTCOME = "define_outcome";
32+
public static final String FUNCTION_CALL = "function_call";
33+
public static final String TOOL_CALL = "tool_call";
34+
public static final String REASONING = "reasoning";
35+
public static final String MCP_CALL = "mcp_call";
36+
public static final String MCP_CALL_OUTPUT = "mcp_call_output";
37+
public static final String SESSION_STATUS = "session_status";
38+
public static final String ERROR = "error";
39+
public static final String SESSION_UPDATED = "session_updated";
40+
public static final String OUTCOME_EVALUATION = "outcome_evaluation";
41+
42+
private SSEEventType() {}
43+
}
44+
45+
public static final class MessageRole {
46+
public static final String USER = "user";
47+
public static final String ASSISTANT = "assistant";
48+
public static final String TOOL = "tool";
49+
50+
private MessageRole() {}
51+
}
52+
53+
public static final class BlockType {
54+
public static final String TEXT = "text";
55+
public static final String IMAGE = "image";
56+
public static final String AUDIO = "audio";
57+
public static final String DATA = "data";
58+
public static final String FILE = "file";
59+
public static final String REFUSAL = "refusal";
60+
public static final String ERROR = "error";
61+
62+
private BlockType() {}
63+
}
64+
65+
public static final class SessionStatusValue {
66+
public static final String IDLE = "idle";
67+
public static final String RUNNING = "running";
68+
public static final String RESCHEDULING = "rescheduling";
69+
public static final String TERMINATED = "terminated";
70+
71+
private SessionStatusValue() {}
72+
}
73+
74+
public static String resolveBaseUrl(String workspace, String region) {
75+
String ws = workspace;
76+
if (ws == null || ws.isEmpty()) {
77+
ws = System.getenv(ENV_WORKSPACE);
78+
}
79+
if (ws == null || ws.isEmpty()) {
80+
throw new IllegalArgumentException(
81+
"workspace is required. Pass it to the constructor, "
82+
+ "set DASHSCOPE_WORKSPACE env var, or set DASHSCOPE_AGENTSTUDIO_URL.");
83+
}
84+
String r = region;
85+
if (r == null || r.isEmpty()) {
86+
r = System.getenv(ENV_REGION);
87+
}
88+
if (r == null || r.isEmpty()) {
89+
r = DEFAULT_REGION;
90+
}
91+
return String.format(BASE_URL_TEMPLATE, ws, r);
92+
}
93+
94+
/**
95+
* Build a {@link GeneralServiceOption} for an agentstudio endpoint. When {@code baseUrl} is
96+
* non-null it overrides the global default; otherwise the request falls back to {@code
97+
* Constants.baseHttpApiUrl} via {@code GeneralApi}.
98+
*/
99+
public static GeneralServiceOption newServiceOption(
100+
HttpMethod method, String path, String baseUrl) {
101+
GeneralServiceOption opt =
102+
GeneralServiceOption.builder()
103+
.protocol(Protocol.HTTP)
104+
.httpMethod(method)
105+
.streamingMode(StreamingMode.OUT)
106+
.path(path)
107+
.build();
108+
if (baseUrl != null) {
109+
opt.setBaseHttpUrl(baseUrl);
110+
}
111+
return opt;
112+
}
113+
114+
/**
115+
* Stamp the client's instance apiKey onto a param if the caller didn't set one. The global
116+
* fallback chain in {@link com.alibaba.dashscope.utils.ApiKey#getApiKey} still applies when
117+
* {@code apiKey} is null.
118+
*/
119+
public static <P extends HalfDuplexParamBase> P withApiKey(String apiKey, P param) {
120+
if (apiKey != null && param.getApiKey() == null) {
121+
param.setApiKey(apiKey);
122+
}
123+
return param;
124+
}
125+
126+
/** Append {@code key=value} (URL-encoded) to {@code sb} if value is non-null. Joins with &. */
127+
public static void appendParam(StringBuilder sb, String key, Object value) {
128+
if (value == null) {
129+
return;
130+
}
131+
if (sb.length() > 0) {
132+
sb.append("&");
133+
}
134+
try {
135+
sb.append(key).append("=").append(URLEncoder.encode(value.toString(), "UTF-8"));
136+
} catch (UnsupportedEncodingException e) {
137+
sb.append(key).append("=").append(value);
138+
}
139+
}
16140
}

0 commit comments

Comments
 (0)