Initial import NDC_1C
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import type { RuntimeAdapter } from "./runtimeAdapter";
|
||||
import type { RunRecord, TaskRecord, TraceEvent } from "../types/accountingAgent";
|
||||
import { ApiError } from "../utils/http";
|
||||
|
||||
export class InMemoryRuntimeAdapter implements RuntimeAdapter {
|
||||
private runs: RunRecord[] = [];
|
||||
private tasks: TaskRecord[] = [];
|
||||
private traces: TraceEvent[] = [];
|
||||
private idempotencyCache = new Map<string, unknown>();
|
||||
|
||||
private now(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
private cacheKey(action: string, key?: string): string | null {
|
||||
if (!key || !key.trim()) return null;
|
||||
return `${action}:${key.trim()}`;
|
||||
}
|
||||
|
||||
private readIdempotency<T>(action: string, idempotencyKey?: string): T | null {
|
||||
const cache = this.cacheKey(action, idempotencyKey);
|
||||
if (!cache) return null;
|
||||
return (this.idempotencyCache.get(cache) as T | undefined) ?? null;
|
||||
}
|
||||
|
||||
private writeIdempotency(action: string, idempotencyKey: string | undefined, value: unknown): void {
|
||||
const cache = this.cacheKey(action, idempotencyKey);
|
||||
if (!cache) return;
|
||||
this.idempotencyCache.set(cache, value);
|
||||
}
|
||||
|
||||
private pushEvent(input: {
|
||||
runId: string;
|
||||
sessionId: string;
|
||||
taskId?: string | null;
|
||||
level?: "info" | "warn" | "error";
|
||||
eventType: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}): void {
|
||||
this.traces.push({
|
||||
timestamp: this.now(),
|
||||
level: input.level ?? "info",
|
||||
service: "llm_normalizer_backend",
|
||||
sessionId: input.sessionId,
|
||||
runId: input.runId,
|
||||
taskId: input.taskId ?? null,
|
||||
eventType: input.eventType,
|
||||
payload: input.payload
|
||||
});
|
||||
}
|
||||
|
||||
public startRun(input: {
|
||||
sessionId?: string;
|
||||
initiator?: string;
|
||||
source?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
idempotencyKey?: string;
|
||||
}): RunRecord {
|
||||
const cached = this.readIdempotency<RunRecord>("startRun", input.idempotencyKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const record: RunRecord = {
|
||||
sessionId: input.sessionId ?? `session_${nanoid(8)}`,
|
||||
runId: `run_${nanoid(10)}`,
|
||||
status: "RUNNING",
|
||||
initiator: input.initiator ?? "operator",
|
||||
source: input.source ?? "gui",
|
||||
createdAt: this.now(),
|
||||
updatedAt: this.now(),
|
||||
metadata: input.metadata ?? {}
|
||||
};
|
||||
this.runs.unshift(record);
|
||||
this.pushEvent({
|
||||
runId: record.runId,
|
||||
sessionId: record.sessionId,
|
||||
eventType: "RUN_STARTED",
|
||||
payload: record.metadata as Record<string, unknown>
|
||||
});
|
||||
this.writeIdempotency("startRun", input.idempotencyKey, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
public finishRun(input: {
|
||||
runId: string;
|
||||
status: "DONE" | "ERROR" | "CANCELLED";
|
||||
source?: string;
|
||||
reason?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
idempotencyKey?: string;
|
||||
}): RunRecord {
|
||||
const cached = this.readIdempotency<RunRecord>("finishRun", input.idempotencyKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const record = this.runs.find((item) => item.runId === input.runId);
|
||||
if (!record) {
|
||||
throw new ApiError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, 404);
|
||||
}
|
||||
record.status = input.status;
|
||||
record.updatedAt = this.now();
|
||||
record.source = input.source ?? record.source;
|
||||
record.metadata = {
|
||||
...(record.metadata ?? {}),
|
||||
...(input.metadata ?? {}),
|
||||
reason: input.reason ?? null
|
||||
};
|
||||
this.pushEvent({
|
||||
runId: record.runId,
|
||||
sessionId: record.sessionId,
|
||||
eventType: `RUN_FINISHED_${input.status}`,
|
||||
level: input.status === "ERROR" ? "error" : "info",
|
||||
payload: { reason: input.reason ?? null }
|
||||
});
|
||||
this.writeIdempotency("finishRun", input.idempotencyKey, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
public listRuns(): RunRecord[] {
|
||||
return [...this.runs];
|
||||
}
|
||||
|
||||
public getRun(runId: string): RunRecord | null {
|
||||
return this.runs.find((item) => item.runId === runId) ?? null;
|
||||
}
|
||||
|
||||
public enqueueTask(input: {
|
||||
runId: string;
|
||||
payload: Record<string, unknown>;
|
||||
source?: string;
|
||||
idempotencyKey?: string;
|
||||
}): TaskRecord {
|
||||
const cached = this.readIdempotency<TaskRecord>("enqueueTask", input.idempotencyKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const run = this.getRun(input.runId);
|
||||
if (!run) {
|
||||
throw new ApiError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, 404);
|
||||
}
|
||||
|
||||
const task: TaskRecord = {
|
||||
taskId: `task_${nanoid(10)}`,
|
||||
runId: run.runId,
|
||||
status: "QUEUED",
|
||||
payload: input.payload,
|
||||
source: input.source ?? "gui",
|
||||
createdAt: this.now(),
|
||||
updatedAt: this.now()
|
||||
};
|
||||
this.tasks.unshift(task);
|
||||
this.pushEvent({
|
||||
runId: run.runId,
|
||||
sessionId: run.sessionId,
|
||||
taskId: task.taskId,
|
||||
eventType: "TASK_ENQUEUED",
|
||||
payload: task.payload
|
||||
});
|
||||
this.writeIdempotency("enqueueTask", input.idempotencyKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public claimTask(): TaskRecord | null {
|
||||
const task = this.tasks.find((item) => item.status === "QUEUED");
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
task.status = "RUNNING";
|
||||
task.updatedAt = this.now();
|
||||
const run = this.getRun(task.runId);
|
||||
if (run) {
|
||||
this.pushEvent({
|
||||
runId: run.runId,
|
||||
sessionId: run.sessionId,
|
||||
taskId: task.taskId,
|
||||
eventType: "TASK_CLAIMED"
|
||||
});
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public completeTask(input: {
|
||||
taskId: string;
|
||||
result: Record<string, unknown>;
|
||||
source?: string;
|
||||
idempotencyKey?: string;
|
||||
}): TaskRecord {
|
||||
const cached = this.readIdempotency<TaskRecord>("completeTask", input.idempotencyKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const task = this.tasks.find((item) => item.taskId === input.taskId);
|
||||
if (!task) {
|
||||
throw new ApiError("TASK_NOT_FOUND", `Task not found: ${input.taskId}`, 404);
|
||||
}
|
||||
task.status = "DONE";
|
||||
task.updatedAt = this.now();
|
||||
task.result = input.result;
|
||||
task.source = input.source ?? task.source;
|
||||
|
||||
const run = this.getRun(task.runId);
|
||||
if (run) {
|
||||
this.pushEvent({
|
||||
runId: run.runId,
|
||||
sessionId: run.sessionId,
|
||||
taskId: task.taskId,
|
||||
eventType: "TASK_DONE",
|
||||
payload: input.result
|
||||
});
|
||||
}
|
||||
this.writeIdempotency("completeTask", input.idempotencyKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public failTask(input: {
|
||||
taskId: string;
|
||||
error: { code: string; message: string; details?: unknown };
|
||||
source?: string;
|
||||
idempotencyKey?: string;
|
||||
}): TaskRecord {
|
||||
const cached = this.readIdempotency<TaskRecord>("failTask", input.idempotencyKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const task = this.tasks.find((item) => item.taskId === input.taskId);
|
||||
if (!task) {
|
||||
throw new ApiError("TASK_NOT_FOUND", `Task not found: ${input.taskId}`, 404);
|
||||
}
|
||||
task.status = "ERROR";
|
||||
task.updatedAt = this.now();
|
||||
task.error = input.error;
|
||||
task.source = input.source ?? task.source;
|
||||
|
||||
const run = this.getRun(task.runId);
|
||||
if (run) {
|
||||
this.pushEvent({
|
||||
runId: run.runId,
|
||||
sessionId: run.sessionId,
|
||||
taskId: task.taskId,
|
||||
eventType: "TASK_ERROR",
|
||||
level: "error",
|
||||
payload: {
|
||||
errorCode: input.error.code,
|
||||
errorMessage: input.error.message
|
||||
}
|
||||
});
|
||||
}
|
||||
this.writeIdempotency("failTask", input.idempotencyKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public getResults(): TaskRecord[] {
|
||||
return this.tasks.filter((item) => item.status === "DONE" || item.status === "ERROR");
|
||||
}
|
||||
|
||||
public getRunTrace(runId: string): TraceEvent[] {
|
||||
return this.traces.filter((item) => item.runId === runId);
|
||||
}
|
||||
|
||||
public health(): { ok: boolean; queueDepth: number; runsTotal: number } {
|
||||
return {
|
||||
ok: true,
|
||||
queueDepth: this.tasks.filter((item) => item.status === "QUEUED").length,
|
||||
runsTotal: this.runs.length
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { RunRecord, TaskRecord, TraceEvent } from "../types/accountingAgent";
|
||||
|
||||
export interface RuntimeAdapter {
|
||||
startRun(input: {
|
||||
sessionId?: string;
|
||||
initiator?: string;
|
||||
source?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
idempotencyKey?: string;
|
||||
}): RunRecord;
|
||||
finishRun(input: {
|
||||
runId: string;
|
||||
status: "DONE" | "ERROR" | "CANCELLED";
|
||||
source?: string;
|
||||
reason?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
idempotencyKey?: string;
|
||||
}): RunRecord;
|
||||
listRuns(): RunRecord[];
|
||||
getRun(runId: string): RunRecord | null;
|
||||
enqueueTask(input: {
|
||||
runId: string;
|
||||
payload: Record<string, unknown>;
|
||||
source?: string;
|
||||
idempotencyKey?: string;
|
||||
}): TaskRecord;
|
||||
claimTask(): TaskRecord | null;
|
||||
completeTask(input: {
|
||||
taskId: string;
|
||||
result: Record<string, unknown>;
|
||||
source?: string;
|
||||
idempotencyKey?: string;
|
||||
}): TaskRecord;
|
||||
failTask(input: {
|
||||
taskId: string;
|
||||
error: { code: string; message: string; details?: unknown };
|
||||
source?: string;
|
||||
idempotencyKey?: string;
|
||||
}): TaskRecord;
|
||||
getResults(): TaskRecord[];
|
||||
getRunTrace(runId: string): TraceEvent[];
|
||||
health(): { ok: boolean; queueDepth: number; runsTotal: number };
|
||||
}
|
||||
Reference in New Issue
Block a user