Skip to content

Commit 5a7cf61

Browse files
kapla73zhangyi970212WB01116753chenanran555
authored
Feat/session runtime resource mount (#123)
* feat(sdk): 新增百炼资源用量查询接口 新增 listModelUsage / listToolUsage 对接 app-control usage.listModelUsage / usage.listToolUsage, 支持 workspace/agent/session 三种 scope 与毫秒级 time_range 交叉校验(session 禁止、其余必传), 公开 UsageScope、ModelUsagePage、ToolUsagePage 等类型;apps/server 暴露 POST /api/usage/models、 POST /api/usage/tools 并刷新 OpenAPI 快照与 webui 生成类型。REST 映射已与 usage 契约方确认为 AgentStudio 资源风格,公网环境启用端点后即通。附 changeset 触发 @openagentpack/sdk minor 版本。 * fix: 修复 minimatch 传递依赖漏洞 webui 经 openapi-typescript / @redocly/openapi-core 引入 minimatch<11.0.1 (GHSA-g3hv-75wg-m249,moderate,globparts 放大 DoS),root overrides 强制 11.0.3, bun.lock 同步重解析;openapi-typescript 生成回归与全量测试通过。 * ci: 统一 CI bun 版本至 1.4.4 bun.lock 由 bun 1.4.4 生成,CI 的 1.3.5 消费新格式 lock 在冷缓存 runner 上 连续两次挂起在 Install dependencies(本地 1.3.5/1.4.4 均可正常安装)。 统一全部 workflow 的 bun 至 1.4.4,与 lock 生成版本一致,audit/install 均已在该版本本地验证。 * fix: 恢复 bun.lock 为默认官方 registry 并锁定 biome 版本 此前在本机重新解析依赖时,本机 npm 配置的公司内网镜像地址(registry.anpm.alibaba-inc.com) 被写入 bun.lock 的 492 个 tarball URL,GitHub runner 公网环境无法访问,导致发布流水线 Install dependencies 无限挂起。用官方 registry 重新生成 lock(恢复默认源的紧凑格式,与 历史成功发布一致),依赖版本顺带升级至最新修复线(zod 4.6.7 / hono 4.13.16 / @hono/node-server 2.1.5 / vite 8.3.5);@biomejs/biome 重解析到 2.6.4 后二进制在 macOS 上执行即被 SIGKILL,直接依赖声明锁定 2.5.3。CI 同款 bun 1.3.5/1.4.4 验证 audit 归零、全量测试通过。 * fix(cli): 适配 @clack/prompts 1.8 的 isCancel 类型签名 依赖重解析后 @clack/prompts 1.5.1→1.8.1,isCancel 谓词不再把 string | symbol 收窄为 string,init 命令的 provider 传入 parseProvider 报 TS2345。取消分支已提前退出,显式断言 string,运行时行为不变。 全 workspace typecheck 与 cli 测试通过。 * feat(sdk): 新增会话资源运行时挂载与查询接口 * fix(sdk): 修复会话资源接口上下文类型限制 --------- Co-authored-by: zhanglei <zl01258556@alibaba-inc.com> Co-authored-by: WB01116753 <xk01116753@alibaba-inc.com> Co-authored-by: 悟扬 <car534511@alibaba-inc.com>
1 parent 157b1ac commit 5a7cf61

8 files changed

Lines changed: 256 additions & 3 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@openagentpack/sdk": minor
3+
---
4+
5+
为百炼 Managed Agents 增加会话运行时资源 API(挂载/列出/查详情/卸载),支持向已创建的
6+
Session 动态追加文件:`mountRemoteSessionResource`、`listRemoteSessionResources`、
7+
`getRemoteSessionResource`、`unmountRemoteSessionResource`(bailian adapter 的
8+
`mountSessionResource` / `listSessionResources` / `getSessionResource` / `unmountSessionResource`
9+
可选方法)。中性层沿用全沙箱路径(`/mnt/session/uploads/…`),bailian wire 自动换算为运行时
10+
端点要求的 `/uploads/…` 前缀并在非法根路径上客户端拒绝;资源响应补齐 `SessionResourceInfo.id`
11+
(`sesrsc_…`,卸载凭据)。挂载与卸载不产生会话事件;provider 返回的 `file_id` 是服务端内部副本
12+
id,完整载荷保留在 `attributes` 中。运行时挂载不进入 `getSession` 的 `resources` 回显,对账请
13+
使用列表端点。合同经 2026-11-02 live 实测校准。
14+
15+
上述四个 SDK 入口接受仅含 Provider 配置的 `ManagedApiContext`,无需项目配置或本地 State;
16+
现有 `ProjectRuntimeContext` 调用保持兼容。

‎packages/sdk/src/index.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export {
151151
getRemoteDeployment,
152152
getRemoteDeploymentRun,
153153
getRemoteEnvironment,
154+
getRemoteSessionResource,
154155
getRemoteSkill,
155156
getRemoteSkillDownloadInfo,
156157
getRemoteSkillVersion,
@@ -161,12 +162,15 @@ export {
161162
listRemoteDeployments,
162163
listRemoteEnvironments,
163164
listRemoteFiles,
165+
listRemoteSessionResources,
164166
listRemoteSkills,
165167
listRemoteSkillVersions,
166168
listRemoteVaults,
169+
mountRemoteSessionResource,
167170
runRemoteDeployment,
168171
sendRemoteSessionEvents,
169172
setRemoteDeploymentPaused,
173+
unmountRemoteSessionResource,
170174
updateRemoteSession,
171175
} from "./internal/core/managed-api-runtime.ts";
172176
export type { ManagedApiTarget } from "./internal/core/managed-api-runtime.ts";
@@ -187,6 +191,9 @@ export type {
187191
ManagedAgentProviderCapabilities,
188192
SessionEventInput,
189193
SessionEventSendResult,
194+
SessionResourceListOptions,
195+
SessionResourceMountInput,
196+
SessionResourcePage,
190197
SessionUpdateInput,
191198
SkillDownloadInfo,
192199
SkillListOptions,

‎packages/sdk/src/internal/core/managed-api-runtime.ts‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import type {
2020
ManagedAgentProviderCapabilities,
2121
SessionEventInput,
2222
SessionEventSendResult,
23+
SessionResourceListOptions,
24+
SessionResourceMountInput,
25+
SessionResourcePage,
2326
SessionUpdateInput,
2427
SkillDownloadInfo,
2528
SkillListOptions,
@@ -30,9 +33,10 @@ import type {
3033
VaultListOptions,
3134
VaultPage,
3235
} from "../types/managed-api.ts";
33-
import type { ProviderSessionInfo } from "../types/session.ts";
36+
import type { ProviderSessionInfo, SessionResourceInfo } from "../types/session.ts";
3437
import type { ProviderSkillInfo } from "../types/skill-info.ts";
3538
import {
39+
type ManagedApiContext,
3640
requireManagedApiMethod as requiredMethod,
3741
resolveManagedApiAdapter as resolveAdapter,
3842
} from "./managed-api-context.ts";
@@ -209,6 +213,49 @@ export async function archiveRemoteSession(
209213
return method.call(adapter, id);
210214
}
211215

216+
export async function mountRemoteSessionResource(
217+
ctx: ManagedApiContext,
218+
sessionId: string,
219+
input: SessionResourceMountInput,
220+
options: ManagedApiTarget = {},
221+
): Promise<SessionResourceInfo> {
222+
const adapter = resolveAdapter(ctx, options.provider);
223+
const method = requiredMethod(adapter.mountSessionResource, adapter.name, "session resource mounting");
224+
return method.call(adapter, sessionId, input);
225+
}
226+
227+
export async function listRemoteSessionResources(
228+
ctx: ManagedApiContext,
229+
sessionId: string,
230+
options: ManagedApiTarget & SessionResourceListOptions = {},
231+
): Promise<SessionResourcePage> {
232+
const adapter = resolveAdapter(ctx, options.provider);
233+
const method = requiredMethod(adapter.listSessionResources, adapter.name, "session resource listing");
234+
return method.call(adapter, sessionId, options);
235+
}
236+
237+
export async function getRemoteSessionResource(
238+
ctx: ManagedApiContext,
239+
sessionId: string,
240+
resourceId: string,
241+
options: ManagedApiTarget = {},
242+
): Promise<SessionResourceInfo> {
243+
const adapter = resolveAdapter(ctx, options.provider);
244+
const method = requiredMethod(adapter.getSessionResource, adapter.name, "session resource lookup");
245+
return method.call(adapter, sessionId, resourceId);
246+
}
247+
248+
export async function unmountRemoteSessionResource(
249+
ctx: ManagedApiContext,
250+
sessionId: string,
251+
resourceId: string,
252+
options: ManagedApiTarget = {},
253+
): Promise<void> {
254+
const adapter = resolveAdapter(ctx, options.provider);
255+
const method = requiredMethod(adapter.unmountSessionResource, adapter.name, "session resource unmounting");
256+
return method.call(adapter, sessionId, resourceId);
257+
}
258+
212259
export async function sendRemoteSessionEvents(
213260
ctx: ProjectRuntimeContext,
214261
id: string,

‎packages/sdk/src/internal/providers/bailian/adapter.ts‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ import type {
3737
FilePage,
3838
SessionEventInput,
3939
SessionEventSendResult,
40+
SessionResourceListOptions,
41+
SessionResourceMountInput,
42+
SessionResourcePage,
4043
SessionUpdateInput,
4144
SkillDownloadInfo,
4245
SkillListOptions,
@@ -56,7 +59,13 @@ import type {
5659
UpdateMemoryInput,
5760
UpdateMemoryStoreInput,
5861
} from "../../types/memory.ts";
59-
import type { ProviderSessionInfo, SessionBindings, SessionFilter, SessionListResult } from "../../types/session.ts";
62+
import type {
63+
ProviderSessionInfo,
64+
SessionBindings,
65+
SessionFilter,
66+
SessionListResult,
67+
SessionResourceInfo,
68+
} from "../../types/session.ts";
6069
import type {
6170
EventListOptions,
6271
EventStreamOptions,
@@ -1211,6 +1220,35 @@ export class BailianAdapter implements ProviderAdapter {
12111220
await this.client.delete(`/sessions/${id}`);
12121221
}
12131222

1223+
async mountSessionResource(sessionId: string, input: SessionResourceMountInput): Promise<SessionResourceInfo> {
1224+
const res = (await this.client.post(`/sessions/${sessionId}/resources`, {
1225+
type: input.type,
1226+
file_id: input.file_id,
1227+
// The runtime endpoint takes the path under the sandbox session root
1228+
// ("/uploads/…"), while the SDK's neutral layer — and the create-session
1229+
// API — use the full sandbox path ("/mnt/session/uploads/…").
1230+
mount_path: toRuntimeResourceMountPath(input.mount_path),
1231+
})) as Record<string, unknown>;
1232+
return toSessionResourceInfo(res);
1233+
}
1234+
1235+
async listSessionResources(sessionId: string, options?: SessionResourceListOptions): Promise<SessionResourcePage> {
1236+
const res = (await this.client.get(withQuery(`/sessions/${sessionId}/resources`, cursorParams(options)))) as Record<
1237+
string,
1238+
unknown
1239+
>;
1240+
return cursorPage(res, toSessionResourceInfo);
1241+
}
1242+
1243+
async getSessionResource(sessionId: string, resourceId: string): Promise<SessionResourceInfo> {
1244+
const res = (await this.client.get(`/sessions/${sessionId}/resources/${resourceId}`)) as Record<string, unknown>;
1245+
return toSessionResourceInfo(res);
1246+
}
1247+
1248+
async unmountSessionResource(sessionId: string, resourceId: string): Promise<void> {
1249+
await this.client.delete(`/sessions/${sessionId}/resources/${resourceId}`);
1250+
}
1251+
12141252
async sendSessionMessage(sessionId: string, message: string): Promise<string | undefined> {
12151253
const body = mapSendMessage(message);
12161254
const res = (await this.client.post(`/sessions/${sessionId}/events`, body)) as Record<string, unknown>;
@@ -1363,6 +1401,38 @@ export function toSessionInfo(res: Record<string, unknown>): ProviderSessionInfo
13631401
return sessionInfo;
13641402
}
13651403

1404+
/** Sandbox session root on bailian; the runtime resources API takes paths relative to it. */
1405+
const BAILIAN_SESSION_SANDBOX_ROOT = "/mnt/session";
1406+
1407+
/**
1408+
* Runtime resource mount_path wire convention: absolute under `/uploads/`. Accept the
1409+
* full sandbox path used by the neutral layer and create-session API (`/mnt/session/uploads/…`),
1410+
* or an already wire-shaped `/uploads/…` path; anything else is rejected client-side.
1411+
*/
1412+
function toRuntimeResourceMountPath(mountPath: string): string {
1413+
const wirePath = mountPath.startsWith(`${BAILIAN_SESSION_SANDBOX_ROOT}/`)
1414+
? mountPath.slice(BAILIAN_SESSION_SANDBOX_ROOT.length)
1415+
: mountPath;
1416+
if (wirePath === "/uploads" || wirePath.startsWith("/uploads/")) return wirePath;
1417+
throw new UserError(
1418+
`bailian runtime mount_path must resolve to '/uploads/…' (sandbox '${BAILIAN_SESSION_SANDBOX_ROOT}/uploads/…'); received '${mountPath}'.`,
1419+
);
1420+
}
1421+
1422+
/** Raw bailian session-resource object → neutral SessionResourceInfo; the full payload rides in `attributes`. */
1423+
function toSessionResourceInfo(res: Record<string, unknown>): SessionResourceInfo {
1424+
return {
1425+
...(typeof res.id === "string" ? { id: res.id } : {}),
1426+
type: (res.type as string) ?? "file",
1427+
...(typeof res.mount_path === "string" ? { mount_path: res.mount_path } : {}),
1428+
...(typeof res.file_id === "string" ? { file_id: res.file_id } : {}),
1429+
...(typeof res.memory_store_id === "string" ? { memory_store_id: res.memory_store_id } : {}),
1430+
...(res.access === "read_write" || res.access === "read_only" ? { access: res.access } : {}),
1431+
...(typeof res.instructions === "string" ? { instructions: res.instructions } : {}),
1432+
attributes: res,
1433+
};
1434+
}
1435+
13661436
// Raw Bailian `/skills` item → neutral ProviderSkillInfo. Kept provider-local (not the
13671437
// shared toRestSkillInfo) because bailian's wire differs: name is `name` (not display_title),
13681438
// the custom catalog reports `customer`, and skills carry a real security-scan status.

‎packages/sdk/src/internal/providers/interface.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import type {
2626
FilePage,
2727
SessionEventInput,
2828
SessionEventSendResult,
29+
SessionResourceListOptions,
30+
SessionResourceMountInput,
31+
SessionResourcePage,
2932
SessionUpdateInput,
3033
SkillDownloadInfo,
3134
SkillListOptions,
@@ -54,7 +57,13 @@ import type {
5457
UpdateMemoryInput,
5558
UpdateMemoryStoreInput,
5659
} from "../types/memory.ts";
57-
import type { ProviderSessionInfo, SessionBindings, SessionFilter, SessionListResult } from "../types/session.ts";
60+
import type {
61+
ProviderSessionInfo,
62+
SessionBindings,
63+
SessionFilter,
64+
SessionListResult,
65+
SessionResourceInfo,
66+
} from "../types/session.ts";
5867
import type {
5968
EventListOptions,
6069
EventStreamOptions,
@@ -452,6 +461,13 @@ export interface ProviderAdapter {
452461
archiveSession?(id: string): Promise<ProviderSessionInfo>;
453462
deleteSession(id: string): Promise<void>;
454463

464+
/** Mount one resource into a RUNNING session. Produces no session events. */
465+
mountSessionResource?(sessionId: string, input: SessionResourceMountInput): Promise<SessionResourceInfo>;
466+
listSessionResources?(sessionId: string, options?: SessionResourceListOptions): Promise<SessionResourcePage>;
467+
getSessionResource?(sessionId: string, resourceId: string): Promise<SessionResourceInfo>;
468+
/** Unmount one resource and clear the provider's internal file copy. Unrecoverable. */
469+
unmountSessionResource?(sessionId: string, resourceId: string): Promise<void>;
470+
455471
sendSessionMessage(sessionId: string, message: string): Promise<string | undefined>;
456472
sendSessionEvents?(sessionId: string, events: SessionEventInput[]): Promise<SessionEventSendResult>;
457473
streamSessionEvents(sessionId: string, options?: EventStreamOptions): AsyncIterable<ProviderSessionEvent>;

‎packages/sdk/src/internal/types/managed-api.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CredentialInjectionLocation, CredentialNetworking } from "./config.ts";
22
import type { CloudAgent, CloudEnvironment, CloudVault } from "./dto.ts";
33
import type { ProviderFileInfo } from "./file.ts";
4+
import type { SessionResourceInfo } from "./session.ts";
45
import type { ProviderSkillInfo } from "./skill-info.ts";
56

67
export interface CursorPage<T> {
@@ -87,6 +88,18 @@ export interface SessionUpdateInput {
8788
metadata?: Record<string, string>;
8889
}
8990

91+
/** One resource to mount into a RUNNING session (provider session-resources API). */
92+
export interface SessionResourceMountInput {
93+
type: "file";
94+
file_id: string;
95+
/** Sandbox path inside the session, e.g. `/mnt/session/uploads/report.csv`. */
96+
mount_path: string;
97+
}
98+
99+
export type SessionResourceListOptions = CursorListOptions;
100+
101+
export type SessionResourcePage = CursorPage<SessionResourceInfo>;
102+
90103
/** Raw AgentStudio message/event input. Provider adapters preserve the wire shape. */
91104
export type SessionEventInput = Record<string, unknown>;
92105

‎packages/sdk/src/internal/types/session.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export type SessionResource = SessionGithubRepositoryResource | SessionMemorySto
2424

2525
/** Provider response resource, including server-assigned sandbox mount paths. */
2626
export interface SessionResourceInfo extends Record<string, unknown> {
27+
/** Provider-assigned resource id (`sesrsc_…`); required to unmount a runtime-mounted resource. */
28+
id?: string;
2729
type: string;
2830
mount_path?: string;
2931
memory_store_id?: string;

‎packages/sdk/tests/unit/bailian-managed-api.test.ts‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,88 @@ describe("Bailian session API additions", () => {
230230
});
231231
});
232232

233+
describe("Bailian session runtime resources", () => {
234+
const resource = {
235+
id: "sesrsc_1",
236+
type: "file",
237+
file_id: "file_internal_copy",
238+
memory_store_id: null,
239+
access: null,
240+
instructions: null,
241+
name: null,
242+
description: null,
243+
mount_path: "/mnt/session/uploads/report.csv",
244+
created_at: "2026-11-02T18:19:52+08:00",
245+
updated_at: "2026-11-02T18:19:52+08:00",
246+
request_id: "req_1",
247+
};
248+
249+
test("mounts a running-session file, mapping the sandbox path to the /uploads wire prefix", async () => {
250+
const calls = installJsonFetch([resource]);
251+
const mounted = await adapter().mountSessionResource("sesn_1", {
252+
type: "file",
253+
file_id: "file_original",
254+
mount_path: "/mnt/session/uploads/report.csv",
255+
});
256+
257+
expect(calls[0]!.method).toBe("POST");
258+
expect(calls[0]!.url).toBe(`${BASE}/sessions/sesn_1/resources`);
259+
expect(calls[0]!.body).toEqual({
260+
type: "file",
261+
file_id: "file_original",
262+
mount_path: "/uploads/report.csv",
263+
});
264+
expect(mounted.id).toBe("sesrsc_1");
265+
expect(mounted.type).toBe("file");
266+
expect(mounted.file_id).toBe("file_internal_copy");
267+
expect(mounted.mount_path).toBe("/mnt/session/uploads/report.csv");
268+
expect(mounted.attributes.request_id).toBe("req_1");
269+
});
270+
271+
test("mount accepts an already wire-shaped /uploads path and rejects foreign roots", async () => {
272+
const calls = installJsonFetch([resource]);
273+
await adapter().mountSessionResource("sesn_1", {
274+
type: "file",
275+
file_id: "file_original",
276+
mount_path: "/uploads/a/b.txt",
277+
});
278+
expect(calls[0]!.body).toEqual(expect.objectContaining({ mount_path: "/uploads/a/b.txt" }));
279+
280+
await expect(
281+
adapter().mountSessionResource("sesn_1", {
282+
type: "file",
283+
file_id: "file_original",
284+
mount_path: "/mnt/session/other/x.txt",
285+
}),
286+
).rejects.toThrow("/uploads/");
287+
});
288+
289+
test("lists, gets and unmounts session resources", async () => {
290+
const calls = installJsonFetch([
291+
{ data: [resource], next_page: "P2" },
292+
resource,
293+
{ id: "sesrsc_1", type: "session_resource_deleted", request_id: "req_2" },
294+
]);
295+
296+
const page = await adapter().listSessionResources("sesn_1", { limit: 10, page: "P1" });
297+
const single = await adapter().getSessionResource("sesn_1", "sesrsc_1");
298+
await adapter().unmountSessionResource("sesn_1", "sesrsc_1");
299+
300+
const listUrl = new URL(calls[0]!.url);
301+
expect(calls[0]!.url).toBe(`${BASE}/sessions/sesn_1/resources?limit=10&page=P1`);
302+
expect(listUrl.searchParams.get("limit")).toBe("10");
303+
expect(listUrl.searchParams.get("page")).toBe("P1");
304+
expect(page.data[0]?.id).toBe("sesrsc_1");
305+
expect(page.data[0]?.file_id).toBe("file_internal_copy");
306+
expect(page.next_page).toBe("P2");
307+
expect(calls[1]!.url).toBe(`${BASE}/sessions/sesn_1/resources/sesrsc_1`);
308+
expect(single.id).toBe("sesrsc_1");
309+
expect(single.mount_path).toBe("/mnt/session/uploads/report.csv");
310+
expect(calls[2]!.method).toBe("DELETE");
311+
expect(calls[2]!.url).toBe(`${BASE}/sessions/sesn_1/resources/sesrsc_1`);
312+
});
313+
});
314+
233315
test("operation capabilities expose thread reads and keep unimplemented thread mutations unsupported", () => {
234316
const capabilities = getManagedAgentProviderCapabilities("bailian");
235317
expect(capabilities.operations["agent.create"]?.supported).toBe(true);

0 commit comments

Comments
 (0)