diff --git a/.github/workflows/sdk-coverage.yml b/.github/workflows/sdk-coverage.yml index 25c8d592d..bdc1e1d7d 100644 --- a/.github/workflows/sdk-coverage.yml +++ b/.github/workflows/sdk-coverage.yml @@ -3,6 +3,7 @@ name: SDK Smoke Tests & Coverage Check on: pull_request: paths: + - 'src/sdk.ts' - 'src/sdk/**' - 'tests/smoketests/object-oriented/**' workflow_dispatch: diff --git a/src/sdk.ts b/src/sdk.ts index 9077ba382..857d19594 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -288,28 +288,28 @@ export class DevboxOps { * * @example * ```typescript - * const sdk = new RunloopSDK(); - * const blueprint = await sdk.blueprint.create({ + * const runloop = new RunloopSDK(); + * const blueprint = await runloop.blueprint.create({ * name: 'my-blueprint', * dockerfile: `FROM ubuntu:22.04 * RUN apt-get update`, * }); - * const devbox = await sdk.devbox.createFromBlueprintId(blueprint.id, { name: 'my-devbox' }); + * const devbox = await runloop.devbox.createFromBlueprintId(blueprint.id, { name: 'my-devbox' }); * ``` * * To use a local directory as a build context, use an object. * * @example * ```typescript - * const sdk = new RunloopSDK(); - * const obj = await sdk.storageObject.uploadFromDir( + * const runloop = new RunloopSDK(); + * const obj = await runloop.storageObject.uploadFromDir( * './', * { * name: 'build-context', * ttl_ms: 3600000, // 1 hour * } * ); - * const blueprint = await sdk.blueprint.create({ + * const blueprint = await runloop.blueprint.create({ * name: 'my-blueprint-with-context', * dockerfile: `FROM ubuntu:22.04 * COPY . .`, @@ -381,7 +381,7 @@ export class BlueprintOps { * * @example * ```typescript - * const sdk = new RunloopSDK(); + * const runloop = new RunloopSDK(); * const snapshot = await devbox.snapshotDisk({ name: 'backup' }); * ... * const devbox = await snapshot.createDevbox(); @@ -429,9 +429,9 @@ export class SnapshotOps { * * @example * ```typescript - * const sdk = new RunloopSDK(); - * const storageObject = await sdk.storageObject.uploadFromFile("./my-file.txt", "my-file.txt"); - * const objects = await sdk.storageObject.list(); + * const runloop = new RunloopSDK(); + * const storageObject = await runloop.storageObject.uploadFromFile("./my-file.txt", "my-file.txt"); + * const objects = await runloop.storageObject.list(); * ``` */ export class StorageObjectOps { @@ -644,8 +644,8 @@ export class StorageObjectOps { * * @example * ```typescript - * const sdk = new RunloopSDK(); - * const agent = await sdk.agent.create({ + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.create({ * name: 'my-npm-agent', * source: { * type: 'npm', @@ -667,6 +667,61 @@ export class AgentOps { /** * Create a new agent. * + * @example + * Create an agent from an NPM package: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.create({ + * name: 'my-agent', + * source: { + * type: 'npm', + * npm: { + * package_name: '@my-org/agent', + * npm_version: '1.0.0' + * } + * } + * }); + * console.log(`Created agent: ${agent.id}`); + * ``` + * + * @example + * Create an agent from a Git repository: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.create({ + * name: 'my-git-agent', + * source: { + * type: 'git', + * git: { + * repository: 'https://github.com/my-org/agent-repo', + * ref: 'main' + * } + * } + * }); + * ``` + * + * @example + * Create an agent from a storage object: + * ```typescript + * const runloop = new RunloopSDK(); + * // First, upload your agent code as a storage object + * const storageObject = await runloop.storageObject.uploadFromDir( + * './my-agent', + * { name: 'agent-package' } + * ); + * + * // Then create an agent from it + * const agent = await runloop.agent.create({ + * name: 'my-object-agent', + * source: { + * type: 'object', + * object: { + * object_id: storageObject.id + * } + * } + * }); + * ``` + * * @param {AgentCreateParams} params - Parameters for creating the agent. * @param {Core.RequestOptions} [options] - Request options. * @returns {Promise} An {@link Agent} instance. @@ -675,9 +730,324 @@ export class AgentOps { return Agent.create(this.client, params, options); } + /** + * Create an agent from an NPM package. + * + * @example + * Basic usage: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromNpm({ + * name: 'my-npm-agent', + * package_name: '@my-org/agent' + * }); + * ``` + * + * @example + * With version and setup commands: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromNpm({ + * name: 'my-npm-agent', + * package_name: '@my-org/agent', + * npm_version: '^1.2.0', + * agent_setup: ['npm run build', 'chmod +x ./run.sh'] + * }); + * ``` + * + * @example + * Using a private registry: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromNpm({ + * name: 'private-agent', + * package_name: '@my-org/private-agent', + * registry_url: 'https://npm.mycompany.com' + * }); + * ``` + * + * @param {object} params - Parameters for creating the agent. + * @param {string} params.package_name - NPM package name. + * @param {string} [params.npm_version] - NPM version constraint. + * @param {string} [params.registry_url] - NPM registry URL. + * @param {string[]} [params.agent_setup] - Setup commands to run after installation. + * @param {Core.RequestOptions} [options] - Request options. + * @returns {Promise} An {@link Agent} instance. + */ + async createFromNpm( + params: Omit & { + package_name: string; + npm_version?: string; + registry_url?: string; + agent_setup?: string[]; + }, + options?: Core.RequestOptions, + ): Promise { + const { package_name, npm_version, registry_url, agent_setup, ...restParams } = params; + + const npmConfig: any = { package_name }; + if (npm_version !== undefined) npmConfig.npm_version = npm_version; + if (registry_url !== undefined) npmConfig.registry_url = registry_url; + if (agent_setup !== undefined) npmConfig.agent_setup = agent_setup; + + return this.create( + { + ...restParams, + source: { type: 'npm', npm: npmConfig }, + }, + options, + ); + } + + /** + * Create an agent from a Pip package. + * + * @example + * Basic usage: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromPip({ + * name: 'my-python-agent', + * package_name: 'my-agent-package' + * }); + * ``` + * + * @example + * With version constraint: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromPip({ + * name: 'my-python-agent', + * package_name: 'my-agent-package', + * pip_version: '>=1.0.0,<2.0.0' + * }); + * ``` + * + * @example + * Using a private PyPI registry: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromPip({ + * name: 'private-python-agent', + * package_name: 'my-private-agent', + * registry_url: 'https://pypi.mycompany.com/simple', + * agent_setup: ['python setup.py install'] + * }); + * ``` + * + * @param {object} params - Parameters for creating the agent. + * @param {string} params.package_name - Pip package name. + * @param {string} [params.pip_version] - Pip version constraint. + * @param {string} [params.registry_url] - Pip registry URL. + * @param {string[]} [params.agent_setup] - Setup commands to run after installation. + * @param {Core.RequestOptions} [options] - Request options. + * @returns {Promise} An {@link Agent} instance. + */ + async createFromPip( + params: Omit & { + package_name: string; + pip_version?: string; + registry_url?: string; + agent_setup?: string[]; + }, + options?: Core.RequestOptions, + ): Promise { + const { package_name, pip_version, registry_url, agent_setup, ...restParams } = params; + + const pipConfig: any = { package_name }; + if (pip_version !== undefined) pipConfig.pip_version = pip_version; + if (registry_url !== undefined) pipConfig.registry_url = registry_url; + if (agent_setup !== undefined) pipConfig.agent_setup = agent_setup; + + return this.create( + { + ...restParams, + source: { type: 'pip', pip: pipConfig }, + }, + options, + ); + } + + /** + * Create an agent from a Git repository. + * + * @example + * Basic usage with public repository: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromGit({ + * name: 'my-git-agent', + * repository: 'https://github.com/my-org/agent-repo' + * }); + * ``` + * + * @example + * With specific branch and setup commands: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromGit({ + * name: 'my-git-agent', + * repository: 'https://github.com/my-org/agent-repo', + * ref: 'develop', + * agent_setup: [ + * 'npm install', + * 'npm run build' + * ] + * }); + * ``` + * + * @example + * Using a specific commit: + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = await runloop.agent.createFromGit({ + * name: 'my-git-agent', + * repository: 'https://github.com/my-org/agent-repo', + * ref: 'a1b2c3d4e5f6' + * }); + * ``` + * + * @param {object} params - Parameters for creating the agent. + * @param {string} params.repository - Git repository URL. + * @param {string} [params.ref] - Optional Git ref (branch/tag/commit), defaults to main/HEAD. + * @param {string[]} [params.agent_setup] - Setup commands to run after cloning. + * @param {Core.RequestOptions} [options] - Request options. + * @returns {Promise} An {@link Agent} instance. + */ + async createFromGit( + params: Omit & { + repository: string; + ref?: string; + agent_setup?: string[]; + }, + options?: Core.RequestOptions, + ): Promise { + const { repository, ref, agent_setup, ...restParams } = params; + + const gitConfig: any = { repository }; + if (ref !== undefined) gitConfig.ref = ref; + if (agent_setup !== undefined) gitConfig.agent_setup = agent_setup; + + return this.create( + { + ...restParams, + source: { type: 'git', git: gitConfig }, + }, + options, + ); + } + + /** + * Create an agent from a storage object. + * + * @example + * Upload agent code and create agent: + * ```typescript + * const runloop = new RunloopSDK(); + * + * // Upload agent directory as a storage object + * const storageObject = await runloop.storageObject.uploadFromDir( + * './my-agent-code', + * { name: 'agent-package' } + * ); + * + * // Create agent from the storage object + * const agent = await runloop.agent.createFromObject({ + * name: 'my-object-agent', + * object_id: storageObject.id + * }); + * ``` + * + * @example + * With setup commands: + * ```typescript + * const runloop = new RunloopSDK(); + * + * const storageObject = await runloop.storageObject.uploadFromDir( + * './my-agent-code', + * { name: 'agent-package' } + * ); + * + * const agent = await runloop.agent.createFromObject({ + * name: 'my-object-agent', + * object_id: storageObject.id, + * agent_setup: [ + * 'chmod +x setup.sh', + * './setup.sh', + * 'pip install -r requirements.txt' + * ] + * }); + * ``` + * + * @example + * Complete workflow: storage object → agent → devbox: + * ```typescript + * const runloop = new RunloopSDK(); + * + * // 1. Upload agent code + * const storageObject = await runloop.storageObject.uploadFromDir( + * './my-agent', + * { name: 'agent-v1' } + * ); + * + * // 2. Create agent from storage object + * const agent = await runloop.agent.createFromObject({ + * name: 'my-agent', + * object_id: storageObject.id + * }); + * + * // 3. Create devbox with agent mounted + * const devbox = await runloop.devbox.create({ + * name: 'devbox-with-agent', + * mounts: [{ + * type: 'agent_mount', + * agent_id: agent.id, + * agent_name: null, + * agent_path: '/home/user/agent' + * }] + * }); + * ``` + * + * @param {object} params - Parameters for creating the agent. + * @param {string} params.object_id - Storage object ID. + * @param {string[]} [params.agent_setup] - Setup commands to run after unpacking. + * @param {Core.RequestOptions} [options] - Request options. + * @returns {Promise} An {@link Agent} instance. + */ + async createFromObject( + params: Omit & { + object_id: string; + agent_setup?: string[]; + }, + options?: Core.RequestOptions, + ): Promise { + const { object_id, agent_setup, ...restParams } = params; + + const objectConfig: any = { object_id }; + if (agent_setup !== undefined) objectConfig.agent_setup = agent_setup; + + return this.create( + { + ...restParams, + source: { type: 'object', object: objectConfig }, + }, + options, + ); + } + /** * Get an agent object by its ID. * + * @example + * ```typescript + * const runloop = new RunloopSDK(); + * const agent = runloop.agent.fromId('agt_1234567890'); + * + * // Get agent information + * const info = await agent.getInfo(); + * console.log(`Agent name: ${info.name}`); + * ``` + * * @param {string} id - The ID of the agent. * @returns {Agent} An {@link Agent} instance. */ @@ -688,6 +1058,28 @@ export class AgentOps { /** * List all agents with optional filters. * + * @example + * List all agents: + * ```typescript + * const runloop = new RunloopSDK(); + * const agents = await runloop.agent.list(); + * + * for (const agent of agents) { + * const info = await agent.getInfo(); + * console.log(`${info.name}: ${info.source?.type}`); + * } + * ``` + * + * @example + * List with filters: + * ```typescript + * const runloop = new RunloopSDK(); + * const agents = await runloop.agent.list({ + * name: 'my-agent', + * limit: 10 + * }); + * ``` + * * @param {AgentListParams} [params] - Optional filter parameters. * @param {Core.RequestOptions} [options] - Request options. * @returns {Promise} An array of {@link Agent} instances. @@ -703,8 +1095,8 @@ export class AgentOps { * @example * ```typescript * import { RunloopSDK } from '@runloop/api-client'; - * const sdk = new RunloopSDK(); - * const devbox = await sdk.devbox.create({ name: 'my-devbox' }); + * const runloop = new RunloopSDK(); + * const devbox = await runloop.devbox.create({ name: 'my-devbox' }); * ``` */ export default Runloop; diff --git a/tests/sdk/agent-ops.test.ts b/tests/sdk/agent-ops.test.ts new file mode 100644 index 000000000..c8bbc1653 --- /dev/null +++ b/tests/sdk/agent-ops.test.ts @@ -0,0 +1,238 @@ +import { AgentOps } from '../../src/sdk'; +import { Agent } from '../../src/sdk/agent'; +import type { AgentView } from '../../src/resources/agents'; + +// Mock the Agent class +jest.mock('../../src/sdk/agent'); + +describe('AgentOps', () => { + let mockClient: any; + let agentOps: AgentOps; + let mockAgentData: AgentView; + + beforeEach(() => { + // Create mock client + jest.clearAllMocks(); + mockClient = { + agents: { + create: jest.fn(), + retrieve: jest.fn(), + list: jest.fn(), + }, + } as any; + + agentOps = new AgentOps(mockClient); + + // Mock agent data + mockAgentData = { + id: 'agent-123', + create_time_ms: Date.now(), + name: 'test-agent', + is_public: false, + source: { + type: 'npm', + npm: { + package_name: '@runloop/example-agent', + }, + }, + }; + + // Mock Agent.create to return a mock Agent instance + const mockAgentInstance = { id: 'agent-123', getInfo: jest.fn() } as unknown as Agent; + jest.spyOn(Agent as any, 'create').mockResolvedValue(mockAgentInstance); + }); + + describe('createFromNpm', () => { + it('should create an agent from npm package', async () => { + await agentOps.createFromNpm({ + name: 'test-agent', + package_name: '@runloop/example-agent', + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'npm', + npm: { + package_name: '@runloop/example-agent', + }, + }, + }, + undefined, + ); + }); + + it('should create an agent with all npm options', async () => { + await agentOps.createFromNpm({ + name: 'test-agent', + package_name: '@runloop/example-agent', + npm_version: '1.2.3', + registry_url: 'https://registry.example.com', + agent_setup: ['npm install', 'npm run setup'], + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'npm', + npm: { + package_name: '@runloop/example-agent', + npm_version: '1.2.3', + registry_url: 'https://registry.example.com', + agent_setup: ['npm install', 'npm run setup'], + }, + }, + }, + undefined, + ); + }); + }); + + describe('createFromPip', () => { + it('should create an agent from pip package', async () => { + await agentOps.createFromPip({ + name: 'test-agent', + package_name: 'runloop-example-agent', + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'pip', + pip: { + package_name: 'runloop-example-agent', + }, + }, + }, + undefined, + ); + }); + + it('should create an agent with all pip options', async () => { + await agentOps.createFromPip({ + name: 'test-agent', + package_name: 'runloop-example-agent', + pip_version: '1.2.3', + registry_url: 'https://pypi.example.com', + agent_setup: ['pip install extra-deps'], + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'pip', + pip: { + package_name: 'runloop-example-agent', + pip_version: '1.2.3', + registry_url: 'https://pypi.example.com', + agent_setup: ['pip install extra-deps'], + }, + }, + }, + undefined, + ); + }); + }); + + describe('createFromGit', () => { + it('should create an agent from git repository', async () => { + await agentOps.createFromGit({ + name: 'test-agent', + repository: 'https://github.com/example/agent-repo', + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'git', + git: { + repository: 'https://github.com/example/agent-repo', + }, + }, + }, + undefined, + ); + }); + + it('should create an agent with all git options', async () => { + await agentOps.createFromGit({ + name: 'test-agent', + repository: 'https://github.com/example/agent-repo', + ref: 'develop', + agent_setup: ['npm install', 'npm run build'], + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'git', + git: { + repository: 'https://github.com/example/agent-repo', + ref: 'develop', + agent_setup: ['npm install', 'npm run build'], + }, + }, + }, + undefined, + ); + }); + }); + + describe('createFromObject', () => { + it('should create an agent from object', async () => { + await agentOps.createFromObject({ + name: 'test-agent', + object_id: 'obj_123', + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'object', + object: { + object_id: 'obj_123', + }, + }, + }, + undefined, + ); + }); + + it('should create an agent with agent_setup', async () => { + await agentOps.createFromObject({ + name: 'test-agent', + object_id: 'obj_123', + agent_setup: ['chmod +x setup.sh', './setup.sh'], + }); + + expect(Agent.create).toHaveBeenCalledWith( + mockClient, + { + name: 'test-agent', + source: { + type: 'object', + object: { + object_id: 'obj_123', + agent_setup: ['chmod +x setup.sh', './setup.sh'], + }, + }, + }, + undefined, + ); + }); + }); +}); diff --git a/tests/smoketests/object-oriented/agent.test.ts b/tests/smoketests/object-oriented/agent.test.ts index a91a7cca9..6ff8bd107 100644 --- a/tests/smoketests/object-oriented/agent.test.ts +++ b/tests/smoketests/object-oriented/agent.test.ts @@ -1,7 +1,7 @@ -import { Agent } from '@runloop/api-client/sdk'; +import { Agent, Devbox, StorageObject } from '@runloop/api-client/sdk'; import { makeClientSDK, THIRTY_SECOND_TIMEOUT, uniqueName } from '../utils'; -const sdk = makeClientSDK(); +const runloop = makeClientSDK(); describe('smoketest: object-oriented agent', () => { describe('agent lifecycle', () => { @@ -9,7 +9,7 @@ describe('smoketest: object-oriented agent', () => { 'create agent basic', async () => { const name = uniqueName('sdk-agent-test-basic'); - const agent = await sdk.agent.create({ + const agent = await runloop.agent.create({ name: name, source: { type: 'npm', @@ -40,7 +40,7 @@ describe('smoketest: object-oriented agent', () => { 'get agent info', async () => { const name = uniqueName('sdk-agent-test-info'); - const agent = await sdk.agent.create({ + const agent = await runloop.agent.create({ name: name, source: { type: 'npm', @@ -69,7 +69,7 @@ describe('smoketest: object-oriented agent', () => { test( 'list agents', async () => { - const agents = await sdk.agent.list({ limit: 10 }); + const agents = await runloop.agent.list({ limit: 10 }); expect(Array.isArray(agents)).toBe(true); // List might be empty, that's okay @@ -82,7 +82,7 @@ describe('smoketest: object-oriented agent', () => { 'get agent by ID', async () => { // Create an agent - const created = await sdk.agent.create({ + const created = await runloop.agent.create({ name: uniqueName('sdk-agent-test-retrieve'), source: { type: 'npm', @@ -94,7 +94,7 @@ describe('smoketest: object-oriented agent', () => { try { // Retrieve it by ID - const retrieved = sdk.agent.fromId(created.id); + const retrieved = runloop.agent.fromId(created.id); expect(retrieved.id).toBe(created.id); // Verify it's the same agent @@ -118,13 +118,13 @@ describe('smoketest: object-oriented agent', () => { }; // Create multiple agents - const agent1 = await sdk.agent.create({ name: uniqueName('sdk-agent-test-list-1'), source: sourceConfig }); - const agent2 = await sdk.agent.create({ name: uniqueName('sdk-agent-test-list-2'), source: sourceConfig }); - const agent3 = await sdk.agent.create({ name: uniqueName('sdk-agent-test-list-3'), source: sourceConfig }); + const agent1 = await runloop.agent.create({ name: uniqueName('sdk-agent-test-list-1'), source: sourceConfig }); + const agent2 = await runloop.agent.create({ name: uniqueName('sdk-agent-test-list-2'), source: sourceConfig }); + const agent3 = await runloop.agent.create({ name: uniqueName('sdk-agent-test-list-3'), source: sourceConfig }); try { // List agents - const agents = await sdk.agent.list({ limit: 100 }); + const agents = await runloop.agent.list({ limit: 100 }); expect(Array.isArray(agents)).toBe(true); expect(agents.length).toBeGreaterThanOrEqual(3); @@ -149,7 +149,7 @@ describe('smoketest: object-oriented agent', () => { async () => { const name = uniqueName('sdk-agent-test-npm'); - const agent = await sdk.agent.create({ + const agent = await runloop.agent.create({ name: name, source: { type: 'npm', @@ -177,7 +177,7 @@ describe('smoketest: object-oriented agent', () => { async () => { const name = uniqueName('sdk-agent-test-git'); - const agent = await sdk.agent.create({ + const agent = await runloop.agent.create({ name: name, source: { type: 'git', @@ -201,4 +201,188 @@ describe('smoketest: object-oriented agent', () => { THIRTY_SECOND_TIMEOUT, ); }); + + describe('agent convenience methods', () => { + test( + 'createFromNpm', + async () => { + const name = uniqueName('sdk-agent-from-npm'); + const agent = await runloop.agent.createFromNpm({ + name: name, + package_name: '@runloop/hello-world-agent', + }); + + try { + expect(agent.id).toBeTruthy(); + const info = await agent.getInfo(); + expect(info.name).toBe(name); + expect(info.source?.type).toBe('npm'); + expect(info.source?.npm?.package_name).toBe('@runloop/hello-world-agent'); + } finally { + // TODO: Add agent cleanup once delete endpoint is implemented + } + }, + THIRTY_SECOND_TIMEOUT, + ); + + test( + 'createFromPip', + async () => { + const name = uniqueName('sdk-agent-from-pip'); + const agent = await runloop.agent.createFromPip({ + name: name, + package_name: 'runloop-example-agent', + }); + + try { + expect(agent.id).toBeTruthy(); + const info = await agent.getInfo(); + expect(info.name).toBe(name); + expect(info.source?.type).toBe('pip'); + expect(info.source?.pip?.package_name).toBe('runloop-example-agent'); + } finally { + // TODO: Add agent cleanup once delete endpoint is implemented + } + }, + THIRTY_SECOND_TIMEOUT, + ); + + test( + 'createFromGit', + async () => { + const name = uniqueName('sdk-agent-from-git'); + const agent = await runloop.agent.createFromGit({ + name: name, + repository: 'https://github.com/runloop/example-agent', + ref: 'main', + }); + + try { + expect(agent.id).toBeTruthy(); + const info = await agent.getInfo(); + expect(info.name).toBe(name); + expect(info.source?.type).toBe('git'); + expect(info.source?.git?.repository).toBe('https://github.com/runloop/example-agent'); + } finally { + // TODO: Add agent cleanup once delete endpoint is implemented + } + }, + THIRTY_SECOND_TIMEOUT, + ); + + test( + 'createFromObject with storage object', + async () => { + let storageObject: StorageObject | undefined; + let agent: Agent | undefined; + + try { + // Create storage object with agent content + storageObject = await runloop.storageObject.create({ + name: uniqueName('sdk-agent-storage-object'), + content_type: 'text', + metadata: { test: 'agent-smoketest' }, + }); + + await storageObject.uploadContent('Agent content for testing'); + await storageObject.complete(); + + // Create agent from storage object + const agentName = uniqueName('sdk-agent-from-object'); + agent = await runloop.agent.createFromObject({ + name: agentName, + object_id: storageObject.id, + }); + + expect(agent.id).toBeTruthy(); + const info = await agent.getInfo(); + expect(info.name).toBe(agentName); + expect(info.source?.type).toBe('object'); + expect(info.source?.object?.object_id).toBe(storageObject.id); + } finally { + if (storageObject) { + await storageObject.delete(); + } + // TODO: Add agent cleanup once delete endpoint is implemented + } + }, + THIRTY_SECOND_TIMEOUT, + ); + }); + + describe('agent with devbox mounting', () => { + test( + 'mount agent from storage object into devbox', + async () => { + let storageObject: StorageObject | undefined; + let agent: Agent | undefined; + let devbox: Devbox | undefined; + + try { + // Create storage object with agent content + storageObject = await runloop.storageObject.create({ + name: uniqueName('sdk-agent-mount-storage'), + content_type: 'text', + metadata: { test: 'agent-mount-smoketest' }, + }); + + await storageObject.uploadContent('Agent content for devbox mounting'); + await storageObject.complete(); + + // Create agent from storage object + agent = await runloop.agent.createFromObject({ + name: uniqueName('sdk-agent-for-mount'), + object_id: storageObject.id, + }); + + // Create devbox with agent mount + devbox = await runloop.devbox.create({ + name: uniqueName('sdk-devbox-with-agent'), + launch_parameters: { + resource_size_request: 'X_SMALL', + keep_alive_time_seconds: 60 * 5, // 5 minutes + }, + mounts: [ + { + type: 'agent_mount', + agent_id: agent.id, + agent_name: null, + agent_path: '/home/user/test-agent', + }, + ], + }); + + expect(devbox.id).toBeTruthy(); + + // Wait for devbox to be running + const info = await devbox.getInfo(); + if (info.status !== 'running') { + // Poll until running (simplified approach) + let attempts = 0; + while (attempts < 60) { + const currentInfo = await devbox.getInfo(); + if (currentInfo.status === 'running') { + break; + } + await new Promise((resolve) => setTimeout(resolve, 5000)); + attempts++; + } + } + + // Verify the agent was mounted + const result = await devbox.cmd.exec('ls -la /home/user/test-agent'); + expect(result.exitCode).toBe(0); + } finally { + if (devbox) { + await devbox.shutdown(); + } + if (storageObject) { + await storageObject.delete(); + } + // TODO: Add agent cleanup once delete endpoint is implemented + } + }, + THIRTY_SECOND_TIMEOUT * 8, + ); + }); });