Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions apps/operator-backend/src/brains/codex_brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { getSkillLibraryText } from "../skills/skill_library.js";
import { persistence } from "../persistence/persistence_manager.js";
import { retrieveMemoryContext } from "../memory/jsonl_memory_store.js";
import { formatProjectProfileForPrompt } from "../memory/project_profile.js";
import {
beginRequirementsPlanningLease,
endRequirementsPlanningLease,
formatRequirementsForPrompt,
resolveRequirementsForChat,
type RequirementsReceipt
} from "../memory/requirements_store.js";
import { getPinnedGoal } from "../session_store.js";
import { compactIncomingToolResult } from "../tool_result_compaction.js";
import { formatActiveGoalContext, getActiveGoalForSession } from "../goals/service.js";
Expand Down Expand Up @@ -363,11 +370,32 @@ export async function decideCodexStreaming(req: ChatRequest, cb: StreamCallbacks
const text = (req.user_text ?? "").toString();
let memBlock = "";
let projectProfileBlock = "";
let requirementsBlock = "";
let requirementsReceipt: RequirementsReceipt | null = null;
let requirementsError = "";
try {
projectProfileBlock = formatProjectProfileForPrompt();
} catch {
projectProfileBlock = "";
}
try {
requirementsReceipt = resolveRequirementsForChat(req);
requirementsBlock = formatRequirementsForPrompt(requirementsReceipt);
} catch (error) {
requirementsReceipt = null;
requirementsBlock = "";
requirementsError = error instanceof Error ? error.message : String(error);
}
if (requirementsError) {
const message = `Durable requirements could not be read safely (${requirementsError}). I stopped before planning or tool actions.`;
cb.onDone?.(message);
return { version: OPERATOR_BACKEND_CONTRACT_VERSION, assistant_message: message, actions: [] };
}
if (requirementsReceipt && requirementsReceipt.status !== "resolved") {
const message = `Durable requirements are ${requirementsReceipt.status}. I stopped before planning or tool actions; resolve or narrow the attached receipt first.`;
cb.onDone?.(message);
return { version: OPERATOR_BACKEND_CONTRACT_VERSION, assistant_message: message, actions: [], requirements_receipt: requirementsReceipt };
}
try {
const query = text.trim() || (getPinnedGoal(req.session_id) ?? "") || "";
const mem = query ? retrieveMemoryContext({ queryText: query, maxEntries: 6 }) : [];
Expand Down Expand Up @@ -397,6 +425,7 @@ export async function decideCodexStreaming(req: ChatRequest, cb: StreamCallbacks
const blocks: string[] = [];
if (activeGoalBlock) blocks.push(activeGoalBlock);
if (projectProfileBlock) blocks.push(projectProfileBlock);
if (requirementsBlock) blocks.push(requirementsBlock);
try {
blocks.push(formatEnvironmentSummaryForPrompt());
} catch {}
Expand All @@ -419,17 +448,50 @@ export async function decideCodexStreaming(req: ChatRequest, cb: StreamCallbacks
}
]
: // If the client sends an empty user_text (legacy tool-loop continuation), still nudge Codex.
[{ type: "text", text: activeGoalBlock ? `${activeGoalBlock}\n\n(continue)` : "(continue)", text_elements: [] as any[] }];
[{ type: "text", text: [activeGoalBlock, requirementsBlock, "(continue)"].filter(Boolean).join("\n\n"), text_elements: [] as any[] }];

const start = (await withTransportRetry(() =>
c.request("turn/start", {
threadId,
input
})
)) as any;
let requirementsLease: ReturnType<typeof beginRequirementsPlanningLease> | null = null;
if (requirementsReceipt) {
try {
const plannedReceiptSha256 = requirementsReceipt.receipt_sha256;
requirementsLease = beginRequirementsPlanningLease(plannedReceiptSha256);
const leasedReceipt = resolveRequirementsForChat(req);
if (leasedReceipt.status !== "resolved" || leasedReceipt.receipt_sha256 !== plannedReceiptSha256) {
endRequirementsPlanningLease(requirementsLease);
requirementsLease = null;
const message = leasedReceipt.status === "resolved"
? "Durable requirements changed while the planning lease was being acquired. I stopped before tool actions; re-run the request against the attached current receipt."
: `Durable requirements became ${leasedReceipt.status} while the planning lease was being acquired. I stopped before tool actions; resolve or narrow the attached receipt first.`;
cb.onDone?.(message);
return { version: OPERATOR_BACKEND_CONTRACT_VERSION, assistant_message: message, actions: [], requirements_receipt: leasedReceipt };
}
requirementsReceipt = leasedReceipt;
} catch (error) {
endRequirementsPlanningLease(requirementsLease);
requirementsLease = null;
const message = `Durable requirements could not be leased and revalidated safely (${error instanceof Error ? error.message : String(error)}). I stopped before planning or tool actions.`;
cb.onDone?.(message);
return { version: OPERATOR_BACKEND_CONTRACT_VERSION, assistant_message: message, actions: [], requirements_receipt: requirementsReceipt };
}
}
let start: any;
try {
start = (await withTransportRetry(() =>
c.request("turn/start", {
threadId,
input
})
)) as any;
} catch (error) {
endRequirementsPlanningLease(requirementsLease);
throw error;
}

const turnId = typeof start?.turn?.id === "string" ? start.turn.id : "";
if (!turnId) throw new Error("Codex turn/start did not return a turn id.");
if (!turnId) {
endRequirementsPlanningLease(requirementsLease);
throw new Error("Codex turn/start did not return a turn id.");
}
try {
appendEvent(req.session_id, "assistant", "codex.turn.start", { thread_id: threadId, turn_id: turnId });
} catch {
Expand Down Expand Up @@ -571,6 +633,7 @@ export async function decideCodexStreaming(req: ChatRequest, cb: StreamCallbacks
);
} finally {
unsubscribe();
endRequirementsPlanningLease(requirementsLease);
}

cb.onDone?.(assistantText);
Expand All @@ -587,6 +650,7 @@ export async function decideCodexStreaming(req: ChatRequest, cb: StreamCallbacks
return {
version: OPERATOR_BACKEND_CONTRACT_VERSION,
assistant_message: assistantText || "",
actions: []
actions: [],
...(requirementsReceipt && (requirementsReceipt.status !== "resolved" || requirementsReceipt.applied.length > 0) ? { requirements_receipt: requirementsReceipt } : {})
};
}
7 changes: 6 additions & 1 deletion apps/operator-backend/src/brains/openai_brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { appendEvent, appendNotification, getRecentStepToolResults } from "../me
import { persistence } from "../persistence/persistence_manager.js";
import { retrieveMemoryContext } from "../memory/jsonl_memory_store.js";
import { formatProjectProfileForPrompt } from "../memory/project_profile.js";
import { captureRequirementsResponseGuard, enforceRequirementsResponseGuard, formatRequirementsPromptBlockSafely } from "../memory/requirements_response_policy.js";
import { createOpenAiClient, resolveOpenAiApiKey } from "../openai_client.js";
import { executeWorkbenchActions, maxWorkbenchActions, type WorkbenchAction, type WorkbenchActionResult } from "../workbench/workbench_runner.js";
import { createArtifactShare } from "../artifacts/artifact_bus.js";
Expand Down Expand Up @@ -14904,7 +14905,7 @@ async function buildPrompt(req: ChatRequest, lane?: { route: SpeedRouteKind; rea
}

try {
const profileBlock = formatProjectProfileForPrompt();
const profileBlock = [formatProjectProfileForPrompt(), formatRequirementsPromptBlockSafely(req)].filter(Boolean).join("\n\n");
if (profileBlock) {
lines.push(profileBlock);
lines.push("");
Expand Down Expand Up @@ -16487,8 +16488,10 @@ export function __testOnlyFinalizeOpenAiResponseForRequest(req: ChatRequest, res
}

async function decideOpenAiInternal(req: ChatRequest, abortSignal?: AbortSignal): Promise<ChatResponse> {
const requirementsGuard = captureRequirementsResponseGuard(req);
const finishResponse = (response: ChatResponse): ChatResponse => {
response = __testOnlyFinalizeOpenAiResponseForRequest(req, response);
response = enforceRequirementsResponseGuard(req, response, requirementsGuard);
if (Array.isArray(response.actions) && response.actions.some((action) => typeof action?.path === "string" && action.path.startsWith("/revit/"))) {
const state = getRedlineFastPathState(req.session_id);
if (!state.phases.first_revit_action_emitted) {
Expand All @@ -16500,6 +16503,8 @@ async function decideOpenAiInternal(req: ChatRequest, abortSignal?: AbortSignal)
return response;
};

if (requirementsGuard.blocker) return finishResponse(requirementsGuard.blocker);

if (isRedlineFocusedTurn(req)) {
noteRedlineFastPathPhase(req.session_id, "request_accepted");
if (
Expand Down
35 changes: 35 additions & 0 deletions apps/operator-backend/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,39 @@ export type ChatResponse = {
bounded: true;
broadened: false;
};
requirements_receipt?: {
schema: "revit-operator.requirements-receipt.v1";
generated_at: string;
status: "resolved" | "conflict" | "overflow";
query: string;
scope_refs: Array<{ kind: "office" | "engineer" | "project" | "client"; id: string }>;
applied: Array<{
requirement_id: string;
revision: number;
scope: { kind: "office" | "engineer" | "project" | "client"; id: string };
key: string;
text: string;
reason: "highest_precedence" | "duplicate" | "lower_precedence" | "superseded";
}>;
suppressed: Array<{
requirement_id: string;
revision: number;
scope: { kind: "office" | "engineer" | "project" | "client"; id: string };
key: string;
text: string;
reason: "highest_precedence" | "duplicate" | "lower_precedence" | "superseded";
}>;
conflicts: Array<{
key: string;
precedence: number;
requirements: Array<{
requirement_id: string;
revision: number;
scope: { kind: "office" | "engineer" | "project" | "client"; id: string };
text: string;
}>;
}>;
overflow: { applied_count: number; suppressed_count: number; conflict_count: number; max_results: number } | null;
receipt_sha256: string;
};
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ChatRequest, ChatResponse, ToolResult } from "../contracts.js";
import type { AecSemanticTaskV1 } from "../aec_semantic_task.js";
import { appendGoalProgress, getActiveGoalForSession, updateGoal } from "../goals/service.js";
import { resolveRequirements, resolveRequirementsForChat, type RequirementsReceipt } from "../memory/requirements_store.js";

type ResolvedView = { id: number; name: string; levelName: string | null };
type Inventory = { view: ResolvedView; elementIds: number[]; inventoryActionId: string };
Expand All @@ -12,20 +13,21 @@ type RuntimeState = {
inventories: Inventory[];
summaries: Map<number, Array<Record<string, unknown>>>;
evidenceActionIds: string[];
requirementsReceipt: RequirementsReceipt | null;
};

const states = new Map<string, RuntimeState>();
const POWER_INSPECTION_CATEGORIES = ["OST_ElectricalFixtures", "OST_ElectricalEquipment", "OST_Wire"] as const;
const MAX_PILOT_VIEWS = 2;
const MAX_ELEMENTS_PER_VIEW = 500;

function response(message: string, actions: ChatResponse["actions"] = []): ChatResponse {
return { version: "operator.backend.v1", assistant_message: message, actions };
function response(message: string, actions: ChatResponse["actions"] = [], requirementsReceipt: RequirementsReceipt | null = null): ChatResponse {
return { version: "operator.backend.v1", assistant_message: message, actions, ...(requirementsReceipt ? { requirements_receipt: requirementsReceipt } : {}) };
}

function terminal(message: string, status: "complete" | "failed"): ChatResponse {
function terminal(message: string, status: "complete" | "failed", requirementsReceipt: RequirementsReceipt | null = null): ChatResponse {
return {
...response(message),
...response(message, [], requirementsReceipt),
aec_query_receipt: {
schema: "revit-operator.aec-query-receipt.v1",
terminal: true,
Expand Down Expand Up @@ -137,7 +139,7 @@ function block(req: ChatRequest, state: RuntimeState, message: string, results:
}))
});
states.delete(req.session_id);
return terminal(`${message} I made no model changes and did not widen the verified Level scope.`, "failed");
return terminal(`${message} I made no model changes and did not widen the verified Level scope.`, "failed", state.requirementsReceipt);
}

function finish(req: ChatRequest, state: RuntimeState, frameResults: ToolResult[]): ChatResponse {
Expand Down Expand Up @@ -206,10 +208,18 @@ function finish(req: ChatRequest, state: RuntimeState, frameResults: ToolResult[
if (goal) updateGoal(goal.id, { current_phase: "precedent_resolution", current_step: "Resolve exact current-project or office-standard power-plan precedent" });
states.delete(req.session_id);
const details = findings.map(({ inventory, summary }) => `${inventory.view.name} (${inventory.view.id}): ${summary}`).join(" ");
return terminal(`I completed the bounded Level power-plan pilot inspection. ${details} I preserved the per-view evidence and next action—resolve an exact precedent before any dry-run or model write.`, "complete");
return terminal(`I completed the bounded Level power-plan pilot inspection. ${details} I preserved the per-view evidence and next action—resolve an exact precedent before any dry-run or model write.`, "complete", state.requirementsReceipt);
}

function continueRuntime(req: ChatRequest, state: RuntimeState): ChatResponse {
try {
const currentReceipt = resolveRequirements({ scope_refs: state.requirementsReceipt?.scope_refs ?? [], query: state.requirementsReceipt?.query ?? "", max_results: 40 });
if (currentReceipt.status !== "resolved" || currentReceipt.receipt_sha256 !== state.requirementsReceipt?.receipt_sha256) {
return block(req, { ...state, requirementsReceipt: currentReceipt }, "Durable requirements changed or became blocked after this power-plan envelope was created; re-plan from the current receipt.", []);
}
} catch (error) {
return block(req, state, `Durable requirements could not be revalidated safely (${error instanceof Error ? error.message : String(error)}).`, []);
}
const results = matchingResults(req, state.actionIds);
if (results.length !== state.actionIds.length) return block(req, state, "One or more bounded power-plan inspection actions are missing.", results);

Expand All @@ -221,10 +231,10 @@ function continueRuntime(req: ChatRequest, state: RuntimeState): ChatResponse {
if (actions.length === 0) {
const frames = frameActions(state.views);
states.set(req.session_id, { ...state, stage: "frame", actionIds: frames.map(action => action.action_id), inventories: accepted, evidenceActionIds: [...state.evidenceActionIds, ...results.map(result => result.action_id)] });
return response("The exact power views are empty for the bounded inspection categories. I am exporting focused before-state frames to preserve that finding.", frames);
return response("The exact power views are empty for the bounded inspection categories. I am exporting focused before-state frames to preserve that finding.", frames, state.requirementsReceipt);
}
states.set(req.session_id, { ...state, stage: "summary", actionIds: actions.map(action => action.action_id), inventories: accepted, evidenceActionIds: [...state.evidenceActionIds, ...results.map(result => result.action_id)] });
return response("I completed exact per-view power inventories and am reading back only those bounded element IDs before visual inspection.", actions);
return response("I completed exact per-view power inventories and am reading back only those bounded element IDs before visual inspection.", actions, state.requirementsReceipt);
}

if (state.stage === "summary") {
Expand All @@ -240,26 +250,42 @@ function continueRuntime(req: ChatRequest, state: RuntimeState): ChatResponse {
}
const frames = frameActions(state.views);
states.set(req.session_id, { ...state, stage: "frame", actionIds: frames.map(action => action.action_id), summaries, evidenceActionIds: [...state.evidenceActionIds, ...results.map(result => result.action_id)] });
return response("The bounded IDs read back exactly. I am exporting one focused visual frame per resolved view before recording findings and next actions.", frames);
return response("The bounded IDs read back exactly. I am exporting one focused visual frame per resolved view before recording findings and next actions.", frames, state.requirementsReceipt);
}

return finish(req, state, results);
}

export function startAecLevelPowerPlanPilot(req: ChatRequest, task: AecSemanticTaskV1, views: ResolvedView[]): ChatResponse | null {
if (!isPowerPlanTask(task)) return null;
const requirementsReceipt = resolveRequirementsForChat(req);
if (requirementsReceipt.status !== "resolved") {
appendGoalProgress(req.session_id, {
summary: `Durable requirements ${requirementsReceipt.status} for the bounded Level power-plan workflow; planning stopped before Revit actions.`,
work_item: { id: "requirements.resolve", title: "Resolve durable requirements", status: "blocked", blocker: `requirements receipt ${requirementsReceipt.receipt_sha256}` }
});
return terminal(`Durable requirements are ${requirementsReceipt.status} for this Level power-plan request. I stopped before Revit actions; resolve or narrow the attached receipt.`, "failed", requirementsReceipt);
}
if (views.length === 0 || views.length > MAX_PILOT_VIEWS) {
const blocker = views.length === 0 ? "No exact power-plan view was resolved." : `The pilot supports at most ${MAX_PILOT_VIEWS} exact power-plan views, but ${views.length} were resolved.`;
appendGoalProgress(req.session_id, { summary: blocker, work_item: { id: "scope.inspect", title: "Inspect current model and view state in the resolved scope", status: "blocked", blocker } });
return terminal(`${blocker} I stopped without model changes rather than silently sampling or widening the scope.`, "failed");
return terminal(`${blocker} I stopped without model changes rather than silently sampling or widening the scope.`, "failed", requirementsReceipt);
}
const actions = inventoryActions(views);
states.set(req.session_id, { task, views, stage: "inventory", actionIds: actions.map(action => action.action_id), inventories: [], summaries: new Map(), evidenceActionIds: [] });
states.set(req.session_id, { task, views, stage: "inventory", actionIds: actions.map(action => action.action_id), inventories: [], summaries: new Map(), evidenceActionIds: [], requirementsReceipt });
appendGoalProgress(req.session_id, {
summary: `Starting a bounded read-only power-plan pilot in ${views.length} exact view(s).`,
assumptions: [{ id: "power.no_implicit_write", statement: "The pilot inspects and records exact per-view evidence before any design proposal; no broad write scope is inferred.", status: "accepted", basis: "underspecified Level power-plan request" }]
assumptions: [
{ id: "power.no_implicit_write", statement: "The pilot inspects and records exact per-view evidence before any design proposal; no broad write scope is inferred.", status: "accepted", basis: "underspecified Level power-plan request" },
{
id: "requirements.receipt",
statement: `Durable requirements receipt ${requirementsReceipt.receipt_sha256}; applied ${requirementsReceipt.applied.map(row => `${row.requirement_id}@${row.revision}`).join(", ") || "none"}.`,
status: "accepted",
basis: "deterministic scoped requirements resolution"
}
]
});
return response(`I resolved ${views.length} exact power-plan view(s). I am now inspecting only bounded electrical fixtures, equipment, and wires in those views; no model write is planned.`, actions);
return response(`I resolved ${views.length} exact power-plan view(s). I am now inspecting only bounded electrical fixtures, equipment, and wires in those views; no model write is planned.`, actions, requirementsReceipt);
}

export function maybeContinueAecLevelPowerPlanPilot(req: ChatRequest): ChatResponse | null {
Expand Down
Loading
Loading