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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,6 @@ apps/docs/api/

# TypeDoc output
docs/

coverage/
tsconfig.tsbuildinfo
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "symbiont-sdk-js",
"version": "1.13.0",
"version": "1.14.3",
"description": "Symbiont JavaScript SDK - Monorepo",
"workspaces": [
"packages/*",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "symbi-agent",
"version": "1.13.0",
"version": "1.14.3",
"description": "Agent management and execution for Symbiont SDK",
"main": "dist/index.js",
"module": "dist/index.esm.js",
Expand Down
226 changes: 216 additions & 10 deletions packages/agent/src/AgentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,95 @@ import {
RequestOptions,
SymbiontConfig,
} from 'symbi-types';
import { buildRuntimeUrl } from './urlUtils';

/**
* Message payload for sending a message to an agent.
*/
export interface SendMessageRequest {
/** Identifier of the sender (agent or user). */
sender: string;
/** Arbitrary message payload. */
payload: unknown;
/** Optional time-to-live in seconds. */
ttlSeconds?: number;
/** Optional topic/subject for the message. */
topic?: string;
/** Optional AgentPin JWT for authenticated delivery. */
agentpinJwt?: string;
}

/**
* Response returned when a message is accepted for delivery.
*/
export interface SendMessageResponse {
message_id: string;
status: string;
}

/**
* Response returned when polling an agent's inbox.
*/
export interface ReceiveMessagesResponse {
messages: unknown[];
}

/**
* Status of a previously sent message.
*/
export interface MessageStatusResponse {
message_id: string;
status: string;
}

/**
* Lifecycle state of an agent for heartbeat reporting.
* PascalCase values mirror the runtime's agent state machine.
*/
export type AgentLifecycleState =
| 'Created'
| 'Ready'
| 'Running'
| 'Suspended'
| 'Completed'
| 'Failed'
| 'Terminated'
| (string & {});

/**
* Heartbeat payload reporting agent liveness and state.
*/
export interface HeartbeatRequest {
/** Current lifecycle state (PascalCase, e.g. Running/Completed/Failed). */
state: AgentLifecycleState;
/** Optional arbitrary metadata. */
metadata?: Record<string, unknown>;
/** Optional result of the last run. */
lastResult?: unknown;
/** Optional AgentPin JWT for authentication. */
agentpinJwt?: string;
}

/**
* Event type for run lifecycle events.
*/
export type AgentEventType =
| 'RunStarted'
| 'RunCompleted'
| 'RunFailed'
| (string & {});

/**
* Event payload pushed to an agent's event stream.
*/
export interface PushEventRequest {
/** Event type (e.g. RunStarted/RunCompleted/RunFailed). */
eventType: AgentEventType;
/** Arbitrary event payload. */
payload: unknown;
/** Optional AgentPin JWT for authentication. */
agentpinJwt?: string;
}

/**
* Simple interface to avoid circular dependency with SymbiontClient
Expand Down Expand Up @@ -128,17 +217,131 @@ export class AgentClient {
}

/**
* Re-execute a previously completed agent with optional new input
* POST /api/v1/agents/{id}/re-execute
* Send a message to an agent's inbox
* POST /agents/{id}/messages
*/
async sendMessage(
agentId: string,
request: SendMessageRequest
): Promise<SendMessageResponse> {
if (!agentId) {
throw new Error('Agent ID is required');
}
if (!request) {
throw new Error('Message request is required');
}

const body: Record<string, unknown> = {
sender: request.sender,
payload: request.payload,
};
if (request.ttlSeconds !== undefined) {
body.ttl_seconds = request.ttlSeconds;
}
if (request.topic !== undefined) {
body.topic = request.topic;
}
if (request.agentpinJwt !== undefined) {
body.agentpin_jwt = request.agentpinJwt;
}

return this.makeRequest<SendMessageResponse>(`/agents/${agentId}/messages`, {
method: 'POST',
body,
});
}

/**
* Receive pending messages for an agent
* GET /agents/{id}/messages
*/
async receiveMessages(agentId: string): Promise<ReceiveMessagesResponse> {
if (!agentId) {
throw new Error('Agent ID is required');
}

return this.makeRequest<ReceiveMessagesResponse>(
`/agents/${agentId}/messages`,
{
method: 'GET',
}
);
}

/**
* Get the delivery status of a previously sent message
* GET /messages/{id}/status
*/
async getMessageStatus(messageId: string): Promise<MessageStatusResponse> {
if (!messageId) {
throw new Error('Message ID is required');
}

return this.makeRequest<MessageStatusResponse>(
`/messages/${messageId}/status`,
{
method: 'GET',
}
);
}

/**
* Report an agent heartbeat
* POST /agents/{id}/heartbeat
*/
async sendHeartbeat(
agentId: string,
request: HeartbeatRequest
): Promise<void> {
if (!agentId) {
throw new Error('Agent ID is required');
}
if (!request) {
throw new Error('Heartbeat request is required');
}

const body: Record<string, unknown> = {
state: request.state,
};
if (request.metadata !== undefined) {
body.metadata = request.metadata;
}
if (request.lastResult !== undefined) {
body.last_result = request.lastResult;
}
if (request.agentpinJwt !== undefined) {
body.agentpin_jwt = request.agentpinJwt;
}

await this.makeRequest<void>(`/agents/${agentId}/heartbeat`, {
method: 'POST',
body,
});
}

/**
* Push a run lifecycle event for an agent
* POST /agents/{id}/events
*/
async reExecuteAgent(agentId: string, input?: unknown): Promise<ExecutionResult> {
async pushEvent(agentId: string, request: PushEventRequest): Promise<void> {
if (!agentId) {
throw new Error('Agent ID is required');
}
if (!request) {
throw new Error('Event request is required');
}

return this.makeRequest<ExecutionResult>(`/api/v1/agents/${agentId}/re-execute`, {
const body: Record<string, unknown> = {
event_type: request.eventType,
payload: request.payload,
};
if (request.agentpinJwt !== undefined) {
body.agentpin_jwt = request.agentpinJwt;
}

await this.makeRequest<void>(`/agents/${agentId}/events`, {
method: 'POST',
body: input !== undefined ? { input } : {},
body,
});
}

Expand Down Expand Up @@ -179,10 +382,10 @@ export class AgentClient {
// Get authentication headers from the parent client
const authHeaders = await this.client.getAuthHeaders(endpoint);

// Build the full URL
// Build the full URL with exactly one /api/v1 prefix
const config = this.client.configuration;
const baseUrl = config.runtimeApiUrl;
const fullUrl = `${baseUrl}${endpoint}`;
const fullUrl = buildRuntimeUrl(baseUrl, endpoint);

// Prepare headers
const headers: Record<string, string> = {
Expand Down Expand Up @@ -219,9 +422,12 @@ export class AgentClient {
return undefined as T;
}

// Parse JSON response
const data = await response.json();
return data as T;
// Parse JSON response, tolerating empty bodies (e.g. 204 No Content)
const text = await response.text();
if (!text) {
return undefined as T;
}
return JSON.parse(text) as T;
} catch (error) {
if (error instanceof Error) {
throw new Error(`AgentClient request failed: ${error.message}`);
Expand Down
3 changes: 2 additions & 1 deletion packages/agent/src/ChannelClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
AddIdentityMappingRequest,
ChannelAuditResponse,
} from 'symbi-types';
import { buildRuntimeUrl } from './urlUtils';

/**
* Simple interface to avoid circular dependency with SymbiontClient
Expand Down Expand Up @@ -178,7 +179,7 @@ export class ChannelClient {
const authHeaders = await this.client.getAuthHeaders(endpoint);
const config = this.client.configuration;
const baseUrl = config.runtimeApiUrl;
const fullUrl = `${baseUrl}${endpoint}`;
const fullUrl = buildRuntimeUrl(baseUrl, endpoint);

const headers: Record<string, string> = {
'Content-Type': 'application/json',
Expand Down
3 changes: 2 additions & 1 deletion packages/agent/src/ScheduleClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
DeleteScheduleResponse,
SchedulerHealthResponse,
} from 'symbi-types';
import { buildRuntimeUrl } from './urlUtils';

/**
* Simple interface to avoid circular dependency with SymbiontClient
Expand Down Expand Up @@ -163,7 +164,7 @@ export class ScheduleClient {
const authHeaders = await this.client.getAuthHeaders(endpoint);
const config = this.client.configuration;
const baseUrl = config.runtimeApiUrl;
const fullUrl = `${baseUrl}${endpoint}`;
const fullUrl = buildRuntimeUrl(baseUrl, endpoint);

const headers: Record<string, string> = {
'Content-Type': 'application/json',
Expand Down
3 changes: 2 additions & 1 deletion packages/agent/src/WorkflowClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
SymbiontConfig,
WorkflowExecutionRequest,
} from 'symbi-types';
import { buildRuntimeUrl } from './urlUtils';

/**
* Simple interface to avoid circular dependency with SymbiontClient
Expand Down Expand Up @@ -53,7 +54,7 @@ export class WorkflowClient {
const authHeaders = await this.client.getAuthHeaders(endpoint);
const config = this.client.configuration;
const baseUrl = config.runtimeApiUrl;
const fullUrl = `${baseUrl}${endpoint}`;
const fullUrl = buildRuntimeUrl(baseUrl, endpoint);

const headers: Record<string, string> = {
'Content-Type': 'application/json',
Expand Down
Loading
Loading