Add core support for tasks to carry a planner oversight override that can supersede workflow settings. - Add nullable plannerOversightLevel task storage, schema migration, store update/create/archive plumbing, and mesh replication support. - Export planner oversight level types/defaults and an effective-level resolver with task-over-workflow precedence. - Document override precedence and add regression coverage for migration, persistence, updates, and resolution. - Add a minor changeset for the published Fusion package. Files changed: .../fn-7509-per-task-planner-oversight-override.md | 7 ++ docs/settings-reference.md | 2 +- packages/core/src/__tests__/db.test.ts | 45 +++++++++++++ packages/core/src/__tests__/store-update.test.ts | 75 ++++++++++++++++++++++ .../__tests__/workflow-settings-resolver.test.ts | 28 ++++++++ packages/core/src/db.ts | 17 ++++- packages/core/src/index.ts | 5 +- packages/core/src/mesh-task-replication.ts | 2 + packages/core/src/store.ts | 15 ++++- packages/core/src/types.ts | 23 +++++++ packages/core/src/workflow-settings-resolver.ts | 28 ++++++++ 11 files changed, 240 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7509 Fusion-Task-Lineage: 41695cc5-34d2-4079-9e34-a8fb40f961fb Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
172 lines
6.0 KiB
TypeScript
172 lines
6.0 KiB
TypeScript
import type { MeshReplicatedTaskCreatePayload, Task, TaskCreateInput, TaskDetail, TaskSource } from "./types.js";
|
|
|
|
export function buildBootstrapPrompt(taskId: string, title: string | undefined, description: string): string {
|
|
const heading = title ? `${taskId}: ${title}` : taskId;
|
|
return `# ${heading}\n\n${description}\n`;
|
|
}
|
|
|
|
export function buildMeshReplicatedTaskCreatePayload(input: {
|
|
taskId: string;
|
|
reservationId: string;
|
|
sourceNodeId: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
prompt: string;
|
|
createInput: TaskCreateInput;
|
|
}): MeshReplicatedTaskCreatePayload {
|
|
return {
|
|
replicationVersion: 1,
|
|
reservationId: input.reservationId,
|
|
taskId: input.taskId,
|
|
sourceNodeId: input.sourceNodeId,
|
|
createdAt: input.createdAt,
|
|
updatedAt: input.updatedAt,
|
|
prompt: input.prompt,
|
|
input: input.createInput,
|
|
};
|
|
}
|
|
|
|
function pruneUndefined<T>(value: T): T {
|
|
if (Array.isArray(value)) {
|
|
return value.map((entry) => pruneUndefined(entry)) as T;
|
|
}
|
|
if (value && typeof value === "object") {
|
|
const entries = Object.entries(value as Record<string, unknown>)
|
|
.filter(([, entry]) => entry !== undefined)
|
|
.map(([key, entry]) => [key, pruneUndefined(entry)]);
|
|
return Object.fromEntries(entries) as T;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeCreateInput(input: TaskCreateInput): TaskCreateInput {
|
|
const source = input.source;
|
|
return pruneUndefined({
|
|
...input,
|
|
column: input.column ?? "triage",
|
|
source: source
|
|
? {
|
|
...source,
|
|
sourceType: source.sourceType ?? "unknown",
|
|
}
|
|
: { sourceType: "unknown" as const },
|
|
dependencies: input.dependencies ?? [],
|
|
enabledWorkflowSteps: input.enabledWorkflowSteps ?? [],
|
|
});
|
|
}
|
|
|
|
function toTaskSource(source: Omit<TaskSource, "sourceType"> & { sourceType?: TaskSource["sourceType"] }): TaskSource {
|
|
return {
|
|
...source,
|
|
sourceType: source.sourceType ?? "unknown",
|
|
};
|
|
}
|
|
|
|
function isSubsetEqual(expected: unknown, actual: unknown): boolean {
|
|
if (Array.isArray(expected)) {
|
|
return Array.isArray(actual)
|
|
&& expected.length === actual.length
|
|
&& expected.every((entry, index) => isSubsetEqual(entry, actual[index]));
|
|
}
|
|
if (expected && typeof expected === "object") {
|
|
if (!actual || typeof actual !== "object") return false;
|
|
const expectedRecord = expected as Record<string, unknown>;
|
|
const actualRecord = actual as Record<string, unknown>;
|
|
return Object.entries(expectedRecord).every(([key, value]) => isSubsetEqual(value, actualRecord[key]));
|
|
}
|
|
return Object.is(expected, actual);
|
|
}
|
|
|
|
export function taskMatchesReplicatedCreate(existing: TaskDetail, payload: MeshReplicatedTaskCreatePayload): boolean {
|
|
const existingPrompt = existing.prompt;
|
|
const existingCreateInput: TaskCreateInput = {
|
|
title: existing.title,
|
|
description: existing.description,
|
|
column: existing.column,
|
|
dependencies: existing.dependencies,
|
|
breakIntoSubtasks: existing.breakIntoSubtasks,
|
|
enabledWorkflowSteps: existing.enabledWorkflowSteps,
|
|
modelPresetId: existing.modelPresetId,
|
|
modelProvider: existing.modelProvider,
|
|
modelId: existing.modelId,
|
|
validatorModelProvider: existing.validatorModelProvider,
|
|
validatorModelId: existing.validatorModelId,
|
|
planningModelProvider: existing.planningModelProvider,
|
|
planningModelId: existing.planningModelId,
|
|
thinkingLevel: existing.thinkingLevel,
|
|
missionId: existing.missionId,
|
|
sliceId: existing.sliceId,
|
|
assignedAgentId: existing.assignedAgentId,
|
|
nodeId: existing.nodeId,
|
|
assigneeUserId: existing.assigneeUserId,
|
|
reviewLevel: existing.reviewLevel,
|
|
executionMode: existing.executionMode,
|
|
plannerOversightLevel: existing.plannerOversightLevel,
|
|
priority: existing.priority,
|
|
sourceIssue: existing.sourceIssue,
|
|
source: toTaskSource({
|
|
sourceType: existing.sourceType,
|
|
sourceAgentId: existing.sourceAgentId,
|
|
sourceRunId: existing.sourceRunId,
|
|
sourceSessionId: existing.sourceSessionId,
|
|
sourceMessageId: existing.sourceMessageId,
|
|
sourceParentTaskId: existing.sourceParentTaskId,
|
|
sourceMetadata: existing.sourceMetadata,
|
|
}),
|
|
baseBranch: existing.baseBranch,
|
|
branch: existing.branch,
|
|
};
|
|
|
|
return (
|
|
existing.id === payload.taskId &&
|
|
existing.createdAt === payload.createdAt &&
|
|
existing.updatedAt === payload.updatedAt &&
|
|
existingPrompt === payload.prompt &&
|
|
isSubsetEqual(normalizeCreateInput(payload.input), normalizeCreateInput(existingCreateInput))
|
|
);
|
|
}
|
|
|
|
export function replicationCollisionError(taskId: string): Error {
|
|
return new Error(`Replicated task payload collision for existing task ${taskId}`);
|
|
}
|
|
|
|
export function toReplicatedCreateInput(task: Task): TaskCreateInput {
|
|
return {
|
|
title: task.title,
|
|
description: task.description,
|
|
column: task.column,
|
|
dependencies: task.dependencies,
|
|
breakIntoSubtasks: task.breakIntoSubtasks,
|
|
enabledWorkflowSteps: task.enabledWorkflowSteps,
|
|
modelPresetId: task.modelPresetId,
|
|
modelProvider: task.modelProvider,
|
|
modelId: task.modelId,
|
|
validatorModelProvider: task.validatorModelProvider,
|
|
validatorModelId: task.validatorModelId,
|
|
planningModelProvider: task.planningModelProvider,
|
|
planningModelId: task.planningModelId,
|
|
thinkingLevel: task.thinkingLevel,
|
|
missionId: task.missionId,
|
|
sliceId: task.sliceId,
|
|
assignedAgentId: task.assignedAgentId,
|
|
nodeId: task.nodeId,
|
|
assigneeUserId: task.assigneeUserId,
|
|
reviewLevel: task.reviewLevel,
|
|
executionMode: task.executionMode,
|
|
plannerOversightLevel: task.plannerOversightLevel,
|
|
priority: task.priority,
|
|
sourceIssue: task.sourceIssue,
|
|
source: toTaskSource({
|
|
sourceType: task.sourceType,
|
|
sourceAgentId: task.sourceAgentId,
|
|
sourceRunId: task.sourceRunId,
|
|
sourceSessionId: task.sourceSessionId,
|
|
sourceMessageId: task.sourceMessageId,
|
|
sourceParentTaskId: task.sourceParentTaskId,
|
|
sourceMetadata: task.sourceMetadata,
|
|
}),
|
|
baseBranch: task.baseBranch,
|
|
branch: task.branch,
|
|
};
|
|
}
|