@artinet/sdk
Version:
A TypeScript SDK for building collaborative AI agents.
324 lines (323 loc) • 11.9 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();
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 = {
...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);
}
}