feat(agent): add controlled task execution
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -13,6 +13,7 @@ export type SessionUiStateRecord = {
|
||||
sessionId: string;
|
||||
isTitleManuallyEdited?: boolean;
|
||||
messages: unknown[];
|
||||
executionPlans?: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
type SessionUiStateContext = {
|
||||
|
||||
Reference in New Issue
Block a user