UNPKG

@artinet/sdk

Version:

A TypeScript SDK for building collaborative AI agents.

92 lines (91 loc) 4.42 kB
/** * Copyright 2025 The Artinet Project * SPDX-License-Identifier: Apache-2.0 */ import { StateMachine } from "../state-machine.js"; import { logger } from "../../../config/index.js"; import * as describe from "../../../create/describe.js"; import { TASK_NOT_FOUND } from "../../../utils/errors.js"; import { formatJson } from "../../../utils/utils.js"; import assert from "assert"; export function createStateMachine({ contextId, service, task: currentTask, overrides, }) { const handler = { contextId: contextId, onStart: async (context) => { assert(context.contextId === contextId, "context mismatch"); logger.info(`onStart[ctx:${contextId}]:`, { taskId: context.taskId }); await service.connections.set(context.contextId); const task = await service.tasks.get(context.taskId); // we now expect the task to be created by the service // so if it's not found we throw an error if (!task) { throw TASK_NOT_FOUND({ taskId: context.taskId, contextId: contextId }); } return task; }, onCancel: async (update, task) => { logger.info(`onCancel[ctx:${contextId}]:`, "cancellation triggered"); logger.debug(`onCancel[ctx:${contextId}]:`, "arguments", update, task); await service.cancellations.set(task.id); const cancellation = describe.update.canceled({ taskId: task.id, contextId: task.contextId, message: update.status?.message, }); /**We've intentionally blocked further updates, so the first cancellation update is responsible for updating stored task state and notifying listeners*/ const updatedTask = await service.tasks.update((await service.contexts.get(contextId)), cancellation); (await service.contexts.get(contextId))?.publisher.emit("update", updatedTask, cancellation); }, onUpdate: async (update, task) => { logger.info(`onUpdate[ctx:${contextId}]:`); logger.debug(`onUpdate[ctx:${contextId}]:`, { taskId: task.id }); if (await service.cancellations.has(task.id)) { logger.warn(`onUpdate[ctx:${contextId}]:`, { taskId: task.id }, "task is cancelled, no longer processing updates"); return task; } return await service.tasks.update((await service.contexts.get(contextId)), update); }, onError: async (error, task) => { logger.error(`onError[ctx:${contextId}]:`, error); if (!task) { logger.error(`onError[ctx:${contextId}]:`, new Error("task not found")); return; } const errorUpdate = describe.update.failed({ taskId: task.id, contextId, message: describe.message({ messageId: `failed:${task.id}`, parts: [ { kind: "text", text: error instanceof Error ? error.message : formatJson(error), }, ], }), }); const context = await service.contexts.get(contextId); if (!context) { logger.error(`onError[ctx:${contextId}]:`, new Error("context not found")); return; } /**triggering onUpdate here with a catch instead of a raw tasks.update call*/ await context.publisher.onUpdate(errorUpdate).catch((error) => { //we capture errors thrown during error handling to ensure we trigger completion gracefully logger.error(`onError: task update error[ctx:${contextId}]:`, error); }); await context.publisher.onComplete(); }, onComplete: async (task) => { assert(task.contextId === contextId, "context mismatch"); logger.info(`onComplete[ctx:${contextId}]: `, { taskId: task.id }); await service.cancellations.delete(task.id); await service.connections.delete(task.contextId); await service.contexts.delete(task.contextId); }, }; return new StateMachine(contextId, { ...handler, ...overrides, }, currentTask); }