@artinet/sdk
Version:
A TypeScript SDK for building collaborative AI agents.
324 lines (323 loc) • 12.1 kB
JavaScript
/**
* Copyright 2025 The Artinet Project
* SPDX-License-Identifier: Apache-2.0
*/
import { A2A } from "../../types/index.js";
import { validateSchema } from "../../utils/schema-validation.js";
import * as describe from "../../create/describe.js";
import { INVALID_REQUEST, TASK_NOT_FOUND } from "../../utils/errors.js";
import { Messenger } from './messenger.js';
import { execute } from './execute.js';
import { createService } from './factory/service.js';
import { getReferences } from './helpers/references.js';
import { logger } from "../../config/index.js";
const taskToMessageParams = (task) => {
const latestUserMessage = task.history?.filter((msg) => msg.role === 'user')?.pop() ??
/**fallback to the first message if no user message is found */
task.history?.[0];
if (!latestUserMessage) {
throw INVALID_REQUEST('No user message found');
}
if (latestUserMessage.contextId && latestUserMessage.contextId !== task.contextId) {
throw INVALID_REQUEST('User message context ID does not match task context ID');
}
const messageParams = {
message: {
...latestUserMessage,
taskId: task.id,
contextId: latestUserMessage.contextId ?? task.contextId,
},
metadata: task.metadata,
};
return messageParams;
};
/**
* @note Comprehensive Extension system coming in a future release
*/
const getExtensions = async (_extensions, _forwardExtensions) => {
// logger.warn("getExtensions: not implemented", { extensions });
return [];
};
/**
* Binds a notifier to a context
* @param notifier - The notifier to bind
* @param context - The context to bind the notifier to
* @returns A new notifier that is bound to the context
*/
const bindNotifier = async (context, taskId, config, notifier) => {
if (!notifier || !config) {
return;
}
context.publisher.on('update', async (task, update) => {
await notifier.notify(task, update, context).catch((error) => {
logger.error('Error sending push notification: ', { error });
});
});
await notifier.register(taskId, config).catch((error) => {
logger.error('Error registering push notification: ', { error });
});
};
/**
* @note We endeavor to remove all optional parameters from below this class.
* This will allow the service to act as the boundary to our Hexagonal Architecture.
*/
export class Service {
_agentCard;
_engine;
_connections;
_cancellations;
_tasks;
_contexts;
_streams;
_handles;
_overrides;
constructor(_agentCard, _engine, _connections, _cancellations, _tasks, _contexts, _streams, _handles, _overrides) {
this._agentCard = _agentCard;
this._engine = _engine;
this._connections = _connections;
this._cancellations = _cancellations;
this._tasks = _tasks;
this._contexts = _contexts;
this._streams = _streams;
this._handles = _handles;
this._overrides = _overrides;
}
get handles() {
return this._handles;
}
set handles(handles) {
this._handles = {
...this._handles,
...handles,
};
}
get agentCard() {
return this._agentCard;
}
get engine() {
return this._engine;
}
set engine(engine) {
this._engine = engine;
}
get connections() {
return this._connections;
}
get cancellations() {
return this._cancellations;
}
get tasks() {
return this._tasks;
}
get contexts() {
return this._contexts;
}
set contexts(contexts) {
this._contexts.list().then((list) => {
list.forEach((context) => {
contexts.set(context.contextId, context);
});
});
this._contexts = contexts;
}
get streams() {
return this._streams;
}
get overrides() {
return this._overrides;
}
set overrides(overrides) {
this._overrides = {
...this._overrides,
...overrides,
};
}
async execute({ engine, context }) {
await execute(engine, context);
}
async getAgentCard() {
return this.agentCard;
}
async stop() {
logger.info(`Service[stop]`);
for (const context of await this.contexts.list()) {
await context.publisher.onCancel(describe.update.canceled({
contextId: context.contextId,
taskId: context.taskId,
message: describe.message('service stopped'),
}));
}
return;
}
async getTask(params, options) {
const taskParams = await validateSchema(A2A.TaskQueryParamsSchema, params);
logger.info(`Service[getTask]:`, { taskId: taskParams.id });
const task = options?.task ?? (await this.tasks.get(taskParams.id));
if (!task) {
throw TASK_NOT_FOUND({ taskId: taskParams.id });
}
const userId = options?.userId;
const messageParams = taskToMessageParams(task);
const context = await this.contexts.create({
contextId: task.contextId,
service: this,
abortSignal: options?.signal,
task: task,
overrides: this.overrides,
messenger: Messenger.create(messageParams),
userId: userId,
});
return await this.handles.getTask(taskParams, context);
}
async cancelTask(params, options) {
const taskParams = await validateSchema(A2A.TaskIdParamsSchema, params);
logger.info(`Service[cancelTask]:`, { taskId: taskParams.id });
const task = options?.task ?? (await this.tasks.get(taskParams.id));
if (!task) {
throw TASK_NOT_FOUND({ taskId: taskParams.id });
}
const userId = options?.userId;
const messageParams = taskToMessageParams(task);
const context = await this.contexts.create({
contextId: task.contextId,
service: this,
abortSignal: options?.signal,
task: task,
overrides: this.overrides,
messenger: Messenger.create(messageParams),
userId: userId,
references: await getReferences(this.tasks, messageParams.message.referenceTaskIds),
});
return await this.handles.cancelTask(taskParams, context);
}
async sendMessage(paramsOrMessage, options) {
const params = describe.messageSendParams(paramsOrMessage);
return await this._sendMessage(params, options);
}
async _sendMessage(params, options) {
const messageParams = await validateSchema(A2A.MessageSendParamsSchema, params);
logger.info(`Service[sendMessage]:`, {
messageId: messageParams.message.messageId,
});
logger.debug(`Service[sendMessage]:`, {
taskId: messageParams.message.taskId,
});
logger.debug(`Service[sendMessage]:`, {
contextId: messageParams.message.contextId,
});
const task = options?.task ??
(await this.tasks.create({
id: messageParams.message.taskId,
contextId: messageParams.message.contextId,
history: [messageParams.message],
metadata: {
...messageParams.metadata,
},
}));
const userId = options?.userId;
const extensions = await getExtensions(messageParams.message.extensions, options?.extensions);
const context = await this.contexts.create({
contextId: task.contextId,
service: this,
abortSignal: options?.signal,
task: task,
overrides: this.overrides,
messenger: Messenger.create(messageParams),
references: await getReferences(this.tasks, messageParams.message.referenceTaskIds),
extensions: extensions,
userId: userId,
});
if (options?.notifier) {
await bindNotifier(context, task.id, messageParams.configuration?.pushNotificationConfig, options.notifier);
}
return await this.handles.sendMessage(messageParams, context);
}
async *sendMessageStream(_params, options) {
let params;
if (typeof _params === 'string' || (typeof _params === 'object' && 'parts' in _params)) {
params = describe.messageSendParams(_params);
}
else {
params = _params;
}
const messageParams = await validateSchema(A2A.MessageSendParamsSchema, params);
yield* this._sendMessageStream(messageParams, options);
}
/**
* @deprecated Use sendMessageStream instead
*/
async *streamMessage(paramsOrMessage, options) {
const params = describe.messageSendParams(paramsOrMessage);
yield* this.sendMessageStream(params, options);
}
async *_sendMessageStream(params, options) {
const messageParams = await validateSchema(A2A.MessageSendParamsSchema, params);
logger.info('Service[streamMessage]:', {
taskId: messageParams.message.taskId,
contextId: messageParams.message.contextId,
});
const task = options?.task ??
(await this.tasks.create({
id: messageParams.message.taskId,
contextId: messageParams.message.contextId,
history: [messageParams.message],
metadata: {
...messageParams.metadata,
},
}));
logger.debug('Service[streamMessage]: task created', {
taskId: task.id,
contextId: task.contextId,
});
const userId = options?.userId;
const extensions = await getExtensions(messageParams.message.extensions, options?.extensions);
const context = await this.contexts.create({
contextId: task.contextId,
service: this,
abortSignal: options?.signal,
task: task,
overrides: this.overrides,
messenger: Messenger.create(messageParams),
references: await getReferences(this.tasks, messageParams.message.referenceTaskIds),
extensions: extensions,
userId: userId,
});
if (options?.notifier) {
await bindNotifier(context, task.id, messageParams.configuration?.pushNotificationConfig, options.notifier);
}
yield* this.handles.streamMessage(messageParams, context);
}
async *resubscribe(params, options) {
const taskParams = await validateSchema(A2A.TaskIdParamsSchema, params);
logger.info(`Service[resubscribe]:`, { taskId: taskParams.id });
const task = await this.tasks.get(taskParams.id);
if (!task) {
throw TASK_NOT_FOUND({ taskId: taskParams.id });
}
logger.debug('Service[resubscribe]:', {
taskId: task.id,
contextId: task.contextId,
});
const messageParams = taskToMessageParams(task);
const userId = options?.userId;
const extensions = await getExtensions(messageParams.message.extensions, options?.extensions);
const context = await this.contexts.create({
contextId: task.contextId,
service: this,
abortSignal: options?.signal,
task: task,
overrides: this.overrides,
references: await getReferences(this.tasks, messageParams.message.referenceTaskIds),
messenger: Messenger.create(messageParams),
extensions: extensions,
userId: userId,
});
if (options?.notifier) {
await bindNotifier(context, task.id, messageParams.configuration?.pushNotificationConfig, options.notifier);
}
yield* this.handles.resubscribe(taskParams, context);
}
static create(params) {
return createService(params);
}
}