diff --git a/.changeset/bailian-session-runtime-resources.md b/.changeset/bailian-session-runtime-resources.md new file mode 100644 index 0000000..de0f39c --- /dev/null +++ b/.changeset/bailian-session-runtime-resources.md @@ -0,0 +1,16 @@ +--- +"@openagentpack/sdk": minor +--- + +为百炼 Managed Agents 增加会话运行时资源 API(挂载/列出/查详情/卸载),支持向已创建的 +Session 动态追加文件:`mountRemoteSessionResource`、`listRemoteSessionResources`、 +`getRemoteSessionResource`、`unmountRemoteSessionResource`(bailian adapter 的 +`mountSessionResource` / `listSessionResources` / `getSessionResource` / `unmountSessionResource` +可选方法)。中性层沿用全沙箱路径(`/mnt/session/uploads/…`),bailian wire 自动换算为运行时 +端点要求的 `/uploads/…` 前缀并在非法根路径上客户端拒绝;资源响应补齐 `SessionResourceInfo.id` +(`sesrsc_…`,卸载凭据)。挂载与卸载不产生会话事件;provider 返回的 `file_id` 是服务端内部副本 +id,完整载荷保留在 `attributes` 中。运行时挂载不进入 `getSession` 的 `resources` 回显,对账请 +使用列表端点。合同经 2026-11-02 live 实测校准。 + +上述四个 SDK 入口接受仅含 Provider 配置的 `ManagedApiContext`,无需项目配置或本地 State; +现有 `ProjectRuntimeContext` 调用保持兼容。 diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index ab3c43a..31e9514 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -151,6 +151,7 @@ export { getRemoteDeployment, getRemoteDeploymentRun, getRemoteEnvironment, + getRemoteSessionResource, getRemoteSkill, getRemoteSkillDownloadInfo, getRemoteSkillVersion, @@ -161,12 +162,15 @@ export { listRemoteDeployments, listRemoteEnvironments, listRemoteFiles, + listRemoteSessionResources, listRemoteSkills, listRemoteSkillVersions, listRemoteVaults, + mountRemoteSessionResource, runRemoteDeployment, sendRemoteSessionEvents, setRemoteDeploymentPaused, + unmountRemoteSessionResource, updateRemoteSession, } from "./internal/core/managed-api-runtime.ts"; export type { ManagedApiTarget } from "./internal/core/managed-api-runtime.ts"; @@ -187,6 +191,9 @@ export type { ManagedAgentProviderCapabilities, SessionEventInput, SessionEventSendResult, + SessionResourceListOptions, + SessionResourceMountInput, + SessionResourcePage, SessionUpdateInput, SkillDownloadInfo, SkillListOptions, diff --git a/packages/sdk/src/internal/core/managed-api-runtime.ts b/packages/sdk/src/internal/core/managed-api-runtime.ts index 228f69f..e740a00 100644 --- a/packages/sdk/src/internal/core/managed-api-runtime.ts +++ b/packages/sdk/src/internal/core/managed-api-runtime.ts @@ -20,6 +20,9 @@ import type { ManagedAgentProviderCapabilities, SessionEventInput, SessionEventSendResult, + SessionResourceListOptions, + SessionResourceMountInput, + SessionResourcePage, SessionUpdateInput, SkillDownloadInfo, SkillListOptions, @@ -30,9 +33,10 @@ import type { VaultListOptions, VaultPage, } from "../types/managed-api.ts"; -import type { ProviderSessionInfo } from "../types/session.ts"; +import type { ProviderSessionInfo, SessionResourceInfo } from "../types/session.ts"; import type { ProviderSkillInfo } from "../types/skill-info.ts"; import { + type ManagedApiContext, requireManagedApiMethod as requiredMethod, resolveManagedApiAdapter as resolveAdapter, } from "./managed-api-context.ts"; @@ -209,6 +213,49 @@ export async function archiveRemoteSession( return method.call(adapter, id); } +export async function mountRemoteSessionResource( + ctx: ManagedApiContext, + sessionId: string, + input: SessionResourceMountInput, + options: ManagedApiTarget = {}, +): Promise { + const adapter = resolveAdapter(ctx, options.provider); + const method = requiredMethod(adapter.mountSessionResource, adapter.name, "session resource mounting"); + return method.call(adapter, sessionId, input); +} + +export async function listRemoteSessionResources( + ctx: ManagedApiContext, + sessionId: string, + options: ManagedApiTarget & SessionResourceListOptions = {}, +): Promise { + const adapter = resolveAdapter(ctx, options.provider); + const method = requiredMethod(adapter.listSessionResources, adapter.name, "session resource listing"); + return method.call(adapter, sessionId, options); +} + +export async function getRemoteSessionResource( + ctx: ManagedApiContext, + sessionId: string, + resourceId: string, + options: ManagedApiTarget = {}, +): Promise { + const adapter = resolveAdapter(ctx, options.provider); + const method = requiredMethod(adapter.getSessionResource, adapter.name, "session resource lookup"); + return method.call(adapter, sessionId, resourceId); +} + +export async function unmountRemoteSessionResource( + ctx: ManagedApiContext, + sessionId: string, + resourceId: string, + options: ManagedApiTarget = {}, +): Promise { + const adapter = resolveAdapter(ctx, options.provider); + const method = requiredMethod(adapter.unmountSessionResource, adapter.name, "session resource unmounting"); + return method.call(adapter, sessionId, resourceId); +} + export async function sendRemoteSessionEvents( ctx: ProjectRuntimeContext, id: string, diff --git a/packages/sdk/src/internal/providers/bailian/adapter.ts b/packages/sdk/src/internal/providers/bailian/adapter.ts index a879b10..5697835 100644 --- a/packages/sdk/src/internal/providers/bailian/adapter.ts +++ b/packages/sdk/src/internal/providers/bailian/adapter.ts @@ -37,6 +37,9 @@ import type { FilePage, SessionEventInput, SessionEventSendResult, + SessionResourceListOptions, + SessionResourceMountInput, + SessionResourcePage, SessionUpdateInput, SkillDownloadInfo, SkillListOptions, @@ -56,7 +59,13 @@ import type { UpdateMemoryInput, UpdateMemoryStoreInput, } from "../../types/memory.ts"; -import type { ProviderSessionInfo, SessionBindings, SessionFilter, SessionListResult } from "../../types/session.ts"; +import type { + ProviderSessionInfo, + SessionBindings, + SessionFilter, + SessionListResult, + SessionResourceInfo, +} from "../../types/session.ts"; import type { EventListOptions, EventStreamOptions, @@ -1211,6 +1220,35 @@ export class BailianAdapter implements ProviderAdapter { await this.client.delete(`/sessions/${id}`); } + async mountSessionResource(sessionId: string, input: SessionResourceMountInput): Promise { + const res = (await this.client.post(`/sessions/${sessionId}/resources`, { + type: input.type, + file_id: input.file_id, + // The runtime endpoint takes the path under the sandbox session root + // ("/uploads/…"), while the SDK's neutral layer — and the create-session + // API — use the full sandbox path ("/mnt/session/uploads/…"). + mount_path: toRuntimeResourceMountPath(input.mount_path), + })) as Record; + return toSessionResourceInfo(res); + } + + async listSessionResources(sessionId: string, options?: SessionResourceListOptions): Promise { + const res = (await this.client.get(withQuery(`/sessions/${sessionId}/resources`, cursorParams(options)))) as Record< + string, + unknown + >; + return cursorPage(res, toSessionResourceInfo); + } + + async getSessionResource(sessionId: string, resourceId: string): Promise { + const res = (await this.client.get(`/sessions/${sessionId}/resources/${resourceId}`)) as Record; + return toSessionResourceInfo(res); + } + + async unmountSessionResource(sessionId: string, resourceId: string): Promise { + await this.client.delete(`/sessions/${sessionId}/resources/${resourceId}`); + } + async sendSessionMessage(sessionId: string, message: string): Promise { const body = mapSendMessage(message); const res = (await this.client.post(`/sessions/${sessionId}/events`, body)) as Record; @@ -1363,6 +1401,38 @@ export function toSessionInfo(res: Record): ProviderSessionInfo return sessionInfo; } +/** Sandbox session root on bailian; the runtime resources API takes paths relative to it. */ +const BAILIAN_SESSION_SANDBOX_ROOT = "/mnt/session"; + +/** + * Runtime resource mount_path wire convention: absolute under `/uploads/`. Accept the + * full sandbox path used by the neutral layer and create-session API (`/mnt/session/uploads/…`), + * or an already wire-shaped `/uploads/…` path; anything else is rejected client-side. + */ +function toRuntimeResourceMountPath(mountPath: string): string { + const wirePath = mountPath.startsWith(`${BAILIAN_SESSION_SANDBOX_ROOT}/`) + ? mountPath.slice(BAILIAN_SESSION_SANDBOX_ROOT.length) + : mountPath; + if (wirePath === "/uploads" || wirePath.startsWith("/uploads/")) return wirePath; + throw new UserError( + `bailian runtime mount_path must resolve to '/uploads/…' (sandbox '${BAILIAN_SESSION_SANDBOX_ROOT}/uploads/…'); received '${mountPath}'.`, + ); +} + +/** Raw bailian session-resource object → neutral SessionResourceInfo; the full payload rides in `attributes`. */ +function toSessionResourceInfo(res: Record): SessionResourceInfo { + return { + ...(typeof res.id === "string" ? { id: res.id } : {}), + type: (res.type as string) ?? "file", + ...(typeof res.mount_path === "string" ? { mount_path: res.mount_path } : {}), + ...(typeof res.file_id === "string" ? { file_id: res.file_id } : {}), + ...(typeof res.memory_store_id === "string" ? { memory_store_id: res.memory_store_id } : {}), + ...(res.access === "read_write" || res.access === "read_only" ? { access: res.access } : {}), + ...(typeof res.instructions === "string" ? { instructions: res.instructions } : {}), + attributes: res, + }; +} + // Raw Bailian `/skills` item → neutral ProviderSkillInfo. Kept provider-local (not the // shared toRestSkillInfo) because bailian's wire differs: name is `name` (not display_title), // the custom catalog reports `customer`, and skills carry a real security-scan status. diff --git a/packages/sdk/src/internal/providers/interface.ts b/packages/sdk/src/internal/providers/interface.ts index 5c26823..acc6f9a 100644 --- a/packages/sdk/src/internal/providers/interface.ts +++ b/packages/sdk/src/internal/providers/interface.ts @@ -26,6 +26,9 @@ import type { FilePage, SessionEventInput, SessionEventSendResult, + SessionResourceListOptions, + SessionResourceMountInput, + SessionResourcePage, SessionUpdateInput, SkillDownloadInfo, SkillListOptions, @@ -54,7 +57,13 @@ import type { UpdateMemoryInput, UpdateMemoryStoreInput, } from "../types/memory.ts"; -import type { ProviderSessionInfo, SessionBindings, SessionFilter, SessionListResult } from "../types/session.ts"; +import type { + ProviderSessionInfo, + SessionBindings, + SessionFilter, + SessionListResult, + SessionResourceInfo, +} from "../types/session.ts"; import type { EventListOptions, EventStreamOptions, @@ -452,6 +461,13 @@ export interface ProviderAdapter { archiveSession?(id: string): Promise; deleteSession(id: string): Promise; + /** Mount one resource into a RUNNING session. Produces no session events. */ + mountSessionResource?(sessionId: string, input: SessionResourceMountInput): Promise; + listSessionResources?(sessionId: string, options?: SessionResourceListOptions): Promise; + getSessionResource?(sessionId: string, resourceId: string): Promise; + /** Unmount one resource and clear the provider's internal file copy. Unrecoverable. */ + unmountSessionResource?(sessionId: string, resourceId: string): Promise; + sendSessionMessage(sessionId: string, message: string): Promise; sendSessionEvents?(sessionId: string, events: SessionEventInput[]): Promise; streamSessionEvents(sessionId: string, options?: EventStreamOptions): AsyncIterable; diff --git a/packages/sdk/src/internal/types/managed-api.ts b/packages/sdk/src/internal/types/managed-api.ts index e67e40d..2f8d39b 100644 --- a/packages/sdk/src/internal/types/managed-api.ts +++ b/packages/sdk/src/internal/types/managed-api.ts @@ -1,6 +1,7 @@ import type { CredentialInjectionLocation, CredentialNetworking } from "./config.ts"; import type { CloudAgent, CloudEnvironment, CloudVault } from "./dto.ts"; import type { ProviderFileInfo } from "./file.ts"; +import type { SessionResourceInfo } from "./session.ts"; import type { ProviderSkillInfo } from "./skill-info.ts"; export interface CursorPage { @@ -87,6 +88,18 @@ export interface SessionUpdateInput { metadata?: Record; } +/** One resource to mount into a RUNNING session (provider session-resources API). */ +export interface SessionResourceMountInput { + type: "file"; + file_id: string; + /** Sandbox path inside the session, e.g. `/mnt/session/uploads/report.csv`. */ + mount_path: string; +} + +export type SessionResourceListOptions = CursorListOptions; + +export type SessionResourcePage = CursorPage; + /** Raw AgentStudio message/event input. Provider adapters preserve the wire shape. */ export type SessionEventInput = Record; diff --git a/packages/sdk/src/internal/types/session.ts b/packages/sdk/src/internal/types/session.ts index c01a687..9d28bf1 100644 --- a/packages/sdk/src/internal/types/session.ts +++ b/packages/sdk/src/internal/types/session.ts @@ -24,6 +24,8 @@ export type SessionResource = SessionGithubRepositoryResource | SessionMemorySto /** Provider response resource, including server-assigned sandbox mount paths. */ export interface SessionResourceInfo extends Record { + /** Provider-assigned resource id (`sesrsc_…`); required to unmount a runtime-mounted resource. */ + id?: string; type: string; mount_path?: string; memory_store_id?: string; diff --git a/packages/sdk/tests/unit/bailian-managed-api.test.ts b/packages/sdk/tests/unit/bailian-managed-api.test.ts index a88f0be..e7257e9 100644 --- a/packages/sdk/tests/unit/bailian-managed-api.test.ts +++ b/packages/sdk/tests/unit/bailian-managed-api.test.ts @@ -230,6 +230,88 @@ describe("Bailian session API additions", () => { }); }); +describe("Bailian session runtime resources", () => { + const resource = { + id: "sesrsc_1", + type: "file", + file_id: "file_internal_copy", + memory_store_id: null, + access: null, + instructions: null, + name: null, + description: null, + mount_path: "/mnt/session/uploads/report.csv", + created_at: "2026-11-02T18:19:52+08:00", + updated_at: "2026-11-02T18:19:52+08:00", + request_id: "req_1", + }; + + test("mounts a running-session file, mapping the sandbox path to the /uploads wire prefix", async () => { + const calls = installJsonFetch([resource]); + const mounted = await adapter().mountSessionResource("sesn_1", { + type: "file", + file_id: "file_original", + mount_path: "/mnt/session/uploads/report.csv", + }); + + expect(calls[0]!.method).toBe("POST"); + expect(calls[0]!.url).toBe(`${BASE}/sessions/sesn_1/resources`); + expect(calls[0]!.body).toEqual({ + type: "file", + file_id: "file_original", + mount_path: "/uploads/report.csv", + }); + expect(mounted.id).toBe("sesrsc_1"); + expect(mounted.type).toBe("file"); + expect(mounted.file_id).toBe("file_internal_copy"); + expect(mounted.mount_path).toBe("/mnt/session/uploads/report.csv"); + expect(mounted.attributes.request_id).toBe("req_1"); + }); + + test("mount accepts an already wire-shaped /uploads path and rejects foreign roots", async () => { + const calls = installJsonFetch([resource]); + await adapter().mountSessionResource("sesn_1", { + type: "file", + file_id: "file_original", + mount_path: "/uploads/a/b.txt", + }); + expect(calls[0]!.body).toEqual(expect.objectContaining({ mount_path: "/uploads/a/b.txt" })); + + await expect( + adapter().mountSessionResource("sesn_1", { + type: "file", + file_id: "file_original", + mount_path: "/mnt/session/other/x.txt", + }), + ).rejects.toThrow("/uploads/"); + }); + + test("lists, gets and unmounts session resources", async () => { + const calls = installJsonFetch([ + { data: [resource], next_page: "P2" }, + resource, + { id: "sesrsc_1", type: "session_resource_deleted", request_id: "req_2" }, + ]); + + const page = await adapter().listSessionResources("sesn_1", { limit: 10, page: "P1" }); + const single = await adapter().getSessionResource("sesn_1", "sesrsc_1"); + await adapter().unmountSessionResource("sesn_1", "sesrsc_1"); + + const listUrl = new URL(calls[0]!.url); + expect(calls[0]!.url).toBe(`${BASE}/sessions/sesn_1/resources?limit=10&page=P1`); + expect(listUrl.searchParams.get("limit")).toBe("10"); + expect(listUrl.searchParams.get("page")).toBe("P1"); + expect(page.data[0]?.id).toBe("sesrsc_1"); + expect(page.data[0]?.file_id).toBe("file_internal_copy"); + expect(page.next_page).toBe("P2"); + expect(calls[1]!.url).toBe(`${BASE}/sessions/sesn_1/resources/sesrsc_1`); + expect(single.id).toBe("sesrsc_1"); + expect(single.mount_path).toBe("/mnt/session/uploads/report.csv"); + expect(calls[2]!.method).toBe("DELETE"); + expect(calls[2]!.url).toBe(`${BASE}/sessions/sesn_1/resources/sesrsc_1`); + }); +}); + test("operation capabilities expose thread reads and keep unimplemented thread mutations unsupported", () => { const capabilities = getManagedAgentProviderCapabilities("bailian"); expect(capabilities.operations["agent.create"]?.supported).toBe(true);