UNPKG

@artinet/sdk

Version:

A TypeScript SDK for building collaborative AI agents.

324 lines (323 loc) 11.9 kB
/** * 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); } }