Bridge mission-linked task provenance into goal diagnostics. - derive task goal ids and goal records from the linked mission hierarchy with a task mission fallback - record provenanceGoalIds in goal-injection diagnostics, run-audit metadata, and executor task logs - add regression coverage and mission/diagnostics docs for derived task-to-goal provenance Files changed: docs/diagnostics.md | 5 +- docs/missions.md | 19 ++++ packages/core/src/__tests__/mission-store.test.ts | 123 +++++++++++++++++++++ packages/core/src/mission-store.ts | 55 ++++++++- packages/engine/src/__tests__/goal-injection-diagnostics-wiring.test.ts | 35 +++++- packages/engine/src/goal-injection-diagnostics.ts | 21 +++- 6 files changed, 247 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-5959 Fusion-Task-Lineage: 7c76ae98-37b1-421c-b477-a0147d034a4c
268 lines
8.9 KiB
TypeScript
268 lines
8.9 KiB
TypeScript
import type { Goal } from "@fusion/core";
|
|
import { buildGoalContextSection, type GoalInjectionResult } from "./goal-context-injector.js";
|
|
import type { TaskStore } from "@fusion/core";
|
|
import type { GoalAnchoringLane } from "./goal-anchoring-audit.js";
|
|
import { emitGoalAnchoringAudit } from "./goal-anchoring-audit.js";
|
|
import type { EngineRunContext } from "./run-audit.js";
|
|
import type { RunAuditor } from "./run-audit.js";
|
|
import { createLogger } from "./logger.js";
|
|
|
|
const diagnosticsLog = createLogger("goal-injection-diagnostics");
|
|
|
|
export type GoalInjectionOutcome = "applied" | "no-goals" | "disabled-or-failed";
|
|
|
|
export type GoalInjectionDisabledReason =
|
|
| "config-disabled"
|
|
| "store-unavailable"
|
|
| "list-failed"
|
|
| "injector-threw";
|
|
|
|
export interface GoalInjectionDiagnostic {
|
|
lane: GoalAnchoringLane;
|
|
outcome: GoalInjectionOutcome;
|
|
goalCount: number;
|
|
goalIds: string[];
|
|
provenanceGoalIds: string[];
|
|
truncated: boolean;
|
|
reason?: GoalInjectionDisabledReason;
|
|
errorClass?: string;
|
|
runId?: string;
|
|
agentId?: string;
|
|
taskId?: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface GoalInjectionDiagnosticInput extends Omit<GoalInjectionDiagnostic, "timestamp" | "provenanceGoalIds"> {
|
|
provenanceGoalIds?: string[];
|
|
store?: TaskStore;
|
|
runContext?: EngineRunContext | null;
|
|
}
|
|
|
|
export interface GoalInjectionClassification {
|
|
outcome: GoalInjectionOutcome;
|
|
goalCount: number;
|
|
goalIds: string[];
|
|
truncated: boolean;
|
|
reason?: GoalInjectionDisabledReason;
|
|
errorClass?: string;
|
|
}
|
|
|
|
export function classifyGoalInjectionResult(result: GoalInjectionResult): GoalInjectionClassification {
|
|
if (result.emittedGoalIds.length === 0) {
|
|
return {
|
|
outcome: "no-goals",
|
|
goalCount: 0,
|
|
goalIds: [],
|
|
truncated: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
outcome: "applied",
|
|
goalCount: result.emittedGoalIds.length,
|
|
goalIds: result.emittedGoalIds,
|
|
truncated: result.truncated !== null,
|
|
};
|
|
}
|
|
|
|
export function classifyGoalInjectionFailure(
|
|
reason: GoalInjectionDisabledReason,
|
|
error?: unknown,
|
|
): GoalInjectionClassification {
|
|
const resolvedErrorClass =
|
|
error && typeof error === "object" && "constructor" in error && typeof (error as { constructor?: { name?: unknown } }).constructor?.name === "string"
|
|
? (error as { constructor: { name: string } }).constructor.name
|
|
: undefined;
|
|
|
|
return {
|
|
outcome: "disabled-or-failed",
|
|
goalCount: 0,
|
|
goalIds: [],
|
|
truncated: false,
|
|
reason,
|
|
...(resolvedErrorClass ? { errorClass: resolvedErrorClass } : {}),
|
|
};
|
|
}
|
|
|
|
export interface GoalContextResolution {
|
|
goalContext: string;
|
|
classification: GoalInjectionClassification;
|
|
}
|
|
|
|
export interface ResolveAndEmitGoalContextInput {
|
|
lane: GoalAnchoringLane;
|
|
store: TaskStore;
|
|
audit: RunAuditor;
|
|
taskId?: string;
|
|
runContext?: EngineRunContext | null;
|
|
}
|
|
|
|
export interface ResolveGoalContextInput {
|
|
listActiveGoals?: () => Goal[];
|
|
injector?: (activeGoals: Goal[]) => GoalInjectionResult;
|
|
disabledReason?: GoalInjectionDisabledReason;
|
|
}
|
|
|
|
export function resolveGoalContextForDiagnostics(input: ResolveGoalContextInput): GoalContextResolution {
|
|
if (!input.listActiveGoals) {
|
|
return {
|
|
goalContext: "",
|
|
classification: classifyGoalInjectionFailure(input.disabledReason ?? "store-unavailable"),
|
|
};
|
|
}
|
|
|
|
try {
|
|
const activeGoals = input.listActiveGoals();
|
|
const injector = input.injector ?? ((goals: Goal[]) => buildGoalContextSection({ activeGoals: goals }));
|
|
try {
|
|
const injectionResult = injector(activeGoals);
|
|
return {
|
|
goalContext: injectionResult.text,
|
|
classification: classifyGoalInjectionResult(injectionResult),
|
|
};
|
|
} catch (injectorError) {
|
|
return {
|
|
goalContext: "",
|
|
classification: classifyGoalInjectionFailure("injector-threw", injectorError),
|
|
};
|
|
}
|
|
} catch (listError) {
|
|
return {
|
|
goalContext: "",
|
|
classification: classifyGoalInjectionFailure("list-failed", listError),
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function resolveAndEmitGoalContext(input: ResolveAndEmitGoalContextInput): Promise<GoalContextResolution> {
|
|
const resolution = resolveGoalContextForDiagnostics({
|
|
listActiveGoals:
|
|
typeof input.store.getGoalStore === "function"
|
|
? () => input.store.getGoalStore().listGoals({ status: "active" })
|
|
: undefined,
|
|
});
|
|
|
|
let provenanceGoalIds: string[] = [];
|
|
if (input.taskId && typeof input.store.getMissionStore === "function") {
|
|
try {
|
|
provenanceGoalIds = input.store.getMissionStore().listGoalIdsForTask(input.taskId);
|
|
} catch (error) {
|
|
diagnosticsLog.warn(
|
|
`failed to resolve goal provenance for task ${input.taskId} in ${input.lane}: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
await emitGoalAnchoringAudit(input.audit, {
|
|
lane: input.lane,
|
|
taskId: input.taskId,
|
|
goalsInjected: resolution.classification.goalCount,
|
|
goalIds: resolution.classification.goalIds,
|
|
truncated: resolution.classification.truncated,
|
|
reason: resolution.classification.outcome === "no-goals" ? "no-active-goals" : undefined,
|
|
});
|
|
|
|
await emitGoalInjectionDiagnostic({
|
|
lane: input.lane,
|
|
...resolution.classification,
|
|
provenanceGoalIds,
|
|
runId: input.runContext?.runId,
|
|
agentId: input.runContext?.agentId,
|
|
taskId: input.taskId,
|
|
store: input.store,
|
|
runContext: input.runContext,
|
|
});
|
|
|
|
return resolution;
|
|
}
|
|
|
|
function formatAgentLogLine(input: GoalInjectionDiagnostic): string {
|
|
const ids = JSON.stringify(input.goalIds);
|
|
const provenanceIds = JSON.stringify(input.provenanceGoalIds);
|
|
const reason = input.reason ? ` reason=${input.reason}` : "";
|
|
const errorClass = input.errorClass ? ` err=${input.errorClass}` : "";
|
|
return `[goal-injection] ${input.outcome} count=${input.goalCount} ids=${ids} provenance=${provenanceIds} truncated=${String(input.truncated)}${reason}${errorClass}`;
|
|
}
|
|
|
|
/**
|
|
* Emit per-run goal-context injection diagnostics for executor/heartbeat lanes.
|
|
*
|
|
* Outcomes:
|
|
* - `applied`: at least one active goal ID was injected.
|
|
* - `no-goals`: injector executed successfully but active goal set was empty.
|
|
* - `disabled-or-failed`: injection was disabled or list/injector execution failed.
|
|
*
|
|
* Guardrail: this emitter stores goal IDs/counts only and must never persist
|
|
* prompt body text, goal titles, or goal descriptions to avoid prompt/PII leakage.
|
|
*
|
|
* This helper is called from FN-5653 wiring sites (executor/heartbeat), not from
|
|
* inside the pure goal-context injector module.
|
|
*/
|
|
export async function emitGoalInjectionDiagnostic(
|
|
input: GoalInjectionDiagnosticInput,
|
|
): Promise<GoalInjectionDiagnostic> {
|
|
const record: GoalInjectionDiagnostic = {
|
|
lane: input.lane,
|
|
outcome: input.outcome,
|
|
goalCount: input.goalCount,
|
|
goalIds: [...input.goalIds],
|
|
provenanceGoalIds: [...(input.provenanceGoalIds ?? [])],
|
|
truncated: input.truncated,
|
|
...(input.reason ? { reason: input.reason } : {}),
|
|
...(input.errorClass ? { errorClass: input.errorClass } : {}),
|
|
...(input.runId ? { runId: input.runId } : {}),
|
|
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
if (input.store && record.taskId) {
|
|
try {
|
|
await input.store.logEntry(record.taskId, formatAgentLogLine(record), undefined, input.runContext ?? undefined);
|
|
} catch (error) {
|
|
diagnosticsLog.warn(
|
|
`failed to append goal-injection task log for ${record.taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const auditStore = input.store;
|
|
const hasAuditWriter = Boolean(auditStore && typeof auditStore.recordRunAuditEvent === "function");
|
|
if (!input.runContext || !hasAuditWriter || !auditStore) {
|
|
diagnosticsLog.warn(
|
|
`goal-injection diagnostic emitted without run-audit side effect (lane=${record.lane}, hasContext=${Boolean(input.runContext)}, hasWriter=${Boolean(hasAuditWriter)})`,
|
|
);
|
|
return record;
|
|
}
|
|
|
|
try {
|
|
await auditStore.recordRunAuditEvent({
|
|
taskId: input.runContext.taskId,
|
|
agentId: input.runContext.agentId,
|
|
runId: input.runContext.runId,
|
|
domain: "database",
|
|
mutationType: "prompt:goal-injection",
|
|
target: record.lane,
|
|
metadata: {
|
|
lane: record.lane,
|
|
outcome: record.outcome,
|
|
goalCount: record.goalCount,
|
|
goalIds: record.goalIds,
|
|
provenanceGoalIds: record.provenanceGoalIds,
|
|
truncated: record.truncated,
|
|
...(record.reason ? { reason: record.reason } : {}),
|
|
...(record.errorClass ? { errorClass: record.errorClass } : {}),
|
|
...(record.runId ? { runId: record.runId } : {}),
|
|
...(record.agentId ? { agentId: record.agentId } : {}),
|
|
...(record.taskId ? { taskId: record.taskId } : {}),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
diagnosticsLog.warn(
|
|
`failed to append goal-injection run-audit event for lane=${record.lane}: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
|
|
return record;
|
|
}
|