Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/orbis-agent-backend/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ export interface AgentWorkspaceRegisterInput {
readonly folderRef: string;
}

export interface AgentWorkspaceCreateFolderInput {
readonly driverId: AgentDriverId;
readonly name: string;
readonly parentFolderRef: string;
}

export interface AgentWorkspaceRegisterResult {
readonly created: boolean;
readonly workspace: AgentWorkspaceDescriptor;
Expand Down Expand Up @@ -219,6 +225,9 @@ export interface AgentBackend {

close(): Promise<void>;
browseWorkspaceFolders(input: AgentWorkspaceBrowseInput): Promise<AgentWorkspaceFolderListing>;
createWorkspaceFolder(
input: AgentWorkspaceCreateFolderInput,
): Promise<AgentWorkspaceFolderDescriptor>;
connectRuntime(ref: AgentSessionRef): Promise<AgentSessionRuntime>;
createSession(input: AgentSessionCreateInput): Promise<AgentSessionRecord>;
listDrivers(): Promise<readonly AgentDriverDescriptor[]>;
Expand Down
1 change: 1 addition & 0 deletions packages/orbis-agent-backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
type AgentModelThinkingLevelMetadata,
type AgentWorkspaceDescriptor,
type AgentWorkspaceBrowseInput,
type AgentWorkspaceCreateFolderInput,
type AgentWorkspaceFolderDescriptor,
type AgentWorkspaceFolderListing,
type AgentWorkspaceListInput,
Expand Down
8 changes: 8 additions & 0 deletions packages/orbis-agent-backend/src/testkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
AgentModelMetadata,
AgentWorkspaceDescriptor,
AgentWorkspaceBrowseInput,
AgentWorkspaceCreateFolderInput,
AgentWorkspaceFolderDescriptor,
AgentWorkspaceFolderListing,
AgentWorkspaceListInput,
AgentWorkspaceRegisterInput,
Expand Down Expand Up @@ -138,6 +140,12 @@ export class FakeAgentBackend implements AgentBackend, FakeRuntimeHost {
throw new AgentBackendError("unsupported", "The fake backend cannot browse folders");
}

async createWorkspaceFolder(
_input: AgentWorkspaceCreateFolderInput,
): Promise<AgentWorkspaceFolderDescriptor> {
throw new AgentBackendError("unsupported", "The fake backend cannot create folders");
}

async registerWorkspace(
_input: AgentWorkspaceRegisterInput,
): Promise<AgentWorkspaceRegisterResult> {
Expand Down
1 change: 1 addition & 0 deletions packages/orbis-remote-agent-protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ export type {
RemoteAgentV2Usage,
RemoteAgentV2UpdateInput,
RemoteAgentV2WorkspaceBrowseInput,
RemoteAgentV2WorkspaceCreateFolderInput,
RemoteAgentV2WorkspaceRegisterInput,
} from "./v2-types";
28 changes: 21 additions & 7 deletions packages/orbis-remote-agent-protocol/src/v2-connection.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { expect, test } from "vitest";

import {
agentTimestamp,
createAgentSessionRef,
type AgentJsonValue,
} from "@orbisapp/orbis-agent-backend";
import {
OrbisTransportError,
type JsonValue,
type TransportEvent,
} from "@orbisapp/transport";
import { OrbisTransportError, type JsonValue, type TransportEvent } from "@orbisapp/transport";
import { expect, test } from "vitest";

import { OrbisRemoteAgentV2Connection } from "./v2-connection";
import {
Expand Down Expand Up @@ -219,6 +214,25 @@ test("v2 connection enforces hello-first and delivers replayable events without
current: null,
entries: [{ ref: "folder-a" }],
});
transport.respond(ORBIS_REMOTE_AGENT_V2_METHODS.workspacesCreateFolder, {
displayName: "New Folder",
hidden: false,
ref: "folder-new",
selectable: true,
});
expect(
await connection.createWorkspaceFolder({
driverId: ref.driverId,
idempotencyKey: "workspace-create-folder-a",
name: "New Folder",
parentFolderRef: "folder-a",
}),
).toEqual({
displayName: "New Folder",
hidden: false,
ref: "folder-new",
selectable: true,
});
transport.respond(ORBIS_REMOTE_AGENT_V2_METHODS.workspacesRegister, {
created: true,
workspace: { displayName: "Projects", ref: "workspace-b" },
Expand Down
11 changes: 11 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type AgentModelMetadata,
type AgentSessionRef,
type AgentWorkspaceDescriptor,
type AgentWorkspaceFolderDescriptor,
type AgentWorkspaceFolderListing,
type AgentWorkspaceRegisterResult,
} from "@orbisapp/orbis-agent-backend";
Expand Down Expand Up @@ -71,6 +72,7 @@ import type {
RemoteAgentV2SessionSummary,
RemoteAgentV2UpdateInput,
RemoteAgentV2WorkspaceBrowseInput,
RemoteAgentV2WorkspaceCreateFolderInput,
RemoteAgentV2WorkspaceRegisterInput,
RemoteAgentV2Usage,
} from "./v2-types";
Expand Down Expand Up @@ -98,6 +100,9 @@ export type RemoteAgentV2SyncResult =

export interface RemoteAgentV2Connection {
browseWorkspaces(input: RemoteAgentV2WorkspaceBrowseInput): Promise<AgentWorkspaceFolderListing>;
createWorkspaceFolder(
input: RemoteAgentV2WorkspaceCreateFolderInput,
): Promise<AgentWorkspaceFolderDescriptor>;
cancel(input: RemoteAgentV2CancelInput): Promise<{ readonly cancelled: boolean }>;
close(): void;
createSession(input: RemoteAgentV2CreateInput): Promise<RemoteAgentV2SessionRecord>;
Expand Down Expand Up @@ -876,6 +881,12 @@ export class OrbisRemoteAgentV2Connection implements RemoteAgentV2Connection {
);
}

async createWorkspaceFolder(input: RemoteAgentV2WorkspaceCreateFolderInput) {
return this.request(ORBIS_REMOTE_AGENT_V2_METHODS.workspacesCreateFolder, input, (value) =>
parseSchema(v2WorkspaceFolderSchema, value, "Created workspace folder"),
);
}

async registerWorkspace(input: RemoteAgentV2WorkspaceRegisterInput) {
return this.request(ORBIS_REMOTE_AGENT_V2_METHODS.workspacesRegister, input, (value) => {
const output = record(value, "Workspace registration result");
Expand Down
5 changes: 5 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const ORBIS_REMOTE_AGENT_V2_METHODS = {
modelsList: "orbis.models.list",
workspacesList: "orbis.workspaces.list",
workspacesBrowse: "orbis.workspaces.browse",
workspacesCreateFolder: "orbis.workspaces.createFolder",
workspacesRegister: "orbis.workspaces.register",
sessionsList: "orbis.sessions.list",
sessionsCreate: "orbis.sessions.create",
Expand All @@ -26,6 +27,10 @@ export const ORBIS_REMOTE_AGENT_V2_METHOD_SCOPES = {
[ORBIS_REMOTE_AGENT_V2_METHODS.modelsList]: [ORBIS_REMOTE_SCOPES.agentRead],
[ORBIS_REMOTE_AGENT_V2_METHODS.workspacesList]: [ORBIS_REMOTE_SCOPES.agentRead],
[ORBIS_REMOTE_AGENT_V2_METHODS.workspacesBrowse]: [ORBIS_REMOTE_SCOPES.workspaceBrowse],
[ORBIS_REMOTE_AGENT_V2_METHODS.workspacesCreateFolder]: [
ORBIS_REMOTE_SCOPES.workspaceBrowse,
ORBIS_REMOTE_SCOPES.agentWrite,
],
[ORBIS_REMOTE_AGENT_V2_METHODS.workspacesRegister]: [
ORBIS_REMOTE_SCOPES.workspaceBrowse,
ORBIS_REMOTE_SCOPES.agentWrite,
Expand Down
35 changes: 28 additions & 7 deletions packages/orbis-remote-agent-protocol/src/v2-host.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { expect, test } from "vitest";

import {
agentBackendId,
agentDeliveryCursor,
Expand All @@ -13,11 +11,8 @@ import {
type AgentJsonValue,
type AgentSessionRef,
} from "@orbisapp/orbis-agent-backend";
import {
OrbisTransportError,
type JsonValue,
type TransportEvent,
} from "@orbisapp/transport";
import { OrbisTransportError, type JsonValue, type TransportEvent } from "@orbisapp/transport";
import { expect, test } from "vitest";

import {
OrbisRemoteAgentV2Host,
Expand Down Expand Up @@ -195,6 +190,12 @@ test("v2 host replays native entries from its cursor index without ACK state", a
title: null,
updatedAt: now,
}),
createWorkspaceFolder: async () => ({
displayName: "New Folder",
hidden: false,
ref: "folder-new",
selectable: true,
}),
hostId: agentBackendId("native"),
listDrivers: async () => [
createAgentDriverDescriptor({
Expand Down Expand Up @@ -298,6 +299,23 @@ test("v2 host replays native entries from its cursor index without ACK state", a
context(),
),
).toEqual({ breadcrumbs: [], current: null, entries: [], truncated: false });
expect(
await host.handleRequest(
ORBIS_REMOTE_AGENT_V2_METHODS.workspacesCreateFolder,
params({
driverId: "dsh",
idempotencyKey: "create-folder-1",
name: "New Folder",
parentFolderRef: "folder-a",
}),
context(),
),
).toEqual({
displayName: "New Folder",
hidden: false,
ref: "folder-new",
selectable: true,
});
expect(
await host.handleRequest(
ORBIS_REMOTE_AGENT_V2_METHODS.workspacesRegister,
Expand Down Expand Up @@ -596,6 +614,9 @@ test("v2 host announces catalog rows that move outside its own session runtimes"
createSession: async () => {
throw new Error("unused");
},
createWorkspaceFolder: async () => {
throw new Error("unused");
},
hostId: agentBackendId("native"),
listDrivers: async () => [],
listModels: async () => [],
Expand Down
37 changes: 37 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
v2ModelsListInputSchema,
v2WorkspacesListInputSchema,
v2WorkspacesBrowseInputSchema,
v2WorkspacesCreateFolderInputSchema,
v2WorkspacesRegisterInputSchema,
v2PromptInputSchema,
v2RefSchema,
Expand Down Expand Up @@ -789,6 +790,8 @@ export class OrbisRemoteAgentV2Host {
}
case ORBIS_REMOTE_AGENT_V2_METHODS.workspacesBrowse:
return toJsonResult(await this.browseWorkspaces(params, context));
case ORBIS_REMOTE_AGENT_V2_METHODS.workspacesCreateFolder:
return await this.createWorkspaceFolder(params, context);
case ORBIS_REMOTE_AGENT_V2_METHODS.workspacesRegister:
return await this.registerWorkspace(params, context);
case ORBIS_REMOTE_AGENT_V2_METHODS.sessionsList:
Expand Down Expand Up @@ -897,6 +900,40 @@ export class OrbisRemoteAgentV2Host {
return await this.backend.browseWorkspaceFolders(driverId, input.folderRef, context.signal);
}

private async createWorkspaceFolder(
params: JsonValue,
context: RemoteAgentHostRequestContext,
): Promise<JsonValue> {
const input = parseSchema(
v2WorkspacesCreateFolderInputSchema,
params,
"Workspace folder creation input",
);
const driverId = agentDriverId(input.driverId);
await this.assertDriverCapability(driverId, "workspace.open");
const key = this.idempotencyKey(
context.peer,
ORBIS_REMOTE_AGENT_V2_METHODS.workspacesCreateFolder,
input.idempotencyKey,
input.driverId,
);
const claim = await this.store.claimIdempotency(key);
if (claim.kind === "accepted") return toJsonResult(claim.result);
if (claim.kind === "pending") {
throw new AgentBackendError(
"unavailable",
"The prior workspace folder creation is still reconciling",
{ retryable: true },
);
}
this.assertRequestActive(context);
const result = toJsonResult(
await this.backend.createWorkspaceFolder(driverId, input.parentFolderRef, input.name),
);
await this.store.completeIdempotency(key, result as AgentJsonValue);
return result;
}

private async registerWorkspace(
params: JsonValue,
context: RemoteAgentHostRequestContext,
Expand Down
9 changes: 9 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,15 @@ export const v2WorkspacesBrowseInputSchema = z
.object({ driverId: nonEmptyString, folderRef: nonEmptyString.optional() })
.passthrough();

export const v2WorkspacesCreateFolderInputSchema = z
.object({
driverId: nonEmptyString,
idempotencyKey: nonEmptyString,
name: nonEmptyString,
parentFolderRef: nonEmptyString,
})
.passthrough();

export const v2WorkspacesRegisterInputSchema = z
.object({
driverId: nonEmptyString,
Expand Down
13 changes: 13 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
AgentModelSelection,
AgentSessionRef,
AgentWorkspaceDescriptor,
AgentWorkspaceFolderDescriptor,
AgentWorkspaceFolderListing,
AgentWorkspaceRegisterResult,
} from "@orbisapp/orbis-agent-backend";
Expand Down Expand Up @@ -227,6 +228,13 @@ export interface RemoteAgentV2WorkspaceRegisterInput {
readonly idempotencyKey: string;
}

export interface RemoteAgentV2WorkspaceCreateFolderInput {
readonly driverId: AgentSessionRef["driverId"];
readonly idempotencyKey: string;
readonly name: string;
readonly parentFolderRef: string;
}

export interface RemoteAgentV2PromptInput {
readonly ref: AgentSessionRef;
readonly content: readonly RemoteAgentV2ContentBlock[];
Expand Down Expand Up @@ -284,6 +292,11 @@ export interface RemoteAgentV2Backend {
folderRef?: string,
signal?: AbortSignal,
): Promise<AgentWorkspaceFolderListing>;
createWorkspaceFolder(
driverId: AgentSessionRef["driverId"],
parentFolderRef: string,
name: string,
): Promise<AgentWorkspaceFolderDescriptor>;
connectRuntime(ref: AgentSessionRef): Promise<RemoteAgentV2Runtime>;
createSession(
input: Omit<RemoteAgentV2CreateInput, "idempotencyKey">,
Expand Down
8 changes: 8 additions & 0 deletions packages/orbis-remote-dsh/src/adapter/dsh-local-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
type AgentModelSelection,
type AgentWorkspaceDescriptor,
type AgentWorkspaceBrowseInput,
type AgentWorkspaceCreateFolderInput,
type AgentWorkspaceFolderDescriptor,
type AgentWorkspaceFolderListing,
type AgentWorkspaceRegisterInput,
type AgentWorkspaceRegisterResult,
Expand Down Expand Up @@ -379,6 +381,12 @@ export class DshLocalBackend implements AgentBackend, DshLocalControllerHost {
throw new AgentBackendError("unsupported", "Local DSH folder browsing is host-owned");
}

async createWorkspaceFolder(
_input: AgentWorkspaceCreateFolderInput,
): Promise<AgentWorkspaceFolderDescriptor> {
throw new AgentBackendError("unsupported", "Local DSH folder creation is host-owned");
}

async registerWorkspace(
_input: AgentWorkspaceRegisterInput,
): Promise<AgentWorkspaceRegisterResult> {
Expand Down
Loading
Loading