From 5349c7b6c8b5cdca56aafb730661b00b234c5c79 Mon Sep 17 00:00:00 2001 From: Glastonburyk <178438005+Glastonburyk@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:55:03 +0000 Subject: [PATCH] fix: address paid issue 257 --- src/errors/runtime-errors.ts | 3 +- src/runtime/agent-runtime.ts | 12 +++ .../runtime/duplicate-in-flight-task.test.ts | 79 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/runtime/duplicate-in-flight-task.test.ts diff --git a/src/errors/runtime-errors.ts b/src/errors/runtime-errors.ts index 379a78a..b7564bb 100644 --- a/src/errors/runtime-errors.ts +++ b/src/errors/runtime-errors.ts @@ -6,7 +6,8 @@ export type RuntimeErrorCode = | "DUPLICATE_TOOL" | "INVALID_TASK" | "EXECUTION_FAILED" - | "MAX_TOOL_CALLS_EXCEEDED"; + | "MAX_TOOL_CALLS_EXCEEDED" + | "DUPLICATE_IN_FLIGHT_TASK"; export class RuntimeError extends Error { public readonly code: RuntimeErrorCode; diff --git a/src/runtime/agent-runtime.ts b/src/runtime/agent-runtime.ts index 40fe51f..91ef352 100644 --- a/src/runtime/agent-runtime.ts +++ b/src/runtime/agent-runtime.ts @@ -104,11 +104,23 @@ export class AgentRuntime { }); } + /** + * Executes a task. Task IDs are unique only while a task is active; callers may + * reuse an ID after the prior execution settles. + */ public async executeTask( task: RuntimeTask ): Promise> { assertRuntimeStarted(this.started); + if (this.inFlightTasks.has(task.taskId)) { + throw new RuntimeError( + "DUPLICATE_IN_FLIGHT_TASK", + `Task "${task.taskId}" is already in flight.`, + { taskId: task.taskId } + ); + } + const agent = this.dependencies.agentManager.getOrCreate(task.agentId); const context: RuntimeContext = { runtimeId: this.runtimeId, diff --git a/tests/runtime/duplicate-in-flight-task.test.ts b/tests/runtime/duplicate-in-flight-task.test.ts new file mode 100644 index 0000000..bb3c515 --- /dev/null +++ b/tests/runtime/duplicate-in-flight-task.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { AgentRuntime } from "../../src/runtime/agent-runtime.js"; + +describe("AgentRuntime duplicate in-flight task IDs", () => { + it("rejects a duplicate active task without emitting a failed lifecycle event", async () => { + const runtime = new AgentRuntime({ runtimeId: "duplicate-id-runtime" }); + let release!: () => void; + + runtime.registerTool({ + name: "blocking", + description: "waits for release", + execute: async () => { + await new Promise((resolve) => { + release = resolve; + }); + return { ok: true }; + } + }); + + const failedEvents: unknown[] = []; + runtime.getDependencies().eventBus.on("runtime.task.failed", (event) => { + failedEvents.push(event); + }); + + await runtime.start(); + const first = runtime.executeTask({ + taskId: "same-id", + agentId: "agent-a", + toolName: "blocking", + input: "first", + payload: {} + }); + + expect(runtime.getInFlightTaskCount()).toBe(1); + + await expect( + runtime.executeTask({ + taskId: "same-id", + agentId: "agent-b", + toolName: "blocking", + input: "duplicate", + payload: {} + }) + ).rejects.toMatchObject({ + code: "DUPLICATE_IN_FLIGHT_TASK", + details: { taskId: "same-id" } + }); + + expect(runtime.getInFlightTaskCount()).toBe(1); + expect(failedEvents).toHaveLength(0); + + release(); + await first; + expect(runtime.getInFlightTaskCount()).toBe(0); + }); + + it("allows the same task ID again after the previous execution settles", async () => { + const runtime = new AgentRuntime({ runtimeId: "task-id-reuse-runtime" }); + runtime.registerTool({ + name: "echo", + description: "returns success", + execute: async () => ({ ok: true }) + }); + + await runtime.start(); + + const task = { + taskId: "reusable-id", + agentId: "agent-a", + toolName: "echo", + input: "echo", + payload: { value: 1 } + }; + + await expect(runtime.executeTask(task)).resolves.toBeDefined(); + await expect(runtime.executeTask(task)).resolves.toBeDefined(); + expect(runtime.getInFlightTaskCount()).toBe(0); + }); +});