mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
fix(core): Record first_response on tool calls and tighten metric tests (no-changelog)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
53c340a4ce
commit
29fbcd03ad
|
|
@ -340,13 +340,14 @@ describe('executeResumableStream', () => {
|
|||
expect(onActivity).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('calls onResponseChunk for text, reasoning and tool starts, before publishing', async () => {
|
||||
it('calls onResponseChunk for text, reasoning and tool calls, before publishing', async () => {
|
||||
const eventBus = createEventBus();
|
||||
const order: string[] = [];
|
||||
eventBus.publish.mockImplementation((_threadId: string, event: { type: string }) => {
|
||||
order.push(event.type);
|
||||
});
|
||||
const onResponseChunk = vi.fn(() => {
|
||||
expect(eventBus.publish).not.toHaveBeenCalledWith(
|
||||
'thread-1',
|
||||
expect.objectContaining({ type: 'text-delta' }),
|
||||
);
|
||||
order.push('callback');
|
||||
});
|
||||
|
||||
await executeResumableStream({
|
||||
|
|
@ -357,6 +358,7 @@ describe('executeResumableStream', () => {
|
|||
{ type: 'start-step' },
|
||||
{ type: 'reasoning-delta', delta: 'Thinking' },
|
||||
{ type: 'tool-input-start', toolCallId: 'call-1', toolName: 'list_workflows' },
|
||||
{ type: 'tool-call', toolCallId: 'call-2', toolName: 'list_workflows', input: {} },
|
||||
textChunk('Hi'),
|
||||
]),
|
||||
},
|
||||
|
|
@ -372,7 +374,16 @@ describe('executeResumableStream', () => {
|
|||
control: { mode: 'manual' },
|
||||
});
|
||||
|
||||
expect(onResponseChunk).toHaveBeenCalledTimes(3);
|
||||
expect(order).toEqual([
|
||||
'callback',
|
||||
'reasoning-delta',
|
||||
'callback',
|
||||
'tool-input-start',
|
||||
'callback',
|
||||
'tool-call',
|
||||
'callback',
|
||||
'text-delta',
|
||||
]);
|
||||
});
|
||||
|
||||
it('assigns stable response IDs from native start-step chunks', async () => {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ const RESPONSE_CHUNK_TYPES: ReadonlyArray<InstanceAiEvent['type']> = [
|
|||
'text-delta',
|
||||
'reasoning-delta',
|
||||
'tool-input-start',
|
||||
// Some providers send a complete tool call without a `tool-input-start`.
|
||||
'tool-call',
|
||||
];
|
||||
|
||||
export interface ResumableStreamSource {
|
||||
|
|
@ -45,7 +47,7 @@ export interface ResumableStreamContext {
|
|||
signal: AbortSignal;
|
||||
logger: Logger;
|
||||
onActivity?: () => void;
|
||||
/** Called for each response chunk (text, reasoning, tool start), just before its event is published. */
|
||||
/** Called for each response chunk (text, reasoning, tool call), just before its event is published. */
|
||||
onResponseChunk?: () => void;
|
||||
/** Stop consuming after the current chunk has been mapped and published. */
|
||||
stopSignal?: () => OrchestratorRunStopSignal | undefined;
|
||||
|
|
|
|||
|
|
@ -2116,23 +2116,38 @@ describe('build-agent tool', () => {
|
|||
});
|
||||
|
||||
it('ORs carried configUpdated with the resumed pass when finishing', async () => {
|
||||
const { context, delegate } = makeContext();
|
||||
context.domainContext!.agentBuilderTarget = { agentId: 'agent-1', projectId: 'proj-1' };
|
||||
vi.mocked(delegate.findOpenSuspensions).mockResolvedValue([
|
||||
{ runId: 'builder-run-1', toolCallId: 'builder-call-1' },
|
||||
]);
|
||||
vi.mocked(delegate.resumeBuild).mockResolvedValue(fakeStream([], 'Done.'));
|
||||
vi.useFakeTimers({ now: 100_000 });
|
||||
try {
|
||||
const { context, delegate } = makeContext();
|
||||
context.domainContext!.agentBuilderTarget = { agentId: 'agent-1', projectId: 'proj-1' };
|
||||
vi.mocked(delegate.findOpenSuspensions).mockResolvedValue([
|
||||
{ runId: 'builder-run-1', toolCallId: 'builder-call-1' },
|
||||
]);
|
||||
vi.mocked(delegate.resumeBuild).mockResolvedValue(fakeStream([], 'Done.'));
|
||||
|
||||
const result = await runToolWithCtx(
|
||||
context,
|
||||
{ message: 'Build it', name: 'New Agent' },
|
||||
{
|
||||
resumeData: { approved: true },
|
||||
suspendPayload: suspendPayloadWithCheckpoint({ configUpdated: true }),
|
||||
},
|
||||
);
|
||||
const result = await runToolWithCtx(
|
||||
context,
|
||||
{ message: 'Build it', name: 'New Agent' },
|
||||
{
|
||||
resumeData: { approved: true },
|
||||
suspendPayload: suspendPayloadWithCheckpoint({
|
||||
configUpdated: true,
|
||||
suspendedAt: 70_000,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.configUpdated).toBe(true);
|
||||
expect(result.configUpdated).toBe(true);
|
||||
expect(emitAgentBuildMetrics).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
outcome: 'completed',
|
||||
configUpdated: true,
|
||||
userWaitMs: 30_000,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('reports carried configUpdated when the resumed pass errors', async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user