Skip to content
Draft
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
19 changes: 15 additions & 4 deletions client/src/contexts/WorkflowContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export const WorkflowContext = createContext(null);

const PENDING_RUNTIME_ACTION_KEY = "pytc.workflow.pendingRuntimeAction.v1";
const PENDING_RUNTIME_ACTION_TTL_MS = 6 * 60 * 60 * 1000;
const PERSISTABLE_RUNTIME_KINDS = new Set(["monitor_training"]);
const PERSISTABLE_RUNTIME_KINDS = new Set([
"monitor_training",
"monitor_inference",
]);

const isPersistableRuntimeAction = (kind) =>
PERSISTABLE_RUNTIME_KINDS.has(kind);
Expand Down Expand Up @@ -955,11 +958,19 @@ export function WorkflowProvider({ children }) {
workflow.id,
durableCommand.id,
);
if ((approvedEffects?.runtime_action || {}).kind === "start_training") {
const runtimeKind = (approvedEffects?.runtime_action || {}).kind;
if (
runtimeKind === "start_training" ||
runtimeKind === "start_inference"
) {
const monitorKind =
runtimeKind === "start_training"
? "monitor_training"
: "monitor_inference";
registerPendingRuntimeAction(
{
id: `monitor_training:${Date.now()}`,
kind: "monitor_training",
id: `${monitorKind}:${Date.now()}`,
kind: monitorKind,
commandId: durableCommand.id,
commandResult,
clientEffects: approvedEffects,
Expand Down
41 changes: 41 additions & 0 deletions client/src/contexts/WorkflowContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,47 @@ describe("WorkflowProvider", () => {
});
});

it("monitors an approved durable inference command without queuing a browser launch", async () => {
approveAgentAction.mockResolvedValue({
workflow: { ...baseWorkflow, stage: "inference" },
client_effects: {
navigate_to: "inference",
set_inference_image_path: "/tmp/image.h5",
set_inference_checkpoint_path: "/tmp/checkpoint.pth.tar",
set_inference_output_path: "/tmp/inference-output",
runtime_action: { kind: "start_inference" },
},
commands: [
{
id: 23,
title: "Start inference",
command: "pytc inference",
},
],
});

renderProvider({
inferenceState: {
setInputImage: jest.fn(),
setCheckpointPath: jest.fn(),
setOutputPath: jest.fn(),
},
});
await screen.findByText("setup");

fireEvent.click(screen.getByText("Approve proposal"));

await waitFor(() => {
expect(runWorkflowCommand).toHaveBeenCalledWith(1, 23);
expect(screen.getByText("monitor_inference")).toBeTruthy();
});
const persisted = JSON.parse(
window.sessionStorage.getItem("pytc.workflow.pendingRuntimeAction.v1"),
);
expect(persisted?.action?.kind).toBe("monitor_inference");
expect(persisted?.action?.commandId).toBe(23);
});

it("exposes direct client-effect execution for chat action cards", async () => {
const setOutputPath = jest.fn();

Expand Down
10 changes: 10 additions & 0 deletions client/src/views/ModelInference.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ function ModelInference({ isInferring, setIsInferring }) {
startInferenceRun(action);
}, [consumeRuntimeAction, pendingRuntimeAction, startInferenceRun]);

useEffect(() => {
if (pendingRuntimeAction?.kind !== "monitor_inference") return;
const action = pendingRuntimeAction;
consumeRuntimeAction?.(action.id);
terminalLoggedRef.current = false;
setIsInferring(true);
setInferenceStatus("Model run accepted. Monitoring process...");
refreshInferenceLogs();
}, [consumeRuntimeAction, pendingRuntimeAction, setIsInferring]);

const handleStartButton = async () => {
await startInferenceRun();
};
Expand Down
43 changes: 33 additions & 10 deletions client/src/views/ModelInference.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ import {
getInferenceStatus,
syncWorkflowInferenceRuntime,
} from "../api";
import { launchInferenceFromContext } from "../runtime/modelLaunch";

const mockAppendWorkflowEvent = jest.fn();
const mockRefreshWorkflow = jest.fn();
const mockRefreshEvents = jest.fn();
const mockRefreshInsights = jest.fn();
const mockRefreshEvidence = jest.fn();
const mockConsumeRuntimeAction = jest.fn();
const mockWorkflowContext = {
workflow: { id: 42, stage: "inference" },
appendEvent: mockAppendWorkflowEvent,
refreshWorkflow: mockRefreshWorkflow,
refreshEvents: mockRefreshEvents,
refreshInsights: mockRefreshInsights,
refreshEvidence: mockRefreshEvidence,
pendingRuntimeAction: null,
consumeRuntimeAction: mockConsumeRuntimeAction,
};

jest.mock("../api", () => ({
getInferenceLogs: jest.fn(),
Expand All @@ -28,16 +40,7 @@ jest.mock("../runtime/modelLaunch", () => ({
}));

jest.mock("../contexts/WorkflowContext", () => ({
useWorkflow: () => ({
workflow: { id: 42, stage: "inference" },
appendEvent: mockAppendWorkflowEvent,
refreshWorkflow: mockRefreshWorkflow,
refreshEvents: mockRefreshEvents,
refreshInsights: mockRefreshInsights,
refreshEvidence: mockRefreshEvidence,
pendingRuntimeAction: null,
consumeRuntimeAction: jest.fn(),
}),
useWorkflow: () => mockWorkflowContext,
}));

jest.mock("../components/Configurator", () => () => <div>Configurator</div>);
Expand Down Expand Up @@ -70,6 +73,7 @@ describe("ModelInference", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.clearAllMocks();
mockWorkflowContext.pendingRuntimeAction = null;
getInferenceLogs.mockResolvedValue({ phase: "finished", metadata: {} });
getInferenceStatus.mockResolvedValue({
isRunning: false,
Expand Down Expand Up @@ -135,4 +139,23 @@ describe("ModelInference", () => {
);
});
});

it("monitors an accepted durable inference command without launching it again", async () => {
mockWorkflowContext.pendingRuntimeAction = {
id: "monitor_inference:23",
kind: "monitor_inference",
commandId: 23,
};
const { setIsInferring } = renderInference({ isInferring: false });

await act(async () => {
await Promise.resolve();
});
expect(mockConsumeRuntimeAction).toHaveBeenCalledWith(
"monitor_inference:23",
);
expect(setIsInferring).toHaveBeenCalledWith(true);
expect(getInferenceLogs).toHaveBeenCalled();
expect(launchInferenceFromContext).not.toHaveBeenCalled();
});
});
Loading