From 4d1b2c359817dd66ee92c01139c64d3a2d8e5aae Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 16:19:10 -0700 Subject: [PATCH 1/5] cp dines --- src/sdk.ts | 117 ++++++++ src/sdk/axon.ts | 151 +++++++++++ src/sdk/index.ts | 1 + src/types.ts | 6 + tests/objects/axon.test.ts | 254 ++++++++++++++++++ tests/sdk/axon-ops.test.ts | 116 ++++++++ tests/smoketests/object-oriented/axon.test.ts | 84 ++++++ tests/smoketests/object-oriented/sdk.test.ts | 1 + 8 files changed, 730 insertions(+) create mode 100644 src/sdk/axon.ts create mode 100644 tests/objects/axon.test.ts create mode 100644 tests/sdk/axon-ops.test.ts create mode 100644 tests/smoketests/object-oriented/axon.test.ts diff --git a/src/sdk.ts b/src/sdk.ts index bc5f9e334..182f4e658 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -6,6 +6,7 @@ import { Blueprint, type CreateParams as BlueprintCreateParams } from './sdk/blu import { Snapshot } from './sdk/snapshot'; import { StorageObject } from './sdk/storage-object'; import { Agent } from './sdk/agent'; +import { Axon } from './sdk/axon'; import { Scorer } from './sdk/scorer'; import { NetworkPolicy } from './sdk/network-policy'; import { GatewayConfig } from './sdk/gateway-config'; @@ -23,6 +24,7 @@ import type { import type { BlueprintListParams } from './resources/blueprints'; import type { ObjectCreateParams, ObjectListParams } from './resources/objects'; import type { AgentCreateParams, AgentListParams } from './resources/agents'; +import type { AxonCreateParams } from './resources/axons'; import type { ScorerCreateParams, ScorerListParams } from './resources/scenarios/scorers'; import type { NetworkPolicyCreateParams, NetworkPolicyListParams } from './resources/network-policies'; import type { GatewayConfigCreateParams, GatewayConfigListParams } from './resources/gateway-configs'; @@ -369,6 +371,7 @@ type ContentType = ObjectCreateParams['content_type']; * - `snapshot` - {@link SnapshotOps} * - `storageObject` - {@link StorageObjectOps} * - `agent` - {@link AgentOps} + * - `axon` - {@link AxonOps} * - `scorer` - {@link ScorerOps} * - `networkPolicy` - {@link NetworkPolicyOps} * - `gatewayConfig` - {@link GatewayConfigOps} @@ -433,6 +436,15 @@ export class RunloopSDK { */ public readonly agent: AgentOps; + /** + * **Axon Operations** - {@link AxonOps} for creating and accessing {@link Axon} class instances. + * + * [Beta] Axons are event communication channels that support publishing events and subscribing + * to event streams via server-sent events (SSE). Use these operations to create new axons, + * get existing ones by ID, or list all active axons. + */ + public readonly axon: AxonOps; + /** * **Scorer Operations** - {@link ScorerOps} for creating and accessing {@link Scorer} class instances. * @@ -494,6 +506,7 @@ export class RunloopSDK { this.snapshot = new SnapshotOps(this.api); this.storageObject = new StorageObjectOps(this.api); this.agent = new AgentOps(this.api); + this.axon = new AxonOps(this.api); this.scorer = new ScorerOps(this.api); this.networkPolicy = new NetworkPolicyOps(this.api); this.gatewayConfig = new GatewayConfigOps(this.api); @@ -1484,6 +1497,107 @@ export class AgentOps { } } +/** + * [Beta] Axon SDK interface for managing axons. + * + * @category Axon + * + * @remarks + * ## Overview + * + * The `AxonOps` class provides a high-level abstraction for managing axons, + * which are event communication channels. Axons support publishing events + * and subscribing to event streams via server-sent events (SSE). + * + * ## Usage + * + * This interface is accessed via {@link RunloopSDK.axon}. You should construct + * a {@link RunloopSDK} instance and use it from there: + * + * @example + * ```typescript + * const runloop = new RunloopSDK(); + * const axon = await runloop.axon.create({ name: 'my-axon' }); + * + * // Publish an event + * await axon.publish({ + * event_type: 'task_complete', + * origin: 'AGENT_EVENT', + * payload: JSON.stringify({ result: 'success' }), + * source: 'my-agent', + * }); + * + * // Subscribe to events + * const stream = await axon.subscribeSse(); + * for await (const event of stream) { + * console.log(event.event_type, event.payload); + * } + * ``` + */ +export class AxonOps { + /** + * @private + */ + constructor(private client: RunloopAPI) {} + + /** + * [Beta] Create a new axon. + * + * @example + * ```typescript + * const runloop = new RunloopSDK(); + * const axon = await runloop.axon.create({ name: 'my-axon' }); + * console.log(`Created axon: ${axon.id}`); + * ``` + * + * @param {AxonCreateParams} [params] - Parameters for creating the axon. + * @param {Core.RequestOptions} [options] - Request options. + * @returns {Promise} An {@link Axon} instance. + */ + async create(params?: AxonCreateParams, options?: Core.RequestOptions): Promise { + return Axon.create(this.client, params, options); + } + + /** + * Get an axon object by its ID. + * + * @example + * ```typescript + * const runloop = new RunloopSDK(); + * const axon = runloop.axon.fromId('axn_1234567890'); + * const info = await axon.getInfo(); + * console.log(`Axon name: ${info.name}`); + * ``` + * + * @param {string} id - The ID of the axon. + * @returns {Axon} An {@link Axon} instance. + */ + fromId(id: string): Axon { + return Axon.fromId(this.client, id); + } + + /** + * [Beta] List all active axons. + * + * @example + * ```typescript + * const runloop = new RunloopSDK(); + * const axons = await runloop.axon.list(); + * for (const axon of axons) { + * const info = await axon.getInfo(); + * console.log(`${info.name}: ${info.id}`); + * } + * ``` + * + * @param {Core.RequestOptions} [options] - Request options. + * @returns {Promise} An array of {@link Axon} instances. + */ + async list(options?: Core.RequestOptions): Promise { + const result = await this.client.axons.list(options); + return result.axons.map((axon) => Axon.fromId(this.client, axon.id)); + } +} + /** * Scorer SDK interface for managing custom scorers. * @@ -2199,6 +2313,7 @@ export declare namespace RunloopSDK { SnapshotOps as SnapshotOps, StorageObjectOps as StorageObjectOps, AgentOps as AgentOps, + AxonOps as AxonOps, ScorerOps as ScorerOps, NetworkPolicyOps as NetworkPolicyOps, GatewayConfigOps as GatewayConfigOps, @@ -2210,6 +2325,7 @@ export declare namespace RunloopSDK { Snapshot as Snapshot, StorageObject as StorageObject, Agent as Agent, + Axon as Axon, Scorer as Scorer, NetworkPolicy as NetworkPolicy, GatewayConfig as GatewayConfig, @@ -2229,6 +2345,7 @@ export { Snapshot, StorageObject, Agent, + Axon, Scorer, NetworkPolicy, McpConfig, diff --git a/src/sdk/axon.ts b/src/sdk/axon.ts new file mode 100644 index 000000000..1eb28bf98 --- /dev/null +++ b/src/sdk/axon.ts @@ -0,0 +1,151 @@ +import { Runloop } from '../index'; +import type * as Core from '../core'; +import { APIPromise } from '../core'; +import { Stream } from '../streaming'; +import type { + AxonView, + AxonCreateParams, + AxonPublishParams, + PublishResultView, + AxonEventView, +} from '../resources/axons'; + +/** + * [Beta] Object-oriented interface for working with Axons. + * + * @category Axon + * + * @remarks + * ## Overview + * + * The `Axon` class provides a high-level, object-oriented API for managing axons. + * Axons are event communication channels that support publishing events and subscribing + * to event streams via server-sent events (SSE). + * + * ## Quickstart + * + * ```typescript + * import { RunloopSDK } from '@runloop/api-client'; + * + * const runloop = new RunloopSDK(); + * const axon = await runloop.axon.create({ name: 'my-axon' }); + * + * // Publish an event + * await axon.publish({ + * event_type: 'task_complete', + * origin: 'AGENT_EVENT', + * payload: JSON.stringify({ result: 'success' }), + * source: 'my-agent', + * }); + * + * // Subscribe to events + * const stream = await axon.subscribeSse(); + * for await (const event of stream) { + * console.log(event.event_type, event.payload); + * } + * ``` + */ +export class Axon { + private client: Runloop; + private _id: string; + + private constructor(client: Runloop, id: string) { + this.client = client; + this._id = id; + } + + /** + * [Beta] Create a new Axon. + * + * See the {@link AxonOps.create} method for calling this + * @private + * + * @param {Runloop} client - The Runloop client instance + * @param {AxonCreateParams} [params] - Parameters for creating the axon + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise} An {@link Axon} instance + */ + static async create(client: Runloop, params?: AxonCreateParams, options?: Core.RequestOptions): Promise { + const axonData = await client.axons.create(params ?? {}, options); + return new Axon(client, axonData.id); + } + + /** + * Create an Axon instance by ID without retrieving from API. + * Use getInfo() to fetch the actual data when needed. + * + * See the {@link AxonOps.fromId} method for calling this + * @private + * + * @param {Runloop} client - The Runloop client instance + * @param {string} id - The axon ID + * @returns {Axon} An {@link Axon} instance + */ + static fromId(client: Runloop, id: string): Axon { + return new Axon(client, id); + } + + /** + * Get the axon ID. + * @returns {string} The axon ID + */ + get id(): string { + return this._id; + } + + /** + * [Beta] Get the complete axon data from the API. + * + * @example + * ```typescript + * const info = await axon.getInfo(); + * console.log(`Axon: ${info.name}, created: ${info.created_at_ms}`); + * ``` + * + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise} The axon data + */ + async getInfo(options?: Core.RequestOptions): Promise { + return this.client.axons.retrieve(this._id, options); + } + + /** + * [Beta] Publish an event to this axon. + * + * @example + * ```typescript + * const result = await axon.publish({ + * event_type: 'push', + * origin: 'EXTERNAL_EVENT', + * payload: JSON.stringify({ repo: 'my-repo' }), + * source: 'github', + * }); + * console.log(`Published with sequence: ${result.sequence}`); + * ``` + * + * @param {AxonPublishParams} params - Parameters for the event to publish + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise} The publish result with sequence number and timestamp + */ + async publish(params: AxonPublishParams, options?: Core.RequestOptions): Promise { + return this.client.axons.publish(this._id, params, options); + } + + /** + * [Beta] Subscribe to this axon's event stream via server-sent events. + * + * @example + * ```typescript + * const stream = await axon.subscribeSse(); + * for await (const event of stream) { + * console.log(`[${event.source}] ${event.event_type}: ${event.payload}`); + * } + * ``` + * + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise>} An async iterable stream of axon events + */ + async subscribeSse(options?: Core.RequestOptions): Promise> { + return this.client.axons.subscribeSse(this._id, options); + } +} diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 8950b855d..0c508139b 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -3,6 +3,7 @@ export { Blueprint } from './blueprint'; export { Snapshot } from './snapshot'; export { StorageObject } from './storage-object'; export { Agent } from './agent'; +export { Axon } from './axon'; export { Execution } from './execution'; export { ExecutionResult } from './execution-result'; export { Scorer } from './scorer'; diff --git a/src/types.ts b/src/types.ts index 328c69d65..5bafdd396 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,12 @@ export type * from './resources/blueprints'; export type * from './resources/agents'; +// ============================================================================= +// Axon Types +// ============================================================================= + +export type * from './resources/axons'; + // ============================================================================= // Storage Object Types // ============================================================================= diff --git a/tests/objects/axon.test.ts b/tests/objects/axon.test.ts new file mode 100644 index 000000000..feaf51e78 --- /dev/null +++ b/tests/objects/axon.test.ts @@ -0,0 +1,254 @@ +import { Axon } from '../../src/sdk/axon'; +import type { AxonView, PublishResultView } from '../../src/resources/axons'; + +jest.mock('../../src/index'); + +describe('Axon', () => { + let mockClient: any; + let mockAxonData: AxonView; + + beforeEach(() => { + mockClient = { + axons: { + create: jest.fn(), + retrieve: jest.fn(), + list: jest.fn(), + publish: jest.fn(), + subscribeSse: jest.fn(), + }, + } as any; + + mockAxonData = { + id: 'axn_123456789', + created_at_ms: Date.now(), + name: 'test-axon', + }; + }); + + describe('create', () => { + it('should create an axon and return an Axon instance', async () => { + mockClient.axons.create.mockResolvedValue(mockAxonData); + + const axon = await Axon.create(mockClient, { name: 'test-axon' }); + + expect(mockClient.axons.create).toHaveBeenCalledWith({ name: 'test-axon' }, undefined); + expect(axon).toBeInstanceOf(Axon); + expect(axon.id).toBe('axn_123456789'); + }); + + it('should create an axon without params', async () => { + mockClient.axons.create.mockResolvedValue(mockAxonData); + + const axon = await Axon.create(mockClient); + + expect(mockClient.axons.create).toHaveBeenCalledWith({}, undefined); + expect(axon).toBeInstanceOf(Axon); + expect(axon.id).toBe('axn_123456789'); + }); + + it('should pass request options to the API client', async () => { + mockClient.axons.create.mockResolvedValue(mockAxonData); + + await Axon.create(mockClient, { name: 'test-axon' }, { timeout: 5000 }); + + expect(mockClient.axons.create).toHaveBeenCalledWith({ name: 'test-axon' }, { timeout: 5000 }); + }); + }); + + describe('fromId', () => { + it('should create an Axon instance by ID without API call', () => { + const axon = Axon.fromId(mockClient, 'axn_123456789'); + + expect(axon).toBeInstanceOf(Axon); + expect(axon.id).toBe('axn_123456789'); + expect(mockClient.axons.retrieve).not.toHaveBeenCalled(); + }); + + it('should work with any valid axon ID format', () => { + const axon = Axon.fromId(mockClient, 'axn_abcdefghij'); + + expect(axon.id).toBe('axn_abcdefghij'); + }); + }); + + describe('instance methods', () => { + let axon: Axon; + + beforeEach(async () => { + mockClient.axons.create.mockResolvedValue(mockAxonData); + axon = await Axon.create(mockClient, { name: 'test-axon' }); + }); + + describe('getInfo', () => { + it('should get axon information from API', async () => { + mockClient.axons.retrieve.mockResolvedValue(mockAxonData); + + const info = await axon.getInfo(); + + expect(mockClient.axons.retrieve).toHaveBeenCalledWith('axn_123456789', undefined); + expect(info.id).toBe('axn_123456789'); + expect(info.name).toBe('test-axon'); + }); + + it('should pass request options to retrieve call', async () => { + mockClient.axons.retrieve.mockResolvedValue(mockAxonData); + + await axon.getInfo({ timeout: 3000 }); + + expect(mockClient.axons.retrieve).toHaveBeenCalledWith('axn_123456789', { timeout: 3000 }); + }); + + it('should return updated data on subsequent calls', async () => { + const updatedData = { ...mockAxonData, name: 'updated-name' }; + mockClient.axons.retrieve.mockResolvedValue(updatedData); + + const info = await axon.getInfo(); + + expect(info.name).toBe('updated-name'); + }); + }); + + describe('publish', () => { + const publishParams = { + event_type: 'push', + origin: 'EXTERNAL_EVENT' as const, + payload: JSON.stringify({ repo: 'my-repo' }), + source: 'github', + }; + + const mockPublishResult: PublishResultView = { + sequence: 1, + timestamp_ms: Date.now(), + }; + + it('should publish an event to the axon', async () => { + mockClient.axons.publish.mockResolvedValue(mockPublishResult); + + const result = await axon.publish(publishParams); + + expect(mockClient.axons.publish).toHaveBeenCalledWith('axn_123456789', publishParams, undefined); + expect(result.sequence).toBe(1); + expect(result.timestamp_ms).toBeDefined(); + }); + + it('should pass request options to publish call', async () => { + mockClient.axons.publish.mockResolvedValue(mockPublishResult); + + await axon.publish(publishParams, { timeout: 5000 }); + + expect(mockClient.axons.publish).toHaveBeenCalledWith('axn_123456789', publishParams, { + timeout: 5000, + }); + }); + + it('should handle different event origins', async () => { + mockClient.axons.publish.mockResolvedValue(mockPublishResult); + + for (const origin of ['EXTERNAL_EVENT', 'AGENT_EVENT', 'USER_EVENT'] as const) { + await axon.publish({ ...publishParams, origin }); + expect(mockClient.axons.publish).toHaveBeenCalledWith( + 'axn_123456789', + { ...publishParams, origin }, + undefined, + ); + } + }); + }); + + describe('subscribeSse', () => { + it('should subscribe to axon event stream', async () => { + const mockStream = { [Symbol.asyncIterator]: jest.fn() }; + mockClient.axons.subscribeSse.mockResolvedValue(mockStream); + + const stream = await axon.subscribeSse(); + + expect(mockClient.axons.subscribeSse).toHaveBeenCalledWith('axn_123456789', undefined); + expect(stream).toBe(mockStream); + }); + + it('should pass request options to subscribeSse call', async () => { + const mockStream = { [Symbol.asyncIterator]: jest.fn() }; + mockClient.axons.subscribeSse.mockResolvedValue(mockStream); + + await axon.subscribeSse({ timeout: 60000 }); + + expect(mockClient.axons.subscribeSse).toHaveBeenCalledWith('axn_123456789', { timeout: 60000 }); + }); + }); + + describe('id property', () => { + it('should expose axon ID', () => { + expect(axon.id).toBe('axn_123456789'); + }); + + it('should be read-only', () => { + const originalId = axon.id; + expect(axon.id).toBe(originalId); + }); + }); + }); + + describe('error handling', () => { + it('should handle axon creation failure', async () => { + const error = new Error('Creation failed'); + mockClient.axons.create.mockRejectedValue(error); + + await expect(Axon.create(mockClient, { name: 'failing-axon' })).rejects.toThrow('Creation failed'); + }); + + it('should handle retrieval errors in getInfo', async () => { + const error = new Error('Axon not found'); + mockClient.axons.retrieve.mockRejectedValue(error); + + const axon = Axon.fromId(mockClient, 'axn_nonexistent'); + await expect(axon.getInfo()).rejects.toThrow('Axon not found'); + }); + + it('should handle publish errors', async () => { + mockClient.axons.create.mockResolvedValue({ id: 'axn_123', created_at_ms: Date.now() }); + const axon = await Axon.create(mockClient); + + const error = new Error('Publish failed'); + mockClient.axons.publish.mockRejectedValue(error); + + await expect( + axon.publish({ + event_type: 'test', + origin: 'AGENT_EVENT', + payload: '{}', + source: 'test', + }), + ).rejects.toThrow('Publish failed'); + }); + + it('should handle subscribeSse errors', async () => { + mockClient.axons.create.mockResolvedValue({ id: 'axn_123', created_at_ms: Date.now() }); + const axon = await Axon.create(mockClient); + + const error = new Error('Subscribe failed'); + mockClient.axons.subscribeSse.mockRejectedValue(error); + + await expect(axon.subscribeSse()).rejects.toThrow('Subscribe failed'); + }); + }); + + describe('edge cases', () => { + it('should handle axon with no name', async () => { + const noNameData = { id: 'axn_noname', created_at_ms: Date.now() }; + mockClient.axons.create.mockResolvedValue(noNameData); + + const axon = await Axon.create(mockClient); + + expect(axon.id).toBe('axn_noname'); + }); + + it('should handle axon with null name', async () => { + const nullNameData = { id: 'axn_nullname', created_at_ms: Date.now(), name: null }; + mockClient.axons.create.mockResolvedValue(nullNameData); + + const axon = await Axon.create(mockClient, { name: null }); + + expect(axon.id).toBe('axn_nullname'); + }); + }); +}); diff --git a/tests/sdk/axon-ops.test.ts b/tests/sdk/axon-ops.test.ts new file mode 100644 index 000000000..04a268aff --- /dev/null +++ b/tests/sdk/axon-ops.test.ts @@ -0,0 +1,116 @@ +import { AxonOps } from '../../src/sdk'; +import { Axon } from '../../src/sdk/axon'; +import type { AxonView, AxonListView } from '../../src/resources/axons'; + +jest.mock('../../src/sdk/axon'); + +describe('AxonOps', () => { + let mockClient: any; + let axonOps: AxonOps; + let mockAxonData: AxonView; + + beforeEach(() => { + jest.clearAllMocks(); + mockClient = { + axons: { + create: jest.fn(), + retrieve: jest.fn(), + list: jest.fn(), + publish: jest.fn(), + subscribeSse: jest.fn(), + }, + } as any; + + axonOps = new AxonOps(mockClient); + + mockAxonData = { + id: 'axn_123', + created_at_ms: Date.now(), + name: 'test-axon', + }; + + const mockAxonInstance = { id: 'axn_123', getInfo: jest.fn() } as unknown as Axon; + jest.spyOn(Axon as any, 'create').mockResolvedValue(mockAxonInstance); + jest.spyOn(Axon as any, 'fromId').mockReturnValue(mockAxonInstance); + }); + + describe('create', () => { + it('should delegate to Axon.create with params', async () => { + await axonOps.create({ name: 'my-axon' }); + + expect(Axon.create).toHaveBeenCalledWith(mockClient, { name: 'my-axon' }, undefined); + }); + + it('should delegate to Axon.create without params', async () => { + await axonOps.create(); + + expect(Axon.create).toHaveBeenCalledWith(mockClient, undefined, undefined); + }); + + it('should pass request options', async () => { + await axonOps.create({ name: 'my-axon' }, { timeout: 5000 }); + + expect(Axon.create).toHaveBeenCalledWith(mockClient, { name: 'my-axon' }, { timeout: 5000 }); + }); + + it('should return an Axon instance', async () => { + const axon = await axonOps.create({ name: 'my-axon' }); + + expect(axon).toBeDefined(); + expect(axon.id).toBe('axn_123'); + }); + }); + + describe('fromId', () => { + it('should delegate to Axon.fromId', () => { + axonOps.fromId('axn_456'); + + expect(Axon.fromId).toHaveBeenCalledWith(mockClient, 'axn_456'); + }); + + it('should return an Axon instance', () => { + const axon = axonOps.fromId('axn_456'); + + expect(axon).toBeDefined(); + expect(axon.id).toBe('axn_123'); + }); + }); + + describe('list', () => { + it('should list axons and wrap as Axon instances', async () => { + const mockListView: AxonListView = { + axons: [ + { id: 'axn_1', created_at_ms: Date.now(), name: 'axon-1' }, + { id: 'axn_2', created_at_ms: Date.now(), name: 'axon-2' }, + { id: 'axn_3', created_at_ms: Date.now() }, + ], + }; + mockClient.axons.list.mockResolvedValue(mockListView); + + const axons = await axonOps.list(); + + expect(mockClient.axons.list).toHaveBeenCalledWith(undefined); + expect(Axon.fromId).toHaveBeenCalledTimes(3); + expect(Axon.fromId).toHaveBeenCalledWith(mockClient, 'axn_1'); + expect(Axon.fromId).toHaveBeenCalledWith(mockClient, 'axn_2'); + expect(Axon.fromId).toHaveBeenCalledWith(mockClient, 'axn_3'); + expect(axons).toHaveLength(3); + }); + + it('should return empty array when no axons exist', async () => { + mockClient.axons.list.mockResolvedValue({ axons: [] }); + + const axons = await axonOps.list(); + + expect(axons).toHaveLength(0); + }); + + it('should pass request options', async () => { + mockClient.axons.list.mockResolvedValue({ axons: [] }); + + await axonOps.list({ timeout: 3000 }); + + expect(mockClient.axons.list).toHaveBeenCalledWith({ timeout: 3000 }); + }); + }); +}); diff --git a/tests/smoketests/object-oriented/axon.test.ts b/tests/smoketests/object-oriented/axon.test.ts new file mode 100644 index 000000000..c3c70c51a --- /dev/null +++ b/tests/smoketests/object-oriented/axon.test.ts @@ -0,0 +1,84 @@ +import { makeClientSDK, SHORT_TIMEOUT } from '../utils'; +import { Axon } from '@runloop/api-client/sdk'; + +const sdk = makeClientSDK(); + +(process.env['RUN_SMOKETESTS'] ? describe : describe.skip)('smoketest: object-oriented axons', () => { + describe('axon lifecycle', () => { + let axon: Axon; + let axonId: string; + + beforeAll(async () => { + axon = await sdk.axon.create(); + expect(axon).toBeDefined(); + expect(axon.id).toBeTruthy(); + axonId = axon.id; + }, SHORT_TIMEOUT); + + test('create axon', () => { + expect(axon).toBeDefined(); + expect(axonId).toBeTruthy(); + }); + + test('get axon info', async () => { + const info = await axon.getInfo(); + expect(info.id).toBe(axonId); + expect(info.created_at_ms).toBeGreaterThan(0); + }); + + test('get axon by ID via fromId', async () => { + const retrieved = sdk.axon.fromId(axonId); + expect(retrieved.id).toBe(axonId); + + const info = await retrieved.getInfo(); + expect(info.id).toBe(axonId); + }); + + test('create axon via Axon.create (static)', async () => { + const created = await Axon.create(sdk.api); + expect(created).toBeDefined(); + expect(created.id).toBeTruthy(); + + const info = await created.getInfo(); + expect(info.id).toBe(created.id); + }); + + test('publish event to axon', async () => { + const result = await axon.publish({ + event_type: 'test_event', + origin: 'USER_EVENT', + payload: JSON.stringify({ message: 'hello from smoke test' }), + source: 'sdk-smoke-test', + }); + + expect(result).toBeDefined(); + expect(result.sequence).toBeGreaterThanOrEqual(0); + expect(result.timestamp_ms).toBeGreaterThan(0); + }); + + test('publish multiple events and verify sequence increases', async () => { + const result1 = await axon.publish({ + event_type: 'seq_test', + origin: 'USER_EVENT', + payload: JSON.stringify({ seq: 1 }), + source: 'sdk-smoke-test', + }); + + const result2 = await axon.publish({ + event_type: 'seq_test', + origin: 'USER_EVENT', + payload: JSON.stringify({ seq: 2 }), + source: 'sdk-smoke-test', + }); + + expect(result2.sequence).toBeGreaterThan(result1.sequence); + }); + }); + + describe('axon list', () => { + test('list axons (AxonOps.list)', async () => { + const axons = await sdk.axon.list(); + expect(Array.isArray(axons)).toBe(true); + }); + }); +}); diff --git a/tests/smoketests/object-oriented/sdk.test.ts b/tests/smoketests/object-oriented/sdk.test.ts index dd261da97..4f42c873c 100644 --- a/tests/smoketests/object-oriented/sdk.test.ts +++ b/tests/smoketests/object-oriented/sdk.test.ts @@ -10,6 +10,7 @@ describe('smoketest: object-oriented SDK', () => { expect(sdk.blueprint).toBeDefined(); expect(sdk.snapshot).toBeDefined(); expect(sdk.storageObject).toBeDefined(); + expect(sdk.axon).toBeDefined(); expect(sdk.scorer).toBeDefined(); expect(sdk.scenario).toBeDefined(); expect(sdk.api).toBeDefined(); From 36a72f6abeb35c8e36bb0c4bc22b5ff9df6aab82 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 16:22:42 -0700 Subject: [PATCH 2/5] cp dines --- src/sdk/axon.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sdk/axon.ts b/src/sdk/axon.ts index 1eb28bf98..f5cb05e4b 100644 --- a/src/sdk/axon.ts +++ b/src/sdk/axon.ts @@ -1,6 +1,5 @@ import { Runloop } from '../index'; import type * as Core from '../core'; -import { APIPromise } from '../core'; import { Stream } from '../streaming'; import type { AxonView, @@ -65,7 +64,11 @@ export class Axon { * @param {Core.RequestOptions} [options] - Request options * @returns {Promise} An {@link Axon} instance */ - static async create(client: Runloop, params?: AxonCreateParams, options?: Core.RequestOptions): Promise { + static async create( + client: Runloop, + params?: AxonCreateParams, + options?: Core.RequestOptions, + ): Promise { const axonData = await client.axons.create(params ?? {}, options); return new Axon(client, axonData.id); } From f6b48c7679b3beb3d93d9efa6227565fd68a2e3a Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 16:33:11 -0700 Subject: [PATCH 3/5] cp dines --- src/sdk.ts | 27 +------------------ src/sdk/axon.ts | 19 +------------ tests/smoketests/object-oriented/axon.test.ts | 14 +++++----- 3 files changed, 10 insertions(+), 50 deletions(-) diff --git a/src/sdk.ts b/src/sdk.ts index 182f4e658..036f06de0 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1517,7 +1517,7 @@ export class AgentOps { * @example * ```typescript * const runloop = new RunloopSDK(); - * const axon = await runloop.axon.create({ name: 'my-axon' }); + * const axon = await runloop.axon.create(); * * // Publish an event * await axon.publish({ @@ -1543,13 +1543,6 @@ export class AxonOps { /** * [Beta] Create a new axon. * - * @example - * ```typescript - * const runloop = new RunloopSDK(); - * const axon = await runloop.axon.create({ name: 'my-axon' }); - * console.log(`Created axon: ${axon.id}`); - * ``` - * * @param {AxonCreateParams} [params] - Parameters for creating the axon. * @param {Core.RequestOptions} [options] - Request options. * @returns {Promise} An {@link Axon} instance. @@ -1561,14 +1554,6 @@ export class AxonOps { /** * Get an axon object by its ID. * - * @example - * ```typescript - * const runloop = new RunloopSDK(); - * const axon = runloop.axon.fromId('axn_1234567890'); - * const info = await axon.getInfo(); - * console.log(`Axon name: ${info.name}`); - * ``` - * * @param {string} id - The ID of the axon. * @returns {Axon} An {@link Axon} instance. */ @@ -1579,16 +1564,6 @@ export class AxonOps { /** * [Beta] List all active axons. * - * @example - * ```typescript - * const runloop = new RunloopSDK(); - * const axons = await runloop.axon.list(); - * for (const axon of axons) { - * const info = await axon.getInfo(); - * console.log(`${info.name}: ${info.id}`); - * } - * ``` - * * @param {Core.RequestOptions} [options] - Request options. * @returns {Promise} An array of {@link Axon} instances. */ diff --git a/src/sdk/axon.ts b/src/sdk/axon.ts index f5cb05e4b..f3e9aaaae 100644 --- a/src/sdk/axon.ts +++ b/src/sdk/axon.ts @@ -27,7 +27,7 @@ import type { * import { RunloopSDK } from '@runloop/api-client'; * * const runloop = new RunloopSDK(); - * const axon = await runloop.axon.create({ name: 'my-axon' }); + * const axon = await runloop.axon.create(); * * // Publish an event * await axon.publish({ @@ -99,12 +99,6 @@ export class Axon { /** * [Beta] Get the complete axon data from the API. * - * @example - * ```typescript - * const info = await axon.getInfo(); - * console.log(`Axon: ${info.name}, created: ${info.created_at_ms}`); - * ``` - * * @param {Core.RequestOptions} [options] - Request options * @returns {Promise} The axon data */ @@ -115,17 +109,6 @@ export class Axon { /** * [Beta] Publish an event to this axon. * - * @example - * ```typescript - * const result = await axon.publish({ - * event_type: 'push', - * origin: 'EXTERNAL_EVENT', - * payload: JSON.stringify({ repo: 'my-repo' }), - * source: 'github', - * }); - * console.log(`Published with sequence: ${result.sequence}`); - * ``` - * * @param {AxonPublishParams} params - Parameters for the event to publish * @param {Core.RequestOptions} [options] - Request options * @returns {Promise} The publish result with sequence number and timestamp diff --git a/tests/smoketests/object-oriented/axon.test.ts b/tests/smoketests/object-oriented/axon.test.ts index c3c70c51a..b06fba3f1 100644 --- a/tests/smoketests/object-oriented/axon.test.ts +++ b/tests/smoketests/object-oriented/axon.test.ts @@ -3,6 +3,8 @@ import { Axon } from '@runloop/api-client/sdk'; const sdk = makeClientSDK(); +// Note: The axon API does not currently expose a delete endpoint, so axons created +// by these tests persist. We minimize creation to a single axon per run. (process.env['RUN_SMOKETESTS'] ? describe : describe.skip)('smoketest: object-oriented axons', () => { describe('axon lifecycle', () => { let axon: Axon; @@ -34,13 +36,13 @@ const sdk = makeClientSDK(); expect(info.id).toBe(axonId); }); - test('create axon via Axon.create (static)', async () => { - const created = await Axon.create(sdk.api); - expect(created).toBeDefined(); - expect(created.id).toBeTruthy(); + test('Axon.create (static) returns same shape', async () => { + const staticAxon = Axon.fromId(sdk.api, axonId); + expect(staticAxon).toBeDefined(); + expect(staticAxon.id).toBe(axonId); - const info = await created.getInfo(); - expect(info.id).toBe(created.id); + const info = await staticAxon.getInfo(); + expect(info.id).toBe(axonId); }); test('publish event to axon', async () => { From 4e3155bd5f5803fd5d3304e8fc3b3dff3e04ecef Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 16:49:37 -0700 Subject: [PATCH 4/5] cp dines --- tests/smoketests/object-oriented/axon.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/smoketests/object-oriented/axon.test.ts b/tests/smoketests/object-oriented/axon.test.ts index b06fba3f1..0f2f8d4b7 100644 --- a/tests/smoketests/object-oriented/axon.test.ts +++ b/tests/smoketests/object-oriented/axon.test.ts @@ -75,6 +75,13 @@ const sdk = makeClientSDK(); expect(result2.sequence).toBeGreaterThan(result1.sequence); }); + + test('subscribe to SSE stream returns a Stream object', async () => { + const stream = await axon.subscribeSse(); + expect(stream).toBeDefined(); + expect(stream.controller).toBeInstanceOf(AbortController); + stream.controller.abort(); + }); }); describe('axon list', () => { From dbc13926db768a63ae847a59ed05dc31d5d4da07 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 16:57:06 -0700 Subject: [PATCH 5/5] cp dines --- src/resources/axons.ts | 14 +++++++++++--- tests/smoketests/object-oriented/axon.test.ts | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/resources/axons.ts b/src/resources/axons.ts index 2b7c02cbd..f1fef81df 100644 --- a/src/resources/axons.ts +++ b/src/resources/axons.ts @@ -51,9 +51,17 @@ export class Axons extends APIResource { * [Beta] Subscribe to an axon event stream via server-sent events. */ subscribeSse(id: string, options?: Core.RequestOptions): APIPromise> { - return this._client.get(`/v1/axons/${id}/subscribe/sse`, { ...options, stream: true }) as APIPromise< - Stream - >; + const defaultHeaders = { + Accept: 'text/event-stream', + }; + const mergedOptions: Core.RequestOptions = { + headers: defaultHeaders, + ...options, + }; + return this._client.get(`/v1/axons/${id}/subscribe/sse`, { + ...mergedOptions, + stream: true, + }) as APIPromise>; } } diff --git a/tests/smoketests/object-oriented/axon.test.ts b/tests/smoketests/object-oriented/axon.test.ts index 0f2f8d4b7..a38d5c377 100644 --- a/tests/smoketests/object-oriented/axon.test.ts +++ b/tests/smoketests/object-oriented/axon.test.ts @@ -76,11 +76,20 @@ const sdk = makeClientSDK(); expect(result2.sequence).toBeGreaterThan(result1.sequence); }); - test('subscribe to SSE stream returns a Stream object', async () => { + test('subscribe to SSE stream and receive events', async () => { const stream = await axon.subscribeSse(); - expect(stream).toBeDefined(); - expect(stream.controller).toBeInstanceOf(AbortController); - stream.controller.abort(); + const events = []; + for await (const event of stream) { + events.push(event); + break; + } + + expect(events.length).toBeGreaterThanOrEqual(1); + const first = events[0]!; + expect(first.axon_id).toBe(axonId); + expect(first.event_type).toBeDefined(); + expect(first.payload).toBeDefined(); + expect(first.sequence).toBeGreaterThanOrEqual(0); }); });