diff --git a/src/resources/axons/axons.ts b/src/resources/axons/axons.ts index 31b6db59a..bec0cde03 100644 --- a/src/resources/axons/axons.ts +++ b/src/resources/axons/axons.ts @@ -66,9 +66,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 mergedOptions: Core.RequestOptions = { + ...options, + headers: { + Accept: 'text/event-stream', + ...options?.headers, + }, + }; + return this._client.get(`/v1/axons/${id}/subscribe/sse`, { + ...mergedOptions, + stream: true, + }) as APIPromise>; } } diff --git a/src/sdk/axon.ts b/src/sdk/axon.ts index f3e9aaaae..e792fc3a6 100644 --- a/src/sdk/axon.ts +++ b/src/sdk/axon.ts @@ -8,6 +8,50 @@ import type { PublishResultView, AxonEventView, } from '../resources/axons'; +import type { + SqlBatchParams, + SqlBatchResultView, + SqlQueryParams, + SqlQueryResultView, +} from '../resources/axons/sql'; + +/** + * SQL operations for an axon's SQLite database. + * + * @category Axon + */ +export class AxonSqlOps { + /** + * @private + */ + constructor( + private client: Runloop, + private axonId: string, + ) {} + + /** + * [Beta] Execute a single parameterized SQL statement against this axon's SQLite database. + * + * @param {SqlQueryParams} params - The SQL query and optional positional parameters + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise} The query result with columns, rows, and metadata + */ + async query(params: SqlQueryParams, options?: Core.RequestOptions): Promise { + return this.client.axons.sql.query(this.axonId, params, options); + } + + /** + * [Beta] Execute multiple SQL statements atomically within a single transaction + * against this axon's SQLite database. + * + * @param {SqlBatchParams} params - The batch of SQL statements to execute + * @param {Core.RequestOptions} [options] - Request options + * @returns {Promise} One result per statement, in order + */ + async batch(params: SqlBatchParams, options?: Core.RequestOptions): Promise { + return this.client.axons.sql.batch(this.axonId, params, options); + } +} /** * [Beta] Object-oriented interface for working with Axons. @@ -42,15 +86,21 @@ import type { * for await (const event of stream) { * console.log(event.event_type, event.payload); * } + * + * // Execute SQL queries + * await axon.sql.query({ sql: 'CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)' }); + * const result = await axon.sql.query({ sql: 'SELECT * FROM tasks WHERE id = ?', params: [1] }); * ``` */ export class Axon { private client: Runloop; private _id: string; + public readonly sql: AxonSqlOps; private constructor(client: Runloop, id: string) { this.client = client; this._id = id; + this.sql = new AxonSqlOps(this.client, this._id); } /** diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 0c508139b..d29659145 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -3,7 +3,7 @@ export { Blueprint } from './blueprint'; export { Snapshot } from './snapshot'; export { StorageObject } from './storage-object'; export { Agent } from './agent'; -export { Axon } from './axon'; +export { Axon, AxonSqlOps } from './axon'; export { Execution } from './execution'; export { ExecutionResult } from './execution-result'; export { Scorer } from './scorer'; diff --git a/tests/smoketests/object-oriented/axon.test.ts b/tests/smoketests/object-oriented/axon.test.ts index a38d5c377..244a1808e 100644 --- a/tests/smoketests/object-oriented/axon.test.ts +++ b/tests/smoketests/object-oriented/axon.test.ts @@ -77,6 +77,14 @@ const sdk = makeClientSDK(); }); test('subscribe to SSE stream and receive events', async () => { + // Ensure at least one event exists so the stream has something to replay + await axon.publish({ + event_type: 'sse_test', + origin: 'USER_EVENT', + payload: JSON.stringify({ sse: true }), + source: 'sdk-smoke-test', + }); + const stream = await axon.subscribeSse(); const events = []; for await (const event of stream) { @@ -91,6 +99,44 @@ const sdk = makeClientSDK(); expect(first.payload).toBeDefined(); expect(first.sequence).toBeGreaterThanOrEqual(0); }); + + test('sql.query: create table and insert row', async () => { + await axon.sql.query({ + sql: 'CREATE TABLE IF NOT EXISTS smoke_test (id INTEGER PRIMARY KEY, value TEXT)', + }); + + await axon.sql.query({ + sql: 'INSERT INTO smoke_test (id, value) VALUES (?, ?)', + params: [1, 'hello'], + }); + + const result = await axon.sql.query({ + sql: 'SELECT * FROM smoke_test WHERE id = ?', + params: [1], + }); + + expect(result.columns).toBeDefined(); + expect(result.columns.length).toBeGreaterThan(0); + expect(result.rows.length).toBe(1); + expect(result.meta.duration_ms).toBeGreaterThanOrEqual(0); + }); + + test('sql.batch: execute multiple statements atomically', async () => { + const result = await axon.sql.batch({ + statements: [ + { sql: 'CREATE TABLE IF NOT EXISTS batch_test (id INTEGER PRIMARY KEY, name TEXT)' }, + { sql: 'INSERT INTO batch_test (id, name) VALUES (?, ?)', params: [1, 'alice'] }, + { sql: 'INSERT INTO batch_test (id, name) VALUES (?, ?)', params: [2, 'bob'] }, + { sql: 'SELECT * FROM batch_test ORDER BY id' }, + ], + }); + + expect(result.results).toBeDefined(); + expect(result.results.length).toBe(4); + const selectResult = result.results[3]!; + expect(selectResult.success).toBeDefined(); + expect(selectResult.success!.rows.length).toBe(2); + }); }); describe('axon list', () => {