feat(agent): add controlled task execution

This commit is contained in:
2026-09-24 04:19:02 +08:00
parent d018bcae67
commit b31033e9ce
48 changed files with 14498 additions and 86 deletions
+83 -2
View File
@@ -11,6 +11,7 @@ import {
} from "../utils/fileStore.js";
export type SessionStatus = "active" | "archived";
export type SessionPurpose = "ordinary_chat" | "task_execution" | "task_collaboration";
export type SessionRecord = {
sessionId: string;
@@ -22,7 +23,27 @@ export type SessionRecord = {
createdAt: string;
updatedAt: string;
status: SessionStatus;
purpose?: SessionPurpose;
taskId?: string;
runId?: string;
requestId?: string;
modelId?: string;
title?: string;
triggerId?: string;
triggerSource?: string;
triggerSubjectType?: string;
triggerSubjectId?: string;
triggerWorkflow?: string;
triggerOccurredAt?: string;
};
export type SessionTriggerMetadata = {
triggerId: string;
source: string;
subjectType: string;
subjectId: string;
workflow: string;
occurredAt: string;
};
type SessionMetadataContext = {
@@ -35,6 +56,12 @@ type SessionMetadataContext = {
type EnsureSessionMetadataInput = SessionMetadataContext & {
sessionId: string;
parentSessionId?: string;
trigger?: SessionTriggerMetadata;
purpose?: SessionPurpose;
taskId?: string;
runId?: string;
requestId?: string;
modelId?: string;
};
export class SessionMetadataStore {
@@ -53,7 +80,15 @@ export class SessionMetadataStore {
this.filePath(sessionId),
);
if (existing) {
if (!matchesContext(existing, input)) {
if (
!matchesContext(existing, input) ||
(input.trigger && existing.triggerId !== input.trigger.triggerId) ||
(input.purpose && readPurpose(existing) !== input.purpose) ||
(input.taskId && existing.taskId !== input.taskId.trim()) ||
(input.runId && existing.runId !== input.runId.trim()) ||
(input.requestId && existing.requestId !== input.requestId.trim()) ||
(input.modelId && existing.modelId !== input.modelId.trim())
) {
throw new SessionContextMismatchError();
}
return { created: false, record: existing };
@@ -70,6 +105,19 @@ export class SessionMetadataStore {
createdAt: now,
updatedAt: now,
status: "active",
purpose: input.purpose ?? (input.trigger ? "task_execution" : "ordinary_chat"),
taskId: input.taskId?.trim() || input.trigger?.subjectId,
runId: input.runId?.trim(),
requestId: input.requestId?.trim(),
modelId: input.modelId?.trim(),
...(input.trigger ? {
triggerId: input.trigger.triggerId,
triggerSource: input.trigger.source,
triggerSubjectType: input.trigger.subjectType,
triggerSubjectId: input.trigger.subjectId,
triggerWorkflow: input.trigger.workflow,
triggerOccurredAt: input.trigger.occurredAt,
} : {}),
};
await atomicWriteJson(
this.filePath(record.sessionId),
@@ -105,7 +153,10 @@ export class SessionMetadataStore {
return next;
}
async list(context: SessionMetadataContext) {
async list(
context: SessionMetadataContext,
filter?: { purpose?: SessionPurpose; taskId?: string },
) {
const files = await listJsonFiles(this.baseDir);
const records = await Promise.all(
files.map((file) => readJsonFile<SessionRecord>(file)),
@@ -117,9 +168,36 @@ export class SessionMetadataStore {
record.actorKey === context.actorKey &&
record.projectKey === context.projectKey,
)
.filter((record) => !filter?.purpose || readPurpose(record) === filter.purpose)
.filter((record) => !filter?.taskId || record.taskId === filter.taskId.trim())
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
}
async findByTask(
context: SessionMetadataContext,
taskId: string,
purpose: SessionPurpose,
) {
const records = await this.list(context, { purpose, taskId });
return records[0] ?? null;
}
async findByTrigger(context: SessionMetadataContext, triggerId: string) {
const normalizedTriggerId = triggerId.trim();
if (!normalizedTriggerId) {
return null;
}
const records = await this.list(context);
return records.find((record) => record.triggerId === normalizedTriggerId) ?? null;
}
async findByRequest(context: SessionMetadataContext, requestId: string) {
const normalized = requestId.trim();
if (!normalized) return null;
const records = await this.list(context);
return records.find((record) => record.requestId === normalized) ?? null;
}
async remove(record: SessionRecord) {
await removeFileIfExists(
this.filePath(record.sessionId),
@@ -136,6 +214,9 @@ const normalizeSessionId = (value?: string) => {
return normalized ? normalized.slice(0, 128) : undefined;
};
export const readPurpose = (record: SessionRecord): SessionPurpose =>
record.purpose ?? (record.triggerId ? "task_execution" : "ordinary_chat");
export class SessionContextMismatchError extends Error {
constructor() {
super("session not found");