Skip to content

Commit 558a8e7

Browse files
committed
[One Workflow] Restore compact validation telemetry for generate_workflow
Closes elastic/security-team#17641. The old low-level edit tools (deleted in #269351) ran a compact validation pass against the generated YAML and surfaced the result via WorkflowsAiTelemetryClient.reportEditResult, which emits validation_passed, validation_error_count, and is_self_correction. The single-entry-point generate_workflow tool stopped doing that, leaving those telemetry fields permanently undefined and removing our ability to track generation-quality regressions. This commit reintroduces the helper, calls api.validateWorkflow against the stringified YAML after generation, and passes the result through to telemetry on the success path. The failure path (generation threw) still has no YAML to validate, so it keeps the existing behavior. Tests cover the valid case, the invalid case (with error-only diagnostic flattening), and the validator-throwing case. Spotted by @yiannisnikolopoulos on #269351 review.
1 parent 0778d0c commit 558a8e7

2 files changed

Lines changed: 108 additions & 2 deletions

File tree

x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/generate_workflow.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ jest.mock('@kbn/agent-builder-workflow-gen', () => ({
2121
const generateWorkflowMock = generateWorkflow as jest.MockedFunction<typeof generateWorkflow>;
2222

2323
describe('generateWorkflowTool', () => {
24+
const validateWorkflowMock = jest.fn().mockResolvedValue({ valid: true, diagnostics: [] });
25+
2426
const workflowsManagement = {
25-
management: { __mock: 'workflowsApi' },
27+
management: { validateWorkflow: validateWorkflowMock },
2628
} as any;
2729

2830
const aiTelemetryClient = {
@@ -73,6 +75,8 @@ describe('generateWorkflowTool', () => {
7375
beforeEach(() => {
7476
generateWorkflowMock.mockReset();
7577
aiTelemetryClient.reportEditResult.mockReset();
78+
validateWorkflowMock.mockReset();
79+
validateWorkflowMock.mockResolvedValue({ valid: true, diagnostics: [] });
7680
});
7781

7882
it('creates a new workflow: adds diff attachment, adds workflow attachment, sends UI event, reports telemetry', async () => {
@@ -245,6 +249,76 @@ describe('generateWorkflowTool', () => {
245249
);
246250
});
247251

252+
it('passes a compact validation result to telemetry when the generated YAML is valid', async () => {
253+
generateWorkflowMock.mockResolvedValueOnce({
254+
workflow: generatedWorkflow,
255+
response: 'created',
256+
} as any);
257+
validateWorkflowMock.mockResolvedValueOnce({ valid: true, diagnostics: [] });
258+
259+
const context = buildContext();
260+
const tool = generateWorkflowTool({ workflowsManagement, aiTelemetryClient });
261+
await tool.handler({ query: 'q' } as any, context);
262+
263+
expect(validateWorkflowMock).toHaveBeenCalledWith(
264+
expect.stringContaining('name: foo'),
265+
'default',
266+
expect.objectContaining({ __mock: 'request' })
267+
);
268+
expect(aiTelemetryClient.reportEditResult).toHaveBeenCalledWith(
269+
expect.objectContaining({
270+
editSuccess: true,
271+
isCreation: true,
272+
validation: { valid: true },
273+
})
274+
);
275+
});
276+
277+
it('passes compact errors to telemetry when the generated YAML fails validation', async () => {
278+
generateWorkflowMock.mockResolvedValueOnce({
279+
workflow: generatedWorkflow,
280+
response: 'created',
281+
} as any);
282+
validateWorkflowMock.mockResolvedValueOnce({
283+
valid: false,
284+
diagnostics: [
285+
{ severity: 'error', source: 'schema', message: 'missing field', path: ['steps', 0] },
286+
{ severity: 'warning', source: 'schema', message: 'soft warning' },
287+
{ severity: 'error', source: 'liquid', message: 'bad template' },
288+
],
289+
});
290+
291+
const context = buildContext();
292+
const tool = generateWorkflowTool({ workflowsManagement, aiTelemetryClient });
293+
await tool.handler({ query: 'q' } as any, context);
294+
295+
expect(aiTelemetryClient.reportEditResult).toHaveBeenCalledWith(
296+
expect.objectContaining({
297+
editSuccess: true,
298+
validation: {
299+
valid: false,
300+
errors: ['[schema] missing field (at steps.0)', '[liquid] bad template'],
301+
},
302+
})
303+
);
304+
});
305+
306+
it('omits validation in telemetry when validateWorkflow throws', async () => {
307+
generateWorkflowMock.mockResolvedValueOnce({
308+
workflow: generatedWorkflow,
309+
response: 'created',
310+
} as any);
311+
validateWorkflowMock.mockRejectedValueOnce(new Error('validator down'));
312+
313+
const context = buildContext();
314+
const tool = generateWorkflowTool({ workflowsManagement, aiTelemetryClient });
315+
await tool.handler({ query: 'q' } as any, context);
316+
317+
expect(aiTelemetryClient.reportEditResult).toHaveBeenCalledWith(
318+
expect.objectContaining({ editSuccess: true, validation: undefined })
319+
);
320+
});
321+
248322
it('returns an errorResult and reports failed telemetry when generateWorkflow throws', async () => {
249323
generateWorkflowMock.mockRejectedValueOnce(new Error('boom'));
250324

x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/generate_workflow.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,47 @@
77

88
import { v4 } from 'uuid';
99
import { z } from '@kbn/zod/v4';
10+
import type { KibanaRequest } from '@kbn/core/server';
1011
import { platformCoreTools, ToolType } from '@kbn/agent-builder-common';
1112
import type { BuiltinToolDefinition } from '@kbn/agent-builder-server';
1213
import { generateWorkflow, type GenerateWorkflowEdit } from '@kbn/agent-builder-workflow-gen';
1314
import { cleanPrompt } from '@kbn/agent-builder-genai-utils/prompts';
1415
import { errorResult, otherResult } from '@kbn/agent-builder-genai-utils/tools/utils/results';
15-
import type { WorkflowsServerPluginSetup } from '@kbn/workflows-management-plugin/server';
16+
import type {
17+
WorkflowsManagementApi,
18+
WorkflowsServerPluginSetup,
19+
} from '@kbn/workflows-management-plugin/server';
1620
import { workflowIdSchema } from '@kbn/workflows-management-plugin/common/lib/workflow_id_schema';
1721
import { WORKFLOW_YAML_ATTACHMENT_TYPE } from '@kbn/workflows/common/constants';
1822
import { stringifyWorkflowDefinition } from '@kbn/workflows-yaml';
1923
import type { WorkflowsAiTelemetryClient } from '../telemetry/workflows_ai_telemetry_client';
2024
import { emitWorkflowDiff, extractConversationId } from './utils/workflow_attachments';
2125

26+
interface CompactValidation {
27+
valid: boolean;
28+
errors?: string[];
29+
}
30+
31+
const runCompactValidation = async (
32+
yaml: string,
33+
api: WorkflowsManagementApi,
34+
spaceId: string,
35+
request: KibanaRequest
36+
): Promise<CompactValidation | undefined> => {
37+
try {
38+
const result = await api.validateWorkflow(yaml, spaceId, request);
39+
if (result.valid) {
40+
return { valid: true };
41+
}
42+
const errors = result.diagnostics
43+
.filter((d) => d.severity === 'error')
44+
.map((d) => `[${d.source}] ${d.message}${d.path ? ` (at ${d.path.join('.')})` : ''}`);
45+
return { valid: false, errors };
46+
} catch {
47+
return undefined;
48+
}
49+
};
50+
2251
const generateWorkflowSchema = z.object({
2352
query: z
2453
.string()
@@ -162,11 +191,14 @@ And you should **not**:
162191
toolId: platformCoreTools.generateWorkflow,
163192
});
164193

194+
const validation = await runCompactValidation(afterYaml, workflowsApi, spaceId, request);
195+
165196
aiTelemetryClient.reportEditResult({
166197
toolId: platformCoreTools.generateWorkflow,
167198
conversationId: extractConversationId(toolContext),
168199
editSuccess: true,
169200
isCreation: !sourceAttachment,
201+
validation,
170202
});
171203

172204
return {

0 commit comments

Comments
 (0)